@indigoai-us/hq-cli 5.345.113 → 5.345.114

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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.114] — 2026-10-13
6
+ - `hq capacity login <provider> --account <id>` signs a registered account
7
+ in again into its existing home. The account id, home, and Keychain item
8
+ stay the same. `--account default` (or the id registered at `~/.claude`)
9
+ signs in the default home without setting `CLAUDE_CONFIG_DIR`. A reserved
10
+ id that names no registered account is refused before any sign-in runs.
11
+ - `hq capacity login --repair` signs in each signed-out account one at a
12
+ time, keeps going when one fails, and ends with a Fixed/Skipped report.
13
+ It exits 1 when any signed-out account was skipped.
14
+ - `hq capacity route` and `hq lanes create --account auto` list every
15
+ signed-out account under `skipped` with its reason and the exact
16
+ `hq capacity login <provider> --account <id>` fix, using the lanes
17
+ account id. The `no_account_with_room` error now carries `skipped`.
18
+
5
19
  ## [5.345.113] — 2026-10-13
6
20
  - `hq daemon sync now --company <slug>` asks the running sync daemon to sync one company right away, without restarting sync. It needs hq-cloud 6.18.68 or later in the daemon; with an older daemon it falls back to the previous restart. `--json` reports which path it took. The desktop app uses this for Sync on the "Added to" banner.
7
21
 
@@ -533,9 +533,11 @@ export function registerCapacityCommand(program) {
533
533
  });
534
534
  if (wantsJson(opts, command)) {
535
535
  console.log(JSON.stringify(result.json ?? loginJson(rows, clock.nowMs, provider), null, 2));
536
+ process.exitCode = result.exitCode;
536
537
  return;
537
538
  }
538
539
  console.log(result.text);
540
+ process.exitCode = result.exitCode;
539
541
  }
540
542
  catch (err) {
541
543
  failCapacity(err);
@@ -3307,6 +3307,7 @@ async function runCreate(ctx, opts) {
3307
3307
  throw new LanesError("no_account_with_room", `No account has room${resetSuffix}`, {
3308
3308
  next_reset: result.next_reset,
3309
3309
  request: result.request,
3310
+ skipped: result.skipped,
3310
3311
  });
3311
3312
  }
3312
3313
  autoChosenProvider = parseLaneProvider(result.pick.provider);
@@ -10,30 +10,76 @@ import * as fs from "node:fs";
10
10
  import * as os from "node:os";
11
11
  import * as path from "node:path";
12
12
  import { createLaneAccountsHandlers } from "../../commands/lanes-accounts.js";
13
+ import { effectiveAccountHome } from "../lanes/account-home.js";
14
+ import { canonicalizeHome, DEFAULT_ACCOUNT_HOMES, loadLanesConfig } from "../lanes/config.js";
13
15
  import { resolveLanesHqRoot } from "../lanes/hq-root.js";
14
16
  import { defaultProbe } from "../lanes/models.js";
17
+ import { parseLaneProvider } from "../lanes/types.js";
15
18
  const ACCOUNT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
16
19
  const RESERVED_ACCOUNT_IDS = new Set(["a", "default"]);
17
20
  export async function loginLaneAccount(input) {
18
- if (!ACCOUNT_ID_PATTERN.test(input.accountId) || RESERVED_ACCOUNT_IDS.has(input.accountId)) {
19
- throw new Error(`Refusing account id ${input.accountId}. Use letters, numbers, dots, underscores, or hyphens, and not a reserved id.`);
21
+ if (!ACCOUNT_ID_PATTERN.test(input.accountId)) {
22
+ throw new Error(`Refusing account id ${input.accountId}. Use letters, numbers, dots, underscores, or hyphens.`);
20
23
  }
21
24
  const hqRoot = input.hqRoot ?? resolveLanesHqRoot({ env: input.env });
25
+ const provider = parseLaneProvider(input.provider);
26
+ const homedir = input.homedir ?? os.homedir;
27
+ const hostname = input.hostname ?? os.hostname();
28
+ const existing = existingAccount(hqRoot, provider, input.accountId, hostname, homedir);
22
29
  let captured;
23
30
  const handlers = createLaneAccountsHandlers(handlerDeps((value) => {
24
31
  captured = value;
25
32
  }));
26
33
  // One registry write. capacityOnly stores enabled:false and the capacity-only
27
34
  // reason in that write, re-checking whether lanes already use the account.
28
- await handlers.runAccountsLogin(loginCtx(input, hqRoot), input.provider, input.accountId, {
35
+ // A registered id signs in again into the home the registry already names.
36
+ await handlers.runAccountsLogin(loginCtx(input, hqRoot), provider, existing?.id ?? input.accountId, {
29
37
  capacityOnly: true,
30
38
  });
31
39
  const account = captured?.account;
32
40
  if (!account?.id || !account.home) {
33
- throw new Error(`lanes accounts login did not register ${input.provider} ${input.accountId}`);
41
+ throw new Error(`lanes accounts login did not register ${provider} ${input.accountId}`);
42
+ }
43
+ if (existing && (account.id !== existing.id || account.home !== existing.home)) {
44
+ throw new Error(`Sign-in for ${provider} ${existing.id} moved it to ${account.id} at ${account.home}; expected ${existing.home}`);
34
45
  }
35
46
  return { id: account.id, home: account.home };
36
47
  }
48
+ /**
49
+ * The registry account an id names, with the home lanes use on this host.
50
+ * `default` names the account registered at the provider's default home.
51
+ * Reserved ids sign in again only when they name a registered account, so a
52
+ * new account never takes `a` or `default`.
53
+ */
54
+ function existingAccount(hqRoot, provider, accountId, hostname, homedir) {
55
+ const accounts = loadLanesConfig(hqRoot).providers[provider].accounts;
56
+ const canonical = (home) => canonicalHome(home, homedir);
57
+ if (accountId === "default") {
58
+ const defaultHome = canonical(DEFAULT_ACCOUNT_HOMES[provider]);
59
+ for (const [id, entry] of Object.entries(accounts)) {
60
+ if (!entry)
61
+ continue;
62
+ const home = canonical(effectiveAccountHome(entry, hostname).home);
63
+ if (home === defaultHome)
64
+ return { id, home };
65
+ }
66
+ throw new Error(`No ${provider} account is registered at the default home. Run hq capacity login ${provider} to add one.`);
67
+ }
68
+ const entry = accounts[accountId];
69
+ if (entry)
70
+ return { id: accountId, home: canonical(effectiveAccountHome(entry, hostname).home) };
71
+ if (RESERVED_ACCOUNT_IDS.has(accountId)) {
72
+ throw new Error(`Refusing account id ${accountId}. It is reserved and no ${provider} account uses it.`);
73
+ }
74
+ return null;
75
+ }
76
+ function canonicalHome(home, homedir) {
77
+ return canonicalizeHome(home, {
78
+ homedir,
79
+ realpathSync: (target) => fs.realpathSync(target),
80
+ existsSync: (target) => fs.existsSync(target),
81
+ });
82
+ }
37
83
  function handlerDeps(printJson) {
38
84
  return {
39
85
  automaticBalanceMoveHistoryReason: () => unused("automaticBalanceMoveHistoryReason"),
@@ -59,8 +105,8 @@ function loginCtx(input, hqRoot) {
59
105
  hostname: () => input.hostname ?? os.hostname(),
60
106
  homedir: input.homedir ?? os.homedir,
61
107
  existsSync: (target) => fs.existsSync(target),
62
- spawnLogin: input.spawnLogin ?? defaultSpawnLogin,
63
- probeProvider: input.probeProvider ?? defaultProbe,
108
+ spawnLogin: defaultHomeEnv(input.spawnLogin ?? defaultSpawnLogin, input.homedir ?? os.homedir),
109
+ probeProvider: defaultHomeProbe(input.probeProvider ?? defaultProbe, input.homedir ?? os.homedir),
64
110
  };
65
111
  return new Proxy(fields, {
66
112
  get(target, prop) {
@@ -72,6 +118,27 @@ function loginCtx(input, hqRoot) {
72
118
  },
73
119
  });
74
120
  }
121
+ /**
122
+ * Claude Code keys its Keychain item on CLAUDE_CONFIG_DIR. The default home
123
+ * keeps the plain `Claude Code-credentials` item only when the variable is
124
+ * unset, so a sign-in into ~/.claude runs without it.
125
+ */
126
+ function defaultHomeEnv(spawnLogin, homedir) {
127
+ return (opts) => spawnLogin({ ...opts, env: withoutDefaultClaudeDir(opts.env, homedir) });
128
+ }
129
+ /** The sign-in check reads the same Keychain item the sign-in wrote. */
130
+ function defaultHomeProbe(probe, homedir) {
131
+ return (command, args, env) => probe(command, args, withoutDefaultClaudeDir(env, homedir));
132
+ }
133
+ function withoutDefaultClaudeDir(env, homedir) {
134
+ const configDir = env.CLAUDE_CONFIG_DIR;
135
+ if (!configDir || canonicalHome(configDir, homedir) !== canonicalHome(DEFAULT_ACCOUNT_HOMES.claude, homedir)) {
136
+ return env;
137
+ }
138
+ const next = { ...env };
139
+ delete next.CLAUDE_CONFIG_DIR;
140
+ return next;
141
+ }
75
142
  function defaultSpawnLogin(opts) {
76
143
  return new Promise((resolve, reject) => {
77
144
  const child = spawn(opts.command, opts.args, {
@@ -1,6 +1,6 @@
1
1
  import type { LaneRecord, LaneState, LanesConfig } from "../lanes/types.js";
2
2
  import { type LaneProvider } from "../lanes/types.js";
3
- import type { NowLimit, NowView } from "./ledger.js";
3
+ import { type NowLimit, type NowView } from "./ledger.js";
4
4
  /** All lane providers, in the stable order boss-route uses for ranking ties. */
5
5
  export declare const ROUTE_PROVIDERS: readonly LaneProvider[];
6
6
  /** Lane states that count against an account's live load. */
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { canonicalizeHome } from "../lanes/config.js";
17
17
  import { LANE_PROVIDERS } from "../lanes/types.js";
18
+ import { capacityLoginCommand } from "./ledger.js";
18
19
  /** All lane providers, in the stable order boss-route uses for ranking ties. */
19
20
  export const ROUTE_PROVIDERS = LANE_PROVIDERS;
20
21
  /** Lane states that count against an account's live load. */
@@ -225,7 +226,10 @@ export function pickAccount(input) {
225
226
  skipped.push({
226
227
  ...base,
227
228
  reason: account.reason ?? `sign-in ${authState}`,
228
- fix: account.fixCommand ?? null,
229
+ // Every signed-out reason (revoked, missing credential, missing
230
+ // scope, auth-failed lane) is fixed by signing the lane account in
231
+ // again, so the fix names the registry id lanes use.
232
+ fix: capacityLoginCommand(providerId, laneMatch.id),
229
233
  });
230
234
  continue;
231
235
  }
@@ -88,6 +88,18 @@ export interface CapacityLoginResult {
88
88
  json?: unknown;
89
89
  }
90
90
  export declare function executeCapacityLogin(request: CapacityLoginRequest, deps: CapacityLoginDeps): Promise<CapacityLoginResult>;
91
+ export interface RepairFixed {
92
+ provider: string;
93
+ label: string;
94
+ account: string;
95
+ home: string;
96
+ }
97
+ export interface RepairSkipped {
98
+ provider: string;
99
+ label: string;
100
+ reason: string;
101
+ fix: string | null;
102
+ }
91
103
  export declare function organizationFor(view: NowView, provider: string, home: string): string | null;
92
104
  export declare function renameCapacityAccount(hqRoot: string, providerRaw: string, fromRaw: string, toRaw: string, options?: {
93
105
  force?: boolean;
@@ -278,8 +278,11 @@ export async function executeCapacityLogin(request, deps) {
278
278
  if (request.account && !request.provider) {
279
279
  throw new SignInUsageError("Pass a provider before --account.");
280
280
  }
281
+ // A reserved id (a, default) is accepted here so a registered account can
282
+ // sign in again into its home. The lane login refuses one that names no
283
+ // registered account.
281
284
  if (request.account)
282
- assertRegistryAccountId(request.account);
285
+ assertAccountIdShape(request.account);
283
286
  const providerFilter = request.provider;
284
287
  const wantsSignIn = Boolean(request.provider || request.account || request.repair);
285
288
  if (!request.interactive && wantsSignIn && !request.json) {
@@ -308,19 +311,69 @@ export async function executeCapacityLogin(request, deps) {
308
311
  }
309
312
  return { exitCode: 0, text: lines.join("\n\n") };
310
313
  }
314
+ /**
315
+ * Signs in again for each signed-out account, one at a time, in roster order.
316
+ * A failed sign-in is reported and the next account still runs. Expired
317
+ * accounts are reported as skipped: opening the account refreshes them.
318
+ */
311
319
  async function runRepair(request, deps) {
312
320
  const plan = repairSelection(deps.rows, request.provider);
313
321
  const lines = [formatRepairPlan(plan)];
322
+ const fixed = [];
323
+ const skipped = [];
314
324
  for (const row of plan.signedOut) {
315
- if (row.label === "default" || RESERVED_IDS.has(row.label)) {
316
- lines.push(`${row.provider} ${row.label}: ${defaultVendorLogin(row.provider)}`);
325
+ const fix = row.command ?? `hq capacity login ${row.provider} --account ${row.label}`;
326
+ let logged;
327
+ try {
328
+ logged = await deps.loginAccount(row.provider, row.label);
329
+ }
330
+ catch (err) {
331
+ skipped.push({ provider: row.provider, label: row.label, reason: errorText(err), fix });
317
332
  continue;
318
333
  }
319
- const logged = await deps.loginAccount(row.provider, row.label);
320
- const view = await deps.readAccount(row.provider, row.label, logged.home);
321
- lines.push(deps.formatReading(view, row.provider, row.label, logged.home));
334
+ fixed.push({ provider: row.provider, label: row.label, account: logged.id, home: logged.home });
335
+ try {
336
+ const view = await deps.readAccount(row.provider, logged.id, logged.home);
337
+ lines.push(deps.formatReading(view, row.provider, row.label, logged.home));
338
+ }
339
+ catch (err) {
340
+ lines.push(`${row.provider} ${row.label}: signed in; usage read failed: ${errorText(err)}`);
341
+ }
322
342
  }
323
- return { exitCode: 0, text: lines.join("\n\n") };
343
+ for (const row of plan.expired) {
344
+ skipped.push({
345
+ provider: row.provider,
346
+ label: row.label,
347
+ reason: row.reason ?? EXPIRED_REASON,
348
+ fix: row.command ? formatRefreshLine(row.provider, row.command) : null,
349
+ });
350
+ }
351
+ lines.push(formatRepairSummary(fixed, skipped));
352
+ const failed = skipped.some((item) => plan.signedOut.some((row) => row.provider === item.provider && row.label === item.label));
353
+ return {
354
+ exitCode: failed ? 1 : 0,
355
+ text: lines.join("\n\n"),
356
+ json: { repair: { fixed, skipped } },
357
+ };
358
+ }
359
+ function formatRepairSummary(fixed, skipped) {
360
+ const lines = ["Fixed:"];
361
+ if (fixed.length === 0)
362
+ lines.push(" none");
363
+ for (const item of fixed)
364
+ lines.push(` ${item.provider} ${item.label}`);
365
+ lines.push("Skipped:");
366
+ if (skipped.length === 0)
367
+ lines.push(" none");
368
+ for (const item of skipped) {
369
+ lines.push(` ${item.provider} ${item.label}: ${item.reason}`);
370
+ if (item.fix)
371
+ lines.push(` ${item.fix}`);
372
+ }
373
+ return lines.join("\n");
374
+ }
375
+ function errorText(err) {
376
+ return err instanceof Error ? err.message : String(err);
324
377
  }
325
378
  async function signInOne(provider, account, deps) {
326
379
  const id = account ?? deps.newAccountId();
@@ -489,20 +542,16 @@ function isAncestor(parent, child) {
489
542
  const relative = path.relative(parent, child);
490
543
  return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
491
544
  }
545
+ function assertAccountIdShape(id) {
546
+ if (!ID_PATTERN.test(id)) {
547
+ throw new SignInUsageError("Account id must start with a letter or number and contain only letters, numbers, dots, underscores, or hyphens.");
548
+ }
549
+ }
492
550
  function assertRegistryAccountId(id) {
493
551
  if (!ID_PATTERN.test(id) || RESERVED_IDS.has(id)) {
494
552
  throw new SignInUsageError("Account id must start with a letter or number, contain only letters, numbers, dots, underscores, or hyphens, and must not be reserved (a, default).");
495
553
  }
496
554
  }
497
- function defaultVendorLogin(provider) {
498
- if (provider === "claude")
499
- return "claude auth login";
500
- if (provider === "codex")
501
- return "codex login";
502
- if (provider === "grok")
503
- return "grok login";
504
- return `hq capacity login ${provider}`;
505
- }
506
555
  function requiredId(value, name) {
507
556
  const id = value.trim();
508
557
  if (!id || id !== value)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.113",
3
+ "version": "5.345.114",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {