Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .starters/default/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,13 @@ logs
.env
.env.*
!.env.example

# Agent skills (external, installed at runtime)
.agents/skills
.cursor/
.trae/
.windsurf/

# Claude Code local overrides
CLAUDE.local.md
.claude/settings.local.json
21 changes: 21 additions & 0 deletions .starters/default/copy-list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"repo": "incubrain/foundry",
"ref": "main",
"files": [
{ "src": "scripts/install-skills.sh" },
{ "src": ".agents/rules/architecture.md" },
{ "src": ".agents/rules/conventions.md" },
{ "src": ".agents/rules/decisions.md" },
{ "src": ".agents/rules/anti-patterns.md" },
{ "src": ".claude/settings.json" },
{ "src": ".claude/skills.json" },
{ "src": ".claude/agents/codebase-explorer.md" },
{ "src": ".claude/agents/nuxt-dev.md" },
{ "src": ".claude/agents/signal-reviewer.md" },
{ "src": "skills/docs-writer/SKILL.md" },
{ "src": "skills/docs-writer/references/MDC-SYNTAX.md" },
{ "src": "skills/docs-writer/references/COMPONENTS.md" },
{ "src": ".prettierrc" },
{ "src": "deploy/vercel.website.json", "dest": "deploy/vercel.json" }
]
}
156 changes: 0 additions & 156 deletions .starters/default/scripts/install-skills.sh

This file was deleted.

3 changes: 3 additions & 0 deletions cli/cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { resolve } from 'node:path'
import { defineCommand, runMain } from 'citty'
import { processCopyList } from './copy-files'
import type { CLIOptions } from './types'

export function createCLI(opts: CLIOptions) {
Expand Down Expand Up @@ -37,6 +38,8 @@ export function createCLI(opts: CLIOptions) {
'-t',
`gh:incubrain/foundry/.starters/${template}`,
])

await processCopyList(dir)
},
})

Expand Down
88 changes: 88 additions & 0 deletions cli/copy-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { readFile, writeFile, mkdir } from 'node:fs/promises'
import { resolve, dirname, join, relative } from 'node:path'
import type { CopyListConfig } from './types'

const GITHUB_RAW = 'https://raw.githubusercontent.com'
const REPO_PATTERN = /^[\w.-]+\/[\w.-]+$/
const REF_PATTERN = /^[\w./-]+$/
const PATH_PATTERN = /^[\w./-]+$/

function validateRepo(repo: string): void {
if (!REPO_PATTERN.test(repo)) {
throw new Error(`Invalid repo format: "${repo}" (expected "owner/name")`)
}
}

function validateRef(ref: string): void {
if (!REF_PATTERN.test(ref)) {
throw new Error(`Invalid ref format: "${ref}"`)
}
}

function validateFilePath(filePath: string): void {
if (!PATH_PATTERN.test(filePath) || filePath.includes('..')) {
throw new Error(`Invalid file path: "${filePath}"`)
}
}

function safeDest(projectDir: string, dest: string): string {
const destPath = resolve(projectDir, dest)
const rel = relative(projectDir, destPath)
if (rel.startsWith('..') || resolve(destPath) !== destPath) {
throw new Error(`Path traversal blocked: "${dest}" resolves outside project`)
}
return destPath
}

export async function processCopyList(projectDir: string): Promise<void> {
const configPath = join(projectDir, 'copy-list.json')

let raw: string
try {
raw = await readFile(configPath, 'utf-8')
}
catch {
return
}

const config: CopyListConfig = JSON.parse(raw)
const repo = config.repo ?? 'incubrain/foundry'
const ref = config.ref ?? 'main'
const { files } = config

validateRepo(repo)
validateRef(ref)

console.log(`\nFetching ${files.length} shared files from ${repo}@${ref}...`)

const results = await Promise.allSettled(
files.map(async (file) => {
validateFilePath(file.src)
const dest = file.dest ?? file.src
validateFilePath(dest)

const url = `${GITHUB_RAW}/${repo}/${ref}/${file.src}`
const destPath = safeDest(projectDir, dest)

const response = await fetch(url)

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 1a500f7

if (!response.ok) {
throw new Error(`${file.src}: ${response.status} ${response.statusText}`)
}

const content = await response.text()
await mkdir(dirname(destPath), { recursive: true })
await writeFile(destPath, content, 'utf-8')

Check warning

Code scanning / CodeQL

Network data written to file Medium

Write to file system depends on
Untrusted data
.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 1a500f7

console.log(` + ${dest}`)
}),
)

const failed = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
if (failed.length > 0) {
console.warn(`\n${failed.length} file(s) failed to fetch:`)
for (const f of failed) {
console.warn(` - ${f.reason}`)
}
}

console.log(`\nDone. ${files.length - failed.length}/${files.length} shared files copied.`)
}
11 changes: 11 additions & 0 deletions cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,14 @@ export interface CLIOptions {
defaults: Record<string, never>
}
}

export interface CopyListFile {
src: string
dest?: string
}

export interface CopyListConfig {
repo?: string
ref?: string
files: CopyListFile[]
}
2 changes: 1 addition & 1 deletion layer/modules/events/server/handlers/webhook.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export default defineEventHandler(async (event) => {
}

// Retry webhook delivery with exponential backoff
const response = await retryWithBackoff(
await retryWithBackoff(
async () => {
const result = await $fetch(url, {
method: 'POST',
Expand Down
2 changes: 1 addition & 1 deletion layer/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export default defineNuxtConfig({

hooks: {
'components:extend': (
components: { pascalName: string; global?: boolean | 'sync' }[],
components: { pascalName: string, global?: boolean | 'sync' }[],
) => {
const globals = components.filter((c: { pascalName: string }) =>
['UButton', 'UIcon'].includes(c.pascalName),
Expand Down
13 changes: 10 additions & 3 deletions layer/server/middleware/docs-redirect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { defineEventHandler, sendRedirect } from 'h3'
import { queryCollectionNavigation } from '@nuxt/content/server'

interface NavItem {
path?: string
body?: unknown
children?: NavItem[]
}

/**
* Redirects docs directory routes to the first child page.
*
Expand Down Expand Up @@ -31,7 +37,7 @@ export default defineEventHandler(async (event) => {
const navigation = await queryCollectionNavigation(event, 'docs')

// Find the matching directory node
const findNode = (items: any[], targetPath: string): any => {
const findNode = (items: NavItem[], targetPath: string): NavItem | null => {
for (const item of items) {
// Normalize paths for comparison (remove trailing slash)
const itemPath = item.path?.replace(/\/$/, '')
Expand All @@ -53,13 +59,14 @@ export default defineEventHandler(async (event) => {

// If we found a node with children but no body (no content file),
// redirect to the first child
if (node?.children?.length > 0 && !node.body) {
if (node?.children?.length && node.children.length > 0 && !node.body) {
const firstChild = node.children[0]
if (firstChild?.path) {
return sendRedirect(event, firstChild.path, 302)
}
}
} catch (error) {
}
catch (error) {
// Fail silently - let the normal 404 handling take over
console.debug('docs-redirect middleware:', error)
}
Expand Down
Loading
Loading