kandev 0.1.3 → 0.1.5

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 CHANGED
@@ -1,77 +1,56 @@
1
- # Kandev Launcher (npx)
1
+ # Kandev
2
2
 
3
- This package powers `npx kandev` by downloading prebuilt release bundles from GitHub Releases and running them locally. It:
4
- - Detects OS/arch and fetches the matching bundle ZIP.
5
- - Verifies the SHA256 checksum when available.
6
- - Extracts the bundle into `~/.kandev/bin/<version>/<platform>/`.
7
- - Starts the backend binary and waits for the `/health` endpoint.
8
- - Starts the Next.js standalone server with runtime `KANDEV_API_BASE_URL`.
9
- - Uses the latest GitHub Release by default, so runtime bundles update automatically.
10
- It also supports local dev runs from a repo checkout.
3
+ Manage tasks. Orchestrate agents. Review changes. Ship value.
11
4
 
12
- ## Updates
13
-
14
- On `run`, the launcher checks npm for the latest `kandev` CLI version and prompts:
15
-
16
- ```
17
- Update available: <current> -> <latest>. Update now? [y/N]
18
- ```
19
-
20
- If you accept, it re-runs `npx kandev@latest` with the same arguments.
21
- This check is skipped in `dev` mode.
22
-
23
- Note: the runtime bundles are pulled from the latest GitHub Release by default, even if the CLI version is unchanged. So:
24
- - **New runtime release without CLI publish**: users get new runtime automatically, but no update prompt.
25
- - **New CLI publish**: users get an update prompt and then re-run with the new CLI.
26
-
27
- You can disable the prompt with:
5
+ ## Quick Start
28
6
 
29
7
  ```bash
30
- KANDEV_NO_UPDATE_PROMPT=1 npx kandev
8
+ npx kandev
31
9
  ```
32
10
 
33
- ## Usage
11
+ Downloads the latest release, starts the backend + web app, and opens your browser. Data (worktrees, SQLite DB) is stored in `~/.kandev` by default.
34
12
 
35
- ```bash
36
- # Run the latest release bundle
37
- npx kandev
13
+ ## What You Get
38
14
 
39
- # Run a specific release tag
40
- npx kandev run --version v0.1.0
15
+ - **Multi-agent support** - Claude Code, Codex, GitHub Copilot, Gemini CLI, Amp, Auggie, OpenCode
16
+ - **Integrated workspace** - Terminal, code editor with LSP, git changes, browser preview, and chat
17
+ - **Kanban & pipeline views** - Organize tasks with opinionated workflows and gates
18
+ - **CLI passthrough** - Drop into raw agent TUI mode for full terminal access
19
+ - **Workspace isolation** - Git worktrees prevent concurrent agents from conflicting
20
+ - **Session management** - Resume and review agent conversations
41
21
 
42
- # Local dev (from repo root)
43
- npx kandev dev
22
+ ## Supported Agents
44
23
 
24
+ | Agent | Default Model | Protocol |
25
+ | ------------------ | -------------- | ----------- |
26
+ | **Claude Code** | Sonnet 4.5 | stream-json |
27
+ | **Codex** | GPT-5.2 Codex | Codex |
28
+ | **GitHub Copilot** | GPT-4.1 | Copilot SDK |
29
+ | **Gemini CLI** | Gemini 3 Flash | ACP |
30
+ | **Amp** | Smart Mode | stream-json |
31
+ | **Auggie** | Sonnet 4.5 | ACP |
32
+ | **OpenCode** | GPT-5 Nano | REST/SSE |
45
33
 
46
- # Local test the built CLI (from repo root)
47
- pnpm -C apps/cli build
48
- pnpm -C apps/cli start
49
- ```
34
+ > **Beta** - Under active development. Expect rough edges and breaking changes.
50
35
 
51
- ## Local Development
36
+ ## Requirements
52
37
 
53
- ```bash
54
- pnpm -C apps/cli dev
55
- ```
38
+ - Node.js (for `npx`)
39
+ - Git
40
+ - Docker (optional - needed for container runtimes)
56
41
 
57
- ## Build / Publish
42
+ ## Platforms
58
43
 
59
- ```bash
60
- pnpm -C apps/cli build
61
- npm publish --access public
62
- ```
44
+ - macOS (Intel + Apple Silicon)
45
+ - Linux (x64)
46
+ - Windows (x64, WSL)
63
47
 
64
- The published package name is `kandev`, with a bin entry `kandev`.
48
+ ## Learn More
65
49
 
66
- ## Release
50
+ Open source, multi-provider, no telemetry, not tied to any cloud.
67
51
 
68
- ```bash
69
- scripts/release/publish-launcher.sh 0.1.0
70
- ```
52
+ See the [GitHub repository](https://github.com/kdlbs/kandev) for architecture, vision, development setup, and contributing guidelines.
71
53
 
72
- ## Environment Overrides
54
+ ## License
73
55
 
74
- - `KANDEV_GITHUB_OWNER`, `KANDEV_GITHUB_REPO`: Override the GitHub repo to fetch releases from.
75
- - `KANDEV_GITHUB_TOKEN`: Optional token for GitHub API rate limits.
76
- - `KANDEV_NO_UPDATE_PROMPT=1`: Disable the update prompt.
77
- - `KANDEV_SKIP_UPDATE=1`: Internal guard to avoid update loops.
56
+ [AGPL-3.0](https://github.com/kdlbs/kandev/blob/main/LICENSE)
package/dist/bundle.js CHANGED
@@ -1,25 +1,57 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.extractZip = extractZip;
39
+ exports.extractTarGz = extractTarGz;
7
40
  exports.ensureExtracted = ensureExtracted;
8
41
  exports.findBundleRoot = findBundleRoot;
9
42
  exports.resolveWebServerPath = resolveWebServerPath;
10
- const adm_zip_1 = __importDefault(require("adm-zip"));
43
+ const tar = __importStar(require("tar"));
11
44
  const node_fs_1 = __importDefault(require("node:fs"));
12
45
  const node_path_1 = __importDefault(require("node:path"));
13
- function extractZip(zipPath, destDir) {
14
- const zip = new adm_zip_1.default(zipPath);
15
- zip.extractAllTo(destDir, true);
46
+ function extractTarGz(archivePath, destDir) {
47
+ tar.extract({ file: archivePath, cwd: destDir, sync: true });
16
48
  }
17
- function ensureExtracted(zipPath, destDir) {
49
+ function ensureExtracted(archivePath, destDir) {
18
50
  const marker = node_path_1.default.join(destDir, ".extracted");
19
51
  if (node_fs_1.default.existsSync(marker)) {
20
52
  return;
21
53
  }
22
- extractZip(zipPath, destDir);
54
+ extractTarGz(archivePath, destDir);
23
55
  node_fs_1.default.writeFileSync(marker, "");
24
56
  }
25
57
  function findBundleRoot(cacheDir) {
@@ -30,13 +62,19 @@ function findBundleRoot(cacheDir) {
30
62
  return cacheDir;
31
63
  }
32
64
  function resolveWebServerPath(bundleDir) {
33
- const direct = node_path_1.default.join(bundleDir, "web", "server.js");
34
- if (node_fs_1.default.existsSync(direct)) {
35
- return direct;
36
- }
37
- const nested = node_path_1.default.join(bundleDir, "web", "apps", "web", "server.js");
38
- if (node_fs_1.default.existsSync(nested)) {
39
- return nested;
65
+ // Next.js standalone output location depends on workspace structure:
66
+ // - non-monorepo: web/server.js
67
+ // - monorepo (apps/ root): web/web/server.js
68
+ // - monorepo (project root): web/apps/web/server.js
69
+ const candidates = [
70
+ node_path_1.default.join(bundleDir, "web", "server.js"),
71
+ node_path_1.default.join(bundleDir, "web", "web", "server.js"),
72
+ node_path_1.default.join(bundleDir, "web", "apps", "web", "server.js"),
73
+ ];
74
+ for (const candidate of candidates) {
75
+ if (node_fs_1.default.existsSync(candidate)) {
76
+ return candidate;
77
+ }
40
78
  }
41
79
  return null;
42
80
  }
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const package_json_1 = __importDefault(require("../package.json"));
9
9
  const dev_1 = require("./dev");
10
10
  const run_1 = require("./run");
11
+ const start_1 = require("./start");
11
12
  const ports_1 = require("./ports");
12
13
  const update_1 = require("./update");
13
14
  function printHelp() {
@@ -16,6 +17,7 @@ function printHelp() {
16
17
  Usage:
17
18
  kandev run [--version <tag>] [--backend-port <port>] [--web-port <port>]
18
19
  kandev dev [--backend-port <port>] [--web-port <port>]
20
+ kandev start [--backend-port <port>] [--web-port <port>] [--verbose] [--debug]
19
21
  kandev [--version <tag>] [--backend-port <port>] [--web-port <port>]
20
22
  kandev --dev [--backend-port <port>] [--web-port <port>]
21
23
 
@@ -24,16 +26,20 @@ Examples:
24
26
  kandev run
25
27
  kandev --dev
26
28
  kandev dev
29
+ kandev start
27
30
  kandev --version v0.1.0
28
31
  kandev --backend-port 18080 --web-port 13000
29
32
 
30
33
  Options:
31
34
  dev Use local repo for dev (make dev + next dev) if available.
35
+ start Use local production build (make build + next start).
32
36
  run Use release bundles (default).
33
37
  --dev Alias for "dev".
34
38
  --version Release tag to install (default: latest).
35
39
  --backend-port Override backend port.
36
40
  --web-port Override web port.
41
+ --verbose, -v Show info logs from backend + web (start mode only).
42
+ --debug Show debug logs + agent message dumps (start mode only).
37
43
  --help, -h Show help.
38
44
  `);
39
45
  }
@@ -45,7 +51,7 @@ function parseArgs(argv) {
45
51
  printHelp();
46
52
  process.exit(0);
47
53
  }
48
- if (arg === "dev" || arg === "run") {
54
+ if (arg === "dev" || arg === "run" || arg === "start") {
49
55
  opts.command = arg;
50
56
  continue;
51
57
  }
@@ -80,6 +86,14 @@ function parseArgs(argv) {
80
86
  opts.webPort = Number(arg.split("=")[1]);
81
87
  continue;
82
88
  }
89
+ if (arg === "--verbose" || arg === "-v") {
90
+ opts.verbose = true;
91
+ continue;
92
+ }
93
+ if (arg === "--debug") {
94
+ opts.debug = true;
95
+ continue;
96
+ }
83
97
  }
84
98
  return opts;
85
99
  }
@@ -117,6 +131,14 @@ async function main() {
117
131
  await (0, dev_1.runDev)({ repoRoot, backendPort, webPort });
118
132
  return;
119
133
  }
134
+ if (raw.command === "start") {
135
+ const repoRoot = findRepoRoot(process.cwd());
136
+ if (!repoRoot) {
137
+ throw new Error("Unable to locate repo root for start. Run from the repo.");
138
+ }
139
+ await (0, start_1.runStart)({ repoRoot, backendPort, webPort, verbose: raw.verbose, debug: raw.debug });
140
+ return;
141
+ }
120
142
  await (0, update_1.maybePromptForUpdate)(package_json_1.default.version, process.argv.slice(2));
121
143
  await (0, run_1.runRelease)({ version: raw.version, backendPort, webPort });
122
144
  }
package/dist/constants.js CHANGED
@@ -3,19 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DATA_DIR = exports.CACHE_DIR = exports.HEALTH_TIMEOUT_MS = exports.RANDOM_PORT_RETRIES = exports.RANDOM_PORT_MAX = exports.RANDOM_PORT_MIN = exports.DEFAULT_AGENTCTL_PORT = exports.DEFAULT_WEB_PORT = exports.DEFAULT_BACKEND_PORT = void 0;
6
+ exports.DATA_DIR = exports.CACHE_DIR = exports.HEALTH_TIMEOUT_MS_DEV = exports.HEALTH_TIMEOUT_MS_RELEASE = exports.RANDOM_PORT_RETRIES = exports.RANDOM_PORT_MAX = exports.RANDOM_PORT_MIN = exports.DEFAULT_MCP_PORT = exports.DEFAULT_AGENTCTL_PORT = exports.DEFAULT_WEB_PORT = exports.DEFAULT_BACKEND_PORT = void 0;
7
7
  const node_os_1 = __importDefault(require("node:os"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  // Default service ports (will auto-fallback if busy).
10
10
  exports.DEFAULT_BACKEND_PORT = 8080;
11
11
  exports.DEFAULT_WEB_PORT = 3000;
12
12
  exports.DEFAULT_AGENTCTL_PORT = 9999;
13
+ exports.DEFAULT_MCP_PORT = 9090;
13
14
  // Random fallback range for port selection.
14
15
  exports.RANDOM_PORT_MIN = 10000;
15
16
  exports.RANDOM_PORT_MAX = 60000;
16
17
  exports.RANDOM_PORT_RETRIES = 10;
17
18
  // Backend healthcheck timeout during startup.
18
- exports.HEALTH_TIMEOUT_MS = 15000;
19
+ exports.HEALTH_TIMEOUT_MS_RELEASE = 15000;
20
+ exports.HEALTH_TIMEOUT_MS_DEV = 60000;
19
21
  // Local user cache/data directories for release bundles and DB.
20
22
  exports.CACHE_DIR = node_path_1.default.join(node_os_1.default.homedir(), ".kandev", "bin");
21
23
  exports.DATA_DIR = node_path_1.default.join(node_os_1.default.homedir(), ".kandev", "data");
package/dist/dev.js CHANGED
@@ -8,42 +8,27 @@ const node_child_process_1 = require("node:child_process");
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const constants_1 = require("./constants");
10
10
  const health_1 = require("./health");
11
- const ports_1 = require("./ports");
12
11
  const process_1 = require("./process");
12
+ const shared_1 = require("./shared");
13
13
  const web_1 = require("./web");
14
14
  async function runDev({ repoRoot, backendPort, webPort }) {
15
- const actualBackendPort = backendPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_BACKEND_PORT));
16
- const actualWebPort = webPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_WEB_PORT));
17
- const agentctlPort = await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_AGENTCTL_PORT);
18
- const backendUrl = `http://localhost:${actualBackendPort}`;
19
- const backendEnv = {
20
- ...process.env,
21
- KANDEV_SERVER_PORT: String(actualBackendPort),
22
- KANDEV_AGENT_STANDALONE_PORT: String(agentctlPort),
23
- };
24
- const webEnv = {
25
- ...process.env,
26
- KANDEV_API_BASE_URL: backendUrl,
27
- NEXT_PUBLIC_KANDEV_API_BASE_URL: backendUrl,
28
- PORT: String(actualWebPort),
29
- NEXT_PUBLIC_KANDEV_DEBUG: "true",
30
- };
31
- console.log("[kandev] dev mode: using local repo");
32
- console.log("[kandev] backend port:", actualBackendPort);
33
- console.log("[kandev] web port:", actualWebPort);
34
- console.log("[kandev] agentctl port:", agentctlPort);
15
+ const ports = await (0, shared_1.pickPorts)(backendPort, webPort);
16
+ const backendEnv = (0, shared_1.buildBackendEnv)({ ports, extra: { KANDEV_MOCK_AGENT: "true" } });
17
+ const webEnv = (0, shared_1.buildWebEnv)({ ports, includeMcp: true, debug: true });
18
+ (0, shared_1.logPortConfig)("dev", "using local repo", ports, true);
35
19
  const supervisor = (0, process_1.createProcessSupervisor)();
36
20
  supervisor.attachSignalHandlers();
37
- const backendProc = (0, node_child_process_1.spawn)("make", ["-C", node_path_1.default.join("apps", "backend"), "dev"], { cwd: repoRoot, env: backendEnv, stdio: "inherit" });
38
- supervisor.children.push(backendProc);
39
- backendProc.on("exit", (code, signal) => {
40
- console.error(`[kandev] backend exited (code=${code}, signal=${signal})`);
41
- const exitCode = signal ? 0 : code ?? 1;
42
- void supervisor.shutdown("backend exit").then(() => process.exit(exitCode));
21
+ const backendProc = (0, node_child_process_1.spawn)("make", ["-C", node_path_1.default.join("apps", "backend"), "dev"], {
22
+ cwd: repoRoot,
23
+ env: backendEnv,
24
+ stdio: "inherit",
43
25
  });
44
- await (0, health_1.waitForHealth)(backendUrl, backendProc, constants_1.HEALTH_TIMEOUT_MS);
45
- console.log(`[kandev] backend ready at ${backendUrl}`);
46
- const webUrl = `http://localhost:${actualWebPort}`;
26
+ supervisor.children.push(backendProc);
27
+ (0, shared_1.attachBackendExitHandler)(backendProc, supervisor);
28
+ const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_DEV);
29
+ await (0, health_1.waitForHealth)(ports.backendUrl, backendProc, healthTimeoutMs);
30
+ console.log(`[kandev] backend ready at ${ports.backendUrl}`);
31
+ const webUrl = `http://localhost:${ports.webPort}`;
47
32
  (0, web_1.launchWebApp)({
48
33
  command: "pnpm",
49
34
  args: ["-C", "apps", "--filter", "@kandev/web", "dev"],
package/dist/github.js CHANGED
@@ -10,7 +10,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
10
10
  const node_https_1 = __importDefault(require("node:https"));
11
11
  const node_path_1 = __importDefault(require("node:path"));
12
12
  // Allow overriding the GitHub repo for forks/testing.
13
- const OWNER = process.env.KANDEV_GITHUB_OWNER || "kandev";
13
+ const OWNER = process.env.KANDEV_GITHUB_OWNER || "kdlbs";
14
14
  const REPO = process.env.KANDEV_GITHUB_REPO || "kandev";
15
15
  const API_BASE = `https://api.github.com/repos/${OWNER}/${REPO}`;
16
16
  function requestJson(url) {
@@ -55,15 +55,21 @@ function downloadFile(url, destPath, expectedSha256, onProgress) {
55
55
  }
56
56
  catch { }
57
57
  };
58
- const req = node_https_1.default.get(url, {
59
- headers: {
60
- "User-Agent": "kandev-npx",
61
- Accept: "application/octet-stream",
62
- ...(process.env.KANDEV_GITHUB_TOKEN
63
- ? { Authorization: `Bearer ${process.env.KANDEV_GITHUB_TOKEN}` }
64
- : {}),
65
- },
66
- }, (res) => {
58
+ const handleResponse = (res) => {
59
+ // Follow redirects (GitHub API returns 302 to signed S3 URL).
60
+ // Strip auth header on redirect to avoid S3 rejecting it.
61
+ if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
62
+ const redirectReq = node_https_1.default.get(res.headers.location, { headers: { "User-Agent": "kandev-npx" } }, handleResponse);
63
+ redirectReq.setTimeout(30000, () => {
64
+ redirectReq.destroy(new Error(`Request timed out downloading ${url}`));
65
+ });
66
+ redirectReq.on("error", (err) => {
67
+ file.close();
68
+ cleanup();
69
+ reject(err);
70
+ });
71
+ return;
72
+ }
67
73
  if (res.statusCode !== 200) {
68
74
  file.close();
69
75
  cleanup();
@@ -94,8 +100,17 @@ function downloadFile(url, destPath, expectedSha256, onProgress) {
94
100
  reject(err);
95
101
  }
96
102
  });
97
- });
98
- req.setTimeout(5000, () => {
103
+ };
104
+ const req = node_https_1.default.get(url, {
105
+ headers: {
106
+ "User-Agent": "kandev-npx",
107
+ Accept: "application/octet-stream",
108
+ ...(process.env.KANDEV_GITHUB_TOKEN
109
+ ? { Authorization: `Bearer ${process.env.KANDEV_GITHUB_TOKEN}` }
110
+ : {}),
111
+ },
112
+ }, handleResponse);
113
+ req.setTimeout(30000, () => {
99
114
  req.destroy(new Error(`Request timed out downloading ${url}`));
100
115
  });
101
116
  req.on("error", (err) => {
@@ -114,7 +129,7 @@ function readSha256(pathToSha) {
114
129
  }
115
130
  const content = node_fs_1.default.readFileSync(pathToSha, "utf8").trim();
116
131
  const first = content.split(/\s+/)[0];
117
- return first || null;
132
+ return first?.toLowerCase() || null;
118
133
  }
119
134
  async function getRelease(version) {
120
135
  if (version) {
@@ -134,7 +149,7 @@ async function ensureAsset(release, assetName, cacheDir, onProgress) {
134
149
  if (!expectedSha) {
135
150
  const shaAsset = findAsset(release, `${assetName}.sha256`);
136
151
  if (shaAsset) {
137
- await downloadFile(shaAsset.browser_download_url, shaPath);
152
+ await downloadFile(shaAsset.url, shaPath);
138
153
  expectedSha = readSha256(shaPath);
139
154
  }
140
155
  }
@@ -150,6 +165,6 @@ async function ensureAsset(release, assetName, cacheDir, onProgress) {
150
165
  }
151
166
  node_fs_1.default.unlinkSync(destPath);
152
167
  }
153
- await downloadFile(asset.browser_download_url, destPath, expectedSha, onProgress);
168
+ await downloadFile(asset.url, destPath, expectedSha, onProgress);
154
169
  return destPath;
155
170
  }
package/dist/health.js CHANGED
@@ -1,10 +1,32 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.delay = delay;
4
+ exports.resolveHealthTimeoutMs = resolveHealthTimeoutMs;
4
5
  exports.waitForHealth = waitForHealth;
5
6
  function delay(ms) {
6
7
  return new Promise((resolve) => setTimeout(resolve, ms));
7
8
  }
9
+ /**
10
+ * Resolves the health check timeout, allowing override via environment variable.
11
+ *
12
+ * The KANDEV_HEALTH_TIMEOUT_MS environment variable can override the default
13
+ * timeout for waiting on backend health checks. This is useful for slower
14
+ * machines or debugging scenarios where the backend takes longer to start.
15
+ *
16
+ * @param defaultMs - Default timeout in milliseconds if env var is not set
17
+ * @returns The resolved timeout in milliseconds
18
+ */
19
+ function resolveHealthTimeoutMs(defaultMs) {
20
+ const raw = process.env.KANDEV_HEALTH_TIMEOUT_MS;
21
+ if (!raw) {
22
+ return defaultMs;
23
+ }
24
+ const parsed = Number(raw);
25
+ if (!Number.isFinite(parsed) || parsed <= 0) {
26
+ return defaultMs;
27
+ }
28
+ return Math.floor(parsed);
29
+ }
8
30
  async function waitForHealth(baseUrl, proc, timeoutMs) {
9
31
  const deadline = Date.now() + timeoutMs;
10
32
  const healthUrl = `${baseUrl}/health`;
package/dist/platform.js CHANGED
@@ -38,13 +38,16 @@ function getPlatformDir() {
38
38
  const arch = getEffectiveArch();
39
39
  if (platform === "linux" && arch === "x64")
40
40
  return "linux-x64";
41
+ if (platform === "linux" && arch === "arm64")
42
+ return "linux-arm64";
41
43
  if (platform === "darwin" && arch === "x64")
42
44
  return "macos-x64";
43
45
  if (platform === "darwin" && arch === "arm64")
44
46
  return "macos-arm64";
45
47
  if (platform === "win32" && arch === "x64")
46
48
  return "windows-x64";
49
+ // Windows ARM64 runs x64 binaries via emulation — no native arm64 build yet
47
50
  if (platform === "win32" && arch === "arm64")
48
- return "windows-arm64";
51
+ return "windows-x64";
49
52
  throw new Error(`Unsupported platform: ${platform}-${arch}`);
50
53
  }
package/dist/ports.js CHANGED
@@ -18,23 +18,39 @@ function ensureValidPort(port, name) {
18
18
  }
19
19
  return port;
20
20
  }
21
- function isPortAvailable(port, host = "0.0.0.0") {
21
+ /**
22
+ * Tries to connect to a port on the given host. Returns true if something
23
+ * is already listening (i.e. the port is in use).
24
+ *
25
+ * This is more reliable than a bind-based check on macOS where
26
+ * SO_REUSEADDR (set by default in Node.js) can allow a bind to succeed
27
+ * even when another process is already listening on the same port.
28
+ */
29
+ function isPortInUse(port, host) {
22
30
  return new Promise((resolve) => {
23
- const server = node_net_1.default.createServer();
24
- server.unref();
25
- server.on("error", (err) => {
26
- if (err.code === "EADDRINUSE" || err.code === "EACCES") {
27
- resolve(false);
28
- }
29
- else {
30
- resolve(false);
31
- }
31
+ const socket = node_net_1.default.createConnection({ port, host });
32
+ socket.once("connect", () => {
33
+ socket.destroy();
34
+ resolve(true);
32
35
  });
33
- server.listen(port, host, () => {
34
- server.close(() => resolve(true));
36
+ socket.once("error", () => {
37
+ resolve(false);
35
38
  });
36
39
  });
37
40
  }
41
+ /**
42
+ * Checks if a port is available by probing both IPv4 and IPv6 loopback.
43
+ *
44
+ * Uses a connect-based check: if we can connect to the port on either
45
+ * 127.0.0.1 or ::1, something is already listening and the port is taken.
46
+ */
47
+ async function isPortAvailable(port) {
48
+ const [v4InUse, v6InUse] = await Promise.all([
49
+ isPortInUse(port, "127.0.0.1"),
50
+ isPortInUse(port, "::1"),
51
+ ]);
52
+ return !v4InUse && !v6InUse;
53
+ }
38
54
  async function reserveSpecificPort(port, host = "127.0.0.1") {
39
55
  return new Promise((resolve) => {
40
56
  const server = node_net_1.default.createServer();
@@ -55,15 +71,19 @@ async function pickAvailablePort(preferred, retries = constants_1.RANDOM_PORT_RE
55
71
  throw new Error(`Unable to find a free port after ${retries + 1} attempts`);
56
72
  }
57
73
  async function pickAndReservePort(preferred, retries = constants_1.RANDOM_PORT_RETRIES) {
58
- const reservedPreferred = await reserveSpecificPort(preferred);
59
- if (reservedPreferred) {
60
- return {
61
- port: preferred,
62
- release: () => new Promise((resolve) => reservedPreferred.close(() => resolve())),
63
- };
74
+ if (await isPortAvailable(preferred)) {
75
+ const reservedPreferred = await reserveSpecificPort(preferred);
76
+ if (reservedPreferred) {
77
+ return {
78
+ port: preferred,
79
+ release: () => new Promise((resolve) => reservedPreferred.close(() => resolve())),
80
+ };
81
+ }
64
82
  }
65
83
  for (let i = 0; i < retries; i += 1) {
66
84
  const candidate = node_crypto_1.default.randomInt(constants_1.RANDOM_PORT_MIN, constants_1.RANDOM_PORT_MAX + 1);
85
+ if (!(await isPortAvailable(candidate)))
86
+ continue;
67
87
  const reserved = await reserveSpecificPort(candidate);
68
88
  if (reserved) {
69
89
  return {
package/dist/process.js CHANGED
@@ -5,24 +5,85 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createProcessSupervisor = createProcessSupervisor;
7
7
  const tree_kill_1 = __importDefault(require("tree-kill"));
8
+ const SHUTDOWN_TIMEOUT_MS = 10000;
8
9
  function createProcessSupervisor() {
9
- let shuttingDown = false;
10
+ let shutdownPromise = null;
10
11
  const children = [];
11
12
  const shutdown = async (reason) => {
12
- if (shuttingDown)
13
- return;
14
- shuttingDown = true;
13
+ // If already shutting down, wait for the existing shutdown to complete
14
+ if (shutdownPromise) {
15
+ return shutdownPromise;
16
+ }
15
17
  console.log(`[kandev] shutting down (${reason})...`);
16
- await Promise.all(children
18
+ // Wait for all child processes to actually exit, not just for signal to be sent
19
+ shutdownPromise = Promise.all(children
17
20
  .filter((child) => child.pid)
18
- .map((child) => new Promise((resolve) => (0, tree_kill_1.default)(child.pid, "SIGTERM", () => resolve()))));
21
+ .map((child) => waitForProcessExit(child, SHUTDOWN_TIMEOUT_MS))).then(() => { });
22
+ return shutdownPromise;
19
23
  };
20
24
  const onSignal = (signal) => {
21
25
  void shutdown(`signal ${signal}`).then(() => process.exit(0));
22
26
  };
23
27
  const attachSignalHandlers = () => {
24
28
  process.on("SIGINT", onSignal);
25
- process.on("SIGTERM", onSignal);
29
+ // SIGTERM is not available on Windows — only attach where supported
30
+ if (process.platform !== "win32") {
31
+ process.on("SIGTERM", onSignal);
32
+ }
26
33
  };
27
34
  return { children, shutdown, attachSignalHandlers };
28
35
  }
36
+ /**
37
+ * Terminate a process and wait for it to exit.
38
+ * On Unix: sends SIGTERM, falls back to SIGKILL after timeout.
39
+ * On Windows: tree-kill uses taskkill (no SIGTERM/SIGKILL distinction).
40
+ */
41
+ function waitForProcessExit(child, timeoutMs) {
42
+ const isWindows = process.platform === "win32";
43
+ return new Promise((resolve) => {
44
+ const pid = child.pid;
45
+ // Check if this is a ChildProcess with exit event support
46
+ const proc = child;
47
+ const hasExitEvent = typeof proc.on === "function" && typeof proc.exitCode !== "undefined";
48
+ // If process already exited, resolve immediately
49
+ if (hasExitEvent && proc.exitCode !== null) {
50
+ resolve();
51
+ return;
52
+ }
53
+ let resolved = false;
54
+ const done = () => {
55
+ if (resolved)
56
+ return;
57
+ resolved = true;
58
+ resolve();
59
+ };
60
+ // Set up timeout for force-kill fallback
61
+ const timeout = setTimeout(() => {
62
+ console.log(`[kandev] process ${pid} did not exit in time, force killing`);
63
+ // On Windows, tree-kill always force-kills (no signal distinction)
64
+ // On Unix, escalate to SIGKILL
65
+ (0, tree_kill_1.default)(pid, isWindows ? undefined : "SIGKILL", done);
66
+ }, timeoutMs);
67
+ // Listen for exit event if available
68
+ if (hasExitEvent) {
69
+ proc.once("exit", () => {
70
+ clearTimeout(timeout);
71
+ done();
72
+ });
73
+ }
74
+ // Graceful termination: SIGTERM on Unix, default kill on Windows
75
+ (0, tree_kill_1.default)(pid, isWindows ? undefined : "SIGTERM", (err) => {
76
+ // If kill fails (process already gone), we're done
77
+ if (err) {
78
+ clearTimeout(timeout);
79
+ done();
80
+ }
81
+ // If no exit event support, resolve after a brief delay
82
+ // (tree-kill callback fires when signal sent, not when process exits)
83
+ if (!hasExitEvent) {
84
+ // For non-ChildProcess objects, we can't wait for exit event
85
+ // Just wait for the timeout
86
+ }
87
+ });
88
+ });
89
+ }
package/dist/run.js CHANGED
@@ -15,21 +15,22 @@ const health_1 = require("./health");
15
15
  const platform_1 = require("./platform");
16
16
  const ports_1 = require("./ports");
17
17
  const process_1 = require("./process");
18
+ const shared_1 = require("./shared");
18
19
  const web_1 = require("./web");
19
20
  async function prepareReleaseBundle({ version, backendPort, webPort, }) {
20
21
  const platformDir = (0, platform_1.getPlatformDir)();
21
22
  const release = await (0, github_1.getRelease)(version);
22
23
  const tag = release.tag_name || "latest";
23
- const assetName = `kandev-${platformDir}.zip`;
24
+ const assetName = `kandev-${platformDir}.tar.gz`;
24
25
  const cacheDir = node_path_1.default.join(constants_2.CACHE_DIR, tag, platformDir);
25
- const zipPath = await (0, github_1.ensureAsset)(release, assetName, cacheDir, (downloaded, total) => {
26
+ const archivePath = await (0, github_1.ensureAsset)(release, assetName, cacheDir, (downloaded, total) => {
26
27
  const percent = total ? Math.round((downloaded / total) * 100) : 0;
27
28
  const mb = (downloaded / (1024 * 1024)).toFixed(1);
28
29
  const totalMb = total ? (total / (1024 * 1024)).toFixed(1) : "?";
29
30
  process.stderr.write(`\r Downloading: ${mb}MB / ${totalMb}MB (${percent}%)`);
30
31
  });
31
32
  process.stderr.write("\n");
32
- (0, bundle_1.ensureExtracted)(zipPath, cacheDir);
33
+ (0, bundle_1.ensureExtracted)(archivePath, cacheDir);
33
34
  const bundleDir = (0, bundle_1.findBundleRoot)(cacheDir);
34
35
  const backendBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("kandev"));
35
36
  if (!node_fs_1.default.existsSync(backendBin)) {
@@ -45,19 +46,23 @@ async function prepareReleaseBundle({ version, backendPort, webPort, }) {
45
46
  const backendUrl = `http://localhost:${actualBackendPort}`;
46
47
  node_fs_1.default.mkdirSync(constants_1.DATA_DIR, { recursive: true });
47
48
  const dbPath = node_path_1.default.join(constants_1.DATA_DIR, "kandev.db");
49
+ // Note: Release mode doesn't configure MCP server ports as it uses
50
+ // the bundled configuration. Only backend and agentctl ports are set.
51
+ // Log level is set to warn for clean production output.
48
52
  const backendEnv = {
49
53
  ...process.env,
50
54
  KANDEV_SERVER_PORT: String(actualBackendPort),
51
55
  KANDEV_AGENT_STANDALONE_PORT: String(agentctlPort),
52
- KANDEV_DB_PATH: dbPath,
56
+ KANDEV_DATABASE_PATH: dbPath,
57
+ KANDEV_LOG_LEVEL: "warn",
53
58
  };
54
59
  const webEnv = {
55
60
  ...process.env,
56
61
  KANDEV_API_BASE_URL: backendUrl,
57
62
  NEXT_PUBLIC_KANDEV_API_BASE_URL: backendUrl,
58
63
  PORT: String(actualWebPort),
59
- NODE_ENV: "production",
60
64
  };
65
+ webEnv.NODE_ENV = "production";
61
66
  return {
62
67
  bundleDir,
63
68
  backendBin,
@@ -80,16 +85,13 @@ function launchReleaseApps(prepared) {
80
85
  stdio: "inherit",
81
86
  });
82
87
  supervisor.children.push(backendProc);
83
- backendProc.on("exit", (code, signal) => {
84
- console.error(`[kandev] backend exited (code=${code}, signal=${signal})`);
85
- void supervisor.shutdown("backend exit").then(() => process.exit(code || 1));
86
- });
88
+ (0, shared_1.attachBackendExitHandler)(backendProc, supervisor);
87
89
  const webServerPath = (0, bundle_1.resolveWebServerPath)(prepared.bundleDir);
88
90
  if (!webServerPath) {
89
91
  throw new Error("Web server entry (server.js) not found in bundle");
90
92
  }
91
93
  const webUrl = `http://localhost:${prepared.webPort}`;
92
- const webProc = (0, web_1.launchWebApp)({
94
+ (0, web_1.launchWebApp)({
93
95
  command: "node",
94
96
  args: [webServerPath],
95
97
  cwd: node_path_1.default.dirname(webServerPath),
@@ -104,7 +106,8 @@ async function runRelease({ version, backendPort, webPort }) {
104
106
  const prepared = await prepareReleaseBundle({ version, backendPort, webPort });
105
107
  const { backendProc } = launchReleaseApps(prepared);
106
108
  // Wait for backend before announcing the web URL.
107
- await (0, health_1.waitForHealth)(prepared.backendUrl, backendProc, constants_1.HEALTH_TIMEOUT_MS);
109
+ const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_RELEASE);
110
+ await (0, health_1.waitForHealth)(prepared.backendUrl, backendProc, healthTimeoutMs);
108
111
  console.log(`[kandev] backend ready at ${prepared.backendUrl}`);
109
112
  console.log(`[kandev] web ready at http://localhost:${prepared.webPort}`);
110
113
  }
package/dist/shared.js ADDED
@@ -0,0 +1,120 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.pickPorts = pickPorts;
10
+ exports.buildBackendEnv = buildBackendEnv;
11
+ exports.buildWebEnv = buildWebEnv;
12
+ exports.logPortConfig = logPortConfig;
13
+ exports.attachBackendExitHandler = attachBackendExitHandler;
14
+ const constants_1 = require("./constants");
15
+ const ports_1 = require("./ports");
16
+ /**
17
+ * Picks available ports for all services, using provided values or finding free ports.
18
+ *
19
+ * @param backendPort - Optional preferred backend port
20
+ * @param webPort - Optional preferred web port
21
+ * @returns Resolved ports for all services
22
+ */
23
+ async function pickPorts(backendPort, webPort) {
24
+ const resolvedBackendPort = backendPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_BACKEND_PORT));
25
+ const resolvedWebPort = webPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_WEB_PORT));
26
+ const agentctlPort = await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_AGENTCTL_PORT);
27
+ const mcpPort = await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_MCP_PORT);
28
+ return {
29
+ backendPort: resolvedBackendPort,
30
+ webPort: resolvedWebPort,
31
+ agentctlPort,
32
+ mcpPort,
33
+ backendUrl: `http://localhost:${resolvedBackendPort}`,
34
+ mcpUrl: `http://localhost:${mcpPort}/sse`,
35
+ };
36
+ }
37
+ /**
38
+ * Builds environment variables for the backend process.
39
+ *
40
+ * @param options - Configuration options for the backend environment
41
+ * @returns Environment object for the backend process
42
+ */
43
+ function buildBackendEnv(options) {
44
+ const { ports, logLevel, extra } = options;
45
+ return {
46
+ ...process.env,
47
+ KANDEV_SERVER_PORT: String(ports.backendPort),
48
+ KANDEV_AGENT_STANDALONE_PORT: String(ports.agentctlPort),
49
+ KANDEV_AGENT_MCP_SERVER_PORT: String(ports.mcpPort),
50
+ ...(logLevel ? { KANDEV_LOG_LEVEL: logLevel } : {}),
51
+ ...extra,
52
+ };
53
+ }
54
+ /**
55
+ * Builds environment variables for the web process.
56
+ *
57
+ * @param options - Configuration options for the web environment
58
+ * @returns Environment object for the web process
59
+ */
60
+ function buildWebEnv(options) {
61
+ const { ports, includeMcp = false, production = false, debug = false } = options;
62
+ // Server-side env vars use localhost (SSR runs on same machine as backend)
63
+ // Client-side uses NEXT_PUBLIC_*_PORT to build URLs dynamically from window.location.hostname
64
+ // This allows accessing the app from any device (iPhone, Tailscale, etc.)
65
+ const env = {
66
+ ...process.env,
67
+ // Server-side: full localhost URL for SSR
68
+ KANDEV_API_BASE_URL: ports.backendUrl,
69
+ // Client-side: only pass ports, client builds URL from current hostname
70
+ NEXT_PUBLIC_KANDEV_API_PORT: String(ports.backendPort),
71
+ PORT: String(ports.webPort),
72
+ };
73
+ if (includeMcp) {
74
+ env.KANDEV_MCP_SERVER_URL = ports.mcpUrl;
75
+ env.NEXT_PUBLIC_KANDEV_MCP_PORT = String(ports.mcpPort);
76
+ }
77
+ if (production) {
78
+ env.NODE_ENV = "production";
79
+ }
80
+ if (debug) {
81
+ env.NEXT_PUBLIC_KANDEV_DEBUG = "true";
82
+ }
83
+ return env;
84
+ }
85
+ /**
86
+ * Logs port configuration to the console.
87
+ *
88
+ * @param mode - The launch mode name (e.g., "dev", "production", "release")
89
+ * @param modeDescription - Human-readable description of the mode
90
+ * @param ports - Port configuration to log
91
+ * @param includeMcp - Whether to log MCP-related ports
92
+ */
93
+ function logPortConfig(mode, modeDescription, ports, includeMcp = false) {
94
+ console.log(`[kandev] ${mode} mode: ${modeDescription}`);
95
+ console.log("[kandev] backend port:", ports.backendPort);
96
+ console.log("[kandev] web port:", ports.webPort);
97
+ console.log("[kandev] agentctl port:", ports.agentctlPort);
98
+ if (includeMcp) {
99
+ console.log("[kandev] mcp port:", ports.mcpPort);
100
+ console.log("[kandev] mcp url:", ports.mcpUrl);
101
+ }
102
+ }
103
+ /**
104
+ * Attaches a standardized exit handler to a backend process.
105
+ *
106
+ * When the backend exits, this handler logs the exit reason and triggers
107
+ * a graceful shutdown of all supervised processes. If the process was
108
+ * killed by a signal, it exits with code 0; otherwise it uses the
109
+ * process exit code (defaulting to 1).
110
+ *
111
+ * @param backendProc - The backend child process
112
+ * @param supervisor - The process supervisor managing child processes
113
+ */
114
+ function attachBackendExitHandler(backendProc, supervisor) {
115
+ backendProc.on("exit", (code, signal) => {
116
+ console.error(`[kandev] backend exited (code=${code}, signal=${signal})`);
117
+ const exitCode = signal ? 0 : (code ?? 1);
118
+ void supervisor.shutdown("backend exit").then(() => process.exit(exitCode));
119
+ });
120
+ }
package/dist/start.js ADDED
@@ -0,0 +1,124 @@
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.runStart = runStart;
19
+ const node_child_process_1 = require("node:child_process");
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 web_1 = require("./web");
28
+ /**
29
+ * Runs the application in production mode using local builds.
30
+ *
31
+ * This function:
32
+ * 1. Validates that build artifacts exist
33
+ * 2. Picks available ports for all services
34
+ * 3. Starts the backend binary (with warn log level for clean output)
35
+ * 4. Starts the web app via `pnpm start`
36
+ * 5. Waits for the backend to be healthy before announcing readiness
37
+ *
38
+ * @param options - Configuration for the start command
39
+ * @throws Error if backend binary or web build is not found
40
+ */
41
+ async function runStart({ repoRoot, backendPort, webPort, verbose = false, debug = false, }) {
42
+ const ports = await (0, shared_1.pickPorts)(backendPort, webPort);
43
+ const backendBin = node_path_1.default.join(repoRoot, "apps", "backend", "bin", (0, platform_1.getBinaryName)("kandev"));
44
+ if (!node_fs_1.default.existsSync(backendBin)) {
45
+ throw new Error("Backend binary not found. Run `make build` first.");
46
+ }
47
+ // Check for standalone build (Next.js standalone output)
48
+ const webServerPath = node_path_1.default.join(repoRoot, "apps", "web", ".next", "standalone", "web", "server.js");
49
+ if (!node_fs_1.default.existsSync(webServerPath)) {
50
+ throw new Error("Web standalone build not found. Run `make build` first.");
51
+ }
52
+ const webStandaloneDir = node_path_1.default.dirname(webServerPath);
53
+ const webStaticDir = node_path_1.default.join(repoRoot, "apps", "web", ".next", "static");
54
+ const standaloneStaticDir = node_path_1.default.join(webStandaloneDir, ".next", "static");
55
+ if (node_fs_1.default.existsSync(webStaticDir) && !node_fs_1.default.existsSync(standaloneStaticDir)) {
56
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(standaloneStaticDir), { recursive: true });
57
+ try {
58
+ node_fs_1.default.symlinkSync(webStaticDir, standaloneStaticDir, "junction");
59
+ }
60
+ catch (err) {
61
+ console.warn(`[kandev] failed to link Next.js static assets: ${err instanceof Error ? err.message : String(err)}`);
62
+ }
63
+ }
64
+ // Link public directory (fonts, images, etc.) into standalone output
65
+ const webPublicDir = node_path_1.default.join(repoRoot, "apps", "web", "public");
66
+ const standalonePublicDir = node_path_1.default.join(webStandaloneDir, "public");
67
+ if (node_fs_1.default.existsSync(webPublicDir) && !node_fs_1.default.existsSync(standalonePublicDir)) {
68
+ try {
69
+ node_fs_1.default.symlinkSync(webPublicDir, standalonePublicDir, "junction");
70
+ }
71
+ catch (err) {
72
+ console.warn(`[kandev] failed to link public assets: ${err instanceof Error ? err.message : String(err)}`);
73
+ }
74
+ }
75
+ // Production mode: use warn log level for clean output unless verbose/debug
76
+ const showOutput = verbose || debug;
77
+ const dbPath = node_path_1.default.join(constants_1.DATA_DIR, "kandev.db");
78
+ const backendEnv = (0, shared_1.buildBackendEnv)({
79
+ ports,
80
+ logLevel: debug ? "debug" : verbose ? "info" : "warn",
81
+ extra: {
82
+ KANDEV_DATABASE_PATH: dbPath,
83
+ ...(debug ? { KANDEV_DEBUG_AGENT_MESSAGES: "true" } : {}),
84
+ },
85
+ });
86
+ const webEnv = (0, shared_1.buildWebEnv)({ ports, includeMcp: true, production: true, debug });
87
+ const supervisor = (0, process_1.createProcessSupervisor)();
88
+ supervisor.attachSignalHandlers();
89
+ // Start backend with piped stdio (quiet mode unless verbose/debug)
90
+ const backendProc = (0, node_child_process_1.spawn)(backendBin, [], {
91
+ cwd: node_path_1.default.dirname(backendBin),
92
+ env: backendEnv,
93
+ stdio: showOutput ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"],
94
+ });
95
+ supervisor.children.push(backendProc);
96
+ // Forward stderr only (warnings/errors) when quiet
97
+ if (!showOutput) {
98
+ backendProc.stderr?.pipe(process.stderr);
99
+ }
100
+ (0, shared_1.attachBackendExitHandler)(backendProc, supervisor);
101
+ const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_RELEASE);
102
+ await (0, health_1.waitForHealth)(ports.backendUrl, backendProc, healthTimeoutMs);
103
+ // Use standalone server.js directly (not pnpm start)
104
+ const webUrl = `http://localhost:${ports.webPort}`;
105
+ (0, web_1.launchWebApp)({
106
+ command: "node",
107
+ args: [webServerPath],
108
+ cwd: webStandaloneDir,
109
+ env: webEnv,
110
+ url: webUrl,
111
+ supervisor,
112
+ label: "web",
113
+ quiet: !showOutput,
114
+ });
115
+ // Print clean summary
116
+ console.log("");
117
+ console.log("[kandev] Server started successfully");
118
+ console.log("");
119
+ console.log(` Web: ${webUrl}`);
120
+ console.log(` API: ${ports.backendUrl}`);
121
+ console.log(` MCP: ${ports.mcpUrl}`);
122
+ console.log(` Database: ${dbPath}`);
123
+ console.log("");
124
+ }
package/dist/update.js CHANGED
@@ -60,7 +60,9 @@ function promptYesNo(question, defaultYes = false) {
60
60
  const suffix = defaultYes ? "[Y/n]" : "[y/N]";
61
61
  rl.question(`${question} ${suffix} `, (answer) => {
62
62
  rl.close();
63
- const normalized = String(answer || "").trim().toLowerCase();
63
+ const normalized = String(answer || "")
64
+ .trim()
65
+ .toLowerCase();
64
66
  if (!normalized) {
65
67
  resolve(Boolean(defaultYes));
66
68
  return;
package/dist/web.js CHANGED
@@ -3,32 +3,46 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.openBrowser = openBrowser;
4
4
  exports.launchWebApp = launchWebApp;
5
5
  const node_child_process_1 = require("node:child_process");
6
+ const node_fs_1 = require("node:fs");
7
+ let _isWSL;
8
+ function isWSL() {
9
+ if (_isWSL === undefined) {
10
+ try {
11
+ _isWSL = (0, node_fs_1.readFileSync)("/proc/version", "utf8").toLowerCase().includes("microsoft");
12
+ }
13
+ catch {
14
+ _isWSL = false;
15
+ }
16
+ }
17
+ return _isWSL;
18
+ }
6
19
  function openBrowser(url) {
7
20
  if (process.env.KANDEV_NO_BROWSER === "1") {
8
21
  return;
9
22
  }
10
- const opener = process.platform === "darwin"
11
- ? "open"
12
- : process.platform === "win32"
13
- ? "cmd"
14
- : "xdg-open";
15
- const args = process.platform === "win32"
16
- ? ["/c", "start", "", url]
17
- : [url];
23
+ const useCmd = process.platform === "win32" || isWSL();
24
+ const opener = process.platform === "darwin" ? "open" : useCmd ? "cmd.exe" : "xdg-open";
25
+ const args = useCmd ? ["/c", "start", "", url] : [url];
18
26
  try {
19
27
  const child = (0, node_child_process_1.spawn)(opener, args, { stdio: "ignore", detached: true });
28
+ child.on("error", () => { }); // ignore async spawn errors (e.g. xdg-open missing)
20
29
  child.unref();
21
30
  }
22
31
  catch {
23
32
  // ignore browser launch errors
24
33
  }
25
34
  }
26
- function launchWebApp({ command, args, cwd, env, url, supervisor, label, }) {
27
- const proc = (0, node_child_process_1.spawn)(command, args, { cwd, env, stdio: "inherit" });
35
+ function launchWebApp({ command, args, cwd, env, url, supervisor, label, quiet = false, }) {
36
+ const stdio = quiet ? ["ignore", "pipe", "pipe"] : "inherit";
37
+ const proc = (0, node_child_process_1.spawn)(command, args, { cwd, env, stdio });
28
38
  supervisor.children.push(proc);
39
+ // In quiet mode, only forward stderr
40
+ if (quiet && proc.stderr) {
41
+ proc.stderr.pipe(process.stderr);
42
+ }
29
43
  proc.on("exit", (code, signal) => {
30
44
  console.error(`[kandev] ${label} exited (code=${code}, signal=${signal})`);
31
- const exitCode = signal ? 0 : code ?? 1;
45
+ const exitCode = signal ? 0 : (code ?? 1);
32
46
  void supervisor.shutdown(`${label} exit`).then(() => process.exit(exitCode));
33
47
  });
34
48
  openBrowser(url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kandev",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "description": "NPX launcher for Kandev",
6
6
  "license": "UNLICENSED",
@@ -14,11 +14,10 @@
14
14
  "dist"
15
15
  ],
16
16
  "dependencies": {
17
- "adm-zip": "^0.5.16",
17
+ "tar": "^7.5.0",
18
18
  "tree-kill": "^1.2.2"
19
19
  },
20
20
  "devDependencies": {
21
- "@types/adm-zip": "^0.5.7",
22
21
  "@types/node": "^20",
23
22
  "tsx": "^4.15.7",
24
23
  "typescript": "^5"