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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/README_Ko-KR.md +245 -0
  4. package/assets/lithermes-plugin/NOTICE.md +37 -0
  5. package/assets/lithermes-plugin/README.md +40 -0
  6. package/assets/lithermes-plugin/__init__.py +179 -0
  7. package/assets/lithermes-plugin/core.py +853 -0
  8. package/assets/lithermes-plugin/litgoal/__init__.py +10 -0
  9. package/assets/lithermes-plugin/litgoal/cli.py +133 -0
  10. package/assets/lithermes-plugin/litgoal/hook.py +48 -0
  11. package/assets/lithermes-plugin/litgoal/model.py +171 -0
  12. package/assets/lithermes-plugin/litgoal/runtime.py +273 -0
  13. package/assets/lithermes-plugin/litgoal/store.py +93 -0
  14. package/assets/lithermes-plugin/litgoal/tools.py +228 -0
  15. package/assets/lithermes-plugin/payload-version.json +471 -0
  16. package/assets/lithermes-plugin/plugin.yaml +9 -0
  17. package/assets/lithermes-plugin/skills/ai-slop-remover/SKILL.md +142 -0
  18. package/assets/lithermes-plugin/skills/comment-checker/SKILL.md +50 -0
  19. package/assets/lithermes-plugin/skills/debugging/SKILL.md +116 -0
  20. package/assets/lithermes-plugin/skills/debugging/references/methodology/00-setup.md +108 -0
  21. package/assets/lithermes-plugin/skills/debugging/references/methodology/02-investigate.md +121 -0
  22. package/assets/lithermes-plugin/skills/debugging/references/methodology/04-oracle-triple.md +136 -0
  23. package/assets/lithermes-plugin/skills/debugging/references/methodology/05-escalate.md +69 -0
  24. package/assets/lithermes-plugin/skills/debugging/references/methodology/06-fix.md +116 -0
  25. package/assets/lithermes-plugin/skills/debugging/references/methodology/08-qa.md +94 -0
  26. package/assets/lithermes-plugin/skills/debugging/references/methodology/09-cleanup.md +164 -0
  27. package/assets/lithermes-plugin/skills/debugging/references/methodology/partial-runtime-evidence.md +229 -0
  28. package/assets/lithermes-plugin/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
  29. package/assets/lithermes-plugin/skills/debugging/references/runtimes/go.md +252 -0
  30. package/assets/lithermes-plugin/skills/debugging/references/runtimes/native-binary.md +484 -0
  31. package/assets/lithermes-plugin/skills/debugging/references/runtimes/node.md +260 -0
  32. package/assets/lithermes-plugin/skills/debugging/references/runtimes/python.md +248 -0
  33. package/assets/lithermes-plugin/skills/debugging/references/runtimes/rust.md +234 -0
  34. package/assets/lithermes-plugin/skills/debugging/references/tools/ghidra.md +212 -0
  35. package/assets/lithermes-plugin/skills/debugging/references/tools/playwright-cli.md +194 -0
  36. package/assets/lithermes-plugin/skills/debugging/references/tools/pwndbg.md +263 -0
  37. package/assets/lithermes-plugin/skills/debugging/references/tools/pwntools.md +265 -0
  38. package/assets/lithermes-plugin/skills/frontend-ui-ux/SKILL.md +77 -0
  39. package/assets/lithermes-plugin/skills/lit-plan/SKILL.md +374 -0
  40. package/assets/lithermes-plugin/skills/litgoal/.gitkeep +0 -0
  41. package/assets/lithermes-plugin/skills/litgoal/SKILL.md +207 -0
  42. package/assets/lithermes-plugin/skills/litwork/SKILL.md +262 -0
  43. package/assets/lithermes-plugin/skills/lsp/SKILL.md +53 -0
  44. package/assets/lithermes-plugin/skills/programming/SKILL.md +463 -0
  45. package/assets/lithermes-plugin/skills/programming/references/go/README.md +90 -0
  46. package/assets/lithermes-plugin/skills/programming/references/go/backend-stack.md +641 -0
  47. package/assets/lithermes-plugin/skills/programming/references/go/bootstrap.md +328 -0
  48. package/assets/lithermes-plugin/skills/programming/references/go/bubbletea-v2.md +360 -0
  49. package/assets/lithermes-plugin/skills/programming/references/go/cobra-stack.md +468 -0
  50. package/assets/lithermes-plugin/skills/programming/references/go/concurrency.md +362 -0
  51. package/assets/lithermes-plugin/skills/programming/references/go/data-modeling.md +329 -0
  52. package/assets/lithermes-plugin/skills/programming/references/go/error-handling.md +359 -0
  53. package/assets/lithermes-plugin/skills/programming/references/go/golangci-strict.md +236 -0
  54. package/assets/lithermes-plugin/skills/programming/references/go/grpc-connect.md +375 -0
  55. package/assets/lithermes-plugin/skills/programming/references/go/libraries.md +337 -0
  56. package/assets/lithermes-plugin/skills/programming/references/go/one-liners.md +202 -0
  57. package/assets/lithermes-plugin/skills/programming/references/go/sqlc-pgx.md +471 -0
  58. package/assets/lithermes-plugin/skills/programming/references/go/testing.md +467 -0
  59. package/assets/lithermes-plugin/skills/programming/references/go/type-patterns.md +298 -0
  60. package/assets/lithermes-plugin/skills/programming/references/python/README.md +314 -0
  61. package/assets/lithermes-plugin/skills/programming/references/python/async-anyio.md +442 -0
  62. package/assets/lithermes-plugin/skills/programming/references/python/data-modeling.md +233 -0
  63. package/assets/lithermes-plugin/skills/programming/references/python/data-processing.md +133 -0
  64. package/assets/lithermes-plugin/skills/programming/references/python/error-handling.md +218 -0
  65. package/assets/lithermes-plugin/skills/programming/references/python/fastapi-stack.md +316 -0
  66. package/assets/lithermes-plugin/skills/programming/references/python/httpx2-optimization.md +360 -0
  67. package/assets/lithermes-plugin/skills/programming/references/python/libraries.md +307 -0
  68. package/assets/lithermes-plugin/skills/programming/references/python/one-liners.md +268 -0
  69. package/assets/lithermes-plugin/skills/programming/references/python/orjson-stack.md +378 -0
  70. package/assets/lithermes-plugin/skills/programming/references/python/pydantic-ai.md +285 -0
  71. package/assets/lithermes-plugin/skills/programming/references/python/pyproject-strict.md +232 -0
  72. package/assets/lithermes-plugin/skills/programming/references/python/textual-tui.md +201 -0
  73. package/assets/lithermes-plugin/skills/programming/references/python/type-patterns.md +176 -0
  74. package/assets/lithermes-plugin/skills/programming/references/rust/README.md +317 -0
  75. package/assets/lithermes-plugin/skills/programming/references/rust/async-tokio.md +299 -0
  76. package/assets/lithermes-plugin/skills/programming/references/rust/axum-stack.md +467 -0
  77. package/assets/lithermes-plugin/skills/programming/references/rust/cargo-strict.md +317 -0
  78. package/assets/lithermes-plugin/skills/programming/references/rust/clap-stack.md +409 -0
  79. package/assets/lithermes-plugin/skills/programming/references/rust/concurrency.md +375 -0
  80. package/assets/lithermes-plugin/skills/programming/references/rust/libraries.md +439 -0
  81. package/assets/lithermes-plugin/skills/programming/references/rust/one-liners.md +291 -0
  82. package/assets/lithermes-plugin/skills/programming/references/rust/proptest-insta.md +429 -0
  83. package/assets/lithermes-plugin/skills/programming/references/rust/type-state.md +354 -0
  84. package/assets/lithermes-plugin/skills/programming/references/rust/unsafe-discipline.md +250 -0
  85. package/assets/lithermes-plugin/skills/programming/references/rust/zero-cost-safety.md +527 -0
  86. package/assets/lithermes-plugin/skills/programming/references/rust-ub/README.md +289 -0
  87. package/assets/lithermes-plugin/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
  88. package/assets/lithermes-plugin/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
  89. package/assets/lithermes-plugin/skills/programming/references/typescript/README.md +195 -0
  90. package/assets/lithermes-plugin/skills/programming/references/typescript/backend-hono.md +672 -0
  91. package/assets/lithermes-plugin/skills/programming/references/typescript/bootstrap.md +199 -0
  92. package/assets/lithermes-plugin/skills/programming/references/typescript/data-modeling.md +202 -0
  93. package/assets/lithermes-plugin/skills/programming/references/typescript/error-handling.md +169 -0
  94. package/assets/lithermes-plugin/skills/programming/references/typescript/tsconfig-strict.md +152 -0
  95. package/assets/lithermes-plugin/skills/programming/references/typescript/type-patterns.md +196 -0
  96. package/assets/lithermes-plugin/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
  97. package/assets/lithermes-plugin/skills/programming/scripts/go/new-project.py +138 -0
  98. package/assets/lithermes-plugin/skills/programming/scripts/go/templates/.editorconfig +13 -0
  99. package/assets/lithermes-plugin/skills/programming/scripts/go/templates/.golangci.yml +95 -0
  100. package/assets/lithermes-plugin/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
  101. package/assets/lithermes-plugin/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
  102. package/assets/lithermes-plugin/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
  103. package/assets/lithermes-plugin/skills/programming/scripts/go/templates/ci.yml +37 -0
  104. package/assets/lithermes-plugin/skills/programming/scripts/go/templates/config.go +24 -0
  105. package/assets/lithermes-plugin/skills/programming/scripts/go/templates/gitignore +15 -0
  106. package/assets/lithermes-plugin/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
  107. package/assets/lithermes-plugin/skills/programming/scripts/go/templates/run.go +15 -0
  108. package/assets/lithermes-plugin/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
  109. package/assets/lithermes-plugin/skills/programming/scripts/python/new-project.py +172 -0
  110. package/assets/lithermes-plugin/skills/programming/scripts/python/new-script.py +116 -0
  111. package/assets/lithermes-plugin/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
  112. package/assets/lithermes-plugin/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
  113. package/assets/lithermes-plugin/skills/programming/scripts/rust/new-project.py +175 -0
  114. package/assets/lithermes-plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
  115. package/assets/lithermes-plugin/skills/programming/scripts/typescript/new-project.ts +177 -0
  116. package/assets/lithermes-plugin/skills/refactor/SKILL.md +770 -0
  117. package/assets/lithermes-plugin/skills/remove-ai-slops/SKILL.md +335 -0
  118. package/assets/lithermes-plugin/skills/review-work/SKILL.md +562 -0
  119. package/assets/lithermes-plugin/skills/rules/SKILL.md +41 -0
  120. package/assets/lithermes-plugin/skills/start-work/SKILL.md +332 -0
  121. package/bin/lithermes.js +8 -0
  122. package/cover.png +0 -0
  123. package/package.json +39 -0
  124. package/src/cli.js +129 -0
  125. package/src/lib/check.js +94 -0
  126. package/src/lib/config.js +170 -0
  127. package/src/lib/files.js +65 -0
  128. package/src/lib/hermesDiscovery.js +50 -0
  129. package/src/lib/hud.js +121 -0
  130. package/src/lib/install.js +159 -0
  131. package/src/lib/patch.js +153 -0
  132. package/src/lib/skins.js +113 -0
  133. package/src/lib/spinner.js +104 -0
@@ -0,0 +1,153 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { sha256, writeFileAtomic } = require("./files");
4
+ const { LitHermesError } = require("./hermesDiscovery");
5
+
6
+ function patchManifestPath(hermesHome) {
7
+ return path.join(hermesHome, "lithermes", "patch-manifest.json");
8
+ }
9
+
10
+ function patchTarget(repo, relative, marker, additions) {
11
+ const file = path.join(repo, relative);
12
+ if (!fs.existsSync(file)) {
13
+ return null;
14
+ }
15
+ const source = fs.readFileSync(file, "utf8");
16
+ if (additions.every((text) => source.includes(text))) {
17
+ return null;
18
+ }
19
+ if (!source.includes(marker)) {
20
+ throw new LitHermesError(`Unsupported Hermes preimage for ${relative}; refusing to patch without a known marker.`, 8);
21
+ }
22
+ const backup = `${file}.lithermes.bak`;
23
+ fs.copyFileSync(file, backup);
24
+ const beforeHash = sha256(file);
25
+ const backupHash = sha256(backup);
26
+ const patched = `${source.replace(/\s*$/, "\n")}\n# LitHermes compatibility patch\n${additions.map((text) => `# ${text}`).join("\n")}\n`;
27
+ writeFileAtomic(file, patched, "utf8");
28
+ return {
29
+ file,
30
+ relative,
31
+ backup,
32
+ beforeHash,
33
+ backupHash,
34
+ patchedHash: sha256(file),
35
+ };
36
+ }
37
+
38
+ function patchCliPayloadDispatch(repo) {
39
+ const relative = "cli.py";
40
+ const file = path.join(repo, relative);
41
+ if (!fs.existsSync(file)) return null;
42
+ const source = fs.readFileSync(file, "utf8");
43
+ if (source.includes("_pending_input.put") && source.includes("agent_message") && source.includes("ast.literal_eval")) {
44
+ return null;
45
+ }
46
+
47
+ const marker = [
48
+ " result = resolve_plugin_command_result(",
49
+ " plugin_handler(user_args)",
50
+ " )",
51
+ " if result:",
52
+ " _cprint(str(result))",
53
+ ].join("\n");
54
+ if (!source.includes(marker)) {
55
+ throw new LitHermesError(`Unsupported Hermes preimage for ${relative}; plugin command dispatch block not recognized.`, 8);
56
+ }
57
+
58
+ const replacement = [
59
+ " result = resolve_plugin_command_result(",
60
+ " plugin_handler(user_args)",
61
+ " )",
62
+ " if isinstance(result, str) and \"agent_message\" in result:",
63
+ " try:",
64
+ " parsed = ast.literal_eval(result)",
65
+ " except (SyntaxError, ValueError):",
66
+ " parsed = None",
67
+ " if isinstance(parsed, dict):",
68
+ " result = parsed",
69
+ " if isinstance(result, dict) and result.get(\"agent_message\"):",
70
+ " display = result.get(\"display\") or result.get(\"message\")",
71
+ " if display:",
72
+ " _cprint(str(display))",
73
+ " if hasattr(self, '_pending_input'):",
74
+ " self._pending_input.put(str(result[\"agent_message\"]))",
75
+ " elif result:",
76
+ " _cprint(str(result))",
77
+ ].join("\n");
78
+
79
+ const backup = `${file}.lithermes.bak`;
80
+ fs.copyFileSync(file, backup);
81
+ const beforeHash = sha256(file);
82
+ const backupHash = sha256(backup);
83
+ writeFileAtomic(file, source.replace(marker, replacement), "utf8");
84
+ return {
85
+ file,
86
+ relative,
87
+ backup,
88
+ beforeHash,
89
+ backupHash,
90
+ patchedHash: sha256(file),
91
+ };
92
+ }
93
+
94
+ function patchInstalledHermes({ hermesHome, hermesRepo }) {
95
+ if (!hermesRepo || !fs.existsSync(hermesRepo)) {
96
+ throw new LitHermesError("Cannot patch Hermes because --hermes-repo was not found. Pass --hermes-repo PATH or run doctor first.", 8);
97
+ }
98
+ const changed = [];
99
+ const records = [];
100
+ const cli = patchCliPayloadDispatch(hermesRepo);
101
+ if (cli) {
102
+ changed.push("cli.py");
103
+ records.push(cli);
104
+ }
105
+ const gateway = patchTarget(hermesRepo, path.join("gateway", "run.py"), "lithermes-patch-target:gateway", [
106
+ 'command.replace("_", "-")',
107
+ "_plugin_agent_dispatch_payload",
108
+ ]);
109
+ if (gateway) {
110
+ changed.push("gateway/run.py");
111
+ records.push(gateway);
112
+ }
113
+ const plugins = patchTarget(hermesRepo, path.join("hermes_cli", "plugins.py"), "lithermes-patch-target:plugins", [
114
+ "auto_load",
115
+ "plugins.enabled",
116
+ ]);
117
+ if (plugins) {
118
+ changed.push("hermes_cli/plugins.py");
119
+ records.push(plugins);
120
+ }
121
+ fs.mkdirSync(path.dirname(patchManifestPath(hermesHome)), { recursive: true });
122
+ writeFileAtomic(patchManifestPath(hermesHome), JSON.stringify({ patchedAt: new Date().toISOString(), records }, null, 2), "utf8");
123
+ return { changed, records };
124
+ }
125
+
126
+ function rollbackPatches({ hermesHome }) {
127
+ const manifest = patchManifestPath(hermesHome);
128
+ if (!fs.existsSync(manifest)) {
129
+ return { message: "No LitHermes patches to roll back." };
130
+ }
131
+ const parsed = JSON.parse(fs.readFileSync(manifest, "utf8"));
132
+ const restored = [];
133
+ for (const record of [...parsed.records].reverse()) {
134
+ if (!fs.existsSync(record.file) || !fs.existsSync(record.backup)) {
135
+ throw new LitHermesError(`Cannot roll back ${record.relative}; patched file or backup is missing.`, 9);
136
+ }
137
+ if (sha256(record.file) !== record.patchedHash || sha256(record.backup) !== record.backupHash) {
138
+ throw new LitHermesError(`Cannot roll back ${record.relative}; file hash changed after patch.`, 9);
139
+ }
140
+ fs.copyFileSync(record.backup, record.file);
141
+ fs.unlinkSync(record.backup);
142
+ restored.push(record.relative);
143
+ }
144
+ fs.unlinkSync(manifest);
145
+ return { message: `Rolled back LitHermes patches: ${restored.join(", ") || "none"}` };
146
+ }
147
+
148
+ module.exports = {
149
+ patchCliPayloadDispatch,
150
+ patchInstalledHermes,
151
+ patchManifestPath,
152
+ rollbackPatches,
153
+ };
@@ -0,0 +1,113 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { writeFileAtomic } = require("./files");
4
+
5
+ // LitHermes HUD accents — 10 presets that drive Hermes' native skin engine.
6
+ // Each becomes ~/.hermes/skins/lithermes-<accent>.yaml; activated via
7
+ // `display.skin` in config.yaml or `/skin lithermes-<accent>` inside Hermes.
8
+ //
9
+ // A curated 10-accent palette (name + ANSI-256 code + order); `hex` is a
10
+ // neon-vivid color of the same hue (Hermes skins use hex, not ANSI codes).
11
+ // ⚠️ accent, code, and order are frozen by tests — do NOT reorder.
12
+ const HUD_ACCENTS = [
13
+ { accent: "cyan", code: 81, hex: "#00F5FF", label: "Cyan", mood: "neon ice" },
14
+ { accent: "blue", code: 39, hex: "#00B4FF", label: "Blue", mood: "arc flash" },
15
+ { accent: "teal", code: 49, hex: "#00FFB2", label: "Teal", mood: "plasma teal" },
16
+ { accent: "green", code: 118, hex: "#39FF14", label: "Green", mood: "laser green" },
17
+ { accent: "lavender", code: 177, hex: "#E040FB", label: "Lavender", mood: "neon violet" },
18
+ { accent: "rose", code: 198, hex: "#FF0066", label: "Rose", mood: "hot magenta" },
19
+ { accent: "gold", code: 220, hex: "#FFE600", label: "Gold", mood: "volt gold" },
20
+ { accent: "orange", code: 208, hex: "#FF6200", label: "Orange", mood: "ember glow" },
21
+ { accent: "slate", code: 75, hex: "#33AAFF", label: "Slate", mood: "cold steel" },
22
+ { accent: "gray", code: 231, hex: "#F0F0F0", label: "Gray", mood: "soft white" },
23
+ ];
24
+
25
+ function skinName(accent) {
26
+ return `lithermes-${accent}`;
27
+ }
28
+
29
+ function skinsDir(hermesHome) {
30
+ return path.join(hermesHome, "skins");
31
+ }
32
+
33
+ // Hermes skins inherit every unset field from the `default` skin, so an accent
34
+ // preset only needs to retint the primary accent color keys.
35
+ function renderSkinYaml(entry) {
36
+ const h = entry.hex;
37
+ return [
38
+ `name: ${skinName(entry.accent)}`,
39
+ `description: LitHermes ${entry.label} accent HUD`,
40
+ "colors:",
41
+ ` banner_title: "${h}"`,
42
+ ` banner_accent: "${h}"`,
43
+ ` banner_border: "${h}"`,
44
+ ` ui_accent: "${h}"`,
45
+ ` ui_label: "${h}"`,
46
+ ` input_rule: "${h}"`,
47
+ ` response_border: "${h}"`,
48
+ ` status_bar_strong: "${h}"`,
49
+ ` session_label: "${h}"`,
50
+ "",
51
+ ].join("\n");
52
+ }
53
+
54
+ function findAccent(token) {
55
+ const raw = String(token == null ? "" : token).trim().toLowerCase();
56
+ if (!raw) return null;
57
+ const bare = raw.startsWith("lithermes-") ? raw.slice("lithermes-".length) : raw;
58
+ const byName = HUD_ACCENTS.find((a) => a.accent === bare);
59
+ if (byName) return byName;
60
+ if (/^\d+$/.test(raw)) {
61
+ const idx = Number(raw) - 1;
62
+ if (idx >= 0 && idx < HUD_ACCENTS.length) return HUD_ACCENTS[idx];
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function installSkins(hermesHome) {
68
+ const dir = skinsDir(hermesHome);
69
+ fs.mkdirSync(dir, { recursive: true });
70
+ const written = [];
71
+ for (const entry of HUD_ACCENTS) {
72
+ const file = path.join(dir, `${skinName(entry.accent)}.yaml`);
73
+ writeFileAtomic(file, renderSkinYaml(entry), "utf8");
74
+ written.push(file);
75
+ }
76
+ return written;
77
+ }
78
+
79
+ function hexToRgb(hex) {
80
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(String(hex));
81
+ if (!m) return null;
82
+ return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)];
83
+ }
84
+
85
+ function supportsColor({ stream = process.stdout, env = process.env } = {}) {
86
+ if (env.NO_COLOR) return false;
87
+ if (env.TERM === "dumb") return false;
88
+ return Boolean(stream && stream.isTTY);
89
+ }
90
+
91
+ // Accent swatch (●●) used to preview an accent in the picker list. Prefers the
92
+ // exact ANSI-256 `code` in bold+bright for a neon-glow feel; falls back to
93
+ // 24-bit truecolor from hex. No-color path returns a plain asterisk.
94
+ function swatch(hex, { color = true, code = null } = {}) {
95
+ if (!color) return "*";
96
+ // Bold (\x1b[1m) + ANSI-256 fg + double dot for width/glow feel + reset
97
+ if (code != null) return `\x1b[1m\x1b[38;5;${code}m●●\x1b[0m`;
98
+ const rgb = hexToRgb(hex);
99
+ if (!rgb) return "*";
100
+ return `\x1b[1m\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m●●\x1b[0m`;
101
+ }
102
+
103
+ module.exports = {
104
+ HUD_ACCENTS,
105
+ skinName,
106
+ skinsDir,
107
+ renderSkinYaml,
108
+ findAccent,
109
+ installSkins,
110
+ hexToRgb,
111
+ supportsColor,
112
+ swatch,
113
+ };
@@ -0,0 +1,104 @@
1
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2
+
3
+ function shouldUseSpinner({ stream = process.stderr, env = process.env, flags = {} } = {}) {
4
+ if (flags["dry-run"] || flags["no-spinner"]) return false;
5
+ if (flags.spinner) return true;
6
+ if (env.LITHERMES_FORCE_SPINNER === "1") return true;
7
+ if (env.CI || env.NO_COLOR || env.TERM === "dumb") return false;
8
+ return Boolean(stream && stream.isTTY);
9
+ }
10
+
11
+ function useColor({ env = process.env } = {}) {
12
+ if (env.NO_COLOR) return false;
13
+ if (env.TERM === "dumb") return false;
14
+ return true;
15
+ }
16
+
17
+ // Minimal ANSI palette. Colours are stripped when NO_COLOR/TERM=dumb is set.
18
+ function makePalette(enabled) {
19
+ const wrap = (open, close) => (s) => (enabled ? `\x1b[${open}m${s}\x1b[${close}m` : s);
20
+ return {
21
+ bold: wrap(1, 22),
22
+ dim: wrap(2, 22),
23
+ green: wrap(32, 39),
24
+ red: wrap(31, 39),
25
+ cyan: wrap(36, 39),
26
+ };
27
+ }
28
+
29
+ // Step-aware install renderer. Each onProgress phase becomes a live spinner line
30
+ // that is finalized in place with its own ✓ (or ✗ on failure), producing an
31
+ // ordered checklist. Preserves the legacy single-spinner tokens that existing
32
+ // tests pin: hidden/shown cursor, braille frames, header text, and completion text.
33
+ function createSpinner({ stream = process.stderr, text = "Installing LitHermes", env = process.env } = {}) {
34
+ const c = makePalette(useColor({ env }));
35
+ let frameIndex = 0;
36
+ let timer = null;
37
+ let active = false;
38
+ let currentLabel = null;
39
+ let stepStart = 0;
40
+
41
+ function elapsed() {
42
+ if (!stepStart) return "";
43
+ const secs = (Date.now() - stepStart) / 1000;
44
+ return c.dim(` (${secs.toFixed(1)}s)`);
45
+ }
46
+
47
+ function paintActive() {
48
+ const frame = c.cyan(frames[frameIndex % frames.length]);
49
+ frameIndex += 1;
50
+ stream.write(`\r${frame} ${currentLabel}${elapsed()}\x1b[K`);
51
+ }
52
+
53
+ function clearTimer() {
54
+ if (timer) clearInterval(timer);
55
+ timer = null;
56
+ }
57
+
58
+ // Finalize the active step line in place with a status glyph, then newline so
59
+ // it persists above the next step.
60
+ function finalizeCurrent(glyph) {
61
+ if (currentLabel === null) return;
62
+ clearTimer();
63
+ stream.write(`\r${glyph} ${currentLabel}${elapsed()}\x1b[K\n`);
64
+ currentLabel = null;
65
+ }
66
+
67
+ function startStep(label) {
68
+ currentLabel = label;
69
+ stepStart = Date.now();
70
+ paintActive();
71
+ timer = setInterval(paintActive, 80);
72
+ if (timer && typeof timer.unref === "function") timer.unref();
73
+ }
74
+
75
+ return {
76
+ start() {
77
+ if (active) return;
78
+ active = true;
79
+ stream.write("\x1b[?25l");
80
+ stream.write(`${c.bold(text)}\n`);
81
+ },
82
+ update(nextText) {
83
+ if (!active) return;
84
+ finalizeCurrent(c.green("✓"));
85
+ startStep(nextText);
86
+ },
87
+ succeed(message = "LitHermes install ready") {
88
+ if (!active) return;
89
+ finalizeCurrent(c.green("✓"));
90
+ clearTimer();
91
+ active = false;
92
+ stream.write(`${c.green("✓")} ${message}\n\x1b[?25h`);
93
+ },
94
+ fail(message = "LitHermes install failed") {
95
+ if (!active) return;
96
+ finalizeCurrent(c.red("✕"));
97
+ clearTimer();
98
+ active = false;
99
+ stream.write(`${c.red("✕")} ${message}\n\x1b[?25h`);
100
+ },
101
+ };
102
+ }
103
+
104
+ module.exports = { createSpinner, shouldUseSpinner };