add-coder 0.3.23 → 0.3.25-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -15
- package/dist/index.js +736 -170
- package/package.json +2 -1
- package/templates/.add-coder-src-hash.json +10 -7
- package/templates/adapters/codex/config.toml.example +20 -0
- package/templates/core/scripts/db-ensure.sh +103 -0
- package/templates/core/scripts/mcp-server/shared/redact.ts +28 -0
- package/templates/core/scripts/mcp-server/shared/response.ts +3 -2
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +69 -2
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +1 -1
- package/templates/core/scripts/mcp-server/tools/index.ts +66 -1
- package/templates/core/scripts/mcp-server.ts +2 -1
- package/templates/core/templates/ports.example.md +49 -0
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/caijuehub/strategies/detect.strategy.ts
|
|
@@ -184,7 +184,7 @@ async function selectFiles(projectRoot, files) {
|
|
|
184
184
|
process.stdout.write(renderList(items, selected, scrollIdx, VISIBLE));
|
|
185
185
|
}
|
|
186
186
|
draw();
|
|
187
|
-
return new Promise((
|
|
187
|
+
return new Promise((resolve12) => {
|
|
188
188
|
input.on("keypress", (_char, key) => {
|
|
189
189
|
if (!key) return;
|
|
190
190
|
if (key.name === "up") {
|
|
@@ -225,7 +225,7 @@ async function selectFiles(projectRoot, files) {
|
|
|
225
225
|
if (selected.has(idx)) result.set(relPath, content);
|
|
226
226
|
idx++;
|
|
227
227
|
}
|
|
228
|
-
|
|
228
|
+
resolve12(result);
|
|
229
229
|
});
|
|
230
230
|
});
|
|
231
231
|
}
|
|
@@ -623,8 +623,18 @@ function renderAdapter4(config, targetDir, dryRun, magicDir) {
|
|
|
623
623
|
}
|
|
624
624
|
|
|
625
625
|
// src/adapters/codex/renderer.ts
|
|
626
|
+
function codexCommandFor(platform) {
|
|
627
|
+
return platform === "win32" ? "cmd /c npx.cmd" : "npx";
|
|
628
|
+
}
|
|
626
629
|
function renderAdapter5(config, targetDir, dryRun, magicDir) {
|
|
627
|
-
|
|
630
|
+
const files = renderAdapterBase(config, magicDir, magicDir === ".vscode", dryRun);
|
|
631
|
+
const codexCommand = codexCommandFor(process.platform);
|
|
632
|
+
for (const [rel, content] of files) {
|
|
633
|
+
if (rel.endsWith("config.toml.example") || rel.endsWith("config.toml")) {
|
|
634
|
+
files.set(rel, content.replaceAll("{{codexCommand}}", codexCommand));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return files;
|
|
628
638
|
}
|
|
629
639
|
|
|
630
640
|
// src/lib/utils.ts
|
|
@@ -645,12 +655,12 @@ function detectPm(projectRoot) {
|
|
|
645
655
|
}
|
|
646
656
|
|
|
647
657
|
// src/cli/prisma-injector.ts
|
|
648
|
-
import { resolve as
|
|
658
|
+
import { resolve as resolve6, dirname as dirname4 } from "path";
|
|
649
659
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
650
660
|
|
|
651
661
|
// src/caijuehub/strategies/prisma.strategy.ts
|
|
652
|
-
import { copyFileSync, existsSync as
|
|
653
|
-
import { resolve as
|
|
662
|
+
import { copyFileSync, existsSync as existsSync9, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
663
|
+
import { resolve as resolve5, join as join6, dirname as dirname3 } from "path";
|
|
654
664
|
|
|
655
665
|
// src/lib/run-command.ts
|
|
656
666
|
import { spawnSync } from "child_process";
|
|
@@ -680,6 +690,205 @@ function commandExists(cmd, platform) {
|
|
|
680
690
|
return !r.error && r.status === 0;
|
|
681
691
|
}
|
|
682
692
|
|
|
693
|
+
// src/lib/db-backup.ts
|
|
694
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, readdirSync as readdirSync2, rmSync } from "fs";
|
|
695
|
+
import { join as join5, resolve as resolve3 } from "path";
|
|
696
|
+
var ADD_TABLES = ["AddUser", "DevOperation", "AuditLog", "HitlRecord", "PlanRecord", "ReviewRecord", "CollabContract"];
|
|
697
|
+
function ensureGitignoreRule(projectRoot) {
|
|
698
|
+
const gitignorePath = resolve3(projectRoot, ".gitignore");
|
|
699
|
+
if (!existsSync7(gitignorePath)) return;
|
|
700
|
+
const content = readFileSync5(gitignorePath, "utf-8");
|
|
701
|
+
if (content.includes(".add/backups/")) return;
|
|
702
|
+
writeFileSync3(gitignorePath, `${content}${content.endsWith("\n") ? "" : "\n"}
|
|
703
|
+
# add-coder \u6570\u636E\u5E93\u540C\u6B65\u5907\u4EFD
|
|
704
|
+
.add/backups/
|
|
705
|
+
`, "utf-8");
|
|
706
|
+
console.log("\u{1F4CB} \u5DF2\u6CE8\u5165\u5BBF\u4E3B .gitignore: .add/backups/");
|
|
707
|
+
}
|
|
708
|
+
async function backupBeforeSync(projectRoot, opts) {
|
|
709
|
+
const dir = resolve3(projectRoot, opts.backupDir);
|
|
710
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
711
|
+
const bakDir = join5(dir, ts);
|
|
712
|
+
ensureGitignoreRule(projectRoot);
|
|
713
|
+
if (opts.datasource === "sqlite" || opts.dbUrl.startsWith("file:")) {
|
|
714
|
+
const dbFile = opts.dbUrl.replace(/^file:/, "");
|
|
715
|
+
if (!existsSync7(dbFile)) return handleBackupFailure(opts, `sqlite \u6587\u4EF6\u4E0D\u5B58\u5728: ${dbFile}`);
|
|
716
|
+
mkdirSync3(bakDir, { recursive: true });
|
|
717
|
+
const copy = join5(bakDir, "dev.db");
|
|
718
|
+
writeFileSync3(copy, readFileSync5(dbFile), "utf-8");
|
|
719
|
+
writeFileSync3(join5(bakDir, "manifest.json"), JSON.stringify({ createdAt: ts, dbUrl: opts.dbUrl, files: ["dev.db"] }, null, 2), "utf-8");
|
|
720
|
+
console.log(`\u2705 \u5907\u4EFD\u5B8C\u6210: ${bakDir}`);
|
|
721
|
+
pruneBackups(dir, opts.backupKeep);
|
|
722
|
+
return bakDir;
|
|
723
|
+
}
|
|
724
|
+
if (!commandExists("pg_dump")) {
|
|
725
|
+
return handleBackupFailure(opts, "pg_dump \u672A\u5B89\u88C5\uFF08postgresql-client \u7F3A\u5931\uFF09");
|
|
726
|
+
}
|
|
727
|
+
mkdirSync3(bakDir, { recursive: true });
|
|
728
|
+
const schemaFile = join5(bakDir, "schema.sql");
|
|
729
|
+
const tablesFile = join5(bakDir, "add-tables.sql");
|
|
730
|
+
const r1 = runCommand("pg_dump", ["--schema-only", "--no-owner", `--dbname=${opts.dbUrl}`], { timeout: 6e4 });
|
|
731
|
+
if (r1.status !== 0) {
|
|
732
|
+
return handleBackupFailure(opts, `pg_dump schema \u5931\u8D25: ${r1.stderr.trim().split("\n").slice(0, 3).join(" | ")}`);
|
|
733
|
+
}
|
|
734
|
+
writeFileSync3(schemaFile, r1.stdout, "utf-8");
|
|
735
|
+
const r2 = runCommand("pg_dump", ["--no-owner", "--if-exists", `--dbname=${opts.dbUrl}`, ...ADD_TABLES.map((t) => `--table=${t}`)], { timeout: 6e4 });
|
|
736
|
+
if (r2.status !== 0) {
|
|
737
|
+
return handleBackupFailure(opts, `pg_dump ADD \u8868\u5931\u8D25: ${r2.stderr.trim().split("\n").slice(0, 3).join(" | ")}`);
|
|
738
|
+
}
|
|
739
|
+
writeFileSync3(tablesFile, r2.stdout, "utf-8");
|
|
740
|
+
writeFileSync3(join5(bakDir, "manifest.json"), JSON.stringify({ createdAt: ts, dbUrl: opts.dbUrl, files: ["schema.sql", "add-tables.sql"] }, null, 2), "utf-8");
|
|
741
|
+
console.log(`\u2705 \u5907\u4EFD\u5B8C\u6210: ${bakDir}`);
|
|
742
|
+
pruneBackups(dir, opts.backupKeep);
|
|
743
|
+
return bakDir;
|
|
744
|
+
}
|
|
745
|
+
async function handleBackupFailure(opts, reason) {
|
|
746
|
+
if (opts.yes) {
|
|
747
|
+
throw new Error(`\u26D4 \u5907\u4EFD\u5931\u8D25\uFF08--yes \u786C\u963B\u65AD\uFF0C\u540C\u6B65\u4E2D\u6B62\uFF09: ${reason}`);
|
|
748
|
+
}
|
|
749
|
+
const a = await ask(`
|
|
750
|
+
\u26A0\uFE0F \u5907\u4EFD\u5931\u8D25\uFF08${reason}\uFF09\u3002\u81EA\u62C5\u98CE\u9669\u7EE7\u7EED\u540C\u6B65\uFF1F[y/N] `);
|
|
751
|
+
if (a !== "y" && a !== "yes") {
|
|
752
|
+
throw new Error(`\u5907\u4EFD\u5931\u8D25\uFF0C\u5DF2\u4E2D\u6B62\u540C\u6B65: ${reason}`);
|
|
753
|
+
}
|
|
754
|
+
console.warn("\u26A0\uFE0F \u7528\u6237\u63A5\u53D7\u81EA\u62C5\u98CE\u9669\u7EE7\u7EED\uFF08manifest \u8BB0 riskAccepted\uFF09");
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
function pruneBackups(dir, backupKeep) {
|
|
758
|
+
if (!existsSync7(dir)) return;
|
|
759
|
+
const entries = readdirSync2(dir).filter((n) => /^\d{4}-\d{2}-\d{2}T/.test(n)).sort();
|
|
760
|
+
const excess = entries.length - backupKeep;
|
|
761
|
+
for (let i = 0; i < excess; i++) {
|
|
762
|
+
rmSync(join5(dir, entries[i]), { recursive: true, force: true });
|
|
763
|
+
console.log(`\u{1F9F9} \u6E05\u7406\u65E7\u5907\u4EFD: ${entries[i]}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/lib/ports-contract.ts
|
|
768
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync3 } from "fs";
|
|
769
|
+
import { resolve as resolve4, basename } from "path";
|
|
770
|
+
|
|
771
|
+
// src/caijuehub/strategies/ports.strategy.ts
|
|
772
|
+
var PORTS_CONFIG = {
|
|
773
|
+
pg: {
|
|
774
|
+
startHint: 5433,
|
|
775
|
+
scanLimit: 100
|
|
776
|
+
},
|
|
777
|
+
behavior: {
|
|
778
|
+
reuseRegistered: true,
|
|
779
|
+
readCrossProject: true,
|
|
780
|
+
onConflict: "ask"
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// src/lib/ports-contract.ts
|
|
785
|
+
import { createConnection } from "net";
|
|
786
|
+
var PORTS_EXAMPLE_REL = "../templates/core/templates/ports.example.md";
|
|
787
|
+
function ensurePortsContract(projectRoot, config, dryRun = false) {
|
|
788
|
+
const target = resolve4(projectRoot, "docs", "ports.md");
|
|
789
|
+
if (existsSync8(target)) return;
|
|
790
|
+
let content;
|
|
791
|
+
try {
|
|
792
|
+
content = readFileSync6(resolve4(import.meta.dirname, PORTS_EXAMPLE_REL), "utf-8");
|
|
793
|
+
} catch {
|
|
794
|
+
console.warn(`\u26A0\uFE0F \u7AEF\u53E3\u5951\u7EA6\u6A21\u677F\u7F3A\u5931\uFF08\u8DF3\u8FC7\u751F\u6210 docs/ports.md\uFF09: ${PORTS_EXAMPLE_REL}`);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const pn = config.projectName || "add-project";
|
|
798
|
+
const rendered = render(content, { ...config, projectName: pn });
|
|
799
|
+
if (dryRun) {
|
|
800
|
+
console.log("[dry-run] \u5C06\u751F\u6210 docs/ports.md");
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
mkdirSync4(resolve4(projectRoot, "docs"), { recursive: true });
|
|
804
|
+
writeFileSync4(target, rendered, "utf-8");
|
|
805
|
+
console.log("\u2705 \u5DF2\u751F\u6210\u7AEF\u53E3\u5951\u7EA6 docs/ports.md\uFF08\u8BF7\u6309\u9879\u76EE\u5B9E\u9645\u767B\u8BB0\u7AEF\u53E3\uFF0C\u793A\u4F8B\u72B6\u6001\u5217\u52FF\u76F4\u63A5\u63D0\u4EA4\uFF09");
|
|
806
|
+
}
|
|
807
|
+
function portInUse(port) {
|
|
808
|
+
return new Promise((r) => {
|
|
809
|
+
const s = createConnection({ port, host: "127.0.0.1" }, () => {
|
|
810
|
+
s.destroy();
|
|
811
|
+
r(true);
|
|
812
|
+
});
|
|
813
|
+
s.on("error", () => r(false));
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
function parseContractPorts(content) {
|
|
817
|
+
const out = [];
|
|
818
|
+
const re = /^\|\s*(\d{2,5})\s*\|/gm;
|
|
819
|
+
let m;
|
|
820
|
+
while ((m = re.exec(content)) !== null) out.push(parseInt(m[1], 10));
|
|
821
|
+
return out;
|
|
822
|
+
}
|
|
823
|
+
function readRegisteredPorts(projectRoot) {
|
|
824
|
+
const p = resolve4(projectRoot, "docs", "ports.md");
|
|
825
|
+
if (!existsSync8(p)) return /* @__PURE__ */ new Set();
|
|
826
|
+
return new Set(parseContractPorts(readFileSync6(p, "utf-8")));
|
|
827
|
+
}
|
|
828
|
+
function readCrossProjectPorts(projectRoot) {
|
|
829
|
+
const ports = /* @__PURE__ */ new Set();
|
|
830
|
+
const parent = resolve4(projectRoot, "..");
|
|
831
|
+
let names = [];
|
|
832
|
+
try {
|
|
833
|
+
names = readdirSync3(parent, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== basename(projectRoot)).map((d) => d.name);
|
|
834
|
+
} catch {
|
|
835
|
+
return ports;
|
|
836
|
+
}
|
|
837
|
+
for (const name of names) {
|
|
838
|
+
const p = resolve4(parent, name, "docs", "ports.md");
|
|
839
|
+
if (existsSync8(p)) {
|
|
840
|
+
parseContractPorts(readFileSync6(p, "utf-8")).forEach((x) => ports.add(x));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return ports;
|
|
844
|
+
}
|
|
845
|
+
function scanPodmanPorts() {
|
|
846
|
+
const ports = /* @__PURE__ */ new Set();
|
|
847
|
+
if (!commandExists("podman")) return ports;
|
|
848
|
+
try {
|
|
849
|
+
const r = runCommand("podman", ["ps", "--format", "{{.Ports}}"]);
|
|
850
|
+
const re = /(\d{1,5})->\d{1,5}\/tcp/g;
|
|
851
|
+
let m;
|
|
852
|
+
while ((m = re.exec(r.stdout)) !== null) ports.add(parseInt(m[1], 10));
|
|
853
|
+
} catch {
|
|
854
|
+
}
|
|
855
|
+
return ports;
|
|
856
|
+
}
|
|
857
|
+
function registerPort(projectRoot, port, svc) {
|
|
858
|
+
const contract = resolve4(projectRoot, "docs", "ports.md");
|
|
859
|
+
if (!existsSync8(contract)) return;
|
|
860
|
+
const env = svc.envKey ? ` \`${svc.envKey}\`` : "";
|
|
861
|
+
const line = `| ${port} | PostgreSQL\uFF08${svc.containerName}\uFF09 | ${svc.name}\uFF08add-coder \u81EA\u52A8\u767B\u8BB0\uFF09 | \u{1F7E2} \u4F7F\u7528\u4E2D | .env.development${env} |
|
|
862
|
+
`;
|
|
863
|
+
writeFileSync4(contract, readFileSync6(contract, "utf-8") + line, "utf-8");
|
|
864
|
+
console.log(`\u{1F4CB} \u7AEF\u53E3\u5951\u7EA6\u767B\u8BB0: ${port}\uFF08${svc.containerName}\uFF09\u2192 docs/ports.md`);
|
|
865
|
+
}
|
|
866
|
+
async function allocatePortsWithContract(projectRoot, services) {
|
|
867
|
+
const cfg = PORTS_CONFIG;
|
|
868
|
+
const used = /* @__PURE__ */ new Set();
|
|
869
|
+
readRegisteredPorts(projectRoot).forEach((p) => used.add(p));
|
|
870
|
+
if (cfg.behavior.readCrossProject) readCrossProjectPorts(projectRoot).forEach((p) => used.add(p));
|
|
871
|
+
scanPodmanPorts().forEach((p) => used.add(p));
|
|
872
|
+
const result = {};
|
|
873
|
+
for (const svc of services) {
|
|
874
|
+
let port = 0;
|
|
875
|
+
for (let p = cfg.pg.startHint; p < cfg.pg.startHint + cfg.pg.scanLimit; p++) {
|
|
876
|
+
if (used.has(p)) continue;
|
|
877
|
+
if (await portInUse(p)) {
|
|
878
|
+
used.add(p);
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
port = p;
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
if (!port) throw new Error(`\u7AEF\u53E3\u5951\u7EA6\u5206\u914D\u5931\u8D25: ${cfg.pg.startHint}-${cfg.pg.startHint + cfg.pg.scanLimit} \u65E0\u7A7A\u95F2\u7AEF\u53E3\uFF08${svc.name}\uFF09`);
|
|
885
|
+
used.add(port);
|
|
886
|
+
result[svc.name] = port;
|
|
887
|
+
registerPort(projectRoot, port, svc);
|
|
888
|
+
}
|
|
889
|
+
return result;
|
|
890
|
+
}
|
|
891
|
+
|
|
683
892
|
// src/caijuehub/strategies/prisma.strategy.ts
|
|
684
893
|
var PRISMA_CONFIG = {
|
|
685
894
|
onMissing: "ask",
|
|
@@ -688,11 +897,295 @@ var PRISMA_CONFIG = {
|
|
|
688
897
|
autoGenerate: true,
|
|
689
898
|
migrationName: "add_workflow_init",
|
|
690
899
|
schemaArg: "--schema=prisma/",
|
|
691
|
-
requiresUserModel: true
|
|
900
|
+
requiresUserModel: true,
|
|
901
|
+
sync: {
|
|
902
|
+
strategy: "atlas",
|
|
903
|
+
addDatabaseUrl: "",
|
|
904
|
+
atlasDevUrl: "",
|
|
905
|
+
backupDir: ".add/backups/prisma-sync",
|
|
906
|
+
backupKeep: 5,
|
|
907
|
+
backupRequiredForPush: true
|
|
908
|
+
}
|
|
692
909
|
};
|
|
910
|
+
var ADD_TABLES2 = ["AddUser", "DevOperation", "AuditLog", "HitlRecord", "PlanRecord", "ReviewRecord", "CollabContract"];
|
|
911
|
+
function readEnvValue(projectRoot, key) {
|
|
912
|
+
const envPath = resolve5(projectRoot, ".env.development");
|
|
913
|
+
if (!existsSync9(envPath)) return "";
|
|
914
|
+
const m = readFileSync7(envPath, "utf-8").match(new RegExp(`^${key}=(.*)$`, "m"));
|
|
915
|
+
return m?.[1]?.trim() || "";
|
|
916
|
+
}
|
|
917
|
+
function appendEnvValue(projectRoot, key, value) {
|
|
918
|
+
const envPath = resolve5(projectRoot, ".env.development");
|
|
919
|
+
const existing = existsSync9(envPath) ? readFileSync7(envPath, "utf-8") : "";
|
|
920
|
+
if (new RegExp(`^${key}=`, "m").test(existing)) return;
|
|
921
|
+
writeFileSync5(envPath, `${existing}${existing.endsWith("\n") ? "" : "\n"}${key}=${value}
|
|
922
|
+
`, "utf-8");
|
|
923
|
+
}
|
|
924
|
+
async function ensureSplitDb(projectRoot, opts) {
|
|
925
|
+
const existing = readEnvValue(projectRoot, "ADD_DATABASE_URL");
|
|
926
|
+
if (existing) {
|
|
927
|
+
console.log("\u5206\u5E93\u6A21\u5F0F\uFF1A\u68C0\u6D4B\u5230 ADD_DATABASE_URL\uFF0CADD \u6CBB\u7406\u6A21\u578B\u5165\u72EC\u7ACB\u5E93");
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
if (opts.dryRun) {
|
|
931
|
+
console.log("[dry-run] \u5C06\u8BE2\u95EE\u662F\u5426\u5206\u5E93\uFF08\u72EC\u7ACB ADD \u6570\u636E\u5E93\uFF0C\u63A8\u8350\uFF09");
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
let choice = "y";
|
|
935
|
+
if (!opts.force && !opts.yes) {
|
|
936
|
+
const a = await ask("\u662F\u5426\u5C06 ADD \u6CBB\u7406\u6A21\u578B\u653E\u5165\u72EC\u7ACB\u6570\u636E\u5E93\uFF08\u63A8\u8350\uFF0C\u9694\u79BB\u4E1A\u52A1\u5E93\uFF09\uFF1F[Y/n] ");
|
|
937
|
+
if (a === "n" || a === "no") choice = "n";
|
|
938
|
+
}
|
|
939
|
+
if (choice === "n") {
|
|
940
|
+
console.log("\u5171\u5E93\u6A21\u5F0F\uFF1AADD \u6CBB\u7406\u6A21\u578B\u8FDB\u5165\u5BBF\u4E3B\u5E93\uFF08diff \u975E ADD \u8868\u53D8\u66F4\u9ED8\u8BA4\u62D2\u7EDD\uFF09");
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
const projectName = readEnvValue(projectRoot, "PROJECT_NAME") || "add-project";
|
|
944
|
+
const dbUser = readEnvValue(projectRoot, "DATABASE_USER") || "admin";
|
|
945
|
+
const dbPass = readEnvValue(projectRoot, "DATABASE_PASSWORD") || "change-me-in-production";
|
|
946
|
+
const dbName = `${projectName}-add`;
|
|
947
|
+
const container = `${projectName}-add-postgres`;
|
|
948
|
+
const port = (await allocatePortsWithContract(projectRoot, [{ name: "add", containerName: `${projectName}-add-postgres`, envKey: "ADD_DATABASE_URL" }])).add;
|
|
949
|
+
console.log(`\u542F\u52A8\u72EC\u7ACB ADD \u5E93\u5BB9\u5668 ${container}\uFF08\u7AEF\u53E3 ${port}\uFF09...`);
|
|
950
|
+
const r = runCommand("podman", [
|
|
951
|
+
"run",
|
|
952
|
+
"-d",
|
|
953
|
+
"--name",
|
|
954
|
+
container,
|
|
955
|
+
"--restart",
|
|
956
|
+
"unless-stopped",
|
|
957
|
+
"-e",
|
|
958
|
+
`POSTGRES_USER=${dbUser}`,
|
|
959
|
+
"-e",
|
|
960
|
+
`POSTGRES_PASSWORD=${dbPass}`,
|
|
961
|
+
"-e",
|
|
962
|
+
`POSTGRES_DB=${dbName}`,
|
|
963
|
+
"-p",
|
|
964
|
+
`127.0.0.1:${port}:5432`,
|
|
965
|
+
"docker.io/postgres:16-alpine"
|
|
966
|
+
], { timeout: 6e4 });
|
|
967
|
+
if (r.status !== 0) {
|
|
968
|
+
throw new Error(`\u72EC\u7ACB ADD \u5E93\u5BB9\u5668\u542F\u52A8\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${r.status}\uFF09: ${r.stderr.trim().slice(0, 200)}`);
|
|
969
|
+
}
|
|
970
|
+
appendEnvValue(projectRoot, "ADD_DATABASE_URL", `postgresql://${dbUser}:${dbPass}@127.0.0.1:${port}/${dbName}?schema=public`);
|
|
971
|
+
const addPrismaPath = resolve5(projectRoot, "prisma", "add.prisma");
|
|
972
|
+
if (existsSync9(addPrismaPath)) {
|
|
973
|
+
const content = readFileSync7(addPrismaPath, "utf-8");
|
|
974
|
+
if (!content.includes("datasource db")) {
|
|
975
|
+
writeFileSync5(addPrismaPath, `datasource db {
|
|
976
|
+
provider = "postgresql"
|
|
977
|
+
url = env("ADD_DATABASE_URL")
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
${content}`, "utf-8");
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
console.log(`\u2705 \u5DF2\u521B\u5EFA\u72EC\u7ACB ADD \u5E93 ${container}\uFF08\u7AEF\u53E3 ${port}\uFF09\uFF0CADD_DATABASE_URL \u5DF2\u5199\u5165 .env.development`);
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
function prismaDiffArgs(pm, schemaTo, fromUrl) {
|
|
987
|
+
const base = pm === "pnpm" ? ["dlx", "prisma", "migrate", "diff"] : ["exec", "prisma", "--", "migrate", "diff"];
|
|
988
|
+
const toFlag = "--to-schema";
|
|
989
|
+
const args = [...base, "--from-empty", toFlag, schemaTo, "--script"];
|
|
990
|
+
if (fromUrl) args.splice(args.indexOf("--from-empty"), 1, "--from-url", fromUrl);
|
|
991
|
+
return args;
|
|
992
|
+
}
|
|
993
|
+
async function provisionDevUrl(projectRoot) {
|
|
994
|
+
const existing = readEnvValue(projectRoot, "ATLAS_DEV_URL");
|
|
995
|
+
if (existing) return existing;
|
|
996
|
+
if (!commandExists("podman")) return null;
|
|
997
|
+
const projectName = readEnvValue(projectRoot, "PROJECT_NAME") || "add-project";
|
|
998
|
+
const container = `${projectName}-add-dev`;
|
|
999
|
+
try {
|
|
1000
|
+
const ps = runCommand("podman", ["ps", "--filter", `name=^/${container}$`, "--format", "{{.Names}}"]);
|
|
1001
|
+
if (ps.stdout.trim()) {
|
|
1002
|
+
const portR = runCommand("podman", ["port", container, "5432/tcp"]);
|
|
1003
|
+
const pm = portR.stdout.match(/127\.0\.0\.1:(\d{2,5})/);
|
|
1004
|
+
if (pm) {
|
|
1005
|
+
const url2 = `postgresql://postgres:postgres@127.0.0.1:${pm[1]}/dev?schema=public`;
|
|
1006
|
+
appendEnvValue(projectRoot, "ATLAS_DEV_URL", url2);
|
|
1007
|
+
return url2;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
} catch {
|
|
1011
|
+
}
|
|
1012
|
+
const ports = await allocatePortsWithContract(projectRoot, [{ name: "dev", containerName: container, envKey: "ATLAS_DEV_URL" }]);
|
|
1013
|
+
const port = ports.dev;
|
|
1014
|
+
const r = runCommand("podman", [
|
|
1015
|
+
"run",
|
|
1016
|
+
"-d",
|
|
1017
|
+
"--name",
|
|
1018
|
+
container,
|
|
1019
|
+
"--restart",
|
|
1020
|
+
"unless-stopped",
|
|
1021
|
+
"-e",
|
|
1022
|
+
"POSTGRES_USER=postgres",
|
|
1023
|
+
"-e",
|
|
1024
|
+
"POSTGRES_PASSWORD=postgres",
|
|
1025
|
+
"-e",
|
|
1026
|
+
"POSTGRES_DB=dev",
|
|
1027
|
+
"-p",
|
|
1028
|
+
`127.0.0.1:${port}:5432`,
|
|
1029
|
+
"docker.io/postgres:16-alpine"
|
|
1030
|
+
], { timeout: 6e4 });
|
|
1031
|
+
if (r.status !== 0) return null;
|
|
1032
|
+
const url = `postgresql://postgres:postgres@127.0.0.1:${port}/dev?schema=public`;
|
|
1033
|
+
appendEnvValue(projectRoot, "ATLAS_DEV_URL", url);
|
|
1034
|
+
console.log(`\u2705 \u5E38\u9A7B dev \u7A7A\u5E93\u5DF2\u521B\u5EFA: ${container}\uFF08\u7AEF\u53E3 ${port}\uFF0C\u4E0D\u9500\u6BC1\uFF0C\u53EF\u968F\u65F6\u91CD\u7F6E\uFF09`);
|
|
1035
|
+
return url;
|
|
1036
|
+
}
|
|
1037
|
+
function resolveAtlasBin(projectRoot) {
|
|
1038
|
+
const pkgBin = resolve5(projectRoot, "node_modules", "add-coder", "node_modules", ".bin", "atlas");
|
|
1039
|
+
if (existsSync9(pkgBin)) return pkgBin;
|
|
1040
|
+
const local = resolve5(projectRoot, "node_modules", ".bin", "atlas");
|
|
1041
|
+
if (existsSync9(local)) return local;
|
|
1042
|
+
return commandExists("atlas") ? "atlas" : null;
|
|
1043
|
+
}
|
|
1044
|
+
function hasNonAddTableChanges(diffSql) {
|
|
1045
|
+
const re = /(?:DROP|ALTER|CREATE)\s+TABLE(?:\s+IF\s+EXISTS)?\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi;
|
|
1046
|
+
let m;
|
|
1047
|
+
while ((m = re.exec(diffSql)) !== null) {
|
|
1048
|
+
if (!ADD_TABLES2.includes(m[1])) return true;
|
|
1049
|
+
}
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
function buildExcludeArgs(projectRoot, splitDb) {
|
|
1053
|
+
if (splitDb) return [];
|
|
1054
|
+
const projectName = readEnvValue(projectRoot, "PROJECT_NAME") || "add-project";
|
|
1055
|
+
const dbUser = readEnvValue(projectRoot, "DATABASE_USER") || "admin";
|
|
1056
|
+
try {
|
|
1057
|
+
const r = runCommand("podman", [
|
|
1058
|
+
"exec",
|
|
1059
|
+
`${projectName}-postgres`,
|
|
1060
|
+
"psql",
|
|
1061
|
+
"-U",
|
|
1062
|
+
dbUser,
|
|
1063
|
+
"-d",
|
|
1064
|
+
projectName,
|
|
1065
|
+
"-tAc",
|
|
1066
|
+
"SELECT string_agg('public.' || table_name, ',') FROM information_schema.tables WHERE table_schema='public' AND table_name NOT IN ('AddUser','DevOperation','AuditLog','HitlRecord','PlanRecord','ReviewRecord','CollabContract');"
|
|
1067
|
+
], { timeout: 3e4 });
|
|
1068
|
+
const tables = r.stdout.trim();
|
|
1069
|
+
return tables ? ["--exclude", tables] : [];
|
|
1070
|
+
} catch {
|
|
1071
|
+
return [];
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
async function runAtlasSync(projectRoot, targetUrl, splitDb, sync, opts) {
|
|
1075
|
+
const pm = detectPm(projectRoot);
|
|
1076
|
+
const schemaTo = splitDb ? resolve5(projectRoot, "prisma", "add.prisma") : resolve5(projectRoot, "prisma");
|
|
1077
|
+
const baselinePath = join6(projectRoot, PRISMA_CONFIG.sync.backupDir, "baseline.sql");
|
|
1078
|
+
mkdirSync5(dirname3(baselinePath), { recursive: true });
|
|
1079
|
+
const diffR = runCommand(pm, prismaDiffArgs(pm, schemaTo, null), { cwd: projectRoot, timeout: 6e4 });
|
|
1080
|
+
if (diffR.status !== 0) {
|
|
1081
|
+
console.warn(`\u26A0\uFE0F baseline \u751F\u6210\u5931\u8D25\uFF08${diffR.stderr.trim().slice(0, 150)}\uFF09\uFF0C\u964D\u7EA7 prisma-diff`);
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
writeFileSync5(baselinePath, diffR.stdout, "utf-8");
|
|
1085
|
+
const atlasBin = resolveAtlasBin(projectRoot);
|
|
1086
|
+
if (!atlasBin) {
|
|
1087
|
+
console.warn("\u26A0\uFE0F atlas \u4E0D\u53EF\u7528\uFF08\u4F9D\u8D56\u672A\u5B89\u88C5\u6216\u5168\u5C40\u7F3A\u5931\uFF09");
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
let devUrl = await provisionDevUrl(projectRoot);
|
|
1091
|
+
if (!devUrl && sync.atlasDevUrl) devUrl = sync.atlasDevUrl;
|
|
1092
|
+
if (!devUrl) {
|
|
1093
|
+
console.warn("\u26A0\uFE0F dev-url \u4E0D\u53EF\u7528\uFF08\u65E0 podman \u4E14\u672A\u914D\u7F6E atlas_dev_url\uFF09\uFF0C\u964D\u7EA7 prisma-diff");
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
1096
|
+
{
|
|
1097
|
+
const diffArgs = ["schema", "diff", "--from", targetUrl, "--to", `file://${baselinePath}`, "--dev-url", devUrl, ...buildExcludeArgs(projectRoot, splitDb)];
|
|
1098
|
+
const diffSqlR = runCommand(atlasBin, diffArgs, { cwd: projectRoot, timeout: 6e4 });
|
|
1099
|
+
if (diffSqlR.status !== 0) {
|
|
1100
|
+
console.warn(`\u26A0\uFE0F atlas diff \u5931\u8D25\uFF08${diffSqlR.stderr.trim().slice(0, 150)}\uFF09\uFF0C\u964D\u7EA7 prisma-diff`);
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
const diffSql = diffSqlR.stdout;
|
|
1104
|
+
const hasSql = /(?:^|\n)\s*(?:CREATE|ALTER|DROP|COMMENT|--\s*(?:Create|Modify|Drop))\b/i.test(diffSql);
|
|
1105
|
+
if (!hasSql) {
|
|
1106
|
+
console.log("\u2705 \u6570\u636E\u5E93\u4E0E\u76EE\u6807 schema \u4E00\u81F4\uFF08\u5E42\u7B49\u51FA\u53E3\uFF09");
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
if (!splitDb && hasNonAddTableChanges(diffSql)) {
|
|
1110
|
+
console.error("\u26D4 \u5171\u5E93\u6A21\u5F0F\u68C0\u6D4B\u5230\u975E ADD \u8868\u53D8\u66F4\uFF08\u9ED8\u8BA4\u62D2\u7EDD\uFF09\u3002\u8BF7\u4EBA\u5DE5\u5BA1\u6838 diff.sql \u6216\u9009\u62E9\u5206\u5E93\u6A21\u5F0F\u3002");
|
|
1111
|
+
console.error(diffSql.split("\n").slice(0, 30).join("\n"));
|
|
1112
|
+
throw new Error("\u5171\u5E93\u6A21\u5F0F\u975E ADD \u8868\u53D8\u66F4\u9ED8\u8BA4\u62D2\u7EDD\uFF08\u53EF\u7528 --yes \u6216\u5206\u5E93\u6A21\u5F0F\u7ED5\u8FC7\uFF09");
|
|
1113
|
+
}
|
|
1114
|
+
if (!opts.yes) {
|
|
1115
|
+
console.log("=== \u5F85\u5E94\u7528 diff SQL ===");
|
|
1116
|
+
console.log(diffSql.split("\n").slice(0, 60).join("\n"));
|
|
1117
|
+
const c = await ask("\u5E94\u7528\u4EE5\u4E0A schema \u53D8\u66F4\uFF1F[y/N] ");
|
|
1118
|
+
if (c !== "y" && c !== "yes") {
|
|
1119
|
+
console.log("\u5DF2\u53D6\u6D88\uFF0C\u672A\u5E94\u7528");
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
const applyArgs = ["schema", "apply", "--url", targetUrl, "--to", `file://${baselinePath}`, "--dev-url", devUrl, ...buildExcludeArgs(projectRoot, splitDb)];
|
|
1124
|
+
const applyR = runCommand(atlasBin, applyArgs, { cwd: projectRoot, timeout: 12e4 });
|
|
1125
|
+
if (applyR.status !== 0) {
|
|
1126
|
+
console.warn(`\u26A0\uFE0F atlas apply \u5931\u8D25\uFF08${applyR.stderr.trim().slice(0, 150)}\uFF09`);
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
console.log("\u2705 Atlas schema \u540C\u6B65\u5B8C\u6210");
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function runPrismaDiffSync(projectRoot, targetUrl, splitDb) {
|
|
1134
|
+
const pm = detectPm(projectRoot);
|
|
1135
|
+
const schemaTo = splitDb ? resolve5(projectRoot, "prisma", "add.prisma") : resolve5(projectRoot, "prisma");
|
|
1136
|
+
const diffPath = join6(projectRoot, PRISMA_CONFIG.sync.backupDir, "diff.sql");
|
|
1137
|
+
mkdirSync5(dirname3(diffPath), { recursive: true });
|
|
1138
|
+
const r = runCommand(pm, prismaDiffArgs(pm, schemaTo, targetUrl), { cwd: projectRoot, timeout: 6e4 });
|
|
1139
|
+
if (r.status !== 0) throw new Error(`prisma migrate diff \u5931\u8D25: ${r.stderr.trim().slice(0, 200)}`);
|
|
1140
|
+
writeFileSync5(diffPath, r.stdout, "utf-8");
|
|
1141
|
+
if (!r.stdout.trim()) {
|
|
1142
|
+
console.log("\u2705 \u6570\u636E\u5E93\u4E0E\u76EE\u6807 schema \u4E00\u81F4\uFF08\u5E42\u7B49\u51FA\u53E3\uFF09");
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const execArgs = pm === "pnpm" ? ["dlx", "prisma", "db", "execute", "--file", diffPath] : ["exec", "prisma", "--", "db", "execute", "--file", diffPath];
|
|
1146
|
+
const e = runCommand(pm, execArgs, { cwd: projectRoot, timeout: 12e4 });
|
|
1147
|
+
if (e.status !== 0) throw new Error(`prisma db execute \u5931\u8D25: ${e.stderr.trim().slice(0, 200)}`);
|
|
1148
|
+
console.log("\u2705 prisma-diff \u540C\u6B65\u5B8C\u6210\uFF08\u514D shadow\uFF09");
|
|
1149
|
+
}
|
|
1150
|
+
function runDbPush(projectRoot) {
|
|
1151
|
+
const pm = detectPm(projectRoot);
|
|
1152
|
+
const args = pm === "pnpm" ? ["dlx", "prisma", "db", "push"] : ["exec", "prisma", "--", "db", "push"];
|
|
1153
|
+
if (PRISMA_CONFIG.schemaArg) args.push(PRISMA_CONFIG.schemaArg);
|
|
1154
|
+
console.log(`\u6267\u884C ${pm} ${args.join(" ")} ...`);
|
|
1155
|
+
const r = runCommand(pm, args, { cwd: projectRoot });
|
|
1156
|
+
if (r.status !== 0) throw new Error(`prisma db push \u9000\u51FA\u7801: ${r.status}`);
|
|
1157
|
+
}
|
|
1158
|
+
async function syncDatabase(projectRoot, opts, splitDb) {
|
|
1159
|
+
const sync = PRISMA_CONFIG.sync;
|
|
1160
|
+
const targetUrl = splitDb ? readEnvValue(projectRoot, "ADD_DATABASE_URL") || sync.addDatabaseUrl : readEnvValue(projectRoot, "DATABASE_URL");
|
|
1161
|
+
if (!targetUrl) {
|
|
1162
|
+
console.warn("\u26A0\uFE0F \u672A\u627E\u5230\u76EE\u6807\u5E93\u8FDE\u63A5\u4E32\uFF08\u5206\u5E93\u9700 ADD_DATABASE_URL / \u5171\u5E93\u9700 DATABASE_URL\uFF09\uFF0C\u8DF3\u8FC7\u540C\u6B65");
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const bak = await backupBeforeSync(projectRoot, { dbUrl: targetUrl, backupDir: sync.backupDir, backupKeep: sync.backupKeep, yes: opts.yes });
|
|
1166
|
+
if (bak === null) {
|
|
1167
|
+
console.warn("\u26A0\uFE0F \u5907\u4EFD\u672A\u5B8C\u6210\uFF08\u7528\u6237\u81EA\u62C5\u98CE\u9669\uFF09\uFF0C\u7EE7\u7EED\u540C\u6B65");
|
|
1168
|
+
}
|
|
1169
|
+
if (sync.strategy === "db-push") {
|
|
1170
|
+
runDbPush(projectRoot);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const atlasBin = resolveAtlasBin(projectRoot);
|
|
1174
|
+
if (atlasBin) {
|
|
1175
|
+
const ok = await runAtlasSync(projectRoot, targetUrl, splitDb, sync, opts);
|
|
1176
|
+
if (ok) return;
|
|
1177
|
+
} else {
|
|
1178
|
+
console.warn("\u26A0\uFE0F atlas \u4E0D\u53EF\u7528\u3002add-coder \u4F9D\u8D56\u81EA\u5E26\uFF1A\u9879\u76EE\u5B89\u88C5 @ariga/atlas\uFF08pnpm add -D @ariga/atlas\uFF09\uFF0C\u6216 npm \u5168\u5C40\u5B89\u88C5");
|
|
1179
|
+
if (!opts.yes) {
|
|
1180
|
+
const a = await ask("\u7EE7\u7EED\u964D\u7EA7 prisma-diff\uFF08\u514D shadow\uFF09\uFF1F[Y/n] ");
|
|
1181
|
+
if (a === "n" || a === "no") throw new Error("\u5DF2\u53D6\u6D88\u540C\u6B65\uFF08atlas \u7F3A\u5931\uFF09");
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
runPrismaDiffSync(projectRoot, targetUrl, splitDb);
|
|
1185
|
+
}
|
|
693
1186
|
function ensurePrismaConfig(projectRoot) {
|
|
694
|
-
const configPath =
|
|
695
|
-
|
|
1187
|
+
const configPath = resolve5(projectRoot, "prisma.config.ts");
|
|
1188
|
+
writeFileSync5(configPath, [
|
|
696
1189
|
'import dotenv from "dotenv";',
|
|
697
1190
|
'import { existsSync } from "fs";',
|
|
698
1191
|
'for (const f of [".env.development.local", ".env.development", ".env.local", ".env"]) {',
|
|
@@ -707,20 +1200,6 @@ function ensurePrismaConfig(projectRoot) {
|
|
|
707
1200
|
"});"
|
|
708
1201
|
].join("\n") + "\n", "utf-8");
|
|
709
1202
|
}
|
|
710
|
-
function backupAddTables(projectRoot) {
|
|
711
|
-
if (!commandExists("pg_dump")) return null;
|
|
712
|
-
const bak = resolve3(projectRoot, `add-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}.sql`);
|
|
713
|
-
const r = runCommand("pg_dump", ["--table=AddUser", "--table=DevOperation", "--table=AuditLog", "--if-exists"], {
|
|
714
|
-
cwd: projectRoot,
|
|
715
|
-
timeout: 3e4
|
|
716
|
-
});
|
|
717
|
-
if (r.stdout.length > 0) {
|
|
718
|
-
writeFileSync3(bak, r.stdout, "utf-8");
|
|
719
|
-
console.log(`>>> \u5907\u4EFD ADD \u8868\u5230 ${bak}`);
|
|
720
|
-
return bak;
|
|
721
|
-
}
|
|
722
|
-
return null;
|
|
723
|
-
}
|
|
724
1203
|
function runPrismaInit(projectRoot, provider, schemaPath) {
|
|
725
1204
|
console.log("\u6267\u884C prisma init ...");
|
|
726
1205
|
const pm = detectPm(projectRoot);
|
|
@@ -732,10 +1211,10 @@ function runPrismaInit(projectRoot, provider, schemaPath) {
|
|
|
732
1211
|
console.error(`\u2717 prisma init \u65E0\u6CD5\u6267\u884C: ${e instanceof Error ? e.message : String(e)}`);
|
|
733
1212
|
initResult = { status: null, stdout: "", stderr: "" };
|
|
734
1213
|
}
|
|
735
|
-
if (initResult.status !== 0 || !
|
|
1214
|
+
if (initResult.status !== 0 || !existsSync9(schemaPath)) {
|
|
736
1215
|
console.error(`\u26A0\uFE0F prisma init \u672A\u5B8C\u6210\uFF08\u9000\u51FA\u7801: ${initResult.status}\uFF09\uFF0C\u56DE\u9000\u624B\u52A8\u521B\u5EFA schema.prisma\u2014\u2014db push \u5C06\u9A8C\u8BC1\u5176\u53EF\u7528\u6027`);
|
|
737
|
-
const prismaDir =
|
|
738
|
-
if (!
|
|
1216
|
+
const prismaDir = resolve5(projectRoot, "prisma");
|
|
1217
|
+
if (!existsSync9(prismaDir)) mkdirSync5(prismaDir, { recursive: true });
|
|
739
1218
|
const content = `generator client {
|
|
740
1219
|
provider = "prisma-client-js"
|
|
741
1220
|
}
|
|
@@ -744,11 +1223,11 @@ datasource db {
|
|
|
744
1223
|
provider = "${provider}"
|
|
745
1224
|
}
|
|
746
1225
|
`;
|
|
747
|
-
|
|
748
|
-
const devEnvPath =
|
|
749
|
-
if (!
|
|
1226
|
+
writeFileSync5(schemaPath, content, "utf-8");
|
|
1227
|
+
const devEnvPath = resolve5(projectRoot, ".env.development");
|
|
1228
|
+
if (!existsSync9(devEnvPath)) {
|
|
750
1229
|
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"';
|
|
751
|
-
|
|
1230
|
+
writeFileSync5(devEnvPath, defaultUrl + "\n", "utf-8");
|
|
752
1231
|
console.log("\u5DF2\u521B\u5EFA .env.development");
|
|
753
1232
|
}
|
|
754
1233
|
return false;
|
|
@@ -756,18 +1235,18 @@ datasource db {
|
|
|
756
1235
|
return true;
|
|
757
1236
|
}
|
|
758
1237
|
function postInitSetup(projectRoot, schemaPath, addPrismaTemplate, destPath) {
|
|
759
|
-
const envPath =
|
|
760
|
-
const devEnvPath =
|
|
761
|
-
if (
|
|
762
|
-
const envContent =
|
|
1238
|
+
const envPath = resolve5(projectRoot, ".env");
|
|
1239
|
+
const devEnvPath = resolve5(projectRoot, ".env.development");
|
|
1240
|
+
if (existsSync9(envPath)) {
|
|
1241
|
+
const envContent = readFileSync7(envPath, "utf-8");
|
|
763
1242
|
const dbUrl = envContent.match(/DATABASE_URL=.*/);
|
|
764
1243
|
if (dbUrl) {
|
|
765
|
-
const existing =
|
|
1244
|
+
const existing = existsSync9(devEnvPath) ? readFileSync7(devEnvPath, "utf-8") : "";
|
|
766
1245
|
if (!existing.includes("DATABASE_URL=")) {
|
|
767
|
-
|
|
1246
|
+
writeFileSync5(devEnvPath, `${existing}${existing ? "\n" : ""}${dbUrl[0]}
|
|
768
1247
|
`, "utf-8");
|
|
769
1248
|
}
|
|
770
|
-
if (
|
|
1249
|
+
if (existsSync9(envPath)) unlinkSync(envPath);
|
|
771
1250
|
console.log("\u5DF2\u5C06 DATABASE_URL \u8FC1\u79FB\u5230 .env.development");
|
|
772
1251
|
}
|
|
773
1252
|
}
|
|
@@ -776,8 +1255,8 @@ function postInitSetup(projectRoot, schemaPath, addPrismaTemplate, destPath) {
|
|
|
776
1255
|
patchGeneratorOutput(schemaPath);
|
|
777
1256
|
}
|
|
778
1257
|
function patchGeneratorOutput(schemaPath) {
|
|
779
|
-
if (!
|
|
780
|
-
let content =
|
|
1258
|
+
if (!existsSync9(schemaPath)) return;
|
|
1259
|
+
let content = readFileSync7(schemaPath, "utf-8");
|
|
781
1260
|
const genBlock = content.match(/generator\s+\w+\s*\{[\s\S]*?\}/);
|
|
782
1261
|
if (!genBlock) {
|
|
783
1262
|
content += `
|
|
@@ -786,23 +1265,23 @@ generator client {
|
|
|
786
1265
|
output = "../src/generated/prisma"
|
|
787
1266
|
}
|
|
788
1267
|
`;
|
|
789
|
-
|
|
1268
|
+
writeFileSync5(schemaPath, content, "utf-8");
|
|
790
1269
|
console.log("\u5DF2\u8FFD\u52A0 generator client\uFF08\u542B output \u2192 src/generated/prisma\uFF09");
|
|
791
1270
|
return;
|
|
792
1271
|
}
|
|
793
1272
|
if (genBlock[0].includes("output")) return;
|
|
794
1273
|
const patched = genBlock[0].replace(/\}\s*$/, ` output = "../src/generated/prisma"
|
|
795
1274
|
}`);
|
|
796
|
-
|
|
1275
|
+
writeFileSync5(schemaPath, content.replace(genBlock[0], patched), "utf-8");
|
|
797
1276
|
console.log("\u5DF2\u6CE8\u5165 generator output \u2192 src/generated/prisma");
|
|
798
1277
|
}
|
|
799
1278
|
async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
|
|
800
1279
|
const C = PRISMA_CONFIG;
|
|
801
|
-
const prismaDir =
|
|
802
|
-
const schemaPath =
|
|
803
|
-
const destPath =
|
|
1280
|
+
const prismaDir = resolve5(projectRoot, "prisma");
|
|
1281
|
+
const schemaPath = resolve5(prismaDir, "schema.prisma");
|
|
1282
|
+
const destPath = resolve5(prismaDir, "add.prisma");
|
|
804
1283
|
let justInited = false;
|
|
805
|
-
if (!
|
|
1284
|
+
if (!existsSync9(prismaDir) || !existsSync9(schemaPath)) {
|
|
806
1285
|
if (C.onMissing === "skip") {
|
|
807
1286
|
console.log("\u8DF3\u8FC7\uFF1A\u7F3A\u5C11 Prisma");
|
|
808
1287
|
return true;
|
|
@@ -821,7 +1300,7 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
|
|
|
821
1300
|
postInitSetup(projectRoot, schemaPath, addPrismaTemplate, destPath);
|
|
822
1301
|
justInited = true;
|
|
823
1302
|
}
|
|
824
|
-
if (
|
|
1303
|
+
if (existsSync9(destPath) && !justInited) {
|
|
825
1304
|
if (options.dryRun) {
|
|
826
1305
|
console.log("[dry-run] \u5DF2\u6709 add.prisma");
|
|
827
1306
|
return true;
|
|
@@ -838,8 +1317,8 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
|
|
|
838
1317
|
console.log("\u8986\u76D6");
|
|
839
1318
|
} else if (choice === "d") {
|
|
840
1319
|
copyFileSync(destPath, destPath + ".bak");
|
|
841
|
-
console.log("=== \u5F53\u524D\uFF08\u5DF2\u5907\u4EFD\uFF09===\n" +
|
|
842
|
-
console.log("=== \u6A21\u677F ===\n" +
|
|
1320
|
+
console.log("=== \u5F53\u524D\uFF08\u5DF2\u5907\u4EFD\uFF09===\n" + readFileSync7(destPath, "utf-8"));
|
|
1321
|
+
console.log("=== \u6A21\u677F ===\n" + readFileSync7(addPrismaTemplate, "utf-8"));
|
|
843
1322
|
if (await ask("\u786E\u8BA4\u8986\u76D6\uFF1F[y/N] ") !== "y") {
|
|
844
1323
|
console.log("\u5DF2\u8DF3\u8FC7");
|
|
845
1324
|
return true;
|
|
@@ -851,19 +1330,14 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
|
|
|
851
1330
|
}
|
|
852
1331
|
}
|
|
853
1332
|
if (options.dryRun) {
|
|
854
|
-
console.log("[dry-run] \u5C06\u6267\u884C
|
|
1333
|
+
console.log("[dry-run] \u5C06\u6267\u884C\u6570\u636E\u5E93\u540C\u6B65\uFF08v2 Atlas \u5F15\u64CE\uFF09");
|
|
855
1334
|
return true;
|
|
856
1335
|
}
|
|
857
1336
|
if (!justInited) copyFileSync(addPrismaTemplate, destPath);
|
|
858
1337
|
try {
|
|
859
1338
|
ensurePrismaConfig(projectRoot);
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const args = pm === "pnpm" ? ["dlx", "prisma", "db", "push"] : ["exec", "prisma", "--", "db", "push"];
|
|
863
|
-
if (C.schemaArg) args.push(C.schemaArg);
|
|
864
|
-
console.log(`\u6267\u884C ${pm} ${args.join(" ")} ...`);
|
|
865
|
-
const r = runCommand(pm, args, { cwd: projectRoot });
|
|
866
|
-
if (r.status !== 0) throw new Error(`prisma db push \u9000\u51FA\u7801: ${r.status}`);
|
|
1339
|
+
const splitDb = await ensureSplitDb(projectRoot, options);
|
|
1340
|
+
await syncDatabase(projectRoot, { yes: options.yes }, splitDb);
|
|
867
1341
|
} catch (err) {
|
|
868
1342
|
if (C.onMigrateFail === "keep") {
|
|
869
1343
|
console.log("\u8FC1\u79FB\u5931\u8D25\uFF0C\u4FDD\u7559\u6587\u4EF6");
|
|
@@ -893,36 +1367,37 @@ ${detail}` : ""}`);
|
|
|
893
1367
|
|
|
894
1368
|
// src/cli/prisma-injector.ts
|
|
895
1369
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
896
|
-
var __dirname2 =
|
|
897
|
-
var ADD_PRISMA_TEMPLATE =
|
|
1370
|
+
var __dirname2 = dirname4(__filename2);
|
|
1371
|
+
var ADD_PRISMA_TEMPLATE = resolve6(__dirname2, "../templates/core/prisma/add.prisma");
|
|
898
1372
|
async function injectPrisma2(projectRoot, options = {}) {
|
|
899
1373
|
return injectPrisma(projectRoot, ADD_PRISMA_TEMPLATE, options);
|
|
900
1374
|
}
|
|
901
1375
|
|
|
902
1376
|
// src/cli/commands/init.ts
|
|
903
|
-
import { readFileSync as
|
|
1377
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync11, mkdirSync as mkdirSync6, copyFileSync as copyFileSync2, readdirSync as readdirSync4 } from "fs";
|
|
904
1378
|
import { createHash } from "crypto";
|
|
905
|
-
import { resolve as
|
|
906
|
-
import {
|
|
1379
|
+
import { resolve as resolve8, dirname as dirname5, join as pathJoin } from "path";
|
|
1380
|
+
import { homedir as homedir2 } from "os";
|
|
1381
|
+
import { createConnection as createConnection2 } from "net";
|
|
907
1382
|
|
|
908
1383
|
// src/lib/model-predownload.ts
|
|
909
|
-
import { existsSync as
|
|
910
|
-
import { join as
|
|
1384
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
1385
|
+
import { join as join7, resolve as resolve7 } from "path";
|
|
911
1386
|
import { homedir } from "os";
|
|
912
1387
|
import { parse as parse2 } from "smol-toml";
|
|
913
1388
|
var DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
914
1389
|
var TOML_CANDIDATES = [
|
|
915
|
-
|
|
916
|
-
|
|
1390
|
+
resolve7(import.meta.dirname, "caijuehub/dps-scoring-rules.toml"),
|
|
1391
|
+
resolve7(import.meta.dirname, "../caijuehub/dps-scoring-rules.toml")
|
|
917
1392
|
];
|
|
918
1393
|
function resolveEmbeddingModel() {
|
|
919
|
-
const tomlPath = TOML_CANDIDATES.find((p) =>
|
|
1394
|
+
const tomlPath = TOML_CANDIDATES.find((p) => existsSync10(p));
|
|
920
1395
|
if (!tomlPath) {
|
|
921
1396
|
throw new Error(
|
|
922
1397
|
`dps-scoring-rules.toml \u672A\u627E\u5230\uFF08\u671F\u671B\u8DEF\u5F84: ${TOML_CANDIDATES.join(" \u6216 ")}\uFF09`
|
|
923
1398
|
);
|
|
924
1399
|
}
|
|
925
|
-
const cfg = parse2(
|
|
1400
|
+
const cfg = parse2(readFileSync8(tomlPath, "utf-8"));
|
|
926
1401
|
const model = cfg.embedding?.model;
|
|
927
1402
|
if (typeof model !== "string" || model.length === 0) {
|
|
928
1403
|
throw new Error(`dps-scoring-rules.toml [embedding] model \u672A\u914D\u7F6E\uFF08${tomlPath}\uFF09`);
|
|
@@ -932,8 +1407,8 @@ function resolveEmbeddingModel() {
|
|
|
932
1407
|
function resolveCacheDir() {
|
|
933
1408
|
const hubCache = process.env.HF_HUB_CACHE;
|
|
934
1409
|
if (hubCache) return hubCache;
|
|
935
|
-
const home = process.env.HF_HOME ||
|
|
936
|
-
return
|
|
1410
|
+
const home = process.env.HF_HOME || join7(homedir(), ".cache", "huggingface");
|
|
1411
|
+
return join7(home, "hub");
|
|
937
1412
|
}
|
|
938
1413
|
function modelCacheName(model) {
|
|
939
1414
|
const parts = model.split("/");
|
|
@@ -943,7 +1418,7 @@ function modelCacheName(model) {
|
|
|
943
1418
|
}
|
|
944
1419
|
function isModelCached(model) {
|
|
945
1420
|
const cacheDir = resolveCacheDir();
|
|
946
|
-
return
|
|
1421
|
+
return existsSync10(join7(cacheDir, modelCacheName(model), "snapshots"));
|
|
947
1422
|
}
|
|
948
1423
|
async function ensureEmbeddingModel(options) {
|
|
949
1424
|
const force = options?.force ?? false;
|
|
@@ -956,8 +1431,8 @@ async function ensureEmbeddingModel(options) {
|
|
|
956
1431
|
const { pipeline, env } = await import("@huggingface/transformers");
|
|
957
1432
|
env.cacheDir = resolveCacheDir();
|
|
958
1433
|
const cacheDir = env.cacheDir;
|
|
959
|
-
const snapshotsDir =
|
|
960
|
-
if (!force &&
|
|
1434
|
+
const snapshotsDir = join7(cacheDir, modelCacheName(model), "snapshots");
|
|
1435
|
+
if (!force && existsSync10(snapshotsDir)) {
|
|
961
1436
|
return { status: "already-cached", model, cacheDir };
|
|
962
1437
|
}
|
|
963
1438
|
env.remoteHost = "https://hf-mirror.com";
|
|
@@ -991,6 +1466,10 @@ var ADAPTER_RENDERERS = {
|
|
|
991
1466
|
};
|
|
992
1467
|
var MAGIC_DIR_MAP = { claude: ".claude", qoder: ".qoder", vscode: ".vscode", trae: ".trae", codex: ".codex" };
|
|
993
1468
|
async function initCommand(options) {
|
|
1469
|
+
if (options.printMcpConfig || options.writeUserConfig) {
|
|
1470
|
+
await handleCodexMcpConfig(options);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
994
1473
|
const ctx = await prepare(options);
|
|
995
1474
|
writeComposeEnv(ctx);
|
|
996
1475
|
if (!options.dryRun) {
|
|
@@ -1001,6 +1480,7 @@ async function initCommand(options) {
|
|
|
1001
1480
|
}
|
|
1002
1481
|
const result = await renderAndWrite(ctx);
|
|
1003
1482
|
const dbFail = await deployDatabase(ctx);
|
|
1483
|
+
ensurePortsContract(ctx.projectRoot, ctx.config, !!options.dryRun);
|
|
1004
1484
|
deployDocs(ctx);
|
|
1005
1485
|
finalize(ctx, result, dbFail);
|
|
1006
1486
|
if (!options.dryRun) {
|
|
@@ -1012,6 +1492,51 @@ async function initCommand(options) {
|
|
|
1012
1492
|
}
|
|
1013
1493
|
}
|
|
1014
1494
|
}
|
|
1495
|
+
async function handleCodexMcpConfig(options) {
|
|
1496
|
+
const projectRoot = process.cwd();
|
|
1497
|
+
const magicDir = MAGIC_DIR_MAP[options.adapter ?? "codex"] ?? ".codex";
|
|
1498
|
+
const config = await loadConfig(projectRoot, options.config, { force: true });
|
|
1499
|
+
config.projectRoot = projectRoot;
|
|
1500
|
+
config.magicDir = magicDir;
|
|
1501
|
+
const files = renderAdapter5(config, projectRoot, !!options.dryRun, magicDir);
|
|
1502
|
+
const tomlEntry = [...files.entries()].find(
|
|
1503
|
+
([rel]) => rel.endsWith("config.toml.example") || rel.endsWith("config.toml")
|
|
1504
|
+
);
|
|
1505
|
+
if (!tomlEntry) {
|
|
1506
|
+
console.error("\u2717 config.toml \u6E32\u67D3\u4EA7\u7269\u7F3A\u5931\uFF08\u68C0\u67E5 templates/adapters/codex/config.toml.example\uFF09");
|
|
1507
|
+
process.exit(1);
|
|
1508
|
+
}
|
|
1509
|
+
const toml = tomlEntry[1];
|
|
1510
|
+
if (options.printMcpConfig) {
|
|
1511
|
+
console.log("\n# \u5C06\u4EE5\u4E0B\u7247\u6BB5\u7C98\u8D34\u5230 ~/.codex/config.toml\uFF08\u6216\u4F7F\u7528 --write-user-config \u81EA\u52A8\u5199\u5165\uFF09");
|
|
1512
|
+
console.log(toml);
|
|
1513
|
+
}
|
|
1514
|
+
if (options.writeUserConfig) {
|
|
1515
|
+
const userConfigPath = pathJoin(homedir2(), ".codex", "config.toml");
|
|
1516
|
+
if (options.dryRun) {
|
|
1517
|
+
console.log(`[dry-run] \u5C06\u5199\u5165 ${userConfigPath}`);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
if (existsSync11(userConfigPath) && readFileSync9(userConfigPath, "utf-8").includes("mcp_servers.add_coder")) {
|
|
1521
|
+
console.log(`\u26A0\uFE0F ${userConfigPath} \u5DF2\u5B58\u5728 mcp_servers.add_coder \u914D\u7F6E\uFF0C\u8DF3\u8FC7\uFF08\u907F\u514D\u91CD\u590D\uFF09`);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
const answer = await ask(`\u5199\u5165 ${userConfigPath}\uFF1F\uFF08y/N\uFF09 \u2192 `);
|
|
1525
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
1526
|
+
console.log("\u5DF2\u53D6\u6D88\u5199\u5165");
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
if (existsSync11(userConfigPath)) {
|
|
1530
|
+
const backup = `${userConfigPath}.bak-${Date.now()}`;
|
|
1531
|
+
copyFileSync2(userConfigPath, backup);
|
|
1532
|
+
console.log(`\u5DF2\u5907\u4EFD\u539F\u914D\u7F6E \u2192 ${backup}`);
|
|
1533
|
+
}
|
|
1534
|
+
mkdirSync6(dirname5(userConfigPath), { recursive: true });
|
|
1535
|
+
writeFileSync6(userConfigPath, `
|
|
1536
|
+
${toml}`, { flag: "a" });
|
|
1537
|
+
console.log(`\u2705 \u5DF2\u5199\u5165 ${userConfigPath}\uFF0C\u91CD\u542F Codex \u751F\u6548`);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1015
1540
|
async function resolveAdapter(projectRoot, specified) {
|
|
1016
1541
|
if (specified) {
|
|
1017
1542
|
if (!MAGIC_DIR_MAP[specified]) throw new Error(`\u672A\u77E5 adapter: ${specified}`);
|
|
@@ -1054,9 +1579,9 @@ async function resolveContainer(force) {
|
|
|
1054
1579
|
if (a !== "" && a !== "1" && a !== "podman") console.log("\u8F93\u5165\u65E0\u6CD5\u8BC6\u522B\uFF0C\u9ED8\u8BA4 podman");
|
|
1055
1580
|
return "podman";
|
|
1056
1581
|
}
|
|
1057
|
-
function
|
|
1582
|
+
function portInUse2(port) {
|
|
1058
1583
|
return new Promise((r) => {
|
|
1059
|
-
const s =
|
|
1584
|
+
const s = createConnection2({ port, host: "127.0.0.1" }, () => {
|
|
1060
1585
|
s.destroy();
|
|
1061
1586
|
r(true);
|
|
1062
1587
|
});
|
|
@@ -1098,7 +1623,7 @@ async function resolveDbCredentials(force) {
|
|
|
1098
1623
|
let port = (await ask(`DATABASE_PORT [${d.port}]: `)).trim() || d.port;
|
|
1099
1624
|
while (true) {
|
|
1100
1625
|
const portNum = parseInt(port);
|
|
1101
|
-
if (!isNaN(portNum) && await
|
|
1626
|
+
if (!isNaN(portNum) && await portInUse2(portNum)) {
|
|
1102
1627
|
console.log(`
|
|
1103
1628
|
\u26A0\uFE0F \u7AEF\u53E3 ${port} \u5DF2\u88AB\u5360\u7528`);
|
|
1104
1629
|
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();
|
|
@@ -1152,8 +1677,8 @@ networks:
|
|
|
1152
1677
|
`;
|
|
1153
1678
|
}
|
|
1154
1679
|
function writeSqliteExportScript(projectRoot, dryRun) {
|
|
1155
|
-
const scriptsDir =
|
|
1156
|
-
const scriptPath =
|
|
1680
|
+
const scriptsDir = resolve8(projectRoot, "scripts");
|
|
1681
|
+
const scriptPath = resolve8(scriptsDir, "export-db.ts");
|
|
1157
1682
|
const content = `import { PrismaClient } from "@prisma/client";
|
|
1158
1683
|
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
1159
1684
|
import { resolve } from "path";
|
|
@@ -1177,22 +1702,22 @@ main().catch((e) => { console.error(e); process.exit(1); });
|
|
|
1177
1702
|
console.log(`[dry-run] \u5C06\u5199\u5165 ${scriptPath}`);
|
|
1178
1703
|
return;
|
|
1179
1704
|
}
|
|
1180
|
-
if (!
|
|
1181
|
-
|
|
1705
|
+
if (!existsSync11(scriptsDir)) mkdirSync6(scriptsDir, { recursive: true });
|
|
1706
|
+
writeFileSync6(scriptPath, content, "utf-8");
|
|
1182
1707
|
console.log("\u5DF2\u751F\u6210 scripts/export-db.ts");
|
|
1183
1708
|
}
|
|
1184
1709
|
function injectDbExportScript(projectRoot, dryRun) {
|
|
1185
|
-
const pkgPath =
|
|
1186
|
-
if (!
|
|
1710
|
+
const pkgPath = resolve8(projectRoot, "package.json");
|
|
1711
|
+
if (!existsSync11(pkgPath)) return;
|
|
1187
1712
|
if (dryRun) {
|
|
1188
1713
|
console.log("[dry-run] \u5C06\u6CE8\u5165 db:export");
|
|
1189
1714
|
return;
|
|
1190
1715
|
}
|
|
1191
|
-
const pkg = JSON.parse(
|
|
1716
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
|
|
1192
1717
|
if (!pkg.scripts) pkg.scripts = {};
|
|
1193
1718
|
if (!pkg.scripts["db:export"]) {
|
|
1194
1719
|
pkg.scripts["db:export"] = "npx tsx scripts/export-db.ts";
|
|
1195
|
-
|
|
1720
|
+
writeFileSync6(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1196
1721
|
console.log("\u5DF2\u5728 package.json \u6CE8\u5165 db:export");
|
|
1197
1722
|
}
|
|
1198
1723
|
}
|
|
@@ -1235,17 +1760,17 @@ function writeComposeEnv(ctx) {
|
|
|
1235
1760
|
if (db.engine !== "postgresql" || !db.container || db.container === "manual") return;
|
|
1236
1761
|
if (!db.reuseExisting) {
|
|
1237
1762
|
const composeName = db.container === "podman" ? "podman-compose.add.yml" : "docker-compose.add.yml";
|
|
1238
|
-
const composePath =
|
|
1239
|
-
if (!options.dryRun && (!
|
|
1240
|
-
|
|
1763
|
+
const composePath = resolve8(projectRoot, composeName);
|
|
1764
|
+
if (!options.dryRun && (!existsSync11(composePath) || options.force)) {
|
|
1765
|
+
writeFileSync6(composePath, composeContent(config.projectName || "add-project"), "utf-8");
|
|
1241
1766
|
console.log(`\u5DF2\u521B\u5EFA ${composeName}`);
|
|
1242
1767
|
}
|
|
1243
1768
|
}
|
|
1244
|
-
const devEnvPath =
|
|
1245
|
-
if (!options.dryRun &&
|
|
1246
|
-
const existing =
|
|
1769
|
+
const devEnvPath = resolve8(projectRoot, ".env.development");
|
|
1770
|
+
if (!options.dryRun && existsSync11(devEnvPath)) {
|
|
1771
|
+
const existing = readFileSync9(devEnvPath, "utf-8");
|
|
1247
1772
|
if (!/^DATABASE_USER=/m.test(existing)) {
|
|
1248
|
-
|
|
1773
|
+
writeFileSync6(devEnvPath, existing + `
|
|
1249
1774
|
DATABASE_USER=${db.user || "admin"}
|
|
1250
1775
|
DATABASE_PASSWORD=${db.password || "change-me-in-production"}
|
|
1251
1776
|
DATABASE_PORT=${db.port || "5433"}
|
|
@@ -1282,12 +1807,12 @@ async function renderAndWrite(ctx) {
|
|
|
1282
1807
|
console.log(`claude adapter (via Agent Host): ${claudeFiles.size} \u6587\u4EF6`);
|
|
1283
1808
|
}
|
|
1284
1809
|
for (const d of [".add", magicDir]) {
|
|
1285
|
-
const reviewsDir =
|
|
1286
|
-
if (!
|
|
1810
|
+
const reviewsDir = resolve8(projectRoot, d, "reviews");
|
|
1811
|
+
if (!existsSync11(reviewsDir)) {
|
|
1287
1812
|
if (dry) {
|
|
1288
1813
|
console.log(`[dry-run] \u5C06\u521B\u5EFA ${reviewsDir}/`);
|
|
1289
1814
|
} else {
|
|
1290
|
-
|
|
1815
|
+
mkdirSync6(reviewsDir, { recursive: true });
|
|
1291
1816
|
}
|
|
1292
1817
|
}
|
|
1293
1818
|
}
|
|
@@ -1295,16 +1820,16 @@ async function renderAndWrite(ctx) {
|
|
|
1295
1820
|
const hashMap = {};
|
|
1296
1821
|
let npmVer = "";
|
|
1297
1822
|
try {
|
|
1298
|
-
npmVer = JSON.parse(
|
|
1823
|
+
npmVer = JSON.parse(readFileSync9(resolve8(projectRoot, "node_modules", "add-coder", "templates", ".add-coder-src-hash.json"), "utf-8"))._version ?? "";
|
|
1299
1824
|
} catch {
|
|
1300
1825
|
}
|
|
1301
1826
|
for (const [rp, c] of allFiles) {
|
|
1302
1827
|
hashMap[rp] = createHash("sha256").update(c).digest("hex").slice(0, 8);
|
|
1303
1828
|
}
|
|
1304
|
-
const hashOut =
|
|
1305
|
-
|
|
1829
|
+
const hashOut = resolve8(projectRoot, magicDir, ".add-coder-hash.json");
|
|
1830
|
+
writeFileSync6(hashOut, JSON.stringify(hashMap, null, 2) + "\n", "utf-8");
|
|
1306
1831
|
if (npmVer) {
|
|
1307
|
-
|
|
1832
|
+
writeFileSync6(resolve8(projectRoot, magicDir, ".add-coder-version"), npmVer + "\n", "utf-8");
|
|
1308
1833
|
}
|
|
1309
1834
|
console.log(`hash: ${Object.keys(hashMap).length} entries \u2192 ${magicDir}/.add-coder-hash.json`);
|
|
1310
1835
|
}
|
|
@@ -1315,7 +1840,7 @@ async function deployDatabase(ctx) {
|
|
|
1315
1840
|
if (options.dryRun) return null;
|
|
1316
1841
|
let fail = null;
|
|
1317
1842
|
if (db.engine === "postgresql" && db.container && db.container !== "manual") {
|
|
1318
|
-
const dbScript =
|
|
1843
|
+
const dbScript = resolve8(projectRoot, magicDir, "scripts", "db-ensure.sh");
|
|
1319
1844
|
const dbEnv = { ...process.env, DATABASE_USER: db.user, DATABASE_PASSWORD: db.password, DATABASE_PORT: db.port, PROJECT_NAME: config.projectName };
|
|
1320
1845
|
const mode = db.reuseExisting ? "manual" : db.container;
|
|
1321
1846
|
console.log(db.reuseExisting ? "\u590D\u7528\u5DF2\u6709 PostgreSQL ..." : `\u90E8\u7F72\u6570\u636E\u5E93 (${db.container}) ...`);
|
|
@@ -1333,8 +1858,8 @@ ${bashRun.stderr.trim().split("\n").slice(0, 5).join("\n")}` : ""}`;
|
|
|
1333
1858
|
}
|
|
1334
1859
|
}
|
|
1335
1860
|
if (db.engine === "postgresql" && db.container === "manual") {
|
|
1336
|
-
const dbScript =
|
|
1337
|
-
if (
|
|
1861
|
+
const dbScript = resolve8(projectRoot, magicDir, "scripts", "db-ensure.sh");
|
|
1862
|
+
if (existsSync11(dbScript)) {
|
|
1338
1863
|
try {
|
|
1339
1864
|
const bashRun = runCommand("bash", [dbScript, "postgresql", "manual"], { cwd: projectRoot, stdio: "inherit" });
|
|
1340
1865
|
if (bashRun.status !== 0) fail = `db-ensure.sh \u9000\u51FA\u7801: ${bashRun.status}${bashRun.stderr ? `
|
|
@@ -1379,17 +1904,17 @@ function deployDocs(ctx) {
|
|
|
1379
1904
|
const { projectRoot, options, config } = ctx;
|
|
1380
1905
|
if (options.dryRun) return;
|
|
1381
1906
|
const pn = config.projectName || "add-project";
|
|
1382
|
-
const docsBase =
|
|
1383
|
-
const groundingSrc =
|
|
1907
|
+
const docsBase = resolve8(projectRoot, "docs", pn, "knowledge");
|
|
1908
|
+
const groundingSrc = resolve8(import.meta.dirname, "../templates/core/templates");
|
|
1384
1909
|
for (const d of ["00-\u9700\u6C42", "01-\u67B6\u6784", "02-\u89C4\u8303"]) {
|
|
1385
|
-
const srcDir =
|
|
1386
|
-
const destDir =
|
|
1387
|
-
if (!
|
|
1388
|
-
if (!
|
|
1389
|
-
for (const f of
|
|
1390
|
-
const src =
|
|
1391
|
-
const dest =
|
|
1392
|
-
if (
|
|
1910
|
+
const srcDir = resolve8(groundingSrc, d);
|
|
1911
|
+
const destDir = resolve8(docsBase, d);
|
|
1912
|
+
if (!existsSync11(destDir)) mkdirSync6(destDir, { recursive: true });
|
|
1913
|
+
if (!existsSync11(srcDir)) continue;
|
|
1914
|
+
for (const f of readdirSync4(srcDir)) {
|
|
1915
|
+
const src = resolve8(srcDir, f);
|
|
1916
|
+
const dest = resolve8(destDir, f);
|
|
1917
|
+
if (existsSync11(dest)) continue;
|
|
1393
1918
|
try {
|
|
1394
1919
|
copyFileSync2(src, dest);
|
|
1395
1920
|
} catch {
|
|
@@ -1405,7 +1930,7 @@ function finalize(ctx, result, dbFail) {
|
|
|
1405
1930
|
return;
|
|
1406
1931
|
}
|
|
1407
1932
|
if (db.engine === "sqlite") console.log("\u6570\u636E\u5907\u4EFD: npm run db:export \u2192 data/exports/");
|
|
1408
|
-
const pkg = JSON.parse(
|
|
1933
|
+
const pkg = JSON.parse(readFileSync9(resolve8(import.meta.dirname, "../package.json"), "utf-8"));
|
|
1409
1934
|
const peerNames = Object.keys(pkg.peerDependencies || {});
|
|
1410
1935
|
if (peerNames.length > 0) {
|
|
1411
1936
|
console.log(`
|
|
@@ -1433,8 +1958,8 @@ function finalize(ctx, result, dbFail) {
|
|
|
1433
1958
|
}
|
|
1434
1959
|
|
|
1435
1960
|
// src/cli/commands/sync.ts
|
|
1436
|
-
import { existsSync as
|
|
1437
|
-
import { resolve as
|
|
1961
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
|
|
1962
|
+
import { resolve as resolve9, dirname as dirname6 } from "path";
|
|
1438
1963
|
import { createHash as createHash2 } from "crypto";
|
|
1439
1964
|
|
|
1440
1965
|
// src/lib/path-normalize.ts
|
|
@@ -1492,7 +2017,7 @@ function hash8(c) {
|
|
|
1492
2017
|
}
|
|
1493
2018
|
function loadHashFile(root, magic) {
|
|
1494
2019
|
try {
|
|
1495
|
-
const raw = JSON.parse(
|
|
2020
|
+
const raw = JSON.parse(readFileSync10(resolve9(root, magic, SYNC_CONFIG.HASH_OUTPUT_FILE), "utf-8"));
|
|
1496
2021
|
const normalized = {};
|
|
1497
2022
|
for (const [k, v] of Object.entries(raw)) normalized[normalizeRelPath(k)] = v;
|
|
1498
2023
|
return normalized;
|
|
@@ -1502,18 +2027,18 @@ function loadHashFile(root, magic) {
|
|
|
1502
2027
|
}
|
|
1503
2028
|
function loadVersionFile(root, magic) {
|
|
1504
2029
|
try {
|
|
1505
|
-
return
|
|
2030
|
+
return readFileSync10(resolve9(root, magic, SYNC_CONFIG.VERSION_SENTINEL), "utf-8").trim();
|
|
1506
2031
|
} catch {
|
|
1507
2032
|
return "";
|
|
1508
2033
|
}
|
|
1509
2034
|
}
|
|
1510
2035
|
function saveVersionFile(root, magic, version2) {
|
|
1511
|
-
|
|
2036
|
+
writeFileSync7(resolve9(root, magic, SYNC_CONFIG.VERSION_SENTINEL), version2 + "\n", "utf-8");
|
|
1512
2037
|
}
|
|
1513
2038
|
function saveHashFile(root, magic, files) {
|
|
1514
2039
|
const m = {};
|
|
1515
2040
|
for (const [p, c] of files) m[p] = c;
|
|
1516
|
-
|
|
2041
|
+
writeFileSync7(resolve9(root, magic, SYNC_CONFIG.HASH_OUTPUT_FILE), JSON.stringify(m, null, 2) + "\n", "utf-8");
|
|
1517
2042
|
}
|
|
1518
2043
|
function mergeFullHash(outHash, candidates, readDiskHash) {
|
|
1519
2044
|
const finalHash = /* @__PURE__ */ new Map();
|
|
@@ -1586,10 +2111,10 @@ async function syncCommand(options = {}) {
|
|
|
1586
2111
|
candidates.set(p, c);
|
|
1587
2112
|
}
|
|
1588
2113
|
const outHash = loadHashFile(projectRoot, magicDir);
|
|
1589
|
-
const srcHashPath =
|
|
2114
|
+
const srcHashPath = resolve9(projectRoot, "node_modules", "add-coder", "templates", ".add-coder-src-hash.json");
|
|
1590
2115
|
let npmVersion = "";
|
|
1591
2116
|
try {
|
|
1592
|
-
npmVersion = JSON.parse(
|
|
2117
|
+
npmVersion = JSON.parse(readFileSync10(srcHashPath, "utf-8"))._version ?? "";
|
|
1593
2118
|
} catch {
|
|
1594
2119
|
}
|
|
1595
2120
|
const installedVersion = loadVersionFile(projectRoot, magicDir);
|
|
@@ -1609,13 +2134,13 @@ async function syncCommand(options = {}) {
|
|
|
1609
2134
|
let sameCount = 0;
|
|
1610
2135
|
for (const [relPath, content] of candidates) {
|
|
1611
2136
|
const key = normalizeRelPath(relPath);
|
|
1612
|
-
const absPath =
|
|
1613
|
-
if (!
|
|
2137
|
+
const absPath = resolve9(projectRoot, relPath);
|
|
2138
|
+
if (!existsSync12(absPath)) {
|
|
1614
2139
|
missingFiles.set(key, content);
|
|
1615
2140
|
} else if (establishBaseline) {
|
|
1616
2141
|
missingFiles.set(key, content);
|
|
1617
2142
|
} else {
|
|
1618
|
-
const curH = hash8(
|
|
2143
|
+
const curH = hash8(readFileSync10(absPath, "utf-8"));
|
|
1619
2144
|
const storedH = outHash[key];
|
|
1620
2145
|
if (storedH && curH === storedH) {
|
|
1621
2146
|
sameCount++;
|
|
@@ -1643,18 +2168,19 @@ async function syncCommand(options = {}) {
|
|
|
1643
2168
|
}
|
|
1644
2169
|
const finalHash = mergeFullHash(
|
|
1645
2170
|
outHash,
|
|
1646
|
-
[...candidates].map(([relPath]) => ({ relPath, absPath:
|
|
1647
|
-
(absPath) =>
|
|
2171
|
+
[...candidates].map(([relPath]) => ({ relPath, absPath: resolve9(projectRoot, relPath) })),
|
|
2172
|
+
(absPath) => existsSync12(absPath) ? hash8(readFileSync10(absPath, "utf-8")) : null
|
|
1648
2173
|
);
|
|
1649
2174
|
saveHashFile(projectRoot, magicDir, finalHash);
|
|
1650
2175
|
saveVersionFile(projectRoot, magicDir, npmVersion);
|
|
1651
2176
|
await checkPrismaDiff(projectRoot, options);
|
|
2177
|
+
ensurePortsContract(projectRoot, config);
|
|
1652
2178
|
await maybeModelDownload(options);
|
|
1653
2179
|
return;
|
|
1654
2180
|
}
|
|
1655
2181
|
const missing = /* @__PURE__ */ new Map();
|
|
1656
2182
|
for (const [relPath, content] of allFiles) {
|
|
1657
|
-
if (!
|
|
2183
|
+
if (!existsSync12(resolve9(projectRoot, relPath))) {
|
|
1658
2184
|
missing.set(relPath, content);
|
|
1659
2185
|
}
|
|
1660
2186
|
}
|
|
@@ -1673,11 +2199,12 @@ async function syncCommand(options = {}) {
|
|
|
1673
2199
|
const result = await writeFiles2(projectRoot, filesToWrite, { yes: true });
|
|
1674
2200
|
console.log(`\u540C\u6B65\u5B8C\u6210: \u65B0\u5EFA ${result.created}, \u8DF3\u8FC7 ${result.skipped}`);
|
|
1675
2201
|
await checkPrismaDiff(projectRoot, options);
|
|
2202
|
+
ensurePortsContract(projectRoot, config);
|
|
1676
2203
|
await maybeModelDownload(options);
|
|
1677
2204
|
}
|
|
1678
2205
|
function printMigrateGuidance(targetPath, changed) {
|
|
1679
2206
|
const g = SYNC_PRISMA_CONFIG.POST_SYNC;
|
|
1680
|
-
const isManaged =
|
|
2207
|
+
const isManaged = existsSync12(resolve9(dirname6(targetPath), "migrations"));
|
|
1681
2208
|
const actions = isManaged ? g.MANAGED_ACTIONS : g.UNMANAGED_ACTIONS;
|
|
1682
2209
|
console.log(` \u25B6 \u5DF2\u5199\u5165 ${changed} \u5904\u53D8\u66F4\uFF0C${g.HEADER}`);
|
|
1683
2210
|
for (const a of actions) {
|
|
@@ -1697,23 +2224,62 @@ function printMigrateGuidance(targetPath, changed) {
|
|
|
1697
2224
|
}
|
|
1698
2225
|
console.log(` \u26A0\uFE0F \u6240\u6709\u573A\u666F\u6536\u5C3E: ${g.FINAL}`);
|
|
1699
2226
|
}
|
|
2227
|
+
async function ensureAtlasCapability(projectRoot) {
|
|
2228
|
+
const bin = resolveAtlasBin(projectRoot);
|
|
2229
|
+
if (bin) {
|
|
2230
|
+
console.log(` \u2705 Atlas \u80FD\u529B\u5C31\u7EEA: ${bin}`);
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
console.warn(" \u26A0\uFE0F Atlas \u4E0D\u53EF\u7528\u2014\u2014\u6570\u636E\u5E93\u540C\u6B65\uFF08Atlas diff/apply\uFF09\u80FD\u529B\u7F3A\u5931");
|
|
2234
|
+
const a = await ask(" \u662F\u5426\u81EA\u52A8\u5B89\u88C5 @ariga/atlas\uFF08npm \u4F9D\u8D56\uFF0C\u8D70 registry\uFF09\uFF1F[Y/n] ");
|
|
2235
|
+
if (a === "n" || a === "no") {
|
|
2236
|
+
console.log(" \u5DF2\u8DF3\u8FC7\u3002\u6570\u636E\u5E93\u540C\u6B65\u5C06\u964D\u7EA7 prisma-diff\uFF08\u514D shadow\uFF09\uFF1B\u53EF\u968F\u65F6\u8865\u88C5\u6062\u590D Atlas");
|
|
2237
|
+
console.log(" \u6587\u6863: README.md \u2192 \u7AE0\u8282\u300CAtlas \u6570\u636E\u5E93\u540C\u6B65\u80FD\u529B\u300D/ DEVELOPMENT.md \xA7\u4E5D");
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
const pm = existsSync12(resolve9(projectRoot, "pnpm-lock.yaml")) ? "pnpm" : "npm";
|
|
2241
|
+
console.log(` \u5B89\u88C5 ${pm} add -D @ariga/atlas ...`);
|
|
2242
|
+
const r = runCommand(pm, ["add", "-D", "@ariga/atlas"], { cwd: projectRoot, timeout: 18e4 });
|
|
2243
|
+
if (r.status !== 0) {
|
|
2244
|
+
console.error(" \u5B89\u88C5\u5931\u8D25\u3002\u8BF7\u624B\u52A8\u6267\u884C:");
|
|
2245
|
+
console.error(` ${pm} add -D @ariga/atlas`);
|
|
2246
|
+
console.error(" pnpm 11 \u6CE8\u610F: \u9700\u5728 pnpm-workspace.yaml allowBuilds \u653E\u884C '@ariga/atlas': true");
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
console.log(" \u2705 @ariga/atlas \u5DF2\u5B89\u88C5\uFF0CAtlas \u80FD\u529B\u5C31\u7EEA\uFF08node_modules/.bin/atlas\uFF09");
|
|
2250
|
+
}
|
|
2251
|
+
function checkHostAtlasSegment(projectRoot) {
|
|
2252
|
+
const script = resolve9(projectRoot, "scripts", "db-ensure.sh");
|
|
2253
|
+
if (!existsSync12(script)) return;
|
|
2254
|
+
const content = readFileSync10(script, "utf-8");
|
|
2255
|
+
if (content.includes("atlas_sync")) return;
|
|
2256
|
+
console.warn(" \u26A0\uFE0F \u5BBF\u4E3B scripts/db-ensure.sh \u672A\u5305\u542B Atlas \u540C\u6B65\u6BB5\uFF08\u65E5\u5E38 db-ensure \u5C06\u7F3A\u5C11 ADD \u6CBB\u7406\u6A21\u578B\u540C\u6B65\uFF09");
|
|
2257
|
+
console.warn(" \u804C\u8D23\u8FB9\u754C: add-coder \u53EA\u540C\u6B65 ADD \u6CBB\u7406\u6A21\u578B(7 \u8868)\uFF1B\u5BBF\u4E3B\u4E1A\u52A1\u8868 diff \u63A8\u8350 Atlas \u4F46\u4E0D\u5F3A\u6C42");
|
|
2258
|
+
console.warn(" \u5408\u5165\u4E09\u6B65\uFF1A");
|
|
2259
|
+
console.warn(" \u2460 \u590D\u5236\u6A21\u677F Atlas \u6A21\u5757\u6BB5: sed -n '/# \u2550\u2550\u2550\u2550 Atlas \u58F0\u660E\u5F0F\u540C\u6B65\u6A21\u5757/,/^fi$/p' node_modules/add-coder/templates/core/scripts/db-ensure.sh");
|
|
2260
|
+
console.warn(" \u2461 \u53D8\u91CF\u9002\u914D\uFF08DB_URL\u2192DATABASE_URL \u7B49\uFF0C\u89C1\u6587\u6863\u53D8\u91CF\u5BF9\u7167\u8868\uFF09");
|
|
2261
|
+
console.warn(" \u2462 \u7C98\u8D34\u5230\u811A\u672C\u672B\u5C3E\uFF08\u8FC1\u79FB/generate \u4E4B\u540E\uFF09\uFF0C\u89E6\u53D1: bash scripts/db-ensure.sh <engine> <container> --migrate");
|
|
2262
|
+
console.warn(" \u5BBF\u4E3B\u4E1A\u52A1\u8868\u63A8\u8350\u505A\u6CD5: \u89C1 DEVELOPMENT.md \xA7\u4E5D 9.5\uFF08\u63A8\u8350 Atlas \u53EF\u9009\uFF1B\u4FDD\u6301 migrate dev/deploy \u4EA6\u53EF\uFF09");
|
|
2263
|
+
}
|
|
1700
2264
|
async function checkPrismaDiff(projectRoot, options) {
|
|
1701
2265
|
if (!options.patch) return;
|
|
1702
|
-
const basePath =
|
|
1703
|
-
const targetPath =
|
|
1704
|
-
if (!
|
|
2266
|
+
const basePath = resolve9(projectRoot, SYNC_PRISMA_CONFIG.BASE_SCHEMA);
|
|
2267
|
+
const targetPath = resolve9(projectRoot, SYNC_PRISMA_CONFIG.TARGET_PATTERN);
|
|
2268
|
+
if (!existsSync12(basePath)) {
|
|
1705
2269
|
console.log(`
|
|
1706
2270
|
\u26A0\uFE0F \u57FA\u51C6 schema \u4E0D\u5B58\u5728: ${basePath}`);
|
|
1707
2271
|
console.log(` \u8BF7\u786E\u4FDD add-coder \u5DF2\u6B63\u786E\u5B89\u88C5\u3002`);
|
|
1708
2272
|
return;
|
|
1709
2273
|
}
|
|
2274
|
+
await ensureAtlasCapability(projectRoot);
|
|
2275
|
+
checkHostAtlasSegment(projectRoot);
|
|
1710
2276
|
const result = diffPrisma(basePath, targetPath);
|
|
1711
2277
|
if (!result.hasDiff) {
|
|
1712
2278
|
console.log(`
|
|
1713
2279
|
\u2705 Prisma schema \u4E0E add-coder \u6807\u51C6\u4E00\u81F4\uFF0C\u65E0\u9700\u540C\u6B65\u3002`);
|
|
1714
2280
|
return;
|
|
1715
2281
|
}
|
|
1716
|
-
const targetExists =
|
|
2282
|
+
const targetExists = existsSync12(targetPath);
|
|
1717
2283
|
let modifiedCount = 0;
|
|
1718
2284
|
console.log(`
|
|
1719
2285
|
\u26A0\uFE0F Prisma schema \u5DEE\u5F02\u68C0\u6D4B:`);
|
|
@@ -1832,8 +2398,8 @@ async function checkPrismaDiff(projectRoot, options) {
|
|
|
1832
2398
|
} else {
|
|
1833
2399
|
console.log(`
|
|
1834
2400
|
\u76EE\u6807 schema \u6587\u4EF6\u4E0D\u5B58\u5728: ${result.targetPath}`);
|
|
1835
|
-
|
|
1836
|
-
|
|
2401
|
+
mkdirSync7(dirname6(targetPath), { recursive: true });
|
|
2402
|
+
writeFileSync7(targetPath, "// add.prisma \u2014 ADD \u6CBB\u7406\u6A21\u578B\n\n", "utf-8");
|
|
1837
2403
|
const n = injectMissingModels(targetPath, result.missing);
|
|
1838
2404
|
console.log(` \u2705 \u5DF2\u521B\u5EFA\u5E76\u6CE8\u5165 ${n} \u4E2A\u6A21\u578B/\u679A\u4E3E`);
|
|
1839
2405
|
if (n > 0) {
|
|
@@ -1864,7 +2430,7 @@ async function handleDiffAction(action, ctx) {
|
|
|
1864
2430
|
}
|
|
1865
2431
|
}
|
|
1866
2432
|
function injectMissingModels(targetPath, models) {
|
|
1867
|
-
let content =
|
|
2433
|
+
let content = readFileSync10(targetPath, "utf-8");
|
|
1868
2434
|
content = content.replace(/\n*$/, "\n");
|
|
1869
2435
|
content += `
|
|
1870
2436
|
// ===== \u7531 add-coder sync --patch \u81EA\u52A8\u6CE8\u5165 (${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}) =====
|
|
@@ -1873,11 +2439,11 @@ function injectMissingModels(targetPath, models) {
|
|
|
1873
2439
|
for (const m of models) {
|
|
1874
2440
|
content += m.body + "\n\n";
|
|
1875
2441
|
}
|
|
1876
|
-
|
|
2442
|
+
writeFileSync7(targetPath, content, "utf-8");
|
|
1877
2443
|
return models.length;
|
|
1878
2444
|
}
|
|
1879
2445
|
function getBaseFieldLines(basePath, modelName) {
|
|
1880
|
-
const content =
|
|
2446
|
+
const content = readFileSync10(basePath, "utf-8");
|
|
1881
2447
|
const blocks = parseSchemaBlocks(content);
|
|
1882
2448
|
const block = blocks.get(`model:${modelName}`) ?? blocks.get(`enum:${modelName}`);
|
|
1883
2449
|
if (!block) return {};
|
|
@@ -1900,7 +2466,7 @@ function injectFieldLines(targetPath, basePath, modelName, fieldKeys) {
|
|
|
1900
2466
|
console.warn(`\u26A0\uFE0F \u6CE8\u5165\u5931\u8D25\uFF1A${modelName} \u5728\u57FA\u51C6\u4E2D\u672A\u627E\u5230\u5B57\u6BB5\u5B9A\u4E49\uFF08${fieldKeys.length} \u4E2A\u5B57\u6BB5\u672A\u5199\u5165\uFF09`);
|
|
1901
2467
|
return 0;
|
|
1902
2468
|
}
|
|
1903
|
-
const content =
|
|
2469
|
+
const content = readFileSync10(targetPath, "utf-8");
|
|
1904
2470
|
const lines = content.split("\n");
|
|
1905
2471
|
const blocks = parseSchemaBlocks(content);
|
|
1906
2472
|
const target = blocks.get(`model:${modelName}`) ?? blocks.get(`enum:${modelName}`);
|
|
@@ -1941,13 +2507,13 @@ function injectFieldLines(targetPath, basePath, modelName, fieldKeys) {
|
|
|
1941
2507
|
...newFieldLines,
|
|
1942
2508
|
...lines.slice(insertIdx)
|
|
1943
2509
|
];
|
|
1944
|
-
|
|
2510
|
+
writeFileSync7(targetPath, merged.join("\n"), "utf-8");
|
|
1945
2511
|
return newFieldLines.length;
|
|
1946
2512
|
}
|
|
1947
2513
|
function overwriteFieldLines(targetPath, basePath, modelName, conflicts) {
|
|
1948
2514
|
const baseFields = getBaseFieldLines(basePath, modelName);
|
|
1949
2515
|
if (Object.keys(baseFields).length === 0) return 0;
|
|
1950
|
-
let content =
|
|
2516
|
+
let content = readFileSync10(targetPath, "utf-8");
|
|
1951
2517
|
let count = 0;
|
|
1952
2518
|
for (const { fieldName } of conflicts) {
|
|
1953
2519
|
const baseLine = baseFields[fieldName];
|
|
@@ -1958,13 +2524,13 @@ function overwriteFieldLines(targetPath, basePath, modelName, conflicts) {
|
|
|
1958
2524
|
content = content.replace(fieldRegex, `${match[1]}${baseLine}`);
|
|
1959
2525
|
count++;
|
|
1960
2526
|
}
|
|
1961
|
-
if (count > 0)
|
|
2527
|
+
if (count > 0) writeFileSync7(targetPath, content, "utf-8");
|
|
1962
2528
|
return count;
|
|
1963
2529
|
}
|
|
1964
2530
|
|
|
1965
2531
|
// src/cli/commands/status.ts
|
|
1966
|
-
import { existsSync as
|
|
1967
|
-
import { resolve as
|
|
2532
|
+
import { existsSync as existsSync13 } from "fs";
|
|
2533
|
+
import { resolve as resolve10 } from "path";
|
|
1968
2534
|
async function statusCommand() {
|
|
1969
2535
|
const projectRoot = process.cwd();
|
|
1970
2536
|
const config = await loadConfig(projectRoot);
|
|
@@ -1973,7 +2539,7 @@ async function statusCommand() {
|
|
|
1973
2539
|
const missing = [];
|
|
1974
2540
|
const present = [];
|
|
1975
2541
|
for (const [relPath] of coreFiles) {
|
|
1976
|
-
if (
|
|
2542
|
+
if (existsSync13(resolve10(projectRoot, relPath))) {
|
|
1977
2543
|
present.push(relPath);
|
|
1978
2544
|
} else {
|
|
1979
2545
|
missing.push(relPath);
|
|
@@ -1991,8 +2557,8 @@ async function statusCommand() {
|
|
|
1991
2557
|
}
|
|
1992
2558
|
|
|
1993
2559
|
// src/cli/commands/stack.ts
|
|
1994
|
-
import { readdirSync as
|
|
1995
|
-
import { resolve as
|
|
2560
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
|
|
2561
|
+
import { resolve as resolve11, join as join8 } from "path";
|
|
1996
2562
|
import { createHash as createHash3 } from "crypto";
|
|
1997
2563
|
var MAGIC_DIR_MAP3 = { claude: ".claude", qoder: ".qoder", vscode: ".vscode", trae: ".trae", codex: ".codex" };
|
|
1998
2564
|
var HASH_OUTPUT_FILE = ".add-coder-hash.json";
|
|
@@ -2009,14 +2575,14 @@ function resolveMagicDir(projectRoot, specified) {
|
|
|
2009
2575
|
return MAGIC_DIR_MAP3[adapter];
|
|
2010
2576
|
}
|
|
2011
2577
|
function listCustomProfiles(projectRoot, magicDir, registryNames) {
|
|
2012
|
-
const dir =
|
|
2013
|
-
if (!
|
|
2014
|
-
return
|
|
2578
|
+
const dir = resolve11(projectRoot, magicDir, "rules", "profiles");
|
|
2579
|
+
if (!existsSync14(dir)) return [];
|
|
2580
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".md") && !registryNames.has(f.replace(/-profile\.md$/, ""))).sort();
|
|
2015
2581
|
}
|
|
2016
2582
|
function profileExists(projectRoot, magicDir, name) {
|
|
2017
2583
|
const registry = loadProfileRegistry();
|
|
2018
2584
|
if (registry.some((p) => p.name === name)) return true;
|
|
2019
|
-
return
|
|
2585
|
+
return existsSync14(resolve11(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`));
|
|
2020
2586
|
}
|
|
2021
2587
|
function stackCommand(sub, name, options = {}) {
|
|
2022
2588
|
const projectRoot = process.cwd();
|
|
@@ -2053,12 +2619,12 @@ function stackCommand(sub, name, options = {}) {
|
|
|
2053
2619
|
console.log("\u6280\u672F\u6808: \u672A\u8BBE\u7F6E\uFF08\u4E2D\u6027\uFF0C\u65E0\u6280\u672F\u6808\u5047\u8BBE\uFF09");
|
|
2054
2620
|
return;
|
|
2055
2621
|
}
|
|
2056
|
-
const profilePath =
|
|
2057
|
-
const stat =
|
|
2622
|
+
const profilePath = resolve11(projectRoot, magicDir, "rules", "profiles", `${current}-profile.md`);
|
|
2623
|
+
const stat = existsSync14(profilePath) ? readFileSync11(profilePath, "utf-8").length : 0;
|
|
2058
2624
|
console.log(`\u6280\u672F\u6808: ${current}`);
|
|
2059
|
-
console.log(`profile \u6587\u4EF6: ${profilePath}${
|
|
2625
|
+
console.log(`profile \u6587\u4EF6: ${profilePath}${existsSync14(profilePath) ? ` (${stat} \u5B57\u7B26)` : "\uFF08\u7F3A\u5931\uFF09"}`);
|
|
2060
2626
|
try {
|
|
2061
|
-
const raw = JSON.parse(
|
|
2627
|
+
const raw = JSON.parse(readFileSync11(resolve11(projectRoot, magicDir, "stack.json"), "utf-8"));
|
|
2062
2628
|
if (raw.updatedAt) console.log(`\u66F4\u65B0\u65F6\u95F4: ${raw.updatedAt}`);
|
|
2063
2629
|
} catch {
|
|
2064
2630
|
}
|
|
@@ -2083,9 +2649,9 @@ function buildConfig(projectRoot, magicDir, stack) {
|
|
|
2083
2649
|
stack
|
|
2084
2650
|
};
|
|
2085
2651
|
try {
|
|
2086
|
-
const pkgPath =
|
|
2087
|
-
if (
|
|
2088
|
-
const pkg = JSON.parse(
|
|
2652
|
+
const pkgPath = resolve11(projectRoot, "package.json");
|
|
2653
|
+
if (existsSync14(pkgPath)) {
|
|
2654
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
2089
2655
|
if (pkg.name) config.projectName = pkg.name;
|
|
2090
2656
|
}
|
|
2091
2657
|
} catch {
|
|
@@ -2111,15 +2677,15 @@ function applyStack(projectRoot, magicDir, name) {
|
|
|
2111
2677
|
}
|
|
2112
2678
|
const hashMap = {};
|
|
2113
2679
|
try {
|
|
2114
|
-
Object.assign(hashMap, JSON.parse(
|
|
2680
|
+
Object.assign(hashMap, JSON.parse(readFileSync11(resolve11(projectRoot, magicDir, HASH_OUTPUT_FILE), "utf-8")));
|
|
2115
2681
|
} catch {
|
|
2116
2682
|
}
|
|
2117
2683
|
let written = 0;
|
|
2118
2684
|
for (const [relPath, content] of stackRelated) {
|
|
2119
2685
|
for (const t of [".add", magicDir]) {
|
|
2120
|
-
const targetPath =
|
|
2121
|
-
|
|
2122
|
-
|
|
2686
|
+
const targetPath = resolve11(projectRoot, relPath.replace(/^\.add/, t));
|
|
2687
|
+
mkdirSync8(join8(targetPath, ".."), { recursive: true });
|
|
2688
|
+
writeFileSync8(targetPath, content, "utf-8");
|
|
2123
2689
|
hashMap[relPath.replace(/^\.add/, t)] = hash82(content);
|
|
2124
2690
|
written++;
|
|
2125
2691
|
}
|
|
@@ -2130,24 +2696,24 @@ function applyStack(projectRoot, magicDir, name) {
|
|
|
2130
2696
|
};
|
|
2131
2697
|
const registry = loadProfileRegistry();
|
|
2132
2698
|
const isBuiltin = registry.some((p) => p.name === name);
|
|
2133
|
-
const profilePathMagic =
|
|
2134
|
-
const profilePathAdd =
|
|
2135
|
-
const projectRulesPath =
|
|
2136
|
-
const projectRulesContent =
|
|
2699
|
+
const profilePathMagic = resolve11(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`);
|
|
2700
|
+
const profilePathAdd = resolve11(projectRoot, ".add", "rules", "profiles", `${name}-profile.md`);
|
|
2701
|
+
const projectRulesPath = resolve11(projectRoot, magicDir, "rules", "project_rules.md");
|
|
2702
|
+
const projectRulesContent = existsSync14(projectRulesPath) ? readFileSync11(projectRulesPath, "utf-8") : "";
|
|
2137
2703
|
if (written === 0) fail(`\u672A\u6E32\u67D3\u4EFB\u4F55 stack \u76F8\u5173\u6587\u4EF6\uFF08${name}\uFF09\u2014\u2014Windows \u8DEF\u5F84\u5339\u914D\u5931\u6548\u9057\u7559\u95EE\u9898`);
|
|
2138
|
-
if (isBuiltin && !
|
|
2139
|
-
if (!
|
|
2704
|
+
if (isBuiltin && !existsSync14(profilePathAdd)) fail(`profile \u672A\u5199\u5165 .add: ${profilePathAdd}`);
|
|
2705
|
+
if (!existsSync14(profilePathMagic)) fail(`profile \u672A\u5199\u5165 ${magicDir}: ${profilePathMagic}`);
|
|
2140
2706
|
if (!projectRulesContent.includes("**\u5F53\u524D\u6280\u672F\u6808**") || !projectRulesContent.includes(name)) fail(`project_rules.md \u672A\u5305\u542B ${name} \u5F15\u7528`);
|
|
2141
|
-
|
|
2707
|
+
writeFileSync8(resolve11(projectRoot, magicDir, HASH_OUTPUT_FILE), JSON.stringify(hashMap, null, 2) + "\n", "utf-8");
|
|
2142
2708
|
console.log(`\u2705 \u6280\u672F\u6808\u5DF2\u8BBE\u7F6E\u4E3A ${name}`);
|
|
2143
2709
|
console.log(` ${magicDir}/stack.json \u2192 ${name}`);
|
|
2144
|
-
console.log(` ${magicDir}/rules/profiles/${name}-profile.md ${
|
|
2710
|
+
console.log(` ${magicDir}/rules/profiles/${name}-profile.md ${existsSync14(resolve11(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`)) ? "\u5DF2\u5C31\u4F4D" : "\uFF08\u81EA\u5B9A\u4E49 profile\uFF0C\u9879\u76EE\u4FA7\u6587\u4EF6\uFF09"}`);
|
|
2145
2711
|
console.log(` project_rules.md \u5F15\u7528\u884C\u5DF2\u66F4\u65B0 + hash \u5DF2\u5237\u65B0\uFF08${written} \u4E2A\u6587\u4EF6\uFF09`);
|
|
2146
2712
|
}
|
|
2147
2713
|
|
|
2148
2714
|
// src/cli/index.ts
|
|
2149
2715
|
var { version } = JSON.parse(
|
|
2150
|
-
|
|
2716
|
+
readFileSync12(new URL("../package.json", import.meta.url), "utf-8")
|
|
2151
2717
|
);
|
|
2152
2718
|
async function modelDownloadCommand(options) {
|
|
2153
2719
|
try {
|
|
@@ -2162,7 +2728,7 @@ async function modelDownloadCommand(options) {
|
|
|
2162
2728
|
}
|
|
2163
2729
|
var program = new Command();
|
|
2164
2730
|
program.name("add-coder").description("\u521D\u59CB\u5316 ADD \u8303\u5F0F\u5DE5\u4F5C\u6D41\u6A21\u677F").version(version);
|
|
2165
|
-
program.command("init").description("\u521D\u59CB\u5316 ADD \u6A21\u677F\u5230\u5F53\u524D\u9879\u76EE").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode | trae | codex").option("--config <path>", "\u6307\u5B9A\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84").option("--force", "\u8986\u76D6\u5DF2\u6709\u6587\u4EF6\uFF0C\u4E0D\u4EA4\u4E92").option("--dry-run", "\u9884\u89C8\u6A21\u5F0F\uFF0C\u4E0D\u5B9E\u9645\u5199\u5165").option("--stack <name>", "\u6280\u672F\u6808\u7EA6\u675F profile \u540D\uFF08\u5982 machineserver\uFF0C\u53EF\u9009\uFF09").option("--skip-model", "\u8DF3\u8FC7 embedding \u6A21\u578B\u9884\u4E0B\u8F7D").action(initCommand);
|
|
2731
|
+
program.command("init").description("\u521D\u59CB\u5316 ADD \u6A21\u677F\u5230\u5F53\u524D\u9879\u76EE").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode | trae | codex").option("--config <path>", "\u6307\u5B9A\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84").option("--force", "\u8986\u76D6\u5DF2\u6709\u6587\u4EF6\uFF0C\u4E0D\u4EA4\u4E92").option("--dry-run", "\u9884\u89C8\u6A21\u5F0F\uFF0C\u4E0D\u5B9E\u9645\u5199\u5165").option("--stack <name>", "\u6280\u672F\u6808\u7EA6\u675F profile \u540D\uFF08\u5982 machineserver\uFF0C\u53EF\u9009\uFF09").option("--skip-model", "\u8DF3\u8FC7 embedding \u6A21\u578B\u9884\u4E0B\u8F7D").option("--print-mcp-config", "Codex: stdout \u8F93\u51FA config.toml \u7247\u6BB5\uFF08\u4E0D\u5199\u76D8\uFF0C\u4E0D\u521D\u59CB\u5316\u9879\u76EE\uFF09").option("--write-user-config", "Codex: \u663E\u5F0F\u786E\u8BA4\u540E\u5199\u5165 ~/.codex/config.toml\uFF08\u5148\u5907\u4EFD\uFF09").action(initCommand);
|
|
2166
2732
|
program.command("sync").description("\u589E\u91CF\u540C\u6B65\u7F3A\u5931\u6587\u4EF6\uFF08--patch \u8986\u76D6\u5DF2\u6709\u6A21\u677F\uFF09").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode | trae | codex").option("--patch", "\u8986\u76D6\u5DF2\u6709\u6A21\u677F\u6587\u4EF6\uFF08\u4E0D\u78B0 plans/specs/reviews\uFF09").option("-i, --interactive", "\u4EA4\u4E92\u5F0F\u9009\u62E9\u8981\u540C\u6B65\u7684\u6587\u4EF6").option("--model", "\u68C0\u6D4B\u5230\u7F3A\u5931\u65F6\u4E0B\u8F7D embedding \u6A21\u578B").action(syncCommand);
|
|
2167
2733
|
program.command("status").description("\u68C0\u67E5 ADD \u6A21\u677F\u5B8C\u6574\u6027").action(statusCommand);
|
|
2168
2734
|
program.command("stack").description("\u7BA1\u7406\u6280\u672F\u6808\u7EA6\u675F profile\uFF08list / set <name> / show / --clear\uFF09").argument("[sub]", "list | set | show").argument("[name]", "set <name>: profile \u540D\uFF08\u5185\u7F6E\u6216\u81EA\u5B9A\u4E49\uFF09").option("--adapter <type>", "\u76EE\u6807 IDE: claude | qoder | vscode | trae | codex").option("--clear", "\u6E05\u9664\u6280\u672F\u6808\u8BBE\u7F6E\uFF08\u4E2D\u6027\uFF09").action(
|