@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
package/dist/commands/daemon.js
CHANGED
|
@@ -24,9 +24,9 @@ import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits }
|
|
|
24
24
|
import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
25
25
|
import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
|
|
26
26
|
import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
|
|
27
|
-
import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
|
|
27
|
+
import { DAEMON_WINDOWS_TASK, ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, windowsInteractiveSession, } from "../lib/daemon/install.js";
|
|
28
28
|
import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
|
|
29
|
-
import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
|
|
29
|
+
import { isDaemonProcess, liveDaemonLock, runningDaemonPid } from "../lib/daemon/lock.js";
|
|
30
30
|
import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy, readHqCloudUpdateState } from "../lib/daemon/hq-cloud.js";
|
|
31
31
|
import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
|
|
32
32
|
import { daemonPaths, ensurePrivateDir } from "../lib/daemon/paths.js";
|
|
@@ -137,6 +137,8 @@ async function loadDefaults(over) {
|
|
|
137
137
|
resolveHqRoot: over.resolveHqRoot ?? ((flag) => resolveDaemonHqRoot(flag, { env, cwd: process.cwd(), home })),
|
|
138
138
|
userHome: over.userHome ?? ((user) => loginHome(user, run)),
|
|
139
139
|
daemonAlive: over.daemonAlive ?? ((pid) => isDaemonProcess(pid)),
|
|
140
|
+
sleep: over.sleep,
|
|
141
|
+
interactiveSession: over.interactiveSession,
|
|
140
142
|
windowsUser: over.windowsUser ?? (platform === "win32" ? mesh.resolveWindowsUser(env) : ""),
|
|
141
143
|
windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
|
|
142
144
|
systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
|
|
@@ -298,12 +300,42 @@ async function runInstall(d, opts) {
|
|
|
298
300
|
d.writeFile(MACHINE_TYPE_MARKER_PATH, `type=${detected.type}\nuser=${runAsUser}\n`);
|
|
299
301
|
}
|
|
300
302
|
const runAsHome = runAsUser ? d.userHome(runAsUser) : undefined;
|
|
301
|
-
const result = installDaemonUnit(hostFor(d, paths, scope === "user"), { scope, runAsUser, runAsHome }, {
|
|
303
|
+
const result = installDaemonUnit(hostFor(d, paths, scope === "user"), { scope, runAsUser, runAsHome }, {
|
|
304
|
+
run: d.run,
|
|
305
|
+
sleep: d.sleep,
|
|
306
|
+
runningDaemon: () => liveDaemonLock(paths.lockPath, d.daemonAlive),
|
|
307
|
+
});
|
|
302
308
|
handOver(d, { paths, detected, runAsUser });
|
|
309
|
+
if (result.warning)
|
|
310
|
+
d.err(result.warning);
|
|
303
311
|
if (result.kind === "manual") {
|
|
304
312
|
d.out(`This platform has no supported service manager. Start the daemon yourself with:\n ${result.manualCommand}`);
|
|
305
313
|
return;
|
|
306
314
|
}
|
|
315
|
+
if (result.startPending) {
|
|
316
|
+
const signedIn = (d.interactiveSession ?? (() => windowsInteractiveSession(d.run, d.windowsUser ?? "")))();
|
|
317
|
+
// A missing daemon is not a failure when the user is signed out, or when
|
|
318
|
+
// Constrained Language Mode refuses the session check; the latter says the
|
|
319
|
+
// check did not run. A check that failed for any other reason (null) is
|
|
320
|
+
// treated like a known sign-in and exits non-zero.
|
|
321
|
+
if (signedIn === false || signedIn === "unavailable") {
|
|
322
|
+
d.out(`Installed: ${result.unitPath}`);
|
|
323
|
+
if (signedIn === "unavailable") {
|
|
324
|
+
d.out("HQ could not check whether you are signed in to Windows, so it could not confirm the daemon started.");
|
|
325
|
+
}
|
|
326
|
+
d.out("The daemon starts at your next sign-in to Windows. It is not running yet.");
|
|
327
|
+
d.out("Check it with: hq daemon status");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const seconds = WINDOWS_START_WAIT_MS / 1000;
|
|
331
|
+
throw new DaemonCommandError([
|
|
332
|
+
signedIn
|
|
333
|
+
? `Installed ${result.unitPath}, but the daemon did not start within ${seconds} seconds although you are signed in to Windows.`
|
|
334
|
+
: `Installed ${result.unitPath}, but the daemon did not start within ${seconds} seconds, and HQ could not check whether you are signed in to Windows.`,
|
|
335
|
+
"Check it with: hq daemon status",
|
|
336
|
+
`The task's last result: powershell -NoProfile -Command "(Get-ScheduledTaskInfo -TaskName '${DAEMON_WINDOWS_TASK}').LastTaskResult"`,
|
|
337
|
+
].join("\n"));
|
|
338
|
+
}
|
|
307
339
|
if (!result.running) {
|
|
308
340
|
throw new DaemonCommandError(`Installed ${result.unitPath}, but it did not start: ${result.error ?? "unknown error"}`);
|
|
309
341
|
}
|
|
@@ -354,6 +386,8 @@ async function runUninstall(d, opts) {
|
|
|
354
386
|
}
|
|
355
387
|
const result = uninstallDaemonUnit(hostFor(d, paths, false), { scope: installScopeFor(detected.type) }, { run: d.run });
|
|
356
388
|
d.out(result.removed ? `Removed ${result.unitPath}` : "hq daemon was not installed.");
|
|
389
|
+
if (result.warning)
|
|
390
|
+
d.err(result.warning);
|
|
357
391
|
if (opts.restoreLegacy) {
|
|
358
392
|
const restored = restoreLegacyUnits(migrate);
|
|
359
393
|
for (const u of restored.restored)
|
package/dist/commands/dm.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
4
4
|
import { resolveDestinationCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
5
5
|
import { isMachineIdentity } from "@indigoai-us/hq-cloud";
|
|
6
6
|
import { peekIdToken } from "../utils/id-token.js";
|
|
7
|
+
import { redactAgentInboxItem as redactInboxCredentials } from "../lib/agent-kit/inbox-redact.js";
|
|
7
8
|
import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN } from "../lib/notify-recipient.js";
|
|
8
9
|
import { dmForwardCliEnabled } from "./dm-forward-flag.js";
|
|
9
10
|
/**
|
|
@@ -1281,9 +1282,15 @@ function formatAgentInboxItem(item) {
|
|
|
1281
1282
|
const text = str("text") || str("body") || str("subject");
|
|
1282
1283
|
return `${chalk.bold(from)} ${chalk.dim(`[${channel}] ${id}${when ? ` ${when}` : ""}`)}\n ${firstLine(text)}`;
|
|
1283
1284
|
}
|
|
1284
|
-
/**
|
|
1285
|
+
/**
|
|
1286
|
+
* Stdout for `hq dm` drops the whole actingContext. Credential removal itself
|
|
1287
|
+
* is redactInboxCredentials, shared with the mirror, so a later edit cannot
|
|
1288
|
+
* leave a credential key on one path only. The extra omit stays here because
|
|
1289
|
+
* terminal output must not grow the delegation block back.
|
|
1290
|
+
*/
|
|
1285
1291
|
function redactAgentInboxItem(item) {
|
|
1286
|
-
const
|
|
1292
|
+
const redacted = redactInboxCredentials(item);
|
|
1293
|
+
const { actingContext: _actingContext, ...safe } = redacted;
|
|
1287
1294
|
return safe;
|
|
1288
1295
|
}
|
|
1289
1296
|
async function runAgentInbox(token, agentUid, opts) {
|
|
@@ -1458,7 +1465,15 @@ async function resolveChannel(token, target) {
|
|
|
1458
1465
|
async function runDmChannel(target, opts) {
|
|
1459
1466
|
try {
|
|
1460
1467
|
const token = await ensureCognitoToken();
|
|
1461
|
-
|
|
1468
|
+
// A channel id with --json needs no lookup: the id is exact and JSON output
|
|
1469
|
+
// never uses the channel's name. Skipping the list saves fetching every
|
|
1470
|
+
// channel the caller is in (~1000 for an owner), which is what made polled
|
|
1471
|
+
// reads (the super-conductor message wake) take seconds instead of
|
|
1472
|
+
// milliseconds.
|
|
1473
|
+
const rawTarget = target.trim().replace(/^#/, "");
|
|
1474
|
+
const channel = opts.json && /^chn_[A-Za-z0-9]+$/.test(rawTarget)
|
|
1475
|
+
? { channelId: rawTarget }
|
|
1476
|
+
: await resolveChannel(token, target);
|
|
1462
1477
|
const channelId = channel.channelId;
|
|
1463
1478
|
const query = {};
|
|
1464
1479
|
if (opts.limit)
|
|
@@ -25,6 +25,7 @@ import { Command } from "commander";
|
|
|
25
25
|
import { type DoctorRegistry } from "../lib/doctor/registry.js";
|
|
26
26
|
import type { FamilyRun } from "../lib/doctor/types.js";
|
|
27
27
|
import { type DoctorPlatform } from "../lib/doctor/report.js";
|
|
28
|
+
import { type TempHomeSweepResult } from "../lib/doctor/temp-home-sweeper.js";
|
|
28
29
|
/**
|
|
29
30
|
* Marker directories that identify an HQ tree root. A directory is an HQ root
|
|
30
31
|
* only when it contains ALL of these. `.claude` + `core` are present in both
|
|
@@ -126,6 +127,15 @@ export interface RunDoctorResult {
|
|
|
126
127
|
export declare function runDoctor(options?: RunDoctorOptions): Promise<RunDoctorResult>;
|
|
127
128
|
export declare function createClientHealthReasonCodeCompanyResolver(hqRoot: string): (companySlug: string | null) => Promise<boolean>;
|
|
128
129
|
export declare function createClientHealthCheckStatusCompanyResolver(hqRoot: string): (companySlug: string | null) => Promise<boolean>;
|
|
130
|
+
/** Test seams for {@link registerDoctorCommand}. */
|
|
131
|
+
export interface DoctorCommandDeps {
|
|
132
|
+
/** Runs the temp-home sweep for `--sweep-temp-homes`. */
|
|
133
|
+
sweepTempHomes?: (options: {
|
|
134
|
+
dryRun: boolean;
|
|
135
|
+
}) => TempHomeSweepResult;
|
|
136
|
+
}
|
|
137
|
+
/** Text report for `hq doctor --sweep-temp-homes`. */
|
|
138
|
+
export declare function renderTempHomeSweepText(result: TempHomeSweepResult): string;
|
|
129
139
|
/** Register the top-level `hq doctor` command so it appears in `hq --help`. */
|
|
130
|
-
export declare function registerDoctorCommand(program: Command): void;
|
|
140
|
+
export declare function registerDoctorCommand(program: Command, deps?: DoctorCommandDeps): void;
|
|
131
141
|
//# sourceMappingURL=doctor.d.ts.map
|
package/dist/commands/doctor.js
CHANGED
|
@@ -37,6 +37,7 @@ import { clientHealthCompanyScopeFromCheckId } from "../lib/doctor/checks/sync-h
|
|
|
37
37
|
import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
|
|
38
38
|
import * as readline from "node:readline";
|
|
39
39
|
import { runSideEffectTests } from "../lib/doctor/deep/sandbox.js";
|
|
40
|
+
import { sweepTempHomes, } from "../lib/doctor/temp-home-sweeper.js";
|
|
40
41
|
/**
|
|
41
42
|
* Marker directories that identify an HQ tree root. A directory is an HQ root
|
|
42
43
|
* only when it contains ALL of these. `.claude` + `core` are present in both
|
|
@@ -315,8 +316,33 @@ export function createClientHealthCheckStatusCompanyResolver(hqRoot) {
|
|
|
315
316
|
});
|
|
316
317
|
};
|
|
317
318
|
}
|
|
319
|
+
/** Text report for `hq doctor --sweep-temp-homes`. */
|
|
320
|
+
export function renderTempHomeSweepText(result) {
|
|
321
|
+
const lines = [];
|
|
322
|
+
const verb = result.dryRun ? "Would remove" : "Removed";
|
|
323
|
+
for (const entry of result.entries) {
|
|
324
|
+
if (entry.action === "removed" || entry.action === "would-remove") {
|
|
325
|
+
lines.push(` ${verb.toLowerCase()} ${entry.path}`);
|
|
326
|
+
}
|
|
327
|
+
else if (entry.action === "failed") {
|
|
328
|
+
lines.push(` failed ${entry.path}: ${entry.error ?? "unknown error"}`);
|
|
329
|
+
}
|
|
330
|
+
else if (entry.reason === "in-use") {
|
|
331
|
+
lines.push(` kept ${entry.path} (in use)`);
|
|
332
|
+
}
|
|
333
|
+
else if (entry.reason === "in-use-unknown") {
|
|
334
|
+
lines.push(` kept ${entry.path} (could not check whether it is in use: ${entry.error ?? "unknown error"})`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const count = result.entries.filter((entry) => entry.action === "removed" || entry.action === "would-remove").length;
|
|
338
|
+
const failed = result.entries.filter((entry) => entry.action === "failed").length;
|
|
339
|
+
lines.push(`${verb} ${count} idle temp home${count === 1 ? "" : "s"}; ` +
|
|
340
|
+
`${result.dryRun ? "would free" : "freed"} ${result.gbFreed.toFixed(2)} GB.` +
|
|
341
|
+
(failed > 0 ? ` ${failed} could not be removed.` : ""));
|
|
342
|
+
return `${lines.join("\n")}\n`;
|
|
343
|
+
}
|
|
318
344
|
/** Register the top-level `hq doctor` command so it appears in `hq --help`. */
|
|
319
|
-
export function registerDoctorCommand(program) {
|
|
345
|
+
export function registerDoctorCommand(program, deps = {}) {
|
|
320
346
|
program
|
|
321
347
|
.command("doctor")
|
|
322
348
|
.description("Verify HQ hook wiring, runtimes, and connection health (read-only; integrations uses one control-plane inventory read).")
|
|
@@ -330,9 +356,38 @@ export function registerDoctorCommand(program) {
|
|
|
330
356
|
.option("--only <families>", "Run only these comma-separated check families (e.g. hooks). Unknown names are rejected; --json then records the scope.")
|
|
331
357
|
.option("--fix", "Apply the allowlisted safe repairs (backs up first; read-only without this flag).")
|
|
332
358
|
.addOption(new Option("--client-health-report", "Resolve the opt-in hq-flags gate used by the client-health report hook.").hideHelp())
|
|
359
|
+
.option("--sweep-temp-homes", "Delete idle temp homes (90+ minutes untouched, not in use): tmp.* folders holding .rustup or .cargo, and hq-temp-home.* folders left by the wrapper. Reports GB freed.")
|
|
360
|
+
.option("--dry-run", "With --sweep-temp-homes, list what would be removed and delete nothing.")
|
|
333
361
|
.option("--yes", "Skip the interactive --fix confirmation (non-interactive use).")
|
|
334
362
|
.option("--force", "Let --fix run despite uncommitted changes under .claude/, .codex/, or .grok/.")
|
|
335
363
|
.action(async (opts, command) => {
|
|
364
|
+
// `--sweep-temp-homes` is a second write path with its own fixed
|
|
365
|
+
// scope (idle, unused temp homes in the temp dirs). Like `--fix`,
|
|
366
|
+
// it refuses a family scope it would otherwise ignore, and it does not
|
|
367
|
+
// combine with `--fix`.
|
|
368
|
+
if (opts.sweepTempHomes === true) {
|
|
369
|
+
if (parseOnly(opts.only) !== undefined || opts.fix === true) {
|
|
370
|
+
process.stderr.write(`hq doctor: --sweep-temp-homes cannot be combined with --only or --fix.\n` +
|
|
371
|
+
` It removes only idle temp homes in the temp folders; run it on its own.\n`);
|
|
372
|
+
process.exitCode = 1;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const sweep = deps.sweepTempHomes ?? ((options) => sweepTempHomes(options));
|
|
376
|
+
const result = sweep({ dryRun: opts.dryRun === true });
|
|
377
|
+
if (opts.json === true) {
|
|
378
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
process.stdout.write(renderTempHomeSweepText(result));
|
|
382
|
+
}
|
|
383
|
+
process.exitCode = result.entries.some((entry) => entry.action === "failed") ? 1 : 0;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (opts.dryRun === true) {
|
|
387
|
+
process.stderr.write(`hq doctor: --dry-run only applies to --sweep-temp-homes.\n`);
|
|
388
|
+
process.exitCode = 1;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
336
391
|
// `--fix` repairs the whole tree from its own allowlist and has no
|
|
337
392
|
// notion of a check family, so a scope passed alongside it could only
|
|
338
393
|
// be ignored. Ignoring it silently is the dangerous reading: an
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
|
|
3
3
|
import { type RuntimeHookTrustResult } from '../utils/hook-trust.js';
|
|
4
|
-
import { type
|
|
4
|
+
import { type ShimPathCheck } from '../lib/windows/user-path.js';
|
|
5
5
|
export interface GlobalInstallOptions {
|
|
6
6
|
/** HQ checkout to install from (absolute). */
|
|
7
7
|
hqRoot: string;
|
|
@@ -28,14 +28,10 @@ export interface GlobalInstallOptions {
|
|
|
28
28
|
marketplaceRepository?: string;
|
|
29
29
|
/** Test seam for the Claude Code CLI; production uses `claude`. */
|
|
30
30
|
runClaude?: (args: string[], home: string, env: NodeJS.ProcessEnv) => void;
|
|
31
|
+
/** Platform override for link creation (default process.platform). */
|
|
32
|
+
platform?: NodeJS.Platform;
|
|
31
33
|
}
|
|
32
|
-
|
|
33
|
-
export declare function isDaemonInstalled(home: string, platform?: NodeJS.Platform, queryTask?: (task: string) => boolean): boolean;
|
|
34
|
-
/**
|
|
35
|
-
* US-010: with hqd installed (and the checkout shipping the shim), hooks go to
|
|
36
|
-
* the hqd shim; otherwise to master-hook.sh / the Codex adapter.
|
|
37
|
-
*/
|
|
38
|
-
export declare function hookTargetFor(hqRoot: string, home: string, daemonInstalled?: boolean, env?: NodeJS.ProcessEnv): HookTarget;
|
|
34
|
+
export { hookTargetFor, isDaemonInstalled } from '../lib/install/hook-target.js';
|
|
39
35
|
export interface ClaudePluginSource {
|
|
40
36
|
sourceDir: string;
|
|
41
37
|
marketplaceName: string;
|
|
@@ -58,6 +54,8 @@ export interface GlobalUninstallOptions {
|
|
|
58
54
|
log?: (line: string) => void;
|
|
59
55
|
/** Test seam for the Claude Code CLI; production uses `claude`. */
|
|
60
56
|
runClaude?: (args: string[], home: string, env: NodeJS.ProcessEnv) => void;
|
|
57
|
+
/** Platform override for link creation (default process.platform). */
|
|
58
|
+
platform?: NodeJS.Platform;
|
|
61
59
|
}
|
|
62
60
|
/** Reverse every install step; non-HQ entries are left untouched. */
|
|
63
61
|
export declare function uninstallGlobalClaude(opts?: GlobalUninstallOptions): string[];
|
|
@@ -83,6 +81,8 @@ export interface GlobalCommandOpts {
|
|
|
83
81
|
cwd?: string;
|
|
84
82
|
/** Test seam for trusting the Codex hooks a global install registers. */
|
|
85
83
|
trustCodexHooks?: (cwd: string, keys: readonly string[]) => Promise<RuntimeHookTrustResult>;
|
|
84
|
+
/** Test seam: is the npm hq.cmd folder on the Windows PATH? Null off Windows. */
|
|
85
|
+
shimPathCheck?: () => ShimPathCheck | null;
|
|
86
86
|
}
|
|
87
87
|
export declare function resolveInstallRoot(explicit: string | undefined, options?: {
|
|
88
88
|
env?: NodeJS.ProcessEnv;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hookTargetFor } from '../lib/install/hook-target.js';
|
|
1
2
|
/**
|
|
2
3
|
* `hq install --global --runtime claude` / `hq uninstall --global --runtime claude`
|
|
3
4
|
* (hq-anywhere-runtime US-005).
|
|
@@ -17,6 +18,7 @@ import { seedRegistryFromHqRoot } from './link.js';
|
|
|
17
18
|
import { resolveHqRoot } from './doctor.js';
|
|
18
19
|
import { anywhereRuntimeEnabled, AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
|
|
19
20
|
import { resolveHqMcpEndpoint } from '../lib/daemon/mcp-endpoint.js';
|
|
21
|
+
import { linkDirectory, removeLinkOnly, removeSkillEntry, skillCopySource } from '../utils/safe-link.js';
|
|
20
22
|
import { chatgptConnectorInstructions } from '../lib/install/chatgpt-targets.js';
|
|
21
23
|
import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
|
|
22
24
|
import { grokConnectorInstructions } from '../lib/install/grok-targets.js';
|
|
@@ -24,33 +26,11 @@ import { readRegistry, registryDir, registryFile, writeRegistry } from '../lib/r
|
|
|
24
26
|
import { readCompaniesManifestMap } from '../lib/work-context/repo-remote.js';
|
|
25
27
|
import { resolveDefaultHqRoot } from '../utils/hq-root-resolver.js';
|
|
26
28
|
import { trustCodexUserHooks } from '../utils/hook-trust.js';
|
|
27
|
-
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../lib/daemon/install.js';
|
|
28
|
-
import { readDaemonConfig } from '../lib/daemon/config.js';
|
|
29
|
-
import { daemonPaths } from '../lib/daemon/paths.js';
|
|
30
29
|
import { assertProviderWrappersInstallable, installProviderWrappers, providerWrapperInstallPaths, removeProviderWrappers } from '../lib/install/provider-wrappers.js';
|
|
30
|
+
import { checkShimOnPath, ensureShimOnUserPath, shimPathWarning } from '../lib/windows/user-path.js';
|
|
31
31
|
import { addAgentsMdBlock, addCodexHooks, classifyLegacySkills, codexAdapterPath, codexCharterSource, codexHookTrustKeys, codexTargets, hasAllCodexHooks, HQ_OWNED_MARKER, LEGACY_BACKUP_NAME, LEGACY_SKILLS_NAME, removeAgentsMdBlock, removeCodexHooks, } from '../lib/install/codex-targets.js';
|
|
32
32
|
import { addClaudeMdBlock, addHqHooks, charterPath, claudeTargets, hasAllHqHooks, HQ_ANYWHERE_PACK, HQ_MCP_SERVER_NAME, hqSkillsSource, lineDiff, masterHookPath, shimPath, MCP_SUBPROCESS_PATH, removeClaudeMdBlock, removeHqHooks, SKILL_LINK_PREFIX, } from '../lib/install/claude-targets.js';
|
|
33
|
-
|
|
34
|
-
export function isDaemonInstalled(home, platform = process.platform, queryTask = (task) => spawnSync('schtasks.exe', ['/Query', '/TN', task], { encoding: 'utf8', windowsHide: true }).status === 0) {
|
|
35
|
-
if (platform === 'win32')
|
|
36
|
-
return queryTask(DAEMON_WINDOWS_TASK);
|
|
37
|
-
const candidates = platform === 'darwin'
|
|
38
|
-
? [path.join(home, 'Library', 'LaunchAgents', `${DAEMON_LAUNCHD_LABEL}.plist`)]
|
|
39
|
-
: platform === 'linux'
|
|
40
|
-
? [path.join(home, '.config', 'systemd', 'user', DAEMON_SYSTEMD_UNIT), path.join(SYSTEM_UNIT_DIR, DAEMON_SYSTEMD_UNIT)]
|
|
41
|
-
: [];
|
|
42
|
-
return candidates.some((p) => fs.existsSync(p));
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* US-010: with hqd installed (and the checkout shipping the shim), hooks go to
|
|
46
|
-
* the hqd shim; otherwise to master-hook.sh / the Codex adapter.
|
|
47
|
-
*/
|
|
48
|
-
export function hookTargetFor(hqRoot, home, daemonInstalled, env = process.env) {
|
|
49
|
-
const installed = daemonInstalled ?? isDaemonInstalled(home);
|
|
50
|
-
const configuredRoot = readDaemonConfig(daemonPaths(home, env).configPath).hqRoot;
|
|
51
|
-
const daemonMatchesRoot = configuredRoot !== undefined && realpathOrSelf(configuredRoot) === realpathOrSelf(hqRoot);
|
|
52
|
-
return installed && daemonMatchesRoot && fs.existsSync(shimPath(hqRoot)) ? 'shim' : 'master';
|
|
53
|
-
}
|
|
33
|
+
export { hookTargetFor, isDaemonInstalled } from '../lib/install/hook-target.js';
|
|
54
34
|
const HQ_PACKAGES_MARKETPLACE_REPOSITORY = 'indigoai-us/hq-packages';
|
|
55
35
|
/** Resolve local overrides first, then the published HQ marketplace. */
|
|
56
36
|
export function resolveClaudePluginSource(explicitDir, env = process.env, repository = HQ_PACKAGES_MARKETPLACE_REPOSITORY) {
|
|
@@ -206,6 +186,22 @@ function mcpManifest(opts) {
|
|
|
206
186
|
}
|
|
207
187
|
return { type: 'stdio', command, args: [entry, 'mcp', 'serve'], env: { PATH: MCP_SUBPROCESS_PATH } };
|
|
208
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* The folder an HQ skill entry stands for: a link's resolved target, or the
|
|
191
|
+
* source recorded in a marked copy. Null for anything else.
|
|
192
|
+
*/
|
|
193
|
+
function skillEntryTarget(skillsDir, entry) {
|
|
194
|
+
const st = fs.lstatSync(entry);
|
|
195
|
+
if (st.isSymbolicLink())
|
|
196
|
+
return path.resolve(skillsDir, fs.readlinkSync(entry));
|
|
197
|
+
return skillCopySource(entry);
|
|
198
|
+
}
|
|
199
|
+
function reportSkillCopies(kinds, log) {
|
|
200
|
+
const copies = kinds.filter((k) => k === 'copy').length;
|
|
201
|
+
if (copies === 0)
|
|
202
|
+
return;
|
|
203
|
+
log(` ${copies} skill folder(s) were copied because Windows refused both a link and a junction. Copies do not follow HQ skill updates; turn on Developer Mode, then uninstall and install again to get links.`);
|
|
204
|
+
}
|
|
209
205
|
function skillLinkName(name) {
|
|
210
206
|
return name.startsWith(SKILL_LINK_PREFIX) ? name : `${SKILL_LINK_PREFIX}${name}`;
|
|
211
207
|
}
|
|
@@ -254,7 +250,7 @@ function planSkills(hqRoot, t) {
|
|
|
254
250
|
}
|
|
255
251
|
if (!st)
|
|
256
252
|
plan.link.push(name);
|
|
257
|
-
else if (
|
|
253
|
+
else if (skillEntryTarget(t.skillsDir, link) === target)
|
|
258
254
|
plan.already.push(name);
|
|
259
255
|
else
|
|
260
256
|
plan.skipped.push(name);
|
|
@@ -283,9 +279,8 @@ function hqSkillLinksForRoot(skillsDir, hqRoot) {
|
|
|
283
279
|
return names.filter((name) => {
|
|
284
280
|
const link = path.join(skillsDir, name);
|
|
285
281
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return path.dirname(path.resolve(skillsDir, fs.readlinkSync(link))) === hqSkillsSource(hqRoot);
|
|
282
|
+
const target = skillEntryTarget(skillsDir, link);
|
|
283
|
+
return target !== null && path.dirname(target) === hqSkillsSource(hqRoot);
|
|
289
284
|
}
|
|
290
285
|
catch (error) {
|
|
291
286
|
if (error.code === 'ENOENT')
|
|
@@ -301,8 +296,8 @@ function hqCompanySkillLinksForRoot(skillsDir, hqRoot) {
|
|
|
301
296
|
const source = hqSkillsSource(hqRoot);
|
|
302
297
|
return hqSkillLinksForRoot(skillsDir, hqRoot).filter((link) => {
|
|
303
298
|
try {
|
|
304
|
-
const target =
|
|
305
|
-
if (path.dirname(target) !== source)
|
|
299
|
+
const target = skillEntryTarget(skillsDir, link);
|
|
300
|
+
if (target === null || path.dirname(target) !== source)
|
|
306
301
|
return false;
|
|
307
302
|
const sourceName = path.basename(target);
|
|
308
303
|
const separator = sourceName.indexOf(':');
|
|
@@ -492,22 +487,41 @@ export function installGlobalClaude(opts) {
|
|
|
492
487
|
throw error;
|
|
493
488
|
}
|
|
494
489
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
490
|
+
// Skill links go first: they are the step most likely to fail (Windows
|
|
491
|
+
// without symlink privilege), and a failure here must not leave ~/.hq/root
|
|
492
|
+
// or the CLAUDE.md block behind. On any error, undo this run's writes.
|
|
493
|
+
const skillsDirExisted = fs.existsSync(t.skillsDir);
|
|
494
|
+
const createdLinks = [];
|
|
495
|
+
const linkKinds = [];
|
|
496
|
+
try {
|
|
497
|
+
if (!pluginSource)
|
|
498
|
+
fs.mkdirSync(t.skillsDir, { recursive: true });
|
|
499
|
+
for (const name of skills.link) {
|
|
500
|
+
const link = path.join(t.skillsDir, skillLinkName(name));
|
|
501
|
+
linkKinds.push(linkDirectory(path.join(hqSkillsSource(hqRoot), name), link, { platform: opts.platform }));
|
|
502
|
+
createdLinks.push(link);
|
|
503
|
+
}
|
|
504
|
+
writeFileAtomic(t.hqPointer, pointerNew);
|
|
505
|
+
if (mdOld !== mdNew) {
|
|
506
|
+
if (mdNew === null) {
|
|
507
|
+
if (fs.existsSync(t.claudeMd))
|
|
508
|
+
fs.unlinkSync(t.claudeMd);
|
|
509
|
+
}
|
|
510
|
+
else
|
|
511
|
+
writeFileAtomic(t.claudeMd, mdNew);
|
|
500
512
|
}
|
|
501
|
-
else
|
|
502
|
-
writeFileAtomic(t.claudeMd, mdNew);
|
|
503
513
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
514
|
+
catch (error) {
|
|
515
|
+
rollbackLinkPhase({
|
|
516
|
+
createdLinks,
|
|
517
|
+
skillsDir: skillsDirExisted ? null : t.skillsDir,
|
|
518
|
+
files: [[t.hqPointer, pointerOld], [t.claudeMd, mdOld]],
|
|
519
|
+
log,
|
|
520
|
+
});
|
|
521
|
+
throw error;
|
|
508
522
|
}
|
|
509
523
|
for (const link of skillLinksToRemove)
|
|
510
|
-
|
|
524
|
+
removeSkillEntry(link, log);
|
|
511
525
|
if (pluginSource) {
|
|
512
526
|
// Migrate a prior direct install: remove only our old hook/MCP entries while
|
|
513
527
|
// retaining the original user settings for a later uninstall restore.
|
|
@@ -574,16 +588,49 @@ export function installGlobalClaude(opts) {
|
|
|
574
588
|
installProviderWrappers(providerBin, home);
|
|
575
589
|
log(`HQ installed for Claude Code from ${hqRoot}.`);
|
|
576
590
|
log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
|
|
591
|
+
reportSkillCopies(linkKinds, log);
|
|
577
592
|
reportSkippedSkills(skills, log, false);
|
|
578
593
|
if (pluginSource)
|
|
579
594
|
log(` plugin: ${pluginSource.pluginId} installed from ${pluginSource.sourceDir}; direct hooks and MCP entries removed`);
|
|
580
595
|
else {
|
|
581
|
-
log(` hooks: ${hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
|
|
596
|
+
log(` hooks: ${typeof hookTarget === 'object' ? hookTarget.engine : hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
|
|
582
597
|
log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.claudeJson}`);
|
|
583
598
|
}
|
|
584
599
|
log(` registry: ${seeded.length} manifest entr${seeded.length === 1 ? 'y' : 'ies'} seeded`);
|
|
585
600
|
return touched;
|
|
586
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* Undo the link phase of a failed install: remove only the links this run
|
|
604
|
+
* created (never recursing into them), put each file back to its pre-run
|
|
605
|
+
* bytes, and drop the skills folder if this run created it and it is empty.
|
|
606
|
+
* A copy that is kept instead of removed is reported through `log`.
|
|
607
|
+
*/
|
|
608
|
+
function rollbackLinkPhase(args) {
|
|
609
|
+
for (const link of [...args.createdLinks].reverse()) {
|
|
610
|
+
try {
|
|
611
|
+
removeSkillEntry(link, args.log);
|
|
612
|
+
}
|
|
613
|
+
catch { /* keep rolling back */ }
|
|
614
|
+
}
|
|
615
|
+
for (const [file, original] of args.files) {
|
|
616
|
+
try {
|
|
617
|
+
if (original === null) {
|
|
618
|
+
if (fs.existsSync(file))
|
|
619
|
+
fs.unlinkSync(file);
|
|
620
|
+
}
|
|
621
|
+
else if (readOrNull(file) !== original)
|
|
622
|
+
writeFileAtomic(file, original);
|
|
623
|
+
}
|
|
624
|
+
catch { /* keep rolling back */ }
|
|
625
|
+
}
|
|
626
|
+
if (args.skillsDir) {
|
|
627
|
+
try {
|
|
628
|
+
if (fs.readdirSync(args.skillsDir).length === 0)
|
|
629
|
+
fs.rmdirSync(args.skillsDir);
|
|
630
|
+
}
|
|
631
|
+
catch { /* keep rolling back */ }
|
|
632
|
+
}
|
|
633
|
+
}
|
|
587
634
|
/** Reverse every install step; non-HQ entries are left untouched. */
|
|
588
635
|
export function uninstallGlobalClaude(opts = {}) {
|
|
589
636
|
const home = resolveEnv({ home: opts.home }).home;
|
|
@@ -621,11 +668,11 @@ export function uninstallGlobalClaude(opts = {}) {
|
|
|
621
668
|
const links = [];
|
|
622
669
|
try {
|
|
623
670
|
for (const d of fs.readdirSync(t.skillsDir, { withFileTypes: true })) {
|
|
624
|
-
if (!d.name.startsWith(SKILL_LINK_PREFIX) || !d.isSymbolicLink())
|
|
671
|
+
if (!d.name.startsWith(SKILL_LINK_PREFIX) || !(d.isSymbolicLink() || d.isDirectory()))
|
|
625
672
|
continue;
|
|
626
673
|
const link = path.join(t.skillsDir, d.name);
|
|
627
|
-
const target =
|
|
628
|
-
if (path.dirname(target) === source)
|
|
674
|
+
const target = skillEntryTarget(t.skillsDir, link);
|
|
675
|
+
if (target !== null && path.dirname(target) === source)
|
|
629
676
|
links.push(link);
|
|
630
677
|
}
|
|
631
678
|
}
|
|
@@ -693,7 +740,7 @@ export function uninstallGlobalClaude(opts = {}) {
|
|
|
693
740
|
}
|
|
694
741
|
}
|
|
695
742
|
for (const link of links)
|
|
696
|
-
|
|
743
|
+
removeSkillEntry(link, log);
|
|
697
744
|
if (md !== null && mdNext !== md) {
|
|
698
745
|
if (mdNext === null || (mdNext === '' && state?.claudeMd.original == null))
|
|
699
746
|
fs.unlinkSync(t.claudeMd);
|
|
@@ -842,12 +889,34 @@ export function installGlobalCodex(opts) {
|
|
|
842
889
|
log(lineDiff(codexConfigFormat.serialize(tomlDoc), codexConfigFormat.serialize(tomlNext)));
|
|
843
890
|
return touched;
|
|
844
891
|
}
|
|
845
|
-
|
|
846
|
-
|
|
892
|
+
// Skill links go first so a link failure leaves no pointer or AGENTS.md
|
|
893
|
+
// block behind (see installGlobalClaude).
|
|
894
|
+
const skillsDirExisted = fs.existsSync(t.skillsDir);
|
|
895
|
+
const createdLinks = [];
|
|
896
|
+
const linkKinds = [];
|
|
897
|
+
try {
|
|
898
|
+
fs.mkdirSync(t.skillsDir, { recursive: true });
|
|
899
|
+
for (const name of skills.link) {
|
|
900
|
+
const link = path.join(t.skillsDir, skillLinkName(name));
|
|
901
|
+
linkKinds.push(linkDirectory(path.join(hqSkillsSource(hqRoot), name), link, { platform: opts.platform }));
|
|
902
|
+
createdLinks.push(link);
|
|
903
|
+
}
|
|
904
|
+
writeFileAtomic(hqPointer, pointerNew);
|
|
905
|
+
writeFileAtomic(t.agentsMd, mdNew);
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
rollbackLinkPhase({
|
|
909
|
+
createdLinks,
|
|
910
|
+
skillsDir: skillsDirExisted ? null : t.skillsDir,
|
|
911
|
+
files: [[hqPointer, pointerOld], [t.agentsMd, mdOld]],
|
|
912
|
+
log,
|
|
913
|
+
});
|
|
914
|
+
throw error;
|
|
915
|
+
}
|
|
847
916
|
// Legacy mirror: replace only when HQ-owned; record enough to put it back.
|
|
848
917
|
let legacyRecord = prior?.legacy ?? { kind: 'none' };
|
|
849
918
|
if (legacy.kind === 'hq-symlink') {
|
|
850
|
-
|
|
919
|
+
removeLinkOnly(legacyPath);
|
|
851
920
|
legacyRecord = { kind: 'symlink', target: legacy.target };
|
|
852
921
|
}
|
|
853
922
|
else if (legacy.kind === 'hq-dir') {
|
|
@@ -857,12 +926,8 @@ export function installGlobalCodex(opts) {
|
|
|
857
926
|
}
|
|
858
927
|
else if (legacy.kind === 'foreign')
|
|
859
928
|
log(legacyWarning(t.skillsDir));
|
|
860
|
-
fs.mkdirSync(t.skillsDir, { recursive: true });
|
|
861
|
-
for (const name of skills.link) {
|
|
862
|
-
fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, skillLinkName(name)));
|
|
863
|
-
}
|
|
864
929
|
for (const link of companySkillLinksToRemove)
|
|
865
|
-
|
|
930
|
+
removeSkillEntry(link, log);
|
|
866
931
|
writeConfigAtomic({
|
|
867
932
|
target: t.hooksJson,
|
|
868
933
|
pack: HQ_ANYWHERE_PACK,
|
|
@@ -905,8 +970,9 @@ export function installGlobalCodex(opts) {
|
|
|
905
970
|
installProviderWrappers(providerBin, home);
|
|
906
971
|
log(`HQ installed for Codex from ${hqRoot}.`);
|
|
907
972
|
log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
|
|
973
|
+
reportSkillCopies(linkKinds, log);
|
|
908
974
|
reportSkippedSkills(skills, log, false);
|
|
909
|
-
log(` hooks: ${hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
|
|
975
|
+
log(` hooks: ${typeof hookTarget === 'object' ? hookTarget.engine : hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
|
|
910
976
|
log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.configToml}`);
|
|
911
977
|
return touched;
|
|
912
978
|
}
|
|
@@ -943,11 +1009,11 @@ export function uninstallGlobalCodex(opts = {}) {
|
|
|
943
1009
|
const links = [];
|
|
944
1010
|
try {
|
|
945
1011
|
for (const d of fs.readdirSync(t.skillsDir, { withFileTypes: true })) {
|
|
946
|
-
if (!d.name.startsWith(SKILL_LINK_PREFIX) || !d.isSymbolicLink())
|
|
1012
|
+
if (!d.name.startsWith(SKILL_LINK_PREFIX) || !(d.isSymbolicLink() || d.isDirectory()))
|
|
947
1013
|
continue;
|
|
948
1014
|
const link = path.join(t.skillsDir, d.name);
|
|
949
|
-
const target =
|
|
950
|
-
if (path.dirname(target) === source)
|
|
1015
|
+
const target = skillEntryTarget(t.skillsDir, link);
|
|
1016
|
+
if (target !== null && path.dirname(target) === source)
|
|
951
1017
|
links.push(link);
|
|
952
1018
|
}
|
|
953
1019
|
}
|
|
@@ -1012,10 +1078,10 @@ export function uninstallGlobalCodex(opts = {}) {
|
|
|
1012
1078
|
}
|
|
1013
1079
|
}
|
|
1014
1080
|
for (const link of links)
|
|
1015
|
-
|
|
1081
|
+
removeSkillEntry(link, log);
|
|
1016
1082
|
if (restoreLegacy && state) {
|
|
1017
1083
|
if (state.legacy.kind === 'symlink')
|
|
1018
|
-
|
|
1084
|
+
linkDirectory(state.legacy.target, legacyPath, { platform: opts.platform, allowCopy: false });
|
|
1019
1085
|
else if (state.legacy.kind === 'dir' && fs.existsSync(state.legacy.backup))
|
|
1020
1086
|
fs.renameSync(state.legacy.backup, legacyPath);
|
|
1021
1087
|
}
|
|
@@ -1124,6 +1190,18 @@ export async function runInstallGlobal(opts) {
|
|
|
1124
1190
|
});
|
|
1125
1191
|
if (runtime === 'codex' && !opts.dryRun)
|
|
1126
1192
|
await trustInstalledCodexHooks(hqRoot, opts);
|
|
1193
|
+
if (!opts.dryRun) {
|
|
1194
|
+
try {
|
|
1195
|
+
ensureShimOnUserPath({ env, entry: process.argv[1] });
|
|
1196
|
+
}
|
|
1197
|
+
catch {
|
|
1198
|
+
// The warning below is the fallback when the user Path cannot be updated.
|
|
1199
|
+
}
|
|
1200
|
+
const shimPath = (opts.shimPathCheck ?? (() => checkShimOnPath({ env })))();
|
|
1201
|
+
if (shimPath)
|
|
1202
|
+
for (const line of shimPathWarning(shimPath))
|
|
1203
|
+
(opts.log ?? console.log)(line);
|
|
1204
|
+
}
|
|
1127
1205
|
}
|
|
1128
1206
|
/**
|
|
1129
1207
|
* Codex skips untrusted hooks, so the hooks.json entries written above do
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneAdmissionLock, withLaneHireLocks } from "../lib/lanes/index.js";
|
|
2
2
|
import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
3
3
|
import type { OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
4
|
-
import type { CpuBusySource } from "../lib/lanes/throttle.js";
|
|
4
|
+
import type { CpuBusySource, MemoryHold } from "../lib/lanes/throttle.js";
|
|
5
5
|
import type { ProbeCommand } from "../lib/lanes/models.js";
|
|
6
6
|
import type { FocusGuardReason } from "../lib/lanes/focus-guard.js";
|
|
7
7
|
import type { KillProcessFn, ProveDetachedFn, SpawnProcessFn } from "../lib/lanes/spawn.js";
|
|
@@ -100,6 +100,8 @@ export interface AdmissionResult {
|
|
|
100
100
|
last_busy: number | null;
|
|
101
101
|
busy_source: CpuBusySource | undefined;
|
|
102
102
|
sampled_at: string | undefined;
|
|
103
|
+
/** Present when the memory gate holds: the signal, its value and the threshold. */
|
|
104
|
+
memory_hold?: MemoryHold;
|
|
103
105
|
};
|
|
104
106
|
}
|
|
105
107
|
export interface PrepareAccountMoveInput {
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -136,12 +136,12 @@ export declare function casUpdateLane(hqRoot: string, laneId: string, updater: (
|
|
|
136
136
|
* reconcile pass resumes the lane once every question is answered.
|
|
137
137
|
*/
|
|
138
138
|
export declare function checkpointInstruction(worker: Pick<ResolvedLaneWorker, "approvalRequired" | "humanCheckpoints">): string;
|
|
139
|
-
export declare function composeWorkerBrief(worker: ResolvedLaneWorker, taskBrief: string): string;
|
|
139
|
+
export declare function composeWorkerBrief(worker: ResolvedLaneWorker, taskBrief: string, hqRoot: string): string;
|
|
140
140
|
/**
|
|
141
|
-
* A lane with no worker still carries the standing authorization paragraph
|
|
142
|
-
*
|
|
141
|
+
* A lane with no worker still carries the standing authorization paragraph,
|
|
142
|
+
* the foreground-only rule and the temp-home rule.
|
|
143
143
|
*/
|
|
144
|
-
export declare function composeGeneralBrief(taskBrief: string): string;
|
|
144
|
+
export declare function composeGeneralBrief(taskBrief: string, hqRoot: string): string;
|
|
145
145
|
export declare const DISPATCH_TERMINAL_REASON: "outpost_dispatch_failed";
|
|
146
146
|
/**
|
|
147
147
|
* The CAS updater a pickup failure applies. A lane that is already terminal
|