fabrica-e-commerce 0.1.16 → 0.1.17
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 +1 -1
- package/src/bridge.js +2 -2
- package/src/cli.js +67 -73
- package/src/deploy.js +62 -64
- package/src/deps.js +61 -122
- package/src/github.js +4 -4
- package/src/ui.js +90 -45
package/package.json
CHANGED
package/src/bridge.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BRIDGE_API_KEY, BRIDGE_ORIGIN, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from './config.js';
|
|
2
2
|
import { FULL_SQL } from './sql.js';
|
|
3
3
|
import { openUrl } from './system.js';
|
|
4
|
-
import { spinner } from './ui.js';
|
|
4
|
+
import { spinner, log } from './ui.js';
|
|
5
5
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
6
|
export async function connectSupabase() {
|
|
7
7
|
const spin = spinner('Posting secure schema job to Fabrica Connect');
|
|
@@ -9,7 +9,7 @@ export async function connectSupabase() {
|
|
|
9
9
|
if (!response.ok) throw new Error(`Bridge rejected job: ${response.status} ${await response.text()}`);
|
|
10
10
|
const job = await response.json();
|
|
11
11
|
spin.succeed('Bridge job created');
|
|
12
|
-
|
|
12
|
+
log(`Open this URL if your browser does not start: ${job.connectUrl}`);
|
|
13
13
|
await openUrl(job.connectUrl);
|
|
14
14
|
const started = Date.now();
|
|
15
15
|
const poll = spinner('Waiting for Supabase authorization and project selection');
|
package/src/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { ensureDependencies, vinsCommand } from './deps.js';
|
|
|
9
9
|
import { BRIDGE_ORIGIN, STORE_REPO } from './config.js';
|
|
10
10
|
import { dataDir, readProjects } from './store.js';
|
|
11
11
|
import { choose, ask } from './prompt.js';
|
|
12
|
-
import { banner, help, kv, section, subBox,
|
|
12
|
+
import { banner, help, kv, section, endSections, subBox, log } from './ui.js';
|
|
13
13
|
import { openUrl } from './system.js';
|
|
14
14
|
|
|
15
15
|
async function packageVersion() {
|
|
@@ -29,6 +29,7 @@ async function build() {
|
|
|
29
29
|
kv('BRIDGE', 'ONLINE');
|
|
30
30
|
kv('SQL', 'Prepared securely (hidden from UI)');
|
|
31
31
|
const supabase = await connectSupabase();
|
|
32
|
+
|
|
32
33
|
const env = await collectEnv(supabase);
|
|
33
34
|
const project = await cloneRepo();
|
|
34
35
|
|
|
@@ -40,6 +41,7 @@ async function build() {
|
|
|
40
41
|
|
|
41
42
|
if (target === 'local') {
|
|
42
43
|
await runLocally(project, env);
|
|
44
|
+
endSections();
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -53,114 +55,119 @@ async function build() {
|
|
|
53
55
|
kv('GitHub repo', record.githubRepo || 'n/a');
|
|
54
56
|
kv('URL', record.productionUrl || 'n/a');
|
|
55
57
|
kv('Path', record.target);
|
|
58
|
+
endSections();
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
// ── list ──────────────────────────────────────────────────────────────────────
|
|
59
62
|
async function list() {
|
|
60
63
|
banner();
|
|
61
64
|
const projects = await readProjects();
|
|
65
|
+
|
|
66
|
+
section('Your Fabrica projects');
|
|
62
67
|
if (!projects.length) {
|
|
63
|
-
|
|
68
|
+
log('No projects found. Run: fabrica build');
|
|
69
|
+
endSections();
|
|
64
70
|
return;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
section('Your Fabrica projects');
|
|
68
73
|
const choices = projects.map((p) => ({
|
|
69
74
|
name: `${p.projectName} (${p.type === 'local' ? 'local' : 'cloud'})`,
|
|
70
75
|
value: p.id,
|
|
71
76
|
}));
|
|
72
77
|
|
|
78
|
+
endSections(); // flush before interactive prompt
|
|
73
79
|
const selected = await choose('Select a project to view details:', choices);
|
|
74
80
|
const project = projects.find((p) => p.id === selected);
|
|
75
81
|
|
|
76
82
|
section(`Project: ${project.projectName}`);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
`Env keys: ${(project.envKeys || []).join(', ')}`,
|
|
86
|
-
];
|
|
87
|
-
subBox(lines);
|
|
83
|
+
kv('Type', project.type === 'local' ? 'local' : 'cloud');
|
|
84
|
+
kv('Created', project.createdAt);
|
|
85
|
+
kv('Path', project.target);
|
|
86
|
+
kv('GitHub', project.githubRepo || 'n/a');
|
|
87
|
+
kv('Supabase', project.supabaseUrl || 'n/a');
|
|
88
|
+
kv('URL', project.productionUrl || 'n/a');
|
|
89
|
+
kv('Env keys', (project.envKeys || []).join(', '));
|
|
90
|
+
endSections();
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
// ── env ───────────────────────────────────────────────────────────────────────
|
|
91
94
|
async function env() {
|
|
92
95
|
banner();
|
|
93
96
|
const projects = await readProjects();
|
|
97
|
+
|
|
98
|
+
section('Environment manager');
|
|
94
99
|
if (!projects.length) {
|
|
95
|
-
|
|
100
|
+
log('No projects found. Run: fabrica build');
|
|
101
|
+
endSections();
|
|
96
102
|
return;
|
|
97
103
|
}
|
|
104
|
+
endSections();
|
|
98
105
|
|
|
99
|
-
section('Environment manager');
|
|
100
|
-
|
|
101
|
-
// Step 1 — pick project type filter
|
|
102
106
|
const typeFilter = await choose('Which projects to show?', [
|
|
103
|
-
{ name: 'All projects',
|
|
104
|
-
{ name: 'Local projects only',
|
|
105
|
-
{ name: 'Cloud (Vercel) projects only',
|
|
107
|
+
{ name: 'All projects', value: 'all' },
|
|
108
|
+
{ name: 'Local projects only', value: 'local' },
|
|
109
|
+
{ name: 'Cloud (Vercel) projects only', value: 'cloud' },
|
|
106
110
|
]);
|
|
107
111
|
|
|
108
112
|
const filtered = typeFilter === 'all' ? projects : projects.filter((p) => p.type === typeFilter);
|
|
109
113
|
if (!filtered.length) {
|
|
110
|
-
|
|
114
|
+
section('Environment manager');
|
|
115
|
+
log(`No ${typeFilter} projects found.`);
|
|
116
|
+
endSections();
|
|
111
117
|
return;
|
|
112
118
|
}
|
|
113
119
|
|
|
114
|
-
// Step 2 — pick project
|
|
115
120
|
const projectId = await choose('Select project:', filtered.map((p) => ({
|
|
116
121
|
name: `${p.projectName} (${p.type === 'local' ? 'local' : 'cloud'})`,
|
|
117
122
|
value: p.id,
|
|
118
123
|
})));
|
|
119
124
|
const project = filtered.find((p) => p.id === projectId);
|
|
120
125
|
|
|
121
|
-
// Step 3 — pick env key
|
|
122
126
|
const envKeys = project.envKeys || [];
|
|
123
127
|
if (!envKeys.length) {
|
|
124
|
-
|
|
128
|
+
section('Environment manager');
|
|
129
|
+
log('No env keys stored for this project.');
|
|
130
|
+
endSections();
|
|
125
131
|
return;
|
|
126
132
|
}
|
|
127
133
|
|
|
128
134
|
const key = await choose('Select env variable to update:', envKeys.map((k) => ({ name: k, value: k })));
|
|
129
|
-
|
|
130
|
-
// Step 4 — new value
|
|
131
135
|
const currentVal = (project.env || {})[key];
|
|
132
136
|
const value = await ask(`New value for ${key}`, currentVal || '');
|
|
133
137
|
|
|
134
138
|
section('Applying update');
|
|
135
139
|
await updateProjectEnv(project, key, value);
|
|
136
140
|
kv('Updated', `${key} → ${project.type === 'local' ? '.env.local' : 'Vercel + redeployed'}`);
|
|
141
|
+
endSections();
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
// ── rerun ─────────────────────────────────────────────────────────────────────
|
|
140
145
|
async function rerun() {
|
|
141
146
|
banner();
|
|
142
147
|
const projects = await readProjects();
|
|
148
|
+
|
|
149
|
+
section('Re-run / re-open project');
|
|
143
150
|
if (!projects.length) {
|
|
144
|
-
|
|
151
|
+
log('No projects found. Run: fabrica build');
|
|
152
|
+
endSections();
|
|
145
153
|
return;
|
|
146
154
|
}
|
|
155
|
+
endSections();
|
|
147
156
|
|
|
148
|
-
section('Re-run / re-open project');
|
|
149
|
-
|
|
150
|
-
// Step 1 — local or cloud
|
|
151
157
|
const typeFilter = await choose('Which type of project?', [
|
|
152
|
-
{ name: 'All projects',
|
|
153
|
-
{ name: 'Local projects',
|
|
158
|
+
{ name: 'All projects', value: 'all' },
|
|
159
|
+
{ name: 'Local projects', value: 'local' },
|
|
154
160
|
{ name: 'Cloud (Vercel) projects', value: 'cloud' },
|
|
155
161
|
]);
|
|
156
162
|
|
|
157
163
|
const filtered = typeFilter === 'all' ? projects : projects.filter((p) => p.type === typeFilter);
|
|
158
164
|
if (!filtered.length) {
|
|
159
|
-
|
|
165
|
+
section('Re-run / re-open project');
|
|
166
|
+
log(`No ${typeFilter} projects found.`);
|
|
167
|
+
endSections();
|
|
160
168
|
return;
|
|
161
169
|
}
|
|
162
170
|
|
|
163
|
-
// Step 2 — pick project
|
|
164
171
|
const projectId = await choose('Select project to re-run:', filtered.map((p) => ({
|
|
165
172
|
name: `${p.projectName} (${p.type === 'local' ? 'local' : 'cloud'})`,
|
|
166
173
|
value: p.id,
|
|
@@ -170,37 +177,27 @@ async function rerun() {
|
|
|
170
177
|
section(`Re-running: ${project.projectName}`);
|
|
171
178
|
|
|
172
179
|
if (project.type === 'local') {
|
|
173
|
-
// Re-run local dev server
|
|
174
180
|
kv('Path', project.target);
|
|
175
|
-
kv('URL',
|
|
181
|
+
kv('URL', 'http://localhost:3000');
|
|
182
|
+
log('Starting dev server — browser opens in 3s...');
|
|
183
|
+
endSections();
|
|
176
184
|
|
|
177
|
-
const { runCommand } = await import('./system.js');
|
|
185
|
+
const { runCommand, runCommandCapture } = await import('./system.js');
|
|
178
186
|
const { access } = await import('node:fs/promises');
|
|
179
187
|
const hasPnpmLock = await access(path.join(project.target, 'pnpm-lock.yaml')).then(() => true, () => false);
|
|
180
|
-
const { runCommandCapture } = await import('./system.js');
|
|
181
188
|
const pnpmAvailable = (await runCommandCapture('pnpm', ['--version'])).code === 0;
|
|
182
189
|
const devCommand = hasPnpmLock && pnpmAvailable ? ['pnpm', ['run', 'dev']] : ['npm', ['run', 'dev']];
|
|
183
|
-
|
|
184
|
-
console.log(' Starting dev server... auto-opening browser in 3s');
|
|
185
190
|
setTimeout(() => openUrl('http://localhost:3000'), 3000);
|
|
186
191
|
await runCommand(devCommand[0], devCommand[1], { cwd: project.target });
|
|
187
192
|
} else {
|
|
188
|
-
// Cloud: show info + open URL
|
|
189
193
|
const url = project.productionUrl || null;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
subBox(lines);
|
|
198
|
-
if (url) {
|
|
199
|
-
kv('Opening', url);
|
|
200
|
-
await openUrl(url);
|
|
201
|
-
} else {
|
|
202
|
-
console.log(' No URL found for this project.');
|
|
203
|
-
}
|
|
194
|
+
kv('GitHub', project.githubRepo || 'n/a');
|
|
195
|
+
kv('URL', url || 'n/a');
|
|
196
|
+
kv('Inspect', project.inspectUrl || 'n/a');
|
|
197
|
+
kv('Created', project.createdAt);
|
|
198
|
+
if (url) { log(`Opening ${url} ...`); }
|
|
199
|
+
endSections();
|
|
200
|
+
if (url) await openUrl(url);
|
|
204
201
|
}
|
|
205
202
|
}
|
|
206
203
|
|
|
@@ -208,28 +205,25 @@ async function rerun() {
|
|
|
208
205
|
async function info() {
|
|
209
206
|
banner();
|
|
210
207
|
section('Package info');
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
'Creator: SPARROW AI SOLUTION',
|
|
219
|
-
];
|
|
220
|
-
subBox(lines);
|
|
208
|
+
kv('Package', `fabrica-e-commerce v${await packageVersion()}`);
|
|
209
|
+
kv('Bridge', BRIDGE_ORIGIN);
|
|
210
|
+
kv('Store repo', STORE_REPO);
|
|
211
|
+
kv('Local data', dataDir);
|
|
212
|
+
kv('Node', process.version);
|
|
213
|
+
kv('Creator', 'SPARROW AI SOLUTION');
|
|
214
|
+
endSections();
|
|
221
215
|
}
|
|
222
216
|
|
|
223
217
|
// ── router ────────────────────────────────────────────────────────────────────
|
|
224
218
|
export async function run(args) {
|
|
225
219
|
const command = args[0] || 'help';
|
|
226
|
-
if (command === 'build')
|
|
227
|
-
if (command === 'list')
|
|
228
|
-
if (command === 'env')
|
|
229
|
-
if (command === 'rerun')
|
|
230
|
-
if (command === 'info' || command === '.info')
|
|
231
|
-
if (command === 'vins' || command === '/vins')
|
|
232
|
-
if (command === 'help' || command === '--help' || command === '-h')
|
|
220
|
+
if (command === 'build') return build();
|
|
221
|
+
if (command === 'list') return list();
|
|
222
|
+
if (command === 'env') return env();
|
|
223
|
+
if (command === 'rerun') return rerun();
|
|
224
|
+
if (command === 'info' || command === '.info') return info();
|
|
225
|
+
if (command === 'vins' || command === '/vins') return vinsCommand();
|
|
226
|
+
if (command === 'help' || command === '--help' || command === '-h') return help();
|
|
233
227
|
console.error(`Unknown command: ${command}`);
|
|
234
228
|
help();
|
|
235
229
|
process.exitCode = 1;
|
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,17 +2,15 @@ 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, section, endSections, spinner, subBox, log } from './ui.js';
|
|
6
6
|
|
|
7
|
-
// ── platform detection ────────────────────────────────────────────────────────
|
|
8
7
|
function isTermux() {
|
|
9
8
|
return process.platform === 'android' ||
|
|
10
9
|
(process.platform === 'linux' && (process.env.TERMUX_VERSION || process.env.PREFIX?.includes('com.termux')));
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
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
|
|
13
|
+
if (process.platform === 'win32' || isTermux()) return runCommand(command, args, options);
|
|
16
14
|
if (await commandExists('sudo')) return runCommand('sudo', [command, ...args], options);
|
|
17
15
|
return runCommand(command, args, options);
|
|
18
16
|
}
|
|
@@ -21,158 +19,105 @@ function runPrivilegedShell(script, options = {}) {
|
|
|
21
19
|
return runCommand('bash', ['-c', script], options);
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
// ── dependency definitions ────────────────────────────────────────────────────
|
|
25
22
|
const DEPENDENCIES = [
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
}
|
|
23
|
+
{ name: 'git', label: 'Git', check: () => checkWithPathRefresh('git'), install: installGit, manualUrl: 'https://git-scm.com/downloads' },
|
|
24
|
+
{ name: 'gh', label: 'GitHub CLI (gh)', check: () => checkWithPathRefresh('gh'), install: installGithubCli, manualUrl: 'https://github.com/cli/cli#installation' },
|
|
25
|
+
{ name: 'vercel', label: 'Vercel CLI (via npx)', check: checkVercelCli, install: warmVercelCli, manualUrl: 'https://vercel.com/docs/cli' },
|
|
47
26
|
];
|
|
48
27
|
|
|
49
|
-
// ── PATH helpers ──────────────────────────────────────────────────────────────
|
|
50
28
|
function addToPathIfDirectory(directory) {
|
|
51
29
|
if (!directory || !fs.existsSync(directory)) return;
|
|
52
|
-
const delimiter = path.delimiter;
|
|
53
30
|
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
|
-
}
|
|
31
|
+
const entries = current.split(path.delimiter).filter(Boolean);
|
|
32
|
+
if (!entries.some((e) => e.toLowerCase() === directory.toLowerCase()))
|
|
33
|
+
process.env.PATH = `${directory}${path.delimiter}${current}`;
|
|
58
34
|
}
|
|
59
35
|
|
|
60
36
|
function refreshWindowsToolPaths() {
|
|
61
37
|
if (process.platform !== 'win32') return;
|
|
62
38
|
const lad = process.env.LOCALAPPDATA;
|
|
63
|
-
const pf
|
|
39
|
+
const pf = process.env.ProgramFiles;
|
|
64
40
|
const pf86 = process.env['ProgramFiles(x86)'];
|
|
65
|
-
const pd
|
|
66
|
-
addToPathIfDirectory(lad && path.join(lad,
|
|
67
|
-
addToPathIfDirectory(pf && path.join(pf,
|
|
41
|
+
const pd = process.env.ProgramData;
|
|
42
|
+
addToPathIfDirectory(lad && path.join(lad, 'Microsoft', 'WinGet', 'Links'));
|
|
43
|
+
addToPathIfDirectory(pf && path.join(pf, 'GitHub CLI'));
|
|
68
44
|
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,
|
|
45
|
+
addToPathIfDirectory(pd && path.join(pd, 'chocolatey', 'bin'));
|
|
46
|
+
addToPathIfDirectory(pf && path.join(pf, 'Git', 'cmd'));
|
|
47
|
+
addToPathIfDirectory(pf && path.join(pf, 'nodejs'));
|
|
72
48
|
}
|
|
73
49
|
|
|
74
|
-
async function checkWithPathRefresh(
|
|
75
|
-
refreshWindowsToolPaths();
|
|
76
|
-
return commandExists(command);
|
|
77
|
-
}
|
|
50
|
+
async function checkWithPathRefresh(cmd) { refreshWindowsToolPaths(); return commandExists(cmd); }
|
|
78
51
|
|
|
79
52
|
async function runFirstSuccessful(candidates, options = {}) {
|
|
80
|
-
for (const [
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
53
|
+
for (const [cmd, args] of candidates) {
|
|
54
|
+
const r = await runCommandCapture(cmd, args, options);
|
|
55
|
+
if (r.code === 0) return r;
|
|
83
56
|
}
|
|
84
57
|
return { code: 1, stdout: '', stderr: 'All fallback commands failed' };
|
|
85
58
|
}
|
|
86
59
|
|
|
87
60
|
async function checkVercelCli() {
|
|
88
61
|
refreshWindowsToolPaths();
|
|
89
|
-
const
|
|
90
|
-
['npx',
|
|
91
|
-
['npm',
|
|
92
|
-
['vercel', ['--version']]
|
|
62
|
+
const r = await runFirstSuccessful([
|
|
63
|
+
['npx', ['--yes', 'vercel@latest', '--version']],
|
|
64
|
+
['npm', ['exec', '--yes', 'vercel@latest', '--', '--version']],
|
|
65
|
+
['vercel', ['--version']],
|
|
93
66
|
]);
|
|
94
|
-
return
|
|
67
|
+
return r.code === 0;
|
|
95
68
|
}
|
|
96
69
|
|
|
97
70
|
async function warmVercelCli() {
|
|
98
71
|
refreshWindowsToolPaths();
|
|
99
|
-
const
|
|
100
|
-
['npx',
|
|
101
|
-
['npm',
|
|
102
|
-
['vercel', ['--version']]
|
|
72
|
+
const r = await runFirstSuccessful([
|
|
73
|
+
['npx', ['--yes', 'vercel@latest', '--version']],
|
|
74
|
+
['npm', ['exec', '--yes', 'vercel@latest', '--', '--version']],
|
|
75
|
+
['vercel', ['--version']],
|
|
103
76
|
]);
|
|
104
|
-
if (
|
|
77
|
+
if (r.code === 0) return;
|
|
105
78
|
await runCommand('npm', ['install', '-g', 'vercel@latest'], { allowFailure: true });
|
|
106
79
|
}
|
|
107
80
|
|
|
108
|
-
// ── Git install ───────────────────────────────────────────────────────────────
|
|
109
81
|
async function installGit() {
|
|
110
82
|
refreshWindowsToolPaths();
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
await
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (platform === 'linux') {
|
|
119
|
-
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;
|
|
123
|
-
}
|
|
83
|
+
const p = process.platform;
|
|
84
|
+
if (isTermux()) return runCommand('pkg', ['install', '-y', 'git'], { allowFailure: true });
|
|
85
|
+
if (p === 'linux') {
|
|
86
|
+
if (await commandExists('apt-get')) { await runPrivileged('apt-get', ['update'], { allowFailure: true }); return runPrivileged('apt-get', ['install', '-y', 'git'], { allowFailure: true }); }
|
|
124
87
|
if (await commandExists('dnf')) return runPrivileged('dnf', ['install', '-y', 'git'], { allowFailure: true });
|
|
125
88
|
if (await commandExists('yum')) return runPrivileged('yum', ['install', '-y', 'git'], { allowFailure: true });
|
|
126
89
|
if (await commandExists('pacman')) return runPrivileged('pacman', ['-Sy', '--noconfirm', 'git'], { allowFailure: true });
|
|
127
90
|
if (await commandExists('zypper')) return runPrivileged('zypper', ['install', '-y', 'git'], { allowFailure: true });
|
|
128
91
|
if (await commandExists('apk')) return runPrivileged('apk', ['add', 'git'], { allowFailure: true });
|
|
129
92
|
}
|
|
130
|
-
if (
|
|
93
|
+
if (p === 'darwin') {
|
|
131
94
|
if (await commandExists('brew')) return runCommand('brew', ['install', 'git'], { allowFailure: true });
|
|
132
|
-
|
|
133
|
-
await runCommand('xcode-select', ['--install'], { allowFailure: true });
|
|
95
|
+
return runCommand('xcode-select', ['--install'], { allowFailure: true });
|
|
134
96
|
}
|
|
135
|
-
if (
|
|
136
|
-
if (await commandExists('winget')) {
|
|
137
|
-
|
|
138
|
-
refreshWindowsToolPaths();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (await commandExists('choco')) {
|
|
142
|
-
await runCommand('choco', ['install', 'git', '-y'], { allowFailure: true });
|
|
143
|
-
refreshWindowsToolPaths();
|
|
144
|
-
}
|
|
97
|
+
if (p === 'win32') {
|
|
98
|
+
if (await commandExists('winget')) { await runCommand('winget', ['install', '--id', 'Git.Git', '-e', '--source', 'winget'], { allowFailure: true }); refreshWindowsToolPaths(); return; }
|
|
99
|
+
if (await commandExists('choco')) { await runCommand('choco', ['install', 'git', '-y'], { allowFailure: true }); refreshWindowsToolPaths(); }
|
|
145
100
|
}
|
|
146
101
|
}
|
|
147
102
|
|
|
148
|
-
// ── GitHub CLI install ────────────────────────────────────────────────────────
|
|
149
103
|
async function installGithubCli() {
|
|
150
104
|
refreshWindowsToolPaths();
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
await runCommand('pkg', ['install', '-y', 'gh'], { allowFailure: true });
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (platform === 'linux') {
|
|
105
|
+
const p = process.platform;
|
|
106
|
+
if (isTermux()) return runCommand('pkg', ['install', '-y', 'gh'], { allowFailure: true });
|
|
107
|
+
if (p === 'linux') {
|
|
159
108
|
if (await commandExists('apt-get')) {
|
|
160
109
|
const direct = await (await commandExists('sudo')
|
|
161
110
|
? runCommandCapture('sudo', ['apt-get', 'install', '-y', 'gh'])
|
|
162
111
|
: runCommandCapture('apt-get', ['install', '-y', 'gh']));
|
|
163
112
|
if (direct.code === 0) return;
|
|
164
|
-
const
|
|
165
|
-
const prefix = usesSudo ? 'sudo ' : '';
|
|
113
|
+
const pre = (await commandExists('sudo')) ? 'sudo ' : '';
|
|
166
114
|
await runPrivilegedShell([
|
|
167
|
-
'set -e',
|
|
168
|
-
`${
|
|
169
|
-
|
|
170
|
-
`${
|
|
171
|
-
`
|
|
172
|
-
`${
|
|
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`
|
|
115
|
+
'set -e', `${pre}apt-get update`, `${pre}apt-get install -y curl ca-certificates gnupg`,
|
|
116
|
+
`${pre}mkdir -p -m 755 /etc/apt/keyrings`,
|
|
117
|
+
`curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | ${pre}tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null`,
|
|
118
|
+
`${pre}chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg`,
|
|
119
|
+
`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`,
|
|
120
|
+
`${pre}apt-get update`, `${pre}apt-get install -y gh`,
|
|
176
121
|
].join(' && '), { allowFailure: true });
|
|
177
122
|
return;
|
|
178
123
|
}
|
|
@@ -181,25 +126,17 @@ async function installGithubCli() {
|
|
|
181
126
|
if (await commandExists('zypper')) return runPrivileged('zypper', ['install', '-y', 'gh'], { allowFailure: true });
|
|
182
127
|
if (await commandExists('apk')) return runCommand('apk', ['add', 'github-cli'], { allowFailure: true });
|
|
183
128
|
}
|
|
184
|
-
if (
|
|
129
|
+
if (p === 'darwin') {
|
|
185
130
|
if (await commandExists('brew')) return runCommand('brew', ['install', 'gh'], { allowFailure: true });
|
|
186
131
|
}
|
|
187
|
-
if (
|
|
188
|
-
if (await commandExists('winget')) {
|
|
189
|
-
|
|
190
|
-
refreshWindowsToolPaths();
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
if (await commandExists('choco')) {
|
|
194
|
-
await runCommand('choco', ['install', 'gh', '-y'], { allowFailure: true });
|
|
195
|
-
refreshWindowsToolPaths();
|
|
196
|
-
}
|
|
132
|
+
if (p === 'win32') {
|
|
133
|
+
if (await commandExists('winget')) { await runCommand('winget', ['install', '--id', 'GitHub.cli', '-e', '--source', 'winget'], { allowFailure: true }); refreshWindowsToolPaths(); return; }
|
|
134
|
+
if (await commandExists('choco')) { await runCommand('choco', ['install', 'gh', '-y'], { allowFailure: true }); refreshWindowsToolPaths(); }
|
|
197
135
|
}
|
|
198
136
|
}
|
|
199
137
|
|
|
200
|
-
// ── Public API ────────────────────────────────────────────────────────────────
|
|
201
138
|
export async function ensureDependencies({ autoInstall = true, names } = {}) {
|
|
202
|
-
const targets = names ? DEPENDENCIES.filter((
|
|
139
|
+
const targets = names ? DEPENDENCIES.filter((d) => names.includes(d.name)) : DEPENDENCIES;
|
|
203
140
|
const results = [];
|
|
204
141
|
for (const dep of targets) {
|
|
205
142
|
const spin = spinner(`Checking ${dep.label}`);
|
|
@@ -211,7 +148,7 @@ export async function ensureDependencies({ autoInstall = true, names } = {}) {
|
|
|
211
148
|
await dep.install();
|
|
212
149
|
present = await dep.check();
|
|
213
150
|
if (present) installSpin.succeed(`${dep.label} installed`);
|
|
214
|
-
else
|
|
151
|
+
else installSpin.fail(`${dep.label} could not be installed automatically`);
|
|
215
152
|
results.push({ ...dep, present, installed: present });
|
|
216
153
|
}
|
|
217
154
|
return results;
|
|
@@ -219,18 +156,20 @@ export async function ensureDependencies({ autoInstall = true, names } = {}) {
|
|
|
219
156
|
|
|
220
157
|
export async function vinsCommand() {
|
|
221
158
|
section('Fabrica dependency check (vins)');
|
|
222
|
-
|
|
223
|
-
kv('Platform', platform);
|
|
159
|
+
kv('Platform', isTermux() ? 'Termux/Android' : process.platform);
|
|
224
160
|
const results = await ensureDependencies();
|
|
161
|
+
|
|
225
162
|
section('Summary');
|
|
226
|
-
let allGood = true;
|
|
227
163
|
const summaryLines = [];
|
|
164
|
+
let allGood = true;
|
|
228
165
|
for (const dep of results) {
|
|
229
166
|
summaryLines.push(`${dep.label}: ${dep.present ? '✓ OK' : '✗ MISSING — install manually'}`);
|
|
230
167
|
if (!dep.present) { allGood = false; summaryLines.push(` Manual: ${dep.manualUrl}`); }
|
|
231
168
|
}
|
|
232
169
|
subBox(summaryLines, { isError: !allGood });
|
|
233
|
-
if (allGood)
|
|
234
|
-
else
|
|
170
|
+
if (allGood) log('All dependencies ready. Run: fabrica build');
|
|
171
|
+
else log('Some deps could not be installed. See links above.');
|
|
172
|
+
endSections();
|
|
173
|
+
if (!allGood) process.exitCode = 1;
|
|
235
174
|
return results;
|
|
236
175
|
}
|
package/src/github.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { commandExists, runCommand, runCommandCapture } from './system.js';
|
|
2
2
|
import { STORE_REPO } from './config.js';
|
|
3
3
|
import { choose } from './prompt.js';
|
|
4
|
-
import { dimOrange, kv, section, spinner } from './ui.js';
|
|
4
|
+
import { dimOrange, kv, section, spinner, log } from './ui.js';
|
|
5
5
|
|
|
6
6
|
export async function isGithubCliInstalled() {
|
|
7
7
|
return commandExists('gh');
|
|
@@ -27,7 +27,7 @@ export async function ensureGithubLogin() {
|
|
|
27
27
|
spin.succeed('Already logged in to GitHub');
|
|
28
28
|
} else {
|
|
29
29
|
spin.fail('Not logged in to GitHub');
|
|
30
|
-
|
|
30
|
+
log('A browser/device flow will open — log in with "gh auth login"...');
|
|
31
31
|
await runCommand('gh', ['auth', 'login', '--hostname', 'github.com', '--git-protocol', 'https', '--web']);
|
|
32
32
|
if (!(await isLoggedInToGithub())) {
|
|
33
33
|
throw new Error('GitHub login was not completed. Run "fabrica build" again after logging in with "gh auth login".');
|
|
@@ -40,7 +40,7 @@ export async function ensureGithubLogin() {
|
|
|
40
40
|
gitSpin.succeed('GitHub API credentials ready');
|
|
41
41
|
} else {
|
|
42
42
|
gitSpin.fail('Could not configure GitHub git credentials automatically');
|
|
43
|
-
|
|
43
|
+
log('Continuing — if Git asks for credentials later, run: gh auth setup-git');
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -157,7 +157,7 @@ async function forkSourceRepo(owner, repoName) {
|
|
|
157
157
|
}
|
|
158
158
|
renameSpin.fail(`Could not rename — continuing with the existing fork name`);
|
|
159
159
|
kv('GitHub repo', `https://github.com/${owner}/${actualName}`);
|
|
160
|
-
|
|
160
|
+
log(`Note: this is named "${actualName}", not "${repoName}", because your GitHub account already had a fork of ${source}.`);
|
|
161
161
|
return actualName;
|
|
162
162
|
}
|
|
163
163
|
|
package/src/ui.js
CHANGED
|
@@ -1,79 +1,120 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
2
|
import boxen from 'boxen';
|
|
3
3
|
|
|
4
|
-
// ──
|
|
4
|
+
// ── colors ────────────────────────────────────────────────────────────────────
|
|
5
5
|
const ESC = '\x1b[';
|
|
6
|
-
export const orange
|
|
6
|
+
export const orange = (t) => `${ESC}38;2;255;138;0m${t}${ESC}0m`;
|
|
7
7
|
export const dimOrange = (t) => `${ESC}38;2;182;95;0m${t}${ESC}0m`;
|
|
8
|
-
export const bold
|
|
9
|
-
export const red
|
|
10
|
-
export const dim
|
|
11
|
-
export const green
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
export const bold = (t) => `${ESC}1m${t}${ESC}0m`;
|
|
9
|
+
export const red = (t) => `${ESC}38;2;220;50;50m${t}${ESC}0m`;
|
|
10
|
+
export const dim = (t) => `${ESC}2m${t}${ESC}0m`;
|
|
11
|
+
export const green = (t) => `${ESC}38;2;80;200;80m${t}${ESC}0m`;
|
|
12
|
+
|
|
13
|
+
// strip ANSI for length calculations
|
|
14
|
+
function stripAnsi(s) { return s.replace(/\x1b\[[0-9;]*m/g, ''); }
|
|
15
|
+
|
|
16
|
+
// ── section buffer ─────────────────────────────────────────────────────────────
|
|
17
|
+
// Each section accumulates lines, then flushes as ONE boxen box when the
|
|
18
|
+
// next section (or flushSection) is called.
|
|
19
|
+
let _currentTitle = null;
|
|
20
|
+
let _lines = []; // raw strings (may contain ANSI)
|
|
21
|
+
let _sectionCount = 0;
|
|
22
|
+
let _isError = false;
|
|
23
|
+
|
|
24
|
+
function flushSection() {
|
|
25
|
+
if (_currentTitle === null) return;
|
|
26
|
+
|
|
27
|
+
const content = _lines.join('\n');
|
|
28
|
+
const rendered = boxen(content || ' ', {
|
|
29
|
+
title: bold(orange(_currentTitle)),
|
|
18
30
|
titleAlignment: 'left',
|
|
19
31
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
20
32
|
margin: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
21
33
|
borderStyle: 'round',
|
|
22
34
|
borderColor: '#ff8a00',
|
|
23
35
|
});
|
|
36
|
+
|
|
37
|
+
if (_sectionCount > 1) console.log(orange(' │'));
|
|
38
|
+
console.log(rendered);
|
|
39
|
+
|
|
40
|
+
_currentTitle = null;
|
|
41
|
+
_lines = [];
|
|
42
|
+
_isError = false;
|
|
24
43
|
}
|
|
25
44
|
|
|
26
|
-
//
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
30
|
-
margin: { top: 0, bottom: 0, left: 4, right: 0 },
|
|
31
|
-
borderStyle: 'single',
|
|
32
|
-
borderColor: isError ? '#dc3232' : '#b65f00',
|
|
33
|
-
});
|
|
45
|
+
// Add a line into the current section buffer
|
|
46
|
+
function addLine(text) {
|
|
47
|
+
_lines.push(text);
|
|
34
48
|
}
|
|
35
49
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
export function resetSectionCount() {
|
|
51
|
+
flushSection();
|
|
52
|
+
_sectionCount = 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── public API ────────────────────────────────────────────────────────────────
|
|
39
56
|
|
|
57
|
+
// Start a new named section (flushes previous one)
|
|
40
58
|
export function section(title) {
|
|
41
|
-
|
|
59
|
+
flushSection();
|
|
42
60
|
_sectionCount++;
|
|
43
|
-
|
|
61
|
+
_currentTitle = title;
|
|
62
|
+
_lines = [];
|
|
44
63
|
}
|
|
45
64
|
|
|
46
|
-
//
|
|
47
|
-
export function
|
|
48
|
-
|
|
49
|
-
console.log(sectionBox(content, title));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Print a sub-step box (indented, for raw tool output)
|
|
53
|
-
export function subBox(lines, { isError = false } = {}) {
|
|
54
|
-
if (!lines || !lines.length) return;
|
|
55
|
-
const content = (isError ? lines.map((l) => red(l)) : lines.map((l) => dimOrange(l))).join('\n');
|
|
56
|
-
console.log(subStepBox(content, isError));
|
|
65
|
+
// Force-flush the current section (call at end of a command)
|
|
66
|
+
export function endSections() {
|
|
67
|
+
flushSection();
|
|
57
68
|
}
|
|
58
69
|
|
|
59
|
-
// kv inside current
|
|
70
|
+
// kv row inside current section
|
|
60
71
|
export function kv(key, value) {
|
|
61
|
-
|
|
72
|
+
addLine(` ${dimOrange('›')} ${bold(key)} ${dimOrange('→')} ${value}`);
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
//
|
|
75
|
+
// spinner — writes inline to stdout while running, then adds result line to buffer
|
|
65
76
|
const CLEAR_LINE = '\x1b[K';
|
|
66
77
|
export function spinner(text) {
|
|
67
|
-
process.stdout.write(
|
|
78
|
+
process.stdout.write(` ${dimOrange('○')} ${text}`);
|
|
68
79
|
return {
|
|
69
|
-
succeed(msg) {
|
|
70
|
-
|
|
80
|
+
succeed(msg) {
|
|
81
|
+
process.stdout.write(`\r${CLEAR_LINE}`);
|
|
82
|
+
addLine(` ${orange('✓')} ${msg}`);
|
|
83
|
+
},
|
|
84
|
+
fail(msg) {
|
|
85
|
+
process.stdout.write(`\r${CLEAR_LINE}`);
|
|
86
|
+
addLine(` ${red('✗')} ${msg}`);
|
|
87
|
+
},
|
|
71
88
|
};
|
|
72
89
|
}
|
|
73
90
|
|
|
91
|
+
// plain log inside current section
|
|
92
|
+
export function log(text) {
|
|
93
|
+
addLine(` ${text}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// sub-step block (e.g. raw Vercel output) — indented, dimmer border
|
|
97
|
+
export function subBox(lines, { isError = false } = {}) {
|
|
98
|
+
if (!lines || !lines.length) return;
|
|
99
|
+
const colored = isError ? lines.map((l) => red(l)) : lines.map((l) => dimOrange(l));
|
|
100
|
+
const content = colored.join('\n');
|
|
101
|
+
const rendered = boxen(content, {
|
|
102
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
103
|
+
margin: { top: 0, bottom: 0, left: 2, right: 0 },
|
|
104
|
+
borderStyle: 'single',
|
|
105
|
+
borderColor: isError ? '#dc3232' : '#b65f00',
|
|
106
|
+
});
|
|
107
|
+
// add each rendered line into buffer so it stays inside the section box
|
|
108
|
+
for (const line of rendered.split('\n')) addLine(line);
|
|
109
|
+
}
|
|
110
|
+
|
|
74
111
|
// ── banner ────────────────────────────────────────────────────────────────────
|
|
75
112
|
export function banner() {
|
|
113
|
+
flushSection();
|
|
76
114
|
_sectionCount = 0;
|
|
115
|
+
_currentTitle = null;
|
|
116
|
+
_lines = [];
|
|
117
|
+
|
|
77
118
|
const art = [
|
|
78
119
|
'███████╗ █████╗ ██████╗ ██████╗ ██╗ ██████╗ █████╗ ',
|
|
79
120
|
'██╔════╝██╔══██╗██╔══██╗██╔══██╗██║██╔════╝██╔══██╗',
|
|
@@ -90,7 +131,6 @@ export function banner() {
|
|
|
90
131
|
|
|
91
132
|
console.log(boxen(`${art}\n\n${subtitle}`, {
|
|
92
133
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
93
|
-
margin: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
94
134
|
borderStyle: 'double',
|
|
95
135
|
borderColor: '#ff8a00',
|
|
96
136
|
}));
|
|
@@ -119,7 +159,12 @@ export function help() {
|
|
|
119
159
|
'',
|
|
120
160
|
dim('Creator: SPARROW AI SOLUTION'),
|
|
121
161
|
].join('\n');
|
|
122
|
-
|
|
123
162
|
console.log(orange(' │'));
|
|
124
|
-
console.log(
|
|
163
|
+
console.log(boxen(content, {
|
|
164
|
+
title: bold(orange('Help')),
|
|
165
|
+
titleAlignment: 'left',
|
|
166
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
167
|
+
borderStyle: 'round',
|
|
168
|
+
borderColor: '#ff8a00',
|
|
169
|
+
}));
|
|
125
170
|
}
|