brnch-cli 0.18.1 → 0.18.3

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 +943 -114
  3. package/package.json +3 -2
package/brnch.mjs CHANGED
@@ -6044,9 +6044,9 @@ var require_lexer = __commonJS({
6044
6044
  }
6045
6045
  }
6046
6046
  *parseQuotedScalar() {
6047
- const quote2 = this.charAt(0);
6048
- let end2 = this.buffer.indexOf(quote2, this.pos + 1);
6049
- if (quote2 === "'") {
6047
+ const quote3 = this.charAt(0);
6048
+ let end2 = this.buffer.indexOf(quote3, this.pos + 1);
6049
+ if (quote3 === "'") {
6050
6050
  while (end2 !== -1 && this.buffer[end2 + 1] === "'")
6051
6051
  end2 = this.buffer.indexOf("'", end2 + 2);
6052
6052
  } else {
@@ -9960,7 +9960,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
9960
9960
  const negativeNumberArg = (arg) => {
9961
9961
  if (!/^-(\d+|\d*\.\d+)(e[+-]?\d+)?$/.test(arg)) return false;
9962
9962
  return !this._getCommandAndAncestors().some(
9963
- (cmd) => cmd.options.map((opt) => opt.short).some((short2) => /^-\d$/.test(short2))
9963
+ (cmd) => cmd.options.map((opt) => opt.short).some((short3) => /^-\d$/.test(short3))
9964
9964
  );
9965
9965
  };
9966
9966
  let activeVariadicOption = null;
@@ -21407,8 +21407,8 @@ var error43 = () => {
21407
21407
  case "not_multiple_of":
21408
21408
  return `N\xFAmero inv\xE1lido: deve ser m\xFAltiplo de ${issue2.divisor}`;
21409
21409
  case "unrecognized_keys": {
21410
- const plural3 = issue2.keys.length > 1 ? "s" : "";
21411
- return `Chave${plural3} inv\xE1lida${plural3}: ${joinValues(issue2.keys, ", ")}`;
21410
+ const plural4 = issue2.keys.length > 1 ? "s" : "";
21411
+ return `Chave${plural4} inv\xE1lida${plural4}: ${joinValues(issue2.keys, ", ")}`;
21412
21412
  }
21413
21413
  case "invalid_key":
21414
21414
  return `Entrada inv\xE1lida n${translateOriginWithArticle(issue2.origin, "definite")}`;
@@ -21552,8 +21552,8 @@ var error44 = () => {
21552
21552
  case "not_multiple_of":
21553
21553
  return `N\xFAmero inv\xE1lido: deve ser m\xFAltiplo de ${issue2.divisor}`;
21554
21554
  case "unrecognized_keys": {
21555
- const plural3 = issue2.keys.length > 1 ? "s" : "";
21556
- return `Chave${plural3} inv\xE1lida${plural3}: ${joinValues(issue2.keys, ", ")}`;
21555
+ const plural4 = issue2.keys.length > 1 ? "s" : "";
21556
+ return `Chave${plural4} inv\xE1lida${plural4}: ${joinValues(issue2.keys, ", ")}`;
21557
21557
  }
21558
21558
  case "invalid_key":
21559
21559
  return `Entrada inv\xE1lida n${translateOriginWithArticle(issue2.origin, "definite")}`;
@@ -30572,7 +30572,8 @@ var PLATFORM_TOKEN_PREFIXES = {
30572
30572
  finv: "organization invitation token",
30573
30573
  fsfa: "pending second-factor sign-in",
30574
30574
  fbkc: "Buildkite checkout token",
30575
- fsvw: "staff read-only view"
30575
+ fsvw: "staff read-only view",
30576
+ fcrt: "container registry token"
30576
30577
  };
30577
30578
  var PLATFORM_TOKEN_RE = new RegExp(`^(${Object.keys(PLATFORM_TOKEN_PREFIXES).join("|")})_[0-9a-f]{64}$`);
30578
30579
 
@@ -31545,15 +31546,15 @@ function redactText(input3, opts = {}) {
31545
31546
  hit("auth-header");
31546
31547
  return `${prefix}${REDACTED}`;
31547
31548
  });
31548
- text2 = text2.replace(ASSIGNMENT, (w, prefix, quote2, value) => {
31549
+ text2 = text2.replace(ASSIGNMENT, (w, prefix, quote3, value) => {
31549
31550
  if (isReference(value) || value.startsWith(REDACTED)) return w;
31550
31551
  hit("assignment");
31551
- return `${prefix}${quote2}${REDACTED}${quote2}`;
31552
+ return `${prefix}${quote3}${REDACTED}${quote3}`;
31552
31553
  });
31553
- text2 = text2.replace(FLAG, (w, prefix, quote2, value) => {
31554
+ text2 = text2.replace(FLAG, (w, prefix, quote3, value) => {
31554
31555
  if (isReference(value) || value.startsWith(REDACTED)) return w;
31555
31556
  hit("flag");
31556
- return `${prefix}${quote2}${REDACTED}${quote2}`;
31557
+ return `${prefix}${quote3}${REDACTED}${quote3}`;
31557
31558
  });
31558
31559
  if (opts.entropy !== false) {
31559
31560
  text2 = text2.replace(TOKENISH, (w) => {
@@ -31632,6 +31633,7 @@ var looksLikeHostedLabel = (label2) => /^brnch-hosted(?:-|$)/.test(label2);
31632
31633
  // packages/domain/src/deploy.ts
31633
31634
  var DEPLOY_LIMITS = {
31634
31635
  maxStepsPerTarget: 12,
31636
+ maxWatchPaths: 50,
31635
31637
  maxTargetsPerRepository: 10,
31636
31638
  maxCommandLength: 2e3,
31637
31639
  maxSummaryLength: 500,
@@ -31645,6 +31647,21 @@ var DEPLOY_LIMITS = {
31645
31647
  var DEPLOY_RUNNER_LABELS = HOSTED_SIZES.map((s) => HOSTED_SIZE_SPECS[s].label);
31646
31648
  var DEFAULT_DEPLOY_RUNNER = HOSTED_SIZE_SPECS.basic.label;
31647
31649
 
31650
+ // packages/domain/src/db-migrations.ts
31651
+ var DB_MIGRATION_LIMITS = {
31652
+ sourcesPerRepo: 20,
31653
+ /** Migration files read from one directory. */
31654
+ files: 2e3,
31655
+ fileBytes: 1024 * 1024,
31656
+ dirBytes: 16 * 1024 * 1024,
31657
+ /** Per statement; queue work is bounded by the platform's 15-minute limit. */
31658
+ statementTimeoutMaxS: 600,
31659
+ lockTimeoutMaxS: 60,
31660
+ sessionSettings: 20,
31661
+ /** How long a worker holds a run before another may take it over. */
31662
+ leaseMs: 15 * 6e4
31663
+ };
31664
+
31648
31665
  // packages/domain/src/isolated-preview.ts
31649
31666
  function parseJsonc(text2) {
31650
31667
  let out = "";
@@ -31683,6 +31700,8 @@ var DELEGATION_MATRIX = Object.freeze({
31683
31700
  "cloudflare/resources": { userToOrg: false, userToRepo: false, orgToRepo: true },
31684
31701
  "cloudflare/d1_migrations": { userToOrg: false, userToRepo: false, orgToRepo: true },
31685
31702
  "cloudflare/secrets_store": { userToOrg: false, userToRepo: false, orgToRepo: true },
31703
+ // Database migrations (ADR-0084) apply one repository's migration directory to the PlanetScale database it is bound to.
31704
+ "planetscale/db_migrations": { userToOrg: false, userToRepo: false, orgToRepo: true },
31686
31705
  // A secret source is read by a repository's workflow runs.
31687
31706
  "infisical/secrets_source": { userToOrg: false, userToRepo: false, orgToRepo: true },
31688
31707
  // AI code review keys (ADR-0051): an organization's key reviews the changes of its repositories.
@@ -31734,7 +31753,7 @@ var PLANS = Object.freeze({
31734
31753
  monthly_cents: 800,
31735
31754
  annual_cents: 8e3,
31736
31755
  per_seat: false,
31737
- features: ["private_review_rules", "ai_code_review_services", "hosted_delegation", "cloudflare_previews", "cloudflare_d1_migrations", "assistant"],
31756
+ features: ["private_review_rules", "ai_code_review_services", "hosted_delegation", "cloudflare_previews", "cloudflare_d1_migrations", "database_migrations", "assistant"],
31738
31757
  minutes_base: 5e3,
31739
31758
  minutes_per_seat: 0,
31740
31759
  concurrent_hosted_jobs: 4,
@@ -31753,7 +31772,7 @@ var PLANS = Object.freeze({
31753
31772
  monthly_cents: 1e3,
31754
31773
  annual_cents: 9600,
31755
31774
  per_seat: true,
31756
- features: ["private_review_rules", "private_members_unlimited", "ai_code_review_services", "hosted_delegation", "cloudflare_previews", "cloudflare_d1_migrations", "premium_ai_models", "assistant", "assistant_proactive"],
31775
+ features: ["private_review_rules", "private_members_unlimited", "ai_code_review_services", "hosted_delegation", "cloudflare_previews", "cloudflare_d1_migrations", "database_migrations", "premium_ai_models", "assistant", "assistant_proactive"],
31757
31776
  minutes_base: 3e3,
31758
31777
  minutes_per_seat: 1e3,
31759
31778
  concurrent_hosted_jobs: 10,
@@ -31772,7 +31791,7 @@ var PLANS = Object.freeze({
31772
31791
  monthly_cents: null,
31773
31792
  annual_cents: null,
31774
31793
  per_seat: true,
31775
- features: ["private_review_rules", "private_members_unlimited", "ai_code_review_services", "hosted_delegation", "cloudflare_previews", "cloudflare_d1_migrations", "premium_ai_models", "assistant", "assistant_proactive"],
31794
+ features: ["private_review_rules", "private_members_unlimited", "ai_code_review_services", "hosted_delegation", "cloudflare_previews", "cloudflare_d1_migrations", "database_migrations", "premium_ai_models", "assistant", "assistant_proactive"],
31776
31795
  minutes_base: 5e4,
31777
31796
  minutes_per_seat: 0,
31778
31797
  concurrent_hosted_jobs: 50,
@@ -32864,6 +32883,22 @@ var ActionsSettingsRequest = external_exports.object({
32864
32883
  allow_untrusted_jobs_on_self_hosted_runners: external_exports.boolean().optional()
32865
32884
  });
32866
32885
  var RunnerLabel = external_exports.string().min(1).max(50).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "letters, digits, '.', '_' and '-'");
32886
+ var ResourceBytes = external_exports.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
32887
+ var RunnerResourceSnapshot = external_exports.object({
32888
+ cpu_percent: external_exports.number().min(0).max(100).nullable(),
32889
+ cpu_count: external_exports.number().int().min(1).max(65536),
32890
+ cpu_interval_ms: external_exports.number().int().min(1).max(6e5).nullable(),
32891
+ memory_total_bytes: ResourceBytes.positive(),
32892
+ memory_available_bytes: ResourceBytes,
32893
+ disk_total_bytes: ResourceBytes.positive().nullable(),
32894
+ disk_available_bytes: ResourceBytes.nullable(),
32895
+ uptime_seconds: external_exports.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
32896
+ job_slots: external_exports.number().int().min(1).max(32)
32897
+ }).strict().superRefine((s, ctx) => {
32898
+ if (s.memory_available_bytes > s.memory_total_bytes) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["memory_available_bytes"], message: "available memory exceeds total memory" });
32899
+ if (s.disk_total_bytes === null !== (s.disk_available_bytes === null) || s.disk_available_bytes !== null && s.disk_available_bytes > s.disk_total_bytes) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["disk_available_bytes"], message: "disk readings must be a valid pair" });
32900
+ if (s.cpu_percent === null !== (s.cpu_interval_ms === null)) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["cpu_interval_ms"], message: "CPU usage requires a sample interval" });
32901
+ });
32867
32902
  var TestRunnerRequest = external_exports.object({
32868
32903
  repository_id: external_exports.string().max(64).nullable().default(null)
32869
32904
  });
@@ -32888,8 +32923,9 @@ var PollJobsRequest = external_exports.object({
32888
32923
  wait_seconds: external_exports.number().int().min(0).max(25).default(20)
32889
32924
  });
32890
32925
  var HeartbeatRequest = external_exports.object({
32891
- running_jobs: external_exports.array(external_exports.string().max(64)).max(32).default([])
32892
- });
32926
+ running_jobs: external_exports.array(external_exports.string().max(64)).max(32).default([]),
32927
+ resources: RunnerResourceSnapshot.optional()
32928
+ }).strict();
32893
32929
  var StepUpdateRequest = external_exports.object({
32894
32930
  name: external_exports.string().max(500),
32895
32931
  status: external_exports.enum(["in_progress", "completed"]),
@@ -33618,7 +33654,7 @@ var CODING_AGENT_SESSION_SCOPES = [
33618
33654
  var EnsureCodingAgentRequest = external_exports.object({ tool: external_exports.enum(AGENT_RUNTIMES) }).strict();
33619
33655
 
33620
33656
  // packages/protocol/src/connections.ts
33621
- var CONNECTION_PURPOSES = ["impact", "secrets_store", "import", "secrets_source", "deploy", "resources", "d1_migrations", "code_review"];
33657
+ var CONNECTION_PURPOSES = ["impact", "secrets_store", "import", "secrets_source", "deploy", "resources", "d1_migrations", "db_migrations", "code_review"];
33622
33658
  var Name3 = external_exports.string().trim().min(1).max(100);
33623
33659
  var Secret2 = external_exports.string().min(1).max(4e3);
33624
33660
  var Id2 = external_exports.string().min(1).max(200).regex(/^[A-Za-z0-9._:-]+$/, "letters, digits, '.', '_', ':' and '-' only");
@@ -33657,9 +33693,10 @@ var CreateConnectionRequest = external_exports.discriminatedUnion("auth", [
33657
33693
  auth: external_exports.literal("api_token"),
33658
33694
  // Cloudflare API tokens (impact, secrets_store, deploy), GitHub fine-grained personal access tokens (import) and
33659
33695
  // AI code review keys (code_review, ADR-0051): CodeRabbit, Greptile, an OpenAI key for Codex, an Anthropic key for
33660
- // Claude Code. The provider/purpose pair is checked server-side.
33661
- provider: external_exports.enum(["cloudflare", "github", "coderabbit", "greptile", "codex", "claude"]),
33662
- purpose: external_exports.enum(["impact", "secrets_store", "import", "deploy", "resources", "d1_migrations", "code_review"]),
33696
+ // Claude Code, a PlanetScale service token (`<id>:<token>`, db_migrations). The provider/purpose pair is checked
33697
+ // server-side.
33698
+ provider: external_exports.enum(["cloudflare", "planetscale", "github", "coderabbit", "greptile", "codex", "claude"]),
33699
+ purpose: external_exports.enum(["impact", "secrets_store", "import", "deploy", "resources", "d1_migrations", "db_migrations", "code_review"]),
33663
33700
  name: Name3,
33664
33701
  credentials: external_exports.object({ api_token: Secret2 })
33665
33702
  }),
@@ -33762,6 +33799,11 @@ var targetFields = {
33762
33799
  preview_check_name: external_exports.string().trim().min(1).max(100).nullable(),
33763
33800
  /** The hosted runner size the deploy jobs run on, by its `runs-on` label: `brnch-hosted` … `brnch-hosted-4` (ADR-0066). */
33764
33801
  runs_on: external_exports.string().trim().min(1).max(40),
33802
+ /**
33803
+ * Paths whose changes deploy the target (CODEOWNERS patterns such as `apps/web/` or `packages/**`, `!` excludes; the
33804
+ * last matching pattern decides). Empty: every change deploys it.
33805
+ */
33806
+ watch_paths: external_exports.array(external_exports.string().trim().min(1).max(500)).max(50),
33765
33807
  steps: external_exports.array(DeployStepSpecBody).max(12)
33766
33808
  };
33767
33809
  var CreateDeployTargetRequest = external_exports.object({
@@ -33779,6 +33821,7 @@ var CreateDeployTargetRequest = external_exports.object({
33779
33821
  preview_agent_changes: targetFields.preview_agent_changes.default(false),
33780
33822
  preview_check_name: targetFields.preview_check_name.default(null),
33781
33823
  runs_on: targetFields.runs_on.default("brnch-hosted"),
33824
+ watch_paths: targetFields.watch_paths.default([]),
33782
33825
  steps: targetFields.steps.default([])
33783
33826
  });
33784
33827
  var UpdateDeployTargetRequest = external_exports.object(targetFields).partial().extend({
@@ -33844,6 +33887,77 @@ var UpsertWorkerEnvRequest = external_exports.object({
33844
33887
  if (v.kind === "var" && v.source !== "value") c8.addIssue({ code: "custom", message: "plain vars take a value", path: ["source"] });
33845
33888
  });
33846
33889
 
33890
+ // 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
+ var DB_EXEC_ORDERS = ["linear", "linear_skip", "non_linear"];
33894
+ var DB_CREDENTIAL_MODES = ["managed_role", "dsn", "branch_password"];
33895
+ var Ident2 = external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_$]{0,62}$/, "letters, digits and _ only (at most 63)");
33896
+ var PsName = external_exports.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/, "a PlanetScale name: letters, digits, '.', '_' and '-'");
33897
+ var SourceFields = {
33898
+ name: external_exports.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9_-]*$/, "lowercase letters, digits, - and _"),
33899
+ /** Directory relative to the repository root ("" = the root). */
33900
+ path: external_exports.string().max(300),
33901
+ format: external_exports.enum(DB_MIGRATION_FORMATS),
33902
+ engine: external_exports.enum(DB_ENGINE_KINDS),
33903
+ /** Branch whose merges create runs; the repository's default branch when omitted. */
33904
+ branch: external_exports.string().min(1).max(200).optional(),
33905
+ /** PlanetScale connection (purpose `db_migrations`) of the organization or the repository. */
33906
+ connection_id: external_exports.string().min(1).max(64),
33907
+ ps_organization: PsName,
33908
+ ps_database: PsName,
33909
+ /** The production branch migrations are applied to. */
33910
+ ps_branch: PsName.default("main"),
33911
+ /** Where the format's revisions table lives (Postgres schema; Atlas default `atlas_schema_revisions`). */
33912
+ revisions_schema: Ident2.nullable().default(null),
33913
+ revisions_table: Ident2.nullable().default(null),
33914
+ /** With no revision history yet, record this version as a baseline instead of applying files up to it. */
33915
+ baseline_version: external_exports.string().min(1).max(100).nullable().default(null),
33916
+ exec_order: external_exports.enum(DB_EXEC_ORDERS).default("linear"),
33917
+ /** Postgres session settings for migration sessions (`lock_timeout`, custom `prefix.name`, …). */
33918
+ session_settings: external_exports.record(external_exports.string().max(126), external_exports.string().max(500)).default({}),
33919
+ statement_timeout_s: external_exports.number().int().min(1).max(600).default(300),
33920
+ lock_timeout_s: external_exports.number().int().min(1).max(60).default(10),
33921
+ credential_mode: external_exports.enum(DB_CREDENTIAL_MODES).optional(),
33922
+ /** Postgres: the role new objects belong to (`SET ROLE`); `postgres` by default for managed roles, none for a DSN. */
33923
+ 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"),
33926
+ /** Validate the directory on changes (evidence `migrations/<name>`). */
33927
+ change_checks: external_exports.boolean().default(true)
33928
+ };
33929
+ var CreateDbMigrationSourceRequest = external_exports.object({
33930
+ ...SourceFields,
33931
+ /** Postgres `credential_mode: dsn` only: `postgresql://user:password@host:5432/db?sslmode=verify-full`. Write-only. */
33932
+ dsn: external_exports.string().min(10).max(2e3).optional()
33933
+ });
33934
+ var UpdateDbMigrationSourceRequest = external_exports.object({
33935
+ path: SourceFields.path,
33936
+ branch: SourceFields.branch,
33937
+ connection_id: SourceFields.connection_id,
33938
+ ps_organization: SourceFields.ps_organization,
33939
+ ps_database: SourceFields.ps_database,
33940
+ ps_branch: PsName,
33941
+ revisions_schema: Ident2.nullable(),
33942
+ revisions_table: Ident2.nullable(),
33943
+ baseline_version: external_exports.string().min(1).max(100).nullable(),
33944
+ exec_order: external_exports.enum(DB_EXEC_ORDERS),
33945
+ session_settings: external_exports.record(external_exports.string().max(126), external_exports.string().max(500)),
33946
+ statement_timeout_s: SourceFields.statement_timeout_s.unwrap(),
33947
+ lock_timeout_s: SourceFields.lock_timeout_s.unwrap(),
33948
+ credential_mode: external_exports.enum(DB_CREDENTIAL_MODES),
33949
+ run_as_role: external_exports.string().regex(/^[a-z_][a-z0-9_]{0,62}$/).nullable(),
33950
+ on_merge: external_exports.enum(["approval", "manual_only"]),
33951
+ change_checks: external_exports.boolean(),
33952
+ state: external_exports.enum(["active", "disabled"]),
33953
+ dsn: external_exports.string().min(10).max(2e3).nullable()
33954
+ }).partial().extend({ version: external_exports.number().int().min(1) });
33955
+ var StartDbMigrationRunRequest = external_exports.object({
33956
+ /** Branch, tag or commit to apply; the source's branch when omitted. */
33957
+ ref: external_exports.string().min(1).max(200).optional()
33958
+ });
33959
+ var DecideDbMigrationRunRequest = external_exports.object({ reason: external_exports.string().max(500).optional() });
33960
+
33847
33961
  // packages/protocol/src/buildkite.ts
33848
33962
  var BUILDKITE_TRUST_MODES = ["contributor_controlled", "pipeline_settings", "target_ref_upload"];
33849
33963
  var Slug3 = external_exports.string().trim().min(1).max(100).regex(/^[a-z0-9][a-z0-9-_]*$/i, "a Buildkite slug (letters, digits, '-' and '_')");
@@ -36403,6 +36517,38 @@ function tasksGroup(t) {
36403
36517
  };
36404
36518
  }
36405
36519
 
36520
+ // packages/sdk/src/groups/db-migrations.ts
36521
+ 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 });
36523
+ return {
36524
+ sources: {
36525
+ list: (repo, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/db-migrations/sources`, options: o }),
36526
+ create: (repo, body, o) => t.json({ method: "POST", path: path2`/v1/repos/${repo}/db-migrations/sources`, json: validate2(CreateDbMigrationSourceRequest, body, "CreateDbMigrationSourceRequest"), options: o }),
36527
+ get: (id2, o) => t.json({ method: "GET", path: path2`/v1/db-migration-sources/${id2}`, options: o }),
36528
+ 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 })
36530
+ },
36531
+ /** Migration directories found in the repository tree; `configured` marks those a source already covers. */
36532
+ suggestions: (repo, ref, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/db-migrations/suggestions`, query: ref ? { ref } : {}, options: o }),
36533
+ /** What applying the source at `ref` would do. `live` reads the database now instead of the last snapshot. */
36534
+ 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 }),
36535
+ /** Reads the applied versions from the database again and returns the plan at the source's branch. */
36536
+ 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. */
36538
+ startRun: (sourceId2, body = {}, o) => t.json({ method: "POST", path: path2`/v1/db-migration-sources/${sourceId2}/runs`, json: validate2(StartDbMigrationRunRequest, body, "StartDbMigrationRunRequest"), options: o }),
36539
+ runs: {
36540
+ list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/db-migrations/runs`, query: { ...q }, options: o }),
36541
+ get: (id2, o) => t.json({ method: "GET", path: path2`/v1/db-migration-runs/${id2}`, options: o })
36542
+ },
36543
+ approve: decide("approve"),
36544
+ reject: decide("reject"),
36545
+ retry: decide("retry"),
36546
+ cancel: decide("cancel"),
36547
+ /** The plan of every source a change touches, with the files the change adds or edits marked. */
36548
+ changePlans: (changeId, o) => t.json({ method: "GET", path: path2`/v1/changes/${changeId}/db-migrations`, options: o })
36549
+ };
36550
+ }
36551
+
36406
36552
  // packages/sdk/src/groups/deploy.ts
36407
36553
  function deployGroup(t) {
36408
36554
  return {
@@ -36475,6 +36621,8 @@ function packagesGroup(t) {
36475
36621
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/packages`, query: { ...q }, options: o }),
36476
36622
  get: (image, o) => t.json({ method: "GET", path: path2`/v1/packages/${image}`, options: o }),
36477
36623
  versions: (image, q = {}, o) => t.json({ method: "GET", path: path2`/v1/packages/${image}/versions`, query: { ...q }, options: o }),
36624
+ /** Deletes an image with no versions left (`CONFLICT` otherwise). */
36625
+ delete: (image, o) => t.void({ method: "DELETE", path: path2`/v1/packages/${image}`, options: o }),
36478
36626
  deleteVersion: (image, digest, o) => t.void({ method: "DELETE", path: path2`/v1/packages/${image}/versions/${digest}`, options: o }),
36479
36627
  deleteTag: (image, tag, o) => t.void({ method: "DELETE", path: path2`/v1/packages/${image}/tags/${tag}`, options: o }),
36480
36628
  usage: (org, o) => t.json({ method: "GET", path: path2`/v1/orgs/${org}/registry`, options: o }),
@@ -36569,6 +36717,7 @@ function createClient(options) {
36569
36717
  secretSyncs: secretSyncsGroup(t),
36570
36718
  grants: grantsGroup(t),
36571
36719
  deploy: deployGroup(t),
36720
+ dbMigrations: dbMigrationsGroup(t),
36572
36721
  codeReview: codeReviewGroup(t),
36573
36722
  releases: releasesGroup(t),
36574
36723
  packages: packagesGroup(t),
@@ -36901,7 +37050,7 @@ function invalidAgentToolFlag(flag) {
36901
37050
  }
36902
37051
 
36903
37052
  // packages/cli/package.json
36904
- var version2 = "0.18.1";
37053
+ var version2 = "0.18.3";
36905
37054
 
36906
37055
  // packages/cli/src/brand.ts
36907
37056
  var BRAND = {
@@ -38972,6 +39121,67 @@ ${final.join("\n")}
38972
39121
  deps.stderr.write("\n");
38973
39122
  }
38974
39123
 
39124
+ // packages/cli/src/program-status.ts
39125
+ var MSG_MAX_BYTES = 512;
39126
+ function encodeMsg(text2) {
39127
+ let line2 = text2.replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ").trim();
39128
+ const enc3 = new TextEncoder();
39129
+ while (enc3.encode(line2).length > MSG_MAX_BYTES) line2 = [...line2].slice(0, -1).join("");
39130
+ return Buffer.from(line2, "utf8").toString("base64");
39131
+ }
39132
+ function programStatus(r) {
39133
+ const pairs = [`state=${r.state}`, `app=${BRAND.cli}`];
39134
+ if (r.state === "blocked") pairs.push(`kind=${r.kind}`);
39135
+ if (r.state === "working" && r.progress !== void 0 && Number.isFinite(r.progress)) pairs.push(`progress=${Math.min(100, Math.max(0, Math.round(r.progress)))}`);
39136
+ if (r.msg) pairs.push(`msg=${encodeMsg(r.msg)}`);
39137
+ return `\x1B]7501;${pairs.join(":")}\x1B\\`;
39138
+ }
39139
+ function programStatusEnabled(deps, agent) {
39140
+ const off = deps.env[envName("PROGRAM_STATUS")]?.trim().toLowerCase();
39141
+ if (off === "0" || off === "false" || off === "off" || off === "no") return false;
39142
+ return deps.stderrIsTTY && !agent && !deps.env.CI && deps.env.TERM !== "dumb";
39143
+ }
39144
+ var silentStatus = {
39145
+ working: () => {
39146
+ },
39147
+ blocked: () => {
39148
+ },
39149
+ ask: () => () => {
39150
+ },
39151
+ finish: () => {
39152
+ }
39153
+ };
39154
+ function statusReporter(out) {
39155
+ let current = null;
39156
+ let waited = false;
39157
+ let last = "";
39158
+ const send = (r) => {
39159
+ const s = programStatus(r);
39160
+ if (s === last) return;
39161
+ last = s;
39162
+ out.write(s);
39163
+ };
39164
+ const report3 = (r) => {
39165
+ current = r;
39166
+ waited = true;
39167
+ send(r);
39168
+ };
39169
+ return {
39170
+ working: (msg, progress) => report3({ state: "working", msg, ...progress !== void 0 ? { progress } : {} }),
39171
+ blocked: (kind, msg) => report3({ state: "blocked", kind, msg }),
39172
+ ask() {
39173
+ send({ state: "blocked", kind: "question", msg: "Waiting for your answer" });
39174
+ return () => send(current ?? { state: "working" });
39175
+ },
39176
+ finish(ok, msg) {
39177
+ if (!waited) return;
39178
+ send({ state: ok ? "done" : "error", msg });
39179
+ waited = false;
39180
+ current = null;
39181
+ }
39182
+ };
39183
+ }
39184
+
38975
39185
  // packages/cli/src/context.ts
38976
39186
  async function findProjectFile(deps) {
38977
39187
  let dir = deps.cwd;
@@ -39316,6 +39526,8 @@ async function createContext(deps, globals, state2) {
39316
39526
  busy: (label2) => {
39317
39527
  if (motion) state2.spinner?.start(label2);
39318
39528
  },
39529
+ /** Tell the terminal what the command waits on (program-status.ts): the server (`working`) or the person (`blocked`). */
39530
+ status: state2.status ?? silentStatus,
39319
39531
  /** Clear the spinner before handing the terminal to something that does not write through `deps` (rt.invoke). */
39320
39532
  idle: () => state2.spinner?.stop(),
39321
39533
  result,
@@ -42184,6 +42396,30 @@ function input2(deps, o) {
42184
42396
  });
42185
42397
  }
42186
42398
 
42399
+ // packages/cli/src/step-up.ts
42400
+ function needsCliStepUp(e) {
42401
+ const err = isBrnchApiError(e) ? e : e;
42402
+ return err?.code === "MFA_REQUIRED" && err.details?.step_up === "cli";
42403
+ }
42404
+ function sameOrigin(ctx, start) {
42405
+ try {
42406
+ return new URL(start.verification_uri_complete).origin === new URL(ctx.baseUrl).origin;
42407
+ } catch {
42408
+ return false;
42409
+ }
42410
+ }
42411
+ async function awaitStepUp(ctx, start) {
42412
+ const deadline = Date.parse(start.expires_at);
42413
+ ctx.status.blocked("auth", `Confirm it's you in the browser (code ${start.user_code})`);
42414
+ for (; ; ) {
42415
+ await ctx.deps.sleep(start.interval * 1e3);
42416
+ const s = await ctx.client("user").auth.stepUp.status(start.id);
42417
+ if (s.status === "approved") return s.step_up_expires_at;
42418
+ if (s.status === "denied") throw new CliError("the step-up was denied in the browser", EXIT.UNAUTHENTICATED, "access_denied");
42419
+ if (s.status === "expired" || ctx.deps.sys.now() > deadline + 5e3) throw new CliError(`the code expired before it was approved; run \`${BRAND.cli} auth step-up\` again`, EXIT.UNAUTHENTICATED, "expired_token");
42420
+ }
42421
+ }
42422
+
42187
42423
  // packages/cli/src/pending-login.ts
42188
42424
  import { mkdir as mkdir11, readFile as readFile9, rm as rm7, writeFile as writeFile10 } from "node:fs/promises";
42189
42425
  import { dirname as dirname11, join as join18 } from "node:path";
@@ -42372,6 +42608,7 @@ var accountChoice = (a) => ({
42372
42608
  });
42373
42609
  async function finishLogin(ctx, start, agentRuntime, hooks) {
42374
42610
  const oauth = ctx.client("user", { anonymous: true }).oauth;
42611
+ ctx.status.blocked("auth", `Approve the sign-in in your browser (code ${start.user_code})`);
42375
42612
  let tok;
42376
42613
  try {
42377
42614
  tok = await pollDeviceToken(oauth, start, CLI_OAUTH_CLIENT_ID, { sleep: ctx.deps.sleep });
@@ -42455,7 +42692,7 @@ function registerAuth(program2, rt) {
42455
42692
  `This sign-in is for ${agentSignIn.label}, an AI coding agent (detected from ${agentSignIn.source}). The browser asks you to approve an agent sign-in: it can read and start agent sessions you sponsor, and never acts as you. To sign in as yourself, run this in your own terminal (or pass --as-human).`
42456
42693
  );
42457
42694
  }
42458
- const sameOrigin = (() => {
42695
+ const sameOrigin2 = (() => {
42459
42696
  try {
42460
42697
  return new URL(start.verification_uri_complete).origin === new URL(ctx.baseUrl).origin;
42461
42698
  } catch {
@@ -42466,7 +42703,7 @@ function registerAuth(program2, rt) {
42466
42703
  ${codeBox(start.user_code, paint(true))}
42467
42704
  ` : `! Your one-time code: ${start.user_code}`);
42468
42705
  let opened = false;
42469
- if (o.browser && ctx.interactive && sameOrigin && (!canPick(ctx) || await pressEnter(ctx.deps, `Press Enter to open ${new URL(start.verification_uri).host} in your browser`)))
42706
+ if (o.browser && ctx.interactive && sameOrigin2 && (!canPick(ctx) || await pressEnter(ctx.deps, `Press Enter to open ${new URL(start.verification_uri).host} in your browser`)))
42470
42707
  opened = await ctx.deps.openUrl(start.verification_uri_complete);
42471
42708
  ctx.info(
42472
42709
  opened ? `Opened ${start.verification_uri} in your browser. ${paint(colorEnabled(ctx.deps, "stderr")).dim("Check that it shows the same code, then approve.")}` : `Open ${start.verification_uri_complete}
@@ -42651,18 +42888,11 @@ Store them somewhere safe and offline (a password manager or on paper).`
42651
42888
  }
42652
42889
  }
42653
42890
  const start = await stepUp.start();
42654
- const sameOrigin = (() => {
42655
- try {
42656
- return new URL(start.verification_uri_complete).origin === new URL(ctx.baseUrl).origin;
42657
- } catch {
42658
- return false;
42659
- }
42660
- })();
42661
42891
  ctx.info(ctx.motion ? `
42662
42892
  ${codeBox(start.user_code, paint(true))}
42663
42893
  ` : `! Your one-time code: ${start.user_code}`);
42664
42894
  let opened = false;
42665
- if (o.browser && ctx.interactive && sameOrigin && (!canPick(ctx) || await pressEnter(ctx.deps, `Press Enter to open ${new URL(start.verification_uri).host} and confirm it's you`)))
42895
+ if (o.browser && ctx.interactive && sameOrigin(ctx, start) && (!canPick(ctx) || await pressEnter(ctx.deps, `Press Enter to open ${new URL(start.verification_uri).host} and confirm it's you`)))
42666
42896
  opened = await ctx.deps.openUrl(start.verification_uri_complete);
42667
42897
  ctx.info(
42668
42898
  opened ? `Opened ${start.verification_uri} in your browser. Check that it shows the same code, then approve and confirm it's you.` : `Open ${start.verification_uri_complete}
@@ -42670,20 +42900,12 @@ ${codeBox(start.user_code, paint(true))}
42670
42900
  );
42671
42901
  if (ctx.motion) ctx.busy(`waiting for you to confirm it's you in the browser \xB7 the code expires in ${minutesLeft(start.expires_at, ctx.deps.sys.now())}`);
42672
42902
  else ctx.info("Waiting for approval\u2026");
42673
- const deadline = Date.parse(start.expires_at);
42674
- for (; ; ) {
42675
- await ctx.deps.sleep(start.interval * 1e3);
42676
- const s = await stepUp.status(start.id);
42677
- if (s.status === "approved") {
42678
- return ctx.result(
42679
- { status: "approved", step_up_expires_at: s.step_up_expires_at },
42680
- (v) => `${p.ok("\u2713")} Stepped up until ${v.step_up_expires_at ? `${clockTime(v.step_up_expires_at)}${left(v.step_up_expires_at)}` : "the server's limit"}: administrative actions work from this login until then.
42903
+ const until = await awaitStepUp(ctx, start);
42904
+ return ctx.result(
42905
+ { status: "approved", step_up_expires_at: until },
42906
+ (v) => `${p.ok("\u2713")} Stepped up until ${v.step_up_expires_at ? `${clockTime(v.step_up_expires_at)}${left(v.step_up_expires_at)}` : "the server's limit"}: administrative actions work from this login until then.
42681
42907
  ${p.dim(`End it sooner: ${BRAND.cli} auth step-up --end`)}`
42682
- );
42683
- }
42684
- if (s.status === "denied") throw new CliError("the step-up was denied in the browser", EXIT.UNAUTHENTICATED, "access_denied");
42685
- if (s.status === "expired" || Date.now() > deadline + 5e3) throw new CliError(`the code expired before it was approved; run \`${BRAND.cli} auth step-up\` again`, EXIT.UNAUTHENTICATED, "expired_token");
42686
- }
42908
+ );
42687
42909
  })
42688
42910
  );
42689
42911
  auth.command("git-credential").description("git credential helper: answers `get` for the configured server with your token (store/erase are no-ops)").argument("<operation>", "get | store | erase (invoked by git)").action(
@@ -43459,6 +43681,7 @@ var Wizard = class {
43459
43681
  /** The spinner says what the step is checking until it prints, asks, or hands over to a composed command. */
43460
43682
  busy(step2) {
43461
43683
  this.ctx.busy(STEP_BUSY[step2]);
43684
+ this.ctx.status.working(STEP_BUSY[step2]);
43462
43685
  }
43463
43686
  /** Global flags every composed command must keep (same server, credential and account). */
43464
43687
  globalArgs() {
@@ -44359,6 +44582,7 @@ var Quickstart = class {
44359
44582
  /** The board row says what is happening now (no-op without a board). */
44360
44583
  running(row, text2) {
44361
44584
  this.board?.run(row, text2);
44585
+ this.ctx.status.working(text2);
44362
44586
  }
44363
44587
  /** A step's outcome: kept for the result, and shown on the board (`show`) or as a line. */
44364
44588
  record(step2, status, detail, show2 = detail) {
@@ -44376,6 +44600,41 @@ var Quickstart = class {
44376
44600
  const g2 = this.ctx.globals;
44377
44601
  return [...g2.url ? ["--url", g2.url] : [], ...g2.token ? ["--token", g2.token] : [], ...g2.account ? ["--account", g2.account] : [], ...g2.asHuman ? ["--as-human"] : [], ...g2.agentTool ? ["--agent-tool", g2.agentTool] : []];
44378
44602
  }
44603
+ /** Whether this run already asked the person to step up (once: a second refusal is the answer). */
44604
+ steppedUp = false;
44605
+ /**
44606
+ * `run`, and again after the person confirms it's them in the browser when the organization requires a second factor
44607
+ * for it (ADR-0062): quickstart runs long enough that a step-up approved before it may have lapsed by the deploy.
44608
+ * `refusal` finds the error in a composed command's `--json` output.
44609
+ */
44610
+ async withStepUp(row, run3, refusal = () => null) {
44611
+ try {
44612
+ const v = await run3();
44613
+ if (!needsCliStepUp(refusal(v)) || !await this.stepUp(row)) return v;
44614
+ } catch (e) {
44615
+ if (!needsCliStepUp(e) || !await this.stepUp(row)) throw e;
44616
+ }
44617
+ return run3();
44618
+ }
44619
+ async stepUp(row) {
44620
+ const { ctx } = this;
44621
+ if (!ctx.interactive || this.steppedUp) return false;
44622
+ this.steppedUp = true;
44623
+ const start = await ctx.client("user").auth.stepUp.start();
44624
+ const opened = sameOrigin(ctx, start) && await ctx.deps.openUrl(start.verification_uri_complete);
44625
+ const todo = `Your organization asks for a second factor here: confirm it's you ${opened ? "in the page that just opened" : `at ${start.verification_uri_complete}`} (code ${start.user_code})`;
44626
+ if (this.board) {
44627
+ this.running(row, "waiting for you to confirm it's you in the browser");
44628
+ this.board.hint(`${todo} ${this.errPaint.dim("(Ctrl-C stops; the repository is ready either way)")}`);
44629
+ } else ctx.info(` ${todo}. Waiting for it\u2026`);
44630
+ try {
44631
+ await awaitStepUp(ctx, start);
44632
+ } finally {
44633
+ this.board?.hint(null);
44634
+ }
44635
+ this.note(row, "confirmed: continuing");
44636
+ return true;
44637
+ }
44379
44638
  /** A composed command, in `cwd`, as JSON. Under the board it runs quietly (its stderr and git's are returned instead). */
44380
44639
  async runJson(argv, cwd) {
44381
44640
  this.ctx.idle();
@@ -44625,7 +44884,7 @@ async function stepDeploy(q, dir, ns, name, o, home2) {
44625
44884
  const later = `connect Cloudflare at ${integrationsUrl(ctx, ns)}, then \`${BRAND.cli} --repo ${spec} deploy setup\` and \`${BRAND.cli} --repo ${spec} deploy run production --wait\``;
44626
44885
  q.running("deploy", "looking for a Cloudflare connection");
44627
44886
  const repo = await ctx.repoId(spec, "user");
44628
- const usable = async () => (await ctx.client("user").deploy.connections(repo)).items.some((c8) => c8.state === "active");
44887
+ const usable = () => q.withStepUp("deploy", async () => (await ctx.client("user").deploy.connections(repo)).items.some((c8) => c8.state === "active"));
44629
44888
  if (!await usable()) {
44630
44889
  if (!ctx.interactive) {
44631
44890
  q.record("deploy", "planned", later, `connect Cloudflare, then \`${BRAND.cli} deploy setup\``);
@@ -44638,6 +44897,7 @@ async function stepDeploy(q, dir, ns, name, o, home2) {
44638
44897
  q.running("deploy", "waiting for Cloudflare to be connected");
44639
44898
  q.board.hint(`${todo} ${q.errPaint.dim("(Ctrl-C stops; the repository is ready either way)")}`);
44640
44899
  } else ctx.info(` ${todo}. Waiting for it (Ctrl-C stops; the repository is ready either way)\u2026`);
44900
+ ctx.status.blocked("auth", "Connect Cloudflare in the browser");
44641
44901
  const deadline2 = ctx.deps.sys.now() + o.connectTimeout * 1e3;
44642
44902
  let ready = false;
44643
44903
  while (!ready && ctx.deps.sys.now() < deadline2) {
@@ -44652,7 +44912,11 @@ async function stepDeploy(q, dir, ns, name, o, home2) {
44652
44912
  }
44653
44913
  q.running("deploy", "creating the deploy target");
44654
44914
  const pinned = home2.cloudflare ? ["--connection", home2.cloudflare.connection, "--account-id", home2.cloudflare.account] : [];
44655
- const setup = await q.runJson(["--repo", spec, "deploy", "setup", ...pinned], dir);
44915
+ const setup = await q.withStepUp(
44916
+ "deploy",
44917
+ () => q.runJson(["--repo", spec, "deploy", "setup", ...pinned], dir),
44918
+ (r) => r.value?.error
44919
+ );
44656
44920
  if (setup.code === EXIT.USAGE) {
44657
44921
  q.record("deploy", "planned", `choose what deploys it, then \`${BRAND.cli} --repo ${spec} deploy setup --connection <icn_\u2026> [--account-id <id>]\` and \`${BRAND.cli} --repo ${spec} deploy run production --wait\`${setup.reason}`, `choose a Cloudflare connection: \`${BRAND.cli} deploy setup --connection \u2026\``);
44658
44922
  return;
@@ -44662,7 +44926,8 @@ async function stepDeploy(q, dir, ns, name, o, home2) {
44662
44926
  return;
44663
44927
  }
44664
44928
  const deploy = ctx.client("user").deploy;
44665
- let d = await deploy.targets.deploy(setup.value.id, {});
44929
+ const target = setup.value.id;
44930
+ let d = await q.withStepUp("deploy", () => deploy.targets.deploy(target, {}));
44666
44931
  const deadline = ctx.deps.sys.now() + o.deployTimeout * 1e3;
44667
44932
  let last = "";
44668
44933
  for (; ; ) {
@@ -46237,6 +46502,7 @@ async function waitDone(ctx, change, interval, timeout) {
46237
46502
  const now = f.reviews.map((r) => `${CODE_REVIEW_PROVIDER_NAMES[r.provider]}: ${r.status}`).join(", ");
46238
46503
  if (now && now !== said) ctx.info(now);
46239
46504
  said = now;
46505
+ ctx.status.working(now ? `AI review: ${now}` : "Waiting for AI review");
46240
46506
  if (!f.pending || Date.now() > deadline) return f;
46241
46507
  await ctx.deps.sleep(Math.max(1, interval) * 1e3);
46242
46508
  }
@@ -46402,6 +46668,7 @@ async function followLive(ctx, c8, first2, deadline, interval) {
46402
46668
  const stop = ctx.deps.ticker(250, draw2);
46403
46669
  try {
46404
46670
  while (!TERMINAL3.has(cur.state) && Date.now() < deadline) {
46671
+ ctx.status.working(`Merge ${cur.state}`);
46405
46672
  await ctx.deps.sleep(Math.max(1, interval) * 1e3);
46406
46673
  cur = await c8.promotions.get(cur.id);
46407
46674
  reached = stepOf(cur.state, reached);
@@ -46426,6 +46693,7 @@ function registerMerge(program2, rt) {
46426
46693
  let last = p.state;
46427
46694
  ctx.info(`promotion ${p.id}: ${p.state}`);
46428
46695
  while (!TERMINAL3.has(p.state) && Date.now() < deadline) {
46696
+ ctx.status.working(`Merge ${p.state}`);
46429
46697
  await ctx.deps.sleep(Math.max(1, o.interval) * 1e3);
46430
46698
  p = await c8.promotions.get(p.id);
46431
46699
  if (p.state !== last) ctx.info(`promotion ${p.id}: ${last = p.state}`);
@@ -46703,6 +46971,7 @@ function registerSecretScanning(program2, rt) {
46703
46971
  const c8 = ctx.client("user");
46704
46972
  let job2 = await c8.secretScanning.scans.start(repo);
46705
46973
  while (o.wait && (job2.state === "queued" || job2.state === "running")) {
46974
+ ctx.status.working(`History scan ${job2.state}`);
46706
46975
  await ctx.deps.sleep(Math.max(1, o.interval) * 1e3);
46707
46976
  job2 = await c8.secretScanning.scans.get(repo, job2.id);
46708
46977
  }
@@ -46992,9 +47261,9 @@ async function findItems(client, specs) {
46992
47261
  }
46993
47262
  async function itemIds(client, specs) {
46994
47263
  const full = (s) => /^notif_[0-9a-z]{26}$/.test(s);
46995
- const short2 = specs.filter((s) => !full(s));
46996
- const items = short2.length ? await findItems(client, short2) : [];
46997
- return specs.map((s) => full(s) ? s : items[short2.indexOf(s)].id);
47264
+ const short3 = specs.filter((s) => !full(s));
47265
+ const items = short3.length ? await findItems(client, short3) : [];
47266
+ return specs.map((s) => full(s) ? s : items[short3.indexOf(s)].id);
46998
47267
  }
46999
47268
  async function repoNames(client, items) {
47000
47269
  const ids = [...new Set(items.map((i) => i.repository_id))];
@@ -47739,6 +48008,187 @@ function registerIntegrations(program2, rt) {
47739
48008
  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)));
47740
48009
  }
47741
48010
 
48011
+ // packages/cli/src/commands/db.ts
48012
+ async function sourceId(ctx, ref, repo) {
48013
+ if (ref.startsWith("dms_")) return ref;
48014
+ const { items } = await ctx.client("user").dbMigrations.sources.list(repo?.id ?? await ctx.repoId(void 0, "user"));
48015
+ const s = items.find((x) => x.name === ref);
48016
+ if (!s) throw new CliError(`no migration source named "${ref}" (have: ${items.map((x) => x.name).join(", ") || "none"})`, EXIT.NOT_FOUND, "NOT_FOUND");
48017
+ return s.id;
48018
+ }
48019
+ var plural3 = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
48020
+ var short = (oid) => oid.slice(0, 12);
48021
+ var database = (s) => `${s.ps_organization}/${s.ps_database}@${s.ps_branch}`;
48022
+ function issueLines(issues) {
48023
+ if (!issues.length) return [];
48024
+ return ["issues:", ...issues.map((i) => ` ${i.level.padEnd(7)} ${i.code} ${i.file ? `${i.file}: ` : ""}${i.message}`)];
48025
+ }
48026
+ 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"]
48029
+ );
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"])}
48031
+
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}`);
48033
+ var renderSuggestions = (v) => v.items.length ? table(
48034
+ v.items.map((s) => ({ ...s, path: s.path || "(root)", configured: s.configured ? "yes" : "no" })),
48035
+ ["path", "format", "files", "revisions_schema", "baseline_version", "configured", "evidence"]
48036
+ ) : "no migration directories found";
48037
+ function renderPlan2(p) {
48038
+ const db = p.database === "live" ? "read now" : p.database === "snapshot" ? `as last read${p.read_at ? ` ${p.read_at}` : ""}` : `not read${p.note ? `: ${p.note}` : ""}`;
48039
+ const lines2 = [`${p.source_name} at ${p.ref} (${short(p.commit_oid)}): ${p.ok ? "ok" : "has errors"}`, `database: ${db}`, `files: ${p.files}, applied: ${p.applied.length}${p.applied.length ? ` (latest ${p.applied[p.applied.length - 1]})` : ""}`];
48040
+ if (p.baseline) lines2.push(`baseline: ${p.baseline}${p.no_history ? " (recorded, not applied: the database has no revision history)" : ""}`);
48041
+ else if (p.no_history && p.database !== "none") lines2.push("no revision history yet: every file is pending");
48042
+ if (p.database !== "live" && p.note) lines2.push(`note: ${p.note}`);
48043
+ if (!p.pending.length) lines2.push("nothing pending");
48044
+ else {
48045
+ lines2.push(`pending (${p.pending.length}):`);
48046
+ for (const f of p.pending) {
48047
+ const notes = [f.resume_from ? `resumes at statement ${f.resume_from + 1}` : "", f.tx_mode === "none" ? "no transaction" : "", f.introduced_by_change ? "in this change" : ""].filter(Boolean);
48048
+ lines2.push(` ${f.name} ${plural3(f.statements, "statement")}${notes.length ? ` (${notes.join(", ")})` : ""}`);
48049
+ }
48050
+ }
48051
+ return [...lines2, ...issueLines(p.issues)].join("\n");
48052
+ }
48053
+ var renderPlans = (v) => v.items.length ? v.items.map(renderPlan2).join("\n\n") : "no migration sources";
48054
+ var NEXT = {
48055
+ awaiting_approval: (id2) => `Waiting for a repository admin: ${BRAND.cli} db approve ${id2} (or reject)`,
48056
+ 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`
48058
+ };
48059
+ function renderRun(r) {
48060
+ const ps = r.planetscale;
48061
+ const head = kv(
48062
+ {
48063
+ ...r,
48064
+ commit: short(r.commit_oid),
48065
+ change: r.change_id ?? "-",
48066
+ requested_by: r.requested_by ? `${r.requested_by.type} ${r.requested_by.id}` : "-",
48067
+ deploy_request: ps?.deploy_request_url ?? (ps?.deploy_request ? `#${ps.deploy_request}` : null),
48068
+ deployment_state: ps?.deployment_state ?? null
48069
+ },
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"]
48071
+ );
48072
+ const files = r.files.length ? table(
48073
+ 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
+ ["name", "status", "statements", "execution_ms", "error"]
48075
+ ) : "no files";
48076
+ const next = NEXT[r.status]?.(r.id);
48077
+ return [head, "", files, ...r.issues.length ? ["", ...issueLines(r.issues)] : [], ...next ? ["", next] : []].join("\n");
48078
+ }
48079
+ var renderRuns = (v) => listWithCursor(
48080
+ { ...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` })) },
48081
+ ["id", "source_name", "status", "kind", "commit", "files", "created_at"]
48082
+ );
48083
+ var oneOf = (flag, values) => (v) => {
48084
+ if (!values.includes(v)) throw usage(`${flag} must be one of ${values.join(", ")}`);
48085
+ return v;
48086
+ };
48087
+ function sessionSettings(pairs) {
48088
+ if (!pairs) return void 0;
48089
+ const out = {};
48090
+ for (const p of pairs) {
48091
+ const eq = p.indexOf("=");
48092
+ if (eq < 1) throw usage(`--session-setting must be name=value (got "${p}")`);
48093
+ out[p.slice(0, eq)] = p.slice(eq + 1);
48094
+ }
48095
+ return out;
48096
+ }
48097
+ 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(
48101
+ act(rt, async (ctx, o) => {
48102
+ let dsn;
48103
+ if (o.dsnFile !== void 0) {
48104
+ if (o.dsnFile === "-" && ctx.deps.stdinIsTTY) throw usage(`pipe the connection string on stdin, e.g. \`pbpaste | ${BRAND.cli} db sources add \u2026 --dsn-file -\``);
48105
+ dsn = (await readText(fileSpec(o.dsnFile), ctx.input)).trim();
48106
+ if (!dsn) throw usage("the connection string is empty");
48107
+ ctx.addSecret(dsn);
48108
+ const password = /^[a-z]+:\/\/[^:/@]*:([^@]+)@/i.exec(dsn)?.[1];
48109
+ if (password) ctx.addSecret(password);
48110
+ }
48111
+ const body = {
48112
+ name: o.name,
48113
+ path: o.path,
48114
+ format: o.format,
48115
+ engine: o.engine,
48116
+ connection_id: o.connection,
48117
+ ps_organization: o.organization,
48118
+ ps_database: o.database,
48119
+ ps_branch: o.psBranch,
48120
+ change_checks: o.changeChecks,
48121
+ ...o.branch ? { branch: o.branch } : {},
48122
+ ...o.revisionsSchema ? { revisions_schema: o.revisionsSchema } : {},
48123
+ ...o.revisionsTable ? { revisions_table: o.revisionsTable } : {},
48124
+ ...o.baseline ? { baseline_version: o.baseline } : {},
48125
+ ...o.execOrder ? { exec_order: o.execOrder } : {},
48126
+ ...o.onMerge ? { on_merge: o.onMerge } : {},
48127
+ ...o.credentialMode ? { credential_mode: o.credentialMode } : {},
48128
+ ...o.runAsRole ? { run_as_role: o.runAsRole } : {},
48129
+ ...o.sessionSetting ? { session_settings: sessionSettings(o.sessionSetting) } : {},
48130
+ ...o.statementTimeout !== void 0 ? { statement_timeout_s: o.statementTimeout } : {},
48131
+ ...o.lockTimeout !== void 0 ? { lock_timeout_s: o.lockTimeout } : {},
48132
+ ...dsn ? { dsn } : {}
48133
+ };
48134
+ ctx.result(await ctx.client("user").dbMigrations.sources.create(await ctx.repoId(void 0, "user"), body), renderSource);
48135
+ })
48136
+ );
48137
+ sources.command("rm <source>").description("Delete a migration source (name or id; its runs stay listed). Requires --yes when non-interactive").action(
48138
+ act(rt, async (ctx, _o, [ref]) => {
48139
+ const id2 = await sourceId(ctx, ref);
48140
+ await ctx.confirm(`delete migration source ${ref}`);
48141
+ await ctx.client("user").dbMigrations.sources.delete(id2);
48142
+ ctx.result({ deleted: id2 }, `deleted ${ref}`);
48143
+ })
48144
+ );
48145
+ 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(
48147
+ act(rt, async (ctx, o, [ref]) => {
48148
+ const c8 = ctx.client("user").dbMigrations;
48149
+ const q = { ref: o.ref, live: o.live };
48150
+ if (ref) {
48151
+ const p = await c8.plan(await sourceId(ctx, ref), q);
48152
+ ctx.result(p, renderPlan2);
48153
+ if (!p.ok) ctx.setExit(EXIT.NOT_READY);
48154
+ return;
48155
+ }
48156
+ const { items } = await c8.sources.list(await ctx.repoId(void 0, "user"));
48157
+ const plans = { items: await Promise.all(items.filter((s) => s.state === "active").map((s) => c8.plan(s.id, q))) };
48158
+ ctx.result(plans, renderPlans);
48159
+ if (plans.items.some((p) => !p.ok)) ctx.setExit(EXIT.NOT_READY);
48160
+ })
48161
+ );
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)));
48163
+ 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
+ act(rt, async (ctx, o) => {
48165
+ const repo = { id: await ctx.repoId(void 0, "user") };
48166
+ const q = { ...o.source ? { source_id: await sourceId(ctx, o.source, repo) } : {}, ...o.limit ? { limit: o.limit } : {}, ...o.cursor ? { cursor: o.cursor } : {} };
48167
+ ctx.result(await ctx.client("user").dbMigrations.runs.list(repo.id, q), renderRuns);
48168
+ })
48169
+ );
48170
+ 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(
48172
+ act(rt, async (ctx, o, [ref]) => {
48173
+ const id2 = await sourceId(ctx, ref);
48174
+ await ctx.confirm(`apply the pending migrations of ${ref}${o.ref ? ` at ${o.ref}` : ""} to its database`);
48175
+ const r = await ctx.client("user").dbMigrations.startRun(id2, o.ref ? { ref: o.ref } : {});
48176
+ ctx.result(r, (v) => v.run ? renderRun(v.run) : `nothing to apply
48177
+
48178
+ ${renderPlan2(v.plan)}`);
48179
+ })
48180
+ );
48181
+ 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)"]
48186
+ ];
48187
+ 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)));
48189
+ }
48190
+ }
48191
+
47742
48192
  // packages/cli/src/commands/deploy.ts
47743
48193
  var TERMINAL4 = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "superseded", "blocked"]);
47744
48194
  var NO_DO_PREVIEWS = "previews off: Cloudflare makes no version URLs for Workers that implement a Durable Object (--previews --preview-mode worker_previews uses Worker Previews; it needs a `previews` block in the Wrangler configuration)";
@@ -47746,7 +48196,7 @@ var renderTargets = (v) => table(
47746
48196
  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)}` : "-" })),
47747
48197
  ["name", "id", "platform", "resource", "env", "branch", "runs_on", "previews", "state", "last_production"]
47748
48198
  );
47749
- var renderTarget = (t) => kv({ ...t, connection: t.connection ? `${t.connection.name} (${t.connection.id}, ${t.connection.state})` : "-", last_production: t.last_production ? `${t.last_production.id} ${t.last_production.status}` : "-" }, [
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}` : "-" }, [
47750
48200
  "id",
47751
48201
  "name",
47752
48202
  "platform",
@@ -47761,6 +48211,7 @@ var renderTarget = (t) => kv({ ...t, connection: t.connection ? `${t.connection.
47761
48211
  "build_command",
47762
48212
  "output_directory",
47763
48213
  "runs_on",
48214
+ "watch_paths",
47764
48215
  "previews",
47765
48216
  "preview_check_name",
47766
48217
  "last_production",
@@ -47797,7 +48248,7 @@ async function stepsOf(ctx, spec) {
47797
48248
  return v;
47798
48249
  }
47799
48250
  function targetOptions(cmd) {
47800
- return cmd.option("--platform <platform>", "workers | pages").option("--connection <connection>", "deploy connection (icn_\u2026)").option("--account-id <id>", "Cloudflare account id").option("--env <name>", "Wrangler environment (deploys with --env)").option("--worker <name>", "Worker name (or Pages project name)").option("--root <dir>", "root directory (where the Wrangler config lives)").option("--branch <name>", "production branch (default: the repository's default branch)").option("--install <cmd>", "install command").option("--build <cmd>", "build command").option("--output-dir <dir>", "Pages: the built site").option("--previews", "upload a preview of every open change").option("--no-previews", "no previews").option("--preview-agent-changes", "also preview changes made by agents or integrations, isolated (Worker Previews only)").option("--no-preview-agent-changes", "previews only for changes people upload").option("--preview-mode <mode>", "versions (preview version with an alias) | worker_previews (`wrangler preview`: Durable Objects allowed, bindings from the `previews` block)").option("--preview-check <name>", "report previews as this check").option("--runs-on <label>", "hosted runner size for the deploy jobs: brnch-hosted (default), brnch-hosted-1 \u2026 brnch-hosted-4 (your plan sets the largest)").option("--name <name>", "target name").option("--steps <json>", `pre/post steps as a JSON array, @file or - (replaces the list), e.g. '[{"id":"migrate","kind":"d1_migrations","phase":"pre"}]'`);
48251
+ return cmd.option("--platform <platform>", "workers | pages").option("--connection <connection>", "deploy connection (icn_\u2026)").option("--account-id <id>", "Cloudflare account id").option("--env <name>", "Wrangler environment (deploys with --env)").option("--worker <name>", "Worker name (or Pages project name)").option("--root <dir>", "root directory (where the Wrangler config lives)").option("--branch <name>", "production branch (default: the repository's default branch)").option("--install <cmd>", "install command").option("--build <cmd>", "build command").option("--output-dir <dir>", "Pages: the built site").option("--previews", "upload a preview of every open change").option("--no-previews", "no previews").option("--preview-agent-changes", "also preview changes made by agents or integrations, isolated (Worker Previews only)").option("--no-preview-agent-changes", "previews only for changes people upload").option("--preview-mode <mode>", "versions (preview version with an alias) | worker_previews (`wrangler preview`: Durable Objects allowed, bindings from the `previews` block)").option("--preview-check <name>", "report previews as this check").option("--runs-on <label>", "hosted runner size for the deploy jobs: brnch-hosted (default), brnch-hosted-1 \u2026 brnch-hosted-4 (your plan sets the largest)").option("--watch-path <pattern>", "deploy only when these paths change (repeatable; replaces the list): CODEOWNERS patterns like apps/web/ or packages/**, ! excludes", (v, prev = []) => [...prev, v]).option("--watch-all", "deploy on every change (clears the watch paths)").option("--name <name>", "target name").option("--steps <json>", `pre/post steps as a JSON array, @file or - (replaces the list), e.g. '[{"id":"migrate","kind":"d1_migrations","phase":"pre"}]'`);
47801
48252
  }
47802
48253
  async function planSetup(ctx, repo, o) {
47803
48254
  const c8 = ctx.client("user").deploy;
@@ -47872,6 +48323,7 @@ async function planSetup(ctx, repo, o) {
47872
48323
  ...o.previewAgentChanges !== void 0 ? { preview_agent_changes: o.previewAgentChanges } : {},
47873
48324
  preview_check_name: o.previewCheck ?? null,
47874
48325
  ...o.runsOn ? { runs_on: o.runsOn } : {},
48326
+ ...o.watchPath ? { watch_paths: o.watchPath } : {},
47875
48327
  ...o.branch ? { branch: o.branch } : {},
47876
48328
  ...o.steps !== void 0 ? { steps: await stepsOf(ctx, o.steps) } : {}
47877
48329
  };
@@ -47883,6 +48335,7 @@ async function waitFor(ctx, d, interval, timeout) {
47883
48335
  for (; ; ) {
47884
48336
  if (d.status !== last) ctx.info(`${d.id}: ${d.status}${d.status_reason ? ` (${d.status_reason})` : ""}`);
47885
48337
  last = d.status;
48338
+ ctx.status.working(`Deploy ${d.status}`);
47886
48339
  if (TERMINAL4.has(d.status) || Date.now() > deadline) return d;
47887
48340
  await ctx.deps.sleep(Math.max(1, interval) * 1e3);
47888
48341
  d = await ctx.client("user").deploy.deployments.get(d.id);
@@ -47907,6 +48360,7 @@ Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}
47907
48360
  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(
47908
48361
  act(rt, async (ctx, o, [ref]) => {
47909
48362
  if (o.disable && o.enable) throw usage("give either --disable or --enable");
48363
+ if (o.watchAll && o.watchPath) throw usage("give either --watch-path or --watch-all");
47910
48364
  const c8 = ctx.client("user").deploy.targets;
47911
48365
  const cur = await c8.get(await targetId(ctx, ref));
47912
48366
  const patch = {
@@ -47926,6 +48380,7 @@ Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}
47926
48380
  preview_agent_changes: o.previewAgentChanges,
47927
48381
  preview_check_name: orNull(o.previewCheck),
47928
48382
  runs_on: o.runsOn,
48383
+ watch_paths: o.watchAll ? [] : o.watchPath,
47929
48384
  state: o.disable ? "disabled" : o.enable ? "active" : void 0,
47930
48385
  steps: await stepsOf(ctx, o.steps)
47931
48386
  };
@@ -47972,7 +48427,7 @@ Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}
47972
48427
  var ITERATION_DONE = /* @__PURE__ */ new Set(["succeeded", "no_changes", "failed", "cancelled"]);
47973
48428
  var STATES = ["working", "waiting", "stopped"];
47974
48429
  var who2 = (a) => a.display_name ?? `${a.type} ${a.id}`;
47975
- var short = (oid) => oid ? oid.slice(0, 12) : null;
48430
+ var short2 = (oid) => oid ? oid.slice(0, 12) : null;
47976
48431
  function vendorArg(v) {
47977
48432
  if (!CODING_AGENT_VENDORS.includes(v)) throw usage(`unknown agent "${v}"; one of ${CODING_AGENT_VENDORS.join(", ")}`);
47978
48433
  return v;
@@ -47992,7 +48447,7 @@ function renderIterations(d) {
47992
48447
  ...i,
47993
48448
  trigger: i.trigger_ref ? `${i.trigger} (${i.trigger_ref})` : i.trigger,
47994
48449
  by: who2(i.trigger_actor),
47995
- head: short(i.head_oid)
48450
+ head: short2(i.head_oid)
47996
48451
  }));
47997
48452
  const claims = d.iterations.map(reported).filter((x) => !!x);
47998
48453
  const errors = d.iterations.filter((i) => i.error).map((i) => ` #${i.number}: ${i.error}`);
@@ -48055,6 +48510,7 @@ async function waitForIteration(ctx, start, o) {
48055
48510
  const line2 = `${delegationStatusLabel(d)}${it ? ` (iteration #${it.number}: ${it.state})` : ""}`;
48056
48511
  if (line2 !== last) ctx.info(line2);
48057
48512
  last = line2;
48513
+ ctx.status.working(`${d.agent.name}: ${line2}`);
48058
48514
  if (it ? ITERATION_DONE.has(it.state) : d.state !== "working") return { d, timedOut: false };
48059
48515
  if (Date.now() > deadline) return { d, timedOut: true };
48060
48516
  await ctx.deps.sleep(Math.max(1, o.interval) * 1e3);
@@ -49006,15 +49462,16 @@ function registerImage(program2, rt) {
49006
49462
  ctx.result(page, (p) => listWithCursor(p, ["reference", "tags", "versions", "updated_at"]));
49007
49463
  })
49008
49464
  );
49009
- image.command("delete <ref>").description("Delete a tag (`<image>:<tag>`) or a version and all its tags (`<image>@sha256:\u2026`); untagged versions are also collected after the retention window").action(
49465
+ image.command("delete <ref>").description("Delete a tag (`<image>:<tag>`), a version and all its tags (`<image>@sha256:\u2026`), or an image with no versions left (`<image>`); untagged versions are also collected after the retention window").action(
49010
49466
  act(rt, async (ctx, _o, [spec]) => {
49011
49467
  const ref = parseImageRef(spec, await registryHost(ctx));
49012
- if (!ref || !ref.tag && !ref.digest) throw usage(`invalid image reference "${spec}": expected <namespace>/<repository>[/<image>]:<tag> or \u2026@sha256:<digest>`);
49468
+ if (!ref) throw usage(`invalid image reference "${spec}": expected <namespace>/<repository>[/<image>][:<tag>|@sha256:<digest>]`);
49013
49469
  const img = await findImage(ctx, ref.name);
49014
49470
  if (ref.digest) await ctx.client().packages.deleteVersion(img.id, ref.digest);
49015
- else await ctx.client().packages.deleteTag(img.id, ref.tag);
49016
- const what = ref.digest ? `version ${ref.digest}` : `tag ${ref.tag}`;
49017
- ctx.result({ image: img.full_name, deleted: ref.digest ? { digest: ref.digest } : { tag: ref.tag } }, `Deleted ${what} of ${img.full_name}.`);
49471
+ else if (ref.tag) await ctx.client().packages.deleteTag(img.id, ref.tag);
49472
+ else await ctx.client().packages.delete(img.id);
49473
+ const what = ref.digest ? `version ${ref.digest} of ${img.full_name}` : ref.tag ? `tag ${ref.tag} of ${img.full_name}` : `image ${img.full_name}`;
49474
+ ctx.result({ image: img.full_name, deleted: ref.digest ? { digest: ref.digest } : ref.tag ? { tag: ref.tag } : { image: img.id } }, `Deleted ${what}.`);
49018
49475
  })
49019
49476
  );
49020
49477
  }
@@ -49260,6 +49717,7 @@ async function waitForPromotion(ctx, p, timeout, interval) {
49260
49717
  let last = p.state;
49261
49718
  ctx.info(`promotion ${p.id}: ${p.state}`);
49262
49719
  while (!TERMINAL5.has(p.state) && Date.now() < deadline) {
49720
+ ctx.status.working(`Merge ${p.state}`);
49263
49721
  await ctx.deps.sleep(Math.max(1, interval) * 1e3);
49264
49722
  p = await ctx.client().promotions.get(p.id);
49265
49723
  if (p.state !== last) ctx.info(`promotion ${p.id}: ${last = p.state}`);
@@ -51633,7 +52091,7 @@ function safeLegs(m) {
51633
52091
  }
51634
52092
 
51635
52093
  // packages/cli/src/commands/actions.ts
51636
- var renderRun = (r) => kv({ ...r, actor: r.actor.display_name ?? r.actor.id, trust: `${r.definition.source} ${r.definition.ref}${r.definition.trusted ? " (trusted)" : " (contributor-controlled)"}` }, [
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)"}` }, [
51637
52095
  "id",
51638
52096
  "workflow_name",
51639
52097
  "run_number",
@@ -51698,7 +52156,7 @@ function registerWorkflow(program2, rt) {
51698
52156
  inputs[kvp.slice(0, i)] = kvp.slice(i + 1);
51699
52157
  }
51700
52158
  const r = await ctx.client("user").actions.workflows.dispatch(await ctx.repoId(void 0, "user"), file2, { ...o.ref ? { ref: o.ref } : {}, inputs });
51701
- ctx.result(r, renderRun);
52159
+ ctx.result(r, renderRun2);
51702
52160
  })
51703
52161
  );
51704
52162
  }
@@ -51722,6 +52180,8 @@ async function watchRun(ctx, runId, interval, timeout, logs) {
51722
52180
  ${text2.trimEnd()}`);
51723
52181
  }
51724
52182
  }
52183
+ const finished = r.jobs.filter((j) => j.status === "completed").length;
52184
+ ctx.status.working(`Run ${r.status}: ${finished}/${r.jobs.length} jobs done`, r.jobs.length ? finished / r.jobs.length * 100 : void 0);
51725
52185
  if (TERMINAL6.has(r.status) || Date.now() > deadline) return r;
51726
52186
  await ctx.deps.sleep(Math.max(1, interval) * 1e3);
51727
52187
  }
@@ -51741,13 +52201,13 @@ function registerRun(program2, rt) {
51741
52201
  const r = await c8.actions.runs.get(id2);
51742
52202
  if (o.log) for (const j of r.jobs) ctx.info(`\u2500\u2500 ${j.name} (${j.status}${j.conclusion ? `/${j.conclusion}` : ""}) \u2500\u2500
51743
52203
  ${(await c8.actions.jobs.logs(j.id)).trimEnd()}`);
51744
- ctx.result(r, renderRun);
52204
+ ctx.result(r, renderRun2);
51745
52205
  })
51746
52206
  );
51747
52207
  run3.command("watch <run>").description("Follow a run until it completes. Exit 0 when it succeeded, 1 when it failed or was cancelled, 7 on timeout").option("--interval <seconds>", "poll interval", (v) => parseIntOption(v, "--interval"), 2).option("--timeout <seconds>", "give up after", (v) => parseIntOption(v, "--timeout"), 3600).option("--log", "print each job's log when it completes").action(
51748
52208
  act(rt, async (ctx, o, [id2]) => {
51749
52209
  const r = await watchRun(ctx, id2, o.interval, o.timeout, !!o.log);
51750
- ctx.result(r, renderRun);
52210
+ ctx.result(r, renderRun2);
51751
52211
  ctx.setExit(runExit(r));
51752
52212
  })
51753
52213
  );
@@ -51772,8 +52232,8 @@ ${(await c8.actions.jobs.logs(j.id)).trimEnd()}`);
51772
52232
  ctx.result({ job_id: job2, conclusion: status }, `job ${job2}: ${status}`);
51773
52233
  })
51774
52234
  );
51775
- run3.command("cancel <run>").description("Cancel a run (running jobs are stopped on their runners)").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client("user").actions.runs.cancel(id2), renderRun)));
51776
- run3.command("rerun <run>").description("Re-run a completed run (all jobs, or --failed and their dependents)").option("--failed", "only failed/cancelled jobs and jobs that depend on them").action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client("user").actions.runs.rerun(id2, { failed_only: !!o.failed }), renderRun)));
52235
+ run3.command("cancel <run>").description("Cancel a run (running jobs are stopped on their runners)").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client("user").actions.runs.cancel(id2), renderRun2)));
52236
+ run3.command("rerun <run>").description("Re-run a completed run (all jobs, or --failed and their dependents)").option("--failed", "only failed/cancelled jobs and jobs that depend on them").action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client("user").actions.runs.rerun(id2, { failed_only: !!o.failed }), renderRun2)));
51777
52237
  }
51778
52238
  function registerSecret(program2, rt) {
51779
52239
  const secret = program2.command("secret").description("Actions secrets of --repo (default) or the organization (--org-level). Values are write-only");
@@ -52354,6 +52814,10 @@ ${res.stdout.slice(-8e3)}`;
52354
52814
  // packages/cli/src/runner/build-image-step.ts
52355
52815
  var BUILDKIT_IMAGE = "moby/buildkit:v0.34.0@sha256:b059f8d7226d0b326bc871af489a5dddf65e36af4d20d14251b072974d9ee87c";
52356
52816
  var COSIGN_IMAGE = "ghcr.io/sigstore/cosign/cosign:v3.1.3@sha256:9e5c2f2edc34351160407ca3416c61855bdf9403c3c5936e0f0be7fc261611b8";
52817
+ var BUILDKIT_RELEASE = { url: "https://github.com/moby/buildkit/releases/download/v0.34.0/buildkit-v0.34.0.linux-amd64.tar.gz", sha256: "14813b1fb2a40082114e6d386c6fc5e7dd5298176ce7ee1a18bc33e73e109ac3" };
52818
+ var COSIGN_RELEASE = { url: "https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign-linux-amd64", sha256: "4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71" };
52819
+ var HOSTED_DIRS = { tools: "/opt/brnch/tools", state: "/var/lib/brnch-buildkit", run: "/run/brnch-buildkit", client: "/run/brnch-buildkit-client", context: "/run/brnch-build-context" };
52820
+ var BUILD_UID = 10999;
52357
52821
  var MAX_METADATA_OUTPUT = 32 * 1024;
52358
52822
  var list2 = (v) => (v ?? "").split(/[\n,]/).map((s) => s.trim()).filter(Boolean);
52359
52823
  var lines = (v) => (v ?? "").split("\n").map((s) => s.trim()).filter(Boolean);
@@ -52406,15 +52870,15 @@ function inside2(p) {
52406
52870
  }
52407
52871
  return parts.join("/");
52408
52872
  }
52409
- function buildctlArgs(i, metadataFile) {
52410
- const ctx = inside2(i.context);
52873
+ function buildLayout(i) {
52874
+ const context = inside2(i.context);
52411
52875
  const file2 = inside2(i.file ?? `${i.context}/Dockerfile`);
52412
- if (ctx === null) return `context: ${i.context} is outside the workspace`;
52413
- if (file2 === null) return `file: ${i.file} is outside the workspace`;
52414
- const slash = file2.lastIndexOf("/");
52415
- const dir = slash >= 0 ? file2.slice(0, slash) : "";
52416
- const base2 = slash >= 0 ? file2.slice(slash + 1) : file2;
52417
- const args = ["build", "--frontend", "dockerfile.v0", "--local", `context=/workspace/${ctx}`.replace(/\/$/, ""), "--local", `dockerfile=/workspace/${dir}`.replace(/\/$/, ""), "--opt", `filename=${base2}`];
52876
+ if (context === null) return `context: ${i.context} is outside the workspace`;
52877
+ if (file2 === null || file2 === "") return `file: ${i.file} is outside the workspace`;
52878
+ return { context, file: file2, filename: file2.slice(file2.lastIndexOf("/") + 1) };
52879
+ }
52880
+ function buildctlArgs(i, layout, metadataFile, root = "/workspace") {
52881
+ const args = ["build", "--frontend", "dockerfile.v0", "--local", `context=${root}/context`, "--local", `dockerfile=${root}/dockerfile`, "--opt", `filename=${layout.filename}`];
52418
52882
  args.push("--opt", `platform=${i.platforms.join(",")}`);
52419
52883
  if (i.target) args.push("--opt", `target=${i.target}`);
52420
52884
  for (const [k, v] of Object.entries(i.buildArgs)) args.push("--opt", `build-arg:${k}=${v}`);
@@ -52427,6 +52891,215 @@ function buildctlArgs(i, metadataFile) {
52427
52891
  args.push("--metadata-file", metadataFile);
52428
52892
  return args;
52429
52893
  }
52894
+ var quote2 = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
52895
+ async function copyContext(d, layout, owner) {
52896
+ const fresh2 = async (hosted) => {
52897
+ if (d.hosted) return await d.run(["sh", "-c", `rm -rf ${hosted} && install -d -m 0700 ${hosted}`], { env: d.env, timeoutMs: 6e4 }) === 0 ? hosted : null;
52898
+ const made = await d.capture(["mktemp", "-d", "/tmp/brnch-build-context.XXXXXXXX"], { env: d.env });
52899
+ const dir2 = made.stdout.trim();
52900
+ return made.code === 0 && dir2.startsWith("/tmp/brnch-build-context.") ? dir2 : null;
52901
+ };
52902
+ const staging = await fresh2(`${HOSTED_DIRS.context}-staging`);
52903
+ const dir = staging && await fresh2(HOSTED_DIRS.context);
52904
+ if (!staging || !dir) {
52905
+ if (staging) await d.run(["rm", "-rf", staging], { env: d.env, timeoutMs: 6e4 });
52906
+ return { error: "could not prepare the build context" };
52907
+ }
52908
+ const asStep = d.stepUser ? ["setpriv", `--reuid=${d.stepUser.uid}`, `--regid=${d.stepUser.gid}`, "--clear-groups", "--"] : [];
52909
+ const asOwner = owner !== null && owner !== 0 ? ["setpriv", `--reuid=${owner}`, `--regid=${owner}`, "--clear-groups", "--"] : [];
52910
+ const ws = (rel) => rel ? `${d.workspace}/${rel}` : d.workspace;
52911
+ const ignore = `${layout.filename}.dockerignore`;
52912
+ const stage = [
52913
+ "set -e",
52914
+ `w=$(cd -- ${quote2(d.workspace)} && pwd -P)`,
52915
+ 'inside() { case "$1" in "$w"/*) ;; *) echo "brnch/build-image@v1: $2 is outside the workspace" >&2; exit 3;; esac; }',
52916
+ `c=$(cd -- ${quote2(ws(layout.context))} && pwd -P)`,
52917
+ 'case "$c" in "$w"|"$w"/*) ;; *) echo "brnch/build-image@v1: the context is outside the workspace" >&2; exit 3;; esac',
52918
+ `fd=$(cd -- "$(dirname -- ${quote2(ws(layout.file))})" && pwd -P)`,
52919
+ `r=$(realpath -- "$fd/$(basename -- ${quote2(ws(layout.file))})")`,
52920
+ `inside "$r" ${quote2(layout.file)}`,
52921
+ `cp -a -- "$c/." ${quote2(`${staging}/context`)}`,
52922
+ `mkdir -- ${quote2(`${staging}/dockerfile`)}`,
52923
+ `cp -- "$r" ${quote2(`${staging}/dockerfile/${layout.filename}`)}`,
52924
+ // The name is the step's input: quoted into a variable, never into shell source (review of #287).
52925
+ `ig=${quote2(ignore)}`,
52926
+ 'if [ -e "$fd/$ig" ]; then i=$(realpath -- "$fd/$ig"); inside "$i" "$ig"; cp -- "$i" ' + quote2(`${staging}/dockerfile/${ignore}`) + "; fi",
52927
+ // Readable by the builder user for stage 2; the step's own files, nothing else.
52928
+ `chmod -R a+rX ${quote2(staging)}`
52929
+ ].join("; ");
52930
+ const fail = async () => {
52931
+ await d.run(["rm", "-rf", staging, dir], { env: d.env, timeoutMs: 6e4 });
52932
+ return { error: `could not copy the build context (${layout.context || "."}) and ${layout.file}: they must be a directory and a file in the workspace that the job can read` };
52933
+ };
52934
+ for (const cmd of [
52935
+ ...d.stepUser ? [["chown", `${d.stepUser.uid}:${d.stepUser.gid}`, staging], ["chmod", "0755", staging]] : [],
52936
+ [...asStep, "sh", "-c", stage],
52937
+ ...owner === null ? [] : [["chown", `${owner}:${owner}`, dir]],
52938
+ // No trailing "/.": a top directory swapped for a link is copied as that link, and refused below.
52939
+ [...asOwner, "cp", "-a", "--", `${staging}/context`, `${staging}/dockerfile`, `${dir}/`],
52940
+ ["sh", "-c", 'for p in "$1/context" "$1/dockerfile"; do [ -d "$p" ] && [ ! -L "$p" ] || exit 1; done', "sh", dir],
52941
+ ["rm", "-rf", staging]
52942
+ ]) {
52943
+ const done2 = await d.run(cmd, { env: d.env, timeoutMs: 10 * 6e4 });
52944
+ if (done2 === null) return null;
52945
+ if (done2 !== 0) return fail();
52946
+ }
52947
+ return dir;
52948
+ }
52949
+ function buildScopes(i, host, repository = "", dockerfile = "") {
52950
+ const name = (ref) => ref.slice(host.length + 1).replace(/@.*$/, "").replace(/:[^/]*$/, "");
52951
+ const want = /* @__PURE__ */ new Map();
52952
+ const add = (ref, actions) => {
52953
+ const n = name(ref);
52954
+ want.set(n, /* @__PURE__ */ new Set([...want.get(n) ?? [], ...actions]));
52955
+ };
52956
+ if (i.push) for (const t of i.tags) add(t, ["pull", "push"]);
52957
+ for (const c8 of i.cacheTo) add(/(?:^|,)ref=([^,]+)/.exec(c8)[1], ["pull", "push"]);
52958
+ for (const c8 of i.cacheFrom) add(/(?:^|,)ref=([^,]+)/.exec(c8)[1], ["pull"]);
52959
+ if (repository && dockerfile) {
52960
+ for (const ref of dockerfileImages(dockerfile)) if (ownImage(ref, host, repository)) add(ref, ["pull"]);
52961
+ }
52962
+ return [...want].map(([n, a]) => `repository:${n}:${[...a].sort().join(",")}`);
52963
+ }
52964
+ function dockerfileImages(dockerfile) {
52965
+ const out = [];
52966
+ const all = dockerfile.replace(/\r\n/g, "\n").split("\n");
52967
+ let escape = "\\";
52968
+ for (const l of all) {
52969
+ const d = /^\s*#\s*(syntax|escape|check)\s*=\s*(\S+)\s*$/i.exec(l);
52970
+ if (!d) break;
52971
+ if (d[1].toLowerCase() === "escape" && (d[2] === "`" || d[2] === "\\")) escape = d[2];
52972
+ }
52973
+ const uncommented = all.filter((l) => !l.trim().startsWith("#")).join("\n");
52974
+ const lines2 = uncommented.split(`${escape}
52975
+ `).join(" ").split("\n");
52976
+ const value = String.raw`("[^"]*"|'[^']*'|[^\s,]+)`;
52977
+ const unquote = (v) => /^(["']).*\1$/.test(v) ? v.slice(1, -1) : v;
52978
+ for (const raw of lines2) {
52979
+ const line2 = raw.trim();
52980
+ if (!line2) continue;
52981
+ const from = /^FROM\s+(?:--platform=\S+\s+)?(\S+)/i.exec(line2);
52982
+ if (from) out.push(from[1]);
52983
+ if (/^(COPY|ADD)\s/i.test(line2)) for (const m of line2.matchAll(new RegExp(`--from=${value}`, "g"))) out.push(unquote(m[1]));
52984
+ if (/^RUN\s/i.test(line2)) for (const m of line2.matchAll(new RegExp(String.raw`--mount=\S*?\bfrom=${value}`, "g"))) out.push(unquote(m[1]));
52985
+ }
52986
+ return out;
52987
+ }
52988
+ var OCI_PATH = /^[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*(?:\/[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*)*$/;
52989
+ function ownImage(ref, host, repository) {
52990
+ const m = /^([^/]+)\/([^:@]+)(?::[\w][\w.-]{0,127})?(?:@sha256:[a-f0-9]{64})?$/.exec(ref);
52991
+ if (!m || m[1] !== host || !OCI_PATH.test(m[2])) return false;
52992
+ const own2 = repository.toLowerCase();
52993
+ return m[2] === own2 || m[2].startsWith(`${own2}/`);
52994
+ }
52995
+ var BUILD_TOKEN_SECONDS = 21600;
52996
+ async function mintBuildToken(d, host, scopes) {
52997
+ const url2 = new URL(`${new URL(d.baseUrl).protocol}//${host}/v2/token`);
52998
+ url2.searchParams.set("service", host);
52999
+ for (const s of scopes) url2.searchParams.append("scope", s);
53000
+ url2.searchParams.set("expires_in", String(BUILD_TOKEN_SECONDS));
53001
+ try {
53002
+ const res = await d.fetch(url2.toString(), { headers: { authorization: `Bearer ${d.jobToken}` } });
53003
+ const body = await res.json().catch(() => null);
53004
+ if (res.ok && typeof body?.token === "string" && body.token) return body.token;
53005
+ return { error: `the registry did not issue a token for this build (HTTP ${res.status})` };
53006
+ } catch (e) {
53007
+ return { error: `could not reach the registry for a token: ${e.message}` };
53008
+ }
53009
+ }
53010
+ function dockerBuilder(d, auth, context) {
53011
+ const name = `brnch-buildkit-${d.jobId.replace(/[^a-z0-9]/gi, "").toLowerCase()}`;
53012
+ const docker = (a, timeoutMs = 10 * 6e4, env = d.env) => d.run(["docker", ...a], { env, timeoutMs });
53013
+ return {
53014
+ metadataFile: "/tmp/brnch-metadata.json",
53015
+ contextRoot: "/workspace",
53016
+ async start() {
53017
+ d.log(`##[group]Start BuildKit (${BUILDKIT_IMAGE.split("@")[0]})`);
53018
+ try {
53019
+ const started = await docker(["run", "-d", "--rm", "--privileged", "--name", name, "-v", `${context}:/workspace:ro`, "-v", `${auth}:/root/.docker:ro`, BUILDKIT_IMAGE]);
53020
+ if (started === null) return null;
53021
+ if (started !== 0) return "could not start the BuildKit container";
53022
+ return await waitReady(() => d.capture(["docker", "exec", name, "buildctl", "debug", "workers"], { env: d.env })) ? "" : "BuildKit did not become ready within 30 seconds";
53023
+ } finally {
53024
+ d.log("##[endgroup]");
53025
+ }
53026
+ },
53027
+ buildctl: (args) => docker(["exec", name, "buildctl", ...args], null),
53028
+ readMetadata: async () => (await d.capture(["docker", "exec", name, "cat", "/tmp/brnch-metadata.json"], { env: d.env })).stdout,
53029
+ sign: (args, env) => docker(["run", "--rm", "--user", "0", "-v", `${auth}:/root/.docker:ro`, "-e", "DOCKER_CONFIG=/root/.docker", "-e", "COSIGN_KEY", "-e", "COSIGN_PASSWORD", COSIGN_IMAGE, ...args], 5 * 6e4, { ...d.env, ...env }),
53030
+ stop: async () => {
53031
+ await d.capture(["docker", "rm", "-f", name], { env: d.env });
53032
+ }
53033
+ };
53034
+ }
53035
+ function hostedBuilder(d, auth, signing, context) {
53036
+ const tools = `${HOSTED_DIRS.tools}/build-image`;
53037
+ const bin = `${tools}/buildkit/bin`;
53038
+ const sock = `unix://${HOSTED_DIRS.client}/buildkitd.sock`;
53039
+ const asBuilder = (cmd) => ["setpriv", `--reuid=${BUILD_UID}`, `--regid=${BUILD_UID}`, "--clear-groups", "--", ...cmd];
53040
+ const buildctl = `${HOSTED_DIRS.client}/buildctl`;
53041
+ const clientEnv = { ...d.env, DOCKER_CONFIG: auth, HOME: HOSTED_DIRS.client };
53042
+ const run3 = (cmd, timeoutMs = 10 * 6e4, env = d.env) => d.run(cmd, { env, timeoutMs });
53043
+ const fetchChecked = async (url2, sha2562, out) => {
53044
+ const got = await run3(["curl", "-fsSL", "--retry", "3", "-o", out, url2], 5 * 6e4);
53045
+ if (got === null) return null;
53046
+ if (got !== 0) return `could not download ${url2}`;
53047
+ const sum = (await d.capture(["sha256sum", out], { env: d.env })).stdout.split(/\s/)[0];
53048
+ return sum === sha2562 ? "" : `${url2} has sha256 ${sum || "(unreadable)"}, not the pinned ${sha2562}`;
53049
+ };
53050
+ return {
53051
+ metadataFile: `${HOSTED_DIRS.client}/metadata.json`,
53052
+ contextRoot: context,
53053
+ async start() {
53054
+ d.log(`##[group]Start BuildKit (${BUILDKIT_RELEASE.url.split("/").at(-1)}, beside the job)`);
53055
+ try {
53056
+ for (const dir of [HOSTED_DIRS.tools, tools, `${tools}/buildkit`, HOSTED_DIRS.state, HOSTED_DIRS.run]) {
53057
+ const made = await run3(["install", "-d", "-m", "0700", dir]);
53058
+ if (made !== 0) return made === null ? null : `could not create ${dir}`;
53059
+ }
53060
+ for (const cmd of [["install", "-d", "-m", "0700", "-o", String(BUILD_UID), "-g", String(BUILD_UID), HOSTED_DIRS.client], ["chown", "-R", `${BUILD_UID}:${BUILD_UID}`, auth]]) {
53061
+ const done2 = await run3(cmd);
53062
+ if (done2 !== 0) return done2 === null ? null : `could not prepare the build user (${cmd[0]})`;
53063
+ }
53064
+ const tgz = `${tools}/buildkit.tar.gz`;
53065
+ const got = await fetchChecked(BUILDKIT_RELEASE.url, BUILDKIT_RELEASE.sha256, tgz);
53066
+ if (got !== "") return got;
53067
+ const unpacked = await run3(["tar", "-xzf", tgz, "-C", `${tools}/buildkit`]);
53068
+ if (unpacked !== 0) return unpacked === null ? null : "could not unpack BuildKit";
53069
+ const client = await run3(["install", "-m", "0755", `${bin}/buildctl`, buildctl]);
53070
+ if (client !== 0) return client === null ? null : "could not install buildctl for the build user";
53071
+ if (signing) {
53072
+ const cosign = await fetchChecked(COSIGN_RELEASE.url, COSIGN_RELEASE.sha256, `${tools}/cosign`);
53073
+ if (cosign !== "") return cosign;
53074
+ if (await run3(["chmod", "0700", `${tools}/cosign`]) !== 0) return "could not install cosign";
53075
+ }
53076
+ const daemon = `${bin}/buildkitd --root ${HOSTED_DIRS.state} --addr ${sock} --group ${BUILD_UID} --oci-worker-snapshotter=overlayfs --containerd-worker=false`;
53077
+ const started = await run3(["sh", "-c", `PATH=${bin}:$PATH nohup ${daemon} > ${HOSTED_DIRS.run}/buildkitd.log 2>&1 & echo $! > ${HOSTED_DIRS.run}/buildkitd.pid`]);
53078
+ if (started !== 0) return started === null ? null : "could not start buildkitd";
53079
+ if (await waitReady(() => d.capture(asBuilder([buildctl, "--addr", sock, "debug", "workers"]), { env: clientEnv }))) return "";
53080
+ d.log((await d.capture(["tail", "-20", `${HOSTED_DIRS.run}/buildkitd.log`], { env: d.env })).stdout);
53081
+ return "BuildKit did not become ready within 30 seconds";
53082
+ } finally {
53083
+ d.log("##[endgroup]");
53084
+ }
53085
+ },
53086
+ // buildctl reads registry credentials from DOCKER_CONFIG on the client side and hands them to buildkitd's session.
53087
+ buildctl: (args) => run3(asBuilder([buildctl, "--addr", sock, ...args]), null, clientEnv),
53088
+ readMetadata: async () => (await d.capture(["cat", `${HOSTED_DIRS.client}/metadata.json`], { env: d.env })).stdout,
53089
+ sign: (args, env) => run3([`${tools}/cosign`, ...args], 5 * 6e4, { ...d.env, ...env, DOCKER_CONFIG: auth, HOME: auth }),
53090
+ stop: async () => {
53091
+ const pid = `${HOSTED_DIRS.run}/buildkitd.pid`;
53092
+ await d.capture(["sh", "-c", `p=$(cat ${pid} 2>/dev/null); if [ -n "$p" ]; then kill $p 2>/dev/null; for i in 1 2 3 4 5 6 7 8 9 10; do kill -0 $p 2>/dev/null || break; sleep 1; done; kill -9 $p 2>/dev/null; fi; rm -rf ${HOSTED_DIRS.client} ${pid}`], { env: d.env });
53093
+ }
53094
+ };
53095
+ }
53096
+ async function waitReady(probe) {
53097
+ for (let i = 0; i < 30; i++) {
53098
+ if ((await probe()).code === 0) return true;
53099
+ await new Promise((r) => setTimeout(r, 1e3));
53100
+ }
53101
+ return false;
53102
+ }
52430
53103
  async function runBuildImage(w, d) {
52431
53104
  const fail = (msg) => {
52432
53105
  d.log(`##[error]brnch/build-image@v1: ${msg}`);
@@ -52438,41 +53111,46 @@ async function runBuildImage(w, d) {
52438
53111
  const bad = checkRefs(parsed, host, d.repository);
52439
53112
  if (bad) return fail(bad);
52440
53113
  if (parsed.push && !parsed.tags.length) return fail("tags are required when pushing (or set push: false)");
52441
- if (d.hosted) {
52442
- return fail("hosted runners cannot build images yet: BuildKit inside Cloudflare Containers is waiting on a capability probe (ADR-0083 \xA76). Run this job on a self-hosted runner with Docker (runs-on: [self-hosted, \u2026]).");
52443
- }
52444
- if ((await d.capture(["docker", "info", "--format", "{{.ServerVersion}}"], { env: d.env })).code !== 0) {
53114
+ if (d.hosted && parsed.platforms.some((p) => p !== "linux/amd64")) return fail("hosted runners are x86_64 and build linux/amd64 only; build linux/arm64 on a self-hosted runner that can");
53115
+ if (!d.hosted && (await d.capture(["docker", "info", "--format", "{{.ServerVersion}}"], { env: d.env })).code !== 0) {
52445
53116
  return fail("this runner has no usable Docker (docker info failed); install Docker or run the job on a runner that has it");
52446
53117
  }
52447
- const args = buildctlArgs(parsed, "/tmp/brnch-metadata.json");
52448
- if (typeof args === "string") return fail(args);
52449
- const auth = `${d.temp}/build-image-${d.jobId}`;
52450
- d.writePrivate(`${auth}/config.json`, JSON.stringify({ auths: { [host]: { auth: btoa(`brnch:${d.jobToken}`) } } }));
52451
- const name = `brnch-buildkit-${d.jobId.replace(/[^a-z0-9]/gi, "").toLowerCase()}`;
52452
- const docker = (a, timeoutMs = 10 * 6e4, env = d.env) => d.run(["docker", ...a], { env, timeoutMs });
53118
+ const layout = buildLayout(parsed);
53119
+ if (typeof layout === "string") return fail(layout);
53120
+ const context = await copyContext(d, layout, d.hosted ? BUILD_UID : d.stepUser ? 0 : null);
53121
+ if (context === null) return { outcome: "cancelled", outputs: {} };
53122
+ if (typeof context !== "string") return fail(context.error);
53123
+ const auth = d.hosted ? `${HOSTED_DIRS.client}/docker` : `${d.temp}/build-image-${d.jobId}`;
53124
+ const builder = d.hosted ? hostedBuilder(d, auth, !!parsed.cosignKey && parsed.push, context) : dockerBuilder(d, auth, context);
53125
+ const args = buildctlArgs(parsed, layout, builder.metadataFile, builder.contextRoot);
53126
+ const dockerfile = (await d.capture(["cat", "--", `${context}/dockerfile/${layout.filename}`], { env: d.env })).stdout;
53127
+ const scopes = buildScopes(parsed, host, d.repository, dockerfile);
53128
+ let credential = null;
53129
+ if (scopes.length) {
53130
+ const minted = await mintBuildToken(d, host, scopes);
53131
+ if (typeof minted !== "string") {
53132
+ await d.run(["rm", "-rf", context], { env: d.env, timeoutMs: 6e4 });
53133
+ return fail(minted.error);
53134
+ }
53135
+ credential = minted;
53136
+ }
53137
+ d.writePrivate(`${auth}/config.json`, JSON.stringify({ auths: credential ? { [host]: { auth: btoa(`brnch:${credential}`) } } : {} }));
52453
53138
  try {
52454
- d.log(`##[group]Start BuildKit (${BUILDKIT_IMAGE.split("@")[0]})`);
52455
- const started = await docker(["run", "-d", "--rm", "--privileged", "--name", name, "-v", `${d.workspace}:/workspace:ro`, "-v", `${auth}:/root/.docker:ro`, BUILDKIT_IMAGE]);
52456
- if (started !== 0) return started === null ? { outcome: "cancelled", outputs: {} } : fail("could not start the BuildKit container");
52457
- let ready = false;
52458
- for (let i = 0; i < 30 && !ready; i++) {
52459
- ready = (await d.capture(["docker", "exec", name, "buildctl", "debug", "workers"], { env: d.env })).code === 0;
52460
- if (!ready) await new Promise((r) => setTimeout(r, 1e3));
52461
- }
52462
- d.log("##[endgroup]");
52463
- if (!ready) return fail("BuildKit did not become ready within 30 seconds");
53139
+ const started = await builder.start();
53140
+ if (started === null) return { outcome: "cancelled", outputs: {} };
53141
+ if (started) return fail(started);
52464
53142
  d.log(`##[group]Build ${parsed.tags.join(", ") || "(no tags)"} for ${parsed.platforms.join(", ")}`);
52465
- const built = await docker(["exec", name, "buildctl", ...args], null);
53143
+ const built = await builder.buildctl(args);
52466
53144
  d.log("##[endgroup]");
52467
53145
  if (built === null) return { outcome: "cancelled", outputs: {} };
52468
53146
  if (built !== 0) return fail(`the build failed (buildctl exited ${built})`);
52469
- const meta3 = await d.capture(["docker", "exec", name, "cat", "/tmp/brnch-metadata.json"], { env: d.env });
53147
+ const meta3 = await builder.readMetadata();
52470
53148
  let digest = "";
52471
53149
  try {
52472
- digest = String(JSON.parse(meta3.stdout)["containerimage.digest"] ?? "");
53150
+ digest = String(JSON.parse(meta3)["containerimage.digest"] ?? "");
52473
53151
  } catch {
52474
53152
  }
52475
- const outputs = { metadata: meta3.stdout.slice(0, MAX_METADATA_OUTPUT) };
53153
+ const outputs = { metadata: meta3.slice(0, MAX_METADATA_OUTPUT) };
52476
53154
  if (parsed.push) {
52477
53155
  if (!/^sha256:[a-f0-9]{64}$/.test(digest)) return fail("the build pushed, but BuildKit reported no image digest");
52478
53156
  const first2 = parsed.tags[0].replace(/[:@][^/]*$/, "");
@@ -52481,9 +53159,9 @@ async function runBuildImage(w, d) {
52481
53159
  d.log(`pushed ${outputs.image}`);
52482
53160
  if (parsed.cosignKey) {
52483
53161
  d.log(`##[group]Sign ${outputs.image} with cosign`);
52484
- const signArgs = ["run", "--rm", "--user", "0", "-v", `${auth}:/root/.docker:ro`, "-e", "DOCKER_CONFIG=/root/.docker", "-e", "COSIGN_KEY", "-e", "COSIGN_PASSWORD", COSIGN_IMAGE, "sign", "--key", "env://COSIGN_KEY", "--yes"];
53162
+ const signArgs = ["sign", "--key", "env://COSIGN_KEY", "--yes"];
52485
53163
  if (!parsed.cosignTlog) signArgs.push("--use-signing-config=false", "--tlog-upload=false");
52486
- const signed = await docker([...signArgs, outputs.image], 5 * 6e4, { ...d.env, COSIGN_KEY: parsed.cosignKey, COSIGN_PASSWORD: parsed.cosignPassword ?? "" });
53164
+ const signed = await builder.sign([...signArgs, outputs.image], { COSIGN_KEY: parsed.cosignKey, COSIGN_PASSWORD: parsed.cosignPassword ?? "" });
52487
53165
  d.log("##[endgroup]");
52488
53166
  if (signed === null) return { outcome: "cancelled", outputs };
52489
53167
  if (signed !== 0) return fail(`cosign could not sign ${outputs.image} (exit ${signed})`);
@@ -52494,8 +53172,9 @@ async function runBuildImage(w, d) {
52494
53172
  }
52495
53173
  return { outcome: "success", outputs };
52496
53174
  } finally {
52497
- await d.capture(["docker", "rm", "-f", name], { env: d.env });
53175
+ await builder.stop();
52498
53176
  d.removeDir(auth);
53177
+ await d.run(["rm", "-rf", context], { env: d.env, timeoutMs: 6e4 });
52499
53178
  }
52500
53179
  }
52501
53180
 
@@ -53170,6 +53849,14 @@ async function runCodeReviewStep(spec, host) {
53170
53849
  return ok && parsed.status !== "failed" ? "success" : "failure";
53171
53850
  }
53172
53851
 
53852
+ // packages/cli/src/runner/checkout-retry.ts
53853
+ var CHECKOUT_RETRY_DELAYS_MS = [15e3, 3e4, 6e4, 12e4, 24e4];
53854
+ var RPC_FAILED = /\bRPC failed; (?:HTTP (?:429|5\d\d)\b|curl (?:6|7|16|18|28|35|52|55|56|92)\b)/;
53855
+ var UNABLE_TO_ACCESS = /\bunable to access '[^']*': (?:The requested URL returned error: (?:429|5\d\d)\b|Failed to connect|Could not resolve host|Connection timed out|Operation timed out|Recv failure|Send failure|Empty reply from server|OpenSSL SSL_)/;
53856
+ function transientGitFailure(line2) {
53857
+ return RPC_FAILED.test(line2) || UNABLE_TO_ACCESS.test(line2);
53858
+ }
53859
+
53173
53860
  // packages/cli/src/runner/executor.ts
53174
53861
  var NODE_OS = { linux: "Linux", darwin: "macOS", win32: "Windows" };
53175
53862
  var NODE_ARCH = { x64: "X64", arm64: "ARM64", arm: "ARM" };
@@ -53419,6 +54106,12 @@ var JobExecution = class {
53419
54106
  }
53420
54107
  spawnSync2("rm", ["-rf", "--", gone], { uid: this.owner.uid, gid: this.owner.gid, stdio: "ignore" });
53421
54108
  }
54109
+ /** Wait `ms`, cut short when the job is cancelled: whether the job may go on. */
54110
+ async pause(ms) {
54111
+ const until = Date.now() + ms;
54112
+ while (!this.cancelled && Date.now() < until) await new Promise((r) => setTimeout(r, Math.min(1e3, until - Date.now())));
54113
+ return !this.cancelled;
54114
+ }
53422
54115
  /** Run one process with streamed, masked output. Resolves to its exit code, or null if it was killed. */
53423
54116
  exec(cmd, o) {
53424
54117
  return new Promise((resolveExit) => {
@@ -53696,18 +54389,41 @@ var JobExecution = class {
53696
54389
  }
53697
54390
  const depth = w["fetch-depth"] === void 0 ? 1 : Number(w["fetch-depth"]);
53698
54391
  const env = this.gitEnv();
53699
- const git4 = (args) => this.exec(["git", ...args], { cwd: this.workspace, env, asStepUser: false, timeoutMs: 30 * 6e4 });
54392
+ let transient = false;
54393
+ const onLine = (l) => {
54394
+ if (transientGitFailure(l)) transient = true;
54395
+ return false;
54396
+ };
54397
+ const git4 = (args) => {
54398
+ transient = false;
54399
+ return this.exec(["git", ...args], { cwd: this.workspace, env, asStepUser: false, timeoutMs: 30 * 6e4, onLine });
54400
+ };
54401
+ const fetchCommit = async (args) => {
54402
+ for (let attempt = 0; ; attempt++) {
54403
+ const code2 = await git4(["fetch", ...args]);
54404
+ const delay = CHECKOUT_RETRY_DELAYS_MS[attempt];
54405
+ if (code2 === 0 || code2 === null || !transient) return code2;
54406
+ if (delay === void 0 || Date.now() + delay >= this.deadline) {
54407
+ this.log("##[error]the server kept failing the fetch; the repository's storage may be busy. Run the job again in a few minutes");
54408
+ return code2;
54409
+ }
54410
+ this.log(`##[brnch] the server could not answer the fetch; retrying in ${delay / 1e3} s (attempt ${attempt + 2} of ${CHECKOUT_RETRY_DELAYS_MS.length + 1})`);
54411
+ if (!await this.pause(delay)) return null;
54412
+ }
54413
+ };
53700
54414
  this.discard(this.workspace);
53701
54415
  mkdirSync3(this.workspace, { mode: 493 });
53702
54416
  const branch = a.ref.startsWith("refs/heads/") ? a.ref.slice("refs/heads/".length) : null;
53703
54417
  const fetchAndCheckout = async () => {
53704
54418
  for (const args of [["init", "-q"], ["remote", "add", "origin", this.gitUrl()]]) if (await git4(args) !== 0) return 1;
53705
- let code2 = depth > 0 ? await git4(["fetch", "--no-tags", "--prune", `--depth=${depth}`, "origin", a.head_sha]) : 1;
53706
- if (code2 !== 0) {
53707
- if (this.cancelled) return null;
53708
- if (depth > 0) this.log("##[brnch] shallow fetch is not available here; fetching full history of the commit");
53709
- code2 = await git4(["fetch", "--no-tags", "origin", a.head_sha]);
53710
- }
54419
+ let code2;
54420
+ if (depth > 0) {
54421
+ code2 = await fetchCommit(["--no-tags", "--prune", `--depth=${depth}`, "origin", a.head_sha]);
54422
+ if (code2 !== 0 && code2 !== null && !transient) {
54423
+ this.log("##[brnch] the shallow fetch failed; fetching full history of the commit");
54424
+ code2 = await fetchCommit(["--no-tags", "origin", a.head_sha]);
54425
+ }
54426
+ } else code2 = await fetchCommit(["--no-tags", "origin", a.head_sha]);
53711
54427
  if (code2 !== 0) return code2;
53712
54428
  return git4(branch ? ["checkout", "-q", "-B", branch, a.head_sha] : ["checkout", "-q", "--detach", a.head_sha]);
53713
54429
  };
@@ -53752,6 +54468,7 @@ var JobExecution = class {
53752
54468
  async buildImageStep(w, env) {
53753
54469
  const r = await runBuildImage(w, {
53754
54470
  baseUrl: this.opts.baseUrl,
54471
+ fetch: (url2, init) => fetch(url2, init),
53755
54472
  registryHost: this.a.registry_host ?? null,
53756
54473
  repository: this.a.repository.full_name,
53757
54474
  jobToken: this.a.job_token,
@@ -53759,6 +54476,7 @@ var JobExecution = class {
53759
54476
  workspace: this.workspace,
53760
54477
  temp: this.temp,
53761
54478
  hosted: this.opts.hosted,
54479
+ stepUser: this.opts.stepUser ? { uid: this.opts.stepUser.uid, gid: this.opts.stepUser.gid } : null,
53762
54480
  log: (l) => this.log(l),
53763
54481
  run: (cmd, o) => this.exec(cmd, { cwd: this.workspace, env: o.env, asStepUser: false, timeoutMs: o.timeoutMs, onLine: this.onCommandLine }),
53764
54482
  capture: async (cmd, o) => {
@@ -53973,12 +54691,81 @@ function sanitizedEnv(env) {
53973
54691
  return out;
53974
54692
  }
53975
54693
 
54694
+ // packages/cli/src/runner/resources.ts
54695
+ import { statfs } from "node:fs/promises";
54696
+ import { cpus, freemem, totalmem, uptime } from "node:os";
54697
+ function cpuPercent(before, after) {
54698
+ if (!before.length || before.length !== after.length) return null;
54699
+ let idle = 0;
54700
+ let total = 0;
54701
+ for (let i = 0; i < after.length; i++) {
54702
+ for (const key of ["user", "nice", "sys", "idle", "irq"]) {
54703
+ const delta = after[i].times[key] - before[i].times[key];
54704
+ if (delta < 0 || !Number.isFinite(delta)) return null;
54705
+ total += delta;
54706
+ if (key === "idle") idle += delta;
54707
+ }
54708
+ }
54709
+ return total > 0 ? Math.round((1 - idle / total) * 1e3) / 10 : null;
54710
+ }
54711
+ var RunnerResourceCollector = class {
54712
+ constructor(workDir, jobSlots) {
54713
+ this.workDir = workDir;
54714
+ this.jobSlots = jobSlots;
54715
+ }
54716
+ workDir;
54717
+ jobSlots;
54718
+ previous = null;
54719
+ disk = null;
54720
+ diskPending = false;
54721
+ sample() {
54722
+ const at = performance.now();
54723
+ try {
54724
+ this.refreshDisk();
54725
+ const current = cpus();
54726
+ const interval = this.previous ? Math.round(at - this.previous.at) : 0;
54727
+ const cpu = this.previous && interval > 0 && interval <= 6e5 ? cpuPercent(this.previous.cpus, current) : null;
54728
+ this.previous = { cpus: current, at };
54729
+ const disk = this.disk && at - this.disk.at <= 6e4 ? this.disk : null;
54730
+ const totalMemory = totalmem();
54731
+ const parsed = RunnerResourceSnapshot.safeParse({
54732
+ cpu_percent: cpu,
54733
+ cpu_count: current.length,
54734
+ cpu_interval_ms: cpu === null ? null : interval,
54735
+ memory_total_bytes: totalMemory,
54736
+ memory_available_bytes: Math.min(totalMemory, Math.max(0, freemem())),
54737
+ disk_total_bytes: disk?.total ?? null,
54738
+ disk_available_bytes: disk?.available ?? null,
54739
+ uptime_seconds: Math.floor(uptime()),
54740
+ job_slots: this.jobSlots
54741
+ });
54742
+ return parsed.success ? parsed.data : void 0;
54743
+ } catch {
54744
+ return void 0;
54745
+ }
54746
+ }
54747
+ refreshDisk() {
54748
+ if (this.diskPending) return;
54749
+ this.diskPending = true;
54750
+ void statfs(this.workDir).then((s) => {
54751
+ const total = s.blocks * s.bsize;
54752
+ const available = Math.min(total, Math.max(0, s.bavail * s.bsize));
54753
+ this.disk = Number.isSafeInteger(total) && total > 0 && Number.isSafeInteger(available) ? { total, available, at: performance.now() } : null;
54754
+ }).catch(() => {
54755
+ this.disk = null;
54756
+ }).finally(() => {
54757
+ this.diskPending = false;
54758
+ });
54759
+ }
54760
+ };
54761
+
53976
54762
  // packages/cli/src/runner/agent.ts
53977
54763
  var RunnerAgent = class {
53978
54764
  constructor(cfg, opts) {
53979
54765
  this.cfg = cfg;
53980
54766
  this.opts = opts;
53981
54767
  this.client = this.clientFor(cfg);
54768
+ this.resources = new RunnerResourceCollector(opts.workDir, opts.once || cfg.ephemeral ? 1 : opts.jobs);
53982
54769
  }
53983
54770
  cfg;
53984
54771
  opts;
@@ -53990,6 +54777,7 @@ var RunnerAgent = class {
53990
54777
  exit = null;
53991
54778
  heartbeatTimer = null;
53992
54779
  taken = 0;
54780
+ resources;
53993
54781
  clientFor(cfg) {
53994
54782
  return createClient({ baseUrl: cfg.url, token: cfg.credential, userAgent: `${BRAND.cli}-runner/${BRAND.version}`, headers: { "x-brnch-runner-version": BRAND.version }, ...this.opts.fetch ? { fetch: this.opts.fetch } : {} });
53995
54783
  }
@@ -54027,7 +54815,7 @@ var RunnerAgent = class {
54027
54815
  }
54028
54816
  async heartbeat() {
54029
54817
  try {
54030
- const r = await this.client.runner.heartbeat({ running_jobs: [...this.running.keys()] });
54818
+ const r = await this.client.runner.heartbeat({ running_jobs: [...this.running.keys()], resources: this.resources.sample() });
54031
54819
  for (const id2 of r.cancel_jobs) this.running.get(id2)?.exec.cancel("cancelled by the server");
54032
54820
  for (const id2 of r.forget_jobs) this.running.get(id2)?.exec.abort("the server reassigned this job");
54033
54821
  if (r.removed) {
@@ -54074,6 +54862,7 @@ var RunnerAgent = class {
54074
54862
  await this.client.runner.diagnostics(diagnostics(this.cfg)).catch(() => void 0);
54075
54863
  const unhook = this.opts.onSignal?.((s) => this.shutdown(s)) ?? (() => void 0);
54076
54864
  this.heartbeatTimer = setInterval(() => void this.heartbeat(), Math.max(1, me.heartbeat_seconds) * 1e3);
54865
+ await this.heartbeat();
54077
54866
  const single = this.opts.once || this.cfg.ephemeral;
54078
54867
  let backoff = 1e3;
54079
54868
  try {
@@ -55091,6 +55880,13 @@ var EXAMPLES = {
55091
55880
  "deploy run": [`${c7} deploy run production --wait || echo "deploy failed (exit $?)"`, `${c7} deploy run dpt_123 --commit 0123456789abcdef0123456789abcdef01234567`],
55092
55881
  "deploy update": [`${c7} deploy update production --build "npm run build:prod"`, `${c7} deploy update production --disable`],
55093
55882
  "deploy rollback": [`${c7} deploy rollback production --to dpl_123`],
55883
+ "db sources add": [
55884
+ `${c7} db suggest # the migration directories found`,
55885
+ `${c7} db sources add --name app --path db/migrations --format atlas --engine postgres --connection icn_123 --organization acme --database app --revisions-schema app_atlas`,
55886
+ `op read "op://Infra/app-dsn/credential" | ${c7} db sources add --name app \u2026 --credential-mode dsn --dsn-file -`
55887
+ ],
55888
+ "db plan": [`${c7} db plan app`, `${c7} db plan app --ref my-branch --live`],
55889
+ "db approve": [`${c7} db runs --source app`, `${c7} db approve dmr_123 --reason "reviewed the plan"`],
55094
55890
  version: [`${c7} version`, `${c7} version --json`],
55095
55891
  upgrade: [`${c7} upgrade --check`, `${c7} upgrade`, `${c7} upgrade --force`],
55096
55892
  doctor: [`${c7} doctor`, `${c7} doctor --json | jq '.checks[] | select(.status != "ok")'`],
@@ -55231,6 +56027,7 @@ function buildProgram(rt) {
55231
56027
  registerMemory(program2, rt);
55232
56028
  registerIntegrations(program2, rt);
55233
56029
  registerDeploy(program2, rt);
56030
+ registerDb(program2, rt);
55234
56031
  registerDelegate(program2, rt);
55235
56032
  registerAgents(program2, rt);
55236
56033
  registerCodingAgents(program2, rt);
@@ -55305,17 +56102,44 @@ function report2(e, deps, state2) {
55305
56102
  `);
55306
56103
  return exitCode;
55307
56104
  }
55308
- async function run2(argv, overrides = {}) {
56105
+ function agentToolFlag(argv) {
56106
+ for (let i = 0; i < argv.length; i++) {
56107
+ if (argv[i] === "--agent-tool") return argv[i + 1] ?? null;
56108
+ if (argv[i].startsWith("--agent-tool=")) return argv[i].slice("--agent-tool=".length);
56109
+ }
56110
+ return null;
56111
+ }
56112
+ async function run2(argv, overrides = {}, parentStatus) {
55309
56113
  const merged = { ...nodeDeps(), ...overrides };
55310
- const deps = merged;
55311
56114
  const split = splitGitArgs(argv);
55312
56115
  const state2 = { exitCode: EXIT.OK, wroteResult: false, secrets: [], json: wantsJson(split.argv) };
56116
+ state2.status = parentStatus ?? (programStatusEnabled(merged, detectAgent(merged.env, agentToolFlag(split.argv))) ? statusReporter(merged.stderr) : silentStatus);
56117
+ const status = state2.status;
56118
+ const deps = {
56119
+ ...merged,
56120
+ prompt: async (q) => {
56121
+ const answered2 = status.ask();
56122
+ try {
56123
+ return await merged.prompt(q);
56124
+ } finally {
56125
+ answered2();
56126
+ }
56127
+ },
56128
+ keys: merged.keys ? (fn2) => {
56129
+ const answered2 = status.ask();
56130
+ const stop = merged.keys(fn2);
56131
+ return () => {
56132
+ stop();
56133
+ answered2();
56134
+ };
56135
+ } : null
56136
+ };
55313
56137
  const invoke2 = async (sub, o = {}) => {
55314
56138
  const out = [];
55315
56139
  const err = [];
55316
56140
  const errSink = (s) => void err.push(s);
55317
- const quiet = o.quiet ? { stderr: { write: errSink }, stderrIsTTY: false, spawnGit: (args, g2) => deps.spawnGit(args, { ...g2, onOutput: errSink }) } : {};
55318
- const code2 = await run2(sub, { ...deps, ...o.cwd ? { cwd: o.cwd } : {}, ...o.capture ? { stdout: { write: (s) => out.push(s) } } : {}, ...quiet });
56141
+ const quiet = o.quiet ? { stderr: { write: errSink }, stderrIsTTY: false, spawnGit: (args, g2) => merged.spawnGit(args, { ...g2, onOutput: errSink }) } : {};
56142
+ const code2 = await run2(sub, { ...merged, ...o.cwd ? { cwd: o.cwd } : {}, ...o.capture ? { stdout: { write: (s) => out.push(s) } } : {}, ...quiet }, status);
55319
56143
  return { code: code2, stdout: out.join(""), stderr: err.join("") };
55320
56144
  };
55321
56145
  const program2 = buildProgram({ deps, state: state2, gitArgs: split.gitArgs, invoke: invoke2 });
@@ -55330,6 +56154,11 @@ async function run2(argv, overrides = {}) {
55330
56154
  await sendCrashReport(r, { baseUrl: state2.baseUrl ?? crashServer(deps.env), fetch: deps.sys.fetch, stderr: deps.stderr });
55331
56155
  }
55332
56156
  }
56157
+ if (!parentStatus) {
56158
+ const name = commandPath(program2, split.argv);
56159
+ const what = name ? `${BRAND.cli} ${name}` : BRAND.cli;
56160
+ status.finish(code === EXIT.OK, code === EXIT.OK ? `${what} finished` : `${what} failed (exit ${code})`);
56161
+ }
55333
56162
  await saveBookmark(deps, state2);
55334
56163
  await updateHint(deps, state2);
55335
56164
  return code;