@dashai/cli 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -3,7 +3,7 @@ import pc from 'picocolors';
3
3
  import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, watch, readdirSync, chmodSync, renameSync, lstatSync, mkdtempSync, statSync } from 'fs';
4
4
  import { hostname, tmpdir, platform, homedir } from 'os';
5
5
  import { resolve, join, relative, dirname, basename, win32, sep } from 'path';
6
- import { spawn, spawnSync } from 'child_process';
6
+ import { execFileSync, spawn, spawnSync } from 'child_process';
7
7
  import { intro, log, select, isCancel, cancel, spinner, outro, text, confirm, multiselect } from '@clack/prompts';
8
8
  import { Command, Option } from 'commander';
9
9
  import open from 'open';
@@ -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;
@@ -1406,13 +1412,13 @@ async function resolveDevPort(requested) {
1406
1412
  }
1407
1413
  async function isPortFree(port) {
1408
1414
  const { createServer } = await import('net');
1409
- return new Promise((resolve14) => {
1415
+ return new Promise((resolve15) => {
1410
1416
  const server = createServer();
1411
1417
  server.once("error", () => {
1412
- resolve14(false);
1418
+ resolve15(false);
1413
1419
  });
1414
1420
  server.once("listening", () => {
1415
- server.close(() => resolve14(true));
1421
+ server.close(() => resolve15(true));
1416
1422
  });
1417
1423
  server.listen(port, "0.0.0.0");
1418
1424
  });
@@ -1493,6 +1499,62 @@ var init_dev = __esm({
1493
1499
  PORT_PROBE_LIMIT = 10;
1494
1500
  }
1495
1501
  });
1502
+ function resolveSdkPackageJson(projectRoot) {
1503
+ let dir = projectRoot;
1504
+ for (; ; ) {
1505
+ const candidate = join(dir, "node_modules", "@dashai", "sdk", "package.json");
1506
+ if (existsSync(candidate)) return candidate;
1507
+ const parent = dirname(dir);
1508
+ if (parent === dir) return null;
1509
+ dir = parent;
1510
+ }
1511
+ }
1512
+ function readInstalledSdk(projectRoot) {
1513
+ const pkgPath = resolveSdkPackageJson(projectRoot);
1514
+ if (pkgPath === null) return null;
1515
+ try {
1516
+ const parsed = JSON.parse(readFileSync(pkgPath, "utf-8"));
1517
+ if (typeof parsed.version !== "string" || parsed.version.length === 0) return null;
1518
+ return { version: parsed.version };
1519
+ } catch {
1520
+ return null;
1521
+ }
1522
+ }
1523
+ function compareVersions(a, b) {
1524
+ const parse = (v) => {
1525
+ const [core = "", pre] = v.trim().replace(/^[^0-9]*/, "").split("-", 2);
1526
+ const nums = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
1527
+ while (nums.length < 3) nums.push(0);
1528
+ return [nums.slice(0, 3), pre !== void 0 && pre.length > 0];
1529
+ };
1530
+ const [an, apre] = parse(a);
1531
+ const [bn, bpre] = parse(b);
1532
+ for (let i = 0; i < 3; i++) {
1533
+ const d = (an[i] ?? 0) - (bn[i] ?? 0);
1534
+ if (d !== 0) return d;
1535
+ }
1536
+ if (apre === bpre) return 0;
1537
+ return apre ? -1 : 1;
1538
+ }
1539
+ function satisfiesFloor(version, floor) {
1540
+ return compareVersions(version, floor) >= 0;
1541
+ }
1542
+ function checkSdkFloor(projectRoot, floor) {
1543
+ const installed = readInstalledSdk(projectRoot);
1544
+ if (installed === null) return { ok: true, installed: null, floor };
1545
+ return {
1546
+ ok: satisfiesFloor(installed.version, floor),
1547
+ installed: installed.version,
1548
+ floor
1549
+ };
1550
+ }
1551
+ var USES_SDK_FLOOR, SCAFFOLD_SDK_RANGE;
1552
+ var init_sdk_version = __esm({
1553
+ "src/lib/sdk-version.ts"() {
1554
+ USES_SDK_FLOOR = "2.5.0";
1555
+ SCAFFOLD_SDK_RANGE = "^2.5.0";
1556
+ }
1557
+ });
1496
1558
  async function selectWorkspace(opts) {
1497
1559
  const workspaces = await apiRequest(
1498
1560
  "GET",
@@ -1551,62 +1613,6 @@ var init_workspaces = __esm({
1551
1613
  init_api_client();
1552
1614
  }
1553
1615
  });
1554
- function resolveSdkPackageJson(projectRoot) {
1555
- let dir = projectRoot;
1556
- for (; ; ) {
1557
- const candidate = join(dir, "node_modules", "@dashai", "sdk", "package.json");
1558
- if (existsSync(candidate)) return candidate;
1559
- const parent = dirname(dir);
1560
- if (parent === dir) return null;
1561
- dir = parent;
1562
- }
1563
- }
1564
- function readInstalledSdk(projectRoot) {
1565
- const pkgPath = resolveSdkPackageJson(projectRoot);
1566
- if (pkgPath === null) return null;
1567
- try {
1568
- const parsed = JSON.parse(readFileSync(pkgPath, "utf-8"));
1569
- if (typeof parsed.version !== "string" || parsed.version.length === 0) return null;
1570
- return { version: parsed.version };
1571
- } catch {
1572
- return null;
1573
- }
1574
- }
1575
- function compareVersions(a, b) {
1576
- const parse = (v) => {
1577
- const [core = "", pre] = v.trim().replace(/^[^0-9]*/, "").split("-", 2);
1578
- const nums = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
1579
- while (nums.length < 3) nums.push(0);
1580
- return [nums.slice(0, 3), pre !== void 0 && pre.length > 0];
1581
- };
1582
- const [an, apre] = parse(a);
1583
- const [bn, bpre] = parse(b);
1584
- for (let i = 0; i < 3; i++) {
1585
- const d = (an[i] ?? 0) - (bn[i] ?? 0);
1586
- if (d !== 0) return d;
1587
- }
1588
- if (apre === bpre) return 0;
1589
- return apre ? -1 : 1;
1590
- }
1591
- function satisfiesFloor(version, floor) {
1592
- return compareVersions(version, floor) >= 0;
1593
- }
1594
- function checkSdkFloor(projectRoot, floor) {
1595
- const installed = readInstalledSdk(projectRoot);
1596
- if (installed === null) return { ok: true, installed: null, floor };
1597
- return {
1598
- ok: satisfiesFloor(installed.version, floor),
1599
- installed: installed.version,
1600
- floor
1601
- };
1602
- }
1603
- var USES_SDK_FLOOR, SCAFFOLD_SDK_RANGE;
1604
- var init_sdk_version = __esm({
1605
- "src/lib/sdk-version.ts"() {
1606
- USES_SDK_FLOOR = "2.5.0";
1607
- SCAFFOLD_SDK_RANGE = "^2.5.0";
1608
- }
1609
- });
1610
1616
 
1611
1617
  // src/lib/scaffold-locks/simple.json
1612
1618
  var simple_default;
@@ -1634,7 +1640,7 @@ var init_simple = __esm({
1634
1640
  typescript: "^5.5.0"
1635
1641
  },
1636
1642
  engines: {
1637
- node: "22.x"
1643
+ node: "22.x || 24.x"
1638
1644
  }
1639
1645
  },
1640
1646
  "node_modules/@dashai/sdk": {
@@ -2679,7 +2685,7 @@ var init_custom = __esm({
2679
2685
  typescript: "^5.5.0"
2680
2686
  },
2681
2687
  engines: {
2682
- node: "22.x"
2688
+ node: "22.x || 24.x"
2683
2689
  }
2684
2690
  },
2685
2691
  "node_modules/@alloc/quick-lru": {
@@ -9990,7 +9996,7 @@ function manifestJson(opts) {
9990
9996
  // everything on the default — a warning on every column just trains
9991
9997
  // people to click through warnings.
9992
9998
  fields: [
9993
- { slug: "title", type: "text", human_edit: "open" },
9999
+ { slug: "title", type: "text", primary: true, human_edit: "open" },
9994
10000
  { slug: "done", type: "boolean", human_edit: "guarded" }
9995
10001
  ]
9996
10002
  }
@@ -10017,9 +10023,10 @@ function packageJson(opts) {
10017
10023
  version: "0.0.1",
10018
10024
  private: true,
10019
10025
  type: "module",
10020
- engines: { node: "22.x" },
10026
+ engines: { node: NODE_ENGINES },
10021
10027
  scripts: {
10022
- dev: "dashwise module dev",
10028
+ dev: "dashwise dev",
10029
+ "dev:watch": "dashwise module dev",
10023
10030
  "dev:next": "next dev",
10024
10031
  "dashwise:generate": "dashwise module generate",
10025
10032
  typecheck: "tsc --noEmit",
@@ -10235,7 +10242,7 @@ function appPageTsx(opts) {
10235
10242
  Module slug: <code>${escapeHtml(opts.slug)}</code>
10236
10243
  </p>
10237
10244
  <p style={{ marginTop: 24, color: '#64748b', fontSize: 14 }}>
10238
- Run <code>dashwise module dev</code> to start the watch loop.
10245
+ Run <code>npm run dev</code> to start the app and the watch loop.
10239
10246
  <br />
10240
10247
  Edit <code>module.json</code> to add tables; regenerated types
10241
10248
  land in <code>@dashai/generated</code>.
@@ -10269,7 +10276,7 @@ debugger to Next), use the legacy two-terminal flow:
10269
10276
 
10270
10277
  \`\`\`bash
10271
10278
  dashwise module generate # produces node_modules/@dashai/generated/
10272
- dashwise module dev # watch module.json + auto-regenerate
10279
+ npm run dev:watch # watch module.json + auto-regenerate
10273
10280
  npm run dev:next # plain Next.js dev server (in another terminal)
10274
10281
  \`\`\`
10275
10282
 
@@ -10314,7 +10321,7 @@ debugger to Next), use the legacy two-terminal flow:
10314
10321
 
10315
10322
  \`\`\`bash
10316
10323
  dashwise module generate # produces node_modules/@dashai/generated/
10317
- dashwise module dev # watch + regen on module.json save (terminal 1)
10324
+ npm run dev:watch # watch + regen on module.json save (terminal 1)
10318
10325
  npm run dev:next # next dev on :3000 (terminal 2)
10319
10326
  \`\`\`
10320
10327
 
@@ -11119,9 +11126,10 @@ function packageJsonCustom(opts) {
11119
11126
  version: "0.0.1",
11120
11127
  private: true,
11121
11128
  type: "module",
11122
- engines: { node: "22.x" },
11129
+ engines: { node: NODE_ENGINES },
11123
11130
  scripts: {
11124
- dev: "dashwise module dev",
11131
+ dev: "dashwise dev",
11132
+ "dev:watch": "dashwise module dev",
11125
11133
  "dev:next": "next dev",
11126
11134
  "dashwise:generate": "dashwise module generate",
11127
11135
  typecheck: "tsc --noEmit",
@@ -12206,7 +12214,9 @@ function claudeMd(opts) {
12206
12214
  Instructions for AI coding agents (Claude Code, Cursor, etc.) working
12207
12215
  in this Dashwise module. Read this before making changes.
12208
12216
 
12209
- ${opts.cloudReady === false ? "" : cloudAgentInstructions()}
12217
+ Every table declares exactly one primary field (the row's name everywhere in Dashwise).
12218
+
12219
+ ${opts.cloudReady === false ? "" : cloudAgentInstructions() + "\n" + claudeCloudNote(opts.apiUrl)}
12210
12220
  ## What this module is
12211
12221
 
12212
12222
  A Dashwise ${isCustom ? "**custom Next.js** module (full app runtime, machine-class)" : "**hand-authored simple** module (isolate runtime)"} authored against \`@dashai/sdk\`.
@@ -12350,17 +12360,18 @@ which calls the schema endpoints and rewrites the local
12350
12360
  Common commands:
12351
12361
 
12352
12362
  \`\`\`
12353
- dashwise table create <slug> --field <slug:type[:required]>
12363
+ dashwise table create <slug> --field title:text:primary
12354
12364
  dashwise table delete <slug>
12355
12365
  dashwise field add <table> <slug:type[:required]>
12356
12366
  dashwise field remove <table> <slug>
12357
12367
  dashwise field set-type <table> <slug> <type>
12358
12368
  \`\`\`
12359
12369
 
12360
- Allowed field types (18): \`text\`, \`long_text\`, \`number\`, \`boolean\`,
12370
+ Allowed field types (21): \`text\`, \`long_text\`, \`number\`, \`boolean\`,
12361
12371
  \`date\`, \`datetime\`, \`single_select\`, \`multi_select\`,
12362
12372
  \`link_row\`, \`file\`, \`multi_file\`, \`email\`, \`url\`,
12363
- \`phone\`, \`formula\`, \`ai\`, \`uuid\`, \`jsonb\`.
12373
+ \`phone\`, \`formula\`, \`ai\`, \`uuid\`, \`jsonb\`,
12374
+ \`rating\`, \`duration\`, \`autonumber\`.
12364
12375
 
12365
12376
  ### \`human_edit\` \u2014 who may hand-edit each column
12366
12377
 
@@ -12614,7 +12625,7 @@ what tables it owns.
12614
12625
 
12615
12626
  | Table | Columns | Notes |
12616
12627
  |---|---|---|
12617
- | \`items\` | \`title:text\`, \`done:boolean\` | seeded example \u2014 edit or replace; \`dashwise table create <slug> --field <slug:type>\` adds more |
12628
+ | \`items\` | \`title:text:primary\`, \`done:boolean\` | seeded example \u2014 edit or replace; \`dashwise table create <slug> --field title:text:primary\` adds more |
12618
12629
 
12619
12630
  ## API / contract
12620
12631
 
@@ -12742,21 +12753,153 @@ primary_region = "iad"
12742
12753
  `;
12743
12754
  }
12744
12755
  function cloudAgentInstructions() {
12745
- return `## Cloud workflow
12756
+ return `## Default way of working
12757
+
12758
+ These are defaults. When the person you work for says otherwise (in the chat,
12759
+ or in their own \`CLAUDE.md\` / \`AGENTS.md\`), follow them instead.
12760
+
12761
+ - Assume the person is not a developer. Before and after each step, say in one
12762
+ or two plain sentences what you are doing and why, for example "I'm saving
12763
+ your changes so Dashwise can see them." When a term like branch or pull
12764
+ request is unavoidable, explain it in a few words.
12765
+ - You handle git. Get the finished work onto the repository's default branch
12766
+ (usually \`main\`) yourself, so the person, Dashwise and the next agent all
12767
+ see the latest version. The person needs no git, branch or pull request
12768
+ knowledge:
12769
+ - Working locally on the person's computer: commit on the default branch and push it.
12770
+ - Working in the cloud (Codex cloud, Claude Code on the web): if your tool
12771
+ lets you push to the default branch or merge your own pull request, do it
12772
+ once the checks pass. If it only lets you push a branch, push it and tell the
12773
+ person in plain words that the changes are saved and that Release in
12774
+ Dashwise includes them. Don't ask them to merge anything.
12775
+ - Never force-push and never rewrite the default branch's history.
12776
+ - The person only confirms Release and Take live in Dashwise. When the work is
12777
+ pushed, tell them it is ready: in Dashwise open the module, click Release,
12778
+ then Take live.
12779
+
12780
+ ## Cloud workflow
12746
12781
 
12747
12782
  - Read \`module.json\`, the task and \`spec.md\` first. Preserve module identity and permission checks.
12748
- - Use Node 22 in the provider environment (\`.nvmrc\` / \`.node-version\`). From the repository root run \`bash scripts/cloud-setup.sh\`.
12783
+ - Use Node 22 or 24 (\`.nvmrc\` prefers 22; Codex's default 24 works too). From the repository root run \`bash scripts/cloud-setup.sh\`.
12749
12784
  - 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.
12750
12785
  - Update \`spec.md\` for behavior changes and \`agent-log.md\` in the same commit. Read \`CLAUDE.md\` for SDK, authorization and UI conventions.
12751
- - 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.
12752
- - 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.
12786
+ - \`npm run smoke:production\` starts the built app on a local loopback port. If a cloud sandbox refuses that port, rerun the command with the network permission; never skip the check.
12787
+ - Report what you changed, the pushed SHA and the checks you ran, including failures, in plain words.
12788
+ - 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.
12753
12789
  - Never automatically release, publish, deploy or take an installation live. Release and Live require an explicit human action.
12790
+ - Even when the person says you may release, you can't do it for them: tell them in one sentence where to click (in Dashwise open the module, click Release, then Take live). Never ask for a \`dashwise login\` or a token just to release.
12791
+
12792
+ ## If you can't see the repository
12793
+
12794
+ Connecting GitHub in the AI tool is not enough for a private repository. Tell
12795
+ the person, in plain words, to also install the tool's GitHub app on the GitHub
12796
+ organisation that owns this repository and give it access to this repository
12797
+ (an organisation owner may have to approve it), then start the task again:
12798
+
12799
+ - Claude: the Claude GitHub app, https://github.com/apps/claude
12800
+ - Codex: the ChatGPT Codex Connector, https://github.com/apps/chatgpt-codex-connector
12801
+
12802
+ ## Dashwise login (only for full development)
12803
+
12804
+ The checks (\`npm run typecheck\`, \`npm test\`, \`npm run build\`,
12805
+ \`npm run smoke:production\`) need no login. Full Dashwise development does:
12806
+ \`npm run dev\` (\`dashwise dev\`: the app plus the watcher), \`dashwise module
12807
+ generate\` and the schema commands. When the task needs them:
12808
+
12809
+ - If \`dashwise\` is not installed, run \`npm install -g @dashai/cli@latest\`.
12810
+ - Ask the person once, in plain words, for example: "To run your module
12811
+ against Dashwise I need to sign in as you. I'll show you a code and a link;
12812
+ open the link and approve." Then run \`dashwise login --no-prompt\` and show
12813
+ them the code and link it prints. The login acts with the person's own rights.
12814
+ - Keep the login outside the repository. In the cloud, run CLI commands with
12815
+ \`XDG_CONFIG_HOME=/tmp/dashwise-config\` so the saved login lands in
12816
+ \`/tmp\`, never in the checkout.
12817
+ - Never write a token into a script, a file in the repository, a commit, an
12818
+ environment setup script or snapshot, or a log.
12819
+ - In the cloud, run CLI commands with \`NODE_USE_ENV_PROXY=1\` so the CLI
12820
+ uses the environment's network proxy (newer CLI versions do this on their own).
12821
+ - A cloud environment forgets the login when it restarts; ask again only when
12822
+ a command reports that the login is missing.
12823
+ `;
12824
+ }
12825
+ function companyApiHost(apiUrl) {
12826
+ if (!apiUrl) return null;
12827
+ try {
12828
+ const host = new URL(apiUrl).host;
12829
+ return host in HOSTED_API_HOSTS ? host : null;
12830
+ } catch {
12831
+ return null;
12832
+ }
12833
+ }
12834
+ function apiHostLine(apiUrl) {
12835
+ const host = companyApiHost(apiUrl);
12836
+ 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";
12837
+ }
12838
+ function codexEnvironmentInstructions(apiUrl) {
12839
+ const host = companyApiHost(apiUrl);
12840
+ const apiUrlValue = host ? `https://${host}` : "https://<the company API host above>";
12841
+ return `## Codex cloud environment
12842
+
12843
+ When the person asks you to set up the Codex cloud environment for this
12844
+ repository, use exactly these settings. Show each one to the person and say in
12845
+ one sentence what it is for.
12846
+
12847
+ | Setting | Value |
12848
+ |---|---|
12849
+ | Node.js | 22 or 24 (24 is Codex's default and works; \`.nvmrc\` prefers 22) |
12850
+ | Setup script | \`if [ -f scripts/cloud-setup.sh ]; then bash scripts/cloud-setup.sh; else echo "No Dashwise template yet, setup skipped"; fi\` |
12851
+ | Agent internet access | On: keep the "package managers" preset and add only the company's Dashwise API host (below) |
12852
+ | Environment variables | \`DASHWISE_API_URL=${apiUrlValue}\`, \`NODE_USE_ENV_PROXY=1\` |
12853
+ | Secrets | none |
12854
+
12855
+ Set the network access before you start the setup, so the first run can
12856
+ already install packages. The preset covers npm and GitHub; the one added
12857
+ domain is:
12858
+
12859
+ - ${apiHostLine(apiUrl)}
12860
+
12861
+ The module and the CLI talk to the company's Dashwise API. Do not add other
12862
+ Dashwise hosts. If the environment offers no preset, allow exactly
12863
+ ${CLOUD_ALLOWED_HOSTS.map((h) => `\`${h}\``).join(", ")} plus that host.
12864
+
12865
+ The setup script checks Node 22 or 24 and installs the exact dependencies with
12866
+ \`npm ci\`. The guard lets a first run pass while the repository holds only a
12867
+ README.
12868
+
12869
+ Never add secrets: no \`dwk_\` module key, no \`dwc_\` CLI token and no password,
12870
+ and never put a \`dashwise login\` into the setup script. Delivering code needs
12871
+ none of them; for full development see "Dashwise login".
12872
+
12873
+ To check the environment, run \`npm run typecheck\`, \`npm test\`,
12874
+ \`npm run build\` and \`npm run smoke:production\`. If one fails because of a
12875
+ setting, fix the setting; otherwise report the failure in plain words.
12876
+
12877
+ If the repository has no \`package.json\` yet (only a README), skip these
12878
+ checks: the setup script already skipped installing, and the first task adds
12879
+ the template. Tell the person the settings are ready.
12880
+
12881
+ When the checks pass, or were skipped for a README-only repository, tell the
12882
+ person: "The environment works. Click Publish to save it for your next tasks."
12883
+ Publishing is their click.
12884
+ `;
12885
+ }
12886
+ function claudeCloudNote(apiUrl) {
12887
+ const host = companyApiHost(apiUrl);
12888
+ return `## Claude cloud
12889
+
12890
+ Claude Code on the web has no environment setup chat. It reads this repository
12891
+ and \`CLAUDE.md\`, and \`.claude/settings.json\` runs \`scripts/cloud-setup.sh\`
12892
+ when a cloud session starts. Node 22 or 24 both work. If
12893
+ the environment has a network setting, allow the same domains as for Codex:
12894
+ ${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.
12754
12895
  `;
12755
12896
  }
12756
12897
  function agentsMd(opts) {
12757
12898
  return `# ${opts.name}: agent instructions
12758
12899
 
12759
- ` + cloudAgentInstructions();
12900
+ Every table declares exactly one primary field (the row's name everywhere in Dashwise).
12901
+
12902
+ ` + cloudAgentInstructions() + "\n" + codexEnvironmentInstructions(opts.apiUrl) + "\n" + claudeCloudNote(opts.apiUrl);
12760
12903
  }
12761
12904
  function nodeVersion() {
12762
12905
  return "22\n";
@@ -12778,9 +12921,12 @@ fi
12778
12921
  repo_root="$(cd -- "$(dirname -- "\${BASH_SOURCE[0]}")/.." && pwd)"
12779
12922
  cd "$repo_root"
12780
12923
  fail() { printf 'cloud-setup: %s\\n' "$1" >&2; exit 1; }
12781
- command -v node >/dev/null 2>&1 || fail 'Node 22 is required. Select Node 22 in your cloud environment, then retry.'
12782
- [[ "$(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.'
12783
- command -v npm >/dev/null 2>&1 || fail 'npm is required with Node 22.'
12924
+ 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.'
12925
+ case "$(node -p 'process.versions.node.split(".")[0]')" in
12926
+ 22|24) ;;
12927
+ *) 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.' ;;
12928
+ esac
12929
+ command -v npm >/dev/null 2>&1 || fail 'npm is required with Node 22 or 24.'
12784
12930
  [[ -f package-lock.json && -f package.json ]] || fail 'Missing package.json or committed package-lock.json. Run this from a complete repository checkout.'
12785
12931
 
12786
12932
  # Only the Claude startup/resume path reuses an unchanged install. Explicit
@@ -12788,15 +12934,21 @@ command -v npm >/dev/null 2>&1 || fail 'npm is required with Node 22.'
12788
12934
  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"));')"
12789
12935
  stamp_file="node_modules/.dashwise-cloud-setup"
12790
12936
  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
12791
- printf 'cloud-setup: dependencies already match Node 22 and the lockfile.\\n'
12937
+ printf 'cloud-setup: dependencies already match this Node version and the lockfile.\\n'
12792
12938
  exit 0
12793
12939
  fi
12794
12940
  # Keep install output out of session logs: third-party lifecycle scripts may
12795
12941
  # echo environment variables. npm's own detailed logs stay in its local cache.
12796
12942
  rm -f "$stamp_file"
12797
- 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.'
12943
+ # Some sandboxes mount HOME read-only; npm then cannot write its cache.
12944
+ npm_cache="\${npm_config_cache:-\${HOME:-}/.npm}"
12945
+ if ! { mkdir -p "$npm_cache" 2>/dev/null && [[ -w "$npm_cache" ]]; }; then
12946
+ export npm_config_cache="\${TMPDIR:-/tmp}/npm-cache"
12947
+ printf 'cloud-setup: %s is not writable, using %s for the npm cache.\\n' "$npm_cache" "$npm_config_cache"
12948
+ fi
12949
+ 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.'
12798
12950
  printf '%s' "$stamp" > "$stamp_file"
12799
- printf 'cloud-setup: dependencies installed with Node 22.\\n'
12951
+ printf 'cloud-setup: dependencies installed with Node %s.\\n' "$(node -p 'process.versions.node')"
12800
12952
  `;
12801
12953
  }
12802
12954
  function claudeSettingsJson() {
@@ -12952,7 +13104,7 @@ function cloudReadme() {
12952
13104
  return `
12953
13105
  ## Cloud builders
12954
13106
 
12955
- Use Node 22 (the root runtime pins) and run \`bash scripts/cloud-setup.sh\`.
13107
+ Use Node 22 (the root runtime pins; 24 also works) and run \`bash scripts/cloud-setup.sh\`.
12956
13108
  Commit \`package-lock.json\` after any dependency change; setup uses \`npm ci\`.
12957
13109
  Checks: \`npm run typecheck\`, \`npm test\`, \`npm run build\`, \`npm run smoke:production\`.
12958
13110
  The smoke command starts the built server, probes anonymous API denial and stops
@@ -12961,33 +13113,49 @@ The example uses \`lib/items.ts\` directly against the SDK, so a fresh checkout
12961
13113
  can build without authenticated code generation. Keep it aligned with the
12962
13114
  example table, or replace it when you author your schema.
12963
13115
 
12964
- Codex: select Node 22 in the provider environment and use
12965
- \`bash scripts/cloud-setup.sh\` as setup/maintenance. GitHub authorization and
12966
- the cloud environment are configured in the provider; these repo files do
12967
- not create an environment or authorize your account.
13116
+ Codex: \`AGENTS.md\` \u2192 "Codex cloud environment" lists the exact settings
13117
+ (Node 22 or 24, setup script, network, \`DASHWISE_API_URL\`, no secrets);
13118
+ Codex's environment setup chat can follow it, then you click Publish. GitHub
13119
+ authorization is configured in the provider: install the ChatGPT Codex
13120
+ Connector on your GitHub organisation so Codex can see a private repository.
12968
13121
 
12969
13122
  Claude Code web: \`.claude/settings.json\` ships a cloud-only SessionStart
12970
13123
  hook for startup/resume. It calls \`bash "$CLAUDE_PROJECT_DIR"/scripts/cloud-setup.sh --claude\`;
12971
13124
  local sessions skip installation and unchanged cloud installs reuse dependencies.
12972
- Select Node 22 in the environment; do not rely on a setup subshell changing the
13125
+ Select Node 22 or 24 in the environment; do not rely on a setup subshell changing the
12973
13126
  runtime of later agent commands. The same script can be used in the environment
12974
13127
  setup field. Allow \`registry.npmjs.org\` for installs (Claude's Trusted list).
12975
13128
  The [official hook recipe](https://code.claude.com/docs/en/cloud-environments#install-dependencies-with-a-sessionstart-hook)
12976
13129
  explains startup and cache behavior.
12977
13130
 
12978
- Read \`AGENTS.md\` / \`CLAUDE.md\`, work on one branch and push. Release and Live
12979
- remain human actions. No Dashwise token is needed for repository code delivery.
13131
+ Read \`AGENTS.md\` / \`CLAUDE.md\`. By default agents get the work onto the
13132
+ default branch themselves and explain each step in plain words; Release in
13133
+ Dashwise includes work an agent could only push to a branch. Release and Live
13134
+ remain human actions. No Dashwise token is needed for
13135
+ repository code delivery.
12980
13136
  `;
12981
13137
  }
12982
- var DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL, MODULE_SLUG_REGEX;
13138
+ var NODE_ENGINES, DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL, MODULE_SLUG_REGEX, CLOUD_ALLOWED_HOSTS, HOSTED_API_HOSTS;
12983
13139
  var init_scaffold = __esm({
12984
13140
  "src/lib/scaffold.ts"() {
12985
13141
  init_sdk_version();
12986
13142
  init_simple();
12987
13143
  init_custom();
13144
+ NODE_ENGINES = "22.x || 24.x";
12988
13145
  DEPLOY_TARGETS = ["railway", "vercel", "fly"];
12989
13146
  SCAFFOLD_DEFAULT_API_URL = "http://localhost:3000";
12990
13147
  MODULE_SLUG_REGEX = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
13148
+ CLOUD_ALLOWED_HOSTS = [
13149
+ "registry.npmjs.org",
13150
+ "github.com",
13151
+ "api.github.com",
13152
+ "raw.githubusercontent.com",
13153
+ "codeload.github.com"
13154
+ ];
13155
+ HOSTED_API_HOSTS = {
13156
+ "api.dashwise.dev": "app.dashwise.dev",
13157
+ "api.dashwise.net": "app.dashwise.net"
13158
+ };
12991
13159
  }
12992
13160
  });
12993
13161
  function isFreshCloneEntry(name) {
@@ -13065,7 +13233,7 @@ function canAdoptCloudFiles(dir, templatePackage) {
13065
13233
  if (lock.lockfileVersion !== 2 && lock.lockfileVersion !== 3 || !root) {
13066
13234
  return false;
13067
13235
  }
13068
- if (pkg.engines?.node !== "22.x" || !pkg.scripts || !expected.scripts) {
13236
+ if (pkg.engines?.node !== "22.x" && pkg.engines?.node !== expected.engines?.node || !pkg.scripts || !expected.scripts) {
13069
13237
  return false;
13070
13238
  }
13071
13239
  for (const script of ["typecheck", "test", "build", "start", "smoke:production"]) {
@@ -13168,12 +13336,14 @@ async function moduleInitCommand(slugArg, options) {
13168
13336
  const templatePackage = (kind === "custom" ? packageJsonCustom : packageJson)({
13169
13337
  slug});
13170
13338
  const cloudReady = writeMode === "all" || canAdoptCloudFiles(dir, templatePackage);
13339
+ const companyApiUrl = resolveExplicitApiUrl();
13171
13340
  const scaffoldOpts = {
13172
13341
  cloudReady,
13173
13342
  slug,
13174
13343
  name,
13175
13344
  kind,
13176
- ...options.git !== void 0 ? { repositoryUrl: options.git } : {}
13345
+ ...options.git !== void 0 ? { repositoryUrl: options.git } : {},
13346
+ ...companyApiUrl !== null ? { apiUrl: companyApiUrl } : {}
13177
13347
  };
13178
13348
  const scaffoldApiUrl = resolveApiUrl();
13179
13349
  const files = [
@@ -13371,7 +13541,7 @@ async function moduleInitCommand(slugArg, options) {
13371
13541
 
13372
13542
  ${dim(" (or, for the two-terminal flow:")}
13373
13543
  ${dim(" dashwise module generate")} ${dim("# materialize typed wrappers")}
13374
- ${dim(" dashwise module dev")} ${dim("# watch + auto-regenerate")}
13544
+ ${dim(" npm run dev:watch")} ${dim("# watch + auto-regenerate")}
13375
13545
  ${dim(" npm run dev:next # next dev on :3000)")}
13376
13546
  ${devInstall ? `
13377
13547
  ${dim(`Dev installation id=${devInstall.installation.id}; API key expires ${devInstall.api_key_expires_at}.`)}
@@ -14023,6 +14193,16 @@ function printApplySummary(applied) {
14023
14193
  const updated = asArray(applied.tables_updated);
14024
14194
  const removed = asArray(applied.tables_removed);
14025
14195
  success(`Manifest applied to the dev install.`);
14196
+ for (const warning of asArray(applied.warnings)) {
14197
+ const message = strProp(warning, "message");
14198
+ if (!message) continue;
14199
+ const warningCode = strProp(warning, "code");
14200
+ const table = strProp(warning, "table");
14201
+ const field = strProp(warning, "field");
14202
+ const location = table ? `${table}${field ? `.${field}` : ""}` : field;
14203
+ const prefix = [warningCode, location].filter(Boolean).join(" ");
14204
+ warn(`${prefix ? `${code(prefix)}: ` : ""}${message}`);
14205
+ }
14026
14206
  if (created.length === 0 && updated.length === 0 && removed.length === 0) {
14027
14207
  info(dim("No table changes (metadata-only or already in sync)."));
14028
14208
  return;
@@ -14132,6 +14312,76 @@ var init_push = __esm({
14132
14312
  }
14133
14313
  });
14134
14314
 
14315
+ // ../sdk/dist/index.js
14316
+ function primaryDeclarationIssues(table) {
14317
+ const invalid = table.fields.findIndex(
14318
+ (field2) => field2.primary !== void 0 && field2.primary !== true
14319
+ );
14320
+ if (invalid !== -1) {
14321
+ return [
14322
+ { path: ["fields", invalid, "primary"], message: "primary must be literal true or omitted" }
14323
+ ];
14324
+ }
14325
+ const marked = table.fields.filter((field2) => field2.primary === true);
14326
+ if (marked.length !== 1) {
14327
+ return [
14328
+ {
14329
+ path: ["fields"],
14330
+ message: `Table ${table.slug}: mark exactly one field as primary: true (the row's label in links, pickers, titles)`
14331
+ }
14332
+ ];
14333
+ }
14334
+ const field = marked[0];
14335
+ const path = ["fields", table.fields.indexOf(field), "primary"];
14336
+ if (field.type === "formula") {
14337
+ return [{ path, message: "formula fields cannot be the primary of a module table yet" }];
14338
+ }
14339
+ if (!PRIMARY_TYPES.has(field.type)) {
14340
+ return [
14341
+ {
14342
+ path,
14343
+ message: `Table ${table.slug}: mark a field that can be the primary with primary: true (${field.slug} of type ${field.type} cannot be primary)`
14344
+ }
14345
+ ];
14346
+ }
14347
+ return [];
14348
+ }
14349
+ function weakPrimaryWarning(table) {
14350
+ const field = table.fields.find((field2) => field2.primary === true);
14351
+ if (!field || !WEAK_PRIMARY_TYPES.has(field.type)) return void 0;
14352
+ return `${field.slug} is a weak row label; prefer a short text field such as a name or title`;
14353
+ }
14354
+ var PRIMARY_TYPES, WEAK_PRIMARY_TYPES;
14355
+ var init_dist = __esm({
14356
+ "../sdk/dist/index.js"() {
14357
+ PRIMARY_TYPES = /* @__PURE__ */ new Set([
14358
+ "text",
14359
+ "long_text",
14360
+ "number",
14361
+ "date",
14362
+ "datetime",
14363
+ "single_select",
14364
+ "multi_select",
14365
+ "email",
14366
+ "url",
14367
+ "phone",
14368
+ "rating",
14369
+ "duration",
14370
+ "autonumber"
14371
+ ]);
14372
+ WEAK_PRIMARY_TYPES = /* @__PURE__ */ new Set([
14373
+ "single_select",
14374
+ "multi_select",
14375
+ "number",
14376
+ "rating",
14377
+ "duration",
14378
+ "date",
14379
+ "datetime",
14380
+ "autonumber"
14381
+ ]);
14382
+ }
14383
+ });
14384
+
14135
14385
  // src/lib/field-spec.ts
14136
14386
  function parseFieldSpec(spec) {
14137
14387
  const parts = spec.split(":");
@@ -14186,6 +14436,7 @@ function parseFieldSpec(spec) {
14186
14436
  );
14187
14437
  }
14188
14438
  if (flag === "required") result.required = true;
14439
+ if (flag === "primary") result.primary = true;
14189
14440
  }
14190
14441
  }
14191
14442
  if ((type === "single_select" || type === "multi_select") && result.options === void 0) {
@@ -14228,6 +14479,9 @@ function parseFieldJson(json) {
14228
14479
  `--field-json missing or invalid ${quote("type")}. Allowed: ${FIELD_TYPES.join(", ")}.`
14229
14480
  );
14230
14481
  }
14482
+ if (o.primary !== void 0 && o.primary !== true) {
14483
+ throw new FieldSpecError("primary must be literal true or omitted");
14484
+ }
14231
14485
  return o;
14232
14486
  }
14233
14487
  function quote(s) {
@@ -14240,6 +14494,9 @@ var init_field_spec = __esm({
14240
14494
  "text",
14241
14495
  "long_text",
14242
14496
  "number",
14497
+ "rating",
14498
+ "duration",
14499
+ "autonumber",
14243
14500
  "boolean",
14244
14501
  "date",
14245
14502
  "datetime",
@@ -14255,7 +14512,7 @@ var init_field_spec = __esm({
14255
14512
  "ai"
14256
14513
  ];
14257
14514
  SLUG_REGEX = /^[a-z][a-z0-9_]*[a-z0-9]$/;
14258
- KNOWN_FLAGS = /* @__PURE__ */ new Set(["required"]);
14515
+ KNOWN_FLAGS = /* @__PURE__ */ new Set(["required", "primary"]);
14259
14516
  DECIMALS_FLAG_REGEX = /^\d+$/;
14260
14517
  MAX_DECIMAL_PLACES = 10;
14261
14518
  FieldSpecError = class extends Error {
@@ -14276,7 +14533,7 @@ __export(create_exports, {
14276
14533
  });
14277
14534
  async function tableCreateCommand(slug, options) {
14278
14535
  if (!slug) {
14279
- fail("Usage: `dashwise table create <slug> --field <slug>:<type>[:required] [...]`");
14536
+ fail("Usage: `dashwise table create <slug> --field <slug>:<type>[:primary,required] [...]`");
14280
14537
  return 1;
14281
14538
  }
14282
14539
  const fields = [];
@@ -14308,6 +14565,15 @@ async function tableCreateCommand(slug, options) {
14308
14565
  }
14309
14566
  seen.add(f.slug);
14310
14567
  }
14568
+ const table = { slug, fields };
14569
+ const issues = primaryDeclarationIssues(table);
14570
+ if (issues.length > 0) {
14571
+ for (const issue of issues) fail(issue.message);
14572
+ info("Mark the row name explicitly, e.g. --field title:text:primary.");
14573
+ return 1;
14574
+ }
14575
+ const primaryWarning = weakPrimaryWarning(table);
14576
+ if (primaryWarning) warn(primaryWarning);
14311
14577
  let ctx;
14312
14578
  try {
14313
14579
  ctx = loadSchemaCommandContext({
@@ -14400,6 +14666,7 @@ function surfaceFailure(err) {
14400
14666
  }
14401
14667
  var init_create = __esm({
14402
14668
  "src/commands/table/create.ts"() {
14669
+ init_dist();
14403
14670
  init_manifest_io();
14404
14671
  init_api_client();
14405
14672
  init_manifest_merge();
@@ -14619,6 +14886,26 @@ var init_set_type = __esm({
14619
14886
  }
14620
14887
  });
14621
14888
 
14889
+ // src/lib/env-proxy.ts
14890
+ var PROXY_VARS = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
14891
+ function hasProxyEnv(env = process.env) {
14892
+ return PROXY_VARS.some((name) => Boolean(env[name]?.trim()));
14893
+ }
14894
+ async function installEnvProxy(env = process.env) {
14895
+ if (!hasProxyEnv(env)) return false;
14896
+ const undici = await import('undici');
14897
+ undici.setGlobalDispatcher(new undici.EnvHttpProxyAgent());
14898
+ Object.assign(globalThis, {
14899
+ fetch: undici.fetch,
14900
+ FormData: undici.FormData,
14901
+ Headers: undici.Headers,
14902
+ Request: undici.Request,
14903
+ Response: undici.Response
14904
+ });
14905
+ env.NODE_USE_ENV_PROXY ??= "1";
14906
+ return true;
14907
+ }
14908
+
14622
14909
  // src/lib/excess-args.ts
14623
14910
  init_output();
14624
14911
  var SMART_DASH_RE = /^[‒–—―−]/;
@@ -15021,6 +15308,440 @@ function toMe(cached) {
15021
15308
  };
15022
15309
  }
15023
15310
 
15311
+ // src/commands/doctor.ts
15312
+ init_api_client();
15313
+ init_config();
15314
+ init_output();
15315
+ init_sdk_version();
15316
+ var MIN_NODE = [20, 18, 1];
15317
+ var CLOUD_FILES = [
15318
+ "scripts/cloud-setup.sh",
15319
+ ".claude/settings.json",
15320
+ "AGENTS.md",
15321
+ "CLAUDE.md"
15322
+ ];
15323
+ async function doctorCommand(options, deps) {
15324
+ const checks = await runDoctorChecks(resolve(options.dir ?? process.cwd()), deps);
15325
+ const ok = !checks.some((c2) => c2.status === "fail");
15326
+ if (options.json) {
15327
+ out(JSON.stringify({ ok, checks }, null, 2));
15328
+ } else {
15329
+ for (const c2 of checks) {
15330
+ out(`${badge(c2.status)} ${c2.label}: ${c2.detail}`);
15331
+ if (c2.fix) out(` ${dim("\u2192")} ${c2.fix}`);
15332
+ }
15333
+ const fails = checks.filter((c2) => c2.status === "fail").length;
15334
+ const warns = checks.filter((c2) => c2.status === "warn").length;
15335
+ out("");
15336
+ out(
15337
+ fails === 0 && warns === 0 ? pc.green("Everything looks good.") : `${fails} problem${fails === 1 ? "" : "s"}, ${warns} warning${warns === 1 ? "" : "s"}.`
15338
+ );
15339
+ }
15340
+ return ok ? 0 : 1;
15341
+ }
15342
+ async function runDoctorChecks(dir, deps) {
15343
+ const checks = [];
15344
+ const apiUrl = resolveApiUrl();
15345
+ checks.push(targetCheck(apiUrl));
15346
+ const cfg = safeReadConfig2();
15347
+ let signedIn = false;
15348
+ if (!cfg?.token) {
15349
+ checks.push({
15350
+ id: "login",
15351
+ label: "Login",
15352
+ status: "fail",
15353
+ detail: "not signed in",
15354
+ fix: `Run ${code("dashwise login")}.`
15355
+ });
15356
+ checks.push(await connectionOnly(apiUrl));
15357
+ } else {
15358
+ try {
15359
+ const me = await apiRequest("GET", "/api/auth/me");
15360
+ signedIn = true;
15361
+ checks.push({
15362
+ id: "login",
15363
+ label: "Login",
15364
+ status: "ok",
15365
+ detail: `signed in as ${me.email}`
15366
+ });
15367
+ checks.push({
15368
+ id: "connection",
15369
+ label: "Connection",
15370
+ status: "ok",
15371
+ detail: `${apiUrl} answers`
15372
+ });
15373
+ } catch (err) {
15374
+ if (err instanceof ApiError && err.code === "NETWORK_ERROR") {
15375
+ checks.push({
15376
+ id: "login",
15377
+ label: "Login",
15378
+ status: "skip",
15379
+ detail: "not checked, Dashwise is not reachable"
15380
+ });
15381
+ checks.push(unreachable(apiUrl));
15382
+ } else if (err instanceof ApiError && err.status === 401) {
15383
+ checks.push({
15384
+ id: "login",
15385
+ label: "Login",
15386
+ status: "fail",
15387
+ detail: "the saved login is expired or was revoked",
15388
+ fix: `Run ${code("dashwise login")} again.`
15389
+ });
15390
+ checks.push({
15391
+ id: "connection",
15392
+ label: "Connection",
15393
+ status: "ok",
15394
+ detail: `${apiUrl} answers`
15395
+ });
15396
+ } else {
15397
+ checks.push({
15398
+ id: "login",
15399
+ label: "Login",
15400
+ status: "fail",
15401
+ detail: `could not check the login (${err.message})`,
15402
+ fix: `Try again; if it stays, run ${code("dashwise login")}.`
15403
+ });
15404
+ checks.push({
15405
+ id: "connection",
15406
+ label: "Connection",
15407
+ status: "warn",
15408
+ detail: `${apiUrl} answered with an error`
15409
+ });
15410
+ }
15411
+ }
15412
+ }
15413
+ checks.push(nodeCheck(deps.nodeVersion));
15414
+ checks.push(
15415
+ await versionCheck(
15416
+ "cli",
15417
+ "Dashwise CLI",
15418
+ deps.cliVersion,
15419
+ await deps.latestVersion("@dashai/cli"),
15420
+ `Run ${code("npx @dashai/cli@latest <command>")} or ${code("npm install -g @dashai/cli@latest")}.`
15421
+ )
15422
+ );
15423
+ const manifestPath2 = join(dir, "module.json");
15424
+ if (!existsSync(manifestPath2)) {
15425
+ checks.push({
15426
+ id: "module",
15427
+ label: "Module",
15428
+ status: "skip",
15429
+ detail: `no ${code("module.json")} here, so the module checks were skipped`,
15430
+ fix: `Run ${code("dashwise doctor")} inside a module folder to check it too.`
15431
+ });
15432
+ return checks;
15433
+ }
15434
+ let parsed = false;
15435
+ let manifest = null;
15436
+ try {
15437
+ manifest = JSON.parse(readFileSync(manifestPath2, "utf-8"));
15438
+ parsed = true;
15439
+ } catch (err) {
15440
+ checks.push({
15441
+ id: "module",
15442
+ label: "module.json",
15443
+ status: "fail",
15444
+ detail: `cannot be read: ${err.message}`,
15445
+ fix: `Fix the JSON in ${code("module.json")}.`
15446
+ });
15447
+ }
15448
+ const header = manifest !== null && typeof manifest === "object" ? manifest.module : void 0;
15449
+ const slug = typeof header?.slug === "string" ? header.slug : null;
15450
+ if (parsed) checks.push(await manifestCheck(manifest, signedIn));
15451
+ const sdk = readInstalledSdk(dir);
15452
+ checks.push(
15453
+ sdk ? await versionCheck(
15454
+ "sdk",
15455
+ "Dashwise SDK",
15456
+ sdk.version,
15457
+ await deps.latestVersion("@dashai/sdk"),
15458
+ `Run ${code("npm install @dashai/sdk@latest")}.`
15459
+ ) : {
15460
+ id: "sdk",
15461
+ label: "Dashwise SDK",
15462
+ status: "warn",
15463
+ detail: "not installed in this folder",
15464
+ fix: `Run ${code("npm install")}.`
15465
+ }
15466
+ );
15467
+ checks.push(await repoCheck(dir, slug, signedIn, deps));
15468
+ const missing = CLOUD_FILES.filter((f) => !existsSync(join(dir, f)));
15469
+ checks.push(
15470
+ missing.length === 0 ? {
15471
+ id: "cloud-setup",
15472
+ label: "Cloud setup files",
15473
+ status: "ok",
15474
+ detail: "present (cloud agents can set up this module)"
15475
+ } : {
15476
+ id: "cloud-setup",
15477
+ label: "Cloud setup files",
15478
+ status: "warn",
15479
+ detail: `missing: ${missing.join(", ")}`,
15480
+ fix: `Copy them from a fresh ${code("dashwise init")} of the template, so Claude Code web and Codex cloud can set this module up.`
15481
+ }
15482
+ );
15483
+ return checks;
15484
+ }
15485
+ function targetCheck(apiUrl) {
15486
+ const source = process.env.DASHWISE_API_URL?.trim() ? "from DASHWISE_API_URL" : "from your saved login";
15487
+ if (sameOrigin(apiUrl, KNOWN_API_URLS.prod)) {
15488
+ return {
15489
+ id: "target",
15490
+ label: "Target",
15491
+ status: "warn",
15492
+ detail: `PRODUCTION (${apiUrl}, ${source}). Commands here change real company data.`,
15493
+ fix: `To build against dev, run ${code(`dashwise login --api-url ${KNOWN_API_URLS.dev}`)} or set ${code(`DASHWISE_API_URL=${KNOWN_API_URLS.dev}`)}.`
15494
+ };
15495
+ }
15496
+ if (sameOrigin(apiUrl, KNOWN_API_URLS.dev)) {
15497
+ return {
15498
+ id: "target",
15499
+ label: "Target",
15500
+ status: "ok",
15501
+ detail: `dev (${apiUrl}, ${source})`
15502
+ };
15503
+ }
15504
+ return {
15505
+ id: "target",
15506
+ label: "Target",
15507
+ status: "ok",
15508
+ detail: `custom (${apiUrl}, ${source})`
15509
+ };
15510
+ }
15511
+ async function connectionOnly(apiUrl) {
15512
+ try {
15513
+ await apiRequest("GET", "/api/auth/me", void 0, { auth: "public" });
15514
+ } catch (err) {
15515
+ if (err instanceof ApiError && err.code === "NETWORK_ERROR") {
15516
+ return unreachable(apiUrl);
15517
+ }
15518
+ }
15519
+ return {
15520
+ id: "connection",
15521
+ label: "Connection",
15522
+ status: "ok",
15523
+ detail: `${apiUrl} answers`
15524
+ };
15525
+ }
15526
+ function unreachable(apiUrl) {
15527
+ return {
15528
+ id: "connection",
15529
+ label: "Connection",
15530
+ status: "fail",
15531
+ detail: `cannot reach ${apiUrl}`,
15532
+ fix: "Check your network, VPN or proxy, and that the address is right."
15533
+ };
15534
+ }
15535
+ function nodeCheck(version) {
15536
+ const parts = version.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10));
15537
+ let supported = parts.length === 3 && parts.every(Number.isFinite);
15538
+ for (let i = 0; supported && i < 3; i++) {
15539
+ if (parts[i] !== MIN_NODE[i]) {
15540
+ supported = parts[i] > MIN_NODE[i];
15541
+ break;
15542
+ }
15543
+ }
15544
+ return supported ? { id: "node", label: "Node", status: "ok", detail: version } : {
15545
+ id: "node",
15546
+ label: "Node",
15547
+ status: "fail",
15548
+ detail: `${version} is too old`,
15549
+ fix: `Install Node ${MIN_NODE.join(".")} or newer (the template pins Node 22).`
15550
+ };
15551
+ }
15552
+ async function versionCheck(id, label, installed, latest, fix) {
15553
+ if (!latest) {
15554
+ return {
15555
+ id,
15556
+ label,
15557
+ status: "ok",
15558
+ detail: `${installed} (could not look up the newest version)`
15559
+ };
15560
+ }
15561
+ return compareVersions(installed, latest) < 0 ? {
15562
+ id,
15563
+ label,
15564
+ status: "warn",
15565
+ detail: `${installed}, newest is ${latest}`,
15566
+ fix
15567
+ } : { id, label, status: "ok", detail: `${installed} (newest)` };
15568
+ }
15569
+ async function manifestCheck(manifest, signedIn) {
15570
+ if (manifest === null || typeof manifest !== "object" || Array.isArray(manifest)) {
15571
+ return {
15572
+ id: "module",
15573
+ label: "module.json",
15574
+ status: "fail",
15575
+ detail: "is not a JSON object",
15576
+ fix: `Run ${code("dashwise module validate")} and fix ${code("module.json")}.`
15577
+ };
15578
+ }
15579
+ if (!signedIn) {
15580
+ return {
15581
+ id: "module",
15582
+ label: "module.json",
15583
+ status: "ok",
15584
+ detail: "readable (not validated, you are not signed in)"
15585
+ };
15586
+ }
15587
+ try {
15588
+ await apiRequest("POST", "/api/modules/manifest/validate", manifest);
15589
+ return {
15590
+ id: "module",
15591
+ label: "module.json",
15592
+ status: "ok",
15593
+ detail: "valid"
15594
+ };
15595
+ } catch (err) {
15596
+ if (err instanceof ApiError && err.status === 400) {
15597
+ return {
15598
+ id: "module",
15599
+ label: "module.json",
15600
+ status: "fail",
15601
+ detail: `invalid: ${err.message}`,
15602
+ fix: `Run ${code("dashwise module validate")} for the full list.`
15603
+ };
15604
+ }
15605
+ return {
15606
+ id: "module",
15607
+ label: "module.json",
15608
+ status: "warn",
15609
+ detail: `not validated (${err.message})`
15610
+ };
15611
+ }
15612
+ }
15613
+ async function repoCheck(dir, slug, signedIn, deps) {
15614
+ const origin = deps.gitOrigin(dir);
15615
+ if (!slug || !signedIn) {
15616
+ return {
15617
+ id: "repository",
15618
+ label: "Repository",
15619
+ status: "skip",
15620
+ detail: !slug ? `no module slug in ${code("module.json")}` : "not checked, you are not signed in"
15621
+ };
15622
+ }
15623
+ let linked = null;
15624
+ try {
15625
+ const mod = await apiRequest(
15626
+ "GET",
15627
+ `/api/modules/by-slug/${encodeURIComponent(slug)}`
15628
+ );
15629
+ if (!("repository_url" in mod)) {
15630
+ return {
15631
+ id: "repository",
15632
+ label: "Repository",
15633
+ status: "warn",
15634
+ code: "REPOSITORY_NOT_VISIBLE",
15635
+ detail: `you cannot see the repository of ${code(slug)}: only its builders and owners in its company can`,
15636
+ fix: "Ask one of the module owners to add you as a builder."
15637
+ };
15638
+ }
15639
+ linked = mod.repository_url ?? null;
15640
+ } catch (err) {
15641
+ return {
15642
+ id: "repository",
15643
+ label: "Repository",
15644
+ status: "warn",
15645
+ detail: err instanceof ApiError && err.status === 404 ? `module ${code(slug)} is not found in this Dashwise` : `could not read the module (${err.message})`,
15646
+ fix: "Check the target above and that you can build this module."
15647
+ };
15648
+ }
15649
+ if (!linked) {
15650
+ return {
15651
+ id: "repository",
15652
+ label: "Repository",
15653
+ status: "warn",
15654
+ code: "REPOSITORY_NOT_LINKED",
15655
+ detail: "the module has no repository linked in Dashwise",
15656
+ fix: "Link one on the module's Build tab in Dashwise."
15657
+ };
15658
+ }
15659
+ if (!origin) {
15660
+ return {
15661
+ id: "repository",
15662
+ label: "Repository",
15663
+ status: "warn",
15664
+ detail: `linked to ${linked}, but this folder has no git origin`,
15665
+ fix: `Run ${code(`git remote add origin ${linked}.git`)}.`
15666
+ };
15667
+ }
15668
+ return sameRepo(origin, linked) ? {
15669
+ id: "repository",
15670
+ label: "Repository",
15671
+ status: "ok",
15672
+ detail: `${linked} (matches git origin)`
15673
+ } : {
15674
+ id: "repository",
15675
+ label: "Repository",
15676
+ status: "warn",
15677
+ detail: `git origin is ${origin}, Dashwise builds from ${linked}`,
15678
+ fix: `Push to ${linked}, or link this repository on the module's Build tab.`
15679
+ };
15680
+ }
15681
+ function repoKey(url) {
15682
+ const m = /github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?\/?$/i.exec(url.trim());
15683
+ return m ? `${m[1]}/${m[2]}`.toLowerCase() : null;
15684
+ }
15685
+ function sameRepo(a, b) {
15686
+ const ka = repoKey(a);
15687
+ return ka !== null && ka === repoKey(b);
15688
+ }
15689
+ function sameOrigin(a, b) {
15690
+ try {
15691
+ return new URL(a).origin === new URL(b).origin;
15692
+ } catch {
15693
+ return false;
15694
+ }
15695
+ }
15696
+ function safeReadConfig2() {
15697
+ try {
15698
+ return readConfig();
15699
+ } catch {
15700
+ return null;
15701
+ }
15702
+ }
15703
+ function badge(status) {
15704
+ switch (status) {
15705
+ case "ok":
15706
+ return pc.green("\u2714");
15707
+ case "warn":
15708
+ return pc.yellow("!");
15709
+ case "fail":
15710
+ return pc.red("\u2716");
15711
+ default:
15712
+ return dim("-");
15713
+ }
15714
+ }
15715
+ function defaultDoctorDeps(cliVersion) {
15716
+ return {
15717
+ cliVersion,
15718
+ nodeVersion: process.version,
15719
+ latestVersion: async (pkg) => {
15720
+ try {
15721
+ const res = await fetch(
15722
+ `https://registry.npmjs.org/${pkg.replace("/", "%2f")}/latest`,
15723
+ { signal: AbortSignal.timeout(4e3) }
15724
+ );
15725
+ if (!res.ok) return null;
15726
+ const body = await res.json();
15727
+ return typeof body.version === "string" ? body.version : null;
15728
+ } catch {
15729
+ return null;
15730
+ }
15731
+ },
15732
+ gitOrigin: (dir) => {
15733
+ try {
15734
+ return execFileSync("git", ["remote", "get-url", "origin"], {
15735
+ cwd: dir,
15736
+ stdio: ["ignore", "pipe", "ignore"]
15737
+ }).toString().trim() || null;
15738
+ } catch {
15739
+ return null;
15740
+ }
15741
+ }
15742
+ };
15743
+ }
15744
+
15024
15745
  // src/commands/mcp.ts
15025
15746
  init_api_client();
15026
15747
  init_config();
@@ -15145,8 +15866,8 @@ function startMcpProxy(opts) {
15145
15866
  let streamUnavailable = false;
15146
15867
  let streamStarted = false;
15147
15868
  let resolveDone;
15148
- const done = new Promise((resolve14) => {
15149
- resolveDone = resolve14;
15869
+ const done = new Promise((resolve15) => {
15870
+ resolveDone = resolve15;
15150
15871
  });
15151
15872
  const trace = (line) => {
15152
15873
  if (verbose) log2(line);
@@ -15632,7 +16353,7 @@ async function revokeSession(endpointId) {
15632
16353
  () => false
15633
16354
  );
15634
16355
  const timeout = new Promise(
15635
- (resolve14) => setTimeout(() => resolve14(false), REVOKE_TIMEOUT_MS).unref()
16356
+ (resolve15) => setTimeout(() => resolve15(false), REVOKE_TIMEOUT_MS).unref()
15636
16357
  );
15637
16358
  await Promise.race([attempt, timeout]);
15638
16359
  }
@@ -15810,7 +16531,7 @@ async function mcpAddCommand(options = {}) {
15810
16531
  fail(`Could not run \`claude\`: ${target.refused} Run the command above yourself.`);
15811
16532
  return 1;
15812
16533
  }
15813
- return await new Promise((resolve14) => {
16534
+ return await new Promise((resolve15) => {
15814
16535
  const child = spawn(target.command, target.args, { shell: target.shell, stdio: "inherit" });
15815
16536
  let settled = false;
15816
16537
  child.on("error", (err) => {
@@ -15820,16 +16541,16 @@ async function mcpAddCommand(options = {}) {
15820
16541
  fail(
15821
16542
  `Claude Code is not on your PATH, so nothing was registered. Run the command above once ${code("claude")} is installed.`
15822
16543
  );
15823
- resolve14(1);
16544
+ resolve15(1);
15824
16545
  return;
15825
16546
  }
15826
16547
  fail(`Could not run \`claude\`: ${err.message}`);
15827
- resolve14(1);
16548
+ resolve15(1);
15828
16549
  });
15829
16550
  child.on("close", (codeOut) => {
15830
16551
  if (settled) return;
15831
16552
  settled = true;
15832
- resolve14(codeOut ?? 1);
16553
+ resolve15(codeOut ?? 1);
15833
16554
  });
15834
16555
  });
15835
16556
  }
@@ -15948,10 +16669,10 @@ function upgradeCommand(plugin, marketplace) {
15948
16669
  var PLUGIN_NAME = "dashwise";
15949
16670
  var MARKETPLACE_NAME = "dashwise";
15950
16671
  var DEFAULT_MARKETPLACE_SOURCE = "Intuize/dashwise-cli";
15951
- var defaultRunner = (cmd, args, opts) => new Promise((resolve14) => {
16672
+ var defaultRunner = (cmd, args, opts) => new Promise((resolve15) => {
15952
16673
  const target = spawnTarget(cmd, args);
15953
16674
  if ("refused" in target) {
15954
- resolve14({ code: null, stdout: "", stderr: target.refused, missing: false });
16675
+ resolve15({ code: null, stdout: "", stderr: target.refused, missing: false });
15955
16676
  return;
15956
16677
  }
15957
16678
  const child = spawn(target.command, target.args, {
@@ -15966,7 +16687,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolve14) => {
15966
16687
  child.on("error", (err) => {
15967
16688
  if (settled) return;
15968
16689
  settled = true;
15969
- resolve14({
16690
+ resolve15({
15970
16691
  code: null,
15971
16692
  stdout,
15972
16693
  stderr: stderr || err.message,
@@ -15976,7 +16697,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolve14) => {
15976
16697
  child.on("close", (exitCode) => {
15977
16698
  if (settled) return;
15978
16699
  settled = true;
15979
- resolve14({ code: exitCode, stdout, stderr, missing: false });
16700
+ resolve15({ code: exitCode, stdout, stderr, missing: false });
15980
16701
  });
15981
16702
  });
15982
16703
  function readSkillsOnDisk() {
@@ -17670,7 +18391,14 @@ async function moduleInstallCommand(slugArg, options) {
17670
18391
  try {
17671
18392
  const installs = await listWorkspaceInstallations(workspace2.id, auth);
17672
18393
  const byKind = selectInstallsByKind(installs, mod.id);
17673
- existing = byKind.production;
18394
+ const production = byKind.all.filter((row2) => !isDevInstallKind(row2.kind));
18395
+ if (production.length > 1) {
18396
+ fail(
18397
+ "Several production installations exist for this module in this company. Select the installation to update in the app. Nothing was changed."
18398
+ );
18399
+ return 1;
18400
+ }
18401
+ existing = production[0];
17674
18402
  devInstall = byKind.dev;
17675
18403
  } catch (err) {
17676
18404
  fail(
@@ -17753,7 +18481,8 @@ async function moduleInstallCommand(slugArg, options) {
17753
18481
  mod.id,
17754
18482
  {
17755
18483
  workspaceId: workspace2.id,
17756
- ...options.version !== void 0 ? { version: options.version } : {},
18484
+ version: targetVersion,
18485
+ ...existing ? { installation_id: existing.id } : {},
17757
18486
  ...options.force !== void 0 ? { force: options.force } : {}
17758
18487
  },
17759
18488
  auth
@@ -17761,6 +18490,12 @@ async function moduleInstallCommand(slugArg, options) {
17761
18490
  } catch (err) {
17762
18491
  return surfaceInstallError(err, mod.slug, workspace2.slug, devInstallReplaced);
17763
18492
  }
18493
+ if (existing && (isInstallGated(result) || result.installation.id !== existing.id)) {
18494
+ fail(
18495
+ `The backend did not confirm an in-place update of installation #${existing.id}. Check its state in the app before retrying; no fallback install will be attempted.`
18496
+ );
18497
+ return 1;
18498
+ }
17764
18499
  if (isInstallGated(result)) {
17765
18500
  if (options.json) {
17766
18501
  out(JSON.stringify(result));
@@ -17780,7 +18515,9 @@ async function moduleInstallCommand(slugArg, options) {
17780
18515
  let verified = false;
17781
18516
  try {
17782
18517
  const after = await listWorkspaceInstallations(workspace2.id, auth);
17783
- row = selectInstallsByKind(after, mod.id).production;
18518
+ row = after.find(
18519
+ (candidate) => candidate.id === result.installation.id && candidate.module_id === mod.id && !isDevInstallKind(candidate.kind)
18520
+ );
17784
18521
  verified = row !== void 0;
17785
18522
  } catch {
17786
18523
  }
@@ -18390,6 +19127,16 @@ function packedFileLines(files, cap = PACKED_FILE_LIST_CAP) {
18390
19127
  if (files.length <= cap) return [...files];
18391
19128
  return [...files.slice(0, cap), `\u2026 and ${files.length - cap} more`];
18392
19129
  }
19130
+ function repositoryLinkedDecision(input) {
19131
+ if (!input.repositoryUrl || input.fromLocal || input.dryRun) return "proceed";
19132
+ return input.interactive ? "confirm" : "refuse";
19133
+ }
19134
+ function canPrompt(stdin, stdout) {
19135
+ return stdin.isTTY === true && stdout.isTTY === true;
19136
+ }
19137
+ function repositoryLinkedWarning(repositoryUrl) {
19138
+ return `These files are not in ${repositoryUrl}; the next repository build replaces them.`;
19139
+ }
18393
19140
  async function modulePublishCommand(options) {
18394
19141
  const projectRoot = resolve(options.dir ?? process.cwd());
18395
19142
  const manifestPath2 = join(projectRoot, "module.json");
@@ -18486,6 +19233,35 @@ async function modulePublishCommand(options) {
18486
19233
  );
18487
19234
  }
18488
19235
  for (const w of tarball.warnings) warn(w);
19236
+ if (mod.repository_url) {
19237
+ const decision = repositoryLinkedDecision({
19238
+ repositoryUrl: mod.repository_url,
19239
+ fromLocal: options.fromLocal === true,
19240
+ dryRun: options.dryRun === true,
19241
+ interactive: canPrompt(process.stdin, process.stdout)
19242
+ });
19243
+ warn(repositoryLinkedWarning(mod.repository_url));
19244
+ if (decision === "refuse") {
19245
+ fail(
19246
+ `${code(slug)} is linked to a repository. Re-run with ${code("--from-local")} to publish these local files anyway.`
19247
+ );
19248
+ return 1;
19249
+ }
19250
+ if (decision === "confirm") {
19251
+ const answer = await select({
19252
+ message: "Publish these local files anyway?",
19253
+ options: [
19254
+ { value: "go", label: "Yes, publish from local files" },
19255
+ { value: "cancel", label: "No, abort" }
19256
+ ],
19257
+ initialValue: "cancel"
19258
+ });
19259
+ if (isCancel(answer) || answer === "cancel") {
19260
+ fail("Aborted.");
19261
+ return 130;
19262
+ }
19263
+ }
19264
+ }
18489
19265
  if (!options.yes && process.stdout.isTTY && !options.dryRun) {
18490
19266
  const answer = await select({
18491
19267
  message: `Upload + publish ${code(`bump:${bump}`)}?`,
@@ -19022,7 +19798,7 @@ async function moduleReleaseCommand(slugArg, options) {
19022
19798
  }
19023
19799
  async function wait() {
19024
19800
  const remaining = Math.max(0, deadline - Date.now());
19025
- await new Promise((resolve14) => setTimeout(resolve14, Math.min(options.pollIntervalMs ?? 4e3, remaining)));
19801
+ await new Promise((resolve15) => setTimeout(resolve15, Math.min(options.pollIntervalMs ?? 4e3, remaining)));
19026
19802
  }
19027
19803
  }
19028
19804
 
@@ -22430,7 +23206,7 @@ function formatTimestamp(iso) {
22430
23206
  return iso.slice(0, 19).replace("T", " ");
22431
23207
  }
22432
23208
  function sleep2(ms) {
22433
- return new Promise((resolve14) => setTimeout(resolve14, ms));
23209
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
22434
23210
  }
22435
23211
 
22436
23212
  // src/commands/module/action-log.ts
@@ -22532,7 +23308,7 @@ async function runTail2(installationId, options) {
22532
23308
  }
22533
23309
  dim(`(tail: transient error \u2014 ${err.message})`);
22534
23310
  }
22535
- await new Promise((resolve14) => setTimeout(resolve14, POLL_INTERVAL_MS2));
23311
+ await new Promise((resolve15) => setTimeout(resolve15, POLL_INTERVAL_MS2));
22536
23312
  }
22537
23313
  }
22538
23314
  function printTable(rows) {
@@ -22917,7 +23693,7 @@ async function sleep3(ms, aborted) {
22917
23693
  const slice = 100;
22918
23694
  let waited = 0;
22919
23695
  while (waited < ms && !aborted()) {
22920
- await new Promise((resolve14) => setTimeout(resolve14, Math.min(slice, ms - waited)));
23696
+ await new Promise((resolve15) => setTimeout(resolve15, Math.min(slice, ms - waited)));
22921
23697
  waited += slice;
22922
23698
  }
22923
23699
  }
@@ -23467,6 +24243,17 @@ program.command("login").description("Sign in via the device-code flow.").option
23467
24243
  });
23468
24244
  process.exitCode = exit;
23469
24245
  });
24246
+ program.command("doctor").description(
24247
+ "Check your setup: which Dashwise you target (dev or prod), login, connection, Node, CLI and SDK versions, and inside a module folder module.json, the repository link and the cloud setup files. Each item says what to do. Exits 1 when something fails."
24248
+ ).option("--dir <path>", "Module folder to check (default: current directory).").option("--json", "Print one JSON object { ok, checks } instead of the report.").action(async (cmdOpts) => {
24249
+ process.exitCode = await doctorCommand(
24250
+ {
24251
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
24252
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
24253
+ },
24254
+ defaultDoctorDeps(getVersion())
24255
+ );
24256
+ });
23470
24257
  program.command("logout").description("Clear the local credentials and revoke the server-side token.").action(async () => {
23471
24258
  process.exitCode = await logoutCommand();
23472
24259
  });
@@ -24057,6 +24844,9 @@ moduleCmd.command("release [slug]").description("Release an exact pushed reposit
24057
24844
  });
24058
24845
  });
24059
24846
  moduleCmd.command("publish").description("Pack the current directory + upload it as a new module version.").option("--dir <path>", "Project root (default: current directory).").option("--bump <direction>", "Semver bump: patch | minor | major.").option("--dry-run", "Validate + upload but do not persist the version row.").option("-y, --yes", "Skip the confirmation prompt (for CI / scripted use).").option(
24847
+ "--from-local",
24848
+ "Publish local files to a module that has a linked repository (the next repository build replaces them)."
24849
+ ).option(
24060
24850
  "--exclude <pattern...>",
24061
24851
  "Extra exclude patterns (segment-matched) appended to the defaults."
24062
24852
  ).option(
@@ -24076,6 +24866,7 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
24076
24866
  ...cmdOpts.bump !== void 0 ? { bump: cmdOpts.bump } : {},
24077
24867
  ...cmdOpts.dryRun !== void 0 ? { dryRun: cmdOpts.dryRun } : {},
24078
24868
  ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
24869
+ ...cmdOpts.fromLocal !== void 0 ? { fromLocal: cmdOpts.fromLocal } : {},
24079
24870
  ...cmdOpts.exclude !== void 0 ? { exclude: cmdOpts.exclude } : {},
24080
24871
  // commander maps `--no-respect-gitignore` to
24081
24872
  // `respectGitignore === false`.
@@ -24332,12 +25123,12 @@ tableCmd.command("create <slug>").description(
24332
25123
  "Create a new table. Pass --field flags to define columns. Calls POST /api/installations/:id/schema/tables and syncs the updated manifest back to local module.json."
24333
25124
  ).option("--dir <path>", "Project root (default: current directory).").option(
24334
25125
  "-f, --field <spec>",
24335
- "Field spec \u2014 `<slug>:<type>[:required]`. Repeat for multiple fields. For options/target/default, use --field-json instead. Example: `--field title:text:required --field done:boolean`.",
25126
+ "Field spec: `<slug>:<type>[:primary,required]`. Exactly one eligible field must be primary. Repeat for multiple fields. For options/target/default, use --field-json instead. Example: `--field title:text:primary,required --field done:boolean`.",
24336
25127
  (val, prev = []) => [...prev, val],
24337
25128
  []
24338
25129
  ).option(
24339
25130
  "--field-json <json>",
24340
- "Field as a JSON object \u2014 `{slug,type,required?,options?,target?,default?}`. Repeatable. Use for fields where the simple syntax can't express the shape.",
25131
+ "Field as a JSON object \u2014 `{slug,type,primary?:true,required?,options?,target?,default?}`. Repeatable. Use for fields where the simple syntax can't express the shape.",
24341
25132
  (val, prev = []) => [...prev, val],
24342
25133
  []
24343
25134
  ).action(
@@ -24401,7 +25192,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
24401
25192
  }
24402
25193
  );
24403
25194
  function getVersion() {
24404
- return "0.23.0";
25195
+ return "0.25.0";
24405
25196
  }
24406
25197
  function parseIntOption(value) {
24407
25198
  const n = Number.parseInt(value, 10);
@@ -24417,6 +25208,7 @@ function collectRepeated(value, previous) {
24417
25208
  // src/bin.ts
24418
25209
  (async () => {
24419
25210
  try {
25211
+ await installEnvProxy();
24420
25212
  await program.parseAsync(process.argv);
24421
25213
  } catch (err) {
24422
25214
  if (err instanceof ExcessArgumentsError) {