kandown 0.1.3 β†’ 0.2.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.
Files changed (4) hide show
  1. package/bin/kandown.js +117 -55
  2. package/bin/tui.js +18977 -729
  3. package/dist/index.html +350 -345
  4. package/package.json +5 -3
package/bin/kandown.js CHANGED
@@ -25,6 +25,16 @@
25
25
  * @exports none
26
26
  */
27
27
  /* eslint-disable no-console */
28
+ // πŸ“– DEV=false prevents Ink from loading react-devtools-core (CJS-only, breaks ESM).
29
+ // Must be set BEFORE any imports because ESM hoists all import statements.
30
+ process.env.DEV = 'false';
31
+ // πŸ“– Polyfill browser globals that some bundled modules expect.
32
+ if (typeof globalThis.self === 'undefined') Object.defineProperty(globalThis, 'self', { value: globalThis });
33
+ if (typeof globalThis.window === 'undefined') Object.defineProperty(globalThis, 'window', { value: globalThis });
34
+ // πŸ“– Make require() available in this ESM module so bundled __require() shims work.
35
+ // tsup's __require checks `typeof require !== "undefined"` β€” this makes it truthy.
36
+ import { createRequire } from 'node:module';
37
+ globalThis.require = createRequire(import.meta.url);
28
38
 
29
39
  import { fileURLToPath } from 'node:url';
30
40
  import { createServer } from 'node:http';
@@ -47,6 +57,51 @@ const PKG_ROOT = resolve(__dirname, '..');
47
57
  const DEFAULT_SERVE_PORT = 2048;
48
58
  const MAX_SERVE_PORT = 2060;
49
59
 
60
+ // πŸ“– Get current CLI version from package.json at PKG_ROOT
61
+ function getCurrentVersion() {
62
+ try {
63
+ const pkg = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'));
64
+ return pkg.version;
65
+ } catch { return null; }
66
+ }
67
+
68
+ // πŸ“– Check npm for a newer version and auto-update if outdated.
69
+ // Runs in background β€” does not block startup. Only activates when running from
70
+ // an installed npm package (not local dev source, where src/ exists).
71
+ // πŸ“– Uses npm install -g to self-upgrade, then re-spawns with the same arguments.
72
+ async function checkForUpdate(argv = process.argv) {
73
+ if (existsSync(join(PKG_ROOT, 'src'))) return; // local dev β€” skip
74
+ const current = getCurrentVersion();
75
+ if (!current) return;
76
+ try {
77
+ const { execSync } = await import('node:child_process');
78
+ const latest = String(execSync('npm view kandown version --json 2>/dev/null', {
79
+ timeout: 8000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
80
+ })).trim().replace(/^"|"$/g, '');
81
+ if (!latest || latest === current) return;
82
+
83
+ log(`${c.yellow}⚑ Auto-updating kandown ${c.reset}${c.dim}${current}${c.reset} ${c.yellow}β†’${c.reset} ${c.green}${latest}${c.reset}…`);
84
+ try {
85
+ execSync('npm install -g kandown 2>/dev/null', {
86
+ timeout: 30000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
87
+ });
88
+ const newVersion = String(execSync('npm view kandown version 2>/dev/null', {
89
+ timeout: 5000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
90
+ })).trim().replace(/^"|"$/g, '');
91
+ if (newVersion === latest) {
92
+ log(`${c.green}βœ“ Updated to v${newVersion}${c.reset} β€” restarting…`);
93
+ const child = spawn(process.argv[0], ['--experimental-vm-modules', ...argv.slice(1)], {
94
+ detached: true, stdio: 'ignore', env: { ...process.env } });
95
+ child.unref();
96
+ process.exit(0);
97
+ }
98
+ } catch {
99
+ log(`${c.yellow}⚠ Auto-update failed β€” will retry on next run${c.reset}`);
100
+ log(` Run ${c.cyan}npm install -g kandown${c.reset} to upgrade manually`);
101
+ }
102
+ } catch { /* offline or npm slow β€” silently skip */ }
103
+ }
104
+
50
105
  const c = {
51
106
  reset: '\x1b[0m',
52
107
  bold: '\x1b[1m',
@@ -65,8 +120,10 @@ const warn = (msg) => log(`${c.yellow}⚠${c.reset} ${msg}`);
65
120
  const err = (msg) => log(`${c.red}βœ—${c.reset} ${msg}`);
66
121
 
67
122
  function help() {
123
+ const v = getCurrentVersion() ?? '?';
68
124
  log(`
69
125
  ${c.bold}kandown${c.reset} ${c.dim}Β· file-based kanban backed by markdown${c.reset}
126
+ ${c.dim}v${v}${c.reset}
70
127
 
71
128
  ${c.bold}Usage:${c.reset}
72
129
  npx kandown [command]
@@ -130,7 +187,7 @@ function appendAgentReference(cwd, agentsFile, kandownPath) {
130
187
  ${marker}
131
188
  ## Task management
132
189
 
133
- **IMPORTANT:** Before touching any task files, you MUST read \`AGENT_KANDOWN.md\`.
190
+ **IMPORTANT:** Before touching any task files, you MUST read \`${kandownPath}/AGENT_KANDOWN.md\`.
134
191
 
135
192
  This project uses a file-based kanban:
136
193
  - **Tasks live in \`${kandownPath}/tasks/t-xxx.md\`** β€” each task file owns its status
@@ -174,35 +231,40 @@ function parseArgs(argv) {
174
231
  return args;
175
232
  }
176
233
 
177
- function cmdInit(rawArgs) {
234
+ /**
235
+ * @returns {{ kandownDir: string, alreadyExisted: boolean }} β€” resolves the
236
+ * kandown directory and auto-inits it if it doesn't exist (no prompt, silent init).
237
+ */
238
+ function ensureKandownDir(rawArgs) {
178
239
  const args = parseArgs(rawArgs);
179
240
  const cwd = process.cwd();
241
+ const explicitPath = rawArgs.includes('--path') || rawArgs.includes('-p');
180
242
  const kandownDir = resolve(cwd, args.path);
181
- const kandownPath = args.path;
182
243
 
183
- log('');
184
- info(`Installing kandown in ${c.bold}${kandownPath}/${c.reset}`);
185
- log('');
244
+ if (existsSync(kandownDir)) return { kandownDir, alreadyExisted: true };
186
245
 
187
- if (existsSync(kandownDir) && !args.force) {
188
- err(`Directory ${c.bold}${kandownPath}/${c.reset} already exists.`);
189
- log(` Use ${c.cyan}--force${c.reset} to overwrite or ${c.cyan}--path <dir>${c.reset} for another location.`);
190
- process.exit(1);
191
- }
246
+ log('');
247
+ info(`No .kandown/ found β€” auto-installing...`);
248
+ doInit(args, cwd, args.path, kandownDir);
249
+ return { kandownDir, alreadyExisted: false };
250
+ }
192
251
 
252
+ /**
253
+ * Performs the actual init work. Returns on error (does not exit).
254
+ * @returns {boolean} true if init succeeded, false otherwise.
255
+ */
256
+ function doInit(args, cwd, kandownPath, kandownDir) {
193
257
  mkdirSync(kandownDir, { recursive: true });
194
258
 
195
- // Copy kandown.html from dist
196
259
  const htmlSrc = join(PKG_ROOT, 'dist', 'index.html');
197
260
  const htmlDest = join(kandownDir, 'kandown.html');
198
261
  if (!existsSync(htmlSrc)) {
199
262
  err(`Missing build output at ${htmlSrc}. Did you run 'npm run build'?`);
200
- process.exit(1);
263
+ return false;
201
264
  }
202
265
  copyFileSync(htmlSrc, htmlDest);
203
266
  success('kandown.html');
204
267
 
205
- // Copy templates
206
268
  const templatesDir = join(PKG_ROOT, 'templates');
207
269
  if (!existsSync(join(kandownDir, 'AGENT.md'))) {
208
270
  copyFileSync(join(templatesDir, 'AGENT.md'), join(kandownDir, 'AGENT.md'));
@@ -222,7 +284,6 @@ function cmdInit(rawArgs) {
222
284
  info('tasks/ already exists (kept)');
223
285
  }
224
286
 
225
- // πŸ“– Copy kandown.json config file (project preferences for UI, agent, fields)
226
287
  if (!existsSync(join(kandownDir, 'kandown.json'))) {
227
288
  copyFileSync(join(templatesDir, 'kandown.json'), join(kandownDir, 'kandown.json'));
228
289
  success('kandown.json');
@@ -230,25 +291,15 @@ function cmdInit(rawArgs) {
230
291
  info('kandown.json already exists (kept)');
231
292
  }
232
293
 
233
- // Copy AGENT_KANDOWN.md to project root (not inside .kandown/)
234
294
  const agentKandownSrc = join(templatesDir, 'AGENT_KANDOWN.md');
235
- const agentKandownDest = join(cwd, 'AGENT_KANDOWN.md');
295
+ const agentKandownDest = join(kandownDir, 'AGENT_KANDOWN.md');
236
296
  if (!existsSync(agentKandownDest)) {
237
297
  copyFileSync(agentKandownSrc, agentKandownDest);
238
- success('AGENT_KANDOWN.md (at project root)');
298
+ success('AGENT_KANDOWN.md');
239
299
  } else {
240
- info('AGENT_KANDOWN.md already exists at project root (kept)');
300
+ info('AGENT_KANDOWN.md already exists (kept)');
241
301
  }
242
302
 
243
- // πŸ“– Copy the compact agent doc β€” used by the CLI board launcher for system prompt injection
244
- const compactSrc = join(templatesDir, 'AGENT_KANDOWN_COMPACT.md');
245
- const compactDest = join(cwd, 'AGENT_KANDOWN_COMPACT.md');
246
- if (existsSync(compactSrc) && !existsSync(compactDest)) {
247
- copyFileSync(compactSrc, compactDest);
248
- success('AGENT_KANDOWN_COMPACT.md (at project root)');
249
- }
250
-
251
- // Integrate with AGENTS.md / CLAUDE.md
252
303
  if (!args.noAgents) {
253
304
  log('');
254
305
  const existingAgents = findAgentsFile(cwd);
@@ -261,6 +312,27 @@ function cmdInit(rawArgs) {
261
312
  }
262
313
  }
263
314
 
315
+ return true;
316
+ }
317
+
318
+ function cmdInit(rawArgs) {
319
+ const args = parseArgs(rawArgs);
320
+ const cwd = process.cwd();
321
+ const kandownPath = args.path;
322
+ const kandownDir = resolve(cwd, kandownPath);
323
+
324
+ log('');
325
+ info(`Installing kandown in ${c.bold}${kandownPath}/${c.reset}`);
326
+ log('');
327
+
328
+ if (existsSync(kandownDir) && !args.force) {
329
+ err(`Directory ${c.bold}${kandownPath}/${c.reset} already exists.`);
330
+ log(` Use ${c.cyan}--force${c.reset} to overwrite or ${c.cyan}--path <dir>${c.reset} for another location.`);
331
+ process.exit(1);
332
+ }
333
+
334
+ if (!doInit(args, cwd, kandownPath, kandownDir)) process.exit(1);
335
+
264
336
  log('');
265
337
  log(`${c.green}${c.bold}Done.${c.reset}`);
266
338
  log('');
@@ -410,21 +482,9 @@ async function listenOnAvailablePort(kandownDir, preferredPort) {
410
482
  * the browser can keep talking to localhost while the terminal board is active.
411
483
  */
412
484
  async function cmdServe(rawArgs) {
413
- const args = parseArgs(rawArgs);
414
- const cwd = process.cwd();
415
- const hasExplicitPath = rawArgs.includes('--path') || rawArgs.includes('-p');
416
- const explicitKandownDir = resolve(cwd, args.path);
417
- const kandownDir = hasExplicitPath || existsSync(explicitKandownDir)
418
- ? explicitKandownDir
419
- : findKandownDir(cwd);
420
- if (!kandownDir || !existsSync(kandownDir)) {
421
- const missingPath = hasExplicitPath ? args.path : '.kandown/';
422
- err(`No ${c.bold}${missingPath}${c.reset} directory found.`);
423
- log(` Run ${c.cyan}npx kandown init${c.reset} to set up kandown in this project.`);
424
- process.exit(1);
425
- }
485
+ const { kandownDir } = ensureKandownDir(rawArgs);
426
486
 
427
- const preferredPort = parsePort(args.port);
487
+ const preferredPort = parsePort(parseArgs(rawArgs).port);
428
488
  const { server, port } = await listenOnAvailablePort(kandownDir, preferredPort);
429
489
  const url = `http://localhost:${port}`;
430
490
 
@@ -438,8 +498,7 @@ async function cmdServe(rawArgs) {
438
498
  info(`Project: ${kandownDir}`);
439
499
  openInBrowser(url);
440
500
  try {
441
- const tuiArgs = kandownDir === explicitKandownDir ? rawArgs : ['--path', kandownDir, ...rawArgs];
442
- await cmdTui('board', tuiArgs);
501
+ await cmdTui('board', rawArgs);
443
502
  } finally {
444
503
  server.close();
445
504
  }
@@ -477,28 +536,31 @@ function findKandownDir(cwd) {
477
536
 
478
537
  // πŸ“– Launches the fullscreen TUI for a given screen (settings, board, etc.)
479
538
  async function cmdTui(screen, rawArgs) {
480
- const args = parseArgs(rawArgs);
481
- const cwd = process.cwd();
482
- const kandownDir = resolve(cwd, args.path);
483
-
484
- if (!existsSync(kandownDir)) {
485
- err(`No ${c.bold}${args.path}/${c.reset} directory found.`);
486
- log(` Run ${c.cyan}npx kandown init${c.reset} first.`);
487
- process.exit(1);
488
- }
539
+ const { kandownDir } = ensureKandownDir(rawArgs);
540
+ const version = getCurrentVersion();
489
541
 
490
542
  try {
491
543
  const { run } = await import(new URL('./tui.js', import.meta.url).href);
492
- await run(screen, kandownDir);
544
+ await run(screen, kandownDir, version);
493
545
  } catch (e) {
494
546
  err(`Failed to launch TUI: ${e.message}`);
495
- log(` Make sure the CLI is built: ${c.cyan}pnpm build:cli${c.reset}`);
496
547
  process.exit(1);
497
548
  }
498
549
  }
499
550
 
500
551
  const [cmd, ...rest] = process.argv.slice(2);
501
552
 
553
+ // πŸ“– Handle --version / -v before any command logic
554
+ if (cmd === '--version' || cmd === '-v') {
555
+ const v = getCurrentVersion() ?? 'unknown';
556
+ log(`kandown v${v}`);
557
+ process.exit(0);
558
+ }
559
+
560
+ // πŸ“– Auto-update check runs before EVERY command (except --version).
561
+ // Uses a short timeout so startup is not noticeably slower.
562
+ await checkForUpdate(rest);
563
+
502
564
  switch (cmd) {
503
565
  case 'init':
504
566
  cmdInit(rest);