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,5 +1,7 @@
1
1
  // src/utils/exec.ts
2
- import { execa } from "execa";
2
+ import {
3
+ execa
4
+ } from "execa";
3
5
  async function commandExists(command) {
4
6
  try {
5
7
  await execa("which", [command]);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/utils/exec.ts","../../../src/orchestrator/detect.ts"],"sourcesContent":["import { execa, type Options as ExecaOptions } from \"execa\";\n\nexport async function run(\n command: string,\n args: string[],\n options?: ExecaOptions\n) {\n return execa(command, args, {\n stdio: \"pipe\",\n ...options,\n });\n}\n\nexport async function commandExists(command: string): Promise<boolean> {\n try {\n await execa(\"which\", [command]);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function runWithOutput(\n command: string,\n args: string[],\n options?: ExecaOptions\n): Promise<string> {\n const result = await execa(command, args, {\n stdio: \"pipe\",\n ...options,\n });\n return result.stdout;\n}\n","import { commandExists } from \"../utils/exec.js\";\nimport type { EngineType } from \"../spec/schema.js\";\n\nexport interface DetectedEngine {\n type: EngineType;\n available: boolean;\n}\n\nexport async function detectEngines(): Promise<DetectedEngine[]> {\n const results: DetectedEngine[] = [];\n\n // Check for v0 API key\n if (process.env.V0_API_KEY) {\n results.push({ type: \"v0\", available: true });\n }\n\n // Check for Claude Code CLI\n if (await commandExists(\"claude\")) {\n results.push({ type: \"claude\", available: true });\n }\n\n // Check for Codex CLI\n if (await commandExists(\"codex\")) {\n results.push({ type: \"codex\", available: true });\n }\n\n return results;\n}\n\nexport function getAvailableEngineTypes(engines: DetectedEngine[]): EngineType[] {\n return engines.filter((e) => e.available).map((e) => e.type);\n}\n"],"mappings":";AAAA,SAAS,aAA2C;AAapD,eAAsB,cAAc,SAAmC;AACrE,MAAI;AACF,UAAM,MAAM,SAAS,CAAC,OAAO,CAAC;AAC9B,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACZA,eAAsB,gBAA2C;AAC/D,QAAM,UAA4B,CAAC;AAGnC,MAAI,QAAQ,IAAI,YAAY;AAC1B,YAAQ,KAAK,EAAE,MAAM,MAAM,WAAW,KAAK,CAAC;AAAA,EAC9C;AAGA,MAAI,MAAM,cAAc,QAAQ,GAAG;AACjC,YAAQ,KAAK,EAAE,MAAM,UAAU,WAAW,KAAK,CAAC;AAAA,EAClD;AAGA,MAAI,MAAM,cAAc,OAAO,GAAG;AAChC,YAAQ,KAAK,EAAE,MAAM,SAAS,WAAW,KAAK,CAAC;AAAA,EACjD;AAEA,SAAO;AACT;AAEO,SAAS,wBAAwB,SAAyC;AAC/E,SAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI;AAC7D;","names":[]}
1
+ {"version":3,"sources":["../../../src/utils/exec.ts","../../../src/orchestrator/detect.ts"],"sourcesContent":["import {\n execa,\n type Options as ExecaOptions,\n type ResultPromise,\n} from \"execa\";\n\nexport function run(\n command: string,\n args: string[],\n options?: ExecaOptions\n): ResultPromise {\n return execa(command, args, {\n stdio: \"pipe\",\n ...options,\n });\n}\n\nexport async function commandExists(command: string): Promise<boolean> {\n try {\n await execa(\"which\", [command]);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function runWithOutput(\n command: string,\n args: string[],\n options?: ExecaOptions\n): Promise<string> {\n const result = await execa(command, args, {\n stdio: \"pipe\",\n ...options,\n });\n if (typeof result.stdout === \"string\") {\n return result.stdout;\n }\n if (result.stdout instanceof Uint8Array) {\n return Buffer.from(result.stdout).toString(\"utf-8\");\n }\n if (Array.isArray(result.stdout)) {\n return result.stdout.join(\"\\n\");\n }\n return \"\";\n}\n","import { commandExists } from \"../utils/exec.js\";\nimport type { EngineType } from \"../spec/schema.js\";\n\nexport interface DetectedEngine {\n type: EngineType;\n available: boolean;\n}\n\nexport async function detectEngines(): Promise<DetectedEngine[]> {\n const results: DetectedEngine[] = [];\n\n // Check for v0 API key\n if (process.env.V0_API_KEY) {\n results.push({ type: \"v0\", available: true });\n }\n\n // Check for Claude Code CLI\n if (await commandExists(\"claude\")) {\n results.push({ type: \"claude\", available: true });\n }\n\n // Check for Codex CLI\n if (await commandExists(\"codex\")) {\n results.push({ type: \"codex\", available: true });\n }\n\n return results;\n}\n\nexport function getAvailableEngineTypes(engines: DetectedEngine[]): EngineType[] {\n return engines.filter((e) => e.available).map((e) => e.type);\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,OAGK;AAaP,eAAsB,cAAc,SAAmC;AACrE,MAAI;AACF,UAAM,MAAM,SAAS,CAAC,OAAO,CAAC;AAC9B,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AChBA,eAAsB,gBAA2C;AAC/D,QAAM,UAA4B,CAAC;AAGnC,MAAI,QAAQ,IAAI,YAAY;AAC1B,YAAQ,KAAK,EAAE,MAAM,MAAM,WAAW,KAAK,CAAC;AAAA,EAC9C;AAGA,MAAI,MAAM,cAAc,QAAQ,GAAG;AACjC,YAAQ,KAAK,EAAE,MAAM,UAAU,WAAW,KAAK,CAAC;AAAA,EAClD;AAGA,MAAI,MAAM,cAAc,OAAO,GAAG;AAChC,YAAQ,KAAK,EAAE,MAAM,SAAS,WAAW,KAAK,CAAC;AAAA,EACjD;AAEA,SAAO;AACT;AAEO,SAAS,wBAAwB,SAAyC;AAC/E,SAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI;AAC7D;","names":[]}
@@ -1,184 +1,103 @@
1
1
  // src/orchestrator/prompt-builder.ts
2
- var FRESH_BUILD_TEMPLATE = `# Task
3
-
4
- Generate a complete, production-ready personal portfolio website.
5
- All data and design preferences are provided in the spec below.
6
- Output a fully working Next.js project that builds and deploys as a static site.
7
-
8
- # Spec
9
-
10
- {{SPEC_JSON}}
11
-
12
- # Technical Requirements
13
-
14
- - Next.js 15 with App Router and TypeScript
15
- - Tailwind CSS v4 for styling
16
- - shadcn/ui components (initialize with \`npx shadcn@latest init --yes\`)
17
- - Static export: set \`output: 'export'\` in next.config.ts
18
- - Build command: \`npm run build\` producing \`out/\` directory
19
- - Zero external API calls at runtime -- all data is embedded in source
20
- - All project data in \`src/data/projects.ts\` as typed constants
21
- - Include \`@media print\` stylesheet in \`src/app/globals.css\` for PDF export
22
- - Responsive: mobile-first with Tailwind breakpoints (sm, md, lg, xl)
23
- - Lighthouse performance score 95+
24
- - Semantic HTML with ARIA labels and keyboard navigation
25
- - No emoji anywhere in text, UI, code, or comments
26
- - Use lucide-react for icons
27
-
28
- # Design Direction
29
-
30
- - Theme: {{THEME}}
31
- - Accent color: {{ACCENT_COLOR}}
32
- - Animation level: {{ANIMATION_LEVEL}}
33
- - Font: {{FONT}}
34
-
35
- Design guidelines:
36
- - Typography-driven layout with large, bold headings
37
- - Generous whitespace between sections
38
- - Single accent color for interactive elements and highlights only
39
- - Project cards should feel substantial but clean
40
- - The site must look hand-crafted, not template-generated
41
- - Dark themes: use zinc/slate backgrounds, not pure black
42
- - Light themes: use warm whites and subtle grays
43
-
44
- # Content Generation
45
-
46
- For each project in the spec:
47
- - Write a 2-3 sentence narrative description based on the README content and tech stack
48
- - Focus on what it does and why it matters
49
- - If user provided an override description, use that instead
50
- - Maintain consistent voice across all descriptions
51
-
52
- For the bio (if set to "auto"):
53
- - Generate a professional, authentic bio based on the project portfolio
54
- - Emphasize shipping velocity and breadth
55
- - Tone: confident, direct, no buzzwords or fluff
56
-
57
- # Sections to Include
58
-
59
- {{SECTIONS_LIST}}
60
-
61
- Hero and Projects are always included. Additional sections as specified.
62
-
63
- # Print / PDF Styles
64
-
65
- In globals.css, add @media print rules:
66
- - Single-column layout
67
- - Hide navigation, footer, animations
68
- - Preserve background colors (user will print with backgrounds enabled)
69
- - Show link URLs inline: \`a[href]::after { content: " (" attr(href) ")"; }\`
70
- - Avoid page breaks inside project cards
71
- - A4-friendly margins and font sizes
72
-
73
- # File Structure to Generate
74
-
75
- \`\`\`
76
- next.config.ts
77
- package.json
78
- tailwind.config.ts
79
- tsconfig.json
80
- postcss.config.mjs
81
- components.json
82
- src/app/layout.tsx
83
- src/app/page.tsx
84
- src/app/globals.css
85
- src/components/ui/ (shadcn components as needed)
86
- src/components/hero.tsx
87
- src/components/project-card.tsx
88
- src/components/project-grid.tsx
89
- src/components/skills.tsx
90
- src/components/about.tsx
91
- src/components/timeline.tsx
92
- src/components/contact.tsx
93
- src/components/navigation.tsx
94
- src/components/footer.tsx
95
- src/data/projects.ts
96
- src/data/owner.ts
97
- src/lib/utils.ts
98
- public/favicon.svg
99
- shipfolio.config.json
100
- \`\`\`
101
-
102
- # Important
103
-
104
- - Generate ALL files needed for the project to build successfully
105
- - Include all shadcn/ui component files that are referenced
106
- - The \`package.json\` must include all dependencies
107
- - \`npm install && npm run build\` must succeed without errors
108
- - Do not use next/image (incompatible with static export) -- use standard <img> tags
109
- - Do not use features that require a server (API routes, middleware, ISR)
110
- `;
111
- var UPDATE_TEMPLATE = `# Task
112
-
113
- Update an existing portfolio website previously generated by shipfolio.
114
- You must preserve the existing design system, layout structure, component
115
- architecture, and any custom modifications the user has made.
116
-
117
- # Existing Site Configuration
118
-
119
- {{EXISTING_CONFIG_JSON}}
120
-
121
- # Changes to Apply
122
-
123
- ## New Projects to Add
124
- {{NEW_PROJECTS}}
125
-
126
- ## Projects to Update (new commits since last scan)
127
- {{UPDATED_PROJECTS}}
128
-
129
- ## Projects to Remove
130
- {{REMOVED_PROJECTS}}
131
-
132
- ## Updated Personal Info
133
- {{PERSONAL_INFO_DIFF}}
134
-
135
- # Rules
136
-
137
- 1. Do NOT change the overall layout, color scheme, or design system
138
- 2. Do NOT reorganize existing components or rename files
139
- 3. Only modify files that need changes for the specified updates
140
- 4. For new projects: follow the exact same card format and component pattern
141
- as existing project cards
142
- 5. Preserve all custom CSS, custom components, and manual edits
143
- 6. Update src/data/projects.ts with new/changed/removed project data
144
- 7. Update src/data/owner.ts if personal info changed
145
- 8. Update shipfolio.config.json with new timestamps and project list
146
- 9. If a new section type is needed, create it following the existing
147
- component patterns and design tokens in the codebase
148
- 10. No emoji anywhere
149
-
150
- # Technical Notes
2
+ import { fileURLToPath } from "url";
3
+
4
+ // src/utils/fs.ts
5
+ import { readFile, writeFile, access, mkdir } from "fs/promises";
6
+ import { join } from "path";
7
+ async function fileExists(path) {
8
+ try {
9
+ await access(path);
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+ async function readText(path) {
16
+ return readFile(path, "utf-8");
17
+ }
151
18
 
152
- - The site uses Next.js 15 + Tailwind CSS + shadcn/ui
153
- - Static export via \`output: 'export'\` in next.config.ts
154
- - Do not break the build -- \`npm run build\` must succeed
155
- - Do not add new dependencies unless absolutely necessary
156
- - Keep the existing @media print styles working
157
- `;
19
+ // src/orchestrator/prompt-builder.ts
20
+ var promptTemplateCache = /* @__PURE__ */ new Map();
21
+ async function loadPromptTemplate(filename) {
22
+ const cached = promptTemplateCache.get(filename);
23
+ if (cached) {
24
+ return cached;
25
+ }
26
+ const candidates = [
27
+ fileURLToPath(new URL(`../prompts/${filename}`, import.meta.url)),
28
+ fileURLToPath(new URL(`../../prompts/${filename}`, import.meta.url)),
29
+ fileURLToPath(new URL(`../../../prompts/${filename}`, import.meta.url))
30
+ ];
31
+ for (const path of candidates) {
32
+ if (await fileExists(path)) {
33
+ const template = await readText(path);
34
+ promptTemplateCache.set(filename, template);
35
+ return template;
36
+ }
37
+ }
38
+ throw new Error(`Prompt template not found: ${filename}`);
39
+ }
40
+ function fillTemplate(template, replacements) {
41
+ return Object.entries(replacements).reduce((text, [key, value]) => {
42
+ return text.split(`{{${key}}}`).join(value);
43
+ }, template);
44
+ }
158
45
  async function buildFreshPrompt(spec) {
159
- let template = FRESH_BUILD_TEMPLATE;
160
- const sectionsText = spec.sections.map((s) => `- ${s}`).join("\n");
161
- template = template.replace("{{SPEC_JSON}}", JSON.stringify(spec, null, 2)).replace("{{THEME}}", spec.style.theme).replace("{{ACCENT_COLOR}}", spec.style.accentColor).replace("{{ANIMATION_LEVEL}}", spec.style.animationLevel).replace("{{FONT}}", spec.style.font).replace("{{SECTIONS_LIST}}", sectionsText);
162
- return template;
46
+ const template = await loadPromptTemplate("fresh-build.md");
47
+ const sectionsText = spec.sections.map((section) => `- ${section}`).join("\n");
48
+ return fillTemplate(template, {
49
+ SPEC_JSON: JSON.stringify(spec, null, 2),
50
+ THEME: spec.style.theme,
51
+ ACCENT_COLOR: spec.style.accentColor,
52
+ ANIMATION_LEVEL: spec.style.animationLevel,
53
+ FONT: spec.style.font,
54
+ SECTIONS_LIST: sectionsText
55
+ });
163
56
  }
164
- async function buildUpdatePrompt(existingConfig, diff) {
165
- let template = UPDATE_TEMPLATE;
166
- const newProjectsText = diff.newProjects.length > 0 ? diff.newProjects.map(
167
- (p) => `- ${p.name}: ${p.description}
168
- Tech: ${p.techStack.join(", ")}
169
- README excerpt: ${(p.readmeContent || "").slice(0, 500)}`
57
+ async function buildUpdatePrompt(existingConfig, diff, preparedNewProjects = [], personalInfoDiff = "No changes") {
58
+ const template = await loadPromptTemplate("update.md");
59
+ const newProjects = preparedNewProjects.length > 0 ? preparedNewProjects : diff.newProjects.map((project) => ({
60
+ ...project,
61
+ included: true,
62
+ overrideDescription: null,
63
+ showSourceLink: !!project.remoteUrl,
64
+ role: "solo",
65
+ trackedProjectPaths: [project.localPath],
66
+ metrics: {},
67
+ caseStudy: {
68
+ featured: false,
69
+ audience: null,
70
+ problem: null,
71
+ solution: null,
72
+ impact: null,
73
+ evidence: [],
74
+ screenshots: []
75
+ }
76
+ }));
77
+ const newProjectsText = newProjects.length > 0 ? newProjects.map(
78
+ (project) => `- ${project.name}: ${project.overrideDescription || project.description}
79
+ Tech: ${project.techStack.join(", ")}
80
+ Demo: ${project.demoUrl || "none"}
81
+ Featured: ${project.caseStudy.featured}
82
+ Case study: ${JSON.stringify(project.caseStudy)}
83
+ README excerpt: ${(project.readmeContent || "").slice(0, 500)}`
170
84
  ).join("\n\n") : "None";
171
85
  const updatedProjectsText = diff.updatedProjects.length > 0 ? diff.updatedProjects.map(
172
- (u) => `- ${u.project.name}: ${u.newCommits} new commits
173
- README changed: ${u.readmeChanged}
174
- Dependencies changed: ${u.depsChanged}`
86
+ (update) => `- ${update.project.name}: ${update.newCommits} new commits
87
+ README changed: ${update.readmeChanged}
88
+ Dependencies changed: ${update.depsChanged}
89
+ Changed paths: ${update.changedPaths.join(", ") || "none"}
90
+ Removed paths: ${update.removedPaths.join(", ") || "none"}
91
+ Case study: ${JSON.stringify(update.project.caseStudy)}`
175
92
  ).join("\n\n") : "None";
176
- const removedProjectsText = diff.removedProjects.length > 0 ? diff.removedProjects.map((p) => `- ${p.name}`).join("\n") : "None";
177
- template = template.replace(
178
- "{{EXISTING_CONFIG_JSON}}",
179
- JSON.stringify(existingConfig, null, 2)
180
- ).replace("{{NEW_PROJECTS}}", newProjectsText).replace("{{UPDATED_PROJECTS}}", updatedProjectsText).replace("{{REMOVED_PROJECTS}}", removedProjectsText).replace("{{PERSONAL_INFO_DIFF}}", "No changes");
181
- return template;
93
+ const removedProjectsText = diff.removedProjects.length > 0 ? diff.removedProjects.map((project) => `- ${project.name}`).join("\n") : "None";
94
+ return fillTemplate(template, {
95
+ EXISTING_CONFIG_JSON: JSON.stringify(existingConfig, null, 2),
96
+ NEW_PROJECTS: newProjectsText,
97
+ UPDATED_PROJECTS: updatedProjectsText,
98
+ REMOVED_PROJECTS: removedProjectsText,
99
+ PERSONAL_INFO_DIFF: personalInfoDiff
100
+ });
182
101
  }
183
102
  export {
184
103
  buildFreshPrompt,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/orchestrator/prompt-builder.ts"],"sourcesContent":["import type { ShipfolioSpec, ShipfolioConfig, SiteDiff } from \"../spec/schema.js\";\n\nconst FRESH_BUILD_TEMPLATE = `# Task\n\nGenerate a complete, production-ready personal portfolio website.\nAll data and design preferences are provided in the spec below.\nOutput a fully working Next.js project that builds and deploys as a static site.\n\n# Spec\n\n{{SPEC_JSON}}\n\n# Technical Requirements\n\n- Next.js 15 with App Router and TypeScript\n- Tailwind CSS v4 for styling\n- shadcn/ui components (initialize with \\`npx shadcn@latest init --yes\\`)\n- Static export: set \\`output: 'export'\\` in next.config.ts\n- Build command: \\`npm run build\\` producing \\`out/\\` directory\n- Zero external API calls at runtime -- all data is embedded in source\n- All project data in \\`src/data/projects.ts\\` as typed constants\n- Include \\`@media print\\` stylesheet in \\`src/app/globals.css\\` for PDF export\n- Responsive: mobile-first with Tailwind breakpoints (sm, md, lg, xl)\n- Lighthouse performance score 95+\n- Semantic HTML with ARIA labels and keyboard navigation\n- No emoji anywhere in text, UI, code, or comments\n- Use lucide-react for icons\n\n# Design Direction\n\n- Theme: {{THEME}}\n- Accent color: {{ACCENT_COLOR}}\n- Animation level: {{ANIMATION_LEVEL}}\n- Font: {{FONT}}\n\nDesign guidelines:\n- Typography-driven layout with large, bold headings\n- Generous whitespace between sections\n- Single accent color for interactive elements and highlights only\n- Project cards should feel substantial but clean\n- The site must look hand-crafted, not template-generated\n- Dark themes: use zinc/slate backgrounds, not pure black\n- Light themes: use warm whites and subtle grays\n\n# Content Generation\n\nFor each project in the spec:\n- Write a 2-3 sentence narrative description based on the README content and tech stack\n- Focus on what it does and why it matters\n- If user provided an override description, use that instead\n- Maintain consistent voice across all descriptions\n\nFor the bio (if set to \"auto\"):\n- Generate a professional, authentic bio based on the project portfolio\n- Emphasize shipping velocity and breadth\n- Tone: confident, direct, no buzzwords or fluff\n\n# Sections to Include\n\n{{SECTIONS_LIST}}\n\nHero and Projects are always included. Additional sections as specified.\n\n# Print / PDF Styles\n\nIn globals.css, add @media print rules:\n- Single-column layout\n- Hide navigation, footer, animations\n- Preserve background colors (user will print with backgrounds enabled)\n- Show link URLs inline: \\`a[href]::after { content: \" (\" attr(href) \")\"; }\\`\n- Avoid page breaks inside project cards\n- A4-friendly margins and font sizes\n\n# File Structure to Generate\n\n\\`\\`\\`\nnext.config.ts\npackage.json\ntailwind.config.ts\ntsconfig.json\npostcss.config.mjs\ncomponents.json\nsrc/app/layout.tsx\nsrc/app/page.tsx\nsrc/app/globals.css\nsrc/components/ui/ (shadcn components as needed)\nsrc/components/hero.tsx\nsrc/components/project-card.tsx\nsrc/components/project-grid.tsx\nsrc/components/skills.tsx\nsrc/components/about.tsx\nsrc/components/timeline.tsx\nsrc/components/contact.tsx\nsrc/components/navigation.tsx\nsrc/components/footer.tsx\nsrc/data/projects.ts\nsrc/data/owner.ts\nsrc/lib/utils.ts\npublic/favicon.svg\nshipfolio.config.json\n\\`\\`\\`\n\n# Important\n\n- Generate ALL files needed for the project to build successfully\n- Include all shadcn/ui component files that are referenced\n- The \\`package.json\\` must include all dependencies\n- \\`npm install && npm run build\\` must succeed without errors\n- Do not use next/image (incompatible with static export) -- use standard <img> tags\n- Do not use features that require a server (API routes, middleware, ISR)\n`;\n\nconst UPDATE_TEMPLATE = `# Task\n\nUpdate an existing portfolio website previously generated by shipfolio.\nYou must preserve the existing design system, layout structure, component\narchitecture, and any custom modifications the user has made.\n\n# Existing Site Configuration\n\n{{EXISTING_CONFIG_JSON}}\n\n# Changes to Apply\n\n## New Projects to Add\n{{NEW_PROJECTS}}\n\n## Projects to Update (new commits since last scan)\n{{UPDATED_PROJECTS}}\n\n## Projects to Remove\n{{REMOVED_PROJECTS}}\n\n## Updated Personal Info\n{{PERSONAL_INFO_DIFF}}\n\n# Rules\n\n1. Do NOT change the overall layout, color scheme, or design system\n2. Do NOT reorganize existing components or rename files\n3. Only modify files that need changes for the specified updates\n4. For new projects: follow the exact same card format and component pattern\n as existing project cards\n5. Preserve all custom CSS, custom components, and manual edits\n6. Update src/data/projects.ts with new/changed/removed project data\n7. Update src/data/owner.ts if personal info changed\n8. Update shipfolio.config.json with new timestamps and project list\n9. If a new section type is needed, create it following the existing\n component patterns and design tokens in the codebase\n10. No emoji anywhere\n\n# Technical Notes\n\n- The site uses Next.js 15 + Tailwind CSS + shadcn/ui\n- Static export via \\`output: 'export'\\` in next.config.ts\n- Do not break the build -- \\`npm run build\\` must succeed\n- Do not add new dependencies unless absolutely necessary\n- Keep the existing @media print styles working\n`;\n\nexport async function buildFreshPrompt(spec: ShipfolioSpec): Promise<string> {\n let template = FRESH_BUILD_TEMPLATE;\n\n const sectionsText = spec.sections\n .map((s) => `- ${s}`)\n .join(\"\\n\");\n\n template = template\n .replace(\"{{SPEC_JSON}}\", JSON.stringify(spec, null, 2))\n .replace(\"{{THEME}}\", spec.style.theme)\n .replace(\"{{ACCENT_COLOR}}\", spec.style.accentColor)\n .replace(\"{{ANIMATION_LEVEL}}\", spec.style.animationLevel)\n .replace(\"{{FONT}}\", spec.style.font)\n .replace(\"{{SECTIONS_LIST}}\", sectionsText);\n\n return template;\n}\n\nexport async function buildUpdatePrompt(\n existingConfig: ShipfolioConfig,\n diff: SiteDiff\n): Promise<string> {\n let template = UPDATE_TEMPLATE;\n\n const newProjectsText =\n diff.newProjects.length > 0\n ? diff.newProjects\n .map(\n (p) =>\n `- ${p.name}: ${p.description}\\n Tech: ${p.techStack.join(\", \")}\\n README excerpt: ${(p.readmeContent || \"\").slice(0, 500)}`\n )\n .join(\"\\n\\n\")\n : \"None\";\n\n const updatedProjectsText =\n diff.updatedProjects.length > 0\n ? diff.updatedProjects\n .map(\n (u) =>\n `- ${u.project.name}: ${u.newCommits} new commits\\n README changed: ${u.readmeChanged}\\n Dependencies changed: ${u.depsChanged}`\n )\n .join(\"\\n\\n\")\n : \"None\";\n\n const removedProjectsText =\n diff.removedProjects.length > 0\n ? diff.removedProjects.map((p) => `- ${p.name}`).join(\"\\n\")\n : \"None\";\n\n template = template\n .replace(\n \"{{EXISTING_CONFIG_JSON}}\",\n JSON.stringify(existingConfig, null, 2)\n )\n .replace(\"{{NEW_PROJECTS}}\", newProjectsText)\n .replace(\"{{UPDATED_PROJECTS}}\", updatedProjectsText)\n .replace(\"{{REMOVED_PROJECTS}}\", removedProjectsText)\n .replace(\"{{PERSONAL_INFO_DIFF}}\", \"No changes\");\n\n return template;\n}\n"],"mappings":";AAEA,IAAM,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA8G7B,IAAM,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgDxB,eAAsB,iBAAiB,MAAsC;AAC3E,MAAI,WAAW;AAEf,QAAM,eAAe,KAAK,SACvB,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,EACnB,KAAK,IAAI;AAEZ,aAAW,SACR,QAAQ,iBAAiB,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC,EACtD,QAAQ,aAAa,KAAK,MAAM,KAAK,EACrC,QAAQ,oBAAoB,KAAK,MAAM,WAAW,EAClD,QAAQ,uBAAuB,KAAK,MAAM,cAAc,EACxD,QAAQ,YAAY,KAAK,MAAM,IAAI,EACnC,QAAQ,qBAAqB,YAAY;AAE5C,SAAO;AACT;AAEA,eAAsB,kBACpB,gBACA,MACiB;AACjB,MAAI,WAAW;AAEf,QAAM,kBACJ,KAAK,YAAY,SAAS,IACtB,KAAK,YACF;AAAA,IACC,CAAC,MACC,KAAK,EAAE,IAAI,KAAK,EAAE,WAAW;AAAA,UAAa,EAAE,UAAU,KAAK,IAAI,CAAC;AAAA,qBAAwB,EAAE,iBAAiB,IAAI,MAAM,GAAG,GAAG,CAAC;AAAA,EAChI,EACC,KAAK,MAAM,IACd;AAEN,QAAM,sBACJ,KAAK,gBAAgB,SAAS,IAC1B,KAAK,gBACF;AAAA,IACC,CAAC,MACC,KAAK,EAAE,QAAQ,IAAI,KAAK,EAAE,UAAU;AAAA,oBAAmC,EAAE,aAAa;AAAA,0BAA6B,EAAE,WAAW;AAAA,EACpI,EACC,KAAK,MAAM,IACd;AAEN,QAAM,sBACJ,KAAK,gBAAgB,SAAS,IAC1B,KAAK,gBAAgB,IAAI,CAAC,MAAM,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,IAAI,IACxD;AAEN,aAAW,SACR;AAAA,IACC;AAAA,IACA,KAAK,UAAU,gBAAgB,MAAM,CAAC;AAAA,EACxC,EACC,QAAQ,oBAAoB,eAAe,EAC3C,QAAQ,wBAAwB,mBAAmB,EACnD,QAAQ,wBAAwB,mBAAmB,EACnD,QAAQ,0BAA0B,YAAY;AAEjD,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../src/orchestrator/prompt-builder.ts","../../../src/utils/fs.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport type {\n ProjectEntry,\n ShipfolioSpec,\n ShipfolioConfig,\n SiteDiff,\n} from \"../spec/schema.js\";\nimport { fileExists, readText } from \"../utils/fs.js\";\n\nconst promptTemplateCache = new Map<string, string>();\n\nasync function loadPromptTemplate(filename: string): Promise<string> {\n const cached = promptTemplateCache.get(filename);\n if (cached) {\n return cached;\n }\n\n const candidates = [\n fileURLToPath(new URL(`../prompts/${filename}`, import.meta.url)),\n fileURLToPath(new URL(`../../prompts/${filename}`, import.meta.url)),\n fileURLToPath(new URL(`../../../prompts/${filename}`, import.meta.url)),\n ];\n\n for (const path of candidates) {\n if (await fileExists(path)) {\n const template = await readText(path);\n promptTemplateCache.set(filename, template);\n return template;\n }\n }\n\n throw new Error(`Prompt template not found: ${filename}`);\n}\n\nfunction fillTemplate(\n template: string,\n replacements: Record<string, string>\n): string {\n return Object.entries(replacements).reduce((text, [key, value]) => {\n return text.split(`{{${key}}}`).join(value);\n }, template);\n}\n\nexport async function buildFreshPrompt(spec: ShipfolioSpec): Promise<string> {\n const template = await loadPromptTemplate(\"fresh-build.md\");\n const sectionsText = spec.sections.map((section) => `- ${section}`).join(\"\\n\");\n\n return fillTemplate(template, {\n SPEC_JSON: JSON.stringify(spec, null, 2),\n THEME: spec.style.theme,\n ACCENT_COLOR: spec.style.accentColor,\n ANIMATION_LEVEL: spec.style.animationLevel,\n FONT: spec.style.font,\n SECTIONS_LIST: sectionsText,\n });\n}\n\nexport async function buildUpdatePrompt(\n existingConfig: ShipfolioConfig,\n diff: SiteDiff,\n preparedNewProjects: ProjectEntry[] = [],\n personalInfoDiff: string = \"No changes\"\n): Promise<string> {\n const template = await loadPromptTemplate(\"update.md\");\n\n const newProjects =\n preparedNewProjects.length > 0\n ? preparedNewProjects\n : diff.newProjects.map((project) => ({\n ...project,\n included: true,\n overrideDescription: null,\n showSourceLink: !!project.remoteUrl,\n role: \"solo\" as const,\n trackedProjectPaths: [project.localPath],\n metrics: {},\n caseStudy: {\n featured: false,\n audience: null,\n problem: null,\n solution: null,\n impact: null,\n evidence: [],\n screenshots: [],\n },\n }));\n\n const newProjectsText =\n newProjects.length > 0\n ? newProjects\n .map(\n (project) =>\n `- ${project.name}: ${project.overrideDescription || project.description}\\n` +\n ` Tech: ${project.techStack.join(\", \")}\\n` +\n ` Demo: ${project.demoUrl || \"none\"}\\n` +\n ` Featured: ${project.caseStudy.featured}\\n` +\n ` Case study: ${JSON.stringify(project.caseStudy)}\\n` +\n ` README excerpt: ${(project.readmeContent || \"\").slice(0, 500)}`\n )\n .join(\"\\n\\n\")\n : \"None\";\n\n const updatedProjectsText =\n diff.updatedProjects.length > 0\n ? diff.updatedProjects\n .map(\n (update) =>\n `- ${update.project.name}: ${update.newCommits} new commits\\n` +\n ` README changed: ${update.readmeChanged}\\n` +\n ` Dependencies changed: ${update.depsChanged}\\n` +\n ` Changed paths: ${update.changedPaths.join(\", \") || \"none\"}\\n` +\n ` Removed paths: ${update.removedPaths.join(\", \") || \"none\"}\\n` +\n ` Case study: ${JSON.stringify(update.project.caseStudy)}`\n )\n .join(\"\\n\\n\")\n : \"None\";\n\n const removedProjectsText =\n diff.removedProjects.length > 0\n ? diff.removedProjects.map((project) => `- ${project.name}`).join(\"\\n\")\n : \"None\";\n\n return fillTemplate(template, {\n EXISTING_CONFIG_JSON: JSON.stringify(existingConfig, null, 2),\n NEW_PROJECTS: newProjectsText,\n UPDATED_PROJECTS: updatedProjectsText,\n REMOVED_PROJECTS: removedProjectsText,\n PERSONAL_INFO_DIFF: personalInfoDiff,\n });\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"],"mappings":"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","names":[]}
@@ -61,7 +61,7 @@ function normalizeGitUrl(url) {
61
61
  }
62
62
  return url;
63
63
  }
64
- async function findGitRepos(rootPath, maxDepth = 3) {
64
+ async function findGitRepos(rootPath, maxDepth = 6) {
65
65
  const { glob } = await import("glob");
66
66
  const gitDirs = await glob("**/.git", {
67
67
  cwd: rootPath,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/scanner/git.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"],"mappings":";AAAA,OAAO,eAAe;AAYtB,eAAsB,WAAW,aAAuC;AACtE,QAAM,MAAM,UAAU,WAAW;AACjC,QAAM,QAAiB;AAAA,IACrB,QAAQ;AAAA,IACR,iBAAiB;AAAA,IACjB,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,WAAW;AAAA,IACX,gBAAgB;AAAA,EAClB;AAEA,MAAI;AACF,UAAM,SAAS,MAAM,IAAI,YAAY;AACrC,QAAI,CAAC,OAAQ,QAAO;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,MAAM,MAAM,IAAI,IAAI,EAAE,UAAU,EAAE,CAAC;AAGzC,QAAI,kBAAiC;AACrC,QAAI;AACF,YAAM,cAAc,MAAM,IAAI,IAAI,CAAC,OAAO,aAAa,gBAAgB,eAAe,CAAC;AACvF,wBAAkB,YAAY,KAAK,KAAK;AAAA,IAC1C,QAAQ;AAAA,IAER;AAEA,QAAI,YAA2B;AAC/B,QAAI;AACF,YAAM,UAAU,MAAM,IAAI,WAAW,IAAI;AACzC,YAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ;AACtD,UAAI,QAAQ,MAAM,OAAO;AACvB,oBAAY,gBAAgB,OAAO,KAAK,KAAK;AAAA,MAC/C;AAAA,IACF,QAAQ;AAAA,IAER;AAGA,QAAI,eAAe;AACnB,QAAI;AACF,YAAM,SAAS,MAAM,IAAI,IAAI,CAAC,YAAY,WAAW,MAAM,CAAC;AAC5D,qBAAe,SAAS,OAAO,KAAK,GAAG,EAAE,KAAK;AAAA,IAChD,QAAQ;AACN,qBAAe;AAAA,IACjB;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,gBAAgB,IAAI,QAAQ,QAAQ;AAAA,MACpC;AAAA,MACA;AAAA,MACA,gBAAgB,IAAI,QAAQ,QAAQ;AAAA,IACtC;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,GAAG,OAAO,QAAQ,KAAK;AAAA,EAClC;AACF;AAEA,SAAS,gBAAgB,KAAqB;AAE5C,MAAI,IAAI,WAAW,MAAM,GAAG;AAC1B,UAAM,IAAI,QAAQ,KAAK,GAAG,EAAE,QAAQ,QAAQ,UAAU;AAAA,EACxD;AAEA,MAAI,IAAI,SAAS,MAAM,GAAG;AACxB,UAAM,IAAI,MAAM,GAAG,EAAE;AAAA,EACvB;AACA,SAAO;AACT;AAEA,eAAsB,aACpB,UACA,WAAmB,GACA;AACnB,QAAM,EAAE,KAAK,IAAI,MAAM,OAAO,MAAM;AACpC,QAAM,UAAU,MAAM,KAAK,WAAW;AAAA,IACpC,KAAK;AAAA,IACL,UAAU,WAAW;AAAA,IACrB,KAAK;AAAA,IACL,QAAQ;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO,QACJ,IAAI,CAAC,WAAW;AACf,UAAM,QAAQ,OAAO,MAAM,GAAG;AAC9B,UAAM,IAAI;AACV,WAAO,MAAM,SAAS,IAClB,GAAG,QAAQ,IAAI,MAAM,KAAK,GAAG,CAAC,KAC9B;AAAA,EACN,CAAC,EACA,KAAK;AACV;","names":[]}
1
+ {"version":3,"sources":["../../../src/scanner/git.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"],"mappings":";AAAA,OAAO,eAAe;AAWtB,eAAsB,WAAW,aAAuC;AACtE,QAAM,MAAM,UAAU,WAAW;AACjC,QAAM,QAAiB;AAAA,IACrB,QAAQ;AAAA,IACR,iBAAiB;AAAA,IACjB,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,WAAW;AAAA,IACX,gBAAgB;AAAA,EAClB;AAEA,MAAI;AACF,UAAM,SAAS,MAAM,IAAI,YAAY;AACrC,QAAI,CAAC,OAAQ,QAAO;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,MAAM,MAAM,IAAI,IAAI,EAAE,UAAU,EAAE,CAAC;AAGzC,QAAI,kBAAiC;AACrC,QAAI;AACF,YAAM,cAAc,MAAM,IAAI,IAAI,CAAC,OAAO,aAAa,gBAAgB,eAAe,CAAC;AACvF,wBAAkB,YAAY,KAAK,KAAK;AAAA,IAC1C,QAAQ;AAAA,IAER;AAEA,QAAI,YAA2B;AAC/B,QAAI;AACF,YAAM,UAAU,MAAM,IAAI,WAAW,IAAI;AACzC,YAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ;AACtD,UAAI,QAAQ,MAAM,OAAO;AACvB,oBAAY,gBAAgB,OAAO,KAAK,KAAK;AAAA,MAC/C;AAAA,IACF,QAAQ;AAAA,IAER;AAGA,QAAI,eAAe;AACnB,QAAI;AACF,YAAM,SAAS,MAAM,IAAI,IAAI,CAAC,YAAY,WAAW,MAAM,CAAC;AAC5D,qBAAe,SAAS,OAAO,KAAK,GAAG,EAAE,KAAK;AAAA,IAChD,QAAQ;AACN,qBAAe;AAAA,IACjB;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,gBAAgB,IAAI,QAAQ,QAAQ;AAAA,MACpC;AAAA,MACA;AAAA,MACA,gBAAgB,IAAI,QAAQ,QAAQ;AAAA,IACtC;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,GAAG,OAAO,QAAQ,KAAK;AAAA,EAClC;AACF;AAEA,SAAS,gBAAgB,KAAqB;AAE5C,MAAI,IAAI,WAAW,MAAM,GAAG;AAC1B,UAAM,IAAI,QAAQ,KAAK,GAAG,EAAE,QAAQ,QAAQ,UAAU;AAAA,EACxD;AAEA,MAAI,IAAI,SAAS,MAAM,GAAG;AACxB,UAAM,IAAI,MAAM,GAAG,EAAE;AAAA,EACvB;AACA,SAAO;AACT;AAEA,eAAsB,aACpB,UACA,WAAmB,GACA;AACnB,QAAM,EAAE,KAAK,IAAI,MAAM,OAAO,MAAM;AACpC,QAAM,UAAU,MAAM,KAAK,WAAW;AAAA,IACpC,KAAK;AAAA,IACL,UAAU,WAAW;AAAA,IACrB,KAAK;AAAA,IACL,QAAQ;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO,QACJ,IAAI,CAAC,WAAW;AACf,UAAM,QAAQ,OAAO,MAAM,GAAG;AAC9B,UAAM,IAAI;AACV,WAAO,MAAM,SAAS,IAClB,GAAG,QAAQ,IAAI,MAAM,KAAK,GAAG,CAAC,KAC9B;AAAA,EACN,CAAC,EACA,KAAK;AACV;","names":[]}
@@ -61,7 +61,7 @@ function normalizeGitUrl(url) {
61
61
  }
62
62
  return url;
63
63
  }
64
- async function findGitRepos(rootPath, maxDepth = 3) {
64
+ async function findGitRepos(rootPath, maxDepth = 6) {
65
65
  const { glob: glob2 } = await import("glob");
66
66
  const gitDirs = await glob2("**/.git", {
67
67
  cwd: rootPath,
@@ -271,6 +271,9 @@ function deriveNameFromPath(projectPath) {
271
271
  return basename(projectPath);
272
272
  }
273
273
 
274
+ // src/scanner/index.ts
275
+ import { dirname, resolve as resolvePath } from "path";
276
+
274
277
  // src/scanner/extractors.ts
275
278
  async function extractReadme(projectPath) {
276
279
  const candidates = [
@@ -366,14 +369,37 @@ var logger = {
366
369
 
367
370
  // src/scanner/index.ts
368
371
  import ora from "ora";
372
+ var DEFAULT_SCAN_DEPTH = 6;
373
+ var NON_GIT_PROJECT_INDICATORS = [
374
+ "package.json",
375
+ "Cargo.toml",
376
+ "go.mod",
377
+ "pyproject.toml",
378
+ "requirements.txt",
379
+ "setup.py"
380
+ ];
381
+ var SCAN_IGNORE_PATTERNS = [
382
+ "**/node_modules/**",
383
+ "**/vendor/**",
384
+ "**/.git/**",
385
+ "**/.next/**",
386
+ "**/dist/**",
387
+ "**/build/**",
388
+ "**/out/**",
389
+ "**/coverage/**"
390
+ ];
369
391
  async function scanProjects(directories) {
370
392
  const allPaths = [];
371
393
  const spinner = ora("Scanning for projects...").start();
372
394
  for (const dir of directories) {
373
395
  try {
374
- const repos = await findGitRepos(dir);
396
+ const repos = await findGitRepos(dir, DEFAULT_SCAN_DEPTH);
375
397
  allPaths.push(...repos);
376
- const nonGitDirs = await findNonGitProjects(dir, new Set(repos));
398
+ const nonGitDirs = await findNonGitProjects(
399
+ dir,
400
+ repos,
401
+ DEFAULT_SCAN_DEPTH
402
+ );
377
403
  allPaths.push(...nonGitDirs);
378
404
  } catch (err) {
379
405
  logger.warn(`Could not scan ${dir}: ${err}`);
@@ -402,31 +428,29 @@ async function scanProjects(directories) {
402
428
  spinner.succeed(`Scanned ${projects.length} projects`);
403
429
  return projects;
404
430
  }
405
- async function findNonGitProjects(rootPath, gitRepos) {
431
+ async function findNonGitProjects(rootPath, gitRepos, maxDepth) {
406
432
  const { glob: glob2 } = await import("glob");
407
- const indicators = [
408
- "*/package.json",
409
- "*/Cargo.toml",
410
- "*/go.mod",
411
- "*/pyproject.toml",
412
- "*/requirements.txt",
413
- "*/setup.py"
414
- ];
415
433
  const found = /* @__PURE__ */ new Set();
416
- for (const pattern of indicators) {
417
- const matches = await glob2(pattern, {
434
+ for (const indicator of NON_GIT_PROJECT_INDICATORS) {
435
+ const matches = await glob2(`**/${indicator}`, {
418
436
  cwd: rootPath,
419
- ignore: ["**/node_modules/**", "**/vendor/**"]
437
+ ignore: SCAN_IGNORE_PATTERNS,
438
+ maxDepth
420
439
  });
421
440
  for (const match of matches) {
422
- const dir = `${rootPath}/${match.split("/")[0]}`;
423
- if (!gitRepos.has(dir) && !found.has(dir)) {
424
- found.add(dir);
441
+ const candidateDir = dirname(resolvePath(rootPath, match));
442
+ if (!isInsideTrackedRepo(candidateDir, gitRepos) && !found.has(candidateDir)) {
443
+ found.add(candidateDir);
425
444
  }
426
445
  }
427
446
  }
428
447
  return [...found].sort();
429
448
  }
449
+ function isInsideTrackedRepo(candidateDir, gitRepos) {
450
+ return gitRepos.some(
451
+ (repoPath) => candidateDir === repoPath || candidateDir.startsWith(`${repoPath}/`)
452
+ );
453
+ }
430
454
  async function extractProjectMeta(projectPath) {
431
455
  const gitMeta = await getGitMeta(projectPath);
432
456
  let techStack = [];