brnch-cli 0.18.2 → 0.19.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/brnch.mjs +1290 -170
  3. package/package.json +1 -1
package/brnch.mjs CHANGED
@@ -6044,9 +6044,9 @@ var require_lexer = __commonJS({
6044
6044
  }
6045
6045
  }
6046
6046
  *parseQuotedScalar() {
6047
- const quote2 = this.charAt(0);
6048
- let end2 = this.buffer.indexOf(quote2, this.pos + 1);
6049
- if (quote2 === "'") {
6047
+ const quote3 = this.charAt(0);
6048
+ let end2 = this.buffer.indexOf(quote3, this.pos + 1);
6049
+ if (quote3 === "'") {
6050
6050
  while (end2 !== -1 && this.buffer[end2 + 1] === "'")
6051
6051
  end2 = this.buffer.indexOf("'", end2 + 2);
6052
6052
  } else {
@@ -9960,7 +9960,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
9960
9960
  const negativeNumberArg = (arg) => {
9961
9961
  if (!/^-(\d+|\d*\.\d+)(e[+-]?\d+)?$/.test(arg)) return false;
9962
9962
  return !this._getCommandAndAncestors().some(
9963
- (cmd) => cmd.options.map((opt) => opt.short).some((short2) => /^-\d$/.test(short2))
9963
+ (cmd) => cmd.options.map((opt) => opt.short).some((short3) => /^-\d$/.test(short3))
9964
9964
  );
9965
9965
  };
9966
9966
  let activeVariadicOption = null;
@@ -21407,8 +21407,8 @@ var error43 = () => {
21407
21407
  case "not_multiple_of":
21408
21408
  return `N\xFAmero inv\xE1lido: deve ser m\xFAltiplo de ${issue2.divisor}`;
21409
21409
  case "unrecognized_keys": {
21410
- const plural3 = issue2.keys.length > 1 ? "s" : "";
21411
- return `Chave${plural3} inv\xE1lida${plural3}: ${joinValues(issue2.keys, ", ")}`;
21410
+ const plural4 = issue2.keys.length > 1 ? "s" : "";
21411
+ return `Chave${plural4} inv\xE1lida${plural4}: ${joinValues(issue2.keys, ", ")}`;
21412
21412
  }
21413
21413
  case "invalid_key":
21414
21414
  return `Entrada inv\xE1lida n${translateOriginWithArticle(issue2.origin, "definite")}`;
@@ -21552,8 +21552,8 @@ var error44 = () => {
21552
21552
  case "not_multiple_of":
21553
21553
  return `N\xFAmero inv\xE1lido: deve ser m\xFAltiplo de ${issue2.divisor}`;
21554
21554
  case "unrecognized_keys": {
21555
- const plural3 = issue2.keys.length > 1 ? "s" : "";
21556
- return `Chave${plural3} inv\xE1lida${plural3}: ${joinValues(issue2.keys, ", ")}`;
21555
+ const plural4 = issue2.keys.length > 1 ? "s" : "";
21556
+ return `Chave${plural4} inv\xE1lida${plural4}: ${joinValues(issue2.keys, ", ")}`;
21557
21557
  }
21558
21558
  case "invalid_key":
21559
21559
  return `Entrada inv\xE1lida n${translateOriginWithArticle(issue2.origin, "definite")}`;
@@ -29789,8 +29789,8 @@ function convertBaseSchema(schema, ctx) {
29789
29789
  });
29790
29790
  }
29791
29791
  ctx.processing.add(refPath);
29792
- const resolved = resolveRef(refPath, ctx);
29793
- const zodSchema2 = convertSchema(resolved, ctx);
29792
+ const resolved2 = resolveRef(refPath, ctx);
29793
+ const zodSchema2 = convertSchema(resolved2, ctx);
29794
29794
  ctx.refs.set(refPath, zodSchema2);
29795
29795
  ctx.processing.delete(refPath);
29796
29796
  return zodSchema2;
@@ -30572,12 +30572,13 @@ var PLATFORM_TOKEN_PREFIXES = {
30572
30572
  finv: "organization invitation token",
30573
30573
  fsfa: "pending second-factor sign-in",
30574
30574
  fbkc: "Buildkite checkout token",
30575
- fsvw: "staff read-only view"
30575
+ fsvw: "staff read-only view",
30576
+ fcrt: "container registry token"
30576
30577
  };
30577
30578
  var PLATFORM_TOKEN_RE = new RegExp(`^(${Object.keys(PLATFORM_TOKEN_PREFIXES).join("|")})_[0-9a-f]{64}$`);
30578
30579
 
30579
30580
  // packages/domain/src/capabilities.ts
30580
- var VIEWER = ["org:read", "members:read", "repo:read", "task:read", "change:read", "session:read", "packages:read"];
30581
+ var VIEWER = ["org:read", "members:read", "repo:read", "task:read", "change:read", "session:read", "packages:read", "migrations:read"];
30581
30582
  var REVIEWER = [...VIEWER, "review:comment", "review:resolve", "review:approve"];
30582
30583
  var DEVELOPER = [
30583
30584
  ...VIEWER,
@@ -30590,9 +30591,11 @@ var DEVELOPER = [
30590
30591
  "review:comment",
30591
30592
  "evidence:report",
30592
30593
  "promotion:request",
30593
- "packages:write"
30594
+ "packages:write",
30595
+ "deploy:read"
30594
30596
  ];
30595
- var MAINTAINER = [...DEVELOPER, ...REVIEWER, "repo:admin", "policy:write", "session:manage", "secret_scanning:read", "secret_scanning:resolve"];
30597
+ var DEPLOY_AND_MIGRATIONS = ["deploy:write", "deploy:trigger", "deploy:manage", "migrations:manage", "migrations:approve"];
30598
+ var MAINTAINER = [...DEVELOPER, ...REVIEWER, "repo:admin", ...DEPLOY_AND_MIGRATIONS, "policy:write", "session:manage", "secret_scanning:read", "secret_scanning:resolve"];
30596
30599
  var ADMIN = [
30597
30600
  ...MAINTAINER,
30598
30601
  "org:admin",
@@ -31545,15 +31548,15 @@ function redactText(input3, opts = {}) {
31545
31548
  hit("auth-header");
31546
31549
  return `${prefix}${REDACTED}`;
31547
31550
  });
31548
- text2 = text2.replace(ASSIGNMENT, (w, prefix, quote2, value) => {
31551
+ text2 = text2.replace(ASSIGNMENT, (w, prefix, quote3, value) => {
31549
31552
  if (isReference(value) || value.startsWith(REDACTED)) return w;
31550
31553
  hit("assignment");
31551
- return `${prefix}${quote2}${REDACTED}${quote2}`;
31554
+ return `${prefix}${quote3}${REDACTED}${quote3}`;
31552
31555
  });
31553
- text2 = text2.replace(FLAG, (w, prefix, quote2, value) => {
31556
+ text2 = text2.replace(FLAG, (w, prefix, quote3, value) => {
31554
31557
  if (isReference(value) || value.startsWith(REDACTED)) return w;
31555
31558
  hit("flag");
31556
- return `${prefix}${quote2}${REDACTED}${quote2}`;
31559
+ return `${prefix}${quote3}${REDACTED}${quote3}`;
31557
31560
  });
31558
31561
  if (opts.entropy !== false) {
31559
31562
  text2 = text2.replace(TOKENISH, (w) => {
@@ -31632,6 +31635,7 @@ var looksLikeHostedLabel = (label2) => /^brnch-hosted(?:-|$)/.test(label2);
31632
31635
  // packages/domain/src/deploy.ts
31633
31636
  var DEPLOY_LIMITS = {
31634
31637
  maxStepsPerTarget: 12,
31638
+ maxWatchPaths: 50,
31635
31639
  maxTargetsPerRepository: 10,
31636
31640
  maxCommandLength: 2e3,
31637
31641
  maxSummaryLength: 500,
@@ -31645,6 +31649,21 @@ var DEPLOY_LIMITS = {
31645
31649
  var DEPLOY_RUNNER_LABELS = HOSTED_SIZES.map((s) => HOSTED_SIZE_SPECS[s].label);
31646
31650
  var DEFAULT_DEPLOY_RUNNER = HOSTED_SIZE_SPECS.basic.label;
31647
31651
 
31652
+ // packages/domain/src/db-migrations.ts
31653
+ var DB_MIGRATION_LIMITS = {
31654
+ sourcesPerRepo: 20,
31655
+ /** Migration files read from one directory. */
31656
+ files: 2e3,
31657
+ fileBytes: 1024 * 1024,
31658
+ dirBytes: 16 * 1024 * 1024,
31659
+ /** Per statement; queue work is bounded by the platform's 15-minute limit. */
31660
+ statementTimeoutMaxS: 600,
31661
+ lockTimeoutMaxS: 60,
31662
+ sessionSettings: 20,
31663
+ /** How long a worker holds a run before another may take it over. */
31664
+ leaseMs: 15 * 6e4
31665
+ };
31666
+
31648
31667
  // packages/domain/src/isolated-preview.ts
31649
31668
  function parseJsonc(text2) {
31650
31669
  let out = "";
@@ -31683,6 +31702,8 @@ var DELEGATION_MATRIX = Object.freeze({
31683
31702
  "cloudflare/resources": { userToOrg: false, userToRepo: false, orgToRepo: true },
31684
31703
  "cloudflare/d1_migrations": { userToOrg: false, userToRepo: false, orgToRepo: true },
31685
31704
  "cloudflare/secrets_store": { userToOrg: false, userToRepo: false, orgToRepo: true },
31705
+ // Database migrations (ADR-0084) apply one repository's migration directory to the PlanetScale database it is bound to.
31706
+ "planetscale/db_migrations": { userToOrg: false, userToRepo: false, orgToRepo: true },
31686
31707
  // A secret source is read by a repository's workflow runs.
31687
31708
  "infisical/secrets_source": { userToOrg: false, userToRepo: false, orgToRepo: true },
31688
31709
  // AI code review keys (ADR-0051): an organization's key reviews the changes of its repositories.
@@ -31734,7 +31755,7 @@ var PLANS = Object.freeze({
31734
31755
  monthly_cents: 800,
31735
31756
  annual_cents: 8e3,
31736
31757
  per_seat: false,
31737
- features: ["private_review_rules", "ai_code_review_services", "hosted_delegation", "cloudflare_previews", "cloudflare_d1_migrations", "assistant"],
31758
+ features: ["private_review_rules", "ai_code_review_services", "hosted_delegation", "cloudflare_previews", "cloudflare_d1_migrations", "database_migrations", "assistant"],
31738
31759
  minutes_base: 5e3,
31739
31760
  minutes_per_seat: 0,
31740
31761
  concurrent_hosted_jobs: 4,
@@ -31753,7 +31774,7 @@ var PLANS = Object.freeze({
31753
31774
  monthly_cents: 1e3,
31754
31775
  annual_cents: 9600,
31755
31776
  per_seat: true,
31756
- features: ["private_review_rules", "private_members_unlimited", "ai_code_review_services", "hosted_delegation", "cloudflare_previews", "cloudflare_d1_migrations", "premium_ai_models", "assistant", "assistant_proactive"],
31777
+ features: ["private_review_rules", "private_members_unlimited", "ai_code_review_services", "hosted_delegation", "cloudflare_previews", "cloudflare_d1_migrations", "database_migrations", "premium_ai_models", "assistant", "assistant_proactive"],
31757
31778
  minutes_base: 3e3,
31758
31779
  minutes_per_seat: 1e3,
31759
31780
  concurrent_hosted_jobs: 10,
@@ -31772,7 +31793,7 @@ var PLANS = Object.freeze({
31772
31793
  monthly_cents: null,
31773
31794
  annual_cents: null,
31774
31795
  per_seat: true,
31775
- features: ["private_review_rules", "private_members_unlimited", "ai_code_review_services", "hosted_delegation", "cloudflare_previews", "cloudflare_d1_migrations", "premium_ai_models", "assistant", "assistant_proactive"],
31796
+ features: ["private_review_rules", "private_members_unlimited", "ai_code_review_services", "hosted_delegation", "cloudflare_previews", "cloudflare_d1_migrations", "database_migrations", "premium_ai_models", "assistant", "assistant_proactive"],
31776
31797
  minutes_base: 5e4,
31777
31798
  minutes_per_seat: 0,
31778
31799
  concurrent_hosted_jobs: 50,
@@ -32140,12 +32161,16 @@ var CreateInvitationRequest = external_exports.object({ email: external_exports.
32140
32161
  var AcceptInvitationRequest = external_exports.object({ token: external_exports.string() });
32141
32162
  var CreateTeamRequest = external_exports.object({ slug: Slug, name: displayLine().min(1).max(100) });
32142
32163
  var AddTeamMemberRequest = external_exports.object({ user_id: external_exports.string() });
32164
+ var GrantExtraCapabilitySchema = external_exports.enum(["deploy:read", "deploy:trigger", "deploy:manage", "migrations:read", "migrations:manage", "migrations:approve"]);
32143
32165
  var CreateGrantRequest = external_exports.object({
32144
32166
  principal_type: external_exports.enum(["user", "team"]),
32145
32167
  principal_id: external_exports.string().min(1).max(100),
32146
32168
  role: RepoGrantRoleSchema,
32169
+ /** Beyond the role, e.g. a developers team that approves a database's migrations. */
32170
+ capabilities: external_exports.array(GrantExtraCapabilitySchema).max(6).optional(),
32147
32171
  expires_at: external_exports.iso.datetime().nullable().optional()
32148
32172
  });
32173
+ var UpdateGrantRequest = external_exports.object({ capabilities: external_exports.array(GrantExtraCapabilitySchema).max(6) });
32149
32174
  var CreateRepoRequest = external_exports.object({
32150
32175
  name: RepoName,
32151
32176
  description: displayText().max(500).default(""),
@@ -32234,11 +32259,16 @@ var PolicyRulesSchema = external_exports.object({
32234
32259
  });
32235
32260
  var AdoptPolicyRequest = external_exports.object({ rules: PolicyRulesSchema, expected_version: external_exports.number().int().nonnegative(), note: external_exports.string().max(1e3).optional() });
32236
32261
  var ApplySecureDefaultsRequest = external_exports.object({ expected_version: external_exports.number().int().nonnegative() });
32262
+ var MAX_TASK_BLOCKERS = 50;
32263
+ var TaskRef = external_exports.union([external_exports.number().int().positive(), external_exports.string().trim().regex(/^(task_[0-9a-z]{1,64}|#?\d{1,9})$/, "expected a task number (12 or #12) or a task_\u2026 id")]);
32264
+ var blockedBy = external_exports.array(TaskRef).max(MAX_TASK_BLOCKERS);
32237
32265
  var CreateTaskRequest = external_exports.object({
32238
32266
  title: displayLine().min(1).max(300),
32239
32267
  description: external_exports.string().max(65536).default(""),
32240
32268
  acceptance_criteria: external_exports.array(external_exports.string().max(1e3)).max(100).default([]),
32241
- labels: external_exports.array(external_exports.string().max(50)).max(20).default([])
32269
+ labels: external_exports.array(external_exports.string().max(50)).max(20).default([]),
32270
+ /** Tasks in the same repository this one is blocked by. */
32271
+ blocked_by: blockedBy.default([])
32242
32272
  });
32243
32273
  var UpdateTaskRequest = external_exports.object({
32244
32274
  title: displayLine().min(1).max(300).optional(),
@@ -32247,8 +32277,13 @@ var UpdateTaskRequest = external_exports.object({
32247
32277
  labels: external_exports.array(external_exports.string().max(50)).max(20).optional(),
32248
32278
  state: external_exports.enum(["open", "in_progress", "blocked", "done", "cancelled"]).optional(),
32249
32279
  owner_id: external_exports.string().nullable().optional(),
32280
+ /** Replaces the whole set of blockers. Links are not task fields: changing them alone leaves `version` as it is. */
32281
+ blocked_by: blockedBy.optional(),
32250
32282
  version: external_exports.number().int()
32251
32283
  });
32284
+ var AddTaskDependencyRequest = external_exports.object({ blocker: TaskRef });
32285
+ var TaskListQuery = external_exports.object({ ready: external_exports.enum(["true", "false"]).optional().transform((v) => v === void 0 ? void 0 : v === "true") });
32286
+ var TaskNumberParam = external_exports.coerce.number().int().positive().max(999999999);
32252
32287
  var ClaimTaskRequest = external_exports.object({ session_id: external_exports.string().optional() });
32253
32288
  var AddLinkRequest = external_exports.object({ kind: external_exports.enum(["external", "change", "task"]), url: external_exports.url().optional(), target_id: external_exports.string().optional(), title: external_exports.string().max(300).optional() });
32254
32289
  var CreateCommentRequest = external_exports.object({ body: external_exports.string().min(1).max(65536) });
@@ -32864,6 +32899,22 @@ var ActionsSettingsRequest = external_exports.object({
32864
32899
  allow_untrusted_jobs_on_self_hosted_runners: external_exports.boolean().optional()
32865
32900
  });
32866
32901
  var RunnerLabel = external_exports.string().min(1).max(50).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "letters, digits, '.', '_' and '-'");
32902
+ var ResourceBytes = external_exports.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
32903
+ var RunnerResourceSnapshot = external_exports.object({
32904
+ cpu_percent: external_exports.number().min(0).max(100).nullable(),
32905
+ cpu_count: external_exports.number().int().min(1).max(65536),
32906
+ cpu_interval_ms: external_exports.number().int().min(1).max(6e5).nullable(),
32907
+ memory_total_bytes: ResourceBytes.positive(),
32908
+ memory_available_bytes: ResourceBytes,
32909
+ disk_total_bytes: ResourceBytes.positive().nullable(),
32910
+ disk_available_bytes: ResourceBytes.nullable(),
32911
+ uptime_seconds: external_exports.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
32912
+ job_slots: external_exports.number().int().min(1).max(32)
32913
+ }).strict().superRefine((s, ctx) => {
32914
+ if (s.memory_available_bytes > s.memory_total_bytes) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["memory_available_bytes"], message: "available memory exceeds total memory" });
32915
+ if (s.disk_total_bytes === null !== (s.disk_available_bytes === null) || s.disk_available_bytes !== null && s.disk_available_bytes > s.disk_total_bytes) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["disk_available_bytes"], message: "disk readings must be a valid pair" });
32916
+ if (s.cpu_percent === null !== (s.cpu_interval_ms === null)) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["cpu_interval_ms"], message: "CPU usage requires a sample interval" });
32917
+ });
32867
32918
  var TestRunnerRequest = external_exports.object({
32868
32919
  repository_id: external_exports.string().max(64).nullable().default(null)
32869
32920
  });
@@ -32888,8 +32939,9 @@ var PollJobsRequest = external_exports.object({
32888
32939
  wait_seconds: external_exports.number().int().min(0).max(25).default(20)
32889
32940
  });
32890
32941
  var HeartbeatRequest = external_exports.object({
32891
- running_jobs: external_exports.array(external_exports.string().max(64)).max(32).default([])
32892
- });
32942
+ running_jobs: external_exports.array(external_exports.string().max(64)).max(32).default([]),
32943
+ resources: RunnerResourceSnapshot.optional()
32944
+ }).strict();
32893
32945
  var StepUpdateRequest = external_exports.object({
32894
32946
  name: external_exports.string().max(500),
32895
32947
  status: external_exports.enum(["in_progress", "completed"]),
@@ -33618,7 +33670,7 @@ var CODING_AGENT_SESSION_SCOPES = [
33618
33670
  var EnsureCodingAgentRequest = external_exports.object({ tool: external_exports.enum(AGENT_RUNTIMES) }).strict();
33619
33671
 
33620
33672
  // packages/protocol/src/connections.ts
33621
- var CONNECTION_PURPOSES = ["impact", "secrets_store", "import", "secrets_source", "deploy", "resources", "d1_migrations", "code_review"];
33673
+ var CONNECTION_PURPOSES = ["impact", "secrets_store", "import", "secrets_source", "deploy", "resources", "d1_migrations", "db_migrations", "code_review"];
33622
33674
  var Name3 = external_exports.string().trim().min(1).max(100);
33623
33675
  var Secret2 = external_exports.string().min(1).max(4e3);
33624
33676
  var Id2 = external_exports.string().min(1).max(200).regex(/^[A-Za-z0-9._:-]+$/, "letters, digits, '.', '_', ':' and '-' only");
@@ -33657,9 +33709,10 @@ var CreateConnectionRequest = external_exports.discriminatedUnion("auth", [
33657
33709
  auth: external_exports.literal("api_token"),
33658
33710
  // Cloudflare API tokens (impact, secrets_store, deploy), GitHub fine-grained personal access tokens (import) and
33659
33711
  // AI code review keys (code_review, ADR-0051): CodeRabbit, Greptile, an OpenAI key for Codex, an Anthropic key for
33660
- // Claude Code. The provider/purpose pair is checked server-side.
33661
- provider: external_exports.enum(["cloudflare", "github", "coderabbit", "greptile", "codex", "claude"]),
33662
- purpose: external_exports.enum(["impact", "secrets_store", "import", "deploy", "resources", "d1_migrations", "code_review"]),
33712
+ // Claude Code, a PlanetScale service token (`<id>:<token>`, db_migrations). The provider/purpose pair is checked
33713
+ // server-side.
33714
+ provider: external_exports.enum(["cloudflare", "planetscale", "github", "coderabbit", "greptile", "codex", "claude"]),
33715
+ purpose: external_exports.enum(["impact", "secrets_store", "import", "deploy", "resources", "d1_migrations", "db_migrations", "code_review"]),
33663
33716
  name: Name3,
33664
33717
  credentials: external_exports.object({ api_token: Secret2 })
33665
33718
  }),
@@ -33762,6 +33815,11 @@ var targetFields = {
33762
33815
  preview_check_name: external_exports.string().trim().min(1).max(100).nullable(),
33763
33816
  /** The hosted runner size the deploy jobs run on, by its `runs-on` label: `brnch-hosted` … `brnch-hosted-4` (ADR-0066). */
33764
33817
  runs_on: external_exports.string().trim().min(1).max(40),
33818
+ /**
33819
+ * Paths whose changes deploy the target (CODEOWNERS patterns such as `apps/web/` or `packages/**`, `!` excludes; the
33820
+ * last matching pattern decides). Empty: every change deploys it.
33821
+ */
33822
+ watch_paths: external_exports.array(external_exports.string().trim().min(1).max(500)).max(50),
33765
33823
  steps: external_exports.array(DeployStepSpecBody).max(12)
33766
33824
  };
33767
33825
  var CreateDeployTargetRequest = external_exports.object({
@@ -33779,6 +33837,7 @@ var CreateDeployTargetRequest = external_exports.object({
33779
33837
  preview_agent_changes: targetFields.preview_agent_changes.default(false),
33780
33838
  preview_check_name: targetFields.preview_check_name.default(null),
33781
33839
  runs_on: targetFields.runs_on.default("brnch-hosted"),
33840
+ watch_paths: targetFields.watch_paths.default([]),
33782
33841
  steps: targetFields.steps.default([])
33783
33842
  });
33784
33843
  var UpdateDeployTargetRequest = external_exports.object(targetFields).partial().extend({
@@ -33844,6 +33903,111 @@ var UpsertWorkerEnvRequest = external_exports.object({
33844
33903
  if (v.kind === "var" && v.source !== "value") c8.addIssue({ code: "custom", message: "plain vars take a value", path: ["source"] });
33845
33904
  });
33846
33905
 
33906
+ // packages/protocol/src/db-migrations.ts
33907
+ var DB_EXEC_ORDERS = ["linear", "linear_skip", "non_linear"];
33908
+ var DB_CREDENTIAL_MODES = ["managed_role", "dsn", "branch_password"];
33909
+ var DB_APPLY_ON = ["merge", "deploy", "manual"];
33910
+ var DB_LINT_LEVELS = ["error", "warning", "off"];
33911
+ var DB_DRIFT_MODES = ["off", "on_run", "daily"];
33912
+ var DbLintPolicySchema = external_exports.object({
33913
+ levels: external_exports.record(external_exports.string().regex(/^[A-Za-z0-9_]{1,40}$/), external_exports.enum(DB_LINT_LEVELS)).refine((r) => Object.keys(r).length <= 50, "at most 50 entries").default({}),
33914
+ nolint: external_exports.enum(["honor", "ignore"]).default("honor"),
33915
+ /** Drift found by the last check fails the deploy's migrations step. */
33916
+ drift_blocks_deploy: external_exports.boolean().default(false)
33917
+ });
33918
+ var Ident2 = external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_$]{0,62}$/, "letters, digits and _ only (at most 63)");
33919
+ var PsName = external_exports.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/, "a PlanetScale name: letters, digits, '.', '_' and '-'");
33920
+ var SourceFields = {
33921
+ name: external_exports.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9_-]*$/, "lowercase letters, digits, - and _"),
33922
+ /** Directory relative to the repository root ("" = the root). */
33923
+ path: external_exports.string().max(300),
33924
+ /** `wrangler` directories are D1 sources, which come from the Wrangler configuration. */
33925
+ format: external_exports.enum(["atlas", "plain", "golang_migrate", "drizzle", "prisma"]),
33926
+ engine: external_exports.enum(["postgres", "vitess"]),
33927
+ /** Branch whose merges create runs; the repository's default branch when omitted. */
33928
+ branch: external_exports.string().min(1).max(200).optional(),
33929
+ /** PlanetScale connection (purpose `db_migrations`) of the organization or the repository. */
33930
+ connection_id: external_exports.string().min(1).max(64),
33931
+ ps_organization: PsName,
33932
+ ps_database: PsName,
33933
+ /** The production branch migrations are applied to. */
33934
+ ps_branch: PsName.default("main"),
33935
+ /** Where the format's revisions table lives (Postgres schema; Atlas default `atlas_schema_revisions`). */
33936
+ revisions_schema: Ident2.nullable().default(null),
33937
+ revisions_table: Ident2.nullable().default(null),
33938
+ /** With no revision history yet, record this version as a baseline instead of applying files up to it. */
33939
+ baseline_version: external_exports.string().min(1).max(100).nullable().default(null),
33940
+ exec_order: external_exports.enum(DB_EXEC_ORDERS).default("linear"),
33941
+ /** Postgres session settings for migration sessions (`lock_timeout`, custom `prefix.name`, …). */
33942
+ session_settings: external_exports.record(external_exports.string().max(126), external_exports.string().max(500)).default({}),
33943
+ statement_timeout_s: external_exports.number().int().min(1).max(600).default(300),
33944
+ lock_timeout_s: external_exports.number().int().min(1).max(60).default(10),
33945
+ credential_mode: external_exports.enum(DB_CREDENTIAL_MODES).optional(),
33946
+ /** Postgres: the role new objects belong to (`SET ROLE`); `postgres` by default for managed roles, none for a DSN. */
33947
+ run_as_role: external_exports.string().regex(/^[a-z_][a-z0-9_]{0,62}$/).nullable().optional(),
33948
+ /** Deprecated: `apply_on` (`approval` = `merge`, `manual_only` = `manual`). */
33949
+ on_merge: external_exports.enum(["approval", "manual_only"]).optional(),
33950
+ apply_on: external_exports.enum(DB_APPLY_ON).optional(),
33951
+ /** `deploy` and `manual` only: whether a person approves before anything is applied (`merge` always requires it). */
33952
+ require_approval: external_exports.boolean().optional(),
33953
+ /** Validate the directory on changes (evidence `migrations/<name>`). */
33954
+ change_checks: external_exports.boolean().default(true),
33955
+ lint: DbLintPolicySchema.optional(),
33956
+ drift_mode: external_exports.enum(DB_DRIFT_MODES).optional()
33957
+ };
33958
+ var CreateDbMigrationSourceRequest = external_exports.object({
33959
+ /** Only PlanetScale sources are created here: D1 sources come from the Wrangler configuration. */
33960
+ provider: external_exports.literal("planetscale").default("planetscale"),
33961
+ ...SourceFields,
33962
+ /** Postgres `credential_mode: dsn` only: `postgresql://user:password@host:5432/db?sslmode=verify-full`. Write-only. */
33963
+ dsn: external_exports.string().min(10).max(2e3).optional()
33964
+ });
33965
+ var UpdateDbMigrationSourceRequest = external_exports.object({
33966
+ path: SourceFields.path,
33967
+ branch: SourceFields.branch,
33968
+ connection_id: SourceFields.connection_id,
33969
+ ps_organization: SourceFields.ps_organization,
33970
+ ps_database: SourceFields.ps_database,
33971
+ ps_branch: PsName,
33972
+ revisions_schema: Ident2.nullable(),
33973
+ revisions_table: Ident2.nullable(),
33974
+ baseline_version: external_exports.string().min(1).max(100).nullable(),
33975
+ exec_order: external_exports.enum(DB_EXEC_ORDERS),
33976
+ session_settings: external_exports.record(external_exports.string().max(126), external_exports.string().max(500)),
33977
+ statement_timeout_s: SourceFields.statement_timeout_s.unwrap(),
33978
+ lock_timeout_s: SourceFields.lock_timeout_s.unwrap(),
33979
+ credential_mode: external_exports.enum(DB_CREDENTIAL_MODES),
33980
+ run_as_role: external_exports.string().regex(/^[a-z_][a-z0-9_]{0,62}$/).nullable(),
33981
+ on_merge: external_exports.enum(["approval", "manual_only"]),
33982
+ apply_on: external_exports.enum(DB_APPLY_ON),
33983
+ require_approval: external_exports.boolean(),
33984
+ change_checks: external_exports.boolean(),
33985
+ lint: DbLintPolicySchema,
33986
+ drift_mode: external_exports.enum(DB_DRIFT_MODES),
33987
+ state: external_exports.enum(["active", "disabled"]),
33988
+ dsn: external_exports.string().min(10).max(2e3).nullable()
33989
+ }).partial().extend({ version: external_exports.number().int().min(1) });
33990
+ var StartDbMigrationRunRequest = external_exports.object({
33991
+ /** Branch, tag or commit to apply; the source's branch when omitted. */
33992
+ ref: external_exports.string().min(1).max(200).optional()
33993
+ });
33994
+ var DecideDbMigrationRunRequest = external_exports.object({
33995
+ reason: external_exports.string().max(500).optional(),
33996
+ /** Approving a run a deploy waits on: redeploy that deploy at the same commit once the run succeeds. */
33997
+ redeploy: external_exports.boolean().optional()
33998
+ });
33999
+
34000
+ // packages/protocol/src/approvals.ts
34001
+ var PutApprovalRuleRequest = external_exports.object({
34002
+ required: external_exports.number().int().min(1).max(10),
34003
+ users: external_exports.array(external_exports.string().min(1).max(100)).max(20).default([]),
34004
+ teams: external_exports.array(external_exports.string().min(1).max(100)).max(10).default([]),
34005
+ separation_of_duties: external_exports.boolean().default(false),
34006
+ /** The rule's version when replacing one (optimistic concurrency); omit to create. */
34007
+ version: external_exports.number().int().positive().optional()
34008
+ });
34009
+ var DecideDeploymentRequest = external_exports.object({ reason: external_exports.string().max(500).optional() });
34010
+
33847
34011
  // packages/protocol/src/buildkite.ts
33848
34012
  var BUILDKITE_TRUST_MODES = ["contributor_controlled", "pipeline_settings", "target_ref_upload"];
33849
34013
  var Slug3 = external_exports.string().trim().min(1).max(100).regex(/^[a-z0-9][a-z0-9-_]*$/i, "a Buildkite slug (letters, digits, '-' and '_')");
@@ -36185,6 +36349,8 @@ function reposGroup(t) {
36185
36349
  grants: {
36186
36350
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/grants`, query: { ...q }, options: o }),
36187
36351
  create: (repo, body, o) => t.json({ method: "POST", path: path2`/v1/repos/${repo}/grants`, json: validate2(CreateGrantRequest, body, "CreateGrantRequest"), options: o }),
36352
+ /** Replace the capabilities the grant carries beyond its role (ADR-0088), e.g. `["migrations:approve"]`. */
36353
+ update: (repo, grant, body, o) => t.json({ method: "PATCH", path: path2`/v1/repos/${repo}/grants/${grant}`, json: validate2(UpdateGrantRequest, body, "UpdateGrantRequest"), options: o }),
36188
36354
  delete: (repo, grant, o) => t.void({ method: "DELETE", path: path2`/v1/repos/${repo}/grants/${grant}`, options: o })
36189
36355
  }
36190
36356
  };
@@ -36387,9 +36553,13 @@ function delegationsGroup(t) {
36387
36553
  function tasksGroup(t) {
36388
36554
  return {
36389
36555
  create: (repo, body, o) => t.json({ method: "POST", path: path2`/v1/repos/${repo}/tasks`, json: validate2(CreateTaskRequest, body, "CreateTaskRequest"), options: o }),
36390
- /** `label` (name or id) and `assignee` (handle or user id) filter the list. */
36556
+ /**
36557
+ * `label` (name or id) and `assignee` (handle or user id) filter the list. `ready: true` keeps only the frontier:
36558
+ * open tasks whose blockers are all done or cancelled (`false`: every other task).
36559
+ */
36391
36560
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/tasks`, query: { ...q }, options: o }),
36392
36561
  get: (task, o) => t.json({ method: "GET", path: path2`/v1/tasks/${task}`, options: o }),
36562
+ byNumber: (repo, number4, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/tasks/by-number/${number4}`, options: o }),
36393
36563
  /** `version` is required: a stale version fails with VERSION_CONFLICT instead of overwriting someone else's edit. */
36394
36564
  update: (task, body, o) => t.json({ method: "PATCH", path: path2`/v1/tasks/${task}`, json: validate2(UpdateTaskRequest, body, "UpdateTaskRequest"), options: o }),
36395
36565
  /** Advisory claim; CONFLICT when someone else holds it. */
@@ -36399,10 +36569,64 @@ function tasksGroup(t) {
36399
36569
  create: (task, body, o) => t.json({ method: "POST", path: path2`/v1/tasks/${task}/comments`, json: validate2(CreateCommentRequest, body, "CreateCommentRequest"), options: o }),
36400
36570
  list: (task, q = {}, o) => t.json({ method: "GET", path: path2`/v1/tasks/${task}/comments`, query: { ...q }, options: o })
36401
36571
  },
36572
+ /** "Blocked by" links within the task's repository (ADR-0089). A blocker is a task number or `task_…` id. */
36573
+ dependencies: {
36574
+ list: (task, o) => t.json({ method: "GET", path: path2`/v1/tasks/${task}/dependencies`, options: o }),
36575
+ /** Idempotent: adding an existing link changes nothing. CYCLE_DETECTED if the blocker already waits on this task. */
36576
+ add: (task, body, o) => t.json({ method: "POST", path: path2`/v1/tasks/${task}/dependencies`, json: validate2(AddTaskDependencyRequest, body, "AddTaskDependencyRequest"), options: o }),
36577
+ remove: (task, blocker, o) => t.void({ method: "DELETE", path: path2`/v1/tasks/${task}/dependencies/${String(blocker)}`, options: o })
36578
+ },
36402
36579
  addLink: (task, body, o) => t.json({ method: "POST", path: path2`/v1/tasks/${task}/links`, json: validate2(AddLinkRequest, body, "AddLinkRequest"), options: o })
36403
36580
  };
36404
36581
  }
36405
36582
 
36583
+ // packages/sdk/src/groups/db-migrations.ts
36584
+ function dbMigrationsGroup(t) {
36585
+ const decide = (decision) => (id2, reason, o) => {
36586
+ const body = typeof reason === "string" ? { reason } : { ...reason?.reason ? { reason: reason.reason } : {}, ...reason?.redeploy ? { redeploy: true } : {} };
36587
+ return t.json({ method: "POST", path: path2`/v1/db-migration-runs/${id2}/${decision}`, json: validate2(DecideDbMigrationRunRequest, body, "DecideDbMigrationRunRequest"), options: o });
36588
+ };
36589
+ return {
36590
+ sources: {
36591
+ list: (repo, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/db-migrations/sources`, options: o }),
36592
+ create: (repo, body, o) => t.json({ method: "POST", path: path2`/v1/repos/${repo}/db-migrations/sources`, json: validate2(CreateDbMigrationSourceRequest, body, "CreateDbMigrationSourceRequest"), options: o }),
36593
+ get: (id2, o) => t.json({ method: "GET", path: path2`/v1/db-migration-sources/${id2}`, options: o }),
36594
+ update: (id2, body, o) => t.json({ method: "PATCH", path: path2`/v1/db-migration-sources/${id2}`, json: validate2(UpdateDbMigrationSourceRequest, body, "UpdateDbMigrationSourceRequest"), options: o }),
36595
+ delete: (id2, o) => t.void({ method: "DELETE", path: path2`/v1/db-migration-sources/${id2}`, options: o }),
36596
+ /** Who may approve the source's runs now (ADR-0088). */
36597
+ approvers: (id2, o) => t.json({ method: "GET", path: path2`/v1/db-migration-sources/${id2}/approvers`, options: o }),
36598
+ /** Protect the database: every run waits for these approvals, and it can no longer apply without approval. */
36599
+ protect: (id2, body, o) => t.json({ method: "PUT", path: path2`/v1/db-migration-sources/${id2}/approval-rule`, json: validate2(PutApprovalRuleRequest, body, "PutApprovalRuleRequest"), options: o }),
36600
+ unprotect: (id2, o) => t.json({ method: "DELETE", path: path2`/v1/db-migration-sources/${id2}/approval-rule`, options: o })
36601
+ },
36602
+ /** Migration directories found in the repository tree; `configured` marks those a source already covers. */
36603
+ suggestions: (repo, ref, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/db-migrations/suggestions`, query: ref ? { ref } : {}, options: o }),
36604
+ /** What applying the source at `ref` would do. `live` reads the database now instead of the last snapshot. */
36605
+ plan: (sourceId2, q = {}, o) => t.json({ method: "GET", path: path2`/v1/db-migration-sources/${sourceId2}/plan`, query: { ref: q.ref, live: q.live ? "true" : void 0 }, options: o }),
36606
+ /** The schema the source's migrations produce at `ref` (the source's branch by default), from replaying them. */
36607
+ schema: (sourceId2, ref, o) => t.json({ method: "GET", path: path2`/v1/db-migration-sources/${sourceId2}/schema`, query: ref ? { ref } : {}, options: o }),
36608
+ /** Compares the database with what its applied migrations produce, now, and stores the result (admins). */
36609
+ checkDrift: (sourceId2, o) => t.json({ method: "POST", path: path2`/v1/db-migration-sources/${sourceId2}/drift`, options: o }),
36610
+ /** Reads the applied versions from the database again and returns the plan at the source's branch. */
36611
+ refresh: (sourceId2, o) => t.json({ method: "POST", path: path2`/v1/db-migration-sources/${sourceId2}/refresh`, options: o }),
36612
+ /**
36613
+ * A manual run (`migrations:manage`). The request is the approval when the caller also holds `migrations:approve` and
36614
+ * the source is not protected; otherwise the run waits for approval. `run` is null when nothing is pending.
36615
+ */
36616
+ startRun: (sourceId2, body = {}, o) => t.json({ method: "POST", path: path2`/v1/db-migration-sources/${sourceId2}/runs`, json: validate2(StartDbMigrationRunRequest, body, "StartDbMigrationRunRequest"), options: o }),
36617
+ runs: {
36618
+ list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/db-migrations/runs`, query: { ...q }, options: o }),
36619
+ get: (id2, o) => t.json({ method: "GET", path: path2`/v1/db-migration-runs/${id2}`, options: o })
36620
+ },
36621
+ approve: decide("approve"),
36622
+ reject: decide("reject"),
36623
+ retry: decide("retry"),
36624
+ cancel: decide("cancel"),
36625
+ /** The plan of every source a change touches: the files the change adds (with statements), lint, schema effect. */
36626
+ changePlans: (changeId, o) => t.json({ method: "GET", path: path2`/v1/changes/${changeId}/db-migrations`, options: o })
36627
+ };
36628
+ }
36629
+
36406
36630
  // packages/sdk/src/groups/deploy.ts
36407
36631
  function deployGroup(t) {
36408
36632
  return {
@@ -36420,12 +36644,22 @@ function deployGroup(t) {
36420
36644
  remove: (id2, o) => t.json({ method: "DELETE", path: path2`/v1/deploy-targets/${id2}`, options: o }),
36421
36645
  /** Deploy the branch head again (or a commit this target deployed before). */
36422
36646
  deploy: (id2, body = {}, o) => t.json({ method: "POST", path: path2`/v1/deploy-targets/${id2}/deploy`, json: validate2(RedeployRequest, body, "RedeployRequest"), options: o }),
36423
- rollback: (id2, body, o) => t.json({ method: "POST", path: path2`/v1/deploy-targets/${id2}/rollback`, json: validate2(RollbackRequest, body, "RollbackRequest"), options: o })
36647
+ rollback: (id2, body, o) => t.json({ method: "POST", path: path2`/v1/deploy-targets/${id2}/rollback`, json: validate2(RollbackRequest, body, "RollbackRequest"), options: o }),
36648
+ /** Who may approve the target's production deploys now (ADR-0088). */
36649
+ approvers: (id2, o) => t.json({ method: "GET", path: path2`/v1/deploy-targets/${id2}/approvers`, options: o }),
36650
+ /** Protect the target: production deploys and rollbacks wait for these approvals. Pass `version` to replace a rule. */
36651
+ protect: (id2, body, o) => t.json({ method: "PUT", path: path2`/v1/deploy-targets/${id2}/approval-rule`, json: validate2(PutApprovalRuleRequest, body, "PutApprovalRuleRequest"), options: o }),
36652
+ /** Remove the rule; deploys waiting for it go on. */
36653
+ unprotect: (id2, o) => t.json({ method: "DELETE", path: path2`/v1/deploy-targets/${id2}/approval-rule`, options: o })
36424
36654
  },
36425
36655
  deployments: {
36426
36656
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/deployments`, query: { ...q }, options: o }),
36427
36657
  get: (id2, o) => t.json({ method: "GET", path: path2`/v1/deployments/${id2}`, options: o }),
36428
36658
  cancel: (id2, o) => t.json({ method: "POST", path: path2`/v1/deployments/${id2}/cancel`, options: o }),
36659
+ /** Approve a deploy of a protected target (people only; the last approval needed starts it). */
36660
+ approve: (id2, o) => t.json({ method: "POST", path: path2`/v1/deployments/${id2}/approve`, json: {}, options: o }),
36661
+ /** Reject it: the deployment is cancelled. */
36662
+ reject: (id2, reason, o) => t.json({ method: "POST", path: path2`/v1/deployments/${id2}/reject`, json: validate2(DecideDeploymentRequest, reason ? { reason } : {}, "DecideDeploymentRequest"), options: o }),
36429
36663
  ofChange: (change, o) => t.json({ method: "GET", path: path2`/v1/changes/${change}/deployments`, options: o })
36430
36664
  }
36431
36665
  };
@@ -36475,6 +36709,8 @@ function packagesGroup(t) {
36475
36709
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/packages`, query: { ...q }, options: o }),
36476
36710
  get: (image, o) => t.json({ method: "GET", path: path2`/v1/packages/${image}`, options: o }),
36477
36711
  versions: (image, q = {}, o) => t.json({ method: "GET", path: path2`/v1/packages/${image}/versions`, query: { ...q }, options: o }),
36712
+ /** Deletes an image with no versions left (`CONFLICT` otherwise). */
36713
+ delete: (image, o) => t.void({ method: "DELETE", path: path2`/v1/packages/${image}`, options: o }),
36478
36714
  deleteVersion: (image, digest, o) => t.void({ method: "DELETE", path: path2`/v1/packages/${image}/versions/${digest}`, options: o }),
36479
36715
  deleteTag: (image, tag, o) => t.void({ method: "DELETE", path: path2`/v1/packages/${image}/tags/${tag}`, options: o }),
36480
36716
  usage: (org, o) => t.json({ method: "GET", path: path2`/v1/orgs/${org}/registry`, options: o }),
@@ -36569,6 +36805,7 @@ function createClient(options) {
36569
36805
  secretSyncs: secretSyncsGroup(t),
36570
36806
  grants: grantsGroup(t),
36571
36807
  deploy: deployGroup(t),
36808
+ dbMigrations: dbMigrationsGroup(t),
36572
36809
  codeReview: codeReviewGroup(t),
36573
36810
  releases: releasesGroup(t),
36574
36811
  packages: packagesGroup(t),
@@ -36901,7 +37138,7 @@ function invalidAgentToolFlag(flag) {
36901
37138
  }
36902
37139
 
36903
37140
  // packages/cli/package.json
36904
- var version2 = "0.18.2";
37141
+ var version2 = "0.19.0";
36905
37142
 
36906
37143
  // packages/cli/src/brand.ts
36907
37144
  var BRAND = {
@@ -38638,18 +38875,13 @@ async function gitConfigPath(deps) {
38638
38875
  }
38639
38876
 
38640
38877
  // packages/cli/src/work-ref.ts
38641
- var MAX_PAGES = 20;
38642
- var PAGE = 100;
38643
38878
  async function issueByNumber(client, repo, n) {
38644
- let cursor2;
38645
- for (let i = 0; i < MAX_PAGES; i++) {
38646
- const page = await client.tasks.list(repo, { limit: PAGE, ...cursor2 ? { cursor: cursor2 } : {} });
38647
- const hit = page.items.find((t) => t.number === n);
38648
- if (hit) return hit.id;
38649
- if (!page.next_cursor) return null;
38650
- cursor2 = page.next_cursor;
38879
+ try {
38880
+ return (await client.tasks.byNumber(repo, n)).id;
38881
+ } catch (e) {
38882
+ if (isBrnchApiError(e) && e.code === "NOT_FOUND") return null;
38883
+ throw e;
38651
38884
  }
38652
- return null;
38653
38885
  }
38654
38886
  async function changeByNumber(client, repo, n) {
38655
38887
  try {
@@ -38675,6 +38907,21 @@ async function resolveChangeId(spec, client, repo) {
38675
38907
  if (!id2) throw notFound(`change #${n}`, repo.spec());
38676
38908
  return id2;
38677
38909
  }
38910
+ var TASK_ARG = "task number (12 or #12) in --repo, or a task_\u2026 id";
38911
+ async function resolveTaskId(spec, client, repo) {
38912
+ const text2 = spec.trim();
38913
+ const rest = text2.startsWith("issue:") ? text2.slice("issue:".length) : text2;
38914
+ if (rest.startsWith("task_")) return rest;
38915
+ const digits = /^#?(\d{1,9})$/.exec(rest)?.[1];
38916
+ if (!digits) {
38917
+ if (rest.startsWith("chg_") || rest.startsWith("change:")) throw usage(`${spec} is a change, not a task`);
38918
+ throw usage(`cannot read task "${spec}": expected a number (12 or #12) or a task_\u2026 id`);
38919
+ }
38920
+ const n = Number(digits);
38921
+ const id2 = await issueByNumber(client, await repo.id(), n);
38922
+ if (!id2) throw notFound(`task #${n}`, repo.spec());
38923
+ return id2;
38924
+ }
38678
38925
  async function resolveWorkRef(spec, client, repoId) {
38679
38926
  const m = /^(issue|change):(.+)$/.exec(spec.trim());
38680
38927
  const kind = m?.[1] ?? null;
@@ -38972,6 +39219,67 @@ ${final.join("\n")}
38972
39219
  deps.stderr.write("\n");
38973
39220
  }
38974
39221
 
39222
+ // packages/cli/src/program-status.ts
39223
+ var MSG_MAX_BYTES = 512;
39224
+ function encodeMsg(text2) {
39225
+ let line2 = text2.replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ").trim();
39226
+ const enc3 = new TextEncoder();
39227
+ while (enc3.encode(line2).length > MSG_MAX_BYTES) line2 = [...line2].slice(0, -1).join("");
39228
+ return Buffer.from(line2, "utf8").toString("base64");
39229
+ }
39230
+ function programStatus(r) {
39231
+ const pairs = [`state=${r.state}`, `app=${BRAND.cli}`];
39232
+ if (r.state === "blocked") pairs.push(`kind=${r.kind}`);
39233
+ if (r.state === "working" && r.progress !== void 0 && Number.isFinite(r.progress)) pairs.push(`progress=${Math.min(100, Math.max(0, Math.round(r.progress)))}`);
39234
+ if (r.msg) pairs.push(`msg=${encodeMsg(r.msg)}`);
39235
+ return `\x1B]7501;${pairs.join(":")}\x1B\\`;
39236
+ }
39237
+ function programStatusEnabled(deps, agent) {
39238
+ const off = deps.env[envName("PROGRAM_STATUS")]?.trim().toLowerCase();
39239
+ if (off === "0" || off === "false" || off === "off" || off === "no") return false;
39240
+ return deps.stderrIsTTY && !agent && !deps.env.CI && deps.env.TERM !== "dumb";
39241
+ }
39242
+ var silentStatus = {
39243
+ working: () => {
39244
+ },
39245
+ blocked: () => {
39246
+ },
39247
+ ask: () => () => {
39248
+ },
39249
+ finish: () => {
39250
+ }
39251
+ };
39252
+ function statusReporter(out) {
39253
+ let current = null;
39254
+ let waited = false;
39255
+ let last = "";
39256
+ const send = (r) => {
39257
+ const s = programStatus(r);
39258
+ if (s === last) return;
39259
+ last = s;
39260
+ out.write(s);
39261
+ };
39262
+ const report3 = (r) => {
39263
+ current = r;
39264
+ waited = true;
39265
+ send(r);
39266
+ };
39267
+ return {
39268
+ working: (msg, progress) => report3({ state: "working", msg, ...progress !== void 0 ? { progress } : {} }),
39269
+ blocked: (kind, msg) => report3({ state: "blocked", kind, msg }),
39270
+ ask() {
39271
+ send({ state: "blocked", kind: "question", msg: "Waiting for your answer" });
39272
+ return () => send(current ?? { state: "working" });
39273
+ },
39274
+ finish(ok, msg) {
39275
+ if (!waited) return;
39276
+ send({ state: ok ? "done" : "error", msg });
39277
+ waited = false;
39278
+ current = null;
39279
+ }
39280
+ };
39281
+ }
39282
+
38975
39283
  // packages/cli/src/context.ts
38976
39284
  async function findProjectFile(deps) {
38977
39285
  let dir = deps.cwd;
@@ -39239,6 +39547,9 @@ async function createContext(deps, globals, state2) {
39239
39547
  function changeId(spec, pref = "agent") {
39240
39548
  return resolveChangeId(spec, client(pref), { id: () => repoId(void 0, pref), spec: () => repoSpec() });
39241
39549
  }
39550
+ function taskId(spec, pref = "agent") {
39551
+ return resolveTaskId(spec, client(pref), { id: () => repoId(void 0, pref), spec: () => repoSpec() });
39552
+ }
39242
39553
  function org() {
39243
39554
  const o = targetOrg ?? storedCred?.account?.default_org;
39244
39555
  if (!o) {
@@ -39309,6 +39620,7 @@ async function createContext(deps, globals, state2) {
39309
39620
  repoSpec,
39310
39621
  repoId,
39311
39622
  changeId,
39623
+ taskId,
39312
39624
  org,
39313
39625
  sessionId,
39314
39626
  info,
@@ -39316,6 +39628,8 @@ async function createContext(deps, globals, state2) {
39316
39628
  busy: (label2) => {
39317
39629
  if (motion) state2.spinner?.start(label2);
39318
39630
  },
39631
+ /** Tell the terminal what the command waits on (program-status.ts): the server (`working`) or the person (`blocked`). */
39632
+ status: state2.status ?? silentStatus,
39319
39633
  /** Clear the spinner before handing the terminal to something that does not write through `deps` (rt.invoke). */
39320
39634
  idle: () => state2.spinner?.stop(),
39321
39635
  result,
@@ -39426,6 +39740,15 @@ function cell(v) {
39426
39740
  if (typeof v === "number" || typeof v === "boolean") return String(v);
39427
39741
  return JSON.stringify(v);
39428
39742
  }
39743
+ function actorLabel(a) {
39744
+ const person = a.handle ? `@${a.handle}` : a.user_name ?? null;
39745
+ if (a.type === "human") return person ?? a.display_name ?? `${a.type} ${a.id}`;
39746
+ if (a.type === "agent") {
39747
+ const name = a.display_name ?? `agent ${a.id}`;
39748
+ return person ? `${name} (agent for ${person})` : name;
39749
+ }
39750
+ return a.display_name ?? `${a.type} ${a.id}`;
39751
+ }
39429
39752
  var tildePath = (path3, home2) => home2 && path3.startsWith(`${home2}/`) ? `~${path3.slice(home2.length)}` : path3;
39430
39753
  function kv(obj3, keys) {
39431
39754
  const o = obj3 ?? {};
@@ -39525,7 +39848,7 @@ ${snippet}
39525
39848
 
39526
39849
  // packages/cli/src/commands/session.ts
39527
39850
  var renderSession = (s) => kv(
39528
- { ...s, agent: `${s.agent.name} (${s.agent.id})`, sponsor: `${s.sponsor.type} ${s.sponsor.id}` },
39851
+ { ...s, agent: `${s.agent.name} (${s.agent.id})`, sponsor: actorLabel(s.sponsor) },
39529
39852
  ["id", "title", "agent", "state", "presence", "attention", "repository_id", "task_id", "claimed_task_id", "sponsor", "scopes", "open_blockers", "last_seen_at", "expires_at", "max_expires_at", "end_reason"]
39530
39853
  );
39531
39854
  var renderStarted = (s) => `${renderSession(s.session)}
@@ -41030,7 +41353,7 @@ function sessionLines(s, now, you) {
41030
41353
  ...s.paths.length ? [` touching: ${pathList(s.paths)}`] : []
41031
41354
  ];
41032
41355
  }
41033
- var claimLine = (c8) => ` ${c8.key} ${c8.holder.display_name ?? c8.holder.id}${c8.session_id ? ` (${c8.session_id})` : ""}${c8.change ? ` \xB7 change #${c8.change.number}` : ""}${c8.note ? ` \xB7 ${c8.note}` : ""}`;
41356
+ var claimLine = (c8) => ` ${c8.key} ${actorLabel(c8.holder)}${c8.session_id ? ` (${c8.session_id})` : ""}${c8.change ? ` \xB7 change #${c8.change.number}` : ""}${c8.note ? ` \xB7 ${c8.note}` : ""}`;
41034
41357
  function renderStatus(st, now = Date.now()) {
41035
41358
  const others = st.sessions.filter((s) => s.id !== st.you);
41036
41359
  const out = [others.length ? `${others.length} other agent session${others.length === 1 ? "" : "s"} working in this repository.` : "No other agent sessions are working in this repository."];
@@ -41585,9 +41908,9 @@ async function runHook(deps, state2, tool, eventArg) {
41585
41908
  let meta3 = await readMeta(deps.env, id2);
41586
41909
  if (!meta3) {
41587
41910
  const baseUrl = await baseUrlOf(deps);
41588
- const resolved = await resolveRepo(deps, info.cwd ?? deps.cwd, baseUrl);
41589
- if (!resolved) return;
41590
- meta3 = { client_key: clientKey, tool: t, model: null, repo: resolved.repo, repo_root: resolved.root, base_url: baseUrl, started_at: new Date(now).toISOString(), last_event_at: new Date(now).toISOString(), ended: false };
41911
+ const resolved2 = await resolveRepo(deps, info.cwd ?? deps.cwd, baseUrl);
41912
+ if (!resolved2) return;
41913
+ meta3 = { client_key: clientKey, tool: t, model: null, repo: resolved2.repo, repo_root: resolved2.root, base_url: baseUrl, started_at: new Date(now).toISOString(), last_event_at: new Date(now).toISOString(), ended: false };
41591
41914
  }
41592
41915
  if (info.model && !meta3.model) meta3.model = redactText(info.model).text.slice(0, 200);
41593
41916
  if (!meta3.model && t === "claude-code" && typeof payload.transcript_path === "string" && /^(Stop|SessionEnd)$/.test(info.event)) meta3.model = await modelFromTranscript(deps, payload.transcript_path);
@@ -41791,22 +42114,22 @@ function registerCapture(program2, rt) {
41791
42114
  act(rt, async (ctx, o, words) => {
41792
42115
  const summary = words.join(" ").trim();
41793
42116
  if (!summary) throw usage("decision text is required");
41794
- const resolved = await resolveRepo(ctx.deps, ctx.deps.cwd, ctx.baseUrl);
41795
- if (!resolved) throw usage(`this directory is not a repository hosted on ${ctx.baseUrl}`);
42117
+ const resolved2 = await resolveRepo(ctx.deps, ctx.deps.cwd, ctx.baseUrl);
42118
+ if (!resolved2) throw usage(`this directory is not a repository hosted on ${ctx.baseUrl}`);
41796
42119
  const now = Date.now();
41797
- const live = (await listSpool(ctx.deps.env)).filter((s) => s.meta.repo_root === resolved.root && !s.meta.ended && now - Date.parse(s.meta.last_event_at) < 12 * 36e5).sort((a, b) => b.meta.last_event_at.localeCompare(a.meta.last_event_at))[0];
42120
+ const live = (await listSpool(ctx.deps.env)).filter((s) => s.meta.repo_root === resolved2.root && !s.meta.ended && now - Date.parse(s.meta.last_event_at) < 12 * 36e5).sort((a, b) => b.meta.last_event_at.localeCompare(a.meta.last_event_at))[0];
41798
42121
  let id2;
41799
42122
  let meta3;
41800
42123
  if (live) {
41801
42124
  id2 = live.id;
41802
42125
  meta3 = live.meta;
41803
42126
  } else {
41804
- const key = `manual:${spoolId(resolved.root)}:${new Date(now).toISOString().slice(0, 10)}`;
42127
+ const key = `manual:${spoolId(resolved2.root)}:${new Date(now).toISOString().slice(0, 10)}`;
41805
42128
  id2 = spoolId(key);
41806
- meta3 = await readMeta(ctx.deps.env, id2) ?? { client_key: key, tool: "manual", model: null, repo: resolved.repo, repo_root: resolved.root, base_url: ctx.baseUrl, started_at: new Date(now).toISOString(), last_event_at: new Date(now).toISOString(), ended: false };
42129
+ meta3 = await readMeta(ctx.deps.env, id2) ?? { client_key: key, tool: "manual", model: null, repo: resolved2.repo, repo_root: resolved2.root, base_url: ctx.baseUrl, started_at: new Date(now).toISOString(), last_event_at: new Date(now).toISOString(), ended: false };
41807
42130
  }
41808
42131
  const paths = (o.file ?? []).map((f) => {
41809
- const rel = relative2(canonical(resolved.root), canonical(isAbsolute6(f) ? f : resolve4(ctx.deps.cwd, f)));
42132
+ const rel = relative2(canonical(resolved2.root), canonical(isAbsolute6(f) ? f : resolve4(ctx.deps.cwd, f)));
41810
42133
  if (!rel || rel.startsWith("..") || isAbsolute6(rel)) throw usage(`${f} is outside the repository`);
41811
42134
  return rel.split(sep2).join("/");
41812
42135
  });
@@ -41850,22 +42173,22 @@ ${report3.errors.join("\n")}` : ""}`);
41850
42173
  rt.state.noUpdateHint = true;
41851
42174
  const [cmd, ...args] = argv;
41852
42175
  if (!cmd) throw usage(`usage: ${BRAND.cli} capture wrap -- <command> [args\u2026]`);
41853
- const resolved = await resolveRepo(ctx.deps, ctx.deps.cwd, ctx.baseUrl);
42176
+ const resolved2 = await resolveRepo(ctx.deps, ctx.deps.cwd, ctx.baseUrl);
41854
42177
  const cfg = await readCaptureConfig(ctx.deps.env);
41855
- const before = resolved && cfg.enabled ? await snapshotWorktree(ctx.deps, resolved.root) : null;
41856
- if (!resolved) ctx.info(`not a repository hosted on ${ctx.baseUrl}: running without capture`);
42178
+ const before = resolved2 && cfg.enabled ? await snapshotWorktree(ctx.deps, resolved2.root) : null;
42179
+ if (!resolved2) ctx.info(`not a repository hosted on ${ctx.baseUrl}: running without capture`);
41857
42180
  const code = await runInherit(cmd, args, ctx.deps.cwd, ctx.deps.env);
41858
42181
  ctx.setExit(code);
41859
- if (!resolved || !before) return;
41860
- const after = await snapshotWorktree(ctx.deps, resolved.root);
42182
+ if (!resolved2 || !before) return;
42183
+ const after = await snapshotWorktree(ctx.deps, resolved2.root);
41861
42184
  if (!after) return;
41862
- const diff = await ctx.deps.sys.run("git", ["-c", "core.hooksPath=/dev/null", "diff", "--no-color", "--no-renames", "--no-ext-diff", "-U0", before, after], { cwd: resolved.root, timeoutMs: 12e4 });
42185
+ const diff = await ctx.deps.sys.run("git", ["-c", "core.hooksPath=/dev/null", "diff", "--no-color", "--no-renames", "--no-ext-diff", "-U0", before, after], { cwd: resolved2.root, timeoutMs: 12e4 });
41863
42186
  const label2 = (o.tool ?? cmd.split(/[\\/]/).pop() ?? "command").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40);
41864
42187
  const now = Date.now();
41865
42188
  const key = `wrapped:${label2}:${randomBytes4(8).toString("hex")}`;
41866
42189
  const id2 = spoolId(key);
41867
- const meta3 = { client_key: key, tool: "wrapped", model: o.model ? redactText(o.model).text.slice(0, 200) : null, repo: resolved.repo, repo_root: resolved.root, base_url: ctx.baseUrl, started_at: new Date(now).toISOString(), last_event_at: new Date(now).toISOString(), ended: true };
41868
- const edits = await editsFromDiff(diff.stdout, { repoRoot: resolved.root, config: cfg, readFile: (p) => ctx.deps.readFile(p).catch(() => null) }, label2);
42190
+ const meta3 = { client_key: key, tool: "wrapped", model: o.model ? redactText(o.model).text.slice(0, 200) : null, repo: resolved2.repo, repo_root: resolved2.root, base_url: ctx.baseUrl, started_at: new Date(now).toISOString(), last_event_at: new Date(now).toISOString(), ended: true };
42191
+ const edits = await editsFromDiff(diff.stdout, { repoRoot: resolved2.root, config: cfg, readFile: (p) => ctx.deps.readFile(p).catch(() => null) }, label2);
41869
42192
  await writeMeta(ctx.deps.env, id2, meta3);
41870
42193
  await appendEvents(ctx.deps.env, id2, [{ type: "capture.started", payload: { source: label2 } }, ...edits, { type: "capture.ended", payload: { reason: `exit ${code}` } }], now);
41871
42194
  startFlush(ctx.deps);
@@ -42198,6 +42521,7 @@ function sameOrigin(ctx, start) {
42198
42521
  }
42199
42522
  async function awaitStepUp(ctx, start) {
42200
42523
  const deadline = Date.parse(start.expires_at);
42524
+ ctx.status.blocked("auth", `Confirm it's you in the browser (code ${start.user_code})`);
42201
42525
  for (; ; ) {
42202
42526
  await ctx.deps.sleep(start.interval * 1e3);
42203
42527
  const s = await ctx.client("user").auth.stepUp.status(start.id);
@@ -42395,6 +42719,7 @@ var accountChoice = (a) => ({
42395
42719
  });
42396
42720
  async function finishLogin(ctx, start, agentRuntime, hooks) {
42397
42721
  const oauth = ctx.client("user", { anonymous: true }).oauth;
42722
+ ctx.status.blocked("auth", `Approve the sign-in in your browser (code ${start.user_code})`);
42398
42723
  let tok;
42399
42724
  try {
42400
42725
  tok = await pollDeviceToken(oauth, start, CLI_OAUTH_CLIENT_ID, { sleep: ctx.deps.sleep });
@@ -43467,6 +43792,7 @@ var Wizard = class {
43467
43792
  /** The spinner says what the step is checking until it prints, asks, or hands over to a composed command. */
43468
43793
  busy(step2) {
43469
43794
  this.ctx.busy(STEP_BUSY[step2]);
43795
+ this.ctx.status.working(STEP_BUSY[step2]);
43470
43796
  }
43471
43797
  /** Global flags every composed command must keep (same server, credential and account). */
43472
43798
  globalArgs() {
@@ -44367,6 +44693,7 @@ var Quickstart = class {
44367
44693
  /** The board row says what is happening now (no-op without a board). */
44368
44694
  running(row, text2) {
44369
44695
  this.board?.run(row, text2);
44696
+ this.ctx.status.working(text2);
44370
44697
  }
44371
44698
  /** A step's outcome: kept for the result, and shown on the board (`show`) or as a line. */
44372
44699
  record(step2, status, detail, show2 = detail) {
@@ -44681,6 +45008,7 @@ async function stepDeploy(q, dir, ns, name, o, home2) {
44681
45008
  q.running("deploy", "waiting for Cloudflare to be connected");
44682
45009
  q.board.hint(`${todo} ${q.errPaint.dim("(Ctrl-C stops; the repository is ready either way)")}`);
44683
45010
  } else ctx.info(` ${todo}. Waiting for it (Ctrl-C stops; the repository is ready either way)\u2026`);
45011
+ ctx.status.blocked("auth", "Connect Cloudflare in the browser");
44684
45012
  const deadline2 = ctx.deps.sys.now() + o.connectTimeout * 1e3;
44685
45013
  let ready = false;
44686
45014
  while (!ready && ctx.deps.sys.now() < deadline2) {
@@ -45701,12 +46029,35 @@ function seedJson(p) {
45701
46029
  }
45702
46030
 
45703
46031
  // packages/cli/src/commands/task.ts
45704
- var renderTask = (t) => kv({ ...t, claimed_by: t.claim ? `${t.claim.actor.type} ${t.claim.actor.id}` : null }, ["id", "number", "title", "state", "owner_id", "claimed_by", "labels", "updated_at", "version"]) + (t.description ? `
46032
+ var BLOCKER_ARG = "the task it is blocked by: number (12 or #12) or task_\u2026 id";
46033
+ var resolved = (t) => t.state === "done" || t.state === "cancelled";
46034
+ var summaries = (list3) => list3.map((t) => `#${t.number} ${t.title} (${t.state})`).join("\n ") || "(none)";
46035
+ function renderDependencies(d) {
46036
+ const more = d.blocks_count > d.blocks.length ? `
46037
+ \u2026 and ${d.blocks_count - d.blocks.length} more` : "";
46038
+ return `Ready: ${d.is_ready ? "yes" : "no"}
46039
+ Blocked by:
46040
+ ${summaries(d.blocked_by)}
46041
+ Blocks:
46042
+ ${summaries(d.blocks)}${more}`;
46043
+ }
46044
+ var renderTask = (t) => kv({ ...t, claimed_by: t.claim ? actorLabel(t.claim.actor) : null }, ["id", "number", "title", "state", "owner_id", "claimed_by", "labels", "updated_at", "version"]) + (t.blocked_by?.length || t.blocks_count ? `
46045
+
46046
+ ${renderDependencies(t)}` : "") + (t.description ? `
45705
46047
 
45706
46048
  ${t.description}` : "") + (t.acceptance_criteria.length ? `
45707
46049
 
45708
46050
  Acceptance criteria:
45709
46051
  ${t.acceptance_criteria.map((c8) => ` - ${c8}`).join("\n")}` : "");
46052
+ var waitingOn = (t) => (t.blocked_by ?? []).filter((b) => !resolved(b)).map((b) => `#${b.number}`).join(" ");
46053
+ function blockersFrom(values, allowNone) {
46054
+ if (!values) return void 0;
46055
+ if (values.length === 1 && values[0] === "none") {
46056
+ if (!allowNone) throw usage("--blocked-by none only clears blockers on task update");
46057
+ return [];
46058
+ }
46059
+ return values.map((v) => v.trim().replace(/^issue:/, ""));
46060
+ }
45710
46061
  async function bodyFrom(ctx, o, required2 = true) {
45711
46062
  if (o.body !== void 0 && o.file !== void 0) throw usage("pass either --body or --file, not both");
45712
46063
  if (o.file !== void 0) return readText(fileSpec(o.file), ctx.input);
@@ -45716,23 +46067,24 @@ async function bodyFrom(ctx, o, required2 = true) {
45716
46067
  }
45717
46068
  function registerTask(program2, rt) {
45718
46069
  const task = program2.command("task").description("Tasks (issues) in --repo");
45719
- task.command("create").description("Create a task").requiredOption("--title <title>", "title").option("--body <text>", "description (Markdown; @file or - for stdin)").option("--file <path>", "read the description from a file or - for stdin").option("--criterion <text>", "acceptance criterion (repeatable)", repeat).option("--label <label>", "label (repeatable)", repeat).action(
46070
+ task.command("create").description("Create a task").requiredOption("--title <title>", "title").option("--body <text>", "description (Markdown; @file or - for stdin)").option("--file <path>", "read the description from a file or - for stdin").option("--criterion <text>", "acceptance criterion (repeatable)", repeat).option("--label <label>", "label (repeatable)", repeat).option("--blocked-by <task>", `${BLOCKER_ARG} (repeatable)`, repeat).action(
45720
46071
  act(rt, async (ctx, o) => {
45721
46072
  const description = await bodyFrom(ctx, o, false);
45722
- const t = await ctx.client().tasks.create(await ctx.repoId(), { title: o.title, description, acceptance_criteria: o.criterion, labels: o.label });
46073
+ const t = await ctx.client().tasks.create(await ctx.repoId(), { title: o.title, description, acceptance_criteria: o.criterion, labels: o.label, blocked_by: blockersFrom(o.blockedBy, false) });
45723
46074
  ctx.result(t, renderTask);
45724
46075
  })
45725
46076
  );
45726
- task.command("list").description("List tasks").option("--state <state>", "open | in_progress | blocked | done | cancelled").option("--limit <n>", "page size", (v) => parseIntOption(v, "--limit")).option("--cursor <cursor>", "page cursor").action(
46077
+ task.command("list").description("List tasks").option("--state <state>", "open | in_progress | blocked | done | cancelled").option("--ready", "only tasks ready to start: open, with every blocker done or cancelled").option("--limit <n>", "page size", (v) => parseIntOption(v, "--limit")).option("--cursor <cursor>", "page cursor").action(
45727
46078
  act(rt, async (ctx, o) => {
45728
- const page = await ctx.client().tasks.list(await ctx.repoId(), { state: o.state, limit: o.limit, cursor: o.cursor });
45729
- ctx.result(page, (v) => listWithCursor(v, ["number", "state", "title", "id"]));
46079
+ const page = await ctx.client().tasks.list(await ctx.repoId(), { state: o.state, ...o.ready ? { ready: true } : {}, limit: o.limit, cursor: o.cursor });
46080
+ ctx.result(page, (v) => listWithCursor({ ...v, items: v.items.map((t) => ({ ...t, waiting_on: waitingOn(t) })) }, ["number", "state", "title", "waiting_on", "id"]));
45730
46081
  })
45731
46082
  );
45732
- task.command("view <task>").description("Show a task").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client().tasks.get(id2), renderTask)));
45733
- task.command("update <task>").description("Update fields; uses the current version unless --expected-version pins it (then stale edits fail with exit 5)").option("--title <title>", "title").option("--body <text>", "description").option("--file <path>", "description from a file or -").option("--state <state>", "open | in_progress | blocked | done | cancelled").option("--owner <user-id>", "owner user id ('none' to clear)").option("--label <label>", "replace labels (repeatable)", repeat).option("--expected-version <n>", "expected version (optimistic concurrency)", (v) => parseIntOption(v, "--expected-version")).action(
45734
- act(rt, async (ctx, o, [id2]) => {
46083
+ task.command("view").argument("<task>", TASK_ARG).description("Show a task, with what it is blocked by and what it blocks").action(act(rt, async (ctx, _o, [spec]) => ctx.result(await ctx.client().tasks.get(await ctx.taskId(spec)), renderTask)));
46084
+ task.command("update").argument("<task>", TASK_ARG).description("Update fields; uses the current version unless --expected-version pins it (then stale edits fail with exit 5)").option("--title <title>", "title").option("--body <text>", "description").option("--file <path>", "description from a file or -").option("--state <state>", "open | in_progress | blocked | done | cancelled").option("--owner <user-id>", "owner user id ('none' to clear)").option("--label <label>", "replace labels (repeatable)", repeat).option("--blocked-by <task>", `replace the blockers: ${BLOCKER_ARG} (repeatable; 'none' to clear)`, repeat).option("--expected-version <n>", "expected version (optimistic concurrency)", (v) => parseIntOption(v, "--expected-version")).action(
46085
+ act(rt, async (ctx, o, [spec]) => {
45735
46086
  const c8 = ctx.client();
46087
+ const id2 = await ctx.taskId(spec);
45736
46088
  const version4 = o.expectedVersion ?? (await c8.tasks.get(id2)).version;
45737
46089
  const t = await c8.tasks.update(id2, {
45738
46090
  title: o.title,
@@ -45740,17 +46092,35 @@ function registerTask(program2, rt) {
45740
46092
  state: o.state,
45741
46093
  owner_id: o.owner === void 0 ? void 0 : o.owner === "none" ? null : o.owner,
45742
46094
  labels: o.label,
46095
+ blocked_by: blockersFrom(o.blockedBy, true),
45743
46096
  version: version4
45744
46097
  });
45745
46098
  ctx.result(t, renderTask);
45746
46099
  })
45747
46100
  );
45748
- task.command("claim <task>").description("Advisory claim (exit 5 if someone else holds it)").option("--session <id>", "claim on behalf of this agent session").action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client().tasks.claim(id2, { session_id: o.session }), renderTask)));
45749
- task.command("release <task>").description("Release your claim").option("--session <id>", "agent session that holds the claim").action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client().tasks.release(id2, { session_id: o.session }), renderTask)));
45750
- task.command("comment <task>").description("Add a comment").option("--body <text>", "comment (Markdown; @file or - for stdin)").option("--file <path>", "read the comment from a file or -").action(
45751
- act(rt, async (ctx, o, [id2]) => {
46101
+ task.command("depend").argument("<task>", TASK_ARG).argument("[blocker]", BLOCKER_ARG).description("Mark <task> as blocked by [blocker] (same repository); without it, show what <task> is blocked by and blocks").action(
46102
+ act(rt, async (ctx, _o, [spec, blocker]) => {
46103
+ const c8 = ctx.client();
46104
+ const id2 = await ctx.taskId(spec);
46105
+ const deps = blocker ? await c8.tasks.dependencies.add(id2, { blocker: blockersFrom([blocker], false)[0] }) : await c8.tasks.dependencies.list(id2);
46106
+ ctx.result(deps, renderDependencies);
46107
+ })
46108
+ );
46109
+ task.command("undepend").argument("<task>", TASK_ARG).argument("<blocker>", BLOCKER_ARG).description("Remove <blocker> from what <task> is blocked by").action(
46110
+ act(rt, async (ctx, _o, [spec, blocker]) => {
46111
+ const id2 = await ctx.taskId(spec);
46112
+ const ref = blockersFrom([blocker], false)[0];
46113
+ await ctx.client().tasks.dependencies.remove(id2, ref);
46114
+ ctx.result({ task_id: id2, removed: ref }, `${spec} is no longer blocked by ${ref}`);
46115
+ })
46116
+ );
46117
+ task.command("claim").argument("<task>", TASK_ARG).description("Advisory claim (exit 5 if someone else holds it)").option("--session <id>", "claim on behalf of this agent session").action(act(rt, async (ctx, o, [spec]) => ctx.result(await ctx.client().tasks.claim(await ctx.taskId(spec), { session_id: o.session }), renderTask)));
46118
+ task.command("release").argument("<task>", TASK_ARG).description("Release your claim").option("--session <id>", "agent session that holds the claim").action(act(rt, async (ctx, o, [spec]) => ctx.result(await ctx.client().tasks.release(await ctx.taskId(spec), { session_id: o.session }), renderTask)));
46119
+ task.command("comment").argument("<task>", TASK_ARG).description("Add a comment").option("--body <text>", "comment (Markdown; @file or - for stdin)").option("--file <path>", "read the comment from a file or -").action(
46120
+ act(rt, async (ctx, o, [spec]) => {
46121
+ const id2 = await ctx.taskId(spec);
45752
46122
  const comment = await ctx.client().tasks.comments.create(id2, { body: await bodyFrom(ctx, o) });
45753
- ctx.result(comment, `Comment ${comment.id} added to ${id2}`);
46123
+ ctx.result(comment, `Comment ${comment.id} added to ${spec}`);
45754
46124
  })
45755
46125
  );
45756
46126
  }
@@ -46032,7 +46402,7 @@ var how = (strategy) => strategy ? `with ${strategy}` : "with the policy's defau
46032
46402
  function renderAutoMerge(a, change, p, now) {
46033
46403
  const ref = change ? String(change.number) : a.change_id;
46034
46404
  const name = change ? `${p.bold(`#${change.number}`)} ${change.title}` : p.bold(a.change_id);
46035
- const who3 = a.enabled_by ? a.enabled_by.display_name ?? a.enabled_by.id : null;
46405
+ const who3 = a.enabled_by ? actorLabel(a.enabled_by) : null;
46036
46406
  const out = [];
46037
46407
  switch (a.state) {
46038
46408
  case "enabled":
@@ -46052,7 +46422,7 @@ function renderAutoMerge(a, change, p, now) {
46052
46422
  out.push(p.dim(` Turn it on: ${BRAND.cli} change auto-merge ${ref}`));
46053
46423
  }
46054
46424
  if (a.history.length) {
46055
- const rows2 = a.history.map((h) => ({ when: ago2(h.at, now), action: h.action, actor: h.actor.display_name ?? h.actor.id, reason: h.reason }));
46425
+ const rows2 = a.history.map((h) => ({ when: ago2(h.at, now), action: h.action, actor: actorLabel(h.actor), reason: h.reason }));
46056
46426
  const w = { when: Math.max(...rows2.map((r) => r.when.length)), action: Math.max(...rows2.map((r) => r.action.length)) };
46057
46427
  out.push("", p.bold("History"));
46058
46428
  for (const r of rows2) out.push(` ${p.dim(r.when.padEnd(w.when))} ${r.action.padEnd(w.action)} ${r.actor}${r.reason ? p.dim(` \xB7 ${r.reason}`) : ""}`);
@@ -46118,11 +46488,11 @@ function stepOf(state2, previous) {
46118
46488
 
46119
46489
  // packages/cli/src/commands/change.ts
46120
46490
  var renderChange = (c8) => kv(
46121
- { ...c8, source: c8.source.workspace_id ?? c8.source.ref, author: `${c8.author.type} ${c8.author.id}`, revision: `#${c8.current_revision.number} ${c8.current_revision.id}` },
46491
+ { ...c8, source: c8.source.workspace_id ?? c8.source.ref, author: actorLabel(c8.author), revision: `#${c8.current_revision.number} ${c8.current_revision.id}` },
46122
46492
  ["number", "id", "title", "state", "target_ref", "source", "task_id", "author", "revision", "current_revision.head_oid", "revision_count", "merged", "version"]
46123
46493
  );
46124
46494
  var renderThread = (t) => `${t.id} [${t.state}${t.blocking ? ", blocking" : ""}${t.anchor.outdated ? ", outdated" : ""}] ${t.anchor.path ?? "(change)"}${t.anchor.start_line ? `:${t.anchor.start_line}` : ""}
46125
- ` + t.comments.map((c8) => ` ${c8.author.display_name ?? c8.author.id}: ${c8.body.split("\n").join("\n ")}`).join("\n");
46495
+ ` + t.comments.map((c8) => ` ${actorLabel(c8.author)}: ${c8.body.split("\n").join("\n ")}`).join("\n");
46126
46496
  function registerChange(program2, rt) {
46127
46497
  const change = program2.command("change").description("Changes (proposed updates to a protected ref)");
46128
46498
  change.command("create").description("Create a change from a workspace or a ref in --repo").requiredOption("--title <title>", "title").option("--body <text>", "description (Markdown; @file or - for stdin)").option("--file <path>", "read the description from a file or -").option("--workspace <id>", "source workspace").option("--ref <ref>", "source ref (e.g. refs/heads/feature)").option("--target <ref>", "target ref (default: the repository's default branch)").option("--task <id>", "linked task").option("--draft", "create as draft").action(
@@ -46142,7 +46512,7 @@ function registerChange(program2, rt) {
46142
46512
  change.command("list").description("List changes in --repo").option("--state <state>", "draft | open | merged | closed").option("--limit <n>", "page size", (v) => parseIntOption(v, "--limit")).option("--cursor <cursor>", "page cursor").action(
46143
46513
  act(rt, async (ctx, o) => {
46144
46514
  const page = await ctx.client().changes.list(await ctx.repoId(), { state: o.state, limit: o.limit, cursor: o.cursor });
46145
- ctx.result(page, (v) => listWithCursor(v, ["number", "state", "title", "target_ref", "id"]));
46515
+ ctx.result(page, (v) => listWithCursor({ ...v, items: v.items.map((c8) => ({ ...c8, author: actorLabel(c8.author) })) }, ["number", "state", "title", "author", "target_ref", "id"]));
46146
46516
  })
46147
46517
  );
46148
46518
  change.command("view").argument("<change>", CHANGE_ARG).description("Show a change").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client().changes.get(await ctx.changeId(id2)), renderChange)));
@@ -46231,7 +46601,7 @@ function registerReview(program2, rt) {
46231
46601
  const id2 = await ctx.changeId(spec);
46232
46602
  const revision = o.revision ?? (await c8.changes.get(id2)).current_revision.id;
46233
46603
  const r = await c8.reviews.create(id2, { revision_id: revision, decision, body: await bodyFrom(ctx, o, bodyRequired) });
46234
- ctx.result(r, (v) => kv({ ...v, reviewer: `${v.reviewer.type} ${v.reviewer.id}` }, ["id", "decision", "revision_id", "reviewer", "current"]));
46604
+ ctx.result(r, (v) => kv({ ...v, reviewer: actorLabel(v.reviewer) }, ["id", "decision", "revision_id", "reviewer", "current"]));
46235
46605
  })
46236
46606
  );
46237
46607
  decide("approve", "approved", "Approve a revision (approvals are bound to that revision)", false);
@@ -46285,6 +46655,7 @@ async function waitDone(ctx, change, interval, timeout) {
46285
46655
  const now = f.reviews.map((r) => `${CODE_REVIEW_PROVIDER_NAMES[r.provider]}: ${r.status}`).join(", ");
46286
46656
  if (now && now !== said) ctx.info(now);
46287
46657
  said = now;
46658
+ ctx.status.working(now ? `AI review: ${now}` : "Waiting for AI review");
46288
46659
  if (!f.pending || Date.now() > deadline) return f;
46289
46660
  await ctx.deps.sleep(Math.max(1, interval) * 1e3);
46290
46661
  }
@@ -46351,7 +46722,7 @@ Wait for the findings: code-review wait ${spec}`);
46351
46722
 
46352
46723
  // packages/cli/src/commands/evidence.ts
46353
46724
  import { basename as basename4, extname, isAbsolute as isAbsolute8, resolve as resolve8 } from "node:path";
46354
- var renderEvidence = (e) => kv({ ...e, issuer: `${e.issuer.type} ${e.issuer.id}` }, ["id", "check_name", "result", "subject_kind", "subject_oid", "origin", "independence", "issuer", "summary", "attachment_ids", "revoked_at"]);
46725
+ var renderEvidence = (e) => kv({ ...e, issuer: actorLabel(e.issuer) }, ["id", "check_name", "result", "subject_kind", "subject_oid", "origin", "independence", "issuer", "summary", "attachment_ids", "revoked_at"]);
46355
46726
  var CONTENT_TYPES = { ".txt": "text/plain", ".log": "text/plain", ".json": "application/json", ".xml": "application/xml", ".html": "text/plain" };
46356
46727
  function registerEvidence(program2, rt) {
46357
46728
  const ev = program2.command("evidence").description("Verification evidence");
@@ -46450,6 +46821,7 @@ async function followLive(ctx, c8, first2, deadline, interval) {
46450
46821
  const stop = ctx.deps.ticker(250, draw2);
46451
46822
  try {
46452
46823
  while (!TERMINAL3.has(cur.state) && Date.now() < deadline) {
46824
+ ctx.status.working(`Merge ${cur.state}`);
46453
46825
  await ctx.deps.sleep(Math.max(1, interval) * 1e3);
46454
46826
  cur = await c8.promotions.get(cur.id);
46455
46827
  reached = stepOf(cur.state, reached);
@@ -46474,6 +46846,7 @@ function registerMerge(program2, rt) {
46474
46846
  let last = p.state;
46475
46847
  ctx.info(`promotion ${p.id}: ${p.state}`);
46476
46848
  while (!TERMINAL3.has(p.state) && Date.now() < deadline) {
46849
+ ctx.status.working(`Merge ${p.state}`);
46477
46850
  await ctx.deps.sleep(Math.max(1, o.interval) * 1e3);
46478
46851
  p = await c8.promotions.get(p.id);
46479
46852
  if (p.state !== last) ctx.info(`promotion ${p.id}: ${last = p.state}`);
@@ -46526,7 +46899,7 @@ var renderCoverage = (c8) => [
46526
46899
  `code owner review ${c8.required ? "required" : "not required"} by policy v${c8.policy_version} (revision ${c8.revision_id})${c8.complete ? "" : " \u2014 path list truncated"}`,
46527
46900
  table(c8.paths.map((p) => ({ ...p, owners: p.owners.join(" ") || "-", approved_by: p.approved_by.join(" ") || "-" })), ["path", "owners", "satisfied", "approved_by"])
46528
46901
  ].join("\n");
46529
- var renderAlerts = (p) => listWithCursor({ ...p, items: p.items.map((a) => ({ ...a, where: `${a.path}:${a.line}`, pushed_by: a.pushed_by.display_name ?? a.pushed_by.id })) }, ["id", "state", "rule", "where", "blob_oid", "masked", "pushed_by", "created_at"]);
46902
+ var renderAlerts = (p) => listWithCursor({ ...p, items: p.items.map((a) => ({ ...a, where: `${a.path}:${a.line}`, pushed_by: actorLabel(a.pushed_by) })) }, ["id", "state", "rule", "where", "blob_oid", "masked", "pushed_by", "created_at"]);
46530
46903
  function registerGovernance(program2, rt) {
46531
46904
  const change = program2.commands.find((c8) => c8.name() === "change") ?? program2.command("change");
46532
46905
  change.command("draft").argument("<change>", CHANGE_ARG).description("Convert an open change back to a draft (drafts cannot merge and notify nobody)").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client().changes.draft(await ctx.changeId(id2)), (c8) => kv(c8, ["id", "number", "title", "state"]))));
@@ -46616,7 +46989,7 @@ new revision #${r.revision.number} ${r.revision.id}`);
46616
46989
  }
46617
46990
  const b = await ctx.client("user").pushProtection.bypasses.create(await ctx.repoId(), { rule: o.rule, path: o.path, blob_oid: o.blob, reason: o.reason, ...principal ? { principal } : {}, ...o.ref ? { ref: o.ref } : {} });
46618
46991
  ctx.result(b, (v) => {
46619
- const who3 = v.principal ? v.principal.display_name ?? `${v.principal.type}:${v.principal.id}` : "nobody";
46992
+ const who3 = v.principal ? actorLabel(v.principal) : "nobody";
46620
46993
  return `${kv({ ...v, for: who3, ref: v.ref ?? "any" }, ["id", "rule", "path", "blob_oid", "for", "ref", "expires_at"])}
46621
46994
  Only ${who3} can use it, for one push, before it expires.`;
46622
46995
  });
@@ -46627,7 +47000,7 @@ Only ${who3} can use it, for one push, before it expires.`;
46627
47000
  rt,
46628
47001
  async (ctx, o) => ctx.result(
46629
47002
  await ctx.client().pushProtection.bypasses.list(await ctx.repoId(), { limit: o.limit, cursor: o.cursor }),
46630
- (p) => listWithCursor({ ...p, items: p.items.map((b) => ({ ...b, for: b.principal ? b.principal.display_name ?? b.principal.id : "-", created_by: b.created_by.display_name ?? b.created_by.id, used: b.used_at ? "yes" : "no" })) }, ["id", "rule", "path", "blob_oid", "for", "created_by", "used", "expires_at", "reason"])
47003
+ (p) => listWithCursor({ ...p, items: p.items.map((b) => ({ ...b, for: b.principal ? actorLabel(b.principal) : "-", created_by: actorLabel(b.created_by), used: b.used_at ? "yes" : "no" })) }, ["id", "rule", "path", "blob_oid", "for", "created_by", "used", "expires_at", "reason"])
46631
47004
  )
46632
47005
  )
46633
47006
  );
@@ -46668,7 +47041,7 @@ var renderAlerts2 = (p) => listWithCursor(
46668
47041
  ["id", "state", "resolution", "confidence", "rule", "repo", "where", "masked", "source", "created_at"]
46669
47042
  );
46670
47043
  var renderAlert = (a) => [
46671
- kv({ ...a, where: where(a), resolved_by: a.resolved_by ? a.resolved_by.display_name ?? a.resolved_by.id : null, looks_fake: a.looks_fake?.label ?? null }, [
47044
+ kv({ ...a, where: where(a), resolved_by: a.resolved_by ? actorLabel(a.resolved_by) : null, looks_fake: a.looks_fake?.label ?? null }, [
46672
47045
  "id",
46673
47046
  "state",
46674
47047
  "resolution",
@@ -46751,6 +47124,7 @@ function registerSecretScanning(program2, rt) {
46751
47124
  const c8 = ctx.client("user");
46752
47125
  let job2 = await c8.secretScanning.scans.start(repo);
46753
47126
  while (o.wait && (job2.state === "queued" || job2.state === "running")) {
47127
+ ctx.status.working(`History scan ${job2.state}`);
46754
47128
  await ctx.deps.sleep(Math.max(1, o.interval) * 1e3);
46755
47129
  job2 = await c8.secretScanning.scans.get(repo, job2.id);
46756
47130
  }
@@ -47008,8 +47382,8 @@ var renderDevices = (v) => v.items.length ? table(
47008
47382
  v.items.map((d) => ({ id: d.id, name: d.name ?? "-", environment: d.environment, app_version: d.app_version ?? "-", last_seen: d.last_seen_at, current: d.current ? "yes" : "" })),
47009
47383
  ["id", "name", "environment", "app_version", "last_seen", "current"]
47010
47384
  ) : "No push devices. The iOS app registers itself when you allow notifications.";
47011
- var PAGE2 = 100;
47012
- var MAX_PAGES2 = 10;
47385
+ var PAGE = 100;
47386
+ var MAX_PAGES = 10;
47013
47387
  async function findItems(client, specs) {
47014
47388
  const wanted = specs.map((s) => s.trim().toLowerCase());
47015
47389
  for (const s of wanted) if (s.length < 4) throw usage(`"${s}" is too short to name an inbox item: use the id \`${c4} inbox list\` shows`);
@@ -47017,8 +47391,8 @@ async function findItems(client, specs) {
47017
47391
  const seen = /* @__PURE__ */ new Set();
47018
47392
  for (const view of ["all", "snoozed", "done"]) {
47019
47393
  let cursor2;
47020
- for (let i = 0; i < MAX_PAGES2; i++) {
47021
- const page = await client.inbox.list({ view, limit: PAGE2, ...cursor2 ? { cursor: cursor2 } : {} });
47394
+ for (let i = 0; i < MAX_PAGES; i++) {
47395
+ const page = await client.inbox.list({ view, limit: PAGE, ...cursor2 ? { cursor: cursor2 } : {} });
47022
47396
  for (const item of page.items) {
47023
47397
  if (seen.has(item.id)) continue;
47024
47398
  seen.add(item.id);
@@ -47040,9 +47414,9 @@ async function findItems(client, specs) {
47040
47414
  }
47041
47415
  async function itemIds(client, specs) {
47042
47416
  const full = (s) => /^notif_[0-9a-z]{26}$/.test(s);
47043
- const short2 = specs.filter((s) => !full(s));
47044
- const items = short2.length ? await findItems(client, short2) : [];
47045
- return specs.map((s) => full(s) ? s : items[short2.indexOf(s)].id);
47417
+ const short3 = specs.filter((s) => !full(s));
47418
+ const items = short3.length ? await findItems(client, short3) : [];
47419
+ return specs.map((s) => full(s) ? s : items[short3.indexOf(s)].id);
47046
47420
  }
47047
47421
  async function repoNames(client, items) {
47048
47422
  const ids = [...new Set(items.map((i) => i.repository_id))];
@@ -47473,13 +47847,13 @@ function renderEntry(e, verb) {
47473
47847
  if (e.flags.length) lines2.push(`flagged: ${e.flags.join(", ")}${e.flags.includes("instructions") ? "; never given to the assistant on its own" : ""}`);
47474
47848
  return lines2.join("\n");
47475
47849
  }
47476
- var PAGE3 = 100;
47477
- var MAX_PAGES3 = 10;
47850
+ var PAGE2 = 100;
47851
+ var MAX_PAGES2 = 10;
47478
47852
  async function reviewable(ctx, org, q) {
47479
47853
  const out = [];
47480
47854
  let cursor2;
47481
- for (let i = 0; i < MAX_PAGES3; i++) {
47482
- const page = await ctx.client("user").assistant.memory.list(org, { ...q, status: "pending", limit: PAGE3, ...cursor2 ? { cursor: cursor2 } : {} });
47855
+ for (let i = 0; i < MAX_PAGES2; i++) {
47856
+ const page = await ctx.client("user").assistant.memory.list(org, { ...q, status: "pending", limit: PAGE2, ...cursor2 ? { cursor: cursor2 } : {} });
47483
47857
  out.push(...page.items.filter((e) => e.can_review));
47484
47858
  if (!page.next_cursor) break;
47485
47859
  cursor2 = page.next_cursor;
@@ -47787,6 +48161,329 @@ function registerIntegrations(program2, rt) {
47787
48161
  integrations.command("inherit <provider> <purpose>").description("Drop --repo's choice and inherit the nearest shared connection again").action(act(rt, async (ctx, _o, [provider, purpose]) => ctx.result(await ctx.client("user").grants.inherit(await ctx.repoId(void 0, "user"), provider, purpose), renderRepo)));
47788
48162
  }
47789
48163
 
48164
+ // packages/cli/src/commands/approvals.ts
48165
+ function protectOptions(cmd) {
48166
+ return cmd.option("--user <handle>", "a person who may approve (@handle or id; repeatable)", repeat).option("--team <slug>", "a team whose members may approve (slug or id; repeatable)", repeat).option("--required <n>", "approvals needed from different people (default 1)", (v) => parseIntOption(v, "--required")).option("--separation-of-duties", "the change's author, its merger and whoever requested the deploy or run cannot approve it");
48167
+ }
48168
+ function protectBody(o, current) {
48169
+ const users = o.user ?? [];
48170
+ const teams = o.team ?? [];
48171
+ if (!users.length && !teams.length && o.required === void 0 && !o.separationOfDuties) throw usage("give --user, --team, --required or --separation-of-duties");
48172
+ return { required: o.required ?? 1, users, teams, separation_of_duties: !!o.separationOfDuties, ...current ? { version: current.version } : {} };
48173
+ }
48174
+ var who2 = (p) => p.handle ? `@${p.handle}` : p.name;
48175
+ var ruleSummary = (r) => r ? `${r.required} approval${r.required === 1 ? "" : "s"} from ${[...r.teams.map((t) => `@${t.slug} team`), ...r.users.map(who2)].join(", ") || "anyone who may approve"}${r.separation_of_duties ? "; not the author, merger or requester" : ""}` : "not protected";
48176
+ var renderApprovers = (v) => kv(
48177
+ {
48178
+ protected: ruleSummary(v.rule),
48179
+ capability: v.capability,
48180
+ eligible: v.eligible.map(who2).join(", ") || "nobody",
48181
+ lacking_capability: v.lacking_capability.length ? `${v.lacking_capability.map(who2).join(", ")} (named, but cannot approve without ${v.capability})` : null,
48182
+ unsatisfiable: v.unsatisfiable ? "yes: fewer people can approve than it needs" : null
48183
+ },
48184
+ ["protected", "capability", "eligible", "lacking_capability", "unsatisfiable"]
48185
+ );
48186
+ var renderRule = (r) => `protected: ${ruleSummary(r)} (version ${r.version})`;
48187
+ function approvalLines(a) {
48188
+ if (!a) return "";
48189
+ const by2 = a.approvals.filter((x) => x.counted).map((x) => who2(x.user));
48190
+ const lines2 = [a.line ?? (a.satisfied ? `approved (${a.counted} of ${a.required})` : null), by2.length ? `approved by ${by2.join(", ")}` : null, a.you && !a.you.can_approve && a.line ? `you cannot approve: ${a.you.message}` : null].filter(Boolean);
48191
+ return lines2.length ? `
48192
+
48193
+ ${lines2.join("\n")}` : "";
48194
+ }
48195
+
48196
+ // packages/cli/src/commands/db.ts
48197
+ async function sourceId(ctx, ref, repo) {
48198
+ if (ref.startsWith("dms_")) return ref;
48199
+ const { items } = await ctx.client("user").dbMigrations.sources.list(repo?.id ?? await ctx.repoId(void 0, "user"));
48200
+ const s = items.find((x) => x.name === ref);
48201
+ if (!s) throw new CliError(`no migration source named "${ref}" (have: ${items.map((x) => x.name).join(", ") || "none"})`, EXIT.NOT_FOUND, "NOT_FOUND");
48202
+ return s.id;
48203
+ }
48204
+ var plural3 = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
48205
+ var short = (oid) => oid.slice(0, 12);
48206
+ var database = (s) => {
48207
+ const t = s.target;
48208
+ return t.provider === "cloudflare_d1" ? `d1:${t.binding}\u2192${t.database_name ?? t.database_id}${t.environment ? ` (env ${t.environment})` : ""}` : `${t.organization}/${t.database}@${t.branch}`;
48209
+ };
48210
+ var PS_FORMATS = ["atlas", "plain", "golang_migrate", "drizzle", "prisma"];
48211
+ var PS_ENGINES = ["postgres", "vitess"];
48212
+ var LEVELS = ["error", "warning", "off"];
48213
+ function lintLines(findings2) {
48214
+ if (!findings2?.length) return [];
48215
+ return ["lint:", ...findings2.map((f) => ` ${f.level.padEnd(12)} ${(f.atlas_code ?? f.code).padEnd(6)} ${f.file}:${f.line} ${f.message}`)];
48216
+ }
48217
+ function diffLines(d) {
48218
+ if (!d) return [];
48219
+ const t = d.tables;
48220
+ const out = [];
48221
+ for (const x of t.added) out.push(` + table ${x.name} (${x.columns.map((c8) => c8.name).join(", ")})`);
48222
+ for (const x of t.renamed) out.push(` ~ table ${x.from} \u2192 ${x.to}`);
48223
+ for (const x of t.changed) {
48224
+ for (const c8 of x.columns.added) out.push(` + ${x.name}.${c8.name} ${c8.type}${c8.nullable ? "" : " NOT NULL"}`);
48225
+ for (const c8 of x.columns.renamed) out.push(` ~ ${x.name}.${c8.from} \u2192 ${c8.to}`);
48226
+ for (const c8 of x.columns.changed) out.push(` ~ ${x.name}.${c8.name} ${c8.before.type} \u2192 ${c8.after.type}`);
48227
+ for (const c8 of x.columns.dropped) out.push(` - ${x.name}.${c8}`);
48228
+ for (const i of x.indexes.added) out.push(` + index ${i} on ${x.name}`);
48229
+ for (const i of x.indexes.dropped) out.push(` - index ${i} on ${x.name}`);
48230
+ for (const k of x.foreign_keys.added) out.push(` + foreign key ${k} on ${x.name}`);
48231
+ for (const k of x.foreign_keys.dropped) out.push(` - foreign key ${k} on ${x.name}`);
48232
+ }
48233
+ for (const x of t.dropped) out.push(` - table ${x.name}`);
48234
+ if (!out.length && !d.unparsed.length) return ["schema effect: none"];
48235
+ return ["schema effect:", ...out, ...d.unparsed.length ? [` (${plural3(d.unparsed.length, "statement")} could not be interpreted, first at ${d.unparsed[0].file}:${d.unparsed[0].line})`] : []];
48236
+ }
48237
+ function renderDrift(d) {
48238
+ if (!d) return "drift: not checked yet";
48239
+ const lines2 = [`drift: ${d.status} (checked ${d.checked_at} against ${plural3(d.applied, "applied migration")})`];
48240
+ if (d.note) lines2.push(`note: ${d.note}`);
48241
+ for (const f of d.findings) lines2.push(` ${f.kind.padEnd(20)} ${f.object} ${f.detail}`);
48242
+ return lines2.join("\n");
48243
+ }
48244
+ function issueLines(issues) {
48245
+ if (!issues.length) return [];
48246
+ return ["issues:", ...issues.map((i) => ` ${i.level.padEnd(7)} ${i.code} ${i.file ? `${i.file}: ` : ""}${i.message}`)];
48247
+ }
48248
+ var renderSources = (v) => table(
48249
+ v.items.map((s) => ({ ...s, database: database(s), applied: s.snapshot ? `${s.snapshot.applied.length} (read ${s.snapshot.read_at})` : "not read yet", drift: s.drift?.status ?? "-" })),
48250
+ ["name", "id", "provider", "path", "format", "database", "branch", "apply_on", "state", "applied", "drift"]
48251
+ );
48252
+ var renderSource = (s) => `${kv(
48253
+ { ...s, approvers: ruleSummary(s.approval_rule), database: database(s), path: s.path || "(repository root)", destructive: s.lint.levels.destructive ?? "error", nolint: s.lint.nolint, ...s.target.provider === "cloudflare_d1" ? { from: `${s.target.config_path} on ${s.branch}` } : {} },
48254
+ s.provider === "cloudflare_d1" ? ["id", "name", "provider", "path", "format", "database", "from", "branch", "revisions_location", "apply_on", "require_approval", "approvers", "change_checks", "destructive", "nolint", "drift_mode", "state", "version"] : ["id", "name", "provider", "path", "format", "engine", "database", "branch", "revisions_location", "baseline_version", "exec_order", "credential_mode", "dsn_set", "run_as_role", "apply_on", "require_approval", "approvers", "change_checks", "destructive", "nolint", "drift_mode", "state", "version"]
48255
+ )}
48256
+
48257
+ ` + (s.apply_on === "merge" ? `Merges to ${s.branch} that add migrations create a run waiting for approval: ${BRAND.cli} db runs` : s.apply_on === "deploy" ? `The production deploy's migrations step applies them${s.require_approval ? ", once the run is approved" : ""}` : `Runs start only by hand: ${BRAND.cli} db apply ${s.name}`);
48258
+ var renderSuggestions = (v) => v.items.length ? table(
48259
+ v.items.map((s) => ({ ...s, path: s.path || "(root)", configured: s.configured ? "yes" : "no" })),
48260
+ ["path", "format", "files", "revisions_schema", "baseline_version", "configured", "evidence"]
48261
+ ) : "no migration directories found";
48262
+ function renderPlan2(p) {
48263
+ const db = p.database === "live" ? "read now" : p.database === "snapshot" ? `as last read${p.read_at ? ` ${p.read_at}` : ""}` : `not read${p.note ? `: ${p.note}` : ""}`;
48264
+ const lines2 = [`${p.source_name} at ${p.ref} (${short(p.commit_oid)}): ${p.ok ? "ok" : "has errors"}`, `database: ${db}`, `files: ${p.files}, applied: ${p.applied.length}${p.applied.length ? ` (latest ${p.applied[p.applied.length - 1]})` : ""}`];
48265
+ if (p.baseline) lines2.push(`baseline: ${p.baseline}${p.no_history ? " (recorded, not applied: the database has no revision history)" : ""}`);
48266
+ else if (p.no_history && p.database !== "none") lines2.push("no revision history yet: every file is pending");
48267
+ if (p.database !== "live" && p.note) lines2.push(`note: ${p.note}`);
48268
+ if (!p.pending.length) lines2.push("nothing pending");
48269
+ else {
48270
+ lines2.push(`pending (${p.pending.length}):`);
48271
+ for (const f of p.pending) {
48272
+ const notes = [f.resume_from ? `resumes at statement ${f.resume_from + 1}` : "", f.tx_mode === "none" ? "no transaction" : "", f.introduced_by_change ? "in this change" : ""].filter(Boolean);
48273
+ lines2.push(` ${f.name} ${plural3(f.statements, "statement")}${notes.length ? ` (${notes.join(", ")})` : ""}`);
48274
+ }
48275
+ }
48276
+ return [...lines2, ...issueLines(p.issues), ...lintLines(p.lint), ...diffLines(p.schema_diff)].join("\n");
48277
+ }
48278
+ var renderPlans = (v) => v.items.length ? v.items.map(renderPlan2).join("\n\n") : "no migration sources";
48279
+ var NEXT = {
48280
+ awaiting_approval: (id2) => `Waiting for approval: ${BRAND.cli} db approve ${id2} (or reject)`,
48281
+ failed: (id2) => `Fix the migration and merge it, then: ${BRAND.cli} db retry ${id2}`,
48282
+ needs_attention: (id2) => `Look at the database (PlanetScale, or the D1 bookmark's restore command), then: ${BRAND.cli} db retry ${id2} or cancel`
48283
+ };
48284
+ function renderRun(r) {
48285
+ const ps = r.backend?.provider === "planetscale" ? r.backend : null;
48286
+ const d1 = r.backend?.provider === "cloudflare_d1" ? r.backend : null;
48287
+ const head = kv(
48288
+ {
48289
+ ...r,
48290
+ commit: short(r.commit_oid),
48291
+ change: r.change_id ?? "-",
48292
+ requested_by: r.requested_by ? `${r.requested_by.type} ${r.requested_by.id}` : "-",
48293
+ deploy_request: ps?.deploy_request_url ?? (ps?.deploy_request ? `#${ps.deploy_request}` : null),
48294
+ deployment_state: ps?.deployment_state ?? null,
48295
+ bookmark: d1?.bookmark ?? null,
48296
+ restore: d1?.restore?.wrangler ?? null
48297
+ },
48298
+ ["id", "source_name", "status", "kind", "provider", "engine", "commit", "change", "deployment_id", "baseline", "requested_by", "approved_by", "approved_at", "decision_reason", "deploy_request", "deployment_state", "bookmark", "restore", "error", "note", "created_at"]
48299
+ );
48300
+ const files = r.files.length ? table(
48301
+ r.files.map((f) => ({ ...f, statements: f.applied_statements ? `${f.applied_statements}/${f.statements}` : f.statements, error: f.error ? `${f.error}${f.error_statement ? ` (in: ${f.error_statement})` : ""}` : null })),
48302
+ ["name", "status", "statements", "execution_ms", "error"]
48303
+ ) : "no files";
48304
+ const next = NEXT[r.status]?.(r.id);
48305
+ return [head, "", files, ...r.issues.length ? ["", ...issueLines(r.issues)] : [], ...next ? ["", next] : []].join("\n") + approvalLines(r.approval);
48306
+ }
48307
+ var renderRuns = (v) => listWithCursor(
48308
+ { ...v, items: v.items.map((r) => ({ ...r, commit: short(r.commit_oid), files: `${r.files.filter((f) => f.status === "applied").length}/${r.files.length} applied` })) },
48309
+ ["id", "source_name", "status", "kind", "commit", "files", "created_at"]
48310
+ );
48311
+ var oneOf = (flag, values) => (v) => {
48312
+ if (!values.includes(v)) throw usage(`${flag} must be one of ${values.join(", ")}`);
48313
+ return v;
48314
+ };
48315
+ function sessionSettings(pairs) {
48316
+ if (!pairs) return void 0;
48317
+ const out = {};
48318
+ for (const p of pairs) {
48319
+ const eq = p.indexOf("=");
48320
+ if (eq < 1) throw usage(`--session-setting must be name=value (got "${p}")`);
48321
+ out[p.slice(0, eq)] = p.slice(eq + 1);
48322
+ }
48323
+ return out;
48324
+ }
48325
+ function registerDb(program2, rt) {
48326
+ const db = program2.command("db").description("Database migrations on PlanetScale and Cloudflare D1 for --repo: sources, plans, lint, runs applied after approval, and drift");
48327
+ const sources = db.command("sources").description("Migration sources of --repo: a directory bound to a PlanetScale database branch or a D1 database (D1 sources come from the Wrangler configuration)").action(act(rt, async (ctx) => ctx.result(await ctx.client("user").dbMigrations.sources.list(await ctx.repoId(void 0, "user")), renderSources)));
48328
+ sources.command("add").description("Add a PlanetScale migration source (migrations:manage). The source is checked against PlanetScale; `db suggest` shows the directories found. D1 sources need no adding: they come from the Wrangler configuration").requiredOption("--name <name>", "source name (lowercase letters, digits, - and _)").requiredOption("--path <dir>", 'migration directory relative to the repository root ("" for the root)').requiredOption("--format <format>", PS_FORMATS.join(" | "), oneOf("--format", PS_FORMATS)).requiredOption("--engine <engine>", PS_ENGINES.join(" | "), oneOf("--engine", PS_ENGINES)).requiredOption("--connection <connection>", "PlanetScale connection for database migrations (icn_\u2026)").requiredOption("--organization <org>", "PlanetScale organization").requiredOption("--database <db>", "PlanetScale database").option("--ps-branch <branch>", "PlanetScale branch migrations are applied to", "main").option("--branch <branch>", "repository branch whose merges create runs (default: the default branch)").option("--revisions-schema <schema>", "where the revisions table lives (Postgres schema)").option("--revisions-table <table>", "revisions table name (default: the format's own)").option("--baseline <version>", "with no revision history yet, record this version as applied instead of applying files up to it").option("--exec-order <order>", DB_EXEC_ORDERS.join(" | "), oneOf("--exec-order", DB_EXEC_ORDERS)).option("--on-merge <mode>", "deprecated: --apply-on (approval = merge, manual_only = manual)", oneOf("--on-merge", ["approval", "manual_only"])).option("--apply-on <when>", `${DB_APPLY_ON.join(" | ")} (default merge: runs wait for approval after merge)`, oneOf("--apply-on", DB_APPLY_ON)).option("--no-approval", "--apply-on deploy or manual: apply without a person's approval").option("--destructive <level>", "lint level for destructive changes: error (default, fails the check) | warning | off", oneOf("--destructive", LEVELS)).option("--drift <mode>", DB_DRIFT_MODES.join(" | "), oneOf("--drift", DB_DRIFT_MODES)).option("--credential-mode <mode>", DB_CREDENTIAL_MODES.join(" | "), oneOf("--credential-mode", DB_CREDENTIAL_MODES)).option("--run-as-role <role>", "Postgres: SET ROLE for migration sessions").option("--session-setting <name=value>", "Postgres session setting for migration sessions (repeatable)", repeat).option("--statement-timeout <seconds>", "statement timeout (at most 600)", (v) => parseIntOption(v, "--statement-timeout")).option("--lock-timeout <seconds>", "lock timeout (at most 60)", (v) => parseIntOption(v, "--lock-timeout")).option("--no-change-checks", "do not validate the directory on changes").option("--dsn-file <path|->", "--credential-mode dsn: file holding the connection string, or - for stdin (never a flag value; never printed)").action(
48329
+ act(rt, async (ctx, o) => {
48330
+ let dsn;
48331
+ if (o.dsnFile !== void 0) {
48332
+ if (o.dsnFile === "-" && ctx.deps.stdinIsTTY) throw usage(`pipe the connection string on stdin, e.g. \`pbpaste | ${BRAND.cli} db sources add \u2026 --dsn-file -\``);
48333
+ dsn = (await readText(fileSpec(o.dsnFile), ctx.input)).trim();
48334
+ if (!dsn) throw usage("the connection string is empty");
48335
+ ctx.addSecret(dsn);
48336
+ const password = /^[a-z]+:\/\/[^:/@]*:([^@]+)@/i.exec(dsn)?.[1];
48337
+ if (password) ctx.addSecret(password);
48338
+ }
48339
+ const body = {
48340
+ name: o.name,
48341
+ path: o.path,
48342
+ format: o.format,
48343
+ engine: o.engine,
48344
+ connection_id: o.connection,
48345
+ ps_organization: o.organization,
48346
+ ps_database: o.database,
48347
+ ps_branch: o.psBranch,
48348
+ change_checks: o.changeChecks,
48349
+ ...o.branch ? { branch: o.branch } : {},
48350
+ ...o.revisionsSchema ? { revisions_schema: o.revisionsSchema } : {},
48351
+ ...o.revisionsTable ? { revisions_table: o.revisionsTable } : {},
48352
+ ...o.baseline ? { baseline_version: o.baseline } : {},
48353
+ ...o.execOrder ? { exec_order: o.execOrder } : {},
48354
+ ...o.onMerge ? { on_merge: o.onMerge } : {},
48355
+ ...o.applyOn ? { apply_on: o.applyOn } : {},
48356
+ ...o.approval === false ? { require_approval: false } : {},
48357
+ ...o.destructive ? { lint: { levels: { destructive: o.destructive }, nolint: "honor", drift_blocks_deploy: false } } : {},
48358
+ ...o.drift ? { drift_mode: o.drift } : {},
48359
+ ...o.credentialMode ? { credential_mode: o.credentialMode } : {},
48360
+ ...o.runAsRole ? { run_as_role: o.runAsRole } : {},
48361
+ ...o.sessionSetting ? { session_settings: sessionSettings(o.sessionSetting) } : {},
48362
+ ...o.statementTimeout !== void 0 ? { statement_timeout_s: o.statementTimeout } : {},
48363
+ ...o.lockTimeout !== void 0 ? { lock_timeout_s: o.lockTimeout } : {},
48364
+ ...dsn ? { dsn } : {}
48365
+ };
48366
+ ctx.result(await ctx.client("user").dbMigrations.sources.create(await ctx.repoId(void 0, "user"), body), renderSource);
48367
+ })
48368
+ );
48369
+ sources.command("set <source>").description("Change how a source applies, its lint and drift checks (migrations:manage; D1 sources: only these)").option("--apply-on <when>", DB_APPLY_ON.join(" | "), oneOf("--apply-on", DB_APPLY_ON)).option("--approval", "each run waits for approval (migrations:approve; a protected source's approvers)").option("--no-approval", "--apply-on deploy or manual: apply without approval").option("--destructive <level>", "lint level for destructive changes: error | warning | off", oneOf("--destructive", LEVELS)).option("--nolint <mode>", "honor | ignore: whether `-- atlas:nolint` in a file acknowledges a finding", oneOf("--nolint", ["honor", "ignore"])).option("--drift <mode>", DB_DRIFT_MODES.join(" | "), oneOf("--drift", DB_DRIFT_MODES)).option("--drift-blocks-deploy <yes|no>", "drift found by the last check fails the deploy's migrations step", oneOf("--drift-blocks-deploy", ["yes", "no"])).option("--enable", "activate the source").option("--disable", "disable the source (no runs, no checks)").action(
48370
+ act(rt, async (ctx, o, [ref]) => {
48371
+ if (o.enable && o.disable) throw usage("--enable and --disable exclude each other");
48372
+ const c8 = ctx.client("user").dbMigrations;
48373
+ const s = await c8.sources.get(await sourceId(ctx, ref));
48374
+ const levels = { ...s.lint.levels };
48375
+ if (o.destructive) {
48376
+ if (o.destructive === "error") delete levels.destructive;
48377
+ else levels.destructive = o.destructive;
48378
+ }
48379
+ const lintChanged = o.destructive || o.nolint || o.driftBlocksDeploy;
48380
+ const body = {
48381
+ version: s.version,
48382
+ ...o.applyOn ? { apply_on: o.applyOn } : {},
48383
+ ...o.approval !== void 0 ? { require_approval: o.approval } : {},
48384
+ ...lintChanged ? { lint: { levels, nolint: o.nolint ?? s.lint.nolint, drift_blocks_deploy: o.driftBlocksDeploy ? o.driftBlocksDeploy === "yes" : s.lint.drift_blocks_deploy } } : {},
48385
+ ...o.drift ? { drift_mode: o.drift } : {},
48386
+ ...o.enable ? { state: "active" } : o.disable ? { state: "disabled" } : {}
48387
+ };
48388
+ if (Object.keys(body).length === 1) throw usage("nothing to change: give --apply-on, --approval, --destructive, --nolint, --drift, --drift-blocks-deploy, --enable or --disable");
48389
+ ctx.result(await c8.sources.update(s.id, body), renderSource);
48390
+ })
48391
+ );
48392
+ protectOptions(sources.command("protect <source>").description("Require approvals for every run of the source (people with migrations:manage). A protected database can no longer apply without approval. Replaces the current rule")).action(
48393
+ act(rt, async (ctx, o, [ref]) => {
48394
+ const c8 = ctx.client("user").dbMigrations.sources;
48395
+ const s = await c8.get(await sourceId(ctx, ref));
48396
+ ctx.result(await c8.protect(s.id, protectBody(o, s.approval_rule)), renderRule);
48397
+ })
48398
+ );
48399
+ sources.command("unprotect <source>").description("Remove the source's approval rule (runs still need approval as its apply mode says). Requires --yes when non-interactive").action(
48400
+ act(rt, async (ctx, _o, [ref]) => {
48401
+ const id2 = await sourceId(ctx, ref);
48402
+ await ctx.confirm(`remove the approval rule of ${ref}`);
48403
+ await ctx.client("user").dbMigrations.sources.unprotect(id2);
48404
+ ctx.result({ unprotected: id2 }, `${ref} is no longer protected`);
48405
+ })
48406
+ );
48407
+ sources.command("approvers <source>").description("Who may approve the source's runs now").action(act(rt, async (ctx, _o, [ref]) => ctx.result(await ctx.client("user").dbMigrations.sources.approvers(await sourceId(ctx, ref)), renderApprovers)));
48408
+ sources.command("rm <source>").description("Delete a migration source (name or id; its runs stay listed). Requires --yes when non-interactive").action(
48409
+ act(rt, async (ctx, _o, [ref]) => {
48410
+ const id2 = await sourceId(ctx, ref);
48411
+ await ctx.confirm(`delete migration source ${ref}`);
48412
+ await ctx.client("user").dbMigrations.sources.delete(id2);
48413
+ ctx.result({ deleted: id2 }, `deleted ${ref}`);
48414
+ })
48415
+ );
48416
+ db.command("suggest").description("Migration directories found in --repo (atlas.sum, drizzle journals, *.up.sql, numbered SQL files), and whether a source covers each").option("--ref <ref>", "branch, tag or commit (default: the default branch)").action(act(rt, async (ctx, o) => ctx.result(await ctx.client("user").dbMigrations.suggestions(await ctx.repoId(void 0, "user"), o.ref), renderSuggestions)));
48417
+ db.command("plan [source]").description("What applying a source (every source when omitted) would do: pending files and issues. Exits 7 when a plan has errors").option("--ref <ref>", "branch, tag or commit (default: the source's branch)").option("--live", "read the database now instead of its last snapshot (migrations:manage)").action(
48418
+ act(rt, async (ctx, o, [ref]) => {
48419
+ const c8 = ctx.client("user").dbMigrations;
48420
+ const q = { ref: o.ref, live: o.live };
48421
+ if (ref) {
48422
+ const p = await c8.plan(await sourceId(ctx, ref), q);
48423
+ ctx.result(p, renderPlan2);
48424
+ if (!p.ok) ctx.setExit(EXIT.NOT_READY);
48425
+ return;
48426
+ }
48427
+ const { items } = await c8.sources.list(await ctx.repoId(void 0, "user"));
48428
+ const plans = { items: await Promise.all(items.filter((s) => s.state === "active").map((s) => c8.plan(s.id, q))) };
48429
+ ctx.result(plans, renderPlans);
48430
+ if (plans.items.some((p) => !p.ok)) ctx.setExit(EXIT.NOT_READY);
48431
+ })
48432
+ );
48433
+ db.command("diff <change>").description("What a change's migrations do: per source, the files it adds, lint findings and the schema effect. Exits 7 when a source would block the change").action(
48434
+ act(rt, async (ctx, _o, [change]) => {
48435
+ const plans = await ctx.client("user").dbMigrations.changePlans(await ctx.changeId(change, "user"));
48436
+ ctx.result(
48437
+ plans,
48438
+ (v) => v.items.length ? v.items.map((p) => {
48439
+ const mine = p.pending.filter((f) => f.introduced_by_change);
48440
+ return [`${p.source_name} (${p.provider === "cloudflare_d1" ? "Cloudflare D1" : `PlanetScale ${p.engine}`} ${p.target_label}): ${p.ok ? "ok" : "blocked"}`, mine.length ? `this change adds: ${mine.map((f) => f.name).join(", ")}` : "this change adds no migrations", ...issueLines(p.issues), ...lintLines(p.lint), ...diffLines(mine.length ? p.schema_diff : null)].join("\n");
48441
+ }).join("\n\n") : "no migration sources follow the change's target branch"
48442
+ );
48443
+ if (plans.items.some((p) => !p.ok)) ctx.setExit(EXIT.NOT_READY);
48444
+ })
48445
+ );
48446
+ db.command("drift <source>").description("The source's last drift check: its database against what the applied migrations produce. --check reads the database now (migrations:manage)").option("--check", "check now").action(
48447
+ act(rt, async (ctx, o, [ref]) => {
48448
+ const c8 = ctx.client("user").dbMigrations;
48449
+ const id2 = await sourceId(ctx, ref);
48450
+ const d = o.check ? await c8.checkDrift(id2) : (await c8.sources.get(id2)).drift;
48451
+ ctx.result(d, renderDrift);
48452
+ if (d?.status === "drift") ctx.setExit(EXIT.NOT_READY);
48453
+ })
48454
+ );
48455
+ db.command("refresh <source>").description("Read the applied versions from the database now and show the plan at the source's branch (migrations:manage)").action(act(rt, async (ctx, _o, [ref]) => ctx.result(await ctx.client("user").dbMigrations.refresh(await sourceId(ctx, ref)), renderPlan2)));
48456
+ db.command("runs").description("Migration runs of --repo, newest first").option("--source <source>", "only this source (name or id)").option("--limit <n>", "page size", (v) => parseIntOption(v, "--limit")).option("--cursor <cursor>", "page cursor").action(
48457
+ act(rt, async (ctx, o) => {
48458
+ const repo = { id: await ctx.repoId(void 0, "user") };
48459
+ const q = { ...o.source ? { source_id: await sourceId(ctx, o.source, repo) } : {}, ...o.limit ? { limit: o.limit } : {}, ...o.cursor ? { cursor: o.cursor } : {} };
48460
+ ctx.result(await ctx.client("user").dbMigrations.runs.list(repo.id, q), renderRuns);
48461
+ })
48462
+ );
48463
+ db.command("run <run>").description("One migration run: status, files and issues").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client("user").dbMigrations.runs.get(id2), renderRun)));
48464
+ db.command("apply <source>").description("Apply a source's pending migrations now (migrations:manage). Your request is the approval when you may approve and the source is not protected; otherwise the run waits for approval. Requires --yes when non-interactive").option("--ref <ref>", "branch, tag or commit to apply (default: the source's branch)").action(
48465
+ act(rt, async (ctx, o, [ref]) => {
48466
+ const id2 = await sourceId(ctx, ref);
48467
+ await ctx.confirm(`apply the pending migrations of ${ref}${o.ref ? ` at ${o.ref}` : ""} to its database`);
48468
+ const r = await ctx.client("user").dbMigrations.startRun(id2, o.ref ? { ref: o.ref } : {});
48469
+ ctx.result(r, (v) => v.run ? renderRun(v.run) : `nothing to apply
48470
+
48471
+ ${renderPlan2(v.plan)}`);
48472
+ })
48473
+ );
48474
+ const decisions = [
48475
+ ["approve", "Approve a run waiting for approval: Brnch applies exactly the files it shows once it has the approvals it needs (migrations:approve; a protected source's approvers)"],
48476
+ ["reject", "Reject a run waiting for approval; nothing is applied (migrations:approve)"],
48477
+ ["retry", "Retry a failed run from where it stopped (migrations:approve)"],
48478
+ ["cancel", "Cancel a run that has not finished (migrations:approve)"]
48479
+ ];
48480
+ for (const [decision, description] of decisions) {
48481
+ const cmd = db.command(`${decision} <run>`).description(description).option("--reason <text>", "why (recorded with the decision)");
48482
+ if (decision === "approve") cmd.option("--redeploy", "a run a production deploy waits on: deploy the same commit again, which applies it");
48483
+ cmd.action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client("user").dbMigrations[decision](id2, o.redeploy ? { reason: o.reason, redeploy: true } : o.reason), renderRun)));
48484
+ }
48485
+ }
48486
+
47790
48487
  // packages/cli/src/commands/deploy.ts
47791
48488
  var TERMINAL4 = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "superseded", "blocked"]);
47792
48489
  var NO_DO_PREVIEWS = "previews off: Cloudflare makes no version URLs for Workers that implement a Durable Object (--previews --preview-mode worker_previews uses Worker Previews; it needs a `previews` block in the Wrangler configuration)";
@@ -47794,7 +48491,7 @@ var renderTargets = (v) => table(
47794
48491
  v.items.map((t) => ({ ...t, resource: t.resource_name, env: t.environment ?? "-", last_production: t.last_production ? `${t.last_production.status} ${t.last_production.commit_oid.slice(0, 12)}` : "-" })),
47795
48492
  ["name", "id", "platform", "resource", "env", "branch", "runs_on", "previews", "state", "last_production"]
47796
48493
  );
47797
- var renderTarget = (t) => kv({ ...t, connection: t.connection ? `${t.connection.name} (${t.connection.id}, ${t.connection.state})` : "-", last_production: t.last_production ? `${t.last_production.id} ${t.last_production.status}` : "-" }, [
48494
+ var renderTarget = (t) => kv({ ...t, approvers: ruleSummary(t.approval_rule), watch_paths: t.watch_paths?.length ? t.watch_paths.join(" ") : "every change", connection: t.connection ? `${t.connection.name} (${t.connection.id}, ${t.connection.state})` : "-", last_production: t.last_production ? `${t.last_production.id} ${t.last_production.status}` : "-" }, [
47798
48495
  "id",
47799
48496
  "name",
47800
48497
  "platform",
@@ -47809,14 +48506,16 @@ var renderTarget = (t) => kv({ ...t, connection: t.connection ? `${t.connection.
47809
48506
  "build_command",
47810
48507
  "output_directory",
47811
48508
  "runs_on",
48509
+ "watch_paths",
47812
48510
  "previews",
47813
48511
  "preview_check_name",
48512
+ "approvers",
47814
48513
  "last_production",
47815
48514
  "version"
47816
48515
  ]);
47817
48516
  var renderDeployment = (d) => kv({ ...d, change: d.change ? `#${d.change.number}` : "-", logs: d.run?.logs_url ?? "-" }, ["id", "target_name", "kind", "environment", "status", "status_reason", "trigger", "commit_oid", "change", "version_id", "url", "alias_url", "verified", "duration_ms", "logs", "created_at"]) + (d.steps.length ? `
47818
48517
 
47819
- ${table(d.steps, ["name", "phase", "status", "summary"])}` : "");
48518
+ ${table(d.steps, ["name", "phase", "status", "summary"])}` : "") + approvalLines(d.approval);
47820
48519
  var renderDeployments = (v) => listWithCursor({ ...v, items: v.items.map((d) => ({ ...d, commit: d.commit_oid.slice(0, 12), change: d.change ? `#${d.change.number}` : "-" })) }, ["id", "target_name", "kind", "status", "commit", "change", "trigger", "created_at"]);
47821
48520
  async function pick3(ctx, what, flag, options, label2) {
47822
48521
  if (options.length === 1) return options[0];
@@ -47845,7 +48544,7 @@ async function stepsOf(ctx, spec) {
47845
48544
  return v;
47846
48545
  }
47847
48546
  function targetOptions(cmd) {
47848
- return cmd.option("--platform <platform>", "workers | pages").option("--connection <connection>", "deploy connection (icn_\u2026)").option("--account-id <id>", "Cloudflare account id").option("--env <name>", "Wrangler environment (deploys with --env)").option("--worker <name>", "Worker name (or Pages project name)").option("--root <dir>", "root directory (where the Wrangler config lives)").option("--branch <name>", "production branch (default: the repository's default branch)").option("--install <cmd>", "install command").option("--build <cmd>", "build command").option("--output-dir <dir>", "Pages: the built site").option("--previews", "upload a preview of every open change").option("--no-previews", "no previews").option("--preview-agent-changes", "also preview changes made by agents or integrations, isolated (Worker Previews only)").option("--no-preview-agent-changes", "previews only for changes people upload").option("--preview-mode <mode>", "versions (preview version with an alias) | worker_previews (`wrangler preview`: Durable Objects allowed, bindings from the `previews` block)").option("--preview-check <name>", "report previews as this check").option("--runs-on <label>", "hosted runner size for the deploy jobs: brnch-hosted (default), brnch-hosted-1 \u2026 brnch-hosted-4 (your plan sets the largest)").option("--name <name>", "target name").option("--steps <json>", `pre/post steps as a JSON array, @file or - (replaces the list), e.g. '[{"id":"migrate","kind":"d1_migrations","phase":"pre"}]'`);
48547
+ return cmd.option("--platform <platform>", "workers | pages").option("--connection <connection>", "deploy connection (icn_\u2026)").option("--account-id <id>", "Cloudflare account id").option("--env <name>", "Wrangler environment (deploys with --env)").option("--worker <name>", "Worker name (or Pages project name)").option("--root <dir>", "root directory (where the Wrangler config lives)").option("--branch <name>", "production branch (default: the repository's default branch)").option("--install <cmd>", "install command").option("--build <cmd>", "build command").option("--output-dir <dir>", "Pages: the built site").option("--previews", "upload a preview of every open change").option("--no-previews", "no previews").option("--preview-agent-changes", "also preview changes made by agents or integrations, isolated (Worker Previews only)").option("--no-preview-agent-changes", "previews only for changes people upload").option("--preview-mode <mode>", "versions (preview version with an alias) | worker_previews (`wrangler preview`: Durable Objects allowed, bindings from the `previews` block)").option("--preview-check <name>", "report previews as this check").option("--runs-on <label>", "hosted runner size for the deploy jobs: brnch-hosted (default), brnch-hosted-1 \u2026 brnch-hosted-4 (your plan sets the largest)").option("--watch-path <pattern>", "deploy only when these paths change (repeatable; replaces the list): CODEOWNERS patterns like apps/web/ or packages/**, ! excludes", (v, prev = []) => [...prev, v]).option("--watch-all", "deploy on every change (clears the watch paths)").option("--name <name>", "target name").option("--steps <json>", `pre/post steps as a JSON array, @file or - (replaces the list), e.g. '[{"id":"migrate","kind":"d1_migrations","phase":"pre"}]'`);
47849
48548
  }
47850
48549
  async function planSetup(ctx, repo, o) {
47851
48550
  const c8 = ctx.client("user").deploy;
@@ -47920,6 +48619,7 @@ async function planSetup(ctx, repo, o) {
47920
48619
  ...o.previewAgentChanges !== void 0 ? { preview_agent_changes: o.previewAgentChanges } : {},
47921
48620
  preview_check_name: o.previewCheck ?? null,
47922
48621
  ...o.runsOn ? { runs_on: o.runsOn } : {},
48622
+ ...o.watchPath ? { watch_paths: o.watchPath } : {},
47923
48623
  ...o.branch ? { branch: o.branch } : {},
47924
48624
  ...o.steps !== void 0 ? { steps: await stepsOf(ctx, o.steps) } : {}
47925
48625
  };
@@ -47931,6 +48631,7 @@ async function waitFor(ctx, d, interval, timeout) {
47931
48631
  for (; ; ) {
47932
48632
  if (d.status !== last) ctx.info(`${d.id}: ${d.status}${d.status_reason ? ` (${d.status_reason})` : ""}`);
47933
48633
  last = d.status;
48634
+ ctx.status.working(`Deploy ${d.status}`);
47934
48635
  if (TERMINAL4.has(d.status) || Date.now() > deadline) return d;
47935
48636
  await ctx.deps.sleep(Math.max(1, interval) * 1e3);
47936
48637
  d = await ctx.client("user").deploy.deployments.get(d.id);
@@ -47950,11 +48651,28 @@ function registerDeploy(program2, rt) {
47950
48651
  Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}`);
47951
48652
  })
47952
48653
  );
47953
- deploy.command("targets").description("Deploy targets of --repo").action(act(rt, async (ctx) => ctx.result(await ctx.client("user").deploy.targets.list(await ctx.repoId(void 0, "user")), renderTargets)));
48654
+ const targets2 = deploy.command("targets").description("Deploy targets of --repo; `protect` requires approvers for a target's production deploys").action(act(rt, async (ctx) => ctx.result(await ctx.client("user").deploy.targets.list(await ctx.repoId(void 0, "user")), renderTargets)));
48655
+ protectOptions(targets2.command("protect <target>").description("Require approvals for the target's production deploys and rollbacks (people with deploy:manage). Replaces the current rule")).action(
48656
+ act(rt, async (ctx, o, [ref]) => {
48657
+ const c8 = ctx.client("user").deploy.targets;
48658
+ const t = await c8.get(await targetId(ctx, ref));
48659
+ ctx.result(await c8.protect(t.id, protectBody(o, t.approval_rule)), renderRule);
48660
+ })
48661
+ );
48662
+ targets2.command("unprotect <target>").description("Remove the target's approval rule; deploys waiting for it go on. Requires --yes when non-interactive").action(
48663
+ act(rt, async (ctx, _o, [ref]) => {
48664
+ const id2 = await targetId(ctx, ref);
48665
+ await ctx.confirm(`remove the approval rule of ${ref}: its production deploys will no longer wait for approval`);
48666
+ await ctx.client("user").deploy.targets.unprotect(id2);
48667
+ ctx.result({ unprotected: id2 }, `${ref} is no longer protected`);
48668
+ })
48669
+ );
48670
+ targets2.command("approvers <target>").description("Who may approve the target's production deploys now").action(act(rt, async (ctx, _o, [ref]) => ctx.result(await ctx.client("user").deploy.targets.approvers(await targetId(ctx, ref)), renderApprovers)));
47954
48671
  deploy.command("target <target>").description("One deploy target (id, or name in --repo)").action(act(rt, async (ctx, _o, [ref]) => ctx.result(await ctx.client("user").deploy.targets.get(await targetId(ctx, ref)), renderTarget)));
47955
48672
  targetOptions(deploy.command("update <target>").description("Change a deploy target (only the given flags; an empty value clears a command or the environment)")).option("--disable", "stop deploying (merges no longer deploy)").option("--enable", "deploy again").action(
47956
48673
  act(rt, async (ctx, o, [ref]) => {
47957
48674
  if (o.disable && o.enable) throw usage("give either --disable or --enable");
48675
+ if (o.watchAll && o.watchPath) throw usage("give either --watch-path or --watch-all");
47958
48676
  const c8 = ctx.client("user").deploy.targets;
47959
48677
  const cur = await c8.get(await targetId(ctx, ref));
47960
48678
  const patch = {
@@ -47974,6 +48692,7 @@ Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}
47974
48692
  preview_agent_changes: o.previewAgentChanges,
47975
48693
  preview_check_name: orNull(o.previewCheck),
47976
48694
  runs_on: o.runsOn,
48695
+ watch_paths: o.watchAll ? [] : o.watchPath,
47977
48696
  state: o.disable ? "disabled" : o.enable ? "active" : void 0,
47978
48697
  steps: await stepsOf(ctx, o.steps)
47979
48698
  };
@@ -48007,6 +48726,8 @@ Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}
48007
48726
  );
48008
48727
  deploy.command("show <deployment>").description("One deployment with its steps").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client("user").deploy.deployments.get(id2), renderDeployment)));
48009
48728
  deploy.command("cancel <deployment>").description("Cancel a queued or running deployment").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client("user").deploy.deployments.cancel(id2), renderDeployment)));
48729
+ deploy.command("approve <deployment>").description("Approve a deploy of a protected target (people who may approve; the last approval needed starts it)").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client("user").deploy.deployments.approve(id2), renderDeployment)));
48730
+ deploy.command("reject <deployment>").description("Reject a deploy waiting for approval; it is cancelled").option("--reason <text>", "why (recorded with the decision)").action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client("user").deploy.deployments.reject(id2, o.reason), renderDeployment)));
48010
48731
  deploy.command("rollback <target>").description("Send all traffic back to the version of an earlier verified production deployment").requiredOption("--to <deployment>", "the deployment to roll back to (dpl_\u2026)").option("--message <text>", "deployment message").action(
48011
48732
  act(rt, async (ctx, o, [ref]) => {
48012
48733
  const id2 = await targetId(ctx, ref);
@@ -48019,8 +48740,7 @@ Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}
48019
48740
  // packages/cli/src/commands/delegation.ts
48020
48741
  var ITERATION_DONE = /* @__PURE__ */ new Set(["succeeded", "no_changes", "failed", "cancelled"]);
48021
48742
  var STATES = ["working", "waiting", "stopped"];
48022
- var who2 = (a) => a.display_name ?? `${a.type} ${a.id}`;
48023
- var short = (oid) => oid ? oid.slice(0, 12) : null;
48743
+ var short2 = (oid) => oid ? oid.slice(0, 12) : null;
48024
48744
  function vendorArg(v) {
48025
48745
  if (!CODING_AGENT_VENDORS.includes(v)) throw usage(`unknown agent "${v}"; one of ${CODING_AGENT_VENDORS.join(", ")}`);
48026
48746
  return v;
@@ -48039,8 +48759,8 @@ function renderIterations(d) {
48039
48759
  const rows2 = d.iterations.map((i) => ({
48040
48760
  ...i,
48041
48761
  trigger: i.trigger_ref ? `${i.trigger} (${i.trigger_ref})` : i.trigger,
48042
- by: who2(i.trigger_actor),
48043
- head: short(i.head_oid)
48762
+ by: actorLabel(i.trigger_actor),
48763
+ head: short2(i.head_oid)
48044
48764
  }));
48045
48765
  const claims = d.iterations.map(reported).filter((x) => !!x);
48046
48766
  const errors = d.iterations.filter((i) => i.error).map((i) => ` #${i.number}: ${i.error}`);
@@ -48061,7 +48781,7 @@ var renderDelegation = (d) => kv(
48061
48781
  ...d,
48062
48782
  agent: `${d.agent.name} (${d.agent.vendor})`,
48063
48783
  status: delegationStatusLabel(d),
48064
- sponsor: who2(d.sponsor),
48784
+ sponsor: actorLabel(d.sponsor),
48065
48785
  branch: `${d.source_ref} \u2192 ${d.target_ref}`,
48066
48786
  automatic: `on review ${d.auto_on_review ? "on" : "off"}, on check failure ${d.auto_on_check_failure ? "on" : "off"}`,
48067
48787
  budget: `iterations ${d.usage.iterations_used}/${d.budget.max_iterations}, tokens ${d.usage.tokens_used}/${d.budget.max_tokens}, ${d.budget.iteration_minutes} min per iteration`
@@ -48103,6 +48823,7 @@ async function waitForIteration(ctx, start, o) {
48103
48823
  const line2 = `${delegationStatusLabel(d)}${it ? ` (iteration #${it.number}: ${it.state})` : ""}`;
48104
48824
  if (line2 !== last) ctx.info(line2);
48105
48825
  last = line2;
48826
+ ctx.status.working(`${d.agent.name}: ${line2}`);
48106
48827
  if (it ? ITERATION_DONE.has(it.state) : d.state !== "working") return { d, timedOut: false };
48107
48828
  if (Date.now() > deadline) return { d, timedOut: true };
48108
48829
  await ctx.deps.sleep(Math.max(1, o.interval) * 1e3);
@@ -48742,7 +49463,18 @@ function registerRepo(program2, rt) {
48742
49463
  await new Promise((r) => setTimeout(r, 2e3));
48743
49464
  job2 = await c8.migrations.get(job2.id);
48744
49465
  }
48745
- const view = { id: job2.id, state: job2.state, issues: job2.report.counts.issues, pull_requests: job2.report.counts.pull_requests, comments: job2.report.counts.comments, releases: job2.report.counts.releases, missing: job2.report.missing.join("; ") || "-" };
49466
+ const deps = job2.report.dependencies;
49467
+ const view = {
49468
+ id: job2.id,
49469
+ state: job2.state,
49470
+ issues: job2.report.counts.issues,
49471
+ pull_requests: job2.report.counts.pull_requests,
49472
+ comments: job2.report.counts.comments,
49473
+ releases: job2.report.counts.releases,
49474
+ ...deps ? { blocked_by_links: `${deps.imported} imported${deps.skipped ? `, ${deps.skipped} skipped` : ""}` } : {},
49475
+ missing: job2.report.missing.join("; ") || "-",
49476
+ warnings: job2.report.warnings.join("; ") || "-"
49477
+ };
48746
49478
  ctx.result(job2, () => kv(view, Object.keys(view)));
48747
49479
  if (o.wait && job2.state !== "completed") throw new CliError(`migration ${job2.state}: ${job2.error ?? job2.report.missing.join("; ")}`, EXIT.NOT_READY);
48748
49480
  })
@@ -48775,24 +49507,39 @@ function registerRepo(program2, rt) {
48775
49507
  if (t.transport === "ssh") {
48776
49508
  if (!t.ssh.ready) throw usage(`this machine is not set up for SSH${t.ssh.alias ? ` to ${t.ssh.alias}` : ""} yet: run \`${BRAND.cli} ssh setup\`, or clone with --https`);
48777
49509
  const url2 = sshCloneUrl(t.ssh.alias, r.org_slug, r.name);
48778
- const code2 = await ctx.deps.spawnGit(["clone", "--", url2, directory], { env: { ...ctx.deps.env, GIT_TERMINAL_PROMPT: "0" }, cwd: ctx.deps.cwd, stdoutToStderr: true });
49510
+ const code2 = await ctx.deps.spawnGit([...SNAPSHOT_CLONE, "clone", "--", url2, directory], { env: { ...ctx.deps.env, GIT_TERMINAL_PROMPT: "0" }, cwd: ctx.deps.cwd, stdoutToStderr: true });
48779
49511
  if (code2 !== 0) throw new CliError(`git clone over SSH exited with status ${code2}; \`${BRAND.cli} ssh test\` explains SSH problems, --https clones without SSH`, EXIT.UNEXPECTED, "GIT_FAILED");
48780
49512
  return ctx.result({ repository: r, directory, transport: "ssh", transport_reason: t.reason, clone_url: url2 }, cloned(r, directory, `over SSH (${url2})`, paint(colorEnabled(ctx.deps, "stdout"))));
48781
49513
  }
48782
49514
  if (t.approval) ctx.info(`Cloning over HTTPS: ${t.reason}. Pass --ssh to use SSH anyway.`);
48783
49515
  const token = await ctx.freshToken("agent");
48784
- const code = await ctx.deps.spawnGit(["clone", "--", r.clone_url, directory], {
49516
+ const helper2 = await credentialHelperConfigured(ctx);
49517
+ const list3 = helper2 && await gitAtLeast(ctx, "2.40.0") && await bundlesOffered(ctx, r.clone_url, token) ? [`--bundle-uri=${r.clone_url}/bundles`] : [];
49518
+ const code = await ctx.deps.spawnGit([...SNAPSHOT_CLONE, "clone", ...list3, "--", r.clone_url, directory], {
48785
49519
  env: gitAuthEnv(ctx.deps.env, token, r.clone_url),
48786
49520
  cwd: ctx.deps.cwd,
48787
49521
  stdoutToStderr: true
48788
49522
  });
48789
49523
  if (code !== 0) throw new CliError(`git clone exited with status ${code}`, EXIT.UNEXPECTED, "GIT_FAILED");
48790
- const helper2 = await credentialHelperConfigured(ctx);
48791
49524
  ctx.info(authGuidance(BRAND.cli, helper2));
48792
49525
  ctx.result({ repository: r, directory, transport: "https", transport_reason: t.reason, clone_url: r.clone_url, credential_helper: helper2 }, cloned(r, directory, "over HTTPS", paint(colorEnabled(ctx.deps, "stdout"))));
48793
49526
  })
48794
49527
  );
48795
49528
  }
49529
+ var SNAPSHOT_CLONE = ["-c", "transfer.bundleURI=true"];
49530
+ async function bundlesOffered(ctx, cloneUrl, token) {
49531
+ try {
49532
+ const res = await ctx.deps.sys.fetch(`${cloneUrl}/bundles`, { headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(5e3) });
49533
+ return res.ok && (await res.text()).includes('[bundle "');
49534
+ } catch {
49535
+ return false;
49536
+ }
49537
+ }
49538
+ async function gitAtLeast(ctx, min) {
49539
+ const r = await ctx.deps.sys.run("git", ["--version"], { timeoutMs: 1e4 }).catch(() => null);
49540
+ const version4 = r?.code === 0 ? /git version (\d+\.\d+\.\d+)/.exec(r.stdout)?.[1] : void 0;
49541
+ return !!version4 && compareVersions(version4, min) >= 0;
49542
+ }
48796
49543
  var hasRepo = (ctx) => {
48797
49544
  try {
48798
49545
  ctx.repoSpec();
@@ -48860,7 +49607,7 @@ function registerGit(program2, rt) {
48860
49607
  }
48861
49608
 
48862
49609
  // packages/cli/src/commands/release.ts
48863
- var renderRelease = (r) => kv({ ...r, author: r.author.display_name ?? r.author.id }, ["id", "tag", "name", "commit_oid", "draft", "author", "created_at", "notes"]);
49610
+ var renderRelease = (r) => kv({ ...r, author: actorLabel(r.author) }, ["id", "tag", "name", "commit_oid", "draft", "author", "created_at", "notes"]);
48864
49611
  function registerRelease(program2, rt) {
48865
49612
  const release = program2.command("release").description("Releases of --repo (a tag with notes)");
48866
49613
  release.command("list").description("Releases, newest first").action(
@@ -49054,15 +49801,16 @@ function registerImage(program2, rt) {
49054
49801
  ctx.result(page, (p) => listWithCursor(p, ["reference", "tags", "versions", "updated_at"]));
49055
49802
  })
49056
49803
  );
49057
- image.command("delete <ref>").description("Delete a tag (`<image>:<tag>`) or a version and all its tags (`<image>@sha256:\u2026`); untagged versions are also collected after the retention window").action(
49804
+ image.command("delete <ref>").description("Delete a tag (`<image>:<tag>`), a version and all its tags (`<image>@sha256:\u2026`), or an image with no versions left (`<image>`); untagged versions are also collected after the retention window").action(
49058
49805
  act(rt, async (ctx, _o, [spec]) => {
49059
49806
  const ref = parseImageRef(spec, await registryHost(ctx));
49060
- if (!ref || !ref.tag && !ref.digest) throw usage(`invalid image reference "${spec}": expected <namespace>/<repository>[/<image>]:<tag> or \u2026@sha256:<digest>`);
49807
+ if (!ref) throw usage(`invalid image reference "${spec}": expected <namespace>/<repository>[/<image>][:<tag>|@sha256:<digest>]`);
49061
49808
  const img = await findImage(ctx, ref.name);
49062
49809
  if (ref.digest) await ctx.client().packages.deleteVersion(img.id, ref.digest);
49063
- else await ctx.client().packages.deleteTag(img.id, ref.tag);
49064
- const what = ref.digest ? `version ${ref.digest}` : `tag ${ref.tag}`;
49065
- ctx.result({ image: img.full_name, deleted: ref.digest ? { digest: ref.digest } : { tag: ref.tag } }, `Deleted ${what} of ${img.full_name}.`);
49810
+ else if (ref.tag) await ctx.client().packages.deleteTag(img.id, ref.tag);
49811
+ else await ctx.client().packages.delete(img.id);
49812
+ const what = ref.digest ? `version ${ref.digest} of ${img.full_name}` : ref.tag ? `tag ${ref.tag} of ${img.full_name}` : `image ${img.full_name}`;
49813
+ ctx.result({ image: img.full_name, deleted: ref.digest ? { digest: ref.digest } : ref.tag ? { tag: ref.tag } : { image: img.id } }, `Deleted ${what}.`);
49066
49814
  })
49067
49815
  );
49068
49816
  }
@@ -49302,12 +50050,13 @@ var renderRangeDiff = (r) => {
49302
50050
  if (r.truncated) lines2.push("(truncated)");
49303
50051
  return lines2.join("\n");
49304
50052
  };
49305
- var renderOperation = (o) => `${o.id} ${o.created_at} ${o.operation}/${o.kind} ${o.ref} ${o.old_oid.slice(0, 12)} \u2192 ${o.new_oid.slice(0, 12)} by ${o.actor.display_name ?? o.actor.id}${o.protected ? " [protected]" : ""}${o.undone_by ? ` (undone by ${o.undone_by})` : ""}`;
50053
+ var renderOperation = (o) => `${o.id} ${o.created_at} ${o.operation}/${o.kind} ${o.ref} ${o.old_oid.slice(0, 12)} \u2192 ${o.new_oid.slice(0, 12)} by ${actorLabel(o.actor)}${o.protected ? " [protected]" : ""}${o.undone_by ? ` (undone by ${o.undone_by})` : ""}`;
49306
50054
  async function waitForPromotion(ctx, p, timeout, interval) {
49307
50055
  const deadline = Date.now() + timeout * 1e3;
49308
50056
  let last = p.state;
49309
50057
  ctx.info(`promotion ${p.id}: ${p.state}`);
49310
50058
  while (!TERMINAL5.has(p.state) && Date.now() < deadline) {
50059
+ ctx.status.working(`Merge ${p.state}`);
49311
50060
  await ctx.deps.sleep(Math.max(1, interval) * 1e3);
49312
50061
  p = await ctx.client().promotions.get(p.id);
49313
50062
  if (p.state !== last) ctx.info(`promotion ${p.id}: ${last = p.state}`);
@@ -49378,7 +50127,7 @@ next: --cursor ${v.next_cursor}` : "") : listWithCursor(v, ["id"]));
49378
50127
  }
49379
50128
 
49380
50129
  // packages/cli/src/commands/coordination.ts
49381
- var holderOf = (c8) => `${c8.holder.display_name ?? c8.holder.id}${c8.session_id ? ` (${c8.session_id})` : ""}`;
50130
+ var holderOf = (c8) => `${actorLabel(c8.holder)}${c8.session_id ? ` (${c8.session_id})` : ""}`;
49382
50131
  var renderClaim = (r) => [
49383
50132
  `${r.already_held ? "Already yours" : "Claimed"}: ${r.claim.key}${r.claim.change ? ` (change #${r.claim.change.number})` : ""}`,
49384
50133
  ...r.skipped.length ? [`Taken by others: ${r.skipped.join(", ")}`] : [],
@@ -51681,7 +52430,7 @@ function safeLegs(m) {
51681
52430
  }
51682
52431
 
51683
52432
  // packages/cli/src/commands/actions.ts
51684
- var renderRun = (r) => kv({ ...r, actor: r.actor.display_name ?? r.actor.id, trust: `${r.definition.source} ${r.definition.ref}${r.definition.trusted ? " (trusted)" : " (contributor-controlled)"}` }, [
52433
+ var renderRun2 = (r) => kv({ ...r, actor: actorLabel(r.actor), trust: `${r.definition.source} ${r.definition.ref}${r.definition.trusted ? " (trusted)" : " (contributor-controlled)"}` }, [
51685
52434
  "id",
51686
52435
  "workflow_name",
51687
52436
  "run_number",
@@ -51746,7 +52495,7 @@ function registerWorkflow(program2, rt) {
51746
52495
  inputs[kvp.slice(0, i)] = kvp.slice(i + 1);
51747
52496
  }
51748
52497
  const r = await ctx.client("user").actions.workflows.dispatch(await ctx.repoId(void 0, "user"), file2, { ...o.ref ? { ref: o.ref } : {}, inputs });
51749
- ctx.result(r, renderRun);
52498
+ ctx.result(r, renderRun2);
51750
52499
  })
51751
52500
  );
51752
52501
  }
@@ -51770,6 +52519,8 @@ async function watchRun(ctx, runId, interval, timeout, logs) {
51770
52519
  ${text2.trimEnd()}`);
51771
52520
  }
51772
52521
  }
52522
+ const finished = r.jobs.filter((j) => j.status === "completed").length;
52523
+ ctx.status.working(`Run ${r.status}: ${finished}/${r.jobs.length} jobs done`, r.jobs.length ? finished / r.jobs.length * 100 : void 0);
51773
52524
  if (TERMINAL6.has(r.status) || Date.now() > deadline) return r;
51774
52525
  await ctx.deps.sleep(Math.max(1, interval) * 1e3);
51775
52526
  }
@@ -51789,13 +52540,13 @@ function registerRun(program2, rt) {
51789
52540
  const r = await c8.actions.runs.get(id2);
51790
52541
  if (o.log) for (const j of r.jobs) ctx.info(`\u2500\u2500 ${j.name} (${j.status}${j.conclusion ? `/${j.conclusion}` : ""}) \u2500\u2500
51791
52542
  ${(await c8.actions.jobs.logs(j.id)).trimEnd()}`);
51792
- ctx.result(r, renderRun);
52543
+ ctx.result(r, renderRun2);
51793
52544
  })
51794
52545
  );
51795
52546
  run3.command("watch <run>").description("Follow a run until it completes. Exit 0 when it succeeded, 1 when it failed or was cancelled, 7 on timeout").option("--interval <seconds>", "poll interval", (v) => parseIntOption(v, "--interval"), 2).option("--timeout <seconds>", "give up after", (v) => parseIntOption(v, "--timeout"), 3600).option("--log", "print each job's log when it completes").action(
51796
52547
  act(rt, async (ctx, o, [id2]) => {
51797
52548
  const r = await watchRun(ctx, id2, o.interval, o.timeout, !!o.log);
51798
- ctx.result(r, renderRun);
52549
+ ctx.result(r, renderRun2);
51799
52550
  ctx.setExit(runExit(r));
51800
52551
  })
51801
52552
  );
@@ -51820,8 +52571,8 @@ ${(await c8.actions.jobs.logs(j.id)).trimEnd()}`);
51820
52571
  ctx.result({ job_id: job2, conclusion: status }, `job ${job2}: ${status}`);
51821
52572
  })
51822
52573
  );
51823
- run3.command("cancel <run>").description("Cancel a run (running jobs are stopped on their runners)").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client("user").actions.runs.cancel(id2), renderRun)));
51824
- run3.command("rerun <run>").description("Re-run a completed run (all jobs, or --failed and their dependents)").option("--failed", "only failed/cancelled jobs and jobs that depend on them").action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client("user").actions.runs.rerun(id2, { failed_only: !!o.failed }), renderRun)));
52574
+ run3.command("cancel <run>").description("Cancel a run (running jobs are stopped on their runners)").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client("user").actions.runs.cancel(id2), renderRun2)));
52575
+ run3.command("rerun <run>").description("Re-run a completed run (all jobs, or --failed and their dependents)").option("--failed", "only failed/cancelled jobs and jobs that depend on them").action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client("user").actions.runs.rerun(id2, { failed_only: !!o.failed }), renderRun2)));
51825
52576
  }
51826
52577
  function registerSecret(program2, rt) {
51827
52578
  const secret = program2.command("secret").description("Actions secrets of --repo (default) or the organization (--org-level). Values are write-only");
@@ -52402,6 +53153,10 @@ ${res.stdout.slice(-8e3)}`;
52402
53153
  // packages/cli/src/runner/build-image-step.ts
52403
53154
  var BUILDKIT_IMAGE = "moby/buildkit:v0.34.0@sha256:b059f8d7226d0b326bc871af489a5dddf65e36af4d20d14251b072974d9ee87c";
52404
53155
  var COSIGN_IMAGE = "ghcr.io/sigstore/cosign/cosign:v3.1.3@sha256:9e5c2f2edc34351160407ca3416c61855bdf9403c3c5936e0f0be7fc261611b8";
53156
+ var BUILDKIT_RELEASE = { url: "https://github.com/moby/buildkit/releases/download/v0.34.0/buildkit-v0.34.0.linux-amd64.tar.gz", sha256: "14813b1fb2a40082114e6d386c6fc5e7dd5298176ce7ee1a18bc33e73e109ac3" };
53157
+ var COSIGN_RELEASE = { url: "https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign-linux-amd64", sha256: "4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71" };
53158
+ var HOSTED_DIRS = { tools: "/opt/brnch/tools", state: "/var/lib/brnch-buildkit", run: "/run/brnch-buildkit", client: "/run/brnch-buildkit-client", context: "/run/brnch-build-context" };
53159
+ var BUILD_UID = 10999;
52405
53160
  var MAX_METADATA_OUTPUT = 32 * 1024;
52406
53161
  var list2 = (v) => (v ?? "").split(/[\n,]/).map((s) => s.trim()).filter(Boolean);
52407
53162
  var lines = (v) => (v ?? "").split("\n").map((s) => s.trim()).filter(Boolean);
@@ -52454,15 +53209,15 @@ function inside2(p) {
52454
53209
  }
52455
53210
  return parts.join("/");
52456
53211
  }
52457
- function buildctlArgs(i, metadataFile) {
52458
- const ctx = inside2(i.context);
53212
+ function buildLayout(i) {
53213
+ const context = inside2(i.context);
52459
53214
  const file2 = inside2(i.file ?? `${i.context}/Dockerfile`);
52460
- if (ctx === null) return `context: ${i.context} is outside the workspace`;
52461
- if (file2 === null) return `file: ${i.file} is outside the workspace`;
52462
- const slash = file2.lastIndexOf("/");
52463
- const dir = slash >= 0 ? file2.slice(0, slash) : "";
52464
- const base2 = slash >= 0 ? file2.slice(slash + 1) : file2;
52465
- const args = ["build", "--frontend", "dockerfile.v0", "--local", `context=/workspace/${ctx}`.replace(/\/$/, ""), "--local", `dockerfile=/workspace/${dir}`.replace(/\/$/, ""), "--opt", `filename=${base2}`];
53215
+ if (context === null) return `context: ${i.context} is outside the workspace`;
53216
+ if (file2 === null || file2 === "") return `file: ${i.file} is outside the workspace`;
53217
+ return { context, file: file2, filename: file2.slice(file2.lastIndexOf("/") + 1) };
53218
+ }
53219
+ function buildctlArgs(i, layout, metadataFile, root = "/workspace") {
53220
+ const args = ["build", "--frontend", "dockerfile.v0", "--local", `context=${root}/context`, "--local", `dockerfile=${root}/dockerfile`, "--opt", `filename=${layout.filename}`];
52466
53221
  args.push("--opt", `platform=${i.platforms.join(",")}`);
52467
53222
  if (i.target) args.push("--opt", `target=${i.target}`);
52468
53223
  for (const [k, v] of Object.entries(i.buildArgs)) args.push("--opt", `build-arg:${k}=${v}`);
@@ -52475,6 +53230,215 @@ function buildctlArgs(i, metadataFile) {
52475
53230
  args.push("--metadata-file", metadataFile);
52476
53231
  return args;
52477
53232
  }
53233
+ var quote2 = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
53234
+ async function copyContext(d, layout, owner) {
53235
+ const fresh2 = async (hosted) => {
53236
+ if (d.hosted) return await d.run(["sh", "-c", `rm -rf ${hosted} && install -d -m 0700 ${hosted}`], { env: d.env, timeoutMs: 6e4 }) === 0 ? hosted : null;
53237
+ const made = await d.capture(["mktemp", "-d", "/tmp/brnch-build-context.XXXXXXXX"], { env: d.env });
53238
+ const dir2 = made.stdout.trim();
53239
+ return made.code === 0 && dir2.startsWith("/tmp/brnch-build-context.") ? dir2 : null;
53240
+ };
53241
+ const staging = await fresh2(`${HOSTED_DIRS.context}-staging`);
53242
+ const dir = staging && await fresh2(HOSTED_DIRS.context);
53243
+ if (!staging || !dir) {
53244
+ if (staging) await d.run(["rm", "-rf", staging], { env: d.env, timeoutMs: 6e4 });
53245
+ return { error: "could not prepare the build context" };
53246
+ }
53247
+ const asStep = d.stepUser ? ["setpriv", `--reuid=${d.stepUser.uid}`, `--regid=${d.stepUser.gid}`, "--clear-groups", "--"] : [];
53248
+ const asOwner = owner !== null && owner !== 0 ? ["setpriv", `--reuid=${owner}`, `--regid=${owner}`, "--clear-groups", "--"] : [];
53249
+ const ws = (rel) => rel ? `${d.workspace}/${rel}` : d.workspace;
53250
+ const ignore = `${layout.filename}.dockerignore`;
53251
+ const stage = [
53252
+ "set -e",
53253
+ `w=$(cd -- ${quote2(d.workspace)} && pwd -P)`,
53254
+ 'inside() { case "$1" in "$w"/*) ;; *) echo "brnch/build-image@v1: $2 is outside the workspace" >&2; exit 3;; esac; }',
53255
+ `c=$(cd -- ${quote2(ws(layout.context))} && pwd -P)`,
53256
+ 'case "$c" in "$w"|"$w"/*) ;; *) echo "brnch/build-image@v1: the context is outside the workspace" >&2; exit 3;; esac',
53257
+ `fd=$(cd -- "$(dirname -- ${quote2(ws(layout.file))})" && pwd -P)`,
53258
+ `r=$(realpath -- "$fd/$(basename -- ${quote2(ws(layout.file))})")`,
53259
+ `inside "$r" ${quote2(layout.file)}`,
53260
+ `cp -a -- "$c/." ${quote2(`${staging}/context`)}`,
53261
+ `mkdir -- ${quote2(`${staging}/dockerfile`)}`,
53262
+ `cp -- "$r" ${quote2(`${staging}/dockerfile/${layout.filename}`)}`,
53263
+ // The name is the step's input: quoted into a variable, never into shell source (review of #287).
53264
+ `ig=${quote2(ignore)}`,
53265
+ 'if [ -e "$fd/$ig" ]; then i=$(realpath -- "$fd/$ig"); inside "$i" "$ig"; cp -- "$i" ' + quote2(`${staging}/dockerfile/${ignore}`) + "; fi",
53266
+ // Readable by the builder user for stage 2; the step's own files, nothing else.
53267
+ `chmod -R a+rX ${quote2(staging)}`
53268
+ ].join("; ");
53269
+ const fail = async () => {
53270
+ await d.run(["rm", "-rf", staging, dir], { env: d.env, timeoutMs: 6e4 });
53271
+ return { error: `could not copy the build context (${layout.context || "."}) and ${layout.file}: they must be a directory and a file in the workspace that the job can read` };
53272
+ };
53273
+ for (const cmd of [
53274
+ ...d.stepUser ? [["chown", `${d.stepUser.uid}:${d.stepUser.gid}`, staging], ["chmod", "0755", staging]] : [],
53275
+ [...asStep, "sh", "-c", stage],
53276
+ ...owner === null ? [] : [["chown", `${owner}:${owner}`, dir]],
53277
+ // No trailing "/.": a top directory swapped for a link is copied as that link, and refused below.
53278
+ [...asOwner, "cp", "-a", "--", `${staging}/context`, `${staging}/dockerfile`, `${dir}/`],
53279
+ ["sh", "-c", 'for p in "$1/context" "$1/dockerfile"; do [ -d "$p" ] && [ ! -L "$p" ] || exit 1; done', "sh", dir],
53280
+ ["rm", "-rf", staging]
53281
+ ]) {
53282
+ const done2 = await d.run(cmd, { env: d.env, timeoutMs: 10 * 6e4 });
53283
+ if (done2 === null) return null;
53284
+ if (done2 !== 0) return fail();
53285
+ }
53286
+ return dir;
53287
+ }
53288
+ function buildScopes(i, host, repository = "", dockerfile = "") {
53289
+ const name = (ref) => ref.slice(host.length + 1).replace(/@.*$/, "").replace(/:[^/]*$/, "");
53290
+ const want = /* @__PURE__ */ new Map();
53291
+ const add = (ref, actions) => {
53292
+ const n = name(ref);
53293
+ want.set(n, /* @__PURE__ */ new Set([...want.get(n) ?? [], ...actions]));
53294
+ };
53295
+ if (i.push) for (const t of i.tags) add(t, ["pull", "push"]);
53296
+ for (const c8 of i.cacheTo) add(/(?:^|,)ref=([^,]+)/.exec(c8)[1], ["pull", "push"]);
53297
+ for (const c8 of i.cacheFrom) add(/(?:^|,)ref=([^,]+)/.exec(c8)[1], ["pull"]);
53298
+ if (repository && dockerfile) {
53299
+ for (const ref of dockerfileImages(dockerfile)) if (ownImage(ref, host, repository)) add(ref, ["pull"]);
53300
+ }
53301
+ return [...want].map(([n, a]) => `repository:${n}:${[...a].sort().join(",")}`);
53302
+ }
53303
+ function dockerfileImages(dockerfile) {
53304
+ const out = [];
53305
+ const all = dockerfile.replace(/\r\n/g, "\n").split("\n");
53306
+ let escape = "\\";
53307
+ for (const l of all) {
53308
+ const d = /^\s*#\s*(syntax|escape|check)\s*=\s*(\S+)\s*$/i.exec(l);
53309
+ if (!d) break;
53310
+ if (d[1].toLowerCase() === "escape" && (d[2] === "`" || d[2] === "\\")) escape = d[2];
53311
+ }
53312
+ const uncommented = all.filter((l) => !l.trim().startsWith("#")).join("\n");
53313
+ const lines2 = uncommented.split(`${escape}
53314
+ `).join(" ").split("\n");
53315
+ const value = String.raw`("[^"]*"|'[^']*'|[^\s,]+)`;
53316
+ const unquote = (v) => /^(["']).*\1$/.test(v) ? v.slice(1, -1) : v;
53317
+ for (const raw of lines2) {
53318
+ const line2 = raw.trim();
53319
+ if (!line2) continue;
53320
+ const from = /^FROM\s+(?:--platform=\S+\s+)?(\S+)/i.exec(line2);
53321
+ if (from) out.push(from[1]);
53322
+ if (/^(COPY|ADD)\s/i.test(line2)) for (const m of line2.matchAll(new RegExp(`--from=${value}`, "g"))) out.push(unquote(m[1]));
53323
+ if (/^RUN\s/i.test(line2)) for (const m of line2.matchAll(new RegExp(String.raw`--mount=\S*?\bfrom=${value}`, "g"))) out.push(unquote(m[1]));
53324
+ }
53325
+ return out;
53326
+ }
53327
+ var OCI_PATH = /^[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*(?:\/[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*)*$/;
53328
+ function ownImage(ref, host, repository) {
53329
+ const m = /^([^/]+)\/([^:@]+)(?::[\w][\w.-]{0,127})?(?:@sha256:[a-f0-9]{64})?$/.exec(ref);
53330
+ if (!m || m[1] !== host || !OCI_PATH.test(m[2])) return false;
53331
+ const own2 = repository.toLowerCase();
53332
+ return m[2] === own2 || m[2].startsWith(`${own2}/`);
53333
+ }
53334
+ var BUILD_TOKEN_SECONDS = 21600;
53335
+ async function mintBuildToken(d, host, scopes) {
53336
+ const url2 = new URL(`${new URL(d.baseUrl).protocol}//${host}/v2/token`);
53337
+ url2.searchParams.set("service", host);
53338
+ for (const s of scopes) url2.searchParams.append("scope", s);
53339
+ url2.searchParams.set("expires_in", String(BUILD_TOKEN_SECONDS));
53340
+ try {
53341
+ const res = await d.fetch(url2.toString(), { headers: { authorization: `Bearer ${d.jobToken}` } });
53342
+ const body = await res.json().catch(() => null);
53343
+ if (res.ok && typeof body?.token === "string" && body.token) return body.token;
53344
+ return { error: `the registry did not issue a token for this build (HTTP ${res.status})` };
53345
+ } catch (e) {
53346
+ return { error: `could not reach the registry for a token: ${e.message}` };
53347
+ }
53348
+ }
53349
+ function dockerBuilder(d, auth, context) {
53350
+ const name = `brnch-buildkit-${d.jobId.replace(/[^a-z0-9]/gi, "").toLowerCase()}`;
53351
+ const docker = (a, timeoutMs = 10 * 6e4, env = d.env) => d.run(["docker", ...a], { env, timeoutMs });
53352
+ return {
53353
+ metadataFile: "/tmp/brnch-metadata.json",
53354
+ contextRoot: "/workspace",
53355
+ async start() {
53356
+ d.log(`##[group]Start BuildKit (${BUILDKIT_IMAGE.split("@")[0]})`);
53357
+ try {
53358
+ const started = await docker(["run", "-d", "--rm", "--privileged", "--name", name, "-v", `${context}:/workspace:ro`, "-v", `${auth}:/root/.docker:ro`, BUILDKIT_IMAGE]);
53359
+ if (started === null) return null;
53360
+ if (started !== 0) return "could not start the BuildKit container";
53361
+ return await waitReady(() => d.capture(["docker", "exec", name, "buildctl", "debug", "workers"], { env: d.env })) ? "" : "BuildKit did not become ready within 30 seconds";
53362
+ } finally {
53363
+ d.log("##[endgroup]");
53364
+ }
53365
+ },
53366
+ buildctl: (args) => docker(["exec", name, "buildctl", ...args], null),
53367
+ readMetadata: async () => (await d.capture(["docker", "exec", name, "cat", "/tmp/brnch-metadata.json"], { env: d.env })).stdout,
53368
+ sign: (args, env) => docker(["run", "--rm", "--user", "0", "-v", `${auth}:/root/.docker:ro`, "-e", "DOCKER_CONFIG=/root/.docker", "-e", "COSIGN_KEY", "-e", "COSIGN_PASSWORD", COSIGN_IMAGE, ...args], 5 * 6e4, { ...d.env, ...env }),
53369
+ stop: async () => {
53370
+ await d.capture(["docker", "rm", "-f", name], { env: d.env });
53371
+ }
53372
+ };
53373
+ }
53374
+ function hostedBuilder(d, auth, signing, context) {
53375
+ const tools = `${HOSTED_DIRS.tools}/build-image`;
53376
+ const bin = `${tools}/buildkit/bin`;
53377
+ const sock = `unix://${HOSTED_DIRS.client}/buildkitd.sock`;
53378
+ const asBuilder = (cmd) => ["setpriv", `--reuid=${BUILD_UID}`, `--regid=${BUILD_UID}`, "--clear-groups", "--", ...cmd];
53379
+ const buildctl = `${HOSTED_DIRS.client}/buildctl`;
53380
+ const clientEnv = { ...d.env, DOCKER_CONFIG: auth, HOME: HOSTED_DIRS.client };
53381
+ const run3 = (cmd, timeoutMs = 10 * 6e4, env = d.env) => d.run(cmd, { env, timeoutMs });
53382
+ const fetchChecked = async (url2, sha2562, out) => {
53383
+ const got = await run3(["curl", "-fsSL", "--retry", "3", "-o", out, url2], 5 * 6e4);
53384
+ if (got === null) return null;
53385
+ if (got !== 0) return `could not download ${url2}`;
53386
+ const sum = (await d.capture(["sha256sum", out], { env: d.env })).stdout.split(/\s/)[0];
53387
+ return sum === sha2562 ? "" : `${url2} has sha256 ${sum || "(unreadable)"}, not the pinned ${sha2562}`;
53388
+ };
53389
+ return {
53390
+ metadataFile: `${HOSTED_DIRS.client}/metadata.json`,
53391
+ contextRoot: context,
53392
+ async start() {
53393
+ d.log(`##[group]Start BuildKit (${BUILDKIT_RELEASE.url.split("/").at(-1)}, beside the job)`);
53394
+ try {
53395
+ for (const dir of [HOSTED_DIRS.tools, tools, `${tools}/buildkit`, HOSTED_DIRS.state, HOSTED_DIRS.run]) {
53396
+ const made = await run3(["install", "-d", "-m", "0700", dir]);
53397
+ if (made !== 0) return made === null ? null : `could not create ${dir}`;
53398
+ }
53399
+ for (const cmd of [["install", "-d", "-m", "0700", "-o", String(BUILD_UID), "-g", String(BUILD_UID), HOSTED_DIRS.client], ["chown", "-R", `${BUILD_UID}:${BUILD_UID}`, auth]]) {
53400
+ const done2 = await run3(cmd);
53401
+ if (done2 !== 0) return done2 === null ? null : `could not prepare the build user (${cmd[0]})`;
53402
+ }
53403
+ const tgz = `${tools}/buildkit.tar.gz`;
53404
+ const got = await fetchChecked(BUILDKIT_RELEASE.url, BUILDKIT_RELEASE.sha256, tgz);
53405
+ if (got !== "") return got;
53406
+ const unpacked = await run3(["tar", "-xzf", tgz, "-C", `${tools}/buildkit`]);
53407
+ if (unpacked !== 0) return unpacked === null ? null : "could not unpack BuildKit";
53408
+ const client = await run3(["install", "-m", "0755", `${bin}/buildctl`, buildctl]);
53409
+ if (client !== 0) return client === null ? null : "could not install buildctl for the build user";
53410
+ if (signing) {
53411
+ const cosign = await fetchChecked(COSIGN_RELEASE.url, COSIGN_RELEASE.sha256, `${tools}/cosign`);
53412
+ if (cosign !== "") return cosign;
53413
+ if (await run3(["chmod", "0700", `${tools}/cosign`]) !== 0) return "could not install cosign";
53414
+ }
53415
+ const daemon = `${bin}/buildkitd --root ${HOSTED_DIRS.state} --addr ${sock} --group ${BUILD_UID} --oci-worker-snapshotter=overlayfs --containerd-worker=false`;
53416
+ const started = await run3(["sh", "-c", `PATH=${bin}:$PATH nohup ${daemon} > ${HOSTED_DIRS.run}/buildkitd.log 2>&1 & echo $! > ${HOSTED_DIRS.run}/buildkitd.pid`]);
53417
+ if (started !== 0) return started === null ? null : "could not start buildkitd";
53418
+ if (await waitReady(() => d.capture(asBuilder([buildctl, "--addr", sock, "debug", "workers"]), { env: clientEnv }))) return "";
53419
+ d.log((await d.capture(["tail", "-20", `${HOSTED_DIRS.run}/buildkitd.log`], { env: d.env })).stdout);
53420
+ return "BuildKit did not become ready within 30 seconds";
53421
+ } finally {
53422
+ d.log("##[endgroup]");
53423
+ }
53424
+ },
53425
+ // buildctl reads registry credentials from DOCKER_CONFIG on the client side and hands them to buildkitd's session.
53426
+ buildctl: (args) => run3(asBuilder([buildctl, "--addr", sock, ...args]), null, clientEnv),
53427
+ readMetadata: async () => (await d.capture(["cat", `${HOSTED_DIRS.client}/metadata.json`], { env: d.env })).stdout,
53428
+ sign: (args, env) => run3([`${tools}/cosign`, ...args], 5 * 6e4, { ...d.env, ...env, DOCKER_CONFIG: auth, HOME: auth }),
53429
+ stop: async () => {
53430
+ const pid = `${HOSTED_DIRS.run}/buildkitd.pid`;
53431
+ await d.capture(["sh", "-c", `p=$(cat ${pid} 2>/dev/null); if [ -n "$p" ]; then kill $p 2>/dev/null; for i in 1 2 3 4 5 6 7 8 9 10; do kill -0 $p 2>/dev/null || break; sleep 1; done; kill -9 $p 2>/dev/null; fi; rm -rf ${HOSTED_DIRS.client} ${pid}`], { env: d.env });
53432
+ }
53433
+ };
53434
+ }
53435
+ async function waitReady(probe) {
53436
+ for (let i = 0; i < 30; i++) {
53437
+ if ((await probe()).code === 0) return true;
53438
+ await new Promise((r) => setTimeout(r, 1e3));
53439
+ }
53440
+ return false;
53441
+ }
52478
53442
  async function runBuildImage(w, d) {
52479
53443
  const fail = (msg) => {
52480
53444
  d.log(`##[error]brnch/build-image@v1: ${msg}`);
@@ -52486,41 +53450,46 @@ async function runBuildImage(w, d) {
52486
53450
  const bad = checkRefs(parsed, host, d.repository);
52487
53451
  if (bad) return fail(bad);
52488
53452
  if (parsed.push && !parsed.tags.length) return fail("tags are required when pushing (or set push: false)");
52489
- if (d.hosted) {
52490
- return fail("hosted runners cannot build images yet: BuildKit inside Cloudflare Containers is waiting on a capability probe (ADR-0083 \xA76). Run this job on a self-hosted runner with Docker (runs-on: [self-hosted, \u2026]).");
52491
- }
52492
- if ((await d.capture(["docker", "info", "--format", "{{.ServerVersion}}"], { env: d.env })).code !== 0) {
53453
+ if (d.hosted && parsed.platforms.some((p) => p !== "linux/amd64")) return fail("hosted runners are x86_64 and build linux/amd64 only; build linux/arm64 on a self-hosted runner that can");
53454
+ if (!d.hosted && (await d.capture(["docker", "info", "--format", "{{.ServerVersion}}"], { env: d.env })).code !== 0) {
52493
53455
  return fail("this runner has no usable Docker (docker info failed); install Docker or run the job on a runner that has it");
52494
53456
  }
52495
- const args = buildctlArgs(parsed, "/tmp/brnch-metadata.json");
52496
- if (typeof args === "string") return fail(args);
52497
- const auth = `${d.temp}/build-image-${d.jobId}`;
52498
- d.writePrivate(`${auth}/config.json`, JSON.stringify({ auths: { [host]: { auth: btoa(`brnch:${d.jobToken}`) } } }));
52499
- const name = `brnch-buildkit-${d.jobId.replace(/[^a-z0-9]/gi, "").toLowerCase()}`;
52500
- const docker = (a, timeoutMs = 10 * 6e4, env = d.env) => d.run(["docker", ...a], { env, timeoutMs });
53457
+ const layout = buildLayout(parsed);
53458
+ if (typeof layout === "string") return fail(layout);
53459
+ const context = await copyContext(d, layout, d.hosted ? BUILD_UID : d.stepUser ? 0 : null);
53460
+ if (context === null) return { outcome: "cancelled", outputs: {} };
53461
+ if (typeof context !== "string") return fail(context.error);
53462
+ const auth = d.hosted ? `${HOSTED_DIRS.client}/docker` : `${d.temp}/build-image-${d.jobId}`;
53463
+ const builder = d.hosted ? hostedBuilder(d, auth, !!parsed.cosignKey && parsed.push, context) : dockerBuilder(d, auth, context);
53464
+ const args = buildctlArgs(parsed, layout, builder.metadataFile, builder.contextRoot);
53465
+ const dockerfile = (await d.capture(["cat", "--", `${context}/dockerfile/${layout.filename}`], { env: d.env })).stdout;
53466
+ const scopes = buildScopes(parsed, host, d.repository, dockerfile);
53467
+ let credential = null;
53468
+ if (scopes.length) {
53469
+ const minted = await mintBuildToken(d, host, scopes);
53470
+ if (typeof minted !== "string") {
53471
+ await d.run(["rm", "-rf", context], { env: d.env, timeoutMs: 6e4 });
53472
+ return fail(minted.error);
53473
+ }
53474
+ credential = minted;
53475
+ }
53476
+ d.writePrivate(`${auth}/config.json`, JSON.stringify({ auths: credential ? { [host]: { auth: btoa(`brnch:${credential}`) } } : {} }));
52501
53477
  try {
52502
- d.log(`##[group]Start BuildKit (${BUILDKIT_IMAGE.split("@")[0]})`);
52503
- const started = await docker(["run", "-d", "--rm", "--privileged", "--name", name, "-v", `${d.workspace}:/workspace:ro`, "-v", `${auth}:/root/.docker:ro`, BUILDKIT_IMAGE]);
52504
- if (started !== 0) return started === null ? { outcome: "cancelled", outputs: {} } : fail("could not start the BuildKit container");
52505
- let ready = false;
52506
- for (let i = 0; i < 30 && !ready; i++) {
52507
- ready = (await d.capture(["docker", "exec", name, "buildctl", "debug", "workers"], { env: d.env })).code === 0;
52508
- if (!ready) await new Promise((r) => setTimeout(r, 1e3));
52509
- }
52510
- d.log("##[endgroup]");
52511
- if (!ready) return fail("BuildKit did not become ready within 30 seconds");
53478
+ const started = await builder.start();
53479
+ if (started === null) return { outcome: "cancelled", outputs: {} };
53480
+ if (started) return fail(started);
52512
53481
  d.log(`##[group]Build ${parsed.tags.join(", ") || "(no tags)"} for ${parsed.platforms.join(", ")}`);
52513
- const built = await docker(["exec", name, "buildctl", ...args], null);
53482
+ const built = await builder.buildctl(args);
52514
53483
  d.log("##[endgroup]");
52515
53484
  if (built === null) return { outcome: "cancelled", outputs: {} };
52516
53485
  if (built !== 0) return fail(`the build failed (buildctl exited ${built})`);
52517
- const meta3 = await d.capture(["docker", "exec", name, "cat", "/tmp/brnch-metadata.json"], { env: d.env });
53486
+ const meta3 = await builder.readMetadata();
52518
53487
  let digest = "";
52519
53488
  try {
52520
- digest = String(JSON.parse(meta3.stdout)["containerimage.digest"] ?? "");
53489
+ digest = String(JSON.parse(meta3)["containerimage.digest"] ?? "");
52521
53490
  } catch {
52522
53491
  }
52523
- const outputs = { metadata: meta3.stdout.slice(0, MAX_METADATA_OUTPUT) };
53492
+ const outputs = { metadata: meta3.slice(0, MAX_METADATA_OUTPUT) };
52524
53493
  if (parsed.push) {
52525
53494
  if (!/^sha256:[a-f0-9]{64}$/.test(digest)) return fail("the build pushed, but BuildKit reported no image digest");
52526
53495
  const first2 = parsed.tags[0].replace(/[:@][^/]*$/, "");
@@ -52529,9 +53498,9 @@ async function runBuildImage(w, d) {
52529
53498
  d.log(`pushed ${outputs.image}`);
52530
53499
  if (parsed.cosignKey) {
52531
53500
  d.log(`##[group]Sign ${outputs.image} with cosign`);
52532
- const signArgs = ["run", "--rm", "--user", "0", "-v", `${auth}:/root/.docker:ro`, "-e", "DOCKER_CONFIG=/root/.docker", "-e", "COSIGN_KEY", "-e", "COSIGN_PASSWORD", COSIGN_IMAGE, "sign", "--key", "env://COSIGN_KEY", "--yes"];
53501
+ const signArgs = ["sign", "--key", "env://COSIGN_KEY", "--yes"];
52533
53502
  if (!parsed.cosignTlog) signArgs.push("--use-signing-config=false", "--tlog-upload=false");
52534
- const signed = await docker([...signArgs, outputs.image], 5 * 6e4, { ...d.env, COSIGN_KEY: parsed.cosignKey, COSIGN_PASSWORD: parsed.cosignPassword ?? "" });
53503
+ const signed = await builder.sign([...signArgs, outputs.image], { COSIGN_KEY: parsed.cosignKey, COSIGN_PASSWORD: parsed.cosignPassword ?? "" });
52535
53504
  d.log("##[endgroup]");
52536
53505
  if (signed === null) return { outcome: "cancelled", outputs };
52537
53506
  if (signed !== 0) return fail(`cosign could not sign ${outputs.image} (exit ${signed})`);
@@ -52542,8 +53511,9 @@ async function runBuildImage(w, d) {
52542
53511
  }
52543
53512
  return { outcome: "success", outputs };
52544
53513
  } finally {
52545
- await d.capture(["docker", "rm", "-f", name], { env: d.env });
53514
+ await builder.stop();
52546
53515
  d.removeDir(auth);
53516
+ await d.run(["rm", "-rf", context], { env: d.env, timeoutMs: 6e4 });
52547
53517
  }
52548
53518
  }
52549
53519
 
@@ -53218,6 +54188,14 @@ async function runCodeReviewStep(spec, host) {
53218
54188
  return ok && parsed.status !== "failed" ? "success" : "failure";
53219
54189
  }
53220
54190
 
54191
+ // packages/cli/src/runner/checkout-retry.ts
54192
+ var CHECKOUT_RETRY_DELAYS_MS = [15e3, 3e4, 6e4, 12e4, 24e4];
54193
+ var RPC_FAILED = /\bRPC failed; (?:HTTP (?:429|5\d\d)\b|curl (?:6|7|16|18|28|35|52|55|56|92)\b)/;
54194
+ var UNABLE_TO_ACCESS = /\bunable to access '[^']*': (?:The requested URL returned error: (?:429|5\d\d)\b|Failed to connect|Could not resolve host|Connection timed out|Operation timed out|Recv failure|Send failure|Empty reply from server|OpenSSL SSL_)/;
54195
+ function transientGitFailure(line2) {
54196
+ return RPC_FAILED.test(line2) || UNABLE_TO_ACCESS.test(line2);
54197
+ }
54198
+
53221
54199
  // packages/cli/src/runner/executor.ts
53222
54200
  var NODE_OS = { linux: "Linux", darwin: "macOS", win32: "Windows" };
53223
54201
  var NODE_ARCH = { x64: "X64", arm64: "ARM64", arm: "ARM" };
@@ -53467,6 +54445,12 @@ var JobExecution = class {
53467
54445
  }
53468
54446
  spawnSync2("rm", ["-rf", "--", gone], { uid: this.owner.uid, gid: this.owner.gid, stdio: "ignore" });
53469
54447
  }
54448
+ /** Wait `ms`, cut short when the job is cancelled: whether the job may go on. */
54449
+ async pause(ms) {
54450
+ const until = Date.now() + ms;
54451
+ while (!this.cancelled && Date.now() < until) await new Promise((r) => setTimeout(r, Math.min(1e3, until - Date.now())));
54452
+ return !this.cancelled;
54453
+ }
53470
54454
  /** Run one process with streamed, masked output. Resolves to its exit code, or null if it was killed. */
53471
54455
  exec(cmd, o) {
53472
54456
  return new Promise((resolveExit) => {
@@ -53744,18 +54728,41 @@ var JobExecution = class {
53744
54728
  }
53745
54729
  const depth = w["fetch-depth"] === void 0 ? 1 : Number(w["fetch-depth"]);
53746
54730
  const env = this.gitEnv();
53747
- const git4 = (args) => this.exec(["git", ...args], { cwd: this.workspace, env, asStepUser: false, timeoutMs: 30 * 6e4 });
54731
+ let transient = false;
54732
+ const onLine = (l) => {
54733
+ if (transientGitFailure(l)) transient = true;
54734
+ return false;
54735
+ };
54736
+ const git4 = (args) => {
54737
+ transient = false;
54738
+ return this.exec(["git", ...args], { cwd: this.workspace, env, asStepUser: false, timeoutMs: 30 * 6e4, onLine });
54739
+ };
54740
+ const fetchCommit = async (args) => {
54741
+ for (let attempt = 0; ; attempt++) {
54742
+ const code2 = await git4(["fetch", ...args]);
54743
+ const delay = CHECKOUT_RETRY_DELAYS_MS[attempt];
54744
+ if (code2 === 0 || code2 === null || !transient) return code2;
54745
+ if (delay === void 0 || Date.now() + delay >= this.deadline) {
54746
+ this.log("##[error]the server kept failing the fetch; the repository's storage may be busy. Run the job again in a few minutes");
54747
+ return code2;
54748
+ }
54749
+ this.log(`##[brnch] the server could not answer the fetch; retrying in ${delay / 1e3} s (attempt ${attempt + 2} of ${CHECKOUT_RETRY_DELAYS_MS.length + 1})`);
54750
+ if (!await this.pause(delay)) return null;
54751
+ }
54752
+ };
53748
54753
  this.discard(this.workspace);
53749
54754
  mkdirSync3(this.workspace, { mode: 493 });
53750
54755
  const branch = a.ref.startsWith("refs/heads/") ? a.ref.slice("refs/heads/".length) : null;
53751
54756
  const fetchAndCheckout = async () => {
53752
54757
  for (const args of [["init", "-q"], ["remote", "add", "origin", this.gitUrl()]]) if (await git4(args) !== 0) return 1;
53753
- let code2 = depth > 0 ? await git4(["fetch", "--no-tags", "--prune", `--depth=${depth}`, "origin", a.head_sha]) : 1;
53754
- if (code2 !== 0) {
53755
- if (this.cancelled) return null;
53756
- if (depth > 0) this.log("##[brnch] shallow fetch is not available here; fetching full history of the commit");
53757
- code2 = await git4(["fetch", "--no-tags", "origin", a.head_sha]);
53758
- }
54758
+ let code2;
54759
+ if (depth > 0) {
54760
+ code2 = await fetchCommit(["--no-tags", "--prune", `--depth=${depth}`, "origin", a.head_sha]);
54761
+ if (code2 !== 0 && code2 !== null && !transient) {
54762
+ this.log("##[brnch] the shallow fetch failed; fetching full history of the commit");
54763
+ code2 = await fetchCommit(["--no-tags", "origin", a.head_sha]);
54764
+ }
54765
+ } else code2 = await fetchCommit(["--no-tags", "origin", a.head_sha]);
53759
54766
  if (code2 !== 0) return code2;
53760
54767
  return git4(branch ? ["checkout", "-q", "-B", branch, a.head_sha] : ["checkout", "-q", "--detach", a.head_sha]);
53761
54768
  };
@@ -53800,6 +54807,7 @@ var JobExecution = class {
53800
54807
  async buildImageStep(w, env) {
53801
54808
  const r = await runBuildImage(w, {
53802
54809
  baseUrl: this.opts.baseUrl,
54810
+ fetch: (url2, init) => fetch(url2, init),
53803
54811
  registryHost: this.a.registry_host ?? null,
53804
54812
  repository: this.a.repository.full_name,
53805
54813
  jobToken: this.a.job_token,
@@ -53807,6 +54815,7 @@ var JobExecution = class {
53807
54815
  workspace: this.workspace,
53808
54816
  temp: this.temp,
53809
54817
  hosted: this.opts.hosted,
54818
+ stepUser: this.opts.stepUser ? { uid: this.opts.stepUser.uid, gid: this.opts.stepUser.gid } : null,
53810
54819
  log: (l) => this.log(l),
53811
54820
  run: (cmd, o) => this.exec(cmd, { cwd: this.workspace, env: o.env, asStepUser: false, timeoutMs: o.timeoutMs, onLine: this.onCommandLine }),
53812
54821
  capture: async (cmd, o) => {
@@ -54021,12 +55030,81 @@ function sanitizedEnv(env) {
54021
55030
  return out;
54022
55031
  }
54023
55032
 
55033
+ // packages/cli/src/runner/resources.ts
55034
+ import { statfs } from "node:fs/promises";
55035
+ import { cpus, freemem, totalmem, uptime } from "node:os";
55036
+ function cpuPercent(before, after) {
55037
+ if (!before.length || before.length !== after.length) return null;
55038
+ let idle = 0;
55039
+ let total = 0;
55040
+ for (let i = 0; i < after.length; i++) {
55041
+ for (const key of ["user", "nice", "sys", "idle", "irq"]) {
55042
+ const delta = after[i].times[key] - before[i].times[key];
55043
+ if (delta < 0 || !Number.isFinite(delta)) return null;
55044
+ total += delta;
55045
+ if (key === "idle") idle += delta;
55046
+ }
55047
+ }
55048
+ return total > 0 ? Math.round((1 - idle / total) * 1e3) / 10 : null;
55049
+ }
55050
+ var RunnerResourceCollector = class {
55051
+ constructor(workDir, jobSlots) {
55052
+ this.workDir = workDir;
55053
+ this.jobSlots = jobSlots;
55054
+ }
55055
+ workDir;
55056
+ jobSlots;
55057
+ previous = null;
55058
+ disk = null;
55059
+ diskPending = false;
55060
+ sample() {
55061
+ const at = performance.now();
55062
+ try {
55063
+ this.refreshDisk();
55064
+ const current = cpus();
55065
+ const interval = this.previous ? Math.round(at - this.previous.at) : 0;
55066
+ const cpu = this.previous && interval > 0 && interval <= 6e5 ? cpuPercent(this.previous.cpus, current) : null;
55067
+ this.previous = { cpus: current, at };
55068
+ const disk = this.disk && at - this.disk.at <= 6e4 ? this.disk : null;
55069
+ const totalMemory = totalmem();
55070
+ const parsed = RunnerResourceSnapshot.safeParse({
55071
+ cpu_percent: cpu,
55072
+ cpu_count: current.length,
55073
+ cpu_interval_ms: cpu === null ? null : interval,
55074
+ memory_total_bytes: totalMemory,
55075
+ memory_available_bytes: Math.min(totalMemory, Math.max(0, freemem())),
55076
+ disk_total_bytes: disk?.total ?? null,
55077
+ disk_available_bytes: disk?.available ?? null,
55078
+ uptime_seconds: Math.floor(uptime()),
55079
+ job_slots: this.jobSlots
55080
+ });
55081
+ return parsed.success ? parsed.data : void 0;
55082
+ } catch {
55083
+ return void 0;
55084
+ }
55085
+ }
55086
+ refreshDisk() {
55087
+ if (this.diskPending) return;
55088
+ this.diskPending = true;
55089
+ void statfs(this.workDir).then((s) => {
55090
+ const total = s.blocks * s.bsize;
55091
+ const available = Math.min(total, Math.max(0, s.bavail * s.bsize));
55092
+ this.disk = Number.isSafeInteger(total) && total > 0 && Number.isSafeInteger(available) ? { total, available, at: performance.now() } : null;
55093
+ }).catch(() => {
55094
+ this.disk = null;
55095
+ }).finally(() => {
55096
+ this.diskPending = false;
55097
+ });
55098
+ }
55099
+ };
55100
+
54024
55101
  // packages/cli/src/runner/agent.ts
54025
55102
  var RunnerAgent = class {
54026
55103
  constructor(cfg, opts) {
54027
55104
  this.cfg = cfg;
54028
55105
  this.opts = opts;
54029
55106
  this.client = this.clientFor(cfg);
55107
+ this.resources = new RunnerResourceCollector(opts.workDir, opts.once || cfg.ephemeral ? 1 : opts.jobs);
54030
55108
  }
54031
55109
  cfg;
54032
55110
  opts;
@@ -54038,6 +55116,7 @@ var RunnerAgent = class {
54038
55116
  exit = null;
54039
55117
  heartbeatTimer = null;
54040
55118
  taken = 0;
55119
+ resources;
54041
55120
  clientFor(cfg) {
54042
55121
  return createClient({ baseUrl: cfg.url, token: cfg.credential, userAgent: `${BRAND.cli}-runner/${BRAND.version}`, headers: { "x-brnch-runner-version": BRAND.version }, ...this.opts.fetch ? { fetch: this.opts.fetch } : {} });
54043
55122
  }
@@ -54075,7 +55154,7 @@ var RunnerAgent = class {
54075
55154
  }
54076
55155
  async heartbeat() {
54077
55156
  try {
54078
- const r = await this.client.runner.heartbeat({ running_jobs: [...this.running.keys()] });
55157
+ const r = await this.client.runner.heartbeat({ running_jobs: [...this.running.keys()], resources: this.resources.sample() });
54079
55158
  for (const id2 of r.cancel_jobs) this.running.get(id2)?.exec.cancel("cancelled by the server");
54080
55159
  for (const id2 of r.forget_jobs) this.running.get(id2)?.exec.abort("the server reassigned this job");
54081
55160
  if (r.removed) {
@@ -54122,6 +55201,7 @@ var RunnerAgent = class {
54122
55201
  await this.client.runner.diagnostics(diagnostics(this.cfg)).catch(() => void 0);
54123
55202
  const unhook = this.opts.onSignal?.((s) => this.shutdown(s)) ?? (() => void 0);
54124
55203
  this.heartbeatTimer = setInterval(() => void this.heartbeat(), Math.max(1, me.heartbeat_seconds) * 1e3);
55204
+ await this.heartbeat();
54125
55205
  const single = this.opts.once || this.cfg.ephemeral;
54126
55206
  let backoff = 1e3;
54127
55207
  try {
@@ -55139,6 +56219,13 @@ var EXAMPLES = {
55139
56219
  "deploy run": [`${c7} deploy run production --wait || echo "deploy failed (exit $?)"`, `${c7} deploy run dpt_123 --commit 0123456789abcdef0123456789abcdef01234567`],
55140
56220
  "deploy update": [`${c7} deploy update production --build "npm run build:prod"`, `${c7} deploy update production --disable`],
55141
56221
  "deploy rollback": [`${c7} deploy rollback production --to dpl_123`],
56222
+ "db sources add": [
56223
+ `${c7} db suggest # the migration directories found`,
56224
+ `${c7} db sources add --name app --path db/migrations --format atlas --engine postgres --connection icn_123 --organization acme --database app --revisions-schema app_atlas`,
56225
+ `op read "op://Infra/app-dsn/credential" | ${c7} db sources add --name app \u2026 --credential-mode dsn --dsn-file -`
56226
+ ],
56227
+ "db plan": [`${c7} db plan app`, `${c7} db plan app --ref my-branch --live`],
56228
+ "db approve": [`${c7} db runs --source app`, `${c7} db approve dmr_123 --reason "reviewed the plan"`],
55142
56229
  version: [`${c7} version`, `${c7} version --json`],
55143
56230
  upgrade: [`${c7} upgrade --check`, `${c7} upgrade`, `${c7} upgrade --force`],
55144
56231
  doctor: [`${c7} doctor`, `${c7} doctor --json | jq '.checks[] | select(.status != "ok")'`],
@@ -55279,6 +56366,7 @@ function buildProgram(rt) {
55279
56366
  registerMemory(program2, rt);
55280
56367
  registerIntegrations(program2, rt);
55281
56368
  registerDeploy(program2, rt);
56369
+ registerDb(program2, rt);
55282
56370
  registerDelegate(program2, rt);
55283
56371
  registerAgents(program2, rt);
55284
56372
  registerCodingAgents(program2, rt);
@@ -55353,17 +56441,44 @@ function report2(e, deps, state2) {
55353
56441
  `);
55354
56442
  return exitCode;
55355
56443
  }
55356
- async function run2(argv, overrides = {}) {
56444
+ function agentToolFlag(argv) {
56445
+ for (let i = 0; i < argv.length; i++) {
56446
+ if (argv[i] === "--agent-tool") return argv[i + 1] ?? null;
56447
+ if (argv[i].startsWith("--agent-tool=")) return argv[i].slice("--agent-tool=".length);
56448
+ }
56449
+ return null;
56450
+ }
56451
+ async function run2(argv, overrides = {}, parentStatus) {
55357
56452
  const merged = { ...nodeDeps(), ...overrides };
55358
- const deps = merged;
55359
56453
  const split = splitGitArgs(argv);
55360
56454
  const state2 = { exitCode: EXIT.OK, wroteResult: false, secrets: [], json: wantsJson(split.argv) };
56455
+ state2.status = parentStatus ?? (programStatusEnabled(merged, detectAgent(merged.env, agentToolFlag(split.argv))) ? statusReporter(merged.stderr) : silentStatus);
56456
+ const status = state2.status;
56457
+ const deps = {
56458
+ ...merged,
56459
+ prompt: async (q) => {
56460
+ const answered2 = status.ask();
56461
+ try {
56462
+ return await merged.prompt(q);
56463
+ } finally {
56464
+ answered2();
56465
+ }
56466
+ },
56467
+ keys: merged.keys ? (fn2) => {
56468
+ const answered2 = status.ask();
56469
+ const stop = merged.keys(fn2);
56470
+ return () => {
56471
+ stop();
56472
+ answered2();
56473
+ };
56474
+ } : null
56475
+ };
55361
56476
  const invoke2 = async (sub, o = {}) => {
55362
56477
  const out = [];
55363
56478
  const err = [];
55364
56479
  const errSink = (s) => void err.push(s);
55365
- const quiet = o.quiet ? { stderr: { write: errSink }, stderrIsTTY: false, spawnGit: (args, g2) => deps.spawnGit(args, { ...g2, onOutput: errSink }) } : {};
55366
- const code2 = await run2(sub, { ...deps, ...o.cwd ? { cwd: o.cwd } : {}, ...o.capture ? { stdout: { write: (s) => out.push(s) } } : {}, ...quiet });
56480
+ const quiet = o.quiet ? { stderr: { write: errSink }, stderrIsTTY: false, spawnGit: (args, g2) => merged.spawnGit(args, { ...g2, onOutput: errSink }) } : {};
56481
+ const code2 = await run2(sub, { ...merged, ...o.cwd ? { cwd: o.cwd } : {}, ...o.capture ? { stdout: { write: (s) => out.push(s) } } : {}, ...quiet }, status);
55367
56482
  return { code: code2, stdout: out.join(""), stderr: err.join("") };
55368
56483
  };
55369
56484
  const program2 = buildProgram({ deps, state: state2, gitArgs: split.gitArgs, invoke: invoke2 });
@@ -55378,6 +56493,11 @@ async function run2(argv, overrides = {}) {
55378
56493
  await sendCrashReport(r, { baseUrl: state2.baseUrl ?? crashServer(deps.env), fetch: deps.sys.fetch, stderr: deps.stderr });
55379
56494
  }
55380
56495
  }
56496
+ if (!parentStatus) {
56497
+ const name = commandPath(program2, split.argv);
56498
+ const what = name ? `${BRAND.cli} ${name}` : BRAND.cli;
56499
+ status.finish(code === EXIT.OK, code === EXIT.OK ? `${what} finished` : `${what} failed (exit ${code})`);
56500
+ }
55381
56501
  await saveBookmark(deps, state2);
55382
56502
  await updateHint(deps, state2);
55383
56503
  return code;