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/package.json +12 -5
- package/src/bridge.js +2 -2
- package/src/clean.js +184 -0
- package/src/cli.js +115 -91
- package/src/deploy.js +62 -64
- package/src/deps.js +173 -126
- package/src/github.js +4 -4
- package/src/sql.js +227 -16
- package/src/ui.js +140 -68
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 {
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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((
|
|
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
|
|
90
|
-
'2)
|
|
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+)/);
|
|
119
|
-
const a = line.match(/Aliased\s+(\S+)/);
|
|
120
|
-
const i = line.match(/Inspect\s+(\S+)/);
|
|
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
|
-
|
|
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 { /*
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
(process.platform === 'linux' &&
|
|
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
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
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((
|
|
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
|
|
32
|
+
const pf = process.env.ProgramFiles;
|
|
64
33
|
const pf86 = process.env['ProgramFiles(x86)'];
|
|
65
|
-
const pd
|
|
66
|
-
addToPathIfDirectory(lad && path.join(lad,
|
|
67
|
-
addToPathIfDirectory(pf && path.join(pf,
|
|
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,
|
|
70
|
-
addToPathIfDirectory(pf && path.join(pf,
|
|
71
|
-
addToPathIfDirectory(pf && path.join(pf,
|
|
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(
|
|
43
|
+
async function checkWithPathRefresh(cmd) {
|
|
75
44
|
refreshWindowsToolPaths();
|
|
76
|
-
return commandExists(
|
|
45
|
+
return commandExists(cmd);
|
|
77
46
|
}
|
|
78
47
|
|
|
79
48
|
async function runFirstSuccessful(candidates, options = {}) {
|
|
80
|
-
for (const [
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
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
|
|
90
|
-
['npx',
|
|
91
|
-
['npm',
|
|
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
|
|
64
|
+
return r.code === 0;
|
|
95
65
|
}
|
|
96
66
|
|
|
97
67
|
async function warmVercelCli() {
|
|
98
68
|
refreshWindowsToolPaths();
|
|
99
|
-
const
|
|
100
|
-
['npx',
|
|
101
|
-
['npm',
|
|
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 (
|
|
74
|
+
if (r.code === 0) return;
|
|
105
75
|
await runCommand('npm', ['install', '-g', 'vercel@latest'], { allowFailure: true });
|
|
106
76
|
}
|
|
107
77
|
|
|
108
|
-
// ── Git
|
|
78
|
+
// ── Git ───────────────────────────────────────────────────────────────────────
|
|
109
79
|
async function installGit() {
|
|
110
80
|
refreshWindowsToolPaths();
|
|
111
|
-
const
|
|
81
|
+
const p = platform();
|
|
112
82
|
|
|
113
|
-
if (
|
|
114
|
-
|
|
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
|
-
|
|
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 (
|
|
97
|
+
if (p === 'darwin') {
|
|
131
98
|
if (await commandExists('brew')) return runCommand('brew', ['install', 'git'], { allowFailure: true });
|
|
132
|
-
|
|
133
|
-
await runCommand('xcode-select', ['--install'], { allowFailure: true });
|
|
99
|
+
return runCommand('xcode-select', ['--install'], { allowFailure: true });
|
|
134
100
|
}
|
|
135
|
-
if (
|
|
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
|
|
114
|
+
// ── GitHub CLI ────────────────────────────────────────────────────────────────
|
|
149
115
|
async function installGithubCli() {
|
|
150
116
|
refreshWindowsToolPaths();
|
|
151
|
-
const
|
|
117
|
+
const p = platform();
|
|
152
118
|
|
|
153
|
-
if (
|
|
154
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
const
|
|
166
|
-
await
|
|
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
|
-
`${
|
|
169
|
-
`${
|
|
170
|
-
`${
|
|
171
|
-
`curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | ${
|
|
172
|
-
`${
|
|
173
|
-
`echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | ${
|
|
174
|
-
`${
|
|
175
|
-
`${
|
|
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 (
|
|
147
|
+
if (p === 'darwin') {
|
|
185
148
|
if (await commandExists('brew')) return runCommand('brew', ['install', 'gh'], { allowFailure: true });
|
|
186
149
|
}
|
|
187
|
-
if (
|
|
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
|
-
|
|
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((
|
|
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) {
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
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
|
-
|
|
251
|
+
|
|
252
|
+
section('Dependency summary');
|
|
253
|
+
divider();
|
|
226
254
|
let allGood = true;
|
|
227
|
-
const summaryLines = [];
|
|
228
255
|
for (const dep of results) {
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
}
|