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