@dashai/cli 0.31.1 → 0.32.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
@@ -548,7 +548,7 @@ async function introspectWorkspaceTable(_installationId, tableNameOrId, options
548
548
  const message = `"${ref}" is not a numeric table id. Pass the workspace table's numeric id (find it in the Dashwise UI URL, or via \`GET /api/tables/application/:applicationId\`). Resolving a table by name is not yet supported by this command.`;
549
549
  throw new ApiError(0, { code: "NON_NUMERIC_TABLE_REF", message }, message);
550
550
  }
551
- const table = await apiRequest(
551
+ const table2 = await apiRequest(
552
552
  "GET",
553
553
  `/api/tables/${asNumber}`,
554
554
  void 0,
@@ -568,9 +568,9 @@ async function introspectWorkspaceTable(_installationId, tableNameOrId, options
568
568
  ...f.is_required !== void 0 ? { required: f.is_required } : {}
569
569
  }));
570
570
  return {
571
- table_id: table.id,
572
- name: table.name,
573
- slug: slugifyFieldName(table.name),
571
+ table_id: table2.id,
572
+ name: table2.name,
573
+ slug: slugifyFieldName(table2.name),
574
574
  fields
575
575
  };
576
576
  }
@@ -639,6 +639,66 @@ async function getRuntimeLogs(installationId, opts = {}, options = {}) {
639
639
  options
640
640
  );
641
641
  }
642
+ async function getModuleSchedules(installationId, options = {}) {
643
+ return apiRequest(
644
+ "GET",
645
+ `/api/modules/installations/${installationId}/schedules`,
646
+ void 0,
647
+ options
648
+ );
649
+ }
650
+ async function listJobRuns(installationId, opts = {}, options = {}) {
651
+ const params = new URLSearchParams();
652
+ if (opts.kind) params.set("kind", opts.kind);
653
+ if (opts.key) params.set("key", opts.key);
654
+ if (opts.cursor !== void 0) params.set("cursor", String(opts.cursor));
655
+ if (opts.limit !== void 0) params.set("limit", String(opts.limit));
656
+ const qs = params.toString();
657
+ return apiRequest(
658
+ "GET",
659
+ `/api/modules/installations/${installationId}/job-runs${qs ? `?${qs}` : ""}`,
660
+ void 0,
661
+ options
662
+ );
663
+ }
664
+ async function runModuleSchedule(installationId, key, options = {}) {
665
+ return apiRequest(
666
+ "POST",
667
+ `/api/modules/installations/${installationId}/schedules/${encodeURIComponent(key)}/run`,
668
+ {},
669
+ options
670
+ );
671
+ }
672
+ async function getModuleWebhooks(installationId, options = {}) {
673
+ return apiRequest(
674
+ "GET",
675
+ `/api/modules/installations/${installationId}/webhooks`,
676
+ void 0,
677
+ options
678
+ );
679
+ }
680
+ async function listWebhookDeliveries(installationId, opts = {}, options = {}) {
681
+ const params = new URLSearchParams();
682
+ if (opts.key) params.set("key", opts.key);
683
+ if (opts.status) params.set("status", opts.status);
684
+ if (opts.cursor !== void 0) params.set("cursor", String(opts.cursor));
685
+ if (opts.limit !== void 0) params.set("limit", String(opts.limit));
686
+ const qs = params.toString();
687
+ return apiRequest(
688
+ "GET",
689
+ `/api/modules/installations/${installationId}/webhook-deliveries${qs ? `?${qs}` : ""}`,
690
+ void 0,
691
+ options
692
+ );
693
+ }
694
+ async function redeliverWebhookDelivery(installationId, deliveryId, options = {}) {
695
+ return apiRequest(
696
+ "POST",
697
+ `/api/modules/installations/${installationId}/webhook-deliveries/${deliveryId}/redeliver`,
698
+ {},
699
+ options
700
+ );
701
+ }
642
702
  async function getInstallationRuntime(installationId, options = {}) {
643
703
  return apiRequest(
644
704
  "GET",
@@ -1017,9 +1077,9 @@ function mergeRemoteManifest(local, remote) {
1017
1077
  }
1018
1078
  return { merged, preserved };
1019
1079
  }
1020
- function fieldSlugs(table) {
1080
+ function fieldSlugs(table2) {
1021
1081
  const set = /* @__PURE__ */ new Set();
1022
- const fields = table?.fields;
1082
+ const fields = table2?.fields;
1023
1083
  if (!Array.isArray(fields)) return set;
1024
1084
  for (const f of fields) {
1025
1085
  if (f && typeof f === "object" && typeof f.slug === "string") {
@@ -14512,17 +14572,17 @@ function referenceFieldWarnings(manifest) {
14512
14572
  const tables = manifest?.owns?.tables;
14513
14573
  if (!Array.isArray(tables)) return [];
14514
14574
  const warnings = [];
14515
- for (const table of tables) {
14516
- if (typeof table?.slug !== "string" || !Array.isArray(table.fields)) continue;
14517
- for (const field of table.fields) {
14575
+ for (const table2 of tables) {
14576
+ if (typeof table2?.slug !== "string" || !Array.isArray(table2.fields)) continue;
14577
+ for (const field of table2.fields) {
14518
14578
  if (typeof field?.slug !== "string" || typeof field.type !== "string") continue;
14519
14579
  if (field.slug === "id" || !REFERENCE_NAME.test(field.slug)) continue;
14520
14580
  if (!PLAIN_TYPES.has(field.type)) continue;
14521
14581
  warnings.push({
14522
- table: table.slug,
14582
+ table: table2.slug,
14523
14583
  field: field.slug,
14524
14584
  type: field.type,
14525
- message: `${table.slug}.${field.slug} is a ${field.type} field named like a reference. If it points at a row in Dashwise, use a link_row field: plain ids break silently when data is copied to another installation. Ignore this for ids from outside systems.`
14585
+ message: `${table2.slug}.${field.slug} is a ${field.type} field named like a reference. If it points at a row in Dashwise, use a link_row field: plain ids break silently when data is copied to another installation. Ignore this for ids from outside systems.`
14526
14586
  });
14527
14587
  }
14528
14588
  }
@@ -15156,9 +15216,9 @@ function printApplySummary(applied) {
15156
15216
  const message = strProp(warning, "message");
15157
15217
  if (!message) continue;
15158
15218
  const warningCode = strProp(warning, "code");
15159
- const table = strProp(warning, "table");
15219
+ const table2 = strProp(warning, "table");
15160
15220
  const field = strProp(warning, "field");
15161
- const location = table ? `${table}${field ? `.${field}` : ""}` : field;
15221
+ const location = table2 ? `${table2}${field ? `.${field}` : ""}` : field;
15162
15222
  const prefix = [warningCode, location].filter(Boolean).join(" ");
15163
15223
  warn(`${prefix ? `${code(prefix)}: ` : ""}${message}`);
15164
15224
  }
@@ -15277,8 +15337,8 @@ var init_push = __esm({
15277
15337
  });
15278
15338
 
15279
15339
  // ../sdk/dist/index.js
15280
- function primaryDeclarationIssues(table) {
15281
- const invalid = table.fields.findIndex(
15340
+ function primaryDeclarationIssues(table2) {
15341
+ const invalid = table2.fields.findIndex(
15282
15342
  (field2) => field2.primary !== void 0 && field2.primary !== true
15283
15343
  );
15284
15344
  if (invalid !== -1) {
@@ -15286,17 +15346,17 @@ function primaryDeclarationIssues(table) {
15286
15346
  { path: ["fields", invalid, "primary"], message: "primary must be literal true or omitted" }
15287
15347
  ];
15288
15348
  }
15289
- const marked = table.fields.filter((field2) => field2.primary === true);
15349
+ const marked = table2.fields.filter((field2) => field2.primary === true);
15290
15350
  if (marked.length !== 1) {
15291
15351
  return [
15292
15352
  {
15293
15353
  path: ["fields"],
15294
- message: `Table ${table.slug}: mark exactly one field as primary: true (the row's label in links, pickers, titles)`
15354
+ message: `Table ${table2.slug}: mark exactly one field as primary: true (the row's label in links, pickers, titles)`
15295
15355
  }
15296
15356
  ];
15297
15357
  }
15298
15358
  const field = marked[0];
15299
- const path = ["fields", table.fields.indexOf(field), "primary"];
15359
+ const path = ["fields", table2.fields.indexOf(field), "primary"];
15300
15360
  if (field.type === "formula") {
15301
15361
  return [{ path, message: "formula fields cannot be the primary of a module table yet" }];
15302
15362
  }
@@ -15304,14 +15364,14 @@ function primaryDeclarationIssues(table) {
15304
15364
  return [
15305
15365
  {
15306
15366
  path,
15307
- message: `Table ${table.slug}: mark a field that can be the primary with primary: true (${field.slug} of type ${field.type} cannot be primary)`
15367
+ message: `Table ${table2.slug}: mark a field that can be the primary with primary: true (${field.slug} of type ${field.type} cannot be primary)`
15308
15368
  }
15309
15369
  ];
15310
15370
  }
15311
15371
  return [];
15312
15372
  }
15313
- function weakPrimaryWarning(table) {
15314
- const field = table.fields.find((field2) => field2.primary === true);
15373
+ function weakPrimaryWarning(table2) {
15374
+ const field = table2.fields.find((field2) => field2.primary === true);
15315
15375
  if (!field || !WEAK_PRIMARY_TYPES.has(field.type)) return void 0;
15316
15376
  return `${field.slug} is a weak row label; prefer a short text field such as a name or title`;
15317
15377
  }
@@ -15529,14 +15589,14 @@ async function tableCreateCommand(slug, options) {
15529
15589
  }
15530
15590
  seen.add(f.slug);
15531
15591
  }
15532
- const table = { slug, fields };
15533
- const issues = primaryDeclarationIssues(table);
15592
+ const table2 = { slug, fields };
15593
+ const issues = primaryDeclarationIssues(table2);
15534
15594
  if (issues.length > 0) {
15535
15595
  for (const issue of issues) fail(issue.message);
15536
15596
  info("Mark the row name explicitly, e.g. --field title:text:primary.");
15537
15597
  return 1;
15538
15598
  }
15539
- const primaryWarning = weakPrimaryWarning(table);
15599
+ const primaryWarning = weakPrimaryWarning(table2);
15540
15600
  if (primaryWarning) warn(primaryWarning);
15541
15601
  let ctx;
15542
15602
  try {
@@ -23361,13 +23421,13 @@ async function buildFieldMap(ctx, candidate, overrides) {
23361
23421
  if (overrideKeys.length === 0) {
23362
23422
  return Object.keys(candidate.auto_field_map).length > 0 ? { ...candidate.auto_field_map } : void 0;
23363
23423
  }
23364
- const table = await introspectWorkspaceTable(
23424
+ const table2 = await introspectWorkspaceTable(
23365
23425
  ctx.installationId,
23366
23426
  String(candidate.table_id),
23367
23427
  ctx.requestOptions
23368
23428
  );
23369
23429
  const byName = /* @__PURE__ */ new Map();
23370
- for (const f of table.fields) {
23430
+ for (const f of table2.fields) {
23371
23431
  byName.set(f.slug, f.id);
23372
23432
  if (f.name) byName.set(f.name, f.id);
23373
23433
  }
@@ -23375,9 +23435,9 @@ async function buildFieldMap(ctx, candidate, overrides) {
23375
23435
  for (const [usesField, physicalName] of Object.entries(overrides)) {
23376
23436
  const id = byName.get(physicalName);
23377
23437
  if (id === void 0) {
23378
- const available = table.fields.map((f) => f.name && f.name !== f.slug ? `${f.slug} (${f.name})` : f.slug).join(", ");
23438
+ const available = table2.fields.map((f) => f.name && f.name !== f.slug ? `${f.slug} (${f.name})` : f.slug).join(", ");
23379
23439
  throw new Error(
23380
- `--map ${usesField}=${physicalName}: table "${table.name}" has no field named "${physicalName}". Available: ${available}.`
23440
+ `--map ${usesField}=${physicalName}: table "${table2.name}" has no field named "${physicalName}". Available: ${available}.`
23381
23441
  );
23382
23442
  }
23383
23443
  merged[usesField] = id;
@@ -23616,9 +23676,9 @@ async function usesAddCommand(options) {
23616
23676
  }
23617
23677
  const scopeExit = await assertTableInInstallWorkspace(ctx, options.fromTable);
23618
23678
  if (scopeExit !== 0) return scopeExit;
23619
- let table;
23679
+ let table2;
23620
23680
  try {
23621
- table = await introspectWorkspaceTable(
23681
+ table2 = await introspectWorkspaceTable(
23622
23682
  ctx.installationId,
23623
23683
  options.fromTable,
23624
23684
  ctx.requestOptions
@@ -23626,27 +23686,27 @@ async function usesAddCommand(options) {
23626
23686
  } catch (err) {
23627
23687
  return surfaceIntrospectError(err, options.fromTable);
23628
23688
  }
23629
- const slotSlug = options.slug ?? slugifyUsesName(table.name);
23689
+ const slotSlug = options.slug ?? slugifyUsesName(table2.name);
23630
23690
  if (!isUsesSlug(slotSlug)) {
23631
23691
  fail(
23632
23692
  `Slot slug ${code(slotSlug || "(empty)")} doesn't match ${code(USES_SLUG_RULE)}. Pass ${code("--slug <snake_case_slug>")} explicitly.`
23633
23693
  );
23634
23694
  return 1;
23635
23695
  }
23636
- const { fields, skipped } = generateFields(table.fields);
23696
+ const { fields, skipped } = generateFields(table2.fields);
23637
23697
  if (fields.length === 0) {
23638
23698
  fail(
23639
- `Table ${code(table.name)} has no fields representable as a v1 ${code("uses")} contract (all are link_row / formula / ai / file). Nothing to declare.`
23699
+ `Table ${code(table2.name)} has no fields representable as a v1 ${code("uses")} contract (all are link_row / formula / ai / file). Nothing to declare.`
23640
23700
  );
23641
23701
  return 1;
23642
23702
  }
23643
23703
  const badFields = fields.filter((f) => !isUsesSlug(f.slug));
23644
23704
  if (badFields.length > 0) {
23645
23705
  fail(
23646
- `${badFields.length} field name(s) on ${code(table.name)} don't fold into a legal ${code("uses")} field slug (${code(USES_SLUG_RULE)}):`
23706
+ `${badFields.length} field name(s) on ${code(table2.name)} don't fold into a legal ${code("uses")} field slug (${code(USES_SLUG_RULE)}):`
23647
23707
  );
23648
23708
  for (const f of badFields) {
23649
- const physical = table.fields.find((p) => p.slug === f.slug);
23709
+ const physical = table2.fields.find((p) => p.slug === f.slug);
23650
23710
  const name = physical?.name ?? f.slug;
23651
23711
  out(` ${dim("-")} ${code(name)} \u2192 ${code(f.slug || "(empty)")}`);
23652
23712
  }
@@ -23660,10 +23720,10 @@ async function usesAddCommand(options) {
23660
23720
  const collidingSlugs = [...slugCounts.entries()].filter(([, n]) => n > 1).map(([slug]) => slug);
23661
23721
  if (collidingSlugs.length > 0) {
23662
23722
  fail(
23663
- `${collidingSlugs.length} generated ${code("uses")} field slug(s) on ${code(table.name)} collide \u2014 the backend refuses a contract with a duplicate field slug:`
23723
+ `${collidingSlugs.length} generated ${code("uses")} field slug(s) on ${code(table2.name)} collide \u2014 the backend refuses a contract with a duplicate field slug:`
23664
23724
  );
23665
23725
  for (const slug of collidingSlugs) {
23666
- const names = table.fields.filter((p) => p.slug === slug && abstractTypeForPhysical(p.type) !== null).map((p) => p.name ?? p.slug);
23726
+ const names = table2.fields.filter((p) => p.slug === slug && abstractTypeForPhysical(p.type) !== null).map((p) => p.name ?? p.slug);
23667
23727
  out(` ${dim("-")} ${names.map((n) => code(n)).join(" + ")} \u2192 ${code(slug)}`);
23668
23728
  }
23669
23729
  info(
@@ -23679,7 +23739,7 @@ async function usesAddCommand(options) {
23679
23739
  };
23680
23740
  if (options.purpose) slot.purpose = options.purpose;
23681
23741
  info(
23682
- `Generated a ${code("uses")} contract ${code(slotSlug)} from table ${code(table.name)} ${dim(`(id ${table.table_id})`)}:`
23742
+ `Generated a ${code("uses")} contract ${code(slotSlug)} from table ${code(table2.name)} ${dim(`(id ${table2.table_id})`)}:`
23683
23743
  );
23684
23744
  out(` ${dim("ops:")} ${ops.join(", ")}`);
23685
23745
  for (const f of fields) {
@@ -23725,24 +23785,24 @@ async function usesAddCommand(options) {
23725
23785
  );
23726
23786
  if (!pushed.ok) {
23727
23787
  info(
23728
- `The manifest edit is saved locally. Push it (${code("dashwise dev")} watches + applies, or re-run once the backend is reachable), then ${code(`dashwise bind ${slotSlug} --table ${table.table_id}`)}.`
23788
+ `The manifest edit is saved locally. Push it (${code("dashwise dev")} watches + applies, or re-run once the backend is reachable), then ${code(`dashwise bind ${slotSlug} --table ${table2.table_id}`)}.`
23729
23789
  );
23730
23790
  return 1;
23731
23791
  }
23732
23792
  if (options.noBind) {
23733
23793
  info(
23734
- `Skipped auto-bind (${code("--no-bind")}). Connect it when ready: ${code(`dashwise bind ${slotSlug} --table ${table.table_id}`)}.`
23794
+ `Skipped auto-bind (${code("--no-bind")}). Connect it when ready: ${code(`dashwise bind ${slotSlug} --table ${table2.table_id}`)}.`
23735
23795
  );
23736
23796
  return 0;
23737
23797
  }
23738
- info(`Binding ${code(slotSlug)} to ${code(table.name)}\u2026`);
23798
+ info(`Binding ${code(slotSlug)} to ${code(table2.name)}\u2026`);
23739
23799
  const bindExit = await bindCommand(slotSlug, {
23740
23800
  ...ctx.dir !== void 0 ? { dir: ctx.dir } : {},
23741
- table: String(table.table_id)
23801
+ table: String(table2.table_id)
23742
23802
  });
23743
23803
  if (bindExit !== 0) {
23744
23804
  warn(
23745
- `The ${code("uses")} slot was declared + pushed, but the auto-bind didn't complete. Retry with ${code(`dashwise bind ${slotSlug} --table ${table.table_id}`)}.`
23805
+ `The ${code("uses")} slot was declared + pushed, but the auto-bind didn't complete. Retry with ${code(`dashwise bind ${slotSlug} --table ${table2.table_id}`)}.`
23746
23806
  );
23747
23807
  return bindExit;
23748
23808
  }
@@ -25889,7 +25949,10 @@ async function moduleLogsCommand(slugArg, options) {
25889
25949
  }
25890
25950
  return runOnce3(installationId, label, options, since, auth);
25891
25951
  }
25892
- async function resolveInstallationFromSlug(slugArg, options, cfg, auth) {
25952
+ async function resolveInstallationFromSlug(slugArg, options, cfg, auth, wording = {
25953
+ command: "module logs",
25954
+ purpose: "read logs from"
25955
+ }) {
25893
25956
  let slug = slugArg;
25894
25957
  if (!slug) {
25895
25958
  try {
@@ -25902,7 +25965,7 @@ async function resolveInstallationFromSlug(slugArg, options, cfg, auth) {
25902
25965
  }
25903
25966
  if (!slug) {
25904
25967
  fail(
25905
- `No module slug \u2014 pass one (${code("dashwise module logs <slug>")}), run from a directory with a valid module.json, or target an install directly with ${code("--install <id>")}.`
25968
+ `No module slug \u2014 pass one (${code(`dashwise ${wording.command} <slug>`)}), run from a directory with a valid module.json, or target an install directly with ${code("--install <id>")}.`
25906
25969
  );
25907
25970
  return 1;
25908
25971
  }
@@ -25944,7 +26007,7 @@ async function resolveInstallationFromSlug(slugArg, options, cfg, auth) {
25944
26007
  }
25945
26008
  if (!row) {
25946
26009
  info(
25947
- `${code(modSlug)} is not installed in workspace ${code(workspace2.slug)} \u2014 nothing to read logs from. Deploy it with ${code(`dashwise module install ${modSlug} --workspace ${workspace2.slug}`)}.`
26010
+ `${code(modSlug)} is not installed in workspace ${code(workspace2.slug)} \u2014 nothing to ${wording.purpose}. Deploy it with ${code(`dashwise module install ${modSlug} --workspace ${workspace2.slug}`)}.`
25948
26011
  );
25949
26012
  return 0;
25950
26013
  }
@@ -26137,6 +26200,432 @@ async function sleep3(ms, aborted) {
26137
26200
  }
26138
26201
  }
26139
26202
 
26203
+ // src/commands/jobs.ts
26204
+ init_api_client();
26205
+ init_config();
26206
+ init_output();
26207
+ var KINDS = ["schedule", "webhook", "manual", "migration", "replay"];
26208
+ var CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
26209
+ function safeExcerpt(text4) {
26210
+ return text4 === null ? null : text4.replace(CONTROL, " ");
26211
+ }
26212
+ function safeRun(run) {
26213
+ return run ? { ...run, error_excerpt: safeExcerpt(run.error_excerpt) } : run;
26214
+ }
26215
+ var KEY = /^[a-z0-9][a-z0-9-]{0,39}$/;
26216
+ async function resolveJobsTarget(slugArg, options, command, purpose = "run jobs on") {
26217
+ if (!isLoggedIn()) {
26218
+ fail(`Not logged in. Run ${code("dashwise login")} first.`);
26219
+ return 1;
26220
+ }
26221
+ const cfg = readConfig();
26222
+ if (!cfg) {
26223
+ fail(`CLI config missing. Run ${code("dashwise login")}.`);
26224
+ return 1;
26225
+ }
26226
+ const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
26227
+ if (options.install !== void 0) {
26228
+ if (!Number.isInteger(options.install) || options.install <= 0) {
26229
+ fail(`--install expects a positive integer installation id (got "${options.install}").`);
26230
+ return 1;
26231
+ }
26232
+ return { installationId: options.install, label: `install #${options.install}`, auth };
26233
+ }
26234
+ const resolved = await resolveInstallationFromSlug(slugArg, options, cfg, auth, {
26235
+ command,
26236
+ purpose
26237
+ });
26238
+ if (typeof resolved === "number") return resolved;
26239
+ return { ...resolved, auth };
26240
+ }
26241
+ async function jobsListCommand(slugArg, options) {
26242
+ const t = await resolveJobsTarget(slugArg, options, "jobs list");
26243
+ if (typeof t === "number") return t;
26244
+ let body;
26245
+ try {
26246
+ body = await getModuleSchedules(t.installationId, t.auth);
26247
+ } catch (err) {
26248
+ return mapJobsError(err, options.json, "read the schedules");
26249
+ }
26250
+ if (options.json) {
26251
+ out(
26252
+ JSON.stringify({
26253
+ ...body,
26254
+ schedules: body.schedules.map((s) => ({ ...s, last_run: safeRun(s.last_run) }))
26255
+ })
26256
+ );
26257
+ return 0;
26258
+ }
26259
+ if (body.schedules.length === 0) {
26260
+ info(`The installed version of ${t.label} declares no schedules.`);
26261
+ return 0;
26262
+ }
26263
+ if (!body.enabled) {
26264
+ warn("Schedules do not run on this platform yet (not enabled, or no job queue). Manual runs may still work.");
26265
+ } else if (!body.active) {
26266
+ warn("This installation does not run its schedules now (offline, held, without a runtime, or on a Workbench).");
26267
+ }
26268
+ const rows = body.schedules.map((s) => [
26269
+ s.key,
26270
+ s.interval ? `every ${s.interval}` : `${s.pattern} UTC`,
26271
+ s.route,
26272
+ s.next_run_at ? formatTs2(s.next_run_at) : "\u2014",
26273
+ s.last_run ? `${colorStatus(s.last_run.status)} ${dim(formatTs2(s.last_run.created_at))}` : dim("never"),
26274
+ s.running ? pc.yellow("running") : ""
26275
+ ]);
26276
+ table(["KEY", "SCHEDULE", "ROUTE", "NEXT RUN", "LAST RUN", ""], rows);
26277
+ out("");
26278
+ info(`${body.schedules.length} ${body.schedules.length === 1 ? "schedule" : "schedules"} (${t.label}).`);
26279
+ return 0;
26280
+ }
26281
+ async function jobsRunsCommand(slugArg, options) {
26282
+ if (options.kind !== void 0 && !KINDS.includes(options.kind)) {
26283
+ fail(`--kind must be one of ${KINDS.join(", ")} (got "${options.kind}").`);
26284
+ return 1;
26285
+ }
26286
+ if (options.key !== void 0 && !KEY.test(options.key)) {
26287
+ fail(`--key must be a schedule key like ${code("lease-sweep")} (got "${options.key}").`);
26288
+ return 1;
26289
+ }
26290
+ if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 200)) {
26291
+ fail(`--limit must be between 1 and 200 (got "${options.limit}").`);
26292
+ return 1;
26293
+ }
26294
+ const t = await resolveJobsTarget(slugArg, options, "jobs runs");
26295
+ if (typeof t === "number") return t;
26296
+ let body;
26297
+ try {
26298
+ body = await listJobRuns(
26299
+ t.installationId,
26300
+ {
26301
+ ...options.kind ? { kind: options.kind } : {},
26302
+ ...options.key ? { key: options.key } : {},
26303
+ ...options.before !== void 0 ? { cursor: options.before } : {},
26304
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
26305
+ },
26306
+ t.auth
26307
+ );
26308
+ } catch (err) {
26309
+ return mapJobsError(err, options.json, "read the run log");
26310
+ }
26311
+ if (options.json) {
26312
+ out(JSON.stringify({ ...body, runs: body.runs.map((r) => safeRun(r)) }));
26313
+ return 0;
26314
+ }
26315
+ if (body.runs.length === 0) {
26316
+ info(`No runs for ${t.label}${options.key ? ` (${options.key})` : ""}.`);
26317
+ return 0;
26318
+ }
26319
+ table(
26320
+ ["TIME", "KEY", "KIND", "TRY", "STATUS", "HTTP", "MS", "ERROR"],
26321
+ body.runs.map((r) => [
26322
+ formatTs2(r.created_at),
26323
+ r.key,
26324
+ r.kind,
26325
+ String(r.attempt),
26326
+ colorStatus(r.status),
26327
+ r.http_status === null ? "" : String(r.http_status),
26328
+ r.duration_ms === null ? "" : String(r.duration_ms),
26329
+ r.error_code ?? ""
26330
+ ])
26331
+ );
26332
+ out("");
26333
+ info(
26334
+ `${body.runs.length} ${body.runs.length === 1 ? "run" : "runs"} (${t.label}).` + (body.next_cursor !== null ? ` Older: ${code(`--before ${body.next_cursor}`)}.` : "")
26335
+ );
26336
+ return 0;
26337
+ }
26338
+ async function jobsRunCommand(key, slugArg, options) {
26339
+ if (!KEY.test(key)) {
26340
+ fail(`"${key}" is not a schedule key (expected ${KEY.source}).`);
26341
+ return 1;
26342
+ }
26343
+ const t = await resolveJobsTarget(slugArg, options, `jobs run ${key}`);
26344
+ if (typeof t === "number") return t;
26345
+ let body;
26346
+ try {
26347
+ body = await runModuleSchedule(t.installationId, key, t.auth);
26348
+ } catch (err) {
26349
+ return mapJobsError(err, options.json, `run ${key}`);
26350
+ }
26351
+ if (options.json) {
26352
+ out(JSON.stringify({ ...body, run: safeRun(body.run) }));
26353
+ return body.run && body.run.status !== "ok" ? 2 : 0;
26354
+ }
26355
+ if (body.queued) {
26356
+ success(`Queued a run of ${code(key)} on ${t.label} (invocation ${body.invocation_id}).`);
26357
+ info(`Follow it with ${code(`dashwise jobs runs --key ${key}`)}.`);
26358
+ return 0;
26359
+ }
26360
+ const run = body.run;
26361
+ if (!run) {
26362
+ warn(`The run of ${code(key)} left no log row (invocation ${body.invocation_id}).`);
26363
+ return 2;
26364
+ }
26365
+ const detail = [
26366
+ run.http_status !== null ? `HTTP ${run.http_status}` : null,
26367
+ run.duration_ms !== null ? `${run.duration_ms} ms` : null,
26368
+ run.error_code
26369
+ ].filter(Boolean).join(", ");
26370
+ if (run.status === "ok") {
26371
+ success(`Ran ${code(key)} on ${t.label}: ok${detail ? ` (${detail})` : ""}.`);
26372
+ return 0;
26373
+ }
26374
+ fail(`Ran ${code(key)} on ${t.label}: ${run.status}${detail ? ` (${detail})` : ""}.`);
26375
+ const excerpt = safeExcerpt(run.error_excerpt);
26376
+ if (excerpt) out(dim(excerpt));
26377
+ return 2;
26378
+ }
26379
+ function formatTs2(iso) {
26380
+ const d = new Date(iso);
26381
+ return Number.isNaN(d.getTime()) ? iso : d.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
26382
+ }
26383
+ function colorStatus(status) {
26384
+ switch (status) {
26385
+ case "ok":
26386
+ return pc.green(status);
26387
+ case "skipped":
26388
+ return pc.dim(status);
26389
+ default:
26390
+ return pc.red(status);
26391
+ }
26392
+ }
26393
+ function table(head, rows) {
26394
+ const visible = (s) => s.replace(/\u001b\[[0-9;]*m/g, "").length;
26395
+ const widths = head.map((h, i) => Math.max(visible(h), ...rows.map((r) => visible(r[i] ?? ""))));
26396
+ const line = (cells) => cells.map((c2, i) => c2 + " ".repeat(Math.max(0, widths[i] - visible(c2)))).join(" ").trimEnd();
26397
+ out(dim(line(head)));
26398
+ for (const r of rows) out(line(r));
26399
+ }
26400
+ function mapJobsError(err, json, what) {
26401
+ const emit = (c2, message2) => {
26402
+ if (json) out(JSON.stringify({ ok: false, error: { code: c2, message: message2 } }));
26403
+ else fail(message2);
26404
+ };
26405
+ if (err instanceof ApiError) {
26406
+ if (err.status === 401 || err.code === "UNAUTHENTICATED") {
26407
+ emit("NOT_LOGGED_IN", `Not logged in. Run ${code("dashwise login")} first.`);
26408
+ return 1;
26409
+ }
26410
+ if (err.code === "SCHEDULE_NOT_FOUND") {
26411
+ emit(err.code, `${err.message} See ${code("dashwise jobs list")}.`);
26412
+ return 1;
26413
+ }
26414
+ if (err.status === 404 || err.code === "INSTALLATION_NOT_FOUND") {
26415
+ emit(
26416
+ "INSTALLATION_NOT_FOUND",
26417
+ `Installation not found, or you may not open it. Check the id (${code("dashwise module status <slug>")}).`
26418
+ );
26419
+ return 1;
26420
+ }
26421
+ if (err.status === 403) {
26422
+ emit(
26423
+ err.code || "FORBIDDEN",
26424
+ `You may not ${what} on this installation: reading needs its logs permission, running needs its settings permission (an installation manager or a company admin).`
26425
+ );
26426
+ return 1;
26427
+ }
26428
+ if (err.code === "FEATURE_NOT_ENABLED" || err.code === "COPY_NOT_RUNNING_JOBS") {
26429
+ emit(err.code, err.message);
26430
+ return 1;
26431
+ }
26432
+ emit(err.code, `Could not ${what}: ${err.code} ${err.status} \u2014 ${err.message}`);
26433
+ return 1;
26434
+ }
26435
+ const message = `Could not ${what}: ${err.message}`;
26436
+ emit("FETCH_FAILED", message);
26437
+ return 1;
26438
+ }
26439
+
26440
+ // src/commands/webhooks.ts
26441
+ init_api_client();
26442
+ init_output();
26443
+ var STATUSES = [
26444
+ "pending",
26445
+ "forwarding",
26446
+ "delivered",
26447
+ "failed",
26448
+ "skipped",
26449
+ "ignored"
26450
+ ];
26451
+ var KEY2 = /^[a-z0-9][a-z0-9-]{0,39}$/;
26452
+ async function webhooksListCommand(slugArg, options) {
26453
+ const t = await resolveJobsTarget(slugArg, options, "webhooks list", "read the webhooks of");
26454
+ if (typeof t === "number") return t;
26455
+ let body;
26456
+ try {
26457
+ body = await getModuleWebhooks(t.installationId, t.auth);
26458
+ } catch (err) {
26459
+ return mapWebhooksError(err, options.json, "read the webhooks");
26460
+ }
26461
+ if (options.json) {
26462
+ out(JSON.stringify(body));
26463
+ return 0;
26464
+ }
26465
+ if (body.webhooks.length === 0) {
26466
+ info(`The installed version of ${t.label} declares no webhooks.`);
26467
+ return 0;
26468
+ }
26469
+ if (!body.enabled) {
26470
+ warn("Webhooks are not received on this platform yet (not enabled): there is no URL to give a provider.");
26471
+ } else if (!body.active) {
26472
+ warn("This installation does not receive webhooks now (offline, held, without a runtime, or on a Workbench).");
26473
+ }
26474
+ table(
26475
+ ["KEY", "PROVIDER", "EVENTS", "ROUTE", "LAST DELIVERY"],
26476
+ body.webhooks.map((w) => [
26477
+ w.key,
26478
+ w.provider,
26479
+ w.events ? w.events.join(",") : dim("all"),
26480
+ w.route,
26481
+ w.last_delivery ? `${colorStatus2(w.last_delivery.status)} ${dim(formatTs2(w.last_delivery.received_at))}` : dim("never")
26482
+ ])
26483
+ );
26484
+ out("");
26485
+ for (const w of body.webhooks) {
26486
+ if (w.url) out(`${w.key}: ${w.url} ${dim(`(signed with ${w.secret})`)}`);
26487
+ }
26488
+ info(`${body.webhooks.length} ${body.webhooks.length === 1 ? "webhook" : "webhooks"} (${t.label}).`);
26489
+ return 0;
26490
+ }
26491
+ async function webhooksDeliveriesCommand(slugArg, options) {
26492
+ if (options.status !== void 0 && !STATUSES.includes(options.status)) {
26493
+ fail(`--status must be one of ${STATUSES.join(", ")} (got "${options.status}").`);
26494
+ return 1;
26495
+ }
26496
+ if (options.key !== void 0 && !KEY2.test(options.key)) {
26497
+ fail(`--key must be a webhook key like ${code("github")} (got "${options.key}").`);
26498
+ return 1;
26499
+ }
26500
+ if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 200)) {
26501
+ fail(`--limit must be between 1 and 200 (got "${options.limit}").`);
26502
+ return 1;
26503
+ }
26504
+ const t = await resolveJobsTarget(slugArg, options, "webhooks deliveries", "read the webhooks of");
26505
+ if (typeof t === "number") return t;
26506
+ let body;
26507
+ try {
26508
+ body = await listWebhookDeliveries(
26509
+ t.installationId,
26510
+ {
26511
+ ...options.key ? { key: options.key } : {},
26512
+ ...options.status ? { status: options.status } : {},
26513
+ ...options.before !== void 0 ? { cursor: options.before } : {},
26514
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
26515
+ },
26516
+ t.auth
26517
+ );
26518
+ } catch (err) {
26519
+ return mapWebhooksError(err, options.json, "read the deliveries");
26520
+ }
26521
+ if (options.json) {
26522
+ out(JSON.stringify(body));
26523
+ return 0;
26524
+ }
26525
+ if (body.deliveries.length === 0) {
26526
+ info(`No deliveries for ${t.label}${options.key ? ` (${options.key})` : ""}.`);
26527
+ return 0;
26528
+ }
26529
+ table(
26530
+ ["RECEIVED", "ID", "KEY", "EVENT", "STATUS", "KIND", "TRIES", "HTTP", "ERROR"],
26531
+ body.deliveries.map((d) => [
26532
+ formatTs2(d.received_at),
26533
+ String(d.id),
26534
+ d.webhook_key,
26535
+ d.event ?? "",
26536
+ colorStatus2(d.status),
26537
+ d.kind,
26538
+ String(d.attempts),
26539
+ d.last_http_status === null ? "" : String(d.last_http_status),
26540
+ d.last_error_code ?? ""
26541
+ ])
26542
+ );
26543
+ out("");
26544
+ info(
26545
+ `${body.deliveries.length} ${body.deliveries.length === 1 ? "delivery" : "deliveries"} (${t.label}).` + (body.next_cursor !== null ? ` Older: ${code(`--before ${body.next_cursor}`)}.` : "")
26546
+ );
26547
+ return 0;
26548
+ }
26549
+ async function webhooksRedeliverCommand(deliveryId, slugArg, options) {
26550
+ if (!/^[1-9][0-9]{0,15}$/.test(deliveryId)) {
26551
+ fail(`"${deliveryId}" is not a delivery id (the ID column of ${code("dashwise webhooks deliveries")}).`);
26552
+ return 1;
26553
+ }
26554
+ const t = await resolveJobsTarget(slugArg, options, `webhooks redeliver ${deliveryId}`, "redeliver webhooks on");
26555
+ if (typeof t === "number") return t;
26556
+ let body;
26557
+ try {
26558
+ body = await redeliverWebhookDelivery(t.installationId, Number(deliveryId), t.auth);
26559
+ } catch (err) {
26560
+ return mapWebhooksError(err, options.json, `redeliver ${deliveryId}`);
26561
+ }
26562
+ if (options.json) {
26563
+ out(JSON.stringify(body));
26564
+ return 0;
26565
+ }
26566
+ success(
26567
+ `Redelivering #${body.delivery.id} (${body.delivery.webhook_key}${body.delivery.event ? `, ${body.delivery.event}` : ""}) to ${t.label} as a replay (invocation ${body.delivery.invocation_id}).`
26568
+ );
26569
+ info(`Follow it with ${code(`dashwise webhooks deliveries --key ${body.delivery.webhook_key}`)}.`);
26570
+ return 0;
26571
+ }
26572
+ function colorStatus2(status) {
26573
+ switch (status) {
26574
+ case "delivered":
26575
+ return pc.green(status);
26576
+ case "pending":
26577
+ case "forwarding":
26578
+ return pc.yellow(status);
26579
+ case "ignored":
26580
+ case "skipped":
26581
+ return pc.dim(status);
26582
+ default:
26583
+ return pc.red(status);
26584
+ }
26585
+ }
26586
+ var PASS_THROUGH = /* @__PURE__ */ new Set([
26587
+ "FEATURE_NOT_ENABLED",
26588
+ "COPY_NOT_RUNNING_JOBS",
26589
+ "WEBHOOK_DELIVERY_NOT_STORED",
26590
+ "WEBHOOK_DELIVERY_IN_PROGRESS",
26591
+ "WEBHOOK_DELIVERY_NOT_FOUND",
26592
+ "WEBHOOK_REDELIVERY_IMPERSONATION_REFUSED"
26593
+ ]);
26594
+ function mapWebhooksError(err, json, what) {
26595
+ const emit = (c2, message) => {
26596
+ if (json) out(JSON.stringify({ ok: false, error: { code: c2, message } }));
26597
+ else fail(message);
26598
+ };
26599
+ if (err instanceof ApiError) {
26600
+ if (err.status === 401 || err.code === "UNAUTHENTICATED") {
26601
+ emit("NOT_LOGGED_IN", `Not logged in. Run ${code("dashwise login")} first.`);
26602
+ return 1;
26603
+ }
26604
+ if (PASS_THROUGH.has(err.code)) {
26605
+ emit(err.code, err.message);
26606
+ return 1;
26607
+ }
26608
+ if (err.status === 404 || err.code === "INSTALLATION_NOT_FOUND") {
26609
+ emit(
26610
+ "INSTALLATION_NOT_FOUND",
26611
+ `Installation not found, or you may not open it. Check the id (${code("dashwise module status <slug>")}).`
26612
+ );
26613
+ return 1;
26614
+ }
26615
+ if (err.status === 403) {
26616
+ emit(
26617
+ err.code || "FORBIDDEN",
26618
+ `You may not ${what} on this installation: reading needs its logs permission, a redelivery its settings permission (an installation manager or a company admin).`
26619
+ );
26620
+ return 1;
26621
+ }
26622
+ emit(err.code, `Could not ${what}: ${err.code} ${err.status} \u2014 ${err.message}`);
26623
+ return 1;
26624
+ }
26625
+ emit("FETCH_FAILED", `Could not ${what}: ${err.message}`);
26626
+ return 1;
26627
+ }
26628
+
26140
26629
  // src/commands/module/rebuild.ts
26141
26630
  init_api_client();
26142
26631
  init_config();
@@ -27006,6 +27495,69 @@ moduleCmd.command("clone <slug>").description("Clone a published module locally.
27006
27495
  });
27007
27496
  }
27008
27497
  );
27498
+ var jobsCmd = program.command("jobs").description(
27499
+ "A module installation's scheduled jobs (`schedules[]` in module.json): `list` the schedules with their next and last run, read the `runs` log, or `run` one now. The installation is resolved like `module logs`: the slug (argument or module.json) and the workspace, or `--install <id>`."
27500
+ );
27501
+ var jobsTargetFlags = (cmd) => cmd.option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--install <id>", "Target an installation id directly (skips slug + workspace resolution).", parseIntOption).option("--json", "Emit the raw JSON response.");
27502
+ var jobsTarget = (o) => ({
27503
+ ...o.dir !== void 0 ? { dir: o.dir } : {},
27504
+ ...o.workspace !== void 0 ? { workspace: o.workspace } : {},
27505
+ ...o.install !== void 0 ? { install: o.install } : {},
27506
+ ...o.json !== void 0 ? { json: o.json } : {}
27507
+ });
27508
+ jobsTargetFlags(
27509
+ jobsCmd.command("list [slug]").description("List the schedules the installed version declares: cron (UTC), route, next run, last run.")
27510
+ ).action(async (slug, cmdOpts) => {
27511
+ process.exitCode = await jobsListCommand(slug, jobsTarget(cmdOpts));
27512
+ });
27513
+ jobsTargetFlags(
27514
+ jobsCmd.command("runs [slug]").description("The run log, newest first: one line per attempt (status, HTTP status, duration, error code).").option("--key <key>", "Only this schedule key.").option("--kind <kind>", "Only this kind: schedule, manual, webhook, migration or replay.").option("--limit <n>", "Rows per page (1-200, default 50).", parseIntOption).option("--before <id>", "Older rows: the id the previous page printed.", parseIntOption)
27515
+ ).action(
27516
+ async (slug, cmdOpts) => {
27517
+ process.exitCode = await jobsRunsCommand(slug, {
27518
+ ...jobsTarget(cmdOpts),
27519
+ ...cmdOpts.key !== void 0 ? { key: cmdOpts.key } : {},
27520
+ ...cmdOpts.kind !== void 0 ? { kind: cmdOpts.kind } : {},
27521
+ ...cmdOpts.limit !== void 0 ? { limit: cmdOpts.limit } : {},
27522
+ ...cmdOpts.before !== void 0 ? { before: cmdOpts.before } : {}
27523
+ });
27524
+ }
27525
+ );
27526
+ jobsTargetFlags(
27527
+ jobsCmd.command("run <key> [slug]").description(
27528
+ "Run schedule <key> now, outside its cron (audited). Needs the installation settings permission. Exit 2 when the run did not succeed."
27529
+ )
27530
+ ).action(async (key, slug, cmdOpts) => {
27531
+ process.exitCode = await jobsRunCommand(key, slug, jobsTarget(cmdOpts));
27532
+ });
27533
+ var webhooksCmd = program.command("webhooks").description(
27534
+ "A module installation's webhooks (`webhooks[]` in module.json): `list` them with the URL to give each provider, read the stored `deliveries`, or `redeliver` one. The platform verifies, deduplicates and stores each delivery before forwarding it to your route."
27535
+ );
27536
+ jobsTargetFlags(
27537
+ webhooksCmd.command("list [slug]").description("List the webhooks the installed version declares: provider, events, route, last delivery, URL.")
27538
+ ).action(async (slug, cmdOpts) => {
27539
+ process.exitCode = await webhooksListCommand(slug, jobsTarget(cmdOpts));
27540
+ });
27541
+ jobsTargetFlags(
27542
+ webhooksCmd.command("deliveries [slug]").description("The stored deliveries, newest first (kept 7 days): event, status, kind, tries, last HTTP status.").option("--key <key>", "Only this webhook key.").option("--status <status>", "Only this status: pending, forwarding, delivered, failed, skipped or ignored.").option("--limit <n>", "Rows per page (1-200, default 50).", parseIntOption).option("--before <id>", "Older rows: the id the previous page printed.", parseIntOption)
27543
+ ).action(
27544
+ async (slug, cmdOpts) => {
27545
+ process.exitCode = await webhooksDeliveriesCommand(slug, {
27546
+ ...jobsTarget(cmdOpts),
27547
+ ...cmdOpts.key !== void 0 ? { key: cmdOpts.key } : {},
27548
+ ...cmdOpts.status !== void 0 ? { status: cmdOpts.status } : {},
27549
+ ...cmdOpts.limit !== void 0 ? { limit: cmdOpts.limit } : {},
27550
+ ...cmdOpts.before !== void 0 ? { before: cmdOpts.before } : {}
27551
+ });
27552
+ }
27553
+ );
27554
+ jobsTargetFlags(
27555
+ webhooksCmd.command("redeliver <delivery-id> [slug]").description(
27556
+ "Forward a stored delivery again (a replay with a new invocation id; audited). Needs the installation settings permission."
27557
+ )
27558
+ ).action(async (deliveryId, slug, cmdOpts) => {
27559
+ process.exitCode = await webhooksRedeliverCommand(deliveryId, slug, jobsTarget(cmdOpts));
27560
+ });
27009
27561
  var depsCmd = program.command("deps").description(
27010
27562
  "Cross-module dependency commands. Bare `dashwise deps` shows the live connect status (resolved / unbound) of each declared dep on your installation; the subcommands manage module.json#dependencies (add / remove / list / check) and the connect lifecycle (bind / unbind)."
27011
27563
  ).option("--dir <path>", "Project root (default: current directory).").option("--json", "Emit the connect-status list as JSON instead of the human-readable view.").action(async (cmdOpts) => {
@@ -27829,7 +28381,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
27829
28381
  }
27830
28382
  );
27831
28383
  function getVersion() {
27832
- return "0.31.1";
28384
+ return "0.32.0";
27833
28385
  }
27834
28386
  function parsePositiveIdOption(value) {
27835
28387
  if (!/^[1-9][0-9]*$/.test(value.trim())) {