@lorekit/cli 1.0.1 → 1.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/README.md +57 -7
- package/bin/lorekit.mjs +35 -7
- package/package.json +6 -2
- package/skill/lorekit-memory/SKILL.md +3 -2
- package/src/config.mjs +249 -26
- package/src/doctor.mjs +12 -5
- package/src/install.mjs +72 -14
- package/src/mcp-server.mjs +18 -1
- package/src/store/created-at.mjs +28 -0
- package/src/store/local.mjs +19 -3
- package/src/telemetry-token.mjs +17 -0
- package/src/telemetry.mjs +325 -0
- package/src/uninstall.mjs +114 -0
- package/src/util.mjs +77 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// `lorekit uninstall` — reverse `install`: remove the skill, the MCP server
|
|
2
|
+
// entry, and the lifecycle hooks for the chosen scope. Surgical — every other
|
|
3
|
+
// server, hook, and setting is left untouched.
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import readline from 'node:readline';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import {
|
|
8
|
+
SKILL_NAME,
|
|
9
|
+
resolveProjectRoot,
|
|
10
|
+
removeSkill,
|
|
11
|
+
removeMcpServer,
|
|
12
|
+
removeClaudeHooks,
|
|
13
|
+
homeDir,
|
|
14
|
+
} from './config.mjs';
|
|
15
|
+
import { log, heading, status, c } from './util.mjs';
|
|
16
|
+
|
|
17
|
+
function ask(question) {
|
|
18
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
19
|
+
return new Promise((resolve) => rl.question(question, (a) => { rl.close(); resolve(a.trim()); }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function uninstall(args) {
|
|
23
|
+
const root = resolveProjectRoot(args.dir);
|
|
24
|
+
const nonInteractive = Boolean(args.yes) || !process.stdin.isTTY;
|
|
25
|
+
|
|
26
|
+
heading('LoreKit uninstall');
|
|
27
|
+
log(` project: ${c.dim(root)}`);
|
|
28
|
+
|
|
29
|
+
// Mirror install's scope selection: --project / --global force it, else
|
|
30
|
+
// prompt when interactive, else default to project.
|
|
31
|
+
let scope = args.global ? 'global' : args.project ? 'project' : null;
|
|
32
|
+
if (!scope) {
|
|
33
|
+
if (nonInteractive) {
|
|
34
|
+
scope = 'project';
|
|
35
|
+
} else {
|
|
36
|
+
const ans = (
|
|
37
|
+
await ask(' Remove from this project or the global install? [project/global] (project): ')
|
|
38
|
+
).toLowerCase();
|
|
39
|
+
scope = ans.startsWith('g') ? 'global' : 'project';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
log(
|
|
43
|
+
` scope: ${c.dim(
|
|
44
|
+
scope === 'global' ? 'global — ~/.claude, applies to every project' : 'project — this repo only',
|
|
45
|
+
)}`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Each removal is independent and best-effort: a corrupt or unwritable config
|
|
49
|
+
// for one target must not crash the run or block the others. `removeMcpServer`
|
|
50
|
+
// / `removeClaudeHooks` throw on an unparseable file (the same fail-safe as
|
|
51
|
+
// install — never clobber what we can't understand), so we catch and report it
|
|
52
|
+
// cleanly, leaving that file byte-for-byte untouched. Atomic writes
|
|
53
|
+
// (writeFileAtomic) guarantee a config can't be half-written even on a crash.
|
|
54
|
+
const skill = attempt(() => removeSkill(root, scope));
|
|
55
|
+
const mcp = attempt(() => removeMcpServer(root, scope));
|
|
56
|
+
const hooks = attempt(() => removeClaudeHooks(root, scope));
|
|
57
|
+
|
|
58
|
+
// Global paths shown relative to ~; project paths repo-relative.
|
|
59
|
+
const display = (p) =>
|
|
60
|
+
scope === 'global' ? p.replace(homeDir(), '~') : path.relative(root, p) || p;
|
|
61
|
+
const mcpLabel = scope === 'global' ? '~/.claude.json' : '.mcp.json';
|
|
62
|
+
|
|
63
|
+
heading('Done');
|
|
64
|
+
report(skill, `skill ${SKILL_NAME}`, {
|
|
65
|
+
done: (r) => `removed → ${display(r.dest)}`,
|
|
66
|
+
noop: 'not installed — nothing to remove',
|
|
67
|
+
});
|
|
68
|
+
report(mcp, mcpLabel, {
|
|
69
|
+
done: (r) => `lorekit server removed → ${display(r.file)}`,
|
|
70
|
+
noop: 'no lorekit server entry — nothing to remove',
|
|
71
|
+
});
|
|
72
|
+
report(hooks, 'hooks', {
|
|
73
|
+
done: (r) => `${r.removed} removed → ${display(r.file)}`,
|
|
74
|
+
noop: 'no lorekit hooks — nothing to remove',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const failed = [skill, mcp, hooks].some((s) => !s.ok);
|
|
78
|
+
const any = (skill.result?.removed || mcp.result?.removed || hooks.result?.removed) && true;
|
|
79
|
+
|
|
80
|
+
if (failed) {
|
|
81
|
+
log(`\n ${c.dim('Some items could not be removed and were left untouched — see above.')}`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
if (!any) {
|
|
85
|
+
const other = scope === 'global' ? '--project' : '--global';
|
|
86
|
+
log(`\n ${c.dim(`Nothing found for the ${scope} scope — try ${other}?`)}`);
|
|
87
|
+
} else {
|
|
88
|
+
log(`\n ${c.dim('Removed. Your other MCP servers, hooks, and settings were left untouched.')}`);
|
|
89
|
+
log(` ${c.dim('Note: your token may remain in shell history or env — rotate it if it leaked.')}`);
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Run a removal step, converting a throw into a reportable outcome so one bad
|
|
95
|
+
// config can't crash the whole uninstall or leave a stack trace on screen.
|
|
96
|
+
function attempt(fn) {
|
|
97
|
+
try {
|
|
98
|
+
return { ok: true, result: fn() };
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return { ok: false, error: e };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Render one status line for a step: a clean error (file left untouched), a
|
|
105
|
+
// "removed" line, or a "nothing to remove" line.
|
|
106
|
+
function report(step, label, { done, noop }) {
|
|
107
|
+
if (!step.ok) {
|
|
108
|
+
status('fail', label, `left untouched — ${step.error.message}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const r = step.result;
|
|
112
|
+
const removed = r.removed;
|
|
113
|
+
status(removed ? 'pass' : 'info', label, removed ? done(r) : noop);
|
|
114
|
+
}
|
package/src/util.mjs
CHANGED
|
@@ -40,6 +40,83 @@ export function status(kind, label, detail) {
|
|
|
40
40
|
log(` ${mark} ${label}${tail}`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Map a raw keypress to a select-list action. Pure so it can be unit-tested
|
|
44
|
+
// without a pseudo-TTY. Supports arrow keys (both the `ESC [` and application
|
|
45
|
+
// `ESC O` cursor modes) and vim-style j/k.
|
|
46
|
+
export function selectAction(key) {
|
|
47
|
+
if (key === '') return 'cancel'; // Ctrl-C
|
|
48
|
+
if (key === '\r' || key === '\n') return 'submit';
|
|
49
|
+
if (key === 'k' || (key.startsWith('') && key.endsWith('A'))) return 'up';
|
|
50
|
+
if (key === 'j' || (key.startsWith('') && key.endsWith('B'))) return 'down';
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Interactive single-choice list. `options` is [{ label, value, hint? }].
|
|
55
|
+
// Arrow keys / j / k move, Enter selects, Ctrl-C aborts. Falls back to the
|
|
56
|
+
// default option when stdin isn't a TTY (CI / piped input), matching the
|
|
57
|
+
// non-interactive install path. Zero-dependency; renders with raw ANSI.
|
|
58
|
+
export function select(question, options, { defaultIndex = 0 } = {}) {
|
|
59
|
+
const { stdin, stdout } = process;
|
|
60
|
+
let index = Math.max(0, Math.min(defaultIndex, options.length - 1));
|
|
61
|
+
|
|
62
|
+
if (!stdin.isTTY) return Promise.resolve(options[index].value);
|
|
63
|
+
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
const render = (first) => {
|
|
66
|
+
if (!first) stdout.write(`[${options.length}A`); // cursor up N lines
|
|
67
|
+
for (let i = 0; i < options.length; i++) {
|
|
68
|
+
const active = i === index;
|
|
69
|
+
const pointer = active ? c.cyan('❯') : ' ';
|
|
70
|
+
const label = active ? c.cyan(options[i].label) : options[i].label;
|
|
71
|
+
const hint = options[i].hint ? ` ${c.dim('— ' + options[i].hint)}` : '';
|
|
72
|
+
stdout.write(`[2K ${pointer} ${label}${hint}\n`); // clear line, write
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
log(` ${question}`);
|
|
77
|
+
stdout.write('[?25l'); // hide cursor
|
|
78
|
+
render(true);
|
|
79
|
+
|
|
80
|
+
const prevRaw = Boolean(stdin.isRaw);
|
|
81
|
+
stdin.setRawMode(true);
|
|
82
|
+
stdin.resume();
|
|
83
|
+
stdin.setEncoding('utf8');
|
|
84
|
+
|
|
85
|
+
const cleanup = () => {
|
|
86
|
+
stdin.setRawMode(prevRaw);
|
|
87
|
+
stdin.pause();
|
|
88
|
+
stdin.removeListener('data', onData);
|
|
89
|
+
stdout.write('[?25h'); // show cursor
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onData = (key) => {
|
|
93
|
+
switch (selectAction(key)) {
|
|
94
|
+
case 'cancel':
|
|
95
|
+
cleanup();
|
|
96
|
+
stdout.write('\n');
|
|
97
|
+
process.exit(130);
|
|
98
|
+
break;
|
|
99
|
+
case 'up':
|
|
100
|
+
index = (index - 1 + options.length) % options.length;
|
|
101
|
+
render(false);
|
|
102
|
+
break;
|
|
103
|
+
case 'down':
|
|
104
|
+
index = (index + 1) % options.length;
|
|
105
|
+
render(false);
|
|
106
|
+
break;
|
|
107
|
+
case 'submit':
|
|
108
|
+
cleanup();
|
|
109
|
+
resolve(options[index].value);
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
stdin.on('data', onData);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
43
120
|
// Minimal flag parser: --key value, --key=value, -k value, and bare --flags.
|
|
44
121
|
// `aliases` maps short → long; `booleans` lists flags that take no value.
|
|
45
122
|
export function parseArgs(argv, { aliases = {}, booleans = [] } = {}) {
|