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.
- package/README.md +28 -6
- package/dist/cli.js +1212 -441
- package/dist/cli.js.map +1 -1
- package/dist/lib/orchestrator/detect.js +3 -1
- package/dist/lib/orchestrator/detect.js.map +1 -1
- package/dist/lib/orchestrator/prompt-builder.js +93 -174
- package/dist/lib/orchestrator/prompt-builder.js.map +1 -1
- package/dist/lib/scanner/git.js +1 -1
- package/dist/lib/scanner/git.js.map +1 -1
- package/dist/lib/scanner/index.js +42 -18
- package/dist/lib/scanner/index.js.map +1 -1
- package/dist/lib/spec/builder.js +100 -3
- package/dist/lib/spec/builder.js.map +1 -1
- package/dist/lib/spec/diff.js +179 -15
- package/dist/lib/spec/diff.js.map +1 -1
- package/package.json +6 -4
- package/prompts/fresh-build.md +15 -0
- package/prompts/update.md +5 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/utils/exec.ts","../../../src/orchestrator/detect.ts"],"sourcesContent":["import {
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
const sectionsText = spec.sections.map((
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
(
|
|
173
|
-
README changed: ${
|
|
174
|
-
Dependencies changed: ${
|
|
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((
|
|
177
|
-
template
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
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":[]}
|
package/dist/lib/scanner/git.js
CHANGED
|
@@ -61,7 +61,7 @@ function normalizeGitUrl(url) {
|
|
|
61
61
|
}
|
|
62
62
|
return url;
|
|
63
63
|
}
|
|
64
|
-
async function findGitRepos(rootPath, maxDepth =
|
|
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\";\
|
|
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 =
|
|
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(
|
|
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
|
|
417
|
-
const matches = await glob2(
|
|
434
|
+
for (const indicator of NON_GIT_PROJECT_INDICATORS) {
|
|
435
|
+
const matches = await glob2(`**/${indicator}`, {
|
|
418
436
|
cwd: rootPath,
|
|
419
|
-
ignore:
|
|
437
|
+
ignore: SCAN_IGNORE_PATTERNS,
|
|
438
|
+
maxDepth
|
|
420
439
|
});
|
|
421
440
|
for (const match of matches) {
|
|
422
|
-
const
|
|
423
|
-
if (!gitRepos
|
|
424
|
-
found.add(
|
|
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 = [];
|