fabrica-e-commerce 0.1.0
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 +102 -0
- package/bin/fabrica.js +7 -0
- package/package.json +43 -0
- package/scripts/check-syntax.js +26 -0
- package/src/bridge.js +26 -0
- package/src/cli.js +66 -0
- package/src/config.js +21 -0
- package/src/deploy.js +47 -0
- package/src/prompt.js +17 -0
- package/src/sql.js +29 -0
- package/src/store.js +26 -0
- package/src/system.js +33 -0
- package/src/ui.js +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# fabrica-e-commerce
|
|
2
|
+
|
|
3
|
+
Orange themed NPX CLI for launching a Fabrica e-commerce storefront from CMD or any terminal.
|
|
4
|
+
|
|
5
|
+
## Install / run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx fabrica-e-commerce help
|
|
9
|
+
npx fabrica-e-commerce build
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The package exposes both `fabrica` and `fabrica-e-commerce` binaries after global install.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g fabrica-e-commerce
|
|
16
|
+
fabrica build
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Local NPX-style package test
|
|
21
|
+
|
|
22
|
+
Before publishing, test the package exactly like a fresh command-line install:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm run build
|
|
26
|
+
npm pack --dry-run
|
|
27
|
+
npm exec --yes --package . -- fabrica help
|
|
28
|
+
npm exec --yes --package . -- fabrica info
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You can also run the bundled pack test, which creates a real `.tgz` tarball and executes the installed binary from that tarball:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm run test:pack
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
On Windows CMD, these commands do not require Unix tools such as `find` or `xargs`; the build script uses a Node.js checker instead.
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
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.
|
|
43
|
+
- `info` / `.info` — prints package, bridge, repository, and local data paths.
|
|
44
|
+
- `help` — prints the command guide.
|
|
45
|
+
|
|
46
|
+
## Build flow
|
|
47
|
+
|
|
48
|
+
1. Posts the hidden schema/seed SQL to `https://sparrow-supabase-connect.lovable.app/api/public/jobs` using the configured bridge API key.
|
|
49
|
+
2. Opens the returned `connectUrl` in the browser and polls until `{ status: "done", url, anonKey }` is returned.
|
|
50
|
+
3. Prompts for user-owned secrets:
|
|
51
|
+
- `RAZORPAY_KEY_ID`
|
|
52
|
+
- `RAZORPAY_KEY_SECRET`
|
|
53
|
+
- `OPENROUTER_API_KEY`
|
|
54
|
+
- `UMAMI_WEBSITE_ID`
|
|
55
|
+
- `UMAMI_API_KEY`
|
|
56
|
+
- `SHIPPO_API_KEY`
|
|
57
|
+
4. Adds hardcoded values requested by the project:
|
|
58
|
+
- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=0000`
|
|
59
|
+
- `UMAMI_API_CLIENT_ENDPOINT=https://api.umami.is/v1`
|
|
60
|
+
- `SUPABASE_SERVICE_ROLE_KEY=0000`
|
|
61
|
+
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
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## Windows Vercel command note
|
|
67
|
+
|
|
68
|
+
The CLI runs npm-launched commands through `npx.cmd` on Windows, so `npx vercel@latest ...` works from CMD and PowerShell instead of failing with `'vercel@latest' is not recognized`. If you still see Vercel login prompts, complete them in the browser and rerun the command.
|
|
69
|
+
|
|
70
|
+
## Automatic npm publishing from GitHub
|
|
71
|
+
|
|
72
|
+
The repository includes a GitHub Actions workflow at `.github/workflows/npm-publish.yml` that verifies the CLI and publishes it to npm on pushes to `main`, `master`, or `work`, and can also be started manually from the Actions tab.
|
|
73
|
+
|
|
74
|
+
Before the workflow can publish, add an npm automation token to your GitHub repository secrets:
|
|
75
|
+
|
|
76
|
+
1. Create an npm token with publish permissions from your npm account.
|
|
77
|
+
2. In GitHub, open **Settings → Secrets and variables → Actions**.
|
|
78
|
+
3. Add a repository secret named `NPM_TOKEN` with that npm token.
|
|
79
|
+
|
|
80
|
+
On every publish run, the workflow:
|
|
81
|
+
|
|
82
|
+
1. Runs `npm run build`.
|
|
83
|
+
2. Runs `npm run test:cli`.
|
|
84
|
+
3. Runs `npm run test:pack`.
|
|
85
|
+
4. Checks the current latest version on npm and bumps `package.json` to the next patch version when needed.
|
|
86
|
+
5. Runs `npm publish --access public --provenance`.
|
|
87
|
+
6. Commits the published version back to GitHub with `[skip npm-publish]` to avoid an infinite publish loop.
|
|
88
|
+
|
|
89
|
+
The workflow publishes `fabrica-e-commerce` as an unscoped public npm package. The package name must be available on npm, and your `NPM_TOKEN` account must have permission to publish it.
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
For npm provenance publishing, `package.json` includes a `repository.url` that matches the GitHub repository used by the workflow. Keep that URL in sync if you move the repository, otherwise npm will reject the provenance bundle.
|
|
93
|
+
|
|
94
|
+
## Local project records
|
|
95
|
+
|
|
96
|
+
Deployment metadata is saved to `~/.fabrica-ecommerce/projects.json`. Secret values are not stored; only the variable names, project path, repo URL, created date, and Supabase URL are saved.
|
|
97
|
+
|
|
98
|
+
## Requirements
|
|
99
|
+
|
|
100
|
+
- Node.js 18.17+
|
|
101
|
+
- Git installed and available in PATH
|
|
102
|
+
- A Vercel account. The Vercel CLI will prompt/login when required.
|
package/bin/fabrica.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fabrica-e-commerce",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Orange themed CMD launcher for deploying Fabrica e-commerce stores with Supabase and Vercel.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fabrica": "bin/fabrica.js",
|
|
8
|
+
"fabrica-e-commerce": "bin/fabrica.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
"scripts/check-syntax.js",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node ./bin/fabrica.js",
|
|
18
|
+
"build": "node ./scripts/check-syntax.js",
|
|
19
|
+
"test:cli": "node ./bin/fabrica.js help && node ./bin/fabrica.js info",
|
|
20
|
+
"test:pack": "node ./scripts/test-packed-cli.js",
|
|
21
|
+
"version:bump:npm": "node ./scripts/bump-version-from-npm.js"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"fabrica",
|
|
25
|
+
"ecommerce",
|
|
26
|
+
"supabase",
|
|
27
|
+
"vercel",
|
|
28
|
+
"cli"
|
|
29
|
+
],
|
|
30
|
+
"author": "Fabrica",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.17"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/trucount/fabrica-e-c"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/trucount/fabrica-e-c#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/trucount/fabrica-e-c/issues"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const files = [path.join('bin', 'fabrica.js')];
|
|
8
|
+
|
|
9
|
+
async function collectJsFiles(directory) {
|
|
10
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
const fullPath = path.join(directory, entry.name);
|
|
13
|
+
if (entry.isDirectory()) await collectJsFiles(fullPath);
|
|
14
|
+
if (entry.isFile() && entry.name.endsWith('.js')) files.push(fullPath);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
await collectJsFiles(path.join(root, 'src'));
|
|
19
|
+
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
const relativeFile = path.relative(root, file);
|
|
22
|
+
const result = spawnSync(process.execPath, ['--check', relativeFile], { stdio: 'inherit', cwd: root });
|
|
23
|
+
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(`Checked ${files.length} JavaScript files.`);
|
package/src/bridge.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BRIDGE_API_KEY, BRIDGE_ORIGIN, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from './config.js';
|
|
2
|
+
import { FULL_SQL } from './sql.js';
|
|
3
|
+
import { openUrl } from './system.js';
|
|
4
|
+
import { spinner } from './ui.js';
|
|
5
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
export async function connectSupabase() {
|
|
7
|
+
const spin = spinner('Posting secure schema job to Fabrica Connect');
|
|
8
|
+
const response = await fetch(`${BRIDGE_ORIGIN}/api/public/jobs`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: BRIDGE_API_KEY, sql: FULL_SQL }) });
|
|
9
|
+
if (!response.ok) throw new Error(`Bridge rejected job: ${response.status} ${await response.text()}`);
|
|
10
|
+
const job = await response.json();
|
|
11
|
+
spin.succeed('Bridge job created');
|
|
12
|
+
console.log(`Open this URL if your browser does not start: ${job.connectUrl}`);
|
|
13
|
+
await openUrl(job.connectUrl);
|
|
14
|
+
const started = Date.now();
|
|
15
|
+
const poll = spinner('Waiting for Supabase authorization and project selection');
|
|
16
|
+
while (Date.now() - started < POLL_TIMEOUT_MS) {
|
|
17
|
+
const pollResponse = await fetch(job.pollUrl);
|
|
18
|
+
if (pollResponse.ok) {
|
|
19
|
+
const payload = await pollResponse.json();
|
|
20
|
+
if (payload.status === 'done' && payload.url && payload.anonKey) { poll.succeed('Supabase project connected'); return { jobId: job.jobId, url: payload.url, anonKey: payload.anonKey }; }
|
|
21
|
+
if (payload.status === 'error') throw new Error(payload.error || 'Bridge job failed');
|
|
22
|
+
}
|
|
23
|
+
await sleep(POLL_INTERVAL_MS);
|
|
24
|
+
}
|
|
25
|
+
throw new Error('Timed out waiting for Supabase bridge');
|
|
26
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { connectSupabase } from './bridge.js';
|
|
6
|
+
import { collectEnv, cloneRepo, deployToVercel, editProjectEnv } from './deploy.js';
|
|
7
|
+
import { BRIDGE_ORIGIN, STORE_REPO } from './config.js';
|
|
8
|
+
import { dataDir, readProjects } from './store.js';
|
|
9
|
+
import { banner, help, kv, section } from './ui.js';
|
|
10
|
+
|
|
11
|
+
async function packageVersion() {
|
|
12
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const pkg = JSON.parse(await readFile(path.join(here, '..', 'package.json'), 'utf8'));
|
|
14
|
+
return pkg.version;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function build() {
|
|
18
|
+
banner();
|
|
19
|
+
section('Supabase Connect');
|
|
20
|
+
kv('BRIDGE', 'ONLINE');
|
|
21
|
+
kv('SQL', 'Prepared securely (hidden from UI)');
|
|
22
|
+
const supabase = await connectSupabase();
|
|
23
|
+
const env = await collectEnv(supabase);
|
|
24
|
+
const project = await cloneRepo();
|
|
25
|
+
const record = await deployToVercel(project, env);
|
|
26
|
+
section('Done');
|
|
27
|
+
kv('Project', record.projectName);
|
|
28
|
+
kv('Path', record.target);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function list() {
|
|
32
|
+
banner();
|
|
33
|
+
const projects = await readProjects();
|
|
34
|
+
if (!projects.length) {
|
|
35
|
+
console.log('No projects found. Run: fabrica build');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
projects.forEach((project, index) => {
|
|
39
|
+
console.log(`\n${index + 1}. ${project.projectName}`);
|
|
40
|
+
kv('Created', project.createdAt);
|
|
41
|
+
kv('Path', project.target);
|
|
42
|
+
kv('Supabase', project.supabaseUrl);
|
|
43
|
+
kv('Env keys', project.envKeys.join(', '));
|
|
44
|
+
});
|
|
45
|
+
await editProjectEnv(projects);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function info() {
|
|
49
|
+
banner();
|
|
50
|
+
kv('Package', `fabrica-e-commerce v${await packageVersion()}`);
|
|
51
|
+
kv('Bridge', BRIDGE_ORIGIN);
|
|
52
|
+
kv('Store repo', STORE_REPO);
|
|
53
|
+
kv('Local data', dataDir);
|
|
54
|
+
kv('Node', process.version);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function run(args) {
|
|
58
|
+
const command = args[0] || 'help';
|
|
59
|
+
if (command === 'build') return build();
|
|
60
|
+
if (command === 'list') return list();
|
|
61
|
+
if (command === 'info' || command === '.info') return info();
|
|
62
|
+
if (command === 'help' || command === '--help' || command === '-h') return help();
|
|
63
|
+
console.error(`Unknown command: ${command}`);
|
|
64
|
+
help();
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const APP_NAME = 'FABRICA E-COMMERCE';
|
|
2
|
+
export const BRIDGE_ORIGIN = 'https://sparrow-supabase-connect.lovable.app';
|
|
3
|
+
export const BRIDGE_API_KEY = 'sparrowaisolutions';
|
|
4
|
+
export const STORE_REPO = 'https://github.com/trucount/fabrica-final-e-c.git';
|
|
5
|
+
export const POLL_INTERVAL_MS = 2500;
|
|
6
|
+
export const POLL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
export const REQUIRED_ENV_KEYS = [
|
|
9
|
+
'RAZORPAY_KEY_ID',
|
|
10
|
+
'RAZORPAY_KEY_SECRET',
|
|
11
|
+
'OPENROUTER_API_KEY',
|
|
12
|
+
'UMAMI_WEBSITE_ID',
|
|
13
|
+
'UMAMI_API_KEY',
|
|
14
|
+
'SHIPPO_API_KEY'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const HARDCODED_ENV = {
|
|
18
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: '0000',
|
|
19
|
+
UMAMI_API_CLIENT_ENDPOINT: 'https://api.umami.is/v1',
|
|
20
|
+
SUPABASE_SERVICE_ROLE_KEY: '0000'
|
|
21
|
+
};
|
package/src/deploy.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import { HARDCODED_ENV, REQUIRED_ENV_KEYS, STORE_REPO } from './config.js';
|
|
4
|
+
import { buildsDir, saveProject } from './store.js';
|
|
5
|
+
import { runCommand } from './system.js';
|
|
6
|
+
import { kv, section, spinner } from './ui.js';
|
|
7
|
+
import { ask, choose } from './prompt.js';
|
|
8
|
+
|
|
9
|
+
export async function collectEnv(supabase) {
|
|
10
|
+
section('Environment variables');
|
|
11
|
+
const env = { ...HARDCODED_ENV, SUPABASE_URL: supabase.url, SUPABASE_ANON_KEY: supabase.anonKey };
|
|
12
|
+
for (const key of REQUIRED_ENV_KEYS) env[key] = await ask(`${key}`);
|
|
13
|
+
return env;
|
|
14
|
+
}
|
|
15
|
+
export async function cloneRepo() {
|
|
16
|
+
section('Clone storefront');
|
|
17
|
+
const projectName = await ask('Vercel project name', `fabrica-store-${Date.now()}`);
|
|
18
|
+
const id = crypto.randomUUID();
|
|
19
|
+
const target = path.join(buildsDir, `${projectName}-${id.slice(0, 8)}`);
|
|
20
|
+
await runCommand('git', ['clone', STORE_REPO, target]);
|
|
21
|
+
return { id, projectName, target };
|
|
22
|
+
}
|
|
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 });
|
|
26
|
+
for (const [key, value] of Object.entries(env)) {
|
|
27
|
+
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}`);
|
|
31
|
+
}
|
|
32
|
+
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 };
|
|
34
|
+
await saveProject(record);
|
|
35
|
+
return record;
|
|
36
|
+
}
|
|
37
|
+
export async function editProjectEnv(projects) {
|
|
38
|
+
if (!projects.length) { console.log('No deployed projects saved yet. Run build first.'); return; }
|
|
39
|
+
const projectId = await choose('Select project:', projects.map((project) => ({ name: `${project.projectName} (${project.createdAt})`, value: project.id })));
|
|
40
|
+
const project = projects.find((item) => item.id === projectId);
|
|
41
|
+
const key = await choose('Variable to replace:', project.envKeys.map((item) => ({ name: item, value: item })));
|
|
42
|
+
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` });
|
|
45
|
+
await runCommand('npx', ['vercel@latest', '--prod', '--yes'], { cwd: project.target });
|
|
46
|
+
kv('Redeployed', project.projectName);
|
|
47
|
+
}
|
package/src/prompt.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
3
|
+
|
|
4
|
+
export async function ask(message, defaultValue = '') {
|
|
5
|
+
const rl = readline.createInterface({ input, output });
|
|
6
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
7
|
+
const answer = await rl.question(`${message}${suffix}: `);
|
|
8
|
+
rl.close();
|
|
9
|
+
return answer || defaultValue;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
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;
|
|
17
|
+
}
|
package/src/sql.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const SCHEMA_SQL = `
|
|
2
|
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
3
|
+
CREATE TABLE IF NOT EXISTS public.site_content (id text NOT NULL, content jsonb NOT NULL, updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT site_content_pkey PRIMARY KEY (id));
|
|
4
|
+
CREATE TABLE IF NOT EXISTS public.collections (id text NOT NULL, name text NOT NULL, description text NOT NULL, image_url text NOT NULL, item_count_label text NOT NULL, sort_order integer NOT NULL DEFAULT 0, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT collections_pkey PRIMARY KEY (id));
|
|
5
|
+
CREATE TABLE IF NOT EXISTS public.products (id text NOT NULL, name text NOT NULL, price numeric NOT NULL CHECK (price >= 0), main_image_url text NOT NULL, gallery_image_urls text[] NOT NULL DEFAULT '{}', category_label text, description text NOT NULL, details text[] NOT NULL DEFAULT '{}', sizes text[] NOT NULL DEFAULT '{}', section text NOT NULL DEFAULT 'general' CHECK (section = ANY (ARRAY['general','new_arrivals','best_sellers'])), collection_ids text[] NOT NULL DEFAULT '{}', sort_order integer NOT NULL DEFAULT 0, is_active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT products_pkey PRIMARY KEY (id));
|
|
6
|
+
CREATE TABLE IF NOT EXISTS public.app_users (id uuid NOT NULL DEFAULT gen_random_uuid(), email text NOT NULL UNIQUE, first_name text NOT NULL, last_name text NOT NULL, phone text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT app_users_pkey PRIMARY KEY (id));
|
|
7
|
+
CREATE TABLE IF NOT EXISTS public.saved_addresses (id uuid NOT NULL DEFAULT gen_random_uuid(), user_id uuid NOT NULL, label text NOT NULL, first_name text NOT NULL, last_name text NOT NULL, phone text NOT NULL, address text NOT NULL, apartment text, city text NOT NULL, state text NOT NULL, zip_code text NOT NULL, country text NOT NULL DEFAULT 'India', is_default boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT saved_addresses_pkey PRIMARY KEY (id), CONSTRAINT saved_addresses_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.app_users(id));
|
|
8
|
+
CREATE TABLE IF NOT EXISTS public.order_policies (id boolean NOT NULL DEFAULT true CHECK (id = true), shipping_amount numeric NOT NULL DEFAULT 15 CHECK (shipping_amount >= 0), free_shipping_threshold numeric NOT NULL DEFAULT 200 CHECK (free_shipping_threshold >= 0), tax_rate numeric NOT NULL DEFAULT 8 CHECK (tax_rate >= 0), automatic_shipping_enabled boolean NOT NULL DEFAULT false, shippo_from_name text NOT NULL DEFAULT '', shippo_from_company text NOT NULL DEFAULT '', shippo_from_street1 text NOT NULL DEFAULT '', shippo_from_street2 text NOT NULL DEFAULT '', shippo_from_city text NOT NULL DEFAULT '', shippo_from_state text NOT NULL DEFAULT '', shippo_from_zip text NOT NULL DEFAULT '', shippo_from_country text NOT NULL DEFAULT 'IN', shippo_from_phone text NOT NULL DEFAULT '', shippo_from_email text NOT NULL DEFAULT '', shippo_from_is_residential boolean NOT NULL DEFAULT false, shippo_parcel_length numeric NOT NULL DEFAULT 10 CHECK (shippo_parcel_length > 0), shippo_parcel_width numeric NOT NULL DEFAULT 10 CHECK (shippo_parcel_width > 0), shippo_parcel_height numeric NOT NULL DEFAULT 4 CHECK (shippo_parcel_height > 0), shippo_parcel_weight numeric NOT NULL DEFAULT 1 CHECK (shippo_parcel_weight > 0), shippo_parcel_distance_unit text NOT NULL DEFAULT 'in' CHECK (shippo_parcel_distance_unit = ANY (ARRAY['in','cm'])), shippo_parcel_mass_unit text NOT NULL DEFAULT 'lb' CHECK (shippo_parcel_mass_unit = ANY (ARRAY['lb','oz','g','kg'])), shippo_label_file_type text NOT NULL DEFAULT 'PDF_4x6' CHECK (shippo_label_file_type = ANY (ARRAY['PNG','PNG_2.3x7.5','PDF','PDF_2.3x7.5','PDF_4x6','PDF_4x8','PDF_A4','PDF_A5','PDF_A6','ZPLII'])), active_theme_name text DEFAULT 'default', updated_at timestamptz NOT NULL DEFAULT now(), show_ticker boolean NOT NULL DEFAULT true, section_styles jsonb NOT NULL DEFAULT '{"homeHero":"video"}', CONSTRAINT order_policies_pkey PRIMARY KEY (id));
|
|
9
|
+
CREATE TABLE IF NOT EXISTS public.coupons (code text NOT NULL, label text NOT NULL, coupon_type text NOT NULL CHECK (coupon_type = ANY (ARRAY['universal','one_time'])), discount_type text NOT NULL CHECK (discount_type = ANY (ARRAY['percent','amount'])), discount_value numeric NOT NULL CHECK (discount_value >= 0), active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT coupons_pkey PRIMARY KEY (code));
|
|
10
|
+
CREATE TABLE IF NOT EXISTS public.orders (id text NOT NULL, user_id uuid, user_email text NOT NULL, status text NOT NULL DEFAULT 'placed' CHECK (status = ANY (ARRAY['placed','packed','in_transit','delivered'])), payment_method text NOT NULL CHECK (payment_method = ANY (ARRAY['cod','razorpay'])), payment_verified boolean NOT NULL DEFAULT false, razorpay_payment_id text, coupon_code text, shipping_address jsonb NOT NULL, totals jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT orders_pkey PRIMARY KEY (id), CONSTRAINT orders_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.app_users(id), CONSTRAINT orders_coupon_code_fkey FOREIGN KEY (coupon_code) REFERENCES public.coupons(code));
|
|
11
|
+
CREATE TABLE IF NOT EXISTS public.order_items (id uuid NOT NULL DEFAULT gen_random_uuid(), order_id text NOT NULL, product_id text, product_name text NOT NULL, product_image text, size text NOT NULL, quantity integer NOT NULL CHECK (quantity > 0), unit_price numeric NOT NULL CHECK (unit_price >= 0), line_total numeric NOT NULL CHECK (line_total >= 0), created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT order_items_pkey PRIMARY KEY (id), CONSTRAINT order_items_order_id_fkey FOREIGN KEY (order_id) REFERENCES public.orders(id), CONSTRAINT order_items_product_id_fkey FOREIGN KEY (product_id) REFERENCES public.products(id));
|
|
12
|
+
CREATE TABLE IF NOT EXISTS public.themes (id uuid NOT NULL DEFAULT gen_random_uuid(), name text NOT NULL UNIQUE, label text NOT NULL, colors jsonb NOT NULL, is_active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT themes_pkey PRIMARY KEY (id));
|
|
13
|
+
`;
|
|
14
|
+
export const SEED_SQL = `
|
|
15
|
+
INSERT INTO public.collections (id, name, description, image_url, item_count_label, sort_order) VALUES ('contemporary','Contemporary Collection','Modern cuts and innovative styling for the forward-thinking gentleman.','/thudarum-sky-blue-blazer.jpg','10 items',30),('evening','Evening Collection','Luxurious velvet and satin pieces designed to make a statement at formal occasions.','/thudarum-navy-velvet-blazer.jpg','6 items',40),('executive','Executive Collection','Bold, sophisticated pieces for the modern power dresser.','/thudarum-burgundy-evening-suit.jpg','12 items',10),('heritage','Heritage Collection','Classic tailoring with timeless appeal.','/thudarum-green-check-blazer.jpg','8 items',20) ON CONFLICT (id) DO NOTHING;
|
|
16
|
+
INSERT INTO public.products (id,name,price,main_image_url,gallery_image_urls,category_label,description,details,sizes,section,collection_ids,sort_order,is_active) VALUES ('classic-taupe-double-breasted-suit','Classic Taupe Double-Breasted Suit',1289.00,'/thudarum-taupe-suit-hero.jpg',ARRAY['/thudarum-taupe-suit-detail.jpg'],'Suits','An impeccably tailored double-breasted suit in refined taupe wool.',ARRAY['100% Italian wool','Double-breasted closure'],ARRAY['38','40','42','44'],'new_arrivals',ARRAY['executive','contemporary'],10,true),('navy-velvet-double-breasted-jacket','Navy Velvet Double-Breasted Jacket',1195.00,'/thudarum-navy-velvet-blazer.jpg',ARRAY['/thudarum-navy-velvet-detail.jpg'],'Blazers','A luxurious navy velvet jacket with elevated texture.',ARRAY['Italian cotton velvet','Evening-ready finish'],ARRAY['38','40','42','44'],'best_sellers',ARRAY['evening'],20,true) ON CONFLICT (id) DO NOTHING;
|
|
17
|
+
INSERT INTO public.coupons (code,label,coupon_type,discount_type,discount_value,active) VALUES ('WELCOME10','Welcome 10%','universal','percent',10.00,true) ON CONFLICT (code) DO NOTHING;
|
|
18
|
+
INSERT INTO public.order_policies (id, shipping_amount, free_shipping_threshold, tax_rate, active_theme_name, show_ticker, section_styles) VALUES (true,15.00,200.00,8.00,'default',true,'{"homeHero":"video"}') ON CONFLICT (id) DO NOTHING;
|
|
19
|
+
INSERT INTO public.themes (name,label,colors,is_active) VALUES ('default','Minimalist White','{"background":"oklch(0.985 0 0)","foreground":"oklch(0.145 0 0)","primary":"oklch(0.205 0 0)"}',true),('golden','Light Golden','{"background":"oklch(0.98 0.01 70)","foreground":"oklch(0.25 0.08 50)","primary":"oklch(0.70 0.18 62)"}',true),('midnight','Midnight Dark','{"background":"oklch(0.15 0.02 240)","foreground":"oklch(0.98 0.01 240)","primary":"oklch(0.6 0.15 250)"}',true) ON CONFLICT (name) DO NOTHING;
|
|
20
|
+
INSERT INTO public.site_content (id,content) VALUES ('site','{"brandName":"FABRICA","tickerMessages":["FREE SHIPPING ON ORDERS OVER ₹200","30-DAY RETURNS","HOST YOUR OWN STORE IN MINUTES"]}'),('home','{"heroTitle":"Launch Your Store","heroSubtitle":"Discover powerful tools crafted for the modern merchant","bestSellersTitle":"Best Sellers","collectionsTitle":"Collections","newArrivalsTitle":"New Arrivals"}') ON CONFLICT (id) DO NOTHING;
|
|
21
|
+
`;
|
|
22
|
+
export const STORAGE_POLICIES_SQL = `
|
|
23
|
+
INSERT INTO storage.buckets (id, name, public) VALUES ('pic', 'pic', true) ON CONFLICT (id) DO NOTHING;
|
|
24
|
+
DO $$ BEGIN CREATE POLICY "pic_select_all" ON storage.objects FOR SELECT USING (bucket_id = 'pic'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
25
|
+
DO $$ BEGIN CREATE POLICY "pic_insert_all" ON storage.objects FOR INSERT WITH CHECK (bucket_id = 'pic'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
26
|
+
DO $$ BEGIN CREATE POLICY "pic_update_all" ON storage.objects FOR UPDATE USING (bucket_id = 'pic'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
27
|
+
DO $$ BEGIN CREATE POLICY "pic_delete_all" ON storage.objects FOR DELETE USING (bucket_id = 'pic'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
28
|
+
`;
|
|
29
|
+
export const FULL_SQL = `${SCHEMA_SQL}\n${SEED_SQL}\n${STORAGE_POLICIES_SQL}`;
|
package/src/store.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const dataDir = path.join(homedir(), '.fabrica-ecommerce');
|
|
6
|
+
export const projectsFile = path.join(dataDir, 'projects.json');
|
|
7
|
+
export const buildsDir = path.join(dataDir, 'builds');
|
|
8
|
+
|
|
9
|
+
export async function ensureStore() {
|
|
10
|
+
await mkdir(buildsDir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function readProjects() {
|
|
14
|
+
await ensureStore();
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(await readFile(projectsFile, 'utf8'));
|
|
17
|
+
} catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function saveProject(project) {
|
|
23
|
+
const projects = await readProjects();
|
|
24
|
+
const next = [project, ...projects.filter((item) => item.id !== project.id)];
|
|
25
|
+
await writeFile(projectsFile, JSON.stringify(next, null, 2));
|
|
26
|
+
}
|
package/src/system.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
|
|
4
|
+
function executable(command) {
|
|
5
|
+
if (process.platform !== 'win32') return command;
|
|
6
|
+
if (['npm', 'npx'].includes(command)) return `${command}.cmd`;
|
|
7
|
+
return command;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function runCommand(command, args, options = {}) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const child = spawn(executable(command), args, {
|
|
13
|
+
stdio: options.input ? ['pipe', 'inherit', 'inherit'] : 'inherit',
|
|
14
|
+
shell: false,
|
|
15
|
+
cwd: options.cwd
|
|
16
|
+
});
|
|
17
|
+
if (options.input) child.stdin.end(options.input);
|
|
18
|
+
child.on('error', (error) => {
|
|
19
|
+
if (options.allowFailure) resolve();
|
|
20
|
+
else reject(error);
|
|
21
|
+
});
|
|
22
|
+
child.on('exit', (code) => code === 0 || options.allowFailure ? resolve() : reject(new Error(`${command} ${args.join(' ')} exited with ${code}`)));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function openUrl(url) {
|
|
27
|
+
if (process.platform === 'win32') {
|
|
28
|
+
await runCommand('cmd', ['/c', 'start', '', url], { allowFailure: true });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const command = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
32
|
+
await runCommand(command, [url], { allowFailure: true });
|
|
33
|
+
}
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
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`;
|
|
5
|
+
|
|
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
|
+
export function spinner(text) { process.stdout.write(dimOrange('> ') + text); return { succeed(msg) { process.stdout.write(`\r${orange('✓')} ${msg}\n`); }, fail(msg) { process.stdout.write(`\r${orange('✗')} ${msg}\n`); } }; }
|
|
20
|
+
export function help() {
|
|
21
|
+
banner();
|
|
22
|
+
console.log(`
|
|
23
|
+
${orange('Commands')}
|
|
24
|
+
${bold('build')} Connect Supabase, collect secrets, clone the store, deploy to Vercel
|
|
25
|
+
${bold('list')} Show deployed Fabrica projects and edit env variables
|
|
26
|
+
${bold('info')} Show package, bridge, repo, and local storage information
|
|
27
|
+
${bold('help')} Show this help screen
|
|
28
|
+
|
|
29
|
+
${orange('Examples')}
|
|
30
|
+
npx fabrica-e-commerce build
|
|
31
|
+
npx fabrica-e-commerce list
|
|
32
|
+
npx fabrica-e-commerce info
|
|
33
|
+
`);
|
|
34
|
+
}
|