@indigoai-us/hq-cli 5.345.28 → 5.345.30
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 +10 -0
- package/assets/scaffold/core/scripts/hq-delegate-verify.sh +1 -1
- package/assets/scaffold/core/scripts/resolve-company.sh +157 -69
- package/dist/commands/core-checkpoint-fast.d.ts +3 -0
- package/dist/commands/core-checkpoint-fast.js +17 -0
- package/dist/commands/core-checkpoint-native.d.ts +76 -0
- package/dist/commands/core-checkpoint-native.js +1252 -0
- package/dist/commands/core-checkpoint.d.ts +2 -75
- package/dist/commands/core-checkpoint.js +2 -1248
- package/dist/commands/core-resolve-company-command.js +1 -0
- package/dist/commands/core-resolve-company-native.d.ts +2 -2
- package/dist/commands/core-resolve-company-native.js +149 -12
- package/dist/fast-scaffold.js +10 -0
- package/dist/lib/doctor/checks/sync-health.js +9 -1
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ type HqResult = {
|
|
|
10
10
|
code: number;
|
|
11
11
|
stdout: string;
|
|
12
12
|
};
|
|
13
|
-
type ResolveCompanyFs = Pick<typeof fs, "readFileSync" | "statSync" | "accessSync">;
|
|
13
|
+
type ResolveCompanyFs = Pick<typeof fs, "readFileSync" | "statSync" | "lstatSync" | "accessSync">;
|
|
14
14
|
export type ResolveCompanyDependencies = {
|
|
15
15
|
cwd: string;
|
|
16
16
|
hqRoot?: string;
|
|
@@ -18,7 +18,7 @@ export type ResolveCompanyDependencies = {
|
|
|
18
18
|
fs?: ResolveCompanyFs;
|
|
19
19
|
stdin?: string;
|
|
20
20
|
stdinIsTty?: boolean;
|
|
21
|
-
spawnHq?: (args: string[], env: NodeJS.ProcessEnv, timeoutMs: number, cwd?: string) => Promise<HqResult>;
|
|
21
|
+
spawnHq?: (args: string[], env: NodeJS.ProcessEnv, timeoutMs: number, cwd?: string, command?: string) => Promise<HqResult>;
|
|
22
22
|
pid?: number;
|
|
23
23
|
random?: () => number;
|
|
24
24
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Native port of resolve-company.sh. This module stays on the fast import path. */
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
|
|
6
|
+
import { buildCmdExeSpawnPlan, isWindowsBatchFile } from "../utils/windows-spawn.js";
|
|
6
7
|
export const RESOLVE_COMPANY_ASSET = "core/scripts/resolve-company.sh";
|
|
7
8
|
export const RESOLVE_COMPANY_HELP = [
|
|
8
9
|
"hq-core: public",
|
|
@@ -15,6 +16,13 @@ export const RESOLVE_COMPANY_HELP = [
|
|
|
15
16
|
"Output: {\"company\":\"<slug>\",\"source\":\"prompt|session|device_default|none\"}",
|
|
16
17
|
"Always exits 0 and never writes state.",
|
|
17
18
|
"",
|
|
19
|
+
"Folder mode (--path <dir>): ask the repo-to-company registry through",
|
|
20
|
+
"`$HQ_CLI_BIN resolve-company --path <dir> --json` (HQ_CLI_BIN defaults to",
|
|
21
|
+
"`hq`). Sources: registry (linked folder), registry_miss (not linked), or",
|
|
22
|
+
"unavailable (CLI missing, failed, timed out, or returned bad output). A",
|
|
23
|
+
"registry answer is accepted only when it names a real companies/<slug>",
|
|
24
|
+
"directory. Folder mode never falls back to prompt, session, or device default.",
|
|
25
|
+
"",
|
|
18
26
|
"set -uo pipefail",
|
|
19
27
|
"",
|
|
20
28
|
"ROOT=\"\"",
|
|
@@ -222,21 +230,49 @@ function validPreflight(text, slug, session, operation) {
|
|
|
222
230
|
value.companySlug === slug &&
|
|
223
231
|
typeof value.companyUid === "string" && value.companyUid.length > 0);
|
|
224
232
|
}
|
|
225
|
-
function
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
233
|
+
function commandPath(name, env) {
|
|
234
|
+
const hasPathSeparator = name.includes("/") || name.includes("\\");
|
|
235
|
+
const pathKey = Object.keys(env).find((key) => key.toUpperCase() === "PATH");
|
|
236
|
+
const candidates = hasPathSeparator
|
|
237
|
+
? [name]
|
|
238
|
+
: (pathKey ? env[pathKey] ?? "" : "").split(path.delimiter).flatMap((directory) => {
|
|
239
|
+
const base = path.join(directory || ".", name);
|
|
240
|
+
return process.platform === "win32" && !path.extname(name)
|
|
241
|
+
? [`${base}.com`, `${base}.exe`, `${base}.bat`, `${base}.cmd`]
|
|
242
|
+
: [base];
|
|
243
|
+
});
|
|
244
|
+
for (const candidate of candidates) {
|
|
229
245
|
try {
|
|
230
246
|
fs.accessSync(candidate, fs.constants.X_OK);
|
|
231
|
-
return
|
|
247
|
+
return candidate;
|
|
232
248
|
}
|
|
233
249
|
catch {
|
|
234
250
|
// `command -v` simply advances to the next PATH entry.
|
|
235
251
|
}
|
|
236
252
|
}
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
function commandOnPath(name, env) {
|
|
256
|
+
if (commandPath(name, env))
|
|
257
|
+
return true;
|
|
258
|
+
if (process.platform !== "win32" || name.includes("/") || name.includes("\\") || path.extname(name))
|
|
259
|
+
return false;
|
|
260
|
+
const pathKey = Object.keys(env).find((key) => key.toUpperCase() === "PATH");
|
|
261
|
+
for (const directory of (pathKey ? env[pathKey] ?? "" : "").split(path.delimiter)) {
|
|
262
|
+
try {
|
|
263
|
+
// Git Bash considers an executable extensionless script a command. Keep
|
|
264
|
+
// the presence check faithful to that shell contract; actual Windows
|
|
265
|
+
// process launch still selects a PATHEXT executable or batch shim.
|
|
266
|
+
fs.accessSync(path.join(directory || ".", name), fs.constants.X_OK);
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// `command -v` advances through each PATH entry.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
237
273
|
return false;
|
|
238
274
|
}
|
|
239
|
-
function runHq(args, env, timeoutMs, cwd) {
|
|
275
|
+
function runHq(args, env, timeoutMs, cwd, command = "hq") {
|
|
240
276
|
return new Promise((resolve) => {
|
|
241
277
|
let settled = false;
|
|
242
278
|
let stdout = "";
|
|
@@ -247,7 +283,15 @@ function runHq(args, env, timeoutMs, cwd) {
|
|
|
247
283
|
if (!child?.pid)
|
|
248
284
|
return;
|
|
249
285
|
try {
|
|
250
|
-
process.
|
|
286
|
+
if (process.platform === "win32") {
|
|
287
|
+
const taskkill = path.join(env.SystemRoot ?? env.WINDIR ?? "C:\\Windows", "System32", "taskkill.exe");
|
|
288
|
+
const result = spawnSync(taskkill, ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true });
|
|
289
|
+
if (result.error || result.status !== 0)
|
|
290
|
+
child.kill(signal);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
process.kill(-child.pid, signal);
|
|
294
|
+
}
|
|
251
295
|
}
|
|
252
296
|
catch (error) {
|
|
253
297
|
if (errorCode(error) !== "ESRCH")
|
|
@@ -279,7 +323,24 @@ function runHq(args, env, timeoutMs, cwd) {
|
|
|
279
323
|
}
|
|
280
324
|
}, timeoutMs);
|
|
281
325
|
try {
|
|
282
|
-
|
|
326
|
+
const resolved = commandPath(command, env);
|
|
327
|
+
if (!resolved) {
|
|
328
|
+
finish({ code: 127, stdout });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
let executable = resolved;
|
|
332
|
+
let executableArgs = args;
|
|
333
|
+
let windowsVerbatimArguments;
|
|
334
|
+
if (process.platform === "win32" && isWindowsBatchFile(resolved)) {
|
|
335
|
+
const plan = buildCmdExeSpawnPlan(resolved, args, env);
|
|
336
|
+
executable = plan.cmd;
|
|
337
|
+
executableArgs = plan.args;
|
|
338
|
+
windowsVerbatimArguments = plan.windowsVerbatimArguments;
|
|
339
|
+
}
|
|
340
|
+
child = spawn(executable, executableArgs, {
|
|
341
|
+
cwd, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "ignore"], env,
|
|
342
|
+
windowsVerbatimArguments,
|
|
343
|
+
});
|
|
283
344
|
}
|
|
284
345
|
catch {
|
|
285
346
|
finish({ code: 127, stdout });
|
|
@@ -301,6 +362,8 @@ function parseArgs(args) {
|
|
|
301
362
|
let root = "";
|
|
302
363
|
let prompt = "";
|
|
303
364
|
let havePrompt = false;
|
|
365
|
+
let folder = "";
|
|
366
|
+
let haveFolder = false;
|
|
304
367
|
for (let index = 0; index < args.length;) {
|
|
305
368
|
const arg = args[index];
|
|
306
369
|
if (arg === "--root") {
|
|
@@ -312,14 +375,84 @@ function parseArgs(args) {
|
|
|
312
375
|
havePrompt = true;
|
|
313
376
|
index += Math.min(2, args.length - index);
|
|
314
377
|
}
|
|
378
|
+
else if (arg === "--path") {
|
|
379
|
+
folder = args[index + 1] ?? "";
|
|
380
|
+
haveFolder = true;
|
|
381
|
+
index += Math.min(2, args.length - index);
|
|
382
|
+
}
|
|
315
383
|
else if (arg === "--help" || arg === "-h") {
|
|
316
|
-
return { root, prompt, havePrompt, help: true };
|
|
384
|
+
return { root, prompt, havePrompt, folder, haveFolder, help: true };
|
|
317
385
|
}
|
|
318
386
|
else {
|
|
319
387
|
index += 1;
|
|
320
388
|
}
|
|
321
389
|
}
|
|
322
|
-
return { root, prompt, havePrompt, help: false };
|
|
390
|
+
return { root, prompt, havePrompt, folder, haveFolder, help: false };
|
|
391
|
+
}
|
|
392
|
+
function unavailable(reason) {
|
|
393
|
+
return {
|
|
394
|
+
...output("", "unavailable"),
|
|
395
|
+
stderr: Buffer.from(`resolve-company: WARNING company registry unavailable (${reason}); binding personal, not guessing a company\n`),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
async function resolveFolder(folder, root, cwd, env, fileSystem, invoke) {
|
|
399
|
+
if (!folder)
|
|
400
|
+
return unavailable("empty --path");
|
|
401
|
+
const cli = env.HQ_CLI_BIN || "hq";
|
|
402
|
+
if (!commandPath(cli, env))
|
|
403
|
+
return unavailable(`'${cli}' not found`);
|
|
404
|
+
if (!commandPath("jq", env))
|
|
405
|
+
return unavailable("jq not found");
|
|
406
|
+
// The registry command is the contract boundary. Calling it preserves its
|
|
407
|
+
// own hq-anywhere gate and avoids importing the full command graph here.
|
|
408
|
+
const hqEnv = {
|
|
409
|
+
...EMPTY_ENV,
|
|
410
|
+
...env,
|
|
411
|
+
HQ_NO_UPDATE_CHECK: "1",
|
|
412
|
+
HQ_RESOLVE_TIMEOUT_MS: env.HQ_RESOLVE_TIMEOUT_MS || "5000",
|
|
413
|
+
};
|
|
414
|
+
const parsedTimeout = Number(hqEnv.HQ_RESOLVE_TIMEOUT_MS);
|
|
415
|
+
const timeoutMs = parsedTimeout || 2_000;
|
|
416
|
+
const response = await invoke(["resolve-company", "--path", folder, "--json"], hqEnv, timeoutMs, cwd, cli);
|
|
417
|
+
const value = record(parseJson(response.stdout));
|
|
418
|
+
if (response.code !== 0 && !(response.code === 1 && value?.company === null)) {
|
|
419
|
+
return unavailable(`'${cli} resolve-company' exited ${response.code}`);
|
|
420
|
+
}
|
|
421
|
+
if (!value)
|
|
422
|
+
return unavailable(`'${cli} resolve-company' returned unreadable output`);
|
|
423
|
+
if (value.company === null || value.company === undefined)
|
|
424
|
+
return output("", "registry_miss");
|
|
425
|
+
if (typeof value.company !== "string")
|
|
426
|
+
return unavailable(`'${cli} resolve-company' returned unreadable output`);
|
|
427
|
+
// jq -r plus command substitution removes trailing newlines before the
|
|
428
|
+
// shell validates the returned slug.
|
|
429
|
+
const slug = value.company.replace(/\n+$/, "");
|
|
430
|
+
if (!slug || slug === "personal" || slug.startsWith("_") || !/^[A-Za-z0-9_-]+$/.test(slug)) {
|
|
431
|
+
return unavailable(`registry returned invalid company '${slug}'`);
|
|
432
|
+
}
|
|
433
|
+
const companyPath = path.join(root, "companies", slug);
|
|
434
|
+
let isDirectory = false;
|
|
435
|
+
let isSymlink = false;
|
|
436
|
+
try {
|
|
437
|
+
isDirectory = fileSystem.statSync(companyPath).isDirectory();
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
const code = errorCode(error);
|
|
441
|
+
if (code !== "ENOENT" && code !== "ENOTDIR" && code !== "EACCES" && code !== "ELOOP")
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
isSymlink = fileSystem.lstatSync(companyPath).isSymbolicLink();
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
const code = errorCode(error);
|
|
449
|
+
if (code !== "ENOENT" && code !== "ENOTDIR" && code !== "EACCES" && code !== "ELOOP")
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
if (!isDirectory || isSymlink) {
|
|
453
|
+
return unavailable(`registry company '${slug}' has no companies/${slug} directory in ${root}`);
|
|
454
|
+
}
|
|
455
|
+
return output(slug, "registry");
|
|
323
456
|
}
|
|
324
457
|
async function readStdin() {
|
|
325
458
|
const chunks = [];
|
|
@@ -333,11 +466,14 @@ export async function resolveCompany(args, dependencies) {
|
|
|
333
466
|
const parsed = parseArgs(args);
|
|
334
467
|
if (parsed.help)
|
|
335
468
|
return { code: 0, stdout: Buffer.from(RESOLVE_COMPANY_HELP), stderr: Buffer.alloc(0) };
|
|
469
|
+
const root = parsed.root || dependencies.hqRoot || env.HQ_ROOT || env.CLAUDE_PROJECT_DIR || dependencies.cwd;
|
|
470
|
+
if (parsed.haveFolder) {
|
|
471
|
+
return await resolveFolder(parsed.folder, root, dependencies.cwd, env, fileSystem, dependencies.spawnHq ?? runHq);
|
|
472
|
+
}
|
|
336
473
|
let prompt = parsed.prompt;
|
|
337
474
|
if (!parsed.havePrompt && dependencies.stdinIsTty !== true) {
|
|
338
475
|
prompt = dependencies.stdin ?? await readStdin();
|
|
339
476
|
}
|
|
340
|
-
const root = parsed.root || dependencies.hqRoot || env.HQ_ROOT || env.CLAUDE_PROJECT_DIR || dependencies.cwd;
|
|
341
477
|
const manifest = path.join(root, "companies/manifest.yaml");
|
|
342
478
|
if (!isFile(manifest, fileSystem))
|
|
343
479
|
return output("", "none");
|
|
@@ -393,6 +529,7 @@ export async function resolveCompany(args, dependencies) {
|
|
|
393
529
|
export async function runResolveCompanyFast(args, stdin, cwd, hqRoot, _commandName, _hqRootArg, stdinReader = readStdin) {
|
|
394
530
|
const parsed = parseArgs(args);
|
|
395
531
|
const actualStdin = parsed.havePrompt || process.stdin.isTTY === true
|
|
532
|
+
|| parsed.haveFolder
|
|
396
533
|
? stdin
|
|
397
534
|
: stdin || await stdinReader();
|
|
398
535
|
const result = await resolveCompany(args, {
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -184,6 +184,16 @@ export const FAST_SCAFFOLD_SCRIPTS = [
|
|
|
184
184
|
shellHqRoot: "live",
|
|
185
185
|
readsStdin: true,
|
|
186
186
|
},
|
|
187
|
+
{
|
|
188
|
+
name: "checkpoint",
|
|
189
|
+
kind: "native",
|
|
190
|
+
modulePath: "./commands/core-checkpoint-fast.js",
|
|
191
|
+
exportName: "runCheckpointFast",
|
|
192
|
+
flagKey: "hq-core-native-fast-route",
|
|
193
|
+
familyFlagKey: "core-native-session",
|
|
194
|
+
fallbackToCommandGraph: true,
|
|
195
|
+
root: "live",
|
|
196
|
+
},
|
|
187
197
|
{
|
|
188
198
|
name: "hq-session",
|
|
189
199
|
kind: "native",
|
|
@@ -170,6 +170,14 @@ const NON_COMPANY_JOURNAL_SLUGS = new Set([
|
|
|
170
170
|
PERSONAL_SCOPE_SLUG,
|
|
171
171
|
PERSONAL_VAULT_SCOPE_SLUG,
|
|
172
172
|
]);
|
|
173
|
+
/** Keep malformed scopes distinct without letting them imitate valid slugs. */
|
|
174
|
+
function journalSlugForCheckId(slug) {
|
|
175
|
+
if (NON_COMPANY_JOURNAL_SLUGS.has(slug) || isSafeCompanySlug(slug)) {
|
|
176
|
+
return slug;
|
|
177
|
+
}
|
|
178
|
+
const encoded = Array.from(slug, (character) => character.codePointAt(0).toString(16)).join("-");
|
|
179
|
+
return `__hq_invalid_journal_scope__${encoded}`;
|
|
180
|
+
}
|
|
173
181
|
/** True when this journal slug names a company rather than the personal tree. */
|
|
174
182
|
export function isCompanyJournalSlug(slug) {
|
|
175
183
|
return !NON_COMPANY_JOURNAL_SLUGS.has(slug);
|
|
@@ -360,7 +368,7 @@ function journalResults(deps, journals) {
|
|
|
360
368
|
const nowMs = deps.now().getTime();
|
|
361
369
|
return journals.map((entry) => {
|
|
362
370
|
const lastSync = entry.journal?.lastSync;
|
|
363
|
-
const checkId = `sync.journal.${entry.slug}`;
|
|
371
|
+
const checkId = `sync.journal.${journalSlugForCheckId(entry.slug)}`;
|
|
364
372
|
// A company journal without a local tree is leftover membership state,
|
|
365
373
|
// not a live sync target. WARN would make `hq doctor --fix` look like it
|
|
366
374
|
// failed to repair something it was never going to touch (US-015 health
|