@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.
Files changed (3) hide show
  1. package/dist/bin.js +2315 -1298
  2. package/dist/bin.js.map +1 -1
  3. 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/scaffold.ts
549
- function isDeployTarget(value) {
550
- return DEPLOY_TARGETS.includes(value);
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 manifestJson(opts) {
553
- const kind = opts.kind ?? "hand_authored";
554
- const manifest = {
555
- schema_version: 2,
556
- module: {
557
- slug: opts.slug,
558
- name: opts.name,
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
- if (opts.repositoryUrl) {
587
- manifest.repository = { type: "git", url: opts.repositoryUrl };
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 JSON.stringify(manifest, null, 2) + "\n";
706
+ return { tablesRemoved, fieldsRemoved };
590
707
  }
591
- function packageJson(opts) {
592
- return JSON.stringify(
593
- {
594
- name: opts.slug,
595
- version: "0.0.1",
596
- private: true,
597
- type: "module",
598
- scripts: {
599
- dev: "dashwise module dev",
600
- "dev:next": "next dev",
601
- "dashwise:generate": "dashwise module generate",
602
- build: "next build",
603
- start: "next start"
604
- },
605
- dependencies: {
606
- "@dashai/sdk": "^2.0.0",
607
- next: "^15.0.0",
608
- react: "^19.0.0",
609
- "react-dom": "^19.0.0"
610
- },
611
- devDependencies: {
612
- "@types/node": "^22.0.0",
613
- "@types/react": "^19.0.0",
614
- "@types/react-dom": "^19.0.0",
615
- typescript: "^5.5.0"
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
- null,
619
- 2
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
- function nextEnvDts() {
677
- return `/// <reference types="next" />
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 the chart tokens into Tailwind's color namespace so \`bg-chart-1\`,
1693
- \`text-chart-2\`, \`border-l-chart-3\`, \u2026 resolve. (\`@import "shadcn/tailwind.css"\`
1694
- already maps the core tokens like \`--primary\`; this covers the ramp.) */
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
- if (existsSync(dir)) {
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
- for (const [relPath, content] of files) {
2513
- const absPath = join(dir, relPath);
2514
- const parentDir = join(absPath, "..");
2515
- if (!existsSync(parentDir)) {
2516
- mkdirSync(parentDir, { recursive: true });
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
- success(
2521
- `Scaffolded ${code(slug)} (${kind === "custom" ? "custom Next.js" : "simple module"}) in ${code(dir)}`
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} --force --workspace <your-workspace-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} --force`)} to retry.`
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} --force`)}.`
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
- warn(
2691
- `A module with slug ${code(slug)} already exists. Pick a different slug, or destroy the existing dev install (LDI-9 coming soon) and re-run.`
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} --force\` to provision one.`
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} --force\` to re-provision.`
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
- // src/commands/module/destroy-dev-install.ts
2772
- var destroy_dev_install_exports = {};
2773
- __export(destroy_dev_install_exports, {
2774
- destroyDevInstallCommand: () => destroyDevInstallCommand
2775
- });
2776
- async function destroyDevInstallCommand(options) {
2777
- let slug = options.module;
2778
- if (!slug) {
2779
- const projectRoot = resolve(options.dir ?? process.cwd());
2780
- const manifest = (() => {
2781
- try {
2782
- return readManifest(projectRoot);
2783
- } catch {
2784
- return null;
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
- function quote(s) {
3015
- return `\`${s}\``;
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/table/create.ts
3050
- var create_exports = {};
3051
- __export(create_exports, {
3052
- surfaceFailure: () => surfaceFailure,
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 tableCreateCommand(slug, options) {
3057
- if (!slug) {
3058
- fail("Usage: `dashwise table create <slug> --field <slug>:<type>[:required] [...]`");
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 fields = [];
3648
+ const projectRoot = resolve(options.dir ?? process.cwd());
3649
+ let manifestSlug;
3062
3650
  try {
3063
- for (const spec of options.field ?? []) {
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
- if (err instanceof FieldSpecError) {
3071
- fail(err.message);
3072
- return 1;
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
- throw err;
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 (fields.length === 0) {
3683
+ if (!apiKey) {
3077
3684
  fail(
3078
- `At least one field is required. Use ${code("--field <slug>:<type>")} (repeatable) or ${code("--field-json '{...}'")} for fields with options/target/default.`
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 seen = /* @__PURE__ */ new Set();
3083
- for (const f of fields) {
3084
- if (seen.has(f.slug)) {
3085
- fail(`Duplicate field slug ${code(f.slug)} in --field flags.`);
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
- let ctx;
3091
- try {
3092
- ctx = loadSchemaCommandContext({
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
- info(`Creating table ${code(slug)} in installation ${ctx.entry.installationId}\u2026`);
3100
- try {
3101
- const result = await createTable(
3102
- { slug, fields },
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
- } catch (err) {
3109
- return surfaceFailure(err);
3761
+ return 0;
3110
3762
  }
3111
- return syncLocalManifest(ctx);
3112
- }
3113
- async function syncLocalManifest(ctx) {
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 { manifest } = await getInstallationSchema(ctx.requestOptions);
3116
- writeManifest(manifest, ctx.projectRoot);
3117
- return 0;
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
- `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.`
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
- `Schema sync to local module.json failed: ${err.message}. The mutation succeeded server-side.`
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 surfaceFailure(err) {
3804
+ function surfaceDestroyError(err) {
3132
3805
  if (err instanceof ApiError) {
3133
3806
  switch (err.code) {
3134
- case "TABLE_SLUG_EXISTS":
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
- `API key rejected by backend. The cached key may be expired or revoked \u2014 re-run \`dashwise init <slug> --force\` to mint a fresh one.`
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 "SCHEMA_LOCKED":
3812
+ case "NOT_WORKSPACE_MEMBER":
3152
3813
  fail(
3153
- `Schema mutations require a local-dev install (kind=local). The cached API key belongs to a non-local install \u2014 schema is locked. Run \`dashwise init <slug> --force\` for a local dev install.`
3814
+ `You're not a member of the install's workspace. Contact a workspace admin.`
3154
3815
  );
3155
3816
  break;
3156
- case "INSTALL_MISMATCH":
3817
+ case "UNAUTHENTICATED":
3157
3818
  fail(
3158
- `API key is bound to a different installation than the URL. The cached install id may be stale \u2014 re-run \`dashwise init <slug> --force\`.`
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
- return syncLocalManifest(ctx);
3827
+ fail(`Unexpected error: ${err.message}`);
3828
+ return 1;
3208
3829
  }
3209
- var init_delete = __esm({
3210
- "src/commands/table/delete.ts"() {
3830
+ var init_destroy_dev_install = __esm({
3831
+ "src/commands/module/destroy-dev-install.ts"() {
3211
3832
  init_api_client();
3212
- init_schema_command_context();
3833
+ init_config();
3834
+ init_manifest_io();
3213
3835
  init_output();
3214
- init_create();
3215
3836
  }
3216
3837
  });
3217
3838
 
3218
- // src/commands/field/add.ts
3219
- var add_exports = {};
3220
- __export(add_exports, {
3221
- fieldAddCommand: () => fieldAddCommand
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 fieldAddCommand(tableSlug, fieldSpec, options) {
3224
- if (!tableSlug) {
3225
- fail(
3226
- "Usage: `dashwise field add <table-slug> <field-slug>:<type>[:required]` or `dashwise field add <table-slug> --field-json '{...}'`"
3227
- );
3228
- return 1;
3229
- }
3230
- const hasPositional = fieldSpec !== void 0;
3231
- const hasJson = options.fieldJson !== void 0;
3232
- if (!hasPositional && !hasJson) {
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
- `Pass either a field spec (e.g. ${code("title:text:required")}) or ${code("--field-json")} with the field's JSON definition.`
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
- if (hasPositional && hasJson) {
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
- `Pass only ONE of the field spec or ${code("--field-json")} \u2014 they're mutually exclusive.`
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
- } catch (err) {
3275
- return surfaceFailure(err);
3871
+ return destroyExit;
3276
3872
  }
3277
- return syncLocalManifest(ctx);
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 init_add = __esm({
3280
- "src/commands/field/add.ts"() {
3281
- init_api_client();
3282
- init_schema_command_context();
3283
- init_field_spec();
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/field/remove.ts
3290
- var remove_exports = {};
3291
- __export(remove_exports, {
3292
- fieldRemoveCommand: () => fieldRemoveCommand
3896
+ // src/commands/schema/push.ts
3897
+ var push_exports = {};
3898
+ __export(push_exports, {
3899
+ schemaPushCommand: () => schemaPushCommand
3293
3900
  });
3294
- async function fieldRemoveCommand(tableSlug, fieldSlug, options) {
3295
- if (!tableSlug || !fieldSlug) {
3296
- fail("Usage: `dashwise field remove <table-slug> <field-slug>`");
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
- ctx = loadSchemaCommandContext({
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
- info(
3309
- `Removing field ${code(`${tableSlug}.${fieldSlug}`)} from installation ${ctx.entry.installationId}\u2026`
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
- if (!FIELD_TYPES.includes(newType)) {
3347
- fail(
3348
- `Type ${code(newType)} is not a known type. Allowed: ${FIELD_TYPES.join(", ")}.`
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
- let ctx;
3920
+ const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
3921
+ let moduleId;
3353
3922
  try {
3354
- ctx = loadSchemaCommandContext({
3355
- ...options.dir !== void 0 ? { dir: options.dir } : {}
3356
- });
3923
+ moduleId = (await getModuleBySlug(slug, auth)).id;
3357
3924
  } catch (err) {
3358
- fail(err.message);
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
- info(
3362
- `Setting type of ${code(`${tableSlug}.${fieldSlug}`)} \u2192 ${code(newType)} on installation ${ctx.entry.installationId}\u2026`
3363
- );
3364
- try {
3365
- const result = await updateField(
3366
- tableSlug,
3367
- fieldSlug,
3368
- { type: newType },
3369
- ctx.requestOptions
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
- success(
3372
- `Updated ${code(`${tableSlug}.${fieldSlug}`)} \u2192 ${code(newType)} (manifest_version=${result.manifest_version})`
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 surfaceFailure(err);
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
- return syncLocalManifest(ctx);
4124
+ fail(`Schema push failed: ${err.message}`);
4125
+ return 1;
3378
4126
  }
3379
- var init_set_type = __esm({
3380
- "src/commands/field/set-type.ts"() {
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
- init_schema_command_context();
3383
- init_field_spec();
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/commands/dev.ts
3390
- init_api_client();
3391
-
3392
- // src/lib/connect-status.ts
3393
- init_api_client();
3394
- init_output();
3395
- async function reportConnectStatus(opts) {
3396
- const [deps, bindings] = await Promise.all([
3397
- safeGet(
3398
- () => getInstallationDependencies(opts.installationId, opts.requestOptions)
3399
- ),
3400
- safeGet(
3401
- () => getInstallationBindings(opts.installationId, opts.requestOptions)
3402
- )
3403
- ]);
3404
- if (deps === null && bindings === null) return;
3405
- const depList = deps ?? [];
3406
- const bindList = bindings ?? [];
3407
- if (depList.length === 0 && bindList.length === 0) return;
3408
- const unboundDeps = depList.filter((d) => d.status === "unbound").length;
3409
- const unboundUses = bindList.filter((b) => b.status === "unbound").length;
3410
- info(
3411
- `Connect status for ${code(opts.moduleSlug)} ${dim(
3412
- `(${depList.length} dep${depList.length === 1 ? "" : "s"}, ${bindList.length} uses slot${bindList.length === 1 ? "" : "s"})`
3413
- )}:`
3414
- );
3415
- for (const d of depList) renderDep(d);
3416
- for (const b of bindList) renderBinding(b);
3417
- const hints = [];
3418
- if (unboundDeps > 0) {
3419
- hints.push(
3420
- `${code("dashwise deps bind <provider-slug>")} once the provider is installed`
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
- if (unboundUses > 0) {
3424
- hints.push(`${code("dashwise bind <uses-slug> --table <name>")} to connect a table`);
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 (hints.length > 0) {
3427
- info(`Connect parked items with ${hints.join("; ")}.`);
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
- out("");
3430
- }
3431
- function renderDep(d) {
3432
- if (d.status === "resolved") {
3433
- const providerLine = d.provider ? ` ${dim(`\u2192 install #${d.provider.installation_id} @ v${d.provider.version}`)}` : "";
3434
- out(` ${pc.green("\u25CF resolved")} ${dim("dep")} ${code(d.slug)}@${d.range ?? "*"}${providerLine}`);
3435
- } else {
3436
- out(
3437
- ` ${pc.yellow("\u25CB unbound")} ${dim("dep")} ${code(d.slug)}@${d.range ?? "*"} ${dim(`reason: ${d.reason ?? "(unspecified)"}`)}`
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
- function renderBinding(b) {
3442
- if (b.status === "bound") {
3443
- const tableLine = b.table ? ` ${dim(`\u2192 ${b.table.name} (id ${b.table.id})`)}` : "";
3444
- out(` ${pc.green("\u25CF bound")} ${dim("uses")} ${code(b.uses_slug)}${tableLine}`);
3445
- } else if (b.status === "declined") {
3446
- out(` ${pc.dim("\u2298 declined")} ${dim("uses")} ${code(b.uses_slug)}`);
3447
- } else {
3448
- out(
3449
- ` ${pc.yellow("\u25CB unbound")} ${dim("uses")} ${code(b.uses_slug)} ${dim(`reason: ${b.reason ?? "(unspecified)"}`)}`
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
- async function safeGet(fn) {
4205
+ function parseFieldJson(json) {
4206
+ let parsed;
3454
4207
  try {
3455
- return await fn();
4208
+ parsed = JSON.parse(json);
3456
4209
  } catch (err) {
3457
- return null;
4210
+ throw new FieldSpecError(
4211
+ `--field-json value is not valid JSON: ${err.message}`
4212
+ );
3458
4213
  }
3459
- }
3460
-
3461
- // src/commands/dev.ts
3462
- init_config();
3463
- init_manifest_io();
3464
-
3465
- // src/lib/manifest-poll.ts
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
- const projectRoot = resolve(options.dir ?? process.cwd());
3538
- const manifestPath2 = `${projectRoot}/module.json`;
3539
- if (!existsSync(manifestPath2)) {
3540
- fail(`No ${code("module.json")} found in ${code(projectRoot)}.`);
3541
- info(`Run ${code("dashwise module init <slug>")} to scaffold a new module first.`);
3542
- return 1;
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
- const manifestSlug = (() => {
3545
- try {
3546
- return readManifest(projectRoot)?.module?.slug;
3547
- } catch {
3548
- return void 0;
3549
- }
3550
- })();
3551
- const savedConfig = (() => {
3552
- try {
3553
- return readConfig();
3554
- } catch {
3555
- return null;
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 apiUrl = options.apiUrl ?? process.env.DASHWISE_API_URL ?? savedConfig?.apiUrl ?? "http://localhost:3000";
3567
- let port;
4277
+ const fields = [];
3568
4278
  try {
3569
- port = await resolveDevPort(options.port);
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
- fail(err.message);
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
- const callbackOrigins = computeCallbackOrigins(port, options.callbackOrigin ?? []);
3598
- try {
3599
- const session = await registerDevSession(apiUrl, apiToken, {
3600
- target_workspace_id: workspaceContext.id,
3601
- callback_origins: callbackOrigins
3602
- });
3603
- revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session.id);
3604
- info(` ${dim("dev session")} ${session.id}`);
3605
- info(` ${dim("workspace")} ${workspaceContext.name} (id=${workspaceContext.id}, slug=${workspaceContext.slug})`);
3606
- info(` ${dim("callbacks")} ${callbackOrigins.join(", ")}`);
3607
- info(` ${dim("expires")} ${session.expires_at}`);
3608
- } catch (err) {
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
- const childEnv = {
3618
- ...process.env,
3619
- DASHWISE_API_URL: apiUrl,
3620
- NEXT_PUBLIC_DASHWISE_API_URL: apiUrl,
3621
- ...apiKeyEnv !== void 0 ? { DASHWISE_API_KEY: apiKeyEnv } : {}
3622
- };
3623
- info(`Dev loop starting \u2014 Ctrl-C to stop.`);
3624
- info(` ${dim("api")} ${apiUrl}`);
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(` ${dim("cli token")} ${maskedToken(apiToken)} ${dim(`(from ${apiToken === savedConfig?.token ? "~/.config/dashwise/auth.json" : "$DASHWISE_API_TOKEN"})`)}`);
3631
- if (apiKeyEnv !== void 0) {
3632
- const source = process.env.DASHWISE_API_KEY === apiKeyEnv ? "$DASHWISE_API_KEY" : `~/.config/dashwise/auth.json#modules[${manifestSlug}]`;
3633
- const installId = moduleEntry?.installationId;
3634
- info(
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
- info(
3642
- ` Data-plane calls (db, qb) AND sign-in will fail until you provision a dev install:
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
- info("");
3648
- if (moduleEntry !== null && manifestSlug !== void 0) {
3649
- await reportConnectStatus({
3650
- installationId: moduleEntry.installationId,
3651
- moduleSlug: manifestSlug,
3652
- requestOptions: { baseUrl: apiUrl, auth: { token: apiToken } }
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
- const children = [];
3656
- if (!options.nextOnly) {
3657
- const sdkChild = spawnSdkWatcher(projectRoot, childEnv);
3658
- children.push({ name: "sdk", child: sdkChild, prefix: PREFIX_SDK });
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
- if (!options.sdkOnly) {
3661
- const nextChild = spawnNextDev(projectRoot, childEnv, port);
3662
- children.push({ name: "next", child: nextChild, prefix: PREFIX_NEXT });
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
- for (const { child, prefix } of children) {
3665
- pipeWithPrefix(child.stdout, process.stdout, prefix);
3666
- pipeWithPrefix(child.stderr, process.stderr, prefix);
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 manifestPoll = null;
3669
- const pollEntry = moduleEntry;
3670
- const enablePoll = !options.noRemotePoll && pollEntry !== null && apiKeyEnv !== void 0 && manifestSlug !== void 0;
3671
- if (enablePoll && pollEntry !== null) {
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
- info(
3694
- ` ${dim("poll")} backend manifest every ${intervalSec}s ${dim("(--no-remote-poll to disable)")}`
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
- info("");
4432
+ } catch (err) {
4433
+ return surfaceFailure(err);
3697
4434
  }
3698
- let shuttingDown = false;
3699
- const forwardSignal = (sig) => {
3700
- if (shuttingDown) return;
3701
- shuttingDown = true;
3702
- if (manifestPoll) {
3703
- manifestPoll.stop();
3704
- manifestPoll = null;
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
- if (manifestPoll) {
3743
- manifestPoll.stop();
3744
- manifestPoll = null;
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 exitCode = Math.max(0, ...results.map((r) => r.code));
3747
- if (exitCode === 0) {
3748
- success("Dev loop stopped cleanly.");
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
- return exitCode;
3751
- }
3752
- function spawnSdkWatcher(projectRoot, env) {
3753
- const child = spawn(
3754
- process.execPath,
3755
- [process.argv[1], "module", "dev", "--dir", projectRoot],
3756
- {
3757
- cwd: projectRoot,
3758
- env,
3759
- stdio: ["ignore", "pipe", "pipe"]
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
- return child;
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
- const pkgPath = `${projectRoot}/package.json`;
3781
- if (!existsSync(pkgPath)) return false;
3782
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
3783
- return typeof pkg.scripts?.[scriptName] === "string";
3784
- } catch {
3785
- return false;
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
- `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`
4492
+ `Adding field ${code(`${tableSlug}.${field.slug}`)} (${field.type}) to installation ${ctx.entry.installationId}\u2026`
3821
4493
  );
3822
- let minted;
3823
4494
  try {
3824
- minted = await createApiKey(
3825
- entry.installationId,
3826
- { name: "local-dev" },
3827
- { baseUrl: apiUrl, auth: { token: cliBearer } }
4495
+ const result = await addField(
4496
+ { table_slug: tableSlug, field },
4497
+ ctx.requestOptions
3828
4498
  );
3829
- } catch (err) {
3830
- if (err instanceof ApiError && err.code === "INSTALLATION_NOT_FOUND") {
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
- return null;
4502
+ } catch (err) {
4503
+ return surfaceFailure(err);
3841
4504
  }
3842
- const upgraded = {
3843
- apiKey: minted.raw_key,
3844
- apiKeyId: minted.api_key.id,
3845
- installationId: entry.installationId,
3846
- expiresAt: minted.api_key.expires_at ?? entry.expiresAt
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
- upsertModuleEntry(slug, upgraded);
4529
+ ctx = loadSchemaCommandContext({
4530
+ ...options.dir !== void 0 ? { dir: options.dir } : {}
4531
+ });
3850
4532
  } catch (err) {
3851
- warn(
3852
- `Minted a new API key for ${code(slug)} but failed to cache it: ${err.message}
3853
- API key (save manually): ${minted.raw_key}`
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
- success(`Upgraded ${code(slug)} to a dwk_ API key (id=${minted.api_key.id}).`);
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 PORT_PROBE_LIMIT = 10;
3864
- async function resolveDevPort(requested) {
3865
- if (requested !== void 0) {
3866
- if (await isPortFree(requested)) return requested;
3867
- const suggestion = await findFirstFreePort(requested + 1, PORT_PROBE_LIMIT);
3868
- 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}\`.`;
3869
- throw new Error(
3870
- `Port ${requested} is already in use. ${suggestionHint}`
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
- const found = await findFirstFreePort(DEFAULT_NEXT_PORT, PORT_PROBE_LIMIT);
3874
- if (found === null) {
3875
- throw new Error(
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
- return found;
3880
- }
3881
- async function isPortFree(port) {
3882
- const { createServer } = await import('net');
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
- server.listen(port, "0.0.0.0");
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
- return null;
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
- if (workspaces.length === 0) {
3909
- throw new Error(
3910
- "No workspaces found for your account. Create a workspace at the DashWise dashboard first."
3911
- );
3912
- }
3913
- if (flag === void 0) {
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
- const asNumber = Number.parseInt(flag, 10);
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 match;
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 revokes the module's cached local-dev credentials, so local ${code("dashwise dev")} against this workspace needs a fresh ${code("dashwise init --force")} afterwards (which in turn cannot coexist with the production install here).
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) return "replace";
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 `dashwise dev` here will need `dashwise init --force` afterwards"
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, or ${code("dashwise init --force")} to restore local dev.`
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
- if (options.json) {
6266
- out(
6267
- JSON.stringify({
6268
- installed: false,
6269
- module: mod.slug,
6270
- workspace: { id: workspace2.id, slug: workspace2.slug },
6271
- latest_version: latestVersion
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 0;
7092
+ return 1;
6275
7093
  }
6276
7094
  info(
6277
- `${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.`)
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
- return 0;
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
- const runtimeProvisioned = isRealRuntimeUrl(row.runtime_url);
6282
- const state = runtimeProvisioned && row.public_url ? "live" : runtimeProvisioned ? "deployed" : "not_deployed";
6283
- const installedVersion = row.module_version?.version ?? null;
6284
- const updateAvailable = isUpdateAvailable(installedVersion, latestVersion);
6285
- if (options.json) {
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
- return 0;
7127
+ } catch (err) {
7128
+ return surfaceUninstallError(err, mod.slug, workspace2.slug);
6303
7129
  }
6304
- const stateLabel = state === "live" ? "Live" : state === "deployed" ? "Deployed" : "Not deployed \u2014 runtime not provisioned";
6305
- out("");
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
- kv(
6314
- "Runtime:",
6315
- runtimeProvisioned ? `provisioned ${dim(`(${row.runtime_url})`)}` : "not provisioned"
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
- kv("Public URL:", row.public_url ?? dim("(none)"));
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
- `Re-run ${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)} to (re-)provision the runtime \u2014 the install POST is idempotent.`
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} --force\` to provision one.`
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} --force\` to provision one.`
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} --force\` to provision one.`
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> --force\` to mint a fresh one.`
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> --force\`.`
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> --force\`.`
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} --force\` to provision one.`
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
- requestOptions: {
8172
- baseUrl: resolveApiUrl(),
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
- process.stdout.write(` ${dim("full install access (no grants)")}
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(` ${dim("scope:")} full install access (no grants)`);
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} --force\` to provision one.`
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} --force\` to provision one.`
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; you can provision later by re-running `dashwise init --force` (or, once it ships, `dashwise module provision-dev-install`)."
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("--all", "Include revoked + expired keys in the output.").option("--json", "Emit JSON instead of the human-friendly table.").action(async (cmdOpts) => {
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("--name <name>", "Override the positional name arg (e.g. for scripted use).").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). Omit for a full-access key.",
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("-y, --yes", "Skip the per-key confirmation message (for CI / scripts).").action(
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("--json", "Emit a JSON array instead of the human-friendly list.").action(async (cmdOpts) => {
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).").action(async (url, cmdOpts) => {
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).").action(async (url, cmdOpts) => {
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. Revokes the module's cached local-dev credentials (re-run `dashwise init --force` for local dev afterwards)."
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. Keeps local module.json intact."
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. Example: `dashwise field add tasks priority --field-json \'{"slug":"priority","type":"single_select","options":["low","medium","high"]}\'`.'
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.14.2";
11103
+ return "0.15.0";
10087
11104
  }
10088
11105
  function parseIntOption(value) {
10089
11106
  const n = Number.parseInt(value, 10);