@dashai/cli 0.8.0 → 0.9.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 +83 -22
- package/dist/bin.js +2209 -337
- package/dist/bin.js.map +1 -1
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -256,53 +256,185 @@ async function revokeApiKey(installationId, keyId, options = {}) {
|
|
|
256
256
|
options
|
|
257
257
|
);
|
|
258
258
|
}
|
|
259
|
-
async function
|
|
259
|
+
async function getOrigins(installationId, options = {}) {
|
|
260
|
+
return apiRequest(
|
|
261
|
+
"GET",
|
|
262
|
+
`/api/installations/${installationId}/origins`,
|
|
263
|
+
void 0,
|
|
264
|
+
options
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
async function setOrigins(installationId, allowedOrigins, options = {}) {
|
|
268
|
+
return apiRequest(
|
|
269
|
+
"PUT",
|
|
270
|
+
`/api/installations/${installationId}/origins`,
|
|
271
|
+
{ allowed_origins: allowedOrigins },
|
|
272
|
+
options
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
async function createTable(body, options = {}) {
|
|
260
276
|
return apiRequest(
|
|
261
277
|
"POST",
|
|
262
|
-
`/api/
|
|
278
|
+
`/api/module-api/schema/tables`,
|
|
263
279
|
body,
|
|
264
280
|
options
|
|
265
281
|
);
|
|
266
282
|
}
|
|
267
|
-
async function dropTable(
|
|
283
|
+
async function dropTable(tableSlug, options = {}) {
|
|
268
284
|
return apiRequest(
|
|
269
285
|
"DELETE",
|
|
270
|
-
`/api/
|
|
286
|
+
`/api/module-api/schema/tables/${tableSlug}`,
|
|
271
287
|
void 0,
|
|
272
288
|
options
|
|
273
289
|
);
|
|
274
290
|
}
|
|
275
|
-
async function addField(
|
|
291
|
+
async function addField(body, options = {}) {
|
|
276
292
|
return apiRequest(
|
|
277
293
|
"POST",
|
|
278
|
-
`/api/
|
|
294
|
+
`/api/module-api/schema/fields`,
|
|
279
295
|
body,
|
|
280
296
|
options
|
|
281
297
|
);
|
|
282
298
|
}
|
|
283
|
-
async function updateField(
|
|
299
|
+
async function updateField(tableSlug, fieldSlug, body, options = {}) {
|
|
284
300
|
return apiRequest(
|
|
285
301
|
"PATCH",
|
|
286
|
-
`/api/
|
|
302
|
+
`/api/module-api/schema/fields/${tableSlug}/${fieldSlug}`,
|
|
287
303
|
body,
|
|
288
304
|
options
|
|
289
305
|
);
|
|
290
306
|
}
|
|
291
|
-
async function getInstallationSchema(
|
|
307
|
+
async function getInstallationSchema(options = {}) {
|
|
292
308
|
return apiRequest(
|
|
293
309
|
"GET",
|
|
294
|
-
`/api/
|
|
310
|
+
`/api/module-api/schema`,
|
|
295
311
|
void 0,
|
|
296
312
|
options
|
|
297
313
|
);
|
|
298
314
|
}
|
|
299
|
-
async function removeField(
|
|
315
|
+
async function removeField(tableSlug, fieldSlug, options = {}) {
|
|
300
316
|
return apiRequest(
|
|
301
317
|
"DELETE",
|
|
302
|
-
`/api/
|
|
318
|
+
`/api/module-api/schema/fields/${tableSlug}/${fieldSlug}`,
|
|
319
|
+
void 0,
|
|
320
|
+
options
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
async function getInstallationDependencies(installationId, options = {}) {
|
|
324
|
+
return apiRequest(
|
|
325
|
+
"GET",
|
|
326
|
+
`/api/installations/${installationId}/dependencies`,
|
|
327
|
+
void 0,
|
|
328
|
+
options
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
async function bindDependency(installationId, providerSlug, body = {}, options = {}) {
|
|
332
|
+
return apiRequest(
|
|
333
|
+
"POST",
|
|
334
|
+
`/api/installations/${installationId}/dependencies/${providerSlug}/bind`,
|
|
335
|
+
body,
|
|
336
|
+
options
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
async function unbindDependency(installationId, providerSlug, options = {}) {
|
|
340
|
+
return apiRequest(
|
|
341
|
+
"POST",
|
|
342
|
+
`/api/installations/${installationId}/dependencies/${providerSlug}/unbind`,
|
|
343
|
+
void 0,
|
|
344
|
+
options
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
async function getInstallationBindings(installationId, options = {}) {
|
|
348
|
+
return apiRequest(
|
|
349
|
+
"GET",
|
|
350
|
+
`/api/installations/${installationId}/bindings`,
|
|
351
|
+
void 0,
|
|
352
|
+
options
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
async function getBindingCandidates(installationId, usesSlug, options = {}) {
|
|
356
|
+
return apiRequest(
|
|
357
|
+
"GET",
|
|
358
|
+
`/api/installations/${installationId}/bindings/${encodeURIComponent(usesSlug)}/candidates`,
|
|
359
|
+
void 0,
|
|
360
|
+
options
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
async function putBinding(installationId, usesSlug, body, options = {}) {
|
|
364
|
+
return apiRequest(
|
|
365
|
+
"PUT",
|
|
366
|
+
`/api/installations/${installationId}/bindings/${encodeURIComponent(usesSlug)}`,
|
|
367
|
+
body,
|
|
368
|
+
options
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
async function unbindBinding(installationId, usesSlug, options = {}) {
|
|
372
|
+
return apiRequest(
|
|
373
|
+
"POST",
|
|
374
|
+
`/api/installations/${installationId}/bindings/${encodeURIComponent(usesSlug)}/unbind`,
|
|
375
|
+
void 0,
|
|
376
|
+
options
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
async function declineBinding(installationId, usesSlug, options = {}) {
|
|
380
|
+
return apiRequest(
|
|
381
|
+
"POST",
|
|
382
|
+
`/api/installations/${installationId}/bindings/${encodeURIComponent(usesSlug)}/decline`,
|
|
383
|
+
void 0,
|
|
384
|
+
options
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
function slugifyFieldName(name) {
|
|
388
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
389
|
+
}
|
|
390
|
+
async function introspectWorkspaceTable(_installationId, tableNameOrId, options = {}) {
|
|
391
|
+
const ref = String(tableNameOrId).trim();
|
|
392
|
+
const asNumber = Number(ref);
|
|
393
|
+
if (!Number.isInteger(asNumber) || asNumber <= 0 || String(asNumber) !== ref) {
|
|
394
|
+
const message = `"${ref}" is not a numeric table id. Pass the workspace table's numeric id (find it in the Dashwise UI URL, or via \`GET /api/tables/application/:applicationId\`). Resolving a table by name is not yet supported by this command.`;
|
|
395
|
+
throw new ApiError(0, { code: "NON_NUMERIC_TABLE_REF", message }, message);
|
|
396
|
+
}
|
|
397
|
+
const table = await apiRequest(
|
|
398
|
+
"GET",
|
|
399
|
+
`/api/tables/${asNumber}`,
|
|
303
400
|
void 0,
|
|
304
401
|
options
|
|
305
402
|
);
|
|
403
|
+
const rawFields = await apiRequest(
|
|
404
|
+
"GET",
|
|
405
|
+
`/api/fields/table/${asNumber}`,
|
|
406
|
+
void 0,
|
|
407
|
+
options
|
|
408
|
+
);
|
|
409
|
+
const fields = rawFields.map((f) => ({
|
|
410
|
+
id: f.id,
|
|
411
|
+
slug: slugifyFieldName(f.name),
|
|
412
|
+
name: f.name,
|
|
413
|
+
type: f.type,
|
|
414
|
+
...f.is_required !== void 0 ? { required: f.is_required } : {}
|
|
415
|
+
}));
|
|
416
|
+
return {
|
|
417
|
+
table_id: table.id,
|
|
418
|
+
name: table.name,
|
|
419
|
+
slug: slugifyFieldName(table.name),
|
|
420
|
+
fields
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
async function getModuleBySlug(slug, options = {}) {
|
|
424
|
+
return apiRequest(
|
|
425
|
+
"GET",
|
|
426
|
+
`/api/modules/by-slug/${encodeURIComponent(slug)}`,
|
|
427
|
+
void 0,
|
|
428
|
+
options
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
async function applyDraftManifest(moduleId, manifest, options = {}) {
|
|
432
|
+
return apiRequest(
|
|
433
|
+
"PUT",
|
|
434
|
+
`/api/modules/${moduleId}/draft/manifest`,
|
|
435
|
+
manifest,
|
|
436
|
+
options
|
|
437
|
+
);
|
|
306
438
|
}
|
|
307
439
|
var ApiError;
|
|
308
440
|
var init_api_client = __esm({
|
|
@@ -326,38 +458,6 @@ var init_api_client = __esm({
|
|
|
326
458
|
};
|
|
327
459
|
}
|
|
328
460
|
});
|
|
329
|
-
function manifestPath(dir) {
|
|
330
|
-
return join(resolve(dir ?? process.cwd()), "module.json");
|
|
331
|
-
}
|
|
332
|
-
function readManifest(dir) {
|
|
333
|
-
const path = manifestPath(dir);
|
|
334
|
-
if (!existsSync(path)) return null;
|
|
335
|
-
const raw = readFileSync(path, "utf-8");
|
|
336
|
-
try {
|
|
337
|
-
return JSON.parse(raw);
|
|
338
|
-
} catch (err) {
|
|
339
|
-
throw new Error(
|
|
340
|
-
`Failed to parse ${path}: ${err.message}. Fix or delete the file and re-run.`
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
function writeManifest(manifest, dir) {
|
|
345
|
-
const path = manifestPath(dir);
|
|
346
|
-
writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
347
|
-
}
|
|
348
|
-
function readManifestOrThrow(dir) {
|
|
349
|
-
const manifest = readManifest(dir);
|
|
350
|
-
if (!manifest) {
|
|
351
|
-
throw new Error(
|
|
352
|
-
`No module.json found in ${resolve(dir ?? process.cwd())}. Run \`dashwise module init\` first or pass \`--dir <path>\`.`
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
return manifest;
|
|
356
|
-
}
|
|
357
|
-
var init_manifest_io = __esm({
|
|
358
|
-
"src/lib/manifest-io.ts"() {
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
461
|
function success(msg) {
|
|
362
462
|
process.stderr.write(`${pc.green("\u2713")} ${msg}
|
|
363
463
|
`);
|
|
@@ -393,6 +493,38 @@ var init_output = __esm({
|
|
|
393
493
|
"src/lib/output.ts"() {
|
|
394
494
|
}
|
|
395
495
|
});
|
|
496
|
+
function manifestPath(dir) {
|
|
497
|
+
return join(resolve(dir ?? process.cwd()), "module.json");
|
|
498
|
+
}
|
|
499
|
+
function readManifest(dir) {
|
|
500
|
+
const path = manifestPath(dir);
|
|
501
|
+
if (!existsSync(path)) return null;
|
|
502
|
+
const raw = readFileSync(path, "utf-8");
|
|
503
|
+
try {
|
|
504
|
+
return JSON.parse(raw);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Failed to parse ${path}: ${err.message}. Fix or delete the file and re-run.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function writeManifest(manifest, dir) {
|
|
512
|
+
const path = manifestPath(dir);
|
|
513
|
+
writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
514
|
+
}
|
|
515
|
+
function readManifestOrThrow(dir) {
|
|
516
|
+
const manifest = readManifest(dir);
|
|
517
|
+
if (!manifest) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`No module.json found in ${resolve(dir ?? process.cwd())}. Run \`dashwise module init\` first or pass \`--dir <path>\`.`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
return manifest;
|
|
523
|
+
}
|
|
524
|
+
var init_manifest_io = __esm({
|
|
525
|
+
"src/lib/manifest-io.ts"() {
|
|
526
|
+
}
|
|
527
|
+
});
|
|
396
528
|
|
|
397
529
|
// src/lib/scaffold.ts
|
|
398
530
|
function isDeployTarget(value) {
|
|
@@ -409,7 +541,16 @@ function manifestJson(opts) {
|
|
|
409
541
|
},
|
|
410
542
|
module_kind: kind,
|
|
411
543
|
owns: {
|
|
412
|
-
tables: [
|
|
544
|
+
tables: [
|
|
545
|
+
{
|
|
546
|
+
slug: "items",
|
|
547
|
+
name: "Items",
|
|
548
|
+
fields: [
|
|
549
|
+
{ slug: "title", type: "text" },
|
|
550
|
+
{ slug: "done", type: "boolean" }
|
|
551
|
+
]
|
|
552
|
+
}
|
|
553
|
+
]
|
|
413
554
|
}
|
|
414
555
|
};
|
|
415
556
|
if (kind === "custom") {
|
|
@@ -440,7 +581,7 @@ function packageJson(opts) {
|
|
|
440
581
|
start: "next start"
|
|
441
582
|
},
|
|
442
583
|
dependencies: {
|
|
443
|
-
"@dashai/sdk": "^0.
|
|
584
|
+
"@dashai/sdk": "^2.0.0",
|
|
444
585
|
next: "^15.0.0",
|
|
445
586
|
react: "^19.0.0",
|
|
446
587
|
"react-dom": "^19.0.0"
|
|
@@ -485,9 +626,27 @@ function tsconfigJson() {
|
|
|
485
626
|
}
|
|
486
627
|
function nextConfig() {
|
|
487
628
|
return `/** @type {import("next").NextConfig} */
|
|
629
|
+
//
|
|
630
|
+
// Phase 2 two-var contract: the deploy env supplies exactly
|
|
631
|
+
// DASHWISE_API_URL + DASHWISE_API_KEY. The browser needs the API URL
|
|
632
|
+
// too (the SDK's <AuthProvider> calls /api/auth/sessions/me on it), so
|
|
633
|
+
// we DERIVE NEXT_PUBLIC_DASHWISE_API_URL here from DASHWISE_API_URL at
|
|
634
|
+
// build/start time \u2014 you never set the NEXT_PUBLIC_ var yourself. If
|
|
635
|
+
// you already have one set (e.g. a custom setup), it wins.
|
|
636
|
+
const apiUrl = process.env.DASHWISE_API_URL;
|
|
637
|
+
|
|
488
638
|
const nextConfig = {
|
|
639
|
+
// Required for the platform-hosted (per-install Lambda) runtime: the
|
|
640
|
+
// injected wrapper probes [.next/standalone/server.js, server.js] and
|
|
641
|
+
// exit(1)s the cold start if neither exists. Without this the built
|
|
642
|
+
// Lambda FATALs. Harmless for local \`next dev\` / \`next start\`.
|
|
643
|
+
output: 'standalone',
|
|
489
644
|
typescript: { ignoreBuildErrors: true },
|
|
490
645
|
eslint: { ignoreDuringBuilds: true },
|
|
646
|
+
env: {
|
|
647
|
+
NEXT_PUBLIC_DASHWISE_API_URL:
|
|
648
|
+
process.env.NEXT_PUBLIC_DASHWISE_API_URL ?? apiUrl,
|
|
649
|
+
},
|
|
491
650
|
};
|
|
492
651
|
export default nextConfig;
|
|
493
652
|
`;
|
|
@@ -531,16 +690,18 @@ function gitignore() {
|
|
|
531
690
|
function envLocalExample(apiUrl = SCAFFOLD_DEFAULT_API_URL) {
|
|
532
691
|
return [
|
|
533
692
|
"# Copy to .env.local + fill in real values.",
|
|
534
|
-
"#
|
|
535
|
-
"# from ~/.config/dashwise/auth.json \u2014 you only need
|
|
536
|
-
"#
|
|
693
|
+
"# Phase 2 two-var contract: exactly these two vars. `dashwise dev`",
|
|
694
|
+
"# auto-injects both from ~/.config/dashwise/auth.json \u2014 you only need",
|
|
695
|
+
"# to edit this file if you point at a non-default backend or run",
|
|
696
|
+
"# `next dev` standalone.",
|
|
697
|
+
"#",
|
|
698
|
+
"# The browser needs the API URL too; next.config derives",
|
|
699
|
+
"# NEXT_PUBLIC_DASHWISE_API_URL from DASHWISE_API_URL, so you never set",
|
|
700
|
+
"# it yourself.",
|
|
537
701
|
`DASHWISE_API_URL=${apiUrl}`,
|
|
538
|
-
"#
|
|
539
|
-
"#
|
|
540
|
-
"
|
|
541
|
-
`NEXT_PUBLIC_DASHWISE_API_URL=${apiUrl}`,
|
|
542
|
-
"DASHWISE_API_TOKEN=",
|
|
543
|
-
"DASHWISE_INSTALLATION_ID=local",
|
|
702
|
+
"# Scoped `dwk_` API key \u2014 mint with `dashwise api-keys create <name>`",
|
|
703
|
+
"# for deploys; `dashwise dev` injects the local-dev key automatically.",
|
|
704
|
+
"DASHWISE_API_KEY=",
|
|
544
705
|
""
|
|
545
706
|
].join("\n");
|
|
546
707
|
}
|
|
@@ -548,24 +709,20 @@ function envLocal(apiUrl = SCAFFOLD_DEFAULT_API_URL) {
|
|
|
548
709
|
return [
|
|
549
710
|
"# Local dev config \u2014 auto-generated by `dashwise module init`.",
|
|
550
711
|
`# Backend URL pinned to your logged-in CLI config (${apiUrl}). Gitignored;`,
|
|
551
|
-
"# safe to edit. `dashwise dev` (the unified loop) auto-injects",
|
|
552
|
-
"#
|
|
553
|
-
"#
|
|
554
|
-
"#
|
|
555
|
-
"# (and NEXT_PUBLIC_DASHWISE_API_URL to match) to point at a different",
|
|
556
|
-
"# backend.",
|
|
712
|
+
"# safe to edit. `dashwise dev` (the unified loop) auto-injects the",
|
|
713
|
+
"# Phase 2 two-var contract \u2014 DASHWISE_API_URL + the per-module `dwk_`",
|
|
714
|
+
"# DASHWISE_API_KEY \u2014 from ~/.config/dashwise/auth.json, so you",
|
|
715
|
+
"# typically don't need to set either here.",
|
|
557
716
|
"#",
|
|
558
|
-
"#
|
|
559
|
-
"#
|
|
560
|
-
"#
|
|
717
|
+
"# The browser needs the API URL too; next.config derives",
|
|
718
|
+
"# NEXT_PUBLIC_DASHWISE_API_URL from DASHWISE_API_URL automatically.",
|
|
719
|
+
"# There is no DASHWISE_INSTALLATION_ID \u2014 the API key resolves the",
|
|
720
|
+
"# install server-side.",
|
|
561
721
|
`DASHWISE_API_URL=${apiUrl}`,
|
|
562
|
-
"#
|
|
563
|
-
"#
|
|
564
|
-
"#
|
|
565
|
-
"
|
|
566
|
-
`NEXT_PUBLIC_DASHWISE_API_URL=${apiUrl}`,
|
|
567
|
-
"DASHWISE_API_TOKEN=",
|
|
568
|
-
"DASHWISE_INSTALLATION_ID=",
|
|
722
|
+
"# Scoped `dwk_` key. Left blank; `dashwise dev` injects the local-dev",
|
|
723
|
+
"# key. For a standalone `next dev`, paste one from `dashwise api-keys",
|
|
724
|
+
"# create <name>`.",
|
|
725
|
+
"DASHWISE_API_KEY=",
|
|
569
726
|
""
|
|
570
727
|
].join("\n");
|
|
571
728
|
}
|
|
@@ -687,7 +844,7 @@ npm run dev:next # next dev on :3000 (terminal 2)
|
|
|
687
844
|
- **\`app/page.tsx\`** \u2014 public landing (allowlisted in \`middleware.ts\`).
|
|
688
845
|
- **\`app/dashboard/page.tsx\`** \u2014 Server Component requiring a valid session via \`getServerSession()\`.
|
|
689
846
|
- **\`app/api/auth/callback/route.ts\`** \u2014 exchanges the SSO one-time code for a session cookie (uses \`exchangeCode()\` from the SDK).
|
|
690
|
-
- **\`app/api/auth/sign-in/route.ts\`** \u2014 entry point that
|
|
847
|
+
- **\`app/api/auth/sign-in/route.ts\`** \u2014 one entry point that calls the backend's \`POST /api/module-api/sso/start-url\` (authenticated with your \`dwk_\` key) and 302s the browser to the returned \`redirect_url\`. Same path in dev + prod.
|
|
691
848
|
- **\`app/api/auth/sign-out/route.ts\`** \u2014 clears the session cookie + tells the backend to revoke.
|
|
692
849
|
|
|
693
850
|
## Imports
|
|
@@ -736,25 +893,23 @@ the canonical block-discovery flow.
|
|
|
736
893
|
|
|
737
894
|
## Deploying elsewhere (custom domain)
|
|
738
895
|
|
|
739
|
-
The scaffolded SSO flow
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
a bearer token) into local dev \u2014 so local development works regardless of
|
|
757
|
-
which path you pick for production.
|
|
896
|
+
The scaffolded SSO flow works on any domain. The \`/api/auth/sign-in\`
|
|
897
|
+
route sends its own public origin as the \`callback_url\` to the backend;
|
|
898
|
+
the backend accepts it as long as the origin is on your installation's
|
|
899
|
+
allowlist. Local dev (\`http://localhost:<port>\`) is allowlisted
|
|
900
|
+
automatically for \`kind='local'\` installs.
|
|
901
|
+
|
|
902
|
+
For a deployed custom domain (e.g. \`https://mymodule.example.com\`), add
|
|
903
|
+
it to the allowlist:
|
|
904
|
+
|
|
905
|
+
\`\`\`bash
|
|
906
|
+
dashwise origins add https://mymodule.example.com
|
|
907
|
+
dashwise origins list # confirm
|
|
908
|
+
\`\`\`
|
|
909
|
+
|
|
910
|
+
That's it \u2014 no middleware changes, no separate token scheme. The single
|
|
911
|
+
\`dwk_\` \`DASHWISE_API_KEY\` drives both the data plane and the SSO
|
|
912
|
+
start-url call.
|
|
758
913
|
`;
|
|
759
914
|
}
|
|
760
915
|
function middlewareTs() {
|
|
@@ -796,7 +951,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
796
951
|
enableSystem
|
|
797
952
|
disableTransitionOnChange
|
|
798
953
|
>
|
|
799
|
-
|
|
954
|
+
{/* sessionEndpoint points at the same-origin /api/auth/session
|
|
955
|
+
route so the host-scoped session cookie is actually sent \u2014
|
|
956
|
+
a cross-origin fetch to the API would drop it (D8). */}
|
|
957
|
+
<AuthProvider sessionEndpoint="/api/auth/session">{children}</AuthProvider>
|
|
800
958
|
</ThemeProvider>
|
|
801
959
|
</body>
|
|
802
960
|
</html>
|
|
@@ -1001,73 +1159,102 @@ function Row({ label, value }: { label: string; value: string }) {
|
|
|
1001
1159
|
`;
|
|
1002
1160
|
}
|
|
1003
1161
|
function appApiAuthSignInTs() {
|
|
1004
|
-
return `// /api/auth/sign-in \u2014 kick off the SSO flow.
|
|
1005
|
-
//
|
|
1006
|
-
// Reads \`?return_to=\` from the caller (defaults to '/'). Branches
|
|
1007
|
-
// on env:
|
|
1162
|
+
return `// /api/auth/sign-in \u2014 kick off the SSO flow (one path, dev + prod).
|
|
1008
1163
|
//
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
// installed in a workspace yet.
|
|
1164
|
+
// Reads \`?return_to=\` from the caller (defaults to '/'), then asks the
|
|
1165
|
+
// DashWise backend for a redirect URL by calling the module-API
|
|
1166
|
+
// server-side start endpoint:
|
|
1013
1167
|
//
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1168
|
+
// POST /api/module-api/sso/start-url
|
|
1169
|
+
// Authorization: Bearer <DASHWISE_API_KEY> (the module's dwk_ key)
|
|
1170
|
+
// { "callback_url": "<this module>/api/auth/callback?return_to=..." }
|
|
1016
1171
|
//
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1172
|
+
// The backend resolves the install from the key (no installation id in
|
|
1173
|
+
// the request), validates the callback_url origin against the install's
|
|
1174
|
+
// allowlist (kind='local' installs implicitly allow localhost), and
|
|
1175
|
+
// returns { redirect_url, expires_in_seconds }. We 302 the browser to
|
|
1176
|
+
// redirect_url, which runs the existing main-app SSO flow and ends in a
|
|
1177
|
+
// 302 back to callback_url?code=<one-time>. The /api/auth/callback route
|
|
1178
|
+
// exchanges that code for a session cookie.
|
|
1019
1179
|
//
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
// need to handle the "user not logged in" case here.
|
|
1180
|
+
// No dev-vs-prod branching, no installation id, no dev-session id \u2014 the
|
|
1181
|
+
// single dwk_ key drives both local dev and production.
|
|
1023
1182
|
import { NextResponse } from 'next/server';
|
|
1024
1183
|
import { getPublicOrigin } from '@/lib/request-origin';
|
|
1025
1184
|
|
|
1026
|
-
export function GET(request: Request) {
|
|
1185
|
+
export async function GET(request: Request) {
|
|
1027
1186
|
const url = new URL(request.url);
|
|
1028
1187
|
const returnTo = url.searchParams.get('return_to') ?? '/';
|
|
1029
1188
|
const apiBaseUrl = process.env.DASHWISE_API_URL;
|
|
1030
|
-
const
|
|
1031
|
-
const devSessionId = process.env.DASHWISE_DEV_SESSION_ID;
|
|
1189
|
+
const apiKey = process.env.DASHWISE_API_KEY;
|
|
1032
1190
|
|
|
1033
1191
|
if (!apiBaseUrl) {
|
|
1034
|
-
return new NextResponse(
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
);
|
|
1192
|
+
return new NextResponse('DASHWISE_API_URL must be set in the environment', {
|
|
1193
|
+
status: 500,
|
|
1194
|
+
});
|
|
1038
1195
|
}
|
|
1039
|
-
if (!
|
|
1196
|
+
if (!apiKey) {
|
|
1040
1197
|
return new NextResponse(
|
|
1041
|
-
'
|
|
1042
|
-
'(local
|
|
1043
|
-
'a dev session, or set DASHWISE_INSTALLATION_ID after publishing your module.',
|
|
1198
|
+
'DASHWISE_API_KEY must be set in the environment. Run \`dashwise dev\` ' +
|
|
1199
|
+
'(local) or paste a key from \`dashwise api-keys create\` (deployed).',
|
|
1044
1200
|
{ status: 500 },
|
|
1045
1201
|
);
|
|
1046
1202
|
}
|
|
1047
1203
|
|
|
1048
|
-
// Absolute URL
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1204
|
+
// Absolute callback URL so the backend knows which host to bounce back
|
|
1205
|
+
// to. \`getPublicOrigin\` reads \`X-Forwarded-Host\` when present so the
|
|
1206
|
+
// URL points at the public domain, not the internal Node origin behind
|
|
1207
|
+
// reverse proxies (Railway/Vercel/Fly). The callback path is fixed at
|
|
1208
|
+
// /api/auth/callback by SDK convention; we tuck \`return_to\` onto it so
|
|
1209
|
+
// the callback route can resume the user's original destination.
|
|
1053
1210
|
const moduleOrigin = getPublicOrigin(request);
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1211
|
+
const callback = new URL('/api/auth/callback', moduleOrigin);
|
|
1212
|
+
callback.searchParams.set('return_to', returnTo);
|
|
1213
|
+
|
|
1214
|
+
let res: Response;
|
|
1215
|
+
try {
|
|
1216
|
+
res = await fetch(\`\${apiBaseUrl}/api/module-api/sso/start-url\`, {
|
|
1217
|
+
method: 'POST',
|
|
1218
|
+
headers: {
|
|
1219
|
+
'Content-Type': 'application/json',
|
|
1220
|
+
Accept: 'application/json',
|
|
1221
|
+
Authorization: \`Bearer \${apiKey}\`,
|
|
1222
|
+
},
|
|
1223
|
+
body: JSON.stringify({ callback_url: callback.toString() }),
|
|
1224
|
+
});
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
const target = new URL('/sign-in-error', moduleOrigin);
|
|
1227
|
+
target.searchParams.set('code', 'server_error');
|
|
1228
|
+
target.searchParams.set('description', (err as Error).message);
|
|
1229
|
+
return NextResponse.redirect(target);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (!res.ok) {
|
|
1233
|
+
// The backend rejected the start (bad key, callback origin not
|
|
1234
|
+
// allowlisted, install missing). Surface a real error page instead
|
|
1235
|
+
// of a raw 4xx.
|
|
1236
|
+
const body = (await res.json().catch(() => null)) as
|
|
1237
|
+
| { code?: string; message?: string }
|
|
1238
|
+
| null;
|
|
1239
|
+
const target = new URL('/sign-in-error', moduleOrigin);
|
|
1240
|
+
target.searchParams.set('code', body?.code ?? 'invalid_request');
|
|
1241
|
+
if (body?.message) target.searchParams.set('description', body.message);
|
|
1242
|
+
return NextResponse.redirect(target);
|
|
1069
1243
|
}
|
|
1070
|
-
|
|
1244
|
+
|
|
1245
|
+
const body = (await res.json().catch(() => null)) as
|
|
1246
|
+
| { redirect_url?: string }
|
|
1247
|
+
| null;
|
|
1248
|
+
if (!body?.redirect_url) {
|
|
1249
|
+
const target = new URL('/sign-in-error', moduleOrigin);
|
|
1250
|
+
target.searchParams.set('code', 'server_error');
|
|
1251
|
+
target.searchParams.set(
|
|
1252
|
+
'description',
|
|
1253
|
+
'start-url response missing redirect_url',
|
|
1254
|
+
);
|
|
1255
|
+
return NextResponse.redirect(target);
|
|
1256
|
+
}
|
|
1257
|
+
return NextResponse.redirect(body.redirect_url);
|
|
1071
1258
|
}
|
|
1072
1259
|
`;
|
|
1073
1260
|
}
|
|
@@ -1139,6 +1326,65 @@ export async function POST(request: Request) {
|
|
|
1139
1326
|
}
|
|
1140
1327
|
`;
|
|
1141
1328
|
}
|
|
1329
|
+
function appApiItemsRouteTs() {
|
|
1330
|
+
return `// Example data-plane route \u2014 lists + creates rows in the seeded
|
|
1331
|
+
// \`items\` table via the generated typed SDK. Delete this (and the
|
|
1332
|
+
// \`items\` table in module.json) once you add your own tables.
|
|
1333
|
+
//
|
|
1334
|
+
// \`db\` comes from @dashai/generated, regenerated on every module.json
|
|
1335
|
+
// save by \`dashwise dev\`. Run it (or \`dashwise module generate\`) first.
|
|
1336
|
+
import { NextResponse } from 'next/server';
|
|
1337
|
+
import { db } from '@dashai/generated';
|
|
1338
|
+
|
|
1339
|
+
export async function GET() {
|
|
1340
|
+
const page = await db.items.list({ limit: 50 });
|
|
1341
|
+
return NextResponse.json(page.rows);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
export async function POST(request: Request) {
|
|
1345
|
+
const body = (await request.json().catch(() => null)) as
|
|
1346
|
+
| { title?: unknown; done?: unknown }
|
|
1347
|
+
| null;
|
|
1348
|
+
const title = typeof body?.title === 'string' ? body.title : '';
|
|
1349
|
+
if (!title) {
|
|
1350
|
+
return NextResponse.json(
|
|
1351
|
+
{ error: 'title is required (string)' },
|
|
1352
|
+
{ status: 400 },
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
const done = typeof body?.done === 'boolean' ? body.done : false;
|
|
1356
|
+
const row = await db.items.create({ title, done });
|
|
1357
|
+
return NextResponse.json(row, { status: 201 });
|
|
1358
|
+
}
|
|
1359
|
+
`;
|
|
1360
|
+
}
|
|
1361
|
+
function appApiAuthSessionTs() {
|
|
1362
|
+
return `// /api/auth/session \u2014 SAME-ORIGIN session probe for client components.
|
|
1363
|
+
//
|
|
1364
|
+
// D8: the browser's <AuthProvider> can't read the session by calling the
|
|
1365
|
+
// cross-origin DashWise API directly, because the \`dashwise.session\`
|
|
1366
|
+
// cookie is host-scoped to THIS module's origin and is never sent to
|
|
1367
|
+
// \`api.dashwise.net\`. So <AuthProvider sessionEndpoint="/api/auth/session">
|
|
1368
|
+
// hits this route (same origin \u2192 the cookie IS sent); we validate it
|
|
1369
|
+
// server-side via \`getServerSession()\` (which forwards the cookie to the
|
|
1370
|
+
// API for us) and return the fully-enriched session \u2014 or \`null\` \u2014 as JSON.
|
|
1371
|
+
//
|
|
1372
|
+
// \`getServerSession()\` already merges the RBAC slice, so the client
|
|
1373
|
+
// receives \`rbacRoles\` / \`rbacPermissions\` and skips a second fetch.
|
|
1374
|
+
import { NextResponse } from 'next/server';
|
|
1375
|
+
import { getServerSession } from '@dashai/sdk/auth/server';
|
|
1376
|
+
|
|
1377
|
+
export async function GET() {
|
|
1378
|
+
const session = await getServerSession();
|
|
1379
|
+
// Always 200 \u2014 \`null\` is a valid "not signed in" answer the client
|
|
1380
|
+
// maps to \`unauthenticated\`. \`no-store\` so a stale signed-in body is
|
|
1381
|
+
// never served to a signed-out user after sign-out.
|
|
1382
|
+
return NextResponse.json(session, {
|
|
1383
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
`;
|
|
1387
|
+
}
|
|
1142
1388
|
function packageJsonCustom(opts) {
|
|
1143
1389
|
return JSON.stringify(
|
|
1144
1390
|
{
|
|
@@ -1154,7 +1400,7 @@ function packageJsonCustom(opts) {
|
|
|
1154
1400
|
start: "next start"
|
|
1155
1401
|
},
|
|
1156
1402
|
dependencies: {
|
|
1157
|
-
"@dashai/sdk": "^0.
|
|
1403
|
+
"@dashai/sdk": "^2.0.0",
|
|
1158
1404
|
next: "^15.0.0",
|
|
1159
1405
|
react: "^19.0.0",
|
|
1160
1406
|
"react-dom": "^19.0.0",
|
|
@@ -1504,7 +1750,7 @@ const ERROR_MESSAGES: Record<string, string> = {
|
|
|
1504
1750
|
server_error:
|
|
1505
1751
|
'The DashWise sign-in server returned an internal error. This is usually transient \u2014 try again in a moment.',
|
|
1506
1752
|
invalid_request:
|
|
1507
|
-
'The sign-in request was malformed. This is almost always a configuration bug \u2014 check that DASHWISE_API_URL and
|
|
1753
|
+
'The sign-in request was malformed. This is almost always a configuration bug \u2014 check that DASHWISE_API_URL and DASHWISE_API_KEY are set correctly and the callback origin is allowlisted (run "dashwise origins add <url>").',
|
|
1508
1754
|
missing_code:
|
|
1509
1755
|
'The callback URL was missing the expected exchange code. This usually means the SSO flow was interrupted.',
|
|
1510
1756
|
exchange_failed:
|
|
@@ -1577,7 +1823,14 @@ function escapeHtml(s) {
|
|
|
1577
1823
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1578
1824
|
}
|
|
1579
1825
|
function isValidSlug(slug) {
|
|
1580
|
-
return
|
|
1826
|
+
return MODULE_SLUG_REGEX.test(slug);
|
|
1827
|
+
}
|
|
1828
|
+
function slugValidationMessage(slug) {
|
|
1829
|
+
if (slug.includes("_")) {
|
|
1830
|
+
const suggestion = slug.replace(/_/g, "-").replace(/^-+|-+$/g, "");
|
|
1831
|
+
return `Slug "${slug}" uses underscores \u2014 module slugs must be kebab-case (letters, digits, hyphens; e.g. "${suggestion || "my-module"}"). Rename it and retry.`;
|
|
1832
|
+
}
|
|
1833
|
+
return `Slug "${slug}" must be kebab-case \u2014 match /^[a-z][a-z0-9-]*[a-z0-9]$/ (a leading letter, then letters/digits/hyphens, no trailing hyphen).`;
|
|
1581
1834
|
}
|
|
1582
1835
|
function claudeMd(opts) {
|
|
1583
1836
|
const isCustom = (opts.kind ?? "hand_authored") === "custom";
|
|
@@ -1660,17 +1913,20 @@ await db.<table>.create({ /* fields */ });
|
|
|
1660
1913
|
const stats = await qb.from('<table>').where({ /* ... */ }).execute();
|
|
1661
1914
|
\`\`\`
|
|
1662
1915
|
|
|
1663
|
-
The data plane authenticates via the per-module
|
|
1664
|
-
in \`~/.config/dashwise/auth.json#modules[${opts.slug}]\` after
|
|
1665
|
-
\`dashwise init\` provisions the dev install. \`dashwise dev\` injects
|
|
1666
|
-
|
|
1916
|
+
The data plane authenticates via the per-module scoped \`dwk_\` API key
|
|
1917
|
+
cached in \`~/.config/dashwise/auth.json#modules[${opts.slug}]\` after
|
|
1918
|
+
\`dashwise init\` provisions the dev install. \`dashwise dev\` injects it
|
|
1919
|
+
as \`DASHWISE_API_KEY\` into the Next.js child env automatically. The
|
|
1920
|
+
key binds the install server-side \u2014 there is no \`DASHWISE_INSTALLATION_ID\`.
|
|
1667
1921
|
|
|
1668
1922
|
## Auth in dev mode
|
|
1669
1923
|
|
|
1670
|
-
\`dashwise dev\` registers a dev-mode SSO session with the backend
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
\`
|
|
1924
|
+
\`dashwise dev\` registers a dev-mode SSO session with the backend so
|
|
1925
|
+
workspace members can sign into the locally-running module via DashWise
|
|
1926
|
+
SSO. The scaffold's \`/api/auth/sign-in\` route is a single path: it
|
|
1927
|
+
calls \`POST /api/module-api/sso/start-url\` with your \`dwk_\` key and
|
|
1928
|
+
follows the returned \`redirect_url\` \u2014 the same code runs in dev and in
|
|
1929
|
+
production, with no installation id or dev-session id in the request.
|
|
1674
1930
|
|
|
1675
1931
|
In Server Components: \`await getServerSession()\` (from
|
|
1676
1932
|
\`@dashai/sdk/auth/server\`).
|
|
@@ -1727,13 +1983,14 @@ what tables it owns.
|
|
|
1727
1983
|
|
|
1728
1984
|
| Table | Columns | Notes |
|
|
1729
1985
|
|---|---|---|
|
|
1730
|
-
|
|
|
1986
|
+
| \`items\` | \`title:text\`, \`done:boolean\` | seeded example \u2014 edit or replace; \`dashwise table create <slug> --field <slug:type>\` adds more |
|
|
1731
1987
|
|
|
1732
1988
|
## API / contract
|
|
1733
1989
|
|
|
1734
1990
|
| Method | Path | Auth | Purpose |
|
|
1735
1991
|
|---|---|---|---|
|
|
1736
|
-
|
|
|
1992
|
+
| GET | \`/api/items\` | dwk_ (server-side) | list \`items\` rows via \`db.items.list()\` |
|
|
1993
|
+
| POST | \`/api/items\` | dwk_ (server-side) | create an \`items\` row via \`db.items.create()\` |
|
|
1737
1994
|
|
|
1738
1995
|
## Business rules
|
|
1739
1996
|
|
|
@@ -1849,11 +2106,12 @@ primary_region = "iad"
|
|
|
1849
2106
|
memory_mb = 256
|
|
1850
2107
|
`;
|
|
1851
2108
|
}
|
|
1852
|
-
var DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL;
|
|
2109
|
+
var DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL, MODULE_SLUG_REGEX;
|
|
1853
2110
|
var init_scaffold = __esm({
|
|
1854
2111
|
"src/lib/scaffold.ts"() {
|
|
1855
2112
|
DEPLOY_TARGETS = ["railway", "vercel", "fly"];
|
|
1856
2113
|
SCAFFOLD_DEFAULT_API_URL = "http://localhost:3000";
|
|
2114
|
+
MODULE_SLUG_REGEX = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
|
|
1857
2115
|
}
|
|
1858
2116
|
});
|
|
1859
2117
|
async function selectWorkspace(opts) {
|
|
@@ -1921,7 +2179,7 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
1921
2179
|
const answer = await text({
|
|
1922
2180
|
message: "Module slug?",
|
|
1923
2181
|
placeholder: "tasks",
|
|
1924
|
-
validate: (v) => isValidSlug(v) ? void 0 :
|
|
2182
|
+
validate: (v) => isValidSlug(v) ? void 0 : slugValidationMessage(v)
|
|
1925
2183
|
});
|
|
1926
2184
|
if (isCancel(answer)) {
|
|
1927
2185
|
fail("Aborted.");
|
|
@@ -1934,7 +2192,7 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
1934
2192
|
return 1;
|
|
1935
2193
|
}
|
|
1936
2194
|
if (!isValidSlug(slug)) {
|
|
1937
|
-
fail(
|
|
2195
|
+
fail(slugValidationMessage(slug));
|
|
1938
2196
|
return 1;
|
|
1939
2197
|
}
|
|
1940
2198
|
const name = options.name ?? humanizeSlug(slug);
|
|
@@ -1982,7 +2240,11 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
1982
2240
|
// discipline + SDK pointers loaded into context from byte one.
|
|
1983
2241
|
["CLAUDE.md", claudeMd(scaffoldOpts)],
|
|
1984
2242
|
["spec.md", specMd(scaffoldOpts)],
|
|
1985
|
-
["agent-log.md", agentLogMd(scaffoldOpts)]
|
|
2243
|
+
["agent-log.md", agentLogMd(scaffoldOpts)],
|
|
2244
|
+
// Example data-plane route (both flavors) — exercises the generated
|
|
2245
|
+
// SDK against the seeded `items` table. Ships for simple + custom
|
|
2246
|
+
// since both produce `@dashai/generated`.
|
|
2247
|
+
["app/api/items/route.ts", appApiItemsRouteTs()]
|
|
1986
2248
|
];
|
|
1987
2249
|
if (kind === "custom") {
|
|
1988
2250
|
files.push(
|
|
@@ -2002,7 +2264,10 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2002
2264
|
["app/sign-in-error/page.tsx", appSignInErrorPageTsx()],
|
|
2003
2265
|
["app/api/auth/sign-in/route.ts", appApiAuthSignInTs()],
|
|
2004
2266
|
["app/api/auth/callback/route.ts", appApiAuthCallbackTs()],
|
|
2005
|
-
["app/api/auth/sign-out/route.ts", appApiAuthSignOutTs()]
|
|
2267
|
+
["app/api/auth/sign-out/route.ts", appApiAuthSignOutTs()],
|
|
2268
|
+
// D8: same-origin session probe that <AuthProvider> hits so the
|
|
2269
|
+
// host-scoped session cookie is actually sent.
|
|
2270
|
+
["app/api/auth/session/route.ts", appApiAuthSessionTs()]
|
|
2006
2271
|
);
|
|
2007
2272
|
} else {
|
|
2008
2273
|
files.push(
|
|
@@ -2061,7 +2326,7 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2061
2326
|
${dim(" dashwise module dev")} ${dim("# watch + auto-regenerate")}
|
|
2062
2327
|
${dim(" npm run dev:next # next dev on :3000)")}
|
|
2063
2328
|
${devInstall ? `
|
|
2064
|
-
${dim(`Dev installation id=${devInstall.installation.id};
|
|
2329
|
+
${dim(`Dev installation id=${devInstall.installation.id}; API key expires ${devInstall.api_key_expires_at}.`)}
|
|
2065
2330
|
` : ""}`);
|
|
2066
2331
|
outro("Happy authoring.");
|
|
2067
2332
|
return 0;
|
|
@@ -2115,20 +2380,21 @@ async function tryProvisionDevInstall(args) {
|
|
|
2115
2380
|
}
|
|
2116
2381
|
try {
|
|
2117
2382
|
upsertModuleEntry(args.slug, {
|
|
2118
|
-
|
|
2383
|
+
apiKey: response.api_key,
|
|
2384
|
+
apiKeyId: response.api_key_id,
|
|
2119
2385
|
installationId: response.installation.id,
|
|
2120
|
-
expiresAt: response.
|
|
2386
|
+
expiresAt: response.api_key_expires_at
|
|
2121
2387
|
});
|
|
2122
2388
|
} catch (err) {
|
|
2123
2389
|
warn(
|
|
2124
|
-
`Provisioned successfully (installation id=${response.installation.id}) but failed to cache the
|
|
2125
|
-
|
|
2126
|
-
|
|
2390
|
+
`Provisioned successfully (installation id=${response.installation.id}) but failed to cache the API key: ${err.message}
|
|
2391
|
+
API key (save this manually): ${response.api_key}
|
|
2392
|
+
Key expires: ${response.api_key_expires_at}`
|
|
2127
2393
|
);
|
|
2128
2394
|
return response;
|
|
2129
2395
|
}
|
|
2130
2396
|
success(
|
|
2131
|
-
`Provisioned dev install id=${response.installation.id}.
|
|
2397
|
+
`Provisioned dev install id=${response.installation.id}. API key cached in your CLI config.`
|
|
2132
2398
|
);
|
|
2133
2399
|
return response;
|
|
2134
2400
|
}
|
|
@@ -2197,6 +2463,7 @@ function loadSchemaCommandContext(opts) {
|
|
|
2197
2463
|
`No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
|
|
2198
2464
|
);
|
|
2199
2465
|
}
|
|
2466
|
+
const token = resolveSchemaAuthToken(entry, slug);
|
|
2200
2467
|
return {
|
|
2201
2468
|
projectRoot,
|
|
2202
2469
|
manifestPath: manifestPath2,
|
|
@@ -2207,10 +2474,16 @@ function loadSchemaCommandContext(opts) {
|
|
|
2207
2474
|
entry,
|
|
2208
2475
|
requestOptions: {
|
|
2209
2476
|
baseUrl: resolveApiUrl(),
|
|
2210
|
-
auth: { token
|
|
2477
|
+
auth: { token }
|
|
2211
2478
|
}
|
|
2212
2479
|
};
|
|
2213
2480
|
}
|
|
2481
|
+
function resolveSchemaAuthToken(entry, slug) {
|
|
2482
|
+
if (entry.apiKey) return entry.apiKey;
|
|
2483
|
+
throw new Error(
|
|
2484
|
+
`The cached credential for "${slug}" is a legacy runtime token, which the schema endpoints no longer accept. Run \`dashwise dev\` once (it mints a dwk_ API key and updates your config), or \`dashwise init ${slug} --force\` to re-provision.`
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2214
2487
|
var init_schema_command_context = __esm({
|
|
2215
2488
|
"src/lib/schema-command-context.ts"() {
|
|
2216
2489
|
init_config();
|
|
@@ -2549,7 +2822,6 @@ async function tableCreateCommand(slug, options) {
|
|
|
2549
2822
|
info(`Creating table ${code(slug)} in installation ${ctx.entry.installationId}\u2026`);
|
|
2550
2823
|
try {
|
|
2551
2824
|
const result = await createTable(
|
|
2552
|
-
ctx.entry.installationId,
|
|
2553
2825
|
{ slug, fields },
|
|
2554
2826
|
ctx.requestOptions
|
|
2555
2827
|
);
|
|
@@ -2563,10 +2835,7 @@ async function tableCreateCommand(slug, options) {
|
|
|
2563
2835
|
}
|
|
2564
2836
|
async function syncLocalManifest(ctx) {
|
|
2565
2837
|
try {
|
|
2566
|
-
const { manifest } = await getInstallationSchema(
|
|
2567
|
-
ctx.entry.installationId,
|
|
2568
|
-
ctx.requestOptions
|
|
2569
|
-
);
|
|
2838
|
+
const { manifest } = await getInstallationSchema(ctx.requestOptions);
|
|
2570
2839
|
writeManifest(manifest, ctx.projectRoot);
|
|
2571
2840
|
return 0;
|
|
2572
2841
|
} catch (err) {
|
|
@@ -2599,17 +2868,17 @@ function surfaceFailure(err) {
|
|
|
2599
2868
|
break;
|
|
2600
2869
|
case "UNAUTHENTICATED":
|
|
2601
2870
|
fail(
|
|
2602
|
-
`
|
|
2871
|
+
`API key rejected by backend. The cached key may be expired or revoked \u2014 re-run \`dashwise init <slug> --force\` to mint a fresh one.`
|
|
2603
2872
|
);
|
|
2604
2873
|
break;
|
|
2605
|
-
case "
|
|
2874
|
+
case "SCHEMA_LOCKED":
|
|
2606
2875
|
fail(
|
|
2607
|
-
`Schema
|
|
2876
|
+
`Schema mutations require a local-dev install (kind=local). The cached API key belongs to a non-local install \u2014 schema is locked. Run \`dashwise init <slug> --force\` for a local dev install.`
|
|
2608
2877
|
);
|
|
2609
2878
|
break;
|
|
2610
2879
|
case "INSTALL_MISMATCH":
|
|
2611
2880
|
fail(
|
|
2612
|
-
`
|
|
2881
|
+
`API key is bound to a different installation than the URL. The cached install id may be stale \u2014 re-run \`dashwise init <slug> --force\`.`
|
|
2613
2882
|
);
|
|
2614
2883
|
break;
|
|
2615
2884
|
default:
|
|
@@ -2651,11 +2920,7 @@ async function tableDeleteCommand(slug, options) {
|
|
|
2651
2920
|
}
|
|
2652
2921
|
info(`Dropping table ${code(slug)} from installation ${ctx.entry.installationId}\u2026`);
|
|
2653
2922
|
try {
|
|
2654
|
-
const result = await dropTable(
|
|
2655
|
-
ctx.entry.installationId,
|
|
2656
|
-
slug,
|
|
2657
|
-
ctx.requestOptions
|
|
2658
|
-
);
|
|
2923
|
+
const result = await dropTable(slug, ctx.requestOptions);
|
|
2659
2924
|
success(
|
|
2660
2925
|
`Dropped table ${code(slug)} (manifest_version=${result.manifest_version}). Physical table soft-deleted; recoverable for 30 days via the dashboard's trash.`
|
|
2661
2926
|
);
|
|
@@ -2723,7 +2988,6 @@ async function fieldAddCommand(tableSlug, fieldSpec, options) {
|
|
|
2723
2988
|
);
|
|
2724
2989
|
try {
|
|
2725
2990
|
const result = await addField(
|
|
2726
|
-
ctx.entry.installationId,
|
|
2727
2991
|
{ table_slug: tableSlug, field },
|
|
2728
2992
|
ctx.requestOptions
|
|
2729
2993
|
);
|
|
@@ -2769,7 +3033,6 @@ async function fieldRemoveCommand(tableSlug, fieldSlug, options) {
|
|
|
2769
3033
|
);
|
|
2770
3034
|
try {
|
|
2771
3035
|
const result = await removeField(
|
|
2772
|
-
ctx.entry.installationId,
|
|
2773
3036
|
tableSlug,
|
|
2774
3037
|
fieldSlug,
|
|
2775
3038
|
ctx.requestOptions
|
|
@@ -2823,7 +3086,6 @@ async function fieldSetTypeCommand(tableSlug, fieldSlug, newType, options) {
|
|
|
2823
3086
|
);
|
|
2824
3087
|
try {
|
|
2825
3088
|
const result = await updateField(
|
|
2826
|
-
ctx.entry.installationId,
|
|
2827
3089
|
tableSlug,
|
|
2828
3090
|
fieldSlug,
|
|
2829
3091
|
{ type: newType },
|
|
@@ -2849,6 +3111,77 @@ var init_set_type = __esm({
|
|
|
2849
3111
|
|
|
2850
3112
|
// src/commands/dev.ts
|
|
2851
3113
|
init_api_client();
|
|
3114
|
+
|
|
3115
|
+
// src/lib/connect-status.ts
|
|
3116
|
+
init_api_client();
|
|
3117
|
+
init_output();
|
|
3118
|
+
async function reportConnectStatus(opts) {
|
|
3119
|
+
const [deps, bindings] = await Promise.all([
|
|
3120
|
+
safeGet(
|
|
3121
|
+
() => getInstallationDependencies(opts.installationId, opts.requestOptions)
|
|
3122
|
+
),
|
|
3123
|
+
safeGet(
|
|
3124
|
+
() => getInstallationBindings(opts.installationId, opts.requestOptions)
|
|
3125
|
+
)
|
|
3126
|
+
]);
|
|
3127
|
+
if (deps === null && bindings === null) return;
|
|
3128
|
+
const depList = deps ?? [];
|
|
3129
|
+
const bindList = bindings ?? [];
|
|
3130
|
+
if (depList.length === 0 && bindList.length === 0) return;
|
|
3131
|
+
const unboundDeps = depList.filter((d) => d.status === "unbound").length;
|
|
3132
|
+
const unboundUses = bindList.filter((b) => b.status === "unbound").length;
|
|
3133
|
+
info(
|
|
3134
|
+
`Connect status for ${code(opts.moduleSlug)} ${dim(
|
|
3135
|
+
`(${depList.length} dep${depList.length === 1 ? "" : "s"}, ${bindList.length} uses slot${bindList.length === 1 ? "" : "s"})`
|
|
3136
|
+
)}:`
|
|
3137
|
+
);
|
|
3138
|
+
for (const d of depList) renderDep(d);
|
|
3139
|
+
for (const b of bindList) renderBinding(b);
|
|
3140
|
+
const hints = [];
|
|
3141
|
+
if (unboundDeps > 0) {
|
|
3142
|
+
hints.push(
|
|
3143
|
+
`${code("dashwise deps bind <provider-slug>")} once the provider is installed`
|
|
3144
|
+
);
|
|
3145
|
+
}
|
|
3146
|
+
if (unboundUses > 0) {
|
|
3147
|
+
hints.push(`${code("dashwise bind <uses-slug> --table <name>")} to connect a table`);
|
|
3148
|
+
}
|
|
3149
|
+
if (hints.length > 0) {
|
|
3150
|
+
info(`Connect parked items with ${hints.join("; ")}.`);
|
|
3151
|
+
}
|
|
3152
|
+
out("");
|
|
3153
|
+
}
|
|
3154
|
+
function renderDep(d) {
|
|
3155
|
+
if (d.status === "resolved") {
|
|
3156
|
+
const providerLine = d.provider ? ` ${dim(`\u2192 install #${d.provider.installation_id} @ v${d.provider.version}`)}` : "";
|
|
3157
|
+
out(` ${pc.green("\u25CF resolved")} ${dim("dep")} ${code(d.slug)}@${d.range ?? "*"}${providerLine}`);
|
|
3158
|
+
} else {
|
|
3159
|
+
out(
|
|
3160
|
+
` ${pc.yellow("\u25CB unbound")} ${dim("dep")} ${code(d.slug)}@${d.range ?? "*"} ${dim(`reason: ${d.reason ?? "(unspecified)"}`)}`
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
function renderBinding(b) {
|
|
3165
|
+
if (b.status === "bound") {
|
|
3166
|
+
const tableLine = b.table ? ` ${dim(`\u2192 ${b.table.name} (id ${b.table.id})`)}` : "";
|
|
3167
|
+
out(` ${pc.green("\u25CF bound")} ${dim("uses")} ${code(b.uses_slug)}${tableLine}`);
|
|
3168
|
+
} else if (b.status === "declined") {
|
|
3169
|
+
out(` ${pc.dim("\u2298 declined")} ${dim("uses")} ${code(b.uses_slug)}`);
|
|
3170
|
+
} else {
|
|
3171
|
+
out(
|
|
3172
|
+
` ${pc.yellow("\u25CB unbound")} ${dim("uses")} ${code(b.uses_slug)} ${dim(`reason: ${b.reason ?? "(unspecified)"}`)}`
|
|
3173
|
+
);
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
async function safeGet(fn) {
|
|
3177
|
+
try {
|
|
3178
|
+
return await fn();
|
|
3179
|
+
} catch (err) {
|
|
3180
|
+
return null;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
// src/commands/dev.ts
|
|
2852
3185
|
init_config();
|
|
2853
3186
|
init_manifest_io();
|
|
2854
3187
|
|
|
@@ -2856,7 +3189,6 @@ init_manifest_io();
|
|
|
2856
3189
|
init_api_client();
|
|
2857
3190
|
function startManifestPollLoop(opts) {
|
|
2858
3191
|
const {
|
|
2859
|
-
installationId,
|
|
2860
3192
|
manifestPath: manifestPath2,
|
|
2861
3193
|
requestOptions,
|
|
2862
3194
|
intervalMs,
|
|
@@ -2869,10 +3201,7 @@ function startManifestPollLoop(opts) {
|
|
|
2869
3201
|
const tick = async () => {
|
|
2870
3202
|
if (stopped) return;
|
|
2871
3203
|
try {
|
|
2872
|
-
const { manifest } = await getInstallationSchema(
|
|
2873
|
-
installationId,
|
|
2874
|
-
requestOptions
|
|
2875
|
-
);
|
|
3204
|
+
const { manifest } = await getInstallationSchema(requestOptions);
|
|
2876
3205
|
const remoteJson = JSON.stringify(manifest, null, 2) + "\n";
|
|
2877
3206
|
const currentLocal = existsSync(manifestPath2) ? readFileSync(manifestPath2, "utf-8") : "";
|
|
2878
3207
|
if (remoteJson === currentLocal) {
|
|
@@ -2967,16 +3296,15 @@ async function devCommand(options) {
|
|
|
2967
3296
|
}
|
|
2968
3297
|
const explicitInstallationId = options.installationId ?? process.env.DASHWISE_INSTALLATION_ID;
|
|
2969
3298
|
const reuseDevSessionId = process.env.DASHWISE_DEV_SESSION_ID;
|
|
2970
|
-
|
|
2971
|
-
const
|
|
3299
|
+
let moduleEntry = manifestSlug !== void 0 ? readModuleEntry(manifestSlug) : null;
|
|
3300
|
+
const healed = manifestSlug !== void 0 ? await resolveModuleApiKey(manifestSlug, moduleEntry, apiUrl, apiToken) : null;
|
|
3301
|
+
if (healed) moduleEntry = healed;
|
|
3302
|
+
const apiKeyEnv = process.env.DASHWISE_API_KEY ?? moduleEntry?.apiKey;
|
|
2972
3303
|
let installationIdEnv;
|
|
2973
|
-
let devSessionIdEnv;
|
|
2974
3304
|
let revokeDevSession;
|
|
2975
3305
|
if (explicitInstallationId && explicitInstallationId !== "local") {
|
|
2976
3306
|
installationIdEnv = explicitInstallationId;
|
|
2977
|
-
} else if (reuseDevSessionId) {
|
|
2978
|
-
devSessionIdEnv = reuseDevSessionId;
|
|
2979
|
-
} else {
|
|
3307
|
+
} else if (reuseDevSessionId) ; else {
|
|
2980
3308
|
let workspaceContext;
|
|
2981
3309
|
try {
|
|
2982
3310
|
workspaceContext = await resolveTargetWorkspace(apiUrl, apiToken, options.workspace);
|
|
@@ -2990,7 +3318,6 @@ async function devCommand(options) {
|
|
|
2990
3318
|
target_workspace_id: workspaceContext.id,
|
|
2991
3319
|
callback_origins: callbackOrigins
|
|
2992
3320
|
});
|
|
2993
|
-
devSessionIdEnv = session.id;
|
|
2994
3321
|
revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session.id);
|
|
2995
3322
|
info(` ${dim("dev session")} ${session.id}`);
|
|
2996
3323
|
info(` ${dim("workspace")} ${workspaceContext.name} (id=${workspaceContext.id}, slug=${workspaceContext.slug})`);
|
|
@@ -3005,43 +3332,39 @@ async function devCommand(options) {
|
|
|
3005
3332
|
return 1;
|
|
3006
3333
|
}
|
|
3007
3334
|
}
|
|
3008
|
-
const childInstallationId = installationIdEnv ?? (runtimeTokenEnv !== void 0 ? "sandbox" : "");
|
|
3009
3335
|
const childEnv = {
|
|
3010
3336
|
...process.env,
|
|
3011
3337
|
DASHWISE_API_URL: apiUrl,
|
|
3012
|
-
// HH-CC-FUP-3: client-side mirror. Next.js inlines NEXT_PUBLIC_*
|
|
3013
|
-
// env vars into the browser bundle at build time (or, in dev, at
|
|
3014
|
-
// process-start time). Without this, the SDK's <AuthProvider> hits
|
|
3015
|
-
// `/api/auth/sessions/me` on its own origin (localhost:<port>),
|
|
3016
|
-
// 404s, and stays unauthenticated. We mirror DASHWISE_API_URL so
|
|
3017
|
-
// the client + server agree on the backend origin without the
|
|
3018
|
-
// module author having to touch .env.local.
|
|
3019
3338
|
NEXT_PUBLIC_DASHWISE_API_URL: apiUrl,
|
|
3020
|
-
|
|
3021
|
-
DASHWISE_INSTALLATION_ID: childInstallationId,
|
|
3022
|
-
...devSessionIdEnv !== void 0 ? { DASHWISE_DEV_SESSION_ID: devSessionIdEnv } : {},
|
|
3023
|
-
...runtimeTokenEnv !== void 0 ? { DASHWISE_RUNTIME_TOKEN: runtimeTokenEnv } : {}
|
|
3339
|
+
...apiKeyEnv !== void 0 ? { DASHWISE_API_KEY: apiKeyEnv } : {}
|
|
3024
3340
|
};
|
|
3025
3341
|
info(`Dev loop starting \u2014 Ctrl-C to stop.`);
|
|
3026
3342
|
info(` ${dim("api")} ${apiUrl}`);
|
|
3027
3343
|
if (installationIdEnv !== void 0) {
|
|
3028
3344
|
info(` ${dim("installation")} ${installationIdEnv} (prod-mode SSO)`);
|
|
3029
3345
|
} else {
|
|
3030
|
-
info(` ${dim("mode")} dev (
|
|
3346
|
+
info(` ${dim("mode")} dev (dev SSO session registered)`);
|
|
3031
3347
|
}
|
|
3032
|
-
info(` ${dim("token")}
|
|
3033
|
-
if (
|
|
3034
|
-
const source = process.env.
|
|
3348
|
+
info(` ${dim("cli token")} ${maskedToken(apiToken)} ${dim(`(from ${apiToken === savedConfig?.token ? "~/.config/dashwise/auth.json" : "$DASHWISE_API_TOKEN"})`)}`);
|
|
3349
|
+
if (apiKeyEnv !== void 0) {
|
|
3350
|
+
const source = process.env.DASHWISE_API_KEY === apiKeyEnv ? "$DASHWISE_API_KEY" : `~/.config/dashwise/auth.json#modules[${manifestSlug}]`;
|
|
3035
3351
|
const installId = moduleEntry?.installationId;
|
|
3036
3352
|
info(
|
|
3037
|
-
` ${dim("
|
|
3353
|
+
` ${dim("api key")} ${maskedToken(apiKeyEnv)} ${dim(`(from ${source}${installId ? `, install id=${installId}` : ""})`)}`
|
|
3038
3354
|
);
|
|
3039
3355
|
} else if (manifestSlug !== void 0 && installationIdEnv === void 0) {
|
|
3040
3356
|
warn(
|
|
3041
|
-
`No
|
|
3357
|
+
`No API key cached for module ${code(manifestSlug)}. Data-plane calls (db, qb) will fail with UnauthenticatedError. Run ${code(`dashwise init ${manifestSlug} --force`)} to provision a dev install + cache a key.`
|
|
3042
3358
|
);
|
|
3043
3359
|
}
|
|
3044
3360
|
info("");
|
|
3361
|
+
if (moduleEntry !== null && manifestSlug !== void 0) {
|
|
3362
|
+
await reportConnectStatus({
|
|
3363
|
+
installationId: moduleEntry.installationId,
|
|
3364
|
+
moduleSlug: manifestSlug,
|
|
3365
|
+
requestOptions: { baseUrl: apiUrl, auth: { token: apiToken } }
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3045
3368
|
const children = [];
|
|
3046
3369
|
if (!options.nextOnly) {
|
|
3047
3370
|
const sdkChild = spawnSdkWatcher(projectRoot, childEnv);
|
|
@@ -3056,18 +3379,19 @@ async function devCommand(options) {
|
|
|
3056
3379
|
pipeWithPrefix(child.stderr, process.stderr, prefix);
|
|
3057
3380
|
}
|
|
3058
3381
|
let manifestPoll = null;
|
|
3059
|
-
const
|
|
3060
|
-
|
|
3382
|
+
const pollEntry = moduleEntry;
|
|
3383
|
+
const enablePoll = !options.noRemotePoll && pollEntry !== null && apiKeyEnv !== void 0 && manifestSlug !== void 0;
|
|
3384
|
+
if (enablePoll && pollEntry !== null) {
|
|
3061
3385
|
const intervalSec = Math.max(
|
|
3062
3386
|
MIN_REMOTE_POLL_INTERVAL_SEC,
|
|
3063
3387
|
options.remotePollIntervalSec ?? DEFAULT_REMOTE_POLL_INTERVAL_SEC
|
|
3064
3388
|
);
|
|
3065
3389
|
manifestPoll = startManifestPollLoop({
|
|
3066
|
-
installationId: moduleEntry.installationId,
|
|
3067
3390
|
manifestPath: manifestPath2,
|
|
3068
3391
|
requestOptions: {
|
|
3069
3392
|
baseUrl: apiUrl,
|
|
3070
|
-
|
|
3393
|
+
// The `dwk_` key resolves the install server-side.
|
|
3394
|
+
auth: { token: apiKeyEnv }
|
|
3071
3395
|
},
|
|
3072
3396
|
intervalMs: intervalSec * 1e3,
|
|
3073
3397
|
onUpdate: (msg) => {
|
|
@@ -3200,13 +3524,58 @@ function maskedToken(token) {
|
|
|
3200
3524
|
const tail = token.slice(-4);
|
|
3201
3525
|
return `${prefix}\u2026${tail}`;
|
|
3202
3526
|
}
|
|
3203
|
-
function
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3527
|
+
async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
|
|
3528
|
+
if (process.env.DASHWISE_API_KEY) return null;
|
|
3529
|
+
if (!entry) return null;
|
|
3530
|
+
if (entry.apiKey) return null;
|
|
3531
|
+
if (!entry.runtimeToken) return null;
|
|
3532
|
+
info(
|
|
3533
|
+
`Upgrading cached credentials for ${code(slug)} to a ${code("dwk_")} API key (the local runtime token format was retired)\u2026`
|
|
3534
|
+
);
|
|
3535
|
+
let minted;
|
|
3536
|
+
try {
|
|
3537
|
+
minted = await createApiKey(
|
|
3538
|
+
entry.installationId,
|
|
3539
|
+
{ name: "local-dev" },
|
|
3540
|
+
{ baseUrl: apiUrl, auth: { token: cliBearer } }
|
|
3541
|
+
);
|
|
3542
|
+
} catch (err) {
|
|
3543
|
+
if (err instanceof ApiError && err.code === "INSTALLATION_NOT_FOUND") {
|
|
3544
|
+
warn(
|
|
3545
|
+
`Could not upgrade credentials for ${code(slug)}: its dev install no longer exists. Run ${code(`dashwise init ${slug} --force`)} to re-provision (a clean re-register).`
|
|
3546
|
+
);
|
|
3547
|
+
return null;
|
|
3548
|
+
}
|
|
3549
|
+
const reason = err instanceof ApiError ? `${err.code}: ${err.message}` : err.message;
|
|
3550
|
+
warn(
|
|
3551
|
+
`Could not upgrade credentials for ${code(slug)} (${reason}). Data-plane calls may fail; run ${code(`dashwise init ${slug} --force`)} to refresh.`
|
|
3552
|
+
);
|
|
3553
|
+
return null;
|
|
3554
|
+
}
|
|
3555
|
+
const upgraded = {
|
|
3556
|
+
apiKey: minted.raw_key,
|
|
3557
|
+
apiKeyId: minted.api_key.id,
|
|
3558
|
+
installationId: entry.installationId,
|
|
3559
|
+
expiresAt: minted.api_key.expires_at ?? entry.expiresAt
|
|
3560
|
+
};
|
|
3561
|
+
try {
|
|
3562
|
+
upsertModuleEntry(slug, upgraded);
|
|
3563
|
+
} catch (err) {
|
|
3564
|
+
warn(
|
|
3565
|
+
`Minted a new API key for ${code(slug)} but failed to cache it: ${err.message}
|
|
3566
|
+
API key (save manually): ${minted.raw_key}`
|
|
3567
|
+
);
|
|
3568
|
+
}
|
|
3569
|
+
success(`Upgraded ${code(slug)} to a dwk_ API key (id=${minted.api_key.id}).`);
|
|
3570
|
+
return upgraded;
|
|
3571
|
+
}
|
|
3572
|
+
function computeCallbackOrigins(port, extra) {
|
|
3573
|
+
const def = `http://localhost:${port}`;
|
|
3574
|
+
return Array.from(/* @__PURE__ */ new Set([def, ...extra]));
|
|
3575
|
+
}
|
|
3576
|
+
var PORT_PROBE_LIMIT = 10;
|
|
3577
|
+
async function resolveDevPort(requested) {
|
|
3578
|
+
if (requested !== void 0) {
|
|
3210
3579
|
if (await isPortFree(requested)) return requested;
|
|
3211
3580
|
const suggestion = await findFirstFreePort(requested + 1, PORT_PROBE_LIMIT);
|
|
3212
3581
|
const suggestionHint = suggestion !== null ? `Try \`dashwise dev --port ${suggestion}\`.` : `No free port found between ${requested + 1} and ${requested + PORT_PROBE_LIMIT} either \u2014 check what's listening with \`lsof -i :${requested}\`.`;
|
|
@@ -3887,7 +4256,7 @@ function isPlausibleRepoUrl(url) {
|
|
|
3887
4256
|
|
|
3888
4257
|
// src/commands/module/convert.ts
|
|
3889
4258
|
init_scaffold();
|
|
3890
|
-
var SDK_DEP_RANGE = "^0.
|
|
4259
|
+
var SDK_DEP_RANGE = "^2.0.0";
|
|
3891
4260
|
var SCAN_DIRS = ["app", "src", "pages", "lib", "components"];
|
|
3892
4261
|
var SCAN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
|
|
3893
4262
|
var MAX_SCAN_FILES = 2e3;
|
|
@@ -3943,6 +4312,7 @@ function convertModule(opts) {
|
|
|
3943
4312
|
writeIfMissing("app/api/auth/sign-in/route.ts", appApiAuthSignInTs());
|
|
3944
4313
|
writeIfMissing("app/api/auth/callback/route.ts", appApiAuthCallbackTs());
|
|
3945
4314
|
writeIfMissing("app/api/auth/sign-out/route.ts", appApiAuthSignOutTs());
|
|
4315
|
+
writeIfMissing("app/api/auth/session/route.ts", appApiAuthSessionTs());
|
|
3946
4316
|
writeIfMissing("app/sign-in-error/page.tsx", appSignInErrorPageTsx());
|
|
3947
4317
|
writeIfMissing(".env.local.example", envLocalExample());
|
|
3948
4318
|
const layoutFile = [
|
|
@@ -3954,7 +4324,7 @@ function convertModule(opts) {
|
|
|
3954
4324
|
writeIfMissing("app/layout.tsx", appLayoutCustomTsx(sopts));
|
|
3955
4325
|
} else if (!readFileSync(join(dir, layoutFile), "utf-8").includes("AuthProvider")) {
|
|
3956
4326
|
warnings.push(
|
|
3957
|
-
`${layoutFile}: wrap your tree in <AuthProvider> from "@dashai/sdk/auth/client" so client components can read the session.`
|
|
4327
|
+
`${layoutFile}: wrap your tree in <AuthProvider sessionEndpoint="/api/auth/session"> from "@dashai/sdk/auth/client" so client components can read the session (the same-origin session route was scaffolded for you).`
|
|
3958
4328
|
);
|
|
3959
4329
|
} else {
|
|
3960
4330
|
skipped.push(layoutFile);
|
|
@@ -4236,6 +4606,7 @@ async function moduleExportCommand(opts) {
|
|
|
4236
4606
|
init_config();
|
|
4237
4607
|
init_output();
|
|
4238
4608
|
init_manifest_io();
|
|
4609
|
+
init_scaffold();
|
|
4239
4610
|
init_workspaces();
|
|
4240
4611
|
init_api_client();
|
|
4241
4612
|
var MODULE_KINDS = ["ai_builder", "hand_authored", "custom"];
|
|
@@ -4258,6 +4629,13 @@ async function moduleRegisterCommand(opts) {
|
|
|
4258
4629
|
fail("module.json is missing `module.slug`.");
|
|
4259
4630
|
return 1;
|
|
4260
4631
|
}
|
|
4632
|
+
if (!isValidSlug(slug)) {
|
|
4633
|
+
fail(
|
|
4634
|
+
`${slugValidationMessage(slug)}
|
|
4635
|
+
Fix ${code("module.slug")} in module.json, then re-run ${code("dashwise module register")}.`
|
|
4636
|
+
);
|
|
4637
|
+
return 1;
|
|
4638
|
+
}
|
|
4261
4639
|
const name = opts.name ?? (typeof manifest.module?.name === "string" ? manifest.module.name : slug);
|
|
4262
4640
|
const moduleKind = MODULE_KINDS.includes(
|
|
4263
4641
|
manifest.module_kind
|
|
@@ -4295,7 +4673,12 @@ async function moduleRegisterCommand(opts) {
|
|
|
4295
4673
|
name,
|
|
4296
4674
|
...description !== void 0 ? { description } : {},
|
|
4297
4675
|
workspaceId: workspace2.id,
|
|
4298
|
-
...moduleKind !== void 0 ? { moduleKind } : {}
|
|
4676
|
+
...moduleKind !== void 0 ? { moduleKind } : {},
|
|
4677
|
+
// P1-C C2: push the whole local manifest so the backend
|
|
4678
|
+
// materializes owns.tables into the fresh draft. `init` omits
|
|
4679
|
+
// this (scaffolds empty); `register` sends it because the local
|
|
4680
|
+
// module.json is the source of truth for an already-authored app.
|
|
4681
|
+
manifest
|
|
4299
4682
|
},
|
|
4300
4683
|
{ baseUrl: resolveApiUrl(), auth: { token: cfg.token } }
|
|
4301
4684
|
);
|
|
@@ -4307,6 +4690,13 @@ async function moduleRegisterCommand(opts) {
|
|
|
4307
4690
|
);
|
|
4308
4691
|
} else if (err.code === "FORBIDDEN") {
|
|
4309
4692
|
fail(`Not a member of workspace "${workspace2.slug}". Join it, then retry.`);
|
|
4693
|
+
} else if (err.code === "MANIFEST_INVALID") {
|
|
4694
|
+
const issues = extractManifestIssues(err);
|
|
4695
|
+
fail(
|
|
4696
|
+
`Your module.json failed validation:
|
|
4697
|
+
${issues}
|
|
4698
|
+
Fix the issues above (or run ${code("dashwise module validate")} for the full report) and retry.`
|
|
4699
|
+
);
|
|
4310
4700
|
} else {
|
|
4311
4701
|
fail(`Registration failed (${err.code || err.status}): ${err.message}`);
|
|
4312
4702
|
}
|
|
@@ -4317,26 +4707,49 @@ async function moduleRegisterCommand(opts) {
|
|
|
4317
4707
|
}
|
|
4318
4708
|
try {
|
|
4319
4709
|
upsertModuleEntry(slug, {
|
|
4320
|
-
|
|
4710
|
+
apiKey: response.api_key,
|
|
4711
|
+
apiKeyId: response.api_key_id,
|
|
4321
4712
|
installationId: response.installation.id,
|
|
4322
|
-
expiresAt: response.
|
|
4713
|
+
expiresAt: response.api_key_expires_at
|
|
4323
4714
|
});
|
|
4324
4715
|
} catch (err) {
|
|
4325
4716
|
warn(
|
|
4326
|
-
`Registered (installation id=${response.installation.id}) but failed to cache the
|
|
4327
|
-
|
|
4328
|
-
Expires: ${response.
|
|
4717
|
+
`Registered (installation id=${response.installation.id}) but failed to cache the API key: ${err.message}
|
|
4718
|
+
API key (save this manually): ${response.api_key}
|
|
4719
|
+
Expires: ${response.api_key_expires_at}`
|
|
4329
4720
|
);
|
|
4330
4721
|
return 0;
|
|
4331
4722
|
}
|
|
4723
|
+
const tablesApplied = response.tables_applied ?? 0;
|
|
4332
4724
|
success(
|
|
4333
|
-
`Registered "${name}" \u2192 module #${response.module.id}, dev install #${response.installation.id}.
|
|
4725
|
+
`Registered "${name}" \u2192 module #${response.module.id}, dev install #${response.installation.id}. API key cached.`
|
|
4334
4726
|
);
|
|
4727
|
+
if (tablesApplied > 0) {
|
|
4728
|
+
info(
|
|
4729
|
+
`Materialized ${code(String(tablesApplied))} ${tablesApplied === 1 ? "table" : "tables"} from your module.json into the dev install.`
|
|
4730
|
+
);
|
|
4731
|
+
}
|
|
4335
4732
|
info(
|
|
4336
|
-
`Next: ${code("dashwise dev")} to run it against DashWise, or ${code("dashwise module publish")} to deploy
|
|
4733
|
+
`Next: ${code("dashwise dev")} to run it against DashWise, or ${code("dashwise module publish")} to deploy.${tablesApplied === 0 ? ` Add tables with ${code("dashwise table create")}.` : ""}`
|
|
4337
4734
|
);
|
|
4338
4735
|
return 0;
|
|
4339
4736
|
}
|
|
4737
|
+
function extractManifestIssues(err) {
|
|
4738
|
+
const issues = err.context?.issues;
|
|
4739
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
4740
|
+
return issues.map((issue) => {
|
|
4741
|
+
if (typeof issue === "string") return ` - ${issue}`;
|
|
4742
|
+
if (issue && typeof issue === "object") {
|
|
4743
|
+
const o = issue;
|
|
4744
|
+
const path = typeof o.path === "string" ? `${o.path}: ` : "";
|
|
4745
|
+
const message = typeof o.message === "string" ? o.message : JSON.stringify(issue);
|
|
4746
|
+
return ` - ${path}${message}`;
|
|
4747
|
+
}
|
|
4748
|
+
return ` - ${String(issue)}`;
|
|
4749
|
+
}).join("\n");
|
|
4750
|
+
}
|
|
4751
|
+
return ` - ${err.message}`;
|
|
4752
|
+
}
|
|
4340
4753
|
|
|
4341
4754
|
// src/commands/module/generate.ts
|
|
4342
4755
|
init_api_client();
|
|
@@ -5174,6 +5587,7 @@ function readIssues(context) {
|
|
|
5174
5587
|
|
|
5175
5588
|
// src/commands/deps/add.ts
|
|
5176
5589
|
init_api_client();
|
|
5590
|
+
init_config();
|
|
5177
5591
|
init_manifest_io();
|
|
5178
5592
|
init_output();
|
|
5179
5593
|
var MIN_PURPOSE_LEN = 10;
|
|
@@ -5309,9 +5723,59 @@ async function depsAddCommand(providerSlug, options) {
|
|
|
5309
5723
|
);
|
|
5310
5724
|
info(`Reads: ${entry.reads.map((r) => `${code(r.table)} (${r.fields.join(", ")})`).join("; ")}`);
|
|
5311
5725
|
info(`Run ${code("dashwise module generate")} to refresh typed wrappers.`);
|
|
5726
|
+
await reportConnectStatus2(manifest.module?.slug, providerSlug);
|
|
5312
5727
|
if (isInteractive) outro("Done.");
|
|
5313
5728
|
return 0;
|
|
5314
5729
|
}
|
|
5730
|
+
async function reportConnectStatus2(consumerSlug, providerSlug) {
|
|
5731
|
+
if (!consumerSlug) return;
|
|
5732
|
+
let cfg;
|
|
5733
|
+
let entry;
|
|
5734
|
+
try {
|
|
5735
|
+
cfg = readConfig();
|
|
5736
|
+
entry = readModuleEntry(consumerSlug);
|
|
5737
|
+
} catch {
|
|
5738
|
+
cfg = null;
|
|
5739
|
+
entry = null;
|
|
5740
|
+
}
|
|
5741
|
+
if (!cfg || !entry) {
|
|
5742
|
+
info(
|
|
5743
|
+
`Once provisioned (${code("dashwise init")}) + pushed (${code("dashwise dev")}), run ${code("dashwise deps")} to see whether ${code(providerSlug)} resolves or parks.`
|
|
5744
|
+
);
|
|
5745
|
+
return;
|
|
5746
|
+
}
|
|
5747
|
+
const requestOptions = {
|
|
5748
|
+
baseUrl: resolveApiUrl(),
|
|
5749
|
+
auth: { token: cfg.token }
|
|
5750
|
+
};
|
|
5751
|
+
try {
|
|
5752
|
+
const deps = await getInstallationDependencies(entry.installationId, requestOptions);
|
|
5753
|
+
const match = deps.find((d) => d.slug === providerSlug);
|
|
5754
|
+
if (!match) {
|
|
5755
|
+
info(
|
|
5756
|
+
`${code(providerSlug)} isn't reconciled server-side yet. Push the manifest (${code("dashwise dev")} watches + applies it) \u2014 then ${code("dashwise deps")} shows its resolve/park status.`
|
|
5757
|
+
);
|
|
5758
|
+
return;
|
|
5759
|
+
}
|
|
5760
|
+
if (match.status === "resolved") {
|
|
5761
|
+
const providerLine = match.provider != null ? ` (${dim(`provider install #${match.provider.installation_id} @ v${match.provider.version}`)})` : "";
|
|
5762
|
+
out(` ${pc.green("\u25CF resolved")}${providerLine}`);
|
|
5763
|
+
} else {
|
|
5764
|
+
out(` ${pc.yellow("\u25CB unbound")} ${dim("reason:")} ${match.reason ?? dim("(unspecified)")}`);
|
|
5765
|
+
info(
|
|
5766
|
+
`Declared but not connected. Install the provider in this workspace, then ${code(`dashwise deps bind ${providerSlug}`)} \u2014 reads return ${code("DEP_UNBOUND")} until it's bound.`
|
|
5767
|
+
);
|
|
5768
|
+
}
|
|
5769
|
+
} catch (err) {
|
|
5770
|
+
if (err instanceof ApiError && err.status === 401) {
|
|
5771
|
+
info(`(Skipped connect-status check \u2014 CLI token stale; run ${code("dashwise login")} to refresh.)`);
|
|
5772
|
+
} else {
|
|
5773
|
+
info(
|
|
5774
|
+
`(Couldn't fetch connect status right now \u2014 run ${code("dashwise deps")} later to see whether ${code(providerSlug)} resolves or parks.)`
|
|
5775
|
+
);
|
|
5776
|
+
}
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5315
5779
|
async function collectInteractive(providerSlug, summary, resolvedVersion, options) {
|
|
5316
5780
|
const reads = [];
|
|
5317
5781
|
if (summary && summary.exposes && summary.exposes.length > 0) {
|
|
@@ -5730,107 +6194,241 @@ async function depsRemoveCommand(providerSlug, options) {
|
|
|
5730
6194
|
return 0;
|
|
5731
6195
|
}
|
|
5732
6196
|
|
|
5733
|
-
// src/commands/
|
|
6197
|
+
// src/commands/deps/connect.ts
|
|
5734
6198
|
init_api_client();
|
|
6199
|
+
init_config();
|
|
5735
6200
|
init_manifest_io();
|
|
5736
|
-
init_schema_command_context();
|
|
5737
6201
|
init_output();
|
|
5738
|
-
|
|
6202
|
+
function loadContext(opts) {
|
|
6203
|
+
const manifest = readManifestOrThrow(opts.dir);
|
|
6204
|
+
const slug = manifest.module?.slug;
|
|
6205
|
+
if (!slug) {
|
|
6206
|
+
throw new Error("module.json has no module.slug \u2014 the manifest is malformed.");
|
|
6207
|
+
}
|
|
6208
|
+
const cfg = readConfig();
|
|
6209
|
+
if (!cfg) {
|
|
6210
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
6211
|
+
}
|
|
6212
|
+
const entry = readModuleEntry(slug);
|
|
6213
|
+
if (!entry) {
|
|
6214
|
+
throw new Error(
|
|
6215
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
|
|
6216
|
+
);
|
|
6217
|
+
}
|
|
6218
|
+
return {
|
|
6219
|
+
installationId: entry.installationId,
|
|
6220
|
+
slug,
|
|
6221
|
+
requestOptions: {
|
|
6222
|
+
baseUrl: resolveApiUrl(),
|
|
6223
|
+
auth: { token: cfg.token }
|
|
6224
|
+
}
|
|
6225
|
+
};
|
|
6226
|
+
}
|
|
6227
|
+
function statusChip(status) {
|
|
6228
|
+
if (status === "resolved") return pc.green("\u25CF resolved");
|
|
6229
|
+
if (status === "unbound") return pc.yellow("\u25CB unbound");
|
|
6230
|
+
return pc.dim(`? ${status}`);
|
|
6231
|
+
}
|
|
6232
|
+
async function depsConnectListCommand(options) {
|
|
5739
6233
|
let ctx;
|
|
5740
6234
|
try {
|
|
5741
|
-
ctx =
|
|
5742
|
-
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
5743
|
-
});
|
|
6235
|
+
ctx = loadContext(options);
|
|
5744
6236
|
} catch (err) {
|
|
5745
6237
|
fail(err.message);
|
|
5746
6238
|
return 1;
|
|
5747
6239
|
}
|
|
5748
|
-
|
|
5749
|
-
let remoteManifest;
|
|
6240
|
+
let deps;
|
|
5750
6241
|
try {
|
|
5751
|
-
|
|
5752
|
-
ctx.entry.installationId,
|
|
5753
|
-
ctx.requestOptions
|
|
5754
|
-
);
|
|
5755
|
-
remoteManifest = response.manifest;
|
|
6242
|
+
deps = await getInstallationDependencies(ctx.installationId, ctx.requestOptions);
|
|
5756
6243
|
} catch (err) {
|
|
5757
|
-
return
|
|
6244
|
+
return surfaceDepError(err);
|
|
5758
6245
|
}
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
if (localJson === remoteJson) {
|
|
5762
|
-
success("Already in sync \u2014 local module.json matches backend.");
|
|
6246
|
+
if (options.json) {
|
|
6247
|
+
out(JSON.stringify(deps, null, 2));
|
|
5763
6248
|
return 0;
|
|
5764
6249
|
}
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
6250
|
+
if (deps.length === 0) {
|
|
6251
|
+
info(`No cross-module dependencies declared for ${code(ctx.slug)}.`);
|
|
6252
|
+
info(`Add one with ${code("dashwise deps add <provider-slug>")}.`);
|
|
6253
|
+
return 0;
|
|
6254
|
+
}
|
|
6255
|
+
const unbound = deps.filter((d) => d.status === "unbound").length;
|
|
6256
|
+
info(
|
|
6257
|
+
`Cross-module dependencies for ${code(ctx.slug)} ${dim(
|
|
6258
|
+
`(${deps.length} declared${unbound > 0 ? `, ${unbound} unbound` : ""})`
|
|
6259
|
+
)}:`
|
|
6260
|
+
);
|
|
6261
|
+
out("");
|
|
6262
|
+
for (const dep of deps) {
|
|
6263
|
+
out(` ${statusChip(dep.status)} ${code(dep.slug)}@${dep.range ?? "*"}`);
|
|
6264
|
+
if (dep.status === "resolved" && dep.provider) {
|
|
6265
|
+
out(
|
|
6266
|
+
` ${dim("provider:")} install #${dep.provider.installation_id} @ v${dep.provider.version}`
|
|
6267
|
+
);
|
|
6268
|
+
}
|
|
6269
|
+
if (dep.status === "unbound") {
|
|
6270
|
+
out(` ${dim("reason:")} ${dep.reason ?? dim("(unspecified)")}`);
|
|
6271
|
+
}
|
|
6272
|
+
out("");
|
|
6273
|
+
}
|
|
6274
|
+
if (unbound > 0) {
|
|
5770
6275
|
info(
|
|
5771
|
-
`
|
|
6276
|
+
`Connect a parked dep with ${code("dashwise deps bind <provider-slug>")} once its provider is installed.`
|
|
5772
6277
|
);
|
|
5773
|
-
|
|
6278
|
+
}
|
|
6279
|
+
return 0;
|
|
6280
|
+
}
|
|
6281
|
+
async function depsBindCommand(providerSlug, options) {
|
|
6282
|
+
if (!providerSlug || typeof providerSlug !== "string") {
|
|
6283
|
+
fail("Usage: `dashwise deps bind <provider-slug> [--installation <id>]`");
|
|
6284
|
+
return 1;
|
|
6285
|
+
}
|
|
6286
|
+
let ctx;
|
|
6287
|
+
try {
|
|
6288
|
+
ctx = loadContext(options);
|
|
6289
|
+
} catch (err) {
|
|
6290
|
+
fail(err.message);
|
|
6291
|
+
return 1;
|
|
6292
|
+
}
|
|
6293
|
+
try {
|
|
6294
|
+
const result = await bindDependency(
|
|
6295
|
+
ctx.installationId,
|
|
6296
|
+
providerSlug,
|
|
6297
|
+
options.installation !== void 0 ? { provider_installation_id: options.installation } : {},
|
|
6298
|
+
ctx.requestOptions
|
|
6299
|
+
);
|
|
6300
|
+
const providerLine = result.provider != null ? ` \u2192 install #${result.provider.installation_id} @ v${result.provider.version}` : "";
|
|
6301
|
+
success(`Bound ${code(providerSlug)} for ${code(ctx.slug)}${providerLine}.`);
|
|
6302
|
+
info("Runtime reads against this dependency will now succeed.");
|
|
5774
6303
|
return 0;
|
|
6304
|
+
} catch (err) {
|
|
6305
|
+
return surfaceBindError(err, providerSlug, ctx.slug);
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
async function depsUnbindCommand(providerSlug, options) {
|
|
6309
|
+
if (!providerSlug || typeof providerSlug !== "string") {
|
|
6310
|
+
fail("Usage: `dashwise deps unbind <provider-slug>`");
|
|
6311
|
+
return 1;
|
|
5775
6312
|
}
|
|
6313
|
+
let ctx;
|
|
5776
6314
|
try {
|
|
5777
|
-
|
|
6315
|
+
ctx = loadContext(options);
|
|
5778
6316
|
} catch (err) {
|
|
5779
|
-
fail(
|
|
6317
|
+
fail(err.message);
|
|
5780
6318
|
return 1;
|
|
5781
6319
|
}
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
6320
|
+
try {
|
|
6321
|
+
await unbindDependency(ctx.installationId, providerSlug, ctx.requestOptions);
|
|
6322
|
+
success(`Unbound ${code(providerSlug)} for ${code(ctx.slug)}.`);
|
|
6323
|
+
warn(
|
|
6324
|
+
`Runtime reads against this dependency now return ${code("DEP_UNBOUND")} until you ${code("dashwise deps bind")} it again.`
|
|
6325
|
+
);
|
|
6326
|
+
return 0;
|
|
6327
|
+
} catch (err) {
|
|
6328
|
+
return surfaceDepError(err);
|
|
6329
|
+
}
|
|
5786
6330
|
}
|
|
5787
|
-
function
|
|
6331
|
+
function surfaceBindError(err, providerSlug, consumerSlug) {
|
|
6332
|
+
if (err instanceof ApiError) {
|
|
6333
|
+
switch (err.code) {
|
|
6334
|
+
case "MISSING_DEPENDENCY":
|
|
6335
|
+
fail(
|
|
6336
|
+
`No installation of ${code(providerSlug)} found in ${code(consumerSlug)}'s workspace. Install the provider module first, then re-run ${code(`dashwise deps bind ${providerSlug}`)}.`
|
|
6337
|
+
);
|
|
6338
|
+
return 1;
|
|
6339
|
+
case "DEPENDENCY_VERSION_MISMATCH":
|
|
6340
|
+
fail(
|
|
6341
|
+
`The installed ${code(providerSlug)} version is outside your declared range (${err.message}). Update the range in module.json (${code(`dashwise deps add ${providerSlug}`)}) or upgrade the provider install.`
|
|
6342
|
+
);
|
|
6343
|
+
return 1;
|
|
6344
|
+
case "PROVIDER_VERSION_RECORD_MISSING":
|
|
6345
|
+
fail(
|
|
6346
|
+
`The provider ${code(providerSlug)}'s version record is missing (${err.message}). This is a provider-side data issue \u2014 the provider may need to re-publish.`
|
|
6347
|
+
);
|
|
6348
|
+
return 1;
|
|
6349
|
+
case "EXPOSES_MISSING":
|
|
6350
|
+
fail(
|
|
6351
|
+
`${code(providerSlug)} no longer exposes a table/field your dependency reads (${err.message}). Trim your ${code("reads[]")} or ask the provider to re-expose it.`
|
|
6352
|
+
);
|
|
6353
|
+
return 1;
|
|
6354
|
+
case "EXPOSES_PRIVATE":
|
|
6355
|
+
fail(
|
|
6356
|
+
`${code(providerSlug)} exposes the requested surface as ${code("private")} \u2014 it can't be read by depending modules (${err.message}).`
|
|
6357
|
+
);
|
|
6358
|
+
return 1;
|
|
6359
|
+
case "ACTION_NOT_EXPOSED":
|
|
6360
|
+
fail(
|
|
6361
|
+
`A cross-module action your dependency declares isn't exposed by ${code(providerSlug)} (${err.message}). Remove it from your dep's ${code("actions[]")} or ask the provider to expose it.`
|
|
6362
|
+
);
|
|
6363
|
+
return 1;
|
|
6364
|
+
case "PROVIDER_NOT_PRODUCTION":
|
|
6365
|
+
fail(
|
|
6366
|
+
`A production install can only bind to a production provider install; the available ${code(providerSlug)} install isn't production (${err.message}).`
|
|
6367
|
+
);
|
|
6368
|
+
return 1;
|
|
6369
|
+
case "DEPENDENCY_NOT_DECLARED":
|
|
6370
|
+
fail(
|
|
6371
|
+
`${code(consumerSlug)}'s module.json doesn't declare a dependency on ${code(providerSlug)}. Add it with ${code(`dashwise deps add ${providerSlug}`)} + push the manifest first.`
|
|
6372
|
+
);
|
|
6373
|
+
return 1;
|
|
6374
|
+
default:
|
|
6375
|
+
fail(
|
|
6376
|
+
`Could not bind ${code(providerSlug)}: ${err.code}${err.message ? ` \u2014 ${err.message}` : ""}. The dependency stays unbound.`
|
|
6377
|
+
);
|
|
6378
|
+
return 1;
|
|
6379
|
+
}
|
|
6380
|
+
}
|
|
6381
|
+
fail(`Bind failed: ${err.message}`);
|
|
6382
|
+
return 1;
|
|
6383
|
+
}
|
|
6384
|
+
function surfaceDepError(err) {
|
|
5788
6385
|
if (err instanceof ApiError) {
|
|
5789
6386
|
switch (err.code) {
|
|
5790
6387
|
case "UNAUTHENTICATED":
|
|
5791
6388
|
fail(
|
|
5792
|
-
`
|
|
6389
|
+
`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
|
|
5793
6390
|
);
|
|
5794
|
-
|
|
5795
|
-
case "
|
|
6391
|
+
return 1;
|
|
6392
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
5796
6393
|
fail(
|
|
5797
|
-
`
|
|
6394
|
+
`You're not a member of this installation's workspace. Dependency management requires workspace membership.`
|
|
5798
6395
|
);
|
|
5799
|
-
|
|
5800
|
-
case "
|
|
6396
|
+
return 1;
|
|
6397
|
+
case "NOT_WORKSPACE_ADMIN":
|
|
6398
|
+
case "FORBIDDEN":
|
|
5801
6399
|
fail(
|
|
5802
|
-
`
|
|
6400
|
+
`Not authorized. Binding / unbinding dependencies requires workspace-admin.`
|
|
5803
6401
|
);
|
|
5804
|
-
|
|
6402
|
+
return 1;
|
|
5805
6403
|
case "INSTALLATION_NOT_FOUND":
|
|
5806
6404
|
fail(
|
|
5807
|
-
`
|
|
6405
|
+
`Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
|
|
5808
6406
|
);
|
|
5809
|
-
|
|
6407
|
+
return 1;
|
|
6408
|
+
case "DEPENDENCY_NOT_DECLARED":
|
|
6409
|
+
fail(
|
|
6410
|
+
`No such dependency declared in this module's manifest. Run ${code("dashwise deps")} to see declared deps.`
|
|
6411
|
+
);
|
|
6412
|
+
return 1;
|
|
5810
6413
|
default:
|
|
5811
6414
|
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
6415
|
+
return 1;
|
|
5812
6416
|
}
|
|
5813
|
-
return 1;
|
|
5814
6417
|
}
|
|
5815
|
-
fail(`
|
|
6418
|
+
fail(`Dependency operation failed: ${err.message}`);
|
|
5816
6419
|
return 1;
|
|
5817
6420
|
}
|
|
5818
6421
|
|
|
5819
|
-
// src/
|
|
5820
|
-
init_scaffold();
|
|
5821
|
-
|
|
5822
|
-
// src/commands/api-keys.ts
|
|
6422
|
+
// src/commands/uses/bind.ts
|
|
5823
6423
|
init_api_client();
|
|
5824
6424
|
init_config();
|
|
5825
6425
|
init_manifest_io();
|
|
5826
6426
|
init_output();
|
|
5827
|
-
function
|
|
6427
|
+
function loadContext2(opts) {
|
|
5828
6428
|
const manifest = readManifestOrThrow(opts.dir);
|
|
5829
6429
|
const slug = manifest.module?.slug;
|
|
5830
6430
|
if (!slug) {
|
|
5831
|
-
throw new Error(
|
|
5832
|
-
`module.json has no module.slug \u2014 the manifest is malformed.`
|
|
5833
|
-
);
|
|
6431
|
+
throw new Error("module.json has no module.slug \u2014 the manifest is malformed.");
|
|
5834
6432
|
}
|
|
5835
6433
|
const cfg = readConfig();
|
|
5836
6434
|
if (!cfg) {
|
|
@@ -5845,47 +6443,895 @@ function loadContext(opts) {
|
|
|
5845
6443
|
return {
|
|
5846
6444
|
installationId: entry.installationId,
|
|
5847
6445
|
slug,
|
|
6446
|
+
...opts.dir !== void 0 ? { dir: opts.dir } : {},
|
|
5848
6447
|
requestOptions: {
|
|
5849
6448
|
baseUrl: resolveApiUrl(),
|
|
5850
6449
|
auth: { token: cfg.token }
|
|
5851
6450
|
}
|
|
5852
6451
|
};
|
|
5853
6452
|
}
|
|
5854
|
-
|
|
6453
|
+
function statusChip2(status) {
|
|
6454
|
+
if (status === "bound") return pc.green("\u25CF bound");
|
|
6455
|
+
if (status === "unbound") return pc.yellow("\u25CB unbound");
|
|
6456
|
+
if (status === "declined") return pc.dim("\u2298 declined");
|
|
6457
|
+
return pc.dim(`? ${status}`);
|
|
6458
|
+
}
|
|
6459
|
+
function opsLine(ops) {
|
|
6460
|
+
return ops.length > 0 ? ops.join(", ") : dim("(none)");
|
|
6461
|
+
}
|
|
6462
|
+
async function bindListCommand(options) {
|
|
5855
6463
|
let ctx;
|
|
5856
6464
|
try {
|
|
5857
|
-
ctx =
|
|
6465
|
+
ctx = loadContext2(options);
|
|
5858
6466
|
} catch (err) {
|
|
5859
6467
|
fail(err.message);
|
|
5860
6468
|
return 1;
|
|
5861
6469
|
}
|
|
5862
|
-
let
|
|
6470
|
+
let bindings;
|
|
5863
6471
|
try {
|
|
5864
|
-
|
|
5865
|
-
keys = resp.api_keys;
|
|
6472
|
+
bindings = await getInstallationBindings(ctx.installationId, ctx.requestOptions);
|
|
5866
6473
|
} catch (err) {
|
|
5867
|
-
return
|
|
6474
|
+
return surfaceBindingError(err);
|
|
5868
6475
|
}
|
|
5869
|
-
const filtered = options.all ? keys : keys.filter((k) => k.revoked_at === null);
|
|
5870
6476
|
if (options.json) {
|
|
5871
|
-
|
|
5872
|
-
`);
|
|
6477
|
+
out(JSON.stringify(bindings, null, 2));
|
|
5873
6478
|
return 0;
|
|
5874
6479
|
}
|
|
5875
|
-
if (
|
|
6480
|
+
if (bindings.length === 0) {
|
|
6481
|
+
info(`No workspace-table ${code("uses")} slots declared for ${code(ctx.slug)}.`);
|
|
5876
6482
|
info(
|
|
5877
|
-
|
|
6483
|
+
`Declare one by hand in ${code("module.json#uses.tables")}, or on-ramp an existing table with ${code("dashwise uses add --from-table <name>")}.`
|
|
5878
6484
|
);
|
|
5879
6485
|
return 0;
|
|
5880
6486
|
}
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
6487
|
+
const unbound = bindings.filter((b) => b.status === "unbound").length;
|
|
6488
|
+
info(
|
|
6489
|
+
`Workspace-table bindings for ${code(ctx.slug)} ${dim(
|
|
6490
|
+
`(${bindings.length} declared${unbound > 0 ? `, ${unbound} unbound` : ""})`
|
|
6491
|
+
)}:`
|
|
6492
|
+
);
|
|
6493
|
+
out("");
|
|
6494
|
+
for (const b of bindings) {
|
|
6495
|
+
const enforced = b.ops.filter((o) => (b.consented_ops ?? []).includes(o));
|
|
6496
|
+
const opsSummary = b.status === "bound" && enforced.length !== b.ops.length ? `${opsLine(enforced)} ${dim(`(declared ${opsLine(b.ops)}; re-bind to consent to the rest)`)}` : opsLine(b.ops);
|
|
6497
|
+
out(` ${statusChip2(b.status)} ${code(b.uses_slug)} ${dim(`[${opsSummary}]`)}`);
|
|
6498
|
+
if (b.purpose) {
|
|
6499
|
+
out(` ${dim("purpose:")} ${b.purpose}`);
|
|
6500
|
+
}
|
|
6501
|
+
if (b.status === "bound" && b.table) {
|
|
6502
|
+
out(` ${dim("table:")} ${b.table.name} ${dim(`(id ${b.table.id})`)}`);
|
|
6503
|
+
const mapped = b.field_map ? Object.keys(b.field_map).length : 0;
|
|
6504
|
+
if (mapped > 0) {
|
|
6505
|
+
out(` ${dim("fields:")} ${mapped} mapped`);
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
if (b.status === "unbound") {
|
|
6509
|
+
out(` ${dim("reason:")} ${b.reason ?? dim("(unspecified)")}`);
|
|
6510
|
+
}
|
|
6511
|
+
if (b.missing_fields && b.missing_fields.length > 0) {
|
|
6512
|
+
out(
|
|
6513
|
+
` ${pc.yellow("missing:")} ${b.missing_fields.join(", ")} ${dim("(required field(s) not mapped)")}`
|
|
6514
|
+
);
|
|
6515
|
+
}
|
|
6516
|
+
out("");
|
|
6517
|
+
}
|
|
6518
|
+
if (unbound > 0) {
|
|
6519
|
+
info(
|
|
6520
|
+
`Connect a parked slot with ${code("dashwise bind <uses-slug> --table <name>")} (or ${code("--create")} to materialize a fresh table).`
|
|
6521
|
+
);
|
|
6522
|
+
}
|
|
6523
|
+
return 0;
|
|
6524
|
+
}
|
|
6525
|
+
async function bindCommand(usesSlug, options) {
|
|
6526
|
+
if (!usesSlug || typeof usesSlug !== "string") {
|
|
6527
|
+
fail(
|
|
6528
|
+
"Usage: `dashwise bind <uses-slug> [--table <name-or-id>] [--map <usesField>=<physicalFieldName>...]`, `--create`, or `--decline`."
|
|
6529
|
+
);
|
|
6530
|
+
return 1;
|
|
6531
|
+
}
|
|
6532
|
+
const modes = [options.create, options.decline, options.table !== void 0].filter(
|
|
6533
|
+
Boolean
|
|
6534
|
+
).length;
|
|
6535
|
+
if (modes > 1) {
|
|
6536
|
+
fail(
|
|
6537
|
+
`Pass at most one of ${code("--table")}, ${code("--create")}, ${code("--decline")} \u2014 they select different bind modes.`
|
|
6538
|
+
);
|
|
6539
|
+
return 1;
|
|
6540
|
+
}
|
|
6541
|
+
if (options.decline && (options.map ?? []).length > 0) {
|
|
6542
|
+
fail(`${code("--map")} makes no sense with ${code("--decline")} (nothing is being bound).`);
|
|
6543
|
+
return 1;
|
|
6544
|
+
}
|
|
6545
|
+
let ctx;
|
|
6546
|
+
try {
|
|
6547
|
+
ctx = loadContext2(options);
|
|
6548
|
+
} catch (err) {
|
|
6549
|
+
fail(err.message);
|
|
6550
|
+
return 1;
|
|
6551
|
+
}
|
|
6552
|
+
if (options.decline) {
|
|
6553
|
+
return declineSlot(ctx, usesSlug);
|
|
6554
|
+
}
|
|
6555
|
+
if (options.create) {
|
|
6556
|
+
return bindViaCreate(ctx, usesSlug);
|
|
6557
|
+
}
|
|
6558
|
+
let mapOverrides;
|
|
6559
|
+
try {
|
|
6560
|
+
mapOverrides = parseMapOverrides(options.map);
|
|
6561
|
+
} catch (err) {
|
|
6562
|
+
fail(err.message);
|
|
6563
|
+
return 1;
|
|
6564
|
+
}
|
|
6565
|
+
let candidates;
|
|
6566
|
+
try {
|
|
6567
|
+
candidates = await getBindingCandidates(
|
|
6568
|
+
ctx.installationId,
|
|
6569
|
+
usesSlug,
|
|
6570
|
+
ctx.requestOptions
|
|
6571
|
+
);
|
|
6572
|
+
} catch (err) {
|
|
6573
|
+
return surfaceBindingError(err);
|
|
6574
|
+
}
|
|
6575
|
+
const chosen = await chooseCandidate(candidates, usesSlug, options.table);
|
|
6576
|
+
if (chosen === "cancelled") {
|
|
6577
|
+
info("Cancelled.");
|
|
6578
|
+
return 130;
|
|
6579
|
+
}
|
|
6580
|
+
if (chosen === null) {
|
|
6581
|
+
return 1;
|
|
6582
|
+
}
|
|
6583
|
+
let fieldMap;
|
|
6584
|
+
try {
|
|
6585
|
+
fieldMap = await buildFieldMap(ctx, chosen, mapOverrides);
|
|
6586
|
+
} catch (err) {
|
|
6587
|
+
if (err instanceof ApiError) return surfaceBindingError(err);
|
|
6588
|
+
fail(err.message);
|
|
6589
|
+
return 1;
|
|
6590
|
+
}
|
|
6591
|
+
let result;
|
|
6592
|
+
try {
|
|
6593
|
+
result = await putBinding(
|
|
6594
|
+
ctx.installationId,
|
|
6595
|
+
usesSlug,
|
|
6596
|
+
fieldMap !== void 0 ? { table_id: chosen.table_id, field_map: fieldMap } : { table_id: chosen.table_id },
|
|
6597
|
+
ctx.requestOptions
|
|
6598
|
+
);
|
|
6599
|
+
} catch (err) {
|
|
6600
|
+
return surfacePutBindingError(err, usesSlug, ctx.slug);
|
|
6601
|
+
}
|
|
6602
|
+
reportBoundOk(result, ctx.slug);
|
|
6603
|
+
return 0;
|
|
6604
|
+
}
|
|
6605
|
+
async function chooseCandidate(candidates, usesSlug, tableFlag) {
|
|
6606
|
+
if (candidates.length === 0) {
|
|
6607
|
+
fail(
|
|
6608
|
+
`No workspace table can satisfy ${code(usesSlug)} yet. Create a matching table (or ${code(`dashwise bind ${usesSlug} --create`)} to materialize one from the declared fields).`
|
|
6609
|
+
);
|
|
6610
|
+
return null;
|
|
6611
|
+
}
|
|
6612
|
+
if (tableFlag !== void 0) {
|
|
6613
|
+
const match = matchCandidate(candidates, tableFlag);
|
|
6614
|
+
if (!match) {
|
|
6615
|
+
const list2 = candidates.map((c2) => ` - ${c2.name} ${dim(`(id ${c2.table_id}, score ${c2.match_score})`)}`).join("\n");
|
|
6616
|
+
fail(
|
|
6617
|
+
`No candidate table matching ${code(tableFlag)} for ${code(usesSlug)}. Candidates:
|
|
6618
|
+
${list2}`
|
|
6619
|
+
);
|
|
6620
|
+
return null;
|
|
6621
|
+
}
|
|
6622
|
+
return match;
|
|
6623
|
+
}
|
|
6624
|
+
if (candidates.length === 1) {
|
|
6625
|
+
return candidates[0];
|
|
6626
|
+
}
|
|
6627
|
+
if (process.stdout.isTTY) {
|
|
6628
|
+
const choice = await select({
|
|
6629
|
+
message: `Which workspace table should ${usesSlug} bind to?`,
|
|
6630
|
+
options: candidates.map((c2) => ({
|
|
6631
|
+
value: String(c2.table_id),
|
|
6632
|
+
label: c2.name,
|
|
6633
|
+
hint: c2.missing_required.length > 0 ? `id=${c2.table_id}, score=${c2.match_score}, missing: ${c2.missing_required.join(", ")}` : `id=${c2.table_id}, score=${c2.match_score}, full coverage`
|
|
6634
|
+
}))
|
|
6635
|
+
});
|
|
6636
|
+
if (isCancel(choice)) return "cancelled";
|
|
6637
|
+
return candidates.find((c2) => String(c2.table_id) === choice) ?? null;
|
|
6638
|
+
}
|
|
6639
|
+
const list = candidates.map((c2) => ` - ${c2.name} ${dim(`(id ${c2.table_id}, score ${c2.match_score})`)}`).join("\n");
|
|
6640
|
+
fail(
|
|
6641
|
+
`${candidates.length} candidate tables for ${code(usesSlug)}. Re-run with ${code("--table <name-or-id>")} to pick one:
|
|
6642
|
+
${list}`
|
|
6643
|
+
);
|
|
6644
|
+
return null;
|
|
6645
|
+
}
|
|
6646
|
+
function matchCandidate(candidates, flag) {
|
|
6647
|
+
const asNumber = Number.parseInt(flag, 10);
|
|
6648
|
+
const isNumeric = !Number.isNaN(asNumber) && String(asNumber) === flag;
|
|
6649
|
+
if (isNumeric) {
|
|
6650
|
+
return candidates.find((c2) => c2.table_id === asNumber);
|
|
6651
|
+
}
|
|
6652
|
+
return candidates.find((c2) => c2.name === flag);
|
|
6653
|
+
}
|
|
6654
|
+
async function buildFieldMap(ctx, candidate, overrides) {
|
|
6655
|
+
const overrideKeys = Object.keys(overrides);
|
|
6656
|
+
if (overrideKeys.length === 0) {
|
|
6657
|
+
return Object.keys(candidate.auto_field_map).length > 0 ? { ...candidate.auto_field_map } : void 0;
|
|
6658
|
+
}
|
|
6659
|
+
const table = await introspectWorkspaceTable(
|
|
6660
|
+
ctx.installationId,
|
|
6661
|
+
String(candidate.table_id),
|
|
6662
|
+
ctx.requestOptions
|
|
6663
|
+
);
|
|
6664
|
+
const byName = /* @__PURE__ */ new Map();
|
|
6665
|
+
for (const f of table.fields) {
|
|
6666
|
+
byName.set(f.slug, f.id);
|
|
6667
|
+
if (f.name) byName.set(f.name, f.id);
|
|
6668
|
+
}
|
|
6669
|
+
const merged = { ...candidate.auto_field_map };
|
|
6670
|
+
for (const [usesField, physicalName] of Object.entries(overrides)) {
|
|
6671
|
+
const id = byName.get(physicalName);
|
|
6672
|
+
if (id === void 0) {
|
|
6673
|
+
const available = table.fields.map((f) => f.name && f.name !== f.slug ? `${f.slug} (${f.name})` : f.slug).join(", ");
|
|
6674
|
+
throw new Error(
|
|
6675
|
+
`--map ${usesField}=${physicalName}: table "${table.name}" has no field named "${physicalName}". Available: ${available}.`
|
|
6676
|
+
);
|
|
6677
|
+
}
|
|
6678
|
+
merged[usesField] = id;
|
|
6679
|
+
}
|
|
6680
|
+
return merged;
|
|
6681
|
+
}
|
|
6682
|
+
function parseMapOverrides(specs) {
|
|
6683
|
+
const out2 = {};
|
|
6684
|
+
for (const spec of specs ?? []) {
|
|
6685
|
+
const eq = spec.indexOf("=");
|
|
6686
|
+
if (eq <= 0 || eq === spec.length - 1) {
|
|
6687
|
+
throw new Error(
|
|
6688
|
+
`Invalid --map "${spec}". Expected \`<usesField>=<physicalFieldName>\` (e.g. \`--map name=full_name\`).`
|
|
6689
|
+
);
|
|
6690
|
+
}
|
|
6691
|
+
const usesField = spec.slice(0, eq).trim();
|
|
6692
|
+
const physicalName = spec.slice(eq + 1).trim();
|
|
6693
|
+
if (!usesField || !physicalName) {
|
|
6694
|
+
throw new Error(`Invalid --map "${spec}": both sides must be non-empty.`);
|
|
6695
|
+
}
|
|
6696
|
+
if (out2[usesField] !== void 0) {
|
|
6697
|
+
throw new Error(`Duplicate --map for uses field "${usesField}".`);
|
|
6698
|
+
}
|
|
6699
|
+
out2[usesField] = physicalName;
|
|
6700
|
+
}
|
|
6701
|
+
return out2;
|
|
6702
|
+
}
|
|
6703
|
+
async function bindViaCreate(ctx, usesSlug) {
|
|
6704
|
+
const slot = findUsesSlot(ctx, usesSlug);
|
|
6705
|
+
if (slot && slot.if_missing !== void 0 && slot.if_missing !== "create") {
|
|
6706
|
+
warn(
|
|
6707
|
+
`${code(usesSlug)} declares ${code(`if_missing: "${slot.if_missing}"`)}, not ${code('"create"')} \u2014 the backend may reject ${code("--create")}. Set it to ${code("create")} in module.json if you want a table materialized.`
|
|
6708
|
+
);
|
|
6709
|
+
}
|
|
6710
|
+
let result;
|
|
6711
|
+
try {
|
|
6712
|
+
result = await putBinding(
|
|
6713
|
+
ctx.installationId,
|
|
6714
|
+
usesSlug,
|
|
6715
|
+
{ create: true },
|
|
6716
|
+
ctx.requestOptions
|
|
6717
|
+
);
|
|
6718
|
+
} catch (err) {
|
|
6719
|
+
return surfacePutBindingError(err, usesSlug, ctx.slug);
|
|
6720
|
+
}
|
|
6721
|
+
if (result.table) {
|
|
6722
|
+
success(
|
|
6723
|
+
`Materialized ${code(result.table.name)} ${dim(`(id ${result.table.id})`)} and bound ${code(usesSlug)} for ${code(ctx.slug)}.`
|
|
6724
|
+
);
|
|
6725
|
+
info(
|
|
6726
|
+
`The new table is a plain user-owned Data Hub table \u2014 it stays in the workspace when the module is uninstalled.`
|
|
6727
|
+
);
|
|
6728
|
+
} else {
|
|
6729
|
+
success(`Bound ${code(usesSlug)} for ${code(ctx.slug)} to a freshly-created table.`);
|
|
6730
|
+
}
|
|
6731
|
+
return 0;
|
|
6732
|
+
}
|
|
6733
|
+
async function declineSlot(ctx, usesSlug) {
|
|
6734
|
+
try {
|
|
6735
|
+
await declineBinding(ctx.installationId, usesSlug, ctx.requestOptions);
|
|
6736
|
+
success(`Declined ${code(usesSlug)} for ${code(ctx.slug)} \u2014 the module runs without it.`);
|
|
6737
|
+
return 0;
|
|
6738
|
+
} catch (err) {
|
|
6739
|
+
return surfacePutBindingError(err, usesSlug, ctx.slug);
|
|
6740
|
+
}
|
|
6741
|
+
}
|
|
6742
|
+
function findUsesSlot(ctx, usesSlug) {
|
|
6743
|
+
try {
|
|
6744
|
+
const manifest = readManifestOrThrow(ctx.dir);
|
|
6745
|
+
return manifest.uses?.tables?.find((t) => t.slug === usesSlug);
|
|
6746
|
+
} catch {
|
|
6747
|
+
return void 0;
|
|
6748
|
+
}
|
|
6749
|
+
}
|
|
6750
|
+
function reportBoundOk(result, moduleSlug) {
|
|
6751
|
+
const tableLine = result.table ? ` \u2192 ${result.table.name} ${dim(`(id ${result.table.id})`)}` : "";
|
|
6752
|
+
success(`Bound ${code(result.uses_slug)} for ${code(moduleSlug)}${tableLine}.`);
|
|
6753
|
+
const consented = result.consented_ops ?? [];
|
|
6754
|
+
info(
|
|
6755
|
+
`Consented ops: ${opsLine(consented)}. Runtime reads/writes against this slot now succeed.`
|
|
6756
|
+
);
|
|
6757
|
+
if (result.missing_fields && result.missing_fields.length > 0) {
|
|
6758
|
+
warn(
|
|
6759
|
+
`Still unmapped required field(s): ${result.missing_fields.join(", ")}. Re-run with ${code("--map <usesField>=<physicalFieldName>")} to map them.`
|
|
6760
|
+
);
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
async function unbindCommand(usesSlug, options) {
|
|
6764
|
+
if (!usesSlug || typeof usesSlug !== "string") {
|
|
6765
|
+
fail("Usage: `dashwise unbind <uses-slug>`");
|
|
6766
|
+
return 1;
|
|
6767
|
+
}
|
|
6768
|
+
let ctx;
|
|
6769
|
+
try {
|
|
6770
|
+
ctx = loadContext2(options);
|
|
6771
|
+
} catch (err) {
|
|
6772
|
+
fail(err.message);
|
|
6773
|
+
return 1;
|
|
6774
|
+
}
|
|
6775
|
+
try {
|
|
6776
|
+
await unbindBinding(ctx.installationId, usesSlug, ctx.requestOptions);
|
|
6777
|
+
success(`Unbound ${code(usesSlug)} for ${code(ctx.slug)}.`);
|
|
6778
|
+
warn(
|
|
6779
|
+
`Runtime reads/writes against this slot now return ${code("USES_UNBOUND")} until you ${code("dashwise bind")} it again.`
|
|
6780
|
+
);
|
|
6781
|
+
return 0;
|
|
6782
|
+
} catch (err) {
|
|
6783
|
+
return surfaceBindingError(err);
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
function surfacePutBindingError(err, usesSlug, moduleSlug) {
|
|
6787
|
+
if (err instanceof ApiError) {
|
|
6788
|
+
switch (err.code) {
|
|
6789
|
+
case "TABLE_NOT_IN_WORKSPACE":
|
|
6790
|
+
fail(
|
|
6791
|
+
`The chosen table isn't in ${code(moduleSlug)}'s workspace (${err.message}). A ${code("uses")} binding can only target a table the install's workspace owns.`
|
|
6792
|
+
);
|
|
6793
|
+
return 1;
|
|
6794
|
+
case "TABLE_IS_MODULE_MANAGED":
|
|
6795
|
+
fail(
|
|
6796
|
+
`That table is module-managed \u2014 ${code("uses")} bindings target plain WORKSPACE tables only. Reach a module's own tables via ${code("dashwise deps")} instead.`
|
|
6797
|
+
);
|
|
6798
|
+
return 1;
|
|
6799
|
+
case "MISSING_REQUIRED_FIELDS":
|
|
6800
|
+
fail(
|
|
6801
|
+
`The table is missing required field(s) the ${code(usesSlug)} contract needs (${err.message}). Map them explicitly with ${code("--map <usesField>=<physicalFieldName>")}, or pick a table that has them.`
|
|
6802
|
+
);
|
|
6803
|
+
return 1;
|
|
6804
|
+
case "FIELD_TYPE_INCOMPATIBLE":
|
|
6805
|
+
fail(
|
|
6806
|
+
`A mapped physical field's type is incompatible with the declared abstract type (${err.message}). Map to a compatible field with ${code("--map")}.`
|
|
6807
|
+
);
|
|
6808
|
+
return 1;
|
|
6809
|
+
case "FIELD_NOT_FOUND":
|
|
6810
|
+
fail(
|
|
6811
|
+
`A ${code("--map")} referenced a physical field that doesn't exist on the table (${err.message}). Check the field name.`
|
|
6812
|
+
);
|
|
6813
|
+
return 1;
|
|
6814
|
+
case "DECLINE_NOT_ALLOWED":
|
|
6815
|
+
fail(
|
|
6816
|
+
`${code(usesSlug)} is a required need \u2014 it can't be declined. Bind it to a table (${code(`dashwise bind ${usesSlug} --table <name>`)}) or set ${code('if_missing: "optional"')} in module.json.`
|
|
6817
|
+
);
|
|
6818
|
+
return 1;
|
|
6819
|
+
case "USES_SLOT_NOT_DECLARED":
|
|
6820
|
+
case "BINDING_NOT_FOUND":
|
|
6821
|
+
fail(
|
|
6822
|
+
`${code(moduleSlug)}'s module.json doesn't declare a ${code("uses")} slot named ${code(usesSlug)}. Add it (or ${code("dashwise uses add --from-table <name>")}) + push the manifest first.`
|
|
6823
|
+
);
|
|
6824
|
+
return 1;
|
|
6825
|
+
case "NOT_WORKSPACE_ADMIN":
|
|
6826
|
+
case "FORBIDDEN":
|
|
6827
|
+
fail(`Not authorized. Binding / unbinding / declining requires workspace-admin.`);
|
|
6828
|
+
return 1;
|
|
6829
|
+
default:
|
|
6830
|
+
fail(
|
|
6831
|
+
`Could not bind ${code(usesSlug)}: ${err.code}${err.message ? ` \u2014 ${err.message}` : ""}. The binding is unchanged.`
|
|
6832
|
+
);
|
|
6833
|
+
return 1;
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
fail(`Bind failed: ${err.message}`);
|
|
6837
|
+
return 1;
|
|
6838
|
+
}
|
|
6839
|
+
function surfaceBindingError(err) {
|
|
6840
|
+
if (err instanceof ApiError) {
|
|
6841
|
+
switch (err.code) {
|
|
6842
|
+
case "UNAUTHENTICATED":
|
|
6843
|
+
fail(
|
|
6844
|
+
`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
|
|
6845
|
+
);
|
|
6846
|
+
return 1;
|
|
6847
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
6848
|
+
fail(
|
|
6849
|
+
`You're not a member of this installation's workspace. Binding management requires workspace membership.`
|
|
6850
|
+
);
|
|
6851
|
+
return 1;
|
|
6852
|
+
case "NOT_WORKSPACE_ADMIN":
|
|
6853
|
+
case "FORBIDDEN":
|
|
6854
|
+
fail(`Not authorized. Binding / unbinding requires workspace-admin.`);
|
|
6855
|
+
return 1;
|
|
6856
|
+
case "INSTALLATION_NOT_FOUND":
|
|
6857
|
+
fail(
|
|
6858
|
+
`Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
|
|
6859
|
+
);
|
|
6860
|
+
return 1;
|
|
6861
|
+
case "USES_SLOT_NOT_DECLARED":
|
|
6862
|
+
case "BINDING_NOT_FOUND":
|
|
6863
|
+
fail(
|
|
6864
|
+
`No such ${code("uses")} slot declared in this module's manifest. Run ${code("dashwise bind")} to see declared slots.`
|
|
6865
|
+
);
|
|
6866
|
+
return 1;
|
|
6867
|
+
default:
|
|
6868
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
6869
|
+
return 1;
|
|
6870
|
+
}
|
|
6871
|
+
}
|
|
6872
|
+
fail(`Binding operation failed: ${err.message}`);
|
|
6873
|
+
return 1;
|
|
6874
|
+
}
|
|
6875
|
+
|
|
6876
|
+
// src/commands/uses/add.ts
|
|
6877
|
+
init_api_client();
|
|
6878
|
+
init_config();
|
|
6879
|
+
init_manifest_io();
|
|
6880
|
+
|
|
6881
|
+
// src/lib/uses-types.ts
|
|
6882
|
+
var PHYSICAL_TO_ABSTRACT = {
|
|
6883
|
+
text: "text",
|
|
6884
|
+
long_text: "long_text",
|
|
6885
|
+
number: "number",
|
|
6886
|
+
rating: "number",
|
|
6887
|
+
boolean: "boolean",
|
|
6888
|
+
// A date-with-time column has physical registry type `date` (with
|
|
6889
|
+
// `config.includeTime`), NOT a distinct `datetime` type — the backend
|
|
6890
|
+
// registry has no `datetime` key. So introspection only ever reports
|
|
6891
|
+
// `date` here; there is no `datetime` physical entry to fold.
|
|
6892
|
+
date: "date",
|
|
6893
|
+
email: "email",
|
|
6894
|
+
url: "url",
|
|
6895
|
+
single_select: "single_select",
|
|
6896
|
+
multi_select: "multi_select"
|
|
6897
|
+
};
|
|
6898
|
+
function abstractTypeForPhysical(physicalType) {
|
|
6899
|
+
return PHYSICAL_TO_ABSTRACT[physicalType] ?? null;
|
|
6900
|
+
}
|
|
6901
|
+
var USES_OPERATIONS = ["create", "read", "update", "delete"];
|
|
6902
|
+
|
|
6903
|
+
// src/commands/uses/add.ts
|
|
6904
|
+
init_output();
|
|
6905
|
+
async function usesAddCommand(options) {
|
|
6906
|
+
if (!options.fromTable) {
|
|
6907
|
+
fail("Usage: `dashwise uses add --from-table <name-or-id>`");
|
|
6908
|
+
return 1;
|
|
6909
|
+
}
|
|
6910
|
+
let ops;
|
|
6911
|
+
try {
|
|
6912
|
+
ops = parseOps(options.ops);
|
|
6913
|
+
} catch (err) {
|
|
6914
|
+
fail(err.message);
|
|
6915
|
+
return 1;
|
|
6916
|
+
}
|
|
6917
|
+
let ctx;
|
|
6918
|
+
try {
|
|
6919
|
+
ctx = loadContext3(options);
|
|
6920
|
+
} catch (err) {
|
|
6921
|
+
fail(err.message);
|
|
6922
|
+
return 1;
|
|
6923
|
+
}
|
|
6924
|
+
let table;
|
|
6925
|
+
try {
|
|
6926
|
+
table = await introspectWorkspaceTable(
|
|
6927
|
+
ctx.installationId,
|
|
6928
|
+
options.fromTable,
|
|
6929
|
+
ctx.requestOptions
|
|
6930
|
+
);
|
|
6931
|
+
} catch (err) {
|
|
6932
|
+
return surfaceIntrospectError(err, options.fromTable);
|
|
6933
|
+
}
|
|
6934
|
+
const slotSlug = options.slug ?? slugify(table.name);
|
|
6935
|
+
if (!isKebab(slotSlug)) {
|
|
6936
|
+
fail(
|
|
6937
|
+
`Generated slot slug ${code(slotSlug)} isn't kebab-case. Pass ${code("--slug <kebab-slug>")} explicitly.`
|
|
6938
|
+
);
|
|
6939
|
+
return 1;
|
|
6940
|
+
}
|
|
6941
|
+
const { fields, skipped } = generateFields(table.fields);
|
|
6942
|
+
if (fields.length === 0) {
|
|
6943
|
+
fail(
|
|
6944
|
+
`Table ${code(table.name)} has no fields representable as a v1 ${code("uses")} contract (all are link_row / formula / ai / file). Nothing to declare.`
|
|
6945
|
+
);
|
|
6946
|
+
return 1;
|
|
6947
|
+
}
|
|
6948
|
+
const slot = {
|
|
6949
|
+
slug: slotSlug,
|
|
6950
|
+
ops,
|
|
6951
|
+
fields,
|
|
6952
|
+
if_missing: "require"
|
|
6953
|
+
};
|
|
6954
|
+
if (options.purpose) slot.purpose = options.purpose;
|
|
6955
|
+
info(
|
|
6956
|
+
`Generated a ${code("uses")} contract ${code(slotSlug)} from table ${code(table.name)} ${dim(`(id ${table.table_id})`)}:`
|
|
6957
|
+
);
|
|
6958
|
+
out(` ${dim("ops:")} ${ops.join(", ")}`);
|
|
6959
|
+
for (const f of fields) {
|
|
6960
|
+
const req = f.required ? dim(" (required)") : "";
|
|
6961
|
+
out(` ${dim("field:")} ${code(f.slug)} \u2192 ${f.types.join(" | ")}${req}`);
|
|
6962
|
+
}
|
|
6963
|
+
if (skipped.length > 0) {
|
|
6964
|
+
warn(
|
|
6965
|
+
`Skipped ${skipped.length} field(s) not representable in a v1 uses contract: ${skipped.join(", ")}.`
|
|
6966
|
+
);
|
|
6967
|
+
}
|
|
6968
|
+
const uses = ctx.manifest.uses ?? { tables: [] };
|
|
6969
|
+
const tables = uses.tables ?? [];
|
|
6970
|
+
const existingIdx = tables.findIndex((t) => t.slug === slotSlug);
|
|
6971
|
+
if (existingIdx >= 0 && !options.yes) {
|
|
6972
|
+
fail(
|
|
6973
|
+
`A ${code("uses")} slot ${code(slotSlug)} already exists in module.json. Re-run with ${code("--yes")} to replace it, or pass a different ${code("--slug")}.`
|
|
6974
|
+
);
|
|
6975
|
+
return 1;
|
|
6976
|
+
}
|
|
6977
|
+
if (existingIdx >= 0) {
|
|
6978
|
+
tables[existingIdx] = slot;
|
|
6979
|
+
} else {
|
|
6980
|
+
tables.push(slot);
|
|
6981
|
+
}
|
|
6982
|
+
uses.tables = tables;
|
|
6983
|
+
ctx.manifest.uses = uses;
|
|
6984
|
+
try {
|
|
6985
|
+
writeManifest(ctx.manifest, ctx.dir);
|
|
6986
|
+
} catch (err) {
|
|
6987
|
+
fail(`Could not write manifest: ${err.message}`);
|
|
6988
|
+
return 1;
|
|
6989
|
+
}
|
|
6990
|
+
success(
|
|
6991
|
+
`${existingIdx >= 0 ? "Replaced" : "Added"} ${code(`uses.tables[${slotSlug}]`)} in module.json.`
|
|
6992
|
+
);
|
|
6993
|
+
const pushed = await pushManifest(ctx);
|
|
6994
|
+
if (!pushed) {
|
|
6995
|
+
info(
|
|
6996
|
+
`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}`)}.`
|
|
6997
|
+
);
|
|
6998
|
+
return 1;
|
|
6999
|
+
}
|
|
7000
|
+
if (options.noBind) {
|
|
7001
|
+
info(
|
|
7002
|
+
`Skipped auto-bind (${code("--no-bind")}). Connect it when ready: ${code(`dashwise bind ${slotSlug} --table ${table.table_id}`)}.`
|
|
7003
|
+
);
|
|
7004
|
+
return 0;
|
|
7005
|
+
}
|
|
7006
|
+
info(`Binding ${code(slotSlug)} to ${code(table.name)}\u2026`);
|
|
7007
|
+
const bindExit = await bindCommand(slotSlug, {
|
|
7008
|
+
...ctx.dir !== void 0 ? { dir: ctx.dir } : {},
|
|
7009
|
+
table: String(table.table_id)
|
|
7010
|
+
});
|
|
7011
|
+
if (bindExit !== 0) {
|
|
7012
|
+
warn(
|
|
7013
|
+
`The ${code("uses")} slot was declared + pushed, but the auto-bind didn't complete. Retry with ${code(`dashwise bind ${slotSlug} --table ${table.table_id}`)}.`
|
|
7014
|
+
);
|
|
7015
|
+
return bindExit;
|
|
7016
|
+
}
|
|
7017
|
+
return 0;
|
|
7018
|
+
}
|
|
7019
|
+
function loadContext3(opts) {
|
|
7020
|
+
const manifest = readManifestOrThrow(opts.dir);
|
|
7021
|
+
const moduleSlug = manifest.module?.slug;
|
|
7022
|
+
if (!moduleSlug) {
|
|
7023
|
+
throw new Error("module.json has no module.slug \u2014 the manifest is malformed.");
|
|
7024
|
+
}
|
|
7025
|
+
const cfg = readConfig();
|
|
7026
|
+
if (!cfg) {
|
|
7027
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
7028
|
+
}
|
|
7029
|
+
const entry = readModuleEntry(moduleSlug);
|
|
7030
|
+
if (!entry) {
|
|
7031
|
+
throw new Error(
|
|
7032
|
+
`No dev install cached for module "${moduleSlug}". Run \`dashwise init ${moduleSlug} --force\` to provision one.`
|
|
7033
|
+
);
|
|
7034
|
+
}
|
|
7035
|
+
return {
|
|
7036
|
+
installationId: entry.installationId,
|
|
7037
|
+
moduleSlug,
|
|
7038
|
+
...opts.dir !== void 0 ? { dir: opts.dir } : {},
|
|
7039
|
+
manifest,
|
|
7040
|
+
requestOptions: {
|
|
7041
|
+
baseUrl: resolveApiUrl(),
|
|
7042
|
+
auth: { token: cfg.token }
|
|
7043
|
+
}
|
|
7044
|
+
};
|
|
7045
|
+
}
|
|
7046
|
+
async function pushManifest(ctx) {
|
|
7047
|
+
let moduleId;
|
|
7048
|
+
try {
|
|
7049
|
+
const mod = await getModuleBySlug(ctx.moduleSlug, ctx.requestOptions);
|
|
7050
|
+
moduleId = mod.id;
|
|
7051
|
+
} catch (err) {
|
|
7052
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
7053
|
+
warn(
|
|
7054
|
+
`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.`
|
|
7055
|
+
);
|
|
7056
|
+
return false;
|
|
7057
|
+
}
|
|
7058
|
+
warn(`Couldn't resolve the module id to push the manifest: ${err.message}`);
|
|
7059
|
+
return false;
|
|
7060
|
+
}
|
|
7061
|
+
try {
|
|
7062
|
+
await applyDraftManifest(
|
|
7063
|
+
moduleId,
|
|
7064
|
+
ctx.manifest,
|
|
7065
|
+
ctx.requestOptions
|
|
7066
|
+
);
|
|
7067
|
+
success("Pushed the manifest \u2014 the backend parked the new slot, ready to bind.");
|
|
7068
|
+
return true;
|
|
7069
|
+
} catch (err) {
|
|
7070
|
+
if (err instanceof ApiError) {
|
|
7071
|
+
if (err.code === "MANIFEST_INVALID" || err.status === 400) {
|
|
7072
|
+
fail(`Manifest rejected by the backend validator: ${err.message}`);
|
|
7073
|
+
const issues = extractIssues(err.context);
|
|
7074
|
+
for (const issue of issues) out(` ${dim("-")} ${issue}`);
|
|
7075
|
+
} else {
|
|
7076
|
+
fail(`Manifest push failed: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
7077
|
+
}
|
|
7078
|
+
} else {
|
|
7079
|
+
fail(`Manifest push failed: ${err.message}`);
|
|
7080
|
+
}
|
|
7081
|
+
return false;
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
function extractIssues(context) {
|
|
7085
|
+
if (!context) return [];
|
|
7086
|
+
const raw = context.issues;
|
|
7087
|
+
if (!Array.isArray(raw)) return [];
|
|
7088
|
+
return raw.map((i) => {
|
|
7089
|
+
if (typeof i === "string") return i;
|
|
7090
|
+
if (i && typeof i === "object") {
|
|
7091
|
+
const o = i;
|
|
7092
|
+
const path = typeof o.path === "string" ? o.path : "";
|
|
7093
|
+
const message = typeof o.message === "string" ? o.message : JSON.stringify(i);
|
|
7094
|
+
return path ? `${path}: ${message}` : message;
|
|
7095
|
+
}
|
|
7096
|
+
return String(i);
|
|
7097
|
+
});
|
|
7098
|
+
}
|
|
7099
|
+
function generateFields(physical) {
|
|
7100
|
+
const fields = [];
|
|
7101
|
+
const skipped = [];
|
|
7102
|
+
for (const f of physical) {
|
|
7103
|
+
const abstract = abstractTypeForPhysical(f.type);
|
|
7104
|
+
if (abstract === null) {
|
|
7105
|
+
skipped.push(`${f.slug} (${f.type})`);
|
|
7106
|
+
continue;
|
|
7107
|
+
}
|
|
7108
|
+
const entry = { slug: f.slug, types: [abstract] };
|
|
7109
|
+
if (f.required) entry.required = true;
|
|
7110
|
+
fields.push(entry);
|
|
7111
|
+
}
|
|
7112
|
+
return { fields, skipped };
|
|
7113
|
+
}
|
|
7114
|
+
function parseOps(opsCsv) {
|
|
7115
|
+
if (opsCsv === void 0) return ["read"];
|
|
7116
|
+
const parsed = opsCsv.split(",").map((o) => o.trim()).filter(Boolean);
|
|
7117
|
+
if (parsed.length === 0) {
|
|
7118
|
+
throw new Error(`--ops must list at least one of ${USES_OPERATIONS.join(", ")}.`);
|
|
7119
|
+
}
|
|
7120
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7121
|
+
for (const op of parsed) {
|
|
7122
|
+
if (!USES_OPERATIONS.includes(op)) {
|
|
7123
|
+
throw new Error(
|
|
7124
|
+
`Invalid --ops value "${op}". Allowed: ${USES_OPERATIONS.join(", ")}.`
|
|
7125
|
+
);
|
|
7126
|
+
}
|
|
7127
|
+
seen.add(op);
|
|
7128
|
+
}
|
|
7129
|
+
return USES_OPERATIONS.filter((o) => seen.has(o));
|
|
7130
|
+
}
|
|
7131
|
+
function slugify(name) {
|
|
7132
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7133
|
+
}
|
|
7134
|
+
var KEBAB_REGEX = /^[a-z][a-z0-9-]*[a-z0-9]$/;
|
|
7135
|
+
function isKebab(slug) {
|
|
7136
|
+
return KEBAB_REGEX.test(slug);
|
|
7137
|
+
}
|
|
7138
|
+
function surfaceIntrospectError(err, tableRef) {
|
|
7139
|
+
if (err instanceof ApiError) {
|
|
7140
|
+
switch (err.code) {
|
|
7141
|
+
case "NON_NUMERIC_TABLE_REF":
|
|
7142
|
+
fail(err.message);
|
|
7143
|
+
return 1;
|
|
7144
|
+
case "NOT_FOUND":
|
|
7145
|
+
fail(
|
|
7146
|
+
`No workspace table with id ${code(tableRef)} is visible to you. Check the id (find it in the Dashwise UI URL, or via ${code("GET /api/tables/application/:applicationId")}).`
|
|
7147
|
+
);
|
|
7148
|
+
return 1;
|
|
7149
|
+
case "TABLE_IS_MODULE_MANAGED":
|
|
7150
|
+
fail(
|
|
7151
|
+
`Table ${code(tableRef)} is module-managed \u2014 ${code("uses")} is for plain WORKSPACE tables. Reach a module's own tables via ${code("dashwise deps")} instead.`
|
|
7152
|
+
);
|
|
7153
|
+
return 1;
|
|
7154
|
+
case "UNAUTHENTICATED":
|
|
7155
|
+
fail(
|
|
7156
|
+
`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
|
|
7157
|
+
);
|
|
7158
|
+
return 1;
|
|
7159
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
7160
|
+
case "FORBIDDEN":
|
|
7161
|
+
fail(`You're not a member of the workspace that owns table ${code(tableRef)}.`);
|
|
7162
|
+
return 1;
|
|
7163
|
+
default:
|
|
7164
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
7165
|
+
return 1;
|
|
7166
|
+
}
|
|
7167
|
+
}
|
|
7168
|
+
fail(`Table introspection failed: ${err.message}`);
|
|
7169
|
+
return 1;
|
|
7170
|
+
}
|
|
7171
|
+
|
|
7172
|
+
// src/commands/pull.ts
|
|
7173
|
+
init_api_client();
|
|
7174
|
+
init_manifest_io();
|
|
7175
|
+
init_schema_command_context();
|
|
7176
|
+
init_output();
|
|
7177
|
+
async function pullCommand(options) {
|
|
7178
|
+
let ctx;
|
|
7179
|
+
try {
|
|
7180
|
+
ctx = loadSchemaCommandContext({
|
|
7181
|
+
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
7182
|
+
});
|
|
7183
|
+
} catch (err) {
|
|
7184
|
+
fail(err.message);
|
|
7185
|
+
return 1;
|
|
7186
|
+
}
|
|
7187
|
+
info(`Pulling manifest for installation ${ctx.entry.installationId}\u2026`);
|
|
7188
|
+
let remoteManifest;
|
|
7189
|
+
try {
|
|
7190
|
+
const response = await getInstallationSchema(ctx.requestOptions);
|
|
7191
|
+
remoteManifest = response.manifest;
|
|
7192
|
+
} catch (err) {
|
|
7193
|
+
return surfacePullFailure(err);
|
|
7194
|
+
}
|
|
7195
|
+
const localJson = JSON.stringify(ctx.manifest, null, 2) + "\n";
|
|
7196
|
+
const remoteJson = JSON.stringify(remoteManifest, null, 2) + "\n";
|
|
7197
|
+
if (localJson === remoteJson) {
|
|
7198
|
+
success("Already in sync \u2014 local module.json matches backend.");
|
|
7199
|
+
return 0;
|
|
7200
|
+
}
|
|
7201
|
+
const localBytes = Buffer.byteLength(localJson, "utf8");
|
|
7202
|
+
const remoteBytes = Buffer.byteLength(remoteJson, "utf8");
|
|
7203
|
+
const delta = remoteBytes - localBytes;
|
|
7204
|
+
const deltaStr = delta === 0 ? "0 bytes" : `${delta > 0 ? "+" : ""}${delta} bytes`;
|
|
7205
|
+
if (options.dryRun) {
|
|
7206
|
+
info(
|
|
7207
|
+
`Drift detected (${deltaStr}). ${code("--dry-run")} \u2014 not writing.`
|
|
7208
|
+
);
|
|
7209
|
+
info(`Run ${code("dashwise pull")} without ${code("--dry-run")} to apply.`);
|
|
7210
|
+
return 0;
|
|
7211
|
+
}
|
|
7212
|
+
try {
|
|
7213
|
+
writeManifest(remoteManifest, ctx.projectRoot);
|
|
7214
|
+
} catch (err) {
|
|
7215
|
+
fail(`Failed to write ${manifestPath(ctx.projectRoot)}: ${err.message}`);
|
|
7216
|
+
return 1;
|
|
7217
|
+
}
|
|
7218
|
+
success(
|
|
7219
|
+
`Synced module.json from backend (${deltaStr}). ${dim(`Run \`dashwise module generate\` to refresh typed SDK if your watcher isn't running.`)}`
|
|
7220
|
+
);
|
|
7221
|
+
return 0;
|
|
7222
|
+
}
|
|
7223
|
+
function surfacePullFailure(err) {
|
|
7224
|
+
if (err instanceof ApiError) {
|
|
7225
|
+
switch (err.code) {
|
|
7226
|
+
case "UNAUTHENTICATED":
|
|
7227
|
+
fail(
|
|
7228
|
+
`Runtime token rejected. The cached token for this module may be expired \u2014 re-run \`dashwise init <slug> --force\` to mint a fresh one.`
|
|
7229
|
+
);
|
|
7230
|
+
break;
|
|
7231
|
+
case "SANDBOX_TOKEN_NOT_ACCEPTED":
|
|
7232
|
+
fail(
|
|
7233
|
+
`Schema-read endpoint requires a local-dev runtime token (kind=local). The cached token is the wrong kind \u2014 re-run \`dashwise init <slug> --force\`.`
|
|
7234
|
+
);
|
|
7235
|
+
break;
|
|
7236
|
+
case "INSTALL_MISMATCH":
|
|
7237
|
+
fail(
|
|
7238
|
+
`Runtime token is bound to a different installation than expected. The cached install id may be stale \u2014 re-run \`dashwise init <slug> --force\`.`
|
|
7239
|
+
);
|
|
7240
|
+
break;
|
|
7241
|
+
case "INSTALLATION_NOT_FOUND":
|
|
7242
|
+
fail(
|
|
7243
|
+
`The backend has no record of this installation. It may have been destroyed (GC, manual cleanup). Re-run \`dashwise init <slug>\` to provision a fresh one.`
|
|
7244
|
+
);
|
|
7245
|
+
break;
|
|
7246
|
+
default:
|
|
7247
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
7248
|
+
}
|
|
7249
|
+
return 1;
|
|
7250
|
+
}
|
|
7251
|
+
fail(`Pull failed: ${err.message}`);
|
|
7252
|
+
return 1;
|
|
7253
|
+
}
|
|
7254
|
+
|
|
7255
|
+
// src/bin.ts
|
|
7256
|
+
init_scaffold();
|
|
7257
|
+
|
|
7258
|
+
// src/commands/api-keys.ts
|
|
7259
|
+
init_api_client();
|
|
7260
|
+
init_config();
|
|
7261
|
+
init_manifest_io();
|
|
7262
|
+
init_output();
|
|
7263
|
+
function loadContext4(opts) {
|
|
7264
|
+
const manifest = readManifestOrThrow(opts.dir);
|
|
7265
|
+
const slug = manifest.module?.slug;
|
|
7266
|
+
if (!slug) {
|
|
7267
|
+
throw new Error(
|
|
7268
|
+
`module.json has no module.slug \u2014 the manifest is malformed.`
|
|
7269
|
+
);
|
|
7270
|
+
}
|
|
7271
|
+
const cfg = readConfig();
|
|
7272
|
+
if (!cfg) {
|
|
7273
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
7274
|
+
}
|
|
7275
|
+
const entry = readModuleEntry(slug);
|
|
7276
|
+
if (!entry) {
|
|
7277
|
+
throw new Error(
|
|
7278
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
|
|
7279
|
+
);
|
|
7280
|
+
}
|
|
7281
|
+
return {
|
|
7282
|
+
installationId: entry.installationId,
|
|
7283
|
+
slug,
|
|
7284
|
+
requestOptions: {
|
|
7285
|
+
baseUrl: resolveApiUrl(),
|
|
7286
|
+
auth: { token: cfg.token }
|
|
7287
|
+
}
|
|
7288
|
+
};
|
|
7289
|
+
}
|
|
7290
|
+
async function apiKeysListCommand(options) {
|
|
7291
|
+
let ctx;
|
|
7292
|
+
try {
|
|
7293
|
+
ctx = loadContext4(options);
|
|
7294
|
+
} catch (err) {
|
|
7295
|
+
fail(err.message);
|
|
7296
|
+
return 1;
|
|
7297
|
+
}
|
|
7298
|
+
let keys;
|
|
7299
|
+
try {
|
|
7300
|
+
const resp = await listApiKeys(ctx.installationId, ctx.requestOptions);
|
|
7301
|
+
keys = resp.api_keys;
|
|
7302
|
+
} catch (err) {
|
|
7303
|
+
return surfaceApiKeysError(err);
|
|
7304
|
+
}
|
|
7305
|
+
const filtered = options.all ? keys : keys.filter((k) => k.revoked_at === null);
|
|
7306
|
+
if (options.json) {
|
|
7307
|
+
process.stdout.write(`${JSON.stringify(filtered, null, 2)}
|
|
7308
|
+
`);
|
|
7309
|
+
return 0;
|
|
7310
|
+
}
|
|
7311
|
+
if (filtered.length === 0) {
|
|
7312
|
+
info(
|
|
7313
|
+
options.all ? `No API keys (active or revoked) for ${code(ctx.slug)}.` : `No active API keys for ${code(ctx.slug)}. Run ${code("dashwise api-keys create <name>")} to mint one, or ${code("dashwise api-keys list --all")} to include revoked.`
|
|
7314
|
+
);
|
|
7315
|
+
return 0;
|
|
7316
|
+
}
|
|
7317
|
+
info(`API keys for ${code(ctx.slug)} (install id=${ctx.installationId}):`);
|
|
7318
|
+
for (const k of filtered) {
|
|
7319
|
+
const status = formatKeyStatus(k);
|
|
7320
|
+
const lastUsed = k.last_used_at ? `last used ${formatRelative(new Date(k.last_used_at))}` : "never used";
|
|
7321
|
+
process.stdout.write(
|
|
7322
|
+
` ${code(`#${k.id}`)} ${k.name} ${dim(`dwk_${k.key_prefix}\u2026`)} ${status} ${dim(`(${lastUsed})`)}
|
|
7323
|
+
`
|
|
5888
7324
|
);
|
|
7325
|
+
const grantLines = formatGrants(k.grants);
|
|
7326
|
+
if (grantLines.length > 0) {
|
|
7327
|
+
for (const line of grantLines) {
|
|
7328
|
+
process.stdout.write(` ${dim(line)}
|
|
7329
|
+
`);
|
|
7330
|
+
}
|
|
7331
|
+
} else if (k.grants !== void 0) {
|
|
7332
|
+
process.stdout.write(` ${dim("full install access (no grants)")}
|
|
7333
|
+
`);
|
|
7334
|
+
}
|
|
5889
7335
|
}
|
|
5890
7336
|
return 0;
|
|
5891
7337
|
}
|
|
@@ -5895,20 +7341,30 @@ async function apiKeysCreateCommand(nameArg, options) {
|
|
|
5895
7341
|
fail("Usage: `dashwise api-keys create <name>` (e.g. `dashwise api-keys create production`)");
|
|
5896
7342
|
return 1;
|
|
5897
7343
|
}
|
|
5898
|
-
let
|
|
7344
|
+
let grants;
|
|
5899
7345
|
try {
|
|
5900
|
-
|
|
7346
|
+
grants = parseGrants(options.grant);
|
|
5901
7347
|
} catch (err) {
|
|
5902
7348
|
fail(err.message);
|
|
5903
7349
|
return 1;
|
|
5904
7350
|
}
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
7351
|
+
let expiresAt;
|
|
7352
|
+
try {
|
|
7353
|
+
expiresAt = resolveExpiresAt(options.expires);
|
|
7354
|
+
} catch (err) {
|
|
7355
|
+
fail(err.message);
|
|
7356
|
+
return 1;
|
|
5908
7357
|
}
|
|
5909
|
-
|
|
5910
|
-
|
|
7358
|
+
let ctx;
|
|
7359
|
+
try {
|
|
7360
|
+
ctx = loadContext4(options);
|
|
7361
|
+
} catch (err) {
|
|
7362
|
+
fail(err.message);
|
|
7363
|
+
return 1;
|
|
5911
7364
|
}
|
|
7365
|
+
const body = { name };
|
|
7366
|
+
if (grants !== void 0) body.grants = grants;
|
|
7367
|
+
if (expiresAt !== void 0) body.expires_at = expiresAt;
|
|
5912
7368
|
let resp;
|
|
5913
7369
|
try {
|
|
5914
7370
|
resp = await createApiKey(ctx.installationId, body, ctx.requestOptions);
|
|
@@ -5927,6 +7383,13 @@ async function apiKeysCreateCommand(nameArg, options) {
|
|
|
5927
7383
|
success(
|
|
5928
7384
|
`Created API key ${code(`#${resp.api_key.id}`)} (${resp.api_key.name}) for ${code(ctx.slug)}.`
|
|
5929
7385
|
);
|
|
7386
|
+
const grantLines = formatGrants(resp.api_key.grants);
|
|
7387
|
+
if (grantLines.length > 0) {
|
|
7388
|
+
info(` ${dim("scope:")} scoped to`);
|
|
7389
|
+
for (const line of grantLines) info(` ${line}`);
|
|
7390
|
+
} else {
|
|
7391
|
+
info(` ${dim("scope:")} full install access (no grants)`);
|
|
7392
|
+
}
|
|
5930
7393
|
info("");
|
|
5931
7394
|
warn(
|
|
5932
7395
|
"This is the ONLY time the full key is displayed. Save it now \u2014 the backend stores a hash, not the plaintext."
|
|
@@ -5935,14 +7398,9 @@ async function apiKeysCreateCommand(nameArg, options) {
|
|
|
5935
7398
|
process.stdout.write(` ${code(resp.raw_key)}
|
|
5936
7399
|
`);
|
|
5937
7400
|
info("");
|
|
5938
|
-
info(`Use in a deployed module's env:`);
|
|
7401
|
+
info(`Use in a deployed module's env (the two-var contract):`);
|
|
7402
|
+
info(` ${code(`DASHWISE_API_URL=${resolveApiUrl()}`)}`);
|
|
5939
7403
|
info(` ${code(`DASHWISE_API_KEY=${resp.raw_key.slice(0, 12)}\u2026`)}`);
|
|
5940
|
-
info(
|
|
5941
|
-
` ${code(`DASHWISE_INSTALLATION_ID=${ctx.installationId}`)}`
|
|
5942
|
-
);
|
|
5943
|
-
info(
|
|
5944
|
-
` ${code(`DASHWISE_API_URL=${resolveApiUrl()}`)}`
|
|
5945
|
-
);
|
|
5946
7404
|
return 0;
|
|
5947
7405
|
}
|
|
5948
7406
|
async function apiKeysRevokeCommand(keyIdArg, options) {
|
|
@@ -5957,7 +7415,7 @@ async function apiKeysRevokeCommand(keyIdArg, options) {
|
|
|
5957
7415
|
}
|
|
5958
7416
|
let ctx;
|
|
5959
7417
|
try {
|
|
5960
|
-
ctx =
|
|
7418
|
+
ctx = loadContext4(options);
|
|
5961
7419
|
} catch (err) {
|
|
5962
7420
|
fail(err.message);
|
|
5963
7421
|
return 1;
|
|
@@ -5991,6 +7449,17 @@ function surfaceApiKeysError(err) {
|
|
|
5991
7449
|
`You're not a member of this installation's workspace. API-key management requires workspace membership.`
|
|
5992
7450
|
);
|
|
5993
7451
|
return 1;
|
|
7452
|
+
case "NOT_WORKSPACE_ADMIN":
|
|
7453
|
+
case "FORBIDDEN":
|
|
7454
|
+
fail(
|
|
7455
|
+
`Not authorized. Minting scoped keys (with --grant) or keys on non-local installs requires workspace-admin; members can mint full-access keys on local dev installs only.`
|
|
7456
|
+
);
|
|
7457
|
+
return 1;
|
|
7458
|
+
case "INVALID_GRANT":
|
|
7459
|
+
fail(
|
|
7460
|
+
`A --grant referenced a table/app the backend rejected (${err.message}). Data Hub grants need a table or app that exists in this workspace; module grants need a table your manifest owns.`
|
|
7461
|
+
);
|
|
7462
|
+
return 1;
|
|
5994
7463
|
case "INSTALLATION_NOT_FOUND":
|
|
5995
7464
|
fail(
|
|
5996
7465
|
`Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
|
|
@@ -6009,6 +7478,117 @@ function surfaceApiKeysError(err) {
|
|
|
6009
7478
|
fail(`API-key operation failed: ${err.message}`);
|
|
6010
7479
|
return 1;
|
|
6011
7480
|
}
|
|
7481
|
+
var GRANT_OPERATIONS = ["create", "read", "update", "delete"];
|
|
7482
|
+
function parseGrants(specs) {
|
|
7483
|
+
if (!specs || specs.length === 0) return void 0;
|
|
7484
|
+
return specs.map((spec) => parseGrantSpec(spec));
|
|
7485
|
+
}
|
|
7486
|
+
function parseGrantSpec(spec) {
|
|
7487
|
+
const raw = spec.trim();
|
|
7488
|
+
const parts = raw.split(":");
|
|
7489
|
+
const source = parts[0];
|
|
7490
|
+
if (source === "module") {
|
|
7491
|
+
if (parts.length !== 3) {
|
|
7492
|
+
throw new Error(
|
|
7493
|
+
`Invalid --grant "${spec}". Module grants are \`module:<tableSlug>:<ops>\` (e.g. \`module:tasks:read,create\`).`
|
|
7494
|
+
);
|
|
7495
|
+
}
|
|
7496
|
+
const tableSlug = parts[1];
|
|
7497
|
+
if (!tableSlug) {
|
|
7498
|
+
throw new Error(`Invalid --grant "${spec}": missing table slug.`);
|
|
7499
|
+
}
|
|
7500
|
+
return {
|
|
7501
|
+
source: "module",
|
|
7502
|
+
tableSlug,
|
|
7503
|
+
operations: parseOps2(parts[2], spec)
|
|
7504
|
+
};
|
|
7505
|
+
}
|
|
7506
|
+
if (source === "datahub") {
|
|
7507
|
+
const scope = parts[1];
|
|
7508
|
+
if (!scope) {
|
|
7509
|
+
throw new Error(
|
|
7510
|
+
`Invalid --grant "${spec}". Data Hub grants are \`datahub:table=<id>:<ops>\`, \`datahub:app=<id>:<ops>\`, or \`datahub:workspace:<ops>\`.`
|
|
7511
|
+
);
|
|
7512
|
+
}
|
|
7513
|
+
if (scope === "workspace") {
|
|
7514
|
+
if (parts.length !== 3) {
|
|
7515
|
+
throw new Error(
|
|
7516
|
+
`Invalid --grant "${spec}". Workspace-wide Data Hub grants are \`datahub:workspace:<ops>\`.`
|
|
7517
|
+
);
|
|
7518
|
+
}
|
|
7519
|
+
return { source: "datahub", operations: parseOps2(parts[2], spec) };
|
|
7520
|
+
}
|
|
7521
|
+
const eq = scope.indexOf("=");
|
|
7522
|
+
const scopeKind = eq === -1 ? scope : scope.slice(0, eq);
|
|
7523
|
+
const scopeVal = eq === -1 ? "" : scope.slice(eq + 1);
|
|
7524
|
+
if (scopeKind !== "table" && scopeKind !== "app" || scopeVal === "") {
|
|
7525
|
+
throw new Error(
|
|
7526
|
+
`Invalid --grant "${spec}". Data Hub scope must be \`table=<id>\`, \`app=<id>\`, or \`workspace\`.`
|
|
7527
|
+
);
|
|
7528
|
+
}
|
|
7529
|
+
if (parts.length !== 3) {
|
|
7530
|
+
throw new Error(
|
|
7531
|
+
`Invalid --grant "${spec}". Data Hub grants are \`datahub:${scopeKind}=<id>:<ops>\`.`
|
|
7532
|
+
);
|
|
7533
|
+
}
|
|
7534
|
+
const id = Number.parseInt(scopeVal, 10);
|
|
7535
|
+
if (!Number.isInteger(id) || id <= 0 || String(id) !== scopeVal) {
|
|
7536
|
+
throw new Error(
|
|
7537
|
+
`Invalid --grant "${spec}": ${scopeKind} id must be a positive integer (got "${scopeVal}").`
|
|
7538
|
+
);
|
|
7539
|
+
}
|
|
7540
|
+
const operations = parseOps2(parts[2], spec);
|
|
7541
|
+
return scopeKind === "table" ? { source: "datahub", tableId: id, operations } : { source: "datahub", applicationId: id, operations };
|
|
7542
|
+
}
|
|
7543
|
+
throw new Error(
|
|
7544
|
+
`Invalid --grant "${spec}": unknown source "${source}". Use \`module:\u2026\` or \`datahub:\u2026\`.`
|
|
7545
|
+
);
|
|
7546
|
+
}
|
|
7547
|
+
function parseOps2(opsCsv, spec) {
|
|
7548
|
+
const ops = opsCsv.split(",").map((o) => o.trim()).filter(Boolean);
|
|
7549
|
+
if (ops.length === 0) {
|
|
7550
|
+
throw new Error(
|
|
7551
|
+
`Invalid --grant "${spec}": at least one operation is required (${GRANT_OPERATIONS.join(", ")}).`
|
|
7552
|
+
);
|
|
7553
|
+
}
|
|
7554
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7555
|
+
for (const op of ops) {
|
|
7556
|
+
if (!GRANT_OPERATIONS.includes(op)) {
|
|
7557
|
+
throw new Error(
|
|
7558
|
+
`Invalid --grant "${spec}": unknown operation "${op}". Allowed: ${GRANT_OPERATIONS.join(", ")}.`
|
|
7559
|
+
);
|
|
7560
|
+
}
|
|
7561
|
+
seen.add(op);
|
|
7562
|
+
}
|
|
7563
|
+
return GRANT_OPERATIONS.filter((op) => seen.has(op));
|
|
7564
|
+
}
|
|
7565
|
+
function resolveExpiresAt(days) {
|
|
7566
|
+
if (days === void 0) return void 0;
|
|
7567
|
+
if (!Number.isFinite(days) || days <= 0) {
|
|
7568
|
+
throw new Error(`--expires must be a positive number of days (got ${days}).`);
|
|
7569
|
+
}
|
|
7570
|
+
return new Date(Date.now() + days * 24 * 60 * 60 * 1e3).toISOString();
|
|
7571
|
+
}
|
|
7572
|
+
function formatGrants(grants) {
|
|
7573
|
+
if (!grants || grants.length === 0) return [];
|
|
7574
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
7575
|
+
for (const g of grants) {
|
|
7576
|
+
const label = grantScopeLabel(g);
|
|
7577
|
+
const existing = byScope.get(label);
|
|
7578
|
+
if (existing) existing.ops.add(g.operation);
|
|
7579
|
+
else byScope.set(label, { label, ops: /* @__PURE__ */ new Set([g.operation]) });
|
|
7580
|
+
}
|
|
7581
|
+
return [...byScope.values()].map((s) => {
|
|
7582
|
+
const ops = GRANT_OPERATIONS.filter((o) => s.ops.has(o));
|
|
7583
|
+
return `${s.label}: ${ops.join(", ")}`;
|
|
7584
|
+
});
|
|
7585
|
+
}
|
|
7586
|
+
function grantScopeLabel(g) {
|
|
7587
|
+
if (g.source === "module") return `module ${g.table_slug ?? "?"}`;
|
|
7588
|
+
if (g.table_id != null) return `datahub table#${g.table_id}`;
|
|
7589
|
+
if (g.application_id != null) return `datahub app#${g.application_id}`;
|
|
7590
|
+
return "datahub workspace";
|
|
7591
|
+
}
|
|
6012
7592
|
function formatKeyStatus(k) {
|
|
6013
7593
|
if (k.revoked_at) return dim("[revoked]");
|
|
6014
7594
|
if (k.expires_at && new Date(k.expires_at).getTime() <= Date.now()) {
|
|
@@ -6028,6 +7608,190 @@ function formatRelative(d) {
|
|
|
6028
7608
|
return `${day}d ago`;
|
|
6029
7609
|
}
|
|
6030
7610
|
|
|
7611
|
+
// src/commands/origins.ts
|
|
7612
|
+
init_api_client();
|
|
7613
|
+
init_config();
|
|
7614
|
+
init_manifest_io();
|
|
7615
|
+
init_output();
|
|
7616
|
+
function loadContext5(opts) {
|
|
7617
|
+
const manifest = readManifestOrThrow(opts.dir);
|
|
7618
|
+
const slug = manifest.module?.slug;
|
|
7619
|
+
if (!slug) {
|
|
7620
|
+
throw new Error(
|
|
7621
|
+
`module.json has no module.slug \u2014 the manifest is malformed.`
|
|
7622
|
+
);
|
|
7623
|
+
}
|
|
7624
|
+
const cfg = readConfig();
|
|
7625
|
+
if (!cfg) {
|
|
7626
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
7627
|
+
}
|
|
7628
|
+
const entry = readModuleEntry(slug);
|
|
7629
|
+
if (!entry) {
|
|
7630
|
+
throw new Error(
|
|
7631
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
|
|
7632
|
+
);
|
|
7633
|
+
}
|
|
7634
|
+
return {
|
|
7635
|
+
installationId: entry.installationId,
|
|
7636
|
+
slug,
|
|
7637
|
+
requestOptions: {
|
|
7638
|
+
baseUrl: resolveApiUrl(),
|
|
7639
|
+
auth: { token: cfg.token }
|
|
7640
|
+
}
|
|
7641
|
+
};
|
|
7642
|
+
}
|
|
7643
|
+
function normalizeOrigin(raw) {
|
|
7644
|
+
const trimmed = raw.trim();
|
|
7645
|
+
try {
|
|
7646
|
+
return new URL(trimmed).origin;
|
|
7647
|
+
} catch {
|
|
7648
|
+
return trimmed;
|
|
7649
|
+
}
|
|
7650
|
+
}
|
|
7651
|
+
async function originsListCommand(options) {
|
|
7652
|
+
let ctx;
|
|
7653
|
+
try {
|
|
7654
|
+
ctx = loadContext5(options);
|
|
7655
|
+
} catch (err) {
|
|
7656
|
+
fail(err.message);
|
|
7657
|
+
return 1;
|
|
7658
|
+
}
|
|
7659
|
+
let origins;
|
|
7660
|
+
try {
|
|
7661
|
+
const resp = await getOrigins(ctx.installationId, ctx.requestOptions);
|
|
7662
|
+
origins = resp.allowed_origins;
|
|
7663
|
+
} catch (err) {
|
|
7664
|
+
return surfaceOriginsError(err);
|
|
7665
|
+
}
|
|
7666
|
+
if (options.json) {
|
|
7667
|
+
process.stdout.write(`${JSON.stringify(origins, null, 2)}
|
|
7668
|
+
`);
|
|
7669
|
+
return 0;
|
|
7670
|
+
}
|
|
7671
|
+
info(
|
|
7672
|
+
`SSO callback origins for ${code(ctx.slug)} ${dim(
|
|
7673
|
+
"(localhost is always allowed for local dev installs)"
|
|
7674
|
+
)}:`
|
|
7675
|
+
);
|
|
7676
|
+
if (origins.length === 0) {
|
|
7677
|
+
info(` ${dim("(none configured \u2014 only implicit localhost applies)")}`);
|
|
7678
|
+
info("");
|
|
7679
|
+
info(
|
|
7680
|
+
`Add a deployed domain with ${code("dashwise origins add https://your-domain")}.`
|
|
7681
|
+
);
|
|
7682
|
+
return 0;
|
|
7683
|
+
}
|
|
7684
|
+
for (const o of origins) {
|
|
7685
|
+
process.stdout.write(` ${o}
|
|
7686
|
+
`);
|
|
7687
|
+
}
|
|
7688
|
+
return 0;
|
|
7689
|
+
}
|
|
7690
|
+
async function originsAddCommand(urlArg, options) {
|
|
7691
|
+
if (!urlArg) {
|
|
7692
|
+
fail("Usage: `dashwise origins add <url>` (e.g. `dashwise origins add https://app.example.com`)");
|
|
7693
|
+
return 1;
|
|
7694
|
+
}
|
|
7695
|
+
const origin = normalizeOrigin(urlArg);
|
|
7696
|
+
let ctx;
|
|
7697
|
+
try {
|
|
7698
|
+
ctx = loadContext5(options);
|
|
7699
|
+
} catch (err) {
|
|
7700
|
+
fail(err.message);
|
|
7701
|
+
return 1;
|
|
7702
|
+
}
|
|
7703
|
+
let current;
|
|
7704
|
+
try {
|
|
7705
|
+
current = (await getOrigins(ctx.installationId, ctx.requestOptions)).allowed_origins;
|
|
7706
|
+
} catch (err) {
|
|
7707
|
+
return surfaceOriginsError(err);
|
|
7708
|
+
}
|
|
7709
|
+
if (current.some((o) => normalizeOrigin(o) === origin)) {
|
|
7710
|
+
info(`${code(origin)} is already allowlisted for ${code(ctx.slug)}. Nothing to do.`);
|
|
7711
|
+
return 0;
|
|
7712
|
+
}
|
|
7713
|
+
const next = [...current, origin];
|
|
7714
|
+
try {
|
|
7715
|
+
const resp = await setOrigins(ctx.installationId, next, ctx.requestOptions);
|
|
7716
|
+
success(`Added ${code(origin)} to the SSO callback allowlist for ${code(ctx.slug)}.`);
|
|
7717
|
+
info(` ${dim(`${resp.allowed_origins.length} origin(s) now configured.`)}`);
|
|
7718
|
+
} catch (err) {
|
|
7719
|
+
return surfaceOriginsError(err);
|
|
7720
|
+
}
|
|
7721
|
+
return 0;
|
|
7722
|
+
}
|
|
7723
|
+
async function originsRemoveCommand(urlArg, options) {
|
|
7724
|
+
if (!urlArg) {
|
|
7725
|
+
fail("Usage: `dashwise origins remove <url>`");
|
|
7726
|
+
return 1;
|
|
7727
|
+
}
|
|
7728
|
+
const origin = normalizeOrigin(urlArg);
|
|
7729
|
+
let ctx;
|
|
7730
|
+
try {
|
|
7731
|
+
ctx = loadContext5(options);
|
|
7732
|
+
} catch (err) {
|
|
7733
|
+
fail(err.message);
|
|
7734
|
+
return 1;
|
|
7735
|
+
}
|
|
7736
|
+
let current;
|
|
7737
|
+
try {
|
|
7738
|
+
current = (await getOrigins(ctx.installationId, ctx.requestOptions)).allowed_origins;
|
|
7739
|
+
} catch (err) {
|
|
7740
|
+
return surfaceOriginsError(err);
|
|
7741
|
+
}
|
|
7742
|
+
const next = current.filter((o) => normalizeOrigin(o) !== origin);
|
|
7743
|
+
if (next.length === current.length) {
|
|
7744
|
+
fail(
|
|
7745
|
+
`${code(origin)} is not in the allowlist for ${code(ctx.slug)}. Run ${code("dashwise origins list")} to see current entries.`
|
|
7746
|
+
);
|
|
7747
|
+
return 1;
|
|
7748
|
+
}
|
|
7749
|
+
try {
|
|
7750
|
+
await setOrigins(ctx.installationId, next, ctx.requestOptions);
|
|
7751
|
+
success(`Removed ${code(origin)} from the SSO callback allowlist for ${code(ctx.slug)}.`);
|
|
7752
|
+
} catch (err) {
|
|
7753
|
+
return surfaceOriginsError(err);
|
|
7754
|
+
}
|
|
7755
|
+
return 0;
|
|
7756
|
+
}
|
|
7757
|
+
function surfaceOriginsError(err) {
|
|
7758
|
+
if (err instanceof ApiError) {
|
|
7759
|
+
switch (err.code) {
|
|
7760
|
+
case "UNAUTHENTICATED":
|
|
7761
|
+
fail(
|
|
7762
|
+
`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
|
|
7763
|
+
);
|
|
7764
|
+
return 1;
|
|
7765
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
7766
|
+
fail(
|
|
7767
|
+
`You're not a member of this installation's workspace. Origins management requires workspace membership.`
|
|
7768
|
+
);
|
|
7769
|
+
return 1;
|
|
7770
|
+
case "NOT_WORKSPACE_ADMIN":
|
|
7771
|
+
case "FORBIDDEN":
|
|
7772
|
+
fail(
|
|
7773
|
+
`Not authorized. Managing SSO callback origins requires workspace-admin.`
|
|
7774
|
+
);
|
|
7775
|
+
return 1;
|
|
7776
|
+
case "INVALID_ORIGIN":
|
|
7777
|
+
fail(
|
|
7778
|
+
`The backend rejected an origin (${err.message}). Non-localhost origins must be absolute https:// URLs.`
|
|
7779
|
+
);
|
|
7780
|
+
return 1;
|
|
7781
|
+
case "INSTALLATION_NOT_FOUND":
|
|
7782
|
+
fail(
|
|
7783
|
+
`Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
|
|
7784
|
+
);
|
|
7785
|
+
return 1;
|
|
7786
|
+
default:
|
|
7787
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
7788
|
+
return 1;
|
|
7789
|
+
}
|
|
7790
|
+
}
|
|
7791
|
+
fail(`Origins operation failed: ${err.message}`);
|
|
7792
|
+
return 1;
|
|
7793
|
+
}
|
|
7794
|
+
|
|
6031
7795
|
// src/commands/env-export.ts
|
|
6032
7796
|
init_api_client();
|
|
6033
7797
|
init_config();
|
|
@@ -6117,16 +7881,6 @@ async function envExportCommand(options) {
|
|
|
6117
7881
|
value: apiUrl,
|
|
6118
7882
|
comment: "# DashWise backend URL \u2014 same value the CLI is logged into."
|
|
6119
7883
|
},
|
|
6120
|
-
{
|
|
6121
|
-
key: "NEXT_PUBLIC_DASHWISE_API_URL",
|
|
6122
|
-
value: apiUrl,
|
|
6123
|
-
comment: "# Client-side mirror \u2014 Next.js inlines NEXT_PUBLIC_* into the browser bundle so <AuthProvider> hits the right origin."
|
|
6124
|
-
},
|
|
6125
|
-
{
|
|
6126
|
-
key: "DASHWISE_INSTALLATION_ID",
|
|
6127
|
-
value: String(installationId),
|
|
6128
|
-
comment: `# Per-workspace install id for module "${slug}".`
|
|
6129
|
-
},
|
|
6130
7884
|
{
|
|
6131
7885
|
key: "DASHWISE_API_KEY",
|
|
6132
7886
|
value: apiKey,
|
|
@@ -6947,8 +8701,15 @@ moduleCmd.command("clone <slug>").description("Clone a published module locally.
|
|
|
6947
8701
|
});
|
|
6948
8702
|
}
|
|
6949
8703
|
);
|
|
6950
|
-
var depsCmd = program.command("deps").description(
|
|
6951
|
-
|
|
8704
|
+
var depsCmd = program.command("deps").description(
|
|
8705
|
+
"Cross-module dependency commands. Bare `dashwise deps` shows the live connect status (resolved / unbound) of each declared dep on your installation; the subcommands manage module.json#dependencies (add / remove / list / check) and the connect lifecycle (bind / unbind)."
|
|
8706
|
+
).option("--dir <path>", "Project root (default: current directory).").option("--json", "Emit the connect-status list as JSON instead of the human-readable view.").action(async (cmdOpts) => {
|
|
8707
|
+
process.exitCode = await depsConnectListCommand({
|
|
8708
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
8709
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
8710
|
+
});
|
|
8711
|
+
});
|
|
8712
|
+
depsCmd.command("list").description("List declared cross-module dependencies (offline local module.json read).").option("--dir <path>", "Project root (default: current directory).").option("--json", "Emit JSON to stdout instead of human-readable columns.").action(async (cmdOpts) => {
|
|
6952
8713
|
process.exitCode = await depsListCommand({
|
|
6953
8714
|
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
6954
8715
|
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
@@ -6980,6 +8741,93 @@ depsCmd.command("check").description("Verify each declared dep still resolves to
|
|
|
6980
8741
|
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
6981
8742
|
});
|
|
6982
8743
|
});
|
|
8744
|
+
depsCmd.command("bind <provider-slug>").description(
|
|
8745
|
+
"(Re-)connect a parked dependency to its provider installation. Re-runs resolution + snapshots the contract; on success runtime reads succeed. Fails (the dep stays unbound) with the park reason if the provider isn't installed / the version doesn't match / a table/action isn't exposed. Workspace-admin."
|
|
8746
|
+
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
8747
|
+
"--installation <id>",
|
|
8748
|
+
"Provider installation id to bind to (disambiguates when the workspace has more than one candidate install of the provider).",
|
|
8749
|
+
parseIntOption
|
|
8750
|
+
).action(
|
|
8751
|
+
async (providerSlug, cmdOpts) => {
|
|
8752
|
+
process.exitCode = await depsBindCommand(providerSlug, {
|
|
8753
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
8754
|
+
...cmdOpts.installation !== void 0 ? { installation: cmdOpts.installation } : {}
|
|
8755
|
+
});
|
|
8756
|
+
}
|
|
8757
|
+
);
|
|
8758
|
+
depsCmd.command("unbind <provider-slug>").description(
|
|
8759
|
+
"Disconnect a bound dependency: NULLs the provider ref + contract snapshot and parks the dep (reason MANUALLY_UNBOUND). Runtime reads then return DEP_UNBOUND until you bind it again. Workspace-admin."
|
|
8760
|
+
).option("--dir <path>", "Project root (default: current directory).").action(async (providerSlug, cmdOpts) => {
|
|
8761
|
+
process.exitCode = await depsUnbindCommand(providerSlug, {
|
|
8762
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
8763
|
+
});
|
|
8764
|
+
});
|
|
8765
|
+
program.command("bind [uses-slug]").description(
|
|
8766
|
+
"Connect the module's declared workspace-table (`uses`) slots to real tables. Bare `dashwise bind` lists every slot's status (bound / unbound / declined) with reasons + missing fields. `dashwise bind <uses-slug> [--table <name-or-id>]` binds one (interactive pick when the match is ambiguous in a TTY); `--map <usesField>=<physicalFieldName>` overrides field mappings; `--create` materializes a fresh table; `--decline` declines an optional-needs slot. Bind/unbind/decline are workspace-admin."
|
|
8767
|
+
).option("--dir <path>", "Project root (default: current directory).").option("--json", "With no slug: emit the binding-status list as JSON.").option(
|
|
8768
|
+
"--table <name-or-id>",
|
|
8769
|
+
"Workspace table (name or numeric id) to bind the slot to. Use for scripts / to skip the interactive pick."
|
|
8770
|
+
).option(
|
|
8771
|
+
"--map <usesField=physicalFieldName>",
|
|
8772
|
+
"Override a field mapping (repeatable). The server auto-completes unmapped fields by name match.",
|
|
8773
|
+
collectRepeated,
|
|
8774
|
+
[]
|
|
8775
|
+
).option("--create", "Materialize a fresh Data Hub table from the declared fields + bind to it.").option("--decline", "Decline this slot (optional-needs only; the module runs without it).").action(
|
|
8776
|
+
async (usesSlug, cmdOpts) => {
|
|
8777
|
+
if (usesSlug === void 0) {
|
|
8778
|
+
process.exitCode = await bindListCommand({
|
|
8779
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
8780
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
8781
|
+
});
|
|
8782
|
+
return;
|
|
8783
|
+
}
|
|
8784
|
+
process.exitCode = await bindCommand(usesSlug, {
|
|
8785
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
8786
|
+
...cmdOpts.table !== void 0 ? { table: cmdOpts.table } : {},
|
|
8787
|
+
...cmdOpts.map !== void 0 && cmdOpts.map.length > 0 ? { map: cmdOpts.map } : {},
|
|
8788
|
+
...cmdOpts.create !== void 0 ? { create: cmdOpts.create } : {},
|
|
8789
|
+
...cmdOpts.decline !== void 0 ? { decline: cmdOpts.decline } : {}
|
|
8790
|
+
});
|
|
8791
|
+
}
|
|
8792
|
+
);
|
|
8793
|
+
program.command("unbind <uses-slug>").description(
|
|
8794
|
+
"Disconnect a bound workspace-table (`uses`) slot: NULLs the table ref + field map and parks the slot (reason MANUALLY_UNBOUND). Runtime reads/writes then return USES_UNBOUND until you bind it again. Workspace-admin."
|
|
8795
|
+
).option("--dir <path>", "Project root (default: current directory).").action(async (usesSlug, cmdOpts) => {
|
|
8796
|
+
process.exitCode = await unbindCommand(usesSlug, {
|
|
8797
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
8798
|
+
});
|
|
8799
|
+
});
|
|
8800
|
+
var usesCmd = program.command("uses").description("Workspace-table (`uses`) contract commands.");
|
|
8801
|
+
usesCmd.command("add").description(
|
|
8802
|
+
'On-ramp an existing workspace table: introspect it, generate the `uses.tables` manifest entry (fields + abstract types, read op by default), write module.json, push (draft-apply reconciles + parks), then bind the new slot to that table. One command from "I have an Employees table" to typed access.'
|
|
8803
|
+
).option("--dir <path>", "Project root (default: current directory).").requiredOption(
|
|
8804
|
+
"--from-table <id>",
|
|
8805
|
+
"The existing workspace table to generate a `uses` contract from \u2014 its NUMERIC id (find it in the Dashwise UI URL). Resolving by table name isn't wired yet."
|
|
8806
|
+
).option(
|
|
8807
|
+
"--slug <slug>",
|
|
8808
|
+
"Override the generated `uses` slot slug (default: slugified table name)."
|
|
8809
|
+
).option(
|
|
8810
|
+
"--ops <list>",
|
|
8811
|
+
"Comma-separated operations to declare (create,read,update,delete). Default: read."
|
|
8812
|
+
).option(
|
|
8813
|
+
"--purpose <text>",
|
|
8814
|
+
"Optional author-facing explanation, shown to admins at bind/consent."
|
|
8815
|
+
).option(
|
|
8816
|
+
"--no-bind",
|
|
8817
|
+
"Only write + push the manifest entry; don't attempt to bind it to the source table."
|
|
8818
|
+
).option("-y, --yes", "Skip the overwrite-existing-slot confirmation prompt.").action(
|
|
8819
|
+
async (cmdOpts) => {
|
|
8820
|
+
process.exitCode = await usesAddCommand({
|
|
8821
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
8822
|
+
fromTable: cmdOpts.fromTable,
|
|
8823
|
+
...cmdOpts.slug !== void 0 ? { slug: cmdOpts.slug } : {},
|
|
8824
|
+
...cmdOpts.ops !== void 0 ? { ops: cmdOpts.ops } : {},
|
|
8825
|
+
...cmdOpts.purpose !== void 0 ? { purpose: cmdOpts.purpose } : {},
|
|
8826
|
+
...cmdOpts.bind === false ? { noBind: true } : {},
|
|
8827
|
+
...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
|
|
8828
|
+
});
|
|
8829
|
+
}
|
|
8830
|
+
);
|
|
6983
8831
|
program.command("query").description(
|
|
6984
8832
|
'Run a typed query against the local module installation via @dashai/sdk/query. Pass one of `--inline "<expr>"`, `--file <path>`, or `--repl`. `qb`, `deps`, `db` are pre-bound to the local module\'s generated SDK.'
|
|
6985
8833
|
).option("--dir <path>", "Project root containing `module.json` (default: current directory).").option("--inline <expression>", "Evaluate a one-liner. e.g., `qb.tasks.where({done:false}).count()`.").option("--file <path>", "Dynamic-import a .mjs/.js file. Default export = Query | AggQuery | Promise | value.").option("--repl", "Open an interactive Node REPL with the generated SDK pre-bound. Top-level await supported.").option("--json", "Emit compact JSON (default: pretty-printed).").option(
|
|
@@ -7024,11 +8872,14 @@ apiKeysCmd.command("list").description(
|
|
|
7024
8872
|
apiKeysCmd.command("create [name]").description(
|
|
7025
8873
|
"Mint a new API key. The plaintext key is displayed ONCE \u2014 save it then."
|
|
7026
8874
|
).option("--dir <path>", "Project root (default: current directory).").option("--name <name>", "Override the positional name arg (e.g. for scripted use).").option(
|
|
7027
|
-
"--
|
|
7028
|
-
"
|
|
8875
|
+
"--grant <spec>",
|
|
8876
|
+
"Scoped grant (repeatable). `module:<tableSlug>:<ops>`, `datahub:table=<id>:<ops>`, `datahub:app=<id>:<ops>`, or `datahub:workspace:<ops>` where <ops> is a comma list of create,read,update,delete. Any grant mints a scoped (default-deny) key (workspace-admin required). Omit for a full-access key.",
|
|
8877
|
+
collectRepeated,
|
|
8878
|
+
[]
|
|
7029
8879
|
).option(
|
|
7030
|
-
"--expires
|
|
7031
|
-
"
|
|
8880
|
+
"--expires <days>",
|
|
8881
|
+
"Days until the key expires (converted to an absolute timestamp). Default: never expires.",
|
|
8882
|
+
parseIntOption
|
|
7032
8883
|
).option(
|
|
7033
8884
|
"--raw-only",
|
|
7034
8885
|
"Print only the plaintext key on stdout; chatter goes to stderr. Useful for shell capture: `KEY=$(dashwise api-keys create prod --raw-only)`"
|
|
@@ -7037,8 +8888,8 @@ apiKeysCmd.command("create [name]").description(
|
|
|
7037
8888
|
process.exitCode = await apiKeysCreateCommand(nameArg, {
|
|
7038
8889
|
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
7039
8890
|
...cmdOpts.name !== void 0 ? { name: cmdOpts.name } : {},
|
|
7040
|
-
...cmdOpts.
|
|
7041
|
-
...cmdOpts.
|
|
8891
|
+
...cmdOpts.grant !== void 0 && cmdOpts.grant.length > 0 ? { grant: cmdOpts.grant } : {},
|
|
8892
|
+
...cmdOpts.expires !== void 0 ? { expires: cmdOpts.expires } : {},
|
|
7042
8893
|
...cmdOpts.rawOnly !== void 0 ? { rawOnly: cmdOpts.rawOnly } : {}
|
|
7043
8894
|
});
|
|
7044
8895
|
}
|
|
@@ -7053,6 +8904,27 @@ apiKeysCmd.command("revoke [key-id]").description(
|
|
|
7053
8904
|
});
|
|
7054
8905
|
}
|
|
7055
8906
|
);
|
|
8907
|
+
var originsCmd = program.command("origins").description(
|
|
8908
|
+
"Manage the current module install's SSO callback-origin allowlist. Local dev (localhost) is always allowed; add deployed domains here."
|
|
8909
|
+
);
|
|
8910
|
+
originsCmd.command("list").description("Show the SSO callback origins configured for the current module install.").option("--dir <path>", "Project root (default: current directory).").option("--json", "Emit a JSON array instead of the human-friendly list.").action(async (cmdOpts) => {
|
|
8911
|
+
process.exitCode = await originsListCommand({
|
|
8912
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
8913
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
8914
|
+
});
|
|
8915
|
+
});
|
|
8916
|
+
originsCmd.command("add <url>").description(
|
|
8917
|
+
"Add an SSO callback origin (e.g. https://app.example.com). Idempotent; non-localhost origins must be https://."
|
|
8918
|
+
).option("--dir <path>", "Project root (default: current directory).").action(async (url, cmdOpts) => {
|
|
8919
|
+
process.exitCode = await originsAddCommand(url, {
|
|
8920
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
8921
|
+
});
|
|
8922
|
+
});
|
|
8923
|
+
originsCmd.command("remove <url>").description("Remove an SSO callback origin from the allowlist.").option("--dir <path>", "Project root (default: current directory).").action(async (url, cmdOpts) => {
|
|
8924
|
+
process.exitCode = await originsRemoveCommand(url, {
|
|
8925
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
8926
|
+
});
|
|
8927
|
+
});
|
|
7056
8928
|
var envCmd = program.command("env").description("Environment-variable helpers for deploying a module.");
|
|
7057
8929
|
envCmd.command("export").description(
|
|
7058
8930
|
"Emit the env vars a deployed module needs to authenticate against DashWise. By default emits .env-style KEY=value lines; --format = env | json | railway | vercel | fly to wrap in platform-native syntax. Use --new-key <name> to mint + inline an API key in one step."
|
|
@@ -7266,7 +9138,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
|
|
|
7266
9138
|
}
|
|
7267
9139
|
})();
|
|
7268
9140
|
function getVersion() {
|
|
7269
|
-
return "0.
|
|
9141
|
+
return "0.9.0";
|
|
7270
9142
|
}
|
|
7271
9143
|
function parseIntOption(value) {
|
|
7272
9144
|
const n = Number.parseInt(value, 10);
|