fabrica-e-commerce 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +98 -46
- package/src/deploy.js +111 -27
- package/src/deps.js +397 -61
- package/src/prompt.js +59 -22
- package/src/ui.js +37 -29
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -3,14 +3,21 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { connectSupabase } from './bridge.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
collectEnv, collectAdminPassword, cloneRepo,
|
|
8
|
+
deployToVercel, updateProjectEnv, ensureVercelLogin, runLocally,
|
|
9
|
+
} from './deploy.js';
|
|
7
10
|
import { createGithubRepoFromClone } from './github.js';
|
|
8
11
|
import { ensureDependencies, vinsCommand } from './deps.js';
|
|
9
12
|
import { cleanCommand } from './clean.js';
|
|
10
13
|
import { BRIDGE_ORIGIN, STORE_REPO } from './config.js';
|
|
11
14
|
import { dataDir, readProjects } from './store.js';
|
|
12
15
|
import { choose, ask } from './prompt.js';
|
|
13
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
banner, help, kv, kvSuccess, kvFail, section, endSections, subBox,
|
|
18
|
+
log, logInfo, logWarn, logSuccess, divider, stepHeader,
|
|
19
|
+
orange, dimOrange, bold, dim, green, cyan, yellow, red, white,
|
|
20
|
+
} from './ui.js';
|
|
14
21
|
import { openUrl } from './system.js';
|
|
15
22
|
|
|
16
23
|
async function packageVersion() {
|
|
@@ -23,31 +30,42 @@ async function packageVersion() {
|
|
|
23
30
|
async function build() {
|
|
24
31
|
banner();
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
// Step 1 — Dependency check
|
|
34
|
+
stepHeader(1, 6, 'Dependency check');
|
|
27
35
|
section('Dependency check');
|
|
28
36
|
await ensureDependencies({ names: ['git'] });
|
|
29
37
|
endSections();
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
// Step 2 — Supabase connect
|
|
40
|
+
stepHeader(2, 6, 'Supabase connect');
|
|
32
41
|
section('Supabase connect');
|
|
33
42
|
kv('Bridge', 'ONLINE');
|
|
34
43
|
kv('SQL', 'Prepared securely (hidden from UI)');
|
|
35
44
|
const supabase = await connectSupabase();
|
|
36
45
|
endSections();
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
// Step 3 — Environment variables
|
|
48
|
+
stepHeader(3, 6, 'Environment variables');
|
|
39
49
|
const env = await collectEnv(supabase);
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
// Step 4 — Admin password
|
|
52
|
+
stepHeader(4, 6, 'Admin password');
|
|
53
|
+
const adminPassword = await collectAdminPassword();
|
|
54
|
+
env['PASS'] = adminPassword;
|
|
55
|
+
|
|
56
|
+
// Step 5 — Clone storefront
|
|
57
|
+
stepHeader(5, 6, 'Clone storefront');
|
|
42
58
|
const project = await cloneRepo();
|
|
43
59
|
|
|
44
|
-
|
|
60
|
+
// Step 6 — Deploy target
|
|
61
|
+
stepHeader(6, 6, 'Deploy target');
|
|
45
62
|
section('Where should Fabrica deploy?');
|
|
63
|
+
logInfo('Choose how you want to run your store:');
|
|
46
64
|
endSections();
|
|
47
65
|
|
|
48
66
|
const target = await choose('Select deployment target:', [
|
|
49
|
-
{ name: '☁
|
|
50
|
-
{ name: '💻 Run locally on this
|
|
67
|
+
{ name: '☁ Deploy on Vercel — cloud, shareable URL', value: 'vercel' },
|
|
68
|
+
{ name: '💻 Run locally — development on this machine', value: 'local' },
|
|
51
69
|
]);
|
|
52
70
|
|
|
53
71
|
if (target === 'local') {
|
|
@@ -63,14 +81,14 @@ async function build() {
|
|
|
63
81
|
const githubRepo = await createGithubRepoFromClone(project);
|
|
64
82
|
const record = await deployToVercel(project, env, githubRepo);
|
|
65
83
|
|
|
66
|
-
section('✓
|
|
84
|
+
section('✓ Deployment complete');
|
|
67
85
|
divider();
|
|
68
|
-
|
|
86
|
+
kvSuccess('Project', record.projectName);
|
|
69
87
|
kv('GitHub', record.githubRepo || dim('n/a'));
|
|
70
|
-
kv('URL',
|
|
71
|
-
kv('Local path', record.target);
|
|
88
|
+
kv('Live URL', record.productionUrl || dim('n/a'));
|
|
89
|
+
kv('Local path', dim(record.target));
|
|
72
90
|
divider();
|
|
73
|
-
|
|
91
|
+
logSuccess('Your Fabrica store is live! 🎉');
|
|
74
92
|
endSections();
|
|
75
93
|
}
|
|
76
94
|
|
|
@@ -81,18 +99,22 @@ async function list() {
|
|
|
81
99
|
|
|
82
100
|
section('Your Fabrica projects');
|
|
83
101
|
if (!projects.length) {
|
|
84
|
-
logInfo('No projects found');
|
|
85
|
-
log(
|
|
102
|
+
logInfo('No projects found yet');
|
|
103
|
+
log(`Run ${cyan('npx fabrica-e-commerce build')} to create your first store`);
|
|
86
104
|
endSections();
|
|
87
105
|
return;
|
|
88
106
|
}
|
|
89
107
|
|
|
90
|
-
kv('Total
|
|
108
|
+
kv('Total', String(projects.length) + ' project' + (projects.length !== 1 ? 's' : ''));
|
|
91
109
|
divider();
|
|
92
110
|
for (const p of projects) {
|
|
93
|
-
const
|
|
94
|
-
|
|
111
|
+
const badge = p.type === 'local'
|
|
112
|
+
? `${yellow('◉')} ${yellow('local')}`
|
|
113
|
+
: `${green('◉')} ${green('cloud')}`;
|
|
114
|
+
log(`${bold(white(p.projectName))} ${badge} ${dim(p.createdAt?.slice(0, 10) || '')}`);
|
|
95
115
|
}
|
|
116
|
+
divider();
|
|
117
|
+
logInfo('Select a project to view its details');
|
|
96
118
|
endSections();
|
|
97
119
|
|
|
98
120
|
const choices = projects.map((p) => ({
|
|
@@ -103,14 +125,19 @@ async function list() {
|
|
|
103
125
|
const selected = await choose('Select a project to view details:', choices);
|
|
104
126
|
const project = projects.find((p) => p.id === selected);
|
|
105
127
|
|
|
106
|
-
section(`Project
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
kv('
|
|
128
|
+
section(`Project — ${bold(orange(project.projectName))}`);
|
|
129
|
+
divider();
|
|
130
|
+
const typeBadge = project.type === 'local' ? yellow('local') : green('cloud');
|
|
131
|
+
kv('Type', typeBadge);
|
|
132
|
+
kv('Created', project.createdAt ? project.createdAt.slice(0, 19).replace('T', ' ') : dim('unknown'));
|
|
133
|
+
kv('Path', dim(project.target || 'n/a'));
|
|
110
134
|
kv('GitHub', project.githubRepo || dim('n/a'));
|
|
111
135
|
kv('Supabase', project.supabaseUrl || dim('n/a'));
|
|
112
136
|
kv('URL', project.productionUrl || dim('n/a'));
|
|
113
|
-
kv('Env keys', (project.envKeys || []).
|
|
137
|
+
kv('Env keys', (project.envKeys || []).length
|
|
138
|
+
? (project.envKeys || []).join(dim(' · '))
|
|
139
|
+
: dim('none'));
|
|
140
|
+
divider();
|
|
114
141
|
endSections();
|
|
115
142
|
}
|
|
116
143
|
|
|
@@ -120,24 +147,26 @@ async function env() {
|
|
|
120
147
|
const projects = await readProjects();
|
|
121
148
|
|
|
122
149
|
section('Environment manager');
|
|
150
|
+
logInfo('Update API keys and secrets for any project.');
|
|
123
151
|
if (!projects.length) {
|
|
124
|
-
|
|
125
|
-
log('
|
|
152
|
+
divider();
|
|
153
|
+
log(`No projects found — run ${cyan('npx fabrica-e-commerce build')} first`);
|
|
126
154
|
endSections();
|
|
127
155
|
return;
|
|
128
156
|
}
|
|
157
|
+
kv('Total projects', String(projects.length));
|
|
129
158
|
endSections();
|
|
130
159
|
|
|
131
160
|
const typeFilter = await choose('Which projects to show?', [
|
|
132
|
-
{ name: 'All projects',
|
|
133
|
-
{ name: 'Local projects only',
|
|
134
|
-
{ name: 'Cloud (Vercel) projects only',
|
|
161
|
+
{ name: '📋 All projects', value: 'all' },
|
|
162
|
+
{ name: '💻 Local projects only', value: 'local' },
|
|
163
|
+
{ name: '☁ Cloud (Vercel) projects only', value: 'cloud' },
|
|
135
164
|
]);
|
|
136
165
|
|
|
137
166
|
const filtered = typeFilter === 'all' ? projects : projects.filter((p) => p.type === typeFilter);
|
|
138
167
|
if (!filtered.length) {
|
|
139
168
|
section('Environment manager');
|
|
140
|
-
|
|
169
|
+
logWarn(`No ${typeFilter} projects found`);
|
|
141
170
|
endSections();
|
|
142
171
|
return;
|
|
143
172
|
}
|
|
@@ -156,13 +185,22 @@ async function env() {
|
|
|
156
185
|
return;
|
|
157
186
|
}
|
|
158
187
|
|
|
188
|
+
section(`Env keys — ${bold(orange(project.projectName))}`);
|
|
189
|
+
divider();
|
|
190
|
+
for (const k of envKeys) log(`${orange('·')} ${white(k)}`);
|
|
191
|
+
divider();
|
|
192
|
+
endSections();
|
|
193
|
+
|
|
159
194
|
const key = await choose('Select env variable to update:', envKeys.map((k) => ({ name: k, value: k })));
|
|
160
195
|
const currentVal = (project.env || {})[key];
|
|
161
|
-
|
|
196
|
+
|
|
197
|
+
console.log();
|
|
198
|
+
const value = await ask(`New value for ${orange(key)}`, currentVal || '');
|
|
199
|
+
console.log();
|
|
162
200
|
|
|
163
201
|
section('Applying update');
|
|
164
202
|
await updateProjectEnv(project, key, value);
|
|
165
|
-
kvSuccess('Updated', `${key}
|
|
203
|
+
kvSuccess('Updated', `${key} → ${project.type === 'local' ? '.env.local' : 'Vercel + redeployed'}`);
|
|
166
204
|
endSections();
|
|
167
205
|
}
|
|
168
206
|
|
|
@@ -173,17 +211,18 @@ async function rerun() {
|
|
|
173
211
|
|
|
174
212
|
section('Re-run / re-open project');
|
|
175
213
|
if (!projects.length) {
|
|
176
|
-
logInfo('No projects found');
|
|
177
|
-
log(
|
|
214
|
+
logInfo('No projects found yet');
|
|
215
|
+
log(`Run ${cyan('npx fabrica-e-commerce build')} to create your first store`);
|
|
178
216
|
endSections();
|
|
179
217
|
return;
|
|
180
218
|
}
|
|
219
|
+
kv('Total', String(projects.length) + ' project' + (projects.length !== 1 ? 's' : ''));
|
|
181
220
|
endSections();
|
|
182
221
|
|
|
183
222
|
const typeFilter = await choose('Which type of project?', [
|
|
184
|
-
{ name: 'All projects',
|
|
185
|
-
{ name: 'Local projects',
|
|
186
|
-
{ name: 'Cloud (Vercel) projects',
|
|
223
|
+
{ name: '📋 All projects', value: 'all' },
|
|
224
|
+
{ name: '💻 Local projects', value: 'local' },
|
|
225
|
+
{ name: '☁ Cloud (Vercel) projects', value: 'cloud' },
|
|
187
226
|
]);
|
|
188
227
|
|
|
189
228
|
const filtered = typeFilter === 'all' ? projects : projects.filter((p) => p.type === typeFilter);
|
|
@@ -200,12 +239,14 @@ async function rerun() {
|
|
|
200
239
|
})));
|
|
201
240
|
const project = filtered.find((p) => p.id === projectId);
|
|
202
241
|
|
|
203
|
-
section(`Re-running
|
|
242
|
+
section(`Re-running — ${bold(orange(project.projectName))}`);
|
|
204
243
|
|
|
205
244
|
if (project.type === 'local') {
|
|
206
|
-
|
|
245
|
+
divider();
|
|
246
|
+
kv('Path', dim(project.target));
|
|
207
247
|
kv('URL', 'http://localhost:3000');
|
|
208
|
-
log('Starting dev server — browser opens in 3s…');
|
|
248
|
+
log(dim('Starting dev server — browser opens in 3s…'));
|
|
249
|
+
divider();
|
|
209
250
|
endSections();
|
|
210
251
|
|
|
211
252
|
const { runCommand, runCommandCapture } = await import('./system.js');
|
|
@@ -217,11 +258,13 @@ async function rerun() {
|
|
|
217
258
|
await runCommand(devCommand[0], devCommand[1], { cwd: project.target });
|
|
218
259
|
} else {
|
|
219
260
|
const url = project.productionUrl || null;
|
|
261
|
+
divider();
|
|
220
262
|
kv('GitHub', project.githubRepo || dim('n/a'));
|
|
221
263
|
kv('URL', url || dim('n/a'));
|
|
222
264
|
kv('Inspect', project.inspectUrl || dim('n/a'));
|
|
223
|
-
kv('Created', project.createdAt
|
|
224
|
-
|
|
265
|
+
kv('Created', project.createdAt ? project.createdAt.slice(0, 19).replace('T', ' ') : dim('unknown'));
|
|
266
|
+
divider();
|
|
267
|
+
if (url) log(dim(`Opening ${url}…`));
|
|
225
268
|
endSections();
|
|
226
269
|
if (url) await openUrl(url);
|
|
227
270
|
}
|
|
@@ -230,14 +273,21 @@ async function rerun() {
|
|
|
230
273
|
// ── info ──────────────────────────────────────────────────────────────────────
|
|
231
274
|
async function info() {
|
|
232
275
|
banner();
|
|
276
|
+
const version = await packageVersion();
|
|
277
|
+
|
|
233
278
|
section('Package info');
|
|
234
|
-
|
|
279
|
+
divider();
|
|
280
|
+
kvSuccess('Package', `fabrica-e-commerce v${version}`);
|
|
235
281
|
kv('Bridge', BRIDGE_ORIGIN);
|
|
236
282
|
kv('Store repo', STORE_REPO);
|
|
237
|
-
kv('Local data', dataDir);
|
|
283
|
+
kv('Local data', dim(dataDir));
|
|
284
|
+
divider();
|
|
238
285
|
kv('Node.js', process.version);
|
|
239
286
|
kv('Platform', process.platform);
|
|
287
|
+
kv('Arch', process.arch);
|
|
288
|
+
divider();
|
|
240
289
|
kv('Creator', 'SPARROW AI SOLUTION');
|
|
290
|
+
kv('License', 'MIT');
|
|
241
291
|
endSections();
|
|
242
292
|
}
|
|
243
293
|
|
|
@@ -254,7 +304,9 @@ export async function run(args) {
|
|
|
254
304
|
if (command === 'vins' || command === '/vins') return vinsCommand();
|
|
255
305
|
if (command === 'help' || command === '--help' || command === '-h') return help();
|
|
256
306
|
|
|
257
|
-
|
|
258
|
-
|
|
307
|
+
section('Unknown command');
|
|
308
|
+
logWarn(`"${command}" is not a valid command`);
|
|
309
|
+
log(`Run ${cyan('npx fabrica-e-commerce help')} to see all commands`);
|
|
310
|
+
endSections();
|
|
259
311
|
process.exitCode = 1;
|
|
260
312
|
}
|
package/src/deploy.js
CHANGED
|
@@ -4,31 +4,96 @@ 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 {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
kv, kvSuccess, kvFail, kvPending, section, endSections, spinner,
|
|
9
|
+
subBox, log, logInfo, logWarn, logSuccess, divider,
|
|
10
|
+
orange, dimOrange, bold, dim, green, cyan, red, white, yellow,
|
|
11
|
+
} from './ui.js';
|
|
12
|
+
import { ask, askPassword, choose } from './prompt.js';
|
|
9
13
|
|
|
10
14
|
const VERCEL_ENVIRONMENTS = ['production', 'preview', 'development'];
|
|
11
15
|
|
|
16
|
+
// ── Env key descriptions ───────────────────────────────────────────────────────
|
|
17
|
+
const ENV_DESCRIPTIONS = {
|
|
18
|
+
RAZORPAY_KEY_ID: 'Razorpay payment key ID',
|
|
19
|
+
RAZORPAY_KEY_SECRET: 'Razorpay payment secret',
|
|
20
|
+
OPENROUTER_API_KEY: 'OpenRouter AI API key',
|
|
21
|
+
UMAMI_WEBSITE_ID: 'Umami analytics website ID',
|
|
22
|
+
UMAMI_API_KEY: 'Umami analytics API key',
|
|
23
|
+
SHIPPO_API_KEY: 'Shippo shipping API key',
|
|
24
|
+
};
|
|
25
|
+
|
|
12
26
|
export async function collectEnv(supabase) {
|
|
27
|
+
// ── Show what will be collected ──────────────────────────────────────────────
|
|
13
28
|
section('Environment variables');
|
|
29
|
+
logInfo('The following API keys are required for your store:');
|
|
30
|
+
divider();
|
|
31
|
+
for (const key of REQUIRED_ENV_KEYS) {
|
|
32
|
+
kvPending(key, ENV_DESCRIPTIONS[key] || '');
|
|
33
|
+
}
|
|
34
|
+
divider();
|
|
35
|
+
log(dim('Enter each value below — press Enter to confirm'));
|
|
36
|
+
endSections();
|
|
37
|
+
|
|
38
|
+
// ── Collect each key interactively ───────────────────────────────────────────
|
|
39
|
+
console.log();
|
|
14
40
|
const env = { ...HARDCODED_ENV, SUPABASE_URL: supabase.url, SUPABASE_ANON_KEY: supabase.anonKey };
|
|
15
41
|
for (const key of REQUIRED_ENV_KEYS) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
42
|
+
env[key] = await ask(key);
|
|
43
|
+
}
|
|
44
|
+
console.log();
|
|
45
|
+
|
|
46
|
+
// ── Summary ───────────────────────────────────────────────────────────────────
|
|
47
|
+
section('Environment variables — collected');
|
|
48
|
+
for (const key of REQUIRED_ENV_KEYS) {
|
|
49
|
+
kvSuccess(key, '••••••••');
|
|
19
50
|
}
|
|
51
|
+
logSuccess('All API keys saved');
|
|
52
|
+
endSections();
|
|
53
|
+
|
|
20
54
|
return env;
|
|
21
55
|
}
|
|
22
56
|
|
|
57
|
+
export async function collectAdminPassword() {
|
|
58
|
+
// ── Show intent box ──────────────────────────────────────────────────────────
|
|
59
|
+
section('Admin & edit panel password');
|
|
60
|
+
logInfo('Set a password to protect your store\'s admin and edit panel.');
|
|
61
|
+
log(dim('This will be stored as the PASS environment variable.'));
|
|
62
|
+
divider();
|
|
63
|
+
log(`${orange('PASS')} ${dimOrange('→')} ${dim('Admin panel access password')}`);
|
|
64
|
+
endSections();
|
|
65
|
+
|
|
66
|
+
// ── Prompt ────────────────────────────────────────────────────────────────────
|
|
67
|
+
console.log();
|
|
68
|
+
const password = await askPassword('Admin panel password (PASS)');
|
|
69
|
+
console.log();
|
|
70
|
+
|
|
71
|
+
// ── Confirm ───────────────────────────────────────────────────────────────────
|
|
72
|
+
section('Admin password — set');
|
|
73
|
+
kvSuccess('PASS', '•••••••• (stored securely)');
|
|
74
|
+
endSections();
|
|
75
|
+
|
|
76
|
+
return password;
|
|
77
|
+
}
|
|
78
|
+
|
|
23
79
|
export async function cloneRepo() {
|
|
24
80
|
endSections();
|
|
25
81
|
section('Clone storefront');
|
|
26
|
-
|
|
82
|
+
logInfo('Enter a name for your project (used as the Vercel project name).');
|
|
83
|
+
endSections();
|
|
84
|
+
|
|
85
|
+
console.log();
|
|
86
|
+
const projectName = await ask('Project name', `fabrica-store-${Date.now()}`);
|
|
87
|
+
console.log();
|
|
88
|
+
|
|
27
89
|
section('Clone storefront');
|
|
28
90
|
const id = crypto.randomUUID();
|
|
29
91
|
const target = path.join(buildsDir, `${projectName}-${id.slice(0, 8)}`);
|
|
30
|
-
|
|
92
|
+
kv('Project', projectName);
|
|
93
|
+
kv('Destination', dim(target));
|
|
94
|
+
log(dim('Cloning repository — this may take a moment...'));
|
|
31
95
|
endSections();
|
|
96
|
+
|
|
32
97
|
await runCommand('git', ['clone', STORE_REPO, target]);
|
|
33
98
|
return { id, projectName, target };
|
|
34
99
|
}
|
|
@@ -40,23 +105,24 @@ export async function isLoggedInToVercel() {
|
|
|
40
105
|
|
|
41
106
|
export async function ensureVercelLogin() {
|
|
42
107
|
section('Vercel login');
|
|
43
|
-
const spin = spinner('Checking Vercel login');
|
|
108
|
+
const spin = spinner('Checking Vercel login…');
|
|
44
109
|
if (await isLoggedInToVercel()) {
|
|
45
|
-
spin.succeed('Already logged in to Vercel');
|
|
110
|
+
spin.succeed(green('Already logged in to Vercel'));
|
|
111
|
+
endSections();
|
|
46
112
|
return;
|
|
47
113
|
}
|
|
48
114
|
spin.fail('Not logged in to Vercel');
|
|
49
|
-
log('Opening "vercel login" — finish the login in your browser
|
|
115
|
+
log(dim('Opening "vercel login" — finish the login in your browser…'));
|
|
50
116
|
endSections();
|
|
51
117
|
await runCommand('npx', ['vercel@latest', 'login']);
|
|
52
118
|
section('Vercel login');
|
|
53
119
|
if (!(await isLoggedInToVercel())) {
|
|
54
120
|
throw new Error('Vercel login was not completed. Run "fabrica build" again after logging in.');
|
|
55
121
|
}
|
|
56
|
-
|
|
122
|
+
kvSuccess('Vercel', 'Logged in successfully');
|
|
123
|
+
endSections();
|
|
57
124
|
}
|
|
58
125
|
|
|
59
|
-
// Capture vercel CLI output and add it into the current section buffer as a subBox
|
|
60
126
|
async function runVercelBoxed(args, options = {}) {
|
|
61
127
|
const result = await runCommandCapture('npx', ['vercel@latest', ...args], options);
|
|
62
128
|
const raw = ((result.stdout || '') + (result.stderr || '')).trim();
|
|
@@ -68,23 +134,35 @@ async function runVercelBoxed(args, options = {}) {
|
|
|
68
134
|
}
|
|
69
135
|
|
|
70
136
|
async function setEnvEverywhere(project, env) {
|
|
137
|
+
section('Setting environment variables');
|
|
138
|
+
logInfo(`Pushing ${Object.keys(env).length} variables to Vercel…`);
|
|
139
|
+
divider();
|
|
71
140
|
for (const [key, value] of Object.entries(env)) {
|
|
72
141
|
const spin = spinner(`Setting ${key}`);
|
|
73
142
|
for (const environment of VERCEL_ENVIRONMENTS) {
|
|
74
143
|
await runVercelBoxed(['env', 'rm', key, environment, '--yes'], { cwd: project.target, allowFailure: true });
|
|
75
144
|
await runVercelBoxed(['env', 'add', key, environment], { cwd: project.target, input: `${value}\n` });
|
|
76
145
|
}
|
|
77
|
-
spin.succeed(
|
|
146
|
+
spin.succeed(`${green(key)} ${dimOrange('→')} ${dim('production · preview · development')}`);
|
|
78
147
|
}
|
|
148
|
+
divider();
|
|
149
|
+
logSuccess('All environment variables pushed to Vercel');
|
|
79
150
|
}
|
|
80
151
|
|
|
81
152
|
async function connectGithubRepo(project, repoUrl) {
|
|
82
|
-
const spin = spinner(`Connecting Vercel project to
|
|
153
|
+
const spin = spinner(`Connecting Vercel project to GitHub…`);
|
|
83
154
|
const attempts = 6;
|
|
84
155
|
let lastError = '';
|
|
85
156
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
86
|
-
const connect = await runCommandCapture(
|
|
87
|
-
|
|
157
|
+
const connect = await runCommandCapture(
|
|
158
|
+
'npx',
|
|
159
|
+
['--yes', 'vercel@latest', 'git', 'connect', repoUrl, '--yes'],
|
|
160
|
+
{ cwd: project.target }
|
|
161
|
+
);
|
|
162
|
+
if (connect.code === 0) {
|
|
163
|
+
spin.succeed(`${green('GitHub repo connected')} ${dim(repoUrl)}`);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
88
166
|
lastError = (connect.stderr || connect.stdout || '').trim();
|
|
89
167
|
if (attempt < attempts) await new Promise((r) => setTimeout(r, attempt * 3000));
|
|
90
168
|
}
|
|
@@ -111,9 +189,11 @@ export async function deployToVercel(project, env, githubRepo) {
|
|
|
111
189
|
await connectGithubRepo(project, githubRepo.repoUrl);
|
|
112
190
|
}
|
|
113
191
|
|
|
192
|
+
endSections();
|
|
114
193
|
await setEnvEverywhere(project, env);
|
|
115
194
|
|
|
116
|
-
|
|
195
|
+
section('Vercel deployment');
|
|
196
|
+
const deploySpin = spinner('Creating production deployment…');
|
|
117
197
|
const deployResult = await runCommandCapture('npx', ['vercel@latest', '--prod', '--yes'], { cwd: project.target });
|
|
118
198
|
const deployOutput = ((deployResult.stdout || '') + (deployResult.stderr || '')).trim();
|
|
119
199
|
|
|
@@ -127,12 +207,11 @@ export async function deployToVercel(project, env, githubRepo) {
|
|
|
127
207
|
}
|
|
128
208
|
|
|
129
209
|
subBox(deployOutput.split('\n').filter(Boolean));
|
|
130
|
-
deploySpin.succeed('Production deployment created');
|
|
210
|
+
deploySpin.succeed('Production deployment created successfully');
|
|
131
211
|
|
|
132
212
|
const openTarget = aliasedUrl || productionUrl;
|
|
133
213
|
if (openTarget) {
|
|
134
|
-
kv('
|
|
135
|
-
// open after flush
|
|
214
|
+
kv('Live URL', openTarget);
|
|
136
215
|
process.nextTick(() => openUrl(openTarget));
|
|
137
216
|
}
|
|
138
217
|
|
|
@@ -162,7 +241,7 @@ export async function updateProjectEnv(project, key, value) {
|
|
|
162
241
|
if (idx >= 0) lines[idx] = `${key}=${value}`;
|
|
163
242
|
else lines.push(`${key}=${value}`);
|
|
164
243
|
await fs.writeFile(envPath, lines.join('\n'), 'utf8');
|
|
165
|
-
|
|
244
|
+
kvSuccess('Updated', `${key} → .env.local`);
|
|
166
245
|
} else {
|
|
167
246
|
await ensureVercelLogin();
|
|
168
247
|
section('Applying update');
|
|
@@ -170,9 +249,9 @@ export async function updateProjectEnv(project, key, value) {
|
|
|
170
249
|
await runVercelBoxed(['env', 'rm', key, environment, '--yes'], { cwd: project.target, allowFailure: true });
|
|
171
250
|
await runVercelBoxed(['env', 'add', key, environment], { cwd: project.target, input: `${value}\n` });
|
|
172
251
|
}
|
|
173
|
-
const spin = spinner('Redeploying
|
|
252
|
+
const spin = spinner('Redeploying…');
|
|
174
253
|
await runVercelBoxed(['--prod', '--yes'], { cwd: project.target });
|
|
175
|
-
spin.succeed(`Redeployed ${project.projectName}`);
|
|
254
|
+
spin.succeed(`Redeployed ${green(project.projectName)}`);
|
|
176
255
|
}
|
|
177
256
|
const updated = { ...project, env: { ...(project.env || {}), [key]: value } };
|
|
178
257
|
await saveProject(updated);
|
|
@@ -180,10 +259,12 @@ export async function updateProjectEnv(project, key, value) {
|
|
|
180
259
|
|
|
181
260
|
export async function runLocally(project, env) {
|
|
182
261
|
section('Local setup');
|
|
262
|
+
logInfo('Writing environment file and starting dev server…');
|
|
263
|
+
divider();
|
|
183
264
|
const envPath = path.join(project.target, '.env.local');
|
|
184
265
|
const contents = Object.entries(env).map(([k, v]) => `${k}=${String(v).replace(/\n/g, '\\n')}`).join('\n') + '\n';
|
|
185
266
|
await fs.writeFile(envPath, contents, 'utf8');
|
|
186
|
-
|
|
267
|
+
kvSuccess('Env file', envPath);
|
|
187
268
|
|
|
188
269
|
const hasPnpmLock = await fs.access(path.join(project.target, 'pnpm-lock.yaml')).then(() => true, () => false);
|
|
189
270
|
const pnpmAvailable = (await runCommandCapture('pnpm', ['--version'])).code === 0;
|
|
@@ -198,15 +279,18 @@ export async function runLocally(project, env) {
|
|
|
198
279
|
await saveProject(record);
|
|
199
280
|
|
|
200
281
|
kv('URL', 'http://localhost:3000');
|
|
201
|
-
log('Installing dependencies
|
|
282
|
+
log(dim('Installing dependencies — this may take a while…'));
|
|
202
283
|
endSections();
|
|
284
|
+
|
|
203
285
|
await runCommand(installCmd[0], installCmd[1], { cwd: project.target });
|
|
204
286
|
|
|
205
287
|
section('Local app');
|
|
206
|
-
|
|
288
|
+
kvSuccess('Project', project.projectName);
|
|
289
|
+
kv('Path', dim(project.target));
|
|
207
290
|
kv('URL', 'http://localhost:3000');
|
|
208
|
-
log('Starting dev server — browser opens in 3s
|
|
291
|
+
log(dim('Starting dev server — browser opens in 3s…'));
|
|
209
292
|
endSections();
|
|
293
|
+
|
|
210
294
|
setTimeout(() => openUrl('http://localhost:3000'), 3000);
|
|
211
295
|
await runCommand(devCmd[0], devCmd[1], { cwd: project.target });
|
|
212
296
|
}
|