@indigoai-us/hq-cli 5.115.6 → 5.117.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 +169 -11
- package/dist/command-catalog.generated.d.ts +220 -2
- package/dist/command-catalog.generated.js +281 -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/bot.d.ts +140 -1
- package/dist/commands/bot.js +757 -22
- 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/bot/api.d.ts +51 -0
- package/dist/lib/bot/api.js +32 -0
- package/dist/lib/bot/daemon.d.ts +17 -0
- package/dist/lib/bot/daemon.js +44 -3
- package/dist/lib/bot/index.d.ts +4 -0
- package/dist/lib/bot/index.js +4 -0
- package/dist/lib/bot/inflight.d.ts +14 -0
- package/dist/lib/bot/local-config.d.ts +70 -0
- package/dist/lib/bot/local-config.js +147 -0
- package/dist/lib/bot/local-name.d.ts +54 -0
- package/dist/lib/bot/local-name.js +114 -0
- package/dist/lib/bot/run.d.ts +9 -0
- package/dist/lib/bot/run.js +117 -24
- package/dist/lib/bot/runnable.d.ts +51 -0
- package/dist/lib/bot/runnable.js +65 -0
- package/dist/lib/bot/self-heal.d.ts +52 -0
- package/dist/lib/bot/self-heal.js +79 -0
- package/dist/lib/bot/split.d.ts +32 -0
- package/dist/lib/bot/split.js +241 -0
- 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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-heal for a launchd registration that outlived its bot.
|
|
3
|
+
*
|
|
4
|
+
* `hq bot run <name>` is what the LaunchAgent runs. When the bot's local
|
|
5
|
+
* config is gone — a wiped ~/.hq, a half-finished reinstall, a bot removed by
|
|
6
|
+
* hand — the run used to exit non-zero, and KeepAlive{SuccessfulExit:false}
|
|
7
|
+
* with ThrottleInterval 5 relaunched it every five seconds forever (one test
|
|
8
|
+
* Mac logged 267 identical "No bot named" lines). So the run now does two
|
|
9
|
+
* things instead: it exits 0, which KeepAlive does not restart, and it removes
|
|
10
|
+
* its own registration, so a stale plist cannot outlive the bot it points at.
|
|
11
|
+
*
|
|
12
|
+
* Order matters here because this code usually IS the launchd job: the log
|
|
13
|
+
* line is written and the plist unlinked first, and the `bootout` — which
|
|
14
|
+
* terminates the caller — goes last, in a detached session that survives it.
|
|
15
|
+
*
|
|
16
|
+
* Everything is injectable (platform, launchctl, fs, home) so tests never
|
|
17
|
+
* touch the real service manager or the real LaunchAgents folder.
|
|
18
|
+
*/
|
|
19
|
+
import { buildBotDaemonPaths, uninstallBotDaemon } from "./daemon.js";
|
|
20
|
+
import { launchdLabel } from "./paths.js";
|
|
21
|
+
/**
|
|
22
|
+
* Take away the startup registration of a bot that has no local config here.
|
|
23
|
+
* Never throws: an unresolvable `hq` binary or an unwritable LaunchAgents
|
|
24
|
+
* folder must not turn into a crash loop of its own.
|
|
25
|
+
*/
|
|
26
|
+
export function removeOrphanBotDaemon(opts, deps = {}) {
|
|
27
|
+
const label = launchdLabel(opts.name);
|
|
28
|
+
const toOutcome = (result) => {
|
|
29
|
+
const removed = /^Removed /.test(result.message);
|
|
30
|
+
return {
|
|
31
|
+
name: opts.name,
|
|
32
|
+
label,
|
|
33
|
+
removed,
|
|
34
|
+
...(result.dest ? { dest: result.dest } : {}),
|
|
35
|
+
message: removed
|
|
36
|
+
? `Removed the startup agent ${label} so it stops relaunching.`
|
|
37
|
+
: `No startup agent for ${opts.name} to remove.`,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
let result;
|
|
41
|
+
try {
|
|
42
|
+
const paths = buildBotDaemonPaths({
|
|
43
|
+
name: opts.name,
|
|
44
|
+
botDir: opts.botDir,
|
|
45
|
+
...(opts.home ? { home: opts.home } : {}),
|
|
46
|
+
// The plist path depends only on the name and home; a placeholder keeps
|
|
47
|
+
// an environment without a resolvable `hq` on PATH from throwing here.
|
|
48
|
+
hqBinary: "hq",
|
|
49
|
+
nodeBinary: process.execPath,
|
|
50
|
+
});
|
|
51
|
+
result = uninstallBotDaemon(paths, {
|
|
52
|
+
...deps,
|
|
53
|
+
// This runs inside the very job being removed, so the bootout has to
|
|
54
|
+
// outlive us: detached by default, overridable for tests and for callers
|
|
55
|
+
// that are not the job.
|
|
56
|
+
bootout: deps.bootout ?? "detached",
|
|
57
|
+
...(opts.announce ? { announce: (r) => opts.announce(toOutcome(r)) } : {}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const problem = err instanceof Error ? err.message : String(err);
|
|
62
|
+
return { name: opts.name, label, removed: false, problem, message: `Could not remove the startup agent ${label}: ${problem}` };
|
|
63
|
+
}
|
|
64
|
+
return toOutcome(result);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* What `hq bot run <name>` prints, once, when the bot is not set up on this
|
|
68
|
+
* computer — plain enough that a person reading ~/.hq/bots/<name>/logs/bot.log
|
|
69
|
+
* knows exactly what to do next.
|
|
70
|
+
*/
|
|
71
|
+
export function orphanBotRunMessage(name, outcome) {
|
|
72
|
+
const lines = [
|
|
73
|
+
`No bot named "${name}" is set up on this computer, so there is nothing to run.`,
|
|
74
|
+
outcome.message,
|
|
75
|
+
`Bring it back with: hq bot adopt ${name} (or restore every bot you own: hq bot restore)`,
|
|
76
|
+
];
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=self-heal.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A long reply, cut into posts HQ Cloud will actually accept.
|
|
3
|
+
*
|
|
4
|
+
* `POST /v1/notify/dm` refuses a body over DM_BODY_MAX characters with
|
|
5
|
+
* `400: Message body exceeds 4000 characters`. Nothing used to split, so a
|
|
6
|
+
* model answer over the limit was sent whole, refused, saved on the in-flight
|
|
7
|
+
* marker and re-sent identically on every poll — the answer was lost, the
|
|
8
|
+
* person was never told, and the bot asked the API to do the impossible about
|
|
9
|
+
* twice a second for as long as it ran.
|
|
10
|
+
*
|
|
11
|
+
* `splitBody` is the fix, and it is pure so it can be tested on its own:
|
|
12
|
+
*
|
|
13
|
+
* - a body at or under the limit comes back untouched, as one part
|
|
14
|
+
* - a longer body is cut on paragraph boundaries first, then on sentence
|
|
15
|
+
* boundaries, and only hard-wrapped when a single sentence is still too long
|
|
16
|
+
* - a fenced code block is kept whole when it fits; when it cannot fit, the
|
|
17
|
+
* fence is closed at the end of one part and reopened (with its language) at
|
|
18
|
+
* the start of the next, so neither part renders as broken markdown
|
|
19
|
+
* - when there is more than one part, each is prefixed `(2/3)` on its own line
|
|
20
|
+
* so the reader can see the order; the prefix is paid for out of the limit,
|
|
21
|
+
* never added on top of it
|
|
22
|
+
*
|
|
23
|
+
* Every returned part is guaranteed to be at most `max` characters long.
|
|
24
|
+
*/
|
|
25
|
+
/** The largest body HQ Cloud accepts on a DM or channel post. */
|
|
26
|
+
export declare const DM_BODY_MAX = 4000;
|
|
27
|
+
/**
|
|
28
|
+
* One outbound body as the ordered parts to post, each at most `max`
|
|
29
|
+
* characters. A body that already fits comes back unchanged.
|
|
30
|
+
*/
|
|
31
|
+
export declare function splitBody(body: string, max?: number): string[];
|
|
32
|
+
//# sourceMappingURL=split.d.ts.map
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A long reply, cut into posts HQ Cloud will actually accept.
|
|
3
|
+
*
|
|
4
|
+
* `POST /v1/notify/dm` refuses a body over DM_BODY_MAX characters with
|
|
5
|
+
* `400: Message body exceeds 4000 characters`. Nothing used to split, so a
|
|
6
|
+
* model answer over the limit was sent whole, refused, saved on the in-flight
|
|
7
|
+
* marker and re-sent identically on every poll — the answer was lost, the
|
|
8
|
+
* person was never told, and the bot asked the API to do the impossible about
|
|
9
|
+
* twice a second for as long as it ran.
|
|
10
|
+
*
|
|
11
|
+
* `splitBody` is the fix, and it is pure so it can be tested on its own:
|
|
12
|
+
*
|
|
13
|
+
* - a body at or under the limit comes back untouched, as one part
|
|
14
|
+
* - a longer body is cut on paragraph boundaries first, then on sentence
|
|
15
|
+
* boundaries, and only hard-wrapped when a single sentence is still too long
|
|
16
|
+
* - a fenced code block is kept whole when it fits; when it cannot fit, the
|
|
17
|
+
* fence is closed at the end of one part and reopened (with its language) at
|
|
18
|
+
* the start of the next, so neither part renders as broken markdown
|
|
19
|
+
* - when there is more than one part, each is prefixed `(2/3)` on its own line
|
|
20
|
+
* so the reader can see the order; the prefix is paid for out of the limit,
|
|
21
|
+
* never added on top of it
|
|
22
|
+
*
|
|
23
|
+
* Every returned part is guaranteed to be at most `max` characters long.
|
|
24
|
+
*/
|
|
25
|
+
/** The largest body HQ Cloud accepts on a DM or channel post. */
|
|
26
|
+
export const DM_BODY_MAX = 4000;
|
|
27
|
+
const FENCE_RE = /^[ \t]*(`{3,}|~{3,})(.*)$/;
|
|
28
|
+
function bare(line) {
|
|
29
|
+
return line.replace(/\r?\n$/, "");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The body as paragraphs and whole fenced code blocks, in order. Concatenating
|
|
33
|
+
* every `raw` reproduces the input exactly, so nothing is lost or reflowed
|
|
34
|
+
* before a cut is actually needed.
|
|
35
|
+
*/
|
|
36
|
+
function segmentize(body) {
|
|
37
|
+
const segments = [];
|
|
38
|
+
let buf = "";
|
|
39
|
+
let fence = null;
|
|
40
|
+
const flush = (fenceOpen) => {
|
|
41
|
+
if (buf)
|
|
42
|
+
segments.push({ raw: buf, fenceOpen });
|
|
43
|
+
buf = "";
|
|
44
|
+
};
|
|
45
|
+
for (const line of body.split(/(?<=\n)/)) {
|
|
46
|
+
const text = bare(line);
|
|
47
|
+
if (fence) {
|
|
48
|
+
buf += line;
|
|
49
|
+
const m = FENCE_RE.exec(text);
|
|
50
|
+
// A closing fence is the same character, at least as long, and bare.
|
|
51
|
+
if (m && m[1][0] === fence.marker[0] && m[1].length >= fence.marker.length && m[2].trim() === "") {
|
|
52
|
+
flush(fence.open);
|
|
53
|
+
fence = null;
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const m = FENCE_RE.exec(text);
|
|
58
|
+
if (m) {
|
|
59
|
+
flush(null);
|
|
60
|
+
fence = { open: text.trimEnd(), marker: m[1] };
|
|
61
|
+
buf = line;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (text.trim() === "") {
|
|
65
|
+
// A blank line closes the paragraph it follows.
|
|
66
|
+
if (buf)
|
|
67
|
+
buf += line;
|
|
68
|
+
else
|
|
69
|
+
buf = line;
|
|
70
|
+
flush(null);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
buf += line;
|
|
74
|
+
}
|
|
75
|
+
flush(fence ? fence.open : null);
|
|
76
|
+
return segments;
|
|
77
|
+
}
|
|
78
|
+
/** Break points inside a paragraph: after sentence-ending punctuation, or at a line end. */
|
|
79
|
+
function splitSentences(text) {
|
|
80
|
+
const parts = [];
|
|
81
|
+
const re = /(?:[.!?…]["'”’)\]]*|\n)[ \t]*\n?[ \t]*/g;
|
|
82
|
+
let start = 0;
|
|
83
|
+
let m;
|
|
84
|
+
while ((m = re.exec(text)) !== null) {
|
|
85
|
+
const end = m.index + m[0].length;
|
|
86
|
+
if (end <= start) {
|
|
87
|
+
re.lastIndex = start + 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
parts.push(text.slice(start, end));
|
|
91
|
+
start = end;
|
|
92
|
+
}
|
|
93
|
+
if (start < text.length)
|
|
94
|
+
parts.push(text.slice(start));
|
|
95
|
+
return parts.length > 0 ? parts : [text];
|
|
96
|
+
}
|
|
97
|
+
/** Last resort: a run of text with no usable break, cut at whitespace when there is any. */
|
|
98
|
+
function hardWrap(text, budget) {
|
|
99
|
+
const out = [];
|
|
100
|
+
let rest = text;
|
|
101
|
+
while (rest.length > budget) {
|
|
102
|
+
const window = rest.slice(0, budget);
|
|
103
|
+
const ws = Math.max(window.lastIndexOf(" "), window.lastIndexOf("\n"));
|
|
104
|
+
const cut = ws > 0 && ws >= Math.floor(budget * 0.6) ? ws : budget;
|
|
105
|
+
const piece = rest.slice(0, cut).trim();
|
|
106
|
+
if (piece)
|
|
107
|
+
out.push(piece);
|
|
108
|
+
rest = rest.slice(cut).replace(/^[ \t\n]+/, "");
|
|
109
|
+
}
|
|
110
|
+
const tail = rest.trim();
|
|
111
|
+
if (tail)
|
|
112
|
+
out.push(tail);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/** A code block too long for one part: closed and reopened around every cut. */
|
|
116
|
+
function splitFenced(segment, budget) {
|
|
117
|
+
const open = segment.fenceOpen ?? "```";
|
|
118
|
+
const marker = FENCE_RE.exec(open)?.[1] ?? "```";
|
|
119
|
+
const overhead = open.length + marker.length + 2; // the two fence lines and their newlines
|
|
120
|
+
const inner = budget - overhead;
|
|
121
|
+
if (inner < 1)
|
|
122
|
+
return splitPlain(segment.raw, budget);
|
|
123
|
+
const lines = segment.raw.split(/(?<=\n)/).slice(1);
|
|
124
|
+
const last = lines[lines.length - 1];
|
|
125
|
+
if (last !== undefined) {
|
|
126
|
+
const m = FENCE_RE.exec(bare(last));
|
|
127
|
+
if (m && m[1][0] === marker[0] && m[2].trim() === "")
|
|
128
|
+
lines.pop();
|
|
129
|
+
}
|
|
130
|
+
const pieces = [];
|
|
131
|
+
let buf = "";
|
|
132
|
+
const emit = () => {
|
|
133
|
+
const code = buf.replace(/\n+$/, "");
|
|
134
|
+
buf = "";
|
|
135
|
+
if (code.trim() === "")
|
|
136
|
+
return;
|
|
137
|
+
pieces.push(`${open}\n${code}\n${marker}`);
|
|
138
|
+
};
|
|
139
|
+
for (const line of lines) {
|
|
140
|
+
if ((buf + line).replace(/\n+$/, "").length <= inner) {
|
|
141
|
+
buf += line;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
emit();
|
|
145
|
+
const one = bare(line);
|
|
146
|
+
if (one.length <= inner) {
|
|
147
|
+
buf = line;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const wrapped of hardWrap(one, inner)) {
|
|
151
|
+
buf = wrapped;
|
|
152
|
+
emit();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
emit();
|
|
156
|
+
return pieces.length > 0 ? pieces : splitPlain(segment.raw, budget);
|
|
157
|
+
}
|
|
158
|
+
/** A paragraph too long for one part: sentences first, hard wrap only if one sentence is still too long. */
|
|
159
|
+
function splitPlain(text, budget) {
|
|
160
|
+
const out = [];
|
|
161
|
+
let buf = "";
|
|
162
|
+
const flush = () => {
|
|
163
|
+
const t = buf.trim();
|
|
164
|
+
buf = "";
|
|
165
|
+
if (t)
|
|
166
|
+
out.push(t);
|
|
167
|
+
};
|
|
168
|
+
for (const sentence of splitSentences(text)) {
|
|
169
|
+
if ((buf + sentence).trim().length <= budget) {
|
|
170
|
+
buf += sentence;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
flush();
|
|
174
|
+
if (sentence.trim().length <= budget) {
|
|
175
|
+
buf = sentence;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
for (const wrapped of hardWrap(sentence.trim(), budget))
|
|
179
|
+
out.push(wrapped);
|
|
180
|
+
}
|
|
181
|
+
flush();
|
|
182
|
+
return out.length > 0 ? out : hardWrap(text.trim(), budget);
|
|
183
|
+
}
|
|
184
|
+
function pack(body, budget) {
|
|
185
|
+
const out = [];
|
|
186
|
+
let acc = "";
|
|
187
|
+
const flush = () => {
|
|
188
|
+
const t = acc.trim();
|
|
189
|
+
acc = "";
|
|
190
|
+
if (t)
|
|
191
|
+
out.push(t);
|
|
192
|
+
};
|
|
193
|
+
for (const segment of segmentize(body)) {
|
|
194
|
+
if ((acc + segment.raw).trim().length <= budget) {
|
|
195
|
+
acc += segment.raw;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
flush();
|
|
199
|
+
if (segment.raw.trim().length <= budget) {
|
|
200
|
+
acc = segment.raw;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
for (const piece of segment.fenceOpen ? splitFenced(segment, budget) : splitPlain(segment.raw, budget)) {
|
|
204
|
+
out.push(piece);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
flush();
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
/** `(2/3)` and its newline, at the widest the count can make it. */
|
|
211
|
+
function prefixWidth(count) {
|
|
212
|
+
return `(${count}/${count})\n`.length;
|
|
213
|
+
}
|
|
214
|
+
function label(parts) {
|
|
215
|
+
if (parts.length < 2)
|
|
216
|
+
return parts;
|
|
217
|
+
return parts.map((part, i) => `(${i + 1}/${parts.length})\n${part}`);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* One outbound body as the ordered parts to post, each at most `max`
|
|
221
|
+
* characters. A body that already fits comes back unchanged.
|
|
222
|
+
*/
|
|
223
|
+
export function splitBody(body, max = DM_BODY_MAX) {
|
|
224
|
+
const limit = Math.max(1, Math.floor(max));
|
|
225
|
+
if (body.length <= limit)
|
|
226
|
+
return [body];
|
|
227
|
+
let reserve = prefixWidth(2);
|
|
228
|
+
let parts = [];
|
|
229
|
+
for (let round = 0; round < 6; round += 1) {
|
|
230
|
+
parts = pack(body, Math.max(1, limit - reserve));
|
|
231
|
+
const needed = parts.length > 1 ? prefixWidth(parts.length) : 0;
|
|
232
|
+
if (needed <= reserve)
|
|
233
|
+
break;
|
|
234
|
+
reserve = needed;
|
|
235
|
+
}
|
|
236
|
+
if (parts.length === 0)
|
|
237
|
+
parts = [body.trim() || body];
|
|
238
|
+
// Belt and braces: whatever the shape of the input, no part may exceed the limit.
|
|
239
|
+
return label(parts).flatMap((part) => (part.length <= limit ? [part] : hardWrap(part, limit)));
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=split.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
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 * as fs from "node:fs";
|
|
8
|
+
import { type RunCommand } from "./launchd.js";
|
|
9
|
+
import { type ServiceHostPaths, type ServicePlatform, type ServiceSpec } from "./types.js";
|
|
10
|
+
export type { ServiceSpec, ServiceHostPaths, ServicePlatform } from "./types.js";
|
|
11
|
+
export { detectServicePlatform } from "./types.js";
|
|
12
|
+
export { renderLaunchdPlist } from "./launchd.js";
|
|
13
|
+
export { renderSystemdUserUnit, systemdUnitName } from "./systemd.js";
|
|
14
|
+
export declare const defaultRunCommand: RunCommand;
|
|
15
|
+
export interface ServiceManagerDeps {
|
|
16
|
+
platform?: ServicePlatform;
|
|
17
|
+
run?: RunCommand;
|
|
18
|
+
writeFileSync?: typeof fs.writeFileSync;
|
|
19
|
+
mkdirSync?: typeof fs.mkdirSync;
|
|
20
|
+
unlinkSync?: typeof fs.unlinkSync;
|
|
21
|
+
existsSync?: typeof fs.existsSync;
|
|
22
|
+
/** When false, render + write units but do not (de)activate them. */
|
|
23
|
+
activate?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface ServiceUnitResult {
|
|
26
|
+
name: string;
|
|
27
|
+
unitPath: string;
|
|
28
|
+
rendered?: string;
|
|
29
|
+
installed: boolean;
|
|
30
|
+
running?: boolean;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ServiceSetResult {
|
|
34
|
+
platform: ServicePlatform;
|
|
35
|
+
units: ServiceUnitResult[];
|
|
36
|
+
/** Human hint when the platform has no service manager integration. */
|
|
37
|
+
manualCommands?: string[];
|
|
38
|
+
}
|
|
39
|
+
export declare function renderUnit(platform: ServicePlatform, spec: ServiceSpec, host: ServiceHostPaths): string;
|
|
40
|
+
export declare function installServices(specs: ServiceSpec[], host: ServiceHostPaths, deps?: ServiceManagerDeps): ServiceSetResult;
|
|
41
|
+
export declare function uninstallServices(specs: ServiceSpec[], host: ServiceHostPaths, deps?: ServiceManagerDeps): ServiceSetResult;
|
|
42
|
+
export declare function servicesStatus(specs: ServiceSpec[], host: ServiceHostPaths, deps?: ServiceManagerDeps): ServiceSetResult;
|
|
43
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -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
|