add-coder 0.1.7 → 0.1.9

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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # add-coder
2
2
 
3
- **AI 代码治理的落地方案** — [codein2027](https://github.com/xiaomingming92/codein2027) 快速构建 ADD 编程范式的完整脚手架。以「审计即基础设施」为核心,彻底打破编程过程黑盒与跨轮失忆,让编程范式进化为可审计、可追溯、可收敛的新时代。
3
+ **AI 代码治理的落地方案** — [codein2027](https://github.com/xiaomingming92/codein2027) 快速构建 ADD 编程范式的完整脚手架。以「审计即基础设施」为核心,彻底打破编程过程黑盒与跨轮失忆,让编程范式进化为可审计、可追溯、可收敛的新时代。[本项目NPM包地址](https://www.npmjs.com/package/add-coder)[本项目GITHUB地址](https://github.com/xiaomingming92/add-coder)
4
+
5
+ > 🧭 **从零上手实操?** 请参见 [GUIDE.md](./GUIDE.md) — 包含触发词速查、需求转 Plan、完整链路演练。
4
6
 
5
7
  ```bash
6
8
  npx add-coder init
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { readFileSync as readFileSync7 } from "fs";
4
+ import { readFileSync as readFileSync8 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/caijuehub/strategies/detect.strategy.ts
@@ -33,9 +33,7 @@ function detectIDE(projectRoot) {
33
33
  }
34
34
 
35
35
  // src/caijuehub/strategies/adapter.strategy.ts
36
- var AUTO_DEPLOY_ADAPTERS = ["claude", "qoder", "vscode"];
37
36
  function resolveAdapters(target) {
38
- if (target === "auto") return AUTO_DEPLOY_ADAPTERS;
39
37
  return [target];
40
38
  }
41
39
 
@@ -53,7 +51,7 @@ import { join as join2 } from "path";
53
51
 
54
52
  // src/config/schema.ts
55
53
  import { z } from "zod";
56
- var AdapterEnum = z.enum(["claude", "qoder", "vscode", "auto"]);
54
+ var AdapterEnum = z.enum(["claude", "qoder", "vscode"]);
57
55
  var AddCoderConfigSchema = z.object({
58
56
  projectName: z.string().min(1, "\u9879\u76EE\u540D\u4E0D\u80FD\u4E3A\u7A7A"),
59
57
  projectRoot: z.string().default(""),
@@ -64,7 +62,8 @@ var AddCoderConfigSchema = z.object({
64
62
  auditLoggerPath: z.string().default("src/lib/agent-audit-logger.ts"),
65
63
  mcpServerCommand: z.string().default("tsx"),
66
64
  agentAuditImport: z.string().default("@/lib/agent-audit-logger"),
67
- adapters: z.array(AdapterEnum).default(["auto"]),
65
+ magicDir: z.string(),
66
+ adapters: z.array(AdapterEnum).default([]),
68
67
  overrides: z.record(z.string(), z.string()).default({})
69
68
  });
70
69
 
@@ -79,7 +78,8 @@ var defaults = {
79
78
  auditLoggerPath: "src/lib/agent-audit-logger.ts",
80
79
  mcpServerCommand: "tsx",
81
80
  agentAuditImport: "@/lib/agent-audit-logger",
82
- adapters: ["auto"],
81
+ magicDir: "",
82
+ adapters: [],
83
83
  overrides: {}
84
84
  };
85
85
 
@@ -113,11 +113,13 @@ ${errors}`);
113
113
  }
114
114
 
115
115
  // src/caijuehub/strategies/writer.strategy.ts
116
- import { existsSync as existsSync3, mkdirSync, writeFileSync, chmodSync } from "fs";
116
+ import { existsSync as existsSync4, mkdirSync, writeFileSync, chmodSync } from "fs";
117
117
  import { join as join3, dirname } from "path";
118
118
 
119
119
  // src/lib/utils.ts
120
120
  import { createInterface } from "readline";
121
+ import { existsSync as existsSync3 } from "fs";
122
+ import { resolve } from "path";
121
123
  function ask(q) {
122
124
  const rl = createInterface({ input: process.stdin, output: process.stdout });
123
125
  return new Promise((r) => {
@@ -127,6 +129,9 @@ function ask(q) {
127
129
  });
128
130
  });
129
131
  }
132
+ function detectPm(projectRoot) {
133
+ return existsSync3(resolve(projectRoot, "pnpm-lock.yaml")) ? "pnpm" : "npm";
134
+ }
130
135
 
131
136
  // src/caijuehub/strategies/writer.strategy.ts
132
137
  var WRITER_CONFIG = {
@@ -137,21 +142,27 @@ var WRITER_CONFIG = {
137
142
  async function writeFiles(projectRoot, files, options = {}) {
138
143
  const C = WRITER_CONFIG;
139
144
  let created = 0, skipped = 0, overwritten = 0;
145
+ let skipAll = false;
140
146
  for (const [relPath, content] of files) {
141
147
  const dest = join3(projectRoot, relPath);
142
148
  if (options.dryRun) {
143
- console.log(`[dry-run] ${existsSync3(dest) ? "\u8986\u76D6" : "\u65B0\u5EFA"}: ${relPath}`);
149
+ console.log(`[dry-run] ${existsSync4(dest) ? "\u8986\u76D6" : "\u65B0\u5EFA"}: ${relPath}`);
144
150
  created++;
145
151
  continue;
146
152
  }
147
- if (existsSync3(dest)) {
153
+ if (existsSync4(dest)) {
148
154
  if (options.force) {
149
155
  overwritten++;
150
- } else if (options.yes || C.onExisting === "skip") {
156
+ } else if (options.yes || skipAll || C.onExisting === "skip") {
151
157
  skipped++;
152
158
  continue;
153
159
  } else {
154
- const choice = await ask(`\u6587\u4EF6\u5DF2\u5B58\u5728 ${relPath}\uFF1A[s]\u8DF3\u8FC7 / [o]\u8986\u76D6\uFF08\u9ED8\u8BA4 s\uFF09: `);
160
+ const choice = await ask(`\u6587\u4EF6\u5DF2\u5B58\u5728 ${relPath}\uFF1A[s]\u8DF3\u8FC7 / [o]\u8986\u76D6 / [a]\u5168\u90E8\u8DF3\u8FC7\uFF08\u9ED8\u8BA4 s\uFF09: `);
161
+ if (choice === "a") {
162
+ skipAll = true;
163
+ skipped++;
164
+ continue;
165
+ }
155
166
  if (choice !== "o" && choice !== "overwrite") {
156
167
  skipped++;
157
168
  continue;
@@ -191,6 +202,7 @@ var PLACEHOLDERS = {
191
202
  "{{docsDir}}": "docsDir",
192
203
  "{{logDir}}": "logDir",
193
204
  "{{envFilePath}}": "envFilePath",
205
+ "{{magicDir}}": "magicDir",
194
206
  "{{auditLoggerPath}}": "auditLoggerPath",
195
207
  "{{mcpServerCommand}}": "mcpServerCommand",
196
208
  "{{agentAuditImport}}": "agentAuditImport"
@@ -198,20 +210,21 @@ var PLACEHOLDERS = {
198
210
  function render(content, config) {
199
211
  let result = content;
200
212
  for (const [placeholder, key] of Object.entries(PLACEHOLDERS)) {
201
- result = result.replaceAll(placeholder, config[key]);
213
+ result = result.replaceAll(placeholder, String(config[key]));
202
214
  }
203
215
  return result;
204
216
  }
205
217
  var TEMPLATES_ROOT = join4(__dirname, "../templates");
206
218
  var CORE_DIR = join4(TEMPLATES_ROOT, "core");
207
219
  var CORE_TARGET = ".add";
220
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["prisma"]);
208
221
  function renderCore(config, dryRun) {
209
222
  const files = /* @__PURE__ */ new Map();
210
223
  function walk(dir, base) {
211
224
  for (const name of readdirSync(dir)) {
212
225
  const full = join4(dir, name);
213
226
  if (statSync(full).isDirectory()) {
214
- walk(full, join4(base, name));
227
+ if (!SKIP_DIRS.has(name)) walk(full, join4(base, name));
215
228
  } else {
216
229
  const content = readFileSync2(full, "utf-8");
217
230
  const rendered = render(content, config);
@@ -321,13 +334,13 @@ function renderAdapter3(config, targetDir, dryRun) {
321
334
  }
322
335
 
323
336
  // src/cli/prisma-injector.ts
324
- import { resolve as resolve2, dirname as dirname6 } from "path";
337
+ import { resolve as resolve3, dirname as dirname6 } from "path";
325
338
  import { fileURLToPath as fileURLToPath5 } from "url";
326
339
 
327
340
  // src/caijuehub/strategies/prisma.strategy.ts
328
341
  import { spawnSync } from "child_process";
329
- import { copyFileSync, existsSync as existsSync4, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
330
- import { resolve } from "path";
342
+ import { copyFileSync, existsSync as existsSync5, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
343
+ import { resolve as resolve2 } from "path";
331
344
  var PRISMA_CONFIG = {
332
345
  onMissing: "ask",
333
346
  onExistingAddPrisma: "ask",
@@ -335,70 +348,118 @@ var PRISMA_CONFIG = {
335
348
  autoGenerate: true,
336
349
  migrationName: "add_workflow_init",
337
350
  schemaArg: "--schema=prisma/",
338
- requiresUserModel: true
351
+ requiresUserModel: false
339
352
  };
353
+ function ensurePrismaConfig(projectRoot) {
354
+ const configPath = resolve2(projectRoot, "prisma.config.ts");
355
+ writeFileSync2(configPath, [
356
+ 'import dotenv from "dotenv";',
357
+ 'import { existsSync } from "fs";',
358
+ 'for (const f of [".env.development.local", ".env.development", ".env.local", ".env"]) {',
359
+ " if (existsSync(f)) { dotenv.config({ path: f }); break; }",
360
+ "}",
361
+ 'import { defineConfig, env } from "prisma/config";',
362
+ "export default defineConfig({",
363
+ ' schema: "prisma",',
364
+ " datasource: {",
365
+ ' url: env("DATABASE_URL"),',
366
+ " },",
367
+ "});"
368
+ ].join("\n") + "\n", "utf-8");
369
+ }
370
+ function backupAddTables(projectRoot) {
371
+ const pgDump = spawnSync("which", ["pg_dump"], { timeout: 2e3 });
372
+ if (pgDump.status !== 0) return null;
373
+ const bak = resolve2(projectRoot, `add-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}.sql`);
374
+ const r = spawnSync("pg_dump", ["--table=AddUser", "--table=DevOperation", "--table=AuditLog", "--if-exists"], {
375
+ cwd: projectRoot,
376
+ stdio: ["ignore", "pipe", "pipe"],
377
+ timeout: 3e4
378
+ });
379
+ if (r.stdout.length > 0) {
380
+ writeFileSync2(bak, r.stdout, "utf-8");
381
+ console.log(`>>> \u5907\u4EFD ADD \u8868\u5230 ${bak}`);
382
+ return bak;
383
+ }
384
+ return null;
385
+ }
386
+ function runPrismaInit(projectRoot, provider, schemaPath) {
387
+ console.log("\u6267\u884C npx prisma init ...");
388
+ const pm = detectPm(projectRoot);
389
+ const initArgs = pm === "pnpm" ? ["dlx", "prisma", "init", "--datasource-provider", provider] : ["prisma", "init", "--datasource-provider", provider];
390
+ const initResult = spawnSync(pm, initArgs, {
391
+ cwd: projectRoot,
392
+ stdio: "inherit",
393
+ shell: false
394
+ });
395
+ if (initResult.status !== 0 || !existsSync5(schemaPath)) {
396
+ console.log("prisma init \u5931\u8D25\uFF0C\u624B\u52A8\u521B\u5EFA schema.prisma ...");
397
+ const prismaDir = resolve2(projectRoot, "prisma");
398
+ if (!existsSync5(prismaDir)) mkdirSync2(prismaDir, { recursive: true });
399
+ const content = `generator client {
400
+ provider = "prisma-client-js"
401
+ }
402
+
403
+ datasource db {
404
+ provider = "${provider}"
405
+ }
406
+ `;
407
+ writeFileSync2(schemaPath, content, "utf-8");
408
+ const devEnvPath = resolve2(projectRoot, ".env.development");
409
+ if (!existsSync5(devEnvPath)) {
410
+ const defaultUrl = provider === "sqlite" ? 'DATABASE_URL="file:./data/dev.db"' : '# \u8BF7\u7F16\u8F91\u4E3A\u4F60\u7684\u6570\u636E\u5E93\u8FDE\u63A5\u4FE1\u606F\nDATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DB?schema=public"';
411
+ writeFileSync2(devEnvPath, defaultUrl + "\n", "utf-8");
412
+ console.log("\u5DF2\u521B\u5EFA .env.development");
413
+ }
414
+ return false;
415
+ }
416
+ return true;
417
+ }
418
+ function postInitSetup(projectRoot, schemaPath, addPrismaTemplate, destPath) {
419
+ const envPath = resolve2(projectRoot, ".env");
420
+ const devEnvPath = resolve2(projectRoot, ".env.development");
421
+ if (existsSync5(envPath)) {
422
+ const envContent = readFileSync6(envPath, "utf-8");
423
+ const dbUrl = envContent.match(/DATABASE_URL=.*/);
424
+ if (dbUrl) {
425
+ const existing = existsSync5(devEnvPath) ? readFileSync6(devEnvPath, "utf-8") : "";
426
+ if (!existing.includes("DATABASE_URL=")) {
427
+ writeFileSync2(devEnvPath, `${existing}${existing ? "\n" : ""}${dbUrl[0]}
428
+ `, "utf-8");
429
+ }
430
+ if (existsSync5(envPath)) unlinkSync(envPath);
431
+ console.log("\u5DF2\u5C06 DATABASE_URL \u8FC1\u79FB\u5230 .env.development");
432
+ }
433
+ }
434
+ copyFileSync(addPrismaTemplate, destPath);
435
+ console.log("\u5DF2\u590D\u5236 add.prisma");
436
+ }
340
437
  async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
341
438
  const C = PRISMA_CONFIG;
342
- const prismaDir = resolve(projectRoot, "prisma");
343
- const schemaPath = resolve(prismaDir, "schema.prisma");
344
- const destPath = resolve(prismaDir, "add.prisma");
345
- if (!existsSync4(prismaDir) || !existsSync4(schemaPath)) {
439
+ const prismaDir = resolve2(projectRoot, "prisma");
440
+ const schemaPath = resolve2(prismaDir, "schema.prisma");
441
+ const destPath = resolve2(prismaDir, "add.prisma");
442
+ let justInited = false;
443
+ if (!existsSync5(prismaDir) || !existsSync5(schemaPath)) {
346
444
  if (C.onMissing === "skip") {
347
445
  console.log("\u8DF3\u8FC7\uFF1A\u7F3A\u5C11 Prisma");
348
446
  return true;
349
447
  }
350
- if (C.onMissing === "ask" && !options.force && !options.yes) {
448
+ const shouldInit = options.force || options.yes;
449
+ if (!shouldInit && C.onMissing === "ask") {
351
450
  const a = await ask("\u9879\u76EE\u7F3A\u5C11 Prisma\uFF0C\u662F\u5426\u6267\u884C prisma init\uFF1F[Y/n] ");
352
- if (a !== "n" && a !== "no") {
353
- console.log("\u6267\u884C npx prisma init ...");
354
- spawnSync("npx", ["prisma", "init", "--datasource-provider", "postgresql"], { cwd: projectRoot, stdio: "inherit", shell: false });
355
- const envPath = resolve(projectRoot, ".env");
356
- const devEnvPath = resolve(projectRoot, ".env.development");
357
- if (existsSync4(envPath)) {
358
- const envContent = readFileSync6(envPath, "utf-8");
359
- const dbUrl = envContent.match(/DATABASE_URL=.*/);
360
- if (dbUrl) {
361
- const existing = existsSync4(devEnvPath) ? readFileSync6(devEnvPath, "utf-8") : "";
362
- if (!existing.includes("DATABASE_URL=")) {
363
- writeFileSync2(devEnvPath, `${existing}${existing ? "\n" : ""}${dbUrl[0]}
364
- `, "utf-8");
365
- }
366
- if (existsSync4(envPath)) unlinkSync(envPath);
367
- console.log("\u5DF2\u5C06 DATABASE_URL \u8FC1\u79FB\u5230 .env.development");
368
- }
369
- }
370
- console.log("\u8BF7\u7F16\u8F91 .env.development \u914D\u7F6E DATABASE_URL\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C add-coder init \u5B8C\u6210\u8FC1\u79FB");
371
- console.log(" \u53EF\u9009\u6587\u4EF6\u4F18\u5148\u7EA7: .env.development.local > .env.development > .env.local > .env");
372
- const schemaContent = readFileSync6(schemaPath, "utf-8");
373
- if (!/model\s+User\s*\{/.test(schemaContent)) {
374
- writeFileSync2(schemaPath, "model User {\n id String @id @default(cuid())\n}\n\n" + schemaContent, "utf-8");
375
- console.log("\u5DF2\u6CE8\u5165 User \u6A21\u578B");
376
- }
377
- copyFileSync(addPrismaTemplate, destPath);
378
- console.log("\u5DF2\u590D\u5236 add.prisma");
379
- return true;
451
+ if (a === "n" || a === "no") {
452
+ throw new Error("\u9879\u76EE\u7F3A\u5C11 Prisma \u914D\u7F6E\u3002ADD \u5DE5\u4F5C\u6D41\u4F9D\u8D56 Prisma + PostgreSQL\u3002");
380
453
  }
454
+ } else if (!shouldInit) {
455
+ throw new Error("\u9879\u76EE\u7F3A\u5C11 Prisma \u914D\u7F6E\u3002ADD \u5DE5\u4F5C\u6D41\u4F9D\u8D56 Prisma + PostgreSQL\u3002");
381
456
  }
382
- throw new Error("\u9879\u76EE\u7F3A\u5C11 Prisma \u914D\u7F6E\u3002ADD \u5DE5\u4F5C\u6D41\u4F9D\u8D56 Prisma + PostgreSQL\u3002");
457
+ const provider = options.datasource || "postgresql";
458
+ runPrismaInit(projectRoot, provider, schemaPath);
459
+ postInitSetup(projectRoot, schemaPath, addPrismaTemplate, destPath);
460
+ justInited = true;
383
461
  }
384
- if (C.requiresUserModel && !/model\s+User\s*\{/.test(readFileSync6(schemaPath, "utf-8"))) {
385
- const userModel = "model User {\n id String @id @default(cuid())\n}\n";
386
- if (options.yes || options.force) {
387
- const schema = readFileSync6(schemaPath, "utf-8");
388
- writeFileSync2(schemaPath, userModel + "\n" + schema, "utf-8");
389
- console.log("\u5DF2\u6CE8\u5165 User \u6A21\u578B\u5230 schema.prisma");
390
- } else {
391
- const a = await ask("schema.prisma \u7F3A\u5C11 User \u6A21\u578B\uFF0C\u662F\u5426\u81EA\u52A8\u6CE8\u5165\uFF1F[Y/n] ");
392
- if (a !== "n" && a !== "no") {
393
- const schema = readFileSync6(schemaPath, "utf-8");
394
- writeFileSync2(schemaPath, userModel + "\n" + schema, "utf-8");
395
- console.log("\u5DF2\u6CE8\u5165 User \u6A21\u578B");
396
- } else {
397
- throw new Error("\u9700\u8981 User \u6A21\u578B\uFF08id: String\uFF09\uFF0C\u8BF7\u5728 prisma/schema.prisma \u4E2D\u521B\u5EFA\u540E\u91CD\u8BD5\u3002");
398
- }
399
- }
400
- }
401
- if (existsSync4(destPath)) {
462
+ if (existsSync5(destPath) && !justInited) {
402
463
  if (options.dryRun) {
403
464
  console.log("[dry-run] \u5DF2\u6709 add.prisma");
404
465
  return true;
@@ -428,17 +489,19 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
428
489
  }
429
490
  }
430
491
  if (options.dryRun) {
431
- console.log("[dry-run] \u5C06\u6267\u884C prisma migrate");
492
+ console.log("[dry-run] \u5C06\u6267\u884C prisma db push");
432
493
  return true;
433
494
  }
434
- copyFileSync(addPrismaTemplate, destPath);
435
- console.log("\u5DF2\u590D\u5236 add.prisma");
495
+ if (!justInited) copyFileSync(addPrismaTemplate, destPath);
436
496
  try {
437
- const args = ["prisma", "migrate", "dev", "--name", C.migrationName];
497
+ ensurePrismaConfig(projectRoot);
498
+ backupAddTables(projectRoot);
499
+ const pm = detectPm(projectRoot);
500
+ const args = pm === "pnpm" ? ["dlx", "prisma", "db", "push"] : ["prisma", "db", "push"];
438
501
  if (C.schemaArg) args.push(C.schemaArg);
439
- console.log(`\u6267\u884C npx ${args.join(" ")} ...`);
440
- const r = spawnSync("npx", args, { cwd: projectRoot, stdio: "inherit", shell: false });
441
- if (r.status !== 0) throw new Error(`prisma migrate dev \u9000\u51FA\u7801: ${r.status}`);
502
+ console.log(`\u6267\u884C ${pm} ${args.join(" ")} ...`);
503
+ const r = spawnSync(pm, args, { cwd: projectRoot, stdio: "inherit", shell: false });
504
+ if (r.status !== 0) throw new Error(`prisma db push \u9000\u51FA\u7801: ${r.status}`);
442
505
  } catch (err) {
443
506
  if (C.onMigrateFail === "keep") {
444
507
  console.log("\u8FC1\u79FB\u5931\u8D25\uFF0C\u4FDD\u7559\u6587\u4EF6");
@@ -452,8 +515,9 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
452
515
  throw new Error(`\u8FC1\u79FB\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
453
516
  }
454
517
  if (C.autoGenerate) {
518
+ const pm = detectPm(projectRoot);
455
519
  console.log("\u6267\u884C prisma generate ...");
456
- spawnSync("npx", ["prisma", "generate"], { cwd: projectRoot, stdio: "inherit", shell: false });
520
+ spawnSync(pm, pm === "pnpm" ? ["dlx", "prisma", "generate"] : ["prisma", "generate"], { cwd: projectRoot, stdio: "inherit", shell: false });
457
521
  }
458
522
  console.log("ADD \u6CBB\u7406\u6A21\u578B\u5DF2\u5C31\u7EEA");
459
523
  return true;
@@ -462,65 +526,314 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
462
526
  // src/cli/prisma-injector.ts
463
527
  var __filename5 = fileURLToPath5(import.meta.url);
464
528
  var __dirname5 = dirname6(__filename5);
465
- var ADD_PRISMA_TEMPLATE = resolve2(__dirname5, "../templates/core/prisma/add.prisma");
529
+ var ADD_PRISMA_TEMPLATE = resolve3(__dirname5, "../templates/core/prisma/add.prisma");
466
530
  async function injectPrisma2(projectRoot, options = {}) {
467
531
  return injectPrisma(projectRoot, ADD_PRISMA_TEMPLATE, options);
468
532
  }
469
533
 
470
534
  // src/cli/commands/init.ts
535
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
536
+ import { resolve as resolve4 } from "path";
537
+ import { spawnSync as spawnSync2 } from "child_process";
538
+ import { createConnection } from "net";
471
539
  var ADAPTER_RENDERERS = {
472
540
  claude: renderAdapter,
473
541
  qoder: renderAdapter2,
474
542
  vscode: renderAdapter3
475
543
  };
544
+ var MAGIC_DIR_MAP = { claude: ".claude", qoder: ".qoder", vscode: ".vscode" };
545
+ async function resolveAdapter(projectRoot, specified) {
546
+ if (specified) {
547
+ if (!MAGIC_DIR_MAP[specified]) throw new Error(`\u672A\u77E5 adapter: ${specified}`);
548
+ console.log(`\u76EE\u6807 IDE: ${specified} (--adapter)`);
549
+ return specified;
550
+ }
551
+ const detected = detectIDE2(projectRoot);
552
+ if (detected !== "auto") {
553
+ console.log(`\u68C0\u6D4B\u5230 IDE: ${detected} (\u81EA\u52A8)`);
554
+ return detected;
555
+ }
556
+ console.log("\u672A\u68C0\u6D4B\u5230 IDE \u73AF\u5883");
557
+ const a = (await ask("\u8BF7\u9009\u62E9\u76EE\u6807 IDE: [1] Qoder [2] Claude [3] VS Code \u2192 ")).trim();
558
+ if (a === "1" || a === "qoder") return "qoder";
559
+ if (a === "2" || a === "claude") return "claude";
560
+ if (a === "3" || a === "vscode") return "vscode";
561
+ console.log("\u8F93\u5165\u65E0\u6CD5\u8BC6\u522B\uFF0C\u9ED8\u8BA4 qoder");
562
+ return "qoder";
563
+ }
564
+ async function resolveDbEngine(force) {
565
+ if (force) {
566
+ console.log("\u6570\u636E\u5E93\u5F15\u64CE: PostgreSQL (--force \u9ED8\u8BA4)");
567
+ return { engine: "postgresql", container: "podman" };
568
+ }
569
+ console.log(["", "\u6570\u636E\u5E93\u5F15\u64CE:", " [1] PostgreSQL (\u63A8\u8350)", " [2] SQLite \u2014 \u96F6\u4F9D\u8D56", " [3] \u81EA\u884C\u7BA1\u7406"].join("\n"));
570
+ const a = (await ask("\u8BF7\u9009\u62E9 [1/2/3] \u2192 ")).trim();
571
+ if (a === "2" || a === "sqlite") return { engine: "sqlite" };
572
+ if (a === "3" || a === "manual") return { engine: "manual" };
573
+ if (a !== "" && a !== "1" && !a.startsWith("postgres")) console.log("\u8F93\u5165\u65E0\u6CD5\u8BC6\u522B\uFF0C\u9ED8\u8BA4 PostgreSQL");
574
+ return { engine: "postgresql" };
575
+ }
576
+ async function resolveContainer(force) {
577
+ if (force) return "podman";
578
+ console.log(["", "\u5BB9\u5668\u8FD0\u884C\u65F6:", " [1] podman (\u63A8\u8350)", " [2] docker", " [3] \u81EA\u884C\u7BA1\u7406"].join("\n"));
579
+ const a = (await ask("\u8BF7\u9009\u62E9 [1/2/3] \u2192 ")).trim();
580
+ if (a === "2" || a === "docker") return "docker";
581
+ if (a === "3" || a === "manual") return "manual";
582
+ if (a !== "" && a !== "1" && a !== "podman") console.log("\u8F93\u5165\u65E0\u6CD5\u8BC6\u522B\uFF0C\u9ED8\u8BA4 podman");
583
+ return "podman";
584
+ }
585
+ function portInUse(port) {
586
+ return new Promise((r) => {
587
+ const s = createConnection({ port, host: "127.0.0.1" }, () => {
588
+ s.destroy();
589
+ r(true);
590
+ });
591
+ s.on("error", () => r(false));
592
+ });
593
+ }
594
+ function hasPgIsready() {
595
+ try {
596
+ const containers = spawnSync2("podman", ["ps", "--filter", "publish=5433", "--format", "{{.Names}}"], { timeout: 3e3 });
597
+ const name = containers.stdout.toString().trim().split("\n")[0];
598
+ if (name && spawnSync2("podman", ["exec", name, "pg_isready", "--version"], { timeout: 2e3 }).status === 0) return true;
599
+ } catch {
600
+ }
601
+ return spawnSync2("which", ["pg_isready"], { timeout: 2e3 }).status === 0;
602
+ }
603
+ function testPostgresConnection(port, user, password, dbName) {
604
+ if (!hasPgIsready()) {
605
+ console.log(" \u26A0\uFE0F \u65E0\u6CD5\u9A8C\u8BC1\u51ED\u636E\uFF08\u5BB9\u5668\u672A\u8FD0\u884C\u4E14 pg_isready \u672A\u5B89\u88C5\uFF09\uFF0C\u4FE1\u4EFB\u8F93\u5165");
606
+ return true;
607
+ }
608
+ const containers = spawnSync2("podman", ["ps", "--filter", "publish=5433", "--format", "{{.Names}}"], { timeout: 3e3 });
609
+ const containerName = containers.stdout.toString().trim().split("\n")[0];
610
+ const args = containerName ? ["exec", containerName, "pg_isready", "-U", user, "-d", dbName] : ["-h", "localhost", "-p", port, "-U", user, "-d", dbName];
611
+ const cmd = containerName ? "podman" : "pg_isready";
612
+ const r = spawnSync2(cmd, args, {
613
+ timeout: 5e3,
614
+ env: containerName ? process.env : { ...process.env, PGPASSWORD: password }
615
+ });
616
+ return r.status === 0;
617
+ }
618
+ async function resolveDbCredentials(force) {
619
+ const d = { user: "admin", password: "change-me-in-production", port: "5433" };
620
+ if (force) return d;
621
+ console.log("", "\u6570\u636E\u5E93\u51ED\u636E\uFF08\u56DE\u8F66\u4F7F\u7528\u9884\u8BBE\u503C\uFF09\uFF1A");
622
+ let port = (await ask(`DATABASE_PORT [${d.port}]: `)).trim() || d.port;
623
+ while (true) {
624
+ const portNum = parseInt(port);
625
+ if (!isNaN(portNum) && await portInUse(portNum)) {
626
+ console.log(`
627
+ \u26A0\uFE0F \u7AEF\u53E3 ${port} \u5DF2\u88AB\u5360\u7528`);
628
+ const choice = (await ask(" [1] \u6362\u7AEF\u53E3 [2] \u8FDE\u63A5\u5DF2\u6709\u5B9E\u4F8B\uFF08\u8F93\u5165\u5176\u7528\u6237/\u5BC6\u7801\uFF09\u2192 ")).trim();
629
+ if (choice === "2") {
630
+ const existingUser = (await ask(` \u7528\u6237: `)).trim() || "admin";
631
+ const existingPass = (await ask(` \u5BC6\u7801: `)).trim() || "change-me-in-production";
632
+ const testDb = (await ask(` \u6D4B\u8BD5\u6570\u636E\u540D (\u9ED8\u8BA4 postgres): `)).trim() || "postgres";
633
+ const ok = testPostgresConnection(port, existingUser, existingPass, testDb);
634
+ if (ok) {
635
+ console.log(` \u2705 \u8FDE\u63A5\u6210\u529F`);
636
+ return { user: existingUser, password: existingPass, port, reuseExisting: true };
637
+ } else {
638
+ console.log(` \u274C \u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u51ED\u636E`);
639
+ }
640
+ }
641
+ } else {
642
+ break;
643
+ }
644
+ port = (await ask(`DATABASE_PORT: `)).trim() || d.port;
645
+ }
646
+ return { user: (await ask(`DATABASE_USER [${d.user}]: `)).trim() || d.user, password: (await ask(`DATABASE_PASSWORD [${d.password}]: `)).trim() || d.password, port };
647
+ }
648
+ function composeContent(projectName) {
649
+ return `services:
650
+ postgres:
651
+ image: docker.io/postgres:16-alpine
652
+ container_name: \${PROJECT_NAME:-${projectName}}-postgres
653
+ restart: unless-stopped
654
+ ports:
655
+ - "127.0.0.1:\${DATABASE_PORT:-5433}:5432"
656
+ volumes:
657
+ - ./data/postgres/\${PROJECT_NAME:-${projectName}}:/var/lib/postgresql/data
658
+ env_file:
659
+ - .env.development
660
+ environment:
661
+ POSTGRES_USER: \${DATABASE_USER:-admin}
662
+ POSTGRES_PASSWORD: \${DATABASE_PASSWORD:-change-me-in-production}
663
+ POSTGRES_DB: \${PROJECT_NAME:-${projectName}}
664
+ TZ: "Asia/Shanghai"
665
+ networks:
666
+ - \${PROJECT_NAME:-${projectName}}-network
667
+ healthcheck:
668
+ test: ["CMD-SHELL", "pg_isready -U \${DATABASE_USER:-admin} -d \${PROJECT_NAME:-${projectName}}"]
669
+ interval: 10s
670
+ timeout: 5s
671
+ retries: 5
672
+
673
+ networks:
674
+ \${PROJECT_NAME:-${projectName}}-network:
675
+ driver: bridge
676
+ `;
677
+ }
678
+ function writeSqliteExportScript(projectRoot, dryRun) {
679
+ const scriptsDir = resolve4(projectRoot, "scripts");
680
+ const scriptPath = resolve4(scriptsDir, "export-db.ts");
681
+ const content = `import { PrismaClient } from "@prisma/client";
682
+ import { writeFileSync, mkdirSync, existsSync } from "fs";
683
+ import { resolve } from "path";
684
+
685
+ const prisma = new PrismaClient();
686
+ const EXPORTS_DIR = resolve(process.cwd(), "data/exports");
687
+
688
+ async function main() {
689
+ if (!existsSync(EXPORTS_DIR)) mkdirSync(EXPORTS_DIR, { recursive: true });
690
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
691
+ const auditLogs = await prisma.auditLog.findMany({ orderBy: { createdAt: "desc" } });
692
+ const devOps = await prisma.devOperation.findMany({ orderBy: { createdAt: "desc" } });
693
+ writeFileSync(resolve(EXPORTS_DIR, \`audit-export-\${ts}.json\`), JSON.stringify({ exportedAt: new Date().toISOString(), auditLogs: { count: auditLogs.length, rows: auditLogs }, devOperations: { count: devOps.length, rows: devOps } }, null, 2), "utf-8");
694
+ console.log(\`\u5DF2\u5BFC\u51FA \${auditLogs.length} AuditLog + \${devOps.length} DevOperation\`);
695
+ await prisma.\\$disconnect();
696
+ }
697
+
698
+ main().catch((e) => { console.error(e); process.exit(1); });
699
+ `;
700
+ if (dryRun) {
701
+ console.log(`[dry-run] \u5C06\u5199\u5165 ${scriptPath}`);
702
+ return;
703
+ }
704
+ if (!existsSync6(scriptsDir)) mkdirSync3(scriptsDir, { recursive: true });
705
+ writeFileSync3(scriptPath, content, "utf-8");
706
+ console.log("\u5DF2\u751F\u6210 scripts/export-db.ts");
707
+ }
708
+ function injectDbExportScript(projectRoot, dryRun) {
709
+ const pkgPath = resolve4(projectRoot, "package.json");
710
+ if (!existsSync6(pkgPath)) return;
711
+ if (dryRun) {
712
+ console.log("[dry-run] \u5C06\u6CE8\u5165 db:export");
713
+ return;
714
+ }
715
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
716
+ if (!pkg.scripts) pkg.scripts = {};
717
+ if (!pkg.scripts["db:export"]) {
718
+ pkg.scripts["db:export"] = "npx tsx scripts/export-db.ts";
719
+ writeFileSync3(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
720
+ console.log("\u5DF2\u5728 package.json \u6CE8\u5165 db:export");
721
+ }
722
+ }
476
723
  async function initCommand(options) {
477
724
  const projectRoot = process.cwd();
478
- const specified = options.adapter && options.adapter !== "auto" ? options.adapter : null;
479
- const target = specified || detectIDE2(projectRoot);
480
- console.log(`\u68C0\u6D4B\u5230 IDE: ${target}${specified ? " (--adapter)" : " (\u81EA\u52A8)"}`);
481
- const config = await loadConfig(projectRoot, options.config, { yes: options.yes, force: options.force });
725
+ const target = await resolveAdapter(projectRoot, options.adapter);
726
+ const magicDir = MAGIC_DIR_MAP[target];
727
+ const config = await loadConfig(projectRoot, options.config, { force: options.force });
482
728
  config.projectRoot = projectRoot;
483
- if (!options.dryRun) {
484
- await injectPrisma2(projectRoot, { yes: options.yes, force: options.force, dryRun: options.dryRun });
729
+ config.magicDir = magicDir;
730
+ const db = await resolveDbEngine(!!options.force);
731
+ if (db.engine === "postgresql") {
732
+ db.container = await resolveContainer(!!options.force);
733
+ if (db.container && db.container !== "manual") {
734
+ Object.assign(db, await resolveDbCredentials(!!options.force));
735
+ }
736
+ }
737
+ if (db.engine === "postgresql" && db.container && db.container !== "manual") {
738
+ if (!db.reuseExisting) {
739
+ const composeName = db.container === "podman" ? "podman-compose.yml" : "docker-compose.yml";
740
+ const composePath = resolve4(projectRoot, composeName);
741
+ if (!options.dryRun && (!existsSync6(composePath) || options.force)) {
742
+ writeFileSync3(composePath, composeContent(config.projectName || "add-project"), "utf-8");
743
+ console.log(`\u5DF2\u521B\u5EFA ${composeName}`);
744
+ }
745
+ }
746
+ const devEnvPath = resolve4(projectRoot, ".env.development");
747
+ if (!options.dryRun && existsSync6(devEnvPath)) {
748
+ const existing = readFileSync7(devEnvPath, "utf-8");
749
+ if (!/^DATABASE_USER=/m.test(existing)) {
750
+ writeFileSync3(devEnvPath, existing + `
751
+ DATABASE_USER=${db.user || "admin"}
752
+ DATABASE_PASSWORD=${db.password || "change-me-in-production"}
753
+ DATABASE_PORT=${db.port || "5433"}
754
+ PROJECT_NAME=${config.projectName || "add-project"}
755
+ `, "utf-8");
756
+ console.log("\u5DF2\u5C06\u51ED\u636E\u8FFD\u52A0\u5230 .env.development");
757
+ }
758
+ }
485
759
  }
486
760
  const coreFiles = renderCore(config, !!options.dryRun);
487
761
  console.log(`Core \u6A21\u677F: ${coreFiles.size} \u6587\u4EF6`);
488
- const CORE_TARGETS = [".add", ".qoder", ".claude"];
762
+ const CORE_TARGETS = [".add", ".qoder", ".claude", ".vscode"];
489
763
  const allFiles = /* @__PURE__ */ new Map();
490
764
  for (const [relPath, content] of coreFiles) {
491
- for (const target2 of CORE_TARGETS) {
492
- const targetPath = relPath.replace(/^\.add/, target2);
493
- if (!allFiles.has(targetPath)) {
494
- allFiles.set(targetPath, content);
495
- }
765
+ for (const t of CORE_TARGETS) {
766
+ const targetPath = relPath.replace(/^\.add/, t);
767
+ if (!allFiles.has(targetPath)) allFiles.set(targetPath, content);
496
768
  }
497
769
  }
498
- const adapters = resolveAdapters2(target);
499
- for (const adapter of adapters) {
770
+ for (const adapter of resolveAdapters2(target)) {
500
771
  const renderFn = ADAPTER_RENDERERS[adapter];
501
772
  if (renderFn) {
502
773
  const adapterFiles = renderFn(config, projectRoot, !!options.dryRun);
503
- for (const [path, content] of adapterFiles) {
504
- allFiles.set(path, content);
505
- }
774
+ for (const [p, c] of adapterFiles) allFiles.set(p, c);
506
775
  console.log(`${adapter} adapter: ${adapterFiles.size} \u6587\u4EF6`);
507
776
  }
508
777
  }
509
- const result = await writeFiles2(projectRoot, allFiles, {
510
- yes: options.yes,
511
- force: options.force,
512
- dryRun: options.dryRun
513
- });
778
+ const result = await writeFiles2(projectRoot, allFiles, { force: options.force, dryRun: options.dryRun });
779
+ if (db.engine === "postgresql" && db.container && db.container !== "manual") {
780
+ if (!options.dryRun) {
781
+ const dbScript = resolve4(projectRoot, ".qoder", "scripts", "db-ensure.sh");
782
+ const dbEnv = { ...process.env, DATABASE_USER: db.user, DATABASE_PASSWORD: db.password, DATABASE_PORT: db.port, PROJECT_NAME: config.projectName };
783
+ const mode = db.reuseExisting ? "manual" : db.container;
784
+ console.log(db.reuseExisting ? "\u590D\u7528\u5DF2\u6709 PostgreSQL ..." : `\u90E8\u7F72\u6570\u636E\u5E93 (${db.container}) ...`);
785
+ spawnSync2("bash", [dbScript, "postgresql", mode, "--migrate"], { cwd: projectRoot, stdio: "inherit", env: dbEnv });
786
+ try {
787
+ await injectPrisma2(projectRoot, { force: !!options.force });
788
+ } catch (e) {
789
+ console.log(`Prisma \u540C\u6B65\u5931\u8D25: ${e instanceof Error ? e.message : String(e)}`);
790
+ }
791
+ }
792
+ }
793
+ if (db.engine === "postgresql" && db.container === "manual") {
794
+ if (!options.dryRun) {
795
+ const dbScript = resolve4(projectRoot, ".qoder", "scripts", "db-ensure.sh");
796
+ if (existsSync6(dbScript)) spawnSync2("bash", [dbScript, "postgresql", "manual"], { cwd: projectRoot, stdio: "inherit" });
797
+ console.log(["", "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501", "ADD \u6A21\u677F\u5DF2\u5C31\u4F4D\u3002\u5728\u5B8C\u6210\u4EE5\u4E0B\u64CD\u4F5C\u524D MCP \u4E0D\u53EF\u7528\uFF1A", "", "1. \u7F16\u8F91 .env.development\uFF0C\u914D\u7F6E DATABASE_URL", "2. \u91CD\u65B0\u8FD0\u884C add-coder init \u5B8C\u6210\u8FC1\u79FB", "", "\u26A0\uFE0F \u975E PG \u6570\u636E\u5E93\u9700\u7F16\u8F91 .qoder/scripts/mcp-server.ts \u624B\u52A8\u914D Prisma 7 adapter", "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"].join("\n"));
798
+ }
799
+ }
800
+ if (db.engine === "manual") {
801
+ if (!options.dryRun) {
802
+ console.log(["", "Prisma \u652F\u6301\u7684 datasource: postgresql / mysql / sqlite / sqlserver / cockroachdb", "\u81EA\u884C prisma init + \u7F16\u8F91 .env.development\uFF0C\u91CD\u65B0 run init \u5B8C\u6210\u8FC1\u79FB\u3002", "", "\u26A0\uFE0F Prisma 7 adapter \u9700\u624B\u52A8\u914D \u2192 .qoder/scripts/mcp-server.ts"].join("\n"));
803
+ }
804
+ }
805
+ if (db.engine === "sqlite") {
806
+ writeSqliteExportScript(projectRoot, !!options.dryRun);
807
+ injectDbExportScript(projectRoot, !!options.dryRun);
808
+ if (!options.dryRun) {
809
+ try {
810
+ await injectPrisma2(projectRoot, { force: !!options.force, datasource: "sqlite" });
811
+ } catch (e) {
812
+ console.log(`SQLite \u540C\u6B65\u5931\u8D25: ${e instanceof Error ? e.message : String(e)}`);
813
+ }
814
+ }
815
+ }
514
816
  console.log(`
515
817
  \u5B8C\u6210: \u65B0\u5EFA ${result.created}, \u8DF3\u8FC7 ${result.skipped}, \u8986\u76D6 ${result.overwritten}`);
516
818
  if (!options.dryRun) {
517
- console.log("\u63D0\u793A: \u91CD\u542F IDE \u4EE5\u52A0\u8F7D hook \u914D\u7F6E");
819
+ if (db.engine === "sqlite") console.log("\u6570\u636E\u5907\u4EFD: npm run db:export \u2192 data/exports/");
820
+ const pkg = JSON.parse(readFileSync7(resolve4(import.meta.dirname, "../package.json"), "utf-8"));
821
+ const peerNames = Object.keys(pkg.peerDependencies || {});
822
+ if (peerNames.length > 0) {
823
+ console.log(`
824
+ \u5B89\u88C5 peer \u4F9D\u8D56 (${peerNames.join(" ")}) ...`);
825
+ const pm = detectPm(projectRoot);
826
+ spawnSync2(pm, pm === "pnpm" ? ["add", ...peerNames] : ["install", ...peerNames], { cwd: projectRoot, stdio: "inherit" });
827
+ }
828
+ if (db.engine !== "manual" && (db.engine !== "postgresql" || db.container !== "manual")) {
829
+ console.log("\u63D0\u793A: \u91CD\u542F IDE \u4EE5\u52A0\u8F7D hook \u914D\u7F6E");
830
+ }
518
831
  }
519
832
  }
520
833
 
521
834
  // src/cli/commands/sync.ts
522
- import { existsSync as existsSync5 } from "fs";
523
- import { resolve as resolve3 } from "path";
835
+ import { existsSync as existsSync7 } from "fs";
836
+ import { resolve as resolve5 } from "path";
524
837
  async function syncCommand() {
525
838
  const projectRoot = process.cwd();
526
839
  const config = await loadConfig(projectRoot);
@@ -528,7 +841,7 @@ async function syncCommand() {
528
841
  const coreFiles = renderCore(config, false);
529
842
  const missing = /* @__PURE__ */ new Map();
530
843
  for (const [relPath, content] of coreFiles) {
531
- if (!existsSync5(resolve3(projectRoot, relPath))) {
844
+ if (!existsSync7(resolve5(projectRoot, relPath))) {
532
845
  missing.set(relPath, content);
533
846
  }
534
847
  }
@@ -536,13 +849,13 @@ async function syncCommand() {
536
849
  console.log("\u6240\u6709 ADD \u6A21\u677F\u6587\u4EF6\u5DF2\u5C31\u4F4D\u3002");
537
850
  return;
538
851
  }
539
- const result = await writeFiles2(projectRoot, missing, { yes: true });
852
+ const result = await writeFiles2(projectRoot, missing, {});
540
853
  console.log(`\u540C\u6B65\u5B8C\u6210: \u65B0\u5EFA ${result.created}, \u8DF3\u8FC7 ${result.skipped}`);
541
854
  }
542
855
 
543
856
  // src/cli/commands/status.ts
544
- import { existsSync as existsSync6 } from "fs";
545
- import { resolve as resolve4 } from "path";
857
+ import { existsSync as existsSync8 } from "fs";
858
+ import { resolve as resolve6 } from "path";
546
859
  async function statusCommand() {
547
860
  const projectRoot = process.cwd();
548
861
  const config = await loadConfig(projectRoot);
@@ -551,29 +864,29 @@ async function statusCommand() {
551
864
  const missing = [];
552
865
  const present = [];
553
866
  for (const [relPath] of coreFiles) {
554
- if (existsSync6(resolve4(projectRoot, relPath))) {
867
+ if (existsSync8(resolve6(projectRoot, relPath))) {
555
868
  present.push(relPath);
556
869
  } else {
557
870
  missing.push(relPath);
558
871
  }
559
872
  }
560
- console.log(`ADD \u6A21\u677F\u5B8C\u6574\u6027\u68C0\u67E5:`);
873
+ console.log("ADD \u6A21\u677F\u5B8C\u6574\u6027\u68C0\u67E5:");
561
874
  console.log(` \u5DF2\u5C31\u4F4D: ${present.length} \u6587\u4EF6`);
562
875
  if (missing.length > 0) {
563
876
  console.log(` \u7F3A\u5931: ${missing.length} \u6587\u4EF6`);
564
877
  missing.forEach((f) => console.log(` - ${f}`));
565
878
  } else {
566
- console.log(` \u6240\u6709\u6587\u4EF6\u5B8C\u6574\u3002`);
879
+ console.log(" \u6240\u6709\u6587\u4EF6\u5B8C\u6574\u3002");
567
880
  }
568
881
  }
569
882
 
570
883
  // src/cli/index.ts
571
884
  var { version } = JSON.parse(
572
- readFileSync7(new URL("../package.json", import.meta.url), "utf-8")
885
+ readFileSync8(new URL("../package.json", import.meta.url), "utf-8")
573
886
  );
574
887
  var program = new Command();
575
888
  program.name("add-coder").description("\u521D\u59CB\u5316 ADD \u8303\u5F0F\u5DE5\u4F5C\u6D41\u6A21\u677F").version(version);
576
- program.command("init").description("\u521D\u59CB\u5316 ADD \u6A21\u677F\u5230\u5F53\u524D\u9879\u76EE").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode | auto", "auto").option("--config <path>", "\u6307\u5B9A\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84").option("--yes", "\u8DF3\u8FC7\u4EA4\u4E92\uFF0C\u53EA\u521B\u5EFA\u65B0\u6587\u4EF6").option("--force", "\u8986\u76D6\u5DF2\u6709\u6587\u4EF6\uFF0C\u4E0D\u4EA4\u4E92").option("--dry-run", "\u9884\u89C8\u6A21\u5F0F\uFF0C\u4E0D\u5B9E\u9645\u5199\u5165").action(initCommand);
889
+ program.command("init").description("\u521D\u59CB\u5316 ADD \u6A21\u677F\u5230\u5F53\u524D\u9879\u76EE").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode").option("--config <path>", "\u6307\u5B9A\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84").option("--force", "\u8986\u76D6\u5DF2\u6709\u6587\u4EF6\uFF0C\u4E0D\u4EA4\u4E92").option("--dry-run", "\u9884\u89C8\u6A21\u5F0F\uFF0C\u4E0D\u5B9E\u9645\u5199\u5165").action(initCommand);
577
890
  program.command("sync").description("\u589E\u91CF\u540C\u6B65\u7F3A\u5931\u6587\u4EF6").action(syncCommand);
578
891
  program.command("status").description("\u68C0\u67E5 ADD \u6A21\u677F\u5B8C\u6574\u6027").action(statusCommand);
579
892
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "add-coder",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "description": "[codein2027](https://github.com/xiaomingming92/codein2027) 快速构建 ADD 编程范式的完整脚手架——AI 代码治理的落地方案。以「审计即基础设施」为核心,彻底打破编程过程黑盒与跨轮失忆,让编程范式进化为可审计、可追溯、可收敛的新时代。npx 即用,人人可体验。",
6
6
  "type": "module",
@@ -39,8 +39,14 @@
39
39
  "zod": "^3.24.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "@prisma/client": "^6.9.0 || ^7.0.0",
43
- "prisma": "^6.9.0 || ^7.0.0"
42
+ "@modelcontextprotocol/sdk": "^1.0.0",
43
+ "@prisma/adapter-libsql": "^7.0.0",
44
+ "@prisma/adapter-pg": "^7.0.0",
45
+ "@prisma/client": "^7.0.0",
46
+ "dotenv": "*",
47
+ "prisma": "^7.0.0",
48
+ "tsx": "*",
49
+ "zod": "^3.24.0"
44
50
  },
45
51
  "peerDependenciesMeta": {
46
52
  "@prisma/client": {
@@ -48,6 +54,24 @@
48
54
  },
49
55
  "prisma": {
50
56
  "optional": false
57
+ },
58
+ "@modelcontextprotocol/sdk": {
59
+ "optional": false
60
+ },
61
+ "zod": {
62
+ "optional": false
63
+ },
64
+ "dotenv": {
65
+ "optional": false
66
+ },
67
+ "tsx": {
68
+ "optional": false
69
+ },
70
+ "@prisma/adapter-pg": {
71
+ "optional": true
72
+ },
73
+ "@prisma/adapter-libsql": {
74
+ "optional": true
51
75
  }
52
76
  },
53
77
  "devDependencies": {
@@ -56,14 +80,17 @@
56
80
  "eslint": "^9.39.4",
57
81
  "tsup": "^8.4.0",
58
82
  "typescript": "^6.0.3",
59
- "typescript-eslint": "^8.62.1"
83
+ "typescript-eslint": "^8.62.1",
84
+ "vitest": "^4.1.10"
60
85
  },
61
86
  "scripts": {
62
87
  "build": "tsup",
88
+ "predev": "npm run db:ensure",
63
89
  "dev": "tsup --watch",
90
+ "db:ensure": "bash scripts/db-ensure.sh",
64
91
  "lint": "eslint src/",
65
92
  "lint:fix": "eslint src/ --fix",
66
93
  "generate": "tsx src/caijuehub/transcribe.ts",
67
- "test": "node bin/add-coder.js --help"
94
+ "test": "vitest run"
68
95
  }
69
96
  }
@@ -0,0 +1,19 @@
1
+ services:
2
+ postgres:
3
+ image: docker.io/postgres:16-alpine
4
+ container_name: {{projectName}}-postgres
5
+ restart: unless-stopped
6
+ ports:
7
+ - "127.0.0.1:5433:5432"
8
+ volumes:
9
+ - ./data/postgres:/var/lib/postgresql/data
10
+ environment:
11
+ POSTGRES_DB: {{projectName}}
12
+ POSTGRES_USER: admin
13
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
14
+ TZ: "Asia/Shanghai"
15
+ healthcheck:
16
+ test: ["CMD-SHELL", "pg_isready -U admin -d {{projectName}}"]
17
+ interval: 10s
18
+ timeout: 5s
19
+ retries: 5
@@ -1,10 +1,17 @@
1
- // add.prisma — ADD 治理模型(npm 包写入,不修改用户业务文件)
2
- // 前置条件:用户项目必须已有 User 模型(id: String),否则 prisma migrate 失败
1
+ // add.prisma — ADD 治理模型,独立于用户业务表
2
+
3
+ model AddUser {
4
+ id String @id @default(cuid())
5
+ username String @unique
6
+ email String?
7
+ devOperations DevOperation[]
8
+ auditLogs AuditLog[]
9
+ }
3
10
 
4
11
  model DevOperation {
5
12
  id String @id @default(cuid())
6
13
  userId String
7
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
14
+ user AddUser @relation(fields: [userId], references: [id], onDelete: Cascade)
8
15
  planKeyword String
9
16
  action String
10
17
  targetType String
@@ -20,7 +27,7 @@ model DevOperation {
20
27
  model AuditLog {
21
28
  id String @id @default(cuid())
22
29
  userId String
23
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
30
+ user AddUser @relation(fields: [userId], references: [id], onDelete: Cascade)
24
31
  action String
25
32
  targetType String
26
33
  targetId String
@@ -31,4 +38,4 @@ model AuditLog {
31
38
  createdAt DateTime @default(now())
32
39
 
33
40
  @@index([traceId])
34
- }
41
+ }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bash
2
+ # db-ensure.sh — 容器启动 + 环境准备
3
+ # prisma init/copy/push/generate 由 init.ts → injectPrisma() 裁决层处理
4
+ # 用法: bash db-ensure.sh <engine> <container> [--migrate]
5
+ set -euo pipefail
6
+
7
+ ENGINE="${1:-postgresql}"
8
+ CONTAINER="${2:-none}"
9
+ DO_MIGRATE="false"
10
+ [[ "${3:-}" == "--migrate" ]] && DO_MIGRATE="true"
11
+
12
+ PROJECT_DIR="${PROJECT_DIR:-$(pwd)}"
13
+ PROJECT_NAME="${PROJECT_NAME:-$(basename "$PROJECT_DIR")}"
14
+ DB_USER="${DATABASE_USER:-admin}"
15
+ DB_PASS="${DATABASE_PASSWORD:-change-me-in-production}"
16
+ DB_PORT="${DATABASE_PORT:-5433}"
17
+ DB_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${PROJECT_NAME}?schema=public"
18
+
19
+ # ADD 表备份
20
+ backup_add_tables() {
21
+ if ! command -v pg_dump > /dev/null 2>&1; then return; fi
22
+ local bak="add-backup-$(date +%Y%m%d_%H%M%S).sql"
23
+ echo ">>> 备份 ADD 表到 $bak ..."
24
+ PGPASSWORD="$DB_PASS" pg_dump -h localhost -p "$DB_PORT" -U "$DB_USER" -d "$PROJECT_NAME" \
25
+ --table=AddUser --table=DevOperation --table=AuditLog --if-exists > "$bak" 2>/dev/null || true
26
+ }
27
+
28
+ # 0. 确保 .env.development 存在
29
+ if [ ! -f "$PROJECT_DIR/.env.development" ]; then
30
+ cat > "$PROJECT_DIR/.env.development" <<EOF
31
+ DATABASE_URL="${DB_URL}"
32
+ DATABASE_USER=${DB_USER}
33
+ DATABASE_PASSWORD=${DB_PASS}
34
+ DATABASE_PORT=${DB_PORT}
35
+ PROJECT_NAME=${PROJECT_NAME}
36
+ EOF
37
+ echo ">>> 已创建 .env.development"
38
+ fi
39
+
40
+ # ── SQLite:无需容器 ──
41
+ if [ "$ENGINE" = "sqlite" ]; then exit 0; fi
42
+
43
+ # ── 自行管理 PostgreSQL ──
44
+ if [ "$CONTAINER" = "none" ] || [ "$CONTAINER" = "manual" ]; then
45
+ echo ">>> 自行管理 PostgreSQL,跳过容器 ..."
46
+ if [ "$DO_MIGRATE" = "true" ]; then
47
+ backup_add_tables
48
+ fi
49
+ exit 0
50
+ fi
51
+
52
+ # ── 容器模式 ──
53
+ COMPOSE_CMD=""
54
+ if [ "$CONTAINER" = "podman" ]; then COMPOSE_CMD="podman-compose"
55
+ elif [ "$CONTAINER" = "docker" ]; then COMPOSE_CMD="docker-compose"
56
+ else echo "未知容器: $CONTAINER"; exit 1
57
+ fi
58
+
59
+ echo ">>> 启动 PostgreSQL ($COMPOSE_CMD up -d) ..."
60
+ $COMPOSE_CMD up -d || {
61
+ echo "容器启动失败,请检查 $COMPOSE_CMD 是否已安装或端口是否冲突"
62
+ exit 1
63
+ }
64
+
65
+ # 等待 PostgreSQL 就绪
66
+ echo ">>> 等待 PostgreSQL 就绪 ..."
67
+ MAX_RETRIES=30
68
+ RETRY=0
69
+ while [ $RETRY -lt $MAX_RETRIES ]; do
70
+ if $COMPOSE_CMD exec -T postgres pg_isready -U "$DB_USER" > /dev/null 2>&1; then
71
+ echo "PostgreSQL 已就绪"; break
72
+ fi
73
+ sleep 1
74
+ RETRY=$((RETRY + 1))
75
+ done
76
+ if [ $RETRY -ge $MAX_RETRIES ]; then
77
+ echo "PostgreSQL 启动超时,请检查: $COMPOSE_CMD logs postgres"
78
+ exit 1
79
+ fi
80
+
81
+ if [ "$DO_MIGRATE" = "true" ]; then
82
+ backup_add_tables
83
+ fi
@@ -11,7 +11,7 @@ const MAGIC_DIR = basename(dirname(__dirname)) // ".qoder" or ".claude"
11
11
  const ENV_CANDIDATES = [".env.development.local", ".env.development", ".env.local", ".env"];
12
12
  for (const f of ENV_CANDIDATES) {
13
13
  const p = resolve(PROJECT_ROOT, f);
14
- if (existsSync(p)) { dotenv.config({ path: p }); break; }
14
+ if (existsSync(p)) { dotenv.config({ path: p, override: true }); break; }
15
15
  }
16
16
 
17
17
  const DATABASE_URL = process.env.DATABASE_URL
@@ -23,13 +23,28 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
23
23
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
24
24
  import { z } from "zod"
25
25
  import { readFile, readdir, stat, mkdir, writeFile } from "fs/promises"
26
- import { join, relative, basename } from "path"
26
+ import { join, relative } from "path"
27
27
  import { existsSync } from "fs"
28
- import { execSync } from "child_process"
29
- import { PrismaClient, Prisma } from "@prisma/client"
28
+ import { spawnSync } from "child_process"
29
+
30
+ const { PrismaClient } = await import("@prisma/client").catch(() =>
31
+ import("../../generated/prisma/client.js").catch(() =>
32
+ import("../../src/generated/prisma/client.js"))
33
+ )
34
+
35
+ // Prisma 7 按 DATABASE_URL 前缀自动选 adapter
36
+ let adapter = undefined
37
+ if (DATABASE_URL.startsWith("postgresql://") || DATABASE_URL.startsWith("postgres://")) {
38
+ const { PrismaPg } = await import("@prisma/adapter-pg")
39
+ adapter = new PrismaPg({ connectionString: DATABASE_URL })
40
+ } else if (DATABASE_URL.startsWith("file:") || DATABASE_URL.startsWith("sqlite:")) {
41
+ const { PrismaLibSQL } = await import("@prisma/adapter-libsql")
42
+ const url = DATABASE_URL.replace(/^file:/, "")
43
+ adapter = new PrismaLibSQL({ url })
44
+ }
30
45
 
31
46
  const prisma = new PrismaClient({
32
- datasourceUrl: DATABASE_URL,
47
+ ...(adapter ? { adapter } : {}),
33
48
  log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
34
49
  })
35
50
 
@@ -51,6 +66,31 @@ async function readFileSafe(filePath: string): Promise<string | null> {
51
66
  }
52
67
  }
53
68
 
69
+ // ── ADD 文档模板校验(共享函数,供 create_plan / check_add_compliance / check_spec_sync 等复用)──
70
+
71
+ interface GuardResult { ok: boolean; issues: string }
72
+
73
+ async function validateDocWithGuard(filePath: string): Promise<GuardResult> {
74
+ const guardScript = join(PROJECT_ROOT, MAGIC_DIR, "hooks", "doc-format-guard.sh")
75
+ if (!existsSync(guardScript)) return { ok: true, issues: "" }
76
+ const content = await readFileSafe(filePath)
77
+ if (!content) return { ok: false, issues: "文件无法读取" }
78
+ const guardInput = JSON.stringify({
79
+ tool_input: { file_path: filePath, file_content: content },
80
+ })
81
+ const guard = spawnSync("/bin/bash", [guardScript], {
82
+ input: guardInput,
83
+ encoding: "utf-8",
84
+ timeout: 15000,
85
+ cwd: PROJECT_ROOT,
86
+ env: { ...process.env, MAGIC_DIR },
87
+ })
88
+ if (guard.status === 2 || guard.stderr?.includes("校验不通过")) {
89
+ return { ok: false, issues: guard.stderr || guard.stdout || "校验失败" }
90
+ }
91
+ return { ok: true, issues: "" }
92
+ }
93
+
54
94
  /** 递归读取目录下所有文件(扁平化返回),支持 ${MAGIC_DIR}/plans/2026-06/05/ 等分层结构 */
55
95
  async function readdirRecursive(dir: string): Promise<string[]> {
56
96
  const results: string[] = []
@@ -1083,11 +1123,11 @@ server.registerTool(
1083
1123
  }
1084
1124
  }
1085
1125
 
1086
- let parsedBefore: Prisma.InputJsonValue | undefined
1087
- let parsedAfter: Prisma.InputJsonValue | undefined
1126
+ let parsedBefore: any | undefined
1127
+ let parsedAfter: any | undefined
1088
1128
  try {
1089
- if (beforeState) parsedBefore = JSON.parse(beforeState) as Prisma.InputJsonValue
1090
- if (afterState) parsedAfter = JSON.parse(afterState) as Prisma.InputJsonValue
1129
+ if (beforeState) parsedBefore = JSON.parse(beforeState)
1130
+ if (afterState) parsedAfter = JSON.parse(afterState)
1091
1131
  } catch {
1092
1132
  const beforePreview = beforeState ? beforeState.slice(0, 80) : "(未传)"
1093
1133
  const afterPreview = afterState ? afterState.slice(0, 80) : "(未传)"
@@ -1098,18 +1138,17 @@ server.registerTool(
1098
1138
  )
1099
1139
  }
1100
1140
 
1101
- let systemUser = await prisma.user.findUnique({
1141
+ let systemUser = await prisma.addUser.findUnique({
1102
1142
  where: { username: "ai-assistant" },
1103
1143
  select: { id: true },
1104
1144
  })
1105
1145
 
1106
1146
  if (!systemUser) {
1107
- systemUser = await prisma.user.create({
1147
+ systemUser = await prisma.addUser.create({
1108
1148
  data: {
1109
1149
  id: "ai-assistant",
1110
1150
  username: "ai-assistant",
1111
- email: "ai-assistant@internal",
1112
- password: "internal",
1151
+ email: "ai-assistant@internal"
1113
1152
  },
1114
1153
  select: { id: true },
1115
1154
  })
@@ -1122,8 +1161,8 @@ server.registerTool(
1122
1161
  action,
1123
1162
  targetType,
1124
1163
  targetId: targetId || "unknown",
1125
- beforeState: parsedBefore ?? Prisma.JsonNull,
1126
- afterState: parsedAfter ?? Prisma.JsonNull,
1164
+ beforeState: parsedBefore ?? null,
1165
+ afterState: parsedAfter ?? null,
1127
1166
  reason: reason || null,
1128
1167
  },
1129
1168
  })
@@ -2080,7 +2119,9 @@ server.registerTool(
2080
2119
  return errorResponse(`未找到匹配的 Plan 文件(关键词: ${args.planKeyword})`)
2081
2120
  }
2082
2121
  const planPath = join(plansDir, planMatch)
2122
+ const planGuard = await validateDocWithGuard(planPath)
2083
2123
  lines.push(`Plan: ${planMatch}`)
2124
+ if (!planGuard.ok) lines.push(` ⚠️ 模板校验: ${planGuard.issues.split("\n")[0]}`)
2084
2125
 
2085
2126
  // 2. 从 Plan 中提取关联的 spec 目录名
2086
2127
  const planContent = await readFileSafe(planPath)
@@ -2387,9 +2428,10 @@ server.registerTool(
2387
2428
  // 5. Git diff 统计
2388
2429
  let changedFiles: string[] = []
2389
2430
  if (isEnabled("gitDiff")) {
2390
- const { execSync } = await import("child_process")
2431
+ const { spawnSync } = await import("child_process")
2391
2432
  try {
2392
- const diffStat = execSync("git diff --name-only", { cwd: PROJECT_ROOT, encoding: "utf-8", timeout: 5000 })
2433
+ const diff = spawnSync("git", ["diff", "--name-only"], { cwd: PROJECT_ROOT, encoding: "utf-8", timeout: 5000 })
2434
+ const diffStat = diff.stdout || ""
2393
2435
  changedFiles = diffStat.trim().split("\n").filter(Boolean)
2394
2436
  } catch {
2395
2437
  lines.push("Git diff: 无法获取(可能无 git 仓库或无暂存变更)")
@@ -2576,6 +2618,9 @@ server.registerTool(
2576
2618
  return errorResponse(`add-route 完整性扫描失败:无法读取文件 ${matchedFile}`)
2577
2619
  }
2578
2620
 
2621
+ // 模板格式校验
2622
+ const routeGuard = await validateDocWithGuard(filePath)
2623
+
2579
2624
  const lines = content.split("\n")
2580
2625
 
2581
2626
  // === 3. 解析 Step 结构和勾选状态 ===
@@ -2681,6 +2726,7 @@ server.registerTool(
2681
2726
  "",
2682
2727
  `整体完成度: ${globalChecked}/${globalTotal} (${completionRate}%)`,
2683
2728
  `状态: ${isComplete ? "✅ complete — add-route 完整闭环" : "⚠️ incomplete — 存在未勾选 Step"}`,
2729
+ `模板校验: ${routeGuard.ok ? "✅ 通过" : `⚠️ ${routeGuard.issues.split("\n")[0]}`}`,
2684
2730
  "",
2685
2731
  ]
2686
2732
 
@@ -3198,8 +3244,9 @@ server.registerTool(
3198
3244
  let scopeScore = 100
3199
3245
  let changedFiles: string[] = []
3200
3246
  try {
3201
- const { execSync } = await import("child_process")
3202
- const diffStat = execSync("git diff --name-only", { cwd: PROJECT_ROOT, encoding: "utf-8", timeout: 5000 })
3247
+ const { spawnSync } = await import("child_process")
3248
+ const diff = spawnSync("git", ["diff", "--name-only"], { cwd: PROJECT_ROOT, encoding: "utf-8", timeout: 5000 })
3249
+ const diffStat = diff.stdout || ""
3203
3250
  changedFiles = diffStat.trim().split("\n").filter(Boolean)
3204
3251
  } catch { /* ignore */ }
3205
3252
 
@@ -3240,12 +3287,13 @@ server.registerTool(
3240
3287
  // ====== 维度二:类型安全(权重 20%) ======
3241
3288
  let typeScore = 100
3242
3289
  try {
3243
- const { execSync } = await import("child_process")
3244
- const tscOut = execSync("npx tsc --noEmit 2>&1 || true", {
3290
+ const { spawnSync } = await import("child_process")
3291
+ const tsc = spawnSync("npx", ["tsc", "--noEmit"], {
3245
3292
  cwd: PROJECT_ROOT,
3246
3293
  encoding: "utf-8",
3247
3294
  timeout: 30000,
3248
3295
  })
3296
+ const tscOut = (tsc.stdout || "") + (tsc.stderr || "")
3249
3297
  // 统计 error TS 行数
3250
3298
  const errorLines = tscOut.split("\n").filter(l => l.includes("error TS")).length
3251
3299
  typeScore = Math.max(0, 100 - errorLines * 10)
@@ -3431,20 +3479,28 @@ server.registerTool(
3431
3479
 
3432
3480
  // 写入 Plan 文件
3433
3481
  await writeFile(filePath, content, "utf-8")
3482
+
3483
+ // 写后校验
3484
+ const guard = await validateDocWithGuard(filePath)
3485
+ if (!guard.ok) {
3486
+ try { await import("fs/promises").then(m => m.unlink(filePath)) } catch { }
3487
+ return errorResponse(`Plan 模板校验不通过,拒绝写入:\n${guard.issues}`)
3488
+ }
3489
+
3434
3490
  parts.push(`✅ Plan 文件已创建: ${relativePath}`)
3435
3491
 
3436
3492
  // 更新 index.md
3437
3493
  const indexScript = join(PROJECT_ROOT, "scripts", "gen-plan-index.sh")
3438
3494
  if (existsSync(indexScript)) {
3439
- try {
3440
- const result = execSync(`/bin/bash "${indexScript}"`, {
3441
- cwd: PROJECT_ROOT,
3442
- encoding: "utf-8",
3443
- timeout: 10000,
3444
- })
3445
- parts.push(`📋 index.md 已更新: ${result.trim()}`)
3446
- } catch (indexErr) {
3447
- parts.push(`⚠️ index.md 更新失败(将在 crontab 或下次调用时自动更新): ${indexErr instanceof Error ? indexErr.message : String(indexErr)}`)
3495
+ const result = spawnSync("/bin/bash", [indexScript], {
3496
+ cwd: PROJECT_ROOT,
3497
+ encoding: "utf-8",
3498
+ timeout: 10000,
3499
+ })
3500
+ if (result.status === 0) {
3501
+ parts.push(`📋 index.md 已更新: ${result.stdout?.trim() || "ok"}`)
3502
+ } else {
3503
+ parts.push(`⚠️ index.md 更新失败: ${result.stderr?.trim() || result.error?.message || "unknown"}`)
3448
3504
  }
3449
3505
  } else {
3450
3506
  parts.push(`⚠️ gen-plan-index.sh 不存在,index.md 将在 crontab 自动更新`)