@gethmy/mcp 1.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -36
- package/dist/cli.js +20938 -20249
- package/dist/http.js +1957 -0
- package/dist/index.js +17833 -17888
- package/dist/lib/__tests__/active-learning.test.js +386 -0
- package/dist/lib/__tests__/agent-performance-profiles.test.js +325 -0
- package/dist/lib/__tests__/auto-session.test.js +661 -0
- package/dist/lib/__tests__/context-assembly.test.js +362 -0
- package/dist/lib/__tests__/graph-expansion.test.js +150 -0
- package/dist/lib/__tests__/integration-memory-crud.test.js +797 -0
- package/dist/lib/__tests__/integration-memory-system.test.js +281 -0
- package/dist/lib/__tests__/lifecycle-maintenance.test.js +207 -0
- package/dist/lib/__tests__/pattern-detection.test.js +295 -0
- package/dist/lib/__tests__/prompt-builder.test.js +418 -0
- package/dist/lib/active-learning.js +878 -0
- package/dist/lib/api-client.js +548 -0
- package/dist/lib/auto-session.js +173 -0
- package/dist/lib/cli.js +127 -0
- package/dist/lib/config.js +205 -0
- package/dist/lib/consolidation.js +243 -0
- package/dist/lib/context-assembly.js +606 -0
- package/dist/lib/graph-expansion.js +163 -0
- package/dist/lib/http.js +174 -0
- package/dist/lib/index.js +7 -0
- package/dist/lib/lifecycle-maintenance.js +88 -0
- package/dist/lib/prompt-builder.js +483 -0
- package/dist/lib/remote.js +166 -0
- package/dist/lib/server.js +3132 -0
- package/dist/lib/tui/agents.js +116 -0
- package/dist/lib/tui/docs.js +558 -0
- package/dist/lib/tui/setup.js +1068 -0
- package/dist/lib/tui/theme.js +95 -0
- package/dist/lib/tui/writer.js +200 -0
- package/dist/remote.js +34534 -0
- package/dist/server.js +31967 -0
- package/package.json +20 -7
- package/src/__tests__/active-learning.test.ts +483 -0
- package/src/__tests__/agent-performance-profiles.test.ts +468 -0
- package/src/__tests__/auto-session.test.ts +912 -0
- package/src/__tests__/context-assembly.test.ts +506 -0
- package/src/__tests__/graph-expansion.test.ts +285 -0
- package/src/__tests__/integration-memory-crud.test.ts +948 -0
- package/src/__tests__/integration-memory-system.test.ts +321 -0
- package/src/__tests__/lifecycle-maintenance.test.ts +238 -0
- package/src/__tests__/pattern-detection.test.ts +438 -0
- package/src/__tests__/prompt-builder.test.ts +505 -0
- package/src/active-learning.ts +1227 -0
- package/src/api-client.ts +963 -0
- package/src/auto-session.ts +218 -0
- package/src/cli.ts +166 -0
- package/src/config.ts +285 -0
- package/src/consolidation.ts +314 -0
- package/src/context-assembly.ts +842 -0
- package/src/graph-expansion.ts +234 -0
- package/src/http.ts +265 -0
- package/src/index.ts +8 -0
- package/src/lifecycle-maintenance.ts +120 -0
- package/src/prompt-builder.ts +681 -0
- package/src/remote.ts +227 -0
- package/src/server.ts +3858 -0
- package/src/tui/agents.ts +154 -0
- package/src/tui/docs.ts +650 -0
- package/src/tui/setup.ts +1281 -0
- package/src/tui/theme.ts +114 -0
- package/src/tui/writer.ts +260 -0
package/src/tui/docs.ts
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
readFileSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
statSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
import { colors, symbols } from "./theme.js";
|
|
10
|
+
|
|
11
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export interface ProjectInfo {
|
|
14
|
+
packageManager: "bun" | "pnpm" | "yarn" | "npm" | null;
|
|
15
|
+
scripts: Record<string, string>;
|
|
16
|
+
language: "typescript" | "javascript" | "go" | "rust" | "python" | "unknown";
|
|
17
|
+
framework: string | null;
|
|
18
|
+
linter: string | null;
|
|
19
|
+
indentStyle: { type: "space" | "tab"; width: number } | null;
|
|
20
|
+
dirs: string[];
|
|
21
|
+
srcDirs: string[];
|
|
22
|
+
monorepo: boolean;
|
|
23
|
+
existingDocs: {
|
|
24
|
+
agentsMd: boolean;
|
|
25
|
+
claudeMd: boolean;
|
|
26
|
+
docsDir: boolean;
|
|
27
|
+
architectureMd: boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DocsIssue {
|
|
32
|
+
severity: "error" | "warning";
|
|
33
|
+
file: string;
|
|
34
|
+
message: string;
|
|
35
|
+
fix?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DocsStepResult {
|
|
39
|
+
files: Array<{ path: string; content: string; type: "text" }>;
|
|
40
|
+
issues: DocsIssue[];
|
|
41
|
+
skipped: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
const IGNORED_DIRS = new Set([
|
|
47
|
+
"node_modules",
|
|
48
|
+
".git",
|
|
49
|
+
"dist",
|
|
50
|
+
"build",
|
|
51
|
+
".next",
|
|
52
|
+
".nuxt",
|
|
53
|
+
".output",
|
|
54
|
+
".vercel",
|
|
55
|
+
".turbo",
|
|
56
|
+
".cache",
|
|
57
|
+
"coverage",
|
|
58
|
+
".harmony-worktrees",
|
|
59
|
+
"__pycache__",
|
|
60
|
+
"target",
|
|
61
|
+
"vendor",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
/** Read a JSON file safely, returning null on failure. */
|
|
65
|
+
function readJson(filePath: string): Record<string, unknown> | null {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Read a text file safely, returning null on failure. */
|
|
74
|
+
function readText(filePath: string): string | null {
|
|
75
|
+
try {
|
|
76
|
+
return readFileSync(filePath, "utf-8");
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** List immediate subdirectories, excluding ignored names. */
|
|
83
|
+
function listDirs(dirPath: string): string[] {
|
|
84
|
+
try {
|
|
85
|
+
return readdirSync(dirPath).filter((entry) => {
|
|
86
|
+
if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
|
|
87
|
+
try {
|
|
88
|
+
return statSync(join(dirPath, entry)).isDirectory();
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Map of common directory names to short descriptions. */
|
|
99
|
+
const DIR_DESCRIPTIONS: Record<string, string> = {
|
|
100
|
+
components: "UI components",
|
|
101
|
+
pages: "Route-level pages",
|
|
102
|
+
routes: "Route-level pages",
|
|
103
|
+
views: "Route-level pages",
|
|
104
|
+
hooks: "Custom hooks",
|
|
105
|
+
lib: "Utilities",
|
|
106
|
+
utils: "Utilities",
|
|
107
|
+
api: "API / server code",
|
|
108
|
+
server: "API / server code",
|
|
109
|
+
contexts: "State management",
|
|
110
|
+
store: "State management",
|
|
111
|
+
stores: "State management",
|
|
112
|
+
types: "Type definitions",
|
|
113
|
+
styles: "Stylesheets",
|
|
114
|
+
public: "Static assets",
|
|
115
|
+
static: "Static assets",
|
|
116
|
+
assets: "Static assets",
|
|
117
|
+
supabase: "Supabase backend",
|
|
118
|
+
functions: "Edge functions",
|
|
119
|
+
packages: "Monorepo packages",
|
|
120
|
+
apps: "Monorepo applications",
|
|
121
|
+
src: "Source code",
|
|
122
|
+
test: "Tests",
|
|
123
|
+
tests: "Tests",
|
|
124
|
+
__tests__: "Tests",
|
|
125
|
+
scripts: "Build / utility scripts",
|
|
126
|
+
config: "Configuration",
|
|
127
|
+
docs: "Documentation",
|
|
128
|
+
migrations: "Database migrations",
|
|
129
|
+
prisma: "Prisma schema & migrations",
|
|
130
|
+
e2e: "End-to-end tests",
|
|
131
|
+
cypress: "Cypress tests",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
function describeDir(name: string): string {
|
|
135
|
+
return DIR_DESCRIPTIONS[name.toLowerCase()] ?? name;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Core functions ──────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Scan the project directory and return structured metadata.
|
|
142
|
+
* Pure static analysis — no AI, no network calls.
|
|
143
|
+
*/
|
|
144
|
+
export function scanProject(cwd: string): ProjectInfo {
|
|
145
|
+
// Package manager
|
|
146
|
+
let packageManager: ProjectInfo["packageManager"] = null;
|
|
147
|
+
if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb"))) {
|
|
148
|
+
packageManager = "bun";
|
|
149
|
+
} else if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
|
|
150
|
+
packageManager = "pnpm";
|
|
151
|
+
} else if (existsSync(join(cwd, "yarn.lock"))) {
|
|
152
|
+
packageManager = "yarn";
|
|
153
|
+
} else if (existsSync(join(cwd, "package.json"))) {
|
|
154
|
+
packageManager = "npm";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Scripts
|
|
158
|
+
const pkg = readJson(join(cwd, "package.json"));
|
|
159
|
+
const scripts: Record<string, string> =
|
|
160
|
+
pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
|
|
161
|
+
? (pkg.scripts as Record<string, string>)
|
|
162
|
+
: {};
|
|
163
|
+
|
|
164
|
+
// Language
|
|
165
|
+
let language: ProjectInfo["language"] = "unknown";
|
|
166
|
+
if (existsSync(join(cwd, "tsconfig.json"))) {
|
|
167
|
+
language = "typescript";
|
|
168
|
+
} else if (existsSync(join(cwd, "go.mod"))) {
|
|
169
|
+
language = "go";
|
|
170
|
+
} else if (existsSync(join(cwd, "Cargo.toml"))) {
|
|
171
|
+
language = "rust";
|
|
172
|
+
} else if (
|
|
173
|
+
existsSync(join(cwd, "setup.py")) ||
|
|
174
|
+
existsSync(join(cwd, "pyproject.toml"))
|
|
175
|
+
) {
|
|
176
|
+
language = "python";
|
|
177
|
+
} else if (pkg) {
|
|
178
|
+
language = "javascript";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Framework detection (from package.json dependencies)
|
|
182
|
+
let framework: string | null = null;
|
|
183
|
+
if (pkg) {
|
|
184
|
+
const deps: Record<string, string> = {
|
|
185
|
+
...(typeof pkg.dependencies === "object" ? (pkg.dependencies as Record<string, string>) : {}),
|
|
186
|
+
...(typeof pkg.devDependencies === "object" ? (pkg.devDependencies as Record<string, string>) : {}),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (deps.next) {
|
|
190
|
+
framework = "next";
|
|
191
|
+
} else if (deps.react && deps.vite) {
|
|
192
|
+
framework = "react+vite";
|
|
193
|
+
} else if (deps.react) {
|
|
194
|
+
framework = "react";
|
|
195
|
+
} else if (deps.vue && deps.vite) {
|
|
196
|
+
framework = "vue+vite";
|
|
197
|
+
} else if (deps.vue && deps.nuxt) {
|
|
198
|
+
framework = "nuxt";
|
|
199
|
+
} else if (deps.vue) {
|
|
200
|
+
framework = "vue";
|
|
201
|
+
} else if (deps.astro) {
|
|
202
|
+
framework = "astro";
|
|
203
|
+
} else if (deps.svelte) {
|
|
204
|
+
framework = "svelte";
|
|
205
|
+
} else if (deps.express) {
|
|
206
|
+
framework = "express";
|
|
207
|
+
} else if (deps.fastify) {
|
|
208
|
+
framework = "fastify";
|
|
209
|
+
} else if (deps.hono) {
|
|
210
|
+
framework = "hono";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Linter
|
|
215
|
+
let linter: string | null = null;
|
|
216
|
+
if (
|
|
217
|
+
existsSync(join(cwd, "biome.json")) ||
|
|
218
|
+
existsSync(join(cwd, "biome.jsonc"))
|
|
219
|
+
) {
|
|
220
|
+
linter = "biome";
|
|
221
|
+
} else {
|
|
222
|
+
const eslintFiles = [
|
|
223
|
+
".eslintrc",
|
|
224
|
+
".eslintrc.js",
|
|
225
|
+
".eslintrc.cjs",
|
|
226
|
+
".eslintrc.json",
|
|
227
|
+
".eslintrc.yml",
|
|
228
|
+
".eslintrc.yaml",
|
|
229
|
+
"eslint.config.js",
|
|
230
|
+
"eslint.config.mjs",
|
|
231
|
+
"eslint.config.cjs",
|
|
232
|
+
"eslint.config.ts",
|
|
233
|
+
];
|
|
234
|
+
if (eslintFiles.some((f) => existsSync(join(cwd, f)))) {
|
|
235
|
+
linter = "eslint";
|
|
236
|
+
} else {
|
|
237
|
+
const prettierFiles = [
|
|
238
|
+
".prettierrc",
|
|
239
|
+
".prettierrc.js",
|
|
240
|
+
".prettierrc.json",
|
|
241
|
+
".prettierrc.yml",
|
|
242
|
+
".prettierrc.yaml",
|
|
243
|
+
"prettier.config.js",
|
|
244
|
+
"prettier.config.mjs",
|
|
245
|
+
];
|
|
246
|
+
if (prettierFiles.some((f) => existsSync(join(cwd, f)))) {
|
|
247
|
+
linter = "prettier";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Indent style
|
|
253
|
+
let indentStyle: ProjectInfo["indentStyle"] = null;
|
|
254
|
+
const biome =
|
|
255
|
+
readJson(join(cwd, "biome.json")) ?? readJson(join(cwd, "biome.jsonc"));
|
|
256
|
+
if (biome) {
|
|
257
|
+
const formatter = biome.formatter as Record<string, unknown> | undefined;
|
|
258
|
+
if (formatter) {
|
|
259
|
+
const type = formatter.indentStyle === "tab" ? "tab" : "space";
|
|
260
|
+
const width =
|
|
261
|
+
typeof formatter.indentWidth === "number" ? formatter.indentWidth : 2;
|
|
262
|
+
indentStyle = { type, width };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!indentStyle) {
|
|
266
|
+
const editorConfig = readText(join(cwd, ".editorconfig"));
|
|
267
|
+
if (editorConfig) {
|
|
268
|
+
const styleMatch = editorConfig.match(/indent_style\s*=\s*(space|tab)/);
|
|
269
|
+
const sizeMatch = editorConfig.match(/indent_size\s*=\s*(\d+)/);
|
|
270
|
+
if (styleMatch) {
|
|
271
|
+
indentStyle = {
|
|
272
|
+
type: styleMatch[1] as "space" | "tab",
|
|
273
|
+
width: sizeMatch ? Number.parseInt(sizeMatch[1], 10) : 2,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Directories
|
|
280
|
+
const dirs = listDirs(cwd);
|
|
281
|
+
const srcDirs = existsSync(join(cwd, "src"))
|
|
282
|
+
? listDirs(join(cwd, "src"))
|
|
283
|
+
: [];
|
|
284
|
+
|
|
285
|
+
// Monorepo
|
|
286
|
+
const monorepo =
|
|
287
|
+
existsSync(join(cwd, "packages")) || existsSync(join(cwd, "apps"));
|
|
288
|
+
|
|
289
|
+
// Existing docs
|
|
290
|
+
const existingDocs = {
|
|
291
|
+
agentsMd: existsSync(join(cwd, "AGENTS.md")),
|
|
292
|
+
claudeMd: existsSync(join(cwd, "CLAUDE.md")),
|
|
293
|
+
docsDir: existsSync(join(cwd, "docs")),
|
|
294
|
+
architectureMd: existsSync(join(cwd, "docs", "architecture.md")),
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
packageManager,
|
|
299
|
+
scripts,
|
|
300
|
+
language,
|
|
301
|
+
framework,
|
|
302
|
+
linter,
|
|
303
|
+
indentStyle,
|
|
304
|
+
dirs,
|
|
305
|
+
srcDirs,
|
|
306
|
+
monorepo,
|
|
307
|
+
existingDocs,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Generators ──────────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
/** Build a human-friendly run command prefix. */
|
|
314
|
+
function runCmd(pm: ProjectInfo["packageManager"]): string {
|
|
315
|
+
if (pm === "bun") return "bun run";
|
|
316
|
+
if (pm === "pnpm") return "pnpm run";
|
|
317
|
+
if (pm === "yarn") return "yarn";
|
|
318
|
+
return "npm run";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Guess a short description for a package.json script based on its name. */
|
|
322
|
+
function describeScript(name: string): string {
|
|
323
|
+
const map: Record<string, string> = {
|
|
324
|
+
dev: "Dev server",
|
|
325
|
+
start: "Start server",
|
|
326
|
+
build: "Production build",
|
|
327
|
+
lint: "Lint",
|
|
328
|
+
"lint:fix": "Lint + autofix",
|
|
329
|
+
format: "Format code",
|
|
330
|
+
test: "Run tests",
|
|
331
|
+
"test:watch": "Run tests (watch)",
|
|
332
|
+
"test:e2e": "End-to-end tests",
|
|
333
|
+
typecheck: "Type-check",
|
|
334
|
+
"type-check": "Type-check",
|
|
335
|
+
preview: "Preview production build",
|
|
336
|
+
deploy: "Deploy",
|
|
337
|
+
generate: "Code generation",
|
|
338
|
+
migrate: "Run migrations",
|
|
339
|
+
seed: "Seed database",
|
|
340
|
+
clean: "Clean build artifacts",
|
|
341
|
+
prepare: "Prepare (husky, etc.)",
|
|
342
|
+
};
|
|
343
|
+
return map[name] ?? "";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Generate a scaffold AGENTS.md from project metadata.
|
|
348
|
+
*/
|
|
349
|
+
export function generateAgentsMd(info: ProjectInfo, _cwd: string): string {
|
|
350
|
+
const lang = info.language === "typescript" ? "TypeScript" : info.language === "javascript" ? "JavaScript" : info.language;
|
|
351
|
+
const frameworkLabel = info.framework ? `${info.framework} ` : "";
|
|
352
|
+
const monoLabel = info.monorepo ? " (monorepo)" : "";
|
|
353
|
+
|
|
354
|
+
const lines: string[] = [];
|
|
355
|
+
lines.push("# AGENTS.md");
|
|
356
|
+
lines.push("");
|
|
357
|
+
lines.push(`${frameworkLabel}${lang} project${monoLabel}.`);
|
|
358
|
+
lines.push("");
|
|
359
|
+
|
|
360
|
+
// Commands
|
|
361
|
+
const scriptEntries = Object.entries(info.scripts);
|
|
362
|
+
if (scriptEntries.length > 0 && info.packageManager) {
|
|
363
|
+
const prefix = runCmd(info.packageManager);
|
|
364
|
+
lines.push("## Commands");
|
|
365
|
+
lines.push("");
|
|
366
|
+
lines.push("```bash");
|
|
367
|
+
|
|
368
|
+
// Calculate padding for alignment
|
|
369
|
+
const commands = scriptEntries.map(([name]) => `${prefix} ${name}`);
|
|
370
|
+
const maxLen = Math.max(...commands.map((c) => c.length));
|
|
371
|
+
|
|
372
|
+
for (let i = 0; i < scriptEntries.length; i++) {
|
|
373
|
+
const [name] = scriptEntries[i];
|
|
374
|
+
const cmd = commands[i];
|
|
375
|
+
const desc = describeScript(name);
|
|
376
|
+
if (desc) {
|
|
377
|
+
lines.push(`${cmd}${" ".repeat(maxLen - cmd.length + 4)}# ${desc}`);
|
|
378
|
+
} else {
|
|
379
|
+
lines.push(cmd);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
lines.push("```");
|
|
384
|
+
lines.push("");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Code standards
|
|
388
|
+
lines.push("## Code Standards");
|
|
389
|
+
lines.push("");
|
|
390
|
+
|
|
391
|
+
const langLabel = info.language === "typescript" ? "TypeScript" : "JavaScript";
|
|
392
|
+
if (info.language === "typescript" || info.language === "javascript") {
|
|
393
|
+
lines.push(`- ${langLabel} with ES modules`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (info.indentStyle) {
|
|
397
|
+
const unit = info.indentStyle.type === "tab" ? "tab" : "space";
|
|
398
|
+
lines.push(`- ${info.indentStyle.width}-${unit} indentation`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (info.linter) {
|
|
402
|
+
lines.push(`- Linted with ${info.linter}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
lines.push("");
|
|
406
|
+
|
|
407
|
+
// Architecture
|
|
408
|
+
lines.push("## Architecture");
|
|
409
|
+
lines.push("");
|
|
410
|
+
|
|
411
|
+
for (const dir of info.dirs) {
|
|
412
|
+
lines.push(`- \`${dir}/\` — ${describeDir(dir)}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (info.srcDirs.length > 0) {
|
|
416
|
+
for (const sub of info.srcDirs) {
|
|
417
|
+
lines.push(` - \`src/${sub}/\` — ${describeDir(sub)}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
lines.push("");
|
|
422
|
+
return lines.join("\n");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Generate a lean CLAUDE.md pointer file.
|
|
427
|
+
*/
|
|
428
|
+
export function generateClaudeMd(info: ProjectInfo): string {
|
|
429
|
+
const lines: string[] = [];
|
|
430
|
+
lines.push("# CLAUDE.md");
|
|
431
|
+
lines.push("");
|
|
432
|
+
lines.push("@AGENTS.md");
|
|
433
|
+
|
|
434
|
+
if (info.existingDocs.architectureMd || info.dirs.includes("docs")) {
|
|
435
|
+
lines.push("@docs/architecture.md");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
lines.push("");
|
|
439
|
+
return lines.join("\n");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Generate an architecture.md scaffold.
|
|
444
|
+
*/
|
|
445
|
+
export function generateArchitectureMd(info: ProjectInfo, _cwd: string): string {
|
|
446
|
+
const lines: string[] = [];
|
|
447
|
+
lines.push("# Architecture");
|
|
448
|
+
lines.push("");
|
|
449
|
+
lines.push("## Directory Structure");
|
|
450
|
+
lines.push("");
|
|
451
|
+
|
|
452
|
+
for (const dir of info.dirs) {
|
|
453
|
+
lines.push(`- \`${dir}/\` — ${describeDir(dir)}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (info.srcDirs.length > 0) {
|
|
457
|
+
lines.push("");
|
|
458
|
+
lines.push("### `src/`");
|
|
459
|
+
lines.push("");
|
|
460
|
+
for (const sub of info.srcDirs) {
|
|
461
|
+
lines.push(`- \`src/${sub}/\` — ${describeDir(sub)}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
lines.push("");
|
|
466
|
+
return lines.join("\n");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ── Verification ────────────────────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Verify existing docs for broken references, stale commands, and dead paths.
|
|
473
|
+
*/
|
|
474
|
+
export function verifyDocs(cwd: string): DocsIssue[] {
|
|
475
|
+
const issues: DocsIssue[] = [];
|
|
476
|
+
|
|
477
|
+
// 1. CLAUDE.md — check @-references
|
|
478
|
+
const claudeMd = readText(join(cwd, "CLAUDE.md"));
|
|
479
|
+
if (claudeMd) {
|
|
480
|
+
for (const line of claudeMd.split("\n")) {
|
|
481
|
+
const match = line.match(/^@(.+)$/);
|
|
482
|
+
if (match) {
|
|
483
|
+
const refPath = match[1].trim();
|
|
484
|
+
if (!existsSync(join(cwd, refPath))) {
|
|
485
|
+
issues.push({
|
|
486
|
+
severity: "error",
|
|
487
|
+
file: "CLAUDE.md",
|
|
488
|
+
message: `Referenced file does not exist: ${refPath}`,
|
|
489
|
+
fix: `Remove the @${refPath} line or create the file`,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 2. AGENTS.md — check commands against package.json scripts
|
|
497
|
+
const agentsMd = readText(join(cwd, "AGENTS.md"));
|
|
498
|
+
const pkg = readJson(join(cwd, "package.json"));
|
|
499
|
+
const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
|
|
500
|
+
? (pkg.scripts as Record<string, string>)
|
|
501
|
+
: {};
|
|
502
|
+
|
|
503
|
+
if (agentsMd) {
|
|
504
|
+
// Extract commands from code blocks
|
|
505
|
+
const codeBlockRe = /```[\s\S]*?```/g;
|
|
506
|
+
let blockMatch: RegExpExecArray | null;
|
|
507
|
+
while ((blockMatch = codeBlockRe.exec(agentsMd)) !== null) {
|
|
508
|
+
const block = blockMatch[0];
|
|
509
|
+
// Match lines that look like package manager commands
|
|
510
|
+
const cmdRe = /(?:bun|npm|pnpm|yarn)\s+(?:run\s+)?(\S+)/g;
|
|
511
|
+
let cmdMatch: RegExpExecArray | null;
|
|
512
|
+
while ((cmdMatch = cmdRe.exec(block)) !== null) {
|
|
513
|
+
const scriptName = cmdMatch[1];
|
|
514
|
+
// Skip if the script name is a built-in (install, init, etc.)
|
|
515
|
+
const builtins = new Set(["install", "init", "create", "exec", "dlx", "x", "test", "start"]);
|
|
516
|
+
if (builtins.has(scriptName)) continue;
|
|
517
|
+
if (Object.keys(pkgScripts).length > 0 && !(scriptName in pkgScripts)) {
|
|
518
|
+
issues.push({
|
|
519
|
+
severity: "warning",
|
|
520
|
+
file: "AGENTS.md",
|
|
521
|
+
message: `Command references script "${scriptName}" which is not in package.json`,
|
|
522
|
+
fix: `Update the command or add "${scriptName}" to package.json scripts`,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 3. Check backtick-quoted paths in AGENTS.md
|
|
529
|
+
checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// 3b. Check backtick-quoted paths in docs/architecture.md
|
|
533
|
+
const archMd = readText(join(cwd, "docs", "architecture.md"));
|
|
534
|
+
if (archMd) {
|
|
535
|
+
checkBacktickPaths(archMd, "docs/architecture.md", cwd, issues);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return issues;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Scan markdown for backtick-quoted paths and check they exist. */
|
|
542
|
+
function checkBacktickPaths(
|
|
543
|
+
content: string,
|
|
544
|
+
file: string,
|
|
545
|
+
cwd: string,
|
|
546
|
+
issues: DocsIssue[],
|
|
547
|
+
): void {
|
|
548
|
+
const pathRe = /`((?:src\/|packages\/|apps\/|supabase\/|docs\/)[^`]+)`/g;
|
|
549
|
+
let match: RegExpExecArray | null;
|
|
550
|
+
const checked = new Set<string>();
|
|
551
|
+
|
|
552
|
+
while ((match = pathRe.exec(content)) !== null) {
|
|
553
|
+
const refPath = match[1].replace(/\/$/, ""); // strip trailing slash
|
|
554
|
+
if (checked.has(refPath)) continue;
|
|
555
|
+
checked.add(refPath);
|
|
556
|
+
|
|
557
|
+
if (!existsSync(join(cwd, refPath))) {
|
|
558
|
+
issues.push({
|
|
559
|
+
severity: "warning",
|
|
560
|
+
file,
|
|
561
|
+
message: `Referenced path does not exist: ${refPath}`,
|
|
562
|
+
fix: `Update or remove the \`${refPath}\` reference`,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ── TUI Entry Point ─────────────────────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Run the docs step of the setup wizard.
|
|
572
|
+
* Scans the project, then either scaffolds new docs or verifies existing ones.
|
|
573
|
+
*/
|
|
574
|
+
export async function runDocsStep(cwd: string): Promise<DocsStepResult> {
|
|
575
|
+
const info = scanProject(cwd);
|
|
576
|
+
const hasDocs = info.existingDocs.agentsMd || info.existingDocs.claudeMd;
|
|
577
|
+
|
|
578
|
+
if (!hasDocs) {
|
|
579
|
+
// No docs — offer to generate
|
|
580
|
+
const shouldGenerate = await p.confirm({
|
|
581
|
+
message: "No project docs found. Generate AGENTS.md and CLAUDE.md?",
|
|
582
|
+
initialValue: true,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
if (p.isCancel(shouldGenerate) || !shouldGenerate) {
|
|
586
|
+
return { files: [], issues: [], skipped: true };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const files: DocsStepResult["files"] = [];
|
|
590
|
+
|
|
591
|
+
files.push({
|
|
592
|
+
path: join(cwd, "AGENTS.md"),
|
|
593
|
+
content: generateAgentsMd(info, cwd),
|
|
594
|
+
type: "text",
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
files.push({
|
|
598
|
+
path: join(cwd, "CLAUDE.md"),
|
|
599
|
+
content: generateClaudeMd(info),
|
|
600
|
+
type: "text",
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Generate architecture.md if docs/ exists or we referenced it
|
|
604
|
+
if (info.dirs.includes("docs") || info.srcDirs.length > 0) {
|
|
605
|
+
files.push({
|
|
606
|
+
path: join(cwd, "docs", "architecture.md"),
|
|
607
|
+
content: generateArchitectureMd(info, cwd),
|
|
608
|
+
type: "text",
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
p.log.success(
|
|
613
|
+
`Generated ${files.length} doc file(s): ${files.map((f) => f.path.replace(cwd + "/", "")).join(", ")}`,
|
|
614
|
+
);
|
|
615
|
+
return { files, issues: [], skipped: false };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Docs exist — offer to verify
|
|
619
|
+
const shouldVerify = await p.confirm({
|
|
620
|
+
message: "Project docs found. Verify for issues?",
|
|
621
|
+
initialValue: false,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
if (p.isCancel(shouldVerify) || !shouldVerify) {
|
|
625
|
+
return { files: [], issues: [], skipped: true };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const issues = verifyDocs(cwd);
|
|
629
|
+
|
|
630
|
+
if (issues.length === 0) {
|
|
631
|
+
p.log.success("No issues found in project docs.");
|
|
632
|
+
} else {
|
|
633
|
+
for (const issue of issues) {
|
|
634
|
+
const prefix = `${colors.bold(issue.file)}:`;
|
|
635
|
+
if (issue.severity === "error") {
|
|
636
|
+
p.log.error(`${prefix} ${issue.message}`);
|
|
637
|
+
} else {
|
|
638
|
+
p.log.warning(`${prefix} ${issue.message}`);
|
|
639
|
+
}
|
|
640
|
+
if (issue.fix) {
|
|
641
|
+
p.log.message(` ${symbols.arrow} ${colors.dim(issue.fix)}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
p.log.info(
|
|
645
|
+
`Found ${issues.length} issue(s) (${issues.filter((i) => i.severity === "error").length} errors, ${issues.filter((i) => i.severity === "warning").length} warnings)`,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return { files: [], issues, skipped: false };
|
|
650
|
+
}
|