gh-glance 0.14.0 → 0.14.1

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/CHANGELOG.md +23 -1
  2. package/index.mjs +56 -4
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.14.1] - 2026-09-08
11
+
12
+ ### Fixed
13
+
14
+ - **A folder with no git remote opens in setup, not in a coordination wait.**
15
+ The onboarding prompt (`No GitHub remote found -- press Enter to create one`)
16
+ was only ever produced from a `gh` error, and with no remote there is no host
17
+ to verify an identity against, so no `gh` command ever ran: the dashboard sat
18
+ on "Can't coordinate API use -- retrying" and "waiting for API budget…" for a
19
+ repository that did not exist. The target is now decided before any
20
+ coordination -- no remote, no `--repo`, no `GH_REPO` means setup -- and
21
+ `--doctor` says so in the same words instead of reporting an unavailable
22
+ identity.
23
+ - **An upgrade no longer stalls on a ledger that never recorded a budget for a
24
+ resource it charged.** A 0.12-era file whose panes only ever observed GraphQL
25
+ holds core reservations with no core budget to date them by, so the migration
26
+ reported "older spend cannot be settled or waited out" -- a hold with no
27
+ deadline, on every launch, forever. Each reservation records the epoch it was
28
+ admitted against; that epoch's reset now serves as the deadline, and a window
29
+ that ended weeks ago clears at once.
30
+
10
31
  ## [0.14.0] - 2026-09-07
11
32
 
12
33
  ### Added
@@ -1029,7 +1050,8 @@ engineering, security, QA and UX. What follows is what changed as a result.
1029
1050
  - The `main` field from `package.json`. It advertised the file as importable,
1030
1051
  but importing it took over the terminal or exited the host process.
1031
1052
 
1032
- [Unreleased]: https://github.com/juan294/gh-glance/compare/v0.14.0...HEAD
1053
+ [Unreleased]: https://github.com/juan294/gh-glance/compare/v0.14.1...HEAD
1054
+ [0.14.1]: https://github.com/juan294/gh-glance/compare/v0.14.0...v0.14.1
1033
1055
  [0.14.0]: https://github.com/juan294/gh-glance/compare/v0.13.3...v0.14.0
1034
1056
  [0.13.3]: https://github.com/juan294/gh-glance/compare/v0.13.2...v0.13.3
1035
1057
  [0.13.2]: https://github.com/juan294/gh-glance/compare/v0.13.1...v0.13.2
package/index.mjs CHANGED
@@ -428,6 +428,9 @@ function isAuthProblem(err) {
428
428
  function isMissingRemote(err) {
429
429
  return /(?:failed to determine base repo:\s*)?no git remotes found/i.test(errText(err));
430
430
  }
431
+ // The shorter of the two, so a verdict recorded without a gh subprocess reads
432
+ // exactly like one gh produced and classifies the same way.
433
+ const NO_REMOTE_ERROR_TEXT = "no git remotes found";
431
434
 
432
435
  const UNUSABLE_OUTPUT_CODE = "GH_GLANCE_UNUSABLE_OUTPUT";
433
436
 
@@ -1060,6 +1063,25 @@ function resolveEffectiveHost({
1060
1063
 
1061
1064
  const UNRESOLVED_REPOSITORY = "Repository could not be resolved; pass --repo owner/name";
1062
1065
 
1066
+ // Whether there is a repository to talk about at all. A folder with no git
1067
+ // remote, no --repo and no GH_REPO is the onboarding state the no-remote verdict
1068
+ // renders -- but that verdict was only ever produced from a gh error, and with
1069
+ // no remote there is no host to verify an identity against, so no gh command
1070
+ // ran: the coordinator reported unknown-host and the dashboard sat on a generic
1071
+ // "retrying" notice, waiting for a budget it could never resolve. Decided from
1072
+ // the same inputs as the host so the two cannot disagree; an ambiguous set of
1073
+ // remotes is still a coordination question, not this one.
1074
+ function noRepositoryTarget({ runtimeRepo = null, ghRepo = null, remoteUrls = [] } = {}) {
1075
+ return !runtimeRepo && !ghRepo && remoteUrls.length === 0;
1076
+ }
1077
+
1078
+ function runtimeHasNoRepositoryTarget(remoteUrls = runtimeRemoteUrls) {
1079
+ return noRepositoryTarget({ runtimeRepo: runtime.repo, ghRepo: process.env.GH_REPO, remoteUrls });
1080
+ }
1081
+
1082
+ const NO_REPOSITORY_TARGET_REPORT =
1083
+ "no repository target: this folder has no git remote (run `gh repo create`, or pass --repo owner/name)";
1084
+
1063
1085
  // The owner/name half of the same question resolveEffectiveHost answers for the
1064
1086
  // host, and in the same precedence: an explicit --repo, then GH_REPO, then an
1065
1087
  // unambiguous git remote. Ambiguity resolves to null rather than to a guess.
@@ -2781,9 +2803,9 @@ function inspectLegacyMigration(root, state, now) {
2781
2803
  for (const resource of RATE_RESOURCES) {
2782
2804
  const budget = legacy.budgets[resource];
2783
2805
  holdUntil = Math.max(holdUntil, budget?.blockUntil ?? 0);
2784
- const uncertain = Object.values(legacy.reservations).some((reservation) =>
2806
+ const uncertain = Object.values(legacy.reservations).filter((reservation) =>
2785
2807
  ["started", "completed"].includes(reservation.status) && reservationCost(reservation, resource, legacy.leases, now) > 0);
2786
- if (uncertain) {
2808
+ if (uncertain.length > 0) {
2787
2809
  // The deadline may only be read from the migrated view for resources the
2788
2810
  // migration kept. A v1 file's budgets are dropped on purpose -- they came
2789
2811
  // from /rate_limit and are not spendable capacity -- but the reset they
@@ -2794,9 +2816,22 @@ function inspectLegacyMigration(root, state, now) {
2794
2816
  // none, and reported legacy-unresolved -- which has no deadline and so
2795
2817
  // never cleared. The file it came from had a perfectly good reset in it
2796
2818
  // the whole time.
2819
+ //
2820
+ // A file can also simply never have recorded a budget for the resource:
2821
+ // a v2 ledger whose panes only ever observed GraphQL still charged core
2822
+ // per request, so its core reservations exist without a core budget to
2823
+ // ask. Each reservation records the epoch it was admitted against, and
2824
+ // that epoch's reset is when its window ended -- the same fact, kept in
2825
+ // the other place. Only a reservation with no budget *and* no epoch is
2826
+ // genuinely undated.
2827
+ const recorded = uncertain
2828
+ .map((reservation) => Number(String(reservation.epochs?.[resource] ?? "").split(":")[1]))
2829
+ .filter(Number.isFinite);
2797
2830
  const reset = Number.isFinite(budget?.resetMs)
2798
2831
  ? budget.resetMs
2799
- : Number(raw?.budgets?.[resource]?.resetMs);
2832
+ : Number.isFinite(Number(raw?.budgets?.[resource]?.resetMs))
2833
+ ? Number(raw.budgets[resource].resetMs)
2834
+ : recorded.length > 0 ? Math.max(...recorded) : NaN;
2800
2835
  if (!Number.isFinite(reset)) return { ok: false, reason: "legacy-unresolved" };
2801
2836
  holdUntil = Math.max(holdUntil, reset + BUDGET_RESET_GRACE_MS);
2802
2837
  }
@@ -6046,7 +6081,9 @@ async function runDoctor() {
6046
6081
  runtimeIdentityCoordinator = createIdentityCoordinator({ host: effectiveHost });
6047
6082
  const verified = await runtimeIdentityCoordinator.refresh();
6048
6083
  const verifiedIdentity = runtimeIdentityCoordinator.current();
6049
- const authStatus = verifiedIdentity ? `${verifiedIdentity.host}: ${verifiedIdentity.login} (verified)` : identityCoordinationMessage(verified.reason);
6084
+ const authStatus = verifiedIdentity
6085
+ ? `${verifiedIdentity.host}: ${verifiedIdentity.login} (verified)`
6086
+ : runtimeHasNoRepositoryTarget(remoteUrls) ? NO_REPOSITORY_TARGET_REPORT : identityCoordinationMessage(verified.reason);
6050
6087
  const resources = verifiedIdentity ? await readRateLimitResources(undefined, effectiveHost).catch(() => null) : null;
6051
6088
  const budget = resources
6052
6089
  ? await rateBudget(resources)
@@ -9987,6 +10024,18 @@ function App({ onCreateRemote = () => {} } = {}) {
9987
10024
  });
9988
10025
  }
9989
10026
 
10027
+ // A folder with no repository target goes straight to setup. The no-remote
10028
+ // verdict is normally a fetch failure, but nothing can be fetched here --
10029
+ // there is no host to verify against -- so the same verdict is recorded on
10030
+ // every tab before coordination is attempted. remoteSetupRef then keeps the
10031
+ // scheduler out, and the Enter hint hands off to `gh repo create`.
10032
+ function enterRemoteSetup() {
10033
+ if (!runtimeHasNoRepositoryTarget()) return false;
10034
+ const failure = toTabError(new Error(NO_REMOTE_ERROR_TEXT));
10035
+ setErrors(Object.fromEntries(TABS.map((t) => [t.key, failure])));
10036
+ return true;
10037
+ }
10038
+
9990
10039
  function publishControlStatus(key, refreshed, snapshot, nowMs) {
9991
10040
  if (pendingBlockPublications.size > 0) {
9992
10041
  pauseCoordination(key, "block-unpublished");
@@ -10989,6 +11038,8 @@ function App({ onCreateRemote = () => {} } = {}) {
10989
11038
  async function bootstrap() {
10990
11039
  remoteUrls = runtimeRemoteUrls.length > 0 ? runtimeRemoteUrls : await gitRemoteUrls();
10991
11040
  runtimeRemoteUrls = remoteUrls;
11041
+ if (cancelled) return;
11042
+ if (enterRemoteSetup()) return;
10992
11043
  await runtimeIdentityCoordinator.refresh();
10993
11044
  if (cancelled) return;
10994
11045
  const currentScope = ensureScope(Date.now());
@@ -11859,6 +11910,7 @@ export {
11859
11910
  isRateLimited,
11860
11911
  isAuthProblem,
11861
11912
  isMissingRemote,
11913
+ noRepositoryTarget,
11862
11914
  isUnusableOutput,
11863
11915
  forwardSignalToChild,
11864
11916
  toTabError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-glance",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "A live-refreshing GitHub dashboard for your terminal: Actions, Issues, Pull Requests, and Security alerts in one narrow pane",
5
5
  "type": "module",
6
6
  "bin": {