kandev 0.16.0 → 0.39.2

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
@@ -4,18 +4,36 @@ Manage tasks. Orchestrate agents. Review changes. Ship value.
4
4
 
5
5
  ## Quick Start
6
6
 
7
+ ### Homebrew
8
+
9
+ ```bash
10
+ brew install kdlbs/kandev/kandev
11
+ kandev
12
+ ```
13
+
14
+ ### NPX (requires npm 7+)
15
+
7
16
  ```bash
8
- npx kandev
17
+ npx kandev@latest
9
18
  ```
10
19
 
11
- Downloads the latest release, starts the backend + web app, and opens your browser. Data (worktrees, SQLite DB) is stored in `~/.kandev` by default.
20
+ Either install path resolves a platform-matched runtime (Go backend, agentctl, Next.js standalone web), launches the backend + web, and opens your browser. Data (worktrees, SQLite DB) is stored in `~/.kandev` by default.
12
21
 
13
22
  ## Version and Updates
14
23
 
15
- - `npx kandev` (run mode) uses the latest GitHub release bundle by default.
16
- - At startup, the launcher prints the resolved release tag (for example: `[kandev] release: v0.2.3 (github latest)`).
17
- - Use `--version <tag>` to pin a specific runtime bundle release.
18
- - CLI package updates are checked from npm and shown as a prompt. They are not forced.
24
+ The package manager owns the runtime version. `kandev@X.Y.Z` ships with the matching runtime.
25
+
26
+ - **Update via Homebrew**: `brew upgrade kandev`
27
+ - **Update via npm/npx**: `npx kandev@latest` or `npm install -g kandev@latest`
28
+ - **Print CLI version**: `kandev --version`
29
+
30
+ ### Advanced: pin a specific runtime tag
31
+
32
+ `--runtime-version <tag>` downloads a specific GitHub release runtime instead of using the installed one. For debugging compatibility issues only:
33
+
34
+ ```bash
35
+ kandev --runtime-version v0.16.0
36
+ ```
19
37
 
20
38
  ## What You Get
21
39
 
package/dist/args.js ADDED
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ParseError = void 0;
4
+ exports.parseArgs = parseArgs;
5
+ exports.resolvePorts = resolvePorts;
6
+ class ParseError extends Error {
7
+ }
8
+ exports.ParseError = ParseError;
9
+ function parseArgs(argv) {
10
+ const opts = { command: "run" };
11
+ let showHelp = false;
12
+ const deprecatedFlags = [];
13
+ const noteDeprecated = (flag) => {
14
+ if (!deprecatedFlags.includes(flag))
15
+ deprecatedFlags.push(flag);
16
+ };
17
+ for (let i = 0; i < argv.length; i += 1) {
18
+ const arg = argv[i];
19
+ if (arg === "--help" || arg === "-h") {
20
+ showHelp = true;
21
+ continue;
22
+ }
23
+ if (arg === "--version" || arg === "-V") {
24
+ opts.showVersion = true;
25
+ continue;
26
+ }
27
+ if (arg === "dev" || arg === "run" || arg === "start") {
28
+ opts.command = arg;
29
+ continue;
30
+ }
31
+ if (arg === "--runtime-version") {
32
+ opts.runtimeVersion = takeValue(argv, i, "--runtime-version");
33
+ i += 1;
34
+ continue;
35
+ }
36
+ if (arg.startsWith("--runtime-version=")) {
37
+ const value = arg.slice("--runtime-version=".length);
38
+ if (value.length === 0)
39
+ throw new ParseError("--runtime-version requires a value");
40
+ opts.runtimeVersion = value;
41
+ continue;
42
+ }
43
+ if (arg === "--dev") {
44
+ opts.command = "dev";
45
+ continue;
46
+ }
47
+ // --port is an alias for --backend-port (the user-facing port in run/start).
48
+ if (arg === "--port" || arg === "--backend-port") {
49
+ opts.backendPort = parsePort(takeValue(argv, i, arg), arg);
50
+ i += 1;
51
+ continue;
52
+ }
53
+ if (arg.startsWith("--port=") || arg.startsWith("--backend-port=")) {
54
+ const flag = arg.startsWith("--port=") ? "--port" : "--backend-port";
55
+ opts.backendPort = parsePort(arg.slice(flag.length + 1), flag);
56
+ continue;
57
+ }
58
+ if (arg === "--web-internal-port") {
59
+ opts.webPort = parsePort(takeValue(argv, i, "--web-internal-port"), "--web-internal-port");
60
+ i += 1;
61
+ continue;
62
+ }
63
+ if (arg.startsWith("--web-internal-port=")) {
64
+ opts.webPort = parsePort(arg.slice("--web-internal-port=".length), "--web-internal-port");
65
+ continue;
66
+ }
67
+ if (arg === "--web-port") {
68
+ opts.webPort = parsePort(takeValue(argv, i, "--web-port"), "--web-port");
69
+ noteDeprecated("--web-port");
70
+ i += 1;
71
+ continue;
72
+ }
73
+ if (arg.startsWith("--web-port=")) {
74
+ opts.webPort = parsePort(arg.slice("--web-port=".length), "--web-port");
75
+ noteDeprecated("--web-port");
76
+ continue;
77
+ }
78
+ if (arg === "--verbose" || arg === "-v") {
79
+ opts.verbose = true;
80
+ continue;
81
+ }
82
+ if (arg === "--debug") {
83
+ opts.debug = true;
84
+ continue;
85
+ }
86
+ }
87
+ return { options: opts, showHelp, deprecatedFlags };
88
+ }
89
+ function takeValue(argv, i, flag) {
90
+ const v = argv[i + 1];
91
+ if (v === undefined || v.startsWith("-")) {
92
+ throw new ParseError(`${flag} requires a value`);
93
+ }
94
+ return v;
95
+ }
96
+ function parsePort(raw, flag) {
97
+ const n = Number(raw);
98
+ if (raw === "" || !Number.isInteger(n) || n < 1 || n > 65535) {
99
+ throw new ParseError(`${flag} value must be an integer between 1 and 65535, got "${raw}"`);
100
+ }
101
+ return n;
102
+ }
103
+ // CLI flags beat env vars; KANDEV_PORT is an alias for KANDEV_BACKEND_PORT.
104
+ function resolvePorts(options, env) {
105
+ return {
106
+ backendPort: options.backendPort ?? envPort(env, "KANDEV_BACKEND_PORT") ?? envPort(env, "KANDEV_PORT"),
107
+ webPort: options.webPort ?? envPort(env, "KANDEV_WEB_PORT"),
108
+ };
109
+ }
110
+ function envPort(env, name) {
111
+ const val = env[name];
112
+ if (val === undefined)
113
+ return undefined;
114
+ const n = Number(val);
115
+ if (val === "" || !Number.isInteger(n) || n < 1 || n > 65535) {
116
+ throw new ParseError(`${name} must be an integer between 1 and 65535, got "${val}"`);
117
+ }
118
+ return n;
119
+ }
package/dist/cli.js CHANGED
@@ -6,20 +6,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const node_path_1 = __importDefault(require("node:path"));
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const package_json_1 = __importDefault(require("../package.json"));
9
+ const args_1 = require("./args");
9
10
  const dev_1 = require("./dev");
10
11
  const run_1 = require("./run");
11
12
  const start_1 = require("./start");
12
13
  const ports_1 = require("./ports");
13
- const update_1 = require("./update");
14
14
  function printHelp() {
15
15
  console.log(`kandev launcher
16
16
 
17
17
  Usage:
18
- kandev run [--version <tag>] [--backend-port <port>] [--web-port <port>] [--verbose] [--debug]
19
- kandev dev [--backend-port <port>] [--web-port <port>]
20
- kandev start [--backend-port <port>] [--web-port <port>] [--verbose] [--debug]
21
- kandev [--version <tag>] [--backend-port <port>] [--web-port <port>] [--verbose] [--debug]
22
- kandev --dev [--backend-port <port>] [--web-port <port>]
18
+ kandev run [--port <port>] [--verbose] [--debug]
19
+ kandev dev [--port <port>]
20
+ kandev start [--port <port>] [--verbose] [--debug]
21
+ kandev [--port <port>] [--verbose] [--debug]
22
+ kandev --dev [--port <port>]
23
23
 
24
24
  Examples:
25
25
  kandev
@@ -27,81 +27,34 @@ Examples:
27
27
  kandev --dev
28
28
  kandev dev
29
29
  kandev start
30
- kandev --version v0.1.0
31
- kandev --backend-port 18080 --web-port 13000
30
+ kandev --version
31
+ kandev --port 3000
32
32
  kandev --debug
33
33
 
34
34
  Options:
35
- dev Use local repo for dev (make dev + next dev) if available.
36
- start Use local production build (make build + next start).
37
- run Use release bundles (default).
35
+ dev Use local repo for dev (make dev + next dev) if available.
36
+ start Use local production build (make build + next start).
37
+ run Use installed runtime bundle (default).
38
38
  --dev Alias for "dev".
39
- --version Release tag to install (default: latest).
40
- --backend-port Override backend port (or KANDEV_BACKEND_PORT env var).
41
- --web-port Override web port (or KANDEV_WEB_PORT env var).
39
+ --version, -V Print CLI version and exit.
40
+ --port Port for the Go backend (the URL kandev opens on in
41
+ start/run). Alias for --backend-port. Also reads
42
+ KANDEV_PORT or KANDEV_BACKEND_PORT.
42
43
  --verbose, -v Show info logs from backend + web.
43
44
  --debug Show debug logs + agent message dumps.
44
45
  --help, -h Show help.
46
+
47
+ Advanced:
48
+ --backend-port Same as --port.
49
+ --web-internal-port Override the internal Next.js port. The Go backend
50
+ reverse-proxies to it; users hit the backend port.
51
+ Also reads KANDEV_WEB_PORT.
52
+ --web-port Deprecated alias for --web-internal-port.
53
+ --runtime-version <tag> Download and use a specific release tag instead of
54
+ the installed runtime. For debugging only.
55
+ Example: kandev --runtime-version v0.16.0
45
56
  `);
46
57
  }
47
- function parseArgs(argv) {
48
- const opts = { command: "run" };
49
- for (let i = 0; i < argv.length; i += 1) {
50
- const arg = argv[i];
51
- if (arg === "--help" || arg === "-h") {
52
- printHelp();
53
- process.exit(0);
54
- }
55
- if (arg === "dev" || arg === "run" || arg === "start") {
56
- opts.command = arg;
57
- continue;
58
- }
59
- if (arg === "--version") {
60
- opts.version = argv[i + 1];
61
- i += 1;
62
- continue;
63
- }
64
- if (arg.startsWith("--version=")) {
65
- opts.version = arg.split("=")[1];
66
- continue;
67
- }
68
- if (arg === "--dev") {
69
- opts.command = "dev";
70
- continue;
71
- }
72
- if (arg === "--backend-port") {
73
- opts.backendPort = Number(argv[i + 1]);
74
- i += 1;
75
- continue;
76
- }
77
- if (arg.startsWith("--backend-port=")) {
78
- opts.backendPort = Number(arg.split("=")[1]);
79
- continue;
80
- }
81
- if (arg === "--web-port") {
82
- opts.webPort = Number(argv[i + 1]);
83
- i += 1;
84
- continue;
85
- }
86
- if (arg.startsWith("--web-port=")) {
87
- opts.webPort = Number(arg.split("=")[1]);
88
- continue;
89
- }
90
- if (arg === "--verbose" || arg === "-v") {
91
- opts.verbose = true;
92
- continue;
93
- }
94
- if (arg === "--debug") {
95
- opts.debug = true;
96
- continue;
97
- }
98
- }
99
- return opts;
100
- }
101
- function envPort(name) {
102
- const val = process.env[name];
103
- return val ? Number(val) : undefined;
104
- }
105
58
  function findRepoRoot(startDir) {
106
59
  let current = node_path_1.default.resolve(startDir);
107
60
  while (true) {
@@ -125,10 +78,22 @@ function findRepoRoot(startDir) {
125
78
  }
126
79
  }
127
80
  async function main() {
128
- const raw = parseArgs(process.argv.slice(2));
129
- const backendPort = (0, ports_1.ensureValidPort)(raw.backendPort ?? envPort("KANDEV_BACKEND_PORT"), "backend port");
130
- const webPort = (0, ports_1.ensureValidPort)(raw.webPort ?? envPort("KANDEV_WEB_PORT"), "web port");
131
- if (raw.command === "dev") {
81
+ const { options, showHelp, deprecatedFlags } = (0, args_1.parseArgs)(process.argv.slice(2));
82
+ if (options.showVersion) {
83
+ console.log(package_json_1.default.version);
84
+ return;
85
+ }
86
+ if (showHelp) {
87
+ printHelp();
88
+ return;
89
+ }
90
+ for (const flag of deprecatedFlags) {
91
+ process.stderr.write(`[kandev] ${flag} is deprecated; use --web-internal-port\n`);
92
+ }
93
+ const resolved = (0, args_1.resolvePorts)(options, process.env);
94
+ const backendPort = (0, ports_1.ensureValidPort)(resolved.backendPort, "backend port");
95
+ const webPort = (0, ports_1.ensureValidPort)(resolved.webPort, "web port");
96
+ if (options.command === "dev") {
132
97
  const repoRoot = findRepoRoot(process.cwd());
133
98
  if (!repoRoot) {
134
99
  throw new Error("Unable to locate repo root for dev. Run from the repo.");
@@ -136,24 +101,34 @@ async function main() {
136
101
  await (0, dev_1.runDev)({ repoRoot, backendPort, webPort });
137
102
  return;
138
103
  }
139
- if (raw.command === "start") {
104
+ if (options.command === "start") {
140
105
  const repoRoot = findRepoRoot(process.cwd());
141
106
  if (!repoRoot) {
142
107
  throw new Error("Unable to locate repo root for start. Run from the repo.");
143
108
  }
144
- await (0, start_1.runStart)({ repoRoot, backendPort, webPort, verbose: raw.verbose, debug: raw.debug });
109
+ await (0, start_1.runStart)({
110
+ repoRoot,
111
+ backendPort,
112
+ webPort,
113
+ verbose: options.verbose,
114
+ debug: options.debug,
115
+ });
145
116
  return;
146
117
  }
147
- await (0, update_1.maybePromptForUpdate)(package_json_1.default.version, process.argv.slice(2));
148
118
  await (0, run_1.runRelease)({
149
- version: raw.version,
119
+ runtimeVersion: options.runtimeVersion,
150
120
  backendPort,
151
121
  webPort,
152
- verbose: raw.verbose,
153
- debug: raw.debug,
122
+ verbose: options.verbose,
123
+ debug: options.debug,
154
124
  });
155
125
  }
156
126
  main().catch((err) => {
127
+ if (err instanceof args_1.ParseError) {
128
+ console.error(`[kandev] ${err.message}`);
129
+ console.error("[kandev] run --help for usage");
130
+ process.exit(2);
131
+ }
157
132
  console.error(`[kandev] ${err instanceof Error ? err.message : String(err)}`);
158
133
  process.exit(1);
159
134
  });
package/dist/constants.js CHANGED
@@ -3,7 +3,7 @@ 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.DEV_KANDEV_DOTDIR = exports.DATA_DIR = exports.CACHE_DIR = exports.KANDEV_TASKS_DIR = exports.KANDEV_HOME_DIR = exports.KANDEV_DOTDIR = 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;
6
+ exports.DEV_KANDEV_DOTDIR = exports.DATA_DIR = exports.CACHE_DIR = exports.KANDEV_TASKS_DIR = exports.KANDEV_HOME_DIR = exports.KANDEV_DOTDIR = exports.HEALTH_TIMEOUT_MS_DEV = exports.HEALTH_TIMEOUT_MS_RELEASE = 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;
7
7
  exports.devKandevHome = devKandevHome;
8
8
  const node_os_1 = __importDefault(require("node:os"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
@@ -13,7 +13,6 @@ const node_path_1 = __importDefault(require("node:path"));
13
13
  exports.DEFAULT_BACKEND_PORT = 38429;
14
14
  exports.DEFAULT_WEB_PORT = 37429;
15
15
  exports.DEFAULT_AGENTCTL_PORT = 39429;
16
- exports.DEFAULT_MCP_PORT = 40429;
17
16
  // Random fallback range for port selection.
18
17
  exports.RANDOM_PORT_MIN = 10000;
19
18
  exports.RANDOM_PORT_MAX = 60000;
package/dist/dev.js CHANGED
@@ -19,6 +19,7 @@ async function runDev({ repoRoot, backendPort, webPort }) {
19
19
  const backendEnv = (0, shared_1.buildBackendEnv)({ ports, extra });
20
20
  const webEnv = (0, shared_1.buildWebEnv)({ ports, debug: true });
21
21
  const logLevel = process.env.KANDEV_LOGGING_LEVEL?.trim() || process.env.KANDEV_LOG_LEVEL?.trim() || "info";
22
+ const webUrl = `http://localhost:${ports.webPort}`;
22
23
  (0, shared_1.logStartupInfo)({
23
24
  header: "dev mode: using local repo",
24
25
  ports,
@@ -38,7 +39,6 @@ async function runDev({ repoRoot, backendPort, webPort }) {
38
39
  console.log("[kandev] starting backend...");
39
40
  await (0, health_1.waitForHealth)(ports.backendUrl, backendProc, healthTimeoutMs);
40
41
  console.log(`[kandev] backend ready at ${ports.backendUrl}`);
41
- const webUrl = `http://localhost:${ports.webPort}`;
42
42
  console.log("[kandev] starting web...");
43
43
  const webProc = (0, web_1.launchWebApp)({
44
44
  command: "pnpm",
@@ -49,7 +49,7 @@ async function runDev({ repoRoot, backendPort, webPort }) {
49
49
  label: "web",
50
50
  });
51
51
  await (0, health_1.waitForUrlReady)(webUrl, webProc, healthTimeoutMs);
52
- console.log(`[kandev] web ready at ${webUrl}`);
52
+ console.log(`[kandev] open: ${webUrl}`);
53
53
  (0, web_1.openBrowser)(webUrl);
54
54
  }
55
55
  // Computes the dev-mode backend env. Dev mode always roots kandev under
package/dist/run.js CHANGED
@@ -18,6 +18,7 @@ const platform_1 = require("./platform");
18
18
  const version_1 = require("./version");
19
19
  const ports_1 = require("./ports");
20
20
  const process_1 = require("./process");
21
+ const runtime_1 = require("./runtime");
21
22
  const shared_1 = require("./shared");
22
23
  const web_1 = require("./web");
23
24
  /**
@@ -77,47 +78,62 @@ function cleanOldReleases(currentTag) {
77
78
  // Non-critical — don't fail the launch if cleanup errors.
78
79
  }
79
80
  }
80
- async function prepareReleaseBundle({ version, backendPort, webPort, verbose = false, debug = false, }) {
81
+ /**
82
+ * Download a specific release version into the local cache.
83
+ * Only used when --runtime-version is given explicitly.
84
+ */
85
+ async function downloadRuntimeVersion(runtimeVersion) {
81
86
  const platformDir = (0, platform_1.getPlatformDir)();
82
- let tag;
83
- let cacheDir;
84
- try {
85
- const release = await (0, github_1.getRelease)(version);
86
- tag = release.tag_name;
87
- const assetName = `kandev-${platformDir}.tar.gz`;
88
- cacheDir = node_path_1.default.join(constants_1.CACHE_DIR, tag, platformDir);
89
- const archivePath = await (0, github_1.ensureAsset)(tag, assetName, cacheDir, (downloaded, total) => {
90
- const percent = total ? Math.round((downloaded / total) * 100) : 0;
91
- const mb = (downloaded / (1024 * 1024)).toFixed(1);
92
- const totalMb = total ? (total / (1024 * 1024)).toFixed(1) : "?";
93
- process.stderr.write(`\r Downloading: ${mb}MB / ${totalMb}MB (${percent}%)`);
94
- });
95
- process.stderr.write("\n");
96
- (0, bundle_1.ensureExtracted)(archivePath, cacheDir);
97
- cleanOldReleases(tag);
98
- }
99
- catch (err) {
100
- // GitHub unreachable — try to launch from cache.
101
- const cached = findCachedRelease(platformDir, version);
102
- if (!cached) {
103
- const target = version ? `version ${version}` : "latest version";
104
- const reason = err instanceof Error ? err.message : String(err);
105
- throw new Error(`Failed to fetch ${target} and no cached release found.\n` +
106
- ` Reason: ${reason}\n` +
107
- ` Run kandev once while online to cache a release for offline use.`);
87
+ const release = await (0, github_1.getRelease)(runtimeVersion);
88
+ const tag = release.tag_name;
89
+ const assetName = `kandev-${platformDir}.tar.gz`;
90
+ const cacheDir = node_path_1.default.join(constants_1.CACHE_DIR, tag, platformDir);
91
+ const archivePath = await (0, github_1.ensureAsset)(tag, assetName, cacheDir, (downloaded, total) => {
92
+ const percent = total ? Math.round((downloaded / total) * 100) : 0;
93
+ const mb = (downloaded / (1024 * 1024)).toFixed(1);
94
+ const totalMb = total ? (total / (1024 * 1024)).toFixed(1) : "?";
95
+ process.stderr.write(`\r Downloading: ${mb}MB / ${totalMb}MB (${percent}%)`);
96
+ });
97
+ process.stderr.write("\n");
98
+ (0, bundle_1.ensureExtracted)(archivePath, cacheDir);
99
+ cleanOldReleases(tag);
100
+ return tag;
101
+ }
102
+ async function prepareBundleForLaunch({ runtimeVersion, backendPort, webPort, verbose = false, debug = false, }) {
103
+ let bundleDir;
104
+ let releaseTag;
105
+ if (runtimeVersion) {
106
+ // Explicit version: ensure it is in the cache (downloading if needed), then resolve.
107
+ const platformDir = (0, platform_1.getPlatformDir)();
108
+ const cached = findCachedRelease(platformDir, runtimeVersion);
109
+ let tag;
110
+ if (cached) {
111
+ tag = cached.tag;
112
+ bundleDir = (0, bundle_1.findBundleRoot)(cached.cacheDir);
108
113
  }
109
- tag = cached.tag;
110
- cacheDir = cached.cacheDir;
111
- process.stderr.write(`[kandev] GitHub unreachable — using cached release ${tag}\n`);
112
- }
113
- const bundleDir = (0, bundle_1.findBundleRoot)(cacheDir);
114
- const backendBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("kandev"));
115
- if (!node_fs_1.default.existsSync(backendBin)) {
116
- throw new Error(`Backend binary not found at ${backendBin}`);
114
+ else {
115
+ try {
116
+ tag = await downloadRuntimeVersion(runtimeVersion);
117
+ const cacheDir = node_path_1.default.join(constants_1.CACHE_DIR, tag, platformDir);
118
+ bundleDir = (0, bundle_1.findBundleRoot)(cacheDir);
119
+ }
120
+ catch (err) {
121
+ const reason = err instanceof Error ? err.message : String(err);
122
+ throw new Error(`Failed to fetch runtime version ${runtimeVersion}.\n` +
123
+ ` Reason: ${reason}\n` +
124
+ ` Run kandev once while online to cache a release for offline use.`);
125
+ }
126
+ }
127
+ // Validate the resolved bundle has all required binaries before launching.
128
+ (0, runtime_1.validateBundle)(bundleDir);
129
+ releaseTag = tag;
117
130
  }
118
- const agentctlBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("agentctl"));
119
- if (!node_fs_1.default.existsSync(agentctlBin)) {
120
- throw new Error(`agentctl binary not found at ${agentctlBin}`);
131
+ else {
132
+ // Default path: resolve from KANDEV_BUNDLE_DIR or installed npm runtime package.
133
+ const resolved = (0, runtime_1.resolveRuntime)();
134
+ bundleDir = resolved.bundleDir;
135
+ // Use KANDEV_VERSION if set (e.g. by Homebrew wrapper), otherwise show source.
136
+ releaseTag = process.env.KANDEV_VERSION ?? `(${resolved.source})`;
121
137
  }
122
138
  const actualBackendPort = backendPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_BACKEND_PORT));
123
139
  const actualWebPort = webPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_WEB_PORT));
@@ -127,10 +143,7 @@ async function prepareReleaseBundle({ version, backendPort, webPort, verbose = f
127
143
  const logLevel = process.env.KANDEV_LOG_LEVEL?.trim() || (debug ? "debug" : verbose ? "info" : "warn");
128
144
  node_fs_1.default.mkdirSync(constants_1.DATA_DIR, { recursive: true });
129
145
  const dbPath = node_path_1.default.join(constants_1.DATA_DIR, "kandev.db");
130
- // Note: Release mode doesn't configure MCP server ports as it uses
131
- // the bundled configuration. Only backend and agentctl ports are set.
132
- // Log level defaults to warn for clean output and can be overridden
133
- // via KANDEV_LOG_LEVEL or --verbose/--debug flags.
146
+ const backendBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("kandev"));
134
147
  const backendEnv = {
135
148
  ...process.env,
136
149
  KANDEV_SERVER_PORT: String(actualBackendPort),
@@ -144,8 +157,6 @@ async function prepareReleaseBundle({ version, backendPort, webPort, verbose = f
144
157
  ...process.env,
145
158
  KANDEV_API_BASE_URL: backendUrl,
146
159
  PORT: String(actualWebPort),
147
- // Ensure Next.js standalone server binds to 127.0.0.1 so localhost health checks work.
148
- // Without this, HOSTNAME from the host environment can cause binding issues.
149
160
  HOSTNAME: "127.0.0.1",
150
161
  };
151
162
  webEnv.NODE_ENV = "production";
@@ -155,8 +166,7 @@ async function prepareReleaseBundle({ version, backendPort, webPort, verbose = f
155
166
  backendUrl,
156
167
  backendEnv,
157
168
  webEnv,
158
- releaseTag: tag,
159
- requestedVersion: version,
169
+ releaseTag,
160
170
  webPort: actualWebPort,
161
171
  agentctlPort,
162
172
  dbPath,
@@ -179,29 +189,20 @@ function attachRingBuffer(stream, maxChars = 64 * 1024) {
179
189
  });
180
190
  return () => buf;
181
191
  }
182
- function launchReleaseApps(prepared) {
183
- const releaseSource = prepared.requestedVersion
184
- ? `(requested: ${prepared.requestedVersion})`
185
- : "(github latest)";
192
+ function launchBundle(prepared) {
186
193
  (0, shared_1.logStartupInfo)({
187
- header: `release: ${prepared.releaseTag} ${releaseSource}`,
194
+ header: `release: ${prepared.releaseTag}`,
188
195
  ports: {
189
196
  backendPort: Number(prepared.backendEnv.KANDEV_SERVER_PORT),
190
197
  webPort: prepared.webPort,
191
198
  agentctlPort: prepared.agentctlPort,
192
- mcpPort: 0,
193
199
  backendUrl: prepared.backendUrl,
194
- mcpUrl: "",
195
200
  },
196
201
  dbPath: prepared.dbPath,
197
202
  logLevel: prepared.logLevel,
198
203
  });
199
204
  const supervisor = (0, process_1.createProcessSupervisor)();
200
205
  supervisor.attachSignalHandlers();
201
- // Start backend: ignore stdin, always show stderr immediately.
202
- // In verbose/debug mode stream stdout live; otherwise capture it into a ring
203
- // buffer so we can dump the last few KB if the healthcheck fails (users were
204
- // previously seeing opaque "timed out" errors with no backend context).
205
206
  const backendProc = (0, node_child_process_1.spawn)(prepared.backendBin, [], {
206
207
  cwd: node_path_1.default.dirname(prepared.backendBin),
207
208
  env: prepared.backendEnv,
@@ -228,9 +229,15 @@ function launchReleaseApps(prepared) {
228
229
  }
229
230
  return { supervisor, backendProc, webServerPath, dumpBackendLogs };
230
231
  }
231
- async function runRelease({ version, backendPort, webPort, verbose = false, debug = false, }) {
232
- const prepared = await prepareReleaseBundle({ version, backendPort, webPort, verbose, debug });
233
- const { supervisor, backendProc, webServerPath, dumpBackendLogs } = launchReleaseApps(prepared);
232
+ async function runRelease({ runtimeVersion, backendPort, webPort, verbose = false, debug = false, }) {
233
+ const prepared = await prepareBundleForLaunch({
234
+ runtimeVersion,
235
+ backendPort,
236
+ webPort,
237
+ verbose,
238
+ debug,
239
+ });
240
+ const { supervisor, backendProc, webServerPath, dumpBackendLogs } = launchBundle(prepared);
234
241
  const healthTimeoutMs = (0, health_1.resolveHealthTimeoutMs)(constants_1.HEALTH_TIMEOUT_MS_RELEASE);
235
242
  console.log("[kandev] starting backend...");
236
243
  await (0, health_1.waitForHealth)(prepared.backendUrl, backendProc, healthTimeoutMs, dumpBackendLogs);
@@ -247,6 +254,6 @@ async function runRelease({ version, backendPort, webPort, verbose = false, debu
247
254
  quiet: !prepared.showOutput,
248
255
  });
249
256
  await (0, health_1.waitForUrlReady)(webUrl, webProc, healthTimeoutMs);
250
- console.log("[kandev] ready at " + prepared.backendUrl);
257
+ console.log("[kandev] open: " + prepared.backendUrl);
251
258
  (0, web_1.openBrowser)(prepared.backendUrl);
252
259
  }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveRuntime = resolveRuntime;
7
+ exports.validateBundle = validateBundle;
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const bundle_1 = require("./bundle");
11
+ const platform_1 = require("./platform");
12
+ const PLATFORM_TO_NPM_PACKAGE = {
13
+ "linux-x64": "@kdlbs/runtime-linux-x64",
14
+ "linux-arm64": "@kdlbs/runtime-linux-arm64",
15
+ "macos-x64": "@kdlbs/runtime-darwin-x64",
16
+ "macos-arm64": "@kdlbs/runtime-darwin-arm64",
17
+ "windows-x64": "@kdlbs/runtime-win32-x64",
18
+ };
19
+ /**
20
+ * Resolve the runtime bundle directory using a two-step priority chain:
21
+ *
22
+ * 1. KANDEV_BUNDLE_DIR env var — set by the Homebrew formula wrapper and
23
+ * useful for local testing. Skips all other resolution.
24
+ * 2. Installed npm runtime package — looks for @kdlbs/runtime-{platform}
25
+ * in node_modules via Node module resolution. Works after
26
+ * `npx kandev@latest` or `npm install -g kandev` (requires npm 7+).
27
+ *
28
+ * The explicit `--runtime-version <tag>` download path is handled directly
29
+ * in run.ts (which manages the GitHub download + cache itself); it does
30
+ * not flow through this function.
31
+ *
32
+ * Throws with an actionable error message if no runtime is found.
33
+ */
34
+ function resolveRuntime() {
35
+ const envBundleDir = process.env.KANDEV_BUNDLE_DIR;
36
+ if (envBundleDir) {
37
+ validateBundle(envBundleDir);
38
+ return { bundleDir: envBundleDir, source: "env" };
39
+ }
40
+ const platformDir = (0, platform_1.getPlatformDir)();
41
+ const packageName = PLATFORM_TO_NPM_PACKAGE[platformDir];
42
+ let pkgJsonPath = null;
43
+ try {
44
+ pkgJsonPath = require.resolve(`${packageName}/package.json`);
45
+ }
46
+ catch {
47
+ // MODULE_NOT_FOUND — npm runtime package is not installed. Fall through
48
+ // to the actionable error below.
49
+ }
50
+ if (pkgJsonPath) {
51
+ // The package IS installed. If validateBundle throws here, the bundle is
52
+ // present but corrupt — surface the error rather than the generic
53
+ // "no runtime found" message below.
54
+ const packageRoot = node_path_1.default.dirname(pkgJsonPath);
55
+ validateBundle(packageRoot);
56
+ return { bundleDir: packageRoot, source: "npm" };
57
+ }
58
+ throw new Error(`No Kandev runtime found for ${platformDir}.\n` +
59
+ ` Install via npm (requires npm 7+): npx kandev@latest\n` +
60
+ ` Install via Homebrew: brew install kdlbs/kandev/kandev\n` +
61
+ ` Download a specific version (debug): kandev --runtime-version <tag>`);
62
+ }
63
+ function validateBundle(bundleDir) {
64
+ const backendBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("kandev"));
65
+ if (!node_fs_1.default.existsSync(backendBin)) {
66
+ throw new Error(`Backend binary not found in bundle at ${bundleDir}`);
67
+ }
68
+ const agentctlBin = node_path_1.default.join(bundleDir, "bin", (0, platform_1.getBinaryName)("agentctl"));
69
+ if (!node_fs_1.default.existsSync(agentctlBin)) {
70
+ throw new Error(`agentctl binary not found in bundle at ${bundleDir}`);
71
+ }
72
+ const webServerPath = (0, bundle_1.resolveWebServerPath)(bundleDir);
73
+ if (!webServerPath) {
74
+ throw new Error(`Web server (server.js) not found in bundle at ${bundleDir}`);
75
+ }
76
+ }
package/dist/shared.js CHANGED
@@ -24,14 +24,11 @@ async function pickPorts(backendPort, webPort) {
24
24
  const resolvedBackendPort = backendPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_BACKEND_PORT));
25
25
  const resolvedWebPort = webPort ?? (await (0, ports_1.pickAvailablePort)(constants_1.DEFAULT_WEB_PORT));
26
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
27
  return {
29
28
  backendPort: resolvedBackendPort,
30
29
  webPort: resolvedWebPort,
31
30
  agentctlPort,
32
- mcpPort,
33
31
  backendUrl: `http://localhost:${resolvedBackendPort}`,
34
- mcpUrl: `http://localhost:${mcpPort}/sse`,
35
32
  };
36
33
  }
37
34
  /**
@@ -47,7 +44,6 @@ function buildBackendEnv(options) {
47
44
  KANDEV_SERVER_PORT: String(ports.backendPort),
48
45
  KANDEV_WEB_INTERNAL_URL: `http://localhost:${ports.webPort}`,
49
46
  KANDEV_AGENT_STANDALONE_PORT: String(ports.agentctlPort),
50
- KANDEV_AGENT_MCP_SERVER_PORT: String(ports.mcpPort),
51
47
  ...(logLevel ? { KANDEV_LOG_LEVEL: logLevel } : {}),
52
48
  ...extra,
53
49
  };
@@ -85,11 +81,13 @@ function buildWebEnv(options) {
85
81
  */
86
82
  function logStartupInfo(options) {
87
83
  const { header, ports, dbPath, logLevel } = options;
84
+ const backendUrl = ports.backendUrl;
85
+ const webUrl = `http://localhost:${ports.webPort}`;
88
86
  console.log(`[kandev] ${header}`);
89
- console.log("[kandev] url: http://localhost:" + ports.backendPort);
87
+ console.log("[kandev] backend:", backendUrl);
88
+ console.log("[kandev] web:", webUrl);
90
89
  console.log("[kandev] agentctl port:", ports.agentctlPort);
91
- console.log("[kandev] mcp port:", ports.mcpPort);
92
- console.log("[kandev] mcp url:", ports.mcpUrl);
90
+ console.log("[kandev] mcp url:", `${backendUrl}/mcp`);
93
91
  if (dbPath) {
94
92
  console.log("[kandev] db path:", dbPath);
95
93
  }
package/dist/start.js CHANGED
@@ -171,6 +171,6 @@ async function runStart({ repoRoot, backendPort, webPort, verbose = false, debug
171
171
  quiet: !showOutput,
172
172
  });
173
173
  await (0, health_1.waitForUrlReady)(webUrl, webProc, healthTimeoutMs);
174
- console.log("[kandev] ready at " + ports.backendUrl);
174
+ console.log("[kandev] open: " + ports.backendUrl);
175
175
  (0, web_1.openBrowser)(ports.backendUrl);
176
176
  }
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "kandev",
3
- "version": "0.16.0",
3
+ "version": "0.39.2",
4
4
  "private": false,
5
- "description": "NPX launcher for Kandev",
5
+ "description": "Launcher for Kandev — manage tasks, orchestrate agents, review changes, and ship value",
6
6
  "license": "AGPL-3.0-only",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/kdlbs/kandev.git"
10
+ },
11
+ "homepage": "https://github.com/kdlbs/kandev",
7
12
  "type": "commonjs",
8
13
  "bin": {
9
14
  "kandev": "bin/cli.js"
@@ -13,12 +18,23 @@
13
18
  "bin",
14
19
  "dist"
15
20
  ],
21
+ "engines": {
22
+ "npm": ">=7"
23
+ },
24
+ "optionalDependencies": {
25
+ "@kdlbs/runtime-linux-x64": "0.39.2",
26
+ "@kdlbs/runtime-linux-arm64": "0.39.2",
27
+ "@kdlbs/runtime-darwin-x64": "0.39.2",
28
+ "@kdlbs/runtime-darwin-arm64": "0.39.2",
29
+ "@kdlbs/runtime-win32-x64": "0.39.2"
30
+ },
16
31
  "dependencies": {
17
32
  "tar": "^7.5.11",
18
33
  "tree-kill": "^1.2.2"
19
34
  },
20
35
  "devDependencies": {
21
36
  "@types/node": "^20",
37
+ "esbuild": "^0.24.0",
22
38
  "tsx": "^4.15.7",
23
39
  "typescript": "^5",
24
40
  "vitest": "^1.6.0"
@@ -26,6 +42,7 @@
26
42
  "scripts": {
27
43
  "dev": "unset npm_config_prefix && tsx src/cli.ts",
28
44
  "build": "tsc -p tsconfig.json",
45
+ "bundle": "esbuild dist/cli.js --bundle --platform=node --format=cjs --outfile=dist/cli.bundle.js",
29
46
  "start": "node dist/cli.js",
30
47
  "test": "vitest run",
31
48
  "prepublishOnly": "pnpm build"
package/dist/update.js DELETED
@@ -1,87 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.maybePromptForUpdate = maybePromptForUpdate;
7
- const node_child_process_1 = require("node:child_process");
8
- const node_https_1 = __importDefault(require("node:https"));
9
- const node_readline_1 = __importDefault(require("node:readline"));
10
- const version_1 = require("./version");
11
- function requestJson(url) {
12
- return new Promise((resolve, reject) => {
13
- const req = node_https_1.default.get(url, { headers: { "User-Agent": "kandev-npx" } }, (res) => {
14
- if (res.statusCode !== 200) {
15
- return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
16
- }
17
- let body = "";
18
- res.on("data", (chunk) => (body += chunk));
19
- res.on("end", () => {
20
- try {
21
- resolve(JSON.parse(body));
22
- }
23
- catch {
24
- reject(new Error(`Failed to parse JSON from ${url}`));
25
- }
26
- });
27
- });
28
- req.setTimeout(5000, () => {
29
- req.destroy(new Error(`Request timed out fetching ${url}`));
30
- });
31
- req.on("error", reject);
32
- });
33
- }
34
- async function getLatestNpmVersion() {
35
- const data = await requestJson("https://registry.npmjs.org/kandev");
36
- return data?.["dist-tags"]?.latest;
37
- }
38
- function promptYesNo(question, defaultYes = false) {
39
- return new Promise((resolve) => {
40
- if (!process.stdin.isTTY) {
41
- resolve(false);
42
- return;
43
- }
44
- const rl = node_readline_1.default.createInterface({
45
- input: process.stdin,
46
- output: process.stdout,
47
- });
48
- const suffix = defaultYes ? "[Y/n]" : "[y/N]";
49
- rl.question(`${question} ${suffix} `, (answer) => {
50
- rl.close();
51
- const normalized = String(answer || "")
52
- .trim()
53
- .toLowerCase();
54
- if (!normalized) {
55
- resolve(Boolean(defaultYes));
56
- return;
57
- }
58
- resolve(normalized === "y" || normalized === "yes");
59
- });
60
- });
61
- }
62
- async function maybePromptForUpdate(currentVersion, args) {
63
- // Allow disabling update checks for CI or automation.
64
- if (process.env.KANDEV_SKIP_UPDATE === "1" || process.env.KANDEV_NO_UPDATE_PROMPT === "1") {
65
- return;
66
- }
67
- try {
68
- const latest = await getLatestNpmVersion();
69
- if (!latest)
70
- return;
71
- if ((0, version_1.compareVersions)(latest, currentVersion) <= 0)
72
- return;
73
- const wantsUpdate = await promptYesNo(`Update available: ${currentVersion} -> ${latest}. Update now?`, false);
74
- if (!wantsUpdate)
75
- return;
76
- const env = { ...process.env, KANDEV_SKIP_UPDATE: "1" };
77
- const child = (0, node_child_process_1.spawn)("npx", ["kandev@latest", ...args], {
78
- stdio: "inherit",
79
- env,
80
- });
81
- child.on("exit", (code) => process.exit(code || 0));
82
- await new Promise(() => { });
83
- }
84
- catch {
85
- // ignore update errors
86
- }
87
- }