@crystalplatforms/adm 0.2.3 → 0.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.
Files changed (89) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +154 -161
  3. package/bin/adm +18 -21
  4. package/package.json +9 -22
  5. package/scripts/completion/adm.bash +12 -55
  6. package/scripts/completion/adm.zsh +21 -58
  7. package/scripts/install.js +8 -8
  8. package/src/commands/config.js +66 -0
  9. package/src/commands/dotfiles.js +49 -32
  10. package/src/commands/feedback.js +12 -0
  11. package/src/commands/help.js +33 -0
  12. package/src/commands/uninstall.js +73 -54
  13. package/src/commands/version.js +7 -0
  14. package/src/commands.js +26 -0
  15. package/src/config/index.js +32 -32
  16. package/src/data/extensions.json +358 -358
  17. package/src/engines.js +38 -0
  18. package/src/install/executor.js +263 -0
  19. package/src/install/hints.js +94 -0
  20. package/src/install/npm-prefix.js +110 -0
  21. package/src/install/plan.js +75 -0
  22. package/src/install/report.js +46 -0
  23. package/src/install/run.js +88 -0
  24. package/src/install/shell-paths.js +129 -0
  25. package/src/legacy-cleanup.js +96 -0
  26. package/src/pkg/builders.js +220 -0
  27. package/src/pkg/managers.js +59 -0
  28. package/src/pkg/package-names.js +229 -0
  29. package/src/pkg/script-urls.js +100 -0
  30. package/src/platform/index.js +129 -0
  31. package/src/platform/os-release.js +27 -0
  32. package/src/platform/shell.js +111 -0
  33. package/src/reports.js +37 -0
  34. package/src/router.js +157 -0
  35. package/src/setup/catalog.js +13 -13
  36. package/src/setup/git-step.js +128 -0
  37. package/src/setup/loader.js +108 -0
  38. package/src/setup/prompts.js +259 -0
  39. package/src/setup/registry.js +48 -0
  40. package/src/setup/spinner.js +67 -0
  41. package/src/setup/wizard.js +319 -195
  42. package/src/upgrade/index.js +499 -0
  43. package/src/utils/closest-match.js +36 -0
  44. package/src/utils/dotfiles-sync.js +89 -89
  45. package/src/utils/file-conflict-handler.js +30 -30
  46. package/bin/adm-tui +0 -13
  47. package/src/ai/knowledge.js +0 -61
  48. package/src/commands/connect.js +0 -76
  49. package/src/commands/issue.js +0 -80
  50. package/src/commands/pr.js +0 -55
  51. package/src/commands/theme.js +0 -103
  52. package/src/commands/upgrade.js +0 -114
  53. package/src/installer/git-config.js +0 -34
  54. package/src/installer/node-installer.js +0 -50
  55. package/src/installer/package-manager-installer.js +0 -31
  56. package/src/installer/ssh-setup.js +0 -31
  57. package/src/installer/system-packages.js +0 -49
  58. package/src/installers/index.js +0 -66
  59. package/src/integrations/ai-backend.js +0 -60
  60. package/src/integrations/ai-providers/registry.js +0 -261
  61. package/src/integrations/github.js +0 -107
  62. package/src/integrations/gitlab.js +0 -121
  63. package/src/plugins/context.js +0 -64
  64. package/src/plugins/loader.js +0 -50
  65. package/src/plugins/runner.js +0 -25
  66. package/src/setup/installer.js +0 -192
  67. package/src/shell/detector.js +0 -10
  68. package/src/tui/app-state.js +0 -374
  69. package/src/tui/app.js +0 -442
  70. package/src/tui/autocomplete.js +0 -57
  71. package/src/tui/commands/registry.js +0 -823
  72. package/src/tui/components/SetupScreen.js +0 -292
  73. package/src/ui/animations.js +0 -24
  74. package/src/ui/spinner.js +0 -34
  75. package/src/ui/theme/cyberpunk.js +0 -14
  76. package/src/ui/theme/dark.js +0 -14
  77. package/src/ui/theme/forest.js +0 -14
  78. package/src/ui/theme/light.js +0 -14
  79. package/src/ui/theme/monokai.js +0 -14
  80. package/src/ui/theme/nord.js +0 -14
  81. package/src/ui/theme.js +0 -49
  82. package/src/utils/command-history.js +0 -43
  83. package/src/utils/error-handler.js +0 -69
  84. package/src/utils/fuzzy-search.js +0 -20
  85. package/src/utils/github-error-handler.js +0 -26
  86. package/src/utils/keychain.js +0 -97
  87. package/src/utils/open-url.js +0 -15
  88. package/src/utils/terminal-detection.js +0 -26
  89. package/src/utils/token-encryption.js +0 -96
@@ -0,0 +1,259 @@
1
+ const chalk = require('chalk');
2
+
3
+ // A flicker-free checkbox prompt — a small, self-contained replacement for
4
+ // inquirer's checkbox on plain Windows consoles.
5
+ //
6
+ // Why not inquirer@8: its ScreenManager repaints with "erase N lines" and
7
+ // "write content" as TWO separate stdout writes, so terminals without double
8
+ // buffering briefly show an empty area — a visible flash on EVERY keypress.
9
+ // This prompt composes erase + full content into ONE write() per render,
10
+ // uses ASCII-only glyphs ([x]/[ ]), and keeps the cursor hidden while active.
11
+ //
12
+ // Same choice contract as inquirer: { name, value, checked?, disabled? }
13
+ // disabled: true or a reason string — rendered dim, not selectable.
14
+ // Resolves with the array of selected (non-disabled) values.
15
+
16
+ // Erases the previous frame (n lines up + clear-to-bottom) — always paired
17
+ // with the new content in the same write().
18
+ function eraseFrame(lines) {
19
+ if (lines <= 0) return '';
20
+ return `\x1b[${lines - 1}A\r\x1b[J`;
21
+ }
22
+
23
+ function checkboxPrompt(options) {
24
+ const {
25
+ name = '',
26
+ message,
27
+ choices,
28
+ pageSize = 7,
29
+ stdout = process.stdout,
30
+ stdin = process.stdin,
31
+ inputStream, // test hook: Event-emitter standing in for raw stdin
32
+ } = options;
33
+
34
+ return new Promise((resolve, reject) => {
35
+ if (!stdin.isTTY && !inputStream) {
36
+ reject(new Error('checkboxPrompt needs a terminal (TTY)'));
37
+ return;
38
+ }
39
+
40
+ // ── state ───────────────────────────────────────────────────
41
+ const state = choices.map(c => ({
42
+ ...c,
43
+ checked: Boolean(c.checked) && !c.disabled,
44
+ isDisabled: Boolean(c.disabled),
45
+ }));
46
+ let index = 0;
47
+ let top = 0;
48
+ let drawnLines = 0;
49
+ let settled = false;
50
+
51
+ const visibleCount = () => Math.min(state.length, Math.max(1, pageSize));
52
+
53
+ const clampScroll = () => {
54
+ const vis = visibleCount();
55
+ if (index < top) top = index;
56
+ if (index >= top + vis) top = index - vis + 1;
57
+ if (top > state.length - vis) top = Math.max(0, state.length - vis);
58
+ };
59
+
60
+ const firstSelectable = () => state.findIndex(c => !c.isDisabled);
61
+ const lastSelectable = () => {
62
+ for (let i = state.length - 1; i >= 0; i--) if (!state[i].isDisabled) return i;
63
+ return state.length - 1;
64
+ };
65
+ const nextSelectable = (from, dir) => {
66
+ let i = from;
67
+ for (let step = 0; step < state.length; step++) {
68
+ i = (i + dir + state.length) % state.length;
69
+ if (!state[i].isDisabled) return i;
70
+ }
71
+ return from;
72
+ };
73
+
74
+ // ── rendering (one write per frame) ─────────────────────────
75
+ const content = () => {
76
+ const lines = [];
77
+ lines.push(`? ${message}`);
78
+ clampScroll();
79
+ const vis = visibleCount();
80
+ for (let i = top; i < top + vis; i++) {
81
+ const c = state[i];
82
+ const marker = c.checked ? '[x]' : '[ ]';
83
+ const pointer = i === index ? chalk.red.bold('>') : ' ';
84
+ const label = c.isDisabled
85
+ ? chalk.dim(`${c.name}${typeof c.disabled === 'string' ? ` (${c.disabled})` : ' (locked)'}`)
86
+ : c.name;
87
+ const line = `${pointer} ${marker} ${label}`;
88
+ // the active row is red + bold so it clearly stands out
89
+ lines.push(i === index ? chalk.red.bold(line) : line);
90
+ }
91
+ lines.push(chalk.dim(' arrows = move · space = select · enter = done'));
92
+ return lines;
93
+ };
94
+
95
+ const render = () => {
96
+ const lines = content();
97
+ const chunk = (drawnLines > 0 ? eraseFrame(drawnLines) : '\x1b[?25l') + lines.join('\n');
98
+ drawnLines = lines.length;
99
+ stdout.write(chunk); // THE fix: erase + content in a single atomic write
100
+ };
101
+
102
+ const close = () => {
103
+ settled = true;
104
+ if (inputStream) {
105
+ inputStream.removeListener('keypress', onKey);
106
+ } else {
107
+ stdin.removeListener('keypress', onKey);
108
+ if (stdin.setRawMode) stdin.setRawMode(false);
109
+ stdin.pause && stdin.pause();
110
+ stdin.resume && stdin.resume();
111
+ }
112
+ stdout.removeListener('resize', onResize);
113
+ stdout.write(eraseFrame(drawnLines) + '\x1b[?25h'); // remove the block, show cursor
114
+ };
115
+
116
+ const finish = () => {
117
+ if (settled) return;
118
+ close();
119
+ resolve(state.filter(c => c.checked && !c.isDisabled).map(c => c.value));
120
+ };
121
+
122
+ const abort = () => {
123
+ if (settled) return;
124
+ close();
125
+ const err = new Error('cancelled');
126
+ err.code = 'ADM_CANCELLED';
127
+ reject(err);
128
+ };
129
+
130
+ // ── keys ────────────────────────────────────────────────────
131
+ const onKey = (ch, key = {}) => {
132
+ const seq = key.name ? key.name : String(ch || '');
133
+ if (key.ctrl && seq === 'c') { abort(); return; }
134
+ switch (seq) {
135
+ case 'up':
136
+ case 'k':
137
+ index = nextSelectable(index, -1);
138
+ break;
139
+ case 'down':
140
+ case 'j':
141
+ index = nextSelectable(index, 1);
142
+ break;
143
+ case 'space':
144
+ if (!state[index].isDisabled) state[index].checked = !state[index].checked;
145
+ break;
146
+ case 'enter':
147
+ case 'return':
148
+ finish();
149
+ return;
150
+ default:
151
+ return; // unknown key → no repaint
152
+ }
153
+ if (!settled) render();
154
+ };
155
+
156
+ const onResize = () => {
157
+ if (!settled) { drawnLines = 0; render(); }
158
+ };
159
+
160
+ stdout.on('resize', onResize);
161
+
162
+ if (inputStream) {
163
+ inputStream.on('keypress', onKey);
164
+ } else {
165
+ const readline = require('readline');
166
+ readline.emitKeypressEvents(stdin);
167
+ if (stdin.setRawMode) stdin.setRawMode(true);
168
+ stdin.resume();
169
+ stdin.on('keypress', onKey);
170
+ }
171
+
172
+ render();
173
+ });
174
+ }
175
+
176
+ // A flicker-free y/n confirm — the same single-write philosophy as
177
+ // checkboxPrompt. Renders "? message (y/n)" on one line; y/n answer
178
+ // immediately, Enter takes the default, Ctrl+C cancels (ADM_CANCELLED).
179
+ // On answer the question is ATOMICALLY replaced (one write) by the
180
+ // caller's `answerLine(confirmed)` done-format line — zero flicker on the
181
+ // transition (owner fix, 0.3.x: the inquirer confirm left its rows on
182
+ // screen and the erase/redraw blink was visible).
183
+ function confirmPrompt(options) {
184
+ const {
185
+ message,
186
+ defaultValue = false,
187
+ answerLine = () => '',
188
+ stdout = process.stdout,
189
+ stdin = process.stdin,
190
+ inputStream,
191
+ } = options;
192
+
193
+ return new Promise((resolve, reject) => {
194
+ if (!stdin.isTTY && !inputStream) {
195
+ reject(new Error('confirmPrompt needs a terminal (TTY)'));
196
+ return;
197
+ }
198
+
199
+ const cols = () => Math.max(20, (stdout.columns || 80) - 1);
200
+ const plain = s => String(s).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
201
+ const question = `? ${message} ${chalk.dim('(y/n)')}`;
202
+ let rows = Math.max(1, Math.ceil((plain(question).length + 4) / cols()));
203
+ let settled = false;
204
+
205
+ const eraseStr = () => (rows > 1 ? `\x1b[${rows - 1}A` : '') + '\r\x1b[J';
206
+
207
+ const detach = () => {
208
+ if (inputStream) {
209
+ inputStream.removeListener('keypress', onKey);
210
+ } else {
211
+ stdin.removeListener('keypress', onKey);
212
+ if (stdin.setRawMode) stdin.setRawMode(false);
213
+ stdin.pause && stdin.pause();
214
+ stdin.resume && stdin.resume();
215
+ }
216
+ };
217
+
218
+ const finish = (value) => {
219
+ if (settled) return;
220
+ settled = true;
221
+ detach();
222
+ // ONE atomic write: hide-cursor cleanup + erase question + done line
223
+ stdout.write(eraseStr() + answerLine(value) + '\n' + '\x1b[?25h');
224
+ resolve(value);
225
+ };
226
+
227
+ const abort = () => {
228
+ if (settled) return;
229
+ settled = true;
230
+ detach();
231
+ stdout.write(eraseStr() + '\x1b[?25h');
232
+ const err = new Error('cancelled');
233
+ err.code = 'ADM_CANCELLED';
234
+ reject(err);
235
+ };
236
+
237
+ const onKey = (ch, key = {}) => {
238
+ const seq = key.name ? key.name : String(ch || '').toLowerCase();
239
+ if (key.ctrl && seq === 'c') { abort(); return; }
240
+ if (seq === 'y') { finish(true); return; }
241
+ if (seq === 'n') { finish(false); return; }
242
+ if (seq === 'enter' || seq === 'return') { finish(defaultValue); return; }
243
+ };
244
+
245
+ stdout.write('\x1b[?25l' + question); // hide cursor + render, one write
246
+
247
+ if (inputStream) {
248
+ inputStream.on('keypress', onKey);
249
+ } else {
250
+ const readline = require('readline');
251
+ readline.emitKeypressEvents(stdin);
252
+ if (stdin.setRawMode) stdin.setRawMode(true);
253
+ stdin.resume();
254
+ stdin.on('keypress', onKey);
255
+ }
256
+ });
257
+ }
258
+
259
+ module.exports = { checkboxPrompt, confirmPrompt, eraseFrame };
@@ -0,0 +1,48 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ // Installed-tools registry (v0.4.0 groundwork, #30): every real install
6
+ // appends { id, name, method, installedAt } to ~/.adm/installed.json.
7
+ // Same id → the entry is replaced (updated timestamp), never duplicated.
8
+
9
+ function registryFile(opts = {}) {
10
+ const dir = opts.configDir || process.env.ADM_CONFIG_DIR || path.join(os.homedir(), '.adm');
11
+ return path.join(dir, 'installed.json');
12
+ }
13
+
14
+ function readRegistry(opts = {}) {
15
+ const fsMod = opts.fs || fs;
16
+ try {
17
+ const data = JSON.parse(fsMod.readFileSync(registryFile(opts), 'utf8'));
18
+ return Array.isArray(data) ? data : [];
19
+ } catch {
20
+ return [];
21
+ }
22
+ }
23
+
24
+ function writeRegistry(entries, opts = {}) {
25
+ const fsMod = opts.fs || fs;
26
+ const file = registryFile(opts);
27
+ fsMod.mkdirSync(path.dirname(file), { recursive: true });
28
+ fsMod.writeFileSync(file, `${JSON.stringify(entries, null, 2)}\n`, 'utf8');
29
+ return file;
30
+ }
31
+
32
+ // Appends/updates an entry. Returns the registry file path.
33
+ function appendInstalled(entry, opts = {}) {
34
+ const now = opts.now || new Date().toISOString();
35
+ const items = readRegistry(opts);
36
+ const record = {
37
+ id: entry.id,
38
+ name: entry.name || entry.id,
39
+ method: entry.method || null,
40
+ installedAt: now,
41
+ };
42
+ const existing = items.findIndex(e => e.id === record.id);
43
+ if (existing >= 0) items[existing] = record;
44
+ else items.push(record);
45
+ return writeRegistry(items, opts);
46
+ }
47
+
48
+ module.exports = { appendInstalled, readRegistry, registryFile };
@@ -0,0 +1,67 @@
1
+ // Minimal in-place spinner — a flicker-free ora substitute for plain
2
+ // Windows consoles.
3
+ //
4
+ // ora repaints with "clear line" + "write frame" as separate writes and uses
5
+ // braille glyphs (double-width on Windows) — the visible result on consoles
6
+ // without double buffering is flicker on every frame. This spinner overwrites
7
+ // the same line with `\r` + a fixed-width ASCII frame in ONE write, so the
8
+ // console never shows an intermediate empty frame.
9
+ const chalk = require('chalk');
10
+
11
+ const FRAMES = ['-', '\\', '|', '/'];
12
+
13
+ function miniSpinner(text, opts = {}) {
14
+ const stdout = opts.stdout || process.stdout;
15
+ const interval = opts.interval || 90;
16
+ let timer = null;
17
+ let frame = 0;
18
+ let current = text;
19
+ let active = false;
20
+ let startedAt = 0;
21
+
22
+ const paint = () => {
23
+ if (!active) return;
24
+ const elapsed = Math.floor((Date.now() - startedAt) / 1000);
25
+ // show elapsed time after 3s — heavy installs look "stuck" otherwise
26
+ const time = elapsed >= 3 ? chalk.dim(` [${elapsed}s]`) : '';
27
+ const line = `${FRAMES[frame % FRAMES.length]} ${current}${time}`;
28
+ stdout.write(`\r${line}`); // single write, same line — no erase, no flash
29
+ frame++;
30
+ };
31
+
32
+ const stopPaint = () => {
33
+ if (timer) { clearInterval(timer); timer = null; }
34
+ };
35
+
36
+ const finish = (mark, msg, color) => {
37
+ stopPaint();
38
+ active = false;
39
+ stdout.write(`\r\x1b[2K${mark} ${msg || current}\n`);
40
+ };
41
+
42
+ return {
43
+ start(t) {
44
+ if (t) current = t;
45
+ active = true;
46
+ frame = 0;
47
+ startedAt = Date.now();
48
+ paint();
49
+ timer = setInterval(paint, interval);
50
+ // keep the process ticking until stopped (mirrors ora's behavior)
51
+ if (timer.unref) timer.unref();
52
+ return this;
53
+ },
54
+ text(t) { current = t; paint(); return this; },
55
+ succeed(msg) { finish(chalk.green('✓'), msg, 'green'); return this; },
56
+ fail(msg) { finish(chalk.red('✗'), msg, 'red'); return this; },
57
+ info(msg) { finish(chalk.yellow('i'), msg, 'yellow'); return this; },
58
+ stop() {
59
+ stopPaint();
60
+ active = false;
61
+ stdout.write('\r\x1b[2K');
62
+ return this;
63
+ },
64
+ };
65
+ }
66
+
67
+ module.exports = { miniSpinner };