kandev 0.64.0 → 0.67.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/README.md +2 -10
- package/bin/cli.js +3 -1
- package/bin/native-shim.js +94 -0
- package/package.json +14 -16
- package/dist/args.js +0 -123
- package/dist/backup.js +0 -98
- package/dist/bundle.js +0 -80
- package/dist/cli.js +0 -155
- package/dist/constants.js +0 -54
- package/dist/dev.js +0 -157
- package/dist/github.js +0 -221
- package/dist/health.js +0 -71
- package/dist/kandev-env.js +0 -28
- package/dist/platform.js +0 -53
- package/dist/ports.js +0 -142
- package/dist/process.js +0 -122
- package/dist/run.js +0 -283
- package/dist/runtime.js +0 -76
- package/dist/service/args.js +0 -131
- package/dist/service/config.js +0 -111
- package/dist/service/health_check.js +0 -84
- package/dist/service/index.js +0 -70
- package/dist/service/install_helpers.js +0 -51
- package/dist/service/launchctl.js +0 -73
- package/dist/service/linux.js +0 -165
- package/dist/service/macos.js +0 -221
- package/dist/service/metadata.js +0 -46
- package/dist/service/paths.js +0 -168
- package/dist/service/self_update.js +0 -223
- package/dist/service/stale_check.js +0 -78
- package/dist/service/templates.js +0 -229
- package/dist/shared.js +0 -214
- package/dist/start.js +0 -188
- package/dist/supervisor/backend.js +0 -83
- package/dist/supervisor/child.js +0 -64
- package/dist/supervisor/control.js +0 -97
- package/dist/supervisor/manifest.js +0 -74
- package/dist/supervisor/paths.js +0 -33
- package/dist/version.js +0 -57
- package/dist/web.js +0 -68
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.readSelfUpdateIntent = readSelfUpdateIntent;
|
|
7
|
-
exports.planSelfUpdate = planSelfUpdate;
|
|
8
|
-
exports.runSelfUpdateCommand = runSelfUpdateCommand;
|
|
9
|
-
const node_child_process_1 = require("node:child_process");
|
|
10
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
12
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
-
const paths_1 = require("./paths");
|
|
14
|
-
function readSelfUpdateIntent(intentPath) {
|
|
15
|
-
return JSON.parse(node_fs_1.default.readFileSync(intentPath, "utf8"));
|
|
16
|
-
}
|
|
17
|
-
function planSelfUpdate(intent, opts = {}) {
|
|
18
|
-
const platform = opts.platform ?? process.platform;
|
|
19
|
-
const target = npmVersion(intent.target_version || intent.target_tag);
|
|
20
|
-
const install = intent.install;
|
|
21
|
-
const installArgs = serviceInstallArgs(install);
|
|
22
|
-
const commands = [];
|
|
23
|
-
if (install.kind === "homebrew") {
|
|
24
|
-
// `brew upgrade` installs the tap formula's current version; it can't be
|
|
25
|
-
// pinned to intent.target_version the way npm/npx can (Homebrew has no
|
|
26
|
-
// stable "install version X" without a separate versioned formula). In
|
|
27
|
-
// practice both target_version and the formula derive from the same latest
|
|
28
|
-
// GitHub release, so they match. If the formula lags, the restarted backend
|
|
29
|
-
// reports the older version and the frontend progress poll (which waits for
|
|
30
|
-
// info.version === target_version) times out gracefully rather than
|
|
31
|
-
// reporting a false success.
|
|
32
|
-
commands.push({ command: "brew", args: ["upgrade", "kandev"] });
|
|
33
|
-
// Re-run service install via the upgraded `kandev` wrapper (resolved on
|
|
34
|
-
// PATH), NOT `node <cli_entry>`. Homebrew installs into version-pinned
|
|
35
|
-
// Cellar dirs, so the recorded cli_entry still points at the OLD bundle
|
|
36
|
-
// after `brew upgrade`; worse, invoking node on it directly runs without
|
|
37
|
-
// KANDEV_BUNDLE_DIR/KANDEV_VERSION, so the install is mis-detected as
|
|
38
|
-
// kind "unknown" and the regenerated unit loses the bundle env — the
|
|
39
|
-
// restarted backend then can't find its runtime and the service stays down.
|
|
40
|
-
// The wrapper is version-stable and re-sets the bundle env. (Mirrors the
|
|
41
|
-
// manual `kandev service install` we already document for Homebrew.)
|
|
42
|
-
commands.push({ command: "kandev", args: installArgs });
|
|
43
|
-
}
|
|
44
|
-
else if (install.kind === "npm") {
|
|
45
|
-
commands.push({ command: "npm", args: npmInstallArgs(install.cli_entry, target) });
|
|
46
|
-
commands.push({ command: install.node_path, args: [install.cli_entry, ...installArgs] });
|
|
47
|
-
}
|
|
48
|
-
else if (install.kind === "npx") {
|
|
49
|
-
commands.push({ command: "npx", args: ["-y", `kandev@${target}`, ...installArgs] });
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
throw new Error(`unsupported install kind "${install.kind}"`);
|
|
53
|
-
}
|
|
54
|
-
commands.push(restartCommand(install, platform, opts.uid));
|
|
55
|
-
return commands;
|
|
56
|
-
}
|
|
57
|
-
function runSelfUpdateCommand(args, runner = spawnCommand) {
|
|
58
|
-
if (!args.intent) {
|
|
59
|
-
throw new Error("kandev service self-update requires --intent <path>");
|
|
60
|
-
}
|
|
61
|
-
const intent = readSelfUpdateIntent(args.intent);
|
|
62
|
-
const commands = planSelfUpdate(intent);
|
|
63
|
-
if (args.dryRun || process.env.KANDEV_E2E_MOCK === "true") {
|
|
64
|
-
console.log(JSON.stringify({
|
|
65
|
-
dry_run: !!args.dryRun,
|
|
66
|
-
fake: process.env.KANDEV_E2E_MOCK === "true",
|
|
67
|
-
target_version: intent.target_version,
|
|
68
|
-
commands,
|
|
69
|
-
}, null, 2));
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const log = openSelfUpdateLog(intent);
|
|
73
|
-
try {
|
|
74
|
-
log?.line(`self-update target ${intent.target_tag} (${intent.target_version})`);
|
|
75
|
-
log?.line(`install kind=${intent.install.kind} manager=${intent.install.manager}`);
|
|
76
|
-
log?.line(`planned ${commands.length} command(s):`);
|
|
77
|
-
commands.forEach((step, i) => log?.line(` [${i + 1}/${commands.length}] ${formatCommand(step)}`));
|
|
78
|
-
runSelfUpdateSteps(commands, runner, log);
|
|
79
|
-
log?.line("self-update completed successfully");
|
|
80
|
-
}
|
|
81
|
-
finally {
|
|
82
|
-
log?.close();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function runSelfUpdateSteps(commands, runner, log) {
|
|
86
|
-
for (const [i, step] of commands.entries()) {
|
|
87
|
-
log?.line(`\n[${i + 1}/${commands.length}] $ ${formatCommand(step)}`);
|
|
88
|
-
const res = runner(step.command, step.args);
|
|
89
|
-
teeCommandOutput(log, res);
|
|
90
|
-
if (res.error) {
|
|
91
|
-
log?.line(`! spawn error: ${res.error.message}`);
|
|
92
|
-
throw res.error;
|
|
93
|
-
}
|
|
94
|
-
if (res.status !== 0) {
|
|
95
|
-
log?.line(`! exited with code ${res.status}`);
|
|
96
|
-
throw new Error(`${formatCommand(step)} failed with code ${res.status}`);
|
|
97
|
-
}
|
|
98
|
-
log?.line(" exit 0");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
function formatCommand(step) {
|
|
102
|
-
return [step.command, ...step.args].join(" ");
|
|
103
|
-
}
|
|
104
|
-
function serviceInstallArgs(install) {
|
|
105
|
-
const args = ["service", "install"];
|
|
106
|
-
if (install.mode === "system")
|
|
107
|
-
args.push("--system");
|
|
108
|
-
args.push("--home-dir", install.home_dir);
|
|
109
|
-
if (install.port !== undefined) {
|
|
110
|
-
args.push("--port", String(install.port));
|
|
111
|
-
}
|
|
112
|
-
return args;
|
|
113
|
-
}
|
|
114
|
-
function npmInstallArgs(cliEntry, target) {
|
|
115
|
-
const args = ["install", "-g"];
|
|
116
|
-
const prefix = npmPrefixFromCliEntry(cliEntry);
|
|
117
|
-
if (prefix) {
|
|
118
|
-
args.push("--prefix", prefix);
|
|
119
|
-
}
|
|
120
|
-
args.push(`kandev@${target}`);
|
|
121
|
-
return args;
|
|
122
|
-
}
|
|
123
|
-
function npmPrefixFromCliEntry(cliEntry) {
|
|
124
|
-
const marker = `${node_path_1.default.sep}lib${node_path_1.default.sep}node_modules${node_path_1.default.sep}kandev${node_path_1.default.sep}`;
|
|
125
|
-
const index = cliEntry.indexOf(marker);
|
|
126
|
-
if (index < 0) {
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
return index === 0 ? node_path_1.default.sep : cliEntry.slice(0, index);
|
|
130
|
-
}
|
|
131
|
-
function restartCommand(install, platform, uid) {
|
|
132
|
-
if (platform === "linux") {
|
|
133
|
-
return install.mode === "system"
|
|
134
|
-
? { command: "systemctl", args: ["restart", paths_1.SERVICE_NAME] }
|
|
135
|
-
: { command: "systemctl", args: ["--user", "restart", paths_1.SERVICE_NAME] };
|
|
136
|
-
}
|
|
137
|
-
if (platform === "darwin") {
|
|
138
|
-
// Resolve the uid lazily inside the darwin branch — Linux never needs it, so
|
|
139
|
-
// os.userInfo() shouldn't run there.
|
|
140
|
-
const resolvedUid = uid ?? node_os_1.default.userInfo().uid;
|
|
141
|
-
const domain = install.mode === "system" ? "system" : `gui/${resolvedUid}`;
|
|
142
|
-
return { command: "launchctl", args: ["kickstart", "-k", `${domain}/${paths_1.LAUNCHD_LABEL}`] };
|
|
143
|
-
}
|
|
144
|
-
throw new Error(`unsupported platform "${platform}"`);
|
|
145
|
-
}
|
|
146
|
-
function npmVersion(versionOrTag) {
|
|
147
|
-
const stripped = versionOrTag.replace(/^v/, "");
|
|
148
|
-
return stripped || "latest";
|
|
149
|
-
}
|
|
150
|
-
function spawnCommand(command, args) {
|
|
151
|
-
// Capture stdout/stderr (rather than inheriting) so the self-update log can
|
|
152
|
-
// tee each step's output. The helper runs detached under launchd/systemd, so
|
|
153
|
-
// its console output would otherwise go nowhere visible — the log file is the
|
|
154
|
-
// only forensic trail when an update fails mid-flight. maxBuffer is bumped
|
|
155
|
-
// well above the 1 MiB default because `npm install -g` is chatty.
|
|
156
|
-
return (0, node_child_process_1.spawnSync)(command, args, { maxBuffer: 64 * 1024 * 1024 });
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Open a timestamped log file under the service's log dir for this self-update
|
|
160
|
-
* run. The helper is spawned outside the service process (so a restart doesn't
|
|
161
|
-
* kill it mid-update), which means it has no service log of its own — without
|
|
162
|
-
* this file a failed update on macOS/launchd is nearly invisible. Best-effort:
|
|
163
|
-
* if the log dir can't be created we return null and the update still runs.
|
|
164
|
-
*/
|
|
165
|
-
function openSelfUpdateLog(intent) {
|
|
166
|
-
const dir = intent.install.log_dir || logDirFromHome(intent.install.home_dir);
|
|
167
|
-
if (!dir) {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
try {
|
|
171
|
-
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
172
|
-
// Self-update logs can contain install paths/env; keep the dir owner-only
|
|
173
|
-
// even if it pre-existed with looser perms.
|
|
174
|
-
node_fs_1.default.chmodSync(dir, 0o700);
|
|
175
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
176
|
-
const filePath = node_path_1.default.join(dir, `self-update-${stamp}.log`);
|
|
177
|
-
const fd = node_fs_1.default.openSync(filePath, "a");
|
|
178
|
-
const write = (chunk) => {
|
|
179
|
-
try {
|
|
180
|
-
// The `Buffer | string` union matches neither writeSync overload
|
|
181
|
-
// directly; the cast picks one — both write the runtime value correctly.
|
|
182
|
-
node_fs_1.default.writeSync(fd, chunk);
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
// A write failure mid-update must not abort the update itself.
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
process.stdout.write(`[kandev] self-update log: ${filePath}\n`);
|
|
189
|
-
write(`# kandev self-update ${new Date().toISOString()}\n`);
|
|
190
|
-
return {
|
|
191
|
-
path: filePath,
|
|
192
|
-
line: (message) => write(`${message}\n`),
|
|
193
|
-
raw: (chunk) => write(chunk),
|
|
194
|
-
close: () => {
|
|
195
|
-
try {
|
|
196
|
-
node_fs_1.default.closeSync(fd);
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
// already closed / never opened
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
function logDirFromHome(homeDir) {
|
|
209
|
-
return homeDir ? node_path_1.default.join(homeDir, "logs") : "";
|
|
210
|
-
}
|
|
211
|
-
// Mirror a command's captured output to both the helper log and the process's
|
|
212
|
-
// own stdout/stderr, so whatever does manage to capture the helper's streams
|
|
213
|
-
// (e.g. launchd's StandardOut/ErrorPath, systemd's journal) still sees it.
|
|
214
|
-
function teeCommandOutput(log, res) {
|
|
215
|
-
if (res.stdout && res.stdout.length) {
|
|
216
|
-
process.stdout.write(res.stdout);
|
|
217
|
-
log?.raw(res.stdout);
|
|
218
|
-
}
|
|
219
|
-
if (res.stderr && res.stderr.length) {
|
|
220
|
-
process.stderr.write(res.stderr);
|
|
221
|
-
log?.raw(res.stderr);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.detectStaleServiceUnit = detectStaleServiceUnit;
|
|
7
|
-
exports.warnIfStaleServiceUnit = warnIfStaleServiceUnit;
|
|
8
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
-
const paths_1 = require("./paths");
|
|
10
|
-
const templates_1 = require("./templates");
|
|
11
|
-
/**
|
|
12
|
-
* Locations of the user-mode service unit we manage. We skip system-mode
|
|
13
|
-
* locations because (a) they require sudo to fix anyway, (b) reading from
|
|
14
|
-
* arbitrary `/etc` paths from a regular launch is noisier than worth it.
|
|
15
|
-
*/
|
|
16
|
-
function userModeUnitPath() {
|
|
17
|
-
if (process.platform === "linux")
|
|
18
|
-
return (0, paths_1.linuxUserUnitPath)();
|
|
19
|
-
if (process.platform === "darwin")
|
|
20
|
-
return (0, paths_1.macosUserPlistPath)();
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Check whether an installed user-mode service unit still references the
|
|
25
|
-
* current invocation's paths.
|
|
26
|
-
*
|
|
27
|
-
* Called once per interactive `kandev` start (skipped for `kandev service ...`
|
|
28
|
-
* commands and headless service runs). The check is intentionally cheap and
|
|
29
|
-
* silent on the happy path — only emits when a problem is detected — so it's
|
|
30
|
-
* fine to run unconditionally.
|
|
31
|
-
*
|
|
32
|
-
* Returns the warning message to print, or null when there's nothing to say.
|
|
33
|
-
*/
|
|
34
|
-
function detectStaleServiceUnit() {
|
|
35
|
-
const unitPath = userModeUnitPath();
|
|
36
|
-
if (!unitPath)
|
|
37
|
-
return null;
|
|
38
|
-
let content;
|
|
39
|
-
try {
|
|
40
|
-
content = node_fs_1.default.readFileSync(unitPath, "utf8");
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return null; // no unit installed
|
|
44
|
-
}
|
|
45
|
-
if (!(0, templates_1.looksLikeManagedUnit)(content))
|
|
46
|
-
return null;
|
|
47
|
-
let launcher;
|
|
48
|
-
try {
|
|
49
|
-
launcher = (0, paths_1.captureLauncher)();
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
// captureLauncher throws when process.argv[1] is missing — extremely rare,
|
|
53
|
-
// but if it happens we can't tell whether the unit is stale or not.
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
// Stale = the unit's hard-coded paths no longer match the running binary.
|
|
57
|
-
// We match on substring rather than parsing the unit so the check works for
|
|
58
|
-
// both systemd Environment= lines and plist <string> entries.
|
|
59
|
-
const nodeFresh = content.includes(launcher.nodePath);
|
|
60
|
-
const cliFresh = content.includes(launcher.cliEntry);
|
|
61
|
-
if (nodeFresh && cliFresh)
|
|
62
|
-
return null;
|
|
63
|
-
return (`Your installed kandev service unit (${unitPath}) references paths that\n` +
|
|
64
|
-
` no longer match this binary. This usually happens after upgrading via\n` +
|
|
65
|
-
` npm or Homebrew. Re-run 'kandev service install' to refresh the unit.`);
|
|
66
|
-
}
|
|
67
|
-
/** Convenience: detect + print to stderr in one call. Safe to call from cli.ts. */
|
|
68
|
-
function warnIfStaleServiceUnit() {
|
|
69
|
-
try {
|
|
70
|
-
const msg = detectStaleServiceUnit();
|
|
71
|
-
if (msg) {
|
|
72
|
-
process.stderr.write(`[kandev] notice: ${msg}\n`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
// Never let a diagnostic check break startup.
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.MANAGED_MARKER_TEXT = void 0;
|
|
7
|
-
exports.looksLikeManagedUnit = looksLikeManagedUnit;
|
|
8
|
-
exports.renderSystemdUnit = renderSystemdUnit;
|
|
9
|
-
exports.renderLaunchdPlist = renderLaunchdPlist;
|
|
10
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
-
// Service PATH includes common per-user agent install dirs so user-installed
|
|
13
|
-
// CLIs (npm user prefix, pipx, fnm, Bun globals like oh-my-pi/omp, OpenCode's
|
|
14
|
-
// installer, etc.) are discoverable. System-mode units still run as the
|
|
15
|
-
// configured User=, so %h resolves to that user's home directory.
|
|
16
|
-
const SYSTEMD_SYSTEM_PATH = "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin";
|
|
17
|
-
const SYSTEMD_AGENT_BIN_PATH = `%h/.local/bin:%h/.bun/bin:%h/.opencode/bin`;
|
|
18
|
-
const SYSTEMD_SERVICE_PATH = `${SYSTEMD_AGENT_BIN_PATH}:${SYSTEMD_SYSTEM_PATH}`;
|
|
19
|
-
const LAUNCHD_SYSTEM_PATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
|
20
|
-
const launchdUserPath = () => `${node_os_1.default.homedir()}/.local/bin:${node_os_1.default.homedir()}/.bun/bin:${LAUNCHD_SYSTEM_PATH}`;
|
|
21
|
-
// Prepend the launcher node's bin dir so `npm`/`npx` resolve under per-user
|
|
22
|
-
// node managers (fnm, nvm, asdf, volta, mise), where node lives in a versioned
|
|
23
|
-
// subdirectory not covered by the system PATH. ExecStart already points at this
|
|
24
|
-
// node, so its parent dir is exactly where the matching npm/npx live — without
|
|
25
|
-
// this, npx-based ACP agents (claude, codex, opencode) fail to spawn.
|
|
26
|
-
//
|
|
27
|
-
// The isAbsolute guard exists because `path.dirname` on POSIX returns `'.'`
|
|
28
|
-
// for a path with no `/` separator (e.g. a Windows-style `C:\…\node.exe`
|
|
29
|
-
// passed through). Prepending `.` would put CWD ahead of system dirs in the
|
|
30
|
-
// daemon's PATH — a privilege-escalation footgun. Production callers always
|
|
31
|
-
// pass `process.execPath` (absolute on Linux/macOS), so the guard only kicks
|
|
32
|
-
// in for non-POSIX or relative inputs from future callers / tests.
|
|
33
|
-
function pathWithNodeBinDir(basePath, nodePath) {
|
|
34
|
-
const nodeBinDir = node_path_1.default.dirname(nodePath);
|
|
35
|
-
if (!node_path_1.default.isAbsolute(nodeBinDir))
|
|
36
|
-
return basePath;
|
|
37
|
-
const parts = basePath.split(":");
|
|
38
|
-
if (parts.includes(nodeBinDir))
|
|
39
|
-
return basePath;
|
|
40
|
-
return `${nodeBinDir}:${basePath}`;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Marker substring baked into every unit/plist kandev writes. Used to safely
|
|
44
|
-
* detect "this file is ours, overwriting is fine" vs "user has put something
|
|
45
|
-
* else here, warn loudly before replacing".
|
|
46
|
-
*/
|
|
47
|
-
exports.MANAGED_MARKER_TEXT = "managed by kandev";
|
|
48
|
-
const SYSTEMD_MARKER = `# ${exports.MANAGED_MARKER_TEXT} — regenerated by \`kandev service install\``;
|
|
49
|
-
const PLIST_MARKER = `<!-- ${exports.MANAGED_MARKER_TEXT} — regenerated by \`kandev service install\` -->`;
|
|
50
|
-
function looksLikeManagedUnit(content) {
|
|
51
|
-
return content.includes(exports.MANAGED_MARKER_TEXT);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Render a systemd unit file for kandev.
|
|
55
|
-
*
|
|
56
|
-
* The unit hard-codes absolute paths so it works without a user PATH. We pass
|
|
57
|
-
* `--headless` so the daemon doesn't try to open a browser.
|
|
58
|
-
*
|
|
59
|
-
* For Homebrew installs the launcher carries a resolved `shimPath` — the
|
|
60
|
-
* floating `<prefix>/bin/kandev` shim that survives `brew upgrade`. When set we
|
|
61
|
-
* exec that shim and omit KANDEV_BUNDLE_DIR / KANDEV_VERSION and the
|
|
62
|
-
* version-pinned node bin dir from PATH, because the shim supplies all three
|
|
63
|
-
* itself. Otherwise (npm / unknown installs) we fall back to the version-pinned
|
|
64
|
-
* node + cli.js and surface KANDEV_BUNDLE_DIR / KANDEV_VERSION only when present.
|
|
65
|
-
*/
|
|
66
|
-
function renderSystemdUnit(input) {
|
|
67
|
-
const shimPath = input.launcher.shimPath;
|
|
68
|
-
const basePath = SYSTEMD_SERVICE_PATH;
|
|
69
|
-
// For the shim, prepend its own bin dir (the Homebrew prefix's `bin`, where
|
|
70
|
-
// node/npm/npx live) so npx-based agents resolve even when the prefix isn't
|
|
71
|
-
// one of the hardcoded defaults. pathWithNodeBinDir dedupes when it already is.
|
|
72
|
-
const pathValue = pathWithNodeBinDir(basePath, shimPath ?? input.launcher.nodePath);
|
|
73
|
-
const env = [
|
|
74
|
-
envLine("KANDEV_HOME_DIR", input.homeDir),
|
|
75
|
-
envLine("KANDEV_LOG_LEVEL", "info"),
|
|
76
|
-
envLine("PATH", pathValue),
|
|
77
|
-
];
|
|
78
|
-
if (input.port !== undefined) {
|
|
79
|
-
env.push(envLine("KANDEV_SERVER_PORT", String(input.port)));
|
|
80
|
-
}
|
|
81
|
-
if (!shimPath && input.launcher.bundleDir) {
|
|
82
|
-
env.push(envLine("KANDEV_BUNDLE_DIR", input.launcher.bundleDir));
|
|
83
|
-
}
|
|
84
|
-
if (!shimPath && input.launcher.version) {
|
|
85
|
-
env.push(envLine("KANDEV_VERSION", input.launcher.version));
|
|
86
|
-
}
|
|
87
|
-
if (input.serviceMetadataPath) {
|
|
88
|
-
env.push(...serviceEnvLines(input, "systemd"));
|
|
89
|
-
}
|
|
90
|
-
const wantedBy = input.mode === "system" ? "multi-user.target" : "default.target";
|
|
91
|
-
const userLine = input.mode === "system" && input.systemUser ? `User=${input.systemUser}\n` : "";
|
|
92
|
-
const exec = shimPath
|
|
93
|
-
? `${quoteForUnit(shimPath)} --headless`
|
|
94
|
-
: `${quoteForUnit(input.launcher.nodePath)} ${quoteForUnit(input.launcher.cliEntry)} --headless`;
|
|
95
|
-
return `${SYSTEMD_MARKER}
|
|
96
|
-
[Unit]
|
|
97
|
-
Description=Kandev autonomous agent platform
|
|
98
|
-
Documentation=https://github.com/kdlbs/kandev
|
|
99
|
-
After=network-online.target
|
|
100
|
-
Wants=network-online.target
|
|
101
|
-
|
|
102
|
-
[Service]
|
|
103
|
-
Type=simple
|
|
104
|
-
ExecStart=${exec}
|
|
105
|
-
${userLine}${env.join("\n")}
|
|
106
|
-
Restart=on-failure
|
|
107
|
-
RestartSec=5s
|
|
108
|
-
KillMode=mixed
|
|
109
|
-
TimeoutStopSec=30s
|
|
110
|
-
|
|
111
|
-
[Install]
|
|
112
|
-
WantedBy=${wantedBy}
|
|
113
|
-
`;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Render a launchd plist for kandev.
|
|
117
|
-
*
|
|
118
|
-
* launchd has no equivalent to systemd's enable-linger: a user agent only runs
|
|
119
|
-
* while the user is logged in. For a Mac-as-server scenario, use --system to
|
|
120
|
-
* get a LaunchDaemon that runs at boot regardless of login.
|
|
121
|
-
*/
|
|
122
|
-
function renderLaunchdPlist(input) {
|
|
123
|
-
const shimPath = input.launcher.shimPath;
|
|
124
|
-
const basePath = input.mode === "system" ? LAUNCHD_SYSTEM_PATH : launchdUserPath();
|
|
125
|
-
// See renderSystemdUnit: prepend the shim's own bin dir so npm/npx resolve.
|
|
126
|
-
const pathValue = pathWithNodeBinDir(basePath, shimPath ?? input.launcher.nodePath);
|
|
127
|
-
const envEntries = [
|
|
128
|
-
["KANDEV_HOME_DIR", input.homeDir],
|
|
129
|
-
["KANDEV_LOG_LEVEL", "info"],
|
|
130
|
-
["PATH", pathValue],
|
|
131
|
-
];
|
|
132
|
-
if (input.port !== undefined) {
|
|
133
|
-
envEntries.push(["KANDEV_SERVER_PORT", String(input.port)]);
|
|
134
|
-
}
|
|
135
|
-
if (!shimPath && input.launcher.bundleDir) {
|
|
136
|
-
envEntries.push(["KANDEV_BUNDLE_DIR", input.launcher.bundleDir]);
|
|
137
|
-
}
|
|
138
|
-
if (!shimPath && input.launcher.version) {
|
|
139
|
-
envEntries.push(["KANDEV_VERSION", input.launcher.version]);
|
|
140
|
-
}
|
|
141
|
-
if (input.serviceMetadataPath) {
|
|
142
|
-
envEntries.push(...serviceEnvEntries(input, "launchd"));
|
|
143
|
-
}
|
|
144
|
-
const envXml = envEntries
|
|
145
|
-
.map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
|
|
146
|
-
.join("\n");
|
|
147
|
-
const args = shimPath
|
|
148
|
-
? [shimPath, "--headless"]
|
|
149
|
-
: [input.launcher.nodePath, input.launcher.cliEntry, "--headless"];
|
|
150
|
-
const argsXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
151
|
-
// For system-mode LaunchDaemons, run as a specific user instead of root.
|
|
152
|
-
// For user agents this directive is omitted — the agent already runs as
|
|
153
|
-
// the loading user.
|
|
154
|
-
const userBlock = input.mode === "system" && input.systemUser
|
|
155
|
-
? ` <key>UserName</key>\n <string>${escapeXml(input.systemUser)}</string>\n`
|
|
156
|
-
: "";
|
|
157
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
158
|
-
${PLIST_MARKER}
|
|
159
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
160
|
-
<plist version="1.0">
|
|
161
|
-
<dict>
|
|
162
|
-
<key>Label</key>
|
|
163
|
-
<string>com.kdlbs.kandev</string>
|
|
164
|
-
<key>ProgramArguments</key>
|
|
165
|
-
<array>
|
|
166
|
-
${argsXml}
|
|
167
|
-
</array>
|
|
168
|
-
<key>EnvironmentVariables</key>
|
|
169
|
-
<dict>
|
|
170
|
-
${envXml}
|
|
171
|
-
</dict>
|
|
172
|
-
${userBlock} <key>RunAtLoad</key>
|
|
173
|
-
<true/>
|
|
174
|
-
<key>KeepAlive</key>
|
|
175
|
-
<true/>
|
|
176
|
-
<key>StandardOutPath</key>
|
|
177
|
-
<string>${escapeXml(`${input.logDir}/service.out`)}</string>
|
|
178
|
-
<key>StandardErrorPath</key>
|
|
179
|
-
<string>${escapeXml(`${input.logDir}/service.err`)}</string>
|
|
180
|
-
<key>WorkingDirectory</key>
|
|
181
|
-
<string>${escapeXml(input.homeDir)}</string>
|
|
182
|
-
</dict>
|
|
183
|
-
</plist>
|
|
184
|
-
`;
|
|
185
|
-
}
|
|
186
|
-
function escapeXml(value) {
|
|
187
|
-
return value
|
|
188
|
-
.replace(/&/g, "&")
|
|
189
|
-
.replace(/</g, "<")
|
|
190
|
-
.replace(/>/g, ">")
|
|
191
|
-
.replace(/"/g, """)
|
|
192
|
-
.replace(/'/g, "'");
|
|
193
|
-
}
|
|
194
|
-
// systemd unit "Environment=" lines can contain spaces by wrapping in double quotes.
|
|
195
|
-
// ExecStart needs special handling for spaces too — paths with spaces must be
|
|
196
|
-
// double-quoted per systemd.unit(5). Escape backslashes before double-quotes so
|
|
197
|
-
// a literal `\` in a path is preserved instead of merging with the following
|
|
198
|
-
// character into an escape sequence.
|
|
199
|
-
function quoteForUnit(value) {
|
|
200
|
-
if (!/[\s"\\]/.test(value))
|
|
201
|
-
return value;
|
|
202
|
-
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
203
|
-
return `"${escaped}"`;
|
|
204
|
-
}
|
|
205
|
-
// Render a systemd `Environment=KEY=value` line, quoting the whole assignment
|
|
206
|
-
// when the value contains whitespace, double-quote, or backslash. Without
|
|
207
|
-
// quoting, systemd splits the line on the first space and treats subsequent
|
|
208
|
-
// tokens as separate KEY=value pairs — which silently corrupts paths like
|
|
209
|
-
// `/home/john doe/.kandev`.
|
|
210
|
-
function envLine(key, value) {
|
|
211
|
-
if (!/[\s"\\]/.test(value))
|
|
212
|
-
return `Environment=${key}=${value}`;
|
|
213
|
-
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
214
|
-
return `Environment="${key}=${escaped}"`;
|
|
215
|
-
}
|
|
216
|
-
function serviceEnvLines(input, manager) {
|
|
217
|
-
return serviceEnvEntries(input, manager).map(([key, value]) => envLine(key, value));
|
|
218
|
-
}
|
|
219
|
-
function serviceEnvEntries(input, manager) {
|
|
220
|
-
if (!input.serviceMetadataPath)
|
|
221
|
-
return [];
|
|
222
|
-
return [
|
|
223
|
-
["KANDEV_RUNNING_AS_SERVICE", "true"],
|
|
224
|
-
["KANDEV_SERVICE_MODE", input.mode],
|
|
225
|
-
["KANDEV_SERVICE_MANAGER", manager],
|
|
226
|
-
["KANDEV_INSTALL_KIND", input.launcher.kind],
|
|
227
|
-
["KANDEV_SERVICE_METADATA", input.serviceMetadataPath],
|
|
228
|
-
];
|
|
229
|
-
}
|