@indigoai-us/hq-cli 5.109.6 → 5.109.8
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 +31 -0
- package/dist/commands/files.js +11 -1
- package/dist/commands/people.d.ts +1 -0
- package/dist/commands/people.js +10 -0
- package/dist/commands/pkg-install.d.ts +0 -14
- package/dist/commands/pkg-install.js +0 -127
- package/dist/commands/whoami.js +28 -2
- package/dist/lib/doctor/checks/runtime-probe.d.ts +11 -11
- package/dist/lib/doctor/checks/runtime-probe.js +15 -15
- package/dist/lib/doctor/compat.d.ts +2 -2
- package/dist/lib/doctor/compat.js +1 -1
- package/dist/startup-registration.d.ts +2 -0
- package/dist/startup-registration.js +14 -3
- package/dist/utils/hq-roots.d.ts +25 -0
- package/dist/utils/hq-roots.js +67 -0
- package/dist/utils/incomplete-install-error.d.ts +45 -8
- package/dist/utils/incomplete-install-error.js +168 -42
- package/dist/utils/install-tree-torn.d.ts +15 -7
- package/dist/utils/install-tree-torn.js +38 -4
- package/dist/utils/people.d.ts +3 -0
- package/dist/utils/registry-client.d.ts +0 -6
- package/dist/utils/registry-client.js +0 -3
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.109.8] — 2026-09-11
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- A background reinstall no longer turns into a crash report (HQ-CLI-1M,
|
|
10
|
+
HQ-CLI-1N). When another program reinstalls hq globally while a command is
|
|
11
|
+
running — the box's own update timer, the desktop app, or a second `hq` — it
|
|
12
|
+
renames hq's files aside and rewrites them file by file over a few seconds, and
|
|
13
|
+
a command already running can try to load a file that vanished in that window.
|
|
14
|
+
hq already knew to treat that as "your install was mid-update, not an hq bug"
|
|
15
|
+
and print a re-run/reinstall note instead of a crash, but it could only do so
|
|
16
|
+
while it could still find its own install directory on disk — which, in this
|
|
17
|
+
exact situation, it usually could not, because that directory is what just got
|
|
18
|
+
renamed away. That one blind spot silenced the recovery for two different crash
|
|
19
|
+
shapes: the ESM-loader `ENOENT` seen in production (HQ-CLI-1M) and a lazy
|
|
20
|
+
CommonJS `require('./sibling')` from a bundled dependency that resolves after
|
|
21
|
+
the tear (HQ-CLI-1N). hq now locates its own directory without reading the
|
|
22
|
+
disk, so the mid-update case is recognized for both: a registration-time tear
|
|
23
|
+
waits for the install to settle and re-runs once, and a tear anywhere else
|
|
24
|
+
prints the reinstall note. A genuine packaging fault in hq's own shipped files
|
|
25
|
+
(a miss under the install's own `dist/` or `assets/`) is still reported.
|
|
26
|
+
|
|
27
|
+
## [5.109.7] — 2026-09-10
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Add `whoami --json`, structured `files acl --json` with explicit absence,
|
|
32
|
+
and `people resolve --membership-only` with canonical company/person IDs.
|
|
33
|
+
Delegation helpers can now verify exact grants without parsing display tables
|
|
34
|
+
or treating a missing ACL as a transport failure. Existing human output stays available.
|
|
35
|
+
|
|
5
36
|
## [5.109.6] — 2026-09-10
|
|
6
37
|
|
|
7
38
|
### Fixed
|
package/dist/commands/files.js
CHANGED
|
@@ -276,7 +276,8 @@ export function registerFilesCommand(program) {
|
|
|
276
276
|
files
|
|
277
277
|
.command("acl <prefix>")
|
|
278
278
|
.description("Show the ACL (access control list) for a file prefix")
|
|
279
|
-
.
|
|
279
|
+
.option("--json", "Output structured ACL data, including explicit absence")
|
|
280
|
+
.action(async (prefix, opts) => {
|
|
280
281
|
try {
|
|
281
282
|
// Read-only, so stripping is safe — but a mismatched anchor would show
|
|
282
283
|
// a DIFFERENT company's ACL than the one the operator pasted.
|
|
@@ -321,7 +322,16 @@ export function registerFilesCommand(program) {
|
|
|
321
322
|
process.exit(1);
|
|
322
323
|
}
|
|
323
324
|
const tree = (await treeRes.json());
|
|
325
|
+
if (!tree || typeof tree.prefix !== 'string' || !Array.isArray(tree.direct)
|
|
326
|
+
|| !Array.isArray(tree.inherited) || !Array.isArray(tree.children)
|
|
327
|
+
|| !(tree.directRow === null || (typeof tree.directRow === 'object' && tree.directRow))) {
|
|
328
|
+
throw new Error('Invalid ACL tree response');
|
|
329
|
+
}
|
|
324
330
|
const row = tree.directRow;
|
|
331
|
+
if (opts.json) {
|
|
332
|
+
console.log(JSON.stringify({ ...tree, schemaVersion: 1, companyUid, exists: row !== null }, null, 2));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
325
335
|
// No own row AND nothing inherited or granted below — preserve the
|
|
326
336
|
// original "no ACL record" exit path.
|
|
327
337
|
if (!row && tree.inherited.length === 0 && tree.children.length === 0) {
|
package/dist/commands/people.js
CHANGED
|
@@ -41,6 +41,8 @@ export function activeMemberToPersonRecord(m) {
|
|
|
41
41
|
slug,
|
|
42
42
|
name,
|
|
43
43
|
email,
|
|
44
|
+
personUid: m.personUid,
|
|
45
|
+
companyUid: m.companyUid,
|
|
44
46
|
role: m.role || undefined,
|
|
45
47
|
type: "internal",
|
|
46
48
|
source: `hq-pro membership: /membership/company/${m.companyUid}`,
|
|
@@ -147,6 +149,13 @@ async function tryFetchRoster(fetchRoster, hqRoot, slug) {
|
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
151
|
export async function resolvePersonWithRosterFallback(input) {
|
|
152
|
+
if (input.opts?.membershipOnly) {
|
|
153
|
+
if (input.opts.localOnly)
|
|
154
|
+
throw new Error('--membership-only cannot be combined with --local-only');
|
|
155
|
+
// No local fallback: callers need authoritative IDs and must see transport errors.
|
|
156
|
+
const roster = await (input.fetchRoster ?? fetchMembershipRoster)(input.hqRoot, input.slug);
|
|
157
|
+
return resolveNameToEmail(roster, input.name);
|
|
158
|
+
}
|
|
150
159
|
const localPeople = listCompanyPeople(input.hqRoot, input.slug);
|
|
151
160
|
const local = resolveNameToEmail(localPeople, input.name);
|
|
152
161
|
if (local.status !== "not_found" || input.opts?.localOnly)
|
|
@@ -239,6 +248,7 @@ export function registerPeopleCommand(program, deps = {}) {
|
|
|
239
248
|
people
|
|
240
249
|
.command("resolve <name>")
|
|
241
250
|
.description("Resolve a person name to their email address")
|
|
251
|
+
.option("--membership-only", "Resolve from authoritative company membership, ignoring local records")
|
|
242
252
|
.option("--json", "Output JSON instead of plain text")
|
|
243
253
|
.option("--local-only", "Skip network fallback; resolve only from the local people roster")
|
|
244
254
|
.action(async (name, opts) => {
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hq packages install <slug> — download, verify, and extract a package (US-005)
|
|
3
|
-
*
|
|
4
|
-
* Flow:
|
|
5
|
-
* 1. Check auth (prompt login if needed)
|
|
6
|
-
* 2. Check entitlement via API
|
|
7
|
-
* 3. Download tarball via presigned URL
|
|
8
|
-
* 4. Verify SHA256 hash
|
|
9
|
-
* 5. Verify RSA signature (if provided)
|
|
10
|
-
* 6. Extract to packages/installed/<slug>/
|
|
11
|
-
* 7. Validate extracted package.yaml slug matches
|
|
12
|
-
* 8. Update packages/registry.yaml
|
|
13
|
-
* 9. Print next-step message
|
|
14
|
-
*/
|
|
15
1
|
import { Command } from 'commander';
|
|
16
2
|
export declare function registerPackageInstallCommand(parent: Command): void;
|
|
17
3
|
//# sourceMappingURL=pkg-install.d.ts.map
|
|
@@ -1,28 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hq packages install <slug> — download, verify, and extract a package (US-005)
|
|
3
|
-
*
|
|
4
|
-
* Flow:
|
|
5
|
-
* 1. Check auth (prompt login if needed)
|
|
6
|
-
* 2. Check entitlement via API
|
|
7
|
-
* 3. Download tarball via presigned URL
|
|
8
|
-
* 4. Verify SHA256 hash
|
|
9
|
-
* 5. Verify RSA signature (if provided)
|
|
10
|
-
* 6. Extract to packages/installed/<slug>/
|
|
11
|
-
* 7. Validate extracted package.yaml slug matches
|
|
12
|
-
* 8. Update packages/registry.yaml
|
|
13
|
-
* 9. Print next-step message
|
|
14
|
-
*/
|
|
15
|
-
import * as fs from 'fs';
|
|
16
|
-
import * as os from 'os';
|
|
17
|
-
import * as path from 'path';
|
|
18
|
-
import { parseUserYaml } from '../utils/user-yaml-error.js';
|
|
19
|
-
import { execSync } from 'child_process';
|
|
20
1
|
import chalk from 'chalk';
|
|
21
|
-
import { ensureCognitoToken } from '../utils/cognito-session.js';
|
|
22
|
-
import { resolveDefaultHqRoot } from '../utils/cognito-session.js';
|
|
23
|
-
import { getRegistryUrl, RegistryClient, } from '../utils/registry-client.js';
|
|
24
|
-
import { verifySha256, verifyRsaSignature } from '../utils/integrity.js';
|
|
25
|
-
import { addToRegistry } from '../utils/registry.js';
|
|
26
2
|
import { peekHqApiKey, requireApiKeyCapability, } from '../utils/resolve-vault-credential.js';
|
|
27
3
|
import { MARKETPLACE_PREFIX, installPack, sourceMatchesPackPattern, } from './pack-install.js';
|
|
28
4
|
/**
|
|
@@ -105,107 +81,4 @@ export function registerPackageInstallCommand(parent) {
|
|
|
105
81
|
}
|
|
106
82
|
});
|
|
107
83
|
}
|
|
108
|
-
async function installPackage(slug, company) {
|
|
109
|
-
// 1. Auth
|
|
110
|
-
const accessToken = await ensureCognitoToken();
|
|
111
|
-
const registryUrl = getRegistryUrl();
|
|
112
|
-
const client = new RegistryClient(registryUrl, accessToken);
|
|
113
|
-
// 2. Check entitlement
|
|
114
|
-
console.log(chalk.dim(`Checking entitlement for ${slug}...`));
|
|
115
|
-
const entitlement = await client.checkEntitlement(slug);
|
|
116
|
-
if (!entitlement.entitled) {
|
|
117
|
-
throw new Error(`You are not entitled to package "${slug}". Visit the registry to purchase or request access.`);
|
|
118
|
-
}
|
|
119
|
-
// 3. Get download URL
|
|
120
|
-
console.log(chalk.dim('Fetching download URL...'));
|
|
121
|
-
const download = await client.getDownloadUrl(slug);
|
|
122
|
-
// 4. Download to temp file
|
|
123
|
-
const tmpDir = os.tmpdir();
|
|
124
|
-
const tmpFile = path.resolve(tmpDir, `hq-pkg-${slug}-${Date.now()}.tar.gz`);
|
|
125
|
-
try {
|
|
126
|
-
console.log(chalk.dim('Downloading package...'));
|
|
127
|
-
const response = await fetch(download.url, {
|
|
128
|
-
signal: AbortSignal.timeout(120_000),
|
|
129
|
-
});
|
|
130
|
-
if (!response.ok) {
|
|
131
|
-
throw new Error(`Download failed (${response.status})`);
|
|
132
|
-
}
|
|
133
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
134
|
-
fs.writeFileSync(tmpFile, buffer);
|
|
135
|
-
// 5. Verify SHA256
|
|
136
|
-
console.log(chalk.dim('Verifying integrity...'));
|
|
137
|
-
const hashValid = await verifySha256(tmpFile, download.sha256);
|
|
138
|
-
if (!hashValid) {
|
|
139
|
-
throw new Error('SHA256 hash mismatch — the downloaded file may be corrupted or tampered with.');
|
|
140
|
-
}
|
|
141
|
-
// 6. Verify RSA signature (if provided)
|
|
142
|
-
if (download.signature) {
|
|
143
|
-
const sigValid = verifyRsaSignature(download.sha256, download.signature);
|
|
144
|
-
if (!sigValid) {
|
|
145
|
-
throw new Error('RSA signature verification failed — the package may have been tampered with or the public key is missing/invalid. Aborting install.');
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// 7. Extract
|
|
149
|
-
const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
|
|
150
|
-
const installDir = path.resolve(hqRoot, 'packages', 'installed', slug);
|
|
151
|
-
// Clean existing installation
|
|
152
|
-
if (fs.existsSync(installDir)) {
|
|
153
|
-
fs.rmSync(installDir, { recursive: true, force: true });
|
|
154
|
-
}
|
|
155
|
-
fs.mkdirSync(installDir, { recursive: true });
|
|
156
|
-
execSync(`tar -xzf "${tmpFile}" -C "${installDir}"`, {
|
|
157
|
-
stdio: 'pipe',
|
|
158
|
-
});
|
|
159
|
-
// 8. Validate package.yaml slug
|
|
160
|
-
const packageYamlPath = path.resolve(installDir, 'package.yaml');
|
|
161
|
-
if (fs.existsSync(packageYamlPath)) {
|
|
162
|
-
const pkgContent = fs.readFileSync(packageYamlPath, 'utf-8');
|
|
163
|
-
const pkgMeta = parseUserYaml(pkgContent, packageYamlPath);
|
|
164
|
-
if (pkgMeta?.slug && pkgMeta.slug !== slug) {
|
|
165
|
-
// Mismatch — clean up and abort
|
|
166
|
-
fs.rmSync(installDir, { recursive: true, force: true });
|
|
167
|
-
throw new Error(`Package slug mismatch: expected "${slug}", got "${pkgMeta.slug}"`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
// 9. Get package info for registry entry
|
|
171
|
-
let version = 'unknown';
|
|
172
|
-
let name = slug;
|
|
173
|
-
try {
|
|
174
|
-
const pkgInfo = await client.getPackage(slug);
|
|
175
|
-
version = pkgInfo.package.latest_version;
|
|
176
|
-
name = pkgInfo.package.name;
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
// Best-effort — fall back to package.yaml if present
|
|
180
|
-
const packageYaml = path.resolve(installDir, 'package.yaml');
|
|
181
|
-
if (fs.existsSync(packageYaml)) {
|
|
182
|
-
const content = fs.readFileSync(packageYaml, 'utf-8');
|
|
183
|
-
const meta = parseUserYaml(content, packageYaml);
|
|
184
|
-
if (meta?.version)
|
|
185
|
-
version = meta.version;
|
|
186
|
-
if (meta?.name)
|
|
187
|
-
name = meta.name;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
// 10. Update registry.yaml
|
|
191
|
-
const now = new Date().toISOString();
|
|
192
|
-
addToRegistry(hqRoot, {
|
|
193
|
-
name,
|
|
194
|
-
slug,
|
|
195
|
-
version,
|
|
196
|
-
source: registryUrl,
|
|
197
|
-
scope: company,
|
|
198
|
-
installed_at: now,
|
|
199
|
-
updated_at: now,
|
|
200
|
-
});
|
|
201
|
-
console.log(chalk.green(`\nInstalled ${slug}@${version} to packages/installed/${slug}/`));
|
|
202
|
-
console.log(chalk.cyan('Run /package-install ' + slug + ' in Claude to merge into your HQ.'));
|
|
203
|
-
}
|
|
204
|
-
finally {
|
|
205
|
-
// Clean up temp file — never leave partial downloads
|
|
206
|
-
if (fs.existsSync(tmpFile)) {
|
|
207
|
-
fs.unlinkSync(tmpFile);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
84
|
//# sourceMappingURL=pkg-install.js.map
|
package/dist/commands/whoami.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { loadCachedTokens, isExpiring, loadMachineCreds, } from '@indigoai-us/hq-cloud';
|
|
6
6
|
import { peekIdToken as decodeIdToken } from "../utils/id-token.js";
|
|
7
|
-
import { resolveCognitoTokenSource } from "../utils/cognito-session.js";
|
|
7
|
+
import { loadMachineCachedTokens, resolveCognitoTokenSource } from "../utils/cognito-session.js";
|
|
8
8
|
function peekIdToken(idToken) {
|
|
9
9
|
const decoded = decodeIdToken(idToken);
|
|
10
10
|
return {
|
|
@@ -22,13 +22,39 @@ export function registerWhoamiCommand(program) {
|
|
|
22
22
|
program
|
|
23
23
|
.command('whoami')
|
|
24
24
|
.description('Show the currently authenticated user')
|
|
25
|
-
.
|
|
25
|
+
.option('--json', 'Output identity metadata as JSON (never tokens)')
|
|
26
|
+
.action(async (opts) => {
|
|
26
27
|
try {
|
|
27
28
|
// Effective source (same as auth status / ensureCognitoToken): env
|
|
28
29
|
// opt-in, fresh-box default-creds when no usable person session, or
|
|
29
30
|
// after a definitive person-refresh rejection marked the session rejected.
|
|
30
31
|
const machine = resolveCognitoTokenSource() === "machine";
|
|
31
32
|
const cached = loadCachedTokens();
|
|
33
|
+
if (opts.json) {
|
|
34
|
+
const machineCreds = machine ? loadMachineCreds() : null;
|
|
35
|
+
const identityCache = machine ? loadMachineCachedTokens() : cached;
|
|
36
|
+
const identity = identityCache ? peekIdToken(identityCache.idToken) : {};
|
|
37
|
+
const claims = identityCache ? decodeIdToken(identityCache.idToken) : {};
|
|
38
|
+
// Modern credentials carry the canonical UID. Legacy caches may only
|
|
39
|
+
// supply a UID when their subject is bound to the selected username.
|
|
40
|
+
const boundLegacyAgent = machineCreds && identity.entityType === 'agent'
|
|
41
|
+
&& (claims['cognito:username'] === machineCreds.username
|
|
42
|
+
|| claims.username === machineCreds.username || claims.sub === machineCreds.username)
|
|
43
|
+
? identity.entityUid ?? null : null;
|
|
44
|
+
const agentUid = machineCreds?.entityUid !== undefined
|
|
45
|
+
? (machineCreds.entityUid.startsWith('agt_') ? machineCreds.entityUid : null)
|
|
46
|
+
: boundLegacyAgent;
|
|
47
|
+
console.log(JSON.stringify({
|
|
48
|
+
schemaVersion: 1,
|
|
49
|
+
authenticated: machine ? Boolean(machineCreds) : Boolean(cached && !isExpiring(cached, 0)),
|
|
50
|
+
tokenSource: machine ? 'machine' : 'person',
|
|
51
|
+
email: machine ? null : identity.email ?? null,
|
|
52
|
+
personUid: !machine && identity.entityType === 'person' ? identity.entityUid ?? null : null,
|
|
53
|
+
agentUid,
|
|
54
|
+
username: machineCreds?.username ?? null,
|
|
55
|
+
}, null, 2));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
32
58
|
if (machine) {
|
|
33
59
|
// Machine identities (company agents) mint sessions on demand from
|
|
34
60
|
// long-lived creds — report the machine identity honestly even when
|
|
@@ -50,7 +50,7 @@ export declare const POLICY_TRIGGER_LEDGER_RELPATH = "workspace/orchestrator/pol
|
|
|
50
50
|
/** The two lifecycle events `check-hq-hooks.sh` requires a command hook on. */
|
|
51
51
|
export declare const REQUIRED_HOOK_EVENTS: readonly ["SessionStart", "PreToolUse"];
|
|
52
52
|
/**
|
|
53
|
-
* Default runtime marker path an agents-v2 (
|
|
53
|
+
* Default runtime marker path an agents-v2 (hq-fleet) box writes its runtime mode
|
|
54
54
|
* to. Overridable via `HQ_RUNTIME_MARKER_FILE`, exactly as the shell reads
|
|
55
55
|
* `${HQ_RUNTIME_MARKER_FILE:-/var/lib/hq-agent/runtime.json}`.
|
|
56
56
|
*/
|
|
@@ -129,31 +129,31 @@ export interface AgentsV2AttestationOptions {
|
|
|
129
129
|
*/
|
|
130
130
|
export declare function isAgentsV2Runtime(hqRoot: string, env?: NodeJS.ProcessEnv): boolean;
|
|
131
131
|
/**
|
|
132
|
-
* Env var overriding the agents-v2 runtime (
|
|
132
|
+
* Env var overriding the agents-v2 runtime (hq-fleet) config path (chiefly for
|
|
133
133
|
* tests), mirroring `HQ_RUNTIME_MARKER_FILE` and the shell's
|
|
134
|
-
* `
|
|
134
|
+
* `HQ_FLEET_CONFIG_FILE`.
|
|
135
135
|
*/
|
|
136
|
-
export declare const
|
|
136
|
+
export declare const HQ_FLEET_CONFIG_ENV = "HQ_FLEET_CONFIG_FILE";
|
|
137
137
|
/**
|
|
138
|
-
* The agents-v2 runtime (
|
|
139
|
-
* adapter into every lifecycle event — the env override, else `~/.
|
|
138
|
+
* The agents-v2 runtime (hq-fleet) config whose `hooks:` block wires the on-box
|
|
139
|
+
* adapter into every lifecycle event — the env override, else `~/.hq-fleet/config.yaml`
|
|
140
140
|
* (where `provision/render-config.sh` renders it). Mirrors the shell's
|
|
141
|
-
* `
|
|
141
|
+
* `HQ_FLEET_CONFIG_FILE="${HQ_FLEET_CONFIG_FILE:-$HOME/.hq-fleet/config.yaml}"`.
|
|
142
142
|
*/
|
|
143
|
-
export declare function
|
|
143
|
+
export declare function resolveHqFleetConfigPath(env?: NodeJS.ProcessEnv): string;
|
|
144
144
|
/**
|
|
145
145
|
* Whether the agents-v2 runtime actually wires the on-box hook adapter.
|
|
146
146
|
*
|
|
147
147
|
* The wiring lives in the RUNTIME's hook config, not `.claude/settings.json`. On
|
|
148
148
|
* a real box the v2 runtime's shell-hook dispatcher (`agent/shell_hooks.py`)
|
|
149
|
-
* reads a `hooks:` block from `~/.
|
|
149
|
+
* reads a `hooks:` block from `~/.hq-fleet/config.yaml` with one entry per
|
|
150
150
|
* lifecycle event, each invoking `hq-agents-v2-hook-adapter.sh`; the adapter in
|
|
151
151
|
* turn READS `.claude/settings.json` (via `hook-adapter-core.sh`) to fan out to
|
|
152
152
|
* the classic `.claude/hooks` set. So `.claude/settings.json` never NAMES the
|
|
153
153
|
* adapter — it wires the classic `hook-gate.sh` hooks — and grepping it for the
|
|
154
154
|
* adapter always fails on a real box (verified on the v2.17 canary
|
|
155
155
|
* i-0277243ad3aed8109, 2026-09-05: settings.json had 0 adapter refs / 94
|
|
156
|
-
* hook-gate.sh refs, while ~/.
|
|
156
|
+
* hook-gate.sh refs, while ~/.hq-fleet/config.yaml wired the adapter across 7
|
|
157
157
|
* events). Require BOTH the adapter installed under the tree at
|
|
158
158
|
* {@link AGENTS_V2_ADAPTER_RELPATH} AND the runtime config invoking it. Mirrors
|
|
159
159
|
* `hq_runtime_config_wires_v2_adapter` in `check-hq-hooks.sh`.
|
|
@@ -169,7 +169,7 @@ export declare function runtimeConfigWiresV2Adapter(hqRoot: string, env?: NodeJS
|
|
|
169
169
|
export declare function v2LedgerPresent(opts: AgentsV2AttestationOptions): boolean;
|
|
170
170
|
/**
|
|
171
171
|
* All three agents-v2 self-attestation conditions. Used only to GRANT PASS to a
|
|
172
|
-
*
|
|
172
|
+
* hq-fleet box that host detection leaves platform-unknown; never to withhold it.
|
|
173
173
|
* The exact conjunction of `agents_v2_attested` in `check-hq-hooks.sh`.
|
|
174
174
|
*/
|
|
175
175
|
export declare function agentsV2Attested(opts: AgentsV2AttestationOptions): boolean;
|
|
@@ -53,7 +53,7 @@ export const POLICY_TRIGGER_LEDGER_RELPATH = "workspace/orchestrator/policy-trig
|
|
|
53
53
|
/** The two lifecycle events `check-hq-hooks.sh` requires a command hook on. */
|
|
54
54
|
export const REQUIRED_HOOK_EVENTS = ["SessionStart", "PreToolUse"];
|
|
55
55
|
/**
|
|
56
|
-
* Default runtime marker path an agents-v2 (
|
|
56
|
+
* Default runtime marker path an agents-v2 (hq-fleet) box writes its runtime mode
|
|
57
57
|
* to. Overridable via `HQ_RUNTIME_MARKER_FILE`, exactly as the shell reads
|
|
58
58
|
* `${HQ_RUNTIME_MARKER_FILE:-/var/lib/hq-agent/runtime.json}`.
|
|
59
59
|
*/
|
|
@@ -161,36 +161,36 @@ function readRuntimeMarkerMode(markerPath) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
/**
|
|
164
|
-
* Env var overriding the agents-v2 runtime (
|
|
164
|
+
* Env var overriding the agents-v2 runtime (hq-fleet) config path (chiefly for
|
|
165
165
|
* tests), mirroring `HQ_RUNTIME_MARKER_FILE` and the shell's
|
|
166
|
-
* `
|
|
166
|
+
* `HQ_FLEET_CONFIG_FILE`.
|
|
167
167
|
*/
|
|
168
|
-
export const
|
|
168
|
+
export const HQ_FLEET_CONFIG_ENV = "HQ_FLEET_CONFIG_FILE";
|
|
169
169
|
/**
|
|
170
|
-
* The agents-v2 runtime (
|
|
171
|
-
* adapter into every lifecycle event — the env override, else `~/.
|
|
170
|
+
* The agents-v2 runtime (hq-fleet) config whose `hooks:` block wires the on-box
|
|
171
|
+
* adapter into every lifecycle event — the env override, else `~/.hq-fleet/config.yaml`
|
|
172
172
|
* (where `provision/render-config.sh` renders it). Mirrors the shell's
|
|
173
|
-
* `
|
|
173
|
+
* `HQ_FLEET_CONFIG_FILE="${HQ_FLEET_CONFIG_FILE:-$HOME/.hq-fleet/config.yaml}"`.
|
|
174
174
|
*/
|
|
175
|
-
export function
|
|
176
|
-
const override = env[
|
|
175
|
+
export function resolveHqFleetConfigPath(env = process.env) {
|
|
176
|
+
const override = env[HQ_FLEET_CONFIG_ENV]?.trim();
|
|
177
177
|
if (override)
|
|
178
178
|
return override;
|
|
179
|
-
return path.join(os.homedir(), ".
|
|
179
|
+
return path.join(os.homedir(), ".hq-fleet", "config.yaml");
|
|
180
180
|
}
|
|
181
181
|
/**
|
|
182
182
|
* Whether the agents-v2 runtime actually wires the on-box hook adapter.
|
|
183
183
|
*
|
|
184
184
|
* The wiring lives in the RUNTIME's hook config, not `.claude/settings.json`. On
|
|
185
185
|
* a real box the v2 runtime's shell-hook dispatcher (`agent/shell_hooks.py`)
|
|
186
|
-
* reads a `hooks:` block from `~/.
|
|
186
|
+
* reads a `hooks:` block from `~/.hq-fleet/config.yaml` with one entry per
|
|
187
187
|
* lifecycle event, each invoking `hq-agents-v2-hook-adapter.sh`; the adapter in
|
|
188
188
|
* turn READS `.claude/settings.json` (via `hook-adapter-core.sh`) to fan out to
|
|
189
189
|
* the classic `.claude/hooks` set. So `.claude/settings.json` never NAMES the
|
|
190
190
|
* adapter — it wires the classic `hook-gate.sh` hooks — and grepping it for the
|
|
191
191
|
* adapter always fails on a real box (verified on the v2.17 canary
|
|
192
192
|
* i-0277243ad3aed8109, 2026-09-05: settings.json had 0 adapter refs / 94
|
|
193
|
-
* hook-gate.sh refs, while ~/.
|
|
193
|
+
* hook-gate.sh refs, while ~/.hq-fleet/config.yaml wired the adapter across 7
|
|
194
194
|
* events). Require BOTH the adapter installed under the tree at
|
|
195
195
|
* {@link AGENTS_V2_ADAPTER_RELPATH} AND the runtime config invoking it. Mirrors
|
|
196
196
|
* `hq_runtime_config_wires_v2_adapter` in `check-hq-hooks.sh`.
|
|
@@ -201,7 +201,7 @@ export function runtimeConfigWiresV2Adapter(hqRoot, env = process.env) {
|
|
|
201
201
|
return false;
|
|
202
202
|
let raw;
|
|
203
203
|
try {
|
|
204
|
-
raw = fs.readFileSync(
|
|
204
|
+
raw = fs.readFileSync(resolveHqFleetConfigPath(env), "utf8");
|
|
205
205
|
}
|
|
206
206
|
catch {
|
|
207
207
|
return false;
|
|
@@ -265,7 +265,7 @@ function ledgerDirHasFreshTxt(dir, cutoffMs) {
|
|
|
265
265
|
}
|
|
266
266
|
/**
|
|
267
267
|
* All three agents-v2 self-attestation conditions. Used only to GRANT PASS to a
|
|
268
|
-
*
|
|
268
|
+
* hq-fleet box that host detection leaves platform-unknown; never to withhold it.
|
|
269
269
|
* The exact conjunction of `agents_v2_attested` in `check-hq-hooks.sh`.
|
|
270
270
|
*/
|
|
271
271
|
export function agentsV2Attested(opts) {
|
|
@@ -286,7 +286,7 @@ export function checkRuntimeProbe(context) {
|
|
|
286
286
|
const checkId = `${RUNTIME_PROBE_PREFIX}.enforcement`;
|
|
287
287
|
const target = POLICY_TRIGGER_LEDGER_RELPATH;
|
|
288
288
|
const scope = sessionId ? ` for session ${sessionId}` : "";
|
|
289
|
-
// agents-v2 (
|
|
289
|
+
// agents-v2 (hq-fleet) self-attestation. hq doctor leaves the hq-fleet host
|
|
290
290
|
// platform-unknown and, on an unknown host, the probe would report UNKNOWN
|
|
291
291
|
// below. But the on-box adapter provably wrote the ledger through the same
|
|
292
292
|
// .claude hooks, so grant PASS — and report platform "agents-v2" — when, and
|
|
@@ -75,12 +75,12 @@ export interface DeriveVerdictOptions {
|
|
|
75
75
|
*/
|
|
76
76
|
requireLedger?: boolean;
|
|
77
77
|
/**
|
|
78
|
-
* Whether this is an attested agents-v2 (
|
|
78
|
+
* Whether this is an attested agents-v2 (hq-fleet) box: the runtime is
|
|
79
79
|
* agents-v2, `.claude/settings.json` wires the on-box adapter, and a
|
|
80
80
|
* policy-trigger ledger exists (the exact session's when a session id is
|
|
81
81
|
* given; otherwise a ledger fresh within the freshness window). When true,
|
|
82
82
|
* OBSERVED is granted even though the doctor's runtime check did not report
|
|
83
|
-
* PASS — because hq doctor leaves the
|
|
83
|
+
* PASS — because hq doctor leaves the hq-fleet host platform-unknown, yet the
|
|
84
84
|
* on-box adapter provably wrote the ledger through the same .claude hooks.
|
|
85
85
|
*
|
|
86
86
|
* This is the exact twin of the `agents_v2_attested` override in
|
|
@@ -81,7 +81,7 @@ export function deriveCheckHqHooksVerdict(doc, options = {}) {
|
|
|
81
81
|
// ledger" contract.
|
|
82
82
|
const runtimeResult = doc.results.find((result) => result.checkId === RUNTIME_ENFORCEMENT_CHECK_ID);
|
|
83
83
|
let runtime = observeRuntime(runtimeResult?.status, requireLedger);
|
|
84
|
-
// agents-v2 self-attestation: hq doctor leaves the
|
|
84
|
+
// agents-v2 self-attestation: hq doctor leaves the hq-fleet host
|
|
85
85
|
// platform-unknown and so does not report the runtime check as PASS, but the
|
|
86
86
|
// on-box adapter provably wrote the ledger through the same .claude hooks.
|
|
87
87
|
// Grant the identical OBSERVED verdict rather than relaying the host-unknown
|
|
@@ -48,6 +48,8 @@ export interface RegisterRecoveryDeps {
|
|
|
48
48
|
resolveInstall?: () => {
|
|
49
49
|
packageRoot: string | null;
|
|
50
50
|
};
|
|
51
|
+
/** Filesystem-free fallback root for the torn window (defaults to stringDerivedPackageRoot). */
|
|
52
|
+
deriveInstallRoot?: () => string | null;
|
|
51
53
|
lockPath?: () => string;
|
|
52
54
|
waitForSettled?: (args: WaitForInstallTreeSettledArgs) => Promise<WaitForInstallTreeSettledResult>;
|
|
53
55
|
/** node flags to forward to the re-exec child (defaults to process.execArgv). */
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { spawnSync } from "node:child_process";
|
|
20
20
|
import { resolveRunningInstall } from "./utils/version-gate.js";
|
|
21
|
+
import { stringDerivedPackageRoot } from "./utils/hq-roots.js";
|
|
21
22
|
import { updateLockPath } from "./utils/update-lock.js";
|
|
22
23
|
import { classifyModuleNotFound, InstallTreeTornError, waitForInstallTreeSettled, } from "./utils/install-tree-torn.js";
|
|
23
24
|
/**
|
|
@@ -122,13 +123,23 @@ export async function registerCommandsWithRecovery(args) {
|
|
|
122
123
|
return { reexecStatus: child.status ?? 1 };
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the running install's package dir for the settle wait's manifest-health
|
|
128
|
+
* and retired-sibling guards. The on-disk resolver returns null in exactly the
|
|
129
|
+
* window this recovery targets — the package directory has been renamed aside, so
|
|
130
|
+
* it cannot be found by reading disk — which would leave readiness keyed on the
|
|
131
|
+
* single missing file alone and let a premature re-exec run against a still-
|
|
132
|
+
* incomplete tree. Fall back to the filesystem-free derived root so those guards
|
|
133
|
+
* stay active while npm is mid-extraction. Tolerates any resolver failure.
|
|
134
|
+
*/
|
|
126
135
|
function resolvePackageRoot(deps) {
|
|
136
|
+
let root;
|
|
127
137
|
try {
|
|
128
|
-
|
|
138
|
+
root = (deps.resolveInstall ?? resolveRunningInstall)().packageRoot;
|
|
129
139
|
}
|
|
130
140
|
catch {
|
|
131
|
-
|
|
141
|
+
root = null;
|
|
132
142
|
}
|
|
143
|
+
return root ?? (deps.deriveInstallRoot ?? stringDerivedPackageRoot)();
|
|
133
144
|
}
|
|
134
145
|
//# sourceMappingURL=startup-registration.js.map
|
package/dist/utils/hq-roots.d.ts
CHANGED
|
@@ -61,6 +61,31 @@ export declare function createPackageRootResolver(options: PackageRootResolverOp
|
|
|
61
61
|
export declare function packageRoot(): string;
|
|
62
62
|
/** Reset the memoized package root. Tests only. */
|
|
63
63
|
export declare function __resetPackageRootCache(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Derive this package's installed root from the running module's OWN path using
|
|
66
|
+
* STRING operations only — no `readFileSync`, `realpathSync`, or `existsSync`.
|
|
67
|
+
*
|
|
68
|
+
* {@link packageRoot} must read `<dir>/package.json` and `realpathSync` the
|
|
69
|
+
* module directory to answer, so it THROWS in exactly the situation that most
|
|
70
|
+
* needs an answer: a concurrent global install has renamed the installed
|
|
71
|
+
* package directory aside (`.hq-cli-<rand>`) and is re-extracting it file by
|
|
72
|
+
* file, so both the manifest walk and the dist-owner fallback fail on a
|
|
73
|
+
* directory that is momentarily gone. This walks the compiled module's
|
|
74
|
+
* ancestors as strings and returns the DEEPEST one whose trailing segments are
|
|
75
|
+
* `node_modules/@indigoai-us/hq-cli`, which stays correct while that directory
|
|
76
|
+
* does not exist on disk.
|
|
77
|
+
*
|
|
78
|
+
* It is a FALLBACK only — {@link packageRoot} validates the manifest name and
|
|
79
|
+
* is authoritative whenever it succeeds. Returns null for a dev checkout or any
|
|
80
|
+
* layout without that segment triple, so a caller that falls back to it can
|
|
81
|
+
* only ever degrade to today's behaviour (no resolution), never widen it.
|
|
82
|
+
*
|
|
83
|
+
* The deepest match is deliberate: a nested `node_modules/@indigoai-us/hq-cli`
|
|
84
|
+
* inside another package resolves to ITSELF (the copy the module belongs to),
|
|
85
|
+
* never an outer decoy. Case is folded only for a Windows-shaped path, matching
|
|
86
|
+
* the POSIX case-sensitivity the rest of this module assumes.
|
|
87
|
+
*/
|
|
88
|
+
export declare function stringDerivedPackageRoot(moduleFilePath?: string): string | null;
|
|
64
89
|
export type LiveRootOptions = {
|
|
65
90
|
/** Explicit `--hq-root` value. Highest precedence. */
|
|
66
91
|
hqRoot?: string;
|
package/dist/utils/hq-roots.js
CHANGED
|
@@ -191,6 +191,73 @@ export function __resetPackageRootCache() {
|
|
|
191
191
|
modulePath: currentModulePath,
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* The trailing path segments that mark an installed copy of this package:
|
|
196
|
+
* `node_modules` followed by the package name's own segments (for
|
|
197
|
+
* `@indigoai-us/hq-cli`, that is `node_modules/@indigoai-us/hq-cli`).
|
|
198
|
+
*/
|
|
199
|
+
const INSTALLED_ROOT_SEGMENTS = ["node_modules", ...CLI_PACKAGE_NAME.split("/")];
|
|
200
|
+
/** A Windows-shaped absolute path (drive letter or UNC), regardless of host OS. */
|
|
201
|
+
function looksWin32Path(p) {
|
|
202
|
+
return /^[a-zA-Z]:[\\/]/.test(p) || /^\\\\/.test(p);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Derive this package's installed root from the running module's OWN path using
|
|
206
|
+
* STRING operations only — no `readFileSync`, `realpathSync`, or `existsSync`.
|
|
207
|
+
*
|
|
208
|
+
* {@link packageRoot} must read `<dir>/package.json` and `realpathSync` the
|
|
209
|
+
* module directory to answer, so it THROWS in exactly the situation that most
|
|
210
|
+
* needs an answer: a concurrent global install has renamed the installed
|
|
211
|
+
* package directory aside (`.hq-cli-<rand>`) and is re-extracting it file by
|
|
212
|
+
* file, so both the manifest walk and the dist-owner fallback fail on a
|
|
213
|
+
* directory that is momentarily gone. This walks the compiled module's
|
|
214
|
+
* ancestors as strings and returns the DEEPEST one whose trailing segments are
|
|
215
|
+
* `node_modules/@indigoai-us/hq-cli`, which stays correct while that directory
|
|
216
|
+
* does not exist on disk.
|
|
217
|
+
*
|
|
218
|
+
* It is a FALLBACK only — {@link packageRoot} validates the manifest name and
|
|
219
|
+
* is authoritative whenever it succeeds. Returns null for a dev checkout or any
|
|
220
|
+
* layout without that segment triple, so a caller that falls back to it can
|
|
221
|
+
* only ever degrade to today's behaviour (no resolution), never widen it.
|
|
222
|
+
*
|
|
223
|
+
* The deepest match is deliberate: a nested `node_modules/@indigoai-us/hq-cli`
|
|
224
|
+
* inside another package resolves to ITSELF (the copy the module belongs to),
|
|
225
|
+
* never an outer decoy. Case is folded only for a Windows-shaped path, matching
|
|
226
|
+
* the POSIX case-sensitivity the rest of this module assumes.
|
|
227
|
+
*/
|
|
228
|
+
export function stringDerivedPackageRoot(moduleFilePath = currentModulePath) {
|
|
229
|
+
if (typeof moduleFilePath !== "string" || moduleFilePath.length === 0) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const wanted = INSTALLED_ROOT_SEGMENTS;
|
|
233
|
+
const caseInsensitive = looksWin32Path(moduleFilePath);
|
|
234
|
+
const sameSegment = (a, b) => caseInsensitive ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
235
|
+
// Tokenise into segments with each one's end offset in the ORIGINAL string,
|
|
236
|
+
// so the returned root keeps the source path's leading root and separators
|
|
237
|
+
// verbatim (a leading `/`, or a `C:\` drive) rather than a rejoined guess.
|
|
238
|
+
const segments = [];
|
|
239
|
+
const segmentPattern = /[^\\/]+/g;
|
|
240
|
+
let match;
|
|
241
|
+
while ((match = segmentPattern.exec(moduleFilePath)) !== null) {
|
|
242
|
+
segments.push({ text: match[0], end: match.index + match[0].length });
|
|
243
|
+
}
|
|
244
|
+
if (segments.length < wanted.length)
|
|
245
|
+
return null;
|
|
246
|
+
// Scan from the deepest segment upward; the first (deepest) leaf whose
|
|
247
|
+
// preceding segments complete the triple wins.
|
|
248
|
+
for (let leaf = segments.length - 1; leaf >= wanted.length - 1; leaf--) {
|
|
249
|
+
let matched = true;
|
|
250
|
+
for (let k = 0; k < wanted.length; k++) {
|
|
251
|
+
if (!sameSegment(segments[leaf - (wanted.length - 1) + k].text, wanted[k])) {
|
|
252
|
+
matched = false;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (matched)
|
|
257
|
+
return moduleFilePath.slice(0, segments[leaf].end);
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
194
261
|
/**
|
|
195
262
|
* Resolve the user's live HQ installation.
|
|
196
263
|
*
|
|
@@ -10,6 +10,8 @@ import * as fs from "fs";
|
|
|
10
10
|
export declare const INCOMPLETE_INSTALL_REMEDY: string;
|
|
11
11
|
/** A resolver for the running install's root; returns null instead of throwing. */
|
|
12
12
|
export type PackageRootResolver = () => string | null;
|
|
13
|
+
/** Which strategy produced the running install's root, recorded in diagnostics. */
|
|
14
|
+
export type PackageRootResolverSource = "manifest-walk" | "string-derivation" | "unresolved" | "injected";
|
|
13
15
|
/**
|
|
14
16
|
* If `err` is an in-process incomplete-install module-load failure — either the
|
|
15
17
|
* CJS relative-sibling shape (HQ-CLI-1N) or the ESM vanished-file shape
|
|
@@ -24,9 +26,21 @@ export type PackageRootResolver = () => string | null;
|
|
|
24
26
|
export declare function incompleteInstallMessage(err: unknown, resolvePackageRoot?: PackageRootResolver, fileSystem?: Pick<typeof fs, "readFileSync">): string | null;
|
|
25
27
|
/** Bounded, scrubber-safe diagnostics for an unattributable esm-loader ENOENT (HQ-CLI-1M). */
|
|
26
28
|
export type IncompleteInstallEsmDiagnostics = {
|
|
29
|
+
/** The resolved install root, or the literal `<unresolved>` when none was found. */
|
|
27
30
|
packageRoot: string;
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
/** True only when a root was actually resolved (by any strategy). */
|
|
32
|
+
packageRootResolved: boolean;
|
|
33
|
+
/** Which resolver answered — so an unresolved root is never read as resolved-but-missing. */
|
|
34
|
+
resolver: PackageRootResolverSource;
|
|
35
|
+
/**
|
|
36
|
+
* Whether `<root>/package.json` and `<root>/node_modules` exist on disk.
|
|
37
|
+
* Present ONLY when a root was resolved: reported for an unresolved root,
|
|
38
|
+
* a bare `false` is indistinguishable from "resolved but the file is missing"
|
|
39
|
+
* — the ambiguity that made the delivered HQ-CLI-1M evidence weaker than
|
|
40
|
+
* intended (the two booleans were uninitialised defaults).
|
|
41
|
+
*/
|
|
42
|
+
packageJsonExists?: boolean;
|
|
43
|
+
nodeModulesExists?: boolean;
|
|
30
44
|
esmLoaderFrame: boolean;
|
|
31
45
|
code: string;
|
|
32
46
|
};
|
|
@@ -42,21 +56,44 @@ export type IncompleteInstallBareDiagnostics = {
|
|
|
42
56
|
requiringPackage: string;
|
|
43
57
|
requirerDeclaresMissing: false;
|
|
44
58
|
};
|
|
59
|
+
/** The four lexical scopes a NOT-suppressed CJS relative requirer can fall into. */
|
|
60
|
+
export type RelativeRequirerScope = "dist" | "assets" | "outside-root" | "unresolved-root";
|
|
45
61
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
62
|
+
* Bounded, scrubber-safe diagnostics for a NOT-suppressed CJS RELATIVE-specifier
|
|
63
|
+
* miss (HQ-CLI-1N): the CJS relative-sibling shape that reached the capture path
|
|
64
|
+
* WITHOUT being suppressed — its requirer sits under the install's own `dist/` or
|
|
65
|
+
* `assets/`, outside the running install entirely, or the install root could not
|
|
66
|
+
* be resolved. Records only a bounded, four-value LEXICAL scope of the requirer
|
|
67
|
+
* against the root — never the requirer path and never the specifier — so
|
|
68
|
+
* grouping cardinality cannot inflate. `code` is always `MODULE_NOT_FOUND`.
|
|
69
|
+
*/
|
|
70
|
+
export type IncompleteInstallRelativeDiagnostics = {
|
|
71
|
+
code: string;
|
|
72
|
+
relativeSpecifier: true;
|
|
73
|
+
packageRoot: string;
|
|
74
|
+
packageRootResolved: boolean;
|
|
75
|
+
resolver: PackageRootResolverSource;
|
|
76
|
+
requirerScope: RelativeRequirerScope;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The enriched shapes as a single OPEN record — every field optional so a
|
|
80
|
+
* consumer can forward any shape to Sentry without narrowing (the boundary and
|
|
81
|
+
* beforeSend only pass the block through). Every value CONSTRUCTED here is
|
|
82
|
+
* exactly one of the strict shapes above; the looseness is only at the read
|
|
50
83
|
* boundary.
|
|
51
84
|
*/
|
|
52
|
-
export type IncompleteInstallDiagnostics = Partial<IncompleteInstallEsmDiagnostics & IncompleteInstallBareDiagnostics>;
|
|
85
|
+
export type IncompleteInstallDiagnostics = Partial<IncompleteInstallEsmDiagnostics & IncompleteInstallBareDiagnostics & IncompleteInstallRelativeDiagnostics>;
|
|
53
86
|
/**
|
|
54
87
|
* When an incomplete-install failure reaches the capture path WITHOUT being
|
|
55
88
|
* suppressed, return a bounded `contexts.incomplete_install` block so the next
|
|
56
89
|
* occurrence carries the evidence this one lacked; otherwise return undefined
|
|
57
|
-
* (bare capture).
|
|
90
|
+
* (bare capture). Three enriched shapes:
|
|
58
91
|
* - a third-party BARE-specifier miss the requirer did not declare (HQ-CLI-1Q)
|
|
59
92
|
* → { missingPackage, requiringPackage, requirerDeclaresMissing:false };
|
|
93
|
+
* - a CJS RELATIVE-specifier miss whose requirer sits under the install's own
|
|
94
|
+
* dist/ or assets/, outside the install, or under an unresolved root
|
|
95
|
+
* (HQ-CLI-1N) → { code, relativeSpecifier:true, packageRoot,
|
|
96
|
+
* packageRootResolved, resolver, requirerScope };
|
|
60
97
|
* - an esm-loader ENOENT whose path did not survive delivery (HQ-CLI-1M) — the
|
|
61
98
|
* shape the delivered payload arrived in, where neither the exception value
|
|
62
99
|
* nor node_system_error carried a `path` → { packageRoot, packageJsonExists,
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
// `fs.readFileSync` ENOENT written by hq's own code stays captured.
|
|
64
64
|
import * as fs from "fs";
|
|
65
65
|
import * as path from "path";
|
|
66
|
-
import { packageRoot } from "./hq-roots.js";
|
|
66
|
+
import { packageRoot, stringDerivedPackageRoot } from "./hq-roots.js";
|
|
67
67
|
import { packageNameOf } from "./install-tree-torn.js";
|
|
68
68
|
import { boundedDiagnosticValue } from "./package-root-diagnostics.js";
|
|
69
69
|
/**
|
|
@@ -85,8 +85,16 @@ export const INCOMPLETE_INSTALL_REMEDY = "hq couldn't load part of its own insta
|
|
|
85
85
|
"`pnpm add -g @indigoai-us/hq-cli`).";
|
|
86
86
|
/** A relative module specifier — `./x`, `../x`, `.\x`, `..\x`. */
|
|
87
87
|
const RELATIVE_SPECIFIER = /^\.\.?[\\/]/;
|
|
88
|
-
/**
|
|
89
|
-
|
|
88
|
+
/**
|
|
89
|
+
* The Node ESM loader's SOURCE-READ frame — `getSourceSync`/`defaultLoad` in
|
|
90
|
+
* `node:internal/modules/esm/load`. Requiring the `esm/load` module frame (not
|
|
91
|
+
* merely any frame under `modules/esm/`) proves the loader was READING the
|
|
92
|
+
* module's source. An ordinary `fs.openSync`/`readFileSync` ENOENT that merely
|
|
93
|
+
* ESCAPES a module's EVALUATION runs under `esm/module_job`, never `esm/load`,
|
|
94
|
+
* so it stays captured rather than suppressed. `load\b` excludes the sibling
|
|
95
|
+
* `esm/loader` module.
|
|
96
|
+
*/
|
|
97
|
+
const ESM_LOADER_FRAME = /node:internal[\\/]modules[\\/]esm[\\/]load\b/;
|
|
90
98
|
const ROOT_DIAGNOSTIC_BYTES = 256;
|
|
91
99
|
const CODE_DIAGNOSTIC_BYTES = 32;
|
|
92
100
|
// Package names live inside hq-cli's own dependency graph, so their universe is
|
|
@@ -95,19 +103,42 @@ const PACKAGE_NAME_DIAGNOSTIC_BYTES = 128;
|
|
|
95
103
|
/** The `<something>/node_modules/<something>` directory-boundary marker. */
|
|
96
104
|
const NODE_MODULES_SEGMENT = "/node_modules/";
|
|
97
105
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
106
|
+
* Resolve the running install's root, trying the authoritative on-disk manifest
|
|
107
|
+
* walk first and falling back to the filesystem-free string derivation ONLY when
|
|
108
|
+
* it throws — the torn-tree case, where a concurrent global install has renamed
|
|
109
|
+
* the package directory aside so packageRoot() cannot read a manifest. Never
|
|
110
|
+
* throws; reports which strategy answered so a captured event is attributable.
|
|
111
|
+
*
|
|
112
|
+
* packageRoot() itself is deliberately NOT widened: resolveBundledAsset() and
|
|
113
|
+
* isWithinPackage() require a root that exists on disk, and handing them a
|
|
114
|
+
* string-derived phantom directory would break them. The fallback lives here,
|
|
115
|
+
* where the only consumer is classification (a lexical prefix test) that needs
|
|
116
|
+
* no live filesystem.
|
|
102
117
|
*/
|
|
103
|
-
function
|
|
118
|
+
function resolvePackageRootWithSource() {
|
|
104
119
|
try {
|
|
105
|
-
return packageRoot();
|
|
120
|
+
return { root: packageRoot(), source: "manifest-walk" };
|
|
106
121
|
}
|
|
107
122
|
catch {
|
|
108
|
-
|
|
123
|
+
const derived = stringDerivedPackageRoot();
|
|
124
|
+
return derived
|
|
125
|
+
? { root: derived, source: "string-derivation" }
|
|
126
|
+
: { root: null, source: "unresolved" };
|
|
109
127
|
}
|
|
110
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* packageRoot() walks up from the compiled module and THROWS
|
|
131
|
+
* PackageRootResolutionError when it cannot resolve. This classifier runs inside
|
|
132
|
+
* beforeSend on EVERY event, so it must never throw — a resolution failure
|
|
133
|
+
* returns null and the error stays captured. When the on-disk walk fails because
|
|
134
|
+
* a concurrent install renamed the package directory aside, the filesystem-free
|
|
135
|
+
* string derivation still supplies the root: that is the HQ-CLI-1M repair —
|
|
136
|
+
* the running install's root must be known even while its directory is
|
|
137
|
+
* momentarily gone.
|
|
138
|
+
*/
|
|
139
|
+
function safePackageRoot() {
|
|
140
|
+
return resolvePackageRootWithSource().root;
|
|
141
|
+
}
|
|
111
142
|
/** Call a (possibly injected) resolver without letting it throw. */
|
|
112
143
|
function resolveRootSafely(resolve) {
|
|
113
144
|
try {
|
|
@@ -137,16 +168,25 @@ function normalizeForCompare(p) {
|
|
|
137
168
|
return looksWin32(p) ? folded.toLowerCase() : folded;
|
|
138
169
|
}
|
|
139
170
|
/**
|
|
140
|
-
* True when `candidate` lives under `<root
|
|
141
|
-
* directory boundary (`<root>` + sep +
|
|
142
|
-
* as `<root>-old
|
|
171
|
+
* True when `candidate` lives directly under `<root>/<subdir>/`. Anchored at a
|
|
172
|
+
* true directory boundary (`<root>` + sep + `<subdir>` + sep) so a sibling such
|
|
173
|
+
* as `<root>-old/<subdir>/...` can never match. `<subdir>` carries no separator,
|
|
174
|
+
* so this stays a single lexical prefix test that needs no live filesystem.
|
|
143
175
|
*/
|
|
144
|
-
function
|
|
176
|
+
function isUnderSubdir(candidate, root, subdir) {
|
|
145
177
|
if (!candidate || !root)
|
|
146
178
|
return false;
|
|
147
|
-
const prefix = `${normalizeForCompare(root)}
|
|
179
|
+
const prefix = `${normalizeForCompare(root)}/${subdir}/`;
|
|
148
180
|
return normalizeForCompare(candidate).startsWith(prefix);
|
|
149
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* True when `candidate` lives under `<root>/node_modules/`. Anchored at a true
|
|
184
|
+
* directory boundary so a sibling such as `<root>-old/node_modules/...` can
|
|
185
|
+
* never match.
|
|
186
|
+
*/
|
|
187
|
+
function isUnderNodeModules(candidate, root) {
|
|
188
|
+
return isUnderSubdir(candidate, root, "node_modules");
|
|
189
|
+
}
|
|
150
190
|
/** The failing specifier from a `Cannot find module '<spec>'` message, or null. */
|
|
151
191
|
function parseMissingSpecifier(message) {
|
|
152
192
|
if (typeof message !== "string")
|
|
@@ -392,13 +432,87 @@ function bareSpecifierCaptureContext(err, resolvePackageRoot, fileSystem) {
|
|
|
392
432
|
},
|
|
393
433
|
};
|
|
394
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Classify a NOT-suppressed CJS relative requirer LEXICALLY against the resolved
|
|
437
|
+
* root — never emitting the path itself, only one of four fixed values. A
|
|
438
|
+
* requirer under `<root>/node_modules/` with a resolved root is ALWAYS suppressed
|
|
439
|
+
* upstream, so that scope is unreachable here and deliberately absent from the
|
|
440
|
+
* enum.
|
|
441
|
+
* - root null (both resolvers failed) → 'unresolved-root'
|
|
442
|
+
* - under `<root>/dist/` → 'dist' (hq-cli's own shipped output)
|
|
443
|
+
* - under `<root>/assets/` → 'assets' (hq-cli's own bundled assets)
|
|
444
|
+
* - anything else (a user project, a `<root>-old` sibling, …) → 'outside-root'
|
|
445
|
+
*/
|
|
446
|
+
function classifyRelativeRequirerScope(requiringFile, root) {
|
|
447
|
+
if (!root)
|
|
448
|
+
return "unresolved-root";
|
|
449
|
+
if (isUnderSubdir(requiringFile, root, "dist"))
|
|
450
|
+
return "dist";
|
|
451
|
+
if (isUnderSubdir(requiringFile, root, "assets"))
|
|
452
|
+
return "assets";
|
|
453
|
+
return "outside-root";
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* When a CJS RELATIVE-specifier miss (HQ-CLI-1N's shape) reached the capture path
|
|
457
|
+
* WITHOUT being suppressed — its requirer sits under the install's own `dist/` or
|
|
458
|
+
* `assets/`, outside the running install entirely, or the install root could not
|
|
459
|
+
* be resolved — return a bounded `incomplete_install` block recording a
|
|
460
|
+
* four-value lexical scope so the next occurrence is attributable instead of
|
|
461
|
+
* bare; otherwise undefined. The suppression decision is delegated to
|
|
462
|
+
* incompleteInstallMessage against the SAME injected resolver, so a suppressed
|
|
463
|
+
* relative miss (requirer under `<root>/node_modules/` with a resolved root) is
|
|
464
|
+
* never double-attributed here. Never throws; never emits a path or the specifier.
|
|
465
|
+
*/
|
|
466
|
+
function relativeSpecifierCaptureContext(err, resolvePackageRoot, fileSystem) {
|
|
467
|
+
if (err === null || typeof err !== "object")
|
|
468
|
+
return undefined;
|
|
469
|
+
const record = err;
|
|
470
|
+
if (record.code !== "MODULE_NOT_FOUND")
|
|
471
|
+
return undefined;
|
|
472
|
+
const requireStack = record.requireStack;
|
|
473
|
+
if (!Array.isArray(requireStack) || typeof requireStack[0] !== "string")
|
|
474
|
+
return undefined;
|
|
475
|
+
const requiringFile = requireStack[0];
|
|
476
|
+
const specifier = parseMissingSpecifier(record.message);
|
|
477
|
+
if (specifier === null || !RELATIVE_SPECIFIER.test(specifier))
|
|
478
|
+
return undefined;
|
|
479
|
+
// Suppressed (requirer under <root>/node_modules with a resolved root) →
|
|
480
|
+
// printed-and-skipped, never captured. Delegated to the classifier against the
|
|
481
|
+
// SAME resolver so the two decisions can never diverge and nothing that would
|
|
482
|
+
// be suppressed is ever double-attributed.
|
|
483
|
+
if (incompleteInstallMessage(err, resolvePackageRoot, readFileFrom(fileSystem)) !== null) {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
// Not suppressed: record which resolver answered and the lexical requirer scope.
|
|
487
|
+
const resolution = resolvePackageRoot === safePackageRoot
|
|
488
|
+
? resolvePackageRootWithSource()
|
|
489
|
+
: {
|
|
490
|
+
root: resolveRootSafely(resolvePackageRoot),
|
|
491
|
+
source: "injected",
|
|
492
|
+
};
|
|
493
|
+
const root = resolution.root;
|
|
494
|
+
return {
|
|
495
|
+
incomplete_install: {
|
|
496
|
+
code: "MODULE_NOT_FOUND",
|
|
497
|
+
relativeSpecifier: true,
|
|
498
|
+
packageRoot: boundedDiagnosticValue(root ?? "<unresolved>", ROOT_DIAGNOSTIC_BYTES),
|
|
499
|
+
packageRootResolved: root !== null,
|
|
500
|
+
resolver: resolution.source,
|
|
501
|
+
requirerScope: classifyRelativeRequirerScope(requiringFile, root),
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
395
505
|
/**
|
|
396
506
|
* When an incomplete-install failure reaches the capture path WITHOUT being
|
|
397
507
|
* suppressed, return a bounded `contexts.incomplete_install` block so the next
|
|
398
508
|
* occurrence carries the evidence this one lacked; otherwise return undefined
|
|
399
|
-
* (bare capture).
|
|
509
|
+
* (bare capture). Three enriched shapes:
|
|
400
510
|
* - a third-party BARE-specifier miss the requirer did not declare (HQ-CLI-1Q)
|
|
401
511
|
* → { missingPackage, requiringPackage, requirerDeclaresMissing:false };
|
|
512
|
+
* - a CJS RELATIVE-specifier miss whose requirer sits under the install's own
|
|
513
|
+
* dist/ or assets/, outside the install, or under an unresolved root
|
|
514
|
+
* (HQ-CLI-1N) → { code, relativeSpecifier:true, packageRoot,
|
|
515
|
+
* packageRootResolved, resolver, requirerScope };
|
|
402
516
|
* - an esm-loader ENOENT whose path did not survive delivery (HQ-CLI-1M) — the
|
|
403
517
|
* shape the delivered payload arrived in, where neither the exception value
|
|
404
518
|
* nor node_system_error carried a `path` → { packageRoot, packageJsonExists,
|
|
@@ -413,7 +527,11 @@ export function incompleteInstallCaptureContext(err, resolvePackageRoot = safePa
|
|
|
413
527
|
const bare = bareSpecifierCaptureContext(err, resolvePackageRoot, fileSystem);
|
|
414
528
|
if (bare)
|
|
415
529
|
return bare;
|
|
416
|
-
// (B) HQ-CLI-
|
|
530
|
+
// (B) HQ-CLI-1N — the not-suppressed CJS relative-sibling miss, made attributable.
|
|
531
|
+
const relative = relativeSpecifierCaptureContext(err, resolvePackageRoot, fileSystem);
|
|
532
|
+
if (relative)
|
|
533
|
+
return relative;
|
|
534
|
+
// (C) HQ-CLI-1M — the path-less esm-loader ENOENT, unchanged.
|
|
417
535
|
if (!isEsmLoaderEnoent(err))
|
|
418
536
|
return undefined;
|
|
419
537
|
// Only instrument what we did NOT already confidently suppress: a path under
|
|
@@ -422,32 +540,40 @@ export function incompleteInstallCaptureContext(err, resolvePackageRoot = safePa
|
|
|
422
540
|
return undefined;
|
|
423
541
|
}
|
|
424
542
|
const record = err;
|
|
425
|
-
const root = resolveRootSafely(resolvePackageRoot);
|
|
426
543
|
const code = typeof record.code === "string" ? record.code : "";
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return {
|
|
444
|
-
incomplete_install: {
|
|
445
|
-
packageRoot: boundedDiagnosticValue(root ?? "<unresolved>", ROOT_DIAGNOSTIC_BYTES),
|
|
446
|
-
packageJsonExists,
|
|
447
|
-
nodeModulesExists,
|
|
448
|
-
esmLoaderFrame: true,
|
|
449
|
-
code: boundedDiagnosticValue(code, CODE_DIAGNOSTIC_BYTES),
|
|
450
|
-
},
|
|
544
|
+
// The default resolver knows which strategy answered (manifest walk vs the
|
|
545
|
+
// string derivation that survives a torn tree); an injected resolver is
|
|
546
|
+
// opaque, so it is recorded as "injected" and its return used as the root.
|
|
547
|
+
const resolution = resolvePackageRoot === safePackageRoot
|
|
548
|
+
? resolvePackageRootWithSource()
|
|
549
|
+
: {
|
|
550
|
+
root: resolveRootSafely(resolvePackageRoot),
|
|
551
|
+
source: "injected",
|
|
552
|
+
};
|
|
553
|
+
const root = resolution.root;
|
|
554
|
+
const diagnostics = {
|
|
555
|
+
packageRoot: boundedDiagnosticValue(root ?? "<unresolved>", ROOT_DIAGNOSTIC_BYTES),
|
|
556
|
+
packageRootResolved: root !== null,
|
|
557
|
+
resolver: resolution.source,
|
|
558
|
+
esmLoaderFrame: true,
|
|
559
|
+
code: boundedDiagnosticValue(code, CODE_DIAGNOSTIC_BYTES),
|
|
451
560
|
};
|
|
561
|
+
// Report the existence booleans ONLY when a root was resolved. Reported for an
|
|
562
|
+
// unresolved root they were uninitialised `false`s indistinguishable from
|
|
563
|
+
// "resolved but missing" — the instrumentation defect the prior fix shipped.
|
|
564
|
+
if (root !== null) {
|
|
565
|
+
diagnostics.packageJsonExists = safeExistsSync(fileSystem, path.join(root, "package.json"));
|
|
566
|
+
diagnostics.nodeModulesExists = safeExistsSync(fileSystem, path.join(root, "node_modules"));
|
|
567
|
+
}
|
|
568
|
+
return { incomplete_install: diagnostics };
|
|
569
|
+
}
|
|
570
|
+
/** existsSync that never throws — any filesystem error reads as "absent". */
|
|
571
|
+
function safeExistsSync(fileSystem, target) {
|
|
572
|
+
try {
|
|
573
|
+
return fileSystem.existsSync(target);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
452
578
|
}
|
|
453
579
|
//# sourceMappingURL=incomplete-install-error.js.map
|
|
@@ -29,8 +29,12 @@
|
|
|
29
29
|
* ../startup-registration.ts; version-gate.ts / self-update.ts / update-lock.ts
|
|
30
30
|
* are only READ (their exported symbols), never modified.
|
|
31
31
|
*/
|
|
32
|
-
/**
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The loader-error `code`s recovery classifies. The first two mean "a module
|
|
34
|
+
* could not be resolved"; `ENOENT` is the esm-loader dialect where a module was
|
|
35
|
+
* present at resolve and gone at read (HQ-CLI-1M) — see {@link ModuleErrorDialect}.
|
|
36
|
+
*/
|
|
37
|
+
export type ModuleNotFoundCode = "ERR_MODULE_NOT_FOUND" | "MODULE_NOT_FOUND" | "ENOENT";
|
|
34
38
|
/**
|
|
35
39
|
* The closed set of resolution-failure dialects, verified on Node v22.23.1 (the
|
|
36
40
|
* @sentry/node import-in-the-middle hook does not change the shapes):
|
|
@@ -42,10 +46,13 @@ export type ModuleNotFoundCode = "ERR_MODULE_NOT_FOUND" | "MODULE_NOT_FOUND";
|
|
|
42
46
|
* '<abs>'` + `requireStack`.
|
|
43
47
|
* - `cjs-package`: CJS require of a bare specifier — `Cannot find module
|
|
44
48
|
* '<name>'` + `requireStack`.
|
|
49
|
+
* - `esm-enoent`: ESM loader ENOENT (HQ-CLI-1M) — a module present at RESOLVE
|
|
50
|
+
* and gone at READ, so getSourceSync/openSync raises ENOENT
|
|
51
|
+
* (not ERR_MODULE_NOT_FOUND). `err.path` is the vanished file.
|
|
45
52
|
* - `unknown`: a module-not-found whose message did not parse; recovery
|
|
46
53
|
* still waits on the lock / retired-dir / quiet signals.
|
|
47
54
|
*/
|
|
48
|
-
export type ModuleErrorDialect = "esm-path" | "esm-package" | "cjs-path" | "cjs-package" | "unknown";
|
|
55
|
+
export type ModuleErrorDialect = "esm-path" | "esm-package" | "cjs-path" | "cjs-package" | "esm-enoent" | "unknown";
|
|
49
56
|
/**
|
|
50
57
|
* The missing thing, re-resolvable by the readiness probe:
|
|
51
58
|
* - `path`: an absolute filesystem path (a `.js` file, or a package dir).
|
|
@@ -82,10 +89,11 @@ export declare function packageNameOf(specifier: string): string;
|
|
|
82
89
|
/**
|
|
83
90
|
* Classify a thrown value as a module-resolution failure and extract the missing
|
|
84
91
|
* target, or return `null` for anything that is not one. The decision is
|
|
85
|
-
* STRUCTURAL: `err.code` must be exactly `ERR_MODULE_NOT_FOUND
|
|
86
|
-
* `MODULE_NOT_FOUND
|
|
87
|
-
*
|
|
88
|
-
*
|
|
92
|
+
* STRUCTURAL: `err.code` must be exactly `ERR_MODULE_NOT_FOUND`,
|
|
93
|
+
* `MODULE_NOT_FOUND`, or — for the esm-loader ENOENT dialect — `ENOENT` under
|
|
94
|
+
* the full conjunction below. No argv, env, or free text is ever consulted, and
|
|
95
|
+
* any other error (including an import-time throw of another class) returns
|
|
96
|
+
* `null` so it is rethrown to the existing boundary unchanged.
|
|
89
97
|
*/
|
|
90
98
|
export declare function classifyModuleNotFound(err: unknown): ClassifiedModuleError | null;
|
|
91
99
|
/** The filesystem surface the probe and wait use, injectable for hermetic tests. */
|
|
@@ -42,6 +42,16 @@ const PACKAGE_ROOT_BYTES = 256;
|
|
|
42
42
|
const ESM_PACKAGE_RE = /^Cannot find package '([^']+)' imported from (.+)$/s;
|
|
43
43
|
const CJS_MODULE_RE = /^Cannot find module '([^']+)'/s;
|
|
44
44
|
const ESM_MODULE_IMPORTED_RE = /^Cannot find module '([^']+)' imported from (.+)$/s;
|
|
45
|
+
/**
|
|
46
|
+
* The Node ESM loader's SOURCE-READ frame — `getSourceSync`/`defaultLoad` in
|
|
47
|
+
* `node:internal/modules/esm/load`. Requiring the `esm/load` module frame (not
|
|
48
|
+
* merely any frame under `modules/esm/`) is what proves the loader was READING
|
|
49
|
+
* the module's source, so an ordinary `fs.openSync`/`readFileSync` ENOENT that
|
|
50
|
+
* merely ESCAPES a module's EVALUATION — which runs under `esm/module_job`,
|
|
51
|
+
* never `esm/load` — is NOT misclassified as a torn install and does not trigger
|
|
52
|
+
* the settle wait. `load\b` also excludes the sibling `esm/loader` module.
|
|
53
|
+
*/
|
|
54
|
+
const ESM_LOADER_FRAME = /node:internal[\\/]modules[\\/]esm[\\/]load\b/;
|
|
45
55
|
/**
|
|
46
56
|
* Reduce a bare specifier to its PACKAGE name: `@scope/name/sub` → `@scope/name`,
|
|
47
57
|
* `name/sub` → `name`, `name` → `name`. This is the unit the readiness probe
|
|
@@ -57,16 +67,40 @@ export function packageNameOf(specifier) {
|
|
|
57
67
|
/**
|
|
58
68
|
* Classify a thrown value as a module-resolution failure and extract the missing
|
|
59
69
|
* target, or return `null` for anything that is not one. The decision is
|
|
60
|
-
* STRUCTURAL: `err.code` must be exactly `ERR_MODULE_NOT_FOUND
|
|
61
|
-
* `MODULE_NOT_FOUND
|
|
62
|
-
*
|
|
63
|
-
*
|
|
70
|
+
* STRUCTURAL: `err.code` must be exactly `ERR_MODULE_NOT_FOUND`,
|
|
71
|
+
* `MODULE_NOT_FOUND`, or — for the esm-loader ENOENT dialect — `ENOENT` under
|
|
72
|
+
* the full conjunction below. No argv, env, or free text is ever consulted, and
|
|
73
|
+
* any other error (including an import-time throw of another class) returns
|
|
74
|
+
* `null` so it is rethrown to the existing boundary unchanged.
|
|
64
75
|
*/
|
|
65
76
|
export function classifyModuleNotFound(err) {
|
|
66
77
|
if (err === null || typeof err !== "object")
|
|
67
78
|
return null;
|
|
68
79
|
const record = err;
|
|
69
80
|
const code = record.code;
|
|
81
|
+
// (0) esm-loader ENOENT (HQ-CLI-1M): a module present at RESOLVE and gone at
|
|
82
|
+
// READ, so Node's ESM loader raises ENOENT from getSourceSync/openSync rather
|
|
83
|
+
// than ERR_MODULE_NOT_FOUND at resolve. Gated on the FULL conjunction — code
|
|
84
|
+
// ENOENT AND syscall 'open' AND an esm-loader stack frame AND a string path —
|
|
85
|
+
// so an ordinary fs.readFileSync/openSync ENOENT written by hq's own code (no
|
|
86
|
+
// loader frame) cannot enter it. The vanished file IS the re-resolvable target
|
|
87
|
+
// the settle wait polls; recovery then waits and re-execs exactly as for the
|
|
88
|
+
// other dialects.
|
|
89
|
+
if (code === "ENOENT") {
|
|
90
|
+
if (record.syscall === "open" &&
|
|
91
|
+
typeof record.path === "string" &&
|
|
92
|
+
typeof record.stack === "string" &&
|
|
93
|
+
ESM_LOADER_FRAME.test(record.stack)) {
|
|
94
|
+
return {
|
|
95
|
+
code,
|
|
96
|
+
dialect: "esm-enoent",
|
|
97
|
+
specifier: record.path,
|
|
98
|
+
importer: "",
|
|
99
|
+
target: { kind: "path", path: record.path },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
70
104
|
if (code !== "ERR_MODULE_NOT_FOUND" && code !== "MODULE_NOT_FOUND")
|
|
71
105
|
return null;
|
|
72
106
|
const message = typeof record.message === "string" ? record.message : "";
|
package/dist/utils/people.d.ts
CHANGED
|
@@ -22,6 +22,9 @@ export interface PersonRecord {
|
|
|
22
22
|
name: string;
|
|
23
23
|
/** Contact email, when recorded. */
|
|
24
24
|
email?: string;
|
|
25
|
+
/** Canonical identity from authoritative company membership, never local YAML. */
|
|
26
|
+
personUid?: string;
|
|
27
|
+
companyUid?: string;
|
|
25
28
|
/** "internal" (team member) | "external" (client, vendor, …). */
|
|
26
29
|
type?: string;
|
|
27
30
|
/** Role/title inside (or relative to) the company. */
|
|
@@ -31,11 +31,6 @@ export interface EntitlementEntry {
|
|
|
31
31
|
export interface EntitlementListResponse {
|
|
32
32
|
entitlements: EntitlementEntry[];
|
|
33
33
|
}
|
|
34
|
-
export interface EntitlementCheckResponse {
|
|
35
|
-
entitled: boolean;
|
|
36
|
-
tier?: string;
|
|
37
|
-
expires_at?: string;
|
|
38
|
-
}
|
|
39
34
|
export interface DownloadResponse {
|
|
40
35
|
url: string;
|
|
41
36
|
sha256: string;
|
|
@@ -59,7 +54,6 @@ export declare class RegistryClient {
|
|
|
59
54
|
}): Promise<PackageListResponse>;
|
|
60
55
|
getPackage(slug: string): Promise<PackageResponse>;
|
|
61
56
|
getMyEntitlements(): Promise<EntitlementListResponse>;
|
|
62
|
-
checkEntitlement(slug: string): Promise<EntitlementCheckResponse>;
|
|
63
57
|
getDownloadUrl(slug: string, version?: string): Promise<DownloadResponse>;
|
|
64
58
|
}
|
|
65
59
|
//# sourceMappingURL=registry-client.d.ts.map
|
|
@@ -88,9 +88,6 @@ export class RegistryClient {
|
|
|
88
88
|
auth: true,
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
|
-
async checkEntitlement(slug) {
|
|
92
|
-
return this.request('GET', `/entitlements/${encodeURIComponent(slug)}`, { auth: true });
|
|
93
|
-
}
|
|
94
91
|
async getDownloadUrl(slug, version) {
|
|
95
92
|
const params = new URLSearchParams();
|
|
96
93
|
if (version)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.109.
|
|
4
|
-
"description": "HQ by Indigo management CLI
|
|
3
|
+
"version": "5.109.8",
|
|
4
|
+
"description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"hq": "dist/index.js",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary",
|
|
27
27
|
"vitest": "vitest",
|
|
28
28
|
"clean": "rm -rf dist",
|
|
29
|
-
"prepare": "husky || true"
|
|
29
|
+
"prepare": "husky || true",
|
|
30
|
+
"check:no-hermes": "bash scripts/check-no-hermes.sh"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
33
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|