@indigoai-us/hq-cli 5.323.0 → 5.325.0
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 -1
- package/dist/bin/hq-auth-refresh.d.ts +5 -1
- package/dist/bin/hq-auth-refresh.js +33 -1
- package/dist/command-catalog.generated.d.ts +125 -6
- package/dist/command-catalog.generated.js +162 -6
- package/dist/command-registration-plan.d.ts +24 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/anywhere-runtime-flag.d.ts +9 -0
- package/dist/commands/anywhere-runtime-flag.js +22 -0
- package/dist/commands/core-derive-trigger-facts-native.js +37 -3
- package/dist/commands/daemon.js +39 -2
- package/dist/commands/db-provision.js +6 -1
- package/dist/commands/doctor.js +8 -4
- package/dist/commands/feedback.d.ts +2 -0
- package/dist/commands/feedback.js +10 -0
- package/dist/commands/install-global.d.ts +46 -0
- package/dist/commands/install-global.js +674 -0
- package/dist/commands/link.d.ts +30 -0
- package/dist/commands/link.js +184 -0
- package/dist/commands/mcp-status.d.ts +6 -0
- package/dist/commands/mcp-status.js +21 -4
- package/dist/commands/pkg-install.js +20 -1
- package/dist/commands/update.d.ts +19 -0
- package/dist/commands/update.js +52 -6
- package/dist/fast-core.d.ts +2 -1
- package/dist/fast-core.js +12 -8
- package/dist/index.js +9 -7
- package/dist/lib/core-utils/timeout-guard-native-flags.d.ts +6 -0
- package/dist/lib/core-utils/timeout-guard-native-flags.js +30 -0
- package/dist/lib/core-utils/timeout-guard-native.d.ts +0 -3
- package/dist/lib/core-utils/timeout-guard-native.js +4 -30
- package/dist/lib/core-utils/timeout-guard.d.ts +2 -0
- package/dist/lib/core-utils/timeout-guard.js +2 -1
- package/dist/lib/daemon/convergence.d.ts +42 -0
- package/dist/lib/daemon/convergence.js +66 -0
- package/dist/lib/daemon/heartbeat.d.ts +43 -0
- package/dist/lib/daemon/heartbeat.js +70 -0
- package/dist/lib/daemon/ops/journal.d.ts +29 -0
- package/dist/lib/daemon/ops/journal.js +141 -0
- package/dist/lib/daemon/ops/memory.d.ts +20 -0
- package/dist/lib/daemon/ops/memory.js +65 -0
- package/dist/lib/daemon/ops/mesh.d.ts +38 -0
- package/dist/lib/daemon/ops/mesh.js +70 -0
- package/dist/lib/daemon/ops/policy.d.ts +78 -0
- package/dist/lib/daemon/ops/policy.js +283 -0
- package/dist/lib/daemon/protocol.d.ts +68 -0
- package/dist/lib/daemon/protocol.js +154 -0
- package/dist/lib/daemon/run.d.ts +10 -0
- package/dist/lib/daemon/run.js +85 -3
- package/dist/lib/daemon/server.d.ts +36 -0
- package/dist/lib/daemon/server.js +306 -0
- package/dist/lib/daemon/session-registry.d.ts +30 -0
- package/dist/lib/daemon/session-registry.js +42 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +38 -0
- package/dist/lib/doctor/checks/anywhere.js +182 -0
- package/dist/lib/doctor/registry.js +6 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/install/claude-targets.d.ts +47 -0
- package/dist/lib/install/claude-targets.js +203 -0
- package/dist/lib/install/codex-targets.d.ts +57 -0
- package/dist/lib/install/codex-targets.js +187 -0
- package/dist/lib/mesh/live/session-seq.js +62 -9
- package/dist/lib/registry/normalize-remote.d.ts +21 -0
- package/dist/lib/registry/normalize-remote.js +68 -0
- package/dist/lib/registry/registry.d.ts +75 -0
- package/dist/lib/registry/registry.js +261 -0
- package/dist/utils/cognito-session.d.ts +5 -1
- package/dist/utils/cognito-session.js +8 -12
- package/dist/utils/feedback-diagnostics.d.ts +2 -0
- package/dist/utils/version-check.d.ts +2 -0
- package/dist/utils/version-check.js +7 -0
- package/package.json +2 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq link / hq unlink / hq resolve-company — repo-to-company registry
|
|
3
|
+
* commands (hq-anywhere US-004). The registry lives at ~/.hq/registry.json;
|
|
4
|
+
* see src/lib/registry/registry.ts for the key and precedence rules.
|
|
5
|
+
*/
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
export interface MembershipCompany {
|
|
8
|
+
companyUid: string;
|
|
9
|
+
companySlug: string;
|
|
10
|
+
}
|
|
11
|
+
export interface LinkDeps {
|
|
12
|
+
/** Registry file path; tests point this at a temp dir. */
|
|
13
|
+
file?: string;
|
|
14
|
+
/** Active memberships for the signed-in user (reuses GET /membership/me). */
|
|
15
|
+
listMemberships?: () => Promise<MembershipCompany[]>;
|
|
16
|
+
now?: () => Date;
|
|
17
|
+
}
|
|
18
|
+
/** Link `dir` to `company` after confirming the caller is an active member. */
|
|
19
|
+
export declare function linkFolder(company: string, dir: string | undefined, deps?: LinkDeps): Promise<{
|
|
20
|
+
key: string;
|
|
21
|
+
company: string;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function unlinkFolder(dir: string | undefined, deps?: LinkDeps): string[];
|
|
24
|
+
export declare function resolveCompany(dir: string | undefined, deps?: LinkDeps): import("../lib/registry/registry.js").ResolveResult;
|
|
25
|
+
/** Seed manifest-sourced entries; called by `hq install --global` (US-005). */
|
|
26
|
+
export declare function seedRegistryFromHqRoot(hqRoot: string, deps?: LinkDeps): import("../lib/registry/registry.js").SeedResult;
|
|
27
|
+
export declare function registerLinkCommand(program: Command): void;
|
|
28
|
+
export declare function registerUnlinkCommand(program: Command): void;
|
|
29
|
+
export declare function registerResolveCompanyCommand(program: Command): void;
|
|
30
|
+
//# sourceMappingURL=link.d.ts.map
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq link / hq unlink / hq resolve-company — repo-to-company registry
|
|
3
|
+
* commands (hq-anywhere US-004). The registry lives at ~/.hq/registry.json;
|
|
4
|
+
* see src/lib/registry/registry.ts for the key and precedence rules.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { Option } from "commander";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
11
|
+
import { listActiveMembershipCompanies } from "../lib/mesh/api.js";
|
|
12
|
+
import { requireAnywhereRuntime } from "./anywhere-runtime-flag.js";
|
|
13
|
+
import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
|
|
14
|
+
import { canonicalDir, keyForDir, readRegistry, registryDir, registryFile, removeEntriesForDir, resolveCompanyForDir, seedFromManifest, setEntry, writeRegistry, } from "../lib/registry/registry.js";
|
|
15
|
+
async function defaultListMemberships() {
|
|
16
|
+
const token = await ensureCognitoToken({ interactive: false });
|
|
17
|
+
return listActiveMembershipCompanies(token);
|
|
18
|
+
}
|
|
19
|
+
function targetDir(dir) {
|
|
20
|
+
const resolved = path.resolve(dir?.trim() || process.cwd());
|
|
21
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
|
|
22
|
+
throw Object.assign(new Error(`Not a directory: ${resolved}`), { expected: true });
|
|
23
|
+
}
|
|
24
|
+
return canonicalDir(resolved);
|
|
25
|
+
}
|
|
26
|
+
/** Link `dir` to `company` after confirming the caller is an active member. */
|
|
27
|
+
export async function linkFolder(company, dir, deps = {}) {
|
|
28
|
+
const slug = company.trim();
|
|
29
|
+
if (!slug)
|
|
30
|
+
throw Object.assign(new Error("Company slug is required."), { expected: true });
|
|
31
|
+
const folder = targetDir(dir);
|
|
32
|
+
const memberships = await (deps.listMemberships ?? defaultListMemberships)();
|
|
33
|
+
const match = memberships.find((m) => m.companySlug.toLowerCase() === slug.toLowerCase() || m.companyUid === slug);
|
|
34
|
+
if (!match) {
|
|
35
|
+
throw Object.assign(new Error(`You are not an active member of '${slug}'. Check \`hq whoami\` and the company slug.`), { expected: true });
|
|
36
|
+
}
|
|
37
|
+
const file = deps.file ?? registryFile();
|
|
38
|
+
const registry = readRegistry(file);
|
|
39
|
+
const key = keyForDir(folder);
|
|
40
|
+
setEntry(registry, key, match.companySlug, "link", deps.now?.() ?? new Date());
|
|
41
|
+
writeRegistry(registry, file);
|
|
42
|
+
return { key, company: match.companySlug };
|
|
43
|
+
}
|
|
44
|
+
export function unlinkFolder(dir, deps = {}) {
|
|
45
|
+
const file = deps.file ?? registryFile();
|
|
46
|
+
const registry = readRegistry(file);
|
|
47
|
+
const removed = removeEntriesForDir(registry, targetDir(dir));
|
|
48
|
+
if (removed.length > 0)
|
|
49
|
+
writeRegistry(registry, file);
|
|
50
|
+
return removed;
|
|
51
|
+
}
|
|
52
|
+
export function resolveCompany(dir, deps = {}) {
|
|
53
|
+
const folder = path.resolve(dir?.trim() || process.cwd());
|
|
54
|
+
let registry;
|
|
55
|
+
try {
|
|
56
|
+
registry = readRegistry(deps.file ?? registryFile());
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return { company: null, reason: err.message };
|
|
60
|
+
}
|
|
61
|
+
return resolveCompanyForDir(registry, folder);
|
|
62
|
+
}
|
|
63
|
+
/** Seed manifest-sourced entries; called by `hq install --global` (US-005). */
|
|
64
|
+
export function seedRegistryFromHqRoot(hqRoot, deps = {}) {
|
|
65
|
+
const companies = readCompaniesManifestMap(hqRoot);
|
|
66
|
+
if (!companies) {
|
|
67
|
+
throw Object.assign(new Error(`No companies/manifest.yaml under ${hqRoot}`), {
|
|
68
|
+
expected: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const file = deps.file ?? registryFile();
|
|
72
|
+
const registry = readRegistry(file);
|
|
73
|
+
const result = seedFromManifest(registry, path.resolve(hqRoot), companies, deps.now?.() ?? new Date());
|
|
74
|
+
writeRegistry(registry, file);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
function printTable(registry) {
|
|
78
|
+
const rows = Object.entries(registry.entries).sort(([a], [b]) => a.localeCompare(b));
|
|
79
|
+
if (rows.length === 0) {
|
|
80
|
+
console.log(chalk.dim(`No linked folders. (${registryFile(registryDir())})`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const width = Math.max(...rows.map(([k]) => k.length), 3);
|
|
84
|
+
console.log(`${"KEY".padEnd(width)} ${"COMPANY".padEnd(20)} ${"SOURCE".padEnd(8)} LINKED AT`);
|
|
85
|
+
for (const [key, e] of rows) {
|
|
86
|
+
console.log(`${key.padEnd(width)} ${e.company.padEnd(20)} ${e.source.padEnd(8)} ${e.linkedAt}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function fail(err) {
|
|
90
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
}
|
|
93
|
+
export function registerLinkCommand(program) {
|
|
94
|
+
program
|
|
95
|
+
.command("link [company]")
|
|
96
|
+
.description("Link this folder (or its git remote) to an HQ company")
|
|
97
|
+
.option("--path <dir>", "Folder to link (default: current directory)")
|
|
98
|
+
.option("--list", "Print every linked folder")
|
|
99
|
+
.option("--json", "Print JSON output")
|
|
100
|
+
.addOption(new Option("--seed-manifest <hqRoot>", "Seed entries from an HQ companies/manifest.yaml").hideHelp())
|
|
101
|
+
.action(async (company, opts) => {
|
|
102
|
+
try {
|
|
103
|
+
requireAnywhereRuntime();
|
|
104
|
+
if (opts.list) {
|
|
105
|
+
const registry = readRegistry();
|
|
106
|
+
if (opts.json)
|
|
107
|
+
console.log(JSON.stringify(registry.entries, null, 2));
|
|
108
|
+
else
|
|
109
|
+
printTable(registry);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (opts.seedManifest) {
|
|
113
|
+
const result = seedRegistryFromHqRoot(opts.seedManifest);
|
|
114
|
+
if (opts.json)
|
|
115
|
+
console.log(JSON.stringify(result, null, 2));
|
|
116
|
+
else
|
|
117
|
+
console.log(`Seeded ${result.added.length} manifest entr${result.added.length === 1 ? "y" : "ies"}.`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!company) {
|
|
121
|
+
fail(new Error("Usage: hq link <company> [--path <dir>] | hq link --list"));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const linked = await linkFolder(company, opts.path);
|
|
125
|
+
if (opts.json)
|
|
126
|
+
console.log(JSON.stringify(linked));
|
|
127
|
+
else
|
|
128
|
+
console.log(`Linked ${linked.key} → ${chalk.bold(linked.company)}`);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
fail(err);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
export function registerUnlinkCommand(program) {
|
|
136
|
+
program
|
|
137
|
+
.command("unlink")
|
|
138
|
+
.description("Remove the HQ company link for this folder")
|
|
139
|
+
.option("--path <dir>", "Folder to unlink (default: current directory)")
|
|
140
|
+
.action((opts) => {
|
|
141
|
+
try {
|
|
142
|
+
requireAnywhereRuntime();
|
|
143
|
+
const removed = unlinkFolder(opts.path);
|
|
144
|
+
if (removed.length === 0)
|
|
145
|
+
console.log(chalk.dim("Nothing linked here."));
|
|
146
|
+
else
|
|
147
|
+
for (const key of removed)
|
|
148
|
+
console.log(`Unlinked ${key}`);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
fail(err);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
export function registerResolveCompanyCommand(program) {
|
|
156
|
+
program
|
|
157
|
+
.command("resolve-company")
|
|
158
|
+
.description("Print the HQ company linked to this folder (never guesses)")
|
|
159
|
+
.option("--path <dir>", "Folder to resolve (default: current directory)")
|
|
160
|
+
.option("--json", "Print JSON output")
|
|
161
|
+
.action((opts) => {
|
|
162
|
+
try {
|
|
163
|
+
requireAnywhereRuntime();
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
fail(err);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const result = resolveCompany(opts.path);
|
|
170
|
+
if (opts.json) {
|
|
171
|
+
console.log(JSON.stringify(result));
|
|
172
|
+
if (result.company === null)
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (result.company !== null)
|
|
177
|
+
console.log(`${result.company}\t${result.key}\t${result.source}`);
|
|
178
|
+
else {
|
|
179
|
+
console.error(result.reason);
|
|
180
|
+
process.exitCode = 1;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=link.js.map
|
|
@@ -22,6 +22,12 @@ export interface RuntimeServerStatus {
|
|
|
22
22
|
headerKeys?: string[];
|
|
23
23
|
/** Env KEYS present on this runtime's def (values are NEVER captured). */
|
|
24
24
|
envKeys?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Whether the def can actually launch: always true for http/sse; for stdio the
|
|
27
|
+
* command is an absolute path that exists and every absolute-path arg exists
|
|
28
|
+
* (policy hq-mcp-absolute-paths). Undefined when not present.
|
|
29
|
+
*/
|
|
30
|
+
healthy?: boolean;
|
|
25
31
|
}
|
|
26
32
|
/** A single HQ-owned MCP server's cross-runtime status. */
|
|
27
33
|
export interface McpServerStatus {
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* commander; {@link registerMcpCommand} wires the `hq mcp status` command group.
|
|
23
23
|
*/
|
|
24
24
|
import * as fs from 'fs';
|
|
25
|
+
import * as path from 'path';
|
|
26
|
+
import { anywhereRuntimeEnabled } from './anywhere-runtime-flag.js';
|
|
25
27
|
import { HQ_PACK_PROVENANCE_KEY, SECRET_REDACTION, backupRoot, claudeConfigFormat, claudeConfigPath, codexConfigFormat, codexConfigPath, CODEX_MCP_SERVERS_KEY, ConfigParseError, isCodexInstalled, readConfigDoc, resolveEnv, } from './mcp-registration.js';
|
|
26
28
|
// ---------------------------------------------------------------------------
|
|
27
29
|
// Pure status computation.
|
|
@@ -140,7 +142,7 @@ function scanCodex(env) {
|
|
|
140
142
|
return { installed: true, parses, configPath, hqServers };
|
|
141
143
|
}
|
|
142
144
|
/** Build the per-runtime, per-server status from a scan (or its absence). */
|
|
143
|
-
function runtimeServerStatus(scan, name) {
|
|
145
|
+
function runtimeServerStatus(scan, name, checkHealth) {
|
|
144
146
|
if (!scan.installed)
|
|
145
147
|
return { presence: 'not-installed' };
|
|
146
148
|
if (!scan.parses)
|
|
@@ -154,8 +156,20 @@ function runtimeServerStatus(scan, name) {
|
|
|
154
156
|
target: targetOf(found.def),
|
|
155
157
|
headerKeys: headerKeysOf(found.def),
|
|
156
158
|
envKeys: envKeysOf(found.def),
|
|
159
|
+
...(checkHealth ? { healthy: isLaunchable(found.def) } : {}),
|
|
157
160
|
};
|
|
158
161
|
}
|
|
162
|
+
/** True when a def can launch (see {@link RuntimeServerStatus.healthy}). */
|
|
163
|
+
function isLaunchable(def) {
|
|
164
|
+
if (transportOf(def) !== 'stdio')
|
|
165
|
+
return true;
|
|
166
|
+
const command = def.command;
|
|
167
|
+
if (typeof command !== 'string' || !path.isAbsolute(command) || !fs.existsSync(command)) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
const args = Array.isArray(def.args) ? def.args : [];
|
|
171
|
+
return args.every((a) => typeof a !== 'string' || !path.isAbsolute(a) || fs.existsSync(a));
|
|
172
|
+
}
|
|
159
173
|
/** List the most-recent backup dir names under `backupRoot` (newest first, capped). */
|
|
160
174
|
function recentBackups(env) {
|
|
161
175
|
const root = backupRoot(env);
|
|
@@ -184,14 +198,15 @@ function recentBackups(env) {
|
|
|
184
198
|
* rather than throwing. Header VALUES are never read into the report.
|
|
185
199
|
*/
|
|
186
200
|
export function computeMcpStatus(env) {
|
|
201
|
+
const checkHealth = anywhereRuntimeEnabled();
|
|
187
202
|
const claudeScan = scanClaude(env);
|
|
188
203
|
const codexScan = scanCodex(env);
|
|
189
204
|
// Union of every HQ-owned server name across both runtimes.
|
|
190
205
|
const names = new Set([...claudeScan.hqServers.keys(), ...codexScan.hqServers.keys()]);
|
|
191
206
|
const servers = [];
|
|
192
207
|
for (const name of [...names].sort()) {
|
|
193
|
-
const claude = runtimeServerStatus(claudeScan, name);
|
|
194
|
-
const codex = runtimeServerStatus(codexScan, name);
|
|
208
|
+
const claude = runtimeServerStatus(claudeScan, name, checkHealth);
|
|
209
|
+
const codex = runtimeServerStatus(codexScan, name, checkHealth);
|
|
195
210
|
// Prefer whichever runtime has the def for the top-level transport/target.
|
|
196
211
|
const claudeFound = claudeScan.hqServers.get(name);
|
|
197
212
|
const codexFound = codexScan.hqServers.get(name);
|
|
@@ -258,7 +273,9 @@ export function computeMcpStatus(env) {
|
|
|
258
273
|
function renderRuntimeLine(label, st) {
|
|
259
274
|
switch (st.presence) {
|
|
260
275
|
case 'present': {
|
|
261
|
-
const parts =
|
|
276
|
+
const parts = st.healthy === undefined
|
|
277
|
+
? ['present']
|
|
278
|
+
: [st.healthy ? 'present (healthy)' : 'present (UNHEALTHY: command not launchable)'];
|
|
262
279
|
if (st.headerKeys && st.headerKeys.length > 0) {
|
|
263
280
|
parts.push(`headers: ${st.headerKeys.map((k) => `${k}=${SECRET_REDACTION}`).join(', ')}`);
|
|
264
281
|
}
|
|
@@ -36,14 +36,33 @@ function assertApiKeyCanAuthorizeInstall(source) {
|
|
|
36
36
|
}
|
|
37
37
|
export function registerPackageInstallCommand(parent) {
|
|
38
38
|
parent
|
|
39
|
-
.command('install
|
|
39
|
+
.command('install [source]')
|
|
40
40
|
.description('Install a package. Sources: bare slug (registry, Cognito-gated), ' +
|
|
41
41
|
'@scope/name[@ver] (npm pack), git URL[#ref], or local path.')
|
|
42
42
|
.option('--company <co>', 'Scope package secret authorization to a specific company')
|
|
43
43
|
.option('--allow-hooks', 'Skip the hooks confirmation prompt (content-pack flow)')
|
|
44
44
|
.option('--allow-mcp', 'Skip the MCP server confirmation prompt (content-pack flow)')
|
|
45
45
|
.option('--branch', 'Follow a ref instead of SHA-pinning (git content-pack flow)')
|
|
46
|
+
.option('--global', 'Install HQ into an agent runtime at user scope (requires the hq-anywhere-runtime flag)')
|
|
47
|
+
.option('--runtime <runtime>', 'Runtime for --global (claude | codex)', 'claude')
|
|
48
|
+
.option('--dry-run', 'With --global: print every file and config diff without writing')
|
|
49
|
+
.option('--hq-root <dir>', 'With --global: HQ checkout to install from (default: walk up from cwd)')
|
|
46
50
|
.action(async (source, opts) => {
|
|
51
|
+
if (opts.global) {
|
|
52
|
+
if (source) {
|
|
53
|
+
console.error("error: package source cannot be used with --global");
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const { runInstallGlobal } = await import('./install-global.js');
|
|
58
|
+
runInstallGlobal(opts);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!source) {
|
|
62
|
+
console.error("error: missing required argument 'source'");
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
47
66
|
try {
|
|
48
67
|
// Gate the top-level `hq install` entrypoint only — `installPack`
|
|
49
68
|
// itself is also called by `hq packs update` (packs.ts), which must
|
|
@@ -3,5 +3,24 @@
|
|
|
3
3
|
* Updates lock for specific module
|
|
4
4
|
*/
|
|
5
5
|
import { Command } from 'commander';
|
|
6
|
+
import { type ConvergenceDeps } from "../lib/daemon/convergence.js";
|
|
7
|
+
export interface RestartAndVerifyDeps extends ConvergenceDeps {
|
|
8
|
+
restart?: () => {
|
|
9
|
+
attempted: boolean;
|
|
10
|
+
restarted: boolean;
|
|
11
|
+
message?: string;
|
|
12
|
+
};
|
|
13
|
+
sendRestartRequest?: () => void;
|
|
14
|
+
home?: string;
|
|
15
|
+
env?: NodeJS.ProcessEnv;
|
|
16
|
+
installedVersion?: () => string | null;
|
|
17
|
+
log?: (line: string) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Restart the daemon after an update and verify it converged on the installed
|
|
21
|
+
* hq-cli (policy hq-self-updater-must-verify-convergence-after-install).
|
|
22
|
+
* Returns false when it did not; the caller must report that as a failure.
|
|
23
|
+
*/
|
|
24
|
+
export declare function restartAndVerifyDaemon(hqRoot: string, deps?: RestartAndVerifyDeps): Promise<boolean>;
|
|
6
25
|
export declare function registerUpdateCommand(program: Command): void;
|
|
7
26
|
//# sourceMappingURL=update.d.ts.map
|
package/dist/commands/update.js
CHANGED
|
@@ -3,9 +3,56 @@
|
|
|
3
3
|
* Updates lock for specific module
|
|
4
4
|
*/
|
|
5
5
|
import * as path from 'path';
|
|
6
|
+
import * as os from "node:os";
|
|
6
7
|
import { findHqRoot, readManifest, readLock, writeLock, getModulesDir } from '../utils/manifest.js';
|
|
7
8
|
import { fetchRepo, pullRepo, getCurrentCommit, isRepo } from '../utils/git.js';
|
|
8
|
-
import {
|
|
9
|
+
import { daemonPaths } from "../lib/daemon/paths.js";
|
|
10
|
+
import { writeControlRequest } from "../lib/daemon/requests.js";
|
|
11
|
+
import { hqdSocketPath } from "../lib/daemon/protocol.js";
|
|
12
|
+
import { describeNonConvergence, installedCliVersion, pingHqdVersion, verifyDaemonConvergence, } from "../lib/daemon/convergence.js";
|
|
13
|
+
import { anywhereRuntimeEnabled } from "./anywhere-runtime-flag.js";
|
|
14
|
+
/**
|
|
15
|
+
* Restart the daemon after an update and verify it converged on the installed
|
|
16
|
+
* hq-cli (policy hq-self-updater-must-verify-convergence-after-install).
|
|
17
|
+
* Returns false when it did not; the caller must report that as a failure.
|
|
18
|
+
*/
|
|
19
|
+
export async function restartAndVerifyDaemon(hqRoot, deps = {}) {
|
|
20
|
+
if (!anywhereRuntimeEnabled())
|
|
21
|
+
return true;
|
|
22
|
+
const log = deps.log ?? ((line) => console.log(line));
|
|
23
|
+
const env = deps.env ?? process.env;
|
|
24
|
+
const home = deps.home ?? os.homedir();
|
|
25
|
+
let restart;
|
|
26
|
+
if (deps.restart)
|
|
27
|
+
restart = deps.restart();
|
|
28
|
+
else {
|
|
29
|
+
try {
|
|
30
|
+
await (deps.ping ?? (() => pingHqdVersion(hqdSocketPath(home, env))))();
|
|
31
|
+
(deps.sendRestartRequest ?? (() => writeControlRequest(daemonPaths(home, env).requestsDir, { action: "restart-daemon" })))();
|
|
32
|
+
restart = { attempted: true, restarted: true };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
restart = { attempted: false, restarted: false, message: `hqd is not running; no restart request was sent (${err instanceof Error ? err.message : String(err)})` };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (restart.message)
|
|
39
|
+
log(restart.message);
|
|
40
|
+
// No installed daemon: nothing is running old code.
|
|
41
|
+
if (!restart.attempted)
|
|
42
|
+
return true;
|
|
43
|
+
const expected = (deps.installedVersion ?? installedCliVersion)();
|
|
44
|
+
if (!expected) {
|
|
45
|
+
log("Daemon convergence: could not read the installed hq-cli version (hq --version failed). Run: hq doctor --only anywhere");
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const result = await verifyDaemonConvergence(expected, deps);
|
|
49
|
+
if (!result.converged) {
|
|
50
|
+
log(describeNonConvergence(result));
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
log(`Daemon converged: hqd runs hq-cli ${expected}.`);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
9
56
|
export function registerUpdateCommand(program) {
|
|
10
57
|
program
|
|
11
58
|
.command('update [module-name]')
|
|
@@ -50,12 +97,11 @@ export function registerUpdateCommand(program) {
|
|
|
50
97
|
}
|
|
51
98
|
}
|
|
52
99
|
writeLock(hqRoot, lock);
|
|
53
|
-
// An installed daemon is a separate Node process. Refresh it
|
|
54
|
-
// best-effort after a successful update without making the update fail.
|
|
55
|
-
const restart = restartDaemonServiceIfInstalled({ hqRoot });
|
|
56
|
-
if (restart.message)
|
|
57
|
-
console.log(restart.message);
|
|
58
100
|
console.log('\nLock file updated.');
|
|
101
|
+
// An installed daemon is a separate Node process. Restart it and
|
|
102
|
+
// verify it runs the installed hq-cli; a mismatch fails the command.
|
|
103
|
+
if (!await restartAndVerifyDaemon(hqRoot))
|
|
104
|
+
process.exitCode = 1;
|
|
59
105
|
}
|
|
60
106
|
catch (error) {
|
|
61
107
|
console.error('Error:', error instanceof Error ? error.message : error);
|
package/dist/fast-core.d.ts
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* entries use the bundled-script runner; native entries import their one handler
|
|
7
7
|
* module. The registration in `core.ts` remains the complete fallback path.
|
|
8
8
|
*/
|
|
9
|
+
import { parseFastCore } from "./fast-scaffold.js";
|
|
9
10
|
export declare function readStdinIfPiped(stream: NodeJS.ReadStream): Promise<string>;
|
|
10
11
|
/**
|
|
11
12
|
* Run the fast-path core command named in `argv` and exit with its code.
|
|
12
13
|
* Only call when {@link isFastCoreRequest} is true; a non-match returns without
|
|
13
14
|
* doing anything.
|
|
14
15
|
*/
|
|
15
|
-
export declare function runFastCore(argv: readonly string[]): Promise<void>;
|
|
16
|
+
export declare function runFastCore(argv: readonly string[], parsed?: ReturnType<typeof parseFastCore>): Promise<void>;
|
|
16
17
|
//# sourceMappingURL=fast-core.d.ts.map
|
package/dist/fast-core.js
CHANGED
|
@@ -6,10 +6,7 @@
|
|
|
6
6
|
* entries use the bundled-script runner; native entries import their one handler
|
|
7
7
|
* module. The registration in `core.ts` remains the complete fallback path.
|
|
8
8
|
*/
|
|
9
|
-
import { resolveLiveRoot } from "./utils/hq-roots.js";
|
|
10
|
-
import { runBundledScript } from "./utils/run-bundled-script.js";
|
|
11
9
|
import { parseFastCore } from "./fast-scaffold.js";
|
|
12
|
-
import { CORE_RRR_WORKTREE_SCOPE_FIX_FLAG } from "./commands/core-rrr-worktree-scope-fix-flag.js";
|
|
13
10
|
export async function readStdinIfPiped(stream) {
|
|
14
11
|
if (stream.isTTY)
|
|
15
12
|
return "";
|
|
@@ -23,8 +20,7 @@ export async function readStdinIfPiped(stream) {
|
|
|
23
20
|
* Only call when {@link isFastCoreRequest} is true; a non-match returns without
|
|
24
21
|
* doing anything.
|
|
25
22
|
*/
|
|
26
|
-
export async function runFastCore(argv) {
|
|
27
|
-
const parsed = parseFastCore(argv);
|
|
23
|
+
export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
28
24
|
if (!parsed)
|
|
29
25
|
return;
|
|
30
26
|
const entry = parsed.entry;
|
|
@@ -41,9 +37,14 @@ export async function runFastCore(argv) {
|
|
|
41
37
|
return;
|
|
42
38
|
}
|
|
43
39
|
const useShell = entry.kind === "native" && Boolean(entry.familyFlagKey) && !familyEnabled;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
let worktreeScopeFixEnabled;
|
|
41
|
+
if (entry.name === "repo-run-registry") {
|
|
42
|
+
const [{ CORE_RRR_WORKTREE_SCOPE_FIX_FLAG }, { fastFlagEnabled }] = await Promise.all([
|
|
43
|
+
import("./commands/core-rrr-worktree-scope-fix-flag.js"),
|
|
44
|
+
import("./native-fast-route.js"),
|
|
45
|
+
]);
|
|
46
|
+
worktreeScopeFixEnabled = await fastFlagEnabled(CORE_RRR_WORKTREE_SCOPE_FIX_FLAG, process.env, { numericEnvironmentOverride: false });
|
|
47
|
+
}
|
|
47
48
|
const shouldInjectRoot = useShell
|
|
48
49
|
? entry.shellHqRoot === "live" || (entry.shellHqRoot === "reconcile-default" &&
|
|
49
50
|
!parsed.args.includes("--hq-root") && !parsed.args.includes("--help") && !parsed.args.includes("-h"))
|
|
@@ -56,6 +57,7 @@ export async function runFastCore(argv) {
|
|
|
56
57
|
// its expected-user-error formatting remains authoritative.
|
|
57
58
|
let hqRoot;
|
|
58
59
|
if (shouldInjectRoot) {
|
|
60
|
+
const { resolveLiveRoot } = await import("./utils/hq-roots.js");
|
|
59
61
|
try {
|
|
60
62
|
hqRoot = resolveLiveRoot(parsed.hqRootArg ? { hqRoot: parsed.hqRootArg } : {});
|
|
61
63
|
}
|
|
@@ -109,6 +111,8 @@ export async function runFastCore(argv) {
|
|
|
109
111
|
: {}),
|
|
110
112
|
...(entry.name === "repo-run-registry" ? { HQ_FLAG_CORE_RRR_WORKTREE_SCOPE_FIX: String(worktreeScopeFixEnabled) } : {}),
|
|
111
113
|
};
|
|
114
|
+
// Keep the rollback runner off native commands' import path.
|
|
115
|
+
const { runBundledScript } = await import("./utils/run-bundled-script.js");
|
|
112
116
|
const { code } = runBundledScript({
|
|
113
117
|
asset,
|
|
114
118
|
args: parsed.args,
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,9 @@ import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.
|
|
|
18
18
|
// so runCli's finally still finalizes release health and flushes Sentry.
|
|
19
19
|
installProcessRejectionBoundary();
|
|
20
20
|
function dispatchCliEntry() {
|
|
21
|
-
|
|
21
|
+
const versionRequest = isVersionRequest(process.argv);
|
|
22
|
+
const parsedFastCore = versionRequest ? null : parseFastCore(process.argv);
|
|
23
|
+
if (versionRequest) {
|
|
22
24
|
// Bare `--version` is the CLI semver only (the contract field updaters
|
|
23
25
|
// compare). `hq version` and `--json` print the labelled HQ/CLI report.
|
|
24
26
|
// Lazy so this path still skips the command graph.
|
|
@@ -36,22 +38,22 @@ function dispatchCliEntry() {
|
|
|
36
38
|
});
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
|
-
else if (
|
|
41
|
+
else if (parsedFastCore?.entry.kind === "native" &&
|
|
40
42
|
(process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "true" ||
|
|
41
43
|
process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "1")) {
|
|
42
44
|
// An explicit enabled override has already decided the global gate. Skip the
|
|
43
45
|
// snapshot reader import on hot native plumbing paths; runFastCore still
|
|
44
46
|
// checks the command-family flag before choosing native or shell rollback.
|
|
45
|
-
void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
|
|
47
|
+
void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv, parsedFastCore));
|
|
46
48
|
}
|
|
47
|
-
else if (
|
|
49
|
+
else if (parsedFastCore?.entry.kind === "native") {
|
|
48
50
|
// Native routes are default-off. Resolve the company-scoped persisted flag
|
|
49
51
|
// without loading the command graph; a miss or stale snapshot takes the
|
|
50
52
|
// existing full-graph route.
|
|
51
53
|
void import("./native-fast-route.js").then(async ({ nativeFastRouteEnabled }) => {
|
|
52
54
|
if (await nativeFastRouteEnabled(process.argv)) {
|
|
53
55
|
const { runFastCore } = await import("./fast-core.js");
|
|
54
|
-
await runFastCore(process.argv);
|
|
56
|
+
await runFastCore(process.argv, parsedFastCore);
|
|
55
57
|
}
|
|
56
58
|
else {
|
|
57
59
|
const { runCli } = await import("./main.js");
|
|
@@ -75,11 +77,11 @@ function dispatchCliEntry() {
|
|
|
75
77
|
});
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
|
-
else if (
|
|
80
|
+
else if (parsedFastCore) {
|
|
79
81
|
// Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
|
|
80
82
|
// dispatches straight to the bundled-script runner, so it never pays for the
|
|
81
83
|
// ~60-module command graph main.ts imports. See src/fast-scaffold.ts.
|
|
82
|
-
void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
|
|
84
|
+
void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv, parsedFastCore));
|
|
83
85
|
}
|
|
84
86
|
else {
|
|
85
87
|
// Keep the command lifecycle detached from module evaluation, as it was
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Flag snapshot reader loaded only when the timeout guard would block. */
|
|
2
|
+
import type { FlagReader } from "../flag-registry.js";
|
|
3
|
+
import { type PersistedFlagSnapshot } from "../flag-snapshot-file.js";
|
|
4
|
+
export declare function createNativeSnapshotFlagReader(persistedSnapshot: PersistedFlagSnapshot | null, companyIdentifiers: readonly string[], env: NodeJS.ProcessEnv): FlagReader;
|
|
5
|
+
export declare function snapshotFlagReader(env: NodeJS.ProcessEnv): FlagReader;
|
|
6
|
+
//# sourceMappingURL=timeout-guard-native-flags.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Flag snapshot reader loaded only when the timeout guard would block. */
|
|
2
|
+
import { readLocalOverride, resolveFlag, } from "@indigoai-us/hq-flags-client";
|
|
3
|
+
import { resolveProcessFlagCompanyContext } from "../process-flag-company-context.js";
|
|
4
|
+
import { readFlagSnapshotFile, } from "../flag-snapshot-file.js";
|
|
5
|
+
export function createNativeSnapshotFlagReader(persistedSnapshot, companyIdentifiers, env) {
|
|
6
|
+
const snapshot = persistedSnapshot
|
|
7
|
+
? { version: 0, flags: persistedSnapshot.flags }
|
|
8
|
+
: null;
|
|
9
|
+
return {
|
|
10
|
+
isEnabled(flagKey, lookup = {}) {
|
|
11
|
+
if (snapshot) {
|
|
12
|
+
return resolveFlag({
|
|
13
|
+
flagKey,
|
|
14
|
+
env,
|
|
15
|
+
snapshot,
|
|
16
|
+
companyIdentifiers,
|
|
17
|
+
lookup,
|
|
18
|
+
}).value;
|
|
19
|
+
}
|
|
20
|
+
return readLocalOverride(flagKey, env) ?? (lookup.fallback ?? false);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function snapshotFlagReader(env) {
|
|
25
|
+
const company = resolveProcessFlagCompanyContext(env);
|
|
26
|
+
const persistedSnapshot = company.companyUid ? readFlagSnapshotFile(company.companyUid, { env }) : null;
|
|
27
|
+
const companyIdentifiers = [company.companyUid, company.companySlug].filter((value) => Boolean(value));
|
|
28
|
+
return createNativeSnapshotFlagReader(persistedSnapshot, companyIdentifiers, env);
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=timeout-guard-native-flags.js.map
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
/** Native fast-route adapter for the shared timeout-guard command. */
|
|
2
|
-
import type { FlagReader } from "../flag-registry.js";
|
|
3
|
-
import { type PersistedFlagSnapshot } from "../flag-snapshot-file.js";
|
|
4
2
|
import { type TimeoutGuardDeps } from "./timeout-guard.js";
|
|
5
|
-
export declare function createNativeSnapshotFlagReader(persistedSnapshot: PersistedFlagSnapshot | null, companyIdentifiers: readonly string[], env: NodeJS.ProcessEnv): FlagReader;
|
|
6
3
|
export declare function runTimeoutGuardFast(_argv: readonly string[], stdin: string, _cwd: string, _hqRoot: string | undefined, dependencies?: TimeoutGuardDeps): Promise<number>;
|
|
7
4
|
//# sourceMappingURL=timeout-guard-native.d.ts.map
|
|
@@ -1,40 +1,14 @@
|
|
|
1
1
|
/** Native fast-route adapter for the shared timeout-guard command. */
|
|
2
|
-
import { readLocalOverride, resolveFlag, } from "@indigoai-us/hq-flags-client";
|
|
3
|
-
import { resolveProcessFlagCompanyContext } from "../process-flag-company-context.js";
|
|
4
|
-
import { readFlagSnapshotFile, } from "../flag-snapshot-file.js";
|
|
5
2
|
import { timeoutGuardCommand } from "./timeout-guard.js";
|
|
6
|
-
export function createNativeSnapshotFlagReader(persistedSnapshot, companyIdentifiers, env) {
|
|
7
|
-
const snapshot = persistedSnapshot
|
|
8
|
-
? { version: 0, flags: persistedSnapshot.flags }
|
|
9
|
-
: null;
|
|
10
|
-
return {
|
|
11
|
-
isEnabled(flagKey, lookup = {}) {
|
|
12
|
-
if (snapshot) {
|
|
13
|
-
return resolveFlag({
|
|
14
|
-
flagKey,
|
|
15
|
-
env,
|
|
16
|
-
snapshot,
|
|
17
|
-
companyIdentifiers,
|
|
18
|
-
lookup,
|
|
19
|
-
}).value;
|
|
20
|
-
}
|
|
21
|
-
return readLocalOverride(flagKey, env) ?? (lookup.fallback ?? false);
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function snapshotFlagReader(companyUid, companySlug, env) {
|
|
26
|
-
const persistedSnapshot = companyUid ? readFlagSnapshotFile(companyUid, { env }) : null;
|
|
27
|
-
const companyIdentifiers = [companyUid, companySlug].filter((value) => Boolean(value));
|
|
28
|
-
return createNativeSnapshotFlagReader(persistedSnapshot, companyIdentifiers, env);
|
|
29
|
-
}
|
|
30
3
|
export async function runTimeoutGuardFast(_argv, stdin, _cwd, _hqRoot, dependencies = {}) {
|
|
31
4
|
const env = dependencies.env ?? process.env;
|
|
32
|
-
const company = resolveProcessFlagCompanyContext(env);
|
|
33
|
-
const flagReader = dependencies.flagReader ?? snapshotFlagReader(company.companyUid, company.companySlug, env);
|
|
34
5
|
return timeoutGuardCommand(stdin, {
|
|
35
6
|
...dependencies,
|
|
36
7
|
env,
|
|
37
|
-
|
|
8
|
+
getFlagReader: dependencies.getFlagReader ?? (async () => {
|
|
9
|
+
const { snapshotFlagReader } = await import("./timeout-guard-native-flags.js");
|
|
10
|
+
return snapshotFlagReader(env);
|
|
11
|
+
}),
|
|
38
12
|
});
|
|
39
13
|
}
|
|
40
14
|
//# sourceMappingURL=timeout-guard-native.js.map
|
|
@@ -49,6 +49,8 @@ export declare function evaluateTimeoutGuard(input: TimeoutGuardInput): GuardVer
|
|
|
49
49
|
export interface TimeoutGuardDeps {
|
|
50
50
|
/** Resolve the current HQ user email for the rollout gate. Default: cached id token. */
|
|
51
51
|
getEmail?: () => string | undefined;
|
|
52
|
+
/** Resolve the flag reader only after an input would otherwise be blocked. */
|
|
53
|
+
getFlagReader?: () => Promise<FlagReader>;
|
|
52
54
|
env?: NodeJS.ProcessEnv;
|
|
53
55
|
stderr?: NodeJS.WritableStream;
|
|
54
56
|
/** Test seam: held registry snapshot reader. */
|