add-coder 0.3.23 → 0.3.24

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/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 readFileSync10 } from "fs";
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((resolve10) => {
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
- resolve10(result);
228
+ resolve12(result);
229
229
  });
230
230
  });
231
231
  }
@@ -645,12 +645,12 @@ function detectPm(projectRoot) {
645
645
  }
646
646
 
647
647
  // src/cli/prisma-injector.ts
648
- import { resolve as resolve4, dirname as dirname3 } from "path";
648
+ import { resolve as resolve6, dirname as dirname4 } from "path";
649
649
  import { fileURLToPath as fileURLToPath2 } from "url";
650
650
 
651
651
  // src/caijuehub/strategies/prisma.strategy.ts
652
- import { copyFileSync, existsSync as existsSync7, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
653
- import { resolve as resolve3 } from "path";
652
+ import { copyFileSync, existsSync as existsSync9, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
653
+ import { resolve as resolve5, join as join6, dirname as dirname3 } from "path";
654
654
 
655
655
  // src/lib/run-command.ts
656
656
  import { spawnSync } from "child_process";
@@ -680,6 +680,205 @@ function commandExists(cmd, platform) {
680
680
  return !r.error && r.status === 0;
681
681
  }
682
682
 
683
+ // src/lib/db-backup.ts
684
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, readdirSync as readdirSync2, rmSync } from "fs";
685
+ import { join as join5, resolve as resolve3 } from "path";
686
+ var ADD_TABLES = ["AddUser", "DevOperation", "AuditLog", "HitlRecord", "PlanRecord", "ReviewRecord", "CollabContract"];
687
+ function ensureGitignoreRule(projectRoot) {
688
+ const gitignorePath = resolve3(projectRoot, ".gitignore");
689
+ if (!existsSync7(gitignorePath)) return;
690
+ const content = readFileSync5(gitignorePath, "utf-8");
691
+ if (content.includes(".add/backups/")) return;
692
+ writeFileSync3(gitignorePath, `${content}${content.endsWith("\n") ? "" : "\n"}
693
+ # add-coder \u6570\u636E\u5E93\u540C\u6B65\u5907\u4EFD
694
+ .add/backups/
695
+ `, "utf-8");
696
+ console.log("\u{1F4CB} \u5DF2\u6CE8\u5165\u5BBF\u4E3B .gitignore: .add/backups/");
697
+ }
698
+ async function backupBeforeSync(projectRoot, opts) {
699
+ const dir = resolve3(projectRoot, opts.backupDir);
700
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
701
+ const bakDir = join5(dir, ts);
702
+ ensureGitignoreRule(projectRoot);
703
+ if (opts.datasource === "sqlite" || opts.dbUrl.startsWith("file:")) {
704
+ const dbFile = opts.dbUrl.replace(/^file:/, "");
705
+ if (!existsSync7(dbFile)) return handleBackupFailure(opts, `sqlite \u6587\u4EF6\u4E0D\u5B58\u5728: ${dbFile}`);
706
+ mkdirSync3(bakDir, { recursive: true });
707
+ const copy = join5(bakDir, "dev.db");
708
+ writeFileSync3(copy, readFileSync5(dbFile), "utf-8");
709
+ writeFileSync3(join5(bakDir, "manifest.json"), JSON.stringify({ createdAt: ts, dbUrl: opts.dbUrl, files: ["dev.db"] }, null, 2), "utf-8");
710
+ console.log(`\u2705 \u5907\u4EFD\u5B8C\u6210: ${bakDir}`);
711
+ pruneBackups(dir, opts.backupKeep);
712
+ return bakDir;
713
+ }
714
+ if (!commandExists("pg_dump")) {
715
+ return handleBackupFailure(opts, "pg_dump \u672A\u5B89\u88C5\uFF08postgresql-client \u7F3A\u5931\uFF09");
716
+ }
717
+ mkdirSync3(bakDir, { recursive: true });
718
+ const schemaFile = join5(bakDir, "schema.sql");
719
+ const tablesFile = join5(bakDir, "add-tables.sql");
720
+ const r1 = runCommand("pg_dump", ["--schema-only", "--no-owner", `--dbname=${opts.dbUrl}`], { timeout: 6e4 });
721
+ if (r1.status !== 0) {
722
+ return handleBackupFailure(opts, `pg_dump schema \u5931\u8D25: ${r1.stderr.trim().split("\n").slice(0, 3).join(" | ")}`);
723
+ }
724
+ writeFileSync3(schemaFile, r1.stdout, "utf-8");
725
+ const r2 = runCommand("pg_dump", ["--no-owner", "--if-exists", `--dbname=${opts.dbUrl}`, ...ADD_TABLES.map((t) => `--table=${t}`)], { timeout: 6e4 });
726
+ if (r2.status !== 0) {
727
+ return handleBackupFailure(opts, `pg_dump ADD \u8868\u5931\u8D25: ${r2.stderr.trim().split("\n").slice(0, 3).join(" | ")}`);
728
+ }
729
+ writeFileSync3(tablesFile, r2.stdout, "utf-8");
730
+ writeFileSync3(join5(bakDir, "manifest.json"), JSON.stringify({ createdAt: ts, dbUrl: opts.dbUrl, files: ["schema.sql", "add-tables.sql"] }, null, 2), "utf-8");
731
+ console.log(`\u2705 \u5907\u4EFD\u5B8C\u6210: ${bakDir}`);
732
+ pruneBackups(dir, opts.backupKeep);
733
+ return bakDir;
734
+ }
735
+ async function handleBackupFailure(opts, reason) {
736
+ if (opts.yes) {
737
+ throw new Error(`\u26D4 \u5907\u4EFD\u5931\u8D25\uFF08--yes \u786C\u963B\u65AD\uFF0C\u540C\u6B65\u4E2D\u6B62\uFF09: ${reason}`);
738
+ }
739
+ const a = await ask(`
740
+ \u26A0\uFE0F \u5907\u4EFD\u5931\u8D25\uFF08${reason}\uFF09\u3002\u81EA\u62C5\u98CE\u9669\u7EE7\u7EED\u540C\u6B65\uFF1F[y/N] `);
741
+ if (a !== "y" && a !== "yes") {
742
+ throw new Error(`\u5907\u4EFD\u5931\u8D25\uFF0C\u5DF2\u4E2D\u6B62\u540C\u6B65: ${reason}`);
743
+ }
744
+ console.warn("\u26A0\uFE0F \u7528\u6237\u63A5\u53D7\u81EA\u62C5\u98CE\u9669\u7EE7\u7EED\uFF08manifest \u8BB0 riskAccepted\uFF09");
745
+ return null;
746
+ }
747
+ function pruneBackups(dir, backupKeep) {
748
+ if (!existsSync7(dir)) return;
749
+ const entries = readdirSync2(dir).filter((n) => /^\d{4}-\d{2}-\d{2}T/.test(n)).sort();
750
+ const excess = entries.length - backupKeep;
751
+ for (let i = 0; i < excess; i++) {
752
+ rmSync(join5(dir, entries[i]), { recursive: true, force: true });
753
+ console.log(`\u{1F9F9} \u6E05\u7406\u65E7\u5907\u4EFD: ${entries[i]}`);
754
+ }
755
+ }
756
+
757
+ // src/lib/ports-contract.ts
758
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync3 } from "fs";
759
+ import { resolve as resolve4, basename } from "path";
760
+
761
+ // src/caijuehub/strategies/ports.strategy.ts
762
+ var PORTS_CONFIG = {
763
+ pg: {
764
+ startHint: 5433,
765
+ scanLimit: 100
766
+ },
767
+ behavior: {
768
+ reuseRegistered: true,
769
+ readCrossProject: true,
770
+ onConflict: "ask"
771
+ }
772
+ };
773
+
774
+ // src/lib/ports-contract.ts
775
+ import { createConnection } from "net";
776
+ var PORTS_EXAMPLE_REL = "../templates/core/templates/ports.example.md";
777
+ function ensurePortsContract(projectRoot, config, dryRun = false) {
778
+ const target = resolve4(projectRoot, "docs", "ports.md");
779
+ if (existsSync8(target)) return;
780
+ let content;
781
+ try {
782
+ content = readFileSync6(resolve4(import.meta.dirname, PORTS_EXAMPLE_REL), "utf-8");
783
+ } catch {
784
+ console.warn(`\u26A0\uFE0F \u7AEF\u53E3\u5951\u7EA6\u6A21\u677F\u7F3A\u5931\uFF08\u8DF3\u8FC7\u751F\u6210 docs/ports.md\uFF09: ${PORTS_EXAMPLE_REL}`);
785
+ return;
786
+ }
787
+ const pn = config.projectName || "add-project";
788
+ const rendered = render(content, { ...config, projectName: pn });
789
+ if (dryRun) {
790
+ console.log("[dry-run] \u5C06\u751F\u6210 docs/ports.md");
791
+ return;
792
+ }
793
+ mkdirSync4(resolve4(projectRoot, "docs"), { recursive: true });
794
+ writeFileSync4(target, rendered, "utf-8");
795
+ 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");
796
+ }
797
+ function portInUse(port) {
798
+ return new Promise((r) => {
799
+ const s = createConnection({ port, host: "127.0.0.1" }, () => {
800
+ s.destroy();
801
+ r(true);
802
+ });
803
+ s.on("error", () => r(false));
804
+ });
805
+ }
806
+ function parseContractPorts(content) {
807
+ const out = [];
808
+ const re = /^\|\s*(\d{2,5})\s*\|/gm;
809
+ let m;
810
+ while ((m = re.exec(content)) !== null) out.push(parseInt(m[1], 10));
811
+ return out;
812
+ }
813
+ function readRegisteredPorts(projectRoot) {
814
+ const p = resolve4(projectRoot, "docs", "ports.md");
815
+ if (!existsSync8(p)) return /* @__PURE__ */ new Set();
816
+ return new Set(parseContractPorts(readFileSync6(p, "utf-8")));
817
+ }
818
+ function readCrossProjectPorts(projectRoot) {
819
+ const ports = /* @__PURE__ */ new Set();
820
+ const parent = resolve4(projectRoot, "..");
821
+ let names = [];
822
+ try {
823
+ names = readdirSync3(parent, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== basename(projectRoot)).map((d) => d.name);
824
+ } catch {
825
+ return ports;
826
+ }
827
+ for (const name of names) {
828
+ const p = resolve4(parent, name, "docs", "ports.md");
829
+ if (existsSync8(p)) {
830
+ parseContractPorts(readFileSync6(p, "utf-8")).forEach((x) => ports.add(x));
831
+ }
832
+ }
833
+ return ports;
834
+ }
835
+ function scanPodmanPorts() {
836
+ const ports = /* @__PURE__ */ new Set();
837
+ if (!commandExists("podman")) return ports;
838
+ try {
839
+ const r = runCommand("podman", ["ps", "--format", "{{.Ports}}"]);
840
+ const re = /(\d{1,5})->\d{1,5}\/tcp/g;
841
+ let m;
842
+ while ((m = re.exec(r.stdout)) !== null) ports.add(parseInt(m[1], 10));
843
+ } catch {
844
+ }
845
+ return ports;
846
+ }
847
+ function registerPort(projectRoot, port, svc) {
848
+ const contract = resolve4(projectRoot, "docs", "ports.md");
849
+ if (!existsSync8(contract)) return;
850
+ const env = svc.envKey ? ` \`${svc.envKey}\`` : "";
851
+ 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} |
852
+ `;
853
+ writeFileSync4(contract, readFileSync6(contract, "utf-8") + line, "utf-8");
854
+ console.log(`\u{1F4CB} \u7AEF\u53E3\u5951\u7EA6\u767B\u8BB0: ${port}\uFF08${svc.containerName}\uFF09\u2192 docs/ports.md`);
855
+ }
856
+ async function allocatePortsWithContract(projectRoot, services) {
857
+ const cfg = PORTS_CONFIG;
858
+ const used = /* @__PURE__ */ new Set();
859
+ readRegisteredPorts(projectRoot).forEach((p) => used.add(p));
860
+ if (cfg.behavior.readCrossProject) readCrossProjectPorts(projectRoot).forEach((p) => used.add(p));
861
+ scanPodmanPorts().forEach((p) => used.add(p));
862
+ const result = {};
863
+ for (const svc of services) {
864
+ let port = 0;
865
+ for (let p = cfg.pg.startHint; p < cfg.pg.startHint + cfg.pg.scanLimit; p++) {
866
+ if (used.has(p)) continue;
867
+ if (await portInUse(p)) {
868
+ used.add(p);
869
+ continue;
870
+ }
871
+ port = p;
872
+ break;
873
+ }
874
+ 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`);
875
+ used.add(port);
876
+ result[svc.name] = port;
877
+ registerPort(projectRoot, port, svc);
878
+ }
879
+ return result;
880
+ }
881
+
683
882
  // src/caijuehub/strategies/prisma.strategy.ts
684
883
  var PRISMA_CONFIG = {
685
884
  onMissing: "ask",
@@ -688,11 +887,295 @@ var PRISMA_CONFIG = {
688
887
  autoGenerate: true,
689
888
  migrationName: "add_workflow_init",
690
889
  schemaArg: "--schema=prisma/",
691
- requiresUserModel: true
890
+ requiresUserModel: true,
891
+ sync: {
892
+ strategy: "atlas",
893
+ addDatabaseUrl: "",
894
+ atlasDevUrl: "",
895
+ backupDir: ".add/backups/prisma-sync",
896
+ backupKeep: 5,
897
+ backupRequiredForPush: true
898
+ }
692
899
  };
900
+ var ADD_TABLES2 = ["AddUser", "DevOperation", "AuditLog", "HitlRecord", "PlanRecord", "ReviewRecord", "CollabContract"];
901
+ function readEnvValue(projectRoot, key) {
902
+ const envPath = resolve5(projectRoot, ".env.development");
903
+ if (!existsSync9(envPath)) return "";
904
+ const m = readFileSync7(envPath, "utf-8").match(new RegExp(`^${key}=(.*)$`, "m"));
905
+ return m?.[1]?.trim() || "";
906
+ }
907
+ function appendEnvValue(projectRoot, key, value) {
908
+ const envPath = resolve5(projectRoot, ".env.development");
909
+ const existing = existsSync9(envPath) ? readFileSync7(envPath, "utf-8") : "";
910
+ if (new RegExp(`^${key}=`, "m").test(existing)) return;
911
+ writeFileSync5(envPath, `${existing}${existing.endsWith("\n") ? "" : "\n"}${key}=${value}
912
+ `, "utf-8");
913
+ }
914
+ async function ensureSplitDb(projectRoot, opts) {
915
+ const existing = readEnvValue(projectRoot, "ADD_DATABASE_URL");
916
+ if (existing) {
917
+ console.log("\u5206\u5E93\u6A21\u5F0F\uFF1A\u68C0\u6D4B\u5230 ADD_DATABASE_URL\uFF0CADD \u6CBB\u7406\u6A21\u578B\u5165\u72EC\u7ACB\u5E93");
918
+ return true;
919
+ }
920
+ if (opts.dryRun) {
921
+ console.log("[dry-run] \u5C06\u8BE2\u95EE\u662F\u5426\u5206\u5E93\uFF08\u72EC\u7ACB ADD \u6570\u636E\u5E93\uFF0C\u63A8\u8350\uFF09");
922
+ return false;
923
+ }
924
+ let choice = "y";
925
+ if (!opts.force && !opts.yes) {
926
+ 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] ");
927
+ if (a === "n" || a === "no") choice = "n";
928
+ }
929
+ if (choice === "n") {
930
+ 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");
931
+ return false;
932
+ }
933
+ const projectName = readEnvValue(projectRoot, "PROJECT_NAME") || "add-project";
934
+ const dbUser = readEnvValue(projectRoot, "DATABASE_USER") || "admin";
935
+ const dbPass = readEnvValue(projectRoot, "DATABASE_PASSWORD") || "change-me-in-production";
936
+ const dbName = `${projectName}-add`;
937
+ const container = `${projectName}-add-postgres`;
938
+ const port = (await allocatePortsWithContract(projectRoot, [{ name: "add", containerName: `${projectName}-add-postgres`, envKey: "ADD_DATABASE_URL" }])).add;
939
+ console.log(`\u542F\u52A8\u72EC\u7ACB ADD \u5E93\u5BB9\u5668 ${container}\uFF08\u7AEF\u53E3 ${port}\uFF09...`);
940
+ const r = runCommand("podman", [
941
+ "run",
942
+ "-d",
943
+ "--name",
944
+ container,
945
+ "--restart",
946
+ "unless-stopped",
947
+ "-e",
948
+ `POSTGRES_USER=${dbUser}`,
949
+ "-e",
950
+ `POSTGRES_PASSWORD=${dbPass}`,
951
+ "-e",
952
+ `POSTGRES_DB=${dbName}`,
953
+ "-p",
954
+ `127.0.0.1:${port}:5432`,
955
+ "docker.io/postgres:16-alpine"
956
+ ], { timeout: 6e4 });
957
+ if (r.status !== 0) {
958
+ 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)}`);
959
+ }
960
+ appendEnvValue(projectRoot, "ADD_DATABASE_URL", `postgresql://${dbUser}:${dbPass}@127.0.0.1:${port}/${dbName}?schema=public`);
961
+ const addPrismaPath = resolve5(projectRoot, "prisma", "add.prisma");
962
+ if (existsSync9(addPrismaPath)) {
963
+ const content = readFileSync7(addPrismaPath, "utf-8");
964
+ if (!content.includes("datasource db")) {
965
+ writeFileSync5(addPrismaPath, `datasource db {
966
+ provider = "postgresql"
967
+ url = env("ADD_DATABASE_URL")
968
+ }
969
+
970
+ ${content}`, "utf-8");
971
+ }
972
+ }
973
+ console.log(`\u2705 \u5DF2\u521B\u5EFA\u72EC\u7ACB ADD \u5E93 ${container}\uFF08\u7AEF\u53E3 ${port}\uFF09\uFF0CADD_DATABASE_URL \u5DF2\u5199\u5165 .env.development`);
974
+ return true;
975
+ }
976
+ function prismaDiffArgs(pm, schemaTo, fromUrl) {
977
+ const base = pm === "pnpm" ? ["dlx", "prisma", "migrate", "diff"] : ["exec", "prisma", "--", "migrate", "diff"];
978
+ const toFlag = "--to-schema";
979
+ const args = [...base, "--from-empty", toFlag, schemaTo, "--script"];
980
+ if (fromUrl) args.splice(args.indexOf("--from-empty"), 1, "--from-url", fromUrl);
981
+ return args;
982
+ }
983
+ async function provisionDevUrl(projectRoot) {
984
+ const existing = readEnvValue(projectRoot, "ATLAS_DEV_URL");
985
+ if (existing) return existing;
986
+ if (!commandExists("podman")) return null;
987
+ const projectName = readEnvValue(projectRoot, "PROJECT_NAME") || "add-project";
988
+ const container = `${projectName}-add-dev`;
989
+ try {
990
+ const ps = runCommand("podman", ["ps", "--filter", `name=^/${container}$`, "--format", "{{.Names}}"]);
991
+ if (ps.stdout.trim()) {
992
+ const portR = runCommand("podman", ["port", container, "5432/tcp"]);
993
+ const pm = portR.stdout.match(/127\.0\.0\.1:(\d{2,5})/);
994
+ if (pm) {
995
+ const url2 = `postgresql://postgres:postgres@127.0.0.1:${pm[1]}/dev?schema=public`;
996
+ appendEnvValue(projectRoot, "ATLAS_DEV_URL", url2);
997
+ return url2;
998
+ }
999
+ }
1000
+ } catch {
1001
+ }
1002
+ const ports = await allocatePortsWithContract(projectRoot, [{ name: "dev", containerName: container, envKey: "ATLAS_DEV_URL" }]);
1003
+ const port = ports.dev;
1004
+ const r = runCommand("podman", [
1005
+ "run",
1006
+ "-d",
1007
+ "--name",
1008
+ container,
1009
+ "--restart",
1010
+ "unless-stopped",
1011
+ "-e",
1012
+ "POSTGRES_USER=postgres",
1013
+ "-e",
1014
+ "POSTGRES_PASSWORD=postgres",
1015
+ "-e",
1016
+ "POSTGRES_DB=dev",
1017
+ "-p",
1018
+ `127.0.0.1:${port}:5432`,
1019
+ "docker.io/postgres:16-alpine"
1020
+ ], { timeout: 6e4 });
1021
+ if (r.status !== 0) return null;
1022
+ const url = `postgresql://postgres:postgres@127.0.0.1:${port}/dev?schema=public`;
1023
+ appendEnvValue(projectRoot, "ATLAS_DEV_URL", url);
1024
+ 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`);
1025
+ return url;
1026
+ }
1027
+ function resolveAtlasBin(projectRoot) {
1028
+ const pkgBin = resolve5(projectRoot, "node_modules", "add-coder", "node_modules", ".bin", "atlas");
1029
+ if (existsSync9(pkgBin)) return pkgBin;
1030
+ const local = resolve5(projectRoot, "node_modules", ".bin", "atlas");
1031
+ if (existsSync9(local)) return local;
1032
+ return commandExists("atlas") ? "atlas" : null;
1033
+ }
1034
+ function hasNonAddTableChanges(diffSql) {
1035
+ const re = /(?:DROP|ALTER|CREATE)\s+TABLE(?:\s+IF\s+EXISTS)?\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi;
1036
+ let m;
1037
+ while ((m = re.exec(diffSql)) !== null) {
1038
+ if (!ADD_TABLES2.includes(m[1])) return true;
1039
+ }
1040
+ return false;
1041
+ }
1042
+ function buildExcludeArgs(projectRoot, splitDb) {
1043
+ if (splitDb) return [];
1044
+ const projectName = readEnvValue(projectRoot, "PROJECT_NAME") || "add-project";
1045
+ const dbUser = readEnvValue(projectRoot, "DATABASE_USER") || "admin";
1046
+ try {
1047
+ const r = runCommand("podman", [
1048
+ "exec",
1049
+ `${projectName}-postgres`,
1050
+ "psql",
1051
+ "-U",
1052
+ dbUser,
1053
+ "-d",
1054
+ projectName,
1055
+ "-tAc",
1056
+ "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');"
1057
+ ], { timeout: 3e4 });
1058
+ const tables = r.stdout.trim();
1059
+ return tables ? ["--exclude", tables] : [];
1060
+ } catch {
1061
+ return [];
1062
+ }
1063
+ }
1064
+ async function runAtlasSync(projectRoot, targetUrl, splitDb, sync, opts) {
1065
+ const pm = detectPm(projectRoot);
1066
+ const schemaTo = splitDb ? resolve5(projectRoot, "prisma", "add.prisma") : resolve5(projectRoot, "prisma");
1067
+ const baselinePath = join6(projectRoot, PRISMA_CONFIG.sync.backupDir, "baseline.sql");
1068
+ mkdirSync5(dirname3(baselinePath), { recursive: true });
1069
+ const diffR = runCommand(pm, prismaDiffArgs(pm, schemaTo, null), { cwd: projectRoot, timeout: 6e4 });
1070
+ if (diffR.status !== 0) {
1071
+ console.warn(`\u26A0\uFE0F baseline \u751F\u6210\u5931\u8D25\uFF08${diffR.stderr.trim().slice(0, 150)}\uFF09\uFF0C\u964D\u7EA7 prisma-diff`);
1072
+ return false;
1073
+ }
1074
+ writeFileSync5(baselinePath, diffR.stdout, "utf-8");
1075
+ const atlasBin = resolveAtlasBin(projectRoot);
1076
+ if (!atlasBin) {
1077
+ console.warn("\u26A0\uFE0F atlas \u4E0D\u53EF\u7528\uFF08\u4F9D\u8D56\u672A\u5B89\u88C5\u6216\u5168\u5C40\u7F3A\u5931\uFF09");
1078
+ return false;
1079
+ }
1080
+ let devUrl = await provisionDevUrl(projectRoot);
1081
+ if (!devUrl && sync.atlasDevUrl) devUrl = sync.atlasDevUrl;
1082
+ if (!devUrl) {
1083
+ 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");
1084
+ return false;
1085
+ }
1086
+ {
1087
+ const diffArgs = ["schema", "diff", "--from", targetUrl, "--to", `file://${baselinePath}`, "--dev-url", devUrl, ...buildExcludeArgs(projectRoot, splitDb)];
1088
+ const diffSqlR = runCommand(atlasBin, diffArgs, { cwd: projectRoot, timeout: 6e4 });
1089
+ if (diffSqlR.status !== 0) {
1090
+ console.warn(`\u26A0\uFE0F atlas diff \u5931\u8D25\uFF08${diffSqlR.stderr.trim().slice(0, 150)}\uFF09\uFF0C\u964D\u7EA7 prisma-diff`);
1091
+ return false;
1092
+ }
1093
+ const diffSql = diffSqlR.stdout;
1094
+ const hasSql = /(?:^|\n)\s*(?:CREATE|ALTER|DROP|COMMENT|--\s*(?:Create|Modify|Drop))\b/i.test(diffSql);
1095
+ if (!hasSql) {
1096
+ console.log("\u2705 \u6570\u636E\u5E93\u4E0E\u76EE\u6807 schema \u4E00\u81F4\uFF08\u5E42\u7B49\u51FA\u53E3\uFF09");
1097
+ return true;
1098
+ }
1099
+ if (!splitDb && hasNonAddTableChanges(diffSql)) {
1100
+ 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");
1101
+ console.error(diffSql.split("\n").slice(0, 30).join("\n"));
1102
+ 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");
1103
+ }
1104
+ if (!opts.yes) {
1105
+ console.log("=== \u5F85\u5E94\u7528 diff SQL ===");
1106
+ console.log(diffSql.split("\n").slice(0, 60).join("\n"));
1107
+ const c = await ask("\u5E94\u7528\u4EE5\u4E0A schema \u53D8\u66F4\uFF1F[y/N] ");
1108
+ if (c !== "y" && c !== "yes") {
1109
+ console.log("\u5DF2\u53D6\u6D88\uFF0C\u672A\u5E94\u7528");
1110
+ return true;
1111
+ }
1112
+ }
1113
+ const applyArgs = ["schema", "apply", "--url", targetUrl, "--to", `file://${baselinePath}`, "--dev-url", devUrl, ...buildExcludeArgs(projectRoot, splitDb)];
1114
+ const applyR = runCommand(atlasBin, applyArgs, { cwd: projectRoot, timeout: 12e4 });
1115
+ if (applyR.status !== 0) {
1116
+ console.warn(`\u26A0\uFE0F atlas apply \u5931\u8D25\uFF08${applyR.stderr.trim().slice(0, 150)}\uFF09`);
1117
+ return false;
1118
+ }
1119
+ console.log("\u2705 Atlas schema \u540C\u6B65\u5B8C\u6210");
1120
+ return true;
1121
+ }
1122
+ }
1123
+ function runPrismaDiffSync(projectRoot, targetUrl, splitDb) {
1124
+ const pm = detectPm(projectRoot);
1125
+ const schemaTo = splitDb ? resolve5(projectRoot, "prisma", "add.prisma") : resolve5(projectRoot, "prisma");
1126
+ const diffPath = join6(projectRoot, PRISMA_CONFIG.sync.backupDir, "diff.sql");
1127
+ mkdirSync5(dirname3(diffPath), { recursive: true });
1128
+ const r = runCommand(pm, prismaDiffArgs(pm, schemaTo, targetUrl), { cwd: projectRoot, timeout: 6e4 });
1129
+ if (r.status !== 0) throw new Error(`prisma migrate diff \u5931\u8D25: ${r.stderr.trim().slice(0, 200)}`);
1130
+ writeFileSync5(diffPath, r.stdout, "utf-8");
1131
+ if (!r.stdout.trim()) {
1132
+ console.log("\u2705 \u6570\u636E\u5E93\u4E0E\u76EE\u6807 schema \u4E00\u81F4\uFF08\u5E42\u7B49\u51FA\u53E3\uFF09");
1133
+ return;
1134
+ }
1135
+ const execArgs = pm === "pnpm" ? ["dlx", "prisma", "db", "execute", "--file", diffPath] : ["exec", "prisma", "--", "db", "execute", "--file", diffPath];
1136
+ const e = runCommand(pm, execArgs, { cwd: projectRoot, timeout: 12e4 });
1137
+ if (e.status !== 0) throw new Error(`prisma db execute \u5931\u8D25: ${e.stderr.trim().slice(0, 200)}`);
1138
+ console.log("\u2705 prisma-diff \u540C\u6B65\u5B8C\u6210\uFF08\u514D shadow\uFF09");
1139
+ }
1140
+ function runDbPush(projectRoot) {
1141
+ const pm = detectPm(projectRoot);
1142
+ const args = pm === "pnpm" ? ["dlx", "prisma", "db", "push"] : ["exec", "prisma", "--", "db", "push"];
1143
+ if (PRISMA_CONFIG.schemaArg) args.push(PRISMA_CONFIG.schemaArg);
1144
+ console.log(`\u6267\u884C ${pm} ${args.join(" ")} ...`);
1145
+ const r = runCommand(pm, args, { cwd: projectRoot });
1146
+ if (r.status !== 0) throw new Error(`prisma db push \u9000\u51FA\u7801: ${r.status}`);
1147
+ }
1148
+ async function syncDatabase(projectRoot, opts, splitDb) {
1149
+ const sync = PRISMA_CONFIG.sync;
1150
+ const targetUrl = splitDb ? readEnvValue(projectRoot, "ADD_DATABASE_URL") || sync.addDatabaseUrl : readEnvValue(projectRoot, "DATABASE_URL");
1151
+ if (!targetUrl) {
1152
+ 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");
1153
+ return;
1154
+ }
1155
+ const bak = await backupBeforeSync(projectRoot, { dbUrl: targetUrl, backupDir: sync.backupDir, backupKeep: sync.backupKeep, yes: opts.yes });
1156
+ if (bak === null) {
1157
+ console.warn("\u26A0\uFE0F \u5907\u4EFD\u672A\u5B8C\u6210\uFF08\u7528\u6237\u81EA\u62C5\u98CE\u9669\uFF09\uFF0C\u7EE7\u7EED\u540C\u6B65");
1158
+ }
1159
+ if (sync.strategy === "db-push") {
1160
+ runDbPush(projectRoot);
1161
+ return;
1162
+ }
1163
+ const atlasBin = resolveAtlasBin(projectRoot);
1164
+ if (atlasBin) {
1165
+ const ok = await runAtlasSync(projectRoot, targetUrl, splitDb, sync, opts);
1166
+ if (ok) return;
1167
+ } else {
1168
+ 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");
1169
+ if (!opts.yes) {
1170
+ const a = await ask("\u7EE7\u7EED\u964D\u7EA7 prisma-diff\uFF08\u514D shadow\uFF09\uFF1F[Y/n] ");
1171
+ if (a === "n" || a === "no") throw new Error("\u5DF2\u53D6\u6D88\u540C\u6B65\uFF08atlas \u7F3A\u5931\uFF09");
1172
+ }
1173
+ }
1174
+ runPrismaDiffSync(projectRoot, targetUrl, splitDb);
1175
+ }
693
1176
  function ensurePrismaConfig(projectRoot) {
694
- const configPath = resolve3(projectRoot, "prisma.config.ts");
695
- writeFileSync3(configPath, [
1177
+ const configPath = resolve5(projectRoot, "prisma.config.ts");
1178
+ writeFileSync5(configPath, [
696
1179
  'import dotenv from "dotenv";',
697
1180
  'import { existsSync } from "fs";',
698
1181
  'for (const f of [".env.development.local", ".env.development", ".env.local", ".env"]) {',
@@ -707,20 +1190,6 @@ function ensurePrismaConfig(projectRoot) {
707
1190
  "});"
708
1191
  ].join("\n") + "\n", "utf-8");
709
1192
  }
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
1193
  function runPrismaInit(projectRoot, provider, schemaPath) {
725
1194
  console.log("\u6267\u884C prisma init ...");
726
1195
  const pm = detectPm(projectRoot);
@@ -732,10 +1201,10 @@ function runPrismaInit(projectRoot, provider, schemaPath) {
732
1201
  console.error(`\u2717 prisma init \u65E0\u6CD5\u6267\u884C: ${e instanceof Error ? e.message : String(e)}`);
733
1202
  initResult = { status: null, stdout: "", stderr: "" };
734
1203
  }
735
- if (initResult.status !== 0 || !existsSync7(schemaPath)) {
1204
+ if (initResult.status !== 0 || !existsSync9(schemaPath)) {
736
1205
  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 = resolve3(projectRoot, "prisma");
738
- if (!existsSync7(prismaDir)) mkdirSync3(prismaDir, { recursive: true });
1206
+ const prismaDir = resolve5(projectRoot, "prisma");
1207
+ if (!existsSync9(prismaDir)) mkdirSync5(prismaDir, { recursive: true });
739
1208
  const content = `generator client {
740
1209
  provider = "prisma-client-js"
741
1210
  }
@@ -744,11 +1213,11 @@ datasource db {
744
1213
  provider = "${provider}"
745
1214
  }
746
1215
  `;
747
- writeFileSync3(schemaPath, content, "utf-8");
748
- const devEnvPath = resolve3(projectRoot, ".env.development");
749
- if (!existsSync7(devEnvPath)) {
1216
+ writeFileSync5(schemaPath, content, "utf-8");
1217
+ const devEnvPath = resolve5(projectRoot, ".env.development");
1218
+ if (!existsSync9(devEnvPath)) {
750
1219
  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
- writeFileSync3(devEnvPath, defaultUrl + "\n", "utf-8");
1220
+ writeFileSync5(devEnvPath, defaultUrl + "\n", "utf-8");
752
1221
  console.log("\u5DF2\u521B\u5EFA .env.development");
753
1222
  }
754
1223
  return false;
@@ -756,18 +1225,18 @@ datasource db {
756
1225
  return true;
757
1226
  }
758
1227
  function postInitSetup(projectRoot, schemaPath, addPrismaTemplate, destPath) {
759
- const envPath = resolve3(projectRoot, ".env");
760
- const devEnvPath = resolve3(projectRoot, ".env.development");
761
- if (existsSync7(envPath)) {
762
- const envContent = readFileSync5(envPath, "utf-8");
1228
+ const envPath = resolve5(projectRoot, ".env");
1229
+ const devEnvPath = resolve5(projectRoot, ".env.development");
1230
+ if (existsSync9(envPath)) {
1231
+ const envContent = readFileSync7(envPath, "utf-8");
763
1232
  const dbUrl = envContent.match(/DATABASE_URL=.*/);
764
1233
  if (dbUrl) {
765
- const existing = existsSync7(devEnvPath) ? readFileSync5(devEnvPath, "utf-8") : "";
1234
+ const existing = existsSync9(devEnvPath) ? readFileSync7(devEnvPath, "utf-8") : "";
766
1235
  if (!existing.includes("DATABASE_URL=")) {
767
- writeFileSync3(devEnvPath, `${existing}${existing ? "\n" : ""}${dbUrl[0]}
1236
+ writeFileSync5(devEnvPath, `${existing}${existing ? "\n" : ""}${dbUrl[0]}
768
1237
  `, "utf-8");
769
1238
  }
770
- if (existsSync7(envPath)) unlinkSync(envPath);
1239
+ if (existsSync9(envPath)) unlinkSync(envPath);
771
1240
  console.log("\u5DF2\u5C06 DATABASE_URL \u8FC1\u79FB\u5230 .env.development");
772
1241
  }
773
1242
  }
@@ -776,8 +1245,8 @@ function postInitSetup(projectRoot, schemaPath, addPrismaTemplate, destPath) {
776
1245
  patchGeneratorOutput(schemaPath);
777
1246
  }
778
1247
  function patchGeneratorOutput(schemaPath) {
779
- if (!existsSync7(schemaPath)) return;
780
- let content = readFileSync5(schemaPath, "utf-8");
1248
+ if (!existsSync9(schemaPath)) return;
1249
+ let content = readFileSync7(schemaPath, "utf-8");
781
1250
  const genBlock = content.match(/generator\s+\w+\s*\{[\s\S]*?\}/);
782
1251
  if (!genBlock) {
783
1252
  content += `
@@ -786,23 +1255,23 @@ generator client {
786
1255
  output = "../src/generated/prisma"
787
1256
  }
788
1257
  `;
789
- writeFileSync3(schemaPath, content, "utf-8");
1258
+ writeFileSync5(schemaPath, content, "utf-8");
790
1259
  console.log("\u5DF2\u8FFD\u52A0 generator client\uFF08\u542B output \u2192 src/generated/prisma\uFF09");
791
1260
  return;
792
1261
  }
793
1262
  if (genBlock[0].includes("output")) return;
794
1263
  const patched = genBlock[0].replace(/\}\s*$/, ` output = "../src/generated/prisma"
795
1264
  }`);
796
- writeFileSync3(schemaPath, content.replace(genBlock[0], patched), "utf-8");
1265
+ writeFileSync5(schemaPath, content.replace(genBlock[0], patched), "utf-8");
797
1266
  console.log("\u5DF2\u6CE8\u5165 generator output \u2192 src/generated/prisma");
798
1267
  }
799
1268
  async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
800
1269
  const C = PRISMA_CONFIG;
801
- const prismaDir = resolve3(projectRoot, "prisma");
802
- const schemaPath = resolve3(prismaDir, "schema.prisma");
803
- const destPath = resolve3(prismaDir, "add.prisma");
1270
+ const prismaDir = resolve5(projectRoot, "prisma");
1271
+ const schemaPath = resolve5(prismaDir, "schema.prisma");
1272
+ const destPath = resolve5(prismaDir, "add.prisma");
804
1273
  let justInited = false;
805
- if (!existsSync7(prismaDir) || !existsSync7(schemaPath)) {
1274
+ if (!existsSync9(prismaDir) || !existsSync9(schemaPath)) {
806
1275
  if (C.onMissing === "skip") {
807
1276
  console.log("\u8DF3\u8FC7\uFF1A\u7F3A\u5C11 Prisma");
808
1277
  return true;
@@ -821,7 +1290,7 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
821
1290
  postInitSetup(projectRoot, schemaPath, addPrismaTemplate, destPath);
822
1291
  justInited = true;
823
1292
  }
824
- if (existsSync7(destPath) && !justInited) {
1293
+ if (existsSync9(destPath) && !justInited) {
825
1294
  if (options.dryRun) {
826
1295
  console.log("[dry-run] \u5DF2\u6709 add.prisma");
827
1296
  return true;
@@ -838,8 +1307,8 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
838
1307
  console.log("\u8986\u76D6");
839
1308
  } else if (choice === "d") {
840
1309
  copyFileSync(destPath, destPath + ".bak");
841
- console.log("=== \u5F53\u524D\uFF08\u5DF2\u5907\u4EFD\uFF09===\n" + readFileSync5(destPath, "utf-8"));
842
- console.log("=== \u6A21\u677F ===\n" + readFileSync5(addPrismaTemplate, "utf-8"));
1310
+ console.log("=== \u5F53\u524D\uFF08\u5DF2\u5907\u4EFD\uFF09===\n" + readFileSync7(destPath, "utf-8"));
1311
+ console.log("=== \u6A21\u677F ===\n" + readFileSync7(addPrismaTemplate, "utf-8"));
843
1312
  if (await ask("\u786E\u8BA4\u8986\u76D6\uFF1F[y/N] ") !== "y") {
844
1313
  console.log("\u5DF2\u8DF3\u8FC7");
845
1314
  return true;
@@ -851,19 +1320,14 @@ async function injectPrisma(projectRoot, addPrismaTemplate, options = {}) {
851
1320
  }
852
1321
  }
853
1322
  if (options.dryRun) {
854
- console.log("[dry-run] \u5C06\u6267\u884C prisma db push");
1323
+ console.log("[dry-run] \u5C06\u6267\u884C\u6570\u636E\u5E93\u540C\u6B65\uFF08v2 Atlas \u5F15\u64CE\uFF09");
855
1324
  return true;
856
1325
  }
857
1326
  if (!justInited) copyFileSync(addPrismaTemplate, destPath);
858
1327
  try {
859
1328
  ensurePrismaConfig(projectRoot);
860
- backupAddTables(projectRoot);
861
- const pm = detectPm(projectRoot);
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}`);
1329
+ const splitDb = await ensureSplitDb(projectRoot, options);
1330
+ await syncDatabase(projectRoot, { yes: options.yes }, splitDb);
867
1331
  } catch (err) {
868
1332
  if (C.onMigrateFail === "keep") {
869
1333
  console.log("\u8FC1\u79FB\u5931\u8D25\uFF0C\u4FDD\u7559\u6587\u4EF6");
@@ -893,36 +1357,36 @@ ${detail}` : ""}`);
893
1357
 
894
1358
  // src/cli/prisma-injector.ts
895
1359
  var __filename2 = fileURLToPath2(import.meta.url);
896
- var __dirname2 = dirname3(__filename2);
897
- var ADD_PRISMA_TEMPLATE = resolve4(__dirname2, "../templates/core/prisma/add.prisma");
1360
+ var __dirname2 = dirname4(__filename2);
1361
+ var ADD_PRISMA_TEMPLATE = resolve6(__dirname2, "../templates/core/prisma/add.prisma");
898
1362
  async function injectPrisma2(projectRoot, options = {}) {
899
1363
  return injectPrisma(projectRoot, ADD_PRISMA_TEMPLATE, options);
900
1364
  }
901
1365
 
902
1366
  // src/cli/commands/init.ts
903
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync9, mkdirSync as mkdirSync4, copyFileSync as copyFileSync2, readdirSync as readdirSync2 } from "fs";
1367
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync11, mkdirSync as mkdirSync6, copyFileSync as copyFileSync2, readdirSync as readdirSync4 } from "fs";
904
1368
  import { createHash } from "crypto";
905
- import { resolve as resolve6 } from "path";
906
- import { createConnection } from "net";
1369
+ import { resolve as resolve8 } from "path";
1370
+ import { createConnection as createConnection2 } from "net";
907
1371
 
908
1372
  // src/lib/model-predownload.ts
909
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
910
- import { join as join5, resolve as resolve5 } from "path";
1373
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
1374
+ import { join as join7, resolve as resolve7 } from "path";
911
1375
  import { homedir } from "os";
912
1376
  import { parse as parse2 } from "smol-toml";
913
1377
  var DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1e3;
914
1378
  var TOML_CANDIDATES = [
915
- resolve5(import.meta.dirname, "caijuehub/dps-scoring-rules.toml"),
916
- resolve5(import.meta.dirname, "../caijuehub/dps-scoring-rules.toml")
1379
+ resolve7(import.meta.dirname, "caijuehub/dps-scoring-rules.toml"),
1380
+ resolve7(import.meta.dirname, "../caijuehub/dps-scoring-rules.toml")
917
1381
  ];
918
1382
  function resolveEmbeddingModel() {
919
- const tomlPath = TOML_CANDIDATES.find((p) => existsSync8(p));
1383
+ const tomlPath = TOML_CANDIDATES.find((p) => existsSync10(p));
920
1384
  if (!tomlPath) {
921
1385
  throw new Error(
922
1386
  `dps-scoring-rules.toml \u672A\u627E\u5230\uFF08\u671F\u671B\u8DEF\u5F84: ${TOML_CANDIDATES.join(" \u6216 ")}\uFF09`
923
1387
  );
924
1388
  }
925
- const cfg = parse2(readFileSync6(tomlPath, "utf-8"));
1389
+ const cfg = parse2(readFileSync8(tomlPath, "utf-8"));
926
1390
  const model = cfg.embedding?.model;
927
1391
  if (typeof model !== "string" || model.length === 0) {
928
1392
  throw new Error(`dps-scoring-rules.toml [embedding] model \u672A\u914D\u7F6E\uFF08${tomlPath}\uFF09`);
@@ -932,8 +1396,8 @@ function resolveEmbeddingModel() {
932
1396
  function resolveCacheDir() {
933
1397
  const hubCache = process.env.HF_HUB_CACHE;
934
1398
  if (hubCache) return hubCache;
935
- const home = process.env.HF_HOME || join5(homedir(), ".cache", "huggingface");
936
- return join5(home, "hub");
1399
+ const home = process.env.HF_HOME || join7(homedir(), ".cache", "huggingface");
1400
+ return join7(home, "hub");
937
1401
  }
938
1402
  function modelCacheName(model) {
939
1403
  const parts = model.split("/");
@@ -943,7 +1407,7 @@ function modelCacheName(model) {
943
1407
  }
944
1408
  function isModelCached(model) {
945
1409
  const cacheDir = resolveCacheDir();
946
- return existsSync8(join5(cacheDir, modelCacheName(model), "snapshots"));
1410
+ return existsSync10(join7(cacheDir, modelCacheName(model), "snapshots"));
947
1411
  }
948
1412
  async function ensureEmbeddingModel(options) {
949
1413
  const force = options?.force ?? false;
@@ -956,8 +1420,8 @@ async function ensureEmbeddingModel(options) {
956
1420
  const { pipeline, env } = await import("@huggingface/transformers");
957
1421
  env.cacheDir = resolveCacheDir();
958
1422
  const cacheDir = env.cacheDir;
959
- const snapshotsDir = join5(cacheDir, modelCacheName(model), "snapshots");
960
- if (!force && existsSync8(snapshotsDir)) {
1423
+ const snapshotsDir = join7(cacheDir, modelCacheName(model), "snapshots");
1424
+ if (!force && existsSync10(snapshotsDir)) {
961
1425
  return { status: "already-cached", model, cacheDir };
962
1426
  }
963
1427
  env.remoteHost = "https://hf-mirror.com";
@@ -1001,6 +1465,7 @@ async function initCommand(options) {
1001
1465
  }
1002
1466
  const result = await renderAndWrite(ctx);
1003
1467
  const dbFail = await deployDatabase(ctx);
1468
+ ensurePortsContract(ctx.projectRoot, ctx.config, !!options.dryRun);
1004
1469
  deployDocs(ctx);
1005
1470
  finalize(ctx, result, dbFail);
1006
1471
  if (!options.dryRun) {
@@ -1054,9 +1519,9 @@ async function resolveContainer(force) {
1054
1519
  if (a !== "" && a !== "1" && a !== "podman") console.log("\u8F93\u5165\u65E0\u6CD5\u8BC6\u522B\uFF0C\u9ED8\u8BA4 podman");
1055
1520
  return "podman";
1056
1521
  }
1057
- function portInUse(port) {
1522
+ function portInUse2(port) {
1058
1523
  return new Promise((r) => {
1059
- const s = createConnection({ port, host: "127.0.0.1" }, () => {
1524
+ const s = createConnection2({ port, host: "127.0.0.1" }, () => {
1060
1525
  s.destroy();
1061
1526
  r(true);
1062
1527
  });
@@ -1098,7 +1563,7 @@ async function resolveDbCredentials(force) {
1098
1563
  let port = (await ask(`DATABASE_PORT [${d.port}]: `)).trim() || d.port;
1099
1564
  while (true) {
1100
1565
  const portNum = parseInt(port);
1101
- if (!isNaN(portNum) && await portInUse(portNum)) {
1566
+ if (!isNaN(portNum) && await portInUse2(portNum)) {
1102
1567
  console.log(`
1103
1568
  \u26A0\uFE0F \u7AEF\u53E3 ${port} \u5DF2\u88AB\u5360\u7528`);
1104
1569
  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 +1617,8 @@ networks:
1152
1617
  `;
1153
1618
  }
1154
1619
  function writeSqliteExportScript(projectRoot, dryRun) {
1155
- const scriptsDir = resolve6(projectRoot, "scripts");
1156
- const scriptPath = resolve6(scriptsDir, "export-db.ts");
1620
+ const scriptsDir = resolve8(projectRoot, "scripts");
1621
+ const scriptPath = resolve8(scriptsDir, "export-db.ts");
1157
1622
  const content = `import { PrismaClient } from "@prisma/client";
1158
1623
  import { writeFileSync, mkdirSync, existsSync } from "fs";
1159
1624
  import { resolve } from "path";
@@ -1177,22 +1642,22 @@ main().catch((e) => { console.error(e); process.exit(1); });
1177
1642
  console.log(`[dry-run] \u5C06\u5199\u5165 ${scriptPath}`);
1178
1643
  return;
1179
1644
  }
1180
- if (!existsSync9(scriptsDir)) mkdirSync4(scriptsDir, { recursive: true });
1181
- writeFileSync4(scriptPath, content, "utf-8");
1645
+ if (!existsSync11(scriptsDir)) mkdirSync6(scriptsDir, { recursive: true });
1646
+ writeFileSync6(scriptPath, content, "utf-8");
1182
1647
  console.log("\u5DF2\u751F\u6210 scripts/export-db.ts");
1183
1648
  }
1184
1649
  function injectDbExportScript(projectRoot, dryRun) {
1185
- const pkgPath = resolve6(projectRoot, "package.json");
1186
- if (!existsSync9(pkgPath)) return;
1650
+ const pkgPath = resolve8(projectRoot, "package.json");
1651
+ if (!existsSync11(pkgPath)) return;
1187
1652
  if (dryRun) {
1188
1653
  console.log("[dry-run] \u5C06\u6CE8\u5165 db:export");
1189
1654
  return;
1190
1655
  }
1191
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
1656
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
1192
1657
  if (!pkg.scripts) pkg.scripts = {};
1193
1658
  if (!pkg.scripts["db:export"]) {
1194
1659
  pkg.scripts["db:export"] = "npx tsx scripts/export-db.ts";
1195
- writeFileSync4(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1660
+ writeFileSync6(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1196
1661
  console.log("\u5DF2\u5728 package.json \u6CE8\u5165 db:export");
1197
1662
  }
1198
1663
  }
@@ -1235,17 +1700,17 @@ function writeComposeEnv(ctx) {
1235
1700
  if (db.engine !== "postgresql" || !db.container || db.container === "manual") return;
1236
1701
  if (!db.reuseExisting) {
1237
1702
  const composeName = db.container === "podman" ? "podman-compose.add.yml" : "docker-compose.add.yml";
1238
- const composePath = resolve6(projectRoot, composeName);
1239
- if (!options.dryRun && (!existsSync9(composePath) || options.force)) {
1240
- writeFileSync4(composePath, composeContent(config.projectName || "add-project"), "utf-8");
1703
+ const composePath = resolve8(projectRoot, composeName);
1704
+ if (!options.dryRun && (!existsSync11(composePath) || options.force)) {
1705
+ writeFileSync6(composePath, composeContent(config.projectName || "add-project"), "utf-8");
1241
1706
  console.log(`\u5DF2\u521B\u5EFA ${composeName}`);
1242
1707
  }
1243
1708
  }
1244
- const devEnvPath = resolve6(projectRoot, ".env.development");
1245
- if (!options.dryRun && existsSync9(devEnvPath)) {
1246
- const existing = readFileSync7(devEnvPath, "utf-8");
1709
+ const devEnvPath = resolve8(projectRoot, ".env.development");
1710
+ if (!options.dryRun && existsSync11(devEnvPath)) {
1711
+ const existing = readFileSync9(devEnvPath, "utf-8");
1247
1712
  if (!/^DATABASE_USER=/m.test(existing)) {
1248
- writeFileSync4(devEnvPath, existing + `
1713
+ writeFileSync6(devEnvPath, existing + `
1249
1714
  DATABASE_USER=${db.user || "admin"}
1250
1715
  DATABASE_PASSWORD=${db.password || "change-me-in-production"}
1251
1716
  DATABASE_PORT=${db.port || "5433"}
@@ -1282,12 +1747,12 @@ async function renderAndWrite(ctx) {
1282
1747
  console.log(`claude adapter (via Agent Host): ${claudeFiles.size} \u6587\u4EF6`);
1283
1748
  }
1284
1749
  for (const d of [".add", magicDir]) {
1285
- const reviewsDir = resolve6(projectRoot, d, "reviews");
1286
- if (!existsSync9(reviewsDir)) {
1750
+ const reviewsDir = resolve8(projectRoot, d, "reviews");
1751
+ if (!existsSync11(reviewsDir)) {
1287
1752
  if (dry) {
1288
1753
  console.log(`[dry-run] \u5C06\u521B\u5EFA ${reviewsDir}/`);
1289
1754
  } else {
1290
- mkdirSync4(reviewsDir, { recursive: true });
1755
+ mkdirSync6(reviewsDir, { recursive: true });
1291
1756
  }
1292
1757
  }
1293
1758
  }
@@ -1295,16 +1760,16 @@ async function renderAndWrite(ctx) {
1295
1760
  const hashMap = {};
1296
1761
  let npmVer = "";
1297
1762
  try {
1298
- npmVer = JSON.parse(readFileSync7(resolve6(projectRoot, "node_modules", "add-coder", "templates", ".add-coder-src-hash.json"), "utf-8"))._version ?? "";
1763
+ npmVer = JSON.parse(readFileSync9(resolve8(projectRoot, "node_modules", "add-coder", "templates", ".add-coder-src-hash.json"), "utf-8"))._version ?? "";
1299
1764
  } catch {
1300
1765
  }
1301
1766
  for (const [rp, c] of allFiles) {
1302
1767
  hashMap[rp] = createHash("sha256").update(c).digest("hex").slice(0, 8);
1303
1768
  }
1304
- const hashOut = resolve6(projectRoot, magicDir, ".add-coder-hash.json");
1305
- writeFileSync4(hashOut, JSON.stringify(hashMap, null, 2) + "\n", "utf-8");
1769
+ const hashOut = resolve8(projectRoot, magicDir, ".add-coder-hash.json");
1770
+ writeFileSync6(hashOut, JSON.stringify(hashMap, null, 2) + "\n", "utf-8");
1306
1771
  if (npmVer) {
1307
- writeFileSync4(resolve6(projectRoot, magicDir, ".add-coder-version"), npmVer + "\n", "utf-8");
1772
+ writeFileSync6(resolve8(projectRoot, magicDir, ".add-coder-version"), npmVer + "\n", "utf-8");
1308
1773
  }
1309
1774
  console.log(`hash: ${Object.keys(hashMap).length} entries \u2192 ${magicDir}/.add-coder-hash.json`);
1310
1775
  }
@@ -1315,7 +1780,7 @@ async function deployDatabase(ctx) {
1315
1780
  if (options.dryRun) return null;
1316
1781
  let fail = null;
1317
1782
  if (db.engine === "postgresql" && db.container && db.container !== "manual") {
1318
- const dbScript = resolve6(projectRoot, magicDir, "scripts", "db-ensure.sh");
1783
+ const dbScript = resolve8(projectRoot, magicDir, "scripts", "db-ensure.sh");
1319
1784
  const dbEnv = { ...process.env, DATABASE_USER: db.user, DATABASE_PASSWORD: db.password, DATABASE_PORT: db.port, PROJECT_NAME: config.projectName };
1320
1785
  const mode = db.reuseExisting ? "manual" : db.container;
1321
1786
  console.log(db.reuseExisting ? "\u590D\u7528\u5DF2\u6709 PostgreSQL ..." : `\u90E8\u7F72\u6570\u636E\u5E93 (${db.container}) ...`);
@@ -1333,8 +1798,8 @@ ${bashRun.stderr.trim().split("\n").slice(0, 5).join("\n")}` : ""}`;
1333
1798
  }
1334
1799
  }
1335
1800
  if (db.engine === "postgresql" && db.container === "manual") {
1336
- const dbScript = resolve6(projectRoot, magicDir, "scripts", "db-ensure.sh");
1337
- if (existsSync9(dbScript)) {
1801
+ const dbScript = resolve8(projectRoot, magicDir, "scripts", "db-ensure.sh");
1802
+ if (existsSync11(dbScript)) {
1338
1803
  try {
1339
1804
  const bashRun = runCommand("bash", [dbScript, "postgresql", "manual"], { cwd: projectRoot, stdio: "inherit" });
1340
1805
  if (bashRun.status !== 0) fail = `db-ensure.sh \u9000\u51FA\u7801: ${bashRun.status}${bashRun.stderr ? `
@@ -1379,17 +1844,17 @@ function deployDocs(ctx) {
1379
1844
  const { projectRoot, options, config } = ctx;
1380
1845
  if (options.dryRun) return;
1381
1846
  const pn = config.projectName || "add-project";
1382
- const docsBase = resolve6(projectRoot, "docs", pn, "knowledge");
1383
- const groundingSrc = resolve6(import.meta.dirname, "../templates/core/templates");
1847
+ const docsBase = resolve8(projectRoot, "docs", pn, "knowledge");
1848
+ const groundingSrc = resolve8(import.meta.dirname, "../templates/core/templates");
1384
1849
  for (const d of ["00-\u9700\u6C42", "01-\u67B6\u6784", "02-\u89C4\u8303"]) {
1385
- const srcDir = resolve6(groundingSrc, d);
1386
- const destDir = resolve6(docsBase, d);
1387
- if (!existsSync9(destDir)) mkdirSync4(destDir, { recursive: true });
1388
- if (!existsSync9(srcDir)) continue;
1389
- for (const f of readdirSync2(srcDir)) {
1390
- const src = resolve6(srcDir, f);
1391
- const dest = resolve6(destDir, f);
1392
- if (existsSync9(dest)) continue;
1850
+ const srcDir = resolve8(groundingSrc, d);
1851
+ const destDir = resolve8(docsBase, d);
1852
+ if (!existsSync11(destDir)) mkdirSync6(destDir, { recursive: true });
1853
+ if (!existsSync11(srcDir)) continue;
1854
+ for (const f of readdirSync4(srcDir)) {
1855
+ const src = resolve8(srcDir, f);
1856
+ const dest = resolve8(destDir, f);
1857
+ if (existsSync11(dest)) continue;
1393
1858
  try {
1394
1859
  copyFileSync2(src, dest);
1395
1860
  } catch {
@@ -1405,7 +1870,7 @@ function finalize(ctx, result, dbFail) {
1405
1870
  return;
1406
1871
  }
1407
1872
  if (db.engine === "sqlite") console.log("\u6570\u636E\u5907\u4EFD: npm run db:export \u2192 data/exports/");
1408
- const pkg = JSON.parse(readFileSync7(resolve6(import.meta.dirname, "../package.json"), "utf-8"));
1873
+ const pkg = JSON.parse(readFileSync9(resolve8(import.meta.dirname, "../package.json"), "utf-8"));
1409
1874
  const peerNames = Object.keys(pkg.peerDependencies || {});
1410
1875
  if (peerNames.length > 0) {
1411
1876
  console.log(`
@@ -1433,8 +1898,8 @@ function finalize(ctx, result, dbFail) {
1433
1898
  }
1434
1899
 
1435
1900
  // src/cli/commands/sync.ts
1436
- import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
1437
- import { resolve as resolve7, dirname as dirname4 } from "path";
1901
+ import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
1902
+ import { resolve as resolve9, dirname as dirname5 } from "path";
1438
1903
  import { createHash as createHash2 } from "crypto";
1439
1904
 
1440
1905
  // src/lib/path-normalize.ts
@@ -1492,7 +1957,7 @@ function hash8(c) {
1492
1957
  }
1493
1958
  function loadHashFile(root, magic) {
1494
1959
  try {
1495
- const raw = JSON.parse(readFileSync8(resolve7(root, magic, SYNC_CONFIG.HASH_OUTPUT_FILE), "utf-8"));
1960
+ const raw = JSON.parse(readFileSync10(resolve9(root, magic, SYNC_CONFIG.HASH_OUTPUT_FILE), "utf-8"));
1496
1961
  const normalized = {};
1497
1962
  for (const [k, v] of Object.entries(raw)) normalized[normalizeRelPath(k)] = v;
1498
1963
  return normalized;
@@ -1502,18 +1967,18 @@ function loadHashFile(root, magic) {
1502
1967
  }
1503
1968
  function loadVersionFile(root, magic) {
1504
1969
  try {
1505
- return readFileSync8(resolve7(root, magic, SYNC_CONFIG.VERSION_SENTINEL), "utf-8").trim();
1970
+ return readFileSync10(resolve9(root, magic, SYNC_CONFIG.VERSION_SENTINEL), "utf-8").trim();
1506
1971
  } catch {
1507
1972
  return "";
1508
1973
  }
1509
1974
  }
1510
1975
  function saveVersionFile(root, magic, version2) {
1511
- writeFileSync5(resolve7(root, magic, SYNC_CONFIG.VERSION_SENTINEL), version2 + "\n", "utf-8");
1976
+ writeFileSync7(resolve9(root, magic, SYNC_CONFIG.VERSION_SENTINEL), version2 + "\n", "utf-8");
1512
1977
  }
1513
1978
  function saveHashFile(root, magic, files) {
1514
1979
  const m = {};
1515
1980
  for (const [p, c] of files) m[p] = c;
1516
- writeFileSync5(resolve7(root, magic, SYNC_CONFIG.HASH_OUTPUT_FILE), JSON.stringify(m, null, 2) + "\n", "utf-8");
1981
+ writeFileSync7(resolve9(root, magic, SYNC_CONFIG.HASH_OUTPUT_FILE), JSON.stringify(m, null, 2) + "\n", "utf-8");
1517
1982
  }
1518
1983
  function mergeFullHash(outHash, candidates, readDiskHash) {
1519
1984
  const finalHash = /* @__PURE__ */ new Map();
@@ -1586,10 +2051,10 @@ async function syncCommand(options = {}) {
1586
2051
  candidates.set(p, c);
1587
2052
  }
1588
2053
  const outHash = loadHashFile(projectRoot, magicDir);
1589
- const srcHashPath = resolve7(projectRoot, "node_modules", "add-coder", "templates", ".add-coder-src-hash.json");
2054
+ const srcHashPath = resolve9(projectRoot, "node_modules", "add-coder", "templates", ".add-coder-src-hash.json");
1590
2055
  let npmVersion = "";
1591
2056
  try {
1592
- npmVersion = JSON.parse(readFileSync8(srcHashPath, "utf-8"))._version ?? "";
2057
+ npmVersion = JSON.parse(readFileSync10(srcHashPath, "utf-8"))._version ?? "";
1593
2058
  } catch {
1594
2059
  }
1595
2060
  const installedVersion = loadVersionFile(projectRoot, magicDir);
@@ -1609,13 +2074,13 @@ async function syncCommand(options = {}) {
1609
2074
  let sameCount = 0;
1610
2075
  for (const [relPath, content] of candidates) {
1611
2076
  const key = normalizeRelPath(relPath);
1612
- const absPath = resolve7(projectRoot, relPath);
1613
- if (!existsSync10(absPath)) {
2077
+ const absPath = resolve9(projectRoot, relPath);
2078
+ if (!existsSync12(absPath)) {
1614
2079
  missingFiles.set(key, content);
1615
2080
  } else if (establishBaseline) {
1616
2081
  missingFiles.set(key, content);
1617
2082
  } else {
1618
- const curH = hash8(readFileSync8(absPath, "utf-8"));
2083
+ const curH = hash8(readFileSync10(absPath, "utf-8"));
1619
2084
  const storedH = outHash[key];
1620
2085
  if (storedH && curH === storedH) {
1621
2086
  sameCount++;
@@ -1643,18 +2108,19 @@ async function syncCommand(options = {}) {
1643
2108
  }
1644
2109
  const finalHash = mergeFullHash(
1645
2110
  outHash,
1646
- [...candidates].map(([relPath]) => ({ relPath, absPath: resolve7(projectRoot, relPath) })),
1647
- (absPath) => existsSync10(absPath) ? hash8(readFileSync8(absPath, "utf-8")) : null
2111
+ [...candidates].map(([relPath]) => ({ relPath, absPath: resolve9(projectRoot, relPath) })),
2112
+ (absPath) => existsSync12(absPath) ? hash8(readFileSync10(absPath, "utf-8")) : null
1648
2113
  );
1649
2114
  saveHashFile(projectRoot, magicDir, finalHash);
1650
2115
  saveVersionFile(projectRoot, magicDir, npmVersion);
1651
2116
  await checkPrismaDiff(projectRoot, options);
2117
+ ensurePortsContract(projectRoot, config);
1652
2118
  await maybeModelDownload(options);
1653
2119
  return;
1654
2120
  }
1655
2121
  const missing = /* @__PURE__ */ new Map();
1656
2122
  for (const [relPath, content] of allFiles) {
1657
- if (!existsSync10(resolve7(projectRoot, relPath))) {
2123
+ if (!existsSync12(resolve9(projectRoot, relPath))) {
1658
2124
  missing.set(relPath, content);
1659
2125
  }
1660
2126
  }
@@ -1673,11 +2139,12 @@ async function syncCommand(options = {}) {
1673
2139
  const result = await writeFiles2(projectRoot, filesToWrite, { yes: true });
1674
2140
  console.log(`\u540C\u6B65\u5B8C\u6210: \u65B0\u5EFA ${result.created}, \u8DF3\u8FC7 ${result.skipped}`);
1675
2141
  await checkPrismaDiff(projectRoot, options);
2142
+ ensurePortsContract(projectRoot, config);
1676
2143
  await maybeModelDownload(options);
1677
2144
  }
1678
2145
  function printMigrateGuidance(targetPath, changed) {
1679
2146
  const g = SYNC_PRISMA_CONFIG.POST_SYNC;
1680
- const isManaged = existsSync10(resolve7(dirname4(targetPath), "migrations"));
2147
+ const isManaged = existsSync12(resolve9(dirname5(targetPath), "migrations"));
1681
2148
  const actions = isManaged ? g.MANAGED_ACTIONS : g.UNMANAGED_ACTIONS;
1682
2149
  console.log(` \u25B6 \u5DF2\u5199\u5165 ${changed} \u5904\u53D8\u66F4\uFF0C${g.HEADER}`);
1683
2150
  for (const a of actions) {
@@ -1697,23 +2164,62 @@ function printMigrateGuidance(targetPath, changed) {
1697
2164
  }
1698
2165
  console.log(` \u26A0\uFE0F \u6240\u6709\u573A\u666F\u6536\u5C3E: ${g.FINAL}`);
1699
2166
  }
2167
+ async function ensureAtlasCapability(projectRoot) {
2168
+ const bin = resolveAtlasBin(projectRoot);
2169
+ if (bin) {
2170
+ console.log(` \u2705 Atlas \u80FD\u529B\u5C31\u7EEA: ${bin}`);
2171
+ return;
2172
+ }
2173
+ console.warn(" \u26A0\uFE0F Atlas \u4E0D\u53EF\u7528\u2014\u2014\u6570\u636E\u5E93\u540C\u6B65\uFF08Atlas diff/apply\uFF09\u80FD\u529B\u7F3A\u5931");
2174
+ const a = await ask(" \u662F\u5426\u81EA\u52A8\u5B89\u88C5 @ariga/atlas\uFF08npm \u4F9D\u8D56\uFF0C\u8D70 registry\uFF09\uFF1F[Y/n] ");
2175
+ if (a === "n" || a === "no") {
2176
+ 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");
2177
+ console.log(" \u6587\u6863: README.md \u2192 \u7AE0\u8282\u300CAtlas \u6570\u636E\u5E93\u540C\u6B65\u80FD\u529B\u300D/ DEVELOPMENT.md \xA7\u4E5D");
2178
+ return;
2179
+ }
2180
+ const pm = existsSync12(resolve9(projectRoot, "pnpm-lock.yaml")) ? "pnpm" : "npm";
2181
+ console.log(` \u5B89\u88C5 ${pm} add -D @ariga/atlas ...`);
2182
+ const r = runCommand(pm, ["add", "-D", "@ariga/atlas"], { cwd: projectRoot, timeout: 18e4 });
2183
+ if (r.status !== 0) {
2184
+ console.error(" \u5B89\u88C5\u5931\u8D25\u3002\u8BF7\u624B\u52A8\u6267\u884C:");
2185
+ console.error(` ${pm} add -D @ariga/atlas`);
2186
+ console.error(" pnpm 11 \u6CE8\u610F: \u9700\u5728 pnpm-workspace.yaml allowBuilds \u653E\u884C '@ariga/atlas': true");
2187
+ return;
2188
+ }
2189
+ console.log(" \u2705 @ariga/atlas \u5DF2\u5B89\u88C5\uFF0CAtlas \u80FD\u529B\u5C31\u7EEA\uFF08node_modules/.bin/atlas\uFF09");
2190
+ }
2191
+ function checkHostAtlasSegment(projectRoot) {
2192
+ const script = resolve9(projectRoot, "scripts", "db-ensure.sh");
2193
+ if (!existsSync12(script)) return;
2194
+ const content = readFileSync10(script, "utf-8");
2195
+ if (content.includes("atlas_sync")) return;
2196
+ 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");
2197
+ 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");
2198
+ console.warn(" \u5408\u5165\u4E09\u6B65\uFF1A");
2199
+ 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");
2200
+ console.warn(" \u2461 \u53D8\u91CF\u9002\u914D\uFF08DB_URL\u2192DATABASE_URL \u7B49\uFF0C\u89C1\u6587\u6863\u53D8\u91CF\u5BF9\u7167\u8868\uFF09");
2201
+ 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");
2202
+ 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");
2203
+ }
1700
2204
  async function checkPrismaDiff(projectRoot, options) {
1701
2205
  if (!options.patch) return;
1702
- const basePath = resolve7(projectRoot, SYNC_PRISMA_CONFIG.BASE_SCHEMA);
1703
- const targetPath = resolve7(projectRoot, SYNC_PRISMA_CONFIG.TARGET_PATTERN);
1704
- if (!existsSync10(basePath)) {
2206
+ const basePath = resolve9(projectRoot, SYNC_PRISMA_CONFIG.BASE_SCHEMA);
2207
+ const targetPath = resolve9(projectRoot, SYNC_PRISMA_CONFIG.TARGET_PATTERN);
2208
+ if (!existsSync12(basePath)) {
1705
2209
  console.log(`
1706
2210
  \u26A0\uFE0F \u57FA\u51C6 schema \u4E0D\u5B58\u5728: ${basePath}`);
1707
2211
  console.log(` \u8BF7\u786E\u4FDD add-coder \u5DF2\u6B63\u786E\u5B89\u88C5\u3002`);
1708
2212
  return;
1709
2213
  }
2214
+ await ensureAtlasCapability(projectRoot);
2215
+ checkHostAtlasSegment(projectRoot);
1710
2216
  const result = diffPrisma(basePath, targetPath);
1711
2217
  if (!result.hasDiff) {
1712
2218
  console.log(`
1713
2219
  \u2705 Prisma schema \u4E0E add-coder \u6807\u51C6\u4E00\u81F4\uFF0C\u65E0\u9700\u540C\u6B65\u3002`);
1714
2220
  return;
1715
2221
  }
1716
- const targetExists = existsSync10(targetPath);
2222
+ const targetExists = existsSync12(targetPath);
1717
2223
  let modifiedCount = 0;
1718
2224
  console.log(`
1719
2225
  \u26A0\uFE0F Prisma schema \u5DEE\u5F02\u68C0\u6D4B:`);
@@ -1832,8 +2338,8 @@ async function checkPrismaDiff(projectRoot, options) {
1832
2338
  } else {
1833
2339
  console.log(`
1834
2340
  \u76EE\u6807 schema \u6587\u4EF6\u4E0D\u5B58\u5728: ${result.targetPath}`);
1835
- mkdirSync5(dirname4(targetPath), { recursive: true });
1836
- writeFileSync5(targetPath, "// add.prisma \u2014 ADD \u6CBB\u7406\u6A21\u578B\n\n", "utf-8");
2341
+ mkdirSync7(dirname5(targetPath), { recursive: true });
2342
+ writeFileSync7(targetPath, "// add.prisma \u2014 ADD \u6CBB\u7406\u6A21\u578B\n\n", "utf-8");
1837
2343
  const n = injectMissingModels(targetPath, result.missing);
1838
2344
  console.log(` \u2705 \u5DF2\u521B\u5EFA\u5E76\u6CE8\u5165 ${n} \u4E2A\u6A21\u578B/\u679A\u4E3E`);
1839
2345
  if (n > 0) {
@@ -1864,7 +2370,7 @@ async function handleDiffAction(action, ctx) {
1864
2370
  }
1865
2371
  }
1866
2372
  function injectMissingModels(targetPath, models) {
1867
- let content = readFileSync8(targetPath, "utf-8");
2373
+ let content = readFileSync10(targetPath, "utf-8");
1868
2374
  content = content.replace(/\n*$/, "\n");
1869
2375
  content += `
1870
2376
  // ===== \u7531 add-coder sync --patch \u81EA\u52A8\u6CE8\u5165 (${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}) =====
@@ -1873,11 +2379,11 @@ function injectMissingModels(targetPath, models) {
1873
2379
  for (const m of models) {
1874
2380
  content += m.body + "\n\n";
1875
2381
  }
1876
- writeFileSync5(targetPath, content, "utf-8");
2382
+ writeFileSync7(targetPath, content, "utf-8");
1877
2383
  return models.length;
1878
2384
  }
1879
2385
  function getBaseFieldLines(basePath, modelName) {
1880
- const content = readFileSync8(basePath, "utf-8");
2386
+ const content = readFileSync10(basePath, "utf-8");
1881
2387
  const blocks = parseSchemaBlocks(content);
1882
2388
  const block = blocks.get(`model:${modelName}`) ?? blocks.get(`enum:${modelName}`);
1883
2389
  if (!block) return {};
@@ -1900,7 +2406,7 @@ function injectFieldLines(targetPath, basePath, modelName, fieldKeys) {
1900
2406
  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
2407
  return 0;
1902
2408
  }
1903
- const content = readFileSync8(targetPath, "utf-8");
2409
+ const content = readFileSync10(targetPath, "utf-8");
1904
2410
  const lines = content.split("\n");
1905
2411
  const blocks = parseSchemaBlocks(content);
1906
2412
  const target = blocks.get(`model:${modelName}`) ?? blocks.get(`enum:${modelName}`);
@@ -1941,13 +2447,13 @@ function injectFieldLines(targetPath, basePath, modelName, fieldKeys) {
1941
2447
  ...newFieldLines,
1942
2448
  ...lines.slice(insertIdx)
1943
2449
  ];
1944
- writeFileSync5(targetPath, merged.join("\n"), "utf-8");
2450
+ writeFileSync7(targetPath, merged.join("\n"), "utf-8");
1945
2451
  return newFieldLines.length;
1946
2452
  }
1947
2453
  function overwriteFieldLines(targetPath, basePath, modelName, conflicts) {
1948
2454
  const baseFields = getBaseFieldLines(basePath, modelName);
1949
2455
  if (Object.keys(baseFields).length === 0) return 0;
1950
- let content = readFileSync8(targetPath, "utf-8");
2456
+ let content = readFileSync10(targetPath, "utf-8");
1951
2457
  let count = 0;
1952
2458
  for (const { fieldName } of conflicts) {
1953
2459
  const baseLine = baseFields[fieldName];
@@ -1958,13 +2464,13 @@ function overwriteFieldLines(targetPath, basePath, modelName, conflicts) {
1958
2464
  content = content.replace(fieldRegex, `${match[1]}${baseLine}`);
1959
2465
  count++;
1960
2466
  }
1961
- if (count > 0) writeFileSync5(targetPath, content, "utf-8");
2467
+ if (count > 0) writeFileSync7(targetPath, content, "utf-8");
1962
2468
  return count;
1963
2469
  }
1964
2470
 
1965
2471
  // src/cli/commands/status.ts
1966
- import { existsSync as existsSync11 } from "fs";
1967
- import { resolve as resolve8 } from "path";
2472
+ import { existsSync as existsSync13 } from "fs";
2473
+ import { resolve as resolve10 } from "path";
1968
2474
  async function statusCommand() {
1969
2475
  const projectRoot = process.cwd();
1970
2476
  const config = await loadConfig(projectRoot);
@@ -1973,7 +2479,7 @@ async function statusCommand() {
1973
2479
  const missing = [];
1974
2480
  const present = [];
1975
2481
  for (const [relPath] of coreFiles) {
1976
- if (existsSync11(resolve8(projectRoot, relPath))) {
2482
+ if (existsSync13(resolve10(projectRoot, relPath))) {
1977
2483
  present.push(relPath);
1978
2484
  } else {
1979
2485
  missing.push(relPath);
@@ -1991,8 +2497,8 @@ async function statusCommand() {
1991
2497
  }
1992
2498
 
1993
2499
  // src/cli/commands/stack.ts
1994
- import { readdirSync as readdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync12, mkdirSync as mkdirSync6 } from "fs";
1995
- import { resolve as resolve9, join as join6 } from "path";
2500
+ import { readdirSync as readdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
2501
+ import { resolve as resolve11, join as join8 } from "path";
1996
2502
  import { createHash as createHash3 } from "crypto";
1997
2503
  var MAGIC_DIR_MAP3 = { claude: ".claude", qoder: ".qoder", vscode: ".vscode", trae: ".trae", codex: ".codex" };
1998
2504
  var HASH_OUTPUT_FILE = ".add-coder-hash.json";
@@ -2009,14 +2515,14 @@ function resolveMagicDir(projectRoot, specified) {
2009
2515
  return MAGIC_DIR_MAP3[adapter];
2010
2516
  }
2011
2517
  function listCustomProfiles(projectRoot, magicDir, registryNames) {
2012
- const dir = resolve9(projectRoot, magicDir, "rules", "profiles");
2013
- if (!existsSync12(dir)) return [];
2014
- return readdirSync3(dir).filter((f) => f.endsWith(".md") && !registryNames.has(f.replace(/-profile\.md$/, ""))).sort();
2518
+ const dir = resolve11(projectRoot, magicDir, "rules", "profiles");
2519
+ if (!existsSync14(dir)) return [];
2520
+ return readdirSync5(dir).filter((f) => f.endsWith(".md") && !registryNames.has(f.replace(/-profile\.md$/, ""))).sort();
2015
2521
  }
2016
2522
  function profileExists(projectRoot, magicDir, name) {
2017
2523
  const registry = loadProfileRegistry();
2018
2524
  if (registry.some((p) => p.name === name)) return true;
2019
- return existsSync12(resolve9(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`));
2525
+ return existsSync14(resolve11(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`));
2020
2526
  }
2021
2527
  function stackCommand(sub, name, options = {}) {
2022
2528
  const projectRoot = process.cwd();
@@ -2053,12 +2559,12 @@ function stackCommand(sub, name, options = {}) {
2053
2559
  console.log("\u6280\u672F\u6808: \u672A\u8BBE\u7F6E\uFF08\u4E2D\u6027\uFF0C\u65E0\u6280\u672F\u6808\u5047\u8BBE\uFF09");
2054
2560
  return;
2055
2561
  }
2056
- const profilePath = resolve9(projectRoot, magicDir, "rules", "profiles", `${current}-profile.md`);
2057
- const stat = existsSync12(profilePath) ? readFileSync9(profilePath, "utf-8").length : 0;
2562
+ const profilePath = resolve11(projectRoot, magicDir, "rules", "profiles", `${current}-profile.md`);
2563
+ const stat = existsSync14(profilePath) ? readFileSync11(profilePath, "utf-8").length : 0;
2058
2564
  console.log(`\u6280\u672F\u6808: ${current}`);
2059
- console.log(`profile \u6587\u4EF6: ${profilePath}${existsSync12(profilePath) ? ` (${stat} \u5B57\u7B26)` : "\uFF08\u7F3A\u5931\uFF09"}`);
2565
+ console.log(`profile \u6587\u4EF6: ${profilePath}${existsSync14(profilePath) ? ` (${stat} \u5B57\u7B26)` : "\uFF08\u7F3A\u5931\uFF09"}`);
2060
2566
  try {
2061
- const raw = JSON.parse(readFileSync9(resolve9(projectRoot, magicDir, "stack.json"), "utf-8"));
2567
+ const raw = JSON.parse(readFileSync11(resolve11(projectRoot, magicDir, "stack.json"), "utf-8"));
2062
2568
  if (raw.updatedAt) console.log(`\u66F4\u65B0\u65F6\u95F4: ${raw.updatedAt}`);
2063
2569
  } catch {
2064
2570
  }
@@ -2083,9 +2589,9 @@ function buildConfig(projectRoot, magicDir, stack) {
2083
2589
  stack
2084
2590
  };
2085
2591
  try {
2086
- const pkgPath = resolve9(projectRoot, "package.json");
2087
- if (existsSync12(pkgPath)) {
2088
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
2592
+ const pkgPath = resolve11(projectRoot, "package.json");
2593
+ if (existsSync14(pkgPath)) {
2594
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
2089
2595
  if (pkg.name) config.projectName = pkg.name;
2090
2596
  }
2091
2597
  } catch {
@@ -2111,15 +2617,15 @@ function applyStack(projectRoot, magicDir, name) {
2111
2617
  }
2112
2618
  const hashMap = {};
2113
2619
  try {
2114
- Object.assign(hashMap, JSON.parse(readFileSync9(resolve9(projectRoot, magicDir, HASH_OUTPUT_FILE), "utf-8")));
2620
+ Object.assign(hashMap, JSON.parse(readFileSync11(resolve11(projectRoot, magicDir, HASH_OUTPUT_FILE), "utf-8")));
2115
2621
  } catch {
2116
2622
  }
2117
2623
  let written = 0;
2118
2624
  for (const [relPath, content] of stackRelated) {
2119
2625
  for (const t of [".add", magicDir]) {
2120
- const targetPath = resolve9(projectRoot, relPath.replace(/^\.add/, t));
2121
- mkdirSync6(join6(targetPath, ".."), { recursive: true });
2122
- writeFileSync6(targetPath, content, "utf-8");
2626
+ const targetPath = resolve11(projectRoot, relPath.replace(/^\.add/, t));
2627
+ mkdirSync8(join8(targetPath, ".."), { recursive: true });
2628
+ writeFileSync8(targetPath, content, "utf-8");
2123
2629
  hashMap[relPath.replace(/^\.add/, t)] = hash82(content);
2124
2630
  written++;
2125
2631
  }
@@ -2130,24 +2636,24 @@ function applyStack(projectRoot, magicDir, name) {
2130
2636
  };
2131
2637
  const registry = loadProfileRegistry();
2132
2638
  const isBuiltin = registry.some((p) => p.name === name);
2133
- const profilePathMagic = resolve9(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`);
2134
- const profilePathAdd = resolve9(projectRoot, ".add", "rules", "profiles", `${name}-profile.md`);
2135
- const projectRulesPath = resolve9(projectRoot, magicDir, "rules", "project_rules.md");
2136
- const projectRulesContent = existsSync12(projectRulesPath) ? readFileSync9(projectRulesPath, "utf-8") : "";
2639
+ const profilePathMagic = resolve11(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`);
2640
+ const profilePathAdd = resolve11(projectRoot, ".add", "rules", "profiles", `${name}-profile.md`);
2641
+ const projectRulesPath = resolve11(projectRoot, magicDir, "rules", "project_rules.md");
2642
+ const projectRulesContent = existsSync14(projectRulesPath) ? readFileSync11(projectRulesPath, "utf-8") : "";
2137
2643
  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 && !existsSync12(profilePathAdd)) fail(`profile \u672A\u5199\u5165 .add: ${profilePathAdd}`);
2139
- if (!existsSync12(profilePathMagic)) fail(`profile \u672A\u5199\u5165 ${magicDir}: ${profilePathMagic}`);
2644
+ if (isBuiltin && !existsSync14(profilePathAdd)) fail(`profile \u672A\u5199\u5165 .add: ${profilePathAdd}`);
2645
+ if (!existsSync14(profilePathMagic)) fail(`profile \u672A\u5199\u5165 ${magicDir}: ${profilePathMagic}`);
2140
2646
  if (!projectRulesContent.includes("**\u5F53\u524D\u6280\u672F\u6808**") || !projectRulesContent.includes(name)) fail(`project_rules.md \u672A\u5305\u542B ${name} \u5F15\u7528`);
2141
- writeFileSync6(resolve9(projectRoot, magicDir, HASH_OUTPUT_FILE), JSON.stringify(hashMap, null, 2) + "\n", "utf-8");
2647
+ writeFileSync8(resolve11(projectRoot, magicDir, HASH_OUTPUT_FILE), JSON.stringify(hashMap, null, 2) + "\n", "utf-8");
2142
2648
  console.log(`\u2705 \u6280\u672F\u6808\u5DF2\u8BBE\u7F6E\u4E3A ${name}`);
2143
2649
  console.log(` ${magicDir}/stack.json \u2192 ${name}`);
2144
- console.log(` ${magicDir}/rules/profiles/${name}-profile.md ${existsSync12(resolve9(projectRoot, magicDir, "rules", "profiles", `${name}-profile.md`)) ? "\u5DF2\u5C31\u4F4D" : "\uFF08\u81EA\u5B9A\u4E49 profile\uFF0C\u9879\u76EE\u4FA7\u6587\u4EF6\uFF09"}`);
2650
+ 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
2651
  console.log(` project_rules.md \u5F15\u7528\u884C\u5DF2\u66F4\u65B0 + hash \u5DF2\u5237\u65B0\uFF08${written} \u4E2A\u6587\u4EF6\uFF09`);
2146
2652
  }
2147
2653
 
2148
2654
  // src/cli/index.ts
2149
2655
  var { version } = JSON.parse(
2150
- readFileSync10(new URL("../package.json", import.meta.url), "utf-8")
2656
+ readFileSync12(new URL("../package.json", import.meta.url), "utf-8")
2151
2657
  );
2152
2658
  async function modelDownloadCommand(options) {
2153
2659
  try {