kandev 0.53.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ exports.linuxSystemUnitPath = linuxSystemUnitPath;
10
10
  exports.macosUserAgentDir = macosUserAgentDir;
11
11
  exports.macosUserPlistPath = macosUserPlistPath;
12
12
  exports.macosSystemPlistPath = macosSystemPlistPath;
13
+ exports.homebrewShimPath = homebrewShimPath;
13
14
  exports.captureLauncher = captureLauncher;
14
15
  exports.resolveHomeDir = resolveHomeDir;
15
16
  exports.resolveLogDir = resolveLogDir;
@@ -45,6 +46,30 @@ function macosUserPlistPath() {
45
46
  function macosSystemPlistPath() {
46
47
  return node_path_1.default.join(exports.MACOS_SYSTEM_DAEMON_DIR, `${exports.LAUNCHD_LABEL}.plist`);
47
48
  }
49
+ // Homebrew is POSIX-only, so the Cellar segment is a hardcoded "/Cellar/"
50
+ // rather than `path.sep`-based. On a Windows CI runner `path.sep` would be
51
+ // "\\", which would never match a POSIX Cellar path and silently break shim
52
+ // derivation (and its tests).
53
+ const HOMEBREW_CELLAR_SEGMENT = "/Cellar/";
54
+ /**
55
+ * Derive the floating Homebrew launcher shim from a Cellar-installed cli.js path.
56
+ *
57
+ * Homebrew installs the CLI under `<prefix>/Cellar/kandev/<version>/...` and
58
+ * symlinks a version-independent shim at `<prefix>/bin/kandev`. That shim sets
59
+ * KANDEV_BUNDLE_DIR / KANDEV_VERSION itself and execs cli.js via the floating
60
+ * `opt/node` symlink, so it keeps working after `brew upgrade` deletes the old
61
+ * Cellar dir. Returns undefined when `cliEntry` isn't a Cellar layout (npm /
62
+ * unknown installs), so callers fall back to the version-pinned paths.
63
+ */
64
+ function homebrewShimPath(cliEntry) {
65
+ const idx = cliEntry.indexOf(HOMEBREW_CELLAR_SEGMENT);
66
+ if (idx === -1)
67
+ return undefined;
68
+ const prefix = cliEntry.slice(0, idx);
69
+ // Homebrew layout is POSIX; use path.posix.join so the result keeps forward
70
+ // slashes regardless of the host the install/tests run on.
71
+ return node_path_1.default.posix.join(prefix, "bin", exports.SERVICE_NAME);
72
+ }
48
73
  /**
49
74
  * Snapshot the current invocation so the service unit can faithfully reproduce it.
50
75
  *
@@ -63,7 +88,17 @@ function captureLauncher() {
63
88
  : cliEntry.includes(`${node_path_1.default.sep}node_modules${node_path_1.default.sep}`)
64
89
  ? "npm"
65
90
  : "unknown";
66
- return { nodePath, cliEntry, kind, bundleDir, version };
91
+ // For Homebrew installs, prefer the floating bin shim so the unit survives
92
+ // `brew upgrade` (which deletes the versioned Cellar dir baked into nodePath
93
+ // /cliEntry). Only adopt it when the shim actually exists on disk; otherwise
94
+ // fall back to the version-pinned paths below.
95
+ let shimPath;
96
+ if (kind === "homebrew") {
97
+ const candidate = homebrewShimPath(cliEntry);
98
+ if (candidate && node_fs_1.default.existsSync(candidate))
99
+ shimPath = candidate;
100
+ }
101
+ return { nodePath, cliEntry, kind, bundleDir, version, shimPath };
67
102
  }
68
103
  function resolveCliEntry() {
69
104
  const argvEntry = process.argv[1];
@@ -9,12 +9,13 @@ exports.renderSystemdUnit = renderSystemdUnit;
9
9
  exports.renderLaunchdPlist = renderLaunchdPlist;
10
10
  const node_os_1 = __importDefault(require("node:os"));
11
11
  const node_path_1 = __importDefault(require("node:path"));
12
- // User-mode PATH includes ~/.local/bin so user-installed agent CLIs (npm user
13
- // prefix, pipx, fnm, etc.) are discoverable.
12
+ // User-mode PATH includes ~/.local/bin and ~/.bun/bin so user-installed agent
13
+ // CLIs (npm user prefix, pipx, fnm, Bun globals like oh-my-pi/omp, etc.) are
14
+ // discoverable.
14
15
  const SYSTEMD_SYSTEM_PATH = "/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin";
15
- const SYSTEMD_USER_PATH = `%h/.local/bin:${SYSTEMD_SYSTEM_PATH}`;
16
+ const SYSTEMD_USER_PATH = `%h/.local/bin:%h/.bun/bin:${SYSTEMD_SYSTEM_PATH}`;
16
17
  const LAUNCHD_SYSTEM_PATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
17
- const launchdUserPath = () => `${node_os_1.default.homedir()}/.local/bin:${LAUNCHD_SYSTEM_PATH}`;
18
+ const launchdUserPath = () => `${node_os_1.default.homedir()}/.local/bin:${node_os_1.default.homedir()}/.bun/bin:${LAUNCHD_SYSTEM_PATH}`;
18
19
  // Prepend the launcher node's bin dir so `npm`/`npx` resolve under per-user
19
20
  // node managers (fnm, nvm, asdf, volta, mise), where node lives in a versioned
20
21
  // subdirectory not covered by the system PATH. ExecStart already points at this
@@ -51,29 +52,41 @@ function looksLikeManagedUnit(content) {
51
52
  * Render a systemd unit file for kandev.
52
53
  *
53
54
  * The unit hard-codes absolute paths so it works without a user PATH. We pass
54
- * `--headless` so the daemon doesn't try to open a browser. KANDEV_BUNDLE_DIR /
55
- * KANDEV_VERSION are surfaced only when present (Homebrew installs only) so
56
- * `npm i -g` installs don't get spurious env vars.
55
+ * `--headless` so the daemon doesn't try to open a browser.
56
+ *
57
+ * For Homebrew installs the launcher carries a resolved `shimPath` — the
58
+ * floating `<prefix>/bin/kandev` shim that survives `brew upgrade`. When set we
59
+ * exec that shim and omit KANDEV_BUNDLE_DIR / KANDEV_VERSION and the
60
+ * version-pinned node bin dir from PATH, because the shim supplies all three
61
+ * itself. Otherwise (npm / unknown installs) we fall back to the version-pinned
62
+ * node + cli.js and surface KANDEV_BUNDLE_DIR / KANDEV_VERSION only when present.
57
63
  */
58
64
  function renderSystemdUnit(input) {
65
+ const shimPath = input.launcher.shimPath;
59
66
  const basePath = input.mode === "system" ? SYSTEMD_SYSTEM_PATH : SYSTEMD_USER_PATH;
67
+ // For the shim, prepend its own bin dir (the Homebrew prefix's `bin`, where
68
+ // node/npm/npx live) so npx-based agents resolve even when the prefix isn't
69
+ // one of the hardcoded defaults. pathWithNodeBinDir dedupes when it already is.
70
+ const pathValue = pathWithNodeBinDir(basePath, shimPath ?? input.launcher.nodePath);
60
71
  const env = [
61
72
  envLine("KANDEV_HOME_DIR", input.homeDir),
62
73
  envLine("KANDEV_LOG_LEVEL", "info"),
63
- envLine("PATH", pathWithNodeBinDir(basePath, input.launcher.nodePath)),
74
+ envLine("PATH", pathValue),
64
75
  ];
65
76
  if (input.port !== undefined) {
66
77
  env.push(envLine("KANDEV_SERVER_PORT", String(input.port)));
67
78
  }
68
- if (input.launcher.bundleDir) {
79
+ if (!shimPath && input.launcher.bundleDir) {
69
80
  env.push(envLine("KANDEV_BUNDLE_DIR", input.launcher.bundleDir));
70
81
  }
71
- if (input.launcher.version) {
82
+ if (!shimPath && input.launcher.version) {
72
83
  env.push(envLine("KANDEV_VERSION", input.launcher.version));
73
84
  }
74
85
  const wantedBy = input.mode === "system" ? "multi-user.target" : "default.target";
75
86
  const userLine = input.mode === "system" && input.systemUser ? `User=${input.systemUser}\n` : "";
76
- const exec = `${quoteForUnit(input.launcher.nodePath)} ${quoteForUnit(input.launcher.cliEntry)} --headless`;
87
+ const exec = shimPath
88
+ ? `${quoteForUnit(shimPath)} --headless`
89
+ : `${quoteForUnit(input.launcher.nodePath)} ${quoteForUnit(input.launcher.cliEntry)} --headless`;
77
90
  return `${SYSTEMD_MARKER}
78
91
  [Unit]
79
92
  Description=Kandev autonomous agent platform
@@ -102,25 +115,30 @@ WantedBy=${wantedBy}
102
115
  * get a LaunchDaemon that runs at boot regardless of login.
103
116
  */
104
117
  function renderLaunchdPlist(input) {
118
+ const shimPath = input.launcher.shimPath;
105
119
  const basePath = input.mode === "system" ? LAUNCHD_SYSTEM_PATH : launchdUserPath();
120
+ // See renderSystemdUnit: prepend the shim's own bin dir so npm/npx resolve.
121
+ const pathValue = pathWithNodeBinDir(basePath, shimPath ?? input.launcher.nodePath);
106
122
  const envEntries = [
107
123
  ["KANDEV_HOME_DIR", input.homeDir],
108
124
  ["KANDEV_LOG_LEVEL", "info"],
109
- ["PATH", pathWithNodeBinDir(basePath, input.launcher.nodePath)],
125
+ ["PATH", pathValue],
110
126
  ];
111
127
  if (input.port !== undefined) {
112
128
  envEntries.push(["KANDEV_SERVER_PORT", String(input.port)]);
113
129
  }
114
- if (input.launcher.bundleDir) {
130
+ if (!shimPath && input.launcher.bundleDir) {
115
131
  envEntries.push(["KANDEV_BUNDLE_DIR", input.launcher.bundleDir]);
116
132
  }
117
- if (input.launcher.version) {
133
+ if (!shimPath && input.launcher.version) {
118
134
  envEntries.push(["KANDEV_VERSION", input.launcher.version]);
119
135
  }
120
136
  const envXml = envEntries
121
137
  .map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
122
138
  .join("\n");
123
- const args = [input.launcher.nodePath, input.launcher.cliEntry, "--headless"];
139
+ const args = shimPath
140
+ ? [shimPath, "--headless"]
141
+ : [input.launcher.nodePath, input.launcher.cliEntry, "--headless"];
124
142
  const argsXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
125
143
  // For system-mode LaunchDaemons, run as a specific user instead of root.
126
144
  // For user agents this directive is omitted — the agent already runs as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kandev",
3
- "version": "0.53.0",
3
+ "version": "0.54.0",
4
4
  "private": false,
5
5
  "description": "Launcher for Kandev — manage tasks, orchestrate agents, review changes, and ship value",
6
6
  "license": "AGPL-3.0-only",
@@ -22,11 +22,11 @@
22
22
  "npm": ">=7"
23
23
  },
24
24
  "optionalDependencies": {
25
- "@kdlbs/runtime-linux-x64": "0.53.0",
26
- "@kdlbs/runtime-linux-arm64": "0.53.0",
27
- "@kdlbs/runtime-darwin-x64": "0.53.0",
28
- "@kdlbs/runtime-darwin-arm64": "0.53.0",
29
- "@kdlbs/runtime-win32-x64": "0.53.0"
25
+ "@kdlbs/runtime-linux-x64": "0.54.0",
26
+ "@kdlbs/runtime-linux-arm64": "0.54.0",
27
+ "@kdlbs/runtime-darwin-x64": "0.54.0",
28
+ "@kdlbs/runtime-darwin-arm64": "0.54.0",
29
+ "@kdlbs/runtime-win32-x64": "0.54.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "tar": "^7.5.11",