fabrica-e-commerce 0.1.0 → 0.1.1

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 CHANGED
@@ -38,8 +38,9 @@ On Windows CMD, these commands do not require Unix tools such as `find` or `xarg
38
38
 
39
39
  ## Commands
40
40
 
41
- - `build` — creates a Fabrica Connect Supabase job, opens the OAuth bridge, asks for required secrets, clones `https://github.com/trucount/fabrica-final-e-c.git`, links a Vercel project, writes production environment variables, and deploys with `vercel --prod`.
42
- - `list` — shows locally saved deployments and lets you replace a saved project's Vercel production environment variable, then redeploys.
41
+ - `build` — checks dependencies (git, GitHub CLI, Vercel CLI), creates a Fabrica Connect Supabase job, opens the OAuth bridge, asks for required secrets, clones `https://github.com/trucount/fabrica-final-e-c.git`, verifies/logs in to Vercel (`vercel login`), pushes that code to a **new GitHub repository** under your account, connects the Vercel project to that repo for continuous deployment, writes every environment variable to production/preview/development, and deploys with `vercel --prod`.
42
+ - `list` — shows locally saved deployments and lets you replace a saved project's Vercel environment variable (across production, preview, and development), then redeploys.
43
+ - `vins` — verifies the CLI's external dependencies (`git`, GitHub CLI `gh`, and the Vercel CLI via `npx`) and automatically installs anything missing using your system's package manager (`apt-get`/`dnf`/`pacman` on Linux, `brew` on macOS, `winget`/`choco` on Windows). Prints manual install links for anything it can't install automatically.
43
44
  - `info` / `.info` — prints package, bridge, repository, and local data paths.
44
45
  - `help` — prints the command guide.
45
46
 
@@ -59,7 +60,22 @@ On Windows CMD, these commands do not require Unix tools such as `find` or `xarg
59
60
  - `UMAMI_API_CLIENT_ENDPOINT=https://api.umami.is/v1`
60
61
  - `SUPABASE_SERVICE_ROLE_KEY=0000`
61
62
  5. Adds `SUPABASE_URL` and `SUPABASE_ANON_KEY` from the bridge response.
62
- 6. Uses `npx vercel@latest link --yes --project <name>`, `vercel env add`, and `vercel --prod --yes` to deploy from the cloned repo.
63
+ 6. **Step 3 deploy:**
64
+ - Verifies you're logged in to Vercel (`vercel whoami`); if not, runs an interactive `vercel login` and waits for it to finish.
65
+ - Logs in to the GitHub CLI if needed (`gh auth login`), detaches the cloned code from the template's git history, and pushes it to a **brand new private GitHub repository** under your account via `gh repo create --source . --push`.
66
+ - Links a new Vercel project with `vercel link --yes --project <name>` and connects it to the new GitHub repo with `vercel git connect` so future pushes auto-deploy.
67
+ - Writes every collected environment variable (including the Supabase URL/anon key) to the **production, preview, and development** environments with `vercel env add`, so the values are permanent for the project, not just the first deploy.
68
+ - Creates the production deployment with `vercel --prod --yes`.
69
+
70
+ ## Dependency check (`vins`)
71
+
72
+ Run `npx fabrica-e-commerce vins` any time to verify the tools the CLI depends on:
73
+
74
+ ```bash
75
+ npx fabrica-e-commerce vins
76
+ ```
77
+
78
+ It checks for `git`, the GitHub CLI (`gh`), and the Vercel CLI (via `npx`), and tries to install anything missing using the right package manager for your OS. If a tool can't be installed automatically (for example, no package manager is available), it prints a manual install link and exits with a non-zero status so scripts can detect the failure.
63
79
 
64
80
 
65
81
 
@@ -99,4 +115,6 @@ Deployment metadata is saved to `~/.fabrica-ecommerce/projects.json`. Secret val
99
115
 
100
116
  - Node.js 18.17+
101
117
  - Git installed and available in PATH
102
- - A Vercel account. The Vercel CLI will prompt/login when required.
118
+ - [GitHub CLI](https://github.com/cli/cli) (`gh`) installed and available in PATH — used to create the new repository and push the storefront code. Logging in (`gh auth login`) is handled interactively by `build` if needed.
119
+ - A Vercel account. The Vercel CLI will prompt/login when required (`build` also runs this check up front).
120
+ - Run `npx fabrica-e-commerce vins` to check for and auto-install `git`/`gh` if either is missing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabrica-e-commerce",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Orange themed CMD launcher for deploying Fabrica e-commerce stores with Supabase and Vercel.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -3,7 +3,9 @@ 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 } from './deploy.js';
6
+ import { collectEnv, cloneRepo, deployToVercel, editProjectEnv, ensureVercelLogin } from './deploy.js';
7
+ import { createGithubRepoFromClone } from './github.js';
8
+ import { ensureDependencies, vinsCommand } from './deps.js';
7
9
  import { BRIDGE_ORIGIN, STORE_REPO } from './config.js';
8
10
  import { dataDir, readProjects } from './store.js';
9
11
  import { banner, help, kv, section } from './ui.js';
@@ -16,15 +18,26 @@ async function packageVersion() {
16
18
 
17
19
  async function build() {
18
20
  banner();
21
+
22
+ section('Dependency check');
23
+ await ensureDependencies({ names: ['git', 'gh', 'vercel'] });
24
+
19
25
  section('Supabase Connect');
20
26
  kv('BRIDGE', 'ONLINE');
21
27
  kv('SQL', 'Prepared securely (hidden from UI)');
22
28
  const supabase = await connectSupabase();
23
29
  const env = await collectEnv(supabase);
24
30
  const project = await cloneRepo();
25
- const record = await deployToVercel(project, env);
31
+
32
+ // Step 3: log in to Vercel first, push the cloned code into a brand new
33
+ // GitHub repo owned by the user, then deploy that repo to a new Vercel
34
+ // project with every env variable (including Supabase) set permanently.
35
+ await ensureVercelLogin();
36
+ const githubRepo = await createGithubRepoFromClone(project);
37
+ const record = await deployToVercel(project, env, githubRepo);
26
38
  section('Done');
27
39
  kv('Project', record.projectName);
40
+ kv('GitHub repo', record.githubRepo || 'n/a');
28
41
  kv('Path', record.target);
29
42
  }
30
43
 
@@ -39,6 +52,7 @@ async function list() {
39
52
  console.log(`\n${index + 1}. ${project.projectName}`);
40
53
  kv('Created', project.createdAt);
41
54
  kv('Path', project.target);
55
+ kv('GitHub repo', project.githubRepo || 'n/a');
42
56
  kv('Supabase', project.supabaseUrl);
43
57
  kv('Env keys', project.envKeys.join(', '));
44
58
  });
@@ -59,6 +73,7 @@ export async function run(args) {
59
73
  if (command === 'build') return build();
60
74
  if (command === 'list') return list();
61
75
  if (command === 'info' || command === '.info') return info();
76
+ if (command === 'vins' || command === '/vins') return vinsCommand();
62
77
  if (command === 'help' || command === '--help' || command === '-h') return help();
63
78
  console.error(`Unknown command: ${command}`);
64
79
  help();
package/src/deploy.js CHANGED
@@ -2,10 +2,12 @@ import path from 'node:path';
2
2
  import crypto from 'node:crypto';
3
3
  import { HARDCODED_ENV, REQUIRED_ENV_KEYS, STORE_REPO } from './config.js';
4
4
  import { buildsDir, saveProject } from './store.js';
5
- import { runCommand } from './system.js';
5
+ import { runCommand, runCommandCapture } from './system.js';
6
6
  import { kv, section, spinner } from './ui.js';
7
7
  import { ask, choose } from './prompt.js';
8
8
 
9
+ const VERCEL_ENVIRONMENTS = ['production', 'preview', 'development'];
10
+
9
11
  export async function collectEnv(supabase) {
10
12
  section('Environment variables');
11
13
  const env = { ...HARDCODED_ENV, SUPABASE_URL: supabase.url, SUPABASE_ANON_KEY: supabase.anonKey };
@@ -20,17 +22,70 @@ export async function cloneRepo() {
20
22
  await runCommand('git', ['clone', STORE_REPO, target]);
21
23
  return { id, projectName, target };
22
24
  }
23
- export async function deployToVercel(project, env) {
24
- section('Vercel deployment');
25
- await runCommand('npx', ['vercel@latest', 'link', '--yes', '--project', project.projectName], { cwd: project.target });
25
+ export async function isLoggedInToVercel() {
26
+ const result = await runCommandCapture('npx', ['--yes', 'vercel@latest', 'whoami']);
27
+ return result.code === 0;
28
+ }
29
+
30
+ // Step 3: verify Vercel login before anything else touches Vercel. If the
31
+ // user isn't logged in, run the real interactive `vercel login` flow (same
32
+ // as typing it in a terminal) and block until it succeeds.
33
+ export async function ensureVercelLogin() {
34
+ section('Vercel login');
35
+ const spin = spinner('Checking Vercel login');
36
+ if (await isLoggedInToVercel()) {
37
+ spin.succeed('Already logged in to Vercel');
38
+ return;
39
+ }
40
+ spin.fail('Not logged in to Vercel');
41
+ console.log('Opening "vercel login" — finish the login in your browser...');
42
+ await runCommand('npx', ['vercel@latest', 'login']);
43
+ if (!(await isLoggedInToVercel())) {
44
+ throw new Error('Vercel login was not completed. Run "fabrica build" again after logging in.');
45
+ }
46
+ kv('Vercel', 'Logged in');
47
+ }
48
+
49
+ // Sets every env var across production, preview, and development so the
50
+ // values are permanent for the project, not just the one deploy.
51
+ async function setEnvEverywhere(project, env) {
26
52
  for (const [key, value] of Object.entries(env)) {
27
53
  const spin = spinner(`Setting ${key}`);
28
- await runCommand('npx', ['vercel@latest', 'env', 'rm', key, 'production', '--yes'], { cwd: project.target, allowFailure: true });
29
- await runCommand('npx', ['vercel@latest', 'env', 'add', key, 'production'], { cwd: project.target, input: `${value}\n` });
30
- spin.succeed(`Set ${key}`);
54
+ for (const environment of VERCEL_ENVIRONMENTS) {
55
+ await runCommand('npx', ['vercel@latest', 'env', 'rm', key, environment, '--yes'], { cwd: project.target, allowFailure: true });
56
+ await runCommand('npx', ['vercel@latest', 'env', 'add', key, environment], { cwd: project.target, input: `${value}\n` });
57
+ }
58
+ spin.succeed(`Set ${key} (production, preview, development)`);
59
+ }
60
+ }
61
+
62
+ export async function deployToVercel(project, env, githubRepo) {
63
+ section('Vercel deployment');
64
+ await ensureVercelLogin();
65
+
66
+ await runCommand('npx', ['vercel@latest', 'link', '--yes', '--project', project.projectName], { cwd: project.target });
67
+
68
+ if (githubRepo?.repoUrl) {
69
+ const spin = spinner(`Connecting Vercel project to ${githubRepo.repoUrl}`);
70
+ const connect = await runCommandCapture('npx', ['--yes', 'vercel@latest', 'git', 'connect', githubRepo.repoUrl, '--yes'], { cwd: project.target });
71
+ if (connect.code === 0) spin.succeed('Vercel project connected to GitHub repo (future pushes auto-deploy)');
72
+ else spin.fail('Could not auto-connect Git — continuing with a direct deploy');
31
73
  }
74
+
75
+ await setEnvEverywhere(project, env);
76
+
77
+ const deploySpin = spinner('Creating production deployment');
32
78
  await runCommand('npx', ['vercel@latest', '--prod', '--yes'], { cwd: project.target });
33
- const record = { ...project, repo: STORE_REPO, createdAt: new Date().toISOString(), envKeys: Object.keys(env), supabaseUrl: env.SUPABASE_URL };
79
+ deploySpin.succeed('Production deployment created');
80
+
81
+ const record = {
82
+ ...project,
83
+ repo: STORE_REPO,
84
+ githubRepo: githubRepo?.repoUrl || null,
85
+ createdAt: new Date().toISOString(),
86
+ envKeys: Object.keys(env),
87
+ supabaseUrl: env.SUPABASE_URL
88
+ };
34
89
  await saveProject(record);
35
90
  return record;
36
91
  }
@@ -40,8 +95,11 @@ export async function editProjectEnv(projects) {
40
95
  const project = projects.find((item) => item.id === projectId);
41
96
  const key = await choose('Variable to replace:', project.envKeys.map((item) => ({ name: item, value: item })));
42
97
  const value = await ask(`New value for ${key}`);
43
- await runCommand('npx', ['vercel@latest', 'env', 'rm', key, 'production', '--yes'], { cwd: project.target, allowFailure: true });
44
- await runCommand('npx', ['vercel@latest', 'env', 'add', key, 'production'], { cwd: project.target, input: `${value}\n` });
98
+ await ensureVercelLogin();
99
+ for (const environment of VERCEL_ENVIRONMENTS) {
100
+ await runCommand('npx', ['vercel@latest', 'env', 'rm', key, environment, '--yes'], { cwd: project.target, allowFailure: true });
101
+ await runCommand('npx', ['vercel@latest', 'env', 'add', key, environment], { cwd: project.target, input: `${value}\n` });
102
+ }
45
103
  await runCommand('npx', ['vercel@latest', '--prod', '--yes'], { cwd: project.target });
46
104
  kv('Redeployed', project.projectName);
47
105
  }
package/src/deps.js ADDED
@@ -0,0 +1,160 @@
1
+ import process from 'node:process';
2
+ import { commandExists, runCommand, runCommandCapture } from './system.js';
3
+ import { kv, section, spinner } from './ui.js';
4
+
5
+ // Some systems (root containers, minimal CI images) have no `sudo` binary
6
+ // at all. Fall back to running the command directly in that case instead
7
+ // of failing outright.
8
+ async function runPrivileged(command, args, options = {}) {
9
+ if (await commandExists('sudo')) {
10
+ return runCommand('sudo', [command, ...args], options);
11
+ }
12
+ return runCommand(command, args, options);
13
+ }
14
+
15
+ function runPrivilegedShell(script, options = {}) {
16
+ return runCommand('bash', ['-c', script], options);
17
+ }
18
+
19
+ // Dependencies the package actually shells out to. "git" and "gh" (GitHub
20
+ // CLI) are real external binaries we depend on; "vercel" is fetched on
21
+ // demand through npx so we just confirm npm/npx can resolve it.
22
+ const DEPENDENCIES = [
23
+ {
24
+ name: 'git',
25
+ label: 'Git',
26
+ check: () => commandExists('git'),
27
+ install: installGit,
28
+ manualUrl: 'https://git-scm.com/downloads'
29
+ },
30
+ {
31
+ name: 'gh',
32
+ label: 'GitHub CLI (gh)',
33
+ check: () => commandExists('gh'),
34
+ install: installGithubCli,
35
+ manualUrl: 'https://github.com/cli/cli#installation'
36
+ },
37
+ {
38
+ name: 'vercel',
39
+ label: 'Vercel CLI (via npx)',
40
+ check: checkVercelCli,
41
+ install: warmVercelCli,
42
+ manualUrl: 'https://vercel.com/docs/cli'
43
+ }
44
+ ];
45
+
46
+ async function checkVercelCli() {
47
+ const result = await runCommandCapture('npx', ['--yes', 'vercel@latest', '--version']);
48
+ return result.code === 0;
49
+ }
50
+
51
+ async function warmVercelCli() {
52
+ // npx fetches vercel on first use, so "installing" it just means priming
53
+ // the npm cache once so later `build`/`list` calls are instant.
54
+ await runCommand('npx', ['--yes', 'vercel@latest', '--version'], { allowFailure: true });
55
+ }
56
+
57
+ async function installGit() {
58
+ const platform = process.platform;
59
+ if (platform === 'linux') {
60
+ if (await commandExists('apt-get')) {
61
+ await runPrivileged('apt-get', ['update'], { allowFailure: true });
62
+ await runPrivileged('apt-get', ['install', '-y', 'git'], { allowFailure: true });
63
+ return;
64
+ }
65
+ if (await commandExists('dnf')) return runPrivileged('dnf', ['install', '-y', 'git'], { allowFailure: true });
66
+ if (await commandExists('yum')) return runPrivileged('yum', ['install', '-y', 'git'], { allowFailure: true });
67
+ if (await commandExists('pacman')) return runPrivileged('pacman', ['-Sy', '--noconfirm', 'git'], { allowFailure: true });
68
+ }
69
+ if (platform === 'darwin') {
70
+ if (await commandExists('brew')) return runCommand('brew', ['install', 'git'], { allowFailure: true });
71
+ }
72
+ if (platform === 'win32') {
73
+ if (await commandExists('winget')) return runCommand('winget', ['install', '--id', 'Git.Git', '-e', '--source', 'winget'], { allowFailure: true });
74
+ if (await commandExists('choco')) return runCommand('choco', ['install', 'git', '-y'], { allowFailure: true });
75
+ }
76
+ }
77
+
78
+ async function installGithubCli() {
79
+ const platform = process.platform;
80
+ if (platform === 'linux') {
81
+ if (await commandExists('apt-get')) {
82
+ // Try the plain package first (present on newer Ubuntu/Debian).
83
+ const direct = await (await commandExists('sudo')
84
+ ? runCommandCapture('sudo', ['apt-get', 'install', '-y', 'gh'])
85
+ : runCommandCapture('apt-get', ['install', '-y', 'gh']));
86
+ if (direct.code === 0) return;
87
+ // Fall back to the official GitHub CLI apt repository setup.
88
+ const usesSudo = await commandExists('sudo');
89
+ const prefix = usesSudo ? 'sudo ' : '';
90
+ await runPrivilegedShell([
91
+ 'set -e',
92
+ `${prefix}apt-get update`,
93
+ `${prefix}apt-get install -y curl ca-certificates gnupg`,
94
+ `${prefix}mkdir -p -m 755 /etc/apt/keyrings`,
95
+ `curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | ${prefix}tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null`,
96
+ `${prefix}chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg`,
97
+ `echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | ${prefix}tee /etc/apt/sources.list.d/github-cli.list > /dev/null`,
98
+ `${prefix}apt-get update`,
99
+ `${prefix}apt-get install -y gh`
100
+ ].join(' && '), { allowFailure: true });
101
+ return;
102
+ }
103
+ if (await commandExists('dnf')) return runPrivileged('dnf', ['install', '-y', 'gh'], { allowFailure: true });
104
+ if (await commandExists('pacman')) return runPrivileged('pacman', ['-Sy', '--noconfirm', 'github-cli'], { allowFailure: true });
105
+ }
106
+ if (platform === 'darwin') {
107
+ if (await commandExists('brew')) return runCommand('brew', ['install', 'gh'], { allowFailure: true });
108
+ }
109
+ if (platform === 'win32') {
110
+ if (await commandExists('winget')) return runCommand('winget', ['install', '--id', 'GitHub.cli', '-e', '--source', 'winget'], { allowFailure: true });
111
+ if (await commandExists('choco')) return runCommand('choco', ['install', 'gh', '-y'], { allowFailure: true });
112
+ }
113
+ }
114
+
115
+ export async function ensureDependencies({ autoInstall = true, names } = {}) {
116
+ const targets = names ? DEPENDENCIES.filter((dep) => names.includes(dep.name)) : DEPENDENCIES;
117
+ const results = [];
118
+ for (const dep of targets) {
119
+ const spin = spinner(`Checking ${dep.label}`);
120
+ let present = await dep.check();
121
+ if (present) {
122
+ spin.succeed(`${dep.label} found`);
123
+ results.push({ ...dep, present, installed: false });
124
+ continue;
125
+ }
126
+ spin.fail(`${dep.label} missing`);
127
+ if (!autoInstall) {
128
+ results.push({ ...dep, present: false, installed: false });
129
+ continue;
130
+ }
131
+ const installSpin = spinner(`Installing ${dep.label}`);
132
+ await dep.install();
133
+ present = await dep.check();
134
+ if (present) installSpin.succeed(`${dep.label} installed`);
135
+ else installSpin.fail(`${dep.label} could not be installed automatically`);
136
+ results.push({ ...dep, present, installed: present });
137
+ }
138
+ return results;
139
+ }
140
+
141
+ export async function vinsCommand() {
142
+ section('Fabrica dependency check (vins)');
143
+ const results = await ensureDependencies();
144
+ section('Summary');
145
+ let allGood = true;
146
+ for (const dep of results) {
147
+ kv(dep.label, dep.present ? 'OK' : 'MISSING — install manually');
148
+ if (!dep.present) {
149
+ allGood = false;
150
+ console.log(` Manual install: ${dep.manualUrl}`);
151
+ }
152
+ }
153
+ if (allGood) {
154
+ console.log('\nAll dependencies are ready. You can run: fabrica build');
155
+ } else {
156
+ console.log('\nSome dependencies could not be installed automatically. Install them manually using the links above, then re-run "fabrica vins".');
157
+ process.exitCode = 1;
158
+ }
159
+ return results;
160
+ }
package/src/github.js ADDED
@@ -0,0 +1,71 @@
1
+ import process from 'node:process';
2
+ import { commandExists, runCommand, runCommandCapture } from './system.js';
3
+ import { kv, section, spinner } from './ui.js';
4
+
5
+ export async function isGithubCliInstalled() {
6
+ return commandExists('gh');
7
+ }
8
+
9
+ export async function isLoggedInToGithub() {
10
+ const result = await runCommandCapture('gh', ['auth', 'status']);
11
+ return result.code === 0;
12
+ }
13
+
14
+ export async function ensureGithubLogin() {
15
+ section('GitHub login');
16
+ if (!(await isGithubCliInstalled())) {
17
+ throw new Error('GitHub CLI (gh) is not installed. Run "fabrica vins" to install dependencies, then try again.');
18
+ }
19
+ const spin = spinner('Checking GitHub CLI login');
20
+ if (await isLoggedInToGithub()) {
21
+ spin.succeed('Already logged in to GitHub');
22
+ return;
23
+ }
24
+ spin.fail('Not logged in to GitHub');
25
+ console.log('A browser/device flow will open so you can log in with "gh auth login"...');
26
+ await runCommand('gh', ['auth', 'login', '--hostname', 'github.com', '--git-protocol', 'https', '--web']);
27
+ if (!(await isLoggedInToGithub())) {
28
+ throw new Error('GitHub login was not completed. Run "fabrica build" again after logging in with "gh auth login".');
29
+ }
30
+ kv('GitHub', 'Logged in');
31
+ }
32
+
33
+ async function getGithubLogin() {
34
+ const result = await runCommandCapture('gh', ['api', 'user', '-q', '.login']);
35
+ if (result.code !== 0) throw new Error('Could not determine the logged in GitHub account.');
36
+ return result.stdout.trim();
37
+ }
38
+
39
+ // Re-homes the cloned storefront code into a brand new GitHub repository
40
+ // owned by the logged-in user, so the deployed Vercel project can stay
41
+ // connected to that repo (git push -> auto deploy) instead of the original
42
+ // template repository.
43
+ export async function createGithubRepoFromClone(project) {
44
+ section('GitHub repository');
45
+ await ensureGithubLogin();
46
+ const owner = await getGithubLogin();
47
+ const repoName = project.projectName;
48
+
49
+ // Detach from the template's git history and start a clean repo so we
50
+ // don't try to push into someone else's repository history.
51
+ if (process.platform === 'win32') {
52
+ await runCommand('cmd', ['/c', 'rmdir', '/s', '/q', '.git'], { cwd: project.target, allowFailure: true });
53
+ } else {
54
+ await runCommand('rm', ['-rf', '.git'], { cwd: project.target, allowFailure: true });
55
+ }
56
+ await runCommand('git', ['init', '-b', 'main'], { cwd: project.target });
57
+ await runCommand('git', ['add', '-A'], { cwd: project.target });
58
+ await runCommand('git', ['-c', 'user.email=fabrica-cli@local', '-c', 'user.name=Fabrica CLI', 'commit', '-m', 'Initial commit from Fabrica storefront'], { cwd: project.target });
59
+
60
+ const spin = spinner(`Creating GitHub repo ${owner}/${repoName}`);
61
+ const create = await runCommandCapture('gh', ['repo', 'create', repoName, '--private', '--source', '.', '--remote', 'origin', '--push'], { cwd: project.target });
62
+ if (create.code !== 0) {
63
+ spin.fail('Could not create GitHub repository');
64
+ throw new Error(create.stderr || 'gh repo create failed');
65
+ }
66
+ spin.succeed(`Pushed code to ${owner}/${repoName}`);
67
+
68
+ const repoUrl = `https://github.com/${owner}/${repoName}`;
69
+ kv('GitHub repo', repoUrl);
70
+ return { owner, repoName, repoUrl };
71
+ }
package/src/system.js CHANGED
@@ -23,6 +23,37 @@ export function runCommand(command, args, options = {}) {
23
23
  });
24
24
  }
25
25
 
26
+ // Same as runCommand but captures stdout/stderr instead of inheriting the
27
+ // parent terminal. Used for silent checks (login status, version probes)
28
+ // where we don't want tool noise printed to the user. Never rejects on a
29
+ // non-zero exit code or missing binary; callers inspect `code`/`error`.
30
+ export function runCommandCapture(command, args, options = {}) {
31
+ return new Promise((resolve) => {
32
+ let child;
33
+ try {
34
+ child = spawn(executable(command), args, {
35
+ stdio: 'pipe',
36
+ shell: false,
37
+ cwd: options.cwd
38
+ });
39
+ } catch (error) {
40
+ resolve({ code: null, stdout: '', stderr: '', error });
41
+ return;
42
+ }
43
+ let stdout = '';
44
+ let stderr = '';
45
+ child.stdout?.on('data', (chunk) => { stdout += chunk; });
46
+ child.stderr?.on('data', (chunk) => { stderr += chunk; });
47
+ child.on('error', (error) => resolve({ code: null, stdout, stderr, error }));
48
+ child.on('exit', (code) => resolve({ code, stdout, stderr }));
49
+ });
50
+ }
51
+
52
+ export function commandExists(command) {
53
+ const probe = process.platform === 'win32' ? ['where', [command]] : ['which', [command]];
54
+ return runCommandCapture(probe[0], probe[1]).then((result) => result.code === 0);
55
+ }
56
+
26
57
  export async function openUrl(url) {
27
58
  if (process.platform === 'win32') {
28
59
  await runCommand('cmd', ['/c', 'start', '', url], { allowFailure: true });
package/src/ui.js CHANGED
@@ -21,14 +21,16 @@ export function help() {
21
21
  banner();
22
22
  console.log(`
23
23
  ${orange('Commands')}
24
- ${bold('build')} Connect Supabase, collect secrets, clone the store, deploy to Vercel
24
+ ${bold('build')} Connect Supabase, collect secrets, clone the store, push to a new GitHub repo, deploy to Vercel
25
25
  ${bold('list')} Show deployed Fabrica projects and edit env variables
26
+ ${bold('vins')} Verify CLI dependencies (git, gh, vercel) and auto-install anything missing
26
27
  ${bold('info')} Show package, bridge, repo, and local storage information
27
28
  ${bold('help')} Show this help screen
28
29
 
29
30
  ${orange('Examples')}
30
31
  npx fabrica-e-commerce build
31
32
  npx fabrica-e-commerce list
33
+ npx fabrica-e-commerce vins
32
34
  npx fabrica-e-commerce info
33
35
  `);
34
36
  }