@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
package/src/reports.js ADDED
@@ -0,0 +1,37 @@
1
+ // Report persistence (owner decision, 0.3.x): every report ADM generates
2
+ // (wizard install, ADM_TOOLS install, upgrade, uninstall) is written to the
3
+ // global cache <configDir>/reports/ — one file per run.
4
+ //
5
+ // Owner requirement: if the config dir itself does not exist the save FAILS
6
+ // LOUDLY (the caller shows a ⚠ line) — ADM must not silently skip
7
+ // persistence and must not resurrect ~/.adm just to store a report. The
8
+ // reports/ subfolder IS auto-created when the config dir exists.
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ // <configDir>/reports — same resolution rules as the registry/config dir.
14
+ function reportsDir(opts = {}) {
15
+ const dir = opts.configDir || process.env.ADM_CONFIG_DIR || path.join(opts.home || os.homedir(), '.adm');
16
+ return path.join(dir, 'reports');
17
+ }
18
+
19
+ // saveReport(kind, body, opts) → { file } on success, { error } when the
20
+ // config dir is missing (nothing is written, nothing auto-created).
21
+ function saveReport(kind, body, opts = {}) {
22
+ const fsMod = opts.fs || fs;
23
+ const dir = reportsDir(opts);
24
+ const configDirPath = path.dirname(dir);
25
+ if (!fsMod.existsSync(configDirPath)) {
26
+ return { error: `cannot save report: ${configDirPath} does not exist` };
27
+ }
28
+ fsMod.mkdirSync(dir, { recursive: true });
29
+ const stamp = (opts.now || new Date()).toISOString()
30
+ .replace(/[:.]/g, '-')
31
+ .slice(0, 19); // 2026-11-11T15-42-08
32
+ const file = path.join(dir, `report-${stamp}.txt`);
33
+ fsMod.writeFileSync(file, String(body), 'utf8');
34
+ return { file };
35
+ }
36
+
37
+ module.exports = { saveReport, reportsDir };
package/src/router.js ADDED
@@ -0,0 +1,157 @@
1
+ const { COMMANDS, AUTOMATION_HINTS } = require('./commands');
2
+ const { getVersion } = require('./commands/version');
3
+ const { showHelp, renderCommandLines } = require('./commands/help');
4
+ const { runFeedback } = require('./commands/feedback');
5
+ const { runConfig } = require('./commands/config');
6
+ const { runUninstall } = require('./commands/uninstall');
7
+ const { runUpgrade } = require('./upgrade');
8
+ const { runDotfilesSync } = require('./commands/dotfiles');
9
+ const { runPlan } = require('./install/plan');
10
+ const { runInstall } = require('./install/run');
11
+ const { runWizard } = require('./setup/wizard');
12
+ const { legacyCleanup } = require('./legacy-cleanup');
13
+ const { closestMatches } = require('./utils/closest-match');
14
+
15
+ function knownTokens() {
16
+ const tokens = [];
17
+ for (const cmd of COMMANDS) {
18
+ tokens.push(cmd.name);
19
+ for (const alias of cmd.aliases || []) tokens.push(alias);
20
+ }
21
+ return tokens;
22
+ }
23
+
24
+ function usageError(message, err) {
25
+ const lines = [
26
+ message,
27
+ '',
28
+ 'Valid commands:',
29
+ ...renderCommandLines(),
30
+ ];
31
+ err(lines.join('\n'));
32
+ return 1;
33
+ }
34
+
35
+ function route(argv, deps = {}) {
36
+ const log = deps.log || (s => process.stdout.write(s + '\n'));
37
+ const err = deps.err || (s => process.stderr.write(s + '\n'));
38
+ const tty = deps.tty !== undefined ? deps.tty : Boolean(process.stdin.isTTY);
39
+
40
+ // ── adm (no args) ────────────────────────────────────────────
41
+ if (argv.length === 0) {
42
+ // Non-interactive tool planning / real install: ADM_TOOLS=<id,id>
43
+ if (process.env.ADM_TOOLS) {
44
+ if (process.env.ADM_DRY_RUN === '1') {
45
+ return runPlan(process.env.ADM_TOOLS, { log, err });
46
+ }
47
+ return runInstall(process.env.ADM_TOOLS, { log, err });
48
+ }
49
+ if (!tty) {
50
+ // Never hang: stdin is not a terminal — the wizard needs a TTY (#26 guard).
51
+ const lines = [
52
+ 'adm needs a terminal for interactive mode.',
53
+ '',
54
+ ...AUTOMATION_HINTS,
55
+ '',
56
+ 'Valid commands:',
57
+ ...renderCommandLines(),
58
+ ];
59
+ err(lines.join('\n'));
60
+ return 1;
61
+ }
62
+ // Interactive setup wizard (#30): categories → tools → install → git step.
63
+ return (deps.wizard || runWizard)({ log, err, tty });
64
+ }
65
+
66
+ const head = argv[0];
67
+ const rest = argv.slice(1);
68
+
69
+ // ── help ─────────────────────────────────────────────────────
70
+ if (head === 'help' || head === '--help' || head === '-h') {
71
+ log(showHelp());
72
+ return 0;
73
+ }
74
+
75
+ // ── -v / --version ───────────────────────────────────────────
76
+ if (head === '-v' || head === '--version') {
77
+ log(getVersion());
78
+ return 0;
79
+ }
80
+
81
+ // ── feedback ─────────────────────────────────────────────────
82
+ if (head === 'feedback') {
83
+ return runFeedback({ log });
84
+ }
85
+
86
+ // ── config [--json] ──────────────────────────────────────────
87
+ if (head === 'config') {
88
+ const json = rest.includes('--json');
89
+ const badFlags = rest.filter(f => f !== '--json');
90
+ if (badFlags.length > 0) return usageError(`Unknown flag for "config": ${badFlags.join(', ')}`, err);
91
+ return runConfig({ json, log, err });
92
+ }
93
+
94
+ // ── uninstall ────────────────────────────────────────────────
95
+ if (head === 'uninstall') {
96
+ if (rest.length > 0) return usageError(`"uninstall" takes no arguments, got: ${rest.join(' ')}`, err);
97
+ return runUninstall({ log, err });
98
+ }
99
+
100
+ // ── upgrade / update (#32) ───────────────────────────────────
101
+ if (head === 'upgrade' || head === 'update') {
102
+ return runUpgrade(rest, { log, err });
103
+ }
104
+
105
+ // ── dotfiles sync ────────────────────────────────────────────
106
+ if (head === 'dotfiles') {
107
+ if (rest[0] !== 'sync') {
108
+ return usageError(`Unknown subcommand for "dotfiles": ${rest[0] || '(none)'}\nValid: adm dotfiles sync`, err);
109
+ }
110
+ return runDotfilesSync(rest.slice(1), { log, err });
111
+ }
112
+
113
+ // ── unknown command ──────────────────────────────────────────
114
+ const suggestions = closestMatches(head, knownTokens());
115
+ const lines = [`Unknown command: ${head}`];
116
+ if (suggestions.length > 0) lines.push(`Did you mean: ${suggestions.join(', ')}?`);
117
+ lines.push('');
118
+ lines.push('Valid commands:');
119
+ lines.push(...renderCommandLines());
120
+ err(lines.join('\n'));
121
+ return 1;
122
+ }
123
+
124
+ // Run one ADM invocation end-to-end and return the process exit code.
125
+ // Legacy cleanup runs on first start of every command except "-v" (kept
126
+ // machine-parsable) and the no-args notices.
127
+ async function main(argv, deps = {}) {
128
+ const log = deps.log || (s => process.stdout.write(s + '\n'));
129
+ const err = deps.err || (s => process.stderr.write(s + '\n'));
130
+ const tty = deps.tty !== undefined ? deps.tty : Boolean(process.stdin.isTTY);
131
+
132
+ const first = argv[0];
133
+ // Cleanup runs on every start except "-v" (machine-parsable output) and
134
+ // "uninstall" (it removes the whole config dir anyway; writing the cleanedAt
135
+ // marker would resurrect the dir before the removal).
136
+ const skipCleanup = first === '-v' || first === '--version' || first === 'uninstall';
137
+
138
+ if (!skipCleanup) {
139
+ // Keep machine-parsable output clean: the cleanup notice goes to stderr
140
+ // when the invoked command prints JSON.
141
+ const machineOutput = first === 'config' && argv.includes('--json');
142
+ try {
143
+ await legacyCleanup({ log: machineOutput ? err : log });
144
+ } catch {
145
+ // Cleanup must never block a command from running.
146
+ }
147
+ }
148
+
149
+ try {
150
+ return (deps.route || route)(argv, { log, err, tty });
151
+ } catch (e) {
152
+ err(`ADM failed: ${e && e.message ? e.message : e}`);
153
+ return 2;
154
+ }
155
+ }
156
+
157
+ module.exports = { route, main };
@@ -1,13 +1,13 @@
1
- const data = require('../data/extensions.json');
2
-
3
- function loadCatalog() {
4
- return data.categories;
5
- }
6
-
7
- function getToolsByCategories(categoryIds) {
8
- const categories = loadCatalog();
9
- const selected = categories.filter(cat => categoryIds.includes(cat.id));
10
- return selected.flatMap(cat => cat.tools);
11
- }
12
-
13
- module.exports = { loadCatalog, getToolsByCategories };
1
+ const data = require('../data/extensions.json');
2
+
3
+ function loadCatalog() {
4
+ return data.categories;
5
+ }
6
+
7
+ function getToolsByCategories(categoryIds) {
8
+ const categories = loadCatalog();
9
+ const selected = categories.filter(cat => categoryIds.includes(cat.id));
10
+ return selected.flatMap(cat => cat.tools);
11
+ }
12
+
13
+ module.exports = { loadCatalog, getToolsByCategories };
@@ -0,0 +1,128 @@
1
+ const { readConfig, writeConfig } = require('../config');
2
+
3
+ // Wizard's git step (#30, story 10): user.name + user.email, nothing else
4
+ // (SSH/GPG intentionally cut). Defaults come from the current git config;
5
+ // values are written via `git config --global` AND stored in
6
+ // ~/.adm/config.json under { git: { name, email } }.
7
+
8
+ // Reads one key from global git config ("" when unset). exec injectable.
9
+ function readGitValue(key, { execFunc } = {}) {
10
+ const exec = execFunc || require('child_process').exec;
11
+ return new Promise(resolve => {
12
+ exec(`git config --global ${key}`, { shell: 'sh' }, (err, stdout) => {
13
+ resolve(err ? '' : String(stdout || '').trim());
14
+ });
15
+ });
16
+ }
17
+
18
+ function gitSetCommand(key, value) {
19
+ const escaped = String(value).replace(/"/g, '\\"');
20
+ return `git config --global ${key} "${escaped}"`;
21
+ }
22
+
23
+ function setGitValue(key, value, { execFunc } = {}) {
24
+ const exec = execFunc || require('child_process').exec;
25
+ return new Promise((resolve, reject) => {
26
+ exec(gitSetCommand(key, value), { shell: 'sh' }, err => (err ? reject(err) : resolve()));
27
+ });
28
+ }
29
+
30
+ async function readSavedGit({ readConfigFn, configDir } = {}) {
31
+ try {
32
+ const cfg = await (readConfigFn || readConfig)(configDir);
33
+ return (cfg && cfg.git) || {};
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+
39
+ // Runs the git step. Returns:
40
+ // { status: 'existing', name, email } both already set — nothing asked
41
+ // { status: 'declined-before' } user declined in a previous run
42
+ // { status: 'skipped' } user declined now (remembered)
43
+ // { status: 'configured', name, email } written via git config + config.json
44
+ async function runGitStep(opts = {}) {
45
+ const {
46
+ execFunc,
47
+ prompt = require('inquirer').prompt,
48
+ log = () => {},
49
+ readConfigFn,
50
+ writeConfigFn,
51
+ configDir,
52
+ } = opts;
53
+
54
+ const fullConfig = await safeReadAll(readConfigFn, configDir);
55
+ const savedGit = (fullConfig && fullConfig.git) || {};
56
+
57
+ const currentName = (await readGitValue('user.name', { execFunc })) || savedGit.name || '';
58
+ const currentEmail = (await readGitValue('user.email', { execFunc })) || savedGit.email || '';
59
+
60
+ if (currentName && currentEmail) {
61
+ log(`git already configured: ${currentName} <${currentEmail}>`);
62
+ return { status: 'existing', name: currentName, email: currentEmail };
63
+ }
64
+
65
+ // A previous "no" is final — never ask again (unless git is configured now).
66
+ if (savedGit.setupDeclined) {
67
+ return { status: 'declined-before' };
68
+ }
69
+
70
+ const { proceed } = await prompt([{
71
+ type: 'confirm',
72
+ name: 'proceed',
73
+ message: 'Configure git identity (user.name / user.email) now?',
74
+ default: true,
75
+ }]);
76
+ if (!proceed) {
77
+ // Remember the decline — the wizard must not re-ask on every start.
78
+ try {
79
+ const writer = writeConfigFn || ((cfg, dir) => writeConfig(cfg, dir));
80
+ await writer({ ...fullConfig, git: { ...savedGit, setupDeclined: true } }, configDir);
81
+ } catch { /* config write must never break the wizard */ }
82
+ return { status: 'skipped' };
83
+ }
84
+
85
+ const answers = await prompt([
86
+ {
87
+ type: 'input',
88
+ name: 'name',
89
+ message: 'git user.name:',
90
+ default: currentName,
91
+ validate: v => String(v).trim().length > 0 || 'name cannot be empty',
92
+ },
93
+ {
94
+ type: 'input',
95
+ name: 'email',
96
+ message: 'git user.email:',
97
+ default: currentEmail,
98
+ validate: v => /.+@.+/.test(String(v).trim()) || 'enter a valid email',
99
+ },
100
+ ]);
101
+
102
+ const name = answers.name.trim();
103
+ const email = answers.email.trim();
104
+ await setGitValue('user.name', name, { execFunc });
105
+ await setGitValue('user.email', email, { execFunc });
106
+
107
+ // Persist in ADM config too (config.json), merged with whatever is saved.
108
+ const writer = writeConfigFn || ((cfg, dir) => writeConfig(cfg, dir));
109
+ const saved = await readSavedGit({ readConfigFn, configDir });
110
+ const full = await safeReadAll(readConfigFn, configDir);
111
+ await writer({
112
+ ...full,
113
+ git: { ...saved, name, email },
114
+ }, configDir);
115
+
116
+ return { status: 'configured', name, email };
117
+ }
118
+
119
+ async function safeReadAll(readConfigFn, configDir) {
120
+ try {
121
+ const read = readConfigFn || readConfig;
122
+ return (await read(configDir)) || {};
123
+ } catch {
124
+ return {};
125
+ }
126
+ }
127
+
128
+ module.exports = { runGitStep, readGitValue, setGitValue, gitSetCommand };
@@ -0,0 +1,108 @@
1
+ // Loader — the dots_12 braille animation (56 frames), ADM's own spinner.
2
+ // Replaces the ASCII \|/- spinner everywhere ADM "works": startup
3
+ // (Preparing…), tool-status scan (Scanning…), per-tool install lines.
4
+ //
5
+ // Frames: dots_12 from cli-spinners/cli-loaders — MIT license.
6
+ // https://cliloaders.com/dots_12 · https://github.com/cbmongithub/cli-loaders
7
+ // (frames also mirrored in the repo root loader.txt)
8
+ //
9
+ // Wrap-aware: install lines carry the FULL command in parentheses, so a
10
+ // frame can span several terminal rows — every repaint erases ALL rows the
11
+ // previous frame occupied (a plain `\r\x1b[2K` would leave leftovers).
12
+ //
13
+ // The terminal cursor is HIDDEN while the loader animates (it would blink
14
+ // on top of the frame) and restored on every finish/stop.
15
+ const chalk = require('chalk');
16
+
17
+ const FRAMES = [
18
+ '⢀⠀', '⡀⠀', '⠄⠀', '⢂⠀', '⡂⠀', '⠅⠀', '⢃⠀', '⡃⠀', '⠍⠀', '⢋⠀', '⡋⠀',
19
+ '⠍⠁', '⢋⠁', '⡋⠁', '⠍⠉', '⠋⠉', '⠋⠉', '⠉⠙', '⠉⠙', '⠉⠩', '⠈⢙', '⠈⡙',
20
+ '⢈⠩', '⡀⢙', '⠄⡙', '⢂⠩', '⡂⢘', '⠅⡘', '⢃⠨', '⡃⢐', '⠍⡐', '⢋⠠', '⡋⢀',
21
+ '⠍⡁', '⢋⠁', '⡋⠁', '⠍⠉', '⠋⠉', '⠋⠉', '⠉⠙', '⠉⠙', '⠉⠩', '⠈⢙', '⠈⡙',
22
+ '⠈⠩', '⠀⢙', '⠀⡙', '⠀⠩', '⠀⢘', '⠀⡘', '⠀⠨', '⠀⢐', '⠀⡐', '⠀⠠', '⠀⢀',
23
+ '⠀⡀',
24
+ ];
25
+
26
+ function stripAnsi(s) {
27
+ return String(s).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
28
+ }
29
+
30
+ // Same method contract as miniSpinner (start/text/succeed/fail/info/stop),
31
+ // so the wizard's spinnerFactory injection works with either.
32
+ // Owner decision: the frame + text render in the terminal's default color
33
+ // (white) — no cyan/blue tint.
34
+ function loader(text, opts = {}) {
35
+ const stdout = opts.stdout || process.stdout;
36
+ const interval = opts.interval || 80;
37
+ const frames = opts.frames || FRAMES;
38
+ let current = text;
39
+ let frame = 0;
40
+ let timer = null;
41
+ let active = false;
42
+ let rows = 0; // visual rows the currently painted frame occupies
43
+ let startedAt = 0;
44
+
45
+ const cols = () => Math.max(20, (stdout.columns || 80) - 1);
46
+
47
+ // erase-string for the currently painted frame
48
+ const eraseStr = () => {
49
+ if (rows <= 0) return '';
50
+ return (rows > 1 ? `\x1b[${rows - 1}A` : '') + '\r\x1b[J';
51
+ };
52
+
53
+ // ONE atomic write per repaint (erase + content together) — separate
54
+ // writes flicker on consoles without double buffering.
55
+ const paint = () => {
56
+ if (!active) return;
57
+ const elapsed = Date.now() - startedAt;
58
+ // long installs show the elapsed time — same contract as before
59
+ const time = elapsed >= 3000 ? chalk.dim(` [${Math.floor(elapsed / 1000)}s]`) : '';
60
+ const line = `${frames[frame % frames.length]} ${current}${time}`;
61
+ stdout.write(eraseStr() + line);
62
+ rows = Math.max(1, Math.ceil(stripAnsi(line).length / cols()));
63
+ frame++;
64
+ };
65
+
66
+ const stopPaint = () => {
67
+ if (timer) { clearInterval(timer); timer = null; }
68
+ };
69
+
70
+ const finish = (mark, msg) => {
71
+ stopPaint();
72
+ active = false;
73
+ // cursor back + erase + status line in ONE write — no flicker
74
+ stdout.write('\x1b[?25h' + eraseStr() + `${mark} ${msg || current}\n`);
75
+ rows = 0;
76
+ };
77
+
78
+ return {
79
+ start(t) {
80
+ if (t) current = t;
81
+ active = true;
82
+ frame = 0;
83
+ startedAt = Date.now();
84
+ const line = `${frames[frame % frames.length]} ${current}`;
85
+ stdout.write('\x1b[?25l' + (rows > 0 ? eraseStr() : '') + line);
86
+ rows = Math.max(1, Math.ceil(stripAnsi(line).length / cols()));
87
+ frame++;
88
+ timer = setInterval(paint, interval);
89
+ if (timer.unref) timer.unref();
90
+ return this;
91
+ },
92
+ text(t) { current = t; paint(); return this; },
93
+ succeed(msg) { finish(chalk.green('✓'), msg); return this; },
94
+ fail(msg) { finish(chalk.red('✗'), msg); return this; },
95
+ info(msg) { finish(chalk.yellow('i'), msg); return this; },
96
+ stop() {
97
+ stopPaint();
98
+ if (active) {
99
+ active = false;
100
+ stdout.write('\x1b[?25h' + eraseStr());
101
+ rows = 0;
102
+ }
103
+ return this;
104
+ },
105
+ };
106
+ }
107
+
108
+ module.exports = { loader, FRAMES };