@mars-stack/cli 0.2.0 ā 1.0.2
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/dist/index.js +137 -12
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/template/.cursor/rules/composition-patterns.mdc +186 -0
- package/template/.cursor/rules/data-access.mdc +29 -0
- package/template/.cursor/rules/project-structure.mdc +34 -0
- package/template/.cursor/rules/security.mdc +25 -0
- package/template/.cursor/rules/testing.mdc +24 -0
- package/template/.cursor/rules/ui-conventions.mdc +29 -0
- package/template/.cursor/skills/add-api-route/SKILL.md +122 -0
- package/template/.cursor/skills/add-audit-log/SKILL.md +375 -0
- package/template/.cursor/skills/add-blog/SKILL.md +447 -0
- package/template/.cursor/skills/add-command-palette/SKILL.md +438 -0
- package/template/.cursor/skills/add-component/SKILL.md +158 -0
- package/template/.cursor/skills/add-crud-routes/SKILL.md +221 -0
- package/template/.cursor/skills/add-e2e-test/SKILL.md +227 -0
- package/template/.cursor/skills/add-error-boundary/SKILL.md +472 -0
- package/template/.cursor/skills/add-feature/SKILL.md +174 -0
- package/template/.cursor/skills/add-middleware/SKILL.md +135 -0
- package/template/.cursor/skills/add-page/SKILL.md +151 -0
- package/template/.cursor/skills/add-prisma-model/SKILL.md +148 -0
- package/template/.cursor/skills/add-protected-resource/SKILL.md +192 -0
- package/template/.cursor/skills/add-role/SKILL.md +156 -0
- package/template/.cursor/skills/add-server-action/SKILL.md +167 -0
- package/template/.cursor/skills/add-webhook/SKILL.md +192 -0
- package/template/.cursor/skills/build-complete-feature/SKILL.md +227 -0
- package/template/.cursor/skills/build-dashboard/SKILL.md +211 -0
- package/template/.cursor/skills/build-data-table/SKILL.md +283 -0
- package/template/.cursor/skills/build-form/SKILL.md +231 -0
- package/template/.cursor/skills/build-landing-page/SKILL.md +248 -0
- package/template/.cursor/skills/configure-ai/SKILL.md +617 -0
- package/template/.cursor/skills/configure-analytics/SKILL.md +413 -0
- package/template/.cursor/skills/configure-dark-mode/SKILL.md +309 -0
- package/template/.cursor/skills/configure-email/SKILL.md +170 -0
- package/template/.cursor/skills/configure-email-verification/SKILL.md +333 -0
- package/template/.cursor/skills/configure-feature-flags/SKILL.md +361 -0
- package/template/.cursor/skills/configure-i18n/SKILL.md +518 -0
- package/template/.cursor/skills/configure-jobs/SKILL.md +500 -0
- package/template/.cursor/skills/configure-magic-links/SKILL.md +385 -0
- package/template/.cursor/skills/configure-multi-tenancy/SKILL.md +611 -0
- package/template/.cursor/skills/configure-notifications/SKILL.md +569 -0
- package/template/.cursor/skills/configure-oauth/SKILL.md +217 -0
- package/template/.cursor/skills/configure-onboarding/SKILL.md +483 -0
- package/template/.cursor/skills/configure-payments/SKILL.md +243 -0
- package/template/.cursor/skills/configure-realtime/SKILL.md +733 -0
- package/template/.cursor/skills/configure-search/SKILL.md +581 -0
- package/template/.cursor/skills/configure-storage/SKILL.md +273 -0
- package/template/.cursor/skills/configure-two-factor/SKILL.md +518 -0
- package/template/.cursor/skills/create-execution-plan/SKILL.md +204 -0
- package/template/.cursor/skills/create-seed/SKILL.md +191 -0
- package/template/.cursor/skills/deploy-to-vercel/SKILL.md +300 -0
- package/template/.cursor/skills/design-tokens/SKILL.md +138 -0
- package/template/.cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
- package/template/.cursor/skills/setup-billing/SKILL.md +322 -0
- package/template/.cursor/skills/setup-project/SKILL.md +104 -0
- package/template/.cursor/skills/setup-teams/SKILL.md +682 -0
- package/template/.cursor/skills/test-api-route/SKILL.md +219 -0
- package/template/.cursor/skills/update-architecture-docs/SKILL.md +99 -0
- package/template/AGENTS.md +104 -0
- package/template/ARCHITECTURE.md +102 -0
- package/template/docs/QUALITY_SCORE.md +20 -0
- package/template/docs/design-docs/conversation-as-system-record.md +70 -0
- package/template/docs/design-docs/core-beliefs.md +43 -0
- package/template/docs/design-docs/index.md +8 -0
- package/template/docs/exec-plans/active/.gitkeep +0 -0
- package/template/docs/exec-plans/completed/.gitkeep +0 -0
- package/template/docs/exec-plans/tech-debt.md +7 -0
- package/template/docs/generated/.gitkeep +0 -0
- package/template/docs/product-specs/index.md +7 -0
- package/template/docs/references/index.md +18 -0
- package/template/e2e/api.spec.ts +20 -0
- package/template/e2e/auth.spec.ts +24 -0
- package/template/e2e/public.spec.ts +25 -0
- package/template/eslint.config.mjs +24 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.ts +45 -0
- package/template/package.json +80 -0
- package/template/playwright.config.ts +31 -0
- package/template/postcss.config.mjs +8 -0
- package/template/prisma/generated/prisma/browser.ts +49 -0
- package/template/prisma/generated/prisma/client.ts +73 -0
- package/template/prisma/generated/prisma/commonInputTypes.ts +406 -0
- package/template/prisma/generated/prisma/enums.ts +15 -0
- package/template/prisma/generated/prisma/internal/class.ts +254 -0
- package/template/prisma/generated/prisma/internal/prismaNamespace.ts +1240 -0
- package/template/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +190 -0
- package/template/prisma/generated/prisma/models/Account.ts +1543 -0
- package/template/prisma/generated/prisma/models/File.ts +1529 -0
- package/template/prisma/generated/prisma/models/Session.ts +1415 -0
- package/template/prisma/generated/prisma/models/Subscription.ts +1455 -0
- package/template/prisma/generated/prisma/models/User.ts +2235 -0
- package/template/prisma/generated/prisma/models/VerificationToken.ts +1099 -0
- package/template/prisma/generated/prisma/models.ts +17 -0
- package/template/prisma/schema/auth.prisma +69 -0
- package/template/prisma/schema/base.prisma +8 -0
- package/template/prisma/schema/file.prisma +15 -0
- package/template/prisma/schema/subscription.prisma +17 -0
- package/template/prisma.config.ts +13 -0
- package/template/scripts/check-architecture.ts +221 -0
- package/template/scripts/check-doc-freshness.ts +242 -0
- package/template/scripts/ensure-db.mjs +291 -0
- package/template/scripts/generate-docs.ts +143 -0
- package/template/scripts/generate-env-example.ts +89 -0
- package/template/scripts/seed.ts +56 -0
- package/template/scripts/update-quality-score.ts +263 -0
- package/template/src/__tests__/architecture.test.ts +114 -0
- package/template/src/app/(auth)/forgotten-password/page.tsx +92 -0
- package/template/src/app/(auth)/layout.tsx +11 -0
- package/template/src/app/(auth)/register/page.tsx +162 -0
- package/template/src/app/(auth)/reset-password/page.tsx +109 -0
- package/template/src/app/(auth)/sign-in/page.tsx +122 -0
- package/template/src/app/(auth)/verify/[token]/page.tsx +87 -0
- package/template/src/app/(auth)/verify/page.tsx +56 -0
- package/template/src/app/(protected)/admin/page.tsx +108 -0
- package/template/src/app/(protected)/dashboard/loading.tsx +20 -0
- package/template/src/app/(protected)/dashboard/page.tsx +22 -0
- package/template/src/app/(protected)/layout.tsx +262 -0
- package/template/src/app/(protected)/settings/page.tsx +370 -0
- package/template/src/app/api/auth/forgot/route.ts +63 -0
- package/template/src/app/api/auth/login/route.ts +121 -0
- package/template/src/app/api/auth/logout/route.ts +19 -0
- package/template/src/app/api/auth/me/route.ts +30 -0
- package/template/src/app/api/auth/reset/route.ts +45 -0
- package/template/src/app/api/auth/signup/route.ts +85 -0
- package/template/src/app/api/auth/verify/route.ts +46 -0
- package/template/src/app/api/csrf/route.ts +12 -0
- package/template/src/app/api/health/route.ts +10 -0
- package/template/src/app/api/protected/admin/users/route.ts +24 -0
- package/template/src/app/api/protected/billing/checkout/route.ts +83 -0
- package/template/src/app/api/protected/billing/portal/route.ts +39 -0
- package/template/src/app/api/protected/files/[fileId]/route.ts +86 -0
- package/template/src/app/api/protected/files/upload/route.ts +64 -0
- package/template/src/app/api/protected/user/password/route.ts +63 -0
- package/template/src/app/api/protected/user/profile/route.ts +35 -0
- package/template/src/app/api/protected/user/sessions/[sessionId]/route.ts +33 -0
- package/template/src/app/api/protected/user/sessions/route.ts +22 -0
- package/template/src/app/api/readiness/route.ts +15 -0
- package/template/src/app/api/webhooks/stripe/route.ts +166 -0
- package/template/src/app/error.tsx +33 -0
- package/template/src/app/layout.tsx +29 -0
- package/template/src/app/not-found.tsx +20 -0
- package/template/src/app/page.tsx +136 -0
- package/template/src/app/privacy/page.tsx +178 -0
- package/template/src/app/providers.tsx +8 -0
- package/template/src/app/terms/page.tsx +139 -0
- package/template/src/config/app.config.ts +70 -0
- package/template/src/config/routes.ts +17 -0
- package/template/src/features/admin/index.ts +11 -0
- package/template/src/features/admin/permissions.ts +64 -0
- package/template/src/features/auth/context/AuthContext.tsx +96 -0
- package/template/src/features/auth/context/index.ts +2 -0
- package/template/src/features/auth/index.ts +3 -0
- package/template/src/features/auth/server/consent.ts +66 -0
- package/template/src/features/auth/server/session-revocation.ts +20 -0
- package/template/src/features/auth/server/sessions.ts +66 -0
- package/template/src/features/auth/server/user.ts +166 -0
- package/template/src/features/auth/types.ts +19 -0
- package/template/src/features/auth/validators.ts +29 -0
- package/template/src/features/billing/server/index.ts +66 -0
- package/template/src/features/billing/types.ts +43 -0
- package/template/src/features/uploads/server/index.ts +49 -0
- package/template/src/features/uploads/types.ts +26 -0
- package/template/src/lib/core/email/templates/base-layout.ts +122 -0
- package/template/src/lib/core/email/templates/index.ts +4 -0
- package/template/src/lib/core/email/templates/password-reset-email.ts +42 -0
- package/template/src/lib/core/email/templates/verification-email.ts +41 -0
- package/template/src/lib/core/email/templates/welcome-email.ts +40 -0
- package/template/src/lib/mars.ts +56 -0
- package/template/src/lib/prisma.ts +19 -0
- package/template/src/proxy.ts +92 -0
- package/template/src/styles/brand.css +15 -0
- package/template/src/styles/globals.css +7 -0
- package/template/tsconfig.json +59 -0
- package/template/vitest.config.ts +41 -0
- package/template/vitest.setup.ts +24 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensures a local database is available before starting development.
|
|
5
|
+
*
|
|
6
|
+
* Strategy (in order):
|
|
7
|
+
* 1. If DATABASE_URL connects to a reachable database, use it and sync schema.
|
|
8
|
+
* 2. Otherwise, start an embedded PostgreSQL via `prisma dev` (PGlite-powered).
|
|
9
|
+
* No Docker. No installation. No external processes.
|
|
10
|
+
* 3. Sync the Prisma schema automatically.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execSync } from 'node:child_process';
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
16
|
+
import { resolve, dirname } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const ROOT = resolve(__dirname, '..');
|
|
21
|
+
|
|
22
|
+
const CYAN = '\x1b[36m';
|
|
23
|
+
const GREEN = '\x1b[32m';
|
|
24
|
+
const YELLOW = '\x1b[33m';
|
|
25
|
+
const DIM = '\x1b[2m';
|
|
26
|
+
const RESET = '\x1b[0m';
|
|
27
|
+
const BOLD = '\x1b[1m';
|
|
28
|
+
|
|
29
|
+
function log(icon, msg) {
|
|
30
|
+
console.log(` ${icon} ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadEnvFile() {
|
|
34
|
+
const envFiles = ['.env.development.local', '.env.development', '.env'];
|
|
35
|
+
for (const file of envFiles) {
|
|
36
|
+
const filePath = resolve(ROOT, file);
|
|
37
|
+
if (existsSync(filePath)) {
|
|
38
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
39
|
+
for (const line of content.split('\n')) {
|
|
40
|
+
const trimmed = line.trim();
|
|
41
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
42
|
+
const eqIdx = trimmed.indexOf('=');
|
|
43
|
+
if (eqIdx === -1) continue;
|
|
44
|
+
const key = trimmed.slice(0, eqIdx);
|
|
45
|
+
let value = trimmed.slice(eqIdx + 1);
|
|
46
|
+
if (
|
|
47
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
48
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
49
|
+
) {
|
|
50
|
+
value = value.slice(1, -1);
|
|
51
|
+
}
|
|
52
|
+
if (!process.env[key]) {
|
|
53
|
+
process.env[key] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function canConnectToDb() {
|
|
61
|
+
try {
|
|
62
|
+
execSync('npx prisma db execute --stdin <<< "SELECT 1"', {
|
|
63
|
+
cwd: ROOT,
|
|
64
|
+
stdio: 'ignore',
|
|
65
|
+
timeout: 8000,
|
|
66
|
+
});
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function pushSchema() {
|
|
74
|
+
try {
|
|
75
|
+
execSync('npx prisma db push --skip-generate --accept-data-loss', {
|
|
76
|
+
cwd: ROOT,
|
|
77
|
+
stdio: 'inherit',
|
|
78
|
+
timeout: 30000,
|
|
79
|
+
env: { ...process.env },
|
|
80
|
+
});
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getProjectName() {
|
|
88
|
+
try {
|
|
89
|
+
const pkg = JSON.parse(
|
|
90
|
+
readFileSync(resolve(ROOT, 'package.json'), 'utf-8'),
|
|
91
|
+
);
|
|
92
|
+
return (pkg.name || 'mars')
|
|
93
|
+
.replace(/^@[^/]+\//, '')
|
|
94
|
+
.replace(/[^a-z0-9-]/g, '-');
|
|
95
|
+
} catch {
|
|
96
|
+
return 'mars';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function stripAnsi(str) {
|
|
101
|
+
return str.replace(
|
|
102
|
+
/[\x1b\x9b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
|
|
103
|
+
'',
|
|
104
|
+
).replace(/\]8;;[^\x1b]*/g, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getPrismaDevTcpUrl(name) {
|
|
108
|
+
try {
|
|
109
|
+
const rawOutput = execSync('npx prisma dev ls 2>/dev/null', {
|
|
110
|
+
cwd: ROOT,
|
|
111
|
+
encoding: 'utf-8',
|
|
112
|
+
timeout: 15000,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const output = stripAnsi(rawOutput);
|
|
116
|
+
const isRunning = output.includes(name) && output.toLowerCase().includes('running');
|
|
117
|
+
|
|
118
|
+
// Extract the TCP postgres:// URL (not prisma+postgres://)
|
|
119
|
+
// Match postgres://...localhost:NNNNN/... with port in the 50000+ range (prisma dev default)
|
|
120
|
+
const tcpMatch = output.match(
|
|
121
|
+
/(?<![+])postgres:\/\/[^\s"]+localhost:\d+\/[^\s"]*/,
|
|
122
|
+
);
|
|
123
|
+
if (tcpMatch) {
|
|
124
|
+
// Clean up any trailing characters
|
|
125
|
+
const url = tcpMatch[0].replace(/[)\]}>]+$/, '');
|
|
126
|
+
return { running: isRunning, url };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { running: isRunning, url: null };
|
|
130
|
+
} catch {
|
|
131
|
+
return { running: false, url: null };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function startPrismaDev(name) {
|
|
136
|
+
try {
|
|
137
|
+
const rawOutput = execSync(
|
|
138
|
+
`npx prisma dev --detach --name="${name}" 2>&1`,
|
|
139
|
+
{ cwd: ROOT, encoding: 'utf-8', timeout: 30000 },
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const output = stripAnsi(rawOutput);
|
|
143
|
+
const tcpMatch = output.match(
|
|
144
|
+
/(?<![+])postgres:\/\/[^\s"]+localhost:\d+\/[^\s"]*/,
|
|
145
|
+
);
|
|
146
|
+
return tcpMatch ? tcpMatch[0].replace(/[)\]}>]+$/, '') : null;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function generateJwtSecret() {
|
|
153
|
+
try {
|
|
154
|
+
return execSync('openssl rand -hex 32', { encoding: 'utf-8' }).trim();
|
|
155
|
+
} catch {
|
|
156
|
+
return crypto.randomBytes(32).toString('hex');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function ensureJwtSecret() {
|
|
161
|
+
if (process.env.JWT_SECRET && process.env.JWT_SECRET.length >= 32) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const envLocalPath = resolve(ROOT, '.env.development.local');
|
|
165
|
+
let content = existsSync(envLocalPath) ? readFileSync(envLocalPath, 'utf-8') : '';
|
|
166
|
+
const secret = generateJwtSecret();
|
|
167
|
+
if (content.includes('JWT_SECRET=')) {
|
|
168
|
+
content = content.replace(/^JWT_SECRET=.*$/m, `JWT_SECRET="${secret}"`);
|
|
169
|
+
} else {
|
|
170
|
+
content += (content && !content.endsWith('\n') ? '\n' : '') + `# Required for auth (ensure-db)\nJWT_SECRET="${secret}"\n`;
|
|
171
|
+
}
|
|
172
|
+
writeFileSync(envLocalPath, content);
|
|
173
|
+
process.env.JWT_SECRET = secret;
|
|
174
|
+
log(`${GREEN}ā${RESET}`, `JWT_SECRET written to .env.development.local`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function writeLocalDbUrl(url) {
|
|
178
|
+
const envLocalPath = resolve(ROOT, '.env.development.local');
|
|
179
|
+
let content = '';
|
|
180
|
+
|
|
181
|
+
if (existsSync(envLocalPath)) {
|
|
182
|
+
content = readFileSync(envLocalPath, 'utf-8');
|
|
183
|
+
if (content.includes('DATABASE_URL=')) {
|
|
184
|
+
content = content.replace(
|
|
185
|
+
/^DATABASE_URL=.*$/m,
|
|
186
|
+
`DATABASE_URL="${url}"`,
|
|
187
|
+
);
|
|
188
|
+
writeFileSync(envLocalPath, content);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const addition = `${content ? '\n' : ''}# Auto-generated by ensure-db (prisma dev)\nDATABASE_URL="${url}"\n`;
|
|
194
|
+
writeFileSync(envLocalPath, content + addition);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function main() {
|
|
198
|
+
console.log();
|
|
199
|
+
console.log(
|
|
200
|
+
`${BOLD}${CYAN}MARS${RESET} ${DIM}Ensuring database...${RESET}`,
|
|
201
|
+
);
|
|
202
|
+
console.log();
|
|
203
|
+
|
|
204
|
+
loadEnvFile();
|
|
205
|
+
ensureJwtSecret();
|
|
206
|
+
|
|
207
|
+
// 1. Check if existing DATABASE_URL works (skip if it's a prisma dev auto-URL)
|
|
208
|
+
const dbUrl = process.env.DATABASE_URL || '';
|
|
209
|
+
const isAutoUrl = dbUrl.includes('localhost:512') || dbUrl.includes('ensure-db');
|
|
210
|
+
if (dbUrl && !isAutoUrl) {
|
|
211
|
+
const connected = canConnectToDb();
|
|
212
|
+
if (connected) {
|
|
213
|
+
log(`${GREEN}ā${RESET}`, 'Database is reachable.');
|
|
214
|
+
log(`${CYAN}ā${RESET}`, 'Syncing schema...');
|
|
215
|
+
pushSchema();
|
|
216
|
+
log(`${GREEN}ā${RESET}`, 'Ready.');
|
|
217
|
+
console.log();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
log(
|
|
221
|
+
`${YELLOW}!${RESET}`,
|
|
222
|
+
`DATABASE_URL is set but not reachable. Starting embedded database.`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 2. Start or connect to embedded Postgres via prisma dev
|
|
227
|
+
const projectName = getProjectName();
|
|
228
|
+
const existing = getPrismaDevTcpUrl(projectName);
|
|
229
|
+
|
|
230
|
+
let tcpUrl = existing.url;
|
|
231
|
+
|
|
232
|
+
if (existing.running && tcpUrl) {
|
|
233
|
+
log(
|
|
234
|
+
`${GREEN}ā${RESET}`,
|
|
235
|
+
`Embedded Postgres "${projectName}" already running.`,
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
log(
|
|
239
|
+
`${CYAN}ā${RESET}`,
|
|
240
|
+
`Starting embedded Postgres via ${BOLD}prisma dev${RESET}...`,
|
|
241
|
+
);
|
|
242
|
+
const startUrl = startPrismaDev(projectName);
|
|
243
|
+
|
|
244
|
+
if (!startUrl) {
|
|
245
|
+
// Try to get the URL from ls (server may have started despite no URL in stdout)
|
|
246
|
+
const retry = getPrismaDevTcpUrl(projectName);
|
|
247
|
+
tcpUrl = retry.url;
|
|
248
|
+
} else {
|
|
249
|
+
tcpUrl = startUrl;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!tcpUrl) {
|
|
253
|
+
log(`${YELLOW}!${RESET}`, 'Could not start embedded database.');
|
|
254
|
+
log(
|
|
255
|
+
' ',
|
|
256
|
+
`${DIM}The app will start anyway ā UI preview works without a database.${RESET}`,
|
|
257
|
+
);
|
|
258
|
+
log(
|
|
259
|
+
' ',
|
|
260
|
+
`${DIM}Set DATABASE_URL in .env to a cloud database (Neon, Supabase) to connect.${RESET}`,
|
|
261
|
+
);
|
|
262
|
+
console.log();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
log(`${GREEN}ā${RESET}`, `Embedded Postgres running.`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 3. Write the TCP connection URL to .env.development.local
|
|
270
|
+
writeLocalDbUrl(tcpUrl);
|
|
271
|
+
process.env.DATABASE_URL = tcpUrl;
|
|
272
|
+
log(`${GREEN}ā${RESET}`, `DATABASE_URL written to .env.development.local`);
|
|
273
|
+
|
|
274
|
+
// 4. Sync schema
|
|
275
|
+
log(`${CYAN}ā${RESET}`, 'Syncing schema...');
|
|
276
|
+
const pushed = pushSchema();
|
|
277
|
+
if (pushed) {
|
|
278
|
+
log(`${GREEN}ā${RESET}`, 'Database schema is up to date.');
|
|
279
|
+
} else {
|
|
280
|
+
log(
|
|
281
|
+
`${YELLOW}!${RESET}`,
|
|
282
|
+
`Schema push failed ā run ${BOLD}yarn db:push${RESET} manually after startup.`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
main().catch((err) => {
|
|
290
|
+
console.error('ensure-db failed:', err.message);
|
|
291
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-generates documentation from the codebase.
|
|
3
|
+
* Outputs to docs/generated/ for snapshot comparison in CI.
|
|
4
|
+
*
|
|
5
|
+
* Usage: npx tsx scripts/generate-docs.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
|
|
11
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
12
|
+
const OUTPUT_DIR = path.join(ROOT, 'docs', 'generated');
|
|
13
|
+
|
|
14
|
+
function ensureDir(dir: string): void {
|
|
15
|
+
if (!fs.existsSync(dir)) {
|
|
16
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function generateDbSchema(): string {
|
|
21
|
+
const schemaDir = path.join(ROOT, 'prisma', 'schema');
|
|
22
|
+
if (!fs.existsSync(schemaDir)) {
|
|
23
|
+
return '# Database Schema\n\nNo Prisma schema files found.\n';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const lines: string[] = [
|
|
27
|
+
'# Database Schema',
|
|
28
|
+
'',
|
|
29
|
+
'Auto-generated from `prisma/schema/` files.',
|
|
30
|
+
'',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const schemaFiles = fs.readdirSync(schemaDir).filter((f) => f.endsWith('.prisma'));
|
|
34
|
+
|
|
35
|
+
for (const file of schemaFiles) {
|
|
36
|
+
const content = fs.readFileSync(path.join(schemaDir, file), 'utf-8');
|
|
37
|
+
const models = content.match(/model\s+(\w+)\s*\{[^}]*\}/g) || [];
|
|
38
|
+
|
|
39
|
+
if (models.length > 0) {
|
|
40
|
+
lines.push(`## ${file}`);
|
|
41
|
+
lines.push('');
|
|
42
|
+
|
|
43
|
+
for (const model of models) {
|
|
44
|
+
const nameMatch = model.match(/model\s+(\w+)/);
|
|
45
|
+
if (!nameMatch) continue;
|
|
46
|
+
|
|
47
|
+
lines.push(`### ${nameMatch[1]}`);
|
|
48
|
+
lines.push('');
|
|
49
|
+
lines.push('```prisma');
|
|
50
|
+
lines.push(model.trim());
|
|
51
|
+
lines.push('```');
|
|
52
|
+
lines.push('');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function generateApiRoutes(): string {
|
|
61
|
+
const apiDir = path.join(ROOT, 'src', 'app', 'api');
|
|
62
|
+
if (!fs.existsSync(apiDir)) {
|
|
63
|
+
return '# API Routes\n\nNo API route directory found.\n';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const lines: string[] = [
|
|
67
|
+
'# API Routes',
|
|
68
|
+
'',
|
|
69
|
+
'Auto-generated from `src/app/api/` directory.',
|
|
70
|
+
'',
|
|
71
|
+
'| Endpoint | Methods | Auth Required |',
|
|
72
|
+
'|----------|---------|---------------|',
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
function scanRoutes(dir: string, prefix: string): void {
|
|
76
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
scanRoutes(path.join(dir, entry.name), `${prefix}/${entry.name}`);
|
|
81
|
+
} else if (entry.name === 'route.ts' || entry.name === 'route.tsx') {
|
|
82
|
+
const content = fs.readFileSync(path.join(dir, entry.name), 'utf-8');
|
|
83
|
+
const methods: string[] = [];
|
|
84
|
+
if (content.includes('export const GET') || content.includes('export async function GET')) methods.push('GET');
|
|
85
|
+
if (content.includes('export const POST') || content.includes('export async function POST')) methods.push('POST');
|
|
86
|
+
if (content.includes('export const PUT') || content.includes('export async function PUT')) methods.push('PUT');
|
|
87
|
+
if (content.includes('export const DELETE') || content.includes('export async function DELETE')) methods.push('DELETE');
|
|
88
|
+
if (content.includes('export const PATCH') || content.includes('export async function PATCH')) methods.push('PATCH');
|
|
89
|
+
|
|
90
|
+
const isProtected = prefix.includes('/protected') || content.includes('withAuth') || content.includes('withRole') || content.includes('withOwnership');
|
|
91
|
+
|
|
92
|
+
lines.push(`| \`/api${prefix}\` | ${methods.join(', ') || 'unknown'} | ${isProtected ? 'Yes' : 'No'} |`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
scanRoutes(apiDir, '');
|
|
98
|
+
lines.push('');
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function generateComponentMap(): string {
|
|
103
|
+
const lines: string[] = [
|
|
104
|
+
'# Component Map',
|
|
105
|
+
'',
|
|
106
|
+
'Auto-generated from `@mars-stack/ui` component exports.',
|
|
107
|
+
'',
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const categories = [
|
|
111
|
+
{ name: 'Primitives', description: 'Atomic UI components with no business logic' },
|
|
112
|
+
{ name: 'Patterns', description: 'Compositions of primitives, still no business logic' },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
for (const category of categories) {
|
|
116
|
+
lines.push(`## ${category.name}`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push(category.description);
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push('> Component list is populated from @mars-stack/ui exports. Run this script after installing dependencies.');
|
|
121
|
+
lines.push('');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function main(): void {
|
|
128
|
+
ensureDir(OUTPUT_DIR);
|
|
129
|
+
|
|
130
|
+
const dbSchema = generateDbSchema();
|
|
131
|
+
fs.writeFileSync(path.join(OUTPUT_DIR, 'db-schema.md'), dbSchema);
|
|
132
|
+
console.log('Generated docs/generated/db-schema.md');
|
|
133
|
+
|
|
134
|
+
const apiRoutes = generateApiRoutes();
|
|
135
|
+
fs.writeFileSync(path.join(OUTPUT_DIR, 'api-routes.md'), apiRoutes);
|
|
136
|
+
console.log('Generated docs/generated/api-routes.md');
|
|
137
|
+
|
|
138
|
+
const componentMap = generateComponentMap();
|
|
139
|
+
fs.writeFileSync(path.join(OUTPUT_DIR, 'component-map.md'), componentMap);
|
|
140
|
+
console.log('Generated docs/generated/component-map.md');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
main();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates .env.example dynamically based on enabled features and services
|
|
3
|
+
* in app.config.ts. Only includes env vars that the current configuration needs.
|
|
4
|
+
*
|
|
5
|
+
* Run: `yarn generate:env` or `tsx scripts/generate-env-example.ts`
|
|
6
|
+
*/
|
|
7
|
+
import { writeFileSync } from 'fs';
|
|
8
|
+
import { resolve } from 'path';
|
|
9
|
+
|
|
10
|
+
// Inline the config reading to avoid TSX path alias issues in scripts
|
|
11
|
+
// In practice, this reads from the compiled config
|
|
12
|
+
const featureEnvMap: Record<string, { vars: string[]; comment: string }> = {
|
|
13
|
+
// Core (always required)
|
|
14
|
+
_core: {
|
|
15
|
+
vars: ['DATABASE_URL', 'JWT_SECRET', 'NEXTAUTH_URL', 'NEXT_PUBLIC_APP_URL'],
|
|
16
|
+
comment: 'Core (always required)',
|
|
17
|
+
},
|
|
18
|
+
// Feature-specific
|
|
19
|
+
googleOAuth: {
|
|
20
|
+
vars: ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'],
|
|
21
|
+
comment: 'Google OAuth',
|
|
22
|
+
},
|
|
23
|
+
billing: {
|
|
24
|
+
vars: ['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET', 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY'],
|
|
25
|
+
comment: 'Stripe Billing',
|
|
26
|
+
},
|
|
27
|
+
analytics: {
|
|
28
|
+
vars: ['NEXT_PUBLIC_ANALYTICS_ID'],
|
|
29
|
+
comment: 'Analytics',
|
|
30
|
+
},
|
|
31
|
+
sentry: {
|
|
32
|
+
vars: ['SENTRY_DSN', 'SENTRY_AUTH_TOKEN'],
|
|
33
|
+
comment: 'Sentry Monitoring',
|
|
34
|
+
},
|
|
35
|
+
ai: {
|
|
36
|
+
vars: ['OPENAI_API_KEY'],
|
|
37
|
+
comment: 'AI Provider',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const _serviceEnvMap: Record<string, { vars: string[]; comment: string }> = {
|
|
42
|
+
sendgrid: {
|
|
43
|
+
vars: ['SENDGRID_API_KEY', 'EMAIL_FROM'],
|
|
44
|
+
comment: 'SendGrid Email',
|
|
45
|
+
},
|
|
46
|
+
resend: {
|
|
47
|
+
vars: ['RESEND_API_KEY', 'EMAIL_FROM'],
|
|
48
|
+
comment: 'Resend Email',
|
|
49
|
+
},
|
|
50
|
+
s3: {
|
|
51
|
+
vars: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_S3_BUCKET', 'AWS_REGION'],
|
|
52
|
+
comment: 'AWS S3 Storage',
|
|
53
|
+
},
|
|
54
|
+
vercel_storage: {
|
|
55
|
+
vars: ['BLOB_READ_WRITE_TOKEN'],
|
|
56
|
+
comment: 'Vercel Blob Storage',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function generateEnvExample(): string {
|
|
61
|
+
const lines: string[] = [
|
|
62
|
+
'# MARS Environment Variables',
|
|
63
|
+
'# Generated from app.config.ts -- only includes vars for enabled features',
|
|
64
|
+
`# Generated at: ${new Date().toISOString()}`,
|
|
65
|
+
'',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Always include core vars
|
|
69
|
+
const core = featureEnvMap._core;
|
|
70
|
+
lines.push(`# ${core.comment}`);
|
|
71
|
+
core.vars.forEach((v) => lines.push(`${v}=`));
|
|
72
|
+
lines.push('');
|
|
73
|
+
|
|
74
|
+
// Redis (always included for rate limiting)
|
|
75
|
+
lines.push('# Redis (rate limiting + caching)');
|
|
76
|
+
lines.push('UPSTASH_REDIS_REST_URL=');
|
|
77
|
+
lines.push('UPSTASH_REDIS_REST_TOKEN=');
|
|
78
|
+
lines.push('');
|
|
79
|
+
|
|
80
|
+
lines.push('# Add feature-specific env vars by enabling features in app.config.ts');
|
|
81
|
+
lines.push('# Then run: yarn generate:env');
|
|
82
|
+
|
|
83
|
+
return lines.join('\n') + '\n';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const output = generateEnvExample();
|
|
87
|
+
const outputPath = resolve(process.cwd(), '.env.example');
|
|
88
|
+
writeFileSync(outputPath, output);
|
|
89
|
+
console.log(`Generated .env.example with core vars`);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { PrismaClient } from '../prisma/generated/prisma/client.js';
|
|
2
|
+
import { PrismaPg } from '@prisma/adapter-pg';
|
|
3
|
+
import { hashPassword } from '@mars-stack/core/auth/password';
|
|
4
|
+
|
|
5
|
+
const adapter = new PrismaPg({
|
|
6
|
+
connectionString: process.env.DATABASE_URL!,
|
|
7
|
+
});
|
|
8
|
+
const prisma = new PrismaClient({ adapter });
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PASSWORD = 'Password123!';
|
|
11
|
+
|
|
12
|
+
interface SeedUser {
|
|
13
|
+
email: string;
|
|
14
|
+
name: string;
|
|
15
|
+
role: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const seedUsers: SeedUser[] = [
|
|
19
|
+
{ email: 'admin@example.com', name: 'Admin', role: 'admin' },
|
|
20
|
+
{ email: 'user@example.com', name: 'Test User', role: 'user' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
async function seed() {
|
|
24
|
+
console.log('š± Seeding database...\n');
|
|
25
|
+
|
|
26
|
+
const hashedPassword = await hashPassword(DEFAULT_PASSWORD);
|
|
27
|
+
const now = new Date();
|
|
28
|
+
|
|
29
|
+
for (const { email, name, role } of seedUsers) {
|
|
30
|
+
const user = await prisma.user.upsert({
|
|
31
|
+
where: { email },
|
|
32
|
+
update: { name, role },
|
|
33
|
+
create: {
|
|
34
|
+
email,
|
|
35
|
+
name,
|
|
36
|
+
password: hashedPassword,
|
|
37
|
+
role,
|
|
38
|
+
emailVerified: now,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log(` ā ${role} user: ${user.email} (id: ${user.id})`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('\nā
Seed complete.');
|
|
46
|
+
console.log(` Default password for all users: ${DEFAULT_PASSWORD}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
seed()
|
|
50
|
+
.catch((error: unknown) => {
|
|
51
|
+
console.error('ā Seed failed:', error);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
})
|
|
54
|
+
.finally(async () => {
|
|
55
|
+
await prisma.$disconnect();
|
|
56
|
+
});
|