@dashai/cli 0.33.2 → 0.34.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/README.md CHANGED
@@ -831,6 +831,7 @@ The CLI persists state at `$XDG_CONFIG_HOME/dashwise/auth.json` (typically `~/.c
831
831
  | --- | --- |
832
832
  | 0 | Success |
833
833
  | 1 | Generic failure (network error, validation, unauthorized) |
834
+ | 6 | `module install` / `module ship`: a blocking release check has not passed (`RELEASE_GATE_BLOCKED`); the checks and the DashAI Builder link are printed, nothing is installed |
834
835
  | 130 | User cancelled (Ctrl-C / `cancel` from a prompt) |
835
836
 
836
837
  ## License
@@ -847,7 +848,7 @@ dashwise module ship my-module --workspace acme --bump patch
847
848
  dashwise module ship my-module --commit-sha <full-40-character-sha> --installation 156
848
849
  ```
849
850
 
850
- Before anything is queued it prints the revision it will release (`Releasing <sha> from main`, from the server's release plan) and names newer work on another branch that is not included. If the release is still building at `--timeout` (exit 4), it prints `dashwise module ship <slug> --build <id>`, which waits for that build and then installs it, without queueing another release. `--yes` skips the install question when nothing is lost; it never accepts data loss. A move that loses data is accepted only by answering in a terminal; without one, it is refused and the refusal prints the `module install --accept-data-loss <drift_hash>` re-run line. **A person takes it live** (DW-323): inside an AI agent session, or when Dashwise answers `GO_LIVE_NEEDS_PERSON` (its person gate refuses CLI tokens a direct go-live), the install half files a go-live request for the existing installation instead, prints the module's Live tab link and waits up to `--wait <seconds>` (default 900; 240 inside an AI agent session, under a coding agent's 10-minute tool-call limit) for a person to approve or decline it there. An approval that is still running the update counts as waiting, not as a failure. A module's first installation in a company is created by a person in Dashwise. Exit codes are `module release`'s while building (4: still building) and `module install`'s after; a proposal ends 0 (live), 3 (declined or replaced by a newer proposal), 1 (the update failed after the approval, or the request cannot be read) or 5 (still waiting; the proposal stays open). To wait longer than one tool call, use `--wait 0` and run the same command again to check: while the proposal is open it files nothing new and ends 5, and 0 once it is live. `--json` prints the proposal's outcome as one object (`go_live_request_id`, `status`, `outcome`, `approve_at`, `awaiting_person`). `module install` behaves the same.
851
+ Before anything is queued it prints the revision it will release (`Releasing <sha> from main`, from the server's release plan) and names newer work on another branch that is not included. If the release is still building at `--timeout` (exit 4), it prints `dashwise module ship <slug> --build <id>`, which waits for that build and then installs it, without queueing another release. `--yes` skips the install question when nothing is lost; it never accepts data loss. A move that loses data is accepted only by answering in a terminal; without one, it is refused and the refusal prints the `module install --accept-data-loss <drift_hash>` re-run line. **A person takes it live** (DW-323): inside an AI agent session, or when Dashwise answers `GO_LIVE_NEEDS_PERSON` (its person gate refuses CLI tokens a direct go-live), the install half files a go-live request for the existing installation instead, prints the module's Live tab link and waits up to `--wait <seconds>` (default 900; 240 inside an AI agent session, under a coding agent's 10-minute tool-call limit) for a person to approve or decline it there. An approval that is still running the update counts as waiting, not as a failure. A module's first installation in a company is created by a person in Dashwise. Exit codes are `module release`'s while building (4: still building) and `module install`'s after; a proposal ends 0 (live), 3 (declined or replaced by a newer proposal), 1 (the update failed after the approval, or the request cannot be read) or 5 (still waiting; the proposal stays open). To wait longer than one tool call, use `--wait 0` and run the same command again to check: while the proposal is open it files nothing new and ends 5, and 0 once it is live. **Where the company governs its releases** (RL-6), the same happens when Dashwise answers `FOUR_EYES_REQUIRED` (you worked on this version, so someone else takes it live), `RELEASE_APPROVER_REQUIRED` (an approver takes versions live) or `SECOND_APPROVAL_REQUIRED` (two approvals for the whole company): the CLI proposes the update for an existing installation; a first installation is requested in Dashwise. `RELEASE_GATE_BLOCKED` (a blocking release check has not passed, on the install or on filing the proposal) prints each blocking check with its status and the module's DashAI Builder link, where the checks run and are fixed, and ends with exit 6; `--json` prints `release_gate_blocked`, `checks`, `run_required`, `override_allowed` and `fix_at`. `--json` prints the proposal's outcome as one object (`go_live_request_id`, `status`, `outcome`, `approve_at`, `awaiting_person`). `module install` behaves the same.
851
852
 
852
853
  ### `dashwise module build-log [slug] <buildId>`
853
854
 
package/dist/bin.js CHANGED
@@ -19377,8 +19377,97 @@ init_scaffold();
19377
19377
  var EXIT_AWAITING_PERSON = 5;
19378
19378
  var DEFAULT_GO_LIVE_WAIT_SEC = 900;
19379
19379
  var AGENT_GO_LIVE_WAIT_SEC = 240;
19380
- function isGoLiveNeedsPerson(err) {
19381
- return err instanceof ApiError && err.code === "GO_LIVE_NEEDS_PERSON";
19380
+ var PROPOSE_INSTEAD_CODES = [
19381
+ "GO_LIVE_NEEDS_PERSON",
19382
+ "FOUR_EYES_REQUIRED",
19383
+ "RELEASE_APPROVER_REQUIRED",
19384
+ "SECOND_APPROVAL_REQUIRED"
19385
+ ];
19386
+ function proposeInsteadCode(err) {
19387
+ if (!(err instanceof ApiError)) return null;
19388
+ return PROPOSE_INSTEAD_CODES.includes(err.code) ? err.code : null;
19389
+ }
19390
+ function proposeInsteadLine(err) {
19391
+ switch (err.code) {
19392
+ case "GO_LIVE_NEEDS_PERSON":
19393
+ return "Dashwise takes modules live through a person: proposing this update instead.";
19394
+ case "FOUR_EYES_REQUIRED":
19395
+ return err.context?.contributors_recorded === false ? "Your company's release policy has someone who did not work on a version take it live, and this version did not record who worked on it: proposing this update for an approver instead." : "You worked on this version, and your company's release policy has someone else take it live: proposing this update for an approver instead.";
19396
+ case "RELEASE_APPROVER_REQUIRED":
19397
+ return "Your company's release policy has an approver take versions live: proposing this update for one instead.";
19398
+ case "SECOND_APPROVAL_REQUIRED":
19399
+ return "Your company's release policy takes a version live for the whole company with two approvals: proposing this update instead.";
19400
+ }
19401
+ }
19402
+ var EXIT_RELEASE_GATE_BLOCKED = 6;
19403
+ function isReleaseGateBlocked(err) {
19404
+ return err instanceof ApiError && err.code === "RELEASE_GATE_BLOCKED";
19405
+ }
19406
+ function builderLink(apiUrl, moduleId) {
19407
+ const path = `/dashai/builder/${moduleId}`;
19408
+ const origin = hostedAppOrigin(apiUrl);
19409
+ return origin ? `${origin}${path}` : path;
19410
+ }
19411
+ var CHECK_NAMES = {
19412
+ manifest: "Manifest",
19413
+ roles_review: "Roles review",
19414
+ bindings: "Data bindings",
19415
+ secrets: "Secrets",
19416
+ security_scan: "Security scan",
19417
+ build: "Build",
19418
+ self_test: "Self-test run",
19419
+ door_inventory: "New ways in",
19420
+ capabilities: "Platform capabilities"
19421
+ };
19422
+ var STATUS_WORDS = {
19423
+ fail: "blocker",
19424
+ warn: "warning",
19425
+ unavailable: "not checked",
19426
+ skipped: "skipped",
19427
+ not_checked: "not run yet"
19428
+ };
19429
+ function blockedChecks(err) {
19430
+ const raw = err.context?.checks;
19431
+ if (!Array.isArray(raw)) return [];
19432
+ return raw.flatMap(
19433
+ (c2) => c2 && typeof c2 === "object" && typeof c2.id === "string" && typeof c2.status === "string" ? [{ id: c2.id, status: c2.status }] : []
19434
+ );
19435
+ }
19436
+ function reportReleaseGateBlocked(err, args) {
19437
+ const checks = blockedChecks(err);
19438
+ const runRequired = err.context?.run_required === true;
19439
+ const overrideAllowed = err.context?.override_allowed === true;
19440
+ const runId = typeof err.context?.run_id === "number" ? err.context.run_id : null;
19441
+ const link = builderLink(args.apiUrl, args.moduleId);
19442
+ if (args.json) {
19443
+ out(
19444
+ JSON.stringify({
19445
+ release_gate_blocked: true,
19446
+ module_id: args.moduleId,
19447
+ version: args.version,
19448
+ installation_id: args.installationId ?? null,
19449
+ checks,
19450
+ run_id: runId,
19451
+ run_required: runRequired,
19452
+ override_allowed: overrideAllowed,
19453
+ fix_at: link
19454
+ })
19455
+ );
19456
+ }
19457
+ const n = checks.length;
19458
+ fail(
19459
+ `v${args.version} of ${code(args.moduleSlug)} cannot go live yet: ` + (n === 0 ? "a blocking release check has not passed." : `${n === 1 ? "a blocking release check has" : `${n} blocking release checks have`} not passed.`) + " Nothing was installed."
19460
+ );
19461
+ for (const c2 of checks) {
19462
+ info(` ${CHECK_NAMES[c2.id] ?? c2.id}: ${STATUS_WORDS[c2.status] ?? c2.status}`);
19463
+ }
19464
+ info(
19465
+ runRequired ? `The release checks have not run on this version yet. Run them in the DashAI Builder: ${code(link)}` : `Fix them in the DashAI Builder, then run the checks again: ${code(link)}`
19466
+ );
19467
+ if (overrideAllowed) {
19468
+ info("You may also override a blocking check there, with a reason. An override is recorded and cannot be undone.");
19469
+ }
19470
+ return EXIT_RELEASE_GATE_BLOCKED;
19382
19471
  }
19383
19472
  function liveTabLink(apiUrl, moduleId, installationId) {
19384
19473
  const path = `/modules?module=${moduleId}&tab=live&copy=${installationId}`;
@@ -19416,6 +19505,16 @@ async function proposeGoLive(args, auth) {
19416
19505
  success(`Installation ${args.installationId} already runs v${args.version}.`);
19417
19506
  return emit(0, null, { already_live: true });
19418
19507
  }
19508
+ if (isReleaseGateBlocked(err)) {
19509
+ return reportReleaseGateBlocked(err, {
19510
+ moduleId: args.moduleId,
19511
+ moduleSlug: args.moduleSlug,
19512
+ version: args.version,
19513
+ apiUrl: args.apiUrl,
19514
+ installationId: args.installationId,
19515
+ ...args.json ? { json: true } : {}
19516
+ });
19517
+ }
19419
19518
  const why = err instanceof ApiError ? `${err.code || err.status} ${err.message}` : err.message;
19420
19519
  fail(`Could not propose v${args.version} for installation ${args.installationId}: ${why}`);
19421
19520
  return emit(1, null, { error: err instanceof ApiError ? err.code || err.status : "NETWORK" });
@@ -19920,15 +20019,16 @@ async function moduleInstallCommand(slugArg, options) {
19920
20019
  );
19921
20020
  break;
19922
20021
  } catch (err) {
19923
- if (isGoLiveNeedsPerson(err)) {
20022
+ const proposeCode = proposeInsteadCode(err);
20023
+ if (proposeCode !== null && err instanceof ApiError) {
19924
20024
  if (!existing) {
19925
20025
  fail(
19926
- `Taking ${code(mod.slug)} live in ${code(workspace2.slug)} is done by a person in Dashwise: create its first installation there.`
20026
+ proposeCode === "GO_LIVE_NEEDS_PERSON" ? `Taking ${code(mod.slug)} live in ${code(workspace2.slug)} is done by a person in Dashwise: create its first installation there.` : `Your company's release policy needs an approval to take ${code(mod.slug)} live in ${code(workspace2.slug)}, and it has no installation there yet: request approval for its first installation in Dashwise (${code(builderLink(auth.baseUrl, mod.id))}).`
19927
20027
  );
19928
20028
  if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
19929
20029
  return 1;
19930
20030
  }
19931
- info("Dashwise takes modules live through a person: proposing this update instead.");
20031
+ info(proposeInsteadLine(err));
19932
20032
  const proposed = await goLiveByProposal(
19933
20033
  mod,
19934
20034
  existing.id,
@@ -19939,6 +20039,18 @@ async function moduleInstallCommand(slugArg, options) {
19939
20039
  if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
19940
20040
  return proposed;
19941
20041
  }
20042
+ if (isReleaseGateBlocked(err)) {
20043
+ const blocked = reportReleaseGateBlocked(err, {
20044
+ moduleId: mod.id,
20045
+ moduleSlug: mod.slug,
20046
+ version: confirmedPlan?.to_version ?? targetVersion,
20047
+ apiUrl: auth.baseUrl,
20048
+ ...existing ? { installationId: existing.id } : {},
20049
+ ...options.json ? { json: true } : {}
20050
+ });
20051
+ if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
20052
+ return blocked;
20053
+ }
19942
20054
  let plan = null;
19943
20055
  if (confirmedPlan && err instanceof ApiError && err.code === "DRIFT_CHANGED" && round < MAX_PLAN_ROUNDS) {
19944
20056
  warn(
@@ -21488,6 +21600,11 @@ async function moduleShipCommand(slugArg, options) {
21488
21600
  if (installExit === EXIT_AWAITING_PERSON) {
21489
21601
  info(`v${build.version} is released; only its go-live waits for a person (not a build: do not ship again).`);
21490
21602
  }
21603
+ if (installExit === EXIT_RELEASE_GATE_BLOCKED) {
21604
+ info(
21605
+ `v${build.version} is released; its release checks block going live (do not ship again). Once they pass, install it with ${code(checkCommand)}.`
21606
+ );
21607
+ }
21491
21608
  return installExit;
21492
21609
  }
21493
21610
  async function defaultBranchHead(slugArg, dir) {
@@ -27987,7 +28104,7 @@ moduleCmd.command("build-log [slug] [buildId]").description(
27987
28104
  }
27988
28105
  );
27989
28106
  moduleCmd.command("ship [slug]").description(
27990
- "Release and install in one command: releases the latest push to the default branch (or --commit-sha), waits for the build, then installs that version, showing what the update does to the data and asking once. Inside an AI agent session (or when Dashwise requires it) it proposes the update instead and waits for a person to take it live on the Live tab."
28107
+ "Release and install in one command: releases the latest push to the default branch (or --commit-sha), waits for the build, then installs that version, showing what the update does to the data and asking once. Inside an AI agent session (or when Dashwise requires it) it proposes the update instead and waits for a person to take it live on the Live tab. Where the company governs releases, an approver takes it live (it proposes the same way), and exit 6 means a blocking release check has not passed: the checks and the DashAI Builder link are printed."
27991
28108
  ).option("--dir <path>", "Project root for module.json slug lookup (default: current directory).").option("--commit-sha <sha>", "Release this pushed revision (full 40-character SHA) instead of the default-branch head.").option("--build <id>", "Resume a ship whose release was still building: wait for that build, then install it. Never queues a release.", parsePositiveIdOption).option("--bump <direction>", "Semver bump: patch | minor | major (default minor).").option("--workspace <slug-or-id>", "Target workspace (as for module install).").option("--installation <id>", "Update exactly this production installation.", parsePositiveIdOption).option("--keep-dev-install", "Install alongside the workspace's dev install (as for module install).").option("--replace-dev-install", "Replace the workspace's dev install (as for module install).").option("-y, --yes", "Skip the install question when nothing is lost. Never accepts data loss.").option("--timeout <seconds>", "Max seconds to wait for the build (default 360); exit 4 means still building.", Number).option(
27992
28109
  "--wait <seconds>",
27993
28110
  "Max seconds to wait for a person to approve a go-live proposal (default 900, 240 inside an AI agent session; 0 proposes and returns). Exit 5: the proposal still waits; run the same command again to check (exit 4 is a release still building).",
@@ -28049,7 +28166,7 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
28049
28166
  }
28050
28167
  );
28051
28168
  moduleCmd.command("install [slug]").alias("deploy").alias("upgrade").description(
28052
- "Install (deploy) a published module into a workspace \u2014 provisions the runtime and prints the public URL. Upgrades/repairs an existing production install idempotently: an upgrade first prints what it does to the installation's data (tables and fields it moves to the Trash or converts, with their counts), asks once, and sends the preview's drift_hash. A move that loses data is accepted only by a person: the one question in a terminal, or --accept-data-loss <drift_hash> with the hash the preview printed. --yes never accepts data loss; with no terminal, or inside an AI agent session, a lossy move is refused and the refusal prints the re-run line. When only a dev install exists it asks whether to install alongside it (--keep-dev-install) or replace it (--replace-dev-install), rather than silently landing on the dev row. Exits 3 \u2014 \"deployed, setup pending\" \u2014 when a required `uses` slot is still unbound: the deploy succeeded but the module throws on its first data call until an admin binds it."
28169
+ "Install (deploy) a published module into a workspace \u2014 provisions the runtime and prints the public URL. Upgrades/repairs an existing production install idempotently: an upgrade first prints what it does to the installation's data (tables and fields it moves to the Trash or converts, with their counts), asks once, and sends the preview's drift_hash. A move that loses data is accepted only by a person: the one question in a terminal, or --accept-data-loss <drift_hash> with the hash the preview printed. --yes never accepts data loss; with no terminal, or inside an AI agent session, a lossy move is refused and the refusal prints the re-run line. When only a dev install exists it asks whether to install alongside it (--keep-dev-install) or replace it (--replace-dev-install), rather than silently landing on the dev row. Exits 3 \u2014 \"deployed, setup pending\" \u2014 when a required `uses` slot is still unbound: the deploy succeeded but the module throws on its first data call until an admin binds it. Where the company governs releases, a version that must be taken live by someone else or an approver is proposed as a go-live request, and exit 6 means a blocking release check has not passed: the checks and the DashAI Builder link are printed, and nothing is installed."
28053
28170
  ).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--version <semver>", "Install a specific published version (default: latest).").option(
28054
28171
  "--force",
28055
28172
  "Upgrade even when it breaks consumer modules (UPGRADE_BREAKS_CONSUMERS): shows which modules it disconnects and what it changes, asks to confirm (--yes confirms disconnections, never data loss), then upgrades exactly that plan."
@@ -28437,7 +28554,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
28437
28554
  }
28438
28555
  );
28439
28556
  function getVersion() {
28440
- return "0.33.2";
28557
+ return "0.34.0";
28441
28558
  }
28442
28559
  function parsePositiveIdOption(value) {
28443
28560
  if (!/^[1-9][0-9]*$/.test(value.trim())) {