add-coder 0.1.8 → 0.1.10

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 编程范式的完整脚手架。以「审计即基础设施」为核心,彻底打破编程过程黑盒与跨轮失忆,让编程范式进化为可审计、可追溯、可收敛的新时代。[本项目NPM包地址](https://www.npmjs.com/package/add-coder)
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;
@@ -199,20 +210,21 @@ var PLACEHOLDERS = {
199
210
  function render(content, config) {
200
211
  let result = content;
201
212
  for (const [placeholder, key] of Object.entries(PLACEHOLDERS)) {
202
- result = result.replaceAll(placeholder, config[key]);
213
+ result = result.replaceAll(placeholder, String(config[key]));
203
214
  }
204
215
  return result;
205
216
  }
206
217
  var TEMPLATES_ROOT = join4(__dirname, "../templates");
207
218
  var CORE_DIR = join4(TEMPLATES_ROOT, "core");
208
219
  var CORE_TARGET = ".add";
220
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["prisma"]);
209
221
  function renderCore(config, dryRun) {
210
222
  const files = /* @__PURE__ */ new Map();
211
223
  function walk(dir, base) {
212
224
  for (const name of readdirSync(dir)) {
213
225
  const full = join4(dir, name);
214
226
  if (statSync(full).isDirectory()) {
215
- walk(full, join4(base, name));
227
+ if (!SKIP_DIRS.has(name)) walk(full, join4(base, name));
216
228
  } else {
217
229
  const content = readFileSync2(full, "utf-8");
218
230
  const rendered = render(content, config);
@@ -322,13 +334,13 @@ function renderAdapter3(config, targetDir, dryRun) {
322
334
  }
323
335
 
324
336
  // src/cli/prisma-injector.ts
325
- import { resolve as resolve2, dirname as dirname6 } from "path";
337
+ import { resolve as resolve3, dirname as dirname6 } from "path";
326
338
  import { fileURLToPath as fileURLToPath5 } from "url";
327
339
 
328
340
  // src/caijuehub/strategies/prisma.strategy.ts
329
341
  import { spawnSync } from "child_process";
330
- import { copyFileSync, existsSync as existsSync4, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
331
- 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";
332
344
  var PRISMA_CONFIG = {
333
345
  onMissing: "ask",
334
346
  onExistingAddPrisma: "ask",
@@ -336,70 +348,118 @@ var PRISMA_CONFIG = {
336
348
  autoGenerate: true,
337
349
  migrationName: "add_workflow_init",
338
350
  schemaArg: "--schema=prisma/",
339
- requiresUserModel: true
351
+ requiresUserModel: false
340
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
+ }
341
437
  async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
342
438
  const C = PRISMA_CONFIG;
343
- const prismaDir = resolve(projectRoot, "prisma");
344
- const schemaPath = resolve(prismaDir, "schema.prisma");
345
- const destPath = resolve(prismaDir, "add.prisma");
346
- 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)) {
347
444
  if (C.onMissing === "skip") {
348
445
  console.log("\u8DF3\u8FC7\uFF1A\u7F3A\u5C11 Prisma");
349
446
  return true;
350
447
  }
351
- if (C.onMissing === "ask" && !options.force && !options.yes) {
448
+ const shouldInit = options.force || options.yes;
449
+ if (!shouldInit && C.onMissing === "ask") {
352
450
  const a = await ask("\u9879\u76EE\u7F3A\u5C11 Prisma\uFF0C\u662F\u5426\u6267\u884C prisma init\uFF1F[Y/n] ");
353
- if (a !== "n" && a !== "no") {
354
- console.log("\u6267\u884C npx prisma init ...");
355
- spawnSync("npx", ["prisma", "init", "--datasource-provider", "postgresql"], { cwd: projectRoot, stdio: "inherit", shell: false });
356
- const envPath = resolve(projectRoot, ".env");
357
- const devEnvPath = resolve(projectRoot, ".env.development");
358
- if (existsSync4(envPath)) {
359
- const envContent = readFileSync6(envPath, "utf-8");
360
- const dbUrl = envContent.match(/DATABASE_URL=.*/);
361
- if (dbUrl) {
362
- const existing = existsSync4(devEnvPath) ? readFileSync6(devEnvPath, "utf-8") : "";
363
- if (!existing.includes("DATABASE_URL=")) {
364
- writeFileSync2(devEnvPath, `${existing}${existing ? "\n" : ""}${dbUrl[0]}
365
- `, "utf-8");
366
- }
367
- if (existsSync4(envPath)) unlinkSync(envPath);
368
- console.log("\u5DF2\u5C06 DATABASE_URL \u8FC1\u79FB\u5230 .env.development");
369
- }
370
- }
371
- 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");
372
- console.log(" \u53EF\u9009\u6587\u4EF6\u4F18\u5148\u7EA7: .env.development.local > .env.development > .env.local > .env");
373
- const schemaContent = readFileSync6(schemaPath, "utf-8");
374
- if (!/model\s+User\s*\{/.test(schemaContent)) {
375
- writeFileSync2(schemaPath, "model User {\n id String @id @default(cuid())\n}\n\n" + schemaContent, "utf-8");
376
- console.log("\u5DF2\u6CE8\u5165 User \u6A21\u578B");
377
- }
378
- copyFileSync(addPrismaTemplate, destPath);
379
- console.log("\u5DF2\u590D\u5236 add.prisma");
380
- 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");
381
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");
382
456
  }
383
- 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;
384
461
  }
385
- if (C.requiresUserModel && !/model\s+User\s*\{/.test(readFileSync6(schemaPath, "utf-8"))) {
386
- const userModel = "model User {\n id String @id @default(cuid())\n}\n";
387
- if (options.yes || options.force) {
388
- const schema = readFileSync6(schemaPath, "utf-8");
389
- writeFileSync2(schemaPath, userModel + "\n" + schema, "utf-8");
390
- console.log("\u5DF2\u6CE8\u5165 User \u6A21\u578B\u5230 schema.prisma");
391
- } else {
392
- const a = await ask("schema.prisma \u7F3A\u5C11 User \u6A21\u578B\uFF0C\u662F\u5426\u81EA\u52A8\u6CE8\u5165\uFF1F[Y/n] ");
393
- if (a !== "n" && a !== "no") {
394
- const schema = readFileSync6(schemaPath, "utf-8");
395
- writeFileSync2(schemaPath, userModel + "\n" + schema, "utf-8");
396
- console.log("\u5DF2\u6CE8\u5165 User \u6A21\u578B");
397
- } else {
398
- throw new Error("\u9700\u8981 User \u6A21\u578B\uFF08id: String\uFF09\uFF0C\u8BF7\u5728 prisma/schema.prisma \u4E2D\u521B\u5EFA\u540E\u91CD\u8BD5\u3002");
399
- }
400
- }
401
- }
402
- if (existsSync4(destPath)) {
462
+ if (existsSync5(destPath) && !justInited) {
403
463
  if (options.dryRun) {
404
464
  console.log("[dry-run] \u5DF2\u6709 add.prisma");
405
465
  return true;
@@ -429,17 +489,19 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
429
489
  }
430
490
  }
431
491
  if (options.dryRun) {
432
- console.log("[dry-run] \u5C06\u6267\u884C prisma migrate");
492
+ console.log("[dry-run] \u5C06\u6267\u884C prisma db push");
433
493
  return true;
434
494
  }
435
- copyFileSync(addPrismaTemplate, destPath);
436
- console.log("\u5DF2\u590D\u5236 add.prisma");
495
+ if (!justInited) copyFileSync(addPrismaTemplate, destPath);
437
496
  try {
438
- 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"];
439
501
  if (C.schemaArg) args.push(C.schemaArg);
440
- console.log(`\u6267\u884C npx ${args.join(" ")} ...`);
441
- const r = spawnSync("npx", args, { cwd: projectRoot, stdio: "inherit", shell: false });
442
- 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}`);
443
505
  } catch (err) {
444
506
  if (C.onMigrateFail === "keep") {
445
507
  console.log("\u8FC1\u79FB\u5931\u8D25\uFF0C\u4FDD\u7559\u6587\u4EF6");
@@ -453,8 +515,9 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
453
515
  throw new Error(`\u8FC1\u79FB\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
454
516
  }
455
517
  if (C.autoGenerate) {
518
+ const pm = detectPm(projectRoot);
456
519
  console.log("\u6267\u884C prisma generate ...");
457
- 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 });
458
521
  }
459
522
  console.log("ADD \u6CBB\u7406\u6A21\u578B\u5DF2\u5C31\u7EEA");
460
523
  return true;
@@ -463,69 +526,314 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
463
526
  // src/cli/prisma-injector.ts
464
527
  var __filename5 = fileURLToPath5(import.meta.url);
465
528
  var __dirname5 = dirname6(__filename5);
466
- var ADD_PRISMA_TEMPLATE = resolve2(__dirname5, "../templates/core/prisma/add.prisma");
529
+ var ADD_PRISMA_TEMPLATE = resolve3(__dirname5, "../templates/core/prisma/add.prisma");
467
530
  async function injectPrisma2(projectRoot, options = {}) {
468
531
  return injectPrisma(projectRoot, ADD_PRISMA_TEMPLATE, options);
469
532
  }
470
533
 
471
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";
472
539
  var ADAPTER_RENDERERS = {
473
540
  claude: renderAdapter,
474
541
  qoder: renderAdapter2,
475
542
  vscode: renderAdapter3
476
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
+ }
477
723
  async function initCommand(options) {
478
724
  const projectRoot = process.cwd();
479
- const specified = options.adapter && options.adapter !== "auto" ? options.adapter : null;
480
- const target = specified || detectIDE2(projectRoot);
481
- console.log(`\u68C0\u6D4B\u5230 IDE: ${target}${specified ? " (--adapter)" : " (\u81EA\u52A8)"}`);
482
- 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 });
483
728
  config.projectRoot = projectRoot;
484
- const MAGIC_DIR_MAP = { claude: ".claude", qoder: ".qoder", vscode: ".vscode" };
485
- const magicDir = MAGIC_DIR_MAP[target] || ".qoder";
486
- console.log(`magicDir: ${magicDir}`);
487
- const renderConfig = { ...config, magicDir };
488
- if (!options.dryRun) {
489
- 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
+ }
490
759
  }
491
- const coreFiles = renderCore(renderConfig, !!options.dryRun);
760
+ const coreFiles = renderCore(config, !!options.dryRun);
492
761
  console.log(`Core \u6A21\u677F: ${coreFiles.size} \u6587\u4EF6`);
493
- const CORE_TARGETS = [".add", ".qoder", ".claude"];
762
+ const CORE_TARGETS = [".add", ".qoder", ".claude", ".vscode"];
494
763
  const allFiles = /* @__PURE__ */ new Map();
495
764
  for (const [relPath, content] of coreFiles) {
496
- for (const target2 of CORE_TARGETS) {
497
- const targetPath = relPath.replace(/^\.add/, target2);
498
- if (!allFiles.has(targetPath)) {
499
- allFiles.set(targetPath, content);
500
- }
765
+ for (const t of CORE_TARGETS) {
766
+ const targetPath = relPath.replace(/^\.add/, t);
767
+ if (!allFiles.has(targetPath)) allFiles.set(targetPath, content);
501
768
  }
502
769
  }
503
- const adapters = resolveAdapters2(target);
504
- for (const adapter of adapters) {
770
+ for (const adapter of resolveAdapters2(target)) {
505
771
  const renderFn = ADAPTER_RENDERERS[adapter];
506
772
  if (renderFn) {
507
773
  const adapterFiles = renderFn(config, projectRoot, !!options.dryRun);
508
- for (const [path, content] of adapterFiles) {
509
- allFiles.set(path, content);
510
- }
774
+ for (const [p, c] of adapterFiles) allFiles.set(p, c);
511
775
  console.log(`${adapter} adapter: ${adapterFiles.size} \u6587\u4EF6`);
512
776
  }
513
777
  }
514
- const result = await writeFiles2(projectRoot, allFiles, {
515
- yes: options.yes,
516
- force: options.force,
517
- dryRun: options.dryRun
518
- });
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
+ }
519
816
  console.log(`
520
817
  \u5B8C\u6210: \u65B0\u5EFA ${result.created}, \u8DF3\u8FC7 ${result.skipped}, \u8986\u76D6 ${result.overwritten}`);
521
818
  if (!options.dryRun) {
522
- 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
+ }
523
831
  }
524
832
  }
525
833
 
526
834
  // src/cli/commands/sync.ts
527
- import { existsSync as existsSync5 } from "fs";
528
- import { resolve as resolve3 } from "path";
835
+ import { existsSync as existsSync7 } from "fs";
836
+ import { resolve as resolve5 } from "path";
529
837
  async function syncCommand() {
530
838
  const projectRoot = process.cwd();
531
839
  const config = await loadConfig(projectRoot);
@@ -533,7 +841,7 @@ async function syncCommand() {
533
841
  const coreFiles = renderCore(config, false);
534
842
  const missing = /* @__PURE__ */ new Map();
535
843
  for (const [relPath, content] of coreFiles) {
536
- if (!existsSync5(resolve3(projectRoot, relPath))) {
844
+ if (!existsSync7(resolve5(projectRoot, relPath))) {
537
845
  missing.set(relPath, content);
538
846
  }
539
847
  }
@@ -541,13 +849,13 @@ async function syncCommand() {
541
849
  console.log("\u6240\u6709 ADD \u6A21\u677F\u6587\u4EF6\u5DF2\u5C31\u4F4D\u3002");
542
850
  return;
543
851
  }
544
- const result = await writeFiles2(projectRoot, missing, { yes: true });
852
+ const result = await writeFiles2(projectRoot, missing, {});
545
853
  console.log(`\u540C\u6B65\u5B8C\u6210: \u65B0\u5EFA ${result.created}, \u8DF3\u8FC7 ${result.skipped}`);
546
854
  }
547
855
 
548
856
  // src/cli/commands/status.ts
549
- import { existsSync as existsSync6 } from "fs";
550
- import { resolve as resolve4 } from "path";
857
+ import { existsSync as existsSync8 } from "fs";
858
+ import { resolve as resolve6 } from "path";
551
859
  async function statusCommand() {
552
860
  const projectRoot = process.cwd();
553
861
  const config = await loadConfig(projectRoot);
@@ -556,29 +864,29 @@ async function statusCommand() {
556
864
  const missing = [];
557
865
  const present = [];
558
866
  for (const [relPath] of coreFiles) {
559
- if (existsSync6(resolve4(projectRoot, relPath))) {
867
+ if (existsSync8(resolve6(projectRoot, relPath))) {
560
868
  present.push(relPath);
561
869
  } else {
562
870
  missing.push(relPath);
563
871
  }
564
872
  }
565
- console.log(`ADD \u6A21\u677F\u5B8C\u6574\u6027\u68C0\u67E5:`);
873
+ console.log("ADD \u6A21\u677F\u5B8C\u6574\u6027\u68C0\u67E5:");
566
874
  console.log(` \u5DF2\u5C31\u4F4D: ${present.length} \u6587\u4EF6`);
567
875
  if (missing.length > 0) {
568
876
  console.log(` \u7F3A\u5931: ${missing.length} \u6587\u4EF6`);
569
877
  missing.forEach((f) => console.log(` - ${f}`));
570
878
  } else {
571
- console.log(` \u6240\u6709\u6587\u4EF6\u5B8C\u6574\u3002`);
879
+ console.log(" \u6240\u6709\u6587\u4EF6\u5B8C\u6574\u3002");
572
880
  }
573
881
  }
574
882
 
575
883
  // src/cli/index.ts
576
884
  var { version } = JSON.parse(
577
- readFileSync7(new URL("../package.json", import.meta.url), "utf-8")
885
+ readFileSync8(new URL("../package.json", import.meta.url), "utf-8")
578
886
  );
579
887
  var program = new Command();
580
888
  program.name("add-coder").description("\u521D\u59CB\u5316 ADD \u8303\u5F0F\u5DE5\u4F5C\u6D41\u6A21\u677F").version(version);
581
- 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);
582
890
  program.command("sync").description("\u589E\u91CF\u540C\u6B65\u7F3A\u5931\u6587\u4EF6").action(syncCommand);
583
891
  program.command("status").description("\u68C0\u67E5 ADD \u6A21\u677F\u5B8C\u6574\u6027").action(statusCommand);
584
892
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "add-coder",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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
@@ -26,10 +26,25 @@ import { readFile, readdir, stat, mkdir, writeFile } from "fs/promises"
26
26
  import { join, relative } from "path"
27
27
  import { existsSync } from "fs"
28
28
  import { spawnSync } from "child_process"
29
- import { PrismaClient, Prisma } from "@prisma/client"
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
 
@@ -1108,11 +1123,11 @@ server.registerTool(
1108
1123
  }
1109
1124
  }
1110
1125
 
1111
- let parsedBefore: Prisma.InputJsonValue | undefined
1112
- let parsedAfter: Prisma.InputJsonValue | undefined
1126
+ let parsedBefore: any | undefined
1127
+ let parsedAfter: any | undefined
1113
1128
  try {
1114
- if (beforeState) parsedBefore = JSON.parse(beforeState) as Prisma.InputJsonValue
1115
- if (afterState) parsedAfter = JSON.parse(afterState) as Prisma.InputJsonValue
1129
+ if (beforeState) parsedBefore = JSON.parse(beforeState)
1130
+ if (afterState) parsedAfter = JSON.parse(afterState)
1116
1131
  } catch {
1117
1132
  const beforePreview = beforeState ? beforeState.slice(0, 80) : "(未传)"
1118
1133
  const afterPreview = afterState ? afterState.slice(0, 80) : "(未传)"
@@ -1123,18 +1138,17 @@ server.registerTool(
1123
1138
  )
1124
1139
  }
1125
1140
 
1126
- let systemUser = await prisma.user.findUnique({
1141
+ let systemUser = await prisma.addUser.findUnique({
1127
1142
  where: { username: "ai-assistant" },
1128
1143
  select: { id: true },
1129
1144
  })
1130
1145
 
1131
1146
  if (!systemUser) {
1132
- systemUser = await prisma.user.create({
1147
+ systemUser = await prisma.addUser.create({
1133
1148
  data: {
1134
1149
  id: "ai-assistant",
1135
1150
  username: "ai-assistant",
1136
- email: "ai-assistant@internal",
1137
- password: "internal",
1151
+ email: "ai-assistant@internal"
1138
1152
  },
1139
1153
  select: { id: true },
1140
1154
  })
@@ -1147,8 +1161,8 @@ server.registerTool(
1147
1161
  action,
1148
1162
  targetType,
1149
1163
  targetId: targetId || "unknown",
1150
- beforeState: parsedBefore ?? Prisma.JsonNull,
1151
- afterState: parsedAfter ?? Prisma.JsonNull,
1164
+ beforeState: parsedBefore ?? null,
1165
+ afterState: parsedAfter ?? null,
1152
1166
  reason: reason || null,
1153
1167
  },
1154
1168
  })