fabrica-e-commerce 0.2.1 → 0.2.3

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/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 { kv, kvSuccess, kvFail, section, endSections, spinner, subBox, log, logInfo, logWarn, divider, orange, dimOrange, bold, dim, green, red, cyan, yellow } from './ui.js';
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
- // ── Vercel ────────────────────────────────────────────────────────────────────
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
- refreshWindowsToolPaths();
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
- return runPrivileged('apt-get', ['install', '-y', 'git'], { allowFailure: true });
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')) return runPrivileged('dnf', ['install', '-y', 'git'], { allowFailure: true });
92
- if (await commandExists('yum')) return runPrivileged('yum', ['install', '-y', 'git'], { allowFailure: true });
93
- if (await commandExists('pacman')) return runPrivileged('pacman', ['-Sy', '--noconfirm', 'git'], { allowFailure: true });
94
- if (await commandExists('zypper')) return runPrivileged('zypper', ['install', '-y', 'git'], { allowFailure: true });
95
- if (await commandExists('apk')) return runPrivileged('apk', ['add', 'git'], { allowFailure: true });
96
- }
97
- if (p === 'darwin') {
98
- if (await commandExists('brew')) return runCommand('brew', ['install', 'git'], { allowFailure: true });
99
- return runCommand('xcode-select', ['--install'], { allowFailure: true });
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 runCommand('winget', ['install', '--id', 'Git.Git', '-e', '--source', 'winget', '--silent'], { allowFailure: true });
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 runCommandCapture('apt-get', ['install', '-y', 'gh']);
126
- if (direct.code === 0) return;
127
- // Fall back to GitHub's official APT repo
128
- const pre = (await commandExists('sudo')) ? 'sudo ' : '';
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 gnupg`,
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')) return runPrivileged('dnf', ['install', '-y', 'gh'], { allowFailure: true });
143
- if (await commandExists('pacman')) return runPrivileged('pacman', ['-Sy', '--noconfirm', 'github-cli'], { allowFailure: true });
144
- if (await commandExists('zypper')) return runPrivileged('zypper', ['install', '-y', 'gh'], { allowFailure: true });
145
- if (await commandExists('apk')) return runCommand('apk', ['add', 'github-cli'], { allowFailure: true });
146
- }
147
- if (p === 'darwin') {
148
- if (await commandExists('brew')) return runCommand('brew', ['install', 'gh'], { allowFailure: true });
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 runCommand('winget', ['install', '--id', 'GitHub.cli', '-e', '--source', 'winget', '--silent'], { allowFailure: true });
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
- async function runPrivileged(command, args, options = {}) {
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, // via npx only
500
+ termuxPkg: null,
501
+ note: 'Runs via npx — no global install needed',
198
502
  },
199
503
  ];
200
504
 
201
- // ── ensureDependencies (shared) ───────────────────────────────────────────────
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
- spin.fail(`${dep.label} not found`);
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 npx-only, attempt inline
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) wSpin.succeed(`Vercel CLI ready via npx`);
223
- else wSpin.fail(`Vercel CLI not available — use: npx vercel`);
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 = p === 'termux' ? 'Termux / Android' : p === 'win32' ? 'Windows' : p === 'darwin' ? 'macOS' : 'Linux';
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 detected — using pkg for native installs, npx for Node tools');
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, `Missing — ${dep.description}`);
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} dependency(ies) could not be installed automatically`);
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(`Install ${dep.label}: ${cyan(`pkg install ${dep.termuxPkg}`)}`);
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(`Install ${dep.label}: ${cyan(dep.manualUrl)}`);
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
- log(green('All dependencies ready — run: fabrica build'));
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();