fabrica-e-commerce 0.1.14 → 0.1.16

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.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Orange themed CMD launcher for deploying Fabrica e-commerce stores with Supabase and Vercel.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,5 +39,8 @@
39
39
  "homepage": "https://github.com/trucount/fabrica-e-c#readme",
40
40
  "bugs": {
41
41
  "url": "https://github.com/trucount/fabrica-e-c/issues"
42
+ },
43
+ "dependencies": {
44
+ "boxen": "^8.0.1"
42
45
  }
43
46
  }
package/src/cli.js CHANGED
@@ -3,13 +3,14 @@ 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, editProjectEnv, ensureVercelLogin, runLocally } from './deploy.js';
6
+ import { collectEnv, cloneRepo, deployToVercel, updateProjectEnv, ensureVercelLogin, runLocally } from './deploy.js';
7
7
  import { createGithubRepoFromClone } from './github.js';
8
8
  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
- import { choose } from './prompt.js';
12
- import { banner, help, kv, section } from './ui.js';
11
+ import { choose, ask } from './prompt.js';
12
+ import { banner, help, kv, section, subBox, resetSectionCount } from './ui.js';
13
+ import { openUrl } from './system.js';
13
14
 
14
15
  async function packageVersion() {
15
16
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -17,6 +18,7 @@ async function packageVersion() {
17
18
  return pkg.version;
18
19
  }
19
20
 
21
+ // ── build ─────────────────────────────────────────────────────────────────────
20
22
  async function build() {
21
23
  banner();
22
24
 
@@ -30,10 +32,10 @@ async function build() {
30
32
  const env = await collectEnv(supabase);
31
33
  const project = await cloneRepo();
32
34
 
33
- section('Step 3: Run target');
35
+ section('Run target');
34
36
  const target = await choose('Where should Fabrica run this storefront?', [
35
37
  { name: 'Deploy on Vercel cloud', value: 'vercel' },
36
- { name: 'Run locally on this computer', value: 'local' }
38
+ { name: 'Run locally on this computer', value: 'local' },
37
39
  ]);
38
40
 
39
41
  if (target === 'local') {
@@ -45,46 +47,189 @@ async function build() {
45
47
  await ensureVercelLogin();
46
48
  const githubRepo = await createGithubRepoFromClone(project);
47
49
  const record = await deployToVercel(project, env, githubRepo);
50
+
48
51
  section('Done');
49
52
  kv('Project', record.projectName);
50
53
  kv('GitHub repo', record.githubRepo || 'n/a');
54
+ kv('URL', record.productionUrl || 'n/a');
51
55
  kv('Path', record.target);
52
56
  }
53
57
 
58
+ // ── list ──────────────────────────────────────────────────────────────────────
54
59
  async function list() {
55
60
  banner();
56
61
  const projects = await readProjects();
57
62
  if (!projects.length) {
58
- console.log('No projects found. Run: fabrica build');
63
+ console.log(' No projects found. Run: fabrica build');
64
+ return;
65
+ }
66
+
67
+ section('Your Fabrica projects');
68
+ const choices = projects.map((p) => ({
69
+ name: `${p.projectName} (${p.type === 'local' ? 'local' : 'cloud'})`,
70
+ value: p.id,
71
+ }));
72
+
73
+ const selected = await choose('Select a project to view details:', choices);
74
+ const project = projects.find((p) => p.id === selected);
75
+
76
+ section(`Project: ${project.projectName}`);
77
+ const lines = [
78
+ `Name: ${project.projectName}`,
79
+ `Type: ${project.type === 'local' ? 'local' : 'cloud'}`,
80
+ `Created: ${project.createdAt}`,
81
+ `Path: ${project.target}`,
82
+ `GitHub: ${project.githubRepo || 'n/a'}`,
83
+ `Supabase: ${project.supabaseUrl || 'n/a'}`,
84
+ `URL: ${project.productionUrl || 'n/a'}`,
85
+ `Env keys: ${(project.envKeys || []).join(', ')}`,
86
+ ];
87
+ subBox(lines);
88
+ }
89
+
90
+ // ── env ───────────────────────────────────────────────────────────────────────
91
+ async function env() {
92
+ banner();
93
+ const projects = await readProjects();
94
+ if (!projects.length) {
95
+ console.log(' No projects found. Run: fabrica build');
59
96
  return;
60
97
  }
61
- projects.forEach((project, index) => {
62
- console.log(`\n${index + 1}. ${project.projectName}`);
63
- kv('Created', project.createdAt);
98
+
99
+ section('Environment manager');
100
+
101
+ // Step 1 — pick project type filter
102
+ const typeFilter = await choose('Which projects to show?', [
103
+ { name: 'All projects', value: 'all' },
104
+ { name: 'Local projects only', value: 'local' },
105
+ { name: 'Cloud (Vercel) projects only', value: 'cloud' },
106
+ ]);
107
+
108
+ const filtered = typeFilter === 'all' ? projects : projects.filter((p) => p.type === typeFilter);
109
+ if (!filtered.length) {
110
+ console.log(` No ${typeFilter} projects found.`);
111
+ return;
112
+ }
113
+
114
+ // Step 2 — pick project
115
+ const projectId = await choose('Select project:', filtered.map((p) => ({
116
+ name: `${p.projectName} (${p.type === 'local' ? 'local' : 'cloud'})`,
117
+ value: p.id,
118
+ })));
119
+ const project = filtered.find((p) => p.id === projectId);
120
+
121
+ // Step 3 — pick env key
122
+ const envKeys = project.envKeys || [];
123
+ if (!envKeys.length) {
124
+ console.log(' No env keys stored for this project.');
125
+ return;
126
+ }
127
+
128
+ const key = await choose('Select env variable to update:', envKeys.map((k) => ({ name: k, value: k })));
129
+
130
+ // Step 4 — new value
131
+ const currentVal = (project.env || {})[key];
132
+ const value = await ask(`New value for ${key}`, currentVal || '');
133
+
134
+ section('Applying update');
135
+ await updateProjectEnv(project, key, value);
136
+ kv('Updated', `${key} → ${project.type === 'local' ? '.env.local' : 'Vercel + redeployed'}`);
137
+ }
138
+
139
+ // ── rerun ─────────────────────────────────────────────────────────────────────
140
+ async function rerun() {
141
+ banner();
142
+ const projects = await readProjects();
143
+ if (!projects.length) {
144
+ console.log(' No projects found. Run: fabrica build');
145
+ return;
146
+ }
147
+
148
+ section('Re-run / re-open project');
149
+
150
+ // Step 1 — local or cloud
151
+ const typeFilter = await choose('Which type of project?', [
152
+ { name: 'All projects', value: 'all' },
153
+ { name: 'Local projects', value: 'local' },
154
+ { name: 'Cloud (Vercel) projects', value: 'cloud' },
155
+ ]);
156
+
157
+ const filtered = typeFilter === 'all' ? projects : projects.filter((p) => p.type === typeFilter);
158
+ if (!filtered.length) {
159
+ console.log(` No ${typeFilter} projects found.`);
160
+ return;
161
+ }
162
+
163
+ // Step 2 — pick project
164
+ const projectId = await choose('Select project to re-run:', filtered.map((p) => ({
165
+ name: `${p.projectName} (${p.type === 'local' ? 'local' : 'cloud'})`,
166
+ value: p.id,
167
+ })));
168
+ const project = filtered.find((p) => p.id === projectId);
169
+
170
+ section(`Re-running: ${project.projectName}`);
171
+
172
+ if (project.type === 'local') {
173
+ // Re-run local dev server
64
174
  kv('Path', project.target);
65
- kv('GitHub repo', project.githubRepo || 'n/a');
66
- kv('Supabase', project.supabaseUrl);
67
- kv('Env keys', project.envKeys.join(', '));
68
- });
69
- await editProjectEnv(projects);
175
+ kv('URL', 'http://localhost:3000');
176
+
177
+ const { runCommand } = await import('./system.js');
178
+ const { access } = await import('node:fs/promises');
179
+ const hasPnpmLock = await access(path.join(project.target, 'pnpm-lock.yaml')).then(() => true, () => false);
180
+ const { runCommandCapture } = await import('./system.js');
181
+ const pnpmAvailable = (await runCommandCapture('pnpm', ['--version'])).code === 0;
182
+ const devCommand = hasPnpmLock && pnpmAvailable ? ['pnpm', ['run', 'dev']] : ['npm', ['run', 'dev']];
183
+
184
+ console.log(' Starting dev server... auto-opening browser in 3s');
185
+ setTimeout(() => openUrl('http://localhost:3000'), 3000);
186
+ await runCommand(devCommand[0], devCommand[1], { cwd: project.target });
187
+ } else {
188
+ // Cloud: show info + open URL
189
+ const url = project.productionUrl || null;
190
+ const lines = [
191
+ `Project: ${project.projectName}`,
192
+ `GitHub: ${project.githubRepo || 'n/a'}`,
193
+ `URL: ${url || 'n/a'}`,
194
+ `Inspect: ${project.inspectUrl || 'n/a'}`,
195
+ `Created: ${project.createdAt}`,
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
+ }
204
+ }
70
205
  }
71
206
 
207
+ // ── info ──────────────────────────────────────────────────────────────────────
72
208
  async function info() {
73
209
  banner();
74
- kv('Package', `fabrica-e-commerce v${await packageVersion()}`);
75
- kv('Bridge', BRIDGE_ORIGIN);
76
- kv('Store repo', STORE_REPO);
77
- kv('Local data', dataDir);
78
- kv('Node', process.version);
210
+ section('Package info');
211
+ const lines = [
212
+ `Package: fabrica-e-commerce v${await packageVersion()}`,
213
+ `Bridge: ${BRIDGE_ORIGIN}`,
214
+ `Store repo: ${STORE_REPO}`,
215
+ `Local data: ${dataDir}`,
216
+ `Node: ${process.version}`,
217
+ '',
218
+ 'Creator: SPARROW AI SOLUTION',
219
+ ];
220
+ subBox(lines);
79
221
  }
80
222
 
223
+ // ── router ────────────────────────────────────────────────────────────────────
81
224
  export async function run(args) {
82
225
  const command = args[0] || 'help';
83
- if (command === 'build') return build();
84
- if (command === 'list') return list();
85
- if (command === 'info' || command === '.info') return info();
86
- if (command === 'vins' || command === '/vins') return vinsCommand();
87
- if (command === 'help' || command === '--help' || command === '-h') return help();
226
+ if (command === 'build') return build();
227
+ if (command === 'list') return list();
228
+ if (command === 'env') return env();
229
+ if (command === 'rerun') return rerun();
230
+ if (command === 'info' || command === '.info') return info();
231
+ if (command === 'vins' || command === '/vins') return vinsCommand();
232
+ if (command === 'help' || command === '--help' || command === '-h') return help();
88
233
  console.error(`Unknown command: ${command}`);
89
234
  help();
90
235
  process.exitCode = 1;
package/src/deploy.js CHANGED
@@ -3,8 +3,8 @@ import path from 'node:path';
3
3
  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
- import { runCommand, runCommandCapture } from './system.js';
7
- import { dimOrange, kv, section, spinner } from './ui.js';
6
+ import { runCommand, runCommandCapture, openUrl } from './system.js';
7
+ import { dimOrange, kv, section, spinner, subBox, red, orange } from './ui.js';
8
8
  import { ask, choose } from './prompt.js';
9
9
 
10
10
  const VERCEL_ENVIRONMENTS = ['production', 'preview', 'development'];
@@ -15,6 +15,7 @@ export async function collectEnv(supabase) {
15
15
  for (const key of REQUIRED_ENV_KEYS) env[key] = await ask(`${key}`);
16
16
  return env;
17
17
  }
18
+
18
19
  export async function cloneRepo() {
19
20
  section('Clone storefront');
20
21
  const projectName = await ask('Vercel project name', `fabrica-store-${Date.now()}`);
@@ -23,14 +24,12 @@ export async function cloneRepo() {
23
24
  await runCommand('git', ['clone', STORE_REPO, target]);
24
25
  return { id, projectName, target };
25
26
  }
27
+
26
28
  export async function isLoggedInToVercel() {
27
29
  const result = await runCommandCapture('npx', ['--yes', 'vercel@latest', 'whoami']);
28
30
  return result.code === 0;
29
31
  }
30
32
 
31
- // Step 3: verify Vercel login before anything else touches Vercel. If the
32
- // user isn't logged in, run the real interactive `vercel login` flow (same
33
- // as typing it in a terminal) and block until it succeeds.
34
33
  export async function ensureVercelLogin() {
35
34
  section('Vercel login');
36
35
  const spin = spinner('Checking Vercel login');
@@ -39,7 +38,7 @@ export async function ensureVercelLogin() {
39
38
  return;
40
39
  }
41
40
  spin.fail('Not logged in to Vercel');
42
- console.log('Opening "vercel login" — finish the login in your browser...');
41
+ console.log(' Opening "vercel login" — finish the login in your browser...');
43
42
  await runCommand('npx', ['vercel@latest', 'login']);
44
43
  if (!(await isLoggedInToVercel())) {
45
44
  throw new Error('Vercel login was not completed. Run "fabrica build" again after logging in.');
@@ -47,24 +46,29 @@ export async function ensureVercelLogin() {
47
46
  kv('Vercel', 'Logged in');
48
47
  }
49
48
 
50
- // Sets every env var across production, preview, and development so the
51
- // values are permanent for the project, not just the one deploy.
49
+ // Capture vercel CLI output and render it in a sub-box, errors in red
50
+ async function runVercelBoxed(args, options = {}) {
51
+ const result = await runCommandCapture('npx', ['vercel@latest', ...args], options);
52
+ const raw = ((result.stdout || '') + (result.stderr || '')).trim();
53
+ if (!raw) return result;
54
+
55
+ const lines = raw.split('\n').map((l) => l.trimEnd()).filter(Boolean);
56
+ const isErr = result.code !== 0 || lines.some((l) => /^Error:/i.test(l));
57
+ subBox(lines, { isError: isErr });
58
+ return result;
59
+ }
60
+
52
61
  async function setEnvEverywhere(project, env) {
53
62
  for (const [key, value] of Object.entries(env)) {
54
63
  const spin = spinner(`Setting ${key}`);
55
64
  for (const environment of VERCEL_ENVIRONMENTS) {
56
- await runCommand('npx', ['vercel@latest', 'env', 'rm', key, environment, '--yes'], { cwd: project.target, allowFailure: true });
57
- await runCommand('npx', ['vercel@latest', 'env', 'add', key, environment], { cwd: project.target, input: `${value}\n` });
65
+ await runVercelBoxed(['env', 'rm', key, environment, '--yes'], { cwd: project.target, allowFailure: true });
66
+ await runVercelBoxed(['env', 'add', key, environment], { cwd: project.target, input: `${value}\n` });
58
67
  }
59
68
  spin.succeed(`Set ${key} (production, preview, development)`);
60
69
  }
61
70
  }
62
71
 
63
- // Vercel's GitHub App integration indexes newly created/forked repos on its
64
- // own delay, separate from GitHub itself being aware of the repo. Right after
65
- // a fork, `vercel git connect` can fail simply because Vercel hasn't synced
66
- // yet — not because anything is actually wrong. Retry with backoff before
67
- // giving up, then fall back to a direct deploy with clear next steps.
68
72
  async function connectGithubRepo(project, repoUrl) {
69
73
  const spin = spinner(`Connecting Vercel project to ${repoUrl}`);
70
74
  const attempts = 6;
@@ -72,17 +76,21 @@ async function connectGithubRepo(project, repoUrl) {
72
76
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
73
77
  const connect = await runCommandCapture('npx', ['--yes', 'vercel@latest', 'git', 'connect', repoUrl, '--yes'], { cwd: project.target });
74
78
  if (connect.code === 0) {
75
- spin.succeed('Vercel project connected to GitHub repo (future pushes auto-deploy)');
79
+ spin.succeed('Vercel project connected to GitHub repo');
76
80
  return true;
77
81
  }
78
82
  lastError = (connect.stderr || connect.stdout || '').trim();
79
83
  if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, attempt * 3000));
80
84
  }
81
85
  spin.fail('Could not auto-connect Git — continuing with a direct deploy');
82
- console.log(dimOrange(' This usually means one of two things:'));
83
- console.log(dimOrange(` 1) Vercel hasn't finished indexing the new fork yet (run "fabrica list" in a minute and re-link manually with: npx vercel git connect ${repoUrl})`));
84
- console.log(dimOrange(' 2) The "Vercel" GitHub App is set to "Only select repositories" and was never granted access to the new fork — fix at https://github.com/settings/installations'));
85
- if (lastError) console.log(dimOrange(` Last error: ${lastError}`));
86
+ subBox([
87
+ 'This usually means one of two things:',
88
+ `1) Vercel hasn't finished indexing the new fork yet`,
89
+ ` Re-link manually: npx vercel git connect ${repoUrl}`,
90
+ '2) The "Vercel" GitHub App is set to "Only select repositories"',
91
+ ' Fix at: https://github.com/settings/installations',
92
+ lastError ? `Last error: ${lastError}` : '',
93
+ ].filter(Boolean), { isError: true });
86
94
  return false;
87
95
  }
88
96
 
@@ -90,7 +98,7 @@ export async function deployToVercel(project, env, githubRepo) {
90
98
  section('Vercel deployment');
91
99
  await ensureVercelLogin();
92
100
 
93
- await runCommand('npx', ['vercel@latest', 'link', '--yes', '--project', project.projectName], { cwd: project.target });
101
+ await runVercelBoxed(['link', '--yes', '--project', project.projectName], { cwd: project.target });
94
102
 
95
103
  if (githubRepo?.repoUrl) {
96
104
  await connectGithubRepo(project, githubRepo.repoUrl);
@@ -99,35 +107,44 @@ export async function deployToVercel(project, env, githubRepo) {
99
107
  await setEnvEverywhere(project, env);
100
108
 
101
109
  const deploySpin = spinner('Creating production deployment');
102
- await runCommand('npx', ['vercel@latest', '--prod', '--yes'], { cwd: project.target });
110
+ const deployResult = await runCommandCapture('npx', ['vercel@latest', '--prod', '--yes'], { cwd: project.target });
111
+ const deployOutput = ((deployResult.stdout || '') + (deployResult.stderr || '')).trim();
112
+
113
+ // Extract URLs from deploy output
114
+ let productionUrl = null;
115
+ let aliasedUrl = null;
116
+ let inspectUrl = null;
117
+ for (const line of deployOutput.split('\n')) {
118
+ const m = line.match(/Production\s+(\S+)/); if (m) productionUrl = m[1];
119
+ const a = line.match(/Aliased\s+(\S+)/); if (a) aliasedUrl = a[1];
120
+ const i = line.match(/Inspect\s+(\S+)/); if (i) inspectUrl = i[1];
121
+ }
122
+
123
+ subBox(deployOutput.split('\n').filter(Boolean));
103
124
  deploySpin.succeed('Production deployment created');
104
125
 
126
+ // Auto-open the aliased/production URL
127
+ const openTarget = aliasedUrl || productionUrl;
128
+ if (openTarget) {
129
+ kv('Opening', openTarget);
130
+ await openUrl(openTarget);
131
+ }
132
+
105
133
  const record = {
106
134
  ...project,
135
+ type: 'cloud',
107
136
  repo: STORE_REPO,
108
137
  githubRepo: githubRepo?.repoUrl || null,
109
138
  createdAt: new Date().toISOString(),
110
139
  envKeys: Object.keys(env),
111
- supabaseUrl: env.SUPABASE_URL
140
+ env,
141
+ supabaseUrl: env.SUPABASE_URL,
142
+ productionUrl: aliasedUrl || productionUrl || null,
143
+ inspectUrl: inspectUrl || null,
112
144
  };
113
145
  await saveProject(record);
114
146
  return record;
115
147
  }
116
- export async function editProjectEnv(projects) {
117
- if (!projects.length) { console.log('No deployed projects saved yet. Run build first.'); return; }
118
- const projectId = await choose('Select project:', projects.map((project) => ({ name: `${project.projectName} (${project.createdAt})`, value: project.id })));
119
- const project = projects.find((item) => item.id === projectId);
120
- const key = await choose('Variable to replace:', project.envKeys.map((item) => ({ name: item, value: item })));
121
- const value = await ask(`New value for ${key}`);
122
- await ensureVercelLogin();
123
- for (const environment of VERCEL_ENVIRONMENTS) {
124
- await runCommand('npx', ['vercel@latest', 'env', 'rm', key, environment, '--yes'], { cwd: project.target, allowFailure: true });
125
- await runCommand('npx', ['vercel@latest', 'env', 'add', key, environment], { cwd: project.target, input: `${value}\n` });
126
- }
127
- await runCommand('npx', ['vercel@latest', '--prod', '--yes'], { cwd: project.target });
128
- kv('Redeployed', project.projectName);
129
- }
130
-
131
148
 
132
149
  export async function runLocally(project, env) {
133
150
  section('Local Next.js setup');
@@ -142,9 +159,56 @@ export async function runLocally(project, env) {
142
159
  const devCommand = hasPnpmLock && pnpmAvailable ? ['pnpm', ['run', 'dev']] : ['npm', ['run', 'dev']];
143
160
 
144
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
+
145
176
  section('Local app');
146
177
  kv('Path', project.target);
147
178
  kv('URL', 'http://localhost:3000');
148
- console.log('Starting the Next.js dev server. Press Ctrl+C to stop it.');
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);
149
183
  await runCommand(devCommand[0], devCommand[1], { cwd: project.target });
150
184
  }
185
+
186
+ // Called by env command - update a single env var for cloud or local project
187
+ export async function updateProjectEnv(project, key, value) {
188
+ if (project.type === 'local') {
189
+ // Update .env.local
190
+ const envPath = path.join(project.target, '.env.local');
191
+ let contents = '';
192
+ try { contents = await fs.readFile(envPath, 'utf8'); } catch { /* no file yet */ }
193
+ const lines = contents.split('\n');
194
+ const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
195
+ const newLine = `${key}=${value}`;
196
+ if (idx >= 0) lines[idx] = newLine;
197
+ else lines.push(newLine);
198
+ await fs.writeFile(envPath, lines.join('\n'), 'utf8');
199
+ kv('Updated', `${key} in .env.local`);
200
+ } else {
201
+ // Cloud: update on Vercel + redeploy
202
+ await ensureVercelLogin();
203
+ for (const environment of VERCEL_ENVIRONMENTS) {
204
+ await runVercelBoxed(['env', 'rm', key, environment, '--yes'], { cwd: project.target, allowFailure: true });
205
+ await runVercelBoxed(['env', 'add', key, environment], { cwd: project.target, input: `${value}\n` });
206
+ }
207
+ const spin = spinner('Redeploying...');
208
+ await runVercelBoxed(['--prod', '--yes'], { cwd: project.target });
209
+ spin.succeed(`Redeployed ${project.projectName}`);
210
+ }
211
+ // Persist updated env in store
212
+ const updated = { ...project, env: { ...(project.env || {}), [key]: value } };
213
+ await saveProject(updated);
214
+ }
package/src/deps.js CHANGED
@@ -2,15 +2,18 @@ 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 } from './ui.js';
5
+ import { kv, section, spinner, subBox } from './ui.js';
6
+
7
+ // ── platform detection ────────────────────────────────────────────────────────
8
+ function isTermux() {
9
+ return process.platform === 'android' ||
10
+ (process.platform === 'linux' && (process.env.TERMUX_VERSION || process.env.PREFIX?.includes('com.termux')));
11
+ }
6
12
 
7
- // Some systems (root containers, minimal CI images) have no `sudo` binary
8
- // at all. Fall back to running the command directly in that case instead
9
- // of failing outright.
10
13
  async function runPrivileged(command, args, options = {}) {
11
- if (await commandExists('sudo')) {
12
- return runCommand('sudo', [command, ...args], options);
13
- }
14
+ if (process.platform === 'win32') return runCommand(command, args, options);
15
+ if (isTermux()) return runCommand(command, args, options); // Termux: no sudo
16
+ if (await commandExists('sudo')) return runCommand('sudo', [command, ...args], options);
14
17
  return runCommand(command, args, options);
15
18
  }
16
19
 
@@ -18,9 +21,7 @@ function runPrivilegedShell(script, options = {}) {
18
21
  return runCommand('bash', ['-c', script], options);
19
22
  }
20
23
 
21
- // Dependencies the package actually shells out to. "git" and "gh" (GitHub
22
- // CLI) are real external binaries we depend on; "vercel" is fetched on
23
- // demand through npx so we just confirm npm/npx can resolve it.
24
+ // ── dependency definitions ────────────────────────────────────────────────────
24
25
  const DEPENDENCIES = [
25
26
  {
26
27
  name: 'git',
@@ -45,7 +46,7 @@ const DEPENDENCIES = [
45
46
  }
46
47
  ];
47
48
 
48
-
49
+ // ── PATH helpers ──────────────────────────────────────────────────────────────
49
50
  function addToPathIfDirectory(directory) {
50
51
  if (!directory || !fs.existsSync(directory)) return;
51
52
  const delimiter = path.delimiter;
@@ -58,16 +59,16 @@ function addToPathIfDirectory(directory) {
58
59
 
59
60
  function refreshWindowsToolPaths() {
60
61
  if (process.platform !== 'win32') return;
61
- const localAppData = process.env.LOCALAPPDATA;
62
- const programFiles = process.env.ProgramFiles;
63
- const programFilesX86 = process.env['ProgramFiles(x86)'];
64
- const programData = process.env.ProgramData;
65
- addToPathIfDirectory(localAppData && path.join(localAppData, 'Microsoft', 'WinGet', 'Links'));
66
- addToPathIfDirectory(programFiles && path.join(programFiles, 'GitHub CLI'));
67
- addToPathIfDirectory(programFilesX86 && path.join(programFilesX86, 'GitHub CLI'));
68
- addToPathIfDirectory(programData && path.join(programData, 'chocolatey', 'bin'));
69
- addToPathIfDirectory(programFiles && path.join(programFiles, 'Git', 'cmd'));
70
- addToPathIfDirectory(programFiles && path.join(programFiles, 'nodejs'));
62
+ const lad = process.env.LOCALAPPDATA;
63
+ const pf = process.env.ProgramFiles;
64
+ const pf86 = process.env['ProgramFiles(x86)'];
65
+ const pd = process.env.ProgramData;
66
+ addToPathIfDirectory(lad && path.join(lad, 'Microsoft', 'WinGet', 'Links'));
67
+ addToPathIfDirectory(pf && path.join(pf, 'GitHub CLI'));
68
+ addToPathIfDirectory(pf86 && path.join(pf86, 'GitHub CLI'));
69
+ addToPathIfDirectory(pd && path.join(pd, 'chocolatey', 'bin'));
70
+ addToPathIfDirectory(pf && path.join(pf, 'Git', 'cmd'));
71
+ addToPathIfDirectory(pf && path.join(pf, 'nodejs'));
71
72
  }
72
73
 
73
74
  async function checkWithPathRefresh(command) {
@@ -101,26 +102,35 @@ async function warmVercelCli() {
101
102
  ['vercel', ['--version']]
102
103
  ]);
103
104
  if (warmed.code === 0) return;
104
- // Last resort: install a global Vercel binary so future invocations have a
105
- // real `vercel` executable even when npx/npm exec cannot launch correctly.
106
105
  await runCommand('npm', ['install', '-g', 'vercel@latest'], { allowFailure: true });
107
106
  }
108
107
 
108
+ // ── Git install ───────────────────────────────────────────────────────────────
109
109
  async function installGit() {
110
110
  refreshWindowsToolPaths();
111
111
  const platform = process.platform;
112
+
113
+ if (isTermux()) {
114
+ await runCommand('pkg', ['install', '-y', 'git'], { allowFailure: true });
115
+ return;
116
+ }
117
+
112
118
  if (platform === 'linux') {
113
119
  if (await commandExists('apt-get')) {
114
120
  await runPrivileged('apt-get', ['update'], { allowFailure: true });
115
121
  await runPrivileged('apt-get', ['install', '-y', 'git'], { allowFailure: true });
116
122
  return;
117
123
  }
118
- if (await commandExists('dnf')) return runPrivileged('dnf', ['install', '-y', 'git'], { allowFailure: true });
119
- if (await commandExists('yum')) return runPrivileged('yum', ['install', '-y', 'git'], { allowFailure: true });
124
+ if (await commandExists('dnf')) return runPrivileged('dnf', ['install', '-y', 'git'], { allowFailure: true });
125
+ if (await commandExists('yum')) return runPrivileged('yum', ['install', '-y', 'git'], { allowFailure: true });
120
126
  if (await commandExists('pacman')) return runPrivileged('pacman', ['-Sy', '--noconfirm', 'git'], { allowFailure: true });
127
+ if (await commandExists('zypper')) return runPrivileged('zypper', ['install', '-y', 'git'], { allowFailure: true });
128
+ if (await commandExists('apk')) return runPrivileged('apk', ['add', 'git'], { allowFailure: true });
121
129
  }
122
130
  if (platform === 'darwin') {
123
131
  if (await commandExists('brew')) return runCommand('brew', ['install', 'git'], { allowFailure: true });
132
+ // Xcode command line tools
133
+ await runCommand('xcode-select', ['--install'], { allowFailure: true });
124
134
  }
125
135
  if (platform === 'win32') {
126
136
  if (await commandExists('winget')) {
@@ -131,22 +141,26 @@ async function installGit() {
131
141
  if (await commandExists('choco')) {
132
142
  await runCommand('choco', ['install', 'git', '-y'], { allowFailure: true });
133
143
  refreshWindowsToolPaths();
134
- return;
135
144
  }
136
145
  }
137
146
  }
138
147
 
148
+ // ── GitHub CLI install ────────────────────────────────────────────────────────
139
149
  async function installGithubCli() {
140
150
  refreshWindowsToolPaths();
141
151
  const platform = process.platform;
152
+
153
+ if (isTermux()) {
154
+ await runCommand('pkg', ['install', '-y', 'gh'], { allowFailure: true });
155
+ return;
156
+ }
157
+
142
158
  if (platform === 'linux') {
143
159
  if (await commandExists('apt-get')) {
144
- // Try the plain package first (present on newer Ubuntu/Debian).
145
160
  const direct = await (await commandExists('sudo')
146
161
  ? runCommandCapture('sudo', ['apt-get', 'install', '-y', 'gh'])
147
162
  : runCommandCapture('apt-get', ['install', '-y', 'gh']));
148
163
  if (direct.code === 0) return;
149
- // Fall back to the official GitHub CLI apt repository setup.
150
164
  const usesSudo = await commandExists('sudo');
151
165
  const prefix = usesSudo ? 'sudo ' : '';
152
166
  await runPrivilegedShell([
@@ -162,8 +176,10 @@ async function installGithubCli() {
162
176
  ].join(' && '), { allowFailure: true });
163
177
  return;
164
178
  }
165
- if (await commandExists('dnf')) return runPrivileged('dnf', ['install', '-y', 'gh'], { allowFailure: true });
179
+ if (await commandExists('dnf')) return runPrivileged('dnf', ['install', '-y', 'gh'], { allowFailure: true });
166
180
  if (await commandExists('pacman')) return runPrivileged('pacman', ['-Sy', '--noconfirm', 'github-cli'], { allowFailure: true });
181
+ if (await commandExists('zypper')) return runPrivileged('zypper', ['install', '-y', 'gh'], { allowFailure: true });
182
+ if (await commandExists('apk')) return runCommand('apk', ['add', 'github-cli'], { allowFailure: true });
167
183
  }
168
184
  if (platform === 'darwin') {
169
185
  if (await commandExists('brew')) return runCommand('brew', ['install', 'gh'], { allowFailure: true });
@@ -177,27 +193,20 @@ async function installGithubCli() {
177
193
  if (await commandExists('choco')) {
178
194
  await runCommand('choco', ['install', 'gh', '-y'], { allowFailure: true });
179
195
  refreshWindowsToolPaths();
180
- return;
181
196
  }
182
197
  }
183
198
  }
184
199
 
200
+ // ── Public API ────────────────────────────────────────────────────────────────
185
201
  export async function ensureDependencies({ autoInstall = true, names } = {}) {
186
202
  const targets = names ? DEPENDENCIES.filter((dep) => names.includes(dep.name)) : DEPENDENCIES;
187
203
  const results = [];
188
204
  for (const dep of targets) {
189
205
  const spin = spinner(`Checking ${dep.label}`);
190
206
  let present = await dep.check();
191
- if (present) {
192
- spin.succeed(`${dep.label} found`);
193
- results.push({ ...dep, present, installed: false });
194
- continue;
195
- }
207
+ if (present) { spin.succeed(`${dep.label} found`); results.push({ ...dep, present, installed: false }); continue; }
196
208
  spin.fail(`${dep.label} missing`);
197
- if (!autoInstall) {
198
- results.push({ ...dep, present: false, installed: false });
199
- continue;
200
- }
209
+ if (!autoInstall) { results.push({ ...dep, present: false, installed: false }); continue; }
201
210
  const installSpin = spinner(`Installing ${dep.label}`);
202
211
  await dep.install();
203
212
  present = await dep.check();
@@ -210,21 +219,18 @@ export async function ensureDependencies({ autoInstall = true, names } = {}) {
210
219
 
211
220
  export async function vinsCommand() {
212
221
  section('Fabrica dependency check (vins)');
222
+ const platform = isTermux() ? 'Termux/Android' : process.platform;
223
+ kv('Platform', platform);
213
224
  const results = await ensureDependencies();
214
225
  section('Summary');
215
226
  let allGood = true;
227
+ const summaryLines = [];
216
228
  for (const dep of results) {
217
- kv(dep.label, dep.present ? 'OK' : 'MISSING — install manually');
218
- if (!dep.present) {
219
- allGood = false;
220
- console.log(` Manual install: ${dep.manualUrl}`);
221
- }
222
- }
223
- if (allGood) {
224
- console.log('\nAll dependencies are ready. You can run: fabrica build');
225
- } else {
226
- console.log('\nSome dependencies could not be installed automatically. Install them manually using the links above, then re-run "fabrica vins".');
227
- process.exitCode = 1;
229
+ summaryLines.push(`${dep.label}: ${dep.present ? 'OK' : 'MISSING — install manually'}`);
230
+ if (!dep.present) { allGood = false; summaryLines.push(` Manual: ${dep.manualUrl}`); }
228
231
  }
232
+ subBox(summaryLines, { isError: !allGood });
233
+ if (allGood) console.log('\n All dependencies ready. Run: fabrica build');
234
+ else { console.log('\n Some deps could not be installed automatically.'); process.exitCode = 1; }
229
235
  return results;
230
236
  }
package/src/prompt.js CHANGED
@@ -1,17 +1,79 @@
1
1
  import readline from 'node:readline/promises';
2
2
  import { stdin as input, stdout as output } from 'node:process';
3
+ import { orange, dimOrange, bold } from './ui.js';
3
4
 
5
+ // Raw readline ask (no frills)
4
6
  export async function ask(message, defaultValue = '') {
5
7
  const rl = readline.createInterface({ input, output });
6
8
  const suffix = defaultValue ? ` (${defaultValue})` : '';
7
- const answer = await rl.question(`${message}${suffix}: `);
9
+ const answer = await rl.question(` ${orange('?')} ${bold(message)}${dimOrange(suffix)}: `);
8
10
  rl.close();
9
- return answer || defaultValue;
11
+ return answer.trim() || defaultValue;
10
12
  }
11
13
 
14
+ // Arrow-key + Enter dropdown selector
12
15
  export async function choose(message, choices) {
13
- console.log(message);
14
- choices.forEach((choice, index) => console.log(` ${index + 1}. ${choice.name}`));
15
- const answer = Number(await ask('Select number', '1'));
16
- return choices[Math.max(0, Math.min(choices.length - 1, answer - 1))].value;
16
+ return new Promise((resolve) => {
17
+ if (!process.stdin.isTTY) {
18
+ // Fallback for non-TTY (piped): numbered list
19
+ console.log(` ${orange('◆')} ${bold(message)}`);
20
+ choices.forEach((choice, i) => console.log(` ${dimOrange((i + 1) + '.')} ${choice.name}`));
21
+ const rl = readline.createInterface({ input, output });
22
+ rl.question(` ${orange('?')} Select number (1): `, (answer) => {
23
+ rl.close();
24
+ const idx = Math.max(0, Math.min(choices.length - 1, (parseInt(answer, 10) || 1) - 1));
25
+ resolve(choices[idx].value);
26
+ });
27
+ return;
28
+ }
29
+
30
+ const ESC = '\x1b[';
31
+ let selected = 0;
32
+
33
+ function render(first) {
34
+ if (!first) {
35
+ // Move up to redraw
36
+ process.stdout.write(`\x1b[${choices.length + 1}A`);
37
+ }
38
+ console.log(` ${orange('◆')} ${bold(message)}`);
39
+ choices.forEach((choice, i) => {
40
+ const cursor = i === selected ? orange('▶ ') : ' ';
41
+ const label = i === selected ? bold(orange(choice.name)) : dimOrange(choice.name);
42
+ console.log(` ${cursor}${label}`);
43
+ });
44
+ }
45
+
46
+ render(true);
47
+
48
+ process.stdin.setRawMode(true);
49
+ process.stdin.resume();
50
+ process.stdin.setEncoding('utf8');
51
+
52
+ function onKey(key) {
53
+ if (key === '\x1b[A' || key === '\x1b[D') { // up / left
54
+ selected = (selected - 1 + choices.length) % choices.length;
55
+ render(false);
56
+ } else if (key === '\x1b[B' || key === '\x1b[C') { // down / right
57
+ selected = (selected + 1) % choices.length;
58
+ render(false);
59
+ } else if (key === '\r' || key === '\n') {
60
+ process.stdin.setRawMode(false);
61
+ process.stdin.pause();
62
+ process.stdin.removeListener('data', onKey);
63
+ // print selected
64
+ process.stdout.write(`\x1b[${choices.length + 1}A`);
65
+ console.log(` ${orange('◆')} ${bold(message)}`);
66
+ choices.forEach((choice, i) => {
67
+ if (i === selected) console.log(` ${orange('▶ ')}${bold(orange(choice.name))}`);
68
+ else console.log(` ${dimOrange(' ' + choice.name)}`);
69
+ });
70
+ resolve(choices[selected].value);
71
+ } else if (key === '\x03') { // Ctrl+C
72
+ process.stdin.setRawMode(false);
73
+ process.exit(0);
74
+ }
75
+ }
76
+
77
+ process.stdin.on('data', onKey);
78
+ });
17
79
  }
package/src/ui.js CHANGED
@@ -1,46 +1,125 @@
1
+ import process from 'node:process';
2
+ import boxen from 'boxen';
3
+
4
+ // ── color helpers (raw ANSI, no deps) ────────────────────────────────────────
1
5
  const ESC = '\x1b[';
2
- export const orange = (text) => `${ESC}38;2;255;138;0m${text}${ESC}0m`;
3
- export const dimOrange = (text) => `${ESC}38;2;182;95;0m${text}${ESC}0m`;
4
- export const bold = (text) => `${ESC}1m${text}${ESC}0m`;
6
+ export const orange = (t) => `${ESC}38;2;255;138;0m${t}${ESC}0m`;
7
+ export const dimOrange = (t) => `${ESC}38;2;182;95;0m${t}${ESC}0m`;
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`;
5
12
 
6
- export function banner() {
7
- console.log(orange(String.raw`
8
- ███████╗ █████╗ ██████╗ ██████╗ ██╗ ██████╗ █████╗
9
- ██╔════╝██╔══██╗██╔══██╗██╔══██╗██║██╔════╝██╔══██╗
10
- █████╗ ███████║██████╔╝██████╔╝██║██║ ███████║
11
- ██╔══╝ ██╔══██║██╔══██╗██╔══██╗██║██║ ██╔══██║
12
- ██║ ██║ ██║██████╔╝██║ ██║██║╚██████╗██║ ██║
13
- ╚═╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝╚═╝ ╚═╝`));
14
- console.log(dimOrange(' CMD → OAUTH BRIDGE // SUPABASE + VERCEL DEPLOYER'));
15
- console.log(orange('──────────────────────────────────────────────────────'));
16
- }
17
- export function section(title) { console.log('\n' + orange(`◆ ${title}`)); }
18
- export function kv(key, value) { console.log(`${dimOrange('>')} ${bold(key)} ${dimOrange('→')} ${value}`); }
19
- // Clear the rest of the line (\x1b[K) before writing the final message so a
20
- // shorter success/fail message never leaves leftover characters from the
21
- // longer spinner text trailing after it (e.g. "✓ Git foundGit").
13
+ // ── boxen presets ─────────────────────────────────────────────────────────────
14
+ // Main section box — rounded corners, orange border
15
+ function sectionBox(content, title) {
16
+ return boxen(content, {
17
+ title: title ? bold(orange(title)) : undefined,
18
+ titleAlignment: 'left',
19
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
20
+ margin: { top: 0, bottom: 0, left: 0, right: 0 },
21
+ borderStyle: 'round',
22
+ borderColor: '#ff8a00',
23
+ });
24
+ }
25
+
26
+ // Sub-step box single border, indented, dim orange or red
27
+ function subStepBox(content, isError = false) {
28
+ return boxen(content, {
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
+ });
34
+ }
35
+
36
+ // ── section tracking (connector │ lines between boxes) ───────────────────────
37
+ let _sectionCount = 0;
38
+ export function resetSectionCount() { _sectionCount = 0; }
39
+
40
+ export function section(title) {
41
+ if (_sectionCount > 0) console.log(orange(' │'));
42
+ _sectionCount++;
43
+ console.log(sectionBox('', title));
44
+ }
45
+
46
+ // Print content in a section-style box (with optional title)
47
+ export function box(lines, title) {
48
+ const content = lines.join('\n');
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));
57
+ }
58
+
59
+ // kv inside current flow
60
+ export function kv(key, value) {
61
+ console.log(` ${dimOrange('›')} ${bold(key)} ${dimOrange('→')} ${value}`);
62
+ }
63
+
64
+ // ── spinner ───────────────────────────────────────────────────────────────────
22
65
  const CLEAR_LINE = '\x1b[K';
23
66
  export function spinner(text) {
24
- process.stdout.write(dimOrange('> ') + text);
67
+ process.stdout.write(' ' + dimOrange(' ') + text);
25
68
  return {
26
- succeed(msg) { process.stdout.write(`\r${CLEAR_LINE}${orange('✓')} ${msg}\n`); },
27
- fail(msg) { process.stdout.write(`\r${CLEAR_LINE}${orange('✗')} ${msg}\n`); }
69
+ succeed(msg) { process.stdout.write(`\r${CLEAR_LINE} ${orange('✓')} ${msg}\n`); },
70
+ fail(msg) { process.stdout.write(`\r${CLEAR_LINE} ${red('✗')} ${msg}\n`); },
28
71
  };
29
72
  }
73
+
74
+ // ── banner ────────────────────────────────────────────────────────────────────
75
+ export function banner() {
76
+ _sectionCount = 0;
77
+ const art = [
78
+ '███████╗ █████╗ ██████╗ ██████╗ ██╗ ██████╗ █████╗ ',
79
+ '██╔════╝██╔══██╗██╔══██╗██╔══██╗██║██╔════╝██╔══██╗',
80
+ '█████╗ ███████║██████╔╝██████╔╝██║██║ ███████║',
81
+ '██╔══╝ ██╔══██║██╔══██╗██╔══██╗██║██║ ██╔══██║',
82
+ '██║ ██║ ██║██████╔╝██║ ██║██║╚██████╗██║ ██║',
83
+ '╚═╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝╚═╝ ╚═╝',
84
+ ].map((l) => orange(l)).join('\n');
85
+
86
+ const subtitle = [
87
+ dimOrange('CMD → OAUTH BRIDGE // SUPABASE + VERCEL DEPLOYER'),
88
+ dim('by SPARROW AI SOLUTION'),
89
+ ].join('\n');
90
+
91
+ console.log(boxen(`${art}\n\n${subtitle}`, {
92
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
93
+ margin: { top: 0, bottom: 0, left: 0, right: 0 },
94
+ borderStyle: 'double',
95
+ borderColor: '#ff8a00',
96
+ }));
97
+ }
98
+
99
+ // ── help ──────────────────────────────────────────────────────────────────────
30
100
  export function help() {
31
101
  banner();
32
- console.log(`
33
- ${orange('Commands')}
34
- ${bold('build')} Connect Supabase, collect secrets, then run locally or deploy to Vercel
35
- ${bold('list')} Show deployed Fabrica projects and edit env variables
36
- ${bold('vins')} Verify CLI dependencies (git, gh, vercel) and auto-install anything missing
37
- ${bold('info')} Show package, bridge, repo, and local storage information
38
- ${bold('help')} Show this help screen
39
-
40
- ${orange('Examples')}
41
- npx fabrica-e-commerce build
42
- npx fabrica-e-commerce list
43
- npx fabrica-e-commerce vins
44
- npx fabrica-e-commerce info
45
- `);
102
+ const content = [
103
+ bold(orange('Commands')),
104
+ '',
105
+ ` ${bold('build')} Connect Supabase, collect secrets, deploy or run locally`,
106
+ ` ${bold('list')} Show Fabrica projects (local & cloud)`,
107
+ ` ${bold('env')} View and update environment variables for any project`,
108
+ ` ${bold('rerun')} Re-run or re-open an existing project`,
109
+ ` ${bold('vins')} Verify & auto-install CLI dependencies`,
110
+ ` ${bold('info')} Package, bridge, repo and storage info`,
111
+ ` ${bold('help')} Show this screen`,
112
+ '',
113
+ bold(orange('Examples')),
114
+ '',
115
+ ` ${dimOrange('$')} npx fabrica-e-commerce build`,
116
+ ` ${dimOrange('$')} npx fabrica-e-commerce env`,
117
+ ` ${dimOrange('$')} npx fabrica-e-commerce rerun`,
118
+ ` ${dimOrange('$')} npx fabrica-e-commerce list`,
119
+ '',
120
+ dim('Creator: SPARROW AI SOLUTION'),
121
+ ].join('\n');
122
+
123
+ console.log(orange(' │'));
124
+ console.log(sectionBox(content, 'Help'));
46
125
  }