@indigoai-us/hq-cli 5.77.6 → 5.77.7
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 +7 -0
- package/dist/commands/integrations.js +24 -1
- package/dist/commands/reindex.d.ts +5 -23
- package/dist/commands/reindex.js +206 -1
- package/dist/utils/version-gate.d.ts +36 -0
- package/dist/utils/version-gate.js +102 -1
- package/package.json +2 -2
- package/pnpm-workspace.yaml +1 -1
- package/src/commands/integrations.test.ts +118 -0
- package/src/commands/integrations.ts +26 -0
- package/src/commands/reindex.test.ts +168 -3
- package/src/commands/reindex.ts +207 -1
- package/src/utils/version-gate.test.ts +176 -0
- package/src/utils/version-gate.ts +127 -1
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { spawnSync } from "node:child_process";
|
|
32
|
-
import { readFileSync } from "node:fs";
|
|
32
|
+
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
33
33
|
import path from "node:path";
|
|
34
34
|
import { fileURLToPath } from "node:url";
|
|
35
35
|
import chalk from "chalk";
|
|
@@ -104,6 +104,109 @@ export function buildPrefixedInstallArgv(prefix: string): string[] {
|
|
|
104
104
|
return ["install", "-g", "--prefix", prefix, LATEST_PACKAGE_SPEC];
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Filesystem surface used by {@link cleanStalePartialInstall}. Injected so the
|
|
109
|
+
* cleanup logic is unit-testable without touching a real global prefix.
|
|
110
|
+
*/
|
|
111
|
+
export interface StaleInstallFs {
|
|
112
|
+
readdirSync: (dir: string) => string[];
|
|
113
|
+
existsSync: (target: string) => boolean;
|
|
114
|
+
readFileSync: (target: string, encoding: "utf-8") => string;
|
|
115
|
+
rmSync: (target: string, options: { recursive: boolean; force: boolean }) => void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const nodeStaleInstallFs: StaleInstallFs = {
|
|
119
|
+
readdirSync: (dir) => readdirSync(dir),
|
|
120
|
+
existsSync,
|
|
121
|
+
readFileSync: (target, encoding) => readFileSync(target, encoding),
|
|
122
|
+
rmSync,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function isHealthyPackageDir(pkgDir: string, fs: StaleInstallFs): boolean {
|
|
126
|
+
try {
|
|
127
|
+
const pkg = JSON.parse(
|
|
128
|
+
fs.readFileSync(path.join(pkgDir, "package.json"), "utf-8"),
|
|
129
|
+
) as { name?: unknown };
|
|
130
|
+
return pkg.name === CLI_NAME;
|
|
131
|
+
} catch {
|
|
132
|
+
return false; // missing / unreadable / malformed package.json ⇒ partial
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Remove leftover artifacts from an interrupted `npm install -g` so a retry can
|
|
138
|
+
* succeed. npm unpacks a package into a `.<pkg>-<rand>` staging dir alongside
|
|
139
|
+
* the final location and then renames it into place; if a previous run was
|
|
140
|
+
* killed mid-rename (or a half-written package dir survives), every subsequent
|
|
141
|
+
* install fails with `ENOTEMPTY` because npm cannot atomically rename over the
|
|
142
|
+
* non-empty leftover. npm does not self-heal this — the stale dir must be
|
|
143
|
+
* removed first.
|
|
144
|
+
*
|
|
145
|
+
* To stay safe we only ever delete:
|
|
146
|
+
* - dot-prefixed npm staging dirs for THIS package (`.hq-cli-*`), and
|
|
147
|
+
* - a package dir whose `package.json` is missing/unreadable or whose `name`
|
|
148
|
+
* is not exactly {@link CLI_NAME} (i.e. a genuinely partial/foreign dir).
|
|
149
|
+
*
|
|
150
|
+
* A healthy install (valid `package.json`, `name === CLI_NAME`) is never
|
|
151
|
+
* touched, so an ordinary version bump still flows through npm untouched.
|
|
152
|
+
*
|
|
153
|
+
* Returns the list of removed paths — empty when there was nothing to clean, so
|
|
154
|
+
* callers can gate a reinstall retry on `removed.length > 0`.
|
|
155
|
+
*/
|
|
156
|
+
export function cleanStalePartialInstall(
|
|
157
|
+
prefix: string,
|
|
158
|
+
fs: StaleInstallFs = nodeStaleInstallFs,
|
|
159
|
+
): string[] {
|
|
160
|
+
const removed: string[] = [];
|
|
161
|
+
const slash = CLI_NAME.indexOf("/");
|
|
162
|
+
const scope = slash === -1 ? null : CLI_NAME.slice(0, slash);
|
|
163
|
+
const leaf = slash === -1 ? CLI_NAME : CLI_NAME.slice(slash + 1);
|
|
164
|
+
const stagingPrefix = `.${leaf}-`;
|
|
165
|
+
|
|
166
|
+
// Global npm keeps packages under `<prefix>/lib/node_modules` (unix) while a
|
|
167
|
+
// bare `--prefix` dir (windows / some sandboxes) uses `<prefix>/node_modules`.
|
|
168
|
+
const nmRoots = [
|
|
169
|
+
path.join(prefix, "lib", "node_modules"),
|
|
170
|
+
path.join(prefix, "node_modules"),
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
for (const nmRoot of nmRoots) {
|
|
174
|
+
// For a scoped package the staging dir + final dir both live inside the
|
|
175
|
+
// scope dir (`.../@indigoai-us/.hq-cli-<rand>`, `.../@indigoai-us/hq-cli`).
|
|
176
|
+
const parentDir = scope ? path.join(nmRoot, scope) : nmRoot;
|
|
177
|
+
let entries: string[];
|
|
178
|
+
try {
|
|
179
|
+
entries = fs.readdirSync(parentDir);
|
|
180
|
+
} catch {
|
|
181
|
+
continue; // this node_modules / scope dir doesn't exist here
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (!entry.startsWith(stagingPrefix)) continue;
|
|
186
|
+
const target = path.join(parentDir, entry);
|
|
187
|
+
try {
|
|
188
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
189
|
+
removed.push(target);
|
|
190
|
+
} catch {
|
|
191
|
+
// best-effort: a dir we can't remove (perms) just means the retry
|
|
192
|
+
// still fails and we fall through to the sudo / manual path.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const pkgDir = path.join(parentDir, leaf);
|
|
197
|
+
if (fs.existsSync(pkgDir) && !isHealthyPackageDir(pkgDir, fs)) {
|
|
198
|
+
try {
|
|
199
|
+
fs.rmSync(pkgDir, { recursive: true, force: true });
|
|
200
|
+
removed.push(pkgDir);
|
|
201
|
+
} catch {
|
|
202
|
+
// best-effort (see above)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return removed;
|
|
208
|
+
}
|
|
209
|
+
|
|
107
210
|
/**
|
|
108
211
|
* Hit POST /v1/client-version/check. Returns the parsed body on 200, or
|
|
109
212
|
* `null` on any failure (caller treats as "no gate"). Tight 3s timeout —
|
|
@@ -213,6 +316,7 @@ function enforceUpdateRequired(
|
|
|
213
316
|
performUpdateString?: (command: string) => UpdateResult;
|
|
214
317
|
resolvePrefix?: () => string | null;
|
|
215
318
|
runner?: UpdateRunner;
|
|
319
|
+
cleanStale?: (prefix: string) => string[];
|
|
216
320
|
} = {},
|
|
217
321
|
): never {
|
|
218
322
|
const banner = chalk.red.bold(
|
|
@@ -262,6 +366,27 @@ function enforceUpdateRequired(
|
|
|
262
366
|
: performUpdate(command!, runner);
|
|
263
367
|
})();
|
|
264
368
|
|
|
369
|
+
// A partial/corrupt global install leaves npm unable to atomically rename its
|
|
370
|
+
// freshly-unpacked package over a leftover directory, so the install above
|
|
371
|
+
// fails with ENOTEMPTY (e.g. a prior interrupted `npm install -g` left a
|
|
372
|
+
// half-written `hq-cli` package dir or a `.hq-cli-<rand>` staging dir under
|
|
373
|
+
// the prefix's node_modules). npm cannot self-heal this. Remove the stale
|
|
374
|
+
// artifacts and retry the install ONCE. Guarded on `cleaned.length > 0` so a
|
|
375
|
+
// plain EACCES on an otherwise-healthy prefix falls straight through to the
|
|
376
|
+
// sudo retry below without a redundant reinstall attempt.
|
|
377
|
+
if (!result.ok && prefix) {
|
|
378
|
+
const cleaner = deps.cleanStale ?? cleanStalePartialInstall;
|
|
379
|
+
const cleaned = cleaner(prefix);
|
|
380
|
+
if (cleaned.length > 0) {
|
|
381
|
+
console.error(
|
|
382
|
+
chalk.dim(
|
|
383
|
+
` Removing stale partial install artifacts and retrying: ${cleaned.join(", ")}`,
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
result = performUpdateCommand("npm", primaryArgs, runner);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
265
390
|
// A root-owned global install (e.g. a system `/usr` install where the CLI runs
|
|
266
391
|
// unprivileged — the outpost agent boxes) can't rewrite the prefix's bin dir,
|
|
267
392
|
// so the install above fails with EACCES (`rename /usr/bin/hq`). Retry ONCE
|
|
@@ -341,6 +466,7 @@ export const __test__ = {
|
|
|
341
466
|
ENDPOINT_PATH,
|
|
342
467
|
FETCH_TIMEOUT_MS,
|
|
343
468
|
buildPrefixedInstallArgv,
|
|
469
|
+
cleanStalePartialInstall,
|
|
344
470
|
enforceUpdateRequired,
|
|
345
471
|
npmPrefixFromPackageDir,
|
|
346
472
|
performUpdate,
|