fabrica-e-commerce 0.2.2 → 0.2.4
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/README.md +113 -78
- package/package.json +1 -1
- package/src/cli.js +98 -46
- package/src/deploy.js +111 -27
- package/src/deps.js +397 -61
- package/src/prompt.js +59 -22
- package/src/ui.js +37 -29
package/src/deps.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import https from 'node:https';
|
|
5
|
+
import os from 'node:os';
|
|
3
6
|
import process from 'node:process';
|
|
4
7
|
import { commandExists, runCommand, runCommandCapture } from './system.js';
|
|
5
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
kv, kvSuccess, kvFail, section, endSections, spinner,
|
|
10
|
+
subBox, log, logInfo, logWarn, divider,
|
|
11
|
+
orange, dimOrange, bold, dim, green, red, cyan, yellow,
|
|
12
|
+
} from './ui.js';
|
|
6
13
|
|
|
7
14
|
export function isTermux() {
|
|
8
15
|
return (
|
|
@@ -17,7 +24,7 @@ function platform() {
|
|
|
17
24
|
return process.platform; // win32 | linux | darwin
|
|
18
25
|
}
|
|
19
26
|
|
|
20
|
-
// ── PATH helpers
|
|
27
|
+
// ── PATH helpers ───────────────────────────────────────────────────────────────
|
|
21
28
|
function addToPathIfDirectory(directory) {
|
|
22
29
|
if (!directory || !fs.existsSync(directory)) return;
|
|
23
30
|
const current = process.env.PATH || '';
|
|
@@ -29,19 +36,50 @@ function addToPathIfDirectory(directory) {
|
|
|
29
36
|
function refreshWindowsToolPaths() {
|
|
30
37
|
if (process.platform !== 'win32') return;
|
|
31
38
|
const lad = process.env.LOCALAPPDATA;
|
|
39
|
+
const up = process.env.USERPROFILE;
|
|
32
40
|
const pf = process.env.ProgramFiles;
|
|
33
41
|
const pf86 = process.env['ProgramFiles(x86)'];
|
|
34
42
|
const pd = process.env.ProgramData;
|
|
43
|
+
// WinGet
|
|
35
44
|
addToPathIfDirectory(lad && path.join(lad, 'Microsoft', 'WinGet', 'Links'));
|
|
45
|
+
addToPathIfDirectory(lad && path.join(lad, 'Microsoft', 'WinGet', 'Packages', 'Git.Git_Microsoft.Winget.Source_8wekyb3d8bbwe', 'cmd'));
|
|
46
|
+
// GitHub CLI
|
|
36
47
|
addToPathIfDirectory(pf && path.join(pf, 'GitHub CLI'));
|
|
37
48
|
addToPathIfDirectory(pf86 && path.join(pf86, 'GitHub CLI'));
|
|
49
|
+
// Scoop
|
|
50
|
+
addToPathIfDirectory(up && path.join(up, 'scoop', 'shims'));
|
|
51
|
+
// Chocolatey
|
|
38
52
|
addToPathIfDirectory(pd && path.join(pd, 'chocolatey', 'bin'));
|
|
53
|
+
// Git
|
|
39
54
|
addToPathIfDirectory(pf && path.join(pf, 'Git', 'cmd'));
|
|
55
|
+
addToPathIfDirectory(pf86 && path.join(pf86, 'Git', 'cmd'));
|
|
56
|
+
// Node
|
|
40
57
|
addToPathIfDirectory(pf && path.join(pf, 'nodejs'));
|
|
58
|
+
addToPathIfDirectory(lad && path.join(lad, 'Programs', 'nodejs'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function refreshUnixToolPaths() {
|
|
62
|
+
// User-local bin dirs (no root required)
|
|
63
|
+
const home = os.homedir();
|
|
64
|
+
addToPathIfDirectory(path.join(home, '.local', 'bin'));
|
|
65
|
+
addToPathIfDirectory(path.join(home, 'bin'));
|
|
66
|
+
addToPathIfDirectory(path.join(home, '.cargo', 'bin'));
|
|
67
|
+
// Homebrew (macOS Intel / Linux)
|
|
68
|
+
addToPathIfDirectory('/usr/local/bin');
|
|
69
|
+
addToPathIfDirectory('/opt/homebrew/bin'); // macOS ARM
|
|
70
|
+
addToPathIfDirectory('/home/linuxbrew/.linuxbrew/bin'); // Linuxbrew
|
|
71
|
+
// Conda/Mamba
|
|
72
|
+
addToPathIfDirectory(path.join(home, 'miniconda3', 'bin'));
|
|
73
|
+
addToPathIfDirectory(path.join(home, 'anaconda3', 'bin'));
|
|
74
|
+
addToPathIfDirectory(path.join(home, 'miniforge3', 'bin'));
|
|
75
|
+
addToPathIfDirectory(path.join(home, 'mambaforge', 'bin'));
|
|
76
|
+
// nix
|
|
77
|
+
addToPathIfDirectory(path.join(home, '.nix-profile', 'bin'));
|
|
41
78
|
}
|
|
42
79
|
|
|
43
80
|
async function checkWithPathRefresh(cmd) {
|
|
44
81
|
refreshWindowsToolPaths();
|
|
82
|
+
refreshUnixToolPaths();
|
|
45
83
|
return commandExists(cmd);
|
|
46
84
|
}
|
|
47
85
|
|
|
@@ -53,9 +91,162 @@ async function runFirstSuccessful(candidates, options = {}) {
|
|
|
53
91
|
return { code: 1, stdout: '', stderr: 'All fallback commands failed' };
|
|
54
92
|
}
|
|
55
93
|
|
|
56
|
-
// ──
|
|
94
|
+
// ── Privilege helper ───────────────────────────────────────────────────────────
|
|
95
|
+
// Try without sudo first; fall back to sudo if available
|
|
96
|
+
async function runPrivileged(command, args, options = {}) {
|
|
97
|
+
const p = platform();
|
|
98
|
+
if (p === 'win32' || p === 'termux') return runCommand(command, args, options);
|
|
99
|
+
// Try without privilege first
|
|
100
|
+
const direct = await runCommandCapture(command, args, options);
|
|
101
|
+
if (direct.code === 0) return direct;
|
|
102
|
+
// Fall back to sudo if available
|
|
103
|
+
if (await commandExists('sudo')) {
|
|
104
|
+
return runCommand('sudo', [command, ...args], { ...options, allowFailure: true });
|
|
105
|
+
}
|
|
106
|
+
return runCommand(command, args, { ...options, allowFailure: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── User-local bin directory (no root) ────────────────────────────────────────
|
|
110
|
+
function userLocalBin() {
|
|
111
|
+
const home = os.homedir();
|
|
112
|
+
if (isTermux()) return process.env.PREFIX ? path.join(process.env.PREFIX, 'bin') : '/data/data/com.termux/files/usr/bin';
|
|
113
|
+
if (process.platform === 'win32') return path.join(process.env.LOCALAPPDATA || home, 'Programs', 'bin');
|
|
114
|
+
return path.join(home, '.local', 'bin');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function ensureUserLocalBin() {
|
|
118
|
+
const dir = userLocalBin();
|
|
119
|
+
try { await fsp.mkdir(dir, { recursive: true }); } catch { /* ignore */ }
|
|
120
|
+
addToPathIfDirectory(dir);
|
|
121
|
+
return dir;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── HTTPS download helper ─────────────────────────────────────────────────────
|
|
125
|
+
function downloadFile(url, dest) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const file = fs.createWriteStream(dest);
|
|
128
|
+
function get(u) {
|
|
129
|
+
https.get(u, (res) => {
|
|
130
|
+
if (res.statusCode === 301 || res.statusCode === 302) return get(res.headers.location);
|
|
131
|
+
if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
|
|
132
|
+
res.pipe(file);
|
|
133
|
+
file.on('finish', () => file.close(resolve));
|
|
134
|
+
}).on('error', reject);
|
|
135
|
+
}
|
|
136
|
+
get(url);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Scoop installer for Windows (no admin) ────────────────────────────────────
|
|
141
|
+
async function installScoop() {
|
|
142
|
+
if (process.platform !== 'win32') return false;
|
|
143
|
+
if (await commandExists('scoop')) return true;
|
|
144
|
+
// Install Scoop via PowerShell — no admin required
|
|
145
|
+
const r = await runCommandCapture('powershell', [
|
|
146
|
+
'-NoProfile', '-ExecutionPolicy', 'Bypass',
|
|
147
|
+
'-Command',
|
|
148
|
+
`Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force; ` +
|
|
149
|
+
`[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; ` +
|
|
150
|
+
`iex (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')`,
|
|
151
|
+
], { allowFailure: true });
|
|
152
|
+
if (r.code === 0) {
|
|
153
|
+
refreshWindowsToolPaths();
|
|
154
|
+
return await commandExists('scoop');
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Homebrew / Linuxbrew installer (no root) ──────────────────────────────────
|
|
160
|
+
async function installBrew() {
|
|
161
|
+
if (await commandExists('brew')) return true;
|
|
162
|
+
const r = await runCommandCapture('bash', [
|
|
163
|
+
'-c',
|
|
164
|
+
'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
|
|
165
|
+
], { allowFailure: true });
|
|
166
|
+
if (r.code === 0) {
|
|
167
|
+
refreshUnixToolPaths();
|
|
168
|
+
return await commandExists('brew');
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── GitHub release binary download (no root, Linux/macOS) ────────────────────
|
|
174
|
+
async function downloadGhBinary() {
|
|
175
|
+
if (process.platform === 'win32') return false;
|
|
176
|
+
|
|
177
|
+
const spin = spinner('Downloading gh binary from GitHub releases…');
|
|
178
|
+
try {
|
|
179
|
+
const arch = process.arch === 'arm64' ? 'arm64' : 'amd64';
|
|
180
|
+
const os_ = process.platform === 'darwin' ? 'macOS' : 'linux';
|
|
181
|
+
const ext = process.platform === 'darwin' ? 'zip' : 'tar.gz';
|
|
182
|
+
|
|
183
|
+
// Fetch latest release tag
|
|
184
|
+
const metaResult = await runCommandCapture('curl', [
|
|
185
|
+
'-fsSL', 'https://api.github.com/repos/cli/cli/releases/latest',
|
|
186
|
+
], { allowFailure: true });
|
|
187
|
+
if (metaResult.code !== 0) { spin.fail('Could not fetch gh release metadata'); return false; }
|
|
188
|
+
|
|
189
|
+
const tag = JSON.parse(metaResult.stdout || '{}').tag_name;
|
|
190
|
+
if (!tag) { spin.fail('Could not determine gh release version'); return false; }
|
|
191
|
+
const ver = tag.replace(/^v/, '');
|
|
192
|
+
|
|
193
|
+
const filename = `gh_${ver}_${os_.toLowerCase()}_${arch}.${ext}`;
|
|
194
|
+
const url = `https://github.com/cli/cli/releases/download/${tag}/${filename}`;
|
|
195
|
+
const tmp = path.join(os.tmpdir(), filename);
|
|
196
|
+
|
|
197
|
+
await downloadFile(url, tmp);
|
|
198
|
+
|
|
199
|
+
const binDir = await ensureUserLocalBin();
|
|
200
|
+
|
|
201
|
+
if (ext === 'tar.gz') {
|
|
202
|
+
await runCommandCapture('tar', ['-xzf', tmp, '-C', os.tmpdir()], { allowFailure: true });
|
|
203
|
+
const extracted = path.join(os.tmpdir(), `gh_${ver}_linux_${arch}`, 'bin', 'gh');
|
|
204
|
+
if (fs.existsSync(extracted)) {
|
|
205
|
+
await fsp.copyFile(extracted, path.join(binDir, 'gh'));
|
|
206
|
+
await fsp.chmod(path.join(binDir, 'gh'), 0o755);
|
|
207
|
+
spin.succeed(`gh binary installed to ${binDir}`);
|
|
208
|
+
refreshUnixToolPaths();
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
// macOS zip
|
|
213
|
+
await runCommandCapture('unzip', ['-o', tmp, '-d', os.tmpdir()], { allowFailure: true });
|
|
214
|
+
const extracted = path.join(os.tmpdir(), `gh_${ver}_macOS_${arch}`, 'bin', 'gh');
|
|
215
|
+
if (fs.existsSync(extracted)) {
|
|
216
|
+
await fsp.copyFile(extracted, path.join(binDir, 'gh'));
|
|
217
|
+
await fsp.chmod(path.join(binDir, 'gh'), 0o755);
|
|
218
|
+
spin.succeed(`gh binary installed to ${binDir}`);
|
|
219
|
+
refreshUnixToolPaths();
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
spin.fail('Could not extract gh binary');
|
|
224
|
+
return false;
|
|
225
|
+
} catch (e) {
|
|
226
|
+
spin.fail(`gh binary download failed: ${e.message}`);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Git portable binary download for Linux (no root) ─────────────────────────
|
|
232
|
+
async function downloadGitBinaryLinux() {
|
|
233
|
+
if (process.platform !== 'linux' || isTermux()) return false;
|
|
234
|
+
// Try conda as the most reliable no-root option on Linux
|
|
235
|
+
if (await commandExists('conda')) {
|
|
236
|
+
const r = await runCommandCapture('conda', ['install', '-y', '-c', 'conda-forge', 'git'], { allowFailure: true });
|
|
237
|
+
if (r.code === 0) { refreshUnixToolPaths(); return true; }
|
|
238
|
+
}
|
|
239
|
+
if (await commandExists('mamba')) {
|
|
240
|
+
const r = await runCommandCapture('mamba', ['install', '-y', '-c', 'conda-forge', 'git'], { allowFailure: true });
|
|
241
|
+
if (r.code === 0) { refreshUnixToolPaths(); return true; }
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Vercel ─────────────────────────────────────────────────────────────────────
|
|
57
247
|
async function checkVercelCli() {
|
|
58
248
|
refreshWindowsToolPaths();
|
|
249
|
+
refreshUnixToolPaths();
|
|
59
250
|
const r = await runFirstSuccessful([
|
|
60
251
|
['npx', ['--yes', 'vercel@latest', '--version']],
|
|
61
252
|
['npm', ['exec', '--yes', 'vercel@latest', '--', '--version']],
|
|
@@ -65,45 +256,107 @@ async function checkVercelCli() {
|
|
|
65
256
|
}
|
|
66
257
|
|
|
67
258
|
async function warmVercelCli() {
|
|
68
|
-
|
|
259
|
+
// Vercel CLI is always available via npx — no install needed
|
|
69
260
|
const r = await runFirstSuccessful([
|
|
70
261
|
['npx', ['--yes', 'vercel@latest', '--version']],
|
|
71
262
|
['npm', ['exec', '--yes', 'vercel@latest', '--', '--version']],
|
|
72
263
|
['vercel', ['--version']],
|
|
73
264
|
]);
|
|
74
265
|
if (r.code === 0) return;
|
|
266
|
+
// Last resort: global install
|
|
75
267
|
await runCommand('npm', ['install', '-g', 'vercel@latest'], { allowFailure: true });
|
|
76
268
|
}
|
|
77
269
|
|
|
78
|
-
// ── Git
|
|
270
|
+
// ── Git installer ──────────────────────────────────────────────────────────────
|
|
79
271
|
async function installGit() {
|
|
80
272
|
refreshWindowsToolPaths();
|
|
273
|
+
refreshUnixToolPaths();
|
|
81
274
|
const p = platform();
|
|
82
275
|
|
|
276
|
+
// ── Termux (no root) ────────────────────────────────────────────────────────
|
|
83
277
|
if (p === 'termux') {
|
|
84
278
|
return runCommand('pkg', ['install', '-y', 'git'], { allowFailure: true });
|
|
85
279
|
}
|
|
280
|
+
|
|
281
|
+
// ── macOS ────────────────────────────────────────────────────────────────────
|
|
282
|
+
if (p === 'darwin') {
|
|
283
|
+
if (await commandExists('brew')) {
|
|
284
|
+
return runCommand('brew', ['install', 'git'], { allowFailure: true });
|
|
285
|
+
}
|
|
286
|
+
// Install Homebrew (no root) then git
|
|
287
|
+
const brewOk = await installBrew();
|
|
288
|
+
if (brewOk) return runCommand('brew', ['install', 'git'], { allowFailure: true });
|
|
289
|
+
// Xcode tools as final fallback
|
|
290
|
+
return runCommand('xcode-select', ['--install'], { allowFailure: true });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Linux ────────────────────────────────────────────────────────────────────
|
|
86
294
|
if (p === 'linux') {
|
|
295
|
+
// 1. Try Homebrew/Linuxbrew (no root)
|
|
296
|
+
if (await commandExists('brew')) {
|
|
297
|
+
const r = await runCommandCapture('brew', ['install', 'git'], { allowFailure: true });
|
|
298
|
+
if (r.code === 0) { refreshUnixToolPaths(); return; }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 2. Try system package managers (with or without root)
|
|
87
302
|
if (await commandExists('apt-get')) {
|
|
88
303
|
await runPrivileged('apt-get', ['update', '-qq'], { allowFailure: true });
|
|
89
|
-
|
|
304
|
+
const r = await runPrivileged('apt-get', ['install', '-y', 'git'], { allowFailure: true });
|
|
305
|
+
if (await commandExists('git')) return;
|
|
90
306
|
}
|
|
91
|
-
if (await commandExists('dnf'))
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (await commandExists('
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
307
|
+
if (await commandExists('dnf')) {
|
|
308
|
+
await runPrivileged('dnf', ['install', '-y', 'git'], { allowFailure: true });
|
|
309
|
+
if (await commandExists('git')) return;
|
|
310
|
+
}
|
|
311
|
+
if (await commandExists('yum')) {
|
|
312
|
+
await runPrivileged('yum', ['install', '-y', 'git'], { allowFailure: true });
|
|
313
|
+
if (await commandExists('git')) return;
|
|
314
|
+
}
|
|
315
|
+
if (await commandExists('pacman')) {
|
|
316
|
+
await runPrivileged('pacman', ['-Sy', '--noconfirm', 'git'], { allowFailure: true });
|
|
317
|
+
if (await commandExists('git')) return;
|
|
318
|
+
}
|
|
319
|
+
if (await commandExists('zypper')) {
|
|
320
|
+
await runPrivileged('zypper', ['install', '-y', 'git'], { allowFailure: true });
|
|
321
|
+
if (await commandExists('git')) return;
|
|
322
|
+
}
|
|
323
|
+
if (await commandExists('apk')) {
|
|
324
|
+
await runPrivileged('apk', ['add', 'git'], { allowFailure: true });
|
|
325
|
+
if (await commandExists('git')) return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 3. Try conda/mamba (no root)
|
|
329
|
+
await downloadGitBinaryLinux();
|
|
330
|
+
|
|
331
|
+
// 4. Install Linuxbrew as last resort (no root)
|
|
332
|
+
const brewOk = await installBrew();
|
|
333
|
+
if (brewOk) {
|
|
334
|
+
await runCommand('brew', ['install', 'git'], { allowFailure: true });
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
100
337
|
}
|
|
338
|
+
|
|
339
|
+
// ── Windows ───────────────────────────────────────────────────────────────────
|
|
101
340
|
if (p === 'win32') {
|
|
341
|
+
// 1. WinGet (no admin for user-scope installs)
|
|
102
342
|
if (await commandExists('winget')) {
|
|
103
|
-
await
|
|
343
|
+
const r = await runCommandCapture('winget', [
|
|
344
|
+
'install', '--id', 'Git.Git', '-e', '--source', 'winget',
|
|
345
|
+
'--scope', 'user', '--silent', '--accept-package-agreements', '--accept-source-agreements',
|
|
346
|
+
], { allowFailure: true });
|
|
347
|
+
refreshWindowsToolPaths();
|
|
348
|
+
if (await commandExists('git')) return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 2. Scoop (no admin)
|
|
352
|
+
const scoopOk = await commandExists('scoop') || await installScoop();
|
|
353
|
+
if (scoopOk) {
|
|
354
|
+
await runCommand('scoop', ['install', 'git'], { allowFailure: true });
|
|
104
355
|
refreshWindowsToolPaths();
|
|
105
|
-
return;
|
|
356
|
+
if (await commandExists('git')) return;
|
|
106
357
|
}
|
|
358
|
+
|
|
359
|
+
// 3. Chocolatey (may need admin, try anyway)
|
|
107
360
|
if (await commandExists('choco')) {
|
|
108
361
|
await runCommand('choco', ['install', 'git', '-y'], { allowFailure: true });
|
|
109
362
|
refreshWindowsToolPaths();
|
|
@@ -111,25 +364,49 @@ async function installGit() {
|
|
|
111
364
|
}
|
|
112
365
|
}
|
|
113
366
|
|
|
114
|
-
// ── GitHub CLI
|
|
367
|
+
// ── GitHub CLI installer ───────────────────────────────────────────────────────
|
|
115
368
|
async function installGithubCli() {
|
|
116
369
|
refreshWindowsToolPaths();
|
|
370
|
+
refreshUnixToolPaths();
|
|
117
371
|
const p = platform();
|
|
118
372
|
|
|
373
|
+
// ── Termux (no root) ────────────────────────────────────────────────────────
|
|
119
374
|
if (p === 'termux') {
|
|
120
375
|
return runCommand('pkg', ['install', '-y', 'gh'], { allowFailure: true });
|
|
121
376
|
}
|
|
377
|
+
|
|
378
|
+
// ── macOS ────────────────────────────────────────────────────────────────────
|
|
379
|
+
if (p === 'darwin') {
|
|
380
|
+
if (await commandExists('brew')) {
|
|
381
|
+
return runCommand('brew', ['install', 'gh'], { allowFailure: true });
|
|
382
|
+
}
|
|
383
|
+
const brewOk = await installBrew();
|
|
384
|
+
if (brewOk) return runCommand('brew', ['install', 'gh'], { allowFailure: true });
|
|
385
|
+
// Binary download fallback
|
|
386
|
+
await downloadGhBinary();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ── Linux ────────────────────────────────────────────────────────────────────
|
|
122
391
|
if (p === 'linux') {
|
|
392
|
+
// 1. Try Homebrew/Linuxbrew (no root)
|
|
393
|
+
if (await commandExists('brew')) {
|
|
394
|
+
const r = await runCommandCapture('brew', ['install', 'gh'], { allowFailure: true });
|
|
395
|
+
if (r.code === 0) { refreshUnixToolPaths(); return; }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 2. Try system package managers
|
|
123
399
|
if (await commandExists('apt-get')) {
|
|
124
400
|
// Try direct first (gh is in Ubuntu 22.04+ repos)
|
|
125
|
-
const direct = await
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
401
|
+
const direct = await runPrivileged('apt-get', ['install', '-y', 'gh'], { allowFailure: true });
|
|
402
|
+
if (await commandExists('gh')) return;
|
|
403
|
+
|
|
404
|
+
// GitHub's official APT repo
|
|
405
|
+
const hasSudo = await commandExists('sudo');
|
|
406
|
+
const pre = hasSudo ? 'sudo ' : '';
|
|
129
407
|
await runCommand('bash', ['-c', [
|
|
130
|
-
'set -e',
|
|
131
408
|
`${pre}apt-get update -qq`,
|
|
132
|
-
`${pre}apt-get install -y curl ca-certificates
|
|
409
|
+
`${pre}apt-get install -y curl ca-certificates`,
|
|
133
410
|
`${pre}mkdir -p -m 755 /etc/apt/keyrings`,
|
|
134
411
|
`curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | ${pre}tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null`,
|
|
135
412
|
`${pre}chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg`,
|
|
@@ -137,22 +414,51 @@ async function installGithubCli() {
|
|
|
137
414
|
`${pre}apt-get update -qq`,
|
|
138
415
|
`${pre}apt-get install -y gh`,
|
|
139
416
|
].join(' && ')], { allowFailure: true });
|
|
140
|
-
return;
|
|
417
|
+
if (await commandExists('gh')) return;
|
|
141
418
|
}
|
|
142
|
-
if (await commandExists('dnf'))
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
419
|
+
if (await commandExists('dnf')) {
|
|
420
|
+
await runPrivileged('dnf', ['install', '-y', 'gh'], { allowFailure: true });
|
|
421
|
+
if (await commandExists('gh')) return;
|
|
422
|
+
}
|
|
423
|
+
if (await commandExists('pacman')) {
|
|
424
|
+
await runPrivileged('pacman', ['-Sy', '--noconfirm', 'github-cli'], { allowFailure: true });
|
|
425
|
+
if (await commandExists('gh')) return;
|
|
426
|
+
}
|
|
427
|
+
if (await commandExists('zypper')) {
|
|
428
|
+
await runPrivileged('zypper', ['install', '-y', 'gh'], { allowFailure: true });
|
|
429
|
+
if (await commandExists('gh')) return;
|
|
430
|
+
}
|
|
431
|
+
if (await commandExists('apk')) {
|
|
432
|
+
await runCommandCapture('apk', ['add', 'github-cli'], { allowFailure: true });
|
|
433
|
+
if (await commandExists('gh')) return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 3. Binary download (no root, works everywhere)
|
|
437
|
+
await downloadGhBinary();
|
|
438
|
+
return;
|
|
149
439
|
}
|
|
440
|
+
|
|
441
|
+
// ── Windows ───────────────────────────────────────────────────────────────────
|
|
150
442
|
if (p === 'win32') {
|
|
443
|
+
// 1. WinGet user-scope (no admin)
|
|
151
444
|
if (await commandExists('winget')) {
|
|
152
|
-
await
|
|
445
|
+
await runCommandCapture('winget', [
|
|
446
|
+
'install', '--id', 'GitHub.cli', '-e', '--source', 'winget',
|
|
447
|
+
'--scope', 'user', '--silent', '--accept-package-agreements', '--accept-source-agreements',
|
|
448
|
+
], { allowFailure: true });
|
|
449
|
+
refreshWindowsToolPaths();
|
|
450
|
+
if (await commandExists('gh')) return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 2. Scoop (no admin)
|
|
454
|
+
const scoopOk = await commandExists('scoop') || await installScoop();
|
|
455
|
+
if (scoopOk) {
|
|
456
|
+
await runCommand('scoop', ['install', 'gh'], { allowFailure: true });
|
|
153
457
|
refreshWindowsToolPaths();
|
|
154
|
-
return;
|
|
458
|
+
if (await commandExists('gh')) return;
|
|
155
459
|
}
|
|
460
|
+
|
|
461
|
+
// 3. Chocolatey
|
|
156
462
|
if (await commandExists('choco')) {
|
|
157
463
|
await runCommand('choco', ['install', 'gh', '-y'], { allowFailure: true });
|
|
158
464
|
refreshWindowsToolPaths();
|
|
@@ -160,14 +466,7 @@ async function installGithubCli() {
|
|
|
160
466
|
}
|
|
161
467
|
}
|
|
162
468
|
|
|
163
|
-
|
|
164
|
-
const p = platform();
|
|
165
|
-
if (p === 'win32' || p === 'termux') return runCommand(command, args, options);
|
|
166
|
-
if (await commandExists('sudo')) return runCommand('sudo', [command, ...args], options);
|
|
167
|
-
return runCommand(command, args, options);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ── dependency table ──────────────────────────────────────────────────────────
|
|
469
|
+
// ── dependency table ───────────────────────────────────────────────────────────
|
|
171
470
|
const DEPENDENCIES = [
|
|
172
471
|
{
|
|
173
472
|
name: 'git',
|
|
@@ -177,6 +476,8 @@ const DEPENDENCIES = [
|
|
|
177
476
|
install: installGit,
|
|
178
477
|
manualUrl: 'https://git-scm.com/downloads',
|
|
179
478
|
termuxPkg: 'git',
|
|
479
|
+
winScoopPkg: 'git',
|
|
480
|
+
brewPkg: 'git',
|
|
180
481
|
},
|
|
181
482
|
{
|
|
182
483
|
name: 'gh',
|
|
@@ -186,6 +487,8 @@ const DEPENDENCIES = [
|
|
|
186
487
|
install: installGithubCli,
|
|
187
488
|
manualUrl: 'https://github.com/cli/cli#installation',
|
|
188
489
|
termuxPkg: 'gh',
|
|
490
|
+
winScoopPkg: 'gh',
|
|
491
|
+
brewPkg: 'gh',
|
|
189
492
|
},
|
|
190
493
|
{
|
|
191
494
|
name: 'vercel',
|
|
@@ -194,14 +497,16 @@ const DEPENDENCIES = [
|
|
|
194
497
|
check: checkVercelCli,
|
|
195
498
|
install: warmVercelCli,
|
|
196
499
|
manualUrl: 'https://vercel.com/docs/cli',
|
|
197
|
-
termuxPkg: null,
|
|
500
|
+
termuxPkg: null,
|
|
501
|
+
note: 'Runs via npx — no global install needed',
|
|
198
502
|
},
|
|
199
503
|
];
|
|
200
504
|
|
|
201
|
-
// ── ensureDependencies
|
|
505
|
+
// ── ensureDependencies ─────────────────────────────────────────────────────────
|
|
202
506
|
export async function ensureDependencies({ autoInstall = true, names } = {}) {
|
|
203
507
|
const targets = names ? DEPENDENCIES.filter((d) => names.includes(d.name)) : DEPENDENCIES;
|
|
204
508
|
const results = [];
|
|
509
|
+
|
|
205
510
|
for (const dep of targets) {
|
|
206
511
|
const spin = spinner(`Checking ${dep.label}…`);
|
|
207
512
|
let present = await dep.check();
|
|
@@ -210,43 +515,66 @@ export async function ensureDependencies({ autoInstall = true, names } = {}) {
|
|
|
210
515
|
results.push({ ...dep, present, installed: false });
|
|
211
516
|
continue;
|
|
212
517
|
}
|
|
213
|
-
|
|
518
|
+
|
|
519
|
+
spin.fail(`${dep.label} not found — attempting auto-install`);
|
|
214
520
|
if (!autoInstall) { results.push({ ...dep, present: false, installed: false }); continue; }
|
|
215
521
|
|
|
216
522
|
const p = platform();
|
|
523
|
+
const installSpin = spinner(`Installing ${dep.label}…`);
|
|
524
|
+
|
|
217
525
|
if (p === 'termux' && dep.termuxPkg === null) {
|
|
218
|
-
// Vercel on Termux
|
|
219
|
-
const wSpin = spinner(`Warming Vercel CLI via npx (Termux)…`);
|
|
526
|
+
// Vercel on Termux via npx only
|
|
220
527
|
await dep.install();
|
|
221
528
|
present = await dep.check();
|
|
222
|
-
if (present)
|
|
223
|
-
else
|
|
529
|
+
if (present) installSpin.succeed(`${green(dep.label)} ready via npx`);
|
|
530
|
+
else installSpin.fail(`${red(dep.label)} not available — run: npx vercel`);
|
|
224
531
|
} else {
|
|
225
|
-
const installSpin = spinner(`Auto-installing ${dep.label}…`);
|
|
226
532
|
await dep.install();
|
|
533
|
+
refreshWindowsToolPaths();
|
|
534
|
+
refreshUnixToolPaths();
|
|
227
535
|
present = await dep.check();
|
|
228
536
|
if (present) installSpin.succeed(`${green(dep.label)} installed successfully`);
|
|
229
537
|
else installSpin.fail(`${red(dep.label)} could not be installed automatically`);
|
|
230
538
|
}
|
|
539
|
+
|
|
231
540
|
results.push({ ...dep, present, installed: present });
|
|
232
541
|
}
|
|
233
542
|
return results;
|
|
234
543
|
}
|
|
235
544
|
|
|
236
|
-
// ── vins command
|
|
545
|
+
// ── vins command ───────────────────────────────────────────────────────────────
|
|
237
546
|
export async function vinsCommand() {
|
|
238
547
|
const p = platform();
|
|
239
|
-
const platformLabel =
|
|
548
|
+
const platformLabel =
|
|
549
|
+
p === 'termux' ? 'Termux / Android' :
|
|
550
|
+
p === 'win32' ? 'Windows' :
|
|
551
|
+
p === 'darwin' ? 'macOS' : 'Linux';
|
|
552
|
+
|
|
553
|
+
const hasSudo = process.platform !== 'win32' && !isTermux() && await commandExists('sudo');
|
|
554
|
+
const hasBrew = await commandExists('brew');
|
|
555
|
+
const hasScoop = process.platform === 'win32' && await commandExists('scoop');
|
|
556
|
+
const hasWinget = process.platform === 'win32' && await commandExists('winget');
|
|
557
|
+
const hasConda = await commandExists('conda') || await commandExists('mamba');
|
|
240
558
|
|
|
241
559
|
section('System info');
|
|
242
560
|
kv('Platform', platformLabel);
|
|
243
561
|
kv('Node.js', process.version);
|
|
244
562
|
kv('Arch', process.arch);
|
|
563
|
+
divider();
|
|
564
|
+
kv('Sudo', hasSudo ? green('available') : dim('not available'));
|
|
565
|
+
kv('Homebrew', hasBrew ? green('available') : dim('not detected'));
|
|
566
|
+
if (process.platform === 'win32') {
|
|
567
|
+
kv('WinGet', hasWinget ? green('available') : dim('not detected'));
|
|
568
|
+
kv('Scoop', hasScoop ? green('available') : dim('not detected — will auto-install if needed'));
|
|
569
|
+
}
|
|
570
|
+
if (hasConda) kv('Conda', green('available'));
|
|
245
571
|
if (p === 'termux') {
|
|
246
|
-
logInfo('Termux
|
|
572
|
+
logInfo('Termux — using pkg for all installs (no root required)');
|
|
573
|
+
} else if (!hasSudo && p === 'linux') {
|
|
574
|
+
logInfo('No sudo detected — will use Homebrew, conda, or binary download');
|
|
247
575
|
}
|
|
248
576
|
|
|
249
|
-
section('Checking dependencies');
|
|
577
|
+
section('Checking & installing dependencies');
|
|
250
578
|
const results = await ensureDependencies();
|
|
251
579
|
|
|
252
580
|
section('Dependency summary');
|
|
@@ -254,9 +582,9 @@ export async function vinsCommand() {
|
|
|
254
582
|
let allGood = true;
|
|
255
583
|
for (const dep of results) {
|
|
256
584
|
if (dep.present) {
|
|
257
|
-
kvSuccess(dep.label, dep.description);
|
|
585
|
+
kvSuccess(dep.label, dep.description + (dep.note ? ` ${dim('·')} ${dim(dep.note)}` : ''));
|
|
258
586
|
} else {
|
|
259
|
-
kvFail(dep.label,
|
|
587
|
+
kvFail(dep.label, dep.description);
|
|
260
588
|
allGood = false;
|
|
261
589
|
}
|
|
262
590
|
}
|
|
@@ -264,17 +592,25 @@ export async function vinsCommand() {
|
|
|
264
592
|
|
|
265
593
|
const missing = results.filter((d) => !d.present);
|
|
266
594
|
if (missing.length) {
|
|
267
|
-
logWarn(`${missing.length}
|
|
595
|
+
logWarn(`${missing.length} tool(s) could not be installed automatically`);
|
|
596
|
+
log(dim('Manual install instructions:'));
|
|
268
597
|
for (const dep of missing) {
|
|
269
598
|
if (p === 'termux' && dep.termuxPkg) {
|
|
270
|
-
log(`
|
|
599
|
+
log(` ${orange(dep.label)}: ${cyan(`pkg install ${dep.termuxPkg}`)}`);
|
|
600
|
+
} else if (p === 'win32') {
|
|
601
|
+
log(` ${orange(dep.label)}: ${cyan(`scoop install ${dep.winScoopPkg || dep.name}`)}`);
|
|
602
|
+
log(` ${dim('or')} ${cyan(dep.manualUrl)}`);
|
|
603
|
+
} else if (p === 'linux' && !hasSudo) {
|
|
604
|
+
log(` ${orange(dep.label)}: ${cyan(`brew install ${dep.brewPkg || dep.name}`)}`);
|
|
605
|
+
log(` ${dim('or')} ${cyan(dep.manualUrl)}`);
|
|
271
606
|
} else {
|
|
272
|
-
log(`
|
|
607
|
+
log(` ${orange(dep.label)}: ${cyan(dep.manualUrl)}`);
|
|
273
608
|
}
|
|
274
609
|
}
|
|
275
|
-
log('Then re-run: fabrica vins');
|
|
610
|
+
log(dim('Then re-run: npx fabrica-e-commerce vins'));
|
|
276
611
|
} else {
|
|
277
|
-
|
|
612
|
+
logInfo(green('All dependencies ready'));
|
|
613
|
+
log(`Run ${cyan('npx fabrica-e-commerce build')} to deploy your store`);
|
|
278
614
|
}
|
|
279
615
|
|
|
280
616
|
endSections();
|