@dashai/cli 0.18.3 → 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.
package/dist/bin.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import pc from 'picocolors';
3
3
  import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, watch, readdirSync, chmodSync, renameSync, mkdtempSync, statSync } from 'fs';
4
- import { tmpdir, platform, homedir } from 'os';
5
- import { resolve, join, relative, dirname, basename, sep } from 'path';
4
+ import { hostname, tmpdir, platform, homedir } from 'os';
5
+ import { resolve, join, relative, dirname, basename, win32, sep } from 'path';
6
6
  import { spawn, spawnSync } from 'child_process';
7
7
  import { intro, log, select, isCancel, cancel, spinner, outro, text, multiselect, confirm } from '@clack/prompts';
8
8
  import { Command, Option } from 'commander';
@@ -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
 
@@ -236,10 +278,13 @@ function asEnvelope(v) {
236
278
  const env = v;
237
279
  const out2 = {};
238
280
  if (typeof env.code === "string") out2.code = env.code;
281
+ else if (typeof env.error === "string" && CODE_SHAPE.test(env.error)) out2.code = env.error;
239
282
  if (typeof env.message === "string") out2.message = env.message;
240
283
  if (typeof env.retriable === "boolean") out2.retriable = env.retriable;
241
284
  if (env.context && typeof env.context === "object") {
242
285
  out2.context = env.context;
286
+ } else if (env.details && typeof env.details === "object") {
287
+ out2.context = env.details;
243
288
  }
244
289
  return out2;
245
290
  }
@@ -419,8 +464,38 @@ async function declineBinding(installationId, usesSlug, options = {}) {
419
464
  options
420
465
  );
421
466
  }
422
- function slugifyFieldName(name) {
423
- 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
+ );
424
499
  }
425
500
  async function introspectWorkspaceTable(_installationId, tableNameOrId, options = {}) {
426
501
  const ref = String(tableNameOrId).trim();
@@ -512,10 +587,11 @@ async function getRuntimeLogs(installationId, opts = {}, options = {}) {
512
587
  options
513
588
  );
514
589
  }
515
- var ApiError;
590
+ var ApiError, CODE_SHAPE, slugifyFieldName;
516
591
  var init_api_client = __esm({
517
592
  "src/lib/api-client.ts"() {
518
593
  init_config();
594
+ init_uses_types();
519
595
  ApiError = class extends Error {
520
596
  status;
521
597
  code;
@@ -532,6 +608,8 @@ var init_api_client = __esm({
532
608
  this.retriable = envelope?.retriable ?? status >= 500;
533
609
  }
534
610
  };
611
+ CODE_SHAPE = /^[A-Z][A-Z0-9_]*$/;
612
+ slugifyFieldName = slugifyUsesName;
535
613
  }
536
614
  });
537
615
 
@@ -601,10 +679,109 @@ async function safeGet(fn) {
601
679
  return null;
602
680
  }
603
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;
604
780
  var init_connect_status = __esm({
605
781
  "src/lib/connect-status.ts"() {
606
782
  init_api_client();
607
783
  init_output();
784
+ SETUP_SETTINGS_PATH = "Modules \u2192 Installed modules \u2192 Technical \u2192 Data connections";
608
785
  }
609
786
  });
610
787
  function manifestPath(dir) {
@@ -801,6 +978,33 @@ var init_manifest_poll = __esm({
801
978
  init_manifest_merge();
802
979
  }
803
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
+ });
804
1008
  async function devCommand(options) {
805
1009
  if (options.sdkOnly && options.nextOnly) {
806
1010
  fail(
@@ -1074,22 +1278,19 @@ function pipeWithPrefix(src, dst, prefix) {
1074
1278
  const lines = combined.split("\n");
1075
1279
  leftover = lines.pop() ?? "";
1076
1280
  for (const line of lines) {
1077
- dst.write(`${prefix}${line}
1281
+ dst.write(`${prefix}${redactSecrets(line)}
1078
1282
  `);
1079
1283
  }
1080
1284
  });
1081
1285
  src.on("end", () => {
1082
1286
  if (leftover.length > 0) {
1083
- dst.write(`${prefix}${leftover}
1287
+ dst.write(`${prefix}${redactSecrets(leftover)}
1084
1288
  `);
1085
1289
  }
1086
1290
  });
1087
1291
  }
1088
1292
  function maskedToken(token) {
1089
- if (token.length <= 12) return "****";
1090
- const prefix = token.slice(0, 4);
1091
- const tail = token.slice(-4);
1092
- return `${prefix}\u2026${tail}`;
1293
+ return maskSecret(token);
1093
1294
  }
1094
1295
  async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
1095
1296
  if (process.env.DASHWISE_API_KEY) return null;
@@ -1167,13 +1368,13 @@ async function resolveDevPort(requested) {
1167
1368
  }
1168
1369
  async function isPortFree(port) {
1169
1370
  const { createServer } = await import('net');
1170
- return new Promise((resolve13) => {
1371
+ return new Promise((resolve14) => {
1171
1372
  const server = createServer();
1172
1373
  server.once("error", () => {
1173
- resolve13(false);
1374
+ resolve14(false);
1174
1375
  });
1175
1376
  server.once("listening", () => {
1176
- server.close(() => resolve13(true));
1377
+ server.close(() => resolve14(true));
1177
1378
  });
1178
1379
  server.listen(port, "0.0.0.0");
1179
1380
  });
@@ -1244,6 +1445,7 @@ var init_dev = __esm({
1244
1445
  init_manifest_io();
1245
1446
  init_manifest_poll();
1246
1447
  init_output();
1448
+ init_redact();
1247
1449
  DEFAULT_NEXT_PORT = 3e3;
1248
1450
  PREFIX_SDK = `\x1B[36m[sdk]\x1B[0m `;
1249
1451
  PREFIX_NEXT = `\x1B[35m[next]\x1B[0m `;
@@ -1253,6 +1455,120 @@ var init_dev = __esm({
1253
1455
  PORT_PROBE_LIMIT = 10;
1254
1456
  }
1255
1457
  });
1458
+ async function selectWorkspace(opts) {
1459
+ const workspaces = await apiRequest(
1460
+ "GET",
1461
+ "/api/workspaces",
1462
+ void 0,
1463
+ { baseUrl: opts.apiUrl, auth: { token: opts.apiToken } }
1464
+ );
1465
+ if (workspaces.length === 0) {
1466
+ throw new Error(
1467
+ "No workspaces found for your account. Create a workspace at the Dashwise dashboard first."
1468
+ );
1469
+ }
1470
+ if (opts.flag !== void 0) {
1471
+ return matchWorkspaceOrThrow(workspaces, opts.flag);
1472
+ }
1473
+ if (workspaces.length === 1) {
1474
+ return workspaces[0];
1475
+ }
1476
+ if (opts.interactive && process.stdout.isTTY) {
1477
+ const choice = await select({
1478
+ message: "Which workspace should this module live in?",
1479
+ options: workspaces.map((w) => ({
1480
+ value: w.slug,
1481
+ label: w.name,
1482
+ hint: `slug=${w.slug}, id=${w.id}`
1483
+ }))
1484
+ });
1485
+ if (isCancel(choice)) {
1486
+ throw new Error("Aborted.");
1487
+ }
1488
+ const picked = workspaces.find((w) => w.slug === choice);
1489
+ if (!picked) {
1490
+ throw new Error(`Internal error: selection "${String(choice)}" did not match any workspace.`);
1491
+ }
1492
+ return picked;
1493
+ }
1494
+ const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id}, name="${w.name}")`).join("\n");
1495
+ throw new Error(
1496
+ `You have ${workspaces.length} workspaces. Re-run with --workspace <slug-or-id> to pick one:
1497
+ ${list}`
1498
+ );
1499
+ }
1500
+ function matchWorkspaceOrThrow(workspaces, flag) {
1501
+ const asNumber = Number.parseInt(flag, 10);
1502
+ const isNumeric = !Number.isNaN(asNumber) && String(asNumber) === flag;
1503
+ const match = isNumeric ? workspaces.find((w) => w.id === asNumber) : workspaces.find((w) => w.slug === flag);
1504
+ if (!match) {
1505
+ const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id})`).join("\n");
1506
+ throw new Error(`No workspace matching --workspace=${flag}. Available:
1507
+ ${list}`);
1508
+ }
1509
+ return match;
1510
+ }
1511
+ var init_workspaces = __esm({
1512
+ "src/lib/workspaces.ts"() {
1513
+ init_api_client();
1514
+ }
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
+ });
1256
1572
 
1257
1573
  // src/lib/scaffold.ts
1258
1574
  function isDeployTarget(value) {
@@ -1333,7 +1649,12 @@ function packageJson(opts) {
1333
1649
  start: "next start"
1334
1650
  },
1335
1651
  dependencies: {
1336
- "@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,
1337
1658
  next: "^15.0.0",
1338
1659
  react: "^19.0.0",
1339
1660
  "react-dom": "^19.0.0"
@@ -2359,10 +2680,11 @@ function packageJsonCustom(opts) {
2359
2680
  start: "next start"
2360
2681
  },
2361
2682
  dependencies: {
2362
- // >=2.4.0 for `@dashai/sdk/platform`, which app/layout.tsx imports. A
2363
- // ^2.3.0 floor resolves to 2.3.0 until 2.4.0 publishes, failing the
2364
- // first `next build` with an opaque module-not-found.
2365
- "@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,
2366
2688
  next: "^15.0.0",
2367
2689
  react: "^19.0.0",
2368
2690
  "react-dom": "^19.0.0",
@@ -3747,69 +4069,63 @@ primary_region = "iad"
3747
4069
  var DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL, MODULE_SLUG_REGEX;
3748
4070
  var init_scaffold = __esm({
3749
4071
  "src/lib/scaffold.ts"() {
4072
+ init_sdk_version();
3750
4073
  DEPLOY_TARGETS = ["railway", "vercel", "fly"];
3751
4074
  SCAFFOLD_DEFAULT_API_URL = "http://localhost:3000";
3752
4075
  MODULE_SLUG_REGEX = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
3753
4076
  }
3754
4077
  });
3755
- async function selectWorkspace(opts) {
3756
- const workspaces = await apiRequest(
3757
- "GET",
3758
- "/api/workspaces",
3759
- void 0,
3760
- { baseUrl: opts.apiUrl, auth: { token: opts.apiToken } }
3761
- );
3762
- if (workspaces.length === 0) {
3763
- throw new Error(
3764
- "No workspaces found for your account. Create a workspace at the Dashwise dashboard first."
3765
- );
3766
- }
3767
- if (opts.flag !== void 0) {
3768
- return matchWorkspaceOrThrow(workspaces, opts.flag);
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
+ };
3769
4102
  }
3770
- if (workspaces.length === 1) {
3771
- return workspaces[0];
4103
+ let entries;
4104
+ try {
4105
+ entries = readdirSync(cwd);
4106
+ } catch {
4107
+ return { dir: nested };
3772
4108
  }
3773
- if (opts.interactive && process.stdout.isTTY) {
3774
- const choice = await select({
3775
- message: "Which workspace should this module live in?",
3776
- options: workspaces.map((w) => ({
3777
- value: w.slug,
3778
- label: w.name,
3779
- hint: `slug=${w.slug}, id=${w.id}`
3780
- }))
3781
- });
3782
- if (isCancel(choice)) {
3783
- throw new Error("Aborted.");
3784
- }
3785
- const picked = workspaces.find((w) => w.slug === choice);
3786
- if (!picked) {
3787
- throw new Error(`Internal error: selection "${String(choice)}" did not match any workspace.`);
3788
- }
3789
- return picked;
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
+ };
3790
4114
  }
3791
- const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id}, name="${w.name}")`).join("\n");
3792
- throw new Error(
3793
- `You have ${workspaces.length} workspaces. Re-run with --workspace <slug-or-id> to pick one:
3794
- ${list}`
3795
- );
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
+ };
3796
4119
  }
3797
- function matchWorkspaceOrThrow(workspaces, flag) {
3798
- const asNumber = Number.parseInt(flag, 10);
3799
- const isNumeric = !Number.isNaN(asNumber) && String(asNumber) === flag;
3800
- const match = isNumeric ? workspaces.find((w) => w.id === asNumber) : workspaces.find((w) => w.slug === flag);
3801
- if (!match) {
3802
- const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id})`).join("\n");
3803
- throw new Error(`No workspace matching --workspace=${flag}. Available:
3804
- ${list}`);
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;
3805
4127
  }
3806
- return match;
3807
4128
  }
3808
- var init_workspaces = __esm({
3809
- "src/lib/workspaces.ts"() {
3810
- init_api_client();
3811
- }
3812
- });
3813
4129
  async function moduleInitCommand(slugArg, options) {
3814
4130
  intro("Scaffolding a new Dashwise module");
3815
4131
  let slug = slugArg;
@@ -3834,7 +4150,14 @@ async function moduleInitCommand(slugArg, options) {
3834
4150
  return 1;
3835
4151
  }
3836
4152
  const name = options.name ?? humanizeSlug(slug);
3837
- 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);
3838
4161
  const manifestExists = existsSync(join(dir, "module.json"));
3839
4162
  if (options.provisionOnly && !manifestExists) {
3840
4163
  fail(
@@ -5353,26 +5676,86 @@ function installExcessArgumentsGuard(program2) {
5353
5676
  init_output();
5354
5677
 
5355
5678
  // src/program.ts
5679
+ init_config();
5356
5680
  init_dev();
5357
5681
 
5358
5682
  // src/commands/login.ts
5359
5683
  init_api_client();
5684
+
5685
+ // src/lib/device-code.ts
5686
+ init_api_client();
5687
+ var DEFAULT_TOKEN_NAME = "dashwise-cli";
5688
+ async function requestDeviceCode(args) {
5689
+ return await apiRequest(
5690
+ "POST",
5691
+ "/api/auth/cli/device-code",
5692
+ { name: args.name ?? DEFAULT_TOKEN_NAME },
5693
+ { auth: "public", baseUrl: args.apiUrl }
5694
+ );
5695
+ }
5696
+ async function pollForApproval(args) {
5697
+ const sleep4 = args.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5698
+ const now = args.now ?? (() => Date.now());
5699
+ const pollIntervalMs = Math.max(1e3, args.intervalSeconds * 1e3);
5700
+ const deadline = now() + args.expiresInSeconds * 1e3;
5701
+ while (now() < deadline) {
5702
+ let res;
5703
+ try {
5704
+ res = await apiRequest(
5705
+ "POST",
5706
+ "/api/auth/cli/poll",
5707
+ { deviceCode: args.deviceCode },
5708
+ { auth: "public", baseUrl: args.apiUrl }
5709
+ );
5710
+ } catch (err) {
5711
+ return { status: "error", message: err.message };
5712
+ }
5713
+ switch (res.status) {
5714
+ case "pending":
5715
+ case "slow_down":
5716
+ await sleep4(pollIntervalMs);
5717
+ continue;
5718
+ case "denied":
5719
+ return { status: "denied" };
5720
+ case "expired":
5721
+ return { status: "expired" };
5722
+ case "approved":
5723
+ if (!res.token || !res.tokenId) return { status: "no_token" };
5724
+ return { status: "approved", token: res.token, tokenId: res.tokenId };
5725
+ }
5726
+ }
5727
+ return { status: "timeout" };
5728
+ }
5729
+ async function fetchProfile(args) {
5730
+ return await apiRequest("GET", "/api/auth/me", void 0, {
5731
+ auth: { token: args.token },
5732
+ baseUrl: args.apiUrl
5733
+ });
5734
+ }
5735
+ function buildCliConfig(args) {
5736
+ return {
5737
+ apiUrl: args.apiUrl,
5738
+ token: args.token,
5739
+ tokenId: args.tokenId,
5740
+ user: {
5741
+ id: args.me.id,
5742
+ email: args.me.email,
5743
+ username: args.me.username,
5744
+ firstName: args.me.first_name,
5745
+ lastName: args.me.last_name
5746
+ }
5747
+ };
5748
+ }
5749
+
5750
+ // src/commands/login.ts
5360
5751
  init_config();
5361
5752
  init_output();
5362
- var DEFAULT_TOKEN_NAME = "dashwise-cli";
5363
5753
  async function loginCommand(options) {
5364
5754
  const apiUrl = (options.apiUrl ?? resolveApiUrl()).replace(/\/+$/, "");
5365
5755
  intro(`Logging into ${code(apiUrl)}`);
5366
5756
  let dc;
5367
5757
  try {
5368
- dc = await apiRequest(
5369
- "POST",
5370
- "/api/auth/cli/device-code",
5371
- {
5372
- name: options.name ?? DEFAULT_TOKEN_NAME
5373
- },
5374
- { auth: "public", baseUrl: apiUrl }
5375
- );
5758
+ dc = await requestDeviceCode({ apiUrl, name: options.name ?? DEFAULT_TOKEN_NAME });
5376
5759
  } catch (err) {
5377
5760
  fail(formatApiError("Could not start login flow", err));
5378
5761
  return 1;
@@ -5412,85 +5795,50 @@ async function loginCommand(options) {
5412
5795
  }
5413
5796
  const sp = spinner();
5414
5797
  sp.start("Waiting for approval");
5415
- const overallDeadline = Date.now() + dc.expiresIn * 1e3;
5416
- let outcome = null;
5417
- const pollIntervalMs = Math.max(1e3, dc.interval * 1e3);
5418
- pollLoop: while (Date.now() < overallDeadline) {
5419
- let res;
5420
- try {
5421
- res = await apiRequest(
5422
- "POST",
5423
- "/api/auth/cli/poll",
5424
- { deviceCode: dc.deviceCode },
5425
- { auth: "public", baseUrl: apiUrl }
5426
- );
5427
- } catch (err) {
5428
- sp.stop(`Poll failed: ${err.message}`);
5798
+ const outcome = await pollForApproval({
5799
+ apiUrl,
5800
+ deviceCode: dc.deviceCode,
5801
+ intervalSeconds: dc.interval,
5802
+ expiresInSeconds: dc.expiresIn
5803
+ });
5804
+ switch (outcome.status) {
5805
+ case "error":
5806
+ sp.stop(`Poll failed: ${outcome.message}`);
5429
5807
  fail("Poll failed mid-flight; you can re-run `dashwise login` to retry.");
5430
5808
  return 1;
5431
- }
5432
- switch (res.status) {
5433
- case "pending":
5434
- case "slow_down":
5435
- await sleep(pollIntervalMs);
5436
- continue pollLoop;
5437
- case "denied":
5438
- sp.stop("Login denied by user.");
5439
- cancel("Aborted.");
5440
- return 1;
5441
- case "expired":
5442
- sp.stop("Login code expired.");
5443
- fail("The code expired before approval. Run `dashwise login` again.");
5444
- return 1;
5445
- case "approved":
5446
- outcome = res;
5447
- break pollLoop;
5448
- }
5449
- }
5450
- if (!outcome) {
5451
- sp.stop("Timed out waiting for approval.");
5452
- fail("Login took too long to be approved. Run `dashwise login` again.");
5453
- return 1;
5454
- }
5455
- if (!outcome.token || !outcome.tokenId) {
5456
- sp.stop("Approval received but token missing from response.");
5457
- fail(
5458
- "Approval received but the backend did not return a token. Run `dashwise logout` then `dashwise login` to retry."
5459
- );
5460
- return 1;
5809
+ case "denied":
5810
+ sp.stop("Login denied by user.");
5811
+ cancel("Aborted.");
5812
+ return 1;
5813
+ case "expired":
5814
+ sp.stop("Login code expired.");
5815
+ fail("The code expired before approval. Run `dashwise login` again.");
5816
+ return 1;
5817
+ case "timeout":
5818
+ sp.stop("Timed out waiting for approval.");
5819
+ fail("Login took too long to be approved. Run `dashwise login` again.");
5820
+ return 1;
5821
+ case "no_token":
5822
+ sp.stop("Approval received but token missing from response.");
5823
+ fail(
5824
+ "Approval received but the backend did not return a token. Run `dashwise logout` then `dashwise login` to retry."
5825
+ );
5826
+ return 1;
5461
5827
  }
5462
5828
  let me;
5463
5829
  try {
5464
- me = await apiRequest("GET", "/api/auth/me", void 0, {
5465
- auth: { token: outcome.token },
5466
- baseUrl: apiUrl
5467
- });
5830
+ me = await fetchProfile({ apiUrl, token: outcome.token });
5468
5831
  } catch (err) {
5469
5832
  sp.stop("Approval accepted but profile fetch failed.");
5470
5833
  fail(formatApiError("Could not load user profile", err));
5471
5834
  return 1;
5472
5835
  }
5473
- const cfg = {
5474
- apiUrl,
5475
- token: outcome.token,
5476
- tokenId: outcome.tokenId,
5477
- user: {
5478
- id: me.id,
5479
- email: me.email,
5480
- username: me.username,
5481
- firstName: me.first_name,
5482
- lastName: me.last_name
5483
- }
5484
- };
5485
- writeConfig(cfg);
5836
+ writeConfig(buildCliConfig({ apiUrl, token: outcome.token, tokenId: outcome.tokenId, me }));
5486
5837
  sp.stop("Approved.");
5487
5838
  success(`Logged in as ${code(me.email)}`);
5488
5839
  outro("You can now run `dashwise profile`, `dashwise workspace list`, etc.");
5489
5840
  return 0;
5490
5841
  }
5491
- function sleep(ms) {
5492
- return new Promise((resolve13) => setTimeout(resolve13, ms));
5493
- }
5494
5842
  function formatSeconds(seconds) {
5495
5843
  if (seconds < 60) return `${seconds}s`;
5496
5844
  const minutes = Math.round(seconds / 60);
@@ -5655,9 +6003,1261 @@ function toMe(cached) {
5655
6003
  };
5656
6004
  }
5657
6005
 
6006
+ // src/commands/mcp.ts
6007
+ init_api_client();
6008
+ init_config();
6009
+ var SESSION_HEADER = "mcp-session-id";
6010
+ var PROTOCOL_HEADER = "MCP-Protocol-Version";
6011
+ var ACCEPT = "application/json, text/event-stream";
6012
+ var PROXY_TOOLS_LIST_ID = "dashwise-cli:tools-list";
6013
+ var PROXY_REINITIALIZE_ID = "dashwise-cli:reinitialize";
6014
+ function defaultEndpointLabel() {
6015
+ const raw = hostname() || "unknown";
6016
+ const cleaned = raw.replace(/\.local$/i, "").replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 64);
6017
+ return cleaned.length > 0 ? cleaned : "unknown";
6018
+ }
6019
+ function redactUrl(url) {
6020
+ try {
6021
+ const parsed = new URL(url);
6022
+ const segments = parsed.pathname.split("/");
6023
+ if (segments.length > 0) segments[segments.length - 1] = "\u2026";
6024
+ return `${parsed.origin}${segments.join("/")}`;
6025
+ } catch {
6026
+ return "<invalid url>";
6027
+ }
6028
+ }
6029
+ function inspectElement(value) {
6030
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
6031
+ const frame = value;
6032
+ const id = typeof frame.id === "string" || typeof frame.id === "number" ? frame.id : null;
6033
+ const method = typeof frame.method === "string" ? frame.method : void 0;
6034
+ let toolName;
6035
+ if (method === "tools/call" && frame.params && typeof frame.params === "object") {
6036
+ const name = frame.params.name;
6037
+ if (typeof name === "string") toolName = name;
6038
+ }
6039
+ return { id, method, toolName };
6040
+ }
6041
+ function inspectFrame(text4) {
6042
+ let value;
6043
+ try {
6044
+ value = JSON.parse(text4);
6045
+ } catch {
6046
+ return null;
6047
+ }
6048
+ if (Array.isArray(value)) {
6049
+ const elements = value.map(inspectElement).filter((e) => e !== null);
6050
+ return {
6051
+ id: null,
6052
+ method: void 0,
6053
+ toolName: void 0,
6054
+ isBatch: true,
6055
+ elements
6056
+ };
6057
+ }
6058
+ const single = inspectElement(value);
6059
+ if (!single) return null;
6060
+ return { ...single, isBatch: false, elements: [single] };
6061
+ }
6062
+ function readOnlyToolsFrom(text4) {
6063
+ let value;
6064
+ try {
6065
+ value = JSON.parse(text4);
6066
+ } catch {
6067
+ return null;
6068
+ }
6069
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
6070
+ const result = value.result;
6071
+ if (!result || typeof result !== "object") return null;
6072
+ const tools = result.tools;
6073
+ if (!Array.isArray(tools)) return null;
6074
+ const out2 = /* @__PURE__ */ new Set();
6075
+ for (const tool of tools) {
6076
+ if (!tool || typeof tool !== "object") continue;
6077
+ const t = tool;
6078
+ const name = typeof t.name === "string" ? t.name : null;
6079
+ const annotations = t.annotations;
6080
+ if (name && annotations && typeof annotations === "object" && annotations.readOnlyHint === true) {
6081
+ out2.add(name);
6082
+ }
6083
+ }
6084
+ return out2;
6085
+ }
6086
+ function isReadOnlyRefusal(id, toolName, knownReadTools) {
6087
+ const reason = knownReadTools === null ? `Refused: \`dashwise mcp --read-only\` could not read this server's tool list, so it cannot tell reads from writes and refuses everything. Reconnect without --read-only to use "${toolName}".` : `Refused: "${toolName}" is not a read-only tool and this session was started with \`dashwise mcp --read-only\`. Reconnect without the flag to use it.`;
6088
+ return {
6089
+ jsonrpc: "2.0",
6090
+ id,
6091
+ result: {
6092
+ isError: true,
6093
+ content: [{ type: "text", text: reason }]
6094
+ }
6095
+ };
6096
+ }
6097
+ function transportErrorFrame(id, message) {
6098
+ return {
6099
+ jsonrpc: "2.0",
6100
+ id,
6101
+ // -32000 is the JSON-RPC "implementation-defined server error" band. A
6102
+ // dead pipe with NO frame is the one outcome worth avoiding above all
6103
+ // others: it leaves Claude Code waiting forever on a request that will
6104
+ // never be answered (`cli-mcp-command.md` §4.4).
6105
+ error: { code: -32e3, message }
6106
+ };
6107
+ }
6108
+ function startMcpProxy(opts) {
6109
+ const stdin = opts.stdin ?? process.stdin;
6110
+ const stdout = opts.stdout ?? process.stdout;
6111
+ const log2 = opts.log ?? ((line) => process.stderr.write(`${line}
6112
+ `));
6113
+ const doFetch = opts.fetchImpl ?? globalThis.fetch;
6114
+ const reconnectMs = opts.reconnectMs ?? 2e3;
6115
+ const verbose = opts.verbose === true;
6116
+ const readOnly = opts.readOnly === true;
6117
+ let session = opts.session;
6118
+ let sessionId = null;
6119
+ let protocolVersion = null;
6120
+ let readTools = null;
6121
+ let reauthAttempt = null;
6122
+ let reauthSpent = false;
6123
+ let stopped = false;
6124
+ let exitCode = 0;
6125
+ let streamTimer = null;
6126
+ let streamAbort = null;
6127
+ let streamUnavailable = false;
6128
+ let streamStarted = false;
6129
+ let resolveDone;
6130
+ const done = new Promise((resolve14) => {
6131
+ resolveDone = resolve14;
6132
+ });
6133
+ const trace = (line) => {
6134
+ if (verbose) log2(line);
6135
+ };
6136
+ const emit = (payload) => {
6137
+ stdout.write(`${JSON.stringify(payload)}
6138
+ `);
6139
+ };
6140
+ const emitFrame = (text4, source) => {
6141
+ let parsed;
6142
+ try {
6143
+ parsed = JSON.parse(text4);
6144
+ } catch {
6145
+ log2(
6146
+ `Discarded a non-JSON response from ${redactUrl(session.connectUrl)} (${source}); stdout carries MCP frames only.`
6147
+ );
6148
+ trace(`discarded ${Buffer.byteLength(text4, "utf8")} bytes of non-JSON`);
6149
+ return;
6150
+ }
6151
+ const looksJsonRpc = Array.isArray(parsed) ? parsed.every(
6152
+ (e) => e !== null && typeof e === "object" && "jsonrpc" in e
6153
+ ) : parsed !== null && typeof parsed === "object" && "jsonrpc" in parsed;
6154
+ if (!looksJsonRpc) {
6155
+ log2(
6156
+ `Discarded a JSON response from ${redactUrl(session.connectUrl)} that is not JSON-RPC (${source}).`
6157
+ );
6158
+ return;
6159
+ }
6160
+ if (text4.includes("\n") || text4.includes("\r")) {
6161
+ trace("reflowed a multi-line frame onto one line (stdio is line-delimited)");
6162
+ stdout.write(`${text4.replace(/[\r\n]+/g, " ")}
6163
+ `);
6164
+ return;
6165
+ }
6166
+ stdout.write(`${text4}
6167
+ `);
6168
+ };
6169
+ const requestHeaders = (isInitialize) => {
6170
+ const headers = {
6171
+ "Content-Type": "application/json",
6172
+ Accept: ACCEPT
6173
+ };
6174
+ if (sessionId) headers["Mcp-Session-Id"] = sessionId;
6175
+ if (protocolVersion && !isInitialize) {
6176
+ headers[PROTOCOL_HEADER] = protocolVersion;
6177
+ }
6178
+ return headers;
6179
+ };
6180
+ const attemptReauth = () => {
6181
+ if (reauthAttempt !== null && !reauthSpent) return reauthAttempt;
6182
+ if (reauthSpent || !opts.reauth) return Promise.resolve(false);
6183
+ const reauth = opts.reauth;
6184
+ reauthAttempt = (async () => {
6185
+ log2("The session key expired or was rotated \u2014 re-authenticating once.");
6186
+ try {
6187
+ session = await reauth();
6188
+ sessionId = null;
6189
+ return true;
6190
+ } catch (err) {
6191
+ log2(`Re-authentication failed: ${err.message}`);
6192
+ return false;
6193
+ } finally {
6194
+ reauthSpent = true;
6195
+ }
6196
+ })();
6197
+ return reauthAttempt;
6198
+ };
6199
+ const reinitialize = async () => {
6200
+ const frame = JSON.stringify({
6201
+ jsonrpc: "2.0",
6202
+ id: PROXY_REINITIALIZE_ID,
6203
+ method: "initialize",
6204
+ params: {
6205
+ // Echo what was negotiated the first time, so the server is not asked
6206
+ // to renegotiate a version the client upstream has already committed
6207
+ // to. The floor is only reached if we never saw a handshake at all.
6208
+ protocolVersion: protocolVersion ?? "2024-11-05",
6209
+ capabilities: {},
6210
+ clientInfo: { name: "dashwise-cli", version: "0" }
6211
+ }
6212
+ });
6213
+ try {
6214
+ const res = await post(frame, true);
6215
+ if (!res || !res.ok) {
6216
+ trace(`re-initialize refused (HTTP ${res?.status ?? "none"}).`);
6217
+ return false;
6218
+ }
6219
+ captureTransportState(res, await res.text());
6220
+ return sessionId !== null;
6221
+ } catch (err) {
6222
+ trace(`re-initialize failed: ${err.message}`);
6223
+ return false;
6224
+ }
6225
+ };
6226
+ const post = async (text4, isInitialize) => {
6227
+ if (stopped) return null;
6228
+ return doFetch(session.connectUrl, {
6229
+ method: "POST",
6230
+ headers: requestHeaders(isInitialize),
6231
+ body: text4
6232
+ });
6233
+ };
6234
+ const captureTransportState = (res, bodyText) => {
6235
+ const returned = res.headers.get(SESSION_HEADER);
6236
+ if (returned) sessionId = returned;
6237
+ if (protocolVersion === null) {
6238
+ try {
6239
+ const parsed = JSON.parse(bodyText);
6240
+ const result = parsed.result;
6241
+ const negotiated = result?.protocolVersion;
6242
+ if (typeof negotiated === "string") protocolVersion = negotiated;
6243
+ } catch {
6244
+ }
6245
+ }
6246
+ const learned = readOnlyToolsFrom(bodyText);
6247
+ if (learned !== null) readTools = learned;
6248
+ };
6249
+ const relay = async (text4, frame) => {
6250
+ const isInitialize = frame?.method === "initialize";
6251
+ let res;
6252
+ try {
6253
+ res = await post(text4, isInitialize);
6254
+ } catch (err) {
6255
+ const message = err.message;
6256
+ log2(`Transport error talking to ${redactUrl(session.connectUrl)}: ${message}`);
6257
+ emit(transportErrorFrame(frame?.id ?? null, `Transport error: ${message}`));
6258
+ return { kind: "failed" };
6259
+ }
6260
+ if (res === null) return { kind: "failed" };
6261
+ if (res.status === 401 && opts.reauth) {
6262
+ const fresh = await attemptReauth();
6263
+ if (!fresh) {
6264
+ emit(
6265
+ transportErrorFrame(
6266
+ frame?.id ?? null,
6267
+ "Re-authentication failed; the Dashwise session could not be renewed."
6268
+ )
6269
+ );
6270
+ exitCode = 1;
6271
+ stop();
6272
+ return { kind: "failed" };
6273
+ }
6274
+ res = await post(text4, isInitialize);
6275
+ if (res === null) return { kind: "failed" };
6276
+ }
6277
+ const bodyText = await res.text();
6278
+ if (!res.ok) {
6279
+ const detail = res.status === 401 ? "the endpoint key is no longer valid (expired, rotated by another `dashwise mcp`, or revoked)" : `HTTP ${res.status}`;
6280
+ log2(`Refused by ${redactUrl(session.connectUrl)}: ${detail}`);
6281
+ emit(transportErrorFrame(frame?.id ?? null, `Dashwise MCP server: ${detail}`));
6282
+ if (res.status === 401) {
6283
+ exitCode = 1;
6284
+ stop();
6285
+ }
6286
+ return { kind: "failed" };
6287
+ }
6288
+ captureTransportState(res, bodyText);
6289
+ if (res.status === 202 || bodyText.trim().length === 0) {
6290
+ return { kind: "empty" };
6291
+ }
6292
+ return { kind: "body", text: bodyText };
6293
+ };
6294
+ const learnReadOnlyTools = async () => {
6295
+ if (!readOnly || readTools !== null || stopped) return;
6296
+ const probe = JSON.stringify({
6297
+ jsonrpc: "2.0",
6298
+ id: PROXY_TOOLS_LIST_ID,
6299
+ method: "tools/list",
6300
+ params: {}
6301
+ });
6302
+ try {
6303
+ const res = await post(probe, false);
6304
+ if (!res || !res.ok) {
6305
+ trace("read-only guard: tools/list probe was refused; failing closed.");
6306
+ return;
6307
+ }
6308
+ const text4 = await res.text();
6309
+ captureTransportState(res, text4);
6310
+ const learned = readOnlyToolsFrom(text4);
6311
+ trace(`read-only guard: ${learned?.size ?? 0} read-only tools learned from tools/list.`);
6312
+ } catch (err) {
6313
+ trace(
6314
+ `read-only guard: tools/list probe failed (${err.message}); failing closed.`
6315
+ );
6316
+ }
6317
+ };
6318
+ const handleLine = async (line) => {
6319
+ const text4 = line.trim();
6320
+ if (text4.length === 0) return;
6321
+ const frame = inspectFrame(text4);
6322
+ trace(
6323
+ `\u2192 ${frame?.method ?? (frame?.isBatch ? "batch" : "frame")} (${Buffer.byteLength(text4, "utf8")} bytes)`
6324
+ );
6325
+ const refusable = readOnly ? (frame?.elements ?? []).filter(
6326
+ (e) => e.toolName !== void 0 && !readTools?.has(e.toolName)
6327
+ ) : [];
6328
+ if (refusable.length > 0) {
6329
+ const names = refusable.map((e) => e.toolName).join(", ");
6330
+ trace(
6331
+ `read-only guard refused ${names}${frame?.isBatch ? " (whole batch)" : ""}`
6332
+ );
6333
+ if (frame?.isBatch) {
6334
+ const replies = frame.elements.filter((e) => e.id !== null).map(
6335
+ (e) => isReadOnlyRefusal(e.id, e.toolName ?? "(batch)", readTools)
6336
+ );
6337
+ if (replies.length > 0) emit(replies);
6338
+ } else {
6339
+ emit(isReadOnlyRefusal(frame.id, refusable[0].toolName, readTools));
6340
+ }
6341
+ return;
6342
+ }
6343
+ const outcome = await relay(text4, frame);
6344
+ if (outcome.kind === "failed") return;
6345
+ if (outcome.kind === "body") {
6346
+ trace(`\u2190 response (${Buffer.byteLength(outcome.text, "utf8")} bytes)`);
6347
+ emitFrame(outcome.text, "POST response");
6348
+ }
6349
+ if (frame?.method === "initialize") {
6350
+ await learnReadOnlyTools();
6351
+ } else if (frame?.method === "notifications/initialized") {
6352
+ openServerStream();
6353
+ }
6354
+ };
6355
+ const scheduleStreamRetry = () => {
6356
+ if (stopped || streamUnavailable) return;
6357
+ streamTimer = setTimeout(() => {
6358
+ streamTimer = null;
6359
+ void runServerStream();
6360
+ }, reconnectMs);
6361
+ };
6362
+ const runServerStream = async () => {
6363
+ if (stopped || streamUnavailable) return;
6364
+ streamAbort = new AbortController();
6365
+ let res;
6366
+ try {
6367
+ res = await doFetch(session.connectUrl, {
6368
+ method: "GET",
6369
+ headers: {
6370
+ Accept: "text/event-stream",
6371
+ ...sessionId ? { "Mcp-Session-Id": sessionId } : {},
6372
+ ...protocolVersion ? { [PROTOCOL_HEADER]: protocolVersion } : {}
6373
+ },
6374
+ signal: streamAbort.signal
6375
+ });
6376
+ } catch (err) {
6377
+ if (stopped) return;
6378
+ trace(`server stream: ${err.message}; retrying.`);
6379
+ scheduleStreamRetry();
6380
+ return;
6381
+ }
6382
+ if (res.status === 405 || res.status === 404) {
6383
+ streamUnavailable = true;
6384
+ trace(
6385
+ `server stream: this server has no server-initiated stream (HTTP ${res.status}); not retrying.`
6386
+ );
6387
+ return;
6388
+ }
6389
+ if (res.status === 401) {
6390
+ const fresh = await attemptReauth();
6391
+ if (fresh) {
6392
+ if (await reinitialize()) {
6393
+ streamUnavailable = false;
6394
+ streamStarted = false;
6395
+ trace("server stream: re-authenticated and re-initialized; reopening.");
6396
+ openServerStream();
6397
+ return;
6398
+ }
6399
+ trace("server stream: re-authenticated but could not re-initialize.");
6400
+ }
6401
+ streamUnavailable = true;
6402
+ log2(
6403
+ `Server stream refused by ${redactUrl(session.connectUrl)}: the session could not be renewed (the endpoint key expired, was rotated, or was revoked).`
6404
+ );
6405
+ emit(
6406
+ transportErrorFrame(
6407
+ null,
6408
+ "Dashwise MCP server: the session could not be renewed; the server stream is closed."
6409
+ )
6410
+ );
6411
+ exitCode = 1;
6412
+ stop();
6413
+ return;
6414
+ }
6415
+ if (!res.ok || !res.body) {
6416
+ streamUnavailable = true;
6417
+ trace(
6418
+ `server stream: HTTP ${res.status}${res.body ? "" : " with no body"}; not retrying.`
6419
+ );
6420
+ return;
6421
+ }
6422
+ trace("server stream: open.");
6423
+ const reader = res.body.getReader();
6424
+ const decoder = new TextDecoder();
6425
+ let buffer2 = "";
6426
+ try {
6427
+ for (; ; ) {
6428
+ const { done: finished, value } = await reader.read();
6429
+ if (finished) break;
6430
+ buffer2 += decoder.decode(value, { stream: true });
6431
+ let sep3 = buffer2.indexOf("\n\n");
6432
+ while (sep3 !== -1) {
6433
+ const event = buffer2.slice(0, sep3);
6434
+ buffer2 = buffer2.slice(sep3 + 2);
6435
+ const payload = eventData(event);
6436
+ if (payload !== null) emitFrame(payload, "server stream");
6437
+ sep3 = buffer2.indexOf("\n\n");
6438
+ }
6439
+ }
6440
+ } catch (err) {
6441
+ if (!stopped) trace(`server stream dropped: ${err.message}`);
6442
+ }
6443
+ if (!stopped) {
6444
+ trace("server stream closed; reconnecting.");
6445
+ scheduleStreamRetry();
6446
+ }
6447
+ };
6448
+ const openServerStream = () => {
6449
+ if (streamStarted || stopped) return;
6450
+ streamStarted = true;
6451
+ void runServerStream();
6452
+ };
6453
+ let pending = Promise.resolve();
6454
+ let buffer = "";
6455
+ const onData = (chunk) => {
6456
+ buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
6457
+ let nl = buffer.indexOf("\n");
6458
+ while (nl !== -1) {
6459
+ const line = buffer.slice(0, nl);
6460
+ buffer = buffer.slice(nl + 1);
6461
+ pending = pending.then(() => handleLine(line));
6462
+ nl = buffer.indexOf("\n");
6463
+ }
6464
+ };
6465
+ const onEnd = () => {
6466
+ void pending.then(() => {
6467
+ stop();
6468
+ });
6469
+ };
6470
+ stdin.on("data", onData);
6471
+ stdin.on("end", onEnd);
6472
+ stdin.on("close", onEnd);
6473
+ if (typeof stdin.resume === "function") {
6474
+ stdin.resume();
6475
+ }
6476
+ function stop() {
6477
+ if (stopped) return;
6478
+ stopped = true;
6479
+ if (streamTimer) {
6480
+ clearTimeout(streamTimer);
6481
+ streamTimer = null;
6482
+ }
6483
+ if (streamAbort) {
6484
+ streamAbort.abort();
6485
+ streamAbort = null;
6486
+ }
6487
+ stdin.off("data", onData);
6488
+ stdin.off("end", onEnd);
6489
+ stdin.off("close", onEnd);
6490
+ if (typeof stdin.pause === "function") {
6491
+ stdin.pause();
6492
+ }
6493
+ resolveDone(exitCode);
6494
+ }
6495
+ return {
6496
+ stop,
6497
+ isRunning: () => !stopped,
6498
+ done
6499
+ };
6500
+ }
6501
+ function eventData(event) {
6502
+ const parts = [];
6503
+ for (const line of event.split("\n")) {
6504
+ if (!line.startsWith("data:")) continue;
6505
+ const chunk = line.slice(5);
6506
+ parts.push(chunk.startsWith(" ") ? chunk.slice(1) : chunk);
6507
+ }
6508
+ if (parts.length === 0) return null;
6509
+ const payload = parts.join("\n").trim();
6510
+ return payload.length > 0 ? payload : null;
6511
+ }
6512
+
6513
+ // src/lib/mcp-scope.ts
6514
+ var MCP_SCOPES_MAX_ITEMS = 64;
6515
+ var MAX_ID = 2147483647;
6516
+ var SCOPE_PATTERN = /^datahub:(?:(table|app)=([1-9][0-9]{0,9})|(workspace)):([crud]{1,4})$/;
6517
+ var MCP_GOVERNANCE_WORKSPACE_READ_SCOPE = "governance:workspace:r";
6518
+ var GRAMMAR_HINT = "Expected datahub:table=<id>:<ops>, datahub:app=<id>:<ops>, datahub:workspace:<ops> or governance:workspace:r, where <ops> is one to four of c, r, u and d, each at most once.";
6519
+ function isMcpScope(raw) {
6520
+ if (typeof raw !== "string") return false;
6521
+ if (raw === MCP_GOVERNANCE_WORKSPACE_READ_SCOPE) return true;
6522
+ const match = SCOPE_PATTERN.exec(raw);
6523
+ if (!match) return false;
6524
+ const idText = match[2];
6525
+ const opsText = match[4];
6526
+ if (new Set(opsText).size !== opsText.length) return false;
6527
+ if (idText !== void 0) {
6528
+ const id = Number(idText);
6529
+ if (!Number.isSafeInteger(id) || id > MAX_ID) return false;
6530
+ }
6531
+ return true;
6532
+ }
6533
+ function checkMcpScopes(scopes, flag = "--scope") {
6534
+ if (scopes.length > MCP_SCOPES_MAX_ITEMS) {
6535
+ return {
6536
+ ok: false,
6537
+ message: `At most ${MCP_SCOPES_MAX_ITEMS} ${flag} grants fit one session; got ${scopes.length}.`
6538
+ };
6539
+ }
6540
+ const bad = scopes.findIndex((scope) => !isMcpScope(scope));
6541
+ if (bad !== -1) {
6542
+ return {
6543
+ ok: false,
6544
+ message: `${flag} ${JSON.stringify(scopes[bad])} is not a scope. ${GRAMMAR_HINT}`
6545
+ };
6546
+ }
6547
+ return { ok: true };
6548
+ }
6549
+
6550
+ // src/commands/mcp.ts
6551
+ init_output();
6552
+ function fileExists(path) {
6553
+ try {
6554
+ return statSync(path).isFile();
6555
+ } catch {
6556
+ return false;
6557
+ }
6558
+ }
6559
+ function whereOnWindows(cmd, env, isFile) {
6560
+ const extensions = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
6561
+ for (const dir of (env.PATH ?? env.Path ?? "").split(";")) {
6562
+ if (!dir) continue;
6563
+ for (const ext of extensions) {
6564
+ const candidate = win32.join(dir, `${cmd}${ext.toLowerCase()}`);
6565
+ if (isFile(candidate)) return candidate;
6566
+ }
6567
+ }
6568
+ return void 0;
6569
+ }
6570
+ function spawnTarget(cmd, args, opts = {}) {
6571
+ const direct = { command: cmd, args, shell: false };
6572
+ if ((opts.platform ?? process.platform) !== "win32") return direct;
6573
+ const found = whereOnWindows(cmd, opts.env ?? process.env, opts.isFile ?? fileExists);
6574
+ if (found === void 0 || !/\.(cmd|bat)$/i.test(found)) return direct;
6575
+ const unsafe = args.find((arg) => /["%\r\n]/.test(arg));
6576
+ if (unsafe !== void 0) {
6577
+ return {
6578
+ refused: `cannot pass ${JSON.stringify(unsafe)} to ${found}: cmd.exe would act on the quote, % or line break in it.`
6579
+ };
6580
+ }
6581
+ return { command: [found, ...args].map((part) => `"${part}"`).join(" "), args: [], shell: true };
6582
+ }
6583
+
6584
+ // src/commands/mcp.ts
6585
+ init_workspaces();
6586
+ var SCOPES_NOT_APPLIED = "This Dashwise server minted the session but did not confirm any scopes, so the key is NOT narrowed. The session was not started, and its revocation was requested. Update the backend, or run without --scope.";
6587
+ var REVOKE_TIMEOUT_MS = 2e3;
6588
+ async function openCliSession(args) {
6589
+ const res = await apiRequest("POST", "/api/mcp-endpoints/cli-session", {
6590
+ workspace: args.workspaceId,
6591
+ label: args.label,
6592
+ // Only when the flag was given: a body without the key is what every
6593
+ // backend accepts, and one before `--scope` refuses the key as unknown.
6594
+ ...args.scopes !== void 0 ? { scopes: args.scopes } : {}
6595
+ });
6596
+ return {
6597
+ connectUrl: res.connect_url,
6598
+ endpointId: res.endpoint_id,
6599
+ expiresAt: res.expires_at,
6600
+ name: res.name,
6601
+ scopes: res.scopes
6602
+ };
6603
+ }
6604
+ function describeScopes(scopes) {
6605
+ if (scopes === null || scopes === void 0) {
6606
+ return "Scopes: none \u2014 this key reaches what your account can in this workspace (narrow it with --scope).";
6607
+ }
6608
+ if (scopes.length === 0) return "Scopes: deny-all \u2014 no data tool is available to this key.";
6609
+ return `Scopes (as stored by the server): ${scopes.join(", ")}.`;
6610
+ }
6611
+ async function revokeSession(endpointId) {
6612
+ const attempt = apiRequest("POST", `/api/mcp-endpoints/${endpointId}/revoke`).then(
6613
+ () => true,
6614
+ () => false
6615
+ );
6616
+ const timeout = new Promise(
6617
+ (resolve14) => setTimeout(() => resolve14(false), REVOKE_TIMEOUT_MS).unref()
6618
+ );
6619
+ await Promise.race([attempt, timeout]);
6620
+ }
6621
+ function describeSessionError(err, apiUrl, scopes) {
6622
+ if (!(err instanceof ApiError)) return err.message;
6623
+ if (err.status === 0) {
6624
+ return `Could not reach the Dashwise backend at ${apiUrl}: ${err.message}`;
6625
+ }
6626
+ if (err.status === 401) {
6627
+ return "Your saved credentials are no longer valid. Run `dashwise login` again.";
6628
+ }
6629
+ if (err.code === "SCOPE_EXCEEDS_CREATOR_ACCESS") {
6630
+ const scope = typeof err.context?.scope === "string" ? err.context.scope : void 0;
6631
+ const lead = scope ? `--scope ${scope} reaches beyond your own access in this workspace` : "A --scope grant reaches beyond your own access in this workspace";
6632
+ return `${lead}; a scope can only narrow what you can already do. ${err.message}`;
6633
+ }
6634
+ if (err.code === "SCOPE_MALFORMED") {
6635
+ const index = typeof err.context?.index === "number" ? err.context.index : void 0;
6636
+ const grant = index !== void 0 ? scopes?.[index] : void 0;
6637
+ return grant !== void 0 ? `The server does not accept --scope ${grant}: ${err.message}` : `The server refused --scope: ${err.message}`;
6638
+ }
6639
+ if (scopes !== void 0 && err.status === 400 && /\bscopes\b/.test(err.message)) {
6640
+ return `The server refused the scopes field (${err.message}). A Dashwise backend older than --scope answers this way; update it, or run without --scope.`;
6641
+ }
6642
+ return err.message;
6643
+ }
6644
+ async function resolveWorkspace(apiUrl, apiToken, flag) {
6645
+ const cfg = readConfig();
6646
+ const wanted = flag ?? cfg?.workspaceSlug;
6647
+ return selectWorkspace({
6648
+ apiUrl,
6649
+ apiToken,
6650
+ ...wanted !== void 0 ? { flag: wanted } : {},
6651
+ // Never interactive: this command's stdout is a protocol channel and its
6652
+ // stdin belongs to the MCP client. A prompt here would deadlock a client
6653
+ // that launched us as a subprocess.
6654
+ interactive: false
6655
+ });
6656
+ }
6657
+ async function mcpCommand(options = {}) {
6658
+ const scopes = options.scopes;
6659
+ if (scopes !== void 0) {
6660
+ const checked = checkMcpScopes(scopes);
6661
+ if (!checked.ok) {
6662
+ fail(checked.message);
6663
+ return 1;
6664
+ }
6665
+ }
6666
+ const cfg = readConfig();
6667
+ if (!cfg) {
6668
+ fail("Not logged in. Run `dashwise login` first.");
6669
+ return 1;
6670
+ }
6671
+ const apiUrl = resolveApiUrl();
6672
+ let workspace2;
6673
+ try {
6674
+ workspace2 = await resolveWorkspace(apiUrl, cfg.token, options.workspace);
6675
+ } catch (err) {
6676
+ fail(err.message);
6677
+ return 1;
6678
+ }
6679
+ const label = options.endpointName ?? defaultEndpointLabel();
6680
+ let session;
6681
+ try {
6682
+ session = await openCliSession({
6683
+ workspaceId: workspace2.id,
6684
+ label,
6685
+ ...scopes !== void 0 ? { scopes } : {}
6686
+ });
6687
+ } catch (err) {
6688
+ fail(describeSessionError(err, apiUrl, scopes));
6689
+ return 1;
6690
+ }
6691
+ if (scopes !== void 0 && !Array.isArray(session.scopes)) {
6692
+ await revokeSession(session.endpointId);
6693
+ fail(SCOPES_NOT_APPLIED);
6694
+ return 1;
6695
+ }
6696
+ info(
6697
+ `Dashwise MCP \u2014 workspace ${workspace2.slug}, endpoint #${session.endpointId} (${session.name}).`
6698
+ );
6699
+ info(`Session key expires ${dim(session.expiresAt)}.`);
6700
+ info(describeScopes(session.scopes));
6701
+ if (options.readOnly) {
6702
+ warn(
6703
+ "Read-only: write tools are refused by this proxy (a client-side guard, not a server-enforced scope \u2014 for that, use --scope)."
6704
+ );
6705
+ }
6706
+ let ownedEndpointId = session.endpointId;
6707
+ const proxy = startMcpProxy({
6708
+ session,
6709
+ reauth: async () => {
6710
+ const fresh = await openCliSession({
6711
+ workspaceId: workspace2.id,
6712
+ label,
6713
+ ...scopes !== void 0 ? { scopes } : {}
6714
+ });
6715
+ if (scopes !== void 0 && !Array.isArray(fresh.scopes)) {
6716
+ await revokeSession(fresh.endpointId);
6717
+ throw new Error(SCOPES_NOT_APPLIED);
6718
+ }
6719
+ ownedEndpointId = fresh.endpointId;
6720
+ return fresh;
6721
+ },
6722
+ ...options.readOnly !== void 0 ? { readOnly: options.readOnly } : {},
6723
+ ...options.verbose !== void 0 ? { verbose: options.verbose } : {}
6724
+ });
6725
+ const shutdown = () => proxy.stop();
6726
+ process.once("SIGINT", shutdown);
6727
+ process.once("SIGTERM", shutdown);
6728
+ try {
6729
+ const exitCode = await proxy.done;
6730
+ await revokeSession(ownedEndpointId);
6731
+ return exitCode;
6732
+ } finally {
6733
+ process.off("SIGINT", shutdown);
6734
+ process.off("SIGTERM", shutdown);
6735
+ }
6736
+ }
6737
+ function claudeMcpAddArgv(args) {
6738
+ const argv = [
6739
+ "mcp",
6740
+ "add",
6741
+ args.serverName,
6742
+ "--scope",
6743
+ args.scope,
6744
+ "--",
6745
+ "dashwise",
6746
+ "mcp",
6747
+ "--workspace",
6748
+ args.workspaceSlug
6749
+ ];
6750
+ for (const grant of args.grantScopes ?? []) argv.push("--scope", grant);
6751
+ if (args.readOnly) argv.push("--read-only");
6752
+ if (args.endpointName) argv.push("--endpoint-name", args.endpointName);
6753
+ return argv;
6754
+ }
6755
+ async function mcpAddCommand(options = {}) {
6756
+ const serverName = options.name ?? "dashwise";
6757
+ const scope = options.scope ?? "local";
6758
+ if (options.grantScopes !== void 0) {
6759
+ const checked = checkMcpScopes(options.grantScopes, "--grant-scope");
6760
+ if (!checked.ok) {
6761
+ fail(checked.message);
6762
+ return 1;
6763
+ }
6764
+ }
6765
+ let workspaceSlug = options.workspace;
6766
+ if (workspaceSlug === void 0) {
6767
+ const cfg = readConfig();
6768
+ if (!cfg) {
6769
+ fail("Not logged in. Run `dashwise login` first, or pass --workspace <slug>.");
6770
+ return 1;
6771
+ }
6772
+ try {
6773
+ workspaceSlug = (await resolveWorkspace(resolveApiUrl(), cfg.token, void 0)).slug;
6774
+ } catch (err) {
6775
+ fail(err.message);
6776
+ return 1;
6777
+ }
6778
+ }
6779
+ const argv = claudeMcpAddArgv({
6780
+ serverName,
6781
+ scope,
6782
+ workspaceSlug,
6783
+ ...options.readOnly !== void 0 ? { readOnly: options.readOnly } : {},
6784
+ ...options.endpointName !== void 0 ? { endpointName: options.endpointName } : {},
6785
+ ...options.grantScopes !== void 0 ? { grantScopes: options.grantScopes } : {}
6786
+ });
6787
+ const printable = `claude ${argv.join(" ")}`;
6788
+ info("Registering the Dashwise MCP server with Claude Code:");
6789
+ out(printable);
6790
+ const target = spawnTarget("claude", argv);
6791
+ if ("refused" in target) {
6792
+ fail(`Could not run \`claude\`: ${target.refused} Run the command above yourself.`);
6793
+ return 1;
6794
+ }
6795
+ return await new Promise((resolve14) => {
6796
+ const child = spawn(target.command, target.args, { shell: target.shell, stdio: "inherit" });
6797
+ let settled = false;
6798
+ child.on("error", (err) => {
6799
+ if (settled) return;
6800
+ settled = true;
6801
+ if (err.code === "ENOENT") {
6802
+ fail(
6803
+ `Claude Code is not on your PATH, so nothing was registered. Run the command above once ${code("claude")} is installed.`
6804
+ );
6805
+ resolve14(1);
6806
+ return;
6807
+ }
6808
+ fail(`Could not run \`claude\`: ${err.message}`);
6809
+ resolve14(1);
6810
+ });
6811
+ child.on("close", (codeOut) => {
6812
+ if (settled) return;
6813
+ settled = true;
6814
+ resolve14(codeOut ?? 1);
6815
+ });
6816
+ });
6817
+ }
6818
+
6819
+ // src/commands/agent.ts
6820
+ init_config();
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
6930
+ var PLUGIN_NAME = "dashwise";
6931
+ var MARKETPLACE_NAME = "dashwise";
6932
+ var DEFAULT_MARKETPLACE_SOURCE = "Intuize/dashwise-cli";
6933
+ var defaultRunner = (cmd, args, opts) => new Promise((resolve14) => {
6934
+ const target = spawnTarget(cmd, args);
6935
+ if ("refused" in target) {
6936
+ resolve14({ code: null, stdout: "", stderr: target.refused, missing: false });
6937
+ return;
6938
+ }
6939
+ const child = spawn(target.command, target.args, {
6940
+ shell: target.shell,
6941
+ stdio: opts.capture ? ["ignore", "pipe", "pipe"] : "inherit"
6942
+ });
6943
+ let stdout = "";
6944
+ let stderr = "";
6945
+ child.stdout?.on("data", (chunk) => stdout += chunk.toString());
6946
+ child.stderr?.on("data", (chunk) => stderr += chunk.toString());
6947
+ let settled = false;
6948
+ child.on("error", (err) => {
6949
+ if (settled) return;
6950
+ settled = true;
6951
+ resolve14({
6952
+ code: null,
6953
+ stdout,
6954
+ stderr: stderr || err.message,
6955
+ missing: err.code === "ENOENT"
6956
+ });
6957
+ });
6958
+ child.on("close", (exitCode) => {
6959
+ if (settled) return;
6960
+ settled = true;
6961
+ resolve14({ code: exitCode, stdout, stderr, missing: false });
6962
+ });
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
+ }
6990
+ function parsePluginList(text4) {
6991
+ let parsed;
6992
+ try {
6993
+ parsed = JSON.parse(text4);
6994
+ } catch {
6995
+ return null;
6996
+ }
6997
+ const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.plugins) ? parsed.plugins : null;
6998
+ if (!list) return null;
6999
+ return list.flatMap((entry) => {
7000
+ if (!entry || typeof entry !== "object") return [];
7001
+ const e = entry;
7002
+ if (typeof e.id !== "string") return [];
7003
+ return [
7004
+ {
7005
+ id: e.id,
7006
+ ...typeof e.version === "string" ? { version: e.version } : {},
7007
+ ...typeof e.scope === "string" ? { scope: e.scope } : {},
7008
+ ...typeof e.enabled === "boolean" ? { enabled: e.enabled } : {}
7009
+ }
7010
+ ];
7011
+ });
7012
+ }
7013
+ function parseMarketplaceList(text4) {
7014
+ let parsed;
7015
+ try {
7016
+ parsed = JSON.parse(text4);
7017
+ } catch {
7018
+ return null;
7019
+ }
7020
+ if (!Array.isArray(parsed)) return null;
7021
+ return parsed.flatMap((entry) => {
7022
+ if (!entry || typeof entry !== "object") return [];
7023
+ const e = entry;
7024
+ if (typeof e.name !== "string") return [];
7025
+ const where = [e.repo, e.url, e.path, e.source].find((v) => typeof v === "string");
7026
+ return [{ name: e.name, ...typeof where === "string" ? { source: where } : {} }];
7027
+ });
7028
+ }
7029
+ function manualSteps(source) {
7030
+ return [
7031
+ `claude plugin marketplace add ${source}`,
7032
+ `claude plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME}`
7033
+ ];
7034
+ }
7035
+ async function agentSetupCommand(options = {}, deps = {}) {
7036
+ const run = deps.run ?? defaultRunner;
7037
+ const loggedIn = deps.loggedIn ?? isLoggedIn;
7038
+ const json = options.json === true;
7039
+ const source = options.marketplace ?? DEFAULT_MARKETPLACE_SOURCE;
7040
+ const pluginId = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
7041
+ const report = {
7042
+ ok: false,
7043
+ action: options.status ? "status" : "setup",
7044
+ claude: { found: false },
7045
+ marketplace: { name: MARKETPLACE_NAME, source, configured: null },
7046
+ plugin: { id: pluginId, installed: null },
7047
+ steps: []
7048
+ };
7049
+ const finish = (exitCode) => {
7050
+ report.ok = exitCode === 0;
7051
+ if (json) out(JSON.stringify(report, null, 2));
7052
+ return exitCode;
7053
+ };
7054
+ const say = (line) => {
7055
+ if (!json) info(line);
7056
+ };
7057
+ const version = await run("claude", ["--version"], { capture: true });
7058
+ if (version.missing && options.status) {
7059
+ report.next = ["Install Claude Code, then run `dashwise agent setup`."];
7060
+ if (!json) {
7061
+ out("Claude Code: not installed (no `claude` on your PATH)");
7062
+ info(`Once it is, ${code("dashwise agent setup")} installs the Dashwise plugin.`);
7063
+ }
7064
+ return finish(0);
7065
+ }
7066
+ if (version.missing) {
7067
+ report.manual_steps = manualSteps(source);
7068
+ if (!json) {
7069
+ warn("Claude Code (`claude`) is not on your PATH, so nothing was installed.");
7070
+ info("Once Claude Code is installed, these two commands add the Dashwise plugin:");
7071
+ for (const line of report.manual_steps) out(line);
7072
+ info(
7073
+ `Or inside Claude Code: ${code(`/plugin marketplace add ${source}`)}, then ${code(`/plugin install ${pluginId}`)}.`
7074
+ );
7075
+ }
7076
+ return finish(0);
7077
+ }
7078
+ if (version.code !== 0) {
7079
+ report.error = `claude --version exited ${version.code}: ${version.stderr.trim()}`;
7080
+ if (!json)
7081
+ fail(
7082
+ `Could not run \`claude --version\`: ${version.stderr.trim() || `exit ${version.code}`}`
7083
+ );
7084
+ return finish(1);
7085
+ }
7086
+ report.claude = { found: true, version: version.stdout.trim() };
7087
+ say(`Claude Code found (${report.claude.version}).`);
7088
+ const listed = await run("claude", ["plugin", "list", "--json"], { capture: true });
7089
+ const plugins = listed.code === 0 ? parsePluginList(listed.stdout) : null;
7090
+ if (plugins) {
7091
+ const ours = plugins.find((p) => p.id.toLowerCase() === pluginId.toLowerCase());
7092
+ const others = plugins.filter((p) => p !== ours && p.id.toLowerCase().startsWith(`${PLUGIN_NAME}@`)).map((p) => p.id);
7093
+ report.plugin = {
7094
+ id: pluginId,
7095
+ installed: ours !== void 0,
7096
+ ...ours?.version !== void 0 ? { version: ours.version } : {},
7097
+ ...ours?.scope !== void 0 ? { scope: ours.scope } : {},
7098
+ ...ours?.enabled !== void 0 ? { enabled: ours.enabled } : {},
7099
+ ...others.length > 0 ? { others } : {}
7100
+ };
7101
+ }
7102
+ const markets = await run("claude", ["plugin", "marketplace", "list", "--json"], {
7103
+ capture: true
7104
+ });
7105
+ const marketplaces = markets.code === 0 ? parseMarketplaceList(markets.stdout) : null;
7106
+ const existing = marketplaces?.find((m) => m.name === MARKETPLACE_NAME);
7107
+ if (marketplaces) {
7108
+ report.marketplace.configured = existing !== void 0;
7109
+ if (existing?.source !== void 0) report.marketplace.configured_source = existing.source;
7110
+ }
7111
+ if (options.status) {
7112
+ if (!json) printStatus(report);
7113
+ if (plugins === null) {
7114
+ report.error = `claude plugin list --json gave nothing this command can read (exit ${listed.code}).`;
7115
+ if (!json) fail(report.error);
7116
+ return finish(1);
7117
+ }
7118
+ return finish(0);
7119
+ }
7120
+ if (report.plugin.installed === true) {
7121
+ if (!json) {
7122
+ success(
7123
+ `${pluginId} is already installed${report.plugin.version ? ` (${report.plugin.version})` : ""}.`
7124
+ );
7125
+ info(`To update it: ${code(`claude plugin update ${pluginId}`)}, then restart Claude Code.`);
7126
+ }
7127
+ return finish(0);
7128
+ }
7129
+ if (existing) {
7130
+ say(
7131
+ `The ${MARKETPLACE_NAME} marketplace is already configured` + (existing.source ? ` (from ${existing.source}).` : ".")
7132
+ );
7133
+ if (options.marketplace !== void 0 && existing.source !== void 0 && existing.source !== source) {
7134
+ if (!json) {
7135
+ warn(
7136
+ `--marketplace ${source} was not used: a marketplace named ${MARKETPLACE_NAME} already points at ${existing.source}. To switch, run ${code(`claude plugin marketplace remove ${MARKETPLACE_NAME}`)} and try again.`
7137
+ );
7138
+ }
7139
+ }
7140
+ } else {
7141
+ const added = await step(run, ["plugin", "marketplace", "add", source], json, report);
7142
+ if (added !== 0) return failed(report, source, json, finish, added);
7143
+ report.marketplace.configured = true;
7144
+ report.marketplace.configured_source = source;
7145
+ }
7146
+ const installed = await step(run, ["plugin", "install", pluginId], json, report);
7147
+ if (installed !== 0) return failed(report, source, json, finish, installed);
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;
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
+ }
7156
+ if (!loggedIn())
7157
+ report.next.push("Run `dashwise login` once, so the plugin\u2019s MCP server can start.");
7158
+ if (!json) {
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.");
7166
+ }
7167
+ return finish(0);
7168
+ }
7169
+ async function step(run, args, json, report) {
7170
+ const printable = `claude ${args.join(" ")}`;
7171
+ if (!json) info(`Running: ${printable}`);
7172
+ const result = await run("claude", args, { capture: json });
7173
+ const exitCode = result.code ?? 1;
7174
+ report.steps.push({ command: printable, exit_code: result.code });
7175
+ if (exitCode !== 0 && json)
7176
+ report.error = result.stderr.trim() || `${printable} exited ${exitCode}`;
7177
+ return exitCode;
7178
+ }
7179
+ function failed(report, source, json, finish, exitCode) {
7180
+ report.manual_steps = manualSteps(source);
7181
+ if (!json) {
7182
+ const last = report.steps[report.steps.length - 1];
7183
+ fail(
7184
+ `\`${last?.command}\` failed (exit ${last?.exit_code ?? "none"}). Nothing after it was run.`
7185
+ );
7186
+ info("To finish by hand:");
7187
+ for (const line of report.manual_steps) out(line);
7188
+ }
7189
+ return finish(exitCode);
7190
+ }
7191
+ function printStatus(report) {
7192
+ out(`Claude Code: ${report.claude.version ?? "found"}`);
7193
+ const m = report.marketplace;
7194
+ out(
7195
+ `Marketplace ${m.name}: ` + (m.configured === null ? "could not tell" : m.configured ? `configured${m.configured_source ? ` (${m.configured_source})` : ""}` : `not configured (setup would add ${m.source})`)
7196
+ );
7197
+ const p = report.plugin;
7198
+ const detail = [
7199
+ p.version,
7200
+ p.scope && `${p.scope} scope`,
7201
+ p.enabled === false ? "disabled" : p.enabled ? "enabled" : void 0
7202
+ ].filter(Boolean).join(", ");
7203
+ out(
7204
+ `Plugin ${p.id}: ` + (p.installed === null ? "could not tell" : p.installed ? `installed${detail ? ` (${detail})` : ""}` : "not installed")
7205
+ );
7206
+ if (p.others?.length) out(`Also installed from other marketplaces: ${p.others.join(", ")}`);
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
+ }
7256
+
5658
7257
  // src/commands/workspace.ts
5659
7258
  init_api_client();
5660
7259
  init_config();
7260
+ init_workspaces();
5661
7261
  init_output();
5662
7262
  async function workspaceListCommand(options = {}) {
5663
7263
  const cfg = readConfig();
@@ -5724,33 +7324,138 @@ async function workspaceUseCommand(slug) {
5724
7324
  fail("Not logged in. Run `dashwise login` first.");
5725
7325
  return 1;
5726
7326
  }
5727
- if (typeof slug !== "string" || slug.length === 0) {
5728
- fail("Usage: `dashwise workspace use <slug>`");
5729
- return 1;
7327
+ if (typeof slug !== "string" || slug.length === 0) {
7328
+ fail("Usage: `dashwise workspace use <slug>`");
7329
+ return 1;
7330
+ }
7331
+ let workspaces = [];
7332
+ try {
7333
+ workspaces = await apiRequest("GET", "/api/workspaces");
7334
+ } catch (err) {
7335
+ fail(`Could not validate the workspace (${err.message}).`);
7336
+ return 1;
7337
+ }
7338
+ const match = workspaces.find(
7339
+ (w) => w.slug === slug || `id:${w.id}` === slug || String(w.id) === slug
7340
+ );
7341
+ if (!match) {
7342
+ fail(`Workspace ${code(slug)} not found in your accessible workspaces.`);
7343
+ out("");
7344
+ info("Run `dashwise workspace list` to see what you can choose.");
7345
+ return 1;
7346
+ }
7347
+ const matchedSlug = match.slug ?? `id:${match.id}`;
7348
+ writeConfig({ ...cfg, workspaceSlug: matchedSlug });
7349
+ success(`Default workspace set to ${code(matchedSlug)}`);
7350
+ kv("Name", match.name);
7351
+ if (match.role ?? match.permissions) {
7352
+ kv("Role", String(match.role ?? match.permissions));
7353
+ }
7354
+ return 0;
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;
5730
7435
  }
5731
- let workspaces = [];
5732
- try {
5733
- workspaces = await apiRequest("GET", "/api/workspaces");
5734
- } catch (err) {
5735
- fail(`Could not validate the workspace (${err.message}).`);
5736
- return 1;
7436
+ if (tables.length === 0) {
7437
+ info(`No tables visible to you in workspace ${code(workspace2.slug)}.`);
7438
+ return 0;
5737
7439
  }
5738
- const match = workspaces.find(
5739
- (w) => w.slug === slug || `id:${w.id}` === slug || String(w.id) === slug
7440
+ out("");
7441
+ info(
7442
+ `${tables.length} table${tables.length === 1 ? "" : "s"} in ${code(workspace2.slug)} ${dim(`(id ${workspace2.id})`)}:`
5740
7443
  );
5741
- if (!match) {
5742
- fail(`Workspace ${code(slug)} not found in your accessible workspaces.`);
5743
- out("");
5744
- info("Run `dashwise workspace list` to see what you can choose.");
5745
- return 1;
5746
- }
5747
- const matchedSlug = match.slug ?? `id:${match.id}`;
5748
- writeConfig({ ...cfg, workspaceSlug: matchedSlug });
5749
- success(`Default workspace set to ${code(matchedSlug)}`);
5750
- kv("Name", match.name);
5751
- if (match.role ?? match.permissions) {
5752
- kv("Role", String(match.role ?? match.permissions));
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}`);
5753
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
+ );
5754
7459
  return 0;
5755
7460
  }
5756
7461
 
@@ -6446,6 +8151,53 @@ function extractManifestIssues(err) {
6446
8151
 
6447
8152
  // src/commands/module/generate.ts
6448
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();
6449
8201
  init_output();
6450
8202
  function outputDir(projectRoot) {
6451
8203
  return join(projectRoot, "node_modules", "@dashai", "generated");
@@ -6491,6 +8243,14 @@ async function moduleGenerateCommand(options) {
6491
8243
  fail(`Could not generate SDK: ${err.message}`);
6492
8244
  return 1;
6493
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);
6494
8254
  if (options.dryRun) {
6495
8255
  info(`Dry run: ${Object.keys(res.files).length} files`);
6496
8256
  info(`Hash: ${code(res.contentHash)}`);
@@ -6513,8 +8273,25 @@ async function moduleGenerateCommand(options) {
6513
8273
  `Generated ${Object.keys(res.files).length} files into ${code("node_modules/@dashai/generated/")}`
6514
8274
  );
6515
8275
  info(`Hash: ${dim(res.contentHash)}`);
8276
+ if (verdict.note !== null && declaresUses(manifest)) info(verdict.note);
8277
+ warnIfSdkBelowUsesFloor(projectRoot, manifest);
6516
8278
  return 0;
6517
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
+ }
6518
8295
 
6519
8296
  // src/commands/module/dev.ts
6520
8297
  init_output();
@@ -6818,6 +8595,7 @@ async function resolveWorkspaceWithFallback(opts) {
6818
8595
 
6819
8596
  // src/commands/module/install.ts
6820
8597
  init_output();
8598
+ init_connect_status();
6821
8599
  async function moduleInstallCommand(slugArg, options) {
6822
8600
  const slug = resolveSlug(slugArg, options.dir);
6823
8601
  if (!slug) {
@@ -6991,6 +8769,8 @@ async function moduleInstallCommand(slugArg, options) {
6991
8769
  const publicUrl = row?.public_url ?? null;
6992
8770
  const installedVersion = row?.module_version?.version ?? targetVersion;
6993
8771
  const runtimeProvisioned = isRealRuntimeUrl(row?.runtime_url);
8772
+ const readiness = await fetchUsesReadiness(result.installation.id, auth);
8773
+ const setupPending = readiness?.setupPending ?? false;
6994
8774
  if (options.json) {
6995
8775
  out(
6996
8776
  JSON.stringify({
@@ -6999,11 +8779,15 @@ async function moduleInstallCommand(slugArg, options) {
6999
8779
  version: installedVersion,
7000
8780
  was_upgrade: result.was_upgrade,
7001
8781
  public_url: publicUrl,
7002
- 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
7003
8787
  })
7004
8788
  );
7005
8789
  if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
7006
- return 0;
8790
+ return setupPending ? EXIT_SETUP_PENDING : 0;
7007
8791
  }
7008
8792
  if (!verified) {
7009
8793
  success(
@@ -7016,9 +8800,15 @@ async function moduleInstallCommand(slugArg, options) {
7016
8800
  return 0;
7017
8801
  }
7018
8802
  if (runtimeProvisioned && publicUrl) {
7019
- success(
7020
- `Deployed ${code(mod.slug)} ${code(`v${installedVersion}`)} \u2014 live at ${code(publicUrl)}`
7021
- );
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
+ }
7022
8812
  info(
7023
8813
  `Install ${code(`#${result.installation.id}`)} (kind=${row?.kind ?? "unknown"}) in workspace ${code(workspace2.slug)}${result.was_upgrade ? " \u2014 upgraded in place." : "."}`
7024
8814
  );
@@ -7037,8 +8827,9 @@ async function moduleInstallCommand(slugArg, options) {
7037
8827
  `No public URL yet \u2014 runtime provisioning didn't complete. Re-running ${code("dashwise module install")} is idempotent and repairs a failed provision.`
7038
8828
  );
7039
8829
  }
8830
+ if (readiness !== null) renderUsesReadiness(readiness, result);
7040
8831
  if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
7041
- return 0;
8832
+ return setupPending ? EXIT_SETUP_PENDING : 0;
7042
8833
  }
7043
8834
  function publishedModuleLocalDevGuidance(slug) {
7044
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.
@@ -7194,6 +8985,18 @@ function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
7194
8985
  }
7195
8986
  return 1;
7196
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
+ }
7197
9000
 
7198
9001
  // src/commands/module/list.ts
7199
9002
  init_api_client();
@@ -7307,6 +9110,109 @@ async function moduleListCommand(options) {
7307
9110
  init_api_client();
7308
9111
  init_config();
7309
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
7310
9216
  var DEFAULT_EXCLUDE_PATTERNS = [
7311
9217
  "node_modules",
7312
9218
  ".next",
@@ -7327,6 +9233,7 @@ var DEFAULT_EXCLUDE_PATTERNS = [
7327
9233
  "dist",
7328
9234
  "build"
7329
9235
  ];
9236
+ var ALWAYS_EXCLUDE_FILES = ["seed.mjs"];
7330
9237
  var PER_FILE_BYTE_LIMIT = 10 * 1024 * 1024;
7331
9238
  var MAX_TARBALL_BYTES = 100 * 1024 * 1024;
7332
9239
  async function packTarball(opts) {
@@ -7338,10 +9245,18 @@ async function packTarball(opts) {
7338
9245
  ...opts.extraExclude ?? []
7339
9246
  ]);
7340
9247
  const filesIncluded = [];
9248
+ const alwaysExcluded = [];
7341
9249
  const warnings = [];
7342
- 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;
7343
9254
  for (const abs of candidates) {
7344
9255
  const rel = relative(opts.cwd, abs);
9256
+ if (ALWAYS_EXCLUDE_FILES.includes(rel)) {
9257
+ alwaysExcluded.push(rel);
9258
+ continue;
9259
+ }
7345
9260
  const size = statSync(abs).size;
7346
9261
  if (size > PER_FILE_BYTE_LIMIT) {
7347
9262
  warnings.push(
@@ -7357,6 +9272,7 @@ async function packTarball(opts) {
7357
9272
  );
7358
9273
  }
7359
9274
  filesIncluded.sort();
9275
+ alwaysExcluded.sort();
7360
9276
  const chunks = [];
7361
9277
  await new Promise((resolveDone, rejectDone) => {
7362
9278
  const stream = tar.c(
@@ -7381,14 +9297,30 @@ async function packTarball(opts) {
7381
9297
  `packTarball: tarball is ${formatBytes(bytes.byteLength)}, exceeds ${formatBytes(MAX_TARBALL_BYTES)} max`
7382
9298
  );
7383
9299
  }
7384
- return { bytes, filesIncluded, warnings };
9300
+ return { bytes, filesIncluded, warnings, gitignoredCount, alwaysExcluded };
7385
9301
  }
7386
- function walk(root, excludeSet) {
9302
+ function walk(root, excludeSet, respectGitignore) {
7387
9303
  const out2 = [];
7388
- 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
+ ];
7389
9313
  while (stack.length > 0) {
7390
- const dir = stack.pop();
7391
- 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
+ }
7392
9324
  let entries;
7393
9325
  try {
7394
9326
  entries = readdirSync(dir, { withFileTypes: true });
@@ -7398,14 +9330,34 @@ function walk(root, excludeSet) {
7398
9330
  for (const entry of entries) {
7399
9331
  if (excludeSet.has(entry.name)) continue;
7400
9332
  const abs = join(dir, entry.name);
7401
- if (entry.isDirectory()) {
7402
- stack.push(abs);
7403
- } 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 {
7404
9344
  out2.push(abs);
7405
9345
  }
7406
9346
  }
7407
9347
  }
7408
- 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("/");
7409
9361
  }
7410
9362
  function formatBytes(n) {
7411
9363
  if (n < 1024) return `${n}B`;
@@ -7415,6 +9367,11 @@ function formatBytes(n) {
7415
9367
 
7416
9368
  // src/commands/module/publish.ts
7417
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
+ }
7418
9375
  async function modulePublishCommand(options) {
7419
9376
  const projectRoot = resolve(options.dir ?? process.cwd());
7420
9377
  const manifestPath2 = join(projectRoot, "module.json");
@@ -7489,7 +9446,8 @@ async function modulePublishCommand(options) {
7489
9446
  try {
7490
9447
  tarball = await packTarball({
7491
9448
  cwd: projectRoot,
7492
- ...options.exclude !== void 0 ? { extraExclude: options.exclude } : {}
9449
+ ...options.exclude !== void 0 ? { extraExclude: options.exclude } : {},
9450
+ respectGitignore: !options.noRespectGitignore
7493
9451
  });
7494
9452
  } catch (err) {
7495
9453
  fail(`Could not pack tarball: ${err.message}`);
@@ -7498,6 +9456,17 @@ async function modulePublishCommand(options) {
7498
9456
  info(
7499
9457
  `Packed ${tarball.filesIncluded.length} file${tarball.filesIncluded.length === 1 ? "" : "s"} into ${formatBytes2(tarball.bytes.byteLength)} (cap ${formatBytes2(MAX_TARBALL_BYTES)}).`
7500
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
+ }
7501
9470
  for (const w of tarball.warnings) warn(w);
7502
9471
  if (!options.yes && process.stdout.isTTY && !options.dryRun) {
7503
9472
  const answer = await select({
@@ -7618,7 +9587,7 @@ async function pollBuild(buildId, moduleId, slug, options) {
7618
9587
  `/api/modules/${moduleId}/builds/${buildId}`
7619
9588
  );
7620
9589
  } catch {
7621
- await sleep2(intervalMs);
9590
+ await sleep(intervalMs);
7622
9591
  continue;
7623
9592
  }
7624
9593
  if (!options.json && build.status !== lastStatus) {
@@ -7647,14 +9616,14 @@ async function pollBuild(buildId, moduleId, slug, options) {
7647
9616
  }
7648
9617
  return 1;
7649
9618
  }
7650
- await sleep2(intervalMs);
9619
+ await sleep(intervalMs);
7651
9620
  }
7652
9621
  warn(
7653
9622
  `Still building after ${options.timeoutSec ?? 360}s \u2014 the build is likely still running. Re-run ${code("dashwise module publish")} to check, or inspect build ${code(`#${buildId}`)}.`
7654
9623
  );
7655
9624
  return 0;
7656
9625
  }
7657
- function sleep2(ms) {
9626
+ function sleep(ms) {
7658
9627
  return new Promise((r) => setTimeout(r, ms));
7659
9628
  }
7660
9629
  function formatBytes2(n) {
@@ -7668,6 +9637,7 @@ init_api_client();
7668
9637
  init_config();
7669
9638
  init_manifest_io();
7670
9639
  init_output();
9640
+ init_connect_status();
7671
9641
  async function moduleStatusCommand(slugArg, options) {
7672
9642
  let slug = slugArg;
7673
9643
  if (!slug) {
@@ -7753,6 +9723,7 @@ async function moduleStatusCommand(slugArg, options) {
7753
9723
  const state = runtimeProvisioned && row.public_url ? "live" : runtimeProvisioned ? "deployed" : "not_deployed";
7754
9724
  const installedVersion = row.module_version?.version ?? null;
7755
9725
  const updateAvailable = isUpdateAvailable(installedVersion, latestVersion);
9726
+ const readiness = await fetchUsesReadiness(row.id, auth);
7756
9727
  if (options.json) {
7757
9728
  out(
7758
9729
  JSON.stringify({
@@ -7768,6 +9739,11 @@ async function moduleStatusCommand(slugArg, options) {
7768
9739
  runtime_url: row.runtime_url,
7769
9740
  public_url: row.public_url,
7770
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,
7771
9747
  // The coexisting dev install, so a script can see BOTH slots — the
7772
9748
  // human output reports it and `--json` used to hide it.
7773
9749
  dev_install: siblingDevInstall ? { id: siblingDevInstall.id, kind: siblingDevInstall.kind ?? null } : null
@@ -7789,8 +9765,12 @@ async function moduleStatusCommand(slugArg, options) {
7789
9765
  runtimeProvisioned ? `provisioned ${dim(`(${row.runtime_url})`)}` : "not provisioned"
7790
9766
  );
7791
9767
  kv("Public URL:", row.public_url ?? dim("(none)"));
7792
- kv("State:", stateLabel);
9768
+ kv(
9769
+ "State:",
9770
+ readiness?.setupPending ? `${stateLabel} ${dim("\u2014 setup pending")}` : stateLabel
9771
+ );
7793
9772
  out("");
9773
+ if (readiness !== null) renderUsesReadiness(readiness, row);
7794
9774
  if (siblingDevInstall) {
7795
9775
  info(
7796
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.`
@@ -7807,6 +9787,20 @@ async function moduleStatusCommand(slugArg, options) {
7807
9787
  }
7808
9788
  return 0;
7809
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
+ }
7810
9804
 
7811
9805
  // src/commands/module/uninstall.ts
7812
9806
  init_api_client();
@@ -9413,30 +11407,8 @@ function surfaceBindingError(err) {
9413
11407
  init_api_client();
9414
11408
  init_config();
9415
11409
  init_manifest_io();
9416
-
9417
- // src/lib/uses-types.ts
9418
- var PHYSICAL_TO_ABSTRACT = {
9419
- text: "text",
9420
- long_text: "long_text",
9421
- number: "number",
9422
- rating: "number",
9423
- boolean: "boolean",
9424
- // A date-with-time column has physical registry type `date` (with
9425
- // `config.includeTime`), NOT a distinct `datetime` type — the backend
9426
- // registry has no `datetime` key. So introspection only ever reports
9427
- // `date` here; there is no `datetime` physical entry to fold.
9428
- date: "date",
9429
- email: "email",
9430
- url: "url",
9431
- single_select: "single_select",
9432
- multi_select: "multi_select"
9433
- };
9434
- function abstractTypeForPhysical(physicalType) {
9435
- return PHYSICAL_TO_ABSTRACT[physicalType] ?? null;
9436
- }
9437
- var USES_OPERATIONS = ["create", "read", "update", "delete"];
9438
-
9439
- // src/commands/uses/add.ts
11410
+ init_uses_types();
11411
+ init_sdk_version();
9440
11412
  init_output();
9441
11413
  async function usesAddCommand(options) {
9442
11414
  if (!options.fromTable) {
@@ -9457,6 +11429,18 @@ async function usesAddCommand(options) {
9457
11429
  fail(err.message);
9458
11430
  return 1;
9459
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;
9460
11444
  let table;
9461
11445
  try {
9462
11446
  table = await introspectWorkspaceTable(
@@ -9467,10 +11451,10 @@ async function usesAddCommand(options) {
9467
11451
  } catch (err) {
9468
11452
  return surfaceIntrospectError(err, options.fromTable);
9469
11453
  }
9470
- const slotSlug = options.slug ?? slugify(table.name);
9471
- if (!isKebab(slotSlug)) {
11454
+ const slotSlug = options.slug ?? slugifyUsesName(table.name);
11455
+ if (!isUsesSlug(slotSlug)) {
9472
11456
  fail(
9473
- `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.`
9474
11458
  );
9475
11459
  return 1;
9476
11460
  }
@@ -9481,6 +11465,37 @@ async function usesAddCommand(options) {
9481
11465
  );
9482
11466
  return 1;
9483
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
+ }
9484
11499
  const slot = {
9485
11500
  slug: slotSlug,
9486
11501
  ops,
@@ -9517,6 +11532,13 @@ async function usesAddCommand(options) {
9517
11532
  }
9518
11533
  uses.tables = tables;
9519
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
+ }
9520
11542
  try {
9521
11543
  writeManifest(ctx.manifest, ctx.dir);
9522
11544
  } catch (err) {
@@ -9526,8 +11548,7 @@ async function usesAddCommand(options) {
9526
11548
  success(
9527
11549
  `${existingIdx >= 0 ? "Replaced" : "Added"} ${code(`uses.tables[${slotSlug}]`)} in module.json.`
9528
11550
  );
9529
- const pushed = await pushManifest(ctx);
9530
- if (!pushed) {
11551
+ if (!pushed.ok) {
9531
11552
  info(
9532
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}`)}.`
9533
11554
  );
@@ -9552,6 +11573,52 @@ async function usesAddCommand(options) {
9552
11573
  }
9553
11574
  return 0;
9554
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
+ }
9555
11622
  function loadContext3(opts) {
9556
11623
  const manifest = readManifestOrThrow(opts.dir);
9557
11624
  const moduleSlug = manifest.module?.slug;
@@ -9570,6 +11637,7 @@ function loadContext3(opts) {
9570
11637
  }
9571
11638
  return {
9572
11639
  installationId: entry.installationId,
11640
+ ...entry.workspaceId !== void 0 ? { workspaceId: entry.workspaceId } : {},
9573
11641
  moduleSlug,
9574
11642
  ...opts.dir !== void 0 ? { dir: opts.dir } : {},
9575
11643
  manifest,
@@ -9589,10 +11657,10 @@ async function pushManifest(ctx) {
9589
11657
  warn(
9590
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.`
9591
11659
  );
9592
- return false;
11660
+ return { ok: false, rejected: false };
9593
11661
  }
9594
11662
  warn(`Couldn't resolve the module id to push the manifest: ${err.message}`);
9595
- return false;
11663
+ return { ok: false, rejected: false };
9596
11664
  }
9597
11665
  try {
9598
11666
  await applyDraftManifest(
@@ -9601,20 +11669,20 @@ async function pushManifest(ctx) {
9601
11669
  ctx.requestOptions
9602
11670
  );
9603
11671
  success("Pushed the manifest \u2014 the backend parked the new slot, ready to bind.");
9604
- return true;
11672
+ return { ok: true };
9605
11673
  } catch (err) {
9606
11674
  if (err instanceof ApiError) {
9607
11675
  if (err.code === "MANIFEST_INVALID" || err.status === 400) {
9608
11676
  fail(`Manifest rejected by the backend validator: ${err.message}`);
9609
11677
  const issues = extractIssues(err.context);
9610
11678
  for (const issue of issues) out(` ${dim("-")} ${issue}`);
9611
- } else {
9612
- fail(`Manifest push failed: ${err.code} ${err.status} \u2014 ${err.message}`);
11679
+ return { ok: false, rejected: true };
9613
11680
  }
9614
- } else {
9615
- fail(`Manifest push failed: ${err.message}`);
11681
+ fail(`Manifest push failed: ${err.code} ${err.status} \u2014 ${err.message}`);
11682
+ return { ok: false, rejected: false };
9616
11683
  }
9617
- return false;
11684
+ fail(`Manifest push failed: ${err.message}`);
11685
+ return { ok: false, rejected: false };
9618
11686
  }
9619
11687
  }
9620
11688
  function extractIssues(context) {
@@ -9664,13 +11732,6 @@ function parseOps(opsCsv) {
9664
11732
  }
9665
11733
  return USES_OPERATIONS.filter((o) => seen.has(o));
9666
11734
  }
9667
- function slugify(name) {
9668
- return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
9669
- }
9670
- var KEBAB_REGEX = /^[a-z][a-z0-9-]*[a-z0-9]$/;
9671
- function isKebab(slug) {
9672
- return KEBAB_REGEX.test(slug);
9673
- }
9674
11735
  function surfaceIntrospectError(err, tableRef) {
9675
11736
  if (err instanceof ApiError) {
9676
11737
  switch (err.code) {
@@ -10861,7 +12922,7 @@ async function runTail(moduleId, options) {
10861
12922
  } catch (err) {
10862
12923
  info(dim(`(tail poll error: ${err.message})`));
10863
12924
  }
10864
- await sleep3(POLL_INTERVAL_MS);
12925
+ await sleep2(POLL_INTERVAL_MS);
10865
12926
  }
10866
12927
  }
10867
12928
  function buildTailParams(options) {
@@ -10974,29 +13035,37 @@ function formatRow(row) {
10974
13035
  function formatTimestamp(iso) {
10975
13036
  return iso.slice(0, 19).replace("T", " ");
10976
13037
  }
10977
- function sleep3(ms) {
10978
- return new Promise((resolve13) => setTimeout(resolve13, ms));
13038
+ function sleep2(ms) {
13039
+ return new Promise((resolve14) => setTimeout(resolve14, ms));
10979
13040
  }
10980
13041
 
10981
13042
  // src/commands/module/action-log.ts
10982
13043
  init_api_client();
13044
+ init_manifest_io();
10983
13045
  init_output();
10984
13046
  var POLL_INTERVAL_MS2 = 5e3;
10985
13047
  async function moduleActionLogCommand(options) {
10986
- if (options.installation === void 0 && options.module === void 0) {
10987
- fail("Must pass either `--installation <id>` or `--module <slug>`.");
10988
- return 1;
10989
- }
10990
13048
  if (options.installation !== void 0 && options.module !== void 0) {
10991
13049
  fail("`--installation` and `--module` are mutually exclusive \u2014 pass one or the other.");
10992
13050
  return 1;
10993
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
+ }
10994
13063
  let installationId = options.installation;
10995
- if (installationId === void 0 && options.module !== void 0) {
13064
+ if (installationId === void 0 && moduleSlug !== void 0) {
10996
13065
  try {
10997
13066
  const resolved = await apiRequest(
10998
13067
  "GET",
10999
- `/api/modules/by-slug/${encodeURIComponent(options.module)}/my-installation`
13068
+ `/api/modules/by-slug/${encodeURIComponent(moduleSlug)}/my-installation`
11000
13069
  );
11001
13070
  installationId = resolved.installation_id;
11002
13071
  } catch (err) {
@@ -11069,7 +13138,7 @@ async function runTail2(installationId, options) {
11069
13138
  }
11070
13139
  dim(`(tail: transient error \u2014 ${err.message})`);
11071
13140
  }
11072
- await new Promise((resolve13) => setTimeout(resolve13, POLL_INTERVAL_MS2));
13141
+ await new Promise((resolve14) => setTimeout(resolve14, POLL_INTERVAL_MS2));
11073
13142
  }
11074
13143
  }
11075
13144
  function printTable(rows) {
@@ -11134,6 +13203,17 @@ function mapAndPrintError(err, json) {
11134
13203
  else fail(msg);
11135
13204
  return 1;
11136
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
+ }
11137
13217
 
11138
13218
  // src/commands/module/logs.ts
11139
13219
  init_api_client();
@@ -11323,7 +13403,7 @@ async function runFollow(installationId, label, opts, auth) {
11323
13403
  return mapLogsError(err, false);
11324
13404
  }
11325
13405
  info(dim(`(follow: transient error \u2014 ${err.message})`));
11326
- await sleep4(POLL_INTERVAL_MS3, () => stopped);
13406
+ await sleep3(POLL_INTERVAL_MS3, () => stopped);
11327
13407
  firstPoll = false;
11328
13408
  continue;
11329
13409
  }
@@ -11346,7 +13426,7 @@ async function runFollow(installationId, label, opts, auth) {
11346
13426
  }
11347
13427
  }
11348
13428
  firstPoll = false;
11349
- await sleep4(POLL_INTERVAL_MS3, () => stopped);
13429
+ await sleep3(POLL_INTERVAL_MS3, () => stopped);
11350
13430
  }
11351
13431
  } finally {
11352
13432
  process.off("SIGINT", onSigint);
@@ -11439,11 +13519,11 @@ function mapLogsError(err, json) {
11439
13519
  else fail(message);
11440
13520
  return 1;
11441
13521
  }
11442
- async function sleep4(ms, aborted) {
13522
+ async function sleep3(ms, aborted) {
11443
13523
  const slice = 100;
11444
13524
  let waited = 0;
11445
13525
  while (waited < ms && !aborted()) {
11446
- await new Promise((resolve13) => setTimeout(resolve13, Math.min(slice, ms - waited)));
13526
+ await new Promise((resolve14) => setTimeout(resolve14, Math.min(slice, ms - waited)));
11447
13527
  waited += slice;
11448
13528
  }
11449
13529
  }
@@ -11453,7 +13533,7 @@ init_output();
11453
13533
  var program = new Command();
11454
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(
11455
13535
  "--api-url <url>",
11456
- "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.`
11457
13537
  );
11458
13538
  program.hook("preAction", () => {
11459
13539
  const apiUrl = program.opts().apiUrl?.trim();
@@ -11479,12 +13559,107 @@ program.command("profile").description("Show the current user, default workspace
11479
13559
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
11480
13560
  });
11481
13561
  });
13562
+ var mcp = program.command("mcp").description(
13563
+ "Run a local MCP server for a workspace, so Claude can read and write your data (`dashwise mcp` runs `serve`; see `dashwise mcp serve --help`), or register it with Claude Code (`add`)."
13564
+ );
13565
+ var collect = (value, previous) => [
13566
+ ...previous ?? [],
13567
+ value
13568
+ ];
13569
+ mcp.command("serve", { isDefault: true }).description(
13570
+ "Run the local MCP server (the default). The key is exchanged at startup and held in memory \u2014 it never reaches a config file or your shell history."
13571
+ ).option(
13572
+ "--workspace <slug>",
13573
+ "Workspace to connect (default: `dashwise workspace use`)."
13574
+ ).option(
13575
+ "--scope <grant>",
13576
+ "Narrow this session's key, enforced by the server. Repeatable; grants add up. `datahub:table=<id>:<ops>`, `datahub:app=<id>:<ops>`, `datahub:workspace:<ops>` (ops: one to four of c r u d) or `governance:workspace:r`. Each must be within your own access. Omitted: the key reaches what your account can.",
13577
+ collect
13578
+ ).option(
13579
+ "--read-only",
13580
+ "Refuse write tools. A CLIENT-SIDE GUARD: this proxy declines the call and the server is not told. It prevents accidents; it is not a fence. For a server-enforced limit, use --scope (for example `--scope datahub:workspace:r`)."
13581
+ ).option(
13582
+ "--endpoint-name <label>",
13583
+ "Label for this machine's endpoint (default: your hostname). Each run gets its own endpoint, named `dashwise-cli@<label>#<id>` \u2014 the server adds the short id so concurrent sessions on one machine are told apart in the endpoints panel, where an admin can revoke one of them. The session lasts 24 hours and is revoked automatically on exit."
13584
+ ).option(
13585
+ "--verbose",
13586
+ "Log frame types and sizes to stderr. Never frame bodies, never the key."
13587
+ ).action(
13588
+ async (cmdOpts) => {
13589
+ process.exitCode = await mcpCommand({
13590
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
13591
+ ...cmdOpts.scope !== void 0 ? { scopes: cmdOpts.scope } : {},
13592
+ ...cmdOpts.readOnly !== void 0 ? { readOnly: cmdOpts.readOnly } : {},
13593
+ ...cmdOpts.endpointName !== void 0 ? { endpointName: cmdOpts.endpointName } : {},
13594
+ ...cmdOpts.verbose !== void 0 ? { verbose: cmdOpts.verbose } : {}
13595
+ });
13596
+ }
13597
+ );
13598
+ mcp.command("add").description(
13599
+ "Register `dashwise mcp` with Claude Code by shelling out to `claude mcp add`. Prints the exact command it runs, so it can be copied when `claude` is absent."
13600
+ ).option("--workspace <slug>", "Workspace to bake into the registration.").option(
13601
+ "--read-only",
13602
+ "Register the read-only form (client-side guard; see `dashwise mcp serve --help`)."
13603
+ ).option("--endpoint-name <label>", "Label for this machine's endpoint.").option(
13604
+ "--name <server-name>",
13605
+ "Name Claude Code shows for the server (default: dashwise)."
13606
+ ).addOption(
13607
+ new Option(
13608
+ "--scope <scope>",
13609
+ "Claude Code's config scope \u2014 where the registration is saved. Not the key's reach: for that, --grant-scope."
13610
+ ).choices(["local", "project", "user"]).default("local")
13611
+ ).option(
13612
+ "--grant-scope <grant>",
13613
+ "Narrow the registered server's key, enforced by the server: written into the registration as `dashwise mcp --scope <grant>`. Repeatable; grants add up. Same grammar as `dashwise mcp serve --scope`.",
13614
+ collect
13615
+ ).action(
13616
+ async (cmdOpts) => {
13617
+ process.exitCode = await mcpAddCommand({
13618
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
13619
+ ...cmdOpts.readOnly !== void 0 ? { readOnly: cmdOpts.readOnly } : {},
13620
+ ...cmdOpts.endpointName !== void 0 ? { endpointName: cmdOpts.endpointName } : {},
13621
+ ...cmdOpts.name !== void 0 ? { name: cmdOpts.name } : {},
13622
+ ...cmdOpts.scope !== void 0 ? { scope: cmdOpts.scope } : {},
13623
+ ...cmdOpts.grantScope !== void 0 ? { grantScopes: cmdOpts.grantScope } : {}
13624
+ });
13625
+ }
13626
+ );
13627
+ var agent = program.command("agent").description(
13628
+ "Set up a coding agent to work with Dashwise (`dashwise agent setup`, `dashwise agent status`)."
13629
+ );
13630
+ agent.command("setup").description(
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."
13632
+ ).option(
13633
+ "--marketplace <source>",
13634
+ `Marketplace to add: owner/repo, a git URL, a local path or a URL (default: ${DEFAULT_MARKETPLACE_SOURCE}).`
13635
+ ).option("--status", "Report what is installed, and change nothing.").option("--json", "Print one JSON object on stdout instead of progress lines.").action(async (cmdOpts) => {
13636
+ process.exitCode = await agentSetupCommand({
13637
+ ...cmdOpts.marketplace !== void 0 ? { marketplace: cmdOpts.marketplace } : {},
13638
+ ...cmdOpts.status !== void 0 ? { status: cmdOpts.status } : {},
13639
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
13640
+ });
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
+ });
11482
13649
  var workspace = program.command("workspace").description("Workspace context commands.");
11483
13650
  workspace.command("list").description("List accessible workspaces.").option("--json", "Emit JSON to stdout instead of human-readable columns.").action(async (cmdOpts) => {
11484
13651
  process.exitCode = await workspaceListCommand({
11485
13652
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
11486
13653
  });
11487
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
+ });
11488
13663
  workspace.command("use <slug>").description("Set the default workspace for subsequent commands.").action(async (slug) => {
11489
13664
  process.exitCode = await workspaceUseCommand(slug);
11490
13665
  });
@@ -11616,12 +13791,18 @@ moduleCmd.command("export [dir]").description(
11616
13791
  });
11617
13792
  }
11618
13793
  );
11619
- 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) => {
11620
- process.exitCode = await moduleGenerateCommand({
11621
- ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
11622
- ...cmdOpts.dryRun !== void 0 ? { dryRun: cmdOpts.dryRun } : {}
11623
- });
11624
- });
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
+ );
11625
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) => {
11626
13807
  process.exitCode = await moduleDevCommand({
11627
13808
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
@@ -11772,7 +13953,7 @@ usesCmd.command("add").description(
11772
13953
  "Comma-separated operations to declare (create,read,update,delete). Default: read."
11773
13954
  ).option(
11774
13955
  "--purpose <text>",
11775
- "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".`
11776
13957
  ).option(
11777
13958
  "--no-bind",
11778
13959
  "Only write + push the manifest entry; don't attempt to bind it to the source table."
@@ -11955,6 +14136,9 @@ envCmd.command("export").description(
11955
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(
11956
14137
  "--exclude <pattern...>",
11957
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)."
11958
14142
  ).option(
11959
14143
  "--no-wait",
11960
14144
  "Enqueue the build and exit immediately (print the build id) instead of polling to completion."
@@ -11970,6 +14154,9 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
11970
14154
  ...cmdOpts.dryRun !== void 0 ? { dryRun: cmdOpts.dryRun } : {},
11971
14155
  ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
11972
14156
  ...cmdOpts.exclude !== void 0 ? { exclude: cmdOpts.exclude } : {},
14157
+ // commander maps `--no-respect-gitignore` to
14158
+ // `respectGitignore === false`.
14159
+ ...cmdOpts.respectGitignore === false ? { noRespectGitignore: true } : {},
11973
14160
  // commander maps `--no-wait` to `wait === false`.
11974
14161
  ...cmdOpts.wait === false ? { noWait: true } : {},
11975
14162
  ...timeoutSec !== void 0 && Number.isFinite(timeoutSec) ? { timeoutSec } : {},
@@ -11978,7 +14165,7 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
11978
14165
  }
11979
14166
  );
11980
14167
  moduleCmd.command("install [slug]").alias("deploy").description(
11981
- "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.'
11982
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(
11983
14170
  "--keep-dev-install",
11984
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."
@@ -12047,7 +14234,10 @@ moduleCmd.command("query-log").description(
12047
14234
  }
12048
14235
  );
12049
14236
  moduleCmd.command("action-log").description(
12050
- "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)."
12051
14241
  ).option(
12052
14242
  "--installation <id>",
12053
14243
  "Installation id to inspect. Mutually exclusive with `--module`.",
@@ -12069,6 +14259,7 @@ moduleCmd.command("action-log").description(
12069
14259
  return;
12070
14260
  }
12071
14261
  process.exitCode = await moduleActionLogCommand({
14262
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
12072
14263
  ...cmdOpts.installation !== void 0 ? { installation: cmdOpts.installation } : {},
12073
14264
  ...cmdOpts.module !== void 0 ? { module: cmdOpts.module } : {},
12074
14265
  ...cmdOpts.role !== void 0 ? { role: cmdOpts.role } : {},
@@ -12226,7 +14417,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
12226
14417
  }
12227
14418
  );
12228
14419
  function getVersion() {
12229
- return "0.18.3";
14420
+ return "0.20.0";
12230
14421
  }
12231
14422
  function parseIntOption(value) {
12232
14423
  const n = Number.parseInt(value, 10);