@dashai/cli 0.8.1 → 0.10.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 +2363 -398
- 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,19 @@ function manifestJson(opts) {
|
|
|
409
541
|
},
|
|
410
542
|
module_kind: kind,
|
|
411
543
|
owns: {
|
|
412
|
-
tables: [
|
|
544
|
+
tables: [
|
|
545
|
+
{
|
|
546
|
+
// NOTE: no `name` key here — the backend manifest validator
|
|
547
|
+
// (POST /api/installations/dev) is strict and rejects unrecognized
|
|
548
|
+
// table keys, and it derives the display name from the slug. A
|
|
549
|
+
// `name` here would fail provisioning's table materialization (#4).
|
|
550
|
+
slug: "items",
|
|
551
|
+
fields: [
|
|
552
|
+
{ slug: "title", type: "text" },
|
|
553
|
+
{ slug: "done", type: "boolean" }
|
|
554
|
+
]
|
|
555
|
+
}
|
|
556
|
+
]
|
|
413
557
|
}
|
|
414
558
|
};
|
|
415
559
|
if (kind === "custom") {
|
|
@@ -440,7 +584,7 @@ function packageJson(opts) {
|
|
|
440
584
|
start: "next start"
|
|
441
585
|
},
|
|
442
586
|
dependencies: {
|
|
443
|
-
"@dashai/sdk": "^0.
|
|
587
|
+
"@dashai/sdk": "^2.0.0",
|
|
444
588
|
next: "^15.0.0",
|
|
445
589
|
react: "^19.0.0",
|
|
446
590
|
"react-dom": "^19.0.0"
|
|
@@ -485,9 +629,27 @@ function tsconfigJson() {
|
|
|
485
629
|
}
|
|
486
630
|
function nextConfig() {
|
|
487
631
|
return `/** @type {import("next").NextConfig} */
|
|
632
|
+
//
|
|
633
|
+
// Phase 2 two-var contract: the deploy env supplies exactly
|
|
634
|
+
// DASHWISE_API_URL + DASHWISE_API_KEY. The browser needs the API URL
|
|
635
|
+
// too (the SDK's <AuthProvider> calls /api/auth/sessions/me on it), so
|
|
636
|
+
// we DERIVE NEXT_PUBLIC_DASHWISE_API_URL here from DASHWISE_API_URL at
|
|
637
|
+
// build/start time \u2014 you never set the NEXT_PUBLIC_ var yourself. If
|
|
638
|
+
// you already have one set (e.g. a custom setup), it wins.
|
|
639
|
+
const apiUrl = process.env.DASHWISE_API_URL;
|
|
640
|
+
|
|
488
641
|
const nextConfig = {
|
|
642
|
+
// Required for the platform-hosted (per-install Lambda) runtime: the
|
|
643
|
+
// injected wrapper probes [.next/standalone/server.js, server.js] and
|
|
644
|
+
// exit(1)s the cold start if neither exists. Without this the built
|
|
645
|
+
// Lambda FATALs. Harmless for local \`next dev\` / \`next start\`.
|
|
646
|
+
output: 'standalone',
|
|
489
647
|
typescript: { ignoreBuildErrors: true },
|
|
490
648
|
eslint: { ignoreDuringBuilds: true },
|
|
649
|
+
env: {
|
|
650
|
+
NEXT_PUBLIC_DASHWISE_API_URL:
|
|
651
|
+
process.env.NEXT_PUBLIC_DASHWISE_API_URL ?? apiUrl,
|
|
652
|
+
},
|
|
491
653
|
};
|
|
492
654
|
export default nextConfig;
|
|
493
655
|
`;
|
|
@@ -531,16 +693,18 @@ function gitignore() {
|
|
|
531
693
|
function envLocalExample(apiUrl = SCAFFOLD_DEFAULT_API_URL) {
|
|
532
694
|
return [
|
|
533
695
|
"# Copy to .env.local + fill in real values.",
|
|
534
|
-
"#
|
|
535
|
-
"# from ~/.config/dashwise/auth.json \u2014 you only need
|
|
536
|
-
"#
|
|
696
|
+
"# Phase 2 two-var contract: exactly these two vars. `dashwise dev`",
|
|
697
|
+
"# auto-injects both from ~/.config/dashwise/auth.json \u2014 you only need",
|
|
698
|
+
"# to edit this file if you point at a non-default backend or run",
|
|
699
|
+
"# `next dev` standalone.",
|
|
700
|
+
"#",
|
|
701
|
+
"# The browser needs the API URL too; next.config derives",
|
|
702
|
+
"# NEXT_PUBLIC_DASHWISE_API_URL from DASHWISE_API_URL, so you never set",
|
|
703
|
+
"# it yourself.",
|
|
537
704
|
`DASHWISE_API_URL=${apiUrl}`,
|
|
538
|
-
"#
|
|
539
|
-
"#
|
|
540
|
-
"
|
|
541
|
-
`NEXT_PUBLIC_DASHWISE_API_URL=${apiUrl}`,
|
|
542
|
-
"DASHWISE_API_TOKEN=",
|
|
543
|
-
"DASHWISE_INSTALLATION_ID=local",
|
|
705
|
+
"# Scoped `dwk_` API key \u2014 mint with `dashwise api-keys create <name>`",
|
|
706
|
+
"# for deploys; `dashwise dev` injects the local-dev key automatically.",
|
|
707
|
+
"DASHWISE_API_KEY=",
|
|
544
708
|
""
|
|
545
709
|
].join("\n");
|
|
546
710
|
}
|
|
@@ -548,24 +712,22 @@ function envLocal(apiUrl = SCAFFOLD_DEFAULT_API_URL) {
|
|
|
548
712
|
return [
|
|
549
713
|
"# Local dev config \u2014 auto-generated by `dashwise module init`.",
|
|
550
714
|
`# 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.",
|
|
715
|
+
"# safe to edit. `dashwise dev` (the unified loop) auto-injects the",
|
|
716
|
+
"# Phase 2 two-var contract \u2014 DASHWISE_API_URL + the per-module `dwk_`",
|
|
717
|
+
"# DASHWISE_API_KEY \u2014 from ~/.config/dashwise/auth.json, so you",
|
|
718
|
+
"# typically don't need to set either here.",
|
|
557
719
|
"#",
|
|
558
|
-
"#
|
|
559
|
-
"#
|
|
560
|
-
"#
|
|
720
|
+
"# The browser needs the API URL too; next.config derives",
|
|
721
|
+
"# NEXT_PUBLIC_DASHWISE_API_URL from DASHWISE_API_URL automatically.",
|
|
722
|
+
"# There is no DASHWISE_INSTALLATION_ID \u2014 the API key resolves the",
|
|
723
|
+
"# install server-side.",
|
|
561
724
|
`DASHWISE_API_URL=${apiUrl}`,
|
|
562
|
-
"#
|
|
563
|
-
"#
|
|
564
|
-
"#
|
|
565
|
-
"#
|
|
566
|
-
`
|
|
567
|
-
"
|
|
568
|
-
"DASHWISE_INSTALLATION_ID=",
|
|
725
|
+
"# Scoped `dwk_` key. Commented out on purpose: `dashwise dev` injects",
|
|
726
|
+
"# the local-dev key from ~/.config/dashwise/auth.json, and an empty",
|
|
727
|
+
"# `DASHWISE_API_KEY=` here would SHADOW that (and make a bare `next dev`",
|
|
728
|
+
"# fail with a confusing empty-key error). For a standalone `next dev`,",
|
|
729
|
+
"# uncomment + paste a key from `dashwise api-keys create <name>`.",
|
|
730
|
+
"# DASHWISE_API_KEY=",
|
|
569
731
|
""
|
|
570
732
|
].join("\n");
|
|
571
733
|
}
|
|
@@ -687,7 +849,7 @@ npm run dev:next # next dev on :3000 (terminal 2)
|
|
|
687
849
|
- **\`app/page.tsx\`** \u2014 public landing (allowlisted in \`middleware.ts\`).
|
|
688
850
|
- **\`app/dashboard/page.tsx\`** \u2014 Server Component requiring a valid session via \`getServerSession()\`.
|
|
689
851
|
- **\`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
|
|
852
|
+
- **\`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
853
|
- **\`app/api/auth/sign-out/route.ts\`** \u2014 clears the session cookie + tells the backend to revoke.
|
|
692
854
|
|
|
693
855
|
## Imports
|
|
@@ -736,25 +898,23 @@ the canonical block-discovery flow.
|
|
|
736
898
|
|
|
737
899
|
## Deploying elsewhere (custom domain)
|
|
738
900
|
|
|
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.
|
|
901
|
+
The scaffolded SSO flow works on any domain. The \`/api/auth/sign-in\`
|
|
902
|
+
route sends its own public origin as the \`callback_url\` to the backend;
|
|
903
|
+
the backend accepts it as long as the origin is on your installation's
|
|
904
|
+
allowlist. Local dev (\`http://localhost:<port>\`) is allowlisted
|
|
905
|
+
automatically for \`kind='local'\` installs.
|
|
906
|
+
|
|
907
|
+
For a deployed custom domain (e.g. \`https://mymodule.example.com\`), add
|
|
908
|
+
it to the allowlist:
|
|
909
|
+
|
|
910
|
+
\`\`\`bash
|
|
911
|
+
dashwise origins add https://mymodule.example.com
|
|
912
|
+
dashwise origins list # confirm
|
|
913
|
+
\`\`\`
|
|
914
|
+
|
|
915
|
+
That's it \u2014 no middleware changes, no separate token scheme. The single
|
|
916
|
+
\`dwk_\` \`DASHWISE_API_KEY\` drives both the data plane and the SSO
|
|
917
|
+
start-url call.
|
|
758
918
|
`;
|
|
759
919
|
}
|
|
760
920
|
function middlewareTs() {
|
|
@@ -796,7 +956,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
796
956
|
enableSystem
|
|
797
957
|
disableTransitionOnChange
|
|
798
958
|
>
|
|
799
|
-
|
|
959
|
+
{/* sessionEndpoint points at the same-origin /api/auth/session
|
|
960
|
+
route so the host-scoped session cookie is actually sent \u2014
|
|
961
|
+
a cross-origin fetch to the API would drop it (D8). */}
|
|
962
|
+
<AuthProvider sessionEndpoint="/api/auth/session">{children}</AuthProvider>
|
|
800
963
|
</ThemeProvider>
|
|
801
964
|
</body>
|
|
802
965
|
</html>
|
|
@@ -1001,73 +1164,102 @@ function Row({ label, value }: { label: string; value: string }) {
|
|
|
1001
1164
|
`;
|
|
1002
1165
|
}
|
|
1003
1166
|
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:
|
|
1167
|
+
return `// /api/auth/sign-in \u2014 kick off the SSO flow (one path, dev + prod).
|
|
1008
1168
|
//
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
// installed in a workspace yet.
|
|
1169
|
+
// Reads \`?return_to=\` from the caller (defaults to '/'), then asks the
|
|
1170
|
+
// DashWise backend for a redirect URL by calling the module-API
|
|
1171
|
+
// server-side start endpoint:
|
|
1013
1172
|
//
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1173
|
+
// POST /api/module-api/sso/start-url
|
|
1174
|
+
// Authorization: Bearer <DASHWISE_API_KEY> (the module's dwk_ key)
|
|
1175
|
+
// { "callback_url": "<this module>/api/auth/callback?return_to=..." }
|
|
1016
1176
|
//
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1177
|
+
// The backend resolves the install from the key (no installation id in
|
|
1178
|
+
// the request), validates the callback_url origin against the install's
|
|
1179
|
+
// allowlist (kind='local' installs implicitly allow localhost), and
|
|
1180
|
+
// returns { redirect_url, expires_in_seconds }. We 302 the browser to
|
|
1181
|
+
// redirect_url, which runs the existing main-app SSO flow and ends in a
|
|
1182
|
+
// 302 back to callback_url?code=<one-time>. The /api/auth/callback route
|
|
1183
|
+
// exchanges that code for a session cookie.
|
|
1019
1184
|
//
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
// need to handle the "user not logged in" case here.
|
|
1185
|
+
// No dev-vs-prod branching, no installation id, no dev-session id \u2014 the
|
|
1186
|
+
// single dwk_ key drives both local dev and production.
|
|
1023
1187
|
import { NextResponse } from 'next/server';
|
|
1024
1188
|
import { getPublicOrigin } from '@/lib/request-origin';
|
|
1025
1189
|
|
|
1026
|
-
export function GET(request: Request) {
|
|
1190
|
+
export async function GET(request: Request) {
|
|
1027
1191
|
const url = new URL(request.url);
|
|
1028
1192
|
const returnTo = url.searchParams.get('return_to') ?? '/';
|
|
1029
1193
|
const apiBaseUrl = process.env.DASHWISE_API_URL;
|
|
1030
|
-
const
|
|
1031
|
-
const devSessionId = process.env.DASHWISE_DEV_SESSION_ID;
|
|
1194
|
+
const apiKey = process.env.DASHWISE_API_KEY;
|
|
1032
1195
|
|
|
1033
1196
|
if (!apiBaseUrl) {
|
|
1034
|
-
return new NextResponse(
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
);
|
|
1197
|
+
return new NextResponse('DASHWISE_API_URL must be set in the environment', {
|
|
1198
|
+
status: 500,
|
|
1199
|
+
});
|
|
1038
1200
|
}
|
|
1039
|
-
if (!
|
|
1201
|
+
if (!apiKey) {
|
|
1040
1202
|
return new NextResponse(
|
|
1041
|
-
'
|
|
1042
|
-
'(local
|
|
1043
|
-
'a dev session, or set DASHWISE_INSTALLATION_ID after publishing your module.',
|
|
1203
|
+
'DASHWISE_API_KEY must be set in the environment. Run \`dashwise dev\` ' +
|
|
1204
|
+
'(local) or paste a key from \`dashwise api-keys create\` (deployed).',
|
|
1044
1205
|
{ status: 500 },
|
|
1045
1206
|
);
|
|
1046
1207
|
}
|
|
1047
1208
|
|
|
1048
|
-
// Absolute URL
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1209
|
+
// Absolute callback URL so the backend knows which host to bounce back
|
|
1210
|
+
// to. \`getPublicOrigin\` reads \`X-Forwarded-Host\` when present so the
|
|
1211
|
+
// URL points at the public domain, not the internal Node origin behind
|
|
1212
|
+
// reverse proxies (Railway/Vercel/Fly). The callback path is fixed at
|
|
1213
|
+
// /api/auth/callback by SDK convention; we tuck \`return_to\` onto it so
|
|
1214
|
+
// the callback route can resume the user's original destination.
|
|
1053
1215
|
const moduleOrigin = getPublicOrigin(request);
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1216
|
+
const callback = new URL('/api/auth/callback', moduleOrigin);
|
|
1217
|
+
callback.searchParams.set('return_to', returnTo);
|
|
1218
|
+
|
|
1219
|
+
let res: Response;
|
|
1220
|
+
try {
|
|
1221
|
+
res = await fetch(\`\${apiBaseUrl}/api/module-api/sso/start-url\`, {
|
|
1222
|
+
method: 'POST',
|
|
1223
|
+
headers: {
|
|
1224
|
+
'Content-Type': 'application/json',
|
|
1225
|
+
Accept: 'application/json',
|
|
1226
|
+
Authorization: \`Bearer \${apiKey}\`,
|
|
1227
|
+
},
|
|
1228
|
+
body: JSON.stringify({ callback_url: callback.toString() }),
|
|
1229
|
+
});
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
const target = new URL('/sign-in-error', moduleOrigin);
|
|
1232
|
+
target.searchParams.set('code', 'server_error');
|
|
1233
|
+
target.searchParams.set('description', (err as Error).message);
|
|
1234
|
+
return NextResponse.redirect(target);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (!res.ok) {
|
|
1238
|
+
// The backend rejected the start (bad key, callback origin not
|
|
1239
|
+
// allowlisted, install missing). Surface a real error page instead
|
|
1240
|
+
// of a raw 4xx.
|
|
1241
|
+
const body = (await res.json().catch(() => null)) as
|
|
1242
|
+
| { code?: string; message?: string }
|
|
1243
|
+
| null;
|
|
1244
|
+
const target = new URL('/sign-in-error', moduleOrigin);
|
|
1245
|
+
target.searchParams.set('code', body?.code ?? 'invalid_request');
|
|
1246
|
+
if (body?.message) target.searchParams.set('description', body.message);
|
|
1247
|
+
return NextResponse.redirect(target);
|
|
1069
1248
|
}
|
|
1070
|
-
|
|
1249
|
+
|
|
1250
|
+
const body = (await res.json().catch(() => null)) as
|
|
1251
|
+
| { redirect_url?: string }
|
|
1252
|
+
| null;
|
|
1253
|
+
if (!body?.redirect_url) {
|
|
1254
|
+
const target = new URL('/sign-in-error', moduleOrigin);
|
|
1255
|
+
target.searchParams.set('code', 'server_error');
|
|
1256
|
+
target.searchParams.set(
|
|
1257
|
+
'description',
|
|
1258
|
+
'start-url response missing redirect_url',
|
|
1259
|
+
);
|
|
1260
|
+
return NextResponse.redirect(target);
|
|
1261
|
+
}
|
|
1262
|
+
return NextResponse.redirect(body.redirect_url);
|
|
1071
1263
|
}
|
|
1072
1264
|
`;
|
|
1073
1265
|
}
|
|
@@ -1139,6 +1331,65 @@ export async function POST(request: Request) {
|
|
|
1139
1331
|
}
|
|
1140
1332
|
`;
|
|
1141
1333
|
}
|
|
1334
|
+
function appApiItemsRouteTs() {
|
|
1335
|
+
return `// Example data-plane route \u2014 lists + creates rows in the seeded
|
|
1336
|
+
// \`items\` table via the generated typed SDK. Delete this (and the
|
|
1337
|
+
// \`items\` table in module.json) once you add your own tables.
|
|
1338
|
+
//
|
|
1339
|
+
// \`db\` comes from @dashai/generated, regenerated on every module.json
|
|
1340
|
+
// save by \`dashwise dev\`. Run it (or \`dashwise module generate\`) first.
|
|
1341
|
+
import { NextResponse } from 'next/server';
|
|
1342
|
+
import { db } from '@dashai/generated';
|
|
1343
|
+
|
|
1344
|
+
export async function GET() {
|
|
1345
|
+
const page = await db.items.list({ limit: 50 });
|
|
1346
|
+
return NextResponse.json(page.rows);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
export async function POST(request: Request) {
|
|
1350
|
+
const body = (await request.json().catch(() => null)) as
|
|
1351
|
+
| { title?: unknown; done?: unknown }
|
|
1352
|
+
| null;
|
|
1353
|
+
const title = typeof body?.title === 'string' ? body.title : '';
|
|
1354
|
+
if (!title) {
|
|
1355
|
+
return NextResponse.json(
|
|
1356
|
+
{ error: 'title is required (string)' },
|
|
1357
|
+
{ status: 400 },
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
const done = typeof body?.done === 'boolean' ? body.done : false;
|
|
1361
|
+
const row = await db.items.create({ title, done });
|
|
1362
|
+
return NextResponse.json(row, { status: 201 });
|
|
1363
|
+
}
|
|
1364
|
+
`;
|
|
1365
|
+
}
|
|
1366
|
+
function appApiAuthSessionTs() {
|
|
1367
|
+
return `// /api/auth/session \u2014 SAME-ORIGIN session probe for client components.
|
|
1368
|
+
//
|
|
1369
|
+
// D8: the browser's <AuthProvider> can't read the session by calling the
|
|
1370
|
+
// cross-origin DashWise API directly, because the \`dashwise.session\`
|
|
1371
|
+
// cookie is host-scoped to THIS module's origin and is never sent to
|
|
1372
|
+
// \`api.dashwise.net\`. So <AuthProvider sessionEndpoint="/api/auth/session">
|
|
1373
|
+
// hits this route (same origin \u2192 the cookie IS sent); we validate it
|
|
1374
|
+
// server-side via \`getServerSession()\` (which forwards the cookie to the
|
|
1375
|
+
// API for us) and return the fully-enriched session \u2014 or \`null\` \u2014 as JSON.
|
|
1376
|
+
//
|
|
1377
|
+
// \`getServerSession()\` already merges the RBAC slice, so the client
|
|
1378
|
+
// receives \`rbacRoles\` / \`rbacPermissions\` and skips a second fetch.
|
|
1379
|
+
import { NextResponse } from 'next/server';
|
|
1380
|
+
import { getServerSession } from '@dashai/sdk/auth/server';
|
|
1381
|
+
|
|
1382
|
+
export async function GET() {
|
|
1383
|
+
const session = await getServerSession();
|
|
1384
|
+
// Always 200 \u2014 \`null\` is a valid "not signed in" answer the client
|
|
1385
|
+
// maps to \`unauthenticated\`. \`no-store\` so a stale signed-in body is
|
|
1386
|
+
// never served to a signed-out user after sign-out.
|
|
1387
|
+
return NextResponse.json(session, {
|
|
1388
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
`;
|
|
1392
|
+
}
|
|
1142
1393
|
function packageJsonCustom(opts) {
|
|
1143
1394
|
return JSON.stringify(
|
|
1144
1395
|
{
|
|
@@ -1154,7 +1405,7 @@ function packageJsonCustom(opts) {
|
|
|
1154
1405
|
start: "next start"
|
|
1155
1406
|
},
|
|
1156
1407
|
dependencies: {
|
|
1157
|
-
"@dashai/sdk": "^0.
|
|
1408
|
+
"@dashai/sdk": "^2.0.0",
|
|
1158
1409
|
next: "^15.0.0",
|
|
1159
1410
|
react: "^19.0.0",
|
|
1160
1411
|
"react-dom": "^19.0.0",
|
|
@@ -1504,7 +1755,7 @@ const ERROR_MESSAGES: Record<string, string> = {
|
|
|
1504
1755
|
server_error:
|
|
1505
1756
|
'The DashWise sign-in server returned an internal error. This is usually transient \u2014 try again in a moment.',
|
|
1506
1757
|
invalid_request:
|
|
1507
|
-
'The sign-in request was malformed. This is almost always a configuration bug \u2014 check that DASHWISE_API_URL and
|
|
1758
|
+
'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
1759
|
missing_code:
|
|
1509
1760
|
'The callback URL was missing the expected exchange code. This usually means the SSO flow was interrupted.',
|
|
1510
1761
|
exchange_failed:
|
|
@@ -1577,7 +1828,14 @@ function escapeHtml(s) {
|
|
|
1577
1828
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1578
1829
|
}
|
|
1579
1830
|
function isValidSlug(slug) {
|
|
1580
|
-
return
|
|
1831
|
+
return MODULE_SLUG_REGEX.test(slug);
|
|
1832
|
+
}
|
|
1833
|
+
function slugValidationMessage(slug) {
|
|
1834
|
+
if (slug.includes("_")) {
|
|
1835
|
+
const suggestion = slug.replace(/_/g, "-").replace(/^-+|-+$/g, "");
|
|
1836
|
+
return `Slug "${slug}" uses underscores \u2014 module slugs must be kebab-case (letters, digits, hyphens; e.g. "${suggestion || "my-module"}"). Rename it and retry.`;
|
|
1837
|
+
}
|
|
1838
|
+
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
1839
|
}
|
|
1582
1840
|
function claudeMd(opts) {
|
|
1583
1841
|
const isCustom = (opts.kind ?? "hand_authored") === "custom";
|
|
@@ -1660,17 +1918,43 @@ await db.<table>.create({ /* fields */ });
|
|
|
1660
1918
|
const stats = await qb.from('<table>').where({ /* ... */ }).execute();
|
|
1661
1919
|
\`\`\`
|
|
1662
1920
|
|
|
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
|
-
|
|
1921
|
+
The data plane authenticates via the per-module scoped \`dwk_\` API key
|
|
1922
|
+
cached in \`~/.config/dashwise/auth.json#modules[${opts.slug}]\` after
|
|
1923
|
+
\`dashwise init\` provisions the dev install. \`dashwise dev\` injects it
|
|
1924
|
+
as \`DASHWISE_API_KEY\` into the Next.js child env automatically. The
|
|
1925
|
+
key binds the install server-side \u2014 there is no \`DASHWISE_INSTALLATION_ID\`.
|
|
1926
|
+
|
|
1927
|
+
### Own tables vs. the shared Data Hub
|
|
1928
|
+
|
|
1929
|
+
The dev key \`dashwise init\` mints gives you **full access to your
|
|
1930
|
+
module's OWN tables** (\`owns.tables\` in \`module.json\`) through \`db\` /
|
|
1931
|
+
\`qb\` \u2014 no grant setup needed. That covers the common case.
|
|
1932
|
+
|
|
1933
|
+
The **shared workspace Data Hub** (other tables in the workspace, reached
|
|
1934
|
+
via \`datahub.table(<id>)\`) is separate: it is default-deny and returns
|
|
1935
|
+
**403** with the dev key until you mint a key that carries an explicit
|
|
1936
|
+
Data Hub grant. Do that with:
|
|
1937
|
+
|
|
1938
|
+
\`\`\`
|
|
1939
|
+
# read a specific Data Hub table:
|
|
1940
|
+
dashwise api-keys create datahub-read --grant datahub:table=<tableId>:read
|
|
1941
|
+
# or a whole database (application):
|
|
1942
|
+
dashwise api-keys create datahub-rw --grant datahub:app=<appId>:read,create,update
|
|
1943
|
+
\`\`\`
|
|
1944
|
+
|
|
1945
|
+
Paste the printed key as \`DASHWISE_API_KEY\` (or, for local dev, set it
|
|
1946
|
+
before \`dashwise dev\` so it wins over the auto-injected own-tables key).
|
|
1947
|
+
So: \`db\`/\`qb\` on your own tables works out of the box; \`datahub\` needs a
|
|
1948
|
+
granted key.
|
|
1667
1949
|
|
|
1668
1950
|
## Auth in dev mode
|
|
1669
1951
|
|
|
1670
|
-
\`dashwise dev\` registers a dev-mode SSO session with the backend
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
\`
|
|
1952
|
+
\`dashwise dev\` registers a dev-mode SSO session with the backend so
|
|
1953
|
+
workspace members can sign into the locally-running module via DashWise
|
|
1954
|
+
SSO. The scaffold's \`/api/auth/sign-in\` route is a single path: it
|
|
1955
|
+
calls \`POST /api/module-api/sso/start-url\` with your \`dwk_\` key and
|
|
1956
|
+
follows the returned \`redirect_url\` \u2014 the same code runs in dev and in
|
|
1957
|
+
production, with no installation id or dev-session id in the request.
|
|
1674
1958
|
|
|
1675
1959
|
In Server Components: \`await getServerSession()\` (from
|
|
1676
1960
|
\`@dashai/sdk/auth/server\`).
|
|
@@ -1727,13 +2011,14 @@ what tables it owns.
|
|
|
1727
2011
|
|
|
1728
2012
|
| Table | Columns | Notes |
|
|
1729
2013
|
|---|---|---|
|
|
1730
|
-
|
|
|
2014
|
+
| \`items\` | \`title:text\`, \`done:boolean\` | seeded example \u2014 edit or replace; \`dashwise table create <slug> --field <slug:type>\` adds more |
|
|
1731
2015
|
|
|
1732
2016
|
## API / contract
|
|
1733
2017
|
|
|
1734
2018
|
| Method | Path | Auth | Purpose |
|
|
1735
2019
|
|---|---|---|---|
|
|
1736
|
-
|
|
|
2020
|
+
| GET | \`/api/items\` | dwk_ (server-side) | list \`items\` rows via \`db.items.list()\` |
|
|
2021
|
+
| POST | \`/api/items\` | dwk_ (server-side) | create an \`items\` row via \`db.items.create()\` |
|
|
1737
2022
|
|
|
1738
2023
|
## Business rules
|
|
1739
2024
|
|
|
@@ -1849,11 +2134,12 @@ primary_region = "iad"
|
|
|
1849
2134
|
memory_mb = 256
|
|
1850
2135
|
`;
|
|
1851
2136
|
}
|
|
1852
|
-
var DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL;
|
|
2137
|
+
var DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL, MODULE_SLUG_REGEX;
|
|
1853
2138
|
var init_scaffold = __esm({
|
|
1854
2139
|
"src/lib/scaffold.ts"() {
|
|
1855
2140
|
DEPLOY_TARGETS = ["railway", "vercel", "fly"];
|
|
1856
2141
|
SCAFFOLD_DEFAULT_API_URL = "http://localhost:3000";
|
|
2142
|
+
MODULE_SLUG_REGEX = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
|
|
1857
2143
|
}
|
|
1858
2144
|
});
|
|
1859
2145
|
async function selectWorkspace(opts) {
|
|
@@ -1921,7 +2207,7 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
1921
2207
|
const answer = await text({
|
|
1922
2208
|
message: "Module slug?",
|
|
1923
2209
|
placeholder: "tasks",
|
|
1924
|
-
validate: (v) => isValidSlug(v) ? void 0 :
|
|
2210
|
+
validate: (v) => isValidSlug(v) ? void 0 : slugValidationMessage(v)
|
|
1925
2211
|
});
|
|
1926
2212
|
if (isCancel(answer)) {
|
|
1927
2213
|
fail("Aborted.");
|
|
@@ -1934,7 +2220,7 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
1934
2220
|
return 1;
|
|
1935
2221
|
}
|
|
1936
2222
|
if (!isValidSlug(slug)) {
|
|
1937
|
-
fail(
|
|
2223
|
+
fail(slugValidationMessage(slug));
|
|
1938
2224
|
return 1;
|
|
1939
2225
|
}
|
|
1940
2226
|
const name = options.name ?? humanizeSlug(slug);
|
|
@@ -1982,7 +2268,11 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
1982
2268
|
// discipline + SDK pointers loaded into context from byte one.
|
|
1983
2269
|
["CLAUDE.md", claudeMd(scaffoldOpts)],
|
|
1984
2270
|
["spec.md", specMd(scaffoldOpts)],
|
|
1985
|
-
["agent-log.md", agentLogMd(scaffoldOpts)]
|
|
2271
|
+
["agent-log.md", agentLogMd(scaffoldOpts)],
|
|
2272
|
+
// Example data-plane route (both flavors) — exercises the generated
|
|
2273
|
+
// SDK against the seeded `items` table. Ships for simple + custom
|
|
2274
|
+
// since both produce `@dashai/generated`.
|
|
2275
|
+
["app/api/items/route.ts", appApiItemsRouteTs()]
|
|
1986
2276
|
];
|
|
1987
2277
|
if (kind === "custom") {
|
|
1988
2278
|
files.push(
|
|
@@ -2002,7 +2292,10 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2002
2292
|
["app/sign-in-error/page.tsx", appSignInErrorPageTsx()],
|
|
2003
2293
|
["app/api/auth/sign-in/route.ts", appApiAuthSignInTs()],
|
|
2004
2294
|
["app/api/auth/callback/route.ts", appApiAuthCallbackTs()],
|
|
2005
|
-
["app/api/auth/sign-out/route.ts", appApiAuthSignOutTs()]
|
|
2295
|
+
["app/api/auth/sign-out/route.ts", appApiAuthSignOutTs()],
|
|
2296
|
+
// D8: same-origin session probe that <AuthProvider> hits so the
|
|
2297
|
+
// host-scoped session cookie is actually sent.
|
|
2298
|
+
["app/api/auth/session/route.ts", appApiAuthSessionTs()]
|
|
2006
2299
|
);
|
|
2007
2300
|
} else {
|
|
2008
2301
|
files.push(
|
|
@@ -2038,18 +2331,40 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2038
2331
|
let devInstall = null;
|
|
2039
2332
|
const wantsProvision = options.provision !== false;
|
|
2040
2333
|
if (wantsProvision) {
|
|
2334
|
+
const manifest = (() => {
|
|
2335
|
+
try {
|
|
2336
|
+
return readManifest(dir) ?? void 0;
|
|
2337
|
+
} catch {
|
|
2338
|
+
return void 0;
|
|
2339
|
+
}
|
|
2340
|
+
})();
|
|
2041
2341
|
devInstall = await tryProvisionDevInstall({
|
|
2042
2342
|
slug,
|
|
2043
2343
|
name,
|
|
2044
2344
|
description: options.description,
|
|
2045
2345
|
workspaceFlag: options.workspace,
|
|
2046
|
-
moduleKind: kind
|
|
2346
|
+
moduleKind: kind,
|
|
2347
|
+
...manifest !== void 0 ? { manifest } : {}
|
|
2047
2348
|
});
|
|
2048
2349
|
} else {
|
|
2049
2350
|
info(
|
|
2050
2351
|
`Skipping provisioning (${code("--no-provision")}). ${dim("Run with provisioning later to enable `dashwise dev` data-plane.")}`
|
|
2051
2352
|
);
|
|
2052
2353
|
}
|
|
2354
|
+
const keyCached = !wantsProvision || readModuleEntry(slug)?.apiKey ? true : false;
|
|
2355
|
+
if (wantsProvision && !keyCached) {
|
|
2356
|
+
fail(
|
|
2357
|
+
`NO API KEY WAS CACHED for ${code(slug)} \u2014 the scaffold is written, but ${code("dashwise dev")} will run with NO token (data-plane + sign-in will fail).`
|
|
2358
|
+
);
|
|
2359
|
+
info(
|
|
2360
|
+
`Fix it by re-running provisioning once the cause above is resolved:
|
|
2361
|
+
${code(`dashwise init ${slug} --force --workspace <your-workspace-slug>`)}
|
|
2362
|
+
Confirm success: the line ${code("\u2713 Provisioned dev install id=\u2026 API key cached")} must appear,
|
|
2363
|
+
and ${code("cat ~/.config/dashwise/auth.json")} must show a non-empty ${code(`modules.${slug}.apiKey`)} starting with ${code("dwk_")}.`
|
|
2364
|
+
);
|
|
2365
|
+
outro("Scaffold written, but provisioning failed \u2014 see above.");
|
|
2366
|
+
return 2;
|
|
2367
|
+
}
|
|
2053
2368
|
info(`Next steps:`);
|
|
2054
2369
|
process.stdout.write(`
|
|
2055
2370
|
${dim("1.")} ${code(`cd ${dir}`)}
|
|
@@ -2061,7 +2376,7 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2061
2376
|
${dim(" dashwise module dev")} ${dim("# watch + auto-regenerate")}
|
|
2062
2377
|
${dim(" npm run dev:next # next dev on :3000)")}
|
|
2063
2378
|
${devInstall ? `
|
|
2064
|
-
${dim(`Dev installation id=${devInstall.installation.id};
|
|
2379
|
+
${dim(`Dev installation id=${devInstall.installation.id}; API key expires ${devInstall.api_key_expires_at}.`)}
|
|
2065
2380
|
` : ""}`);
|
|
2066
2381
|
outro("Happy authoring.");
|
|
2067
2382
|
return 0;
|
|
@@ -2078,6 +2393,20 @@ async function tryProvisionDevInstall(args) {
|
|
|
2078
2393
|
warn("CLI config disappeared mid-init. Skipping provisioning.");
|
|
2079
2394
|
return null;
|
|
2080
2395
|
}
|
|
2396
|
+
const targetApiUrl = resolveApiUrl();
|
|
2397
|
+
info(`Backend: ${code(targetApiUrl)}`);
|
|
2398
|
+
const hostOf = (u) => {
|
|
2399
|
+
try {
|
|
2400
|
+
return new URL(u).host;
|
|
2401
|
+
} catch {
|
|
2402
|
+
return u;
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2405
|
+
if (hostOf(targetApiUrl) !== hostOf(cfg.apiUrl)) {
|
|
2406
|
+
warn(
|
|
2407
|
+
`Your saved login is for ${code(cfg.apiUrl)} but this run targets ${code(targetApiUrl)}. Provisioning uses the saved token, which the target backend will reject (401). Log in against the target first: ${code(`dashwise login --api-url ${targetApiUrl}`)}.`
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2081
2410
|
let workspace2;
|
|
2082
2411
|
try {
|
|
2083
2412
|
workspace2 = await selectWorkspace({
|
|
@@ -2093,42 +2422,60 @@ async function tryProvisionDevInstall(args) {
|
|
|
2093
2422
|
info(
|
|
2094
2423
|
`Provisioning dev install in workspace ${code(workspace2.slug)} (id=${workspace2.id})\u2026`
|
|
2095
2424
|
);
|
|
2425
|
+
const baseBody = {
|
|
2426
|
+
slug: args.slug,
|
|
2427
|
+
name: args.name,
|
|
2428
|
+
...args.description !== void 0 ? { description: args.description } : {},
|
|
2429
|
+
workspaceId: workspace2.id,
|
|
2430
|
+
...args.moduleKind !== void 0 ? { moduleKind: args.moduleKind } : {}
|
|
2431
|
+
};
|
|
2432
|
+
const reqOpts = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
|
|
2096
2433
|
let response;
|
|
2097
2434
|
try {
|
|
2098
2435
|
response = await createDevInstallation(
|
|
2099
|
-
{
|
|
2100
|
-
|
|
2101
|
-
name: args.name,
|
|
2102
|
-
...args.description !== void 0 ? { description: args.description } : {},
|
|
2103
|
-
workspaceId: workspace2.id,
|
|
2104
|
-
...args.moduleKind !== void 0 ? { moduleKind: args.moduleKind } : {}
|
|
2105
|
-
},
|
|
2106
|
-
{ baseUrl: resolveApiUrl(), auth: { token: cfg.token } }
|
|
2436
|
+
{ ...baseBody, ...args.manifest !== void 0 ? { manifest: args.manifest } : {} },
|
|
2437
|
+
reqOpts
|
|
2107
2438
|
);
|
|
2108
2439
|
} catch (err) {
|
|
2109
|
-
if (err instanceof ApiError) {
|
|
2440
|
+
if (err instanceof ApiError && err.code === "MANIFEST_INVALID" && args.manifest !== void 0) {
|
|
2441
|
+
warn(
|
|
2442
|
+
`Couldn't pre-materialize the seeded table(s) (${err.message}). Provisioning without them; add tables later with ${code("dashwise table create")}.`
|
|
2443
|
+
);
|
|
2444
|
+
try {
|
|
2445
|
+
response = await createDevInstallation(baseBody, reqOpts);
|
|
2446
|
+
} catch (err2) {
|
|
2447
|
+
if (err2 instanceof ApiError) {
|
|
2448
|
+
surfaceProvisionApiError(err2, args.slug);
|
|
2449
|
+
return null;
|
|
2450
|
+
}
|
|
2451
|
+
warn(`Provisioning failed: ${err2.message}`);
|
|
2452
|
+
return null;
|
|
2453
|
+
}
|
|
2454
|
+
} else if (err instanceof ApiError) {
|
|
2110
2455
|
surfaceProvisionApiError(err, args.slug);
|
|
2111
2456
|
return null;
|
|
2457
|
+
} else {
|
|
2458
|
+
warn(`Provisioning failed: ${err.message}`);
|
|
2459
|
+
return null;
|
|
2112
2460
|
}
|
|
2113
|
-
warn(`Provisioning failed: ${err.message}`);
|
|
2114
|
-
return null;
|
|
2115
2461
|
}
|
|
2116
2462
|
try {
|
|
2117
2463
|
upsertModuleEntry(args.slug, {
|
|
2118
|
-
|
|
2464
|
+
apiKey: response.api_key,
|
|
2465
|
+
apiKeyId: response.api_key_id,
|
|
2119
2466
|
installationId: response.installation.id,
|
|
2120
|
-
expiresAt: response.
|
|
2467
|
+
expiresAt: response.api_key_expires_at
|
|
2121
2468
|
});
|
|
2122
2469
|
} catch (err) {
|
|
2123
2470
|
warn(
|
|
2124
|
-
`Provisioned successfully (installation id=${response.installation.id}) but failed to cache the
|
|
2125
|
-
|
|
2126
|
-
|
|
2471
|
+
`Provisioned successfully (installation id=${response.installation.id}) but failed to cache the API key: ${err.message}
|
|
2472
|
+
API key (save this manually): ${response.api_key}
|
|
2473
|
+
Key expires: ${response.api_key_expires_at}`
|
|
2127
2474
|
);
|
|
2128
2475
|
return response;
|
|
2129
2476
|
}
|
|
2130
2477
|
success(
|
|
2131
|
-
`Provisioned dev install id=${response.installation.id}.
|
|
2478
|
+
`Provisioned dev install id=${response.installation.id}. API key cached in your CLI config.` + (response.tables_applied > 0 ? ` ${response.tables_applied} seeded table(s) materialized.` : "")
|
|
2132
2479
|
);
|
|
2133
2480
|
return response;
|
|
2134
2481
|
}
|
|
@@ -2173,6 +2520,7 @@ var init_init = __esm({
|
|
|
2173
2520
|
init_output();
|
|
2174
2521
|
init_api_client();
|
|
2175
2522
|
init_config();
|
|
2523
|
+
init_manifest_io();
|
|
2176
2524
|
init_workspaces();
|
|
2177
2525
|
}
|
|
2178
2526
|
});
|
|
@@ -2197,6 +2545,7 @@ function loadSchemaCommandContext(opts) {
|
|
|
2197
2545
|
`No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
|
|
2198
2546
|
);
|
|
2199
2547
|
}
|
|
2548
|
+
const token = resolveSchemaAuthToken(entry, slug);
|
|
2200
2549
|
return {
|
|
2201
2550
|
projectRoot,
|
|
2202
2551
|
manifestPath: manifestPath2,
|
|
@@ -2207,10 +2556,16 @@ function loadSchemaCommandContext(opts) {
|
|
|
2207
2556
|
entry,
|
|
2208
2557
|
requestOptions: {
|
|
2209
2558
|
baseUrl: resolveApiUrl(),
|
|
2210
|
-
auth: { token
|
|
2559
|
+
auth: { token }
|
|
2211
2560
|
}
|
|
2212
2561
|
};
|
|
2213
2562
|
}
|
|
2563
|
+
function resolveSchemaAuthToken(entry, slug) {
|
|
2564
|
+
if (entry.apiKey) return entry.apiKey;
|
|
2565
|
+
throw new Error(
|
|
2566
|
+
`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.`
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2214
2569
|
var init_schema_command_context = __esm({
|
|
2215
2570
|
"src/lib/schema-command-context.ts"() {
|
|
2216
2571
|
init_config();
|
|
@@ -2549,7 +2904,6 @@ async function tableCreateCommand(slug, options) {
|
|
|
2549
2904
|
info(`Creating table ${code(slug)} in installation ${ctx.entry.installationId}\u2026`);
|
|
2550
2905
|
try {
|
|
2551
2906
|
const result = await createTable(
|
|
2552
|
-
ctx.entry.installationId,
|
|
2553
2907
|
{ slug, fields },
|
|
2554
2908
|
ctx.requestOptions
|
|
2555
2909
|
);
|
|
@@ -2563,10 +2917,7 @@ async function tableCreateCommand(slug, options) {
|
|
|
2563
2917
|
}
|
|
2564
2918
|
async function syncLocalManifest(ctx) {
|
|
2565
2919
|
try {
|
|
2566
|
-
const { manifest } = await getInstallationSchema(
|
|
2567
|
-
ctx.entry.installationId,
|
|
2568
|
-
ctx.requestOptions
|
|
2569
|
-
);
|
|
2920
|
+
const { manifest } = await getInstallationSchema(ctx.requestOptions);
|
|
2570
2921
|
writeManifest(manifest, ctx.projectRoot);
|
|
2571
2922
|
return 0;
|
|
2572
2923
|
} catch (err) {
|
|
@@ -2599,17 +2950,17 @@ function surfaceFailure(err) {
|
|
|
2599
2950
|
break;
|
|
2600
2951
|
case "UNAUTHENTICATED":
|
|
2601
2952
|
fail(
|
|
2602
|
-
`
|
|
2953
|
+
`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
2954
|
);
|
|
2604
2955
|
break;
|
|
2605
|
-
case "
|
|
2956
|
+
case "SCHEMA_LOCKED":
|
|
2606
2957
|
fail(
|
|
2607
|
-
`Schema
|
|
2958
|
+
`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
2959
|
);
|
|
2609
2960
|
break;
|
|
2610
2961
|
case "INSTALL_MISMATCH":
|
|
2611
2962
|
fail(
|
|
2612
|
-
`
|
|
2963
|
+
`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
2964
|
);
|
|
2614
2965
|
break;
|
|
2615
2966
|
default:
|
|
@@ -2651,11 +3002,7 @@ async function tableDeleteCommand(slug, options) {
|
|
|
2651
3002
|
}
|
|
2652
3003
|
info(`Dropping table ${code(slug)} from installation ${ctx.entry.installationId}\u2026`);
|
|
2653
3004
|
try {
|
|
2654
|
-
const result = await dropTable(
|
|
2655
|
-
ctx.entry.installationId,
|
|
2656
|
-
slug,
|
|
2657
|
-
ctx.requestOptions
|
|
2658
|
-
);
|
|
3005
|
+
const result = await dropTable(slug, ctx.requestOptions);
|
|
2659
3006
|
success(
|
|
2660
3007
|
`Dropped table ${code(slug)} (manifest_version=${result.manifest_version}). Physical table soft-deleted; recoverable for 30 days via the dashboard's trash.`
|
|
2661
3008
|
);
|
|
@@ -2723,7 +3070,6 @@ async function fieldAddCommand(tableSlug, fieldSpec, options) {
|
|
|
2723
3070
|
);
|
|
2724
3071
|
try {
|
|
2725
3072
|
const result = await addField(
|
|
2726
|
-
ctx.entry.installationId,
|
|
2727
3073
|
{ table_slug: tableSlug, field },
|
|
2728
3074
|
ctx.requestOptions
|
|
2729
3075
|
);
|
|
@@ -2769,7 +3115,6 @@ async function fieldRemoveCommand(tableSlug, fieldSlug, options) {
|
|
|
2769
3115
|
);
|
|
2770
3116
|
try {
|
|
2771
3117
|
const result = await removeField(
|
|
2772
|
-
ctx.entry.installationId,
|
|
2773
3118
|
tableSlug,
|
|
2774
3119
|
fieldSlug,
|
|
2775
3120
|
ctx.requestOptions
|
|
@@ -2823,7 +3168,6 @@ async function fieldSetTypeCommand(tableSlug, fieldSlug, newType, options) {
|
|
|
2823
3168
|
);
|
|
2824
3169
|
try {
|
|
2825
3170
|
const result = await updateField(
|
|
2826
|
-
ctx.entry.installationId,
|
|
2827
3171
|
tableSlug,
|
|
2828
3172
|
fieldSlug,
|
|
2829
3173
|
{ type: newType },
|
|
@@ -2849,14 +3193,84 @@ var init_set_type = __esm({
|
|
|
2849
3193
|
|
|
2850
3194
|
// src/commands/dev.ts
|
|
2851
3195
|
init_api_client();
|
|
2852
|
-
init_config();
|
|
2853
|
-
init_manifest_io();
|
|
2854
3196
|
|
|
2855
|
-
// src/lib/
|
|
3197
|
+
// src/lib/connect-status.ts
|
|
2856
3198
|
init_api_client();
|
|
2857
|
-
|
|
3199
|
+
init_output();
|
|
3200
|
+
async function reportConnectStatus(opts) {
|
|
3201
|
+
const [deps, bindings] = await Promise.all([
|
|
3202
|
+
safeGet(
|
|
3203
|
+
() => getInstallationDependencies(opts.installationId, opts.requestOptions)
|
|
3204
|
+
),
|
|
3205
|
+
safeGet(
|
|
3206
|
+
() => getInstallationBindings(opts.installationId, opts.requestOptions)
|
|
3207
|
+
)
|
|
3208
|
+
]);
|
|
3209
|
+
if (deps === null && bindings === null) return;
|
|
3210
|
+
const depList = deps ?? [];
|
|
3211
|
+
const bindList = bindings ?? [];
|
|
3212
|
+
if (depList.length === 0 && bindList.length === 0) return;
|
|
3213
|
+
const unboundDeps = depList.filter((d) => d.status === "unbound").length;
|
|
3214
|
+
const unboundUses = bindList.filter((b) => b.status === "unbound").length;
|
|
3215
|
+
info(
|
|
3216
|
+
`Connect status for ${code(opts.moduleSlug)} ${dim(
|
|
3217
|
+
`(${depList.length} dep${depList.length === 1 ? "" : "s"}, ${bindList.length} uses slot${bindList.length === 1 ? "" : "s"})`
|
|
3218
|
+
)}:`
|
|
3219
|
+
);
|
|
3220
|
+
for (const d of depList) renderDep(d);
|
|
3221
|
+
for (const b of bindList) renderBinding(b);
|
|
3222
|
+
const hints = [];
|
|
3223
|
+
if (unboundDeps > 0) {
|
|
3224
|
+
hints.push(
|
|
3225
|
+
`${code("dashwise deps bind <provider-slug>")} once the provider is installed`
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
3228
|
+
if (unboundUses > 0) {
|
|
3229
|
+
hints.push(`${code("dashwise bind <uses-slug> --table <name>")} to connect a table`);
|
|
3230
|
+
}
|
|
3231
|
+
if (hints.length > 0) {
|
|
3232
|
+
info(`Connect parked items with ${hints.join("; ")}.`);
|
|
3233
|
+
}
|
|
3234
|
+
out("");
|
|
3235
|
+
}
|
|
3236
|
+
function renderDep(d) {
|
|
3237
|
+
if (d.status === "resolved") {
|
|
3238
|
+
const providerLine = d.provider ? ` ${dim(`\u2192 install #${d.provider.installation_id} @ v${d.provider.version}`)}` : "";
|
|
3239
|
+
out(` ${pc.green("\u25CF resolved")} ${dim("dep")} ${code(d.slug)}@${d.range ?? "*"}${providerLine}`);
|
|
3240
|
+
} else {
|
|
3241
|
+
out(
|
|
3242
|
+
` ${pc.yellow("\u25CB unbound")} ${dim("dep")} ${code(d.slug)}@${d.range ?? "*"} ${dim(`reason: ${d.reason ?? "(unspecified)"}`)}`
|
|
3243
|
+
);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
function renderBinding(b) {
|
|
3247
|
+
if (b.status === "bound") {
|
|
3248
|
+
const tableLine = b.table ? ` ${dim(`\u2192 ${b.table.name} (id ${b.table.id})`)}` : "";
|
|
3249
|
+
out(` ${pc.green("\u25CF bound")} ${dim("uses")} ${code(b.uses_slug)}${tableLine}`);
|
|
3250
|
+
} else if (b.status === "declined") {
|
|
3251
|
+
out(` ${pc.dim("\u2298 declined")} ${dim("uses")} ${code(b.uses_slug)}`);
|
|
3252
|
+
} else {
|
|
3253
|
+
out(
|
|
3254
|
+
` ${pc.yellow("\u25CB unbound")} ${dim("uses")} ${code(b.uses_slug)} ${dim(`reason: ${b.reason ?? "(unspecified)"}`)}`
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
async function safeGet(fn) {
|
|
3259
|
+
try {
|
|
3260
|
+
return await fn();
|
|
3261
|
+
} catch (err) {
|
|
3262
|
+
return null;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
// src/commands/dev.ts
|
|
3267
|
+
init_config();
|
|
3268
|
+
init_manifest_io();
|
|
3269
|
+
|
|
3270
|
+
// src/lib/manifest-poll.ts
|
|
3271
|
+
init_api_client();
|
|
3272
|
+
function startManifestPollLoop(opts) {
|
|
2858
3273
|
const {
|
|
2859
|
-
installationId,
|
|
2860
3274
|
manifestPath: manifestPath2,
|
|
2861
3275
|
requestOptions,
|
|
2862
3276
|
intervalMs,
|
|
@@ -2869,10 +3283,7 @@ function startManifestPollLoop(opts) {
|
|
|
2869
3283
|
const tick = async () => {
|
|
2870
3284
|
if (stopped) return;
|
|
2871
3285
|
try {
|
|
2872
|
-
const { manifest } = await getInstallationSchema(
|
|
2873
|
-
installationId,
|
|
2874
|
-
requestOptions
|
|
2875
|
-
);
|
|
3286
|
+
const { manifest } = await getInstallationSchema(requestOptions);
|
|
2876
3287
|
const remoteJson = JSON.stringify(manifest, null, 2) + "\n";
|
|
2877
3288
|
const currentLocal = existsSync(manifestPath2) ? readFileSync(manifestPath2, "utf-8") : "";
|
|
2878
3289
|
if (remoteJson === currentLocal) {
|
|
@@ -2967,16 +3378,15 @@ async function devCommand(options) {
|
|
|
2967
3378
|
}
|
|
2968
3379
|
const explicitInstallationId = options.installationId ?? process.env.DASHWISE_INSTALLATION_ID;
|
|
2969
3380
|
const reuseDevSessionId = process.env.DASHWISE_DEV_SESSION_ID;
|
|
2970
|
-
|
|
2971
|
-
const
|
|
3381
|
+
let moduleEntry = manifestSlug !== void 0 ? readModuleEntry(manifestSlug) : null;
|
|
3382
|
+
const healed = manifestSlug !== void 0 ? await resolveModuleApiKey(manifestSlug, moduleEntry, apiUrl, apiToken) : null;
|
|
3383
|
+
if (healed) moduleEntry = healed;
|
|
3384
|
+
const apiKeyEnv = process.env.DASHWISE_API_KEY ?? moduleEntry?.apiKey;
|
|
2972
3385
|
let installationIdEnv;
|
|
2973
|
-
let devSessionIdEnv;
|
|
2974
3386
|
let revokeDevSession;
|
|
2975
3387
|
if (explicitInstallationId && explicitInstallationId !== "local") {
|
|
2976
3388
|
installationIdEnv = explicitInstallationId;
|
|
2977
|
-
} else if (reuseDevSessionId) {
|
|
2978
|
-
devSessionIdEnv = reuseDevSessionId;
|
|
2979
|
-
} else {
|
|
3389
|
+
} else if (reuseDevSessionId) ; else {
|
|
2980
3390
|
let workspaceContext;
|
|
2981
3391
|
try {
|
|
2982
3392
|
workspaceContext = await resolveTargetWorkspace(apiUrl, apiToken, options.workspace);
|
|
@@ -2990,7 +3400,6 @@ async function devCommand(options) {
|
|
|
2990
3400
|
target_workspace_id: workspaceContext.id,
|
|
2991
3401
|
callback_origins: callbackOrigins
|
|
2992
3402
|
});
|
|
2993
|
-
devSessionIdEnv = session.id;
|
|
2994
3403
|
revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session.id);
|
|
2995
3404
|
info(` ${dim("dev session")} ${session.id}`);
|
|
2996
3405
|
info(` ${dim("workspace")} ${workspaceContext.name} (id=${workspaceContext.id}, slug=${workspaceContext.slug})`);
|
|
@@ -3005,43 +3414,44 @@ async function devCommand(options) {
|
|
|
3005
3414
|
return 1;
|
|
3006
3415
|
}
|
|
3007
3416
|
}
|
|
3008
|
-
const childInstallationId = installationIdEnv ?? (runtimeTokenEnv !== void 0 ? "sandbox" : "");
|
|
3009
3417
|
const childEnv = {
|
|
3010
3418
|
...process.env,
|
|
3011
3419
|
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
3420
|
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 } : {}
|
|
3421
|
+
...apiKeyEnv !== void 0 ? { DASHWISE_API_KEY: apiKeyEnv } : {}
|
|
3024
3422
|
};
|
|
3025
3423
|
info(`Dev loop starting \u2014 Ctrl-C to stop.`);
|
|
3026
3424
|
info(` ${dim("api")} ${apiUrl}`);
|
|
3027
3425
|
if (installationIdEnv !== void 0) {
|
|
3028
3426
|
info(` ${dim("installation")} ${installationIdEnv} (prod-mode SSO)`);
|
|
3029
3427
|
} else {
|
|
3030
|
-
info(` ${dim("mode")} dev (
|
|
3428
|
+
info(` ${dim("mode")} dev (dev SSO session registered)`);
|
|
3031
3429
|
}
|
|
3032
|
-
info(` ${dim("token")}
|
|
3033
|
-
if (
|
|
3034
|
-
const source = process.env.
|
|
3430
|
+
info(` ${dim("cli token")} ${maskedToken(apiToken)} ${dim(`(from ${apiToken === savedConfig?.token ? "~/.config/dashwise/auth.json" : "$DASHWISE_API_TOKEN"})`)}`);
|
|
3431
|
+
if (apiKeyEnv !== void 0) {
|
|
3432
|
+
const source = process.env.DASHWISE_API_KEY === apiKeyEnv ? "$DASHWISE_API_KEY" : `~/.config/dashwise/auth.json#modules[${manifestSlug}]`;
|
|
3035
3433
|
const installId = moduleEntry?.installationId;
|
|
3036
3434
|
info(
|
|
3037
|
-
` ${dim("
|
|
3435
|
+
` ${dim("api key")} ${maskedToken(apiKeyEnv)} ${dim(`(from ${source}${installId ? `, install id=${installId}` : ""})`)}`
|
|
3038
3436
|
);
|
|
3039
3437
|
} else if (manifestSlug !== void 0 && installationIdEnv === void 0) {
|
|
3040
|
-
|
|
3041
|
-
`No
|
|
3438
|
+
fail(
|
|
3439
|
+
`No ${code("dwk_")} API key for module ${code(manifestSlug)} \u2014 the module will run with NO token.`
|
|
3440
|
+
);
|
|
3441
|
+
info(
|
|
3442
|
+
` Data-plane calls (db, qb) AND sign-in will fail until you provision a dev install:
|
|
3443
|
+
${code(`dashwise init ${manifestSlug} --force --workspace <your-workspace-slug>`)}
|
|
3444
|
+
(Ensure you're logged into the right backend: ${code(`dashwise login --api-url ${apiUrl}`)}.)`
|
|
3042
3445
|
);
|
|
3043
3446
|
}
|
|
3044
3447
|
info("");
|
|
3448
|
+
if (moduleEntry !== null && manifestSlug !== void 0) {
|
|
3449
|
+
await reportConnectStatus({
|
|
3450
|
+
installationId: moduleEntry.installationId,
|
|
3451
|
+
moduleSlug: manifestSlug,
|
|
3452
|
+
requestOptions: { baseUrl: apiUrl, auth: { token: apiToken } }
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3045
3455
|
const children = [];
|
|
3046
3456
|
if (!options.nextOnly) {
|
|
3047
3457
|
const sdkChild = spawnSdkWatcher(projectRoot, childEnv);
|
|
@@ -3056,18 +3466,19 @@ async function devCommand(options) {
|
|
|
3056
3466
|
pipeWithPrefix(child.stderr, process.stderr, prefix);
|
|
3057
3467
|
}
|
|
3058
3468
|
let manifestPoll = null;
|
|
3059
|
-
const
|
|
3060
|
-
|
|
3469
|
+
const pollEntry = moduleEntry;
|
|
3470
|
+
const enablePoll = !options.noRemotePoll && pollEntry !== null && apiKeyEnv !== void 0 && manifestSlug !== void 0;
|
|
3471
|
+
if (enablePoll && pollEntry !== null) {
|
|
3061
3472
|
const intervalSec = Math.max(
|
|
3062
3473
|
MIN_REMOTE_POLL_INTERVAL_SEC,
|
|
3063
3474
|
options.remotePollIntervalSec ?? DEFAULT_REMOTE_POLL_INTERVAL_SEC
|
|
3064
3475
|
);
|
|
3065
3476
|
manifestPoll = startManifestPollLoop({
|
|
3066
|
-
installationId: moduleEntry.installationId,
|
|
3067
3477
|
manifestPath: manifestPath2,
|
|
3068
3478
|
requestOptions: {
|
|
3069
3479
|
baseUrl: apiUrl,
|
|
3070
|
-
|
|
3480
|
+
// The `dwk_` key resolves the install server-side.
|
|
3481
|
+
auth: { token: apiKeyEnv }
|
|
3071
3482
|
},
|
|
3072
3483
|
intervalMs: intervalSec * 1e3,
|
|
3073
3484
|
onUpdate: (msg) => {
|
|
@@ -3200,6 +3611,51 @@ function maskedToken(token) {
|
|
|
3200
3611
|
const tail = token.slice(-4);
|
|
3201
3612
|
return `${prefix}\u2026${tail}`;
|
|
3202
3613
|
}
|
|
3614
|
+
async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
|
|
3615
|
+
if (process.env.DASHWISE_API_KEY) return null;
|
|
3616
|
+
if (!entry) return null;
|
|
3617
|
+
if (entry.apiKey) return null;
|
|
3618
|
+
if (!entry.installationId) return null;
|
|
3619
|
+
info(
|
|
3620
|
+
`No usable ${code("dwk_")} API key cached for ${code(slug)} \u2014 minting a fresh one against dev install ${entry.installationId} (a stale CLI can cache an empty key)\u2026`
|
|
3621
|
+
);
|
|
3622
|
+
let minted;
|
|
3623
|
+
try {
|
|
3624
|
+
minted = await createApiKey(
|
|
3625
|
+
entry.installationId,
|
|
3626
|
+
{ name: "local-dev" },
|
|
3627
|
+
{ baseUrl: apiUrl, auth: { token: cliBearer } }
|
|
3628
|
+
);
|
|
3629
|
+
} catch (err) {
|
|
3630
|
+
if (err instanceof ApiError && err.code === "INSTALLATION_NOT_FOUND") {
|
|
3631
|
+
warn(
|
|
3632
|
+
`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).`
|
|
3633
|
+
);
|
|
3634
|
+
return null;
|
|
3635
|
+
}
|
|
3636
|
+
const reason = err instanceof ApiError ? `${err.code}: ${err.message}` : err.message;
|
|
3637
|
+
warn(
|
|
3638
|
+
`Could not upgrade credentials for ${code(slug)} (${reason}). Data-plane calls may fail; run ${code(`dashwise init ${slug} --force`)} to refresh.`
|
|
3639
|
+
);
|
|
3640
|
+
return null;
|
|
3641
|
+
}
|
|
3642
|
+
const upgraded = {
|
|
3643
|
+
apiKey: minted.raw_key,
|
|
3644
|
+
apiKeyId: minted.api_key.id,
|
|
3645
|
+
installationId: entry.installationId,
|
|
3646
|
+
expiresAt: minted.api_key.expires_at ?? entry.expiresAt
|
|
3647
|
+
};
|
|
3648
|
+
try {
|
|
3649
|
+
upsertModuleEntry(slug, upgraded);
|
|
3650
|
+
} catch (err) {
|
|
3651
|
+
warn(
|
|
3652
|
+
`Minted a new API key for ${code(slug)} but failed to cache it: ${err.message}
|
|
3653
|
+
API key (save manually): ${minted.raw_key}`
|
|
3654
|
+
);
|
|
3655
|
+
}
|
|
3656
|
+
success(`Upgraded ${code(slug)} to a dwk_ API key (id=${minted.api_key.id}).`);
|
|
3657
|
+
return upgraded;
|
|
3658
|
+
}
|
|
3203
3659
|
function computeCallbackOrigins(port, extra) {
|
|
3204
3660
|
const def = `http://localhost:${port}`;
|
|
3205
3661
|
return Array.from(/* @__PURE__ */ new Set([def, ...extra]));
|
|
@@ -3887,7 +4343,7 @@ function isPlausibleRepoUrl(url) {
|
|
|
3887
4343
|
|
|
3888
4344
|
// src/commands/module/convert.ts
|
|
3889
4345
|
init_scaffold();
|
|
3890
|
-
var SDK_DEP_RANGE = "^0.
|
|
4346
|
+
var SDK_DEP_RANGE = "^2.0.0";
|
|
3891
4347
|
var SCAN_DIRS = ["app", "src", "pages", "lib", "components"];
|
|
3892
4348
|
var SCAN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
|
|
3893
4349
|
var MAX_SCAN_FILES = 2e3;
|
|
@@ -3943,6 +4399,7 @@ function convertModule(opts) {
|
|
|
3943
4399
|
writeIfMissing("app/api/auth/sign-in/route.ts", appApiAuthSignInTs());
|
|
3944
4400
|
writeIfMissing("app/api/auth/callback/route.ts", appApiAuthCallbackTs());
|
|
3945
4401
|
writeIfMissing("app/api/auth/sign-out/route.ts", appApiAuthSignOutTs());
|
|
4402
|
+
writeIfMissing("app/api/auth/session/route.ts", appApiAuthSessionTs());
|
|
3946
4403
|
writeIfMissing("app/sign-in-error/page.tsx", appSignInErrorPageTsx());
|
|
3947
4404
|
writeIfMissing(".env.local.example", envLocalExample());
|
|
3948
4405
|
const layoutFile = [
|
|
@@ -3954,7 +4411,7 @@ function convertModule(opts) {
|
|
|
3954
4411
|
writeIfMissing("app/layout.tsx", appLayoutCustomTsx(sopts));
|
|
3955
4412
|
} else if (!readFileSync(join(dir, layoutFile), "utf-8").includes("AuthProvider")) {
|
|
3956
4413
|
warnings.push(
|
|
3957
|
-
`${layoutFile}: wrap your tree in <AuthProvider> from "@dashai/sdk/auth/client" so client components can read the session.`
|
|
4414
|
+
`${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
4415
|
);
|
|
3959
4416
|
} else {
|
|
3960
4417
|
skipped.push(layoutFile);
|
|
@@ -4236,6 +4693,7 @@ async function moduleExportCommand(opts) {
|
|
|
4236
4693
|
init_config();
|
|
4237
4694
|
init_output();
|
|
4238
4695
|
init_manifest_io();
|
|
4696
|
+
init_scaffold();
|
|
4239
4697
|
init_workspaces();
|
|
4240
4698
|
init_api_client();
|
|
4241
4699
|
var MODULE_KINDS = ["ai_builder", "hand_authored", "custom"];
|
|
@@ -4258,6 +4716,13 @@ async function moduleRegisterCommand(opts) {
|
|
|
4258
4716
|
fail("module.json is missing `module.slug`.");
|
|
4259
4717
|
return 1;
|
|
4260
4718
|
}
|
|
4719
|
+
if (!isValidSlug(slug)) {
|
|
4720
|
+
fail(
|
|
4721
|
+
`${slugValidationMessage(slug)}
|
|
4722
|
+
Fix ${code("module.slug")} in module.json, then re-run ${code("dashwise module register")}.`
|
|
4723
|
+
);
|
|
4724
|
+
return 1;
|
|
4725
|
+
}
|
|
4261
4726
|
const name = opts.name ?? (typeof manifest.module?.name === "string" ? manifest.module.name : slug);
|
|
4262
4727
|
const moduleKind = MODULE_KINDS.includes(
|
|
4263
4728
|
manifest.module_kind
|
|
@@ -4295,7 +4760,12 @@ async function moduleRegisterCommand(opts) {
|
|
|
4295
4760
|
name,
|
|
4296
4761
|
...description !== void 0 ? { description } : {},
|
|
4297
4762
|
workspaceId: workspace2.id,
|
|
4298
|
-
...moduleKind !== void 0 ? { moduleKind } : {}
|
|
4763
|
+
...moduleKind !== void 0 ? { moduleKind } : {},
|
|
4764
|
+
// P1-C C2: push the whole local manifest so the backend
|
|
4765
|
+
// materializes owns.tables into the fresh draft. `init` omits
|
|
4766
|
+
// this (scaffolds empty); `register` sends it because the local
|
|
4767
|
+
// module.json is the source of truth for an already-authored app.
|
|
4768
|
+
manifest
|
|
4299
4769
|
},
|
|
4300
4770
|
{ baseUrl: resolveApiUrl(), auth: { token: cfg.token } }
|
|
4301
4771
|
);
|
|
@@ -4307,6 +4777,13 @@ async function moduleRegisterCommand(opts) {
|
|
|
4307
4777
|
);
|
|
4308
4778
|
} else if (err.code === "FORBIDDEN") {
|
|
4309
4779
|
fail(`Not a member of workspace "${workspace2.slug}". Join it, then retry.`);
|
|
4780
|
+
} else if (err.code === "MANIFEST_INVALID") {
|
|
4781
|
+
const issues = extractManifestIssues(err);
|
|
4782
|
+
fail(
|
|
4783
|
+
`Your module.json failed validation:
|
|
4784
|
+
${issues}
|
|
4785
|
+
Fix the issues above (or run ${code("dashwise module validate")} for the full report) and retry.`
|
|
4786
|
+
);
|
|
4310
4787
|
} else {
|
|
4311
4788
|
fail(`Registration failed (${err.code || err.status}): ${err.message}`);
|
|
4312
4789
|
}
|
|
@@ -4317,26 +4794,49 @@ async function moduleRegisterCommand(opts) {
|
|
|
4317
4794
|
}
|
|
4318
4795
|
try {
|
|
4319
4796
|
upsertModuleEntry(slug, {
|
|
4320
|
-
|
|
4797
|
+
apiKey: response.api_key,
|
|
4798
|
+
apiKeyId: response.api_key_id,
|
|
4321
4799
|
installationId: response.installation.id,
|
|
4322
|
-
expiresAt: response.
|
|
4800
|
+
expiresAt: response.api_key_expires_at
|
|
4323
4801
|
});
|
|
4324
4802
|
} catch (err) {
|
|
4325
4803
|
warn(
|
|
4326
|
-
`Registered (installation id=${response.installation.id}) but failed to cache the
|
|
4327
|
-
|
|
4328
|
-
Expires: ${response.
|
|
4804
|
+
`Registered (installation id=${response.installation.id}) but failed to cache the API key: ${err.message}
|
|
4805
|
+
API key (save this manually): ${response.api_key}
|
|
4806
|
+
Expires: ${response.api_key_expires_at}`
|
|
4329
4807
|
);
|
|
4330
4808
|
return 0;
|
|
4331
4809
|
}
|
|
4810
|
+
const tablesApplied = response.tables_applied ?? 0;
|
|
4332
4811
|
success(
|
|
4333
|
-
`Registered "${name}" \u2192 module #${response.module.id}, dev install #${response.installation.id}.
|
|
4812
|
+
`Registered "${name}" \u2192 module #${response.module.id}, dev install #${response.installation.id}. API key cached.`
|
|
4334
4813
|
);
|
|
4814
|
+
if (tablesApplied > 0) {
|
|
4815
|
+
info(
|
|
4816
|
+
`Materialized ${code(String(tablesApplied))} ${tablesApplied === 1 ? "table" : "tables"} from your module.json into the dev install.`
|
|
4817
|
+
);
|
|
4818
|
+
}
|
|
4335
4819
|
info(
|
|
4336
|
-
`Next: ${code("dashwise dev")} to run it against DashWise, or ${code("dashwise module publish")} to deploy
|
|
4820
|
+
`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
4821
|
);
|
|
4338
4822
|
return 0;
|
|
4339
4823
|
}
|
|
4824
|
+
function extractManifestIssues(err) {
|
|
4825
|
+
const issues = err.context?.issues;
|
|
4826
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
4827
|
+
return issues.map((issue) => {
|
|
4828
|
+
if (typeof issue === "string") return ` - ${issue}`;
|
|
4829
|
+
if (issue && typeof issue === "object") {
|
|
4830
|
+
const o = issue;
|
|
4831
|
+
const path = typeof o.path === "string" ? `${o.path}: ` : "";
|
|
4832
|
+
const message = typeof o.message === "string" ? o.message : JSON.stringify(issue);
|
|
4833
|
+
return ` - ${path}${message}`;
|
|
4834
|
+
}
|
|
4835
|
+
return ` - ${String(issue)}`;
|
|
4836
|
+
}).join("\n");
|
|
4837
|
+
}
|
|
4838
|
+
return ` - ${err.message}`;
|
|
4839
|
+
}
|
|
4340
4840
|
|
|
4341
4841
|
// src/commands/module/generate.ts
|
|
4342
4842
|
init_api_client();
|
|
@@ -5174,6 +5674,7 @@ function readIssues(context) {
|
|
|
5174
5674
|
|
|
5175
5675
|
// src/commands/deps/add.ts
|
|
5176
5676
|
init_api_client();
|
|
5677
|
+
init_config();
|
|
5177
5678
|
init_manifest_io();
|
|
5178
5679
|
init_output();
|
|
5179
5680
|
var MIN_PURPOSE_LEN = 10;
|
|
@@ -5309,9 +5810,59 @@ async function depsAddCommand(providerSlug, options) {
|
|
|
5309
5810
|
);
|
|
5310
5811
|
info(`Reads: ${entry.reads.map((r) => `${code(r.table)} (${r.fields.join(", ")})`).join("; ")}`);
|
|
5311
5812
|
info(`Run ${code("dashwise module generate")} to refresh typed wrappers.`);
|
|
5813
|
+
await reportConnectStatus2(manifest.module?.slug, providerSlug);
|
|
5312
5814
|
if (isInteractive) outro("Done.");
|
|
5313
5815
|
return 0;
|
|
5314
5816
|
}
|
|
5817
|
+
async function reportConnectStatus2(consumerSlug, providerSlug) {
|
|
5818
|
+
if (!consumerSlug) return;
|
|
5819
|
+
let cfg;
|
|
5820
|
+
let entry;
|
|
5821
|
+
try {
|
|
5822
|
+
cfg = readConfig();
|
|
5823
|
+
entry = readModuleEntry(consumerSlug);
|
|
5824
|
+
} catch {
|
|
5825
|
+
cfg = null;
|
|
5826
|
+
entry = null;
|
|
5827
|
+
}
|
|
5828
|
+
if (!cfg || !entry) {
|
|
5829
|
+
info(
|
|
5830
|
+
`Once provisioned (${code("dashwise init")}) + pushed (${code("dashwise dev")}), run ${code("dashwise deps")} to see whether ${code(providerSlug)} resolves or parks.`
|
|
5831
|
+
);
|
|
5832
|
+
return;
|
|
5833
|
+
}
|
|
5834
|
+
const requestOptions = {
|
|
5835
|
+
baseUrl: resolveApiUrl(),
|
|
5836
|
+
auth: { token: cfg.token }
|
|
5837
|
+
};
|
|
5838
|
+
try {
|
|
5839
|
+
const deps = await getInstallationDependencies(entry.installationId, requestOptions);
|
|
5840
|
+
const match = deps.find((d) => d.slug === providerSlug);
|
|
5841
|
+
if (!match) {
|
|
5842
|
+
info(
|
|
5843
|
+
`${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.`
|
|
5844
|
+
);
|
|
5845
|
+
return;
|
|
5846
|
+
}
|
|
5847
|
+
if (match.status === "resolved") {
|
|
5848
|
+
const providerLine = match.provider != null ? ` (${dim(`provider install #${match.provider.installation_id} @ v${match.provider.version}`)})` : "";
|
|
5849
|
+
out(` ${pc.green("\u25CF resolved")}${providerLine}`);
|
|
5850
|
+
} else {
|
|
5851
|
+
out(` ${pc.yellow("\u25CB unbound")} ${dim("reason:")} ${match.reason ?? dim("(unspecified)")}`);
|
|
5852
|
+
info(
|
|
5853
|
+
`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.`
|
|
5854
|
+
);
|
|
5855
|
+
}
|
|
5856
|
+
} catch (err) {
|
|
5857
|
+
if (err instanceof ApiError && err.status === 401) {
|
|
5858
|
+
info(`(Skipped connect-status check \u2014 CLI token stale; run ${code("dashwise login")} to refresh.)`);
|
|
5859
|
+
} else {
|
|
5860
|
+
info(
|
|
5861
|
+
`(Couldn't fetch connect status right now \u2014 run ${code("dashwise deps")} later to see whether ${code(providerSlug)} resolves or parks.)`
|
|
5862
|
+
);
|
|
5863
|
+
}
|
|
5864
|
+
}
|
|
5865
|
+
}
|
|
5315
5866
|
async function collectInteractive(providerSlug, summary, resolvedVersion, options) {
|
|
5316
5867
|
const reads = [];
|
|
5317
5868
|
if (summary && summary.exposes && summary.exposes.length > 0) {
|
|
@@ -5730,96 +6281,7 @@ async function depsRemoveCommand(providerSlug, options) {
|
|
|
5730
6281
|
return 0;
|
|
5731
6282
|
}
|
|
5732
6283
|
|
|
5733
|
-
// src/commands/
|
|
5734
|
-
init_api_client();
|
|
5735
|
-
init_manifest_io();
|
|
5736
|
-
init_schema_command_context();
|
|
5737
|
-
init_output();
|
|
5738
|
-
async function pullCommand(options) {
|
|
5739
|
-
let ctx;
|
|
5740
|
-
try {
|
|
5741
|
-
ctx = loadSchemaCommandContext({
|
|
5742
|
-
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
5743
|
-
});
|
|
5744
|
-
} catch (err) {
|
|
5745
|
-
fail(err.message);
|
|
5746
|
-
return 1;
|
|
5747
|
-
}
|
|
5748
|
-
info(`Pulling manifest for installation ${ctx.entry.installationId}\u2026`);
|
|
5749
|
-
let remoteManifest;
|
|
5750
|
-
try {
|
|
5751
|
-
const response = await getInstallationSchema(
|
|
5752
|
-
ctx.entry.installationId,
|
|
5753
|
-
ctx.requestOptions
|
|
5754
|
-
);
|
|
5755
|
-
remoteManifest = response.manifest;
|
|
5756
|
-
} catch (err) {
|
|
5757
|
-
return surfacePullFailure(err);
|
|
5758
|
-
}
|
|
5759
|
-
const localJson = JSON.stringify(ctx.manifest, null, 2) + "\n";
|
|
5760
|
-
const remoteJson = JSON.stringify(remoteManifest, null, 2) + "\n";
|
|
5761
|
-
if (localJson === remoteJson) {
|
|
5762
|
-
success("Already in sync \u2014 local module.json matches backend.");
|
|
5763
|
-
return 0;
|
|
5764
|
-
}
|
|
5765
|
-
const localBytes = Buffer.byteLength(localJson, "utf8");
|
|
5766
|
-
const remoteBytes = Buffer.byteLength(remoteJson, "utf8");
|
|
5767
|
-
const delta = remoteBytes - localBytes;
|
|
5768
|
-
const deltaStr = delta === 0 ? "0 bytes" : `${delta > 0 ? "+" : ""}${delta} bytes`;
|
|
5769
|
-
if (options.dryRun) {
|
|
5770
|
-
info(
|
|
5771
|
-
`Drift detected (${deltaStr}). ${code("--dry-run")} \u2014 not writing.`
|
|
5772
|
-
);
|
|
5773
|
-
info(`Run ${code("dashwise pull")} without ${code("--dry-run")} to apply.`);
|
|
5774
|
-
return 0;
|
|
5775
|
-
}
|
|
5776
|
-
try {
|
|
5777
|
-
writeManifest(remoteManifest, ctx.projectRoot);
|
|
5778
|
-
} catch (err) {
|
|
5779
|
-
fail(`Failed to write ${manifestPath(ctx.projectRoot)}: ${err.message}`);
|
|
5780
|
-
return 1;
|
|
5781
|
-
}
|
|
5782
|
-
success(
|
|
5783
|
-
`Synced module.json from backend (${deltaStr}). ${dim(`Run \`dashwise module generate\` to refresh typed SDK if your watcher isn't running.`)}`
|
|
5784
|
-
);
|
|
5785
|
-
return 0;
|
|
5786
|
-
}
|
|
5787
|
-
function surfacePullFailure(err) {
|
|
5788
|
-
if (err instanceof ApiError) {
|
|
5789
|
-
switch (err.code) {
|
|
5790
|
-
case "UNAUTHENTICATED":
|
|
5791
|
-
fail(
|
|
5792
|
-
`Runtime token rejected. The cached token for this module may be expired \u2014 re-run \`dashwise init <slug> --force\` to mint a fresh one.`
|
|
5793
|
-
);
|
|
5794
|
-
break;
|
|
5795
|
-
case "SANDBOX_TOKEN_NOT_ACCEPTED":
|
|
5796
|
-
fail(
|
|
5797
|
-
`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\`.`
|
|
5798
|
-
);
|
|
5799
|
-
break;
|
|
5800
|
-
case "INSTALL_MISMATCH":
|
|
5801
|
-
fail(
|
|
5802
|
-
`Runtime token is bound to a different installation than expected. The cached install id may be stale \u2014 re-run \`dashwise init <slug> --force\`.`
|
|
5803
|
-
);
|
|
5804
|
-
break;
|
|
5805
|
-
case "INSTALLATION_NOT_FOUND":
|
|
5806
|
-
fail(
|
|
5807
|
-
`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.`
|
|
5808
|
-
);
|
|
5809
|
-
break;
|
|
5810
|
-
default:
|
|
5811
|
-
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
5812
|
-
}
|
|
5813
|
-
return 1;
|
|
5814
|
-
}
|
|
5815
|
-
fail(`Pull failed: ${err.message}`);
|
|
5816
|
-
return 1;
|
|
5817
|
-
}
|
|
5818
|
-
|
|
5819
|
-
// src/bin.ts
|
|
5820
|
-
init_scaffold();
|
|
5821
|
-
|
|
5822
|
-
// src/commands/api-keys.ts
|
|
6284
|
+
// src/commands/deps/connect.ts
|
|
5823
6285
|
init_api_client();
|
|
5824
6286
|
init_config();
|
|
5825
6287
|
init_manifest_io();
|
|
@@ -5828,9 +6290,7 @@ function loadContext(opts) {
|
|
|
5828
6290
|
const manifest = readManifestOrThrow(opts.dir);
|
|
5829
6291
|
const slug = manifest.module?.slug;
|
|
5830
6292
|
if (!slug) {
|
|
5831
|
-
throw new Error(
|
|
5832
|
-
`module.json has no module.slug \u2014 the manifest is malformed.`
|
|
5833
|
-
);
|
|
6293
|
+
throw new Error("module.json has no module.slug \u2014 the manifest is malformed.");
|
|
5834
6294
|
}
|
|
5835
6295
|
const cfg = readConfig();
|
|
5836
6296
|
if (!cfg) {
|
|
@@ -5851,7 +6311,12 @@ function loadContext(opts) {
|
|
|
5851
6311
|
}
|
|
5852
6312
|
};
|
|
5853
6313
|
}
|
|
5854
|
-
|
|
6314
|
+
function statusChip(status) {
|
|
6315
|
+
if (status === "resolved") return pc.green("\u25CF resolved");
|
|
6316
|
+
if (status === "unbound") return pc.yellow("\u25CB unbound");
|
|
6317
|
+
return pc.dim(`? ${status}`);
|
|
6318
|
+
}
|
|
6319
|
+
async function depsConnectListCommand(options) {
|
|
5855
6320
|
let ctx;
|
|
5856
6321
|
try {
|
|
5857
6322
|
ctx = loadContext(options);
|
|
@@ -5859,40 +6324,50 @@ async function apiKeysListCommand(options) {
|
|
|
5859
6324
|
fail(err.message);
|
|
5860
6325
|
return 1;
|
|
5861
6326
|
}
|
|
5862
|
-
let
|
|
6327
|
+
let deps;
|
|
5863
6328
|
try {
|
|
5864
|
-
|
|
5865
|
-
keys = resp.api_keys;
|
|
6329
|
+
deps = await getInstallationDependencies(ctx.installationId, ctx.requestOptions);
|
|
5866
6330
|
} catch (err) {
|
|
5867
|
-
return
|
|
6331
|
+
return surfaceDepError(err);
|
|
5868
6332
|
}
|
|
5869
|
-
const filtered = options.all ? keys : keys.filter((k) => k.revoked_at === null);
|
|
5870
6333
|
if (options.json) {
|
|
5871
|
-
|
|
5872
|
-
`);
|
|
6334
|
+
out(JSON.stringify(deps, null, 2));
|
|
5873
6335
|
return 0;
|
|
5874
6336
|
}
|
|
5875
|
-
if (
|
|
5876
|
-
info(
|
|
5877
|
-
|
|
5878
|
-
);
|
|
6337
|
+
if (deps.length === 0) {
|
|
6338
|
+
info(`No cross-module dependencies declared for ${code(ctx.slug)}.`);
|
|
6339
|
+
info(`Add one with ${code("dashwise deps add <provider-slug>")}.`);
|
|
5879
6340
|
return 0;
|
|
5880
6341
|
}
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
6342
|
+
const unbound = deps.filter((d) => d.status === "unbound").length;
|
|
6343
|
+
info(
|
|
6344
|
+
`Cross-module dependencies for ${code(ctx.slug)} ${dim(
|
|
6345
|
+
`(${deps.length} declared${unbound > 0 ? `, ${unbound} unbound` : ""})`
|
|
6346
|
+
)}:`
|
|
6347
|
+
);
|
|
6348
|
+
out("");
|
|
6349
|
+
for (const dep of deps) {
|
|
6350
|
+
out(` ${statusChip(dep.status)} ${code(dep.slug)}@${dep.range ?? "*"}`);
|
|
6351
|
+
if (dep.status === "resolved" && dep.provider) {
|
|
6352
|
+
out(
|
|
6353
|
+
` ${dim("provider:")} install #${dep.provider.installation_id} @ v${dep.provider.version}`
|
|
6354
|
+
);
|
|
6355
|
+
}
|
|
6356
|
+
if (dep.status === "unbound") {
|
|
6357
|
+
out(` ${dim("reason:")} ${dep.reason ?? dim("(unspecified)")}`);
|
|
6358
|
+
}
|
|
6359
|
+
out("");
|
|
6360
|
+
}
|
|
6361
|
+
if (unbound > 0) {
|
|
6362
|
+
info(
|
|
6363
|
+
`Connect a parked dep with ${code("dashwise deps bind <provider-slug>")} once its provider is installed.`
|
|
5888
6364
|
);
|
|
5889
6365
|
}
|
|
5890
6366
|
return 0;
|
|
5891
6367
|
}
|
|
5892
|
-
async function
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
fail("Usage: `dashwise api-keys create <name>` (e.g. `dashwise api-keys create production`)");
|
|
6368
|
+
async function depsBindCommand(providerSlug, options) {
|
|
6369
|
+
if (!providerSlug || typeof providerSlug !== "string") {
|
|
6370
|
+
fail("Usage: `dashwise deps bind <provider-slug> [--installation <id>]`");
|
|
5896
6371
|
return 1;
|
|
5897
6372
|
}
|
|
5898
6373
|
let ctx;
|
|
@@ -5902,23 +6377,1091 @@ async function apiKeysCreateCommand(nameArg, options) {
|
|
|
5902
6377
|
fail(err.message);
|
|
5903
6378
|
return 1;
|
|
5904
6379
|
}
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
6380
|
+
try {
|
|
6381
|
+
const result = await bindDependency(
|
|
6382
|
+
ctx.installationId,
|
|
6383
|
+
providerSlug,
|
|
6384
|
+
options.installation !== void 0 ? { provider_installation_id: options.installation } : {},
|
|
6385
|
+
ctx.requestOptions
|
|
6386
|
+
);
|
|
6387
|
+
const providerLine = result.provider != null ? ` \u2192 install #${result.provider.installation_id} @ v${result.provider.version}` : "";
|
|
6388
|
+
success(`Bound ${code(providerSlug)} for ${code(ctx.slug)}${providerLine}.`);
|
|
6389
|
+
info("Runtime reads against this dependency will now succeed.");
|
|
6390
|
+
return 0;
|
|
6391
|
+
} catch (err) {
|
|
6392
|
+
return surfaceBindError(err, providerSlug, ctx.slug);
|
|
5908
6393
|
}
|
|
5909
|
-
|
|
5910
|
-
|
|
6394
|
+
}
|
|
6395
|
+
async function depsUnbindCommand(providerSlug, options) {
|
|
6396
|
+
if (!providerSlug || typeof providerSlug !== "string") {
|
|
6397
|
+
fail("Usage: `dashwise deps unbind <provider-slug>`");
|
|
6398
|
+
return 1;
|
|
5911
6399
|
}
|
|
5912
|
-
let
|
|
6400
|
+
let ctx;
|
|
5913
6401
|
try {
|
|
5914
|
-
|
|
6402
|
+
ctx = loadContext(options);
|
|
5915
6403
|
} catch (err) {
|
|
5916
|
-
|
|
6404
|
+
fail(err.message);
|
|
6405
|
+
return 1;
|
|
5917
6406
|
}
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
6407
|
+
try {
|
|
6408
|
+
await unbindDependency(ctx.installationId, providerSlug, ctx.requestOptions);
|
|
6409
|
+
success(`Unbound ${code(providerSlug)} for ${code(ctx.slug)}.`);
|
|
6410
|
+
warn(
|
|
6411
|
+
`Runtime reads against this dependency now return ${code("DEP_UNBOUND")} until you ${code("dashwise deps bind")} it again.`
|
|
6412
|
+
);
|
|
6413
|
+
return 0;
|
|
6414
|
+
} catch (err) {
|
|
6415
|
+
return surfaceDepError(err);
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6418
|
+
function surfaceBindError(err, providerSlug, consumerSlug) {
|
|
6419
|
+
if (err instanceof ApiError) {
|
|
6420
|
+
switch (err.code) {
|
|
6421
|
+
case "MISSING_DEPENDENCY":
|
|
6422
|
+
fail(
|
|
6423
|
+
`No installation of ${code(providerSlug)} found in ${code(consumerSlug)}'s workspace. Install the provider module first, then re-run ${code(`dashwise deps bind ${providerSlug}`)}.`
|
|
6424
|
+
);
|
|
6425
|
+
return 1;
|
|
6426
|
+
case "DEPENDENCY_VERSION_MISMATCH":
|
|
6427
|
+
fail(
|
|
6428
|
+
`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.`
|
|
6429
|
+
);
|
|
6430
|
+
return 1;
|
|
6431
|
+
case "PROVIDER_VERSION_RECORD_MISSING":
|
|
6432
|
+
fail(
|
|
6433
|
+
`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.`
|
|
6434
|
+
);
|
|
6435
|
+
return 1;
|
|
6436
|
+
case "EXPOSES_MISSING":
|
|
6437
|
+
fail(
|
|
6438
|
+
`${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.`
|
|
6439
|
+
);
|
|
6440
|
+
return 1;
|
|
6441
|
+
case "EXPOSES_PRIVATE":
|
|
6442
|
+
fail(
|
|
6443
|
+
`${code(providerSlug)} exposes the requested surface as ${code("private")} \u2014 it can't be read by depending modules (${err.message}).`
|
|
6444
|
+
);
|
|
6445
|
+
return 1;
|
|
6446
|
+
case "ACTION_NOT_EXPOSED":
|
|
6447
|
+
fail(
|
|
6448
|
+
`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.`
|
|
6449
|
+
);
|
|
6450
|
+
return 1;
|
|
6451
|
+
case "PROVIDER_NOT_PRODUCTION":
|
|
6452
|
+
fail(
|
|
6453
|
+
`A production install can only bind to a production provider install; the available ${code(providerSlug)} install isn't production (${err.message}).`
|
|
6454
|
+
);
|
|
6455
|
+
return 1;
|
|
6456
|
+
case "DEPENDENCY_NOT_DECLARED":
|
|
6457
|
+
fail(
|
|
6458
|
+
`${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.`
|
|
6459
|
+
);
|
|
6460
|
+
return 1;
|
|
6461
|
+
default:
|
|
6462
|
+
fail(
|
|
6463
|
+
`Could not bind ${code(providerSlug)}: ${err.code}${err.message ? ` \u2014 ${err.message}` : ""}. The dependency stays unbound.`
|
|
6464
|
+
);
|
|
6465
|
+
return 1;
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
fail(`Bind failed: ${err.message}`);
|
|
6469
|
+
return 1;
|
|
6470
|
+
}
|
|
6471
|
+
function surfaceDepError(err) {
|
|
6472
|
+
if (err instanceof ApiError) {
|
|
6473
|
+
switch (err.code) {
|
|
6474
|
+
case "UNAUTHENTICATED":
|
|
6475
|
+
fail(
|
|
6476
|
+
`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
|
|
6477
|
+
);
|
|
6478
|
+
return 1;
|
|
6479
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
6480
|
+
fail(
|
|
6481
|
+
`You're not a member of this installation's workspace. Dependency management requires workspace membership.`
|
|
6482
|
+
);
|
|
6483
|
+
return 1;
|
|
6484
|
+
case "NOT_WORKSPACE_ADMIN":
|
|
6485
|
+
case "FORBIDDEN":
|
|
6486
|
+
fail(
|
|
6487
|
+
`Not authorized. Binding / unbinding dependencies requires workspace-admin.`
|
|
6488
|
+
);
|
|
6489
|
+
return 1;
|
|
6490
|
+
case "INSTALLATION_NOT_FOUND":
|
|
6491
|
+
fail(
|
|
6492
|
+
`Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
|
|
6493
|
+
);
|
|
6494
|
+
return 1;
|
|
6495
|
+
case "DEPENDENCY_NOT_DECLARED":
|
|
6496
|
+
fail(
|
|
6497
|
+
`No such dependency declared in this module's manifest. Run ${code("dashwise deps")} to see declared deps.`
|
|
6498
|
+
);
|
|
6499
|
+
return 1;
|
|
6500
|
+
default:
|
|
6501
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
6502
|
+
return 1;
|
|
6503
|
+
}
|
|
6504
|
+
}
|
|
6505
|
+
fail(`Dependency operation failed: ${err.message}`);
|
|
6506
|
+
return 1;
|
|
6507
|
+
}
|
|
6508
|
+
|
|
6509
|
+
// src/commands/uses/bind.ts
|
|
6510
|
+
init_api_client();
|
|
6511
|
+
init_config();
|
|
6512
|
+
init_manifest_io();
|
|
6513
|
+
init_output();
|
|
6514
|
+
function loadContext2(opts) {
|
|
6515
|
+
const manifest = readManifestOrThrow(opts.dir);
|
|
6516
|
+
const slug = manifest.module?.slug;
|
|
6517
|
+
if (!slug) {
|
|
6518
|
+
throw new Error("module.json has no module.slug \u2014 the manifest is malformed.");
|
|
6519
|
+
}
|
|
6520
|
+
const cfg = readConfig();
|
|
6521
|
+
if (!cfg) {
|
|
6522
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
6523
|
+
}
|
|
6524
|
+
const entry = readModuleEntry(slug);
|
|
6525
|
+
if (!entry) {
|
|
6526
|
+
throw new Error(
|
|
6527
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
|
|
6528
|
+
);
|
|
6529
|
+
}
|
|
6530
|
+
return {
|
|
6531
|
+
installationId: entry.installationId,
|
|
6532
|
+
slug,
|
|
6533
|
+
...opts.dir !== void 0 ? { dir: opts.dir } : {},
|
|
6534
|
+
requestOptions: {
|
|
6535
|
+
baseUrl: resolveApiUrl(),
|
|
6536
|
+
auth: { token: cfg.token }
|
|
6537
|
+
}
|
|
6538
|
+
};
|
|
6539
|
+
}
|
|
6540
|
+
function statusChip2(status) {
|
|
6541
|
+
if (status === "bound") return pc.green("\u25CF bound");
|
|
6542
|
+
if (status === "unbound") return pc.yellow("\u25CB unbound");
|
|
6543
|
+
if (status === "declined") return pc.dim("\u2298 declined");
|
|
6544
|
+
return pc.dim(`? ${status}`);
|
|
6545
|
+
}
|
|
6546
|
+
function opsLine(ops) {
|
|
6547
|
+
return ops.length > 0 ? ops.join(", ") : dim("(none)");
|
|
6548
|
+
}
|
|
6549
|
+
async function bindListCommand(options) {
|
|
6550
|
+
let ctx;
|
|
6551
|
+
try {
|
|
6552
|
+
ctx = loadContext2(options);
|
|
6553
|
+
} catch (err) {
|
|
6554
|
+
fail(err.message);
|
|
6555
|
+
return 1;
|
|
6556
|
+
}
|
|
6557
|
+
let bindings;
|
|
6558
|
+
try {
|
|
6559
|
+
bindings = await getInstallationBindings(ctx.installationId, ctx.requestOptions);
|
|
6560
|
+
} catch (err) {
|
|
6561
|
+
return surfaceBindingError(err);
|
|
6562
|
+
}
|
|
6563
|
+
if (options.json) {
|
|
6564
|
+
out(JSON.stringify(bindings, null, 2));
|
|
6565
|
+
return 0;
|
|
6566
|
+
}
|
|
6567
|
+
if (bindings.length === 0) {
|
|
6568
|
+
info(`No workspace-table ${code("uses")} slots declared for ${code(ctx.slug)}.`);
|
|
6569
|
+
info(
|
|
6570
|
+
`Declare one by hand in ${code("module.json#uses.tables")}, or on-ramp an existing table with ${code("dashwise uses add --from-table <name>")}.`
|
|
6571
|
+
);
|
|
6572
|
+
return 0;
|
|
6573
|
+
}
|
|
6574
|
+
const unbound = bindings.filter((b) => b.status === "unbound").length;
|
|
6575
|
+
info(
|
|
6576
|
+
`Workspace-table bindings for ${code(ctx.slug)} ${dim(
|
|
6577
|
+
`(${bindings.length} declared${unbound > 0 ? `, ${unbound} unbound` : ""})`
|
|
6578
|
+
)}:`
|
|
6579
|
+
);
|
|
6580
|
+
out("");
|
|
6581
|
+
for (const b of bindings) {
|
|
6582
|
+
const enforced = b.ops.filter((o) => (b.consented_ops ?? []).includes(o));
|
|
6583
|
+
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);
|
|
6584
|
+
out(` ${statusChip2(b.status)} ${code(b.uses_slug)} ${dim(`[${opsSummary}]`)}`);
|
|
6585
|
+
if (b.purpose) {
|
|
6586
|
+
out(` ${dim("purpose:")} ${b.purpose}`);
|
|
6587
|
+
}
|
|
6588
|
+
if (b.status === "bound" && b.table) {
|
|
6589
|
+
out(` ${dim("table:")} ${b.table.name} ${dim(`(id ${b.table.id})`)}`);
|
|
6590
|
+
const mapped = b.field_map ? Object.keys(b.field_map).length : 0;
|
|
6591
|
+
if (mapped > 0) {
|
|
6592
|
+
out(` ${dim("fields:")} ${mapped} mapped`);
|
|
6593
|
+
}
|
|
6594
|
+
}
|
|
6595
|
+
if (b.status === "unbound") {
|
|
6596
|
+
out(` ${dim("reason:")} ${b.reason ?? dim("(unspecified)")}`);
|
|
6597
|
+
}
|
|
6598
|
+
if (b.missing_fields && b.missing_fields.length > 0) {
|
|
6599
|
+
out(
|
|
6600
|
+
` ${pc.yellow("missing:")} ${b.missing_fields.join(", ")} ${dim("(required field(s) not mapped)")}`
|
|
6601
|
+
);
|
|
6602
|
+
}
|
|
6603
|
+
out("");
|
|
6604
|
+
}
|
|
6605
|
+
if (unbound > 0) {
|
|
6606
|
+
info(
|
|
6607
|
+
`Connect a parked slot with ${code("dashwise bind <uses-slug> --table <name>")} (or ${code("--create")} to materialize a fresh table).`
|
|
6608
|
+
);
|
|
6609
|
+
}
|
|
6610
|
+
return 0;
|
|
6611
|
+
}
|
|
6612
|
+
async function bindCommand(usesSlug, options) {
|
|
6613
|
+
if (!usesSlug || typeof usesSlug !== "string") {
|
|
6614
|
+
fail(
|
|
6615
|
+
"Usage: `dashwise bind <uses-slug> [--table <name-or-id>] [--map <usesField>=<physicalFieldName>...]`, `--create`, or `--decline`."
|
|
6616
|
+
);
|
|
6617
|
+
return 1;
|
|
6618
|
+
}
|
|
6619
|
+
const modes = [options.create, options.decline, options.table !== void 0].filter(
|
|
6620
|
+
Boolean
|
|
6621
|
+
).length;
|
|
6622
|
+
if (modes > 1) {
|
|
6623
|
+
fail(
|
|
6624
|
+
`Pass at most one of ${code("--table")}, ${code("--create")}, ${code("--decline")} \u2014 they select different bind modes.`
|
|
6625
|
+
);
|
|
6626
|
+
return 1;
|
|
6627
|
+
}
|
|
6628
|
+
if (options.decline && (options.map ?? []).length > 0) {
|
|
6629
|
+
fail(`${code("--map")} makes no sense with ${code("--decline")} (nothing is being bound).`);
|
|
6630
|
+
return 1;
|
|
6631
|
+
}
|
|
6632
|
+
let ctx;
|
|
6633
|
+
try {
|
|
6634
|
+
ctx = loadContext2(options);
|
|
6635
|
+
} catch (err) {
|
|
6636
|
+
fail(err.message);
|
|
6637
|
+
return 1;
|
|
6638
|
+
}
|
|
6639
|
+
if (options.decline) {
|
|
6640
|
+
return declineSlot(ctx, usesSlug);
|
|
6641
|
+
}
|
|
6642
|
+
if (options.create) {
|
|
6643
|
+
return bindViaCreate(ctx, usesSlug);
|
|
6644
|
+
}
|
|
6645
|
+
let mapOverrides;
|
|
6646
|
+
try {
|
|
6647
|
+
mapOverrides = parseMapOverrides(options.map);
|
|
6648
|
+
} catch (err) {
|
|
6649
|
+
fail(err.message);
|
|
6650
|
+
return 1;
|
|
6651
|
+
}
|
|
6652
|
+
let candidates;
|
|
6653
|
+
try {
|
|
6654
|
+
candidates = await getBindingCandidates(
|
|
6655
|
+
ctx.installationId,
|
|
6656
|
+
usesSlug,
|
|
6657
|
+
ctx.requestOptions
|
|
6658
|
+
);
|
|
6659
|
+
} catch (err) {
|
|
6660
|
+
return surfaceBindingError(err);
|
|
6661
|
+
}
|
|
6662
|
+
const chosen = await chooseCandidate(candidates, usesSlug, options.table);
|
|
6663
|
+
if (chosen === "cancelled") {
|
|
6664
|
+
info("Cancelled.");
|
|
6665
|
+
return 130;
|
|
6666
|
+
}
|
|
6667
|
+
if (chosen === null) {
|
|
6668
|
+
return 1;
|
|
6669
|
+
}
|
|
6670
|
+
let fieldMap;
|
|
6671
|
+
try {
|
|
6672
|
+
fieldMap = await buildFieldMap(ctx, chosen, mapOverrides);
|
|
6673
|
+
} catch (err) {
|
|
6674
|
+
if (err instanceof ApiError) return surfaceBindingError(err);
|
|
6675
|
+
fail(err.message);
|
|
6676
|
+
return 1;
|
|
6677
|
+
}
|
|
6678
|
+
let result;
|
|
6679
|
+
try {
|
|
6680
|
+
result = await putBinding(
|
|
6681
|
+
ctx.installationId,
|
|
6682
|
+
usesSlug,
|
|
6683
|
+
fieldMap !== void 0 ? { table_id: chosen.table_id, field_map: fieldMap } : { table_id: chosen.table_id },
|
|
6684
|
+
ctx.requestOptions
|
|
6685
|
+
);
|
|
6686
|
+
} catch (err) {
|
|
6687
|
+
return surfacePutBindingError(err, usesSlug, ctx.slug);
|
|
6688
|
+
}
|
|
6689
|
+
reportBoundOk(result, ctx.slug);
|
|
6690
|
+
return 0;
|
|
6691
|
+
}
|
|
6692
|
+
async function chooseCandidate(candidates, usesSlug, tableFlag) {
|
|
6693
|
+
if (candidates.length === 0) {
|
|
6694
|
+
fail(
|
|
6695
|
+
`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).`
|
|
6696
|
+
);
|
|
6697
|
+
return null;
|
|
6698
|
+
}
|
|
6699
|
+
if (tableFlag !== void 0) {
|
|
6700
|
+
const match = matchCandidate(candidates, tableFlag);
|
|
6701
|
+
if (!match) {
|
|
6702
|
+
const list2 = candidates.map((c2) => ` - ${c2.name} ${dim(`(id ${c2.table_id}, score ${c2.match_score})`)}`).join("\n");
|
|
6703
|
+
fail(
|
|
6704
|
+
`No candidate table matching ${code(tableFlag)} for ${code(usesSlug)}. Candidates:
|
|
6705
|
+
${list2}`
|
|
6706
|
+
);
|
|
6707
|
+
return null;
|
|
6708
|
+
}
|
|
6709
|
+
return match;
|
|
6710
|
+
}
|
|
6711
|
+
if (candidates.length === 1) {
|
|
6712
|
+
return candidates[0];
|
|
6713
|
+
}
|
|
6714
|
+
if (process.stdout.isTTY) {
|
|
6715
|
+
const choice = await select({
|
|
6716
|
+
message: `Which workspace table should ${usesSlug} bind to?`,
|
|
6717
|
+
options: candidates.map((c2) => ({
|
|
6718
|
+
value: String(c2.table_id),
|
|
6719
|
+
label: c2.name,
|
|
6720
|
+
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`
|
|
6721
|
+
}))
|
|
6722
|
+
});
|
|
6723
|
+
if (isCancel(choice)) return "cancelled";
|
|
6724
|
+
return candidates.find((c2) => String(c2.table_id) === choice) ?? null;
|
|
6725
|
+
}
|
|
6726
|
+
const list = candidates.map((c2) => ` - ${c2.name} ${dim(`(id ${c2.table_id}, score ${c2.match_score})`)}`).join("\n");
|
|
6727
|
+
fail(
|
|
6728
|
+
`${candidates.length} candidate tables for ${code(usesSlug)}. Re-run with ${code("--table <name-or-id>")} to pick one:
|
|
6729
|
+
${list}`
|
|
6730
|
+
);
|
|
6731
|
+
return null;
|
|
6732
|
+
}
|
|
6733
|
+
function matchCandidate(candidates, flag) {
|
|
6734
|
+
const asNumber = Number.parseInt(flag, 10);
|
|
6735
|
+
const isNumeric = !Number.isNaN(asNumber) && String(asNumber) === flag;
|
|
6736
|
+
if (isNumeric) {
|
|
6737
|
+
return candidates.find((c2) => c2.table_id === asNumber);
|
|
6738
|
+
}
|
|
6739
|
+
return candidates.find((c2) => c2.name === flag);
|
|
6740
|
+
}
|
|
6741
|
+
async function buildFieldMap(ctx, candidate, overrides) {
|
|
6742
|
+
const overrideKeys = Object.keys(overrides);
|
|
6743
|
+
if (overrideKeys.length === 0) {
|
|
6744
|
+
return Object.keys(candidate.auto_field_map).length > 0 ? { ...candidate.auto_field_map } : void 0;
|
|
6745
|
+
}
|
|
6746
|
+
const table = await introspectWorkspaceTable(
|
|
6747
|
+
ctx.installationId,
|
|
6748
|
+
String(candidate.table_id),
|
|
6749
|
+
ctx.requestOptions
|
|
6750
|
+
);
|
|
6751
|
+
const byName = /* @__PURE__ */ new Map();
|
|
6752
|
+
for (const f of table.fields) {
|
|
6753
|
+
byName.set(f.slug, f.id);
|
|
6754
|
+
if (f.name) byName.set(f.name, f.id);
|
|
6755
|
+
}
|
|
6756
|
+
const merged = { ...candidate.auto_field_map };
|
|
6757
|
+
for (const [usesField, physicalName] of Object.entries(overrides)) {
|
|
6758
|
+
const id = byName.get(physicalName);
|
|
6759
|
+
if (id === void 0) {
|
|
6760
|
+
const available = table.fields.map((f) => f.name && f.name !== f.slug ? `${f.slug} (${f.name})` : f.slug).join(", ");
|
|
6761
|
+
throw new Error(
|
|
6762
|
+
`--map ${usesField}=${physicalName}: table "${table.name}" has no field named "${physicalName}". Available: ${available}.`
|
|
6763
|
+
);
|
|
6764
|
+
}
|
|
6765
|
+
merged[usesField] = id;
|
|
6766
|
+
}
|
|
6767
|
+
return merged;
|
|
6768
|
+
}
|
|
6769
|
+
function parseMapOverrides(specs) {
|
|
6770
|
+
const out2 = {};
|
|
6771
|
+
for (const spec of specs ?? []) {
|
|
6772
|
+
const eq = spec.indexOf("=");
|
|
6773
|
+
if (eq <= 0 || eq === spec.length - 1) {
|
|
6774
|
+
throw new Error(
|
|
6775
|
+
`Invalid --map "${spec}". Expected \`<usesField>=<physicalFieldName>\` (e.g. \`--map name=full_name\`).`
|
|
6776
|
+
);
|
|
6777
|
+
}
|
|
6778
|
+
const usesField = spec.slice(0, eq).trim();
|
|
6779
|
+
const physicalName = spec.slice(eq + 1).trim();
|
|
6780
|
+
if (!usesField || !physicalName) {
|
|
6781
|
+
throw new Error(`Invalid --map "${spec}": both sides must be non-empty.`);
|
|
6782
|
+
}
|
|
6783
|
+
if (out2[usesField] !== void 0) {
|
|
6784
|
+
throw new Error(`Duplicate --map for uses field "${usesField}".`);
|
|
6785
|
+
}
|
|
6786
|
+
out2[usesField] = physicalName;
|
|
6787
|
+
}
|
|
6788
|
+
return out2;
|
|
6789
|
+
}
|
|
6790
|
+
async function bindViaCreate(ctx, usesSlug) {
|
|
6791
|
+
const slot = findUsesSlot(ctx, usesSlug);
|
|
6792
|
+
if (slot && slot.if_missing !== void 0 && slot.if_missing !== "create") {
|
|
6793
|
+
warn(
|
|
6794
|
+
`${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.`
|
|
6795
|
+
);
|
|
6796
|
+
}
|
|
6797
|
+
let result;
|
|
6798
|
+
try {
|
|
6799
|
+
result = await putBinding(
|
|
6800
|
+
ctx.installationId,
|
|
6801
|
+
usesSlug,
|
|
6802
|
+
{ create: true },
|
|
6803
|
+
ctx.requestOptions
|
|
6804
|
+
);
|
|
6805
|
+
} catch (err) {
|
|
6806
|
+
return surfacePutBindingError(err, usesSlug, ctx.slug);
|
|
6807
|
+
}
|
|
6808
|
+
if (result.table) {
|
|
6809
|
+
success(
|
|
6810
|
+
`Materialized ${code(result.table.name)} ${dim(`(id ${result.table.id})`)} and bound ${code(usesSlug)} for ${code(ctx.slug)}.`
|
|
6811
|
+
);
|
|
6812
|
+
info(
|
|
6813
|
+
`The new table is a plain user-owned Data Hub table \u2014 it stays in the workspace when the module is uninstalled.`
|
|
6814
|
+
);
|
|
6815
|
+
} else {
|
|
6816
|
+
success(`Bound ${code(usesSlug)} for ${code(ctx.slug)} to a freshly-created table.`);
|
|
6817
|
+
}
|
|
6818
|
+
return 0;
|
|
6819
|
+
}
|
|
6820
|
+
async function declineSlot(ctx, usesSlug) {
|
|
6821
|
+
try {
|
|
6822
|
+
await declineBinding(ctx.installationId, usesSlug, ctx.requestOptions);
|
|
6823
|
+
success(`Declined ${code(usesSlug)} for ${code(ctx.slug)} \u2014 the module runs without it.`);
|
|
6824
|
+
return 0;
|
|
6825
|
+
} catch (err) {
|
|
6826
|
+
return surfacePutBindingError(err, usesSlug, ctx.slug);
|
|
6827
|
+
}
|
|
6828
|
+
}
|
|
6829
|
+
function findUsesSlot(ctx, usesSlug) {
|
|
6830
|
+
try {
|
|
6831
|
+
const manifest = readManifestOrThrow(ctx.dir);
|
|
6832
|
+
return manifest.uses?.tables?.find((t) => t.slug === usesSlug);
|
|
6833
|
+
} catch {
|
|
6834
|
+
return void 0;
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6837
|
+
function reportBoundOk(result, moduleSlug) {
|
|
6838
|
+
const tableLine = result.table ? ` \u2192 ${result.table.name} ${dim(`(id ${result.table.id})`)}` : "";
|
|
6839
|
+
success(`Bound ${code(result.uses_slug)} for ${code(moduleSlug)}${tableLine}.`);
|
|
6840
|
+
const consented = result.consented_ops ?? [];
|
|
6841
|
+
info(
|
|
6842
|
+
`Consented ops: ${opsLine(consented)}. Runtime reads/writes against this slot now succeed.`
|
|
6843
|
+
);
|
|
6844
|
+
if (result.missing_fields && result.missing_fields.length > 0) {
|
|
6845
|
+
warn(
|
|
6846
|
+
`Still unmapped required field(s): ${result.missing_fields.join(", ")}. Re-run with ${code("--map <usesField>=<physicalFieldName>")} to map them.`
|
|
6847
|
+
);
|
|
6848
|
+
}
|
|
6849
|
+
}
|
|
6850
|
+
async function unbindCommand(usesSlug, options) {
|
|
6851
|
+
if (!usesSlug || typeof usesSlug !== "string") {
|
|
6852
|
+
fail("Usage: `dashwise unbind <uses-slug>`");
|
|
6853
|
+
return 1;
|
|
6854
|
+
}
|
|
6855
|
+
let ctx;
|
|
6856
|
+
try {
|
|
6857
|
+
ctx = loadContext2(options);
|
|
6858
|
+
} catch (err) {
|
|
6859
|
+
fail(err.message);
|
|
6860
|
+
return 1;
|
|
6861
|
+
}
|
|
6862
|
+
try {
|
|
6863
|
+
await unbindBinding(ctx.installationId, usesSlug, ctx.requestOptions);
|
|
6864
|
+
success(`Unbound ${code(usesSlug)} for ${code(ctx.slug)}.`);
|
|
6865
|
+
warn(
|
|
6866
|
+
`Runtime reads/writes against this slot now return ${code("USES_UNBOUND")} until you ${code("dashwise bind")} it again.`
|
|
6867
|
+
);
|
|
6868
|
+
return 0;
|
|
6869
|
+
} catch (err) {
|
|
6870
|
+
return surfaceBindingError(err);
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
function surfacePutBindingError(err, usesSlug, moduleSlug) {
|
|
6874
|
+
if (err instanceof ApiError) {
|
|
6875
|
+
switch (err.code) {
|
|
6876
|
+
case "TABLE_NOT_IN_WORKSPACE":
|
|
6877
|
+
fail(
|
|
6878
|
+
`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.`
|
|
6879
|
+
);
|
|
6880
|
+
return 1;
|
|
6881
|
+
case "TABLE_IS_MODULE_MANAGED":
|
|
6882
|
+
fail(
|
|
6883
|
+
`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.`
|
|
6884
|
+
);
|
|
6885
|
+
return 1;
|
|
6886
|
+
case "MISSING_REQUIRED_FIELDS":
|
|
6887
|
+
fail(
|
|
6888
|
+
`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.`
|
|
6889
|
+
);
|
|
6890
|
+
return 1;
|
|
6891
|
+
case "FIELD_TYPE_INCOMPATIBLE":
|
|
6892
|
+
fail(
|
|
6893
|
+
`A mapped physical field's type is incompatible with the declared abstract type (${err.message}). Map to a compatible field with ${code("--map")}.`
|
|
6894
|
+
);
|
|
6895
|
+
return 1;
|
|
6896
|
+
case "FIELD_NOT_FOUND":
|
|
6897
|
+
fail(
|
|
6898
|
+
`A ${code("--map")} referenced a physical field that doesn't exist on the table (${err.message}). Check the field name.`
|
|
6899
|
+
);
|
|
6900
|
+
return 1;
|
|
6901
|
+
case "DECLINE_NOT_ALLOWED":
|
|
6902
|
+
fail(
|
|
6903
|
+
`${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.`
|
|
6904
|
+
);
|
|
6905
|
+
return 1;
|
|
6906
|
+
case "USES_SLOT_NOT_DECLARED":
|
|
6907
|
+
case "BINDING_NOT_FOUND":
|
|
6908
|
+
fail(
|
|
6909
|
+
`${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.`
|
|
6910
|
+
);
|
|
6911
|
+
return 1;
|
|
6912
|
+
case "NOT_WORKSPACE_ADMIN":
|
|
6913
|
+
case "FORBIDDEN":
|
|
6914
|
+
fail(`Not authorized. Binding / unbinding / declining requires workspace-admin.`);
|
|
6915
|
+
return 1;
|
|
6916
|
+
default:
|
|
6917
|
+
fail(
|
|
6918
|
+
`Could not bind ${code(usesSlug)}: ${err.code}${err.message ? ` \u2014 ${err.message}` : ""}. The binding is unchanged.`
|
|
6919
|
+
);
|
|
6920
|
+
return 1;
|
|
6921
|
+
}
|
|
6922
|
+
}
|
|
6923
|
+
fail(`Bind failed: ${err.message}`);
|
|
6924
|
+
return 1;
|
|
6925
|
+
}
|
|
6926
|
+
function surfaceBindingError(err) {
|
|
6927
|
+
if (err instanceof ApiError) {
|
|
6928
|
+
switch (err.code) {
|
|
6929
|
+
case "UNAUTHENTICATED":
|
|
6930
|
+
fail(
|
|
6931
|
+
`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
|
|
6932
|
+
);
|
|
6933
|
+
return 1;
|
|
6934
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
6935
|
+
fail(
|
|
6936
|
+
`You're not a member of this installation's workspace. Binding management requires workspace membership.`
|
|
6937
|
+
);
|
|
6938
|
+
return 1;
|
|
6939
|
+
case "NOT_WORKSPACE_ADMIN":
|
|
6940
|
+
case "FORBIDDEN":
|
|
6941
|
+
fail(`Not authorized. Binding / unbinding requires workspace-admin.`);
|
|
6942
|
+
return 1;
|
|
6943
|
+
case "INSTALLATION_NOT_FOUND":
|
|
6944
|
+
fail(
|
|
6945
|
+
`Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
|
|
6946
|
+
);
|
|
6947
|
+
return 1;
|
|
6948
|
+
case "USES_SLOT_NOT_DECLARED":
|
|
6949
|
+
case "BINDING_NOT_FOUND":
|
|
6950
|
+
fail(
|
|
6951
|
+
`No such ${code("uses")} slot declared in this module's manifest. Run ${code("dashwise bind")} to see declared slots.`
|
|
6952
|
+
);
|
|
6953
|
+
return 1;
|
|
6954
|
+
default:
|
|
6955
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
6956
|
+
return 1;
|
|
6957
|
+
}
|
|
6958
|
+
}
|
|
6959
|
+
fail(`Binding operation failed: ${err.message}`);
|
|
6960
|
+
return 1;
|
|
6961
|
+
}
|
|
6962
|
+
|
|
6963
|
+
// src/commands/uses/add.ts
|
|
6964
|
+
init_api_client();
|
|
6965
|
+
init_config();
|
|
6966
|
+
init_manifest_io();
|
|
6967
|
+
|
|
6968
|
+
// src/lib/uses-types.ts
|
|
6969
|
+
var PHYSICAL_TO_ABSTRACT = {
|
|
6970
|
+
text: "text",
|
|
6971
|
+
long_text: "long_text",
|
|
6972
|
+
number: "number",
|
|
6973
|
+
rating: "number",
|
|
6974
|
+
boolean: "boolean",
|
|
6975
|
+
// A date-with-time column has physical registry type `date` (with
|
|
6976
|
+
// `config.includeTime`), NOT a distinct `datetime` type — the backend
|
|
6977
|
+
// registry has no `datetime` key. So introspection only ever reports
|
|
6978
|
+
// `date` here; there is no `datetime` physical entry to fold.
|
|
6979
|
+
date: "date",
|
|
6980
|
+
email: "email",
|
|
6981
|
+
url: "url",
|
|
6982
|
+
single_select: "single_select",
|
|
6983
|
+
multi_select: "multi_select"
|
|
6984
|
+
};
|
|
6985
|
+
function abstractTypeForPhysical(physicalType) {
|
|
6986
|
+
return PHYSICAL_TO_ABSTRACT[physicalType] ?? null;
|
|
6987
|
+
}
|
|
6988
|
+
var USES_OPERATIONS = ["create", "read", "update", "delete"];
|
|
6989
|
+
|
|
6990
|
+
// src/commands/uses/add.ts
|
|
6991
|
+
init_output();
|
|
6992
|
+
async function usesAddCommand(options) {
|
|
6993
|
+
if (!options.fromTable) {
|
|
6994
|
+
fail("Usage: `dashwise uses add --from-table <name-or-id>`");
|
|
6995
|
+
return 1;
|
|
6996
|
+
}
|
|
6997
|
+
let ops;
|
|
6998
|
+
try {
|
|
6999
|
+
ops = parseOps(options.ops);
|
|
7000
|
+
} catch (err) {
|
|
7001
|
+
fail(err.message);
|
|
7002
|
+
return 1;
|
|
7003
|
+
}
|
|
7004
|
+
let ctx;
|
|
7005
|
+
try {
|
|
7006
|
+
ctx = loadContext3(options);
|
|
7007
|
+
} catch (err) {
|
|
7008
|
+
fail(err.message);
|
|
7009
|
+
return 1;
|
|
7010
|
+
}
|
|
7011
|
+
let table;
|
|
7012
|
+
try {
|
|
7013
|
+
table = await introspectWorkspaceTable(
|
|
7014
|
+
ctx.installationId,
|
|
7015
|
+
options.fromTable,
|
|
7016
|
+
ctx.requestOptions
|
|
7017
|
+
);
|
|
7018
|
+
} catch (err) {
|
|
7019
|
+
return surfaceIntrospectError(err, options.fromTable);
|
|
7020
|
+
}
|
|
7021
|
+
const slotSlug = options.slug ?? slugify(table.name);
|
|
7022
|
+
if (!isKebab(slotSlug)) {
|
|
7023
|
+
fail(
|
|
7024
|
+
`Generated slot slug ${code(slotSlug)} isn't kebab-case. Pass ${code("--slug <kebab-slug>")} explicitly.`
|
|
7025
|
+
);
|
|
7026
|
+
return 1;
|
|
7027
|
+
}
|
|
7028
|
+
const { fields, skipped } = generateFields(table.fields);
|
|
7029
|
+
if (fields.length === 0) {
|
|
7030
|
+
fail(
|
|
7031
|
+
`Table ${code(table.name)} has no fields representable as a v1 ${code("uses")} contract (all are link_row / formula / ai / file). Nothing to declare.`
|
|
7032
|
+
);
|
|
7033
|
+
return 1;
|
|
7034
|
+
}
|
|
7035
|
+
const slot = {
|
|
7036
|
+
slug: slotSlug,
|
|
7037
|
+
ops,
|
|
7038
|
+
fields,
|
|
7039
|
+
if_missing: "require"
|
|
7040
|
+
};
|
|
7041
|
+
if (options.purpose) slot.purpose = options.purpose;
|
|
7042
|
+
info(
|
|
7043
|
+
`Generated a ${code("uses")} contract ${code(slotSlug)} from table ${code(table.name)} ${dim(`(id ${table.table_id})`)}:`
|
|
7044
|
+
);
|
|
7045
|
+
out(` ${dim("ops:")} ${ops.join(", ")}`);
|
|
7046
|
+
for (const f of fields) {
|
|
7047
|
+
const req = f.required ? dim(" (required)") : "";
|
|
7048
|
+
out(` ${dim("field:")} ${code(f.slug)} \u2192 ${f.types.join(" | ")}${req}`);
|
|
7049
|
+
}
|
|
7050
|
+
if (skipped.length > 0) {
|
|
7051
|
+
warn(
|
|
7052
|
+
`Skipped ${skipped.length} field(s) not representable in a v1 uses contract: ${skipped.join(", ")}.`
|
|
7053
|
+
);
|
|
7054
|
+
}
|
|
7055
|
+
const uses = ctx.manifest.uses ?? { tables: [] };
|
|
7056
|
+
const tables = uses.tables ?? [];
|
|
7057
|
+
const existingIdx = tables.findIndex((t) => t.slug === slotSlug);
|
|
7058
|
+
if (existingIdx >= 0 && !options.yes) {
|
|
7059
|
+
fail(
|
|
7060
|
+
`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")}.`
|
|
7061
|
+
);
|
|
7062
|
+
return 1;
|
|
7063
|
+
}
|
|
7064
|
+
if (existingIdx >= 0) {
|
|
7065
|
+
tables[existingIdx] = slot;
|
|
7066
|
+
} else {
|
|
7067
|
+
tables.push(slot);
|
|
7068
|
+
}
|
|
7069
|
+
uses.tables = tables;
|
|
7070
|
+
ctx.manifest.uses = uses;
|
|
7071
|
+
try {
|
|
7072
|
+
writeManifest(ctx.manifest, ctx.dir);
|
|
7073
|
+
} catch (err) {
|
|
7074
|
+
fail(`Could not write manifest: ${err.message}`);
|
|
7075
|
+
return 1;
|
|
7076
|
+
}
|
|
7077
|
+
success(
|
|
7078
|
+
`${existingIdx >= 0 ? "Replaced" : "Added"} ${code(`uses.tables[${slotSlug}]`)} in module.json.`
|
|
7079
|
+
);
|
|
7080
|
+
const pushed = await pushManifest(ctx);
|
|
7081
|
+
if (!pushed) {
|
|
7082
|
+
info(
|
|
7083
|
+
`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}`)}.`
|
|
7084
|
+
);
|
|
7085
|
+
return 1;
|
|
7086
|
+
}
|
|
7087
|
+
if (options.noBind) {
|
|
7088
|
+
info(
|
|
7089
|
+
`Skipped auto-bind (${code("--no-bind")}). Connect it when ready: ${code(`dashwise bind ${slotSlug} --table ${table.table_id}`)}.`
|
|
7090
|
+
);
|
|
7091
|
+
return 0;
|
|
7092
|
+
}
|
|
7093
|
+
info(`Binding ${code(slotSlug)} to ${code(table.name)}\u2026`);
|
|
7094
|
+
const bindExit = await bindCommand(slotSlug, {
|
|
7095
|
+
...ctx.dir !== void 0 ? { dir: ctx.dir } : {},
|
|
7096
|
+
table: String(table.table_id)
|
|
7097
|
+
});
|
|
7098
|
+
if (bindExit !== 0) {
|
|
7099
|
+
warn(
|
|
7100
|
+
`The ${code("uses")} slot was declared + pushed, but the auto-bind didn't complete. Retry with ${code(`dashwise bind ${slotSlug} --table ${table.table_id}`)}.`
|
|
7101
|
+
);
|
|
7102
|
+
return bindExit;
|
|
7103
|
+
}
|
|
7104
|
+
return 0;
|
|
7105
|
+
}
|
|
7106
|
+
function loadContext3(opts) {
|
|
7107
|
+
const manifest = readManifestOrThrow(opts.dir);
|
|
7108
|
+
const moduleSlug = manifest.module?.slug;
|
|
7109
|
+
if (!moduleSlug) {
|
|
7110
|
+
throw new Error("module.json has no module.slug \u2014 the manifest is malformed.");
|
|
7111
|
+
}
|
|
7112
|
+
const cfg = readConfig();
|
|
7113
|
+
if (!cfg) {
|
|
7114
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
7115
|
+
}
|
|
7116
|
+
const entry = readModuleEntry(moduleSlug);
|
|
7117
|
+
if (!entry) {
|
|
7118
|
+
throw new Error(
|
|
7119
|
+
`No dev install cached for module "${moduleSlug}". Run \`dashwise init ${moduleSlug} --force\` to provision one.`
|
|
7120
|
+
);
|
|
7121
|
+
}
|
|
7122
|
+
return {
|
|
7123
|
+
installationId: entry.installationId,
|
|
7124
|
+
moduleSlug,
|
|
7125
|
+
...opts.dir !== void 0 ? { dir: opts.dir } : {},
|
|
7126
|
+
manifest,
|
|
7127
|
+
requestOptions: {
|
|
7128
|
+
baseUrl: resolveApiUrl(),
|
|
7129
|
+
auth: { token: cfg.token }
|
|
7130
|
+
}
|
|
7131
|
+
};
|
|
7132
|
+
}
|
|
7133
|
+
async function pushManifest(ctx) {
|
|
7134
|
+
let moduleId;
|
|
7135
|
+
try {
|
|
7136
|
+
const mod = await getModuleBySlug(ctx.moduleSlug, ctx.requestOptions);
|
|
7137
|
+
moduleId = mod.id;
|
|
7138
|
+
} catch (err) {
|
|
7139
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
7140
|
+
warn(
|
|
7141
|
+
`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.`
|
|
7142
|
+
);
|
|
7143
|
+
return false;
|
|
7144
|
+
}
|
|
7145
|
+
warn(`Couldn't resolve the module id to push the manifest: ${err.message}`);
|
|
7146
|
+
return false;
|
|
7147
|
+
}
|
|
7148
|
+
try {
|
|
7149
|
+
await applyDraftManifest(
|
|
7150
|
+
moduleId,
|
|
7151
|
+
ctx.manifest,
|
|
7152
|
+
ctx.requestOptions
|
|
7153
|
+
);
|
|
7154
|
+
success("Pushed the manifest \u2014 the backend parked the new slot, ready to bind.");
|
|
7155
|
+
return true;
|
|
7156
|
+
} catch (err) {
|
|
7157
|
+
if (err instanceof ApiError) {
|
|
7158
|
+
if (err.code === "MANIFEST_INVALID" || err.status === 400) {
|
|
7159
|
+
fail(`Manifest rejected by the backend validator: ${err.message}`);
|
|
7160
|
+
const issues = extractIssues(err.context);
|
|
7161
|
+
for (const issue of issues) out(` ${dim("-")} ${issue}`);
|
|
7162
|
+
} else {
|
|
7163
|
+
fail(`Manifest push failed: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
7164
|
+
}
|
|
7165
|
+
} else {
|
|
7166
|
+
fail(`Manifest push failed: ${err.message}`);
|
|
7167
|
+
}
|
|
7168
|
+
return false;
|
|
7169
|
+
}
|
|
7170
|
+
}
|
|
7171
|
+
function extractIssues(context) {
|
|
7172
|
+
if (!context) return [];
|
|
7173
|
+
const raw = context.issues;
|
|
7174
|
+
if (!Array.isArray(raw)) return [];
|
|
7175
|
+
return raw.map((i) => {
|
|
7176
|
+
if (typeof i === "string") return i;
|
|
7177
|
+
if (i && typeof i === "object") {
|
|
7178
|
+
const o = i;
|
|
7179
|
+
const path = typeof o.path === "string" ? o.path : "";
|
|
7180
|
+
const message = typeof o.message === "string" ? o.message : JSON.stringify(i);
|
|
7181
|
+
return path ? `${path}: ${message}` : message;
|
|
7182
|
+
}
|
|
7183
|
+
return String(i);
|
|
7184
|
+
});
|
|
7185
|
+
}
|
|
7186
|
+
function generateFields(physical) {
|
|
7187
|
+
const fields = [];
|
|
7188
|
+
const skipped = [];
|
|
7189
|
+
for (const f of physical) {
|
|
7190
|
+
const abstract = abstractTypeForPhysical(f.type);
|
|
7191
|
+
if (abstract === null) {
|
|
7192
|
+
skipped.push(`${f.slug} (${f.type})`);
|
|
7193
|
+
continue;
|
|
7194
|
+
}
|
|
7195
|
+
const entry = { slug: f.slug, types: [abstract] };
|
|
7196
|
+
if (f.required) entry.required = true;
|
|
7197
|
+
fields.push(entry);
|
|
7198
|
+
}
|
|
7199
|
+
return { fields, skipped };
|
|
7200
|
+
}
|
|
7201
|
+
function parseOps(opsCsv) {
|
|
7202
|
+
if (opsCsv === void 0) return ["read"];
|
|
7203
|
+
const parsed = opsCsv.split(",").map((o) => o.trim()).filter(Boolean);
|
|
7204
|
+
if (parsed.length === 0) {
|
|
7205
|
+
throw new Error(`--ops must list at least one of ${USES_OPERATIONS.join(", ")}.`);
|
|
7206
|
+
}
|
|
7207
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7208
|
+
for (const op of parsed) {
|
|
7209
|
+
if (!USES_OPERATIONS.includes(op)) {
|
|
7210
|
+
throw new Error(
|
|
7211
|
+
`Invalid --ops value "${op}". Allowed: ${USES_OPERATIONS.join(", ")}.`
|
|
7212
|
+
);
|
|
7213
|
+
}
|
|
7214
|
+
seen.add(op);
|
|
7215
|
+
}
|
|
7216
|
+
return USES_OPERATIONS.filter((o) => seen.has(o));
|
|
7217
|
+
}
|
|
7218
|
+
function slugify(name) {
|
|
7219
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7220
|
+
}
|
|
7221
|
+
var KEBAB_REGEX = /^[a-z][a-z0-9-]*[a-z0-9]$/;
|
|
7222
|
+
function isKebab(slug) {
|
|
7223
|
+
return KEBAB_REGEX.test(slug);
|
|
7224
|
+
}
|
|
7225
|
+
function surfaceIntrospectError(err, tableRef) {
|
|
7226
|
+
if (err instanceof ApiError) {
|
|
7227
|
+
switch (err.code) {
|
|
7228
|
+
case "NON_NUMERIC_TABLE_REF":
|
|
7229
|
+
fail(err.message);
|
|
7230
|
+
return 1;
|
|
7231
|
+
case "NOT_FOUND":
|
|
7232
|
+
fail(
|
|
7233
|
+
`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")}).`
|
|
7234
|
+
);
|
|
7235
|
+
return 1;
|
|
7236
|
+
case "TABLE_IS_MODULE_MANAGED":
|
|
7237
|
+
fail(
|
|
7238
|
+
`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.`
|
|
7239
|
+
);
|
|
7240
|
+
return 1;
|
|
7241
|
+
case "UNAUTHENTICATED":
|
|
7242
|
+
fail(
|
|
7243
|
+
`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
|
|
7244
|
+
);
|
|
7245
|
+
return 1;
|
|
7246
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
7247
|
+
case "FORBIDDEN":
|
|
7248
|
+
fail(`You're not a member of the workspace that owns table ${code(tableRef)}.`);
|
|
7249
|
+
return 1;
|
|
7250
|
+
default:
|
|
7251
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
7252
|
+
return 1;
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
fail(`Table introspection failed: ${err.message}`);
|
|
7256
|
+
return 1;
|
|
7257
|
+
}
|
|
7258
|
+
|
|
7259
|
+
// src/commands/pull.ts
|
|
7260
|
+
init_api_client();
|
|
7261
|
+
init_manifest_io();
|
|
7262
|
+
init_schema_command_context();
|
|
7263
|
+
init_output();
|
|
7264
|
+
async function pullCommand(options) {
|
|
7265
|
+
let ctx;
|
|
7266
|
+
try {
|
|
7267
|
+
ctx = loadSchemaCommandContext({
|
|
7268
|
+
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
7269
|
+
});
|
|
7270
|
+
} catch (err) {
|
|
7271
|
+
fail(err.message);
|
|
7272
|
+
return 1;
|
|
7273
|
+
}
|
|
7274
|
+
info(`Pulling manifest for installation ${ctx.entry.installationId}\u2026`);
|
|
7275
|
+
let remoteManifest;
|
|
7276
|
+
try {
|
|
7277
|
+
const response = await getInstallationSchema(ctx.requestOptions);
|
|
7278
|
+
remoteManifest = response.manifest;
|
|
7279
|
+
} catch (err) {
|
|
7280
|
+
return surfacePullFailure(err);
|
|
7281
|
+
}
|
|
7282
|
+
const localJson = JSON.stringify(ctx.manifest, null, 2) + "\n";
|
|
7283
|
+
const remoteJson = JSON.stringify(remoteManifest, null, 2) + "\n";
|
|
7284
|
+
if (localJson === remoteJson) {
|
|
7285
|
+
success("Already in sync \u2014 local module.json matches backend.");
|
|
7286
|
+
return 0;
|
|
7287
|
+
}
|
|
7288
|
+
const localBytes = Buffer.byteLength(localJson, "utf8");
|
|
7289
|
+
const remoteBytes = Buffer.byteLength(remoteJson, "utf8");
|
|
7290
|
+
const delta = remoteBytes - localBytes;
|
|
7291
|
+
const deltaStr = delta === 0 ? "0 bytes" : `${delta > 0 ? "+" : ""}${delta} bytes`;
|
|
7292
|
+
if (options.dryRun) {
|
|
7293
|
+
info(
|
|
7294
|
+
`Drift detected (${deltaStr}). ${code("--dry-run")} \u2014 not writing.`
|
|
7295
|
+
);
|
|
7296
|
+
info(`Run ${code("dashwise pull")} without ${code("--dry-run")} to apply.`);
|
|
7297
|
+
return 0;
|
|
7298
|
+
}
|
|
7299
|
+
try {
|
|
7300
|
+
writeManifest(remoteManifest, ctx.projectRoot);
|
|
7301
|
+
} catch (err) {
|
|
7302
|
+
fail(`Failed to write ${manifestPath(ctx.projectRoot)}: ${err.message}`);
|
|
7303
|
+
return 1;
|
|
7304
|
+
}
|
|
7305
|
+
success(
|
|
7306
|
+
`Synced module.json from backend (${deltaStr}). ${dim(`Run \`dashwise module generate\` to refresh typed SDK if your watcher isn't running.`)}`
|
|
7307
|
+
);
|
|
7308
|
+
return 0;
|
|
7309
|
+
}
|
|
7310
|
+
function surfacePullFailure(err) {
|
|
7311
|
+
if (err instanceof ApiError) {
|
|
7312
|
+
switch (err.code) {
|
|
7313
|
+
case "UNAUTHENTICATED":
|
|
7314
|
+
fail(
|
|
7315
|
+
`Runtime token rejected. The cached token for this module may be expired \u2014 re-run \`dashwise init <slug> --force\` to mint a fresh one.`
|
|
7316
|
+
);
|
|
7317
|
+
break;
|
|
7318
|
+
case "SANDBOX_TOKEN_NOT_ACCEPTED":
|
|
7319
|
+
fail(
|
|
7320
|
+
`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\`.`
|
|
7321
|
+
);
|
|
7322
|
+
break;
|
|
7323
|
+
case "INSTALL_MISMATCH":
|
|
7324
|
+
fail(
|
|
7325
|
+
`Runtime token is bound to a different installation than expected. The cached install id may be stale \u2014 re-run \`dashwise init <slug> --force\`.`
|
|
7326
|
+
);
|
|
7327
|
+
break;
|
|
7328
|
+
case "INSTALLATION_NOT_FOUND":
|
|
7329
|
+
fail(
|
|
7330
|
+
`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.`
|
|
7331
|
+
);
|
|
7332
|
+
break;
|
|
7333
|
+
default:
|
|
7334
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
7335
|
+
}
|
|
7336
|
+
return 1;
|
|
7337
|
+
}
|
|
7338
|
+
fail(`Pull failed: ${err.message}`);
|
|
7339
|
+
return 1;
|
|
7340
|
+
}
|
|
7341
|
+
|
|
7342
|
+
// src/bin.ts
|
|
7343
|
+
init_scaffold();
|
|
7344
|
+
|
|
7345
|
+
// src/commands/api-keys.ts
|
|
7346
|
+
init_api_client();
|
|
7347
|
+
init_config();
|
|
7348
|
+
init_manifest_io();
|
|
7349
|
+
init_output();
|
|
7350
|
+
function loadContext4(opts) {
|
|
7351
|
+
const manifest = readManifestOrThrow(opts.dir);
|
|
7352
|
+
const slug = manifest.module?.slug;
|
|
7353
|
+
if (!slug) {
|
|
7354
|
+
throw new Error(
|
|
7355
|
+
`module.json has no module.slug \u2014 the manifest is malformed.`
|
|
7356
|
+
);
|
|
7357
|
+
}
|
|
7358
|
+
const cfg = readConfig();
|
|
7359
|
+
if (!cfg) {
|
|
7360
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
7361
|
+
}
|
|
7362
|
+
const entry = readModuleEntry(slug);
|
|
7363
|
+
if (!entry) {
|
|
7364
|
+
throw new Error(
|
|
7365
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
|
|
7366
|
+
);
|
|
7367
|
+
}
|
|
7368
|
+
return {
|
|
7369
|
+
installationId: entry.installationId,
|
|
7370
|
+
slug,
|
|
7371
|
+
requestOptions: {
|
|
7372
|
+
baseUrl: resolveApiUrl(),
|
|
7373
|
+
auth: { token: cfg.token }
|
|
7374
|
+
}
|
|
7375
|
+
};
|
|
7376
|
+
}
|
|
7377
|
+
async function apiKeysListCommand(options) {
|
|
7378
|
+
let ctx;
|
|
7379
|
+
try {
|
|
7380
|
+
ctx = loadContext4(options);
|
|
7381
|
+
} catch (err) {
|
|
7382
|
+
fail(err.message);
|
|
7383
|
+
return 1;
|
|
7384
|
+
}
|
|
7385
|
+
let keys;
|
|
7386
|
+
try {
|
|
7387
|
+
const resp = await listApiKeys(ctx.installationId, ctx.requestOptions);
|
|
7388
|
+
keys = resp.api_keys;
|
|
7389
|
+
} catch (err) {
|
|
7390
|
+
return surfaceApiKeysError(err);
|
|
7391
|
+
}
|
|
7392
|
+
const filtered = options.all ? keys : keys.filter((k) => k.revoked_at === null);
|
|
7393
|
+
if (options.json) {
|
|
7394
|
+
process.stdout.write(`${JSON.stringify(filtered, null, 2)}
|
|
7395
|
+
`);
|
|
7396
|
+
return 0;
|
|
7397
|
+
}
|
|
7398
|
+
if (filtered.length === 0) {
|
|
7399
|
+
info(
|
|
7400
|
+
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.`
|
|
7401
|
+
);
|
|
7402
|
+
return 0;
|
|
7403
|
+
}
|
|
7404
|
+
info(`API keys for ${code(ctx.slug)} (install id=${ctx.installationId}):`);
|
|
7405
|
+
for (const k of filtered) {
|
|
7406
|
+
const status = formatKeyStatus(k);
|
|
7407
|
+
const lastUsed = k.last_used_at ? `last used ${formatRelative(new Date(k.last_used_at))}` : "never used";
|
|
7408
|
+
process.stdout.write(
|
|
7409
|
+
` ${code(`#${k.id}`)} ${k.name} ${dim(`dwk_${k.key_prefix}\u2026`)} ${status} ${dim(`(${lastUsed})`)}
|
|
7410
|
+
`
|
|
7411
|
+
);
|
|
7412
|
+
const grantLines = formatGrants(k.grants);
|
|
7413
|
+
if (grantLines.length > 0) {
|
|
7414
|
+
for (const line of grantLines) {
|
|
7415
|
+
process.stdout.write(` ${dim(line)}
|
|
7416
|
+
`);
|
|
7417
|
+
}
|
|
7418
|
+
} else if (k.grants !== void 0) {
|
|
7419
|
+
process.stdout.write(` ${dim("full install access (no grants)")}
|
|
7420
|
+
`);
|
|
7421
|
+
}
|
|
7422
|
+
}
|
|
7423
|
+
return 0;
|
|
7424
|
+
}
|
|
7425
|
+
async function apiKeysCreateCommand(nameArg, options) {
|
|
7426
|
+
const name = nameArg ?? options.name;
|
|
7427
|
+
if (!name) {
|
|
7428
|
+
fail("Usage: `dashwise api-keys create <name>` (e.g. `dashwise api-keys create production`)");
|
|
7429
|
+
return 1;
|
|
7430
|
+
}
|
|
7431
|
+
let grants;
|
|
7432
|
+
try {
|
|
7433
|
+
grants = parseGrants(options.grant);
|
|
7434
|
+
} catch (err) {
|
|
7435
|
+
fail(err.message);
|
|
7436
|
+
return 1;
|
|
7437
|
+
}
|
|
7438
|
+
let expiresAt;
|
|
7439
|
+
try {
|
|
7440
|
+
expiresAt = resolveExpiresAt(options.expires);
|
|
7441
|
+
} catch (err) {
|
|
7442
|
+
fail(err.message);
|
|
7443
|
+
return 1;
|
|
7444
|
+
}
|
|
7445
|
+
let ctx;
|
|
7446
|
+
try {
|
|
7447
|
+
ctx = loadContext4(options);
|
|
7448
|
+
} catch (err) {
|
|
7449
|
+
fail(err.message);
|
|
7450
|
+
return 1;
|
|
7451
|
+
}
|
|
7452
|
+
const body = { name };
|
|
7453
|
+
if (grants !== void 0) body.grants = grants;
|
|
7454
|
+
if (expiresAt !== void 0) body.expires_at = expiresAt;
|
|
7455
|
+
let resp;
|
|
7456
|
+
try {
|
|
7457
|
+
resp = await createApiKey(ctx.installationId, body, ctx.requestOptions);
|
|
7458
|
+
} catch (err) {
|
|
7459
|
+
return surfaceApiKeysError(err);
|
|
7460
|
+
}
|
|
7461
|
+
if (options.rawOnly) {
|
|
7462
|
+
process.stderr.write(
|
|
7463
|
+
`Created API key #${resp.api_key.id} (${resp.api_key.name}) for install ${ctx.installationId}.
|
|
7464
|
+
`
|
|
5922
7465
|
);
|
|
5923
7466
|
process.stdout.write(`${resp.raw_key}
|
|
5924
7467
|
`);
|
|
@@ -5927,6 +7470,13 @@ async function apiKeysCreateCommand(nameArg, options) {
|
|
|
5927
7470
|
success(
|
|
5928
7471
|
`Created API key ${code(`#${resp.api_key.id}`)} (${resp.api_key.name}) for ${code(ctx.slug)}.`
|
|
5929
7472
|
);
|
|
7473
|
+
const grantLines = formatGrants(resp.api_key.grants);
|
|
7474
|
+
if (grantLines.length > 0) {
|
|
7475
|
+
info(` ${dim("scope:")} scoped to`);
|
|
7476
|
+
for (const line of grantLines) info(` ${line}`);
|
|
7477
|
+
} else {
|
|
7478
|
+
info(` ${dim("scope:")} full install access (no grants)`);
|
|
7479
|
+
}
|
|
5930
7480
|
info("");
|
|
5931
7481
|
warn(
|
|
5932
7482
|
"This is the ONLY time the full key is displayed. Save it now \u2014 the backend stores a hash, not the plaintext."
|
|
@@ -5935,14 +7485,9 @@ async function apiKeysCreateCommand(nameArg, options) {
|
|
|
5935
7485
|
process.stdout.write(` ${code(resp.raw_key)}
|
|
5936
7486
|
`);
|
|
5937
7487
|
info("");
|
|
5938
|
-
info(`Use in a deployed module's env:`);
|
|
7488
|
+
info(`Use in a deployed module's env (the two-var contract):`);
|
|
7489
|
+
info(` ${code(`DASHWISE_API_URL=${resolveApiUrl()}`)}`);
|
|
5939
7490
|
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
7491
|
return 0;
|
|
5947
7492
|
}
|
|
5948
7493
|
async function apiKeysRevokeCommand(keyIdArg, options) {
|
|
@@ -5957,7 +7502,7 @@ async function apiKeysRevokeCommand(keyIdArg, options) {
|
|
|
5957
7502
|
}
|
|
5958
7503
|
let ctx;
|
|
5959
7504
|
try {
|
|
5960
|
-
ctx =
|
|
7505
|
+
ctx = loadContext4(options);
|
|
5961
7506
|
} catch (err) {
|
|
5962
7507
|
fail(err.message);
|
|
5963
7508
|
return 1;
|
|
@@ -5991,6 +7536,17 @@ function surfaceApiKeysError(err) {
|
|
|
5991
7536
|
`You're not a member of this installation's workspace. API-key management requires workspace membership.`
|
|
5992
7537
|
);
|
|
5993
7538
|
return 1;
|
|
7539
|
+
case "NOT_WORKSPACE_ADMIN":
|
|
7540
|
+
case "FORBIDDEN":
|
|
7541
|
+
fail(
|
|
7542
|
+
`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.`
|
|
7543
|
+
);
|
|
7544
|
+
return 1;
|
|
7545
|
+
case "INVALID_GRANT":
|
|
7546
|
+
fail(
|
|
7547
|
+
`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.`
|
|
7548
|
+
);
|
|
7549
|
+
return 1;
|
|
5994
7550
|
case "INSTALLATION_NOT_FOUND":
|
|
5995
7551
|
fail(
|
|
5996
7552
|
`Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
|
|
@@ -6009,6 +7565,117 @@ function surfaceApiKeysError(err) {
|
|
|
6009
7565
|
fail(`API-key operation failed: ${err.message}`);
|
|
6010
7566
|
return 1;
|
|
6011
7567
|
}
|
|
7568
|
+
var GRANT_OPERATIONS = ["create", "read", "update", "delete"];
|
|
7569
|
+
function parseGrants(specs) {
|
|
7570
|
+
if (!specs || specs.length === 0) return void 0;
|
|
7571
|
+
return specs.map((spec) => parseGrantSpec(spec));
|
|
7572
|
+
}
|
|
7573
|
+
function parseGrantSpec(spec) {
|
|
7574
|
+
const raw = spec.trim();
|
|
7575
|
+
const parts = raw.split(":");
|
|
7576
|
+
const source = parts[0];
|
|
7577
|
+
if (source === "module") {
|
|
7578
|
+
if (parts.length !== 3) {
|
|
7579
|
+
throw new Error(
|
|
7580
|
+
`Invalid --grant "${spec}". Module grants are \`module:<tableSlug>:<ops>\` (e.g. \`module:tasks:read,create\`).`
|
|
7581
|
+
);
|
|
7582
|
+
}
|
|
7583
|
+
const tableSlug = parts[1];
|
|
7584
|
+
if (!tableSlug) {
|
|
7585
|
+
throw new Error(`Invalid --grant "${spec}": missing table slug.`);
|
|
7586
|
+
}
|
|
7587
|
+
return {
|
|
7588
|
+
source: "module",
|
|
7589
|
+
tableSlug,
|
|
7590
|
+
operations: parseOps2(parts[2], spec)
|
|
7591
|
+
};
|
|
7592
|
+
}
|
|
7593
|
+
if (source === "datahub") {
|
|
7594
|
+
const scope = parts[1];
|
|
7595
|
+
if (!scope) {
|
|
7596
|
+
throw new Error(
|
|
7597
|
+
`Invalid --grant "${spec}". Data Hub grants are \`datahub:table=<id>:<ops>\`, \`datahub:app=<id>:<ops>\`, or \`datahub:workspace:<ops>\`.`
|
|
7598
|
+
);
|
|
7599
|
+
}
|
|
7600
|
+
if (scope === "workspace") {
|
|
7601
|
+
if (parts.length !== 3) {
|
|
7602
|
+
throw new Error(
|
|
7603
|
+
`Invalid --grant "${spec}". Workspace-wide Data Hub grants are \`datahub:workspace:<ops>\`.`
|
|
7604
|
+
);
|
|
7605
|
+
}
|
|
7606
|
+
return { source: "datahub", operations: parseOps2(parts[2], spec) };
|
|
7607
|
+
}
|
|
7608
|
+
const eq = scope.indexOf("=");
|
|
7609
|
+
const scopeKind = eq === -1 ? scope : scope.slice(0, eq);
|
|
7610
|
+
const scopeVal = eq === -1 ? "" : scope.slice(eq + 1);
|
|
7611
|
+
if (scopeKind !== "table" && scopeKind !== "app" || scopeVal === "") {
|
|
7612
|
+
throw new Error(
|
|
7613
|
+
`Invalid --grant "${spec}". Data Hub scope must be \`table=<id>\`, \`app=<id>\`, or \`workspace\`.`
|
|
7614
|
+
);
|
|
7615
|
+
}
|
|
7616
|
+
if (parts.length !== 3) {
|
|
7617
|
+
throw new Error(
|
|
7618
|
+
`Invalid --grant "${spec}". Data Hub grants are \`datahub:${scopeKind}=<id>:<ops>\`.`
|
|
7619
|
+
);
|
|
7620
|
+
}
|
|
7621
|
+
const id = Number.parseInt(scopeVal, 10);
|
|
7622
|
+
if (!Number.isInteger(id) || id <= 0 || String(id) !== scopeVal) {
|
|
7623
|
+
throw new Error(
|
|
7624
|
+
`Invalid --grant "${spec}": ${scopeKind} id must be a positive integer (got "${scopeVal}").`
|
|
7625
|
+
);
|
|
7626
|
+
}
|
|
7627
|
+
const operations = parseOps2(parts[2], spec);
|
|
7628
|
+
return scopeKind === "table" ? { source: "datahub", tableId: id, operations } : { source: "datahub", applicationId: id, operations };
|
|
7629
|
+
}
|
|
7630
|
+
throw new Error(
|
|
7631
|
+
`Invalid --grant "${spec}": unknown source "${source}". Use \`module:\u2026\` or \`datahub:\u2026\`.`
|
|
7632
|
+
);
|
|
7633
|
+
}
|
|
7634
|
+
function parseOps2(opsCsv, spec) {
|
|
7635
|
+
const ops = opsCsv.split(",").map((o) => o.trim()).filter(Boolean);
|
|
7636
|
+
if (ops.length === 0) {
|
|
7637
|
+
throw new Error(
|
|
7638
|
+
`Invalid --grant "${spec}": at least one operation is required (${GRANT_OPERATIONS.join(", ")}).`
|
|
7639
|
+
);
|
|
7640
|
+
}
|
|
7641
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7642
|
+
for (const op of ops) {
|
|
7643
|
+
if (!GRANT_OPERATIONS.includes(op)) {
|
|
7644
|
+
throw new Error(
|
|
7645
|
+
`Invalid --grant "${spec}": unknown operation "${op}". Allowed: ${GRANT_OPERATIONS.join(", ")}.`
|
|
7646
|
+
);
|
|
7647
|
+
}
|
|
7648
|
+
seen.add(op);
|
|
7649
|
+
}
|
|
7650
|
+
return GRANT_OPERATIONS.filter((op) => seen.has(op));
|
|
7651
|
+
}
|
|
7652
|
+
function resolveExpiresAt(days) {
|
|
7653
|
+
if (days === void 0) return void 0;
|
|
7654
|
+
if (!Number.isFinite(days) || days <= 0) {
|
|
7655
|
+
throw new Error(`--expires must be a positive number of days (got ${days}).`);
|
|
7656
|
+
}
|
|
7657
|
+
return new Date(Date.now() + days * 24 * 60 * 60 * 1e3).toISOString();
|
|
7658
|
+
}
|
|
7659
|
+
function formatGrants(grants) {
|
|
7660
|
+
if (!grants || grants.length === 0) return [];
|
|
7661
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
7662
|
+
for (const g of grants) {
|
|
7663
|
+
const label = grantScopeLabel(g);
|
|
7664
|
+
const existing = byScope.get(label);
|
|
7665
|
+
if (existing) existing.ops.add(g.operation);
|
|
7666
|
+
else byScope.set(label, { label, ops: /* @__PURE__ */ new Set([g.operation]) });
|
|
7667
|
+
}
|
|
7668
|
+
return [...byScope.values()].map((s) => {
|
|
7669
|
+
const ops = GRANT_OPERATIONS.filter((o) => s.ops.has(o));
|
|
7670
|
+
return `${s.label}: ${ops.join(", ")}`;
|
|
7671
|
+
});
|
|
7672
|
+
}
|
|
7673
|
+
function grantScopeLabel(g) {
|
|
7674
|
+
if (g.source === "module") return `module ${g.table_slug ?? "?"}`;
|
|
7675
|
+
if (g.table_id != null) return `datahub table#${g.table_id}`;
|
|
7676
|
+
if (g.application_id != null) return `datahub app#${g.application_id}`;
|
|
7677
|
+
return "datahub workspace";
|
|
7678
|
+
}
|
|
6012
7679
|
function formatKeyStatus(k) {
|
|
6013
7680
|
if (k.revoked_at) return dim("[revoked]");
|
|
6014
7681
|
if (k.expires_at && new Date(k.expires_at).getTime() <= Date.now()) {
|
|
@@ -6028,6 +7695,190 @@ function formatRelative(d) {
|
|
|
6028
7695
|
return `${day}d ago`;
|
|
6029
7696
|
}
|
|
6030
7697
|
|
|
7698
|
+
// src/commands/origins.ts
|
|
7699
|
+
init_api_client();
|
|
7700
|
+
init_config();
|
|
7701
|
+
init_manifest_io();
|
|
7702
|
+
init_output();
|
|
7703
|
+
function loadContext5(opts) {
|
|
7704
|
+
const manifest = readManifestOrThrow(opts.dir);
|
|
7705
|
+
const slug = manifest.module?.slug;
|
|
7706
|
+
if (!slug) {
|
|
7707
|
+
throw new Error(
|
|
7708
|
+
`module.json has no module.slug \u2014 the manifest is malformed.`
|
|
7709
|
+
);
|
|
7710
|
+
}
|
|
7711
|
+
const cfg = readConfig();
|
|
7712
|
+
if (!cfg) {
|
|
7713
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
7714
|
+
}
|
|
7715
|
+
const entry = readModuleEntry(slug);
|
|
7716
|
+
if (!entry) {
|
|
7717
|
+
throw new Error(
|
|
7718
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
|
|
7719
|
+
);
|
|
7720
|
+
}
|
|
7721
|
+
return {
|
|
7722
|
+
installationId: entry.installationId,
|
|
7723
|
+
slug,
|
|
7724
|
+
requestOptions: {
|
|
7725
|
+
baseUrl: resolveApiUrl(),
|
|
7726
|
+
auth: { token: cfg.token }
|
|
7727
|
+
}
|
|
7728
|
+
};
|
|
7729
|
+
}
|
|
7730
|
+
function normalizeOrigin(raw) {
|
|
7731
|
+
const trimmed = raw.trim();
|
|
7732
|
+
try {
|
|
7733
|
+
return new URL(trimmed).origin;
|
|
7734
|
+
} catch {
|
|
7735
|
+
return trimmed;
|
|
7736
|
+
}
|
|
7737
|
+
}
|
|
7738
|
+
async function originsListCommand(options) {
|
|
7739
|
+
let ctx;
|
|
7740
|
+
try {
|
|
7741
|
+
ctx = loadContext5(options);
|
|
7742
|
+
} catch (err) {
|
|
7743
|
+
fail(err.message);
|
|
7744
|
+
return 1;
|
|
7745
|
+
}
|
|
7746
|
+
let origins;
|
|
7747
|
+
try {
|
|
7748
|
+
const resp = await getOrigins(ctx.installationId, ctx.requestOptions);
|
|
7749
|
+
origins = resp.allowed_origins;
|
|
7750
|
+
} catch (err) {
|
|
7751
|
+
return surfaceOriginsError(err);
|
|
7752
|
+
}
|
|
7753
|
+
if (options.json) {
|
|
7754
|
+
process.stdout.write(`${JSON.stringify(origins, null, 2)}
|
|
7755
|
+
`);
|
|
7756
|
+
return 0;
|
|
7757
|
+
}
|
|
7758
|
+
info(
|
|
7759
|
+
`SSO callback origins for ${code(ctx.slug)} ${dim(
|
|
7760
|
+
"(localhost is always allowed for local dev installs)"
|
|
7761
|
+
)}:`
|
|
7762
|
+
);
|
|
7763
|
+
if (origins.length === 0) {
|
|
7764
|
+
info(` ${dim("(none configured \u2014 only implicit localhost applies)")}`);
|
|
7765
|
+
info("");
|
|
7766
|
+
info(
|
|
7767
|
+
`Add a deployed domain with ${code("dashwise origins add https://your-domain")}.`
|
|
7768
|
+
);
|
|
7769
|
+
return 0;
|
|
7770
|
+
}
|
|
7771
|
+
for (const o of origins) {
|
|
7772
|
+
process.stdout.write(` ${o}
|
|
7773
|
+
`);
|
|
7774
|
+
}
|
|
7775
|
+
return 0;
|
|
7776
|
+
}
|
|
7777
|
+
async function originsAddCommand(urlArg, options) {
|
|
7778
|
+
if (!urlArg) {
|
|
7779
|
+
fail("Usage: `dashwise origins add <url>` (e.g. `dashwise origins add https://app.example.com`)");
|
|
7780
|
+
return 1;
|
|
7781
|
+
}
|
|
7782
|
+
const origin = normalizeOrigin(urlArg);
|
|
7783
|
+
let ctx;
|
|
7784
|
+
try {
|
|
7785
|
+
ctx = loadContext5(options);
|
|
7786
|
+
} catch (err) {
|
|
7787
|
+
fail(err.message);
|
|
7788
|
+
return 1;
|
|
7789
|
+
}
|
|
7790
|
+
let current;
|
|
7791
|
+
try {
|
|
7792
|
+
current = (await getOrigins(ctx.installationId, ctx.requestOptions)).allowed_origins;
|
|
7793
|
+
} catch (err) {
|
|
7794
|
+
return surfaceOriginsError(err);
|
|
7795
|
+
}
|
|
7796
|
+
if (current.some((o) => normalizeOrigin(o) === origin)) {
|
|
7797
|
+
info(`${code(origin)} is already allowlisted for ${code(ctx.slug)}. Nothing to do.`);
|
|
7798
|
+
return 0;
|
|
7799
|
+
}
|
|
7800
|
+
const next = [...current, origin];
|
|
7801
|
+
try {
|
|
7802
|
+
const resp = await setOrigins(ctx.installationId, next, ctx.requestOptions);
|
|
7803
|
+
success(`Added ${code(origin)} to the SSO callback allowlist for ${code(ctx.slug)}.`);
|
|
7804
|
+
info(` ${dim(`${resp.allowed_origins.length} origin(s) now configured.`)}`);
|
|
7805
|
+
} catch (err) {
|
|
7806
|
+
return surfaceOriginsError(err);
|
|
7807
|
+
}
|
|
7808
|
+
return 0;
|
|
7809
|
+
}
|
|
7810
|
+
async function originsRemoveCommand(urlArg, options) {
|
|
7811
|
+
if (!urlArg) {
|
|
7812
|
+
fail("Usage: `dashwise origins remove <url>`");
|
|
7813
|
+
return 1;
|
|
7814
|
+
}
|
|
7815
|
+
const origin = normalizeOrigin(urlArg);
|
|
7816
|
+
let ctx;
|
|
7817
|
+
try {
|
|
7818
|
+
ctx = loadContext5(options);
|
|
7819
|
+
} catch (err) {
|
|
7820
|
+
fail(err.message);
|
|
7821
|
+
return 1;
|
|
7822
|
+
}
|
|
7823
|
+
let current;
|
|
7824
|
+
try {
|
|
7825
|
+
current = (await getOrigins(ctx.installationId, ctx.requestOptions)).allowed_origins;
|
|
7826
|
+
} catch (err) {
|
|
7827
|
+
return surfaceOriginsError(err);
|
|
7828
|
+
}
|
|
7829
|
+
const next = current.filter((o) => normalizeOrigin(o) !== origin);
|
|
7830
|
+
if (next.length === current.length) {
|
|
7831
|
+
fail(
|
|
7832
|
+
`${code(origin)} is not in the allowlist for ${code(ctx.slug)}. Run ${code("dashwise origins list")} to see current entries.`
|
|
7833
|
+
);
|
|
7834
|
+
return 1;
|
|
7835
|
+
}
|
|
7836
|
+
try {
|
|
7837
|
+
await setOrigins(ctx.installationId, next, ctx.requestOptions);
|
|
7838
|
+
success(`Removed ${code(origin)} from the SSO callback allowlist for ${code(ctx.slug)}.`);
|
|
7839
|
+
} catch (err) {
|
|
7840
|
+
return surfaceOriginsError(err);
|
|
7841
|
+
}
|
|
7842
|
+
return 0;
|
|
7843
|
+
}
|
|
7844
|
+
function surfaceOriginsError(err) {
|
|
7845
|
+
if (err instanceof ApiError) {
|
|
7846
|
+
switch (err.code) {
|
|
7847
|
+
case "UNAUTHENTICATED":
|
|
7848
|
+
fail(
|
|
7849
|
+
`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
|
|
7850
|
+
);
|
|
7851
|
+
return 1;
|
|
7852
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
7853
|
+
fail(
|
|
7854
|
+
`You're not a member of this installation's workspace. Origins management requires workspace membership.`
|
|
7855
|
+
);
|
|
7856
|
+
return 1;
|
|
7857
|
+
case "NOT_WORKSPACE_ADMIN":
|
|
7858
|
+
case "FORBIDDEN":
|
|
7859
|
+
fail(
|
|
7860
|
+
`Not authorized. Managing SSO callback origins requires workspace-admin.`
|
|
7861
|
+
);
|
|
7862
|
+
return 1;
|
|
7863
|
+
case "INVALID_ORIGIN":
|
|
7864
|
+
fail(
|
|
7865
|
+
`The backend rejected an origin (${err.message}). Non-localhost origins must be absolute https:// URLs.`
|
|
7866
|
+
);
|
|
7867
|
+
return 1;
|
|
7868
|
+
case "INSTALLATION_NOT_FOUND":
|
|
7869
|
+
fail(
|
|
7870
|
+
`Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
|
|
7871
|
+
);
|
|
7872
|
+
return 1;
|
|
7873
|
+
default:
|
|
7874
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
7875
|
+
return 1;
|
|
7876
|
+
}
|
|
7877
|
+
}
|
|
7878
|
+
fail(`Origins operation failed: ${err.message}`);
|
|
7879
|
+
return 1;
|
|
7880
|
+
}
|
|
7881
|
+
|
|
6031
7882
|
// src/commands/env-export.ts
|
|
6032
7883
|
init_api_client();
|
|
6033
7884
|
init_config();
|
|
@@ -6117,16 +7968,6 @@ async function envExportCommand(options) {
|
|
|
6117
7968
|
value: apiUrl,
|
|
6118
7969
|
comment: "# DashWise backend URL \u2014 same value the CLI is logged into."
|
|
6119
7970
|
},
|
|
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
7971
|
{
|
|
6131
7972
|
key: "DASHWISE_API_KEY",
|
|
6132
7973
|
value: apiKey,
|
|
@@ -6747,6 +8588,12 @@ program.name("dashwise").description("DashWise CLI \u2014 log in, author modules
|
|
|
6747
8588
|
"--api-url <url>",
|
|
6748
8589
|
"Override DashWise backend URL (default: $DASHWISE_API_URL or saved config or http://localhost:3000)"
|
|
6749
8590
|
);
|
|
8591
|
+
program.hook("preAction", () => {
|
|
8592
|
+
const apiUrl = program.opts().apiUrl?.trim();
|
|
8593
|
+
if (apiUrl) {
|
|
8594
|
+
process.env.DASHWISE_API_URL = apiUrl.replace(/\/+$/, "");
|
|
8595
|
+
}
|
|
8596
|
+
});
|
|
6750
8597
|
program.command("login").description("Sign in via the device-code flow.").option("--no-prompt", 'Skip the "open browser" prompt; just print the URL.').option("--name <name>", "Suggested name for the minted token (default: dashwise-cli).").action(async (cmdOpts) => {
|
|
6751
8598
|
const apiUrl = program.opts().apiUrl;
|
|
6752
8599
|
const exit = await loginCommand({
|
|
@@ -6947,8 +8794,15 @@ moduleCmd.command("clone <slug>").description("Clone a published module locally.
|
|
|
6947
8794
|
});
|
|
6948
8795
|
}
|
|
6949
8796
|
);
|
|
6950
|
-
var depsCmd = program.command("deps").description(
|
|
6951
|
-
|
|
8797
|
+
var depsCmd = program.command("deps").description(
|
|
8798
|
+
"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)."
|
|
8799
|
+
).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) => {
|
|
8800
|
+
process.exitCode = await depsConnectListCommand({
|
|
8801
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
8802
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
8803
|
+
});
|
|
8804
|
+
});
|
|
8805
|
+
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
8806
|
process.exitCode = await depsListCommand({
|
|
6953
8807
|
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
6954
8808
|
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
@@ -6980,6 +8834,93 @@ depsCmd.command("check").description("Verify each declared dep still resolves to
|
|
|
6980
8834
|
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
6981
8835
|
});
|
|
6982
8836
|
});
|
|
8837
|
+
depsCmd.command("bind <provider-slug>").description(
|
|
8838
|
+
"(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."
|
|
8839
|
+
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
8840
|
+
"--installation <id>",
|
|
8841
|
+
"Provider installation id to bind to (disambiguates when the workspace has more than one candidate install of the provider).",
|
|
8842
|
+
parseIntOption
|
|
8843
|
+
).action(
|
|
8844
|
+
async (providerSlug, cmdOpts) => {
|
|
8845
|
+
process.exitCode = await depsBindCommand(providerSlug, {
|
|
8846
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
8847
|
+
...cmdOpts.installation !== void 0 ? { installation: cmdOpts.installation } : {}
|
|
8848
|
+
});
|
|
8849
|
+
}
|
|
8850
|
+
);
|
|
8851
|
+
depsCmd.command("unbind <provider-slug>").description(
|
|
8852
|
+
"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."
|
|
8853
|
+
).option("--dir <path>", "Project root (default: current directory).").action(async (providerSlug, cmdOpts) => {
|
|
8854
|
+
process.exitCode = await depsUnbindCommand(providerSlug, {
|
|
8855
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
8856
|
+
});
|
|
8857
|
+
});
|
|
8858
|
+
program.command("bind [uses-slug]").description(
|
|
8859
|
+
"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."
|
|
8860
|
+
).option("--dir <path>", "Project root (default: current directory).").option("--json", "With no slug: emit the binding-status list as JSON.").option(
|
|
8861
|
+
"--table <name-or-id>",
|
|
8862
|
+
"Workspace table (name or numeric id) to bind the slot to. Use for scripts / to skip the interactive pick."
|
|
8863
|
+
).option(
|
|
8864
|
+
"--map <usesField=physicalFieldName>",
|
|
8865
|
+
"Override a field mapping (repeatable). The server auto-completes unmapped fields by name match.",
|
|
8866
|
+
collectRepeated,
|
|
8867
|
+
[]
|
|
8868
|
+
).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(
|
|
8869
|
+
async (usesSlug, cmdOpts) => {
|
|
8870
|
+
if (usesSlug === void 0) {
|
|
8871
|
+
process.exitCode = await bindListCommand({
|
|
8872
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
8873
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
8874
|
+
});
|
|
8875
|
+
return;
|
|
8876
|
+
}
|
|
8877
|
+
process.exitCode = await bindCommand(usesSlug, {
|
|
8878
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
8879
|
+
...cmdOpts.table !== void 0 ? { table: cmdOpts.table } : {},
|
|
8880
|
+
...cmdOpts.map !== void 0 && cmdOpts.map.length > 0 ? { map: cmdOpts.map } : {},
|
|
8881
|
+
...cmdOpts.create !== void 0 ? { create: cmdOpts.create } : {},
|
|
8882
|
+
...cmdOpts.decline !== void 0 ? { decline: cmdOpts.decline } : {}
|
|
8883
|
+
});
|
|
8884
|
+
}
|
|
8885
|
+
);
|
|
8886
|
+
program.command("unbind <uses-slug>").description(
|
|
8887
|
+
"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."
|
|
8888
|
+
).option("--dir <path>", "Project root (default: current directory).").action(async (usesSlug, cmdOpts) => {
|
|
8889
|
+
process.exitCode = await unbindCommand(usesSlug, {
|
|
8890
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
8891
|
+
});
|
|
8892
|
+
});
|
|
8893
|
+
var usesCmd = program.command("uses").description("Workspace-table (`uses`) contract commands.");
|
|
8894
|
+
usesCmd.command("add").description(
|
|
8895
|
+
'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.'
|
|
8896
|
+
).option("--dir <path>", "Project root (default: current directory).").requiredOption(
|
|
8897
|
+
"--from-table <id>",
|
|
8898
|
+
"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."
|
|
8899
|
+
).option(
|
|
8900
|
+
"--slug <slug>",
|
|
8901
|
+
"Override the generated `uses` slot slug (default: slugified table name)."
|
|
8902
|
+
).option(
|
|
8903
|
+
"--ops <list>",
|
|
8904
|
+
"Comma-separated operations to declare (create,read,update,delete). Default: read."
|
|
8905
|
+
).option(
|
|
8906
|
+
"--purpose <text>",
|
|
8907
|
+
"Optional author-facing explanation, shown to admins at bind/consent."
|
|
8908
|
+
).option(
|
|
8909
|
+
"--no-bind",
|
|
8910
|
+
"Only write + push the manifest entry; don't attempt to bind it to the source table."
|
|
8911
|
+
).option("-y, --yes", "Skip the overwrite-existing-slot confirmation prompt.").action(
|
|
8912
|
+
async (cmdOpts) => {
|
|
8913
|
+
process.exitCode = await usesAddCommand({
|
|
8914
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
8915
|
+
fromTable: cmdOpts.fromTable,
|
|
8916
|
+
...cmdOpts.slug !== void 0 ? { slug: cmdOpts.slug } : {},
|
|
8917
|
+
...cmdOpts.ops !== void 0 ? { ops: cmdOpts.ops } : {},
|
|
8918
|
+
...cmdOpts.purpose !== void 0 ? { purpose: cmdOpts.purpose } : {},
|
|
8919
|
+
...cmdOpts.bind === false ? { noBind: true } : {},
|
|
8920
|
+
...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
|
|
8921
|
+
});
|
|
8922
|
+
}
|
|
8923
|
+
);
|
|
6983
8924
|
program.command("query").description(
|
|
6984
8925
|
'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
8926
|
).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 +8965,14 @@ apiKeysCmd.command("list").description(
|
|
|
7024
8965
|
apiKeysCmd.command("create [name]").description(
|
|
7025
8966
|
"Mint a new API key. The plaintext key is displayed ONCE \u2014 save it then."
|
|
7026
8967
|
).option("--dir <path>", "Project root (default: current directory).").option("--name <name>", "Override the positional name arg (e.g. for scripted use).").option(
|
|
7027
|
-
"--
|
|
7028
|
-
"
|
|
8968
|
+
"--grant <spec>",
|
|
8969
|
+
"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.",
|
|
8970
|
+
collectRepeated,
|
|
8971
|
+
[]
|
|
7029
8972
|
).option(
|
|
7030
|
-
"--expires
|
|
7031
|
-
"
|
|
8973
|
+
"--expires <days>",
|
|
8974
|
+
"Days until the key expires (converted to an absolute timestamp). Default: never expires.",
|
|
8975
|
+
parseIntOption
|
|
7032
8976
|
).option(
|
|
7033
8977
|
"--raw-only",
|
|
7034
8978
|
"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 +8981,8 @@ apiKeysCmd.command("create [name]").description(
|
|
|
7037
8981
|
process.exitCode = await apiKeysCreateCommand(nameArg, {
|
|
7038
8982
|
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
7039
8983
|
...cmdOpts.name !== void 0 ? { name: cmdOpts.name } : {},
|
|
7040
|
-
...cmdOpts.
|
|
7041
|
-
...cmdOpts.
|
|
8984
|
+
...cmdOpts.grant !== void 0 && cmdOpts.grant.length > 0 ? { grant: cmdOpts.grant } : {},
|
|
8985
|
+
...cmdOpts.expires !== void 0 ? { expires: cmdOpts.expires } : {},
|
|
7042
8986
|
...cmdOpts.rawOnly !== void 0 ? { rawOnly: cmdOpts.rawOnly } : {}
|
|
7043
8987
|
});
|
|
7044
8988
|
}
|
|
@@ -7053,6 +8997,27 @@ apiKeysCmd.command("revoke [key-id]").description(
|
|
|
7053
8997
|
});
|
|
7054
8998
|
}
|
|
7055
8999
|
);
|
|
9000
|
+
var originsCmd = program.command("origins").description(
|
|
9001
|
+
"Manage the current module install's SSO callback-origin allowlist. Local dev (localhost) is always allowed; add deployed domains here."
|
|
9002
|
+
);
|
|
9003
|
+
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) => {
|
|
9004
|
+
process.exitCode = await originsListCommand({
|
|
9005
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
9006
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
9007
|
+
});
|
|
9008
|
+
});
|
|
9009
|
+
originsCmd.command("add <url>").description(
|
|
9010
|
+
"Add an SSO callback origin (e.g. https://app.example.com). Idempotent; non-localhost origins must be https://."
|
|
9011
|
+
).option("--dir <path>", "Project root (default: current directory).").action(async (url, cmdOpts) => {
|
|
9012
|
+
process.exitCode = await originsAddCommand(url, {
|
|
9013
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
9014
|
+
});
|
|
9015
|
+
});
|
|
9016
|
+
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) => {
|
|
9017
|
+
process.exitCode = await originsRemoveCommand(url, {
|
|
9018
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
9019
|
+
});
|
|
9020
|
+
});
|
|
7056
9021
|
var envCmd = program.command("env").description("Environment-variable helpers for deploying a module.");
|
|
7057
9022
|
envCmd.command("export").description(
|
|
7058
9023
|
"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 +9231,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
|
|
|
7266
9231
|
}
|
|
7267
9232
|
})();
|
|
7268
9233
|
function getVersion() {
|
|
7269
|
-
return "0.
|
|
9234
|
+
return "0.10.0";
|
|
7270
9235
|
}
|
|
7271
9236
|
function parseIntOption(value) {
|
|
7272
9237
|
const n = Number.parseInt(value, 10);
|