@indigoai-us/hq-cli 5.115.5 → 5.116.0
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 +96 -0
- package/dist/command-catalog.generated.d.ts +162 -2
- package/dist/command-catalog.generated.js +205 -2
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +105 -0
- package/dist/commands/agent-enroll.js +273 -0
- package/dist/commands/agent-kit.d.ts +53 -0
- package/dist/commands/agent-kit.js +260 -0
- package/dist/commands/agent-mcp.d.ts +22 -0
- package/dist/commands/agent-mcp.js +104 -0
- package/dist/commands/agent-probe.d.ts +71 -0
- package/dist/commands/agent-probe.js +294 -0
- package/dist/commands/agent.d.ts +12 -0
- package/dist/commands/agent.js +23 -0
- package/dist/commands/agents.d.ts +27 -0
- package/dist/commands/agents.js +280 -6
- package/dist/commands/secrets.js +17 -5
- package/dist/lib/agent-kit/creds.d.ts +60 -0
- package/dist/lib/agent-kit/creds.js +123 -0
- package/dist/lib/agent-kit/kit-config.d.ts +29 -0
- package/dist/lib/agent-kit/kit-config.js +54 -0
- package/dist/lib/agent-kit/log.d.ts +17 -0
- package/dist/lib/agent-kit/log.js +46 -0
- package/dist/lib/agent-kit/mcp/jsonrpc.d.ts +84 -0
- package/dist/lib/agent-kit/mcp/jsonrpc.js +164 -0
- package/dist/lib/agent-kit/mcp/tools.d.ts +45 -0
- package/dist/lib/agent-kit/mcp/tools.js +280 -0
- package/dist/lib/agent-kit/paths.d.ts +42 -0
- package/dist/lib/agent-kit/paths.js +56 -0
- package/dist/lib/agent-kit/run/heartbeat.d.ts +52 -0
- package/dist/lib/agent-kit/run/heartbeat.js +97 -0
- package/dist/lib/agent-kit/run/inbox.d.ts +59 -0
- package/dist/lib/agent-kit/run/inbox.js +152 -0
- package/dist/lib/agent-kit/run/mesh-listener.d.ts +58 -0
- package/dist/lib/agent-kit/run/mesh-listener.js +193 -0
- package/dist/lib/agent-kit/run/sync.d.ts +33 -0
- package/dist/lib/agent-kit/run/sync.js +58 -0
- package/dist/lib/agent-kit/services.d.ts +21 -0
- package/dist/lib/agent-kit/services.js +46 -0
- package/dist/lib/agent-kit/skills.d.ts +18 -0
- package/dist/lib/agent-kit/skills.js +149 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +19 -0
- package/dist/lib/doctor/checks/sync-health.js +55 -2
- package/dist/lib/doctor/fix/apply.d.ts +43 -6
- package/dist/lib/doctor/fix/apply.js +116 -18
- package/dist/lib/doctor/fix/remediation.d.ts +8 -3
- package/dist/lib/doctor/fix/remediation.js +21 -2
- package/dist/lib/scan-packages/index.js +158 -1
- package/dist/lib/service-manager/index.d.ts +43 -0
- package/dist/lib/service-manager/index.js +114 -0
- package/dist/lib/service-manager/launchd.d.ts +23 -0
- package/dist/lib/service-manager/launchd.js +81 -0
- package/dist/lib/service-manager/systemd.d.ts +19 -0
- package/dist/lib/service-manager/systemd.js +72 -0
- package/dist/lib/service-manager/types.d.ts +32 -0
- package/dist/lib/service-manager/types.js +26 -0
- package/dist/utils/self-update.js +2 -30
- package/dist/utils/update-command-supervisor.cjs +194 -0
- package/dist/utils/version-gate.d.ts +18 -0
- package/dist/utils/version-gate.js +126 -7
- package/package.json +2 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install / uninstall / status for a SET of user-level services, dispatching
|
|
3
|
+
* to launchd or systemd. All filesystem and process-control effects are
|
|
4
|
+
* injectable so the kit tests exercise the real rendering and dispatch
|
|
5
|
+
* without touching the host's service manager.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import { activateLaunchd, deactivateLaunchd, launchAgentsDir, launchdIsLoaded, launchdPlistPath, renderLaunchdPlist, } from "./launchd.js";
|
|
10
|
+
import { activateSystemd, deactivateSystemd, renderSystemdUserUnit, systemdIsActive, systemdUnitName, systemdUnitPath, systemdUserDir, } from "./systemd.js";
|
|
11
|
+
import { detectServicePlatform, } from "./types.js";
|
|
12
|
+
export { detectServicePlatform } from "./types.js";
|
|
13
|
+
export { renderLaunchdPlist } from "./launchd.js";
|
|
14
|
+
export { renderSystemdUserUnit, systemdUnitName } from "./systemd.js";
|
|
15
|
+
export const defaultRunCommand = (cmd, args) => {
|
|
16
|
+
const r = spawnSync(cmd, args, { encoding: "utf8" });
|
|
17
|
+
return {
|
|
18
|
+
status: r.status,
|
|
19
|
+
stdout: r.stdout ?? "",
|
|
20
|
+
stderr: r.error ? r.error.message : (r.stderr ?? ""),
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
function unitPathFor(platform, home, spec) {
|
|
24
|
+
return platform === "darwin" ? launchdPlistPath(home, spec) : systemdUnitPath(home, spec);
|
|
25
|
+
}
|
|
26
|
+
export function renderUnit(platform, spec, host) {
|
|
27
|
+
return platform === "darwin"
|
|
28
|
+
? renderLaunchdPlist(spec, host)
|
|
29
|
+
: renderSystemdUserUnit(spec, host);
|
|
30
|
+
}
|
|
31
|
+
export function installServices(specs, host, deps = {}) {
|
|
32
|
+
const platform = deps.platform ?? detectServicePlatform();
|
|
33
|
+
const run = deps.run ?? defaultRunCommand;
|
|
34
|
+
const writeFileSync = deps.writeFileSync ?? fs.writeFileSync;
|
|
35
|
+
const mkdirSync = deps.mkdirSync ?? fs.mkdirSync;
|
|
36
|
+
const activate = deps.activate ?? true;
|
|
37
|
+
if (platform === "other") {
|
|
38
|
+
return {
|
|
39
|
+
platform,
|
|
40
|
+
units: specs.map((s) => ({ name: s.name, unitPath: "", installed: false })),
|
|
41
|
+
manualCommands: specs.map((s) => `${host.nodeBinary} ${host.hqBinary} ${s.args.join(" ")} # >> ${s.logPath}`),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const dir = platform === "darwin" ? launchAgentsDir(host.home) : systemdUserDir(host.home);
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
const units = [];
|
|
47
|
+
for (const spec of specs) {
|
|
48
|
+
const unitPath = unitPathFor(platform, host.home, spec);
|
|
49
|
+
const rendered = renderUnit(platform, spec, host);
|
|
50
|
+
writeFileSync(unitPath, rendered, { mode: 0o600 });
|
|
51
|
+
const unit = { name: spec.name, unitPath, rendered, installed: true };
|
|
52
|
+
if (activate) {
|
|
53
|
+
try {
|
|
54
|
+
if (platform === "darwin")
|
|
55
|
+
activateLaunchd(unitPath, run);
|
|
56
|
+
else
|
|
57
|
+
activateSystemd(systemdUnitName(spec), run);
|
|
58
|
+
unit.running = true;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
unit.running = false;
|
|
62
|
+
unit.error = err instanceof Error ? err.message : String(err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
units.push(unit);
|
|
66
|
+
}
|
|
67
|
+
return { platform, units };
|
|
68
|
+
}
|
|
69
|
+
export function uninstallServices(specs, host, deps = {}) {
|
|
70
|
+
const platform = deps.platform ?? detectServicePlatform();
|
|
71
|
+
const run = deps.run ?? defaultRunCommand;
|
|
72
|
+
const unlinkSync = deps.unlinkSync ?? fs.unlinkSync;
|
|
73
|
+
const existsSync = deps.existsSync ?? fs.existsSync;
|
|
74
|
+
const activate = deps.activate ?? true;
|
|
75
|
+
if (platform === "other") {
|
|
76
|
+
return { platform, units: specs.map((s) => ({ name: s.name, unitPath: "", installed: false })) };
|
|
77
|
+
}
|
|
78
|
+
const units = [];
|
|
79
|
+
for (const spec of specs) {
|
|
80
|
+
const unitPath = unitPathFor(platform, host.home, spec);
|
|
81
|
+
const present = existsSync(unitPath);
|
|
82
|
+
if (present) {
|
|
83
|
+
if (activate) {
|
|
84
|
+
if (platform === "darwin")
|
|
85
|
+
deactivateLaunchd(unitPath, run);
|
|
86
|
+
else
|
|
87
|
+
deactivateSystemd(systemdUnitName(spec), run);
|
|
88
|
+
}
|
|
89
|
+
unlinkSync(unitPath);
|
|
90
|
+
}
|
|
91
|
+
units.push({ name: spec.name, unitPath, installed: false, running: false });
|
|
92
|
+
}
|
|
93
|
+
return { platform, units };
|
|
94
|
+
}
|
|
95
|
+
export function servicesStatus(specs, host, deps = {}) {
|
|
96
|
+
const platform = deps.platform ?? detectServicePlatform();
|
|
97
|
+
const run = deps.run ?? defaultRunCommand;
|
|
98
|
+
const existsSync = deps.existsSync ?? fs.existsSync;
|
|
99
|
+
if (platform === "other") {
|
|
100
|
+
return { platform, units: specs.map((s) => ({ name: s.name, unitPath: "", installed: false })) };
|
|
101
|
+
}
|
|
102
|
+
const units = specs.map((spec) => {
|
|
103
|
+
const unitPath = unitPathFor(platform, host.home, spec);
|
|
104
|
+
const installed = existsSync(unitPath);
|
|
105
|
+
const running = installed
|
|
106
|
+
? platform === "darwin"
|
|
107
|
+
? launchdIsLoaded(spec.label, run)
|
|
108
|
+
: systemdIsActive(systemdUnitName(spec), run)
|
|
109
|
+
: false;
|
|
110
|
+
return { name: spec.name, unitPath, installed, running };
|
|
111
|
+
});
|
|
112
|
+
return { platform, units };
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS LaunchAgent rendering + activation for kit services.
|
|
3
|
+
*
|
|
4
|
+
* Units live at ~/Library/LaunchAgents/<label>.plist. Activation uses the
|
|
5
|
+
* modern `launchctl bootstrap gui/<uid>` form (bootout first so a re-install
|
|
6
|
+
* picks up a changed plist). Command execution is injected so tests never
|
|
7
|
+
* touch launchctl.
|
|
8
|
+
*/
|
|
9
|
+
import { type ServiceHostPaths, type ServiceSpec } from "./types.js";
|
|
10
|
+
export declare const LAUNCHD_PATH_ENV = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
|
11
|
+
export declare function launchAgentsDir(home: string): string;
|
|
12
|
+
export declare function launchdPlistPath(home: string, spec: Pick<ServiceSpec, "label">): string;
|
|
13
|
+
export declare function renderLaunchdPlist(spec: ServiceSpec, host: ServiceHostPaths): string;
|
|
14
|
+
export type RunCommand = (cmd: string, args: string[]) => {
|
|
15
|
+
status: number | null;
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
};
|
|
19
|
+
/** (Re)load a plist: bootout is best-effort (not loaded yet is fine). */
|
|
20
|
+
export declare function activateLaunchd(plistPath: string, run: RunCommand, uid?: number): void;
|
|
21
|
+
export declare function deactivateLaunchd(plistPath: string, run: RunCommand, uid?: number): void;
|
|
22
|
+
export declare function launchdIsLoaded(label: string, run: RunCommand, uid?: number): boolean;
|
|
23
|
+
//# sourceMappingURL=launchd.d.ts.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS LaunchAgent rendering + activation for kit services.
|
|
3
|
+
*
|
|
4
|
+
* Units live at ~/Library/LaunchAgents/<label>.plist. Activation uses the
|
|
5
|
+
* modern `launchctl bootstrap gui/<uid>` form (bootout first so a re-install
|
|
6
|
+
* picks up a changed plist). Command execution is injected so tests never
|
|
7
|
+
* touch launchctl.
|
|
8
|
+
*/
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { escapeXml, } from "./types.js";
|
|
12
|
+
export const LAUNCHD_PATH_ENV = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
|
13
|
+
export function launchAgentsDir(home) {
|
|
14
|
+
return path.join(home, "Library", "LaunchAgents");
|
|
15
|
+
}
|
|
16
|
+
export function launchdPlistPath(home, spec) {
|
|
17
|
+
return path.join(launchAgentsDir(home), `${spec.label}.plist`);
|
|
18
|
+
}
|
|
19
|
+
export function renderLaunchdPlist(spec, host) {
|
|
20
|
+
const args = [host.nodeBinary, host.hqBinary, ...spec.args]
|
|
21
|
+
.map((a) => ` <string>${escapeXml(a)}</string>`)
|
|
22
|
+
.join("\n");
|
|
23
|
+
const env = {
|
|
24
|
+
HOME: host.home,
|
|
25
|
+
PATH: LAUNCHD_PATH_ENV,
|
|
26
|
+
...spec.env,
|
|
27
|
+
};
|
|
28
|
+
const envXml = Object.entries(env)
|
|
29
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
30
|
+
.map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
|
|
31
|
+
.join("\n");
|
|
32
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
33
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
34
|
+
<plist version="1.0">
|
|
35
|
+
<dict>
|
|
36
|
+
<key>Label</key>
|
|
37
|
+
<string>${escapeXml(spec.label)}</string>
|
|
38
|
+
<key>ProgramArguments</key>
|
|
39
|
+
<array>
|
|
40
|
+
${args}
|
|
41
|
+
</array>
|
|
42
|
+
<key>WorkingDirectory</key>
|
|
43
|
+
<string>${escapeXml(spec.workingDir)}</string>
|
|
44
|
+
<key>RunAtLoad</key>
|
|
45
|
+
<true/>
|
|
46
|
+
<key>KeepAlive</key>
|
|
47
|
+
<true/>
|
|
48
|
+
<key>ThrottleInterval</key>
|
|
49
|
+
<integer>${spec.restartSec}</integer>
|
|
50
|
+
<key>StandardOutPath</key>
|
|
51
|
+
<string>${escapeXml(spec.logPath)}</string>
|
|
52
|
+
<key>StandardErrorPath</key>
|
|
53
|
+
<string>${escapeXml(spec.logPath)}</string>
|
|
54
|
+
<key>EnvironmentVariables</key>
|
|
55
|
+
<dict>
|
|
56
|
+
${envXml}
|
|
57
|
+
</dict>
|
|
58
|
+
</dict>
|
|
59
|
+
</plist>
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
function guiDomain(uid = os.userInfo().uid) {
|
|
63
|
+
return `gui/${uid}`;
|
|
64
|
+
}
|
|
65
|
+
/** (Re)load a plist: bootout is best-effort (not loaded yet is fine). */
|
|
66
|
+
export function activateLaunchd(plistPath, run, uid) {
|
|
67
|
+
const domain = guiDomain(uid);
|
|
68
|
+
run("launchctl", ["bootout", domain, plistPath]);
|
|
69
|
+
const r = run("launchctl", ["bootstrap", domain, plistPath]);
|
|
70
|
+
if (r.status !== 0) {
|
|
71
|
+
throw new Error(`launchctl bootstrap failed for ${plistPath}: ${r.stderr.trim() || r.stdout.trim() || `exit ${r.status}`}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function deactivateLaunchd(plistPath, run, uid) {
|
|
75
|
+
run("launchctl", ["bootout", guiDomain(uid), plistPath]);
|
|
76
|
+
}
|
|
77
|
+
export function launchdIsLoaded(label, run, uid) {
|
|
78
|
+
const r = run("launchctl", ["print", `${guiDomain(uid)}/${label}`]);
|
|
79
|
+
return r.status === 0;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=launchd.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux systemd USER unit rendering + activation for kit services.
|
|
3
|
+
*
|
|
4
|
+
* Units live at ~/.config/systemd/user/<name>.service and are enabled with
|
|
5
|
+
* `systemctl --user enable --now`. A host without a user session bus (some
|
|
6
|
+
* containers, headless boxes) cannot run user units; the kit's Docker image
|
|
7
|
+
* is the escape hatch there. Command execution is injected for tests.
|
|
8
|
+
*/
|
|
9
|
+
import { type ServiceHostPaths, type ServiceSpec } from "./types.js";
|
|
10
|
+
import type { RunCommand } from "./launchd.js";
|
|
11
|
+
export declare const SYSTEMD_PATH_ENV = "/usr/local/bin:/usr/bin:/bin";
|
|
12
|
+
export declare function systemdUserDir(home: string): string;
|
|
13
|
+
export declare function systemdUnitName(spec: Pick<ServiceSpec, "name">): string;
|
|
14
|
+
export declare function systemdUnitPath(home: string, spec: Pick<ServiceSpec, "name">): string;
|
|
15
|
+
export declare function renderSystemdUserUnit(spec: ServiceSpec, host: ServiceHostPaths): string;
|
|
16
|
+
export declare function activateSystemd(unitName: string, run: RunCommand): void;
|
|
17
|
+
export declare function deactivateSystemd(unitName: string, run: RunCommand): void;
|
|
18
|
+
export declare function systemdIsActive(unitName: string, run: RunCommand): boolean;
|
|
19
|
+
//# sourceMappingURL=systemd.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux systemd USER unit rendering + activation for kit services.
|
|
3
|
+
*
|
|
4
|
+
* Units live at ~/.config/systemd/user/<name>.service and are enabled with
|
|
5
|
+
* `systemctl --user enable --now`. A host without a user session bus (some
|
|
6
|
+
* containers, headless boxes) cannot run user units; the kit's Docker image
|
|
7
|
+
* is the escape hatch there. Command execution is injected for tests.
|
|
8
|
+
*/
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { shellQuote, } from "./types.js";
|
|
11
|
+
export const SYSTEMD_PATH_ENV = "/usr/local/bin:/usr/bin:/bin";
|
|
12
|
+
export function systemdUserDir(home) {
|
|
13
|
+
return path.join(home, ".config", "systemd", "user");
|
|
14
|
+
}
|
|
15
|
+
export function systemdUnitName(spec) {
|
|
16
|
+
return `hq-agent-${spec.name}.service`;
|
|
17
|
+
}
|
|
18
|
+
export function systemdUnitPath(home, spec) {
|
|
19
|
+
return path.join(systemdUserDir(home), systemdUnitName(spec));
|
|
20
|
+
}
|
|
21
|
+
export function renderSystemdUserUnit(spec, host) {
|
|
22
|
+
const exec = [host.nodeBinary, host.hqBinary, ...spec.args].map(shellQuote).join(" ");
|
|
23
|
+
const env = {
|
|
24
|
+
HOME: host.home,
|
|
25
|
+
PATH: SYSTEMD_PATH_ENV,
|
|
26
|
+
...spec.env,
|
|
27
|
+
};
|
|
28
|
+
const envLines = Object.entries(env)
|
|
29
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
30
|
+
.map(([k, v]) => `Environment=${shellQuote(`${k}=${v}`)}`)
|
|
31
|
+
.join("\n");
|
|
32
|
+
return `[Unit]
|
|
33
|
+
Description=${spec.description}
|
|
34
|
+
After=network-online.target
|
|
35
|
+
Wants=network-online.target
|
|
36
|
+
|
|
37
|
+
[Service]
|
|
38
|
+
Type=simple
|
|
39
|
+
ExecStart=${exec}
|
|
40
|
+
WorkingDirectory=${spec.workingDir}
|
|
41
|
+
Restart=always
|
|
42
|
+
RestartSec=${spec.restartSec}
|
|
43
|
+
${envLines}
|
|
44
|
+
StandardOutput=append:${spec.logPath}
|
|
45
|
+
StandardError=append:${spec.logPath}
|
|
46
|
+
|
|
47
|
+
[Install]
|
|
48
|
+
WantedBy=default.target
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
export function activateSystemd(unitName, run) {
|
|
52
|
+
const reload = run("systemctl", ["--user", "daemon-reload"]);
|
|
53
|
+
if (reload.status !== 0) {
|
|
54
|
+
throw new Error(`systemctl --user daemon-reload failed: ${reload.stderr.trim() || `exit ${reload.status}`}. ` +
|
|
55
|
+
`No user session bus? Use the hq-agent-kit Docker image instead.`);
|
|
56
|
+
}
|
|
57
|
+
const r = run("systemctl", ["--user", "enable", "--now", unitName]);
|
|
58
|
+
if (r.status !== 0) {
|
|
59
|
+
throw new Error(`systemctl --user enable --now ${unitName} failed: ${r.stderr.trim() || `exit ${r.status}`}`);
|
|
60
|
+
}
|
|
61
|
+
// A re-install with a changed unit must restart the running instance.
|
|
62
|
+
run("systemctl", ["--user", "restart", unitName]);
|
|
63
|
+
}
|
|
64
|
+
export function deactivateSystemd(unitName, run) {
|
|
65
|
+
run("systemctl", ["--user", "disable", "--now", unitName]);
|
|
66
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
67
|
+
}
|
|
68
|
+
export function systemdIsActive(unitName, run) {
|
|
69
|
+
const r = run("systemctl", ["--user", "is-active", "--quiet", unitName]);
|
|
70
|
+
return r.status === 0;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=systemd.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-neutral description of a user-level background service. The
|
|
3
|
+
* launchd and systemd renderers turn one of these into a unit; the kit
|
|
4
|
+
* installer owns the list. Every service is just `node hq <args…>` so the
|
|
5
|
+
* unit files carry no logic of their own.
|
|
6
|
+
*/
|
|
7
|
+
export interface ServiceSpec {
|
|
8
|
+
/** Short name — unit file stem and log file stem (`sync`, `mesh`, …). */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Reverse-DNS launchd label; systemd derives its unit name from `name`. */
|
|
11
|
+
label: string;
|
|
12
|
+
description: string;
|
|
13
|
+
/** Arguments passed to the hq binary (after `node hq`). */
|
|
14
|
+
args: string[];
|
|
15
|
+
/** Absolute log path (stdout + stderr appended). */
|
|
16
|
+
logPath: string;
|
|
17
|
+
workingDir: string;
|
|
18
|
+
/** Extra environment for the unit (HOME/PATH are always set). */
|
|
19
|
+
env: Record<string, string>;
|
|
20
|
+
/** Seconds to wait before a restart after exit. */
|
|
21
|
+
restartSec: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ServiceHostPaths {
|
|
24
|
+
home: string;
|
|
25
|
+
nodeBinary: string;
|
|
26
|
+
hqBinary: string;
|
|
27
|
+
}
|
|
28
|
+
export type ServicePlatform = "darwin" | "linux" | "other";
|
|
29
|
+
export declare function detectServicePlatform(platform?: NodeJS.Platform): ServicePlatform;
|
|
30
|
+
export declare function escapeXml(value: string): string;
|
|
31
|
+
export declare function shellQuote(value: string): string;
|
|
32
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-neutral description of a user-level background service. The
|
|
3
|
+
* launchd and systemd renderers turn one of these into a unit; the kit
|
|
4
|
+
* installer owns the list. Every service is just `node hq <args…>` so the
|
|
5
|
+
* unit files carry no logic of their own.
|
|
6
|
+
*/
|
|
7
|
+
export function detectServicePlatform(platform = process.platform) {
|
|
8
|
+
if (platform === "darwin")
|
|
9
|
+
return "darwin";
|
|
10
|
+
if (platform === "linux")
|
|
11
|
+
return "linux";
|
|
12
|
+
return "other";
|
|
13
|
+
}
|
|
14
|
+
export function escapeXml(value) {
|
|
15
|
+
return value
|
|
16
|
+
.replace(/&/g, "&")
|
|
17
|
+
.replace(/</g, "<")
|
|
18
|
+
.replace(/>/g, ">")
|
|
19
|
+
.replace(/"/g, """);
|
|
20
|
+
}
|
|
21
|
+
export function shellQuote(value) {
|
|
22
|
+
if (/^[A-Za-z0-9_./:=-]+$/.test(value))
|
|
23
|
+
return value;
|
|
24
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -60,7 +60,7 @@ import { spawnSync } from "node:child_process";
|
|
|
60
60
|
import semver from "semver";
|
|
61
61
|
import chalk from "chalk";
|
|
62
62
|
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
63
|
-
import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, checkUpdateConvergence,
|
|
63
|
+
import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, checkUpdateConvergence, isLocalDependencyInstall, isPrefixWritable, nonWritablePrefixNote, openInstallOutput, pnpmUpdateEnv, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, } from "./version-gate.js";
|
|
64
64
|
import { acquireUpdateLock as acquireSharedUpdateLock } from "./update-lock.js";
|
|
65
65
|
import { markLatestIneffective } from "./version-check.js";
|
|
66
66
|
/**
|
|
@@ -70,8 +70,6 @@ import { markLatestIneffective } from "./version-check.js";
|
|
|
70
70
|
export const REEXEC_GUARD_ENV = "HQ_RESCUE_SELF_UPDATED";
|
|
71
71
|
const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(CLI_NAME)}/latest`;
|
|
72
72
|
const FETCH_TIMEOUT_MS = 3_000;
|
|
73
|
-
/** Tail of captured package-manager stderr kept for the failure warning. */
|
|
74
|
-
const DETAIL_MAX_CHARS = 400;
|
|
75
73
|
/** npm `latest` for this package, or null on any failure (offline, 5xx, bad body). */
|
|
76
74
|
async function fetchLatestVersion() {
|
|
77
75
|
try {
|
|
@@ -115,33 +113,7 @@ export function runUpdateQuiet(cmd, args, env) {
|
|
|
115
113
|
// the installer down with it mid-write. See `openInstallOutput`.
|
|
116
114
|
const output = openInstallOutput(false);
|
|
117
115
|
try {
|
|
118
|
-
|
|
119
|
-
// `inOwnProcessGroup`: the install must survive a signal aimed at this
|
|
120
|
-
// process, or a killed `hq` leaves a corrupted global install behind.
|
|
121
|
-
const result = spawnSync(plan.cmd, plan.args, inOwnProcessGroup({
|
|
122
|
-
stdio: output.stdio,
|
|
123
|
-
shell: plan.shell,
|
|
124
|
-
...(env ? { env } : {}),
|
|
125
|
-
}));
|
|
126
|
-
if (result.error) {
|
|
127
|
-
const code = result.error.code;
|
|
128
|
-
return { ok: false, code, detail: result.error.message };
|
|
129
|
-
}
|
|
130
|
-
if (result.status !== 0) {
|
|
131
|
-
const tail = output.stderrTail().slice(-DETAIL_MAX_CHARS);
|
|
132
|
-
return {
|
|
133
|
-
ok: false,
|
|
134
|
-
detail: tail || `exit ${result.status ?? "signal"}`,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
return { ok: true };
|
|
138
|
-
}
|
|
139
|
-
catch (err) {
|
|
140
|
-
return {
|
|
141
|
-
ok: false,
|
|
142
|
-
code: err?.code,
|
|
143
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
144
|
-
};
|
|
116
|
+
return runSupervisedUpdateCommand(cmd, args, output, env);
|
|
145
117
|
}
|
|
146
118
|
finally {
|
|
147
119
|
output.dispose();
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawn: nativeSpawn } = require("node:child_process");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
|
|
6
|
+
function windowsTaskkillArgs(pid) {
|
|
7
|
+
return ["/PID", String(pid), "/T", "/F"];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function errorInfo(error) {
|
|
11
|
+
return {
|
|
12
|
+
code: error && typeof error.code === "string" ? error.code : undefined,
|
|
13
|
+
detail:
|
|
14
|
+
error && typeof error.message === "string" ? error.message : String(error ?? "unknown error"),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeMarker(filename, contents, log) {
|
|
19
|
+
try {
|
|
20
|
+
fs.writeFileSync(filename, contents);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const { detail } = errorInfo(error);
|
|
23
|
+
log(`[hq update supervisor] failed to record updater status: ${detail}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Spawn and supervise one package-manager update. On POSIX the detached child
|
|
29
|
+
* is its own process-group leader, so signals reach the leader and every
|
|
30
|
+
* descendant. Windows command shims run below a shell, therefore taskkill is
|
|
31
|
+
* used there to terminate the full descendant tree instead of only that shell.
|
|
32
|
+
*
|
|
33
|
+
* Dependencies are injectable to pin failure handling without requiring root
|
|
34
|
+
* privileges or a Windows host in the test suite.
|
|
35
|
+
*/
|
|
36
|
+
function runUpdateSupervisor(config, dependencies = {}) {
|
|
37
|
+
const spawn = dependencies.spawn || nativeSpawn;
|
|
38
|
+
const signal = dependencies.kill || process.kill.bind(process);
|
|
39
|
+
const setTimer = dependencies.setTimeout || setTimeout;
|
|
40
|
+
const clearTimer = dependencies.clearTimeout || clearTimeout;
|
|
41
|
+
const platform = dependencies.platform || process.platform;
|
|
42
|
+
const exit = dependencies.exit || ((code) => process.exit(code));
|
|
43
|
+
const log = dependencies.log || console.error;
|
|
44
|
+
|
|
45
|
+
let child;
|
|
46
|
+
let deadlineTimer;
|
|
47
|
+
let graceTimer;
|
|
48
|
+
let reapTimer;
|
|
49
|
+
let finished = false;
|
|
50
|
+
let timedOut = false;
|
|
51
|
+
let childClosed = false;
|
|
52
|
+
let treeTerminationConfirmed = false;
|
|
53
|
+
|
|
54
|
+
function clear(timer) {
|
|
55
|
+
if (timer !== undefined) clearTimer(timer);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function finish(code, options = {}) {
|
|
59
|
+
if (finished) return;
|
|
60
|
+
finished = true;
|
|
61
|
+
clear(deadlineTimer);
|
|
62
|
+
clear(graceTimer);
|
|
63
|
+
clear(reapTimer);
|
|
64
|
+
if (options.timedOut) writeMarker(config.timeoutMarker, "timed-out", log);
|
|
65
|
+
if (options.error) writeMarker(config.errorMarker, JSON.stringify(options.error), log);
|
|
66
|
+
exit(code);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function timeoutFailure(code, detail) {
|
|
70
|
+
finish(124, {
|
|
71
|
+
timedOut: true,
|
|
72
|
+
error: { code, detail },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function timeoutAfterReap() {
|
|
77
|
+
finish(124, { timedOut: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function waitForReap(after) {
|
|
81
|
+
if (childClosed) {
|
|
82
|
+
timeoutAfterReap();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
reapTimer = setTimer(() => {
|
|
86
|
+
timeoutFailure(
|
|
87
|
+
"EREAP",
|
|
88
|
+
`timed out after ${config.timeoutMs}ms and ${after} did not close the updater process`,
|
|
89
|
+
);
|
|
90
|
+
}, config.reapMs);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function signalProcessGroup(signalName) {
|
|
94
|
+
if (!child || !child.pid) {
|
|
95
|
+
timeoutFailure("ESRCH", `failed to send ${signalName}: updater has no process ID`);
|
|
96
|
+
return "failed";
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
signal(-child.pid, signalName);
|
|
100
|
+
return "sent";
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const { code, detail } = errorInfo(error);
|
|
103
|
+
if (code === "ESRCH") return "gone";
|
|
104
|
+
// An EPERM here is possible after the sudo retry: waiting would violate
|
|
105
|
+
// the deadline, while claiming the process tree was reaped would lie.
|
|
106
|
+
timeoutFailure(code, `failed to send ${signalName} to updater process group: ${detail}`);
|
|
107
|
+
return "failed";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function terminateWindowsTree() {
|
|
112
|
+
if (!child || !child.pid) {
|
|
113
|
+
timeoutFailure("ESRCH", "failed to start taskkill: updater has no process ID");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
let killer;
|
|
117
|
+
try {
|
|
118
|
+
killer = spawn("taskkill", windowsTaskkillArgs(child.pid), {
|
|
119
|
+
stdio: "ignore",
|
|
120
|
+
windowsHide: true,
|
|
121
|
+
});
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const { code, detail } = errorInfo(error);
|
|
124
|
+
timeoutFailure(code, `failed to start taskkill for updater process tree: ${detail}`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
killer.once("error", (error) => {
|
|
128
|
+
const { code, detail } = errorInfo(error);
|
|
129
|
+
timeoutFailure(code, `taskkill failed for updater process tree: ${detail}`);
|
|
130
|
+
});
|
|
131
|
+
killer.once("close", (code) => {
|
|
132
|
+
if (code !== 0) {
|
|
133
|
+
timeoutFailure("ETASKKILL", `taskkill exited ${code ?? "without a status"} for updater process tree`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
treeTerminationConfirmed = true;
|
|
137
|
+
waitForReap("taskkill");
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
child = spawn(config.cmd, config.args, {
|
|
143
|
+
detached: true,
|
|
144
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
145
|
+
shell: config.shell,
|
|
146
|
+
});
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const { code, detail } = errorInfo(error);
|
|
149
|
+
finish(127, { error: { code, detail } });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
child.once("error", (error) => {
|
|
154
|
+
const { code, detail } = errorInfo(error);
|
|
155
|
+
finish(127, { error: { code, detail } });
|
|
156
|
+
});
|
|
157
|
+
child.once("close", (code) => {
|
|
158
|
+
childClosed = true;
|
|
159
|
+
if (!timedOut) {
|
|
160
|
+
finish(code === 0 ? 0 : (code ?? 1));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (treeTerminationConfirmed) timeoutAfterReap();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
deadlineTimer = setTimer(() => {
|
|
167
|
+
if (finished) return;
|
|
168
|
+
timedOut = true;
|
|
169
|
+
if (platform === "win32") {
|
|
170
|
+
terminateWindowsTree();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const term = signalProcessGroup("SIGTERM");
|
|
174
|
+
if (term === "failed") return;
|
|
175
|
+
if (term === "gone") {
|
|
176
|
+
treeTerminationConfirmed = true;
|
|
177
|
+
waitForReap("the already-exited process group");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
graceTimer = setTimer(() => {
|
|
181
|
+
if (finished) return;
|
|
182
|
+
const kill = signalProcessGroup("SIGKILL");
|
|
183
|
+
if (kill === "failed") return;
|
|
184
|
+
treeTerminationConfirmed = true;
|
|
185
|
+
waitForReap(kill === "gone" ? "the already-exited process group" : "SIGKILL");
|
|
186
|
+
}, config.graceMs);
|
|
187
|
+
}, config.timeoutMs);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (require.main === module) {
|
|
191
|
+
runUpdateSupervisor(JSON.parse(process.argv[2]));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { runUpdateSupervisor, windowsTaskkillArgs };
|
|
@@ -283,6 +283,7 @@ export { buildSpawnPlan, quoteForWindowsShell };
|
|
|
283
283
|
* protection silently disappearing.
|
|
284
284
|
*/
|
|
285
285
|
export declare function inOwnProcessGroup<T extends object>(options: T): T;
|
|
286
|
+
declare function updateTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
286
287
|
/**
|
|
287
288
|
* Where an install's output goes — the other half of surviving a killed parent.
|
|
288
289
|
*
|
|
@@ -307,9 +308,21 @@ export interface InstallOutput {
|
|
|
307
308
|
stdio: ("ignore" | "inherit" | number)[];
|
|
308
309
|
/** Captured stderr tail, or "" when output was inherited by a terminal. */
|
|
309
310
|
stderrTail: () => string;
|
|
311
|
+
/** Private side-channel from the supervisor, kept out of installer output. */
|
|
312
|
+
timeoutMarker: string;
|
|
313
|
+
/** Private spawn-error side-channel that preserves the prior result contract. */
|
|
314
|
+
errorMarker: string;
|
|
315
|
+
/** Whether the asynchronous installer supervisor reported a deadline expiry. */
|
|
316
|
+
timedOut: () => boolean;
|
|
317
|
+
/** Original package-manager launch error, if the supervisor could start. */
|
|
318
|
+
spawnError: () => {
|
|
319
|
+
code?: string;
|
|
320
|
+
detail?: string;
|
|
321
|
+
} | undefined;
|
|
310
322
|
dispose: () => void;
|
|
311
323
|
}
|
|
312
324
|
export declare function openInstallOutput(verbose: boolean, isTty?: boolean): InstallOutput;
|
|
325
|
+
export declare function runSupervisedUpdateCommand(cmd: string, args: string[], output: InstallOutput, env?: NodeJS.ProcessEnv): UpdateResult;
|
|
313
326
|
export declare function runUpdateCommand(cmd: string, args: string[], env?: NodeJS.ProcessEnv): UpdateResult;
|
|
314
327
|
declare function performUpdateCommand(cmd: string, args: string[], runner?: UpdateRunner, env?: NodeJS.ProcessEnv): UpdateResult;
|
|
315
328
|
declare function performUpdate(command: string, runner?: UpdateRunner): UpdateResult;
|
|
@@ -385,6 +398,9 @@ interface EnforceUpdateDeps {
|
|
|
385
398
|
* Exit codes:
|
|
386
399
|
* 0 — update succeeded; user must rerun their command
|
|
387
400
|
* 75 — update failed (EX_TEMPFAIL; common for sudo/EACCES on system npm)
|
|
401
|
+
*
|
|
402
|
+
* A timed-out updater is the exception: it has been forcibly reaped, so the
|
|
403
|
+
* gate returns and lets the user's command run on the current version.
|
|
388
404
|
*/
|
|
389
405
|
declare function enforceUpdateRequired(decision: VersionCheckResponse, deps?: EnforceUpdateDeps): void;
|
|
390
406
|
/**
|
|
@@ -418,6 +434,7 @@ export declare function shouldSkipGate(argv: readonly string[]): boolean;
|
|
|
418
434
|
export declare const __test__: {
|
|
419
435
|
CLIENT_ID: string;
|
|
420
436
|
CONVERGENCE_TIMEOUT_MS: number;
|
|
437
|
+
DEFAULT_UPDATE_TIMEOUT_MS: number;
|
|
421
438
|
ENDPOINT_PATH: string;
|
|
422
439
|
FETCH_TIMEOUT_MS: number;
|
|
423
440
|
checkUpdateConvergence: typeof checkUpdateConvergence;
|
|
@@ -448,5 +465,6 @@ export declare const __test__: {
|
|
|
448
465
|
resolveRunningInstall: typeof resolveRunningInstall;
|
|
449
466
|
resolveRunningManager: typeof resolveRunningManager;
|
|
450
467
|
resolveRunningPrefix: typeof resolveRunningPrefix;
|
|
468
|
+
updateTimeoutMs: typeof updateTimeoutMs;
|
|
451
469
|
};
|
|
452
470
|
//# sourceMappingURL=version-gate.d.ts.map
|