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.
- package/README.md +1 -1
- package/brnch.mjs +75 -27
- package/package.json +3 -2
package/README.md
CHANGED
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.
|
|
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
|
|
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 &&
|
|
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
|
|
42674
|
-
|
|
42675
|
-
|
|
42676
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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",
|