kandown 0.15.2 → 0.15.4
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/bin/kandown.js +65 -9
- package/bin/tui.js +40 -9
- package/dist/index.html +3 -3
- package/package.json +1 -1
package/bin/kandown.js
CHANGED
|
@@ -199,15 +199,14 @@ async function checkForUpdate(argv = process.argv) {
|
|
|
199
199
|
|
|
200
200
|
if (!latest || semverGt(current, latest) >= 0) return; // up to date or offline
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
log(`${c.yellow}⚡ Update available:${c.reset} kandown ${c.dim}${current}${c.reset} → ${c.green}${latest}${c.reset}`);
|
|
204
|
-
info('Auto-updating…');
|
|
202
|
+
tuiDone('⚡', `Update available: ${c.dim}kandown ${current}${c.reset} → ${c.green}${latest}${c.reset}`);
|
|
205
203
|
|
|
206
204
|
// 📖 Step 2: Create lock file to prevent concurrent updates.
|
|
207
205
|
try { writeFileSync(lockFile, `${process.pid}\n${now}`, 'utf8'); } catch { /* ignore */ }
|
|
208
206
|
|
|
209
|
-
// 📖 Step 3: Run the update via npm or pnpm.
|
|
210
|
-
|
|
207
|
+
// 📖 Step 3: Run the update via npm or pnpm with animated progress.
|
|
208
|
+
tuiProgress(`Updating to ${latest}…`, 25);
|
|
209
|
+
|
|
211
210
|
const updateOk = await new Promise((resolve) => {
|
|
212
211
|
const tryInstall = (cmd) => {
|
|
213
212
|
return new Promise((res) => {
|
|
@@ -233,7 +232,7 @@ async function checkForUpdate(argv = process.argv) {
|
|
|
233
232
|
try { if (existsSync(lockFile)) unlinkSync(lockFile); } catch { /* ignore */ }
|
|
234
233
|
|
|
235
234
|
if (!updateOk) {
|
|
236
|
-
|
|
235
|
+
tuiDone('✗', `${c.yellow}Auto-update failed${c.reset} — continuing with current version`);
|
|
237
236
|
log(` Run ${c.cyan}npm install -g kandown${c.reset} to upgrade manually`);
|
|
238
237
|
log('');
|
|
239
238
|
return;
|
|
@@ -257,14 +256,13 @@ async function checkForUpdate(argv = process.argv) {
|
|
|
257
256
|
});
|
|
258
257
|
|
|
259
258
|
if (!postVersion || semverGt(postVersion, latest) < 0) {
|
|
260
|
-
|
|
261
|
-
warn('Update did not apply — continuing with current version');
|
|
259
|
+
tuiDone('✗', `${c.yellow}Update did not apply${c.reset} — continuing with current version`);
|
|
262
260
|
log(` Run ${c.cyan}npm install -g kandown${c.reset} to upgrade manually`);
|
|
263
261
|
log('');
|
|
264
262
|
return;
|
|
265
263
|
}
|
|
266
264
|
|
|
267
|
-
|
|
265
|
+
tuiDone('✓', `${c.green}Updated to v${postVersion}${c.reset} — restarting…`);
|
|
268
266
|
log('');
|
|
269
267
|
|
|
270
268
|
// 📖 Step 5: Respawn with the new version.
|
|
@@ -309,6 +307,64 @@ const info = (msg) => log(`${c.cyan}→${c.reset} ${msg}`);
|
|
|
309
307
|
const warn = (msg) => log(`${c.yellow}⚠${c.reset} ${msg}`);
|
|
310
308
|
const err = (msg) => log(`${c.red}✗${c.reset} ${msg}`);
|
|
311
309
|
|
|
310
|
+
// ─── TUI spinner & progress bar ─────────────────────────────────────────────
|
|
311
|
+
// 📖 Lightweight terminal animation helpers for the auto-updater.
|
|
312
|
+
// Uses ANSI escape codes (carriage return + clear line) to redraw in-place.
|
|
313
|
+
// Only animate when stdout is a TTY; otherwise fall back to plain text.
|
|
314
|
+
|
|
315
|
+
const _SP_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
316
|
+
let _spTimer = null;
|
|
317
|
+
let _spIdx = 0;
|
|
318
|
+
const _isTTY = process.stdout.isTTY;
|
|
319
|
+
|
|
320
|
+
function _tuiClear() {
|
|
321
|
+
process.stdout.write('\r\x1b[K');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Start an animated spinner with the given text. */
|
|
325
|
+
function tuiSpinner(text) {
|
|
326
|
+
if (_spTimer) clearInterval(_spTimer);
|
|
327
|
+
_spIdx = 0;
|
|
328
|
+
_tuiClear();
|
|
329
|
+
if (!_isTTY) { process.stdout.write(` ${text}\n`); return; }
|
|
330
|
+
_spTimer = setInterval(() => {
|
|
331
|
+
process.stdout.write(`\r\x1b[K${_SP_FRAMES[_spIdx % _SP_FRAMES.length]} ${text}`);
|
|
332
|
+
_spIdx++;
|
|
333
|
+
}, 100);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 📖 Start a time-estimated progress bar + spinner.
|
|
338
|
+
* Shows a filling bar with percentage based on elapsed time.
|
|
339
|
+
* Caps at 95% until the caller calls tuiDone().
|
|
340
|
+
*/
|
|
341
|
+
function tuiProgress(text, estimateSec = 25, barWidth = 15) {
|
|
342
|
+
if (_spTimer) clearInterval(_spTimer);
|
|
343
|
+
_spIdx = 0;
|
|
344
|
+
_tuiClear();
|
|
345
|
+
if (!_isTTY) { process.stdout.write(` ${text}\n`); return; }
|
|
346
|
+
const start = Date.now();
|
|
347
|
+
_spTimer = setInterval(() => {
|
|
348
|
+
const elapsed = (Date.now() - start) / 1000;
|
|
349
|
+
const pct = Math.min(Math.round((elapsed / estimateSec) * 100), 95);
|
|
350
|
+
const filled = Math.round(barWidth * pct / 100);
|
|
351
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
352
|
+
process.stdout.write(`\r\x1b[K${_SP_FRAMES[_spIdx % _SP_FRAMES.length]} ${text} ${bar} ${pct}%`);
|
|
353
|
+
_spIdx++;
|
|
354
|
+
}, 120);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Stop the current animation and print a final status line. */
|
|
358
|
+
function tuiDone(symbol, text) {
|
|
359
|
+
if (_spTimer) { clearInterval(_spTimer); _spTimer = null; }
|
|
360
|
+
if (_isTTY) {
|
|
361
|
+
_tuiClear();
|
|
362
|
+
process.stdout.write(`${symbol} ${text}\n`);
|
|
363
|
+
} else {
|
|
364
|
+
log(`${symbol} ${text}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
312
368
|
function help() {
|
|
313
369
|
const v = getCurrentVersion() ?? '?';
|
|
314
370
|
log(`
|
package/bin/tui.js
CHANGED
|
@@ -54651,6 +54651,7 @@ function moveTaskToColumn(kandownDir, taskId, targetColumn) {
|
|
|
54651
54651
|
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
54652
54652
|
import { dirname as dirname2, join as join3 } from "path";
|
|
54653
54653
|
import { spawn } from "child_process";
|
|
54654
|
+
import { createConnection } from "net";
|
|
54654
54655
|
function metadataPath(kandownDir) {
|
|
54655
54656
|
return join3(kandownDir, "daemon.json");
|
|
54656
54657
|
}
|
|
@@ -54708,6 +54709,20 @@ async function fetchDaemonInfo(port) {
|
|
|
54708
54709
|
return null;
|
|
54709
54710
|
}
|
|
54710
54711
|
}
|
|
54712
|
+
function isPortListening(port, timeoutMs = 400) {
|
|
54713
|
+
return new Promise((resolve3) => {
|
|
54714
|
+
const socket = createConnection({ port, host: "127.0.0.1" }, () => {
|
|
54715
|
+
socket.destroy();
|
|
54716
|
+
resolve3(true);
|
|
54717
|
+
});
|
|
54718
|
+
socket.on("error", () => resolve3(false));
|
|
54719
|
+
socket.setTimeout(timeoutMs);
|
|
54720
|
+
socket.on("timeout", () => {
|
|
54721
|
+
socket.destroy();
|
|
54722
|
+
resolve3(false);
|
|
54723
|
+
});
|
|
54724
|
+
});
|
|
54725
|
+
}
|
|
54711
54726
|
async function getDaemonStatus(kandownDir) {
|
|
54712
54727
|
const metadata = readDaemonMetadata(kandownDir);
|
|
54713
54728
|
if (!metadata) return { running: false, metadata: null };
|
|
@@ -54716,18 +54731,23 @@ async function getDaemonStatus(kandownDir) {
|
|
|
54716
54731
|
return { running: false, metadata: null };
|
|
54717
54732
|
}
|
|
54718
54733
|
const remote = await fetchDaemonInfo(metadata.port);
|
|
54719
|
-
if (!remote
|
|
54734
|
+
if (!remote) {
|
|
54735
|
+
return { running: false, metadata: null };
|
|
54736
|
+
}
|
|
54737
|
+
if (remote.pid !== metadata.pid || remote.kandownDir !== kandownDir) {
|
|
54720
54738
|
removeDaemonMetadata(kandownDir);
|
|
54721
54739
|
return { running: false, metadata: null };
|
|
54722
54740
|
}
|
|
54723
54741
|
return { running: true, metadata };
|
|
54724
54742
|
}
|
|
54725
|
-
async function waitForDaemon(kandownDir, timeoutMs =
|
|
54743
|
+
async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
|
|
54726
54744
|
const started = Date.now();
|
|
54727
54745
|
while (Date.now() - started < timeoutMs) {
|
|
54728
|
-
const
|
|
54729
|
-
if (
|
|
54730
|
-
|
|
54746
|
+
const metadata = readDaemonMetadata(kandownDir);
|
|
54747
|
+
if (metadata && isProcessAlive(metadata.pid) && await isPortListening(metadata.port)) {
|
|
54748
|
+
return { running: true, metadata };
|
|
54749
|
+
}
|
|
54750
|
+
await new Promise((resolve3) => setTimeout(resolve3, 120));
|
|
54731
54751
|
}
|
|
54732
54752
|
return { running: false, metadata: null };
|
|
54733
54753
|
}
|
|
@@ -57082,8 +57102,15 @@ var MENU_HEIGHT = 2;
|
|
|
57082
57102
|
var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1);
|
|
57083
57103
|
var TASKS_START_Y = 5;
|
|
57084
57104
|
function truncate(str, maxLen) {
|
|
57105
|
+
if (maxLen <= 0) return "";
|
|
57085
57106
|
if (str.length <= maxLen) return str;
|
|
57086
|
-
return str.slice(0, maxLen - 1) + "\u2026";
|
|
57107
|
+
return str.slice(0, Math.max(0, maxLen - 1)) + "\u2026";
|
|
57108
|
+
}
|
|
57109
|
+
function terminalHyperlink(label, url) {
|
|
57110
|
+
return `\x1B]8;;${url}\x07${label}\x1B]8;;\x07`;
|
|
57111
|
+
}
|
|
57112
|
+
function webLinkLabel(url) {
|
|
57113
|
+
return `\u2197 ${url.replace(/^https?:\/\//, "")}`;
|
|
57087
57114
|
}
|
|
57088
57115
|
function pad(str, len) {
|
|
57089
57116
|
const t = truncate(str, len);
|
|
@@ -57258,11 +57285,15 @@ function BoardHeader({ title, inTmux, modeHint, version, daemonStatus, daemonBus
|
|
|
57258
57285
|
const rightWidth = Math.max(0, width - leftWidth - daemonWidth);
|
|
57259
57286
|
const left = pad(` \u25C6 KANDOWN${tmuxHint}${versionTag} ${title}`, leftWidth);
|
|
57260
57287
|
const daemon = pad(daemonLabel, daemonWidth);
|
|
57261
|
-
const
|
|
57288
|
+
const webUrl = daemonStatus.running ? daemonStatus.metadata?.url : null;
|
|
57289
|
+
const rightPlain = webUrl ? truncate(webLinkLabel(webUrl), rightWidth) : truncate(hint, rightWidth);
|
|
57290
|
+
const right = rightPlain.padStart(rightWidth, " ");
|
|
57291
|
+
const rightPadding = right.slice(0, Math.max(0, right.length - rightPlain.length));
|
|
57292
|
+
const rightContent = webUrl ? `${rightPadding}${terminalHyperlink(rightPlain, webUrl)}` : right;
|
|
57262
57293
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { marginBottom: 1, children: [
|
|
57263
57294
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { bold: true, color: "cyan", children: left }),
|
|
57264
57295
|
daemonWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: daemonStatus.running ? "green" : "yellow", bold: true, children: daemon }),
|
|
57265
|
-
rightWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", dimColor:
|
|
57296
|
+
rightWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: webUrl ? "blue" : "gray", dimColor: !webUrl, underline: !!webUrl, children: rightContent })
|
|
57266
57297
|
] });
|
|
57267
57298
|
}
|
|
57268
57299
|
function StatusBar({ message, task, daemonStatus }) {
|
|
@@ -58006,7 +58037,7 @@ function Board({ kandownDir, version }) {
|
|
|
58006
58037
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { flexDirection: "column", children: [
|
|
58007
58038
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { marginBottom: 1, justifyContent: "space-between", children: [
|
|
58008
58039
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: "Esc back \xB7 a agent \xB7 j/k scroll" }),
|
|
58009
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "gray", dimColor: true, children: [
|
|
58040
|
+
daemonStatus.running && daemonStatus.metadata ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "blue", underline: true, children: terminalHyperlink(webLinkLabel(daemonStatus.metadata.url), daemonStatus.metadata.url) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "gray", dimColor: true, children: [
|
|
58010
58041
|
"KANDOWN ",
|
|
58011
58042
|
board.title
|
|
58012
58043
|
] })
|