@dashai/cli 0.16.0 → 0.17.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
@@ -464,6 +464,19 @@ async function applyDraftManifest(moduleId, manifest, options = {}) {
464
464
  options
465
465
  );
466
466
  }
467
+ async function getRuntimeLogs(installationId, opts = {}, options = {}) {
468
+ const params = new URLSearchParams();
469
+ if (opts.since !== void 0 && opts.since !== "") params.set("since", opts.since);
470
+ if (opts.limit !== void 0) params.set("limit", String(opts.limit));
471
+ if (opts.tail) params.set("tail", "true");
472
+ const qs = params.toString();
473
+ return apiRequest(
474
+ "GET",
475
+ `/api/modules/installations/${installationId}/runtime-logs${qs ? `?${qs}` : ""}`,
476
+ void 0,
477
+ options
478
+ );
479
+ }
467
480
  var ApiError;
468
481
  var init_api_client = __esm({
469
482
  "src/lib/api-client.ts"() {
@@ -4324,9 +4337,29 @@ function parseFieldSpec(spec) {
4324
4337
  `The ${quote("unique")} flag is not supported on field specs \u2014 uniqueness is a TABLE-level index. Declare it in module.json as ${quote(`owns.tables[].indexes: [{ "fields": ["${slug}"], "unique": true }]`)} and apply it with ${quote("dashwise schema push")}.`
4325
4338
  );
4326
4339
  }
4340
+ if (DECIMALS_FLAG_REGEX.test(flag)) {
4341
+ if (type !== "number") {
4342
+ throw new FieldSpecError(
4343
+ `Decimal places (${quote(flag)}) are only valid on ${quote("number")} fields, not ${quote(type)}. Field spec ${quote(spec)} sets a scale on a non-number type.`
4344
+ );
4345
+ }
4346
+ if (result.decimal_places !== void 0) {
4347
+ throw new FieldSpecError(
4348
+ `Decimal places specified twice in field spec ${quote(spec)}.`
4349
+ );
4350
+ }
4351
+ const places = Number.parseInt(flag, 10);
4352
+ if (places < 0 || places > MAX_DECIMAL_PLACES) {
4353
+ throw new FieldSpecError(
4354
+ `Decimal places must be between 0 and ${MAX_DECIMAL_PLACES} (got ${quote(flag)}) in field spec ${quote(spec)}.`
4355
+ );
4356
+ }
4357
+ result.decimal_places = places;
4358
+ continue;
4359
+ }
4327
4360
  if (!KNOWN_FLAGS.has(flag)) {
4328
4361
  throw new FieldSpecError(
4329
- `Unknown flag ${quote(flag)} in field spec ${quote(spec)}. Known flags: ${[...KNOWN_FLAGS].join(", ")}. For options/target/default, use ${quote("--field-json")} instead.`
4362
+ `Unknown flag ${quote(flag)} in field spec ${quote(spec)}. Known flags: ${[...KNOWN_FLAGS].join(", ")}${type === "number" ? ` (plus a bare 0\u2013${MAX_DECIMAL_PLACES} integer for decimal places, e.g. ${quote(`${slug}:number:2`)})` : ""}. For options/target/default, use ${quote("--field-json")} instead.`
4330
4363
  );
4331
4364
  }
4332
4365
  if (flag === "required") result.required = true;
@@ -4377,7 +4410,7 @@ function parseFieldJson(json) {
4377
4410
  function quote(s) {
4378
4411
  return `\`${s}\``;
4379
4412
  }
4380
- var FIELD_TYPES, SLUG_REGEX, KNOWN_FLAGS, FieldSpecError;
4413
+ var FIELD_TYPES, SLUG_REGEX, KNOWN_FLAGS, DECIMALS_FLAG_REGEX, MAX_DECIMAL_PLACES, FieldSpecError;
4381
4414
  var init_field_spec = __esm({
4382
4415
  "src/lib/field-spec.ts"() {
4383
4416
  FIELD_TYPES = [
@@ -4400,6 +4433,8 @@ var init_field_spec = __esm({
4400
4433
  ];
4401
4434
  SLUG_REGEX = /^[a-z][a-z0-9_]*[a-z0-9]$/;
4402
4435
  KNOWN_FLAGS = /* @__PURE__ */ new Set(["required"]);
4436
+ DECIMALS_FLAG_REGEX = /^\d+$/;
4437
+ MAX_DECIMAL_PLACES = 10;
4403
4438
  FieldSpecError = class extends Error {
4404
4439
  constructor(message) {
4405
4440
  super(message);
@@ -7241,13 +7276,13 @@ async function moduleUninstallCommand(slugArg, options) {
7241
7276
  }
7242
7277
  info(
7243
7278
  `Uninstalling ${code(mod.slug)} (install ${code(`#${row.id}`)}, kind=${row.kind ?? "production"}${row.module_version?.version ? `, v${row.module_version.version}` : ""}) from workspace ${code(workspace2.slug)}.
7244
- The app is soft-deleted, recoverable ONLY by a PLATFORM OPERATOR (contact support/HQ) within ~7 days \u2014 it does NOT appear in your dashboard trash.
7245
- An operator restore KEEPS the public URL${row.public_url ? ` ${code(row.public_url)}` : ""}; a REINSTALL mints a NEW one \u2014 deep links to the old URL break.`
7279
+ The app is soft-deleted and appears in your workspace trash \u2014 a workspace admin can self-serve restore its DATA (~30 days). Reviving a running module within ~7 days is a platform-operator action.
7280
+ A REINSTALL now KEEPS the same public URL${row.public_url ? ` ${code(row.public_url)}` : ""}, so deep links survive. (On backends older than 2026-07-15: restore is operator-only and a reinstall mints a new URL.)`
7246
7281
  );
7247
7282
  const answer = await select({
7248
7283
  message: `Uninstall ${code(mod.slug)} from ${code(workspace2.slug)}?`,
7249
7284
  options: [
7250
- { value: "uninstall", label: "Uninstall \u2014 soft-delete the app (operator-restorable ~7 days)" },
7285
+ { value: "uninstall", label: "Uninstall \u2014 soft-delete the app (data restorable from workspace trash ~30 days)" },
7251
7286
  { value: "cancel", label: "Cancel \u2014 nothing is changed" }
7252
7287
  ],
7253
7288
  initialValue: "cancel"
@@ -7279,8 +7314,8 @@ async function moduleUninstallCommand(slugArg, options) {
7279
7314
  `Uninstalled ${code(mod.slug)} (install ${code(`#${row.id}`)}) from workspace ${code(workspace2.slug)}.`
7280
7315
  );
7281
7316
  info(
7282
- `The app was soft-deleted. If this was a mistake: only a PLATFORM OPERATOR can restore it (contact support/HQ) within ~7 days \u2014 it does NOT appear in your dashboard trash. An operator restore keeps the same public URL.
7283
- Reinstalling instead (${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)}) mints a NEW public URL.`
7317
+ `The app was soft-deleted. If this was a mistake: restore its DATA yourself from your workspace trash (~30 days); to bring the running module back within ~7 days, contact a platform operator (support/HQ).
7318
+ Reinstalling (${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)}) now KEEPS the same public URL. (Older backends, < 2026-07-15: restore is operator-only and reinstall mints a new URL.)`
7284
7319
  );
7285
7320
  if (options.parkConsumers) {
7286
7321
  info(
@@ -10489,6 +10524,319 @@ function mapAndPrintError(err, json) {
10489
10524
  return 1;
10490
10525
  }
10491
10526
 
10527
+ // src/commands/module/logs.ts
10528
+ init_api_client();
10529
+ init_config();
10530
+ init_manifest_io();
10531
+ init_output();
10532
+ var POLL_INTERVAL_MS3 = 2e3;
10533
+ var DURATION_UNIT_MS = {
10534
+ s: 1e3,
10535
+ m: 6e4,
10536
+ h: 36e5,
10537
+ d: 864e5
10538
+ };
10539
+ function resolveSince(raw) {
10540
+ if (raw === void 0) return void 0;
10541
+ const trimmed = raw.trim();
10542
+ if (trimmed === "") return void 0;
10543
+ const m = /^(\d+)\s*(s|m|h|d)$/i.exec(trimmed);
10544
+ if (m) {
10545
+ const n = Number.parseInt(m[1], 10);
10546
+ const unitMs = DURATION_UNIT_MS[m[2].toLowerCase()];
10547
+ return new Date(Date.now() - n * unitMs).toISOString();
10548
+ }
10549
+ return trimmed;
10550
+ }
10551
+ async function moduleLogsCommand(slugArg, options) {
10552
+ if (options.follow && options.json) {
10553
+ fail("`--follow` and `--json` are mutually exclusive.");
10554
+ return 1;
10555
+ }
10556
+ if (!isLoggedIn()) {
10557
+ fail(`Not logged in. Run ${code("dashwise login")} first.`);
10558
+ return 1;
10559
+ }
10560
+ const cfg = readConfig();
10561
+ if (!cfg) {
10562
+ fail(`CLI config missing. Run ${code("dashwise login")}.`);
10563
+ return 1;
10564
+ }
10565
+ const auth = {
10566
+ baseUrl: resolveApiUrl(),
10567
+ auth: { token: cfg.token }
10568
+ };
10569
+ const since = resolveSince(options.since);
10570
+ let installationId;
10571
+ let label;
10572
+ if (options.install !== void 0) {
10573
+ if (!Number.isInteger(options.install) || options.install <= 0) {
10574
+ fail(
10575
+ `--install expects a positive integer installation id (got "${options.install}").`
10576
+ );
10577
+ return 1;
10578
+ }
10579
+ installationId = options.install;
10580
+ label = `install #${installationId}`;
10581
+ } else {
10582
+ const resolved = await resolveInstallationFromSlug(slugArg, options, cfg, auth);
10583
+ if (typeof resolved === "number") return resolved;
10584
+ installationId = resolved.installationId;
10585
+ label = resolved.label;
10586
+ }
10587
+ if (options.follow) {
10588
+ return runFollow(installationId, label, { since, limit: options.limit }, auth);
10589
+ }
10590
+ return runOnce3(installationId, label, options, since, auth);
10591
+ }
10592
+ async function resolveInstallationFromSlug(slugArg, options, cfg, auth) {
10593
+ let slug = slugArg;
10594
+ if (!slug) {
10595
+ try {
10596
+ const fromManifest = readManifest(options.dir)?.module?.slug;
10597
+ if (typeof fromManifest === "string" && fromManifest.length > 0) {
10598
+ slug = fromManifest;
10599
+ }
10600
+ } catch {
10601
+ }
10602
+ }
10603
+ if (!slug) {
10604
+ fail(
10605
+ `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>")}.`
10606
+ );
10607
+ return 1;
10608
+ }
10609
+ let modId;
10610
+ let modSlug;
10611
+ try {
10612
+ const mod = await getModuleBySlug(slug, auth);
10613
+ modId = mod.id;
10614
+ modSlug = mod.slug;
10615
+ } catch (err) {
10616
+ if (err instanceof ApiError && err.status === 404) {
10617
+ fail(`Module ${code(slug)} not found on the backend.`);
10618
+ return 1;
10619
+ }
10620
+ fail(`Could not resolve module ${code(slug)}: ${err.message}`);
10621
+ return 1;
10622
+ }
10623
+ let workspace2;
10624
+ try {
10625
+ workspace2 = await resolveWorkspaceWithFallback({
10626
+ userFlag: options.workspace,
10627
+ cachedWorkspaceId: readModuleEntry(slug)?.workspaceId,
10628
+ configDefault: cfg.workspaceSlug,
10629
+ apiUrl: resolveApiUrl(),
10630
+ apiToken: cfg.token
10631
+ });
10632
+ } catch (err) {
10633
+ fail(`Workspace selection failed: ${err.message}`);
10634
+ return 1;
10635
+ }
10636
+ let row;
10637
+ try {
10638
+ const installs = await listWorkspaceInstallations(workspace2.id, auth);
10639
+ const matches = installs.filter((r) => r.module_id === modId);
10640
+ row = matches.find((r) => !isDevInstallKind(r.kind) && isRealRuntimeUrl(r.runtime_url)) ?? matches.find((r) => !isDevInstallKind(r.kind)) ?? matches[0];
10641
+ } catch (err) {
10642
+ fail(`Could not read workspace installations: ${err.message}`);
10643
+ return 1;
10644
+ }
10645
+ if (!row) {
10646
+ info(
10647
+ `${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}`)}.`
10648
+ );
10649
+ return 0;
10650
+ }
10651
+ return {
10652
+ installationId: row.id,
10653
+ label: `${modSlug} in ${workspace2.slug} (install #${row.id})`
10654
+ };
10655
+ }
10656
+ async function runOnce3(installationId, label, options, since, auth) {
10657
+ let body;
10658
+ try {
10659
+ body = await getRuntimeLogs(
10660
+ installationId,
10661
+ {
10662
+ ...since !== void 0 && !options.tail ? { since } : {},
10663
+ ...options.limit !== void 0 ? { limit: options.limit } : {},
10664
+ ...options.tail ? { tail: true } : {}
10665
+ },
10666
+ auth
10667
+ );
10668
+ } catch (err) {
10669
+ return mapLogsError(err, options.json);
10670
+ }
10671
+ if (options.json) {
10672
+ out(JSON.stringify(body));
10673
+ return 0;
10674
+ }
10675
+ if (body.entries.length === 0) {
10676
+ info(`No runtime log entries for ${label} in the requested window.`);
10677
+ return 0;
10678
+ }
10679
+ for (const entry of body.entries) out(formatRow2(entry));
10680
+ out("");
10681
+ info(
10682
+ `${body.entries.length} ${body.entries.length === 1 ? "line" : "lines"} (${label}).`
10683
+ );
10684
+ return 0;
10685
+ }
10686
+ async function runFollow(installationId, label, opts, auth) {
10687
+ info(`Following runtime logs for ${label} (every ${POLL_INTERVAL_MS3 / 1e3}s). Ctrl-C to stop.`);
10688
+ out("");
10689
+ let stopped = false;
10690
+ const onSigint = () => {
10691
+ stopped = true;
10692
+ };
10693
+ process.on("SIGINT", onSigint);
10694
+ let lastTs;
10695
+ let seenAtLastTs = /* @__PURE__ */ new Set();
10696
+ let firstPoll = true;
10697
+ try {
10698
+ while (!stopped) {
10699
+ const query = {};
10700
+ if (opts.limit !== void 0) query.limit = opts.limit;
10701
+ if (firstPoll) {
10702
+ if (opts.since !== void 0) query.since = opts.since;
10703
+ else query.tail = true;
10704
+ } else if (lastTs !== void 0) {
10705
+ query.since = new Date(lastTs).toISOString();
10706
+ }
10707
+ let body;
10708
+ try {
10709
+ body = await getRuntimeLogs(installationId, query, auth);
10710
+ } catch (err) {
10711
+ if (isFatalFollowError(err)) {
10712
+ return mapLogsError(err, false);
10713
+ }
10714
+ info(dim(`(follow: transient error \u2014 ${err.message})`));
10715
+ await sleep4(POLL_INTERVAL_MS3, () => stopped);
10716
+ firstPoll = false;
10717
+ continue;
10718
+ }
10719
+ const fresh = [];
10720
+ for (const entry of body.entries) {
10721
+ if (lastTs !== void 0 && entry.ts < lastTs) continue;
10722
+ if (lastTs !== void 0 && entry.ts === lastTs && seenAtLastTs.has(entryKey(entry))) {
10723
+ continue;
10724
+ }
10725
+ fresh.push(entry);
10726
+ }
10727
+ fresh.sort((a, b) => a.ts - b.ts);
10728
+ for (const entry of fresh) out(formatRow2(entry));
10729
+ if (fresh.length > 0) {
10730
+ const newMax = fresh[fresh.length - 1].ts;
10731
+ if (newMax !== lastTs) seenAtLastTs = /* @__PURE__ */ new Set();
10732
+ lastTs = newMax;
10733
+ for (const entry of fresh) {
10734
+ if (entry.ts === lastTs) seenAtLastTs.add(entryKey(entry));
10735
+ }
10736
+ }
10737
+ firstPoll = false;
10738
+ await sleep4(POLL_INTERVAL_MS3, () => stopped);
10739
+ }
10740
+ } finally {
10741
+ process.off("SIGINT", onSigint);
10742
+ }
10743
+ out("");
10744
+ return 0;
10745
+ }
10746
+ function isFatalFollowError(err) {
10747
+ if (!(err instanceof ApiError)) return false;
10748
+ if (err.status === 401 || err.status === 403 || err.status === 404) return true;
10749
+ return err.code === "INSTALLATION_NOT_FOUND" || err.code === "RUNTIME_NOT_PROVISIONED" || err.code === "INVALID_SINCE";
10750
+ }
10751
+ function formatTs(ts) {
10752
+ if (!Number.isFinite(ts)) return "--:--:--";
10753
+ return new Date(ts).toISOString().slice(11, 19);
10754
+ }
10755
+ function colorLevel(level) {
10756
+ const padded = level.toUpperCase().padEnd(5);
10757
+ switch (level) {
10758
+ case "error":
10759
+ return pc.red(padded);
10760
+ case "warn":
10761
+ return pc.yellow(padded);
10762
+ case "info":
10763
+ return pc.green(padded);
10764
+ case "debug":
10765
+ default:
10766
+ return dim(padded);
10767
+ }
10768
+ }
10769
+ function formatRow2(entry) {
10770
+ const time = dim(formatTs(entry.ts));
10771
+ const level = colorLevel(entry.level);
10772
+ const reqId = entry.request_id ? ` ${dim(`[${entry.request_id}]`)}` : "";
10773
+ return `${time} ${level} ${entry.message}${reqId}`;
10774
+ }
10775
+ function entryKey(entry) {
10776
+ return `${entry.ts}\0${entry.request_id ?? ""}\0${entry.message}`;
10777
+ }
10778
+ function mapLogsError(err, json) {
10779
+ if (err instanceof ApiError) {
10780
+ const emit = (code_, message2) => {
10781
+ if (json) out(JSON.stringify({ ok: false, error: { code: code_, message: message2 } }));
10782
+ else fail(message2);
10783
+ };
10784
+ if (err.status === 401 || err.code === "UNAUTHENTICATED") {
10785
+ emit("NOT_LOGGED_IN", `Not logged in. Run ${code("dashwise login")} first.`);
10786
+ return 1;
10787
+ }
10788
+ if (err.status === 403) {
10789
+ emit(
10790
+ "FORBIDDEN",
10791
+ `Reading runtime logs requires workspace-admin on this install's workspace.`
10792
+ );
10793
+ return 1;
10794
+ }
10795
+ if (err.status === 404 || err.code === "INSTALLATION_NOT_FOUND") {
10796
+ emit(
10797
+ "INSTALLATION_NOT_FOUND",
10798
+ `Installation not found. Check the id (${code("dashwise module status <slug>")}) \u2014 it may have been uninstalled.`
10799
+ );
10800
+ return 1;
10801
+ }
10802
+ if (err.code === "RUNTIME_NOT_PROVISIONED") {
10803
+ emit(
10804
+ "RUNTIME_NOT_PROVISIONED",
10805
+ `This install has no running runtime yet \u2014 deploy it first (${code("dashwise module install <slug>")}). The ${code("kind=local")} dev install never provisions a machine runtime, so it has no runtime logs.`
10806
+ );
10807
+ return 1;
10808
+ }
10809
+ if (err.code === "INVALID_SINCE") {
10810
+ emit(
10811
+ "INVALID_SINCE",
10812
+ `${code("--since")} must be an ISO-8601 timestamp (e.g. ${code("2026-07-15T09:00:00Z")}) or a duration like ${code("15m")} / ${code("2h")} / ${code("2d")}.`
10813
+ );
10814
+ return 1;
10815
+ }
10816
+ if (err.code.startsWith("RUNTIME_")) {
10817
+ emit(
10818
+ err.code,
10819
+ `The runtime host rejected the log read (${err.code}): ${err.message}. This is a host-side error \u2014 retry, or check the runtime with ${code("dashwise module status <slug>")}.`
10820
+ );
10821
+ return 1;
10822
+ }
10823
+ emit(err.code, `Could not read runtime logs: ${err.code} ${err.status} \u2014 ${err.message}`);
10824
+ return 1;
10825
+ }
10826
+ const message = `Could not read runtime logs: ${err.message}`;
10827
+ if (json) out(JSON.stringify({ ok: false, error: { code: "FETCH_FAILED", message } }));
10828
+ else fail(message);
10829
+ return 1;
10830
+ }
10831
+ async function sleep4(ms, aborted) {
10832
+ const slice = 100;
10833
+ let waited = 0;
10834
+ while (waited < ms && !aborted()) {
10835
+ await new Promise((resolve13) => setTimeout(resolve13, Math.min(slice, ms - waited)));
10836
+ waited += slice;
10837
+ }
10838
+ }
10839
+
10492
10840
  // src/bin.ts
10493
10841
  init_output();
10494
10842
  var program = new Command();
@@ -11115,6 +11463,29 @@ moduleCmd.command("action-log").description(
11115
11463
  });
11116
11464
  }
11117
11465
  );
11466
+ moduleCmd.command("logs [slug]").description(
11467
+ "Read a deployed module install's runtime logs (the host's log group). Resolves the install from the slug + workspace like `module status`, or target any install directly with `--install <id>`. `--since 15m|2h|<iso>` sets a lower bound; `--tail` shows the most-recent page; `--follow`/`-f` streams new lines every ~2s (Ctrl-C to stop). Workspace-admin."
11468
+ ).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(
11469
+ "--install <id>",
11470
+ "Target a specific installation id directly (deployed installs included). Find ids via `dashwise module status <slug>`. Skips slug + workspace resolution.",
11471
+ parseIntOption
11472
+ ).option(
11473
+ "--since <iso-or-duration>",
11474
+ 'Only lines newer than this. Accepts an ISO-8601 timestamp or a duration like "15m", "2h", "2d". Ignored with --tail.'
11475
+ ).option("--limit <n>", "Max lines per fetch (1-1000, backend-clamped, default 100).", parseIntOption).option("--tail", "Show the most-recent page (ignores --since).").option("-f, --follow", "Poll every ~2s for new lines. Ctrl-C to stop. Mutually exclusive with --json.").option("--json", "Emit the raw `{ entries }` JSON response. Ignored with --follow.").action(
11476
+ async (slug, cmdOpts) => {
11477
+ process.exitCode = await moduleLogsCommand(slug, {
11478
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
11479
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
11480
+ ...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
11481
+ ...cmdOpts.since !== void 0 ? { since: cmdOpts.since } : {},
11482
+ ...cmdOpts.limit !== void 0 ? { limit: cmdOpts.limit } : {},
11483
+ ...cmdOpts.tail !== void 0 ? { tail: cmdOpts.tail } : {},
11484
+ ...cmdOpts.follow !== void 0 ? { follow: cmdOpts.follow } : {},
11485
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
11486
+ });
11487
+ }
11488
+ );
11118
11489
  moduleCmd.command("destroy-dev-install").description(
11119
11490
  "Destroy the local-dev install for the current module. Tears down the install row + dashboard tile; if the module had no other installs and was never published, drops the module too. Local module.json is preserved (only the cached runtime token + install id in auth.json get cleared)."
11120
11491
  ).option("--dir <path>", "Project root (default: current directory).").option(
@@ -11248,7 +11619,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
11248
11619
  }
11249
11620
  })();
11250
11621
  function getVersion() {
11251
- return "0.16.0";
11622
+ return "0.17.0";
11252
11623
  }
11253
11624
  function parseIntOption(value) {
11254
11625
  const n = Number.parseInt(value, 10);