fabrica-e-commerce 0.1.3 → 0.1.5
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 +2 -2
- package/package.json +1 -1
- package/src/github.js +121 -18
- package/src/system.js +1 -0
- package/src/ui.js +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ On Windows CMD, these commands do not require Unix tools such as `find` or `xarg
|
|
|
38
38
|
|
|
39
39
|
## Commands
|
|
40
40
|
|
|
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`),
|
|
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`), publishes 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
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
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.
|
|
44
44
|
- `info` / `.info` — prints package, bridge, repository, and local data paths.
|
|
@@ -62,7 +62,7 @@ On Windows CMD, these commands do not require Unix tools such as `find` or `xarg
|
|
|
62
62
|
5. Adds `SUPABASE_URL` and `SUPABASE_ANON_KEY` from the bridge response.
|
|
63
63
|
6. **Step 3 — deploy:**
|
|
64
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
|
|
65
|
+
- Logs in to the GitHub CLI if needed (`gh auth login`), detaches the cloned code from the template's git history, and publishes it to a **brand new private GitHub repository** under your account using the GitHub API, avoiding separate `git push` credential prompts.
|
|
66
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
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
68
|
- Creates the production deployment with `vercel --prod --yes`.
|
package/package.json
CHANGED
package/src/github.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import process from 'node:process';
|
|
2
4
|
import { commandExists, runCommand, runCommandCapture } from './system.js';
|
|
5
|
+
import { choose } from './prompt.js';
|
|
3
6
|
import { kv, section, spinner } from './ui.js';
|
|
4
7
|
|
|
5
8
|
export async function isGithubCliInstalled() {
|
|
@@ -11,6 +14,11 @@ export async function isLoggedInToGithub() {
|
|
|
11
14
|
return result.code === 0;
|
|
12
15
|
}
|
|
13
16
|
|
|
17
|
+
async function setupGithubGitCredentials() {
|
|
18
|
+
const result = await runCommandCapture('gh', ['auth', 'setup-git']);
|
|
19
|
+
return result.code === 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
export async function ensureGithubLogin() {
|
|
15
23
|
section('GitHub login');
|
|
16
24
|
if (!(await isGithubCliInstalled())) {
|
|
@@ -19,15 +27,23 @@ export async function ensureGithubLogin() {
|
|
|
19
27
|
const spin = spinner('Checking GitHub CLI login');
|
|
20
28
|
if (await isLoggedInToGithub()) {
|
|
21
29
|
spin.succeed('Already logged in to GitHub');
|
|
22
|
-
|
|
30
|
+
} else {
|
|
31
|
+
spin.fail('Not logged in to GitHub');
|
|
32
|
+
console.log('A browser/device flow will open so you can log in with "gh auth login"...');
|
|
33
|
+
await runCommand('gh', ['auth', 'login', '--hostname', 'github.com', '--git-protocol', 'https', '--web']);
|
|
34
|
+
if (!(await isLoggedInToGithub())) {
|
|
35
|
+
throw new Error('GitHub login was not completed. Run "fabrica build" again after logging in with "gh auth login".');
|
|
36
|
+
}
|
|
37
|
+
kv('GitHub', 'Logged in');
|
|
23
38
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
await
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
|
|
40
|
+
const gitSpin = spinner('Configuring GitHub API credentials');
|
|
41
|
+
if (await setupGithubGitCredentials()) {
|
|
42
|
+
gitSpin.succeed('GitHub API credentials ready');
|
|
43
|
+
} else {
|
|
44
|
+
gitSpin.fail('Could not configure GitHub git credentials automatically');
|
|
45
|
+
console.log('Continuing with GitHub API publishing; if Git later asks for credentials, run: gh auth setup-git');
|
|
29
46
|
}
|
|
30
|
-
kv('GitHub', 'Logged in');
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
async function getGithubLogin() {
|
|
@@ -36,15 +52,106 @@ async function getGithubLogin() {
|
|
|
36
52
|
return result.stdout.trim();
|
|
37
53
|
}
|
|
38
54
|
|
|
55
|
+
async function githubRepoExists(owner, repoName) {
|
|
56
|
+
const result = await runCommandCapture('gh', ['repo', 'view', `${owner}/${repoName}`, '--json', 'name']);
|
|
57
|
+
return result.code === 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function ensureGithubRepo(owner, repoName) {
|
|
61
|
+
const fullName = `${owner}/${repoName}`;
|
|
62
|
+
if (await githubRepoExists(owner, repoName)) {
|
|
63
|
+
const action = await choose(`GitHub repo ${fullName} already exists. What should Fabrica do?`, [
|
|
64
|
+
{ name: 'Use the existing repo and publish this storefront to it', value: 'use' },
|
|
65
|
+
{ name: 'Stop so I can choose a different Vercel project/repo name', value: 'stop' }
|
|
66
|
+
]);
|
|
67
|
+
if (action === 'stop') throw new Error(`GitHub repo ${fullName} already exists. Re-run build with a different project name.`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const spin = spinner(`Creating GitHub repo ${fullName}`);
|
|
72
|
+
const create = await runCommandCapture('gh', ['repo', 'create', fullName, '--private']);
|
|
73
|
+
if (create.code !== 0) {
|
|
74
|
+
spin.fail('Could not create GitHub repository');
|
|
75
|
+
throw new Error(create.stderr || 'gh repo create failed');
|
|
76
|
+
}
|
|
77
|
+
spin.succeed(`Created GitHub repo ${fullName}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function setOrigin(project, repoUrl) {
|
|
81
|
+
const existing = await runCommandCapture('git', ['remote', 'get-url', 'origin'], { cwd: project.target });
|
|
82
|
+
if (existing.code === 0) {
|
|
83
|
+
await runCommand('git', ['remote', 'set-url', 'origin', repoUrl], { cwd: project.target });
|
|
84
|
+
} else {
|
|
85
|
+
await runCommand('git', ['remote', 'add', 'origin', repoUrl], { cwd: project.target });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async function ghApiJson(args, body) {
|
|
91
|
+
const result = await runCommandCapture('gh', ['api', ...args, '--input', '-'], { input: JSON.stringify(body) });
|
|
92
|
+
if (result.code !== 0) throw new Error(result.stderr || result.stdout || `gh api ${args.join(' ')} failed`);
|
|
93
|
+
return result.stdout ? JSON.parse(result.stdout) : {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function ghApiGet(args) {
|
|
97
|
+
const result = await runCommandCapture('gh', ['api', ...args]);
|
|
98
|
+
if (result.code !== 0) return null;
|
|
99
|
+
return result.stdout ? JSON.parse(result.stdout) : {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function listTrackedFiles(project) {
|
|
103
|
+
const result = await runCommandCapture('git', ['ls-files', '-z'], { cwd: project.target });
|
|
104
|
+
if (result.code !== 0) throw new Error(result.stderr || 'Could not list storefront files for GitHub upload.');
|
|
105
|
+
return result.stdout.split('\0').filter(Boolean);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function createGithubBlob(project, owner, repoName, filePath) {
|
|
109
|
+
const absolute = path.join(project.target, filePath);
|
|
110
|
+
const content = await fs.readFile(absolute);
|
|
111
|
+
const blob = await ghApiJson([`repos/${owner}/${repoName}/git/blobs`], {
|
|
112
|
+
content: content.toString('base64'),
|
|
113
|
+
encoding: 'base64'
|
|
114
|
+
});
|
|
115
|
+
return { path: filePath.replace(/\\/g, '/'), mode: '100644', type: 'blob', sha: blob.sha };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function publishWithGithubApi(project, owner, repoName) {
|
|
119
|
+
const spin = spinner('Publishing storefront through GitHub API');
|
|
120
|
+
const files = await listTrackedFiles(project);
|
|
121
|
+
const treeItems = [];
|
|
122
|
+
for (const filePath of files) {
|
|
123
|
+
treeItems.push(await createGithubBlob(project, owner, repoName, filePath));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const tree = await ghApiJson([`repos/${owner}/${repoName}/git/trees`], { tree: treeItems });
|
|
127
|
+
|
|
128
|
+
const existingRef = await ghApiGet([`repos/${owner}/${repoName}/git/ref/heads/main`]);
|
|
129
|
+
const parents = existingRef?.object?.sha ? [existingRef.object.sha] : [];
|
|
130
|
+
const commit = await ghApiJson([`repos/${owner}/${repoName}/git/commits`], {
|
|
131
|
+
message: 'Initial commit from Fabrica storefront',
|
|
132
|
+
tree: tree.sha,
|
|
133
|
+
parents
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (existingRef?.ref) {
|
|
137
|
+
await ghApiJson([`repos/${owner}/${repoName}/git/refs/heads/main`, '--method', 'PATCH'], { sha: commit.sha, force: true });
|
|
138
|
+
} else {
|
|
139
|
+
await ghApiJson([`repos/${owner}/${repoName}/git/refs`], { ref: 'refs/heads/main', sha: commit.sha });
|
|
140
|
+
}
|
|
141
|
+
spin.succeed('Published storefront to GitHub without git push');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
39
145
|
// Re-homes the cloned storefront code into a brand new GitHub repository
|
|
40
146
|
// owned by the logged-in user, so the deployed Vercel project can stay
|
|
41
|
-
// connected to that repo (
|
|
147
|
+
// connected to that repo (future changes can auto deploy) instead of the original
|
|
42
148
|
// template repository.
|
|
43
149
|
export async function createGithubRepoFromClone(project) {
|
|
44
150
|
section('GitHub repository');
|
|
45
151
|
await ensureGithubLogin();
|
|
46
152
|
const owner = await getGithubLogin();
|
|
47
153
|
const repoName = project.projectName;
|
|
154
|
+
const repoUrl = `https://github.com/${owner}/${repoName}.git`;
|
|
48
155
|
|
|
49
156
|
// Detach from the template's git history and start a clean repo so we
|
|
50
157
|
// don't try to push into someone else's repository history.
|
|
@@ -57,15 +164,11 @@ export async function createGithubRepoFromClone(project) {
|
|
|
57
164
|
await runCommand('git', ['add', '-A'], { cwd: project.target });
|
|
58
165
|
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
166
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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}`);
|
|
167
|
+
await ensureGithubRepo(owner, repoName);
|
|
168
|
+
await setOrigin(project, repoUrl);
|
|
169
|
+
await publishWithGithubApi(project, owner, repoName);
|
|
67
170
|
|
|
68
|
-
const
|
|
69
|
-
kv('GitHub repo',
|
|
70
|
-
return { owner, repoName, repoUrl };
|
|
171
|
+
const browserUrl = `https://github.com/${owner}/${repoName}`;
|
|
172
|
+
kv('GitHub repo', browserUrl);
|
|
173
|
+
return { owner, repoName, repoUrl: browserUrl };
|
|
71
174
|
}
|
package/src/system.js
CHANGED
|
@@ -46,6 +46,7 @@ export function runCommandCapture(command, args, options = {}) {
|
|
|
46
46
|
let child;
|
|
47
47
|
try {
|
|
48
48
|
child = spawn(executable(command), args, spawnOptions(command, options, 'pipe'));
|
|
49
|
+
if (options.input) child.stdin.end(options.input);
|
|
49
50
|
} catch (error) {
|
|
50
51
|
resolve({ code: null, stdout: '', stderr: '', error });
|
|
51
52
|
return;
|
package/src/ui.js
CHANGED
|
@@ -21,7 +21,7 @@ export function help() {
|
|
|
21
21
|
banner();
|
|
22
22
|
console.log(`
|
|
23
23
|
${orange('Commands')}
|
|
24
|
-
${bold('build')} Connect Supabase, collect secrets, clone the store,
|
|
24
|
+
${bold('build')} Connect Supabase, collect secrets, clone the store, publish to a new GitHub repo, deploy to Vercel
|
|
25
25
|
${bold('list')} Show deployed Fabrica projects and edit env variables
|
|
26
26
|
${bold('vins')} Verify CLI dependencies (git, gh, vercel) and auto-install anything missing
|
|
27
27
|
${bold('info')} Show package, bridge, repo, and local storage information
|