@dashai/cli 0.3.2 → 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(
@@ -2845,10 +2911,43 @@ async function devCommand(options) {
2845
2911
  pipeWithPrefix(child.stdout, process.stdout, prefix);
2846
2912
  pipeWithPrefix(child.stderr, process.stderr, prefix);
2847
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
+ }
2848
2943
  let shuttingDown = false;
2849
2944
  const forwardSignal = (sig) => {
2850
2945
  if (shuttingDown) return;
2851
2946
  shuttingDown = true;
2947
+ if (manifestPoll) {
2948
+ manifestPoll.stop();
2949
+ manifestPoll = null;
2950
+ }
2852
2951
  info(`
2853
2952
  Received ${sig}, stopping children\u2026`);
2854
2953
  for (const { child } of children) {
@@ -2885,6 +2984,10 @@ Received ${sig}, stopping children\u2026`);
2885
2984
  }
2886
2985
  }
2887
2986
  }
2987
+ if (manifestPoll) {
2988
+ manifestPoll.stop();
2989
+ manifestPoll = null;
2990
+ }
2888
2991
  const exitCode = Math.max(0, ...results.map((r) => r.code));
2889
2992
  if (exitCode === 0) {
2890
2993
  success("Dev loop stopped cleanly.");
@@ -5030,6 +5133,92 @@ async function depsRemoveCommand(providerSlug, options) {
5030
5133
  return 0;
5031
5134
  }
5032
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
+
5033
5222
  // src/commands/query.ts
5034
5223
  init_manifest_io();
5035
5224
  init_output();
@@ -5635,7 +5824,14 @@ program.command("dev").description(
5635
5824
  ).option(
5636
5825
  "--api-url <url>",
5637
5826
  "Override the `DASHWISE_API_URL` env var passed to children (default: saved CLI config)."
5638
- ).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(
5639
5835
  async (cmdOpts) => {
5640
5836
  process.exitCode = await devCommand({
5641
5837
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
@@ -5645,7 +5841,11 @@ program.command("dev").description(
5645
5841
  ...cmdOpts.apiUrl !== void 0 ? { apiUrl: cmdOpts.apiUrl } : {},
5646
5842
  ...cmdOpts.port !== void 0 ? { port: cmdOpts.port } : {},
5647
5843
  ...cmdOpts.sdkOnly !== void 0 ? { sdkOnly: cmdOpts.sdkOnly } : {},
5648
- ...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 } : {}
5649
5849
  });
5650
5850
  }
5651
5851
  );
@@ -5797,6 +5997,14 @@ program.command("query").description(
5797
5997
  });
5798
5998
  }
5799
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
+ });
5800
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(
5801
6009
  "--exclude <pattern...>",
5802
6010
  "Extra exclude patterns (segment-matched) appended to the defaults."
@@ -5988,7 +6196,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
5988
6196
  }
5989
6197
  })();
5990
6198
  function getVersion() {
5991
- return "0.3.2";
6199
+ return "0.4.0";
5992
6200
  }
5993
6201
  function parseIntOption(value) {
5994
6202
  const n = Number.parseInt(value, 10);