@dashai/cli 0.25.0 → 0.27.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
@@ -4,7 +4,7 @@ import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, watch, read
4
4
  import { hostname, tmpdir, platform, homedir } from 'os';
5
5
  import { resolve, join, relative, dirname, basename, win32, sep } from 'path';
6
6
  import { execFileSync, spawn, spawnSync } from 'child_process';
7
- import { intro, log, select, isCancel, cancel, spinner, outro, text, confirm, multiselect } from '@clack/prompts';
7
+ import { intro, log, select, isCancel, cancel, spinner, outro, text, confirm as confirm$1, multiselect } from '@clack/prompts';
8
8
  import { Command, Option } from 'commander';
9
9
  import open from 'open';
10
10
  import AdmZip from 'adm-zip';
@@ -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
  }
@@ -1119,16 +1187,16 @@ async function devCommand(options) {
1119
1187
  }
1120
1188
  const callbackOrigins = computeCallbackOrigins(port, options.callbackOrigin ?? []);
1121
1189
  try {
1122
- const session = await registerDevSession(apiUrl, apiToken, {
1190
+ const session2 = await registerDevSession(apiUrl, apiToken, {
1123
1191
  target_workspace_id: workspaceContext.id,
1124
1192
  callback_origins: callbackOrigins
1125
1193
  });
1126
- revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session.id);
1127
- devHandoffEnv = devSessionHandoffEnv(session);
1128
- info(` ${dim("dev session")} ${session.id}`);
1194
+ revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session2.id);
1195
+ devHandoffEnv = devSessionHandoffEnv(session2);
1196
+ info(` ${dim("dev session")} ${session2.id}`);
1129
1197
  info(` ${dim("workspace")} ${workspaceContext.name} (id=${workspaceContext.id}, slug=${workspaceContext.slug})`);
1130
1198
  info(` ${dim("callbacks")} ${callbackOrigins.join(", ")}`);
1131
- info(` ${dim("expires")} ${session.expires_at}`);
1199
+ info(` ${dim("expires")} ${session2.expires_at}`);
1132
1200
  info(
1133
1201
  ` ${dim("sign-in")} ${Object.keys(devHandoffEnv).length > 0 ? "zero-click (server-side dev handoff armed)" : "browser SSO (backend issued no handoff secret \u2014 pre-2026-07-15 backend)"}`
1134
1202
  );
@@ -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({
@@ -1381,11 +1455,11 @@ async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
1381
1455
  success(`Upgraded ${code(slug)} to a dwk_ API key (id=${minted.api_key.id}).`);
1382
1456
  return upgraded;
1383
1457
  }
1384
- function devSessionHandoffEnv(session) {
1385
- const secret = session.handoff_secret;
1458
+ function devSessionHandoffEnv(session2) {
1459
+ const secret = session2.handoff_secret;
1386
1460
  if (typeof secret !== "string" || secret.length === 0) return {};
1387
1461
  return {
1388
- DASHWISE_DEV_SESSION_ID: session.id,
1462
+ DASHWISE_DEV_SESSION_ID: session2.id,
1389
1463
  DASHWISE_DEV_HANDOFF_SECRET: secret
1390
1464
  };
1391
1465
  }
@@ -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
 
@@ -12773,9 +12853,7 @@ or in their own \`CLAUDE.md\` / \`AGENTS.md\`), follow them instead.
12773
12853
  person in plain words that the changes are saved and that Release in
12774
12854
  Dashwise includes them. Don't ask them to merge anything.
12775
12855
  - Never force-push and never rewrite the default branch's history.
12776
- - The person only confirms Release and Take live in Dashwise. When the work is
12777
- pushed, tell them it is ready: in Dashwise open the module, click Release,
12778
- then Take live.
12856
+ - When the work is pushed, tell the person it is ready to release.
12779
12857
 
12780
12858
  ## Cloud workflow
12781
12859
 
@@ -12786,8 +12864,8 @@ or in their own \`CLAUDE.md\` / \`AGENTS.md\`), follow them instead.
12786
12864
  - \`npm run smoke:production\` starts the built app on a local loopback port. If a cloud sandbox refuses that port, rerun the command with the network permission; never skip the check.
12787
12865
  - Report what you changed, the pushed SHA and the checks you ran, including failures, in plain words.
12788
12866
  - Never print, persist or commit secrets. Repository code delivery needs no Dashwise token. Do not log in or access company data for setup or checks; see "Dashwise login" for full development.
12789
- - Never automatically release, publish, deploy or take an installation live. Release and Live require an explicit human action.
12790
- - Even when the person says you may release, you can't do it for them: tell them in one sentence where to click (in Dashwise open the module, click Release, then Take live). Never ask for a \`dashwise login\` or a token just to release.
12867
+ - Before releasing, installing/updating an installation or taking live, propose the exact action in the chat and wait for the person to accept or change it; never do it on your own.
12868
+ - When the person accepts, do exactly that action if your Dashwise connection allows it; if Dashwise refuses, tell them what it said (for example a permission to turn on in My Account). If you have no way to do it, tell them in one sentence where to click (in Dashwise open the module, click Release, then Take live). Never ask for a \`dashwise login\` or a token just to release.
12791
12869
 
12792
12870
  ## If you can't see the repository
12793
12871
 
@@ -13468,6 +13546,9 @@ async function moduleInitCommand(slugArg, options) {
13468
13546
  writeFileSync(absPath, content, "utf-8");
13469
13547
  written++;
13470
13548
  }
13549
+ reportSecretLines(
13550
+ syncEnvLocalSecretLines(dir, declaredSecretNames(readManifest(dir)))
13551
+ );
13471
13552
  }
13472
13553
  if (writeMode !== "none" && !cloudReady) {
13473
13554
  process.stdout.write(
@@ -13710,10 +13791,297 @@ var init_init = __esm({
13710
13791
  init_output();
13711
13792
  init_api_client();
13712
13793
  init_config();
13794
+ init_declared_secrets();
13713
13795
  init_manifest_io();
13714
13796
  init_workspaces();
13715
13797
  }
13716
13798
  });
13799
+
13800
+ // src/lib/manifest-evolution.ts
13801
+ function compareManifests(base, head, history = []) {
13802
+ const violations = [];
13803
+ const additions = [];
13804
+ const warnings = [];
13805
+ const v = (rule, where, detail) => violations.push({ rule, where, detail });
13806
+ const baseTables = tablesOf(base);
13807
+ const headTables = tablesOf(head);
13808
+ const everTables = /* @__PURE__ */ new Set();
13809
+ const everFields = /* @__PURE__ */ new Set();
13810
+ for (const m of history) {
13811
+ for (const [slug, t] of tablesOf(m)) {
13812
+ everTables.add(slug);
13813
+ for (const f of t.fields ?? []) everFields.add(`${slug}.${f.slug}`);
13814
+ }
13815
+ }
13816
+ for (const [slug, bt] of baseTables) {
13817
+ const ht = headTables.get(slug);
13818
+ if (!ht) {
13819
+ v("removed_table", slug, "tables are never removed; deprecate instead");
13820
+ continue;
13821
+ }
13822
+ const bf = fieldsOf(bt);
13823
+ const hf = fieldsOf(ht);
13824
+ for (const [fs, f] of bf) {
13825
+ const g = hf.get(fs);
13826
+ const where = `${slug}.${fs}`;
13827
+ if (!g) {
13828
+ v("removed_field", where, "fields are never removed; stop writing it and set human_edit: open");
13829
+ continue;
13830
+ }
13831
+ if (f.type !== g.type) v("retyped_field", where, `${f.type} \u2192 ${g.type}`);
13832
+ if ((f.decimal_places ?? null) !== (g.decimal_places ?? null)) {
13833
+ v("retyped_field", where, `decimal_places ${f.decimal_places ?? "none"} \u2192 ${g.decimal_places ?? "none"}`);
13834
+ }
13835
+ if ((f.target ?? null) !== (g.target ?? null)) v("retyped_field", where, `link target ${f.target} \u2192 ${g.target}`);
13836
+ if (f.required !== true && g.required === true) {
13837
+ v("required_tightened", where, "old rows are empty; enforce it in the tool layer instead");
13838
+ }
13839
+ const bo = f.options ?? [];
13840
+ const ho = g.options ?? [];
13841
+ for (const [i, o] of bo.entries()) {
13842
+ if (!ho.includes(o)) v("removed_option", where, `option "${String(o)}" removed`);
13843
+ else if (ho[i] !== o) {
13844
+ v("reordered_option", where, `option "${String(o)}" moved from ${i} to ${ho.indexOf(o)}; options are append-only`);
13845
+ }
13846
+ }
13847
+ for (const o of ho.slice(bo.length)) if (!bo.includes(o)) additions.push(`option ${where} "${String(o)}"`);
13848
+ }
13849
+ for (const [fs] of hf) {
13850
+ if (bf.has(fs)) continue;
13851
+ if (everFields.has(`${slug}.${fs}`)) {
13852
+ v("slug_reused", `${slug}.${fs}`, "this slug was removed in an earlier version and may not come back");
13853
+ } else additions.push(`field ${slug}.${fs}`);
13854
+ }
13855
+ const hu = new Set(uniqueIndexes(ht));
13856
+ for (const idx of uniqueIndexes(bt)) {
13857
+ if (!hu.has(idx)) v("dropped_unique_index", `${slug}(${idx})`, "a unique index is a protocol lock; it may not be dropped or changed");
13858
+ }
13859
+ const bu = new Set(uniqueIndexes(bt));
13860
+ for (const idx of hu) {
13861
+ if (bu.has(idx)) continue;
13862
+ const overExisting = idx.split(",").some((f) => f.length > 0 && bf.has(f));
13863
+ if (overExisting) {
13864
+ 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");
13865
+ } else additions.push(`unique index ${slug}(${idx})`);
13866
+ }
13867
+ }
13868
+ for (const [slug] of headTables) {
13869
+ if (baseTables.has(slug)) continue;
13870
+ if (everTables.has(slug)) v("slug_reused", slug, "this table slug was removed in an earlier version and may not come back");
13871
+ else additions.push(`table ${slug}`);
13872
+ }
13873
+ const baseViews = new Set((base?.owns?.views ?? []).map((x) => x.slug));
13874
+ const headViews = new Set((head?.owns?.views ?? []).map((x) => x.slug));
13875
+ for (const s of baseViews) if (!headViews.has(s)) warnings.push(`view ${s} removed (callers of qbViews.${s} break)`);
13876
+ for (const s of headViews) if (!baseViews.has(s)) additions.push(`view ${s}`);
13877
+ return { violations, additions, warnings };
13878
+ }
13879
+ var tablesOf, fieldsOf, uniqueIndexes;
13880
+ var init_manifest_evolution = __esm({
13881
+ "src/lib/manifest-evolution.ts"() {
13882
+ tablesOf = (m) => new Map((m?.owns?.tables ?? []).map((t) => [t.slug, t]));
13883
+ fieldsOf = (t) => new Map((t?.fields ?? []).map((f) => [f.slug, f]));
13884
+ uniqueIndexes = (t) => (t?.indexes ?? []).filter((i) => i.unique === true).map((i) => (i.fields ?? []).join(","));
13885
+ }
13886
+ });
13887
+
13888
+ // src/commands/module/diff-check.ts
13889
+ var diff_check_exports = {};
13890
+ __export(diff_check_exports, {
13891
+ EvolutionCheckError: () => EvolutionCheckError,
13892
+ moduleDiffCheckCommand: () => moduleDiffCheckCommand,
13893
+ printEvolutionReport: () => printEvolutionReport,
13894
+ publishEvolutionGate: () => publishEvolutionGate,
13895
+ runEvolutionCheck: () => runEvolutionCheck
13896
+ });
13897
+ function readJson(path) {
13898
+ try {
13899
+ return JSON.parse(readFileSync(path, "utf8"));
13900
+ } catch (err) {
13901
+ throw new EvolutionCheckError(`could not read ${path}: ${err.message}`);
13902
+ }
13903
+ }
13904
+ function git(cwd, args) {
13905
+ try {
13906
+ return execFileSync("git", ["-C", cwd, ...args], {
13907
+ encoding: "utf8",
13908
+ stdio: ["ignore", "pipe", "pipe"],
13909
+ maxBuffer: 64 * 1024 * 1024
13910
+ });
13911
+ } catch (err) {
13912
+ const stderr = err.stderr?.trim();
13913
+ throw new EvolutionCheckError(`git ${args.join(" ")} failed${stderr ? `: ${stderr}` : ""}`);
13914
+ }
13915
+ }
13916
+ async function runEvolutionCheck(options) {
13917
+ const projectRoot = resolve(options.dir ?? process.cwd());
13918
+ const headPath = options.head ? resolve(options.head) : join(projectRoot, "module.json");
13919
+ if (!existsSync(headPath)) throw new EvolutionCheckError(`no manifest at ${headPath}`);
13920
+ const head = readJson(headPath);
13921
+ const baseFile = options.baseFile ?? (options.base && existsSync(options.base) ? options.base : void 0);
13922
+ if (baseFile) {
13923
+ const base = readJson(baseFile);
13924
+ return { ...compareManifests(base, head), base: baseFile, head: headPath, history: 0, nothingReleased: false };
13925
+ }
13926
+ if (options.base) {
13927
+ const ref = options.base;
13928
+ const base = JSON.parse(git(projectRoot, ["show", `${ref}:./module.json`]));
13929
+ let history2 = [];
13930
+ if (!options.noHistory) {
13931
+ const shas = git(projectRoot, ["log", "--format=%H", "--reverse", ref, "--", "module.json"]).split("\n").filter(Boolean);
13932
+ history2 = shas.slice(0, -1).map((sha) => JSON.parse(git(projectRoot, ["show", `${sha}:./module.json`])));
13933
+ }
13934
+ return {
13935
+ ...compareManifests(base, head, history2),
13936
+ base: `${ref}:module.json`,
13937
+ head: headPath,
13938
+ history: history2.length,
13939
+ nothingReleased: false
13940
+ };
13941
+ }
13942
+ const slug = head.module?.slug;
13943
+ if (!slug) throw new EvolutionCheckError(`${headPath} has no module.slug`);
13944
+ let mod;
13945
+ let versions;
13946
+ try {
13947
+ mod = await apiRequest(
13948
+ "GET",
13949
+ `/api/modules/by-slug/${encodeURIComponent(slug)}`
13950
+ );
13951
+ if (!mod.current_version) {
13952
+ return {
13953
+ violations: [],
13954
+ additions: [],
13955
+ warnings: [],
13956
+ base: "(nothing released)",
13957
+ head: headPath,
13958
+ history: 0,
13959
+ nothingReleased: true
13960
+ };
13961
+ }
13962
+ versions = await apiRequest("GET", `/api/modules/${mod.id}/versions`);
13963
+ } catch (err) {
13964
+ const why = err instanceof ApiError ? `${err.code || err.status} ${err.message}` : err.message;
13965
+ throw new EvolutionCheckError(
13966
+ `could not read the released manifest of ${slug} (${why}); pass --base <ref|file> to check offline`
13967
+ );
13968
+ }
13969
+ const released = versions.find((v) => v.version === mod.current_version);
13970
+ if (!released?.manifest_jsonb) {
13971
+ throw new EvolutionCheckError(
13972
+ `the platform returned no manifest for ${slug} v${mod.current_version}; pass --base <ref|file> to check offline`
13973
+ );
13974
+ }
13975
+ const releasedAt = released.created_on ? Date.parse(released.created_on) : Number.POSITIVE_INFINITY;
13976
+ 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);
13977
+ return {
13978
+ ...compareManifests(released.manifest_jsonb, head, history),
13979
+ base: `${slug} v${mod.current_version} (released)`,
13980
+ head: headPath,
13981
+ history: history.length,
13982
+ nothingReleased: false
13983
+ };
13984
+ }
13985
+ function printEvolutionReport(r, print = out) {
13986
+ print(`module diff --check: ${r.head} against ${r.base} (${r.history} older version${r.history === 1 ? "" : "s"} scanned)`);
13987
+ for (const a of r.additions) print(` + ${a}`);
13988
+ for (const w of r.warnings) print(` ! ${w}`);
13989
+ for (const x of r.violations) print(` \u2717 ${x.rule}: ${x.where}: ${x.detail}`);
13990
+ }
13991
+ async function moduleDiffCheckCommand(options) {
13992
+ let r;
13993
+ try {
13994
+ r = await runEvolutionCheck(options);
13995
+ } catch (err) {
13996
+ if (err instanceof EvolutionCheckError) {
13997
+ if (options.json) out(JSON.stringify({ ok: false, error: { code: "CHECK_FAILED", message: err.message } }));
13998
+ else fail(`module diff --check: ${err.message}`);
13999
+ return 2;
14000
+ }
14001
+ throw err;
14002
+ }
14003
+ if (options.json) {
14004
+ out(JSON.stringify({ ok: r.violations.length === 0, ...r }, null, 2));
14005
+ return r.violations.length === 0 ? 0 : 1;
14006
+ }
14007
+ if (r.nothingReleased) {
14008
+ success("Nothing released yet: every change is additive.");
14009
+ return 0;
14010
+ }
14011
+ printEvolutionReport(r);
14012
+ if (r.violations.length === 0) {
14013
+ success("additive");
14014
+ return 0;
14015
+ }
14016
+ fail(`${r.violations.length} violation(s): not additive. Released copies would lose data or fail to upgrade.`);
14017
+ info(`Add a new field or table instead, and stop writing the old one (${code('human_edit: "open"')}).`);
14018
+ return 1;
14019
+ }
14020
+ async function publishEvolutionGate(projectRoot, strict) {
14021
+ let r;
14022
+ try {
14023
+ r = await runEvolutionCheck({ dir: projectRoot });
14024
+ } catch (err) {
14025
+ const msg = err instanceof Error ? err.message : String(err);
14026
+ if (strict) {
14027
+ fail(`Evolution check could not run (${msg}). Refusing with --strict. Nothing was published.`);
14028
+ return 1;
14029
+ }
14030
+ warn(`Evolution check skipped: ${msg}.`);
14031
+ return 0;
14032
+ }
14033
+ if (r.nothingReleased || r.violations.length === 0) return 0;
14034
+ printEvolutionReport(r, (s) => (strict ? fail : warn)(s));
14035
+ if (strict) {
14036
+ fail(
14037
+ `module.json is not an additive evolution of the released version (${r.violations.length} violation(s)). Refusing with --strict. Nothing was published.`
14038
+ );
14039
+ return 1;
14040
+ }
14041
+ warn(
14042
+ `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.`
14043
+ );
14044
+ return 0;
14045
+ }
14046
+ var EvolutionCheckError;
14047
+ var init_diff_check = __esm({
14048
+ "src/commands/module/diff-check.ts"() {
14049
+ init_api_client();
14050
+ init_manifest_evolution();
14051
+ init_output();
14052
+ EvolutionCheckError = class extends Error {
14053
+ };
14054
+ }
14055
+ });
14056
+
14057
+ // src/lib/manifest-reference-lint.ts
14058
+ function referenceFieldWarnings(manifest) {
14059
+ const tables = manifest?.owns?.tables;
14060
+ if (!Array.isArray(tables)) return [];
14061
+ const warnings = [];
14062
+ for (const table of tables) {
14063
+ if (typeof table?.slug !== "string" || !Array.isArray(table.fields)) continue;
14064
+ for (const field of table.fields) {
14065
+ if (typeof field?.slug !== "string" || typeof field.type !== "string") continue;
14066
+ if (field.slug === "id" || !REFERENCE_NAME.test(field.slug)) continue;
14067
+ if (!PLAIN_TYPES.has(field.type)) continue;
14068
+ warnings.push({
14069
+ table: table.slug,
14070
+ field: field.slug,
14071
+ type: field.type,
14072
+ 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.`
14073
+ });
14074
+ }
14075
+ }
14076
+ return warnings;
14077
+ }
14078
+ var REFERENCE_NAME, PLAIN_TYPES;
14079
+ var init_manifest_reference_lint = __esm({
14080
+ "src/lib/manifest-reference-lint.ts"() {
14081
+ REFERENCE_NAME = /(?:_id|_ref|Id|Ref)$/;
14082
+ PLAIN_TYPES = /* @__PURE__ */ new Set(["number", "text", "autonumber"]);
14083
+ }
14084
+ });
13717
14085
  function loadSchemaCommandContext(opts) {
13718
14086
  const projectRoot = resolve(opts.dir ?? process.cwd());
13719
14087
  const manifestPath2 = join(projectRoot, "module.json");
@@ -13786,6 +14154,89 @@ var init_dotenv_local = __esm({
13786
14154
  }
13787
14155
  });
13788
14156
 
14157
+ // src/lib/install-target.ts
14158
+ function keyDisplayPrefix(apiKey) {
14159
+ if (!apiKey.startsWith(KEY_SCHEME)) return null;
14160
+ const prefix = apiKey.slice(KEY_SCHEME.length, KEY_SCHEME.length + KEY_PREFIX_CHARS);
14161
+ return prefix.length === KEY_PREFIX_CHARS ? prefix : null;
14162
+ }
14163
+ async function keyBelongsToInstall(apiKey, installationId, options, now = /* @__PURE__ */ new Date()) {
14164
+ const prefix = keyDisplayPrefix(apiKey);
14165
+ if (prefix === null) return false;
14166
+ const { api_keys } = await listApiKeys(installationId, options);
14167
+ return api_keys.some(
14168
+ (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)
14169
+ );
14170
+ }
14171
+ function formatInstallTarget(id, detail) {
14172
+ return detail ? `\u2192 install #${id} (${detail})` : `\u2192 install #${id}`;
14173
+ }
14174
+ async function nameKeyTarget(args) {
14175
+ const { apiKey, keySource, entry, cfg, apiUrl, install } = args;
14176
+ const request = cfg ? { baseUrl: apiUrl, auth: { token: cfg.token }, signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) } : null;
14177
+ if (install === void 0) {
14178
+ if (keySource !== "cached" || !entry) {
14179
+ info(
14180
+ `\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.`
14181
+ );
14182
+ return 0;
14183
+ }
14184
+ if (!request || entry.workspaceId === void 0) {
14185
+ info(formatInstallTarget(entry.installationId, "the cached dev install"));
14186
+ return 0;
14187
+ }
14188
+ try {
14189
+ const workspaces = await apiRequest("GET", "/api/workspaces", void 0, request);
14190
+ const ws = workspaces.find((w) => w.id === entry.workspaceId);
14191
+ info(formatInstallTarget(entry.installationId, `${ws ? ws.slug : `workspace #${entry.workspaceId}`}, local`));
14192
+ } catch {
14193
+ info(formatInstallTarget(entry.installationId));
14194
+ }
14195
+ return 0;
14196
+ }
14197
+ const source = keySource === "cached" ? "the cached key" : `${code("DASHWISE_API_KEY")} (from ${keySource === "environment" ? "the environment" : code(".env.local")})`;
14198
+ if (!request) {
14199
+ fail(
14200
+ `${code("--install")} looks the install up with your CLI login, and there is none. Run ${code("dashwise login")} first. Nothing was run.`
14201
+ );
14202
+ return 1;
14203
+ }
14204
+ let belongs;
14205
+ try {
14206
+ belongs = await keyBelongsToInstall(apiKey, install, request);
14207
+ } catch (err) {
14208
+ fail(
14209
+ `Could not read the keys of install #${install} to confirm ${source} (${describe(err)}). Refusing to run against an unconfirmed install. Nothing was run.`
14210
+ );
14211
+ return 1;
14212
+ }
14213
+ const cachedDev = entry?.installationId === install ? "the cached dev install, " : "";
14214
+ if (!belongs) {
14215
+ const other = entry && entry.installationId !== install && entry.apiKey === apiKey ? ` It is the cached key of install #${entry.installationId}.` : "";
14216
+ fail(`${source} is not a live key of install #${install}.${other} Nothing was run.`);
14217
+ info(
14218
+ `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.`
14219
+ );
14220
+ return 1;
14221
+ }
14222
+ info(formatInstallTarget(install, `${cachedDev}key checked`));
14223
+ return 0;
14224
+ }
14225
+ function describe(err) {
14226
+ if (err instanceof ApiError) return `${err.code || err.status} ${err.message}`;
14227
+ return err.message;
14228
+ }
14229
+ var KEY_PREFIX_CHARS, KEY_SCHEME, LOOKUP_TIMEOUT_MS;
14230
+ var init_install_target = __esm({
14231
+ "src/lib/install-target.ts"() {
14232
+ init_api_client();
14233
+ init_output();
14234
+ KEY_PREFIX_CHARS = 8;
14235
+ KEY_SCHEME = "dwk_";
14236
+ LOOKUP_TIMEOUT_MS = 1500;
14237
+ }
14238
+ });
14239
+
13789
14240
  // src/commands/run.ts
13790
14241
  var run_exports = {};
13791
14242
  __export(run_exports, {
@@ -13798,6 +14249,10 @@ async function runCommand(cmdArgs, options) {
13798
14249
  );
13799
14250
  return 1;
13800
14251
  }
14252
+ if (options.install !== void 0 && (!Number.isInteger(options.install) || options.install <= 0)) {
14253
+ fail(`--install expects a positive integer installation id (got "${options.install}").`);
14254
+ return 1;
14255
+ }
13801
14256
  const projectRoot = resolve(options.dir ?? process.cwd());
13802
14257
  let manifestSlug;
13803
14258
  try {
@@ -13810,6 +14265,7 @@ async function runCommand(cmdArgs, options) {
13810
14265
  fail(`module.json has no ${code("module.slug")} \u2014 the manifest is malformed.`);
13811
14266
  return 1;
13812
14267
  }
14268
+ const keyInShell = process.env.DASHWISE_API_KEY !== void 0;
13813
14269
  hydrateEnvFromDotenvLocal(projectRoot);
13814
14270
  const cfg = (() => {
13815
14271
  try {
@@ -13820,8 +14276,19 @@ async function runCommand(cmdArgs, options) {
13820
14276
  })();
13821
14277
  const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
13822
14278
  let apiKey = process.env.DASHWISE_API_KEY;
14279
+ let keySource = keyInShell ? "environment" : ".env.local";
14280
+ let entry = null;
13823
14281
  if (!apiKey) {
13824
- let entry = readModuleEntry(manifestSlug);
14282
+ entry = readModuleEntry(manifestSlug);
14283
+ if (entry && options.install !== void 0 && entry.installationId !== options.install) {
14284
+ fail(
14285
+ `No key for install #${options.install}: the cached ${code("dwk_")} key of ${code(manifestSlug)} belongs to install #${entry.installationId}. Nothing was run.`
14286
+ );
14287
+ info(
14288
+ `Set ${code("DASHWISE_API_KEY")} to a key of install #${options.install} (mint one with ${code(`dashwise api-keys create <name> --install ${options.install}`)}).`
14289
+ );
14290
+ return 1;
14291
+ }
13825
14292
  if (cfg) {
13826
14293
  const healed = await resolveModuleApiKey(
13827
14294
  manifestSlug,
@@ -13832,6 +14299,7 @@ async function runCommand(cmdArgs, options) {
13832
14299
  if (healed) entry = healed;
13833
14300
  }
13834
14301
  apiKey = entry?.apiKey;
14302
+ keySource = "cached";
13835
14303
  }
13836
14304
  if (!apiKey) {
13837
14305
  fail(
@@ -13842,6 +14310,15 @@ async function runCommand(cmdArgs, options) {
13842
14310
  );
13843
14311
  return 1;
13844
14312
  }
14313
+ const target = await nameKeyTarget({
14314
+ apiKey,
14315
+ keySource,
14316
+ entry: entry ?? readModuleEntry(manifestSlug),
14317
+ cfg,
14318
+ apiUrl,
14319
+ ...options.install !== void 0 ? { install: options.install } : {}
14320
+ });
14321
+ if (target !== 0) return target;
13845
14322
  const [cmd, ...rest] = cmdArgs;
13846
14323
  info(
13847
14324
  `${dim("run")} ${cmd} ${rest.join(" ")} ${dim(`(DASHWISE_API_URL=${apiUrl}, DASHWISE_API_KEY injected)`)}`
@@ -13871,6 +14348,7 @@ var init_run = __esm({
13871
14348
  "src/commands/run.ts"() {
13872
14349
  init_config();
13873
14350
  init_dotenv_local();
14351
+ init_install_target();
13874
14352
  init_manifest_io();
13875
14353
  init_output();
13876
14354
  init_dev();
@@ -13914,7 +14392,7 @@ async function destroyDevInstallCommand(options) {
13914
14392
  return 0;
13915
14393
  }
13916
14394
  if (!options.yes && process.stdout.isTTY) {
13917
- const choice = await confirm({
14395
+ const choice = await confirm$1({
13918
14396
  message: `Destroy dev install ${code(`${slug} (id=${entry.installationId})`)}? Tables and rows will be soft-deleted (recoverable for 30 days via dashboard trash).`
13919
14397
  });
13920
14398
  if (isCancel(choice) || !choice) {
@@ -14161,6 +14639,7 @@ async function schemaPushCommand(options) {
14161
14639
  return surfaceSchemaPushError(err, slug);
14162
14640
  }
14163
14641
  printApplySummary(applied);
14642
+ for (const w of referenceFieldWarnings(manifest)) warn(w.message);
14164
14643
  if (dwkAuth) {
14165
14644
  try {
14166
14645
  const { manifest: canonical } = await getInstallationSchema(dwkAuth);
@@ -14309,6 +14788,7 @@ var init_push = __esm({
14309
14788
  init_manifest_io();
14310
14789
  init_manifest_merge();
14311
14790
  init_output();
14791
+ init_manifest_reference_lint();
14312
14792
  }
14313
14793
  });
14314
14794
 
@@ -15853,7 +16333,7 @@ function startMcpProxy(opts) {
15853
16333
  const reconnectMs = opts.reconnectMs ?? 2e3;
15854
16334
  const verbose = opts.verbose === true;
15855
16335
  const readOnly = opts.readOnly === true;
15856
- let session = opts.session;
16336
+ let session2 = opts.session;
15857
16337
  let sessionId = null;
15858
16338
  let protocolVersion = null;
15859
16339
  let readTools = null;
@@ -15866,8 +16346,8 @@ function startMcpProxy(opts) {
15866
16346
  let streamUnavailable = false;
15867
16347
  let streamStarted = false;
15868
16348
  let resolveDone;
15869
- const done = new Promise((resolve15) => {
15870
- resolveDone = resolve15;
16349
+ const done = new Promise((resolve16) => {
16350
+ resolveDone = resolve16;
15871
16351
  });
15872
16352
  const trace = (line) => {
15873
16353
  if (verbose) log2(line);
@@ -15882,7 +16362,7 @@ function startMcpProxy(opts) {
15882
16362
  parsed = JSON.parse(text4);
15883
16363
  } catch {
15884
16364
  log2(
15885
- `Discarded a non-JSON response from ${redactUrl(session.connectUrl)} (${source}); stdout carries MCP frames only.`
16365
+ `Discarded a non-JSON response from ${redactUrl(session2.connectUrl)} (${source}); stdout carries MCP frames only.`
15886
16366
  );
15887
16367
  trace(`discarded ${Buffer.byteLength(text4, "utf8")} bytes of non-JSON`);
15888
16368
  return;
@@ -15892,7 +16372,7 @@ function startMcpProxy(opts) {
15892
16372
  ) : parsed !== null && typeof parsed === "object" && "jsonrpc" in parsed;
15893
16373
  if (!looksJsonRpc) {
15894
16374
  log2(
15895
- `Discarded a JSON response from ${redactUrl(session.connectUrl)} that is not JSON-RPC (${source}).`
16375
+ `Discarded a JSON response from ${redactUrl(session2.connectUrl)} that is not JSON-RPC (${source}).`
15896
16376
  );
15897
16377
  return;
15898
16378
  }
@@ -15923,7 +16403,7 @@ function startMcpProxy(opts) {
15923
16403
  reauthAttempt = (async () => {
15924
16404
  log2("The session key expired or was rotated \u2014 re-authenticating once.");
15925
16405
  try {
15926
- session = await reauth();
16406
+ session2 = await reauth();
15927
16407
  sessionId = null;
15928
16408
  return true;
15929
16409
  } catch (err) {
@@ -15964,7 +16444,7 @@ function startMcpProxy(opts) {
15964
16444
  };
15965
16445
  const post = async (text4, isInitialize) => {
15966
16446
  if (stopped) return null;
15967
- return doFetch(session.connectUrl, {
16447
+ return doFetch(session2.connectUrl, {
15968
16448
  method: "POST",
15969
16449
  headers: requestHeaders(isInitialize),
15970
16450
  body: text4
@@ -15992,7 +16472,7 @@ function startMcpProxy(opts) {
15992
16472
  res = await post(text4, isInitialize);
15993
16473
  } catch (err) {
15994
16474
  const message = err.message;
15995
- log2(`Transport error talking to ${redactUrl(session.connectUrl)}: ${message}`);
16475
+ log2(`Transport error talking to ${redactUrl(session2.connectUrl)}: ${message}`);
15996
16476
  emit(transportErrorFrame(frame?.id ?? null, `Transport error: ${message}`));
15997
16477
  return { kind: "failed" };
15998
16478
  }
@@ -16016,7 +16496,7 @@ function startMcpProxy(opts) {
16016
16496
  const bodyText = await res.text();
16017
16497
  if (!res.ok) {
16018
16498
  const detail = res.status === 401 ? "the endpoint key is no longer valid (expired, rotated by another `dashwise mcp`, or revoked)" : `HTTP ${res.status}`;
16019
- log2(`Refused by ${redactUrl(session.connectUrl)}: ${detail}`);
16499
+ log2(`Refused by ${redactUrl(session2.connectUrl)}: ${detail}`);
16020
16500
  emit(transportErrorFrame(frame?.id ?? null, `Dashwise MCP server: ${detail}`));
16021
16501
  if (res.status === 401) {
16022
16502
  exitCode = 1;
@@ -16103,7 +16583,7 @@ function startMcpProxy(opts) {
16103
16583
  streamAbort = new AbortController();
16104
16584
  let res;
16105
16585
  try {
16106
- res = await doFetch(session.connectUrl, {
16586
+ res = await doFetch(session2.connectUrl, {
16107
16587
  method: "GET",
16108
16588
  headers: {
16109
16589
  Accept: "text/event-stream",
@@ -16139,7 +16619,7 @@ function startMcpProxy(opts) {
16139
16619
  }
16140
16620
  streamUnavailable = true;
16141
16621
  log2(
16142
- `Server stream refused by ${redactUrl(session.connectUrl)}: the session could not be renewed (the endpoint key expired, was rotated, or was revoked).`
16622
+ `Server stream refused by ${redactUrl(session2.connectUrl)}: the session could not be renewed (the endpoint key expired, was rotated, or was revoked).`
16143
16623
  );
16144
16624
  emit(
16145
16625
  transportErrorFrame(
@@ -16353,7 +16833,7 @@ async function revokeSession(endpointId) {
16353
16833
  () => false
16354
16834
  );
16355
16835
  const timeout = new Promise(
16356
- (resolve15) => setTimeout(() => resolve15(false), REVOKE_TIMEOUT_MS).unref()
16836
+ (resolve16) => setTimeout(() => resolve16(false), REVOKE_TIMEOUT_MS).unref()
16357
16837
  );
16358
16838
  await Promise.race([attempt, timeout]);
16359
16839
  }
@@ -16416,9 +16896,9 @@ async function mcpCommand(options = {}) {
16416
16896
  return 1;
16417
16897
  }
16418
16898
  const label = options.endpointName ?? defaultEndpointLabel();
16419
- let session;
16899
+ let session2;
16420
16900
  try {
16421
- session = await openCliSession({
16901
+ session2 = await openCliSession({
16422
16902
  workspaceId: workspace2.id,
16423
16903
  label,
16424
16904
  ...scopes !== void 0 ? { scopes } : {}
@@ -16427,24 +16907,24 @@ async function mcpCommand(options = {}) {
16427
16907
  fail(describeSessionError(err, apiUrl, scopes));
16428
16908
  return 1;
16429
16909
  }
16430
- if (scopes !== void 0 && !Array.isArray(session.scopes)) {
16431
- await revokeSession(session.endpointId);
16910
+ if (scopes !== void 0 && !Array.isArray(session2.scopes)) {
16911
+ await revokeSession(session2.endpointId);
16432
16912
  fail(SCOPES_NOT_APPLIED);
16433
16913
  return 1;
16434
16914
  }
16435
16915
  info(
16436
- `Dashwise MCP \u2014 workspace ${workspace2.slug}, endpoint #${session.endpointId} (${session.name}).`
16916
+ `Dashwise MCP \u2014 workspace ${workspace2.slug}, endpoint #${session2.endpointId} (${session2.name}).`
16437
16917
  );
16438
- info(`Session key expires ${dim(session.expiresAt)}.`);
16439
- info(describeScopes(session.scopes));
16918
+ info(`Session key expires ${dim(session2.expiresAt)}.`);
16919
+ info(describeScopes(session2.scopes));
16440
16920
  if (options.readOnly) {
16441
16921
  warn(
16442
16922
  "Read-only: write tools are refused by this proxy (a client-side guard, not a server-enforced scope \u2014 for that, use --scope)."
16443
16923
  );
16444
16924
  }
16445
- let ownedEndpointId = session.endpointId;
16925
+ let ownedEndpointId = session2.endpointId;
16446
16926
  const proxy = startMcpProxy({
16447
- session,
16927
+ session: session2,
16448
16928
  reauth: async () => {
16449
16929
  const fresh = await openCliSession({
16450
16930
  workspaceId: workspace2.id,
@@ -16531,7 +17011,7 @@ async function mcpAddCommand(options = {}) {
16531
17011
  fail(`Could not run \`claude\`: ${target.refused} Run the command above yourself.`);
16532
17012
  return 1;
16533
17013
  }
16534
- return await new Promise((resolve15) => {
17014
+ return await new Promise((resolve16) => {
16535
17015
  const child = spawn(target.command, target.args, { shell: target.shell, stdio: "inherit" });
16536
17016
  let settled = false;
16537
17017
  child.on("error", (err) => {
@@ -16541,16 +17021,16 @@ async function mcpAddCommand(options = {}) {
16541
17021
  fail(
16542
17022
  `Claude Code is not on your PATH, so nothing was registered. Run the command above once ${code("claude")} is installed.`
16543
17023
  );
16544
- resolve15(1);
17024
+ resolve16(1);
16545
17025
  return;
16546
17026
  }
16547
17027
  fail(`Could not run \`claude\`: ${err.message}`);
16548
- resolve15(1);
17028
+ resolve16(1);
16549
17029
  });
16550
17030
  child.on("close", (codeOut) => {
16551
17031
  if (settled) return;
16552
17032
  settled = true;
16553
- resolve15(codeOut ?? 1);
17033
+ resolve16(codeOut ?? 1);
16554
17034
  });
16555
17035
  });
16556
17036
  }
@@ -16669,10 +17149,10 @@ function upgradeCommand(plugin, marketplace) {
16669
17149
  var PLUGIN_NAME = "dashwise";
16670
17150
  var MARKETPLACE_NAME = "dashwise";
16671
17151
  var DEFAULT_MARKETPLACE_SOURCE = "Intuize/dashwise-cli";
16672
- var defaultRunner = (cmd, args, opts) => new Promise((resolve15) => {
17152
+ var defaultRunner = (cmd, args, opts) => new Promise((resolve16) => {
16673
17153
  const target = spawnTarget(cmd, args);
16674
17154
  if ("refused" in target) {
16675
- resolve15({ code: null, stdout: "", stderr: target.refused, missing: false });
17155
+ resolve16({ code: null, stdout: "", stderr: target.refused, missing: false });
16676
17156
  return;
16677
17157
  }
16678
17158
  const child = spawn(target.command, target.args, {
@@ -16687,7 +17167,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolve15) => {
16687
17167
  child.on("error", (err) => {
16688
17168
  if (settled) return;
16689
17169
  settled = true;
16690
- resolve15({
17170
+ resolve16({
16691
17171
  code: null,
16692
17172
  stdout,
16693
17173
  stderr: stderr || err.message,
@@ -16697,7 +17177,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolve15) => {
16697
17177
  child.on("close", (exitCode) => {
16698
17178
  if (settled) return;
16699
17179
  settled = true;
16700
- resolve15({ code: exitCode, stdout, stderr, missing: false });
17180
+ resolve16({ code: exitCode, stdout, stderr, missing: false });
16701
17181
  });
16702
17182
  });
16703
17183
  function readSkillsOnDisk() {
@@ -17938,6 +18418,7 @@ function judgeGeneratorStamp(stamp, opts = {}) {
17938
18418
  // src/commands/module/generate.ts
17939
18419
  init_sdk_version();
17940
18420
  init_output();
18421
+ init_declared_secrets();
17941
18422
  function outputDir(projectRoot) {
17942
18423
  return join(projectRoot, "node_modules", "@dashai", "generated");
17943
18424
  }
@@ -18014,6 +18495,9 @@ async function moduleGenerateCommand(options) {
18014
18495
  info(`Hash: ${dim(res.contentHash)}`);
18015
18496
  if (verdict.note !== null && declaresUses(manifest)) info(verdict.note);
18016
18497
  warnIfSdkBelowUsesFloor(projectRoot, manifest);
18498
+ reportSecretLines(
18499
+ syncEnvLocalSecretLines(projectRoot, declaredSecretNames(manifest))
18500
+ );
18017
18501
  return 0;
18018
18502
  }
18019
18503
  function declaresUses(manifest) {
@@ -18272,6 +18756,101 @@ init_init();
18272
18756
  // src/commands/module/install.ts
18273
18757
  init_api_client();
18274
18758
  init_config();
18759
+
18760
+ // src/lib/agent-session.ts
18761
+ var MARKERS = [
18762
+ ["CLAUDECODE", (v) => v === "1"],
18763
+ ["CLAUDE_CODE_REMOTE", (v) => v === "true"],
18764
+ ["CODEX_SANDBOX", (v) => v.length > 0],
18765
+ ["CODEX_SANDBOX_NETWORK_DISABLED", (v) => v.length > 0],
18766
+ ["CURSOR_AGENT", (v) => v.length > 0],
18767
+ ["DW_AGENT", (v) => v.length > 0],
18768
+ ["DASHWISE_AGENT_SESSION", (v) => v === "1" || v === "true"]
18769
+ ];
18770
+ function agentSessionMarker(env = process.env) {
18771
+ for (const [name, test] of MARKERS) {
18772
+ const value = env[name];
18773
+ if (value !== void 0 && test(value)) return name;
18774
+ }
18775
+ return null;
18776
+ }
18777
+
18778
+ // src/lib/drift-preview.ts
18779
+ init_api_client();
18780
+ function redactedConsumer(redacted) {
18781
+ switch (redacted) {
18782
+ case "private_copy":
18783
+ return "someone's private copy";
18784
+ case "other_team_copy":
18785
+ return "another team's copy";
18786
+ case "development_copy":
18787
+ return "a development copy you cannot see";
18788
+ default:
18789
+ return "a copy you cannot see";
18790
+ }
18791
+ }
18792
+ var WIDEN_WORDS = {
18793
+ uses_need_added: "a new table it reads from another module",
18794
+ uses_ops_widened: "more operations on a table it reads",
18795
+ uses_fields_added: "more fields of a table it reads",
18796
+ dependency_added: "a new or wider dependency",
18797
+ exposes_widened: "more of what it exposes to other modules",
18798
+ permissions_added: "more permissions",
18799
+ roles_widened: "more roles or permission keys",
18800
+ outbound_added: "new outbound APIs (data can leave)",
18801
+ secrets_added: "new secrets",
18802
+ public_access_enabled: "public access"
18803
+ };
18804
+ function describeWidening(widening) {
18805
+ return widening.map((w) => WIDEN_WORDS[w] ?? w).join(", ");
18806
+ }
18807
+ async function getUpgradePreview(installationId, version, options = {}) {
18808
+ return apiRequest(
18809
+ "GET",
18810
+ `/api/installations/${installationId}/upgrade-preview?version=${encodeURIComponent(version)}`,
18811
+ void 0,
18812
+ options
18813
+ );
18814
+ }
18815
+ function count(n, unit) {
18816
+ if (n === null) return `${unit}s not counted`;
18817
+ return `${n.toLocaleString("en-US")} ${unit}${n === 1 ? "" : "s"}`;
18818
+ }
18819
+ function describeDrift(drift) {
18820
+ const lines = [];
18821
+ const d = drift.details;
18822
+ const removedTables = d?.tables_to_remove ?? drift.tables_to_remove.map((t) => ({ table: t, name: t, rows: null }));
18823
+ for (const t of removedTables) {
18824
+ lines.push({
18825
+ loses: t.rows !== 0,
18826
+ text: `Moves table ${t.table} (${count(t.rows, "row")}) to the Trash`
18827
+ });
18828
+ }
18829
+ const removedFields = d?.fields_to_remove ?? drift.fields_to_remove.map((f) => ({ ...f, name: f.field, values: null }));
18830
+ for (const f of removedFields) {
18831
+ lines.push({
18832
+ loses: f.values !== 0,
18833
+ text: `Moves field ${f.table}.${f.field} (${count(f.values, "value")}) to the Trash`
18834
+ });
18835
+ }
18836
+ const retyped = d?.fields_to_retype ?? (drift.fields_to_retype ?? []).map((f) => ({ ...f, name: f.field, values: null }));
18837
+ for (const f of retyped) {
18838
+ lines.push({
18839
+ loses: f.values !== 0,
18840
+ 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)`
18841
+ });
18842
+ }
18843
+ lines.sort((a, b) => Number(b.loses) - Number(a.loses));
18844
+ for (const t of drift.tables_to_create) {
18845
+ lines.push({ loses: false, text: `Adds table ${t}` });
18846
+ }
18847
+ for (const f of drift.fields_to_create) {
18848
+ lines.push({ loses: false, text: `Adds field ${f.table}.${f.field}` });
18849
+ }
18850
+ return lines;
18851
+ }
18852
+
18853
+ // src/commands/module/install.ts
18275
18854
  init_manifest_io();
18276
18855
 
18277
18856
  // src/lib/module-lifecycle.ts
@@ -18450,18 +19029,31 @@ async function moduleInstallCommand(slugArg, options) {
18450
19029
  );
18451
19030
  }
18452
19031
  }
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)}?`;
19032
+ let preview;
19033
+ let lossy = false;
19034
+ let lossAccepted = false;
19035
+ if (existing) {
19036
+ const rerun = rerunLine(mod.slug, workspace2.slug, targetVersion, options, devInstallToReplace !== void 0);
19037
+ const previewed = await previewUpgrade(existing.id, targetVersion, options, auth, rerun, devInstallToReplace?.id);
19038
+ if (typeof previewed === "number") return previewed;
19039
+ preview = previewed.preview;
19040
+ lossy = previewed.lossy;
19041
+ lossAccepted = previewed.accepted;
19042
+ }
19043
+ const askForLoss = lossy && !lossAccepted;
19044
+ if (askForLoss || !options.yes && process.stdout.isTTY && !confirmedViaPicker) {
19045
+ const lossClause = lossy ? " It moves the data listed above to the Trash or converts it." : "";
19046
+ 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
19047
  const answer = await select({
18456
19048
  message,
18457
19049
  options: [
18458
19050
  {
18459
19051
  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"
19052
+ 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
19053
  },
18462
19054
  { value: "cancel", label: "No, abort" }
18463
19055
  ],
18464
- initialValue: devInstallToReplace ? "cancel" : "go"
19056
+ initialValue: devInstallToReplace || lossy ? "cancel" : "go"
18465
19057
  });
18466
19058
  if (isCancel(answer) || answer === "cancel") {
18467
19059
  fail("Aborted. Nothing was changed.");
@@ -18483,7 +19075,9 @@ async function moduleInstallCommand(slugArg, options) {
18483
19075
  workspaceId: workspace2.id,
18484
19076
  version: targetVersion,
18485
19077
  ...existing ? { installation_id: existing.id } : {},
18486
- ...options.force !== void 0 ? { force: options.force } : {}
19078
+ ...options.force !== void 0 ? { force: options.force } : {},
19079
+ // P5-08: confirms exactly the delta that was shown (409 DRIFT_CHANGED otherwise).
19080
+ ...preview ? { drift_hash: preview.drift_hash } : {}
18487
19081
  },
18488
19082
  auth
18489
19083
  );
@@ -18535,6 +19129,9 @@ async function moduleInstallCommand(slugArg, options) {
18535
19129
  was_upgrade: result.was_upgrade,
18536
19130
  public_url: publicUrl,
18537
19131
  verified,
19132
+ // P5-08: the confirmed preview; `null` for a fresh install (nothing to preview).
19133
+ drift_hash: preview?.drift_hash ?? null,
19134
+ data_loss: preview ? lossy : null,
18538
19135
  // `null` when the binding surface was unreachable; `[]` when the
18539
19136
  // module declares no slots. The two are not the same thing.
18540
19137
  uses: readiness === null ? null : usesInstallJson(readiness),
@@ -18703,6 +19300,19 @@ function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
18703
19300
  Ask the module's author to invite this workspace (or open the install gate), then retry.`
18704
19301
  );
18705
19302
  break;
19303
+ case "DRIFT_CHANGED":
19304
+ fail(
19305
+ `Install ${code(slug)} changed since its upgrade preview, so the confirmed change no longer matches. Nothing was changed.
19306
+ Re-run ${code("dashwise module install")} to see the new preview.`
19307
+ );
19308
+ break;
19309
+ case "DATA_LOSS_CONFIRM_REQUIRED":
19310
+ case "UPGRADE_CONFIRM_REQUIRED":
19311
+ fail(
19312
+ `The platform asks for a confirmed preview before this upgrade (${err.code}). Nothing was changed.
19313
+ Re-run ${code("dashwise module install")}: it previews the upgrade and sends the confirmation.`
19314
+ );
19315
+ break;
18706
19316
  case "UPGRADE_BREAKS_CONSUMERS":
18707
19317
  fail(
18708
19318
  `Upgrading ${code(slug)} would break modules that consume its current schema.
@@ -18752,6 +19362,75 @@ function usesInstallJson(readiness) {
18752
19362
  }))
18753
19363
  };
18754
19364
  }
19365
+ async function previewUpgrade(installationId, targetVersion, options, auth, rerun, devInstallToReplaceId) {
19366
+ let preview;
19367
+ try {
19368
+ preview = await getUpgradePreview(installationId, targetVersion, auth);
19369
+ } catch (err) {
19370
+ const why = err instanceof ApiError ? `${err.code || err.status} ${err.message}` : err.message;
19371
+ const checkNote = options.acceptDataLoss !== void 0 ? `, so ${code("--accept-data-loss")} cannot be checked` : "";
19372
+ fail(
19373
+ `Could not preview what upgrading install #${installationId} to ${code(`v${targetVersion}`)} does to its data (${why})${checkNote}. Nothing was changed.`
19374
+ );
19375
+ info(`An upgrade is never applied without its preview; retry when the platform answers.`);
19376
+ return 1;
19377
+ }
19378
+ const lines = describeDrift(preview.drift);
19379
+ const lossy = lines.some((l) => l.loses);
19380
+ const from = preview.from_version ? `v${preview.from_version}` : "its current version";
19381
+ if (lines.length === 0) {
19382
+ info(`Upgrading install #${installationId} from ${from} to v${preview.to_version} changes no table or field.`);
19383
+ } else {
19384
+ info(`What upgrading install #${installationId} from ${from} to v${preview.to_version} does to its data:`);
19385
+ for (const line of lines) {
19386
+ (line.loses ? warn : info)(` ${line.text}`);
19387
+ }
19388
+ }
19389
+ const broken = preview.breaks_consumers ?? [];
19390
+ if (broken.length > 0) {
19391
+ const names = broken.map(
19392
+ (c2) => c2.module_slug ? `${c2.module_slug}${c2.installation_id ? ` #${c2.installation_id}` : ""}` : redactedConsumer(c2.redacted)
19393
+ );
19394
+ warn(` Breaks ${broken.length} module${broken.length === 1 ? "" : "s"} that read${broken.length === 1 ? "s" : ""} this one: ${names.join(", ")}`);
19395
+ }
19396
+ if (preview.approval_required) {
19397
+ const asks = preview.widening && preview.widening.length > 0 ? `: it asks for ${describeWidening(preview.widening)}` : "";
19398
+ warn(` This update waits for the module author's approval${asks}.`);
19399
+ }
19400
+ if (options.acceptDataLoss !== void 0 && options.acceptDataLoss !== preview.drift_hash) {
19401
+ fail(
19402
+ `${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.`
19403
+ );
19404
+ info(`To accept the change listed above, a person re-runs:
19405
+ ${rerun(preview.drift_hash)}`);
19406
+ return 1;
19407
+ }
19408
+ const accepted = lossy && options.acceptDataLoss === preview.drift_hash;
19409
+ if (lossy && !accepted) {
19410
+ const yesNote = options.yes ? ` ${code("--yes")} does not accept data loss.` : "";
19411
+ const marker = agentSessionMarker();
19412
+ 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;
19413
+ if (why) {
19414
+ const alsoDev = devInstallToReplaceId !== void 0 ? ` and destroys dev install #${devInstallToReplaceId} first` : "";
19415
+ fail(`This upgrade loses the data listed above${alsoDev}, and ${why}.${yesNote} Refused. Nothing was changed.`);
19416
+ info(`To accept exactly that loss, a person re-runs:
19417
+ ${rerun(preview.drift_hash)}`);
19418
+ return 1;
19419
+ }
19420
+ }
19421
+ return { preview, lossy, accepted };
19422
+ }
19423
+ function rerunLine(slug, workspaceSlug, targetVersion, options, replacingDevInstall) {
19424
+ return (hash) => [
19425
+ "dashwise module install",
19426
+ slug,
19427
+ `--workspace ${workspaceSlug}`,
19428
+ `--version ${targetVersion}`,
19429
+ ...options.force ? ["--force"] : [],
19430
+ ...replacingDevInstall ? ["--replace-dev-install"] : [],
19431
+ `--accept-data-loss ${hash}`
19432
+ ].join(" ");
19433
+ }
18755
19434
 
18756
19435
  // src/commands/module/list.ts
18757
19436
  init_api_client();
@@ -19121,6 +19800,7 @@ function formatBytes(n) {
19121
19800
  }
19122
19801
 
19123
19802
  // src/commands/module/publish.ts
19803
+ init_diff_check();
19124
19804
  var VALID_BUMP = /* @__PURE__ */ new Set(["patch", "minor", "major"]);
19125
19805
  var PACKED_FILE_LIST_CAP = 20;
19126
19806
  function packedFileLines(files, cap = PACKED_FILE_LIST_CAP) {
@@ -19207,6 +19887,10 @@ async function modulePublishCommand(options) {
19207
19887
  info(
19208
19888
  `Module: ${code(mod.slug)} (id ${mod.id})${mod.current_version ? `, current ${code(`v${mod.current_version}`)}` : dim(", no published version yet")}`
19209
19889
  );
19890
+ if (mod.current_version) {
19891
+ const gate = await publishEvolutionGate(projectRoot, options.strict === true);
19892
+ if (gate !== 0) return gate;
19893
+ }
19210
19894
  let tarball;
19211
19895
  try {
19212
19896
  tarball = await packTarball({
@@ -19563,6 +20247,22 @@ function refusalHint(code2, context, subject = {}) {
19563
20247
  message: `Too many builds of ${named} in the last hour. Try again later.`,
19564
20248
  needsGrant: false
19565
20249
  };
20250
+ // Install invitations (`dashwise module invites`, Südbau retro 6).
20251
+ case "AUTHOR_ADMIN_REQUIRED":
20252
+ return {
20253
+ message: `Only an admin of the company that owns ${named} can manage its install invitations. Ask that company's admin.`,
20254
+ needsGrant: true
20255
+ };
20256
+ case "COMPANY_ADMIN_REQUIRED":
20257
+ return {
20258
+ message: "Only an admin of this company can see or decline its install invitations. Ask your company's admin.",
20259
+ needsGrant: true
20260
+ };
20261
+ case "MODULE_PRIVATE_SHARE_FIRST":
20262
+ return {
20263
+ message: `${named} is still private. Share it with your company in Dashwise first, then invite another company.`,
20264
+ needsGrant: false
20265
+ };
19566
20266
  default:
19567
20267
  if (MODULE_RIGHT_CODES.has(code2)) {
19568
20268
  return {
@@ -19798,7 +20498,7 @@ async function moduleReleaseCommand(slugArg, options) {
19798
20498
  }
19799
20499
  async function wait() {
19800
20500
  const remaining = Math.max(0, deadline - Date.now());
19801
- await new Promise((resolve15) => setTimeout(resolve15, Math.min(options.pollIntervalMs ?? 4e3, remaining)));
20501
+ await new Promise((resolve16) => setTimeout(resolve16, Math.min(options.pollIntervalMs ?? 4e3, remaining)));
19802
20502
  }
19803
20503
  }
19804
20504
 
@@ -19972,6 +20672,133 @@ function usesJson(readiness) {
19972
20672
  };
19973
20673
  }
19974
20674
 
20675
+ // src/commands/module/installations.ts
20676
+ init_api_client();
20677
+ init_config();
20678
+ init_output();
20679
+ var AUDIENCE = {
20680
+ company: "company",
20681
+ teams: "Team Spaces",
20682
+ people: "named people"
20683
+ };
20684
+ var givesRole = (roleKey) => typeof roleKey === "string" && roleKey !== "none";
20685
+ function governanceAudience(row) {
20686
+ if (!row.rules) return null;
20687
+ if (givesRole(row.rules.company)) return AUDIENCE.company;
20688
+ if (row.rules.teams.some((t) => t.live === true && givesRole(t.role_key))) return AUDIENCE.teams;
20689
+ return AUDIENCE.people;
20690
+ }
20691
+ function toListEntry(row) {
20692
+ return {
20693
+ id: row.id,
20694
+ module_slug: row.module?.slug ?? null,
20695
+ name: row.name ?? row.module?.name ?? null,
20696
+ audience: row.reach?.kind ? AUDIENCE[row.reach.kind] ?? row.reach.kind : null,
20697
+ version: row.module_version?.version ?? null,
20698
+ held: row.held === true,
20699
+ development: isDevInstallKind(row.kind),
20700
+ public_url: row.public_url,
20701
+ admin_view_only: false
20702
+ };
20703
+ }
20704
+ async function moduleInstallationsCommand(slug, options) {
20705
+ if (!isLoggedIn()) {
20706
+ fail(`Not logged in. Run ${code("dashwise login")} first.`);
20707
+ return 1;
20708
+ }
20709
+ const cfg = readConfig();
20710
+ if (!cfg) {
20711
+ fail(`CLI config missing. Run ${code("dashwise login")}.`);
20712
+ return 1;
20713
+ }
20714
+ const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
20715
+ let workspace2;
20716
+ try {
20717
+ workspace2 = await resolveWorkspaceWithFallback({
20718
+ userFlag: options.workspace,
20719
+ cachedWorkspaceId: slug ? readModuleEntry(slug)?.workspaceId : void 0,
20720
+ configDefault: cfg.workspaceSlug,
20721
+ apiUrl: resolveApiUrl(),
20722
+ apiToken: cfg.token
20723
+ });
20724
+ } catch (err) {
20725
+ fail(`Company selection failed: ${err.message}`);
20726
+ return 1;
20727
+ }
20728
+ let rows;
20729
+ try {
20730
+ rows = await listWorkspaceInstallations(workspace2.id, auth);
20731
+ } catch (err) {
20732
+ fail(`Could not read the installations in ${code(workspace2.slug)}: ${err.message}`);
20733
+ return 1;
20734
+ }
20735
+ const governance = [];
20736
+ try {
20737
+ const govRows = await apiRequest(
20738
+ "GET",
20739
+ `/api/modules/workspaces/${workspace2.id}/installations/governance`,
20740
+ void 0,
20741
+ auth
20742
+ );
20743
+ const seen = new Set(rows.map((row) => row.id));
20744
+ const slugOf = new Map(rows.map((row) => [row.module_id, row.module?.slug ?? null]));
20745
+ let slugModuleId;
20746
+ if (slug) {
20747
+ slugModuleId = rows.find((row) => row.module?.slug === slug)?.module_id;
20748
+ if (slugModuleId === void 0) {
20749
+ try {
20750
+ slugModuleId = (await getModuleBySlug(slug, auth)).id;
20751
+ } catch {
20752
+ }
20753
+ }
20754
+ }
20755
+ for (const g of govRows) {
20756
+ if (g.trashed || seen.has(g.installation_id)) continue;
20757
+ if (slug && g.module_id !== slugModuleId) continue;
20758
+ governance.push({
20759
+ id: g.installation_id,
20760
+ module_slug: slugOf.get(g.module_id) ?? (slug && g.module_id === slugModuleId ? slug : null),
20761
+ name: g.name || null,
20762
+ audience: governanceAudience(g),
20763
+ version: null,
20764
+ held: g.held === true,
20765
+ development: null,
20766
+ public_url: null,
20767
+ admin_view_only: true
20768
+ });
20769
+ }
20770
+ } catch (err) {
20771
+ if (!(err instanceof ApiError && (err.status === 403 || err.status === 404))) {
20772
+ fail(`Could not read the company's full installation list: ${err.message}`);
20773
+ return 1;
20774
+ }
20775
+ }
20776
+ const entries = rows.filter((row) => !slug || row.module?.slug === slug).map(toListEntry).concat(governance).filter((entry) => options.dev || !entry.development).sort((a, b) => (a.module_slug ?? "").localeCompare(b.module_slug ?? "") || a.id - b.id);
20777
+ if (options.json) {
20778
+ out(JSON.stringify({ workspace: workspace2.slug, installations: entries }));
20779
+ return 0;
20780
+ }
20781
+ if (entries.length === 0) {
20782
+ info(
20783
+ slug ? `No installations of ${code(slug)} you can see in ${code(workspace2.slug)}.` : `No installations you can see in ${code(workspace2.slug)}.`
20784
+ );
20785
+ return 0;
20786
+ }
20787
+ for (const e of entries) {
20788
+ const tags = [
20789
+ e.audience,
20790
+ e.version ? `v${e.version}` : null,
20791
+ e.held ? "held" : null,
20792
+ e.development ? "development" : null,
20793
+ e.admin_view_only ? "admin view, not open to you" : null
20794
+ ].filter(Boolean);
20795
+ out(
20796
+ `#${e.id} ${e.module_slug ?? "?"} ${e.name ? `"${e.name}"` : ""} ${dim(tags.join(", "))}${e.public_url ? ` ${e.public_url}` : ""}`.replace(/ {3,}/g, " ")
20797
+ );
20798
+ }
20799
+ return 0;
20800
+ }
20801
+
19975
20802
  // src/commands/module/uninstall.ts
19976
20803
  init_api_client();
19977
20804
  init_config();
@@ -20142,6 +20969,269 @@ function surfaceUninstallError(err, slug, workspaceSlug) {
20142
20969
  return 1;
20143
20970
  }
20144
20971
 
20972
+ // src/commands/module/invites.ts
20973
+ init_api_client();
20974
+ init_config();
20975
+ init_output();
20976
+ var positiveId2 = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
20977
+ function session(json) {
20978
+ const cfg = readConfig();
20979
+ if (!isLoggedIn() || !cfg) return reportCliError(`Not logged in. Run ${code("dashwise login")} first.`, { json });
20980
+ return {
20981
+ auth: { baseUrl: resolveApiUrl(), auth: { token: cfg.token } },
20982
+ token: cfg.token,
20983
+ workspaceSlug: cfg.workspaceSlug
20984
+ };
20985
+ }
20986
+ async function resolveModule(slug, s, json) {
20987
+ try {
20988
+ const mod = await getModuleBySlug(slug, s.auth);
20989
+ return { id: mod.id, slug: mod.slug };
20990
+ } catch (err) {
20991
+ return reportRefusal(err, { module: slug }, { json, action: "Resolving the module" });
20992
+ }
20993
+ }
20994
+ async function confirm(question, confirmLabel, opts) {
20995
+ if (opts.yes) return null;
20996
+ if (opts.json || !process.stdin.isTTY || !process.stdout.isTTY) {
20997
+ return reportCliError("This changes an invitation. Pass --yes to confirm in non-interactive or --json mode.", {
20998
+ json: opts.json,
20999
+ code: "USAGE"
21000
+ });
21001
+ }
21002
+ const answer = await select({
21003
+ message: question,
21004
+ options: [
21005
+ { value: "cancel", label: "Cancel" },
21006
+ { value: "go", label: confirmLabel }
21007
+ ],
21008
+ initialValue: "cancel"
21009
+ });
21010
+ if (isCancel(answer) || answer !== "go") {
21011
+ reportCliError("Aborted.", {});
21012
+ return 130;
21013
+ }
21014
+ return null;
21015
+ }
21016
+ function companyLabel(row) {
21017
+ return row.company_name ? `${row.company_name} (company #${row.workspace_id})` : `company #${row.workspace_id}`;
21018
+ }
21019
+ async function moduleInvitesListCommand(slug, options) {
21020
+ const { json } = options;
21021
+ const s = session(json);
21022
+ if (typeof s === "number") return s;
21023
+ const mod = await resolveModule(slug, s, json);
21024
+ if (typeof mod === "number") return mod;
21025
+ let rows;
21026
+ try {
21027
+ rows = await apiRequest("GET", `/api/modules/${mod.id}/invites`, void 0, s.auth);
21028
+ } catch (err) {
21029
+ return reportRefusal(err, { module: mod.slug }, { json, action: "Reading the invitations" });
21030
+ }
21031
+ if (json) {
21032
+ out(JSON.stringify({ module: mod.slug, invites: rows }));
21033
+ return 0;
21034
+ }
21035
+ if (rows.length === 0) {
21036
+ info(`No open invitations for ${code(mod.slug)}.`);
21037
+ return 0;
21038
+ }
21039
+ for (const row of rows) {
21040
+ const note = row.note ? ` ${dim(`"${row.note}"`)}` : "";
21041
+ out(`#${row.id} ${companyLabel(row)} ${dim(row.invited_at.slice(0, 10))}${note}`);
21042
+ }
21043
+ return 0;
21044
+ }
21045
+ async function moduleInvitesSendCommand(slug, options) {
21046
+ const { json } = options;
21047
+ const usage2 = (message) => reportCliError(message, { json, code: "USAGE" });
21048
+ if (!options.to) return usage2("Pass --to <company id> to name the company to invite.");
21049
+ const s = session(json);
21050
+ if (typeof s === "number") return s;
21051
+ let target;
21052
+ const numeric = /^[1-9][0-9]*$/.test(options.to) ? Number(options.to) : void 0;
21053
+ let mine;
21054
+ try {
21055
+ mine = await apiRequest("GET", "/api/workspaces", void 0, s.auth);
21056
+ } catch (err) {
21057
+ return reportRefusal(err, {}, { json, action: "Reading your companies" });
21058
+ }
21059
+ const own = mine.find((w) => numeric !== void 0 ? w.id === numeric : w.slug === options.to);
21060
+ if (own) {
21061
+ target = { id: own.id, label: `${own.name} (company #${own.id})` };
21062
+ } else if (positiveId2(numeric)) {
21063
+ target = { id: numeric, label: `company #${numeric}` };
21064
+ } else {
21065
+ return usage2(
21066
+ `${options.to} is not one of your companies. For another company, pass its company id (an admin of that company finds it in its settings).`
21067
+ );
21068
+ }
21069
+ const mod = await resolveModule(slug, s, json);
21070
+ if (typeof mod === "number") return mod;
21071
+ const stop = await confirm(
21072
+ [
21073
+ `Invite ${target.label} to install ${mod.slug}?`,
21074
+ `- Admins of that company can then see ${mod.slug} and install it.`,
21075
+ "- Their data stays theirs; you do not see it.",
21076
+ `- Your module's logic and its updates reach their installations.`,
21077
+ "- You see their company name only after one of their admins installs it or asks to.",
21078
+ `- You can withdraw the invitation at any time with ${code(`dashwise module invites revoke ${mod.slug} <invitation id>`)}.`
21079
+ ].join("\n"),
21080
+ "Send the invitation",
21081
+ options
21082
+ );
21083
+ if (stop !== null) return stop;
21084
+ let invite;
21085
+ try {
21086
+ const body = await apiRequest(
21087
+ "POST",
21088
+ `/api/modules/${mod.id}/invites`,
21089
+ { workspaceId: target.id, ...options.note !== void 0 ? { note: options.note } : {} },
21090
+ s.auth
21091
+ );
21092
+ invite = body.invite;
21093
+ } catch (err) {
21094
+ return reportRefusal(err, { module: mod.slug }, { json, action: "Sending the invitation" });
21095
+ }
21096
+ if (json) out(JSON.stringify({ module: mod.slug, invite }));
21097
+ else success(`Invited ${target.label} to install ${mod.slug} (invitation #${invite.id}).`);
21098
+ return 0;
21099
+ }
21100
+ async function moduleInvitesRevokeCommand(slug, inviteId, options) {
21101
+ const { json } = options;
21102
+ if (!positiveId2(inviteId)) {
21103
+ return reportCliError(`Pass the invitation id. List them with ${code(`dashwise module invites list ${slug}`)}.`, {
21104
+ json,
21105
+ code: "USAGE"
21106
+ });
21107
+ }
21108
+ const s = session(json);
21109
+ if (typeof s === "number") return s;
21110
+ const mod = await resolveModule(slug, s, json);
21111
+ if (typeof mod === "number") return mod;
21112
+ const stop = await confirm(
21113
+ [
21114
+ `Withdraw invitation #${inviteId} for ${mod.slug}?`,
21115
+ `- Installations made with it stay, but they no longer receive updates of ${mod.slug}.`,
21116
+ "- No new installation can start from it."
21117
+ ].join("\n"),
21118
+ "Withdraw the invitation",
21119
+ options
21120
+ );
21121
+ if (stop !== null) return stop;
21122
+ try {
21123
+ await apiRequest("DELETE", `/api/modules/${mod.id}/invites/${inviteId}`, void 0, s.auth);
21124
+ } catch (err) {
21125
+ return reportRefusal(err, { module: mod.slug }, { json, action: "Withdrawing the invitation" });
21126
+ }
21127
+ if (json) out(JSON.stringify({ module: mod.slug, revoked: inviteId }));
21128
+ else success(`Withdrew invitation #${inviteId} for ${mod.slug}.`);
21129
+ return 0;
21130
+ }
21131
+ async function resolveOwnCompany(s, flag, opts) {
21132
+ const { json } = opts;
21133
+ let mine;
21134
+ try {
21135
+ mine = await apiRequest("GET", "/api/workspaces", void 0, s.auth);
21136
+ } catch (err) {
21137
+ return reportRefusal(err, {}, { json, action: "Reading your companies" });
21138
+ }
21139
+ const list = mine.map((w) => `${w.slug} (company #${w.id})`).join(", ");
21140
+ const wanted = flag ?? s.workspaceSlug;
21141
+ if (wanted !== void 0) {
21142
+ const found = mine.find((w) => w.slug === wanted || String(w.id) === wanted);
21143
+ if (found) return found;
21144
+ return reportCliError(`${wanted} is not one of your companies. Your companies: ${list}.`, { json, code: "USAGE" });
21145
+ }
21146
+ if (mine.length === 1) return mine[0];
21147
+ if (mine.length === 0) return reportCliError("Your account belongs to no company yet.", { json });
21148
+ if (!opts.interactive || json || !process.stdin.isTTY || !process.stdout.isTTY) {
21149
+ return reportCliError(`Choose the company with --workspace <slug or id>. Your companies: ${list}.`, {
21150
+ json,
21151
+ code: "USAGE"
21152
+ });
21153
+ }
21154
+ const choice = await select({
21155
+ message: "Which company?",
21156
+ options: mine.map((w) => ({ value: String(w.id), label: w.name, hint: w.slug }))
21157
+ });
21158
+ const picked = isCancel(choice) ? void 0 : mine.find((w) => String(w.id) === choice);
21159
+ if (!picked) {
21160
+ reportCliError("Aborted.", {});
21161
+ return 130;
21162
+ }
21163
+ return picked;
21164
+ }
21165
+ async function moduleInvitesReceivedCommand(options) {
21166
+ const { json } = options;
21167
+ const s = session(json);
21168
+ if (typeof s === "number") return s;
21169
+ const company = await resolveOwnCompany(s, options.workspace, { json, interactive: true });
21170
+ if (typeof company === "number") return company;
21171
+ let rows;
21172
+ try {
21173
+ rows = await apiRequest(
21174
+ "GET",
21175
+ `/api/modules/workspaces/${company.id}/invites`,
21176
+ void 0,
21177
+ s.auth
21178
+ );
21179
+ } catch (err) {
21180
+ return reportRefusal(err, {}, { json, action: "Reading the invitations" });
21181
+ }
21182
+ if (json) {
21183
+ out(JSON.stringify({ workspace: company.slug, invites: rows }));
21184
+ return 0;
21185
+ }
21186
+ if (rows.length === 0) {
21187
+ info(`No open invitations for ${code(company.slug)}.`);
21188
+ return 0;
21189
+ }
21190
+ for (const row of rows) {
21191
+ const note = row.note ? ` ${dim(`"${row.note}"`)}` : "";
21192
+ out(
21193
+ `#${row.id} ${row.module.slug} ${dim(`v${row.module.current_version}`)} from ${row.from_company.name} ${dim(row.invited_at.slice(0, 10))}${note}`
21194
+ );
21195
+ }
21196
+ const inCompany = `--workspace ${company.slug}`;
21197
+ info(
21198
+ `Install one with ${code(`dashwise module install <slug> ${inCompany}`)}, or decline it with ${code(`dashwise module invites decline <id> ${inCompany}`)}.`
21199
+ );
21200
+ return 0;
21201
+ }
21202
+ async function moduleInvitesDeclineCommand(inviteId, options) {
21203
+ const { json } = options;
21204
+ if (!positiveId2(inviteId)) {
21205
+ return reportCliError(`Pass the invitation id. List them with ${code("dashwise module invites received")}.`, {
21206
+ json,
21207
+ code: "USAGE"
21208
+ });
21209
+ }
21210
+ const s = session(json);
21211
+ if (typeof s === "number") return s;
21212
+ const company = await resolveOwnCompany(s, options.workspace, { json, interactive: !options.yes });
21213
+ if (typeof company === "number") return company;
21214
+ const stop = await confirm(
21215
+ `Decline invitation #${inviteId} for ${company.slug}? The module's company may invite you again.`,
21216
+ "Decline the invitation",
21217
+ options
21218
+ );
21219
+ if (stop !== null) return stop;
21220
+ try {
21221
+ await apiRequest(
21222
+ "POST",
21223
+ `/api/modules/workspaces/${company.id}/invites/${inviteId}/decline`,
21224
+ void 0,
21225
+ s.auth
21226
+ );
21227
+ } catch (err) {
21228
+ return reportRefusal(err, {}, { json, action: "Declining the invitation" });
21229
+ }
21230
+ if (json) out(JSON.stringify({ workspace: company.slug, declined: inviteId }));
21231
+ else success(`Declined invitation #${inviteId}.`);
21232
+ return 0;
21233
+ }
21234
+
20145
21235
  // src/commands/module/pull-data.ts
20146
21236
  init_api_client();
20147
21237
  init_output();
@@ -20220,6 +21310,7 @@ async function modulePullDataCommand(options) {
20220
21310
  // src/commands/module/validate.ts
20221
21311
  init_api_client();
20222
21312
  init_output();
21313
+ init_manifest_reference_lint();
20223
21314
  async function moduleValidateCommand(options) {
20224
21315
  const projectRoot = resolve(options.dir ?? process.cwd());
20225
21316
  const manifestPath2 = join(projectRoot, "module.json");
@@ -20244,6 +21335,7 @@ async function moduleValidateCommand(options) {
20244
21335
  );
20245
21336
  if (res.ok) {
20246
21337
  success("Manifest is valid.");
21338
+ for (const w of referenceFieldWarnings(manifest)) warn(w.message);
20247
21339
  return 0;
20248
21340
  }
20249
21341
  fail(`Unexpected response shape: ${JSON.stringify(res)}`);
@@ -22646,6 +23738,23 @@ init_api_client();
22646
23738
  init_config();
22647
23739
  init_manifest_io();
22648
23740
  init_output();
23741
+ init_declared_secrets();
23742
+ var SECRETS_NOTE = "# Declared secrets (runtime.secrets[]): set each value on your host; Dashwise never exports a secret value.";
23743
+ function secretNameLines(names, format) {
23744
+ const lines = names.map((name) => {
23745
+ switch (format) {
23746
+ case "env":
23747
+ return `# ${name}=`;
23748
+ case "railway":
23749
+ return `# ${name}: railway variables set ${name}='<value>'`;
23750
+ case "vercel":
23751
+ return `# ${name}: vercel env add ${name} production`;
23752
+ case "fly":
23753
+ return `# ${name}: fly secrets set ${name}='<value>'`;
23754
+ }
23755
+ });
23756
+ return [SECRETS_NOTE, ...lines].join("\n");
23757
+ }
22649
23758
  var PLACEHOLDER = "<run-`dashwise-api-keys-create-<name>`-and-paste-here>";
22650
23759
  async function envExportCommand(options) {
22651
23760
  const format = options.format ?? "env";
@@ -22736,7 +23845,19 @@ async function envExportCommand(options) {
22736
23845
  ...apiKeyComment !== null ? { comment: apiKeyComment } : {}
22737
23846
  }
22738
23847
  ];
23848
+ const secretNames = options.withSecretNames ? declaredSecretNames(manifest) : [];
22739
23849
  process.stdout.write(render(entries, format));
23850
+ if (secretNames.length > 0) {
23851
+ if (format === "json") {
23852
+ info(
23853
+ `Declared secrets to set on your host (Dashwise never exports a secret value): ${secretNames.join(", ")}`
23854
+ );
23855
+ } else {
23856
+ process.stdout.write(`
23857
+
23858
+ ${secretNameLines(secretNames, format)}`);
23859
+ }
23860
+ }
22740
23861
  if (format !== "json") process.stdout.write("\n");
22741
23862
  if (apiKey === PLACEHOLDER) {
22742
23863
  info("");
@@ -22792,6 +23913,7 @@ init_config();
22792
23913
  init_dotenv_local();
22793
23914
  init_manifest_io();
22794
23915
  init_output();
23916
+ init_install_target();
22795
23917
  init_dev();
22796
23918
  async function queryCommand(options) {
22797
23919
  const projectRoot = resolve(options.dir ?? process.cwd());
@@ -22814,17 +23936,25 @@ async function queryCommand(options) {
22814
23936
  );
22815
23937
  return 1;
22816
23938
  }
23939
+ const keyInShell = process.env.DASHWISE_API_KEY !== void 0;
22817
23940
  hydrateEnvFromDotenvLocal(projectRoot);
23941
+ const cfg = (() => {
23942
+ try {
23943
+ return readConfig();
23944
+ } catch {
23945
+ return null;
23946
+ }
23947
+ })();
23948
+ const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
23949
+ let keySource = keyInShell ? "environment" : ".env.local";
23950
+ let entry = manifestSlug ? readModuleEntry(manifestSlug) : null;
22818
23951
  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);
23952
+ if (entry && options.install !== void 0 && entry.installationId !== options.install) {
23953
+ fail(
23954
+ `No key for install #${options.install}: the cached ${code("dwk_")} key of ${code(manifestSlug)} belongs to install #${entry.installationId}. Nothing was run.`
23955
+ );
23956
+ return 1;
23957
+ }
22828
23958
  if (cfg) {
22829
23959
  const healed = await resolveModuleApiKey(
22830
23960
  manifestSlug,
@@ -22836,11 +23966,26 @@ async function queryCommand(options) {
22836
23966
  }
22837
23967
  if (entry?.apiKey) {
22838
23968
  process.env.DASHWISE_API_KEY = entry.apiKey;
23969
+ keySource = "cached";
22839
23970
  if (!process.env.DASHWISE_API_URL) {
22840
23971
  process.env.DASHWISE_API_URL = apiUrl;
22841
23972
  }
22842
23973
  }
22843
23974
  }
23975
+ if (process.env.DASHWISE_API_KEY) {
23976
+ const target = await nameKeyTarget({
23977
+ apiKey: process.env.DASHWISE_API_KEY,
23978
+ keySource,
23979
+ entry,
23980
+ cfg,
23981
+ apiUrl: process.env.DASHWISE_API_URL ?? apiUrl,
23982
+ ...options.install !== void 0 ? { install: options.install } : {}
23983
+ });
23984
+ if (target !== 0) return target;
23985
+ } else if (options.install !== void 0) {
23986
+ fail(`No ${code("dwk_")} key to check against install #${options.install}. Set ${code("DASHWISE_API_KEY")}. Nothing was run.`);
23987
+ return 1;
23988
+ }
22844
23989
  const modes = [
22845
23990
  options.inline !== void 0 ? "inline" : null,
22846
23991
  options.file !== void 0 ? "file" : null,
@@ -23206,7 +24351,7 @@ function formatTimestamp(iso) {
23206
24351
  return iso.slice(0, 19).replace("T", " ");
23207
24352
  }
23208
24353
  function sleep2(ms) {
23209
- return new Promise((resolve15) => setTimeout(resolve15, ms));
24354
+ return new Promise((resolve16) => setTimeout(resolve16, ms));
23210
24355
  }
23211
24356
 
23212
24357
  // src/commands/module/action-log.ts
@@ -23308,7 +24453,7 @@ async function runTail2(installationId, options) {
23308
24453
  }
23309
24454
  dim(`(tail: transient error \u2014 ${err.message})`);
23310
24455
  }
23311
- await new Promise((resolve15) => setTimeout(resolve15, POLL_INTERVAL_MS2));
24456
+ await new Promise((resolve16) => setTimeout(resolve16, POLL_INTERVAL_MS2));
23312
24457
  }
23313
24458
  }
23314
24459
  function printTable(rows) {
@@ -23693,7 +24838,7 @@ async function sleep3(ms, aborted) {
23693
24838
  const slice = 100;
23694
24839
  let waited = 0;
23695
24840
  while (waited < ms && !aborted()) {
23696
- await new Promise((resolve15) => setTimeout(resolve15, Math.min(slice, ms - waited)));
24841
+ await new Promise((resolve16) => setTimeout(resolve16, Math.min(slice, ms - waited)));
23697
24842
  waited += slice;
23698
24843
  }
23699
24844
  }
@@ -24034,7 +25179,7 @@ async function moduleRuntimeSetCommand(slugArg, options) {
24034
25179
  const slug = before.module.slug;
24035
25180
  const subject = { module: slug, installationId: before.installation_id };
24036
25181
  if (streaming !== void 0 && !options.yes && !options.json && process.stdin.isTTY) {
24037
- const ok = await confirm({
25182
+ const ok = await confirm$1({
24038
25183
  message: `Turn streaming ${streaming === "RESPONSE_STREAM" ? "on" : "off"} for ${slug} (install #${before.installation_id})? The switch takes about a minute, and pages may come back empty while it runs.`
24039
25184
  });
24040
25185
  if (isCancel(ok) || ok !== true) {
@@ -24524,12 +25669,29 @@ moduleCmd.command("list").description("List modules you own (or `--all` for owne
24524
25669
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
24525
25670
  });
24526
25671
  });
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
- });
25672
+ moduleCmd.command("diff").description(
25673
+ "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."
25674
+ ).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(
25675
+ async (cmdOpts) => {
25676
+ if (cmdOpts.check) {
25677
+ const { moduleDiffCheckCommand: moduleDiffCheckCommand2 } = await Promise.resolve().then(() => (init_diff_check(), diff_check_exports));
25678
+ process.exitCode = await moduleDiffCheckCommand2({
25679
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
25680
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
25681
+ ...cmdOpts.base !== void 0 ? { base: cmdOpts.base } : {},
25682
+ ...cmdOpts.baseFile !== void 0 ? { baseFile: cmdOpts.baseFile } : {},
25683
+ ...cmdOpts.head !== void 0 ? { head: cmdOpts.head } : {},
25684
+ // commander maps `--no-history` to `history === false`.
25685
+ ...cmdOpts.history === false ? { noHistory: true } : {}
25686
+ });
25687
+ return;
25688
+ }
25689
+ process.exitCode = await moduleDiffCommand({
25690
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
25691
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
25692
+ });
25693
+ }
25694
+ );
24533
25695
  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
25696
  process.exitCode = await modulePullDataCommand({
24535
25697
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
@@ -24681,6 +25843,10 @@ program.command("query").description(
24681
25843
  ).option(
24682
25844
  "--stream",
24683
25845
  "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."
25846
+ ).option(
25847
+ "--install <id>",
25848
+ "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.",
25849
+ parseIntOption
24684
25850
  ).action(
24685
25851
  async (cmdOpts) => {
24686
25852
  process.exitCode = await queryCommand({
@@ -24690,16 +25856,22 @@ program.command("query").description(
24690
25856
  ...cmdOpts.repl !== void 0 ? { repl: cmdOpts.repl } : {},
24691
25857
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
24692
25858
  ...cmdOpts.explain !== void 0 ? { explain: cmdOpts.explain } : {},
24693
- ...cmdOpts.stream !== void 0 ? { stream: cmdOpts.stream } : {}
25859
+ ...cmdOpts.stream !== void 0 ? { stream: cmdOpts.stream } : {},
25860
+ ...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {}
24694
25861
  });
24695
25862
  }
24696
25863
  );
24697
25864
  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) => {
25865
+ "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`."
25866
+ ).option("--dir <path>", "Project root containing `module.json` (default: current directory).").option(
25867
+ "--install <id>",
25868
+ "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.",
25869
+ parseIntOption
25870
+ ).action(async (cmdArgs, cmdOpts) => {
24700
25871
  const { runCommand: runCommand2 } = await Promise.resolve().then(() => (init_run(), run_exports));
24701
25872
  process.exitCode = await runCommand2(cmdArgs ?? [], {
24702
- ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
25873
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
25874
+ ...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {}
24703
25875
  });
24704
25876
  });
24705
25877
  program.command("pull").description(
@@ -24826,13 +25998,17 @@ envCmd.command("export").description(
24826
25998
  ).option(
24827
25999
  "--key <dwk_value>",
24828
26000
  "Use a previously-minted API key (paste plaintext). (Mutually exclusive with --new-key.)"
26001
+ ).option(
26002
+ "--with-secret-names",
26003
+ "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
26004
  ).action(
24830
26005
  async (cmdOpts) => {
24831
26006
  process.exitCode = await envExportCommand({
24832
26007
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
24833
26008
  ...cmdOpts.format !== void 0 ? { format: cmdOpts.format } : {},
24834
26009
  ...cmdOpts.newKey !== void 0 ? { newKey: cmdOpts.newKey } : {},
24835
- ...cmdOpts.key !== void 0 ? { key: cmdOpts.key } : {}
26010
+ ...cmdOpts.key !== void 0 ? { key: cmdOpts.key } : {},
26011
+ ...cmdOpts.withSecretNames ? { withSecretNames: true } : {}
24836
26012
  });
24837
26013
  }
24838
26014
  );
@@ -24858,7 +26034,10 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
24858
26034
  ).option(
24859
26035
  "--timeout <seconds>",
24860
26036
  "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(
26037
+ ).option("--json", "Emit the final build result as machine-readable JSON.").option(
26038
+ "--strict",
26039
+ "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."
26040
+ ).action(
24862
26041
  async (cmdOpts) => {
24863
26042
  const timeoutSec = cmdOpts.timeout !== void 0 ? Number(cmdOpts.timeout) : void 0;
24864
26043
  process.exitCode = await modulePublishCommand({
@@ -24874,19 +26053,23 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
24874
26053
  // commander maps `--no-wait` to `wait === false`.
24875
26054
  ...cmdOpts.wait === false ? { noWait: true } : {},
24876
26055
  ...timeoutSec !== void 0 && Number.isFinite(timeoutSec) ? { timeoutSec } : {},
24877
- ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26056
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
26057
+ ...cmdOpts.strict !== void 0 ? { strict: cmdOpts.strict } : {}
24878
26058
  });
24879
26059
  }
24880
26060
  );
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.'
26061
+ moduleCmd.command("install [slug]").alias("deploy").alias("upgrade").description(
26062
+ "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
26063
  ).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
26064
  "--keep-dev-install",
24885
26065
  "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
26066
  ).option(
24887
26067
  "--replace-dev-install",
24888
26068
  "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(
26069
+ ).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(
26070
+ "--accept-data-loss <drift_hash>",
26071
+ "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)."
26072
+ ).option("--json", "Emit the final result as machine-readable JSON.").action(
24890
26073
  async (slug, cmdOpts) => {
24891
26074
  process.exitCode = await moduleInstallCommand(slug, {
24892
26075
  ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
@@ -24896,6 +26079,7 @@ moduleCmd.command("install [slug]").alias("deploy").description(
24896
26079
  ...cmdOpts.replaceDevInstall !== void 0 ? { replaceDevInstall: cmdOpts.replaceDevInstall } : {},
24897
26080
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
24898
26081
  ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
26082
+ ...cmdOpts.acceptDataLoss !== void 0 ? { acceptDataLoss: cmdOpts.acceptDataLoss } : {},
24899
26083
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
24900
26084
  });
24901
26085
  }
@@ -24915,6 +26099,63 @@ moduleCmd.command("uninstall [slug]").description(
24915
26099
  });
24916
26100
  }
24917
26101
  );
26102
+ var invitesCmd = moduleCmd.command("invites").description(
26103
+ "Install invitations: send, list and withdraw them for your module, or see and decline the ones your company received."
26104
+ );
26105
+ var inviteIdArg = (value) => value !== void 0 && /^[1-9][0-9]*$/.test(value) ? Number(value) : void 0;
26106
+ invitesCmd.command("list <slug>").description("List the open install invitations for your module.").option("--json", "Emit machine-readable JSON.").action(async (slug, cmdOpts) => {
26107
+ process.exitCode = await moduleInvitesListCommand(slug, {
26108
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26109
+ });
26110
+ });
26111
+ invitesCmd.command("send <slug>").description(
26112
+ "Invite another company to install your module. Needs admin rights in the module's company. Asks before sending; pass --yes in scripts."
26113
+ ).option("--to <company>", "The company id to invite, or the slug of a company you belong to.").option("--note <text>", "A short note shown to the invited company (max 1000 characters).").option("-y, --yes", "Send without asking (required in non-interactive or --json mode).").option("--json", "Emit machine-readable JSON.").action(
26114
+ async (slug, cmdOpts) => {
26115
+ process.exitCode = await moduleInvitesSendCommand(slug, {
26116
+ ...cmdOpts.to !== void 0 ? { to: cmdOpts.to } : {},
26117
+ ...cmdOpts.note !== void 0 ? { note: cmdOpts.note } : {},
26118
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
26119
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26120
+ });
26121
+ }
26122
+ );
26123
+ invitesCmd.command("revoke <slug> <invite-id>").description(
26124
+ "Withdraw an install invitation. Installations made before stay. Asks first; pass --yes in scripts."
26125
+ ).option("-y, --yes", "Withdraw without asking (required in non-interactive or --json mode).").option("--json", "Emit machine-readable JSON.").action(async (slug, inviteId, cmdOpts) => {
26126
+ process.exitCode = await moduleInvitesRevokeCommand(slug, inviteIdArg(inviteId), {
26127
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
26128
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26129
+ });
26130
+ });
26131
+ invitesCmd.command("received").description("List the open install invitations your company received (company admins).").option("--workspace <slug-or-id>", "Company (default: config default or interactive picker).").option("--json", "Emit machine-readable JSON.").action(async (cmdOpts) => {
26132
+ process.exitCode = await moduleInvitesReceivedCommand({
26133
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
26134
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26135
+ });
26136
+ });
26137
+ invitesCmd.command("decline <invite-id>").description(
26138
+ "Decline an install invitation your company received (company admins). Asks first; pass --yes in scripts."
26139
+ ).option("--workspace <slug-or-id>", "Company (default: config default or interactive picker).").option("-y, --yes", "Decline without asking (required in non-interactive or --json mode).").option("--json", "Emit machine-readable JSON.").action(
26140
+ async (inviteId, cmdOpts) => {
26141
+ process.exitCode = await moduleInvitesDeclineCommand(inviteIdArg(inviteId), {
26142
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
26143
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
26144
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26145
+ });
26146
+ }
26147
+ );
26148
+ moduleCmd.command("installations [slug]").description(
26149
+ "List the installations in a company that you can see: id, module, name, audience, version, held state and public address. With a slug, only that module. Read-only."
26150
+ ).option("--workspace <slug-or-id>", "Company (default: cached module company, config default, or interactive picker).").option("--dev", "Include development installations.").option("--json", "Emit machine-readable JSON.").action(
26151
+ async (slug, cmdOpts) => {
26152
+ process.exitCode = await moduleInstallationsCommand(slug, {
26153
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
26154
+ ...cmdOpts.dev !== void 0 ? { dev: cmdOpts.dev } : {},
26155
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26156
+ });
26157
+ }
26158
+ );
24918
26159
  moduleCmd.command("status [slug]").description(
24919
26160
  "Show a module's deploy state in a workspace: install kind, installed vs latest published version, runtime provisioning, and the public URL."
24920
26161
  ).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("--json", "Emit machine-readable JSON.").action(
@@ -25192,7 +26433,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
25192
26433
  }
25193
26434
  );
25194
26435
  function getVersion() {
25195
- return "0.25.0";
26436
+ return "0.27.0";
25196
26437
  }
25197
26438
  function parseIntOption(value) {
25198
26439
  const n = Number.parseInt(value, 10);