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.
- 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
package/dist/service/linux.js
DELETED
|
@@ -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
|
-
}
|
package/dist/service/macos.js
DELETED
|
@@ -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(/</g, "<")
|
|
204
|
-
.replace(/>/g, ">")
|
|
205
|
-
.replace(/"/g, '"')
|
|
206
|
-
.replace(/'/g, "'")
|
|
207
|
-
.replace(/&/g, "&"); // last — must not re-decode a `&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
|
-
}
|
package/dist/service/metadata.js
DELETED
|
@@ -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
|
-
}
|
package/dist/service/paths.js
DELETED
|
@@ -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
|
-
}
|