@dashai/cli 0.3.2 → 0.5.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
@@ -231,6 +231,30 @@ async function destroyDevInstallation(installationId, options = {}) {
231
231
  options
232
232
  );
233
233
  }
234
+ async function createApiKey(installationId, body, options = {}) {
235
+ return apiRequest(
236
+ "POST",
237
+ `/api/installations/${installationId}/api-keys`,
238
+ body,
239
+ options
240
+ );
241
+ }
242
+ async function listApiKeys(installationId, options = {}) {
243
+ return apiRequest(
244
+ "GET",
245
+ `/api/installations/${installationId}/api-keys`,
246
+ void 0,
247
+ options
248
+ );
249
+ }
250
+ async function revokeApiKey(installationId, keyId, options = {}) {
251
+ return apiRequest(
252
+ "DELETE",
253
+ `/api/installations/${installationId}/api-keys/${keyId}`,
254
+ void 0,
255
+ options
256
+ );
257
+ }
234
258
  async function createTable(installationId, body, options = {}) {
235
259
  return apiRequest(
236
260
  "POST",
@@ -2032,6 +2056,47 @@ var init_init = __esm({
2032
2056
  init_workspaces();
2033
2057
  }
2034
2058
  });
2059
+ function loadSchemaCommandContext(opts) {
2060
+ const projectRoot = resolve(opts.dir ?? process.cwd());
2061
+ const manifestPath2 = join(projectRoot, "module.json");
2062
+ const manifest = readManifestOrThrow(projectRoot);
2063
+ const manifestRawBytes = readFileSync(manifestPath2, "utf-8");
2064
+ const slug = manifest.module?.slug;
2065
+ if (!slug) {
2066
+ throw new Error(
2067
+ `module.json at ${manifestPath2} has no module.slug \u2014 the manifest is malformed.`
2068
+ );
2069
+ }
2070
+ const cfg = readConfig();
2071
+ if (!cfg) {
2072
+ throw new Error("Not logged in. Run `dashwise login` first.");
2073
+ }
2074
+ const entry = readModuleEntry(slug);
2075
+ if (!entry) {
2076
+ throw new Error(
2077
+ `No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
2078
+ );
2079
+ }
2080
+ return {
2081
+ projectRoot,
2082
+ manifestPath: manifestPath2,
2083
+ manifest,
2084
+ manifestRawBytes,
2085
+ slug,
2086
+ cfg,
2087
+ entry,
2088
+ requestOptions: {
2089
+ baseUrl: resolveApiUrl(),
2090
+ auth: { token: entry.runtimeToken }
2091
+ }
2092
+ };
2093
+ }
2094
+ var init_schema_command_context = __esm({
2095
+ "src/lib/schema-command-context.ts"() {
2096
+ init_config();
2097
+ init_manifest_io();
2098
+ }
2099
+ });
2035
2100
 
2036
2101
  // src/commands/module/destroy-dev-install.ts
2037
2102
  var destroy_dev_install_exports = {};
@@ -2202,47 +2267,6 @@ var init_reset_dev_install = __esm({
2202
2267
  init_output();
2203
2268
  }
2204
2269
  });
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
2270
 
2247
2271
  // src/lib/field-spec.ts
2248
2272
  function parseFieldSpec(spec) {
@@ -2707,10 +2731,76 @@ var init_set_type = __esm({
2707
2731
  init_api_client();
2708
2732
  init_config();
2709
2733
  init_manifest_io();
2734
+
2735
+ // src/lib/manifest-poll.ts
2736
+ init_api_client();
2737
+ function startManifestPollLoop(opts) {
2738
+ const {
2739
+ installationId,
2740
+ manifestPath: manifestPath2,
2741
+ requestOptions,
2742
+ intervalMs,
2743
+ onUpdate,
2744
+ onError
2745
+ } = opts;
2746
+ let lastSeenJson = existsSync(manifestPath2) ? readFileSync(manifestPath2, "utf-8") : "";
2747
+ let timer = null;
2748
+ let stopped = false;
2749
+ const tick = async () => {
2750
+ if (stopped) return;
2751
+ try {
2752
+ const { manifest } = await getInstallationSchema(
2753
+ installationId,
2754
+ requestOptions
2755
+ );
2756
+ const remoteJson = JSON.stringify(manifest, null, 2) + "\n";
2757
+ const currentLocal = existsSync(manifestPath2) ? readFileSync(manifestPath2, "utf-8") : "";
2758
+ if (remoteJson === currentLocal) {
2759
+ lastSeenJson = remoteJson;
2760
+ } else if (remoteJson !== lastSeenJson) {
2761
+ writeFileSync(manifestPath2, remoteJson, "utf-8");
2762
+ lastSeenJson = remoteJson;
2763
+ if (onUpdate) {
2764
+ const delta = Buffer.byteLength(remoteJson, "utf8") - Buffer.byteLength(currentLocal, "utf8");
2765
+ const deltaStr = delta === 0 ? "0 bytes" : `${delta > 0 ? "+" : ""}${delta} bytes`;
2766
+ onUpdate(
2767
+ `Backend manifest changed \u2014 synced to module.json (${deltaStr}).`
2768
+ );
2769
+ }
2770
+ }
2771
+ } catch (err) {
2772
+ if (onError) {
2773
+ const message = err instanceof ApiError ? `${err.code} (HTTP ${err.status}): ${err.message}` : err.message;
2774
+ onError(new Error(message));
2775
+ }
2776
+ } finally {
2777
+ if (!stopped) {
2778
+ timer = setTimeout(() => void tick(), intervalMs);
2779
+ }
2780
+ }
2781
+ };
2782
+ timer = setTimeout(() => void tick(), intervalMs);
2783
+ return {
2784
+ stop: () => {
2785
+ if (stopped) return;
2786
+ stopped = true;
2787
+ if (timer) {
2788
+ clearTimeout(timer);
2789
+ timer = null;
2790
+ }
2791
+ },
2792
+ isRunning: () => !stopped
2793
+ };
2794
+ }
2795
+
2796
+ // src/commands/dev.ts
2710
2797
  init_output();
2711
2798
  var DEFAULT_NEXT_PORT = 3e3;
2712
2799
  var PREFIX_SDK = `\x1B[36m[sdk]\x1B[0m `;
2713
2800
  var PREFIX_NEXT = `\x1B[35m[next]\x1B[0m `;
2801
+ var PREFIX_POLL = `\x1B[33m[poll]\x1B[0m `;
2802
+ var DEFAULT_REMOTE_POLL_INTERVAL_SEC = 5;
2803
+ var MIN_REMOTE_POLL_INTERVAL_SEC = 1;
2714
2804
  async function devCommand(options) {
2715
2805
  if (options.sdkOnly && options.nextOnly) {
2716
2806
  fail(
@@ -2845,10 +2935,43 @@ async function devCommand(options) {
2845
2935
  pipeWithPrefix(child.stdout, process.stdout, prefix);
2846
2936
  pipeWithPrefix(child.stderr, process.stderr, prefix);
2847
2937
  }
2938
+ let manifestPoll = null;
2939
+ const enablePoll = !options.noRemotePoll && moduleEntry !== null && manifestSlug !== void 0;
2940
+ if (enablePoll) {
2941
+ const intervalSec = Math.max(
2942
+ MIN_REMOTE_POLL_INTERVAL_SEC,
2943
+ options.remotePollIntervalSec ?? DEFAULT_REMOTE_POLL_INTERVAL_SEC
2944
+ );
2945
+ manifestPoll = startManifestPollLoop({
2946
+ installationId: moduleEntry.installationId,
2947
+ manifestPath: manifestPath2,
2948
+ requestOptions: {
2949
+ baseUrl: apiUrl,
2950
+ auth: { token: moduleEntry.runtimeToken }
2951
+ },
2952
+ intervalMs: intervalSec * 1e3,
2953
+ onUpdate: (msg) => {
2954
+ process.stdout.write(`${PREFIX_POLL}${msg}
2955
+ `);
2956
+ },
2957
+ onError: (err) => {
2958
+ process.stderr.write(`${PREFIX_POLL}${err.message}
2959
+ `);
2960
+ }
2961
+ });
2962
+ info(
2963
+ ` ${dim("poll")} backend manifest every ${intervalSec}s ${dim("(--no-remote-poll to disable)")}`
2964
+ );
2965
+ info("");
2966
+ }
2848
2967
  let shuttingDown = false;
2849
2968
  const forwardSignal = (sig) => {
2850
2969
  if (shuttingDown) return;
2851
2970
  shuttingDown = true;
2971
+ if (manifestPoll) {
2972
+ manifestPoll.stop();
2973
+ manifestPoll = null;
2974
+ }
2852
2975
  info(`
2853
2976
  Received ${sig}, stopping children\u2026`);
2854
2977
  for (const { child } of children) {
@@ -2885,6 +3008,10 @@ Received ${sig}, stopping children\u2026`);
2885
3008
  }
2886
3009
  }
2887
3010
  }
3011
+ if (manifestPoll) {
3012
+ manifestPoll.stop();
3013
+ manifestPoll = null;
3014
+ }
2888
3015
  const exitCode = Math.max(0, ...results.map((r) => r.code));
2889
3016
  if (exitCode === 0) {
2890
3017
  success("Dev loop stopped cleanly.");
@@ -5030,6 +5157,457 @@ async function depsRemoveCommand(providerSlug, options) {
5030
5157
  return 0;
5031
5158
  }
5032
5159
 
5160
+ // src/commands/pull.ts
5161
+ init_api_client();
5162
+ init_manifest_io();
5163
+ init_schema_command_context();
5164
+ init_output();
5165
+ async function pullCommand(options) {
5166
+ let ctx;
5167
+ try {
5168
+ ctx = loadSchemaCommandContext({
5169
+ ...options.dir !== void 0 ? { dir: options.dir } : {}
5170
+ });
5171
+ } catch (err) {
5172
+ fail(err.message);
5173
+ return 1;
5174
+ }
5175
+ info(`Pulling manifest for installation ${ctx.entry.installationId}\u2026`);
5176
+ let remoteManifest;
5177
+ try {
5178
+ const response = await getInstallationSchema(
5179
+ ctx.entry.installationId,
5180
+ ctx.requestOptions
5181
+ );
5182
+ remoteManifest = response.manifest;
5183
+ } catch (err) {
5184
+ return surfacePullFailure(err);
5185
+ }
5186
+ const localJson = JSON.stringify(ctx.manifest, null, 2) + "\n";
5187
+ const remoteJson = JSON.stringify(remoteManifest, null, 2) + "\n";
5188
+ if (localJson === remoteJson) {
5189
+ success("Already in sync \u2014 local module.json matches backend.");
5190
+ return 0;
5191
+ }
5192
+ const localBytes = Buffer.byteLength(localJson, "utf8");
5193
+ const remoteBytes = Buffer.byteLength(remoteJson, "utf8");
5194
+ const delta = remoteBytes - localBytes;
5195
+ const deltaStr = delta === 0 ? "0 bytes" : `${delta > 0 ? "+" : ""}${delta} bytes`;
5196
+ if (options.dryRun) {
5197
+ info(
5198
+ `Drift detected (${deltaStr}). ${code("--dry-run")} \u2014 not writing.`
5199
+ );
5200
+ info(`Run ${code("dashwise pull")} without ${code("--dry-run")} to apply.`);
5201
+ return 0;
5202
+ }
5203
+ try {
5204
+ writeManifest(remoteManifest, ctx.projectRoot);
5205
+ } catch (err) {
5206
+ fail(`Failed to write ${manifestPath(ctx.projectRoot)}: ${err.message}`);
5207
+ return 1;
5208
+ }
5209
+ success(
5210
+ `Synced module.json from backend (${deltaStr}). ${dim(`Run \`dashwise module generate\` to refresh typed SDK if your watcher isn't running.`)}`
5211
+ );
5212
+ return 0;
5213
+ }
5214
+ function surfacePullFailure(err) {
5215
+ if (err instanceof ApiError) {
5216
+ switch (err.code) {
5217
+ case "UNAUTHENTICATED":
5218
+ fail(
5219
+ `Runtime token rejected. The cached token for this module may be expired \u2014 re-run \`dashwise init <slug> --force\` to mint a fresh one.`
5220
+ );
5221
+ break;
5222
+ case "SANDBOX_TOKEN_NOT_ACCEPTED":
5223
+ fail(
5224
+ `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\`.`
5225
+ );
5226
+ break;
5227
+ case "INSTALL_MISMATCH":
5228
+ fail(
5229
+ `Runtime token is bound to a different installation than expected. The cached install id may be stale \u2014 re-run \`dashwise init <slug> --force\`.`
5230
+ );
5231
+ break;
5232
+ case "INSTALLATION_NOT_FOUND":
5233
+ fail(
5234
+ `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.`
5235
+ );
5236
+ break;
5237
+ default:
5238
+ fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
5239
+ }
5240
+ return 1;
5241
+ }
5242
+ fail(`Pull failed: ${err.message}`);
5243
+ return 1;
5244
+ }
5245
+
5246
+ // src/commands/api-keys.ts
5247
+ init_api_client();
5248
+ init_config();
5249
+ init_manifest_io();
5250
+ init_output();
5251
+ function loadContext(opts) {
5252
+ const manifest = readManifestOrThrow(opts.dir);
5253
+ const slug = manifest.module?.slug;
5254
+ if (!slug) {
5255
+ throw new Error(
5256
+ `module.json has no module.slug \u2014 the manifest is malformed.`
5257
+ );
5258
+ }
5259
+ const cfg = readConfig();
5260
+ if (!cfg) {
5261
+ throw new Error("Not logged in. Run `dashwise login` first.");
5262
+ }
5263
+ const entry = readModuleEntry(slug);
5264
+ if (!entry) {
5265
+ throw new Error(
5266
+ `No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
5267
+ );
5268
+ }
5269
+ return {
5270
+ installationId: entry.installationId,
5271
+ slug,
5272
+ requestOptions: {
5273
+ baseUrl: resolveApiUrl(),
5274
+ auth: { token: cfg.token }
5275
+ }
5276
+ };
5277
+ }
5278
+ async function apiKeysListCommand(options) {
5279
+ let ctx;
5280
+ try {
5281
+ ctx = loadContext(options);
5282
+ } catch (err) {
5283
+ fail(err.message);
5284
+ return 1;
5285
+ }
5286
+ let keys;
5287
+ try {
5288
+ const resp = await listApiKeys(ctx.installationId, ctx.requestOptions);
5289
+ keys = resp.api_keys;
5290
+ } catch (err) {
5291
+ return surfaceApiKeysError(err);
5292
+ }
5293
+ const filtered = options.all ? keys : keys.filter((k) => k.revoked_at === null);
5294
+ if (options.json) {
5295
+ process.stdout.write(`${JSON.stringify(filtered, null, 2)}
5296
+ `);
5297
+ return 0;
5298
+ }
5299
+ if (filtered.length === 0) {
5300
+ info(
5301
+ options.all ? `No API keys (active or revoked) for ${code(ctx.slug)}.` : `No active API keys for ${code(ctx.slug)}. Run ${code("dashwise api-keys create <name>")} to mint one, or ${code("dashwise api-keys list --all")} to include revoked.`
5302
+ );
5303
+ return 0;
5304
+ }
5305
+ info(`API keys for ${code(ctx.slug)} (install id=${ctx.installationId}):`);
5306
+ for (const k of filtered) {
5307
+ const status = formatKeyStatus(k);
5308
+ const lastUsed = k.last_used_at ? `last used ${formatRelative(new Date(k.last_used_at))}` : "never used";
5309
+ process.stdout.write(
5310
+ ` ${code(`#${k.id}`)} ${k.name} ${dim(`dwk_${k.key_prefix}\u2026`)} ${status} ${dim(`(${lastUsed})`)}
5311
+ `
5312
+ );
5313
+ }
5314
+ return 0;
5315
+ }
5316
+ async function apiKeysCreateCommand(nameArg, options) {
5317
+ const name = nameArg ?? options.name;
5318
+ if (!name) {
5319
+ fail("Usage: `dashwise api-keys create <name>` (e.g. `dashwise api-keys create production`)");
5320
+ return 1;
5321
+ }
5322
+ let ctx;
5323
+ try {
5324
+ ctx = loadContext(options);
5325
+ } catch (err) {
5326
+ fail(err.message);
5327
+ return 1;
5328
+ }
5329
+ const body = { name };
5330
+ if (options.scopes !== void 0) {
5331
+ body.scopes = options.scopes.split(",").map((s) => s.trim()).filter(Boolean);
5332
+ }
5333
+ if (options.expiresAt !== void 0) {
5334
+ body.expires_at = options.expiresAt;
5335
+ }
5336
+ let resp;
5337
+ try {
5338
+ resp = await createApiKey(ctx.installationId, body, ctx.requestOptions);
5339
+ } catch (err) {
5340
+ return surfaceApiKeysError(err);
5341
+ }
5342
+ if (options.rawOnly) {
5343
+ process.stderr.write(
5344
+ `Created API key #${resp.api_key.id} (${resp.api_key.name}) for install ${ctx.installationId}.
5345
+ `
5346
+ );
5347
+ process.stdout.write(`${resp.raw_key}
5348
+ `);
5349
+ return 0;
5350
+ }
5351
+ success(
5352
+ `Created API key ${code(`#${resp.api_key.id}`)} (${resp.api_key.name}) for ${code(ctx.slug)}.`
5353
+ );
5354
+ info("");
5355
+ warn(
5356
+ "This is the ONLY time the full key is displayed. Save it now \u2014 the backend stores a hash, not the plaintext."
5357
+ );
5358
+ info("");
5359
+ process.stdout.write(` ${code(resp.raw_key)}
5360
+ `);
5361
+ info("");
5362
+ info(`Use in a deployed module's env:`);
5363
+ info(` ${code(`DASHWISE_API_KEY=${resp.raw_key.slice(0, 12)}\u2026`)}`);
5364
+ info(
5365
+ ` ${code(`DASHWISE_INSTALLATION_ID=${ctx.installationId}`)}`
5366
+ );
5367
+ info(
5368
+ ` ${code(`DASHWISE_API_URL=${resolveApiUrl()}`)}`
5369
+ );
5370
+ return 0;
5371
+ }
5372
+ async function apiKeysRevokeCommand(keyIdArg, options) {
5373
+ if (!keyIdArg) {
5374
+ fail("Usage: `dashwise api-keys revoke <key-id>` \u2014 id from `dashwise api-keys list`");
5375
+ return 1;
5376
+ }
5377
+ const keyId = Number.parseInt(keyIdArg, 10);
5378
+ if (!Number.isFinite(keyId) || keyId <= 0) {
5379
+ fail(`Invalid key id "${keyIdArg}" \u2014 expected a positive integer.`);
5380
+ return 1;
5381
+ }
5382
+ let ctx;
5383
+ try {
5384
+ ctx = loadContext(options);
5385
+ } catch (err) {
5386
+ fail(err.message);
5387
+ return 1;
5388
+ }
5389
+ if (!options.yes) {
5390
+ info(
5391
+ `Revoking API key #${keyId} for ${code(ctx.slug)} (install ${ctx.installationId}). Pass ${code("--yes")} to skip this confirmation in scripts.`
5392
+ );
5393
+ }
5394
+ let resp;
5395
+ try {
5396
+ resp = await revokeApiKey(ctx.installationId, keyId, ctx.requestOptions);
5397
+ } catch (err) {
5398
+ return surfaceApiKeysError(err);
5399
+ }
5400
+ success(
5401
+ `Revoked API key #${resp.api_key.id} (${resp.api_key.name}). Future requests with this key will return 401.`
5402
+ );
5403
+ return 0;
5404
+ }
5405
+ function surfaceApiKeysError(err) {
5406
+ if (err instanceof ApiError) {
5407
+ switch (err.code) {
5408
+ case "UNAUTHENTICATED":
5409
+ fail(
5410
+ `Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
5411
+ );
5412
+ return 1;
5413
+ case "NOT_WORKSPACE_MEMBER":
5414
+ fail(
5415
+ `You're not a member of this installation's workspace. API-key management requires workspace membership.`
5416
+ );
5417
+ return 1;
5418
+ case "INSTALLATION_NOT_FOUND":
5419
+ fail(
5420
+ `Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
5421
+ );
5422
+ return 1;
5423
+ case "API_KEY_NOT_FOUND":
5424
+ fail(
5425
+ `API key not found for this installation. Use ${code("dashwise api-keys list")} to see valid ids.`
5426
+ );
5427
+ return 1;
5428
+ default:
5429
+ fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
5430
+ return 1;
5431
+ }
5432
+ }
5433
+ fail(`API-key operation failed: ${err.message}`);
5434
+ return 1;
5435
+ }
5436
+ function formatKeyStatus(k) {
5437
+ if (k.revoked_at) return dim("[revoked]");
5438
+ if (k.expires_at && new Date(k.expires_at).getTime() <= Date.now()) {
5439
+ return dim("[expired]");
5440
+ }
5441
+ return dim("[active]");
5442
+ }
5443
+ function formatRelative(d) {
5444
+ const deltaMs = Date.now() - d.getTime();
5445
+ const sec = Math.floor(deltaMs / 1e3);
5446
+ if (sec < 60) return `${sec}s ago`;
5447
+ const min = Math.floor(sec / 60);
5448
+ if (min < 60) return `${min}m ago`;
5449
+ const hr = Math.floor(min / 60);
5450
+ if (hr < 24) return `${hr}h ago`;
5451
+ const day = Math.floor(hr / 24);
5452
+ return `${day}d ago`;
5453
+ }
5454
+
5455
+ // src/commands/env-export.ts
5456
+ init_api_client();
5457
+ init_config();
5458
+ init_manifest_io();
5459
+ init_output();
5460
+ var PLACEHOLDER = "<run-`dashwise-api-keys-create-<name>`-and-paste-here>";
5461
+ async function envExportCommand(options) {
5462
+ const format = options.format ?? "env";
5463
+ if (!["env", "json", "railway", "vercel", "fly"].includes(format)) {
5464
+ fail(
5465
+ `Unknown --format "${format}". Allowed: env | json | railway | vercel | fly.`
5466
+ );
5467
+ return 1;
5468
+ }
5469
+ if (options.newKey && options.key) {
5470
+ fail(
5471
+ `Pass either --new-key (mint fresh) or --key (paste existing). Not both.`
5472
+ );
5473
+ return 1;
5474
+ }
5475
+ let manifest;
5476
+ try {
5477
+ manifest = readManifestOrThrow(options.dir);
5478
+ } catch (err) {
5479
+ fail(err.message);
5480
+ return 1;
5481
+ }
5482
+ const slug = manifest.module?.slug;
5483
+ if (!slug) {
5484
+ fail("module.json has no module.slug \u2014 the manifest is malformed.");
5485
+ return 1;
5486
+ }
5487
+ const cfg = readConfig();
5488
+ if (!cfg) {
5489
+ fail("Not logged in. Run `dashwise login` first.");
5490
+ return 1;
5491
+ }
5492
+ const entry = readModuleEntry(slug);
5493
+ if (!entry) {
5494
+ fail(
5495
+ `No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
5496
+ );
5497
+ return 1;
5498
+ }
5499
+ const apiUrl = resolveApiUrl();
5500
+ const installationId = entry.installationId;
5501
+ let apiKey;
5502
+ let apiKeyComment = null;
5503
+ if (options.key !== void 0) {
5504
+ if (!options.key.startsWith("dwk_")) {
5505
+ fail(`--key value must start with \`dwk_\` (got "${options.key.slice(0, 8)}\u2026").`);
5506
+ return 1;
5507
+ }
5508
+ apiKey = options.key;
5509
+ apiKeyComment = "# DASHWISE_API_KEY supplied via --key";
5510
+ } else if (options.newKey !== void 0) {
5511
+ try {
5512
+ const resp = await createApiKey(
5513
+ installationId,
5514
+ { name: options.newKey },
5515
+ {
5516
+ baseUrl: apiUrl,
5517
+ auth: { token: cfg.token }
5518
+ }
5519
+ );
5520
+ apiKey = resp.raw_key;
5521
+ apiKeyComment = `# DASHWISE_API_KEY minted by --new-key (id=${resp.api_key.id}, name="${resp.api_key.name}")`;
5522
+ process.stderr.write(
5523
+ `${dim("[env export]")} Minted API key #${resp.api_key.id} (${resp.api_key.name}). The plaintext below is the ONLY time it's shown \u2014 save the file or paste it now.
5524
+ `
5525
+ );
5526
+ } catch (err) {
5527
+ if (err instanceof ApiError) {
5528
+ fail(`Failed to mint API key: ${err.code} ${err.status} \u2014 ${err.message}`);
5529
+ } else {
5530
+ fail(`Failed to mint API key: ${err.message}`);
5531
+ }
5532
+ return 1;
5533
+ }
5534
+ } else {
5535
+ apiKey = PLACEHOLDER;
5536
+ apiKeyComment = "# DASHWISE_API_KEY \u2014 run `dashwise api-keys create <name>` to mint one + paste here.";
5537
+ }
5538
+ const entries = [
5539
+ {
5540
+ key: "DASHWISE_API_URL",
5541
+ value: apiUrl,
5542
+ comment: "# DashWise backend URL \u2014 same value the CLI is logged into."
5543
+ },
5544
+ {
5545
+ key: "NEXT_PUBLIC_DASHWISE_API_URL",
5546
+ value: apiUrl,
5547
+ comment: "# Client-side mirror \u2014 Next.js inlines NEXT_PUBLIC_* into the browser bundle so <AuthProvider> hits the right origin."
5548
+ },
5549
+ {
5550
+ key: "DASHWISE_INSTALLATION_ID",
5551
+ value: String(installationId),
5552
+ comment: `# Per-workspace install id for module "${slug}".`
5553
+ },
5554
+ {
5555
+ key: "DASHWISE_API_KEY",
5556
+ value: apiKey,
5557
+ ...apiKeyComment !== null ? { comment: apiKeyComment } : {}
5558
+ }
5559
+ ];
5560
+ process.stdout.write(render(entries, format));
5561
+ if (format !== "json") process.stdout.write("\n");
5562
+ if (apiKey === PLACEHOLDER) {
5563
+ info("");
5564
+ info(
5565
+ `Next: ${code(`dashwise api-keys create <name>`)} \u2192 paste the plaintext into ${code("DASHWISE_API_KEY")}.`
5566
+ );
5567
+ } else if (options.newKey !== void 0) {
5568
+ info("");
5569
+ warn(
5570
+ "API key plaintext is shown ONLY once. Save the output of this command now."
5571
+ );
5572
+ }
5573
+ return 0;
5574
+ }
5575
+ function render(entries, format) {
5576
+ switch (format) {
5577
+ case "env": {
5578
+ const lines = [];
5579
+ for (const e of entries) {
5580
+ if (e.comment) lines.push(e.comment);
5581
+ lines.push(`${e.key}=${e.value}`);
5582
+ lines.push("");
5583
+ }
5584
+ return lines.join("\n").trimEnd();
5585
+ }
5586
+ case "json": {
5587
+ const obj = {};
5588
+ for (const e of entries) obj[e.key] = e.value;
5589
+ return JSON.stringify(obj, null, 2);
5590
+ }
5591
+ case "railway": {
5592
+ const parts = entries.map((e) => `${e.key}='${e.value.replace(/'/g, "'\\''")}'`);
5593
+ return `railway variables set ${parts.join(" ")}`;
5594
+ }
5595
+ case "vercel": {
5596
+ const lines = entries.map(
5597
+ (e) => `echo ${shellQuote(e.value)} | vercel env add ${e.key} production`
5598
+ );
5599
+ return lines.join("\n");
5600
+ }
5601
+ case "fly": {
5602
+ const parts = entries.map((e) => `${e.key}=${shellQuote(e.value)}`);
5603
+ return `fly secrets set ${parts.join(" ")}`;
5604
+ }
5605
+ }
5606
+ }
5607
+ function shellQuote(value) {
5608
+ return `'${value.replace(/'/g, "'\\''")}'`;
5609
+ }
5610
+
5033
5611
  // src/commands/query.ts
5034
5612
  init_manifest_io();
5035
5613
  init_output();
@@ -5635,7 +6213,14 @@ program.command("dev").description(
5635
6213
  ).option(
5636
6214
  "--api-url <url>",
5637
6215
  "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(
6216
+ ).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(
6217
+ "--no-remote-poll",
6218
+ "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)."
6219
+ ).option(
6220
+ "--remote-poll-interval <seconds>",
6221
+ "Backend manifest poll interval, in seconds. Default: 5. Minimum: 1.",
6222
+ parseIntOption
6223
+ ).action(
5639
6224
  async (cmdOpts) => {
5640
6225
  process.exitCode = await devCommand({
5641
6226
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
@@ -5645,7 +6230,11 @@ program.command("dev").description(
5645
6230
  ...cmdOpts.apiUrl !== void 0 ? { apiUrl: cmdOpts.apiUrl } : {},
5646
6231
  ...cmdOpts.port !== void 0 ? { port: cmdOpts.port } : {},
5647
6232
  ...cmdOpts.sdkOnly !== void 0 ? { sdkOnly: cmdOpts.sdkOnly } : {},
5648
- ...cmdOpts.nextOnly !== void 0 ? { nextOnly: cmdOpts.nextOnly } : {}
6233
+ ...cmdOpts.nextOnly !== void 0 ? { nextOnly: cmdOpts.nextOnly } : {},
6234
+ // Commander's --no-foo flag maps to `foo: false`. devCommand's
6235
+ // option is `noRemotePoll: boolean` — invert to map cleanly.
6236
+ ...cmdOpts.remotePoll === false ? { noRemotePoll: true } : {},
6237
+ ...cmdOpts.remotePollInterval !== void 0 ? { remotePollIntervalSec: cmdOpts.remotePollInterval } : {}
5649
6238
  });
5650
6239
  }
5651
6240
  );
@@ -5797,6 +6386,80 @@ program.command("query").description(
5797
6386
  });
5798
6387
  }
5799
6388
  );
6389
+ program.command("pull").description(
6390
+ "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."
6391
+ ).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) => {
6392
+ process.exitCode = await pullCommand({
6393
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
6394
+ ...cmdOpts.dryRun !== void 0 ? { dryRun: cmdOpts.dryRun } : {}
6395
+ });
6396
+ });
6397
+ var apiKeysCmd = program.command("api-keys").description(
6398
+ "Manage per-installation API keys (`dwk_*`). Pasted into a deployed module's env for production data-plane auth."
6399
+ );
6400
+ apiKeysCmd.command("list").description(
6401
+ "List API keys for the current module's installation. Active only by default; pass --all for revoked too."
6402
+ ).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) => {
6403
+ process.exitCode = await apiKeysListCommand({
6404
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
6405
+ ...cmdOpts.all !== void 0 ? { all: cmdOpts.all } : {},
6406
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
6407
+ });
6408
+ });
6409
+ apiKeysCmd.command("create [name]").description(
6410
+ "Mint a new API key. The plaintext key is displayed ONCE \u2014 save it then."
6411
+ ).option("--dir <path>", "Project root (default: current directory).").option("--name <name>", "Override the positional name arg (e.g. for scripted use).").option(
6412
+ "--scopes <csv>",
6413
+ "Comma-separated scope strings (e.g. `runtime:read,runtime:write`). Default: full install access."
6414
+ ).option(
6415
+ "--expires-at <iso>",
6416
+ "ISO-8601 expiry timestamp (e.g. 2026-12-31T23:59:59Z). Default: never expires."
6417
+ ).option(
6418
+ "--raw-only",
6419
+ "Print only the plaintext key on stdout; chatter goes to stderr. Useful for shell capture: `KEY=$(dashwise api-keys create prod --raw-only)`"
6420
+ ).action(
6421
+ async (nameArg, cmdOpts) => {
6422
+ process.exitCode = await apiKeysCreateCommand(nameArg, {
6423
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
6424
+ ...cmdOpts.name !== void 0 ? { name: cmdOpts.name } : {},
6425
+ ...cmdOpts.scopes !== void 0 ? { scopes: cmdOpts.scopes } : {},
6426
+ ...cmdOpts.expiresAt !== void 0 ? { expiresAt: cmdOpts.expiresAt } : {},
6427
+ ...cmdOpts.rawOnly !== void 0 ? { rawOnly: cmdOpts.rawOnly } : {}
6428
+ });
6429
+ }
6430
+ );
6431
+ apiKeysCmd.command("revoke [key-id]").description(
6432
+ "Soft-revoke an API key. Backend keeps the row for audit; further requests return 401."
6433
+ ).option("--dir <path>", "Project root (default: current directory).").option("-y, --yes", "Skip the per-key confirmation message (for CI / scripts).").action(
6434
+ async (keyIdArg, cmdOpts) => {
6435
+ process.exitCode = await apiKeysRevokeCommand(keyIdArg, {
6436
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
6437
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
6438
+ });
6439
+ }
6440
+ );
6441
+ var envCmd = program.command("env").description("Environment-variable helpers for deploying a module.");
6442
+ envCmd.command("export").description(
6443
+ "Emit the env vars a deployed module needs to authenticate against DashWise. By default emits .env-style KEY=value lines; --format = env | json | railway | vercel | fly to wrap in platform-native syntax. Use --new-key <name> to mint + inline an API key in one step."
6444
+ ).option("--dir <path>", "Project root (default: current directory).").option(
6445
+ "--format <fmt>",
6446
+ "Output format: env | json | railway | vercel | fly (default: env)."
6447
+ ).option(
6448
+ "--new-key <name>",
6449
+ "Mint a fresh API key with this name and inline the plaintext. (Mutually exclusive with --key.)"
6450
+ ).option(
6451
+ "--key <dwk_value>",
6452
+ "Use a previously-minted API key (paste plaintext). (Mutually exclusive with --new-key.)"
6453
+ ).action(
6454
+ async (cmdOpts) => {
6455
+ process.exitCode = await envExportCommand({
6456
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
6457
+ ...cmdOpts.format !== void 0 ? { format: cmdOpts.format } : {},
6458
+ ...cmdOpts.newKey !== void 0 ? { newKey: cmdOpts.newKey } : {},
6459
+ ...cmdOpts.key !== void 0 ? { key: cmdOpts.key } : {}
6460
+ });
6461
+ }
6462
+ );
5800
6463
  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
6464
  "--exclude <pattern...>",
5802
6465
  "Extra exclude patterns (segment-matched) appended to the defaults."
@@ -5988,7 +6651,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
5988
6651
  }
5989
6652
  })();
5990
6653
  function getVersion() {
5991
- return "0.3.2";
6654
+ return "0.5.0";
5992
6655
  }
5993
6656
  function parseIntOption(value) {
5994
6657
  const n = Number.parseInt(value, 10);