fabrica-e-commerce 0.2.2 → 0.2.4
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/README.md +113 -78
- 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/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
|
}
|