kandev 0.49.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.
- package/README.md +11 -0
- package/dist/args.js +4 -0
- package/dist/cli.js +20 -0
- package/dist/dev.js +7 -2
- package/dist/ports.js +23 -11
- package/dist/run.js +5 -1
- package/dist/service/args.js +105 -0
- package/dist/service/config.js +111 -0
- package/dist/service/health_check.js +84 -0
- package/dist/service/index.js +63 -0
- package/dist/service/install_helpers.js +51 -0
- package/dist/service/linux.js +147 -0
- package/dist/service/macos.js +196 -0
- package/dist/service/paths.js +126 -0
- package/dist/service/stale_check.js +78 -0
- package/dist/service/templates.js +157 -0
- package/dist/shared.js +93 -8
- package/package.json +6 -6
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MANAGED_MARKER_TEXT = void 0;
|
|
4
|
+
exports.looksLikeManagedUnit = looksLikeManagedUnit;
|
|
5
|
+
exports.renderSystemdUnit = renderSystemdUnit;
|
|
6
|
+
exports.renderLaunchdPlist = renderLaunchdPlist;
|
|
7
|
+
const SYSTEMD_PATH = "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin";
|
|
8
|
+
const LAUNCHD_PATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
|
9
|
+
/**
|
|
10
|
+
* Marker substring baked into every unit/plist kandev writes. Used to safely
|
|
11
|
+
* detect "this file is ours, overwriting is fine" vs "user has put something
|
|
12
|
+
* else here, warn loudly before replacing".
|
|
13
|
+
*/
|
|
14
|
+
exports.MANAGED_MARKER_TEXT = "managed by kandev";
|
|
15
|
+
const SYSTEMD_MARKER = `# ${exports.MANAGED_MARKER_TEXT} — regenerated by \`kandev service install\``;
|
|
16
|
+
const PLIST_MARKER = `<!-- ${exports.MANAGED_MARKER_TEXT} — regenerated by \`kandev service install\` -->`;
|
|
17
|
+
function looksLikeManagedUnit(content) {
|
|
18
|
+
return content.includes(exports.MANAGED_MARKER_TEXT);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Render a systemd unit file for kandev.
|
|
22
|
+
*
|
|
23
|
+
* The unit hard-codes absolute paths so it works without a user PATH. We pass
|
|
24
|
+
* `--headless` so the daemon doesn't try to open a browser. KANDEV_BUNDLE_DIR /
|
|
25
|
+
* KANDEV_VERSION are surfaced only when present (Homebrew installs only) so
|
|
26
|
+
* `npm i -g` installs don't get spurious env vars.
|
|
27
|
+
*/
|
|
28
|
+
function renderSystemdUnit(input) {
|
|
29
|
+
const env = [
|
|
30
|
+
envLine("KANDEV_HOME_DIR", input.homeDir),
|
|
31
|
+
envLine("KANDEV_LOG_LEVEL", "info"),
|
|
32
|
+
envLine("PATH", SYSTEMD_PATH),
|
|
33
|
+
];
|
|
34
|
+
if (input.port !== undefined) {
|
|
35
|
+
env.push(envLine("KANDEV_SERVER_PORT", String(input.port)));
|
|
36
|
+
}
|
|
37
|
+
if (input.launcher.bundleDir) {
|
|
38
|
+
env.push(envLine("KANDEV_BUNDLE_DIR", input.launcher.bundleDir));
|
|
39
|
+
}
|
|
40
|
+
if (input.launcher.version) {
|
|
41
|
+
env.push(envLine("KANDEV_VERSION", input.launcher.version));
|
|
42
|
+
}
|
|
43
|
+
const wantedBy = input.mode === "system" ? "multi-user.target" : "default.target";
|
|
44
|
+
const userLine = input.mode === "system" && input.systemUser ? `User=${input.systemUser}\n` : "";
|
|
45
|
+
const exec = `${quoteForUnit(input.launcher.nodePath)} ${quoteForUnit(input.launcher.cliEntry)} --headless`;
|
|
46
|
+
return `${SYSTEMD_MARKER}
|
|
47
|
+
[Unit]
|
|
48
|
+
Description=Kandev autonomous agent platform
|
|
49
|
+
Documentation=https://github.com/kdlbs/kandev
|
|
50
|
+
After=network-online.target
|
|
51
|
+
Wants=network-online.target
|
|
52
|
+
|
|
53
|
+
[Service]
|
|
54
|
+
Type=simple
|
|
55
|
+
ExecStart=${exec}
|
|
56
|
+
${userLine}${env.join("\n")}
|
|
57
|
+
Restart=on-failure
|
|
58
|
+
RestartSec=5s
|
|
59
|
+
KillMode=mixed
|
|
60
|
+
TimeoutStopSec=30s
|
|
61
|
+
|
|
62
|
+
[Install]
|
|
63
|
+
WantedBy=${wantedBy}
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Render a launchd plist for kandev.
|
|
68
|
+
*
|
|
69
|
+
* launchd has no equivalent to systemd's enable-linger: a user agent only runs
|
|
70
|
+
* while the user is logged in. For a Mac-as-server scenario, use --system to
|
|
71
|
+
* get a LaunchDaemon that runs at boot regardless of login.
|
|
72
|
+
*/
|
|
73
|
+
function renderLaunchdPlist(input) {
|
|
74
|
+
const envEntries = [
|
|
75
|
+
["KANDEV_HOME_DIR", input.homeDir],
|
|
76
|
+
["KANDEV_LOG_LEVEL", "info"],
|
|
77
|
+
["PATH", LAUNCHD_PATH],
|
|
78
|
+
];
|
|
79
|
+
if (input.port !== undefined) {
|
|
80
|
+
envEntries.push(["KANDEV_SERVER_PORT", String(input.port)]);
|
|
81
|
+
}
|
|
82
|
+
if (input.launcher.bundleDir) {
|
|
83
|
+
envEntries.push(["KANDEV_BUNDLE_DIR", input.launcher.bundleDir]);
|
|
84
|
+
}
|
|
85
|
+
if (input.launcher.version) {
|
|
86
|
+
envEntries.push(["KANDEV_VERSION", input.launcher.version]);
|
|
87
|
+
}
|
|
88
|
+
const envXml = envEntries
|
|
89
|
+
.map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
|
|
90
|
+
.join("\n");
|
|
91
|
+
const args = [input.launcher.nodePath, input.launcher.cliEntry, "--headless"];
|
|
92
|
+
const argsXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
93
|
+
// For system-mode LaunchDaemons, run as a specific user instead of root.
|
|
94
|
+
// For user agents this directive is omitted — the agent already runs as
|
|
95
|
+
// the loading user.
|
|
96
|
+
const userBlock = input.mode === "system" && input.systemUser
|
|
97
|
+
? ` <key>UserName</key>\n <string>${escapeXml(input.systemUser)}</string>\n`
|
|
98
|
+
: "";
|
|
99
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
100
|
+
${PLIST_MARKER}
|
|
101
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
102
|
+
<plist version="1.0">
|
|
103
|
+
<dict>
|
|
104
|
+
<key>Label</key>
|
|
105
|
+
<string>com.kdlbs.kandev</string>
|
|
106
|
+
<key>ProgramArguments</key>
|
|
107
|
+
<array>
|
|
108
|
+
${argsXml}
|
|
109
|
+
</array>
|
|
110
|
+
<key>EnvironmentVariables</key>
|
|
111
|
+
<dict>
|
|
112
|
+
${envXml}
|
|
113
|
+
</dict>
|
|
114
|
+
${userBlock} <key>RunAtLoad</key>
|
|
115
|
+
<true/>
|
|
116
|
+
<key>KeepAlive</key>
|
|
117
|
+
<true/>
|
|
118
|
+
<key>StandardOutPath</key>
|
|
119
|
+
<string>${escapeXml(`${input.logDir}/service.out`)}</string>
|
|
120
|
+
<key>StandardErrorPath</key>
|
|
121
|
+
<string>${escapeXml(`${input.logDir}/service.err`)}</string>
|
|
122
|
+
<key>WorkingDirectory</key>
|
|
123
|
+
<string>${escapeXml(input.homeDir)}</string>
|
|
124
|
+
</dict>
|
|
125
|
+
</plist>
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
function escapeXml(value) {
|
|
129
|
+
return value
|
|
130
|
+
.replace(/&/g, "&")
|
|
131
|
+
.replace(/</g, "<")
|
|
132
|
+
.replace(/>/g, ">")
|
|
133
|
+
.replace(/"/g, """)
|
|
134
|
+
.replace(/'/g, "'");
|
|
135
|
+
}
|
|
136
|
+
// systemd unit "Environment=" lines can contain spaces by wrapping in double quotes.
|
|
137
|
+
// ExecStart needs special handling for spaces too — paths with spaces must be
|
|
138
|
+
// double-quoted per systemd.unit(5). Escape backslashes before double-quotes so
|
|
139
|
+
// a literal `\` in a path is preserved instead of merging with the following
|
|
140
|
+
// character into an escape sequence.
|
|
141
|
+
function quoteForUnit(value) {
|
|
142
|
+
if (!/[\s"\\]/.test(value))
|
|
143
|
+
return value;
|
|
144
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
145
|
+
return `"${escaped}"`;
|
|
146
|
+
}
|
|
147
|
+
// Render a systemd `Environment=KEY=value` line, quoting the whole assignment
|
|
148
|
+
// when the value contains whitespace, double-quote, or backslash. Without
|
|
149
|
+
// quoting, systemd splits the line on the first space and treats subsequent
|
|
150
|
+
// tokens as separate KEY=value pairs — which silently corrupts paths like
|
|
151
|
+
// `/home/john doe/.kandev`.
|
|
152
|
+
function envLine(key, value) {
|
|
153
|
+
if (!/[\s"\\]/.test(value))
|
|
154
|
+
return `Environment=${key}=${value}`;
|
|
155
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
156
|
+
return `Environment="${key}=${escaped}"`;
|
|
157
|
+
}
|
package/dist/shared.js
CHANGED
|
@@ -5,12 +5,18 @@
|
|
|
5
5
|
* This module extracts common patterns used across different launch modes
|
|
6
6
|
* to reduce duplication and ensure consistent behavior.
|
|
7
7
|
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
8
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
12
|
exports.pickPorts = pickPorts;
|
|
10
13
|
exports.buildBackendEnv = buildBackendEnv;
|
|
11
14
|
exports.buildWebEnv = buildWebEnv;
|
|
15
|
+
exports.listHostNetworkAddresses = listHostNetworkAddresses;
|
|
12
16
|
exports.logStartupInfo = logStartupInfo;
|
|
17
|
+
exports.networkUrlsForPort = networkUrlsForPort;
|
|
13
18
|
exports.attachBackendExitHandler = attachBackendExitHandler;
|
|
19
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
14
20
|
const constants_1 = require("./constants");
|
|
15
21
|
const ports_1 = require("./ports");
|
|
16
22
|
/**
|
|
@@ -80,31 +86,110 @@ function buildWebEnv(options) {
|
|
|
80
86
|
// URLs like `https://host:38429/...` that aren't reachable behind a
|
|
81
87
|
// reverse proxy / ingress / Cloudflare tunnel.
|
|
82
88
|
env.NEXT_PUBLIC_KANDEV_API_PORT = String(ports.backendPort);
|
|
89
|
+
// Auto-allow the host's own LAN / VPN addresses so a dev hitting the dev
|
|
90
|
+
// server from another device on the same network (Tailscale, LAN IP, WSL
|
|
91
|
+
// mirrored mode, etc.) passes Next.js's allowedDevOrigins check and HMR
|
|
92
|
+
// works. The user can still extend the list via NEXT_ALLOWED_DEV_ORIGINS.
|
|
93
|
+
// Skip the assignment when there's nothing to add — keeps the env clean
|
|
94
|
+
// for the loopback-only case.
|
|
95
|
+
const merged = mergeAllowedDevOrigins(process.env.NEXT_ALLOWED_DEV_ORIGINS, listHostNetworkAddresses());
|
|
96
|
+
if (merged)
|
|
97
|
+
env.NEXT_ALLOWED_DEV_ORIGINS = merged;
|
|
83
98
|
}
|
|
84
99
|
if (debug) {
|
|
85
100
|
env.NEXT_PUBLIC_KANDEV_DEBUG = "true";
|
|
86
101
|
}
|
|
87
102
|
return env;
|
|
88
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Returns the host's non-loopback, non-internal IPv4/IPv6 addresses. Used to
|
|
106
|
+
* auto-populate Next.js `allowedDevOrigins` so the dev server accepts
|
|
107
|
+
* connections from LAN / Tailscale / SSH-forwarded clients.
|
|
108
|
+
*/
|
|
109
|
+
function listHostNetworkAddresses() {
|
|
110
|
+
const v4 = [];
|
|
111
|
+
const v6 = [];
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
const interfaces = node_os_1.default.networkInterfaces();
|
|
114
|
+
for (const addrs of Object.values(interfaces)) {
|
|
115
|
+
if (!addrs)
|
|
116
|
+
continue;
|
|
117
|
+
for (const addr of addrs) {
|
|
118
|
+
if (addr.internal)
|
|
119
|
+
continue;
|
|
120
|
+
// Skip link-local IPv6 (fe80::/10) and link-local IPv4 (169.254.0.0/16,
|
|
121
|
+
// RFC 3927) — neither is reachable from a remote machine, and the
|
|
122
|
+
// 169.254 range in particular is what Hyper-V assigns to its phantom
|
|
123
|
+
// WSL adapter, which clutters the startup output. The regex covers the
|
|
124
|
+
// full /10 (fe80::–febf::); OS stacks only assign fe80::/64 in practice
|
|
125
|
+
// but a stricter check is the same effort and removes the surprise.
|
|
126
|
+
if (addr.family === "IPv6" && /^fe[89ab]/i.test(addr.address))
|
|
127
|
+
continue;
|
|
128
|
+
if (addr.family === "IPv4" && addr.address.startsWith("169.254."))
|
|
129
|
+
continue;
|
|
130
|
+
if (seen.has(addr.address))
|
|
131
|
+
continue;
|
|
132
|
+
seen.add(addr.address);
|
|
133
|
+
if (addr.family === "IPv4")
|
|
134
|
+
v4.push(addr.address);
|
|
135
|
+
else
|
|
136
|
+
v6.push(addr.address);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// IPv4 first — LAN + Tailscale IPv4 are what people usually want.
|
|
140
|
+
return [...v4, ...v6];
|
|
141
|
+
}
|
|
142
|
+
function mergeAllowedDevOrigins(existing, extra) {
|
|
143
|
+
const set = new Set();
|
|
144
|
+
if (existing) {
|
|
145
|
+
for (const s of existing.split(",")) {
|
|
146
|
+
const trimmed = s.trim();
|
|
147
|
+
if (trimmed)
|
|
148
|
+
set.add(trimmed);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const s of extra)
|
|
152
|
+
set.add(s);
|
|
153
|
+
return [...set].join(",");
|
|
154
|
+
}
|
|
89
155
|
/**
|
|
90
156
|
* Logs a unified startup info block to the console.
|
|
157
|
+
*
|
|
158
|
+
* Shows only the URL the user actually opens — start/run modes have the Go
|
|
159
|
+
* backend reverse-proxy Next.js on a single port, dev mode hits Next.js
|
|
160
|
+
* directly. The other port and the agentctl port are internal plumbing and
|
|
161
|
+
* would only mislead. Below the URL, lists the same port on each non-loopback
|
|
162
|
+
* interface (LAN, Tailscale) so a user opening the app remotely sees the
|
|
163
|
+
* right address.
|
|
91
164
|
*/
|
|
92
165
|
function logStartupInfo(options) {
|
|
93
|
-
const { header, ports, dbPath, logLevel } = options;
|
|
94
|
-
const
|
|
95
|
-
const
|
|
166
|
+
const { header, ports, primary = "backend", dbPath, logLevel } = options;
|
|
167
|
+
const primaryPort = primary === "web" ? ports.webPort : ports.backendPort;
|
|
168
|
+
const primaryUrl = `http://localhost:${primaryPort}`;
|
|
169
|
+
const networkHosts = listHostNetworkAddresses();
|
|
96
170
|
console.log(`[kandev] ${header}`);
|
|
97
|
-
console.log("[kandev]
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
171
|
+
console.log("[kandev] url:", primaryUrl);
|
|
172
|
+
for (const url of networkUrlsForPort(primaryPort, networkHosts)) {
|
|
173
|
+
console.log("[kandev] network:", url);
|
|
174
|
+
}
|
|
175
|
+
console.log("[kandev] mcp:", `${ports.backendUrl}/mcp`);
|
|
101
176
|
if (dbPath) {
|
|
102
|
-
console.log("[kandev] db
|
|
177
|
+
console.log("[kandev] db:", dbPath);
|
|
103
178
|
}
|
|
104
179
|
if (logLevel) {
|
|
105
180
|
console.log("[kandev] log level:", logLevel);
|
|
106
181
|
}
|
|
107
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Builds `http://<host>:<port>` URLs from a list of host addresses, wrapping
|
|
185
|
+
* IPv6 addresses in brackets per RFC 3986.
|
|
186
|
+
*/
|
|
187
|
+
function networkUrlsForPort(port, hosts) {
|
|
188
|
+
return hosts.map((host) => {
|
|
189
|
+
const formatted = host.includes(":") ? `[${host}]` : host;
|
|
190
|
+
return `http://${formatted}:${port}`;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
108
193
|
/**
|
|
109
194
|
* Attaches a standardized exit handler to a backend process.
|
|
110
195
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kandev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Launcher for Kandev — manage tasks, orchestrate agents, review changes, and ship value",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
"npm": ">=7"
|
|
23
23
|
},
|
|
24
24
|
"optionalDependencies": {
|
|
25
|
-
"@kdlbs/runtime-linux-x64": "0.
|
|
26
|
-
"@kdlbs/runtime-linux-arm64": "0.
|
|
27
|
-
"@kdlbs/runtime-darwin-x64": "0.
|
|
28
|
-
"@kdlbs/runtime-darwin-arm64": "0.
|
|
29
|
-
"@kdlbs/runtime-win32-x64": "0.
|
|
25
|
+
"@kdlbs/runtime-linux-x64": "0.50.0",
|
|
26
|
+
"@kdlbs/runtime-linux-arm64": "0.50.0",
|
|
27
|
+
"@kdlbs/runtime-darwin-x64": "0.50.0",
|
|
28
|
+
"@kdlbs/runtime-darwin-arm64": "0.50.0",
|
|
29
|
+
"@kdlbs/runtime-win32-x64": "0.50.0"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"tar": "^7.5.11",
|