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 +3 -1
- package/dist/index.js +425 -112
- package/package.json +32 -5
- package/templates/core/podman-compose.yml +19 -0
- package/templates/core/prisma/add.prisma +12 -5
- package/templates/core/scripts/db-ensure.sh +83 -0
- package/templates/core/scripts/mcp-server.ts +86 -30
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
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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] ${
|
|
149
|
+
console.log(`[dry-run] ${existsSync4(dest) ? "\u8986\u76D6" : "\u65B0\u5EFA"}: ${relPath}`);
|
|
144
150
|
created++;
|
|
145
151
|
continue;
|
|
146
152
|
}
|
|
147
|
-
if (
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
343
|
-
const schemaPath =
|
|
344
|
-
const destPath =
|
|
345
|
-
|
|
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
|
-
|
|
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
|
|
353
|
-
|
|
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
|
-
|
|
457
|
+
const provider = options.datasource || "postgresql";
|
|
458
|
+
runPrismaInit(projectRoot, provider, schemaPath);
|
|
459
|
+
postInitSetup(projectRoot, schemaPath, addPrismaTemplate, destPath);
|
|
460
|
+
justInited = true;
|
|
383
461
|
}
|
|
384
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
|
440
|
-
const r = spawnSync(
|
|
441
|
-
if (r.status !== 0) throw new Error(`prisma
|
|
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("
|
|
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 =
|
|
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
|
|
479
|
-
const
|
|
480
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
|
492
|
-
const targetPath = relPath.replace(/^\.add/,
|
|
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
|
|
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 [
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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("\
|
|
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
|
|
523
|
-
import { resolve as
|
|
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 (!
|
|
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, {
|
|
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
|
|
545
|
-
import { resolve as
|
|
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 (
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
"@
|
|
43
|
-
"prisma": "^
|
|
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": "
|
|
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
|
|
2
|
-
|
|
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
|
|
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
|
|
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
|
|
26
|
+
import { join, relative } from "path"
|
|
27
27
|
import { existsSync } from "fs"
|
|
28
|
-
import {
|
|
29
|
-
|
|
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
|
-
|
|
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:
|
|
1087
|
-
let parsedAfter:
|
|
1126
|
+
let parsedBefore: any | undefined
|
|
1127
|
+
let parsedAfter: any | undefined
|
|
1088
1128
|
try {
|
|
1089
|
-
if (beforeState) parsedBefore = JSON.parse(beforeState)
|
|
1090
|
-
if (afterState) parsedAfter = JSON.parse(afterState)
|
|
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.
|
|
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.
|
|
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 ??
|
|
1126
|
-
afterState: parsedAfter ??
|
|
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 {
|
|
2431
|
+
const { spawnSync } = await import("child_process")
|
|
2391
2432
|
try {
|
|
2392
|
-
const
|
|
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 {
|
|
3202
|
-
const
|
|
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 {
|
|
3244
|
-
const
|
|
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
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
parts.push(`📋 index.md 已更新: ${result.trim()}`)
|
|
3446
|
-
}
|
|
3447
|
-
parts.push(`⚠️ index.md
|
|
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 自动更新`)
|