@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.
- package/LICENSE +21 -21
- package/README.md +154 -161
- package/bin/adm +18 -21
- package/package.json +9 -22
- package/scripts/completion/adm.bash +12 -55
- package/scripts/completion/adm.zsh +21 -58
- package/scripts/install.js +8 -8
- package/src/commands/config.js +66 -0
- package/src/commands/dotfiles.js +49 -32
- package/src/commands/feedback.js +12 -0
- package/src/commands/help.js +33 -0
- package/src/commands/uninstall.js +73 -54
- package/src/commands/version.js +7 -0
- package/src/commands.js +26 -0
- package/src/config/index.js +32 -32
- package/src/data/extensions.json +358 -358
- package/src/engines.js +38 -0
- package/src/install/executor.js +263 -0
- package/src/install/hints.js +94 -0
- package/src/install/npm-prefix.js +110 -0
- package/src/install/plan.js +75 -0
- package/src/install/report.js +46 -0
- package/src/install/run.js +88 -0
- package/src/install/shell-paths.js +129 -0
- package/src/legacy-cleanup.js +96 -0
- package/src/pkg/builders.js +220 -0
- package/src/pkg/managers.js +59 -0
- package/src/pkg/package-names.js +229 -0
- package/src/pkg/script-urls.js +100 -0
- package/src/platform/index.js +129 -0
- package/src/platform/os-release.js +27 -0
- package/src/platform/shell.js +111 -0
- package/src/reports.js +37 -0
- package/src/router.js +157 -0
- package/src/setup/catalog.js +13 -13
- package/src/setup/git-step.js +128 -0
- package/src/setup/loader.js +108 -0
- package/src/setup/prompts.js +259 -0
- package/src/setup/registry.js +48 -0
- package/src/setup/spinner.js +67 -0
- package/src/setup/wizard.js +319 -195
- package/src/upgrade/index.js +499 -0
- package/src/utils/closest-match.js +36 -0
- package/src/utils/dotfiles-sync.js +89 -89
- package/src/utils/file-conflict-handler.js +30 -30
- package/bin/adm-tui +0 -13
- package/src/ai/knowledge.js +0 -61
- package/src/commands/connect.js +0 -76
- package/src/commands/issue.js +0 -80
- package/src/commands/pr.js +0 -55
- package/src/commands/theme.js +0 -103
- package/src/commands/upgrade.js +0 -114
- package/src/installer/git-config.js +0 -34
- package/src/installer/node-installer.js +0 -50
- package/src/installer/package-manager-installer.js +0 -31
- package/src/installer/ssh-setup.js +0 -31
- package/src/installer/system-packages.js +0 -49
- package/src/installers/index.js +0 -66
- package/src/integrations/ai-backend.js +0 -60
- package/src/integrations/ai-providers/registry.js +0 -261
- package/src/integrations/github.js +0 -107
- package/src/integrations/gitlab.js +0 -121
- package/src/plugins/context.js +0 -64
- package/src/plugins/loader.js +0 -50
- package/src/plugins/runner.js +0 -25
- package/src/setup/installer.js +0 -192
- package/src/shell/detector.js +0 -10
- package/src/tui/app-state.js +0 -374
- package/src/tui/app.js +0 -442
- package/src/tui/autocomplete.js +0 -57
- package/src/tui/commands/registry.js +0 -823
- package/src/tui/components/SetupScreen.js +0 -292
- package/src/ui/animations.js +0 -24
- package/src/ui/spinner.js +0 -34
- package/src/ui/theme/cyberpunk.js +0 -14
- package/src/ui/theme/dark.js +0 -14
- package/src/ui/theme/forest.js +0 -14
- package/src/ui/theme/light.js +0 -14
- package/src/ui/theme/monokai.js +0 -14
- package/src/ui/theme/nord.js +0 -14
- package/src/ui/theme.js +0 -49
- package/src/utils/command-history.js +0 -43
- package/src/utils/error-handler.js +0 -69
- package/src/utils/fuzzy-search.js +0 -20
- package/src/utils/github-error-handler.js +0 -26
- package/src/utils/keychain.js +0 -97
- package/src/utils/open-url.js +0 -15
- package/src/utils/terminal-detection.js +0 -26
- 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 };
|
package/src/setup/catalog.js
CHANGED
|
@@ -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 };
|