fabrica-e-commerce 0.1.16 → 0.2.1

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/deploy.js CHANGED
@@ -4,7 +4,7 @@ import crypto from 'node:crypto';
4
4
  import { HARDCODED_ENV, REQUIRED_ENV_KEYS, STORE_REPO } from './config.js';
5
5
  import { buildsDir, saveProject } from './store.js';
6
6
  import { runCommand, runCommandCapture, openUrl } from './system.js';
7
- import { dimOrange, kv, section, spinner, subBox, red, orange } from './ui.js';
7
+ import { kv, section, endSections, spinner, subBox, log, red, dimOrange } from './ui.js';
8
8
  import { ask, choose } from './prompt.js';
9
9
 
10
10
  const VERCEL_ENVIRONMENTS = ['production', 'preview', 'development'];
@@ -12,15 +12,23 @@ const VERCEL_ENVIRONMENTS = ['production', 'preview', 'development'];
12
12
  export async function collectEnv(supabase) {
13
13
  section('Environment variables');
14
14
  const env = { ...HARDCODED_ENV, SUPABASE_URL: supabase.url, SUPABASE_ANON_KEY: supabase.anonKey };
15
- for (const key of REQUIRED_ENV_KEYS) env[key] = await ask(`${key}`);
15
+ for (const key of REQUIRED_ENV_KEYS) {
16
+ endSections(); // flush so the ask() prompt appears cleanly
17
+ env[key] = await ask(`${key}`);
18
+ section('Environment variables');
19
+ }
16
20
  return env;
17
21
  }
18
22
 
19
23
  export async function cloneRepo() {
24
+ endSections();
20
25
  section('Clone storefront');
21
26
  const projectName = await ask('Vercel project name', `fabrica-store-${Date.now()}`);
27
+ section('Clone storefront');
22
28
  const id = crypto.randomUUID();
23
29
  const target = path.join(buildsDir, `${projectName}-${id.slice(0, 8)}`);
30
+ // git clone output goes directly to inherited stdio — show it outside the box
31
+ endSections();
24
32
  await runCommand('git', ['clone', STORE_REPO, target]);
25
33
  return { id, projectName, target };
26
34
  }
@@ -38,20 +46,21 @@ export async function ensureVercelLogin() {
38
46
  return;
39
47
  }
40
48
  spin.fail('Not logged in to Vercel');
41
- console.log(' Opening "vercel login" — finish the login in your browser...');
49
+ log('Opening "vercel login" — finish the login in your browser...');
50
+ endSections();
42
51
  await runCommand('npx', ['vercel@latest', 'login']);
52
+ section('Vercel login');
43
53
  if (!(await isLoggedInToVercel())) {
44
54
  throw new Error('Vercel login was not completed. Run "fabrica build" again after logging in.');
45
55
  }
46
56
  kv('Vercel', 'Logged in');
47
57
  }
48
58
 
49
- // Capture vercel CLI output and render it in a sub-box, errors in red
59
+ // Capture vercel CLI output and add it into the current section buffer as a subBox
50
60
  async function runVercelBoxed(args, options = {}) {
51
61
  const result = await runCommandCapture('npx', ['vercel@latest', ...args], options);
52
62
  const raw = ((result.stdout || '') + (result.stderr || '')).trim();
53
63
  if (!raw) return result;
54
-
55
64
  const lines = raw.split('\n').map((l) => l.trimEnd()).filter(Boolean);
56
65
  const isErr = result.code !== 0 || lines.some((l) => /^Error:/i.test(l));
57
66
  subBox(lines, { isError: isErr });
@@ -73,21 +82,18 @@ async function connectGithubRepo(project, repoUrl) {
73
82
  const spin = spinner(`Connecting Vercel project to ${repoUrl}`);
74
83
  const attempts = 6;
75
84
  let lastError = '';
76
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
85
+ for (let attempt = 1; attempt <= attempts; attempt++) {
77
86
  const connect = await runCommandCapture('npx', ['--yes', 'vercel@latest', 'git', 'connect', repoUrl, '--yes'], { cwd: project.target });
78
- if (connect.code === 0) {
79
- spin.succeed('Vercel project connected to GitHub repo');
80
- return true;
81
- }
87
+ if (connect.code === 0) { spin.succeed('Vercel project connected to GitHub repo'); return true; }
82
88
  lastError = (connect.stderr || connect.stdout || '').trim();
83
- if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, attempt * 3000));
89
+ if (attempt < attempts) await new Promise((r) => setTimeout(r, attempt * 3000));
84
90
  }
85
91
  spin.fail('Could not auto-connect Git — continuing with a direct deploy');
86
92
  subBox([
87
93
  'This usually means one of two things:',
88
94
  `1) Vercel hasn't finished indexing the new fork yet`,
89
- ` Re-link manually: npx vercel git connect ${repoUrl}`,
90
- '2) The "Vercel" GitHub App is set to "Only select repositories"',
95
+ ` Re-link: npx vercel git connect ${repoUrl}`,
96
+ '2) Vercel GitHub App set to "Only select repositories"',
91
97
  ' Fix at: https://github.com/settings/installations',
92
98
  lastError ? `Last error: ${lastError}` : '',
93
99
  ].filter(Boolean), { isError: true });
@@ -97,6 +103,7 @@ async function connectGithubRepo(project, repoUrl) {
97
103
  export async function deployToVercel(project, env, githubRepo) {
98
104
  section('Vercel deployment');
99
105
  await ensureVercelLogin();
106
+ section('Vercel deployment');
100
107
 
101
108
  await runVercelBoxed(['link', '--yes', '--project', project.projectName], { cwd: project.target });
102
109
 
@@ -110,24 +117,23 @@ export async function deployToVercel(project, env, githubRepo) {
110
117
  const deployResult = await runCommandCapture('npx', ['vercel@latest', '--prod', '--yes'], { cwd: project.target });
111
118
  const deployOutput = ((deployResult.stdout || '') + (deployResult.stderr || '')).trim();
112
119
 
113
- // Extract URLs from deploy output
114
120
  let productionUrl = null;
115
121
  let aliasedUrl = null;
116
122
  let inspectUrl = null;
117
123
  for (const line of deployOutput.split('\n')) {
118
- const m = line.match(/Production\s+(\S+)/); if (m) productionUrl = m[1];
119
- const a = line.match(/Aliased\s+(\S+)/); if (a) aliasedUrl = a[1];
120
- const i = line.match(/Inspect\s+(\S+)/); if (i) inspectUrl = i[1];
124
+ const m = line.match(/Production\s+(\S+)/); if (m) productionUrl = m[1];
125
+ const a = line.match(/Aliased\s+(\S+)/); if (a) aliasedUrl = a[1];
126
+ const i = line.match(/Inspect\s+(\S+)/); if (i) inspectUrl = i[1];
121
127
  }
122
128
 
123
129
  subBox(deployOutput.split('\n').filter(Boolean));
124
130
  deploySpin.succeed('Production deployment created');
125
131
 
126
- // Auto-open the aliased/production URL
127
132
  const openTarget = aliasedUrl || productionUrl;
128
133
  if (openTarget) {
129
134
  kv('Opening', openTarget);
130
- await openUrl(openTarget);
135
+ // open after flush
136
+ process.nextTick(() => openUrl(openTarget));
131
137
  }
132
138
 
133
139
  const record = {
@@ -146,60 +152,20 @@ export async function deployToVercel(project, env, githubRepo) {
146
152
  return record;
147
153
  }
148
154
 
149
- export async function runLocally(project, env) {
150
- section('Local Next.js setup');
151
- const envPath = path.join(project.target, '.env.local');
152
- const contents = Object.entries(env).map(([key, value]) => `${key}=${String(value).replace(/\n/g, '\\n')}`).join('\n') + '\n';
153
- await fs.writeFile(envPath, contents, 'utf8');
154
- kv('Env file', envPath);
155
-
156
- const hasPnpmLock = await fs.access(path.join(project.target, 'pnpm-lock.yaml')).then(() => true, () => false);
157
- const pnpmAvailable = (await runCommandCapture('pnpm', ['--version'])).code === 0;
158
- const installCommand = hasPnpmLock && pnpmAvailable ? ['pnpm', ['install']] : ['npm', ['install']];
159
- const devCommand = hasPnpmLock && pnpmAvailable ? ['pnpm', ['run', 'dev']] : ['npm', ['run', 'dev']];
160
-
161
- await runCommand(installCommand[0], installCommand[1], { cwd: project.target });
162
-
163
- const record = {
164
- ...project,
165
- type: 'local',
166
- repo: STORE_REPO,
167
- githubRepo: null,
168
- createdAt: new Date().toISOString(),
169
- envKeys: Object.keys(env),
170
- env,
171
- supabaseUrl: env.SUPABASE_URL,
172
- productionUrl: 'http://localhost:3000',
173
- };
174
- await saveProject(record);
175
-
176
- section('Local app');
177
- kv('Path', project.target);
178
- kv('URL', 'http://localhost:3000');
179
- console.log(' Starting the Next.js dev server. Press Ctrl+C to stop.');
180
-
181
- // Auto-open after a brief delay so server has time to start
182
- setTimeout(() => openUrl('http://localhost:3000'), 3000);
183
- await runCommand(devCommand[0], devCommand[1], { cwd: project.target });
184
- }
185
-
186
- // Called by env command - update a single env var for cloud or local project
187
155
  export async function updateProjectEnv(project, key, value) {
188
156
  if (project.type === 'local') {
189
- // Update .env.local
190
157
  const envPath = path.join(project.target, '.env.local');
191
158
  let contents = '';
192
- try { contents = await fs.readFile(envPath, 'utf8'); } catch { /* no file yet */ }
159
+ try { contents = await fs.readFile(envPath, 'utf8'); } catch { /* new file */ }
193
160
  const lines = contents.split('\n');
194
161
  const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
195
- const newLine = `${key}=${value}`;
196
- if (idx >= 0) lines[idx] = newLine;
197
- else lines.push(newLine);
162
+ if (idx >= 0) lines[idx] = `${key}=${value}`;
163
+ else lines.push(`${key}=${value}`);
198
164
  await fs.writeFile(envPath, lines.join('\n'), 'utf8');
199
165
  kv('Updated', `${key} in .env.local`);
200
166
  } else {
201
- // Cloud: update on Vercel + redeploy
202
167
  await ensureVercelLogin();
168
+ section('Applying update');
203
169
  for (const environment of VERCEL_ENVIRONMENTS) {
204
170
  await runVercelBoxed(['env', 'rm', key, environment, '--yes'], { cwd: project.target, allowFailure: true });
205
171
  await runVercelBoxed(['env', 'add', key, environment], { cwd: project.target, input: `${value}\n` });
@@ -208,7 +174,39 @@ export async function updateProjectEnv(project, key, value) {
208
174
  await runVercelBoxed(['--prod', '--yes'], { cwd: project.target });
209
175
  spin.succeed(`Redeployed ${project.projectName}`);
210
176
  }
211
- // Persist updated env in store
212
177
  const updated = { ...project, env: { ...(project.env || {}), [key]: value } };
213
178
  await saveProject(updated);
214
179
  }
180
+
181
+ export async function runLocally(project, env) {
182
+ section('Local setup');
183
+ const envPath = path.join(project.target, '.env.local');
184
+ const contents = Object.entries(env).map(([k, v]) => `${k}=${String(v).replace(/\n/g, '\\n')}`).join('\n') + '\n';
185
+ await fs.writeFile(envPath, contents, 'utf8');
186
+ kv('Env file', envPath);
187
+
188
+ const hasPnpmLock = await fs.access(path.join(project.target, 'pnpm-lock.yaml')).then(() => true, () => false);
189
+ const pnpmAvailable = (await runCommandCapture('pnpm', ['--version'])).code === 0;
190
+ const installCmd = hasPnpmLock && pnpmAvailable ? ['pnpm', ['install']] : ['npm', ['install']];
191
+ const devCmd = hasPnpmLock && pnpmAvailable ? ['pnpm', ['run', 'dev']] : ['npm', ['run', 'dev']];
192
+
193
+ const record = {
194
+ ...project, type: 'local', repo: STORE_REPO, githubRepo: null,
195
+ createdAt: new Date().toISOString(), envKeys: Object.keys(env),
196
+ env, supabaseUrl: env.SUPABASE_URL, productionUrl: 'http://localhost:3000',
197
+ };
198
+ await saveProject(record);
199
+
200
+ kv('URL', 'http://localhost:3000');
201
+ log('Installing dependencies...');
202
+ endSections();
203
+ await runCommand(installCmd[0], installCmd[1], { cwd: project.target });
204
+
205
+ section('Local app');
206
+ kv('Path', project.target);
207
+ kv('URL', 'http://localhost:3000');
208
+ log('Starting dev server — browser opens in 3s...');
209
+ endSections();
210
+ setTimeout(() => openUrl('http://localhost:3000'), 3000);
211
+ await runCommand(devCmd[0], devCmd[1], { cwd: project.target });
212
+ }
package/src/deps.js CHANGED
@@ -2,124 +2,91 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import process from 'node:process';
4
4
  import { commandExists, runCommand, runCommandCapture } from './system.js';
5
- import { kv, section, spinner, subBox } from './ui.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';
6
6
 
7
- // ── platform detection ────────────────────────────────────────────────────────
8
- function isTermux() {
9
- return process.platform === 'android' ||
10
- (process.platform === 'linux' && (process.env.TERMUX_VERSION || process.env.PREFIX?.includes('com.termux')));
7
+ export function isTermux() {
8
+ return (
9
+ process.platform === 'android' ||
10
+ (process.platform === 'linux' &&
11
+ (process.env.TERMUX_VERSION || process.env.PREFIX?.includes('com.termux')))
12
+ );
11
13
  }
12
14
 
13
- async function runPrivileged(command, args, options = {}) {
14
- if (process.platform === 'win32') return runCommand(command, args, options);
15
- if (isTermux()) return runCommand(command, args, options); // Termux: no sudo
16
- if (await commandExists('sudo')) return runCommand('sudo', [command, ...args], options);
17
- return runCommand(command, args, options);
18
- }
19
-
20
- function runPrivilegedShell(script, options = {}) {
21
- return runCommand('bash', ['-c', script], options);
15
+ function platform() {
16
+ if (isTermux()) return 'termux';
17
+ return process.platform; // win32 | linux | darwin
22
18
  }
23
19
 
24
- // ── dependency definitions ────────────────────────────────────────────────────
25
- const DEPENDENCIES = [
26
- {
27
- name: 'git',
28
- label: 'Git',
29
- check: () => checkWithPathRefresh('git'),
30
- install: installGit,
31
- manualUrl: 'https://git-scm.com/downloads'
32
- },
33
- {
34
- name: 'gh',
35
- label: 'GitHub CLI (gh)',
36
- check: () => checkWithPathRefresh('gh'),
37
- install: installGithubCli,
38
- manualUrl: 'https://github.com/cli/cli#installation'
39
- },
40
- {
41
- name: 'vercel',
42
- label: 'Vercel CLI (via npx)',
43
- check: checkVercelCli,
44
- install: warmVercelCli,
45
- manualUrl: 'https://vercel.com/docs/cli'
46
- }
47
- ];
48
-
49
20
  // ── PATH helpers ──────────────────────────────────────────────────────────────
50
21
  function addToPathIfDirectory(directory) {
51
22
  if (!directory || !fs.existsSync(directory)) return;
52
- const delimiter = path.delimiter;
53
23
  const current = process.env.PATH || '';
54
- const entries = current.split(delimiter).filter(Boolean);
55
- if (!entries.some((entry) => entry.toLowerCase() === directory.toLowerCase())) {
56
- process.env.PATH = `${directory}${delimiter}${current}`;
57
- }
24
+ const entries = current.split(path.delimiter).filter(Boolean);
25
+ if (!entries.some((e) => e.toLowerCase() === directory.toLowerCase()))
26
+ process.env.PATH = `${directory}${path.delimiter}${current}`;
58
27
  }
59
28
 
60
29
  function refreshWindowsToolPaths() {
61
30
  if (process.platform !== 'win32') return;
62
31
  const lad = process.env.LOCALAPPDATA;
63
- const pf = process.env.ProgramFiles;
32
+ const pf = process.env.ProgramFiles;
64
33
  const pf86 = process.env['ProgramFiles(x86)'];
65
- const pd = process.env.ProgramData;
66
- addToPathIfDirectory(lad && path.join(lad, 'Microsoft', 'WinGet', 'Links'));
67
- addToPathIfDirectory(pf && path.join(pf, 'GitHub CLI'));
34
+ const pd = process.env.ProgramData;
35
+ addToPathIfDirectory(lad && path.join(lad, 'Microsoft', 'WinGet', 'Links'));
36
+ addToPathIfDirectory(pf && path.join(pf, 'GitHub CLI'));
68
37
  addToPathIfDirectory(pf86 && path.join(pf86, 'GitHub CLI'));
69
- addToPathIfDirectory(pd && path.join(pd, 'chocolatey', 'bin'));
70
- addToPathIfDirectory(pf && path.join(pf, 'Git', 'cmd'));
71
- addToPathIfDirectory(pf && path.join(pf, 'nodejs'));
38
+ addToPathIfDirectory(pd && path.join(pd, 'chocolatey', 'bin'));
39
+ addToPathIfDirectory(pf && path.join(pf, 'Git', 'cmd'));
40
+ addToPathIfDirectory(pf && path.join(pf, 'nodejs'));
72
41
  }
73
42
 
74
- async function checkWithPathRefresh(command) {
43
+ async function checkWithPathRefresh(cmd) {
75
44
  refreshWindowsToolPaths();
76
- return commandExists(command);
45
+ return commandExists(cmd);
77
46
  }
78
47
 
79
48
  async function runFirstSuccessful(candidates, options = {}) {
80
- for (const [command, args] of candidates) {
81
- const result = await runCommandCapture(command, args, options);
82
- if (result.code === 0) return result;
49
+ for (const [cmd, args] of candidates) {
50
+ const r = await runCommandCapture(cmd, args, options);
51
+ if (r.code === 0) return r;
83
52
  }
84
53
  return { code: 1, stdout: '', stderr: 'All fallback commands failed' };
85
54
  }
86
55
 
56
+ // ── Vercel ────────────────────────────────────────────────────────────────────
87
57
  async function checkVercelCli() {
88
58
  refreshWindowsToolPaths();
89
- const result = await runFirstSuccessful([
90
- ['npx', ['--yes', 'vercel@latest', '--version']],
91
- ['npm', ['exec', '--yes', 'vercel@latest', '--', '--version']],
92
- ['vercel', ['--version']]
59
+ const r = await runFirstSuccessful([
60
+ ['npx', ['--yes', 'vercel@latest', '--version']],
61
+ ['npm', ['exec', '--yes', 'vercel@latest', '--', '--version']],
62
+ ['vercel', ['--version']],
93
63
  ]);
94
- return result.code === 0;
64
+ return r.code === 0;
95
65
  }
96
66
 
97
67
  async function warmVercelCli() {
98
68
  refreshWindowsToolPaths();
99
- const warmed = await runFirstSuccessful([
100
- ['npx', ['--yes', 'vercel@latest', '--version']],
101
- ['npm', ['exec', '--yes', 'vercel@latest', '--', '--version']],
102
- ['vercel', ['--version']]
69
+ const r = await runFirstSuccessful([
70
+ ['npx', ['--yes', 'vercel@latest', '--version']],
71
+ ['npm', ['exec', '--yes', 'vercel@latest', '--', '--version']],
72
+ ['vercel', ['--version']],
103
73
  ]);
104
- if (warmed.code === 0) return;
74
+ if (r.code === 0) return;
105
75
  await runCommand('npm', ['install', '-g', 'vercel@latest'], { allowFailure: true });
106
76
  }
107
77
 
108
- // ── Git install ───────────────────────────────────────────────────────────────
78
+ // ── Git ───────────────────────────────────────────────────────────────────────
109
79
  async function installGit() {
110
80
  refreshWindowsToolPaths();
111
- const platform = process.platform;
81
+ const p = platform();
112
82
 
113
- if (isTermux()) {
114
- await runCommand('pkg', ['install', '-y', 'git'], { allowFailure: true });
115
- return;
83
+ if (p === 'termux') {
84
+ return runCommand('pkg', ['install', '-y', 'git'], { allowFailure: true });
116
85
  }
117
-
118
- if (platform === 'linux') {
86
+ if (p === 'linux') {
119
87
  if (await commandExists('apt-get')) {
120
- await runPrivileged('apt-get', ['update'], { allowFailure: true });
121
- await runPrivileged('apt-get', ['install', '-y', 'git'], { allowFailure: true });
122
- return;
88
+ await runPrivileged('apt-get', ['update', '-qq'], { allowFailure: true });
89
+ return runPrivileged('apt-get', ['install', '-y', 'git'], { allowFailure: true });
123
90
  }
124
91
  if (await commandExists('dnf')) return runPrivileged('dnf', ['install', '-y', 'git'], { allowFailure: true });
125
92
  if (await commandExists('yum')) return runPrivileged('yum', ['install', '-y', 'git'], { allowFailure: true });
@@ -127,14 +94,13 @@ async function installGit() {
127
94
  if (await commandExists('zypper')) return runPrivileged('zypper', ['install', '-y', 'git'], { allowFailure: true });
128
95
  if (await commandExists('apk')) return runPrivileged('apk', ['add', 'git'], { allowFailure: true });
129
96
  }
130
- if (platform === 'darwin') {
97
+ if (p === 'darwin') {
131
98
  if (await commandExists('brew')) return runCommand('brew', ['install', 'git'], { allowFailure: true });
132
- // Xcode command line tools
133
- await runCommand('xcode-select', ['--install'], { allowFailure: true });
99
+ return runCommand('xcode-select', ['--install'], { allowFailure: true });
134
100
  }
135
- if (platform === 'win32') {
101
+ if (p === 'win32') {
136
102
  if (await commandExists('winget')) {
137
- await runCommand('winget', ['install', '--id', 'Git.Git', '-e', '--source', 'winget'], { allowFailure: true });
103
+ await runCommand('winget', ['install', '--id', 'Git.Git', '-e', '--source', 'winget', '--silent'], { allowFailure: true });
138
104
  refreshWindowsToolPaths();
139
105
  return;
140
106
  }
@@ -145,35 +111,32 @@ async function installGit() {
145
111
  }
146
112
  }
147
113
 
148
- // ── GitHub CLI install ────────────────────────────────────────────────────────
114
+ // ── GitHub CLI ────────────────────────────────────────────────────────────────
149
115
  async function installGithubCli() {
150
116
  refreshWindowsToolPaths();
151
- const platform = process.platform;
117
+ const p = platform();
152
118
 
153
- if (isTermux()) {
154
- await runCommand('pkg', ['install', '-y', 'gh'], { allowFailure: true });
155
- return;
119
+ if (p === 'termux') {
120
+ return runCommand('pkg', ['install', '-y', 'gh'], { allowFailure: true });
156
121
  }
157
-
158
- if (platform === 'linux') {
122
+ if (p === 'linux') {
159
123
  if (await commandExists('apt-get')) {
160
- const direct = await (await commandExists('sudo')
161
- ? runCommandCapture('sudo', ['apt-get', 'install', '-y', 'gh'])
162
- : runCommandCapture('apt-get', ['install', '-y', 'gh']));
124
+ // Try direct first (gh is in Ubuntu 22.04+ repos)
125
+ const direct = await runCommandCapture('apt-get', ['install', '-y', 'gh']);
163
126
  if (direct.code === 0) return;
164
- const usesSudo = await commandExists('sudo');
165
- const prefix = usesSudo ? 'sudo ' : '';
166
- await runPrivilegedShell([
127
+ // Fall back to GitHub's official APT repo
128
+ const pre = (await commandExists('sudo')) ? 'sudo ' : '';
129
+ await runCommand('bash', ['-c', [
167
130
  'set -e',
168
- `${prefix}apt-get update`,
169
- `${prefix}apt-get install -y curl ca-certificates gnupg`,
170
- `${prefix}mkdir -p -m 755 /etc/apt/keyrings`,
171
- `curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | ${prefix}tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null`,
172
- `${prefix}chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg`,
173
- `echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | ${prefix}tee /etc/apt/sources.list.d/github-cli.list > /dev/null`,
174
- `${prefix}apt-get update`,
175
- `${prefix}apt-get install -y gh`
176
- ].join(' && '), { allowFailure: true });
131
+ `${pre}apt-get update -qq`,
132
+ `${pre}apt-get install -y curl ca-certificates gnupg`,
133
+ `${pre}mkdir -p -m 755 /etc/apt/keyrings`,
134
+ `curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | ${pre}tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null`,
135
+ `${pre}chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg`,
136
+ `echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | ${pre}tee /etc/apt/sources.list.d/github-cli.list > /dev/null`,
137
+ `${pre}apt-get update -qq`,
138
+ `${pre}apt-get install -y gh`,
139
+ ].join(' && ')], { allowFailure: true });
177
140
  return;
178
141
  }
179
142
  if (await commandExists('dnf')) return runPrivileged('dnf', ['install', '-y', 'gh'], { allowFailure: true });
@@ -181,12 +144,12 @@ async function installGithubCli() {
181
144
  if (await commandExists('zypper')) return runPrivileged('zypper', ['install', '-y', 'gh'], { allowFailure: true });
182
145
  if (await commandExists('apk')) return runCommand('apk', ['add', 'github-cli'], { allowFailure: true });
183
146
  }
184
- if (platform === 'darwin') {
147
+ if (p === 'darwin') {
185
148
  if (await commandExists('brew')) return runCommand('brew', ['install', 'gh'], { allowFailure: true });
186
149
  }
187
- if (platform === 'win32') {
150
+ if (p === 'win32') {
188
151
  if (await commandExists('winget')) {
189
- await runCommand('winget', ['install', '--id', 'GitHub.cli', '-e', '--source', 'winget'], { allowFailure: true });
152
+ await runCommand('winget', ['install', '--id', 'GitHub.cli', '-e', '--source', 'winget', '--silent'], { allowFailure: true });
190
153
  refreshWindowsToolPaths();
191
154
  return;
192
155
  }
@@ -197,40 +160,124 @@ async function installGithubCli() {
197
160
  }
198
161
  }
199
162
 
200
- // ── Public API ────────────────────────────────────────────────────────────────
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 ──────────────────────────────────────────────────────────
171
+ const DEPENDENCIES = [
172
+ {
173
+ name: 'git',
174
+ label: 'Git',
175
+ description: 'Version control — clones the storefront repo',
176
+ check: () => checkWithPathRefresh('git'),
177
+ install: installGit,
178
+ manualUrl: 'https://git-scm.com/downloads',
179
+ termuxPkg: 'git',
180
+ },
181
+ {
182
+ name: 'gh',
183
+ label: 'GitHub CLI',
184
+ description: 'Creates your GitHub repo and manages auth',
185
+ check: () => checkWithPathRefresh('gh'),
186
+ install: installGithubCli,
187
+ manualUrl: 'https://github.com/cli/cli#installation',
188
+ termuxPkg: 'gh',
189
+ },
190
+ {
191
+ name: 'vercel',
192
+ label: 'Vercel CLI',
193
+ description: 'Deploys the storefront to Vercel cloud',
194
+ check: checkVercelCli,
195
+ install: warmVercelCli,
196
+ manualUrl: 'https://vercel.com/docs/cli',
197
+ termuxPkg: null, // via npx only
198
+ },
199
+ ];
200
+
201
+ // ── ensureDependencies (shared) ───────────────────────────────────────────────
201
202
  export async function ensureDependencies({ autoInstall = true, names } = {}) {
202
- const targets = names ? DEPENDENCIES.filter((dep) => names.includes(dep.name)) : DEPENDENCIES;
203
+ const targets = names ? DEPENDENCIES.filter((d) => names.includes(d.name)) : DEPENDENCIES;
203
204
  const results = [];
204
205
  for (const dep of targets) {
205
- const spin = spinner(`Checking ${dep.label}`);
206
+ const spin = spinner(`Checking ${dep.label}…`);
206
207
  let present = await dep.check();
207
- if (present) { spin.succeed(`${dep.label} found`); results.push({ ...dep, present, installed: false }); continue; }
208
- spin.fail(`${dep.label} missing`);
208
+ if (present) {
209
+ spin.succeed(`${green(dep.label)} is ready`);
210
+ results.push({ ...dep, present, installed: false });
211
+ continue;
212
+ }
213
+ spin.fail(`${dep.label} not found`);
209
214
  if (!autoInstall) { results.push({ ...dep, present: false, installed: false }); continue; }
210
- const installSpin = spinner(`Installing ${dep.label}`);
211
- await dep.install();
212
- present = await dep.check();
213
- if (present) installSpin.succeed(`${dep.label} installed`);
214
- else installSpin.fail(`${dep.label} could not be installed automatically`);
215
+
216
+ const p = platform();
217
+ if (p === 'termux' && dep.termuxPkg === null) {
218
+ // Vercel on Termux — npx-only, attempt inline
219
+ const wSpin = spinner(`Warming Vercel CLI via npx (Termux)…`);
220
+ await dep.install();
221
+ 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`);
224
+ } else {
225
+ const installSpin = spinner(`Auto-installing ${dep.label}…`);
226
+ await dep.install();
227
+ present = await dep.check();
228
+ if (present) installSpin.succeed(`${green(dep.label)} installed successfully`);
229
+ else installSpin.fail(`${red(dep.label)} could not be installed automatically`);
230
+ }
215
231
  results.push({ ...dep, present, installed: present });
216
232
  }
217
233
  return results;
218
234
  }
219
235
 
236
+ // ── vins command ──────────────────────────────────────────────────────────────
220
237
  export async function vinsCommand() {
221
- section('Fabrica dependency check (vins)');
222
- const platform = isTermux() ? 'Termux/Android' : process.platform;
223
- kv('Platform', platform);
238
+ const p = platform();
239
+ const platformLabel = p === 'termux' ? 'Termux / Android' : p === 'win32' ? 'Windows' : p === 'darwin' ? 'macOS' : 'Linux';
240
+
241
+ section('System info');
242
+ kv('Platform', platformLabel);
243
+ kv('Node.js', process.version);
244
+ kv('Arch', process.arch);
245
+ if (p === 'termux') {
246
+ logInfo('Termux detected — using pkg for native installs, npx for Node tools');
247
+ }
248
+
249
+ section('Checking dependencies');
224
250
  const results = await ensureDependencies();
225
- section('Summary');
251
+
252
+ section('Dependency summary');
253
+ divider();
226
254
  let allGood = true;
227
- const summaryLines = [];
228
255
  for (const dep of results) {
229
- summaryLines.push(`${dep.label}: ${dep.present ? '✓ OK' : '✗ MISSING — install manually'}`);
230
- if (!dep.present) { allGood = false; summaryLines.push(` Manual: ${dep.manualUrl}`); }
256
+ if (dep.present) {
257
+ kvSuccess(dep.label, dep.description);
258
+ } else {
259
+ kvFail(dep.label, `Missing — ${dep.description}`);
260
+ allGood = false;
261
+ }
231
262
  }
232
- subBox(summaryLines, { isError: !allGood });
233
- if (allGood) console.log('\n All dependencies ready. Run: fabrica build');
234
- else { console.log('\n Some deps could not be installed automatically.'); process.exitCode = 1; }
263
+ divider();
264
+
265
+ const missing = results.filter((d) => !d.present);
266
+ if (missing.length) {
267
+ logWarn(`${missing.length} dependency(ies) could not be installed automatically`);
268
+ for (const dep of missing) {
269
+ if (p === 'termux' && dep.termuxPkg) {
270
+ log(`Install ${dep.label}: ${cyan(`pkg install ${dep.termuxPkg}`)}`);
271
+ } else {
272
+ log(`Install ${dep.label}: ${cyan(dep.manualUrl)}`);
273
+ }
274
+ }
275
+ log('Then re-run: fabrica vins');
276
+ } else {
277
+ log(green('All dependencies ready — run: fabrica build'));
278
+ }
279
+
280
+ endSections();
281
+ if (!allGood) process.exitCode = 1;
235
282
  return results;
236
283
  }