kodelyth-ecc 2.2.1 → 2.3.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/CHANGELOG.md +36 -0
- package/VERSION +1 -1
- package/bin/kodelyth-ecc.js +12 -0
- package/package.json +3 -2
- package/scripts/cli/menu.js +288 -0
- package/scripts/cli/update-check.js +77 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Kodelyth ECC are documented here.
|
|
4
4
|
|
|
5
|
+
## v2.3.0 — Interactive arrow-key CLI menu (July 2026)
|
|
6
|
+
|
|
7
|
+
Run `kodelythecc` (or `kodelyth-ecc`) with no args in a real terminal and an arrow-key menu opens.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
**Interactive menu** (`scripts/cli/menu.js` — zero dependencies)
|
|
12
|
+
- Arrow-key up/down navigation, Enter to select, `q` / esc / Ctrl+C to quit
|
|
13
|
+
- Vim-style `j`/`k` also work
|
|
14
|
+
- Options:
|
|
15
|
+
- **Update to vX.Y.Z** — only shown when a newer version exists on npm (24h-cached check)
|
|
16
|
+
- **Open Dashboard** — boot localhost dashboard (RTK + Terse + Codebase + Memory, all real data)
|
|
17
|
+
- **Install ECC for another IDE** — inline picker across 13 IDE targets, runs `--target X` non-interactively
|
|
18
|
+
- **RTK / Terse / Codebase status** — inline `kodelyth-ecc <sub> status` runs, then returns to menu on Enter
|
|
19
|
+
- **Run in background** — daemonises the dashboard, writes PID to `~/.kodelythecc/dashboard-daemon.pid`, log to `~/.kodelythecc/dashboard-daemon.log`, prints stop command
|
|
20
|
+
- **Exit**
|
|
21
|
+
|
|
22
|
+
**Update checker** (`scripts/cli/update-check.js`)
|
|
23
|
+
- Polls `https://registry.npmjs.org/kodelyth-ecc/latest` with 2.5s timeout
|
|
24
|
+
- Result cached to `~/.kodelythecc/update-check.json` for 24 hours to avoid nagging or spam
|
|
25
|
+
- Menu shows `up to date` / `latest: vX.Y.Z` / `update available: vX.Y.Z` (yellow) as appropriate
|
|
26
|
+
- Non-blocking: menu renders in <1s even if npm is slow
|
|
27
|
+
|
|
28
|
+
### Behaviour
|
|
29
|
+
|
|
30
|
+
- **Menu opens only when interactive** — real TTY on both stdin and stdout, no `CI` env, no `KODELYTH_NO_MENU=1` env
|
|
31
|
+
- **Piped stdin / CI runs the installer** — same as before, no menu, no breakage
|
|
32
|
+
- **Passing any flag or subcommand** — skips the menu entirely
|
|
33
|
+
- **Background daemon** — the dashboard child is `detach()`ed and `unref()`ed, survives the parent shell exit. Stop with `kill $(cat ~/.kodelythecc/dashboard-daemon.pid)`
|
|
34
|
+
|
|
35
|
+
### Verified live on this Mac
|
|
36
|
+
|
|
37
|
+
- `node bin/kodelyth-ecc.js` in a TTY → menu opens, arrow keys navigate
|
|
38
|
+
- `echo | node bin/kodelyth-ecc.js` → menu correctly skips, installer runs as before
|
|
39
|
+
- `require('./scripts/cli/update-check.js').check({current: '2.2.2'})` → live npm hit, returns `{latest: '2.2.2', updateAvailable: false, cached: false}` — real data, no fake fallback
|
|
40
|
+
|
|
5
41
|
## v2.2.0 — Codebase graph integration + self-documenting CLI (July 2026)
|
|
6
42
|
|
|
7
43
|
ECC now stacks three complementary memory + savings layers, all local, all real:
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.3.0
|
package/bin/kodelyth-ecc.js
CHANGED
|
@@ -33,6 +33,18 @@ const args = process.argv.slice(2).filter((a, i, arr) => {
|
|
|
33
33
|
});
|
|
34
34
|
const isWin = os.platform() === 'win32';
|
|
35
35
|
|
|
36
|
+
// ── Interactive menu — fires when invoked with no args on a real terminal ───
|
|
37
|
+
// Skip if the user passed any flag/subcommand, if piped/CI, or with --no-menu.
|
|
38
|
+
if (args.length === 0
|
|
39
|
+
&& process.stdin.isTTY
|
|
40
|
+
&& process.stdout.isTTY
|
|
41
|
+
&& !process.env.CI
|
|
42
|
+
&& !process.env.KODELYTH_NO_MENU) {
|
|
43
|
+
require(path.join(ROOT, 'scripts', 'cli', 'menu.js')).main()
|
|
44
|
+
.catch(err => { process.stderr.write((err.stack || err.message) + '\n'); process.exit(1); });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
// ── Per-command help — runs BEFORE any subcommand block consumes --help ─────
|
|
37
49
|
if (args[1] === '--help' || args[1] === '-h') {
|
|
38
50
|
const HELP_MAP = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodelyth-ecc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Production-grade AI coding toolkit — 70 agents (incl. devil-mode adversarial crew), 194 skills, 97 commands, parallel multi-agent commands, semantic intent routing, self-learning memory, and a built-in MCP server (16 tools / 6 prompts / 377 resources) that bridges to Claude Desktop, LangGraph, AutoGen, CrewAI, and OpenAI Agents SDK. Works with Claude Code, Windsurf, Cursor, Codex, Antigravity, OpenCode, Cline, RooCode, Aider, Kimi, and Gemini CLI.",
|
|
5
5
|
"author": "Kodelyth <github.com/sifxprime>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
"productivity"
|
|
57
57
|
],
|
|
58
58
|
"bin": {
|
|
59
|
-
"kodelyth-ecc": "bin/kodelyth-ecc.js"
|
|
59
|
+
"kodelyth-ecc": "bin/kodelyth-ecc.js",
|
|
60
|
+
"kodelythecc": "bin/kodelyth-ecc.js"
|
|
60
61
|
},
|
|
61
62
|
"scripts": {
|
|
62
63
|
"test": "node tests/run-all.js",
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// scripts/cli/menu.js
|
|
2
|
+
// Interactive arrow-key CLI menu. Zero deps. Uses raw stdin + ANSI escapes.
|
|
3
|
+
// Triggered when `kodelythecc` is run with no args in a TTY.
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { spawn, spawnSync } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
// ── ANSI helpers (no chalk dep) ─────────────────────────────────────────────
|
|
13
|
+
const C = {
|
|
14
|
+
reset: '\x1b[0m',
|
|
15
|
+
dim: '\x1b[2m',
|
|
16
|
+
bold: '\x1b[1m',
|
|
17
|
+
cyan: '\x1b[36m',
|
|
18
|
+
green: '\x1b[32m',
|
|
19
|
+
yellow: '\x1b[33m',
|
|
20
|
+
magenta:'\x1b[35m',
|
|
21
|
+
red: '\x1b[31m',
|
|
22
|
+
gray: '\x1b[90m',
|
|
23
|
+
invert: '\x1b[7m',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function clear() { process.stdout.write('\x1b[2J\x1b[H'); }
|
|
27
|
+
function hideCursor() { process.stdout.write('\x1b[?25l'); }
|
|
28
|
+
function showCursor() { process.stdout.write('\x1b[?25h'); }
|
|
29
|
+
function moveTo(row) { process.stdout.write(`\x1b[${row};1H`); }
|
|
30
|
+
|
|
31
|
+
// ── Build the menu options dynamically ──────────────────────────────────────
|
|
32
|
+
async function buildOptions({ ROOT }) {
|
|
33
|
+
const pkg = require(path.join(ROOT, 'package.json'));
|
|
34
|
+
const current = pkg.version;
|
|
35
|
+
|
|
36
|
+
// Update check — fires async, doesn't block the menu.
|
|
37
|
+
const { check } = require('./update-check.js');
|
|
38
|
+
const updateP = check({ current }).catch(() => ({ updateAvailable: false, latest: null }));
|
|
39
|
+
|
|
40
|
+
// Wait max 1s for the update check to return (so first render isn't a full 2.5s wait)
|
|
41
|
+
const update = await Promise.race([
|
|
42
|
+
updateP,
|
|
43
|
+
new Promise(r => setTimeout(() => r({ updateAvailable: false, latest: null, pending: true }), 1000)),
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const opts = [];
|
|
47
|
+
if (update.updateAvailable) {
|
|
48
|
+
opts.push({
|
|
49
|
+
label: `Update to v${update.latest}`,
|
|
50
|
+
hint: 'npm i -g kodelyth-ecc (installs new version)',
|
|
51
|
+
run: () => runUpdate(),
|
|
52
|
+
badge: 'NEW',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
opts.push({
|
|
56
|
+
label: 'Open Dashboard',
|
|
57
|
+
hint: 'localhost observability — RTK, Terse, Codebase, Memory (all real data)',
|
|
58
|
+
run: () => runDashboard(),
|
|
59
|
+
});
|
|
60
|
+
opts.push({
|
|
61
|
+
label: 'Install ECC for another IDE',
|
|
62
|
+
hint: 'claude-code, cursor, windsurf, antigravity, codex, opencode, gemini, cline, roocode, kimi',
|
|
63
|
+
run: () => pickIdeAndInstall(ROOT),
|
|
64
|
+
});
|
|
65
|
+
opts.push({
|
|
66
|
+
label: 'RTK status (input token savings)',
|
|
67
|
+
hint: 'Show RTK version + wired IDEs + savings',
|
|
68
|
+
run: () => runSub(['rtk', 'status']),
|
|
69
|
+
});
|
|
70
|
+
opts.push({
|
|
71
|
+
label: 'Terse status (output compression)',
|
|
72
|
+
hint: 'Skill install state, ledger totals',
|
|
73
|
+
run: () => runSub(['terse', 'status']),
|
|
74
|
+
});
|
|
75
|
+
opts.push({
|
|
76
|
+
label: 'Codebase graph status',
|
|
77
|
+
hint: 'codebase-memory-mcp (DeusData) — 158 languages, structural queries',
|
|
78
|
+
run: () => runSub(['codebase', 'status']),
|
|
79
|
+
});
|
|
80
|
+
opts.push({
|
|
81
|
+
label: 'Memory stats (BM25 recall)',
|
|
82
|
+
hint: 'Local BM25 memory — captures, projects, top tags',
|
|
83
|
+
run: () => runSub(['dashboard', '--port', '5747', '--no-open']),
|
|
84
|
+
background: true,
|
|
85
|
+
backgroundNote: 'Dashboard started at http://127.0.0.1:5747 — visit for full stats. Ctrl+C in this menu to stop watching.',
|
|
86
|
+
});
|
|
87
|
+
opts.push({
|
|
88
|
+
label: 'Run in background (dashboard + hooks live)',
|
|
89
|
+
hint: 'Detach dashboard as a daemon so /api routes stay reachable',
|
|
90
|
+
run: () => runBackground(),
|
|
91
|
+
});
|
|
92
|
+
opts.push({
|
|
93
|
+
label: 'Exit',
|
|
94
|
+
hint: '',
|
|
95
|
+
run: () => process.exit(0),
|
|
96
|
+
exit: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return { opts, current, update };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Actions ─────────────────────────────────────────────────────────────────
|
|
103
|
+
function runUpdate() {
|
|
104
|
+
cleanup();
|
|
105
|
+
console.log(`${C.cyan}Running: npm install -g kodelyth-ecc${C.reset}\n`);
|
|
106
|
+
const r = spawnSync('npm', ['install', '-g', 'kodelyth-ecc'], { stdio: 'inherit' });
|
|
107
|
+
process.exit(r.status ?? 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function runDashboard() {
|
|
111
|
+
cleanup();
|
|
112
|
+
console.log(`${C.cyan}Booting dashboard on http://127.0.0.1:5747${C.reset}`);
|
|
113
|
+
console.log(`${C.gray}(Ctrl+C to stop)${C.reset}\n`);
|
|
114
|
+
const r = spawnSync(process.execPath, [
|
|
115
|
+
path.join(__dirname, '..', '..', 'bin', 'kodelyth-ecc.js'),
|
|
116
|
+
'dashboard'
|
|
117
|
+
], { stdio: 'inherit' });
|
|
118
|
+
process.exit(r.status ?? 0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function runSub(args) {
|
|
122
|
+
cleanup();
|
|
123
|
+
const r = spawnSync(process.execPath, [
|
|
124
|
+
path.join(__dirname, '..', '..', 'bin', 'kodelyth-ecc.js'),
|
|
125
|
+
...args,
|
|
126
|
+
], { stdio: 'inherit' });
|
|
127
|
+
console.log(`\n${C.gray}Press Enter to return to menu…${C.reset}`);
|
|
128
|
+
waitForEnter().then(() => main().catch(() => process.exit(0)));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function pickIdeAndInstall(ROOT) {
|
|
132
|
+
const targets = [
|
|
133
|
+
'claude-code', 'cursor-project', 'windsurf-home', 'windsurf-project',
|
|
134
|
+
'antigravity', 'codex-home', 'opencode', 'cline', 'roocode',
|
|
135
|
+
'gemini-home', 'gemini-project', 'kimi', 'aider',
|
|
136
|
+
];
|
|
137
|
+
const chosen = await pick('Pick an IDE to install for', targets);
|
|
138
|
+
if (chosen == null) return main();
|
|
139
|
+
cleanup();
|
|
140
|
+
console.log(`${C.cyan}Installing ECC for ${targets[chosen]}…${C.reset}\n`);
|
|
141
|
+
const r = spawnSync(process.execPath, [
|
|
142
|
+
path.join(ROOT, 'bin', 'kodelyth-ecc.js'),
|
|
143
|
+
'--target', targets[chosen],
|
|
144
|
+
], {
|
|
145
|
+
stdio: 'inherit',
|
|
146
|
+
env: { ...process.env, KODELYTH_NONINTERACTIVE: '1' },
|
|
147
|
+
});
|
|
148
|
+
console.log(`\n${C.gray}Press Enter to return to menu…${C.reset}`);
|
|
149
|
+
waitForEnter().then(() => main().catch(() => process.exit(0)));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function runBackground() {
|
|
153
|
+
cleanup();
|
|
154
|
+
const logDir = path.join(os.homedir(), '.kodelythecc');
|
|
155
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
156
|
+
const logFile = path.join(logDir, 'dashboard-daemon.log');
|
|
157
|
+
const pidFile = path.join(logDir, 'dashboard-daemon.pid');
|
|
158
|
+
const child = spawn(process.execPath, [
|
|
159
|
+
path.join(__dirname, '..', '..', 'bin', 'kodelyth-ecc.js'),
|
|
160
|
+
'dashboard', '--port', '5747', '--no-open',
|
|
161
|
+
], {
|
|
162
|
+
detached: true, stdio: ['ignore', fs.openSync(logFile, 'a'), fs.openSync(logFile, 'a')],
|
|
163
|
+
});
|
|
164
|
+
child.unref();
|
|
165
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
166
|
+
console.log(`${C.green}✓${C.reset} Dashboard daemon started (pid ${child.pid})`);
|
|
167
|
+
console.log(` URL: ${C.cyan}http://127.0.0.1:5747${C.reset}`);
|
|
168
|
+
console.log(` Log: ${logFile}`);
|
|
169
|
+
console.log(` Pid: ${pidFile}`);
|
|
170
|
+
console.log(` Stop: ${C.dim}kill $(cat ${pidFile})${C.reset}`);
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Render + keyboard loop ──────────────────────────────────────────────────
|
|
175
|
+
let _keyHandler = null;
|
|
176
|
+
function cleanup() {
|
|
177
|
+
showCursor();
|
|
178
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
179
|
+
try { process.stdin.pause(); } catch {}
|
|
180
|
+
if (_keyHandler) { process.stdin.removeListener('data', _keyHandler); _keyHandler = null; }
|
|
181
|
+
}
|
|
182
|
+
process.on('exit', cleanup);
|
|
183
|
+
process.on('SIGINT', () => { cleanup(); console.log(); process.exit(0); });
|
|
184
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
185
|
+
|
|
186
|
+
function render({ current, update, opts, index }) {
|
|
187
|
+
clear();
|
|
188
|
+
const w = process.stdout.columns || 80;
|
|
189
|
+
const bar = '─'.repeat(Math.max(20, w - 4));
|
|
190
|
+
let updateLine = C.gray + 'up to date' + C.reset;
|
|
191
|
+
if (update?.pending) updateLine = C.dim + 'checking npm registry…' + C.reset;
|
|
192
|
+
else if (update?.updateAvailable) updateLine = C.yellow + `${C.bold}update available: v${update.latest}${C.reset}`;
|
|
193
|
+
else if (update?.latest && update.latest !== current) updateLine = C.gray + `latest: v${update.latest}` + C.reset;
|
|
194
|
+
|
|
195
|
+
console.log(`${C.cyan}${C.bold}⚙ Kodelyth ECC${C.reset} ${C.gray}v${current}${C.reset} ${C.dim}·${C.reset} ${C.magenta}Elite Code Crew${C.reset} ${updateLine}`);
|
|
196
|
+
console.log(`${C.gray}${bar}${C.reset}`);
|
|
197
|
+
console.log('');
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < opts.length; i++) {
|
|
200
|
+
const o = opts[i];
|
|
201
|
+
const active = i === index;
|
|
202
|
+
const marker = active ? `${C.cyan} ▸ ${C.reset}` : ' ';
|
|
203
|
+
const label = active ? `${C.bold}${o.label}${C.reset}` : o.label;
|
|
204
|
+
const hint = o.hint ? `${C.gray}${o.hint}${C.reset}` : '';
|
|
205
|
+
const badge = o.badge ? ` ${C.yellow}[${o.badge}]${C.reset}` : '';
|
|
206
|
+
// Two columns: label (36 wide) then hint.
|
|
207
|
+
const labelText = (active ? '▸ ' : ' ') + o.label + (o.badge ? ` [${o.badge}]` : '');
|
|
208
|
+
const pad = ' '.repeat(Math.max(2, 40 - labelText.length));
|
|
209
|
+
const colored = active
|
|
210
|
+
? `${C.cyan} ▸ ${C.bold}${o.label}${C.reset}${badge}${pad}${hint}`
|
|
211
|
+
: ` ${o.label}${badge}${pad}${hint}`;
|
|
212
|
+
console.log(colored);
|
|
213
|
+
}
|
|
214
|
+
console.log('');
|
|
215
|
+
console.log(`${C.gray}↑/↓ navigate · ⏎ select · q / esc / Ctrl+C to quit${C.reset}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function pick(title, items) {
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
let idx = 0;
|
|
221
|
+
const draw = () => {
|
|
222
|
+
clear();
|
|
223
|
+
console.log(`${C.cyan}${C.bold}${title}${C.reset}\n`);
|
|
224
|
+
for (let i = 0; i < items.length; i++) {
|
|
225
|
+
console.log(i === idx ? `${C.cyan} ▸ ${C.bold}${items[i]}${C.reset}` : ` ${items[i]}`);
|
|
226
|
+
}
|
|
227
|
+
console.log(`\n${C.gray}↑/↓ move · ⏎ pick · esc / q cancel${C.reset}`);
|
|
228
|
+
};
|
|
229
|
+
hideCursor();
|
|
230
|
+
process.stdin.setRawMode(true); process.stdin.resume();
|
|
231
|
+
draw();
|
|
232
|
+
const h = (b) => {
|
|
233
|
+
const s = b.toString();
|
|
234
|
+
if (s === '\x1b[A' || s === 'k') { idx = (idx - 1 + items.length) % items.length; draw(); }
|
|
235
|
+
else if (s === '\x1b[B' || s === 'j') { idx = (idx + 1) % items.length; draw(); }
|
|
236
|
+
else if (s === '\r' || s === '\n') {
|
|
237
|
+
process.stdin.removeListener('data', h); resolve(idx);
|
|
238
|
+
} else if (s === 'q' || s === '\x1b' || s === '\x03') {
|
|
239
|
+
process.stdin.removeListener('data', h); resolve(null);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
process.stdin.on('data', h);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function waitForEnter() {
|
|
247
|
+
return new Promise((resolve) => {
|
|
248
|
+
process.stdin.setRawMode(true); process.stdin.resume();
|
|
249
|
+
const h = (b) => {
|
|
250
|
+
const s = b.toString();
|
|
251
|
+
if (s === '\r' || s === '\n' || s === 'q' || s === '\x1b' || s === '\x03') {
|
|
252
|
+
process.stdin.removeListener('data', h); resolve();
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
process.stdin.on('data', h);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Main loop ───────────────────────────────────────────────────────────────
|
|
260
|
+
async function main() {
|
|
261
|
+
const ROOT = path.join(__dirname, '..', '..');
|
|
262
|
+
const { opts, current, update } = await buildOptions({ ROOT });
|
|
263
|
+
let index = 0;
|
|
264
|
+
hideCursor();
|
|
265
|
+
process.stdin.setRawMode(true);
|
|
266
|
+
process.stdin.resume();
|
|
267
|
+
render({ current, update, opts, index });
|
|
268
|
+
|
|
269
|
+
_keyHandler = (buf) => {
|
|
270
|
+
const s = buf.toString();
|
|
271
|
+
if (s === '\x1b[A' || s === 'k') { index = (index - 1 + opts.length) % opts.length; render({ current, update, opts, index }); }
|
|
272
|
+
else if (s === '\x1b[B' || s === 'j') { index = (index + 1) % opts.length; render({ current, update, opts, index }); }
|
|
273
|
+
else if (s === '\r' || s === '\n') {
|
|
274
|
+
const chosen = opts[index];
|
|
275
|
+
process.stdin.removeListener('data', _keyHandler);
|
|
276
|
+
_keyHandler = null;
|
|
277
|
+
Promise.resolve(chosen.run()).catch(err => {
|
|
278
|
+
cleanup();
|
|
279
|
+
console.error(err.message || err);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else if (s === 'q' || s === '\x1b' || s === '\x03') { cleanup(); console.log(); process.exit(0); }
|
|
284
|
+
};
|
|
285
|
+
process.stdin.on('data', _keyHandler);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = { main };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// scripts/cli/update-check.js
|
|
2
|
+
// Poll npm for the latest kodelyth-ecc version. Cached for 24h to avoid spam.
|
|
3
|
+
// Zero deps — uses Node's built-in https.
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const CACHE_DIR = path.join(os.homedir(), '.kodelythecc');
|
|
13
|
+
const CACHE_FILE = path.join(CACHE_DIR, 'update-check.json');
|
|
14
|
+
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24h
|
|
15
|
+
|
|
16
|
+
function readCache() {
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
|
19
|
+
if (Date.now() - raw.at < CACHE_TTL) return raw;
|
|
20
|
+
} catch { /* no cache or corrupt */ }
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeCache(latest) {
|
|
25
|
+
try { fs.mkdirSync(CACHE_DIR, { recursive: true }); } catch {}
|
|
26
|
+
try { fs.writeFileSync(CACHE_FILE, JSON.stringify({ at: Date.now(), latest })); } catch {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fetchLatest() {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const req = https.get('https://registry.npmjs.org/kodelyth-ecc/latest', {
|
|
32
|
+
timeout: 2500,
|
|
33
|
+
headers: { accept: 'application/json' },
|
|
34
|
+
}, (res) => {
|
|
35
|
+
if (res.statusCode !== 200) { res.resume(); return resolve(null); }
|
|
36
|
+
let body = '';
|
|
37
|
+
res.on('data', c => body += c);
|
|
38
|
+
res.on('end', () => {
|
|
39
|
+
try { resolve(JSON.parse(body).version || null); } catch { resolve(null); }
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
req.on('error', () => resolve(null));
|
|
43
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function cmpVersion(a, b) {
|
|
48
|
+
const pa = a.split('.').map(n => parseInt(n, 10) || 0);
|
|
49
|
+
const pb = b.split('.').map(n => parseInt(n, 10) || 0);
|
|
50
|
+
for (let i = 0; i < 3; i++) {
|
|
51
|
+
if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Public: returns { current, latest, updateAvailable, cached } or nulls on failure.
|
|
57
|
+
async function check({ current, force = false } = {}) {
|
|
58
|
+
if (!force) {
|
|
59
|
+
const c = readCache();
|
|
60
|
+
if (c) {
|
|
61
|
+
return {
|
|
62
|
+
current, latest: c.latest,
|
|
63
|
+
updateAvailable: c.latest && cmpVersion(c.latest, current) > 0,
|
|
64
|
+
cached: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const latest = await fetchLatest();
|
|
69
|
+
if (latest) writeCache(latest);
|
|
70
|
+
return {
|
|
71
|
+
current, latest,
|
|
72
|
+
updateAvailable: !!(latest && cmpVersion(latest, current) > 0),
|
|
73
|
+
cached: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { check, cmpVersion };
|