@dashai/cli 0.25.0 → 0.26.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/dist/bin.js CHANGED
@@ -828,6 +828,74 @@ var init_connect_status = __esm({
828
828
  SETUP_SETTINGS_PATH = "Modules \u2192 Installed modules \u2192 Technical \u2192 Data connections";
829
829
  }
830
830
  });
831
+ function declaredSecretNames(manifest) {
832
+ const names = [];
833
+ for (const s of manifest?.runtime?.secrets ?? []) {
834
+ if (typeof s?.key === "string" && SECRET_KEY.test(s.key) && !names.includes(s.key)) {
835
+ names.push(s.key);
836
+ }
837
+ }
838
+ return names;
839
+ }
840
+ function envLocalSetNames(projectRoot) {
841
+ const set = /* @__PURE__ */ new Set();
842
+ const path = join(projectRoot, ".env.local");
843
+ if (!existsSync(path)) return set;
844
+ for (const line of readFileSync(path, "utf-8").split(/\r?\n/)) {
845
+ const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
846
+ if (!m) continue;
847
+ const value = m[2].trim().replace(/^(['"])(.*)\1$/, "$2");
848
+ if (value.length > 0) set.add(m[1]);
849
+ }
850
+ return set;
851
+ }
852
+ function missingSecretNames(declared, env, envLocal2) {
853
+ return declared.filter((k) => !(env[k] !== void 0 && env[k] !== "") && !envLocal2.has(k));
854
+ }
855
+ function secretsBannerLine(declared, missing) {
856
+ if (declared.length === 0) return null;
857
+ if (missing.length === 0) return `${declared.length} declared, all set`;
858
+ return `${declared.length} declared, ${missing.length} missing: ${missing.join(", ")} (add ${missing.length === 1 ? "it" : "them"} to .env.local)`;
859
+ }
860
+ function withSecretLines(content, names) {
861
+ const mentioned = (k) => new RegExp(`^\\s*#?\\s*(?:export\\s+)?${k}\\s*=`, "m").test(content);
862
+ const add = names.filter((k) => !mentioned(k));
863
+ if (add.length === 0) return content;
864
+ const hasHeader = content.includes(ENV_LOCAL_SECRETS_HEADER[0]);
865
+ const block = [...hasHeader ? [] : ENV_LOCAL_SECRETS_HEADER, ...add.map((k) => `# ${k}=`)].join("\n");
866
+ const base = content.length === 0 || content.endsWith("\n") ? content : `${content}
867
+ `;
868
+ return `${base}${base.length > 0 && !hasHeader ? "\n" : ""}${block}
869
+ `;
870
+ }
871
+ function syncEnvLocalSecretLines(projectRoot, names) {
872
+ if (names.length === 0) return [];
873
+ const path = join(projectRoot, ".env.local");
874
+ const before = existsSync(path) ? readFileSync(path, "utf-8") : "";
875
+ const after = withSecretLines(before, names);
876
+ if (after === before) return [];
877
+ writeFileSync(path, after, "utf-8");
878
+ return names.filter((k) => !new RegExp(`^\\s*#?\\s*(?:export\\s+)?${k}\\s*=`, "m").test(before));
879
+ }
880
+ function reportSecretLines(added) {
881
+ if (added.length === 0) return;
882
+ info(
883
+ `Added ${added.map((k) => code(`# ${k}=`)).join(", ")} to ${code(".env.local")}: set dev values there. Hosted modules do not receive declared secrets yet.`
884
+ );
885
+ }
886
+ var SECRET_KEY, ENV_LOCAL_SECRETS_HEADER;
887
+ var init_declared_secrets = __esm({
888
+ "src/lib/declared-secrets.ts"() {
889
+ init_output();
890
+ SECRET_KEY = /^[A-Z][A-Z0-9_]*$/;
891
+ ENV_LOCAL_SECRETS_HEADER = [
892
+ "# Declared runtime secrets (module.json runtime.secrets[]). Set a dev value",
893
+ "# after the `=` and remove the `#`; `dashwise dev` names any that are missing.",
894
+ "# Hosted modules do not receive declared secrets yet (platform item U1);",
895
+ "# locally `dashwise dev` runs `next dev`, which reads them from this file."
896
+ ];
897
+ }
898
+ });
831
899
  function manifestPath(dir) {
832
900
  return join(resolve(dir ?? process.cwd()), "module.json");
833
901
  }
@@ -1172,6 +1240,12 @@ async function devCommand(options) {
1172
1240
  (Ensure you're logged into the right backend: ${code(`dashwise login --api-url ${apiUrl}`)}.)`
1173
1241
  );
1174
1242
  }
1243
+ {
1244
+ const declared = declaredSecretNames(readManifest(projectRoot));
1245
+ const missing = missingSecretNames(declared, childEnv, envLocalSetNames(projectRoot));
1246
+ const line = secretsBannerLine(declared, missing);
1247
+ if (line !== null) (missing.length > 0 ? warn : info)(` ${dim("secrets")} ${line}`);
1248
+ }
1175
1249
  info("");
1176
1250
  if (moduleEntry !== null && manifestSlug !== void 0) {
1177
1251
  await reportConnectStatus({
@@ -1412,13 +1486,13 @@ async function resolveDevPort(requested) {
1412
1486
  }
1413
1487
  async function isPortFree(port) {
1414
1488
  const { createServer } = await import('net');
1415
- return new Promise((resolve15) => {
1489
+ return new Promise((resolve16) => {
1416
1490
  const server = createServer();
1417
1491
  server.once("error", () => {
1418
- resolve15(false);
1492
+ resolve16(false);
1419
1493
  });
1420
1494
  server.once("listening", () => {
1421
- server.close(() => resolve15(true));
1495
+ server.close(() => resolve16(true));
1422
1496
  });
1423
1497
  server.listen(port, "0.0.0.0");
1424
1498
  });
@@ -1486,6 +1560,7 @@ var init_dev = __esm({
1486
1560
  init_api_client();
1487
1561
  init_connect_status();
1488
1562
  init_config();
1563
+ init_declared_secrets();
1489
1564
  init_manifest_io();
1490
1565
  init_manifest_poll();
1491
1566
  init_output();
@@ -12595,9 +12670,14 @@ dashwise module destroy-dev-install # tear down when done (cleans backend + aut
12595
12670
  - **No untyped \`any\`.** The generated SDK gives you types \u2014 use
12596
12671
  them. If you need an escape hatch, add a \`@ts-expect-error\` with
12597
12672
  a comment explaining why.
12598
- - **No secrets in code or env files.** Use \`runtime.secrets[]\` in
12599
- \`module.json\`; values are encrypted at rest and resolved at
12600
- runtime.
12673
+ - **No secrets in code or committed env files.** Declare each one in
12674
+ \`runtime.secrets[]\` in \`module.json\` (by name; values never go in the
12675
+ repo). Hosted modules do not receive declared secrets yet (platform item
12676
+ U1): until that lands, a hosted module cannot read them. Locally, put dev
12677
+ values in the gitignored \`.env.local\` (\`dashwise init\` and
12678
+ \`dashwise module generate\` add a commented \`# KEY=\` line per declared
12679
+ secret); \`dashwise dev\` runs \`next dev\`, which loads them, and names
12680
+ any that are missing.
12601
12681
 
12602
12682
  ## Pointers
12603
12683
 
@@ -13468,6 +13548,9 @@ async function moduleInitCommand(slugArg, options) {
13468
13548
  writeFileSync(absPath, content, "utf-8");
13469
13549
  written++;
13470
13550
  }
13551
+ reportSecretLines(
13552
+ syncEnvLocalSecretLines(dir, declaredSecretNames(readManifest(dir)))
13553
+ );
13471
13554
  }
13472
13555
  if (writeMode !== "none" && !cloudReady) {
13473
13556
  process.stdout.write(
@@ -13710,10 +13793,297 @@ var init_init = __esm({
13710
13793
  init_output();
13711
13794
  init_api_client();
13712
13795
  init_config();
13796
+ init_declared_secrets();
13713
13797
  init_manifest_io();
13714
13798
  init_workspaces();
13715
13799
  }
13716
13800
  });
13801
+
13802
+ // src/lib/manifest-evolution.ts
13803
+ function compareManifests(base, head, history = []) {
13804
+ const violations = [];
13805
+ const additions = [];
13806
+ const warnings = [];
13807
+ const v = (rule, where, detail) => violations.push({ rule, where, detail });
13808
+ const baseTables = tablesOf(base);
13809
+ const headTables = tablesOf(head);
13810
+ const everTables = /* @__PURE__ */ new Set();
13811
+ const everFields = /* @__PURE__ */ new Set();
13812
+ for (const m of history) {
13813
+ for (const [slug, t] of tablesOf(m)) {
13814
+ everTables.add(slug);
13815
+ for (const f of t.fields ?? []) everFields.add(`${slug}.${f.slug}`);
13816
+ }
13817
+ }
13818
+ for (const [slug, bt] of baseTables) {
13819
+ const ht = headTables.get(slug);
13820
+ if (!ht) {
13821
+ v("removed_table", slug, "tables are never removed; deprecate instead");
13822
+ continue;
13823
+ }
13824
+ const bf = fieldsOf(bt);
13825
+ const hf = fieldsOf(ht);
13826
+ for (const [fs, f] of bf) {
13827
+ const g = hf.get(fs);
13828
+ const where = `${slug}.${fs}`;
13829
+ if (!g) {
13830
+ v("removed_field", where, "fields are never removed; stop writing it and set human_edit: open");
13831
+ continue;
13832
+ }
13833
+ if (f.type !== g.type) v("retyped_field", where, `${f.type} \u2192 ${g.type}`);
13834
+ if ((f.decimal_places ?? null) !== (g.decimal_places ?? null)) {
13835
+ v("retyped_field", where, `decimal_places ${f.decimal_places ?? "none"} \u2192 ${g.decimal_places ?? "none"}`);
13836
+ }
13837
+ if ((f.target ?? null) !== (g.target ?? null)) v("retyped_field", where, `link target ${f.target} \u2192 ${g.target}`);
13838
+ if (f.required !== true && g.required === true) {
13839
+ v("required_tightened", where, "old rows are empty; enforce it in the tool layer instead");
13840
+ }
13841
+ const bo = f.options ?? [];
13842
+ const ho = g.options ?? [];
13843
+ for (const [i, o] of bo.entries()) {
13844
+ if (!ho.includes(o)) v("removed_option", where, `option "${String(o)}" removed`);
13845
+ else if (ho[i] !== o) {
13846
+ v("reordered_option", where, `option "${String(o)}" moved from ${i} to ${ho.indexOf(o)}; options are append-only`);
13847
+ }
13848
+ }
13849
+ for (const o of ho.slice(bo.length)) if (!bo.includes(o)) additions.push(`option ${where} "${String(o)}"`);
13850
+ }
13851
+ for (const [fs] of hf) {
13852
+ if (bf.has(fs)) continue;
13853
+ if (everFields.has(`${slug}.${fs}`)) {
13854
+ v("slug_reused", `${slug}.${fs}`, "this slug was removed in an earlier version and may not come back");
13855
+ } else additions.push(`field ${slug}.${fs}`);
13856
+ }
13857
+ const hu = new Set(uniqueIndexes(ht));
13858
+ for (const idx of uniqueIndexes(bt)) {
13859
+ if (!hu.has(idx)) v("dropped_unique_index", `${slug}(${idx})`, "a unique index is a protocol lock; it may not be dropped or changed");
13860
+ }
13861
+ const bu = new Set(uniqueIndexes(bt));
13862
+ for (const idx of hu) {
13863
+ if (bu.has(idx)) continue;
13864
+ const overExisting = idx.split(",").some((f) => f.length > 0 && bf.has(f));
13865
+ if (overExisting) {
13866
+ v("unique_on_populated", `${slug}(${idx})`, "a new unique index (indexes[].unique) over existing fields fails on duplicates already stored; add a new field instead");
13867
+ } else additions.push(`unique index ${slug}(${idx})`);
13868
+ }
13869
+ }
13870
+ for (const [slug] of headTables) {
13871
+ if (baseTables.has(slug)) continue;
13872
+ if (everTables.has(slug)) v("slug_reused", slug, "this table slug was removed in an earlier version and may not come back");
13873
+ else additions.push(`table ${slug}`);
13874
+ }
13875
+ const baseViews = new Set((base?.owns?.views ?? []).map((x) => x.slug));
13876
+ const headViews = new Set((head?.owns?.views ?? []).map((x) => x.slug));
13877
+ for (const s of baseViews) if (!headViews.has(s)) warnings.push(`view ${s} removed (callers of qbViews.${s} break)`);
13878
+ for (const s of headViews) if (!baseViews.has(s)) additions.push(`view ${s}`);
13879
+ return { violations, additions, warnings };
13880
+ }
13881
+ var tablesOf, fieldsOf, uniqueIndexes;
13882
+ var init_manifest_evolution = __esm({
13883
+ "src/lib/manifest-evolution.ts"() {
13884
+ tablesOf = (m) => new Map((m?.owns?.tables ?? []).map((t) => [t.slug, t]));
13885
+ fieldsOf = (t) => new Map((t?.fields ?? []).map((f) => [f.slug, f]));
13886
+ uniqueIndexes = (t) => (t?.indexes ?? []).filter((i) => i.unique === true).map((i) => (i.fields ?? []).join(","));
13887
+ }
13888
+ });
13889
+
13890
+ // src/commands/module/diff-check.ts
13891
+ var diff_check_exports = {};
13892
+ __export(diff_check_exports, {
13893
+ EvolutionCheckError: () => EvolutionCheckError,
13894
+ moduleDiffCheckCommand: () => moduleDiffCheckCommand,
13895
+ printEvolutionReport: () => printEvolutionReport,
13896
+ publishEvolutionGate: () => publishEvolutionGate,
13897
+ runEvolutionCheck: () => runEvolutionCheck
13898
+ });
13899
+ function readJson(path) {
13900
+ try {
13901
+ return JSON.parse(readFileSync(path, "utf8"));
13902
+ } catch (err) {
13903
+ throw new EvolutionCheckError(`could not read ${path}: ${err.message}`);
13904
+ }
13905
+ }
13906
+ function git(cwd, args) {
13907
+ try {
13908
+ return execFileSync("git", ["-C", cwd, ...args], {
13909
+ encoding: "utf8",
13910
+ stdio: ["ignore", "pipe", "pipe"],
13911
+ maxBuffer: 64 * 1024 * 1024
13912
+ });
13913
+ } catch (err) {
13914
+ const stderr = err.stderr?.trim();
13915
+ throw new EvolutionCheckError(`git ${args.join(" ")} failed${stderr ? `: ${stderr}` : ""}`);
13916
+ }
13917
+ }
13918
+ async function runEvolutionCheck(options) {
13919
+ const projectRoot = resolve(options.dir ?? process.cwd());
13920
+ const headPath = options.head ? resolve(options.head) : join(projectRoot, "module.json");
13921
+ if (!existsSync(headPath)) throw new EvolutionCheckError(`no manifest at ${headPath}`);
13922
+ const head = readJson(headPath);
13923
+ const baseFile = options.baseFile ?? (options.base && existsSync(options.base) ? options.base : void 0);
13924
+ if (baseFile) {
13925
+ const base = readJson(baseFile);
13926
+ return { ...compareManifests(base, head), base: baseFile, head: headPath, history: 0, nothingReleased: false };
13927
+ }
13928
+ if (options.base) {
13929
+ const ref = options.base;
13930
+ const base = JSON.parse(git(projectRoot, ["show", `${ref}:./module.json`]));
13931
+ let history2 = [];
13932
+ if (!options.noHistory) {
13933
+ const shas = git(projectRoot, ["log", "--format=%H", "--reverse", ref, "--", "module.json"]).split("\n").filter(Boolean);
13934
+ history2 = shas.slice(0, -1).map((sha) => JSON.parse(git(projectRoot, ["show", `${sha}:./module.json`])));
13935
+ }
13936
+ return {
13937
+ ...compareManifests(base, head, history2),
13938
+ base: `${ref}:module.json`,
13939
+ head: headPath,
13940
+ history: history2.length,
13941
+ nothingReleased: false
13942
+ };
13943
+ }
13944
+ const slug = head.module?.slug;
13945
+ if (!slug) throw new EvolutionCheckError(`${headPath} has no module.slug`);
13946
+ let mod;
13947
+ let versions;
13948
+ try {
13949
+ mod = await apiRequest(
13950
+ "GET",
13951
+ `/api/modules/by-slug/${encodeURIComponent(slug)}`
13952
+ );
13953
+ if (!mod.current_version) {
13954
+ return {
13955
+ violations: [],
13956
+ additions: [],
13957
+ warnings: [],
13958
+ base: "(nothing released)",
13959
+ head: headPath,
13960
+ history: 0,
13961
+ nothingReleased: true
13962
+ };
13963
+ }
13964
+ versions = await apiRequest("GET", `/api/modules/${mod.id}/versions`);
13965
+ } catch (err) {
13966
+ const why = err instanceof ApiError ? `${err.code || err.status} ${err.message}` : err.message;
13967
+ throw new EvolutionCheckError(
13968
+ `could not read the released manifest of ${slug} (${why}); pass --base <ref|file> to check offline`
13969
+ );
13970
+ }
13971
+ const released = versions.find((v) => v.version === mod.current_version);
13972
+ if (!released?.manifest_jsonb) {
13973
+ throw new EvolutionCheckError(
13974
+ `the platform returned no manifest for ${slug} v${mod.current_version}; pass --base <ref|file> to check offline`
13975
+ );
13976
+ }
13977
+ const releasedAt = released.created_on ? Date.parse(released.created_on) : Number.POSITIVE_INFINITY;
13978
+ const history = options.noHistory ? [] : versions.filter((v) => v !== released && v.manifest_jsonb && v.created_on && Date.parse(v.created_on) < releasedAt).sort((a, b) => Date.parse(a.created_on) - Date.parse(b.created_on)).map((v) => v.manifest_jsonb);
13979
+ return {
13980
+ ...compareManifests(released.manifest_jsonb, head, history),
13981
+ base: `${slug} v${mod.current_version} (released)`,
13982
+ head: headPath,
13983
+ history: history.length,
13984
+ nothingReleased: false
13985
+ };
13986
+ }
13987
+ function printEvolutionReport(r, print = out) {
13988
+ print(`module diff --check: ${r.head} against ${r.base} (${r.history} older version${r.history === 1 ? "" : "s"} scanned)`);
13989
+ for (const a of r.additions) print(` + ${a}`);
13990
+ for (const w of r.warnings) print(` ! ${w}`);
13991
+ for (const x of r.violations) print(` \u2717 ${x.rule}: ${x.where}: ${x.detail}`);
13992
+ }
13993
+ async function moduleDiffCheckCommand(options) {
13994
+ let r;
13995
+ try {
13996
+ r = await runEvolutionCheck(options);
13997
+ } catch (err) {
13998
+ if (err instanceof EvolutionCheckError) {
13999
+ if (options.json) out(JSON.stringify({ ok: false, error: { code: "CHECK_FAILED", message: err.message } }));
14000
+ else fail(`module diff --check: ${err.message}`);
14001
+ return 2;
14002
+ }
14003
+ throw err;
14004
+ }
14005
+ if (options.json) {
14006
+ out(JSON.stringify({ ok: r.violations.length === 0, ...r }, null, 2));
14007
+ return r.violations.length === 0 ? 0 : 1;
14008
+ }
14009
+ if (r.nothingReleased) {
14010
+ success("Nothing released yet: every change is additive.");
14011
+ return 0;
14012
+ }
14013
+ printEvolutionReport(r);
14014
+ if (r.violations.length === 0) {
14015
+ success("additive");
14016
+ return 0;
14017
+ }
14018
+ fail(`${r.violations.length} violation(s): not additive. Released copies would lose data or fail to upgrade.`);
14019
+ info(`Add a new field or table instead, and stop writing the old one (${code('human_edit: "open"')}).`);
14020
+ return 1;
14021
+ }
14022
+ async function publishEvolutionGate(projectRoot, strict) {
14023
+ let r;
14024
+ try {
14025
+ r = await runEvolutionCheck({ dir: projectRoot });
14026
+ } catch (err) {
14027
+ const msg = err instanceof Error ? err.message : String(err);
14028
+ if (strict) {
14029
+ fail(`Evolution check could not run (${msg}). Refusing with --strict. Nothing was published.`);
14030
+ return 1;
14031
+ }
14032
+ warn(`Evolution check skipped: ${msg}.`);
14033
+ return 0;
14034
+ }
14035
+ if (r.nothingReleased || r.violations.length === 0) return 0;
14036
+ printEvolutionReport(r, (s) => (strict ? fail : warn)(s));
14037
+ if (strict) {
14038
+ fail(
14039
+ `module.json is not an additive evolution of the released version (${r.violations.length} violation(s)). Refusing with --strict. Nothing was published.`
14040
+ );
14041
+ return 1;
14042
+ }
14043
+ warn(
14044
+ `module.json is not an additive evolution of the released version: installs that upgrade lose the data above. ${code("--strict")} turns this into a refusal; ${code("dashwise module diff --check")} shows it on its own.`
14045
+ );
14046
+ return 0;
14047
+ }
14048
+ var EvolutionCheckError;
14049
+ var init_diff_check = __esm({
14050
+ "src/commands/module/diff-check.ts"() {
14051
+ init_api_client();
14052
+ init_manifest_evolution();
14053
+ init_output();
14054
+ EvolutionCheckError = class extends Error {
14055
+ };
14056
+ }
14057
+ });
14058
+
14059
+ // src/lib/manifest-reference-lint.ts
14060
+ function referenceFieldWarnings(manifest) {
14061
+ const tables = manifest?.owns?.tables;
14062
+ if (!Array.isArray(tables)) return [];
14063
+ const warnings = [];
14064
+ for (const table of tables) {
14065
+ if (typeof table?.slug !== "string" || !Array.isArray(table.fields)) continue;
14066
+ for (const field of table.fields) {
14067
+ if (typeof field?.slug !== "string" || typeof field.type !== "string") continue;
14068
+ if (field.slug === "id" || !REFERENCE_NAME.test(field.slug)) continue;
14069
+ if (!PLAIN_TYPES.has(field.type)) continue;
14070
+ warnings.push({
14071
+ table: table.slug,
14072
+ field: field.slug,
14073
+ type: field.type,
14074
+ message: `${table.slug}.${field.slug} is a ${field.type} field named like a reference. If it points at a row in Dashwise, use a link_row field: plain ids break silently when data is copied to another installation. Ignore this for ids from outside systems.`
14075
+ });
14076
+ }
14077
+ }
14078
+ return warnings;
14079
+ }
14080
+ var REFERENCE_NAME, PLAIN_TYPES;
14081
+ var init_manifest_reference_lint = __esm({
14082
+ "src/lib/manifest-reference-lint.ts"() {
14083
+ REFERENCE_NAME = /(?:_id|_ref|Id|Ref)$/;
14084
+ PLAIN_TYPES = /* @__PURE__ */ new Set(["number", "text", "autonumber"]);
14085
+ }
14086
+ });
13717
14087
  function loadSchemaCommandContext(opts) {
13718
14088
  const projectRoot = resolve(opts.dir ?? process.cwd());
13719
14089
  const manifestPath2 = join(projectRoot, "module.json");
@@ -13786,6 +14156,89 @@ var init_dotenv_local = __esm({
13786
14156
  }
13787
14157
  });
13788
14158
 
14159
+ // src/lib/install-target.ts
14160
+ function keyDisplayPrefix(apiKey) {
14161
+ if (!apiKey.startsWith(KEY_SCHEME)) return null;
14162
+ const prefix = apiKey.slice(KEY_SCHEME.length, KEY_SCHEME.length + KEY_PREFIX_CHARS);
14163
+ return prefix.length === KEY_PREFIX_CHARS ? prefix : null;
14164
+ }
14165
+ async function keyBelongsToInstall(apiKey, installationId, options, now = /* @__PURE__ */ new Date()) {
14166
+ const prefix = keyDisplayPrefix(apiKey);
14167
+ if (prefix === null) return false;
14168
+ const { api_keys } = await listApiKeys(installationId, options);
14169
+ return api_keys.some(
14170
+ (k) => k.installation_id === installationId && k.key_prefix === prefix && (k.status === void 0 || k.status === "active") && k.revoked_at === null && (k.expires_at === null || new Date(k.expires_at) > now)
14171
+ );
14172
+ }
14173
+ function formatInstallTarget(id, detail) {
14174
+ return detail ? `\u2192 install #${id} (${detail})` : `\u2192 install #${id}`;
14175
+ }
14176
+ async function nameKeyTarget(args) {
14177
+ const { apiKey, keySource, entry, cfg, apiUrl, install } = args;
14178
+ const request = cfg ? { baseUrl: apiUrl, auth: { token: cfg.token }, signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) } : null;
14179
+ if (install === void 0) {
14180
+ if (keySource !== "cached" || !entry) {
14181
+ info(
14182
+ `\u2192 install not checked: ${code("DASHWISE_API_KEY")} comes from ${keySource === "environment" ? "the environment" : code(".env.local")}. Pass ${code("--install <id>")} to name its target and refuse a mismatch.`
14183
+ );
14184
+ return 0;
14185
+ }
14186
+ if (!request || entry.workspaceId === void 0) {
14187
+ info(formatInstallTarget(entry.installationId, "the cached dev install"));
14188
+ return 0;
14189
+ }
14190
+ try {
14191
+ const workspaces = await apiRequest("GET", "/api/workspaces", void 0, request);
14192
+ const ws = workspaces.find((w) => w.id === entry.workspaceId);
14193
+ info(formatInstallTarget(entry.installationId, `${ws ? ws.slug : `workspace #${entry.workspaceId}`}, local`));
14194
+ } catch {
14195
+ info(formatInstallTarget(entry.installationId));
14196
+ }
14197
+ return 0;
14198
+ }
14199
+ const source = keySource === "cached" ? "the cached key" : `${code("DASHWISE_API_KEY")} (from ${keySource === "environment" ? "the environment" : code(".env.local")})`;
14200
+ if (!request) {
14201
+ fail(
14202
+ `${code("--install")} looks the install up with your CLI login, and there is none. Run ${code("dashwise login")} first. Nothing was run.`
14203
+ );
14204
+ return 1;
14205
+ }
14206
+ let belongs;
14207
+ try {
14208
+ belongs = await keyBelongsToInstall(apiKey, install, request);
14209
+ } catch (err) {
14210
+ fail(
14211
+ `Could not read the keys of install #${install} to confirm ${source} (${describe(err)}). Refusing to run against an unconfirmed install. Nothing was run.`
14212
+ );
14213
+ return 1;
14214
+ }
14215
+ const cachedDev = entry?.installationId === install ? "the cached dev install, " : "";
14216
+ if (!belongs) {
14217
+ const other = entry && entry.installationId !== install && entry.apiKey === apiKey ? ` It is the cached key of install #${entry.installationId}.` : "";
14218
+ fail(`${source} is not a live key of install #${install}.${other} Nothing was run.`);
14219
+ info(
14220
+ `Use a key of install #${install} (mint one with ${code(`dashwise api-keys create <name> --install ${install}`)}), or drop ${code("--install")} to run against the key's own install.`
14221
+ );
14222
+ return 1;
14223
+ }
14224
+ info(formatInstallTarget(install, `${cachedDev}key checked`));
14225
+ return 0;
14226
+ }
14227
+ function describe(err) {
14228
+ if (err instanceof ApiError) return `${err.code || err.status} ${err.message}`;
14229
+ return err.message;
14230
+ }
14231
+ var KEY_PREFIX_CHARS, KEY_SCHEME, LOOKUP_TIMEOUT_MS;
14232
+ var init_install_target = __esm({
14233
+ "src/lib/install-target.ts"() {
14234
+ init_api_client();
14235
+ init_output();
14236
+ KEY_PREFIX_CHARS = 8;
14237
+ KEY_SCHEME = "dwk_";
14238
+ LOOKUP_TIMEOUT_MS = 1500;
14239
+ }
14240
+ });
14241
+
13789
14242
  // src/commands/run.ts
13790
14243
  var run_exports = {};
13791
14244
  __export(run_exports, {
@@ -13798,6 +14251,10 @@ async function runCommand(cmdArgs, options) {
13798
14251
  );
13799
14252
  return 1;
13800
14253
  }
14254
+ if (options.install !== void 0 && (!Number.isInteger(options.install) || options.install <= 0)) {
14255
+ fail(`--install expects a positive integer installation id (got "${options.install}").`);
14256
+ return 1;
14257
+ }
13801
14258
  const projectRoot = resolve(options.dir ?? process.cwd());
13802
14259
  let manifestSlug;
13803
14260
  try {
@@ -13810,6 +14267,7 @@ async function runCommand(cmdArgs, options) {
13810
14267
  fail(`module.json has no ${code("module.slug")} \u2014 the manifest is malformed.`);
13811
14268
  return 1;
13812
14269
  }
14270
+ const keyInShell = process.env.DASHWISE_API_KEY !== void 0;
13813
14271
  hydrateEnvFromDotenvLocal(projectRoot);
13814
14272
  const cfg = (() => {
13815
14273
  try {
@@ -13820,8 +14278,19 @@ async function runCommand(cmdArgs, options) {
13820
14278
  })();
13821
14279
  const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
13822
14280
  let apiKey = process.env.DASHWISE_API_KEY;
14281
+ let keySource = keyInShell ? "environment" : ".env.local";
14282
+ let entry = null;
13823
14283
  if (!apiKey) {
13824
- let entry = readModuleEntry(manifestSlug);
14284
+ entry = readModuleEntry(manifestSlug);
14285
+ if (entry && options.install !== void 0 && entry.installationId !== options.install) {
14286
+ fail(
14287
+ `No key for install #${options.install}: the cached ${code("dwk_")} key of ${code(manifestSlug)} belongs to install #${entry.installationId}. Nothing was run.`
14288
+ );
14289
+ info(
14290
+ `Set ${code("DASHWISE_API_KEY")} to a key of install #${options.install} (mint one with ${code(`dashwise api-keys create <name> --install ${options.install}`)}).`
14291
+ );
14292
+ return 1;
14293
+ }
13825
14294
  if (cfg) {
13826
14295
  const healed = await resolveModuleApiKey(
13827
14296
  manifestSlug,
@@ -13832,6 +14301,7 @@ async function runCommand(cmdArgs, options) {
13832
14301
  if (healed) entry = healed;
13833
14302
  }
13834
14303
  apiKey = entry?.apiKey;
14304
+ keySource = "cached";
13835
14305
  }
13836
14306
  if (!apiKey) {
13837
14307
  fail(
@@ -13842,6 +14312,15 @@ async function runCommand(cmdArgs, options) {
13842
14312
  );
13843
14313
  return 1;
13844
14314
  }
14315
+ const target = await nameKeyTarget({
14316
+ apiKey,
14317
+ keySource,
14318
+ entry: entry ?? readModuleEntry(manifestSlug),
14319
+ cfg,
14320
+ apiUrl,
14321
+ ...options.install !== void 0 ? { install: options.install } : {}
14322
+ });
14323
+ if (target !== 0) return target;
13845
14324
  const [cmd, ...rest] = cmdArgs;
13846
14325
  info(
13847
14326
  `${dim("run")} ${cmd} ${rest.join(" ")} ${dim(`(DASHWISE_API_URL=${apiUrl}, DASHWISE_API_KEY injected)`)}`
@@ -13871,6 +14350,7 @@ var init_run = __esm({
13871
14350
  "src/commands/run.ts"() {
13872
14351
  init_config();
13873
14352
  init_dotenv_local();
14353
+ init_install_target();
13874
14354
  init_manifest_io();
13875
14355
  init_output();
13876
14356
  init_dev();
@@ -14161,6 +14641,7 @@ async function schemaPushCommand(options) {
14161
14641
  return surfaceSchemaPushError(err, slug);
14162
14642
  }
14163
14643
  printApplySummary(applied);
14644
+ for (const w of referenceFieldWarnings(manifest)) warn(w.message);
14164
14645
  if (dwkAuth) {
14165
14646
  try {
14166
14647
  const { manifest: canonical } = await getInstallationSchema(dwkAuth);
@@ -14309,6 +14790,7 @@ var init_push = __esm({
14309
14790
  init_manifest_io();
14310
14791
  init_manifest_merge();
14311
14792
  init_output();
14793
+ init_manifest_reference_lint();
14312
14794
  }
14313
14795
  });
14314
14796
 
@@ -15866,8 +16348,8 @@ function startMcpProxy(opts) {
15866
16348
  let streamUnavailable = false;
15867
16349
  let streamStarted = false;
15868
16350
  let resolveDone;
15869
- const done = new Promise((resolve15) => {
15870
- resolveDone = resolve15;
16351
+ const done = new Promise((resolve16) => {
16352
+ resolveDone = resolve16;
15871
16353
  });
15872
16354
  const trace = (line) => {
15873
16355
  if (verbose) log2(line);
@@ -16353,7 +16835,7 @@ async function revokeSession(endpointId) {
16353
16835
  () => false
16354
16836
  );
16355
16837
  const timeout = new Promise(
16356
- (resolve15) => setTimeout(() => resolve15(false), REVOKE_TIMEOUT_MS).unref()
16838
+ (resolve16) => setTimeout(() => resolve16(false), REVOKE_TIMEOUT_MS).unref()
16357
16839
  );
16358
16840
  await Promise.race([attempt, timeout]);
16359
16841
  }
@@ -16531,7 +17013,7 @@ async function mcpAddCommand(options = {}) {
16531
17013
  fail(`Could not run \`claude\`: ${target.refused} Run the command above yourself.`);
16532
17014
  return 1;
16533
17015
  }
16534
- return await new Promise((resolve15) => {
17016
+ return await new Promise((resolve16) => {
16535
17017
  const child = spawn(target.command, target.args, { shell: target.shell, stdio: "inherit" });
16536
17018
  let settled = false;
16537
17019
  child.on("error", (err) => {
@@ -16541,16 +17023,16 @@ async function mcpAddCommand(options = {}) {
16541
17023
  fail(
16542
17024
  `Claude Code is not on your PATH, so nothing was registered. Run the command above once ${code("claude")} is installed.`
16543
17025
  );
16544
- resolve15(1);
17026
+ resolve16(1);
16545
17027
  return;
16546
17028
  }
16547
17029
  fail(`Could not run \`claude\`: ${err.message}`);
16548
- resolve15(1);
17030
+ resolve16(1);
16549
17031
  });
16550
17032
  child.on("close", (codeOut) => {
16551
17033
  if (settled) return;
16552
17034
  settled = true;
16553
- resolve15(codeOut ?? 1);
17035
+ resolve16(codeOut ?? 1);
16554
17036
  });
16555
17037
  });
16556
17038
  }
@@ -16669,10 +17151,10 @@ function upgradeCommand(plugin, marketplace) {
16669
17151
  var PLUGIN_NAME = "dashwise";
16670
17152
  var MARKETPLACE_NAME = "dashwise";
16671
17153
  var DEFAULT_MARKETPLACE_SOURCE = "Intuize/dashwise-cli";
16672
- var defaultRunner = (cmd, args, opts) => new Promise((resolve15) => {
17154
+ var defaultRunner = (cmd, args, opts) => new Promise((resolve16) => {
16673
17155
  const target = spawnTarget(cmd, args);
16674
17156
  if ("refused" in target) {
16675
- resolve15({ code: null, stdout: "", stderr: target.refused, missing: false });
17157
+ resolve16({ code: null, stdout: "", stderr: target.refused, missing: false });
16676
17158
  return;
16677
17159
  }
16678
17160
  const child = spawn(target.command, target.args, {
@@ -16687,7 +17169,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolve15) => {
16687
17169
  child.on("error", (err) => {
16688
17170
  if (settled) return;
16689
17171
  settled = true;
16690
- resolve15({
17172
+ resolve16({
16691
17173
  code: null,
16692
17174
  stdout,
16693
17175
  stderr: stderr || err.message,
@@ -16697,7 +17179,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolve15) => {
16697
17179
  child.on("close", (exitCode) => {
16698
17180
  if (settled) return;
16699
17181
  settled = true;
16700
- resolve15({ code: exitCode, stdout, stderr, missing: false });
17182
+ resolve16({ code: exitCode, stdout, stderr, missing: false });
16701
17183
  });
16702
17184
  });
16703
17185
  function readSkillsOnDisk() {
@@ -17938,6 +18420,7 @@ function judgeGeneratorStamp(stamp, opts = {}) {
17938
18420
  // src/commands/module/generate.ts
17939
18421
  init_sdk_version();
17940
18422
  init_output();
18423
+ init_declared_secrets();
17941
18424
  function outputDir(projectRoot) {
17942
18425
  return join(projectRoot, "node_modules", "@dashai", "generated");
17943
18426
  }
@@ -18014,6 +18497,9 @@ async function moduleGenerateCommand(options) {
18014
18497
  info(`Hash: ${dim(res.contentHash)}`);
18015
18498
  if (verdict.note !== null && declaresUses(manifest)) info(verdict.note);
18016
18499
  warnIfSdkBelowUsesFloor(projectRoot, manifest);
18500
+ reportSecretLines(
18501
+ syncEnvLocalSecretLines(projectRoot, declaredSecretNames(manifest))
18502
+ );
18017
18503
  return 0;
18018
18504
  }
18019
18505
  function declaresUses(manifest) {
@@ -18272,6 +18758,101 @@ init_init();
18272
18758
  // src/commands/module/install.ts
18273
18759
  init_api_client();
18274
18760
  init_config();
18761
+
18762
+ // src/lib/agent-session.ts
18763
+ var MARKERS = [
18764
+ ["CLAUDECODE", (v) => v === "1"],
18765
+ ["CLAUDE_CODE_REMOTE", (v) => v === "true"],
18766
+ ["CODEX_SANDBOX", (v) => v.length > 0],
18767
+ ["CODEX_SANDBOX_NETWORK_DISABLED", (v) => v.length > 0],
18768
+ ["CURSOR_AGENT", (v) => v.length > 0],
18769
+ ["DW_AGENT", (v) => v.length > 0],
18770
+ ["DASHWISE_AGENT_SESSION", (v) => v === "1" || v === "true"]
18771
+ ];
18772
+ function agentSessionMarker(env = process.env) {
18773
+ for (const [name, test] of MARKERS) {
18774
+ const value = env[name];
18775
+ if (value !== void 0 && test(value)) return name;
18776
+ }
18777
+ return null;
18778
+ }
18779
+
18780
+ // src/lib/drift-preview.ts
18781
+ init_api_client();
18782
+ function redactedConsumer(redacted) {
18783
+ switch (redacted) {
18784
+ case "private_copy":
18785
+ return "someone's private copy";
18786
+ case "other_team_copy":
18787
+ return "another team's copy";
18788
+ case "development_copy":
18789
+ return "a development copy you cannot see";
18790
+ default:
18791
+ return "a copy you cannot see";
18792
+ }
18793
+ }
18794
+ var WIDEN_WORDS = {
18795
+ uses_need_added: "a new table it reads from another module",
18796
+ uses_ops_widened: "more operations on a table it reads",
18797
+ uses_fields_added: "more fields of a table it reads",
18798
+ dependency_added: "a new or wider dependency",
18799
+ exposes_widened: "more of what it exposes to other modules",
18800
+ permissions_added: "more permissions",
18801
+ roles_widened: "more roles or permission keys",
18802
+ outbound_added: "new outbound APIs (data can leave)",
18803
+ secrets_added: "new secrets",
18804
+ public_access_enabled: "public access"
18805
+ };
18806
+ function describeWidening(widening) {
18807
+ return widening.map((w) => WIDEN_WORDS[w] ?? w).join(", ");
18808
+ }
18809
+ async function getUpgradePreview(installationId, version, options = {}) {
18810
+ return apiRequest(
18811
+ "GET",
18812
+ `/api/installations/${installationId}/upgrade-preview?version=${encodeURIComponent(version)}`,
18813
+ void 0,
18814
+ options
18815
+ );
18816
+ }
18817
+ function count(n, unit) {
18818
+ if (n === null) return `${unit}s not counted`;
18819
+ return `${n.toLocaleString("en-US")} ${unit}${n === 1 ? "" : "s"}`;
18820
+ }
18821
+ function describeDrift(drift) {
18822
+ const lines = [];
18823
+ const d = drift.details;
18824
+ const removedTables = d?.tables_to_remove ?? drift.tables_to_remove.map((t) => ({ table: t, name: t, rows: null }));
18825
+ for (const t of removedTables) {
18826
+ lines.push({
18827
+ loses: t.rows !== 0,
18828
+ text: `Moves table ${t.table} (${count(t.rows, "row")}) to the Trash`
18829
+ });
18830
+ }
18831
+ const removedFields = d?.fields_to_remove ?? drift.fields_to_remove.map((f) => ({ ...f, name: f.field, values: null }));
18832
+ for (const f of removedFields) {
18833
+ lines.push({
18834
+ loses: f.values !== 0,
18835
+ text: `Moves field ${f.table}.${f.field} (${count(f.values, "value")}) to the Trash`
18836
+ });
18837
+ }
18838
+ const retyped = d?.fields_to_retype ?? (drift.fields_to_retype ?? []).map((f) => ({ ...f, name: f.field, values: null }));
18839
+ for (const f of retyped) {
18840
+ lines.push({
18841
+ loses: f.values !== 0,
18842
+ text: `Converts field ${f.table}.${f.field} from ${f.from} to ${f.to} (${count(f.values, "value")}; a value that does not convert is lost)`
18843
+ });
18844
+ }
18845
+ lines.sort((a, b) => Number(b.loses) - Number(a.loses));
18846
+ for (const t of drift.tables_to_create) {
18847
+ lines.push({ loses: false, text: `Adds table ${t}` });
18848
+ }
18849
+ for (const f of drift.fields_to_create) {
18850
+ lines.push({ loses: false, text: `Adds field ${f.table}.${f.field}` });
18851
+ }
18852
+ return lines;
18853
+ }
18854
+
18855
+ // src/commands/module/install.ts
18275
18856
  init_manifest_io();
18276
18857
 
18277
18858
  // src/lib/module-lifecycle.ts
@@ -18450,18 +19031,31 @@ async function moduleInstallCommand(slugArg, options) {
18450
19031
  );
18451
19032
  }
18452
19033
  }
18453
- if (!options.yes && process.stdout.isTTY && !confirmedViaPicker) {
18454
- const message = devInstallToReplace ? isUpgrade ? `Destroy the dev install (revokes local-dev credentials) and upgrade ${code(mod.slug)} to ${code(`v${targetVersion}`)} in ${code(workspace2.slug)}?` : `Replace the dev install (revokes local-dev credentials) and install ${code(mod.slug)} ${code(`v${targetVersion}`)} into ${code(workspace2.slug)} as production?` : `${isUpgrade ? "Upgrade" : "Install"} ${code(mod.slug)} ${code(`v${targetVersion}`)} into workspace ${code(workspace2.slug)}?`;
19034
+ let preview;
19035
+ let lossy = false;
19036
+ let lossAccepted = false;
19037
+ if (existing) {
19038
+ const rerun = rerunLine(mod.slug, workspace2.slug, targetVersion, options, devInstallToReplace !== void 0);
19039
+ const previewed = await previewUpgrade(existing.id, targetVersion, options, auth, rerun, devInstallToReplace?.id);
19040
+ if (typeof previewed === "number") return previewed;
19041
+ preview = previewed.preview;
19042
+ lossy = previewed.lossy;
19043
+ lossAccepted = previewed.accepted;
19044
+ }
19045
+ const askForLoss = lossy && !lossAccepted;
19046
+ if (askForLoss || !options.yes && process.stdout.isTTY && !confirmedViaPicker) {
19047
+ const lossClause = lossy ? " It moves the data listed above to the Trash or converts it." : "";
19048
+ const message = devInstallToReplace ? isUpgrade ? `Destroy the dev install (revokes local-dev credentials) and upgrade ${code(mod.slug)} to ${code(`v${targetVersion}`)} in ${code(workspace2.slug)}?${lossClause}` : `Replace the dev install (revokes local-dev credentials) and install ${code(mod.slug)} ${code(`v${targetVersion}`)} into ${code(workspace2.slug)} as production?` : `${isUpgrade ? "Upgrade" : "Install"} ${code(mod.slug)} ${code(`v${targetVersion}`)} into workspace ${code(workspace2.slug)}?${lossClause}`;
18455
19049
  const answer = await select({
18456
19050
  message,
18457
19051
  options: [
18458
19052
  {
18459
19053
  value: "go",
18460
- label: devInstallToReplace ? isUpgrade ? "Yes, destroy the dev install + upgrade" : "Yes, replace the dev install + production-install" : isUpgrade ? "Yes, upgrade now" : "Yes, install now"
19054
+ label: devInstallToReplace ? isUpgrade ? lossy ? "Yes, destroy the dev install + upgrade and lose the data listed above" : "Yes, destroy the dev install + upgrade" : "Yes, replace the dev install + production-install" : isUpgrade ? lossy ? "Yes, upgrade and lose the data listed above" : "Yes, upgrade now" : "Yes, install now"
18461
19055
  },
18462
19056
  { value: "cancel", label: "No, abort" }
18463
19057
  ],
18464
- initialValue: devInstallToReplace ? "cancel" : "go"
19058
+ initialValue: devInstallToReplace || lossy ? "cancel" : "go"
18465
19059
  });
18466
19060
  if (isCancel(answer) || answer === "cancel") {
18467
19061
  fail("Aborted. Nothing was changed.");
@@ -18483,7 +19077,9 @@ async function moduleInstallCommand(slugArg, options) {
18483
19077
  workspaceId: workspace2.id,
18484
19078
  version: targetVersion,
18485
19079
  ...existing ? { installation_id: existing.id } : {},
18486
- ...options.force !== void 0 ? { force: options.force } : {}
19080
+ ...options.force !== void 0 ? { force: options.force } : {},
19081
+ // P5-08: confirms exactly the delta that was shown (409 DRIFT_CHANGED otherwise).
19082
+ ...preview ? { drift_hash: preview.drift_hash } : {}
18487
19083
  },
18488
19084
  auth
18489
19085
  );
@@ -18535,6 +19131,9 @@ async function moduleInstallCommand(slugArg, options) {
18535
19131
  was_upgrade: result.was_upgrade,
18536
19132
  public_url: publicUrl,
18537
19133
  verified,
19134
+ // P5-08: the confirmed preview; `null` for a fresh install (nothing to preview).
19135
+ drift_hash: preview?.drift_hash ?? null,
19136
+ data_loss: preview ? lossy : null,
18538
19137
  // `null` when the binding surface was unreachable; `[]` when the
18539
19138
  // module declares no slots. The two are not the same thing.
18540
19139
  uses: readiness === null ? null : usesInstallJson(readiness),
@@ -18703,6 +19302,19 @@ function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
18703
19302
  Ask the module's author to invite this workspace (or open the install gate), then retry.`
18704
19303
  );
18705
19304
  break;
19305
+ case "DRIFT_CHANGED":
19306
+ fail(
19307
+ `Install ${code(slug)} changed since its upgrade preview, so the confirmed change no longer matches. Nothing was changed.
19308
+ Re-run ${code("dashwise module install")} to see the new preview.`
19309
+ );
19310
+ break;
19311
+ case "DATA_LOSS_CONFIRM_REQUIRED":
19312
+ case "UPGRADE_CONFIRM_REQUIRED":
19313
+ fail(
19314
+ `The platform asks for a confirmed preview before this upgrade (${err.code}). Nothing was changed.
19315
+ Re-run ${code("dashwise module install")}: it previews the upgrade and sends the confirmation.`
19316
+ );
19317
+ break;
18706
19318
  case "UPGRADE_BREAKS_CONSUMERS":
18707
19319
  fail(
18708
19320
  `Upgrading ${code(slug)} would break modules that consume its current schema.
@@ -18752,6 +19364,75 @@ function usesInstallJson(readiness) {
18752
19364
  }))
18753
19365
  };
18754
19366
  }
19367
+ async function previewUpgrade(installationId, targetVersion, options, auth, rerun, devInstallToReplaceId) {
19368
+ let preview;
19369
+ try {
19370
+ preview = await getUpgradePreview(installationId, targetVersion, auth);
19371
+ } catch (err) {
19372
+ const why = err instanceof ApiError ? `${err.code || err.status} ${err.message}` : err.message;
19373
+ const checkNote = options.acceptDataLoss !== void 0 ? `, so ${code("--accept-data-loss")} cannot be checked` : "";
19374
+ fail(
19375
+ `Could not preview what upgrading install #${installationId} to ${code(`v${targetVersion}`)} does to its data (${why})${checkNote}. Nothing was changed.`
19376
+ );
19377
+ info(`An upgrade is never applied without its preview; retry when the platform answers.`);
19378
+ return 1;
19379
+ }
19380
+ const lines = describeDrift(preview.drift);
19381
+ const lossy = lines.some((l) => l.loses);
19382
+ const from = preview.from_version ? `v${preview.from_version}` : "its current version";
19383
+ if (lines.length === 0) {
19384
+ info(`Upgrading install #${installationId} from ${from} to v${preview.to_version} changes no table or field.`);
19385
+ } else {
19386
+ info(`What upgrading install #${installationId} from ${from} to v${preview.to_version} does to its data:`);
19387
+ for (const line of lines) {
19388
+ (line.loses ? warn : info)(` ${line.text}`);
19389
+ }
19390
+ }
19391
+ const broken = preview.breaks_consumers ?? [];
19392
+ if (broken.length > 0) {
19393
+ const names = broken.map(
19394
+ (c2) => c2.module_slug ? `${c2.module_slug}${c2.installation_id ? ` #${c2.installation_id}` : ""}` : redactedConsumer(c2.redacted)
19395
+ );
19396
+ warn(` Breaks ${broken.length} module${broken.length === 1 ? "" : "s"} that read${broken.length === 1 ? "s" : ""} this one: ${names.join(", ")}`);
19397
+ }
19398
+ if (preview.approval_required) {
19399
+ const asks = preview.widening && preview.widening.length > 0 ? `: it asks for ${describeWidening(preview.widening)}` : "";
19400
+ warn(` This update waits for the module author's approval${asks}.`);
19401
+ }
19402
+ if (options.acceptDataLoss !== void 0 && options.acceptDataLoss !== preview.drift_hash) {
19403
+ fail(
19404
+ `${code("--accept-data-loss")} does not match this upgrade's preview: the copy or the release changed since that hash was printed. Nothing was changed.`
19405
+ );
19406
+ info(`To accept the change listed above, a person re-runs:
19407
+ ${rerun(preview.drift_hash)}`);
19408
+ return 1;
19409
+ }
19410
+ const accepted = lossy && options.acceptDataLoss === preview.drift_hash;
19411
+ if (lossy && !accepted) {
19412
+ const yesNote = options.yes ? ` ${code("--yes")} does not accept data loss.` : "";
19413
+ const marker = agentSessionMarker();
19414
+ const why = marker ? `this is an agent session (${code(marker)} is set): a person decides` : !process.stdout.isTTY ? "there is no terminal to ask in" : null;
19415
+ if (why) {
19416
+ const alsoDev = devInstallToReplaceId !== void 0 ? ` and destroys dev install #${devInstallToReplaceId} first` : "";
19417
+ fail(`This upgrade loses the data listed above${alsoDev}, and ${why}.${yesNote} Refused. Nothing was changed.`);
19418
+ info(`To accept exactly that loss, a person re-runs:
19419
+ ${rerun(preview.drift_hash)}`);
19420
+ return 1;
19421
+ }
19422
+ }
19423
+ return { preview, lossy, accepted };
19424
+ }
19425
+ function rerunLine(slug, workspaceSlug, targetVersion, options, replacingDevInstall) {
19426
+ return (hash) => [
19427
+ "dashwise module install",
19428
+ slug,
19429
+ `--workspace ${workspaceSlug}`,
19430
+ `--version ${targetVersion}`,
19431
+ ...options.force ? ["--force"] : [],
19432
+ ...replacingDevInstall ? ["--replace-dev-install"] : [],
19433
+ `--accept-data-loss ${hash}`
19434
+ ].join(" ");
19435
+ }
18755
19436
 
18756
19437
  // src/commands/module/list.ts
18757
19438
  init_api_client();
@@ -19121,6 +19802,7 @@ function formatBytes(n) {
19121
19802
  }
19122
19803
 
19123
19804
  // src/commands/module/publish.ts
19805
+ init_diff_check();
19124
19806
  var VALID_BUMP = /* @__PURE__ */ new Set(["patch", "minor", "major"]);
19125
19807
  var PACKED_FILE_LIST_CAP = 20;
19126
19808
  function packedFileLines(files, cap = PACKED_FILE_LIST_CAP) {
@@ -19207,6 +19889,10 @@ async function modulePublishCommand(options) {
19207
19889
  info(
19208
19890
  `Module: ${code(mod.slug)} (id ${mod.id})${mod.current_version ? `, current ${code(`v${mod.current_version}`)}` : dim(", no published version yet")}`
19209
19891
  );
19892
+ if (mod.current_version) {
19893
+ const gate = await publishEvolutionGate(projectRoot, options.strict === true);
19894
+ if (gate !== 0) return gate;
19895
+ }
19210
19896
  let tarball;
19211
19897
  try {
19212
19898
  tarball = await packTarball({
@@ -19798,7 +20484,7 @@ async function moduleReleaseCommand(slugArg, options) {
19798
20484
  }
19799
20485
  async function wait() {
19800
20486
  const remaining = Math.max(0, deadline - Date.now());
19801
- await new Promise((resolve15) => setTimeout(resolve15, Math.min(options.pollIntervalMs ?? 4e3, remaining)));
20487
+ await new Promise((resolve16) => setTimeout(resolve16, Math.min(options.pollIntervalMs ?? 4e3, remaining)));
19802
20488
  }
19803
20489
  }
19804
20490
 
@@ -20220,6 +20906,7 @@ async function modulePullDataCommand(options) {
20220
20906
  // src/commands/module/validate.ts
20221
20907
  init_api_client();
20222
20908
  init_output();
20909
+ init_manifest_reference_lint();
20223
20910
  async function moduleValidateCommand(options) {
20224
20911
  const projectRoot = resolve(options.dir ?? process.cwd());
20225
20912
  const manifestPath2 = join(projectRoot, "module.json");
@@ -20244,6 +20931,7 @@ async function moduleValidateCommand(options) {
20244
20931
  );
20245
20932
  if (res.ok) {
20246
20933
  success("Manifest is valid.");
20934
+ for (const w of referenceFieldWarnings(manifest)) warn(w.message);
20247
20935
  return 0;
20248
20936
  }
20249
20937
  fail(`Unexpected response shape: ${JSON.stringify(res)}`);
@@ -22646,6 +23334,23 @@ init_api_client();
22646
23334
  init_config();
22647
23335
  init_manifest_io();
22648
23336
  init_output();
23337
+ init_declared_secrets();
23338
+ var SECRETS_NOTE = "# Declared secrets (runtime.secrets[]): set each value on your host; Dashwise never exports a secret value.";
23339
+ function secretNameLines(names, format) {
23340
+ const lines = names.map((name) => {
23341
+ switch (format) {
23342
+ case "env":
23343
+ return `# ${name}=`;
23344
+ case "railway":
23345
+ return `# ${name}: railway variables set ${name}='<value>'`;
23346
+ case "vercel":
23347
+ return `# ${name}: vercel env add ${name} production`;
23348
+ case "fly":
23349
+ return `# ${name}: fly secrets set ${name}='<value>'`;
23350
+ }
23351
+ });
23352
+ return [SECRETS_NOTE, ...lines].join("\n");
23353
+ }
22649
23354
  var PLACEHOLDER = "<run-`dashwise-api-keys-create-<name>`-and-paste-here>";
22650
23355
  async function envExportCommand(options) {
22651
23356
  const format = options.format ?? "env";
@@ -22736,7 +23441,19 @@ async function envExportCommand(options) {
22736
23441
  ...apiKeyComment !== null ? { comment: apiKeyComment } : {}
22737
23442
  }
22738
23443
  ];
23444
+ const secretNames = options.withSecretNames ? declaredSecretNames(manifest) : [];
22739
23445
  process.stdout.write(render(entries, format));
23446
+ if (secretNames.length > 0) {
23447
+ if (format === "json") {
23448
+ info(
23449
+ `Declared secrets to set on your host (Dashwise never exports a secret value): ${secretNames.join(", ")}`
23450
+ );
23451
+ } else {
23452
+ process.stdout.write(`
23453
+
23454
+ ${secretNameLines(secretNames, format)}`);
23455
+ }
23456
+ }
22740
23457
  if (format !== "json") process.stdout.write("\n");
22741
23458
  if (apiKey === PLACEHOLDER) {
22742
23459
  info("");
@@ -22792,6 +23509,7 @@ init_config();
22792
23509
  init_dotenv_local();
22793
23510
  init_manifest_io();
22794
23511
  init_output();
23512
+ init_install_target();
22795
23513
  init_dev();
22796
23514
  async function queryCommand(options) {
22797
23515
  const projectRoot = resolve(options.dir ?? process.cwd());
@@ -22814,17 +23532,25 @@ async function queryCommand(options) {
22814
23532
  );
22815
23533
  return 1;
22816
23534
  }
23535
+ const keyInShell = process.env.DASHWISE_API_KEY !== void 0;
22817
23536
  hydrateEnvFromDotenvLocal(projectRoot);
23537
+ const cfg = (() => {
23538
+ try {
23539
+ return readConfig();
23540
+ } catch {
23541
+ return null;
23542
+ }
23543
+ })();
23544
+ const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
23545
+ let keySource = keyInShell ? "environment" : ".env.local";
23546
+ let entry = manifestSlug ? readModuleEntry(manifestSlug) : null;
22818
23547
  if (!process.env.DASHWISE_API_KEY && manifestSlug) {
22819
- const cfg = (() => {
22820
- try {
22821
- return readConfig();
22822
- } catch {
22823
- return null;
22824
- }
22825
- })();
22826
- const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
22827
- let entry = readModuleEntry(manifestSlug);
23548
+ if (entry && options.install !== void 0 && entry.installationId !== options.install) {
23549
+ fail(
23550
+ `No key for install #${options.install}: the cached ${code("dwk_")} key of ${code(manifestSlug)} belongs to install #${entry.installationId}. Nothing was run.`
23551
+ );
23552
+ return 1;
23553
+ }
22828
23554
  if (cfg) {
22829
23555
  const healed = await resolveModuleApiKey(
22830
23556
  manifestSlug,
@@ -22836,11 +23562,26 @@ async function queryCommand(options) {
22836
23562
  }
22837
23563
  if (entry?.apiKey) {
22838
23564
  process.env.DASHWISE_API_KEY = entry.apiKey;
23565
+ keySource = "cached";
22839
23566
  if (!process.env.DASHWISE_API_URL) {
22840
23567
  process.env.DASHWISE_API_URL = apiUrl;
22841
23568
  }
22842
23569
  }
22843
23570
  }
23571
+ if (process.env.DASHWISE_API_KEY) {
23572
+ const target = await nameKeyTarget({
23573
+ apiKey: process.env.DASHWISE_API_KEY,
23574
+ keySource,
23575
+ entry,
23576
+ cfg,
23577
+ apiUrl: process.env.DASHWISE_API_URL ?? apiUrl,
23578
+ ...options.install !== void 0 ? { install: options.install } : {}
23579
+ });
23580
+ if (target !== 0) return target;
23581
+ } else if (options.install !== void 0) {
23582
+ fail(`No ${code("dwk_")} key to check against install #${options.install}. Set ${code("DASHWISE_API_KEY")}. Nothing was run.`);
23583
+ return 1;
23584
+ }
22844
23585
  const modes = [
22845
23586
  options.inline !== void 0 ? "inline" : null,
22846
23587
  options.file !== void 0 ? "file" : null,
@@ -23206,7 +23947,7 @@ function formatTimestamp(iso) {
23206
23947
  return iso.slice(0, 19).replace("T", " ");
23207
23948
  }
23208
23949
  function sleep2(ms) {
23209
- return new Promise((resolve15) => setTimeout(resolve15, ms));
23950
+ return new Promise((resolve16) => setTimeout(resolve16, ms));
23210
23951
  }
23211
23952
 
23212
23953
  // src/commands/module/action-log.ts
@@ -23308,7 +24049,7 @@ async function runTail2(installationId, options) {
23308
24049
  }
23309
24050
  dim(`(tail: transient error \u2014 ${err.message})`);
23310
24051
  }
23311
- await new Promise((resolve15) => setTimeout(resolve15, POLL_INTERVAL_MS2));
24052
+ await new Promise((resolve16) => setTimeout(resolve16, POLL_INTERVAL_MS2));
23312
24053
  }
23313
24054
  }
23314
24055
  function printTable(rows) {
@@ -23693,7 +24434,7 @@ async function sleep3(ms, aborted) {
23693
24434
  const slice = 100;
23694
24435
  let waited = 0;
23695
24436
  while (waited < ms && !aborted()) {
23696
- await new Promise((resolve15) => setTimeout(resolve15, Math.min(slice, ms - waited)));
24437
+ await new Promise((resolve16) => setTimeout(resolve16, Math.min(slice, ms - waited)));
23697
24438
  waited += slice;
23698
24439
  }
23699
24440
  }
@@ -24524,12 +25265,29 @@ moduleCmd.command("list").description("List modules you own (or `--all` for owne
24524
25265
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
24525
25266
  });
24526
25267
  });
24527
- moduleCmd.command("diff").description("Show what would change at the next publish vs the currently-published version.").option("--dir <path>", "Project root (default: current directory).").option("--json", "Emit the structured diff as JSON instead of the human-readable summary.").action(async (cmdOpts) => {
24528
- process.exitCode = await moduleDiffCommand({
24529
- ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
24530
- ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
24531
- });
24532
- });
25268
+ moduleCmd.command("diff").description(
25269
+ "Show what would change at the next publish vs the currently-published version. With --check: exit 1 when module.json is not an additive evolution of the released manifest (a removed table or field, a retype, a removed or reordered option, a new unique index (`indexes[].unique`) over an existing field, a dropped unique index, required tightened, or a slug reused after an earlier removal), 2 on a usage or read error. Offline with --base."
25270
+ ).option("--dir <path>", "Project root (default: current directory).").option("--json", "Emit the structured diff as JSON instead of the human-readable summary.").option("--check", "Check additive evolution against the released manifest (exit 0 additive, 1 not additive, 2 error).").option("--base <ref|path>", "With --check: the base manifest from a git ref (its module.json, history from its log) or, when the path exists, a file. Default: the released version on the platform.").option("--base-file <path>", "With --check: the base manifest from a file.").option("--head <path>", "With --check: the manifest to check (default: <dir>/module.json).").option("--no-history", "With --check: skip the slug-reuse scan of older versions.").option("--dry-run", "Accepted for uniformity; --check never writes anything.").action(
25271
+ async (cmdOpts) => {
25272
+ if (cmdOpts.check) {
25273
+ const { moduleDiffCheckCommand: moduleDiffCheckCommand2 } = await Promise.resolve().then(() => (init_diff_check(), diff_check_exports));
25274
+ process.exitCode = await moduleDiffCheckCommand2({
25275
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
25276
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
25277
+ ...cmdOpts.base !== void 0 ? { base: cmdOpts.base } : {},
25278
+ ...cmdOpts.baseFile !== void 0 ? { baseFile: cmdOpts.baseFile } : {},
25279
+ ...cmdOpts.head !== void 0 ? { head: cmdOpts.head } : {},
25280
+ // commander maps `--no-history` to `history === false`.
25281
+ ...cmdOpts.history === false ? { noHistory: true } : {}
25282
+ });
25283
+ return;
25284
+ }
25285
+ process.exitCode = await moduleDiffCommand({
25286
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
25287
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
25288
+ });
25289
+ }
25290
+ );
24533
25291
  moduleCmd.command("pull-data").description("Export the module's own-table rows to a local JSON file (for fixtures, local dev seeding).").option("--dir <path>", "Project root (default: current directory).").option("--limit <n>", "Max rows per table (1-1000, default 100).", parseIntOption).option("--out <path>", "Output file path (default: ./dashwise-data.json).").action(async (cmdOpts) => {
24534
25292
  process.exitCode = await modulePullDataCommand({
24535
25293
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
@@ -24681,6 +25439,10 @@ program.command("query").description(
24681
25439
  ).option(
24682
25440
  "--stream",
24683
25441
  "Call .stream() instead of .execute() \u2014 prints rows incrementally as JSON-lines (one row per line). Lets you see progress on big queries + Ctrl-C cleanly midway. Mutually exclusive with --explain and --repl."
25442
+ ).option(
25443
+ "--install <id>",
25444
+ "The install the key must belong to: reads that install's key list with your CLI login (one request, 1.5 s), prints `\u2192 install #<id> (key checked)` and refuses (exit 1) when DASHWISE_API_KEY is not an active key of that install or the list cannot be read.",
25445
+ parseIntOption
24684
25446
  ).action(
24685
25447
  async (cmdOpts) => {
24686
25448
  process.exitCode = await queryCommand({
@@ -24690,16 +25452,22 @@ program.command("query").description(
24690
25452
  ...cmdOpts.repl !== void 0 ? { repl: cmdOpts.repl } : {},
24691
25453
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
24692
25454
  ...cmdOpts.explain !== void 0 ? { explain: cmdOpts.explain } : {},
24693
- ...cmdOpts.stream !== void 0 ? { stream: cmdOpts.stream } : {}
25455
+ ...cmdOpts.stream !== void 0 ? { stream: cmdOpts.stream } : {},
25456
+ ...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {}
24694
25457
  });
24695
25458
  }
24696
25459
  );
24697
25460
  program.command("run [cmd...]").description(
24698
- "Run a command with the module's Dashwise env injected \u2014 resolves DASHWISE_API_URL + DASHWISE_API_KEY through the same chain as `dashwise query` (explicit env \u2192 .env.local \u2192 the cached dwk_ key) and spawns the command with them overlaid, inheriting stdio and propagating the exit code. Example: `dashwise run -- node seed.mjs`."
24699
- ).option("--dir <path>", "Project root containing `module.json` (default: current directory).").action(async (cmdArgs, cmdOpts) => {
25461
+ "Run a command with the module's Dashwise env injected \u2014 resolves DASHWISE_API_URL + DASHWISE_API_KEY through the same chain as `dashwise query` (explicit env \u2192 .env.local \u2192 the cached dwk_ key) and spawns the command with them overlaid, inheriting stdio and propagating the exit code. Names its target first (`\u2192 install #<id> (\u2026)`, at most one lookup request); `--install <id>` refuses a key of any other install. Example: `dashwise run --install 42 -- node seed.mjs`."
25462
+ ).option("--dir <path>", "Project root containing `module.json` (default: current directory).").option(
25463
+ "--install <id>",
25464
+ "The install the key must belong to. Reads that install's key list with your CLI login (one request, 1.5 s), prints `\u2192 install #<id> (key checked)` and refuses (exit 1, nothing run) when DASHWISE_API_KEY is not an active key of that install or the list cannot be read.",
25465
+ parseIntOption
25466
+ ).action(async (cmdArgs, cmdOpts) => {
24700
25467
  const { runCommand: runCommand2 } = await Promise.resolve().then(() => (init_run(), run_exports));
24701
25468
  process.exitCode = await runCommand2(cmdArgs ?? [], {
24702
- ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
25469
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
25470
+ ...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {}
24703
25471
  });
24704
25472
  });
24705
25473
  program.command("pull").description(
@@ -24826,13 +25594,17 @@ envCmd.command("export").description(
24826
25594
  ).option(
24827
25595
  "--key <dwk_value>",
24828
25596
  "Use a previously-minted API key (paste plaintext). (Mutually exclusive with --new-key.)"
25597
+ ).option(
25598
+ "--with-secret-names",
25599
+ "Also name each secret declared in module.json runtime.secrets[] as a commented `# KEY=` line (a comment naming the command for railway/vercel/fly; stderr for json). Never a value: values never leave Dashwise; set them on your host."
24829
25600
  ).action(
24830
25601
  async (cmdOpts) => {
24831
25602
  process.exitCode = await envExportCommand({
24832
25603
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
24833
25604
  ...cmdOpts.format !== void 0 ? { format: cmdOpts.format } : {},
24834
25605
  ...cmdOpts.newKey !== void 0 ? { newKey: cmdOpts.newKey } : {},
24835
- ...cmdOpts.key !== void 0 ? { key: cmdOpts.key } : {}
25606
+ ...cmdOpts.key !== void 0 ? { key: cmdOpts.key } : {},
25607
+ ...cmdOpts.withSecretNames ? { withSecretNames: true } : {}
24836
25608
  });
24837
25609
  }
24838
25610
  );
@@ -24858,7 +25630,10 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
24858
25630
  ).option(
24859
25631
  "--timeout <seconds>",
24860
25632
  "Max seconds to wait for an async build before giving up polling (default 360)."
24861
- ).option("--json", "Emit the final build result as machine-readable JSON.").action(
25633
+ ).option("--json", "Emit the final build result as machine-readable JSON.").option(
25634
+ "--strict",
25635
+ "Refuse to publish when module.json is not an additive evolution of the released version (the `module diff --check` rules), or when that check cannot run. Without it the check only warns."
25636
+ ).action(
24862
25637
  async (cmdOpts) => {
24863
25638
  const timeoutSec = cmdOpts.timeout !== void 0 ? Number(cmdOpts.timeout) : void 0;
24864
25639
  process.exitCode = await modulePublishCommand({
@@ -24874,19 +25649,23 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
24874
25649
  // commander maps `--no-wait` to `wait === false`.
24875
25650
  ...cmdOpts.wait === false ? { noWait: true } : {},
24876
25651
  ...timeoutSec !== void 0 && Number.isFinite(timeoutSec) ? { timeoutSec } : {},
24877
- ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
25652
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
25653
+ ...cmdOpts.strict !== void 0 ? { strict: cmdOpts.strict } : {}
24878
25654
  });
24879
25655
  }
24880
25656
  );
24881
- moduleCmd.command("install [slug]").alias("deploy").description(
24882
- 'Install (deploy) a published module into a workspace \u2014 provisions the runtime and prints the public URL. Upgrades/repairs an existing production install idempotently. When only a dev install exists it asks whether to install alongside it (--keep-dev-install) or replace it (--replace-dev-install), rather than silently landing on the dev row. Exits 3 \u2014 "deployed, setup pending" \u2014 when a required `uses` slot is still unbound: the deploy succeeded but the module throws on its first data call until an admin binds it.'
25657
+ moduleCmd.command("install [slug]").alias("deploy").alias("upgrade").description(
25658
+ "Install (deploy) a published module into a workspace \u2014 provisions the runtime and prints the public URL. Upgrades/repairs an existing production install idempotently: an upgrade first prints what it does to the copy's data (tables and fields it moves to the Trash or converts, with their counts), asks once, and sends the preview's drift_hash. A move that loses data is accepted only by a person: the one question in a terminal, or --accept-data-loss <drift_hash> with the hash the preview printed. --yes never accepts data loss; with no terminal, or inside an AI agent session, a lossy move is refused and the refusal prints the re-run line. When only a dev install exists it asks whether to install alongside it (--keep-dev-install) or replace it (--replace-dev-install), rather than silently landing on the dev row. Exits 3 \u2014 \"deployed, setup pending\" \u2014 when a required `uses` slot is still unbound: the deploy succeeded but the module throws on its first data call until an admin binds it."
24883
25659
  ).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--version <semver>", "Install a specific published version (default: latest).").option("--force", "Proceed even when the upgrade breaks consumer modules (UPGRADE_BREAKS_CONSUMERS).").option(
24884
25660
  "--keep-dev-install",
24885
25661
  "Install production ALONGSIDE the workspace's kind=local dev install, leaving local dev working. Non-destructive. Backends >= 2026-07-15 keep a separate live-install slot per kind group, so the two coexist."
24886
25662
  ).option(
24887
25663
  "--replace-dev-install",
24888
25664
  "Destroy the workspace's kind=local dev install first, then fresh production install. Revokes the cached local-dev credentials; on backends >= 2026-07-15 local dev can be re-provisioned afterwards (dashwise module reset-dev-install, or dashwise init <slug> --workspace <ws>) and will coexist with the production install. On OLDER backends this is one-way \u2014 prefer --keep-dev-install."
24889
- ).option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("-y, --yes", "Skip the confirmation prompt (for CI / scripted use).").option("--json", "Emit the final result as machine-readable JSON.").action(
25665
+ ).option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("-y, --yes", "Skip the confirmation prompt (for CI / scripted use). Never accepts data loss: see --accept-data-loss.").option(
25666
+ "--accept-data-loss <drift_hash>",
25667
+ "Accept the data loss of this upgrade: the drift_hash its preview printed. Only that exact hash accepts it (a person decides; an agent stops and reports the hash)."
25668
+ ).option("--json", "Emit the final result as machine-readable JSON.").action(
24890
25669
  async (slug, cmdOpts) => {
24891
25670
  process.exitCode = await moduleInstallCommand(slug, {
24892
25671
  ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
@@ -24896,6 +25675,7 @@ moduleCmd.command("install [slug]").alias("deploy").description(
24896
25675
  ...cmdOpts.replaceDevInstall !== void 0 ? { replaceDevInstall: cmdOpts.replaceDevInstall } : {},
24897
25676
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
24898
25677
  ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
25678
+ ...cmdOpts.acceptDataLoss !== void 0 ? { acceptDataLoss: cmdOpts.acceptDataLoss } : {},
24899
25679
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
24900
25680
  });
24901
25681
  }
@@ -25192,7 +25972,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
25192
25972
  }
25193
25973
  );
25194
25974
  function getVersion() {
25195
- return "0.25.0";
25975
+ return "0.26.0";
25196
25976
  }
25197
25977
  function parseIntOption(value) {
25198
25978
  const n = Number.parseInt(value, 10);