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/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 { kv, section, endSections, spinner, subBox, log, red, dimOrange } from './ui.js';
8
- import { ask, choose } from './prompt.js';
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
- endSections(); // flush so the ask() prompt appears cleanly
17
- env[key] = await ask(`${key}`);
18
- section('Environment variables');
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
- const projectName = await ask('Vercel project name', `fabrica-store-${Date.now()}`);
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
- // git clone output goes directly to inherited stdio — show it outside the box
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
- kv('Vercel', 'Logged in');
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(`Set ${key} (production, preview, development)`);
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 ${repoUrl}`);
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('npx', ['--yes', 'vercel@latest', 'git', 'connect', repoUrl, '--yes'], { cwd: project.target });
87
- if (connect.code === 0) { spin.succeed('Vercel project connected to GitHub repo'); return true; }
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
- const deploySpin = spinner('Creating production deployment');
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('Opening', openTarget);
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
- kv('Updated', `${key} in .env.local`);
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
- kv('Env file', envPath);
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
- kv('Path', project.target);
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
  }