@dashai/cli 0.14.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +2476 -1311
- package/dist/bin.js.map +1 -1
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -3,9 +3,9 @@ import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, watch, read
|
|
|
3
3
|
import { tmpdir, platform, homedir } from 'os';
|
|
4
4
|
import { resolve, join, relative, dirname, basename, sep } from 'path';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
|
+
import { spawn, spawnSync } from 'child_process';
|
|
6
7
|
import { intro, log, select, isCancel, cancel, spinner, outro, text, multiselect, confirm } from '@clack/prompts';
|
|
7
8
|
import { Command, Option } from 'commander';
|
|
8
|
-
import { spawn, spawnSync } from 'child_process';
|
|
9
9
|
import open from 'open';
|
|
10
10
|
import AdmZip from 'adm-zip';
|
|
11
11
|
import * as tar from 'tar';
|
|
@@ -447,6 +447,15 @@ async function listWorkspaceInstallations(workspaceId, options = {}) {
|
|
|
447
447
|
options
|
|
448
448
|
);
|
|
449
449
|
}
|
|
450
|
+
async function uninstallModuleInstallation(installationId, opts = {}, options = {}) {
|
|
451
|
+
const qs = opts.parkConsumers ? "?park_consumers=true" : "";
|
|
452
|
+
await apiRequest(
|
|
453
|
+
"DELETE",
|
|
454
|
+
`/api/modules/installations/${installationId}${qs}`,
|
|
455
|
+
void 0,
|
|
456
|
+
options
|
|
457
|
+
);
|
|
458
|
+
}
|
|
450
459
|
async function applyDraftManifest(moduleId, manifest, options = {}) {
|
|
451
460
|
return apiRequest(
|
|
452
461
|
"PUT",
|
|
@@ -512,6 +521,79 @@ var init_output = __esm({
|
|
|
512
521
|
"src/lib/output.ts"() {
|
|
513
522
|
}
|
|
514
523
|
});
|
|
524
|
+
|
|
525
|
+
// src/lib/connect-status.ts
|
|
526
|
+
async function reportConnectStatus(opts) {
|
|
527
|
+
const [deps, bindings] = await Promise.all([
|
|
528
|
+
safeGet(
|
|
529
|
+
() => getInstallationDependencies(opts.installationId, opts.requestOptions)
|
|
530
|
+
),
|
|
531
|
+
safeGet(
|
|
532
|
+
() => getInstallationBindings(opts.installationId, opts.requestOptions)
|
|
533
|
+
)
|
|
534
|
+
]);
|
|
535
|
+
if (deps === null && bindings === null) return;
|
|
536
|
+
const depList = deps ?? [];
|
|
537
|
+
const bindList = bindings ?? [];
|
|
538
|
+
if (depList.length === 0 && bindList.length === 0) return;
|
|
539
|
+
const unboundDeps = depList.filter((d) => d.status === "unbound").length;
|
|
540
|
+
const unboundUses = bindList.filter((b) => b.status === "unbound").length;
|
|
541
|
+
info(
|
|
542
|
+
`Connect status for ${code(opts.moduleSlug)} ${dim(
|
|
543
|
+
`(${depList.length} dep${depList.length === 1 ? "" : "s"}, ${bindList.length} uses slot${bindList.length === 1 ? "" : "s"})`
|
|
544
|
+
)}:`
|
|
545
|
+
);
|
|
546
|
+
for (const d of depList) renderDep(d);
|
|
547
|
+
for (const b of bindList) renderBinding(b);
|
|
548
|
+
const hints = [];
|
|
549
|
+
if (unboundDeps > 0) {
|
|
550
|
+
hints.push(
|
|
551
|
+
`${code("dashwise deps bind <provider-slug>")} once the provider is installed`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
if (unboundUses > 0) {
|
|
555
|
+
hints.push(`${code("dashwise bind <uses-slug> --table <name>")} to connect a table`);
|
|
556
|
+
}
|
|
557
|
+
if (hints.length > 0) {
|
|
558
|
+
info(`Connect parked items with ${hints.join("; ")}.`);
|
|
559
|
+
}
|
|
560
|
+
out("");
|
|
561
|
+
}
|
|
562
|
+
function renderDep(d) {
|
|
563
|
+
if (d.status === "resolved") {
|
|
564
|
+
const providerLine = d.provider ? ` ${dim(`\u2192 install #${d.provider.installation_id} @ v${d.provider.version}`)}` : "";
|
|
565
|
+
out(` ${pc.green("\u25CF resolved")} ${dim("dep")} ${code(d.slug)}@${d.range ?? "*"}${providerLine}`);
|
|
566
|
+
} else {
|
|
567
|
+
out(
|
|
568
|
+
` ${pc.yellow("\u25CB unbound")} ${dim("dep")} ${code(d.slug)}@${d.range ?? "*"} ${dim(`reason: ${d.reason ?? "(unspecified)"}`)}`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function renderBinding(b) {
|
|
573
|
+
if (b.status === "bound") {
|
|
574
|
+
const tableLine = b.table ? ` ${dim(`\u2192 ${b.table.name} (id ${b.table.id})`)}` : "";
|
|
575
|
+
out(` ${pc.green("\u25CF bound")} ${dim("uses")} ${code(b.uses_slug)}${tableLine}`);
|
|
576
|
+
} else if (b.status === "declined") {
|
|
577
|
+
out(` ${pc.dim("\u2298 declined")} ${dim("uses")} ${code(b.uses_slug)}`);
|
|
578
|
+
} else {
|
|
579
|
+
out(
|
|
580
|
+
` ${pc.yellow("\u25CB unbound")} ${dim("uses")} ${code(b.uses_slug)} ${dim(`reason: ${b.reason ?? "(unspecified)"}`)}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async function safeGet(fn) {
|
|
585
|
+
try {
|
|
586
|
+
return await fn();
|
|
587
|
+
} catch (err) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
var init_connect_status = __esm({
|
|
592
|
+
"src/lib/connect-status.ts"() {
|
|
593
|
+
init_api_client();
|
|
594
|
+
init_output();
|
|
595
|
+
}
|
|
596
|
+
});
|
|
515
597
|
function manifestPath(dir) {
|
|
516
598
|
return join(resolve(dir ?? process.cwd()), "module.json");
|
|
517
599
|
}
|
|
@@ -545,137 +627,751 @@ var init_manifest_io = __esm({
|
|
|
545
627
|
}
|
|
546
628
|
});
|
|
547
629
|
|
|
548
|
-
// src/lib/
|
|
549
|
-
function
|
|
550
|
-
|
|
630
|
+
// src/lib/manifest-merge.ts
|
|
631
|
+
function tablesBySlug(manifest) {
|
|
632
|
+
const map = /* @__PURE__ */ new Map();
|
|
633
|
+
const owns = manifest?.owns;
|
|
634
|
+
if (!owns || typeof owns !== "object") return map;
|
|
635
|
+
const tables = owns.tables;
|
|
636
|
+
if (!Array.isArray(tables)) return map;
|
|
637
|
+
for (const t of tables) {
|
|
638
|
+
if (t && typeof t === "object" && typeof t.slug === "string") {
|
|
639
|
+
map.set(t.slug, t);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return map;
|
|
643
|
+
}
|
|
644
|
+
function mergeRemoteManifest(local, remote) {
|
|
645
|
+
const preserved = [];
|
|
646
|
+
const merged = { ...remote };
|
|
647
|
+
if (!local || typeof local !== "object" || Array.isArray(local)) {
|
|
648
|
+
return { merged, preserved };
|
|
649
|
+
}
|
|
650
|
+
for (const [key, value] of Object.entries(local)) {
|
|
651
|
+
if (!(key in remote)) {
|
|
652
|
+
merged[key] = value;
|
|
653
|
+
preserved.push(key);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const localTables = tablesBySlug(local);
|
|
657
|
+
const remoteOwns = remote.owns;
|
|
658
|
+
if (remoteOwns && typeof remoteOwns === "object" && Array.isArray(remoteOwns.tables)) {
|
|
659
|
+
const remoteTables = remoteOwns.tables;
|
|
660
|
+
let touched = false;
|
|
661
|
+
const mergedTables = remoteTables.map((t) => {
|
|
662
|
+
if (!t || typeof t !== "object") return t;
|
|
663
|
+
const slug = t.slug;
|
|
664
|
+
if (typeof slug !== "string") return t;
|
|
665
|
+
const localTable = localTables.get(slug);
|
|
666
|
+
if (localTable && Array.isArray(localTable.indexes) && t.indexes === void 0) {
|
|
667
|
+
preserved.push(`owns.tables[${slug}].indexes`);
|
|
668
|
+
touched = true;
|
|
669
|
+
return { ...t, indexes: localTable.indexes };
|
|
670
|
+
}
|
|
671
|
+
return t;
|
|
672
|
+
});
|
|
673
|
+
if (touched) {
|
|
674
|
+
merged.owns = { ...remoteOwns, tables: mergedTables };
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return { merged, preserved };
|
|
551
678
|
}
|
|
552
|
-
function
|
|
553
|
-
const
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
version: "0.0.1"
|
|
560
|
-
},
|
|
561
|
-
module_kind: kind,
|
|
562
|
-
owns: {
|
|
563
|
-
tables: [
|
|
564
|
-
{
|
|
565
|
-
// NOTE: no `name` key here — the backend manifest validator
|
|
566
|
-
// (POST /api/installations/dev) is strict and rejects unrecognized
|
|
567
|
-
// table keys, and it derives the display name from the slug. A
|
|
568
|
-
// `name` here would fail provisioning's table materialization (#4).
|
|
569
|
-
slug: "items",
|
|
570
|
-
fields: [
|
|
571
|
-
{ slug: "title", type: "text" },
|
|
572
|
-
{ slug: "done", type: "boolean" }
|
|
573
|
-
]
|
|
574
|
-
}
|
|
575
|
-
]
|
|
679
|
+
function fieldSlugs(table) {
|
|
680
|
+
const set = /* @__PURE__ */ new Set();
|
|
681
|
+
const fields = table?.fields;
|
|
682
|
+
if (!Array.isArray(fields)) return set;
|
|
683
|
+
for (const f of fields) {
|
|
684
|
+
if (f && typeof f === "object" && typeof f.slug === "string") {
|
|
685
|
+
set.add(f.slug);
|
|
576
686
|
}
|
|
577
|
-
};
|
|
578
|
-
if (kind === "custom") {
|
|
579
|
-
manifest.runtime = {
|
|
580
|
-
type: "nextjs",
|
|
581
|
-
kind: "machine",
|
|
582
|
-
machine_size: "shared-cpu-1x",
|
|
583
|
-
server_actions: true
|
|
584
|
-
};
|
|
585
687
|
}
|
|
586
|
-
|
|
587
|
-
|
|
688
|
+
return set;
|
|
689
|
+
}
|
|
690
|
+
function computeDestructiveDelta(local, remote) {
|
|
691
|
+
const tablesRemoved = [];
|
|
692
|
+
const fieldsRemoved = [];
|
|
693
|
+
const localTables = tablesBySlug(local);
|
|
694
|
+
const remoteTables = tablesBySlug(remote);
|
|
695
|
+
for (const [slug, remoteTable] of remoteTables) {
|
|
696
|
+
const localTable = localTables.get(slug);
|
|
697
|
+
if (!localTable) {
|
|
698
|
+
tablesRemoved.push(slug);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
const localFields = fieldSlugs(localTable);
|
|
702
|
+
for (const field of fieldSlugs(remoteTable)) {
|
|
703
|
+
if (!localFields.has(field)) fieldsRemoved.push(`${slug}.${field}`);
|
|
704
|
+
}
|
|
588
705
|
}
|
|
589
|
-
return
|
|
706
|
+
return { tablesRemoved, fieldsRemoved };
|
|
590
707
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
708
|
+
var PRESERVED_BLOCKS_NOTICE;
|
|
709
|
+
var init_manifest_merge = __esm({
|
|
710
|
+
"src/lib/manifest-merge.ts"() {
|
|
711
|
+
PRESERVED_BLOCKS_NOTICE = "[sync] preserved local-only manifest blocks (rbac/indexes) \u2014 run `dashwise schema push` to apply them to the dev install";
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
function startManifestPollLoop(opts) {
|
|
715
|
+
const {
|
|
716
|
+
manifestPath: manifestPath2,
|
|
717
|
+
requestOptions,
|
|
718
|
+
intervalMs,
|
|
719
|
+
onUpdate,
|
|
720
|
+
onError
|
|
721
|
+
} = opts;
|
|
722
|
+
let lastSeenRemoteJson = null;
|
|
723
|
+
let timer = null;
|
|
724
|
+
let stopped = false;
|
|
725
|
+
let preservedNoticeShown = false;
|
|
726
|
+
const tick = async () => {
|
|
727
|
+
if (stopped) return;
|
|
728
|
+
try {
|
|
729
|
+
const { manifest } = await getInstallationSchema(requestOptions);
|
|
730
|
+
const remoteJson = JSON.stringify(manifest, null, 2) + "\n";
|
|
731
|
+
const currentLocal = existsSync(manifestPath2) ? readFileSync(manifestPath2, "utf-8") : "";
|
|
732
|
+
if (remoteJson === currentLocal) {
|
|
733
|
+
lastSeenRemoteJson = remoteJson;
|
|
734
|
+
} else if (remoteJson === lastSeenRemoteJson) {
|
|
735
|
+
} else {
|
|
736
|
+
const localParsed = (() => {
|
|
737
|
+
try {
|
|
738
|
+
return currentLocal ? JSON.parse(currentLocal) : null;
|
|
739
|
+
} catch {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
})();
|
|
743
|
+
const { merged, preserved } = mergeRemoteManifest(localParsed, manifest);
|
|
744
|
+
const mergedJson = JSON.stringify(merged, null, 2) + "\n";
|
|
745
|
+
lastSeenRemoteJson = remoteJson;
|
|
746
|
+
if (mergedJson !== currentLocal) {
|
|
747
|
+
writeFileSync(manifestPath2, mergedJson, "utf-8");
|
|
748
|
+
if (onUpdate) {
|
|
749
|
+
const delta = Buffer.byteLength(mergedJson, "utf8") - Buffer.byteLength(currentLocal, "utf8");
|
|
750
|
+
const deltaStr = delta === 0 ? "0 bytes" : `${delta > 0 ? "+" : ""}${delta} bytes`;
|
|
751
|
+
onUpdate(
|
|
752
|
+
`Backend manifest changed \u2014 synced to module.json (${deltaStr}).`
|
|
753
|
+
);
|
|
754
|
+
if (preserved.length > 0 && !preservedNoticeShown) {
|
|
755
|
+
preservedNoticeShown = true;
|
|
756
|
+
onUpdate(PRESERVED_BLOCKS_NOTICE);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
} catch (err) {
|
|
762
|
+
if (onError) {
|
|
763
|
+
const message = err instanceof ApiError ? `${err.code} (HTTP ${err.status}): ${err.message}` : err.message;
|
|
764
|
+
onError(new Error(message));
|
|
765
|
+
}
|
|
766
|
+
} finally {
|
|
767
|
+
if (!stopped) {
|
|
768
|
+
timer = setTimeout(() => void tick(), intervalMs);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
timer = setTimeout(() => void tick(), intervalMs);
|
|
773
|
+
return {
|
|
774
|
+
stop: () => {
|
|
775
|
+
if (stopped) return;
|
|
776
|
+
stopped = true;
|
|
777
|
+
if (timer) {
|
|
778
|
+
clearTimeout(timer);
|
|
779
|
+
timer = null;
|
|
616
780
|
}
|
|
617
781
|
},
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
) + "\n";
|
|
621
|
-
}
|
|
622
|
-
function tsconfigJson() {
|
|
623
|
-
return JSON.stringify(
|
|
624
|
-
{
|
|
625
|
-
compilerOptions: {
|
|
626
|
-
target: "ES2022",
|
|
627
|
-
lib: ["dom", "dom.iterable", "ES2022"],
|
|
628
|
-
module: "ESNext",
|
|
629
|
-
moduleResolution: "Bundler",
|
|
630
|
-
jsx: "preserve",
|
|
631
|
-
strict: true,
|
|
632
|
-
skipLibCheck: true,
|
|
633
|
-
resolveJsonModule: true,
|
|
634
|
-
esModuleInterop: true,
|
|
635
|
-
isolatedModules: true,
|
|
636
|
-
allowJs: false,
|
|
637
|
-
noEmit: true,
|
|
638
|
-
incremental: true,
|
|
639
|
-
plugins: [{ name: "next" }],
|
|
640
|
-
paths: { "@/*": ["./*"] }
|
|
641
|
-
},
|
|
642
|
-
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
643
|
-
exclude: ["node_modules"]
|
|
644
|
-
},
|
|
645
|
-
null,
|
|
646
|
-
2
|
|
647
|
-
) + "\n";
|
|
648
|
-
}
|
|
649
|
-
function nextConfig() {
|
|
650
|
-
return `/** @type {import("next").NextConfig} */
|
|
651
|
-
//
|
|
652
|
-
// Phase 2 two-var contract: the deploy env supplies exactly
|
|
653
|
-
// DASHWISE_API_URL + DASHWISE_API_KEY. The browser needs the API URL
|
|
654
|
-
// too (the SDK's <AuthProvider> calls /api/auth/sessions/me on it), so
|
|
655
|
-
// we DERIVE NEXT_PUBLIC_DASHWISE_API_URL here from DASHWISE_API_URL at
|
|
656
|
-
// build/start time \u2014 you never set the NEXT_PUBLIC_ var yourself. If
|
|
657
|
-
// you already have one set (e.g. a custom setup), it wins.
|
|
658
|
-
const apiUrl = process.env.DASHWISE_API_URL;
|
|
659
|
-
|
|
660
|
-
const nextConfig = {
|
|
661
|
-
// Required for the platform-hosted (per-install Lambda) runtime: the
|
|
662
|
-
// injected wrapper probes [.next/standalone/server.js, server.js] and
|
|
663
|
-
// exit(1)s the cold start if neither exists. Without this the built
|
|
664
|
-
// Lambda FATALs. Harmless for local \`next dev\` / \`next start\`.
|
|
665
|
-
output: 'standalone',
|
|
666
|
-
typescript: { ignoreBuildErrors: true },
|
|
667
|
-
eslint: { ignoreDuringBuilds: true },
|
|
668
|
-
env: {
|
|
669
|
-
NEXT_PUBLIC_DASHWISE_API_URL:
|
|
670
|
-
process.env.NEXT_PUBLIC_DASHWISE_API_URL ?? apiUrl,
|
|
671
|
-
},
|
|
672
|
-
};
|
|
673
|
-
export default nextConfig;
|
|
674
|
-
`;
|
|
782
|
+
isRunning: () => !stopped
|
|
783
|
+
};
|
|
675
784
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
785
|
+
var init_manifest_poll = __esm({
|
|
786
|
+
"src/lib/manifest-poll.ts"() {
|
|
787
|
+
init_api_client();
|
|
788
|
+
init_manifest_merge();
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
async function devCommand(options) {
|
|
792
|
+
if (options.sdkOnly && options.nextOnly) {
|
|
793
|
+
fail(
|
|
794
|
+
`Cannot pass both ${code("--sdk-only")} and ${code("--next-only")} \u2014 they're mutually exclusive.`
|
|
795
|
+
);
|
|
796
|
+
return 1;
|
|
797
|
+
}
|
|
798
|
+
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
799
|
+
const manifestPath2 = `${projectRoot}/module.json`;
|
|
800
|
+
if (!existsSync(manifestPath2)) {
|
|
801
|
+
fail(`No ${code("module.json")} found in ${code(projectRoot)}.`);
|
|
802
|
+
info(`Run ${code("dashwise module init <slug>")} to scaffold a new module first.`);
|
|
803
|
+
return 1;
|
|
804
|
+
}
|
|
805
|
+
const manifestSlug = (() => {
|
|
806
|
+
try {
|
|
807
|
+
return readManifest(projectRoot)?.module?.slug;
|
|
808
|
+
} catch {
|
|
809
|
+
return void 0;
|
|
810
|
+
}
|
|
811
|
+
})();
|
|
812
|
+
const savedConfig = (() => {
|
|
813
|
+
try {
|
|
814
|
+
return readConfig();
|
|
815
|
+
} catch {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
})();
|
|
819
|
+
const apiToken = process.env.DASHWISE_API_TOKEN ?? savedConfig?.token;
|
|
820
|
+
if (!apiToken) {
|
|
821
|
+
fail(`Not authenticated. Run ${code("dashwise login")} first.`);
|
|
822
|
+
info(
|
|
823
|
+
"The unified dev loop reads your CLI token from ~/.config/dashwise/auth.json and injects it into both child processes so the SDK can authenticate without you editing .env.local."
|
|
824
|
+
);
|
|
825
|
+
return 1;
|
|
826
|
+
}
|
|
827
|
+
const apiUrl = options.apiUrl ?? process.env.DASHWISE_API_URL ?? savedConfig?.apiUrl ?? "http://localhost:3000";
|
|
828
|
+
let port;
|
|
829
|
+
try {
|
|
830
|
+
port = await resolveDevPort(options.port);
|
|
831
|
+
} catch (err) {
|
|
832
|
+
fail(err.message);
|
|
833
|
+
return 1;
|
|
834
|
+
}
|
|
835
|
+
const explicitInstallationId = options.installationId ?? process.env.DASHWISE_INSTALLATION_ID;
|
|
836
|
+
const reuseDevSessionId = process.env.DASHWISE_DEV_SESSION_ID;
|
|
837
|
+
let moduleEntry = manifestSlug !== void 0 ? readModuleEntry(manifestSlug) : null;
|
|
838
|
+
const healed = manifestSlug !== void 0 ? await resolveModuleApiKey(manifestSlug, moduleEntry, apiUrl, apiToken) : null;
|
|
839
|
+
if (healed) moduleEntry = healed;
|
|
840
|
+
const apiKeyEnv = process.env.DASHWISE_API_KEY ?? moduleEntry?.apiKey;
|
|
841
|
+
let installationIdEnv;
|
|
842
|
+
let revokeDevSession;
|
|
843
|
+
let devHandoffEnv = {};
|
|
844
|
+
if (explicitInstallationId && explicitInstallationId !== "local") {
|
|
845
|
+
installationIdEnv = explicitInstallationId;
|
|
846
|
+
} else if (reuseDevSessionId) ; else {
|
|
847
|
+
const effectiveWorkspace = options.workspace ?? moduleEntry?.workspaceId?.toString();
|
|
848
|
+
let workspaceContext;
|
|
849
|
+
try {
|
|
850
|
+
workspaceContext = await resolveTargetWorkspace(
|
|
851
|
+
apiUrl,
|
|
852
|
+
apiToken,
|
|
853
|
+
effectiveWorkspace
|
|
854
|
+
);
|
|
855
|
+
} catch (err) {
|
|
856
|
+
fail(err.message);
|
|
857
|
+
return 1;
|
|
858
|
+
}
|
|
859
|
+
const callbackOrigins = computeCallbackOrigins(port, options.callbackOrigin ?? []);
|
|
860
|
+
try {
|
|
861
|
+
const session = await registerDevSession(apiUrl, apiToken, {
|
|
862
|
+
target_workspace_id: workspaceContext.id,
|
|
863
|
+
callback_origins: callbackOrigins
|
|
864
|
+
});
|
|
865
|
+
revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session.id);
|
|
866
|
+
devHandoffEnv = devSessionHandoffEnv(session);
|
|
867
|
+
info(` ${dim("dev session")} ${session.id}`);
|
|
868
|
+
info(` ${dim("workspace")} ${workspaceContext.name} (id=${workspaceContext.id}, slug=${workspaceContext.slug})`);
|
|
869
|
+
info(` ${dim("callbacks")} ${callbackOrigins.join(", ")}`);
|
|
870
|
+
info(` ${dim("expires")} ${session.expires_at}`);
|
|
871
|
+
info(
|
|
872
|
+
` ${dim("sign-in")} ${Object.keys(devHandoffEnv).length > 0 ? "zero-click (server-side dev handoff armed)" : "browser SSO (backend issued no handoff secret \u2014 pre-2026-07-15 backend)"}`
|
|
873
|
+
);
|
|
874
|
+
} catch (err) {
|
|
875
|
+
if (err instanceof ApiError) {
|
|
876
|
+
fail(`Failed to register dev session: ${err.code} \u2014 ${err.message}`);
|
|
877
|
+
} else {
|
|
878
|
+
fail(`Failed to register dev session: ${err.message}`);
|
|
879
|
+
}
|
|
880
|
+
return 1;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const childEnv = {
|
|
884
|
+
...process.env,
|
|
885
|
+
DASHWISE_API_URL: apiUrl,
|
|
886
|
+
NEXT_PUBLIC_DASHWISE_API_URL: apiUrl,
|
|
887
|
+
...apiKeyEnv !== void 0 ? { DASHWISE_API_KEY: apiKeyEnv } : {},
|
|
888
|
+
...devHandoffEnv
|
|
889
|
+
};
|
|
890
|
+
info(`Dev loop starting \u2014 Ctrl-C to stop.`);
|
|
891
|
+
info(` ${dim("api")} ${apiUrl}`);
|
|
892
|
+
if (installationIdEnv !== void 0) {
|
|
893
|
+
info(` ${dim("installation")} ${installationIdEnv} (prod-mode SSO)`);
|
|
894
|
+
} else {
|
|
895
|
+
info(` ${dim("mode")} dev (dev SSO session registered)`);
|
|
896
|
+
}
|
|
897
|
+
info(` ${dim("cli token")} ${maskedToken(apiToken)} ${dim(`(from ${apiToken === savedConfig?.token ? "~/.config/dashwise/auth.json" : "$DASHWISE_API_TOKEN"})`)}`);
|
|
898
|
+
if (apiKeyEnv !== void 0) {
|
|
899
|
+
const source = process.env.DASHWISE_API_KEY === apiKeyEnv ? "$DASHWISE_API_KEY" : `~/.config/dashwise/auth.json#modules[${manifestSlug}]`;
|
|
900
|
+
const installId = moduleEntry?.installationId;
|
|
901
|
+
info(
|
|
902
|
+
` ${dim("api key")} ${maskedToken(apiKeyEnv)} ${dim(`(from ${source}${installId ? `, install id=${installId}` : ""})`)}`
|
|
903
|
+
);
|
|
904
|
+
} else if (manifestSlug !== void 0 && installationIdEnv === void 0) {
|
|
905
|
+
fail(
|
|
906
|
+
`No ${code("dwk_")} API key for module ${code(manifestSlug)} \u2014 the module will run with NO token.`
|
|
907
|
+
);
|
|
908
|
+
info(
|
|
909
|
+
` Data-plane calls (db, qb) AND sign-in will fail until you provision a dev install:
|
|
910
|
+
${code(`dashwise init ${manifestSlug} --workspace <your-workspace-slug>`)}
|
|
911
|
+
(Ensure you're logged into the right backend: ${code(`dashwise login --api-url ${apiUrl}`)}.)`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
info("");
|
|
915
|
+
if (moduleEntry !== null && manifestSlug !== void 0) {
|
|
916
|
+
await reportConnectStatus({
|
|
917
|
+
installationId: moduleEntry.installationId,
|
|
918
|
+
moduleSlug: manifestSlug,
|
|
919
|
+
requestOptions: { baseUrl: apiUrl, auth: { token: apiToken } }
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
const children = [];
|
|
923
|
+
if (!options.nextOnly) {
|
|
924
|
+
const sdkChild = spawnSdkWatcher(projectRoot, childEnv);
|
|
925
|
+
children.push({ name: "sdk", child: sdkChild, prefix: PREFIX_SDK });
|
|
926
|
+
}
|
|
927
|
+
if (!options.sdkOnly) {
|
|
928
|
+
const nextChild = spawnNextDev(projectRoot, childEnv, port);
|
|
929
|
+
children.push({ name: "next", child: nextChild, prefix: PREFIX_NEXT });
|
|
930
|
+
}
|
|
931
|
+
for (const { child, prefix } of children) {
|
|
932
|
+
pipeWithPrefix(child.stdout, process.stdout, prefix);
|
|
933
|
+
pipeWithPrefix(child.stderr, process.stderr, prefix);
|
|
934
|
+
}
|
|
935
|
+
let manifestPoll = null;
|
|
936
|
+
const pollEntry = moduleEntry;
|
|
937
|
+
const enablePoll = !options.noRemotePoll && pollEntry !== null && apiKeyEnv !== void 0 && manifestSlug !== void 0;
|
|
938
|
+
if (enablePoll && pollEntry !== null) {
|
|
939
|
+
const intervalSec = Math.max(
|
|
940
|
+
MIN_REMOTE_POLL_INTERVAL_SEC,
|
|
941
|
+
options.remotePollIntervalSec ?? DEFAULT_REMOTE_POLL_INTERVAL_SEC
|
|
942
|
+
);
|
|
943
|
+
manifestPoll = startManifestPollLoop({
|
|
944
|
+
manifestPath: manifestPath2,
|
|
945
|
+
requestOptions: {
|
|
946
|
+
baseUrl: apiUrl,
|
|
947
|
+
// The `dwk_` key resolves the install server-side.
|
|
948
|
+
auth: { token: apiKeyEnv }
|
|
949
|
+
},
|
|
950
|
+
intervalMs: intervalSec * 1e3,
|
|
951
|
+
onUpdate: (msg) => {
|
|
952
|
+
process.stdout.write(`${PREFIX_POLL}${msg}
|
|
953
|
+
`);
|
|
954
|
+
},
|
|
955
|
+
onError: (err) => {
|
|
956
|
+
process.stderr.write(`${PREFIX_POLL}${err.message}
|
|
957
|
+
`);
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
info(
|
|
961
|
+
` ${dim("poll")} backend manifest every ${intervalSec}s ${dim("(--no-remote-poll to disable)")}`
|
|
962
|
+
);
|
|
963
|
+
info("");
|
|
964
|
+
}
|
|
965
|
+
let shuttingDown = false;
|
|
966
|
+
const forwardSignal = (sig) => {
|
|
967
|
+
if (shuttingDown) return;
|
|
968
|
+
shuttingDown = true;
|
|
969
|
+
if (manifestPoll) {
|
|
970
|
+
manifestPoll.stop();
|
|
971
|
+
manifestPoll = null;
|
|
972
|
+
}
|
|
973
|
+
info(`
|
|
974
|
+
Received ${sig}, stopping children\u2026`);
|
|
975
|
+
for (const { child } of children) {
|
|
976
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
977
|
+
child.kill(sig);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (revokeDevSession) {
|
|
981
|
+
revokeDevSession().catch(() => {
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
|
986
|
+
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
|
987
|
+
const results = await Promise.all(
|
|
988
|
+
children.map(
|
|
989
|
+
({ name, child }) => new Promise(
|
|
990
|
+
(resolveExit) => {
|
|
991
|
+
child.on("exit", (exitCode2, signal) => {
|
|
992
|
+
const killed = signal !== null;
|
|
993
|
+
const finalCode = exitCode2 ?? (killed ? 0 : 1);
|
|
994
|
+
resolveExit({ name, code: finalCode, killed });
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
)
|
|
998
|
+
)
|
|
999
|
+
);
|
|
1000
|
+
const crashed = results.find((r) => !r.killed && r.code !== 0);
|
|
1001
|
+
if (crashed && !shuttingDown) {
|
|
1002
|
+
warn(`[${crashed.name}] exited with code ${crashed.code} \u2014 stopping others.`);
|
|
1003
|
+
for (const { child } of children) {
|
|
1004
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
1005
|
+
child.kill("SIGTERM");
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (manifestPoll) {
|
|
1010
|
+
manifestPoll.stop();
|
|
1011
|
+
manifestPoll = null;
|
|
1012
|
+
}
|
|
1013
|
+
const exitCode = Math.max(0, ...results.map((r) => r.code));
|
|
1014
|
+
if (exitCode === 0) {
|
|
1015
|
+
success("Dev loop stopped cleanly.");
|
|
1016
|
+
}
|
|
1017
|
+
return exitCode;
|
|
1018
|
+
}
|
|
1019
|
+
function spawnSdkWatcher(projectRoot, env) {
|
|
1020
|
+
const child = spawn(
|
|
1021
|
+
process.execPath,
|
|
1022
|
+
[process.argv[1], "module", "dev", "--dir", projectRoot],
|
|
1023
|
+
{
|
|
1024
|
+
cwd: projectRoot,
|
|
1025
|
+
env,
|
|
1026
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1027
|
+
}
|
|
1028
|
+
);
|
|
1029
|
+
return child;
|
|
1030
|
+
}
|
|
1031
|
+
function spawnNextDev(projectRoot, env, port) {
|
|
1032
|
+
const portArgs = port !== void 0 ? ["--", "--port", String(port)] : [];
|
|
1033
|
+
const hasDevNextScript = hasNpmScript(projectRoot, "dev:next");
|
|
1034
|
+
const [cmd, args] = hasDevNextScript ? ["npm", ["run", "dev:next", ...portArgs]] : ["npx", ["next", "dev", ...port !== void 0 ? ["--port", String(port)] : []]];
|
|
1035
|
+
const child = spawn(cmd, args, {
|
|
1036
|
+
cwd: projectRoot,
|
|
1037
|
+
env,
|
|
1038
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1039
|
+
// Windows requires `shell: true` for `npm` / `npx` because they're
|
|
1040
|
+
// `.cmd` shims, not real binaries.
|
|
1041
|
+
shell: process.platform === "win32"
|
|
1042
|
+
});
|
|
1043
|
+
return child;
|
|
1044
|
+
}
|
|
1045
|
+
function hasNpmScript(projectRoot, scriptName) {
|
|
1046
|
+
try {
|
|
1047
|
+
const pkgPath = `${projectRoot}/package.json`;
|
|
1048
|
+
if (!existsSync(pkgPath)) return false;
|
|
1049
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1050
|
+
return typeof pkg.scripts?.[scriptName] === "string";
|
|
1051
|
+
} catch {
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function pipeWithPrefix(src, dst, prefix) {
|
|
1056
|
+
if (!src) return;
|
|
1057
|
+
let leftover = "";
|
|
1058
|
+
src.on("data", (chunk) => {
|
|
1059
|
+
const text4 = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
1060
|
+
const combined = leftover + text4;
|
|
1061
|
+
const lines = combined.split("\n");
|
|
1062
|
+
leftover = lines.pop() ?? "";
|
|
1063
|
+
for (const line of lines) {
|
|
1064
|
+
dst.write(`${prefix}${line}
|
|
1065
|
+
`);
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
src.on("end", () => {
|
|
1069
|
+
if (leftover.length > 0) {
|
|
1070
|
+
dst.write(`${prefix}${leftover}
|
|
1071
|
+
`);
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
function maskedToken(token) {
|
|
1076
|
+
if (token.length <= 12) return "****";
|
|
1077
|
+
const prefix = token.slice(0, 4);
|
|
1078
|
+
const tail = token.slice(-4);
|
|
1079
|
+
return `${prefix}\u2026${tail}`;
|
|
1080
|
+
}
|
|
1081
|
+
async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
|
|
1082
|
+
if (process.env.DASHWISE_API_KEY) return null;
|
|
1083
|
+
if (!entry) return null;
|
|
1084
|
+
if (entry.apiKey) return null;
|
|
1085
|
+
if (!entry.installationId) return null;
|
|
1086
|
+
info(
|
|
1087
|
+
`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`
|
|
1088
|
+
);
|
|
1089
|
+
let minted;
|
|
1090
|
+
try {
|
|
1091
|
+
minted = await createApiKey(
|
|
1092
|
+
entry.installationId,
|
|
1093
|
+
{ name: "local-dev" },
|
|
1094
|
+
{ baseUrl: apiUrl, auth: { token: cliBearer } }
|
|
1095
|
+
);
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
if (err instanceof ApiError && err.code === "INSTALLATION_NOT_FOUND") {
|
|
1098
|
+
warn(
|
|
1099
|
+
`Could not upgrade credentials for ${code(slug)}: its dev install no longer exists. Run ${code(`dashwise init ${slug}`)} to re-provision (a clean re-register; files are preserved).`
|
|
1100
|
+
);
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
const reason = err instanceof ApiError ? `${err.code}: ${err.message}` : err.message;
|
|
1104
|
+
warn(
|
|
1105
|
+
`Could not upgrade credentials for ${code(slug)} (${reason}). Data-plane calls may fail; run ${code(`dashwise init ${slug}`)} to refresh (files are preserved).`
|
|
1106
|
+
);
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
const upgraded = {
|
|
1110
|
+
apiKey: minted.raw_key,
|
|
1111
|
+
apiKeyId: minted.api_key.id,
|
|
1112
|
+
installationId: entry.installationId,
|
|
1113
|
+
expiresAt: minted.api_key.expires_at ?? entry.expiresAt
|
|
1114
|
+
};
|
|
1115
|
+
try {
|
|
1116
|
+
upsertModuleEntry(slug, upgraded);
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
warn(
|
|
1119
|
+
`Minted a new API key for ${code(slug)} but failed to cache it: ${err.message}
|
|
1120
|
+
API key (save manually): ${minted.raw_key}`
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
success(`Upgraded ${code(slug)} to a dwk_ API key (id=${minted.api_key.id}).`);
|
|
1124
|
+
return upgraded;
|
|
1125
|
+
}
|
|
1126
|
+
function devSessionHandoffEnv(session) {
|
|
1127
|
+
const secret = session.handoff_secret;
|
|
1128
|
+
if (typeof secret !== "string" || secret.length === 0) return {};
|
|
1129
|
+
return {
|
|
1130
|
+
DASHWISE_DEV_SESSION_ID: session.id,
|
|
1131
|
+
DASHWISE_DEV_HANDOFF_SECRET: secret
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function computeCallbackOrigins(port, extra) {
|
|
1135
|
+
const def = `http://localhost:${port}`;
|
|
1136
|
+
return Array.from(/* @__PURE__ */ new Set([def, ...extra]));
|
|
1137
|
+
}
|
|
1138
|
+
async function resolveDevPort(requested) {
|
|
1139
|
+
if (requested !== void 0) {
|
|
1140
|
+
if (await isPortFree(requested)) return requested;
|
|
1141
|
+
const suggestion = await findFirstFreePort(requested + 1, PORT_PROBE_LIMIT);
|
|
1142
|
+
const suggestionHint = suggestion !== null ? `Try \`dashwise dev --port ${suggestion}\`.` : `No free port found between ${requested + 1} and ${requested + PORT_PROBE_LIMIT} either \u2014 check what's listening with \`lsof -i :${requested}\`.`;
|
|
1143
|
+
throw new Error(
|
|
1144
|
+
`Port ${requested} is already in use. ${suggestionHint}`
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
const found = await findFirstFreePort(DEFAULT_NEXT_PORT, PORT_PROBE_LIMIT);
|
|
1148
|
+
if (found === null) {
|
|
1149
|
+
throw new Error(
|
|
1150
|
+
`No free port found between ${DEFAULT_NEXT_PORT} and ${DEFAULT_NEXT_PORT + PORT_PROBE_LIMIT - 1}. Free a port (e.g. \`lsof -i :${DEFAULT_NEXT_PORT}\`) or pass \`--port <N>\` explicitly.`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
return found;
|
|
1154
|
+
}
|
|
1155
|
+
async function isPortFree(port) {
|
|
1156
|
+
const { createServer } = await import('net');
|
|
1157
|
+
return new Promise((resolve13) => {
|
|
1158
|
+
const server = createServer();
|
|
1159
|
+
server.once("error", () => {
|
|
1160
|
+
resolve13(false);
|
|
1161
|
+
});
|
|
1162
|
+
server.once("listening", () => {
|
|
1163
|
+
server.close(() => resolve13(true));
|
|
1164
|
+
});
|
|
1165
|
+
server.listen(port, "0.0.0.0");
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
async function findFirstFreePort(start, limit) {
|
|
1169
|
+
for (let i = 0; i < limit; i++) {
|
|
1170
|
+
const port = start + i;
|
|
1171
|
+
if (await isPortFree(port)) return port;
|
|
1172
|
+
}
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
async function resolveTargetWorkspace(apiUrl, apiToken, flag) {
|
|
1176
|
+
const workspaces = await apiRequest(
|
|
1177
|
+
"GET",
|
|
1178
|
+
"/api/workspaces",
|
|
1179
|
+
void 0,
|
|
1180
|
+
{ baseUrl: apiUrl, auth: { token: apiToken } }
|
|
1181
|
+
);
|
|
1182
|
+
if (workspaces.length === 0) {
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
"No workspaces found for your account. Create a workspace at the DashWise dashboard first."
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
if (flag === void 0) {
|
|
1188
|
+
if (workspaces.length === 1) {
|
|
1189
|
+
return workspaces[0];
|
|
1190
|
+
}
|
|
1191
|
+
const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id}, name="${w.name}")`).join("\n");
|
|
1192
|
+
throw new Error(
|
|
1193
|
+
`You have ${workspaces.length} workspaces. Re-run with --workspace <slug-or-id> to pick one:
|
|
1194
|
+
${list}`
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
const asNumber = Number.parseInt(flag, 10);
|
|
1198
|
+
const isNumeric = !Number.isNaN(asNumber) && String(asNumber) === flag;
|
|
1199
|
+
const match = isNumeric ? workspaces.find((w) => w.id === asNumber) : workspaces.find((w) => w.slug === flag);
|
|
1200
|
+
if (!match) {
|
|
1201
|
+
const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id})`).join("\n");
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
`No workspace matching --workspace=${flag}. Available:
|
|
1204
|
+
${list}`
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
return match;
|
|
1208
|
+
}
|
|
1209
|
+
async function registerDevSession(apiUrl, apiToken, body) {
|
|
1210
|
+
return apiRequest(
|
|
1211
|
+
"POST",
|
|
1212
|
+
"/api/auth/dev/sessions",
|
|
1213
|
+
body,
|
|
1214
|
+
{ baseUrl: apiUrl, auth: { token: apiToken } }
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
async function revokeDevSessionBestEffort(apiUrl, apiToken, id) {
|
|
1218
|
+
await apiRequest(
|
|
1219
|
+
"DELETE",
|
|
1220
|
+
`/api/auth/dev/sessions/${encodeURIComponent(id)}`,
|
|
1221
|
+
void 0,
|
|
1222
|
+
{ baseUrl: apiUrl, auth: { token: apiToken } }
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
var DEFAULT_NEXT_PORT, PREFIX_SDK, PREFIX_NEXT, PREFIX_POLL, DEFAULT_REMOTE_POLL_INTERVAL_SEC, MIN_REMOTE_POLL_INTERVAL_SEC, PORT_PROBE_LIMIT;
|
|
1226
|
+
var init_dev = __esm({
|
|
1227
|
+
"src/commands/dev.ts"() {
|
|
1228
|
+
init_api_client();
|
|
1229
|
+
init_connect_status();
|
|
1230
|
+
init_config();
|
|
1231
|
+
init_manifest_io();
|
|
1232
|
+
init_manifest_poll();
|
|
1233
|
+
init_output();
|
|
1234
|
+
DEFAULT_NEXT_PORT = 3e3;
|
|
1235
|
+
PREFIX_SDK = `\x1B[36m[sdk]\x1B[0m `;
|
|
1236
|
+
PREFIX_NEXT = `\x1B[35m[next]\x1B[0m `;
|
|
1237
|
+
PREFIX_POLL = `\x1B[33m[poll]\x1B[0m `;
|
|
1238
|
+
DEFAULT_REMOTE_POLL_INTERVAL_SEC = 5;
|
|
1239
|
+
MIN_REMOTE_POLL_INTERVAL_SEC = 1;
|
|
1240
|
+
PORT_PROBE_LIMIT = 10;
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
// src/lib/scaffold.ts
|
|
1245
|
+
function isDeployTarget(value) {
|
|
1246
|
+
return DEPLOY_TARGETS.includes(value);
|
|
1247
|
+
}
|
|
1248
|
+
function manifestJson(opts) {
|
|
1249
|
+
const kind = opts.kind ?? "hand_authored";
|
|
1250
|
+
const manifest = {
|
|
1251
|
+
schema_version: 2,
|
|
1252
|
+
module: {
|
|
1253
|
+
slug: opts.slug,
|
|
1254
|
+
name: opts.name,
|
|
1255
|
+
version: "0.0.1"
|
|
1256
|
+
},
|
|
1257
|
+
module_kind: kind,
|
|
1258
|
+
owns: {
|
|
1259
|
+
tables: [
|
|
1260
|
+
{
|
|
1261
|
+
// NOTE: no `name` key here — the backend manifest validator
|
|
1262
|
+
// (POST /api/installations/dev) is strict and rejects unrecognized
|
|
1263
|
+
// table keys, and it derives the display name from the slug. A
|
|
1264
|
+
// `name` here would fail provisioning's table materialization (#4).
|
|
1265
|
+
slug: "items",
|
|
1266
|
+
fields: [
|
|
1267
|
+
{ slug: "title", type: "text" },
|
|
1268
|
+
{ slug: "done", type: "boolean" }
|
|
1269
|
+
]
|
|
1270
|
+
}
|
|
1271
|
+
]
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
if (kind === "custom") {
|
|
1275
|
+
manifest.runtime = {
|
|
1276
|
+
type: "nextjs",
|
|
1277
|
+
kind: "machine",
|
|
1278
|
+
machine_size: "shared-cpu-1x",
|
|
1279
|
+
server_actions: true
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
if (opts.repositoryUrl) {
|
|
1283
|
+
manifest.repository = { type: "git", url: opts.repositoryUrl };
|
|
1284
|
+
}
|
|
1285
|
+
return JSON.stringify(manifest, null, 2) + "\n";
|
|
1286
|
+
}
|
|
1287
|
+
function packageJson(opts) {
|
|
1288
|
+
return JSON.stringify(
|
|
1289
|
+
{
|
|
1290
|
+
name: opts.slug,
|
|
1291
|
+
version: "0.0.1",
|
|
1292
|
+
private: true,
|
|
1293
|
+
type: "module",
|
|
1294
|
+
scripts: {
|
|
1295
|
+
dev: "dashwise module dev",
|
|
1296
|
+
"dev:next": "next dev",
|
|
1297
|
+
"dashwise:generate": "dashwise module generate",
|
|
1298
|
+
build: "next build",
|
|
1299
|
+
start: "next start"
|
|
1300
|
+
},
|
|
1301
|
+
dependencies: {
|
|
1302
|
+
"@dashai/sdk": "^2.3.0",
|
|
1303
|
+
next: "^15.0.0",
|
|
1304
|
+
react: "^19.0.0",
|
|
1305
|
+
"react-dom": "^19.0.0"
|
|
1306
|
+
},
|
|
1307
|
+
devDependencies: {
|
|
1308
|
+
"@types/node": "^22.0.0",
|
|
1309
|
+
"@types/react": "^19.0.0",
|
|
1310
|
+
"@types/react-dom": "^19.0.0",
|
|
1311
|
+
typescript: "^5.5.0"
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
null,
|
|
1315
|
+
2
|
|
1316
|
+
) + "\n";
|
|
1317
|
+
}
|
|
1318
|
+
function tsconfigJson() {
|
|
1319
|
+
return JSON.stringify(
|
|
1320
|
+
{
|
|
1321
|
+
compilerOptions: {
|
|
1322
|
+
target: "ES2022",
|
|
1323
|
+
lib: ["dom", "dom.iterable", "ES2022"],
|
|
1324
|
+
module: "ESNext",
|
|
1325
|
+
moduleResolution: "Bundler",
|
|
1326
|
+
jsx: "preserve",
|
|
1327
|
+
strict: true,
|
|
1328
|
+
skipLibCheck: true,
|
|
1329
|
+
resolveJsonModule: true,
|
|
1330
|
+
esModuleInterop: true,
|
|
1331
|
+
isolatedModules: true,
|
|
1332
|
+
allowJs: false,
|
|
1333
|
+
noEmit: true,
|
|
1334
|
+
incremental: true,
|
|
1335
|
+
plugins: [{ name: "next" }],
|
|
1336
|
+
paths: { "@/*": ["./*"] }
|
|
1337
|
+
},
|
|
1338
|
+
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
1339
|
+
exclude: ["node_modules"]
|
|
1340
|
+
},
|
|
1341
|
+
null,
|
|
1342
|
+
2
|
|
1343
|
+
) + "\n";
|
|
1344
|
+
}
|
|
1345
|
+
function nextConfig() {
|
|
1346
|
+
return `/** @type {import("next").NextConfig} */
|
|
1347
|
+
//
|
|
1348
|
+
// Phase 2 two-var contract: the deploy env supplies exactly
|
|
1349
|
+
// DASHWISE_API_URL + DASHWISE_API_KEY. The browser needs the API URL
|
|
1350
|
+
// too (the SDK's <AuthProvider> calls /api/auth/sessions/me on it), so
|
|
1351
|
+
// we DERIVE NEXT_PUBLIC_DASHWISE_API_URL here from DASHWISE_API_URL at
|
|
1352
|
+
// build/start time \u2014 you never set the NEXT_PUBLIC_ var yourself. If
|
|
1353
|
+
// you already have one set (e.g. a custom setup), it wins.
|
|
1354
|
+
const apiUrl = process.env.DASHWISE_API_URL;
|
|
1355
|
+
|
|
1356
|
+
const nextConfig = {
|
|
1357
|
+
// Required for the platform-hosted (per-install Lambda) runtime: the
|
|
1358
|
+
// injected wrapper probes [.next/standalone/server.js, server.js] and
|
|
1359
|
+
// exit(1)s the cold start if neither exists. Without this the built
|
|
1360
|
+
// Lambda FATALs. Harmless for local \`next dev\` / \`next start\`.
|
|
1361
|
+
output: 'standalone',
|
|
1362
|
+
typescript: { ignoreBuildErrors: true },
|
|
1363
|
+
eslint: { ignoreDuringBuilds: true },
|
|
1364
|
+
env: {
|
|
1365
|
+
NEXT_PUBLIC_DASHWISE_API_URL:
|
|
1366
|
+
process.env.NEXT_PUBLIC_DASHWISE_API_URL ?? apiUrl,
|
|
1367
|
+
},
|
|
1368
|
+
};
|
|
1369
|
+
export default nextConfig;
|
|
1370
|
+
`;
|
|
1371
|
+
}
|
|
1372
|
+
function nextEnvDts() {
|
|
1373
|
+
return `/// <reference types="next" />
|
|
1374
|
+
/// <reference types="next/image-types/global" />
|
|
679
1375
|
`;
|
|
680
1376
|
}
|
|
681
1377
|
function gitignore() {
|
|
@@ -1271,8 +1967,20 @@ function appApiAuthSignInTs() {
|
|
|
1271
1967
|
// 302 back to callback_url?code=<one-time>. The /api/auth/callback route
|
|
1272
1968
|
// exchanges that code for a session cookie.
|
|
1273
1969
|
//
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1970
|
+
// ZERO-CLICK LOCAL DEV (CLI \u2265 0.16.0 + backend \u2265 2026-07-15): when
|
|
1971
|
+
// \`dashwise dev\` injected BOTH DASHWISE_DEV_SESSION_ID and
|
|
1972
|
+
// DASHWISE_DEV_HANDOFF_SECRET, this route first presents the handoff
|
|
1973
|
+
// secret to the backend SERVER-SIDE (POST /api/auth/dev/sso/authorize)
|
|
1974
|
+
// and 302s the browser straight to the returned callback URL \u2014 the user
|
|
1975
|
+
// NEVER sees a login page and no platform browser session is needed. On
|
|
1976
|
+
// ANY failure it logs one server-side warn line and falls back to the
|
|
1977
|
+
// browser flow above unchanged (that path still matters for signing in
|
|
1978
|
+
// as a DIFFERENT workspace member and for stale dev sessions). The
|
|
1979
|
+
// secret is server-env-only: never in a URL, response body, or the
|
|
1980
|
+
// client bundle.
|
|
1981
|
+
//
|
|
1982
|
+
// No dev-vs-prod branching for the browser flow \u2014 the single dwk_ key
|
|
1983
|
+
// drives both local dev and production.
|
|
1276
1984
|
import { NextResponse } from 'next/server';
|
|
1277
1985
|
import { getPublicOrigin } from '@/lib/request-origin';
|
|
1278
1986
|
|
|
@@ -1292,6 +2000,30 @@ export async function GET(request: Request) {
|
|
|
1292
2000
|
status: 500,
|
|
1293
2001
|
});
|
|
1294
2002
|
}
|
|
2003
|
+
|
|
2004
|
+
// \`getPublicOrigin\` reads \`X-Forwarded-Host\` when present so URLs
|
|
2005
|
+
// point at the public domain, not the internal Node origin behind
|
|
2006
|
+
// reverse proxies (Railway/Vercel/Fly).
|
|
2007
|
+
const moduleOrigin = getPublicOrigin(request);
|
|
2008
|
+
|
|
2009
|
+
// \u2500\u2500 Zero-click dev handoff (server-side; runs BEFORE the dwk_-key
|
|
2010
|
+
// guard \u2014 the authorize endpoint authenticates with the handoff
|
|
2011
|
+
// secret, not the key). Both env vars must be present: \`dashwise dev\`
|
|
2012
|
+
// injects them together, and only when the backend issued a secret.
|
|
2013
|
+
const devSessionId = process.env.DASHWISE_DEV_SESSION_ID;
|
|
2014
|
+
const devHandoffSecret = process.env.DASHWISE_DEV_HANDOFF_SECRET;
|
|
2015
|
+
if (devSessionId && devHandoffSecret) {
|
|
2016
|
+
const callbackUrl = await tryDevHandoff(
|
|
2017
|
+
apiBaseUrl,
|
|
2018
|
+
devSessionId,
|
|
2019
|
+
devHandoffSecret,
|
|
2020
|
+
returnTo,
|
|
2021
|
+
moduleOrigin,
|
|
2022
|
+
);
|
|
2023
|
+
if (callbackUrl) return NextResponse.redirect(callbackUrl);
|
|
2024
|
+
// null \u2192 warned server-side already; fall through to the browser flow.
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1295
2027
|
if (!apiKey) {
|
|
1296
2028
|
return new NextResponse(
|
|
1297
2029
|
'DASHWISE_API_KEY must be set in the environment. Run \`dashwise dev\` ' +
|
|
@@ -1301,12 +2033,9 @@ export async function GET(request: Request) {
|
|
|
1301
2033
|
}
|
|
1302
2034
|
|
|
1303
2035
|
// Absolute callback URL so the backend knows which host to bounce back
|
|
1304
|
-
// to.
|
|
1305
|
-
//
|
|
1306
|
-
//
|
|
1307
|
-
// /api/auth/callback by SDK convention; we tuck \`return_to\` onto it so
|
|
1308
|
-
// the callback route can resume the user's original destination.
|
|
1309
|
-
const moduleOrigin = getPublicOrigin(request);
|
|
2036
|
+
// to. The callback path is fixed at /api/auth/callback by SDK
|
|
2037
|
+
// convention; we tuck \`return_to\` onto it so the callback route can
|
|
2038
|
+
// resume the user's original destination.
|
|
1310
2039
|
const callback = new URL('/api/auth/callback', moduleOrigin);
|
|
1311
2040
|
callback.searchParams.set('return_to', returnTo);
|
|
1312
2041
|
|
|
@@ -1359,6 +2088,74 @@ export async function GET(request: Request) {
|
|
|
1359
2088
|
}
|
|
1360
2089
|
return NextResponse.redirect(body.redirect_url);
|
|
1361
2090
|
}
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* Zero-click dev sign-in (CLI \u2265 0.16.0 + backend \u2265 2026-07-15).
|
|
2094
|
+
*
|
|
2095
|
+
* SERVER-SIDE ONLY: presents the dev session's one-time handoff secret
|
|
2096
|
+
* to \`POST /api/auth/dev/sso/authorize\`. On 200 the backend mints a
|
|
2097
|
+
* single-use 60s exchange code for the DEV'S OWN user and returns
|
|
2098
|
+
* \`{ callback_url }\` (= \`<origin>/api/auth/callback?code=\u2026&return_to=\u2026\`)
|
|
2099
|
+
* \u2014 the caller 302s the browser straight there; the normal callback
|
|
2100
|
+
* route exchanges the code exactly like the browser flow.
|
|
2101
|
+
*
|
|
2102
|
+
* Returns the callback URL, or \`null\` on ANY failure (404
|
|
2103
|
+
* DEV_SESSION_NOT_FOUND, 401 DEV_HANDOFF_INVALID, 400 INVALID_RETURN_TO,
|
|
2104
|
+
* 403 WORKSPACE_NOT_MEMBER, network) after logging a one-line warn \u2014
|
|
2105
|
+
* the caller then falls back to the browser SSO flow, which remains the
|
|
2106
|
+
* path for signing in as a DIFFERENT workspace member and for stale dev
|
|
2107
|
+
* sessions. The secret must never appear in a URL or response body.
|
|
2108
|
+
*/
|
|
2109
|
+
async function tryDevHandoff(
|
|
2110
|
+
apiBaseUrl: string,
|
|
2111
|
+
devSessionId: string,
|
|
2112
|
+
devHandoffSecret: string,
|
|
2113
|
+
returnTo: string,
|
|
2114
|
+
moduleOrigin: string,
|
|
2115
|
+
): Promise<string | null> {
|
|
2116
|
+
// Same root-relative-only rule as the callback's safeReturnTo \u2014 an
|
|
2117
|
+
// absolute/protocol-relative return_to must not steer the handoff.
|
|
2118
|
+
const path =
|
|
2119
|
+
returnTo.startsWith('/') && !returnTo.startsWith('//') ? returnTo : '/';
|
|
2120
|
+
// \`return_to\` is the user's ABSOLUTE destination on this module's own
|
|
2121
|
+
// (loopback) origin; the backend validates origin allowlist + loopback
|
|
2122
|
+
// and builds the callback URL from it.
|
|
2123
|
+
const destination = new URL(path, moduleOrigin);
|
|
2124
|
+
let res: Response;
|
|
2125
|
+
try {
|
|
2126
|
+
res = await fetch(\`\${apiBaseUrl}/api/auth/dev/sso/authorize\`, {
|
|
2127
|
+
method: 'POST',
|
|
2128
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
2129
|
+
body: JSON.stringify({
|
|
2130
|
+
dev_session_id: devSessionId,
|
|
2131
|
+
handoff_secret: devHandoffSecret,
|
|
2132
|
+
return_to: destination.toString(),
|
|
2133
|
+
}),
|
|
2134
|
+
});
|
|
2135
|
+
} catch (err) {
|
|
2136
|
+
console.warn(
|
|
2137
|
+
\`[auth] dev handoff failed (\${(err as Error).message}) \u2014 falling back to browser SSO\`,
|
|
2138
|
+
);
|
|
2139
|
+
return null;
|
|
2140
|
+
}
|
|
2141
|
+
if (!res.ok) {
|
|
2142
|
+
const body = (await res.json().catch(() => null)) as { code?: string } | null;
|
|
2143
|
+
console.warn(
|
|
2144
|
+
\`[auth] dev handoff rejected (\${body?.code ?? \`HTTP \${res.status}\`}) \u2014 falling back to browser SSO\`,
|
|
2145
|
+
);
|
|
2146
|
+
return null;
|
|
2147
|
+
}
|
|
2148
|
+
const body = (await res.json().catch(() => null)) as
|
|
2149
|
+
| { callback_url?: string }
|
|
2150
|
+
| null;
|
|
2151
|
+
if (!body?.callback_url) {
|
|
2152
|
+
console.warn(
|
|
2153
|
+
'[auth] dev handoff response missing callback_url \u2014 falling back to browser SSO',
|
|
2154
|
+
);
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
return body.callback_url;
|
|
2158
|
+
}
|
|
1362
2159
|
`;
|
|
1363
2160
|
}
|
|
1364
2161
|
function appApiAuthCallbackTs() {
|
|
@@ -1367,7 +2164,14 @@ function appApiAuthCallbackTs() {
|
|
|
1367
2164
|
// SSO flow failed. Two paths:
|
|
1368
2165
|
//
|
|
1369
2166
|
// 1. Success path: exchange \`?code\` for a session cookie via
|
|
1370
|
-
// \`exchangeCode()\`, then redirect to \`return_to\`.
|
|
2167
|
+
// \`exchangeCode()\`, then redirect to \`return_to\`. SDK \u2265 2.3.0:
|
|
2168
|
+
// the result's \`cookieWritten\` flag surfaces a cookie store that
|
|
2169
|
+
// rejected the write \u2014 without it the exchange "succeeds", the
|
|
2170
|
+
// browser has no session, and the user silently bounces back to
|
|
2171
|
+
// sign-in. We render /sign-in-error?code=cookie_write_failed
|
|
2172
|
+
// instead. \`requestUrl\` lets the SDK fall back to the actual
|
|
2173
|
+
// request protocol for the cookie's Secure attribute when no
|
|
2174
|
+
// x-forwarded-proto header is present (plain-http \`next start\`).
|
|
1371
2175
|
// 2. Error path: SSO returned \`?error=\` \u2014 redirect to the polished
|
|
1372
2176
|
// sign-in-error page so the user sees a real error UI instead of
|
|
1373
2177
|
// a raw 4xx page. The error code maps to known SSO error codes
|
|
@@ -1404,7 +2208,17 @@ export async function GET(request: Request) {
|
|
|
1404
2208
|
return NextResponse.redirect(target);
|
|
1405
2209
|
}
|
|
1406
2210
|
try {
|
|
1407
|
-
|
|
2211
|
+
// Sets the dashwise.session cookie. \`requestUrl\` = the Secure-attr
|
|
2212
|
+
// protocol fallback when no x-forwarded-proto header is present.
|
|
2213
|
+
const result = await exchangeCode(code, { requestUrl: request.url });
|
|
2214
|
+
if (!result.cookieWritten) {
|
|
2215
|
+
// The exchange succeeded but the cookie store rejected the write \u2014
|
|
2216
|
+
// the browser has NO session. Surface it instead of redirecting
|
|
2217
|
+
// into an unauthenticated loop (SDK \u2265 2.3.0).
|
|
2218
|
+
const target = new URL('/sign-in-error', publicOrigin);
|
|
2219
|
+
target.searchParams.set('code', 'cookie_write_failed');
|
|
2220
|
+
return NextResponse.redirect(target);
|
|
2221
|
+
}
|
|
1408
2222
|
} catch (err) {
|
|
1409
2223
|
const target = new URL('/sign-in-error', publicOrigin);
|
|
1410
2224
|
target.searchParams.set('code', 'exchange_failed');
|
|
@@ -1441,7 +2255,12 @@ import { signOut } from '@dashai/sdk/auth/server';
|
|
|
1441
2255
|
import { getPublicOrigin } from '@/lib/request-origin';
|
|
1442
2256
|
|
|
1443
2257
|
export async function POST(request: Request) {
|
|
1444
|
-
|
|
2258
|
+
// Pass requestUrl so the cookie-CLEAR resolves its Secure attribute the
|
|
2259
|
+
// SAME way the sign-in SET did (exchangeCode also gets request.url) \u2014
|
|
2260
|
+
// otherwise a plain-http localhost dev server sets a non-Secure cookie
|
|
2261
|
+
// but tries to clear a Secure one, which the browser ignores, leaving a
|
|
2262
|
+
// stale (backend-revoked, but client-present) cookie after sign-out.
|
|
2263
|
+
await signOut({ requestUrl: request.url });
|
|
1445
2264
|
// Build the redirect against the public origin so we don't land
|
|
1446
2265
|
// the user on \`http://localhost:<PORT>/\` when running behind a
|
|
1447
2266
|
// reverse proxy (Railway/Vercel/Fly).
|
|
@@ -1528,7 +2347,7 @@ function packageJsonCustom(opts) {
|
|
|
1528
2347
|
start: "next start"
|
|
1529
2348
|
},
|
|
1530
2349
|
dependencies: {
|
|
1531
|
-
"@dashai/sdk": "^2.
|
|
2350
|
+
"@dashai/sdk": "^2.3.0",
|
|
1532
2351
|
next: "^15.0.0",
|
|
1533
2352
|
react: "^19.0.0",
|
|
1534
2353
|
"react-dom": "^19.0.0",
|
|
@@ -1689,15 +2508,40 @@ function globalsCss() {
|
|
|
1689
2508
|
--chart-5: oklch(0.67 0.2 15);
|
|
1690
2509
|
}
|
|
1691
2510
|
|
|
1692
|
-
/* Wire
|
|
1693
|
-
\`
|
|
1694
|
-
|
|
2511
|
+
/* Wire EVERY design token into Tailwind's namespace. In Tailwind 4,
|
|
2512
|
+
utilities like \`bg-primary\` / \`text-muted-foreground\` / \`rounded-lg\`
|
|
2513
|
+
only exist if @theme defines the matching \`--color-*\` / \`--radius-*\`
|
|
2514
|
+
variable \u2014 the \`shadcn\` npm package's tailwind.css maps NONE of them
|
|
2515
|
+
(it ships only keyframes + variants), so without this block every
|
|
2516
|
+
semantic-token utility silently compiles to nothing. */
|
|
1695
2517
|
@theme inline {
|
|
2518
|
+
--color-background: var(--background);
|
|
2519
|
+
--color-foreground: var(--foreground);
|
|
2520
|
+
--color-card: var(--card);
|
|
2521
|
+
--color-card-foreground: var(--card-foreground);
|
|
2522
|
+
--color-popover: var(--popover);
|
|
2523
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
2524
|
+
--color-primary: var(--primary);
|
|
2525
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
2526
|
+
--color-secondary: var(--secondary);
|
|
2527
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
2528
|
+
--color-muted: var(--muted);
|
|
2529
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
2530
|
+
--color-accent: var(--accent);
|
|
2531
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
2532
|
+
--color-destructive: var(--destructive);
|
|
2533
|
+
--color-border: var(--border);
|
|
2534
|
+
--color-input: var(--input);
|
|
2535
|
+
--color-ring: var(--ring);
|
|
1696
2536
|
--color-chart-1: var(--chart-1);
|
|
1697
2537
|
--color-chart-2: var(--chart-2);
|
|
1698
2538
|
--color-chart-3: var(--chart-3);
|
|
1699
2539
|
--color-chart-4: var(--chart-4);
|
|
1700
2540
|
--color-chart-5: var(--chart-5);
|
|
2541
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
2542
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
2543
|
+
--radius-lg: var(--radius);
|
|
2544
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
1701
2545
|
}
|
|
1702
2546
|
|
|
1703
2547
|
* {
|
|
@@ -1897,6 +2741,90 @@ export function ThemeProvider({
|
|
|
1897
2741
|
}
|
|
1898
2742
|
`;
|
|
1899
2743
|
}
|
|
2744
|
+
function appErrorTsx() {
|
|
2745
|
+
return `"use client";
|
|
2746
|
+
|
|
2747
|
+
/**
|
|
2748
|
+
* Root error boundary. Next.js renders this when anything below the
|
|
2749
|
+
* layout throws during render or data loading.
|
|
2750
|
+
*
|
|
2751
|
+
* - \`error.digest\` is the server-side correlation id \u2014 surface it so a
|
|
2752
|
+
* bug report can be matched to the runtime logs.
|
|
2753
|
+
* - \`reset()\` re-renders the segment; for transient failures (network
|
|
2754
|
+
* blip, expired session mid-flight) a retry often just works.
|
|
2755
|
+
*/
|
|
2756
|
+
import { Button } from "@/components/ui/button";
|
|
2757
|
+
import {
|
|
2758
|
+
Card,
|
|
2759
|
+
CardContent,
|
|
2760
|
+
CardDescription,
|
|
2761
|
+
CardHeader,
|
|
2762
|
+
CardTitle,
|
|
2763
|
+
} from "@/components/ui/card";
|
|
2764
|
+
|
|
2765
|
+
export default function Error({
|
|
2766
|
+
error,
|
|
2767
|
+
reset,
|
|
2768
|
+
}: {
|
|
2769
|
+
error: Error & { digest?: string };
|
|
2770
|
+
reset: () => void;
|
|
2771
|
+
}) {
|
|
2772
|
+
return (
|
|
2773
|
+
<div className="flex min-h-screen items-center justify-center bg-background p-6">
|
|
2774
|
+
<Card className="w-full max-w-md">
|
|
2775
|
+
<CardHeader>
|
|
2776
|
+
<CardTitle>Something went wrong</CardTitle>
|
|
2777
|
+
<CardDescription>
|
|
2778
|
+
An unexpected error occurred while rendering this page.
|
|
2779
|
+
</CardDescription>
|
|
2780
|
+
</CardHeader>
|
|
2781
|
+
<CardContent className="space-y-4">
|
|
2782
|
+
{error.digest ? (
|
|
2783
|
+
<p className="text-sm text-muted-foreground">
|
|
2784
|
+
Error digest:{" "}
|
|
2785
|
+
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
|
|
2786
|
+
{error.digest}
|
|
2787
|
+
</code>
|
|
2788
|
+
</p>
|
|
2789
|
+
) : null}
|
|
2790
|
+
<Button onClick={() => reset()}>Try again</Button>
|
|
2791
|
+
</CardContent>
|
|
2792
|
+
</Card>
|
|
2793
|
+
</div>
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
`;
|
|
2797
|
+
}
|
|
2798
|
+
function seedMjs() {
|
|
2799
|
+
return `/**
|
|
2800
|
+
* seed.mjs \u2014 OPTIONAL seed/maintenance script template.
|
|
2801
|
+
*
|
|
2802
|
+
* Run it with the module's DashWise env injected (DASHWISE_API_URL +
|
|
2803
|
+
* DASHWISE_API_KEY resolved like \`dashwise query\`: explicit env \u2192
|
|
2804
|
+
* .env.local \u2192 the dwk_ key cached by your CLI login):
|
|
2805
|
+
*
|
|
2806
|
+
* dashwise run -- node seed.mjs
|
|
2807
|
+
*
|
|
2808
|
+
* (Requires \`dashwise module generate\` to have run at least once so
|
|
2809
|
+
* \`@dashai/generated\` exists \u2014 \`dashwise dev\` does this for you.)
|
|
2810
|
+
*
|
|
2811
|
+
* \u26A0 PUBLISH WARNING: \`dashwise module publish\` packs the ENTIRE project
|
|
2812
|
+
* tree (it does NOT read .gitignore). Delete this file \u2014 or publish with
|
|
2813
|
+
* \`--exclude seed.mjs\` \u2014 before publishing, so seed data/logic doesn't
|
|
2814
|
+
* ship inside your module bundle.
|
|
2815
|
+
*/
|
|
2816
|
+
|
|
2817
|
+
// Uncomment to use (the scaffold's example \`items\` table):
|
|
2818
|
+
//
|
|
2819
|
+
// import { db } from '@dashai/generated';
|
|
2820
|
+
//
|
|
2821
|
+
// const created = await db.items.create({ title: 'Seeded item', done: false });
|
|
2822
|
+
// console.log('created items row', created.id);
|
|
2823
|
+
//
|
|
2824
|
+
// const rows = await db.items.list({ limit: 10 });
|
|
2825
|
+
// console.log('items now:', rows.length);
|
|
2826
|
+
`;
|
|
2827
|
+
}
|
|
1900
2828
|
function appSignInErrorPageTsx() {
|
|
1901
2829
|
return `import Link from 'next/link';
|
|
1902
2830
|
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
|
@@ -1920,6 +2848,8 @@ const ERROR_MESSAGES: Record<string, string> = {
|
|
|
1920
2848
|
'The callback URL was missing the expected exchange code. This usually means the SSO flow was interrupted.',
|
|
1921
2849
|
exchange_failed:
|
|
1922
2850
|
"The exchange code couldn't be redeemed for a session. The code may have expired (they're single-use and short-lived) \u2014 try signing in again.",
|
|
2851
|
+
cookie_write_failed:
|
|
2852
|
+
'Sign-in completed, but the session cookie could not be written, so the session would have been lost. This means exchangeCode ran outside a Route Handler / Server Action context (Next can only set cookies there) or next/headers was unavailable \u2014 verify /api/auth/callback is a standard route handler and check the module server logs.',
|
|
1923
2853
|
};
|
|
1924
2854
|
|
|
1925
2855
|
interface SignInErrorPageProps {
|
|
@@ -2417,15 +3347,23 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2417
3347
|
}
|
|
2418
3348
|
const name = options.name ?? humanizeSlug(slug);
|
|
2419
3349
|
const dir = resolve(options.dir ?? slug);
|
|
2420
|
-
|
|
3350
|
+
const manifestExists = existsSync(join(dir, "module.json"));
|
|
3351
|
+
if (options.provisionOnly && !manifestExists) {
|
|
3352
|
+
fail(
|
|
3353
|
+
`Provision-only mode needs an existing ${code("module.json")} in ${code(dir)} \u2014 there is nothing to provision against. Run ${code(`dashwise init ${slug}`)} to scaffold first.`
|
|
3354
|
+
);
|
|
3355
|
+
return 1;
|
|
3356
|
+
}
|
|
3357
|
+
if (!options.provisionOnly && existsSync(dir)) {
|
|
2421
3358
|
const entries = readdirSync(dir);
|
|
2422
|
-
if (entries.length > 0 && !options.force) {
|
|
3359
|
+
if (entries.length > 0 && !options.force && !manifestExists) {
|
|
2423
3360
|
fail(
|
|
2424
3361
|
`Directory ${code(dir)} is not empty. Pass ${code("--force")} to scaffold anyway (existing files will be overwritten).`
|
|
2425
3362
|
);
|
|
2426
3363
|
return 1;
|
|
2427
3364
|
}
|
|
2428
3365
|
}
|
|
3366
|
+
const writeMode = options.provisionOnly ? "none" : manifestExists && !options.force ? "missing-only" : "all";
|
|
2429
3367
|
if (options.simple && options.custom) {
|
|
2430
3368
|
fail(
|
|
2431
3369
|
`Cannot pass both ${code("--simple")} and ${code("--custom")} \u2014 they're mutually exclusive. ${code("--simple")} selects the legacy minimal scaffold; the default (no flag) is the full Next.js scaffold (formerly ${code("--custom")}).`
|
|
@@ -2464,7 +3402,11 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2464
3402
|
// Example data-plane route (both flavors) — exercises the generated
|
|
2465
3403
|
// SDK against the seeded `items` table. Ships for simple + custom
|
|
2466
3404
|
// since both produce `@dashai/generated`.
|
|
2467
|
-
["app/api/items/route.ts", appApiItemsRouteTs()]
|
|
3405
|
+
["app/api/items/route.ts", appApiItemsRouteTs()],
|
|
3406
|
+
// C12/D5 (Wave 2) — commented-out seed-script template. Pairs with
|
|
3407
|
+
// `dashwise run -- node seed.mjs`; carries the publish-packs-
|
|
3408
|
+
// everything warning so it never ships inside a bundle by accident.
|
|
3409
|
+
["seed.mjs", seedMjs()]
|
|
2468
3410
|
];
|
|
2469
3411
|
if (kind === "custom") {
|
|
2470
3412
|
files.push(
|
|
@@ -2480,6 +3422,10 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2480
3422
|
["components/ui/card.tsx", componentsUiCardTsx()],
|
|
2481
3423
|
["app/layout.tsx", appLayoutCustomTsx(scaffoldOpts)],
|
|
2482
3424
|
["app/page.tsx", appPageCustomTsx(scaffoldOpts)],
|
|
3425
|
+
// C10 (Wave 2) — root error boundary: branded card, error.digest,
|
|
3426
|
+
// reset() retry, semantic tokens (which resolve now that
|
|
3427
|
+
// globals.css maps them in @theme).
|
|
3428
|
+
["app/error.tsx", appErrorTsx()],
|
|
2483
3429
|
["app/dashboard/page.tsx", appDashboardPageTsx()],
|
|
2484
3430
|
["app/sign-in-error/page.tsx", appSignInErrorPageTsx()],
|
|
2485
3431
|
["app/api/auth/sign-in/route.ts", appApiAuthSignInTs()],
|
|
@@ -2509,17 +3455,36 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2509
3455
|
break;
|
|
2510
3456
|
}
|
|
2511
3457
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
3458
|
+
let written = 0;
|
|
3459
|
+
let skipped = 0;
|
|
3460
|
+
if (writeMode !== "none") {
|
|
3461
|
+
for (const [relPath, content] of files) {
|
|
3462
|
+
const absPath = join(dir, relPath);
|
|
3463
|
+
if (writeMode === "missing-only" && existsSync(absPath)) {
|
|
3464
|
+
skipped++;
|
|
3465
|
+
continue;
|
|
3466
|
+
}
|
|
3467
|
+
const parentDir = join(absPath, "..");
|
|
3468
|
+
if (!existsSync(parentDir)) {
|
|
3469
|
+
mkdirSync(parentDir, { recursive: true });
|
|
3470
|
+
}
|
|
3471
|
+
writeFileSync(absPath, content, "utf-8");
|
|
3472
|
+
written++;
|
|
2517
3473
|
}
|
|
2518
|
-
writeFileSync(absPath, content, "utf-8");
|
|
2519
3474
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
3475
|
+
if (writeMode === "none") {
|
|
3476
|
+
info(
|
|
3477
|
+
`Existing scaffold in ${code(dir)} left untouched (provision-only) \u2014 re-provisioning the backend dev install.`
|
|
3478
|
+
);
|
|
3479
|
+
} else if (writeMode === "missing-only") {
|
|
3480
|
+
success(
|
|
3481
|
+
`Existing module project in ${code(dir)} \u2014 wrote ${written} missing file(s), preserved ${skipped} existing file(s). ${dim("(Pass --force to overwrite everything.)")}`
|
|
3482
|
+
);
|
|
3483
|
+
} else {
|
|
3484
|
+
success(
|
|
3485
|
+
`Scaffolded ${code(slug)} (${kind === "custom" ? "custom Next.js" : "simple module"}) in ${code(dir)}`
|
|
3486
|
+
);
|
|
3487
|
+
}
|
|
2523
3488
|
let devInstall = null;
|
|
2524
3489
|
const wantsProvision = options.provision !== false;
|
|
2525
3490
|
if (wantsProvision) {
|
|
@@ -2530,12 +3495,16 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2530
3495
|
return void 0;
|
|
2531
3496
|
}
|
|
2532
3497
|
})();
|
|
3498
|
+
const manifestKind = (() => {
|
|
3499
|
+
const mk = manifest?.module_kind;
|
|
3500
|
+
return mk === "ai_builder" || mk === "hand_authored" || mk === "custom" ? mk : void 0;
|
|
3501
|
+
})();
|
|
2533
3502
|
devInstall = await tryProvisionDevInstall({
|
|
2534
3503
|
slug,
|
|
2535
3504
|
name,
|
|
2536
3505
|
description: options.description,
|
|
2537
3506
|
workspaceFlag: options.workspace,
|
|
2538
|
-
moduleKind: kind,
|
|
3507
|
+
moduleKind: manifestKind ?? kind,
|
|
2539
3508
|
...manifest !== void 0 ? { manifest } : {}
|
|
2540
3509
|
});
|
|
2541
3510
|
} else {
|
|
@@ -2550,7 +3519,8 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2550
3519
|
);
|
|
2551
3520
|
info(
|
|
2552
3521
|
`Fix it by re-running provisioning once the cause above is resolved:
|
|
2553
|
-
${code(`dashwise init ${slug} --
|
|
3522
|
+
${code(`dashwise init ${slug} --workspace <your-workspace-slug>`)}
|
|
3523
|
+
(Existing files are preserved \u2014 a re-run only provisions + fills in missing files.)
|
|
2554
3524
|
Confirm success: the line ${code("\u2713 Provisioned dev install id=\u2026 API key cached")} must appear,
|
|
2555
3525
|
and ${code("cat ~/.config/dashwise/auth.json")} must show a non-empty ${code(`modules.${slug}.apiKey`)} starting with ${code("dwk_")}.`
|
|
2556
3526
|
);
|
|
@@ -2576,7 +3546,7 @@ ${devInstall ? `
|
|
|
2576
3546
|
async function tryProvisionDevInstall(args) {
|
|
2577
3547
|
if (!isLoggedIn()) {
|
|
2578
3548
|
warn(
|
|
2579
|
-
`Not logged in \u2014 skipping dev-install provisioning. Run ${code("dashwise login")} then ${code(`dashwise init ${args.slug}
|
|
3549
|
+
`Not logged in \u2014 skipping dev-install provisioning. Run ${code("dashwise login")} then ${code(`dashwise init ${args.slug}`)} to retry (existing files are preserved).`
|
|
2580
3550
|
);
|
|
2581
3551
|
return null;
|
|
2582
3552
|
}
|
|
@@ -2678,7 +3648,7 @@ function surfaceProvisionApiError(err, slug) {
|
|
|
2678
3648
|
switch (err.code) {
|
|
2679
3649
|
case "UNAUTHENTICATED":
|
|
2680
3650
|
warn(
|
|
2681
|
-
`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then ${code(`dashwise init ${slug}
|
|
3651
|
+
`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then ${code(`dashwise init ${slug}`)} (existing files are preserved).`
|
|
2682
3652
|
);
|
|
2683
3653
|
return;
|
|
2684
3654
|
case "FORBIDDEN":
|
|
@@ -2686,11 +3656,30 @@ function surfaceProvisionApiError(err, slug) {
|
|
|
2686
3656
|
`Not authorized to create a module in that workspace. Confirm you're a member at the DashWise dashboard.`
|
|
2687
3657
|
);
|
|
2688
3658
|
return;
|
|
2689
|
-
case "
|
|
3659
|
+
case "DEV_INSTALL_ALREADY_EXISTS": {
|
|
3660
|
+
const cached = readModuleEntry(slug);
|
|
2690
3661
|
warn(
|
|
2691
|
-
`
|
|
3662
|
+
`Module ${code(slug)} already has a live dev install${cached?.installationId ? ` (cached id=${cached.installationId})` : ""} \u2014 only ONE dev install per module can exist.
|
|
3663
|
+
Run ${code("dashwise module reset-dev-install")} for a clean slate (keeps ALL local files), or ${code("dashwise module destroy-dev-install")} then re-run init.`
|
|
2692
3664
|
);
|
|
2693
3665
|
return;
|
|
3666
|
+
}
|
|
3667
|
+
case "SLUG_ALREADY_EXISTS": {
|
|
3668
|
+
const cached = readModuleEntry(slug);
|
|
3669
|
+
if (cached) {
|
|
3670
|
+
warn(
|
|
3671
|
+
`A module with slug ${code(slug)} already exists, and you have its dev install cached (id=${cached.installationId}).
|
|
3672
|
+
For a clean slate run ${code("dashwise module reset-dev-install")} (keeps all local files), or ${code("dashwise module destroy-dev-install")} then re-run init.`
|
|
3673
|
+
);
|
|
3674
|
+
} else {
|
|
3675
|
+
warn(
|
|
3676
|
+
`A module with slug ${code(slug)} already exists.
|
|
3677
|
+
- If it belongs to ANOTHER workspace: module slugs are globally unique \u2014 pick a NEW slug (duplicate the code if needed).
|
|
3678
|
+
- If it's YOUR OWN published module: backends \u2265 2026-07-15 re-provision a dev install from this exact command (${code(`dashwise init ${slug} --workspace <ws>`)} \u2014 the dev install coexists with the production install), so this 409 means the backend predates that. Older-backend fallback (production repair): ${code("dashwise api-keys create local-dev --install <id> --full-access")} + ${code("dashwise origins add http://localhost:3000 --install <id>")} (id via ${code(`dashwise module status ${slug}`)}), then run bare ${code("next dev")} \u2014 and CLEAN UP after (revoke the key, remove the origin).`
|
|
3679
|
+
);
|
|
3680
|
+
}
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
2694
3683
|
case "RUNTIME_DATA_API_DISABLED":
|
|
2695
3684
|
warn(
|
|
2696
3685
|
`Backend has the local-dev API turned off (BUILDER_SESSION_TOKEN_SECRET unset). Contact the operator.`
|
|
@@ -2737,7 +3726,7 @@ function loadSchemaCommandContext(opts) {
|
|
|
2737
3726
|
const entry = readModuleEntry(slug);
|
|
2738
3727
|
if (!entry) {
|
|
2739
3728
|
throw new Error(
|
|
2740
|
-
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}
|
|
3729
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}\` to provision one (existing files are preserved).`
|
|
2741
3730
|
);
|
|
2742
3731
|
}
|
|
2743
3732
|
const token = resolveSchemaAuthToken(entry, slug);
|
|
@@ -2758,7 +3747,7 @@ function loadSchemaCommandContext(opts) {
|
|
|
2758
3747
|
function resolveSchemaAuthToken(entry, slug) {
|
|
2759
3748
|
if (entry.apiKey) return entry.apiKey;
|
|
2760
3749
|
throw new Error(
|
|
2761
|
-
`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}
|
|
3750
|
+
`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}\` to re-provision (existing files are preserved).`
|
|
2762
3751
|
);
|
|
2763
3752
|
}
|
|
2764
3753
|
var init_schema_command_context = __esm({
|
|
@@ -2767,1187 +3756,1013 @@ var init_schema_command_context = __esm({
|
|
|
2767
3756
|
init_manifest_io();
|
|
2768
3757
|
}
|
|
2769
3758
|
});
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
const
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
}
|
|
2787
|
-
slug = manifest?.module?.slug;
|
|
2788
|
-
if (!slug) {
|
|
2789
|
-
fail(
|
|
2790
|
-
`No module slug \u2014 pass ${code("--module <slug>")} or run from a directory with a valid module.json.`
|
|
2791
|
-
);
|
|
2792
|
-
return 1;
|
|
2793
|
-
}
|
|
2794
|
-
}
|
|
2795
|
-
const cfg = readConfig();
|
|
2796
|
-
if (!cfg) {
|
|
2797
|
-
fail("Not logged in. Run `dashwise login` first.");
|
|
2798
|
-
return 1;
|
|
2799
|
-
}
|
|
2800
|
-
const entry = readModuleEntry(slug);
|
|
2801
|
-
if (!entry) {
|
|
2802
|
-
warn(
|
|
2803
|
-
`No cached dev install for ${code(slug)} in auth.json. Nothing to do.`
|
|
2804
|
-
);
|
|
2805
|
-
return 0;
|
|
2806
|
-
}
|
|
2807
|
-
if (!options.yes && process.stdout.isTTY) {
|
|
2808
|
-
const choice = await confirm({
|
|
2809
|
-
message: `Destroy dev install ${code(`${slug} (id=${entry.installationId})`)}? Tables and rows will be soft-deleted (recoverable for 30 days via dashboard trash).`
|
|
2810
|
-
});
|
|
2811
|
-
if (isCancel(choice) || !choice) {
|
|
2812
|
-
info("Aborted.");
|
|
2813
|
-
return 130;
|
|
2814
|
-
}
|
|
2815
|
-
}
|
|
2816
|
-
let backendCleaned = false;
|
|
2817
|
-
try {
|
|
2818
|
-
const response = await destroyDevInstallation(entry.installationId, {
|
|
2819
|
-
baseUrl: resolveApiUrl(),
|
|
2820
|
-
auth: { token: cfg.token }
|
|
2821
|
-
});
|
|
2822
|
-
backendCleaned = true;
|
|
2823
|
-
success(
|
|
2824
|
-
`Destroyed dev install ${code(`${slug} (id=${response.installation_id})`)}${response.module_also_dropped ? " \u2014 module also dropped (no other installs, unpublished)." : " (module kept \u2014 other installs reference it or it has a published version)."}`
|
|
2825
|
-
);
|
|
2826
|
-
} catch (err) {
|
|
2827
|
-
if (err instanceof ApiError && err.code === "INSTALLATION_NOT_FOUND") {
|
|
2828
|
-
warn(
|
|
2829
|
-
`Install ${entry.installationId} not found on the backend \u2014 already destroyed elsewhere. Cleaning up local cache.`
|
|
2830
|
-
);
|
|
2831
|
-
backendCleaned = true;
|
|
2832
|
-
} else {
|
|
2833
|
-
return surfaceDestroyError(err);
|
|
2834
|
-
}
|
|
2835
|
-
}
|
|
2836
|
-
if (backendCleaned) {
|
|
2837
|
-
try {
|
|
2838
|
-
removeModuleEntry(slug);
|
|
2839
|
-
info(`Removed ${code(slug)} entry from auth.json.`);
|
|
2840
|
-
} catch (err) {
|
|
2841
|
-
warn(
|
|
2842
|
-
`Backend cleanup succeeded but failed to update auth.json: ${err.message}. Edit ${code("~/.config/dashwise/auth.json")} manually to remove the ${code(`modules["${slug}"]`)} entry.`
|
|
2843
|
-
);
|
|
2844
|
-
}
|
|
2845
|
-
}
|
|
2846
|
-
return 0;
|
|
2847
|
-
}
|
|
2848
|
-
function surfaceDestroyError(err) {
|
|
2849
|
-
if (err instanceof ApiError) {
|
|
2850
|
-
switch (err.code) {
|
|
2851
|
-
case "INSTALLATION_KIND_NOT_LOCAL":
|
|
2852
|
-
fail(
|
|
2853
|
-
`Refusing to destroy: the install isn't kind=local (it may have been promoted to production). Use the dashboard's uninstall flow instead.`
|
|
2854
|
-
);
|
|
2855
|
-
break;
|
|
2856
|
-
case "NOT_WORKSPACE_MEMBER":
|
|
2857
|
-
fail(
|
|
2858
|
-
`You're not a member of the install's workspace. Contact a workspace admin.`
|
|
2859
|
-
);
|
|
2860
|
-
break;
|
|
2861
|
-
case "UNAUTHENTICATED":
|
|
2862
|
-
fail(
|
|
2863
|
-
`Auth rejected by backend \u2014 your CLI bearer may be stale. Re-run ${code("dashwise login")} and try again.`
|
|
2864
|
-
);
|
|
2865
|
-
break;
|
|
2866
|
-
default:
|
|
2867
|
-
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
2868
|
-
}
|
|
2869
|
-
return 1;
|
|
2870
|
-
}
|
|
2871
|
-
fail(`Unexpected error: ${err.message}`);
|
|
2872
|
-
return 1;
|
|
2873
|
-
}
|
|
2874
|
-
var init_destroy_dev_install = __esm({
|
|
2875
|
-
"src/commands/module/destroy-dev-install.ts"() {
|
|
2876
|
-
init_api_client();
|
|
2877
|
-
init_config();
|
|
2878
|
-
init_manifest_io();
|
|
2879
|
-
init_output();
|
|
2880
|
-
}
|
|
2881
|
-
});
|
|
2882
|
-
|
|
2883
|
-
// src/commands/module/reset-dev-install.ts
|
|
2884
|
-
var reset_dev_install_exports = {};
|
|
2885
|
-
__export(reset_dev_install_exports, {
|
|
2886
|
-
resetDevInstallCommand: () => resetDevInstallCommand
|
|
2887
|
-
});
|
|
2888
|
-
async function resetDevInstallCommand(options) {
|
|
2889
|
-
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
2890
|
-
const manifest = (() => {
|
|
2891
|
-
try {
|
|
2892
|
-
return readManifest(projectRoot);
|
|
2893
|
-
} catch {
|
|
2894
|
-
return null;
|
|
2895
|
-
}
|
|
2896
|
-
})();
|
|
2897
|
-
const slug = options.module ?? manifest?.module?.slug;
|
|
2898
|
-
if (!slug) {
|
|
2899
|
-
fail(
|
|
2900
|
-
`No module slug \u2014 pass ${code("--module <slug>")} or run from a directory with a valid module.json.`
|
|
2901
|
-
);
|
|
2902
|
-
return 1;
|
|
2903
|
-
}
|
|
2904
|
-
const name = manifest?.module?.name ?? slug;
|
|
2905
|
-
info(`Resetting dev install for ${code(slug)} \u2014 phase 1/2: destroy\u2026`);
|
|
2906
|
-
const destroyExit = await destroyDevInstallCommand({
|
|
2907
|
-
...options.dir !== void 0 ? { dir: options.dir } : {},
|
|
2908
|
-
module: slug,
|
|
2909
|
-
...options.yes !== void 0 ? { yes: options.yes } : {}
|
|
2910
|
-
});
|
|
2911
|
-
if (destroyExit !== 0) {
|
|
2912
|
-
fail(
|
|
2913
|
-
`Destroy phase failed (exit ${destroyExit}); aborting reset. Fix the underlying error and re-run.`
|
|
2914
|
-
);
|
|
2915
|
-
return destroyExit;
|
|
2916
|
-
}
|
|
2917
|
-
info(`Reset phase 2/2: re-provisioning\u2026`);
|
|
2918
|
-
return moduleInitCommand(slug, {
|
|
2919
|
-
name,
|
|
2920
|
-
...options.dir !== void 0 ? { dir: options.dir } : { dir: projectRoot },
|
|
2921
|
-
// `--force` so init doesn't refuse to overwrite the existing
|
|
2922
|
-
// module.json (we're keeping the local scaffold; only the
|
|
2923
|
-
// backend install is being recreated).
|
|
2924
|
-
force: true,
|
|
2925
|
-
// Keep the current scaffold layout — don't try to detect or
|
|
2926
|
-
// change `simple` vs `custom`. The user can re-init manually
|
|
2927
|
-
// with `dashwise init` if they want to swap scaffolds.
|
|
2928
|
-
provision: true,
|
|
2929
|
-
...options.workspace !== void 0 ? { workspace: options.workspace } : {}
|
|
2930
|
-
});
|
|
2931
|
-
}
|
|
2932
|
-
var init_reset_dev_install = __esm({
|
|
2933
|
-
"src/commands/module/reset-dev-install.ts"() {
|
|
2934
|
-
init_destroy_dev_install();
|
|
2935
|
-
init_init();
|
|
2936
|
-
init_manifest_io();
|
|
2937
|
-
init_output();
|
|
2938
|
-
}
|
|
2939
|
-
});
|
|
2940
|
-
|
|
2941
|
-
// src/lib/field-spec.ts
|
|
2942
|
-
function parseFieldSpec(spec) {
|
|
2943
|
-
const parts = spec.split(":");
|
|
2944
|
-
if (parts.length < 2) {
|
|
2945
|
-
throw new FieldSpecError(
|
|
2946
|
-
`Field spec ${quote(spec)} is malformed \u2014 expected ${quote("<slug>:<type>[:flags]")}.`
|
|
2947
|
-
);
|
|
2948
|
-
}
|
|
2949
|
-
const [slug, type, ...flagParts] = parts;
|
|
2950
|
-
if (!SLUG_REGEX.test(slug)) {
|
|
2951
|
-
throw new FieldSpecError(
|
|
2952
|
-
`Field slug ${quote(slug)} must match ${quote(SLUG_REGEX.source)} (snake_case, 2+ chars).`
|
|
2953
|
-
);
|
|
2954
|
-
}
|
|
2955
|
-
if (!FIELD_TYPES.includes(type)) {
|
|
2956
|
-
throw new FieldSpecError(
|
|
2957
|
-
`Field type ${quote(type)} is not a known type. Allowed: ${FIELD_TYPES.join(", ")}.`
|
|
2958
|
-
);
|
|
2959
|
-
}
|
|
2960
|
-
const result = { slug, type };
|
|
2961
|
-
if (flagParts.length > 0) {
|
|
2962
|
-
const flags = flagParts.flatMap((segment) => segment.split(",")).map((f) => f.trim()).filter((f) => f.length > 0);
|
|
2963
|
-
for (const flag of flags) {
|
|
2964
|
-
if (!KNOWN_FLAGS.has(flag)) {
|
|
2965
|
-
throw new FieldSpecError(
|
|
2966
|
-
`Unknown flag ${quote(flag)} in field spec ${quote(spec)}. Known flags: ${[...KNOWN_FLAGS].join(", ")}. For options/target/default, use ${quote("--field-json")} instead.`
|
|
2967
|
-
);
|
|
2968
|
-
}
|
|
2969
|
-
if (flag === "required") result.required = true;
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
2972
|
-
if ((type === "single_select" || type === "multi_select") && result.options === void 0) {
|
|
2973
|
-
throw new FieldSpecError(
|
|
2974
|
-
`Type ${quote(type)} requires an ${quote("options")} list. Use ${quote("--field-json")} for this field \u2014 e.g. ${quote(`'{"slug":"${slug}","type":"${type}","options":["a","b"]}'`)}.`
|
|
2975
|
-
);
|
|
2976
|
-
}
|
|
2977
|
-
if (type === "link_row" && result.target === void 0) {
|
|
2978
|
-
throw new FieldSpecError(
|
|
2979
|
-
`Type ${quote("link_row")} requires a ${quote("target")}. Use ${quote("--field-json")}.`
|
|
2980
|
-
);
|
|
2981
|
-
}
|
|
2982
|
-
if (type === "formula" || type === "ai") {
|
|
2983
|
-
throw new FieldSpecError(
|
|
2984
|
-
`Type ${quote(type)} requires extra config (${quote("formula")} or ${quote("prompt")}). Use ${quote("--field-json")}.`
|
|
2985
|
-
);
|
|
2986
|
-
}
|
|
2987
|
-
return result;
|
|
2988
|
-
}
|
|
2989
|
-
function parseFieldJson(json) {
|
|
2990
|
-
let parsed;
|
|
2991
|
-
try {
|
|
2992
|
-
parsed = JSON.parse(json);
|
|
2993
|
-
} catch (err) {
|
|
2994
|
-
throw new FieldSpecError(
|
|
2995
|
-
`--field-json value is not valid JSON: ${err.message}`
|
|
2996
|
-
);
|
|
2997
|
-
}
|
|
2998
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2999
|
-
throw new FieldSpecError("--field-json must be a JSON object.");
|
|
3000
|
-
}
|
|
3001
|
-
const o = parsed;
|
|
3002
|
-
if (typeof o.slug !== "string" || !SLUG_REGEX.test(o.slug)) {
|
|
3003
|
-
throw new FieldSpecError(
|
|
3004
|
-
`--field-json missing or invalid ${quote("slug")}. Must match ${quote(SLUG_REGEX.source)}.`
|
|
3005
|
-
);
|
|
3006
|
-
}
|
|
3007
|
-
if (typeof o.type !== "string" || !FIELD_TYPES.includes(o.type)) {
|
|
3008
|
-
throw new FieldSpecError(
|
|
3009
|
-
`--field-json missing or invalid ${quote("type")}. Allowed: ${FIELD_TYPES.join(", ")}.`
|
|
3010
|
-
);
|
|
3759
|
+
function hydrateEnvFromDotenvLocal(projectRoot) {
|
|
3760
|
+
const envFile = join(projectRoot, ".env.local");
|
|
3761
|
+
if (!existsSync(envFile)) return;
|
|
3762
|
+
const raw = readFileSync(envFile, "utf-8");
|
|
3763
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
3764
|
+
const trimmed = line.trim();
|
|
3765
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
3766
|
+
const eq = trimmed.indexOf("=");
|
|
3767
|
+
if (eq <= 0) continue;
|
|
3768
|
+
const key = trimmed.slice(0, eq).trim();
|
|
3769
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
3770
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3771
|
+
value = value.slice(1, -1);
|
|
3772
|
+
}
|
|
3773
|
+
if (process.env[key] === void 0) {
|
|
3774
|
+
process.env[key] = value;
|
|
3775
|
+
}
|
|
3011
3776
|
}
|
|
3012
|
-
return o;
|
|
3013
|
-
}
|
|
3014
|
-
function quote(s) {
|
|
3015
|
-
return `\`${s}\``;
|
|
3016
3777
|
}
|
|
3017
|
-
var
|
|
3018
|
-
|
|
3019
|
-
"src/lib/field-spec.ts"() {
|
|
3020
|
-
FIELD_TYPES = [
|
|
3021
|
-
"text",
|
|
3022
|
-
"long_text",
|
|
3023
|
-
"number",
|
|
3024
|
-
"boolean",
|
|
3025
|
-
"date",
|
|
3026
|
-
"datetime",
|
|
3027
|
-
"single_select",
|
|
3028
|
-
"multi_select",
|
|
3029
|
-
"link_row",
|
|
3030
|
-
"file",
|
|
3031
|
-
"multi_file",
|
|
3032
|
-
"email",
|
|
3033
|
-
"url",
|
|
3034
|
-
"phone",
|
|
3035
|
-
"formula",
|
|
3036
|
-
"ai"
|
|
3037
|
-
];
|
|
3038
|
-
SLUG_REGEX = /^[a-z][a-z0-9_]*[a-z0-9]$/;
|
|
3039
|
-
KNOWN_FLAGS = /* @__PURE__ */ new Set(["required", "unique"]);
|
|
3040
|
-
FieldSpecError = class extends Error {
|
|
3041
|
-
constructor(message) {
|
|
3042
|
-
super(message);
|
|
3043
|
-
this.name = "FieldSpecError";
|
|
3044
|
-
}
|
|
3045
|
-
};
|
|
3778
|
+
var init_dotenv_local = __esm({
|
|
3779
|
+
"src/lib/dotenv-local.ts"() {
|
|
3046
3780
|
}
|
|
3047
3781
|
});
|
|
3048
3782
|
|
|
3049
|
-
// src/commands/
|
|
3050
|
-
var
|
|
3051
|
-
__export(
|
|
3052
|
-
|
|
3053
|
-
syncLocalManifest: () => syncLocalManifest,
|
|
3054
|
-
tableCreateCommand: () => tableCreateCommand
|
|
3783
|
+
// src/commands/run.ts
|
|
3784
|
+
var run_exports = {};
|
|
3785
|
+
__export(run_exports, {
|
|
3786
|
+
runCommand: () => runCommand
|
|
3055
3787
|
});
|
|
3056
|
-
async function
|
|
3057
|
-
if (!
|
|
3058
|
-
fail(
|
|
3788
|
+
async function runCommand(cmdArgs, options) {
|
|
3789
|
+
if (!cmdArgs || cmdArgs.length === 0) {
|
|
3790
|
+
fail(
|
|
3791
|
+
`Usage: ${code("dashwise run -- <command\u2026>")} (e.g. ${code("dashwise run -- node seed.mjs")})`
|
|
3792
|
+
);
|
|
3059
3793
|
return 1;
|
|
3060
3794
|
}
|
|
3061
|
-
const
|
|
3795
|
+
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
3796
|
+
let manifestSlug;
|
|
3062
3797
|
try {
|
|
3063
|
-
|
|
3064
|
-
fields.push(parseFieldSpec(spec));
|
|
3065
|
-
}
|
|
3066
|
-
for (const json of options.fieldJson ?? []) {
|
|
3067
|
-
fields.push(parseFieldJson(json));
|
|
3068
|
-
}
|
|
3798
|
+
manifestSlug = readManifestOrThrow(projectRoot).module?.slug;
|
|
3069
3799
|
} catch (err) {
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3800
|
+
fail(err.message);
|
|
3801
|
+
return 1;
|
|
3802
|
+
}
|
|
3803
|
+
if (!manifestSlug) {
|
|
3804
|
+
fail(`module.json has no ${code("module.slug")} \u2014 the manifest is malformed.`);
|
|
3805
|
+
return 1;
|
|
3806
|
+
}
|
|
3807
|
+
hydrateEnvFromDotenvLocal(projectRoot);
|
|
3808
|
+
const cfg = (() => {
|
|
3809
|
+
try {
|
|
3810
|
+
return readConfig();
|
|
3811
|
+
} catch {
|
|
3812
|
+
return null;
|
|
3073
3813
|
}
|
|
3074
|
-
|
|
3814
|
+
})();
|
|
3815
|
+
const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
|
|
3816
|
+
let apiKey = process.env.DASHWISE_API_KEY;
|
|
3817
|
+
if (!apiKey) {
|
|
3818
|
+
let entry = readModuleEntry(manifestSlug);
|
|
3819
|
+
if (cfg) {
|
|
3820
|
+
const healed = await resolveModuleApiKey(
|
|
3821
|
+
manifestSlug,
|
|
3822
|
+
entry,
|
|
3823
|
+
apiUrl,
|
|
3824
|
+
cfg.token
|
|
3825
|
+
);
|
|
3826
|
+
if (healed) entry = healed;
|
|
3827
|
+
}
|
|
3828
|
+
apiKey = entry?.apiKey;
|
|
3075
3829
|
}
|
|
3076
|
-
if (
|
|
3830
|
+
if (!apiKey) {
|
|
3077
3831
|
fail(
|
|
3078
|
-
`
|
|
3832
|
+
`No ${code("dwk_")} API key for module ${code(manifestSlug)} \u2014 the command would run with NO token.`
|
|
3833
|
+
);
|
|
3834
|
+
info(
|
|
3835
|
+
`Provision a dev install first (${code(`dashwise init ${manifestSlug} --workspace <your-workspace-slug>`)}), or set ${code("DASHWISE_API_KEY")} explicitly.`
|
|
3079
3836
|
);
|
|
3080
3837
|
return 1;
|
|
3081
3838
|
}
|
|
3082
|
-
const
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3839
|
+
const [cmd, ...rest] = cmdArgs;
|
|
3840
|
+
info(
|
|
3841
|
+
`${dim("run")} ${cmd} ${rest.join(" ")} ${dim(`(DASHWISE_API_URL=${apiUrl}, DASHWISE_API_KEY injected)`)}`
|
|
3842
|
+
);
|
|
3843
|
+
const child = spawn(cmd, rest, {
|
|
3844
|
+
cwd: projectRoot,
|
|
3845
|
+
stdio: "inherit",
|
|
3846
|
+
env: {
|
|
3847
|
+
...process.env,
|
|
3848
|
+
DASHWISE_API_URL: apiUrl,
|
|
3849
|
+
DASHWISE_API_KEY: apiKey
|
|
3850
|
+
},
|
|
3851
|
+
// Windows needs shell:true for .cmd shims (npm / npx).
|
|
3852
|
+
shell: process.platform === "win32"
|
|
3853
|
+
});
|
|
3854
|
+
return new Promise((resolveExit) => {
|
|
3855
|
+
child.on("error", (err) => {
|
|
3856
|
+
fail(`Could not spawn ${code(cmd)}: ${err.message}`);
|
|
3857
|
+
resolveExit(1);
|
|
3858
|
+
});
|
|
3859
|
+
child.on("exit", (exitCode, signal) => {
|
|
3860
|
+
resolveExit(exitCode ?? (signal !== null ? 1 : 0));
|
|
3861
|
+
});
|
|
3862
|
+
});
|
|
3863
|
+
}
|
|
3864
|
+
var init_run = __esm({
|
|
3865
|
+
"src/commands/run.ts"() {
|
|
3866
|
+
init_config();
|
|
3867
|
+
init_dotenv_local();
|
|
3868
|
+
init_manifest_io();
|
|
3869
|
+
init_output();
|
|
3870
|
+
init_dev();
|
|
3871
|
+
}
|
|
3872
|
+
});
|
|
3873
|
+
|
|
3874
|
+
// src/commands/module/destroy-dev-install.ts
|
|
3875
|
+
var destroy_dev_install_exports = {};
|
|
3876
|
+
__export(destroy_dev_install_exports, {
|
|
3877
|
+
destroyDevInstallCommand: () => destroyDevInstallCommand
|
|
3878
|
+
});
|
|
3879
|
+
async function destroyDevInstallCommand(options) {
|
|
3880
|
+
let slug = options.module;
|
|
3881
|
+
if (!slug) {
|
|
3882
|
+
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
3883
|
+
const manifest = (() => {
|
|
3884
|
+
try {
|
|
3885
|
+
return readManifest(projectRoot);
|
|
3886
|
+
} catch {
|
|
3887
|
+
return null;
|
|
3888
|
+
}
|
|
3889
|
+
})();
|
|
3890
|
+
slug = manifest?.module?.slug;
|
|
3891
|
+
if (!slug) {
|
|
3892
|
+
fail(
|
|
3893
|
+
`No module slug \u2014 pass ${code("--module <slug>")} or run from a directory with a valid module.json.`
|
|
3894
|
+
);
|
|
3086
3895
|
return 1;
|
|
3087
3896
|
}
|
|
3088
|
-
seen.add(f.slug);
|
|
3089
3897
|
}
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
3094
|
-
});
|
|
3095
|
-
} catch (err) {
|
|
3096
|
-
fail(err.message);
|
|
3898
|
+
const cfg = readConfig();
|
|
3899
|
+
if (!cfg) {
|
|
3900
|
+
fail("Not logged in. Run `dashwise login` first.");
|
|
3097
3901
|
return 1;
|
|
3098
3902
|
}
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
{
|
|
3103
|
-
ctx.requestOptions
|
|
3104
|
-
);
|
|
3105
|
-
success(
|
|
3106
|
-
`Created table ${code(slug)} (physical_table_id=${result.physical_table_id}, manifest_version=${result.manifest_version})`
|
|
3903
|
+
const entry = readModuleEntry(slug);
|
|
3904
|
+
if (!entry) {
|
|
3905
|
+
warn(
|
|
3906
|
+
`No cached dev install for ${code(slug)} in auth.json. Nothing to do.`
|
|
3107
3907
|
);
|
|
3108
|
-
|
|
3109
|
-
return surfaceFailure(err);
|
|
3908
|
+
return 0;
|
|
3110
3909
|
}
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3910
|
+
if (!options.yes && process.stdout.isTTY) {
|
|
3911
|
+
const choice = await confirm({
|
|
3912
|
+
message: `Destroy dev install ${code(`${slug} (id=${entry.installationId})`)}? Tables and rows will be soft-deleted (recoverable for 30 days via dashboard trash).`
|
|
3913
|
+
});
|
|
3914
|
+
if (isCancel(choice) || !choice) {
|
|
3915
|
+
info("Aborted.");
|
|
3916
|
+
return 130;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
let backendCleaned = false;
|
|
3114
3920
|
try {
|
|
3115
|
-
const
|
|
3116
|
-
|
|
3117
|
-
|
|
3921
|
+
const response = await destroyDevInstallation(entry.installationId, {
|
|
3922
|
+
baseUrl: resolveApiUrl(),
|
|
3923
|
+
auth: { token: cfg.token }
|
|
3924
|
+
});
|
|
3925
|
+
backendCleaned = true;
|
|
3926
|
+
success(
|
|
3927
|
+
`Destroyed dev install ${code(`${slug} (id=${response.installation_id})`)}${response.module_also_dropped ? " \u2014 module also dropped (no other installs, unpublished)." : " (module kept \u2014 other installs reference it or it has a published version)."}`
|
|
3928
|
+
);
|
|
3118
3929
|
} catch (err) {
|
|
3119
|
-
if (err instanceof ApiError) {
|
|
3930
|
+
if (err instanceof ApiError && err.code === "INSTALLATION_NOT_FOUND") {
|
|
3120
3931
|
warn(
|
|
3121
|
-
`
|
|
3932
|
+
`Install ${entry.installationId} not found on the backend \u2014 already destroyed elsewhere. Cleaning up local cache.`
|
|
3122
3933
|
);
|
|
3934
|
+
backendCleaned = true;
|
|
3123
3935
|
} else {
|
|
3936
|
+
return surfaceDestroyError(err);
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
if (backendCleaned) {
|
|
3940
|
+
try {
|
|
3941
|
+
removeModuleEntry(slug);
|
|
3942
|
+
info(`Removed ${code(slug)} entry from auth.json.`);
|
|
3943
|
+
} catch (err) {
|
|
3124
3944
|
warn(
|
|
3125
|
-
`
|
|
3945
|
+
`Backend cleanup succeeded but failed to update auth.json: ${err.message}. Edit ${code("~/.config/dashwise/auth.json")} manually to remove the ${code(`modules["${slug}"]`)} entry.`
|
|
3126
3946
|
);
|
|
3127
3947
|
}
|
|
3128
|
-
return 0;
|
|
3129
3948
|
}
|
|
3949
|
+
return 0;
|
|
3130
3950
|
}
|
|
3131
|
-
function
|
|
3951
|
+
function surfaceDestroyError(err) {
|
|
3132
3952
|
if (err instanceof ApiError) {
|
|
3133
3953
|
switch (err.code) {
|
|
3134
|
-
case "
|
|
3135
|
-
fail(`A table with this slug already exists in the manifest.`);
|
|
3136
|
-
break;
|
|
3137
|
-
case "FIELD_SLUG_EXISTS":
|
|
3138
|
-
fail(`A field with this slug already exists on the table.`);
|
|
3139
|
-
break;
|
|
3140
|
-
case "TABLE_NOT_FOUND":
|
|
3141
|
-
fail(`Table not found in the manifest.`);
|
|
3142
|
-
break;
|
|
3143
|
-
case "FIELD_NOT_FOUND":
|
|
3144
|
-
fail(`Field not found on the table.`);
|
|
3145
|
-
break;
|
|
3146
|
-
case "UNAUTHENTICATED":
|
|
3954
|
+
case "INSTALLATION_KIND_NOT_LOCAL":
|
|
3147
3955
|
fail(
|
|
3148
|
-
`
|
|
3956
|
+
`Refusing to destroy: the install isn't kind=local (it may have been promoted to production). Use the dashboard's uninstall flow instead.`
|
|
3149
3957
|
);
|
|
3150
3958
|
break;
|
|
3151
|
-
case "
|
|
3959
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
3152
3960
|
fail(
|
|
3153
|
-
`
|
|
3961
|
+
`You're not a member of the install's workspace. Contact a workspace admin.`
|
|
3154
3962
|
);
|
|
3155
3963
|
break;
|
|
3156
|
-
case "
|
|
3964
|
+
case "UNAUTHENTICATED":
|
|
3157
3965
|
fail(
|
|
3158
|
-
`
|
|
3966
|
+
`Auth rejected by backend \u2014 your CLI bearer may be stale. Re-run ${code("dashwise login")} and try again.`
|
|
3159
3967
|
);
|
|
3160
3968
|
break;
|
|
3161
3969
|
default:
|
|
3162
|
-
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
3163
|
-
}
|
|
3164
|
-
return 1;
|
|
3165
|
-
}
|
|
3166
|
-
fail(`Unexpected error: ${err.message}`);
|
|
3167
|
-
return 1;
|
|
3168
|
-
}
|
|
3169
|
-
var init_create = __esm({
|
|
3170
|
-
"src/commands/table/create.ts"() {
|
|
3171
|
-
init_manifest_io();
|
|
3172
|
-
init_api_client();
|
|
3173
|
-
init_schema_command_context();
|
|
3174
|
-
init_field_spec();
|
|
3175
|
-
init_output();
|
|
3176
|
-
}
|
|
3177
|
-
});
|
|
3178
|
-
|
|
3179
|
-
// src/commands/table/delete.ts
|
|
3180
|
-
var delete_exports = {};
|
|
3181
|
-
__export(delete_exports, {
|
|
3182
|
-
tableDeleteCommand: () => tableDeleteCommand
|
|
3183
|
-
});
|
|
3184
|
-
async function tableDeleteCommand(slug, options) {
|
|
3185
|
-
if (!slug) {
|
|
3186
|
-
fail("Usage: `dashwise table delete <slug>`");
|
|
3187
|
-
return 1;
|
|
3188
|
-
}
|
|
3189
|
-
let ctx;
|
|
3190
|
-
try {
|
|
3191
|
-
ctx = loadSchemaCommandContext({
|
|
3192
|
-
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
3193
|
-
});
|
|
3194
|
-
} catch (err) {
|
|
3195
|
-
fail(err.message);
|
|
3196
|
-
return 1;
|
|
3197
|
-
}
|
|
3198
|
-
info(`Dropping table ${code(slug)} from installation ${ctx.entry.installationId}\u2026`);
|
|
3199
|
-
try {
|
|
3200
|
-
const result = await dropTable(slug, ctx.requestOptions);
|
|
3201
|
-
success(
|
|
3202
|
-
`Dropped table ${code(slug)} (manifest_version=${result.manifest_version}). Physical table soft-deleted; recoverable for 30 days via the dashboard's trash.`
|
|
3203
|
-
);
|
|
3204
|
-
} catch (err) {
|
|
3205
|
-
return surfaceFailure(err);
|
|
3970
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
3971
|
+
}
|
|
3972
|
+
return 1;
|
|
3206
3973
|
}
|
|
3207
|
-
|
|
3974
|
+
fail(`Unexpected error: ${err.message}`);
|
|
3975
|
+
return 1;
|
|
3208
3976
|
}
|
|
3209
|
-
var
|
|
3210
|
-
"src/commands/
|
|
3977
|
+
var init_destroy_dev_install = __esm({
|
|
3978
|
+
"src/commands/module/destroy-dev-install.ts"() {
|
|
3211
3979
|
init_api_client();
|
|
3212
|
-
|
|
3980
|
+
init_config();
|
|
3981
|
+
init_manifest_io();
|
|
3213
3982
|
init_output();
|
|
3214
|
-
init_create();
|
|
3215
3983
|
}
|
|
3216
3984
|
});
|
|
3217
3985
|
|
|
3218
|
-
// src/commands/
|
|
3219
|
-
var
|
|
3220
|
-
__export(
|
|
3221
|
-
|
|
3986
|
+
// src/commands/module/reset-dev-install.ts
|
|
3987
|
+
var reset_dev_install_exports = {};
|
|
3988
|
+
__export(reset_dev_install_exports, {
|
|
3989
|
+
resetDevInstallCommand: () => resetDevInstallCommand
|
|
3222
3990
|
});
|
|
3223
|
-
async function
|
|
3224
|
-
|
|
3991
|
+
async function resetDevInstallCommand(options) {
|
|
3992
|
+
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
3993
|
+
const manifest = (() => {
|
|
3994
|
+
try {
|
|
3995
|
+
return readManifest(projectRoot);
|
|
3996
|
+
} catch {
|
|
3997
|
+
return null;
|
|
3998
|
+
}
|
|
3999
|
+
})();
|
|
4000
|
+
const slug = options.module ?? manifest?.module?.slug;
|
|
4001
|
+
if (!slug) {
|
|
3225
4002
|
fail(
|
|
3226
|
-
|
|
4003
|
+
`No module slug \u2014 pass ${code("--module <slug>")} or run from a directory with a valid module.json.`
|
|
3227
4004
|
);
|
|
3228
4005
|
return 1;
|
|
3229
4006
|
}
|
|
3230
|
-
const
|
|
3231
|
-
|
|
3232
|
-
|
|
4007
|
+
const name = manifest?.module?.name ?? slug;
|
|
4008
|
+
info(`Resetting dev install for ${code(slug)} \u2014 phase 1/2: destroy\u2026`);
|
|
4009
|
+
const destroyExit = await destroyDevInstallCommand({
|
|
4010
|
+
...options.dir !== void 0 ? { dir: options.dir } : {},
|
|
4011
|
+
module: slug,
|
|
4012
|
+
...options.yes !== void 0 ? { yes: options.yes } : {}
|
|
4013
|
+
});
|
|
4014
|
+
if (destroyExit !== 0) {
|
|
3233
4015
|
fail(
|
|
3234
|
-
`
|
|
4016
|
+
`Destroy phase failed (exit ${destroyExit}); aborting reset. Fix the underlying error and re-run.`
|
|
3235
4017
|
);
|
|
4018
|
+
return destroyExit;
|
|
4019
|
+
}
|
|
4020
|
+
info(`Reset phase 2/2: re-provisioning\u2026`);
|
|
4021
|
+
return moduleInitCommand(slug, {
|
|
4022
|
+
name,
|
|
4023
|
+
...options.dir !== void 0 ? { dir: options.dir } : { dir: projectRoot },
|
|
4024
|
+
// Provision-only: write NO files — every local file (module.json
|
|
4025
|
+
// included) stays exactly as-is; only the backend dev install +
|
|
4026
|
+
// the auth.json credentials entry are recreated. This is what the
|
|
4027
|
+
// command's "keeps local module.json intact" contract always
|
|
4028
|
+
// promised (pre-fix `force: true` re-scaffolded over user code).
|
|
4029
|
+
provisionOnly: true,
|
|
4030
|
+
provision: true,
|
|
4031
|
+
...options.workspace !== void 0 ? { workspace: options.workspace } : {}
|
|
4032
|
+
});
|
|
4033
|
+
}
|
|
4034
|
+
var init_reset_dev_install = __esm({
|
|
4035
|
+
"src/commands/module/reset-dev-install.ts"() {
|
|
4036
|
+
init_destroy_dev_install();
|
|
4037
|
+
init_init();
|
|
4038
|
+
init_manifest_io();
|
|
4039
|
+
init_output();
|
|
4040
|
+
}
|
|
4041
|
+
});
|
|
4042
|
+
|
|
4043
|
+
// src/commands/schema/push.ts
|
|
4044
|
+
var push_exports = {};
|
|
4045
|
+
__export(push_exports, {
|
|
4046
|
+
schemaPushCommand: () => schemaPushCommand
|
|
4047
|
+
});
|
|
4048
|
+
async function schemaPushCommand(options) {
|
|
4049
|
+
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
4050
|
+
let manifest;
|
|
4051
|
+
try {
|
|
4052
|
+
manifest = readManifestOrThrow(projectRoot);
|
|
4053
|
+
} catch (err) {
|
|
4054
|
+
fail(err.message);
|
|
3236
4055
|
return 1;
|
|
3237
4056
|
}
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
);
|
|
4057
|
+
const slug = manifest.module?.slug;
|
|
4058
|
+
if (!slug) {
|
|
4059
|
+
fail(`module.json has no ${code("module.slug")} \u2014 the manifest is malformed.`);
|
|
3242
4060
|
return 1;
|
|
3243
4061
|
}
|
|
3244
|
-
|
|
4062
|
+
const cfg = readConfig();
|
|
4063
|
+
if (!cfg) {
|
|
4064
|
+
fail(`Not logged in. Run ${code("dashwise login")} first.`);
|
|
4065
|
+
return 1;
|
|
4066
|
+
}
|
|
4067
|
+
const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
|
|
4068
|
+
let moduleId;
|
|
3245
4069
|
try {
|
|
3246
|
-
|
|
4070
|
+
moduleId = (await getModuleBySlug(slug, auth)).id;
|
|
3247
4071
|
} catch (err) {
|
|
3248
|
-
if (err instanceof
|
|
3249
|
-
fail(
|
|
4072
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
4073
|
+
fail(
|
|
4074
|
+
`Module ${code(slug)} isn't registered on the backend yet. Run ${code("dashwise init")} (or ${code("dashwise module register")}) first.`
|
|
4075
|
+
);
|
|
3250
4076
|
return 1;
|
|
3251
4077
|
}
|
|
3252
|
-
|
|
3253
|
-
}
|
|
3254
|
-
let ctx;
|
|
3255
|
-
try {
|
|
3256
|
-
ctx = loadSchemaCommandContext({
|
|
3257
|
-
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
3258
|
-
});
|
|
3259
|
-
} catch (err) {
|
|
3260
|
-
fail(err.message);
|
|
4078
|
+
fail(`Could not resolve module ${code(slug)}: ${err.message}`);
|
|
3261
4079
|
return 1;
|
|
3262
4080
|
}
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
4081
|
+
const entry = readModuleEntry(slug);
|
|
4082
|
+
const dwkAuth = entry?.apiKey ? { baseUrl: resolveApiUrl(), auth: { token: entry.apiKey } } : null;
|
|
4083
|
+
let remote = null;
|
|
4084
|
+
let previewUnavailableReason = null;
|
|
4085
|
+
if (!dwkAuth) {
|
|
4086
|
+
previewUnavailableReason = `no dev-install ${code("dwk_")} key is cached for ${code(slug)} in auth.json`;
|
|
4087
|
+
} else {
|
|
4088
|
+
try {
|
|
4089
|
+
remote = (await getInstallationSchema(dwkAuth)).manifest;
|
|
4090
|
+
} catch (err) {
|
|
4091
|
+
const detail = err instanceof ApiError ? `${err.code} ${err.status}: ${err.message}` : err.message;
|
|
4092
|
+
previewUnavailableReason = `reading the dev-install manifest failed (${detail}) \u2014 the cached key may be expired or revoked`;
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
if (remote) {
|
|
4096
|
+
const delta = computeDestructiveDelta(
|
|
4097
|
+
manifest,
|
|
4098
|
+
remote
|
|
3270
4099
|
);
|
|
3271
|
-
|
|
3272
|
-
`
|
|
4100
|
+
if (delta.tablesRemoved.length > 0 || delta.fieldsRemoved.length > 0) {
|
|
4101
|
+
warn(`This push REMOVES schema from the dev install (live DDL):`);
|
|
4102
|
+
for (const t of delta.tablesRemoved) {
|
|
4103
|
+
out(` - table ${code(t)} ${dim("(soft-deleted; rows recoverable for 30 days via trash)")}`);
|
|
4104
|
+
}
|
|
4105
|
+
for (const f of delta.fieldsRemoved) {
|
|
4106
|
+
out(` - field ${code(f)} ${dim("(column dropped)")}`);
|
|
4107
|
+
}
|
|
4108
|
+
if (!options.yes) {
|
|
4109
|
+
if (!process.stdout.isTTY) {
|
|
4110
|
+
fail(
|
|
4111
|
+
`Refusing the destructive push without confirmation. Re-run with ${code("--yes")} to apply the removals.`
|
|
4112
|
+
);
|
|
4113
|
+
return 1;
|
|
4114
|
+
}
|
|
4115
|
+
const answer = await select({
|
|
4116
|
+
message: `Apply these removals to the dev install?`,
|
|
4117
|
+
options: [
|
|
4118
|
+
{ value: "apply", label: "Apply \u2014 remove the tables/fields listed above" },
|
|
4119
|
+
{ value: "cancel", label: "Cancel \u2014 nothing is changed" }
|
|
4120
|
+
],
|
|
4121
|
+
initialValue: "cancel"
|
|
4122
|
+
});
|
|
4123
|
+
if (isCancel(answer) || answer === "cancel") {
|
|
4124
|
+
fail("Aborted. Nothing was changed.");
|
|
4125
|
+
return 130;
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
} else {
|
|
4130
|
+
warn(
|
|
4131
|
+
`Cannot preview what this push would change: ${previewUnavailableReason}.
|
|
4132
|
+
Without the preview there is no way to know whether the push REMOVES tables/fields (live DDL on the dev install).`
|
|
4133
|
+
);
|
|
4134
|
+
if (!options.yes) {
|
|
4135
|
+
fail(
|
|
4136
|
+
`Refusing to push without the destructive-change preview. Re-run with ${code("--yes")} to push anyway (the backend still validates the apply).`
|
|
4137
|
+
);
|
|
4138
|
+
return 1;
|
|
4139
|
+
}
|
|
4140
|
+
info(
|
|
4141
|
+
dim(
|
|
4142
|
+
`--yes passed \u2014 pushing WITHOUT the destructive-change preview.`
|
|
4143
|
+
)
|
|
4144
|
+
);
|
|
4145
|
+
}
|
|
4146
|
+
info(`Pushing module.json to the dev install of ${code(slug)} (module id=${moduleId})\u2026`);
|
|
4147
|
+
let applied;
|
|
4148
|
+
try {
|
|
4149
|
+
applied = await applyDraftManifest(
|
|
4150
|
+
moduleId,
|
|
4151
|
+
manifest,
|
|
4152
|
+
auth
|
|
3273
4153
|
);
|
|
3274
4154
|
} catch (err) {
|
|
3275
|
-
return
|
|
4155
|
+
return surfaceSchemaPushError(err, slug);
|
|
3276
4156
|
}
|
|
3277
|
-
|
|
4157
|
+
printApplySummary(applied);
|
|
4158
|
+
if (dwkAuth) {
|
|
4159
|
+
try {
|
|
4160
|
+
const { manifest: canonical } = await getInstallationSchema(dwkAuth);
|
|
4161
|
+
const localNow = (() => {
|
|
4162
|
+
try {
|
|
4163
|
+
return readManifest(projectRoot);
|
|
4164
|
+
} catch {
|
|
4165
|
+
return null;
|
|
4166
|
+
}
|
|
4167
|
+
})();
|
|
4168
|
+
const { merged, preserved } = mergeRemoteManifest(localNow, canonical);
|
|
4169
|
+
writeManifest(merged, projectRoot);
|
|
4170
|
+
if (preserved.length > 0) {
|
|
4171
|
+
info(
|
|
4172
|
+
dim(
|
|
4173
|
+
`[sync] kept local-only manifest blocks the backend does not echo (${preserved.join(", ")}) \u2014 they were part of this push and stay in module.json.`
|
|
4174
|
+
)
|
|
4175
|
+
);
|
|
4176
|
+
}
|
|
4177
|
+
} catch (err) {
|
|
4178
|
+
warn(
|
|
4179
|
+
`Push succeeded but the local sync-back failed: ${err.message}. Local module.json is what you pushed; the next sync reconciles.`
|
|
4180
|
+
);
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
return 0;
|
|
3278
4184
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
4185
|
+
function printApplySummary(applied) {
|
|
4186
|
+
const created = asArray(applied.tables_created);
|
|
4187
|
+
const updated = asArray(applied.tables_updated);
|
|
4188
|
+
const removed = asArray(applied.tables_removed);
|
|
4189
|
+
success(`Manifest applied to the dev install.`);
|
|
4190
|
+
if (created.length === 0 && updated.length === 0 && removed.length === 0) {
|
|
4191
|
+
info(dim("No table changes (metadata-only or already in sync)."));
|
|
4192
|
+
return;
|
|
3286
4193
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
__export(remove_exports, {
|
|
3292
|
-
fieldRemoveCommand: () => fieldRemoveCommand
|
|
3293
|
-
});
|
|
3294
|
-
async function fieldRemoveCommand(tableSlug, fieldSlug, options) {
|
|
3295
|
-
if (!tableSlug || !fieldSlug) {
|
|
3296
|
-
fail("Usage: `dashwise field remove <table-slug> <field-slug>`");
|
|
3297
|
-
return 1;
|
|
4194
|
+
for (const t of created) {
|
|
4195
|
+
const slug = strProp(t, "slug") ?? "(table)";
|
|
4196
|
+
const fields = asArray(t.fields_created);
|
|
4197
|
+
out(` + table ${code(slug)}${fields.length > 0 ? dim(` (${fields.length} field${fields.length === 1 ? "" : "s"})`) : ""}`);
|
|
3298
4198
|
}
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
4199
|
+
for (const t of updated) {
|
|
4200
|
+
const slug = strProp(t, "slug") ?? "(table)";
|
|
4201
|
+
const added = asArray(t.fields_added);
|
|
4202
|
+
const dropped = asArray(t.fields_removed);
|
|
4203
|
+
const bits = [];
|
|
4204
|
+
if (added.length > 0) bits.push(`+${added.length} field${added.length === 1 ? "" : "s"}`);
|
|
4205
|
+
if (dropped.length > 0) bits.push(`-${dropped.length} field${dropped.length === 1 ? "" : "s"}`);
|
|
4206
|
+
out(` ~ table ${code(slug)}${bits.length > 0 ? dim(` (${bits.join(", ")})`) : ""}`);
|
|
3307
4207
|
}
|
|
3308
|
-
|
|
3309
|
-
`
|
|
3310
|
-
);
|
|
3311
|
-
try {
|
|
3312
|
-
const result = await removeField(
|
|
3313
|
-
tableSlug,
|
|
3314
|
-
fieldSlug,
|
|
3315
|
-
ctx.requestOptions
|
|
3316
|
-
);
|
|
3317
|
-
success(
|
|
3318
|
-
`Removed field ${code(`${tableSlug}.${fieldSlug}`)} (manifest_version=${result.manifest_version})`
|
|
3319
|
-
);
|
|
3320
|
-
} catch (err) {
|
|
3321
|
-
return surfaceFailure(err);
|
|
4208
|
+
for (const t of removed) {
|
|
4209
|
+
out(` - table ${code(typeof t === "string" ? t : JSON.stringify(t))}`);
|
|
3322
4210
|
}
|
|
3323
|
-
return syncLocalManifest(ctx);
|
|
3324
4211
|
}
|
|
3325
|
-
|
|
3326
|
-
|
|
4212
|
+
function asArray(v) {
|
|
4213
|
+
return Array.isArray(v) ? v : [];
|
|
4214
|
+
}
|
|
4215
|
+
function strProp(v, key) {
|
|
4216
|
+
if (v && typeof v === "object" && typeof v[key] === "string") {
|
|
4217
|
+
return v[key];
|
|
4218
|
+
}
|
|
4219
|
+
return void 0;
|
|
4220
|
+
}
|
|
4221
|
+
function surfaceSchemaPushError(err, slug) {
|
|
4222
|
+
if (err instanceof ApiError) {
|
|
4223
|
+
switch (err.code) {
|
|
4224
|
+
case "MANIFEST_INVALID":
|
|
4225
|
+
fail(`Manifest rejected by the backend validator: ${err.message}`);
|
|
4226
|
+
for (const issue of extractIssues2(err.context)) {
|
|
4227
|
+
out(` ${dim("-")} ${issue}`);
|
|
4228
|
+
}
|
|
4229
|
+
return 1;
|
|
4230
|
+
case "DRAFT_INSTALLATION_MISSING":
|
|
4231
|
+
fail(
|
|
4232
|
+
`Module ${code(slug)} has no dev (local/sandbox) install in its owner workspace \u2014 production installs have NO draft plane, so there is nothing to push onto.
|
|
4233
|
+
This happens after ${code("dashwise module install --replace-dev-install")}. Schema changes now ship as published versions: edit module.json, ${code("dashwise module publish --bump <patch|minor|major>")}, then ${code("dashwise module install")} to upgrade.`
|
|
4234
|
+
);
|
|
4235
|
+
return 1;
|
|
4236
|
+
case "UNIQUE_INDEX_BACKFILL_CONFLICT":
|
|
4237
|
+
fail(
|
|
4238
|
+
`Cannot create a UNIQUE index \u2014 existing live rows already violate it: ${err.message}
|
|
4239
|
+
De-duplicate the conflicting rows (only live rows count; trashed rows don't block since indexes are partial over live rows), then re-run ${code("dashwise schema push")}.`
|
|
4240
|
+
);
|
|
4241
|
+
return 1;
|
|
4242
|
+
case "CHECK_CONSTRAINT_BACKFILL_CONFLICT":
|
|
4243
|
+
fail(
|
|
4244
|
+
`Cannot add the CHECK constraint \u2014 existing rows violate its expression: ${err.message}
|
|
4245
|
+
Fix or remove the violating rows, then re-run ${code("dashwise schema push")}.`
|
|
4246
|
+
);
|
|
4247
|
+
return 1;
|
|
4248
|
+
case "UNAUTHENTICATED":
|
|
4249
|
+
fail(
|
|
4250
|
+
`Auth rejected by the backend \u2014 your CLI token may be stale. Re-run ${code("dashwise login")} and try again.`
|
|
4251
|
+
);
|
|
4252
|
+
return 1;
|
|
4253
|
+
case "FORBIDDEN":
|
|
4254
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
4255
|
+
fail(
|
|
4256
|
+
`Not authorized: pushing a draft manifest requires membership of the module's owner workspace.`
|
|
4257
|
+
);
|
|
4258
|
+
return 1;
|
|
4259
|
+
default:
|
|
4260
|
+
if (err.status === 400) {
|
|
4261
|
+
fail(`Manifest rejected: ${err.message}`);
|
|
4262
|
+
for (const issue of extractIssues2(err.context)) {
|
|
4263
|
+
out(` ${dim("-")} ${issue}`);
|
|
4264
|
+
}
|
|
4265
|
+
return 1;
|
|
4266
|
+
}
|
|
4267
|
+
fail(`Schema push failed (${err.code} ${err.status}): ${err.message}`);
|
|
4268
|
+
return 1;
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
fail(`Schema push failed: ${err.message}`);
|
|
4272
|
+
return 1;
|
|
4273
|
+
}
|
|
4274
|
+
function extractIssues2(context) {
|
|
4275
|
+
if (!context) return [];
|
|
4276
|
+
const raw = context.issues;
|
|
4277
|
+
if (!Array.isArray(raw)) return [];
|
|
4278
|
+
return raw.map((i) => {
|
|
4279
|
+
if (typeof i === "string") return i;
|
|
4280
|
+
if (i && typeof i === "object") {
|
|
4281
|
+
const o = i;
|
|
4282
|
+
const path = typeof o.path === "string" ? o.path : "";
|
|
4283
|
+
const message = typeof o.message === "string" ? o.message : JSON.stringify(i);
|
|
4284
|
+
return path ? `${path}: ${message}` : message;
|
|
4285
|
+
}
|
|
4286
|
+
return String(i);
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
var init_push = __esm({
|
|
4290
|
+
"src/commands/schema/push.ts"() {
|
|
3327
4291
|
init_api_client();
|
|
3328
|
-
|
|
4292
|
+
init_config();
|
|
4293
|
+
init_manifest_io();
|
|
4294
|
+
init_manifest_merge();
|
|
3329
4295
|
init_output();
|
|
3330
|
-
init_create();
|
|
3331
4296
|
}
|
|
3332
4297
|
});
|
|
3333
4298
|
|
|
3334
|
-
// src/
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
if (!tableSlug || !fieldSlug || !newType) {
|
|
3341
|
-
fail(
|
|
3342
|
-
"Usage: `dashwise field set-type <table-slug> <field-slug> <new-type>`"
|
|
4299
|
+
// src/lib/field-spec.ts
|
|
4300
|
+
function parseFieldSpec(spec) {
|
|
4301
|
+
const parts = spec.split(":");
|
|
4302
|
+
if (parts.length < 2) {
|
|
4303
|
+
throw new FieldSpecError(
|
|
4304
|
+
`Field spec ${quote(spec)} is malformed \u2014 expected ${quote("<slug>:<type>[:flags]")}.`
|
|
3343
4305
|
);
|
|
3344
|
-
return 1;
|
|
3345
4306
|
}
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
4307
|
+
const [slug, type, ...flagParts] = parts;
|
|
4308
|
+
if (!SLUG_REGEX.test(slug)) {
|
|
4309
|
+
throw new FieldSpecError(
|
|
4310
|
+
`Field slug ${quote(slug)} must match ${quote(SLUG_REGEX.source)} (snake_case, 2+ chars).`
|
|
3349
4311
|
);
|
|
3350
|
-
return 1;
|
|
3351
|
-
}
|
|
3352
|
-
let ctx;
|
|
3353
|
-
try {
|
|
3354
|
-
ctx = loadSchemaCommandContext({
|
|
3355
|
-
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
3356
|
-
});
|
|
3357
|
-
} catch (err) {
|
|
3358
|
-
fail(err.message);
|
|
3359
|
-
return 1;
|
|
3360
4312
|
}
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
try {
|
|
3365
|
-
const result = await updateField(
|
|
3366
|
-
tableSlug,
|
|
3367
|
-
fieldSlug,
|
|
3368
|
-
{ type: newType },
|
|
3369
|
-
ctx.requestOptions
|
|
3370
|
-
);
|
|
3371
|
-
success(
|
|
3372
|
-
`Updated ${code(`${tableSlug}.${fieldSlug}`)} \u2192 ${code(newType)} (manifest_version=${result.manifest_version})`
|
|
4313
|
+
if (!FIELD_TYPES.includes(type)) {
|
|
4314
|
+
throw new FieldSpecError(
|
|
4315
|
+
`Field type ${quote(type)} is not a known type. Allowed: ${FIELD_TYPES.join(", ")}.`
|
|
3373
4316
|
);
|
|
3374
|
-
} catch (err) {
|
|
3375
|
-
return surfaceFailure(err);
|
|
3376
4317
|
}
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
4318
|
+
const result = { slug, type };
|
|
4319
|
+
if (flagParts.length > 0) {
|
|
4320
|
+
const flags = flagParts.flatMap((segment) => segment.split(",")).map((f) => f.trim()).filter((f) => f.length > 0);
|
|
4321
|
+
for (const flag of flags) {
|
|
4322
|
+
if (flag === "unique") {
|
|
4323
|
+
throw new FieldSpecError(
|
|
4324
|
+
`The ${quote("unique")} flag is not supported on field specs \u2014 uniqueness is a TABLE-level index. Declare it in module.json as ${quote(`owns.tables[].indexes: [{ "fields": ["${slug}"], "unique": true }]`)} and apply it with ${quote("dashwise schema push")}.`
|
|
4325
|
+
);
|
|
4326
|
+
}
|
|
4327
|
+
if (!KNOWN_FLAGS.has(flag)) {
|
|
4328
|
+
throw new FieldSpecError(
|
|
4329
|
+
`Unknown flag ${quote(flag)} in field spec ${quote(spec)}. Known flags: ${[...KNOWN_FLAGS].join(", ")}. For options/target/default, use ${quote("--field-json")} instead.`
|
|
4330
|
+
);
|
|
4331
|
+
}
|
|
4332
|
+
if (flag === "required") result.required = true;
|
|
4333
|
+
}
|
|
3386
4334
|
}
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
init_api_client();
|
|
3391
|
-
|
|
3392
|
-
// src/lib/connect-status.ts
|
|
3393
|
-
init_api_client();
|
|
3394
|
-
init_output();
|
|
3395
|
-
async function reportConnectStatus(opts) {
|
|
3396
|
-
const [deps, bindings] = await Promise.all([
|
|
3397
|
-
safeGet(
|
|
3398
|
-
() => getInstallationDependencies(opts.installationId, opts.requestOptions)
|
|
3399
|
-
),
|
|
3400
|
-
safeGet(
|
|
3401
|
-
() => getInstallationBindings(opts.installationId, opts.requestOptions)
|
|
3402
|
-
)
|
|
3403
|
-
]);
|
|
3404
|
-
if (deps === null && bindings === null) return;
|
|
3405
|
-
const depList = deps ?? [];
|
|
3406
|
-
const bindList = bindings ?? [];
|
|
3407
|
-
if (depList.length === 0 && bindList.length === 0) return;
|
|
3408
|
-
const unboundDeps = depList.filter((d) => d.status === "unbound").length;
|
|
3409
|
-
const unboundUses = bindList.filter((b) => b.status === "unbound").length;
|
|
3410
|
-
info(
|
|
3411
|
-
`Connect status for ${code(opts.moduleSlug)} ${dim(
|
|
3412
|
-
`(${depList.length} dep${depList.length === 1 ? "" : "s"}, ${bindList.length} uses slot${bindList.length === 1 ? "" : "s"})`
|
|
3413
|
-
)}:`
|
|
3414
|
-
);
|
|
3415
|
-
for (const d of depList) renderDep(d);
|
|
3416
|
-
for (const b of bindList) renderBinding(b);
|
|
3417
|
-
const hints = [];
|
|
3418
|
-
if (unboundDeps > 0) {
|
|
3419
|
-
hints.push(
|
|
3420
|
-
`${code("dashwise deps bind <provider-slug>")} once the provider is installed`
|
|
4335
|
+
if ((type === "single_select" || type === "multi_select") && result.options === void 0) {
|
|
4336
|
+
throw new FieldSpecError(
|
|
4337
|
+
`Type ${quote(type)} requires an ${quote("options")} list. Use ${quote("--field-json")} for this field \u2014 e.g. ${quote(`'{"slug":"${slug}","type":"${type}","options":["a","b"]}'`)}.`
|
|
3421
4338
|
);
|
|
3422
4339
|
}
|
|
3423
|
-
if (
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
if (hints.length > 0) {
|
|
3427
|
-
info(`Connect parked items with ${hints.join("; ")}.`);
|
|
3428
|
-
}
|
|
3429
|
-
out("");
|
|
3430
|
-
}
|
|
3431
|
-
function renderDep(d) {
|
|
3432
|
-
if (d.status === "resolved") {
|
|
3433
|
-
const providerLine = d.provider ? ` ${dim(`\u2192 install #${d.provider.installation_id} @ v${d.provider.version}`)}` : "";
|
|
3434
|
-
out(` ${pc.green("\u25CF resolved")} ${dim("dep")} ${code(d.slug)}@${d.range ?? "*"}${providerLine}`);
|
|
3435
|
-
} else {
|
|
3436
|
-
out(
|
|
3437
|
-
` ${pc.yellow("\u25CB unbound")} ${dim("dep")} ${code(d.slug)}@${d.range ?? "*"} ${dim(`reason: ${d.reason ?? "(unspecified)"}`)}`
|
|
4340
|
+
if (type === "link_row" && result.target === void 0) {
|
|
4341
|
+
throw new FieldSpecError(
|
|
4342
|
+
`Type ${quote("link_row")} requires a ${quote("target")}. Use ${quote("--field-json")}.`
|
|
3438
4343
|
);
|
|
3439
4344
|
}
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
const tableLine = b.table ? ` ${dim(`\u2192 ${b.table.name} (id ${b.table.id})`)}` : "";
|
|
3444
|
-
out(` ${pc.green("\u25CF bound")} ${dim("uses")} ${code(b.uses_slug)}${tableLine}`);
|
|
3445
|
-
} else if (b.status === "declined") {
|
|
3446
|
-
out(` ${pc.dim("\u2298 declined")} ${dim("uses")} ${code(b.uses_slug)}`);
|
|
3447
|
-
} else {
|
|
3448
|
-
out(
|
|
3449
|
-
` ${pc.yellow("\u25CB unbound")} ${dim("uses")} ${code(b.uses_slug)} ${dim(`reason: ${b.reason ?? "(unspecified)"}`)}`
|
|
4345
|
+
if (type === "formula" || type === "ai") {
|
|
4346
|
+
throw new FieldSpecError(
|
|
4347
|
+
`Type ${quote(type)} requires extra config (${quote("formula")} or ${quote("prompt")}). Use ${quote("--field-json")}.`
|
|
3450
4348
|
);
|
|
3451
4349
|
}
|
|
4350
|
+
return result;
|
|
3452
4351
|
}
|
|
3453
|
-
|
|
4352
|
+
function parseFieldJson(json) {
|
|
4353
|
+
let parsed;
|
|
3454
4354
|
try {
|
|
3455
|
-
|
|
4355
|
+
parsed = JSON.parse(json);
|
|
3456
4356
|
} catch (err) {
|
|
3457
|
-
|
|
4357
|
+
throw new FieldSpecError(
|
|
4358
|
+
`--field-json value is not valid JSON: ${err.message}`
|
|
4359
|
+
);
|
|
3458
4360
|
}
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
init_api_client();
|
|
3467
|
-
function startManifestPollLoop(opts) {
|
|
3468
|
-
const {
|
|
3469
|
-
manifestPath: manifestPath2,
|
|
3470
|
-
requestOptions,
|
|
3471
|
-
intervalMs,
|
|
3472
|
-
onUpdate,
|
|
3473
|
-
onError
|
|
3474
|
-
} = opts;
|
|
3475
|
-
let lastSeenJson = existsSync(manifestPath2) ? readFileSync(manifestPath2, "utf-8") : "";
|
|
3476
|
-
let timer = null;
|
|
3477
|
-
let stopped = false;
|
|
3478
|
-
const tick = async () => {
|
|
3479
|
-
if (stopped) return;
|
|
3480
|
-
try {
|
|
3481
|
-
const { manifest } = await getInstallationSchema(requestOptions);
|
|
3482
|
-
const remoteJson = JSON.stringify(manifest, null, 2) + "\n";
|
|
3483
|
-
const currentLocal = existsSync(manifestPath2) ? readFileSync(manifestPath2, "utf-8") : "";
|
|
3484
|
-
if (remoteJson === currentLocal) {
|
|
3485
|
-
lastSeenJson = remoteJson;
|
|
3486
|
-
} else if (remoteJson !== lastSeenJson) {
|
|
3487
|
-
writeFileSync(manifestPath2, remoteJson, "utf-8");
|
|
3488
|
-
lastSeenJson = remoteJson;
|
|
3489
|
-
if (onUpdate) {
|
|
3490
|
-
const delta = Buffer.byteLength(remoteJson, "utf8") - Buffer.byteLength(currentLocal, "utf8");
|
|
3491
|
-
const deltaStr = delta === 0 ? "0 bytes" : `${delta > 0 ? "+" : ""}${delta} bytes`;
|
|
3492
|
-
onUpdate(
|
|
3493
|
-
`Backend manifest changed \u2014 synced to module.json (${deltaStr}).`
|
|
3494
|
-
);
|
|
3495
|
-
}
|
|
3496
|
-
}
|
|
3497
|
-
} catch (err) {
|
|
3498
|
-
if (onError) {
|
|
3499
|
-
const message = err instanceof ApiError ? `${err.code} (HTTP ${err.status}): ${err.message}` : err.message;
|
|
3500
|
-
onError(new Error(message));
|
|
3501
|
-
}
|
|
3502
|
-
} finally {
|
|
3503
|
-
if (!stopped) {
|
|
3504
|
-
timer = setTimeout(() => void tick(), intervalMs);
|
|
3505
|
-
}
|
|
3506
|
-
}
|
|
3507
|
-
};
|
|
3508
|
-
timer = setTimeout(() => void tick(), intervalMs);
|
|
3509
|
-
return {
|
|
3510
|
-
stop: () => {
|
|
3511
|
-
if (stopped) return;
|
|
3512
|
-
stopped = true;
|
|
3513
|
-
if (timer) {
|
|
3514
|
-
clearTimeout(timer);
|
|
3515
|
-
timer = null;
|
|
3516
|
-
}
|
|
3517
|
-
},
|
|
3518
|
-
isRunning: () => !stopped
|
|
3519
|
-
};
|
|
3520
|
-
}
|
|
3521
|
-
|
|
3522
|
-
// src/commands/dev.ts
|
|
3523
|
-
init_output();
|
|
3524
|
-
var DEFAULT_NEXT_PORT = 3e3;
|
|
3525
|
-
var PREFIX_SDK = `\x1B[36m[sdk]\x1B[0m `;
|
|
3526
|
-
var PREFIX_NEXT = `\x1B[35m[next]\x1B[0m `;
|
|
3527
|
-
var PREFIX_POLL = `\x1B[33m[poll]\x1B[0m `;
|
|
3528
|
-
var DEFAULT_REMOTE_POLL_INTERVAL_SEC = 5;
|
|
3529
|
-
var MIN_REMOTE_POLL_INTERVAL_SEC = 1;
|
|
3530
|
-
async function devCommand(options) {
|
|
3531
|
-
if (options.sdkOnly && options.nextOnly) {
|
|
3532
|
-
fail(
|
|
3533
|
-
`Cannot pass both ${code("--sdk-only")} and ${code("--next-only")} \u2014 they're mutually exclusive.`
|
|
4361
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4362
|
+
throw new FieldSpecError("--field-json must be a JSON object.");
|
|
4363
|
+
}
|
|
4364
|
+
const o = parsed;
|
|
4365
|
+
if (typeof o.slug !== "string" || !SLUG_REGEX.test(o.slug)) {
|
|
4366
|
+
throw new FieldSpecError(
|
|
4367
|
+
`--field-json missing or invalid ${quote("slug")}. Must match ${quote(SLUG_REGEX.source)}.`
|
|
3534
4368
|
);
|
|
3535
|
-
return 1;
|
|
3536
4369
|
}
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
4370
|
+
if (typeof o.type !== "string" || !FIELD_TYPES.includes(o.type)) {
|
|
4371
|
+
throw new FieldSpecError(
|
|
4372
|
+
`--field-json missing or invalid ${quote("type")}. Allowed: ${FIELD_TYPES.join(", ")}.`
|
|
4373
|
+
);
|
|
4374
|
+
}
|
|
4375
|
+
return o;
|
|
4376
|
+
}
|
|
4377
|
+
function quote(s) {
|
|
4378
|
+
return `\`${s}\``;
|
|
4379
|
+
}
|
|
4380
|
+
var FIELD_TYPES, SLUG_REGEX, KNOWN_FLAGS, FieldSpecError;
|
|
4381
|
+
var init_field_spec = __esm({
|
|
4382
|
+
"src/lib/field-spec.ts"() {
|
|
4383
|
+
FIELD_TYPES = [
|
|
4384
|
+
"text",
|
|
4385
|
+
"long_text",
|
|
4386
|
+
"number",
|
|
4387
|
+
"boolean",
|
|
4388
|
+
"date",
|
|
4389
|
+
"datetime",
|
|
4390
|
+
"single_select",
|
|
4391
|
+
"multi_select",
|
|
4392
|
+
"link_row",
|
|
4393
|
+
"file",
|
|
4394
|
+
"multi_file",
|
|
4395
|
+
"email",
|
|
4396
|
+
"url",
|
|
4397
|
+
"phone",
|
|
4398
|
+
"formula",
|
|
4399
|
+
"ai"
|
|
4400
|
+
];
|
|
4401
|
+
SLUG_REGEX = /^[a-z][a-z0-9_]*[a-z0-9]$/;
|
|
4402
|
+
KNOWN_FLAGS = /* @__PURE__ */ new Set(["required"]);
|
|
4403
|
+
FieldSpecError = class extends Error {
|
|
4404
|
+
constructor(message) {
|
|
4405
|
+
super(message);
|
|
4406
|
+
this.name = "FieldSpecError";
|
|
4407
|
+
}
|
|
4408
|
+
};
|
|
3543
4409
|
}
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
}
|
|
3557
|
-
})();
|
|
3558
|
-
const apiToken = process.env.DASHWISE_API_TOKEN ?? savedConfig?.token;
|
|
3559
|
-
if (!apiToken) {
|
|
3560
|
-
fail(`Not authenticated. Run ${code("dashwise login")} first.`);
|
|
3561
|
-
info(
|
|
3562
|
-
"The unified dev loop reads your CLI token from ~/.config/dashwise/auth.json and injects it into both child processes so the SDK can authenticate without you editing .env.local."
|
|
3563
|
-
);
|
|
4410
|
+
});
|
|
4411
|
+
|
|
4412
|
+
// src/commands/table/create.ts
|
|
4413
|
+
var create_exports = {};
|
|
4414
|
+
__export(create_exports, {
|
|
4415
|
+
surfaceFailure: () => surfaceFailure,
|
|
4416
|
+
syncLocalManifest: () => syncLocalManifest,
|
|
4417
|
+
tableCreateCommand: () => tableCreateCommand
|
|
4418
|
+
});
|
|
4419
|
+
async function tableCreateCommand(slug, options) {
|
|
4420
|
+
if (!slug) {
|
|
4421
|
+
fail("Usage: `dashwise table create <slug> --field <slug>:<type>[:required] [...]`");
|
|
3564
4422
|
return 1;
|
|
3565
4423
|
}
|
|
3566
|
-
const
|
|
3567
|
-
let port;
|
|
4424
|
+
const fields = [];
|
|
3568
4425
|
try {
|
|
3569
|
-
|
|
4426
|
+
for (const spec of options.field ?? []) {
|
|
4427
|
+
fields.push(parseFieldSpec(spec));
|
|
4428
|
+
}
|
|
4429
|
+
for (const json of options.fieldJson ?? []) {
|
|
4430
|
+
fields.push(parseFieldJson(json));
|
|
4431
|
+
}
|
|
3570
4432
|
} catch (err) {
|
|
3571
|
-
|
|
3572
|
-
return 1;
|
|
3573
|
-
}
|
|
3574
|
-
const explicitInstallationId = options.installationId ?? process.env.DASHWISE_INSTALLATION_ID;
|
|
3575
|
-
const reuseDevSessionId = process.env.DASHWISE_DEV_SESSION_ID;
|
|
3576
|
-
let moduleEntry = manifestSlug !== void 0 ? readModuleEntry(manifestSlug) : null;
|
|
3577
|
-
const healed = manifestSlug !== void 0 ? await resolveModuleApiKey(manifestSlug, moduleEntry, apiUrl, apiToken) : null;
|
|
3578
|
-
if (healed) moduleEntry = healed;
|
|
3579
|
-
const apiKeyEnv = process.env.DASHWISE_API_KEY ?? moduleEntry?.apiKey;
|
|
3580
|
-
let installationIdEnv;
|
|
3581
|
-
let revokeDevSession;
|
|
3582
|
-
if (explicitInstallationId && explicitInstallationId !== "local") {
|
|
3583
|
-
installationIdEnv = explicitInstallationId;
|
|
3584
|
-
} else if (reuseDevSessionId) ; else {
|
|
3585
|
-
const effectiveWorkspace = options.workspace ?? moduleEntry?.workspaceId?.toString();
|
|
3586
|
-
let workspaceContext;
|
|
3587
|
-
try {
|
|
3588
|
-
workspaceContext = await resolveTargetWorkspace(
|
|
3589
|
-
apiUrl,
|
|
3590
|
-
apiToken,
|
|
3591
|
-
effectiveWorkspace
|
|
3592
|
-
);
|
|
3593
|
-
} catch (err) {
|
|
4433
|
+
if (err instanceof FieldSpecError) {
|
|
3594
4434
|
fail(err.message);
|
|
3595
4435
|
return 1;
|
|
3596
4436
|
}
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
if (err instanceof ApiError) {
|
|
3610
|
-
fail(`Failed to register dev session: ${err.code} \u2014 ${err.message}`);
|
|
3611
|
-
} else {
|
|
3612
|
-
fail(`Failed to register dev session: ${err.message}`);
|
|
3613
|
-
}
|
|
4437
|
+
throw err;
|
|
4438
|
+
}
|
|
4439
|
+
if (fields.length === 0) {
|
|
4440
|
+
fail(
|
|
4441
|
+
`At least one field is required. Use ${code("--field <slug>:<type>")} (repeatable) or ${code("--field-json '{...}'")} for fields with options/target/default.`
|
|
4442
|
+
);
|
|
4443
|
+
return 1;
|
|
4444
|
+
}
|
|
4445
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4446
|
+
for (const f of fields) {
|
|
4447
|
+
if (seen.has(f.slug)) {
|
|
4448
|
+
fail(`Duplicate field slug ${code(f.slug)} in --field flags.`);
|
|
3614
4449
|
return 1;
|
|
3615
4450
|
}
|
|
4451
|
+
seen.add(f.slug);
|
|
3616
4452
|
}
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
}
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
if (installationIdEnv !== void 0) {
|
|
3626
|
-
info(` ${dim("installation")} ${installationIdEnv} (prod-mode SSO)`);
|
|
3627
|
-
} else {
|
|
3628
|
-
info(` ${dim("mode")} dev (dev SSO session registered)`);
|
|
4453
|
+
let ctx;
|
|
4454
|
+
try {
|
|
4455
|
+
ctx = loadSchemaCommandContext({
|
|
4456
|
+
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
4457
|
+
});
|
|
4458
|
+
} catch (err) {
|
|
4459
|
+
fail(err.message);
|
|
4460
|
+
return 1;
|
|
3629
4461
|
}
|
|
3630
|
-
info(`
|
|
3631
|
-
|
|
3632
|
-
const
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
` ${dim("api key")} ${maskedToken(apiKeyEnv)} ${dim(`(from ${source}${installId ? `, install id=${installId}` : ""})`)}`
|
|
3636
|
-
);
|
|
3637
|
-
} else if (manifestSlug !== void 0 && installationIdEnv === void 0) {
|
|
3638
|
-
fail(
|
|
3639
|
-
`No ${code("dwk_")} API key for module ${code(manifestSlug)} \u2014 the module will run with NO token.`
|
|
4462
|
+
info(`Creating table ${code(slug)} in installation ${ctx.entry.installationId}\u2026`);
|
|
4463
|
+
try {
|
|
4464
|
+
const result = await createTable(
|
|
4465
|
+
{ slug, fields },
|
|
4466
|
+
ctx.requestOptions
|
|
3640
4467
|
);
|
|
3641
|
-
|
|
3642
|
-
`
|
|
3643
|
-
${code(`dashwise init ${manifestSlug} --force --workspace <your-workspace-slug>`)}
|
|
3644
|
-
(Ensure you're logged into the right backend: ${code(`dashwise login --api-url ${apiUrl}`)}.)`
|
|
4468
|
+
success(
|
|
4469
|
+
`Created table ${code(slug)} (physical_table_id=${result.physical_table_id}, manifest_version=${result.manifest_version})`
|
|
3645
4470
|
);
|
|
4471
|
+
} catch (err) {
|
|
4472
|
+
return surfaceFailure(err);
|
|
3646
4473
|
}
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
4474
|
+
return syncLocalManifest(ctx);
|
|
4475
|
+
}
|
|
4476
|
+
async function syncLocalManifest(ctx) {
|
|
4477
|
+
try {
|
|
4478
|
+
const { manifest } = await getInstallationSchema(ctx.requestOptions);
|
|
4479
|
+
const local = (() => {
|
|
4480
|
+
try {
|
|
4481
|
+
return readManifest(ctx.projectRoot);
|
|
4482
|
+
} catch {
|
|
4483
|
+
return null;
|
|
4484
|
+
}
|
|
4485
|
+
})();
|
|
4486
|
+
const { merged, preserved } = mergeRemoteManifest(local, manifest);
|
|
4487
|
+
writeManifest(merged, ctx.projectRoot);
|
|
4488
|
+
if (preserved.length > 0) {
|
|
4489
|
+
info(PRESERVED_BLOCKS_NOTICE);
|
|
4490
|
+
}
|
|
4491
|
+
return 0;
|
|
4492
|
+
} catch (err) {
|
|
4493
|
+
if (err instanceof ApiError) {
|
|
4494
|
+
warn(
|
|
4495
|
+
`Schema sync to local module.json failed (${err.code} ${err.status}): ${err.message}. The mutation succeeded server-side; re-run your next command to retry the local sync.`
|
|
4496
|
+
);
|
|
4497
|
+
} else {
|
|
4498
|
+
warn(
|
|
4499
|
+
`Schema sync to local module.json failed: ${err.message}. The mutation succeeded server-side.`
|
|
4500
|
+
);
|
|
4501
|
+
}
|
|
4502
|
+
return 0;
|
|
3654
4503
|
}
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
4504
|
+
}
|
|
4505
|
+
function surfaceFailure(err) {
|
|
4506
|
+
if (err instanceof ApiError) {
|
|
4507
|
+
switch (err.code) {
|
|
4508
|
+
case "TABLE_SLUG_EXISTS":
|
|
4509
|
+
fail(`A table with this slug already exists in the manifest.`);
|
|
4510
|
+
break;
|
|
4511
|
+
case "FIELD_SLUG_EXISTS":
|
|
4512
|
+
fail(`A field with this slug already exists on the table.`);
|
|
4513
|
+
break;
|
|
4514
|
+
case "TABLE_NOT_FOUND":
|
|
4515
|
+
fail(`Table not found in the manifest.`);
|
|
4516
|
+
break;
|
|
4517
|
+
case "FIELD_NOT_FOUND":
|
|
4518
|
+
fail(`Field not found on the table.`);
|
|
4519
|
+
break;
|
|
4520
|
+
case "UNAUTHENTICATED":
|
|
4521
|
+
fail(
|
|
4522
|
+
`API key rejected by backend. The cached key may be expired or revoked \u2014 re-run \`dashwise init <slug>\` to mint a fresh one (existing files are preserved).`
|
|
4523
|
+
);
|
|
4524
|
+
break;
|
|
4525
|
+
case "SCHEMA_LOCKED":
|
|
4526
|
+
fail(
|
|
4527
|
+
`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>\` for a local dev install (existing files are preserved).`
|
|
4528
|
+
);
|
|
4529
|
+
break;
|
|
4530
|
+
case "INSTALL_MISMATCH":
|
|
4531
|
+
fail(
|
|
4532
|
+
`API key is bound to a different installation than the URL. The cached install id may be stale \u2014 re-run \`dashwise init <slug>\`.`
|
|
4533
|
+
);
|
|
4534
|
+
break;
|
|
4535
|
+
default:
|
|
4536
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
4537
|
+
}
|
|
4538
|
+
return 1;
|
|
3659
4539
|
}
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
4540
|
+
fail(`Unexpected error: ${err.message}`);
|
|
4541
|
+
return 1;
|
|
4542
|
+
}
|
|
4543
|
+
var init_create = __esm({
|
|
4544
|
+
"src/commands/table/create.ts"() {
|
|
4545
|
+
init_manifest_io();
|
|
4546
|
+
init_api_client();
|
|
4547
|
+
init_manifest_merge();
|
|
4548
|
+
init_schema_command_context();
|
|
4549
|
+
init_field_spec();
|
|
4550
|
+
init_output();
|
|
3663
4551
|
}
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
4552
|
+
});
|
|
4553
|
+
|
|
4554
|
+
// src/commands/table/delete.ts
|
|
4555
|
+
var delete_exports = {};
|
|
4556
|
+
__export(delete_exports, {
|
|
4557
|
+
tableDeleteCommand: () => tableDeleteCommand
|
|
4558
|
+
});
|
|
4559
|
+
async function tableDeleteCommand(slug, options) {
|
|
4560
|
+
if (!slug) {
|
|
4561
|
+
fail("Usage: `dashwise table delete <slug>`");
|
|
4562
|
+
return 1;
|
|
3667
4563
|
}
|
|
3668
|
-
let
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
const intervalSec = Math.max(
|
|
3673
|
-
MIN_REMOTE_POLL_INTERVAL_SEC,
|
|
3674
|
-
options.remotePollIntervalSec ?? DEFAULT_REMOTE_POLL_INTERVAL_SEC
|
|
3675
|
-
);
|
|
3676
|
-
manifestPoll = startManifestPollLoop({
|
|
3677
|
-
manifestPath: manifestPath2,
|
|
3678
|
-
requestOptions: {
|
|
3679
|
-
baseUrl: apiUrl,
|
|
3680
|
-
// The `dwk_` key resolves the install server-side.
|
|
3681
|
-
auth: { token: apiKeyEnv }
|
|
3682
|
-
},
|
|
3683
|
-
intervalMs: intervalSec * 1e3,
|
|
3684
|
-
onUpdate: (msg) => {
|
|
3685
|
-
process.stdout.write(`${PREFIX_POLL}${msg}
|
|
3686
|
-
`);
|
|
3687
|
-
},
|
|
3688
|
-
onError: (err) => {
|
|
3689
|
-
process.stderr.write(`${PREFIX_POLL}${err.message}
|
|
3690
|
-
`);
|
|
3691
|
-
}
|
|
4564
|
+
let ctx;
|
|
4565
|
+
try {
|
|
4566
|
+
ctx = loadSchemaCommandContext({
|
|
4567
|
+
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
3692
4568
|
});
|
|
3693
|
-
|
|
3694
|
-
|
|
4569
|
+
} catch (err) {
|
|
4570
|
+
fail(err.message);
|
|
4571
|
+
return 1;
|
|
4572
|
+
}
|
|
4573
|
+
info(`Dropping table ${code(slug)} from installation ${ctx.entry.installationId}\u2026`);
|
|
4574
|
+
try {
|
|
4575
|
+
const result = await dropTable(slug, ctx.requestOptions);
|
|
4576
|
+
success(
|
|
4577
|
+
`Dropped table ${code(slug)} (manifest_version=${result.manifest_version}). Physical table soft-deleted; recoverable for 30 days via the dashboard's trash.`
|
|
3695
4578
|
);
|
|
3696
|
-
|
|
4579
|
+
} catch (err) {
|
|
4580
|
+
return surfaceFailure(err);
|
|
3697
4581
|
}
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
info(`
|
|
3707
|
-
Received ${sig}, stopping children\u2026`);
|
|
3708
|
-
for (const { child } of children) {
|
|
3709
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
3710
|
-
child.kill(sig);
|
|
3711
|
-
}
|
|
3712
|
-
}
|
|
3713
|
-
if (revokeDevSession) {
|
|
3714
|
-
revokeDevSession().catch(() => {
|
|
3715
|
-
});
|
|
3716
|
-
}
|
|
3717
|
-
};
|
|
3718
|
-
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
|
3719
|
-
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
|
3720
|
-
const results = await Promise.all(
|
|
3721
|
-
children.map(
|
|
3722
|
-
({ name, child }) => new Promise(
|
|
3723
|
-
(resolveExit) => {
|
|
3724
|
-
child.on("exit", (exitCode2, signal) => {
|
|
3725
|
-
const killed = signal !== null;
|
|
3726
|
-
const finalCode = exitCode2 ?? (killed ? 0 : 1);
|
|
3727
|
-
resolveExit({ name, code: finalCode, killed });
|
|
3728
|
-
});
|
|
3729
|
-
}
|
|
3730
|
-
)
|
|
3731
|
-
)
|
|
3732
|
-
);
|
|
3733
|
-
const crashed = results.find((r) => !r.killed && r.code !== 0);
|
|
3734
|
-
if (crashed && !shuttingDown) {
|
|
3735
|
-
warn(`[${crashed.name}] exited with code ${crashed.code} \u2014 stopping others.`);
|
|
3736
|
-
for (const { child } of children) {
|
|
3737
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
3738
|
-
child.kill("SIGTERM");
|
|
3739
|
-
}
|
|
3740
|
-
}
|
|
4582
|
+
return syncLocalManifest(ctx);
|
|
4583
|
+
}
|
|
4584
|
+
var init_delete = __esm({
|
|
4585
|
+
"src/commands/table/delete.ts"() {
|
|
4586
|
+
init_api_client();
|
|
4587
|
+
init_schema_command_context();
|
|
4588
|
+
init_output();
|
|
4589
|
+
init_create();
|
|
3741
4590
|
}
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
4591
|
+
});
|
|
4592
|
+
|
|
4593
|
+
// src/commands/field/add.ts
|
|
4594
|
+
var add_exports = {};
|
|
4595
|
+
__export(add_exports, {
|
|
4596
|
+
fieldAddCommand: () => fieldAddCommand
|
|
4597
|
+
});
|
|
4598
|
+
async function fieldAddCommand(tableSlug, fieldSpec, options) {
|
|
4599
|
+
if (!tableSlug) {
|
|
4600
|
+
fail(
|
|
4601
|
+
"Usage: `dashwise field add <table-slug> <field-slug>:<type>[:required]` or `dashwise field add <table-slug> --field-json '{...}'`"
|
|
4602
|
+
);
|
|
4603
|
+
return 1;
|
|
3745
4604
|
}
|
|
3746
|
-
const
|
|
3747
|
-
|
|
3748
|
-
|
|
4605
|
+
const hasPositional = fieldSpec !== void 0;
|
|
4606
|
+
const hasJson = options.fieldJson !== void 0;
|
|
4607
|
+
if (!hasPositional && !hasJson) {
|
|
4608
|
+
fail(
|
|
4609
|
+
`Pass either a field spec (e.g. ${code("title:text:required")}) or ${code("--field-json")} with the field's JSON definition.`
|
|
4610
|
+
);
|
|
4611
|
+
return 1;
|
|
3749
4612
|
}
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
4613
|
+
if (hasPositional && hasJson) {
|
|
4614
|
+
fail(
|
|
4615
|
+
`Pass only ONE of the field spec or ${code("--field-json")} \u2014 they're mutually exclusive.`
|
|
4616
|
+
);
|
|
4617
|
+
return 1;
|
|
4618
|
+
}
|
|
4619
|
+
let field;
|
|
4620
|
+
try {
|
|
4621
|
+
field = hasJson ? parseFieldJson(options.fieldJson) : parseFieldSpec(fieldSpec);
|
|
4622
|
+
} catch (err) {
|
|
4623
|
+
if (err instanceof FieldSpecError) {
|
|
4624
|
+
fail(err.message);
|
|
4625
|
+
return 1;
|
|
3760
4626
|
}
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
function spawnNextDev(projectRoot, env, port) {
|
|
3765
|
-
const portArgs = port !== void 0 ? ["--", "--port", String(port)] : [];
|
|
3766
|
-
const hasDevNextScript = hasNpmScript(projectRoot, "dev:next");
|
|
3767
|
-
const [cmd, args] = hasDevNextScript ? ["npm", ["run", "dev:next", ...portArgs]] : ["npx", ["next", "dev", ...port !== void 0 ? ["--port", String(port)] : []]];
|
|
3768
|
-
const child = spawn(cmd, args, {
|
|
3769
|
-
cwd: projectRoot,
|
|
3770
|
-
env,
|
|
3771
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3772
|
-
// Windows requires `shell: true` for `npm` / `npx` because they're
|
|
3773
|
-
// `.cmd` shims, not real binaries.
|
|
3774
|
-
shell: process.platform === "win32"
|
|
3775
|
-
});
|
|
3776
|
-
return child;
|
|
3777
|
-
}
|
|
3778
|
-
function hasNpmScript(projectRoot, scriptName) {
|
|
4627
|
+
throw err;
|
|
4628
|
+
}
|
|
4629
|
+
let ctx;
|
|
3779
4630
|
try {
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
return
|
|
4631
|
+
ctx = loadSchemaCommandContext({
|
|
4632
|
+
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
4633
|
+
});
|
|
4634
|
+
} catch (err) {
|
|
4635
|
+
fail(err.message);
|
|
4636
|
+
return 1;
|
|
3786
4637
|
}
|
|
3787
|
-
}
|
|
3788
|
-
function pipeWithPrefix(src, dst, prefix) {
|
|
3789
|
-
if (!src) return;
|
|
3790
|
-
let leftover = "";
|
|
3791
|
-
src.on("data", (chunk) => {
|
|
3792
|
-
const text3 = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
3793
|
-
const combined = leftover + text3;
|
|
3794
|
-
const lines = combined.split("\n");
|
|
3795
|
-
leftover = lines.pop() ?? "";
|
|
3796
|
-
for (const line of lines) {
|
|
3797
|
-
dst.write(`${prefix}${line}
|
|
3798
|
-
`);
|
|
3799
|
-
}
|
|
3800
|
-
});
|
|
3801
|
-
src.on("end", () => {
|
|
3802
|
-
if (leftover.length > 0) {
|
|
3803
|
-
dst.write(`${prefix}${leftover}
|
|
3804
|
-
`);
|
|
3805
|
-
}
|
|
3806
|
-
});
|
|
3807
|
-
}
|
|
3808
|
-
function maskedToken(token) {
|
|
3809
|
-
if (token.length <= 12) return "****";
|
|
3810
|
-
const prefix = token.slice(0, 4);
|
|
3811
|
-
const tail = token.slice(-4);
|
|
3812
|
-
return `${prefix}\u2026${tail}`;
|
|
3813
|
-
}
|
|
3814
|
-
async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
|
|
3815
|
-
if (process.env.DASHWISE_API_KEY) return null;
|
|
3816
|
-
if (!entry) return null;
|
|
3817
|
-
if (entry.apiKey) return null;
|
|
3818
|
-
if (!entry.installationId) return null;
|
|
3819
4638
|
info(
|
|
3820
|
-
`
|
|
4639
|
+
`Adding field ${code(`${tableSlug}.${field.slug}`)} (${field.type}) to installation ${ctx.entry.installationId}\u2026`
|
|
3821
4640
|
);
|
|
3822
|
-
let minted;
|
|
3823
4641
|
try {
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
{ baseUrl: apiUrl, auth: { token: cliBearer } }
|
|
4642
|
+
const result = await addField(
|
|
4643
|
+
{ table_slug: tableSlug, field },
|
|
4644
|
+
ctx.requestOptions
|
|
3828
4645
|
);
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
warn(
|
|
3832
|
-
`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).`
|
|
3833
|
-
);
|
|
3834
|
-
return null;
|
|
3835
|
-
}
|
|
3836
|
-
const reason = err instanceof ApiError ? `${err.code}: ${err.message}` : err.message;
|
|
3837
|
-
warn(
|
|
3838
|
-
`Could not upgrade credentials for ${code(slug)} (${reason}). Data-plane calls may fail; run ${code(`dashwise init ${slug} --force`)} to refresh.`
|
|
4646
|
+
success(
|
|
4647
|
+
`Added field ${code(`${tableSlug}.${field.slug}`)} (manifest_version=${result.manifest_version})`
|
|
3839
4648
|
);
|
|
3840
|
-
|
|
4649
|
+
} catch (err) {
|
|
4650
|
+
return surfaceFailure(err);
|
|
3841
4651
|
}
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
4652
|
+
return syncLocalManifest(ctx);
|
|
4653
|
+
}
|
|
4654
|
+
var init_add = __esm({
|
|
4655
|
+
"src/commands/field/add.ts"() {
|
|
4656
|
+
init_api_client();
|
|
4657
|
+
init_schema_command_context();
|
|
4658
|
+
init_field_spec();
|
|
4659
|
+
init_output();
|
|
4660
|
+
init_create();
|
|
4661
|
+
}
|
|
4662
|
+
});
|
|
4663
|
+
|
|
4664
|
+
// src/commands/field/remove.ts
|
|
4665
|
+
var remove_exports = {};
|
|
4666
|
+
__export(remove_exports, {
|
|
4667
|
+
fieldRemoveCommand: () => fieldRemoveCommand
|
|
4668
|
+
});
|
|
4669
|
+
async function fieldRemoveCommand(tableSlug, fieldSlug, options) {
|
|
4670
|
+
if (!tableSlug || !fieldSlug) {
|
|
4671
|
+
fail("Usage: `dashwise field remove <table-slug> <field-slug>`");
|
|
4672
|
+
return 1;
|
|
4673
|
+
}
|
|
4674
|
+
let ctx;
|
|
3848
4675
|
try {
|
|
3849
|
-
|
|
4676
|
+
ctx = loadSchemaCommandContext({
|
|
4677
|
+
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
4678
|
+
});
|
|
3850
4679
|
} catch (err) {
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
4680
|
+
fail(err.message);
|
|
4681
|
+
return 1;
|
|
4682
|
+
}
|
|
4683
|
+
info(
|
|
4684
|
+
`Removing field ${code(`${tableSlug}.${fieldSlug}`)} from installation ${ctx.entry.installationId}\u2026`
|
|
4685
|
+
);
|
|
4686
|
+
try {
|
|
4687
|
+
const result = await removeField(
|
|
4688
|
+
tableSlug,
|
|
4689
|
+
fieldSlug,
|
|
4690
|
+
ctx.requestOptions
|
|
4691
|
+
);
|
|
4692
|
+
success(
|
|
4693
|
+
`Removed field ${code(`${tableSlug}.${fieldSlug}`)} (manifest_version=${result.manifest_version})`
|
|
3854
4694
|
);
|
|
4695
|
+
} catch (err) {
|
|
4696
|
+
return surfaceFailure(err);
|
|
3855
4697
|
}
|
|
3856
|
-
|
|
3857
|
-
return upgraded;
|
|
3858
|
-
}
|
|
3859
|
-
function computeCallbackOrigins(port, extra) {
|
|
3860
|
-
const def = `http://localhost:${port}`;
|
|
3861
|
-
return Array.from(/* @__PURE__ */ new Set([def, ...extra]));
|
|
4698
|
+
return syncLocalManifest(ctx);
|
|
3862
4699
|
}
|
|
3863
|
-
var
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
4700
|
+
var init_remove = __esm({
|
|
4701
|
+
"src/commands/field/remove.ts"() {
|
|
4702
|
+
init_api_client();
|
|
4703
|
+
init_schema_command_context();
|
|
4704
|
+
init_output();
|
|
4705
|
+
init_create();
|
|
4706
|
+
}
|
|
4707
|
+
});
|
|
4708
|
+
|
|
4709
|
+
// src/commands/field/set-type.ts
|
|
4710
|
+
var set_type_exports = {};
|
|
4711
|
+
__export(set_type_exports, {
|
|
4712
|
+
fieldSetTypeCommand: () => fieldSetTypeCommand
|
|
4713
|
+
});
|
|
4714
|
+
async function fieldSetTypeCommand(tableSlug, fieldSlug, newType, options) {
|
|
4715
|
+
if (!tableSlug || !fieldSlug || !newType) {
|
|
4716
|
+
fail(
|
|
4717
|
+
"Usage: `dashwise field set-type <table-slug> <field-slug> <new-type>`"
|
|
3871
4718
|
);
|
|
4719
|
+
return 1;
|
|
3872
4720
|
}
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
`No free port found between ${DEFAULT_NEXT_PORT} and ${DEFAULT_NEXT_PORT + PORT_PROBE_LIMIT - 1}. Free a port (e.g. \`lsof -i :${DEFAULT_NEXT_PORT}\`) or pass \`--port <N>\` explicitly.`
|
|
4721
|
+
if (!FIELD_TYPES.includes(newType)) {
|
|
4722
|
+
fail(
|
|
4723
|
+
`Type ${code(newType)} is not a known type. Allowed: ${FIELD_TYPES.join(", ")}.`
|
|
3877
4724
|
);
|
|
4725
|
+
return 1;
|
|
3878
4726
|
}
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
return new Promise((resolve13) => {
|
|
3884
|
-
const server = createServer();
|
|
3885
|
-
server.once("error", () => {
|
|
3886
|
-
resolve13(false);
|
|
3887
|
-
});
|
|
3888
|
-
server.once("listening", () => {
|
|
3889
|
-
server.close(() => resolve13(true));
|
|
4727
|
+
let ctx;
|
|
4728
|
+
try {
|
|
4729
|
+
ctx = loadSchemaCommandContext({
|
|
4730
|
+
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
3890
4731
|
});
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
async function findFirstFreePort(start, limit) {
|
|
3895
|
-
for (let i = 0; i < limit; i++) {
|
|
3896
|
-
const port = start + i;
|
|
3897
|
-
if (await isPortFree(port)) return port;
|
|
4732
|
+
} catch (err) {
|
|
4733
|
+
fail(err.message);
|
|
4734
|
+
return 1;
|
|
3898
4735
|
}
|
|
3899
|
-
|
|
3900
|
-
}
|
|
3901
|
-
async function resolveTargetWorkspace(apiUrl, apiToken, flag) {
|
|
3902
|
-
const workspaces = await apiRequest(
|
|
3903
|
-
"GET",
|
|
3904
|
-
"/api/workspaces",
|
|
3905
|
-
void 0,
|
|
3906
|
-
{ baseUrl: apiUrl, auth: { token: apiToken } }
|
|
4736
|
+
info(
|
|
4737
|
+
`Setting type of ${code(`${tableSlug}.${fieldSlug}`)} \u2192 ${code(newType)} on installation ${ctx.entry.installationId}\u2026`
|
|
3907
4738
|
);
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
if (workspaces.length === 1) {
|
|
3915
|
-
return workspaces[0];
|
|
3916
|
-
}
|
|
3917
|
-
const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id}, name="${w.name}")`).join("\n");
|
|
3918
|
-
throw new Error(
|
|
3919
|
-
`You have ${workspaces.length} workspaces. Re-run with --workspace <slug-or-id> to pick one:
|
|
3920
|
-
${list}`
|
|
4739
|
+
try {
|
|
4740
|
+
const result = await updateField(
|
|
4741
|
+
tableSlug,
|
|
4742
|
+
fieldSlug,
|
|
4743
|
+
{ type: newType },
|
|
4744
|
+
ctx.requestOptions
|
|
3921
4745
|
);
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
const isNumeric = !Number.isNaN(asNumber) && String(asNumber) === flag;
|
|
3925
|
-
const match = isNumeric ? workspaces.find((w) => w.id === asNumber) : workspaces.find((w) => w.slug === flag);
|
|
3926
|
-
if (!match) {
|
|
3927
|
-
const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id})`).join("\n");
|
|
3928
|
-
throw new Error(
|
|
3929
|
-
`No workspace matching --workspace=${flag}. Available:
|
|
3930
|
-
${list}`
|
|
4746
|
+
success(
|
|
4747
|
+
`Updated ${code(`${tableSlug}.${fieldSlug}`)} \u2192 ${code(newType)} (manifest_version=${result.manifest_version})`
|
|
3931
4748
|
);
|
|
4749
|
+
} catch (err) {
|
|
4750
|
+
return surfaceFailure(err);
|
|
3932
4751
|
}
|
|
3933
|
-
return
|
|
3934
|
-
}
|
|
3935
|
-
async function registerDevSession(apiUrl, apiToken, body) {
|
|
3936
|
-
return apiRequest(
|
|
3937
|
-
"POST",
|
|
3938
|
-
"/api/auth/dev/sessions",
|
|
3939
|
-
body,
|
|
3940
|
-
{ baseUrl: apiUrl, auth: { token: apiToken } }
|
|
3941
|
-
);
|
|
3942
|
-
}
|
|
3943
|
-
async function revokeDevSessionBestEffort(apiUrl, apiToken, id) {
|
|
3944
|
-
await apiRequest(
|
|
3945
|
-
"DELETE",
|
|
3946
|
-
`/api/auth/dev/sessions/${encodeURIComponent(id)}`,
|
|
3947
|
-
void 0,
|
|
3948
|
-
{ baseUrl: apiUrl, auth: { token: apiToken } }
|
|
3949
|
-
);
|
|
4752
|
+
return syncLocalManifest(ctx);
|
|
3950
4753
|
}
|
|
4754
|
+
var init_set_type = __esm({
|
|
4755
|
+
"src/commands/field/set-type.ts"() {
|
|
4756
|
+
init_api_client();
|
|
4757
|
+
init_schema_command_context();
|
|
4758
|
+
init_field_spec();
|
|
4759
|
+
init_output();
|
|
4760
|
+
init_create();
|
|
4761
|
+
}
|
|
4762
|
+
});
|
|
4763
|
+
|
|
4764
|
+
// src/bin.ts
|
|
4765
|
+
init_dev();
|
|
3951
4766
|
|
|
3952
4767
|
// src/commands/login.ts
|
|
3953
4768
|
init_api_client();
|
|
@@ -5534,6 +6349,7 @@ async function moduleInstallCommand(slugArg, options) {
|
|
|
5534
6349
|
if (isInstallGated(result)) {
|
|
5535
6350
|
if (options.json) {
|
|
5536
6351
|
out(JSON.stringify(result));
|
|
6352
|
+
if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
|
|
5537
6353
|
return 0;
|
|
5538
6354
|
}
|
|
5539
6355
|
success(
|
|
@@ -5542,6 +6358,7 @@ async function moduleInstallCommand(slugArg, options) {
|
|
|
5542
6358
|
info(
|
|
5543
6359
|
`This module requires author approval before it installs. Once approved, check with ${code(`dashwise module status ${mod.slug} --workspace ${workspace2.slug}`)}.`
|
|
5544
6360
|
);
|
|
6361
|
+
if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
|
|
5545
6362
|
return 0;
|
|
5546
6363
|
}
|
|
5547
6364
|
let row;
|
|
@@ -5566,6 +6383,7 @@ async function moduleInstallCommand(slugArg, options) {
|
|
|
5566
6383
|
verified
|
|
5567
6384
|
})
|
|
5568
6385
|
);
|
|
6386
|
+
if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
|
|
5569
6387
|
return 0;
|
|
5570
6388
|
}
|
|
5571
6389
|
if (!verified) {
|
|
@@ -5575,6 +6393,7 @@ async function moduleInstallCommand(slugArg, options) {
|
|
|
5575
6393
|
info(
|
|
5576
6394
|
`The verification read failed \u2014 run ${code(`dashwise module status ${mod.slug} --workspace ${workspace2.slug}`)} to see the deploy state and public URL.`
|
|
5577
6395
|
);
|
|
6396
|
+
if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
|
|
5578
6397
|
return 0;
|
|
5579
6398
|
}
|
|
5580
6399
|
if (runtimeProvisioned && publicUrl) {
|
|
@@ -5599,8 +6418,21 @@ async function moduleInstallCommand(slugArg, options) {
|
|
|
5599
6418
|
`No public URL yet \u2014 runtime provisioning didn't complete. Re-running ${code("dashwise module install")} is idempotent and repairs a failed provision.`
|
|
5600
6419
|
);
|
|
5601
6420
|
}
|
|
6421
|
+
if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
|
|
5602
6422
|
return 0;
|
|
5603
6423
|
}
|
|
6424
|
+
function publishedModuleLocalDevGuidance(slug) {
|
|
6425
|
+
return `Re-provision local dev any time with ${code(`dashwise init ${slug} --workspace <ws>`)} (or ${code("dashwise module reset-dev-install")}) \u2014 requires backend \u2265 2026-07-15; the fresh dev install COEXISTS with the production install.
|
|
6426
|
+
Older backends 409 SLUG_ALREADY_EXISTS on that path (no re-provision anywhere). Fallback for them (production repair):
|
|
6427
|
+
- Develop against the production install with a scoped key: ${code(`dashwise api-keys create local-dev --install <id> --full-access`)} + ${code(`dashwise origins add http://localhost:3000 --install <id>`)} (id via ${code(`dashwise module status ${slug}`)}), then run bare ${code("next dev")} \u2014 and CLEAN UP after (revoke the key, remove the origin).
|
|
6428
|
+
- Or duplicate the code under a NEW slug for greenfield iteration.`;
|
|
6429
|
+
}
|
|
6430
|
+
function printDevInstallAftermath(slug, workspaceSlug) {
|
|
6431
|
+
warn(
|
|
6432
|
+
`Local dev for ${code(slug)} is DISCONNECTED (the dev install in ${code(workspaceSlug)} and its ${code("dwk_")} key were destroyed).
|
|
6433
|
+
${publishedModuleLocalDevGuidance(slug)}`
|
|
6434
|
+
);
|
|
6435
|
+
}
|
|
5604
6436
|
function resolveSlug(slugArg, dir) {
|
|
5605
6437
|
if (slugArg) return slugArg;
|
|
5606
6438
|
try {
|
|
@@ -5621,11 +6453,14 @@ async function resolveDevInstallCollision(existing, slug, workspaceSlug, options
|
|
|
5621
6453
|
return 1;
|
|
5622
6454
|
}
|
|
5623
6455
|
const explanation = `Workspace ${code(workspaceSlug)} already has the DEV install of ${code(slug)} (kind=local, id=${existing.id}) created by ${code("dashwise init")}.
|
|
5624
|
-
|
|
6456
|
+
Install ${code("kind")} is immutable: a plain install could land on that row's upgrade path \u2014 the runtime would provision, but it would STAY a dev install, hidden from the app sidebar.
|
|
5625
6457
|
Options:
|
|
5626
|
-
- ${code("--replace-dev-install")} \u2014 destroy the dev install, then fresh production install. Recommended when development is done.
|
|
6458
|
+
- ${code("--replace-dev-install")} \u2014 destroy the dev install, then fresh production install. Recommended when development is done. Local dev can be re-provisioned afterwards with ${code(`dashwise init ${slug} --workspace <ws>`)} on backends \u2265 2026-07-15 (dev + production installs coexist); on OLDER backends this is one-way \u2014 no re-provision anywhere once published (interim: a scoped key on the production install via ${code("api-keys create --install <id> --full-access")}, or duplicate the code under a NEW slug).
|
|
5627
6459
|
- ${code("--workspace <other>")} \u2014 deploy to a separate test/client workspace and keep this dev install. Recommended for ongoing development.`;
|
|
5628
|
-
if (options.replaceDevInstall)
|
|
6460
|
+
if (options.replaceDevInstall) {
|
|
6461
|
+
info(explanation);
|
|
6462
|
+
return "replace";
|
|
6463
|
+
}
|
|
5629
6464
|
if (process.stdout.isTTY && !options.yes) {
|
|
5630
6465
|
info(explanation);
|
|
5631
6466
|
const answer = await select({
|
|
@@ -5634,7 +6469,7 @@ async function resolveDevInstallCollision(existing, slug, workspaceSlug, options
|
|
|
5634
6469
|
{
|
|
5635
6470
|
value: "replace",
|
|
5636
6471
|
label: "Replace \u2014 destroy the dev install, then production-install",
|
|
5637
|
-
hint: "local `dashwise
|
|
6472
|
+
hint: "revokes local-dev credentials; re-provision later via `dashwise init` (backend \u2265 2026-07-15 \u2014 older backends: scoped-key repair only)"
|
|
5638
6473
|
},
|
|
5639
6474
|
{ value: "cancel", label: "Cancel \u2014 I will deploy to another workspace" }
|
|
5640
6475
|
],
|
|
@@ -5670,9 +6505,7 @@ async function replaceDevInstall(existing, slug, auth) {
|
|
|
5670
6505
|
if (entry && entry.installationId === existing.id) {
|
|
5671
6506
|
try {
|
|
5672
6507
|
removeModuleEntry(slug);
|
|
5673
|
-
info(
|
|
5674
|
-
`Removed ${code(slug)} from auth.json \u2014 its local-dev credentials are revoked. Local ${code("dashwise dev")} needs ${code("dashwise init --force")} (ideally against a different workspace).`
|
|
5675
|
-
);
|
|
6508
|
+
info(`Removed ${code(slug)} from auth.json \u2014 its local-dev credentials are revoked.`);
|
|
5676
6509
|
} catch (err) {
|
|
5677
6510
|
warn(
|
|
5678
6511
|
`Dev install destroyed but auth.json cleanup failed: ${err.message}. Remove the ${code(`modules["${slug}"]`)} entry from ${code("~/.config/dashwise/auth.json")} manually.`
|
|
@@ -5721,7 +6554,8 @@ function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
|
|
|
5721
6554
|
}
|
|
5722
6555
|
if (devInstallReplaced) {
|
|
5723
6556
|
warn(
|
|
5724
|
-
`Note: the dev install was already removed (that is not rolled back). Re-run ${code("dashwise module install")} to complete the production install
|
|
6557
|
+
`Note: the dev install was already removed (that is not rolled back). Re-run ${code("dashwise module install")} to complete the production install.
|
|
6558
|
+
${publishedModuleLocalDevGuidance(slug)}`
|
|
5725
6559
|
);
|
|
5726
6560
|
}
|
|
5727
6561
|
return 1;
|
|
@@ -6214,7 +7048,131 @@ async function moduleStatusCommand(slugArg, options) {
|
|
|
6214
7048
|
}
|
|
6215
7049
|
if (!slug) {
|
|
6216
7050
|
fail(
|
|
6217
|
-
`No module slug \u2014 pass one (${code("dashwise module status <slug>")}) or run from a directory with a valid module.json.`
|
|
7051
|
+
`No module slug \u2014 pass one (${code("dashwise module status <slug>")}) or run from a directory with a valid module.json.`
|
|
7052
|
+
);
|
|
7053
|
+
return 1;
|
|
7054
|
+
}
|
|
7055
|
+
if (!isLoggedIn()) {
|
|
7056
|
+
fail(`Not logged in. Run ${code("dashwise login")} first.`);
|
|
7057
|
+
return 1;
|
|
7058
|
+
}
|
|
7059
|
+
const cfg = readConfig();
|
|
7060
|
+
if (!cfg) {
|
|
7061
|
+
fail(`CLI config missing. Run ${code("dashwise login")}.`);
|
|
7062
|
+
return 1;
|
|
7063
|
+
}
|
|
7064
|
+
const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
|
|
7065
|
+
let mod;
|
|
7066
|
+
try {
|
|
7067
|
+
mod = await getModuleBySlug(slug, auth);
|
|
7068
|
+
} catch (err) {
|
|
7069
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
7070
|
+
fail(`Module ${code(slug)} not found on the backend.`);
|
|
7071
|
+
return 1;
|
|
7072
|
+
}
|
|
7073
|
+
fail(`Could not resolve module ${code(slug)}: ${err.message}`);
|
|
7074
|
+
return 1;
|
|
7075
|
+
}
|
|
7076
|
+
let workspace2;
|
|
7077
|
+
try {
|
|
7078
|
+
workspace2 = await resolveWorkspaceWithFallback({
|
|
7079
|
+
userFlag: options.workspace,
|
|
7080
|
+
cachedWorkspaceId: readModuleEntry(slug)?.workspaceId,
|
|
7081
|
+
configDefault: cfg.workspaceSlug,
|
|
7082
|
+
apiUrl: resolveApiUrl(),
|
|
7083
|
+
apiToken: cfg.token
|
|
7084
|
+
});
|
|
7085
|
+
} catch (err) {
|
|
7086
|
+
fail(`Workspace selection failed: ${err.message}`);
|
|
7087
|
+
return 1;
|
|
7088
|
+
}
|
|
7089
|
+
let row;
|
|
7090
|
+
try {
|
|
7091
|
+
const installs = await listWorkspaceInstallations(workspace2.id, auth);
|
|
7092
|
+
row = installs.find((r) => r.module_id === mod.id);
|
|
7093
|
+
} catch (err) {
|
|
7094
|
+
fail(`Could not read workspace installations: ${err.message}`);
|
|
7095
|
+
return 1;
|
|
7096
|
+
}
|
|
7097
|
+
const latestVersion = row?.module?.current_version ?? mod.current_version;
|
|
7098
|
+
if (!row) {
|
|
7099
|
+
if (options.json) {
|
|
7100
|
+
out(
|
|
7101
|
+
JSON.stringify({
|
|
7102
|
+
installed: false,
|
|
7103
|
+
module: mod.slug,
|
|
7104
|
+
workspace: { id: workspace2.id, slug: workspace2.slug },
|
|
7105
|
+
latest_version: latestVersion
|
|
7106
|
+
})
|
|
7107
|
+
);
|
|
7108
|
+
return 0;
|
|
7109
|
+
}
|
|
7110
|
+
info(
|
|
7111
|
+
`${code(mod.slug)} is not installed in workspace ${code(workspace2.slug)}.` + (latestVersion ? ` Deploy it with ${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)}.` : ` It has no published version yet \u2014 run ${code("dashwise module publish")} first.`)
|
|
7112
|
+
);
|
|
7113
|
+
return 0;
|
|
7114
|
+
}
|
|
7115
|
+
const runtimeProvisioned = isRealRuntimeUrl(row.runtime_url);
|
|
7116
|
+
const state = runtimeProvisioned && row.public_url ? "live" : runtimeProvisioned ? "deployed" : "not_deployed";
|
|
7117
|
+
const installedVersion = row.module_version?.version ?? null;
|
|
7118
|
+
const updateAvailable = isUpdateAvailable(installedVersion, latestVersion);
|
|
7119
|
+
if (options.json) {
|
|
7120
|
+
out(
|
|
7121
|
+
JSON.stringify({
|
|
7122
|
+
installed: true,
|
|
7123
|
+
module: mod.slug,
|
|
7124
|
+
workspace: { id: workspace2.id, slug: workspace2.slug },
|
|
7125
|
+
installation_id: row.id,
|
|
7126
|
+
kind: row.kind ?? null,
|
|
7127
|
+
version: installedVersion,
|
|
7128
|
+
latest_version: latestVersion,
|
|
7129
|
+
update_available: updateAvailable,
|
|
7130
|
+
runtime_provisioned: runtimeProvisioned,
|
|
7131
|
+
runtime_url: row.runtime_url,
|
|
7132
|
+
public_url: row.public_url,
|
|
7133
|
+
state
|
|
7134
|
+
})
|
|
7135
|
+
);
|
|
7136
|
+
return 0;
|
|
7137
|
+
}
|
|
7138
|
+
const stateLabel = state === "live" ? "Live" : state === "deployed" ? "Deployed" : "Not deployed \u2014 runtime not provisioned";
|
|
7139
|
+
out("");
|
|
7140
|
+
kv("Module:", `${mod.slug} ${dim(`(id ${mod.id})`)}`);
|
|
7141
|
+
kv("Workspace:", `${workspace2.slug} ${dim(`(id ${workspace2.id})`)}`);
|
|
7142
|
+
kv("Install:", `#${row.id} ${dim(`(kind=${row.kind ?? "unknown"})`)}`);
|
|
7143
|
+
kv(
|
|
7144
|
+
"Version:",
|
|
7145
|
+
`${installedVersion ?? dim("(unknown)")}${updateAvailable ? ` ${code(`update available: v${latestVersion} \u2014 run \`dashwise module install ${mod.slug}\``)}` : latestVersion ? ` ${dim(`(latest published: v${latestVersion})`)}` : ""}`
|
|
7146
|
+
);
|
|
7147
|
+
kv(
|
|
7148
|
+
"Runtime:",
|
|
7149
|
+
runtimeProvisioned ? `provisioned ${dim(`(${row.runtime_url})`)}` : "not provisioned"
|
|
7150
|
+
);
|
|
7151
|
+
kv("Public URL:", row.public_url ?? dim("(none)"));
|
|
7152
|
+
kv("State:", stateLabel);
|
|
7153
|
+
out("");
|
|
7154
|
+
if (row.kind === "local") {
|
|
7155
|
+
info(
|
|
7156
|
+
`This is the ${code("kind=local")} dev install \u2014 hidden from the app sidebar. A production deploy needs ${code("dashwise module install --replace-dev-install")} (or ${code("--workspace <other>")}).`
|
|
7157
|
+
);
|
|
7158
|
+
} else if (state === "not_deployed") {
|
|
7159
|
+
info(
|
|
7160
|
+
`Re-run ${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)} to (re-)provision the runtime \u2014 the install POST is idempotent.`
|
|
7161
|
+
);
|
|
7162
|
+
}
|
|
7163
|
+
return 0;
|
|
7164
|
+
}
|
|
7165
|
+
|
|
7166
|
+
// src/commands/module/uninstall.ts
|
|
7167
|
+
init_api_client();
|
|
7168
|
+
init_config();
|
|
7169
|
+
init_manifest_io();
|
|
7170
|
+
init_output();
|
|
7171
|
+
async function moduleUninstallCommand(slugArg, options) {
|
|
7172
|
+
const slug = resolveSlug2(slugArg, options.dir);
|
|
7173
|
+
if (!slug) {
|
|
7174
|
+
fail(
|
|
7175
|
+
`No module slug \u2014 pass one (${code("dashwise module uninstall <slug>")}) or run from a directory with a valid module.json.`
|
|
6218
7176
|
);
|
|
6219
7177
|
return 1;
|
|
6220
7178
|
}
|
|
@@ -6233,7 +7191,7 @@ async function moduleStatusCommand(slugArg, options) {
|
|
|
6233
7191
|
mod = await getModuleBySlug(slug, auth);
|
|
6234
7192
|
} catch (err) {
|
|
6235
7193
|
if (err instanceof ApiError && err.status === 404) {
|
|
6236
|
-
fail(`Module ${code(slug)} not found on the backend.`);
|
|
7194
|
+
fail(`Module ${code(slug)} not found on the backend \u2014 nothing to uninstall.`);
|
|
6237
7195
|
return 1;
|
|
6238
7196
|
}
|
|
6239
7197
|
fail(`Could not resolve module ${code(slug)}: ${err.message}`);
|
|
@@ -6260,74 +7218,119 @@ async function moduleStatusCommand(slugArg, options) {
|
|
|
6260
7218
|
fail(`Could not read workspace installations: ${err.message}`);
|
|
6261
7219
|
return 1;
|
|
6262
7220
|
}
|
|
6263
|
-
const latestVersion = row?.module?.current_version ?? mod.current_version;
|
|
6264
7221
|
if (!row) {
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
7222
|
+
fail(
|
|
7223
|
+
`${code(mod.slug)} is not installed in workspace ${code(workspace2.slug)} \u2014 nothing to uninstall.`
|
|
7224
|
+
);
|
|
7225
|
+
return 1;
|
|
7226
|
+
}
|
|
7227
|
+
if (row.kind === "local") {
|
|
7228
|
+
fail(
|
|
7229
|
+
`Install ${code(`#${row.id}`)} of ${code(mod.slug)} in ${code(workspace2.slug)} is the ${code("kind=local")} DEV install backing local development.
|
|
7230
|
+
Use ${code("dashwise module destroy-dev-install")} (or ${code("dashwise module reset-dev-install")} for a clean re-provision) instead \u2014 it also revokes the cached local-dev credentials.`
|
|
7231
|
+
);
|
|
7232
|
+
return 1;
|
|
7233
|
+
}
|
|
7234
|
+
const scriptedOk = options.yes === true && options.workspace !== void 0;
|
|
7235
|
+
if (!scriptedOk) {
|
|
7236
|
+
if (!process.stdout.isTTY) {
|
|
7237
|
+
fail(
|
|
7238
|
+
`Refusing to uninstall without confirmation. In scripts, pass BOTH ${code("--yes")} AND an explicit ${code("--workspace <slug-or-id>")} (a bare ${code("--yes")} never uninstalls from an implicitly-resolved workspace).`
|
|
6273
7239
|
);
|
|
6274
|
-
return
|
|
7240
|
+
return 1;
|
|
6275
7241
|
}
|
|
6276
7242
|
info(
|
|
6277
|
-
|
|
7243
|
+
`Uninstalling ${code(mod.slug)} (install ${code(`#${row.id}`)}, kind=${row.kind ?? "production"}${row.module_version?.version ? `, v${row.module_version.version}` : ""}) from workspace ${code(workspace2.slug)}.
|
|
7244
|
+
The app is soft-deleted, recoverable ONLY by a PLATFORM OPERATOR (contact support/HQ) within ~7 days \u2014 it does NOT appear in your dashboard trash.
|
|
7245
|
+
An operator restore KEEPS the public URL${row.public_url ? ` ${code(row.public_url)}` : ""}; a REINSTALL mints a NEW one \u2014 deep links to the old URL break.`
|
|
6278
7246
|
);
|
|
6279
|
-
|
|
7247
|
+
const answer = await select({
|
|
7248
|
+
message: `Uninstall ${code(mod.slug)} from ${code(workspace2.slug)}?`,
|
|
7249
|
+
options: [
|
|
7250
|
+
{ value: "uninstall", label: "Uninstall \u2014 soft-delete the app (operator-restorable ~7 days)" },
|
|
7251
|
+
{ value: "cancel", label: "Cancel \u2014 nothing is changed" }
|
|
7252
|
+
],
|
|
7253
|
+
initialValue: "cancel"
|
|
7254
|
+
});
|
|
7255
|
+
if (isCancel(answer) || answer === "cancel") {
|
|
7256
|
+
fail("Aborted. Nothing was changed.");
|
|
7257
|
+
return 130;
|
|
7258
|
+
}
|
|
7259
|
+
const typed = await text({
|
|
7260
|
+
message: `Type the module slug (${code(mod.slug)}) to confirm:`
|
|
7261
|
+
});
|
|
7262
|
+
if (isCancel(typed) || typed !== mod.slug) {
|
|
7263
|
+
fail(
|
|
7264
|
+
`Confirmation did not match ${code(mod.slug)}. Aborted \u2014 nothing was changed.`
|
|
7265
|
+
);
|
|
7266
|
+
return 130;
|
|
7267
|
+
}
|
|
6280
7268
|
}
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
out(
|
|
6287
|
-
JSON.stringify({
|
|
6288
|
-
installed: true,
|
|
6289
|
-
module: mod.slug,
|
|
6290
|
-
workspace: { id: workspace2.id, slug: workspace2.slug },
|
|
6291
|
-
installation_id: row.id,
|
|
6292
|
-
kind: row.kind ?? null,
|
|
6293
|
-
version: installedVersion,
|
|
6294
|
-
latest_version: latestVersion,
|
|
6295
|
-
update_available: updateAvailable,
|
|
6296
|
-
runtime_provisioned: runtimeProvisioned,
|
|
6297
|
-
runtime_url: row.runtime_url,
|
|
6298
|
-
public_url: row.public_url,
|
|
6299
|
-
state
|
|
6300
|
-
})
|
|
7269
|
+
try {
|
|
7270
|
+
await uninstallModuleInstallation(
|
|
7271
|
+
row.id,
|
|
7272
|
+
{ ...options.parkConsumers ? { parkConsumers: true } : {} },
|
|
7273
|
+
auth
|
|
6301
7274
|
);
|
|
6302
|
-
|
|
7275
|
+
} catch (err) {
|
|
7276
|
+
return surfaceUninstallError(err, mod.slug, workspace2.slug);
|
|
6303
7277
|
}
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
kv("Module:", `${mod.slug} ${dim(`(id ${mod.id})`)}`);
|
|
6307
|
-
kv("Workspace:", `${workspace2.slug} ${dim(`(id ${workspace2.id})`)}`);
|
|
6308
|
-
kv("Install:", `#${row.id} ${dim(`(kind=${row.kind ?? "unknown"})`)}`);
|
|
6309
|
-
kv(
|
|
6310
|
-
"Version:",
|
|
6311
|
-
`${installedVersion ?? dim("(unknown)")}${updateAvailable ? ` ${code(`update available: v${latestVersion} \u2014 run \`dashwise module install ${mod.slug}\``)}` : latestVersion ? ` ${dim(`(latest published: v${latestVersion})`)}` : ""}`
|
|
7278
|
+
success(
|
|
7279
|
+
`Uninstalled ${code(mod.slug)} (install ${code(`#${row.id}`)}) from workspace ${code(workspace2.slug)}.`
|
|
6312
7280
|
);
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
7281
|
+
info(
|
|
7282
|
+
`The app was soft-deleted. If this was a mistake: only a PLATFORM OPERATOR can restore it (contact support/HQ) within ~7 days \u2014 it does NOT appear in your dashboard trash. An operator restore keeps the same public URL.
|
|
7283
|
+
Reinstalling instead (${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)}) mints a NEW public URL.`
|
|
6316
7284
|
);
|
|
6317
|
-
|
|
6318
|
-
kv("State:", stateLabel);
|
|
6319
|
-
out("");
|
|
6320
|
-
if (row.kind === "local") {
|
|
6321
|
-
info(
|
|
6322
|
-
`This is the ${code("kind=local")} dev install \u2014 hidden from the app sidebar. A production deploy needs ${code("dashwise module install --replace-dev-install")} (or ${code("--workspace <other>")}).`
|
|
6323
|
-
);
|
|
6324
|
-
} else if (state === "not_deployed") {
|
|
7285
|
+
if (options.parkConsumers) {
|
|
6325
7286
|
info(
|
|
6326
|
-
`
|
|
7287
|
+
`Consumer modules that depended on this install were parked (reason PROVIDER_UNINSTALLED) \u2014 they keep running with the dependency unbound until re-bound.`
|
|
6327
7288
|
);
|
|
6328
7289
|
}
|
|
6329
7290
|
return 0;
|
|
6330
7291
|
}
|
|
7292
|
+
function resolveSlug2(slugArg, dir) {
|
|
7293
|
+
if (slugArg) return slugArg;
|
|
7294
|
+
try {
|
|
7295
|
+
const manifest = readManifest(dir);
|
|
7296
|
+
const slug = manifest?.module?.slug;
|
|
7297
|
+
return typeof slug === "string" && slug.length > 0 ? slug : void 0;
|
|
7298
|
+
} catch {
|
|
7299
|
+
return void 0;
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
function surfaceUninstallError(err, slug, workspaceSlug) {
|
|
7303
|
+
if (err instanceof ApiError) {
|
|
7304
|
+
if (err.status === 404 || err.code === "NOT_FOUND" || err.code === "INSTALLATION_NOT_FOUND") {
|
|
7305
|
+
warn(`The install is already gone server-side \u2014 nothing to do.`);
|
|
7306
|
+
return 0;
|
|
7307
|
+
}
|
|
7308
|
+
switch (err.code) {
|
|
7309
|
+
case "PROVIDER_HAS_CONSUMERS":
|
|
7310
|
+
fail(
|
|
7311
|
+
`${code(slug)} provides data to other installed modules \u2014 uninstalling it would break them.
|
|
7312
|
+
Re-run with ${code("--park-consumers")} to uninstall anyway: each consumer's dependency is parked (reason PROVIDER_UNINSTALLED) and the consumers keep running with that dependency unbound.`
|
|
7313
|
+
);
|
|
7314
|
+
break;
|
|
7315
|
+
case "NOT_WORKSPACE_ADMIN":
|
|
7316
|
+
case "FORBIDDEN":
|
|
7317
|
+
fail(
|
|
7318
|
+
`You must be an ADMIN of workspace ${code(workspaceSlug)} to uninstall modules from it.`
|
|
7319
|
+
);
|
|
7320
|
+
break;
|
|
7321
|
+
case "UNAUTHENTICATED":
|
|
7322
|
+
fail(
|
|
7323
|
+
`Auth rejected by the backend \u2014 your CLI token may be stale. Re-run ${code("dashwise login")} and try again.`
|
|
7324
|
+
);
|
|
7325
|
+
break;
|
|
7326
|
+
default:
|
|
7327
|
+
fail(`Uninstall failed (${err.code} ${err.status}): ${err.message}`);
|
|
7328
|
+
}
|
|
7329
|
+
return 1;
|
|
7330
|
+
}
|
|
7331
|
+
fail(`Uninstall failed: ${err.message}`);
|
|
7332
|
+
return 1;
|
|
7333
|
+
}
|
|
6331
7334
|
|
|
6332
7335
|
// src/commands/module/pull-data.ts
|
|
6333
7336
|
init_api_client();
|
|
@@ -7099,7 +8102,7 @@ function loadContext(opts) {
|
|
|
7099
8102
|
const entry = readModuleEntry(slug);
|
|
7100
8103
|
if (!entry) {
|
|
7101
8104
|
throw new Error(
|
|
7102
|
-
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}
|
|
8105
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}\` to provision one (existing files are preserved).`
|
|
7103
8106
|
);
|
|
7104
8107
|
}
|
|
7105
8108
|
return {
|
|
@@ -7324,7 +8327,7 @@ function loadContext2(opts) {
|
|
|
7324
8327
|
const entry = readModuleEntry(slug);
|
|
7325
8328
|
if (!entry) {
|
|
7326
8329
|
throw new Error(
|
|
7327
|
-
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}
|
|
8330
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}\` to provision one (existing files are preserved).`
|
|
7328
8331
|
);
|
|
7329
8332
|
}
|
|
7330
8333
|
return {
|
|
@@ -7916,7 +8919,7 @@ function loadContext3(opts) {
|
|
|
7916
8919
|
const entry = readModuleEntry(moduleSlug);
|
|
7917
8920
|
if (!entry) {
|
|
7918
8921
|
throw new Error(
|
|
7919
|
-
`No dev install cached for module "${moduleSlug}". Run \`dashwise init ${moduleSlug}
|
|
8922
|
+
`No dev install cached for module "${moduleSlug}". Run \`dashwise init ${moduleSlug}\` to provision one (existing files are preserved).`
|
|
7920
8923
|
);
|
|
7921
8924
|
}
|
|
7922
8925
|
return {
|
|
@@ -8112,17 +9115,17 @@ function surfacePullFailure(err) {
|
|
|
8112
9115
|
switch (err.code) {
|
|
8113
9116
|
case "UNAUTHENTICATED":
|
|
8114
9117
|
fail(
|
|
8115
|
-
`Runtime token rejected. The cached token for this module may be expired \u2014 re-run \`dashwise init <slug
|
|
9118
|
+
`Runtime token rejected. The cached token for this module may be expired \u2014 re-run \`dashwise init <slug>\` to mint a fresh one (existing files are preserved).`
|
|
8116
9119
|
);
|
|
8117
9120
|
break;
|
|
8118
9121
|
case "SANDBOX_TOKEN_NOT_ACCEPTED":
|
|
8119
9122
|
fail(
|
|
8120
|
-
`Schema-read endpoint requires a local-dev runtime token (kind=local). The cached token is the wrong kind \u2014 re-run \`dashwise init <slug
|
|
9123
|
+
`Schema-read endpoint requires a local-dev runtime token (kind=local). The cached token is the wrong kind \u2014 re-run \`dashwise init <slug>\`.`
|
|
8121
9124
|
);
|
|
8122
9125
|
break;
|
|
8123
9126
|
case "INSTALL_MISMATCH":
|
|
8124
9127
|
fail(
|
|
8125
|
-
`Runtime token is bound to a different installation than expected. The cached install id may be stale \u2014 re-run \`dashwise init <slug
|
|
9128
|
+
`Runtime token is bound to a different installation than expected. The cached install id may be stale \u2014 re-run \`dashwise init <slug>\`.`
|
|
8126
9129
|
);
|
|
8127
9130
|
break;
|
|
8128
9131
|
case "INSTALLATION_NOT_FOUND":
|
|
@@ -8148,6 +9151,32 @@ init_config();
|
|
|
8148
9151
|
init_manifest_io();
|
|
8149
9152
|
init_output();
|
|
8150
9153
|
function loadContext4(opts) {
|
|
9154
|
+
const cfg = readConfig();
|
|
9155
|
+
if (!cfg) {
|
|
9156
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
9157
|
+
}
|
|
9158
|
+
const requestOptions = {
|
|
9159
|
+
baseUrl: resolveApiUrl(),
|
|
9160
|
+
auth: { token: cfg.token }
|
|
9161
|
+
};
|
|
9162
|
+
if (opts.install !== void 0) {
|
|
9163
|
+
if (!Number.isInteger(opts.install) || opts.install <= 0) {
|
|
9164
|
+
throw new Error(
|
|
9165
|
+
`--install expects a positive integer installation id (got "${opts.install}").`
|
|
9166
|
+
);
|
|
9167
|
+
}
|
|
9168
|
+
let slug2;
|
|
9169
|
+
try {
|
|
9170
|
+
slug2 = readManifest(opts.dir)?.module?.slug;
|
|
9171
|
+
} catch {
|
|
9172
|
+
}
|
|
9173
|
+
return {
|
|
9174
|
+
installationId: opts.install,
|
|
9175
|
+
slug: slug2 ?? `install#${opts.install}`,
|
|
9176
|
+
installOverride: true,
|
|
9177
|
+
requestOptions
|
|
9178
|
+
};
|
|
9179
|
+
}
|
|
8151
9180
|
const manifest = readManifestOrThrow(opts.dir);
|
|
8152
9181
|
const slug = manifest.module?.slug;
|
|
8153
9182
|
if (!slug) {
|
|
@@ -8155,25 +9184,36 @@ function loadContext4(opts) {
|
|
|
8155
9184
|
`module.json has no module.slug \u2014 the manifest is malformed.`
|
|
8156
9185
|
);
|
|
8157
9186
|
}
|
|
8158
|
-
const cfg = readConfig();
|
|
8159
|
-
if (!cfg) {
|
|
8160
|
-
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
8161
|
-
}
|
|
8162
9187
|
const entry = readModuleEntry(slug);
|
|
8163
9188
|
if (!entry) {
|
|
8164
9189
|
throw new Error(
|
|
8165
|
-
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}
|
|
9190
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}\` to provision one (existing files are preserved), or pass \`--install <id>\` to target any installation (production included) \u2014 find ids via \`dashwise module status ${slug}\`.`
|
|
8166
9191
|
);
|
|
8167
9192
|
}
|
|
8168
9193
|
return {
|
|
8169
9194
|
installationId: entry.installationId,
|
|
8170
9195
|
slug,
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
auth: { token: cfg.token }
|
|
8174
|
-
}
|
|
9196
|
+
installOverride: false,
|
|
9197
|
+
requestOptions
|
|
8175
9198
|
};
|
|
8176
9199
|
}
|
|
9200
|
+
async function resolveInstallKind(ctx) {
|
|
9201
|
+
if (!ctx.installOverride) return "local";
|
|
9202
|
+
const entry = readModuleEntry(ctx.slug);
|
|
9203
|
+
if (entry && entry.installationId === ctx.installationId) return "local";
|
|
9204
|
+
if (entry?.workspaceId !== void 0) {
|
|
9205
|
+
try {
|
|
9206
|
+
const rows = await listWorkspaceInstallations(
|
|
9207
|
+
entry.workspaceId,
|
|
9208
|
+
ctx.requestOptions
|
|
9209
|
+
);
|
|
9210
|
+
const row = rows.find((r) => r.id === ctx.installationId);
|
|
9211
|
+
if (row) return row.kind ?? "production";
|
|
9212
|
+
} catch {
|
|
9213
|
+
}
|
|
9214
|
+
}
|
|
9215
|
+
return void 0;
|
|
9216
|
+
}
|
|
8177
9217
|
async function apiKeysListCommand(options) {
|
|
8178
9218
|
let ctx;
|
|
8179
9219
|
try {
|
|
@@ -8201,6 +9241,7 @@ async function apiKeysListCommand(options) {
|
|
|
8201
9241
|
);
|
|
8202
9242
|
return 0;
|
|
8203
9243
|
}
|
|
9244
|
+
const installKind = await resolveInstallKind(ctx);
|
|
8204
9245
|
info(`API keys for ${code(ctx.slug)} (install id=${ctx.installationId}):`);
|
|
8205
9246
|
for (const k of filtered) {
|
|
8206
9247
|
const status = formatKeyStatus(k);
|
|
@@ -8216,7 +9257,8 @@ async function apiKeysListCommand(options) {
|
|
|
8216
9257
|
`);
|
|
8217
9258
|
}
|
|
8218
9259
|
} else if (k.grants !== void 0) {
|
|
8219
|
-
|
|
9260
|
+
const label = installKind === "local" ? "full own-table access (local dev install)" : "NO access \u2014 grantless key on a non-local install (mint with --grant or --full-access)";
|
|
9261
|
+
process.stdout.write(` ${dim(label)}
|
|
8220
9262
|
`);
|
|
8221
9263
|
}
|
|
8222
9264
|
}
|
|
@@ -8228,6 +9270,12 @@ async function apiKeysCreateCommand(nameArg, options) {
|
|
|
8228
9270
|
fail("Usage: `dashwise api-keys create <name>` (e.g. `dashwise api-keys create production`)");
|
|
8229
9271
|
return 1;
|
|
8230
9272
|
}
|
|
9273
|
+
if (options.fullAccess && options.grant && options.grant.length > 0) {
|
|
9274
|
+
fail(
|
|
9275
|
+
`${code("--full-access")} and ${code("--grant")} are mutually exclusive \u2014 pass one or the other.`
|
|
9276
|
+
);
|
|
9277
|
+
return 1;
|
|
9278
|
+
}
|
|
8231
9279
|
let grants;
|
|
8232
9280
|
try {
|
|
8233
9281
|
grants = parseGrants(options.grant);
|
|
@@ -8249,8 +9297,18 @@ async function apiKeysCreateCommand(nameArg, options) {
|
|
|
8249
9297
|
fail(err.message);
|
|
8250
9298
|
return 1;
|
|
8251
9299
|
}
|
|
9300
|
+
const installKind = await resolveInstallKind(ctx);
|
|
9301
|
+
const grantless = grants === void 0 && options.fullAccess !== true;
|
|
9302
|
+
if (grantless && installKind !== "local") {
|
|
9303
|
+
warn(
|
|
9304
|
+
`MINTING A DENY-ALL KEY: install ${code(String(ctx.installationId))} is ${installKind ? `kind=${installKind}` : "not the local dev install"}, and a grantless key on a non-local install can access NOTHING \u2014 every data-plane call will 403.
|
|
9305
|
+
Pass ${code("--full-access")} for full-CRUD over the module's own tables, or ${code("--grant <spec>")} for least-privilege scopes.
|
|
9306
|
+
(Grantless = full own-table access is a ${code("kind=local")} dev-install convenience only.)`
|
|
9307
|
+
);
|
|
9308
|
+
}
|
|
8252
9309
|
const body = { name };
|
|
8253
9310
|
if (grants !== void 0) body.grants = grants;
|
|
9311
|
+
if (options.fullAccess === true) body.full_own_tables = true;
|
|
8254
9312
|
if (expiresAt !== void 0) body.expires_at = expiresAt;
|
|
8255
9313
|
let resp;
|
|
8256
9314
|
try {
|
|
@@ -8274,8 +9332,12 @@ async function apiKeysCreateCommand(nameArg, options) {
|
|
|
8274
9332
|
if (grantLines.length > 0) {
|
|
8275
9333
|
info(` ${dim("scope:")} scoped to`);
|
|
8276
9334
|
for (const line of grantLines) info(` ${line}`);
|
|
9335
|
+
} else if (installKind === "local") {
|
|
9336
|
+
info(` ${dim("scope:")} full own-table access (grantless key on the local dev install)`);
|
|
8277
9337
|
} else {
|
|
8278
|
-
info(
|
|
9338
|
+
info(
|
|
9339
|
+
` ${dim("scope:")} NO access \u2014 grantless key on a non-local install (re-mint with ${code("--full-access")} or ${code("--grant")})`
|
|
9340
|
+
);
|
|
8279
9341
|
}
|
|
8280
9342
|
info("");
|
|
8281
9343
|
warn(
|
|
@@ -8347,6 +9409,11 @@ function surfaceApiKeysError(err) {
|
|
|
8347
9409
|
`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.`
|
|
8348
9410
|
);
|
|
8349
9411
|
return 1;
|
|
9412
|
+
case "GRANTS_CONFLICT":
|
|
9413
|
+
fail(
|
|
9414
|
+
`--full-access and --grant are mutually exclusive \u2014 pass one or the other.`
|
|
9415
|
+
);
|
|
9416
|
+
return 1;
|
|
8350
9417
|
case "INSTALLATION_NOT_FOUND":
|
|
8351
9418
|
fail(
|
|
8352
9419
|
`Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
|
|
@@ -8501,6 +9568,31 @@ init_config();
|
|
|
8501
9568
|
init_manifest_io();
|
|
8502
9569
|
init_output();
|
|
8503
9570
|
function loadContext5(opts) {
|
|
9571
|
+
const cfg = readConfig();
|
|
9572
|
+
if (!cfg) {
|
|
9573
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
9574
|
+
}
|
|
9575
|
+
const requestOptions = {
|
|
9576
|
+
baseUrl: resolveApiUrl(),
|
|
9577
|
+
auth: { token: cfg.token }
|
|
9578
|
+
};
|
|
9579
|
+
if (opts.install !== void 0) {
|
|
9580
|
+
if (!Number.isInteger(opts.install) || opts.install <= 0) {
|
|
9581
|
+
throw new Error(
|
|
9582
|
+
`--install expects a positive integer installation id (got "${opts.install}").`
|
|
9583
|
+
);
|
|
9584
|
+
}
|
|
9585
|
+
let slug2;
|
|
9586
|
+
try {
|
|
9587
|
+
slug2 = readManifest(opts.dir)?.module?.slug;
|
|
9588
|
+
} catch {
|
|
9589
|
+
}
|
|
9590
|
+
return {
|
|
9591
|
+
installationId: opts.install,
|
|
9592
|
+
slug: slug2 ?? `install#${opts.install}`,
|
|
9593
|
+
requestOptions
|
|
9594
|
+
};
|
|
9595
|
+
}
|
|
8504
9596
|
const manifest = readManifestOrThrow(opts.dir);
|
|
8505
9597
|
const slug = manifest.module?.slug;
|
|
8506
9598
|
if (!slug) {
|
|
@@ -8508,23 +9600,16 @@ function loadContext5(opts) {
|
|
|
8508
9600
|
`module.json has no module.slug \u2014 the manifest is malformed.`
|
|
8509
9601
|
);
|
|
8510
9602
|
}
|
|
8511
|
-
const cfg = readConfig();
|
|
8512
|
-
if (!cfg) {
|
|
8513
|
-
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
8514
|
-
}
|
|
8515
9603
|
const entry = readModuleEntry(slug);
|
|
8516
9604
|
if (!entry) {
|
|
8517
9605
|
throw new Error(
|
|
8518
|
-
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}
|
|
9606
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}\` to provision one (existing files are preserved), or pass \`--install <id>\` to target any installation (production included) \u2014 find ids via \`dashwise module status ${slug}\`.`
|
|
8519
9607
|
);
|
|
8520
9608
|
}
|
|
8521
9609
|
return {
|
|
8522
9610
|
installationId: entry.installationId,
|
|
8523
9611
|
slug,
|
|
8524
|
-
requestOptions
|
|
8525
|
-
baseUrl: resolveApiUrl(),
|
|
8526
|
-
auth: { token: cfg.token }
|
|
8527
|
-
}
|
|
9612
|
+
requestOptions
|
|
8528
9613
|
};
|
|
8529
9614
|
}
|
|
8530
9615
|
function normalizeOrigin(raw) {
|
|
@@ -8719,7 +9804,7 @@ async function envExportCommand(options) {
|
|
|
8719
9804
|
const entry = readModuleEntry(slug);
|
|
8720
9805
|
if (!entry) {
|
|
8721
9806
|
fail(
|
|
8722
|
-
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}
|
|
9807
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}\` to provision one (existing files are preserved).`
|
|
8723
9808
|
);
|
|
8724
9809
|
return 1;
|
|
8725
9810
|
}
|
|
@@ -8826,12 +9911,16 @@ function shellQuote(value) {
|
|
|
8826
9911
|
}
|
|
8827
9912
|
|
|
8828
9913
|
// src/commands/query.ts
|
|
9914
|
+
init_config();
|
|
9915
|
+
init_dotenv_local();
|
|
8829
9916
|
init_manifest_io();
|
|
8830
9917
|
init_output();
|
|
9918
|
+
init_dev();
|
|
8831
9919
|
async function queryCommand(options) {
|
|
8832
9920
|
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
9921
|
+
let manifestSlug;
|
|
8833
9922
|
try {
|
|
8834
|
-
readManifestOrThrow(projectRoot);
|
|
9923
|
+
manifestSlug = readManifestOrThrow(projectRoot).module?.slug;
|
|
8835
9924
|
} catch (err) {
|
|
8836
9925
|
fail(err.message);
|
|
8837
9926
|
return 1;
|
|
@@ -8849,6 +9938,32 @@ async function queryCommand(options) {
|
|
|
8849
9938
|
return 1;
|
|
8850
9939
|
}
|
|
8851
9940
|
hydrateEnvFromDotenvLocal(projectRoot);
|
|
9941
|
+
if (!process.env.DASHWISE_API_KEY && manifestSlug) {
|
|
9942
|
+
const cfg = (() => {
|
|
9943
|
+
try {
|
|
9944
|
+
return readConfig();
|
|
9945
|
+
} catch {
|
|
9946
|
+
return null;
|
|
9947
|
+
}
|
|
9948
|
+
})();
|
|
9949
|
+
const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
|
|
9950
|
+
let entry = readModuleEntry(manifestSlug);
|
|
9951
|
+
if (cfg) {
|
|
9952
|
+
const healed = await resolveModuleApiKey(
|
|
9953
|
+
manifestSlug,
|
|
9954
|
+
entry,
|
|
9955
|
+
apiUrl,
|
|
9956
|
+
cfg.token
|
|
9957
|
+
);
|
|
9958
|
+
if (healed) entry = healed;
|
|
9959
|
+
}
|
|
9960
|
+
if (entry?.apiKey) {
|
|
9961
|
+
process.env.DASHWISE_API_KEY = entry.apiKey;
|
|
9962
|
+
if (!process.env.DASHWISE_API_URL) {
|
|
9963
|
+
process.env.DASHWISE_API_URL = apiUrl;
|
|
9964
|
+
}
|
|
9965
|
+
}
|
|
9966
|
+
}
|
|
8852
9967
|
const modes = [
|
|
8853
9968
|
options.inline !== void 0 ? "inline" : null,
|
|
8854
9969
|
options.file !== void 0 ? "file" : null,
|
|
@@ -9025,25 +10140,6 @@ function generatedEntryUrl(generatedDir) {
|
|
|
9025
10140
|
async function dynamicImportFromPath(generatedDir) {
|
|
9026
10141
|
return await import(generatedEntryUrl(generatedDir));
|
|
9027
10142
|
}
|
|
9028
|
-
function hydrateEnvFromDotenvLocal(projectRoot) {
|
|
9029
|
-
const envFile = join(projectRoot, ".env.local");
|
|
9030
|
-
if (!existsSync(envFile)) return;
|
|
9031
|
-
const raw = readFileSync(envFile, "utf-8");
|
|
9032
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
9033
|
-
const trimmed = line.trim();
|
|
9034
|
-
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
9035
|
-
const eq = trimmed.indexOf("=");
|
|
9036
|
-
if (eq <= 0) continue;
|
|
9037
|
-
const key = trimmed.slice(0, eq).trim();
|
|
9038
|
-
let value = trimmed.slice(eq + 1).trim();
|
|
9039
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
9040
|
-
value = value.slice(1, -1);
|
|
9041
|
-
}
|
|
9042
|
-
if (process.env[key] === void 0) {
|
|
9043
|
-
process.env[key] = value;
|
|
9044
|
-
}
|
|
9045
|
-
}
|
|
9046
|
-
}
|
|
9047
10143
|
|
|
9048
10144
|
// src/commands/module/query-log.ts
|
|
9049
10145
|
init_api_client();
|
|
@@ -9487,7 +10583,7 @@ var initOptions = (cmd) => cmd.option("--dir <path>", "Override the target direc
|
|
|
9487
10583
|
"Target workspace for the dev installation (slug or numeric id). When omitted in a TTY, the CLI prompts; in non-interactive mode it auto-picks when you have one workspace and errors out otherwise."
|
|
9488
10584
|
).option(
|
|
9489
10585
|
"--no-provision",
|
|
9490
|
-
"Skip the `POST /api/installations/dev` step. Local scaffold is still written;
|
|
10586
|
+
"Skip the `POST /api/installations/dev` step. Local scaffold is still written; provision later by re-running `dashwise init <slug>` in the project dir (existing files are preserved \u2014 only missing ones are written)."
|
|
9491
10587
|
).addOption(
|
|
9492
10588
|
new Option(
|
|
9493
10589
|
"--deploy-target <platform>",
|
|
@@ -9754,6 +10850,14 @@ program.command("query").description(
|
|
|
9754
10850
|
});
|
|
9755
10851
|
}
|
|
9756
10852
|
);
|
|
10853
|
+
program.command("run [cmd...]").description(
|
|
10854
|
+
"Run a command with the module's DashWise env injected \u2014 resolves DASHWISE_API_URL + DASHWISE_API_KEY through the same chain as `dashwise query` (explicit env \u2192 .env.local \u2192 the cached dwk_ key) and spawns the command with them overlaid, inheriting stdio and propagating the exit code. Example: `dashwise run -- node seed.mjs`."
|
|
10855
|
+
).option("--dir <path>", "Project root containing `module.json` (default: current directory).").action(async (cmdArgs, cmdOpts) => {
|
|
10856
|
+
const { runCommand: runCommand2 } = await Promise.resolve().then(() => (init_run(), run_exports));
|
|
10857
|
+
process.exitCode = await runCommand2(cmdArgs ?? [], {
|
|
10858
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
10859
|
+
});
|
|
10860
|
+
});
|
|
9757
10861
|
program.command("pull").description(
|
|
9758
10862
|
"Sync local module.json from the backend. Fetches the canonical manifest for the current install and writes it to disk. Inverse of `dashwise table/field` commands."
|
|
9759
10863
|
).option("--dir <path>", "Project root containing `module.json` (default: current directory).").option("--dry-run", "Fetch + report drift but do not write to module.json.").action(async (cmdOpts) => {
|
|
@@ -9767,20 +10871,32 @@ var apiKeysCmd = program.command("api-keys").description(
|
|
|
9767
10871
|
);
|
|
9768
10872
|
apiKeysCmd.command("list").description(
|
|
9769
10873
|
"List API keys for the current module's installation. Active only by default; pass --all for revoked too."
|
|
9770
|
-
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10874
|
+
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10875
|
+
"--install <id>",
|
|
10876
|
+
"Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
|
|
10877
|
+
parseIntOption
|
|
10878
|
+
).option("--all", "Include revoked + expired keys in the output.").option("--json", "Emit JSON instead of the human-friendly table.").action(async (cmdOpts) => {
|
|
9771
10879
|
process.exitCode = await apiKeysListCommand({
|
|
9772
10880
|
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10881
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
|
|
9773
10882
|
...cmdOpts.all !== void 0 ? { all: cmdOpts.all } : {},
|
|
9774
10883
|
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
9775
10884
|
});
|
|
9776
10885
|
});
|
|
9777
10886
|
apiKeysCmd.command("create [name]").description(
|
|
9778
10887
|
"Mint a new API key. The plaintext key is displayed ONCE \u2014 save it then."
|
|
9779
|
-
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10888
|
+
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10889
|
+
"--install <id>",
|
|
10890
|
+
"Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
|
|
10891
|
+
parseIntOption
|
|
10892
|
+
).option("--name <name>", "Override the positional name arg (e.g. for scripted use).").option(
|
|
9780
10893
|
"--grant <spec>",
|
|
9781
|
-
"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).
|
|
10894
|
+
"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). Mutually exclusive with --full-access.",
|
|
9782
10895
|
collectRepeated,
|
|
9783
10896
|
[]
|
|
10897
|
+
).option(
|
|
10898
|
+
"--full-access",
|
|
10899
|
+
"Mint with server-enumerated full-CRUD grants over every table the pinned manifest owns \u2014 the honest deploy-key default (a grantless key on a NON-local install is deny-all). Mutually exclusive with --grant."
|
|
9784
10900
|
).option(
|
|
9785
10901
|
"--expires <days>",
|
|
9786
10902
|
"Days until the key expires (converted to an absolute timestamp). Default: never expires.",
|
|
@@ -9792,8 +10908,10 @@ apiKeysCmd.command("create [name]").description(
|
|
|
9792
10908
|
async (nameArg, cmdOpts) => {
|
|
9793
10909
|
process.exitCode = await apiKeysCreateCommand(nameArg, {
|
|
9794
10910
|
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10911
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
|
|
9795
10912
|
...cmdOpts.name !== void 0 ? { name: cmdOpts.name } : {},
|
|
9796
10913
|
...cmdOpts.grant !== void 0 && cmdOpts.grant.length > 0 ? { grant: cmdOpts.grant } : {},
|
|
10914
|
+
...cmdOpts.fullAccess !== void 0 ? { fullAccess: cmdOpts.fullAccess } : {},
|
|
9797
10915
|
...cmdOpts.expires !== void 0 ? { expires: cmdOpts.expires } : {},
|
|
9798
10916
|
...cmdOpts.rawOnly !== void 0 ? { rawOnly: cmdOpts.rawOnly } : {}
|
|
9799
10917
|
});
|
|
@@ -9801,10 +10919,15 @@ apiKeysCmd.command("create [name]").description(
|
|
|
9801
10919
|
);
|
|
9802
10920
|
apiKeysCmd.command("revoke [key-id]").description(
|
|
9803
10921
|
"Soft-revoke an API key. Backend keeps the row for audit; further requests return 401."
|
|
9804
|
-
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10922
|
+
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10923
|
+
"--install <id>",
|
|
10924
|
+
"Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
|
|
10925
|
+
parseIntOption
|
|
10926
|
+
).option("-y, --yes", "Skip the per-key confirmation message (for CI / scripts).").action(
|
|
9805
10927
|
async (keyIdArg, cmdOpts) => {
|
|
9806
10928
|
process.exitCode = await apiKeysRevokeCommand(keyIdArg, {
|
|
9807
10929
|
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10930
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
|
|
9808
10931
|
...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
|
|
9809
10932
|
});
|
|
9810
10933
|
}
|
|
@@ -9812,22 +10935,37 @@ apiKeysCmd.command("revoke [key-id]").description(
|
|
|
9812
10935
|
var originsCmd = program.command("origins").description(
|
|
9813
10936
|
"Manage the current module install's SSO callback-origin allowlist. Local dev (localhost) is always allowed; add deployed domains here."
|
|
9814
10937
|
);
|
|
9815
|
-
originsCmd.command("list").description("Show the SSO callback origins configured for the current module install.").option("--dir <path>", "Project root (default: current directory).").option(
|
|
10938
|
+
originsCmd.command("list").description("Show the SSO callback origins configured for the current module install.").option("--dir <path>", "Project root (default: current directory).").option(
|
|
10939
|
+
"--install <id>",
|
|
10940
|
+
"Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
|
|
10941
|
+
parseIntOption
|
|
10942
|
+
).option("--json", "Emit a JSON array instead of the human-friendly list.").action(async (cmdOpts) => {
|
|
9816
10943
|
process.exitCode = await originsListCommand({
|
|
9817
10944
|
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10945
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
|
|
9818
10946
|
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
9819
10947
|
});
|
|
9820
10948
|
});
|
|
9821
10949
|
originsCmd.command("add <url>").description(
|
|
9822
10950
|
"Add an SSO callback origin (e.g. https://app.example.com). Idempotent; non-localhost origins must be https://."
|
|
9823
|
-
).option("--dir <path>", "Project root (default: current directory).").
|
|
10951
|
+
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10952
|
+
"--install <id>",
|
|
10953
|
+
"Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
|
|
10954
|
+
parseIntOption
|
|
10955
|
+
).action(async (url, cmdOpts) => {
|
|
9824
10956
|
process.exitCode = await originsAddCommand(url, {
|
|
9825
|
-
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
10957
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10958
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {}
|
|
9826
10959
|
});
|
|
9827
10960
|
});
|
|
9828
|
-
originsCmd.command("remove <url>").description("Remove an SSO callback origin from the allowlist.").option("--dir <path>", "Project root (default: current directory).").
|
|
10961
|
+
originsCmd.command("remove <url>").description("Remove an SSO callback origin from the allowlist.").option("--dir <path>", "Project root (default: current directory).").option(
|
|
10962
|
+
"--install <id>",
|
|
10963
|
+
"Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
|
|
10964
|
+
parseIntOption
|
|
10965
|
+
).action(async (url, cmdOpts) => {
|
|
9829
10966
|
process.exitCode = await originsRemoveCommand(url, {
|
|
9830
|
-
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
10967
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10968
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {}
|
|
9831
10969
|
});
|
|
9832
10970
|
});
|
|
9833
10971
|
var envCmd = program.command("env").description("Environment-variable helpers for deploying a module.");
|
|
@@ -9881,7 +11019,7 @@ moduleCmd.command("install [slug]").alias("deploy").description(
|
|
|
9881
11019
|
"Install (deploy) a published module into a workspace \u2014 provisions the runtime and prints the public URL. Upgrades/repairs an existing production install idempotently. Refuses to silently land on a dev install: use --replace-dev-install or --workspace <other>."
|
|
9882
11020
|
).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--version <semver>", "Install a specific published version (default: latest).").option("--force", "Proceed even when the upgrade breaks consumer modules (UPGRADE_BREAKS_CONSUMERS).").option(
|
|
9883
11021
|
"--replace-dev-install",
|
|
9884
|
-
"Destroy the workspace's kind=local dev install first, then fresh production install.
|
|
11022
|
+
"Destroy the workspace's kind=local dev install first, then fresh production install. PERMANENTLY disables local dev for this module in this workspace (credentials revoked; a published module cannot be re-provisioned here) \u2014 continue development in a different workspace."
|
|
9885
11023
|
).option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("-y, --yes", "Skip the confirmation prompt (for CI / scripted use).").option("--json", "Emit the final result as machine-readable JSON.").action(
|
|
9886
11024
|
async (slug, cmdOpts) => {
|
|
9887
11025
|
process.exitCode = await moduleInstallCommand(slug, {
|
|
@@ -9895,6 +11033,21 @@ moduleCmd.command("install [slug]").alias("deploy").description(
|
|
|
9895
11033
|
});
|
|
9896
11034
|
}
|
|
9897
11035
|
);
|
|
11036
|
+
moduleCmd.command("uninstall [slug]").description(
|
|
11037
|
+
"Uninstall a module from a workspace. The app is soft-deleted \u2014 recoverable ONLY by a platform operator (contact support/HQ) within ~7 days; it does NOT appear in your dashboard trash. An operator restore keeps the public URL; a reinstall mints a NEW one. Refuses the kind=local dev install: use `dashwise module destroy-dev-install` for that. Interactive runs confirm twice (including typing the slug); scripts must pass BOTH --yes and an explicit --workspace."
|
|
11038
|
+
).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option(
|
|
11039
|
+
"--park-consumers",
|
|
11040
|
+
"Uninstall even when live consumer modules depend on this install \u2014 their dependencies are parked (PROVIDER_UNINSTALLED) instead of blocking with PROVIDER_HAS_CONSUMERS."
|
|
11041
|
+
).option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("-y, --yes", "Skip the confirmation prompts. Only honored together with an explicit --workspace.").action(
|
|
11042
|
+
async (slug, cmdOpts) => {
|
|
11043
|
+
process.exitCode = await moduleUninstallCommand(slug, {
|
|
11044
|
+
...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
|
|
11045
|
+
...cmdOpts.parkConsumers !== void 0 ? { parkConsumers: cmdOpts.parkConsumers } : {},
|
|
11046
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
11047
|
+
...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
|
|
11048
|
+
});
|
|
11049
|
+
}
|
|
11050
|
+
);
|
|
9898
11051
|
moduleCmd.command("status [slug]").description(
|
|
9899
11052
|
"Show a module's deploy state in a workspace: install kind, installed vs latest published version, runtime provisioning, and the public URL."
|
|
9900
11053
|
).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("--json", "Emit machine-readable JSON.").action(
|
|
@@ -9978,7 +11131,7 @@ moduleCmd.command("destroy-dev-install").description(
|
|
|
9978
11131
|
}
|
|
9979
11132
|
);
|
|
9980
11133
|
moduleCmd.command("reset-dev-install").description(
|
|
9981
|
-
"Destroy + re-provision the local-dev install in one shot. Useful for nuking accumulated test data, refreshing a compromised runtime token, or recovering from a wedged schema state.
|
|
11134
|
+
"Destroy + re-provision the local-dev install in one shot. Useful for nuking accumulated test data, refreshing a compromised runtime token, or recovering from a wedged schema state. Provision-only re-init: ALL local files (module.json, app code) are left untouched \u2014 only the backend install + cached credentials are recreated."
|
|
9982
11135
|
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
9983
11136
|
"--module <slug>",
|
|
9984
11137
|
"Override the module slug (default: from local module.json)."
|
|
@@ -9996,6 +11149,18 @@ moduleCmd.command("reset-dev-install").description(
|
|
|
9996
11149
|
});
|
|
9997
11150
|
}
|
|
9998
11151
|
);
|
|
11152
|
+
var schemaCmd = program.command("schema").description(
|
|
11153
|
+
"Whole-manifest schema commands. `schema push` applies the local module.json to the dev install (the missing write half of the sync loop \u2014 hand-edited rbac/indexes/table blocks finally reach the backend)."
|
|
11154
|
+
);
|
|
11155
|
+
schemaCmd.command("push").description(
|
|
11156
|
+
"Apply the LOCAL module.json to the dev install via PUT /api/modules/:id/draft/manifest (greenfield or delta). Destructive removals (tables/fields) are previewed and require confirmation unless --yes. Afterwards the canonical manifest is synced back with merge semantics (local-only rbac/indexes blocks survive)."
|
|
11157
|
+
).option("--dir <path>", "Project root containing `module.json` (default: current directory).").option("-y, --yes", "Skip the destructive-change confirmation (CI / scripted runs).").action(async (cmdOpts) => {
|
|
11158
|
+
const { schemaPushCommand: schemaPushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
|
|
11159
|
+
process.exitCode = await schemaPushCommand2({
|
|
11160
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
11161
|
+
...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
|
|
11162
|
+
});
|
|
11163
|
+
});
|
|
9999
11164
|
var tableCmd = program.command("table").description(
|
|
10000
11165
|
"Schema commands for the current module \u2014 create/delete tables in the dev installation. Requires a per-module dev install (run `dashwise init` first)."
|
|
10001
11166
|
);
|
|
@@ -10033,7 +11198,7 @@ var fieldCmd = program.command("field").description(
|
|
|
10033
11198
|
"Schema commands for fields within the current module \u2014 add/remove/change-type. Field rename is deferred to v1.1 per the plan."
|
|
10034
11199
|
);
|
|
10035
11200
|
fieldCmd.command("add <table-slug> [field-spec]").description(
|
|
10036
|
-
'Add a field to a table. Pass the spec as `<slug>:<type>[:required]` OR use --field-json for fields with options/target/default.
|
|
11201
|
+
'Add a field to a table. Pass the spec as `<slug>:<type>[:required]` OR use --field-json for fields with options/target/default (never both \u2014 the slug lives inside the JSON). Examples: `dashwise field add tasks priority:text` or `dashwise field add tasks --field-json \'{"slug":"priority","type":"single_select","options":["low","medium","high"]}\'`.'
|
|
10037
11202
|
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10038
11203
|
"--field-json <json>",
|
|
10039
11204
|
"Field as a JSON object. Mutually exclusive with the positional field spec."
|
|
@@ -10083,7 +11248,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
|
|
|
10083
11248
|
}
|
|
10084
11249
|
})();
|
|
10085
11250
|
function getVersion() {
|
|
10086
|
-
return "0.
|
|
11251
|
+
return "0.16.0";
|
|
10087
11252
|
}
|
|
10088
11253
|
function parseIntOption(value) {
|
|
10089
11254
|
const n = Number.parseInt(value, 10);
|