@indigoai-us/hq-cli 5.345.147 → 5.345.149
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 +65 -0
- package/README.md +2 -0
- package/assets/bot-workers/setup/worker.yaml +11 -0
- package/dist/command-catalog.generated.d.ts +341 -11
- package/dist/command-catalog.generated.js +421 -11
- package/dist/command-help.js +2 -1
- package/dist/commands/access.d.ts +45 -2
- package/dist/commands/access.js +537 -223
- package/dist/commands/agent-inbox.js +2 -1
- package/dist/commands/billing-usage.d.ts +33 -2
- package/dist/commands/billing-usage.js +75 -4
- package/dist/commands/bot.js +2 -2
- package/dist/commands/channels.d.ts +27 -0
- package/dist/commands/channels.js +108 -4
- package/dist/commands/cloud.js +31 -1
- package/dist/commands/core-refresh-vault-access-native.d.ts +33 -0
- package/dist/commands/core-refresh-vault-access-native.js +356 -28
- package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
- package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
- package/dist/commands/core.js +9 -0
- package/dist/commands/daemon.d.ts +7 -0
- package/dist/commands/daemon.js +37 -3
- package/dist/commands/dm.js +18 -3
- package/dist/commands/doctor.d.ts +11 -1
- package/dist/commands/doctor.js +56 -1
- package/dist/commands/install-global.d.ts +8 -8
- package/dist/commands/install-global.js +139 -61
- package/dist/commands/lanes-shared.d.ts +3 -1
- package/dist/commands/lanes.d.ts +4 -4
- package/dist/commands/lanes.js +380 -25
- package/dist/commands/library-add.d.ts +31 -1
- package/dist/commands/library-add.js +69 -2
- package/dist/commands/library.js +37 -3
- package/dist/commands/pack-install.d.ts +2 -2
- package/dist/commands/pack-install.js +133 -35
- package/dist/commands/reindex.js +19 -0
- package/dist/commands/sync-manifest.d.ts +2 -9
- package/dist/commands/sync-manifest.js +4 -15
- package/dist/hook-engine-path.d.ts +4 -0
- package/dist/hook-engine-path.js +43 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2783 -2354
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +76 -5
- package/dist/lazy-commands.js +2 -0
- package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
- package/dist/lib/agent-kit/inbox-redact.js +361 -0
- package/dist/lib/agent-kit/inbox-state.js +11 -2
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +36 -7
- package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
- package/dist/lib/anywhere-runtime-flag-key.js +9 -0
- package/dist/lib/billing/usage-cost.d.ts +31 -1
- package/dist/lib/billing/usage-cost.js +46 -0
- package/dist/lib/company-skill-wrapper.js +6 -4
- package/dist/lib/daemon/install.d.ts +47 -1
- package/dist/lib/daemon/install.js +181 -15
- package/dist/lib/daemon/lock.d.ts +32 -0
- package/dist/lib/daemon/lock.js +55 -2
- package/dist/lib/db/local.d.ts +2 -2
- package/dist/lib/db/local.js +2 -2
- package/dist/lib/db/sql.js +2 -2
- package/dist/lib/db/sqlite-binding.d.ts +22 -0
- package/dist/lib/db/sqlite-binding.js +168 -0
- package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +15 -0
- package/dist/lib/doctor/checks/anywhere.js +77 -11
- package/dist/lib/doctor/checks/cli-path.d.ts +12 -0
- package/dist/lib/doctor/checks/cli-path.js +46 -0
- package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
- package/dist/lib/doctor/checks/hook-engine.js +17 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
- package/dist/lib/doctor/checks/sync-health.js +38 -2
- package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
- package/dist/lib/doctor/checks/temp-homes.js +54 -0
- package/dist/lib/doctor/fix/apply.d.ts +90 -1
- package/dist/lib/doctor/fix/apply.js +176 -10
- package/dist/lib/doctor/fix/json-output.d.ts +4 -1
- package/dist/lib/doctor/fix/json-output.js +6 -0
- package/dist/lib/doctor/fix/remediation.d.ts +7 -1
- package/dist/lib/doctor/fix/remediation.js +23 -1
- package/dist/lib/doctor/registry.js +10 -0
- package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
- package/dist/lib/doctor/temp-home-sweeper.js +418 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/flag-registry.js +9 -1
- package/dist/lib/flag-snapshot-file.js +8 -3
- package/dist/lib/hook-engine/reconcile.d.ts +9 -0
- package/dist/lib/hook-engine/reconcile.js +134 -0
- package/dist/lib/hook-engine/registration.d.ts +17 -0
- package/dist/lib/hook-engine/registration.js +50 -0
- package/dist/lib/hook-engine/resolve.d.ts +31 -0
- package/dist/lib/hook-engine/resolve.js +134 -0
- package/dist/lib/install/claude-targets.d.ts +2 -1
- package/dist/lib/install/claude-targets.js +8 -3
- package/dist/lib/install/codex-targets.d.ts +3 -0
- package/dist/lib/install/codex-targets.js +15 -4
- package/dist/lib/install/hook-target.d.ts +9 -0
- package/dist/lib/install/hook-target.js +39 -0
- package/dist/lib/lanes/admission-host.d.ts +1 -0
- package/dist/lib/lanes/admission-host.js +8 -1
- package/dist/lib/lanes/admission.d.ts +4 -0
- package/dist/lib/lanes/admission.js +6 -2
- package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/keep-alive.js +4 -1
- package/dist/lib/lanes/local-host-identity.d.ts +40 -0
- package/dist/lib/lanes/local-host-identity.js +314 -0
- package/dist/lib/lanes/reaper.d.ts +1 -1
- package/dist/lib/lanes/reaper.js +7 -0
- package/dist/lib/lanes/reconcile.d.ts +4 -0
- package/dist/lib/lanes/reconcile.js +16 -8
- package/dist/lib/lanes/spawn.d.ts +20 -0
- package/dist/lib/lanes/spawn.js +48 -2
- package/dist/lib/lanes/status-fields.js +6 -1
- package/dist/lib/lanes/store.js +8 -1
- package/dist/lib/lanes/throttle.d.ts +49 -6
- package/dist/lib/lanes/throttle.js +154 -17
- package/dist/lib/lanes/types.d.ts +22 -1
- package/dist/lib/lanes/types.js +27 -3
- package/dist/lib/lanes/watcher.js +27 -2
- package/dist/lib/lanes/worker-continuity.d.ts +2 -0
- package/dist/lib/lanes/worker-continuity.js +18 -11
- package/dist/lib/manifest-disabled.d.ts +13 -0
- package/dist/lib/manifest-disabled.js +19 -0
- package/dist/lib/service-manager/launchd.d.ts +1 -0
- package/dist/lib/service-manager/launchd.js +1 -1
- package/dist/lib/windows/console-encoding.d.ts +24 -0
- package/dist/lib/windows/console-encoding.js +83 -0
- package/dist/lib/windows/install-followup.d.ts +35 -0
- package/dist/lib/windows/install-followup.js +142 -0
- package/dist/lib/windows/user-path.d.ts +125 -0
- package/dist/lib/windows/user-path.js +275 -0
- package/dist/lib/work-context/auto-bind.d.ts +1 -1
- package/dist/lib/work-context/auto-bind.js +22 -3
- package/dist/lib/work-context/lane-bind.d.ts +31 -0
- package/dist/lib/work-context/lane-bind.js +101 -0
- package/dist/lib/work-context/organize.js +1 -0
- package/dist/lib/work-context/prd-sync.js +7 -0
- package/dist/lib/work-context/project.d.ts +36 -10
- package/dist/lib/work-context/project.js +130 -41
- package/dist/lib/work-context/reconcile.d.ts +6 -0
- package/dist/lib/work-context/reconcile.js +25 -5
- package/dist/lib/work-context/state.d.ts +6 -0
- package/dist/lib/work-context/state.js +29 -1
- package/dist/main.js +2 -1
- package/dist/unknown-command.d.ts +18 -0
- package/dist/unknown-command.js +88 -0
- package/dist/utils/cognito-session.d.ts +8 -0
- package/dist/utils/cognito-session.js +14 -2
- package/dist/utils/copy-tree.d.ts +61 -0
- package/dist/utils/copy-tree.js +249 -0
- package/dist/utils/environmental-error.js +7 -3
- package/dist/utils/hook-trust.d.ts +9 -0
- package/dist/utils/hook-trust.js +30 -2
- package/dist/utils/npm-invocation.d.ts +27 -0
- package/dist/utils/npm-invocation.js +54 -0
- package/dist/utils/qmd-native-binding-error.js +3 -1
- package/dist/utils/resolve-executable.d.ts +2 -0
- package/dist/utils/resolve-executable.js +6 -2
- package/dist/utils/safe-link.d.ts +50 -0
- package/dist/utils/safe-link.js +300 -0
- package/package.json +17 -5
- package/scripts/hook-engine-manifest.mjs +22 -0
- package/scripts/install-hook-engine.mjs +10 -0
|
@@ -28,7 +28,7 @@ import http from "node:http";
|
|
|
28
28
|
import https from "node:https";
|
|
29
29
|
import path from "node:path";
|
|
30
30
|
import { Readable } from "node:stream";
|
|
31
|
-
import { formatCloudAssetBlock, formatCloudPlaceholder, parseCloudPlaceholder, } from "@indigoai-us/hq-cloud";
|
|
31
|
+
import { formatCloudAssetBlock, formatCloudPlaceholder, isCoveredByAny, parseCloudPlaceholder, resolvePullScope, } from "@indigoai-us/hq-cloud";
|
|
32
32
|
import { PLACEHOLDER_SUFFIX, companyOffloadKey, containedPath, durablePlaceholderFs, renderPointer, resolvePlaceholder, } from "./storage-offload.js";
|
|
33
33
|
import { formatPlanGateError, formatUpgradeRefusal, planGateErrorFromResponse, } from "../utils/plan-gate-error.js";
|
|
34
34
|
/** Metadata key hq-pro binds to `checksumSha256` (sync's content hash). */
|
|
@@ -321,7 +321,74 @@ export async function runLibraryAdd(inputs, options, deps) {
|
|
|
321
321
|
.catch((err) => ({ status: "failed", note: err.message }));
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
|
-
|
|
324
|
+
const written = items.flatMap((item) => (item.ok && item.placeholderPath ? [item.placeholderPath] : []));
|
|
325
|
+
if (written.length === 0)
|
|
326
|
+
return { company: slug, items };
|
|
327
|
+
const syncScope = await ensureLibraryInSyncScope(company, dirRel, written, deps);
|
|
328
|
+
return { company: slug, items, ...(syncScope ? { syncScope } : {}) };
|
|
329
|
+
}
|
|
330
|
+
const LIBRARY_FOLDER = "library/";
|
|
331
|
+
/**
|
|
332
|
+
* A `shared` or `custom` sync pushes only paths inside the membership's
|
|
333
|
+
* prefix set (explicit grants plus pins), so a placeholder written outside it
|
|
334
|
+
* never reaches the vault. Pin it with `hq files get`'s pin writer: the
|
|
335
|
+
* default library folder as a whole, and for any other `--to` folder only the
|
|
336
|
+
* written placeholders, so the rest of that folder stays out of sync.
|
|
337
|
+
*/
|
|
338
|
+
export async function ensureLibraryInSyncScope(company, dirRel, placeholderPaths, deps) {
|
|
339
|
+
if (!deps.syncScope || !deps.addPin)
|
|
340
|
+
return undefined;
|
|
341
|
+
const companyRel = (rel) => rel.split("/").slice(2).join("/");
|
|
342
|
+
const isLibrary = `${companyRel(dirRel)}/` === LIBRARY_FOLDER;
|
|
343
|
+
const targets = isLibrary ? [LIBRARY_FOLDER] : placeholderPaths.map(companyRel);
|
|
344
|
+
const command = isLibrary ? `hq files get ${LIBRARY_FOLDER} --company ${company.slug}` : undefined;
|
|
345
|
+
let scope;
|
|
346
|
+
try {
|
|
347
|
+
scope = await deps.syncScope(company);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
return {
|
|
351
|
+
status: "not-pinned",
|
|
352
|
+
pins: targets,
|
|
353
|
+
...(command ? { command } : {}),
|
|
354
|
+
reason: `could not read your sync scope: ${err.message}`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const missing = scope.syncMode === "all" ? [] : targets.filter((t) => !isCoveredByAny(t, scope.prefixSet ?? []));
|
|
358
|
+
if (missing.length === 0)
|
|
359
|
+
return { status: "in-scope", pins: [] };
|
|
360
|
+
try {
|
|
361
|
+
for (const target of missing)
|
|
362
|
+
deps.addPin(deps.hqRoot, company.slug, target);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
return {
|
|
366
|
+
status: "not-pinned",
|
|
367
|
+
pins: missing,
|
|
368
|
+
...(command ? { command } : {}),
|
|
369
|
+
reason: `could not write the pin: ${err.message}`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return { status: "pinned", pins: missing };
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* {@link resolvePullScope} answers `all` when any call fails. Report the
|
|
376
|
+
* failure instead, so a failed lookup is not mistaken for full sync.
|
|
377
|
+
*/
|
|
378
|
+
export async function resolveLibrarySyncScope(client, company, hqRoot) {
|
|
379
|
+
let failure;
|
|
380
|
+
const watch = (call) => call.catch((err) => {
|
|
381
|
+
failure ??= err;
|
|
382
|
+
throw err;
|
|
383
|
+
});
|
|
384
|
+
const scope = await resolvePullScope({
|
|
385
|
+
listMyMemberships: () => watch(client.listMyMemberships()),
|
|
386
|
+
getMembershipSyncConfig: (id) => watch(client.getMembershipSyncConfig(id)),
|
|
387
|
+
listMyExplicitGrants: (uid) => watch(client.listMyExplicitGrants(uid)),
|
|
388
|
+
}, company.uid, company.slug, hqRoot);
|
|
389
|
+
if (failure !== undefined)
|
|
390
|
+
throw failure instanceof Error ? failure : new Error(String(failure));
|
|
391
|
+
return scope;
|
|
325
392
|
}
|
|
326
393
|
async function writeAssetPlaceholder(p, options, deps, dirAbs) {
|
|
327
394
|
const ts = offloadTimestamp(deps.now());
|
package/dist/commands/library.js
CHANGED
|
@@ -35,7 +35,7 @@ import { findHqRoot } from "../utils/manifest.js";
|
|
|
35
35
|
import { networkTransportErrorCode } from "../utils/network-transport-error.js";
|
|
36
36
|
import { resolveVaultCredentialForCapability } from "../utils/resolve-vault-credential.js";
|
|
37
37
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
38
|
-
import { defaultLibraryIo, runLibraryAdd, runLibraryTag, } from "./library-add.js";
|
|
38
|
+
import { defaultLibraryIo, resolveLibrarySyncScope, runLibraryAdd, runLibraryTag, } from "./library-add.js";
|
|
39
39
|
import { captionAddedAsset, extractCaptionFrames, runLibraryDescribe, runLocalClaude } from "./library-caption.js";
|
|
40
40
|
import { LIBRARY_LINK_DEFAULT_MINUTES, LIBRARY_LINK_MAX_MINUTES, runLibraryLink } from "./library-link.js";
|
|
41
41
|
/** Mirrors hq-pro's MAX_ASSET_SEARCH_TAGS / tag length bound. */
|
|
@@ -295,8 +295,39 @@ export function formatLibraryAddResult(result) {
|
|
|
295
295
|
return `${line}\n ${chalk.dim(`AI caption added (${item.caption.captionedBy}).`)}`;
|
|
296
296
|
return `${line}\n ${chalk.yellow(item.caption.note ?? "Captioning skipped.")}`;
|
|
297
297
|
})
|
|
298
|
+
.concat(formatLibrarySyncScope(result))
|
|
298
299
|
.join("\n");
|
|
299
300
|
}
|
|
301
|
+
function formatLibrarySyncScope({ company, syncScope }) {
|
|
302
|
+
if (!syncScope || syncScope.status === "in-scope")
|
|
303
|
+
return [];
|
|
304
|
+
const pins = syncScope.pins.map((pin) => `${company}:${pin}`).join(", ");
|
|
305
|
+
if (syncScope.status === "pinned") {
|
|
306
|
+
return [chalk.dim(`Pinned ${pins} so sync uploads these placeholders to the company vault.`)];
|
|
307
|
+
}
|
|
308
|
+
const fix = syncScope.command
|
|
309
|
+
? `Pin the folder with: ${syncScope.command}`
|
|
310
|
+
: `Add ${syncScope.pins.join(", ")} under "${company}" in .hq/pins.json at the HQ root.`;
|
|
311
|
+
return [
|
|
312
|
+
chalk.yellow(`Warning: sync may not upload these placeholders (${syncScope.reason}). ` +
|
|
313
|
+
`Until it does, teammates and cloud search will not see them. ${fix}`),
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
/** Sync scope wiring for `hq library add`; an API key has no sync tree to check. */
|
|
317
|
+
async function librarySyncScopeDeps(credential, hqRoot) {
|
|
318
|
+
if (credential.kind !== "cognito")
|
|
319
|
+
return {};
|
|
320
|
+
const [{ VaultClient }, { buildVaultConfig }, { addPin }] = await Promise.all([
|
|
321
|
+
import("@indigoai-us/hq-cloud"),
|
|
322
|
+
import("../utils/cognito-session.js"),
|
|
323
|
+
import("./files-browse.js"),
|
|
324
|
+
]);
|
|
325
|
+
const client = new VaultClient(buildVaultConfig(credential.token));
|
|
326
|
+
return {
|
|
327
|
+
syncScope: (company) => resolveLibrarySyncScope(client, company, hqRoot),
|
|
328
|
+
addPin,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
300
331
|
/** GET a presigned URL. Errors never echo the URL, which carries a signature. */
|
|
301
332
|
async function downloadPresigned(url) {
|
|
302
333
|
const res = await fetch(url);
|
|
@@ -366,9 +397,11 @@ export function registerLibraryCommand(program) {
|
|
|
366
397
|
.option("--json", "Print machine-readable JSON")
|
|
367
398
|
.action(async (sources, options) => {
|
|
368
399
|
try {
|
|
369
|
-
const
|
|
400
|
+
const credential = await resolveVaultCredentialForCapability("files:write", "library add", {
|
|
370
401
|
interactive: true,
|
|
371
402
|
});
|
|
403
|
+
const { token } = credential;
|
|
404
|
+
const hqRoot = findHqRoot();
|
|
372
405
|
const result = await runLibraryAdd(sources, {
|
|
373
406
|
company: options.company,
|
|
374
407
|
to: options.to,
|
|
@@ -376,11 +409,12 @@ export function registerLibraryCommand(program) {
|
|
|
376
409
|
title: options.title,
|
|
377
410
|
note: options.note,
|
|
378
411
|
}, {
|
|
379
|
-
hqRoot
|
|
412
|
+
hqRoot,
|
|
380
413
|
now: () => new Date(),
|
|
381
414
|
resolveCompany: (slug) => libraryCompany(token, slug),
|
|
382
415
|
presign: (body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
|
|
383
416
|
...defaultLibraryIo,
|
|
417
|
+
...(await librarySyncScopeDeps(credential, hqRoot)),
|
|
384
418
|
...(options.caption === false
|
|
385
419
|
? {}
|
|
386
420
|
: {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Flow:
|
|
24
24
|
* 1. Classify source → transport
|
|
25
|
-
* 2. Fetch payload to tmpdir (npm pack | git clone |
|
|
25
|
+
* 2. Fetch payload to tmpdir (npm pack | git clone | local copy)
|
|
26
26
|
* 3. Parse + validate package.yaml (10 checks from spec)
|
|
27
27
|
* 4. Evaluate `conditional` predicate — skip if exits non-zero
|
|
28
28
|
* 5. Confirm hooks if `contributes.hooks` non-empty (unless --allow-hooks)
|
|
@@ -438,7 +438,7 @@ export interface InstallPackOptions {
|
|
|
438
438
|
* Route this function's human output to stderr (and silence scan-packages
|
|
439
439
|
* stdout) so a caller emitting machine-readable JSON keeps stdout clean.
|
|
440
440
|
* The fetch tools are already quiet/stderr-only (`npm pack --silent`,
|
|
441
|
-
* `
|
|
441
|
+
* `git clone` progress -> stderr), so this is sufficient.
|
|
442
442
|
*/
|
|
443
443
|
quiet?: boolean;
|
|
444
444
|
/**
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Flow:
|
|
24
24
|
* 1. Classify source → transport
|
|
25
|
-
* 2. Fetch payload to tmpdir (npm pack | git clone |
|
|
25
|
+
* 2. Fetch payload to tmpdir (npm pack | git clone | local copy)
|
|
26
26
|
* 3. Parse + validate package.yaml (10 checks from spec)
|
|
27
27
|
* 4. Evaluate `conditional` predicate — skip if exits non-zero
|
|
28
28
|
* 5. Confirm hooks if `contributes.hooks` non-empty (unless --allow-hooks)
|
|
@@ -38,7 +38,7 @@ import * as os from 'os';
|
|
|
38
38
|
import * as path from 'path';
|
|
39
39
|
import * as readline from 'readline';
|
|
40
40
|
import { parseUserYaml } from '../utils/user-yaml-error.js';
|
|
41
|
-
import { createHash, createPublicKey, verify as cryptoVerify, } from 'node:crypto';
|
|
41
|
+
import { createHash, createPublicKey, randomBytes, verify as cryptoVerify, } from 'node:crypto';
|
|
42
42
|
import { trustedMarketplaceSigningKey } from '../lib/pack-signing-keys.js';
|
|
43
43
|
import { execFileSync, spawnSync } from 'child_process';
|
|
44
44
|
import chalk from 'chalk';
|
|
@@ -51,6 +51,8 @@ import { codexTargets } from '../lib/install/codex-targets.js';
|
|
|
51
51
|
import { readHqVersion, routeContribution, listInstalledPacks, mcpServerNameForContribution } from '../utils/pack-contributions.js';
|
|
52
52
|
import { CONTRIBUTION_TABLE, payloadFor } from '../utils/contribution-table.js';
|
|
53
53
|
import { safeExtractTarball } from './safe-extract.js';
|
|
54
|
+
import { copyTree, removeTreeNoFollow } from '../utils/copy-tree.js';
|
|
55
|
+
import { npmInvocation } from '../utils/npm-invocation.js';
|
|
54
56
|
import { runResidentCommandWithPackageLease } from '../utils/resident-package-preload.js';
|
|
55
57
|
import { getCompanyUid, vaultApiFetch, vaultApiFetchPublic, } from '../utils/vault-api.js';
|
|
56
58
|
import { ensureCognitoToken } from '../utils/cognito-session.js';
|
|
@@ -116,6 +118,18 @@ async function latestWithDiskCache(cacheKey, opts, refresh) {
|
|
|
116
118
|
export const MARKETPLACE_PREFIX = 'marketplace:';
|
|
117
119
|
/** Pinned source allowed to claim the reserved hq-anywhere pack name via its CLI alias. */
|
|
118
120
|
export const PINNED_HQ_ANYWHERE_SOURCE = 'github:indigoai-us/hq-packages#packages/hq-pack-hq-anywhere';
|
|
121
|
+
/**
|
|
122
|
+
* Windows path forms a user (or `os.tmpdir()`) hands to `hq install`: a drive
|
|
123
|
+
* path (`C:\packs\x`, `C:/packs/x`), a UNC path (`\\server\share\x`), or a
|
|
124
|
+
* backslash-relative path (`.\x`, `..\x`). None of these can be a registry
|
|
125
|
+
* slug, npm name or git URL, so they always mean the local transport.
|
|
126
|
+
*/
|
|
127
|
+
function isWindowsPathSource(source) {
|
|
128
|
+
return (/^[A-Za-z]:[\\/]/.test(source) ||
|
|
129
|
+
source.startsWith('\\\\') ||
|
|
130
|
+
source.startsWith('.\\') ||
|
|
131
|
+
source.startsWith('..\\'));
|
|
132
|
+
}
|
|
119
133
|
export function classify(source) {
|
|
120
134
|
// US-006: marketplace:<slug>[@version] is recognized BEFORE the legacy
|
|
121
135
|
// registry fallback and ahead of every other transport. The slug is resolved
|
|
@@ -144,7 +158,8 @@ export function classify(source) {
|
|
|
144
158
|
if (source.startsWith('./') ||
|
|
145
159
|
source.startsWith('../') ||
|
|
146
160
|
source.startsWith('/') ||
|
|
147
|
-
source.startsWith('file:')
|
|
161
|
+
source.startsWith('file:') ||
|
|
162
|
+
isWindowsPathSource(source)) {
|
|
148
163
|
return 'local';
|
|
149
164
|
}
|
|
150
165
|
// Fallback: bare slug — caller (dispatcher) routes those to legacy flow.
|
|
@@ -204,8 +219,10 @@ function fetchNpm(source, tmpDir, integrity) {
|
|
|
204
219
|
// Use `npm pack` to grab the tarball without actually installing anything.
|
|
205
220
|
// Capture output to get the produced filename. Arguments are passed as an
|
|
206
221
|
// argv array — never interpolated into a shell string — so `source` cannot
|
|
207
|
-
// break out even if it contains metacharacters.
|
|
208
|
-
|
|
222
|
+
// break out even if it contains metacharacters. On Windows npm is a .cmd
|
|
223
|
+
// shim, so npmInvocation runs npm-cli.js with node instead of a shell.
|
|
224
|
+
const npm = npmInvocation(['pack', '--silent', '--pack-destination', tmpDir, source]);
|
|
225
|
+
const out = execFileSync(npm.command, npm.args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'inherit'] });
|
|
209
226
|
const tarball = out.trim().split('\n').filter(Boolean).pop();
|
|
210
227
|
if (!tarball) {
|
|
211
228
|
throw new Error(`npm pack produced no tarball for "${source}"`);
|
|
@@ -557,7 +574,7 @@ function fetchGit(source, tmpDir, followBranch) {
|
|
|
557
574
|
}
|
|
558
575
|
payloadDir = path.join(tmpDir, 'payload');
|
|
559
576
|
fs.mkdirSync(payloadDir, { recursive: true });
|
|
560
|
-
|
|
577
|
+
copyPayloadDir(subAbs, payloadDir);
|
|
561
578
|
}
|
|
562
579
|
else {
|
|
563
580
|
// Drop .git — the pack should be file content, not a nested repo
|
|
@@ -581,24 +598,17 @@ function fetchLocal(source, tmpDir) {
|
|
|
581
598
|
}
|
|
582
599
|
const payloadDir = path.join(tmpDir, 'local');
|
|
583
600
|
fs.mkdirSync(payloadDir, { recursive: true });
|
|
584
|
-
|
|
601
|
+
copyPayloadDir(abs, payloadDir);
|
|
585
602
|
return { payloadDir, resolvedSource: clean };
|
|
586
603
|
}
|
|
587
604
|
/**
|
|
588
|
-
*
|
|
589
|
-
*
|
|
605
|
+
* Copy a pack source directory's contents into the payload dir, skipping VCS
|
|
606
|
+
* metadata, installed dependencies and Finder files at any depth. This is the
|
|
607
|
+
* Node equivalent of the earlier `rsync -a --exclude=...` call, so it also
|
|
608
|
+
* works on Windows, which ships no rsync. Links are copied as links.
|
|
590
609
|
*/
|
|
591
|
-
function
|
|
592
|
-
|
|
593
|
-
const destSlashed = dest.endsWith('/') ? dest : `${dest}/`;
|
|
594
|
-
execFileSync('rsync', [
|
|
595
|
-
'-a',
|
|
596
|
-
'--exclude=.git',
|
|
597
|
-
'--exclude=node_modules',
|
|
598
|
-
'--exclude=.DS_Store',
|
|
599
|
-
srcSlashed,
|
|
600
|
-
destSlashed,
|
|
601
|
-
], { stdio: 'inherit' });
|
|
610
|
+
function copyPayloadDir(src, dest) {
|
|
611
|
+
copyTree(src, dest, { exclude: ['.git', 'node_modules', '.DS_Store'] });
|
|
602
612
|
}
|
|
603
613
|
/**
|
|
604
614
|
* Ask git whether <ref> resolves as a named branch or tag on <url>. Returns
|
|
@@ -1640,33 +1650,121 @@ export function installToPackages(payloadDir, pkg, hqRoot) {
|
|
|
1640
1650
|
const packagesDir = path.join(hqRoot, 'core', 'packages');
|
|
1641
1651
|
fs.mkdirSync(packagesDir, { recursive: true });
|
|
1642
1652
|
const destDir = path.join(packagesDir, pkg.name);
|
|
1643
|
-
// SECURITY/ATOMICITY (US-020): wire atomically.
|
|
1653
|
+
// SECURITY/ATOMICITY (US-020): wire atomically. Copy into a sibling staging
|
|
1644
1654
|
// dir on the SAME filesystem first; only swap it into the final destination
|
|
1645
|
-
// once the copy fully succeeds. A failed/partial
|
|
1655
|
+
// once the copy fully succeeds. A failed/partial copy therefore never
|
|
1646
1656
|
// leaves a half-wired pack dir in place (which scan-packages.sh would then
|
|
1647
1657
|
// wire into host paths). On any error the staging dir is rolled back.
|
|
1648
1658
|
const stagingDir = fs.mkdtempSync(path.join(packagesDir, `.${pkg.name}.staging-`));
|
|
1659
|
+
let asideDir;
|
|
1660
|
+
// Set when the staging dir holds the only complete copy of the new pack and
|
|
1661
|
+
// must survive the cleanup below.
|
|
1662
|
+
let keepStaging = false;
|
|
1649
1663
|
try {
|
|
1650
|
-
//
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
//
|
|
1656
|
-
//
|
|
1657
|
-
//
|
|
1658
|
-
|
|
1659
|
-
|
|
1664
|
+
// Keeps modes, mtimes and symlinks (as links) like `rsync -a`, without
|
|
1665
|
+
// spawning rsync, which Windows does not ship. `finalRoot` makes Windows
|
|
1666
|
+
// junction fallbacks point into destDir, where the copy ends up after the
|
|
1667
|
+
// rename below, so they do not dangle once stagingDir is gone.
|
|
1668
|
+
copyTree(payloadDir, stagingDir, { finalRoot: destDir });
|
|
1669
|
+
// Swap into place. The prior install is renamed aside (not deleted) first,
|
|
1670
|
+
// so if the second rename fails it can be put back and the old pack stays
|
|
1671
|
+
// installed. The aside name is unique and not pre-created: on Windows a
|
|
1672
|
+
// rename onto an existing directory fails.
|
|
1673
|
+
if (lstatOrNull(destDir)) {
|
|
1674
|
+
const aside = path.join(packagesDir, `.${pkg.name}.old-${process.pid}-${Date.now()}-${randomBytes(4).toString('hex')}`);
|
|
1675
|
+
fs.renameSync(destDir, aside);
|
|
1676
|
+
asideDir = aside;
|
|
1677
|
+
}
|
|
1678
|
+
try {
|
|
1679
|
+
fs.renameSync(stagingDir, destDir);
|
|
1680
|
+
}
|
|
1681
|
+
catch (err) {
|
|
1682
|
+
if (asideDir) {
|
|
1683
|
+
try {
|
|
1684
|
+
fs.renameSync(asideDir, destDir);
|
|
1685
|
+
asideDir = undefined;
|
|
1686
|
+
}
|
|
1687
|
+
catch (restoreErr) {
|
|
1688
|
+
// Nothing is installed at destDir now. Keep both copies so either
|
|
1689
|
+
// can be moved into place by hand.
|
|
1690
|
+
keepStaging = true;
|
|
1691
|
+
throw new Error(`Could not move the new ${pkg.name} into ${destDir} (${errorMessage(err)}), and could not restore ` +
|
|
1692
|
+
`the previous install from ${asideDir} (${errorMessage(restoreErr)}). Nothing is installed at ` +
|
|
1693
|
+
`${destDir}. The previous install is at ${asideDir} and the complete new copy is at ${stagingDir}. ` +
|
|
1694
|
+
`Move one of them to ${destDir} by hand.`, { cause: err });
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
throw err;
|
|
1660
1698
|
}
|
|
1661
|
-
fs.renameSync(stagingDir, destDir);
|
|
1662
1699
|
}
|
|
1663
1700
|
finally {
|
|
1664
|
-
|
|
1665
|
-
|
|
1701
|
+
// Cleanup must never replace the error that got us here, so failures are
|
|
1702
|
+
// reported and left behind rather than thrown.
|
|
1703
|
+
if (!keepStaging) {
|
|
1704
|
+
try {
|
|
1705
|
+
removeTreeNoFollow(stagingDir);
|
|
1706
|
+
}
|
|
1707
|
+
catch (cleanupErr) {
|
|
1708
|
+
process.stderr.write(`warning: could not remove staging dir ${stagingDir}: ${errorMessage(cleanupErr)}\n`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
// The new pack is in place. Links inside the old install are removed as
|
|
1713
|
+
// links, never followed, so data they point at survives a reinstall.
|
|
1714
|
+
if (asideDir) {
|
|
1715
|
+
try {
|
|
1716
|
+
removeTreeNoFollow(asideDir);
|
|
1717
|
+
}
|
|
1718
|
+
catch (cleanupErr) {
|
|
1719
|
+
process.stderr.write(`warning: installed ${pkg.name}, but could not remove the previous install at ${asideDir}: ` +
|
|
1720
|
+
`${errorMessage(cleanupErr)}. Delete it by hand.\n`);
|
|
1666
1721
|
}
|
|
1667
1722
|
}
|
|
1723
|
+
sweepLeftovers(packagesDir, pkg.name);
|
|
1668
1724
|
return destDir;
|
|
1669
1725
|
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Remove `.<name>.staging-*` and `.<name>.old-*` siblings that an earlier
|
|
1728
|
+
* interrupted or failed install of the same pack left behind. Runs only after
|
|
1729
|
+
* a new install is in place, so a copy kept for manual recovery is not removed
|
|
1730
|
+
* while nothing else is installed. Links inside are removed as links, and a
|
|
1731
|
+
* failure only warns.
|
|
1732
|
+
*/
|
|
1733
|
+
function sweepLeftovers(packagesDir, name) {
|
|
1734
|
+
const prefixes = [`.${name}.staging-`, `.${name}.old-`];
|
|
1735
|
+
let entries;
|
|
1736
|
+
try {
|
|
1737
|
+
entries = fs.readdirSync(packagesDir);
|
|
1738
|
+
}
|
|
1739
|
+
catch (err) {
|
|
1740
|
+
process.stderr.write(`warning: could not list ${packagesDir} for leftover install folders: ${errorMessage(err)}\n`);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
for (const entry of entries) {
|
|
1744
|
+
if (!prefixes.some((prefix) => entry.startsWith(prefix)))
|
|
1745
|
+
continue;
|
|
1746
|
+
const leftover = path.join(packagesDir, entry);
|
|
1747
|
+
try {
|
|
1748
|
+
removeTreeNoFollow(leftover);
|
|
1749
|
+
}
|
|
1750
|
+
catch (err) {
|
|
1751
|
+
process.stderr.write(`warning: could not remove leftover install folder ${leftover}: ${errorMessage(err)}\n`);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
function lstatOrNull(target) {
|
|
1756
|
+
try {
|
|
1757
|
+
return fs.lstatSync(target);
|
|
1758
|
+
}
|
|
1759
|
+
catch (err) {
|
|
1760
|
+
if (err.code === 'ENOENT')
|
|
1761
|
+
return null;
|
|
1762
|
+
throw err;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
function errorMessage(err) {
|
|
1766
|
+
return err instanceof Error ? err.message : String(err);
|
|
1767
|
+
}
|
|
1670
1768
|
/**
|
|
1671
1769
|
* Stamp the install-time `source:` into the destination package.yaml. This is
|
|
1672
1770
|
* the only on-disk record of which `recommended_packages[].source` produced
|
package/dist/commands/reindex.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { reconcileHookRegistrations } from '../lib/hook-engine/reconcile.js';
|
|
2
|
+
import { trustCodexUserHooks } from '../utils/hook-trust.js';
|
|
1
3
|
/**
|
|
2
4
|
* hq reindex — surface namespaced skills and commands, clean up retired
|
|
3
5
|
* personal-overlay mirror links, capture coding-harness session logs, regenerate
|
|
@@ -775,6 +777,23 @@ export function registerReindexCommand(program) {
|
|
|
775
777
|
// means a sync/rescue holds the shared operation lock and may be
|
|
776
778
|
// rewriting .claude/settings.json right now, and this is an unlocked
|
|
777
779
|
// whole-file read-modify-write.
|
|
780
|
+
// Registration reconcile is best-effort: a failure leaves the current
|
|
781
|
+
// registrations in place and must not stop the rest of reindex.
|
|
782
|
+
try {
|
|
783
|
+
// A file that failed is left as it was; the keys of files that were
|
|
784
|
+
// rewritten are still trusted so Codex keeps running their hooks.
|
|
785
|
+
const { codexUserKeys, failures } = reconcileHookRegistrations(hqRoot);
|
|
786
|
+
for (const failure of failures)
|
|
787
|
+
console.warn(`reindex: hook registration reconcile skipped ${failure}`);
|
|
788
|
+
if (codexUserKeys.length) {
|
|
789
|
+
const trust = await trustCodexUserHooks(hqRoot, codexUserKeys);
|
|
790
|
+
if (trust.status === 'failed')
|
|
791
|
+
console.warn(`reindex: Codex hook trust needs repair: ${trust.reason}`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
console.warn(`reindex: hook registration reconcile skipped: ${error.message}`);
|
|
796
|
+
}
|
|
778
797
|
dropProjectDefaultModeForBypassUser(hqRoot);
|
|
779
798
|
await trustHqRuntimeHooks(hqRoot, undefined, {
|
|
780
799
|
rootAliases: [literalHqRoot],
|
|
@@ -42,17 +42,10 @@
|
|
|
42
42
|
*/
|
|
43
43
|
import { Command } from "commander";
|
|
44
44
|
import type { ManifestUploadTransport, RunManifestUploadPassOptions, RunManifestUploadPassResult } from "@indigoai-us/hq-cloud";
|
|
45
|
+
import { MANIFEST_DISABLED_ENV, manifestUploadsDisabled } from "../lib/manifest-disabled.js";
|
|
45
46
|
/** The server route the pass POSTs each chunk to. */
|
|
46
47
|
export declare const MANIFEST_UPLOAD_PATH = "/v1/sync-manifest/upload";
|
|
47
|
-
|
|
48
|
-
* The kill switch hq-cloud's pass honours. Restated locally (rather than
|
|
49
|
-
* imported) for two reasons: it lives in the same unreleased module as the
|
|
50
|
-
* manifest exports, and — more importantly — we must be able to read it
|
|
51
|
-
* BEFORE deciding to do any work at all.
|
|
52
|
-
*/
|
|
53
|
-
export declare const MANIFEST_DISABLED_ENV = "HQ_SYNC_MANIFEST_DISABLED";
|
|
54
|
-
/** True when the kill switch is set to anything other than empty/0/false. */
|
|
55
|
-
export declare function manifestUploadsDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
48
|
+
export { MANIFEST_DISABLED_ENV, manifestUploadsDisabled };
|
|
56
49
|
/** The literal `--scope` value that selects the personal (non-company) tree. */
|
|
57
50
|
export declare const PERSONAL_SCOPE = "personal";
|
|
58
51
|
/**
|
|
@@ -51,6 +51,7 @@ import chalk from "chalk";
|
|
|
51
51
|
// at runtime and so cost nothing here.
|
|
52
52
|
import { VaultClient } from "@indigoai-us/hq-cloud";
|
|
53
53
|
import { MANIFEST_MIN_HQ_CLOUD_VERSION, manifestExportsAvailable, requireManifestExports, } from "../lib/hq-cloud-manifest.js";
|
|
54
|
+
import { MANIFEST_DISABLED_ENV, manifestUploadsDisabled, } from "../lib/manifest-disabled.js";
|
|
54
55
|
import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
55
56
|
import { loadClientHealthState, persistClientHealthState, } from "../utils/client-health.js";
|
|
56
57
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
@@ -58,21 +59,9 @@ import { readActiveCompanySlug } from "./sync-mode.js";
|
|
|
58
59
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
59
60
|
/** The server route the pass POSTs each chunk to. */
|
|
60
61
|
export const MANIFEST_UPLOAD_PATH = "/v1/sync-manifest/upload";
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* manifest exports, and — more importantly — we must be able to read it
|
|
65
|
-
* BEFORE deciding to do any work at all.
|
|
66
|
-
*/
|
|
67
|
-
export const MANIFEST_DISABLED_ENV = "HQ_SYNC_MANIFEST_DISABLED";
|
|
68
|
-
/** True when the kill switch is set to anything other than empty/0/false. */
|
|
69
|
-
export function manifestUploadsDisabled(env = process.env) {
|
|
70
|
-
const raw = env[MANIFEST_DISABLED_ENV];
|
|
71
|
-
if (raw === undefined)
|
|
72
|
-
return false;
|
|
73
|
-
const value = raw.trim().toLowerCase();
|
|
74
|
-
return value !== "" && value !== "0" && value !== "false";
|
|
75
|
-
}
|
|
62
|
+
// The kill switch lives in a leaf module so `hq doctor` can read it without
|
|
63
|
+
// loading this command. Re-exported for existing importers.
|
|
64
|
+
export { MANIFEST_DISABLED_ENV, manifestUploadsDisabled };
|
|
76
65
|
/** The literal `--scope` value that selects the personal (non-company) tree. */
|
|
77
66
|
export const PERSONAL_SCOPE = "personal";
|
|
78
67
|
// ── Pure helpers ────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Dependency-free routing: this command must never load the online CLI graph. */
|
|
2
|
+
export declare function hookEnginePathArgs(argv: readonly string[]): string[] | null;
|
|
3
|
+
export declare function runHookEnginePath(args: readonly string[]): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=hook-engine-path.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Dependency-free routing: this command must never load the online CLI graph. */
|
|
2
|
+
export function hookEnginePathArgs(argv) {
|
|
3
|
+
if (argv[2] !== 'core')
|
|
4
|
+
return null;
|
|
5
|
+
let i = 3;
|
|
6
|
+
if (argv[i] === '--hq-root' && argv[i + 1])
|
|
7
|
+
i += 2;
|
|
8
|
+
else if (argv[i]?.startsWith('--hq-root='))
|
|
9
|
+
i++;
|
|
10
|
+
return argv[i] === 'hook-engine-path' ? argv.slice(i + 1) : null;
|
|
11
|
+
}
|
|
12
|
+
export async function runHookEnginePath(args) {
|
|
13
|
+
try {
|
|
14
|
+
if (args.some(arg => !['--repair', '--help', '-h', '--for-registration'].includes(arg))) {
|
|
15
|
+
throw new Error('Usage: hq core hook-engine-path [--repair] [--for-registration]');
|
|
16
|
+
}
|
|
17
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
18
|
+
console.log('Usage: hq core hook-engine-path [--repair] [--for-registration]\nResolve and pin the installed native hook engine offline.\n--for-registration prints a path only when the launcher flag is enabled.');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const { installHookEngine, hookEngineRequired, HOOK_ENGINE_REPAIR } = await import('./lib/hook-engine/resolve.js');
|
|
22
|
+
if (args.includes('--for-registration')) {
|
|
23
|
+
const { launcherEnabled } = await import('./lib/hook-engine/registration.js');
|
|
24
|
+
if (!launcherEnabled()) {
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const binary = installHookEngine({ repair: args.includes('--repair') });
|
|
30
|
+
if (binary)
|
|
31
|
+
console.log(binary);
|
|
32
|
+
else {
|
|
33
|
+
console.error(`Native hook engine missing; ${hookEngineRequired() ? 'repair before running hooks' : 'existing bash hooks remain active'}. Repair: ${HOOK_ENGINE_REPAIR}`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
// Catch locally: the global rejection reporter imports main.js and Sentry.
|
|
39
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=hook-engine-path.js.map
|