@qlucent/fishi 0.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/dist/index.js +1611 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1611 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk6 from "chalk";
|
|
6
|
+
|
|
7
|
+
// src/commands/init.ts
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
import inquirer from "inquirer";
|
|
11
|
+
import path3 from "path";
|
|
12
|
+
import fs3 from "fs";
|
|
13
|
+
|
|
14
|
+
// src/analyzers/detector.ts
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
async function detectProjectType(targetDir) {
|
|
18
|
+
const checks = [];
|
|
19
|
+
const srcDirs = ["src", "app", "lib", "packages"];
|
|
20
|
+
const hasSourceCode = srcDirs.some((dir) => {
|
|
21
|
+
const dirPath = path.join(targetDir, dir);
|
|
22
|
+
if (!fs.existsSync(dirPath)) return false;
|
|
23
|
+
return hasCodeFiles(dirPath);
|
|
24
|
+
});
|
|
25
|
+
checks.push({
|
|
26
|
+
check: "Contains source code (src/, app/, lib/)",
|
|
27
|
+
passed: hasSourceCode,
|
|
28
|
+
evidence: hasSourceCode ? `Found code in ${srcDirs.filter((d) => fs.existsSync(path.join(targetDir, d))).join(", ")}` : void 0
|
|
29
|
+
});
|
|
30
|
+
const depFiles = [
|
|
31
|
+
"package.json",
|
|
32
|
+
"requirements.txt",
|
|
33
|
+
"Pipfile",
|
|
34
|
+
"go.mod",
|
|
35
|
+
"Cargo.toml",
|
|
36
|
+
"pom.xml",
|
|
37
|
+
"build.gradle",
|
|
38
|
+
"Gemfile",
|
|
39
|
+
"composer.json"
|
|
40
|
+
];
|
|
41
|
+
const foundDeps = depFiles.filter((f) => fs.existsSync(path.join(targetDir, f)));
|
|
42
|
+
checks.push({
|
|
43
|
+
check: "Has dependency/package files",
|
|
44
|
+
passed: foundDeps.length > 0,
|
|
45
|
+
evidence: foundDeps.length > 0 ? foundDeps.join(", ") : void 0
|
|
46
|
+
});
|
|
47
|
+
const hasGitHistory = await checkGitHistory(targetDir);
|
|
48
|
+
checks.push({
|
|
49
|
+
check: "Has git history with >5 commits",
|
|
50
|
+
passed: hasGitHistory
|
|
51
|
+
});
|
|
52
|
+
const testDirs = ["tests", "test", "__tests__", "spec", "e2e"];
|
|
53
|
+
const hasTests = testDirs.some((dir) => {
|
|
54
|
+
const dirPath = path.join(targetDir, dir);
|
|
55
|
+
return fs.existsSync(dirPath) && hasCodeFiles(dirPath);
|
|
56
|
+
});
|
|
57
|
+
const hasTestFiles = hasSourceCode && checkForTestFiles(targetDir);
|
|
58
|
+
checks.push({
|
|
59
|
+
check: "Has test files",
|
|
60
|
+
passed: hasTests || hasTestFiles,
|
|
61
|
+
evidence: hasTests ? `Found test directory: ${testDirs.filter((d) => fs.existsSync(path.join(targetDir, d))).join(", ")}` : void 0
|
|
62
|
+
});
|
|
63
|
+
const docIndicators = ["docs", "documentation", "README.md", "CONTRIBUTING.md"];
|
|
64
|
+
const hasDocs = docIndicators.some((f) => fs.existsSync(path.join(targetDir, f)));
|
|
65
|
+
checks.push({
|
|
66
|
+
check: "Has documentation",
|
|
67
|
+
passed: hasDocs,
|
|
68
|
+
evidence: hasDocs ? docIndicators.filter((f) => fs.existsSync(path.join(targetDir, f))).join(", ") : void 0
|
|
69
|
+
});
|
|
70
|
+
const ciIndicators = [
|
|
71
|
+
".github/workflows",
|
|
72
|
+
".gitlab-ci.yml",
|
|
73
|
+
"Jenkinsfile",
|
|
74
|
+
".circleci",
|
|
75
|
+
"Dockerfile",
|
|
76
|
+
"docker-compose.yml",
|
|
77
|
+
".travis.yml"
|
|
78
|
+
];
|
|
79
|
+
const hasCiCd = ciIndicators.some((f) => fs.existsSync(path.join(targetDir, f)));
|
|
80
|
+
checks.push({
|
|
81
|
+
check: "Has CI/CD configuration",
|
|
82
|
+
passed: hasCiCd,
|
|
83
|
+
evidence: hasCiCd ? ciIndicators.filter((f) => fs.existsSync(path.join(targetDir, f))).join(", ") : void 0
|
|
84
|
+
});
|
|
85
|
+
const passedCount = checks.filter((c) => c.passed).length;
|
|
86
|
+
let type;
|
|
87
|
+
let confidence;
|
|
88
|
+
if (passedCount <= 1) {
|
|
89
|
+
type = "greenfield";
|
|
90
|
+
confidence = Math.round((6 - passedCount) / 6 * 100);
|
|
91
|
+
} else if (passedCount >= 4) {
|
|
92
|
+
type = "brownfield";
|
|
93
|
+
confidence = Math.round(passedCount / 6 * 100);
|
|
94
|
+
} else {
|
|
95
|
+
type = passedCount >= 3 ? "brownfield" : "hybrid";
|
|
96
|
+
confidence = Math.round((passedCount + 1) / 6 * 100);
|
|
97
|
+
}
|
|
98
|
+
return { type, checks, confidence };
|
|
99
|
+
}
|
|
100
|
+
function hasCodeFiles(dirPath) {
|
|
101
|
+
const codeExtensions = [
|
|
102
|
+
".ts",
|
|
103
|
+
".tsx",
|
|
104
|
+
".js",
|
|
105
|
+
".jsx",
|
|
106
|
+
".py",
|
|
107
|
+
".go",
|
|
108
|
+
".rs",
|
|
109
|
+
".java",
|
|
110
|
+
".rb",
|
|
111
|
+
".php",
|
|
112
|
+
".cs",
|
|
113
|
+
".cpp",
|
|
114
|
+
".c",
|
|
115
|
+
".swift",
|
|
116
|
+
".kt"
|
|
117
|
+
];
|
|
118
|
+
try {
|
|
119
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (entry.isFile()) {
|
|
122
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
123
|
+
if (codeExtensions.includes(ext)) return true;
|
|
124
|
+
}
|
|
125
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
126
|
+
if (hasCodeFiles(path.join(dirPath, entry.name))) return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
function checkForTestFiles(targetDir) {
|
|
134
|
+
const testPatterns = [".test.", ".spec.", "_test.", "_spec."];
|
|
135
|
+
try {
|
|
136
|
+
const walkDir = (dir, depth) => {
|
|
137
|
+
if (depth > 3) return false;
|
|
138
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (entry.isFile() && testPatterns.some((p) => entry.name.includes(p))) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
144
|
+
if (walkDir(path.join(dir, entry.name), depth + 1)) return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
};
|
|
149
|
+
return walkDir(targetDir, 0);
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function checkGitHistory(targetDir) {
|
|
155
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
156
|
+
try {
|
|
157
|
+
const result = execSync2("git rev-list --count HEAD", {
|
|
158
|
+
cwd: targetDir,
|
|
159
|
+
encoding: "utf-8",
|
|
160
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
161
|
+
});
|
|
162
|
+
return parseInt(result.trim(), 10) > 5;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/analyzers/brownfield.ts
|
|
169
|
+
import fs2 from "fs";
|
|
170
|
+
import path2 from "path";
|
|
171
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
172
|
+
"node_modules",
|
|
173
|
+
"dist",
|
|
174
|
+
"build",
|
|
175
|
+
".next",
|
|
176
|
+
".nuxt",
|
|
177
|
+
".svelte-kit",
|
|
178
|
+
"__pycache__",
|
|
179
|
+
".git",
|
|
180
|
+
".trees",
|
|
181
|
+
"coverage",
|
|
182
|
+
".turbo",
|
|
183
|
+
".cache",
|
|
184
|
+
".output",
|
|
185
|
+
"target",
|
|
186
|
+
"vendor",
|
|
187
|
+
".venv",
|
|
188
|
+
"venv",
|
|
189
|
+
"env"
|
|
190
|
+
]);
|
|
191
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
192
|
+
".ts",
|
|
193
|
+
".tsx",
|
|
194
|
+
".js",
|
|
195
|
+
".jsx",
|
|
196
|
+
".mjs",
|
|
197
|
+
".cjs",
|
|
198
|
+
".py",
|
|
199
|
+
".go",
|
|
200
|
+
".rs",
|
|
201
|
+
".java",
|
|
202
|
+
".kt",
|
|
203
|
+
".rb",
|
|
204
|
+
".vue",
|
|
205
|
+
".svelte",
|
|
206
|
+
".astro",
|
|
207
|
+
".php",
|
|
208
|
+
".cs",
|
|
209
|
+
".cpp",
|
|
210
|
+
".c",
|
|
211
|
+
".h"
|
|
212
|
+
]);
|
|
213
|
+
var TEST_PATTERNS = [
|
|
214
|
+
/\.test\.[tj]sx?$/,
|
|
215
|
+
/\.spec\.[tj]sx?$/,
|
|
216
|
+
/test_.*\.py$/,
|
|
217
|
+
/.*_test\.py$/,
|
|
218
|
+
/.*_test\.go$/,
|
|
219
|
+
/Test\.java$/
|
|
220
|
+
];
|
|
221
|
+
var CONFIG_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
222
|
+
".json",
|
|
223
|
+
".yaml",
|
|
224
|
+
".yml",
|
|
225
|
+
".toml",
|
|
226
|
+
".ini",
|
|
227
|
+
".env",
|
|
228
|
+
".config.js",
|
|
229
|
+
".config.ts",
|
|
230
|
+
".config.mjs",
|
|
231
|
+
".config.cjs"
|
|
232
|
+
]);
|
|
233
|
+
var DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".txt", ".rst", ".adoc"]);
|
|
234
|
+
async function runBrownfieldAnalysis(targetDir) {
|
|
235
|
+
const analysis = {
|
|
236
|
+
language: null,
|
|
237
|
+
framework: null,
|
|
238
|
+
dependencyCount: 0,
|
|
239
|
+
hasTests: false,
|
|
240
|
+
hasCiCd: false,
|
|
241
|
+
conventions: [],
|
|
242
|
+
techDebt: [],
|
|
243
|
+
structure: "",
|
|
244
|
+
testFramework: null,
|
|
245
|
+
packageManager: null,
|
|
246
|
+
linter: null,
|
|
247
|
+
formatter: null,
|
|
248
|
+
cssFramework: null,
|
|
249
|
+
orm: null,
|
|
250
|
+
database: null,
|
|
251
|
+
authProvider: null,
|
|
252
|
+
apiStyle: null,
|
|
253
|
+
monorepo: false,
|
|
254
|
+
codePatterns: [],
|
|
255
|
+
fileStats: {
|
|
256
|
+
totalFiles: 0,
|
|
257
|
+
codeFiles: 0,
|
|
258
|
+
testFiles: 0,
|
|
259
|
+
configFiles: 0,
|
|
260
|
+
docFiles: 0,
|
|
261
|
+
largestFiles: []
|
|
262
|
+
},
|
|
263
|
+
existingAgents: []
|
|
264
|
+
};
|
|
265
|
+
const allFiles = collectFiles(targetDir, 5);
|
|
266
|
+
analysis.fileStats = computeFileStats(targetDir, allFiles);
|
|
267
|
+
analysis.packageManager = detectPackageManager(targetDir);
|
|
268
|
+
const packageJsonPath = path2.join(targetDir, "package.json");
|
|
269
|
+
let pkg = null;
|
|
270
|
+
let allDeps = {};
|
|
271
|
+
if (fs2.existsSync(packageJsonPath)) {
|
|
272
|
+
try {
|
|
273
|
+
pkg = JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8"));
|
|
274
|
+
allDeps = {
|
|
275
|
+
...pkg?.dependencies || {},
|
|
276
|
+
...pkg?.devDependencies || {}
|
|
277
|
+
};
|
|
278
|
+
analysis.dependencyCount = Object.keys(allDeps).length;
|
|
279
|
+
if (allDeps.typescript || allDeps["ts-node"] || fs2.existsSync(path2.join(targetDir, "tsconfig.json"))) {
|
|
280
|
+
analysis.language = "typescript";
|
|
281
|
+
} else {
|
|
282
|
+
analysis.language = "javascript";
|
|
283
|
+
}
|
|
284
|
+
if (allDeps.next) analysis.framework = "nextjs";
|
|
285
|
+
else if (allDeps.nuxt) analysis.framework = "nuxt";
|
|
286
|
+
else if (allDeps["@sveltejs/kit"]) analysis.framework = "sveltekit";
|
|
287
|
+
else if (allDeps.svelte) analysis.framework = "svelte";
|
|
288
|
+
else if (allDeps.astro || allDeps["@astrojs/node"]) analysis.framework = "astro";
|
|
289
|
+
else if (allDeps.react) analysis.framework = "react";
|
|
290
|
+
else if (allDeps.vue) analysis.framework = "vue";
|
|
291
|
+
else if (allDeps.express) analysis.framework = "express";
|
|
292
|
+
else if (allDeps.fastify) analysis.framework = "fastify";
|
|
293
|
+
else if (allDeps.hono) analysis.framework = "hono";
|
|
294
|
+
else if (allDeps["@nestjs/core"]) analysis.framework = "nestjs";
|
|
295
|
+
else if (allDeps.remix || allDeps["@remix-run/node"]) analysis.framework = "remix";
|
|
296
|
+
if (allDeps.vitest) {
|
|
297
|
+
analysis.testFramework = "vitest";
|
|
298
|
+
analysis.hasTests = true;
|
|
299
|
+
} else if (allDeps.jest || allDeps["@jest/core"]) {
|
|
300
|
+
analysis.testFramework = "jest";
|
|
301
|
+
analysis.hasTests = true;
|
|
302
|
+
} else if (allDeps.mocha) {
|
|
303
|
+
analysis.testFramework = "mocha";
|
|
304
|
+
analysis.hasTests = true;
|
|
305
|
+
} else if (allDeps["@playwright/test"]) {
|
|
306
|
+
analysis.testFramework = "playwright";
|
|
307
|
+
analysis.hasTests = true;
|
|
308
|
+
} else if (allDeps.cypress) {
|
|
309
|
+
analysis.testFramework = "cypress";
|
|
310
|
+
analysis.hasTests = true;
|
|
311
|
+
}
|
|
312
|
+
analysis.linter = detectLinter(targetDir, allDeps);
|
|
313
|
+
analysis.formatter = detectFormatter(targetDir, allDeps);
|
|
314
|
+
analysis.cssFramework = detectCssFramework(allDeps);
|
|
315
|
+
analysis.orm = detectOrm(allDeps);
|
|
316
|
+
analysis.database = detectDatabase(targetDir, allDeps);
|
|
317
|
+
analysis.authProvider = detectAuthProvider(allDeps);
|
|
318
|
+
analysis.apiStyle = detectApiStyle(targetDir, allDeps);
|
|
319
|
+
analysis.monorepo = detectMonorepo(targetDir, pkg);
|
|
320
|
+
if (allDeps.eslint || analysis.linter === "eslint") analysis.conventions.push("ESLint configured");
|
|
321
|
+
if (allDeps.prettier || analysis.formatter === "prettier") analysis.conventions.push("Prettier configured");
|
|
322
|
+
if (analysis.linter === "biome" || analysis.formatter === "biome") analysis.conventions.push("Biome configured");
|
|
323
|
+
if (allDeps.husky) analysis.conventions.push("Husky git hooks");
|
|
324
|
+
if (allDeps["lint-staged"]) analysis.conventions.push("Lint-staged configured");
|
|
325
|
+
if (pkg?.type === "module") analysis.conventions.push("ES Modules");
|
|
326
|
+
if (allDeps["@commitlint/cli"] || allDeps["commitlint"]) analysis.conventions.push("Commitlint configured");
|
|
327
|
+
if (allDeps["semantic-release"]) analysis.conventions.push("Semantic release configured");
|
|
328
|
+
if (allDeps.changesets || allDeps["@changesets/cli"]) analysis.conventions.push("Changesets configured");
|
|
329
|
+
if (analysis.testFramework) analysis.conventions.push(`${capitalize(analysis.testFramework)} testing`);
|
|
330
|
+
const nodeEngines = pkg?.engines;
|
|
331
|
+
if (nodeEngines?.node) {
|
|
332
|
+
const version = parseInt(nodeEngines.node.replace(/[^0-9]/g, ""));
|
|
333
|
+
if (version < 18) {
|
|
334
|
+
analysis.techDebt.push("Node.js version below 18");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (allDeps.tslint) analysis.techDebt.push("TSLint is deprecated \u2014 migrate to ESLint");
|
|
338
|
+
if (allDeps.moment) analysis.techDebt.push("moment.js \u2014 consider date-fns or dayjs");
|
|
339
|
+
if (allDeps.request) analysis.techDebt.push("request is deprecated \u2014 use fetch or undici");
|
|
340
|
+
if (allDeps.lodash && !allDeps["lodash-es"]) analysis.techDebt.push("Full lodash bundle \u2014 consider lodash-es or individual imports");
|
|
341
|
+
if (analysis.dependencyCount > 100) analysis.techDebt.push(`High dependency count (${analysis.dependencyCount})`);
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const requirementsPath = path2.join(targetDir, "requirements.txt");
|
|
346
|
+
const pyprojectPath = path2.join(targetDir, "pyproject.toml");
|
|
347
|
+
if (fs2.existsSync(requirementsPath) || fs2.existsSync(pyprojectPath)) {
|
|
348
|
+
analysis.language = analysis.language || "python";
|
|
349
|
+
if (fs2.existsSync(pyprojectPath)) {
|
|
350
|
+
const pyproject = fs2.readFileSync(pyprojectPath, "utf-8");
|
|
351
|
+
if (pyproject.includes("pytest")) {
|
|
352
|
+
analysis.testFramework = "pytest";
|
|
353
|
+
analysis.hasTests = true;
|
|
354
|
+
}
|
|
355
|
+
if (pyproject.includes("ruff")) analysis.linter = "ruff";
|
|
356
|
+
else if (pyproject.includes("flake8")) analysis.linter = "flake8";
|
|
357
|
+
if (pyproject.includes("black")) analysis.formatter = "black";
|
|
358
|
+
else if (pyproject.includes("ruff")) analysis.formatter = analysis.formatter || "ruff";
|
|
359
|
+
if (pyproject.includes("poetry")) analysis.packageManager = "poetry";
|
|
360
|
+
else if (pyproject.includes("pdm")) analysis.packageManager = "pdm";
|
|
361
|
+
if (pyproject.includes("sqlalchemy")) analysis.orm = "sqlalchemy";
|
|
362
|
+
else if (pyproject.includes("django")) analysis.orm = "django-orm";
|
|
363
|
+
if (pyproject.includes("django")) analysis.framework = "django";
|
|
364
|
+
else if (pyproject.includes("fastapi")) analysis.framework = "fastapi";
|
|
365
|
+
else if (pyproject.includes("flask")) analysis.framework = "flask";
|
|
366
|
+
}
|
|
367
|
+
if (fs2.existsSync(requirementsPath)) {
|
|
368
|
+
const reqs = fs2.readFileSync(requirementsPath, "utf-8");
|
|
369
|
+
analysis.dependencyCount = reqs.split("\n").filter((l) => l.trim() && !l.startsWith("#")).length;
|
|
370
|
+
if (reqs.includes("django")) analysis.framework = analysis.framework || "django";
|
|
371
|
+
else if (reqs.includes("fastapi")) analysis.framework = analysis.framework || "fastapi";
|
|
372
|
+
else if (reqs.includes("flask")) analysis.framework = analysis.framework || "flask";
|
|
373
|
+
if (reqs.includes("pytest")) {
|
|
374
|
+
analysis.testFramework = "pytest";
|
|
375
|
+
analysis.hasTests = true;
|
|
376
|
+
}
|
|
377
|
+
if (reqs.includes("sqlalchemy")) analysis.orm = analysis.orm || "sqlalchemy";
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (fs2.existsSync(path2.join(targetDir, "go.mod"))) {
|
|
381
|
+
analysis.language = analysis.language || "go";
|
|
382
|
+
}
|
|
383
|
+
if (fs2.existsSync(path2.join(targetDir, "Cargo.toml"))) {
|
|
384
|
+
analysis.language = analysis.language || "rust";
|
|
385
|
+
}
|
|
386
|
+
analysis.hasCiCd = fs2.existsSync(path2.join(targetDir, ".github", "workflows")) || fs2.existsSync(path2.join(targetDir, ".gitlab-ci.yml")) || fs2.existsSync(path2.join(targetDir, ".circleci")) || fs2.existsSync(path2.join(targetDir, "Jenkinsfile")) || fs2.existsSync(path2.join(targetDir, "Dockerfile"));
|
|
387
|
+
if (!analysis.hasTests) {
|
|
388
|
+
const testDirs = ["tests", "test", "__tests__", "spec"];
|
|
389
|
+
analysis.hasTests = testDirs.some((d) => fs2.existsSync(path2.join(targetDir, d)));
|
|
390
|
+
}
|
|
391
|
+
analysis.codePatterns = detectCodePatterns(targetDir, allFiles);
|
|
392
|
+
analysis.existingAgents = detectExistingAgents(targetDir);
|
|
393
|
+
analysis.structure = mapDirectoryStructure(targetDir, 3);
|
|
394
|
+
return analysis;
|
|
395
|
+
}
|
|
396
|
+
function detectPackageManager(targetDir) {
|
|
397
|
+
if (fs2.existsSync(path2.join(targetDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
398
|
+
if (fs2.existsSync(path2.join(targetDir, "bun.lockb")) || fs2.existsSync(path2.join(targetDir, "bun.lock"))) return "bun";
|
|
399
|
+
if (fs2.existsSync(path2.join(targetDir, "yarn.lock"))) return "yarn";
|
|
400
|
+
if (fs2.existsSync(path2.join(targetDir, "package-lock.json"))) return "npm";
|
|
401
|
+
const pkgPath = path2.join(targetDir, "package.json");
|
|
402
|
+
if (fs2.existsSync(pkgPath)) {
|
|
403
|
+
try {
|
|
404
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
405
|
+
if (pkg.packageManager) {
|
|
406
|
+
if (pkg.packageManager.startsWith("pnpm")) return "pnpm";
|
|
407
|
+
if (pkg.packageManager.startsWith("yarn")) return "yarn";
|
|
408
|
+
if (pkg.packageManager.startsWith("bun")) return "bun";
|
|
409
|
+
if (pkg.packageManager.startsWith("npm")) return "npm";
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
function detectLinter(targetDir, deps) {
|
|
417
|
+
if (deps["@biomejs/biome"] || deps.biome || fs2.existsSync(path2.join(targetDir, "biome.json")) || fs2.existsSync(path2.join(targetDir, "biome.jsonc"))) {
|
|
418
|
+
return "biome";
|
|
419
|
+
}
|
|
420
|
+
if (deps.eslint || fs2.existsSync(path2.join(targetDir, ".eslintrc.json")) || fs2.existsSync(path2.join(targetDir, ".eslintrc.js")) || fs2.existsSync(path2.join(targetDir, ".eslintrc.cjs")) || fs2.existsSync(path2.join(targetDir, ".eslintrc.yml")) || fs2.existsSync(path2.join(targetDir, ".eslintrc.yaml")) || fs2.existsSync(path2.join(targetDir, ".eslintrc")) || fs2.existsSync(path2.join(targetDir, "eslint.config.js")) || fs2.existsSync(path2.join(targetDir, "eslint.config.mjs")) || fs2.existsSync(path2.join(targetDir, "eslint.config.ts"))) {
|
|
421
|
+
return "eslint";
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
function detectFormatter(targetDir, deps) {
|
|
426
|
+
if (deps["@biomejs/biome"] || deps.biome || fs2.existsSync(path2.join(targetDir, "biome.json")) || fs2.existsSync(path2.join(targetDir, "biome.jsonc"))) {
|
|
427
|
+
return "biome";
|
|
428
|
+
}
|
|
429
|
+
if (deps.prettier || fs2.existsSync(path2.join(targetDir, ".prettierrc")) || fs2.existsSync(path2.join(targetDir, ".prettierrc.json")) || fs2.existsSync(path2.join(targetDir, ".prettierrc.js")) || fs2.existsSync(path2.join(targetDir, ".prettierrc.cjs")) || fs2.existsSync(path2.join(targetDir, ".prettierrc.yaml")) || fs2.existsSync(path2.join(targetDir, ".prettierrc.yml")) || fs2.existsSync(path2.join(targetDir, ".prettierrc.toml")) || fs2.existsSync(path2.join(targetDir, "prettier.config.js")) || fs2.existsSync(path2.join(targetDir, "prettier.config.cjs")) || fs2.existsSync(path2.join(targetDir, "prettier.config.mjs"))) {
|
|
430
|
+
return "prettier";
|
|
431
|
+
}
|
|
432
|
+
if (deps.dprint || fs2.existsSync(path2.join(targetDir, "dprint.json"))) {
|
|
433
|
+
return "dprint";
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
function detectCssFramework(deps) {
|
|
438
|
+
if (deps.tailwindcss) return "tailwind";
|
|
439
|
+
if (deps["styled-components"]) return "styled-components";
|
|
440
|
+
if (deps["@emotion/styled"] || deps["@emotion/react"]) return "emotion";
|
|
441
|
+
if (deps["@chakra-ui/react"]) return "chakra-ui";
|
|
442
|
+
if (deps["@mantine/core"]) return "mantine";
|
|
443
|
+
if (deps["@mui/material"] || deps["@material-ui/core"]) return "material-ui";
|
|
444
|
+
if (deps.sass || deps["node-sass"]) return "sass";
|
|
445
|
+
if (deps["@vanilla-extract/css"]) return "vanilla-extract";
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
function detectOrm(deps) {
|
|
449
|
+
if (deps["@prisma/client"] || deps.prisma) return "prisma";
|
|
450
|
+
if (deps["drizzle-orm"]) return "drizzle";
|
|
451
|
+
if (deps.sequelize) return "sequelize";
|
|
452
|
+
if (deps.typeorm) return "typeorm";
|
|
453
|
+
if (deps.mongoose) return "mongoose";
|
|
454
|
+
if (deps.knex) return "knex";
|
|
455
|
+
if (deps.kysely) return "kysely";
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
function detectDatabase(targetDir, deps) {
|
|
459
|
+
const prismaSchemaPath = path2.join(targetDir, "prisma", "schema.prisma");
|
|
460
|
+
if (fs2.existsSync(prismaSchemaPath)) {
|
|
461
|
+
try {
|
|
462
|
+
const schema = fs2.readFileSync(prismaSchemaPath, "utf-8");
|
|
463
|
+
if (schema.includes('provider = "postgresql"') || schema.includes('provider = "postgres"')) return "postgres";
|
|
464
|
+
if (schema.includes('provider = "mysql"')) return "mysql";
|
|
465
|
+
if (schema.includes('provider = "sqlite"')) return "sqlite";
|
|
466
|
+
if (schema.includes('provider = "mongodb"')) return "mongodb";
|
|
467
|
+
if (schema.includes('provider = "sqlserver"')) return "sqlserver";
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const envFiles = [".env", ".env.local", ".env.development"];
|
|
472
|
+
for (const envFile of envFiles) {
|
|
473
|
+
const envPath = path2.join(targetDir, envFile);
|
|
474
|
+
if (fs2.existsSync(envPath)) {
|
|
475
|
+
try {
|
|
476
|
+
const envContent = fs2.readFileSync(envPath, "utf-8");
|
|
477
|
+
if (envContent.includes("postgres://") || envContent.includes("postgresql://")) return "postgres";
|
|
478
|
+
if (envContent.includes("mysql://")) return "mysql";
|
|
479
|
+
if (envContent.includes("mongodb://") || envContent.includes("mongodb+srv://")) return "mongodb";
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (deps.pg || deps["@neondatabase/serverless"] || deps["postgres"]) return "postgres";
|
|
485
|
+
if (deps.mysql2 || deps.mysql) return "mysql";
|
|
486
|
+
if (deps.mongodb) return "mongodb";
|
|
487
|
+
if (deps["better-sqlite3"] || deps.sqlite3) return "sqlite";
|
|
488
|
+
if (deps.redis || deps.ioredis) return "redis";
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
function detectAuthProvider(deps) {
|
|
492
|
+
const depKeys = Object.keys(deps);
|
|
493
|
+
if (depKeys.some((k) => k.startsWith("@clerk/"))) return "clerk";
|
|
494
|
+
if (deps["next-auth"] || deps["@auth/core"]) return "next-auth";
|
|
495
|
+
if (depKeys.some((k) => k.startsWith("@auth0/"))) return "auth0";
|
|
496
|
+
if (deps["@supabase/auth-helpers-nextjs"] || deps["@supabase/supabase-js"]) return "supabase-auth";
|
|
497
|
+
if (deps.passport) return "passport";
|
|
498
|
+
if (deps["firebase-admin"] || deps.firebase) return "firebase-auth";
|
|
499
|
+
if (depKeys.some((k) => k.startsWith("@lucia-auth/"))) return "lucia";
|
|
500
|
+
if (deps["better-auth"]) return "better-auth";
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
function detectApiStyle(targetDir, deps) {
|
|
504
|
+
const depKeys = Object.keys(deps);
|
|
505
|
+
if (deps.graphql || deps["@apollo/server"] || deps["@apollo/client"] || deps["graphql-yoga"] || deps.urql) {
|
|
506
|
+
return "graphql";
|
|
507
|
+
}
|
|
508
|
+
if (depKeys.some((k) => k.startsWith("@trpc/"))) return "trpc";
|
|
509
|
+
if (fs2.existsSync(path2.join(targetDir, "swagger.json")) || fs2.existsSync(path2.join(targetDir, "openapi.json")) || fs2.existsSync(path2.join(targetDir, "openapi.yaml")) || fs2.existsSync(path2.join(targetDir, "openapi.yml")) || deps["swagger-ui-express"] || deps["@nestjs/swagger"]) {
|
|
510
|
+
return "rest";
|
|
511
|
+
}
|
|
512
|
+
if (deps["@grpc/grpc-js"] || deps["@grpc/proto-loader"]) return "grpc";
|
|
513
|
+
if (deps.express || deps.fastify || deps.hono) return "rest";
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
function detectMonorepo(targetDir, pkg) {
|
|
517
|
+
if (fs2.existsSync(path2.join(targetDir, "turbo.json"))) return true;
|
|
518
|
+
if (fs2.existsSync(path2.join(targetDir, "nx.json"))) return true;
|
|
519
|
+
if (fs2.existsSync(path2.join(targetDir, "lerna.json"))) return true;
|
|
520
|
+
if (fs2.existsSync(path2.join(targetDir, "pnpm-workspace.yaml"))) return true;
|
|
521
|
+
if (pkg?.workspaces) return true;
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
function collectFiles(dir, maxDepth, depth = 0) {
|
|
525
|
+
if (depth >= maxDepth) return [];
|
|
526
|
+
const files = [];
|
|
527
|
+
try {
|
|
528
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
529
|
+
for (const entry of entries) {
|
|
530
|
+
if (entry.name.startsWith(".") && depth === 0 && entry.name !== ".env") continue;
|
|
531
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
532
|
+
const fullPath = path2.join(dir, entry.name);
|
|
533
|
+
if (entry.isDirectory()) {
|
|
534
|
+
files.push(...collectFiles(fullPath, maxDepth, depth + 1));
|
|
535
|
+
} else {
|
|
536
|
+
files.push(fullPath);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
}
|
|
541
|
+
return files;
|
|
542
|
+
}
|
|
543
|
+
function computeFileStats(targetDir, allFiles) {
|
|
544
|
+
const stats = {
|
|
545
|
+
totalFiles: allFiles.length,
|
|
546
|
+
codeFiles: 0,
|
|
547
|
+
testFiles: 0,
|
|
548
|
+
configFiles: 0,
|
|
549
|
+
docFiles: 0,
|
|
550
|
+
largestFiles: []
|
|
551
|
+
};
|
|
552
|
+
const codeFileSizes = [];
|
|
553
|
+
for (const filePath of allFiles) {
|
|
554
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
555
|
+
const basename = path2.basename(filePath);
|
|
556
|
+
const relativePath = path2.relative(targetDir, filePath);
|
|
557
|
+
const isTest = TEST_PATTERNS.some((p) => p.test(basename));
|
|
558
|
+
const isCode = CODE_EXTENSIONS.has(ext);
|
|
559
|
+
const isConfig = CONFIG_EXTENSIONS.has(ext) || basename.includes(".config.");
|
|
560
|
+
const isDoc = DOC_EXTENSIONS.has(ext);
|
|
561
|
+
if (isTest) {
|
|
562
|
+
stats.testFiles++;
|
|
563
|
+
stats.codeFiles++;
|
|
564
|
+
} else if (isCode) {
|
|
565
|
+
stats.codeFiles++;
|
|
566
|
+
}
|
|
567
|
+
if (isConfig) stats.configFiles++;
|
|
568
|
+
if (isDoc) stats.docFiles++;
|
|
569
|
+
if (isCode) {
|
|
570
|
+
try {
|
|
571
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
572
|
+
const lineCount = content.split("\n").length;
|
|
573
|
+
codeFileSizes.push({ path: relativePath, lines: lineCount });
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
codeFileSizes.sort((a, b) => b.lines - a.lines);
|
|
579
|
+
stats.largestFiles = codeFileSizes.slice(0, 5);
|
|
580
|
+
return stats;
|
|
581
|
+
}
|
|
582
|
+
function detectCodePatterns(targetDir, allFiles) {
|
|
583
|
+
const patterns = [];
|
|
584
|
+
const indexFiles = allFiles.filter((f) => {
|
|
585
|
+
const base = path2.basename(f);
|
|
586
|
+
return base === "index.ts" || base === "index.js" || base === "index.tsx" || base === "index.jsx";
|
|
587
|
+
});
|
|
588
|
+
if (indexFiles.length > 3) {
|
|
589
|
+
let hasBarrel = false;
|
|
590
|
+
for (const indexFile of indexFiles.slice(0, 5)) {
|
|
591
|
+
try {
|
|
592
|
+
const content = fs2.readFileSync(indexFile, "utf-8");
|
|
593
|
+
if (content.includes("export {") || content.includes("export *") || content.includes("export { default")) {
|
|
594
|
+
hasBarrel = true;
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
} catch {
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (hasBarrel) {
|
|
601
|
+
patterns.push({
|
|
602
|
+
name: "barrel-exports",
|
|
603
|
+
evidence: `Found ${indexFiles.length} index files with re-exports`,
|
|
604
|
+
confidence: 85
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const srcDir = path2.join(targetDir, "src");
|
|
609
|
+
if (fs2.existsSync(srcDir)) {
|
|
610
|
+
try {
|
|
611
|
+
const srcEntries = fs2.readdirSync(srcDir, { withFileTypes: true });
|
|
612
|
+
const featureDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
613
|
+
if (featureDirs.includes("features") || featureDirs.includes("modules")) {
|
|
614
|
+
patterns.push({
|
|
615
|
+
name: "feature-folders",
|
|
616
|
+
evidence: `src/ contains feature-based directory structure`,
|
|
617
|
+
confidence: 90
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
} catch {
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const mvcIndicators = ["controllers", "models", "views", "services", "routes"];
|
|
624
|
+
const foundMvc = mvcIndicators.filter((dir) => {
|
|
625
|
+
return fs2.existsSync(path2.join(targetDir, "src", dir)) || fs2.existsSync(path2.join(targetDir, dir));
|
|
626
|
+
});
|
|
627
|
+
if (foundMvc.length >= 3) {
|
|
628
|
+
patterns.push({
|
|
629
|
+
name: "mvc",
|
|
630
|
+
evidence: `Found directories: ${foundMvc.join(", ")}`,
|
|
631
|
+
confidence: 80
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
const cleanIndicators = ["domain", "application", "infrastructure", "ports", "adapters"];
|
|
635
|
+
const foundClean = cleanIndicators.filter((dir) => {
|
|
636
|
+
return fs2.existsSync(path2.join(targetDir, "src", dir));
|
|
637
|
+
});
|
|
638
|
+
if (foundClean.length >= 2) {
|
|
639
|
+
patterns.push({
|
|
640
|
+
name: "clean-architecture",
|
|
641
|
+
evidence: `Found directories: ${foundClean.join(", ")}`,
|
|
642
|
+
confidence: 75
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
const testFilesInSrc = allFiles.filter((f) => {
|
|
646
|
+
const rel = path2.relative(targetDir, f);
|
|
647
|
+
return (rel.startsWith("src") || rel.startsWith("app")) && TEST_PATTERNS.some((p) => p.test(path2.basename(f)));
|
|
648
|
+
});
|
|
649
|
+
if (testFilesInSrc.length > 2) {
|
|
650
|
+
patterns.push({
|
|
651
|
+
name: "colocated-tests",
|
|
652
|
+
evidence: `Found ${testFilesInSrc.length} test files alongside source files`,
|
|
653
|
+
confidence: 85
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
const componentDirs = allFiles.filter((f) => {
|
|
657
|
+
const rel = path2.relative(targetDir, f);
|
|
658
|
+
return rel.includes("components") && CODE_EXTENSIONS.has(path2.extname(f));
|
|
659
|
+
});
|
|
660
|
+
if (componentDirs.length > 5) {
|
|
661
|
+
patterns.push({
|
|
662
|
+
name: "component-driven",
|
|
663
|
+
evidence: `Found ${componentDirs.length} files in component directories`,
|
|
664
|
+
confidence: 80
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
if (fs2.existsSync(path2.join(targetDir, "app")) || fs2.existsSync(path2.join(targetDir, "src", "app"))) {
|
|
668
|
+
const appDir = fs2.existsSync(path2.join(targetDir, "src", "app")) ? path2.join(targetDir, "src", "app") : path2.join(targetDir, "app");
|
|
669
|
+
try {
|
|
670
|
+
const entries = fs2.readdirSync(appDir);
|
|
671
|
+
if (entries.some((e) => e.startsWith("page.") || e.startsWith("layout."))) {
|
|
672
|
+
patterns.push({
|
|
673
|
+
name: "app-router",
|
|
674
|
+
evidence: "Next.js App Router pattern detected",
|
|
675
|
+
confidence: 95
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
} catch {
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (fs2.existsSync(path2.join(targetDir, "pages")) || fs2.existsSync(path2.join(targetDir, "src", "pages"))) {
|
|
682
|
+
patterns.push({
|
|
683
|
+
name: "file-based-routing",
|
|
684
|
+
evidence: "pages/ directory detected",
|
|
685
|
+
confidence: 80
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
return patterns;
|
|
689
|
+
}
|
|
690
|
+
function detectExistingAgents(targetDir) {
|
|
691
|
+
const agentsDir = path2.join(targetDir, ".claude", "agents");
|
|
692
|
+
if (!fs2.existsSync(agentsDir)) return [];
|
|
693
|
+
const agents = [];
|
|
694
|
+
try {
|
|
695
|
+
const walk = (dir) => {
|
|
696
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
697
|
+
for (const entry of entries) {
|
|
698
|
+
if (entry.isDirectory()) {
|
|
699
|
+
walk(path2.join(dir, entry.name));
|
|
700
|
+
} else if (entry.name.endsWith(".md")) {
|
|
701
|
+
agents.push(path2.relative(agentsDir, path2.join(dir, entry.name)));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
walk(agentsDir);
|
|
706
|
+
} catch {
|
|
707
|
+
}
|
|
708
|
+
return agents;
|
|
709
|
+
}
|
|
710
|
+
function mapDirectoryStructure(dir, maxDepth, depth = 0) {
|
|
711
|
+
if (depth >= maxDepth) return "";
|
|
712
|
+
const indent = " ".repeat(depth);
|
|
713
|
+
let result = "";
|
|
714
|
+
try {
|
|
715
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
716
|
+
const filtered = entries.filter(
|
|
717
|
+
(e) => !e.name.startsWith(".") && !IGNORED_DIRS.has(e.name)
|
|
718
|
+
);
|
|
719
|
+
for (const entry of filtered.slice(0, 25)) {
|
|
720
|
+
if (entry.isDirectory()) {
|
|
721
|
+
result += `${indent}${entry.name}/
|
|
722
|
+
`;
|
|
723
|
+
result += mapDirectoryStructure(path2.join(dir, entry.name), maxDepth, depth + 1);
|
|
724
|
+
} else {
|
|
725
|
+
result += `${indent}${entry.name}
|
|
726
|
+
`;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (filtered.length > 25) {
|
|
730
|
+
result += `${indent}... and ${filtered.length - 25} more
|
|
731
|
+
`;
|
|
732
|
+
}
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
return result;
|
|
736
|
+
}
|
|
737
|
+
function capitalize(s) {
|
|
738
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/analyzers/brownfield-report.ts
|
|
742
|
+
function generateBrownfieldReport(analysis) {
|
|
743
|
+
const sections = [];
|
|
744
|
+
sections.push(`# Brownfield Analysis Report
|
|
745
|
+
|
|
746
|
+
> Auto-generated by FISHI brownfield analyzer.
|
|
747
|
+
> This report captures the state of the codebase at initialization time.
|
|
748
|
+
|
|
749
|
+
---`);
|
|
750
|
+
sections.push(buildArchitectureSection(analysis));
|
|
751
|
+
sections.push(buildTechStackSection(analysis));
|
|
752
|
+
sections.push(buildToolingSection(analysis));
|
|
753
|
+
sections.push(buildStatsSection(analysis));
|
|
754
|
+
sections.push(buildConventionsSection(analysis));
|
|
755
|
+
sections.push(buildTechDebtSection(analysis));
|
|
756
|
+
sections.push(buildRecommendationsSection(analysis));
|
|
757
|
+
return sections.join("\n\n");
|
|
758
|
+
}
|
|
759
|
+
function buildArchitectureSection(analysis) {
|
|
760
|
+
let section = `## Architecture Overview
|
|
761
|
+
`;
|
|
762
|
+
if (analysis.monorepo) {
|
|
763
|
+
section += `
|
|
764
|
+
**Monorepo**: Yes
|
|
765
|
+
`;
|
|
766
|
+
}
|
|
767
|
+
if (analysis.codePatterns.length > 0) {
|
|
768
|
+
section += `
|
|
769
|
+
### Detected Patterns
|
|
770
|
+
|
|
771
|
+
`;
|
|
772
|
+
for (const pattern of analysis.codePatterns) {
|
|
773
|
+
const confidenceLabel = pattern.confidence >= 80 ? "high" : pattern.confidence >= 60 ? "medium" : "low";
|
|
774
|
+
section += `- **${pattern.name}** (${confidenceLabel} confidence) \u2014 ${pattern.evidence}
|
|
775
|
+
`;
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
section += `
|
|
779
|
+
No strong architectural patterns detected.
|
|
780
|
+
`;
|
|
781
|
+
}
|
|
782
|
+
if (analysis.structure) {
|
|
783
|
+
section += `
|
|
784
|
+
### Directory Structure
|
|
785
|
+
|
|
786
|
+
\`\`\`
|
|
787
|
+
${analysis.structure}\`\`\`
|
|
788
|
+
`;
|
|
789
|
+
}
|
|
790
|
+
return section;
|
|
791
|
+
}
|
|
792
|
+
function buildTechStackSection(analysis) {
|
|
793
|
+
const rows = [
|
|
794
|
+
["Language", analysis.language || "Unknown"],
|
|
795
|
+
["Framework", analysis.framework || "None detected"],
|
|
796
|
+
["ORM", analysis.orm || "None detected"],
|
|
797
|
+
["Database", analysis.database || "None detected"],
|
|
798
|
+
["Auth Provider", analysis.authProvider || "None detected"],
|
|
799
|
+
["CSS Framework", analysis.cssFramework || "None detected"],
|
|
800
|
+
["API Style", analysis.apiStyle || "None detected"]
|
|
801
|
+
];
|
|
802
|
+
let section = `## Tech Stack Inventory
|
|
803
|
+
|
|
804
|
+
`;
|
|
805
|
+
section += `| Category | Value |
|
|
806
|
+
|----------|-------|
|
|
807
|
+
`;
|
|
808
|
+
for (const [cat, val] of rows) {
|
|
809
|
+
section += `| ${cat} | ${val} |
|
|
810
|
+
`;
|
|
811
|
+
}
|
|
812
|
+
return section;
|
|
813
|
+
}
|
|
814
|
+
function buildToolingSection(analysis) {
|
|
815
|
+
const rows = [
|
|
816
|
+
["Package Manager", analysis.packageManager || "Unknown"],
|
|
817
|
+
["Linter", analysis.linter || "None detected"],
|
|
818
|
+
["Formatter", analysis.formatter || "None detected"],
|
|
819
|
+
["Test Framework", analysis.testFramework || "None detected"],
|
|
820
|
+
["Has Tests", analysis.hasTests ? "Yes" : "No"],
|
|
821
|
+
["Has CI/CD", analysis.hasCiCd ? "Yes" : "No"]
|
|
822
|
+
];
|
|
823
|
+
let section = `## Tooling Inventory
|
|
824
|
+
|
|
825
|
+
`;
|
|
826
|
+
section += `| Tool | Value |
|
|
827
|
+
|------|-------|
|
|
828
|
+
`;
|
|
829
|
+
for (const [tool, val] of rows) {
|
|
830
|
+
section += `| ${tool} | ${val} |
|
|
831
|
+
`;
|
|
832
|
+
}
|
|
833
|
+
return section;
|
|
834
|
+
}
|
|
835
|
+
function buildStatsSection(analysis) {
|
|
836
|
+
const { fileStats } = analysis;
|
|
837
|
+
let section = `## Code Statistics
|
|
838
|
+
|
|
839
|
+
`;
|
|
840
|
+
section += `| Metric | Count |
|
|
841
|
+
|--------|-------|
|
|
842
|
+
`;
|
|
843
|
+
section += `| Total Files | ${fileStats.totalFiles} |
|
|
844
|
+
`;
|
|
845
|
+
section += `| Code Files | ${fileStats.codeFiles} |
|
|
846
|
+
`;
|
|
847
|
+
section += `| Test Files | ${fileStats.testFiles} |
|
|
848
|
+
`;
|
|
849
|
+
section += `| Config Files | ${fileStats.configFiles} |
|
|
850
|
+
`;
|
|
851
|
+
section += `| Doc Files | ${fileStats.docFiles} |
|
|
852
|
+
`;
|
|
853
|
+
section += `| Dependencies | ${analysis.dependencyCount} |
|
|
854
|
+
`;
|
|
855
|
+
if (fileStats.largestFiles.length > 0) {
|
|
856
|
+
section += `
|
|
857
|
+
### Largest Code Files
|
|
858
|
+
|
|
859
|
+
`;
|
|
860
|
+
section += `| File | Lines |
|
|
861
|
+
|------|-------|
|
|
862
|
+
`;
|
|
863
|
+
for (const file of fileStats.largestFiles) {
|
|
864
|
+
section += `| \`${file.path}\` | ${file.lines} |
|
|
865
|
+
`;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return section;
|
|
869
|
+
}
|
|
870
|
+
function buildConventionsSection(analysis) {
|
|
871
|
+
let section = `## Conventions Discovered
|
|
872
|
+
|
|
873
|
+
`;
|
|
874
|
+
if (analysis.conventions.length > 0) {
|
|
875
|
+
for (const conv of analysis.conventions) {
|
|
876
|
+
section += `- ${conv}
|
|
877
|
+
`;
|
|
878
|
+
}
|
|
879
|
+
} else {
|
|
880
|
+
section += `_No specific conventions detected._
|
|
881
|
+
`;
|
|
882
|
+
}
|
|
883
|
+
return section;
|
|
884
|
+
}
|
|
885
|
+
function buildTechDebtSection(analysis) {
|
|
886
|
+
let section = `## Tech Debt Signals
|
|
887
|
+
|
|
888
|
+
`;
|
|
889
|
+
if (analysis.techDebt.length > 0) {
|
|
890
|
+
for (const debt of analysis.techDebt) {
|
|
891
|
+
section += `- ${debt}
|
|
892
|
+
`;
|
|
893
|
+
}
|
|
894
|
+
} else {
|
|
895
|
+
section += `_No significant tech debt signals detected._
|
|
896
|
+
`;
|
|
897
|
+
}
|
|
898
|
+
return section;
|
|
899
|
+
}
|
|
900
|
+
function buildRecommendationsSection(analysis) {
|
|
901
|
+
const recs = [];
|
|
902
|
+
if (!analysis.hasTests) {
|
|
903
|
+
recs.push("Add a test framework and establish a testing baseline before making changes.");
|
|
904
|
+
}
|
|
905
|
+
if (!analysis.hasCiCd) {
|
|
906
|
+
recs.push("Set up CI/CD to validate changes automatically.");
|
|
907
|
+
}
|
|
908
|
+
if (!analysis.linter) {
|
|
909
|
+
recs.push("Add a linter (ESLint or Biome recommended) to enforce code consistency.");
|
|
910
|
+
}
|
|
911
|
+
if (!analysis.formatter) {
|
|
912
|
+
recs.push("Add a formatter (Prettier or Biome recommended) to maintain consistent style.");
|
|
913
|
+
}
|
|
914
|
+
if (analysis.techDebt.length > 0) {
|
|
915
|
+
recs.push("Address tech debt items before adding new features \u2014 see Tech Debt section above.");
|
|
916
|
+
}
|
|
917
|
+
if (analysis.existingAgents.length > 0) {
|
|
918
|
+
recs.push(`Existing agent definitions found (${analysis.existingAgents.length}). Review for compatibility with FISHI agents.`);
|
|
919
|
+
}
|
|
920
|
+
if (analysis.fileStats.largestFiles.some((f) => f.lines > 500)) {
|
|
921
|
+
recs.push("Some large files detected. Consider refactoring files over 500 lines into smaller modules.");
|
|
922
|
+
}
|
|
923
|
+
if (analysis.dependencyCount > 80) {
|
|
924
|
+
recs.push("High dependency count. Audit unused dependencies with `depcheck` or similar tools.");
|
|
925
|
+
}
|
|
926
|
+
if (analysis.codePatterns.length > 0) {
|
|
927
|
+
recs.push("Respect discovered code patterns when adding new features. See Architecture section.");
|
|
928
|
+
}
|
|
929
|
+
let section = `## Recommendations for FISHI Integration
|
|
930
|
+
|
|
931
|
+
`;
|
|
932
|
+
if (recs.length > 0) {
|
|
933
|
+
for (const rec of recs) {
|
|
934
|
+
section += `- ${rec}
|
|
935
|
+
`;
|
|
936
|
+
}
|
|
937
|
+
} else {
|
|
938
|
+
section += `Codebase is in good shape. Proceed with normal FISHI workflow.
|
|
939
|
+
`;
|
|
940
|
+
}
|
|
941
|
+
return section;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/generators/scaffold.ts
|
|
945
|
+
import { generateScaffold } from "@qlucent/fishi-core";
|
|
946
|
+
async function scaffold(targetDir, options) {
|
|
947
|
+
return generateScaffold(targetDir, options);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/commands/init.ts
|
|
951
|
+
async function initCommand(description, options) {
|
|
952
|
+
const targetDir = process.cwd();
|
|
953
|
+
const projectName = path3.basename(targetDir);
|
|
954
|
+
console.log("");
|
|
955
|
+
console.log(chalk.cyan.bold(" \u{1F41F} FISHI \u2014 Fuck It, Ship It"));
|
|
956
|
+
console.log(chalk.gray(" Autonomous agent framework for Claude Code"));
|
|
957
|
+
console.log("");
|
|
958
|
+
if (fs3.existsSync(path3.join(targetDir, ".fishi"))) {
|
|
959
|
+
console.log(
|
|
960
|
+
chalk.yellow(" \u26A0 FISHI is already initialized in this directory.")
|
|
961
|
+
);
|
|
962
|
+
console.log(chalk.gray(" Run `fishi status` to see project state."));
|
|
963
|
+
console.log(
|
|
964
|
+
chalk.gray(" Run `fishi reset` to start over from a checkpoint.")
|
|
965
|
+
);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const spinner = ora("Analyzing project directory...").start();
|
|
969
|
+
const detection = await detectProjectType(targetDir);
|
|
970
|
+
spinner.succeed(
|
|
971
|
+
`Project type: ${chalk.bold(detection.type)} (${detection.confidence}% confidence)`
|
|
972
|
+
);
|
|
973
|
+
for (const check of detection.checks) {
|
|
974
|
+
const icon = check.passed ? chalk.green(" \u2713") : chalk.gray(" \u25CB");
|
|
975
|
+
console.log(`${icon} ${check.check}${check.evidence ? chalk.gray(` \u2014 ${check.evidence}`) : ""}`);
|
|
976
|
+
}
|
|
977
|
+
console.log("");
|
|
978
|
+
let initOptions;
|
|
979
|
+
if (description) {
|
|
980
|
+
console.log(chalk.green(" \u2192 Zero-config mode: using provided description"));
|
|
981
|
+
console.log("");
|
|
982
|
+
initOptions = {
|
|
983
|
+
description,
|
|
984
|
+
interactive: false,
|
|
985
|
+
costMode: options.costMode,
|
|
986
|
+
language: options.language,
|
|
987
|
+
framework: options.framework
|
|
988
|
+
};
|
|
989
|
+
} else if (options.interactive === false) {
|
|
990
|
+
console.log(chalk.yellow(" \u2192 Non-interactive mode without description"));
|
|
991
|
+
initOptions = {
|
|
992
|
+
interactive: false,
|
|
993
|
+
costMode: options.costMode,
|
|
994
|
+
language: options.language,
|
|
995
|
+
framework: options.framework
|
|
996
|
+
};
|
|
997
|
+
} else {
|
|
998
|
+
initOptions = await runWizard(options);
|
|
999
|
+
}
|
|
1000
|
+
let brownfieldAnalysis = null;
|
|
1001
|
+
if (detection.type === "brownfield" || detection.type === "hybrid") {
|
|
1002
|
+
console.log("");
|
|
1003
|
+
const analysisSpinner = ora("Running deep brownfield codebase analysis...").start();
|
|
1004
|
+
brownfieldAnalysis = await runBrownfieldAnalysis(targetDir);
|
|
1005
|
+
analysisSpinner.succeed("Brownfield analysis complete");
|
|
1006
|
+
console.log("");
|
|
1007
|
+
printBrownfieldSummary(brownfieldAnalysis);
|
|
1008
|
+
if (!initOptions.language && brownfieldAnalysis.language) {
|
|
1009
|
+
initOptions.language = brownfieldAnalysis.language;
|
|
1010
|
+
}
|
|
1011
|
+
if (!initOptions.framework && brownfieldAnalysis.framework) {
|
|
1012
|
+
initOptions.framework = brownfieldAnalysis.framework;
|
|
1013
|
+
}
|
|
1014
|
+
if (initOptions.interactive !== false) {
|
|
1015
|
+
const { proceed } = await inquirer.prompt([
|
|
1016
|
+
{
|
|
1017
|
+
type: "list",
|
|
1018
|
+
name: "proceed",
|
|
1019
|
+
message: "How would you like to proceed?",
|
|
1020
|
+
choices: [
|
|
1021
|
+
{
|
|
1022
|
+
name: "Accept analysis and scaffold FISHI",
|
|
1023
|
+
value: "accept"
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
name: "Adjust settings before scaffolding",
|
|
1027
|
+
value: "adjust"
|
|
1028
|
+
},
|
|
1029
|
+
{ name: "Cancel", value: "cancel" }
|
|
1030
|
+
]
|
|
1031
|
+
}
|
|
1032
|
+
]);
|
|
1033
|
+
if (proceed === "cancel") {
|
|
1034
|
+
console.log(chalk.yellow("\n Cancelled. No files were created."));
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (proceed === "adjust") {
|
|
1038
|
+
const adjustments = await inquirer.prompt([
|
|
1039
|
+
{
|
|
1040
|
+
type: "input",
|
|
1041
|
+
name: "language",
|
|
1042
|
+
message: "Primary language:",
|
|
1043
|
+
default: initOptions.language
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
type: "input",
|
|
1047
|
+
name: "framework",
|
|
1048
|
+
message: "Framework:",
|
|
1049
|
+
default: initOptions.framework
|
|
1050
|
+
}
|
|
1051
|
+
]);
|
|
1052
|
+
initOptions.language = adjustments.language;
|
|
1053
|
+
initOptions.framework = adjustments.framework;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
console.log("");
|
|
1058
|
+
const scaffoldSpinner = ora("Scaffolding FISHI framework...").start();
|
|
1059
|
+
try {
|
|
1060
|
+
const brownfieldData = brownfieldAnalysis ? {
|
|
1061
|
+
language: brownfieldAnalysis.language,
|
|
1062
|
+
framework: brownfieldAnalysis.framework,
|
|
1063
|
+
testFramework: brownfieldAnalysis.testFramework,
|
|
1064
|
+
packageManager: brownfieldAnalysis.packageManager,
|
|
1065
|
+
linter: brownfieldAnalysis.linter,
|
|
1066
|
+
formatter: brownfieldAnalysis.formatter,
|
|
1067
|
+
cssFramework: brownfieldAnalysis.cssFramework,
|
|
1068
|
+
orm: brownfieldAnalysis.orm,
|
|
1069
|
+
database: brownfieldAnalysis.database,
|
|
1070
|
+
authProvider: brownfieldAnalysis.authProvider,
|
|
1071
|
+
apiStyle: brownfieldAnalysis.apiStyle,
|
|
1072
|
+
monorepo: brownfieldAnalysis.monorepo,
|
|
1073
|
+
conventions: brownfieldAnalysis.conventions,
|
|
1074
|
+
codePatterns: brownfieldAnalysis.codePatterns,
|
|
1075
|
+
fileStats: {
|
|
1076
|
+
totalFiles: brownfieldAnalysis.fileStats.totalFiles,
|
|
1077
|
+
codeFiles: brownfieldAnalysis.fileStats.codeFiles,
|
|
1078
|
+
testFiles: brownfieldAnalysis.fileStats.testFiles
|
|
1079
|
+
}
|
|
1080
|
+
} : void 0;
|
|
1081
|
+
const result = await scaffold(targetDir, {
|
|
1082
|
+
...initOptions,
|
|
1083
|
+
projectName,
|
|
1084
|
+
projectType: detection.type,
|
|
1085
|
+
brownfieldAnalysis: brownfieldData
|
|
1086
|
+
});
|
|
1087
|
+
if (brownfieldAnalysis) {
|
|
1088
|
+
const reportPath = path3.join(targetDir, ".fishi", "memory", "brownfield-analysis.md");
|
|
1089
|
+
const reportDir = path3.dirname(reportPath);
|
|
1090
|
+
if (!fs3.existsSync(reportDir)) {
|
|
1091
|
+
fs3.mkdirSync(reportDir, { recursive: true });
|
|
1092
|
+
}
|
|
1093
|
+
const report = generateBrownfieldReport(brownfieldAnalysis);
|
|
1094
|
+
fs3.writeFileSync(reportPath, report, "utf-8");
|
|
1095
|
+
}
|
|
1096
|
+
scaffoldSpinner.succeed("FISHI framework scaffolded successfully!");
|
|
1097
|
+
console.log("");
|
|
1098
|
+
console.log(chalk.bold(" Created:"));
|
|
1099
|
+
console.log(chalk.gray(` \u{1F4C1} .claude/agents/ \u2014 ${result.agentCount} agents (master + coordinators + workers)`));
|
|
1100
|
+
console.log(chalk.gray(` \u{1F4C1} .claude/skills/ \u2014 ${result.skillCount} skills`));
|
|
1101
|
+
console.log(chalk.gray(` \u{1F4C1} .claude/commands/ \u2014 ${result.commandCount} slash commands`));
|
|
1102
|
+
console.log(chalk.gray(` \u{1F4C1} .fishi/ \u2014 Framework config, TaskBoard, hooks, state`));
|
|
1103
|
+
console.log(chalk.gray(` \u{1F4C4} .claude/CLAUDE.md \u2014 Project instructions`));
|
|
1104
|
+
console.log(chalk.gray(` \u{1F4C4} .claude/settings.json \u2014 Hooks & permissions`));
|
|
1105
|
+
console.log(chalk.gray(` \u{1F4C4} .mcp.json \u2014 MCP server config`));
|
|
1106
|
+
if (brownfieldAnalysis) {
|
|
1107
|
+
console.log(chalk.gray(` \u{1F4C4} .fishi/memory/brownfield-analysis.md \u2014 Codebase analysis report`));
|
|
1108
|
+
console.log("");
|
|
1109
|
+
console.log(chalk.bold(" Brownfield conventions written to CLAUDE.md:"));
|
|
1110
|
+
if (brownfieldAnalysis.packageManager)
|
|
1111
|
+
console.log(chalk.gray(` Package manager: ${brownfieldAnalysis.packageManager}`));
|
|
1112
|
+
if (brownfieldAnalysis.linter)
|
|
1113
|
+
console.log(chalk.gray(` Linter: ${brownfieldAnalysis.linter}`));
|
|
1114
|
+
if (brownfieldAnalysis.formatter)
|
|
1115
|
+
console.log(chalk.gray(` Formatter: ${brownfieldAnalysis.formatter}`));
|
|
1116
|
+
if (brownfieldAnalysis.testFramework)
|
|
1117
|
+
console.log(chalk.gray(` Test framework: ${brownfieldAnalysis.testFramework}`));
|
|
1118
|
+
if (brownfieldAnalysis.orm)
|
|
1119
|
+
console.log(chalk.gray(` ORM: ${brownfieldAnalysis.orm}`));
|
|
1120
|
+
if (brownfieldAnalysis.apiStyle)
|
|
1121
|
+
console.log(chalk.gray(` API style: ${brownfieldAnalysis.apiStyle}`));
|
|
1122
|
+
if (brownfieldAnalysis.codePatterns.length > 0)
|
|
1123
|
+
console.log(chalk.gray(` Patterns: ${brownfieldAnalysis.codePatterns.map((p) => p.name).join(", ")}`));
|
|
1124
|
+
}
|
|
1125
|
+
console.log("");
|
|
1126
|
+
console.log(chalk.cyan.bold(" \u{1F41F} Ready to ship!"));
|
|
1127
|
+
console.log(chalk.gray(" Run `claude` to start, then use `/fishi-init` to begin your project."));
|
|
1128
|
+
console.log("");
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
scaffoldSpinner.fail("Scaffolding failed");
|
|
1131
|
+
console.error(chalk.red(`
|
|
1132
|
+
Error: ${error instanceof Error ? error.message : error}`));
|
|
1133
|
+
process.exit(1);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function printBrownfieldSummary(analysis) {
|
|
1137
|
+
console.log(chalk.bold(" Codebase Analysis:"));
|
|
1138
|
+
console.log("");
|
|
1139
|
+
console.log(chalk.white(" Stack"));
|
|
1140
|
+
console.log(chalk.gray(` Language: ${analysis.language || "unknown"}`));
|
|
1141
|
+
console.log(chalk.gray(` Framework: ${analysis.framework || "none detected"}`));
|
|
1142
|
+
console.log(chalk.gray(` Dependencies: ${analysis.dependencyCount}`));
|
|
1143
|
+
if (analysis.monorepo) console.log(chalk.gray(` Monorepo: yes`));
|
|
1144
|
+
console.log("");
|
|
1145
|
+
console.log(chalk.white(" Tooling"));
|
|
1146
|
+
console.log(chalk.gray(` Package mgr: ${analysis.packageManager || "unknown"}`));
|
|
1147
|
+
console.log(chalk.gray(` Linter: ${analysis.linter || "none"}`));
|
|
1148
|
+
console.log(chalk.gray(` Formatter: ${analysis.formatter || "none"}`));
|
|
1149
|
+
console.log(chalk.gray(` Test framework: ${analysis.testFramework || "none"}`));
|
|
1150
|
+
console.log(chalk.gray(` Has tests: ${analysis.hasTests ? "yes" : "no"}`));
|
|
1151
|
+
console.log(chalk.gray(` Has CI/CD: ${analysis.hasCiCd ? "yes" : "no"}`));
|
|
1152
|
+
console.log("");
|
|
1153
|
+
if (analysis.orm || analysis.database || analysis.authProvider || analysis.apiStyle) {
|
|
1154
|
+
console.log(chalk.white(" Data & API"));
|
|
1155
|
+
if (analysis.orm) console.log(chalk.gray(` ORM: ${analysis.orm}`));
|
|
1156
|
+
if (analysis.database) console.log(chalk.gray(` Database: ${analysis.database}`));
|
|
1157
|
+
if (analysis.authProvider) console.log(chalk.gray(` Auth: ${analysis.authProvider}`));
|
|
1158
|
+
if (analysis.apiStyle) console.log(chalk.gray(` API style: ${analysis.apiStyle}`));
|
|
1159
|
+
if (analysis.cssFramework) console.log(chalk.gray(` CSS framework: ${analysis.cssFramework}`));
|
|
1160
|
+
console.log("");
|
|
1161
|
+
}
|
|
1162
|
+
console.log(chalk.white(" File Statistics"));
|
|
1163
|
+
console.log(chalk.gray(` Total files: ${analysis.fileStats.totalFiles}`));
|
|
1164
|
+
console.log(chalk.gray(` Code files: ${analysis.fileStats.codeFiles}`));
|
|
1165
|
+
console.log(chalk.gray(` Test files: ${analysis.fileStats.testFiles}`));
|
|
1166
|
+
console.log(chalk.gray(` Config files: ${analysis.fileStats.configFiles}`));
|
|
1167
|
+
console.log("");
|
|
1168
|
+
if (analysis.codePatterns.length > 0) {
|
|
1169
|
+
console.log(chalk.white(" Detected Patterns"));
|
|
1170
|
+
for (const pattern of analysis.codePatterns) {
|
|
1171
|
+
const confidence = pattern.confidence >= 80 ? chalk.green("high") : chalk.yellow("med");
|
|
1172
|
+
console.log(chalk.gray(` ${pattern.name} (${confidence}${chalk.gray(")")}`));
|
|
1173
|
+
}
|
|
1174
|
+
console.log("");
|
|
1175
|
+
}
|
|
1176
|
+
if (analysis.techDebt.length > 0) {
|
|
1177
|
+
console.log(chalk.white(" Tech Debt Signals"));
|
|
1178
|
+
for (const debt of analysis.techDebt) {
|
|
1179
|
+
console.log(chalk.yellow(` \u26A0 ${debt}`));
|
|
1180
|
+
}
|
|
1181
|
+
console.log("");
|
|
1182
|
+
}
|
|
1183
|
+
if (analysis.existingAgents.length > 0) {
|
|
1184
|
+
console.log(chalk.white(" Existing Agents"));
|
|
1185
|
+
for (const agent of analysis.existingAgents) {
|
|
1186
|
+
console.log(chalk.gray(` ${agent}`));
|
|
1187
|
+
}
|
|
1188
|
+
console.log("");
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
async function runWizard(options) {
|
|
1192
|
+
const answers = await inquirer.prompt([
|
|
1193
|
+
{
|
|
1194
|
+
type: "input",
|
|
1195
|
+
name: "description",
|
|
1196
|
+
message: "What are you building?",
|
|
1197
|
+
validate: (input) => input.trim().length > 0 || "Please describe your project"
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
type: "list",
|
|
1201
|
+
name: "costMode",
|
|
1202
|
+
message: "Cost mode:",
|
|
1203
|
+
choices: [
|
|
1204
|
+
{
|
|
1205
|
+
name: "Balanced (recommended) \u2014 Opus for critical, Sonnet for dev, Haiku for docs",
|
|
1206
|
+
value: "balanced"
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
name: "Performance \u2014 More Opus, faster results, higher cost",
|
|
1210
|
+
value: "performance"
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
name: "Economy \u2014 More Haiku, slower but cheaper",
|
|
1214
|
+
value: "economy"
|
|
1215
|
+
}
|
|
1216
|
+
],
|
|
1217
|
+
default: "balanced"
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
type: "input",
|
|
1221
|
+
name: "language",
|
|
1222
|
+
message: "Primary language (or press Enter to let FISHI decide):",
|
|
1223
|
+
default: options.language || ""
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
type: "input",
|
|
1227
|
+
name: "framework",
|
|
1228
|
+
message: "Framework (or press Enter to let FISHI decide):",
|
|
1229
|
+
default: options.framework || ""
|
|
1230
|
+
}
|
|
1231
|
+
]);
|
|
1232
|
+
return {
|
|
1233
|
+
description: answers.description,
|
|
1234
|
+
interactive: true,
|
|
1235
|
+
costMode: answers.costMode,
|
|
1236
|
+
language: answers.language || void 0,
|
|
1237
|
+
framework: answers.framework || void 0
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// src/commands/status.ts
|
|
1242
|
+
import chalk2 from "chalk";
|
|
1243
|
+
import fs4 from "fs";
|
|
1244
|
+
import path4 from "path";
|
|
1245
|
+
import { parse as parseYaml } from "yaml";
|
|
1246
|
+
async function statusCommand() {
|
|
1247
|
+
const targetDir = process.cwd();
|
|
1248
|
+
const fishiDir = path4.join(targetDir, ".fishi");
|
|
1249
|
+
if (!fs4.existsSync(fishiDir)) {
|
|
1250
|
+
console.log(chalk2.yellow("\n FISHI is not initialized in this directory."));
|
|
1251
|
+
console.log(chalk2.gray(" Run `fishi init` to get started.\n"));
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
console.log("");
|
|
1255
|
+
console.log(chalk2.cyan.bold(" \u{1F41F} FISHI Status"));
|
|
1256
|
+
console.log("");
|
|
1257
|
+
const projectPath = path4.join(fishiDir, "state", "project.yaml");
|
|
1258
|
+
if (fs4.existsSync(projectPath)) {
|
|
1259
|
+
const projectState = parseYaml(fs4.readFileSync(projectPath, "utf-8"));
|
|
1260
|
+
console.log(chalk2.bold(" Project:"));
|
|
1261
|
+
console.log(chalk2.gray(` Name: ${projectState.project_name || "unnamed"}`));
|
|
1262
|
+
console.log(chalk2.gray(` Type: ${projectState.project_type || "unknown"}`));
|
|
1263
|
+
console.log(chalk2.gray(` Phase: ${projectState.current_phase || "init"}`));
|
|
1264
|
+
console.log(chalk2.gray(` Sprint: ${projectState.current_sprint || 0}`));
|
|
1265
|
+
console.log("");
|
|
1266
|
+
}
|
|
1267
|
+
const boardPath = path4.join(fishiDir, "taskboard", "board.md");
|
|
1268
|
+
if (fs4.existsSync(boardPath)) {
|
|
1269
|
+
const boardContent = fs4.readFileSync(boardPath, "utf-8");
|
|
1270
|
+
const counts = {
|
|
1271
|
+
backlog: (boardContent.match(/## 📋 Backlog[\s\S]*?(?=## |$)/)?.[0]?.match(/### TASK-/g) || []).length,
|
|
1272
|
+
ready: (boardContent.match(/## 🟡 Ready[\s\S]*?(?=## |$)/)?.[0]?.match(/### TASK-/g) || []).length,
|
|
1273
|
+
inProgress: (boardContent.match(/## 🔵 In Progress[\s\S]*?(?=## |$)/)?.[0]?.match(/### TASK-/g) || []).length,
|
|
1274
|
+
review: (boardContent.match(/## 🟠 Review[\s\S]*?(?=## |$)/)?.[0]?.match(/### TASK-/g) || []).length,
|
|
1275
|
+
done: (boardContent.match(/## ✅ Done[\s\S]*?(?=## |$)/)?.[0]?.match(/### TASK-/g) || []).length
|
|
1276
|
+
};
|
|
1277
|
+
console.log(chalk2.bold(" TaskBoard:"));
|
|
1278
|
+
console.log(chalk2.gray(` \u{1F4CB} Backlog: ${counts.backlog}`));
|
|
1279
|
+
console.log(chalk2.yellow(` \u{1F7E1} Ready: ${counts.ready}`));
|
|
1280
|
+
console.log(chalk2.blue(` \u{1F535} In Progress: ${counts.inProgress}`));
|
|
1281
|
+
console.log(chalk2.hex("#FFA500")(` \u{1F7E0} Review: ${counts.review}`));
|
|
1282
|
+
console.log(chalk2.green(` \u2705 Done: ${counts.done}`));
|
|
1283
|
+
console.log("");
|
|
1284
|
+
}
|
|
1285
|
+
const registryPath = path4.join(fishiDir, "state", "agent-registry.yaml");
|
|
1286
|
+
if (fs4.existsSync(registryPath)) {
|
|
1287
|
+
const registry = parseYaml(fs4.readFileSync(registryPath, "utf-8"));
|
|
1288
|
+
const agents = registry?.agents || [];
|
|
1289
|
+
const active = agents.filter((a) => a.status === "active" || a.status === "working");
|
|
1290
|
+
const dynamic = agents.filter((a) => a.dynamic === true);
|
|
1291
|
+
console.log(chalk2.bold(" Agents:"));
|
|
1292
|
+
console.log(chalk2.gray(` Total: ${agents.length} (${dynamic.length} dynamic)`));
|
|
1293
|
+
console.log(chalk2.gray(` Active: ${active.length}`));
|
|
1294
|
+
for (const agent of active) {
|
|
1295
|
+
console.log(chalk2.cyan(` \u2192 ${agent.name}: ${agent.task || "idle"}`));
|
|
1296
|
+
}
|
|
1297
|
+
console.log("");
|
|
1298
|
+
}
|
|
1299
|
+
const checkpointDir = path4.join(fishiDir, "state", "checkpoints");
|
|
1300
|
+
if (fs4.existsSync(checkpointDir)) {
|
|
1301
|
+
const checkpoints = fs4.readdirSync(checkpointDir).filter((f) => f.endsWith(".yaml")).sort();
|
|
1302
|
+
if (checkpoints.length > 0) {
|
|
1303
|
+
const latest = checkpoints[checkpoints.length - 1];
|
|
1304
|
+
console.log(chalk2.bold(" Checkpoints:"));
|
|
1305
|
+
console.log(chalk2.gray(` Latest: ${latest}`));
|
|
1306
|
+
console.log(chalk2.gray(` Total: ${checkpoints.length}`));
|
|
1307
|
+
console.log("");
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
const treesDir = path4.join(targetDir, ".trees");
|
|
1311
|
+
if (fs4.existsSync(treesDir)) {
|
|
1312
|
+
const trees = fs4.readdirSync(treesDir).filter((f) => {
|
|
1313
|
+
return fs4.statSync(path4.join(treesDir, f)).isDirectory();
|
|
1314
|
+
});
|
|
1315
|
+
if (trees.length > 0) {
|
|
1316
|
+
console.log(chalk2.bold(" Active Worktrees:"));
|
|
1317
|
+
for (const tree of trees) {
|
|
1318
|
+
console.log(chalk2.gray(` \u{1F4C2} .trees/${tree}/`));
|
|
1319
|
+
}
|
|
1320
|
+
console.log("");
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// src/commands/mcp.ts
|
|
1326
|
+
import chalk3 from "chalk";
|
|
1327
|
+
import fs5 from "fs";
|
|
1328
|
+
import path5 from "path";
|
|
1329
|
+
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
1330
|
+
var MCP_CATALOG = {
|
|
1331
|
+
github: {
|
|
1332
|
+
command: "http",
|
|
1333
|
+
args: ["https://api.githubcopilot.com/mcp/"],
|
|
1334
|
+
description: "GitHub \u2014 PRs, issues, code search, Actions"
|
|
1335
|
+
},
|
|
1336
|
+
"sequential-thinking": {
|
|
1337
|
+
command: "npx",
|
|
1338
|
+
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
|
1339
|
+
description: "Sequential Thinking \u2014 complex problem decomposition"
|
|
1340
|
+
},
|
|
1341
|
+
context7: {
|
|
1342
|
+
command: "npx",
|
|
1343
|
+
args: ["-y", "@upstash/context7-mcp@latest"],
|
|
1344
|
+
description: "Context7 \u2014 up-to-date library documentation"
|
|
1345
|
+
},
|
|
1346
|
+
perplexity: {
|
|
1347
|
+
command: "npx",
|
|
1348
|
+
args: ["-y", "perplexity-mcp"],
|
|
1349
|
+
description: "Perplexity \u2014 web search with citations"
|
|
1350
|
+
},
|
|
1351
|
+
supabase: {
|
|
1352
|
+
command: "npx",
|
|
1353
|
+
args: ["-y", "supabase-mcp"],
|
|
1354
|
+
description: "Supabase \u2014 Postgres, auth, storage"
|
|
1355
|
+
},
|
|
1356
|
+
playwright: {
|
|
1357
|
+
command: "npx",
|
|
1358
|
+
args: ["-y", "@anthropic/playwright-mcp"],
|
|
1359
|
+
description: "Playwright \u2014 browser automation, E2E testing"
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
async function mcpCommand(action, name) {
|
|
1363
|
+
const targetDir = process.cwd();
|
|
1364
|
+
const mcpPath = path5.join(targetDir, ".mcp.json");
|
|
1365
|
+
switch (action) {
|
|
1366
|
+
case "list":
|
|
1367
|
+
return listMcps(mcpPath);
|
|
1368
|
+
case "add":
|
|
1369
|
+
if (!name) {
|
|
1370
|
+
console.log(chalk3.red("\n Usage: fishi mcp add <name>\n"));
|
|
1371
|
+
console.log(chalk3.bold(" Available MCPs:"));
|
|
1372
|
+
for (const [key, value] of Object.entries(MCP_CATALOG)) {
|
|
1373
|
+
console.log(chalk3.gray(` ${key} \u2014 ${value.description}`));
|
|
1374
|
+
}
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
return addMcp(mcpPath, name);
|
|
1378
|
+
case "remove":
|
|
1379
|
+
if (!name) {
|
|
1380
|
+
console.log(chalk3.red("\n Usage: fishi mcp remove <name>\n"));
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
return removeMcp(mcpPath, name);
|
|
1384
|
+
default:
|
|
1385
|
+
console.log(chalk3.red(`
|
|
1386
|
+
Unknown action: ${action}`));
|
|
1387
|
+
console.log(chalk3.gray(" Valid actions: add | list | remove\n"));
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
function listMcps(mcpPath) {
|
|
1391
|
+
if (!fs5.existsSync(mcpPath)) {
|
|
1392
|
+
console.log(chalk3.yellow("\n No .mcp.json found. Run `fishi init` first.\n"));
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
const config = JSON.parse(fs5.readFileSync(mcpPath, "utf-8"));
|
|
1396
|
+
const servers = Object.keys(config);
|
|
1397
|
+
console.log(chalk3.bold("\n Configured MCP Servers:"));
|
|
1398
|
+
if (servers.length === 0) {
|
|
1399
|
+
console.log(chalk3.gray(" None configured.\n"));
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
for (const server of servers) {
|
|
1403
|
+
const entry = config[server];
|
|
1404
|
+
const catalogEntry = MCP_CATALOG[server];
|
|
1405
|
+
const desc = catalogEntry ? chalk3.gray(` \u2014 ${catalogEntry.description}`) : "";
|
|
1406
|
+
console.log(chalk3.cyan(` \u2713 ${server}`) + desc);
|
|
1407
|
+
}
|
|
1408
|
+
console.log("");
|
|
1409
|
+
}
|
|
1410
|
+
function addMcp(mcpPath, name) {
|
|
1411
|
+
const catalog = MCP_CATALOG[name];
|
|
1412
|
+
if (!catalog) {
|
|
1413
|
+
console.log(chalk3.yellow(`
|
|
1414
|
+
"${name}" not found in FISHI catalog.`));
|
|
1415
|
+
console.log(chalk3.gray(" Available: " + Object.keys(MCP_CATALOG).join(", ")));
|
|
1416
|
+
console.log(chalk3.gray(" You can manually add it to .mcp.json\n"));
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
let config = {};
|
|
1420
|
+
if (fs5.existsSync(mcpPath)) {
|
|
1421
|
+
config = JSON.parse(fs5.readFileSync(mcpPath, "utf-8"));
|
|
1422
|
+
}
|
|
1423
|
+
if (config[name]) {
|
|
1424
|
+
console.log(chalk3.yellow(`
|
|
1425
|
+
"${name}" is already configured.
|
|
1426
|
+
`));
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
if (catalog.command === "http") {
|
|
1430
|
+
config[name] = { type: "http", url: catalog.args[0] };
|
|
1431
|
+
} else {
|
|
1432
|
+
config[name] = { type: "stdio", command: catalog.command, args: catalog.args };
|
|
1433
|
+
}
|
|
1434
|
+
fs5.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1435
|
+
console.log(chalk3.green(`
|
|
1436
|
+
\u2713 Added ${name}: ${catalog.description}
|
|
1437
|
+
`));
|
|
1438
|
+
const registryPath = path5.join(path5.dirname(mcpPath), ".fishi", "mcp-registry.yaml");
|
|
1439
|
+
if (fs5.existsSync(registryPath)) {
|
|
1440
|
+
const registry = parseYaml2(fs5.readFileSync(registryPath, "utf-8")) || {};
|
|
1441
|
+
if (!registry.installed_mcps) registry.installed_mcps = { core: [], project: [], user: [] };
|
|
1442
|
+
if (!registry.installed_mcps.project.includes(name)) {
|
|
1443
|
+
registry.installed_mcps.project.push(name);
|
|
1444
|
+
fs5.writeFileSync(registryPath, stringifyYaml(registry));
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
function removeMcp(mcpPath, name) {
|
|
1449
|
+
if (!fs5.existsSync(mcpPath)) {
|
|
1450
|
+
console.log(chalk3.yellow("\n No .mcp.json found.\n"));
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
const config = JSON.parse(fs5.readFileSync(mcpPath, "utf-8"));
|
|
1454
|
+
if (!config[name]) {
|
|
1455
|
+
console.log(chalk3.yellow(`
|
|
1456
|
+
"${name}" is not configured.
|
|
1457
|
+
`));
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
delete config[name];
|
|
1461
|
+
fs5.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1462
|
+
console.log(chalk3.green(`
|
|
1463
|
+
\u2713 Removed ${name}
|
|
1464
|
+
`));
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// src/commands/reset.ts
|
|
1468
|
+
import chalk4 from "chalk";
|
|
1469
|
+
import fs6 from "fs";
|
|
1470
|
+
import path6 from "path";
|
|
1471
|
+
import { parse as parseYaml3 } from "yaml";
|
|
1472
|
+
async function resetCommand(checkpoint) {
|
|
1473
|
+
const targetDir = process.cwd();
|
|
1474
|
+
const checkpointDir = path6.join(targetDir, ".fishi", "state", "checkpoints");
|
|
1475
|
+
if (!fs6.existsSync(checkpointDir)) {
|
|
1476
|
+
console.log(chalk4.yellow("\n No checkpoints found. Is FISHI initialized?\n"));
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
const checkpoints = fs6.readdirSync(checkpointDir).filter((f) => f.endsWith(".yaml")).sort();
|
|
1480
|
+
if (checkpoints.length === 0) {
|
|
1481
|
+
console.log(chalk4.yellow("\n No checkpoints available.\n"));
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
let targetFile;
|
|
1485
|
+
if (checkpoint) {
|
|
1486
|
+
const match = checkpoints.find(
|
|
1487
|
+
(f) => f.includes(checkpoint) || f === `${checkpoint}.yaml`
|
|
1488
|
+
);
|
|
1489
|
+
if (!match) {
|
|
1490
|
+
console.log(chalk4.red(`
|
|
1491
|
+
Checkpoint "${checkpoint}" not found.`));
|
|
1492
|
+
console.log(chalk4.gray(" Available:"));
|
|
1493
|
+
for (const cp of checkpoints) {
|
|
1494
|
+
console.log(chalk4.gray(` ${cp}`));
|
|
1495
|
+
}
|
|
1496
|
+
console.log("");
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
targetFile = match;
|
|
1500
|
+
} else {
|
|
1501
|
+
targetFile = checkpoints[checkpoints.length - 1];
|
|
1502
|
+
}
|
|
1503
|
+
const targetPath = path6.join(checkpointDir, targetFile);
|
|
1504
|
+
const checkpointData = parseYaml3(fs6.readFileSync(targetPath, "utf-8"));
|
|
1505
|
+
console.log(chalk4.bold("\n Resetting to checkpoint:"));
|
|
1506
|
+
console.log(chalk4.gray(` File: ${targetFile}`));
|
|
1507
|
+
console.log(chalk4.gray(` Phase: ${checkpointData.phase || "unknown"}`));
|
|
1508
|
+
console.log(chalk4.gray(` Sprint: ${checkpointData.sprint || 0}`));
|
|
1509
|
+
console.log(chalk4.gray(` Timestamp: ${checkpointData.timestamp || "unknown"}`));
|
|
1510
|
+
console.log("");
|
|
1511
|
+
const projectPath = path6.join(targetDir, ".fishi", "state", "project.yaml");
|
|
1512
|
+
fs6.copyFileSync(targetPath, projectPath);
|
|
1513
|
+
console.log(chalk4.green(" \u2713 State restored from checkpoint."));
|
|
1514
|
+
console.log(chalk4.gray(" Run `claude` and the session will resume from this point.\n"));
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/commands/validate.ts
|
|
1518
|
+
import chalk5 from "chalk";
|
|
1519
|
+
import fs7 from "fs";
|
|
1520
|
+
import path7 from "path";
|
|
1521
|
+
import { execSync } from "child_process";
|
|
1522
|
+
async function validateCommand() {
|
|
1523
|
+
const targetDir = process.cwd();
|
|
1524
|
+
const fishiDir = path7.join(targetDir, ".fishi");
|
|
1525
|
+
if (!fs7.existsSync(fishiDir)) {
|
|
1526
|
+
console.log(chalk5.yellow("\n FISHI is not initialized in this directory."));
|
|
1527
|
+
console.log(chalk5.gray(" Run `fishi init` to get started.\n"));
|
|
1528
|
+
process.exitCode = 1;
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
const scriptPath = path7.join(fishiDir, "scripts", "validate-scaffold.mjs");
|
|
1532
|
+
if (!fs7.existsSync(scriptPath)) {
|
|
1533
|
+
console.log(chalk5.yellow("\n Validation script not found."));
|
|
1534
|
+
console.log(
|
|
1535
|
+
chalk5.gray(
|
|
1536
|
+
" Expected: .fishi/scripts/validate-scaffold.mjs\n Re-run `fishi init` to regenerate scaffold files.\n"
|
|
1537
|
+
)
|
|
1538
|
+
);
|
|
1539
|
+
process.exitCode = 1;
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
console.log("");
|
|
1543
|
+
console.log(chalk5.cyan.bold(" FISHI Scaffold Validation"));
|
|
1544
|
+
console.log("");
|
|
1545
|
+
try {
|
|
1546
|
+
const result = execSync(`node "${scriptPath}"`, {
|
|
1547
|
+
cwd: targetDir,
|
|
1548
|
+
encoding: "utf-8",
|
|
1549
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1550
|
+
env: { ...process.env, FISHI_PROJECT_ROOT: targetDir }
|
|
1551
|
+
});
|
|
1552
|
+
for (const line of result.split("\n")) {
|
|
1553
|
+
if (!line.trim()) {
|
|
1554
|
+
console.log(line);
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
if (line.includes("\u2713")) {
|
|
1558
|
+
console.log(chalk5.green(line));
|
|
1559
|
+
} else if (line.includes("\u2717")) {
|
|
1560
|
+
console.log(chalk5.red(line));
|
|
1561
|
+
} else if (line.includes("\u26A0")) {
|
|
1562
|
+
console.log(chalk5.yellow(line));
|
|
1563
|
+
} else if (line.includes("\u2705")) {
|
|
1564
|
+
console.log(chalk5.green.bold(line));
|
|
1565
|
+
} else {
|
|
1566
|
+
console.log(chalk5.gray(line));
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
console.log("");
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
const error = err;
|
|
1572
|
+
if (error.stdout) {
|
|
1573
|
+
for (const line of error.stdout.split("\n")) {
|
|
1574
|
+
if (!line.trim()) {
|
|
1575
|
+
console.log(line);
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
if (line.includes("\u2713")) {
|
|
1579
|
+
console.log(chalk5.green(line));
|
|
1580
|
+
} else if (line.includes("\u2717")) {
|
|
1581
|
+
console.log(chalk5.red(line));
|
|
1582
|
+
} else if (line.includes("\u26A0")) {
|
|
1583
|
+
console.log(chalk5.yellow(line));
|
|
1584
|
+
} else {
|
|
1585
|
+
console.log(chalk5.gray(line));
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (error.stderr) {
|
|
1590
|
+
console.error(chalk5.red(error.stderr));
|
|
1591
|
+
}
|
|
1592
|
+
console.log("");
|
|
1593
|
+
process.exitCode = 1;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// src/index.ts
|
|
1598
|
+
var program = new Command();
|
|
1599
|
+
program.name("fishi").description(
|
|
1600
|
+
chalk6.cyan("\u{1F41F} FISHI") + " \u2014 Your AI Dev Team That Actually Ships\n Autonomous agent framework for Claude Code"
|
|
1601
|
+
).version("0.1.0");
|
|
1602
|
+
program.command("init").description("Initialize FISHI in the current directory").argument("[description]", "Project description (skip wizard with zero-config)").option("-l, --language <lang>", "Primary language (e.g., typescript, python)").option("-f, --framework <framework>", "Framework (e.g., nextjs, express, django)").option(
|
|
1603
|
+
"-c, --cost-mode <mode>",
|
|
1604
|
+
"Cost mode: performance | balanced | economy",
|
|
1605
|
+
"balanced"
|
|
1606
|
+
).option("--no-interactive", "Skip interactive wizard even without description").action(initCommand);
|
|
1607
|
+
program.command("status").description("Show project status, active agents, and TaskBoard summary").action(statusCommand);
|
|
1608
|
+
program.command("mcp").description("Manage MCP server integrations").argument("<action>", "Action: add | list | remove").argument("[name]", "MCP server name").action(mcpCommand);
|
|
1609
|
+
program.command("reset").description("Rollback to a previous checkpoint").argument("[checkpoint]", "Checkpoint ID (defaults to latest)").action(resetCommand);
|
|
1610
|
+
program.command("validate").description("Validate scaffold integrity \u2014 checks files, frontmatter, cross-references, pipeline, and permissions").action(validateCommand);
|
|
1611
|
+
program.parse();
|