shipfolio 1.0.10 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/scanner/git.ts","../../../src/utils/fs.ts","../../../src/scanner/detectors/node.ts","../../../src/scanner/detectors/python.ts","../../../src/scanner/detectors/rust.ts","../../../src/scanner/detectors/go.ts","../../../src/scanner/detectors/generic.ts","../../../src/scanner/extractors.ts","../../../src/utils/logger.ts","../../../src/scanner/index.ts"],"sourcesContent":["import simpleGit from \"simple-git\";\nimport { basename } from \"node:path\";\n\nexport interface GitMeta {\n isRepo: boolean;\n firstCommitDate: string | null;\n lastCommitDate: string | null;\n totalCommits: number;\n remoteUrl: string | null;\n lastCommitHash: string | null;\n}\n\nexport async function getGitMeta(projectPath: string): Promise<GitMeta> {\n const git = simpleGit(projectPath);\n const empty: GitMeta = {\n isRepo: false,\n firstCommitDate: null,\n lastCommitDate: null,\n totalCommits: 0,\n remoteUrl: null,\n lastCommitHash: null,\n };\n\n try {\n const isRepo = await git.checkIsRepo();\n if (!isRepo) return empty;\n } catch {\n return empty;\n }\n\n try {\n const log = await git.log({ maxCount: 1 });\n\n // Get first commit date\n let firstCommitDate: string | null = null;\n try {\n const firstResult = await git.raw([\"log\", \"--reverse\", \"--format=%aI\", \"--max-count=1\"]);\n firstCommitDate = firstResult.trim() || null;\n } catch {\n // ignore\n }\n\n let remoteUrl: string | null = null;\n try {\n const remotes = await git.getRemotes(true);\n const origin = remotes.find((r) => r.name === \"origin\");\n if (origin?.refs?.fetch) {\n remoteUrl = normalizeGitUrl(origin.refs.fetch);\n }\n } catch {\n // no remotes\n }\n\n // Get total commit count\n let totalCommits = 0;\n try {\n const result = await git.raw([\"rev-list\", \"--count\", \"HEAD\"]);\n totalCommits = parseInt(result.trim(), 10) || 0;\n } catch {\n totalCommits = 0;\n }\n\n return {\n isRepo: true,\n firstCommitDate,\n lastCommitDate: log.latest?.date || null,\n totalCommits,\n remoteUrl,\n lastCommitHash: log.latest?.hash || null,\n };\n } catch {\n return { ...empty, isRepo: true };\n }\n}\n\nfunction normalizeGitUrl(url: string): string {\n // Convert SSH to HTTPS\n if (url.startsWith(\"git@\")) {\n url = url.replace(\":\", \"/\").replace(\"git@\", \"https://\");\n }\n // Remove .git suffix\n if (url.endsWith(\".git\")) {\n url = url.slice(0, -4);\n }\n return url;\n}\n\nexport async function findGitRepos(\n rootPath: string,\n maxDepth: number = 3\n): Promise<string[]> {\n const { glob } = await import(\"glob\");\n const gitDirs = await glob(\"**/.git\", {\n cwd: rootPath,\n maxDepth: maxDepth + 1,\n dot: true,\n ignore: [\n \"**/node_modules/**/.git\",\n \"**/vendor/**/.git\",\n \"**/__pycache__/**/.git\",\n ],\n });\n\n return gitDirs\n .map((gitDir) => {\n const parts = gitDir.split(\"/\");\n parts.pop(); // remove .git\n return parts.length > 0\n ? `${rootPath}/${parts.join(\"/\")}`\n : rootPath;\n })\n .sort();\n}\n","import { readFile, writeFile, access, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport async function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function readJson<T = unknown>(path: string): Promise<T> {\n const content = await readFile(path, \"utf-8\");\n return JSON.parse(content) as T;\n}\n\nexport async function writeJson(path: string, data: unknown): Promise<void> {\n await writeFile(path, JSON.stringify(data, null, 2), \"utf-8\");\n}\n\nexport async function readText(path: string): Promise<string> {\n return readFile(path, \"utf-8\");\n}\n\nexport async function writeText(path: string, content: string): Promise<void> {\n await writeFile(path, content, \"utf-8\");\n}\n\nexport async function ensureDir(path: string): Promise<void> {\n await mkdir(path, { recursive: true });\n}\n\nexport { join };\n","import { fileExists, readJson, join } from \"../../utils/fs.js\";\n\ninterface PackageJson {\n name?: string;\n description?: string;\n homepage?: string;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n}\n\nexport async function detectNode(projectPath: string) {\n const pkgPath = join(projectPath, \"package.json\");\n if (!(await fileExists(pkgPath))) return null;\n\n const pkg = await readJson<PackageJson>(pkgPath);\n const allDeps = {\n ...pkg.dependencies,\n ...pkg.devDependencies,\n };\n const depNames = Object.keys(allDeps);\n\n const techStack: string[] = [];\n\n // Frameworks\n if (depNames.includes(\"next\")) techStack.push(\"Next.js\");\n else if (depNames.includes(\"nuxt\")) techStack.push(\"Nuxt\");\n else if (depNames.includes(\"astro\")) techStack.push(\"Astro\");\n else if (depNames.includes(\"svelte\") || depNames.includes(\"@sveltejs/kit\"))\n techStack.push(\"Svelte\");\n\n // UI libraries\n if (depNames.includes(\"react\")) techStack.push(\"React\");\n if (depNames.includes(\"vue\")) techStack.push(\"Vue\");\n\n // Styling\n if (depNames.includes(\"tailwindcss\")) techStack.push(\"Tailwind CSS\");\n\n // Backend\n if (depNames.includes(\"express\")) techStack.push(\"Express\");\n if (depNames.includes(\"fastify\")) techStack.push(\"Fastify\");\n if (depNames.includes(\"hono\")) techStack.push(\"Hono\");\n\n // Database\n if (depNames.includes(\"prisma\") || depNames.includes(\"@prisma/client\"))\n techStack.push(\"Prisma\");\n if (depNames.includes(\"drizzle-orm\")) techStack.push(\"Drizzle\");\n if (depNames.includes(\"mongoose\")) techStack.push(\"MongoDB\");\n\n // AI/ML\n if (depNames.includes(\"openai\")) techStack.push(\"OpenAI\");\n if (depNames.includes(\"@anthropic-ai/sdk\")) techStack.push(\"Claude API\");\n if (depNames.includes(\"langchain\")) techStack.push(\"LangChain\");\n\n // Language\n if (depNames.includes(\"typescript\")) techStack.push(\"TypeScript\");\n else techStack.push(\"JavaScript\");\n\n return {\n name: pkg.name || null,\n description: pkg.description || null,\n homepage: pkg.homepage || null,\n techStack,\n };\n}\n","import { fileExists, readText, join } from \"../../utils/fs.js\";\n\nexport async function detectPython(projectPath: string) {\n const techStack: string[] = [\"Python\"];\n\n const reqPath = join(projectPath, \"requirements.txt\");\n const pyprojectPath = join(projectPath, \"pyproject.toml\");\n const setupPath = join(projectPath, \"setup.py\");\n\n let deps = \"\";\n\n if (await fileExists(reqPath)) {\n deps = await readText(reqPath);\n } else if (await fileExists(pyprojectPath)) {\n deps = await readText(pyprojectPath);\n } else if (await fileExists(setupPath)) {\n deps = await readText(setupPath);\n } else {\n return null;\n }\n\n const depsLower = deps.toLowerCase();\n\n if (depsLower.includes(\"django\")) techStack.push(\"Django\");\n if (depsLower.includes(\"flask\")) techStack.push(\"Flask\");\n if (depsLower.includes(\"fastapi\")) techStack.push(\"FastAPI\");\n if (depsLower.includes(\"pytorch\") || depsLower.includes(\"torch\"))\n techStack.push(\"PyTorch\");\n if (depsLower.includes(\"tensorflow\")) techStack.push(\"TensorFlow\");\n if (depsLower.includes(\"transformers\")) techStack.push(\"Transformers\");\n if (depsLower.includes(\"langchain\")) techStack.push(\"LangChain\");\n if (depsLower.includes(\"openai\")) techStack.push(\"OpenAI\");\n if (depsLower.includes(\"pandas\")) techStack.push(\"Pandas\");\n if (depsLower.includes(\"numpy\")) techStack.push(\"NumPy\");\n if (depsLower.includes(\"scikit\")) techStack.push(\"Scikit-learn\");\n\n return { techStack };\n}\n","import { fileExists, readText, join } from \"../../utils/fs.js\";\n\nexport async function detectRust(projectPath: string) {\n const cargoPath = join(projectPath, \"Cargo.toml\");\n if (!(await fileExists(cargoPath))) return null;\n\n const content = await readText(cargoPath);\n const techStack: string[] = [\"Rust\"];\n\n if (content.includes(\"actix\")) techStack.push(\"Actix\");\n if (content.includes(\"axum\")) techStack.push(\"Axum\");\n if (content.includes(\"tokio\")) techStack.push(\"Tokio\");\n if (content.includes(\"wasm\")) techStack.push(\"WebAssembly\");\n if (content.includes(\"tauri\")) techStack.push(\"Tauri\");\n if (content.includes(\"diesel\")) techStack.push(\"Diesel\");\n if (content.includes(\"sqlx\")) techStack.push(\"SQLx\");\n\n // Extract name from [package] section\n const nameMatch = content.match(/\\[package\\][\\s\\S]*?name\\s*=\\s*\"([^\"]+)\"/);\n const descMatch = content.match(\n /\\[package\\][\\s\\S]*?description\\s*=\\s*\"([^\"]+)\"/\n );\n\n return {\n name: nameMatch?.[1] || null,\n description: descMatch?.[1] || null,\n techStack,\n };\n}\n","import { fileExists, readText, join } from \"../../utils/fs.js\";\n\nexport async function detectGo(projectPath: string) {\n const goModPath = join(projectPath, \"go.mod\");\n if (!(await fileExists(goModPath))) return null;\n\n const content = await readText(goModPath);\n const techStack: string[] = [\"Go\"];\n\n if (content.includes(\"gin-gonic\")) techStack.push(\"Gin\");\n if (content.includes(\"echo\")) techStack.push(\"Echo\");\n if (content.includes(\"fiber\")) techStack.push(\"Fiber\");\n if (content.includes(\"grpc\")) techStack.push(\"gRPC\");\n if (content.includes(\"gorm\")) techStack.push(\"GORM\");\n if (content.includes(\"cobra\")) techStack.push(\"Cobra\");\n if (content.includes(\"ent\")) techStack.push(\"Ent\");\n\n return { techStack };\n}\n","import { glob } from \"glob\";\nimport { basename, extname } from \"node:path\";\n\nconst EXTENSION_MAP: Record<string, string> = {\n \".ts\": \"TypeScript\",\n \".tsx\": \"TypeScript\",\n \".js\": \"JavaScript\",\n \".jsx\": \"JavaScript\",\n \".py\": \"Python\",\n \".rs\": \"Rust\",\n \".go\": \"Go\",\n \".java\": \"Java\",\n \".kt\": \"Kotlin\",\n \".swift\": \"Swift\",\n \".rb\": \"Ruby\",\n \".php\": \"PHP\",\n \".cs\": \"C#\",\n \".cpp\": \"C++\",\n \".c\": \"C\",\n \".dart\": \"Dart\",\n \".lua\": \"Lua\",\n \".zig\": \"Zig\",\n \".sol\": \"Solidity\",\n \".ex\": \"Elixir\",\n \".exs\": \"Elixir\",\n};\n\nconst IGNORE_DIRS = [\n \"node_modules\",\n \".git\",\n \"dist\",\n \"build\",\n \"out\",\n \".next\",\n \"target\",\n \"vendor\",\n \"__pycache__\",\n \".venv\",\n \"venv\",\n];\n\nexport async function detectLanguages(\n projectPath: string\n): Promise<Record<string, number>> {\n const ignorePattern = IGNORE_DIRS.map((d) => `**/${d}/**`);\n const files = await glob(\"**/*.*\", {\n cwd: projectPath,\n ignore: ignorePattern,\n nodir: true,\n maxDepth: 5,\n });\n\n const counts: Record<string, number> = {};\n for (const file of files) {\n const ext = extname(file).toLowerCase();\n const lang = EXTENSION_MAP[ext];\n if (lang) {\n counts[lang] = (counts[lang] || 0) + 1;\n }\n }\n\n return counts;\n}\n\nexport function deriveNameFromPath(projectPath: string): string {\n return basename(projectPath);\n}\n","import { fileExists, readText, join } from \"../utils/fs.js\";\n\nexport async function extractReadme(\n projectPath: string\n): Promise<string | null> {\n const candidates = [\n \"README.md\",\n \"readme.md\",\n \"Readme.md\",\n \"README.txt\",\n \"README\",\n ];\n for (const name of candidates) {\n const p = join(projectPath, name);\n if (await fileExists(p)) {\n const content = await readText(p);\n // Limit to first 3000 chars to keep spec manageable\n return content.slice(0, 3000);\n }\n }\n return null;\n}\n\nexport function extractFirstParagraph(readme: string | null): string | null {\n if (!readme) return null;\n // Skip title lines (# heading)\n const lines = readme.split(\"\\n\");\n let collecting = false;\n const paragraphLines: string[] = [];\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed.startsWith(\"#\")) {\n if (collecting && paragraphLines.length > 0) break;\n collecting = true;\n continue;\n }\n if (collecting && trimmed === \"\" && paragraphLines.length > 0) break;\n if (collecting && trimmed !== \"\" && !trimmed.startsWith(\"![\") && !trimmed.startsWith(\"[![\")) {\n paragraphLines.push(trimmed);\n }\n if (!collecting && trimmed !== \"\" && !trimmed.startsWith(\"[\") && !trimmed.startsWith(\"!\") && !trimmed.startsWith(\"[![\")) {\n paragraphLines.push(trimmed);\n collecting = true;\n }\n }\n\n return paragraphLines.length > 0 ? paragraphLines.join(\" \") : null;\n}\n\nexport function extractDemoUrl(readme: string | null): string | null {\n if (!readme) return null;\n // Look for common demo URL patterns\n const patterns = [\n /(?:demo|live|website|site|url|link)[\\s:]*\\[?[^\\]]*\\]?\\(?(https?:\\/\\/[^\\s)]+)/i,\n /\\[(?:demo|live|website|try it)\\]\\((https?:\\/\\/[^\\s)]+)\\)/i,\n /(https?:\\/\\/[^\\s)]+\\.(?:vercel|netlify|pages\\.dev|herokuapp|railway)\\.app[^\\s)]*)/i,\n ];\n for (const pattern of patterns) {\n const match = readme.match(pattern);\n if (match?.[1]) return match[1];\n }\n return null;\n}\n","import chalk from \"chalk\";\n\nexport const logger = {\n info(msg: string) {\n console.log(chalk.cyan(`-- ${msg}`));\n },\n success(msg: string) {\n console.log(chalk.green(`-- ${msg}`));\n },\n warn(msg: string) {\n console.log(chalk.yellow(`-- ${msg}`));\n },\n error(msg: string) {\n console.error(chalk.red(`-- ${msg}`));\n },\n plain(msg: string) {\n console.log(msg);\n },\n blank() {\n console.log();\n },\n header(msg: string) {\n console.log();\n console.log(chalk.bold(msg));\n console.log();\n },\n table(rows: string[][]) {\n if (rows.length === 0) return;\n const colWidths = rows[0].map((_, colIdx) =>\n Math.max(...rows.map((row) => (row[colIdx] || \"\").length))\n );\n for (const row of rows) {\n const line = row\n .map((cell, i) => (cell || \"\").padEnd(colWidths[i] + 2))\n .join(\"\");\n console.log(` ${line}`);\n }\n },\n};\n","import { findGitRepos, getGitMeta } from \"./git.js\";\nimport { detectNode } from \"./detectors/node.js\";\nimport { detectPython } from \"./detectors/python.js\";\nimport { detectRust } from \"./detectors/rust.js\";\nimport { detectGo } from \"./detectors/go.js\";\nimport { detectLanguages, deriveNameFromPath } from \"./detectors/generic.js\";\nimport {\n extractReadme,\n extractFirstParagraph,\n extractDemoUrl,\n} from \"./extractors.js\";\nimport type { ProjectMeta } from \"../spec/schema.js\";\nimport { logger } from \"../utils/logger.js\";\nimport ora from \"ora\";\n\nexport async function scanProjects(\n directories: string[]\n): Promise<ProjectMeta[]> {\n const allPaths: string[] = [];\n\n const spinner = ora(\"Scanning for projects...\").start();\n\n for (const dir of directories) {\n try {\n // Find git repos\n const repos = await findGitRepos(dir);\n allPaths.push(...repos);\n\n // Also find non-git project directories (with package.json, Cargo.toml, etc.)\n const nonGitDirs = await findNonGitProjects(dir, new Set(repos));\n allPaths.push(...nonGitDirs);\n } catch (err) {\n logger.warn(`Could not scan ${dir}: ${err}`);\n }\n }\n\n // Deduplicate\n const uniquePaths = [...new Set(allPaths)];\n spinner.text = `Found ${uniquePaths.length} projects. Extracting metadata...`;\n\n const projects: ProjectMeta[] = [];\n\n for (const projectPath of uniquePaths) {\n try {\n const project = await extractProjectMeta(projectPath);\n if (project) {\n projects.push(project);\n }\n } catch (err) {\n // Skip projects that fail extraction\n }\n }\n\n // Deduplicate IDs (same folder name in different parent dirs)\n const idCounts = new Map<string, number>();\n for (const project of projects) {\n const count = idCounts.get(project.id) || 0;\n if (count > 0) {\n project.id = `${project.id}-${count}`;\n }\n idCounts.set(project.id.replace(/-\\d+$/, \"\"), count + 1);\n }\n\n spinner.succeed(`Scanned ${projects.length} projects`);\n return projects;\n}\n\nasync function findNonGitProjects(\n rootPath: string,\n gitRepos: Set<string>\n): Promise<string[]> {\n const { glob } = await import(\"glob\");\n const indicators = [\n \"*/package.json\",\n \"*/Cargo.toml\",\n \"*/go.mod\",\n \"*/pyproject.toml\",\n \"*/requirements.txt\",\n \"*/setup.py\",\n ];\n const found = new Set<string>();\n for (const pattern of indicators) {\n const matches = await glob(pattern, {\n cwd: rootPath,\n ignore: [\"**/node_modules/**\", \"**/vendor/**\"],\n });\n for (const match of matches) {\n const dir = `${rootPath}/${match.split(\"/\")[0]}`;\n if (!gitRepos.has(dir) && !found.has(dir)) {\n found.add(dir);\n }\n }\n }\n return [...found].sort();\n}\n\nasync function extractProjectMeta(\n projectPath: string\n): Promise<ProjectMeta | null> {\n const gitMeta = await getGitMeta(projectPath);\n\n // Detect tech stack from various sources\n let techStack: string[] = [];\n let name: string | null = null;\n let description: string | null = null;\n let homepage: string | null = null;\n\n const nodeInfo = await detectNode(projectPath);\n if (nodeInfo) {\n techStack.push(...nodeInfo.techStack);\n name = nodeInfo.name || name;\n description = nodeInfo.description || description;\n homepage = nodeInfo.homepage || homepage;\n }\n\n const pythonInfo = await detectPython(projectPath);\n if (pythonInfo) {\n techStack.push(...pythonInfo.techStack);\n }\n\n const rustInfo = await detectRust(projectPath);\n if (rustInfo) {\n techStack.push(...rustInfo.techStack);\n name = rustInfo.name || name;\n description = rustInfo.description || description;\n }\n\n const goInfo = await detectGo(projectPath);\n if (goInfo) {\n techStack.push(...goInfo.techStack);\n }\n\n // Deduplicate tech stack\n techStack = [...new Set(techStack)];\n\n // Language breakdown\n const languages = await detectLanguages(projectPath);\n\n // If no tech stack detected, derive from languages\n if (techStack.length === 0) {\n const topLangs = Object.entries(languages)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 3)\n .map(([lang]) => lang);\n techStack = topLangs;\n }\n\n // README\n const readmeContent = await extractReadme(projectPath);\n if (!description) {\n description = extractFirstParagraph(readmeContent) || \"\";\n }\n\n // Demo URL\n const demoUrl = homepage || extractDemoUrl(readmeContent);\n\n // Name fallback\n if (!name) {\n name = deriveNameFromPath(projectPath);\n }\n\n // Generate stable ID from path (use folder name to keep it readable, but\n // append a hash suffix when the same folder name appears in multiple paths)\n const folderName = deriveNameFromPath(projectPath);\n const id = folderName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n\n return {\n id,\n name,\n localPath: projectPath,\n description: description || \"\",\n techStack,\n languages,\n firstCommitDate: gitMeta.firstCommitDate || \"\",\n lastCommitDate: gitMeta.lastCommitDate || \"\",\n totalCommits: gitMeta.totalCommits,\n remoteUrl: gitMeta.remoteUrl,\n demoUrl,\n readmeContent,\n lastScannedCommit: gitMeta.lastCommitHash || \"\",\n };\n}\n"],"mappings":"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","names":["glob","glob"]}
1
+ {"version":3,"sources":["../../../src/scanner/git.ts","../../../src/utils/fs.ts","../../../src/scanner/detectors/node.ts","../../../src/scanner/detectors/python.ts","../../../src/scanner/detectors/rust.ts","../../../src/scanner/detectors/go.ts","../../../src/scanner/detectors/generic.ts","../../../src/scanner/index.ts","../../../src/scanner/extractors.ts","../../../src/utils/logger.ts"],"sourcesContent":["import simpleGit from \"simple-git\";\n\nexport interface GitMeta {\n isRepo: boolean;\n firstCommitDate: string | null;\n lastCommitDate: string | null;\n totalCommits: number;\n remoteUrl: string | null;\n lastCommitHash: string | null;\n}\n\nexport async function getGitMeta(projectPath: string): Promise<GitMeta> {\n const git = simpleGit(projectPath);\n const empty: GitMeta = {\n isRepo: false,\n firstCommitDate: null,\n lastCommitDate: null,\n totalCommits: 0,\n remoteUrl: null,\n lastCommitHash: null,\n };\n\n try {\n const isRepo = await git.checkIsRepo();\n if (!isRepo) return empty;\n } catch {\n return empty;\n }\n\n try {\n const log = await git.log({ maxCount: 1 });\n\n // Get first commit date\n let firstCommitDate: string | null = null;\n try {\n const firstResult = await git.raw([\"log\", \"--reverse\", \"--format=%aI\", \"--max-count=1\"]);\n firstCommitDate = firstResult.trim() || null;\n } catch {\n // ignore\n }\n\n let remoteUrl: string | null = null;\n try {\n const remotes = await git.getRemotes(true);\n const origin = remotes.find((r) => r.name === \"origin\");\n if (origin?.refs?.fetch) {\n remoteUrl = normalizeGitUrl(origin.refs.fetch);\n }\n } catch {\n // no remotes\n }\n\n // Get total commit count\n let totalCommits = 0;\n try {\n const result = await git.raw([\"rev-list\", \"--count\", \"HEAD\"]);\n totalCommits = parseInt(result.trim(), 10) || 0;\n } catch {\n totalCommits = 0;\n }\n\n return {\n isRepo: true,\n firstCommitDate,\n lastCommitDate: log.latest?.date || null,\n totalCommits,\n remoteUrl,\n lastCommitHash: log.latest?.hash || null,\n };\n } catch {\n return { ...empty, isRepo: true };\n }\n}\n\nfunction normalizeGitUrl(url: string): string {\n // Convert SSH to HTTPS\n if (url.startsWith(\"git@\")) {\n url = url.replace(\":\", \"/\").replace(\"git@\", \"https://\");\n }\n // Remove .git suffix\n if (url.endsWith(\".git\")) {\n url = url.slice(0, -4);\n }\n return url;\n}\n\nexport async function findGitRepos(\n rootPath: string,\n maxDepth: number = 6\n): Promise<string[]> {\n const { glob } = await import(\"glob\");\n const gitDirs = await glob(\"**/.git\", {\n cwd: rootPath,\n maxDepth: maxDepth + 1,\n dot: true,\n ignore: [\n \"**/node_modules/**/.git\",\n \"**/vendor/**/.git\",\n \"**/__pycache__/**/.git\",\n ],\n });\n\n return gitDirs\n .map((gitDir) => {\n const parts = gitDir.split(\"/\");\n parts.pop(); // remove .git\n return parts.length > 0\n ? `${rootPath}/${parts.join(\"/\")}`\n : rootPath;\n })\n .sort();\n}\n","import { readFile, writeFile, access, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport async function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function readJson<T = unknown>(path: string): Promise<T> {\n const content = await readFile(path, \"utf-8\");\n return JSON.parse(content) as T;\n}\n\nexport async function writeJson(path: string, data: unknown): Promise<void> {\n await writeFile(path, JSON.stringify(data, null, 2), \"utf-8\");\n}\n\nexport async function readText(path: string): Promise<string> {\n return readFile(path, \"utf-8\");\n}\n\nexport async function writeText(path: string, content: string): Promise<void> {\n await writeFile(path, content, \"utf-8\");\n}\n\nexport async function ensureDir(path: string): Promise<void> {\n await mkdir(path, { recursive: true });\n}\n\nexport { join };\n","import { fileExists, readJson, join } from \"../../utils/fs.js\";\n\ninterface PackageJson {\n name?: string;\n description?: string;\n homepage?: string;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n}\n\nexport async function detectNode(projectPath: string) {\n const pkgPath = join(projectPath, \"package.json\");\n if (!(await fileExists(pkgPath))) return null;\n\n const pkg = await readJson<PackageJson>(pkgPath);\n const allDeps = {\n ...pkg.dependencies,\n ...pkg.devDependencies,\n };\n const depNames = Object.keys(allDeps);\n\n const techStack: string[] = [];\n\n // Frameworks\n if (depNames.includes(\"next\")) techStack.push(\"Next.js\");\n else if (depNames.includes(\"nuxt\")) techStack.push(\"Nuxt\");\n else if (depNames.includes(\"astro\")) techStack.push(\"Astro\");\n else if (depNames.includes(\"svelte\") || depNames.includes(\"@sveltejs/kit\"))\n techStack.push(\"Svelte\");\n\n // UI libraries\n if (depNames.includes(\"react\")) techStack.push(\"React\");\n if (depNames.includes(\"vue\")) techStack.push(\"Vue\");\n\n // Styling\n if (depNames.includes(\"tailwindcss\")) techStack.push(\"Tailwind CSS\");\n\n // Backend\n if (depNames.includes(\"express\")) techStack.push(\"Express\");\n if (depNames.includes(\"fastify\")) techStack.push(\"Fastify\");\n if (depNames.includes(\"hono\")) techStack.push(\"Hono\");\n\n // Database\n if (depNames.includes(\"prisma\") || depNames.includes(\"@prisma/client\"))\n techStack.push(\"Prisma\");\n if (depNames.includes(\"drizzle-orm\")) techStack.push(\"Drizzle\");\n if (depNames.includes(\"mongoose\")) techStack.push(\"MongoDB\");\n\n // AI/ML\n if (depNames.includes(\"openai\")) techStack.push(\"OpenAI\");\n if (depNames.includes(\"@anthropic-ai/sdk\")) techStack.push(\"Claude API\");\n if (depNames.includes(\"langchain\")) techStack.push(\"LangChain\");\n\n // Language\n if (depNames.includes(\"typescript\")) techStack.push(\"TypeScript\");\n else techStack.push(\"JavaScript\");\n\n return {\n name: pkg.name || null,\n description: pkg.description || null,\n homepage: pkg.homepage || null,\n techStack,\n };\n}\n","import { fileExists, readText, join } from \"../../utils/fs.js\";\n\nexport async function detectPython(projectPath: string) {\n const techStack: string[] = [\"Python\"];\n\n const reqPath = join(projectPath, \"requirements.txt\");\n const pyprojectPath = join(projectPath, \"pyproject.toml\");\n const setupPath = join(projectPath, \"setup.py\");\n\n let deps = \"\";\n\n if (await fileExists(reqPath)) {\n deps = await readText(reqPath);\n } else if (await fileExists(pyprojectPath)) {\n deps = await readText(pyprojectPath);\n } else if (await fileExists(setupPath)) {\n deps = await readText(setupPath);\n } else {\n return null;\n }\n\n const depsLower = deps.toLowerCase();\n\n if (depsLower.includes(\"django\")) techStack.push(\"Django\");\n if (depsLower.includes(\"flask\")) techStack.push(\"Flask\");\n if (depsLower.includes(\"fastapi\")) techStack.push(\"FastAPI\");\n if (depsLower.includes(\"pytorch\") || depsLower.includes(\"torch\"))\n techStack.push(\"PyTorch\");\n if (depsLower.includes(\"tensorflow\")) techStack.push(\"TensorFlow\");\n if (depsLower.includes(\"transformers\")) techStack.push(\"Transformers\");\n if (depsLower.includes(\"langchain\")) techStack.push(\"LangChain\");\n if (depsLower.includes(\"openai\")) techStack.push(\"OpenAI\");\n if (depsLower.includes(\"pandas\")) techStack.push(\"Pandas\");\n if (depsLower.includes(\"numpy\")) techStack.push(\"NumPy\");\n if (depsLower.includes(\"scikit\")) techStack.push(\"Scikit-learn\");\n\n return { techStack };\n}\n","import { fileExists, readText, join } from \"../../utils/fs.js\";\n\nexport async function detectRust(projectPath: string) {\n const cargoPath = join(projectPath, \"Cargo.toml\");\n if (!(await fileExists(cargoPath))) return null;\n\n const content = await readText(cargoPath);\n const techStack: string[] = [\"Rust\"];\n\n if (content.includes(\"actix\")) techStack.push(\"Actix\");\n if (content.includes(\"axum\")) techStack.push(\"Axum\");\n if (content.includes(\"tokio\")) techStack.push(\"Tokio\");\n if (content.includes(\"wasm\")) techStack.push(\"WebAssembly\");\n if (content.includes(\"tauri\")) techStack.push(\"Tauri\");\n if (content.includes(\"diesel\")) techStack.push(\"Diesel\");\n if (content.includes(\"sqlx\")) techStack.push(\"SQLx\");\n\n // Extract name from [package] section\n const nameMatch = content.match(/\\[package\\][\\s\\S]*?name\\s*=\\s*\"([^\"]+)\"/);\n const descMatch = content.match(\n /\\[package\\][\\s\\S]*?description\\s*=\\s*\"([^\"]+)\"/\n );\n\n return {\n name: nameMatch?.[1] || null,\n description: descMatch?.[1] || null,\n techStack,\n };\n}\n","import { fileExists, readText, join } from \"../../utils/fs.js\";\n\nexport async function detectGo(projectPath: string) {\n const goModPath = join(projectPath, \"go.mod\");\n if (!(await fileExists(goModPath))) return null;\n\n const content = await readText(goModPath);\n const techStack: string[] = [\"Go\"];\n\n if (content.includes(\"gin-gonic\")) techStack.push(\"Gin\");\n if (content.includes(\"echo\")) techStack.push(\"Echo\");\n if (content.includes(\"fiber\")) techStack.push(\"Fiber\");\n if (content.includes(\"grpc\")) techStack.push(\"gRPC\");\n if (content.includes(\"gorm\")) techStack.push(\"GORM\");\n if (content.includes(\"cobra\")) techStack.push(\"Cobra\");\n if (content.includes(\"ent\")) techStack.push(\"Ent\");\n\n return { techStack };\n}\n","import { glob } from \"glob\";\nimport { basename, extname } from \"node:path\";\n\nconst EXTENSION_MAP: Record<string, string> = {\n \".ts\": \"TypeScript\",\n \".tsx\": \"TypeScript\",\n \".js\": \"JavaScript\",\n \".jsx\": \"JavaScript\",\n \".py\": \"Python\",\n \".rs\": \"Rust\",\n \".go\": \"Go\",\n \".java\": \"Java\",\n \".kt\": \"Kotlin\",\n \".swift\": \"Swift\",\n \".rb\": \"Ruby\",\n \".php\": \"PHP\",\n \".cs\": \"C#\",\n \".cpp\": \"C++\",\n \".c\": \"C\",\n \".dart\": \"Dart\",\n \".lua\": \"Lua\",\n \".zig\": \"Zig\",\n \".sol\": \"Solidity\",\n \".ex\": \"Elixir\",\n \".exs\": \"Elixir\",\n};\n\nconst IGNORE_DIRS = [\n \"node_modules\",\n \".git\",\n \"dist\",\n \"build\",\n \"out\",\n \".next\",\n \"target\",\n \"vendor\",\n \"__pycache__\",\n \".venv\",\n \"venv\",\n];\n\nexport async function detectLanguages(\n projectPath: string\n): Promise<Record<string, number>> {\n const ignorePattern = IGNORE_DIRS.map((d) => `**/${d}/**`);\n const files = await glob(\"**/*.*\", {\n cwd: projectPath,\n ignore: ignorePattern,\n nodir: true,\n maxDepth: 5,\n });\n\n const counts: Record<string, number> = {};\n for (const file of files) {\n const ext = extname(file).toLowerCase();\n const lang = EXTENSION_MAP[ext];\n if (lang) {\n counts[lang] = (counts[lang] || 0) + 1;\n }\n }\n\n return counts;\n}\n\nexport function deriveNameFromPath(projectPath: string): string {\n return basename(projectPath);\n}\n","import { findGitRepos, getGitMeta } from \"./git.js\";\nimport { detectNode } from \"./detectors/node.js\";\nimport { detectPython } from \"./detectors/python.js\";\nimport { detectRust } from \"./detectors/rust.js\";\nimport { detectGo } from \"./detectors/go.js\";\nimport { detectLanguages, deriveNameFromPath } from \"./detectors/generic.js\";\nimport { dirname, resolve as resolvePath } from \"node:path\";\nimport {\n extractReadme,\n extractFirstParagraph,\n extractDemoUrl,\n} from \"./extractors.js\";\nimport type { ProjectMeta } from \"../spec/schema.js\";\nimport { logger } from \"../utils/logger.js\";\nimport ora from \"ora\";\n\nconst DEFAULT_SCAN_DEPTH = 6;\nconst NON_GIT_PROJECT_INDICATORS = [\n \"package.json\",\n \"Cargo.toml\",\n \"go.mod\",\n \"pyproject.toml\",\n \"requirements.txt\",\n \"setup.py\",\n];\nconst SCAN_IGNORE_PATTERNS = [\n \"**/node_modules/**\",\n \"**/vendor/**\",\n \"**/.git/**\",\n \"**/.next/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/out/**\",\n \"**/coverage/**\",\n];\n\nexport async function scanProjects(\n directories: string[]\n): Promise<ProjectMeta[]> {\n const allPaths: string[] = [];\n\n const spinner = ora(\"Scanning for projects...\").start();\n\n for (const dir of directories) {\n try {\n // Find git repos\n const repos = await findGitRepos(dir, DEFAULT_SCAN_DEPTH);\n allPaths.push(...repos);\n\n // Also find non-git project directories (with package.json, Cargo.toml, etc.)\n const nonGitDirs = await findNonGitProjects(\n dir,\n repos,\n DEFAULT_SCAN_DEPTH\n );\n allPaths.push(...nonGitDirs);\n } catch (err) {\n logger.warn(`Could not scan ${dir}: ${err}`);\n }\n }\n\n // Deduplicate\n const uniquePaths = [...new Set(allPaths)];\n spinner.text = `Found ${uniquePaths.length} projects. Extracting metadata...`;\n\n const projects: ProjectMeta[] = [];\n\n for (const projectPath of uniquePaths) {\n try {\n const project = await extractProjectMeta(projectPath);\n if (project) {\n projects.push(project);\n }\n } catch (err) {\n // Skip projects that fail extraction\n }\n }\n\n // Deduplicate IDs (same folder name in different parent dirs)\n const idCounts = new Map<string, number>();\n for (const project of projects) {\n const count = idCounts.get(project.id) || 0;\n if (count > 0) {\n project.id = `${project.id}-${count}`;\n }\n idCounts.set(project.id.replace(/-\\d+$/, \"\"), count + 1);\n }\n\n spinner.succeed(`Scanned ${projects.length} projects`);\n return projects;\n}\n\nasync function findNonGitProjects(\n rootPath: string,\n gitRepos: string[],\n maxDepth: number\n): Promise<string[]> {\n const { glob } = await import(\"glob\");\n const found = new Set<string>();\n\n for (const indicator of NON_GIT_PROJECT_INDICATORS) {\n const matches = await glob(`**/${indicator}`, {\n cwd: rootPath,\n ignore: SCAN_IGNORE_PATTERNS,\n maxDepth,\n });\n\n for (const match of matches) {\n const candidateDir = dirname(resolvePath(rootPath, match));\n if (\n !isInsideTrackedRepo(candidateDir, gitRepos) &&\n !found.has(candidateDir)\n ) {\n found.add(candidateDir);\n }\n }\n }\n\n return [...found].sort();\n}\n\nfunction isInsideTrackedRepo(candidateDir: string, gitRepos: string[]): boolean {\n return gitRepos.some(\n (repoPath) =>\n candidateDir === repoPath || candidateDir.startsWith(`${repoPath}/`)\n );\n}\n\nasync function extractProjectMeta(\n projectPath: string\n): Promise<ProjectMeta | null> {\n const gitMeta = await getGitMeta(projectPath);\n\n // Detect tech stack from various sources\n let techStack: string[] = [];\n let name: string | null = null;\n let description: string | null = null;\n let homepage: string | null = null;\n\n const nodeInfo = await detectNode(projectPath);\n if (nodeInfo) {\n techStack.push(...nodeInfo.techStack);\n name = nodeInfo.name || name;\n description = nodeInfo.description || description;\n homepage = nodeInfo.homepage || homepage;\n }\n\n const pythonInfo = await detectPython(projectPath);\n if (pythonInfo) {\n techStack.push(...pythonInfo.techStack);\n }\n\n const rustInfo = await detectRust(projectPath);\n if (rustInfo) {\n techStack.push(...rustInfo.techStack);\n name = rustInfo.name || name;\n description = rustInfo.description || description;\n }\n\n const goInfo = await detectGo(projectPath);\n if (goInfo) {\n techStack.push(...goInfo.techStack);\n }\n\n // Deduplicate tech stack\n techStack = [...new Set(techStack)];\n\n // Language breakdown\n const languages = await detectLanguages(projectPath);\n\n // If no tech stack detected, derive from languages\n if (techStack.length === 0) {\n const topLangs = Object.entries(languages)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 3)\n .map(([lang]) => lang);\n techStack = topLangs;\n }\n\n // README\n const readmeContent = await extractReadme(projectPath);\n if (!description) {\n description = extractFirstParagraph(readmeContent) || \"\";\n }\n\n // Demo URL\n const demoUrl = homepage || extractDemoUrl(readmeContent);\n\n // Name fallback\n if (!name) {\n name = deriveNameFromPath(projectPath);\n }\n\n // Generate stable ID from path (use folder name to keep it readable, but\n // append a hash suffix when the same folder name appears in multiple paths)\n const folderName = deriveNameFromPath(projectPath);\n const id = folderName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n\n return {\n id,\n name,\n localPath: projectPath,\n description: description || \"\",\n techStack,\n languages,\n firstCommitDate: gitMeta.firstCommitDate || \"\",\n lastCommitDate: gitMeta.lastCommitDate || \"\",\n totalCommits: gitMeta.totalCommits,\n remoteUrl: gitMeta.remoteUrl,\n demoUrl,\n readmeContent,\n lastScannedCommit: gitMeta.lastCommitHash || \"\",\n };\n}\n","import { fileExists, readText, join } from \"../utils/fs.js\";\n\nexport async function extractReadme(\n projectPath: string\n): Promise<string | null> {\n const candidates = [\n \"README.md\",\n \"readme.md\",\n \"Readme.md\",\n \"README.txt\",\n \"README\",\n ];\n for (const name of candidates) {\n const p = join(projectPath, name);\n if (await fileExists(p)) {\n const content = await readText(p);\n // Limit to first 3000 chars to keep spec manageable\n return content.slice(0, 3000);\n }\n }\n return null;\n}\n\nexport function extractFirstParagraph(readme: string | null): string | null {\n if (!readme) return null;\n // Skip title lines (# heading)\n const lines = readme.split(\"\\n\");\n let collecting = false;\n const paragraphLines: string[] = [];\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed.startsWith(\"#\")) {\n if (collecting && paragraphLines.length > 0) break;\n collecting = true;\n continue;\n }\n if (collecting && trimmed === \"\" && paragraphLines.length > 0) break;\n if (collecting && trimmed !== \"\" && !trimmed.startsWith(\"![\") && !trimmed.startsWith(\"[![\")) {\n paragraphLines.push(trimmed);\n }\n if (!collecting && trimmed !== \"\" && !trimmed.startsWith(\"[\") && !trimmed.startsWith(\"!\") && !trimmed.startsWith(\"[![\")) {\n paragraphLines.push(trimmed);\n collecting = true;\n }\n }\n\n return paragraphLines.length > 0 ? paragraphLines.join(\" \") : null;\n}\n\nexport function extractDemoUrl(readme: string | null): string | null {\n if (!readme) return null;\n // Look for common demo URL patterns\n const patterns = [\n /(?:demo|live|website|site|url|link)[\\s:]*\\[?[^\\]]*\\]?\\(?(https?:\\/\\/[^\\s)]+)/i,\n /\\[(?:demo|live|website|try it)\\]\\((https?:\\/\\/[^\\s)]+)\\)/i,\n /(https?:\\/\\/[^\\s)]+\\.(?:vercel|netlify|pages\\.dev|herokuapp|railway)\\.app[^\\s)]*)/i,\n ];\n for (const pattern of patterns) {\n const match = readme.match(pattern);\n if (match?.[1]) return match[1];\n }\n return null;\n}\n","import chalk from \"chalk\";\n\nexport const logger = {\n info(msg: string) {\n console.log(chalk.cyan(`-- ${msg}`));\n },\n success(msg: string) {\n console.log(chalk.green(`-- ${msg}`));\n },\n warn(msg: string) {\n console.log(chalk.yellow(`-- ${msg}`));\n },\n error(msg: string) {\n console.error(chalk.red(`-- ${msg}`));\n },\n plain(msg: string) {\n console.log(msg);\n },\n blank() {\n console.log();\n },\n header(msg: string) {\n console.log();\n console.log(chalk.bold(msg));\n console.log();\n },\n table(rows: string[][]) {\n if (rows.length === 0) return;\n const colWidths = rows[0].map((_, colIdx) =>\n Math.max(...rows.map((row) => (row[colIdx] || \"\").length))\n );\n for (const row of rows) {\n const line = row\n .map((cell, i) => (cell || \"\").padEnd(colWidths[i] + 2))\n .join(\"\");\n console.log(` ${line}`);\n }\n },\n};\n"],"mappings":"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","names":["glob","glob"]}
@@ -1,7 +1,104 @@
1
+ // src/spec/schema.ts
2
+ import { z } from "zod";
3
+ var sectionIdSchema = z.enum([
4
+ "hero",
5
+ "projects",
6
+ "skills",
7
+ "about",
8
+ "timeline",
9
+ "blog",
10
+ "metrics",
11
+ "contact"
12
+ ]);
13
+ var engineTypeSchema = z.enum(["claude", "codex", "v0"]);
14
+ var deployPlatformSchema = z.enum(["cloudflare", "vercel", "local"]);
15
+ var styleConfigSchema = z.object({
16
+ theme: z.enum(["dark-minimal", "light-clean", "monochrome", "custom"]),
17
+ accentColor: z.string(),
18
+ font: z.string(),
19
+ animationLevel: z.enum(["none", "subtle", "moderate"])
20
+ });
21
+ var projectMetaSchema = z.object({
22
+ id: z.string(),
23
+ name: z.string(),
24
+ localPath: z.string(),
25
+ description: z.string(),
26
+ techStack: z.array(z.string()),
27
+ languages: z.record(z.number()),
28
+ firstCommitDate: z.string(),
29
+ lastCommitDate: z.string(),
30
+ totalCommits: z.number().int().nonnegative(),
31
+ remoteUrl: z.string().nullable(),
32
+ demoUrl: z.string().nullable(),
33
+ readmeContent: z.string().nullable(),
34
+ lastScannedCommit: z.string()
35
+ });
36
+ var projectCaseStudySchema = z.object({
37
+ featured: z.boolean(),
38
+ audience: z.string().nullable(),
39
+ problem: z.string().nullable(),
40
+ solution: z.string().nullable(),
41
+ impact: z.string().nullable(),
42
+ evidence: z.array(z.string()),
43
+ screenshots: z.array(z.string())
44
+ });
45
+ var ownerInfoSchema = z.object({
46
+ name: z.string(),
47
+ tagline: z.string(),
48
+ bio: z.union([z.literal("auto"), z.string()]),
49
+ photoUrl: z.string().nullable(),
50
+ social: z.object({
51
+ github: z.string().optional(),
52
+ twitter: z.string().optional(),
53
+ linkedin: z.string().optional(),
54
+ blog: z.string().optional(),
55
+ email: z.string().optional()
56
+ })
57
+ });
58
+ var projectEntrySchema = projectMetaSchema.extend({
59
+ included: z.boolean(),
60
+ overrideDescription: z.string().nullable(),
61
+ showSourceLink: z.boolean(),
62
+ role: z.enum(["solo", "lead", "contributor"]),
63
+ trackedProjectPaths: z.array(z.string()),
64
+ metrics: z.object({
65
+ users: z.string().optional(),
66
+ revenue: z.string().optional(),
67
+ downloads: z.string().optional(),
68
+ stars: z.number().int().optional(),
69
+ custom: z.record(z.string()).optional()
70
+ }),
71
+ caseStudy: projectCaseStudySchema,
72
+ children: z.array(projectMetaSchema).optional()
73
+ });
74
+ var deployConfigSchema = z.object({
75
+ platform: deployPlatformSchema,
76
+ projectName: z.string(),
77
+ customDomain: z.string().optional(),
78
+ url: z.string().optional()
79
+ });
80
+ var shipfolioSpecSchema = z.object({
81
+ version: z.string(),
82
+ generatedAt: z.string(),
83
+ engine: engineTypeSchema,
84
+ framework: z.literal("next"),
85
+ style: styleConfigSchema,
86
+ owner: ownerInfoSchema,
87
+ projects: z.array(projectEntrySchema),
88
+ sections: z.array(sectionIdSchema),
89
+ deploy: deployConfigSchema
90
+ });
91
+ var shipfolioConfigSchema = shipfolioSpecSchema.extend({
92
+ sitePath: z.string()
93
+ });
94
+ function parseShipfolioSpec(input) {
95
+ return shipfolioSpecSchema.parse(input);
96
+ }
97
+
1
98
  // src/spec/builder.ts
2
99
  function buildSpec(interview) {
3
- return {
4
- version: "1.0.0",
100
+ return parseShipfolioSpec({
101
+ version: "1.1.0",
5
102
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6
103
  engine: interview.engine,
7
104
  framework: "next",
@@ -14,7 +111,7 @@ function buildSpec(interview) {
14
111
  projectName: interview.deploy.projectName,
15
112
  customDomain: interview.deploy.customDomain
16
113
  }
17
- };
114
+ });
18
115
  }
19
116
  export {
20
117
  buildSpec
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/spec/builder.ts"],"sourcesContent":["import type { ShipfolioSpec } from \"./schema.js\";\nimport type { InterviewResult } from \"../interviewer/index.js\";\n\nexport function buildSpec(interview: InterviewResult): ShipfolioSpec {\n return {\n version: \"1.0.0\",\n generatedAt: new Date().toISOString(),\n engine: interview.engine,\n framework: \"next\",\n style: interview.style,\n owner: interview.owner,\n projects: interview.projects,\n sections: interview.sections,\n deploy: {\n platform: interview.deploy.platform,\n projectName: interview.deploy.projectName,\n customDomain: interview.deploy.customDomain,\n },\n };\n}\n"],"mappings":";AAGO,SAAS,UAAU,WAA2C;AACnE,SAAO;AAAA,IACL,SAAS;AAAA,IACT,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,QAAQ,UAAU;AAAA,IAClB,WAAW;AAAA,IACX,OAAO,UAAU;AAAA,IACjB,OAAO,UAAU;AAAA,IACjB,UAAU,UAAU;AAAA,IACpB,UAAU,UAAU;AAAA,IACpB,QAAQ;AAAA,MACN,UAAU,UAAU,OAAO;AAAA,MAC3B,aAAa,UAAU,OAAO;AAAA,MAC9B,cAAc,UAAU,OAAO;AAAA,IACjC;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/spec/schema.ts","../../../src/spec/builder.ts"],"sourcesContent":["import { z } from \"zod\";\n\nexport const sectionIdSchema = z.enum([\n \"hero\",\n \"projects\",\n \"skills\",\n \"about\",\n \"timeline\",\n \"blog\",\n \"metrics\",\n \"contact\",\n]);\nexport type SectionId = z.infer<typeof sectionIdSchema>;\n\nexport const engineTypeSchema = z.enum([\"claude\", \"codex\", \"v0\"]);\nexport type EngineType = z.infer<typeof engineTypeSchema>;\n\nexport const deployPlatformSchema = z.enum([\"cloudflare\", \"vercel\", \"local\"]);\nexport type DeployPlatform = z.infer<typeof deployPlatformSchema>;\n\nexport const styleConfigSchema = z.object({\n theme: z.enum([\"dark-minimal\", \"light-clean\", \"monochrome\", \"custom\"]),\n accentColor: z.string(),\n font: z.string(),\n animationLevel: z.enum([\"none\", \"subtle\", \"moderate\"]),\n});\nexport type StyleConfig = z.infer<typeof styleConfigSchema>;\n\nexport const projectMetaSchema = z.object({\n id: z.string(),\n name: z.string(),\n localPath: z.string(),\n description: z.string(),\n techStack: z.array(z.string()),\n languages: z.record(z.number()),\n firstCommitDate: z.string(),\n lastCommitDate: z.string(),\n totalCommits: z.number().int().nonnegative(),\n remoteUrl: z.string().nullable(),\n demoUrl: z.string().nullable(),\n readmeContent: z.string().nullable(),\n lastScannedCommit: z.string(),\n});\nexport type ProjectMeta = z.infer<typeof projectMetaSchema>;\n\nexport const projectCaseStudySchema = z.object({\n featured: z.boolean(),\n audience: z.string().nullable(),\n problem: z.string().nullable(),\n solution: z.string().nullable(),\n impact: z.string().nullable(),\n evidence: z.array(z.string()),\n screenshots: z.array(z.string()),\n});\nexport type ProjectCaseStudy = z.infer<typeof projectCaseStudySchema>;\n\nexport const ownerInfoSchema = z.object({\n name: z.string(),\n tagline: z.string(),\n bio: z.union([z.literal(\"auto\"), z.string()]),\n photoUrl: z.string().nullable(),\n social: z.object({\n github: z.string().optional(),\n twitter: z.string().optional(),\n linkedin: z.string().optional(),\n blog: z.string().optional(),\n email: z.string().optional(),\n }),\n});\nexport type OwnerInfo = z.infer<typeof ownerInfoSchema>;\n\nexport const projectEntrySchema: z.ZodType<{\n id: string;\n name: string;\n localPath: string;\n description: string;\n techStack: string[];\n languages: Record<string, number>;\n firstCommitDate: string;\n lastCommitDate: string;\n totalCommits: number;\n remoteUrl: string | null;\n demoUrl: string | null;\n readmeContent: string | null;\n lastScannedCommit: string;\n included: boolean;\n overrideDescription: string | null;\n showSourceLink: boolean;\n role: \"solo\" | \"lead\" | \"contributor\";\n trackedProjectPaths: string[];\n metrics: {\n users?: string | undefined;\n revenue?: string | undefined;\n downloads?: string | undefined;\n stars?: number | undefined;\n custom?: Record<string, string> | undefined;\n };\n caseStudy: ProjectCaseStudy;\n children?: ProjectMeta[] | undefined;\n}> = projectMetaSchema.extend({\n included: z.boolean(),\n overrideDescription: z.string().nullable(),\n showSourceLink: z.boolean(),\n role: z.enum([\"solo\", \"lead\", \"contributor\"]),\n trackedProjectPaths: z.array(z.string()),\n metrics: z.object({\n users: z.string().optional(),\n revenue: z.string().optional(),\n downloads: z.string().optional(),\n stars: z.number().int().optional(),\n custom: z.record(z.string()).optional(),\n }),\n caseStudy: projectCaseStudySchema,\n children: z.array(projectMetaSchema).optional(),\n});\nexport type ProjectEntry = z.infer<typeof projectEntrySchema>;\n\nexport const deployConfigSchema = z.object({\n platform: deployPlatformSchema,\n projectName: z.string(),\n customDomain: z.string().optional(),\n url: z.string().optional(),\n});\nexport type DeployConfig = z.infer<typeof deployConfigSchema>;\n\nexport const shipfolioSpecSchema = z.object({\n version: z.string(),\n generatedAt: z.string(),\n engine: engineTypeSchema,\n framework: z.literal(\"next\"),\n style: styleConfigSchema,\n owner: ownerInfoSchema,\n projects: z.array(projectEntrySchema),\n sections: z.array(sectionIdSchema),\n deploy: deployConfigSchema,\n});\nexport type ShipfolioSpec = z.infer<typeof shipfolioSpecSchema>;\n\nexport const shipfolioConfigSchema = shipfolioSpecSchema.extend({\n sitePath: z.string(),\n});\nexport type ShipfolioConfig = z.infer<typeof shipfolioConfigSchema>;\n\nexport function parseShipfolioSpec(input: unknown): ShipfolioSpec {\n return shipfolioSpecSchema.parse(input);\n}\n\nexport function parseShipfolioConfig(input: unknown): ShipfolioConfig {\n return shipfolioConfigSchema.parse(input);\n}\n\nexport interface ProjectUpdate {\n project: ProjectEntry;\n newCommits: number;\n readmeChanged: boolean;\n depsChanged: boolean;\n changedPaths: string[];\n removedPaths: string[];\n}\n\nexport interface SiteDiff {\n newProjects: ProjectMeta[];\n updatedProjects: ProjectUpdate[];\n removedProjects: ProjectEntry[];\n unchangedProjects: ProjectEntry[];\n}\n","import { parseShipfolioSpec, type ShipfolioSpec } from \"./schema.js\";\nimport type { InterviewResult } from \"../interviewer/index.js\";\n\nexport function buildSpec(interview: InterviewResult): ShipfolioSpec {\n return parseShipfolioSpec({\n version: \"1.1.0\",\n generatedAt: new Date().toISOString(),\n engine: interview.engine,\n framework: \"next\",\n style: interview.style,\n owner: interview.owner,\n projects: interview.projects,\n sections: interview.sections,\n deploy: {\n platform: interview.deploy.platform,\n projectName: interview.deploy.projectName,\n customDomain: interview.deploy.customDomain,\n },\n });\n}\n"],"mappings":"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","names":[]}
@@ -1,32 +1,196 @@
1
+ // src/spec/project-utils.ts
2
+ function slugify(value) {
3
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4
+ }
5
+ function uniqueStrings(values) {
6
+ return [...new Set(values.filter(Boolean))];
7
+ }
8
+ function defaultCaseStudy(featured = false) {
9
+ return {
10
+ featured,
11
+ audience: null,
12
+ problem: null,
13
+ solution: null,
14
+ impact: null,
15
+ evidence: [],
16
+ screenshots: []
17
+ };
18
+ }
19
+ function buildTrackingSignature(projects) {
20
+ return projects.map((project) => `${project.localPath}:${project.lastScannedCommit || "none"}`).sort().join("|");
21
+ }
22
+ function buildMergedProjectMeta(projects, name) {
23
+ const techStack = uniqueStrings(projects.flatMap((project) => project.techStack));
24
+ const languages = {};
25
+ for (const project of projects) {
26
+ for (const [lang, count] of Object.entries(project.languages)) {
27
+ languages[lang] = (languages[lang] || 0) + count;
28
+ }
29
+ }
30
+ const firstDates = projects.map((project) => project.firstCommitDate).filter(Boolean).sort();
31
+ const lastDates = projects.map((project) => project.lastCommitDate).filter(Boolean).sort();
32
+ const totalCommits = projects.reduce(
33
+ (sum, project) => sum + project.totalCommits,
34
+ 0
35
+ );
36
+ const remoteUrl = projects.find((project) => project.remoteUrl)?.remoteUrl || null;
37
+ const demoUrl = projects.find((project) => project.demoUrl)?.demoUrl || null;
38
+ const readmeParts = projects.filter((project) => project.readmeContent).map((project) => `--- ${project.name} ---
39
+ ${project.readmeContent}`);
40
+ const readmeContent = readmeParts.length > 0 ? readmeParts.join("\n\n") : null;
41
+ const descriptions = projects.map((project) => project.description).filter(Boolean);
42
+ return {
43
+ id: slugify(name),
44
+ name,
45
+ localPath: projects[0]?.localPath || "",
46
+ description: descriptions.join(" | "),
47
+ techStack,
48
+ languages,
49
+ firstCommitDate: firstDates[0] || "",
50
+ lastCommitDate: lastDates[lastDates.length - 1] || "",
51
+ totalCommits,
52
+ remoteUrl,
53
+ demoUrl,
54
+ readmeContent,
55
+ lastScannedCommit: buildTrackingSignature(projects)
56
+ };
57
+ }
58
+ function getTrackedProjectPaths(project) {
59
+ if (project.trackedProjectPaths && project.trackedProjectPaths.length > 0) {
60
+ return uniqueStrings(project.trackedProjectPaths);
61
+ }
62
+ if (project.children && project.children.length > 0) {
63
+ return uniqueStrings(project.children.map((child) => child.localPath));
64
+ }
65
+ return project.localPath ? [project.localPath] : [];
66
+ }
67
+ function createProjectEntry(meta, overrides = {}) {
68
+ const baseMetrics = overrides.metrics || {};
69
+ const baseCaseStudy = overrides.caseStudy ? {
70
+ ...defaultCaseStudy(overrides.caseStudy.featured),
71
+ ...overrides.caseStudy,
72
+ evidence: overrides.caseStudy.evidence || [],
73
+ screenshots: overrides.caseStudy.screenshots || []
74
+ } : defaultCaseStudy(false);
75
+ return {
76
+ ...meta,
77
+ included: overrides.included ?? true,
78
+ overrideDescription: overrides.overrideDescription ?? null,
79
+ showSourceLink: overrides.showSourceLink ?? !!meta.remoteUrl,
80
+ role: overrides.role ?? "solo",
81
+ trackedProjectPaths: overrides.trackedProjectPaths && overrides.trackedProjectPaths.length > 0 ? uniqueStrings(overrides.trackedProjectPaths) : [meta.localPath],
82
+ metrics: {
83
+ ...baseMetrics,
84
+ custom: baseMetrics.custom || void 0
85
+ },
86
+ caseStudy: baseCaseStudy,
87
+ children: overrides.children
88
+ };
89
+ }
90
+
1
91
  // src/spec/diff.ts
2
92
  function computeDiff(oldConfig, newScan) {
3
- const oldProjectMap = new Map(
4
- oldConfig.projects.map((p) => [p.localPath, p])
5
- );
6
93
  const newProjectMap = new Map(newScan.map((p) => [p.localPath, p]));
94
+ const trackedOldPaths = /* @__PURE__ */ new Set();
7
95
  const newProjects = [];
8
96
  const updatedProjects = [];
9
97
  const removedProjects = [];
10
98
  const unchangedProjects = [];
11
- for (const [path, meta] of newProjectMap) {
12
- const oldProject = oldProjectMap.get(path);
13
- if (!oldProject) {
14
- newProjects.push(meta);
15
- } else if (meta.lastScannedCommit !== oldProject.lastScannedCommit) {
16
- const newCommits = meta.totalCommits - oldProject.totalCommits;
99
+ for (const oldProject of oldConfig.projects) {
100
+ const trackedPaths = getTrackedProjectPaths(oldProject);
101
+ for (const trackedPath of trackedPaths) {
102
+ trackedOldPaths.add(trackedPath);
103
+ }
104
+ const currentMatches = trackedPaths.map((path) => newProjectMap.get(path)).filter((project) => Boolean(project));
105
+ const removedPaths = trackedPaths.filter((path) => !newProjectMap.has(path));
106
+ if (currentMatches.length === 0) {
107
+ removedProjects.push(oldProject);
108
+ continue;
109
+ }
110
+ if (trackedPaths.length > 1) {
111
+ const mergedMeta = buildMergedProjectMeta(currentMatches, oldProject.name);
112
+ const oldChildrenByPath = new Map(
113
+ (oldProject.children || []).map((child) => [child.localPath, child])
114
+ );
115
+ const oldTrackingSignature = oldProject.children && oldProject.children.length > 0 ? buildTrackingSignature(oldProject.children) : oldProject.lastScannedCommit;
116
+ let readmeChanged = removedPaths.length > 0;
117
+ let depsChanged = removedPaths.length > 0;
118
+ let newCommits = 0;
119
+ for (const currentProject2 of currentMatches) {
120
+ const oldChild = oldChildrenByPath.get(currentProject2.localPath);
121
+ if (!oldChild) {
122
+ readmeChanged = true;
123
+ depsChanged = true;
124
+ newCommits += currentProject2.totalCommits;
125
+ continue;
126
+ }
127
+ if (currentProject2.readmeContent !== oldChild.readmeContent) {
128
+ readmeChanged = true;
129
+ }
130
+ if (JSON.stringify(currentProject2.techStack) !== JSON.stringify(oldChild.techStack)) {
131
+ depsChanged = true;
132
+ }
133
+ newCommits += Math.max(
134
+ currentProject2.totalCommits - oldChild.totalCommits,
135
+ 0
136
+ );
137
+ }
138
+ const changedPaths = currentMatches.filter((currentProject2) => {
139
+ const oldChild = oldChildrenByPath.get(currentProject2.localPath);
140
+ if (!oldChild) return true;
141
+ return currentProject2.lastScannedCommit !== oldChild.lastScannedCommit;
142
+ }).map((project) => project.localPath);
143
+ const hasUpdate = removedPaths.length > 0 || trackedPaths.length !== currentMatches.length || mergedMeta.lastScannedCommit !== oldTrackingSignature || readmeChanged || depsChanged;
144
+ if (hasUpdate) {
145
+ updatedProjects.push({
146
+ project: createProjectEntry(mergedMeta, {
147
+ included: oldProject.included,
148
+ overrideDescription: oldProject.overrideDescription,
149
+ showSourceLink: oldProject.showSourceLink,
150
+ role: oldProject.role,
151
+ metrics: oldProject.metrics,
152
+ caseStudy: oldProject.caseStudy,
153
+ trackedProjectPaths: currentMatches.map((project) => project.localPath),
154
+ children: currentMatches
155
+ }),
156
+ newCommits,
157
+ readmeChanged,
158
+ depsChanged,
159
+ changedPaths,
160
+ removedPaths
161
+ });
162
+ } else {
163
+ unchangedProjects.push(oldProject);
164
+ }
165
+ continue;
166
+ }
167
+ const currentProject = currentMatches[0];
168
+ if (currentProject.lastScannedCommit !== oldProject.lastScannedCommit) {
169
+ const newCommits = currentProject.totalCommits - oldProject.totalCommits;
17
170
  updatedProjects.push({
18
- project: meta,
171
+ project: createProjectEntry(currentProject, {
172
+ included: oldProject.included,
173
+ overrideDescription: oldProject.overrideDescription,
174
+ showSourceLink: oldProject.showSourceLink,
175
+ role: oldProject.role,
176
+ metrics: oldProject.metrics,
177
+ caseStudy: oldProject.caseStudy,
178
+ trackedProjectPaths: trackedPaths,
179
+ children: oldProject.children
180
+ }),
19
181
  newCommits: Math.max(newCommits, 0),
20
- readmeChanged: meta.readmeContent !== oldProject.readmeContent,
21
- depsChanged: JSON.stringify(meta.techStack) !== JSON.stringify(oldProject.techStack)
182
+ readmeChanged: currentProject.readmeContent !== oldProject.readmeContent,
183
+ depsChanged: JSON.stringify(currentProject.techStack) !== JSON.stringify(oldProject.techStack),
184
+ changedPaths: [currentProject.localPath],
185
+ removedPaths
22
186
  });
23
187
  } else {
24
188
  unchangedProjects.push(oldProject);
25
189
  }
26
190
  }
27
- for (const [path, project] of oldProjectMap) {
28
- if (!newProjectMap.has(path)) {
29
- removedProjects.push(project);
191
+ for (const [path, meta] of newProjectMap) {
192
+ if (!trackedOldPaths.has(path)) {
193
+ newProjects.push(meta);
30
194
  }
31
195
  }
32
196
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/spec/diff.ts"],"sourcesContent":["import type {\n ProjectMeta,\n ProjectEntry,\n ShipfolioConfig,\n SiteDiff,\n ProjectUpdate,\n} from \"./schema.js\";\n\nexport function computeDiff(\n oldConfig: ShipfolioConfig,\n newScan: ProjectMeta[]\n): SiteDiff {\n const oldProjectMap = new Map(\n oldConfig.projects.map((p) => [p.localPath, p])\n );\n const newProjectMap = new Map(newScan.map((p) => [p.localPath, p]));\n\n const newProjects: ProjectMeta[] = [];\n const updatedProjects: ProjectUpdate[] = [];\n const removedProjects: ProjectEntry[] = [];\n const unchangedProjects: ProjectEntry[] = [];\n\n // Check new and updated\n for (const [path, meta] of newProjectMap) {\n const oldProject = oldProjectMap.get(path);\n if (!oldProject) {\n newProjects.push(meta);\n } else if (meta.lastScannedCommit !== oldProject.lastScannedCommit) {\n const newCommits = meta.totalCommits - oldProject.totalCommits;\n updatedProjects.push({\n project: meta,\n newCommits: Math.max(newCommits, 0),\n readmeChanged: meta.readmeContent !== oldProject.readmeContent,\n depsChanged:\n JSON.stringify(meta.techStack) !==\n JSON.stringify(oldProject.techStack),\n });\n } else {\n unchangedProjects.push(oldProject);\n }\n }\n\n // Check removed\n for (const [path, project] of oldProjectMap) {\n if (!newProjectMap.has(path)) {\n removedProjects.push(project);\n }\n }\n\n return {\n newProjects,\n updatedProjects,\n removedProjects,\n unchangedProjects,\n };\n}\n"],"mappings":";AAQO,SAAS,YACd,WACA,SACU;AACV,QAAM,gBAAgB,IAAI;AAAA,IACxB,UAAU,SAAS,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,CAAC;AAAA,EAChD;AACA,QAAM,gBAAgB,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC;AAElE,QAAM,cAA6B,CAAC;AACpC,QAAM,kBAAmC,CAAC;AAC1C,QAAM,kBAAkC,CAAC;AACzC,QAAM,oBAAoC,CAAC;AAG3C,aAAW,CAAC,MAAM,IAAI,KAAK,eAAe;AACxC,UAAM,aAAa,cAAc,IAAI,IAAI;AACzC,QAAI,CAAC,YAAY;AACf,kBAAY,KAAK,IAAI;AAAA,IACvB,WAAW,KAAK,sBAAsB,WAAW,mBAAmB;AAClE,YAAM,aAAa,KAAK,eAAe,WAAW;AAClD,sBAAgB,KAAK;AAAA,QACnB,SAAS;AAAA,QACT,YAAY,KAAK,IAAI,YAAY,CAAC;AAAA,QAClC,eAAe,KAAK,kBAAkB,WAAW;AAAA,QACjD,aACE,KAAK,UAAU,KAAK,SAAS,MAC7B,KAAK,UAAU,WAAW,SAAS;AAAA,MACvC,CAAC;AAAA,IACH,OAAO;AACL,wBAAkB,KAAK,UAAU;AAAA,IACnC;AAAA,EACF;AAGA,aAAW,CAAC,MAAM,OAAO,KAAK,eAAe;AAC3C,QAAI,CAAC,cAAc,IAAI,IAAI,GAAG;AAC5B,sBAAgB,KAAK,OAAO;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/spec/project-utils.ts","../../../src/spec/diff.ts"],"sourcesContent":["import type {\n ProjectCaseStudy,\n ProjectEntry,\n ProjectMeta,\n} from \"./schema.js\";\n\nfunction slugify(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n\nfunction uniqueStrings(values: string[]): string[] {\n return [...new Set(values.filter(Boolean))];\n}\n\nexport function defaultCaseStudy(featured = false): ProjectCaseStudy {\n return {\n featured,\n audience: null,\n problem: null,\n solution: null,\n impact: null,\n evidence: [],\n screenshots: [],\n };\n}\n\nexport function buildTrackingSignature(projects: ProjectMeta[]): string {\n return projects\n .map((project) => `${project.localPath}:${project.lastScannedCommit || \"none\"}`)\n .sort()\n .join(\"|\");\n}\n\nexport function buildMergedProjectMeta(\n projects: ProjectMeta[],\n name: string\n): ProjectMeta {\n const techStack = uniqueStrings(projects.flatMap((project) => project.techStack));\n\n const languages: Record<string, number> = {};\n for (const project of projects) {\n for (const [lang, count] of Object.entries(project.languages)) {\n languages[lang] = (languages[lang] || 0) + count;\n }\n }\n\n const firstDates = projects\n .map((project) => project.firstCommitDate)\n .filter(Boolean)\n .sort();\n const lastDates = projects\n .map((project) => project.lastCommitDate)\n .filter(Boolean)\n .sort();\n\n const totalCommits = projects.reduce(\n (sum, project) => sum + project.totalCommits,\n 0\n );\n\n const remoteUrl =\n projects.find((project) => project.remoteUrl)?.remoteUrl || null;\n const demoUrl =\n projects.find((project) => project.demoUrl)?.demoUrl || null;\n\n const readmeParts = projects\n .filter((project) => project.readmeContent)\n .map((project) => `--- ${project.name} ---\\n${project.readmeContent}`);\n const readmeContent = readmeParts.length > 0 ? readmeParts.join(\"\\n\\n\") : null;\n\n const descriptions = projects\n .map((project) => project.description)\n .filter(Boolean);\n\n return {\n id: slugify(name),\n name,\n localPath: projects[0]?.localPath || \"\",\n description: descriptions.join(\" | \"),\n techStack,\n languages,\n firstCommitDate: firstDates[0] || \"\",\n lastCommitDate: lastDates[lastDates.length - 1] || \"\",\n totalCommits,\n remoteUrl,\n demoUrl,\n readmeContent,\n lastScannedCommit: buildTrackingSignature(projects),\n };\n}\n\nexport function getTrackedProjectPaths(\n project: Pick<ProjectMeta, \"localPath\"> & {\n trackedProjectPaths?: string[];\n children?: ProjectMeta[];\n }\n): string[] {\n if (project.trackedProjectPaths && project.trackedProjectPaths.length > 0) {\n return uniqueStrings(project.trackedProjectPaths);\n }\n if (project.children && project.children.length > 0) {\n return uniqueStrings(project.children.map((child) => child.localPath));\n }\n return project.localPath ? [project.localPath] : [];\n}\n\nexport function createProjectEntry(\n meta: ProjectMeta,\n overrides: Partial<ProjectEntry> = {}\n): ProjectEntry {\n const baseMetrics = overrides.metrics || {};\n const baseCaseStudy = overrides.caseStudy\n ? {\n ...defaultCaseStudy(overrides.caseStudy.featured),\n ...overrides.caseStudy,\n evidence: overrides.caseStudy.evidence || [],\n screenshots: overrides.caseStudy.screenshots || [],\n }\n : defaultCaseStudy(false);\n\n return {\n ...meta,\n included: overrides.included ?? true,\n overrideDescription: overrides.overrideDescription ?? null,\n showSourceLink: overrides.showSourceLink ?? !!meta.remoteUrl,\n role: overrides.role ?? \"solo\",\n trackedProjectPaths:\n overrides.trackedProjectPaths && overrides.trackedProjectPaths.length > 0\n ? uniqueStrings(overrides.trackedProjectPaths)\n : [meta.localPath],\n metrics: {\n ...baseMetrics,\n custom: baseMetrics.custom || undefined,\n },\n caseStudy: baseCaseStudy,\n children: overrides.children,\n };\n}\n","import type {\n ProjectMeta,\n ProjectEntry,\n ShipfolioConfig,\n SiteDiff,\n ProjectUpdate,\n} from \"./schema.js\";\nimport {\n buildMergedProjectMeta,\n buildTrackingSignature,\n createProjectEntry,\n getTrackedProjectPaths,\n} from \"./project-utils.js\";\n\nexport function computeDiff(\n oldConfig: ShipfolioConfig,\n newScan: ProjectMeta[]\n): SiteDiff {\n const newProjectMap = new Map(newScan.map((p) => [p.localPath, p]));\n const trackedOldPaths = new Set<string>();\n\n const newProjects: ProjectMeta[] = [];\n const updatedProjects: ProjectUpdate[] = [];\n const removedProjects: ProjectEntry[] = [];\n const unchangedProjects: ProjectEntry[] = [];\n\n for (const oldProject of oldConfig.projects) {\n const trackedPaths = getTrackedProjectPaths(oldProject);\n for (const trackedPath of trackedPaths) {\n trackedOldPaths.add(trackedPath);\n }\n\n const currentMatches = trackedPaths\n .map((path) => newProjectMap.get(path))\n .filter((project): project is ProjectMeta => Boolean(project));\n const removedPaths = trackedPaths.filter((path) => !newProjectMap.has(path));\n\n if (currentMatches.length === 0) {\n removedProjects.push(oldProject);\n continue;\n }\n\n if (trackedPaths.length > 1) {\n const mergedMeta = buildMergedProjectMeta(currentMatches, oldProject.name);\n const oldChildrenByPath = new Map(\n (oldProject.children || []).map((child) => [child.localPath, child])\n );\n const oldTrackingSignature =\n oldProject.children && oldProject.children.length > 0\n ? buildTrackingSignature(oldProject.children)\n : oldProject.lastScannedCommit;\n\n let readmeChanged = removedPaths.length > 0;\n let depsChanged = removedPaths.length > 0;\n let newCommits = 0;\n\n for (const currentProject of currentMatches) {\n const oldChild = oldChildrenByPath.get(currentProject.localPath);\n if (!oldChild) {\n readmeChanged = true;\n depsChanged = true;\n newCommits += currentProject.totalCommits;\n continue;\n }\n\n if (currentProject.readmeContent !== oldChild.readmeContent) {\n readmeChanged = true;\n }\n if (\n JSON.stringify(currentProject.techStack) !==\n JSON.stringify(oldChild.techStack)\n ) {\n depsChanged = true;\n }\n newCommits += Math.max(\n currentProject.totalCommits - oldChild.totalCommits,\n 0\n );\n }\n\n const changedPaths = currentMatches\n .filter((currentProject) => {\n const oldChild = oldChildrenByPath.get(currentProject.localPath);\n if (!oldChild) return true;\n return currentProject.lastScannedCommit !== oldChild.lastScannedCommit;\n })\n .map((project) => project.localPath);\n\n const hasUpdate =\n removedPaths.length > 0 ||\n trackedPaths.length !== currentMatches.length ||\n mergedMeta.lastScannedCommit !== oldTrackingSignature ||\n readmeChanged ||\n depsChanged;\n\n if (hasUpdate) {\n updatedProjects.push({\n project: createProjectEntry(mergedMeta, {\n included: oldProject.included,\n overrideDescription: oldProject.overrideDescription,\n showSourceLink: oldProject.showSourceLink,\n role: oldProject.role,\n metrics: oldProject.metrics,\n caseStudy: oldProject.caseStudy,\n trackedProjectPaths: currentMatches.map((project) => project.localPath),\n children: currentMatches,\n }),\n newCommits,\n readmeChanged,\n depsChanged,\n changedPaths,\n removedPaths,\n });\n } else {\n unchangedProjects.push(oldProject);\n }\n\n continue;\n }\n\n const currentProject = currentMatches[0];\n if (currentProject.lastScannedCommit !== oldProject.lastScannedCommit) {\n const newCommits = currentProject.totalCommits - oldProject.totalCommits;\n updatedProjects.push({\n project: createProjectEntry(currentProject, {\n included: oldProject.included,\n overrideDescription: oldProject.overrideDescription,\n showSourceLink: oldProject.showSourceLink,\n role: oldProject.role,\n metrics: oldProject.metrics,\n caseStudy: oldProject.caseStudy,\n trackedProjectPaths: trackedPaths,\n children: oldProject.children,\n }),\n newCommits: Math.max(newCommits, 0),\n readmeChanged: currentProject.readmeContent !== oldProject.readmeContent,\n depsChanged:\n JSON.stringify(currentProject.techStack) !==\n JSON.stringify(oldProject.techStack),\n changedPaths: [currentProject.localPath],\n removedPaths,\n });\n } else {\n unchangedProjects.push(oldProject);\n }\n }\n\n for (const [path, meta] of newProjectMap) {\n if (!trackedOldPaths.has(path)) {\n newProjects.push(meta);\n }\n }\n\n return {\n newProjects,\n updatedProjects,\n removedProjects,\n unchangedProjects,\n };\n}\n"],"mappings":"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","names":["currentProject"]}
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "shipfolio",
3
- "version": "1.0.10",
3
+ "version": "1.1.0",
4
4
  "description": "Generate and deploy your personal portfolio site from local projects using AI",
5
5
  "type": "module",
6
6
  "bin": {
7
- "shipfolio": "./bin/cli.js"
7
+ "shipfolio": "bin/cli.js"
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsup",
11
11
  "dev": "tsup --watch",
12
- "prepublishOnly": "npm run build"
12
+ "typecheck": "tsc --noEmit",
13
+ "prepublishOnly": "npm run typecheck && npm run build"
13
14
  },
14
15
  "files": [
15
16
  "bin",
@@ -35,7 +36,8 @@
35
36
  "ora": "^8.2.0",
36
37
  "playwright": "^1.50.0",
37
38
  "serve-handler": "^6.1.6",
38
- "simple-git": "^3.27.0"
39
+ "simple-git": "^3.27.0",
40
+ "zod": "^3.25.76"
39
41
  },
40
42
  "devDependencies": {
41
43
  "@types/node": "^22.12.0",
@@ -46,8 +46,20 @@ For each project in the spec:
46
46
  - Write a 2-3 sentence narrative description based on the README content and tech stack
47
47
  - Focus on what it does and why it matters
48
48
  - If user provided an override description, use that instead
49
+ - If caseStudy fields are present, treat them as the highest-priority source of truth
49
50
  - Maintain consistent voice across all descriptions
50
51
 
52
+ For featured projects:
53
+ - Present them more prominently than non-featured projects
54
+ - Use the problem, solution, impact, and evidence fields to shape richer case-study content
55
+ - Prefer concrete proof points over generic claims
56
+
57
+ General content rules:
58
+ - Do not fabricate blog posts, metrics, user numbers, screenshots, or timeline events that are not present in the spec
59
+ - If a section lacks real data, keep it concise and derive it only from the provided projects and owner info
60
+ - Use screenshot URLs only when they are provided in the spec
61
+ - Make the strongest 1-2 featured projects carry the page narrative; keep the rest compact
62
+
51
63
  For the bio (if set to "auto"):
52
64
  - Generate a professional, authentic bio based on the project portfolio
53
65
  - Emphasize shipping velocity and breadth
@@ -88,6 +100,8 @@ src/components/project-grid.tsx
88
100
  src/components/skills.tsx
89
101
  src/components/about.tsx
90
102
  src/components/timeline.tsx
103
+ src/components/blog.tsx
104
+ src/components/metrics.tsx
91
105
  src/components/contact.tsx
92
106
  src/components/navigation.tsx
93
107
  src/components/footer.tsx
@@ -106,3 +120,4 @@ shipfolio.config.json
106
120
  - `npm install && npm run build` must succeed without errors
107
121
  - Do not use next/image (incompatible with static export) -- use standard <img> tags
108
122
  - Do not use features that require a server (API routes, middleware, ISR)
123
+ - Do not set `typescript.ignoreBuildErrors` or bypass build failures
package/prompts/update.md CHANGED
@@ -30,8 +30,10 @@ architecture, and any custom modifications the user has made.
30
30
  4. For new projects: follow the exact same card format and component pattern
31
31
  as existing project cards
32
32
  5. Preserve all custom CSS, custom components, and manual edits
33
- 6. Update src/data/projects.ts with new/changed/removed project data
34
- 7. Update src/data/owner.ts if personal info changed
33
+ 6. Update the existing content source of truth used by the site. If the site already
34
+ has structured data files, modify those; otherwise make the smallest viable edits
35
+ to the current components
36
+ 7. Update owner/profile data wherever the current site stores it
35
37
  8. Update shipfolio.config.json with new timestamps and project list
36
38
  9. If a new section type is needed, create it following the existing
37
39
  component patterns and design tokens in the codebase
@@ -44,3 +46,4 @@ architecture, and any custom modifications the user has made.
44
46
  - Do not break the build -- `npm run build` must succeed
45
47
  - Do not add new dependencies unless absolutely necessary
46
48
  - Keep the existing @media print styles working
49
+ - Do not set `typescript.ignoreBuildErrors` or bypass build failures