lithermes-ai 0.5.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/LICENSE +21 -0
- package/README.md +245 -0
- package/README_Ko-KR.md +245 -0
- package/assets/lithermes-plugin/NOTICE.md +37 -0
- package/assets/lithermes-plugin/README.md +40 -0
- package/assets/lithermes-plugin/__init__.py +179 -0
- package/assets/lithermes-plugin/core.py +853 -0
- package/assets/lithermes-plugin/litgoal/__init__.py +10 -0
- package/assets/lithermes-plugin/litgoal/cli.py +133 -0
- package/assets/lithermes-plugin/litgoal/hook.py +48 -0
- package/assets/lithermes-plugin/litgoal/model.py +171 -0
- package/assets/lithermes-plugin/litgoal/runtime.py +273 -0
- package/assets/lithermes-plugin/litgoal/store.py +93 -0
- package/assets/lithermes-plugin/litgoal/tools.py +228 -0
- package/assets/lithermes-plugin/payload-version.json +471 -0
- package/assets/lithermes-plugin/plugin.yaml +9 -0
- package/assets/lithermes-plugin/skills/ai-slop-remover/SKILL.md +142 -0
- package/assets/lithermes-plugin/skills/comment-checker/SKILL.md +50 -0
- package/assets/lithermes-plugin/skills/debugging/SKILL.md +116 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/00-setup.md +108 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/02-investigate.md +121 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/04-oracle-triple.md +136 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/05-escalate.md +69 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/06-fix.md +116 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/08-qa.md +94 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/09-cleanup.md +164 -0
- package/assets/lithermes-plugin/skills/debugging/references/methodology/partial-runtime-evidence.md +229 -0
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/go.md +252 -0
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/native-binary.md +484 -0
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/node.md +260 -0
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/python.md +248 -0
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/rust.md +234 -0
- package/assets/lithermes-plugin/skills/debugging/references/tools/ghidra.md +212 -0
- package/assets/lithermes-plugin/skills/debugging/references/tools/playwright-cli.md +194 -0
- package/assets/lithermes-plugin/skills/debugging/references/tools/pwndbg.md +263 -0
- package/assets/lithermes-plugin/skills/debugging/references/tools/pwntools.md +265 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/SKILL.md +77 -0
- package/assets/lithermes-plugin/skills/lit-plan/SKILL.md +374 -0
- package/assets/lithermes-plugin/skills/litgoal/.gitkeep +0 -0
- package/assets/lithermes-plugin/skills/litgoal/SKILL.md +207 -0
- package/assets/lithermes-plugin/skills/litwork/SKILL.md +262 -0
- package/assets/lithermes-plugin/skills/lsp/SKILL.md +53 -0
- package/assets/lithermes-plugin/skills/programming/SKILL.md +463 -0
- package/assets/lithermes-plugin/skills/programming/references/go/README.md +90 -0
- package/assets/lithermes-plugin/skills/programming/references/go/backend-stack.md +641 -0
- package/assets/lithermes-plugin/skills/programming/references/go/bootstrap.md +328 -0
- package/assets/lithermes-plugin/skills/programming/references/go/bubbletea-v2.md +360 -0
- package/assets/lithermes-plugin/skills/programming/references/go/cobra-stack.md +468 -0
- package/assets/lithermes-plugin/skills/programming/references/go/concurrency.md +362 -0
- package/assets/lithermes-plugin/skills/programming/references/go/data-modeling.md +329 -0
- package/assets/lithermes-plugin/skills/programming/references/go/error-handling.md +359 -0
- package/assets/lithermes-plugin/skills/programming/references/go/golangci-strict.md +236 -0
- package/assets/lithermes-plugin/skills/programming/references/go/grpc-connect.md +375 -0
- package/assets/lithermes-plugin/skills/programming/references/go/libraries.md +337 -0
- package/assets/lithermes-plugin/skills/programming/references/go/one-liners.md +202 -0
- package/assets/lithermes-plugin/skills/programming/references/go/sqlc-pgx.md +471 -0
- package/assets/lithermes-plugin/skills/programming/references/go/testing.md +467 -0
- package/assets/lithermes-plugin/skills/programming/references/go/type-patterns.md +298 -0
- package/assets/lithermes-plugin/skills/programming/references/python/README.md +314 -0
- package/assets/lithermes-plugin/skills/programming/references/python/async-anyio.md +442 -0
- package/assets/lithermes-plugin/skills/programming/references/python/data-modeling.md +233 -0
- package/assets/lithermes-plugin/skills/programming/references/python/data-processing.md +133 -0
- package/assets/lithermes-plugin/skills/programming/references/python/error-handling.md +218 -0
- package/assets/lithermes-plugin/skills/programming/references/python/fastapi-stack.md +316 -0
- package/assets/lithermes-plugin/skills/programming/references/python/httpx2-optimization.md +360 -0
- package/assets/lithermes-plugin/skills/programming/references/python/libraries.md +307 -0
- package/assets/lithermes-plugin/skills/programming/references/python/one-liners.md +268 -0
- package/assets/lithermes-plugin/skills/programming/references/python/orjson-stack.md +378 -0
- package/assets/lithermes-plugin/skills/programming/references/python/pydantic-ai.md +285 -0
- package/assets/lithermes-plugin/skills/programming/references/python/pyproject-strict.md +232 -0
- package/assets/lithermes-plugin/skills/programming/references/python/textual-tui.md +201 -0
- package/assets/lithermes-plugin/skills/programming/references/python/type-patterns.md +176 -0
- package/assets/lithermes-plugin/skills/programming/references/rust/README.md +317 -0
- package/assets/lithermes-plugin/skills/programming/references/rust/async-tokio.md +299 -0
- package/assets/lithermes-plugin/skills/programming/references/rust/axum-stack.md +467 -0
- package/assets/lithermes-plugin/skills/programming/references/rust/cargo-strict.md +317 -0
- package/assets/lithermes-plugin/skills/programming/references/rust/clap-stack.md +409 -0
- package/assets/lithermes-plugin/skills/programming/references/rust/concurrency.md +375 -0
- package/assets/lithermes-plugin/skills/programming/references/rust/libraries.md +439 -0
- package/assets/lithermes-plugin/skills/programming/references/rust/one-liners.md +291 -0
- package/assets/lithermes-plugin/skills/programming/references/rust/proptest-insta.md +429 -0
- package/assets/lithermes-plugin/skills/programming/references/rust/type-state.md +354 -0
- package/assets/lithermes-plugin/skills/programming/references/rust/unsafe-discipline.md +250 -0
- package/assets/lithermes-plugin/skills/programming/references/rust/zero-cost-safety.md +527 -0
- package/assets/lithermes-plugin/skills/programming/references/rust-ub/README.md +289 -0
- package/assets/lithermes-plugin/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
- package/assets/lithermes-plugin/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
- package/assets/lithermes-plugin/skills/programming/references/typescript/README.md +195 -0
- package/assets/lithermes-plugin/skills/programming/references/typescript/backend-hono.md +672 -0
- package/assets/lithermes-plugin/skills/programming/references/typescript/bootstrap.md +199 -0
- package/assets/lithermes-plugin/skills/programming/references/typescript/data-modeling.md +202 -0
- package/assets/lithermes-plugin/skills/programming/references/typescript/error-handling.md +169 -0
- package/assets/lithermes-plugin/skills/programming/references/typescript/tsconfig-strict.md +152 -0
- package/assets/lithermes-plugin/skills/programming/references/typescript/type-patterns.md +196 -0
- package/assets/lithermes-plugin/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
- package/assets/lithermes-plugin/skills/programming/scripts/go/new-project.py +138 -0
- package/assets/lithermes-plugin/skills/programming/scripts/go/templates/.editorconfig +13 -0
- package/assets/lithermes-plugin/skills/programming/scripts/go/templates/.golangci.yml +95 -0
- package/assets/lithermes-plugin/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
- package/assets/lithermes-plugin/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
- package/assets/lithermes-plugin/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
- package/assets/lithermes-plugin/skills/programming/scripts/go/templates/ci.yml +37 -0
- package/assets/lithermes-plugin/skills/programming/scripts/go/templates/config.go +24 -0
- package/assets/lithermes-plugin/skills/programming/scripts/go/templates/gitignore +15 -0
- package/assets/lithermes-plugin/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
- package/assets/lithermes-plugin/skills/programming/scripts/go/templates/run.go +15 -0
- package/assets/lithermes-plugin/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
- package/assets/lithermes-plugin/skills/programming/scripts/python/new-project.py +172 -0
- package/assets/lithermes-plugin/skills/programming/scripts/python/new-script.py +116 -0
- package/assets/lithermes-plugin/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
- package/assets/lithermes-plugin/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
- package/assets/lithermes-plugin/skills/programming/scripts/rust/new-project.py +175 -0
- package/assets/lithermes-plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
- package/assets/lithermes-plugin/skills/programming/scripts/typescript/new-project.ts +177 -0
- package/assets/lithermes-plugin/skills/refactor/SKILL.md +770 -0
- package/assets/lithermes-plugin/skills/remove-ai-slops/SKILL.md +335 -0
- package/assets/lithermes-plugin/skills/review-work/SKILL.md +562 -0
- package/assets/lithermes-plugin/skills/rules/SKILL.md +41 -0
- package/assets/lithermes-plugin/skills/start-work/SKILL.md +332 -0
- package/bin/lithermes.js +8 -0
- package/cover.png +0 -0
- package/package.json +39 -0
- package/src/cli.js +129 -0
- package/src/lib/check.js +94 -0
- package/src/lib/config.js +170 -0
- package/src/lib/files.js +65 -0
- package/src/lib/hermesDiscovery.js +50 -0
- package/src/lib/hud.js +121 -0
- package/src/lib/install.js +159 -0
- package/src/lib/patch.js +153 -0
- package/src/lib/skins.js +113 -0
- package/src/lib/spinner.js +104 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { writeFileAtomic } = require("./files");
|
|
4
|
+
|
|
5
|
+
function configPath(hermesHome) {
|
|
6
|
+
return path.join(hermesHome, "config.yaml");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function readConfig(hermesHome) {
|
|
10
|
+
const file = configPath(hermesHome);
|
|
11
|
+
return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function findPluginsBlock(lines) {
|
|
15
|
+
const start = lines.findIndex((line) => /^plugins:\s*$/.test(line));
|
|
16
|
+
if (start === -1) return null;
|
|
17
|
+
let end = lines.length;
|
|
18
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
19
|
+
if (/^\S/.test(lines[i]) && !/^---\s*$/.test(lines[i])) {
|
|
20
|
+
end = i;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { start, end, lines: lines.slice(start, end) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function configHasLitHermes(text) {
|
|
28
|
+
const block = findPluginsBlock(text.split(/\r?\n/));
|
|
29
|
+
if (!block) return false;
|
|
30
|
+
return block.lines.some((line, index) => {
|
|
31
|
+
if (/^\s*enabled:\s*\[[^\]]*lithermes[^\]]*\]/.test(line)) return true;
|
|
32
|
+
if (!/^\s*enabled:\s*$/.test(line)) return false;
|
|
33
|
+
for (let i = index + 1; i < block.lines.length; i += 1) {
|
|
34
|
+
const child = block.lines[i];
|
|
35
|
+
if (/^\s{2}\S/.test(child) && !/^\s{4}-/.test(child)) return false;
|
|
36
|
+
if (/^\s*-\s*lithermes\s*$/.test(child)) return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function enableLitHermesConfig(text) {
|
|
43
|
+
if (configHasLitHermes(text)) return text;
|
|
44
|
+
if (!text.trim()) {
|
|
45
|
+
return "plugins:\n enabled:\n - lithermes\n";
|
|
46
|
+
}
|
|
47
|
+
const trailingNewline = text.endsWith("\n");
|
|
48
|
+
const lines = text.split(/\r?\n/);
|
|
49
|
+
if (!trailingNewline && lines[lines.length - 1] === "") lines.pop();
|
|
50
|
+
const block = findPluginsBlock(lines);
|
|
51
|
+
if (block) {
|
|
52
|
+
for (let i = block.start + 1; i < block.end; i += 1) {
|
|
53
|
+
const line = lines[i];
|
|
54
|
+
if (/^\s{2}enabled:\s*\[[^\]]*\]/.test(line)) {
|
|
55
|
+
lines[i] = line.replace(/\[([^\]]*)\]/, (_match, inner) => {
|
|
56
|
+
const values = inner.split(",").map((item) => item.trim()).filter(Boolean);
|
|
57
|
+
values.push("lithermes");
|
|
58
|
+
return `[${values.join(", ")}]`;
|
|
59
|
+
});
|
|
60
|
+
return `${lines.join("\n")}${trailingNewline ? "\n" : ""}`;
|
|
61
|
+
}
|
|
62
|
+
if (/^\s{2}enabled:\s*$/.test(line)) {
|
|
63
|
+
lines.splice(i + 1, 0, " - lithermes");
|
|
64
|
+
return `${lines.join("\n")}${trailingNewline ? "\n" : ""}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
lines.splice(block.start + 1, 0, " enabled:", " - lithermes");
|
|
68
|
+
return `${lines.join("\n")}${trailingNewline ? "\n" : ""}`;
|
|
69
|
+
}
|
|
70
|
+
const suffix = text.endsWith("\n") ? "" : "\n";
|
|
71
|
+
return `${text}${suffix}plugins:\n enabled:\n - lithermes\n`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function disableLitHermesConfig(text) {
|
|
75
|
+
const trailingNewline = text.endsWith("\n");
|
|
76
|
+
const lines = text.split(/\r?\n/);
|
|
77
|
+
const block = findPluginsBlock(lines);
|
|
78
|
+
if (!block) return text;
|
|
79
|
+
for (let i = block.start + 1; i < block.end; i += 1) {
|
|
80
|
+
const line = lines[i];
|
|
81
|
+
if (/^\s{2}enabled:\s*\[[^\]]*lithermes[^\]]*\]/.test(line)) {
|
|
82
|
+
const inner = line.match(/\[([^\]]*)\]/)?.[1] || "";
|
|
83
|
+
const values = inner.split(",").map((item) => item.trim()).filter((item) => item && item !== "lithermes");
|
|
84
|
+
lines[i] = ` enabled: [${values.join(", ")}]`;
|
|
85
|
+
}
|
|
86
|
+
if (/^\s*-\s*lithermes\s*$/.test(line)) {
|
|
87
|
+
lines.splice(i, 1);
|
|
88
|
+
i -= 1;
|
|
89
|
+
block.end -= 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return `${lines.join("\n")}${trailingNewline ? "\n" : ""}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function findTopBlock(lines, key) {
|
|
96
|
+
const re = new RegExp(`^${key}:\\s*$`);
|
|
97
|
+
const start = lines.findIndex((line) => re.test(line));
|
|
98
|
+
if (start === -1) return null;
|
|
99
|
+
let end = lines.length;
|
|
100
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
101
|
+
if (/^\S/.test(lines[i]) && !/^---\s*$/.test(lines[i])) {
|
|
102
|
+
end = i;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { start, end };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readDisplaySkinConfig(text) {
|
|
110
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
111
|
+
const block = findTopBlock(lines, "display");
|
|
112
|
+
if (!block) return null;
|
|
113
|
+
for (let i = block.start + 1; i < block.end; i += 1) {
|
|
114
|
+
const m = /^\s{2}skin:\s*(\S.*?)\s*$/.exec(lines[i]);
|
|
115
|
+
if (m) return m[1];
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function setDisplaySkinConfig(text, skinName) {
|
|
121
|
+
const value = String(skinName || "").trim();
|
|
122
|
+
if (!value) return text;
|
|
123
|
+
const trailingNewline = !text || text.endsWith("\n");
|
|
124
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
125
|
+
if (!trailingNewline && lines[lines.length - 1] === "") lines.pop();
|
|
126
|
+
const block = findTopBlock(lines, "display");
|
|
127
|
+
if (block) {
|
|
128
|
+
for (let i = block.start + 1; i < block.end; i += 1) {
|
|
129
|
+
if (/^\s{2}skin:\s*/.test(lines[i])) {
|
|
130
|
+
lines[i] = ` skin: ${value}`;
|
|
131
|
+
return `${lines.join("\n").replace(/\n*$/, "")}\n`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
lines.splice(block.start + 1, 0, ` skin: ${value}`);
|
|
135
|
+
return `${lines.join("\n").replace(/\n*$/, "")}\n`;
|
|
136
|
+
}
|
|
137
|
+
const body = lines.join("\n").replace(/\n*$/, "");
|
|
138
|
+
const prefix = body ? `${body}\n` : "";
|
|
139
|
+
return `${prefix}display:\n skin: ${value}\n`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function clearDisplaySkinConfig(text) {
|
|
143
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
144
|
+
const block = findTopBlock(lines, "display");
|
|
145
|
+
if (!block) return text;
|
|
146
|
+
for (let i = block.start + 1; i < block.end; i += 1) {
|
|
147
|
+
if (/^\s{2}skin:\s*/.test(lines[i])) {
|
|
148
|
+
lines.splice(i, 1);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return `${lines.join("\n").replace(/\n*$/, "")}\n`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function writeConfig(hermesHome, text) {
|
|
156
|
+
fs.mkdirSync(hermesHome, { recursive: true });
|
|
157
|
+
writeFileAtomic(configPath(hermesHome), text, "utf8");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
clearDisplaySkinConfig,
|
|
162
|
+
configHasLitHermes,
|
|
163
|
+
configPath,
|
|
164
|
+
disableLitHermesConfig,
|
|
165
|
+
enableLitHermesConfig,
|
|
166
|
+
readConfig,
|
|
167
|
+
readDisplaySkinConfig,
|
|
168
|
+
setDisplaySkinConfig,
|
|
169
|
+
writeConfig,
|
|
170
|
+
};
|
package/src/lib/files.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
function listFiles(root) {
|
|
6
|
+
const out = [];
|
|
7
|
+
if (!fs.existsSync(root)) return out;
|
|
8
|
+
for (const name of fs.readdirSync(root)) {
|
|
9
|
+
if (name === "__pycache__" || name.endsWith(".pyc")) continue;
|
|
10
|
+
const full = path.join(root, name);
|
|
11
|
+
const stat = fs.statSync(full);
|
|
12
|
+
if (stat.isDirectory()) {
|
|
13
|
+
out.push(...listFiles(full));
|
|
14
|
+
} else if (stat.isFile()) {
|
|
15
|
+
out.push(full);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sha256(file) {
|
|
22
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function copyTree(src, dest) {
|
|
26
|
+
const copied = [];
|
|
27
|
+
for (const file of listFiles(src)) {
|
|
28
|
+
const relative = path.relative(src, file);
|
|
29
|
+
const target = path.join(dest, relative);
|
|
30
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
31
|
+
fs.copyFileSync(file, target);
|
|
32
|
+
copied.push({ path: relative, sha256: sha256(target) });
|
|
33
|
+
}
|
|
34
|
+
return copied;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Atomic write: write to a .tmp sibling, fsync, then rename into place.
|
|
38
|
+
// fsync failure is non-fatal (degrades gracefully — still renames).
|
|
39
|
+
function writeFileAtomic(filePath, data, encoding) {
|
|
40
|
+
const tmp = `${filePath}.tmp`;
|
|
41
|
+
const enc = encoding || "utf8";
|
|
42
|
+
const fd = fs.openSync(tmp, "w");
|
|
43
|
+
try {
|
|
44
|
+
fs.writeSync(fd, data, null, enc);
|
|
45
|
+
try {
|
|
46
|
+
fs.fsyncSync(fd);
|
|
47
|
+
} catch {
|
|
48
|
+
// fsync not supported on all platforms/fs; degrade gracefully
|
|
49
|
+
}
|
|
50
|
+
} finally {
|
|
51
|
+
fs.closeSync(fd);
|
|
52
|
+
}
|
|
53
|
+
fs.renameSync(tmp, filePath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function removeEmptyDirs(dir, stopAt) {
|
|
57
|
+
let current = dir;
|
|
58
|
+
while (current && current !== stopAt && current.startsWith(stopAt) && fs.existsSync(current)) {
|
|
59
|
+
if (fs.readdirSync(current).length > 0) break;
|
|
60
|
+
fs.rmdirSync(current);
|
|
61
|
+
current = path.dirname(current);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { copyTree, listFiles, removeEmptyDirs, sha256, writeFileAtomic };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
class LitHermesError extends Error {
|
|
6
|
+
constructor(message, exitCode = 1) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.exitCode = exitCode;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function expandHome(value) {
|
|
13
|
+
if (!value) return value;
|
|
14
|
+
if (value === "~") return os.homedir();
|
|
15
|
+
if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function defaultHermesHome(flags = {}) {
|
|
20
|
+
return path.resolve(expandHome(flags["hermes-home"] || process.env.HERMES_HOME || "~/.hermes"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function defaultHermesRepo(flags = {}, hermesHome = defaultHermesHome(flags)) {
|
|
24
|
+
if (flags["hermes-repo"]) return path.resolve(expandHome(flags["hermes-repo"]));
|
|
25
|
+
const candidate = path.join(hermesHome, "hermes-agent");
|
|
26
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureHermesHome(flags = {}, { forInstall = false } = {}) {
|
|
30
|
+
const hermesHome = defaultHermesHome(flags);
|
|
31
|
+
if (!fs.existsSync(hermesHome)) {
|
|
32
|
+
if (forInstall) {
|
|
33
|
+
fs.mkdirSync(hermesHome, { recursive: true });
|
|
34
|
+
} else {
|
|
35
|
+
throw new LitHermesError(`Hermes installation not found at ${hermesHome}. Pass --hermes-home PATH to select it.`, 2);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
hermesHome,
|
|
40
|
+
hermesRepo: defaultHermesRepo(flags, hermesHome),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
LitHermesError,
|
|
46
|
+
defaultHermesHome,
|
|
47
|
+
defaultHermesRepo,
|
|
48
|
+
ensureHermesHome,
|
|
49
|
+
expandHome,
|
|
50
|
+
};
|
package/src/lib/hud.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const os = require("node:os");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const readline = require("node:readline");
|
|
4
|
+
const skins = require("./skins");
|
|
5
|
+
const {
|
|
6
|
+
readConfig,
|
|
7
|
+
writeConfig,
|
|
8
|
+
setDisplaySkinConfig,
|
|
9
|
+
clearDisplaySkinConfig,
|
|
10
|
+
readDisplaySkinConfig,
|
|
11
|
+
} = require("./config");
|
|
12
|
+
|
|
13
|
+
function resolveHome(flags = {}) {
|
|
14
|
+
const fromFlag = flags["hermes-home"];
|
|
15
|
+
if (typeof fromFlag === "string" && fromFlag.trim()) return path.resolve(fromFlag);
|
|
16
|
+
const env = (process.env.HERMES_HOME || "").trim();
|
|
17
|
+
if (env) return path.resolve(env);
|
|
18
|
+
return path.join(os.homedir(), ".hermes");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ANSI helpers — all output goes through these so no-color paths stay clean.
|
|
22
|
+
const BOLD = (s, on) => on ? `\x1b[1m${s}\x1b[22m` : s;
|
|
23
|
+
const DIM = (s, on) => on ? `\x1b[2m${s}\x1b[22m` : s;
|
|
24
|
+
const FG256 = (code, s, on) => on ? `\x1b[38;5;${code}m${s}\x1b[39m` : s;
|
|
25
|
+
|
|
26
|
+
function listHud({ color = false, hermesHome = null } = {}) {
|
|
27
|
+
const current = hermesHome ? readDisplaySkinConfig(readConfig(hermesHome)) : null;
|
|
28
|
+
|
|
29
|
+
// Header — fire brand mark + neon label when color is on
|
|
30
|
+
const brand = color ? "🔥" : "[fire]";
|
|
31
|
+
const header = color
|
|
32
|
+
? `${brand} ${BOLD("LitHermes HUD", true)} ${DIM("— neon accents", true)}`
|
|
33
|
+
: `${brand} LitHermes HUD — neon accents`;
|
|
34
|
+
const lines = [header, ""];
|
|
35
|
+
|
|
36
|
+
skins.HUD_ACCENTS.forEach((a, i) => {
|
|
37
|
+
const sw = skins.swatch(a.hex, { color, code: a.code });
|
|
38
|
+
const isActive = current === skins.skinName(a.accent);
|
|
39
|
+
|
|
40
|
+
// Active indicator: 🔥 in color, * in plain
|
|
41
|
+
const activeMark = isActive ? (color ? " 🔥" : " *") : "";
|
|
42
|
+
|
|
43
|
+
// Mood label — dimmed in color mode for a secondary-info feel
|
|
44
|
+
const moodText = a.mood ? ` ${DIM(a.mood, color)}` : "";
|
|
45
|
+
|
|
46
|
+
// Accent name — bold + accent color when active, plain bold otherwise
|
|
47
|
+
const accentLabel = color
|
|
48
|
+
? (isActive
|
|
49
|
+
? BOLD(FG256(a.code, a.accent.padEnd(9), true), true)
|
|
50
|
+
: BOLD(a.accent.padEnd(9), true))
|
|
51
|
+
: a.accent.padEnd(9);
|
|
52
|
+
|
|
53
|
+
const hexLabel = color ? DIM(a.hex, true) : a.hex;
|
|
54
|
+
|
|
55
|
+
lines.push(
|
|
56
|
+
` ${String(i + 1).padStart(2)}. ${sw} ${accentLabel} ${hexLabel}${moodText}${activeMark}`
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
lines.push("");
|
|
61
|
+
const applyCmd = color ? BOLD("lithermes hud <accent>", true) : "lithermes hud <accent>";
|
|
62
|
+
const clearCmd = color ? BOLD("lithermes hud off", true) : "lithermes hud off";
|
|
63
|
+
const skinCmd = color ? BOLD("/skin lithermes-<accent>", true) : "/skin lithermes-<accent>";
|
|
64
|
+
lines.push(`Apply: ${applyCmd} (e.g. lithermes hud rose, or hud 1)`);
|
|
65
|
+
lines.push(`Clear: ${clearCmd}`);
|
|
66
|
+
lines.push(`In Hermes: ${skinCmd} (or set display.skin in config.yaml)`);
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function applyHud(token, flags = {}) {
|
|
71
|
+
const entry = skins.findAccent(token);
|
|
72
|
+
if (!entry) {
|
|
73
|
+
const names = skins.HUD_ACCENTS.map((a) => a.accent).join(", ");
|
|
74
|
+
const err = new Error(`unknown accent '${token}'. choose one of: ${names}`);
|
|
75
|
+
err.exitCode = 4;
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
const home = resolveHome(flags);
|
|
79
|
+
const written = skins.installSkins(home);
|
|
80
|
+
const name = skins.skinName(entry.accent);
|
|
81
|
+
const before = readConfig(home);
|
|
82
|
+
const after = setDisplaySkinConfig(before, name);
|
|
83
|
+
if (after !== before) writeConfig(home, after);
|
|
84
|
+
return [
|
|
85
|
+
`LitHermes HUD set to ${entry.label} (${name}).`,
|
|
86
|
+
`Installed ${written.length} skins -> ${skins.skinsDir(home)}`,
|
|
87
|
+
`config: display.skin: ${name}`,
|
|
88
|
+
`Restart Hermes, or run \`/skin ${name}\` in a session, to apply.`,
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function clearHud(flags = {}) {
|
|
93
|
+
const home = resolveHome(flags);
|
|
94
|
+
const before = readConfig(home);
|
|
95
|
+
const after = clearDisplaySkinConfig(before);
|
|
96
|
+
if (after !== before) writeConfig(home, after);
|
|
97
|
+
return "LitHermes HUD cleared (display.skin removed). Restart Hermes to revert to the default skin.";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Interactive accent picker used during `lithermes install` on a TTY.
|
|
101
|
+
// Returns the chosen accent entry, or null to skip (empty / unknown answer).
|
|
102
|
+
function promptAccent({ input = process.stdin, output = process.stdout, color = false } = {}) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
output.write("Choose a Hermes HUD accent:\n");
|
|
105
|
+
skins.HUD_ACCENTS.forEach((a, i) => {
|
|
106
|
+
const sw = skins.swatch(a.hex, { color, code: a.code });
|
|
107
|
+
output.write(` ${String(i + 1).padStart(2)}. ${sw} ${a.accent}\n`);
|
|
108
|
+
});
|
|
109
|
+
const rl = readline.createInterface({ input, output });
|
|
110
|
+
// Resolve to null if the stream closes without an answer (e.g. piped input
|
|
111
|
+
// that ends without a newline); the question callback resolves first when an
|
|
112
|
+
// answer arrives, so this is a no-op in the normal path.
|
|
113
|
+
rl.on("close", () => resolve(null));
|
|
114
|
+
rl.question("Pick 1-10 or name (Enter to skip): ", (answer) => {
|
|
115
|
+
resolve(skins.findAccent(answer)); // resolve BEFORE close so the answer wins
|
|
116
|
+
rl.close();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { resolveHome, listHud, applyHud, clearHud, promptAccent };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { disableLitHermesConfig, enableLitHermesConfig, readConfig, writeConfig } = require("./config");
|
|
4
|
+
const { copyTree, listFiles, removeEmptyDirs, sha256, writeFileAtomic } = require("./files");
|
|
5
|
+
const { ensureHermesHome, LitHermesError } = require("./hermesDiscovery");
|
|
6
|
+
const { patchInstalledHermes, rollbackPatches } = require("./patch");
|
|
7
|
+
const { installSkins } = require("./skins");
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(__dirname, "..", "..");
|
|
10
|
+
const assetRoot = path.join(packageRoot, "assets", "lithermes-plugin");
|
|
11
|
+
|
|
12
|
+
function manifestPath(hermesHome) {
|
|
13
|
+
return path.join(hermesHome, "lithermes", "install-manifest.json");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function pluginDest(hermesHome) {
|
|
17
|
+
return path.join(hermesHome, "plugins", "lithermes");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function withLock(hermesHome, fn) {
|
|
21
|
+
const lock = path.join(hermesHome, "lithermes", "install.lock");
|
|
22
|
+
fs.mkdirSync(path.dirname(lock), { recursive: true });
|
|
23
|
+
let fd;
|
|
24
|
+
try {
|
|
25
|
+
fd = fs.openSync(lock, "wx");
|
|
26
|
+
fs.writeFileSync(fd, String(process.pid));
|
|
27
|
+
} catch {
|
|
28
|
+
throw new LitHermesError(`LitHermes install lock already exists at ${lock}`, 3);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return fn();
|
|
32
|
+
} finally {
|
|
33
|
+
if (fd) fs.closeSync(fd);
|
|
34
|
+
if (fs.existsSync(lock)) fs.unlinkSync(lock);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadManifest(hermesHome) {
|
|
39
|
+
const file = manifestPath(hermesHome);
|
|
40
|
+
if (!fs.existsSync(file)) return null;
|
|
41
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function installedMatchesManifest(hermesHome, manifest) {
|
|
45
|
+
if (!manifest) return false;
|
|
46
|
+
const dest = pluginDest(hermesHome);
|
|
47
|
+
return manifest.files.every((entry) => {
|
|
48
|
+
const file = path.join(dest, entry.path);
|
|
49
|
+
return fs.existsSync(file) && sha256(file) === entry.sha256;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function installLitHermes(flags = {}) {
|
|
54
|
+
const { hermesHome, hermesRepo } = ensureHermesHome(flags, { forInstall: true });
|
|
55
|
+
const dest = pluginDest(hermesHome);
|
|
56
|
+
const dryRun = Boolean(flags["dry-run"]);
|
|
57
|
+
const onProgress = typeof flags.onProgress === "function" ? flags.onProgress : () => {};
|
|
58
|
+
if (dryRun) {
|
|
59
|
+
return {
|
|
60
|
+
message: [
|
|
61
|
+
"DRY RUN: would install LitHermes",
|
|
62
|
+
`plugin: ${dest}`,
|
|
63
|
+
`config: ${path.join(hermesHome, "config.yaml")}`,
|
|
64
|
+
`manifest: ${manifestPath(hermesHome)}`,
|
|
65
|
+
].join("\n"),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (!flags.yes) {
|
|
69
|
+
throw new LitHermesError("Refusing to mutate Hermes config without --yes. Re-run with --dry-run to inspect changes.", 4);
|
|
70
|
+
}
|
|
71
|
+
onProgress("Preparing Hermes config");
|
|
72
|
+
return withLock(hermesHome, () => {
|
|
73
|
+
onProgress("Inspecting existing plugin");
|
|
74
|
+
const existingManifest = loadManifest(hermesHome);
|
|
75
|
+
const wasUpToDate = fs.existsSync(dest) && installedMatchesManifest(hermesHome, existingManifest);
|
|
76
|
+
if (fs.existsSync(dest) && !installedMatchesManifest(hermesHome, existingManifest) && !flags.force) {
|
|
77
|
+
throw new LitHermesError(`Existing LitHermes plugin at ${dest} is not manifest-owned. Use --force after backing it up.`, 5);
|
|
78
|
+
}
|
|
79
|
+
onProgress("Copying LitHermes payload");
|
|
80
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
81
|
+
const files = copyTree(assetRoot, dest);
|
|
82
|
+
onProgress("Writing Hermes config");
|
|
83
|
+
const beforeConfig = readConfig(hermesHome);
|
|
84
|
+
const afterConfig = enableLitHermesConfig(beforeConfig);
|
|
85
|
+
writeConfig(hermesHome, afterConfig);
|
|
86
|
+
onProgress("Installing HUD skins");
|
|
87
|
+
let skinFiles = [];
|
|
88
|
+
try {
|
|
89
|
+
skinFiles = installSkins(hermesHome);
|
|
90
|
+
} catch {
|
|
91
|
+
// HUD skins are optional polish; never fail the install over them.
|
|
92
|
+
}
|
|
93
|
+
onProgress("Recording install manifest");
|
|
94
|
+
const manifest = {
|
|
95
|
+
version: require(path.join(packageRoot, "package.json")).version,
|
|
96
|
+
installedAt: new Date().toISOString(),
|
|
97
|
+
pluginPath: dest,
|
|
98
|
+
configPath: path.join(hermesHome, "config.yaml"),
|
|
99
|
+
files,
|
|
100
|
+
configAddedLitHermes: beforeConfig !== afterConfig,
|
|
101
|
+
};
|
|
102
|
+
fs.mkdirSync(path.dirname(manifestPath(hermesHome)), { recursive: true });
|
|
103
|
+
writeFileAtomic(manifestPath(hermesHome), JSON.stringify(manifest, null, 2), "utf8");
|
|
104
|
+
let patchResult = null;
|
|
105
|
+
let patchWarning = null;
|
|
106
|
+
const shouldPatch = Boolean(flags["patch-installed-hermes"] || (!flags["no-patch-installed-hermes"] && hermesRepo));
|
|
107
|
+
if (shouldPatch) {
|
|
108
|
+
onProgress("Checking Hermes compatibility patches");
|
|
109
|
+
try {
|
|
110
|
+
patchResult = patchInstalledHermes({ hermesHome, hermesRepo, force: flags.force });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (flags["patch-installed-hermes"]) throw error;
|
|
113
|
+
patchWarning = error.message;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const lines = [
|
|
117
|
+
wasUpToDate ? "LitHermes already up to date" : "Installed LitHermes",
|
|
118
|
+
`plugin: ${dest}`,
|
|
119
|
+
];
|
|
120
|
+
if (patchResult) lines.push(`patches: ${patchResult.changed.length ? patchResult.changed.join(", ") : "none needed"}`);
|
|
121
|
+
if (patchWarning) lines.push(`patches: skipped (${patchWarning})`);
|
|
122
|
+
if (skinFiles.length) lines.push(`HUD skins: ${skinFiles.length} accents installed — pick one with \`npx lithermes hud <accent>\``);
|
|
123
|
+
lines.push("Restart any running Hermes gateway to load new plugins.");
|
|
124
|
+
return { message: lines.join("\n") };
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function uninstallLitHermes(flags = {}) {
|
|
129
|
+
const { hermesHome } = ensureHermesHome(flags);
|
|
130
|
+
if (!flags.yes) {
|
|
131
|
+
throw new LitHermesError("Refusing to uninstall without --yes.", 4);
|
|
132
|
+
}
|
|
133
|
+
return withLock(hermesHome, () => {
|
|
134
|
+
const rollback = flags["rollback-patches"] ? rollbackPatches({ hermesHome }) : null;
|
|
135
|
+
const manifest = loadManifest(hermesHome);
|
|
136
|
+
if (!manifest) return { message: rollback ? `LitHermes is not installed by this installer.\n${rollback.message}` : "LitHermes is not installed by this installer." };
|
|
137
|
+
const dest = pluginDest(hermesHome);
|
|
138
|
+
for (const entry of [...manifest.files].reverse()) {
|
|
139
|
+
const file = path.join(dest, entry.path);
|
|
140
|
+
if (fs.existsSync(file) && sha256(file) === entry.sha256) {
|
|
141
|
+
fs.unlinkSync(file);
|
|
142
|
+
removeEmptyDirs(path.dirname(file), dest);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (fs.existsSync(dest) && listFiles(dest).length === 0) fs.rmSync(dest, { recursive: true, force: true });
|
|
146
|
+
writeConfig(hermesHome, disableLitHermesConfig(readConfig(hermesHome)));
|
|
147
|
+
fs.unlinkSync(manifestPath(hermesHome));
|
|
148
|
+
return { message: rollback ? `Uninstalled LitHermes\n${rollback.message}` : "Uninstalled LitHermes" };
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
assetRoot,
|
|
154
|
+
installLitHermes,
|
|
155
|
+
loadManifest,
|
|
156
|
+
manifestPath,
|
|
157
|
+
pluginDest,
|
|
158
|
+
uninstallLitHermes,
|
|
159
|
+
};
|