kandev 0.65.0 → 0.69.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,165 +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.runLinuxService = runLinuxService;
7
- const node_child_process_1 = require("node:child_process");
8
- const node_fs_1 = __importDefault(require("node:fs"));
9
- const node_path_1 = __importDefault(require("node:path"));
10
- const health_check_1 = require("./health_check");
11
- const install_helpers_1 = require("./install_helpers");
12
- const metadata_1 = require("./metadata");
13
- const paths_1 = require("./paths");
14
- const templates_1 = require("./templates");
15
- function makeCtx(args) {
16
- const isSystem = !!args.system;
17
- const unitDir = isSystem ? paths_1.LINUX_SYSTEM_UNIT_DIR : (0, paths_1.linuxUserUnitDir)();
18
- const systemctlArgs = isSystem ? [] : ["--user"];
19
- return {
20
- args,
21
- systemctlArgs,
22
- unitPath: node_path_1.default.join(unitDir, `${paths_1.SERVICE_NAME}.service`),
23
- isSystem,
24
- };
25
- }
26
- async function runLinuxService(args) {
27
- if (!(0, install_helpers_1.commandExists)("systemctl")) {
28
- throw new Error("systemctl not found. Linux service install requires systemd.");
29
- }
30
- const ctx = makeCtx(args);
31
- switch (args.action) {
32
- case "install":
33
- return installAsync(ctx);
34
- case "uninstall":
35
- return uninstall(ctx);
36
- case "start":
37
- return runSystemctl(ctx, ["start", paths_1.SERVICE_NAME]);
38
- case "stop":
39
- return runSystemctl(ctx, ["stop", paths_1.SERVICE_NAME]);
40
- case "restart":
41
- return runSystemctl(ctx, ["restart", paths_1.SERVICE_NAME]);
42
- case "status":
43
- return runSystemctl(ctx, ["status", paths_1.SERVICE_NAME], { allowFailure: true });
44
- case "logs":
45
- return showLogs(ctx);
46
- case "config":
47
- // Handled by the dispatcher in index.ts before reaching the platform layer.
48
- throw new Error("unreachable: config action handled in service/index.ts");
49
- case "self-update":
50
- // Handled by the dispatcher in index.ts before reaching the platform layer.
51
- throw new Error("unreachable: self-update action handled in service/index.ts");
52
- default: {
53
- const _exhaustive = args.action;
54
- throw new Error(`unhandled service action: ${_exhaustive}`);
55
- }
56
- }
57
- }
58
- async function installAsync(ctx) {
59
- installSync(ctx);
60
- await (0, health_check_1.waitForServiceHealth)(ctx.args.port, () => (0, health_check_1.dumpJournalctlLogs)({ unit: paths_1.SERVICE_NAME, isSystem: ctx.isSystem, lines: 50 }));
61
- }
62
- function installSync(ctx) {
63
- const launcher = (0, paths_1.captureLauncher)();
64
- const homeDir = (0, paths_1.resolveHomeDir)(ctx.args.homeDir, ctx.isSystem);
65
- const logDir = (0, paths_1.resolveLogDir)(homeDir);
66
- const metadataPath = (0, metadata_1.serviceMetadataPath)(homeDir);
67
- const mode = ctx.isSystem ? "system" : "user";
68
- const systemUser = ctx.isSystem ? (0, paths_1.resolveServiceUser)(true) : undefined;
69
- const unit = (0, templates_1.renderSystemdUnit)({
70
- launcher,
71
- homeDir,
72
- logDir,
73
- port: ctx.args.port,
74
- systemUser,
75
- mode,
76
- serviceMetadataPath: metadataPath,
77
- });
78
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(ctx.unitPath), { recursive: true });
79
- const outcome = (0, install_helpers_1.writeUnitFile)(ctx.unitPath, unit);
80
- (0, metadata_1.writeServiceInstallMetadata)(metadataPath, (0, metadata_1.buildServiceInstallMetadata)({
81
- manager: "systemd",
82
- mode,
83
- launcher,
84
- homeDir,
85
- logDir,
86
- servicePath: ctx.unitPath,
87
- port: ctx.args.port,
88
- systemUser,
89
- }));
90
- runSystemctl(ctx, ["daemon-reload"]);
91
- // Always run enable --now so 'install' is fully idempotent: if the user
92
- // manually disabled or stopped the service, re-running install brings it
93
- // back online without changing the unit file.
94
- runSystemctl(ctx, ["enable", "--now", paths_1.SERVICE_NAME]);
95
- console.log(outcome === "unchanged"
96
- ? "[kandev] service is enabled and running"
97
- : "[kandev] service enabled and started");
98
- if (!ctx.isSystem && !ctx.args.noBootStart) {
99
- maybePromptLinger();
100
- }
101
- printPostInstallHints(ctx);
102
- }
103
- function uninstall(ctx) {
104
- // Disable and stop, ignoring failures since the unit may already be stopped.
105
- runSystemctl(ctx, ["disable", "--now", paths_1.SERVICE_NAME], { allowFailure: true });
106
- if (node_fs_1.default.existsSync(ctx.unitPath)) {
107
- node_fs_1.default.unlinkSync(ctx.unitPath);
108
- console.log(`[kandev] removed ${ctx.unitPath}`);
109
- }
110
- else {
111
- console.log(`[kandev] no unit file at ${ctx.unitPath}`);
112
- }
113
- runSystemctl(ctx, ["daemon-reload"], { allowFailure: true });
114
- }
115
- function showLogs(ctx) {
116
- const journalArgs = ctx.isSystem ? ["-u", paths_1.SERVICE_NAME] : ["--user-unit", paths_1.SERVICE_NAME];
117
- if (ctx.args.follow)
118
- journalArgs.push("-f");
119
- else
120
- journalArgs.push("-n", "200", "--no-pager");
121
- const res = (0, node_child_process_1.spawnSync)("journalctl", journalArgs, { stdio: "inherit" });
122
- if (res.status !== 0 && !ctx.args.follow) {
123
- throw new Error(`journalctl exited with code ${res.status}`);
124
- }
125
- }
126
- function runSystemctl(ctx, args, opts = {}) {
127
- const argv = [...ctx.systemctlArgs, ...args];
128
- const res = (0, node_child_process_1.spawnSync)("systemctl", argv, { stdio: "inherit" });
129
- if (res.status !== 0 && !opts.allowFailure) {
130
- throw new Error(`systemctl ${argv.join(" ")} failed with code ${res.status}`);
131
- }
132
- }
133
- function lingerEnabled(user) {
134
- try {
135
- const out = (0, node_child_process_1.execFileSync)("loginctl", ["show-user", user, "--property=Linger"], {
136
- encoding: "utf8",
137
- });
138
- return out.trim().toLowerCase() === "linger=yes";
139
- }
140
- catch {
141
- // loginctl may not be available or user record missing — assume off.
142
- return false;
143
- }
144
- }
145
- function maybePromptLinger() {
146
- const user = (0, paths_1.currentUsername)();
147
- if (lingerEnabled(user)) {
148
- console.log("[kandev] enable-linger already active — kandev will start at boot");
149
- return;
150
- }
151
- console.log("");
152
- console.log("[kandev] User services only run while you're logged in.");
153
- console.log("[kandev] To start kandev at boot, run:");
154
- console.log(`[kandev] sudo loginctl enable-linger ${user}`);
155
- console.log("[kandev] (Pass --no-boot-start to skip this notice next time.)");
156
- }
157
- function printPostInstallHints(ctx) {
158
- const ctl = ctx.isSystem ? "sudo systemctl" : "systemctl --user";
159
- const journal = ctx.isSystem ? "sudo journalctl" : "journalctl --user-unit";
160
- console.log("");
161
- console.log("[kandev] Useful commands:");
162
- console.log(`[kandev] ${ctl} status ${paths_1.SERVICE_NAME}`);
163
- console.log(`[kandev] ${ctl} restart ${paths_1.SERVICE_NAME}`);
164
- console.log(`[kandev] ${journal} ${ctx.isSystem ? "-u " : ""}${paths_1.SERVICE_NAME} -f`);
165
- }
@@ -1,221 +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.runMacosService = runMacosService;
7
- exports.readInstalledLogPaths = readInstalledLogPaths;
8
- const node_child_process_1 = require("node:child_process");
9
- const node_fs_1 = __importDefault(require("node:fs"));
10
- const node_os_1 = __importDefault(require("node:os"));
11
- const node_path_1 = __importDefault(require("node:path"));
12
- const health_check_1 = require("./health_check");
13
- const install_helpers_1 = require("./install_helpers");
14
- const launchctl_1 = require("./launchctl");
15
- const metadata_1 = require("./metadata");
16
- const paths_1 = require("./paths");
17
- const templates_1 = require("./templates");
18
- function makeCtx(args) {
19
- const isSystem = !!args.system;
20
- const dir = isSystem ? paths_1.MACOS_SYSTEM_DAEMON_DIR : (0, paths_1.macosUserAgentDir)();
21
- const uid = node_os_1.default.userInfo().uid;
22
- const domain = isSystem ? "system" : `gui/${uid}`;
23
- return {
24
- args,
25
- plistPath: node_path_1.default.join(dir, `${paths_1.LAUNCHD_LABEL}.plist`),
26
- isSystem,
27
- domain,
28
- target: `${domain}/${paths_1.LAUNCHD_LABEL}`,
29
- };
30
- }
31
- async function runMacosService(args) {
32
- if (!(0, install_helpers_1.commandExists)("launchctl")) {
33
- throw new Error("launchctl not found. macOS service install requires launchd.");
34
- }
35
- const ctx = makeCtx(args);
36
- switch (args.action) {
37
- case "install":
38
- return installAsync(ctx);
39
- case "uninstall":
40
- return uninstall(ctx);
41
- case "start":
42
- return startService(ctx);
43
- case "stop":
44
- return stopService(ctx);
45
- case "restart":
46
- return restartService(ctx);
47
- case "status":
48
- return showStatus(ctx);
49
- case "logs":
50
- return showLogs(ctx);
51
- case "config":
52
- // Handled by the dispatcher in index.ts before reaching the platform layer.
53
- throw new Error("unreachable: config action handled in service/index.ts");
54
- case "self-update":
55
- // Handled by the dispatcher in index.ts before reaching the platform layer.
56
- throw new Error("unreachable: self-update action handled in service/index.ts");
57
- default: {
58
- const _exhaustive = args.action;
59
- throw new Error(`unhandled service action: ${_exhaustive}`);
60
- }
61
- }
62
- }
63
- async function installAsync(ctx) {
64
- const { logDir } = installSync(ctx);
65
- await (0, health_check_1.waitForServiceHealth)(ctx.args.port, () => (0, health_check_1.dumpLaunchdLogs)({ logDir, lines: 50 }));
66
- }
67
- function installSync(ctx) {
68
- const launcher = (0, paths_1.captureLauncher)();
69
- const homeDir = (0, paths_1.resolveHomeDir)(ctx.args.homeDir, ctx.isSystem);
70
- const logDir = (0, paths_1.resolveLogDir)(homeDir);
71
- const metadataPath = (0, metadata_1.serviceMetadataPath)(homeDir);
72
- const mode = ctx.isSystem ? "system" : "user";
73
- const systemUser = ctx.isSystem ? (0, paths_1.resolveServiceUser)(true) : undefined;
74
- node_fs_1.default.mkdirSync(logDir, { recursive: true });
75
- const plist = (0, templates_1.renderLaunchdPlist)({
76
- launcher,
77
- homeDir,
78
- logDir,
79
- port: ctx.args.port,
80
- systemUser,
81
- mode,
82
- serviceMetadataPath: metadataPath,
83
- });
84
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(ctx.plistPath), { recursive: true });
85
- const outcome = (0, install_helpers_1.writeUnitFile)(ctx.plistPath, plist);
86
- (0, metadata_1.writeServiceInstallMetadata)(metadataPath, (0, metadata_1.buildServiceInstallMetadata)({
87
- manager: "launchd",
88
- mode,
89
- launcher,
90
- homeDir,
91
- logDir,
92
- servicePath: ctx.plistPath,
93
- port: ctx.args.port,
94
- systemUser,
95
- }));
96
- // launchctl bootstrap fails if the label is already loaded — bootout first
97
- // (ignoring its error if nothing was loaded). This means 'install' is
98
- // idempotent: re-running it reloads the unit even if the file is unchanged,
99
- // which is how we recover from a user manually unloading the service.
100
- //
101
- // `reloadService` waits for bootout's async teardown before bootstrapping and
102
- // retries the transient EIO ("Bootstrap failed: 5") that launchd returns while
103
- // a still-running instance is torn down — the failure that broke self-update,
104
- // where this install runs against a live service. See launchctl.ts.
105
- (0, launchctl_1.reloadService)(ctx.target, ctx.domain, ctx.plistPath);
106
- runLaunchctl(["enable", ctx.target], { allowFailure: true });
107
- console.log(outcome === "unchanged"
108
- ? "[kandev] service is loaded and running"
109
- : "[kandev] service loaded and started");
110
- printPostInstallHints(ctx, logDir);
111
- return { logDir };
112
- }
113
- function uninstall(ctx) {
114
- runLaunchctl(["bootout", ctx.target], { allowFailure: true });
115
- if (node_fs_1.default.existsSync(ctx.plistPath)) {
116
- node_fs_1.default.unlinkSync(ctx.plistPath);
117
- console.log(`[kandev] removed ${ctx.plistPath}`);
118
- }
119
- else {
120
- console.log(`[kandev] no plist at ${ctx.plistPath}`);
121
- }
122
- }
123
- // `bootstrap` loads the job (start) and `bootout` fully unloads it (stop).
124
- // We use these instead of `kickstart` + `kill` because the plist sets
125
- // `KeepAlive=true` — under KeepAlive, `kill SIGTERM` does not stop the
126
- // service: launchd just respawns it seconds later. Only `bootout` removes
127
- // the job from launchd's supervision.
128
- //
129
- // start/restart both have to handle two pre-states — job loaded vs not —
130
- // so each begins with a bootout-then-bootstrap dance similar to installSync.
131
- function startService(ctx) {
132
- // Idempotent: if the label is already loaded, bootstrap would fail. Bootout
133
- // first (waiting for teardown) so start works whether the service was
134
- // previously running or stopped.
135
- (0, launchctl_1.reloadService)(ctx.target, ctx.domain, ctx.plistPath);
136
- }
137
- function stopService(ctx) {
138
- runLaunchctl(["bootout", ctx.target], { allowFailure: true });
139
- }
140
- // `kickstart -k` atomically kills and restarts a loaded service. If the job
141
- // was previously stopped (bootout'd), the target no longer exists in the
142
- // launchd domain and kickstart fails — fall back to bootstrap to reload it.
143
- function restartService(ctx) {
144
- const res = (0, node_child_process_1.spawnSync)("launchctl", ["kickstart", "-k", ctx.target], { stdio: "inherit" });
145
- if (res.status === 0)
146
- return;
147
- // kickstart fails when the job isn't loaded — reload it (waiting for any
148
- // residual teardown and retrying the transient bootstrap EIO).
149
- (0, launchctl_1.reloadService)(ctx.target, ctx.domain, ctx.plistPath);
150
- }
151
- function showStatus(ctx) {
152
- const res = (0, node_child_process_1.spawnSync)("launchctl", ["print", ctx.target], {
153
- stdio: "inherit",
154
- });
155
- if (res.status !== 0) {
156
- console.log(`[kandev] service not loaded in ${ctx.domain}`);
157
- }
158
- }
159
- function showLogs(ctx) {
160
- // Pull the log paths from the *installed* plist rather than recomputing
161
- // from defaults — `--home-dir` is install-only, so a user who installed
162
- // with a custom home dir would otherwise see "no logs yet" at the wrong
163
- // location while logs accumulate at the real path.
164
- const installed = readInstalledLogPaths(ctx.plistPath);
165
- const homeDir = (0, paths_1.resolveHomeDir)(ctx.args.homeDir, ctx.isSystem);
166
- const fallbackDir = (0, paths_1.resolveLogDir)(homeDir);
167
- const outPath = installed?.out ?? node_path_1.default.join(fallbackDir, "service.out");
168
- const errPath = installed?.err ?? node_path_1.default.join(fallbackDir, "service.err");
169
- const tailArgs = ctx.args.follow ? ["-f", "-n", "200"] : ["-n", "200"];
170
- const targets = [outPath, errPath].filter((p) => node_fs_1.default.existsSync(p));
171
- if (targets.length === 0) {
172
- const checkedDir = installed ? node_path_1.default.dirname(installed.err) : fallbackDir;
173
- console.log(`[kandev] no logs yet at ${checkedDir}`);
174
- return;
175
- }
176
- (0, node_child_process_1.spawnSync)("tail", [...tailArgs, ...targets], { stdio: "inherit" });
177
- }
178
- /**
179
- * Pull the literal StandardOutPath / StandardErrorPath values out of an
180
- * installed plist. Plist XML is rigidly formatted by our renderer, so a
181
- * regex match is enough — avoids pulling in a plist parser for two strings.
182
- * Returns null when the plist is missing or doesn't contain the keys.
183
- */
184
- function readInstalledLogPaths(plistPath) {
185
- let content;
186
- try {
187
- content = node_fs_1.default.readFileSync(plistPath, "utf8");
188
- }
189
- catch {
190
- return null;
191
- }
192
- const outMatch = /<key>StandardOutPath<\/key>\s*<string>([^<]+)<\/string>/.exec(content);
193
- const errMatch = /<key>StandardErrorPath<\/key>\s*<string>([^<]+)<\/string>/.exec(content);
194
- if (!outMatch || !errMatch)
195
- return null;
196
- // `renderLaunchdPlist` runs values through `escapeXml`, so a path containing
197
- // `&`, `<`, etc. is stored escaped in the plist. Decode before returning so
198
- // the caller can stat/tail the actual file on disk.
199
- return { out: unescapeXml(outMatch[1]), err: unescapeXml(errMatch[1]) };
200
- }
201
- function unescapeXml(value) {
202
- return value
203
- .replace(/&lt;/g, "<")
204
- .replace(/&gt;/g, ">")
205
- .replace(/&quot;/g, '"')
206
- .replace(/&apos;/g, "'")
207
- .replace(/&amp;/g, "&"); // last — must not re-decode a `&amp;amp;`-style sequence
208
- }
209
- function runLaunchctl(args, opts = {}) {
210
- const res = (0, node_child_process_1.spawnSync)("launchctl", args, { stdio: "inherit" });
211
- if (res.status !== 0 && !opts.allowFailure) {
212
- throw new Error(`launchctl ${args.join(" ")} failed with code ${res.status}`);
213
- }
214
- }
215
- function printPostInstallHints(ctx, logDir) {
216
- console.log("");
217
- console.log("[kandev] Useful commands:");
218
- console.log(`[kandev] launchctl print ${ctx.target}`);
219
- console.log(`[kandev] kandev service restart${ctx.isSystem ? " --system" : ""}`);
220
- console.log(`[kandev] tail -f ${node_path_1.default.join(logDir, "service.err")}`);
221
- }
@@ -1,46 +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.serviceMetadataPath = serviceMetadataPath;
7
- exports.buildServiceInstallMetadata = buildServiceInstallMetadata;
8
- exports.writeServiceInstallMetadata = writeServiceInstallMetadata;
9
- const node_fs_1 = __importDefault(require("node:fs"));
10
- const node_path_1 = __importDefault(require("node:path"));
11
- function serviceMetadataPath(homeDir) {
12
- return node_path_1.default.join(homeDir, "service", "install.json");
13
- }
14
- function buildServiceInstallMetadata(input) {
15
- const out = {
16
- version: 1,
17
- manager: input.manager,
18
- mode: input.mode,
19
- kind: input.launcher.kind,
20
- home_dir: input.homeDir,
21
- log_dir: input.logDir,
22
- service_path: input.servicePath,
23
- node_path: input.launcher.nodePath,
24
- cli_entry: input.launcher.cliEntry,
25
- installed_at: (input.now ?? new Date()).toISOString(),
26
- };
27
- if (input.launcher.bundleDir)
28
- out.bundle_dir = input.launcher.bundleDir;
29
- if (input.launcher.version)
30
- out.launcher_version = input.launcher.version;
31
- if (input.port !== undefined)
32
- out.port = input.port;
33
- if (input.systemUser)
34
- out.system_user = input.systemUser;
35
- return out;
36
- }
37
- function writeServiceInstallMetadata(metadataPath, metadata) {
38
- const dir = node_path_1.default.dirname(metadataPath);
39
- // `mode` on mkdir/writeFile only applies when the path is created. chmod after
40
- // so a pre-existing service dir / install.json is tightened to owner-only too
41
- // (it can hold launcher paths and the metadata that gates self-update).
42
- node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
43
- node_fs_1.default.chmodSync(dir, 0o700);
44
- node_fs_1.default.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, { mode: 0o600 });
45
- node_fs_1.default.chmodSync(metadataPath, 0o600);
46
- }
@@ -1,168 +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.MACOS_SYSTEM_DAEMON_DIR = exports.LINUX_SYSTEM_UNIT_DIR = exports.LAUNCHD_LABEL = exports.SERVICE_NAME = void 0;
7
- exports.linuxUserUnitDir = linuxUserUnitDir;
8
- exports.linuxUserUnitPath = linuxUserUnitPath;
9
- exports.linuxSystemUnitPath = linuxSystemUnitPath;
10
- exports.macosUserAgentDir = macosUserAgentDir;
11
- exports.macosUserPlistPath = macosUserPlistPath;
12
- exports.macosSystemPlistPath = macosSystemPlistPath;
13
- exports.homebrewShimPath = homebrewShimPath;
14
- exports.captureLauncher = captureLauncher;
15
- exports.resolveHomeDir = resolveHomeDir;
16
- exports.resolveLogDir = resolveLogDir;
17
- exports.currentUsername = currentUsername;
18
- exports.resolveServiceUser = resolveServiceUser;
19
- const node_fs_1 = __importDefault(require("node:fs"));
20
- const node_os_1 = __importDefault(require("node:os"));
21
- const node_path_1 = __importDefault(require("node:path"));
22
- const constants_1 = require("../constants");
23
- /** Service unit/plist locations. Single source of truth for where kandev */
24
- /** writes/reads its unit files — linux.ts, macos.ts, config.ts, stale_check.ts */
25
- /** all consume these. Exposed as functions (not eager constants) so tests can */
26
- /** mock `os.homedir()` between cases. */
27
- exports.SERVICE_NAME = "kandev";
28
- exports.LAUNCHD_LABEL = "com.kdlbs.kandev";
29
- exports.LINUX_SYSTEM_UNIT_DIR = "/etc/systemd/system";
30
- exports.MACOS_SYSTEM_DAEMON_DIR = "/Library/LaunchDaemons";
31
- function linuxUserUnitDir() {
32
- return node_path_1.default.join(node_os_1.default.homedir(), ".config", "systemd", "user");
33
- }
34
- function linuxUserUnitPath() {
35
- return node_path_1.default.join(linuxUserUnitDir(), `${exports.SERVICE_NAME}.service`);
36
- }
37
- function linuxSystemUnitPath() {
38
- return node_path_1.default.join(exports.LINUX_SYSTEM_UNIT_DIR, `${exports.SERVICE_NAME}.service`);
39
- }
40
- function macosUserAgentDir() {
41
- return node_path_1.default.join(node_os_1.default.homedir(), "Library", "LaunchAgents");
42
- }
43
- function macosUserPlistPath() {
44
- return node_path_1.default.join(macosUserAgentDir(), `${exports.LAUNCHD_LABEL}.plist`);
45
- }
46
- function macosSystemPlistPath() {
47
- return node_path_1.default.join(exports.MACOS_SYSTEM_DAEMON_DIR, `${exports.LAUNCHD_LABEL}.plist`);
48
- }
49
- // Homebrew is POSIX-only, so the Cellar segment is a hardcoded "/Cellar/"
50
- // rather than `path.sep`-based. On a Windows CI runner `path.sep` would be
51
- // "\\", which would never match a POSIX Cellar path and silently break shim
52
- // derivation (and its tests).
53
- const HOMEBREW_CELLAR_SEGMENT = "/Cellar/";
54
- /**
55
- * Derive the floating Homebrew launcher shim from a Cellar-installed cli.js path.
56
- *
57
- * Homebrew installs the CLI under `<prefix>/Cellar/kandev/<version>/...` and
58
- * symlinks a version-independent shim at `<prefix>/bin/kandev`. That shim sets
59
- * KANDEV_BUNDLE_DIR / KANDEV_VERSION itself and execs cli.js via the floating
60
- * `opt/node` symlink, so it keeps working after `brew upgrade` deletes the old
61
- * Cellar dir. Returns undefined when `cliEntry` isn't a Cellar layout (npm /
62
- * unknown installs), so callers fall back to the version-pinned paths.
63
- */
64
- function homebrewShimPath(cliEntry) {
65
- const idx = cliEntry.indexOf(HOMEBREW_CELLAR_SEGMENT);
66
- if (idx === -1)
67
- return undefined;
68
- const prefix = cliEntry.slice(0, idx);
69
- // Homebrew layout is POSIX; use path.posix.join so the result keeps forward
70
- // slashes regardless of the host the install/tests run on.
71
- return node_path_1.default.posix.join(prefix, "bin", exports.SERVICE_NAME);
72
- }
73
- /**
74
- * Snapshot the current invocation so the service unit can faithfully reproduce it.
75
- *
76
- * The unit file hard-codes absolute paths because systemd/launchd start with an
77
- * empty PATH and may not see the user's `node` or `kandev` shim. By recording
78
- * `process.execPath` (node) and the resolved CLI entry at install time we avoid
79
- * any PATH lookups at service-run time.
80
- */
81
- function captureLauncher() {
82
- const nodePath = process.execPath;
83
- const cliEntry = resolveCliEntry();
84
- const bundleDir = process.env.KANDEV_BUNDLE_DIR;
85
- const version = process.env.KANDEV_VERSION;
86
- const kind = bundleDir
87
- ? homebrewShimPath(cliEntry)
88
- ? "homebrew"
89
- : "local"
90
- : looksLikeNpxEntry(cliEntry)
91
- ? "npx"
92
- : cliEntry.includes(`${node_path_1.default.sep}node_modules${node_path_1.default.sep}`)
93
- ? "npm"
94
- : "unknown";
95
- // For Homebrew installs, prefer the floating bin shim so the unit survives
96
- // `brew upgrade` (which deletes the versioned Cellar dir baked into nodePath
97
- // /cliEntry). Only adopt it when the shim actually exists on disk; otherwise
98
- // fall back to the version-pinned paths below.
99
- let shimPath;
100
- if (kind === "homebrew") {
101
- const candidate = homebrewShimPath(cliEntry);
102
- if (candidate && node_fs_1.default.existsSync(candidate))
103
- shimPath = candidate;
104
- }
105
- return { nodePath, cliEntry, kind, bundleDir, version, shimPath };
106
- }
107
- function looksLikeNpxEntry(cliEntry) {
108
- return cliEntry.includes(`${node_path_1.default.sep}_npx${node_path_1.default.sep}`);
109
- }
110
- function resolveCliEntry() {
111
- const argvEntry = process.argv[1];
112
- if (argvEntry && node_fs_1.default.existsSync(argvEntry)) {
113
- return node_path_1.default.resolve(node_fs_1.default.realpathSync(argvEntry));
114
- }
115
- throw new Error("could not resolve the kandev CLI entry path from process.argv[1]; " +
116
- "rerun via the kandev binary");
117
- }
118
- /** Resolve the home directory used for the unit's KANDEV_HOME_DIR env. */
119
- function resolveHomeDir(override, runAsRoot) {
120
- if (override) {
121
- // Node's path.resolve doesn't expand `~`; users often type `--home-dir ~/foo`
122
- // (especially via shell escapes that defer expansion), so do it ourselves.
123
- const expanded = override === "~" || override.startsWith(`~${node_path_1.default.sep}`)
124
- ? node_path_1.default.join(node_os_1.default.homedir(), override.slice(1))
125
- : override;
126
- return node_path_1.default.resolve(expanded);
127
- }
128
- if (runAsRoot) {
129
- // System units default to /var/lib/kandev so root-owned data lives outside any
130
- // single user's $HOME (where it would be unreachable to other users).
131
- return "/var/lib/kandev";
132
- }
133
- return constants_1.KANDEV_HOME_DIR;
134
- }
135
- /** Absolute path to the log directory used by the unit for stdout/stderr. */
136
- function resolveLogDir(homeDir) {
137
- return node_path_1.default.join(homeDir, "logs");
138
- }
139
- /** Current username (the EUID the CLI is running as). */
140
- function currentUsername() {
141
- return node_os_1.default.userInfo().username;
142
- }
143
- /**
144
- * Resolve which user the service should run as.
145
- *
146
- * For user-mode installs this is always the current user (matters for hints
147
- * printed back to the user, not for the unit itself — user units don't set
148
- * `User=`).
149
- *
150
- * For system-mode installs the CLI is typically invoked via sudo, which makes
151
- * `os.userInfo().username` resolve to `root`. We prefer `SUDO_USER` so the
152
- * daemon runs as the human who installed it (with access to their `~/.kandev`,
153
- * git config, agent CLI credentials, etc) rather than as root.
154
- *
155
- * If the user genuinely wants a root-owned daemon they can run sudo with
156
- * `-E` stripped or pass `--run-as root` (future flag) — but the common case
157
- * (`sudo kandev service install --system`) gets the safe default.
158
- */
159
- function resolveServiceUser(isSystem) {
160
- if (!isSystem) {
161
- return currentUsername();
162
- }
163
- const sudoUser = process.env.SUDO_USER;
164
- if (sudoUser && sudoUser !== "root") {
165
- return sudoUser;
166
- }
167
- return currentUsername();
168
- }