brnch-cli 0.18.1 → 0.18.2

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 +75 -27
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # brnch 0.18.1
1
+ # brnch 0.18.2
2
2
 
3
3
  Command-line client for [Brnch](https://brnch.io) — Git hosting for humans and agents. Single file, no runtime dependencies, Node 20 or newer.
4
4
 
package/brnch.mjs CHANGED
@@ -36901,7 +36901,7 @@ function invalidAgentToolFlag(flag) {
36901
36901
  }
36902
36902
 
36903
36903
  // packages/cli/package.json
36904
- var version2 = "0.18.1";
36904
+ var version2 = "0.18.2";
36905
36905
 
36906
36906
  // packages/cli/src/brand.ts
36907
36907
  var BRAND = {
@@ -42184,6 +42184,29 @@ function input2(deps, o) {
42184
42184
  });
42185
42185
  }
42186
42186
 
42187
+ // packages/cli/src/step-up.ts
42188
+ function needsCliStepUp(e) {
42189
+ const err = isBrnchApiError(e) ? e : e;
42190
+ return err?.code === "MFA_REQUIRED" && err.details?.step_up === "cli";
42191
+ }
42192
+ function sameOrigin(ctx, start) {
42193
+ try {
42194
+ return new URL(start.verification_uri_complete).origin === new URL(ctx.baseUrl).origin;
42195
+ } catch {
42196
+ return false;
42197
+ }
42198
+ }
42199
+ async function awaitStepUp(ctx, start) {
42200
+ const deadline = Date.parse(start.expires_at);
42201
+ for (; ; ) {
42202
+ await ctx.deps.sleep(start.interval * 1e3);
42203
+ const s = await ctx.client("user").auth.stepUp.status(start.id);
42204
+ if (s.status === "approved") return s.step_up_expires_at;
42205
+ if (s.status === "denied") throw new CliError("the step-up was denied in the browser", EXIT.UNAUTHENTICATED, "access_denied");
42206
+ 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");
42207
+ }
42208
+ }
42209
+
42187
42210
  // packages/cli/src/pending-login.ts
42188
42211
  import { mkdir as mkdir11, readFile as readFile9, rm as rm7, writeFile as writeFile10 } from "node:fs/promises";
42189
42212
  import { dirname as dirname11, join as join18 } from "node:path";
@@ -42455,7 +42478,7 @@ function registerAuth(program2, rt) {
42455
42478
  `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
42479
  );
42457
42480
  }
42458
- const sameOrigin = (() => {
42481
+ const sameOrigin2 = (() => {
42459
42482
  try {
42460
42483
  return new URL(start.verification_uri_complete).origin === new URL(ctx.baseUrl).origin;
42461
42484
  } catch {
@@ -42466,7 +42489,7 @@ function registerAuth(program2, rt) {
42466
42489
  ${codeBox(start.user_code, paint(true))}
42467
42490
  ` : `! Your one-time code: ${start.user_code}`);
42468
42491
  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`)))
42492
+ 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
42493
  opened = await ctx.deps.openUrl(start.verification_uri_complete);
42471
42494
  ctx.info(
42472
42495
  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 +42674,11 @@ Store them somewhere safe and offline (a password manager or on paper).`
42651
42674
  }
42652
42675
  }
42653
42676
  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
42677
  ctx.info(ctx.motion ? `
42662
42678
  ${codeBox(start.user_code, paint(true))}
42663
42679
  ` : `! Your one-time code: ${start.user_code}`);
42664
42680
  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`)))
42681
+ 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
42682
  opened = await ctx.deps.openUrl(start.verification_uri_complete);
42667
42683
  ctx.info(
42668
42684
  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 +42686,12 @@ ${codeBox(start.user_code, paint(true))}
42670
42686
  );
42671
42687
  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
42688
  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.
42689
+ const until = await awaitStepUp(ctx, start);
42690
+ return ctx.result(
42691
+ { status: "approved", step_up_expires_at: until },
42692
+ (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
42693
  ${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
- }
42694
+ );
42687
42695
  })
42688
42696
  );
42689
42697
  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(
@@ -44376,6 +44384,41 @@ var Quickstart = class {
44376
44384
  const g2 = this.ctx.globals;
44377
44385
  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
44386
  }
44387
+ /** Whether this run already asked the person to step up (once: a second refusal is the answer). */
44388
+ steppedUp = false;
44389
+ /**
44390
+ * `run`, and again after the person confirms it's them in the browser when the organization requires a second factor
44391
+ * for it (ADR-0062): quickstart runs long enough that a step-up approved before it may have lapsed by the deploy.
44392
+ * `refusal` finds the error in a composed command's `--json` output.
44393
+ */
44394
+ async withStepUp(row, run3, refusal = () => null) {
44395
+ try {
44396
+ const v = await run3();
44397
+ if (!needsCliStepUp(refusal(v)) || !await this.stepUp(row)) return v;
44398
+ } catch (e) {
44399
+ if (!needsCliStepUp(e) || !await this.stepUp(row)) throw e;
44400
+ }
44401
+ return run3();
44402
+ }
44403
+ async stepUp(row) {
44404
+ const { ctx } = this;
44405
+ if (!ctx.interactive || this.steppedUp) return false;
44406
+ this.steppedUp = true;
44407
+ const start = await ctx.client("user").auth.stepUp.start();
44408
+ const opened = sameOrigin(ctx, start) && await ctx.deps.openUrl(start.verification_uri_complete);
44409
+ 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})`;
44410
+ if (this.board) {
44411
+ this.running(row, "waiting for you to confirm it's you in the browser");
44412
+ this.board.hint(`${todo} ${this.errPaint.dim("(Ctrl-C stops; the repository is ready either way)")}`);
44413
+ } else ctx.info(` ${todo}. Waiting for it\u2026`);
44414
+ try {
44415
+ await awaitStepUp(ctx, start);
44416
+ } finally {
44417
+ this.board?.hint(null);
44418
+ }
44419
+ this.note(row, "confirmed: continuing");
44420
+ return true;
44421
+ }
44379
44422
  /** A composed command, in `cwd`, as JSON. Under the board it runs quietly (its stderr and git's are returned instead). */
44380
44423
  async runJson(argv, cwd) {
44381
44424
  this.ctx.idle();
@@ -44625,7 +44668,7 @@ async function stepDeploy(q, dir, ns, name, o, home2) {
44625
44668
  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
44669
  q.running("deploy", "looking for a Cloudflare connection");
44627
44670
  const repo = await ctx.repoId(spec, "user");
44628
- const usable = async () => (await ctx.client("user").deploy.connections(repo)).items.some((c8) => c8.state === "active");
44671
+ const usable = () => q.withStepUp("deploy", async () => (await ctx.client("user").deploy.connections(repo)).items.some((c8) => c8.state === "active"));
44629
44672
  if (!await usable()) {
44630
44673
  if (!ctx.interactive) {
44631
44674
  q.record("deploy", "planned", later, `connect Cloudflare, then \`${BRAND.cli} deploy setup\``);
@@ -44652,7 +44695,11 @@ async function stepDeploy(q, dir, ns, name, o, home2) {
44652
44695
  }
44653
44696
  q.running("deploy", "creating the deploy target");
44654
44697
  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);
44698
+ const setup = await q.withStepUp(
44699
+ "deploy",
44700
+ () => q.runJson(["--repo", spec, "deploy", "setup", ...pinned], dir),
44701
+ (r) => r.value?.error
44702
+ );
44656
44703
  if (setup.code === EXIT.USAGE) {
44657
44704
  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
44705
  return;
@@ -44662,7 +44709,8 @@ async function stepDeploy(q, dir, ns, name, o, home2) {
44662
44709
  return;
44663
44710
  }
44664
44711
  const deploy = ctx.client("user").deploy;
44665
- let d = await deploy.targets.deploy(setup.value.id, {});
44712
+ const target = setup.value.id;
44713
+ let d = await q.withStepUp("deploy", () => deploy.targets.deploy(target, {}));
44666
44714
  const deadline = ctx.deps.sys.now() + o.deployTimeout * 1e3;
44667
44715
  let last = "";
44668
44716
  for (; ; ) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brnch-cli",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "description": "Brnch CLI: Git hosting for humans and agents, from the command line",
5
5
  "keywords": [
6
6
  "git",
@@ -16,7 +16,8 @@
16
16
  "license": "UNLICENSED",
17
17
  "type": "module",
18
18
  "bin": {
19
- "brnch": "brnch.mjs"
19
+ "brnch": "brnch.mjs",
20
+ "docker-credential-brnch": "brnch.mjs"
20
21
  },
21
22
  "files": [
22
23
  "brnch.mjs",