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,131 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseServiceArgs = parseServiceArgs;
4
- const args_1 = require("../args");
5
- const VALID_ACTIONS = new Set([
6
- "install",
7
- "uninstall",
8
- "start",
9
- "stop",
10
- "restart",
11
- "status",
12
- "logs",
13
- "config",
14
- "self-update",
15
- ]);
16
- function parseServiceArgs(argv) {
17
- if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
18
- return { action: "install", showHelp: true };
19
- }
20
- const head = argv[0];
21
- if (!VALID_ACTIONS.has(head)) {
22
- throw new args_1.ParseError(`unknown service action "${head}". expected one of: ${[...VALID_ACTIONS].join(", ")}`);
23
- }
24
- const out = { action: head };
25
- for (let i = 1; i < argv.length; i += 1) {
26
- const arg = argv[i];
27
- if (arg === "--help" || arg === "-h") {
28
- out.showHelp = true;
29
- continue;
30
- }
31
- if (arg === "--system") {
32
- out.system = true;
33
- continue;
34
- }
35
- if (arg === "--no-boot-start") {
36
- out.noBootStart = true;
37
- continue;
38
- }
39
- if (arg === "-f" || arg === "--follow") {
40
- out.follow = true;
41
- continue;
42
- }
43
- if (arg === "--dry-run") {
44
- out.dryRun = true;
45
- continue;
46
- }
47
- if (arg === "--intent") {
48
- out.intent = takeValue(argv, i, "--intent");
49
- i += 1;
50
- continue;
51
- }
52
- if (arg.startsWith("--intent=")) {
53
- const value = arg.slice("--intent=".length);
54
- if (value.length === 0)
55
- throw new args_1.ParseError("--intent requires a value");
56
- out.intent = value;
57
- continue;
58
- }
59
- if (arg === "--port") {
60
- out.port = parsePort(takeValue(argv, i, "--port"), "--port");
61
- i += 1;
62
- continue;
63
- }
64
- if (arg.startsWith("--port=")) {
65
- out.port = parsePort(arg.slice("--port=".length), "--port");
66
- continue;
67
- }
68
- if (arg === "--home-dir") {
69
- out.homeDir = takeValue(argv, i, "--home-dir");
70
- i += 1;
71
- continue;
72
- }
73
- if (arg.startsWith("--home-dir=")) {
74
- const value = arg.slice("--home-dir=".length);
75
- if (value.length === 0)
76
- throw new args_1.ParseError("--home-dir requires a value");
77
- out.homeDir = value;
78
- continue;
79
- }
80
- throw new args_1.ParseError(`unknown flag "${arg}" for kandev service ${head}`);
81
- }
82
- validateActionFlags(out);
83
- return out;
84
- }
85
- /**
86
- * Reject flag combinations that silently no-op so the user gets immediate
87
- * feedback instead of a successful command that ignored their input. The
88
- * matrix is small enough that explicit checks beat a generic flag-applicability
89
- * table.
90
- */
91
- function validateActionFlags(args) {
92
- if (args.follow && args.action !== "logs") {
93
- throw new args_1.ParseError(`--follow only applies to 'kandev service logs', not '${args.action}'`);
94
- }
95
- if (args.dryRun && args.action !== "self-update") {
96
- throw new args_1.ParseError(`--dry-run only applies to 'kandev service self-update', not '${args.action}'`);
97
- }
98
- if (args.intent && args.action !== "self-update") {
99
- throw new args_1.ParseError(`--intent only applies to 'kandev service self-update', not '${args.action}'`);
100
- }
101
- if (args.action === "self-update" && !args.showHelp && !args.intent) {
102
- throw new args_1.ParseError("kandev service self-update requires --intent <path>");
103
- }
104
- const installOnly = ["port", "homeDir", "noBootStart"];
105
- if (args.action !== "install") {
106
- for (const flag of installOnly) {
107
- if (args[flag] !== undefined) {
108
- const display = flag === "homeDir"
109
- ? "--home-dir"
110
- : flag === "noBootStart"
111
- ? "--no-boot-start"
112
- : `--${flag}`;
113
- throw new args_1.ParseError(`${display} only applies to 'kandev service install', not '${args.action}'`);
114
- }
115
- }
116
- }
117
- }
118
- function takeValue(argv, i, flag) {
119
- const v = argv[i + 1];
120
- if (v === undefined || v.startsWith("-")) {
121
- throw new args_1.ParseError(`${flag} requires a value`);
122
- }
123
- return v;
124
- }
125
- function parsePort(raw, flag) {
126
- const n = Number(raw);
127
- if (raw === "" || !Number.isInteger(n) || n < 1 || n > 65535) {
128
- throw new args_1.ParseError(`${flag} value must be an integer between 1 and 65535, got "${raw}"`);
129
- }
130
- return n;
131
- }
@@ -1,111 +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.printServiceConfig = printServiceConfig;
7
- const node_child_process_1 = require("node:child_process");
8
- const node_fs_1 = __importDefault(require("node:fs"));
9
- const node_os_1 = __importDefault(require("node:os"));
10
- const constants_1 = require("../constants");
11
- const paths_1 = require("./paths");
12
- const templates_1 = require("./templates");
13
- /**
14
- * Print a human-readable summary of what kandev knows about the local
15
- * service install: paths, env vars, whether a unit exists, whether it's
16
- * active. Used for "why isn't it running?" diagnosis without needing to
17
- * remember the right systemctl / launchctl invocation.
18
- */
19
- function printServiceConfig(args) {
20
- const isSystem = !!args.system;
21
- const launcher = (0, paths_1.captureLauncher)();
22
- const homeDir = (0, paths_1.resolveHomeDir)(args.homeDir, isSystem);
23
- const logDir = (0, paths_1.resolveLogDir)(homeDir);
24
- console.log("=== kandev service config ===");
25
- console.log(`platform: ${process.platform}`);
26
- console.log(`mode: ${isSystem ? "system" : "user"}`);
27
- console.log(`launcher kind: ${launcher.kind}`);
28
- console.log(`node path: ${launcher.nodePath}`);
29
- console.log(`cli entry: ${launcher.cliEntry}`);
30
- if (launcher.bundleDir)
31
- console.log(`bundle dir: ${launcher.bundleDir}`);
32
- if (launcher.version)
33
- console.log(`version: ${launcher.version}`);
34
- console.log("");
35
- console.log(`KANDEV_HOME_DIR: ${homeDir}`);
36
- console.log(`log dir: ${logDir}`);
37
- console.log(`port: ${args.port ?? `(default ${constants_1.DEFAULT_BACKEND_PORT})`}`);
38
- if (isSystem) {
39
- console.log(`run as user: ${(0, paths_1.resolveServiceUser)(true)}`);
40
- }
41
- console.log("");
42
- if (process.platform === "linux") {
43
- printLinuxUnit(isSystem);
44
- }
45
- else if (process.platform === "darwin") {
46
- printMacosUnit(isSystem);
47
- }
48
- else {
49
- console.log(`unit: (unsupported on ${process.platform})`);
50
- }
51
- }
52
- function printLinuxUnit(isSystem) {
53
- const unitPath = isSystem ? (0, paths_1.linuxSystemUnitPath)() : (0, paths_1.linuxUserUnitPath)();
54
- console.log(`unit path: ${unitPath}`);
55
- const present = node_fs_1.default.existsSync(unitPath);
56
- console.log(`installed: ${present ? "yes" : "no"}`);
57
- if (present) {
58
- const content = safeRead(unitPath);
59
- console.log(`managed by us: ${content && (0, templates_1.looksLikeManagedUnit)(content) ? "yes" : "no"}`);
60
- }
61
- const active = systemctlIsActive(isSystem);
62
- if (active !== null)
63
- console.log(`active state: ${active}`);
64
- }
65
- function printMacosUnit(isSystem) {
66
- const plistPath = isSystem ? (0, paths_1.macosSystemPlistPath)() : (0, paths_1.macosUserPlistPath)();
67
- console.log(`plist path: ${plistPath}`);
68
- const present = node_fs_1.default.existsSync(plistPath);
69
- console.log(`installed: ${present ? "yes" : "no"}`);
70
- if (present) {
71
- const content = safeRead(plistPath);
72
- console.log(`managed by us: ${content && (0, templates_1.looksLikeManagedUnit)(content) ? "yes" : "no"}`);
73
- }
74
- console.log(`loaded: ${launchctlIsLoaded(isSystem) ? "yes" : "no"}`);
75
- }
76
- function safeRead(p) {
77
- try {
78
- return node_fs_1.default.readFileSync(p, "utf8");
79
- }
80
- catch {
81
- return null;
82
- }
83
- }
84
- function systemctlIsActive(isSystem) {
85
- try {
86
- const args = isSystem ? ["is-active", paths_1.SERVICE_NAME] : ["--user", "is-active", paths_1.SERVICE_NAME];
87
- const out = (0, node_child_process_1.execFileSync)("systemctl", args, { encoding: "utf8" });
88
- return out.trim();
89
- }
90
- catch (err) {
91
- // is-active returns nonzero for inactive/failed/unknown — read the output anyway.
92
- // execFileSync attaches stdout to the thrown error; guard with a type check
93
- // instead of an unsafe cast so this keeps working if the error shape changes.
94
- if (err !== null && typeof err === "object" && "stdout" in err) {
95
- const { stdout } = err;
96
- if (stdout)
97
- return String(stdout).trim();
98
- }
99
- return null;
100
- }
101
- }
102
- function launchctlIsLoaded(isSystem) {
103
- try {
104
- const domain = isSystem ? "system" : `gui/${node_os_1.default.userInfo().uid}`;
105
- (0, node_child_process_1.execFileSync)("launchctl", ["print", `${domain}/${paths_1.LAUNCHD_LABEL}`], { stdio: "ignore" });
106
- return true;
107
- }
108
- catch {
109
- return false;
110
- }
111
- }
@@ -1,84 +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.waitForServiceHealth = waitForServiceHealth;
7
- exports.dumpJournalctlLogs = dumpJournalctlLogs;
8
- exports.dumpLaunchdLogs = dumpLaunchdLogs;
9
- const node_child_process_1 = require("node:child_process");
10
- const node_fs_1 = __importDefault(require("node:fs"));
11
- const node_path_1 = __importDefault(require("node:path"));
12
- const constants_1 = require("../constants");
13
- const health_1 = require("../health");
14
- const DEFAULT_TIMEOUT_MS = 30000;
15
- const POLL_INTERVAL_MS = 500;
16
- // Per-request timeout. Without this, undici's default headersTimeout (5min)
17
- // can stall a single fetch — e.g. TCP accepted but the backend hangs before
18
- // writing response headers — and silently overrun the outer deadline.
19
- const REQUEST_TIMEOUT_MS = 2000;
20
- /**
21
- * Poll the kandev /health endpoint to confirm the freshly-installed service
22
- * actually came up. On success, the user gets immediate confirmation; on
23
- * failure, we dump the tail of the service logs so the diagnosis isn't a
24
- * scavenger hunt across `journalctl` / `tail`.
25
- *
26
- * Timeout is fixed at 30s — long enough to absorb a slow first launch
27
- * (binary unpacking, sqlite migration), short enough to fail fast when the
28
- * unit is genuinely broken.
29
- */
30
- async function waitForServiceHealth(port, dumpLogs) {
31
- const url = `http://localhost:${port ?? constants_1.DEFAULT_BACKEND_PORT}/health`;
32
- const timeoutMs = (0, health_1.resolveHealthTimeoutMs)(DEFAULT_TIMEOUT_MS);
33
- const deadline = Date.now() + timeoutMs;
34
- process.stderr.write(`[kandev] waiting for service to be healthy at ${url}\n`);
35
- while (Date.now() < deadline) {
36
- try {
37
- const res = await fetch(url, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
38
- if (res.ok) {
39
- process.stderr.write(`[kandev] service is healthy\n`);
40
- return;
41
- }
42
- }
43
- catch {
44
- // not up yet (or per-request timeout fired); keep polling
45
- }
46
- await (0, health_1.delay)(POLL_INTERVAL_MS);
47
- }
48
- process.stderr.write(`[kandev] service did not become healthy within ${timeoutMs}ms\n`);
49
- process.stderr.write(`[kandev] dumping recent service logs:\n`);
50
- dumpLogs();
51
- throw new Error("kandev service was installed but the /health endpoint never responded. " +
52
- "Inspect the logs above and re-run 'kandev service install' once fixed. " +
53
- "If the service needs more time to come up, set KANDEV_HEALTH_TIMEOUT_MS=120000.");
54
- }
55
- /** Dump the last N lines of a systemd unit's logs via journalctl. */
56
- function dumpJournalctlLogs(opts) {
57
- const args = opts.isSystem
58
- ? ["-u", opts.unit, "-n", String(opts.lines), "--no-pager"]
59
- : ["--user-unit", opts.unit, "-n", String(opts.lines), "--no-pager"];
60
- try {
61
- (0, node_child_process_1.spawnSync)("journalctl", args, { stdio: "inherit" });
62
- }
63
- catch (err) {
64
- // journalctl may be unavailable in containerized or stripped-down setups.
65
- // We're already in a failure path; don't compound it with a thrown error.
66
- process.stderr.write(`[kandev] (could not run journalctl: ${err instanceof Error ? err.message : String(err)})\n`);
67
- }
68
- }
69
- /** Dump the last N lines of launchd-managed log files via `tail`. */
70
- function dumpLaunchdLogs(opts) {
71
- const candidates = ["service.err", "service.out"]
72
- .map((name) => node_path_1.default.join(opts.logDir, name))
73
- .filter((p) => node_fs_1.default.existsSync(p));
74
- if (candidates.length === 0) {
75
- process.stderr.write(`[kandev] (no logs found in ${opts.logDir})\n`);
76
- return;
77
- }
78
- try {
79
- (0, node_child_process_1.spawnSync)("tail", ["-n", String(opts.lines), ...candidates], { stdio: "inherit" });
80
- }
81
- catch (err) {
82
- process.stderr.write(`[kandev] (could not run tail: ${err instanceof Error ? err.message : String(err)})\n`);
83
- }
84
- }
@@ -1,70 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.printServiceHelp = printServiceHelp;
4
- exports.runServiceCommand = runServiceCommand;
5
- const args_1 = require("./args");
6
- const config_1 = require("./config");
7
- const linux_1 = require("./linux");
8
- const macos_1 = require("./macos");
9
- const self_update_1 = require("./self_update");
10
- function printServiceHelp() {
11
- console.log(`kandev service — install kandev as an OS-managed service
12
-
13
- Usage:
14
- kandev service install [--system] [--port <port>] [--home-dir <path>] [--no-boot-start]
15
- kandev service uninstall [--system]
16
- kandev service start|stop|restart|status [--system]
17
- kandev service logs [-f] [--system]
18
- kandev service config [--system]
19
-
20
- Modes:
21
- default User-level service.
22
- Linux: ~/.config/systemd/user/kandev.service
23
- macOS: ~/Library/LaunchAgents/com.kdlbs.kandev.plist
24
- Runs as the current user. On Linux, only starts at boot
25
- if 'loginctl enable-linger <user>' has been run.
26
- --system System-level service. Requires sudo.
27
- Linux: /etc/systemd/system/kandev.service
28
- macOS: /Library/LaunchDaemons/com.kdlbs.kandev.plist
29
- Starts at boot regardless of login state.
30
-
31
- Flags:
32
- --port <port> Backend port baked into the unit (KANDEV_SERVER_PORT).
33
- --home-dir <path> KANDEV_HOME_DIR baked into the unit.
34
- Defaults: ~/.kandev (user), /var/lib/kandev (system).
35
- --no-boot-start (Linux user mode) Skip the enable-linger hint.
36
- -f, --follow (logs) Stream logs instead of dumping the tail.
37
-
38
- Updates:
39
- After 'npm update -g kandev' or 'brew upgrade kandev', re-run
40
- 'kandev service install' to refresh paths in the unit file.
41
- `);
42
- }
43
- async function runServiceCommand(argv) {
44
- const args = (0, args_1.parseServiceArgs)(argv);
45
- if (args.showHelp) {
46
- printServiceHelp();
47
- return;
48
- }
49
- // 'config' is read-only and identical across platforms — handle here so we
50
- // don't need to duplicate it in both linux.ts and macos.ts.
51
- if (args.action === "config") {
52
- (0, config_1.printServiceConfig)(args);
53
- return;
54
- }
55
- // Hidden helper entrypoint used by the backend self-update endpoint. It is
56
- // intentionally absent from help output.
57
- if (args.action === "self-update") {
58
- (0, self_update_1.runSelfUpdateCommand)(args);
59
- return;
60
- }
61
- switch (process.platform) {
62
- case "linux":
63
- return (0, linux_1.runLinuxService)(args);
64
- case "darwin":
65
- return (0, macos_1.runMacosService)(args);
66
- default:
67
- throw new Error(`kandev service is not yet supported on ${process.platform}. ` +
68
- `Currently supported: linux (systemd), darwin (launchd).`);
69
- }
70
- }
@@ -1,51 +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.commandExists = commandExists;
7
- exports.writeUnitFile = writeUnitFile;
8
- const node_child_process_1 = require("node:child_process");
9
- const node_fs_1 = __importDefault(require("node:fs"));
10
- const templates_1 = require("./templates");
11
- /** Cheap PATH lookup; returns true if `cmd` resolves via `which`. */
12
- function commandExists(cmd) {
13
- const res = (0, node_child_process_1.spawnSync)("which", [cmd], { stdio: "ignore" });
14
- return res.status === 0;
15
- }
16
- /**
17
- * Write `content` to `targetPath` with idempotent + foreign-file handling.
18
- *
19
- * - Missing file → created (no warning)
20
- * - Existing managed file, same content → unchanged (no write, no warning)
21
- * - Existing managed file, different content → updated (overwrite, brief log)
22
- * - Existing file that doesn't look managed → replaced-foreign (overwrite,
23
- * loud warning so the user notices we clobbered something)
24
- *
25
- * The "managed" check looks for kandev's marker substring; users who hand-edit
26
- * past the marker lose the no-op shortcut but still get an "updated" log line,
27
- * which is the expected workflow.
28
- */
29
- function writeUnitFile(targetPath, content) {
30
- if (!node_fs_1.default.existsSync(targetPath)) {
31
- node_fs_1.default.writeFileSync(targetPath, content, { mode: 0o644 });
32
- console.log(`[kandev] wrote ${targetPath}`);
33
- return "created";
34
- }
35
- const existing = node_fs_1.default.readFileSync(targetPath, "utf8");
36
- if (existing === content) {
37
- console.log(`[kandev] ${targetPath} is already up to date`);
38
- return "unchanged";
39
- }
40
- if (!(0, templates_1.looksLikeManagedUnit)(existing)) {
41
- console.log(`[kandev] WARNING: ${targetPath} exists but doesn't look like a kandev-managed file.`);
42
- console.log(`[kandev] The existing file will be replaced. A backup is saved alongside.`);
43
- node_fs_1.default.copyFileSync(targetPath, `${targetPath}.bak`);
44
- node_fs_1.default.writeFileSync(targetPath, content, { mode: 0o644 });
45
- console.log(`[kandev] replaced ${targetPath} (backup: ${targetPath}.bak)`);
46
- return "replaced-foreign";
47
- }
48
- node_fs_1.default.writeFileSync(targetPath, content, { mode: 0o644 });
49
- console.log(`[kandev] updated ${targetPath}`);
50
- return "updated";
51
- }
@@ -1,73 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.spawnLaunchctl = spawnLaunchctl;
4
- exports.sleepSync = sleepSync;
5
- exports.bootoutAndWait = bootoutAndWait;
6
- exports.bootstrapWithRetry = bootstrapWithRetry;
7
- exports.reloadService = reloadService;
8
- const node_child_process_1 = require("node:child_process");
9
- const BOOTOUT_POLL_INTERVAL_MS = 100;
10
- const BOOTOUT_POLL_ATTEMPTS = 50; // ~5s ceiling waiting for teardown
11
- const BOOTSTRAP_MAX_ATTEMPTS = 5;
12
- const BOOTSTRAP_RETRY_BASE_MS = 300;
13
- function spawnLaunchctl(args, stdio) {
14
- const res = (0, node_child_process_1.spawnSync)("launchctl", args, { stdio });
15
- return { status: res.status };
16
- }
17
- // Block the current thread for `ms`. The launchctl orchestration runs in the
18
- // synchronous `service install`/`start`/`restart` paths, so we can't await a
19
- // timer — Atomics.wait gives a dependency-free blocking sleep.
20
- function sleepSync(ms) {
21
- if (ms <= 0)
22
- return;
23
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
24
- }
25
- function isLoaded(target, run) {
26
- // `launchctl print <target>` exits non-zero once the label is gone.
27
- return run(["print", target], "ignore").status === 0;
28
- }
29
- /**
30
- * Boot out a launchd job and wait until launchd has fully removed it. A no-op
31
- * (returns promptly) when the label isn't loaded, so fresh installs aren't
32
- * slowed down.
33
- */
34
- function bootoutAndWait(target, deps = {}) {
35
- const run = deps.run ?? spawnLaunchctl;
36
- const sleep = deps.sleep ?? sleepSync;
37
- run(["bootout", target], "ignore");
38
- for (let attempt = 0; attempt < BOOTOUT_POLL_ATTEMPTS; attempt += 1) {
39
- if (!isLoaded(target, run)) {
40
- return;
41
- }
42
- sleep(BOOTOUT_POLL_INTERVAL_MS);
43
- }
44
- }
45
- /**
46
- * Bootstrap a launchd job, retrying through the transient EIO that launchd
47
- * returns while a previous instance of the same label finishes tearing down.
48
- * Throws with the last exit code if every attempt fails.
49
- */
50
- function bootstrapWithRetry(domain, plistPath, deps = {}) {
51
- const run = deps.run ?? spawnLaunchctl;
52
- const sleep = deps.sleep ?? sleepSync;
53
- let lastStatus = null;
54
- for (let attempt = 1; attempt <= BOOTSTRAP_MAX_ATTEMPTS; attempt += 1) {
55
- lastStatus = run(["bootstrap", domain, plistPath], "inherit").status;
56
- if (lastStatus === 0) {
57
- return;
58
- }
59
- if (attempt < BOOTSTRAP_MAX_ATTEMPTS) {
60
- sleep(BOOTSTRAP_RETRY_BASE_MS * attempt);
61
- }
62
- }
63
- throw new Error(`launchctl bootstrap ${domain} ${plistPath} failed with code ${lastStatus}`);
64
- }
65
- /**
66
- * Reload a launchd job: fully unload (waiting for teardown) then bootstrap with
67
- * retry. This is the safe sequence whenever the target might currently be
68
- * running — installs that refresh a live service, and `start`.
69
- */
70
- function reloadService(target, domain, plistPath, deps = {}) {
71
- bootoutAndWait(target, deps);
72
- bootstrapWithRetry(domain, plistPath, deps);
73
- }