@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/setup/wizard.js
CHANGED
|
@@ -1,220 +1,344 @@
|
|
|
1
|
-
const inquirer = require('inquirer');
|
|
2
1
|
const chalk = require('chalk');
|
|
3
|
-
const
|
|
4
|
-
const {
|
|
5
|
-
const {
|
|
6
|
-
const {
|
|
7
|
-
const {
|
|
8
|
-
const {
|
|
9
|
-
const {
|
|
10
|
-
const {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
console.log(chalk.cyan('\n ADM Setup Wizard\n'));
|
|
17
|
-
console.log(chalk.gray(` Detected shell: ${shell}`));
|
|
18
|
-
console.log(chalk.gray(` Detected OS: ${detectPlatform()}\n`));
|
|
19
|
-
|
|
20
|
-
if (dry) {
|
|
21
|
-
return { dryRun: true, shell, platform: detectPlatform(), planned: getFullPlan() };
|
|
22
|
-
}
|
|
2
|
+
const { loadCatalog, getToolsByCategories } = require('./catalog');
|
|
3
|
+
const { detectEnvironment } = require('../platform');
|
|
4
|
+
const { buildToolPlan, withFallbackRoute } = require('../pkg/builders');
|
|
5
|
+
const { isInstalled, executeInstall } = require('../install/executor');
|
|
6
|
+
const { renderResults } = require('../install/report');
|
|
7
|
+
const { appendInstalled } = require('./registry');
|
|
8
|
+
const { runGitStep } = require('./git-step');
|
|
9
|
+
const { readConfig } = require('../config');
|
|
10
|
+
const { checkboxPrompt, confirmPrompt } = require('./prompts');
|
|
11
|
+
const { loader } = require('./loader');
|
|
12
|
+
const { saveReport } = require('../reports');
|
|
13
|
+
const { getVersion } = require('../commands/version');
|
|
23
14
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
15
|
+
// The interactive setup wizard (#30): banner → Preparing… → categories →
|
|
16
|
+
// Scanning… → tools → install → "Worked for…" → report → git step.
|
|
17
|
+
//
|
|
18
|
+
// Acceptance-critical details:
|
|
19
|
+
// - the banner (CrystalPlatforms ADM-CLI vX.Y.Z) is printed ONCE and stays
|
|
20
|
+
// on top for the whole session
|
|
21
|
+
// - all "work" phases use the dots_12 loader: Preparing… (startup),
|
|
22
|
+
// Scanning… (status probes) and every install line
|
|
23
|
+
// - install lines carry the full command in parentheses and the whole
|
|
24
|
+
// install process collapses into one "Worked for <time>" line when it
|
|
25
|
+
// ends — the report below stays
|
|
26
|
+
// - the tools screen marks installed tools "✓ …" but keeps them
|
|
27
|
+
// selectable — picking one triggers a REINSTALL (`reinstalled`) (#34)
|
|
28
|
+
// - manual/link tools stay selectable: no automatic route → `manual`
|
|
29
|
+
// entry with a copyable hint (guidance, not a failure — owner decision)
|
|
30
|
+
// - every report is persisted to <configDir>/reports/ (owner decision);
|
|
31
|
+
// a missing config dir produces a loud ⚠ line, never a silent skip
|
|
32
|
+
//
|
|
33
|
+
// All I/O is injectable (prompt, execFunc, spinnerFactory, configDir,
|
|
34
|
+
// stdout, env) so the whole flow is testable without a TTY.
|
|
35
|
+
|
|
36
|
+
// Pre-checks which tools are already installed (per statusSource probe).
|
|
37
|
+
async function collectStatuses(entries, opts = {}) {
|
|
38
|
+
const map = new Map();
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
let installed = false;
|
|
41
|
+
try {
|
|
42
|
+
installed = entry.status === 'planned' ? await isInstalled(entry, opts) : false;
|
|
43
|
+
} catch {
|
|
44
|
+
installed = false;
|
|
43
45
|
}
|
|
46
|
+
map.set(entry.id, { entry, installed });
|
|
44
47
|
}
|
|
48
|
+
return map;
|
|
49
|
+
}
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{ name: 'Install latest LTS (via nvm)', value: 'lts' },
|
|
56
|
-
{ name: 'Install specific version', value: 'custom' },
|
|
57
|
-
{ name: 'Skip Node.js installation', value: 'skip' },
|
|
58
|
-
],
|
|
59
|
-
default: 'lts',
|
|
60
|
-
}]);
|
|
61
|
-
let customVersion;
|
|
62
|
-
if (nodeChoice === 'custom') {
|
|
63
|
-
({ customVersion } = await inquirer.prompt([{
|
|
64
|
-
type: 'input', name: 'customVersion', message: 'Enter Node.js version (e.g. 18.17.0):',
|
|
65
|
-
}]));
|
|
51
|
+
// Builds the tools checkbox choices with status marks (#30 story 8).
|
|
52
|
+
// Everything stays selectable (#34): installed tools carry a "✓" mark (and
|
|
53
|
+
// reinstall on pick), manual/link tools keep a clean label (and report as
|
|
54
|
+
// `manual` guidance on pick) — no disabled rows, no locked tools.
|
|
55
|
+
function toolChoices(tools, statuses) {
|
|
56
|
+
return tools.map(tool => {
|
|
57
|
+
const st = statuses.get(tool.id);
|
|
58
|
+
if (st && st.installed) {
|
|
59
|
+
return { name: `✓ ${tool.name}`, value: tool.id, checked: false };
|
|
66
60
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
61
|
+
return { name: tool.name, value: tool.id, checked: false };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fmtDuration(ms) {
|
|
66
|
+
const s = Math.max(1, Math.round(ms / 1000));
|
|
67
|
+
if (s < 60) return `${s}s`;
|
|
68
|
+
const m = Math.floor(s / 60);
|
|
69
|
+
const rest = s % 60;
|
|
70
|
+
if (m < 60) return `${m}m ${rest}s`;
|
|
71
|
+
const h = Math.floor(m / 60);
|
|
72
|
+
return `${h}h ${m % 60}m ${rest}s`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Startup phases are FAST (a few ms) — without this the loader would flash
|
|
76
|
+
// invisibly. Owner decision: Preparing… must actually be seen.
|
|
77
|
+
async function ensureVisible(since, minMs) {
|
|
78
|
+
const left = minMs - (Date.now() - since);
|
|
79
|
+
if (left > 0) await new Promise(r => setTimeout(r, left));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function runWizard(opts = {}) {
|
|
83
|
+
const {
|
|
84
|
+
log = s => process.stdout.write(s + '\n'),
|
|
85
|
+
prompt = require('inquirer').prompt,
|
|
86
|
+
// Custom checkbox + loader: both repaint atomically (single stdout
|
|
87
|
+
// write per frame) — inquirer/ora flicker badly on plain consoles.
|
|
88
|
+
checkbox = checkboxPrompt,
|
|
89
|
+
confirm = confirmPrompt, // y/n prompt (injectable for tests)
|
|
90
|
+
spinnerFactory = text => loader(text),
|
|
91
|
+
execFunc,
|
|
92
|
+
home,
|
|
93
|
+
configDir,
|
|
94
|
+
stdout = process.stdout,
|
|
95
|
+
env: envOverride, // injectable for tests (defaults to real detection)
|
|
96
|
+
} = opts;
|
|
97
|
+
|
|
98
|
+
// Ctrl+C in a prompt → graceful exit, exit code 0.
|
|
99
|
+
const guard = p => p.catch(e => {
|
|
100
|
+
if (e && e.code === 'ADM_CANCELLED') {
|
|
101
|
+
log('Cancelled — nothing was changed.');
|
|
102
|
+
return null;
|
|
76
103
|
}
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
throw e;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Cursor-position collapse (owner fix, 0.3.x): instead of COUNTING lines
|
|
108
|
+
// to erase (inquirer leftovers, wrapped frames or child-process output can
|
|
109
|
+
// skew any count), remember the cursor position before a phase and wipe
|
|
110
|
+
// from that exact spot down afterwards.
|
|
111
|
+
const cursorSave = () => { if (stdout.isTTY) stdout.write('\x1b[s'); };
|
|
112
|
+
const cursorRestoreClear = () => { if (stdout.isTTY) stdout.write('\x1b[u\x1b[J'); };
|
|
113
|
+
|
|
114
|
+
// ── banner — printed once, stays on top for the whole session ─────
|
|
115
|
+
log(chalk.bold(`CrystalPlatforms ADM-CLI v${getVersion()}`));
|
|
116
|
+
|
|
117
|
+
// ── Preparing… — catalog, environment and saved config under the loader
|
|
118
|
+
const prepStart = Date.now();
|
|
119
|
+
const prep = spinnerFactory('Preparing...');
|
|
120
|
+
prep.start();
|
|
121
|
+
const env = envOverride || detectEnvironment();
|
|
122
|
+
const catalog = loadCatalog();
|
|
123
|
+
let declinedBefore = false;
|
|
124
|
+
try {
|
|
125
|
+
const saved = await readConfig(configDir);
|
|
126
|
+
declinedBefore = Boolean(saved && saved.git && saved.git.setupDeclined);
|
|
127
|
+
} catch {
|
|
128
|
+
declinedBefore = false;
|
|
79
129
|
}
|
|
130
|
+
await ensureVisible(prepStart, 600); // the phase is real but fast — keep it visible
|
|
131
|
+
prep.stop();
|
|
80
132
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
} else {
|
|
93
|
-
console.log(chalk.green(` pnpm ${pnpmStatus.version} already installed`));
|
|
133
|
+
// ── 1. categories — ALL on one screen (story 6) ─────────────────
|
|
134
|
+
const categories = await guard(checkbox({
|
|
135
|
+
name: 'categories',
|
|
136
|
+
message: 'Choose categories...',
|
|
137
|
+
choices: catalog.map(c => ({ name: c.name, value: c.id })),
|
|
138
|
+
pageSize: catalog.length,
|
|
139
|
+
}));
|
|
140
|
+
if (categories === null) return 0;
|
|
141
|
+
if (!categories || categories.length === 0) {
|
|
142
|
+
log('Nothing selected — nothing to do.');
|
|
143
|
+
return 0;
|
|
94
144
|
}
|
|
145
|
+
log(`${chalk.green('✓')} Chosen ${categories.length} categor${categories.length === 1 ? 'y' : 'ies'}.`);
|
|
146
|
+
|
|
147
|
+
// ── 2. tools for the selected categories, with status (Scanning…) ──
|
|
148
|
+
const scanStart = Date.now();
|
|
149
|
+
const scan = spinnerFactory('Scanning...');
|
|
150
|
+
scan.start();
|
|
151
|
+
const tools = getToolsByCategories(categories);
|
|
152
|
+
const statuses = await collectStatuses(tools.map(tool => ({
|
|
153
|
+
id: tool.id,
|
|
154
|
+
name: tool.name,
|
|
155
|
+
category: tool.id,
|
|
156
|
+
method: tool.installMethod,
|
|
157
|
+
...buildToolPlan(tool, env),
|
|
158
|
+
})), { execFunc, home });
|
|
159
|
+
await ensureVisible(scanStart, 500); // keep the loader + Scanning… visible
|
|
160
|
+
scan.stop();
|
|
95
161
|
|
|
96
|
-
|
|
97
|
-
const { tools } = await inquirer.prompt([{
|
|
98
|
-
type: 'checkbox',
|
|
162
|
+
const selectedIds = await guard(checkbox({
|
|
99
163
|
name: 'tools',
|
|
100
|
-
message: '
|
|
101
|
-
choices:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
164
|
+
message: 'Choose tools to install...',
|
|
165
|
+
choices: toolChoices(tools, statuses),
|
|
166
|
+
pageSize: Math.min(Math.max(tools.length, 10), 15),
|
|
167
|
+
}));
|
|
168
|
+
if (selectedIds === null) return 0;
|
|
169
|
+
if (!selectedIds || selectedIds.length === 0) {
|
|
170
|
+
log('Nothing selected — nothing to do.');
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
log(`${chalk.green('✓')} Chosen ${selectedIds.length} tool${selectedIds.length === 1 ? '' : 's'} to install.`);
|
|
174
|
+
|
|
175
|
+
// Everything the user picked is attempted (#34):
|
|
176
|
+
// - an installed pick forces a REINSTALL (executor skips its
|
|
177
|
+
// "already installed" probe) and lands as `reinstalled` in the report
|
|
178
|
+
// - tools with NO route on this platform (manual after the fallback,
|
|
179
|
+
// link-only extensions) run nothing — they land in the report as
|
|
180
|
+
// `manual` entries with a copyable hint. Owner decision (0.3.0 HITL):
|
|
181
|
+
// a missing route is GUIDANCE, not a failure.
|
|
182
|
+
const toInstall = (selectedIds || [])
|
|
183
|
+
.map(id => statuses.get(id))
|
|
184
|
+
.filter(st => st && st.entry)
|
|
185
|
+
.map(st => {
|
|
186
|
+
const entry = withFallbackRoute(st.entry, env);
|
|
187
|
+
entry.force = Boolean(st.installed);
|
|
188
|
+
return entry;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ── split picks: runnable vs. guidance-only ─────────────────────
|
|
192
|
+
const manualResults = [];
|
|
193
|
+
const runnable = [];
|
|
194
|
+
for (const entry of toInstall) {
|
|
195
|
+
if (entry.status === 'manual') {
|
|
196
|
+
manualResults.push({
|
|
197
|
+
id: entry.id, name: entry.name, category: entry.category, method: entry.method,
|
|
198
|
+
status: 'manual', hint: entry.reason,
|
|
199
|
+
});
|
|
200
|
+
} else if (entry.status === 'link') {
|
|
201
|
+
manualResults.push({
|
|
202
|
+
id: entry.id, name: entry.name, category: entry.category, method: entry.method,
|
|
203
|
+
status: 'manual', hint: `install from: ${entry.url || '(no link known)'}`,
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
runnable.push(entry);
|
|
127
207
|
}
|
|
128
208
|
}
|
|
129
209
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
210
|
+
// ── Windows runtime question (HITL finding, 0.3.0) ──────────────
|
|
211
|
+
// ADM always runs under Node. On Windows, installing Node.js spawns an
|
|
212
|
+
// MSI whose Restart Manager restarts running node.exe processes — it can
|
|
213
|
+
// kill THIS session mid-run. Owner decision: ASK, and on confirmation
|
|
214
|
+
// really install (the risk belongs to whoever answered "yes"). POSIX is
|
|
215
|
+
// safe (nvm installs into ~/.nvm, nothing restarts processes).
|
|
216
|
+
if ((env.os || process.platform) === 'win32') {
|
|
217
|
+
const nodeIdx = runnable.findIndex(e => e.id === 'node');
|
|
218
|
+
if (nodeIdx >= 0) {
|
|
219
|
+
let confirmed = false;
|
|
220
|
+
const nodeMsg = 'Installing Node.js from inside ADM can restart this session (the Windows installer restarts running node.exe). Install anyway?';
|
|
221
|
+
try {
|
|
222
|
+
// the confirm atomically replaces its question with the done-format
|
|
223
|
+
// line (single write — owner fix: no flicker on y/n)
|
|
224
|
+
confirmed = await confirm({
|
|
225
|
+
message: nodeMsg,
|
|
226
|
+
defaultValue: false,
|
|
227
|
+
answerLine: ok => ok
|
|
228
|
+
? `${chalk.green('✓')} Node.js install confirmed.`
|
|
229
|
+
: `${chalk.yellow('❗')} Node.js install declined — manual command in the report.`,
|
|
230
|
+
});
|
|
231
|
+
} catch {
|
|
232
|
+
confirmed = false; // prompt cancelled → treat as declined
|
|
233
|
+
log(`${chalk.yellow('❗')} Node.js install declined — manual command in the report.`);
|
|
234
|
+
}
|
|
235
|
+
if (!confirmed) {
|
|
236
|
+
const [entry] = runnable.splice(nodeIdx, 1);
|
|
237
|
+
manualResults.push({
|
|
238
|
+
id: entry.id, name: entry.name, category: entry.category, method: entry.method,
|
|
239
|
+
status: 'manual',
|
|
240
|
+
hint: 'close ADM, then run in a separate terminal: winget install --id OpenJS.NodeJS.LTS --exact --accept-source-agreements --accept-package-agreements',
|
|
241
|
+
});
|
|
160
242
|
}
|
|
161
243
|
}
|
|
162
|
-
} else {
|
|
163
|
-
console.log(chalk.green(' SSH key already exists'));
|
|
164
244
|
}
|
|
165
245
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
s
|
|
246
|
+
let gitOffered = false;
|
|
247
|
+
|
|
248
|
+
if (runnable.length === 0 && manualResults.length === 0) {
|
|
249
|
+
log('Nothing selected — nothing to do.');
|
|
250
|
+
} else {
|
|
251
|
+
let results = manualResults;
|
|
252
|
+
let workedLine = null;
|
|
253
|
+
|
|
254
|
+
if (runnable.length > 0) {
|
|
255
|
+
// ── 3. install with the loader per tool — the line carries the
|
|
256
|
+
// full command in parentheses, and after 3s its [elapsed] (owner decision)
|
|
257
|
+
let spinner = null;
|
|
258
|
+
let prevRows = 0; // rows the previous tool's finished status line occupies
|
|
259
|
+
const eraseRows = n => {
|
|
260
|
+
if (n > 0 && stdout.isTTY) stdout.write((n > 1 ? `\x1b[${n - 1}A` : '') + '\r\x1b[J');
|
|
261
|
+
};
|
|
262
|
+
const installStart = Date.now();
|
|
263
|
+
cursorSave(); // collapse anchor: everything below is wiped on finish
|
|
264
|
+
const outcome = await executeInstall(runnable, {
|
|
265
|
+
execFunc,
|
|
266
|
+
home,
|
|
267
|
+
rcFile: env.rcFile,
|
|
268
|
+
onStart: ({ tool }) => {
|
|
269
|
+
if (spinner) spinner.stop();
|
|
270
|
+
// owner decision: ONE install line at a time — the previous
|
|
271
|
+
// tool's status line disappears as the next tool starts
|
|
272
|
+
eraseRows(prevRows);
|
|
273
|
+
prevRows = 0;
|
|
274
|
+
spinner = spinnerFactory(`${tool.force ? 'Reinstalling' : 'Installing'} ${tool.name} (${tool.command})`);
|
|
275
|
+
spinner.start();
|
|
276
|
+
},
|
|
277
|
+
onProgress: ({ tool, result }) => {
|
|
278
|
+
if (!spinner) return;
|
|
279
|
+
if (result.status === 'installed') spinner.succeed(`${tool.name} ${tool.force ? 'reinstalled' : 'installed'}`);
|
|
280
|
+
else if (result.status === 'skipped') spinner.info(`${tool.name} already installed`);
|
|
281
|
+
else spinner.fail(`${tool.name} failed (see report below)`);
|
|
282
|
+
spinner = null;
|
|
283
|
+
prevRows = 1; // status lines are short — always exactly one row
|
|
284
|
+
},
|
|
285
|
+
onInstalled: entry => appendInstalled(entry, { configDir }),
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
workedLine = `Worked for ${fmtDuration(Date.now() - installStart)}`;
|
|
289
|
+
|
|
290
|
+
// ── 4. report assembly: ✓ installed / ✓ reinstalled / i manual ──
|
|
291
|
+
const reinstalledIds = new Set(runnable.filter(e => e.force).map(e => e.id));
|
|
292
|
+
results = [...outcome.results.map(r => {
|
|
293
|
+
if (r.status === 'installed' && reinstalledIds.has(r.id)) {
|
|
294
|
+
return { ...r, status: 'reinstalled' };
|
|
295
|
+
}
|
|
296
|
+
return r;
|
|
297
|
+
}), ...manualResults];
|
|
298
|
+
|
|
299
|
+
// The git-identity question belongs to a git/gh FRESH install — a
|
|
300
|
+
// reinstall means the user already has it configured.
|
|
301
|
+
if (outcome.installed > 0 && !declinedBefore) {
|
|
302
|
+
const GIT_TOOLS = new Set(['git', 'gh']);
|
|
303
|
+
gitOffered = results.some(r => r.status === 'installed' && GIT_TOOLS.has(r.id));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── collapse (owner decision): restore the saved cursor position and
|
|
307
|
+
// clear — the whole install process disappears EXACTLY, whatever
|
|
308
|
+
// wrapped or printed in between; the report below stays. Non-TTY
|
|
309
|
+
// (tests) never drew the lines → the save/restore is skipped there.
|
|
310
|
+
cursorRestoreClear();
|
|
179
311
|
}
|
|
312
|
+
|
|
313
|
+
const reportText = renderResults(results, { summary: false });
|
|
314
|
+
|
|
315
|
+
// persist FIRST so the report heading can carry the saved path
|
|
316
|
+
const header = [
|
|
317
|
+
`ADM install report — ${new Date().toISOString()}`,
|
|
318
|
+
`ADM ${getVersion()} · ${env.os}${env.distro ? `/${env.distro}` : ''} · manager: ${env.manager} · shell: ${env.shell}`,
|
|
319
|
+
workedLine,
|
|
320
|
+
].filter(l => l !== null);
|
|
321
|
+
const body = [...header, '', reportText].join('\n') + '\n';
|
|
322
|
+
const saved = saveReport('install', body, { configDir });
|
|
323
|
+
const savedNote = saved.file
|
|
324
|
+
? chalk.dim(` (saved: ${saved.file})`)
|
|
325
|
+
: chalk.dim(` (⚠ not saved: ${saved.error})`);
|
|
326
|
+
|
|
327
|
+
if (workedLine) log(chalk.bold(workedLine));
|
|
328
|
+
log('');
|
|
329
|
+
log(chalk.bold('Install report:') + savedNote);
|
|
330
|
+
log(reportText);
|
|
180
331
|
}
|
|
181
332
|
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
installed: true,
|
|
190
|
-
setupCompletedAt: new Date().toISOString(),
|
|
191
|
-
shell,
|
|
192
|
-
platform,
|
|
193
|
-
nodeVersion,
|
|
194
|
-
pnpm: pnpmStatus.installed || (await isPnpmInstalled()).installed,
|
|
195
|
-
tools,
|
|
196
|
-
git: { name: gitAnswers.name, email: gitAnswers.email, gpgsign: gitAnswers.gpgsign },
|
|
197
|
-
assistant,
|
|
198
|
-
dotfiles: dotfilesUrl || null,
|
|
199
|
-
};
|
|
200
|
-
await writeConfig(config);
|
|
201
|
-
|
|
202
|
-
console.log(chalk.cyan('\n Setup complete! Config saved to ~/.adm/config.json\n'));
|
|
203
|
-
return config;
|
|
204
|
-
}
|
|
333
|
+
// ── 5. git step (story 10) — only after an actual install happened ──
|
|
334
|
+
// Reviewing the list without installing anything must not nag about git;
|
|
335
|
+
// a declined setup is remembered in config.json and never asked again.
|
|
336
|
+
if (gitOffered) {
|
|
337
|
+
const gitResult = await runGitStep({ execFunc, prompt, log, configDir });
|
|
338
|
+
if (gitResult.status === 'skipped') log('OK — you can set it later in any terminal: git config --global user.name "You"');
|
|
339
|
+
}
|
|
205
340
|
|
|
206
|
-
|
|
207
|
-
return [
|
|
208
|
-
'Detect and install system package manager (brew/apt)',
|
|
209
|
-
'Install Node.js via nvm',
|
|
210
|
-
'Install pnpm',
|
|
211
|
-
'Install optional tools (Vite, esbuild, TypeScript, Docker, gh)',
|
|
212
|
-
'Configure git (user.name, user.email, gpgsign)',
|
|
213
|
-
'Generate SSH key (ED25519)',
|
|
214
|
-
'Clone dotfiles repo',
|
|
215
|
-
'Enable assistant mode',
|
|
216
|
-
'Save config to ~/.adm/config.json',
|
|
217
|
-
];
|
|
341
|
+
return 0;
|
|
218
342
|
}
|
|
219
343
|
|
|
220
|
-
module.exports = {
|
|
344
|
+
module.exports = { runWizard, collectStatuses, toolChoices };
|