@great-arrow/cli 0.1.49 → 0.1.51

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 (2) hide show
  1. package/dist/index.js +88 -22
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9399,7 +9399,7 @@ function isNodeError(value) {
9399
9399
 
9400
9400
  // src/lib/version.ts
9401
9401
  var CLI_PACKAGE_NAME = "@great-arrow/cli";
9402
- var CLI_VERSION = "0.1.49";
9402
+ var CLI_VERSION = "0.1.51";
9403
9403
  var GAD_INSTALL_VERSION = "2026.08.08";
9404
9404
  var GAD_INSTALL_MIN_VERSION = "2026.06.22";
9405
9405
  var GAD_INSTALL_HEADER = "X-GAD-Install-Version";
@@ -10414,14 +10414,14 @@ async function storedPairOtherThan(ours) {
10414
10414
  debugSwallow("refresh.reread", err);
10415
10415
  }
10416
10416
  if (stored?.kind !== "oauth") return null;
10417
+ if (stored.credential.clientId !== ours.clientId || stored.credential.apiUrl.replace(/\/$/, "") !== ours.apiUrl.replace(/\/$/, "")) {
10418
+ throw networkError(
10419
+ "The stored session changed while renewing it.",
10420
+ "Retry the command with the current session."
10421
+ );
10422
+ }
10417
10423
  return stored.credential.refreshToken === ours.refreshToken ? null : stored.credential;
10418
10424
  }
10419
- async function rotatedByAnotherProcess(ours) {
10420
- const theirs = await storedPairOtherThan(ours);
10421
- if (!theirs) return null;
10422
- if (Date.now() >= theirs.accessExpiresAt - REFRESH_SKEW_MS) return null;
10423
- return theirs;
10424
- }
10425
10425
  async function resolveAuth() {
10426
10426
  const config = await loadConfig();
10427
10427
  const configApiUrl = config.api_url || DEFAULT_API_URL;
@@ -10512,20 +10512,37 @@ async function ensureFreshAccessToken(credential) {
10512
10512
  }
10513
10513
  async function doRefresh(credential) {
10514
10514
  const held = await acquireRefreshLock();
10515
+ let current = credential;
10515
10516
  try {
10516
- const theirs = await rotatedByAnotherProcess(credential);
10517
- if (theirs) return theirs;
10517
+ const theirs = await storedPairOtherThan(credential);
10518
+ if (theirs) {
10519
+ current = theirs;
10520
+ if (Date.now() < current.accessExpiresAt - REFRESH_SKEW_MS)
10521
+ return current;
10522
+ }
10523
+ if (!held) {
10524
+ throw networkError(
10525
+ "Session renewal is already in progress.",
10526
+ "Retry shortly; your saved session has been kept."
10527
+ );
10528
+ }
10518
10529
  const refreshed = await refreshTokens({
10519
- apiUrl: credential.apiUrl,
10520
- clientId: credential.clientId,
10521
- refreshToken: credential.refreshToken
10530
+ apiUrl: current.apiUrl,
10531
+ clientId: current.clientId,
10532
+ refreshToken: current.refreshToken
10522
10533
  });
10523
10534
  await saveOAuthCredential(refreshed);
10524
10535
  return refreshed;
10525
10536
  } catch (err) {
10526
10537
  if (err instanceof RefreshRejected) {
10527
- const theirs = await storedPairOtherThan(credential);
10528
- if (theirs) return theirs;
10538
+ const theirs = await storedPairOtherThan(current);
10539
+ if (theirs) {
10540
+ if (Date.now() < theirs.accessExpiresAt) return theirs;
10541
+ throw networkError(
10542
+ "Another process updated the session during renewal.",
10543
+ "Retry shortly; your saved session has been kept."
10544
+ );
10545
+ }
10529
10546
  await clearStoredCredential();
10530
10547
  throw sessionExpired();
10531
10548
  }
@@ -10543,12 +10560,13 @@ var EXIT_SESSION_EXPIRED = 2;
10543
10560
  var EXIT_NOT_OAUTH = 3;
10544
10561
  function authCommand() {
10545
10562
  const cmd = new Command("auth").description("Authentication helpers");
10546
- cmd.command("token").description("Print a fresh OAuth access token, refreshing it if needed").option("--json", "Output {accessToken, accessExpiresAt, apiUrl} as JSON").action(async (options) => {
10563
+ cmd.command("token").description("Print a fresh OAuth access token, refreshing it if needed").option("--json", "Output {accessToken, accessExpiresAt, apiUrl} as JSON").action(async (options, actionCommand) => {
10547
10564
  try {
10548
10565
  const t = await getFreshOAuthAccessToken();
10549
10566
  writeCliLocator();
10567
+ const jsonOutput = options.json || actionCommand.optsWithGlobals().json;
10550
10568
  process.stdout.write(
10551
- options.json ? `${JSON.stringify(t)}
10569
+ jsonOutput ? `${JSON.stringify(t)}
10552
10570
  ` : `${t.accessToken}
10553
10571
  `
10554
10572
  );
@@ -11958,6 +11976,29 @@ import { readFileSync } from "node:fs";
11958
11976
 
11959
11977
  // src/lib/work-status.ts
11960
11978
  import { execFile as execFile2, execFileSync as execFileSync2 } from "node:child_process";
11979
+
11980
+ // ../../src/lib/work-risk/push-state.ts
11981
+ var REMOTE_BRANCH_REF_PREFIX = "refs/remotes/origin/";
11982
+ var REMOTE_BRANCH_TIPS_FORMAT = "%(refname)%09%(objectname)";
11983
+ function parseRemoteBranchTips(raw) {
11984
+ const tips = /* @__PURE__ */ new Map();
11985
+ for (const line of raw.split("\n")) {
11986
+ const [refname, sha] = line.split(" ");
11987
+ if (!refname?.startsWith(REMOTE_BRANCH_REF_PREFIX) || !sha) continue;
11988
+ const branch = refname.slice(REMOTE_BRANCH_REF_PREFIX.length);
11989
+ if (!branch || branch === "HEAD") continue;
11990
+ tips.set(branch, sha.trim());
11991
+ }
11992
+ return tips;
11993
+ }
11994
+ function remoteHasHead(branch, head, tips, isAncestor2) {
11995
+ const tip = tips.get(branch);
11996
+ if (!tip) return false;
11997
+ if (tip === head) return true;
11998
+ return isAncestor2(head, tip);
11999
+ }
12000
+
12001
+ // src/lib/work-status.ts
11961
12002
  var LOCAL_COMMAND_TIMEOUT_MS = 2e4;
11962
12003
  function run(command, args, cwd) {
11963
12004
  try {
@@ -12070,9 +12111,31 @@ function auditLocalBranches(root) {
12070
12111
  root
12071
12112
  ).split("\n").filter(Boolean)
12072
12113
  );
12114
+ const tipsResult = git(
12115
+ [
12116
+ "for-each-ref",
12117
+ `--format=${REMOTE_BRANCH_TIPS_FORMAT}`,
12118
+ "refs/remotes/origin"
12119
+ ],
12120
+ root
12121
+ );
12122
+ const tips = parseRemoteBranchTips(
12123
+ tipsResult.status === 0 ? tipsResult.stdout : ""
12124
+ );
12073
12125
  return raw.split("\n").filter(Boolean).flatMap((line) => {
12074
12126
  const parsed = parseLocalBranchLine(line, exact, unmerged);
12075
- return parsed ? [parsed] : [];
12127
+ if (!parsed) return [];
12128
+ return [
12129
+ {
12130
+ ...parsed,
12131
+ remoteHasHead: remoteHasHead(
12132
+ parsed.branch,
12133
+ parsed.head,
12134
+ tips,
12135
+ (ancestor, descendant) => isAncestor(ancestor, descendant, root)
12136
+ )
12137
+ }
12138
+ ];
12076
12139
  });
12077
12140
  }
12078
12141
  function parseLocalBranchLine(line, exact, unmerged) {
@@ -12131,7 +12194,7 @@ function classifyWorktree(audit) {
12131
12194
  if (audit.path.includes("/.claude/worktrees/")) return "agent-harness";
12132
12195
  if (audit.mergedToMain) return "cleanup-ready";
12133
12196
  if (audit.upstreamGone) return "upstream-gone";
12134
- if (!audit.upstream) return "local-rescue";
12197
+ if (!audit.remoteHasHead) return "local-rescue";
12135
12198
  return "active-unmerged";
12136
12199
  }
12137
12200
  async function auditWorktrees(root, localBranches) {
@@ -12159,6 +12222,7 @@ async function auditWorktrees(root, localBranches) {
12159
12222
  mergedToMain: branch?.mergedToMain ?? false,
12160
12223
  upstream: branch?.upstream ?? null,
12161
12224
  upstreamGone: branch?.upstreamGone ?? false,
12225
+ remoteHasHead: branch?.remoteHasHead ?? false,
12162
12226
  behindMain: branch?.behindMain ?? 0,
12163
12227
  aheadMain: branch?.aheadMain ?? 0,
12164
12228
  countsExact: branch?.countsExact ?? false
@@ -12178,6 +12242,7 @@ function auditDetachedLocalBranches(localBranches, checkedOutBranches) {
12178
12242
  countsExact,
12179
12243
  upstream,
12180
12244
  upstreamGone,
12245
+ remoteHasHead: remoteHasHead2,
12181
12246
  committedAt
12182
12247
  }) => ({
12183
12248
  branch,
@@ -12186,12 +12251,13 @@ function auditDetachedLocalBranches(localBranches, checkedOutBranches) {
12186
12251
  behindMain,
12187
12252
  aheadMain,
12188
12253
  countsExact,
12189
- // `upstream` is what separates "never left this disk" from "pushed,
12190
- // just has no PR yet" — the single bit that decides whether work can
12191
- // actually be lost. It was computed and then dropped here, so the
12192
- // server classified both as one undifferentiated "stranded".
12254
+ // `remoteHasHead` is what separates "never left this disk" from
12255
+ // "pushed, just has no PR yet" — the single bit that decides whether
12256
+ // work can actually be lost. `upstream` alone cannot: a task branch
12257
+ // could carry origin/main as its upstream without ever being pushed.
12193
12258
  upstream,
12194
12259
  upstreamGone,
12260
+ remoteHasHead: remoteHasHead2,
12195
12261
  // Without this the grace window read `ageMs = Infinity` for every
12196
12262
  // branch, so a branch was "stranded" from its first second.
12197
12263
  committedAt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@great-arrow/cli",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "description": "GreatArrow.ai CLI tool",
5
5
  "type": "module",
6
6
  "bin": {