kangentic 0.1.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/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ See the LICENSE file in the root of this repository:
2
+ https://github.com/Kangentic/kangentic/blob/main/LICENSE
3
+
4
+ SPDX-License-Identifier: AGPL-3.0-only
5
+ Copyright (C) 2025-2026 VORPAHL LLC. All rights reserved.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # Kangentic
2
+
3
+ **Visual Agent Orchestration for Claude Code**
4
+
5
+ One command to install and launch [Kangentic](https://github.com/Kangentic/kangentic) -- a cross-platform desktop Kanban for Claude Code agents.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npx kangentic
11
+ ```
12
+
13
+ This downloads the pre-built binary for your platform from GitHub Releases, installs it, and launches the app. After the first run, the app manages its own updates automatically (Windows and macOS).
14
+
15
+ ### Open a specific project
16
+
17
+ ```bash
18
+ npx kangentic /path/to/your/project
19
+ ```
20
+
21
+ ## How it works
22
+
23
+ 1. Detects your platform (Windows, macOS, Linux) and architecture (x64, arm64)
24
+ 2. Downloads the matching installer from [GitHub Releases](https://github.com/Kangentic/kangentic/releases)
25
+ 3. Installs per platform:
26
+ - **Windows:** Runs NSIS installer silently to `%LOCALAPPDATA%\Programs\Kangentic\`
27
+ - **macOS:** Extracts .zip to `~/Applications/Kangentic.app`
28
+ - **Linux:** Installs .deb via `sudo dpkg -i` (prompts for password)
29
+ 4. Launches the app
30
+
31
+ ## Updates
32
+
33
+ After the initial install, you generally don't need to run `npx kangentic` again:
34
+
35
+ - **Windows:** electron-updater installs new versions silently on restart.
36
+ - **macOS:** Built-in auto-updater downloads in the background and prompts to restart. (Requires code signing.)
37
+ - **Linux:** No auto-update. Re-run `npx kangentic` or download from [GitHub Releases](https://github.com/Kangentic/kangentic/releases).
38
+
39
+ ### Install a specific version
40
+
41
+ ```bash
42
+ npx kangentic@0.2.0
43
+ ```
44
+
45
+ The launcher version matches the app version. Specifying a version downloads that exact release.
46
+
47
+ ## Prerequisites
48
+
49
+ - **[Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)** installed and on your PATH
50
+ - **[Git 2.25+](https://git-scm.com/)** for worktree support
51
+
52
+ ## Links
53
+
54
+ - [GitHub](https://github.com/Kangentic/kangentic)
55
+ - [Documentation](https://github.com/Kangentic/kangentic/tree/main/docs)
56
+ - [Installation Guide](https://github.com/Kangentic/kangentic/blob/main/docs/installation.md)
57
+
58
+ ## License
59
+
60
+ [AGPL-3.0-only](https://github.com/Kangentic/kangentic/blob/main/LICENSE)
@@ -0,0 +1,409 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Kangentic npx launcher
4
+ // Downloads, installs, and launches Kangentic from GitHub Releases.
5
+ // Zero dependencies -- pure Node.js built-ins only.
6
+
7
+ 'use strict';
8
+
9
+ const https = require('https');
10
+ const http = require('http');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+ const { execFileSync, spawn } = require('child_process');
15
+
16
+ const VERSION = require('../package.json').version;
17
+ const REPO_OWNER = 'Kangentic';
18
+ const REPO_NAME = 'kangentic';
19
+ const MAX_REDIRECTS = 5;
20
+
21
+ // --- Platform detection ---
22
+
23
+ function getPlatformInfo() {
24
+ const platform = process.platform;
25
+ const arch = process.arch;
26
+
27
+ if (platform === 'win32') {
28
+ return { platform: 'win32', arch: 'x64', extension: 'exe' };
29
+ }
30
+ if (platform === 'darwin') {
31
+ return { platform: 'darwin', arch, extension: 'zip' };
32
+ }
33
+ if (platform === 'linux') {
34
+ return { platform: 'linux', arch: 'x64', extension: 'deb' };
35
+ }
36
+
37
+ return null;
38
+ }
39
+
40
+ // --- Install path detection ---
41
+
42
+ function getInstallPath(platformInfo) {
43
+ if (platformInfo.platform === 'win32') {
44
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
45
+ return path.join(localAppData, 'Programs', 'Kangentic', 'Kangentic.exe');
46
+ }
47
+ if (platformInfo.platform === 'darwin') {
48
+ const userApps = path.join(os.homedir(), 'Applications', 'Kangentic.app');
49
+ const systemApps = '/Applications/Kangentic.app';
50
+ if (fs.existsSync(userApps)) return userApps;
51
+ if (fs.existsSync(systemApps)) return systemApps;
52
+ return userApps; // default install target
53
+ }
54
+ if (platformInfo.platform === 'linux') {
55
+ return '/usr/bin/kangentic';
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function isInstalled(platformInfo) {
61
+ const installPath = getInstallPath(platformInfo);
62
+ if (!fs.existsSync(installPath)) return false;
63
+
64
+ // On Windows, the exe existing is sufficient (NSIS install)
65
+ if (platformInfo.platform === 'win32') {
66
+ return true;
67
+ }
68
+
69
+ // On macOS, check the app bundle's version in Info.plist
70
+ if (platformInfo.platform === 'darwin') {
71
+ try {
72
+ const plistPath = path.join(installPath, 'Contents', 'Info.plist');
73
+ const plistContent = fs.readFileSync(plistPath, 'utf-8');
74
+ const versionMatch = plistContent.match(/<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/);
75
+ if (versionMatch && versionMatch[1] !== VERSION) return false;
76
+ } catch {
77
+ // Can't read plist, treat as not installed
78
+ return false;
79
+ }
80
+ }
81
+
82
+ // On Linux, check `kangentic --version` output
83
+ if (platformInfo.platform === 'linux') {
84
+ try {
85
+ const output = execFileSync(installPath, ['--version'], { encoding: 'utf-8', timeout: 5000 }).trim();
86
+ if (!output.includes(VERSION)) return false;
87
+ } catch {
88
+ // Can't check version, treat as installed (binary exists)
89
+ }
90
+ }
91
+
92
+ return true;
93
+ }
94
+
95
+ // --- Download URL construction ---
96
+
97
+ function getArtifactFilename(platformInfo) {
98
+ const version = VERSION;
99
+
100
+ if (platformInfo.platform === 'win32') {
101
+ // NSIS produces "Kangentic-Setup-X.Y.Z.exe"
102
+ return `Kangentic-Setup-${version}.exe`;
103
+ }
104
+ if (platformInfo.platform === 'darwin') {
105
+ return `Kangentic-${version}-${platformInfo.arch}-mac.zip`;
106
+ }
107
+ if (platformInfo.platform === 'linux') {
108
+ // Check if rpm-based system
109
+ try {
110
+ execFileSync('which', ['rpm'], { stdio: 'ignore' });
111
+ const hasApt = (() => {
112
+ try {
113
+ execFileSync('which', ['apt'], { stdio: 'ignore' });
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ })();
119
+ if (!hasApt) {
120
+ return `kangentic-${version}-1.x86_64.rpm`;
121
+ }
122
+ } catch {
123
+ // not rpm-based, use deb
124
+ }
125
+ return `kangentic_${version}_amd64.deb`;
126
+ }
127
+ return null;
128
+ }
129
+
130
+ function getDownloadUrl(platformInfo) {
131
+ const filename = getArtifactFilename(platformInfo);
132
+ if (!filename) return null;
133
+ return `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/v${VERSION}/${filename}`;
134
+ }
135
+
136
+ // --- HTTP download with redirect following ---
137
+
138
+ function download(url, destinationPath, redirectCount) {
139
+ if (redirectCount === undefined) redirectCount = 0;
140
+
141
+ return new Promise((resolve, reject) => {
142
+ if (redirectCount > MAX_REDIRECTS) {
143
+ reject(new Error('Too many redirects'));
144
+ return;
145
+ }
146
+
147
+ const protocol = url.startsWith('https') ? https : http;
148
+ const request = protocol.get(url, (response) => {
149
+ // Follow redirects (GitHub -> S3)
150
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
151
+ download(response.headers.location, destinationPath, redirectCount + 1)
152
+ .then(resolve)
153
+ .catch(reject);
154
+ return;
155
+ }
156
+
157
+ if (response.statusCode !== 200) {
158
+ reject(new Error(`Download failed: HTTP ${response.statusCode}`));
159
+ return;
160
+ }
161
+
162
+ const totalBytes = parseInt(response.headers['content-length'] || '0', 10);
163
+ let downloadedBytes = 0;
164
+
165
+ const fileStream = fs.createWriteStream(destinationPath);
166
+ response.on('data', (chunk) => {
167
+ downloadedBytes += chunk.length;
168
+ if (totalBytes > 0) {
169
+ const megabytesDownloaded = (downloadedBytes / 1024 / 1024).toFixed(1);
170
+ const megabytesTotal = (totalBytes / 1024 / 1024).toFixed(1);
171
+ const percent = Math.round((downloadedBytes / totalBytes) * 100);
172
+ process.stdout.write(`\rDownloading... ${megabytesDownloaded}/${megabytesTotal} MB (${percent}%)`);
173
+ } else {
174
+ const megabytesDownloaded = (downloadedBytes / 1024 / 1024).toFixed(1);
175
+ process.stdout.write(`\rDownloading... ${megabytesDownloaded} MB`);
176
+ }
177
+ });
178
+
179
+ response.pipe(fileStream);
180
+
181
+ fileStream.on('finish', () => {
182
+ process.stdout.write('\n');
183
+ fileStream.close();
184
+ resolve();
185
+ });
186
+
187
+ fileStream.on('error', reject);
188
+ });
189
+
190
+ request.on('error', reject);
191
+ });
192
+ }
193
+
194
+ // --- Platform-specific install ---
195
+
196
+ function installWindows(artifactPath) {
197
+ console.log('Installing Kangentic (NSIS installer)...');
198
+ execFileSync(artifactPath, ['/S'], { stdio: 'ignore' });
199
+ console.log('Installation complete.');
200
+ }
201
+
202
+ function installMacOS(artifactPath) {
203
+ const tempDir = path.dirname(artifactPath);
204
+ const extractDir = path.join(tempDir, 'kangentic-extract');
205
+
206
+ console.log('Extracting Kangentic.app...');
207
+ fs.mkdirSync(extractDir, { recursive: true });
208
+ execFileSync('unzip', ['-o', '-q', artifactPath, '-d', extractDir]);
209
+
210
+ const appSource = path.join(extractDir, 'Kangentic.app');
211
+ const appTarget = path.join(os.homedir(), 'Applications', 'Kangentic.app');
212
+
213
+ // Ensure ~/Applications exists
214
+ const userAppsDir = path.join(os.homedir(), 'Applications');
215
+ fs.mkdirSync(userAppsDir, { recursive: true });
216
+
217
+ // Remove old install if present
218
+ if (fs.existsSync(appTarget)) {
219
+ fs.rmSync(appTarget, { recursive: true, force: true });
220
+ }
221
+
222
+ console.log(`Installing to ${appTarget}...`);
223
+ fs.renameSync(appSource, appTarget);
224
+
225
+ // Clean up extract dir
226
+ fs.rmSync(extractDir, { recursive: true, force: true });
227
+
228
+ console.log('Installation complete.');
229
+ }
230
+
231
+ function installLinux(artifactPath) {
232
+ console.log('Installing Kangentic...');
233
+ if (artifactPath.endsWith('.deb')) {
234
+ console.log('Running: sudo dpkg -i (you may be prompted for your password)');
235
+ execFileSync('sudo', ['dpkg', '-i', artifactPath], { stdio: 'inherit' });
236
+ } else if (artifactPath.endsWith('.rpm')) {
237
+ console.log('Running: sudo rpm -i (you may be prompted for your password)');
238
+ execFileSync('sudo', ['rpm', '-i', artifactPath], { stdio: 'inherit' });
239
+ }
240
+ console.log('Installation complete.');
241
+ }
242
+
243
+ function install(platformInfo, artifactPath) {
244
+ if (platformInfo.platform === 'win32') {
245
+ installWindows(artifactPath);
246
+ } else if (platformInfo.platform === 'darwin') {
247
+ installMacOS(artifactPath);
248
+ } else if (platformInfo.platform === 'linux') {
249
+ installLinux(artifactPath);
250
+ }
251
+ }
252
+
253
+ // --- Launch ---
254
+
255
+ function launch(platformInfo, targetDir, dataDir) {
256
+ const installPath = getInstallPath(platformInfo);
257
+
258
+ if (!fs.existsSync(installPath)) {
259
+ console.error('Error: Kangentic installation not found after install.');
260
+ console.error(`Expected at: ${installPath}`);
261
+ process.exit(1);
262
+ }
263
+
264
+ console.log('Launching Kangentic...');
265
+
266
+ const childEnv = { ...process.env };
267
+ if (dataDir) {
268
+ childEnv.KANGENTIC_DATA_DIR = dataDir;
269
+ }
270
+
271
+ if (platformInfo.platform === 'win32') {
272
+ const child = spawn(installPath, [`--cwd=${targetDir}`], {
273
+ detached: true,
274
+ stdio: 'ignore',
275
+ env: childEnv,
276
+ });
277
+ child.unref();
278
+ } else if (platformInfo.platform === 'darwin') {
279
+ const openArgs = ['-a', installPath, '--args', `--cwd=${targetDir}`];
280
+ if (dataDir) {
281
+ openArgs.push(`--data-dir=${dataDir}`);
282
+ }
283
+ execFileSync('open', openArgs);
284
+ } else if (platformInfo.platform === 'linux') {
285
+ const child = spawn(installPath, [`--cwd=${targetDir}`], {
286
+ detached: true,
287
+ stdio: 'ignore',
288
+ env: childEnv,
289
+ });
290
+ child.unref();
291
+ }
292
+ }
293
+
294
+ // --- Config directory for temp downloads ---
295
+
296
+ function getTempDir() {
297
+ const platform = process.platform;
298
+ let base;
299
+ if (platform === 'win32') {
300
+ base = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
301
+ } else if (platform === 'darwin') {
302
+ base = path.join(os.homedir(), 'Library', 'Application Support');
303
+ } else {
304
+ base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
305
+ }
306
+ const tempDir = path.join(base, 'kangentic', 'launcher');
307
+ fs.mkdirSync(tempDir, { recursive: true });
308
+ return tempDir;
309
+ }
310
+
311
+ // --- Main ---
312
+
313
+ function parseDataDir(arguments_) {
314
+ for (let index = 0; index < arguments_.length; index++) {
315
+ const argument = arguments_[index];
316
+ if (argument.startsWith('--data-dir=')) {
317
+ return argument.slice('--data-dir='.length);
318
+ }
319
+ if (argument === '--data-dir' && index + 1 < arguments_.length) {
320
+ const nextArgument = arguments_[index + 1];
321
+ if (!nextArgument.startsWith('-')) {
322
+ return nextArgument;
323
+ }
324
+ }
325
+ }
326
+ return null;
327
+ }
328
+
329
+ function findTargetDir(arguments_) {
330
+ for (let index = 0; index < arguments_.length; index++) {
331
+ const argument = arguments_[index];
332
+ if (argument.startsWith('-')) {
333
+ // Skip --data-dir's value argument
334
+ if (argument === '--data-dir' && index + 1 < arguments_.length) {
335
+ index++;
336
+ }
337
+ continue;
338
+ }
339
+ return path.resolve(argument);
340
+ }
341
+ return process.cwd();
342
+ }
343
+
344
+ async function main() {
345
+ const arguments_ = process.argv.slice(2);
346
+
347
+ // Determine target directory (first positional argument, skipping flags and their values)
348
+ const targetDir = findTargetDir(arguments_);
349
+
350
+ // Resolve data directory: env var takes priority, then --data-dir flag
351
+ const dataDirFlag = parseDataDir(arguments_);
352
+ const dataDir = process.env.KANGENTIC_DATA_DIR || dataDirFlag;
353
+
354
+ // Detect platform
355
+ const platformInfo = getPlatformInfo();
356
+ if (!platformInfo) {
357
+ console.error(`Unsupported platform: ${process.platform} ${process.arch}`);
358
+ console.error(`Download manually from: https://github.com/${REPO_OWNER}/${REPO_NAME}/releases`);
359
+ process.exit(1);
360
+ }
361
+
362
+ // Check if already installed
363
+ if (isInstalled(platformInfo)) {
364
+ console.log(`Kangentic v${VERSION} is already installed.`);
365
+ launch(platformInfo, targetDir, dataDir);
366
+ return;
367
+ }
368
+
369
+ // Download
370
+ const downloadUrl = getDownloadUrl(platformInfo);
371
+ const artifactFilename = getArtifactFilename(platformInfo).replace(/%20/g, ' ');
372
+ const tempDir = getTempDir();
373
+ const artifactPath = path.join(tempDir, artifactFilename);
374
+
375
+ console.log(`Kangentic v${VERSION} is not installed. Downloading...`);
376
+ console.log(`URL: ${downloadUrl.replace(/%20/g, ' ')}`);
377
+
378
+ try {
379
+ await download(downloadUrl, artifactPath);
380
+ } catch (error) {
381
+ console.error(`\nDownload failed: ${error.message}`);
382
+ console.error(`\nDownload manually from: https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/tag/v${VERSION}`);
383
+ process.exit(1);
384
+ }
385
+
386
+ // Install
387
+ try {
388
+ install(platformInfo, artifactPath);
389
+ } catch (error) {
390
+ console.error(`\nInstallation failed: ${error.message}`);
391
+ console.error(`\nTry installing manually. The downloaded file is at: ${artifactPath}`);
392
+ process.exit(1);
393
+ }
394
+
395
+ // Clean up downloaded artifact
396
+ try {
397
+ fs.unlinkSync(artifactPath);
398
+ } catch {
399
+ // ignore cleanup errors
400
+ }
401
+
402
+ // Launch
403
+ launch(platformInfo, targetDir, dataDir);
404
+ }
405
+
406
+ main().catch((error) => {
407
+ console.error(`Unexpected error: ${error.message}`);
408
+ process.exit(1);
409
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "kangentic",
3
+ "version": "0.1.0",
4
+ "description": "Install and launch Kangentic -- visual agent orchestration for Claude Code",
5
+ "bin": {
6
+ "kangentic": "./bin/kangentic.js"
7
+ },
8
+ "repository": "https://github.com/Kangentic/kangentic",
9
+ "keywords": [
10
+ "kangentic",
11
+ "kanban",
12
+ "claude",
13
+ "claude-code",
14
+ "agent",
15
+ "electron",
16
+ "desktop"
17
+ ],
18
+ "author": {
19
+ "name": "Kangentic",
20
+ "url": "https://github.com/Kangentic"
21
+ },
22
+ "license": "AGPL-3.0-only",
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "files": [
27
+ "bin/",
28
+ "LICENSE",
29
+ "README.md"
30
+ ]
31
+ }