@indigoai-us/hq-cli 5.345.91 → 5.345.93

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 (47) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/assets/scaffold/core/scripts/qmd-reindex-bg.sh +17 -4
  3. package/dist/command-catalog.generated.d.ts +252 -0
  4. package/dist/command-catalog.generated.js +306 -0
  5. package/dist/command-registration-plan.d.ts +6 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/capacity.d.ts +71 -0
  8. package/dist/commands/capacity.js +666 -0
  9. package/dist/commands/integrations-connect.js +64 -5
  10. package/dist/commands/lanes-accounts.d.ts +1 -0
  11. package/dist/commands/lanes-accounts.js +6 -0
  12. package/dist/lib/capacity/accounts.d.ts +36 -0
  13. package/dist/lib/capacity/accounts.js +90 -0
  14. package/dist/lib/capacity/collect.d.ts +33 -0
  15. package/dist/lib/capacity/collect.js +164 -0
  16. package/dist/lib/capacity/collectors/claude-oauth.d.ts +145 -0
  17. package/dist/lib/capacity/collectors/claude-oauth.js +1211 -0
  18. package/dist/lib/capacity/collectors/claude-statusline.d.ts +72 -0
  19. package/dist/lib/capacity/collectors/claude-statusline.js +436 -0
  20. package/dist/lib/capacity/collectors/codex.d.ts +120 -0
  21. package/dist/lib/capacity/collectors/codex.js +1134 -0
  22. package/dist/lib/capacity/collectors/grok.d.ts +86 -0
  23. package/dist/lib/capacity/collectors/grok.js +588 -0
  24. package/dist/lib/capacity/lane-login.d.ts +29 -0
  25. package/dist/lib/capacity/lane-login.js +91 -0
  26. package/dist/lib/capacity/ledger.d.ts +399 -0
  27. package/dist/lib/capacity/ledger.js +1441 -0
  28. package/dist/lib/capacity/project.d.ts +12 -0
  29. package/dist/lib/capacity/project.js +264 -0
  30. package/dist/lib/capacity/read.d.ts +62 -0
  31. package/dist/lib/capacity/read.js +264 -0
  32. package/dist/lib/capacity/refusal.d.ts +30 -0
  33. package/dist/lib/capacity/refusal.js +183 -0
  34. package/dist/lib/capacity/scheduler.d.ts +76 -0
  35. package/dist/lib/capacity/scheduler.js +800 -0
  36. package/dist/lib/capacity/sign-in.d.ts +103 -0
  37. package/dist/lib/capacity/sign-in.js +517 -0
  38. package/dist/lib/capacity/widget.d.ts +22 -0
  39. package/dist/lib/capacity/widget.js +418 -0
  40. package/dist/lib/lanes/spawn.js +12 -0
  41. package/dist/lib/mesh/live/daemon/run.d.ts +5 -0
  42. package/dist/lib/mesh/live/daemon/run.js +29 -1
  43. package/dist/lib/search-index/background.d.ts +31 -8
  44. package/dist/lib/search-index/background.js +86 -7
  45. package/dist/lib/search-index/index.d.ts +24 -0
  46. package/dist/lib/search-index/index.js +25 -0
  47. package/package.json +1 -1
@@ -0,0 +1,103 @@
1
+ /**
2
+ * hq capacity login / accounts (US-012).
3
+ *
4
+ * Account homes, vendor login, and registry writes stay in
5
+ * `hq lanes accounts login`. This module decides which accounts to show,
6
+ * which ones are expired rather than signed out, and which registry id to
7
+ * keep after the first usage read. It never refreshes a token.
8
+ */
9
+ import { type CapacityAccount } from "./accounts.js";
10
+ import { type NowView } from "./ledger.js";
11
+ export type SignInState = "signed_in" | "expired" | "signed_out" | "unknown";
12
+ export interface RosterRow {
13
+ provider: string;
14
+ label: string;
15
+ organization: string | null;
16
+ state: SignInState;
17
+ reason: string | null;
18
+ command: string | null;
19
+ observedAt: string | null;
20
+ home: string;
21
+ /** True when the registry account is read for usage and is not used by lanes. */
22
+ usageOnly?: boolean;
23
+ }
24
+ /** Sentence printed for an expired Claude credential. Other commands pass through. */
25
+ export declare function formatRefreshLine(provider: string, command: string): string;
26
+ export declare function laneEnableCommand(provider: string, label: string): string;
27
+ export declare class SignInUsageError extends Error {
28
+ constructor(message: string);
29
+ }
30
+ export declare function formatCredits(value: string | number | null): string;
31
+ export declare function promptsAllowed(input: {
32
+ stdinTty: boolean;
33
+ stdoutTty: boolean;
34
+ json?: boolean;
35
+ }): boolean;
36
+ export declare function organizationSlug(name: string): string | null;
37
+ export declare function uniqueAccountId(base: string, taken: ReadonlySet<string>): string;
38
+ export declare function generateAccountId(randomHex: string): string;
39
+ export declare function addAccountCommands(provider?: string): string[];
40
+ export declare function presentAccount(account: {
41
+ authState?: string;
42
+ signedOut?: boolean;
43
+ reason?: string | null;
44
+ fixCommand?: string | null;
45
+ homes?: string[];
46
+ limits?: unknown[];
47
+ labels?: string[];
48
+ }, provider: string, userHome: string): {
49
+ state: SignInState;
50
+ reason: string | null;
51
+ command: string | null;
52
+ };
53
+ export declare function rosterFrom(accounts: readonly CapacityAccount[], view: NowView, userHome: string): RosterRow[];
54
+ export declare function formatRoster(rows: readonly RosterRow[], nowMs: number): string;
55
+ export declare function formatLoginText(rows: readonly RosterRow[], nowMs: number, extra?: {
56
+ repair?: boolean;
57
+ provider?: string;
58
+ }): string;
59
+ export declare function repairSelection(rows: readonly RosterRow[], provider?: string): {
60
+ signedOut: RosterRow[];
61
+ expired: RosterRow[];
62
+ };
63
+ export declare function loginJson(rows: readonly RosterRow[], nowMs: number, provider?: string): unknown;
64
+ export interface CapacityLoginRequest {
65
+ provider?: string;
66
+ account?: string;
67
+ repair?: boolean;
68
+ json?: boolean;
69
+ interactive: boolean;
70
+ }
71
+ export interface CapacityLoginDeps {
72
+ rows: readonly RosterRow[];
73
+ nowMs: number;
74
+ ask: (question: string) => Promise<boolean>;
75
+ loginAccount: (provider: string, accountId: string) => Promise<{
76
+ id: string;
77
+ home: string;
78
+ }>;
79
+ readAccount: (provider: string, accountId: string, home: string) => Promise<NowView>;
80
+ renameAccount: (provider: string, from: string, to: string) => void;
81
+ takenIds: (provider: string) => string[];
82
+ formatReading: (view: NowView, provider: string, label: string, home: string) => string;
83
+ newAccountId: () => string;
84
+ }
85
+ export interface CapacityLoginResult {
86
+ exitCode: number;
87
+ text: string;
88
+ json?: unknown;
89
+ }
90
+ export declare function executeCapacityLogin(request: CapacityLoginRequest, deps: CapacityLoginDeps): Promise<CapacityLoginResult>;
91
+ export declare function organizationFor(view: NowView, provider: string, home: string): string | null;
92
+ export declare function renameCapacityAccount(hqRoot: string, providerRaw: string, fromRaw: string, toRaw: string, options?: {
93
+ force?: boolean;
94
+ }): void;
95
+ export declare function removeCapacityAccount(hqRoot: string, providerRaw: string, idRaw: string, options?: {
96
+ deleteHome?: boolean;
97
+ force?: boolean;
98
+ hostname?: string;
99
+ homedir?: () => string;
100
+ }): {
101
+ removedHome: boolean;
102
+ };
103
+ //# sourceMappingURL=sign-in.d.ts.map
@@ -0,0 +1,517 @@
1
+ /**
2
+ * hq capacity login / accounts (US-012).
3
+ *
4
+ * Account homes, vendor login, and registry writes stay in
5
+ * `hq lanes accounts login`. This module decides which accounts to show,
6
+ * which ones are expired rather than signed out, and which registry id to
7
+ * keep after the first usage read. It never refreshes a token.
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { defaultProviderHome } from "./accounts.js";
12
+ import { EXPIRED_REASON, claudeOpenCommand, isExpiredReason, } from "./ledger.js";
13
+ import { effectiveAccountHome } from "../lanes/account-home.js";
14
+ import { canonicalizeHome, loadLanesConfig, writeLanesConfig } from "../lanes/config.js";
15
+ import { LANE_PROVIDERS, parseLaneProvider } from "../lanes/types.js";
16
+ import { formatSpan } from "./read.js";
17
+ /** Sentence printed for an expired Claude credential. Other commands pass through. */
18
+ export function formatRefreshLine(provider, command) {
19
+ if (provider === "claude" && (command === "claude" || command.startsWith("CLAUDE_CONFIG_DIR="))) {
20
+ return `Open Claude Code once to refresh: ${command}`;
21
+ }
22
+ return command;
23
+ }
24
+ export function laneEnableCommand(provider, label) {
25
+ return `hq lanes accounts enable ${provider} ${label}`;
26
+ }
27
+ export class SignInUsageError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = "SignInUsageError";
31
+ }
32
+ }
33
+ const RESERVED_IDS = new Set(["a", "default"]);
34
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
35
+ export function formatCredits(value) {
36
+ if (value === null || value === "")
37
+ return "—";
38
+ if (typeof value === "number") {
39
+ if (!Number.isFinite(value))
40
+ return "—";
41
+ return trimDecimals(value);
42
+ }
43
+ const trimmed = value.trim();
44
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed))
45
+ return trimDecimals(Number(trimmed));
46
+ return trimmed;
47
+ }
48
+ function trimDecimals(value) {
49
+ const rounded = Math.round((value + Number.EPSILON) * 100) / 100;
50
+ return rounded.toFixed(2).replace(/\.00$/, "").replace(/(\.\d)0$/, "$1");
51
+ }
52
+ export function promptsAllowed(input) {
53
+ return input.stdinTty && input.stdoutTty && !input.json;
54
+ }
55
+ export function organizationSlug(name) {
56
+ // A domain-shaped organization ("getindigo.ai", from an individual plan)
57
+ // labels the account by its first part: "getindigo".
58
+ const domain = /^([a-z0-9-]+)(\.[a-z0-9-]+)+$/i.exec(name.trim());
59
+ const source = domain ? domain[1] : name;
60
+ const slug = source
61
+ .normalize("NFKD")
62
+ .replace(/[\u0300-\u036f]/g, "")
63
+ .toLowerCase()
64
+ .replace(/[^a-z0-9]+/g, "-")
65
+ .replace(/^-+|-+$/g, "")
66
+ .slice(0, 40);
67
+ if (!slug || !/[a-z0-9]/.test(slug) || RESERVED_IDS.has(slug))
68
+ return null;
69
+ return slug;
70
+ }
71
+ export function uniqueAccountId(base, taken) {
72
+ if (!taken.has(base) && !RESERVED_IDS.has(base))
73
+ return base;
74
+ for (let n = 2; n < 1000; n += 1) {
75
+ const candidate = `${base}-${n}`;
76
+ if (!taken.has(candidate))
77
+ return candidate;
78
+ }
79
+ throw new SignInUsageError(`No free account id near ${base}`);
80
+ }
81
+ export function generateAccountId(randomHex) {
82
+ const hex = randomHex.toLowerCase().replace(/[^a-f0-9]/g, "").slice(0, 8);
83
+ if (hex.length < 4)
84
+ throw new SignInUsageError("Could not generate an account id");
85
+ return `acct-${hex}`;
86
+ }
87
+ export function addAccountCommands(provider) {
88
+ const providers = provider ? [parseLaneProvider(provider)] : [...LANE_PROVIDERS];
89
+ return providers.map((name) => `hq capacity login ${name}`);
90
+ }
91
+ export function presentAccount(account, provider, userHome) {
92
+ const label = account.labels?.find((item) => item !== "default") ?? account.labels?.[0];
93
+ if (account.authState === "expired" || isExpiredReason(account.reason)) {
94
+ const home = account.homes?.[0] ?? null;
95
+ return {
96
+ state: "expired",
97
+ reason: EXPIRED_REASON,
98
+ command: provider === "claude"
99
+ ? claudeOpenCommand(home ?? (label === "default" ? null : home), userHome)
100
+ : account.fixCommand ?? null,
101
+ };
102
+ }
103
+ const signedOut = account.authState === "signed_out"
104
+ || account.signedOut === true
105
+ || isSignedOutReason(account.reason);
106
+ if (signedOut) {
107
+ const reason = account.reason && !isExpiredReason(account.reason)
108
+ ? account.reason
109
+ : "signed out: credential missing";
110
+ return {
111
+ state: "signed_out",
112
+ reason,
113
+ command: account.fixCommand ?? null,
114
+ };
115
+ }
116
+ const hasLimits = Array.isArray(account.limits) && account.limits.length > 0;
117
+ if (account.authState === "ok" || hasLimits) {
118
+ return { state: "signed_in", reason: null, command: null };
119
+ }
120
+ return { state: "unknown", reason: null, command: null };
121
+ }
122
+ function isSignedOutReason(reason) {
123
+ if (!reason || isExpiredReason(reason))
124
+ return false;
125
+ return reason.startsWith("signed out:")
126
+ || reason === "unavailable: auth"
127
+ || reason === "unavailable: missing_scope";
128
+ }
129
+ export function rosterFrom(accounts, view, userHome) {
130
+ const defaultHome = new Map();
131
+ for (const account of accounts) {
132
+ if (account.label === "default")
133
+ defaultHome.set(account.provider, path.resolve(account.home));
134
+ }
135
+ const rows = [];
136
+ for (const account of accounts) {
137
+ const folded = defaultHome.get(account.provider);
138
+ if (account.label !== "default" && folded && path.resolve(account.home) === folded)
139
+ continue;
140
+ const group = view.providers.find((provider) => provider.provider === account.provider);
141
+ const match = matchAccount(group?.accounts, account);
142
+ const presented = match
143
+ ? presentAccount({ ...match, homes: [account.home], labels: [account.label] }, account.provider, userHome)
144
+ : { state: "unknown", reason: null, command: null };
145
+ rows.push({
146
+ provider: account.provider,
147
+ label: account.label,
148
+ organization: match?.organization ?? null,
149
+ state: presented.state,
150
+ reason: presented.reason,
151
+ command: presented.state === "signed_out"
152
+ ? presented.command ?? `hq capacity login ${account.provider} --account ${account.label}`
153
+ : presented.command,
154
+ observedAt: match?.observedAt ?? latestLimitAt(match),
155
+ home: account.home,
156
+ usageOnly: account.lanes === false,
157
+ });
158
+ }
159
+ return rows;
160
+ }
161
+ function matchAccount(accounts, account) {
162
+ if (!accounts)
163
+ return undefined;
164
+ const resolved = path.resolve(account.home);
165
+ return accounts.find((row) => row.labels.includes(account.label))
166
+ ?? accounts.find((row) => row.homes.some((home) => path.resolve(home) === resolved));
167
+ }
168
+ function latestLimitAt(account) {
169
+ if (!account)
170
+ return null;
171
+ let latest = "";
172
+ for (const limit of account.limits) {
173
+ if (limit.observedAt > latest)
174
+ latest = limit.observedAt;
175
+ }
176
+ return latest || null;
177
+ }
178
+ export function formatRoster(rows, nowMs) {
179
+ if (rows.length === 0)
180
+ return "No accounts.";
181
+ const headers = ["PROVIDER", "LABEL", "ORGANIZATION", "STATE", "LAST READING"];
182
+ const body = rows.map((row) => [
183
+ row.provider,
184
+ row.usageOnly ? `${row.label} (usage only)` : row.label,
185
+ row.organization ?? "—",
186
+ row.state,
187
+ readingAge(row.observedAt, nowMs),
188
+ ]);
189
+ const lines = [formatTable(headers, body)];
190
+ for (const row of rows) {
191
+ if (!row.usageOnly)
192
+ continue;
193
+ lines.push("", `${row.provider} ${row.label} usage only`);
194
+ lines.push(laneEnableCommand(row.provider, row.label));
195
+ }
196
+ for (const row of rows) {
197
+ if (row.state !== "expired" && row.state !== "signed_out")
198
+ continue;
199
+ lines.push("", `${row.provider} ${row.label}`, row.reason ?? row.state);
200
+ if (row.command)
201
+ lines.push(formatRefreshLine(row.provider, row.command));
202
+ }
203
+ return lines.join("\n");
204
+ }
205
+ export function formatLoginText(rows, nowMs, extra) {
206
+ const parts = [formatRoster(rows, nowMs)];
207
+ if (extra?.repair) {
208
+ const plan = repairSelection(rows, extra.provider);
209
+ parts.push(formatRepairPlan(plan));
210
+ }
211
+ parts.push(["Add an account:", ...addAccountCommands(extra?.provider)].join("\n"));
212
+ return parts.filter((part) => part.length > 0).join("\n\n");
213
+ }
214
+ export function repairSelection(rows, provider) {
215
+ const scoped = provider ? rows.filter((row) => row.provider === provider) : rows;
216
+ return {
217
+ signedOut: scoped.filter((row) => row.state === "signed_out"),
218
+ expired: scoped.filter((row) => row.state === "expired"),
219
+ };
220
+ }
221
+ function formatRepairPlan(plan) {
222
+ const lines = [];
223
+ if (plan.signedOut.length === 0)
224
+ lines.push("No signed-out accounts to repair.");
225
+ else {
226
+ lines.push("Signed out (login again):");
227
+ for (const row of plan.signedOut) {
228
+ lines.push(row.command ?? `hq capacity login ${row.provider} --account ${row.label}`);
229
+ }
230
+ }
231
+ if (plan.expired.length > 0) {
232
+ lines.push("", "Expired (open the account; login is not repeated):");
233
+ for (const row of plan.expired) {
234
+ lines.push(`${row.provider} ${row.label}`);
235
+ lines.push(row.reason ?? EXPIRED_REASON);
236
+ if (row.command)
237
+ lines.push(formatRefreshLine(row.provider, row.command));
238
+ }
239
+ }
240
+ return lines.join("\n");
241
+ }
242
+ export function loginJson(rows, nowMs, provider) {
243
+ return {
244
+ accounts: rows.map((row) => ({
245
+ provider: row.provider,
246
+ label: row.label,
247
+ organization: row.organization,
248
+ state: row.state,
249
+ reason: row.reason,
250
+ command: row.command ? formatRefreshLine(row.provider, row.command) : null,
251
+ usageOnly: row.usageOnly === true,
252
+ laneCommand: row.usageOnly ? laneEnableCommand(row.provider, row.label) : null,
253
+ lastReadingAge: readingAge(row.observedAt, nowMs),
254
+ })),
255
+ addAccountCommands: addAccountCommands(provider),
256
+ };
257
+ }
258
+ function readingAge(observedAt, nowMs) {
259
+ if (!observedAt)
260
+ return "—";
261
+ const at = Date.parse(observedAt);
262
+ if (Number.isNaN(at))
263
+ return "—";
264
+ return `${formatSpan(Math.floor((nowMs - at) / 1000))} ago`;
265
+ }
266
+ function formatTable(headers, rows) {
267
+ const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)));
268
+ const render = (row) => row.map((entry, index) => entry.padEnd(widths[index] ?? 0)).join(" ");
269
+ const lines = [render(headers)];
270
+ if (rows.length > 0)
271
+ lines.push(render(widths.map((width) => "-".repeat(width))));
272
+ lines.push(...rows.map(render));
273
+ return lines.join("\n");
274
+ }
275
+ export async function executeCapacityLogin(request, deps) {
276
+ if (request.provider)
277
+ parseLaneProvider(request.provider);
278
+ if (request.account && !request.provider) {
279
+ throw new SignInUsageError("Pass a provider before --account.");
280
+ }
281
+ if (request.account)
282
+ assertRegistryAccountId(request.account);
283
+ const providerFilter = request.provider;
284
+ const wantsSignIn = Boolean(request.provider || request.account || request.repair);
285
+ if (!request.interactive && wantsSignIn && !request.json) {
286
+ return { exitCode: 1, text: "sign-in needs an interactive terminal" };
287
+ }
288
+ if (!request.interactive || request.json) {
289
+ return {
290
+ exitCode: 0,
291
+ text: formatLoginText(deps.rows, deps.nowMs, { repair: request.repair, provider: providerFilter }),
292
+ json: loginJson(deps.rows, deps.nowMs, providerFilter),
293
+ };
294
+ }
295
+ if (request.repair) {
296
+ return runRepair(request, deps);
297
+ }
298
+ if (request.provider) {
299
+ const block = await signInOne(request.provider, request.account, deps);
300
+ return { exitCode: 0, text: block };
301
+ }
302
+ const lines = [formatRoster(deps.rows, deps.nowMs)];
303
+ for (const provider of LANE_PROVIDERS) {
304
+ const yes = await deps.ask(`Sign into another ${provider} account?`);
305
+ if (!yes)
306
+ continue;
307
+ lines.push(await signInOne(provider, undefined, deps));
308
+ }
309
+ return { exitCode: 0, text: lines.join("\n\n") };
310
+ }
311
+ async function runRepair(request, deps) {
312
+ const plan = repairSelection(deps.rows, request.provider);
313
+ const lines = [formatRepairPlan(plan)];
314
+ 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)}`);
317
+ continue;
318
+ }
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));
322
+ }
323
+ return { exitCode: 0, text: lines.join("\n\n") };
324
+ }
325
+ async function signInOne(provider, account, deps) {
326
+ const id = account ?? deps.newAccountId();
327
+ const logged = await deps.loginAccount(provider, id);
328
+ const view = await deps.readAccount(provider, logged.id, logged.home);
329
+ let label = logged.id;
330
+ if (!account) {
331
+ const org = organizationFor(view, provider, logged.home);
332
+ const slug = org ? organizationSlug(org) : null;
333
+ if (slug) {
334
+ const taken = new Set([...deps.takenIds(provider), ...RESERVED_IDS]);
335
+ taken.delete(logged.id);
336
+ const next = uniqueAccountId(slug, taken);
337
+ if (next !== logged.id) {
338
+ deps.renameAccount(provider, logged.id, next);
339
+ label = next;
340
+ }
341
+ }
342
+ }
343
+ return deps.formatReading(view, provider, label, logged.home);
344
+ }
345
+ export function organizationFor(view, provider, home) {
346
+ const group = view.providers.find((item) => item.provider === provider);
347
+ const resolved = path.resolve(home);
348
+ const account = group?.accounts?.find((row) => row.homes.some((item) => path.resolve(item) === resolved));
349
+ return account?.organization ?? null;
350
+ }
351
+ export function renameCapacityAccount(hqRoot, providerRaw, fromRaw, toRaw, options = {}) {
352
+ const provider = parseLaneProvider(providerRaw);
353
+ const from = requiredId(fromRaw, "account id");
354
+ const to = requiredId(toRaw, "new id");
355
+ assertRegistryAccountId(to);
356
+ assertNotLiveLane(hqRoot, provider, from, options.force);
357
+ const config = loadLanesConfig(hqRoot);
358
+ const accounts = config.providers[provider].accounts;
359
+ const current = accounts[from];
360
+ if (!current)
361
+ throw new SignInUsageError(unknownAccount(provider, from, accounts));
362
+ if (from === to)
363
+ return;
364
+ if (accounts[to])
365
+ throw new SignInUsageError(`${provider} account ${to} already exists.`);
366
+ accounts[to] = current;
367
+ delete accounts[from];
368
+ if (config.providers[provider].default_account === from) {
369
+ config.providers[provider].default_account = to;
370
+ }
371
+ const preferred = config.providers[provider].preferred_accounts;
372
+ if (preferred) {
373
+ config.providers[provider].preferred_accounts = preferred.map((id) => (id === from ? to : id))
374
+ .filter((id, index, all) => all.indexOf(id) === index);
375
+ }
376
+ writeLanesConfig(hqRoot, config);
377
+ }
378
+ export function removeCapacityAccount(hqRoot, providerRaw, idRaw, options = {}) {
379
+ const provider = parseLaneProvider(providerRaw);
380
+ const id = requiredId(idRaw, "account id");
381
+ const config = loadLanesConfig(hqRoot);
382
+ const accounts = config.providers[provider].accounts;
383
+ const current = accounts[id];
384
+ if (!current)
385
+ throw new SignInUsageError(unknownAccount(provider, id, accounts));
386
+ assertNotLiveLane(hqRoot, provider, id, options.force);
387
+ const hostname = options.hostname ?? "";
388
+ const home = hostname
389
+ ? effectiveAccountHome(current, hostname).home
390
+ : current.home;
391
+ if (options.deleteHome) {
392
+ assertDeletableHome(provider, id, home, options.homedir ?? (() => ""), hqRoot, config);
393
+ fs.rmSync(home, { recursive: true, force: true });
394
+ }
395
+ delete accounts[id];
396
+ if (config.providers[provider].default_account === id)
397
+ delete config.providers[provider].default_account;
398
+ if (config.providers[provider].preferred_accounts) {
399
+ config.providers[provider].preferred_accounts = config.providers[provider].preferred_accounts
400
+ ?.filter((item) => item !== id);
401
+ }
402
+ try {
403
+ writeLanesConfig(hqRoot, config);
404
+ }
405
+ catch (err) {
406
+ if (!options.deleteHome)
407
+ throw err;
408
+ const message = err instanceof Error ? err.message : String(err);
409
+ throw new SignInUsageError(`Deleted the home for ${provider} ${id}, but the registry was not updated: ${message}`);
410
+ }
411
+ return { removedHome: options.deleteHome === true };
412
+ }
413
+ const LIVE_LANE_STATES = new Set(["running", "queued", "blocked", "awaiting_input"]);
414
+ function assertNotLiveLane(hqRoot, provider, id, force) {
415
+ if (force)
416
+ return;
417
+ const dir = path.join(hqRoot, "workspace", "lanes", "lanes");
418
+ if (!fs.existsSync(dir))
419
+ return;
420
+ let names;
421
+ try {
422
+ names = fs.readdirSync(dir);
423
+ }
424
+ catch {
425
+ return;
426
+ }
427
+ for (const name of names) {
428
+ if (!name.endsWith(".json") || name.includes(".conflict-") || name.startsWith("."))
429
+ continue;
430
+ let parsed;
431
+ try {
432
+ parsed = JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
433
+ }
434
+ catch {
435
+ continue;
436
+ }
437
+ if (parsed.provider !== provider || parsed.account !== id)
438
+ continue;
439
+ if (typeof parsed.state !== "string" || !LIVE_LANE_STATES.has(parsed.state))
440
+ continue;
441
+ const laneId = typeof parsed.lane_id === "string" ? parsed.lane_id : name.replace(/\.json$/, "");
442
+ throw new SignInUsageError(`Refusing to change ${provider} ${id}: lane ${laneId} is ${parsed.state}. Pass --force to change it anyway.`);
443
+ }
444
+ }
445
+ function assertDeletableHome(provider, accountId, home, homedir, hqRoot, config) {
446
+ const userHome = homedir();
447
+ if (!userHome)
448
+ throw new SignInUsageError("Refusing to delete an account home without a home directory.");
449
+ const deps = {
450
+ homedir: () => userHome,
451
+ realpathSync: (target) => fs.realpathSync(target),
452
+ existsSync: (target) => fs.existsSync(target),
453
+ };
454
+ const resolved = canonicalizeHome(home, deps);
455
+ const user = canonicalizeHome(userHome, deps);
456
+ if (resolved === user)
457
+ throw new SignInUsageError("Refusing to delete the home directory.");
458
+ const root = canonicalizeHome(hqRoot, deps);
459
+ if (resolved === root || isAncestor(resolved, root)) {
460
+ throw new SignInUsageError("Refusing to delete the HQ root or one of its parents.");
461
+ }
462
+ for (const name of LANE_PROVIDERS) {
463
+ const defaults = canonicalizeHome(defaultProviderHome(name, userHome), deps);
464
+ if (resolved === defaults) {
465
+ throw new SignInUsageError(`Refusing to delete the default ${name} home. Remove the account without --delete-home.`);
466
+ }
467
+ if (isAncestor(resolved, defaults)) {
468
+ throw new SignInUsageError(`Refusing to delete a parent of the default ${name} home.`);
469
+ }
470
+ }
471
+ const expected = canonicalizeHome(path.join(userHome, `.${provider}-${accountId}`), deps);
472
+ if (resolved !== expected) {
473
+ throw new SignInUsageError(`Refusing to delete ${home}. Only ~/.${provider}-${accountId} can be deleted.`);
474
+ }
475
+ for (const other of LANE_PROVIDERS) {
476
+ for (const [id, account] of Object.entries(config.providers[other].accounts)) {
477
+ if (!account)
478
+ continue;
479
+ if (other === provider && id === accountId)
480
+ continue;
481
+ const otherHome = canonicalizeHome(account.home, deps);
482
+ if (otherHome === resolved) {
483
+ throw new SignInUsageError(`Refusing to delete ${home}. Another account still uses that home.`);
484
+ }
485
+ }
486
+ }
487
+ }
488
+ function isAncestor(parent, child) {
489
+ const relative = path.relative(parent, child);
490
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
491
+ }
492
+ function assertRegistryAccountId(id) {
493
+ if (!ID_PATTERN.test(id) || RESERVED_IDS.has(id)) {
494
+ 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
+ }
496
+ }
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
+ function requiredId(value, name) {
507
+ const id = value.trim();
508
+ if (!id || id !== value)
509
+ throw new SignInUsageError(`${name} must be a non-empty id`);
510
+ return id;
511
+ }
512
+ function unknownAccount(provider, id, accounts) {
513
+ const known = Object.keys(accounts);
514
+ const suffix = known.length > 0 ? ` Known: ${known.join(", ")}.` : "";
515
+ return `Unknown ${provider} account ${id}.${suffix}`;
516
+ }
517
+ //# sourceMappingURL=sign-in.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * HTML card for `hq capacity --widget`.
3
+ *
4
+ * Renders the projected now view only. It does not read credentials, homes,
5
+ * or account ids. The clock and time zone are arguments so tests can pin both.
6
+ */
7
+ import type { CapacityAccount } from "./accounts.js";
8
+ import { type NowView } from "./ledger.js";
9
+ export interface CapacityWidgetInput {
10
+ view?: NowView;
11
+ accounts?: readonly CapacityAccount[];
12
+ nowMs: number;
13
+ timeZone: string;
14
+ stale?: boolean;
15
+ /** False when collector.json says the collector is off. */
16
+ collectorEnabled?: boolean | null;
17
+ /** True when the ledger directory or now.json is missing. */
18
+ ledgerMissing?: boolean;
19
+ }
20
+ export declare function capacityWidgetTimeZone(env?: NodeJS.ProcessEnv): string;
21
+ export declare function renderCapacityWidget(input: CapacityWidgetInput): string;
22
+ //# sourceMappingURL=widget.d.ts.map