shipfolio 1.0.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.
@@ -0,0 +1,457 @@
1
+ // src/scanner/git.ts
2
+ import simpleGit from "simple-git";
3
+ async function getGitMeta(projectPath) {
4
+ const git = simpleGit(projectPath);
5
+ const empty = {
6
+ isRepo: false,
7
+ firstCommitDate: null,
8
+ lastCommitDate: null,
9
+ totalCommits: 0,
10
+ remoteUrl: null,
11
+ lastCommitHash: null
12
+ };
13
+ try {
14
+ const isRepo = await git.checkIsRepo();
15
+ if (!isRepo) return empty;
16
+ } catch {
17
+ return empty;
18
+ }
19
+ try {
20
+ const log = await git.log({ maxCount: 1 });
21
+ let firstCommitDate = null;
22
+ try {
23
+ const firstResult = await git.raw(["log", "--reverse", "--format=%aI", "--max-count=1"]);
24
+ firstCommitDate = firstResult.trim() || null;
25
+ } catch {
26
+ }
27
+ let remoteUrl = null;
28
+ try {
29
+ const remotes = await git.getRemotes(true);
30
+ const origin = remotes.find((r) => r.name === "origin");
31
+ if (origin?.refs?.fetch) {
32
+ remoteUrl = normalizeGitUrl(origin.refs.fetch);
33
+ }
34
+ } catch {
35
+ }
36
+ let totalCommits = 0;
37
+ try {
38
+ const result = await git.raw(["rev-list", "--count", "HEAD"]);
39
+ totalCommits = parseInt(result.trim(), 10) || 0;
40
+ } catch {
41
+ totalCommits = 0;
42
+ }
43
+ return {
44
+ isRepo: true,
45
+ firstCommitDate,
46
+ lastCommitDate: log.latest?.date || null,
47
+ totalCommits,
48
+ remoteUrl,
49
+ lastCommitHash: log.latest?.hash || null
50
+ };
51
+ } catch {
52
+ return { ...empty, isRepo: true };
53
+ }
54
+ }
55
+ function normalizeGitUrl(url) {
56
+ if (url.startsWith("git@")) {
57
+ url = url.replace(":", "/").replace("git@", "https://");
58
+ }
59
+ if (url.endsWith(".git")) {
60
+ url = url.slice(0, -4);
61
+ }
62
+ return url;
63
+ }
64
+ async function findGitRepos(rootPath, maxDepth = 3) {
65
+ const { glob: glob2 } = await import("glob");
66
+ const gitDirs = await glob2("**/.git", {
67
+ cwd: rootPath,
68
+ maxDepth: maxDepth + 1,
69
+ dot: true,
70
+ ignore: [
71
+ "**/node_modules/**/.git",
72
+ "**/vendor/**/.git",
73
+ "**/__pycache__/**/.git"
74
+ ]
75
+ });
76
+ return gitDirs.map((gitDir) => {
77
+ const parts = gitDir.split("/");
78
+ parts.pop();
79
+ return parts.length > 0 ? `${rootPath}/${parts.join("/")}` : rootPath;
80
+ }).sort();
81
+ }
82
+
83
+ // src/utils/fs.ts
84
+ import { readFile, writeFile, access, mkdir } from "fs/promises";
85
+ import { join } from "path";
86
+ async function fileExists(path) {
87
+ try {
88
+ await access(path);
89
+ return true;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+ async function readJson(path) {
95
+ const content = await readFile(path, "utf-8");
96
+ return JSON.parse(content);
97
+ }
98
+ async function readText(path) {
99
+ return readFile(path, "utf-8");
100
+ }
101
+
102
+ // src/scanner/detectors/node.ts
103
+ async function detectNode(projectPath) {
104
+ const pkgPath = join(projectPath, "package.json");
105
+ if (!await fileExists(pkgPath)) return null;
106
+ const pkg = await readJson(pkgPath);
107
+ const allDeps = {
108
+ ...pkg.dependencies,
109
+ ...pkg.devDependencies
110
+ };
111
+ const depNames = Object.keys(allDeps);
112
+ const techStack = [];
113
+ if (depNames.includes("next")) techStack.push("Next.js");
114
+ else if (depNames.includes("nuxt")) techStack.push("Nuxt");
115
+ else if (depNames.includes("astro")) techStack.push("Astro");
116
+ else if (depNames.includes("svelte") || depNames.includes("@sveltejs/kit"))
117
+ techStack.push("Svelte");
118
+ if (depNames.includes("react")) techStack.push("React");
119
+ if (depNames.includes("vue")) techStack.push("Vue");
120
+ if (depNames.includes("tailwindcss")) techStack.push("Tailwind CSS");
121
+ if (depNames.includes("express")) techStack.push("Express");
122
+ if (depNames.includes("fastify")) techStack.push("Fastify");
123
+ if (depNames.includes("hono")) techStack.push("Hono");
124
+ if (depNames.includes("prisma") || depNames.includes("@prisma/client"))
125
+ techStack.push("Prisma");
126
+ if (depNames.includes("drizzle-orm")) techStack.push("Drizzle");
127
+ if (depNames.includes("mongoose")) techStack.push("MongoDB");
128
+ if (depNames.includes("openai")) techStack.push("OpenAI");
129
+ if (depNames.includes("@anthropic-ai/sdk")) techStack.push("Claude API");
130
+ if (depNames.includes("langchain")) techStack.push("LangChain");
131
+ if (depNames.includes("typescript")) techStack.push("TypeScript");
132
+ else techStack.push("JavaScript");
133
+ return {
134
+ name: pkg.name || null,
135
+ description: pkg.description || null,
136
+ homepage: pkg.homepage || null,
137
+ techStack
138
+ };
139
+ }
140
+
141
+ // src/scanner/detectors/python.ts
142
+ async function detectPython(projectPath) {
143
+ const techStack = ["Python"];
144
+ const reqPath = join(projectPath, "requirements.txt");
145
+ const pyprojectPath = join(projectPath, "pyproject.toml");
146
+ const setupPath = join(projectPath, "setup.py");
147
+ let deps = "";
148
+ if (await fileExists(reqPath)) {
149
+ deps = await readText(reqPath);
150
+ } else if (await fileExists(pyprojectPath)) {
151
+ deps = await readText(pyprojectPath);
152
+ } else if (await fileExists(setupPath)) {
153
+ deps = await readText(setupPath);
154
+ } else {
155
+ return null;
156
+ }
157
+ const depsLower = deps.toLowerCase();
158
+ if (depsLower.includes("django")) techStack.push("Django");
159
+ if (depsLower.includes("flask")) techStack.push("Flask");
160
+ if (depsLower.includes("fastapi")) techStack.push("FastAPI");
161
+ if (depsLower.includes("pytorch") || depsLower.includes("torch"))
162
+ techStack.push("PyTorch");
163
+ if (depsLower.includes("tensorflow")) techStack.push("TensorFlow");
164
+ if (depsLower.includes("transformers")) techStack.push("Transformers");
165
+ if (depsLower.includes("langchain")) techStack.push("LangChain");
166
+ if (depsLower.includes("openai")) techStack.push("OpenAI");
167
+ if (depsLower.includes("pandas")) techStack.push("Pandas");
168
+ if (depsLower.includes("numpy")) techStack.push("NumPy");
169
+ if (depsLower.includes("scikit")) techStack.push("Scikit-learn");
170
+ return { techStack };
171
+ }
172
+
173
+ // src/scanner/detectors/rust.ts
174
+ async function detectRust(projectPath) {
175
+ const cargoPath = join(projectPath, "Cargo.toml");
176
+ if (!await fileExists(cargoPath)) return null;
177
+ const content = await readText(cargoPath);
178
+ const techStack = ["Rust"];
179
+ if (content.includes("actix")) techStack.push("Actix");
180
+ if (content.includes("axum")) techStack.push("Axum");
181
+ if (content.includes("tokio")) techStack.push("Tokio");
182
+ if (content.includes("wasm")) techStack.push("WebAssembly");
183
+ if (content.includes("tauri")) techStack.push("Tauri");
184
+ if (content.includes("diesel")) techStack.push("Diesel");
185
+ if (content.includes("sqlx")) techStack.push("SQLx");
186
+ const nameMatch = content.match(/\[package\][\s\S]*?name\s*=\s*"([^"]+)"/);
187
+ const descMatch = content.match(
188
+ /\[package\][\s\S]*?description\s*=\s*"([^"]+)"/
189
+ );
190
+ return {
191
+ name: nameMatch?.[1] || null,
192
+ description: descMatch?.[1] || null,
193
+ techStack
194
+ };
195
+ }
196
+
197
+ // src/scanner/detectors/go.ts
198
+ async function detectGo(projectPath) {
199
+ const goModPath = join(projectPath, "go.mod");
200
+ if (!await fileExists(goModPath)) return null;
201
+ const content = await readText(goModPath);
202
+ const techStack = ["Go"];
203
+ if (content.includes("gin-gonic")) techStack.push("Gin");
204
+ if (content.includes("echo")) techStack.push("Echo");
205
+ if (content.includes("fiber")) techStack.push("Fiber");
206
+ if (content.includes("grpc")) techStack.push("gRPC");
207
+ if (content.includes("gorm")) techStack.push("GORM");
208
+ if (content.includes("cobra")) techStack.push("Cobra");
209
+ if (content.includes("ent")) techStack.push("Ent");
210
+ return { techStack };
211
+ }
212
+
213
+ // src/scanner/detectors/generic.ts
214
+ import { glob } from "glob";
215
+ import { basename, extname } from "path";
216
+ var EXTENSION_MAP = {
217
+ ".ts": "TypeScript",
218
+ ".tsx": "TypeScript",
219
+ ".js": "JavaScript",
220
+ ".jsx": "JavaScript",
221
+ ".py": "Python",
222
+ ".rs": "Rust",
223
+ ".go": "Go",
224
+ ".java": "Java",
225
+ ".kt": "Kotlin",
226
+ ".swift": "Swift",
227
+ ".rb": "Ruby",
228
+ ".php": "PHP",
229
+ ".cs": "C#",
230
+ ".cpp": "C++",
231
+ ".c": "C",
232
+ ".dart": "Dart",
233
+ ".lua": "Lua",
234
+ ".zig": "Zig",
235
+ ".sol": "Solidity",
236
+ ".ex": "Elixir",
237
+ ".exs": "Elixir"
238
+ };
239
+ var IGNORE_DIRS = [
240
+ "node_modules",
241
+ ".git",
242
+ "dist",
243
+ "build",
244
+ "out",
245
+ ".next",
246
+ "target",
247
+ "vendor",
248
+ "__pycache__",
249
+ ".venv",
250
+ "venv"
251
+ ];
252
+ async function detectLanguages(projectPath) {
253
+ const ignorePattern = IGNORE_DIRS.map((d) => `**/${d}/**`);
254
+ const files = await glob("**/*.*", {
255
+ cwd: projectPath,
256
+ ignore: ignorePattern,
257
+ nodir: true,
258
+ maxDepth: 5
259
+ });
260
+ const counts = {};
261
+ for (const file of files) {
262
+ const ext = extname(file).toLowerCase();
263
+ const lang = EXTENSION_MAP[ext];
264
+ if (lang) {
265
+ counts[lang] = (counts[lang] || 0) + 1;
266
+ }
267
+ }
268
+ return counts;
269
+ }
270
+ function deriveNameFromPath(projectPath) {
271
+ return basename(projectPath);
272
+ }
273
+
274
+ // src/scanner/extractors.ts
275
+ async function extractReadme(projectPath) {
276
+ const candidates = [
277
+ "README.md",
278
+ "readme.md",
279
+ "Readme.md",
280
+ "README.txt",
281
+ "README"
282
+ ];
283
+ for (const name of candidates) {
284
+ const p = join(projectPath, name);
285
+ if (await fileExists(p)) {
286
+ const content = await readText(p);
287
+ return content.slice(0, 3e3);
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+ function extractFirstParagraph(readme) {
293
+ if (!readme) return null;
294
+ const lines = readme.split("\n");
295
+ let collecting = false;
296
+ const paragraphLines = [];
297
+ for (const line of lines) {
298
+ const trimmed = line.trim();
299
+ if (trimmed.startsWith("#")) {
300
+ if (collecting && paragraphLines.length > 0) break;
301
+ collecting = true;
302
+ continue;
303
+ }
304
+ if (collecting && trimmed === "" && paragraphLines.length > 0) break;
305
+ if (collecting && trimmed !== "") {
306
+ paragraphLines.push(trimmed);
307
+ }
308
+ if (!collecting && trimmed !== "" && !trimmed.startsWith("[")) {
309
+ paragraphLines.push(trimmed);
310
+ collecting = true;
311
+ }
312
+ }
313
+ return paragraphLines.length > 0 ? paragraphLines.join(" ") : null;
314
+ }
315
+ function extractDemoUrl(readme) {
316
+ if (!readme) return null;
317
+ const patterns = [
318
+ /(?:demo|live|website|site|url|link)[\s:]*\[?[^\]]*\]?\(?(https?:\/\/[^\s)]+)/i,
319
+ /\[(?:demo|live|website|try it)\]\((https?:\/\/[^\s)]+)\)/i,
320
+ /(https?:\/\/[^\s)]+\.(?:vercel|netlify|pages\.dev|herokuapp|railway)\.app[^\s)]*)/i
321
+ ];
322
+ for (const pattern of patterns) {
323
+ const match = readme.match(pattern);
324
+ if (match?.[1]) return match[1];
325
+ }
326
+ return null;
327
+ }
328
+
329
+ // src/utils/logger.ts
330
+ import chalk from "chalk";
331
+ var logger = {
332
+ info(msg) {
333
+ console.log(chalk.cyan(`-- ${msg}`));
334
+ },
335
+ success(msg) {
336
+ console.log(chalk.green(`-- ${msg}`));
337
+ },
338
+ warn(msg) {
339
+ console.log(chalk.yellow(`-- ${msg}`));
340
+ },
341
+ error(msg) {
342
+ console.error(chalk.red(`-- ${msg}`));
343
+ },
344
+ plain(msg) {
345
+ console.log(msg);
346
+ },
347
+ blank() {
348
+ console.log();
349
+ },
350
+ header(msg) {
351
+ console.log();
352
+ console.log(chalk.bold(msg));
353
+ console.log();
354
+ },
355
+ table(rows) {
356
+ if (rows.length === 0) return;
357
+ const colWidths = rows[0].map(
358
+ (_, colIdx) => Math.max(...rows.map((row) => (row[colIdx] || "").length))
359
+ );
360
+ for (const row of rows) {
361
+ const line = row.map((cell, i) => (cell || "").padEnd(colWidths[i] + 2)).join("");
362
+ console.log(` ${line}`);
363
+ }
364
+ }
365
+ };
366
+
367
+ // src/scanner/index.ts
368
+ import ora from "ora";
369
+ async function scanProjects(directories) {
370
+ const allRepos = [];
371
+ const spinner = ora("Scanning for projects...").start();
372
+ for (const dir of directories) {
373
+ try {
374
+ const repos = await findGitRepos(dir);
375
+ allRepos.push(...repos);
376
+ } catch (err) {
377
+ logger.warn(`Could not scan ${dir}: ${err}`);
378
+ }
379
+ }
380
+ const uniqueRepos = [...new Set(allRepos)];
381
+ spinner.text = `Found ${uniqueRepos.length} repositories. Extracting metadata...`;
382
+ const projects = [];
383
+ for (const repoPath of uniqueRepos) {
384
+ try {
385
+ const project = await extractProjectMeta(repoPath);
386
+ if (project) {
387
+ projects.push(project);
388
+ }
389
+ } catch (err) {
390
+ }
391
+ }
392
+ spinner.succeed(`Scanned ${projects.length} projects`);
393
+ return projects;
394
+ }
395
+ async function extractProjectMeta(projectPath) {
396
+ const gitMeta = await getGitMeta(projectPath);
397
+ if (!gitMeta.isRepo) return null;
398
+ let techStack = [];
399
+ let name = null;
400
+ let description = null;
401
+ let homepage = null;
402
+ const nodeInfo = await detectNode(projectPath);
403
+ if (nodeInfo) {
404
+ techStack.push(...nodeInfo.techStack);
405
+ name = nodeInfo.name || name;
406
+ description = nodeInfo.description || description;
407
+ homepage = nodeInfo.homepage || homepage;
408
+ }
409
+ const pythonInfo = await detectPython(projectPath);
410
+ if (pythonInfo) {
411
+ techStack.push(...pythonInfo.techStack);
412
+ }
413
+ const rustInfo = await detectRust(projectPath);
414
+ if (rustInfo) {
415
+ techStack.push(...rustInfo.techStack);
416
+ name = rustInfo.name || name;
417
+ description = rustInfo.description || description;
418
+ }
419
+ const goInfo = await detectGo(projectPath);
420
+ if (goInfo) {
421
+ techStack.push(...goInfo.techStack);
422
+ }
423
+ techStack = [...new Set(techStack)];
424
+ const languages = await detectLanguages(projectPath);
425
+ if (techStack.length === 0) {
426
+ const topLangs = Object.entries(languages).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([lang]) => lang);
427
+ techStack = topLangs;
428
+ }
429
+ const readmeContent = await extractReadme(projectPath);
430
+ if (!description) {
431
+ description = extractFirstParagraph(readmeContent) || "";
432
+ }
433
+ const demoUrl = homepage || extractDemoUrl(readmeContent);
434
+ if (!name) {
435
+ name = deriveNameFromPath(projectPath);
436
+ }
437
+ const id = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
438
+ return {
439
+ id,
440
+ name,
441
+ localPath: projectPath,
442
+ description: description || "",
443
+ techStack,
444
+ languages,
445
+ firstCommitDate: gitMeta.firstCommitDate || "",
446
+ lastCommitDate: gitMeta.lastCommitDate || "",
447
+ totalCommits: gitMeta.totalCommits,
448
+ remoteUrl: gitMeta.remoteUrl,
449
+ demoUrl,
450
+ readmeContent,
451
+ lastScannedCommit: gitMeta.lastCommitHash || ""
452
+ };
453
+ }
454
+ export {
455
+ scanProjects
456
+ };
457
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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 !== \"\") {\n paragraphLines.push(trimmed);\n }\n if (!collecting && trimmed !== \"\" && !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 allRepos: string[] = [];\n\n const spinner = ora(\"Scanning for projects...\").start();\n\n for (const dir of directories) {\n try {\n const repos = await findGitRepos(dir);\n allRepos.push(...repos);\n } catch (err) {\n logger.warn(`Could not scan ${dir}: ${err}`);\n }\n }\n\n // Deduplicate\n const uniqueRepos = [...new Set(allRepos)];\n spinner.text = `Found ${uniqueRepos.length} repositories. Extracting metadata...`;\n\n const projects: ProjectMeta[] = [];\n\n for (const repoPath of uniqueRepos) {\n try {\n const project = await extractProjectMeta(repoPath);\n if (project) {\n projects.push(project);\n }\n } catch (err) {\n // Skip projects that fail extraction\n }\n }\n\n spinner.succeed(`Scanned ${projects.length} projects`);\n return projects;\n}\n\nasync function extractProjectMeta(\n projectPath: string\n): Promise<ProjectMeta | null> {\n const gitMeta = await getGitMeta(projectPath);\n if (!gitMeta.isRepo) return null;\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\n const id = name\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"]}
@@ -0,0 +1,21 @@
1
+ // src/spec/builder.ts
2
+ function buildSpec(interview) {
3
+ return {
4
+ version: "1.0.0",
5
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6
+ engine: interview.engine,
7
+ framework: "next",
8
+ style: interview.style,
9
+ owner: interview.owner,
10
+ projects: interview.projects,
11
+ sections: interview.sections,
12
+ deploy: {
13
+ platform: interview.deploy.platform,
14
+ projectName: interview.deploy.projectName
15
+ }
16
+ };
17
+ }
18
+ export {
19
+ buildSpec
20
+ };
21
+ //# sourceMappingURL=builder.js.map
@@ -0,0 +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 },\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,IAChC;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,42 @@
1
+ // src/spec/diff.ts
2
+ function computeDiff(oldConfig, newScan) {
3
+ const oldProjectMap = new Map(
4
+ oldConfig.projects.map((p) => [p.localPath, p])
5
+ );
6
+ const newProjectMap = new Map(newScan.map((p) => [p.localPath, p]));
7
+ const newProjects = [];
8
+ const updatedProjects = [];
9
+ const removedProjects = [];
10
+ 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;
17
+ updatedProjects.push({
18
+ project: meta,
19
+ newCommits: Math.max(newCommits, 0),
20
+ readmeChanged: meta.readmeContent !== oldProject.readmeContent,
21
+ depsChanged: JSON.stringify(meta.techStack) !== JSON.stringify(oldProject.techStack)
22
+ });
23
+ } else {
24
+ unchangedProjects.push(oldProject);
25
+ }
26
+ }
27
+ for (const [path, project] of oldProjectMap) {
28
+ if (!newProjectMap.has(path)) {
29
+ removedProjects.push(project);
30
+ }
31
+ }
32
+ return {
33
+ newProjects,
34
+ updatedProjects,
35
+ removedProjects,
36
+ unchangedProjects
37
+ };
38
+ }
39
+ export {
40
+ computeDiff
41
+ };
42
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +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":[]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "shipfolio",
3
+ "version": "1.0.0",
4
+ "description": "Generate and deploy your personal portfolio site from local projects using AI",
5
+ "type": "module",
6
+ "bin": {
7
+ "shipfolio": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup",
11
+ "dev": "tsup --watch",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "dist",
17
+ "prompts"
18
+ ],
19
+ "keywords": [
20
+ "portfolio",
21
+ "developer",
22
+ "vibe-coding",
23
+ "personal-site",
24
+ "cli",
25
+ "ai"
26
+ ],
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@clack/prompts": "^0.9.1",
30
+ "chalk": "^5.4.1",
31
+ "commander": "^13.1.0",
32
+ "execa": "^9.5.2",
33
+ "glob": "^11.0.1",
34
+ "openai": "^4.85.0",
35
+ "ora": "^8.2.0",
36
+ "playwright": "^1.50.0",
37
+ "serve-handler": "^6.1.6",
38
+ "simple-git": "^3.27.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.12.0",
42
+ "@types/serve-handler": "^6.1.4",
43
+ "tsup": "^8.4.0",
44
+ "typescript": "^5.7.3"
45
+ },
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ }
49
+ }