add-coder 0.3.16 → 0.3.18

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.
Files changed (33) hide show
  1. package/dist/index.js +269 -37
  2. package/package.json +1 -1
  3. package/templates/.add-coder-src-hash.json +31 -24
  4. package/templates/adapters/claude/hooks/pre-tool-use.sh +6 -1
  5. package/templates/adapters/codex/hooks/pre-tool-use.sh +7 -1
  6. package/templates/adapters/qoder/hooks/pre-tool-use.sh +6 -1
  7. package/templates/adapters/trae/hooks/pre-tool-use.sh +7 -1
  8. package/templates/adapters/vscode/hooks/pre-tool-use.sh +6 -1
  9. package/templates/core/hooks/pre-tool-use.sh +7 -1
  10. package/templates/core/prisma/add.prisma +35 -0
  11. package/templates/core/rules/profiles/index.toml +12 -0
  12. package/templates/core/rules/profiles/machineserver-profile.md +22 -0
  13. package/templates/core/rules/profiles/webapp-profile.md +61 -0
  14. package/templates/core/rules/project_rules.md +6 -54
  15. package/templates/core/scripts/mcp-server/tools/audit.ts +23 -14
  16. package/templates/core/scripts/mcp-server/tools/context.ts +20 -2
  17. package/templates/core/scripts/mcp-server/tools/contract.ts +202 -0
  18. package/templates/core/scripts/mcp-server/tools/docs.ts +2 -2
  19. package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_completeness.ts +2 -2
  20. package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_status.ts +2 -2
  21. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +2 -2
  22. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +2 -2
  23. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
  24. package/templates/core/scripts/mcp-server/tools/gateway/index.ts +2 -2
  25. package/templates/core/scripts/mcp-server/tools/hitl.ts +13 -5
  26. package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +2 -2
  27. package/templates/core/scripts/mcp-server/tools/index.ts +29 -10
  28. package/templates/core/scripts/mcp-server/tools/plan.ts +5 -2
  29. package/templates/core/scripts/mcp-server/tools/quality.ts +2 -2
  30. package/templates/core/scripts/mcp-server/tools/registrar.ts +11 -0
  31. package/templates/core/scripts/mcp-server/tools/review.ts +2 -2
  32. package/templates/core/templates/collab-contract-template.md +203 -0
  33. package/templates/core/templates/collab-contract-template.schema.json +133 -0
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 readFileSync8 } from "fs";
4
+ import { readFileSync as readFileSync9 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/caijuehub/strategies/detect.strategy.ts
@@ -63,6 +63,7 @@ var AddCoderConfigSchema = z.object({
63
63
  mcpServerCommand: z.string().default("tsx"),
64
64
  agentAuditImport: z.string().default("@/lib/agent-audit-logger"),
65
65
  magicDir: z.string(),
66
+ stack: z.string().optional(),
66
67
  adapters: z.array(AdapterEnum).default([]),
67
68
  overrides: z.record(z.string(), z.string()).default({})
68
69
  });
@@ -79,6 +80,7 @@ var defaults = {
79
80
  mcpServerCommand: "tsx",
80
81
  agentAuditImport: "@/lib/agent-audit-logger",
81
82
  magicDir: "",
83
+ stack: "",
82
84
  adapters: [],
83
85
  overrides: {}
84
86
  };
@@ -182,7 +184,7 @@ async function selectFiles(projectRoot, files) {
182
184
  process.stdout.write(renderList(items, selected, scrollIdx, VISIBLE));
183
185
  }
184
186
  draw();
185
- return new Promise((resolve8) => {
187
+ return new Promise((resolve9) => {
186
188
  input.on("keypress", (_char, key) => {
187
189
  if (!key) return;
188
190
  if (key.name === "up") {
@@ -223,7 +225,7 @@ async function selectFiles(projectRoot, files) {
223
225
  if (selected.has(idx)) result.set(relPath, content);
224
226
  idx++;
225
227
  }
226
- resolve8(result);
228
+ resolve9(result);
227
229
  });
228
230
  });
229
231
  }
@@ -314,7 +316,7 @@ var SYNC_PRISMA_CONFIG = {
314
316
  { label: "\u751F\u4EA7/CI\uFF08\u53EA\u5E94\u7528\u5DF2\u6709\u8FC1\u79FB\uFF09", cmd: "npx prisma migrate deploy" },
315
317
  { label: "\u4E0D\u786E\u5B9A\uFF1F\u5148\u67E5\u72B6\u6001", cmd: "npx prisma migrate status" }
316
318
  ],
317
- P3005: { hint: "\u82E5\u62A5 P3005\uFF08\u5E93\u6709\u8868\u4F46\u65E0\u8FC1\u79FB\u8BB0\u5F55\uFF09= \u9700\u5148\u57FA\u7EBF\u5316:", cmd: "npx prisma migrate resolve --applied <\u8FC1\u79FB\u540D>\uFF0C\u518D\u6267\u884C deploy" },
319
+ P3005: { hint: "\u82E5\u62A5 P3005\uFF08\u5E93\u6709\u8868\u4F46\u65E0\u8FC1\u79FB\u8BB0\u5F55\uFF09= \u9700\u5148\u57FA\u7EBF\u5316:", cmd: "npx prisma migrate resolve --applied <\uFFFD\uFFFD\u79FB\u540D>\uFF0C\u518D\u6267\u884C deploy" },
318
320
  UNMANAGED_ACTIONS: [
319
321
  { label: "\u9996\u6B21\u63A5\u5165\u8FC1\u79FB\u7BA1\u7406\uFF08\u7A7A\u5E93\uFF09", cmd: "npx prisma migrate dev --name init" },
320
322
  { label: "\u5E93\u5DF2\u6709\u8868\uFF08db push \u5EFA\u8FC7\uFF09", hint: "\u5148\u624B\u52A8\u57FA\u7EBF\u5316\uFF0C\u518D deploy:", steps: ["npx prisma migrate diff --from-empty --to-schema-datamodel <schema\u6587\u4EF6> --script", "\u5C06\u8F93\u51FA\u5199\u5165 prisma/migrations/<\u65F6\u95F4\u6233>_init/migration.sql", "npx prisma migrate resolve --applied <\u65F6\u95F4\u6233>_init"] }
@@ -400,7 +402,7 @@ function diffPrisma(basePath, targetPath) {
400
402
  }
401
403
 
402
404
  // src/core/renderer.ts
403
- import { readFileSync as readFileSync4, readdirSync, statSync, existsSync as existsSync5 } from "fs";
405
+ import { readFileSync as readFileSync4, readdirSync, statSync, existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
404
406
  import { join as join4, relative, dirname as dirname2 } from "path";
405
407
  import { fileURLToPath } from "url";
406
408
  import { parse } from "smol-toml";
@@ -418,6 +420,24 @@ var PLACEHOLDERS = {
418
420
  "{{mcpServerCommand}}": "mcpServerCommand",
419
421
  "{{agentAuditImport}}": "agentAuditImport"
420
422
  };
423
+ function renderStackPlaceholders(result, config) {
424
+ const stackName = config.stack || "\u672A\u8BBE\u7F6E";
425
+ const stackProfile = config.stack ? `${config.stack}-profile.md` : "\u672A\u8BBE\u7F6E";
426
+ let refLine;
427
+ if (config.stack) {
428
+ refLine = [
429
+ `\u672C\u9879\u76EE\u7684\u6280\u672F\u6808\u7EA6\u675F\u7531 \`${config.magicDir}/rules/profiles/${stackProfile}\` \u5B9A\u4E49\uFF08\u7531 \`add-coder stack set\` \u7BA1\u7406\uFF09\u3002`,
430
+ `- **\u5F53\u524D\u6280\u672F\u6808**: \`${stackName}\`\uFF08${stackProfile} \u5DF2\u751F\u6548\uFF0CAI \u5FC5\u987B\u9075\u5B88\u5176\u4E2D\u5168\u90E8\u7EA6\u675F\uFF09`
431
+ ].join("\n");
432
+ } else {
433
+ refLine = [
434
+ "\u672C\u9879\u76EE\u7684\u6280\u672F\u6808\u672A\u8BBE\u7F6E\uFF0C\u4E0D\u65BD\u52A0\u4EFB\u4F55\u6280\u672F\u6808\u5047\u8BBE\u3002",
435
+ "AI \u5FC5\u987B\u901A\u8FC7 `get_project_context` \u8BFB\u53D6\u9879\u76EE\u5B9E\u9645\u4EE3\u7801\u63A8\u65AD\u771F\u5B9E\u6280\u672F\u6808\uFF0C\u7981\u6B62\u5957\u7528\u6A21\u677F\u6216\u6848\u4F8B\u4E2D\u7684\u9ED8\u8BA4\u6280\u672F\u6808\u3002",
436
+ "\uFF08\u53EF\u7528 `add-coder stack set <name>` \u542F\u7528\u6280\u672F\u6808\u7EA6\u675F\uFF09"
437
+ ].join("\n");
438
+ }
439
+ return result.replaceAll("{{stackReferenceLine}}", refLine).replaceAll("{{stackName}}", stackName).replaceAll("{{stackProfile}}", stackProfile);
440
+ }
421
441
  function loadDpsThresholds() {
422
442
  try {
423
443
  const candidates = [
@@ -455,12 +475,44 @@ function render(content, config) {
455
475
  result = result.replaceAll("{{dpsPass}}", thresholds.pass);
456
476
  result = result.replaceAll("{{dpsWarn}}", thresholds.warn);
457
477
  }
478
+ result = renderStackPlaceholders(result, config);
458
479
  return result;
459
480
  }
460
481
  var TEMPLATES_ROOT = join4(__dirname, "../templates");
461
482
  var CORE_DIR = join4(TEMPLATES_ROOT, "core");
462
483
  var CORE_TARGET = ".add";
463
- var SKIP_DIRS = /* @__PURE__ */ new Set(["prisma"]);
484
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["prisma", "profiles"]);
485
+ function loadProfileRegistry() {
486
+ try {
487
+ const tomlPath = join4(CORE_DIR, "rules", "profiles", "index.toml");
488
+ if (!existsSync5(tomlPath)) return [];
489
+ const doc = parse(readFileSync4(tomlPath, "utf-8"));
490
+ return doc.profile || [];
491
+ } catch (e) {
492
+ const errorMessage = e instanceof Error ? e.message : String(e);
493
+ console.warn(`[renderer] profile \u6CE8\u518C\u8868\u8BFB\u53D6\u5931\u8D25: ${errorMessage}`);
494
+ return [];
495
+ }
496
+ }
497
+ function loadStack(projectRoot, magicDir) {
498
+ try {
499
+ const p = join4(projectRoot, magicDir, "stack.json");
500
+ if (!existsSync5(p)) return "";
501
+ const doc = JSON.parse(readFileSync4(p, "utf-8"));
502
+ return typeof doc.stack === "string" ? doc.stack : "";
503
+ } catch {
504
+ return "";
505
+ }
506
+ }
507
+ function saveStack(projectRoot, magicDir, stack) {
508
+ const dir = join4(projectRoot, magicDir);
509
+ mkdirSync2(dir, { recursive: true });
510
+ writeFileSync2(
511
+ join4(dir, "stack.json"),
512
+ JSON.stringify({ stack, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
513
+ "utf-8"
514
+ );
515
+ }
464
516
  function renderCore(config, dryRun) {
465
517
  const files = /* @__PURE__ */ new Map();
466
518
  function walk(dir, base) {
@@ -480,6 +532,17 @@ function renderCore(config, dryRun) {
480
532
  }
481
533
  }
482
534
  walk(CORE_DIR, "");
535
+ if (config.stack) {
536
+ const registry = loadProfileRegistry();
537
+ const profile = registry.find((p) => p.name === config.stack);
538
+ if (profile) {
539
+ const src = join4(CORE_DIR, "rules", "profiles", profile.file);
540
+ if (existsSync5(src)) {
541
+ const rendered = render(readFileSync4(src, "utf-8"), config);
542
+ files.set(join4(".add", "rules", "profiles", profile.file), rendered);
543
+ }
544
+ }
545
+ }
483
546
  if (dryRun) {
484
547
  console.log(`[dry-run] Core templates: ${files.size} files`);
485
548
  }
@@ -565,7 +628,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
565
628
 
566
629
  // src/caijuehub/strategies/prisma.strategy.ts
567
630
  import { spawnSync } from "child_process";
568
- import { copyFileSync, existsSync as existsSync7, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
631
+ import { copyFileSync, existsSync as existsSync7, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
569
632
  import { resolve as resolve3 } from "path";
570
633
  var PRISMA_CONFIG = {
571
634
  onMissing: "ask",
@@ -578,7 +641,7 @@ var PRISMA_CONFIG = {
578
641
  };
579
642
  function ensurePrismaConfig(projectRoot) {
580
643
  const configPath = resolve3(projectRoot, "prisma.config.ts");
581
- writeFileSync2(configPath, [
644
+ writeFileSync3(configPath, [
582
645
  'import dotenv from "dotenv";',
583
646
  'import { existsSync } from "fs";',
584
647
  'for (const f of [".env.development.local", ".env.development", ".env.local", ".env"]) {',
@@ -603,7 +666,7 @@ function backupAddTables(projectRoot) {
603
666
  timeout: 3e4
604
667
  });
605
668
  if (r.stdout.length > 0) {
606
- writeFileSync2(bak, r.stdout, "utf-8");
669
+ writeFileSync3(bak, r.stdout, "utf-8");
607
670
  console.log(`>>> \u5907\u4EFD ADD \u8868\u5230 ${bak}`);
608
671
  return bak;
609
672
  }
@@ -621,7 +684,7 @@ function runPrismaInit(projectRoot, provider, schemaPath) {
621
684
  if (initResult.status !== 0 || !existsSync7(schemaPath)) {
622
685
  console.log("prisma init \u5931\u8D25\uFF0C\u624B\u52A8\u521B\u5EFA schema.prisma ...");
623
686
  const prismaDir = resolve3(projectRoot, "prisma");
624
- if (!existsSync7(prismaDir)) mkdirSync2(prismaDir, { recursive: true });
687
+ if (!existsSync7(prismaDir)) mkdirSync3(prismaDir, { recursive: true });
625
688
  const content = `generator client {
626
689
  provider = "prisma-client-js"
627
690
  }
@@ -630,11 +693,11 @@ datasource db {
630
693
  provider = "${provider}"
631
694
  }
632
695
  `;
633
- writeFileSync2(schemaPath, content, "utf-8");
696
+ writeFileSync3(schemaPath, content, "utf-8");
634
697
  const devEnvPath = resolve3(projectRoot, ".env.development");
635
698
  if (!existsSync7(devEnvPath)) {
636
699
  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"';
637
- writeFileSync2(devEnvPath, defaultUrl + "\n", "utf-8");
700
+ writeFileSync3(devEnvPath, defaultUrl + "\n", "utf-8");
638
701
  console.log("\u5DF2\u521B\u5EFA .env.development");
639
702
  }
640
703
  return false;
@@ -650,7 +713,7 @@ function postInitSetup(projectRoot, schemaPath, addPrismaTemplate, destPath) {
650
713
  if (dbUrl) {
651
714
  const existing = existsSync7(devEnvPath) ? readFileSync5(devEnvPath, "utf-8") : "";
652
715
  if (!existing.includes("DATABASE_URL=")) {
653
- writeFileSync2(devEnvPath, `${existing}${existing ? "\n" : ""}${dbUrl[0]}
716
+ writeFileSync3(devEnvPath, `${existing}${existing ? "\n" : ""}${dbUrl[0]}
654
717
  `, "utf-8");
655
718
  }
656
719
  if (existsSync7(envPath)) unlinkSync(envPath);
@@ -758,7 +821,7 @@ async function injectPrisma2(projectRoot, options = {}) {
758
821
  }
759
822
 
760
823
  // src/cli/commands/init.ts
761
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync8, mkdirSync as mkdirSync3, copyFileSync as copyFileSync2, readdirSync as readdirSync2 } from "fs";
824
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync4, copyFileSync as copyFileSync2, readdirSync as readdirSync2 } from "fs";
762
825
  import { createHash } from "crypto";
763
826
  import { resolve as resolve5 } from "path";
764
827
  import { spawnSync as spawnSync2 } from "child_process";
@@ -774,6 +837,12 @@ var MAGIC_DIR_MAP = { claude: ".claude", qoder: ".qoder", vscode: ".vscode", tra
774
837
  async function initCommand(options) {
775
838
  const ctx = await prepare(options);
776
839
  writeComposeEnv(ctx);
840
+ if (!options.dryRun) {
841
+ saveStack(ctx.projectRoot, ctx.magicDir, ctx.stack);
842
+ if (ctx.stack) console.log(`\u6280\u672F\u6808\u72B6\u6001\u5DF2\u5199\u5165 ${ctx.magicDir}/stack.json \u2192 ${ctx.stack}`);
843
+ } else if (ctx.stack) {
844
+ console.log(`[dry-run] \u5C06\u5199\u5165 ${ctx.magicDir}/stack.json \u2192 ${ctx.stack}`);
845
+ }
777
846
  const result = await renderAndWrite(ctx);
778
847
  await deployDatabase(ctx);
779
848
  deployDocs(ctx);
@@ -791,10 +860,12 @@ async function resolveAdapter(projectRoot, specified) {
791
860
  return detected;
792
861
  }
793
862
  console.log("\u672A\u68C0\u6D4B\u5230 IDE \u73AF\u5883");
794
- const a = (await ask("\u8BF7\u9009\u62E9\u76EE\u6807 IDE: [1] Qoder [2] Claude [3] VS Code \u2192 ")).trim();
863
+ const a = (await ask("\u8BF7\u9009\u62E9\u76EE\u6807 IDE: [1] Qoder [2] Claude [3] VS Code [4] Trae [5] Codex \u2192 ")).trim();
795
864
  if (a === "1" || a === "qoder") return "qoder";
796
865
  if (a === "2" || a === "claude") return "claude";
797
866
  if (a === "3" || a === "vscode") return "vscode";
867
+ if (a === "4" || a === "trae") return "trae";
868
+ if (a === "5" || a === "codex") return "codex";
798
869
  console.log("\u8F93\u5165\u65E0\u6CD5\u8BC6\u522B\uFF0C\u9ED8\u8BA4 qoder");
799
870
  return "qoder";
800
871
  }
@@ -938,8 +1009,8 @@ main().catch((e) => { console.error(e); process.exit(1); });
938
1009
  console.log(`[dry-run] \u5C06\u5199\u5165 ${scriptPath}`);
939
1010
  return;
940
1011
  }
941
- if (!existsSync8(scriptsDir)) mkdirSync3(scriptsDir, { recursive: true });
942
- writeFileSync3(scriptPath, content, "utf-8");
1012
+ if (!existsSync8(scriptsDir)) mkdirSync4(scriptsDir, { recursive: true });
1013
+ writeFileSync4(scriptPath, content, "utf-8");
943
1014
  console.log("\u5DF2\u751F\u6210 scripts/export-db.ts");
944
1015
  }
945
1016
  function injectDbExportScript(projectRoot, dryRun) {
@@ -953,10 +1024,26 @@ function injectDbExportScript(projectRoot, dryRun) {
953
1024
  if (!pkg.scripts) pkg.scripts = {};
954
1025
  if (!pkg.scripts["db:export"]) {
955
1026
  pkg.scripts["db:export"] = "npx tsx scripts/export-db.ts";
956
- writeFileSync3(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1027
+ writeFileSync4(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
957
1028
  console.log("\u5DF2\u5728 package.json \u6CE8\u5165 db:export");
958
1029
  }
959
1030
  }
1031
+ async function resolveStack(options) {
1032
+ if (options.stack) {
1033
+ console.log(`\u6280\u672F\u6808\u7EA6\u675F: ${options.stack} (--stack)`);
1034
+ return options.stack;
1035
+ }
1036
+ if (options.force) return "";
1037
+ console.log(["", "\u6280\u672F\u6808\u7EA6\u675F\uFF08\u53EF\u9009\uFF09:", " [1] \u4E0D\u8BBE\u7F6E\uFF08\u4E2D\u6027\uFF0C\u63A8\u8350\uFF09", " [2] webapp", " [3] machineserver", " [4] \u81EA\u5B9A\u4E49\uFF08\u8F93\u5165 profile \u540D\uFF09"].join("\n"));
1038
+ const a = (await ask("\u8BF7\u9009\u62E9 [1/2/3/4] \u2192 ")).trim();
1039
+ if (a === "2" || a === "webapp") return "webapp";
1040
+ if (a === "3" || a === "machineserver") return "machineserver";
1041
+ if (a === "4" || a === "custom") {
1042
+ const name = (await ask("\u81EA\u5B9A\u4E49 profile \u540D\uFF08\u9700\u5DF2\u5728 .qoder/rules/profiles/ \u5B58\u5728\uFF09: ")).trim();
1043
+ return name || "";
1044
+ }
1045
+ return "";
1046
+ }
960
1047
  async function prepare(options) {
961
1048
  const projectRoot = process.cwd();
962
1049
  const target = await resolveAdapter(projectRoot, options.adapter);
@@ -964,6 +1051,8 @@ async function prepare(options) {
964
1051
  const config = await loadConfig(projectRoot, options.config, { force: options.force });
965
1052
  config.projectRoot = projectRoot;
966
1053
  config.magicDir = magicDir;
1054
+ const stack = await resolveStack(options);
1055
+ config.stack = stack;
967
1056
  const db = await resolveDbEngine(!!options.force);
968
1057
  if (db.engine === "postgresql") {
969
1058
  db.container = await resolveContainer(!!options.force);
@@ -971,7 +1060,7 @@ async function prepare(options) {
971
1060
  Object.assign(db, await resolveDbCredentials(!!options.force));
972
1061
  }
973
1062
  }
974
- return { projectRoot, options, target, magicDir, config, db };
1063
+ return { projectRoot, options, target, magicDir, config, db, stack };
975
1064
  }
976
1065
  function writeComposeEnv(ctx) {
977
1066
  const { projectRoot, options, config, db } = ctx;
@@ -980,7 +1069,7 @@ function writeComposeEnv(ctx) {
980
1069
  const composeName = db.container === "podman" ? "podman-compose.add.yml" : "docker-compose.add.yml";
981
1070
  const composePath = resolve5(projectRoot, composeName);
982
1071
  if (!options.dryRun && (!existsSync8(composePath) || options.force)) {
983
- writeFileSync3(composePath, composeContent(config.projectName || "add-project"), "utf-8");
1072
+ writeFileSync4(composePath, composeContent(config.projectName || "add-project"), "utf-8");
984
1073
  console.log(`\u5DF2\u521B\u5EFA ${composeName}`);
985
1074
  }
986
1075
  }
@@ -988,7 +1077,7 @@ function writeComposeEnv(ctx) {
988
1077
  if (!options.dryRun && existsSync8(devEnvPath)) {
989
1078
  const existing = readFileSync6(devEnvPath, "utf-8");
990
1079
  if (!/^DATABASE_USER=/m.test(existing)) {
991
- writeFileSync3(devEnvPath, existing + `
1080
+ writeFileSync4(devEnvPath, existing + `
992
1081
  DATABASE_USER=${db.user || "admin"}
993
1082
  DATABASE_PASSWORD=${db.password || "change-me-in-production"}
994
1083
  DATABASE_PORT=${db.port || "5433"}
@@ -1030,7 +1119,7 @@ async function renderAndWrite(ctx) {
1030
1119
  if (dry) {
1031
1120
  console.log(`[dry-run] \u5C06\u521B\u5EFA ${reviewsDir}/`);
1032
1121
  } else {
1033
- mkdirSync3(reviewsDir, { recursive: true });
1122
+ mkdirSync4(reviewsDir, { recursive: true });
1034
1123
  }
1035
1124
  }
1036
1125
  }
@@ -1045,9 +1134,9 @@ async function renderAndWrite(ctx) {
1045
1134
  hashMap[rp] = createHash("sha256").update(c).digest("hex").slice(0, 8);
1046
1135
  }
1047
1136
  const hashOut = resolve5(projectRoot, magicDir, ".add-coder-hash.json");
1048
- writeFileSync3(hashOut, JSON.stringify(hashMap, null, 2) + "\n", "utf-8");
1137
+ writeFileSync4(hashOut, JSON.stringify(hashMap, null, 2) + "\n", "utf-8");
1049
1138
  if (npmVer) {
1050
- writeFileSync3(resolve5(projectRoot, magicDir, ".add-coder-version"), npmVer + "\n", "utf-8");
1139
+ writeFileSync4(resolve5(projectRoot, magicDir, ".add-coder-version"), npmVer + "\n", "utf-8");
1051
1140
  }
1052
1141
  console.log(`hash: ${Object.keys(hashMap).length} entries \u2192 ${magicDir}/.add-coder-hash.json`);
1053
1142
  }
@@ -1111,7 +1200,7 @@ function deployDocs(ctx) {
1111
1200
  for (const d of ["00-\u9700\u6C42", "01-\u67B6\u6784", "02-\u89C4\u8303"]) {
1112
1201
  const srcDir = resolve5(groundingSrc, d);
1113
1202
  const destDir = resolve5(docsBase, d);
1114
- if (!existsSync8(destDir)) mkdirSync3(destDir, { recursive: true });
1203
+ if (!existsSync8(destDir)) mkdirSync4(destDir, { recursive: true });
1115
1204
  if (!existsSync8(srcDir)) continue;
1116
1205
  for (const f of readdirSync2(srcDir)) {
1117
1206
  const src = resolve5(srcDir, f);
@@ -1144,13 +1233,13 @@ function finalize(ctx, result) {
1144
1233
  }
1145
1234
 
1146
1235
  // src/cli/commands/sync.ts
1147
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
1236
+ import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
1148
1237
  import { resolve as resolve6, dirname as dirname4 } from "path";
1149
1238
  import { createHash as createHash2 } from "crypto";
1150
1239
 
1151
1240
  // src/caijuehub/strategies/sync.strategy.ts
1152
1241
  var SYNC_CONFIG = {
1153
- PATCH_GUARD: [/[/]plans[/]/, /[/]specs[/]/, /[/]reviews[/]/],
1242
+ PATCH_GUARD: [/[/]plans[/]/, /[/]specs[/]/, /[/]reviews[/]/, /[/]rules[/]profiles[/]/],
1154
1243
  HASH_OUTPUT_FILE: ".add-coder-hash.json",
1155
1244
  HASH_SRC_FILE: "templates/.add-coder-src-hash.json",
1156
1245
  HASH_HEX_LENGTH: 8,
@@ -1211,12 +1300,12 @@ function loadVersionFile(root, magic) {
1211
1300
  }
1212
1301
  }
1213
1302
  function saveVersionFile(root, magic, version2) {
1214
- writeFileSync4(resolve6(root, magic, SYNC_CONFIG.VERSION_SENTINEL), version2 + "\n", "utf-8");
1303
+ writeFileSync5(resolve6(root, magic, SYNC_CONFIG.VERSION_SENTINEL), version2 + "\n", "utf-8");
1215
1304
  }
1216
1305
  function saveHashFile(root, magic, files) {
1217
1306
  const m = {};
1218
1307
  for (const [p, c] of files) m[p] = hash8(c);
1219
- writeFileSync4(resolve6(root, magic, SYNC_CONFIG.HASH_OUTPUT_FILE), JSON.stringify(m, null, 2) + "\n", "utf-8");
1308
+ writeFileSync5(resolve6(root, magic, SYNC_CONFIG.HASH_OUTPUT_FILE), JSON.stringify(m, null, 2) + "\n", "utf-8");
1220
1309
  }
1221
1310
  async function syncCommand(options = {}) {
1222
1311
  const projectRoot = process.cwd();
@@ -1497,8 +1586,8 @@ async function checkPrismaDiff(projectRoot, options) {
1497
1586
  } else {
1498
1587
  console.log(`
1499
1588
  \u76EE\u6807 schema \u6587\u4EF6\u4E0D\u5B58\u5728: ${result.targetPath}`);
1500
- mkdirSync4(dirname4(targetPath), { recursive: true });
1501
- writeFileSync4(targetPath, "// add.prisma \u2014 ADD \u6CBB\u7406\u6A21\u578B\n\n", "utf-8");
1589
+ mkdirSync5(dirname4(targetPath), { recursive: true });
1590
+ writeFileSync5(targetPath, "// add.prisma \u2014 ADD \u6CBB\u7406\u6A21\u578B\n\n", "utf-8");
1502
1591
  const n = injectMissingModels(targetPath, result.missing);
1503
1592
  console.log(` \u2705 \u5DF2\u521B\u5EFA\u5E76\u6CE8\u5165 ${n} \u4E2A\u6A21\u578B/\u679A\u4E3E`);
1504
1593
  if (n > 0) {
@@ -1538,7 +1627,7 @@ function injectMissingModels(targetPath, models) {
1538
1627
  for (const m of models) {
1539
1628
  content += m.body + "\n\n";
1540
1629
  }
1541
- writeFileSync4(targetPath, content, "utf-8");
1630
+ writeFileSync5(targetPath, content, "utf-8");
1542
1631
  return models.length;
1543
1632
  }
1544
1633
  function getBaseFieldLines(basePath, modelName) {
@@ -1573,7 +1662,7 @@ function injectFieldLines(targetPath, basePath, modelName, fieldKeys) {
1573
1662
  ${fieldLine}$3`);
1574
1663
  count++;
1575
1664
  }
1576
- if (count > 0) writeFileSync4(targetPath, content, "utf-8");
1665
+ if (count > 0) writeFileSync5(targetPath, content, "utf-8");
1577
1666
  return count;
1578
1667
  }
1579
1668
  function overwriteFieldLines(targetPath, basePath, modelName, conflicts) {
@@ -1590,7 +1679,7 @@ function overwriteFieldLines(targetPath, basePath, modelName, conflicts) {
1590
1679
  content = content.replace(fieldRegex, `${match[1]}${baseLine}`);
1591
1680
  count++;
1592
1681
  }
1593
- if (count > 0) writeFileSync4(targetPath, content, "utf-8");
1682
+ if (count > 0) writeFileSync5(targetPath, content, "utf-8");
1594
1683
  return count;
1595
1684
  }
1596
1685
 
@@ -1621,13 +1710,156 @@ async function statusCommand() {
1621
1710
  }
1622
1711
  }
1623
1712
 
1713
+ // src/cli/commands/stack.ts
1714
+ import { readdirSync as readdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
1715
+ import { resolve as resolve8, join as join5 } from "path";
1716
+ import { createHash as createHash3 } from "crypto";
1717
+ var MAGIC_DIR_MAP3 = { claude: ".claude", qoder: ".qoder", vscode: ".vscode", trae: ".trae", codex: ".codex" };
1718
+ var HASH_OUTPUT_FILE = ".add-coder-hash.json";
1719
+ function hash82(c) {
1720
+ return createHash3("sha256").update(c).digest("hex").slice(0, 8);
1721
+ }
1722
+ function resolveMagicDir(projectRoot, specified) {
1723
+ if (specified) {
1724
+ if (!MAGIC_DIR_MAP3[specified]) throw new Error(`\u672A\u77E5 adapter: ${specified}`);
1725
+ return MAGIC_DIR_MAP3[specified];
1726
+ }
1727
+ const detected = detectIDE2(projectRoot);
1728
+ const adapter = detected !== "auto" ? detected : "qoder";
1729
+ return MAGIC_DIR_MAP3[adapter];
1730
+ }
1731
+ function listCustomProfiles(projectRoot, magicDir, registryNames) {
1732
+ const dir = resolve8(projectRoot, magicDir, "rules", "profiles");
1733
+ if (!existsSync11(dir)) return [];
1734
+ return readdirSync3(dir).filter((f) => f.endsWith(".md") && !registryNames.has(f.replace(/-profile\.md$/, ""))).sort();
1735
+ }
1736
+ function profileExists(projectRoot, magicDir, name) {
1737
+ const registry = loadProfileRegistry();
1738
+ if (registry.some((p) => p.name === name)) return true;
1739
+ return existsSync11(resolve8(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`));
1740
+ }
1741
+ function stackCommand(sub, name, options = {}) {
1742
+ const projectRoot = process.cwd();
1743
+ const magicDir = resolveMagicDir(projectRoot, options.adapter);
1744
+ const registry = loadProfileRegistry();
1745
+ if (options.clear) {
1746
+ saveStack(projectRoot, magicDir, "");
1747
+ console.log(`\u5DF2\u6E05\u9664\u6280\u672F\u6808\u8BBE\u7F6E\uFF08${magicDir}/stack.json \u2192 \u4E2D\u6027\uFF09`);
1748
+ return;
1749
+ }
1750
+ if (sub === "list" || !sub) {
1751
+ const current = loadStack(projectRoot, magicDir);
1752
+ console.log("\u6280\u672F\u6808 profile \u5217\u8868:");
1753
+ console.log(" \u5185\u7F6E\uFF08\u6CE8\u518C\u8868\uFF09:");
1754
+ for (const p of registry) {
1755
+ const mark = p.name === current ? " \u2190 \u5F53\u524D" : "";
1756
+ console.log(` ${p.name} \u2014 ${p.description}${mark}`);
1757
+ }
1758
+ const custom = listCustomProfiles(projectRoot, magicDir, new Set(registry.map((p) => p.name)));
1759
+ if (custom.length > 0) {
1760
+ console.log(" \u81EA\u5B9A\u4E49\uFF08\u9879\u76EE\u4FA7\uFF09:");
1761
+ for (const c of custom) {
1762
+ const name2 = c.replace(/-profile\.md$/, "");
1763
+ const mark = name2 === current ? " \u2190 \u5F53\u524D" : "";
1764
+ console.log(` ${name2}${mark}`);
1765
+ }
1766
+ }
1767
+ if (!current) console.log("\n\u5F53\u524D: \u672A\u8BBE\u7F6E\uFF08\u4E0D\u65BD\u52A0\u4EFB\u4F55\u6280\u672F\u6808\u5047\u8BBE\uFF09");
1768
+ return;
1769
+ }
1770
+ if (sub === "show") {
1771
+ const current = loadStack(projectRoot, magicDir);
1772
+ if (!current) {
1773
+ console.log("\u6280\u672F\u6808: \u672A\u8BBE\u7F6E\uFF08\u4E2D\u6027\uFF0C\u65E0\u6280\u672F\u6808\u5047\u8BBE\uFF09");
1774
+ return;
1775
+ }
1776
+ const profilePath = resolve8(projectRoot, magicDir, "rules", "profiles", `${current}-profile.md`);
1777
+ const stat = existsSync11(profilePath) ? readFileSync8(profilePath, "utf-8").length : 0;
1778
+ console.log(`\u6280\u672F\u6808: ${current}`);
1779
+ console.log(`profile \u6587\u4EF6: ${profilePath}${existsSync11(profilePath) ? ` (${stat} \u5B57\u7B26)` : "\uFF08\u7F3A\u5931\uFF09"}`);
1780
+ try {
1781
+ const raw = JSON.parse(readFileSync8(resolve8(projectRoot, magicDir, "stack.json"), "utf-8"));
1782
+ if (raw.updatedAt) console.log(`\u66F4\u65B0\u65F6\u95F4: ${raw.updatedAt}`);
1783
+ } catch {
1784
+ }
1785
+ return;
1786
+ }
1787
+ if (sub === "set") {
1788
+ if (!name) {
1789
+ console.log("\u7528\u6CD5: add-coder stack set <name>\uFF08\u5185\u7F6E: " + registry.map((p) => p.name).join(" | ") + "\uFF0C\u6216\u81EA\u5B9A\u4E49 profile \u540D\uFF09");
1790
+ return;
1791
+ }
1792
+ applyStack(projectRoot, magicDir, name);
1793
+ return;
1794
+ }
1795
+ console.log(`\u672A\u77E5\u5B50\u547D\u4EE4: ${sub}\uFF08\u53EF\u7528: list / set <name> / show / --clear\uFF09`);
1796
+ }
1797
+ function buildConfig(projectRoot, magicDir, stack) {
1798
+ const config = {
1799
+ ...defaults,
1800
+ projectName: "add-project",
1801
+ projectRoot,
1802
+ magicDir,
1803
+ stack
1804
+ };
1805
+ try {
1806
+ const pkgPath = resolve8(projectRoot, "package.json");
1807
+ if (existsSync11(pkgPath)) {
1808
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
1809
+ if (pkg.name) config.projectName = pkg.name;
1810
+ }
1811
+ } catch {
1812
+ }
1813
+ return config;
1814
+ }
1815
+ function applyStack(projectRoot, magicDir, name) {
1816
+ if (!profileExists(projectRoot, magicDir, name)) {
1817
+ const registry = loadProfileRegistry();
1818
+ console.error(`\u2717 profile \u4E0D\u5B58\u5728: ${name}`);
1819
+ console.error(` \u5185\u7F6E: ${registry.map((p) => p.name).join(" | ")}\uFF08\u6216\u81EA\u5B9A\u4E49: ${magicDir}/rules/profiles/{name}-profile.md\uFF09`);
1820
+ process.exit(1);
1821
+ }
1822
+ const config = buildConfig(projectRoot, magicDir, name);
1823
+ saveStack(projectRoot, magicDir, name);
1824
+ const coreFiles = renderCore(config, false);
1825
+ const stackRelated = /* @__PURE__ */ new Map();
1826
+ for (const [relPath, content] of coreFiles) {
1827
+ if (relPath.includes("/rules/profiles/") || relPath.endsWith("/rules/project_rules.md")) {
1828
+ stackRelated.set(relPath, content);
1829
+ }
1830
+ }
1831
+ const hashMap = {};
1832
+ try {
1833
+ Object.assign(hashMap, JSON.parse(readFileSync8(resolve8(projectRoot, magicDir, HASH_OUTPUT_FILE), "utf-8")));
1834
+ } catch {
1835
+ }
1836
+ let written = 0;
1837
+ for (const [relPath, content] of stackRelated) {
1838
+ for (const t of [".add", magicDir]) {
1839
+ const targetPath = resolve8(projectRoot, relPath.replace(/^\.add/, t));
1840
+ mkdirSync6(join5(targetPath, ".."), { recursive: true });
1841
+ writeFileSync6(targetPath, content, "utf-8");
1842
+ hashMap[relPath.replace(/^\.add/, t)] = hash82(content);
1843
+ written++;
1844
+ }
1845
+ }
1846
+ writeFileSync6(resolve8(projectRoot, magicDir, HASH_OUTPUT_FILE), JSON.stringify(hashMap, null, 2) + "\n", "utf-8");
1847
+ console.log(`\u2705 \u6280\u672F\u6808\u5DF2\u8BBE\u7F6E\u4E3A ${name}`);
1848
+ console.log(` ${magicDir}/stack.json \u2192 ${name}`);
1849
+ console.log(` ${magicDir}/rules/profiles/${name}-profile.md ${existsSync11(resolve8(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`)) ? "\u5DF2\u5C31\u4F4D" : "\uFF08\u81EA\u5B9A\u4E49 profile\uFF0C\u9879\u76EE\u4FA7\u6587\u4EF6\uFF09"}`);
1850
+ console.log(` project_rules.md \u5F15\u7528\u884C\u5DF2\u66F4\u65B0 + hash \u5DF2\u5237\u65B0\uFF08${written} \u4E2A\u6587\u4EF6\uFF09`);
1851
+ }
1852
+
1624
1853
  // src/cli/index.ts
1625
1854
  var { version } = JSON.parse(
1626
- readFileSync8(new URL("../package.json", import.meta.url), "utf-8")
1855
+ readFileSync9(new URL("../package.json", import.meta.url), "utf-8")
1627
1856
  );
1628
1857
  var program = new Command();
1629
1858
  program.name("add-coder").description("\u521D\u59CB\u5316 ADD \u8303\u5F0F\u5DE5\u4F5C\u6D41\u6A21\u677F").version(version);
1630
- 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);
1631
- program.command("sync").description("\u589E\u91CF\u540C\u6B65\u7F3A\u5931\u6587\u4EF6\uFF08--patch \u8986\u76D6\u5DF2\u6709\u6A21\u677F\uFF09").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode").option("--patch", "\u8986\u76D6\u5DF2\u6709\u6A21\u677F\u6587\u4EF6\uFF08\u4E0D\u78B0 plans/specs/reviews\uFF09").option("-i, --interactive", "\u4EA4\u4E92\u5F0F\u9009\u62E9\u8981\u540C\u6B65\u7684\u6587\u4EF6").action(syncCommand);
1859
+ program.command("init").description("\u521D\u59CB\u5316 ADD \u6A21\u677F\u5230\u5F53\u524D\u9879\u76EE").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode | trae | codex").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").option("--stack <name>", "\u6280\u672F\u6808\u7EA6\u675F profile \u540D\uFF08\u5982 machineserver\uFF0C\u53EF\u9009\uFF09").action(initCommand);
1860
+ program.command("sync").description("\u589E\u91CF\u540C\u6B65\u7F3A\u5931\u6587\u4EF6\uFF08--patch \u8986\u76D6\u5DF2\u6709\u6A21\u677F\uFF09").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode | trae | codex").option("--patch", "\u8986\u76D6\u5DF2\u6709\u6A21\u677F\u6587\u4EF6\uFF08\u4E0D\u78B0 plans/specs/reviews\uFF09").option("-i, --interactive", "\u4EA4\u4E92\u5F0F\u9009\u62E9\u8981\u540C\u6B65\u7684\u6587\u4EF6").action(syncCommand);
1632
1861
  program.command("status").description("\u68C0\u67E5 ADD \u6A21\u677F\u5B8C\u6574\u6027").action(statusCommand);
1862
+ program.command("stack").description("\u7BA1\u7406\u6280\u672F\u6808\u7EA6\u675F profile\uFF08list / set <name> / show / --clear\uFF09").argument("[sub]", "list | set | show").argument("[name]", "set <name>: profile \u540D\uFF08\u5185\u7F6E\u6216\u81EA\u5B9A\u4E49\uFF09").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode | trae | codex").option("--clear", "\u6E05\u9664\u6280\u672F\u6808\u8BBE\u7F6E\uFF08\u4E2D\u6027\uFF09").action(
1863
+ (sub, name, options) => stackCommand(sub, name, options)
1864
+ );
1633
1865
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "add-coder",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "private": false,
5
5
  "description": "[codein2027](https://github.com/xiaomingming92/codein2027) - A complete scaffolding for building the ADD programming paradigm, the implementation layer for AI code governance. Core principle: Audit as Infrastructure. Breaks the black-box programming process and cross-session amnesia, evolving programming paradigms into an auditable, traceable, and convergent new era. npx-ready.\n\n[codein2027](https://github.com/xiaomingming92/codein2027) 快速构建 ADD 编程范式的完整脚手架——AI 代码治理的落地方案。以「审计即基础设施」为核心,彻底打破编程过程黑盒与跨轮失忆,让编程范式进化为可审计、可追溯、可收敛的新时代。npx 即用,人人可体验。",
6
6
  "repository": {