@dashai/cli 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -2032,6 +2032,47 @@ var init_init = __esm({
2032
2032
  init_workspaces();
2033
2033
  }
2034
2034
  });
2035
+ function loadSchemaCommandContext(opts) {
2036
+ const projectRoot = resolve(opts.dir ?? process.cwd());
2037
+ const manifestPath2 = join(projectRoot, "module.json");
2038
+ const manifest = readManifestOrThrow(projectRoot);
2039
+ const manifestRawBytes = readFileSync(manifestPath2, "utf-8");
2040
+ const slug = manifest.module?.slug;
2041
+ if (!slug) {
2042
+ throw new Error(
2043
+ `module.json at ${manifestPath2} has no module.slug \u2014 the manifest is malformed.`
2044
+ );
2045
+ }
2046
+ const cfg = readConfig();
2047
+ if (!cfg) {
2048
+ throw new Error("Not logged in. Run `dashwise login` first.");
2049
+ }
2050
+ const entry = readModuleEntry(slug);
2051
+ if (!entry) {
2052
+ throw new Error(
2053
+ `No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
2054
+ );
2055
+ }
2056
+ return {
2057
+ projectRoot,
2058
+ manifestPath: manifestPath2,
2059
+ manifest,
2060
+ manifestRawBytes,
2061
+ slug,
2062
+ cfg,
2063
+ entry,
2064
+ requestOptions: {
2065
+ baseUrl: resolveApiUrl(),
2066
+ auth: { token: entry.runtimeToken }
2067
+ }
2068
+ };
2069
+ }
2070
+ var init_schema_command_context = __esm({
2071
+ "src/lib/schema-command-context.ts"() {
2072
+ init_config();
2073
+ init_manifest_io();
2074
+ }
2075
+ });
2035
2076
 
2036
2077
  // src/commands/module/destroy-dev-install.ts
2037
2078
  var destroy_dev_install_exports = {};
@@ -2202,47 +2243,6 @@ var init_reset_dev_install = __esm({
2202
2243
  init_output();
2203
2244
  }
2204
2245
  });
2205
- function loadSchemaCommandContext(opts) {
2206
- const projectRoot = resolve(opts.dir ?? process.cwd());
2207
- const manifestPath2 = join(projectRoot, "module.json");
2208
- const manifest = readManifestOrThrow(projectRoot);
2209
- const manifestRawBytes = readFileSync(manifestPath2, "utf-8");
2210
- const slug = manifest.module?.slug;
2211
- if (!slug) {
2212
- throw new Error(
2213
- `module.json at ${manifestPath2} has no module.slug \u2014 the manifest is malformed.`
2214
- );
2215
- }
2216
- const cfg = readConfig();
2217
- if (!cfg) {
2218
- throw new Error("Not logged in. Run `dashwise login` first.");
2219
- }
2220
- const entry = readModuleEntry(slug);
2221
- if (!entry) {
2222
- throw new Error(
2223
- `No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
2224
- );
2225
- }
2226
- return {
2227
- projectRoot,
2228
- manifestPath: manifestPath2,
2229
- manifest,
2230
- manifestRawBytes,
2231
- slug,
2232
- cfg,
2233
- entry,
2234
- requestOptions: {
2235
- baseUrl: resolveApiUrl(),
2236
- auth: { token: entry.runtimeToken }
2237
- }
2238
- };
2239
- }
2240
- var init_schema_command_context = __esm({
2241
- "src/lib/schema-command-context.ts"() {
2242
- init_config();
2243
- init_manifest_io();
2244
- }
2245
- });
2246
2246
 
2247
2247
  // src/lib/field-spec.ts
2248
2248
  function parseFieldSpec(spec) {
@@ -2707,10 +2707,76 @@ var init_set_type = __esm({
2707
2707
  init_api_client();
2708
2708
  init_config();
2709
2709
  init_manifest_io();
2710
+
2711
+ // src/lib/manifest-poll.ts
2712
+ init_api_client();
2713
+ function startManifestPollLoop(opts) {
2714
+ const {
2715
+ installationId,
2716
+ manifestPath: manifestPath2,
2717
+ requestOptions,
2718
+ intervalMs,
2719
+ onUpdate,
2720
+ onError
2721
+ } = opts;
2722
+ let lastSeenJson = existsSync(manifestPath2) ? readFileSync(manifestPath2, "utf-8") : "";
2723
+ let timer = null;
2724
+ let stopped = false;
2725
+ const tick = async () => {
2726
+ if (stopped) return;
2727
+ try {
2728
+ const { manifest } = await getInstallationSchema(
2729
+ installationId,
2730
+ requestOptions
2731
+ );
2732
+ const remoteJson = JSON.stringify(manifest, null, 2) + "\n";
2733
+ const currentLocal = existsSync(manifestPath2) ? readFileSync(manifestPath2, "utf-8") : "";
2734
+ if (remoteJson === currentLocal) {
2735
+ lastSeenJson = remoteJson;
2736
+ } else if (remoteJson !== lastSeenJson) {
2737
+ writeFileSync(manifestPath2, remoteJson, "utf-8");
2738
+ lastSeenJson = remoteJson;
2739
+ if (onUpdate) {
2740
+ const delta = Buffer.byteLength(remoteJson, "utf8") - Buffer.byteLength(currentLocal, "utf8");
2741
+ const deltaStr = delta === 0 ? "0 bytes" : `${delta > 0 ? "+" : ""}${delta} bytes`;
2742
+ onUpdate(
2743
+ `Backend manifest changed \u2014 synced to module.json (${deltaStr}).`
2744
+ );
2745
+ }
2746
+ }
2747
+ } catch (err) {
2748
+ if (onError) {
2749
+ const message = err instanceof ApiError ? `${err.code} (HTTP ${err.status}): ${err.message}` : err.message;
2750
+ onError(new Error(message));
2751
+ }
2752
+ } finally {
2753
+ if (!stopped) {
2754
+ timer = setTimeout(() => void tick(), intervalMs);
2755
+ }
2756
+ }
2757
+ };
2758
+ timer = setTimeout(() => void tick(), intervalMs);
2759
+ return {
2760
+ stop: () => {
2761
+ if (stopped) return;
2762
+ stopped = true;
2763
+ if (timer) {
2764
+ clearTimeout(timer);
2765
+ timer = null;
2766
+ }
2767
+ },
2768
+ isRunning: () => !stopped
2769
+ };
2770
+ }
2771
+
2772
+ // src/commands/dev.ts
2710
2773
  init_output();
2711
2774
  var DEFAULT_NEXT_PORT = 3e3;
2712
2775
  var PREFIX_SDK = `\x1B[36m[sdk]\x1B[0m `;
2713
2776
  var PREFIX_NEXT = `\x1B[35m[next]\x1B[0m `;
2777
+ var PREFIX_POLL = `\x1B[33m[poll]\x1B[0m `;
2778
+ var DEFAULT_REMOTE_POLL_INTERVAL_SEC = 5;
2779
+ var MIN_REMOTE_POLL_INTERVAL_SEC = 1;
2714
2780
  async function devCommand(options) {
2715
2781
  if (options.sdkOnly && options.nextOnly) {
2716
2782
  fail(
@@ -2748,7 +2814,13 @@ async function devCommand(options) {
2748
2814
  return 1;
2749
2815
  }
2750
2816
  const apiUrl = options.apiUrl ?? process.env.DASHWISE_API_URL ?? savedConfig?.apiUrl ?? "http://localhost:3000";
2751
- const port = options.port ?? DEFAULT_NEXT_PORT;
2817
+ let port;
2818
+ try {
2819
+ port = await resolveDevPort(options.port);
2820
+ } catch (err) {
2821
+ fail(err.message);
2822
+ return 1;
2823
+ }
2752
2824
  const explicitInstallationId = options.installationId ?? process.env.DASHWISE_INSTALLATION_ID;
2753
2825
  const reuseDevSessionId = process.env.DASHWISE_DEV_SESSION_ID;
2754
2826
  const moduleEntry = manifestSlug !== void 0 ? readModuleEntry(manifestSlug) : null;
@@ -2839,10 +2911,43 @@ async function devCommand(options) {
2839
2911
  pipeWithPrefix(child.stdout, process.stdout, prefix);
2840
2912
  pipeWithPrefix(child.stderr, process.stderr, prefix);
2841
2913
  }
2914
+ let manifestPoll = null;
2915
+ const enablePoll = !options.noRemotePoll && moduleEntry !== void 0 && manifestSlug !== void 0;
2916
+ if (enablePoll) {
2917
+ const intervalSec = Math.max(
2918
+ MIN_REMOTE_POLL_INTERVAL_SEC,
2919
+ options.remotePollIntervalSec ?? DEFAULT_REMOTE_POLL_INTERVAL_SEC
2920
+ );
2921
+ manifestPoll = startManifestPollLoop({
2922
+ installationId: moduleEntry.installationId,
2923
+ manifestPath: manifestPath2,
2924
+ requestOptions: {
2925
+ baseUrl: apiUrl,
2926
+ auth: { token: moduleEntry.runtimeToken }
2927
+ },
2928
+ intervalMs: intervalSec * 1e3,
2929
+ onUpdate: (msg) => {
2930
+ process.stdout.write(`${PREFIX_POLL}${msg}
2931
+ `);
2932
+ },
2933
+ onError: (err) => {
2934
+ process.stderr.write(`${PREFIX_POLL}${err.message}
2935
+ `);
2936
+ }
2937
+ });
2938
+ info(
2939
+ ` ${dim("poll")} backend manifest every ${intervalSec}s ${dim("(--no-remote-poll to disable)")}`
2940
+ );
2941
+ info("");
2942
+ }
2842
2943
  let shuttingDown = false;
2843
2944
  const forwardSignal = (sig) => {
2844
2945
  if (shuttingDown) return;
2845
2946
  shuttingDown = true;
2947
+ if (manifestPoll) {
2948
+ manifestPoll.stop();
2949
+ manifestPoll = null;
2950
+ }
2846
2951
  info(`
2847
2952
  Received ${sig}, stopping children\u2026`);
2848
2953
  for (const { child } of children) {
@@ -2879,6 +2984,10 @@ Received ${sig}, stopping children\u2026`);
2879
2984
  }
2880
2985
  }
2881
2986
  }
2987
+ if (manifestPoll) {
2988
+ manifestPoll.stop();
2989
+ manifestPoll = null;
2990
+ }
2882
2991
  const exitCode = Math.max(0, ...results.map((r) => r.code));
2883
2992
  if (exitCode === 0) {
2884
2993
  success("Dev loop stopped cleanly.");
@@ -2951,6 +3060,44 @@ function computeCallbackOrigins(port, extra) {
2951
3060
  const def = `http://localhost:${port}`;
2952
3061
  return Array.from(/* @__PURE__ */ new Set([def, ...extra]));
2953
3062
  }
3063
+ var PORT_PROBE_LIMIT = 10;
3064
+ async function resolveDevPort(requested) {
3065
+ if (requested !== void 0) {
3066
+ if (await isPortFree(requested)) return requested;
3067
+ const suggestion = await findFirstFreePort(requested + 1, PORT_PROBE_LIMIT);
3068
+ 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}\`.`;
3069
+ throw new Error(
3070
+ `Port ${requested} is already in use. ${suggestionHint}`
3071
+ );
3072
+ }
3073
+ const found = await findFirstFreePort(DEFAULT_NEXT_PORT, PORT_PROBE_LIMIT);
3074
+ if (found === null) {
3075
+ throw new Error(
3076
+ `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.`
3077
+ );
3078
+ }
3079
+ return found;
3080
+ }
3081
+ async function isPortFree(port) {
3082
+ const { createServer } = await import('net');
3083
+ return new Promise((resolve11) => {
3084
+ const server = createServer();
3085
+ server.once("error", () => {
3086
+ resolve11(false);
3087
+ });
3088
+ server.once("listening", () => {
3089
+ server.close(() => resolve11(true));
3090
+ });
3091
+ server.listen(port, "0.0.0.0");
3092
+ });
3093
+ }
3094
+ async function findFirstFreePort(start, limit) {
3095
+ for (let i = 0; i < limit; i++) {
3096
+ const port = start + i;
3097
+ if (await isPortFree(port)) return port;
3098
+ }
3099
+ return null;
3100
+ }
2954
3101
  async function resolveTargetWorkspace(apiUrl, apiToken, flag) {
2955
3102
  const workspaces = await apiRequest(
2956
3103
  "GET",
@@ -4986,6 +5133,92 @@ async function depsRemoveCommand(providerSlug, options) {
4986
5133
  return 0;
4987
5134
  }
4988
5135
 
5136
+ // src/commands/pull.ts
5137
+ init_api_client();
5138
+ init_manifest_io();
5139
+ init_schema_command_context();
5140
+ init_output();
5141
+ async function pullCommand(options) {
5142
+ let ctx;
5143
+ try {
5144
+ ctx = loadSchemaCommandContext({
5145
+ ...options.dir !== void 0 ? { dir: options.dir } : {}
5146
+ });
5147
+ } catch (err) {
5148
+ fail(err.message);
5149
+ return 1;
5150
+ }
5151
+ info(`Pulling manifest for installation ${ctx.entry.installationId}\u2026`);
5152
+ let remoteManifest;
5153
+ try {
5154
+ const response = await getInstallationSchema(
5155
+ ctx.entry.installationId,
5156
+ ctx.requestOptions
5157
+ );
5158
+ remoteManifest = response.manifest;
5159
+ } catch (err) {
5160
+ return surfacePullFailure(err);
5161
+ }
5162
+ const localJson = JSON.stringify(ctx.manifest, null, 2) + "\n";
5163
+ const remoteJson = JSON.stringify(remoteManifest, null, 2) + "\n";
5164
+ if (localJson === remoteJson) {
5165
+ success("Already in sync \u2014 local module.json matches backend.");
5166
+ return 0;
5167
+ }
5168
+ const localBytes = Buffer.byteLength(localJson, "utf8");
5169
+ const remoteBytes = Buffer.byteLength(remoteJson, "utf8");
5170
+ const delta = remoteBytes - localBytes;
5171
+ const deltaStr = delta === 0 ? "0 bytes" : `${delta > 0 ? "+" : ""}${delta} bytes`;
5172
+ if (options.dryRun) {
5173
+ info(
5174
+ `Drift detected (${deltaStr}). ${code("--dry-run")} \u2014 not writing.`
5175
+ );
5176
+ info(`Run ${code("dashwise pull")} without ${code("--dry-run")} to apply.`);
5177
+ return 0;
5178
+ }
5179
+ try {
5180
+ writeManifest(remoteManifest, ctx.projectRoot);
5181
+ } catch (err) {
5182
+ fail(`Failed to write ${manifestPath(ctx.projectRoot)}: ${err.message}`);
5183
+ return 1;
5184
+ }
5185
+ success(
5186
+ `Synced module.json from backend (${deltaStr}). ${dim(`Run \`dashwise module generate\` to refresh typed SDK if your watcher isn't running.`)}`
5187
+ );
5188
+ return 0;
5189
+ }
5190
+ function surfacePullFailure(err) {
5191
+ if (err instanceof ApiError) {
5192
+ switch (err.code) {
5193
+ case "UNAUTHENTICATED":
5194
+ fail(
5195
+ `Runtime token rejected. The cached token for this module may be expired \u2014 re-run \`dashwise init <slug> --force\` to mint a fresh one.`
5196
+ );
5197
+ break;
5198
+ case "SANDBOX_TOKEN_NOT_ACCEPTED":
5199
+ fail(
5200
+ `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\`.`
5201
+ );
5202
+ break;
5203
+ case "INSTALL_MISMATCH":
5204
+ fail(
5205
+ `Runtime token is bound to a different installation than expected. The cached install id may be stale \u2014 re-run \`dashwise init <slug> --force\`.`
5206
+ );
5207
+ break;
5208
+ case "INSTALLATION_NOT_FOUND":
5209
+ fail(
5210
+ `The backend has no record of this installation. It may have been destroyed (GC, manual cleanup). Re-run \`dashwise init <slug>\` to provision a fresh one.`
5211
+ );
5212
+ break;
5213
+ default:
5214
+ fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
5215
+ }
5216
+ return 1;
5217
+ }
5218
+ fail(`Pull failed: ${err.message}`);
5219
+ return 1;
5220
+ }
5221
+
4989
5222
  // src/commands/query.ts
4990
5223
  init_manifest_io();
4991
5224
  init_output();
@@ -5591,7 +5824,14 @@ program.command("dev").description(
5591
5824
  ).option(
5592
5825
  "--api-url <url>",
5593
5826
  "Override the `DASHWISE_API_URL` env var passed to children (default: saved CLI config)."
5594
- ).option("--port <number>", "Port for `next dev` (default: 3000).", parseIntOption).option("--sdk-only", "Only run the manifest watcher; skip `next dev`.").option("--next-only", "Only run `next dev`; skip the manifest watcher.").action(
5827
+ ).option("--port <number>", "Port for `next dev` (default: 3000).", parseIntOption).option("--sdk-only", "Only run the manifest watcher; skip `next dev`.").option("--next-only", "Only run `next dev`; skip the manifest watcher.").option(
5828
+ "--no-remote-poll",
5829
+ "Disable periodic backend manifest polling. By default, `dashwise dev` polls `GET /api/installations/:id/schema` every few seconds + writes module.json when the backend has advanced (dashboard edits, AI builder mutations, teammate commands)."
5830
+ ).option(
5831
+ "--remote-poll-interval <seconds>",
5832
+ "Backend manifest poll interval, in seconds. Default: 5. Minimum: 1.",
5833
+ parseIntOption
5834
+ ).action(
5595
5835
  async (cmdOpts) => {
5596
5836
  process.exitCode = await devCommand({
5597
5837
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
@@ -5601,7 +5841,11 @@ program.command("dev").description(
5601
5841
  ...cmdOpts.apiUrl !== void 0 ? { apiUrl: cmdOpts.apiUrl } : {},
5602
5842
  ...cmdOpts.port !== void 0 ? { port: cmdOpts.port } : {},
5603
5843
  ...cmdOpts.sdkOnly !== void 0 ? { sdkOnly: cmdOpts.sdkOnly } : {},
5604
- ...cmdOpts.nextOnly !== void 0 ? { nextOnly: cmdOpts.nextOnly } : {}
5844
+ ...cmdOpts.nextOnly !== void 0 ? { nextOnly: cmdOpts.nextOnly } : {},
5845
+ // Commander's --no-foo flag maps to `foo: false`. devCommand's
5846
+ // option is `noRemotePoll: boolean` — invert to map cleanly.
5847
+ ...cmdOpts.remotePoll === false ? { noRemotePoll: true } : {},
5848
+ ...cmdOpts.remotePollInterval !== void 0 ? { remotePollIntervalSec: cmdOpts.remotePollInterval } : {}
5605
5849
  });
5606
5850
  }
5607
5851
  );
@@ -5753,6 +5997,14 @@ program.command("query").description(
5753
5997
  });
5754
5998
  }
5755
5999
  );
6000
+ program.command("pull").description(
6001
+ "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."
6002
+ ).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) => {
6003
+ process.exitCode = await pullCommand({
6004
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
6005
+ ...cmdOpts.dryRun !== void 0 ? { dryRun: cmdOpts.dryRun } : {}
6006
+ });
6007
+ });
5756
6008
  moduleCmd.command("publish").description("Pack the current directory + upload it as a new module version.").option("--dir <path>", "Project root (default: current directory).").option("--bump <direction>", "Semver bump: patch | minor | major.").option("--dry-run", "Validate + upload but do not persist the version row.").option("-y, --yes", "Skip the confirmation prompt (for CI / scripted use).").option(
5757
6009
  "--exclude <pattern...>",
5758
6010
  "Extra exclude patterns (segment-matched) appended to the defaults."
@@ -5944,7 +6196,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
5944
6196
  }
5945
6197
  })();
5946
6198
  function getVersion() {
5947
- return "0.3.1";
6199
+ return "0.4.0";
5948
6200
  }
5949
6201
  function parseIntOption(value) {
5950
6202
  const n = Number.parseInt(value, 10);