kablan 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # Kablan
2
+
3
+ Run coding agents — Claude Code, Codex, Gemini CLI, Amp and others — against your repositories
4
+ from a board, and watch them work.
5
+
6
+ Each task gets its own git worktree and its own branch, so several agents can run at once without
7
+ standing on each other. You follow the conversation, review the diff, start the project's dev
8
+ server, and merge or open a PR when it looks right.
9
+
10
+ ## Run it
11
+
12
+ ```bash
13
+ npx kablan
14
+ ```
15
+
16
+ Nothing to install. This downloads the binary for your platform, caches it under `~/.kablan/bin`,
17
+ and opens Kablan in your browser.
18
+
19
+ ## Install it as an app (macOS)
20
+
21
+ ```bash
22
+ npx kablan --install
23
+ ```
24
+
25
+ Puts `Kablan.app` in `~/Applications`. Opening it starts Kablan in the background — no terminal
26
+ window — and opens your browser. Output goes to `~/Library/Logs/Kablan/kablan.log`.
27
+
28
+ Because the bundle is assembled on your machine rather than downloaded, macOS does not quarantine
29
+ it: no "unidentified developer" prompt, and nothing to notarise.
30
+
31
+ - Update: `npx kablan@latest --install`
32
+ - Remove: `npx kablan --uninstall` (cached binaries stay in `~/.kablan/bin`)
33
+
34
+ ## What you need
35
+
36
+ - **Node 18 or newer**, to run this wrapper.
37
+ - **A coding agent, already authenticated.** Kablan drives each agent's own CLI, so it uses the
38
+ subscription you already have and never sees your model credentials.
39
+ - **Git.** Every attempt is a worktree on its own branch.
40
+
41
+ Platforms: macOS on Apple silicon and Intel, Linux x64, Windows x64.
42
+
43
+ ## Environment
44
+
45
+ | Variable | What it does |
46
+ | --- | --- |
47
+ | `PORT` | Port to serve on. Defaults to one the OS picks. |
48
+ | `HOST` | Host to bind. Defaults to `127.0.0.1`. |
49
+ | `KABLAN_LOCAL=1` | Use binaries from `npx-cli/dist/` instead of a release, for development. |
50
+ | `KABLAN_DEBUG=1` | Verbose wrapper output. |
51
+
52
+ ## MCP
53
+
54
+ ```bash
55
+ npx kablan --mcp
56
+ ```
57
+
58
+ Runs the MCP task server on stdio, so an agent can create and update Kablan tasks itself.
59
+
60
+ ## About
61
+
62
+ Kablan is a fork of [Vibe Kanban](https://github.com/BloopAI/vibe-kanban) by Bloop AI, used under
63
+ the Apache License 2.0. It is not affiliated with or endorsed by Bloop AI.
64
+
65
+ Source and issues: [github.com/AmarShaked/kablan.dev](https://github.com/AmarShaked/kablan.dev)
Binary file
package/bin/cli.js ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync, spawn } = require("child_process");
4
+ const AdmZip = require("adm-zip");
5
+ const path = require("path");
6
+ const fs = require("fs");
7
+ const { ensureBinary, BINARY_TAG, CACHE_DIR, LOCAL_DEV_MODE, LOCAL_DIST_DIR, getLatestVersion } = require("./download");
8
+ const { installMacApp, uninstallMacApp, appDir } = require("./install-app");
9
+
10
+ const CLI_VERSION = require("../package.json").version;
11
+
12
+ // Resolve effective arch for our published 64-bit binaries only.
13
+ // Any ARM → arm64; anything else → x64. On macOS, handle Rosetta.
14
+ function getEffectiveArch() {
15
+ const platform = process.platform;
16
+ const nodeArch = process.arch;
17
+
18
+ if (platform === "darwin") {
19
+ // If Node itself is arm64, we're natively on Apple silicon
20
+ if (nodeArch === "arm64") return "arm64";
21
+
22
+ // Otherwise check for Rosetta translation
23
+ try {
24
+ const translated = execSync("sysctl -in sysctl.proc_translated", {
25
+ encoding: "utf8",
26
+ }).trim();
27
+ if (translated === "1") return "arm64";
28
+ } catch {
29
+ // sysctl key not present → assume true Intel
30
+ }
31
+ return "x64";
32
+ }
33
+
34
+ // Non-macOS: coerce to broad families we support
35
+ if (/arm/i.test(nodeArch)) return "arm64";
36
+
37
+ // On Windows with 32-bit Node (ia32), detect OS arch via env
38
+ if (platform === "win32") {
39
+ const pa = process.env.PROCESSOR_ARCHITECTURE || "";
40
+ const paw = process.env.PROCESSOR_ARCHITEW6432 || "";
41
+ if (/arm/i.test(pa) || /arm/i.test(paw)) return "arm64";
42
+ }
43
+
44
+ return "x64";
45
+ }
46
+
47
+ const platform = process.platform;
48
+ const arch = getEffectiveArch();
49
+
50
+ // Map to our build target names
51
+ function getPlatformDir() {
52
+ if (platform === "linux" && arch === "x64") return "linux-x64";
53
+ if (platform === "linux" && arch === "arm64") return "linux-arm64";
54
+ if (platform === "win32" && arch === "x64") return "windows-x64";
55
+ if (platform === "win32" && arch === "arm64") return "windows-arm64";
56
+ if (platform === "darwin" && arch === "x64") return "macos-x64";
57
+ if (platform === "darwin" && arch === "arm64") return "macos-arm64";
58
+
59
+ console.error(`Unsupported platform: ${platform}-${arch}`);
60
+ console.error("Supported platforms:");
61
+ console.error(" - Linux x64");
62
+ console.error(" - Linux ARM64");
63
+ console.error(" - Windows x64");
64
+ console.error(" - Windows ARM64");
65
+ console.error(" - macOS x64 (Intel)");
66
+ console.error(" - macOS ARM64 (Apple Silicon)");
67
+ process.exit(1);
68
+ }
69
+
70
+ function getBinaryName(base) {
71
+ return platform === "win32" ? `${base}.exe` : base;
72
+ }
73
+
74
+ const platformDir = getPlatformDir();
75
+ // In local dev mode, extract directly to dist directory; otherwise use global cache
76
+ const versionCacheDir = LOCAL_DEV_MODE
77
+ ? path.join(LOCAL_DIST_DIR, platformDir)
78
+ : path.join(CACHE_DIR, BINARY_TAG, platformDir);
79
+
80
+ function showProgress(downloaded, total) {
81
+ const percent = total ? Math.round((downloaded / total) * 100) : 0;
82
+ const mb = (downloaded / (1024 * 1024)).toFixed(1);
83
+ const totalMb = total ? (total / (1024 * 1024)).toFixed(1) : "?";
84
+ process.stderr.write(`\r Downloading: ${mb}MB / ${totalMb}MB (${percent}%)`);
85
+ }
86
+
87
+ async function extractAndRun(baseName, launch) {
88
+ const binName = getBinaryName(baseName);
89
+ const binPath = path.join(versionCacheDir, binName);
90
+ const zipPath = path.join(versionCacheDir, `${baseName}.zip`);
91
+
92
+ // Clean old binary if exists
93
+ try {
94
+ if (fs.existsSync(binPath)) {
95
+ fs.unlinkSync(binPath);
96
+ }
97
+ } catch (err) {
98
+ if (process.env.KABLAN_DEBUG) {
99
+ console.warn(`Warning: Could not delete existing binary: ${err.message}`);
100
+ }
101
+ }
102
+
103
+ // Download if not cached
104
+ if (!fs.existsSync(zipPath)) {
105
+ console.error(`Downloading ${baseName}...`);
106
+ try {
107
+ await ensureBinary(platformDir, baseName, showProgress);
108
+ console.error(""); // newline after progress
109
+ } catch (err) {
110
+ console.error(`\nDownload failed: ${err.message}`);
111
+ process.exit(1);
112
+ }
113
+ }
114
+
115
+ // Extract
116
+ if (!fs.existsSync(binPath)) {
117
+ try {
118
+ const zip = new AdmZip(zipPath);
119
+ zip.extractAllTo(versionCacheDir, true);
120
+ } catch (err) {
121
+ console.error("Extraction failed:", err.message);
122
+ try {
123
+ fs.unlinkSync(zipPath);
124
+ } catch {}
125
+ process.exit(1);
126
+ }
127
+ }
128
+
129
+ if (!fs.existsSync(binPath)) {
130
+ console.error(`Extracted binary not found at: ${binPath}`);
131
+ console.error("This usually indicates a corrupt download. Please try again.");
132
+ process.exit(1);
133
+ }
134
+
135
+ // Set permissions (non-Windows)
136
+ if (platform !== "win32") {
137
+ try {
138
+ fs.chmodSync(binPath, 0o755);
139
+ } catch {}
140
+ }
141
+
142
+ return launch(binPath);
143
+ }
144
+
145
+ async function main() {
146
+ fs.mkdirSync(versionCacheDir, { recursive: true });
147
+
148
+ const args = process.argv.slice(2);
149
+ const isMcpMode = args.includes("--mcp");
150
+
151
+ // `--install` puts a double-clickable app in ~/Applications and exits; it never starts a
152
+ // server itself, so re-running it to update is safe while Kablan is open.
153
+ if (args.includes("--uninstall")) {
154
+ const removed = uninstallMacApp();
155
+ console.log(removed ? `Removed ${removed}` : `Nothing installed at ${appDir()}`);
156
+ console.log("Cached binaries are still in ~/.kablan/bin; delete that to reclaim the space.");
157
+ return;
158
+ }
159
+
160
+ if (args.includes("--install")) {
161
+ await extractAndRun("kablan", (bin) => {
162
+ const app = installMacApp(bin, CLI_VERSION);
163
+ console.log(`\nInstalled ${app}`);
164
+ console.log("Open it from Launchpad or Spotlight — it starts Kablan in the background");
165
+ console.log("and opens your browser. Logs: ~/Library/Logs/Kablan/kablan.log");
166
+ console.log("\nUpdate with `npx kablan@latest --install`, remove with `npx kablan --uninstall`.");
167
+ });
168
+ return;
169
+ }
170
+
171
+ // Non-blocking update check. Skipped in MCP mode, where stdout is a protocol stream, and in
172
+ // local dev mode, where the binaries did not come from a release.
173
+ if (!isMcpMode && !LOCAL_DEV_MODE) {
174
+ getLatestVersion()
175
+ .then((latest) => {
176
+ if (latest && latest !== CLI_VERSION) {
177
+ setTimeout(() => {
178
+ console.log(`\nUpdate available: ${CLI_VERSION} -> ${latest}`);
179
+ console.log(`Run: npx kablan@latest`);
180
+ }, 2000);
181
+ }
182
+ })
183
+ .catch(() => {});
184
+ }
185
+
186
+ if (isMcpMode) {
187
+ await extractAndRun("kablan-mcp", (bin) => {
188
+ const proc = spawn(bin, [], { stdio: "inherit" });
189
+ proc.on("exit", (c) => process.exit(c || 0));
190
+ proc.on("error", (e) => {
191
+ console.error("MCP server error:", e.message);
192
+ process.exit(1);
193
+ });
194
+ process.on("SIGINT", () => {
195
+ proc.kill("SIGINT");
196
+ });
197
+ process.on("SIGTERM", () => proc.kill("SIGTERM"));
198
+ });
199
+ } else {
200
+ const modeLabel = LOCAL_DEV_MODE ? " (local dev)" : "";
201
+ console.log(`Starting kablan v${CLI_VERSION}${modeLabel}...`);
202
+ await extractAndRun("kablan", (bin) => {
203
+ if (platform === "win32") {
204
+ execSync(`"${bin}"`, { stdio: "inherit" });
205
+ } else {
206
+ execSync(`"${bin}"`, { stdio: "inherit" });
207
+ }
208
+ });
209
+ }
210
+ }
211
+
212
+ main().catch((err) => {
213
+ console.error("Fatal error:", err.message);
214
+ if (process.env.KABLAN_DEBUG) {
215
+ console.error(err.stack);
216
+ }
217
+ process.exit(1);
218
+ });
@@ -0,0 +1,195 @@
1
+ const https = require("https");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const crypto = require("crypto");
5
+
6
+ /**
7
+ * Where the binaries come from.
8
+ *
9
+ * Upstream hosted these in a Cloudflare R2 bucket whose URL was substituted in at pack time —
10
+ * infrastructure this fork does not have, which is why `npx kablan` could never have worked by
11
+ * renaming the package alone. They come from this repository's own GitHub release instead: one
12
+ * artifact store, one place to look when a download fails, and no second account to keep alive.
13
+ *
14
+ * The tag is derived from this package's own version rather than injected, which makes it
15
+ * impossible to publish a package pointing at binaries from a different release.
16
+ */
17
+ const REPO = "AmarShaked/kablan.dev";
18
+ const PKG_VERSION = require("../package.json").version;
19
+ const BINARY_TAG = `v${PKG_VERSION}`;
20
+ const RELEASE_BASE = `https://github.com/${REPO}/releases/download/${BINARY_TAG}`;
21
+
22
+ const CACHE_DIR = path.join(require("os").homedir(), ".kablan", "bin");
23
+
24
+ // Local development mode: use binaries from npx-cli/dist/ instead of the release.
25
+ // Only activate if dist/ exists (i.e., running from source after local-build.sh)
26
+ const LOCAL_DIST_DIR = path.join(__dirname, "..", "dist");
27
+ const LOCAL_DEV_MODE = fs.existsSync(LOCAL_DIST_DIR) || process.env.KABLAN_LOCAL === "1";
28
+
29
+ function get(url, onResponse, reject) {
30
+ https
31
+ .get(url, { headers: { "User-Agent": `kablan-cli/${PKG_VERSION}` } }, (res) => {
32
+ if (res.statusCode === 301 || res.statusCode === 302) {
33
+ return get(res.headers.location, onResponse, reject);
34
+ }
35
+ onResponse(res);
36
+ })
37
+ .on("error", reject);
38
+ }
39
+
40
+ async function fetchText(url) {
41
+ return new Promise((resolve, reject) => {
42
+ get(
43
+ url,
44
+ (res) => {
45
+ if (res.statusCode !== 200) {
46
+ return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
47
+ }
48
+ let data = "";
49
+ res.on("data", (chunk) => (data += chunk));
50
+ res.on("end", () => resolve(data));
51
+ },
52
+ reject
53
+ );
54
+ });
55
+ }
56
+
57
+ async function fetchJson(url) {
58
+ return JSON.parse(await fetchText(url));
59
+ }
60
+
61
+ async function downloadFile(url, destPath, expectedSha256, onProgress) {
62
+ const tempPath = destPath + ".tmp";
63
+ return new Promise((resolve, reject) => {
64
+ const file = fs.createWriteStream(tempPath);
65
+ const hash = crypto.createHash("sha256");
66
+
67
+ const cleanup = () => {
68
+ try {
69
+ fs.unlinkSync(tempPath);
70
+ } catch {}
71
+ };
72
+
73
+ get(
74
+ url,
75
+ (res) => {
76
+ if (res.statusCode !== 200) {
77
+ file.close();
78
+ cleanup();
79
+ return reject(new Error(`HTTP ${res.statusCode} downloading ${url}`));
80
+ }
81
+
82
+ const totalSize = parseInt(res.headers["content-length"], 10);
83
+ let downloadedSize = 0;
84
+
85
+ res.on("data", (chunk) => {
86
+ downloadedSize += chunk.length;
87
+ hash.update(chunk);
88
+ if (onProgress) onProgress(downloadedSize, totalSize);
89
+ });
90
+ res.pipe(file);
91
+
92
+ file.on("finish", () => {
93
+ file.close();
94
+ const actualSha256 = hash.digest("hex");
95
+ if (expectedSha256 && actualSha256 !== expectedSha256) {
96
+ cleanup();
97
+ reject(
98
+ new Error(`Checksum mismatch: expected ${expectedSha256}, got ${actualSha256}`)
99
+ );
100
+ } else {
101
+ try {
102
+ fs.renameSync(tempPath, destPath);
103
+ resolve(destPath);
104
+ } catch (err) {
105
+ cleanup();
106
+ reject(err);
107
+ }
108
+ }
109
+ });
110
+ },
111
+ (err) => {
112
+ file.close();
113
+ cleanup();
114
+ reject(err);
115
+ }
116
+ );
117
+ });
118
+ }
119
+
120
+ /**
121
+ * The release's checksum file, parsed into { filename: sha256 }. Fetched at most once per run.
122
+ * A release without one still installs — the download is over HTTPS from a pinned tag — but the
123
+ * extra check is cheap and catches a truncated or swapped asset.
124
+ */
125
+ let checksumsPromise = null;
126
+ function getChecksums() {
127
+ if (!checksumsPromise) {
128
+ checksumsPromise = fetchText(`${RELEASE_BASE}/sha256sums.txt`)
129
+ .then((text) => {
130
+ const map = {};
131
+ for (const line of text.split("\n")) {
132
+ const m = line.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/i);
133
+ if (m) map[m[2]] = m[1].toLowerCase();
134
+ }
135
+ return map;
136
+ })
137
+ .catch(() => ({}));
138
+ }
139
+ return checksumsPromise;
140
+ }
141
+
142
+ async function ensureBinary(platform, binaryName, onProgress) {
143
+ // In local dev mode, use binaries directly from npx-cli/dist/
144
+ if (LOCAL_DEV_MODE) {
145
+ const localZipPath = path.join(LOCAL_DIST_DIR, platform, `${binaryName}.zip`);
146
+ if (fs.existsSync(localZipPath)) {
147
+ return localZipPath;
148
+ }
149
+ throw new Error(
150
+ `Local binary not found: ${localZipPath}\n` +
151
+ `Run ./local-build.sh first to build the binaries.`
152
+ );
153
+ }
154
+
155
+ const cacheDir = path.join(CACHE_DIR, BINARY_TAG, platform);
156
+ const zipPath = path.join(cacheDir, `${binaryName}.zip`);
157
+
158
+ if (fs.existsSync(zipPath)) return zipPath;
159
+
160
+ fs.mkdirSync(cacheDir, { recursive: true });
161
+
162
+ // Assets are flat on the release, so the platform is part of the name.
163
+ const assetName = `${binaryName}-${platform}.zip`;
164
+ const checksums = await getChecksums();
165
+
166
+ try {
167
+ await downloadFile(`${RELEASE_BASE}/${assetName}`, zipPath, checksums[assetName], onProgress);
168
+ } catch (err) {
169
+ if (/HTTP 404/.test(err.message)) {
170
+ throw new Error(
171
+ `${binaryName} is not published for ${platform} in ${BINARY_TAG}.\n` +
172
+ `See https://github.com/${REPO}/releases/tag/${BINARY_TAG} for what that release contains.`
173
+ );
174
+ }
175
+ throw err;
176
+ }
177
+
178
+ return zipPath;
179
+ }
180
+
181
+ async function getLatestVersion() {
182
+ const release = await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
183
+ return String(release.tag_name || "").replace(/^v/, "");
184
+ }
185
+
186
+ module.exports = {
187
+ REPO,
188
+ RELEASE_BASE,
189
+ BINARY_TAG,
190
+ CACHE_DIR,
191
+ LOCAL_DEV_MODE,
192
+ LOCAL_DIST_DIR,
193
+ ensureBinary,
194
+ getLatestVersion,
195
+ };
@@ -0,0 +1,136 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const { execFileSync } = require("child_process");
5
+
6
+ /**
7
+ * Installs Kablan into ~/Applications as a real macOS app.
8
+ *
9
+ * The point is a double-clickable icon that starts Kablan in the background — no Terminal
10
+ * window, no command to remember. It also happens to sidestep Gatekeeper: the bundle is
11
+ * assembled here, on this machine, from a binary this process downloaded, so nothing carries the
12
+ * quarantine flag that produces "unidentified developer". A downloaded .app would need
13
+ * notarisation and a paid Apple account; one built locally needs neither.
14
+ */
15
+
16
+ const APP_NAME = "Kablan";
17
+ const BUNDLE_ID = "dev.kablan.app";
18
+
19
+ function appDir() {
20
+ // ~/Applications rather than /Applications: it needs no password, and Spotlight and Launchpad
21
+ // both index it.
22
+ return path.join(os.homedir(), "Applications", `${APP_NAME}.app`);
23
+ }
24
+
25
+ /**
26
+ * A GUI-launched process inherits a minimal PATH — roughly /usr/bin:/bin — so the coding agents,
27
+ * node and pnpm are all missing and every task fails at its first command. The launcher asks the
28
+ * login shell what PATH really is, the same thing a terminal would have.
29
+ */
30
+ function launcherScript() {
31
+ return `#!/bin/bash
32
+ # Generated by \`npx kablan --install\`. Re-run that to update.
33
+ set -u
34
+
35
+ HERE="$(cd "$(dirname "$0")" && pwd)"
36
+
37
+ # Adopt the login shell's PATH; a GUI launch would otherwise see almost nothing.
38
+ SHELL_BIN="\${SHELL:-/bin/zsh}"
39
+ RESOLVED="$("$SHELL_BIN" -ilc 'printf %s "$PATH"' 2>/dev/null || true)"
40
+ if [ -n "$RESOLVED" ]; then
41
+ export PATH="$RESOLVED"
42
+ fi
43
+ for dir in /opt/homebrew/bin /usr/local/bin /usr/bin /bin; do
44
+ case ":$PATH:" in *":$dir:"*) ;; *) PATH="$dir:$PATH" ;; esac
45
+ done
46
+ export PATH
47
+
48
+ # There is no terminal attached, so keep output where it can be read after the fact.
49
+ LOG_DIR="$HOME/Library/Logs/${APP_NAME}"
50
+ mkdir -p "$LOG_DIR"
51
+
52
+ exec "$HERE/${APP_NAME.toLowerCase()}-server" >>"$LOG_DIR/${APP_NAME.toLowerCase()}.log" 2>&1
53
+ `;
54
+ }
55
+
56
+ function infoPlist(version) {
57
+ return `<?xml version="1.0" encoding="UTF-8"?>
58
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
59
+ <plist version="1.0">
60
+ <dict>
61
+ <key>CFBundleName</key><string>${APP_NAME}</string>
62
+ <key>CFBundleDisplayName</key><string>${APP_NAME}</string>
63
+ <key>CFBundleIdentifier</key><string>${BUNDLE_ID}</string>
64
+ <key>CFBundleVersion</key><string>${version}</string>
65
+ <key>CFBundleShortVersionString</key><string>${version}</string>
66
+ <key>CFBundleExecutable</key><string>${APP_NAME}</string>
67
+ <key>CFBundleIconFile</key><string>${APP_NAME}</string>
68
+ <key>CFBundlePackageType</key><string>APPL</string>
69
+ <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
70
+ <key>NSHighResolutionCapable</key><true/>
71
+ <key>LSMinimumSystemVersion</key><string>11.0</string>
72
+ </dict>
73
+ </plist>
74
+ `;
75
+ }
76
+
77
+ /**
78
+ * @param binPath the extracted server binary to install
79
+ * @param version the wrapper's version, shown in Finder's Get Info
80
+ */
81
+ function installMacApp(binPath, version) {
82
+ if (process.platform !== "darwin") {
83
+ throw new Error(
84
+ `--install builds a macOS app bundle and this is ${process.platform}.\n` +
85
+ `On Linux and Windows, run \`npx kablan\` directly.`
86
+ );
87
+ }
88
+
89
+ const app = appDir();
90
+ const macos = path.join(app, "Contents", "MacOS");
91
+ const resources = path.join(app, "Contents", "Resources");
92
+
93
+ // Replace wholesale rather than merging, so an older layout cannot leave stale files behind.
94
+ fs.rmSync(app, { recursive: true, force: true });
95
+ fs.mkdirSync(macos, { recursive: true });
96
+ fs.mkdirSync(resources, { recursive: true });
97
+
98
+ fs.writeFileSync(path.join(app, "Contents", "Info.plist"), infoPlist(version));
99
+
100
+ const serverName = `${APP_NAME.toLowerCase()}-server`;
101
+ fs.copyFileSync(binPath, path.join(macos, serverName));
102
+ fs.chmodSync(path.join(macos, serverName), 0o755);
103
+
104
+ const launcher = path.join(macos, APP_NAME);
105
+ fs.writeFileSync(launcher, launcherScript());
106
+ fs.chmodSync(launcher, 0o755);
107
+
108
+ const icon = path.join(__dirname, "..", "assets", `${APP_NAME}.icns`);
109
+ if (fs.existsSync(icon)) {
110
+ fs.copyFileSync(icon, path.join(resources, `${APP_NAME}.icns`));
111
+ }
112
+
113
+ // Finder caches icons per bundle; without this the app can show a blank page icon until
114
+ // something else invalidates it.
115
+ try {
116
+ execFileSync("/usr/bin/touch", [app]);
117
+ execFileSync(
118
+ "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister",
119
+ ["-f", app],
120
+ { stdio: "ignore" }
121
+ );
122
+ } catch {
123
+ // Cosmetic only — the app works either way.
124
+ }
125
+
126
+ return app;
127
+ }
128
+
129
+ function uninstallMacApp() {
130
+ const app = appDir();
131
+ if (!fs.existsSync(app)) return null;
132
+ fs.rmSync(app, { recursive: true, force: true });
133
+ return app;
134
+ }
135
+
136
+ module.exports = { installMacApp, uninstallMacApp, appDir, APP_NAME };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "kablan",
3
+ "version": "0.5.0",
4
+ "description": "NPX wrapper around the Kablan server and its MCP task server",
5
+ "license": "Apache-2.0",
6
+ "author": "Shaked Amar",
7
+ "homepage": "https://github.com/AmarShaked/kablan.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/AmarShaked/kablan.dev.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/AmarShaked/kablan.dev/issues"
14
+ },
15
+ "keywords": [
16
+ "claude-code",
17
+ "codex",
18
+ "coding-agent",
19
+ "kanban",
20
+ "git-worktree",
21
+ "ai"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "bin": {
27
+ "kablan": "bin/cli.js"
28
+ },
29
+ "files": [
30
+ "bin",
31
+ "assets"
32
+ ],
33
+ "dependencies": {
34
+ "adm-zip": "^0.5.16"
35
+ },
36
+ "private": false
37
+ }