brnch-cli 0.18.3 → 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 +459 -120
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # brnch 0.18.3
1
+ # brnch 0.19.0
2
2
 
3
3
  Command-line client for [Brnch](https://brnch.io) — Git hosting for humans and agents. Single file, no runtime dependencies, Node 20 or newer.
4
4
 
package/brnch.mjs CHANGED
@@ -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;
@@ -30578,7 +30578,7 @@ var PLATFORM_TOKEN_PREFIXES = {
30578
30578
  var PLATFORM_TOKEN_RE = new RegExp(`^(${Object.keys(PLATFORM_TOKEN_PREFIXES).join("|")})_[0-9a-f]{64}$`);
30579
30579
 
30580
30580
  // packages/domain/src/capabilities.ts
30581
- 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"];
30582
30582
  var REVIEWER = [...VIEWER, "review:comment", "review:resolve", "review:approve"];
30583
30583
  var DEVELOPER = [
30584
30584
  ...VIEWER,
@@ -30591,9 +30591,11 @@ var DEVELOPER = [
30591
30591
  "review:comment",
30592
30592
  "evidence:report",
30593
30593
  "promotion:request",
30594
- "packages:write"
30594
+ "packages:write",
30595
+ "deploy:read"
30595
30596
  ];
30596
- 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"];
30597
30599
  var ADMIN = [
30598
30600
  ...MAINTAINER,
30599
30601
  "org:admin",
@@ -32159,12 +32161,16 @@ var CreateInvitationRequest = external_exports.object({ email: external_exports.
32159
32161
  var AcceptInvitationRequest = external_exports.object({ token: external_exports.string() });
32160
32162
  var CreateTeamRequest = external_exports.object({ slug: Slug, name: displayLine().min(1).max(100) });
32161
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"]);
32162
32165
  var CreateGrantRequest = external_exports.object({
32163
32166
  principal_type: external_exports.enum(["user", "team"]),
32164
32167
  principal_id: external_exports.string().min(1).max(100),
32165
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(),
32166
32171
  expires_at: external_exports.iso.datetime().nullable().optional()
32167
32172
  });
32173
+ var UpdateGrantRequest = external_exports.object({ capabilities: external_exports.array(GrantExtraCapabilitySchema).max(6) });
32168
32174
  var CreateRepoRequest = external_exports.object({
32169
32175
  name: RepoName,
32170
32176
  description: displayText().max(500).default(""),
@@ -32253,11 +32259,16 @@ var PolicyRulesSchema = external_exports.object({
32253
32259
  });
32254
32260
  var AdoptPolicyRequest = external_exports.object({ rules: PolicyRulesSchema, expected_version: external_exports.number().int().nonnegative(), note: external_exports.string().max(1e3).optional() });
32255
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);
32256
32265
  var CreateTaskRequest = external_exports.object({
32257
32266
  title: displayLine().min(1).max(300),
32258
32267
  description: external_exports.string().max(65536).default(""),
32259
32268
  acceptance_criteria: external_exports.array(external_exports.string().max(1e3)).max(100).default([]),
32260
- 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([])
32261
32272
  });
32262
32273
  var UpdateTaskRequest = external_exports.object({
32263
32274
  title: displayLine().min(1).max(300).optional(),
@@ -32266,8 +32277,13 @@ var UpdateTaskRequest = external_exports.object({
32266
32277
  labels: external_exports.array(external_exports.string().max(50)).max(20).optional(),
32267
32278
  state: external_exports.enum(["open", "in_progress", "blocked", "done", "cancelled"]).optional(),
32268
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(),
32269
32282
  version: external_exports.number().int()
32270
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);
32271
32287
  var ClaimTaskRequest = external_exports.object({ session_id: external_exports.string().optional() });
32272
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() });
32273
32289
  var CreateCommentRequest = external_exports.object({ body: external_exports.string().min(1).max(65536) });
@@ -33888,18 +33904,26 @@ var UpsertWorkerEnvRequest = external_exports.object({
33888
33904
  });
33889
33905
 
33890
33906
  // packages/protocol/src/db-migrations.ts
33891
- var DB_MIGRATION_FORMATS = ["atlas", "plain", "golang_migrate", "drizzle", "prisma"];
33892
- var DB_ENGINE_KINDS = ["postgres", "vitess"];
33893
33907
  var DB_EXEC_ORDERS = ["linear", "linear_skip", "non_linear"];
33894
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
+ });
33895
33918
  var Ident2 = external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_$]{0,62}$/, "letters, digits and _ only (at most 63)");
33896
33919
  var PsName = external_exports.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/, "a PlanetScale name: letters, digits, '.', '_' and '-'");
33897
33920
  var SourceFields = {
33898
33921
  name: external_exports.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9_-]*$/, "lowercase letters, digits, - and _"),
33899
33922
  /** Directory relative to the repository root ("" = the root). */
33900
33923
  path: external_exports.string().max(300),
33901
- format: external_exports.enum(DB_MIGRATION_FORMATS),
33902
- engine: external_exports.enum(DB_ENGINE_KINDS),
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"]),
33903
33927
  /** Branch whose merges create runs; the repository's default branch when omitted. */
33904
33928
  branch: external_exports.string().min(1).max(200).optional(),
33905
33929
  /** PlanetScale connection (purpose `db_migrations`) of the organization or the repository. */
@@ -33921,12 +33945,19 @@ var SourceFields = {
33921
33945
  credential_mode: external_exports.enum(DB_CREDENTIAL_MODES).optional(),
33922
33946
  /** Postgres: the role new objects belong to (`SET ROLE`); `postgres` by default for managed roles, none for a DSN. */
33923
33947
  run_as_role: external_exports.string().regex(/^[a-z_][a-z0-9_]{0,62}$/).nullable().optional(),
33924
- /** `approval`: merges create runs that wait for approval. `manual_only`: runs start only by hand. */
33925
- on_merge: external_exports.enum(["approval", "manual_only"]).default("approval"),
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(),
33926
33953
  /** Validate the directory on changes (evidence `migrations/<name>`). */
33927
- change_checks: external_exports.boolean().default(true)
33954
+ change_checks: external_exports.boolean().default(true),
33955
+ lint: DbLintPolicySchema.optional(),
33956
+ drift_mode: external_exports.enum(DB_DRIFT_MODES).optional()
33928
33957
  };
33929
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"),
33930
33961
  ...SourceFields,
33931
33962
  /** Postgres `credential_mode: dsn` only: `postgresql://user:password@host:5432/db?sslmode=verify-full`. Write-only. */
33932
33963
  dsn: external_exports.string().min(10).max(2e3).optional()
@@ -33948,7 +33979,11 @@ var UpdateDbMigrationSourceRequest = external_exports.object({
33948
33979
  credential_mode: external_exports.enum(DB_CREDENTIAL_MODES),
33949
33980
  run_as_role: external_exports.string().regex(/^[a-z_][a-z0-9_]{0,62}$/).nullable(),
33950
33981
  on_merge: external_exports.enum(["approval", "manual_only"]),
33982
+ apply_on: external_exports.enum(DB_APPLY_ON),
33983
+ require_approval: external_exports.boolean(),
33951
33984
  change_checks: external_exports.boolean(),
33985
+ lint: DbLintPolicySchema,
33986
+ drift_mode: external_exports.enum(DB_DRIFT_MODES),
33952
33987
  state: external_exports.enum(["active", "disabled"]),
33953
33988
  dsn: external_exports.string().min(10).max(2e3).nullable()
33954
33989
  }).partial().extend({ version: external_exports.number().int().min(1) });
@@ -33956,7 +33991,22 @@ var StartDbMigrationRunRequest = external_exports.object({
33956
33991
  /** Branch, tag or commit to apply; the source's branch when omitted. */
33957
33992
  ref: external_exports.string().min(1).max(200).optional()
33958
33993
  });
33959
- var DecideDbMigrationRunRequest = external_exports.object({ reason: external_exports.string().max(500).optional() });
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() });
33960
34010
 
33961
34011
  // packages/protocol/src/buildkite.ts
33962
34012
  var BUILDKITE_TRUST_MODES = ["contributor_controlled", "pipeline_settings", "target_ref_upload"];
@@ -36299,6 +36349,8 @@ function reposGroup(t) {
36299
36349
  grants: {
36300
36350
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/grants`, query: { ...q }, options: o }),
36301
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 }),
36302
36354
  delete: (repo, grant, o) => t.void({ method: "DELETE", path: path2`/v1/repos/${repo}/grants/${grant}`, options: o })
36303
36355
  }
36304
36356
  };
@@ -36501,9 +36553,13 @@ function delegationsGroup(t) {
36501
36553
  function tasksGroup(t) {
36502
36554
  return {
36503
36555
  create: (repo, body, o) => t.json({ method: "POST", path: path2`/v1/repos/${repo}/tasks`, json: validate2(CreateTaskRequest, body, "CreateTaskRequest"), options: o }),
36504
- /** `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
+ */
36505
36560
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/tasks`, query: { ...q }, options: o }),
36506
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 }),
36507
36563
  /** `version` is required: a stale version fails with VERSION_CONFLICT instead of overwriting someone else's edit. */
36508
36564
  update: (task, body, o) => t.json({ method: "PATCH", path: path2`/v1/tasks/${task}`, json: validate2(UpdateTaskRequest, body, "UpdateTaskRequest"), options: o }),
36509
36565
  /** Advisory claim; CONFLICT when someone else holds it. */
@@ -36513,28 +36569,50 @@ function tasksGroup(t) {
36513
36569
  create: (task, body, o) => t.json({ method: "POST", path: path2`/v1/tasks/${task}/comments`, json: validate2(CreateCommentRequest, body, "CreateCommentRequest"), options: o }),
36514
36570
  list: (task, q = {}, o) => t.json({ method: "GET", path: path2`/v1/tasks/${task}/comments`, query: { ...q }, options: o })
36515
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
+ },
36516
36579
  addLink: (task, body, o) => t.json({ method: "POST", path: path2`/v1/tasks/${task}/links`, json: validate2(AddLinkRequest, body, "AddLinkRequest"), options: o })
36517
36580
  };
36518
36581
  }
36519
36582
 
36520
36583
  // packages/sdk/src/groups/db-migrations.ts
36521
36584
  function dbMigrationsGroup(t) {
36522
- const decide = (decision) => (id2, reason, o) => t.json({ method: "POST", path: path2`/v1/db-migration-runs/${id2}/${decision}`, json: validate2(DecideDbMigrationRunRequest, reason ? { reason } : {}, "DecideDbMigrationRunRequest"), options: o });
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
+ };
36523
36589
  return {
36524
36590
  sources: {
36525
36591
  list: (repo, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/db-migrations/sources`, options: o }),
36526
36592
  create: (repo, body, o) => t.json({ method: "POST", path: path2`/v1/repos/${repo}/db-migrations/sources`, json: validate2(CreateDbMigrationSourceRequest, body, "CreateDbMigrationSourceRequest"), options: o }),
36527
36593
  get: (id2, o) => t.json({ method: "GET", path: path2`/v1/db-migration-sources/${id2}`, options: o }),
36528
36594
  update: (id2, body, o) => t.json({ method: "PATCH", path: path2`/v1/db-migration-sources/${id2}`, json: validate2(UpdateDbMigrationSourceRequest, body, "UpdateDbMigrationSourceRequest"), options: o }),
36529
- delete: (id2, o) => t.void({ method: "DELETE", path: path2`/v1/db-migration-sources/${id2}`, 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 })
36530
36601
  },
36531
36602
  /** Migration directories found in the repository tree; `configured` marks those a source already covers. */
36532
36603
  suggestions: (repo, ref, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/db-migrations/suggestions`, query: ref ? { ref } : {}, options: o }),
36533
36604
  /** What applying the source at `ref` would do. `live` reads the database now instead of the last snapshot. */
36534
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 }),
36535
36610
  /** Reads the applied versions from the database again and returns the plan at the source's branch. */
36536
36611
  refresh: (sourceId2, o) => t.json({ method: "POST", path: path2`/v1/db-migration-sources/${sourceId2}/refresh`, options: o }),
36537
- /** A manual run: the caller's request is the approval. `run` is null when nothing is pending. */
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
+ */
36538
36616
  startRun: (sourceId2, body = {}, o) => t.json({ method: "POST", path: path2`/v1/db-migration-sources/${sourceId2}/runs`, json: validate2(StartDbMigrationRunRequest, body, "StartDbMigrationRunRequest"), options: o }),
36539
36617
  runs: {
36540
36618
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/db-migrations/runs`, query: { ...q }, options: o }),
@@ -36544,7 +36622,7 @@ function dbMigrationsGroup(t) {
36544
36622
  reject: decide("reject"),
36545
36623
  retry: decide("retry"),
36546
36624
  cancel: decide("cancel"),
36547
- /** The plan of every source a change touches, with the files the change adds or edits marked. */
36625
+ /** The plan of every source a change touches: the files the change adds (with statements), lint, schema effect. */
36548
36626
  changePlans: (changeId, o) => t.json({ method: "GET", path: path2`/v1/changes/${changeId}/db-migrations`, options: o })
36549
36627
  };
36550
36628
  }
@@ -36566,12 +36644,22 @@ function deployGroup(t) {
36566
36644
  remove: (id2, o) => t.json({ method: "DELETE", path: path2`/v1/deploy-targets/${id2}`, options: o }),
36567
36645
  /** Deploy the branch head again (or a commit this target deployed before). */
36568
36646
  deploy: (id2, body = {}, o) => t.json({ method: "POST", path: path2`/v1/deploy-targets/${id2}/deploy`, json: validate2(RedeployRequest, body, "RedeployRequest"), options: o }),
36569
- 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 })
36570
36654
  },
36571
36655
  deployments: {
36572
36656
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/deployments`, query: { ...q }, options: o }),
36573
36657
  get: (id2, o) => t.json({ method: "GET", path: path2`/v1/deployments/${id2}`, options: o }),
36574
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 }),
36575
36663
  ofChange: (change, o) => t.json({ method: "GET", path: path2`/v1/changes/${change}/deployments`, options: o })
36576
36664
  }
36577
36665
  };
@@ -37050,7 +37138,7 @@ function invalidAgentToolFlag(flag) {
37050
37138
  }
37051
37139
 
37052
37140
  // packages/cli/package.json
37053
- var version2 = "0.18.3";
37141
+ var version2 = "0.19.0";
37054
37142
 
37055
37143
  // packages/cli/src/brand.ts
37056
37144
  var BRAND = {
@@ -38787,18 +38875,13 @@ async function gitConfigPath(deps) {
38787
38875
  }
38788
38876
 
38789
38877
  // packages/cli/src/work-ref.ts
38790
- var MAX_PAGES = 20;
38791
- var PAGE = 100;
38792
38878
  async function issueByNumber(client, repo, n) {
38793
- let cursor2;
38794
- for (let i = 0; i < MAX_PAGES; i++) {
38795
- const page = await client.tasks.list(repo, { limit: PAGE, ...cursor2 ? { cursor: cursor2 } : {} });
38796
- const hit = page.items.find((t) => t.number === n);
38797
- if (hit) return hit.id;
38798
- if (!page.next_cursor) return null;
38799
- 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;
38800
38884
  }
38801
- return null;
38802
38885
  }
38803
38886
  async function changeByNumber(client, repo, n) {
38804
38887
  try {
@@ -38824,6 +38907,21 @@ async function resolveChangeId(spec, client, repo) {
38824
38907
  if (!id2) throw notFound(`change #${n}`, repo.spec());
38825
38908
  return id2;
38826
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
+ }
38827
38925
  async function resolveWorkRef(spec, client, repoId) {
38828
38926
  const m = /^(issue|change):(.+)$/.exec(spec.trim());
38829
38927
  const kind = m?.[1] ?? null;
@@ -39449,6 +39547,9 @@ async function createContext(deps, globals, state2) {
39449
39547
  function changeId(spec, pref = "agent") {
39450
39548
  return resolveChangeId(spec, client(pref), { id: () => repoId(void 0, pref), spec: () => repoSpec() });
39451
39549
  }
39550
+ function taskId(spec, pref = "agent") {
39551
+ return resolveTaskId(spec, client(pref), { id: () => repoId(void 0, pref), spec: () => repoSpec() });
39552
+ }
39452
39553
  function org() {
39453
39554
  const o = targetOrg ?? storedCred?.account?.default_org;
39454
39555
  if (!o) {
@@ -39519,6 +39620,7 @@ async function createContext(deps, globals, state2) {
39519
39620
  repoSpec,
39520
39621
  repoId,
39521
39622
  changeId,
39623
+ taskId,
39522
39624
  org,
39523
39625
  sessionId,
39524
39626
  info,
@@ -39638,6 +39740,15 @@ function cell(v) {
39638
39740
  if (typeof v === "number" || typeof v === "boolean") return String(v);
39639
39741
  return JSON.stringify(v);
39640
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
+ }
39641
39752
  var tildePath = (path3, home2) => home2 && path3.startsWith(`${home2}/`) ? `~${path3.slice(home2.length)}` : path3;
39642
39753
  function kv(obj3, keys) {
39643
39754
  const o = obj3 ?? {};
@@ -39737,7 +39848,7 @@ ${snippet}
39737
39848
 
39738
39849
  // packages/cli/src/commands/session.ts
39739
39850
  var renderSession = (s) => kv(
39740
- { ...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) },
39741
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"]
39742
39853
  );
39743
39854
  var renderStarted = (s) => `${renderSession(s.session)}
@@ -41242,7 +41353,7 @@ function sessionLines(s, now, you) {
41242
41353
  ...s.paths.length ? [` touching: ${pathList(s.paths)}`] : []
41243
41354
  ];
41244
41355
  }
41245
- 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}` : ""}`;
41246
41357
  function renderStatus(st, now = Date.now()) {
41247
41358
  const others = st.sessions.filter((s) => s.id !== st.you);
41248
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."];
@@ -41797,9 +41908,9 @@ async function runHook(deps, state2, tool, eventArg) {
41797
41908
  let meta3 = await readMeta(deps.env, id2);
41798
41909
  if (!meta3) {
41799
41910
  const baseUrl = await baseUrlOf(deps);
41800
- const resolved = await resolveRepo(deps, info.cwd ?? deps.cwd, baseUrl);
41801
- if (!resolved) return;
41802
- 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 };
41803
41914
  }
41804
41915
  if (info.model && !meta3.model) meta3.model = redactText(info.model).text.slice(0, 200);
41805
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);
@@ -42003,22 +42114,22 @@ function registerCapture(program2, rt) {
42003
42114
  act(rt, async (ctx, o, words) => {
42004
42115
  const summary = words.join(" ").trim();
42005
42116
  if (!summary) throw usage("decision text is required");
42006
- const resolved = await resolveRepo(ctx.deps, ctx.deps.cwd, ctx.baseUrl);
42007
- 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}`);
42008
42119
  const now = Date.now();
42009
- 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];
42010
42121
  let id2;
42011
42122
  let meta3;
42012
42123
  if (live) {
42013
42124
  id2 = live.id;
42014
42125
  meta3 = live.meta;
42015
42126
  } else {
42016
- 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)}`;
42017
42128
  id2 = spoolId(key);
42018
- 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 };
42019
42130
  }
42020
42131
  const paths = (o.file ?? []).map((f) => {
42021
- 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)));
42022
42133
  if (!rel || rel.startsWith("..") || isAbsolute6(rel)) throw usage(`${f} is outside the repository`);
42023
42134
  return rel.split(sep2).join("/");
42024
42135
  });
@@ -42062,22 +42173,22 @@ ${report3.errors.join("\n")}` : ""}`);
42062
42173
  rt.state.noUpdateHint = true;
42063
42174
  const [cmd, ...args] = argv;
42064
42175
  if (!cmd) throw usage(`usage: ${BRAND.cli} capture wrap -- <command> [args\u2026]`);
42065
- const resolved = await resolveRepo(ctx.deps, ctx.deps.cwd, ctx.baseUrl);
42176
+ const resolved2 = await resolveRepo(ctx.deps, ctx.deps.cwd, ctx.baseUrl);
42066
42177
  const cfg = await readCaptureConfig(ctx.deps.env);
42067
- const before = resolved && cfg.enabled ? await snapshotWorktree(ctx.deps, resolved.root) : null;
42068
- 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`);
42069
42180
  const code = await runInherit(cmd, args, ctx.deps.cwd, ctx.deps.env);
42070
42181
  ctx.setExit(code);
42071
- if (!resolved || !before) return;
42072
- const after = await snapshotWorktree(ctx.deps, resolved.root);
42182
+ if (!resolved2 || !before) return;
42183
+ const after = await snapshotWorktree(ctx.deps, resolved2.root);
42073
42184
  if (!after) return;
42074
- 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 });
42075
42186
  const label2 = (o.tool ?? cmd.split(/[\\/]/).pop() ?? "command").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40);
42076
42187
  const now = Date.now();
42077
42188
  const key = `wrapped:${label2}:${randomBytes4(8).toString("hex")}`;
42078
42189
  const id2 = spoolId(key);
42079
- 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 };
42080
- 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);
42081
42192
  await writeMeta(ctx.deps.env, id2, meta3);
42082
42193
  await appendEvents(ctx.deps.env, id2, [{ type: "capture.started", payload: { source: label2 } }, ...edits, { type: "capture.ended", payload: { reason: `exit ${code}` } }], now);
42083
42194
  startFlush(ctx.deps);
@@ -45918,12 +46029,35 @@ function seedJson(p) {
45918
46029
  }
45919
46030
 
45920
46031
  // packages/cli/src/commands/task.ts
45921
- 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 ? `
45922
46047
 
45923
46048
  ${t.description}` : "") + (t.acceptance_criteria.length ? `
45924
46049
 
45925
46050
  Acceptance criteria:
45926
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
+ }
45927
46061
  async function bodyFrom(ctx, o, required2 = true) {
45928
46062
  if (o.body !== void 0 && o.file !== void 0) throw usage("pass either --body or --file, not both");
45929
46063
  if (o.file !== void 0) return readText(fileSpec(o.file), ctx.input);
@@ -45933,23 +46067,24 @@ async function bodyFrom(ctx, o, required2 = true) {
45933
46067
  }
45934
46068
  function registerTask(program2, rt) {
45935
46069
  const task = program2.command("task").description("Tasks (issues) in --repo");
45936
- 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(
45937
46071
  act(rt, async (ctx, o) => {
45938
46072
  const description = await bodyFrom(ctx, o, false);
45939
- 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) });
45940
46074
  ctx.result(t, renderTask);
45941
46075
  })
45942
46076
  );
45943
- 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(
45944
46078
  act(rt, async (ctx, o) => {
45945
- const page = await ctx.client().tasks.list(await ctx.repoId(), { state: o.state, limit: o.limit, cursor: o.cursor });
45946
- 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"]));
45947
46081
  })
45948
46082
  );
45949
- task.command("view <task>").description("Show a task").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client().tasks.get(id2), renderTask)));
45950
- 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(
45951
- 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]) => {
45952
46086
  const c8 = ctx.client();
46087
+ const id2 = await ctx.taskId(spec);
45953
46088
  const version4 = o.expectedVersion ?? (await c8.tasks.get(id2)).version;
45954
46089
  const t = await c8.tasks.update(id2, {
45955
46090
  title: o.title,
@@ -45957,17 +46092,35 @@ function registerTask(program2, rt) {
45957
46092
  state: o.state,
45958
46093
  owner_id: o.owner === void 0 ? void 0 : o.owner === "none" ? null : o.owner,
45959
46094
  labels: o.label,
46095
+ blocked_by: blockersFrom(o.blockedBy, true),
45960
46096
  version: version4
45961
46097
  });
45962
46098
  ctx.result(t, renderTask);
45963
46099
  })
45964
46100
  );
45965
- 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)));
45966
- 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)));
45967
- 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(
45968
- 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);
45969
46122
  const comment = await ctx.client().tasks.comments.create(id2, { body: await bodyFrom(ctx, o) });
45970
- ctx.result(comment, `Comment ${comment.id} added to ${id2}`);
46123
+ ctx.result(comment, `Comment ${comment.id} added to ${spec}`);
45971
46124
  })
45972
46125
  );
45973
46126
  }
@@ -46249,7 +46402,7 @@ var how = (strategy) => strategy ? `with ${strategy}` : "with the policy's defau
46249
46402
  function renderAutoMerge(a, change, p, now) {
46250
46403
  const ref = change ? String(change.number) : a.change_id;
46251
46404
  const name = change ? `${p.bold(`#${change.number}`)} ${change.title}` : p.bold(a.change_id);
46252
- 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;
46253
46406
  const out = [];
46254
46407
  switch (a.state) {
46255
46408
  case "enabled":
@@ -46269,7 +46422,7 @@ function renderAutoMerge(a, change, p, now) {
46269
46422
  out.push(p.dim(` Turn it on: ${BRAND.cli} change auto-merge ${ref}`));
46270
46423
  }
46271
46424
  if (a.history.length) {
46272
- 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 }));
46273
46426
  const w = { when: Math.max(...rows2.map((r) => r.when.length)), action: Math.max(...rows2.map((r) => r.action.length)) };
46274
46427
  out.push("", p.bold("History"));
46275
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}`) : ""}`);
@@ -46335,11 +46488,11 @@ function stepOf(state2, previous) {
46335
46488
 
46336
46489
  // packages/cli/src/commands/change.ts
46337
46490
  var renderChange = (c8) => kv(
46338
- { ...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}` },
46339
46492
  ["number", "id", "title", "state", "target_ref", "source", "task_id", "author", "revision", "current_revision.head_oid", "revision_count", "merged", "version"]
46340
46493
  );
46341
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}` : ""}
46342
- ` + 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");
46343
46496
  function registerChange(program2, rt) {
46344
46497
  const change = program2.command("change").description("Changes (proposed updates to a protected ref)");
46345
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(
@@ -46359,7 +46512,7 @@ function registerChange(program2, rt) {
46359
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(
46360
46513
  act(rt, async (ctx, o) => {
46361
46514
  const page = await ctx.client().changes.list(await ctx.repoId(), { state: o.state, limit: o.limit, cursor: o.cursor });
46362
- 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"]));
46363
46516
  })
46364
46517
  );
46365
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)));
@@ -46448,7 +46601,7 @@ function registerReview(program2, rt) {
46448
46601
  const id2 = await ctx.changeId(spec);
46449
46602
  const revision = o.revision ?? (await c8.changes.get(id2)).current_revision.id;
46450
46603
  const r = await c8.reviews.create(id2, { revision_id: revision, decision, body: await bodyFrom(ctx, o, bodyRequired) });
46451
- 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"]));
46452
46605
  })
46453
46606
  );
46454
46607
  decide("approve", "approved", "Approve a revision (approvals are bound to that revision)", false);
@@ -46569,7 +46722,7 @@ Wait for the findings: code-review wait ${spec}`);
46569
46722
 
46570
46723
  // packages/cli/src/commands/evidence.ts
46571
46724
  import { basename as basename4, extname, isAbsolute as isAbsolute8, resolve as resolve8 } from "node:path";
46572
- 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"]);
46573
46726
  var CONTENT_TYPES = { ".txt": "text/plain", ".log": "text/plain", ".json": "application/json", ".xml": "application/xml", ".html": "text/plain" };
46574
46727
  function registerEvidence(program2, rt) {
46575
46728
  const ev = program2.command("evidence").description("Verification evidence");
@@ -46746,7 +46899,7 @@ var renderCoverage = (c8) => [
46746
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"}`,
46747
46900
  table(c8.paths.map((p) => ({ ...p, owners: p.owners.join(" ") || "-", approved_by: p.approved_by.join(" ") || "-" })), ["path", "owners", "satisfied", "approved_by"])
46748
46901
  ].join("\n");
46749
- 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"]);
46750
46903
  function registerGovernance(program2, rt) {
46751
46904
  const change = program2.commands.find((c8) => c8.name() === "change") ?? program2.command("change");
46752
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"]))));
@@ -46836,7 +46989,7 @@ new revision #${r.revision.number} ${r.revision.id}`);
46836
46989
  }
46837
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 } : {} });
46838
46991
  ctx.result(b, (v) => {
46839
- const who3 = v.principal ? v.principal.display_name ?? `${v.principal.type}:${v.principal.id}` : "nobody";
46992
+ const who3 = v.principal ? actorLabel(v.principal) : "nobody";
46840
46993
  return `${kv({ ...v, for: who3, ref: v.ref ?? "any" }, ["id", "rule", "path", "blob_oid", "for", "ref", "expires_at"])}
46841
46994
  Only ${who3} can use it, for one push, before it expires.`;
46842
46995
  });
@@ -46847,7 +47000,7 @@ Only ${who3} can use it, for one push, before it expires.`;
46847
47000
  rt,
46848
47001
  async (ctx, o) => ctx.result(
46849
47002
  await ctx.client().pushProtection.bypasses.list(await ctx.repoId(), { limit: o.limit, cursor: o.cursor }),
46850
- (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"])
46851
47004
  )
46852
47005
  )
46853
47006
  );
@@ -46888,7 +47041,7 @@ var renderAlerts2 = (p) => listWithCursor(
46888
47041
  ["id", "state", "resolution", "confidence", "rule", "repo", "where", "masked", "source", "created_at"]
46889
47042
  );
46890
47043
  var renderAlert = (a) => [
46891
- 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 }, [
46892
47045
  "id",
46893
47046
  "state",
46894
47047
  "resolution",
@@ -47229,8 +47382,8 @@ var renderDevices = (v) => v.items.length ? table(
47229
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" : "" })),
47230
47383
  ["id", "name", "environment", "app_version", "last_seen", "current"]
47231
47384
  ) : "No push devices. The iOS app registers itself when you allow notifications.";
47232
- var PAGE2 = 100;
47233
- var MAX_PAGES2 = 10;
47385
+ var PAGE = 100;
47386
+ var MAX_PAGES = 10;
47234
47387
  async function findItems(client, specs) {
47235
47388
  const wanted = specs.map((s) => s.trim().toLowerCase());
47236
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`);
@@ -47238,8 +47391,8 @@ async function findItems(client, specs) {
47238
47391
  const seen = /* @__PURE__ */ new Set();
47239
47392
  for (const view of ["all", "snoozed", "done"]) {
47240
47393
  let cursor2;
47241
- for (let i = 0; i < MAX_PAGES2; i++) {
47242
- 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 } : {} });
47243
47396
  for (const item of page.items) {
47244
47397
  if (seen.has(item.id)) continue;
47245
47398
  seen.add(item.id);
@@ -47694,13 +47847,13 @@ function renderEntry(e, verb) {
47694
47847
  if (e.flags.length) lines2.push(`flagged: ${e.flags.join(", ")}${e.flags.includes("instructions") ? "; never given to the assistant on its own" : ""}`);
47695
47848
  return lines2.join("\n");
47696
47849
  }
47697
- var PAGE3 = 100;
47698
- var MAX_PAGES3 = 10;
47850
+ var PAGE2 = 100;
47851
+ var MAX_PAGES2 = 10;
47699
47852
  async function reviewable(ctx, org, q) {
47700
47853
  const out = [];
47701
47854
  let cursor2;
47702
- for (let i = 0; i < MAX_PAGES3; i++) {
47703
- 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 } : {} });
47704
47857
  out.push(...page.items.filter((e) => e.can_review));
47705
47858
  if (!page.next_cursor) break;
47706
47859
  cursor2 = page.next_cursor;
@@ -48008,6 +48161,38 @@ function registerIntegrations(program2, rt) {
48008
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)));
48009
48162
  }
48010
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
+
48011
48196
  // packages/cli/src/commands/db.ts
48012
48197
  async function sourceId(ctx, ref, repo) {
48013
48198
  if (ref.startsWith("dms_")) return ref;
@@ -48018,18 +48203,58 @@ async function sourceId(ctx, ref, repo) {
48018
48203
  }
48019
48204
  var plural3 = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
48020
48205
  var short = (oid) => oid.slice(0, 12);
48021
- var database = (s) => `${s.ps_organization}/${s.ps_database}@${s.ps_branch}`;
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
+ }
48022
48244
  function issueLines(issues) {
48023
48245
  if (!issues.length) return [];
48024
48246
  return ["issues:", ...issues.map((i) => ` ${i.level.padEnd(7)} ${i.code} ${i.file ? `${i.file}: ` : ""}${i.message}`)];
48025
48247
  }
48026
48248
  var renderSources = (v) => table(
48027
- v.items.map((s) => ({ ...s, database: database(s), applied: s.snapshot ? `${s.snapshot.applied.length} (read ${s.snapshot.read_at})` : "not read yet" })),
48028
- ["name", "id", "path", "format", "engine", "database", "branch", "on_merge", "state", "applied"]
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"]
48029
48251
  );
48030
- var renderSource = (s) => `${kv({ ...s, database: database(s), path: s.path || "(repository root)" }, ["id", "name", "path", "format", "engine", "database", "branch", "revisions_location", "baseline_version", "exec_order", "credential_mode", "dsn_set", "run_as_role", "on_merge", "change_checks", "state", "version"])}
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
+ )}
48031
48256
 
48032
- ` + (s.on_merge === "approval" ? `Merges to ${s.branch} that add migrations create a run waiting for approval: ${BRAND.cli} db runs` : `Runs start only by hand: ${BRAND.cli} db apply ${s.name}`);
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}`);
48033
48258
  var renderSuggestions = (v) => v.items.length ? table(
48034
48259
  v.items.map((s) => ({ ...s, path: s.path || "(root)", configured: s.configured ? "yes" : "no" })),
48035
48260
  ["path", "format", "files", "revisions_schema", "baseline_version", "configured", "evidence"]
@@ -48048,16 +48273,17 @@ function renderPlan2(p) {
48048
48273
  lines2.push(` ${f.name} ${plural3(f.statements, "statement")}${notes.length ? ` (${notes.join(", ")})` : ""}`);
48049
48274
  }
48050
48275
  }
48051
- return [...lines2, ...issueLines(p.issues)].join("\n");
48276
+ return [...lines2, ...issueLines(p.issues), ...lintLines(p.lint), ...diffLines(p.schema_diff)].join("\n");
48052
48277
  }
48053
48278
  var renderPlans = (v) => v.items.length ? v.items.map(renderPlan2).join("\n\n") : "no migration sources";
48054
48279
  var NEXT = {
48055
- awaiting_approval: (id2) => `Waiting for a repository admin: ${BRAND.cli} db approve ${id2} (or reject)`,
48280
+ awaiting_approval: (id2) => `Waiting for approval: ${BRAND.cli} db approve ${id2} (or reject)`,
48056
48281
  failed: (id2) => `Fix the migration and merge it, then: ${BRAND.cli} db retry ${id2}`,
48057
- needs_attention: (id2) => `Look at the run in PlanetScale, then: ${BRAND.cli} db retry ${id2} or cancel`
48282
+ needs_attention: (id2) => `Look at the database (PlanetScale, or the D1 bookmark's restore command), then: ${BRAND.cli} db retry ${id2} or cancel`
48058
48283
  };
48059
48284
  function renderRun(r) {
48060
- const ps = r.planetscale;
48285
+ const ps = r.backend?.provider === "planetscale" ? r.backend : null;
48286
+ const d1 = r.backend?.provider === "cloudflare_d1" ? r.backend : null;
48061
48287
  const head = kv(
48062
48288
  {
48063
48289
  ...r,
@@ -48065,16 +48291,18 @@ function renderRun(r) {
48065
48291
  change: r.change_id ?? "-",
48066
48292
  requested_by: r.requested_by ? `${r.requested_by.type} ${r.requested_by.id}` : "-",
48067
48293
  deploy_request: ps?.deploy_request_url ?? (ps?.deploy_request ? `#${ps.deploy_request}` : null),
48068
- deployment_state: ps?.deployment_state ?? null
48294
+ deployment_state: ps?.deployment_state ?? null,
48295
+ bookmark: d1?.bookmark ?? null,
48296
+ restore: d1?.restore?.wrangler ?? null
48069
48297
  },
48070
- ["id", "source_name", "status", "kind", "engine", "commit", "change", "baseline", "requested_by", "approved_by", "approved_at", "decision_reason", "deploy_request", "deployment_state", "error", "note", "created_at"]
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"]
48071
48299
  );
48072
48300
  const files = r.files.length ? table(
48073
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 })),
48074
48302
  ["name", "status", "statements", "execution_ms", "error"]
48075
48303
  ) : "no files";
48076
48304
  const next = NEXT[r.status]?.(r.id);
48077
- return [head, "", files, ...r.issues.length ? ["", ...issueLines(r.issues)] : [], ...next ? ["", next] : []].join("\n");
48305
+ return [head, "", files, ...r.issues.length ? ["", ...issueLines(r.issues)] : [], ...next ? ["", next] : []].join("\n") + approvalLines(r.approval);
48078
48306
  }
48079
48307
  var renderRuns = (v) => listWithCursor(
48080
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` })) },
@@ -48095,9 +48323,9 @@ function sessionSettings(pairs) {
48095
48323
  return out;
48096
48324
  }
48097
48325
  function registerDb(program2, rt) {
48098
- const db = program2.command("db").description("Database migrations on PlanetScale for --repo: sources, plans, and runs applied after merge and approval");
48099
- const sources = db.command("sources").description("Migration sources of --repo: a directory bound to a PlanetScale database branch").action(act(rt, async (ctx) => ctx.result(await ctx.client("user").dbMigrations.sources.list(await ctx.repoId(void 0, "user")), renderSources)));
48100
- sources.command("add").description("Add a migration source (repository admins). The source is checked against PlanetScale; `db suggest` shows the directories found").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>", DB_MIGRATION_FORMATS.join(" | "), oneOf("--format", DB_MIGRATION_FORMATS)).requiredOption("--engine <engine>", DB_ENGINE_KINDS.join(" | "), oneOf("--engine", DB_ENGINE_KINDS)).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>", "approval (merges create runs waiting for approval) | manual_only", oneOf("--on-merge", ["approval", "manual_only"])).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(
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(
48101
48329
  act(rt, async (ctx, o) => {
48102
48330
  let dsn;
48103
48331
  if (o.dsnFile !== void 0) {
@@ -48124,6 +48352,10 @@ function registerDb(program2, rt) {
48124
48352
  ...o.baseline ? { baseline_version: o.baseline } : {},
48125
48353
  ...o.execOrder ? { exec_order: o.execOrder } : {},
48126
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 } : {},
48127
48359
  ...o.credentialMode ? { credential_mode: o.credentialMode } : {},
48128
48360
  ...o.runAsRole ? { run_as_role: o.runAsRole } : {},
48129
48361
  ...o.sessionSetting ? { session_settings: sessionSettings(o.sessionSetting) } : {},
@@ -48134,6 +48366,45 @@ function registerDb(program2, rt) {
48134
48366
  ctx.result(await ctx.client("user").dbMigrations.sources.create(await ctx.repoId(void 0, "user"), body), renderSource);
48135
48367
  })
48136
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)));
48137
48408
  sources.command("rm <source>").description("Delete a migration source (name or id; its runs stay listed). Requires --yes when non-interactive").action(
48138
48409
  act(rt, async (ctx, _o, [ref]) => {
48139
48410
  const id2 = await sourceId(ctx, ref);
@@ -48143,7 +48414,7 @@ function registerDb(program2, rt) {
48143
48414
  })
48144
48415
  );
48145
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)));
48146
- 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 (repository admins)").action(
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(
48147
48418
  act(rt, async (ctx, o, [ref]) => {
48148
48419
  const c8 = ctx.client("user").dbMigrations;
48149
48420
  const q = { ref: o.ref, live: o.live };
@@ -48159,7 +48430,29 @@ function registerDb(program2, rt) {
48159
48430
  if (plans.items.some((p) => !p.ok)) ctx.setExit(EXIT.NOT_READY);
48160
48431
  })
48161
48432
  );
48162
- db.command("refresh <source>").description("Read the applied versions from the database now and show the plan at the source's branch (repository admins)").action(act(rt, async (ctx, _o, [ref]) => ctx.result(await ctx.client("user").dbMigrations.refresh(await sourceId(ctx, ref)), renderPlan2)));
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)));
48163
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(
48164
48457
  act(rt, async (ctx, o) => {
48165
48458
  const repo = { id: await ctx.repoId(void 0, "user") };
@@ -48168,7 +48461,7 @@ function registerDb(program2, rt) {
48168
48461
  })
48169
48462
  );
48170
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)));
48171
- db.command("apply <source>").description("Apply a source's pending migrations now (repository admins; your request is the approval). Requires --yes when non-interactive").option("--ref <ref>", "branch, tag or commit to apply (default: the source's branch)").action(
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(
48172
48465
  act(rt, async (ctx, o, [ref]) => {
48173
48466
  const id2 = await sourceId(ctx, ref);
48174
48467
  await ctx.confirm(`apply the pending migrations of ${ref}${o.ref ? ` at ${o.ref}` : ""} to its database`);
@@ -48179,13 +48472,15 @@ ${renderPlan2(v.plan)}`);
48179
48472
  })
48180
48473
  );
48181
48474
  const decisions = [
48182
- ["approve", "Approve a run waiting for approval: Brnch applies exactly the files it shows (repository admins)"],
48183
- ["reject", "Reject a run waiting for approval; nothing is applied (repository admins)"],
48184
- ["retry", "Retry a failed run from where it stopped (repository admins)"],
48185
- ["cancel", "Cancel a run that has not finished (repository admins)"]
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)"]
48186
48479
  ];
48187
48480
  for (const [decision, description] of decisions) {
48188
- db.command(`${decision} <run>`).description(description).option("--reason <text>", "why (recorded with the decision)").action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client("user").dbMigrations[decision](id2, o.reason), renderRun)));
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)));
48189
48484
  }
48190
48485
  }
48191
48486
 
@@ -48196,7 +48491,7 @@ var renderTargets = (v) => table(
48196
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)}` : "-" })),
48197
48492
  ["name", "id", "platform", "resource", "env", "branch", "runs_on", "previews", "state", "last_production"]
48198
48493
  );
48199
- var renderTarget = (t) => kv({ ...t, 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}` : "-" }, [
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}` : "-" }, [
48200
48495
  "id",
48201
48496
  "name",
48202
48497
  "platform",
@@ -48214,12 +48509,13 @@ var renderTarget = (t) => kv({ ...t, watch_paths: t.watch_paths?.length ? t.watc
48214
48509
  "watch_paths",
48215
48510
  "previews",
48216
48511
  "preview_check_name",
48512
+ "approvers",
48217
48513
  "last_production",
48218
48514
  "version"
48219
48515
  ]);
48220
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 ? `
48221
48517
 
48222
- ${table(d.steps, ["name", "phase", "status", "summary"])}` : "");
48518
+ ${table(d.steps, ["name", "phase", "status", "summary"])}` : "") + approvalLines(d.approval);
48223
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"]);
48224
48520
  async function pick3(ctx, what, flag, options, label2) {
48225
48521
  if (options.length === 1) return options[0];
@@ -48355,7 +48651,23 @@ function registerDeploy(program2, rt) {
48355
48651
  Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}`);
48356
48652
  })
48357
48653
  );
48358
- 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)));
48359
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)));
48360
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(
48361
48673
  act(rt, async (ctx, o, [ref]) => {
@@ -48414,6 +48726,8 @@ Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}
48414
48726
  );
48415
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)));
48416
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)));
48417
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(
48418
48732
  act(rt, async (ctx, o, [ref]) => {
48419
48733
  const id2 = await targetId(ctx, ref);
@@ -48426,7 +48740,6 @@ Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}
48426
48740
  // packages/cli/src/commands/delegation.ts
48427
48741
  var ITERATION_DONE = /* @__PURE__ */ new Set(["succeeded", "no_changes", "failed", "cancelled"]);
48428
48742
  var STATES = ["working", "waiting", "stopped"];
48429
- var who2 = (a) => a.display_name ?? `${a.type} ${a.id}`;
48430
48743
  var short2 = (oid) => oid ? oid.slice(0, 12) : null;
48431
48744
  function vendorArg(v) {
48432
48745
  if (!CODING_AGENT_VENDORS.includes(v)) throw usage(`unknown agent "${v}"; one of ${CODING_AGENT_VENDORS.join(", ")}`);
@@ -48446,7 +48759,7 @@ function renderIterations(d) {
48446
48759
  const rows2 = d.iterations.map((i) => ({
48447
48760
  ...i,
48448
48761
  trigger: i.trigger_ref ? `${i.trigger} (${i.trigger_ref})` : i.trigger,
48449
- by: who2(i.trigger_actor),
48762
+ by: actorLabel(i.trigger_actor),
48450
48763
  head: short2(i.head_oid)
48451
48764
  }));
48452
48765
  const claims = d.iterations.map(reported).filter((x) => !!x);
@@ -48468,7 +48781,7 @@ var renderDelegation = (d) => kv(
48468
48781
  ...d,
48469
48782
  agent: `${d.agent.name} (${d.agent.vendor})`,
48470
48783
  status: delegationStatusLabel(d),
48471
- sponsor: who2(d.sponsor),
48784
+ sponsor: actorLabel(d.sponsor),
48472
48785
  branch: `${d.source_ref} \u2192 ${d.target_ref}`,
48473
48786
  automatic: `on review ${d.auto_on_review ? "on" : "off"}, on check failure ${d.auto_on_check_failure ? "on" : "off"}`,
48474
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`
@@ -49150,7 +49463,18 @@ function registerRepo(program2, rt) {
49150
49463
  await new Promise((r) => setTimeout(r, 2e3));
49151
49464
  job2 = await c8.migrations.get(job2.id);
49152
49465
  }
49153
- 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
+ };
49154
49478
  ctx.result(job2, () => kv(view, Object.keys(view)));
49155
49479
  if (o.wait && job2.state !== "completed") throw new CliError(`migration ${job2.state}: ${job2.error ?? job2.report.missing.join("; ")}`, EXIT.NOT_READY);
49156
49480
  })
@@ -49183,24 +49507,39 @@ function registerRepo(program2, rt) {
49183
49507
  if (t.transport === "ssh") {
49184
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`);
49185
49509
  const url2 = sshCloneUrl(t.ssh.alias, r.org_slug, r.name);
49186
- 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 });
49187
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");
49188
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"))));
49189
49513
  }
49190
49514
  if (t.approval) ctx.info(`Cloning over HTTPS: ${t.reason}. Pass --ssh to use SSH anyway.`);
49191
49515
  const token = await ctx.freshToken("agent");
49192
- 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], {
49193
49519
  env: gitAuthEnv(ctx.deps.env, token, r.clone_url),
49194
49520
  cwd: ctx.deps.cwd,
49195
49521
  stdoutToStderr: true
49196
49522
  });
49197
49523
  if (code !== 0) throw new CliError(`git clone exited with status ${code}`, EXIT.UNEXPECTED, "GIT_FAILED");
49198
- const helper2 = await credentialHelperConfigured(ctx);
49199
49524
  ctx.info(authGuidance(BRAND.cli, helper2));
49200
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"))));
49201
49526
  })
49202
49527
  );
49203
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
+ }
49204
49543
  var hasRepo = (ctx) => {
49205
49544
  try {
49206
49545
  ctx.repoSpec();
@@ -49268,7 +49607,7 @@ function registerGit(program2, rt) {
49268
49607
  }
49269
49608
 
49270
49609
  // packages/cli/src/commands/release.ts
49271
- 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"]);
49272
49611
  function registerRelease(program2, rt) {
49273
49612
  const release = program2.command("release").description("Releases of --repo (a tag with notes)");
49274
49613
  release.command("list").description("Releases, newest first").action(
@@ -49711,7 +50050,7 @@ var renderRangeDiff = (r) => {
49711
50050
  if (r.truncated) lines2.push("(truncated)");
49712
50051
  return lines2.join("\n");
49713
50052
  };
49714
- 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})` : ""}`;
49715
50054
  async function waitForPromotion(ctx, p, timeout, interval) {
49716
50055
  const deadline = Date.now() + timeout * 1e3;
49717
50056
  let last = p.state;
@@ -49788,7 +50127,7 @@ next: --cursor ${v.next_cursor}` : "") : listWithCursor(v, ["id"]));
49788
50127
  }
49789
50128
 
49790
50129
  // packages/cli/src/commands/coordination.ts
49791
- 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})` : ""}`;
49792
50131
  var renderClaim = (r) => [
49793
50132
  `${r.already_held ? "Already yours" : "Claimed"}: ${r.claim.key}${r.claim.change ? ` (change #${r.claim.change.number})` : ""}`,
49794
50133
  ...r.skipped.length ? [`Taken by others: ${r.skipped.join(", ")}`] : [],
@@ -52091,7 +52430,7 @@ function safeLegs(m) {
52091
52430
  }
52092
52431
 
52093
52432
  // packages/cli/src/commands/actions.ts
52094
- var renderRun2 = (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)"}` }, [
52095
52434
  "id",
52096
52435
  "workflow_name",
52097
52436
  "run_number",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brnch-cli",
3
- "version": "0.18.3",
3
+ "version": "0.19.0",
4
4
  "description": "Brnch CLI: Git hosting for humans and agents, from the command line",
5
5
  "keywords": [
6
6
  "git",