@dashai/cli 0.14.2 → 0.15.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 +2315 -1298
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
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,136 +627,736 @@ 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
|
-
|
|
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
|
+
if (explicitInstallationId && explicitInstallationId !== "local") {
|
|
844
|
+
installationIdEnv = explicitInstallationId;
|
|
845
|
+
} else if (reuseDevSessionId) ; else {
|
|
846
|
+
const effectiveWorkspace = options.workspace ?? moduleEntry?.workspaceId?.toString();
|
|
847
|
+
let workspaceContext;
|
|
848
|
+
try {
|
|
849
|
+
workspaceContext = await resolveTargetWorkspace(
|
|
850
|
+
apiUrl,
|
|
851
|
+
apiToken,
|
|
852
|
+
effectiveWorkspace
|
|
853
|
+
);
|
|
854
|
+
} catch (err) {
|
|
855
|
+
fail(err.message);
|
|
856
|
+
return 1;
|
|
857
|
+
}
|
|
858
|
+
const callbackOrigins = computeCallbackOrigins(port, options.callbackOrigin ?? []);
|
|
859
|
+
try {
|
|
860
|
+
const session = await registerDevSession(apiUrl, apiToken, {
|
|
861
|
+
target_workspace_id: workspaceContext.id,
|
|
862
|
+
callback_origins: callbackOrigins
|
|
863
|
+
});
|
|
864
|
+
revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session.id);
|
|
865
|
+
info(` ${dim("dev session")} ${session.id}`);
|
|
866
|
+
info(` ${dim("workspace")} ${workspaceContext.name} (id=${workspaceContext.id}, slug=${workspaceContext.slug})`);
|
|
867
|
+
info(` ${dim("callbacks")} ${callbackOrigins.join(", ")}`);
|
|
868
|
+
info(` ${dim("expires")} ${session.expires_at}`);
|
|
869
|
+
} catch (err) {
|
|
870
|
+
if (err instanceof ApiError) {
|
|
871
|
+
fail(`Failed to register dev session: ${err.code} \u2014 ${err.message}`);
|
|
872
|
+
} else {
|
|
873
|
+
fail(`Failed to register dev session: ${err.message}`);
|
|
874
|
+
}
|
|
875
|
+
return 1;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
const childEnv = {
|
|
879
|
+
...process.env,
|
|
880
|
+
DASHWISE_API_URL: apiUrl,
|
|
881
|
+
NEXT_PUBLIC_DASHWISE_API_URL: apiUrl,
|
|
882
|
+
...apiKeyEnv !== void 0 ? { DASHWISE_API_KEY: apiKeyEnv } : {}
|
|
883
|
+
};
|
|
884
|
+
info(`Dev loop starting \u2014 Ctrl-C to stop.`);
|
|
885
|
+
info(` ${dim("api")} ${apiUrl}`);
|
|
886
|
+
if (installationIdEnv !== void 0) {
|
|
887
|
+
info(` ${dim("installation")} ${installationIdEnv} (prod-mode SSO)`);
|
|
888
|
+
} else {
|
|
889
|
+
info(` ${dim("mode")} dev (dev SSO session registered)`);
|
|
890
|
+
}
|
|
891
|
+
info(` ${dim("cli token")} ${maskedToken(apiToken)} ${dim(`(from ${apiToken === savedConfig?.token ? "~/.config/dashwise/auth.json" : "$DASHWISE_API_TOKEN"})`)}`);
|
|
892
|
+
if (apiKeyEnv !== void 0) {
|
|
893
|
+
const source = process.env.DASHWISE_API_KEY === apiKeyEnv ? "$DASHWISE_API_KEY" : `~/.config/dashwise/auth.json#modules[${manifestSlug}]`;
|
|
894
|
+
const installId = moduleEntry?.installationId;
|
|
895
|
+
info(
|
|
896
|
+
` ${dim("api key")} ${maskedToken(apiKeyEnv)} ${dim(`(from ${source}${installId ? `, install id=${installId}` : ""})`)}`
|
|
897
|
+
);
|
|
898
|
+
} else if (manifestSlug !== void 0 && installationIdEnv === void 0) {
|
|
899
|
+
fail(
|
|
900
|
+
`No ${code("dwk_")} API key for module ${code(manifestSlug)} \u2014 the module will run with NO token.`
|
|
901
|
+
);
|
|
902
|
+
info(
|
|
903
|
+
` Data-plane calls (db, qb) AND sign-in will fail until you provision a dev install:
|
|
904
|
+
${code(`dashwise init ${manifestSlug} --workspace <your-workspace-slug>`)}
|
|
905
|
+
(Ensure you're logged into the right backend: ${code(`dashwise login --api-url ${apiUrl}`)}.)`
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
info("");
|
|
909
|
+
if (moduleEntry !== null && manifestSlug !== void 0) {
|
|
910
|
+
await reportConnectStatus({
|
|
911
|
+
installationId: moduleEntry.installationId,
|
|
912
|
+
moduleSlug: manifestSlug,
|
|
913
|
+
requestOptions: { baseUrl: apiUrl, auth: { token: apiToken } }
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
const children = [];
|
|
917
|
+
if (!options.nextOnly) {
|
|
918
|
+
const sdkChild = spawnSdkWatcher(projectRoot, childEnv);
|
|
919
|
+
children.push({ name: "sdk", child: sdkChild, prefix: PREFIX_SDK });
|
|
920
|
+
}
|
|
921
|
+
if (!options.sdkOnly) {
|
|
922
|
+
const nextChild = spawnNextDev(projectRoot, childEnv, port);
|
|
923
|
+
children.push({ name: "next", child: nextChild, prefix: PREFIX_NEXT });
|
|
924
|
+
}
|
|
925
|
+
for (const { child, prefix } of children) {
|
|
926
|
+
pipeWithPrefix(child.stdout, process.stdout, prefix);
|
|
927
|
+
pipeWithPrefix(child.stderr, process.stderr, prefix);
|
|
928
|
+
}
|
|
929
|
+
let manifestPoll = null;
|
|
930
|
+
const pollEntry = moduleEntry;
|
|
931
|
+
const enablePoll = !options.noRemotePoll && pollEntry !== null && apiKeyEnv !== void 0 && manifestSlug !== void 0;
|
|
932
|
+
if (enablePoll && pollEntry !== null) {
|
|
933
|
+
const intervalSec = Math.max(
|
|
934
|
+
MIN_REMOTE_POLL_INTERVAL_SEC,
|
|
935
|
+
options.remotePollIntervalSec ?? DEFAULT_REMOTE_POLL_INTERVAL_SEC
|
|
936
|
+
);
|
|
937
|
+
manifestPoll = startManifestPollLoop({
|
|
938
|
+
manifestPath: manifestPath2,
|
|
939
|
+
requestOptions: {
|
|
940
|
+
baseUrl: apiUrl,
|
|
941
|
+
// The `dwk_` key resolves the install server-side.
|
|
942
|
+
auth: { token: apiKeyEnv }
|
|
943
|
+
},
|
|
944
|
+
intervalMs: intervalSec * 1e3,
|
|
945
|
+
onUpdate: (msg) => {
|
|
946
|
+
process.stdout.write(`${PREFIX_POLL}${msg}
|
|
947
|
+
`);
|
|
948
|
+
},
|
|
949
|
+
onError: (err) => {
|
|
950
|
+
process.stderr.write(`${PREFIX_POLL}${err.message}
|
|
951
|
+
`);
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
info(
|
|
955
|
+
` ${dim("poll")} backend manifest every ${intervalSec}s ${dim("(--no-remote-poll to disable)")}`
|
|
956
|
+
);
|
|
957
|
+
info("");
|
|
958
|
+
}
|
|
959
|
+
let shuttingDown = false;
|
|
960
|
+
const forwardSignal = (sig) => {
|
|
961
|
+
if (shuttingDown) return;
|
|
962
|
+
shuttingDown = true;
|
|
963
|
+
if (manifestPoll) {
|
|
964
|
+
manifestPoll.stop();
|
|
965
|
+
manifestPoll = null;
|
|
966
|
+
}
|
|
967
|
+
info(`
|
|
968
|
+
Received ${sig}, stopping children\u2026`);
|
|
969
|
+
for (const { child } of children) {
|
|
970
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
971
|
+
child.kill(sig);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (revokeDevSession) {
|
|
975
|
+
revokeDevSession().catch(() => {
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
|
980
|
+
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
|
981
|
+
const results = await Promise.all(
|
|
982
|
+
children.map(
|
|
983
|
+
({ name, child }) => new Promise(
|
|
984
|
+
(resolveExit) => {
|
|
985
|
+
child.on("exit", (exitCode2, signal) => {
|
|
986
|
+
const killed = signal !== null;
|
|
987
|
+
const finalCode = exitCode2 ?? (killed ? 0 : 1);
|
|
988
|
+
resolveExit({ name, code: finalCode, killed });
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
)
|
|
992
|
+
)
|
|
993
|
+
);
|
|
994
|
+
const crashed = results.find((r) => !r.killed && r.code !== 0);
|
|
995
|
+
if (crashed && !shuttingDown) {
|
|
996
|
+
warn(`[${crashed.name}] exited with code ${crashed.code} \u2014 stopping others.`);
|
|
997
|
+
for (const { child } of children) {
|
|
998
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
999
|
+
child.kill("SIGTERM");
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (manifestPoll) {
|
|
1004
|
+
manifestPoll.stop();
|
|
1005
|
+
manifestPoll = null;
|
|
1006
|
+
}
|
|
1007
|
+
const exitCode = Math.max(0, ...results.map((r) => r.code));
|
|
1008
|
+
if (exitCode === 0) {
|
|
1009
|
+
success("Dev loop stopped cleanly.");
|
|
1010
|
+
}
|
|
1011
|
+
return exitCode;
|
|
1012
|
+
}
|
|
1013
|
+
function spawnSdkWatcher(projectRoot, env) {
|
|
1014
|
+
const child = spawn(
|
|
1015
|
+
process.execPath,
|
|
1016
|
+
[process.argv[1], "module", "dev", "--dir", projectRoot],
|
|
1017
|
+
{
|
|
1018
|
+
cwd: projectRoot,
|
|
1019
|
+
env,
|
|
1020
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1021
|
+
}
|
|
1022
|
+
);
|
|
1023
|
+
return child;
|
|
1024
|
+
}
|
|
1025
|
+
function spawnNextDev(projectRoot, env, port) {
|
|
1026
|
+
const portArgs = port !== void 0 ? ["--", "--port", String(port)] : [];
|
|
1027
|
+
const hasDevNextScript = hasNpmScript(projectRoot, "dev:next");
|
|
1028
|
+
const [cmd, args] = hasDevNextScript ? ["npm", ["run", "dev:next", ...portArgs]] : ["npx", ["next", "dev", ...port !== void 0 ? ["--port", String(port)] : []]];
|
|
1029
|
+
const child = spawn(cmd, args, {
|
|
1030
|
+
cwd: projectRoot,
|
|
1031
|
+
env,
|
|
1032
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1033
|
+
// Windows requires `shell: true` for `npm` / `npx` because they're
|
|
1034
|
+
// `.cmd` shims, not real binaries.
|
|
1035
|
+
shell: process.platform === "win32"
|
|
1036
|
+
});
|
|
1037
|
+
return child;
|
|
1038
|
+
}
|
|
1039
|
+
function hasNpmScript(projectRoot, scriptName) {
|
|
1040
|
+
try {
|
|
1041
|
+
const pkgPath = `${projectRoot}/package.json`;
|
|
1042
|
+
if (!existsSync(pkgPath)) return false;
|
|
1043
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1044
|
+
return typeof pkg.scripts?.[scriptName] === "string";
|
|
1045
|
+
} catch {
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function pipeWithPrefix(src, dst, prefix) {
|
|
1050
|
+
if (!src) return;
|
|
1051
|
+
let leftover = "";
|
|
1052
|
+
src.on("data", (chunk) => {
|
|
1053
|
+
const text4 = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
1054
|
+
const combined = leftover + text4;
|
|
1055
|
+
const lines = combined.split("\n");
|
|
1056
|
+
leftover = lines.pop() ?? "";
|
|
1057
|
+
for (const line of lines) {
|
|
1058
|
+
dst.write(`${prefix}${line}
|
|
1059
|
+
`);
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
src.on("end", () => {
|
|
1063
|
+
if (leftover.length > 0) {
|
|
1064
|
+
dst.write(`${prefix}${leftover}
|
|
1065
|
+
`);
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
function maskedToken(token) {
|
|
1070
|
+
if (token.length <= 12) return "****";
|
|
1071
|
+
const prefix = token.slice(0, 4);
|
|
1072
|
+
const tail = token.slice(-4);
|
|
1073
|
+
return `${prefix}\u2026${tail}`;
|
|
1074
|
+
}
|
|
1075
|
+
async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
|
|
1076
|
+
if (process.env.DASHWISE_API_KEY) return null;
|
|
1077
|
+
if (!entry) return null;
|
|
1078
|
+
if (entry.apiKey) return null;
|
|
1079
|
+
if (!entry.installationId) return null;
|
|
1080
|
+
info(
|
|
1081
|
+
`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`
|
|
1082
|
+
);
|
|
1083
|
+
let minted;
|
|
1084
|
+
try {
|
|
1085
|
+
minted = await createApiKey(
|
|
1086
|
+
entry.installationId,
|
|
1087
|
+
{ name: "local-dev" },
|
|
1088
|
+
{ baseUrl: apiUrl, auth: { token: cliBearer } }
|
|
1089
|
+
);
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
if (err instanceof ApiError && err.code === "INSTALLATION_NOT_FOUND") {
|
|
1092
|
+
warn(
|
|
1093
|
+
`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).`
|
|
1094
|
+
);
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
const reason = err instanceof ApiError ? `${err.code}: ${err.message}` : err.message;
|
|
1098
|
+
warn(
|
|
1099
|
+
`Could not upgrade credentials for ${code(slug)} (${reason}). Data-plane calls may fail; run ${code(`dashwise init ${slug}`)} to refresh (files are preserved).`
|
|
1100
|
+
);
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
const upgraded = {
|
|
1104
|
+
apiKey: minted.raw_key,
|
|
1105
|
+
apiKeyId: minted.api_key.id,
|
|
1106
|
+
installationId: entry.installationId,
|
|
1107
|
+
expiresAt: minted.api_key.expires_at ?? entry.expiresAt
|
|
1108
|
+
};
|
|
1109
|
+
try {
|
|
1110
|
+
upsertModuleEntry(slug, upgraded);
|
|
1111
|
+
} catch (err) {
|
|
1112
|
+
warn(
|
|
1113
|
+
`Minted a new API key for ${code(slug)} but failed to cache it: ${err.message}
|
|
1114
|
+
API key (save manually): ${minted.raw_key}`
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
success(`Upgraded ${code(slug)} to a dwk_ API key (id=${minted.api_key.id}).`);
|
|
1118
|
+
return upgraded;
|
|
1119
|
+
}
|
|
1120
|
+
function computeCallbackOrigins(port, extra) {
|
|
1121
|
+
const def = `http://localhost:${port}`;
|
|
1122
|
+
return Array.from(/* @__PURE__ */ new Set([def, ...extra]));
|
|
1123
|
+
}
|
|
1124
|
+
async function resolveDevPort(requested) {
|
|
1125
|
+
if (requested !== void 0) {
|
|
1126
|
+
if (await isPortFree(requested)) return requested;
|
|
1127
|
+
const suggestion = await findFirstFreePort(requested + 1, PORT_PROBE_LIMIT);
|
|
1128
|
+
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}\`.`;
|
|
1129
|
+
throw new Error(
|
|
1130
|
+
`Port ${requested} is already in use. ${suggestionHint}`
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
const found = await findFirstFreePort(DEFAULT_NEXT_PORT, PORT_PROBE_LIMIT);
|
|
1134
|
+
if (found === null) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
`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.`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
return found;
|
|
1140
|
+
}
|
|
1141
|
+
async function isPortFree(port) {
|
|
1142
|
+
const { createServer } = await import('net');
|
|
1143
|
+
return new Promise((resolve13) => {
|
|
1144
|
+
const server = createServer();
|
|
1145
|
+
server.once("error", () => {
|
|
1146
|
+
resolve13(false);
|
|
1147
|
+
});
|
|
1148
|
+
server.once("listening", () => {
|
|
1149
|
+
server.close(() => resolve13(true));
|
|
1150
|
+
});
|
|
1151
|
+
server.listen(port, "0.0.0.0");
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
async function findFirstFreePort(start, limit) {
|
|
1155
|
+
for (let i = 0; i < limit; i++) {
|
|
1156
|
+
const port = start + i;
|
|
1157
|
+
if (await isPortFree(port)) return port;
|
|
1158
|
+
}
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
async function resolveTargetWorkspace(apiUrl, apiToken, flag) {
|
|
1162
|
+
const workspaces = await apiRequest(
|
|
1163
|
+
"GET",
|
|
1164
|
+
"/api/workspaces",
|
|
1165
|
+
void 0,
|
|
1166
|
+
{ baseUrl: apiUrl, auth: { token: apiToken } }
|
|
1167
|
+
);
|
|
1168
|
+
if (workspaces.length === 0) {
|
|
1169
|
+
throw new Error(
|
|
1170
|
+
"No workspaces found for your account. Create a workspace at the DashWise dashboard first."
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
if (flag === void 0) {
|
|
1174
|
+
if (workspaces.length === 1) {
|
|
1175
|
+
return workspaces[0];
|
|
1176
|
+
}
|
|
1177
|
+
const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id}, name="${w.name}")`).join("\n");
|
|
1178
|
+
throw new Error(
|
|
1179
|
+
`You have ${workspaces.length} workspaces. Re-run with --workspace <slug-or-id> to pick one:
|
|
1180
|
+
${list}`
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
const asNumber = Number.parseInt(flag, 10);
|
|
1184
|
+
const isNumeric = !Number.isNaN(asNumber) && String(asNumber) === flag;
|
|
1185
|
+
const match = isNumeric ? workspaces.find((w) => w.id === asNumber) : workspaces.find((w) => w.slug === flag);
|
|
1186
|
+
if (!match) {
|
|
1187
|
+
const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id})`).join("\n");
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
`No workspace matching --workspace=${flag}. Available:
|
|
1190
|
+
${list}`
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
return match;
|
|
1194
|
+
}
|
|
1195
|
+
async function registerDevSession(apiUrl, apiToken, body) {
|
|
1196
|
+
return apiRequest(
|
|
1197
|
+
"POST",
|
|
1198
|
+
"/api/auth/dev/sessions",
|
|
1199
|
+
body,
|
|
1200
|
+
{ baseUrl: apiUrl, auth: { token: apiToken } }
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
async function revokeDevSessionBestEffort(apiUrl, apiToken, id) {
|
|
1204
|
+
await apiRequest(
|
|
1205
|
+
"DELETE",
|
|
1206
|
+
`/api/auth/dev/sessions/${encodeURIComponent(id)}`,
|
|
1207
|
+
void 0,
|
|
1208
|
+
{ baseUrl: apiUrl, auth: { token: apiToken } }
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
var DEFAULT_NEXT_PORT, PREFIX_SDK, PREFIX_NEXT, PREFIX_POLL, DEFAULT_REMOTE_POLL_INTERVAL_SEC, MIN_REMOTE_POLL_INTERVAL_SEC, PORT_PROBE_LIMIT;
|
|
1212
|
+
var init_dev = __esm({
|
|
1213
|
+
"src/commands/dev.ts"() {
|
|
1214
|
+
init_api_client();
|
|
1215
|
+
init_connect_status();
|
|
1216
|
+
init_config();
|
|
1217
|
+
init_manifest_io();
|
|
1218
|
+
init_manifest_poll();
|
|
1219
|
+
init_output();
|
|
1220
|
+
DEFAULT_NEXT_PORT = 3e3;
|
|
1221
|
+
PREFIX_SDK = `\x1B[36m[sdk]\x1B[0m `;
|
|
1222
|
+
PREFIX_NEXT = `\x1B[35m[next]\x1B[0m `;
|
|
1223
|
+
PREFIX_POLL = `\x1B[33m[poll]\x1B[0m `;
|
|
1224
|
+
DEFAULT_REMOTE_POLL_INTERVAL_SEC = 5;
|
|
1225
|
+
MIN_REMOTE_POLL_INTERVAL_SEC = 1;
|
|
1226
|
+
PORT_PROBE_LIMIT = 10;
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
// src/lib/scaffold.ts
|
|
1231
|
+
function isDeployTarget(value) {
|
|
1232
|
+
return DEPLOY_TARGETS.includes(value);
|
|
1233
|
+
}
|
|
1234
|
+
function manifestJson(opts) {
|
|
1235
|
+
const kind = opts.kind ?? "hand_authored";
|
|
1236
|
+
const manifest = {
|
|
1237
|
+
schema_version: 2,
|
|
1238
|
+
module: {
|
|
1239
|
+
slug: opts.slug,
|
|
1240
|
+
name: opts.name,
|
|
1241
|
+
version: "0.0.1"
|
|
1242
|
+
},
|
|
1243
|
+
module_kind: kind,
|
|
1244
|
+
owns: {
|
|
1245
|
+
tables: [
|
|
1246
|
+
{
|
|
1247
|
+
// NOTE: no `name` key here — the backend manifest validator
|
|
1248
|
+
// (POST /api/installations/dev) is strict and rejects unrecognized
|
|
1249
|
+
// table keys, and it derives the display name from the slug. A
|
|
1250
|
+
// `name` here would fail provisioning's table materialization (#4).
|
|
1251
|
+
slug: "items",
|
|
1252
|
+
fields: [
|
|
1253
|
+
{ slug: "title", type: "text" },
|
|
1254
|
+
{ slug: "done", type: "boolean" }
|
|
1255
|
+
]
|
|
1256
|
+
}
|
|
1257
|
+
]
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
if (kind === "custom") {
|
|
1261
|
+
manifest.runtime = {
|
|
1262
|
+
type: "nextjs",
|
|
1263
|
+
kind: "machine",
|
|
1264
|
+
machine_size: "shared-cpu-1x",
|
|
1265
|
+
server_actions: true
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
if (opts.repositoryUrl) {
|
|
1269
|
+
manifest.repository = { type: "git", url: opts.repositoryUrl };
|
|
1270
|
+
}
|
|
1271
|
+
return JSON.stringify(manifest, null, 2) + "\n";
|
|
1272
|
+
}
|
|
1273
|
+
function packageJson(opts) {
|
|
1274
|
+
return JSON.stringify(
|
|
1275
|
+
{
|
|
1276
|
+
name: opts.slug,
|
|
1277
|
+
version: "0.0.1",
|
|
1278
|
+
private: true,
|
|
1279
|
+
type: "module",
|
|
1280
|
+
scripts: {
|
|
1281
|
+
dev: "dashwise module dev",
|
|
1282
|
+
"dev:next": "next dev",
|
|
1283
|
+
"dashwise:generate": "dashwise module generate",
|
|
1284
|
+
build: "next build",
|
|
1285
|
+
start: "next start"
|
|
1286
|
+
},
|
|
1287
|
+
dependencies: {
|
|
1288
|
+
"@dashai/sdk": "^2.0.0",
|
|
1289
|
+
next: "^15.0.0",
|
|
1290
|
+
react: "^19.0.0",
|
|
1291
|
+
"react-dom": "^19.0.0"
|
|
1292
|
+
},
|
|
1293
|
+
devDependencies: {
|
|
1294
|
+
"@types/node": "^22.0.0",
|
|
1295
|
+
"@types/react": "^19.0.0",
|
|
1296
|
+
"@types/react-dom": "^19.0.0",
|
|
1297
|
+
typescript: "^5.5.0"
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
null,
|
|
1301
|
+
2
|
|
1302
|
+
) + "\n";
|
|
1303
|
+
}
|
|
1304
|
+
function tsconfigJson() {
|
|
1305
|
+
return JSON.stringify(
|
|
1306
|
+
{
|
|
1307
|
+
compilerOptions: {
|
|
1308
|
+
target: "ES2022",
|
|
1309
|
+
lib: ["dom", "dom.iterable", "ES2022"],
|
|
1310
|
+
module: "ESNext",
|
|
1311
|
+
moduleResolution: "Bundler",
|
|
1312
|
+
jsx: "preserve",
|
|
1313
|
+
strict: true,
|
|
1314
|
+
skipLibCheck: true,
|
|
1315
|
+
resolveJsonModule: true,
|
|
1316
|
+
esModuleInterop: true,
|
|
1317
|
+
isolatedModules: true,
|
|
1318
|
+
allowJs: false,
|
|
1319
|
+
noEmit: true,
|
|
1320
|
+
incremental: true,
|
|
1321
|
+
plugins: [{ name: "next" }],
|
|
1322
|
+
paths: { "@/*": ["./*"] }
|
|
1323
|
+
},
|
|
1324
|
+
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
1325
|
+
exclude: ["node_modules"]
|
|
1326
|
+
},
|
|
1327
|
+
null,
|
|
1328
|
+
2
|
|
1329
|
+
) + "\n";
|
|
1330
|
+
}
|
|
1331
|
+
function nextConfig() {
|
|
1332
|
+
return `/** @type {import("next").NextConfig} */
|
|
1333
|
+
//
|
|
1334
|
+
// Phase 2 two-var contract: the deploy env supplies exactly
|
|
1335
|
+
// DASHWISE_API_URL + DASHWISE_API_KEY. The browser needs the API URL
|
|
1336
|
+
// too (the SDK's <AuthProvider> calls /api/auth/sessions/me on it), so
|
|
1337
|
+
// we DERIVE NEXT_PUBLIC_DASHWISE_API_URL here from DASHWISE_API_URL at
|
|
1338
|
+
// build/start time \u2014 you never set the NEXT_PUBLIC_ var yourself. If
|
|
1339
|
+
// you already have one set (e.g. a custom setup), it wins.
|
|
1340
|
+
const apiUrl = process.env.DASHWISE_API_URL;
|
|
1341
|
+
|
|
1342
|
+
const nextConfig = {
|
|
1343
|
+
// Required for the platform-hosted (per-install Lambda) runtime: the
|
|
1344
|
+
// injected wrapper probes [.next/standalone/server.js, server.js] and
|
|
1345
|
+
// exit(1)s the cold start if neither exists. Without this the built
|
|
1346
|
+
// Lambda FATALs. Harmless for local \`next dev\` / \`next start\`.
|
|
1347
|
+
output: 'standalone',
|
|
1348
|
+
typescript: { ignoreBuildErrors: true },
|
|
1349
|
+
eslint: { ignoreDuringBuilds: true },
|
|
1350
|
+
env: {
|
|
1351
|
+
NEXT_PUBLIC_DASHWISE_API_URL:
|
|
1352
|
+
process.env.NEXT_PUBLIC_DASHWISE_API_URL ?? apiUrl,
|
|
1353
|
+
},
|
|
1354
|
+
};
|
|
1355
|
+
export default nextConfig;
|
|
1356
|
+
`;
|
|
1357
|
+
}
|
|
1358
|
+
function nextEnvDts() {
|
|
1359
|
+
return `/// <reference types="next" />
|
|
678
1360
|
/// <reference types="next/image-types/global" />
|
|
679
1361
|
`;
|
|
680
1362
|
}
|
|
@@ -1689,15 +2371,40 @@ function globalsCss() {
|
|
|
1689
2371
|
--chart-5: oklch(0.67 0.2 15);
|
|
1690
2372
|
}
|
|
1691
2373
|
|
|
1692
|
-
/* Wire
|
|
1693
|
-
\`
|
|
1694
|
-
|
|
2374
|
+
/* Wire EVERY design token into Tailwind's namespace. In Tailwind 4,
|
|
2375
|
+
utilities like \`bg-primary\` / \`text-muted-foreground\` / \`rounded-lg\`
|
|
2376
|
+
only exist if @theme defines the matching \`--color-*\` / \`--radius-*\`
|
|
2377
|
+
variable \u2014 the \`shadcn\` npm package's tailwind.css maps NONE of them
|
|
2378
|
+
(it ships only keyframes + variants), so without this block every
|
|
2379
|
+
semantic-token utility silently compiles to nothing. */
|
|
1695
2380
|
@theme inline {
|
|
2381
|
+
--color-background: var(--background);
|
|
2382
|
+
--color-foreground: var(--foreground);
|
|
2383
|
+
--color-card: var(--card);
|
|
2384
|
+
--color-card-foreground: var(--card-foreground);
|
|
2385
|
+
--color-popover: var(--popover);
|
|
2386
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
2387
|
+
--color-primary: var(--primary);
|
|
2388
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
2389
|
+
--color-secondary: var(--secondary);
|
|
2390
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
2391
|
+
--color-muted: var(--muted);
|
|
2392
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
2393
|
+
--color-accent: var(--accent);
|
|
2394
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
2395
|
+
--color-destructive: var(--destructive);
|
|
2396
|
+
--color-border: var(--border);
|
|
2397
|
+
--color-input: var(--input);
|
|
2398
|
+
--color-ring: var(--ring);
|
|
1696
2399
|
--color-chart-1: var(--chart-1);
|
|
1697
2400
|
--color-chart-2: var(--chart-2);
|
|
1698
2401
|
--color-chart-3: var(--chart-3);
|
|
1699
2402
|
--color-chart-4: var(--chart-4);
|
|
1700
2403
|
--color-chart-5: var(--chart-5);
|
|
2404
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
2405
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
2406
|
+
--radius-lg: var(--radius);
|
|
2407
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
1701
2408
|
}
|
|
1702
2409
|
|
|
1703
2410
|
* {
|
|
@@ -1897,6 +2604,90 @@ export function ThemeProvider({
|
|
|
1897
2604
|
}
|
|
1898
2605
|
`;
|
|
1899
2606
|
}
|
|
2607
|
+
function appErrorTsx() {
|
|
2608
|
+
return `"use client";
|
|
2609
|
+
|
|
2610
|
+
/**
|
|
2611
|
+
* Root error boundary. Next.js renders this when anything below the
|
|
2612
|
+
* layout throws during render or data loading.
|
|
2613
|
+
*
|
|
2614
|
+
* - \`error.digest\` is the server-side correlation id \u2014 surface it so a
|
|
2615
|
+
* bug report can be matched to the runtime logs.
|
|
2616
|
+
* - \`reset()\` re-renders the segment; for transient failures (network
|
|
2617
|
+
* blip, expired session mid-flight) a retry often just works.
|
|
2618
|
+
*/
|
|
2619
|
+
import { Button } from "@/components/ui/button";
|
|
2620
|
+
import {
|
|
2621
|
+
Card,
|
|
2622
|
+
CardContent,
|
|
2623
|
+
CardDescription,
|
|
2624
|
+
CardHeader,
|
|
2625
|
+
CardTitle,
|
|
2626
|
+
} from "@/components/ui/card";
|
|
2627
|
+
|
|
2628
|
+
export default function Error({
|
|
2629
|
+
error,
|
|
2630
|
+
reset,
|
|
2631
|
+
}: {
|
|
2632
|
+
error: Error & { digest?: string };
|
|
2633
|
+
reset: () => void;
|
|
2634
|
+
}) {
|
|
2635
|
+
return (
|
|
2636
|
+
<div className="flex min-h-screen items-center justify-center bg-background p-6">
|
|
2637
|
+
<Card className="w-full max-w-md">
|
|
2638
|
+
<CardHeader>
|
|
2639
|
+
<CardTitle>Something went wrong</CardTitle>
|
|
2640
|
+
<CardDescription>
|
|
2641
|
+
An unexpected error occurred while rendering this page.
|
|
2642
|
+
</CardDescription>
|
|
2643
|
+
</CardHeader>
|
|
2644
|
+
<CardContent className="space-y-4">
|
|
2645
|
+
{error.digest ? (
|
|
2646
|
+
<p className="text-sm text-muted-foreground">
|
|
2647
|
+
Error digest:{" "}
|
|
2648
|
+
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
|
|
2649
|
+
{error.digest}
|
|
2650
|
+
</code>
|
|
2651
|
+
</p>
|
|
2652
|
+
) : null}
|
|
2653
|
+
<Button onClick={() => reset()}>Try again</Button>
|
|
2654
|
+
</CardContent>
|
|
2655
|
+
</Card>
|
|
2656
|
+
</div>
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
`;
|
|
2660
|
+
}
|
|
2661
|
+
function seedMjs() {
|
|
2662
|
+
return `/**
|
|
2663
|
+
* seed.mjs \u2014 OPTIONAL seed/maintenance script template.
|
|
2664
|
+
*
|
|
2665
|
+
* Run it with the module's DashWise env injected (DASHWISE_API_URL +
|
|
2666
|
+
* DASHWISE_API_KEY resolved like \`dashwise query\`: explicit env \u2192
|
|
2667
|
+
* .env.local \u2192 the dwk_ key cached by your CLI login):
|
|
2668
|
+
*
|
|
2669
|
+
* dashwise run -- node seed.mjs
|
|
2670
|
+
*
|
|
2671
|
+
* (Requires \`dashwise module generate\` to have run at least once so
|
|
2672
|
+
* \`@dashai/generated\` exists \u2014 \`dashwise dev\` does this for you.)
|
|
2673
|
+
*
|
|
2674
|
+
* \u26A0 PUBLISH WARNING: \`dashwise module publish\` packs the ENTIRE project
|
|
2675
|
+
* tree (it does NOT read .gitignore). Delete this file \u2014 or publish with
|
|
2676
|
+
* \`--exclude seed.mjs\` \u2014 before publishing, so seed data/logic doesn't
|
|
2677
|
+
* ship inside your module bundle.
|
|
2678
|
+
*/
|
|
2679
|
+
|
|
2680
|
+
// Uncomment to use (the scaffold's example \`items\` table):
|
|
2681
|
+
//
|
|
2682
|
+
// import { db } from '@dashai/generated';
|
|
2683
|
+
//
|
|
2684
|
+
// const created = await db.items.create({ title: 'Seeded item', done: false });
|
|
2685
|
+
// console.log('created items row', created.id);
|
|
2686
|
+
//
|
|
2687
|
+
// const rows = await db.items.list({ limit: 10 });
|
|
2688
|
+
// console.log('items now:', rows.length);
|
|
2689
|
+
`;
|
|
2690
|
+
}
|
|
1900
2691
|
function appSignInErrorPageTsx() {
|
|
1901
2692
|
return `import Link from 'next/link';
|
|
1902
2693
|
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
|
@@ -2417,15 +3208,23 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2417
3208
|
}
|
|
2418
3209
|
const name = options.name ?? humanizeSlug(slug);
|
|
2419
3210
|
const dir = resolve(options.dir ?? slug);
|
|
2420
|
-
|
|
3211
|
+
const manifestExists = existsSync(join(dir, "module.json"));
|
|
3212
|
+
if (options.provisionOnly && !manifestExists) {
|
|
3213
|
+
fail(
|
|
3214
|
+
`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.`
|
|
3215
|
+
);
|
|
3216
|
+
return 1;
|
|
3217
|
+
}
|
|
3218
|
+
if (!options.provisionOnly && existsSync(dir)) {
|
|
2421
3219
|
const entries = readdirSync(dir);
|
|
2422
|
-
if (entries.length > 0 && !options.force) {
|
|
3220
|
+
if (entries.length > 0 && !options.force && !manifestExists) {
|
|
2423
3221
|
fail(
|
|
2424
3222
|
`Directory ${code(dir)} is not empty. Pass ${code("--force")} to scaffold anyway (existing files will be overwritten).`
|
|
2425
3223
|
);
|
|
2426
3224
|
return 1;
|
|
2427
3225
|
}
|
|
2428
3226
|
}
|
|
3227
|
+
const writeMode = options.provisionOnly ? "none" : manifestExists && !options.force ? "missing-only" : "all";
|
|
2429
3228
|
if (options.simple && options.custom) {
|
|
2430
3229
|
fail(
|
|
2431
3230
|
`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 +3263,11 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2464
3263
|
// Example data-plane route (both flavors) — exercises the generated
|
|
2465
3264
|
// SDK against the seeded `items` table. Ships for simple + custom
|
|
2466
3265
|
// since both produce `@dashai/generated`.
|
|
2467
|
-
["app/api/items/route.ts", appApiItemsRouteTs()]
|
|
3266
|
+
["app/api/items/route.ts", appApiItemsRouteTs()],
|
|
3267
|
+
// C12/D5 (Wave 2) — commented-out seed-script template. Pairs with
|
|
3268
|
+
// `dashwise run -- node seed.mjs`; carries the publish-packs-
|
|
3269
|
+
// everything warning so it never ships inside a bundle by accident.
|
|
3270
|
+
["seed.mjs", seedMjs()]
|
|
2468
3271
|
];
|
|
2469
3272
|
if (kind === "custom") {
|
|
2470
3273
|
files.push(
|
|
@@ -2480,6 +3283,10 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2480
3283
|
["components/ui/card.tsx", componentsUiCardTsx()],
|
|
2481
3284
|
["app/layout.tsx", appLayoutCustomTsx(scaffoldOpts)],
|
|
2482
3285
|
["app/page.tsx", appPageCustomTsx(scaffoldOpts)],
|
|
3286
|
+
// C10 (Wave 2) — root error boundary: branded card, error.digest,
|
|
3287
|
+
// reset() retry, semantic tokens (which resolve now that
|
|
3288
|
+
// globals.css maps them in @theme).
|
|
3289
|
+
["app/error.tsx", appErrorTsx()],
|
|
2483
3290
|
["app/dashboard/page.tsx", appDashboardPageTsx()],
|
|
2484
3291
|
["app/sign-in-error/page.tsx", appSignInErrorPageTsx()],
|
|
2485
3292
|
["app/api/auth/sign-in/route.ts", appApiAuthSignInTs()],
|
|
@@ -2509,17 +3316,36 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2509
3316
|
break;
|
|
2510
3317
|
}
|
|
2511
3318
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
3319
|
+
let written = 0;
|
|
3320
|
+
let skipped = 0;
|
|
3321
|
+
if (writeMode !== "none") {
|
|
3322
|
+
for (const [relPath, content] of files) {
|
|
3323
|
+
const absPath = join(dir, relPath);
|
|
3324
|
+
if (writeMode === "missing-only" && existsSync(absPath)) {
|
|
3325
|
+
skipped++;
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3328
|
+
const parentDir = join(absPath, "..");
|
|
3329
|
+
if (!existsSync(parentDir)) {
|
|
3330
|
+
mkdirSync(parentDir, { recursive: true });
|
|
3331
|
+
}
|
|
3332
|
+
writeFileSync(absPath, content, "utf-8");
|
|
3333
|
+
written++;
|
|
2517
3334
|
}
|
|
2518
|
-
writeFileSync(absPath, content, "utf-8");
|
|
2519
3335
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
3336
|
+
if (writeMode === "none") {
|
|
3337
|
+
info(
|
|
3338
|
+
`Existing scaffold in ${code(dir)} left untouched (provision-only) \u2014 re-provisioning the backend dev install.`
|
|
3339
|
+
);
|
|
3340
|
+
} else if (writeMode === "missing-only") {
|
|
3341
|
+
success(
|
|
3342
|
+
`Existing module project in ${code(dir)} \u2014 wrote ${written} missing file(s), preserved ${skipped} existing file(s). ${dim("(Pass --force to overwrite everything.)")}`
|
|
3343
|
+
);
|
|
3344
|
+
} else {
|
|
3345
|
+
success(
|
|
3346
|
+
`Scaffolded ${code(slug)} (${kind === "custom" ? "custom Next.js" : "simple module"}) in ${code(dir)}`
|
|
3347
|
+
);
|
|
3348
|
+
}
|
|
2523
3349
|
let devInstall = null;
|
|
2524
3350
|
const wantsProvision = options.provision !== false;
|
|
2525
3351
|
if (wantsProvision) {
|
|
@@ -2530,12 +3356,16 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2530
3356
|
return void 0;
|
|
2531
3357
|
}
|
|
2532
3358
|
})();
|
|
3359
|
+
const manifestKind = (() => {
|
|
3360
|
+
const mk = manifest?.module_kind;
|
|
3361
|
+
return mk === "ai_builder" || mk === "hand_authored" || mk === "custom" ? mk : void 0;
|
|
3362
|
+
})();
|
|
2533
3363
|
devInstall = await tryProvisionDevInstall({
|
|
2534
3364
|
slug,
|
|
2535
3365
|
name,
|
|
2536
3366
|
description: options.description,
|
|
2537
3367
|
workspaceFlag: options.workspace,
|
|
2538
|
-
moduleKind: kind,
|
|
3368
|
+
moduleKind: manifestKind ?? kind,
|
|
2539
3369
|
...manifest !== void 0 ? { manifest } : {}
|
|
2540
3370
|
});
|
|
2541
3371
|
} else {
|
|
@@ -2550,7 +3380,8 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
2550
3380
|
);
|
|
2551
3381
|
info(
|
|
2552
3382
|
`Fix it by re-running provisioning once the cause above is resolved:
|
|
2553
|
-
${code(`dashwise init ${slug} --
|
|
3383
|
+
${code(`dashwise init ${slug} --workspace <your-workspace-slug>`)}
|
|
3384
|
+
(Existing files are preserved \u2014 a re-run only provisions + fills in missing files.)
|
|
2554
3385
|
Confirm success: the line ${code("\u2713 Provisioned dev install id=\u2026 API key cached")} must appear,
|
|
2555
3386
|
and ${code("cat ~/.config/dashwise/auth.json")} must show a non-empty ${code(`modules.${slug}.apiKey`)} starting with ${code("dwk_")}.`
|
|
2556
3387
|
);
|
|
@@ -2576,7 +3407,7 @@ ${devInstall ? `
|
|
|
2576
3407
|
async function tryProvisionDevInstall(args) {
|
|
2577
3408
|
if (!isLoggedIn()) {
|
|
2578
3409
|
warn(
|
|
2579
|
-
`Not logged in \u2014 skipping dev-install provisioning. Run ${code("dashwise login")} then ${code(`dashwise init ${args.slug}
|
|
3410
|
+
`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
3411
|
);
|
|
2581
3412
|
return null;
|
|
2582
3413
|
}
|
|
@@ -2678,7 +3509,7 @@ function surfaceProvisionApiError(err, slug) {
|
|
|
2678
3509
|
switch (err.code) {
|
|
2679
3510
|
case "UNAUTHENTICATED":
|
|
2680
3511
|
warn(
|
|
2681
|
-
`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then ${code(`dashwise init ${slug}
|
|
3512
|
+
`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
3513
|
);
|
|
2683
3514
|
return;
|
|
2684
3515
|
case "FORBIDDEN":
|
|
@@ -2686,11 +3517,22 @@ function surfaceProvisionApiError(err, slug) {
|
|
|
2686
3517
|
`Not authorized to create a module in that workspace. Confirm you're a member at the DashWise dashboard.`
|
|
2687
3518
|
);
|
|
2688
3519
|
return;
|
|
2689
|
-
case "SLUG_ALREADY_EXISTS":
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
3520
|
+
case "SLUG_ALREADY_EXISTS": {
|
|
3521
|
+
const cached = readModuleEntry(slug);
|
|
3522
|
+
if (cached) {
|
|
3523
|
+
warn(
|
|
3524
|
+
`A module with slug ${code(slug)} already exists, and you have its dev install cached (id=${cached.installationId}).
|
|
3525
|
+
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.`
|
|
3526
|
+
);
|
|
3527
|
+
} else {
|
|
3528
|
+
warn(
|
|
3529
|
+
`A module with slug ${code(slug)} already exists (published and/or production-installed). Local development for an already-published module is not currently possible \u2014 module slugs are GLOBAL, so this 409s in every workspace (a platform fix is planned). Interim options:
|
|
3530
|
+
- 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).
|
|
3531
|
+
- Or pick a NEW slug (duplicate the code) for greenfield iteration.`
|
|
3532
|
+
);
|
|
3533
|
+
}
|
|
2693
3534
|
return;
|
|
3535
|
+
}
|
|
2694
3536
|
case "RUNTIME_DATA_API_DISABLED":
|
|
2695
3537
|
warn(
|
|
2696
3538
|
`Backend has the local-dev API turned off (BUILDER_SESSION_TOKEN_SECRET unset). Contact the operator.`
|
|
@@ -2737,7 +3579,7 @@ function loadSchemaCommandContext(opts) {
|
|
|
2737
3579
|
const entry = readModuleEntry(slug);
|
|
2738
3580
|
if (!entry) {
|
|
2739
3581
|
throw new Error(
|
|
2740
|
-
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}
|
|
3582
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}\` to provision one (existing files are preserved).`
|
|
2741
3583
|
);
|
|
2742
3584
|
}
|
|
2743
3585
|
const token = resolveSchemaAuthToken(entry, slug);
|
|
@@ -2758,7 +3600,7 @@ function loadSchemaCommandContext(opts) {
|
|
|
2758
3600
|
function resolveSchemaAuthToken(entry, slug) {
|
|
2759
3601
|
if (entry.apiKey) return entry.apiKey;
|
|
2760
3602
|
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}
|
|
3603
|
+
`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
3604
|
);
|
|
2763
3605
|
}
|
|
2764
3606
|
var init_schema_command_context = __esm({
|
|
@@ -2767,1187 +3609,1013 @@ var init_schema_command_context = __esm({
|
|
|
2767
3609
|
init_manifest_io();
|
|
2768
3610
|
}
|
|
2769
3611
|
});
|
|
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
|
-
);
|
|
3612
|
+
function hydrateEnvFromDotenvLocal(projectRoot) {
|
|
3613
|
+
const envFile = join(projectRoot, ".env.local");
|
|
3614
|
+
if (!existsSync(envFile)) return;
|
|
3615
|
+
const raw = readFileSync(envFile, "utf-8");
|
|
3616
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
3617
|
+
const trimmed = line.trim();
|
|
3618
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
3619
|
+
const eq = trimmed.indexOf("=");
|
|
3620
|
+
if (eq <= 0) continue;
|
|
3621
|
+
const key = trimmed.slice(0, eq).trim();
|
|
3622
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
3623
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3624
|
+
value = value.slice(1, -1);
|
|
3625
|
+
}
|
|
3626
|
+
if (process.env[key] === void 0) {
|
|
3627
|
+
process.env[key] = value;
|
|
3628
|
+
}
|
|
3011
3629
|
}
|
|
3012
|
-
return o;
|
|
3013
3630
|
}
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
}
|
|
3017
|
-
var FIELD_TYPES, SLUG_REGEX, KNOWN_FLAGS, FieldSpecError;
|
|
3018
|
-
var init_field_spec = __esm({
|
|
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
|
-
};
|
|
3631
|
+
var init_dotenv_local = __esm({
|
|
3632
|
+
"src/lib/dotenv-local.ts"() {
|
|
3046
3633
|
}
|
|
3047
3634
|
});
|
|
3048
3635
|
|
|
3049
|
-
// src/commands/
|
|
3050
|
-
var
|
|
3051
|
-
__export(
|
|
3052
|
-
|
|
3053
|
-
syncLocalManifest: () => syncLocalManifest,
|
|
3054
|
-
tableCreateCommand: () => tableCreateCommand
|
|
3636
|
+
// src/commands/run.ts
|
|
3637
|
+
var run_exports = {};
|
|
3638
|
+
__export(run_exports, {
|
|
3639
|
+
runCommand: () => runCommand
|
|
3055
3640
|
});
|
|
3056
|
-
async function
|
|
3057
|
-
if (!
|
|
3058
|
-
fail(
|
|
3641
|
+
async function runCommand(cmdArgs, options) {
|
|
3642
|
+
if (!cmdArgs || cmdArgs.length === 0) {
|
|
3643
|
+
fail(
|
|
3644
|
+
`Usage: ${code("dashwise run -- <command\u2026>")} (e.g. ${code("dashwise run -- node seed.mjs")})`
|
|
3645
|
+
);
|
|
3059
3646
|
return 1;
|
|
3060
3647
|
}
|
|
3061
|
-
const
|
|
3648
|
+
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
3649
|
+
let manifestSlug;
|
|
3062
3650
|
try {
|
|
3063
|
-
|
|
3064
|
-
fields.push(parseFieldSpec(spec));
|
|
3065
|
-
}
|
|
3066
|
-
for (const json of options.fieldJson ?? []) {
|
|
3067
|
-
fields.push(parseFieldJson(json));
|
|
3068
|
-
}
|
|
3651
|
+
manifestSlug = readManifestOrThrow(projectRoot).module?.slug;
|
|
3069
3652
|
} catch (err) {
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3653
|
+
fail(err.message);
|
|
3654
|
+
return 1;
|
|
3655
|
+
}
|
|
3656
|
+
if (!manifestSlug) {
|
|
3657
|
+
fail(`module.json has no ${code("module.slug")} \u2014 the manifest is malformed.`);
|
|
3658
|
+
return 1;
|
|
3659
|
+
}
|
|
3660
|
+
hydrateEnvFromDotenvLocal(projectRoot);
|
|
3661
|
+
const cfg = (() => {
|
|
3662
|
+
try {
|
|
3663
|
+
return readConfig();
|
|
3664
|
+
} catch {
|
|
3665
|
+
return null;
|
|
3073
3666
|
}
|
|
3074
|
-
|
|
3667
|
+
})();
|
|
3668
|
+
const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
|
|
3669
|
+
let apiKey = process.env.DASHWISE_API_KEY;
|
|
3670
|
+
if (!apiKey) {
|
|
3671
|
+
let entry = readModuleEntry(manifestSlug);
|
|
3672
|
+
if (cfg) {
|
|
3673
|
+
const healed = await resolveModuleApiKey(
|
|
3674
|
+
manifestSlug,
|
|
3675
|
+
entry,
|
|
3676
|
+
apiUrl,
|
|
3677
|
+
cfg.token
|
|
3678
|
+
);
|
|
3679
|
+
if (healed) entry = healed;
|
|
3680
|
+
}
|
|
3681
|
+
apiKey = entry?.apiKey;
|
|
3075
3682
|
}
|
|
3076
|
-
if (
|
|
3683
|
+
if (!apiKey) {
|
|
3077
3684
|
fail(
|
|
3078
|
-
`
|
|
3685
|
+
`No ${code("dwk_")} API key for module ${code(manifestSlug)} \u2014 the command would run with NO token.`
|
|
3686
|
+
);
|
|
3687
|
+
info(
|
|
3688
|
+
`Provision a dev install first (${code(`dashwise init ${manifestSlug} --workspace <your-workspace-slug>`)}), or set ${code("DASHWISE_API_KEY")} explicitly.`
|
|
3079
3689
|
);
|
|
3080
3690
|
return 1;
|
|
3081
3691
|
}
|
|
3082
|
-
const
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3692
|
+
const [cmd, ...rest] = cmdArgs;
|
|
3693
|
+
info(
|
|
3694
|
+
`${dim("run")} ${cmd} ${rest.join(" ")} ${dim(`(DASHWISE_API_URL=${apiUrl}, DASHWISE_API_KEY injected)`)}`
|
|
3695
|
+
);
|
|
3696
|
+
const child = spawn(cmd, rest, {
|
|
3697
|
+
cwd: projectRoot,
|
|
3698
|
+
stdio: "inherit",
|
|
3699
|
+
env: {
|
|
3700
|
+
...process.env,
|
|
3701
|
+
DASHWISE_API_URL: apiUrl,
|
|
3702
|
+
DASHWISE_API_KEY: apiKey
|
|
3703
|
+
},
|
|
3704
|
+
// Windows needs shell:true for .cmd shims (npm / npx).
|
|
3705
|
+
shell: process.platform === "win32"
|
|
3706
|
+
});
|
|
3707
|
+
return new Promise((resolveExit) => {
|
|
3708
|
+
child.on("error", (err) => {
|
|
3709
|
+
fail(`Could not spawn ${code(cmd)}: ${err.message}`);
|
|
3710
|
+
resolveExit(1);
|
|
3711
|
+
});
|
|
3712
|
+
child.on("exit", (exitCode, signal) => {
|
|
3713
|
+
resolveExit(exitCode ?? (signal !== null ? 1 : 0));
|
|
3714
|
+
});
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3717
|
+
var init_run = __esm({
|
|
3718
|
+
"src/commands/run.ts"() {
|
|
3719
|
+
init_config();
|
|
3720
|
+
init_dotenv_local();
|
|
3721
|
+
init_manifest_io();
|
|
3722
|
+
init_output();
|
|
3723
|
+
init_dev();
|
|
3724
|
+
}
|
|
3725
|
+
});
|
|
3726
|
+
|
|
3727
|
+
// src/commands/module/destroy-dev-install.ts
|
|
3728
|
+
var destroy_dev_install_exports = {};
|
|
3729
|
+
__export(destroy_dev_install_exports, {
|
|
3730
|
+
destroyDevInstallCommand: () => destroyDevInstallCommand
|
|
3731
|
+
});
|
|
3732
|
+
async function destroyDevInstallCommand(options) {
|
|
3733
|
+
let slug = options.module;
|
|
3734
|
+
if (!slug) {
|
|
3735
|
+
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
3736
|
+
const manifest = (() => {
|
|
3737
|
+
try {
|
|
3738
|
+
return readManifest(projectRoot);
|
|
3739
|
+
} catch {
|
|
3740
|
+
return null;
|
|
3741
|
+
}
|
|
3742
|
+
})();
|
|
3743
|
+
slug = manifest?.module?.slug;
|
|
3744
|
+
if (!slug) {
|
|
3745
|
+
fail(
|
|
3746
|
+
`No module slug \u2014 pass ${code("--module <slug>")} or run from a directory with a valid module.json.`
|
|
3747
|
+
);
|
|
3086
3748
|
return 1;
|
|
3087
3749
|
}
|
|
3088
|
-
seen.add(f.slug);
|
|
3089
3750
|
}
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
3094
|
-
});
|
|
3095
|
-
} catch (err) {
|
|
3096
|
-
fail(err.message);
|
|
3751
|
+
const cfg = readConfig();
|
|
3752
|
+
if (!cfg) {
|
|
3753
|
+
fail("Not logged in. Run `dashwise login` first.");
|
|
3097
3754
|
return 1;
|
|
3098
3755
|
}
|
|
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})`
|
|
3756
|
+
const entry = readModuleEntry(slug);
|
|
3757
|
+
if (!entry) {
|
|
3758
|
+
warn(
|
|
3759
|
+
`No cached dev install for ${code(slug)} in auth.json. Nothing to do.`
|
|
3107
3760
|
);
|
|
3108
|
-
|
|
3109
|
-
return surfaceFailure(err);
|
|
3761
|
+
return 0;
|
|
3110
3762
|
}
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3763
|
+
if (!options.yes && process.stdout.isTTY) {
|
|
3764
|
+
const choice = await confirm({
|
|
3765
|
+
message: `Destroy dev install ${code(`${slug} (id=${entry.installationId})`)}? Tables and rows will be soft-deleted (recoverable for 30 days via dashboard trash).`
|
|
3766
|
+
});
|
|
3767
|
+
if (isCancel(choice) || !choice) {
|
|
3768
|
+
info("Aborted.");
|
|
3769
|
+
return 130;
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
let backendCleaned = false;
|
|
3114
3773
|
try {
|
|
3115
|
-
const
|
|
3116
|
-
|
|
3117
|
-
|
|
3774
|
+
const response = await destroyDevInstallation(entry.installationId, {
|
|
3775
|
+
baseUrl: resolveApiUrl(),
|
|
3776
|
+
auth: { token: cfg.token }
|
|
3777
|
+
});
|
|
3778
|
+
backendCleaned = true;
|
|
3779
|
+
success(
|
|
3780
|
+
`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)."}`
|
|
3781
|
+
);
|
|
3118
3782
|
} catch (err) {
|
|
3119
|
-
if (err instanceof ApiError) {
|
|
3783
|
+
if (err instanceof ApiError && err.code === "INSTALLATION_NOT_FOUND") {
|
|
3120
3784
|
warn(
|
|
3121
|
-
`
|
|
3785
|
+
`Install ${entry.installationId} not found on the backend \u2014 already destroyed elsewhere. Cleaning up local cache.`
|
|
3122
3786
|
);
|
|
3787
|
+
backendCleaned = true;
|
|
3123
3788
|
} else {
|
|
3789
|
+
return surfaceDestroyError(err);
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
if (backendCleaned) {
|
|
3793
|
+
try {
|
|
3794
|
+
removeModuleEntry(slug);
|
|
3795
|
+
info(`Removed ${code(slug)} entry from auth.json.`);
|
|
3796
|
+
} catch (err) {
|
|
3124
3797
|
warn(
|
|
3125
|
-
`
|
|
3798
|
+
`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
3799
|
);
|
|
3127
3800
|
}
|
|
3128
|
-
return 0;
|
|
3129
3801
|
}
|
|
3802
|
+
return 0;
|
|
3130
3803
|
}
|
|
3131
|
-
function
|
|
3804
|
+
function surfaceDestroyError(err) {
|
|
3132
3805
|
if (err instanceof ApiError) {
|
|
3133
3806
|
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":
|
|
3807
|
+
case "INSTALLATION_KIND_NOT_LOCAL":
|
|
3147
3808
|
fail(
|
|
3148
|
-
`
|
|
3809
|
+
`Refusing to destroy: the install isn't kind=local (it may have been promoted to production). Use the dashboard's uninstall flow instead.`
|
|
3149
3810
|
);
|
|
3150
3811
|
break;
|
|
3151
|
-
case "
|
|
3812
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
3152
3813
|
fail(
|
|
3153
|
-
`
|
|
3814
|
+
`You're not a member of the install's workspace. Contact a workspace admin.`
|
|
3154
3815
|
);
|
|
3155
3816
|
break;
|
|
3156
|
-
case "
|
|
3817
|
+
case "UNAUTHENTICATED":
|
|
3157
3818
|
fail(
|
|
3158
|
-
`
|
|
3819
|
+
`Auth rejected by backend \u2014 your CLI bearer may be stale. Re-run ${code("dashwise login")} and try again.`
|
|
3159
3820
|
);
|
|
3160
3821
|
break;
|
|
3161
3822
|
default:
|
|
3162
3823
|
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);
|
|
3824
|
+
}
|
|
3825
|
+
return 1;
|
|
3206
3826
|
}
|
|
3207
|
-
|
|
3827
|
+
fail(`Unexpected error: ${err.message}`);
|
|
3828
|
+
return 1;
|
|
3208
3829
|
}
|
|
3209
|
-
var
|
|
3210
|
-
"src/commands/
|
|
3830
|
+
var init_destroy_dev_install = __esm({
|
|
3831
|
+
"src/commands/module/destroy-dev-install.ts"() {
|
|
3211
3832
|
init_api_client();
|
|
3212
|
-
|
|
3833
|
+
init_config();
|
|
3834
|
+
init_manifest_io();
|
|
3213
3835
|
init_output();
|
|
3214
|
-
init_create();
|
|
3215
3836
|
}
|
|
3216
3837
|
});
|
|
3217
3838
|
|
|
3218
|
-
// src/commands/
|
|
3219
|
-
var
|
|
3220
|
-
__export(
|
|
3221
|
-
|
|
3839
|
+
// src/commands/module/reset-dev-install.ts
|
|
3840
|
+
var reset_dev_install_exports = {};
|
|
3841
|
+
__export(reset_dev_install_exports, {
|
|
3842
|
+
resetDevInstallCommand: () => resetDevInstallCommand
|
|
3222
3843
|
});
|
|
3223
|
-
async function
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3844
|
+
async function resetDevInstallCommand(options) {
|
|
3845
|
+
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
3846
|
+
const manifest = (() => {
|
|
3847
|
+
try {
|
|
3848
|
+
return readManifest(projectRoot);
|
|
3849
|
+
} catch {
|
|
3850
|
+
return null;
|
|
3851
|
+
}
|
|
3852
|
+
})();
|
|
3853
|
+
const slug = options.module ?? manifest?.module?.slug;
|
|
3854
|
+
if (!slug) {
|
|
3233
3855
|
fail(
|
|
3234
|
-
`
|
|
3856
|
+
`No module slug \u2014 pass ${code("--module <slug>")} or run from a directory with a valid module.json.`
|
|
3235
3857
|
);
|
|
3236
3858
|
return 1;
|
|
3237
3859
|
}
|
|
3238
|
-
|
|
3860
|
+
const name = manifest?.module?.name ?? slug;
|
|
3861
|
+
info(`Resetting dev install for ${code(slug)} \u2014 phase 1/2: destroy\u2026`);
|
|
3862
|
+
const destroyExit = await destroyDevInstallCommand({
|
|
3863
|
+
...options.dir !== void 0 ? { dir: options.dir } : {},
|
|
3864
|
+
module: slug,
|
|
3865
|
+
...options.yes !== void 0 ? { yes: options.yes } : {}
|
|
3866
|
+
});
|
|
3867
|
+
if (destroyExit !== 0) {
|
|
3239
3868
|
fail(
|
|
3240
|
-
`
|
|
3241
|
-
);
|
|
3242
|
-
return 1;
|
|
3243
|
-
}
|
|
3244
|
-
let field;
|
|
3245
|
-
try {
|
|
3246
|
-
field = hasJson ? parseFieldJson(options.fieldJson) : parseFieldSpec(fieldSpec);
|
|
3247
|
-
} catch (err) {
|
|
3248
|
-
if (err instanceof FieldSpecError) {
|
|
3249
|
-
fail(err.message);
|
|
3250
|
-
return 1;
|
|
3251
|
-
}
|
|
3252
|
-
throw err;
|
|
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);
|
|
3261
|
-
return 1;
|
|
3262
|
-
}
|
|
3263
|
-
info(
|
|
3264
|
-
`Adding field ${code(`${tableSlug}.${field.slug}`)} (${field.type}) to installation ${ctx.entry.installationId}\u2026`
|
|
3265
|
-
);
|
|
3266
|
-
try {
|
|
3267
|
-
const result = await addField(
|
|
3268
|
-
{ table_slug: tableSlug, field },
|
|
3269
|
-
ctx.requestOptions
|
|
3270
|
-
);
|
|
3271
|
-
success(
|
|
3272
|
-
`Added field ${code(`${tableSlug}.${field.slug}`)} (manifest_version=${result.manifest_version})`
|
|
3869
|
+
`Destroy phase failed (exit ${destroyExit}); aborting reset. Fix the underlying error and re-run.`
|
|
3273
3870
|
);
|
|
3274
|
-
|
|
3275
|
-
return surfaceFailure(err);
|
|
3871
|
+
return destroyExit;
|
|
3276
3872
|
}
|
|
3277
|
-
|
|
3873
|
+
info(`Reset phase 2/2: re-provisioning\u2026`);
|
|
3874
|
+
return moduleInitCommand(slug, {
|
|
3875
|
+
name,
|
|
3876
|
+
...options.dir !== void 0 ? { dir: options.dir } : { dir: projectRoot },
|
|
3877
|
+
// Provision-only: write NO files — every local file (module.json
|
|
3878
|
+
// included) stays exactly as-is; only the backend dev install +
|
|
3879
|
+
// the auth.json credentials entry are recreated. This is what the
|
|
3880
|
+
// command's "keeps local module.json intact" contract always
|
|
3881
|
+
// promised (pre-fix `force: true` re-scaffolded over user code).
|
|
3882
|
+
provisionOnly: true,
|
|
3883
|
+
provision: true,
|
|
3884
|
+
...options.workspace !== void 0 ? { workspace: options.workspace } : {}
|
|
3885
|
+
});
|
|
3278
3886
|
}
|
|
3279
|
-
var
|
|
3280
|
-
"src/commands/
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3887
|
+
var init_reset_dev_install = __esm({
|
|
3888
|
+
"src/commands/module/reset-dev-install.ts"() {
|
|
3889
|
+
init_destroy_dev_install();
|
|
3890
|
+
init_init();
|
|
3891
|
+
init_manifest_io();
|
|
3284
3892
|
init_output();
|
|
3285
|
-
init_create();
|
|
3286
3893
|
}
|
|
3287
3894
|
});
|
|
3288
3895
|
|
|
3289
|
-
// src/commands/
|
|
3290
|
-
var
|
|
3291
|
-
__export(
|
|
3292
|
-
|
|
3896
|
+
// src/commands/schema/push.ts
|
|
3897
|
+
var push_exports = {};
|
|
3898
|
+
__export(push_exports, {
|
|
3899
|
+
schemaPushCommand: () => schemaPushCommand
|
|
3293
3900
|
});
|
|
3294
|
-
async function
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
return 1;
|
|
3298
|
-
}
|
|
3299
|
-
let ctx;
|
|
3901
|
+
async function schemaPushCommand(options) {
|
|
3902
|
+
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
3903
|
+
let manifest;
|
|
3300
3904
|
try {
|
|
3301
|
-
|
|
3302
|
-
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
3303
|
-
});
|
|
3905
|
+
manifest = readManifestOrThrow(projectRoot);
|
|
3304
3906
|
} catch (err) {
|
|
3305
3907
|
fail(err.message);
|
|
3306
3908
|
return 1;
|
|
3307
3909
|
}
|
|
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);
|
|
3322
|
-
}
|
|
3323
|
-
return syncLocalManifest(ctx);
|
|
3324
|
-
}
|
|
3325
|
-
var init_remove = __esm({
|
|
3326
|
-
"src/commands/field/remove.ts"() {
|
|
3327
|
-
init_api_client();
|
|
3328
|
-
init_schema_command_context();
|
|
3329
|
-
init_output();
|
|
3330
|
-
init_create();
|
|
3331
|
-
}
|
|
3332
|
-
});
|
|
3333
|
-
|
|
3334
|
-
// src/commands/field/set-type.ts
|
|
3335
|
-
var set_type_exports = {};
|
|
3336
|
-
__export(set_type_exports, {
|
|
3337
|
-
fieldSetTypeCommand: () => fieldSetTypeCommand
|
|
3338
|
-
});
|
|
3339
|
-
async function fieldSetTypeCommand(tableSlug, fieldSlug, newType, options) {
|
|
3340
|
-
if (!tableSlug || !fieldSlug || !newType) {
|
|
3341
|
-
fail(
|
|
3342
|
-
"Usage: `dashwise field set-type <table-slug> <field-slug> <new-type>`"
|
|
3343
|
-
);
|
|
3910
|
+
const slug = manifest.module?.slug;
|
|
3911
|
+
if (!slug) {
|
|
3912
|
+
fail(`module.json has no ${code("module.slug")} \u2014 the manifest is malformed.`);
|
|
3344
3913
|
return 1;
|
|
3345
3914
|
}
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
);
|
|
3915
|
+
const cfg = readConfig();
|
|
3916
|
+
if (!cfg) {
|
|
3917
|
+
fail(`Not logged in. Run ${code("dashwise login")} first.`);
|
|
3350
3918
|
return 1;
|
|
3351
3919
|
}
|
|
3352
|
-
|
|
3920
|
+
const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
|
|
3921
|
+
let moduleId;
|
|
3353
3922
|
try {
|
|
3354
|
-
|
|
3355
|
-
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
3356
|
-
});
|
|
3923
|
+
moduleId = (await getModuleBySlug(slug, auth)).id;
|
|
3357
3924
|
} catch (err) {
|
|
3358
|
-
|
|
3925
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
3926
|
+
fail(
|
|
3927
|
+
`Module ${code(slug)} isn't registered on the backend yet. Run ${code("dashwise init")} (or ${code("dashwise module register")}) first.`
|
|
3928
|
+
);
|
|
3929
|
+
return 1;
|
|
3930
|
+
}
|
|
3931
|
+
fail(`Could not resolve module ${code(slug)}: ${err.message}`);
|
|
3359
3932
|
return 1;
|
|
3360
3933
|
}
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3934
|
+
const entry = readModuleEntry(slug);
|
|
3935
|
+
const dwkAuth = entry?.apiKey ? { baseUrl: resolveApiUrl(), auth: { token: entry.apiKey } } : null;
|
|
3936
|
+
let remote = null;
|
|
3937
|
+
let previewUnavailableReason = null;
|
|
3938
|
+
if (!dwkAuth) {
|
|
3939
|
+
previewUnavailableReason = `no dev-install ${code("dwk_")} key is cached for ${code(slug)} in auth.json`;
|
|
3940
|
+
} else {
|
|
3941
|
+
try {
|
|
3942
|
+
remote = (await getInstallationSchema(dwkAuth)).manifest;
|
|
3943
|
+
} catch (err) {
|
|
3944
|
+
const detail = err instanceof ApiError ? `${err.code} ${err.status}: ${err.message}` : err.message;
|
|
3945
|
+
previewUnavailableReason = `reading the dev-install manifest failed (${detail}) \u2014 the cached key may be expired or revoked`;
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
if (remote) {
|
|
3949
|
+
const delta = computeDestructiveDelta(
|
|
3950
|
+
manifest,
|
|
3951
|
+
remote
|
|
3370
3952
|
);
|
|
3371
|
-
|
|
3372
|
-
|
|
3953
|
+
if (delta.tablesRemoved.length > 0 || delta.fieldsRemoved.length > 0) {
|
|
3954
|
+
warn(`This push REMOVES schema from the dev install (live DDL):`);
|
|
3955
|
+
for (const t of delta.tablesRemoved) {
|
|
3956
|
+
out(` - table ${code(t)} ${dim("(soft-deleted; rows recoverable for 30 days via trash)")}`);
|
|
3957
|
+
}
|
|
3958
|
+
for (const f of delta.fieldsRemoved) {
|
|
3959
|
+
out(` - field ${code(f)} ${dim("(column dropped)")}`);
|
|
3960
|
+
}
|
|
3961
|
+
if (!options.yes) {
|
|
3962
|
+
if (!process.stdout.isTTY) {
|
|
3963
|
+
fail(
|
|
3964
|
+
`Refusing the destructive push without confirmation. Re-run with ${code("--yes")} to apply the removals.`
|
|
3965
|
+
);
|
|
3966
|
+
return 1;
|
|
3967
|
+
}
|
|
3968
|
+
const answer = await select({
|
|
3969
|
+
message: `Apply these removals to the dev install?`,
|
|
3970
|
+
options: [
|
|
3971
|
+
{ value: "apply", label: "Apply \u2014 remove the tables/fields listed above" },
|
|
3972
|
+
{ value: "cancel", label: "Cancel \u2014 nothing is changed" }
|
|
3973
|
+
],
|
|
3974
|
+
initialValue: "cancel"
|
|
3975
|
+
});
|
|
3976
|
+
if (isCancel(answer) || answer === "cancel") {
|
|
3977
|
+
fail("Aborted. Nothing was changed.");
|
|
3978
|
+
return 130;
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
} else {
|
|
3983
|
+
warn(
|
|
3984
|
+
`Cannot preview what this push would change: ${previewUnavailableReason}.
|
|
3985
|
+
Without the preview there is no way to know whether the push REMOVES tables/fields (live DDL on the dev install).`
|
|
3986
|
+
);
|
|
3987
|
+
if (!options.yes) {
|
|
3988
|
+
fail(
|
|
3989
|
+
`Refusing to push without the destructive-change preview. Re-run with ${code("--yes")} to push anyway (the backend still validates the apply).`
|
|
3990
|
+
);
|
|
3991
|
+
return 1;
|
|
3992
|
+
}
|
|
3993
|
+
info(
|
|
3994
|
+
dim(
|
|
3995
|
+
`--yes passed \u2014 pushing WITHOUT the destructive-change preview.`
|
|
3996
|
+
)
|
|
3997
|
+
);
|
|
3998
|
+
}
|
|
3999
|
+
info(`Pushing module.json to the dev install of ${code(slug)} (module id=${moduleId})\u2026`);
|
|
4000
|
+
let applied;
|
|
4001
|
+
try {
|
|
4002
|
+
applied = await applyDraftManifest(
|
|
4003
|
+
moduleId,
|
|
4004
|
+
manifest,
|
|
4005
|
+
auth
|
|
3373
4006
|
);
|
|
3374
4007
|
} catch (err) {
|
|
3375
|
-
return
|
|
4008
|
+
return surfaceSchemaPushError(err, slug);
|
|
4009
|
+
}
|
|
4010
|
+
printApplySummary(applied);
|
|
4011
|
+
if (dwkAuth) {
|
|
4012
|
+
try {
|
|
4013
|
+
const { manifest: canonical } = await getInstallationSchema(dwkAuth);
|
|
4014
|
+
const localNow = (() => {
|
|
4015
|
+
try {
|
|
4016
|
+
return readManifest(projectRoot);
|
|
4017
|
+
} catch {
|
|
4018
|
+
return null;
|
|
4019
|
+
}
|
|
4020
|
+
})();
|
|
4021
|
+
const { merged, preserved } = mergeRemoteManifest(localNow, canonical);
|
|
4022
|
+
writeManifest(merged, projectRoot);
|
|
4023
|
+
if (preserved.length > 0) {
|
|
4024
|
+
info(
|
|
4025
|
+
dim(
|
|
4026
|
+
`[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.`
|
|
4027
|
+
)
|
|
4028
|
+
);
|
|
4029
|
+
}
|
|
4030
|
+
} catch (err) {
|
|
4031
|
+
warn(
|
|
4032
|
+
`Push succeeded but the local sync-back failed: ${err.message}. Local module.json is what you pushed; the next sync reconciles.`
|
|
4033
|
+
);
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
return 0;
|
|
4037
|
+
}
|
|
4038
|
+
function printApplySummary(applied) {
|
|
4039
|
+
const created = asArray(applied.tables_created);
|
|
4040
|
+
const updated = asArray(applied.tables_updated);
|
|
4041
|
+
const removed = asArray(applied.tables_removed);
|
|
4042
|
+
success(`Manifest applied to the dev install.`);
|
|
4043
|
+
if (created.length === 0 && updated.length === 0 && removed.length === 0) {
|
|
4044
|
+
info(dim("No table changes (metadata-only or already in sync)."));
|
|
4045
|
+
return;
|
|
4046
|
+
}
|
|
4047
|
+
for (const t of created) {
|
|
4048
|
+
const slug = strProp(t, "slug") ?? "(table)";
|
|
4049
|
+
const fields = asArray(t.fields_created);
|
|
4050
|
+
out(` + table ${code(slug)}${fields.length > 0 ? dim(` (${fields.length} field${fields.length === 1 ? "" : "s"})`) : ""}`);
|
|
4051
|
+
}
|
|
4052
|
+
for (const t of updated) {
|
|
4053
|
+
const slug = strProp(t, "slug") ?? "(table)";
|
|
4054
|
+
const added = asArray(t.fields_added);
|
|
4055
|
+
const dropped = asArray(t.fields_removed);
|
|
4056
|
+
const bits = [];
|
|
4057
|
+
if (added.length > 0) bits.push(`+${added.length} field${added.length === 1 ? "" : "s"}`);
|
|
4058
|
+
if (dropped.length > 0) bits.push(`-${dropped.length} field${dropped.length === 1 ? "" : "s"}`);
|
|
4059
|
+
out(` ~ table ${code(slug)}${bits.length > 0 ? dim(` (${bits.join(", ")})`) : ""}`);
|
|
4060
|
+
}
|
|
4061
|
+
for (const t of removed) {
|
|
4062
|
+
out(` - table ${code(typeof t === "string" ? t : JSON.stringify(t))}`);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
function asArray(v) {
|
|
4066
|
+
return Array.isArray(v) ? v : [];
|
|
4067
|
+
}
|
|
4068
|
+
function strProp(v, key) {
|
|
4069
|
+
if (v && typeof v === "object" && typeof v[key] === "string") {
|
|
4070
|
+
return v[key];
|
|
4071
|
+
}
|
|
4072
|
+
return void 0;
|
|
4073
|
+
}
|
|
4074
|
+
function surfaceSchemaPushError(err, slug) {
|
|
4075
|
+
if (err instanceof ApiError) {
|
|
4076
|
+
switch (err.code) {
|
|
4077
|
+
case "MANIFEST_INVALID":
|
|
4078
|
+
fail(`Manifest rejected by the backend validator: ${err.message}`);
|
|
4079
|
+
for (const issue of extractIssues2(err.context)) {
|
|
4080
|
+
out(` ${dim("-")} ${issue}`);
|
|
4081
|
+
}
|
|
4082
|
+
return 1;
|
|
4083
|
+
case "DRAFT_INSTALLATION_MISSING":
|
|
4084
|
+
fail(
|
|
4085
|
+
`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.
|
|
4086
|
+
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.`
|
|
4087
|
+
);
|
|
4088
|
+
return 1;
|
|
4089
|
+
case "UNIQUE_INDEX_BACKFILL_CONFLICT":
|
|
4090
|
+
fail(
|
|
4091
|
+
`Cannot create a UNIQUE index \u2014 existing live rows already violate it: ${err.message}
|
|
4092
|
+
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")}.`
|
|
4093
|
+
);
|
|
4094
|
+
return 1;
|
|
4095
|
+
case "CHECK_CONSTRAINT_BACKFILL_CONFLICT":
|
|
4096
|
+
fail(
|
|
4097
|
+
`Cannot add the CHECK constraint \u2014 existing rows violate its expression: ${err.message}
|
|
4098
|
+
Fix or remove the violating rows, then re-run ${code("dashwise schema push")}.`
|
|
4099
|
+
);
|
|
4100
|
+
return 1;
|
|
4101
|
+
case "UNAUTHENTICATED":
|
|
4102
|
+
fail(
|
|
4103
|
+
`Auth rejected by the backend \u2014 your CLI token may be stale. Re-run ${code("dashwise login")} and try again.`
|
|
4104
|
+
);
|
|
4105
|
+
return 1;
|
|
4106
|
+
case "FORBIDDEN":
|
|
4107
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
4108
|
+
fail(
|
|
4109
|
+
`Not authorized: pushing a draft manifest requires membership of the module's owner workspace.`
|
|
4110
|
+
);
|
|
4111
|
+
return 1;
|
|
4112
|
+
default:
|
|
4113
|
+
if (err.status === 400) {
|
|
4114
|
+
fail(`Manifest rejected: ${err.message}`);
|
|
4115
|
+
for (const issue of extractIssues2(err.context)) {
|
|
4116
|
+
out(` ${dim("-")} ${issue}`);
|
|
4117
|
+
}
|
|
4118
|
+
return 1;
|
|
4119
|
+
}
|
|
4120
|
+
fail(`Schema push failed (${err.code} ${err.status}): ${err.message}`);
|
|
4121
|
+
return 1;
|
|
4122
|
+
}
|
|
3376
4123
|
}
|
|
3377
|
-
|
|
4124
|
+
fail(`Schema push failed: ${err.message}`);
|
|
4125
|
+
return 1;
|
|
3378
4126
|
}
|
|
3379
|
-
|
|
3380
|
-
|
|
4127
|
+
function extractIssues2(context) {
|
|
4128
|
+
if (!context) return [];
|
|
4129
|
+
const raw = context.issues;
|
|
4130
|
+
if (!Array.isArray(raw)) return [];
|
|
4131
|
+
return raw.map((i) => {
|
|
4132
|
+
if (typeof i === "string") return i;
|
|
4133
|
+
if (i && typeof i === "object") {
|
|
4134
|
+
const o = i;
|
|
4135
|
+
const path = typeof o.path === "string" ? o.path : "";
|
|
4136
|
+
const message = typeof o.message === "string" ? o.message : JSON.stringify(i);
|
|
4137
|
+
return path ? `${path}: ${message}` : message;
|
|
4138
|
+
}
|
|
4139
|
+
return String(i);
|
|
4140
|
+
});
|
|
4141
|
+
}
|
|
4142
|
+
var init_push = __esm({
|
|
4143
|
+
"src/commands/schema/push.ts"() {
|
|
3381
4144
|
init_api_client();
|
|
3382
|
-
|
|
3383
|
-
|
|
4145
|
+
init_config();
|
|
4146
|
+
init_manifest_io();
|
|
4147
|
+
init_manifest_merge();
|
|
3384
4148
|
init_output();
|
|
3385
|
-
init_create();
|
|
3386
4149
|
}
|
|
3387
4150
|
});
|
|
3388
4151
|
|
|
3389
|
-
// src/
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
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`
|
|
4152
|
+
// src/lib/field-spec.ts
|
|
4153
|
+
function parseFieldSpec(spec) {
|
|
4154
|
+
const parts = spec.split(":");
|
|
4155
|
+
if (parts.length < 2) {
|
|
4156
|
+
throw new FieldSpecError(
|
|
4157
|
+
`Field spec ${quote(spec)} is malformed \u2014 expected ${quote("<slug>:<type>[:flags]")}.`
|
|
3421
4158
|
);
|
|
3422
4159
|
}
|
|
3423
|
-
|
|
3424
|
-
|
|
4160
|
+
const [slug, type, ...flagParts] = parts;
|
|
4161
|
+
if (!SLUG_REGEX.test(slug)) {
|
|
4162
|
+
throw new FieldSpecError(
|
|
4163
|
+
`Field slug ${quote(slug)} must match ${quote(SLUG_REGEX.source)} (snake_case, 2+ chars).`
|
|
4164
|
+
);
|
|
3425
4165
|
}
|
|
3426
|
-
if (
|
|
3427
|
-
|
|
4166
|
+
if (!FIELD_TYPES.includes(type)) {
|
|
4167
|
+
throw new FieldSpecError(
|
|
4168
|
+
`Field type ${quote(type)} is not a known type. Allowed: ${FIELD_TYPES.join(", ")}.`
|
|
4169
|
+
);
|
|
3428
4170
|
}
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
4171
|
+
const result = { slug, type };
|
|
4172
|
+
if (flagParts.length > 0) {
|
|
4173
|
+
const flags = flagParts.flatMap((segment) => segment.split(",")).map((f) => f.trim()).filter((f) => f.length > 0);
|
|
4174
|
+
for (const flag of flags) {
|
|
4175
|
+
if (flag === "unique") {
|
|
4176
|
+
throw new FieldSpecError(
|
|
4177
|
+
`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")}.`
|
|
4178
|
+
);
|
|
4179
|
+
}
|
|
4180
|
+
if (!KNOWN_FLAGS.has(flag)) {
|
|
4181
|
+
throw new FieldSpecError(
|
|
4182
|
+
`Unknown flag ${quote(flag)} in field spec ${quote(spec)}. Known flags: ${[...KNOWN_FLAGS].join(", ")}. For options/target/default, use ${quote("--field-json")} instead.`
|
|
4183
|
+
);
|
|
4184
|
+
}
|
|
4185
|
+
if (flag === "required") result.required = true;
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
if ((type === "single_select" || type === "multi_select") && result.options === void 0) {
|
|
4189
|
+
throw new FieldSpecError(
|
|
4190
|
+
`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"]}'`)}.`
|
|
3438
4191
|
);
|
|
3439
4192
|
}
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
out(
|
|
3449
|
-
` ${pc.yellow("\u25CB unbound")} ${dim("uses")} ${code(b.uses_slug)} ${dim(`reason: ${b.reason ?? "(unspecified)"}`)}`
|
|
4193
|
+
if (type === "link_row" && result.target === void 0) {
|
|
4194
|
+
throw new FieldSpecError(
|
|
4195
|
+
`Type ${quote("link_row")} requires a ${quote("target")}. Use ${quote("--field-json")}.`
|
|
4196
|
+
);
|
|
4197
|
+
}
|
|
4198
|
+
if (type === "formula" || type === "ai") {
|
|
4199
|
+
throw new FieldSpecError(
|
|
4200
|
+
`Type ${quote(type)} requires extra config (${quote("formula")} or ${quote("prompt")}). Use ${quote("--field-json")}.`
|
|
3450
4201
|
);
|
|
3451
4202
|
}
|
|
4203
|
+
return result;
|
|
3452
4204
|
}
|
|
3453
|
-
|
|
4205
|
+
function parseFieldJson(json) {
|
|
4206
|
+
let parsed;
|
|
3454
4207
|
try {
|
|
3455
|
-
|
|
4208
|
+
parsed = JSON.parse(json);
|
|
3456
4209
|
} catch (err) {
|
|
3457
|
-
|
|
4210
|
+
throw new FieldSpecError(
|
|
4211
|
+
`--field-json value is not valid JSON: ${err.message}`
|
|
4212
|
+
);
|
|
3458
4213
|
}
|
|
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.`
|
|
4214
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4215
|
+
throw new FieldSpecError("--field-json must be a JSON object.");
|
|
4216
|
+
}
|
|
4217
|
+
const o = parsed;
|
|
4218
|
+
if (typeof o.slug !== "string" || !SLUG_REGEX.test(o.slug)) {
|
|
4219
|
+
throw new FieldSpecError(
|
|
4220
|
+
`--field-json missing or invalid ${quote("slug")}. Must match ${quote(SLUG_REGEX.source)}.`
|
|
3534
4221
|
);
|
|
3535
|
-
return 1;
|
|
3536
4222
|
}
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
4223
|
+
if (typeof o.type !== "string" || !FIELD_TYPES.includes(o.type)) {
|
|
4224
|
+
throw new FieldSpecError(
|
|
4225
|
+
`--field-json missing or invalid ${quote("type")}. Allowed: ${FIELD_TYPES.join(", ")}.`
|
|
4226
|
+
);
|
|
4227
|
+
}
|
|
4228
|
+
return o;
|
|
4229
|
+
}
|
|
4230
|
+
function quote(s) {
|
|
4231
|
+
return `\`${s}\``;
|
|
4232
|
+
}
|
|
4233
|
+
var FIELD_TYPES, SLUG_REGEX, KNOWN_FLAGS, FieldSpecError;
|
|
4234
|
+
var init_field_spec = __esm({
|
|
4235
|
+
"src/lib/field-spec.ts"() {
|
|
4236
|
+
FIELD_TYPES = [
|
|
4237
|
+
"text",
|
|
4238
|
+
"long_text",
|
|
4239
|
+
"number",
|
|
4240
|
+
"boolean",
|
|
4241
|
+
"date",
|
|
4242
|
+
"datetime",
|
|
4243
|
+
"single_select",
|
|
4244
|
+
"multi_select",
|
|
4245
|
+
"link_row",
|
|
4246
|
+
"file",
|
|
4247
|
+
"multi_file",
|
|
4248
|
+
"email",
|
|
4249
|
+
"url",
|
|
4250
|
+
"phone",
|
|
4251
|
+
"formula",
|
|
4252
|
+
"ai"
|
|
4253
|
+
];
|
|
4254
|
+
SLUG_REGEX = /^[a-z][a-z0-9_]*[a-z0-9]$/;
|
|
4255
|
+
KNOWN_FLAGS = /* @__PURE__ */ new Set(["required"]);
|
|
4256
|
+
FieldSpecError = class extends Error {
|
|
4257
|
+
constructor(message) {
|
|
4258
|
+
super(message);
|
|
4259
|
+
this.name = "FieldSpecError";
|
|
4260
|
+
}
|
|
4261
|
+
};
|
|
3543
4262
|
}
|
|
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
|
-
);
|
|
4263
|
+
});
|
|
4264
|
+
|
|
4265
|
+
// src/commands/table/create.ts
|
|
4266
|
+
var create_exports = {};
|
|
4267
|
+
__export(create_exports, {
|
|
4268
|
+
surfaceFailure: () => surfaceFailure,
|
|
4269
|
+
syncLocalManifest: () => syncLocalManifest,
|
|
4270
|
+
tableCreateCommand: () => tableCreateCommand
|
|
4271
|
+
});
|
|
4272
|
+
async function tableCreateCommand(slug, options) {
|
|
4273
|
+
if (!slug) {
|
|
4274
|
+
fail("Usage: `dashwise table create <slug> --field <slug>:<type>[:required] [...]`");
|
|
3564
4275
|
return 1;
|
|
3565
4276
|
}
|
|
3566
|
-
const
|
|
3567
|
-
let port;
|
|
4277
|
+
const fields = [];
|
|
3568
4278
|
try {
|
|
3569
|
-
|
|
4279
|
+
for (const spec of options.field ?? []) {
|
|
4280
|
+
fields.push(parseFieldSpec(spec));
|
|
4281
|
+
}
|
|
4282
|
+
for (const json of options.fieldJson ?? []) {
|
|
4283
|
+
fields.push(parseFieldJson(json));
|
|
4284
|
+
}
|
|
3570
4285
|
} 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) {
|
|
4286
|
+
if (err instanceof FieldSpecError) {
|
|
3594
4287
|
fail(err.message);
|
|
3595
4288
|
return 1;
|
|
3596
4289
|
}
|
|
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
|
-
}
|
|
4290
|
+
throw err;
|
|
4291
|
+
}
|
|
4292
|
+
if (fields.length === 0) {
|
|
4293
|
+
fail(
|
|
4294
|
+
`At least one field is required. Use ${code("--field <slug>:<type>")} (repeatable) or ${code("--field-json '{...}'")} for fields with options/target/default.`
|
|
4295
|
+
);
|
|
4296
|
+
return 1;
|
|
4297
|
+
}
|
|
4298
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4299
|
+
for (const f of fields) {
|
|
4300
|
+
if (seen.has(f.slug)) {
|
|
4301
|
+
fail(`Duplicate field slug ${code(f.slug)} in --field flags.`);
|
|
3614
4302
|
return 1;
|
|
3615
4303
|
}
|
|
4304
|
+
seen.add(f.slug);
|
|
3616
4305
|
}
|
|
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)`);
|
|
4306
|
+
let ctx;
|
|
4307
|
+
try {
|
|
4308
|
+
ctx = loadSchemaCommandContext({
|
|
4309
|
+
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
4310
|
+
});
|
|
4311
|
+
} catch (err) {
|
|
4312
|
+
fail(err.message);
|
|
4313
|
+
return 1;
|
|
3629
4314
|
}
|
|
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.`
|
|
4315
|
+
info(`Creating table ${code(slug)} in installation ${ctx.entry.installationId}\u2026`);
|
|
4316
|
+
try {
|
|
4317
|
+
const result = await createTable(
|
|
4318
|
+
{ slug, fields },
|
|
4319
|
+
ctx.requestOptions
|
|
3640
4320
|
);
|
|
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}`)}.)`
|
|
4321
|
+
success(
|
|
4322
|
+
`Created table ${code(slug)} (physical_table_id=${result.physical_table_id}, manifest_version=${result.manifest_version})`
|
|
3645
4323
|
);
|
|
4324
|
+
} catch (err) {
|
|
4325
|
+
return surfaceFailure(err);
|
|
3646
4326
|
}
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
4327
|
+
return syncLocalManifest(ctx);
|
|
4328
|
+
}
|
|
4329
|
+
async function syncLocalManifest(ctx) {
|
|
4330
|
+
try {
|
|
4331
|
+
const { manifest } = await getInstallationSchema(ctx.requestOptions);
|
|
4332
|
+
const local = (() => {
|
|
4333
|
+
try {
|
|
4334
|
+
return readManifest(ctx.projectRoot);
|
|
4335
|
+
} catch {
|
|
4336
|
+
return null;
|
|
4337
|
+
}
|
|
4338
|
+
})();
|
|
4339
|
+
const { merged, preserved } = mergeRemoteManifest(local, manifest);
|
|
4340
|
+
writeManifest(merged, ctx.projectRoot);
|
|
4341
|
+
if (preserved.length > 0) {
|
|
4342
|
+
info(PRESERVED_BLOCKS_NOTICE);
|
|
4343
|
+
}
|
|
4344
|
+
return 0;
|
|
4345
|
+
} catch (err) {
|
|
4346
|
+
if (err instanceof ApiError) {
|
|
4347
|
+
warn(
|
|
4348
|
+
`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.`
|
|
4349
|
+
);
|
|
4350
|
+
} else {
|
|
4351
|
+
warn(
|
|
4352
|
+
`Schema sync to local module.json failed: ${err.message}. The mutation succeeded server-side.`
|
|
4353
|
+
);
|
|
4354
|
+
}
|
|
4355
|
+
return 0;
|
|
3654
4356
|
}
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
4357
|
+
}
|
|
4358
|
+
function surfaceFailure(err) {
|
|
4359
|
+
if (err instanceof ApiError) {
|
|
4360
|
+
switch (err.code) {
|
|
4361
|
+
case "TABLE_SLUG_EXISTS":
|
|
4362
|
+
fail(`A table with this slug already exists in the manifest.`);
|
|
4363
|
+
break;
|
|
4364
|
+
case "FIELD_SLUG_EXISTS":
|
|
4365
|
+
fail(`A field with this slug already exists on the table.`);
|
|
4366
|
+
break;
|
|
4367
|
+
case "TABLE_NOT_FOUND":
|
|
4368
|
+
fail(`Table not found in the manifest.`);
|
|
4369
|
+
break;
|
|
4370
|
+
case "FIELD_NOT_FOUND":
|
|
4371
|
+
fail(`Field not found on the table.`);
|
|
4372
|
+
break;
|
|
4373
|
+
case "UNAUTHENTICATED":
|
|
4374
|
+
fail(
|
|
4375
|
+
`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).`
|
|
4376
|
+
);
|
|
4377
|
+
break;
|
|
4378
|
+
case "SCHEMA_LOCKED":
|
|
4379
|
+
fail(
|
|
4380
|
+
`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).`
|
|
4381
|
+
);
|
|
4382
|
+
break;
|
|
4383
|
+
case "INSTALL_MISMATCH":
|
|
4384
|
+
fail(
|
|
4385
|
+
`API key is bound to a different installation than the URL. The cached install id may be stale \u2014 re-run \`dashwise init <slug>\`.`
|
|
4386
|
+
);
|
|
4387
|
+
break;
|
|
4388
|
+
default:
|
|
4389
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
4390
|
+
}
|
|
4391
|
+
return 1;
|
|
3659
4392
|
}
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
4393
|
+
fail(`Unexpected error: ${err.message}`);
|
|
4394
|
+
return 1;
|
|
4395
|
+
}
|
|
4396
|
+
var init_create = __esm({
|
|
4397
|
+
"src/commands/table/create.ts"() {
|
|
4398
|
+
init_manifest_io();
|
|
4399
|
+
init_api_client();
|
|
4400
|
+
init_manifest_merge();
|
|
4401
|
+
init_schema_command_context();
|
|
4402
|
+
init_field_spec();
|
|
4403
|
+
init_output();
|
|
3663
4404
|
}
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
4405
|
+
});
|
|
4406
|
+
|
|
4407
|
+
// src/commands/table/delete.ts
|
|
4408
|
+
var delete_exports = {};
|
|
4409
|
+
__export(delete_exports, {
|
|
4410
|
+
tableDeleteCommand: () => tableDeleteCommand
|
|
4411
|
+
});
|
|
4412
|
+
async function tableDeleteCommand(slug, options) {
|
|
4413
|
+
if (!slug) {
|
|
4414
|
+
fail("Usage: `dashwise table delete <slug>`");
|
|
4415
|
+
return 1;
|
|
3667
4416
|
}
|
|
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
|
-
}
|
|
4417
|
+
let ctx;
|
|
4418
|
+
try {
|
|
4419
|
+
ctx = loadSchemaCommandContext({
|
|
4420
|
+
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
3692
4421
|
});
|
|
3693
|
-
|
|
3694
|
-
|
|
4422
|
+
} catch (err) {
|
|
4423
|
+
fail(err.message);
|
|
4424
|
+
return 1;
|
|
4425
|
+
}
|
|
4426
|
+
info(`Dropping table ${code(slug)} from installation ${ctx.entry.installationId}\u2026`);
|
|
4427
|
+
try {
|
|
4428
|
+
const result = await dropTable(slug, ctx.requestOptions);
|
|
4429
|
+
success(
|
|
4430
|
+
`Dropped table ${code(slug)} (manifest_version=${result.manifest_version}). Physical table soft-deleted; recoverable for 30 days via the dashboard's trash.`
|
|
3695
4431
|
);
|
|
3696
|
-
|
|
4432
|
+
} catch (err) {
|
|
4433
|
+
return surfaceFailure(err);
|
|
3697
4434
|
}
|
|
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
|
-
}
|
|
4435
|
+
return syncLocalManifest(ctx);
|
|
4436
|
+
}
|
|
4437
|
+
var init_delete = __esm({
|
|
4438
|
+
"src/commands/table/delete.ts"() {
|
|
4439
|
+
init_api_client();
|
|
4440
|
+
init_schema_command_context();
|
|
4441
|
+
init_output();
|
|
4442
|
+
init_create();
|
|
3741
4443
|
}
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
4444
|
+
});
|
|
4445
|
+
|
|
4446
|
+
// src/commands/field/add.ts
|
|
4447
|
+
var add_exports = {};
|
|
4448
|
+
__export(add_exports, {
|
|
4449
|
+
fieldAddCommand: () => fieldAddCommand
|
|
4450
|
+
});
|
|
4451
|
+
async function fieldAddCommand(tableSlug, fieldSpec, options) {
|
|
4452
|
+
if (!tableSlug) {
|
|
4453
|
+
fail(
|
|
4454
|
+
"Usage: `dashwise field add <table-slug> <field-slug>:<type>[:required]` or `dashwise field add <table-slug> --field-json '{...}'`"
|
|
4455
|
+
);
|
|
4456
|
+
return 1;
|
|
3745
4457
|
}
|
|
3746
|
-
const
|
|
3747
|
-
|
|
3748
|
-
|
|
4458
|
+
const hasPositional = fieldSpec !== void 0;
|
|
4459
|
+
const hasJson = options.fieldJson !== void 0;
|
|
4460
|
+
if (!hasPositional && !hasJson) {
|
|
4461
|
+
fail(
|
|
4462
|
+
`Pass either a field spec (e.g. ${code("title:text:required")}) or ${code("--field-json")} with the field's JSON definition.`
|
|
4463
|
+
);
|
|
4464
|
+
return 1;
|
|
3749
4465
|
}
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
4466
|
+
if (hasPositional && hasJson) {
|
|
4467
|
+
fail(
|
|
4468
|
+
`Pass only ONE of the field spec or ${code("--field-json")} \u2014 they're mutually exclusive.`
|
|
4469
|
+
);
|
|
4470
|
+
return 1;
|
|
4471
|
+
}
|
|
4472
|
+
let field;
|
|
4473
|
+
try {
|
|
4474
|
+
field = hasJson ? parseFieldJson(options.fieldJson) : parseFieldSpec(fieldSpec);
|
|
4475
|
+
} catch (err) {
|
|
4476
|
+
if (err instanceof FieldSpecError) {
|
|
4477
|
+
fail(err.message);
|
|
4478
|
+
return 1;
|
|
3760
4479
|
}
|
|
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) {
|
|
4480
|
+
throw err;
|
|
4481
|
+
}
|
|
4482
|
+
let ctx;
|
|
3779
4483
|
try {
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
return
|
|
4484
|
+
ctx = loadSchemaCommandContext({
|
|
4485
|
+
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
4486
|
+
});
|
|
4487
|
+
} catch (err) {
|
|
4488
|
+
fail(err.message);
|
|
4489
|
+
return 1;
|
|
3786
4490
|
}
|
|
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
4491
|
info(
|
|
3820
|
-
`
|
|
4492
|
+
`Adding field ${code(`${tableSlug}.${field.slug}`)} (${field.type}) to installation ${ctx.entry.installationId}\u2026`
|
|
3821
4493
|
);
|
|
3822
|
-
let minted;
|
|
3823
4494
|
try {
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
{ baseUrl: apiUrl, auth: { token: cliBearer } }
|
|
4495
|
+
const result = await addField(
|
|
4496
|
+
{ table_slug: tableSlug, field },
|
|
4497
|
+
ctx.requestOptions
|
|
3828
4498
|
);
|
|
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.`
|
|
4499
|
+
success(
|
|
4500
|
+
`Added field ${code(`${tableSlug}.${field.slug}`)} (manifest_version=${result.manifest_version})`
|
|
3839
4501
|
);
|
|
3840
|
-
|
|
4502
|
+
} catch (err) {
|
|
4503
|
+
return surfaceFailure(err);
|
|
3841
4504
|
}
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
4505
|
+
return syncLocalManifest(ctx);
|
|
4506
|
+
}
|
|
4507
|
+
var init_add = __esm({
|
|
4508
|
+
"src/commands/field/add.ts"() {
|
|
4509
|
+
init_api_client();
|
|
4510
|
+
init_schema_command_context();
|
|
4511
|
+
init_field_spec();
|
|
4512
|
+
init_output();
|
|
4513
|
+
init_create();
|
|
4514
|
+
}
|
|
4515
|
+
});
|
|
4516
|
+
|
|
4517
|
+
// src/commands/field/remove.ts
|
|
4518
|
+
var remove_exports = {};
|
|
4519
|
+
__export(remove_exports, {
|
|
4520
|
+
fieldRemoveCommand: () => fieldRemoveCommand
|
|
4521
|
+
});
|
|
4522
|
+
async function fieldRemoveCommand(tableSlug, fieldSlug, options) {
|
|
4523
|
+
if (!tableSlug || !fieldSlug) {
|
|
4524
|
+
fail("Usage: `dashwise field remove <table-slug> <field-slug>`");
|
|
4525
|
+
return 1;
|
|
4526
|
+
}
|
|
4527
|
+
let ctx;
|
|
3848
4528
|
try {
|
|
3849
|
-
|
|
4529
|
+
ctx = loadSchemaCommandContext({
|
|
4530
|
+
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
4531
|
+
});
|
|
3850
4532
|
} catch (err) {
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
4533
|
+
fail(err.message);
|
|
4534
|
+
return 1;
|
|
4535
|
+
}
|
|
4536
|
+
info(
|
|
4537
|
+
`Removing field ${code(`${tableSlug}.${fieldSlug}`)} from installation ${ctx.entry.installationId}\u2026`
|
|
4538
|
+
);
|
|
4539
|
+
try {
|
|
4540
|
+
const result = await removeField(
|
|
4541
|
+
tableSlug,
|
|
4542
|
+
fieldSlug,
|
|
4543
|
+
ctx.requestOptions
|
|
4544
|
+
);
|
|
4545
|
+
success(
|
|
4546
|
+
`Removed field ${code(`${tableSlug}.${fieldSlug}`)} (manifest_version=${result.manifest_version})`
|
|
3854
4547
|
);
|
|
4548
|
+
} catch (err) {
|
|
4549
|
+
return surfaceFailure(err);
|
|
3855
4550
|
}
|
|
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]));
|
|
4551
|
+
return syncLocalManifest(ctx);
|
|
3862
4552
|
}
|
|
3863
|
-
var
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
4553
|
+
var init_remove = __esm({
|
|
4554
|
+
"src/commands/field/remove.ts"() {
|
|
4555
|
+
init_api_client();
|
|
4556
|
+
init_schema_command_context();
|
|
4557
|
+
init_output();
|
|
4558
|
+
init_create();
|
|
4559
|
+
}
|
|
4560
|
+
});
|
|
4561
|
+
|
|
4562
|
+
// src/commands/field/set-type.ts
|
|
4563
|
+
var set_type_exports = {};
|
|
4564
|
+
__export(set_type_exports, {
|
|
4565
|
+
fieldSetTypeCommand: () => fieldSetTypeCommand
|
|
4566
|
+
});
|
|
4567
|
+
async function fieldSetTypeCommand(tableSlug, fieldSlug, newType, options) {
|
|
4568
|
+
if (!tableSlug || !fieldSlug || !newType) {
|
|
4569
|
+
fail(
|
|
4570
|
+
"Usage: `dashwise field set-type <table-slug> <field-slug> <new-type>`"
|
|
3871
4571
|
);
|
|
4572
|
+
return 1;
|
|
3872
4573
|
}
|
|
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.`
|
|
4574
|
+
if (!FIELD_TYPES.includes(newType)) {
|
|
4575
|
+
fail(
|
|
4576
|
+
`Type ${code(newType)} is not a known type. Allowed: ${FIELD_TYPES.join(", ")}.`
|
|
3877
4577
|
);
|
|
4578
|
+
return 1;
|
|
3878
4579
|
}
|
|
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));
|
|
4580
|
+
let ctx;
|
|
4581
|
+
try {
|
|
4582
|
+
ctx = loadSchemaCommandContext({
|
|
4583
|
+
...options.dir !== void 0 ? { dir: options.dir } : {}
|
|
3890
4584
|
});
|
|
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;
|
|
4585
|
+
} catch (err) {
|
|
4586
|
+
fail(err.message);
|
|
4587
|
+
return 1;
|
|
3898
4588
|
}
|
|
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 } }
|
|
4589
|
+
info(
|
|
4590
|
+
`Setting type of ${code(`${tableSlug}.${fieldSlug}`)} \u2192 ${code(newType)} on installation ${ctx.entry.installationId}\u2026`
|
|
3907
4591
|
);
|
|
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}`
|
|
4592
|
+
try {
|
|
4593
|
+
const result = await updateField(
|
|
4594
|
+
tableSlug,
|
|
4595
|
+
fieldSlug,
|
|
4596
|
+
{ type: newType },
|
|
4597
|
+
ctx.requestOptions
|
|
3921
4598
|
);
|
|
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}`
|
|
4599
|
+
success(
|
|
4600
|
+
`Updated ${code(`${tableSlug}.${fieldSlug}`)} \u2192 ${code(newType)} (manifest_version=${result.manifest_version})`
|
|
3931
4601
|
);
|
|
4602
|
+
} catch (err) {
|
|
4603
|
+
return surfaceFailure(err);
|
|
3932
4604
|
}
|
|
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
|
-
);
|
|
4605
|
+
return syncLocalManifest(ctx);
|
|
3950
4606
|
}
|
|
4607
|
+
var init_set_type = __esm({
|
|
4608
|
+
"src/commands/field/set-type.ts"() {
|
|
4609
|
+
init_api_client();
|
|
4610
|
+
init_schema_command_context();
|
|
4611
|
+
init_field_spec();
|
|
4612
|
+
init_output();
|
|
4613
|
+
init_create();
|
|
4614
|
+
}
|
|
4615
|
+
});
|
|
4616
|
+
|
|
4617
|
+
// src/bin.ts
|
|
4618
|
+
init_dev();
|
|
3951
4619
|
|
|
3952
4620
|
// src/commands/login.ts
|
|
3953
4621
|
init_api_client();
|
|
@@ -5534,6 +6202,7 @@ async function moduleInstallCommand(slugArg, options) {
|
|
|
5534
6202
|
if (isInstallGated(result)) {
|
|
5535
6203
|
if (options.json) {
|
|
5536
6204
|
out(JSON.stringify(result));
|
|
6205
|
+
if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
|
|
5537
6206
|
return 0;
|
|
5538
6207
|
}
|
|
5539
6208
|
success(
|
|
@@ -5542,6 +6211,7 @@ async function moduleInstallCommand(slugArg, options) {
|
|
|
5542
6211
|
info(
|
|
5543
6212
|
`This module requires author approval before it installs. Once approved, check with ${code(`dashwise module status ${mod.slug} --workspace ${workspace2.slug}`)}.`
|
|
5544
6213
|
);
|
|
6214
|
+
if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
|
|
5545
6215
|
return 0;
|
|
5546
6216
|
}
|
|
5547
6217
|
let row;
|
|
@@ -5566,6 +6236,7 @@ async function moduleInstallCommand(slugArg, options) {
|
|
|
5566
6236
|
verified
|
|
5567
6237
|
})
|
|
5568
6238
|
);
|
|
6239
|
+
if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
|
|
5569
6240
|
return 0;
|
|
5570
6241
|
}
|
|
5571
6242
|
if (!verified) {
|
|
@@ -5575,6 +6246,7 @@ async function moduleInstallCommand(slugArg, options) {
|
|
|
5575
6246
|
info(
|
|
5576
6247
|
`The verification read failed \u2014 run ${code(`dashwise module status ${mod.slug} --workspace ${workspace2.slug}`)} to see the deploy state and public URL.`
|
|
5577
6248
|
);
|
|
6249
|
+
if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
|
|
5578
6250
|
return 0;
|
|
5579
6251
|
}
|
|
5580
6252
|
if (runtimeProvisioned && publicUrl) {
|
|
@@ -5599,8 +6271,20 @@ async function moduleInstallCommand(slugArg, options) {
|
|
|
5599
6271
|
`No public URL yet \u2014 runtime provisioning didn't complete. Re-running ${code("dashwise module install")} is idempotent and repairs a failed provision.`
|
|
5600
6272
|
);
|
|
5601
6273
|
}
|
|
6274
|
+
if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
|
|
5602
6275
|
return 0;
|
|
5603
6276
|
}
|
|
6277
|
+
function publishedModuleLocalDevGuidance(slug) {
|
|
6278
|
+
return `Local development for an already-published module is not currently possible \u2014 in ANY workspace (module slugs are global; a platform fix is planned). Interim options:
|
|
6279
|
+
- 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).
|
|
6280
|
+
- Or duplicate the code under a NEW slug for greenfield iteration.`;
|
|
6281
|
+
}
|
|
6282
|
+
function printDevInstallAftermath(slug, workspaceSlug) {
|
|
6283
|
+
warn(
|
|
6284
|
+
`Local dev for ${code(slug)} is now DISABLED (the dev install in ${code(workspaceSlug)} and its ${code("dwk_")} key were destroyed).
|
|
6285
|
+
${publishedModuleLocalDevGuidance(slug)}`
|
|
6286
|
+
);
|
|
6287
|
+
}
|
|
5604
6288
|
function resolveSlug(slugArg, dir) {
|
|
5605
6289
|
if (slugArg) return slugArg;
|
|
5606
6290
|
try {
|
|
@@ -5623,9 +6307,12 @@ async function resolveDevInstallCollision(existing, slug, workspaceSlug, options
|
|
|
5623
6307
|
const explanation = `Workspace ${code(workspaceSlug)} already has the DEV install of ${code(slug)} (kind=local, id=${existing.id}) created by ${code("dashwise init")}.
|
|
5624
6308
|
Only ONE install per (workspace, module) can exist, and ${code("kind")} is immutable: a plain install would upgrade that row in place \u2014 the runtime would provision, but it would STAY a dev install, hidden from the app sidebar.
|
|
5625
6309
|
Options:
|
|
5626
|
-
- ${code("--replace-dev-install")} \u2014 destroy the dev install, then fresh production install. Recommended when development is done. Note: this
|
|
6310
|
+
- ${code("--replace-dev-install")} \u2014 destroy the dev install, then fresh production install. Recommended when development is done. Note: this PERMANENTLY disables local dev for ${code(slug)} \u2014 module slugs are global, so a dev install cannot be re-provisioned in ANY workspace once the module is 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
6311
|
- ${code("--workspace <other>")} \u2014 deploy to a separate test/client workspace and keep this dev install. Recommended for ongoing development.`;
|
|
5628
|
-
if (options.replaceDevInstall)
|
|
6312
|
+
if (options.replaceDevInstall) {
|
|
6313
|
+
info(explanation);
|
|
6314
|
+
return "replace";
|
|
6315
|
+
}
|
|
5629
6316
|
if (process.stdout.isTTY && !options.yes) {
|
|
5630
6317
|
info(explanation);
|
|
5631
6318
|
const answer = await select({
|
|
@@ -5634,7 +6321,7 @@ async function resolveDevInstallCollision(existing, slug, workspaceSlug, options
|
|
|
5634
6321
|
{
|
|
5635
6322
|
value: "replace",
|
|
5636
6323
|
label: "Replace \u2014 destroy the dev install, then production-install",
|
|
5637
|
-
hint: "local
|
|
6324
|
+
hint: "local dev for this module is permanently disabled afterwards (interim repair: scoped key on the production install)"
|
|
5638
6325
|
},
|
|
5639
6326
|
{ value: "cancel", label: "Cancel \u2014 I will deploy to another workspace" }
|
|
5640
6327
|
],
|
|
@@ -5670,9 +6357,7 @@ async function replaceDevInstall(existing, slug, auth) {
|
|
|
5670
6357
|
if (entry && entry.installationId === existing.id) {
|
|
5671
6358
|
try {
|
|
5672
6359
|
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
|
-
);
|
|
6360
|
+
info(`Removed ${code(slug)} from auth.json \u2014 its local-dev credentials are revoked.`);
|
|
5676
6361
|
} catch (err) {
|
|
5677
6362
|
warn(
|
|
5678
6363
|
`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 +6406,8 @@ function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
|
|
|
5721
6406
|
}
|
|
5722
6407
|
if (devInstallReplaced) {
|
|
5723
6408
|
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
|
|
6409
|
+
`Note: the dev install was already removed (that is not rolled back). Re-run ${code("dashwise module install")} to complete the production install.
|
|
6410
|
+
${publishedModuleLocalDevGuidance(slug)}`
|
|
5725
6411
|
);
|
|
5726
6412
|
}
|
|
5727
6413
|
return 1;
|
|
@@ -6214,7 +6900,131 @@ async function moduleStatusCommand(slugArg, options) {
|
|
|
6214
6900
|
}
|
|
6215
6901
|
if (!slug) {
|
|
6216
6902
|
fail(
|
|
6217
|
-
`No module slug \u2014 pass one (${code("dashwise module status <slug>")}) or run from a directory with a valid module.json.`
|
|
6903
|
+
`No module slug \u2014 pass one (${code("dashwise module status <slug>")}) or run from a directory with a valid module.json.`
|
|
6904
|
+
);
|
|
6905
|
+
return 1;
|
|
6906
|
+
}
|
|
6907
|
+
if (!isLoggedIn()) {
|
|
6908
|
+
fail(`Not logged in. Run ${code("dashwise login")} first.`);
|
|
6909
|
+
return 1;
|
|
6910
|
+
}
|
|
6911
|
+
const cfg = readConfig();
|
|
6912
|
+
if (!cfg) {
|
|
6913
|
+
fail(`CLI config missing. Run ${code("dashwise login")}.`);
|
|
6914
|
+
return 1;
|
|
6915
|
+
}
|
|
6916
|
+
const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
|
|
6917
|
+
let mod;
|
|
6918
|
+
try {
|
|
6919
|
+
mod = await getModuleBySlug(slug, auth);
|
|
6920
|
+
} catch (err) {
|
|
6921
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
6922
|
+
fail(`Module ${code(slug)} not found on the backend.`);
|
|
6923
|
+
return 1;
|
|
6924
|
+
}
|
|
6925
|
+
fail(`Could not resolve module ${code(slug)}: ${err.message}`);
|
|
6926
|
+
return 1;
|
|
6927
|
+
}
|
|
6928
|
+
let workspace2;
|
|
6929
|
+
try {
|
|
6930
|
+
workspace2 = await resolveWorkspaceWithFallback({
|
|
6931
|
+
userFlag: options.workspace,
|
|
6932
|
+
cachedWorkspaceId: readModuleEntry(slug)?.workspaceId,
|
|
6933
|
+
configDefault: cfg.workspaceSlug,
|
|
6934
|
+
apiUrl: resolveApiUrl(),
|
|
6935
|
+
apiToken: cfg.token
|
|
6936
|
+
});
|
|
6937
|
+
} catch (err) {
|
|
6938
|
+
fail(`Workspace selection failed: ${err.message}`);
|
|
6939
|
+
return 1;
|
|
6940
|
+
}
|
|
6941
|
+
let row;
|
|
6942
|
+
try {
|
|
6943
|
+
const installs = await listWorkspaceInstallations(workspace2.id, auth);
|
|
6944
|
+
row = installs.find((r) => r.module_id === mod.id);
|
|
6945
|
+
} catch (err) {
|
|
6946
|
+
fail(`Could not read workspace installations: ${err.message}`);
|
|
6947
|
+
return 1;
|
|
6948
|
+
}
|
|
6949
|
+
const latestVersion = row?.module?.current_version ?? mod.current_version;
|
|
6950
|
+
if (!row) {
|
|
6951
|
+
if (options.json) {
|
|
6952
|
+
out(
|
|
6953
|
+
JSON.stringify({
|
|
6954
|
+
installed: false,
|
|
6955
|
+
module: mod.slug,
|
|
6956
|
+
workspace: { id: workspace2.id, slug: workspace2.slug },
|
|
6957
|
+
latest_version: latestVersion
|
|
6958
|
+
})
|
|
6959
|
+
);
|
|
6960
|
+
return 0;
|
|
6961
|
+
}
|
|
6962
|
+
info(
|
|
6963
|
+
`${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.`)
|
|
6964
|
+
);
|
|
6965
|
+
return 0;
|
|
6966
|
+
}
|
|
6967
|
+
const runtimeProvisioned = isRealRuntimeUrl(row.runtime_url);
|
|
6968
|
+
const state = runtimeProvisioned && row.public_url ? "live" : runtimeProvisioned ? "deployed" : "not_deployed";
|
|
6969
|
+
const installedVersion = row.module_version?.version ?? null;
|
|
6970
|
+
const updateAvailable = isUpdateAvailable(installedVersion, latestVersion);
|
|
6971
|
+
if (options.json) {
|
|
6972
|
+
out(
|
|
6973
|
+
JSON.stringify({
|
|
6974
|
+
installed: true,
|
|
6975
|
+
module: mod.slug,
|
|
6976
|
+
workspace: { id: workspace2.id, slug: workspace2.slug },
|
|
6977
|
+
installation_id: row.id,
|
|
6978
|
+
kind: row.kind ?? null,
|
|
6979
|
+
version: installedVersion,
|
|
6980
|
+
latest_version: latestVersion,
|
|
6981
|
+
update_available: updateAvailable,
|
|
6982
|
+
runtime_provisioned: runtimeProvisioned,
|
|
6983
|
+
runtime_url: row.runtime_url,
|
|
6984
|
+
public_url: row.public_url,
|
|
6985
|
+
state
|
|
6986
|
+
})
|
|
6987
|
+
);
|
|
6988
|
+
return 0;
|
|
6989
|
+
}
|
|
6990
|
+
const stateLabel = state === "live" ? "Live" : state === "deployed" ? "Deployed" : "Not deployed \u2014 runtime not provisioned";
|
|
6991
|
+
out("");
|
|
6992
|
+
kv("Module:", `${mod.slug} ${dim(`(id ${mod.id})`)}`);
|
|
6993
|
+
kv("Workspace:", `${workspace2.slug} ${dim(`(id ${workspace2.id})`)}`);
|
|
6994
|
+
kv("Install:", `#${row.id} ${dim(`(kind=${row.kind ?? "unknown"})`)}`);
|
|
6995
|
+
kv(
|
|
6996
|
+
"Version:",
|
|
6997
|
+
`${installedVersion ?? dim("(unknown)")}${updateAvailable ? ` ${code(`update available: v${latestVersion} \u2014 run \`dashwise module install ${mod.slug}\``)}` : latestVersion ? ` ${dim(`(latest published: v${latestVersion})`)}` : ""}`
|
|
6998
|
+
);
|
|
6999
|
+
kv(
|
|
7000
|
+
"Runtime:",
|
|
7001
|
+
runtimeProvisioned ? `provisioned ${dim(`(${row.runtime_url})`)}` : "not provisioned"
|
|
7002
|
+
);
|
|
7003
|
+
kv("Public URL:", row.public_url ?? dim("(none)"));
|
|
7004
|
+
kv("State:", stateLabel);
|
|
7005
|
+
out("");
|
|
7006
|
+
if (row.kind === "local") {
|
|
7007
|
+
info(
|
|
7008
|
+
`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>")}).`
|
|
7009
|
+
);
|
|
7010
|
+
} else if (state === "not_deployed") {
|
|
7011
|
+
info(
|
|
7012
|
+
`Re-run ${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)} to (re-)provision the runtime \u2014 the install POST is idempotent.`
|
|
7013
|
+
);
|
|
7014
|
+
}
|
|
7015
|
+
return 0;
|
|
7016
|
+
}
|
|
7017
|
+
|
|
7018
|
+
// src/commands/module/uninstall.ts
|
|
7019
|
+
init_api_client();
|
|
7020
|
+
init_config();
|
|
7021
|
+
init_manifest_io();
|
|
7022
|
+
init_output();
|
|
7023
|
+
async function moduleUninstallCommand(slugArg, options) {
|
|
7024
|
+
const slug = resolveSlug2(slugArg, options.dir);
|
|
7025
|
+
if (!slug) {
|
|
7026
|
+
fail(
|
|
7027
|
+
`No module slug \u2014 pass one (${code("dashwise module uninstall <slug>")}) or run from a directory with a valid module.json.`
|
|
6218
7028
|
);
|
|
6219
7029
|
return 1;
|
|
6220
7030
|
}
|
|
@@ -6233,7 +7043,7 @@ async function moduleStatusCommand(slugArg, options) {
|
|
|
6233
7043
|
mod = await getModuleBySlug(slug, auth);
|
|
6234
7044
|
} catch (err) {
|
|
6235
7045
|
if (err instanceof ApiError && err.status === 404) {
|
|
6236
|
-
fail(`Module ${code(slug)} not found on the backend.`);
|
|
7046
|
+
fail(`Module ${code(slug)} not found on the backend \u2014 nothing to uninstall.`);
|
|
6237
7047
|
return 1;
|
|
6238
7048
|
}
|
|
6239
7049
|
fail(`Could not resolve module ${code(slug)}: ${err.message}`);
|
|
@@ -6260,74 +7070,119 @@ async function moduleStatusCommand(slugArg, options) {
|
|
|
6260
7070
|
fail(`Could not read workspace installations: ${err.message}`);
|
|
6261
7071
|
return 1;
|
|
6262
7072
|
}
|
|
6263
|
-
const latestVersion = row?.module?.current_version ?? mod.current_version;
|
|
6264
7073
|
if (!row) {
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
7074
|
+
fail(
|
|
7075
|
+
`${code(mod.slug)} is not installed in workspace ${code(workspace2.slug)} \u2014 nothing to uninstall.`
|
|
7076
|
+
);
|
|
7077
|
+
return 1;
|
|
7078
|
+
}
|
|
7079
|
+
if (row.kind === "local") {
|
|
7080
|
+
fail(
|
|
7081
|
+
`Install ${code(`#${row.id}`)} of ${code(mod.slug)} in ${code(workspace2.slug)} is the ${code("kind=local")} DEV install backing local development.
|
|
7082
|
+
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.`
|
|
7083
|
+
);
|
|
7084
|
+
return 1;
|
|
7085
|
+
}
|
|
7086
|
+
const scriptedOk = options.yes === true && options.workspace !== void 0;
|
|
7087
|
+
if (!scriptedOk) {
|
|
7088
|
+
if (!process.stdout.isTTY) {
|
|
7089
|
+
fail(
|
|
7090
|
+
`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
7091
|
);
|
|
6274
|
-
return
|
|
7092
|
+
return 1;
|
|
6275
7093
|
}
|
|
6276
7094
|
info(
|
|
6277
|
-
|
|
7095
|
+
`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)}.
|
|
7096
|
+
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.
|
|
7097
|
+
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
7098
|
);
|
|
6279
|
-
|
|
7099
|
+
const answer = await select({
|
|
7100
|
+
message: `Uninstall ${code(mod.slug)} from ${code(workspace2.slug)}?`,
|
|
7101
|
+
options: [
|
|
7102
|
+
{ value: "uninstall", label: "Uninstall \u2014 soft-delete the app (operator-restorable ~7 days)" },
|
|
7103
|
+
{ value: "cancel", label: "Cancel \u2014 nothing is changed" }
|
|
7104
|
+
],
|
|
7105
|
+
initialValue: "cancel"
|
|
7106
|
+
});
|
|
7107
|
+
if (isCancel(answer) || answer === "cancel") {
|
|
7108
|
+
fail("Aborted. Nothing was changed.");
|
|
7109
|
+
return 130;
|
|
7110
|
+
}
|
|
7111
|
+
const typed = await text({
|
|
7112
|
+
message: `Type the module slug (${code(mod.slug)}) to confirm:`
|
|
7113
|
+
});
|
|
7114
|
+
if (isCancel(typed) || typed !== mod.slug) {
|
|
7115
|
+
fail(
|
|
7116
|
+
`Confirmation did not match ${code(mod.slug)}. Aborted \u2014 nothing was changed.`
|
|
7117
|
+
);
|
|
7118
|
+
return 130;
|
|
7119
|
+
}
|
|
6280
7120
|
}
|
|
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
|
-
})
|
|
7121
|
+
try {
|
|
7122
|
+
await uninstallModuleInstallation(
|
|
7123
|
+
row.id,
|
|
7124
|
+
{ ...options.parkConsumers ? { parkConsumers: true } : {} },
|
|
7125
|
+
auth
|
|
6301
7126
|
);
|
|
6302
|
-
|
|
7127
|
+
} catch (err) {
|
|
7128
|
+
return surfaceUninstallError(err, mod.slug, workspace2.slug);
|
|
6303
7129
|
}
|
|
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})`)}` : ""}`
|
|
7130
|
+
success(
|
|
7131
|
+
`Uninstalled ${code(mod.slug)} (install ${code(`#${row.id}`)}) from workspace ${code(workspace2.slug)}.`
|
|
6312
7132
|
);
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
7133
|
+
info(
|
|
7134
|
+
`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.
|
|
7135
|
+
Reinstalling instead (${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)}) mints a NEW public URL.`
|
|
6316
7136
|
);
|
|
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") {
|
|
7137
|
+
if (options.parkConsumers) {
|
|
6325
7138
|
info(
|
|
6326
|
-
`
|
|
7139
|
+
`Consumer modules that depended on this install were parked (reason PROVIDER_UNINSTALLED) \u2014 they keep running with the dependency unbound until re-bound.`
|
|
6327
7140
|
);
|
|
6328
7141
|
}
|
|
6329
7142
|
return 0;
|
|
6330
7143
|
}
|
|
7144
|
+
function resolveSlug2(slugArg, dir) {
|
|
7145
|
+
if (slugArg) return slugArg;
|
|
7146
|
+
try {
|
|
7147
|
+
const manifest = readManifest(dir);
|
|
7148
|
+
const slug = manifest?.module?.slug;
|
|
7149
|
+
return typeof slug === "string" && slug.length > 0 ? slug : void 0;
|
|
7150
|
+
} catch {
|
|
7151
|
+
return void 0;
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
function surfaceUninstallError(err, slug, workspaceSlug) {
|
|
7155
|
+
if (err instanceof ApiError) {
|
|
7156
|
+
if (err.status === 404 || err.code === "NOT_FOUND" || err.code === "INSTALLATION_NOT_FOUND") {
|
|
7157
|
+
warn(`The install is already gone server-side \u2014 nothing to do.`);
|
|
7158
|
+
return 0;
|
|
7159
|
+
}
|
|
7160
|
+
switch (err.code) {
|
|
7161
|
+
case "PROVIDER_HAS_CONSUMERS":
|
|
7162
|
+
fail(
|
|
7163
|
+
`${code(slug)} provides data to other installed modules \u2014 uninstalling it would break them.
|
|
7164
|
+
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.`
|
|
7165
|
+
);
|
|
7166
|
+
break;
|
|
7167
|
+
case "NOT_WORKSPACE_ADMIN":
|
|
7168
|
+
case "FORBIDDEN":
|
|
7169
|
+
fail(
|
|
7170
|
+
`You must be an ADMIN of workspace ${code(workspaceSlug)} to uninstall modules from it.`
|
|
7171
|
+
);
|
|
7172
|
+
break;
|
|
7173
|
+
case "UNAUTHENTICATED":
|
|
7174
|
+
fail(
|
|
7175
|
+
`Auth rejected by the backend \u2014 your CLI token may be stale. Re-run ${code("dashwise login")} and try again.`
|
|
7176
|
+
);
|
|
7177
|
+
break;
|
|
7178
|
+
default:
|
|
7179
|
+
fail(`Uninstall failed (${err.code} ${err.status}): ${err.message}`);
|
|
7180
|
+
}
|
|
7181
|
+
return 1;
|
|
7182
|
+
}
|
|
7183
|
+
fail(`Uninstall failed: ${err.message}`);
|
|
7184
|
+
return 1;
|
|
7185
|
+
}
|
|
6331
7186
|
|
|
6332
7187
|
// src/commands/module/pull-data.ts
|
|
6333
7188
|
init_api_client();
|
|
@@ -7099,7 +7954,7 @@ function loadContext(opts) {
|
|
|
7099
7954
|
const entry = readModuleEntry(slug);
|
|
7100
7955
|
if (!entry) {
|
|
7101
7956
|
throw new Error(
|
|
7102
|
-
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}
|
|
7957
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}\` to provision one (existing files are preserved).`
|
|
7103
7958
|
);
|
|
7104
7959
|
}
|
|
7105
7960
|
return {
|
|
@@ -7324,7 +8179,7 @@ function loadContext2(opts) {
|
|
|
7324
8179
|
const entry = readModuleEntry(slug);
|
|
7325
8180
|
if (!entry) {
|
|
7326
8181
|
throw new Error(
|
|
7327
|
-
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}
|
|
8182
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}\` to provision one (existing files are preserved).`
|
|
7328
8183
|
);
|
|
7329
8184
|
}
|
|
7330
8185
|
return {
|
|
@@ -7916,7 +8771,7 @@ function loadContext3(opts) {
|
|
|
7916
8771
|
const entry = readModuleEntry(moduleSlug);
|
|
7917
8772
|
if (!entry) {
|
|
7918
8773
|
throw new Error(
|
|
7919
|
-
`No dev install cached for module "${moduleSlug}". Run \`dashwise init ${moduleSlug}
|
|
8774
|
+
`No dev install cached for module "${moduleSlug}". Run \`dashwise init ${moduleSlug}\` to provision one (existing files are preserved).`
|
|
7920
8775
|
);
|
|
7921
8776
|
}
|
|
7922
8777
|
return {
|
|
@@ -8112,17 +8967,17 @@ function surfacePullFailure(err) {
|
|
|
8112
8967
|
switch (err.code) {
|
|
8113
8968
|
case "UNAUTHENTICATED":
|
|
8114
8969
|
fail(
|
|
8115
|
-
`Runtime token rejected. The cached token for this module may be expired \u2014 re-run \`dashwise init <slug
|
|
8970
|
+
`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
8971
|
);
|
|
8117
8972
|
break;
|
|
8118
8973
|
case "SANDBOX_TOKEN_NOT_ACCEPTED":
|
|
8119
8974
|
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
|
|
8975
|
+
`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
8976
|
);
|
|
8122
8977
|
break;
|
|
8123
8978
|
case "INSTALL_MISMATCH":
|
|
8124
8979
|
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
|
|
8980
|
+
`Runtime token is bound to a different installation than expected. The cached install id may be stale \u2014 re-run \`dashwise init <slug>\`.`
|
|
8126
8981
|
);
|
|
8127
8982
|
break;
|
|
8128
8983
|
case "INSTALLATION_NOT_FOUND":
|
|
@@ -8148,6 +9003,32 @@ init_config();
|
|
|
8148
9003
|
init_manifest_io();
|
|
8149
9004
|
init_output();
|
|
8150
9005
|
function loadContext4(opts) {
|
|
9006
|
+
const cfg = readConfig();
|
|
9007
|
+
if (!cfg) {
|
|
9008
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
9009
|
+
}
|
|
9010
|
+
const requestOptions = {
|
|
9011
|
+
baseUrl: resolveApiUrl(),
|
|
9012
|
+
auth: { token: cfg.token }
|
|
9013
|
+
};
|
|
9014
|
+
if (opts.install !== void 0) {
|
|
9015
|
+
if (!Number.isInteger(opts.install) || opts.install <= 0) {
|
|
9016
|
+
throw new Error(
|
|
9017
|
+
`--install expects a positive integer installation id (got "${opts.install}").`
|
|
9018
|
+
);
|
|
9019
|
+
}
|
|
9020
|
+
let slug2;
|
|
9021
|
+
try {
|
|
9022
|
+
slug2 = readManifest(opts.dir)?.module?.slug;
|
|
9023
|
+
} catch {
|
|
9024
|
+
}
|
|
9025
|
+
return {
|
|
9026
|
+
installationId: opts.install,
|
|
9027
|
+
slug: slug2 ?? `install#${opts.install}`,
|
|
9028
|
+
installOverride: true,
|
|
9029
|
+
requestOptions
|
|
9030
|
+
};
|
|
9031
|
+
}
|
|
8151
9032
|
const manifest = readManifestOrThrow(opts.dir);
|
|
8152
9033
|
const slug = manifest.module?.slug;
|
|
8153
9034
|
if (!slug) {
|
|
@@ -8155,25 +9036,36 @@ function loadContext4(opts) {
|
|
|
8155
9036
|
`module.json has no module.slug \u2014 the manifest is malformed.`
|
|
8156
9037
|
);
|
|
8157
9038
|
}
|
|
8158
|
-
const cfg = readConfig();
|
|
8159
|
-
if (!cfg) {
|
|
8160
|
-
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
8161
|
-
}
|
|
8162
9039
|
const entry = readModuleEntry(slug);
|
|
8163
9040
|
if (!entry) {
|
|
8164
9041
|
throw new Error(
|
|
8165
|
-
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}
|
|
9042
|
+
`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
9043
|
);
|
|
8167
9044
|
}
|
|
8168
9045
|
return {
|
|
8169
9046
|
installationId: entry.installationId,
|
|
8170
9047
|
slug,
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
auth: { token: cfg.token }
|
|
8174
|
-
}
|
|
9048
|
+
installOverride: false,
|
|
9049
|
+
requestOptions
|
|
8175
9050
|
};
|
|
8176
9051
|
}
|
|
9052
|
+
async function resolveInstallKind(ctx) {
|
|
9053
|
+
if (!ctx.installOverride) return "local";
|
|
9054
|
+
const entry = readModuleEntry(ctx.slug);
|
|
9055
|
+
if (entry && entry.installationId === ctx.installationId) return "local";
|
|
9056
|
+
if (entry?.workspaceId !== void 0) {
|
|
9057
|
+
try {
|
|
9058
|
+
const rows = await listWorkspaceInstallations(
|
|
9059
|
+
entry.workspaceId,
|
|
9060
|
+
ctx.requestOptions
|
|
9061
|
+
);
|
|
9062
|
+
const row = rows.find((r) => r.id === ctx.installationId);
|
|
9063
|
+
if (row) return row.kind ?? "production";
|
|
9064
|
+
} catch {
|
|
9065
|
+
}
|
|
9066
|
+
}
|
|
9067
|
+
return void 0;
|
|
9068
|
+
}
|
|
8177
9069
|
async function apiKeysListCommand(options) {
|
|
8178
9070
|
let ctx;
|
|
8179
9071
|
try {
|
|
@@ -8201,6 +9093,7 @@ async function apiKeysListCommand(options) {
|
|
|
8201
9093
|
);
|
|
8202
9094
|
return 0;
|
|
8203
9095
|
}
|
|
9096
|
+
const installKind = await resolveInstallKind(ctx);
|
|
8204
9097
|
info(`API keys for ${code(ctx.slug)} (install id=${ctx.installationId}):`);
|
|
8205
9098
|
for (const k of filtered) {
|
|
8206
9099
|
const status = formatKeyStatus(k);
|
|
@@ -8216,7 +9109,8 @@ async function apiKeysListCommand(options) {
|
|
|
8216
9109
|
`);
|
|
8217
9110
|
}
|
|
8218
9111
|
} else if (k.grants !== void 0) {
|
|
8219
|
-
|
|
9112
|
+
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)";
|
|
9113
|
+
process.stdout.write(` ${dim(label)}
|
|
8220
9114
|
`);
|
|
8221
9115
|
}
|
|
8222
9116
|
}
|
|
@@ -8228,6 +9122,12 @@ async function apiKeysCreateCommand(nameArg, options) {
|
|
|
8228
9122
|
fail("Usage: `dashwise api-keys create <name>` (e.g. `dashwise api-keys create production`)");
|
|
8229
9123
|
return 1;
|
|
8230
9124
|
}
|
|
9125
|
+
if (options.fullAccess && options.grant && options.grant.length > 0) {
|
|
9126
|
+
fail(
|
|
9127
|
+
`${code("--full-access")} and ${code("--grant")} are mutually exclusive \u2014 pass one or the other.`
|
|
9128
|
+
);
|
|
9129
|
+
return 1;
|
|
9130
|
+
}
|
|
8231
9131
|
let grants;
|
|
8232
9132
|
try {
|
|
8233
9133
|
grants = parseGrants(options.grant);
|
|
@@ -8249,8 +9149,18 @@ async function apiKeysCreateCommand(nameArg, options) {
|
|
|
8249
9149
|
fail(err.message);
|
|
8250
9150
|
return 1;
|
|
8251
9151
|
}
|
|
9152
|
+
const installKind = await resolveInstallKind(ctx);
|
|
9153
|
+
const grantless = grants === void 0 && options.fullAccess !== true;
|
|
9154
|
+
if (grantless && installKind !== "local") {
|
|
9155
|
+
warn(
|
|
9156
|
+
`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.
|
|
9157
|
+
Pass ${code("--full-access")} for full-CRUD over the module's own tables, or ${code("--grant <spec>")} for least-privilege scopes.
|
|
9158
|
+
(Grantless = full own-table access is a ${code("kind=local")} dev-install convenience only.)`
|
|
9159
|
+
);
|
|
9160
|
+
}
|
|
8252
9161
|
const body = { name };
|
|
8253
9162
|
if (grants !== void 0) body.grants = grants;
|
|
9163
|
+
if (options.fullAccess === true) body.full_own_tables = true;
|
|
8254
9164
|
if (expiresAt !== void 0) body.expires_at = expiresAt;
|
|
8255
9165
|
let resp;
|
|
8256
9166
|
try {
|
|
@@ -8274,8 +9184,12 @@ async function apiKeysCreateCommand(nameArg, options) {
|
|
|
8274
9184
|
if (grantLines.length > 0) {
|
|
8275
9185
|
info(` ${dim("scope:")} scoped to`);
|
|
8276
9186
|
for (const line of grantLines) info(` ${line}`);
|
|
9187
|
+
} else if (installKind === "local") {
|
|
9188
|
+
info(` ${dim("scope:")} full own-table access (grantless key on the local dev install)`);
|
|
8277
9189
|
} else {
|
|
8278
|
-
info(
|
|
9190
|
+
info(
|
|
9191
|
+
` ${dim("scope:")} NO access \u2014 grantless key on a non-local install (re-mint with ${code("--full-access")} or ${code("--grant")})`
|
|
9192
|
+
);
|
|
8279
9193
|
}
|
|
8280
9194
|
info("");
|
|
8281
9195
|
warn(
|
|
@@ -8347,6 +9261,11 @@ function surfaceApiKeysError(err) {
|
|
|
8347
9261
|
`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
9262
|
);
|
|
8349
9263
|
return 1;
|
|
9264
|
+
case "GRANTS_CONFLICT":
|
|
9265
|
+
fail(
|
|
9266
|
+
`--full-access and --grant are mutually exclusive \u2014 pass one or the other.`
|
|
9267
|
+
);
|
|
9268
|
+
return 1;
|
|
8350
9269
|
case "INSTALLATION_NOT_FOUND":
|
|
8351
9270
|
fail(
|
|
8352
9271
|
`Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
|
|
@@ -8501,6 +9420,31 @@ init_config();
|
|
|
8501
9420
|
init_manifest_io();
|
|
8502
9421
|
init_output();
|
|
8503
9422
|
function loadContext5(opts) {
|
|
9423
|
+
const cfg = readConfig();
|
|
9424
|
+
if (!cfg) {
|
|
9425
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
9426
|
+
}
|
|
9427
|
+
const requestOptions = {
|
|
9428
|
+
baseUrl: resolveApiUrl(),
|
|
9429
|
+
auth: { token: cfg.token }
|
|
9430
|
+
};
|
|
9431
|
+
if (opts.install !== void 0) {
|
|
9432
|
+
if (!Number.isInteger(opts.install) || opts.install <= 0) {
|
|
9433
|
+
throw new Error(
|
|
9434
|
+
`--install expects a positive integer installation id (got "${opts.install}").`
|
|
9435
|
+
);
|
|
9436
|
+
}
|
|
9437
|
+
let slug2;
|
|
9438
|
+
try {
|
|
9439
|
+
slug2 = readManifest(opts.dir)?.module?.slug;
|
|
9440
|
+
} catch {
|
|
9441
|
+
}
|
|
9442
|
+
return {
|
|
9443
|
+
installationId: opts.install,
|
|
9444
|
+
slug: slug2 ?? `install#${opts.install}`,
|
|
9445
|
+
requestOptions
|
|
9446
|
+
};
|
|
9447
|
+
}
|
|
8504
9448
|
const manifest = readManifestOrThrow(opts.dir);
|
|
8505
9449
|
const slug = manifest.module?.slug;
|
|
8506
9450
|
if (!slug) {
|
|
@@ -8508,23 +9452,16 @@ function loadContext5(opts) {
|
|
|
8508
9452
|
`module.json has no module.slug \u2014 the manifest is malformed.`
|
|
8509
9453
|
);
|
|
8510
9454
|
}
|
|
8511
|
-
const cfg = readConfig();
|
|
8512
|
-
if (!cfg) {
|
|
8513
|
-
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
8514
|
-
}
|
|
8515
9455
|
const entry = readModuleEntry(slug);
|
|
8516
9456
|
if (!entry) {
|
|
8517
9457
|
throw new Error(
|
|
8518
|
-
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}
|
|
9458
|
+
`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
9459
|
);
|
|
8520
9460
|
}
|
|
8521
9461
|
return {
|
|
8522
9462
|
installationId: entry.installationId,
|
|
8523
9463
|
slug,
|
|
8524
|
-
requestOptions
|
|
8525
|
-
baseUrl: resolveApiUrl(),
|
|
8526
|
-
auth: { token: cfg.token }
|
|
8527
|
-
}
|
|
9464
|
+
requestOptions
|
|
8528
9465
|
};
|
|
8529
9466
|
}
|
|
8530
9467
|
function normalizeOrigin(raw) {
|
|
@@ -8719,7 +9656,7 @@ async function envExportCommand(options) {
|
|
|
8719
9656
|
const entry = readModuleEntry(slug);
|
|
8720
9657
|
if (!entry) {
|
|
8721
9658
|
fail(
|
|
8722
|
-
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}
|
|
9659
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug}\` to provision one (existing files are preserved).`
|
|
8723
9660
|
);
|
|
8724
9661
|
return 1;
|
|
8725
9662
|
}
|
|
@@ -8826,12 +9763,16 @@ function shellQuote(value) {
|
|
|
8826
9763
|
}
|
|
8827
9764
|
|
|
8828
9765
|
// src/commands/query.ts
|
|
9766
|
+
init_config();
|
|
9767
|
+
init_dotenv_local();
|
|
8829
9768
|
init_manifest_io();
|
|
8830
9769
|
init_output();
|
|
9770
|
+
init_dev();
|
|
8831
9771
|
async function queryCommand(options) {
|
|
8832
9772
|
const projectRoot = resolve(options.dir ?? process.cwd());
|
|
9773
|
+
let manifestSlug;
|
|
8833
9774
|
try {
|
|
8834
|
-
readManifestOrThrow(projectRoot);
|
|
9775
|
+
manifestSlug = readManifestOrThrow(projectRoot).module?.slug;
|
|
8835
9776
|
} catch (err) {
|
|
8836
9777
|
fail(err.message);
|
|
8837
9778
|
return 1;
|
|
@@ -8849,6 +9790,32 @@ async function queryCommand(options) {
|
|
|
8849
9790
|
return 1;
|
|
8850
9791
|
}
|
|
8851
9792
|
hydrateEnvFromDotenvLocal(projectRoot);
|
|
9793
|
+
if (!process.env.DASHWISE_API_KEY && manifestSlug) {
|
|
9794
|
+
const cfg = (() => {
|
|
9795
|
+
try {
|
|
9796
|
+
return readConfig();
|
|
9797
|
+
} catch {
|
|
9798
|
+
return null;
|
|
9799
|
+
}
|
|
9800
|
+
})();
|
|
9801
|
+
const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
|
|
9802
|
+
let entry = readModuleEntry(manifestSlug);
|
|
9803
|
+
if (cfg) {
|
|
9804
|
+
const healed = await resolveModuleApiKey(
|
|
9805
|
+
manifestSlug,
|
|
9806
|
+
entry,
|
|
9807
|
+
apiUrl,
|
|
9808
|
+
cfg.token
|
|
9809
|
+
);
|
|
9810
|
+
if (healed) entry = healed;
|
|
9811
|
+
}
|
|
9812
|
+
if (entry?.apiKey) {
|
|
9813
|
+
process.env.DASHWISE_API_KEY = entry.apiKey;
|
|
9814
|
+
if (!process.env.DASHWISE_API_URL) {
|
|
9815
|
+
process.env.DASHWISE_API_URL = apiUrl;
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
9818
|
+
}
|
|
8852
9819
|
const modes = [
|
|
8853
9820
|
options.inline !== void 0 ? "inline" : null,
|
|
8854
9821
|
options.file !== void 0 ? "file" : null,
|
|
@@ -9025,25 +9992,6 @@ function generatedEntryUrl(generatedDir) {
|
|
|
9025
9992
|
async function dynamicImportFromPath(generatedDir) {
|
|
9026
9993
|
return await import(generatedEntryUrl(generatedDir));
|
|
9027
9994
|
}
|
|
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
9995
|
|
|
9048
9996
|
// src/commands/module/query-log.ts
|
|
9049
9997
|
init_api_client();
|
|
@@ -9487,7 +10435,7 @@ var initOptions = (cmd) => cmd.option("--dir <path>", "Override the target direc
|
|
|
9487
10435
|
"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
10436
|
).option(
|
|
9489
10437
|
"--no-provision",
|
|
9490
|
-
"Skip the `POST /api/installations/dev` step. Local scaffold is still written;
|
|
10438
|
+
"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
10439
|
).addOption(
|
|
9492
10440
|
new Option(
|
|
9493
10441
|
"--deploy-target <platform>",
|
|
@@ -9754,6 +10702,14 @@ program.command("query").description(
|
|
|
9754
10702
|
});
|
|
9755
10703
|
}
|
|
9756
10704
|
);
|
|
10705
|
+
program.command("run [cmd...]").description(
|
|
10706
|
+
"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`."
|
|
10707
|
+
).option("--dir <path>", "Project root containing `module.json` (default: current directory).").action(async (cmdArgs, cmdOpts) => {
|
|
10708
|
+
const { runCommand: runCommand2 } = await Promise.resolve().then(() => (init_run(), run_exports));
|
|
10709
|
+
process.exitCode = await runCommand2(cmdArgs ?? [], {
|
|
10710
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
10711
|
+
});
|
|
10712
|
+
});
|
|
9757
10713
|
program.command("pull").description(
|
|
9758
10714
|
"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
10715
|
).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 +10723,32 @@ var apiKeysCmd = program.command("api-keys").description(
|
|
|
9767
10723
|
);
|
|
9768
10724
|
apiKeysCmd.command("list").description(
|
|
9769
10725
|
"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(
|
|
10726
|
+
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10727
|
+
"--install <id>",
|
|
10728
|
+
"Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
|
|
10729
|
+
parseIntOption
|
|
10730
|
+
).option("--all", "Include revoked + expired keys in the output.").option("--json", "Emit JSON instead of the human-friendly table.").action(async (cmdOpts) => {
|
|
9771
10731
|
process.exitCode = await apiKeysListCommand({
|
|
9772
10732
|
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10733
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
|
|
9773
10734
|
...cmdOpts.all !== void 0 ? { all: cmdOpts.all } : {},
|
|
9774
10735
|
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
9775
10736
|
});
|
|
9776
10737
|
});
|
|
9777
10738
|
apiKeysCmd.command("create [name]").description(
|
|
9778
10739
|
"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(
|
|
10740
|
+
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10741
|
+
"--install <id>",
|
|
10742
|
+
"Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
|
|
10743
|
+
parseIntOption
|
|
10744
|
+
).option("--name <name>", "Override the positional name arg (e.g. for scripted use).").option(
|
|
9780
10745
|
"--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).
|
|
10746
|
+
"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
10747
|
collectRepeated,
|
|
9783
10748
|
[]
|
|
10749
|
+
).option(
|
|
10750
|
+
"--full-access",
|
|
10751
|
+
"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
10752
|
).option(
|
|
9785
10753
|
"--expires <days>",
|
|
9786
10754
|
"Days until the key expires (converted to an absolute timestamp). Default: never expires.",
|
|
@@ -9792,8 +10760,10 @@ apiKeysCmd.command("create [name]").description(
|
|
|
9792
10760
|
async (nameArg, cmdOpts) => {
|
|
9793
10761
|
process.exitCode = await apiKeysCreateCommand(nameArg, {
|
|
9794
10762
|
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10763
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
|
|
9795
10764
|
...cmdOpts.name !== void 0 ? { name: cmdOpts.name } : {},
|
|
9796
10765
|
...cmdOpts.grant !== void 0 && cmdOpts.grant.length > 0 ? { grant: cmdOpts.grant } : {},
|
|
10766
|
+
...cmdOpts.fullAccess !== void 0 ? { fullAccess: cmdOpts.fullAccess } : {},
|
|
9797
10767
|
...cmdOpts.expires !== void 0 ? { expires: cmdOpts.expires } : {},
|
|
9798
10768
|
...cmdOpts.rawOnly !== void 0 ? { rawOnly: cmdOpts.rawOnly } : {}
|
|
9799
10769
|
});
|
|
@@ -9801,10 +10771,15 @@ apiKeysCmd.command("create [name]").description(
|
|
|
9801
10771
|
);
|
|
9802
10772
|
apiKeysCmd.command("revoke [key-id]").description(
|
|
9803
10773
|
"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(
|
|
10774
|
+
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10775
|
+
"--install <id>",
|
|
10776
|
+
"Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
|
|
10777
|
+
parseIntOption
|
|
10778
|
+
).option("-y, --yes", "Skip the per-key confirmation message (for CI / scripts).").action(
|
|
9805
10779
|
async (keyIdArg, cmdOpts) => {
|
|
9806
10780
|
process.exitCode = await apiKeysRevokeCommand(keyIdArg, {
|
|
9807
10781
|
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10782
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
|
|
9808
10783
|
...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
|
|
9809
10784
|
});
|
|
9810
10785
|
}
|
|
@@ -9812,22 +10787,37 @@ apiKeysCmd.command("revoke [key-id]").description(
|
|
|
9812
10787
|
var originsCmd = program.command("origins").description(
|
|
9813
10788
|
"Manage the current module install's SSO callback-origin allowlist. Local dev (localhost) is always allowed; add deployed domains here."
|
|
9814
10789
|
);
|
|
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(
|
|
10790
|
+
originsCmd.command("list").description("Show the SSO callback origins configured for the current module install.").option("--dir <path>", "Project root (default: current directory).").option(
|
|
10791
|
+
"--install <id>",
|
|
10792
|
+
"Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
|
|
10793
|
+
parseIntOption
|
|
10794
|
+
).option("--json", "Emit a JSON array instead of the human-friendly list.").action(async (cmdOpts) => {
|
|
9816
10795
|
process.exitCode = await originsListCommand({
|
|
9817
10796
|
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10797
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
|
|
9818
10798
|
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
9819
10799
|
});
|
|
9820
10800
|
});
|
|
9821
10801
|
originsCmd.command("add <url>").description(
|
|
9822
10802
|
"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).").
|
|
10803
|
+
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10804
|
+
"--install <id>",
|
|
10805
|
+
"Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
|
|
10806
|
+
parseIntOption
|
|
10807
|
+
).action(async (url, cmdOpts) => {
|
|
9824
10808
|
process.exitCode = await originsAddCommand(url, {
|
|
9825
|
-
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
10809
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10810
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {}
|
|
9826
10811
|
});
|
|
9827
10812
|
});
|
|
9828
|
-
originsCmd.command("remove <url>").description("Remove an SSO callback origin from the allowlist.").option("--dir <path>", "Project root (default: current directory).").
|
|
10813
|
+
originsCmd.command("remove <url>").description("Remove an SSO callback origin from the allowlist.").option("--dir <path>", "Project root (default: current directory).").option(
|
|
10814
|
+
"--install <id>",
|
|
10815
|
+
"Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
|
|
10816
|
+
parseIntOption
|
|
10817
|
+
).action(async (url, cmdOpts) => {
|
|
9829
10818
|
process.exitCode = await originsRemoveCommand(url, {
|
|
9830
|
-
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
|
|
10819
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10820
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {}
|
|
9831
10821
|
});
|
|
9832
10822
|
});
|
|
9833
10823
|
var envCmd = program.command("env").description("Environment-variable helpers for deploying a module.");
|
|
@@ -9881,7 +10871,7 @@ moduleCmd.command("install [slug]").alias("deploy").description(
|
|
|
9881
10871
|
"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
10872
|
).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
10873
|
"--replace-dev-install",
|
|
9884
|
-
"Destroy the workspace's kind=local dev install first, then fresh production install.
|
|
10874
|
+
"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
10875
|
).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
10876
|
async (slug, cmdOpts) => {
|
|
9887
10877
|
process.exitCode = await moduleInstallCommand(slug, {
|
|
@@ -9895,6 +10885,21 @@ moduleCmd.command("install [slug]").alias("deploy").description(
|
|
|
9895
10885
|
});
|
|
9896
10886
|
}
|
|
9897
10887
|
);
|
|
10888
|
+
moduleCmd.command("uninstall [slug]").description(
|
|
10889
|
+
"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."
|
|
10890
|
+
).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option(
|
|
10891
|
+
"--park-consumers",
|
|
10892
|
+
"Uninstall even when live consumer modules depend on this install \u2014 their dependencies are parked (PROVIDER_UNINSTALLED) instead of blocking with PROVIDER_HAS_CONSUMERS."
|
|
10893
|
+
).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(
|
|
10894
|
+
async (slug, cmdOpts) => {
|
|
10895
|
+
process.exitCode = await moduleUninstallCommand(slug, {
|
|
10896
|
+
...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
|
|
10897
|
+
...cmdOpts.parkConsumers !== void 0 ? { parkConsumers: cmdOpts.parkConsumers } : {},
|
|
10898
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
10899
|
+
...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
|
|
10900
|
+
});
|
|
10901
|
+
}
|
|
10902
|
+
);
|
|
9898
10903
|
moduleCmd.command("status [slug]").description(
|
|
9899
10904
|
"Show a module's deploy state in a workspace: install kind, installed vs latest published version, runtime provisioning, and the public URL."
|
|
9900
10905
|
).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 +10983,7 @@ moduleCmd.command("destroy-dev-install").description(
|
|
|
9978
10983
|
}
|
|
9979
10984
|
);
|
|
9980
10985
|
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.
|
|
10986
|
+
"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
10987
|
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
9983
10988
|
"--module <slug>",
|
|
9984
10989
|
"Override the module slug (default: from local module.json)."
|
|
@@ -9996,6 +11001,18 @@ moduleCmd.command("reset-dev-install").description(
|
|
|
9996
11001
|
});
|
|
9997
11002
|
}
|
|
9998
11003
|
);
|
|
11004
|
+
var schemaCmd = program.command("schema").description(
|
|
11005
|
+
"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)."
|
|
11006
|
+
);
|
|
11007
|
+
schemaCmd.command("push").description(
|
|
11008
|
+
"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)."
|
|
11009
|
+
).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) => {
|
|
11010
|
+
const { schemaPushCommand: schemaPushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
|
|
11011
|
+
process.exitCode = await schemaPushCommand2({
|
|
11012
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
11013
|
+
...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
|
|
11014
|
+
});
|
|
11015
|
+
});
|
|
9999
11016
|
var tableCmd = program.command("table").description(
|
|
10000
11017
|
"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
11018
|
);
|
|
@@ -10033,7 +11050,7 @@ var fieldCmd = program.command("field").description(
|
|
|
10033
11050
|
"Schema commands for fields within the current module \u2014 add/remove/change-type. Field rename is deferred to v1.1 per the plan."
|
|
10034
11051
|
);
|
|
10035
11052
|
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.
|
|
11053
|
+
'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
11054
|
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
10038
11055
|
"--field-json <json>",
|
|
10039
11056
|
"Field as a JSON object. Mutually exclusive with the positional field spec."
|
|
@@ -10083,7 +11100,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
|
|
|
10083
11100
|
}
|
|
10084
11101
|
})();
|
|
10085
11102
|
function getVersion() {
|
|
10086
|
-
return "0.
|
|
11103
|
+
return "0.15.0";
|
|
10087
11104
|
}
|
|
10088
11105
|
function parseIntOption(value) {
|
|
10089
11106
|
const n = Number.parseInt(value, 10);
|