kandev 0.64.0 → 0.67.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/shared.js
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Shared utilities for CLI commands (dev, start, run).
|
|
4
|
-
*
|
|
5
|
-
* This module extracts common patterns used across different launch modes
|
|
6
|
-
* to reduce duplication and ensure consistent behavior.
|
|
7
|
-
*/
|
|
8
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
-
};
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.pickPorts = pickPorts;
|
|
13
|
-
exports.buildBackendEnv = buildBackendEnv;
|
|
14
|
-
exports.buildWebEnv = buildWebEnv;
|
|
15
|
-
exports.listHostNetworkAddresses = listHostNetworkAddresses;
|
|
16
|
-
exports.logStartupInfo = logStartupInfo;
|
|
17
|
-
exports.networkUrlsForPort = networkUrlsForPort;
|
|
18
|
-
exports.attachBackendExitHandler = attachBackendExitHandler;
|
|
19
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
20
|
-
const constants_1 = require("./constants");
|
|
21
|
-
const ports_1 = require("./ports");
|
|
22
|
-
/**
|
|
23
|
-
* Picks available ports for all services, using provided values or finding free ports.
|
|
24
|
-
*
|
|
25
|
-
* @param backendPort - Optional preferred backend port
|
|
26
|
-
* @param webPort - Optional preferred web port
|
|
27
|
-
* @returns Resolved ports for all services
|
|
28
|
-
*/
|
|
29
|
-
async function pickPorts(backendPort, webPort) {
|
|
30
|
-
const resolvedBackendPort = backendPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_BACKEND_PORT));
|
|
31
|
-
const resolvedWebPort = webPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_WEB_PORT));
|
|
32
|
-
const agentctlPort = await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_AGENTCTL_PORT);
|
|
33
|
-
return {
|
|
34
|
-
backendPort: resolvedBackendPort,
|
|
35
|
-
webPort: resolvedWebPort,
|
|
36
|
-
agentctlPort,
|
|
37
|
-
backendUrl: `http://localhost:${resolvedBackendPort}`,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Builds environment variables for the backend process.
|
|
42
|
-
*
|
|
43
|
-
* @param options - Configuration options for the backend environment
|
|
44
|
-
* @returns Environment object for the backend process
|
|
45
|
-
*/
|
|
46
|
-
function buildBackendEnv(options) {
|
|
47
|
-
const { ports, logLevel, extra } = options;
|
|
48
|
-
return {
|
|
49
|
-
...process.env,
|
|
50
|
-
KANDEV_SERVER_PORT: String(ports.backendPort),
|
|
51
|
-
KANDEV_WEB_INTERNAL_URL: `http://localhost:${ports.webPort}`,
|
|
52
|
-
KANDEV_AGENT_STANDALONE_PORT: String(ports.agentctlPort),
|
|
53
|
-
...(logLevel ? { KANDEV_LOG_LEVEL: logLevel } : {}),
|
|
54
|
-
...extra,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Builds environment variables for the web process.
|
|
59
|
-
*
|
|
60
|
-
* @param options - Configuration options for the web environment
|
|
61
|
-
* @returns Environment object for the web process
|
|
62
|
-
*/
|
|
63
|
-
function buildWebEnv(options) {
|
|
64
|
-
const { ports, production = false, debug = false } = options;
|
|
65
|
-
const env = {
|
|
66
|
-
...process.env,
|
|
67
|
-
// Server-side: full localhost URL for SSR fetches (Next.js SSR → Go backend)
|
|
68
|
-
KANDEV_API_BASE_URL: ports.backendUrl,
|
|
69
|
-
PORT: String(ports.webPort),
|
|
70
|
-
// Ensure Next.js standalone server binds to 127.0.0.1 so localhost health checks work.
|
|
71
|
-
// Without this, HOSTNAME from the host environment can cause binding issues.
|
|
72
|
-
HOSTNAME: "127.0.0.1",
|
|
73
|
-
};
|
|
74
|
-
if (production) {
|
|
75
|
-
env.NODE_ENV = "production";
|
|
76
|
-
// Explicitly unset so a host-level NEXT_PUBLIC_KANDEV_API_PORT (from a .env
|
|
77
|
-
// file, Docker env, or CI variable) cannot leak through the process.env
|
|
78
|
-
// spread above and reintroduce the cross-origin URL problem.
|
|
79
|
-
delete env.NEXT_PUBLIC_KANDEV_API_PORT;
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
// Dev mode only: browser hits the web port directly, so the client needs to
|
|
83
|
-
// know the backend port for API calls. In production the Go backend
|
|
84
|
-
// reverse-proxies Next.js on a single port, so the client uses same-origin
|
|
85
|
-
// and this var must NOT be set — otherwise the client builds cross-origin
|
|
86
|
-
// URLs like `https://host:38429/...` that aren't reachable behind a
|
|
87
|
-
// reverse proxy / ingress / Cloudflare tunnel.
|
|
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;
|
|
98
|
-
}
|
|
99
|
-
if (debug) {
|
|
100
|
-
env.KANDEV_DEBUG = "true";
|
|
101
|
-
env.NEXT_PUBLIC_KANDEV_DEBUG = "true";
|
|
102
|
-
}
|
|
103
|
-
return env;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Returns the host's non-loopback, non-internal IPv4/IPv6 addresses. Used to
|
|
107
|
-
* auto-populate Next.js `allowedDevOrigins` so the dev server accepts
|
|
108
|
-
* connections from LAN / Tailscale / SSH-forwarded clients.
|
|
109
|
-
*/
|
|
110
|
-
function listHostNetworkAddresses() {
|
|
111
|
-
const v4 = [];
|
|
112
|
-
const v6 = [];
|
|
113
|
-
const seen = new Set();
|
|
114
|
-
const interfaces = node_os_1.default.networkInterfaces();
|
|
115
|
-
for (const addrs of Object.values(interfaces)) {
|
|
116
|
-
if (!addrs)
|
|
117
|
-
continue;
|
|
118
|
-
for (const addr of addrs) {
|
|
119
|
-
if (addr.internal)
|
|
120
|
-
continue;
|
|
121
|
-
// Skip link-local IPv6 (fe80::/10) and link-local IPv4 (169.254.0.0/16,
|
|
122
|
-
// RFC 3927) — neither is reachable from a remote machine, and the
|
|
123
|
-
// 169.254 range in particular is what Hyper-V assigns to its phantom
|
|
124
|
-
// WSL adapter, which clutters the startup output. The regex covers the
|
|
125
|
-
// full /10 (fe80::–febf::); OS stacks only assign fe80::/64 in practice
|
|
126
|
-
// but a stricter check is the same effort and removes the surprise.
|
|
127
|
-
if (addr.family === "IPv6" && /^fe[89ab]/i.test(addr.address))
|
|
128
|
-
continue;
|
|
129
|
-
if (addr.family === "IPv4" && addr.address.startsWith("169.254."))
|
|
130
|
-
continue;
|
|
131
|
-
if (seen.has(addr.address))
|
|
132
|
-
continue;
|
|
133
|
-
seen.add(addr.address);
|
|
134
|
-
if (addr.family === "IPv4")
|
|
135
|
-
v4.push(addr.address);
|
|
136
|
-
else
|
|
137
|
-
v6.push(addr.address);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// IPv4 first — LAN + Tailscale IPv4 are what people usually want.
|
|
141
|
-
return [...v4, ...v6];
|
|
142
|
-
}
|
|
143
|
-
function mergeAllowedDevOrigins(existing, extra) {
|
|
144
|
-
const set = new Set();
|
|
145
|
-
if (existing) {
|
|
146
|
-
for (const s of existing.split(",")) {
|
|
147
|
-
const trimmed = s.trim();
|
|
148
|
-
if (trimmed)
|
|
149
|
-
set.add(trimmed);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
for (const s of extra)
|
|
153
|
-
set.add(s);
|
|
154
|
-
return [...set].join(",");
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Logs a unified startup info block to the console.
|
|
158
|
-
*
|
|
159
|
-
* Shows only the URL the user actually opens — start/run modes have the Go
|
|
160
|
-
* backend reverse-proxy Next.js on a single port, dev mode hits Next.js
|
|
161
|
-
* directly. The other port and the agentctl port are internal plumbing and
|
|
162
|
-
* would only mislead. Below the URL, lists the same port on each non-loopback
|
|
163
|
-
* interface (LAN, Tailscale) so a user opening the app remotely sees the
|
|
164
|
-
* right address.
|
|
165
|
-
*/
|
|
166
|
-
function logStartupInfo(options) {
|
|
167
|
-
const { header, ports, primary = "backend", dbPath, logLevel } = options;
|
|
168
|
-
const primaryPort = primary === "web" ? ports.webPort : ports.backendPort;
|
|
169
|
-
const primaryUrl = `http://localhost:${primaryPort}`;
|
|
170
|
-
const networkHosts = listHostNetworkAddresses();
|
|
171
|
-
console.log(`[kandev] ${header}`);
|
|
172
|
-
console.log("[kandev] url:", primaryUrl);
|
|
173
|
-
for (const url of networkUrlsForPort(primaryPort, networkHosts)) {
|
|
174
|
-
console.log("[kandev] network:", url);
|
|
175
|
-
}
|
|
176
|
-
console.log("[kandev] mcp:", `${ports.backendUrl}/mcp`);
|
|
177
|
-
if (dbPath) {
|
|
178
|
-
console.log("[kandev] db:", dbPath);
|
|
179
|
-
}
|
|
180
|
-
if (logLevel) {
|
|
181
|
-
console.log("[kandev] log level:", logLevel);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Builds `http://<host>:<port>` URLs from a list of host addresses, wrapping
|
|
186
|
-
* IPv6 addresses in brackets per RFC 3986.
|
|
187
|
-
*/
|
|
188
|
-
function networkUrlsForPort(port, hosts) {
|
|
189
|
-
return hosts.map((host) => {
|
|
190
|
-
const formatted = host.includes(":") ? `[${host}]` : host;
|
|
191
|
-
return `http://${formatted}:${port}`;
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Attaches a standardized exit handler to a backend process.
|
|
196
|
-
*
|
|
197
|
-
* When the backend exits, this handler logs the exit reason and triggers
|
|
198
|
-
* a graceful shutdown of all supervised processes. If the process was
|
|
199
|
-
* killed by a signal, it exits with code 0; otherwise it uses the
|
|
200
|
-
* process exit code (defaulting to 1).
|
|
201
|
-
*
|
|
202
|
-
* @param backendProc - The backend child process
|
|
203
|
-
* @param supervisor - The process supervisor managing child processes
|
|
204
|
-
*/
|
|
205
|
-
function attachBackendExitHandler(backendProc, supervisor, options = {}) {
|
|
206
|
-
backendProc.on("exit", (code, signal) => {
|
|
207
|
-
console.error(`[kandev] backend exited (code=${code}, signal=${signal})`);
|
|
208
|
-
if (options.shouldShutdown && !options.shouldShutdown()) {
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
const exitCode = signal ? 0 : (code ?? 1);
|
|
212
|
-
void supervisor.shutdown("backend exit").then(() => process.exit(exitCode));
|
|
213
|
-
});
|
|
214
|
-
}
|
package/dist/start.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Production start command for running local builds.
|
|
4
|
-
*
|
|
5
|
-
* This module implements the `kandev start` command, which runs the locally
|
|
6
|
-
* built backend binary and web app in production mode. Unlike `kandev dev`
|
|
7
|
-
* which uses hot-reloading, this runs the optimized production builds.
|
|
8
|
-
*
|
|
9
|
-
* Prerequisites:
|
|
10
|
-
* - Backend must be built: `make build-backend`
|
|
11
|
-
* - Web app must be built: `make build-web`
|
|
12
|
-
* - Or simply: `make build` (builds both)
|
|
13
|
-
*/
|
|
14
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
-
};
|
|
17
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.resolveStandaloneServerPath = resolveStandaloneServerPath;
|
|
19
|
-
exports.runStart = runStart;
|
|
20
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
-
const constants_1 = require("./constants");
|
|
23
|
-
const health_1 = require("./health");
|
|
24
|
-
const platform_1 = require("./platform");
|
|
25
|
-
const process_1 = require("./process");
|
|
26
|
-
const shared_1 = require("./shared");
|
|
27
|
-
const backend_1 = require("./supervisor/backend");
|
|
28
|
-
const web_1 = require("./web");
|
|
29
|
-
/**
|
|
30
|
-
* Locates the standalone Next.js `server.js` inside `apps/web/.next/standalone/`.
|
|
31
|
-
*
|
|
32
|
-
* Normally this sits at `.next/standalone/web/server.js`, but Turbopack may
|
|
33
|
-
* place it under a deeper path (e.g. `.next/standalone/Users/.../web/server.js`)
|
|
34
|
-
* when it detects the wrong project root (typically caused by a stray
|
|
35
|
-
* `package-lock.json` in a parent directory). We look for any `web/server.js`
|
|
36
|
-
* below the standalone directory so `kandev start` keeps working.
|
|
37
|
-
*
|
|
38
|
-
* @returns absolute path to `server.js`, or null if it cannot be found.
|
|
39
|
-
*/
|
|
40
|
-
function resolveStandaloneServerPath(repoRoot) {
|
|
41
|
-
const standaloneDir = node_path_1.default.join(repoRoot, "apps", "web", ".next", "standalone");
|
|
42
|
-
const expected = node_path_1.default.join(standaloneDir, "web", "server.js");
|
|
43
|
-
if (node_fs_1.default.existsSync(expected))
|
|
44
|
-
return expected;
|
|
45
|
-
if (!node_fs_1.default.existsSync(standaloneDir))
|
|
46
|
-
return null;
|
|
47
|
-
return findWebServerJs(standaloneDir);
|
|
48
|
-
}
|
|
49
|
-
// Walks `dir` manually instead of relying on `Dirent.parentPath` (Node 20.12+),
|
|
50
|
-
// so the CLI keeps working on older Node 20.x runtimes installed via npx.
|
|
51
|
-
function findWebServerJs(dir) {
|
|
52
|
-
const stack = [dir];
|
|
53
|
-
while (stack.length > 0) {
|
|
54
|
-
const current = stack.pop();
|
|
55
|
-
let entries;
|
|
56
|
-
try {
|
|
57
|
-
entries = node_fs_1.default.readdirSync(current, { withFileTypes: true });
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
for (const entry of entries) {
|
|
63
|
-
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
64
|
-
stack.push(node_path_1.default.join(current, entry.name));
|
|
65
|
-
}
|
|
66
|
-
else if (entry.isFile() && entry.name === "server.js" && node_path_1.default.basename(current) === "web") {
|
|
67
|
-
return node_path_1.default.join(current, entry.name);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Runs the application in production mode using local builds.
|
|
75
|
-
*
|
|
76
|
-
* This function:
|
|
77
|
-
* 1. Validates that build artifacts exist
|
|
78
|
-
* 2. Picks available ports for all services
|
|
79
|
-
* 3. Starts the backend binary (with warn log level for clean output)
|
|
80
|
-
* 4. Starts the web app via `pnpm start`
|
|
81
|
-
* 5. Waits for the backend to be healthy before announcing readiness
|
|
82
|
-
*
|
|
83
|
-
* @param options - Configuration for the start command
|
|
84
|
-
* @throws Error if backend binary or web build is not found
|
|
85
|
-
*/
|
|
86
|
-
async function runStart({ repoRoot, backendPort, webPort, verbose = false, debug = false, headless = false, }) {
|
|
87
|
-
const ports = await (0, shared_1.pickPorts)(backendPort, webPort);
|
|
88
|
-
const backendBin = node_path_1.default.join(repoRoot, "apps", "backend", "bin", (0, platform_1.getBinaryName)("kandev"));
|
|
89
|
-
if (!node_fs_1.default.existsSync(backendBin)) {
|
|
90
|
-
throw new Error("Backend binary not found. Run `make build` first.");
|
|
91
|
-
}
|
|
92
|
-
// Check for standalone build (Next.js standalone output)
|
|
93
|
-
const webServerPath = resolveStandaloneServerPath(repoRoot);
|
|
94
|
-
if (!webServerPath) {
|
|
95
|
-
const standaloneDir = node_path_1.default.join(repoRoot, "apps", "web", ".next", "standalone");
|
|
96
|
-
if (!node_fs_1.default.existsSync(standaloneDir)) {
|
|
97
|
-
throw new Error("Web standalone build not found. Run `make build` first.");
|
|
98
|
-
}
|
|
99
|
-
throw new Error(`Web standalone build is missing server.js under ${standaloneDir}. ` +
|
|
100
|
-
"This can happen when Next.js/Turbopack detects a different project root " +
|
|
101
|
-
"(e.g. a stray package-lock.json in a parent directory). " +
|
|
102
|
-
"Remove the stray lockfile and re-run `make build`.");
|
|
103
|
-
}
|
|
104
|
-
const webStandaloneDir = node_path_1.default.dirname(webServerPath);
|
|
105
|
-
const webStaticDir = node_path_1.default.join(repoRoot, "apps", "web", ".next", "static");
|
|
106
|
-
const standaloneStaticDir = node_path_1.default.join(webStandaloneDir, ".next", "static");
|
|
107
|
-
if (node_fs_1.default.existsSync(webStaticDir) && !node_fs_1.default.existsSync(standaloneStaticDir)) {
|
|
108
|
-
node_fs_1.default.mkdirSync(node_path_1.default.dirname(standaloneStaticDir), { recursive: true });
|
|
109
|
-
try {
|
|
110
|
-
node_fs_1.default.symlinkSync(webStaticDir, standaloneStaticDir, "junction");
|
|
111
|
-
}
|
|
112
|
-
catch (err) {
|
|
113
|
-
console.warn(`[kandev] failed to link Next.js static assets: ${err instanceof Error ? err.message : String(err)}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
// Link public directory (fonts, images, etc.) into standalone output
|
|
117
|
-
const webPublicDir = node_path_1.default.join(repoRoot, "apps", "web", "public");
|
|
118
|
-
const standalonePublicDir = node_path_1.default.join(webStandaloneDir, "public");
|
|
119
|
-
if (node_fs_1.default.existsSync(webPublicDir) && !node_fs_1.default.existsSync(standalonePublicDir)) {
|
|
120
|
-
try {
|
|
121
|
-
node_fs_1.default.symlinkSync(webPublicDir, standalonePublicDir, "junction");
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
console.warn(`[kandev] failed to link public assets: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// Production mode: use warn log level for clean output unless verbose/debug
|
|
128
|
-
const showOutput = verbose || debug;
|
|
129
|
-
node_fs_1.default.mkdirSync((0, constants_1.resolveDataDir)(), { recursive: true });
|
|
130
|
-
// The data dir holds the SQLite DB; keep it owner-only even if it pre-existed
|
|
131
|
-
// with a looser umask-derived mode.
|
|
132
|
-
node_fs_1.default.chmodSync((0, constants_1.resolveDataDir)(), 0o700);
|
|
133
|
-
const dbPath = (0, constants_1.resolveDatabasePath)();
|
|
134
|
-
const logLevel = process.env.KANDEV_LOG_LEVEL?.trim() || (debug ? "debug" : verbose ? "info" : "warn");
|
|
135
|
-
const backendEnv = (0, shared_1.buildBackendEnv)({
|
|
136
|
-
ports,
|
|
137
|
-
logLevel,
|
|
138
|
-
extra: {
|
|
139
|
-
KANDEV_DATABASE_PATH: dbPath,
|
|
140
|
-
...(debug ? { KANDEV_DEBUG_AGENT_MESSAGES: "true", KANDEV_DEBUG_PPROF_ENABLED: "true" } : {}),
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
const webEnv = (0, shared_1.buildWebEnv)({ ports, production: true, debug });
|
|
144
|
-
(0, shared_1.logStartupInfo)({
|
|
145
|
-
header: "start mode: using local build",
|
|
146
|
-
ports,
|
|
147
|
-
dbPath,
|
|
148
|
-
logLevel,
|
|
149
|
-
});
|
|
150
|
-
const supervisor = (0, process_1.createProcessSupervisor)();
|
|
151
|
-
supervisor.attachSignalHandlers();
|
|
152
|
-
// Start backend: ignore stdin, show stdout only in verbose/debug mode, always show stderr
|
|
153
|
-
// Stderr is always inherited to ensure error messages are visible immediately (no pipe buffering)
|
|
154
|
-
const backend = await (0, backend_1.launchRestartableBackend)({
|
|
155
|
-
command: backendBin,
|
|
156
|
-
args: [],
|
|
157
|
-
cwd: node_path_1.default.dirname(backendBin),
|
|
158
|
-
env: backendEnv,
|
|
159
|
-
homeDir: (0, constants_1.resolveKandevHomeDir)(),
|
|
160
|
-
ports,
|
|
161
|
-
mode: "start",
|
|
162
|
-
stdio: showOutput ? ["ignore", "inherit", "inherit"] : ["ignore", "ignore", "inherit"],
|
|
163
|
-
supervisor,
|
|
164
|
-
});
|
|
165
|
-
const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_RELEASE);
|
|
166
|
-
console.log("[kandev] starting backend...");
|
|
167
|
-
await (0, health_1.waitForHealth)(ports.backendUrl, backend.proc, healthTimeoutMs);
|
|
168
|
-
console.log(`[kandev] backend ready at ${ports.backendUrl}`);
|
|
169
|
-
// Use standalone server.js directly (not pnpm start)
|
|
170
|
-
const webUrl = `http://localhost:${ports.webPort}`;
|
|
171
|
-
console.log("[kandev] starting web...");
|
|
172
|
-
const webProc = (0, web_1.launchWebApp)({
|
|
173
|
-
command: "node",
|
|
174
|
-
args: [webServerPath],
|
|
175
|
-
cwd: webStandaloneDir,
|
|
176
|
-
env: webEnv,
|
|
177
|
-
supervisor,
|
|
178
|
-
label: "web",
|
|
179
|
-
quiet: !showOutput,
|
|
180
|
-
});
|
|
181
|
-
await (0, health_1.waitForUrlReady)(webUrl, webProc, healthTimeoutMs);
|
|
182
|
-
console.log("[kandev] open: " + ports.backendUrl);
|
|
183
|
-
if (headless) {
|
|
184
|
-
console.log(`[kandev] ready (headless) at ${ports.backendUrl}`);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
(0, web_1.openBrowser)(ports.backendUrl);
|
|
188
|
-
}
|
|
@@ -1,83 +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.launchRestartableBackend = launchRestartableBackend;
|
|
7
|
-
exports.shouldUseSupervisor = shouldUseSupervisor;
|
|
8
|
-
exports.withSupervisorEnv = withSupervisorEnv;
|
|
9
|
-
const node_child_process_1 = require("node:child_process");
|
|
10
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
-
const shared_1 = require("../shared");
|
|
12
|
-
const manifest_1 = require("./manifest");
|
|
13
|
-
const paths_1 = require("./paths");
|
|
14
|
-
const child_1 = require("./child");
|
|
15
|
-
const control_1 = require("./control");
|
|
16
|
-
async function launchRestartableBackend({ command, args, cwd, env, homeDir, ports, mode, stdio, supervisor, }) {
|
|
17
|
-
if (!shouldUseSupervisor(env)) {
|
|
18
|
-
const proc = (0, node_child_process_1.spawn)(command, args, { cwd, env, stdio });
|
|
19
|
-
supervisor.children.push(proc);
|
|
20
|
-
(0, shared_1.attachBackendExitHandler)(proc, supervisor);
|
|
21
|
-
return { proc, control: null, env };
|
|
22
|
-
}
|
|
23
|
-
const supervisorEnv = withSupervisorEnv(env, homeDir);
|
|
24
|
-
const manifest = (0, manifest_1.buildLaunchManifest)({
|
|
25
|
-
backend_executable: resolveExecutable(command),
|
|
26
|
-
argv: args,
|
|
27
|
-
cwd,
|
|
28
|
-
env: supervisorEnv,
|
|
29
|
-
home_dir: homeDir,
|
|
30
|
-
port: ports.backendPort,
|
|
31
|
-
mode,
|
|
32
|
-
});
|
|
33
|
-
(0, manifest_1.writeLaunchManifest)(manifest, supervisorEnv.KANDEV_SUPERVISOR_MANIFEST);
|
|
34
|
-
const child = (0, child_1.createRestartableChild)(manifest, { stdio, extraEnv: supervisorEnv });
|
|
35
|
-
let restarting = false;
|
|
36
|
-
const attachExit = (proc) => {
|
|
37
|
-
(0, shared_1.attachBackendExitHandler)(proc, supervisor, {
|
|
38
|
-
shouldShutdown: () => !restarting,
|
|
39
|
-
});
|
|
40
|
-
};
|
|
41
|
-
const proc = child.start();
|
|
42
|
-
supervisor.children.push(proc);
|
|
43
|
-
attachExit(proc);
|
|
44
|
-
const control = await (0, control_1.startControlServer)(supervisorEnv.KANDEV_SUPERVISOR_SOCKET, async () => {
|
|
45
|
-
restarting = true;
|
|
46
|
-
try {
|
|
47
|
-
const next = await child.restart();
|
|
48
|
-
supervisor.children.push(next);
|
|
49
|
-
attachExit(next);
|
|
50
|
-
}
|
|
51
|
-
finally {
|
|
52
|
-
restarting = false;
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
return { proc, control, env: supervisorEnv };
|
|
56
|
-
}
|
|
57
|
-
function shouldUseSupervisor(env = process.env) {
|
|
58
|
-
return env.KANDEV_NO_SUPERVISOR !== "true";
|
|
59
|
-
}
|
|
60
|
-
function withSupervisorEnv(env, homeDir) {
|
|
61
|
-
(0, paths_1.prepareSupervisorDir)(homeDir);
|
|
62
|
-
return {
|
|
63
|
-
...env,
|
|
64
|
-
KANDEV_SUPERVISOR_SOCKET: (0, paths_1.socketPath)(homeDir),
|
|
65
|
-
KANDEV_SUPERVISOR_MANIFEST: (0, paths_1.manifestPath)(homeDir),
|
|
66
|
-
KANDEV_RESTART_ADAPTER: "supervisor",
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
function resolveExecutable(command) {
|
|
70
|
-
if (node_path_1.default.isAbsolute(command))
|
|
71
|
-
return command;
|
|
72
|
-
const found = (0, node_child_process_1.spawnSync)(process.platform === "win32" ? "where" : "which", [command], {
|
|
73
|
-
encoding: "utf8",
|
|
74
|
-
});
|
|
75
|
-
const first = found.stdout
|
|
76
|
-
?.split(/\r?\n/)
|
|
77
|
-
.map((line) => line.trim())
|
|
78
|
-
.find(Boolean);
|
|
79
|
-
if (!first) {
|
|
80
|
-
throw new Error(`Unable to resolve executable ${command}`);
|
|
81
|
-
}
|
|
82
|
-
return first;
|
|
83
|
-
}
|
package/dist/supervisor/child.js
DELETED
|
@@ -1,64 +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.createRestartableChild = createRestartableChild;
|
|
7
|
-
const node_child_process_1 = require("node:child_process");
|
|
8
|
-
const tree_kill_1 = __importDefault(require("tree-kill"));
|
|
9
|
-
const RESTART_TIMEOUT_MS = 10000;
|
|
10
|
-
function createRestartableChild(manifest, options = {}) {
|
|
11
|
-
let child = null;
|
|
12
|
-
const start = () => {
|
|
13
|
-
child = (0, node_child_process_1.spawn)(manifest.backend_executable, manifest.argv, {
|
|
14
|
-
cwd: manifest.cwd,
|
|
15
|
-
env: {
|
|
16
|
-
...process.env,
|
|
17
|
-
...manifest.env,
|
|
18
|
-
...options.extraEnv,
|
|
19
|
-
},
|
|
20
|
-
stdio: options.stdio ?? "inherit",
|
|
21
|
-
});
|
|
22
|
-
return child;
|
|
23
|
-
};
|
|
24
|
-
const stop = async () => {
|
|
25
|
-
if (!child?.pid || child.exitCode !== null)
|
|
26
|
-
return;
|
|
27
|
-
await terminate(child, RESTART_TIMEOUT_MS);
|
|
28
|
-
};
|
|
29
|
-
const restart = async () => {
|
|
30
|
-
await stop();
|
|
31
|
-
return start();
|
|
32
|
-
};
|
|
33
|
-
return {
|
|
34
|
-
current: () => child,
|
|
35
|
-
start,
|
|
36
|
-
restart,
|
|
37
|
-
stop,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function terminate(proc, timeoutMs) {
|
|
41
|
-
return new Promise((resolve) => {
|
|
42
|
-
const pid = proc.pid;
|
|
43
|
-
if (!pid) {
|
|
44
|
-
resolve();
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
let done = false;
|
|
48
|
-
const finish = () => {
|
|
49
|
-
if (done)
|
|
50
|
-
return;
|
|
51
|
-
done = true;
|
|
52
|
-
clearTimeout(timeout);
|
|
53
|
-
resolve();
|
|
54
|
-
};
|
|
55
|
-
const timeout = setTimeout(() => {
|
|
56
|
-
(0, tree_kill_1.default)(pid, process.platform === "win32" ? undefined : "SIGKILL", finish);
|
|
57
|
-
}, timeoutMs);
|
|
58
|
-
proc.once("exit", finish);
|
|
59
|
-
(0, tree_kill_1.default)(pid, process.platform === "win32" ? undefined : "SIGTERM", (err) => {
|
|
60
|
-
if (err)
|
|
61
|
-
finish();
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
}
|
|
@@ -1,97 +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.startControlServer = startControlServer;
|
|
7
|
-
exports.requestRestart = requestRestart;
|
|
8
|
-
const node_net_1 = __importDefault(require("node:net"));
|
|
9
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
-
function startControlServer(socket, onRestart) {
|
|
11
|
-
let restartInProgress = false;
|
|
12
|
-
const scheduleRestart = () => {
|
|
13
|
-
if (restartInProgress)
|
|
14
|
-
return false;
|
|
15
|
-
restartInProgress = true;
|
|
16
|
-
setTimeout(() => {
|
|
17
|
-
void onRestart().finally(() => {
|
|
18
|
-
restartInProgress = false;
|
|
19
|
-
});
|
|
20
|
-
}, 0);
|
|
21
|
-
return true;
|
|
22
|
-
};
|
|
23
|
-
if (process.platform !== "win32" && node_fs_1.default.existsSync(socket)) {
|
|
24
|
-
node_fs_1.default.unlinkSync(socket);
|
|
25
|
-
}
|
|
26
|
-
const server = node_net_1.default.createServer((conn) => {
|
|
27
|
-
let buf = "";
|
|
28
|
-
conn.setEncoding("utf8");
|
|
29
|
-
conn.on("data", (chunk) => {
|
|
30
|
-
buf += chunk;
|
|
31
|
-
if (!buf.includes("\n"))
|
|
32
|
-
return;
|
|
33
|
-
const line = buf.slice(0, buf.indexOf("\n"));
|
|
34
|
-
void handleControlLine(line, scheduleRestart)
|
|
35
|
-
.then((resp) => {
|
|
36
|
-
conn.end(`${JSON.stringify(resp)}\n`);
|
|
37
|
-
})
|
|
38
|
-
.catch((err) => {
|
|
39
|
-
conn.end(`${JSON.stringify({
|
|
40
|
-
accepted: false,
|
|
41
|
-
message: err instanceof Error ? err.message : String(err),
|
|
42
|
-
})}\n`);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
return new Promise((resolve, reject) => {
|
|
47
|
-
server.once("error", reject);
|
|
48
|
-
server.listen(socket, () => {
|
|
49
|
-
server.off("error", reject);
|
|
50
|
-
resolve({
|
|
51
|
-
close: () => new Promise((closeResolve, closeReject) => {
|
|
52
|
-
server.close((err) => {
|
|
53
|
-
if (process.platform !== "win32" && node_fs_1.default.existsSync(socket)) {
|
|
54
|
-
node_fs_1.default.unlinkSync(socket);
|
|
55
|
-
}
|
|
56
|
-
if (err)
|
|
57
|
-
closeReject(err);
|
|
58
|
-
else
|
|
59
|
-
closeResolve();
|
|
60
|
-
});
|
|
61
|
-
}),
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
function requestRestart(socket) {
|
|
67
|
-
return new Promise((resolve, reject) => {
|
|
68
|
-
const conn = node_net_1.default.createConnection(socket);
|
|
69
|
-
let buf = "";
|
|
70
|
-
conn.setEncoding("utf8");
|
|
71
|
-
conn.on("connect", () => {
|
|
72
|
-
conn.write(`${JSON.stringify({ action: "restart" })}\n`);
|
|
73
|
-
});
|
|
74
|
-
conn.on("data", (chunk) => {
|
|
75
|
-
buf += chunk;
|
|
76
|
-
});
|
|
77
|
-
conn.on("error", reject);
|
|
78
|
-
conn.on("end", () => {
|
|
79
|
-
try {
|
|
80
|
-
resolve(JSON.parse(buf.trim()));
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
reject(err);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
async function handleControlLine(line, scheduleRestart) {
|
|
89
|
-
const req = JSON.parse(line);
|
|
90
|
-
if (req.action !== "restart") {
|
|
91
|
-
return { accepted: false, message: `unsupported action ${String(req.action)}` };
|
|
92
|
-
}
|
|
93
|
-
if (!scheduleRestart()) {
|
|
94
|
-
return { accepted: false, message: "Restart already in progress" };
|
|
95
|
-
}
|
|
96
|
-
return { accepted: true, message: "Restart accepted" };
|
|
97
|
-
}
|