kandown 0.4.0 → 0.7.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 +1 -1
- package/bin/kandown.js +189 -40
- package/bin/tui.js +547 -80
- package/dist/index.html +217 -217
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,7 +180,7 @@ Contributions are welcome! Please read the existing code style and conventions b
|
|
|
180
180
|
git clone https://github.com/vava-nessa/kandown.git
|
|
181
181
|
cd kandown
|
|
182
182
|
pnpm install
|
|
183
|
-
pnpm dev # Web UI at localhost:
|
|
183
|
+
pnpm dev # Web UI at localhost:5176
|
|
184
184
|
```
|
|
185
185
|
|
|
186
186
|
### Scripts
|
package/bin/kandown.js
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* → appendAgentReference — injects a Kandown task-management reference
|
|
17
17
|
* → createAgentsFileIfMissing — creates AGENTS.md when none exists
|
|
18
18
|
* → parseArgs — parses shared CLI flags
|
|
19
|
+
* → resolveKandownBin — resolves the global kandown binary path for respawn
|
|
20
|
+
* → semverGt — compares two semver strings
|
|
21
|
+
* → checkForUpdate — non-blocking auto-updater with lock file and graceful fallback
|
|
19
22
|
* → cmdInit — installs `.kandown`
|
|
20
23
|
* → cmdUpdate — refreshes installed kandown.html
|
|
21
24
|
* → injectServerRoot — injects the CLI server root into single-file HTML
|
|
@@ -69,50 +72,191 @@ function getCurrentVersion() {
|
|
|
69
72
|
} catch { return null; }
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
/**
|
|
76
|
+
* 📖 Resolves the kandown binary path for respawning after an update.
|
|
77
|
+
* Tries, in order: npm global bin → pnpm global bin → process.execPath fallback.
|
|
78
|
+
* @returns {string|null} Absolute path to the kandown binary, or null.
|
|
79
|
+
*/
|
|
80
|
+
function resolveKandownBin() {
|
|
81
|
+
try {
|
|
82
|
+
const npmBin = String(execSync('npm config get prefix 2>/dev/null', {
|
|
83
|
+
timeout: 3000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
84
|
+
})).trim();
|
|
85
|
+
if (existsSync(join(npmBin, 'bin', 'kandown'))) return join(npmBin, 'bin', 'kandown');
|
|
86
|
+
} catch { /* npm not available */ }
|
|
87
|
+
try {
|
|
88
|
+
const pnpmBin = String(execSync('pnpm config get prefix 2>/dev/null', {
|
|
89
|
+
timeout: 3000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
90
|
+
})).trim();
|
|
91
|
+
if (existsSync(join(pnpmBin, 'bin', 'kandown'))) return join(pnpmBin, 'bin', 'kandown');
|
|
92
|
+
} catch { /* pnpm not available */ }
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 📖 Compares two semver strings (major.minor.patch).
|
|
98
|
+
* @returns {number} 1 if a > b, -1 if a < b, 0 if equal.
|
|
99
|
+
*/
|
|
100
|
+
function semverGt(a, b) {
|
|
101
|
+
const pa = a.replace(/^v/, '').split('.').map(Number);
|
|
102
|
+
const pb = b.replace(/^v/, '').split('.').map(Number);
|
|
103
|
+
for (let i = 0; i < 3; i++) {
|
|
104
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
105
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
106
|
+
}
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 📖 Check npm for a newer version and auto-update if outdated.
|
|
112
|
+
*
|
|
113
|
+
* Design principles:
|
|
114
|
+
* - 🚀 Non-blocking: spawns npm check as a background child process.
|
|
115
|
+
* - 🔒 Lock file: prevents concurrent update races when multiple kandown
|
|
116
|
+
* instances start simultaneously.
|
|
117
|
+
* - 🛡️ Resilient: if the update fails for any reason (network, permissions,
|
|
118
|
+
* npm registry downtime), the current version continues normally.
|
|
119
|
+
* - 🔄 Respawn: after a successful update, re-spawns the CLI with the same
|
|
120
|
+
* arguments so the user gets the new version immediately.
|
|
121
|
+
* - 📦 Package manager agnostic: tries npm, then pnpm, for both the update
|
|
122
|
+
* and the binary resolution.
|
|
123
|
+
*
|
|
124
|
+
* Only activates when running from an installed npm package
|
|
125
|
+
* (not local dev source, where `src/` exists in PKG_ROOT).
|
|
126
|
+
*/
|
|
76
127
|
async function checkForUpdate(argv = process.argv) {
|
|
77
|
-
|
|
128
|
+
// 📖 Local dev — skip entirely
|
|
129
|
+
if (existsSync(join(PKG_ROOT, 'src'))) return;
|
|
130
|
+
|
|
78
131
|
const current = getCurrentVersion();
|
|
79
132
|
if (!current) return;
|
|
133
|
+
|
|
134
|
+
// 📖 Skip if a lock file exists — another kandown instance is already updating.
|
|
135
|
+
// The lock auto-expires after 60 seconds to handle stale locks from crashed processes.
|
|
136
|
+
const lockFile = join(PKG_ROOT, '.update.lock');
|
|
137
|
+
const now = Date.now();
|
|
80
138
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
139
|
+
if (existsSync(lockFile)) {
|
|
140
|
+
const lockAge = now - statSync(lockFile).mtimeMs;
|
|
141
|
+
if (lockAge < 60_000) return; // another process is handling the update
|
|
142
|
+
unlinkSync(lockFile); // stale lock — remove it
|
|
143
|
+
}
|
|
144
|
+
} catch { /* ignore lock errors */ }
|
|
145
|
+
|
|
146
|
+
// 📖 Step 1: Check latest version on npm registry (non-blocking).
|
|
147
|
+
// We use `npm view` in a spawned child process with a short timeout.
|
|
148
|
+
const latest = await new Promise((resolve) => {
|
|
149
|
+
const child = spawn('npm', ['view', 'kandown', 'version'], {
|
|
150
|
+
timeout: 6000,
|
|
151
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
152
|
+
env: { ...process.env },
|
|
153
|
+
// 📖 Detach so we can kill cleanly on timeout
|
|
154
|
+
detached: false,
|
|
155
|
+
});
|
|
156
|
+
let stdout = '';
|
|
157
|
+
child.stdout.on('data', (d) => { stdout += d; });
|
|
158
|
+
child.stderr.on('data', () => {}); // silence stderr
|
|
159
|
+
child.on('error', () => resolve(null));
|
|
160
|
+
child.on('close', (code) => {
|
|
161
|
+
if (code !== 0) return resolve(null);
|
|
162
|
+
const v = stdout.trim().replace(/^"|"$/g, '');
|
|
163
|
+
resolve(v || null);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
86
166
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
167
|
+
if (!latest || semverGt(current, latest) >= 0) return; // up to date or offline
|
|
168
|
+
|
|
169
|
+
log('');
|
|
170
|
+
log(`${c.yellow}⚡ Update available:${c.reset} kandown ${c.dim}${current}${c.reset} → ${c.green}${latest}${c.reset}`);
|
|
171
|
+
info('Auto-updating…');
|
|
172
|
+
|
|
173
|
+
// 📖 Step 2: Create lock file to prevent concurrent updates.
|
|
174
|
+
try { writeFileSync(lockFile, `${process.pid}\n${now}`, 'utf8'); } catch { /* ignore */ }
|
|
175
|
+
|
|
176
|
+
// 📖 Step 3: Run the update via npm or pnpm.
|
|
177
|
+
// Try npm first, fall back to pnpm.
|
|
178
|
+
const updateOk = await new Promise((resolve) => {
|
|
179
|
+
const tryInstall = (cmd) => {
|
|
180
|
+
return new Promise((res) => {
|
|
181
|
+
const child = spawn(cmd, ['install', '-g', 'kandown'], {
|
|
182
|
+
timeout: 45000,
|
|
183
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
184
|
+
env: { ...process.env },
|
|
185
|
+
detached: false,
|
|
186
|
+
});
|
|
187
|
+
child.stderr.on('data', () => {}); // silence npm noise
|
|
188
|
+
child.stdout.on('data', () => {});
|
|
189
|
+
child.on('error', () => res(false));
|
|
190
|
+
child.on('close', (code) => res(code === 0));
|
|
91
191
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
192
|
+
};
|
|
193
|
+
tryInstall('npm').then((ok) => {
|
|
194
|
+
if (ok) resolve(true);
|
|
195
|
+
else tryInstall('pnpm').then(resolve);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// 📖 Clean up lock file regardless of outcome.
|
|
200
|
+
try { if (existsSync(lockFile)) unlinkSync(lockFile); } catch { /* ignore */ }
|
|
201
|
+
|
|
202
|
+
if (!updateOk) {
|
|
203
|
+
warn('Auto-update failed — continuing with current version');
|
|
204
|
+
log(` Run ${c.cyan}npm install -g kandown${c.reset} to upgrade manually`);
|
|
205
|
+
log('');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 📖 Step 4: Verify the update actually landed.
|
|
210
|
+
const postVersion = await new Promise((resolve) => {
|
|
211
|
+
const child = spawn('npm', ['view', 'kandown', 'version'], {
|
|
212
|
+
timeout: 5000,
|
|
213
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
214
|
+
detached: false,
|
|
215
|
+
});
|
|
216
|
+
let stdout = '';
|
|
217
|
+
child.stdout.on('data', (d) => { stdout += d; });
|
|
218
|
+
child.stderr.on('data', () => {});
|
|
219
|
+
child.on('error', () => resolve(null));
|
|
220
|
+
child.on('close', (code) => {
|
|
221
|
+
if (code !== 0) return resolve(null);
|
|
222
|
+
resolve(stdout.trim().replace(/^"|"$/g, '') || null);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (!postVersion || semverGt(postVersion, latest) < 0) {
|
|
227
|
+
// Install claimed success but version didn't change — probably a permissions issue.
|
|
228
|
+
warn('Update did not apply — continuing with current version');
|
|
229
|
+
log(` Run ${c.cyan}npm install -g kandown${c.reset} to upgrade manually`);
|
|
230
|
+
log('');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
success(`Updated to v${postVersion} — restarting…`);
|
|
235
|
+
log('');
|
|
236
|
+
|
|
237
|
+
// 📖 Step 5: Respawn with the new version.
|
|
238
|
+
// Pass --no-update-check to the child so it doesn't try to update again.
|
|
239
|
+
const bin = resolveKandownBin();
|
|
240
|
+
const childArgs = ['--no-update-check', ...argv.slice(2)];
|
|
241
|
+
|
|
242
|
+
if (bin) {
|
|
243
|
+
const child = spawn(bin, childArgs, {
|
|
244
|
+
detached: true,
|
|
245
|
+
stdio: 'inherit',
|
|
246
|
+
env: { ...process.env },
|
|
247
|
+
});
|
|
248
|
+
child.unref();
|
|
249
|
+
} else {
|
|
250
|
+
// 📖 Fallback: re-use the current binary path (works for npx).
|
|
251
|
+
const child = spawn(process.argv[0], [process.argv[1], ...childArgs], {
|
|
252
|
+
detached: true,
|
|
253
|
+
stdio: 'inherit',
|
|
254
|
+
env: { ...process.env },
|
|
255
|
+
});
|
|
256
|
+
child.unref();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
process.exit(0);
|
|
116
260
|
}
|
|
117
261
|
|
|
118
262
|
const c = {
|
|
@@ -764,7 +908,8 @@ async function cmdTui(screen, rawArgs) {
|
|
|
764
908
|
}
|
|
765
909
|
}
|
|
766
910
|
|
|
767
|
-
const
|
|
911
|
+
const rawArgs = process.argv.slice(2).filter((a) => a !== '--no-update-check');
|
|
912
|
+
const [cmd, ...rest] = rawArgs;
|
|
768
913
|
|
|
769
914
|
// 📖 Handle --version / -v before any command logic
|
|
770
915
|
if (cmd === '--version' || cmd === '-v') {
|
|
@@ -773,9 +918,13 @@ if (cmd === '--version' || cmd === '-v') {
|
|
|
773
918
|
process.exit(0);
|
|
774
919
|
}
|
|
775
920
|
|
|
921
|
+
// 📖 Skip auto-update if this is a respawned child after an update.
|
|
922
|
+
// The parent passes --no-update-check to prevent an infinite update loop.
|
|
923
|
+
const skipUpdate = process.argv.slice(2).includes('--no-update-check');
|
|
924
|
+
|
|
776
925
|
// 📖 Auto-update check runs before EVERY command (except --version).
|
|
777
926
|
// Uses a short timeout so startup is not noticeably slower.
|
|
778
|
-
await checkForUpdate(
|
|
927
|
+
if (!skipUpdate) await checkForUpdate(process.argv);
|
|
779
928
|
|
|
780
929
|
switch (cmd) {
|
|
781
930
|
case 'init':
|