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 +36 -57
- package/dist/bundle.js +52 -14
- package/dist/cli.js +23 -1
- package/dist/constants.js +4 -2
- package/dist/dev.js +15 -30
- package/dist/github.js +30 -15
- package/dist/health.js +22 -0
- package/dist/platform.js +4 -1
- package/dist/ports.js +38 -18
- package/dist/process.js +68 -7
- package/dist/run.js +14 -11
- package/dist/shared.js +120 -0
- package/dist/start.js +124 -0
- package/dist/update.js +3 -1
- package/dist/web.js +25 -11
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -1,77 +1,56 @@
|
|
|
1
|
-
# Kandev
|
|
1
|
+
# Kandev
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
8
|
+
npx kandev
|
|
31
9
|
```
|
|
32
10
|
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
# Run the latest release bundle
|
|
37
|
-
npx kandev
|
|
13
|
+
## What You Get
|
|
38
14
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
36
|
+
## Requirements
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
38
|
+
- Node.js (for `npx`)
|
|
39
|
+
- Git
|
|
40
|
+
- Docker (optional - needed for container runtimes)
|
|
56
41
|
|
|
57
|
-
##
|
|
42
|
+
## Platforms
|
|
58
43
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```
|
|
44
|
+
- macOS (Intel + Apple Silicon)
|
|
45
|
+
- Linux (x64)
|
|
46
|
+
- Windows (x64, WSL)
|
|
63
47
|
|
|
64
|
-
|
|
48
|
+
## Learn More
|
|
65
49
|
|
|
66
|
-
|
|
50
|
+
Open source, multi-provider, no telemetry, not tied to any cloud.
|
|
67
51
|
|
|
68
|
-
|
|
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
|
-
##
|
|
54
|
+
## License
|
|
73
55
|
|
|
74
|
-
-
|
|
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.
|
|
39
|
+
exports.extractTarGz = extractTarGz;
|
|
7
40
|
exports.ensureExtracted = ensureExtracted;
|
|
8
41
|
exports.findBundleRoot = findBundleRoot;
|
|
9
42
|
exports.resolveWebServerPath = resolveWebServerPath;
|
|
10
|
-
const
|
|
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
|
|
14
|
-
|
|
15
|
-
zip.extractAllTo(destDir, true);
|
|
46
|
+
function extractTarGz(archivePath, destDir) {
|
|
47
|
+
tar.extract({ file: archivePath, cwd: destDir, sync: true });
|
|
16
48
|
}
|
|
17
|
-
function ensureExtracted(
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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.
|
|
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
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
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"], {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
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 || "
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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-
|
|
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
|
-
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
10
|
+
let shutdownPromise = null;
|
|
10
11
|
const children = [];
|
|
11
12
|
const shutdown = async (reason) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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}.
|
|
24
|
+
const assetName = `kandev-${platformDir}.tar.gz`;
|
|
24
25
|
const cacheDir = node_path_1.default.join(constants_2.CACHE_DIR, tag, platformDir);
|
|
25
|
-
const
|
|
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)(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 || "")
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
+
"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
|
-
"
|
|
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"
|