@dashai/cli 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -112,6 +112,12 @@ function resolveApiUrl() {
112
112
  if (cfg?.apiUrl) return cfg.apiUrl.replace(/\/+$/, "");
113
113
  return DEFAULT_API_URL;
114
114
  }
115
+ function resolveExplicitApiUrl() {
116
+ const fromEnv = process.env.DASHWISE_API_URL?.trim();
117
+ if (fromEnv) return fromEnv.replace(/\/+$/, "");
118
+ const cfg = safeReadConfig();
119
+ return cfg?.apiUrl ? cfg.apiUrl.replace(/\/+$/, "") : null;
120
+ }
115
121
  function readSavedToken() {
116
122
  const cfg = safeReadConfig();
117
123
  return cfg?.token ?? null;
@@ -822,6 +828,74 @@ var init_connect_status = __esm({
822
828
  SETUP_SETTINGS_PATH = "Modules \u2192 Installed modules \u2192 Technical \u2192 Data connections";
823
829
  }
824
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
+ });
825
899
  function manifestPath(dir) {
826
900
  return join(resolve(dir ?? process.cwd()), "module.json");
827
901
  }
@@ -1166,6 +1240,12 @@ async function devCommand(options) {
1166
1240
  (Ensure you're logged into the right backend: ${code(`dashwise login --api-url ${apiUrl}`)}.)`
1167
1241
  );
1168
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
+ }
1169
1249
  info("");
1170
1250
  if (moduleEntry !== null && manifestSlug !== void 0) {
1171
1251
  await reportConnectStatus({
@@ -1406,13 +1486,13 @@ async function resolveDevPort(requested) {
1406
1486
  }
1407
1487
  async function isPortFree(port) {
1408
1488
  const { createServer } = await import('net');
1409
- return new Promise((resolve15) => {
1489
+ return new Promise((resolve16) => {
1410
1490
  const server = createServer();
1411
1491
  server.once("error", () => {
1412
- resolve15(false);
1492
+ resolve16(false);
1413
1493
  });
1414
1494
  server.once("listening", () => {
1415
- server.close(() => resolve15(true));
1495
+ server.close(() => resolve16(true));
1416
1496
  });
1417
1497
  server.listen(port, "0.0.0.0");
1418
1498
  });
@@ -1480,6 +1560,7 @@ var init_dev = __esm({
1480
1560
  init_api_client();
1481
1561
  init_connect_status();
1482
1562
  init_config();
1563
+ init_declared_secrets();
1483
1564
  init_manifest_io();
1484
1565
  init_manifest_poll();
1485
1566
  init_output();
@@ -1634,7 +1715,7 @@ var init_simple = __esm({
1634
1715
  typescript: "^5.5.0"
1635
1716
  },
1636
1717
  engines: {
1637
- node: "22.x"
1718
+ node: "22.x || 24.x"
1638
1719
  }
1639
1720
  },
1640
1721
  "node_modules/@dashai/sdk": {
@@ -2679,7 +2760,7 @@ var init_custom = __esm({
2679
2760
  typescript: "^5.5.0"
2680
2761
  },
2681
2762
  engines: {
2682
- node: "22.x"
2763
+ node: "22.x || 24.x"
2683
2764
  }
2684
2765
  },
2685
2766
  "node_modules/@alloc/quick-lru": {
@@ -10017,9 +10098,10 @@ function packageJson(opts) {
10017
10098
  version: "0.0.1",
10018
10099
  private: true,
10019
10100
  type: "module",
10020
- engines: { node: "22.x" },
10101
+ engines: { node: NODE_ENGINES },
10021
10102
  scripts: {
10022
- dev: "dashwise module dev",
10103
+ dev: "dashwise dev",
10104
+ "dev:watch": "dashwise module dev",
10023
10105
  "dev:next": "next dev",
10024
10106
  "dashwise:generate": "dashwise module generate",
10025
10107
  typecheck: "tsc --noEmit",
@@ -10235,7 +10317,7 @@ function appPageTsx(opts) {
10235
10317
  Module slug: <code>${escapeHtml(opts.slug)}</code>
10236
10318
  </p>
10237
10319
  <p style={{ marginTop: 24, color: '#64748b', fontSize: 14 }}>
10238
- Run <code>dashwise module dev</code> to start the watch loop.
10320
+ Run <code>npm run dev</code> to start the app and the watch loop.
10239
10321
  <br />
10240
10322
  Edit <code>module.json</code> to add tables; regenerated types
10241
10323
  land in <code>@dashai/generated</code>.
@@ -10269,7 +10351,7 @@ debugger to Next), use the legacy two-terminal flow:
10269
10351
 
10270
10352
  \`\`\`bash
10271
10353
  dashwise module generate # produces node_modules/@dashai/generated/
10272
- dashwise module dev # watch module.json + auto-regenerate
10354
+ npm run dev:watch # watch module.json + auto-regenerate
10273
10355
  npm run dev:next # plain Next.js dev server (in another terminal)
10274
10356
  \`\`\`
10275
10357
 
@@ -10314,7 +10396,7 @@ debugger to Next), use the legacy two-terminal flow:
10314
10396
 
10315
10397
  \`\`\`bash
10316
10398
  dashwise module generate # produces node_modules/@dashai/generated/
10317
- dashwise module dev # watch + regen on module.json save (terminal 1)
10399
+ npm run dev:watch # watch + regen on module.json save (terminal 1)
10318
10400
  npm run dev:next # next dev on :3000 (terminal 2)
10319
10401
  \`\`\`
10320
10402
 
@@ -11119,9 +11201,10 @@ function packageJsonCustom(opts) {
11119
11201
  version: "0.0.1",
11120
11202
  private: true,
11121
11203
  type: "module",
11122
- engines: { node: "22.x" },
11204
+ engines: { node: NODE_ENGINES },
11123
11205
  scripts: {
11124
- dev: "dashwise module dev",
11206
+ dev: "dashwise dev",
11207
+ "dev:watch": "dashwise module dev",
11125
11208
  "dev:next": "next dev",
11126
11209
  "dashwise:generate": "dashwise module generate",
11127
11210
  typecheck: "tsc --noEmit",
@@ -12208,7 +12291,7 @@ in this Dashwise module. Read this before making changes.
12208
12291
 
12209
12292
  Every table declares exactly one primary field (the row's name everywhere in Dashwise).
12210
12293
 
12211
- ${opts.cloudReady === false ? "" : cloudAgentInstructions()}
12294
+ ${opts.cloudReady === false ? "" : cloudAgentInstructions() + "\n" + claudeCloudNote(opts.apiUrl)}
12212
12295
  ## What this module is
12213
12296
 
12214
12297
  A Dashwise ${isCustom ? "**custom Next.js** module (full app runtime, machine-class)" : "**hand-authored simple** module (isolate runtime)"} authored against \`@dashai/sdk\`.
@@ -12587,9 +12670,14 @@ dashwise module destroy-dev-install # tear down when done (cleans backend + aut
12587
12670
  - **No untyped \`any\`.** The generated SDK gives you types \u2014 use
12588
12671
  them. If you need an escape hatch, add a \`@ts-expect-error\` with
12589
12672
  a comment explaining why.
12590
- - **No secrets in code or env files.** Use \`runtime.secrets[]\` in
12591
- \`module.json\`; values are encrypted at rest and resolved at
12592
- 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.
12593
12681
 
12594
12682
  ## Pointers
12595
12683
 
@@ -12745,15 +12833,145 @@ primary_region = "iad"
12745
12833
  `;
12746
12834
  }
12747
12835
  function cloudAgentInstructions() {
12748
- return `## Cloud workflow
12836
+ return `## Default way of working
12837
+
12838
+ These are defaults. When the person you work for says otherwise (in the chat,
12839
+ or in their own \`CLAUDE.md\` / \`AGENTS.md\`), follow them instead.
12840
+
12841
+ - Assume the person is not a developer. Before and after each step, say in one
12842
+ or two plain sentences what you are doing and why, for example "I'm saving
12843
+ your changes so Dashwise can see them." When a term like branch or pull
12844
+ request is unavoidable, explain it in a few words.
12845
+ - You handle git. Get the finished work onto the repository's default branch
12846
+ (usually \`main\`) yourself, so the person, Dashwise and the next agent all
12847
+ see the latest version. The person needs no git, branch or pull request
12848
+ knowledge:
12849
+ - Working locally on the person's computer: commit on the default branch and push it.
12850
+ - Working in the cloud (Codex cloud, Claude Code on the web): if your tool
12851
+ lets you push to the default branch or merge your own pull request, do it
12852
+ once the checks pass. If it only lets you push a branch, push it and tell the
12853
+ person in plain words that the changes are saved and that Release in
12854
+ Dashwise includes them. Don't ask them to merge anything.
12855
+ - Never force-push and never rewrite the default branch's history.
12856
+ - The person only confirms Release and Take live in Dashwise. When the work is
12857
+ pushed, tell them it is ready: in Dashwise open the module, click Release,
12858
+ then Take live.
12859
+
12860
+ ## Cloud workflow
12749
12861
 
12750
12862
  - Read \`module.json\`, the task and \`spec.md\` first. Preserve module identity and permission checks.
12751
- - Use Node 22 in the provider environment (\`.nvmrc\` / \`.node-version\`). From the repository root run \`bash scripts/cloud-setup.sh\`.
12863
+ - Use Node 22 or 24 (\`.nvmrc\` prefers 22; Codex's default 24 works too). From the repository root run \`bash scripts/cloud-setup.sh\`.
12752
12864
  - After edits run \`npm run typecheck\`, \`npm test\`, \`npm run build\`, then \`npm run smoke:production\`. Add behavior tests for your changes. A dev server does not prove production readiness.
12753
12865
  - Update \`spec.md\` for behavior changes and \`agent-log.md\` in the same commit. Read \`CLAUDE.md\` for SDK, authorization and UI conventions.
12754
- - Work on one branch and push it. Report the pushed SHA and checks, including failures. No user merge is required for Dashwise to receive the branch.
12755
- - 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.
12866
+ - \`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.
12867
+ - Report what you changed, the pushed SHA and the checks you ran, including failures, in plain words.
12868
+ - 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.
12756
12869
  - Never automatically release, publish, deploy or take an installation live. Release and Live require an explicit human action.
12870
+ - 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.
12871
+
12872
+ ## If you can't see the repository
12873
+
12874
+ Connecting GitHub in the AI tool is not enough for a private repository. Tell
12875
+ the person, in plain words, to also install the tool's GitHub app on the GitHub
12876
+ organisation that owns this repository and give it access to this repository
12877
+ (an organisation owner may have to approve it), then start the task again:
12878
+
12879
+ - Claude: the Claude GitHub app, https://github.com/apps/claude
12880
+ - Codex: the ChatGPT Codex Connector, https://github.com/apps/chatgpt-codex-connector
12881
+
12882
+ ## Dashwise login (only for full development)
12883
+
12884
+ The checks (\`npm run typecheck\`, \`npm test\`, \`npm run build\`,
12885
+ \`npm run smoke:production\`) need no login. Full Dashwise development does:
12886
+ \`npm run dev\` (\`dashwise dev\`: the app plus the watcher), \`dashwise module
12887
+ generate\` and the schema commands. When the task needs them:
12888
+
12889
+ - If \`dashwise\` is not installed, run \`npm install -g @dashai/cli@latest\`.
12890
+ - Ask the person once, in plain words, for example: "To run your module
12891
+ against Dashwise I need to sign in as you. I'll show you a code and a link;
12892
+ open the link and approve." Then run \`dashwise login --no-prompt\` and show
12893
+ them the code and link it prints. The login acts with the person's own rights.
12894
+ - Keep the login outside the repository. In the cloud, run CLI commands with
12895
+ \`XDG_CONFIG_HOME=/tmp/dashwise-config\` so the saved login lands in
12896
+ \`/tmp\`, never in the checkout.
12897
+ - Never write a token into a script, a file in the repository, a commit, an
12898
+ environment setup script or snapshot, or a log.
12899
+ - In the cloud, run CLI commands with \`NODE_USE_ENV_PROXY=1\` so the CLI
12900
+ uses the environment's network proxy (newer CLI versions do this on their own).
12901
+ - A cloud environment forgets the login when it restarts; ask again only when
12902
+ a command reports that the login is missing.
12903
+ `;
12904
+ }
12905
+ function companyApiHost(apiUrl) {
12906
+ if (!apiUrl) return null;
12907
+ try {
12908
+ const host = new URL(apiUrl).host;
12909
+ return host in HOSTED_API_HOSTS ? host : null;
12910
+ } catch {
12911
+ return null;
12912
+ }
12913
+ }
12914
+ function apiHostLine(apiUrl) {
12915
+ const host = companyApiHost(apiUrl);
12916
+ return host ? `\`${host}\` (this company's Dashwise API; the company uses Dashwise at ${HOSTED_API_HOSTS[host]})` : "the company's Dashwise API host: `api.dashwise.net` if the person opens Dashwise at app.dashwise.net, `api.dashwise.dev` if at app.dashwise.dev. If you can't tell, ask them which address they use";
12917
+ }
12918
+ function codexEnvironmentInstructions(apiUrl) {
12919
+ const host = companyApiHost(apiUrl);
12920
+ const apiUrlValue = host ? `https://${host}` : "https://<the company API host above>";
12921
+ return `## Codex cloud environment
12922
+
12923
+ When the person asks you to set up the Codex cloud environment for this
12924
+ repository, use exactly these settings. Show each one to the person and say in
12925
+ one sentence what it is for.
12926
+
12927
+ | Setting | Value |
12928
+ |---|---|
12929
+ | Node.js | 22 or 24 (24 is Codex's default and works; \`.nvmrc\` prefers 22) |
12930
+ | Setup script | \`if [ -f scripts/cloud-setup.sh ]; then bash scripts/cloud-setup.sh; else echo "No Dashwise template yet, setup skipped"; fi\` |
12931
+ | Agent internet access | On: keep the "package managers" preset and add only the company's Dashwise API host (below) |
12932
+ | Environment variables | \`DASHWISE_API_URL=${apiUrlValue}\`, \`NODE_USE_ENV_PROXY=1\` |
12933
+ | Secrets | none |
12934
+
12935
+ Set the network access before you start the setup, so the first run can
12936
+ already install packages. The preset covers npm and GitHub; the one added
12937
+ domain is:
12938
+
12939
+ - ${apiHostLine(apiUrl)}
12940
+
12941
+ The module and the CLI talk to the company's Dashwise API. Do not add other
12942
+ Dashwise hosts. If the environment offers no preset, allow exactly
12943
+ ${CLOUD_ALLOWED_HOSTS.map((h) => `\`${h}\``).join(", ")} plus that host.
12944
+
12945
+ The setup script checks Node 22 or 24 and installs the exact dependencies with
12946
+ \`npm ci\`. The guard lets a first run pass while the repository holds only a
12947
+ README.
12948
+
12949
+ Never add secrets: no \`dwk_\` module key, no \`dwc_\` CLI token and no password,
12950
+ and never put a \`dashwise login\` into the setup script. Delivering code needs
12951
+ none of them; for full development see "Dashwise login".
12952
+
12953
+ To check the environment, run \`npm run typecheck\`, \`npm test\`,
12954
+ \`npm run build\` and \`npm run smoke:production\`. If one fails because of a
12955
+ setting, fix the setting; otherwise report the failure in plain words.
12956
+
12957
+ If the repository has no \`package.json\` yet (only a README), skip these
12958
+ checks: the setup script already skipped installing, and the first task adds
12959
+ the template. Tell the person the settings are ready.
12960
+
12961
+ When the checks pass, or were skipped for a README-only repository, tell the
12962
+ person: "The environment works. Click Publish to save it for your next tasks."
12963
+ Publishing is their click.
12964
+ `;
12965
+ }
12966
+ function claudeCloudNote(apiUrl) {
12967
+ const host = companyApiHost(apiUrl);
12968
+ return `## Claude cloud
12969
+
12970
+ Claude Code on the web has no environment setup chat. It reads this repository
12971
+ and \`CLAUDE.md\`, and \`.claude/settings.json\` runs \`scripts/cloud-setup.sh\`
12972
+ when a cloud session starts. Node 22 or 24 both work. If
12973
+ the environment has a network setting, allow the same domains as for Codex:
12974
+ ${CLOUD_ALLOWED_HOSTS.map((h) => `\`${h}\``).join(", ")} and ${host ? `\`${host}\`` : "the company's Dashwise API host (`api.dashwise.net` for app.dashwise.net, `api.dashwise.dev` for app.dashwise.dev)"}. No secrets.
12757
12975
  `;
12758
12976
  }
12759
12977
  function agentsMd(opts) {
@@ -12761,7 +12979,7 @@ function agentsMd(opts) {
12761
12979
 
12762
12980
  Every table declares exactly one primary field (the row's name everywhere in Dashwise).
12763
12981
 
12764
- ` + cloudAgentInstructions();
12982
+ ` + cloudAgentInstructions() + "\n" + codexEnvironmentInstructions(opts.apiUrl) + "\n" + claudeCloudNote(opts.apiUrl);
12765
12983
  }
12766
12984
  function nodeVersion() {
12767
12985
  return "22\n";
@@ -12783,9 +13001,12 @@ fi
12783
13001
  repo_root="$(cd -- "$(dirname -- "\${BASH_SOURCE[0]}")/.." && pwd)"
12784
13002
  cd "$repo_root"
12785
13003
  fail() { printf 'cloud-setup: %s\\n' "$1" >&2; exit 1; }
12786
- command -v node >/dev/null 2>&1 || fail 'Node 22 is required. Select Node 22 in your cloud environment, then retry.'
12787
- [[ "$(node -p 'process.versions.node.split(".")[0]')" == "22" ]] || fail 'Node 22 is required. Select Node 22 in your cloud environment, then retry. A subshell-only runtime change will not persist into agent commands.'
12788
- command -v npm >/dev/null 2>&1 || fail 'npm is required with Node 22.'
13004
+ command -v node >/dev/null 2>&1 || fail 'Node 22 or 24 is required. Select Node 22 or 24 in your cloud environment, then retry.'
13005
+ case "$(node -p 'process.versions.node.split(".")[0]')" in
13006
+ 22|24) ;;
13007
+ *) fail 'Node 22 or 24 is required. Select Node 22 or 24 in your cloud environment, then retry. A subshell-only runtime change will not persist into agent commands.' ;;
13008
+ esac
13009
+ command -v npm >/dev/null 2>&1 || fail 'npm is required with Node 22 or 24.'
12789
13010
  [[ -f package-lock.json && -f package.json ]] || fail 'Missing package.json or committed package-lock.json. Run this from a complete repository checkout.'
12790
13011
 
12791
13012
  # Only the Claude startup/resume path reuses an unchanged install. Explicit
@@ -12793,15 +13014,21 @@ command -v npm >/dev/null 2>&1 || fail 'npm is required with Node 22.'
12793
13014
  stamp="$(node --input-type=module -e 'import {createHash} from "node:crypto"; import {readFileSync} from "node:fs"; const hash=createHash("sha256"); for (const path of ["package.json", "package-lock.json", "scripts/cloud-setup.sh"]) hash.update(readFileSync(path)); hash.update(process.version); hash.update(process.platform); hash.update(process.arch); process.stdout.write(hash.digest("hex"));')"
12794
13015
  stamp_file="node_modules/.dashwise-cloud-setup"
12795
13016
  if [[ "\${1:-}" == "--claude" && -f "$stamp_file" && "$(cat "$stamp_file")" == "$stamp" && -x node_modules/.bin/next && -x node_modules/.bin/tsc && -f node_modules/@dashai/sdk/package.json ]]; then
12796
- printf 'cloud-setup: dependencies already match Node 22 and the lockfile.\\n'
13017
+ printf 'cloud-setup: dependencies already match this Node version and the lockfile.\\n'
12797
13018
  exit 0
12798
13019
  fi
12799
13020
  # Keep install output out of session logs: third-party lifecycle scripts may
12800
13021
  # echo environment variables. npm's own detailed logs stay in its local cache.
12801
13022
  rm -f "$stamp_file"
12802
- npm ci --include=dev --no-audit --no-fund > /dev/null 2>&1 || fail 'npm ci failed. Check the committed lockfile, Node 22 and network access to registry.npmjs.org. Review npm logs privately; never paste credentials.'
13023
+ # Some sandboxes mount HOME read-only; npm then cannot write its cache.
13024
+ npm_cache="\${npm_config_cache:-\${HOME:-}/.npm}"
13025
+ if ! { mkdir -p "$npm_cache" 2>/dev/null && [[ -w "$npm_cache" ]]; }; then
13026
+ export npm_config_cache="\${TMPDIR:-/tmp}/npm-cache"
13027
+ printf 'cloud-setup: %s is not writable, using %s for the npm cache.\\n' "$npm_cache" "$npm_config_cache"
13028
+ fi
13029
+ npm ci --include=dev --no-audit --no-fund > /dev/null 2>&1 || fail 'npm ci failed. Check the committed lockfile, Node 22 or 24 and network access to registry.npmjs.org. Review npm logs privately; never paste credentials.'
12803
13030
  printf '%s' "$stamp" > "$stamp_file"
12804
- printf 'cloud-setup: dependencies installed with Node 22.\\n'
13031
+ printf 'cloud-setup: dependencies installed with Node %s.\\n' "$(node -p 'process.versions.node')"
12805
13032
  `;
12806
13033
  }
12807
13034
  function claudeSettingsJson() {
@@ -12957,7 +13184,7 @@ function cloudReadme() {
12957
13184
  return `
12958
13185
  ## Cloud builders
12959
13186
 
12960
- Use Node 22 (the root runtime pins) and run \`bash scripts/cloud-setup.sh\`.
13187
+ Use Node 22 (the root runtime pins; 24 also works) and run \`bash scripts/cloud-setup.sh\`.
12961
13188
  Commit \`package-lock.json\` after any dependency change; setup uses \`npm ci\`.
12962
13189
  Checks: \`npm run typecheck\`, \`npm test\`, \`npm run build\`, \`npm run smoke:production\`.
12963
13190
  The smoke command starts the built server, probes anonymous API denial and stops
@@ -12966,33 +13193,49 @@ The example uses \`lib/items.ts\` directly against the SDK, so a fresh checkout
12966
13193
  can build without authenticated code generation. Keep it aligned with the
12967
13194
  example table, or replace it when you author your schema.
12968
13195
 
12969
- Codex: select Node 22 in the provider environment and use
12970
- \`bash scripts/cloud-setup.sh\` as setup/maintenance. GitHub authorization and
12971
- the cloud environment are configured in the provider; these repo files do
12972
- not create an environment or authorize your account.
13196
+ Codex: \`AGENTS.md\` \u2192 "Codex cloud environment" lists the exact settings
13197
+ (Node 22 or 24, setup script, network, \`DASHWISE_API_URL\`, no secrets);
13198
+ Codex's environment setup chat can follow it, then you click Publish. GitHub
13199
+ authorization is configured in the provider: install the ChatGPT Codex
13200
+ Connector on your GitHub organisation so Codex can see a private repository.
12973
13201
 
12974
13202
  Claude Code web: \`.claude/settings.json\` ships a cloud-only SessionStart
12975
13203
  hook for startup/resume. It calls \`bash "$CLAUDE_PROJECT_DIR"/scripts/cloud-setup.sh --claude\`;
12976
13204
  local sessions skip installation and unchanged cloud installs reuse dependencies.
12977
- Select Node 22 in the environment; do not rely on a setup subshell changing the
13205
+ Select Node 22 or 24 in the environment; do not rely on a setup subshell changing the
12978
13206
  runtime of later agent commands. The same script can be used in the environment
12979
13207
  setup field. Allow \`registry.npmjs.org\` for installs (Claude's Trusted list).
12980
13208
  The [official hook recipe](https://code.claude.com/docs/en/cloud-environments#install-dependencies-with-a-sessionstart-hook)
12981
13209
  explains startup and cache behavior.
12982
13210
 
12983
- Read \`AGENTS.md\` / \`CLAUDE.md\`, work on one branch and push. Release and Live
12984
- remain human actions. No Dashwise token is needed for repository code delivery.
13211
+ Read \`AGENTS.md\` / \`CLAUDE.md\`. By default agents get the work onto the
13212
+ default branch themselves and explain each step in plain words; Release in
13213
+ Dashwise includes work an agent could only push to a branch. Release and Live
13214
+ remain human actions. No Dashwise token is needed for
13215
+ repository code delivery.
12985
13216
  `;
12986
13217
  }
12987
- var DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL, MODULE_SLUG_REGEX;
13218
+ var NODE_ENGINES, DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL, MODULE_SLUG_REGEX, CLOUD_ALLOWED_HOSTS, HOSTED_API_HOSTS;
12988
13219
  var init_scaffold = __esm({
12989
13220
  "src/lib/scaffold.ts"() {
12990
13221
  init_sdk_version();
12991
13222
  init_simple();
12992
13223
  init_custom();
13224
+ NODE_ENGINES = "22.x || 24.x";
12993
13225
  DEPLOY_TARGETS = ["railway", "vercel", "fly"];
12994
13226
  SCAFFOLD_DEFAULT_API_URL = "http://localhost:3000";
12995
13227
  MODULE_SLUG_REGEX = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
13228
+ CLOUD_ALLOWED_HOSTS = [
13229
+ "registry.npmjs.org",
13230
+ "github.com",
13231
+ "api.github.com",
13232
+ "raw.githubusercontent.com",
13233
+ "codeload.github.com"
13234
+ ];
13235
+ HOSTED_API_HOSTS = {
13236
+ "api.dashwise.dev": "app.dashwise.dev",
13237
+ "api.dashwise.net": "app.dashwise.net"
13238
+ };
12996
13239
  }
12997
13240
  });
12998
13241
  function isFreshCloneEntry(name) {
@@ -13070,7 +13313,7 @@ function canAdoptCloudFiles(dir, templatePackage) {
13070
13313
  if (lock.lockfileVersion !== 2 && lock.lockfileVersion !== 3 || !root) {
13071
13314
  return false;
13072
13315
  }
13073
- if (pkg.engines?.node !== "22.x" || !pkg.scripts || !expected.scripts) {
13316
+ if (pkg.engines?.node !== "22.x" && pkg.engines?.node !== expected.engines?.node || !pkg.scripts || !expected.scripts) {
13074
13317
  return false;
13075
13318
  }
13076
13319
  for (const script of ["typecheck", "test", "build", "start", "smoke:production"]) {
@@ -13173,12 +13416,14 @@ async function moduleInitCommand(slugArg, options) {
13173
13416
  const templatePackage = (kind === "custom" ? packageJsonCustom : packageJson)({
13174
13417
  slug});
13175
13418
  const cloudReady = writeMode === "all" || canAdoptCloudFiles(dir, templatePackage);
13419
+ const companyApiUrl = resolveExplicitApiUrl();
13176
13420
  const scaffoldOpts = {
13177
13421
  cloudReady,
13178
13422
  slug,
13179
13423
  name,
13180
13424
  kind,
13181
- ...options.git !== void 0 ? { repositoryUrl: options.git } : {}
13425
+ ...options.git !== void 0 ? { repositoryUrl: options.git } : {},
13426
+ ...companyApiUrl !== null ? { apiUrl: companyApiUrl } : {}
13182
13427
  };
13183
13428
  const scaffoldApiUrl = resolveApiUrl();
13184
13429
  const files = [
@@ -13303,6 +13548,9 @@ async function moduleInitCommand(slugArg, options) {
13303
13548
  writeFileSync(absPath, content, "utf-8");
13304
13549
  written++;
13305
13550
  }
13551
+ reportSecretLines(
13552
+ syncEnvLocalSecretLines(dir, declaredSecretNames(readManifest(dir)))
13553
+ );
13306
13554
  }
13307
13555
  if (writeMode !== "none" && !cloudReady) {
13308
13556
  process.stdout.write(
@@ -13376,7 +13624,7 @@ async function moduleInitCommand(slugArg, options) {
13376
13624
 
13377
13625
  ${dim(" (or, for the two-terminal flow:")}
13378
13626
  ${dim(" dashwise module generate")} ${dim("# materialize typed wrappers")}
13379
- ${dim(" dashwise module dev")} ${dim("# watch + auto-regenerate")}
13627
+ ${dim(" npm run dev:watch")} ${dim("# watch + auto-regenerate")}
13380
13628
  ${dim(" npm run dev:next # next dev on :3000)")}
13381
13629
  ${devInstall ? `
13382
13630
  ${dim(`Dev installation id=${devInstall.installation.id}; API key expires ${devInstall.api_key_expires_at}.`)}
@@ -13545,10 +13793,297 @@ var init_init = __esm({
13545
13793
  init_output();
13546
13794
  init_api_client();
13547
13795
  init_config();
13796
+ init_declared_secrets();
13548
13797
  init_manifest_io();
13549
13798
  init_workspaces();
13550
13799
  }
13551
13800
  });
13801
+
13802
+ // src/lib/manifest-evolution.ts
13803
+ function compareManifests(base, head, history = []) {
13804
+ const violations = [];
13805
+ const additions = [];
13806
+ const warnings = [];
13807
+ const v = (rule, where, detail) => violations.push({ rule, where, detail });
13808
+ const baseTables = tablesOf(base);
13809
+ const headTables = tablesOf(head);
13810
+ const everTables = /* @__PURE__ */ new Set();
13811
+ const everFields = /* @__PURE__ */ new Set();
13812
+ for (const m of history) {
13813
+ for (const [slug, t] of tablesOf(m)) {
13814
+ everTables.add(slug);
13815
+ for (const f of t.fields ?? []) everFields.add(`${slug}.${f.slug}`);
13816
+ }
13817
+ }
13818
+ for (const [slug, bt] of baseTables) {
13819
+ const ht = headTables.get(slug);
13820
+ if (!ht) {
13821
+ v("removed_table", slug, "tables are never removed; deprecate instead");
13822
+ continue;
13823
+ }
13824
+ const bf = fieldsOf(bt);
13825
+ const hf = fieldsOf(ht);
13826
+ for (const [fs, f] of bf) {
13827
+ const g = hf.get(fs);
13828
+ const where = `${slug}.${fs}`;
13829
+ if (!g) {
13830
+ v("removed_field", where, "fields are never removed; stop writing it and set human_edit: open");
13831
+ continue;
13832
+ }
13833
+ if (f.type !== g.type) v("retyped_field", where, `${f.type} \u2192 ${g.type}`);
13834
+ if ((f.decimal_places ?? null) !== (g.decimal_places ?? null)) {
13835
+ v("retyped_field", where, `decimal_places ${f.decimal_places ?? "none"} \u2192 ${g.decimal_places ?? "none"}`);
13836
+ }
13837
+ if ((f.target ?? null) !== (g.target ?? null)) v("retyped_field", where, `link target ${f.target} \u2192 ${g.target}`);
13838
+ if (f.required !== true && g.required === true) {
13839
+ v("required_tightened", where, "old rows are empty; enforce it in the tool layer instead");
13840
+ }
13841
+ const bo = f.options ?? [];
13842
+ const ho = g.options ?? [];
13843
+ for (const [i, o] of bo.entries()) {
13844
+ if (!ho.includes(o)) v("removed_option", where, `option "${String(o)}" removed`);
13845
+ else if (ho[i] !== o) {
13846
+ v("reordered_option", where, `option "${String(o)}" moved from ${i} to ${ho.indexOf(o)}; options are append-only`);
13847
+ }
13848
+ }
13849
+ for (const o of ho.slice(bo.length)) if (!bo.includes(o)) additions.push(`option ${where} "${String(o)}"`);
13850
+ }
13851
+ for (const [fs] of hf) {
13852
+ if (bf.has(fs)) continue;
13853
+ if (everFields.has(`${slug}.${fs}`)) {
13854
+ v("slug_reused", `${slug}.${fs}`, "this slug was removed in an earlier version and may not come back");
13855
+ } else additions.push(`field ${slug}.${fs}`);
13856
+ }
13857
+ const hu = new Set(uniqueIndexes(ht));
13858
+ for (const idx of uniqueIndexes(bt)) {
13859
+ if (!hu.has(idx)) v("dropped_unique_index", `${slug}(${idx})`, "a unique index is a protocol lock; it may not be dropped or changed");
13860
+ }
13861
+ const bu = new Set(uniqueIndexes(bt));
13862
+ for (const idx of hu) {
13863
+ if (bu.has(idx)) continue;
13864
+ const overExisting = idx.split(",").some((f) => f.length > 0 && bf.has(f));
13865
+ if (overExisting) {
13866
+ v("unique_on_populated", `${slug}(${idx})`, "a new unique index (indexes[].unique) over existing fields fails on duplicates already stored; add a new field instead");
13867
+ } else additions.push(`unique index ${slug}(${idx})`);
13868
+ }
13869
+ }
13870
+ for (const [slug] of headTables) {
13871
+ if (baseTables.has(slug)) continue;
13872
+ if (everTables.has(slug)) v("slug_reused", slug, "this table slug was removed in an earlier version and may not come back");
13873
+ else additions.push(`table ${slug}`);
13874
+ }
13875
+ const baseViews = new Set((base?.owns?.views ?? []).map((x) => x.slug));
13876
+ const headViews = new Set((head?.owns?.views ?? []).map((x) => x.slug));
13877
+ for (const s of baseViews) if (!headViews.has(s)) warnings.push(`view ${s} removed (callers of qbViews.${s} break)`);
13878
+ for (const s of headViews) if (!baseViews.has(s)) additions.push(`view ${s}`);
13879
+ return { violations, additions, warnings };
13880
+ }
13881
+ var tablesOf, fieldsOf, uniqueIndexes;
13882
+ var init_manifest_evolution = __esm({
13883
+ "src/lib/manifest-evolution.ts"() {
13884
+ tablesOf = (m) => new Map((m?.owns?.tables ?? []).map((t) => [t.slug, t]));
13885
+ fieldsOf = (t) => new Map((t?.fields ?? []).map((f) => [f.slug, f]));
13886
+ uniqueIndexes = (t) => (t?.indexes ?? []).filter((i) => i.unique === true).map((i) => (i.fields ?? []).join(","));
13887
+ }
13888
+ });
13889
+
13890
+ // src/commands/module/diff-check.ts
13891
+ var diff_check_exports = {};
13892
+ __export(diff_check_exports, {
13893
+ EvolutionCheckError: () => EvolutionCheckError,
13894
+ moduleDiffCheckCommand: () => moduleDiffCheckCommand,
13895
+ printEvolutionReport: () => printEvolutionReport,
13896
+ publishEvolutionGate: () => publishEvolutionGate,
13897
+ runEvolutionCheck: () => runEvolutionCheck
13898
+ });
13899
+ function readJson(path) {
13900
+ try {
13901
+ return JSON.parse(readFileSync(path, "utf8"));
13902
+ } catch (err) {
13903
+ throw new EvolutionCheckError(`could not read ${path}: ${err.message}`);
13904
+ }
13905
+ }
13906
+ function git(cwd, args) {
13907
+ try {
13908
+ return execFileSync("git", ["-C", cwd, ...args], {
13909
+ encoding: "utf8",
13910
+ stdio: ["ignore", "pipe", "pipe"],
13911
+ maxBuffer: 64 * 1024 * 1024
13912
+ });
13913
+ } catch (err) {
13914
+ const stderr = err.stderr?.trim();
13915
+ throw new EvolutionCheckError(`git ${args.join(" ")} failed${stderr ? `: ${stderr}` : ""}`);
13916
+ }
13917
+ }
13918
+ async function runEvolutionCheck(options) {
13919
+ const projectRoot = resolve(options.dir ?? process.cwd());
13920
+ const headPath = options.head ? resolve(options.head) : join(projectRoot, "module.json");
13921
+ if (!existsSync(headPath)) throw new EvolutionCheckError(`no manifest at ${headPath}`);
13922
+ const head = readJson(headPath);
13923
+ const baseFile = options.baseFile ?? (options.base && existsSync(options.base) ? options.base : void 0);
13924
+ if (baseFile) {
13925
+ const base = readJson(baseFile);
13926
+ return { ...compareManifests(base, head), base: baseFile, head: headPath, history: 0, nothingReleased: false };
13927
+ }
13928
+ if (options.base) {
13929
+ const ref = options.base;
13930
+ const base = JSON.parse(git(projectRoot, ["show", `${ref}:./module.json`]));
13931
+ let history2 = [];
13932
+ if (!options.noHistory) {
13933
+ const shas = git(projectRoot, ["log", "--format=%H", "--reverse", ref, "--", "module.json"]).split("\n").filter(Boolean);
13934
+ history2 = shas.slice(0, -1).map((sha) => JSON.parse(git(projectRoot, ["show", `${sha}:./module.json`])));
13935
+ }
13936
+ return {
13937
+ ...compareManifests(base, head, history2),
13938
+ base: `${ref}:module.json`,
13939
+ head: headPath,
13940
+ history: history2.length,
13941
+ nothingReleased: false
13942
+ };
13943
+ }
13944
+ const slug = head.module?.slug;
13945
+ if (!slug) throw new EvolutionCheckError(`${headPath} has no module.slug`);
13946
+ let mod;
13947
+ let versions;
13948
+ try {
13949
+ mod = await apiRequest(
13950
+ "GET",
13951
+ `/api/modules/by-slug/${encodeURIComponent(slug)}`
13952
+ );
13953
+ if (!mod.current_version) {
13954
+ return {
13955
+ violations: [],
13956
+ additions: [],
13957
+ warnings: [],
13958
+ base: "(nothing released)",
13959
+ head: headPath,
13960
+ history: 0,
13961
+ nothingReleased: true
13962
+ };
13963
+ }
13964
+ versions = await apiRequest("GET", `/api/modules/${mod.id}/versions`);
13965
+ } catch (err) {
13966
+ const why = err instanceof ApiError ? `${err.code || err.status} ${err.message}` : err.message;
13967
+ throw new EvolutionCheckError(
13968
+ `could not read the released manifest of ${slug} (${why}); pass --base <ref|file> to check offline`
13969
+ );
13970
+ }
13971
+ const released = versions.find((v) => v.version === mod.current_version);
13972
+ if (!released?.manifest_jsonb) {
13973
+ throw new EvolutionCheckError(
13974
+ `the platform returned no manifest for ${slug} v${mod.current_version}; pass --base <ref|file> to check offline`
13975
+ );
13976
+ }
13977
+ const releasedAt = released.created_on ? Date.parse(released.created_on) : Number.POSITIVE_INFINITY;
13978
+ const history = options.noHistory ? [] : versions.filter((v) => v !== released && v.manifest_jsonb && v.created_on && Date.parse(v.created_on) < releasedAt).sort((a, b) => Date.parse(a.created_on) - Date.parse(b.created_on)).map((v) => v.manifest_jsonb);
13979
+ return {
13980
+ ...compareManifests(released.manifest_jsonb, head, history),
13981
+ base: `${slug} v${mod.current_version} (released)`,
13982
+ head: headPath,
13983
+ history: history.length,
13984
+ nothingReleased: false
13985
+ };
13986
+ }
13987
+ function printEvolutionReport(r, print = out) {
13988
+ print(`module diff --check: ${r.head} against ${r.base} (${r.history} older version${r.history === 1 ? "" : "s"} scanned)`);
13989
+ for (const a of r.additions) print(` + ${a}`);
13990
+ for (const w of r.warnings) print(` ! ${w}`);
13991
+ for (const x of r.violations) print(` \u2717 ${x.rule}: ${x.where}: ${x.detail}`);
13992
+ }
13993
+ async function moduleDiffCheckCommand(options) {
13994
+ let r;
13995
+ try {
13996
+ r = await runEvolutionCheck(options);
13997
+ } catch (err) {
13998
+ if (err instanceof EvolutionCheckError) {
13999
+ if (options.json) out(JSON.stringify({ ok: false, error: { code: "CHECK_FAILED", message: err.message } }));
14000
+ else fail(`module diff --check: ${err.message}`);
14001
+ return 2;
14002
+ }
14003
+ throw err;
14004
+ }
14005
+ if (options.json) {
14006
+ out(JSON.stringify({ ok: r.violations.length === 0, ...r }, null, 2));
14007
+ return r.violations.length === 0 ? 0 : 1;
14008
+ }
14009
+ if (r.nothingReleased) {
14010
+ success("Nothing released yet: every change is additive.");
14011
+ return 0;
14012
+ }
14013
+ printEvolutionReport(r);
14014
+ if (r.violations.length === 0) {
14015
+ success("additive");
14016
+ return 0;
14017
+ }
14018
+ fail(`${r.violations.length} violation(s): not additive. Released copies would lose data or fail to upgrade.`);
14019
+ info(`Add a new field or table instead, and stop writing the old one (${code('human_edit: "open"')}).`);
14020
+ return 1;
14021
+ }
14022
+ async function publishEvolutionGate(projectRoot, strict) {
14023
+ let r;
14024
+ try {
14025
+ r = await runEvolutionCheck({ dir: projectRoot });
14026
+ } catch (err) {
14027
+ const msg = err instanceof Error ? err.message : String(err);
14028
+ if (strict) {
14029
+ fail(`Evolution check could not run (${msg}). Refusing with --strict. Nothing was published.`);
14030
+ return 1;
14031
+ }
14032
+ warn(`Evolution check skipped: ${msg}.`);
14033
+ return 0;
14034
+ }
14035
+ if (r.nothingReleased || r.violations.length === 0) return 0;
14036
+ printEvolutionReport(r, (s) => (strict ? fail : warn)(s));
14037
+ if (strict) {
14038
+ fail(
14039
+ `module.json is not an additive evolution of the released version (${r.violations.length} violation(s)). Refusing with --strict. Nothing was published.`
14040
+ );
14041
+ return 1;
14042
+ }
14043
+ warn(
14044
+ `module.json is not an additive evolution of the released version: installs that upgrade lose the data above. ${code("--strict")} turns this into a refusal; ${code("dashwise module diff --check")} shows it on its own.`
14045
+ );
14046
+ return 0;
14047
+ }
14048
+ var EvolutionCheckError;
14049
+ var init_diff_check = __esm({
14050
+ "src/commands/module/diff-check.ts"() {
14051
+ init_api_client();
14052
+ init_manifest_evolution();
14053
+ init_output();
14054
+ EvolutionCheckError = class extends Error {
14055
+ };
14056
+ }
14057
+ });
14058
+
14059
+ // src/lib/manifest-reference-lint.ts
14060
+ function referenceFieldWarnings(manifest) {
14061
+ const tables = manifest?.owns?.tables;
14062
+ if (!Array.isArray(tables)) return [];
14063
+ const warnings = [];
14064
+ for (const table of tables) {
14065
+ if (typeof table?.slug !== "string" || !Array.isArray(table.fields)) continue;
14066
+ for (const field of table.fields) {
14067
+ if (typeof field?.slug !== "string" || typeof field.type !== "string") continue;
14068
+ if (field.slug === "id" || !REFERENCE_NAME.test(field.slug)) continue;
14069
+ if (!PLAIN_TYPES.has(field.type)) continue;
14070
+ warnings.push({
14071
+ table: table.slug,
14072
+ field: field.slug,
14073
+ type: field.type,
14074
+ message: `${table.slug}.${field.slug} is a ${field.type} field named like a reference. If it points at a row in Dashwise, use a link_row field: plain ids break silently when data is copied to another installation. Ignore this for ids from outside systems.`
14075
+ });
14076
+ }
14077
+ }
14078
+ return warnings;
14079
+ }
14080
+ var REFERENCE_NAME, PLAIN_TYPES;
14081
+ var init_manifest_reference_lint = __esm({
14082
+ "src/lib/manifest-reference-lint.ts"() {
14083
+ REFERENCE_NAME = /(?:_id|_ref|Id|Ref)$/;
14084
+ PLAIN_TYPES = /* @__PURE__ */ new Set(["number", "text", "autonumber"]);
14085
+ }
14086
+ });
13552
14087
  function loadSchemaCommandContext(opts) {
13553
14088
  const projectRoot = resolve(opts.dir ?? process.cwd());
13554
14089
  const manifestPath2 = join(projectRoot, "module.json");
@@ -13621,6 +14156,89 @@ var init_dotenv_local = __esm({
13621
14156
  }
13622
14157
  });
13623
14158
 
14159
+ // src/lib/install-target.ts
14160
+ function keyDisplayPrefix(apiKey) {
14161
+ if (!apiKey.startsWith(KEY_SCHEME)) return null;
14162
+ const prefix = apiKey.slice(KEY_SCHEME.length, KEY_SCHEME.length + KEY_PREFIX_CHARS);
14163
+ return prefix.length === KEY_PREFIX_CHARS ? prefix : null;
14164
+ }
14165
+ async function keyBelongsToInstall(apiKey, installationId, options, now = /* @__PURE__ */ new Date()) {
14166
+ const prefix = keyDisplayPrefix(apiKey);
14167
+ if (prefix === null) return false;
14168
+ const { api_keys } = await listApiKeys(installationId, options);
14169
+ return api_keys.some(
14170
+ (k) => k.installation_id === installationId && k.key_prefix === prefix && (k.status === void 0 || k.status === "active") && k.revoked_at === null && (k.expires_at === null || new Date(k.expires_at) > now)
14171
+ );
14172
+ }
14173
+ function formatInstallTarget(id, detail) {
14174
+ return detail ? `\u2192 install #${id} (${detail})` : `\u2192 install #${id}`;
14175
+ }
14176
+ async function nameKeyTarget(args) {
14177
+ const { apiKey, keySource, entry, cfg, apiUrl, install } = args;
14178
+ const request = cfg ? { baseUrl: apiUrl, auth: { token: cfg.token }, signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) } : null;
14179
+ if (install === void 0) {
14180
+ if (keySource !== "cached" || !entry) {
14181
+ info(
14182
+ `\u2192 install not checked: ${code("DASHWISE_API_KEY")} comes from ${keySource === "environment" ? "the environment" : code(".env.local")}. Pass ${code("--install <id>")} to name its target and refuse a mismatch.`
14183
+ );
14184
+ return 0;
14185
+ }
14186
+ if (!request || entry.workspaceId === void 0) {
14187
+ info(formatInstallTarget(entry.installationId, "the cached dev install"));
14188
+ return 0;
14189
+ }
14190
+ try {
14191
+ const workspaces = await apiRequest("GET", "/api/workspaces", void 0, request);
14192
+ const ws = workspaces.find((w) => w.id === entry.workspaceId);
14193
+ info(formatInstallTarget(entry.installationId, `${ws ? ws.slug : `workspace #${entry.workspaceId}`}, local`));
14194
+ } catch {
14195
+ info(formatInstallTarget(entry.installationId));
14196
+ }
14197
+ return 0;
14198
+ }
14199
+ const source = keySource === "cached" ? "the cached key" : `${code("DASHWISE_API_KEY")} (from ${keySource === "environment" ? "the environment" : code(".env.local")})`;
14200
+ if (!request) {
14201
+ fail(
14202
+ `${code("--install")} looks the install up with your CLI login, and there is none. Run ${code("dashwise login")} first. Nothing was run.`
14203
+ );
14204
+ return 1;
14205
+ }
14206
+ let belongs;
14207
+ try {
14208
+ belongs = await keyBelongsToInstall(apiKey, install, request);
14209
+ } catch (err) {
14210
+ fail(
14211
+ `Could not read the keys of install #${install} to confirm ${source} (${describe(err)}). Refusing to run against an unconfirmed install. Nothing was run.`
14212
+ );
14213
+ return 1;
14214
+ }
14215
+ const cachedDev = entry?.installationId === install ? "the cached dev install, " : "";
14216
+ if (!belongs) {
14217
+ const other = entry && entry.installationId !== install && entry.apiKey === apiKey ? ` It is the cached key of install #${entry.installationId}.` : "";
14218
+ fail(`${source} is not a live key of install #${install}.${other} Nothing was run.`);
14219
+ info(
14220
+ `Use a key of install #${install} (mint one with ${code(`dashwise api-keys create <name> --install ${install}`)}), or drop ${code("--install")} to run against the key's own install.`
14221
+ );
14222
+ return 1;
14223
+ }
14224
+ info(formatInstallTarget(install, `${cachedDev}key checked`));
14225
+ return 0;
14226
+ }
14227
+ function describe(err) {
14228
+ if (err instanceof ApiError) return `${err.code || err.status} ${err.message}`;
14229
+ return err.message;
14230
+ }
14231
+ var KEY_PREFIX_CHARS, KEY_SCHEME, LOOKUP_TIMEOUT_MS;
14232
+ var init_install_target = __esm({
14233
+ "src/lib/install-target.ts"() {
14234
+ init_api_client();
14235
+ init_output();
14236
+ KEY_PREFIX_CHARS = 8;
14237
+ KEY_SCHEME = "dwk_";
14238
+ LOOKUP_TIMEOUT_MS = 1500;
14239
+ }
14240
+ });
14241
+
13624
14242
  // src/commands/run.ts
13625
14243
  var run_exports = {};
13626
14244
  __export(run_exports, {
@@ -13633,6 +14251,10 @@ async function runCommand(cmdArgs, options) {
13633
14251
  );
13634
14252
  return 1;
13635
14253
  }
14254
+ if (options.install !== void 0 && (!Number.isInteger(options.install) || options.install <= 0)) {
14255
+ fail(`--install expects a positive integer installation id (got "${options.install}").`);
14256
+ return 1;
14257
+ }
13636
14258
  const projectRoot = resolve(options.dir ?? process.cwd());
13637
14259
  let manifestSlug;
13638
14260
  try {
@@ -13645,6 +14267,7 @@ async function runCommand(cmdArgs, options) {
13645
14267
  fail(`module.json has no ${code("module.slug")} \u2014 the manifest is malformed.`);
13646
14268
  return 1;
13647
14269
  }
14270
+ const keyInShell = process.env.DASHWISE_API_KEY !== void 0;
13648
14271
  hydrateEnvFromDotenvLocal(projectRoot);
13649
14272
  const cfg = (() => {
13650
14273
  try {
@@ -13655,8 +14278,19 @@ async function runCommand(cmdArgs, options) {
13655
14278
  })();
13656
14279
  const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
13657
14280
  let apiKey = process.env.DASHWISE_API_KEY;
14281
+ let keySource = keyInShell ? "environment" : ".env.local";
14282
+ let entry = null;
13658
14283
  if (!apiKey) {
13659
- let entry = readModuleEntry(manifestSlug);
14284
+ entry = readModuleEntry(manifestSlug);
14285
+ if (entry && options.install !== void 0 && entry.installationId !== options.install) {
14286
+ fail(
14287
+ `No key for install #${options.install}: the cached ${code("dwk_")} key of ${code(manifestSlug)} belongs to install #${entry.installationId}. Nothing was run.`
14288
+ );
14289
+ info(
14290
+ `Set ${code("DASHWISE_API_KEY")} to a key of install #${options.install} (mint one with ${code(`dashwise api-keys create <name> --install ${options.install}`)}).`
14291
+ );
14292
+ return 1;
14293
+ }
13660
14294
  if (cfg) {
13661
14295
  const healed = await resolveModuleApiKey(
13662
14296
  manifestSlug,
@@ -13667,6 +14301,7 @@ async function runCommand(cmdArgs, options) {
13667
14301
  if (healed) entry = healed;
13668
14302
  }
13669
14303
  apiKey = entry?.apiKey;
14304
+ keySource = "cached";
13670
14305
  }
13671
14306
  if (!apiKey) {
13672
14307
  fail(
@@ -13677,6 +14312,15 @@ async function runCommand(cmdArgs, options) {
13677
14312
  );
13678
14313
  return 1;
13679
14314
  }
14315
+ const target = await nameKeyTarget({
14316
+ apiKey,
14317
+ keySource,
14318
+ entry: entry ?? readModuleEntry(manifestSlug),
14319
+ cfg,
14320
+ apiUrl,
14321
+ ...options.install !== void 0 ? { install: options.install } : {}
14322
+ });
14323
+ if (target !== 0) return target;
13680
14324
  const [cmd, ...rest] = cmdArgs;
13681
14325
  info(
13682
14326
  `${dim("run")} ${cmd} ${rest.join(" ")} ${dim(`(DASHWISE_API_URL=${apiUrl}, DASHWISE_API_KEY injected)`)}`
@@ -13706,6 +14350,7 @@ var init_run = __esm({
13706
14350
  "src/commands/run.ts"() {
13707
14351
  init_config();
13708
14352
  init_dotenv_local();
14353
+ init_install_target();
13709
14354
  init_manifest_io();
13710
14355
  init_output();
13711
14356
  init_dev();
@@ -13996,6 +14641,7 @@ async function schemaPushCommand(options) {
13996
14641
  return surfaceSchemaPushError(err, slug);
13997
14642
  }
13998
14643
  printApplySummary(applied);
14644
+ for (const w of referenceFieldWarnings(manifest)) warn(w.message);
13999
14645
  if (dwkAuth) {
14000
14646
  try {
14001
14647
  const { manifest: canonical } = await getInstallationSchema(dwkAuth);
@@ -14144,6 +14790,7 @@ var init_push = __esm({
14144
14790
  init_manifest_io();
14145
14791
  init_manifest_merge();
14146
14792
  init_output();
14793
+ init_manifest_reference_lint();
14147
14794
  }
14148
14795
  });
14149
14796
 
@@ -14721,6 +15368,26 @@ var init_set_type = __esm({
14721
15368
  }
14722
15369
  });
14723
15370
 
15371
+ // src/lib/env-proxy.ts
15372
+ var PROXY_VARS = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
15373
+ function hasProxyEnv(env = process.env) {
15374
+ return PROXY_VARS.some((name) => Boolean(env[name]?.trim()));
15375
+ }
15376
+ async function installEnvProxy(env = process.env) {
15377
+ if (!hasProxyEnv(env)) return false;
15378
+ const undici = await import('undici');
15379
+ undici.setGlobalDispatcher(new undici.EnvHttpProxyAgent());
15380
+ Object.assign(globalThis, {
15381
+ fetch: undici.fetch,
15382
+ FormData: undici.FormData,
15383
+ Headers: undici.Headers,
15384
+ Request: undici.Request,
15385
+ Response: undici.Response
15386
+ });
15387
+ env.NODE_USE_ENV_PROXY ??= "1";
15388
+ return true;
15389
+ }
15390
+
14724
15391
  // src/lib/excess-args.ts
14725
15392
  init_output();
14726
15393
  var SMART_DASH_RE = /^[‒–—―−]/;
@@ -15128,7 +15795,7 @@ init_api_client();
15128
15795
  init_config();
15129
15796
  init_output();
15130
15797
  init_sdk_version();
15131
- var MIN_NODE_MAJOR = 20;
15798
+ var MIN_NODE = [20, 18, 1];
15132
15799
  var CLOUD_FILES = [
15133
15800
  "scripts/cloud-setup.sh",
15134
15801
  ".claude/settings.json",
@@ -15348,13 +16015,20 @@ function unreachable(apiUrl) {
15348
16015
  };
15349
16016
  }
15350
16017
  function nodeCheck(version) {
15351
- const major = Number.parseInt(version.replace(/^v/, ""), 10);
15352
- return Number.isFinite(major) && major >= MIN_NODE_MAJOR ? { id: "node", label: "Node", status: "ok", detail: version } : {
16018
+ const parts = version.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10));
16019
+ let supported = parts.length === 3 && parts.every(Number.isFinite);
16020
+ for (let i = 0; supported && i < 3; i++) {
16021
+ if (parts[i] !== MIN_NODE[i]) {
16022
+ supported = parts[i] > MIN_NODE[i];
16023
+ break;
16024
+ }
16025
+ }
16026
+ return supported ? { id: "node", label: "Node", status: "ok", detail: version } : {
15353
16027
  id: "node",
15354
16028
  label: "Node",
15355
16029
  status: "fail",
15356
16030
  detail: `${version} is too old`,
15357
- fix: `Install Node ${MIN_NODE_MAJOR} or newer (the template pins Node 22).`
16031
+ fix: `Install Node ${MIN_NODE.join(".")} or newer (the template pins Node 22).`
15358
16032
  };
15359
16033
  }
15360
16034
  async function versionCheck(id, label, installed, latest, fix) {
@@ -15674,8 +16348,8 @@ function startMcpProxy(opts) {
15674
16348
  let streamUnavailable = false;
15675
16349
  let streamStarted = false;
15676
16350
  let resolveDone;
15677
- const done = new Promise((resolve15) => {
15678
- resolveDone = resolve15;
16351
+ const done = new Promise((resolve16) => {
16352
+ resolveDone = resolve16;
15679
16353
  });
15680
16354
  const trace = (line) => {
15681
16355
  if (verbose) log2(line);
@@ -16161,7 +16835,7 @@ async function revokeSession(endpointId) {
16161
16835
  () => false
16162
16836
  );
16163
16837
  const timeout = new Promise(
16164
- (resolve15) => setTimeout(() => resolve15(false), REVOKE_TIMEOUT_MS).unref()
16838
+ (resolve16) => setTimeout(() => resolve16(false), REVOKE_TIMEOUT_MS).unref()
16165
16839
  );
16166
16840
  await Promise.race([attempt, timeout]);
16167
16841
  }
@@ -16339,7 +17013,7 @@ async function mcpAddCommand(options = {}) {
16339
17013
  fail(`Could not run \`claude\`: ${target.refused} Run the command above yourself.`);
16340
17014
  return 1;
16341
17015
  }
16342
- return await new Promise((resolve15) => {
17016
+ return await new Promise((resolve16) => {
16343
17017
  const child = spawn(target.command, target.args, { shell: target.shell, stdio: "inherit" });
16344
17018
  let settled = false;
16345
17019
  child.on("error", (err) => {
@@ -16349,16 +17023,16 @@ async function mcpAddCommand(options = {}) {
16349
17023
  fail(
16350
17024
  `Claude Code is not on your PATH, so nothing was registered. Run the command above once ${code("claude")} is installed.`
16351
17025
  );
16352
- resolve15(1);
17026
+ resolve16(1);
16353
17027
  return;
16354
17028
  }
16355
17029
  fail(`Could not run \`claude\`: ${err.message}`);
16356
- resolve15(1);
17030
+ resolve16(1);
16357
17031
  });
16358
17032
  child.on("close", (codeOut) => {
16359
17033
  if (settled) return;
16360
17034
  settled = true;
16361
- resolve15(codeOut ?? 1);
17035
+ resolve16(codeOut ?? 1);
16362
17036
  });
16363
17037
  });
16364
17038
  }
@@ -16477,10 +17151,10 @@ function upgradeCommand(plugin, marketplace) {
16477
17151
  var PLUGIN_NAME = "dashwise";
16478
17152
  var MARKETPLACE_NAME = "dashwise";
16479
17153
  var DEFAULT_MARKETPLACE_SOURCE = "Intuize/dashwise-cli";
16480
- var defaultRunner = (cmd, args, opts) => new Promise((resolve15) => {
17154
+ var defaultRunner = (cmd, args, opts) => new Promise((resolve16) => {
16481
17155
  const target = spawnTarget(cmd, args);
16482
17156
  if ("refused" in target) {
16483
- resolve15({ code: null, stdout: "", stderr: target.refused, missing: false });
17157
+ resolve16({ code: null, stdout: "", stderr: target.refused, missing: false });
16484
17158
  return;
16485
17159
  }
16486
17160
  const child = spawn(target.command, target.args, {
@@ -16495,7 +17169,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolve15) => {
16495
17169
  child.on("error", (err) => {
16496
17170
  if (settled) return;
16497
17171
  settled = true;
16498
- resolve15({
17172
+ resolve16({
16499
17173
  code: null,
16500
17174
  stdout,
16501
17175
  stderr: stderr || err.message,
@@ -16505,7 +17179,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolve15) => {
16505
17179
  child.on("close", (exitCode) => {
16506
17180
  if (settled) return;
16507
17181
  settled = true;
16508
- resolve15({ code: exitCode, stdout, stderr, missing: false });
17182
+ resolve16({ code: exitCode, stdout, stderr, missing: false });
16509
17183
  });
16510
17184
  });
16511
17185
  function readSkillsOnDisk() {
@@ -17746,6 +18420,7 @@ function judgeGeneratorStamp(stamp, opts = {}) {
17746
18420
  // src/commands/module/generate.ts
17747
18421
  init_sdk_version();
17748
18422
  init_output();
18423
+ init_declared_secrets();
17749
18424
  function outputDir(projectRoot) {
17750
18425
  return join(projectRoot, "node_modules", "@dashai", "generated");
17751
18426
  }
@@ -17822,6 +18497,9 @@ async function moduleGenerateCommand(options) {
17822
18497
  info(`Hash: ${dim(res.contentHash)}`);
17823
18498
  if (verdict.note !== null && declaresUses(manifest)) info(verdict.note);
17824
18499
  warnIfSdkBelowUsesFloor(projectRoot, manifest);
18500
+ reportSecretLines(
18501
+ syncEnvLocalSecretLines(projectRoot, declaredSecretNames(manifest))
18502
+ );
17825
18503
  return 0;
17826
18504
  }
17827
18505
  function declaresUses(manifest) {
@@ -18080,6 +18758,101 @@ init_init();
18080
18758
  // src/commands/module/install.ts
18081
18759
  init_api_client();
18082
18760
  init_config();
18761
+
18762
+ // src/lib/agent-session.ts
18763
+ var MARKERS = [
18764
+ ["CLAUDECODE", (v) => v === "1"],
18765
+ ["CLAUDE_CODE_REMOTE", (v) => v === "true"],
18766
+ ["CODEX_SANDBOX", (v) => v.length > 0],
18767
+ ["CODEX_SANDBOX_NETWORK_DISABLED", (v) => v.length > 0],
18768
+ ["CURSOR_AGENT", (v) => v.length > 0],
18769
+ ["DW_AGENT", (v) => v.length > 0],
18770
+ ["DASHWISE_AGENT_SESSION", (v) => v === "1" || v === "true"]
18771
+ ];
18772
+ function agentSessionMarker(env = process.env) {
18773
+ for (const [name, test] of MARKERS) {
18774
+ const value = env[name];
18775
+ if (value !== void 0 && test(value)) return name;
18776
+ }
18777
+ return null;
18778
+ }
18779
+
18780
+ // src/lib/drift-preview.ts
18781
+ init_api_client();
18782
+ function redactedConsumer(redacted) {
18783
+ switch (redacted) {
18784
+ case "private_copy":
18785
+ return "someone's private copy";
18786
+ case "other_team_copy":
18787
+ return "another team's copy";
18788
+ case "development_copy":
18789
+ return "a development copy you cannot see";
18790
+ default:
18791
+ return "a copy you cannot see";
18792
+ }
18793
+ }
18794
+ var WIDEN_WORDS = {
18795
+ uses_need_added: "a new table it reads from another module",
18796
+ uses_ops_widened: "more operations on a table it reads",
18797
+ uses_fields_added: "more fields of a table it reads",
18798
+ dependency_added: "a new or wider dependency",
18799
+ exposes_widened: "more of what it exposes to other modules",
18800
+ permissions_added: "more permissions",
18801
+ roles_widened: "more roles or permission keys",
18802
+ outbound_added: "new outbound APIs (data can leave)",
18803
+ secrets_added: "new secrets",
18804
+ public_access_enabled: "public access"
18805
+ };
18806
+ function describeWidening(widening) {
18807
+ return widening.map((w) => WIDEN_WORDS[w] ?? w).join(", ");
18808
+ }
18809
+ async function getUpgradePreview(installationId, version, options = {}) {
18810
+ return apiRequest(
18811
+ "GET",
18812
+ `/api/installations/${installationId}/upgrade-preview?version=${encodeURIComponent(version)}`,
18813
+ void 0,
18814
+ options
18815
+ );
18816
+ }
18817
+ function count(n, unit) {
18818
+ if (n === null) return `${unit}s not counted`;
18819
+ return `${n.toLocaleString("en-US")} ${unit}${n === 1 ? "" : "s"}`;
18820
+ }
18821
+ function describeDrift(drift) {
18822
+ const lines = [];
18823
+ const d = drift.details;
18824
+ const removedTables = d?.tables_to_remove ?? drift.tables_to_remove.map((t) => ({ table: t, name: t, rows: null }));
18825
+ for (const t of removedTables) {
18826
+ lines.push({
18827
+ loses: t.rows !== 0,
18828
+ text: `Moves table ${t.table} (${count(t.rows, "row")}) to the Trash`
18829
+ });
18830
+ }
18831
+ const removedFields = d?.fields_to_remove ?? drift.fields_to_remove.map((f) => ({ ...f, name: f.field, values: null }));
18832
+ for (const f of removedFields) {
18833
+ lines.push({
18834
+ loses: f.values !== 0,
18835
+ text: `Moves field ${f.table}.${f.field} (${count(f.values, "value")}) to the Trash`
18836
+ });
18837
+ }
18838
+ const retyped = d?.fields_to_retype ?? (drift.fields_to_retype ?? []).map((f) => ({ ...f, name: f.field, values: null }));
18839
+ for (const f of retyped) {
18840
+ lines.push({
18841
+ loses: f.values !== 0,
18842
+ text: `Converts field ${f.table}.${f.field} from ${f.from} to ${f.to} (${count(f.values, "value")}; a value that does not convert is lost)`
18843
+ });
18844
+ }
18845
+ lines.sort((a, b) => Number(b.loses) - Number(a.loses));
18846
+ for (const t of drift.tables_to_create) {
18847
+ lines.push({ loses: false, text: `Adds table ${t}` });
18848
+ }
18849
+ for (const f of drift.fields_to_create) {
18850
+ lines.push({ loses: false, text: `Adds field ${f.table}.${f.field}` });
18851
+ }
18852
+ return lines;
18853
+ }
18854
+
18855
+ // src/commands/module/install.ts
18083
18856
  init_manifest_io();
18084
18857
 
18085
18858
  // src/lib/module-lifecycle.ts
@@ -18258,18 +19031,31 @@ async function moduleInstallCommand(slugArg, options) {
18258
19031
  );
18259
19032
  }
18260
19033
  }
18261
- if (!options.yes && process.stdout.isTTY && !confirmedViaPicker) {
18262
- const message = devInstallToReplace ? isUpgrade ? `Destroy the dev install (revokes local-dev credentials) and upgrade ${code(mod.slug)} to ${code(`v${targetVersion}`)} in ${code(workspace2.slug)}?` : `Replace the dev install (revokes local-dev credentials) and install ${code(mod.slug)} ${code(`v${targetVersion}`)} into ${code(workspace2.slug)} as production?` : `${isUpgrade ? "Upgrade" : "Install"} ${code(mod.slug)} ${code(`v${targetVersion}`)} into workspace ${code(workspace2.slug)}?`;
19034
+ let preview;
19035
+ let lossy = false;
19036
+ let lossAccepted = false;
19037
+ if (existing) {
19038
+ const rerun = rerunLine(mod.slug, workspace2.slug, targetVersion, options, devInstallToReplace !== void 0);
19039
+ const previewed = await previewUpgrade(existing.id, targetVersion, options, auth, rerun, devInstallToReplace?.id);
19040
+ if (typeof previewed === "number") return previewed;
19041
+ preview = previewed.preview;
19042
+ lossy = previewed.lossy;
19043
+ lossAccepted = previewed.accepted;
19044
+ }
19045
+ const askForLoss = lossy && !lossAccepted;
19046
+ if (askForLoss || !options.yes && process.stdout.isTTY && !confirmedViaPicker) {
19047
+ const lossClause = lossy ? " It moves the data listed above to the Trash or converts it." : "";
19048
+ const message = devInstallToReplace ? isUpgrade ? `Destroy the dev install (revokes local-dev credentials) and upgrade ${code(mod.slug)} to ${code(`v${targetVersion}`)} in ${code(workspace2.slug)}?${lossClause}` : `Replace the dev install (revokes local-dev credentials) and install ${code(mod.slug)} ${code(`v${targetVersion}`)} into ${code(workspace2.slug)} as production?` : `${isUpgrade ? "Upgrade" : "Install"} ${code(mod.slug)} ${code(`v${targetVersion}`)} into workspace ${code(workspace2.slug)}?${lossClause}`;
18263
19049
  const answer = await select({
18264
19050
  message,
18265
19051
  options: [
18266
19052
  {
18267
19053
  value: "go",
18268
- label: devInstallToReplace ? isUpgrade ? "Yes, destroy the dev install + upgrade" : "Yes, replace the dev install + production-install" : isUpgrade ? "Yes, upgrade now" : "Yes, install now"
19054
+ label: devInstallToReplace ? isUpgrade ? lossy ? "Yes, destroy the dev install + upgrade and lose the data listed above" : "Yes, destroy the dev install + upgrade" : "Yes, replace the dev install + production-install" : isUpgrade ? lossy ? "Yes, upgrade and lose the data listed above" : "Yes, upgrade now" : "Yes, install now"
18269
19055
  },
18270
19056
  { value: "cancel", label: "No, abort" }
18271
19057
  ],
18272
- initialValue: devInstallToReplace ? "cancel" : "go"
19058
+ initialValue: devInstallToReplace || lossy ? "cancel" : "go"
18273
19059
  });
18274
19060
  if (isCancel(answer) || answer === "cancel") {
18275
19061
  fail("Aborted. Nothing was changed.");
@@ -18291,7 +19077,9 @@ async function moduleInstallCommand(slugArg, options) {
18291
19077
  workspaceId: workspace2.id,
18292
19078
  version: targetVersion,
18293
19079
  ...existing ? { installation_id: existing.id } : {},
18294
- ...options.force !== void 0 ? { force: options.force } : {}
19080
+ ...options.force !== void 0 ? { force: options.force } : {},
19081
+ // P5-08: confirms exactly the delta that was shown (409 DRIFT_CHANGED otherwise).
19082
+ ...preview ? { drift_hash: preview.drift_hash } : {}
18295
19083
  },
18296
19084
  auth
18297
19085
  );
@@ -18343,6 +19131,9 @@ async function moduleInstallCommand(slugArg, options) {
18343
19131
  was_upgrade: result.was_upgrade,
18344
19132
  public_url: publicUrl,
18345
19133
  verified,
19134
+ // P5-08: the confirmed preview; `null` for a fresh install (nothing to preview).
19135
+ drift_hash: preview?.drift_hash ?? null,
19136
+ data_loss: preview ? lossy : null,
18346
19137
  // `null` when the binding surface was unreachable; `[]` when the
18347
19138
  // module declares no slots. The two are not the same thing.
18348
19139
  uses: readiness === null ? null : usesInstallJson(readiness),
@@ -18511,6 +19302,19 @@ function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
18511
19302
  Ask the module's author to invite this workspace (or open the install gate), then retry.`
18512
19303
  );
18513
19304
  break;
19305
+ case "DRIFT_CHANGED":
19306
+ fail(
19307
+ `Install ${code(slug)} changed since its upgrade preview, so the confirmed change no longer matches. Nothing was changed.
19308
+ Re-run ${code("dashwise module install")} to see the new preview.`
19309
+ );
19310
+ break;
19311
+ case "DATA_LOSS_CONFIRM_REQUIRED":
19312
+ case "UPGRADE_CONFIRM_REQUIRED":
19313
+ fail(
19314
+ `The platform asks for a confirmed preview before this upgrade (${err.code}). Nothing was changed.
19315
+ Re-run ${code("dashwise module install")}: it previews the upgrade and sends the confirmation.`
19316
+ );
19317
+ break;
18514
19318
  case "UPGRADE_BREAKS_CONSUMERS":
18515
19319
  fail(
18516
19320
  `Upgrading ${code(slug)} would break modules that consume its current schema.
@@ -18560,6 +19364,75 @@ function usesInstallJson(readiness) {
18560
19364
  }))
18561
19365
  };
18562
19366
  }
19367
+ async function previewUpgrade(installationId, targetVersion, options, auth, rerun, devInstallToReplaceId) {
19368
+ let preview;
19369
+ try {
19370
+ preview = await getUpgradePreview(installationId, targetVersion, auth);
19371
+ } catch (err) {
19372
+ const why = err instanceof ApiError ? `${err.code || err.status} ${err.message}` : err.message;
19373
+ const checkNote = options.acceptDataLoss !== void 0 ? `, so ${code("--accept-data-loss")} cannot be checked` : "";
19374
+ fail(
19375
+ `Could not preview what upgrading install #${installationId} to ${code(`v${targetVersion}`)} does to its data (${why})${checkNote}. Nothing was changed.`
19376
+ );
19377
+ info(`An upgrade is never applied without its preview; retry when the platform answers.`);
19378
+ return 1;
19379
+ }
19380
+ const lines = describeDrift(preview.drift);
19381
+ const lossy = lines.some((l) => l.loses);
19382
+ const from = preview.from_version ? `v${preview.from_version}` : "its current version";
19383
+ if (lines.length === 0) {
19384
+ info(`Upgrading install #${installationId} from ${from} to v${preview.to_version} changes no table or field.`);
19385
+ } else {
19386
+ info(`What upgrading install #${installationId} from ${from} to v${preview.to_version} does to its data:`);
19387
+ for (const line of lines) {
19388
+ (line.loses ? warn : info)(` ${line.text}`);
19389
+ }
19390
+ }
19391
+ const broken = preview.breaks_consumers ?? [];
19392
+ if (broken.length > 0) {
19393
+ const names = broken.map(
19394
+ (c2) => c2.module_slug ? `${c2.module_slug}${c2.installation_id ? ` #${c2.installation_id}` : ""}` : redactedConsumer(c2.redacted)
19395
+ );
19396
+ warn(` Breaks ${broken.length} module${broken.length === 1 ? "" : "s"} that read${broken.length === 1 ? "s" : ""} this one: ${names.join(", ")}`);
19397
+ }
19398
+ if (preview.approval_required) {
19399
+ const asks = preview.widening && preview.widening.length > 0 ? `: it asks for ${describeWidening(preview.widening)}` : "";
19400
+ warn(` This update waits for the module author's approval${asks}.`);
19401
+ }
19402
+ if (options.acceptDataLoss !== void 0 && options.acceptDataLoss !== preview.drift_hash) {
19403
+ fail(
19404
+ `${code("--accept-data-loss")} does not match this upgrade's preview: the copy or the release changed since that hash was printed. Nothing was changed.`
19405
+ );
19406
+ info(`To accept the change listed above, a person re-runs:
19407
+ ${rerun(preview.drift_hash)}`);
19408
+ return 1;
19409
+ }
19410
+ const accepted = lossy && options.acceptDataLoss === preview.drift_hash;
19411
+ if (lossy && !accepted) {
19412
+ const yesNote = options.yes ? ` ${code("--yes")} does not accept data loss.` : "";
19413
+ const marker = agentSessionMarker();
19414
+ const why = marker ? `this is an agent session (${code(marker)} is set): a person decides` : !process.stdout.isTTY ? "there is no terminal to ask in" : null;
19415
+ if (why) {
19416
+ const alsoDev = devInstallToReplaceId !== void 0 ? ` and destroys dev install #${devInstallToReplaceId} first` : "";
19417
+ fail(`This upgrade loses the data listed above${alsoDev}, and ${why}.${yesNote} Refused. Nothing was changed.`);
19418
+ info(`To accept exactly that loss, a person re-runs:
19419
+ ${rerun(preview.drift_hash)}`);
19420
+ return 1;
19421
+ }
19422
+ }
19423
+ return { preview, lossy, accepted };
19424
+ }
19425
+ function rerunLine(slug, workspaceSlug, targetVersion, options, replacingDevInstall) {
19426
+ return (hash) => [
19427
+ "dashwise module install",
19428
+ slug,
19429
+ `--workspace ${workspaceSlug}`,
19430
+ `--version ${targetVersion}`,
19431
+ ...options.force ? ["--force"] : [],
19432
+ ...replacingDevInstall ? ["--replace-dev-install"] : [],
19433
+ `--accept-data-loss ${hash}`
19434
+ ].join(" ");
19435
+ }
18563
19436
 
18564
19437
  // src/commands/module/list.ts
18565
19438
  init_api_client();
@@ -18929,6 +19802,7 @@ function formatBytes(n) {
18929
19802
  }
18930
19803
 
18931
19804
  // src/commands/module/publish.ts
19805
+ init_diff_check();
18932
19806
  var VALID_BUMP = /* @__PURE__ */ new Set(["patch", "minor", "major"]);
18933
19807
  var PACKED_FILE_LIST_CAP = 20;
18934
19808
  function packedFileLines(files, cap = PACKED_FILE_LIST_CAP) {
@@ -19015,6 +19889,10 @@ async function modulePublishCommand(options) {
19015
19889
  info(
19016
19890
  `Module: ${code(mod.slug)} (id ${mod.id})${mod.current_version ? `, current ${code(`v${mod.current_version}`)}` : dim(", no published version yet")}`
19017
19891
  );
19892
+ if (mod.current_version) {
19893
+ const gate = await publishEvolutionGate(projectRoot, options.strict === true);
19894
+ if (gate !== 0) return gate;
19895
+ }
19018
19896
  let tarball;
19019
19897
  try {
19020
19898
  tarball = await packTarball({
@@ -19606,7 +20484,7 @@ async function moduleReleaseCommand(slugArg, options) {
19606
20484
  }
19607
20485
  async function wait() {
19608
20486
  const remaining = Math.max(0, deadline - Date.now());
19609
- await new Promise((resolve15) => setTimeout(resolve15, Math.min(options.pollIntervalMs ?? 4e3, remaining)));
20487
+ await new Promise((resolve16) => setTimeout(resolve16, Math.min(options.pollIntervalMs ?? 4e3, remaining)));
19610
20488
  }
19611
20489
  }
19612
20490
 
@@ -20028,6 +20906,7 @@ async function modulePullDataCommand(options) {
20028
20906
  // src/commands/module/validate.ts
20029
20907
  init_api_client();
20030
20908
  init_output();
20909
+ init_manifest_reference_lint();
20031
20910
  async function moduleValidateCommand(options) {
20032
20911
  const projectRoot = resolve(options.dir ?? process.cwd());
20033
20912
  const manifestPath2 = join(projectRoot, "module.json");
@@ -20052,6 +20931,7 @@ async function moduleValidateCommand(options) {
20052
20931
  );
20053
20932
  if (res.ok) {
20054
20933
  success("Manifest is valid.");
20934
+ for (const w of referenceFieldWarnings(manifest)) warn(w.message);
20055
20935
  return 0;
20056
20936
  }
20057
20937
  fail(`Unexpected response shape: ${JSON.stringify(res)}`);
@@ -22454,6 +23334,23 @@ init_api_client();
22454
23334
  init_config();
22455
23335
  init_manifest_io();
22456
23336
  init_output();
23337
+ init_declared_secrets();
23338
+ var SECRETS_NOTE = "# Declared secrets (runtime.secrets[]): set each value on your host; Dashwise never exports a secret value.";
23339
+ function secretNameLines(names, format) {
23340
+ const lines = names.map((name) => {
23341
+ switch (format) {
23342
+ case "env":
23343
+ return `# ${name}=`;
23344
+ case "railway":
23345
+ return `# ${name}: railway variables set ${name}='<value>'`;
23346
+ case "vercel":
23347
+ return `# ${name}: vercel env add ${name} production`;
23348
+ case "fly":
23349
+ return `# ${name}: fly secrets set ${name}='<value>'`;
23350
+ }
23351
+ });
23352
+ return [SECRETS_NOTE, ...lines].join("\n");
23353
+ }
22457
23354
  var PLACEHOLDER = "<run-`dashwise-api-keys-create-<name>`-and-paste-here>";
22458
23355
  async function envExportCommand(options) {
22459
23356
  const format = options.format ?? "env";
@@ -22544,7 +23441,19 @@ async function envExportCommand(options) {
22544
23441
  ...apiKeyComment !== null ? { comment: apiKeyComment } : {}
22545
23442
  }
22546
23443
  ];
23444
+ const secretNames = options.withSecretNames ? declaredSecretNames(manifest) : [];
22547
23445
  process.stdout.write(render(entries, format));
23446
+ if (secretNames.length > 0) {
23447
+ if (format === "json") {
23448
+ info(
23449
+ `Declared secrets to set on your host (Dashwise never exports a secret value): ${secretNames.join(", ")}`
23450
+ );
23451
+ } else {
23452
+ process.stdout.write(`
23453
+
23454
+ ${secretNameLines(secretNames, format)}`);
23455
+ }
23456
+ }
22548
23457
  if (format !== "json") process.stdout.write("\n");
22549
23458
  if (apiKey === PLACEHOLDER) {
22550
23459
  info("");
@@ -22600,6 +23509,7 @@ init_config();
22600
23509
  init_dotenv_local();
22601
23510
  init_manifest_io();
22602
23511
  init_output();
23512
+ init_install_target();
22603
23513
  init_dev();
22604
23514
  async function queryCommand(options) {
22605
23515
  const projectRoot = resolve(options.dir ?? process.cwd());
@@ -22622,17 +23532,25 @@ async function queryCommand(options) {
22622
23532
  );
22623
23533
  return 1;
22624
23534
  }
23535
+ const keyInShell = process.env.DASHWISE_API_KEY !== void 0;
22625
23536
  hydrateEnvFromDotenvLocal(projectRoot);
23537
+ const cfg = (() => {
23538
+ try {
23539
+ return readConfig();
23540
+ } catch {
23541
+ return null;
23542
+ }
23543
+ })();
23544
+ const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
23545
+ let keySource = keyInShell ? "environment" : ".env.local";
23546
+ let entry = manifestSlug ? readModuleEntry(manifestSlug) : null;
22626
23547
  if (!process.env.DASHWISE_API_KEY && manifestSlug) {
22627
- const cfg = (() => {
22628
- try {
22629
- return readConfig();
22630
- } catch {
22631
- return null;
22632
- }
22633
- })();
22634
- const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
22635
- let entry = readModuleEntry(manifestSlug);
23548
+ if (entry && options.install !== void 0 && entry.installationId !== options.install) {
23549
+ fail(
23550
+ `No key for install #${options.install}: the cached ${code("dwk_")} key of ${code(manifestSlug)} belongs to install #${entry.installationId}. Nothing was run.`
23551
+ );
23552
+ return 1;
23553
+ }
22636
23554
  if (cfg) {
22637
23555
  const healed = await resolveModuleApiKey(
22638
23556
  manifestSlug,
@@ -22644,11 +23562,26 @@ async function queryCommand(options) {
22644
23562
  }
22645
23563
  if (entry?.apiKey) {
22646
23564
  process.env.DASHWISE_API_KEY = entry.apiKey;
23565
+ keySource = "cached";
22647
23566
  if (!process.env.DASHWISE_API_URL) {
22648
23567
  process.env.DASHWISE_API_URL = apiUrl;
22649
23568
  }
22650
23569
  }
22651
23570
  }
23571
+ if (process.env.DASHWISE_API_KEY) {
23572
+ const target = await nameKeyTarget({
23573
+ apiKey: process.env.DASHWISE_API_KEY,
23574
+ keySource,
23575
+ entry,
23576
+ cfg,
23577
+ apiUrl: process.env.DASHWISE_API_URL ?? apiUrl,
23578
+ ...options.install !== void 0 ? { install: options.install } : {}
23579
+ });
23580
+ if (target !== 0) return target;
23581
+ } else if (options.install !== void 0) {
23582
+ fail(`No ${code("dwk_")} key to check against install #${options.install}. Set ${code("DASHWISE_API_KEY")}. Nothing was run.`);
23583
+ return 1;
23584
+ }
22652
23585
  const modes = [
22653
23586
  options.inline !== void 0 ? "inline" : null,
22654
23587
  options.file !== void 0 ? "file" : null,
@@ -23014,7 +23947,7 @@ function formatTimestamp(iso) {
23014
23947
  return iso.slice(0, 19).replace("T", " ");
23015
23948
  }
23016
23949
  function sleep2(ms) {
23017
- return new Promise((resolve15) => setTimeout(resolve15, ms));
23950
+ return new Promise((resolve16) => setTimeout(resolve16, ms));
23018
23951
  }
23019
23952
 
23020
23953
  // src/commands/module/action-log.ts
@@ -23116,7 +24049,7 @@ async function runTail2(installationId, options) {
23116
24049
  }
23117
24050
  dim(`(tail: transient error \u2014 ${err.message})`);
23118
24051
  }
23119
- await new Promise((resolve15) => setTimeout(resolve15, POLL_INTERVAL_MS2));
24052
+ await new Promise((resolve16) => setTimeout(resolve16, POLL_INTERVAL_MS2));
23120
24053
  }
23121
24054
  }
23122
24055
  function printTable(rows) {
@@ -23501,7 +24434,7 @@ async function sleep3(ms, aborted) {
23501
24434
  const slice = 100;
23502
24435
  let waited = 0;
23503
24436
  while (waited < ms && !aborted()) {
23504
- await new Promise((resolve15) => setTimeout(resolve15, Math.min(slice, ms - waited)));
24437
+ await new Promise((resolve16) => setTimeout(resolve16, Math.min(slice, ms - waited)));
23505
24438
  waited += slice;
23506
24439
  }
23507
24440
  }
@@ -24332,12 +25265,29 @@ moduleCmd.command("list").description("List modules you own (or `--all` for owne
24332
25265
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
24333
25266
  });
24334
25267
  });
24335
- 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) => {
24336
- process.exitCode = await moduleDiffCommand({
24337
- ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
24338
- ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
24339
- });
24340
- });
25268
+ moduleCmd.command("diff").description(
25269
+ "Show what would change at the next publish vs the currently-published version. With --check: exit 1 when module.json is not an additive evolution of the released manifest (a removed table or field, a retype, a removed or reordered option, a new unique index (`indexes[].unique`) over an existing field, a dropped unique index, required tightened, or a slug reused after an earlier removal), 2 on a usage or read error. Offline with --base."
25270
+ ).option("--dir <path>", "Project root (default: current directory).").option("--json", "Emit the structured diff as JSON instead of the human-readable summary.").option("--check", "Check additive evolution against the released manifest (exit 0 additive, 1 not additive, 2 error).").option("--base <ref|path>", "With --check: the base manifest from a git ref (its module.json, history from its log) or, when the path exists, a file. Default: the released version on the platform.").option("--base-file <path>", "With --check: the base manifest from a file.").option("--head <path>", "With --check: the manifest to check (default: <dir>/module.json).").option("--no-history", "With --check: skip the slug-reuse scan of older versions.").option("--dry-run", "Accepted for uniformity; --check never writes anything.").action(
25271
+ async (cmdOpts) => {
25272
+ if (cmdOpts.check) {
25273
+ const { moduleDiffCheckCommand: moduleDiffCheckCommand2 } = await Promise.resolve().then(() => (init_diff_check(), diff_check_exports));
25274
+ process.exitCode = await moduleDiffCheckCommand2({
25275
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
25276
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
25277
+ ...cmdOpts.base !== void 0 ? { base: cmdOpts.base } : {},
25278
+ ...cmdOpts.baseFile !== void 0 ? { baseFile: cmdOpts.baseFile } : {},
25279
+ ...cmdOpts.head !== void 0 ? { head: cmdOpts.head } : {},
25280
+ // commander maps `--no-history` to `history === false`.
25281
+ ...cmdOpts.history === false ? { noHistory: true } : {}
25282
+ });
25283
+ return;
25284
+ }
25285
+ process.exitCode = await moduleDiffCommand({
25286
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
25287
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
25288
+ });
25289
+ }
25290
+ );
24341
25291
  moduleCmd.command("pull-data").description("Export the module's own-table rows to a local JSON file (for fixtures, local dev seeding).").option("--dir <path>", "Project root (default: current directory).").option("--limit <n>", "Max rows per table (1-1000, default 100).", parseIntOption).option("--out <path>", "Output file path (default: ./dashwise-data.json).").action(async (cmdOpts) => {
24342
25292
  process.exitCode = await modulePullDataCommand({
24343
25293
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
@@ -24489,6 +25439,10 @@ program.command("query").description(
24489
25439
  ).option(
24490
25440
  "--stream",
24491
25441
  "Call .stream() instead of .execute() \u2014 prints rows incrementally as JSON-lines (one row per line). Lets you see progress on big queries + Ctrl-C cleanly midway. Mutually exclusive with --explain and --repl."
25442
+ ).option(
25443
+ "--install <id>",
25444
+ "The install the key must belong to: reads that install's key list with your CLI login (one request, 1.5 s), prints `\u2192 install #<id> (key checked)` and refuses (exit 1) when DASHWISE_API_KEY is not an active key of that install or the list cannot be read.",
25445
+ parseIntOption
24492
25446
  ).action(
24493
25447
  async (cmdOpts) => {
24494
25448
  process.exitCode = await queryCommand({
@@ -24498,16 +25452,22 @@ program.command("query").description(
24498
25452
  ...cmdOpts.repl !== void 0 ? { repl: cmdOpts.repl } : {},
24499
25453
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
24500
25454
  ...cmdOpts.explain !== void 0 ? { explain: cmdOpts.explain } : {},
24501
- ...cmdOpts.stream !== void 0 ? { stream: cmdOpts.stream } : {}
25455
+ ...cmdOpts.stream !== void 0 ? { stream: cmdOpts.stream } : {},
25456
+ ...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {}
24502
25457
  });
24503
25458
  }
24504
25459
  );
24505
25460
  program.command("run [cmd...]").description(
24506
- "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`."
24507
- ).option("--dir <path>", "Project root containing `module.json` (default: current directory).").action(async (cmdArgs, cmdOpts) => {
25461
+ "Run a command with the module's Dashwise env injected \u2014 resolves DASHWISE_API_URL + DASHWISE_API_KEY through the same chain as `dashwise query` (explicit env \u2192 .env.local \u2192 the cached dwk_ key) and spawns the command with them overlaid, inheriting stdio and propagating the exit code. Names its target first (`\u2192 install #<id> (\u2026)`, at most one lookup request); `--install <id>` refuses a key of any other install. Example: `dashwise run --install 42 -- node seed.mjs`."
25462
+ ).option("--dir <path>", "Project root containing `module.json` (default: current directory).").option(
25463
+ "--install <id>",
25464
+ "The install the key must belong to. Reads that install's key list with your CLI login (one request, 1.5 s), prints `\u2192 install #<id> (key checked)` and refuses (exit 1, nothing run) when DASHWISE_API_KEY is not an active key of that install or the list cannot be read.",
25465
+ parseIntOption
25466
+ ).action(async (cmdArgs, cmdOpts) => {
24508
25467
  const { runCommand: runCommand2 } = await Promise.resolve().then(() => (init_run(), run_exports));
24509
25468
  process.exitCode = await runCommand2(cmdArgs ?? [], {
24510
- ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
25469
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
25470
+ ...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {}
24511
25471
  });
24512
25472
  });
24513
25473
  program.command("pull").description(
@@ -24634,13 +25594,17 @@ envCmd.command("export").description(
24634
25594
  ).option(
24635
25595
  "--key <dwk_value>",
24636
25596
  "Use a previously-minted API key (paste plaintext). (Mutually exclusive with --new-key.)"
25597
+ ).option(
25598
+ "--with-secret-names",
25599
+ "Also name each secret declared in module.json runtime.secrets[] as a commented `# KEY=` line (a comment naming the command for railway/vercel/fly; stderr for json). Never a value: values never leave Dashwise; set them on your host."
24637
25600
  ).action(
24638
25601
  async (cmdOpts) => {
24639
25602
  process.exitCode = await envExportCommand({
24640
25603
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
24641
25604
  ...cmdOpts.format !== void 0 ? { format: cmdOpts.format } : {},
24642
25605
  ...cmdOpts.newKey !== void 0 ? { newKey: cmdOpts.newKey } : {},
24643
- ...cmdOpts.key !== void 0 ? { key: cmdOpts.key } : {}
25606
+ ...cmdOpts.key !== void 0 ? { key: cmdOpts.key } : {},
25607
+ ...cmdOpts.withSecretNames ? { withSecretNames: true } : {}
24644
25608
  });
24645
25609
  }
24646
25610
  );
@@ -24666,7 +25630,10 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
24666
25630
  ).option(
24667
25631
  "--timeout <seconds>",
24668
25632
  "Max seconds to wait for an async build before giving up polling (default 360)."
24669
- ).option("--json", "Emit the final build result as machine-readable JSON.").action(
25633
+ ).option("--json", "Emit the final build result as machine-readable JSON.").option(
25634
+ "--strict",
25635
+ "Refuse to publish when module.json is not an additive evolution of the released version (the `module diff --check` rules), or when that check cannot run. Without it the check only warns."
25636
+ ).action(
24670
25637
  async (cmdOpts) => {
24671
25638
  const timeoutSec = cmdOpts.timeout !== void 0 ? Number(cmdOpts.timeout) : void 0;
24672
25639
  process.exitCode = await modulePublishCommand({
@@ -24682,19 +25649,23 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
24682
25649
  // commander maps `--no-wait` to `wait === false`.
24683
25650
  ...cmdOpts.wait === false ? { noWait: true } : {},
24684
25651
  ...timeoutSec !== void 0 && Number.isFinite(timeoutSec) ? { timeoutSec } : {},
24685
- ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
25652
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
25653
+ ...cmdOpts.strict !== void 0 ? { strict: cmdOpts.strict } : {}
24686
25654
  });
24687
25655
  }
24688
25656
  );
24689
- moduleCmd.command("install [slug]").alias("deploy").description(
24690
- 'Install (deploy) a published module into a workspace \u2014 provisions the runtime and prints the public URL. Upgrades/repairs an existing production install idempotently. When only a dev install exists it asks whether to install alongside it (--keep-dev-install) or replace it (--replace-dev-install), rather than silently landing on the dev row. Exits 3 \u2014 "deployed, setup pending" \u2014 when a required `uses` slot is still unbound: the deploy succeeded but the module throws on its first data call until an admin binds it.'
25657
+ moduleCmd.command("install [slug]").alias("deploy").alias("upgrade").description(
25658
+ "Install (deploy) a published module into a workspace \u2014 provisions the runtime and prints the public URL. Upgrades/repairs an existing production install idempotently: an upgrade first prints what it does to the copy's data (tables and fields it moves to the Trash or converts, with their counts), asks once, and sends the preview's drift_hash. A move that loses data is accepted only by a person: the one question in a terminal, or --accept-data-loss <drift_hash> with the hash the preview printed. --yes never accepts data loss; with no terminal, or inside an AI agent session, a lossy move is refused and the refusal prints the re-run line. When only a dev install exists it asks whether to install alongside it (--keep-dev-install) or replace it (--replace-dev-install), rather than silently landing on the dev row. Exits 3 \u2014 \"deployed, setup pending\" \u2014 when a required `uses` slot is still unbound: the deploy succeeded but the module throws on its first data call until an admin binds it."
24691
25659
  ).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--version <semver>", "Install a specific published version (default: latest).").option("--force", "Proceed even when the upgrade breaks consumer modules (UPGRADE_BREAKS_CONSUMERS).").option(
24692
25660
  "--keep-dev-install",
24693
25661
  "Install production ALONGSIDE the workspace's kind=local dev install, leaving local dev working. Non-destructive. Backends >= 2026-07-15 keep a separate live-install slot per kind group, so the two coexist."
24694
25662
  ).option(
24695
25663
  "--replace-dev-install",
24696
25664
  "Destroy the workspace's kind=local dev install first, then fresh production install. Revokes the cached local-dev credentials; on backends >= 2026-07-15 local dev can be re-provisioned afterwards (dashwise module reset-dev-install, or dashwise init <slug> --workspace <ws>) and will coexist with the production install. On OLDER backends this is one-way \u2014 prefer --keep-dev-install."
24697
- ).option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("-y, --yes", "Skip the confirmation prompt (for CI / scripted use).").option("--json", "Emit the final result as machine-readable JSON.").action(
25665
+ ).option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("-y, --yes", "Skip the confirmation prompt (for CI / scripted use). Never accepts data loss: see --accept-data-loss.").option(
25666
+ "--accept-data-loss <drift_hash>",
25667
+ "Accept the data loss of this upgrade: the drift_hash its preview printed. Only that exact hash accepts it (a person decides; an agent stops and reports the hash)."
25668
+ ).option("--json", "Emit the final result as machine-readable JSON.").action(
24698
25669
  async (slug, cmdOpts) => {
24699
25670
  process.exitCode = await moduleInstallCommand(slug, {
24700
25671
  ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
@@ -24704,6 +25675,7 @@ moduleCmd.command("install [slug]").alias("deploy").description(
24704
25675
  ...cmdOpts.replaceDevInstall !== void 0 ? { replaceDevInstall: cmdOpts.replaceDevInstall } : {},
24705
25676
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
24706
25677
  ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
25678
+ ...cmdOpts.acceptDataLoss !== void 0 ? { acceptDataLoss: cmdOpts.acceptDataLoss } : {},
24707
25679
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
24708
25680
  });
24709
25681
  }
@@ -25000,7 +25972,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
25000
25972
  }
25001
25973
  );
25002
25974
  function getVersion() {
25003
- return "0.24.0";
25975
+ return "0.26.0";
25004
25976
  }
25005
25977
  function parseIntOption(value) {
25006
25978
  const n = Number.parseInt(value, 10);
@@ -25016,6 +25988,7 @@ function collectRepeated(value, previous) {
25016
25988
  // src/bin.ts
25017
25989
  (async () => {
25018
25990
  try {
25991
+ await installEnvProxy();
25019
25992
  await program.parseAsync(process.argv);
25020
25993
  } catch (err) {
25021
25994
  if (err instanceof ExcessArgumentsError) {