@indigoai-us/hq-cli 5.345.36 → 5.345.38
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 +11 -0
- package/assets/scaffold/core/scripts/detect-stale-core-policy-mirror.sh +2 -1
- package/dist/command-catalog.generated.d.ts +34 -0
- package/dist/command-catalog.generated.js +40 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/core.js +1 -1
- package/dist/commands/provider.d.ts +7 -0
- package/dist/commands/provider.js +14 -0
- package/dist/lib/core-utils/token-usage-report.js +1 -1
- package/dist/lib/index-render/companies.js +2 -1
- package/dist/lib/lanes/story-ledger.d.ts +1 -1
- package/dist/lib/lanes/story-ledger.js +26 -5
- package/dist/lib/provider-launcher.d.ts +11 -0
- package/dist/lib/provider-launcher.js +59 -0
- package/dist/lib/workers-registry/index.js +28 -3
- package/dist/utils/self-update.js +143 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.38] — 2026-10-07
|
|
6
|
+
- Windows CI parity suites now derive their shell-subprocess time budget from a warm-up measured on the same runner instead of a fixed 7-second deadline.
|
|
7
|
+
|
|
8
|
+
## [5.345.37] — 2026-10-07
|
|
9
|
+
- Keep pnpm's public global package link readable while self-update scans and replaces existing global installs, so Corepack-managed updates can complete with rollback protection intact.
|
|
10
|
+
|
|
11
|
+
- Add published-package parity coverage for native core commands and align help output and symlinked project indexing with the bundled scripts.
|
|
12
|
+
- When Windows blocks writing the derived workers registry, `hq core` now reports a partial result instead of terminating on an uncaught filesystem error.
|
|
13
|
+
- Lane story ledger updates now keep state and status aligned, clear stale single-lane assignments, and use the canonical `review` state.
|
|
14
|
+
- Add `hq provider launch` for noninteractive Claude Code and Codex CLI runs. When either provider reports the known unauthenticated response, the launcher prints the matching sign-in command.
|
|
15
|
+
|
|
5
16
|
## [5.345.36] — 2026-10-07
|
|
6
17
|
- A queued `hq sync` no longer pins the installed `hq` version while it waits its turn, so `hq pkg update` is no longer blocked for the length of somebody else's sync.
|
|
7
18
|
- The flag-registry background refresh no longer throws as it starts, so running any `hq` command stops filing a crash report that nothing in the command caused.
|
|
@@ -44,7 +44,8 @@ case "${1:-}" in
|
|
|
44
44
|
--check) MODE="check" ;;
|
|
45
45
|
--prune-identical) MODE="prune" ;;
|
|
46
46
|
-h|--help)
|
|
47
|
-
|
|
47
|
+
help_end="$(awk 'NR == 1 { next } /^#/ { last = NR; next } /^[[:space:]]*$/ { next } { exit } END { print last }' "$0")"
|
|
48
|
+
sed -n "2,${help_end}p" "$0"
|
|
48
49
|
exit 0 ;;
|
|
49
50
|
*)
|
|
50
51
|
printf 'detect-stale-core-policy-mirror: unknown argument: %s\n' "$1" >&2
|
|
@@ -1286,6 +1286,40 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1286
1286
|
readonly hidden: false;
|
|
1287
1287
|
}];
|
|
1288
1288
|
readonly subcommands: readonly [];
|
|
1289
|
+
}, {
|
|
1290
|
+
readonly name: "provider";
|
|
1291
|
+
readonly description: "Launch supported AI provider CLIs";
|
|
1292
|
+
readonly aliases: readonly [];
|
|
1293
|
+
readonly hidden: false;
|
|
1294
|
+
readonly helpOption: true;
|
|
1295
|
+
readonly helpCommand: true;
|
|
1296
|
+
readonly usage: "[options] [command]";
|
|
1297
|
+
readonly helpText: readonly [];
|
|
1298
|
+
readonly arguments: readonly [];
|
|
1299
|
+
readonly options: readonly [];
|
|
1300
|
+
readonly subcommands: readonly [{
|
|
1301
|
+
readonly name: "launch";
|
|
1302
|
+
readonly description: "Run a noninteractive Claude Code or Codex command with sign-in guidance";
|
|
1303
|
+
readonly aliases: readonly [];
|
|
1304
|
+
readonly hidden: false;
|
|
1305
|
+
readonly helpOption: true;
|
|
1306
|
+
readonly helpCommand: false;
|
|
1307
|
+
readonly usage: "[options] <name> [args...]";
|
|
1308
|
+
readonly helpText: readonly [];
|
|
1309
|
+
readonly arguments: readonly [{
|
|
1310
|
+
readonly name: "name";
|
|
1311
|
+
readonly required: true;
|
|
1312
|
+
readonly variadic: false;
|
|
1313
|
+
readonly description: "";
|
|
1314
|
+
}, {
|
|
1315
|
+
readonly name: "args";
|
|
1316
|
+
readonly required: false;
|
|
1317
|
+
readonly variadic: true;
|
|
1318
|
+
readonly description: "";
|
|
1319
|
+
}];
|
|
1320
|
+
readonly options: readonly [];
|
|
1321
|
+
readonly subcommands: readonly [];
|
|
1322
|
+
}];
|
|
1289
1323
|
}, {
|
|
1290
1324
|
readonly name: "version";
|
|
1291
1325
|
readonly description: "Show HQ, CLI, and latest hq-core versions";
|
|
@@ -1542,6 +1542,46 @@ export const COMMAND_CATALOG = [
|
|
|
1542
1542
|
],
|
|
1543
1543
|
"subcommands": []
|
|
1544
1544
|
},
|
|
1545
|
+
{
|
|
1546
|
+
"name": "provider",
|
|
1547
|
+
"description": "Launch supported AI provider CLIs",
|
|
1548
|
+
"aliases": [],
|
|
1549
|
+
"hidden": false,
|
|
1550
|
+
"helpOption": true,
|
|
1551
|
+
"helpCommand": true,
|
|
1552
|
+
"usage": "[options] [command]",
|
|
1553
|
+
"helpText": [],
|
|
1554
|
+
"arguments": [],
|
|
1555
|
+
"options": [],
|
|
1556
|
+
"subcommands": [
|
|
1557
|
+
{
|
|
1558
|
+
"name": "launch",
|
|
1559
|
+
"description": "Run a noninteractive Claude Code or Codex command with sign-in guidance",
|
|
1560
|
+
"aliases": [],
|
|
1561
|
+
"hidden": false,
|
|
1562
|
+
"helpOption": true,
|
|
1563
|
+
"helpCommand": false,
|
|
1564
|
+
"usage": "[options] <name> [args...]",
|
|
1565
|
+
"helpText": [],
|
|
1566
|
+
"arguments": [
|
|
1567
|
+
{
|
|
1568
|
+
"name": "name",
|
|
1569
|
+
"required": true,
|
|
1570
|
+
"variadic": false,
|
|
1571
|
+
"description": ""
|
|
1572
|
+
},
|
|
1573
|
+
{
|
|
1574
|
+
"name": "args",
|
|
1575
|
+
"required": false,
|
|
1576
|
+
"variadic": true,
|
|
1577
|
+
"description": ""
|
|
1578
|
+
}
|
|
1579
|
+
],
|
|
1580
|
+
"options": [],
|
|
1581
|
+
"subcommands": []
|
|
1582
|
+
}
|
|
1583
|
+
]
|
|
1584
|
+
},
|
|
1545
1585
|
{
|
|
1546
1586
|
"name": "version",
|
|
1547
1587
|
"description": "Show HQ, CLI, and latest hq-core versions",
|
|
@@ -201,6 +201,12 @@ export declare const REGISTRATION_PLAN: readonly [{
|
|
|
201
201
|
readonly parent: "program";
|
|
202
202
|
readonly module: "./commands/link.js";
|
|
203
203
|
readonly exportName: "registerResolveCompanyCommand";
|
|
204
|
+
}, {
|
|
205
|
+
readonly type: "registrar";
|
|
206
|
+
readonly root: "provider";
|
|
207
|
+
readonly parent: "program";
|
|
208
|
+
readonly module: "./commands/provider.js";
|
|
209
|
+
readonly exportName: "registerProviderCommand";
|
|
204
210
|
}, {
|
|
205
211
|
readonly type: "registrar";
|
|
206
212
|
readonly root: "version";
|
|
@@ -36,6 +36,7 @@ export const REGISTRATION_PLAN = [
|
|
|
36
36
|
{ type: "registrar", root: "unlink", parent: "program", module: "./commands/link.js", exportName: "registerUnlinkCommand" },
|
|
37
37
|
{ type: "registrar", root: "uninstall", parent: "program", module: "./commands/install-global.js", exportName: "registerUninstallCommand" },
|
|
38
38
|
{ type: "registrar", root: "resolve-company", parent: "program", module: "./commands/link.js", exportName: "registerResolveCompanyCommand" },
|
|
39
|
+
{ type: "registrar", root: "provider", parent: "program", module: "./commands/provider.js", exportName: "registerProviderCommand" },
|
|
39
40
|
{ type: "registrar", root: "version", parent: "program", module: "./commands/version.js", exportName: "registerVersionCommand" },
|
|
40
41
|
{ type: "registrar", root: "auth", parent: "program", module: "./commands/auth.js", exportName: "registerAuthCommands" },
|
|
41
42
|
{ type: "registrar", root: "secrets", parent: "program", module: "./commands/secrets.js", exportName: "registerSecretsCommand" },
|
package/dist/commands/core.js
CHANGED
|
@@ -532,7 +532,7 @@ export function registerCoreCommands(program) {
|
|
|
532
532
|
? generateWorkersRegistry(hqRoot)
|
|
533
533
|
: scanPackages(hqRoot, { quiet: process.env.HQ_SCAN_QUIET === "1" });
|
|
534
534
|
if (result.status !== 0)
|
|
535
|
-
process.
|
|
535
|
+
process.exitCode = result.status;
|
|
536
536
|
});
|
|
537
537
|
describeCoreCommand(command, {
|
|
538
538
|
execution: "native",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { type ProviderLauncherDependencies } from "../lib/provider-launcher.js";
|
|
3
|
+
export interface ProviderCommandDependencies extends ProviderLauncherDependencies {
|
|
4
|
+
setExitCode?: (code: number) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function registerProviderCommand(program: Command, dependencies?: ProviderCommandDependencies): void;
|
|
7
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { runProviderLauncher } from "../lib/provider-launcher.js";
|
|
2
|
+
export function registerProviderCommand(program, dependencies = {}) {
|
|
3
|
+
program
|
|
4
|
+
.command("provider")
|
|
5
|
+
.description("Launch supported AI provider CLIs")
|
|
6
|
+
.command("launch <name> [args...]")
|
|
7
|
+
.description("Run a noninteractive Claude Code or Codex command with sign-in guidance")
|
|
8
|
+
.allowUnknownOption()
|
|
9
|
+
.action(async (name, args) => {
|
|
10
|
+
const exitCode = await runProviderLauncher(name, args, dependencies);
|
|
11
|
+
(dependencies.setExitCode ?? ((code) => { process.exitCode = code; }))(exitCode);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -164,7 +164,7 @@ export function tokenUsageReport(args = [], options = {}) {
|
|
|
164
164
|
index += 2;
|
|
165
165
|
}
|
|
166
166
|
else if (arg === "-h" || arg === "--help") {
|
|
167
|
-
stdout("token-usage-report.sh — Daily effective-token totals + top burners from Claude Code session JSONLs.\nUsage:\n scripts/token-usage-report.sh # last 7 days, table\n scripts/token-usage-report.sh --last 14 # last 14 days\n scripts/token-usage-report.sh --since 2026-05-01\n scripts/token-usage-report.sh --json # machine-readable\n scripts/token-usage-report.sh --project-dir <path> #
|
|
167
|
+
stdout("token-usage-report.sh — Daily effective-token totals + top burners from Claude Code session JSONLs.\nUsage:\n scripts/token-usage-report.sh # last 7 days, table\n scripts/token-usage-report.sh --last 14 # last 14 days\n scripts/token-usage-report.sh --since 2026-05-01\n scripts/token-usage-report.sh --json # machine-readable\n scripts/token-usage-report.sh --project-dir <path> # read one activity folder\nEffective tokens weighting (matches weekly rate-limit volume):\n input + 5×output + 1.25×cache_creation + 0.1×cache_read\nResolve roots in the same precedence order as the native command. Explicit\nproject directories and environment overrides select one exact activity root;\notherwise include standard, desktop-saved, and discovered Claude roots.\n--- Compare-windows mode (US-011) ---\nFormat: --before YYYY-MM-DD:YYYY-MM-DD --after YYYY-MM-DD:YYYY-MM-DD\n");
|
|
168
168
|
return 0;
|
|
169
169
|
}
|
|
170
170
|
else {
|
|
@@ -15,7 +15,8 @@ function description(root, company, name) {
|
|
|
15
15
|
case "knowledge": return exists(root, `${base}/.git`) ? "Embedded git repo" : `Inline knowledge (${countImmediate(root, base, (n, f) => !isHidden(n) && fs.statSync(f).isDirectory())} subdirs)`;
|
|
16
16
|
case "settings": return `${countImmediate(root, base, (n, f) => !isHidden(n) && fs.statSync(f).isDirectory())} integration(s)`;
|
|
17
17
|
case "policies": return `${countImmediate(root, base, (n) => n.endsWith(".md"))} policy file(s)`;
|
|
18
|
-
|
|
18
|
+
// Match `find -type d`: count actual directories, not symlinks to directories.
|
|
19
|
+
case "projects": return `${countImmediate(root, base, (n, f) => !n.startsWith("_") && !isHidden(n) && fs.lstatSync(f).isDirectory())} project(s)`;
|
|
19
20
|
case "workers": return `${countImmediate(root, base, (n, f) => !isHidden(n) && fs.statSync(f).isDirectory())} worker(s)`;
|
|
20
21
|
case "repos": return `${countImmediate(root, base, (n, f) => { const s = fs.lstatSync(f); return !isHidden(n) && (s.isDirectory() || s.isSymbolicLink()); })} repo link(s)`;
|
|
21
22
|
case "registry": return `Resource registry (${countImmediate(root, `${base}/resources`, (n) => n.endsWith(".yaml"))} resource(s))`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type LaneRecord, type LaneState } from "./types.js";
|
|
2
2
|
export declare const LANE_STORY_LEDGER_FLAG = "lanes.story-ledger";
|
|
3
|
-
declare const STORY_STATES: readonly ["todo", "in_progress", "
|
|
3
|
+
declare const STORY_STATES: readonly ["todo", "in_progress", "review", "waiting", "blocked", "done", "dropped"];
|
|
4
4
|
export interface ProjectStoryLedger {
|
|
5
5
|
projectId: string;
|
|
6
6
|
companySlug?: string;
|
|
@@ -8,8 +8,8 @@ export const LANE_STORY_LEDGER_FLAG = "lanes.story-ledger";
|
|
|
8
8
|
const LOCK_TIMEOUT_MS = 10_000;
|
|
9
9
|
const LOCK_STALE_MS = 60_000;
|
|
10
10
|
const LOCK_RETRY_MS = 10;
|
|
11
|
-
const STORY_STATES = ["todo", "in_progress", "
|
|
12
|
-
const MANAGER_OWNED_STATES = new Set(["
|
|
11
|
+
const STORY_STATES = ["todo", "in_progress", "review", "waiting", "blocked", "done", "dropped"];
|
|
12
|
+
const MANAGER_OWNED_STATES = new Set(["review", "done", "dropped"]);
|
|
13
13
|
const TERMINAL_STATES = new Set(["done", "failed", "interrupted", "cancelled", "killed"]);
|
|
14
14
|
export function laneStoryLedgerEnabled() {
|
|
15
15
|
return resolveProcessFlagGate(LANE_STORY_LEDGER_FLAG, { fallback: false }, () => false);
|
|
@@ -515,7 +515,7 @@ function validPriorQuestions(value, storyId) {
|
|
|
515
515
|
}
|
|
516
516
|
return value;
|
|
517
517
|
}
|
|
518
|
-
function setLaneStatusFields(story, lane, laneStoryStatus) {
|
|
518
|
+
function setLaneStatusFields(story, lane, laneStoryStatus, isCurrentAttribution) {
|
|
519
519
|
const laneStatus = story.lane_status;
|
|
520
520
|
if (laneStatus !== undefined && !isJsonObject(laneStatus)) {
|
|
521
521
|
throw new LanesError("story_ledger_invalid", "lane_status must be an object for story " + String(story.id), { story_id: story.id });
|
|
@@ -569,6 +569,9 @@ function setLaneStatusFields(story, lane, laneStoryStatus) {
|
|
|
569
569
|
else {
|
|
570
570
|
delete story.pending_questions;
|
|
571
571
|
}
|
|
572
|
+
if (story.state === "in_review" || story.state === "verification") {
|
|
573
|
+
story.state = "review";
|
|
574
|
+
}
|
|
572
575
|
if (MANAGER_OWNED_STATES.has(String(story.state))) {
|
|
573
576
|
// A manager decision always wins over lane-derived status.
|
|
574
577
|
}
|
|
@@ -576,7 +579,7 @@ function setLaneStatusFields(story, lane, laneStoryStatus) {
|
|
|
576
579
|
story.state = "blocked";
|
|
577
580
|
}
|
|
578
581
|
else if (laneStoryStatus === "review") {
|
|
579
|
-
story.state = "
|
|
582
|
+
story.state = "review";
|
|
580
583
|
}
|
|
581
584
|
else if (laneStoryStatus === "done") {
|
|
582
585
|
story.state = "done";
|
|
@@ -590,6 +593,22 @@ function setLaneStatusFields(story, lane, laneStoryStatus) {
|
|
|
590
593
|
? previous
|
|
591
594
|
: "in_progress";
|
|
592
595
|
}
|
|
596
|
+
if (typeof story.state === "string" && STORY_STATES.includes(story.state)) {
|
|
597
|
+
story.status = story.state;
|
|
598
|
+
}
|
|
599
|
+
if (story.state === "blocked") {
|
|
600
|
+
const reasons = blockers.map((entry) => {
|
|
601
|
+
const reason = typeof entry.blocked_on === "string" ? entry.blocked_on.trim() : "";
|
|
602
|
+
return reason || `Lane ${String(entry.lane_id)} is blocked`;
|
|
603
|
+
});
|
|
604
|
+
if (reasons.length > 0)
|
|
605
|
+
story.blocked_reason = [...new Set(reasons)].join("\n");
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
delete story.blocked_reason;
|
|
609
|
+
}
|
|
610
|
+
if (!isCurrentAttribution && story.lane === lane.lane_id)
|
|
611
|
+
delete story.lane;
|
|
593
612
|
if (blockers.length > 0)
|
|
594
613
|
story.lane_status = { saved };
|
|
595
614
|
else
|
|
@@ -611,7 +630,7 @@ function updateOneStory(current, ledger, lane, storyId, projectId, questionRefs)
|
|
|
611
630
|
(laneStoriesForLedger(lane).length <= 1 ? lane.last_envelope?.blocked_reason : undefined);
|
|
612
631
|
const laneState = storyLaneState(lane, laneStory?.status, pendingQuestionIds);
|
|
613
632
|
setLaneEntry(story, lane, laneState, laneStory?.status, prLinks, pendingQuestionIds, blockedReason);
|
|
614
|
-
setLaneStatusFields(story, lane, laneStory?.status);
|
|
633
|
+
setLaneStatusFields(story, lane, laneStory?.status, lane.project_id === projectId && lane.story_id === storyId);
|
|
615
634
|
if (prLinks.length > 0) {
|
|
616
635
|
if (story.prs === undefined)
|
|
617
636
|
story.prs = [];
|
|
@@ -695,6 +714,8 @@ function storyLaneState(lane, laneStoryStatus, pendingQuestionIds) {
|
|
|
695
714
|
}
|
|
696
715
|
function storyState(story) {
|
|
697
716
|
const state = story.state;
|
|
717
|
+
if (state === "in_review" || state === "verification")
|
|
718
|
+
return "review";
|
|
698
719
|
if (typeof state === "string" && STORY_STATES.includes(state)) {
|
|
699
720
|
return state;
|
|
700
721
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import type { Writable } from "node:stream";
|
|
3
|
+
export type ProviderName = "claude" | "codex";
|
|
4
|
+
export interface ProviderLauncherDependencies {
|
|
5
|
+
spawn?: typeof nodeSpawn;
|
|
6
|
+
stdout?: Writable;
|
|
7
|
+
stderr?: Writable;
|
|
8
|
+
}
|
|
9
|
+
export declare function providerLoginGuidance(provider: ProviderName, output: string): string | null;
|
|
10
|
+
export declare function runProviderLauncher(providerValue: string, args: readonly string[], dependencies?: ProviderLauncherDependencies): Promise<number>;
|
|
11
|
+
//# sourceMappingURL=provider-launcher.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
export function providerLoginGuidance(provider, output) {
|
|
3
|
+
if (provider === "claude" && /not logged in|please run\s+\/login/i.test(output)) {
|
|
4
|
+
return "Claude Code is not signed in. Sign in with the provider command: claude /login";
|
|
5
|
+
}
|
|
6
|
+
if (provider === "codex" &&
|
|
7
|
+
/(?:http\s*)?401\b/i.test(output) &&
|
|
8
|
+
/missing bearer or basic authentication in header/i.test(output)) {
|
|
9
|
+
return "Codex is not signed in. Sign in with the provider command: codex login";
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
export async function runProviderLauncher(providerValue, args, dependencies = {}) {
|
|
14
|
+
const provider = providerValue;
|
|
15
|
+
if (provider !== "claude" && provider !== "codex") {
|
|
16
|
+
write(dependencies.stderr, `Unsupported provider '${providerValue}'. Choose claude or codex.\n`);
|
|
17
|
+
return 64;
|
|
18
|
+
}
|
|
19
|
+
if (provider === "claude" && !args.some((arg) => arg === "-p" || arg === "--print")) {
|
|
20
|
+
write(dependencies.stderr, "Provider launch supports noninteractive Claude Code commands. Include -p or --print after --.\n");
|
|
21
|
+
return 64;
|
|
22
|
+
}
|
|
23
|
+
if (provider === "codex" && args[0] !== "exec") {
|
|
24
|
+
write(dependencies.stderr, "Provider launch supports noninteractive Codex commands. Start the arguments after -- with exec.\n");
|
|
25
|
+
return 64;
|
|
26
|
+
}
|
|
27
|
+
const child = (dependencies.spawn ?? nodeSpawn)(provider, [...args], {
|
|
28
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
29
|
+
windowsHide: true,
|
|
30
|
+
});
|
|
31
|
+
let outputTail = "";
|
|
32
|
+
const capture = (stream, destination) => {
|
|
33
|
+
if (!stream)
|
|
34
|
+
return;
|
|
35
|
+
stream.on("data", (chunk) => {
|
|
36
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
|
|
37
|
+
outputTail = `${outputTail}${text}`.slice(-16_384);
|
|
38
|
+
destination?.write(text);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
capture(child.stdout, dependencies.stdout ?? process.stdout);
|
|
42
|
+
capture(child.stderr, dependencies.stderr ?? process.stderr);
|
|
43
|
+
return await new Promise((resolve) => {
|
|
44
|
+
child.once("error", () => {
|
|
45
|
+
write(dependencies.stderr, `Could not start ${provider === "claude" ? "Claude Code" : "Codex"}. Confirm the CLI is installed and available on PATH.\n`);
|
|
46
|
+
resolve(127);
|
|
47
|
+
});
|
|
48
|
+
child.once("close", (code) => {
|
|
49
|
+
const guidance = providerLoginGuidance(provider, outputTail);
|
|
50
|
+
if (guidance)
|
|
51
|
+
write(dependencies.stderr, `${guidance}\n`);
|
|
52
|
+
resolve(code ?? 1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function write(stream, message) {
|
|
57
|
+
(stream ?? process.stderr).write(message);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=provider-launcher.js.map
|
|
@@ -241,18 +241,43 @@ export function generateWorkersRegistry(hqRoot, options = {}) {
|
|
|
241
241
|
const output = registryText(options.now ?? new Date(), kept);
|
|
242
242
|
const registry = path.join(hqRoot, 'core/workers/registry.yaml');
|
|
243
243
|
let written = true;
|
|
244
|
+
let writeFailed = false;
|
|
244
245
|
if (fs.existsSync(registry) && withoutTimestamp(fs.readFileSync(registry, 'utf8')) === withoutTimestamp(output))
|
|
245
246
|
written = false;
|
|
246
247
|
if (written) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
try {
|
|
249
|
+
fs.mkdirSync(path.dirname(registry), { recursive: true });
|
|
250
|
+
fs.writeFileSync(registry, output);
|
|
251
|
+
log('generate-workers-registry: wrote core/workers/registry.yaml');
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
writeFailed = true;
|
|
255
|
+
written = false;
|
|
256
|
+
const code = error instanceof Error && 'code' in error && typeof error.code === 'string'
|
|
257
|
+
? error.code
|
|
258
|
+
: 'unknown';
|
|
259
|
+
log(`generate-workers-registry: WARNING unable to write ${registry} (${code}) — grant write access to the registry path and re-run; a later run will rewrite the derived file once access is restored.`);
|
|
260
|
+
void import('../../sentry.js')
|
|
261
|
+
.then(({ Sentry }) => Sentry.captureMessage('generate-workers-registry: registry write failed; partial result returned', {
|
|
262
|
+
level: 'warning',
|
|
263
|
+
fingerprint: ['workers-registry', 'write-failed'],
|
|
264
|
+
tags: { registry_write_error: code },
|
|
265
|
+
}))
|
|
266
|
+
.catch(() => {
|
|
267
|
+
log('generate-workers-registry: WARNING unable to send the registry write failure report to Sentry.');
|
|
268
|
+
});
|
|
269
|
+
}
|
|
250
270
|
}
|
|
251
271
|
else {
|
|
252
272
|
log('generate-workers-registry: core/workers/registry.yaml unchanged');
|
|
253
273
|
}
|
|
254
274
|
if (quarantined > 0) {
|
|
255
275
|
log(`generate-workers-registry: wrote registry for all VALID workers; quarantined ${quarantined} problem(s) above (loud + partial, never a silent total block). Fix the reported worker.yaml file(s) and re-run to register them.`);
|
|
276
|
+
}
|
|
277
|
+
if (writeFailed) {
|
|
278
|
+
log('generate-workers-registry: registry was not written; the command completed with a partial result.');
|
|
279
|
+
}
|
|
280
|
+
if (quarantined > 0 || writeFailed) {
|
|
256
281
|
return { status: 1, written, quarantined };
|
|
257
282
|
}
|
|
258
283
|
return { status: 0, written, quarantined };
|
|
@@ -79,10 +79,11 @@ export const REEXEC_GUARD_ENV = UPDATE_RECURSION_GUARD_ENV;
|
|
|
79
79
|
const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(CLI_NAME)}/latest`;
|
|
80
80
|
const FETCH_TIMEOUT_MS = 3_000;
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
82
|
+
* Back up the working CLI package before asking the package manager to replace
|
|
83
|
+
* it. If the backup is unavailable, the caller proceeds without rollback
|
|
84
|
+
* protection. pnpm's .pnpm package tree is immutable store content, so back up
|
|
85
|
+
* its selecting public link while leaving that link available for pnpm's scan
|
|
86
|
+
* of existing global installs.
|
|
86
87
|
*/
|
|
87
88
|
function findPnpmPublicLink(packageRoot) {
|
|
88
89
|
const home = derivePnpmHome({ manager: "pnpm", prefix: null, packageRoot });
|
|
@@ -187,6 +188,114 @@ function snapshotNpmBinLinks(prefix, packageRoot) {
|
|
|
187
188
|
}
|
|
188
189
|
return links;
|
|
189
190
|
}
|
|
191
|
+
function snapshotPnpmBinShims(pnpmHome, packageRoot) {
|
|
192
|
+
const binMap = readNpmBinMap(packageRoot);
|
|
193
|
+
if (!binMap)
|
|
194
|
+
return [];
|
|
195
|
+
const binDirectory = path.join(pnpmHome, "bin");
|
|
196
|
+
const shims = [];
|
|
197
|
+
for (const name of Object.keys(binMap)) {
|
|
198
|
+
for (const suffix of ["", ".cmd", ".ps1"]) {
|
|
199
|
+
const shimPath = path.join(binDirectory, `${name}${suffix}`);
|
|
200
|
+
try {
|
|
201
|
+
const stat = fs.lstatSync(shimPath);
|
|
202
|
+
if (!stat.isFile())
|
|
203
|
+
continue;
|
|
204
|
+
const content = fs.readFileSync(shimPath);
|
|
205
|
+
if (!pnpmShimTargetsPackage(content.toString("utf8"), packageRoot))
|
|
206
|
+
continue;
|
|
207
|
+
shims.push({ path: shimPath, content, mode: stat.mode & 0o777 });
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// A missing or unreadable command shim cannot be safely snapshotted.
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return shims;
|
|
215
|
+
}
|
|
216
|
+
function pnpmShimTarget(content) {
|
|
217
|
+
const match = content.match(/^# cmd-shim-target=(.+)$/m);
|
|
218
|
+
return match?.[1]?.trim() || null;
|
|
219
|
+
}
|
|
220
|
+
function pnpmShimTargetsPackage(content, packageRoot) {
|
|
221
|
+
const target = pnpmShimTarget(content);
|
|
222
|
+
if (!target)
|
|
223
|
+
return false;
|
|
224
|
+
try {
|
|
225
|
+
return isPathInside(fs.realpathSync(target), packageRoot);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function pnpmShimTargetsGlobalInstall(content, binDirectory) {
|
|
232
|
+
const target = pnpmShimTarget(content);
|
|
233
|
+
if (!target)
|
|
234
|
+
return false;
|
|
235
|
+
return isPathInside(path.resolve(target), path.join(path.dirname(binDirectory), "global"));
|
|
236
|
+
}
|
|
237
|
+
function restorePnpmBinShims(snapshot) {
|
|
238
|
+
if (!snapshot.pnpmBinDirectory || !snapshot.pnpmBinPackageRoot || !snapshot.pnpmBinShims?.length)
|
|
239
|
+
return true;
|
|
240
|
+
let restored = true;
|
|
241
|
+
for (const shim of snapshot.pnpmBinShims) {
|
|
242
|
+
if (path.dirname(shim.path) !== snapshot.pnpmBinDirectory) {
|
|
243
|
+
reportBinLinkConflict(shim.path, "the saved pnpm shim path is outside the pnpm bin directory");
|
|
244
|
+
restored = false;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
let existing;
|
|
248
|
+
try {
|
|
249
|
+
existing = fs.lstatSync(shim.path);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (error.code !== "ENOENT") {
|
|
253
|
+
reportBinLinkConflict(shim.path, errorMessage(error));
|
|
254
|
+
restored = false;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (existing) {
|
|
259
|
+
if (!existing.isFile() || existing.isSymbolicLink()) {
|
|
260
|
+
reportBinLinkConflict(shim.path, "another entry owns this command name");
|
|
261
|
+
restored = false;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const currentText = fs.readFileSync(shim.path, "utf8");
|
|
265
|
+
if (!pnpmShimTargetsGlobalInstall(currentText, snapshot.pnpmBinDirectory)) {
|
|
266
|
+
reportBinLinkConflict(shim.path, "the existing file is not a pnpm global command shim");
|
|
267
|
+
restored = false;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const temporary = `${shim.path}.hq-cli-restore-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
272
|
+
try {
|
|
273
|
+
fs.mkdirSync(snapshot.pnpmBinDirectory, { recursive: true });
|
|
274
|
+
fs.writeFileSync(temporary, shim.content, { mode: shim.mode, flag: "wx" });
|
|
275
|
+
if (!existing) {
|
|
276
|
+
fs.linkSync(temporary, shim.path);
|
|
277
|
+
fs.rmSync(temporary, { force: true });
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
const current = fs.lstatSync(shim.path);
|
|
281
|
+
if (!current.isFile() || current.isSymbolicLink()) {
|
|
282
|
+
throw new Error("the pnpm shim changed while it was being restored");
|
|
283
|
+
}
|
|
284
|
+
const currentText = fs.readFileSync(shim.path, "utf8");
|
|
285
|
+
if (!pnpmShimTargetsGlobalInstall(currentText, snapshot.pnpmBinDirectory)) {
|
|
286
|
+
throw new Error("the pnpm shim changed while it was being restored");
|
|
287
|
+
}
|
|
288
|
+
fs.renameSync(temporary, shim.path);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
fs.rmSync(temporary, { force: true });
|
|
293
|
+
reportBinLinkConflict(shim.path, errorMessage(error));
|
|
294
|
+
restored = false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return restored;
|
|
298
|
+
}
|
|
190
299
|
function restoreNpmBinLinks(snapshot) {
|
|
191
300
|
if (!snapshot.npmBinDirectory || !snapshot.npmBinPackageRoot || !snapshot.npmBinLinks?.length)
|
|
192
301
|
return true;
|
|
@@ -391,12 +500,16 @@ function snapshotPathsAreSafe(snapshot) {
|
|
|
391
500
|
snapshot.resolvedTarget,
|
|
392
501
|
snapshot.publicLinkPath,
|
|
393
502
|
snapshot.npmBinPackageRoot,
|
|
503
|
+
snapshot.pnpmBinPackageRoot,
|
|
394
504
|
].filter((value) => value !== undefined);
|
|
395
505
|
if (!paths.every(isInsideNodeModules))
|
|
396
506
|
return false;
|
|
397
507
|
if (snapshot.npmBinLinks?.some((bin) => !snapshot.npmBinDirectory || path.dirname(bin.path) !== snapshot.npmBinDirectory ||
|
|
398
508
|
!isPathInside(bin.path, snapshot.npmBinDirectory)))
|
|
399
509
|
return false;
|
|
510
|
+
if (snapshot.pnpmBinShims?.some((shim) => !snapshot.pnpmBinDirectory || path.dirname(shim.path) !== snapshot.pnpmBinDirectory ||
|
|
511
|
+
!isPathInside(shim.path, snapshot.pnpmBinDirectory)))
|
|
512
|
+
return false;
|
|
400
513
|
return true;
|
|
401
514
|
}
|
|
402
515
|
function snapshotInstall(install, renamePath = fs.renameSync) {
|
|
@@ -406,6 +519,7 @@ function snapshotInstall(install, renamePath = fs.renameSync) {
|
|
|
406
519
|
let movedPath;
|
|
407
520
|
let backupPath;
|
|
408
521
|
let didMove = false;
|
|
522
|
+
let copiedPnpmLink = false;
|
|
409
523
|
try {
|
|
410
524
|
const entry = fs.lstatSync(packageRoot);
|
|
411
525
|
let sourcePath = packageRoot;
|
|
@@ -440,9 +554,25 @@ function snapshotInstall(install, renamePath = fs.renameSync) {
|
|
|
440
554
|
const npmBinLinks = backupKind === "package" && install.manager === "npm" && install.prefix
|
|
441
555
|
? snapshotNpmBinLinks(install.prefix, sourcePath)
|
|
442
556
|
: undefined;
|
|
557
|
+
const pnpmHome = backupKind === "pnpm-public-link"
|
|
558
|
+
? derivePnpmHome({ manager: "pnpm", prefix: null, packageRoot: sourcePath }) ?? undefined
|
|
559
|
+
: undefined;
|
|
560
|
+
const pnpmBinShims = pnpmHome ? snapshotPnpmBinShims(pnpmHome, sourcePath) : undefined;
|
|
443
561
|
movedPath = backupKind === "pnpm-public-link" ? pnpmPublicLinkPath : sourcePath;
|
|
444
562
|
backupPath = path.join(path.dirname(movedPath), `.hq-cli-backup-${process.pid}-${randomBytes(8).toString("hex")}`);
|
|
445
|
-
|
|
563
|
+
if (backupKind === "pnpm-public-link") {
|
|
564
|
+
// `pnpm add -g` scans every existing global package before it can update
|
|
565
|
+
// this one. Moving the active link makes its package.json disappear
|
|
566
|
+
// during that scan, so preserve a duplicate link for rollback instead.
|
|
567
|
+
const backupTarget = publicLink?.target ?? resolvedTarget ?? symlinkTarget;
|
|
568
|
+
if (!backupTarget)
|
|
569
|
+
return null;
|
|
570
|
+
fs.symlinkSync(backupTarget, backupPath, process.platform === "win32" ? "junction" : "dir");
|
|
571
|
+
copiedPnpmLink = true;
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
renamePath(movedPath, backupPath);
|
|
575
|
+
}
|
|
446
576
|
didMove = true;
|
|
447
577
|
return {
|
|
448
578
|
installPath: packageRoot,
|
|
@@ -458,12 +588,18 @@ function snapshotInstall(install, renamePath = fs.renameSync) {
|
|
|
458
588
|
npmBinDirectory: npmBinLinks ? path.join(install.prefix, "bin") : undefined,
|
|
459
589
|
npmBinPackageRoot: npmBinLinks ? sourcePath : undefined,
|
|
460
590
|
npmBinLinks,
|
|
591
|
+
pnpmBinDirectory: pnpmBinShims?.length ? path.join(pnpmHome, "bin") : undefined,
|
|
592
|
+
pnpmBinPackageRoot: pnpmBinShims?.length ? sourcePath : undefined,
|
|
593
|
+
pnpmBinShims,
|
|
461
594
|
};
|
|
462
595
|
}
|
|
463
596
|
catch {
|
|
464
597
|
if (didMove && movedPath && backupPath) {
|
|
465
598
|
try {
|
|
466
|
-
|
|
599
|
+
if (copiedPnpmLink)
|
|
600
|
+
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
601
|
+
else
|
|
602
|
+
fs.renameSync(backupPath, movedPath);
|
|
467
603
|
}
|
|
468
604
|
catch {
|
|
469
605
|
// The update may proceed without rollback if the move cannot be undone.
|
|
@@ -483,7 +619,7 @@ function restoreInstall(snapshot, renamePath = fs.renameSync) {
|
|
|
483
619
|
fs.rmSync(publicLinkPath, { recursive: true, force: true });
|
|
484
620
|
fs.mkdirSync(path.dirname(publicLinkPath), { recursive: true });
|
|
485
621
|
renamePath(snapshot.backupPath, publicLinkPath);
|
|
486
|
-
return
|
|
622
|
+
return restorePnpmBinShims(snapshot);
|
|
487
623
|
}
|
|
488
624
|
fs.rmSync(snapshot.installPath, { recursive: true, force: true });
|
|
489
625
|
if (snapshot.symlinkTarget !== undefined && snapshot.resolvedTarget !== undefined) {
|