fabrica-e-commerce 0.2.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabrica-e-commerce",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "CLI launcher for deploying Fabrica e-commerce stores — Supabase + Vercel, works on Windows, macOS, Linux & Termux.",
5
5
  "type": "module",
6
6
  "bin": {
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 { collectEnv, cloneRepo, deployToVercel, updateProjectEnv, ensureVercelLogin, runLocally } from './deploy.js';
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 { banner, help, kv, kvSuccess, kvFail, section, endSections, subBox, log, logInfo, logWarn, divider, stepHeader, orange, dimOrange, bold, dim, green, cyan } from './ui.js';
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
- stepHeader(1, 5, 'Dependency check');
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
- stepHeader(2, 5, 'Supabase connect');
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
- stepHeader(3, 5, 'Environment variables');
47
+ // Step 3 Environment variables
48
+ stepHeader(3, 6, 'Environment variables');
39
49
  const env = await collectEnv(supabase);
40
50
 
41
- stepHeader(4, 5, 'Clone storefront');
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
- stepHeader(5, 5, 'Deploy target');
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: '☁ Deploy on Vercel (cloud, shareable URL)', value: 'vercel' },
50
- { name: '💻 Run locally on this computer', value: 'local' },
67
+ { name: '☁ Deploy on Vercelcloud, 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('✓ Deployment complete');
84
+ section('✓ Deployment complete');
67
85
  divider();
68
- kv('Project', record.projectName);
86
+ kvSuccess('Project', record.projectName);
69
87
  kv('GitHub', record.githubRepo || dim('n/a'));
70
- kv('URL', record.productionUrl || dim('n/a'));
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
- log(green('Your Fabrica store is live!'));
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('Run: npx fabrica-e-commerce build');
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 projects', String(projects.length));
108
+ kv('Total', String(projects.length) + ' project' + (projects.length !== 1 ? 's' : ''));
91
109
  divider();
92
110
  for (const p of projects) {
93
- const type = p.type === 'local' ? cyan('local') : green('cloud');
94
- log(`${bold(p.projectName)} ${type} ${dim(p.createdAt?.slice(0,10) || '')}`);
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: ${bold(project.projectName)}`);
107
- kv('Type', project.type === 'local' ? cyan('local') : green('cloud'));
108
- kv('Created', project.createdAt || dim('unknown'));
109
- kv('Path', project.target || dim('n/a'));
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 || []).join(', ') || dim('none'));
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
- logInfo('No projects found');
125
- log('Run: npx fabrica-e-commerce build');
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', value: 'all' },
133
- { name: 'Local projects only', value: 'local' },
134
- { name: 'Cloud (Vercel) projects only', value: 'cloud' },
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
- log(`No ${typeFilter} projects found`);
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
- const value = await ask(`New value for ${key}`, currentVal || '');
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} ${project.type === 'local' ? '.env.local' : 'Vercel + redeployed'}`);
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('Run: npx fabrica-e-commerce build');
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', value: 'all' },
185
- { name: 'Local projects', value: 'local' },
186
- { name: 'Cloud (Vercel) projects', value: 'cloud' },
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: ${bold(project.projectName)}`);
242
+ section(`Re-running ${bold(orange(project.projectName))}`);
204
243
 
205
244
  if (project.type === 'local') {
206
- kv('Path', project.target);
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 || dim('unknown'));
224
- if (url) log(`Opening ${url} …`);
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
- kv('Package', `fabrica-e-commerce v${await packageVersion()}`);
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
- console.error(`Unknown command: ${command}`);
258
- help();
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 { 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
  }