@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/README.md +149 -0
- package/dist/bin.js +2451 -260
- package/dist/bin.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
|
423
|
-
|
|
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
|
-
|
|
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((
|
|
1371
|
+
return new Promise((resolve14) => {
|
|
1171
1372
|
const server = createServer();
|
|
1172
1373
|
server.once("error", () => {
|
|
1173
|
-
|
|
1374
|
+
resolve14(false);
|
|
1174
1375
|
});
|
|
1175
1376
|
server.once("listening", () => {
|
|
1176
|
-
server.close(() =>
|
|
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
|
-
|
|
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
|
|
2363
|
-
//
|
|
2364
|
-
//
|
|
2365
|
-
"
|
|
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
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
)
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
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
|
-
|
|
3771
|
-
|
|
4103
|
+
let entries;
|
|
4104
|
+
try {
|
|
4105
|
+
entries = readdirSync(cwd);
|
|
4106
|
+
} catch {
|
|
4107
|
+
return { dir: nested };
|
|
3772
4108
|
}
|
|
3773
|
-
if (
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
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
|
-
|
|
3792
|
-
|
|
3793
|
-
`You
|
|
3794
|
-
|
|
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
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
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
|
|
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
|
|
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
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
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
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
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
|
-
|
|
5739
|
-
|
|
7440
|
+
out("");
|
|
7441
|
+
info(
|
|
7442
|
+
`${tables.length} table${tables.length === 1 ? "" : "s"} in ${code(workspace2.slug)} ${dim(`(id ${workspace2.id})`)}:`
|
|
5740
7443
|
);
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
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
|
-
|
|
7020
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
7391
|
-
if (!
|
|
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
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 ??
|
|
9471
|
-
if (!
|
|
11454
|
+
const slotSlug = options.slug ?? slugifyUsesName(table.name);
|
|
11455
|
+
if (!isUsesSlug(slotSlug)) {
|
|
9472
11456
|
fail(
|
|
9473
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
9612
|
-
fail(`Manifest push failed: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
11679
|
+
return { ok: false, rejected: true };
|
|
9613
11680
|
}
|
|
9614
|
-
|
|
9615
|
-
|
|
11681
|
+
fail(`Manifest push failed: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
11682
|
+
return { ok: false, rejected: false };
|
|
9616
11683
|
}
|
|
9617
|
-
|
|
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
|
|
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
|
|
10978
|
-
return new Promise((
|
|
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 &&
|
|
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(
|
|
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((
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
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
|
-
|
|
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.").
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
14420
|
+
return "0.20.0";
|
|
12230
14421
|
}
|
|
12231
14422
|
function parseIntOption(value) {
|
|
12232
14423
|
const n = Number.parseInt(value, 10);
|