@dashai/cli 0.19.0 → 0.20.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.
Files changed (3) hide show
  1. package/dist/bin.js +1148 -121
  2. package/dist/bin.js.map +1 -1
  3. package/package.json +2 -2
package/dist/bin.js CHANGED
@@ -110,7 +110,7 @@ function resolveApiUrl() {
110
110
  if (fromEnv) return fromEnv.replace(/\/+$/, "");
111
111
  const cfg = safeReadConfig();
112
112
  if (cfg?.apiUrl) return cfg.apiUrl.replace(/\/+$/, "");
113
- return "http://localhost:3000";
113
+ return DEFAULT_API_URL;
114
114
  }
115
115
  function readSavedToken() {
116
116
  const cfg = safeReadConfig();
@@ -172,11 +172,53 @@ function removeModuleEntry(slug) {
172
172
  function isLoggedIn() {
173
173
  return readSavedToken() !== null;
174
174
  }
175
- var FILE_NAME, DIR_NAME;
175
+ var FILE_NAME, DIR_NAME, DEFAULT_API_URL, KNOWN_API_URLS;
176
176
  var init_config = __esm({
177
177
  "src/lib/config.ts"() {
178
178
  FILE_NAME = "auth.json";
179
179
  DIR_NAME = "dashwise";
180
+ DEFAULT_API_URL = "https://api.dashwise.dev";
181
+ KNOWN_API_URLS = {
182
+ dev: "https://api.dashwise.dev",
183
+ prod: "https://api.dashwise.net"
184
+ };
185
+ }
186
+ });
187
+
188
+ // src/lib/uses-types.ts
189
+ function abstractTypeForPhysical(physicalType) {
190
+ return PHYSICAL_TO_ABSTRACT[physicalType] ?? null;
191
+ }
192
+ function slugifyUsesName(name) {
193
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
194
+ }
195
+ function isUsesSlug(slug) {
196
+ return slug.length >= USES_SLUG_MIN_LENGTH && slug.length <= USES_SLUG_MAX_LENGTH && USES_SLUG_REGEX.test(slug);
197
+ }
198
+ var PHYSICAL_TO_ABSTRACT, USES_OPERATIONS, USES_SLUG_REGEX, USES_SLUG_MIN_LENGTH, USES_SLUG_MAX_LENGTH, USES_SLUG_RULE;
199
+ var init_uses_types = __esm({
200
+ "src/lib/uses-types.ts"() {
201
+ PHYSICAL_TO_ABSTRACT = {
202
+ text: "text",
203
+ long_text: "long_text",
204
+ number: "number",
205
+ rating: "number",
206
+ boolean: "boolean",
207
+ // A date-with-time column has physical registry type `date` (with
208
+ // `config.includeTime`), NOT a distinct `datetime` type — the backend
209
+ // registry has no `datetime` key. So introspection only ever reports
210
+ // `date` here; there is no `datetime` physical entry to fold.
211
+ date: "date",
212
+ email: "email",
213
+ url: "url",
214
+ single_select: "single_select",
215
+ multi_select: "multi_select"
216
+ };
217
+ USES_OPERATIONS = ["create", "read", "update", "delete"];
218
+ USES_SLUG_REGEX = /^[a-z][a-z0-9_]*[a-z0-9]$/;
219
+ USES_SLUG_MIN_LENGTH = 2;
220
+ USES_SLUG_MAX_LENGTH = 63;
221
+ USES_SLUG_RULE = `^[a-z][a-z0-9_]*[a-z0-9]$ (snake_case, length ${USES_SLUG_MIN_LENGTH}\u2013${USES_SLUG_MAX_LENGTH})`;
180
222
  }
181
223
  });
182
224
 
@@ -422,8 +464,38 @@ async function declineBinding(installationId, usesSlug, options = {}) {
422
464
  options
423
465
  );
424
466
  }
425
- function slugifyFieldName(name) {
426
- return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
467
+ async function listWorkspaceTables(workspaceId, options = {}) {
468
+ const apps = await apiRequest(
469
+ "GET",
470
+ `/api/applications/workspace/${workspaceId}`,
471
+ void 0,
472
+ options
473
+ );
474
+ const perApp = await Promise.all(
475
+ apps.map(async (app) => {
476
+ let tables;
477
+ try {
478
+ tables = await apiRequest(
479
+ "GET",
480
+ `/api/tables/application/${app.id}`,
481
+ void 0,
482
+ options
483
+ );
484
+ } catch {
485
+ return [];
486
+ }
487
+ return tables.map((t) => ({
488
+ table_id: t.id,
489
+ name: t.name,
490
+ module_id: t.module_id,
491
+ application_id: app.id,
492
+ application_name: app.name
493
+ }));
494
+ })
495
+ );
496
+ return perApp.flat().sort(
497
+ (a, b) => a.application_name.localeCompare(b.application_name) || a.name.localeCompare(b.name)
498
+ );
427
499
  }
428
500
  async function introspectWorkspaceTable(_installationId, tableNameOrId, options = {}) {
429
501
  const ref = String(tableNameOrId).trim();
@@ -515,10 +587,11 @@ async function getRuntimeLogs(installationId, opts = {}, options = {}) {
515
587
  options
516
588
  );
517
589
  }
518
- var ApiError, CODE_SHAPE;
590
+ var ApiError, CODE_SHAPE, slugifyFieldName;
519
591
  var init_api_client = __esm({
520
592
  "src/lib/api-client.ts"() {
521
593
  init_config();
594
+ init_uses_types();
522
595
  ApiError = class extends Error {
523
596
  status;
524
597
  code;
@@ -536,6 +609,7 @@ var init_api_client = __esm({
536
609
  }
537
610
  };
538
611
  CODE_SHAPE = /^[A-Z][A-Z0-9_]*$/;
612
+ slugifyFieldName = slugifyUsesName;
539
613
  }
540
614
  });
541
615
 
@@ -605,10 +679,109 @@ async function safeGet(fn) {
605
679
  return null;
606
680
  }
607
681
  }
682
+ function isRequiredSlot(b) {
683
+ return b.if_missing !== "optional";
684
+ }
685
+ function summarizeUsesReadiness(bindings) {
686
+ const slots = [...bindings];
687
+ const bound = slots.filter((b) => b.status === "bound");
688
+ return {
689
+ slots,
690
+ bound,
691
+ settings: {
692
+ settings_path: firstWireValue(slots.map((b) => b.settings_path)),
693
+ settings_url: firstWireValue(slots.map((b) => b.settings_url))
694
+ },
695
+ pending: slots.filter((b) => b.status === "unbound" && isRequiredSlot(b)),
696
+ optionalUnbound: slots.filter(
697
+ (b) => b.status === "unbound" && !isRequiredSlot(b)
698
+ ),
699
+ declined: slots.filter((b) => b.status === "declined"),
700
+ incomplete: bound.filter((b) => (b.missing_fields?.length ?? 0) > 0),
701
+ setupPending: slots.some((b) => b.status === "unbound" && isRequiredSlot(b))
702
+ };
703
+ }
704
+ async function fetchUsesReadiness(installationId, requestOptions) {
705
+ const bindings = await safeGet(
706
+ () => getInstallationBindings(installationId, requestOptions)
707
+ );
708
+ if (bindings === null) return null;
709
+ return summarizeUsesReadiness(bindings);
710
+ }
711
+ function wireValue(v) {
712
+ const trimmed = typeof v === "string" ? v.trim() : "";
713
+ return trimmed.length > 0 ? trimmed : null;
714
+ }
715
+ function firstWireValue(values) {
716
+ for (const v of values) {
717
+ const resolved = wireValue(v);
718
+ if (resolved !== null) return resolved;
719
+ }
720
+ return null;
721
+ }
722
+ function resolveSettingsPath(wire) {
723
+ return wireValue(wire) ?? SETUP_SETTINGS_PATH;
724
+ }
725
+ function resolveSettingsUrl(wire) {
726
+ const value = wireValue(wire);
727
+ if (value === null) return null;
728
+ return /^https?:\/\//i.test(value) ? value : null;
729
+ }
730
+ function renderUsesReadiness(readiness, wire) {
731
+ if (readiness.slots.length === 0) return;
732
+ const { slots } = readiness;
733
+ info(
734
+ `Data-plane readiness ${dim(
735
+ `(${readiness.bound.length}/${slots.length} slot${slots.length === 1 ? "" : "s"} bound)`
736
+ )}:`
737
+ );
738
+ for (const b of slots) renderReadinessSlot(b);
739
+ if (readiness.pending.length > 0) {
740
+ const names = readiness.pending.map((b) => b.uses_slug);
741
+ info(
742
+ `${names.length} required slot${names.length === 1 ? "" : "s"} await${names.length === 1 ? "s" : ""} setup \u2014 until then the module throws ${code("USES_UNBOUND")} on its first data call, which end users see as an error digest.`
743
+ );
744
+ for (const b of readiness.pending) {
745
+ out(` ${dim("-")} ${code(`dashwise bind ${b.uses_slug} --table <name-or-id>`)}`);
746
+ }
747
+ const path = resolveSettingsPath(
748
+ wireValue(wire?.settings_path) ?? readiness.settings.settings_path
749
+ );
750
+ const url = resolveSettingsUrl(wire?.settings_url) ?? resolveSettingsUrl(readiness.settings.settings_url);
751
+ info(
752
+ `A workspace admin can do the same in the Dashwise app, under ${code(`Settings \u2192 ${path}`)}, where each declared table slot is bound.`
753
+ );
754
+ if (url !== null) out(` ${dim("\u2192")} ${url}`);
755
+ }
756
+ for (const b of readiness.incomplete) {
757
+ info(
758
+ `${code(b.uses_slug)} is bound but ${(b.missing_fields ?? []).length} required field(s) are unmapped: ${(b.missing_fields ?? []).join(", ")}. Re-bind with ${code(`dashwise bind ${b.uses_slug} --map <field>=<physical>`)}.`
759
+ );
760
+ }
761
+ }
762
+ function renderReadinessSlot(b) {
763
+ const required = b.if_missing === "optional" ? dim(" (optional)") : "";
764
+ if (b.status === "bound") {
765
+ const tableLine = b.table ? ` ${dim(`\u2192 ${b.table.name} (id ${b.table.id})`)}` : "";
766
+ const gap = (b.missing_fields?.length ?? 0) > 0 ? ` ${pc.yellow(`(${b.missing_fields?.length} field(s) unmapped)`)}` : "";
767
+ out(` ${pc.green("\u25CF bound")} ${code(b.uses_slug)}${tableLine}${gap}`);
768
+ return;
769
+ }
770
+ if (b.status === "declined") {
771
+ out(` ${pc.dim("\u2298 declined")} ${code(b.uses_slug)}`);
772
+ return;
773
+ }
774
+ const chip = b.if_missing === "optional" ? pc.dim("\u25CB unbound") : pc.yellow("\u25CB unbound");
775
+ out(
776
+ ` ${chip} ${code(b.uses_slug)}${required} ${dim(`reason: ${b.reason ?? "(unspecified)"}`)}`
777
+ );
778
+ }
779
+ var SETUP_SETTINGS_PATH;
608
780
  var init_connect_status = __esm({
609
781
  "src/lib/connect-status.ts"() {
610
782
  init_api_client();
611
783
  init_output();
784
+ SETUP_SETTINGS_PATH = "Modules \u2192 Installed modules \u2192 Technical \u2192 Data connections";
612
785
  }
613
786
  });
614
787
  function manifestPath(dir) {
@@ -805,6 +978,33 @@ var init_manifest_poll = __esm({
805
978
  init_manifest_merge();
806
979
  }
807
980
  });
981
+
982
+ // src/lib/redact.ts
983
+ function maskSecret(value) {
984
+ if (value.length <= 12) return "****";
985
+ return `${value.slice(0, 4)}\u2026${value.slice(-4)}`;
986
+ }
987
+ function redactSecrets(line) {
988
+ let out2 = line;
989
+ for (const pattern of SECRET_PATTERNS) {
990
+ pattern.lastIndex = 0;
991
+ out2 = out2.replace(pattern, (match) => maskSecret(match));
992
+ }
993
+ return out2;
994
+ }
995
+ var SECRET_PATTERNS;
996
+ var init_redact = __esm({
997
+ "src/lib/redact.ts"() {
998
+ SECRET_PATTERNS = [
999
+ // A JWT — three base64url segments. The SSO exchange code (C-10), and
1000
+ // any access token that finds its way into a URL or a header dump.
1001
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/g,
1002
+ // The Dashwise credential family: dwk_ installation keys, dwc_ CLI
1003
+ // tokens, dwm_ endpoint keys, dwa_/dwr_/dwo_ OAuth tokens.
1004
+ /\bdw[kcmaro]_[A-Za-z0-9_-]{16,}/g
1005
+ ];
1006
+ }
1007
+ });
808
1008
  async function devCommand(options) {
809
1009
  if (options.sdkOnly && options.nextOnly) {
810
1010
  fail(
@@ -1078,22 +1278,19 @@ function pipeWithPrefix(src, dst, prefix) {
1078
1278
  const lines = combined.split("\n");
1079
1279
  leftover = lines.pop() ?? "";
1080
1280
  for (const line of lines) {
1081
- dst.write(`${prefix}${line}
1281
+ dst.write(`${prefix}${redactSecrets(line)}
1082
1282
  `);
1083
1283
  }
1084
1284
  });
1085
1285
  src.on("end", () => {
1086
1286
  if (leftover.length > 0) {
1087
- dst.write(`${prefix}${leftover}
1287
+ dst.write(`${prefix}${redactSecrets(leftover)}
1088
1288
  `);
1089
1289
  }
1090
1290
  });
1091
1291
  }
1092
1292
  function maskedToken(token) {
1093
- if (token.length <= 12) return "****";
1094
- const prefix = token.slice(0, 4);
1095
- const tail = token.slice(-4);
1096
- return `${prefix}\u2026${tail}`;
1293
+ return maskSecret(token);
1097
1294
  }
1098
1295
  async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
1099
1296
  if (process.env.DASHWISE_API_KEY) return null;
@@ -1171,13 +1368,13 @@ async function resolveDevPort(requested) {
1171
1368
  }
1172
1369
  async function isPortFree(port) {
1173
1370
  const { createServer } = await import('net');
1174
- return new Promise((resolve13) => {
1371
+ return new Promise((resolve14) => {
1175
1372
  const server = createServer();
1176
1373
  server.once("error", () => {
1177
- resolve13(false);
1374
+ resolve14(false);
1178
1375
  });
1179
1376
  server.once("listening", () => {
1180
- server.close(() => resolve13(true));
1377
+ server.close(() => resolve14(true));
1181
1378
  });
1182
1379
  server.listen(port, "0.0.0.0");
1183
1380
  });
@@ -1248,6 +1445,7 @@ var init_dev = __esm({
1248
1445
  init_manifest_io();
1249
1446
  init_manifest_poll();
1250
1447
  init_output();
1448
+ init_redact();
1251
1449
  DEFAULT_NEXT_PORT = 3e3;
1252
1450
  PREFIX_SDK = `\x1B[36m[sdk]\x1B[0m `;
1253
1451
  PREFIX_NEXT = `\x1B[35m[next]\x1B[0m `;
@@ -1315,6 +1513,62 @@ var init_workspaces = __esm({
1315
1513
  init_api_client();
1316
1514
  }
1317
1515
  });
1516
+ function resolveSdkPackageJson(projectRoot) {
1517
+ let dir = projectRoot;
1518
+ for (; ; ) {
1519
+ const candidate = join(dir, "node_modules", "@dashai", "sdk", "package.json");
1520
+ if (existsSync(candidate)) return candidate;
1521
+ const parent = dirname(dir);
1522
+ if (parent === dir) return null;
1523
+ dir = parent;
1524
+ }
1525
+ }
1526
+ function readInstalledSdk(projectRoot) {
1527
+ const pkgPath = resolveSdkPackageJson(projectRoot);
1528
+ if (pkgPath === null) return null;
1529
+ try {
1530
+ const parsed = JSON.parse(readFileSync(pkgPath, "utf-8"));
1531
+ if (typeof parsed.version !== "string" || parsed.version.length === 0) return null;
1532
+ return { version: parsed.version };
1533
+ } catch {
1534
+ return null;
1535
+ }
1536
+ }
1537
+ function compareVersions(a, b) {
1538
+ const parse = (v) => {
1539
+ const [core = "", pre] = v.trim().replace(/^[^0-9]*/, "").split("-", 2);
1540
+ const nums = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
1541
+ while (nums.length < 3) nums.push(0);
1542
+ return [nums.slice(0, 3), pre !== void 0 && pre.length > 0];
1543
+ };
1544
+ const [an, apre] = parse(a);
1545
+ const [bn, bpre] = parse(b);
1546
+ for (let i = 0; i < 3; i++) {
1547
+ const d = (an[i] ?? 0) - (bn[i] ?? 0);
1548
+ if (d !== 0) return d;
1549
+ }
1550
+ if (apre === bpre) return 0;
1551
+ return apre ? -1 : 1;
1552
+ }
1553
+ function satisfiesFloor(version, floor) {
1554
+ return compareVersions(version, floor) >= 0;
1555
+ }
1556
+ function checkSdkFloor(projectRoot, floor) {
1557
+ const installed = readInstalledSdk(projectRoot);
1558
+ if (installed === null) return { ok: true, installed: null, floor };
1559
+ return {
1560
+ ok: satisfiesFloor(installed.version, floor),
1561
+ installed: installed.version,
1562
+ floor
1563
+ };
1564
+ }
1565
+ var USES_SDK_FLOOR, SCAFFOLD_SDK_RANGE;
1566
+ var init_sdk_version = __esm({
1567
+ "src/lib/sdk-version.ts"() {
1568
+ USES_SDK_FLOOR = "2.5.0";
1569
+ SCAFFOLD_SDK_RANGE = "^2.5.0";
1570
+ }
1571
+ });
1318
1572
 
1319
1573
  // src/lib/scaffold.ts
1320
1574
  function isDeployTarget(value) {
@@ -1395,7 +1649,12 @@ function packageJson(opts) {
1395
1649
  start: "next start"
1396
1650
  },
1397
1651
  dependencies: {
1398
- "@dashai/sdk": "^2.3.0",
1652
+ // The floor every scaffold feature can actually run on
1653
+ // (S-02): `uses` needs >= 2.5.0, and a project resolved below
1654
+ // it fails with "every import from @dashai/generated throws"
1655
+ // — a symptom that names nothing. SCAFFOLD_SDK_RANGE is the
1656
+ // single place this is stated.
1657
+ "@dashai/sdk": SCAFFOLD_SDK_RANGE,
1399
1658
  next: "^15.0.0",
1400
1659
  react: "^19.0.0",
1401
1660
  "react-dom": "^19.0.0"
@@ -2421,10 +2680,11 @@ function packageJsonCustom(opts) {
2421
2680
  start: "next start"
2422
2681
  },
2423
2682
  dependencies: {
2424
- // >=2.4.0 for `@dashai/sdk/platform`, which app/layout.tsx imports. A
2425
- // ^2.3.0 floor resolves to 2.3.0 until 2.4.0 publishes, failing the
2426
- // first `next build` with an opaque module-not-found.
2427
- "@dashai/sdk": "^2.4.0",
2683
+ // >=2.4.0 for `@dashai/sdk/platform`, which app/layout.tsx
2684
+ // imports, and >=2.5.0 for `uses` (S-02) — a project resolved
2685
+ // below that fails with "every import from @dashai/generated
2686
+ // throws", which points nowhere near the cause.
2687
+ "@dashai/sdk": SCAFFOLD_SDK_RANGE,
2428
2688
  next: "^15.0.0",
2429
2689
  react: "^19.0.0",
2430
2690
  "react-dom": "^19.0.0",
@@ -3809,11 +4069,63 @@ primary_region = "iad"
3809
4069
  var DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL, MODULE_SLUG_REGEX;
3810
4070
  var init_scaffold = __esm({
3811
4071
  "src/lib/scaffold.ts"() {
4072
+ init_sdk_version();
3812
4073
  DEPLOY_TARGETS = ["railway", "vercel", "fly"];
3813
4074
  SCAFFOLD_DEFAULT_API_URL = "http://localhost:3000";
3814
4075
  MODULE_SLUG_REGEX = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
3815
4076
  }
3816
4077
  });
4078
+ function resolveTargetDir(slug, dirFlag) {
4079
+ if (dirFlag !== void 0) return { dir: resolve(dirFlag) };
4080
+ const cwd = process.cwd();
4081
+ const nested = resolve(slug);
4082
+ if (basename(cwd) !== slug) return { dir: nested };
4083
+ const manifestPath2 = join(cwd, "module.json");
4084
+ if (existsSync(manifestPath2)) {
4085
+ const existingSlug = readManifestSlug(manifestPath2);
4086
+ if (existingSlug === slug) {
4087
+ return {
4088
+ dir: cwd,
4089
+ notice: `Re-provisioning ${code(slug)} in place \u2014 ${code(cwd)} already holds its ${code("module.json")}.`
4090
+ };
4091
+ }
4092
+ if (existingSlug !== null) {
4093
+ return {
4094
+ dir: cwd,
4095
+ refuse: `${code(cwd)} is the project directory for ${code(existingSlug)}, not ${code(slug)}. Run this from the parent directory, or pass ${code(`--dir <path>`)} to say where ${code(slug)} should go.`
4096
+ };
4097
+ }
4098
+ return {
4099
+ dir: cwd,
4100
+ notice: `Scaffolding into ${code(cwd)} \u2014 it already holds a ${code("module.json")}.`
4101
+ };
4102
+ }
4103
+ let entries;
4104
+ try {
4105
+ entries = readdirSync(cwd);
4106
+ } catch {
4107
+ return { dir: nested };
4108
+ }
4109
+ if (entries.length === 0) {
4110
+ return {
4111
+ dir: cwd,
4112
+ notice: `Scaffolding into ${code(cwd)} \u2014 an empty directory already named ${code(slug)}.`
4113
+ };
4114
+ }
4115
+ return {
4116
+ dir: nested,
4117
+ nestWarning: `You are inside a directory called ${code(slug)}, so this creates ${code(nested)}. Pass ${code("--dir .")} to scaffold here instead.`
4118
+ };
4119
+ }
4120
+ function readManifestSlug(manifestPath2) {
4121
+ try {
4122
+ const parsed = JSON.parse(readFileSync(manifestPath2, "utf-8"));
4123
+ const value = parsed.module?.slug;
4124
+ return typeof value === "string" && value.length > 0 ? value : null;
4125
+ } catch {
4126
+ return null;
4127
+ }
4128
+ }
3817
4129
  async function moduleInitCommand(slugArg, options) {
3818
4130
  intro("Scaffolding a new Dashwise module");
3819
4131
  let slug = slugArg;
@@ -3838,7 +4150,14 @@ async function moduleInitCommand(slugArg, options) {
3838
4150
  return 1;
3839
4151
  }
3840
4152
  const name = options.name ?? humanizeSlug(slug);
3841
- const dir = resolve(options.dir ?? slug);
4153
+ const resolved = resolveTargetDir(slug, options.dir);
4154
+ if (resolved.refuse) {
4155
+ fail(resolved.refuse);
4156
+ return 1;
4157
+ }
4158
+ const dir = resolved.dir;
4159
+ if (resolved.notice) info(resolved.notice);
4160
+ if (resolved.nestWarning) warn(resolved.nestWarning);
3842
4161
  const manifestExists = existsSync(join(dir, "module.json"));
3843
4162
  if (options.provisionOnly && !manifestExists) {
3844
4163
  fail(
@@ -5357,6 +5676,7 @@ function installExcessArgumentsGuard(program2) {
5357
5676
  init_output();
5358
5677
 
5359
5678
  // src/program.ts
5679
+ init_config();
5360
5680
  init_dev();
5361
5681
 
5362
5682
  // src/commands/login.ts
@@ -5807,8 +6127,8 @@ function startMcpProxy(opts) {
5807
6127
  let streamUnavailable = false;
5808
6128
  let streamStarted = false;
5809
6129
  let resolveDone;
5810
- const done = new Promise((resolve13) => {
5811
- resolveDone = resolve13;
6130
+ const done = new Promise((resolve14) => {
6131
+ resolveDone = resolve14;
5812
6132
  });
5813
6133
  const trace = (line) => {
5814
6134
  if (verbose) log2(line);
@@ -6108,13 +6428,13 @@ function startMcpProxy(opts) {
6108
6428
  const { done: finished, value } = await reader.read();
6109
6429
  if (finished) break;
6110
6430
  buffer2 += decoder.decode(value, { stream: true });
6111
- let sep2 = buffer2.indexOf("\n\n");
6112
- while (sep2 !== -1) {
6113
- const event = buffer2.slice(0, sep2);
6114
- buffer2 = buffer2.slice(sep2 + 2);
6431
+ let sep3 = buffer2.indexOf("\n\n");
6432
+ while (sep3 !== -1) {
6433
+ const event = buffer2.slice(0, sep3);
6434
+ buffer2 = buffer2.slice(sep3 + 2);
6115
6435
  const payload = eventData(event);
6116
6436
  if (payload !== null) emitFrame(payload, "server stream");
6117
- sep2 = buffer2.indexOf("\n\n");
6437
+ sep3 = buffer2.indexOf("\n\n");
6118
6438
  }
6119
6439
  }
6120
6440
  } catch (err) {
@@ -6294,7 +6614,7 @@ async function revokeSession(endpointId) {
6294
6614
  () => false
6295
6615
  );
6296
6616
  const timeout = new Promise(
6297
- (resolve13) => setTimeout(() => resolve13(false), REVOKE_TIMEOUT_MS).unref()
6617
+ (resolve14) => setTimeout(() => resolve14(false), REVOKE_TIMEOUT_MS).unref()
6298
6618
  );
6299
6619
  await Promise.race([attempt, timeout]);
6300
6620
  }
@@ -6472,7 +6792,7 @@ async function mcpAddCommand(options = {}) {
6472
6792
  fail(`Could not run \`claude\`: ${target.refused} Run the command above yourself.`);
6473
6793
  return 1;
6474
6794
  }
6475
- return await new Promise((resolve13) => {
6795
+ return await new Promise((resolve14) => {
6476
6796
  const child = spawn(target.command, target.args, { shell: target.shell, stdio: "inherit" });
6477
6797
  let settled = false;
6478
6798
  child.on("error", (err) => {
@@ -6482,16 +6802,16 @@ async function mcpAddCommand(options = {}) {
6482
6802
  fail(
6483
6803
  `Claude Code is not on your PATH, so nothing was registered. Run the command above once ${code("claude")} is installed.`
6484
6804
  );
6485
- resolve13(1);
6805
+ resolve14(1);
6486
6806
  return;
6487
6807
  }
6488
6808
  fail(`Could not run \`claude\`: ${err.message}`);
6489
- resolve13(1);
6809
+ resolve14(1);
6490
6810
  });
6491
6811
  child.on("close", (codeOut) => {
6492
6812
  if (settled) return;
6493
6813
  settled = true;
6494
- resolve13(codeOut ?? 1);
6814
+ resolve14(codeOut ?? 1);
6495
6815
  });
6496
6816
  });
6497
6817
  }
@@ -6499,13 +6819,121 @@ async function mcpAddCommand(options = {}) {
6499
6819
  // src/commands/agent.ts
6500
6820
  init_config();
6501
6821
  init_output();
6822
+ function claudeConfigDir() {
6823
+ const override = process.env.CLAUDE_CONFIG_DIR;
6824
+ if (override !== void 0 && override.length > 0) return override;
6825
+ return join(homedir(), ".claude");
6826
+ }
6827
+ function pluginCacheRoot(marketplace, plugin) {
6828
+ return join(claudeConfigDir(), "plugins", "cache", marketplace, plugin);
6829
+ }
6830
+ function marketplacePluginRoot(marketplace, plugin) {
6831
+ return join(
6832
+ claudeConfigDir(),
6833
+ "plugins",
6834
+ "marketplaces",
6835
+ marketplace,
6836
+ "plugins",
6837
+ plugin
6838
+ );
6839
+ }
6840
+ function readSkillVersion(skillMdPath) {
6841
+ let text4;
6842
+ try {
6843
+ text4 = readFileSync(skillMdPath, "utf-8");
6844
+ } catch {
6845
+ return null;
6846
+ }
6847
+ const lines = text4.split(/\r?\n/);
6848
+ if (lines[0]?.trim() !== "---") return null;
6849
+ for (let i = 1; i < lines.length; i++) {
6850
+ const line = lines[i];
6851
+ if (line.trim() === "---") break;
6852
+ const match = /^version:\s*(.+?)\s*$/.exec(line);
6853
+ if (match) return match[1].replace(/^['"]|['"]$/g, "");
6854
+ }
6855
+ return null;
6856
+ }
6857
+ function readSkills(skillsDir) {
6858
+ if (!existsSync(skillsDir)) return [];
6859
+ let entries;
6860
+ try {
6861
+ entries = readdirSync(skillsDir, { withFileTypes: true });
6862
+ } catch {
6863
+ return [];
6864
+ }
6865
+ return entries.filter((e) => e.isDirectory()).map((e) => {
6866
+ const dir = join(skillsDir, e.name);
6867
+ return {
6868
+ name: e.name,
6869
+ version: readSkillVersion(join(dir, "SKILL.md")),
6870
+ path: dir
6871
+ };
6872
+ }).sort((a, b) => a.name.localeCompare(b.name));
6873
+ }
6874
+ function compareDotted(a, b) {
6875
+ const pa = a.split(".");
6876
+ const pb = b.split(".");
6877
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
6878
+ const na = Number.parseInt(pa[i] ?? "0", 10);
6879
+ const nb = Number.parseInt(pb[i] ?? "0", 10);
6880
+ if (Number.isNaN(na) || Number.isNaN(nb)) return a.localeCompare(b);
6881
+ if (na !== nb) return na - nb;
6882
+ }
6883
+ return 0;
6884
+ }
6885
+ function readInstalledPlugin(marketplace, plugin) {
6886
+ const root = pluginCacheRoot(marketplace, plugin);
6887
+ if (!existsSync(root)) return null;
6888
+ let versions;
6889
+ try {
6890
+ versions = readdirSync(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6891
+ } catch {
6892
+ return null;
6893
+ }
6894
+ if (versions.length === 0) return null;
6895
+ const version = versions.sort(compareDotted)[versions.length - 1];
6896
+ const path = join(root, version);
6897
+ const skillsPath = join(path, "skills");
6898
+ return {
6899
+ version,
6900
+ path,
6901
+ skillsPath: existsSync(skillsPath) ? skillsPath : null,
6902
+ skills: readSkills(skillsPath)
6903
+ };
6904
+ }
6905
+ function readMarketplacePlugin(marketplace, plugin) {
6906
+ const root = marketplacePluginRoot(marketplace, plugin);
6907
+ const manifest = join(root, ".claude-plugin", "plugin.json");
6908
+ if (!existsSync(manifest)) return null;
6909
+ let version;
6910
+ try {
6911
+ const parsed = JSON.parse(readFileSync(manifest, "utf-8"));
6912
+ if (typeof parsed.version !== "string" || parsed.version.length === 0) return null;
6913
+ version = parsed.version;
6914
+ } catch {
6915
+ return null;
6916
+ }
6917
+ const skillsPath = join(root, "skills");
6918
+ return {
6919
+ version,
6920
+ path: root,
6921
+ skillsPath: existsSync(skillsPath) ? skillsPath : null,
6922
+ skills: readSkills(skillsPath)
6923
+ };
6924
+ }
6925
+ function upgradeCommand(plugin, marketplace) {
6926
+ return `claude plugin update ${plugin}@${marketplace}`;
6927
+ }
6928
+
6929
+ // src/commands/agent.ts
6502
6930
  var PLUGIN_NAME = "dashwise";
6503
6931
  var MARKETPLACE_NAME = "dashwise";
6504
6932
  var DEFAULT_MARKETPLACE_SOURCE = "Intuize/dashwise-cli";
6505
- var defaultRunner = (cmd, args, opts) => new Promise((resolve13) => {
6933
+ var defaultRunner = (cmd, args, opts) => new Promise((resolve14) => {
6506
6934
  const target = spawnTarget(cmd, args);
6507
6935
  if ("refused" in target) {
6508
- resolve13({ code: null, stdout: "", stderr: target.refused, missing: false });
6936
+ resolve14({ code: null, stdout: "", stderr: target.refused, missing: false });
6509
6937
  return;
6510
6938
  }
6511
6939
  const child = spawn(target.command, target.args, {
@@ -6520,7 +6948,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolve13) => {
6520
6948
  child.on("error", (err) => {
6521
6949
  if (settled) return;
6522
6950
  settled = true;
6523
- resolve13({
6951
+ resolve14({
6524
6952
  code: null,
6525
6953
  stdout,
6526
6954
  stderr: stderr || err.message,
@@ -6530,9 +6958,35 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolve13) => {
6530
6958
  child.on("close", (exitCode) => {
6531
6959
  if (settled) return;
6532
6960
  settled = true;
6533
- resolve13({ code: exitCode, stdout, stderr, missing: false });
6961
+ resolve14({ code: exitCode, stdout, stderr, missing: false });
6534
6962
  });
6535
6963
  });
6964
+ function readSkillsOnDisk() {
6965
+ const installed = readInstalledPlugin(MARKETPLACE_NAME, PLUGIN_NAME);
6966
+ const latest = readMarketplacePlugin(MARKETPLACE_NAME, PLUGIN_NAME);
6967
+ return {
6968
+ skills_path: installed?.skillsPath ?? null,
6969
+ installed_version: installed?.version ?? null,
6970
+ latest_version: latest?.version ?? null,
6971
+ skills: (installed?.skills ?? []).map((sk) => ({
6972
+ name: sk.name,
6973
+ version: sk.version,
6974
+ path: sk.path
6975
+ })),
6976
+ upgrade_command: upgradeCommand(PLUGIN_NAME, MARKETPLACE_NAME)
6977
+ };
6978
+ }
6979
+ function printSkillsOnDisk(disk) {
6980
+ if (disk.skills_path === null) return;
6981
+ info(
6982
+ `The skills are readable on disk now, without a restart: ${code(disk.skills_path)}`
6983
+ );
6984
+ for (const sk of disk.skills) {
6985
+ out(
6986
+ ` ${dim("skill:")} ${sk.name}${sk.version ? ` ${dim(`v${sk.version}`)}` : ""}`
6987
+ );
6988
+ }
6989
+ }
6536
6990
  function parsePluginList(text4) {
6537
6991
  let parsed;
6538
6992
  try {
@@ -6692,12 +7146,23 @@ async function agentSetupCommand(options = {}, deps = {}) {
6692
7146
  const installed = await step(run, ["plugin", "install", pluginId], json, report);
6693
7147
  if (installed !== 0) return failed(report, source, json, finish, installed);
6694
7148
  report.plugin.installed = true;
7149
+ const disk = readSkillsOnDisk();
7150
+ report.on_disk = disk;
7151
+ if (disk.installed_version !== null) report.plugin.version = disk.installed_version;
6695
7152
  report.next = ["Restart Claude Code so it loads the plugin."];
7153
+ if (disk.skills_path !== null) {
7154
+ report.next.push(`Or read the skills now, without restarting: ${disk.skills_path}`);
7155
+ }
6696
7156
  if (!loggedIn())
6697
7157
  report.next.push("Run `dashwise login` once, so the plugin\u2019s MCP server can start.");
6698
7158
  if (!json) {
6699
- success(`Installed ${pluginId}.`);
6700
- for (const line of report.next) info(line);
7159
+ success(
7160
+ `Installed ${pluginId}${disk.installed_version ? ` v${disk.installed_version}` : ""}.`
7161
+ );
7162
+ info("Restart Claude Code so it loads the plugin.");
7163
+ printSkillsOnDisk(disk);
7164
+ if (!loggedIn())
7165
+ info("Run `dashwise login` once, so the plugin\u2019s MCP server can start.");
6701
7166
  }
6702
7167
  return finish(0);
6703
7168
  }
@@ -6740,10 +7205,59 @@ function printStatus(report) {
6740
7205
  );
6741
7206
  if (p.others?.length) out(`Also installed from other marketplaces: ${p.others.join(", ")}`);
6742
7207
  }
7208
+ async function agentStatusCommand(options = {}) {
7209
+ const installed = readInstalledPlugin(MARKETPLACE_NAME, PLUGIN_NAME);
7210
+ const latest = readMarketplacePlugin(MARKETPLACE_NAME, PLUGIN_NAME);
7211
+ const upgrade = upgradeCommand(PLUGIN_NAME, MARKETPLACE_NAME);
7212
+ const report = {
7213
+ plugin: `${PLUGIN_NAME}@${MARKETPLACE_NAME}`,
7214
+ marketplace: MARKETPLACE_NAME,
7215
+ installed_version: installed?.version ?? null,
7216
+ latest_version: latest?.version ?? null,
7217
+ up_to_date: installed && latest ? installed.version === latest.version : null,
7218
+ skills_path: installed?.skillsPath ?? null,
7219
+ skills: (installed?.skills ?? []).map((sk) => ({
7220
+ name: sk.name,
7221
+ version: sk.version,
7222
+ path: sk.path
7223
+ })),
7224
+ latest_skills: (latest?.skills ?? []).map((sk) => ({
7225
+ name: sk.name,
7226
+ version: sk.version
7227
+ })),
7228
+ upgrade_command: upgrade
7229
+ };
7230
+ if (options.json === true) {
7231
+ out(JSON.stringify(report, null, 2));
7232
+ return 0;
7233
+ }
7234
+ if (installed === null) {
7235
+ out(`Plugin ${report.plugin}: not installed`);
7236
+ info(`Install it with ${code("dashwise agent setup")}.`);
7237
+ return 0;
7238
+ }
7239
+ out(`Plugin ${report.plugin}: ${installed.version} installed`);
7240
+ out(
7241
+ `Marketplace copy: ${latest === null ? "could not tell (no marketplace clone on this machine)" : `${latest.version}${report.up_to_date ? " \u2014 up to date" : ""}`}`
7242
+ );
7243
+ if (report.skills_path !== null) out(`Skills: ${report.skills_path}`);
7244
+ for (const sk of report.skills) {
7245
+ const newer = report.latest_skills.find((l) => l.name === sk.name);
7246
+ const drift = newer && newer.version && sk.version && newer.version !== sk.version ? ` ${dim(`\u2192 ${newer.version} available`)}` : "";
7247
+ out(` - ${sk.name}${sk.version ? ` v${sk.version}` : dim(" (unversioned)")}${drift}`);
7248
+ }
7249
+ if (report.up_to_date === false) {
7250
+ info(`A newer plugin is available. Upgrade with ${code(upgrade)}.`);
7251
+ } else if (report.up_to_date === null) {
7252
+ info(`To check for a newer version: ${code(upgrade)}.`);
7253
+ }
7254
+ return 0;
7255
+ }
6743
7256
 
6744
7257
  // src/commands/workspace.ts
6745
7258
  init_api_client();
6746
7259
  init_config();
7260
+ init_workspaces();
6747
7261
  init_output();
6748
7262
  async function workspaceListCommand(options = {}) {
6749
7263
  const cfg = readConfig();
@@ -6839,6 +7353,111 @@ async function workspaceUseCommand(slug) {
6839
7353
  }
6840
7354
  return 0;
6841
7355
  }
7356
+ async function workspaceTablesCommand(options = {}) {
7357
+ const cfg = readConfig();
7358
+ if (!cfg) {
7359
+ if (options.json) {
7360
+ out(
7361
+ JSON.stringify({
7362
+ ok: false,
7363
+ error: { code: "NOT_LOGGED_IN", message: "Run `dashwise login` first." }
7364
+ })
7365
+ );
7366
+ } else {
7367
+ fail("Not logged in. Run `dashwise login` first.");
7368
+ }
7369
+ return 1;
7370
+ }
7371
+ let workspace2;
7372
+ try {
7373
+ workspace2 = await selectWorkspace({
7374
+ apiUrl: resolveApiUrl(),
7375
+ apiToken: cfg.token,
7376
+ ...options.workspace !== void 0 ? { flag: options.workspace } : cfg.workspaceSlug !== void 0 ? { flag: cfg.workspaceSlug } : {},
7377
+ // A table listing is a read; prompting for a workspace mid-script
7378
+ // would be worse than saying which flag to pass.
7379
+ interactive: false
7380
+ });
7381
+ } catch (err) {
7382
+ if (options.json) {
7383
+ out(
7384
+ JSON.stringify({
7385
+ ok: false,
7386
+ error: { code: "WORKSPACE_UNRESOLVED", message: err.message }
7387
+ })
7388
+ );
7389
+ } else {
7390
+ fail(err.message);
7391
+ }
7392
+ return 1;
7393
+ }
7394
+ let tables;
7395
+ try {
7396
+ tables = await listWorkspaceTables(workspace2.id, {
7397
+ baseUrl: resolveApiUrl(),
7398
+ auth: { token: cfg.token }
7399
+ });
7400
+ } catch (err) {
7401
+ if (err instanceof ApiError && err.status === 401) {
7402
+ if (options.json) {
7403
+ out(
7404
+ JSON.stringify({
7405
+ ok: false,
7406
+ error: { code: "TOKEN_INVALID", message: "Saved token is no longer valid." }
7407
+ })
7408
+ );
7409
+ } else {
7410
+ fail("Saved token is no longer valid. Run `dashwise login` again.");
7411
+ }
7412
+ return 1;
7413
+ }
7414
+ if (options.json) {
7415
+ out(
7416
+ JSON.stringify({
7417
+ ok: false,
7418
+ error: { code: "FETCH_FAILED", message: err.message }
7419
+ })
7420
+ );
7421
+ } else {
7422
+ fail(`Could not list tables: ${err.message}`);
7423
+ }
7424
+ return 1;
7425
+ }
7426
+ if (options.json) {
7427
+ out(
7428
+ JSON.stringify({
7429
+ ok: true,
7430
+ workspace: { id: workspace2.id, slug: workspace2.slug, name: workspace2.name },
7431
+ tables
7432
+ })
7433
+ );
7434
+ return 0;
7435
+ }
7436
+ if (tables.length === 0) {
7437
+ info(`No tables visible to you in workspace ${code(workspace2.slug)}.`);
7438
+ return 0;
7439
+ }
7440
+ out("");
7441
+ info(
7442
+ `${tables.length} table${tables.length === 1 ? "" : "s"} in ${code(workspace2.slug)} ${dim(`(id ${workspace2.id})`)}:`
7443
+ );
7444
+ let lastApp = null;
7445
+ for (const t of tables) {
7446
+ if (t.application_name !== lastApp) {
7447
+ out(` ${dim(t.application_name)}`);
7448
+ lastApp = t.application_name;
7449
+ }
7450
+ out(` ${String(t.table_id).padEnd(8)} ${t.name}`);
7451
+ }
7452
+ out("");
7453
+ info(
7454
+ `Use an id with ${code("dashwise uses add --from-table <id>")} to declare a contract on that table.`
7455
+ );
7456
+ out(
7457
+ ` ${dim(`Not every table can back a uses slot \u2014 a module-managed one is refused; reach those with \`dashwise deps\`.`)}`
7458
+ );
7459
+ return 0;
7460
+ }
6842
7461
 
6843
7462
  // src/commands/module/clone.ts
6844
7463
  init_api_client();
@@ -7532,6 +8151,53 @@ function extractManifestIssues(err) {
7532
8151
 
7533
8152
  // src/commands/module/generate.ts
7534
8153
  init_api_client();
8154
+
8155
+ // src/lib/generator-version.ts
8156
+ var SUPPORTED_GENERATOR_MAJOR = 3;
8157
+ var STAMP_KEY = "dashwiseGeneratedBy";
8158
+ var STAMP_RE = /^sdk-generator@(\d+)(?:\.|$)/;
8159
+ function readGeneratorStamp(files) {
8160
+ const pkg = files["package.json"];
8161
+ if (typeof pkg !== "string") return { kind: "unstamped" };
8162
+ let parsed;
8163
+ try {
8164
+ parsed = JSON.parse(pkg);
8165
+ } catch {
8166
+ return { kind: "unstamped" };
8167
+ }
8168
+ const stamp = parsed?.[STAMP_KEY];
8169
+ if (typeof stamp !== "string") return { kind: "unstamped" };
8170
+ const match = STAMP_RE.exec(stamp);
8171
+ if (!match) return { kind: "unstamped" };
8172
+ return { kind: "stamped", stamp, major: Number.parseInt(match[1], 10) };
8173
+ }
8174
+ var ALLOW_NEWER_GENERATOR_FLAG = "--allow-newer-generator";
8175
+ var SUPPORTED_MAJOR_NOTE = "Generator 3: select fields in `uses` slots are typed as the wire sends them \u2014 the option id on a read, the id or the exact label on a write.";
8176
+ function judgeGeneratorStamp(stamp, opts = {}) {
8177
+ if (stamp.kind === "unstamped") return { ok: true, note: null, warning: null };
8178
+ if (stamp.major > SUPPORTED_GENERATOR_MAJOR) {
8179
+ const skew = `This backend's type generator is ${stamp.stamp} \u2014 major ${stamp.major}, and this @dashai/cli understands major ${SUPPORTED_GENERATOR_MAJOR}. A generator major changes the shape of the generated types`;
8180
+ if (opts.allowNewerGenerator) {
8181
+ return {
8182
+ ok: true,
8183
+ note: null,
8184
+ warning: `${skew}, and ${ALLOW_NEWER_GENERATOR_FLAG} told this CLI to write them anyway. The files are the backend's either way \u2014 generation is server-side \u2014 but nothing here can describe what changed about them, so a compile error in your own code after this is the expected shape of the skew. Upgrade with \`npm i -g @dashai/cli@latest\` when a release exists.`
8185
+ };
8186
+ }
8187
+ return {
8188
+ ok: false,
8189
+ code: "GENERATOR_MAJOR_UNSUPPORTED",
8190
+ message: `${skew}, so nothing was written: upgrade with \`npm i -g @dashai/cli@latest\`, then re-run \`dashwise module generate\`. If no release understands it yet, re-run with ${ALLOW_NEWER_GENERATOR_FLAG} to write it anyway \u2014 the output is identical either way, and you lose only this CLI's account of what changed.`
8191
+ };
8192
+ }
8193
+ if (stamp.major === SUPPORTED_GENERATOR_MAJOR) {
8194
+ return { ok: true, note: SUPPORTED_MAJOR_NOTE, warning: null };
8195
+ }
8196
+ return { ok: true, note: null, warning: null };
8197
+ }
8198
+
8199
+ // src/commands/module/generate.ts
8200
+ init_sdk_version();
7535
8201
  init_output();
7536
8202
  function outputDir(projectRoot) {
7537
8203
  return join(projectRoot, "node_modules", "@dashai", "generated");
@@ -7577,6 +8243,14 @@ async function moduleGenerateCommand(options) {
7577
8243
  fail(`Could not generate SDK: ${err.message}`);
7578
8244
  return 1;
7579
8245
  }
8246
+ const verdict = judgeGeneratorStamp(readGeneratorStamp(res.files), {
8247
+ ...options.allowNewerGenerator !== void 0 ? { allowNewerGenerator: options.allowNewerGenerator } : {}
8248
+ });
8249
+ if (!verdict.ok) {
8250
+ fail(`${verdict.code}: ${verdict.message}`);
8251
+ return 1;
8252
+ }
8253
+ if (verdict.warning !== null) warn(verdict.warning);
7580
8254
  if (options.dryRun) {
7581
8255
  info(`Dry run: ${Object.keys(res.files).length} files`);
7582
8256
  info(`Hash: ${code(res.contentHash)}`);
@@ -7599,8 +8273,25 @@ async function moduleGenerateCommand(options) {
7599
8273
  `Generated ${Object.keys(res.files).length} files into ${code("node_modules/@dashai/generated/")}`
7600
8274
  );
7601
8275
  info(`Hash: ${dim(res.contentHash)}`);
8276
+ if (verdict.note !== null && declaresUses(manifest)) info(verdict.note);
8277
+ warnIfSdkBelowUsesFloor(projectRoot, manifest);
7602
8278
  return 0;
7603
8279
  }
8280
+ function declaresUses(manifest) {
8281
+ const uses = manifest?.uses;
8282
+ return Array.isArray(uses?.tables) && uses.tables.length > 0;
8283
+ }
8284
+ function warnIfSdkBelowUsesFloor(projectRoot, manifest) {
8285
+ if (!declaresUses(manifest)) return;
8286
+ const sdk = checkSdkFloor(projectRoot, USES_SDK_FLOOR);
8287
+ if (sdk.ok) return;
8288
+ warn(
8289
+ `This manifest declares ${code("uses")}, which needs ${code(`@dashai/sdk >= ${sdk.floor}`)} \u2014 this project has ${code(sdk.installed ?? "none")}.`
8290
+ );
8291
+ info(
8292
+ `Every import from ${code("@dashai/generated")} will throw until you upgrade, including your own tables: ${code(`npm install @dashai/sdk@^${sdk.floor}`)}.`
8293
+ );
8294
+ }
7604
8295
 
7605
8296
  // src/commands/module/dev.ts
7606
8297
  init_output();
@@ -7904,6 +8595,7 @@ async function resolveWorkspaceWithFallback(opts) {
7904
8595
 
7905
8596
  // src/commands/module/install.ts
7906
8597
  init_output();
8598
+ init_connect_status();
7907
8599
  async function moduleInstallCommand(slugArg, options) {
7908
8600
  const slug = resolveSlug(slugArg, options.dir);
7909
8601
  if (!slug) {
@@ -8077,6 +8769,8 @@ async function moduleInstallCommand(slugArg, options) {
8077
8769
  const publicUrl = row?.public_url ?? null;
8078
8770
  const installedVersion = row?.module_version?.version ?? targetVersion;
8079
8771
  const runtimeProvisioned = isRealRuntimeUrl(row?.runtime_url);
8772
+ const readiness = await fetchUsesReadiness(result.installation.id, auth);
8773
+ const setupPending = readiness?.setupPending ?? false;
8080
8774
  if (options.json) {
8081
8775
  out(
8082
8776
  JSON.stringify({
@@ -8085,11 +8779,15 @@ async function moduleInstallCommand(slugArg, options) {
8085
8779
  version: installedVersion,
8086
8780
  was_upgrade: result.was_upgrade,
8087
8781
  public_url: publicUrl,
8088
- verified
8782
+ verified,
8783
+ // `null` when the binding surface was unreachable; `[]` when the
8784
+ // module declares no slots. The two are not the same thing.
8785
+ uses: readiness === null ? null : usesInstallJson(readiness),
8786
+ setup_pending: setupPending
8089
8787
  })
8090
8788
  );
8091
8789
  if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
8092
- return 0;
8790
+ return setupPending ? EXIT_SETUP_PENDING : 0;
8093
8791
  }
8094
8792
  if (!verified) {
8095
8793
  success(
@@ -8102,9 +8800,15 @@ async function moduleInstallCommand(slugArg, options) {
8102
8800
  return 0;
8103
8801
  }
8104
8802
  if (runtimeProvisioned && publicUrl) {
8105
- success(
8106
- `Deployed ${code(mod.slug)} ${code(`v${installedVersion}`)} \u2014 live at ${code(publicUrl)}`
8107
- );
8803
+ if (setupPending) {
8804
+ success(
8805
+ `Deployed ${code(mod.slug)} ${code(`v${installedVersion}`)} at ${code(publicUrl)} \u2014 ${code("setup pending")}, not usable yet.`
8806
+ );
8807
+ } else {
8808
+ success(
8809
+ `Deployed ${code(mod.slug)} ${code(`v${installedVersion}`)} \u2014 live at ${code(publicUrl)}`
8810
+ );
8811
+ }
8108
8812
  info(
8109
8813
  `Install ${code(`#${result.installation.id}`)} (kind=${row?.kind ?? "unknown"}) in workspace ${code(workspace2.slug)}${result.was_upgrade ? " \u2014 upgraded in place." : "."}`
8110
8814
  );
@@ -8123,8 +8827,9 @@ async function moduleInstallCommand(slugArg, options) {
8123
8827
  `No public URL yet \u2014 runtime provisioning didn't complete. Re-running ${code("dashwise module install")} is idempotent and repairs a failed provision.`
8124
8828
  );
8125
8829
  }
8830
+ if (readiness !== null) renderUsesReadiness(readiness, result);
8126
8831
  if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
8127
- return 0;
8832
+ return setupPending ? EXIT_SETUP_PENDING : 0;
8128
8833
  }
8129
8834
  function publishedModuleLocalDevGuidance(slug) {
8130
8835
  return `Re-provision local dev any time with ${code(`dashwise init ${slug} --workspace <ws>`)} (or ${code("dashwise module reset-dev-install")}) \u2014 requires backend \u2265 2026-07-15; the fresh dev install COEXISTS with the production install.
@@ -8280,6 +8985,18 @@ function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
8280
8985
  }
8281
8986
  return 1;
8282
8987
  }
8988
+ var EXIT_SETUP_PENDING = 3;
8989
+ function usesInstallJson(readiness) {
8990
+ return {
8991
+ pending: readiness.pending.map((b) => b.uses_slug),
8992
+ slots: readiness.slots.map((b) => ({
8993
+ uses_slug: b.uses_slug,
8994
+ status: b.status,
8995
+ required: b.if_missing !== "optional",
8996
+ table: b.table ?? null
8997
+ }))
8998
+ };
8999
+ }
8283
9000
 
8284
9001
  // src/commands/module/list.ts
8285
9002
  init_api_client();
@@ -8393,6 +9110,109 @@ async function moduleListCommand(options) {
8393
9110
  init_api_client();
8394
9111
  init_config();
8395
9112
  init_output();
9113
+
9114
+ // src/lib/gitignore.ts
9115
+ function escapeLiteral(ch) {
9116
+ return ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9117
+ }
9118
+ function patternToRegex(pattern, anchored) {
9119
+ let out2 = "";
9120
+ let i = 0;
9121
+ while (i < pattern.length) {
9122
+ const ch = pattern[i];
9123
+ if (ch === "*") {
9124
+ if (pattern[i + 1] === "*") {
9125
+ const nextIsSlash = pattern[i + 2] === "/";
9126
+ if (nextIsSlash) {
9127
+ out2 += "(?:.*/)?";
9128
+ i += 3;
9129
+ } else {
9130
+ out2 += ".*";
9131
+ i += 2;
9132
+ }
9133
+ continue;
9134
+ }
9135
+ out2 += "[^/]*";
9136
+ i += 1;
9137
+ continue;
9138
+ }
9139
+ if (ch === "?") {
9140
+ out2 += "[^/]";
9141
+ i += 1;
9142
+ continue;
9143
+ }
9144
+ if (ch === "\\" && i + 1 < pattern.length) {
9145
+ out2 += escapeLiteral(pattern[i + 1]);
9146
+ i += 2;
9147
+ continue;
9148
+ }
9149
+ out2 += escapeLiteral(ch);
9150
+ i += 1;
9151
+ }
9152
+ const prefix = anchored ? "^" : "^(?:.*/)?";
9153
+ return new RegExp(`${prefix}${out2}(?:/.*)?$`);
9154
+ }
9155
+ function parseGitignore(content) {
9156
+ const rules = [];
9157
+ for (const rawLine of content.split(/\r?\n/)) {
9158
+ const line = rawLine.replace(/(?<!\\)\s+$/, "");
9159
+ if (line.length === 0 || line.startsWith("#")) continue;
9160
+ let pattern = line;
9161
+ let negated = false;
9162
+ if (pattern.startsWith("!")) {
9163
+ negated = true;
9164
+ pattern = pattern.slice(1);
9165
+ } else if (pattern.startsWith("\\!")) {
9166
+ pattern = pattern.slice(1);
9167
+ }
9168
+ if (pattern.length === 0) continue;
9169
+ let dirOnly = false;
9170
+ if (pattern.endsWith("/")) {
9171
+ dirOnly = true;
9172
+ pattern = pattern.slice(0, -1);
9173
+ }
9174
+ if (pattern.length === 0) continue;
9175
+ let anchored = pattern.includes("/");
9176
+ if (pattern.startsWith("/")) {
9177
+ anchored = true;
9178
+ pattern = pattern.slice(1);
9179
+ }
9180
+ if (pattern.length === 0) continue;
9181
+ rules.push({
9182
+ negated,
9183
+ dirOnly,
9184
+ regex: patternToRegex(pattern, anchored),
9185
+ source: line
9186
+ });
9187
+ }
9188
+ return rules;
9189
+ }
9190
+ function isIgnored(relPath, isDir, scopes) {
9191
+ let ignored = false;
9192
+ for (const scope of scopes) {
9193
+ if (scope.base !== "" && !relPath.startsWith(`${scope.base}/`)) continue;
9194
+ const local = scope.base === "" ? relPath : relPath.slice(scope.base.length + 1);
9195
+ if (local.length === 0) continue;
9196
+ const segments = local.split("/");
9197
+ const candidates = [];
9198
+ for (let i = 0; i < segments.length - 1; i++) {
9199
+ candidates.push({ path: segments.slice(0, i + 1).join("/"), isDir: true });
9200
+ }
9201
+ candidates.push({ path: local, isDir });
9202
+ for (const rule of scope.rules) {
9203
+ for (const candidate of candidates) {
9204
+ if (rule.dirOnly && !candidate.isDir) continue;
9205
+ if (rule.regex.test(candidate.path)) {
9206
+ ignored = !rule.negated;
9207
+ break;
9208
+ }
9209
+ }
9210
+ }
9211
+ }
9212
+ return ignored;
9213
+ }
9214
+
9215
+ // src/lib/tarball.ts
8396
9216
  var DEFAULT_EXCLUDE_PATTERNS = [
8397
9217
  "node_modules",
8398
9218
  ".next",
@@ -8413,6 +9233,7 @@ var DEFAULT_EXCLUDE_PATTERNS = [
8413
9233
  "dist",
8414
9234
  "build"
8415
9235
  ];
9236
+ var ALWAYS_EXCLUDE_FILES = ["seed.mjs"];
8416
9237
  var PER_FILE_BYTE_LIMIT = 10 * 1024 * 1024;
8417
9238
  var MAX_TARBALL_BYTES = 100 * 1024 * 1024;
8418
9239
  async function packTarball(opts) {
@@ -8424,10 +9245,18 @@ async function packTarball(opts) {
8424
9245
  ...opts.extraExclude ?? []
8425
9246
  ]);
8426
9247
  const filesIncluded = [];
9248
+ const alwaysExcluded = [];
8427
9249
  const warnings = [];
8428
- const candidates = walk(opts.cwd, excludeSet);
9250
+ const respectGitignore = opts.respectGitignore !== false;
9251
+ const walked = walk(opts.cwd, excludeSet, respectGitignore);
9252
+ const candidates = walked.files;
9253
+ let gitignoredCount = walked.ignoredCount;
8429
9254
  for (const abs of candidates) {
8430
9255
  const rel = relative(opts.cwd, abs);
9256
+ if (ALWAYS_EXCLUDE_FILES.includes(rel)) {
9257
+ alwaysExcluded.push(rel);
9258
+ continue;
9259
+ }
8431
9260
  const size = statSync(abs).size;
8432
9261
  if (size > PER_FILE_BYTE_LIMIT) {
8433
9262
  warnings.push(
@@ -8443,6 +9272,7 @@ async function packTarball(opts) {
8443
9272
  );
8444
9273
  }
8445
9274
  filesIncluded.sort();
9275
+ alwaysExcluded.sort();
8446
9276
  const chunks = [];
8447
9277
  await new Promise((resolveDone, rejectDone) => {
8448
9278
  const stream = tar.c(
@@ -8467,14 +9297,30 @@ async function packTarball(opts) {
8467
9297
  `packTarball: tarball is ${formatBytes(bytes.byteLength)}, exceeds ${formatBytes(MAX_TARBALL_BYTES)} max`
8468
9298
  );
8469
9299
  }
8470
- return { bytes, filesIncluded, warnings };
9300
+ return { bytes, filesIncluded, warnings, gitignoredCount, alwaysExcluded };
8471
9301
  }
8472
- function walk(root, excludeSet) {
9302
+ function walk(root, excludeSet, respectGitignore) {
8473
9303
  const out2 = [];
8474
- const stack = [root];
9304
+ let ignoredCount = 0;
9305
+ const rootScopes = [];
9306
+ if (respectGitignore) {
9307
+ const rootIgnore = readIgnoreFile(root);
9308
+ if (rootIgnore !== null) rootScopes.push({ base: "", rules: rootIgnore });
9309
+ }
9310
+ const stack = [
9311
+ { dir: root, scopes: rootScopes }
9312
+ ];
8475
9313
  while (stack.length > 0) {
8476
- const dir = stack.pop();
8477
- if (!dir) continue;
9314
+ const frame = stack.pop();
9315
+ if (!frame) continue;
9316
+ const { dir } = frame;
9317
+ let scopes = frame.scopes;
9318
+ if (respectGitignore && dir !== root) {
9319
+ const nested = readIgnoreFile(dir);
9320
+ if (nested !== null) {
9321
+ scopes = [...scopes, { base: toPosixRel(root, dir), rules: nested }];
9322
+ }
9323
+ }
8478
9324
  let entries;
8479
9325
  try {
8480
9326
  entries = readdirSync(dir, { withFileTypes: true });
@@ -8484,14 +9330,34 @@ function walk(root, excludeSet) {
8484
9330
  for (const entry of entries) {
8485
9331
  if (excludeSet.has(entry.name)) continue;
8486
9332
  const abs = join(dir, entry.name);
8487
- if (entry.isDirectory()) {
8488
- stack.push(abs);
8489
- } else if (entry.isFile()) {
9333
+ const isDir = entry.isDirectory();
9334
+ if (!isDir && !entry.isFile()) continue;
9335
+ if (respectGitignore && scopes.length > 0) {
9336
+ if (isIgnored(toPosixRel(root, abs), isDir, scopes)) {
9337
+ ignoredCount += 1;
9338
+ continue;
9339
+ }
9340
+ }
9341
+ if (isDir) {
9342
+ stack.push({ dir: abs, scopes });
9343
+ } else {
8490
9344
  out2.push(abs);
8491
9345
  }
8492
9346
  }
8493
9347
  }
8494
- return out2;
9348
+ return { files: out2, ignoredCount };
9349
+ }
9350
+ function readIgnoreFile(dir) {
9351
+ const path = join(dir, ".gitignore");
9352
+ if (!existsSync(path)) return null;
9353
+ try {
9354
+ return parseGitignore(readFileSync(path, "utf-8"));
9355
+ } catch {
9356
+ return null;
9357
+ }
9358
+ }
9359
+ function toPosixRel(root, abs) {
9360
+ return relative(root, abs).split(sep).join("/");
8495
9361
  }
8496
9362
  function formatBytes(n) {
8497
9363
  if (n < 1024) return `${n}B`;
@@ -8501,6 +9367,11 @@ function formatBytes(n) {
8501
9367
 
8502
9368
  // src/commands/module/publish.ts
8503
9369
  var VALID_BUMP = /* @__PURE__ */ new Set(["patch", "minor", "major"]);
9370
+ var PACKED_FILE_LIST_CAP = 20;
9371
+ function packedFileLines(files, cap = PACKED_FILE_LIST_CAP) {
9372
+ if (files.length <= cap) return [...files];
9373
+ return [...files.slice(0, cap), `\u2026 and ${files.length - cap} more`];
9374
+ }
8504
9375
  async function modulePublishCommand(options) {
8505
9376
  const projectRoot = resolve(options.dir ?? process.cwd());
8506
9377
  const manifestPath2 = join(projectRoot, "module.json");
@@ -8575,7 +9446,8 @@ async function modulePublishCommand(options) {
8575
9446
  try {
8576
9447
  tarball = await packTarball({
8577
9448
  cwd: projectRoot,
8578
- ...options.exclude !== void 0 ? { extraExclude: options.exclude } : {}
9449
+ ...options.exclude !== void 0 ? { extraExclude: options.exclude } : {},
9450
+ respectGitignore: !options.noRespectGitignore
8579
9451
  });
8580
9452
  } catch (err) {
8581
9453
  fail(`Could not pack tarball: ${err.message}`);
@@ -8584,6 +9456,17 @@ async function modulePublishCommand(options) {
8584
9456
  info(
8585
9457
  `Packed ${tarball.filesIncluded.length} file${tarball.filesIncluded.length === 1 ? "" : "s"} into ${formatBytes2(tarball.bytes.byteLength)} (cap ${formatBytes2(MAX_TARBALL_BYTES)}).`
8586
9458
  );
9459
+ for (const line of packedFileLines(tarball.filesIncluded)) out(` ${dim(line)}`);
9460
+ if (tarball.alwaysExcluded.length > 0) {
9461
+ info(
9462
+ `${tarball.alwaysExcluded.map((f) => code(f)).join(", ")} excluded \u2014 scaffold-local development ${tarball.alwaysExcluded.length === 1 ? "file, not part" : "files, not part"} of the running module, and never published.`
9463
+ );
9464
+ }
9465
+ if (tarball.gitignoredCount > 0) {
9466
+ info(
9467
+ `${tarball.gitignoredCount} path${tarball.gitignoredCount === 1 ? "" : "s"} excluded by ${code(".gitignore")}${options.noRespectGitignore ? "" : ` ${dim("(--no-respect-gitignore to include them)")}`}.`
9468
+ );
9469
+ }
8587
9470
  for (const w of tarball.warnings) warn(w);
8588
9471
  if (!options.yes && process.stdout.isTTY && !options.dryRun) {
8589
9472
  const answer = await select({
@@ -8754,6 +9637,7 @@ init_api_client();
8754
9637
  init_config();
8755
9638
  init_manifest_io();
8756
9639
  init_output();
9640
+ init_connect_status();
8757
9641
  async function moduleStatusCommand(slugArg, options) {
8758
9642
  let slug = slugArg;
8759
9643
  if (!slug) {
@@ -8839,6 +9723,7 @@ async function moduleStatusCommand(slugArg, options) {
8839
9723
  const state = runtimeProvisioned && row.public_url ? "live" : runtimeProvisioned ? "deployed" : "not_deployed";
8840
9724
  const installedVersion = row.module_version?.version ?? null;
8841
9725
  const updateAvailable = isUpdateAvailable(installedVersion, latestVersion);
9726
+ const readiness = await fetchUsesReadiness(row.id, auth);
8842
9727
  if (options.json) {
8843
9728
  out(
8844
9729
  JSON.stringify({
@@ -8854,6 +9739,11 @@ async function moduleStatusCommand(slugArg, options) {
8854
9739
  runtime_url: row.runtime_url,
8855
9740
  public_url: row.public_url,
8856
9741
  state,
9742
+ // C-09: per-slot data-plane readiness for THIS install. `null`
9743
+ // when the binding surface was unreachable — distinct from `[]`,
9744
+ // which means the module declares no slots.
9745
+ uses: readiness === null ? null : usesJson(readiness),
9746
+ setup_pending: readiness?.setupPending ?? false,
8857
9747
  // The coexisting dev install, so a script can see BOTH slots — the
8858
9748
  // human output reports it and `--json` used to hide it.
8859
9749
  dev_install: siblingDevInstall ? { id: siblingDevInstall.id, kind: siblingDevInstall.kind ?? null } : null
@@ -8875,8 +9765,12 @@ async function moduleStatusCommand(slugArg, options) {
8875
9765
  runtimeProvisioned ? `provisioned ${dim(`(${row.runtime_url})`)}` : "not provisioned"
8876
9766
  );
8877
9767
  kv("Public URL:", row.public_url ?? dim("(none)"));
8878
- kv("State:", stateLabel);
9768
+ kv(
9769
+ "State:",
9770
+ readiness?.setupPending ? `${stateLabel} ${dim("\u2014 setup pending")}` : stateLabel
9771
+ );
8879
9772
  out("");
9773
+ if (readiness !== null) renderUsesReadiness(readiness, row);
8880
9774
  if (siblingDevInstall) {
8881
9775
  info(
8882
9776
  `A ${code(`kind=${siblingDevInstall.kind ?? "unknown"}`)} dev install (#${siblingDevInstall.id}) also exists in this workspace and coexists with the production install above \u2014 that is the one ${code("dashwise dev")} talks to.`
@@ -8893,6 +9787,20 @@ async function moduleStatusCommand(slugArg, options) {
8893
9787
  }
8894
9788
  return 0;
8895
9789
  }
9790
+ function usesJson(readiness) {
9791
+ return {
9792
+ setup_pending: readiness.setupPending,
9793
+ slots: readiness.slots.map((b) => ({
9794
+ uses_slug: b.uses_slug,
9795
+ status: b.status,
9796
+ if_missing: b.if_missing ?? null,
9797
+ required: b.if_missing !== "optional",
9798
+ table: b.table ?? null,
9799
+ missing_fields: b.missing_fields ?? [],
9800
+ reason: b.reason ?? null
9801
+ }))
9802
+ };
9803
+ }
8896
9804
 
8897
9805
  // src/commands/module/uninstall.ts
8898
9806
  init_api_client();
@@ -10499,30 +11407,8 @@ function surfaceBindingError(err) {
10499
11407
  init_api_client();
10500
11408
  init_config();
10501
11409
  init_manifest_io();
10502
-
10503
- // src/lib/uses-types.ts
10504
- var PHYSICAL_TO_ABSTRACT = {
10505
- text: "text",
10506
- long_text: "long_text",
10507
- number: "number",
10508
- rating: "number",
10509
- boolean: "boolean",
10510
- // A date-with-time column has physical registry type `date` (with
10511
- // `config.includeTime`), NOT a distinct `datetime` type — the backend
10512
- // registry has no `datetime` key. So introspection only ever reports
10513
- // `date` here; there is no `datetime` physical entry to fold.
10514
- date: "date",
10515
- email: "email",
10516
- url: "url",
10517
- single_select: "single_select",
10518
- multi_select: "multi_select"
10519
- };
10520
- function abstractTypeForPhysical(physicalType) {
10521
- return PHYSICAL_TO_ABSTRACT[physicalType] ?? null;
10522
- }
10523
- var USES_OPERATIONS = ["create", "read", "update", "delete"];
10524
-
10525
- // src/commands/uses/add.ts
11410
+ init_uses_types();
11411
+ init_sdk_version();
10526
11412
  init_output();
10527
11413
  async function usesAddCommand(options) {
10528
11414
  if (!options.fromTable) {
@@ -10543,6 +11429,18 @@ async function usesAddCommand(options) {
10543
11429
  fail(err.message);
10544
11430
  return 1;
10545
11431
  }
11432
+ const sdk = checkSdkFloor(resolve(options.dir ?? process.cwd()), USES_SDK_FLOOR);
11433
+ if (!sdk.ok) {
11434
+ fail(
11435
+ `A ${code("uses")} contract needs ${code(`@dashai/sdk >= ${sdk.floor}`)}; this project has ${code(sdk.installed ?? "none")}.`
11436
+ );
11437
+ info(
11438
+ `Below ${code(sdk.floor)} every import from ${code("@dashai/generated")} throws \u2014 including your own tables. Upgrade first: ${code(`npm install @dashai/sdk@^${sdk.floor}`)}.`
11439
+ );
11440
+ return 1;
11441
+ }
11442
+ const scopeExit = await assertTableInInstallWorkspace(ctx, options.fromTable);
11443
+ if (scopeExit !== 0) return scopeExit;
10546
11444
  let table;
10547
11445
  try {
10548
11446
  table = await introspectWorkspaceTable(
@@ -10553,10 +11451,10 @@ async function usesAddCommand(options) {
10553
11451
  } catch (err) {
10554
11452
  return surfaceIntrospectError(err, options.fromTable);
10555
11453
  }
10556
- const slotSlug = options.slug ?? slugify(table.name);
10557
- if (!isKebab(slotSlug)) {
11454
+ const slotSlug = options.slug ?? slugifyUsesName(table.name);
11455
+ if (!isUsesSlug(slotSlug)) {
10558
11456
  fail(
10559
- `Generated slot slug ${code(slotSlug)} isn't kebab-case. Pass ${code("--slug <kebab-slug>")} explicitly.`
11457
+ `Slot slug ${code(slotSlug || "(empty)")} doesn't match ${code(USES_SLUG_RULE)}. Pass ${code("--slug <snake_case_slug>")} explicitly.`
10560
11458
  );
10561
11459
  return 1;
10562
11460
  }
@@ -10567,6 +11465,37 @@ async function usesAddCommand(options) {
10567
11465
  );
10568
11466
  return 1;
10569
11467
  }
11468
+ const badFields = fields.filter((f) => !isUsesSlug(f.slug));
11469
+ if (badFields.length > 0) {
11470
+ fail(
11471
+ `${badFields.length} field name(s) on ${code(table.name)} don't fold into a legal ${code("uses")} field slug (${code(USES_SLUG_RULE)}):`
11472
+ );
11473
+ for (const f of badFields) {
11474
+ const physical = table.fields.find((p) => p.slug === f.slug);
11475
+ const name = physical?.name ?? f.slug;
11476
+ out(` ${dim("-")} ${code(name)} \u2192 ${code(f.slug || "(empty)")}`);
11477
+ }
11478
+ info(
11479
+ `Rename the field(s) in Dashwise, or declare the slot by hand in ${code("module.json#uses.tables")}. Nothing was written.`
11480
+ );
11481
+ return 1;
11482
+ }
11483
+ const slugCounts = /* @__PURE__ */ new Map();
11484
+ for (const f of fields) slugCounts.set(f.slug, (slugCounts.get(f.slug) ?? 0) + 1);
11485
+ const collidingSlugs = [...slugCounts.entries()].filter(([, n]) => n > 1).map(([slug]) => slug);
11486
+ if (collidingSlugs.length > 0) {
11487
+ fail(
11488
+ `${collidingSlugs.length} generated ${code("uses")} field slug(s) on ${code(table.name)} collide \u2014 the backend refuses a contract with a duplicate field slug:`
11489
+ );
11490
+ for (const slug of collidingSlugs) {
11491
+ const names = table.fields.filter((p) => p.slug === slug && abstractTypeForPhysical(p.type) !== null).map((p) => p.name ?? p.slug);
11492
+ out(` ${dim("-")} ${names.map((n) => code(n)).join(" + ")} \u2192 ${code(slug)}`);
11493
+ }
11494
+ info(
11495
+ `Rename one of each pair in Dashwise, or declare the slot by hand in ${code("module.json#uses.tables")}. Nothing was written.`
11496
+ );
11497
+ return 1;
11498
+ }
10570
11499
  const slot = {
10571
11500
  slug: slotSlug,
10572
11501
  ops,
@@ -10603,6 +11532,13 @@ async function usesAddCommand(options) {
10603
11532
  }
10604
11533
  uses.tables = tables;
10605
11534
  ctx.manifest.uses = uses;
11535
+ const pushed = await pushManifest(ctx);
11536
+ if (!pushed.ok && pushed.rejected) {
11537
+ info(
11538
+ `${code("module.json")} was left unchanged \u2014 the generated slot was rejected before it could be written.`
11539
+ );
11540
+ return 1;
11541
+ }
10606
11542
  try {
10607
11543
  writeManifest(ctx.manifest, ctx.dir);
10608
11544
  } catch (err) {
@@ -10612,8 +11548,7 @@ async function usesAddCommand(options) {
10612
11548
  success(
10613
11549
  `${existingIdx >= 0 ? "Replaced" : "Added"} ${code(`uses.tables[${slotSlug}]`)} in module.json.`
10614
11550
  );
10615
- const pushed = await pushManifest(ctx);
10616
- if (!pushed) {
11551
+ if (!pushed.ok) {
10617
11552
  info(
10618
11553
  `The manifest edit is saved locally. Push it (${code("dashwise dev")} watches + applies, or re-run once the backend is reachable), then ${code(`dashwise bind ${slotSlug} --table ${table.table_id}`)}.`
10619
11554
  );
@@ -10638,6 +11573,52 @@ async function usesAddCommand(options) {
10638
11573
  }
10639
11574
  return 0;
10640
11575
  }
11576
+ async function assertTableInInstallWorkspace(ctx, fromTable) {
11577
+ if (ctx.workspaceId === void 0) return 0;
11578
+ const wanted = Number(fromTable);
11579
+ if (!Number.isInteger(wanted) || wanted <= 0) return 0;
11580
+ let tables;
11581
+ try {
11582
+ tables = await listWorkspaceTables(ctx.workspaceId, ctx.requestOptions);
11583
+ } catch {
11584
+ return 0;
11585
+ }
11586
+ const wantedRow = tables.find((t) => t.table_id === wanted);
11587
+ if (wantedRow !== void 0 && !isModuleManaged(wantedRow)) return 0;
11588
+ if (wantedRow !== void 0) {
11589
+ fail(
11590
+ `Table ${code(String(wanted))} (${wantedRow.name}) is managed by a module, so a ${code("uses")} contract on it could never be bound (${code("TABLE_IS_MODULE_MANAGED")}).`
11591
+ );
11592
+ info(
11593
+ `A ${code("uses")} slot binds a table the WORKSPACE owns. Reach another module's tables as a dependency instead: ${code("dashwise deps")}.`
11594
+ );
11595
+ return 1;
11596
+ }
11597
+ const bindable = tables.filter((t) => !isModuleManaged(t));
11598
+ const hidden = tables.length - bindable.length;
11599
+ fail(
11600
+ `Table ${code(String(wanted))} is not in this install's workspace, so a ${code("uses")} contract on it could never be bound.`
11601
+ );
11602
+ if (bindable.length === 0) {
11603
+ info(
11604
+ hidden > 0 ? `Every table visible to you in that workspace (${hidden}) is managed by a module, so none of them can back a ${code("uses")} slot either. Create a workspace table in Dashwise, or reach a module's tables with ${code("dashwise deps")}.` : `No tables are visible to you in that workspace. Create one in Dashwise, or re-provision the dev install with ${code("dashwise init")} against the right workspace.`
11605
+ );
11606
+ return 1;
11607
+ }
11608
+ out(` ${dim(`${bindable.length} table(s) in this workspace:`)}`);
11609
+ for (const t of bindable) {
11610
+ out(
11611
+ ` - ${t.name} ${dim(`(id ${t.table_id}, in ${t.application_name})`)}`
11612
+ );
11613
+ }
11614
+ info(
11615
+ hidden > 0 ? `That is the workspace's table list \u2014 ${hidden} module-managed table(s) are not shown, because a ${code("uses")} slot cannot bind one (${code("TABLE_IS_MODULE_MANAGED")}); reach those with ${code("dashwise deps")}.` : `That is the workspace's table list \u2014 the server decides which of them can back a ${code("uses")} slot, and refuses a module-managed one (${code("TABLE_IS_MODULE_MANAGED")}); reach those with ${code("dashwise deps")}.`
11616
+ );
11617
+ return 1;
11618
+ }
11619
+ function isModuleManaged(t) {
11620
+ return typeof t.module_id === "number";
11621
+ }
10641
11622
  function loadContext3(opts) {
10642
11623
  const manifest = readManifestOrThrow(opts.dir);
10643
11624
  const moduleSlug = manifest.module?.slug;
@@ -10656,6 +11637,7 @@ function loadContext3(opts) {
10656
11637
  }
10657
11638
  return {
10658
11639
  installationId: entry.installationId,
11640
+ ...entry.workspaceId !== void 0 ? { workspaceId: entry.workspaceId } : {},
10659
11641
  moduleSlug,
10660
11642
  ...opts.dir !== void 0 ? { dir: opts.dir } : {},
10661
11643
  manifest,
@@ -10675,10 +11657,10 @@ async function pushManifest(ctx) {
10675
11657
  warn(
10676
11658
  `Module ${code(ctx.moduleSlug)} isn't registered on the backend yet. Run ${code("dashwise module register")} (or ${code("dashwise init")}) before on-ramping a table.`
10677
11659
  );
10678
- return false;
11660
+ return { ok: false, rejected: false };
10679
11661
  }
10680
11662
  warn(`Couldn't resolve the module id to push the manifest: ${err.message}`);
10681
- return false;
11663
+ return { ok: false, rejected: false };
10682
11664
  }
10683
11665
  try {
10684
11666
  await applyDraftManifest(
@@ -10687,20 +11669,20 @@ async function pushManifest(ctx) {
10687
11669
  ctx.requestOptions
10688
11670
  );
10689
11671
  success("Pushed the manifest \u2014 the backend parked the new slot, ready to bind.");
10690
- return true;
11672
+ return { ok: true };
10691
11673
  } catch (err) {
10692
11674
  if (err instanceof ApiError) {
10693
11675
  if (err.code === "MANIFEST_INVALID" || err.status === 400) {
10694
11676
  fail(`Manifest rejected by the backend validator: ${err.message}`);
10695
11677
  const issues = extractIssues(err.context);
10696
11678
  for (const issue of issues) out(` ${dim("-")} ${issue}`);
10697
- } else {
10698
- fail(`Manifest push failed: ${err.code} ${err.status} \u2014 ${err.message}`);
11679
+ return { ok: false, rejected: true };
10699
11680
  }
10700
- } else {
10701
- fail(`Manifest push failed: ${err.message}`);
11681
+ fail(`Manifest push failed: ${err.code} ${err.status} \u2014 ${err.message}`);
11682
+ return { ok: false, rejected: false };
10702
11683
  }
10703
- return false;
11684
+ fail(`Manifest push failed: ${err.message}`);
11685
+ return { ok: false, rejected: false };
10704
11686
  }
10705
11687
  }
10706
11688
  function extractIssues(context) {
@@ -10750,13 +11732,6 @@ function parseOps(opsCsv) {
10750
11732
  }
10751
11733
  return USES_OPERATIONS.filter((o) => seen.has(o));
10752
11734
  }
10753
- function slugify(name) {
10754
- return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10755
- }
10756
- var KEBAB_REGEX = /^[a-z][a-z0-9-]*[a-z0-9]$/;
10757
- function isKebab(slug) {
10758
- return KEBAB_REGEX.test(slug);
10759
- }
10760
11735
  function surfaceIntrospectError(err, tableRef) {
10761
11736
  if (err instanceof ApiError) {
10762
11737
  switch (err.code) {
@@ -12061,28 +13036,36 @@ function formatTimestamp(iso) {
12061
13036
  return iso.slice(0, 19).replace("T", " ");
12062
13037
  }
12063
13038
  function sleep2(ms) {
12064
- return new Promise((resolve13) => setTimeout(resolve13, ms));
13039
+ return new Promise((resolve14) => setTimeout(resolve14, ms));
12065
13040
  }
12066
13041
 
12067
13042
  // src/commands/module/action-log.ts
12068
13043
  init_api_client();
13044
+ init_manifest_io();
12069
13045
  init_output();
12070
13046
  var POLL_INTERVAL_MS2 = 5e3;
12071
13047
  async function moduleActionLogCommand(options) {
12072
- if (options.installation === void 0 && options.module === void 0) {
12073
- fail("Must pass either `--installation <id>` or `--module <slug>`.");
12074
- return 1;
12075
- }
12076
13048
  if (options.installation !== void 0 && options.module !== void 0) {
12077
13049
  fail("`--installation` and `--module` are mutually exclusive \u2014 pass one or the other.");
12078
13050
  return 1;
12079
13051
  }
13052
+ let moduleSlug = options.module;
13053
+ if (options.installation === void 0 && moduleSlug === void 0) {
13054
+ const inferred = inferModuleSlug(options.dir);
13055
+ if (inferred === null) {
13056
+ fail(
13057
+ "Pass `--installation <id>` or `--module <slug>`, or run this inside a module project directory containing module.json."
13058
+ );
13059
+ return 1;
13060
+ }
13061
+ moduleSlug = inferred;
13062
+ }
12080
13063
  let installationId = options.installation;
12081
- if (installationId === void 0 && options.module !== void 0) {
13064
+ if (installationId === void 0 && moduleSlug !== void 0) {
12082
13065
  try {
12083
13066
  const resolved = await apiRequest(
12084
13067
  "GET",
12085
- `/api/modules/by-slug/${encodeURIComponent(options.module)}/my-installation`
13068
+ `/api/modules/by-slug/${encodeURIComponent(moduleSlug)}/my-installation`
12086
13069
  );
12087
13070
  installationId = resolved.installation_id;
12088
13071
  } catch (err) {
@@ -12155,7 +13138,7 @@ async function runTail2(installationId, options) {
12155
13138
  }
12156
13139
  dim(`(tail: transient error \u2014 ${err.message})`);
12157
13140
  }
12158
- await new Promise((resolve13) => setTimeout(resolve13, POLL_INTERVAL_MS2));
13141
+ await new Promise((resolve14) => setTimeout(resolve14, POLL_INTERVAL_MS2));
12159
13142
  }
12160
13143
  }
12161
13144
  function printTable(rows) {
@@ -12220,6 +13203,17 @@ function mapAndPrintError(err, json) {
12220
13203
  else fail(msg);
12221
13204
  return 1;
12222
13205
  }
13206
+ function inferModuleSlug(dir) {
13207
+ try {
13208
+ const manifest = readManifestOrThrow(
13209
+ dir ?? process.cwd()
13210
+ );
13211
+ const slug = manifest?.module?.slug;
13212
+ return typeof slug === "string" && slug.length > 0 ? slug : null;
13213
+ } catch {
13214
+ return null;
13215
+ }
13216
+ }
12223
13217
 
12224
13218
  // src/commands/module/logs.ts
12225
13219
  init_api_client();
@@ -12529,7 +13523,7 @@ async function sleep3(ms, aborted) {
12529
13523
  const slice = 100;
12530
13524
  let waited = 0;
12531
13525
  while (waited < ms && !aborted()) {
12532
- await new Promise((resolve13) => setTimeout(resolve13, Math.min(slice, ms - waited)));
13526
+ await new Promise((resolve14) => setTimeout(resolve14, Math.min(slice, ms - waited)));
12533
13527
  waited += slice;
12534
13528
  }
12535
13529
  }
@@ -12539,7 +13533,7 @@ init_output();
12539
13533
  var program = new Command();
12540
13534
  program.name("dashwise").description("Dashwise CLI \u2014 log in, author modules, publish to the marketplace.").version(getVersion(), "-v, --version", "Show the CLI version + exit").option(
12541
13535
  "--api-url <url>",
12542
- "Override Dashwise backend URL (default: $DASHWISE_API_URL or saved config or http://localhost:3000)"
13536
+ `Override Dashwise backend URL (default: $DASHWISE_API_URL, else your saved login, else ${KNOWN_API_URLS.dev}). Prod is ${KNOWN_API_URLS.prod}; pass a localhost URL to target a backend you are running yourself.`
12543
13537
  );
12544
13538
  program.hook("preAction", () => {
12545
13539
  const apiUrl = program.opts().apiUrl?.trim();
@@ -12630,7 +13624,9 @@ mcp.command("add").description(
12630
13624
  });
12631
13625
  }
12632
13626
  );
12633
- var agent = program.command("agent").description("Set up a coding agent to work with Dashwise (`dashwise agent setup`).");
13627
+ var agent = program.command("agent").description(
13628
+ "Set up a coding agent to work with Dashwise (`dashwise agent setup`, `dashwise agent status`)."
13629
+ );
12634
13630
  agent.command("setup").description(
12635
13631
  "Install the Dashwise plugin into Claude Code \u2014 its skills, the `dashwise` MCP server and the secrets guard \u2014 by running `claude plugin marketplace add` and `claude plugin install`. Without Claude Code, prints the two commands instead."
12636
13632
  ).option(
@@ -12643,12 +13639,27 @@ agent.command("setup").description(
12643
13639
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
12644
13640
  });
12645
13641
  });
13642
+ agent.command("status").description(
13643
+ "Report the installed Dashwise plugin and skills: their versions, the path the skills are readable at without a restart, the newest version this machine has fetched, and the command that upgrades. Reads the filesystem only \u2014 it does not need Claude Code on the PATH."
13644
+ ).option("--json", "Print one JSON object on stdout instead of the report.").action(async (cmdOpts) => {
13645
+ process.exitCode = await agentStatusCommand({
13646
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
13647
+ });
13648
+ });
12646
13649
  var workspace = program.command("workspace").description("Workspace context commands.");
12647
13650
  workspace.command("list").description("List accessible workspaces.").option("--json", "Emit JSON to stdout instead of human-readable columns.").action(async (cmdOpts) => {
12648
13651
  process.exitCode = await workspaceListCommand({
12649
13652
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
12650
13653
  });
12651
13654
  });
13655
+ workspace.command("tables").description(
13656
+ "List a workspace's tables with their numeric ids \u2014 the ids `dashwise uses add --from-table <id>` needs. Defaults to the saved default workspace."
13657
+ ).option("--workspace <slug>", "Workspace slug or numeric id (default: the saved default).").option("--json", "Emit JSON to stdout instead of human-readable columns.").action(async (cmdOpts) => {
13658
+ process.exitCode = await workspaceTablesCommand({
13659
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
13660
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
13661
+ });
13662
+ });
12652
13663
  workspace.command("use <slug>").description("Set the default workspace for subsequent commands.").action(async (slug) => {
12653
13664
  process.exitCode = await workspaceUseCommand(slug);
12654
13665
  });
@@ -12780,12 +13791,18 @@ moduleCmd.command("export [dir]").description(
12780
13791
  });
12781
13792
  }
12782
13793
  );
12783
- moduleCmd.command("generate").description("Regenerate typed wrappers from the local module.json into node_modules/@dashai/generated/.").option("--dir <path>", "Project root (default: current directory).").option("--dry-run", "Print summary without writing files.").action(async (cmdOpts) => {
12784
- process.exitCode = await moduleGenerateCommand({
12785
- ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
12786
- ...cmdOpts.dryRun !== void 0 ? { dryRun: cmdOpts.dryRun } : {}
12787
- });
12788
- });
13794
+ moduleCmd.command("generate").description("Regenerate typed wrappers from the local module.json into node_modules/@dashai/generated/. Refuses output stamped with a generator major this CLI does not know (GENERATOR_MAJOR_UNSUPPORTED) before writing anything.").option("--dir <path>", "Project root (default: current directory).").option("--dry-run", "Print summary without writing files.").option(
13795
+ "--allow-newer-generator",
13796
+ "Write output from a generator major this CLI does not know, with a warning, instead of refusing (GENERATOR_MAJOR_UNSUPPORTED). Generation is server-side, so the files are identical to what a newer CLI would write \u2014 you lose only this CLI's account of what changed. For a backend that has rolled a generator major ahead of the CLI release."
13797
+ ).action(
13798
+ async (cmdOpts) => {
13799
+ process.exitCode = await moduleGenerateCommand({
13800
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
13801
+ ...cmdOpts.dryRun !== void 0 ? { dryRun: cmdOpts.dryRun } : {},
13802
+ ...cmdOpts.allowNewerGenerator !== void 0 ? { allowNewerGenerator: cmdOpts.allowNewerGenerator } : {}
13803
+ });
13804
+ }
13805
+ );
12789
13806
  moduleCmd.command("dev").description("Watch module.json + regenerate typed wrappers on every save. Ctrl-C to stop.").option("--dir <path>", "Project root (default: current directory).").option("--debounce-ms <n>", "Filesystem change debounce window (default: 250ms).", parseIntOption).action(async (cmdOpts) => {
12790
13807
  process.exitCode = await moduleDevCommand({
12791
13808
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
@@ -12936,7 +13953,7 @@ usesCmd.command("add").description(
12936
13953
  "Comma-separated operations to declare (create,read,update,delete). Default: read."
12937
13954
  ).option(
12938
13955
  "--purpose <text>",
12939
- "Optional author-facing explanation, shown to admins at bind/consent."
13956
+ `What your module does with these fields, in the admin's words \u2014 shown at bind/consent (up to 500 chars). Be specific: "Reads invoice totals to draw the revenue chart", not "access data".`
12940
13957
  ).option(
12941
13958
  "--no-bind",
12942
13959
  "Only write + push the manifest entry; don't attempt to bind it to the source table."
@@ -13119,6 +14136,9 @@ envCmd.command("export").description(
13119
14136
  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(
13120
14137
  "--exclude <pattern...>",
13121
14138
  "Extra exclude patterns (segment-matched) appended to the defaults."
14139
+ ).option(
14140
+ "--no-respect-gitignore",
14141
+ "Pack files the project's .gitignore excludes (default: respect it)."
13122
14142
  ).option(
13123
14143
  "--no-wait",
13124
14144
  "Enqueue the build and exit immediately (print the build id) instead of polling to completion."
@@ -13134,6 +14154,9 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
13134
14154
  ...cmdOpts.dryRun !== void 0 ? { dryRun: cmdOpts.dryRun } : {},
13135
14155
  ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
13136
14156
  ...cmdOpts.exclude !== void 0 ? { exclude: cmdOpts.exclude } : {},
14157
+ // commander maps `--no-respect-gitignore` to
14158
+ // `respectGitignore === false`.
14159
+ ...cmdOpts.respectGitignore === false ? { noRespectGitignore: true } : {},
13137
14160
  // commander maps `--no-wait` to `wait === false`.
13138
14161
  ...cmdOpts.wait === false ? { noWait: true } : {},
13139
14162
  ...timeoutSec !== void 0 && Number.isFinite(timeoutSec) ? { timeoutSec } : {},
@@ -13142,7 +14165,7 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
13142
14165
  }
13143
14166
  );
13144
14167
  moduleCmd.command("install [slug]").alias("deploy").description(
13145
- "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."
14168
+ '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.'
13146
14169
  ).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(
13147
14170
  "--keep-dev-install",
13148
14171
  "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."
@@ -13211,7 +14234,10 @@ moduleCmd.command("query-log").description(
13211
14234
  }
13212
14235
  );
13213
14236
  moduleCmd.command("action-log").description(
13214
- "Read the per-installation server-action audit log. Shows both cross-module and own-module dispatches. Filter by role (caller / provider), slow, since, cross-module-only. `--tail` streams new entries every 5s. Pass either `--installation <id>` or `--module <slug>` (slug resolves to your accessible installation)."
14237
+ "Read the per-installation server-action audit log. Shows both cross-module and own-module dispatches. Filter by role (caller / provider), slow, since, cross-module-only. `--tail` streams new entries every 5s. Pass `--installation <id>` or `--module <slug>`, or run it inside a module project and the slug is read from module.json like every other module command."
14238
+ ).option(
14239
+ "--dir <path>",
14240
+ "Project root to read module.json from when neither --installation nor --module is given (default: current directory)."
13215
14241
  ).option(
13216
14242
  "--installation <id>",
13217
14243
  "Installation id to inspect. Mutually exclusive with `--module`.",
@@ -13233,6 +14259,7 @@ moduleCmd.command("action-log").description(
13233
14259
  return;
13234
14260
  }
13235
14261
  process.exitCode = await moduleActionLogCommand({
14262
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
13236
14263
  ...cmdOpts.installation !== void 0 ? { installation: cmdOpts.installation } : {},
13237
14264
  ...cmdOpts.module !== void 0 ? { module: cmdOpts.module } : {},
13238
14265
  ...cmdOpts.role !== void 0 ? { role: cmdOpts.role } : {},
@@ -13390,7 +14417,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
13390
14417
  }
13391
14418
  );
13392
14419
  function getVersion() {
13393
- return "0.19.0";
14420
+ return "0.20.0";
13394
14421
  }
13395
14422
  function parseIntOption(value) {
13396
14423
  const n = Number.parseInt(value, 10);