@dashai/cli 0.4.0 → 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",
@@ -2912,7 +2936,7 @@ async function devCommand(options) {
2912
2936
  pipeWithPrefix(child.stderr, process.stderr, prefix);
2913
2937
  }
2914
2938
  let manifestPoll = null;
2915
- const enablePoll = !options.noRemotePoll && moduleEntry !== void 0 && manifestSlug !== void 0;
2939
+ const enablePoll = !options.noRemotePoll && moduleEntry !== null && manifestSlug !== void 0;
2916
2940
  if (enablePoll) {
2917
2941
  const intervalSec = Math.max(
2918
2942
  MIN_REMOTE_POLL_INTERVAL_SEC,
@@ -5219,6 +5243,371 @@ function surfacePullFailure(err) {
5219
5243
  return 1;
5220
5244
  }
5221
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
+
5222
5611
  // src/commands/query.ts
5223
5612
  init_manifest_io();
5224
5613
  init_output();
@@ -6005,6 +6394,72 @@ program.command("pull").description(
6005
6394
  ...cmdOpts.dryRun !== void 0 ? { dryRun: cmdOpts.dryRun } : {}
6006
6395
  });
6007
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
+ );
6008
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(
6009
6464
  "--exclude <pattern...>",
6010
6465
  "Extra exclude patterns (segment-matched) appended to the defaults."
@@ -6196,7 +6651,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
6196
6651
  }
6197
6652
  })();
6198
6653
  function getVersion() {
6199
- return "0.4.0";
6654
+ return "0.5.0";
6200
6655
  }
6201
6656
  function parseIntOption(value) {
6202
6657
  const n = Number.parseInt(value, 10);