@indigoai-us/hq-cloud 6.13.1 → 6.13.3
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/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +30 -0
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +48 -1
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.js +77 -0
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/cli/sync.js +46 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +88 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +10 -0
- package/dist/ignore.js.map +1 -1
- package/dist/ignore.test.js +29 -6
- package/dist/ignore.test.js.map +1 -1
- package/dist/operation-lock.d.ts +31 -0
- package/dist/operation-lock.d.ts.map +1 -1
- package/dist/operation-lock.js +75 -2
- package/dist/operation-lock.js.map +1 -1
- package/dist/operation-lock.test.js +81 -1
- package/dist/operation-lock.test.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/reindex.test.ts +92 -0
- package/src/cli/reindex.ts +46 -0
- package/src/cli/sync.test.ts +105 -0
- package/src/cli/sync.ts +61 -1
- package/src/ignore.test.ts +32 -6
- package/src/ignore.ts +11 -0
- package/src/operation-lock.test.ts +93 -0
- package/src/operation-lock.ts +78 -2
package/src/operation-lock.ts
CHANGED
|
@@ -103,6 +103,12 @@ import * as path from "path";
|
|
|
103
103
|
/** Process exit code used when an operation is refused because the lock is held. */
|
|
104
104
|
export const OPERATION_LOCKED_EXIT = 17;
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Process exit code used when an operation is refused because the lock's state
|
|
108
|
+
* directory is not writable (a permission-class fs error, not a live holder).
|
|
109
|
+
*/
|
|
110
|
+
export const OPERATION_LOCK_UNWRITABLE_EXIT = 18;
|
|
111
|
+
|
|
106
112
|
export interface LockInfo {
|
|
107
113
|
pid: number;
|
|
108
114
|
command: string;
|
|
@@ -128,6 +134,36 @@ export class OperationLockedError extends Error {
|
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Thrown by `acquireOperationLock*` when the per-root lock CANNOT BE CREATED
|
|
139
|
+
* because its state directory is not writable — a permission-class fs error
|
|
140
|
+
* (`EPERM` / `EACCES` / `EROFS`) on the lock dir or its temp file, NOT a live
|
|
141
|
+
* holder. Distinct from {@link OperationLockedError}: nothing is holding the
|
|
142
|
+
* lock; this process simply cannot write there — e.g. `~/.hq` owned by another
|
|
143
|
+
* user (created by a past `sudo` run), a macOS privacy/security restriction
|
|
144
|
+
* (which surfaces as `EPERM: operation not permitted`), or a read-only volume.
|
|
145
|
+
* Callers turn this into a clear, actionable message + clean exit rather than a
|
|
146
|
+
* raw uncaught `fs` crash (HQ-CLI-2).
|
|
147
|
+
*/
|
|
148
|
+
export class OperationLockUnwritableError extends Error {
|
|
149
|
+
constructor(
|
|
150
|
+
public readonly lockDir: string,
|
|
151
|
+
public readonly cause: NodeJS.ErrnoException,
|
|
152
|
+
) {
|
|
153
|
+
super(
|
|
154
|
+
`Cannot create the HQ operation lock in "${lockDir}" ` +
|
|
155
|
+
`(${cause.code}: ${cause.message}). That directory is not writable, so ` +
|
|
156
|
+
`HQ can't guard this operation against a concurrent run. Likely causes: ` +
|
|
157
|
+
`it is owned by another user (e.g. created by a past "sudo" command), a ` +
|
|
158
|
+
`macOS privacy/security restriction, or a read-only volume. To fix: make ` +
|
|
159
|
+
`it writable (e.g. "sudo chown -R $(whoami) ~/.hq"), or point HQ at a ` +
|
|
160
|
+
`writable state directory with HQ_STATE_DIR=<dir>. To bypass the lock for ` +
|
|
161
|
+
`a single run (drops concurrency protection), set HQ_DISABLE_OP_LOCK=1.`,
|
|
162
|
+
);
|
|
163
|
+
this.name = "OperationLockUnwritableError";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
131
167
|
export interface LockHandle {
|
|
132
168
|
/** Absolute path of the lock file. */
|
|
133
169
|
readonly path: string;
|
|
@@ -363,10 +399,42 @@ function disabledHandle(hqRoot: string, command: string): LockHandle {
|
|
|
363
399
|
return { ...NOOP_HANDLE_BASE, path: "", info };
|
|
364
400
|
}
|
|
365
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Permission-class fs error codes that mean the lock's state directory itself is
|
|
404
|
+
* unusable for THIS process (not a transient/EEXIST race). macOS surfaces the
|
|
405
|
+
* restricted-location case as `EPERM`; Linux as `EACCES`; a read-only mount as
|
|
406
|
+
* `EROFS`.
|
|
407
|
+
*/
|
|
408
|
+
const UNWRITABLE_LOCK_CODES = new Set(["EPERM", "EACCES", "EROFS"]);
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* If `err` is a permission-class fs error against the lock dir, rethrow it as an
|
|
412
|
+
* actionable {@link OperationLockUnwritableError}; otherwise rethrow it
|
|
413
|
+
* unchanged. Always throws (return type `never`).
|
|
414
|
+
*
|
|
415
|
+
* Exported for unit testing: ESM module namespaces can't be spied, so the
|
|
416
|
+
* classification decision is verified directly here rather than by mocking
|
|
417
|
+
* `fs.openSync` (see operation-lock.test.ts, HQ-CLI-2).
|
|
418
|
+
*/
|
|
419
|
+
export function rethrowLockCreateError(err: unknown, lockDir: string): never {
|
|
420
|
+
const e = err as NodeJS.ErrnoException | null;
|
|
421
|
+
if (e && typeof e.code === "string" && UNWRITABLE_LOCK_CODES.has(e.code)) {
|
|
422
|
+
throw new OperationLockUnwritableError(lockDir, e);
|
|
423
|
+
}
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
426
|
+
|
|
366
427
|
/** Build the lock payload + ensure the locks dir exists. */
|
|
367
428
|
function prepareLock(hqRoot: string, command: string): { p: string; info: LockInfo; payload: string } {
|
|
368
429
|
const p = lockPathFor(hqRoot);
|
|
369
|
-
|
|
430
|
+
const dir = path.dirname(p);
|
|
431
|
+
try {
|
|
432
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
433
|
+
} catch (err) {
|
|
434
|
+
// The state dir (e.g. ~/.hq) can't be created — surface it as an actionable
|
|
435
|
+
// unwritable-lock error rather than a raw mkdir crash.
|
|
436
|
+
rethrowLockCreateError(err, dir);
|
|
437
|
+
}
|
|
370
438
|
const info: LockInfo = {
|
|
371
439
|
pid: process.pid,
|
|
372
440
|
command,
|
|
@@ -381,7 +449,15 @@ function writeLockTemp(dir: string, payload: string): string {
|
|
|
381
449
|
dir,
|
|
382
450
|
`.operation-lock.${process.pid}.${Date.now()}.${crypto.randomBytes(6).toString("hex")}.tmp`,
|
|
383
451
|
);
|
|
384
|
-
|
|
452
|
+
let fd: number;
|
|
453
|
+
try {
|
|
454
|
+
fd = fs.openSync(tmp, "wx", 0o600);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
// The locks dir isn't writable (e.g. macOS EPERM / Linux EACCES on a
|
|
457
|
+
// root-owned or restricted ~/.hq) — surface an actionable error, not a raw
|
|
458
|
+
// `EPERM: operation not permitted, open` crash (HQ-CLI-2).
|
|
459
|
+
rethrowLockCreateError(err, dir);
|
|
460
|
+
}
|
|
385
461
|
let closed = false;
|
|
386
462
|
try {
|
|
387
463
|
fs.writeSync(fd, payload);
|