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.
@@ -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, "&amp;")
189
- .replace(/</g, "&lt;")
190
- .replace(/>/g, "&gt;")
191
- .replace(/"/g, "&quot;")
192
- .replace(/'/g, "&apos;");
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
- }