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 +22 -4
- package/package.json +1 -1
- package/src/cli.js +17 -2
- package/src/deploy.js +68 -10
- package/src/deps.js +160 -0
- package/src/github.js +71 -0
- package/src/system.js +31 -0
- package/src/ui.js +3 -1
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`,
|
|
42
|
-
- `list` — shows locally saved deployments and lets you replace a saved project's Vercel
|
|
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.
|
|
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
|
-
-
|
|
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
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
|
-
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
|
|
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
|
}
|