vibecarbon 0.6.0 → 0.7.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/carbon/package.json +3 -3
- package/carbon/scripts/db-migrate.js +49 -0
- package/carbon/scripts/dev-init.js +4 -0
- package/carbon/scripts/dev.js +39 -4
- package/carbon/scripts/lib/vite-log-filter.js +29 -0
- package/carbon/src/client/locales/en.json +1 -1
- package/carbon/src/server/lib/env.ts +12 -1
- package/carbon/supabase/migrations/00001_init.sql +14 -2
- package/carbon/supabase/migrations/00003_pg_cron.sql +4 -0
- package/carbon/supabase/migrations/00004_contact_newsletter.sql +2 -1
- package/carbon/vite.config.ts +14 -1
- package/carbon/volumes/db/super-admin.dev.sql +5 -1
- package/carbon/volumes/db/super-admin.sql +4 -1
- package/package.json +1 -1
- package/src/create.js +58 -0
package/carbon/package.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"docker:reset:all": "node scripts/docker-down.js -v --rmi --all",
|
|
40
40
|
"docker:prod:up": "node scripts/docker-up.js --prod",
|
|
41
41
|
"docker:prod:down": "node scripts/docker-down.js --prod",
|
|
42
|
-
"db:migrate": "
|
|
42
|
+
"db:migrate": "node scripts/db-migrate.js",
|
|
43
43
|
"db:reset": "node scripts/docker-down.js -v && node scripts/docker-up.js",
|
|
44
44
|
"k8s:apply": "node scripts/k8s-apply.js",
|
|
45
45
|
"k8s:delete": "node scripts/k8s-delete.js",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@mdx-js/react": "^3.1.1",
|
|
60
60
|
"@mdx-js/rollup": "^3.1.1",
|
|
61
61
|
"@scalar/api-reference-react": "^0.9.49",
|
|
62
|
-
"@supabase/supabase-js": "^2.
|
|
62
|
+
"@supabase/supabase-js": "^2.108.2",
|
|
63
63
|
"@tanstack/react-query": "^5.100.14",
|
|
64
64
|
"class-variance-authority": "^0.7.1",
|
|
65
65
|
"clsx": "^2.1.1",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"lenis": "^1.3.25",
|
|
76
76
|
"lucide-react": "^1.17.0",
|
|
77
77
|
"next-themes": "^0.4.6",
|
|
78
|
-
"nodemailer": "^
|
|
78
|
+
"nodemailer": "^9.0.1",
|
|
79
79
|
"pino": "^10.3.1",
|
|
80
80
|
"react": "^19.2.7",
|
|
81
81
|
"react-day-picker": "^10.0.1",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Quiet database migration runner for local dev (`pnpm db:migrate`).
|
|
4
|
+
*
|
|
5
|
+
* Applies every /migrations/*.sql (lexical order) then /tmp/super-admin.sql
|
|
6
|
+
* inside the running `db` compose service. psql runs with notices suppressed
|
|
7
|
+
* (client_min_messages=warning), command tags silenced (-q), and stdout
|
|
8
|
+
* discarded (>/dev/null), so a normal idempotent re-run prints ~one line per
|
|
9
|
+
* file instead of ~200. Real WARNING/ERROR text is preserved on stderr and the
|
|
10
|
+
* first error aborts the run (ON_ERROR_STOP + set -e).
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
/** The bash program run inside the db container. Exported for unit testing. */
|
|
15
|
+
export function buildMigrateScript() {
|
|
16
|
+
// Single quiet psql invocation, parameterised by the shell var "$f" / a path.
|
|
17
|
+
const psql = (file) =>
|
|
18
|
+
`PGOPTIONS="-c client_min_messages=warning" ` +
|
|
19
|
+
`psql -U postgres -d postgres -v ON_ERROR_STOP=1 -q -f ${file} >/dev/null`;
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
'set -e',
|
|
23
|
+
'for f in /migrations/*.sql; do',
|
|
24
|
+
` ${psql('"$f"')}`,
|
|
25
|
+
' echo " ✓ $(basename "$f")"',
|
|
26
|
+
'done',
|
|
27
|
+
psql('/tmp/super-admin.sql'),
|
|
28
|
+
'echo " ✓ super-admin"',
|
|
29
|
+
].join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function main() {
|
|
33
|
+
console.log('Running database migrations…');
|
|
34
|
+
const res = spawnSync(
|
|
35
|
+
'docker',
|
|
36
|
+
['compose', 'exec', '-T', 'db', 'bash', '-c', buildMigrateScript()],
|
|
37
|
+
{ stdio: 'inherit' },
|
|
38
|
+
);
|
|
39
|
+
if (res.error) {
|
|
40
|
+
console.error(res.error.message);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
process.exit(res.status ?? 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Run only when invoked directly, not when imported by the test.
|
|
47
|
+
if (process.argv[1] && process.argv[1].endsWith('db-migrate.js')) {
|
|
48
|
+
main();
|
|
49
|
+
}
|
|
@@ -95,6 +95,10 @@ ADMIN_PASSWORD="${DEV.ADMIN_PASSWORD}"
|
|
|
95
95
|
# Site URL (base port — offset applied dynamically, see above)
|
|
96
96
|
SITE_URL="http://localhost:5173"
|
|
97
97
|
|
|
98
|
+
# Public URL — substituted into index.html's og:/twitter: meta tags by Vite's
|
|
99
|
+
# %VITE_PUBLIC_URL% replacement. Must be defined or Vite warns on every reload.
|
|
100
|
+
VITE_PUBLIC_URL="http://localhost:5173"
|
|
101
|
+
|
|
98
102
|
# OAuth (disabled for dev)
|
|
99
103
|
GOOGLE_ENABLED="false"
|
|
100
104
|
GOOGLE_CLIENT_ID=""
|
package/carbon/scripts/dev.js
CHANGED
|
@@ -109,22 +109,53 @@ if (conflicts.length > 0) {
|
|
|
109
109
|
process.exit(1);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
// The service-URL banner is printed *after* both dev servers are listening (see
|
|
113
|
+
// announceWhenReady below) rather than up-front. Otherwise Vite's "ready in Xms"
|
|
114
|
+
// and the API's startup logs print afterward and scroll the URLs off-screen.
|
|
115
|
+
function printBanner() {
|
|
116
|
+
console.log(`
|
|
117
|
+
${cyan('✦')} ${bold(projectName)} is ready
|
|
114
118
|
|
|
115
119
|
${cyan('→')} ${bold('Frontend:')} ${cyan(`http://localhost:${ports.vite}`)}
|
|
116
120
|
${cyan('→')} ${bold('API:')} ${cyan(`http://localhost:${ports.api}`)}
|
|
117
|
-
${cyan('→')} ${bold('Studio:')} ${cyan('http://studio.localhost')} (
|
|
118
|
-
${cyan('→')} ${bold('Traefik:')} ${cyan('http://traefik.localhost')} (
|
|
121
|
+
${cyan('→')} ${bold('Studio:')} ${cyan('http://studio.localhost')} (no auth in dev)
|
|
122
|
+
${cyan('→')} ${bold('Traefik:')} ${cyan('http://traefik.localhost')} (no auth in dev)
|
|
119
123
|
`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
127
|
+
|
|
128
|
+
// Poll until both dev servers hold their ports (i.e. are listening), then print
|
|
129
|
+
// the URL banner so it lands as the last thing on screen. We watch the ports
|
|
130
|
+
// instead of piping child stdout because `stdio: 'inherit'` keeps Vite's TTY
|
|
131
|
+
// (colours + interactive `h+enter` shortcuts); piping would forfeit both.
|
|
132
|
+
async function announceWhenReady() {
|
|
133
|
+
const deadline = Date.now() + 30_000; // print anyway after 30s, don't hang
|
|
134
|
+
while (Date.now() < deadline) {
|
|
135
|
+
if (shuttingDown) return;
|
|
136
|
+
const free = await Promise.all([isPortFree(ports.vite), isPortFree(ports.api)]);
|
|
137
|
+
if (free.every((isFree) => !isFree)) break; // both ports in use → listening
|
|
138
|
+
await delay(200);
|
|
139
|
+
}
|
|
140
|
+
if (shuttingDown) return;
|
|
141
|
+
// Let the servers' own "ready"/"Starting server" lines flush first.
|
|
142
|
+
await delay(400);
|
|
143
|
+
if (!shuttingDown) printBanner();
|
|
144
|
+
}
|
|
120
145
|
|
|
121
146
|
const children = [];
|
|
122
147
|
|
|
148
|
+
// pnpm exports its own config as npm_config_* env vars; the nested `npx`
|
|
149
|
+
// (real npm) then warns "Unknown env config …". Silence npm's own warnings
|
|
150
|
+
// for these dev tool processes — tsx/vite write their own logs, not via npm.
|
|
151
|
+
const childEnv = { ...process.env, npm_config_loglevel: 'error' };
|
|
152
|
+
|
|
123
153
|
// Spawn server process with its own process group for clean shutdown
|
|
124
154
|
const server = spawn('npx', ['tsx', 'watch', '--env-file=.env.local', 'src/server/index.ts'], {
|
|
125
155
|
stdio: 'inherit',
|
|
126
156
|
shell: isWindows,
|
|
127
157
|
detached: !isWindows, // Create process group on Unix for clean group kill
|
|
158
|
+
env: childEnv,
|
|
128
159
|
});
|
|
129
160
|
children.push(server);
|
|
130
161
|
|
|
@@ -133,12 +164,16 @@ const client = spawn('npx', ['vite'], {
|
|
|
133
164
|
stdio: 'inherit',
|
|
134
165
|
shell: isWindows,
|
|
135
166
|
detached: !isWindows,
|
|
167
|
+
env: childEnv,
|
|
136
168
|
});
|
|
137
169
|
children.push(client);
|
|
138
170
|
|
|
139
171
|
// Track if we're shutting down
|
|
140
172
|
let shuttingDown = false;
|
|
141
173
|
|
|
174
|
+
// Print the URL banner once both servers are up (fire-and-forget).
|
|
175
|
+
announceWhenReady();
|
|
176
|
+
|
|
142
177
|
function shutdown(exitCode = 0) {
|
|
143
178
|
if (shuttingDown) return;
|
|
144
179
|
shuttingDown = true;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Matches ANSI SGR escape sequences (colour/style codes). In a TTY, Vite wraps
|
|
2
|
+
// the banner labels in colour codes — and crucially places the reset *between*
|
|
3
|
+
// the word and its colon (e.g. `bold("Local") + ":"` → "Local\x1b[22m:"), so the
|
|
4
|
+
// raw line never contains the literal substring "Local:". We strip the escapes
|
|
5
|
+
// before matching. Built from String.fromCharCode(27) rather than a control-char
|
|
6
|
+
// regex literal, which Biome forbids.
|
|
7
|
+
const ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* True when a Vite log line is the dev-server URL banner that our own dev.js
|
|
11
|
+
* banner already prints (so vite.config.ts's customLogger can drop it). Keeps
|
|
12
|
+
* the "ready in Xms" header, HMR/reload info, warnings, and errors.
|
|
13
|
+
*
|
|
14
|
+
* @param {unknown} msg
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
export function isViteUrlBannerLine(msg) {
|
|
18
|
+
if (typeof msg !== 'string') return false;
|
|
19
|
+
// Strip colour codes so matches work the same in a TTY (coloured) and when
|
|
20
|
+
// piped/CI (plain). Vite splits labels and their colons into separate escape
|
|
21
|
+
// wrappers, so substring checks must run against the de-coloured text.
|
|
22
|
+
const plain = msg.replace(ANSI_RE, '');
|
|
23
|
+
const hasUrl = plain.includes('http://') || plain.includes('https://');
|
|
24
|
+
// Vite's startup banner: "➜ Local: <url>" / "➜ Network: <url>".
|
|
25
|
+
if (hasUrl && (plain.includes('Local:') || plain.includes('Network:'))) return true;
|
|
26
|
+
// The interactive shortcuts hint printed right after the URLs.
|
|
27
|
+
if (plain.includes('press h + enter')) return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
"hero": {
|
|
275
275
|
"badge": "Fully Automated · Self-Hosted",
|
|
276
276
|
"headline1": "Professional Grade",
|
|
277
|
-
"subheading": "Full-stack SaaS
|
|
277
|
+
"subheading": "Full-stack SaaS starter kit with automated everything — built for AI-driven development.",
|
|
278
278
|
"getUpdates": "Get Updates"
|
|
279
279
|
},
|
|
280
280
|
"logoStrip": {
|
|
@@ -94,8 +94,19 @@ if (process.env.NODE_ENV !== 'production' && portOffset !== 0) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// Treat empty-string env vars as unset before validating. Docker Compose
|
|
98
|
+
// `env_file` (and other env sources) surface an unset optional var as "" rather
|
|
99
|
+
// than omitting it; zod's `.optional()` permits `undefined` but NOT "", so an
|
|
100
|
+
// empty SMTP_ADMIN_EMAIL / REDIS_URL / SMTP_PORT would fail .email()/.url()/
|
|
101
|
+
// .number() and crash boot with "Server configuration error". Stripping blanks
|
|
102
|
+
// makes "" mean "not set", matching operator intent (and how the k8s Secret
|
|
103
|
+
// path already skips empty values).
|
|
104
|
+
const cleanedEnv = Object.fromEntries(
|
|
105
|
+
Object.entries(process.env).filter(([, value]) => value !== ''),
|
|
106
|
+
);
|
|
107
|
+
|
|
97
108
|
// Validate environment variables
|
|
98
|
-
const parsed = envSchema.safeParse(
|
|
109
|
+
const parsed = envSchema.safeParse(cleanedEnv);
|
|
99
110
|
|
|
100
111
|
if (!parsed.success) {
|
|
101
112
|
// In production, don't expose which specific environment variables failed validation
|
|
@@ -82,7 +82,8 @@ CREATE TABLE IF NOT EXISTS customers (
|
|
|
82
82
|
|
|
83
83
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_user_id ON customers(user_id) WHERE user_id IS NOT NULL;
|
|
84
84
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_organization_id ON customers(organization_id) WHERE organization_id IS NOT NULL;
|
|
85
|
-
|
|
85
|
+
-- stripe_customer_id is UNIQUE (see column def), which already creates an index;
|
|
86
|
+
-- a separate idx_ would be a duplicate.
|
|
86
87
|
|
|
87
88
|
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
88
89
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
@@ -99,7 +100,8 @@ CREATE TABLE IF NOT EXISTS subscriptions (
|
|
|
99
100
|
);
|
|
100
101
|
|
|
101
102
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_customer_id ON subscriptions(customer_id);
|
|
102
|
-
|
|
103
|
+
-- stripe_subscription_id is UNIQUE (see column def), which already creates an
|
|
104
|
+
-- index; a separate idx_ would be a duplicate.
|
|
103
105
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
|
|
104
106
|
|
|
105
107
|
-- ============================================================================
|
|
@@ -132,6 +134,11 @@ CREATE INDEX IF NOT EXISTS idx_notifications_org
|
|
|
132
134
|
ON notifications(organization_id)
|
|
133
135
|
WHERE organization_id IS NOT NULL;
|
|
134
136
|
|
|
137
|
+
-- Covering index for the created_by FK (avoids seq scans on auth.users deletes).
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_created_by
|
|
139
|
+
ON notifications(created_by)
|
|
140
|
+
WHERE created_by IS NOT NULL;
|
|
141
|
+
|
|
135
142
|
CREATE TABLE IF NOT EXISTS notification_dismissals (
|
|
136
143
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
137
144
|
notification_id UUID NOT NULL REFERENCES notifications(id) ON DELETE CASCADE,
|
|
@@ -167,6 +174,11 @@ CREATE TABLE IF NOT EXISTS app_settings (
|
|
|
167
174
|
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
|
168
175
|
);
|
|
169
176
|
|
|
177
|
+
-- Covering index for the updated_by FK (avoids seq scans on auth.users deletes).
|
|
178
|
+
CREATE INDEX IF NOT EXISTS idx_app_settings_updated_by
|
|
179
|
+
ON app_settings(updated_by)
|
|
180
|
+
WHERE updated_by IS NOT NULL;
|
|
181
|
+
|
|
170
182
|
-- WARNING: This table is publicly readable by the anonymous role to support
|
|
171
183
|
-- pre-authentication flows (e.g. checking if MFA is enforced).
|
|
172
184
|
-- DO NOT STORE SECRETS, TOKENS, OR SENSITIVE ENVIRONMENT VARIABLES HERE.
|
|
@@ -48,6 +48,10 @@ CREATE OR REPLACE FUNCTION public.log_cron_job(
|
|
|
48
48
|
RETURNS void
|
|
49
49
|
LANGUAGE plpgsql
|
|
50
50
|
SECURITY DEFINER
|
|
51
|
+
-- Pin search_path so this SECURITY DEFINER function can't be hijacked by a
|
|
52
|
+
-- malicious object in an attacker-controlled schema. All refs below are already
|
|
53
|
+
-- schema-qualified (public.cron_job_history), so an empty path is safe.
|
|
54
|
+
SET search_path = ''
|
|
51
55
|
AS $$
|
|
52
56
|
BEGIN
|
|
53
57
|
INSERT INTO public.cron_job_history (job_name, status, finished_at, result, error)
|
|
@@ -52,7 +52,8 @@ CREATE TABLE IF NOT EXISTS public.newsletter_subscribers (
|
|
|
52
52
|
);
|
|
53
53
|
|
|
54
54
|
CREATE INDEX IF NOT EXISTS idx_newsletter_subscribers_status ON public.newsletter_subscribers (status);
|
|
55
|
-
|
|
55
|
+
-- email is UNIQUE (see column def), which already creates an index; a separate
|
|
56
|
+
-- idx_ would be a duplicate.
|
|
56
57
|
|
|
57
58
|
ALTER TABLE public.newsletter_subscribers ENABLE ROW LEVEL SECURITY;
|
|
58
59
|
|
package/carbon/vite.config.ts
CHANGED
|
@@ -7,7 +7,8 @@ import rehypeSlug from 'rehype-slug';
|
|
|
7
7
|
import remarkFrontmatter from 'remark-frontmatter';
|
|
8
8
|
import remarkGfm from 'remark-gfm';
|
|
9
9
|
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
|
|
10
|
-
import { defineConfig, loadEnv } from 'vite';
|
|
10
|
+
import { createLogger, defineConfig, loadEnv } from 'vite';
|
|
11
|
+
import { isViteUrlBannerLine } from './scripts/lib/vite-log-filter.js';
|
|
11
12
|
|
|
12
13
|
// Calculate ports based on offset and overrides
|
|
13
14
|
function getPort(envVarName: string, defaultPort: number, offset: number): number {
|
|
@@ -36,7 +37,19 @@ export default defineConfig(({ mode }) => {
|
|
|
36
37
|
}
|
|
37
38
|
: {};
|
|
38
39
|
|
|
40
|
+
const logger = createLogger();
|
|
41
|
+
const baseInfo = logger.info.bind(logger);
|
|
42
|
+
// Our dev.js banner is the single source of truth for service URLs; drop
|
|
43
|
+
// Vite's redundant Local/Network/"press h" lines but keep readiness, HMR,
|
|
44
|
+
// warnings, and errors.
|
|
45
|
+
logger.info = (msg, options) => {
|
|
46
|
+
if (isViteUrlBannerLine(msg)) return;
|
|
47
|
+
baseInfo(msg, options);
|
|
48
|
+
};
|
|
49
|
+
|
|
39
50
|
return {
|
|
51
|
+
customLogger: logger,
|
|
52
|
+
|
|
40
53
|
plugins: [
|
|
41
54
|
mdx({
|
|
42
55
|
remarkPlugins: [remarkFrontmatter, remarkGfm, remarkMdxFrontmatter],
|
|
@@ -8,7 +8,11 @@ DECLARE
|
|
|
8
8
|
new_user_id UUID;
|
|
9
9
|
BEGIN
|
|
10
10
|
-- Only create if admin email is provided and not empty
|
|
11
|
-
|
|
11
|
+
-- Skip when the email is still an unreplaced "{{ADMIN_EMAIL}}" template token
|
|
12
|
+
-- (e.g. the raw dev template mounted at /tmp/super-admin.sql): the existence
|
|
13
|
+
-- check could never match, so it would always attempt a duplicate INSERT and
|
|
14
|
+
-- collide on UNIQUE(phone) with the admin created at container init.
|
|
15
|
+
IF admin_email IS NOT NULL AND admin_email != '' AND admin_email NOT LIKE '{{%' THEN
|
|
12
16
|
|
|
13
17
|
-- Check if user already exists
|
|
14
18
|
IF NOT EXISTS (SELECT 1 FROM auth.users WHERE email = admin_email) THEN
|
|
@@ -9,7 +9,10 @@ DECLARE
|
|
|
9
9
|
new_user_id UUID;
|
|
10
10
|
BEGIN
|
|
11
11
|
-- Only create if admin email is provided and not empty
|
|
12
|
-
|
|
12
|
+
-- Skip when the email is still an unreplaced "{{ADMIN_EMAIL}}" template token:
|
|
13
|
+
-- the existence check could never match, so it would always attempt a
|
|
14
|
+
-- duplicate INSERT and collide on UNIQUE(phone) with an existing admin.
|
|
15
|
+
IF admin_email IS NOT NULL AND admin_email != '' AND admin_email NOT LIKE '{{%' THEN
|
|
13
16
|
|
|
14
17
|
-- Check if user already exists
|
|
15
18
|
IF NOT EXISTS (SELECT 1 FROM auth.users WHERE email = admin_email) THEN
|
package/package.json
CHANGED
package/src/create.js
CHANGED
|
@@ -1165,6 +1165,64 @@ ${runCmd} test:prepush
|
|
|
1165
1165
|
);
|
|
1166
1166
|
await tick();
|
|
1167
1167
|
|
|
1168
|
+
// Create the initial commit so the user starts from a clean, tracked
|
|
1169
|
+
// template baseline (not a pile of untracked files). Guarded by the same
|
|
1170
|
+
// `gitAvailable` flag as `git init` above — if git isn't on PATH we already
|
|
1171
|
+
// logged the skip note and there's no repo to commit into.
|
|
1172
|
+
if (gitAvailable) {
|
|
1173
|
+
s.message('Creating initial commit');
|
|
1174
|
+
|
|
1175
|
+
// The pre-commit hook we just installed runs `${pm} lint` under `set -e`,
|
|
1176
|
+
// and node_modules is absent by default (install is opt-in via -install),
|
|
1177
|
+
// so a verified commit would fail. The template is vibecarbon-authored and
|
|
1178
|
+
// already clean; the hooks exist to gate the user's *future* commits.
|
|
1179
|
+
const commitArgs = ['git', 'commit', '--no-verify', '-m', 'Initial commit from vibecarbon'];
|
|
1180
|
+
|
|
1181
|
+
// `git commit` aborts when no author identity is configured. If neither
|
|
1182
|
+
// user.name nor user.email is set, prepend fallback identity via `-c`
|
|
1183
|
+
// (command-line `-c` only overrides config files, and is itself overridden
|
|
1184
|
+
// by any GIT_AUTHOR_* env — so a real identity, by config or env, always
|
|
1185
|
+
// wins; the fallback only fills a total void).
|
|
1186
|
+
const name = runCommand(['git', 'config', 'user.name'], {
|
|
1187
|
+
cwd: projectDir,
|
|
1188
|
+
cleanEnv: true,
|
|
1189
|
+
silent: true,
|
|
1190
|
+
ignoreError: true,
|
|
1191
|
+
});
|
|
1192
|
+
const email = runCommand(['git', 'config', 'user.email'], {
|
|
1193
|
+
cwd: projectDir,
|
|
1194
|
+
cleanEnv: true,
|
|
1195
|
+
silent: true,
|
|
1196
|
+
ignoreError: true,
|
|
1197
|
+
});
|
|
1198
|
+
if (!name || !email || !String(name).trim() || !String(email).trim()) {
|
|
1199
|
+
commitArgs.splice(
|
|
1200
|
+
1,
|
|
1201
|
+
0,
|
|
1202
|
+
'-c',
|
|
1203
|
+
'user.name=vibecarbon',
|
|
1204
|
+
'-c',
|
|
1205
|
+
'user.email=vibecarbon@users.noreply.github.com',
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const staged = runCommand(['git', 'add', '-A'], {
|
|
1210
|
+
cwd: projectDir,
|
|
1211
|
+
cleanEnv: true,
|
|
1212
|
+
ignoreError: true,
|
|
1213
|
+
});
|
|
1214
|
+
const committed =
|
|
1215
|
+
staged !== null &&
|
|
1216
|
+
runCommand(commitArgs, { cwd: projectDir, cleanEnv: true, ignoreError: true }) !== null;
|
|
1217
|
+
|
|
1218
|
+
if (!committed) {
|
|
1219
|
+
p.log.warn(
|
|
1220
|
+
"Couldn't create the initial commit — run `git add -A && git commit` in the project to capture the baseline.",
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
await tick();
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1168
1226
|
s.stop('Project created successfully');
|
|
1169
1227
|
registerProject(projectName, projectDir);
|
|
1170
1228
|
tracker.finish();
|