@usequark/quark-create-app 1.23.11
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 +210 -0
- package/package.json +56 -0
- package/src/dependabot-config.test.js +128 -0
- package/src/deploy/adapters/iac.js +203 -0
- package/src/deploy/adapters/index.js +27 -0
- package/src/deploy/adapters/railway.js +508 -0
- package/src/deploy/adapters/railway.test.js +318 -0
- package/src/deploy/contract.js +59 -0
- package/src/deploy/deploy.integration.test.js +183 -0
- package/src/deploy/deploy.js +430 -0
- package/src/deploy/deploy.test.js +105 -0
- package/src/deploy/discovery.js +115 -0
- package/src/deploy/discovery.test.js +141 -0
- package/src/deploy/index.js +38 -0
- package/src/deploy/inspect.js +130 -0
- package/src/index.js +2920 -0
- package/src/mobile-template.test.js +232 -0
- package/src/prepare-hooks.test.js +535 -0
- package/src/scaffold-guards.test.js +140 -0
- package/src/scaffold-output.test.js +228 -0
- package/src/template-scripts.test.js +160 -0
- package/src/utils.js +77 -0
- package/src/utils.test.js +95 -0
- package/src/worker-dockerfile.test.js +85 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.dockerignore +14 -0
- package/templates/base-project/.env.railway.example +87 -0
- package/templates/base-project/.github/copilot-instructions.md +65 -0
- package/templates/base-project/.github/dependabot.yml +31 -0
- package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
- package/templates/base-project/.github/workflows/ci.yml +116 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/install-windows.yml +27 -0
- package/templates/base-project/.github/workflows/release.yml +67 -0
- package/templates/base-project/.railway/railway.ts +40 -0
- package/templates/base-project/CLAUDE.md +163 -0
- package/templates/base-project/MAIN.md +21 -0
- package/templates/base-project/README.md +164 -0
- package/templates/base-project/apps/web/.env.example +99 -0
- package/templates/base-project/apps/web/Dockerfile +38 -0
- package/templates/base-project/apps/web/biome.json +8 -0
- package/templates/base-project/apps/web/jsconfig.json +9 -0
- package/templates/base-project/apps/web/next.config.js +154 -0
- package/templates/base-project/apps/web/package.json +42 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
- package/templates/base-project/apps/web/public/quark.svg +47 -0
- package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
- package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
- package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
- package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
- package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
- package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
- package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +58 -0
- package/templates/base-project/apps/web/src/app/globals.css +1166 -0
- package/templates/base-project/apps/web/src/app/icon.svg +47 -0
- package/templates/base-project/apps/web/src/app/layout.js +57 -0
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/not-found.js +39 -0
- package/templates/base-project/apps/web/src/app/page.js +262 -0
- package/templates/base-project/apps/web/src/app/robots.js +23 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
- package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
- package/templates/base-project/apps/web/src/lib/auth.js +191 -0
- package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
- package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
- package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
- package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
- package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
- package/templates/base-project/apps/web/src/preflight.js +55 -0
- package/templates/base-project/apps/web/src/proxy.js +328 -0
- package/templates/base-project/biome.json +52 -0
- package/templates/base-project/docker-compose.override.yml +17 -0
- package/templates/base-project/docker-compose.yml +37 -0
- package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
- package/templates/base-project/docs/START_HERE.md +70 -0
- package/templates/base-project/docs/adr/README.md +42 -0
- package/templates/base-project/package.json +58 -0
- package/templates/base-project/packages/db/package.json +38 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
- package/templates/base-project/packages/db/prisma/seed.js +152 -0
- package/templates/base-project/packages/db/prisma.config.js +27 -0
- package/templates/base-project/packages/db/src/client.js +75 -0
- package/templates/base-project/packages/db/src/connection.js +44 -0
- package/templates/base-project/packages/db/src/context.js +275 -0
- package/templates/base-project/packages/db/src/index.js +5 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/packages/db/src/queries.js +292 -0
- package/templates/base-project/packages/db/src/schemas.js +30 -0
- package/templates/base-project/packages/db/src/umami.js +299 -0
- package/templates/base-project/pnpm-workspace.yaml +20 -0
- package/templates/base-project/scripts/check-loading.mjs +49 -0
- package/templates/base-project/scripts/check-standards.mjs +186 -0
- package/templates/base-project/scripts/clean-workspace.mjs +479 -0
- package/templates/base-project/scripts/dev-preflight.mjs +48 -0
- package/templates/base-project/scripts/doctor.js +408 -0
- package/templates/base-project/scripts/prepare.js +490 -0
- package/templates/base-project/scripts/run-tests.mjs +71 -0
- package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
- package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
- package/templates/base-project/skills/add-model/SKILL.md +28 -0
- package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
- package/templates/base-project/skills/i18n/SKILL.md +211 -0
- package/templates/base-project/skills/payment/SKILL.md +280 -0
- package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
- package/templates/base-project/turbo.json +143 -0
- package/templates/config/package.json +20 -0
- package/templates/config/src/app-url.js +134 -0
- package/templates/config/src/environment.js +284 -0
- package/templates/config/src/index.js +16 -0
- package/templates/config/src/load-config.js +157 -0
- package/templates/config/src/rate-limit.js +136 -0
- package/templates/config/src/redis.js +43 -0
- package/templates/config/src/validate-env.js +532 -0
- package/templates/jobs/README.md +52 -0
- package/templates/jobs/package.json +9 -0
- package/templates/jobs/src/definitions.js +13 -0
- package/templates/jobs/src/index.js +1 -0
- package/templates/mobile/.env.example +16 -0
- package/templates/mobile/CHECKLIST.md +40 -0
- package/templates/mobile/README.md +130 -0
- package/templates/mobile/app/(app)/_layout.tsx +83 -0
- package/templates/mobile/app/(app)/index.tsx +147 -0
- package/templates/mobile/app/(app)/profile.tsx +204 -0
- package/templates/mobile/app/(app)/settings.tsx +80 -0
- package/templates/mobile/app/(auth)/_layout.tsx +10 -0
- package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
- package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
- package/templates/mobile/app/+not-found.tsx +32 -0
- package/templates/mobile/app/_layout.tsx +18 -0
- package/templates/mobile/app.json +47 -0
- package/templates/mobile/assets/adaptive-icon.png +0 -0
- package/templates/mobile/assets/icon.png +0 -0
- package/templates/mobile/assets/notification-icon.png +0 -0
- package/templates/mobile/assets/splash.png +0 -0
- package/templates/mobile/biome.json +27 -0
- package/templates/mobile/components/AuthGate.tsx +19 -0
- package/templates/mobile/components/ThemeProvider.tsx +23 -0
- package/templates/mobile/eas.json +29 -0
- package/templates/mobile/hooks/use-auth.ts +127 -0
- package/templates/mobile/lib/api-client.ts +138 -0
- package/templates/mobile/lib/api.ts +83 -0
- package/templates/mobile/lib/auth-providers-google.ts +74 -0
- package/templates/mobile/lib/auth-providers.ts +74 -0
- package/templates/mobile/lib/auth.ts +100 -0
- package/templates/mobile/lib/config.ts +14 -0
- package/templates/mobile/lib/errors.ts +46 -0
- package/templates/mobile/lib/notifications.ts +77 -0
- package/templates/mobile/lib/storage.ts +25 -0
- package/templates/mobile/lib/theme-tokens.ts +16 -0
- package/templates/mobile/package.json +37 -0
- package/templates/mobile/tsconfig.json +10 -0
- package/templates/pwa/app/_components/PWARegister.js +15 -0
- package/templates/pwa/app/manifest.js +32 -0
- package/templates/pwa/public/sw.js +72 -0
- package/templates/ui/README.md +44 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/src/badge.js +23 -0
- package/templates/ui/src/button.js +111 -0
- package/templates/ui/src/card.js +128 -0
- package/templates/ui/src/checkbox.js +20 -0
- package/templates/ui/src/container.js +13 -0
- package/templates/ui/src/dialog.js +137 -0
- package/templates/ui/src/error-banner.js +15 -0
- package/templates/ui/src/footer.js +239 -0
- package/templates/ui/src/form-field.integration-test.js +142 -0
- package/templates/ui/src/form-field.js +50 -0
- package/templates/ui/src/index.js +24 -0
- package/templates/ui/src/input.js +11 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/lightbox.integration-test.js +167 -0
- package/templates/ui/src/lightbox.js +243 -0
- package/templates/ui/src/logo.js +52 -0
- package/templates/ui/src/navbar.js +602 -0
- package/templates/ui/src/password-input.js +31 -0
- package/templates/ui/src/rich-text.js +514 -0
- package/templates/ui/src/select.js +317 -0
- package/templates/ui/src/skeleton.js +10 -0
- package/templates/ui/src/spinner.js +31 -0
- package/templates/ui/src/table.js +169 -0
- package/templates/ui/src/textarea.js +11 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +196 -0
- package/templates/ui/src/toast.js +206 -0
- package/templates/worker/Dockerfile +64 -0
- package/templates/worker/README.md +56 -0
- package/templates/worker/package.json +36 -0
- package/templates/worker/src/config/pricing.js +46 -0
- package/templates/worker/src/handlers/email-job-validation.js +36 -0
- package/templates/worker/src/handlers/email.js +98 -0
- package/templates/worker/src/handlers/files.js +59 -0
- package/templates/worker/src/handlers/index.js +20 -0
- package/templates/worker/src/handlers/push-notification.js +234 -0
- package/templates/worker/src/index.js +505 -0
- package/templates/worker/src/lib/railway.js +438 -0
- package/templates/worker/src/lib/tokens.js +81 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cross-platform git hooks setup script.
|
|
4
|
+
*
|
|
5
|
+
* Handles two layouts:
|
|
6
|
+
* - Project at the git root: installs hooks via simple-git-hooks (as before).
|
|
7
|
+
* - Project nested inside a larger repository (monorepo, Conductor workspace):
|
|
8
|
+
* git always runs hooks from the repository root, so plain `pnpm nano-staged`
|
|
9
|
+
* hooks would run in the wrong directory. Instead, the project registers
|
|
10
|
+
* itself in a shared dispatcher that runs each project's command from its own
|
|
11
|
+
* directory. Multiple nested Quark projects compose instead of overwriting
|
|
12
|
+
* each other, and hooks owned by other tools are never overwritten or deleted.
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
15
|
+
import crypto from "node:crypto";
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
21
|
+
const PROJECT_DIR = path.resolve(path.dirname(SCRIPT_PATH), "..");
|
|
22
|
+
|
|
23
|
+
// Marks hook files generated by this script so they can be safely replaced.
|
|
24
|
+
const HOOK_MARKER = "quark:scoped-git-hook";
|
|
25
|
+
|
|
26
|
+
// Directory (inside the git hooks dir) holding the dispatcher and one entry
|
|
27
|
+
// file per project and hook.
|
|
28
|
+
const SCOPED_HOOKS_DIR = "quark-scoped-hooks";
|
|
29
|
+
const ENTRIES_DIR = "entries";
|
|
30
|
+
|
|
31
|
+
// Hook names accepted by git (same list simple-git-hooks supports).
|
|
32
|
+
const VALID_HOOKS = new Set([
|
|
33
|
+
"applypatch-msg",
|
|
34
|
+
"pre-applypatch",
|
|
35
|
+
"post-applypatch",
|
|
36
|
+
"pre-commit",
|
|
37
|
+
"pre-merge-commit",
|
|
38
|
+
"prepare-commit-msg",
|
|
39
|
+
"commit-msg",
|
|
40
|
+
"post-commit",
|
|
41
|
+
"pre-rebase",
|
|
42
|
+
"post-checkout",
|
|
43
|
+
"post-merge",
|
|
44
|
+
"pre-push",
|
|
45
|
+
"pre-receive",
|
|
46
|
+
"update",
|
|
47
|
+
"proc-receive",
|
|
48
|
+
"post-receive",
|
|
49
|
+
"post-update",
|
|
50
|
+
"reference-transaction",
|
|
51
|
+
"push-to-checkout",
|
|
52
|
+
"pre-auto-gc",
|
|
53
|
+
"post-rewrite",
|
|
54
|
+
"sendemail-validate",
|
|
55
|
+
"fsmonitor-watchman",
|
|
56
|
+
"p4-changelist",
|
|
57
|
+
"p4-prepare-changelist",
|
|
58
|
+
"p4-post-changelist",
|
|
59
|
+
"p4-pre-submit",
|
|
60
|
+
"post-index-change",
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
function git(args, cwd) {
|
|
64
|
+
return execFileSync("git", args, {
|
|
65
|
+
cwd,
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
68
|
+
}).trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve symlinks so paths reported by git and paths reported by the caller
|
|
73
|
+
* can be compared and combined reliably (macOS tmpdirs are symlinked).
|
|
74
|
+
*/
|
|
75
|
+
function realpath(dir) {
|
|
76
|
+
try {
|
|
77
|
+
return fs.realpathSync(dir);
|
|
78
|
+
} catch {
|
|
79
|
+
return path.resolve(dir);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function projectEntryName(relativeProjectPath, hook) {
|
|
84
|
+
const hash = crypto
|
|
85
|
+
.createHash("sha256")
|
|
86
|
+
.update(relativeProjectPath)
|
|
87
|
+
.digest("hex")
|
|
88
|
+
.slice(0, 16);
|
|
89
|
+
return `${hash}.${hook}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Absolute path of the repository root, or null when not inside a git repo.
|
|
94
|
+
*/
|
|
95
|
+
export function getGitRoot(projectDir = PROJECT_DIR) {
|
|
96
|
+
try {
|
|
97
|
+
const root = git(["rev-parse", "--show-toplevel"], projectDir);
|
|
98
|
+
return root ? realpath(path.resolve(projectDir, root)) : null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Absolute path of the hooks directory git uses for this project. Respects
|
|
106
|
+
* core.hooksPath and resolves to the shared hooks directory in worktrees.
|
|
107
|
+
*/
|
|
108
|
+
export function getHooksDir(projectDir = PROJECT_DIR) {
|
|
109
|
+
try {
|
|
110
|
+
const hooks = git(["rev-parse", "--git-path", "hooks"], projectDir);
|
|
111
|
+
return hooks ? path.resolve(realpath(projectDir), hooks) : null;
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Reads the simple-git-hooks config from <dir>/package.json, if present.
|
|
119
|
+
*/
|
|
120
|
+
export function readHookConfig(dir) {
|
|
121
|
+
try {
|
|
122
|
+
const packageJson = JSON.parse(
|
|
123
|
+
fs.readFileSync(path.join(dir, "package.json"), "utf8"),
|
|
124
|
+
);
|
|
125
|
+
const config = packageJson["simple-git-hooks"];
|
|
126
|
+
return config && typeof config === "object" && !Array.isArray(config)
|
|
127
|
+
? config
|
|
128
|
+
: null;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Hooks installed by simple-git-hooks or by this script may be replaced.
|
|
136
|
+
* Hooks owned by anything else are left alone.
|
|
137
|
+
*/
|
|
138
|
+
function isManagedHook(contents) {
|
|
139
|
+
return (
|
|
140
|
+
contents.includes(HOOK_MARKER) || contents.includes("SKIP_SIMPLE_GIT_HOOKS")
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Shell shim installed as the actual git hook. It only delegates to the
|
|
146
|
+
* dispatcher; all per-project state lives in quark-scoped-hooks/.
|
|
147
|
+
*/
|
|
148
|
+
export function buildHookShim(hook) {
|
|
149
|
+
return `#!/bin/sh
|
|
150
|
+
# ${HOOK_MARKER} - generated by scripts/prepare.js. Do not edit.
|
|
151
|
+
# Runs the "${hook}" command of every nested Quark project that the current
|
|
152
|
+
# operation affects. See quark-scoped-hooks/dispatch.js.
|
|
153
|
+
if [ "$SKIP_SIMPLE_GIT_HOOKS" = "1" ] || [ "$SKIP_QUARK_GIT_HOOKS" = "1" ]; then
|
|
154
|
+
exit 0
|
|
155
|
+
fi
|
|
156
|
+
if [ -f "$SIMPLE_GIT_HOOKS_RC" ]; then
|
|
157
|
+
. "$SIMPLE_GIT_HOOKS_RC"
|
|
158
|
+
fi
|
|
159
|
+
exec node "$(dirname "$0")/${SCOPED_HOOKS_DIR}/dispatch.js" ${hook}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Node dispatcher installed once per hooks directory. It reads the project
|
|
165
|
+
* entries, works out which projects the current commit or push affects, and
|
|
166
|
+
* runs each project's hook command from that project's directory.
|
|
167
|
+
*/
|
|
168
|
+
const DISPATCHER_SOURCE = `#!/usr/bin/env node
|
|
169
|
+
/**
|
|
170
|
+
* ${HOOK_MARKER} dispatcher.
|
|
171
|
+
* Generated by scripts/prepare.js - do not edit.
|
|
172
|
+
*
|
|
173
|
+
* Git runs hooks from the repository root. This dispatcher finds the nested
|
|
174
|
+
* Quark projects registered in ./${ENTRIES_DIR}, works out which of them the
|
|
175
|
+
* current commit or push affects, and runs each project's hook command from
|
|
176
|
+
* its own directory.
|
|
177
|
+
*/
|
|
178
|
+
import { execFileSync } from "node:child_process";
|
|
179
|
+
import fs from "node:fs";
|
|
180
|
+
import path from "node:path";
|
|
181
|
+
import { fileURLToPath } from "node:url";
|
|
182
|
+
|
|
183
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
184
|
+
const ENTRIES_DIR = path.join(SCRIPT_DIR, "${ENTRIES_DIR}");
|
|
185
|
+
const ZERO_OID = /^0+$/;
|
|
186
|
+
|
|
187
|
+
const hook = process.argv[2];
|
|
188
|
+
if (!hook) process.exit(0);
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
process.env.SKIP_SIMPLE_GIT_HOOKS === "1" ||
|
|
192
|
+
process.env.SKIP_QUARK_GIT_HOOKS === "1"
|
|
193
|
+
) {
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function git(args) {
|
|
198
|
+
return execFileSync("git", args, {
|
|
199
|
+
encoding: "utf8",
|
|
200
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
201
|
+
}).trim();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function readEntries() {
|
|
205
|
+
let names;
|
|
206
|
+
try {
|
|
207
|
+
names = fs.readdirSync(ENTRIES_DIR);
|
|
208
|
+
} catch {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
const entries = [];
|
|
212
|
+
for (const name of names) {
|
|
213
|
+
if (!name.endsWith("." + hook)) continue;
|
|
214
|
+
try {
|
|
215
|
+
const entry = JSON.parse(
|
|
216
|
+
fs.readFileSync(path.join(ENTRIES_DIR, name), "utf8"),
|
|
217
|
+
);
|
|
218
|
+
if (
|
|
219
|
+
entry &&
|
|
220
|
+
typeof entry.project === "string" &&
|
|
221
|
+
typeof entry.command === "string"
|
|
222
|
+
) {
|
|
223
|
+
entries.push(entry);
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// Ignore malformed entries.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return entries;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function filesUnderProject(files, project) {
|
|
233
|
+
return files.some(
|
|
234
|
+
(file) => file === project || file.startsWith(project + "/"),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function stagedFiles() {
|
|
239
|
+
try {
|
|
240
|
+
return git(["diff", "--cached", "--name-only", "-z"])
|
|
241
|
+
.split("\\0")
|
|
242
|
+
.filter(Boolean);
|
|
243
|
+
} catch {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function prePushProjects(entries) {
|
|
249
|
+
let input = "";
|
|
250
|
+
try {
|
|
251
|
+
input = fs.readFileSync(0, "utf8");
|
|
252
|
+
} catch {
|
|
253
|
+
input = "";
|
|
254
|
+
}
|
|
255
|
+
const relevant = new Set();
|
|
256
|
+
for (const line of input.split("\\n")) {
|
|
257
|
+
const parts = line.trim().split(/\\s+/);
|
|
258
|
+
if (parts.length < 4) continue;
|
|
259
|
+
const localOid = parts[1];
|
|
260
|
+
const remoteOid = parts[3];
|
|
261
|
+
if (!localOid || ZERO_OID.test(localOid)) continue; // deleted ref
|
|
262
|
+
if (!remoteOid || ZERO_OID.test(remoteOid)) {
|
|
263
|
+
// New branch: there is nothing to diff against, run every project.
|
|
264
|
+
for (const entry of entries) relevant.add(entry.project);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
let changed;
|
|
268
|
+
try {
|
|
269
|
+
changed = git(["diff", "--name-only", "-z", remoteOid + ".." + localOid])
|
|
270
|
+
.split("\\0")
|
|
271
|
+
.filter(Boolean);
|
|
272
|
+
} catch {
|
|
273
|
+
// Unknown remote commit: run every project to stay safe.
|
|
274
|
+
for (const entry of entries) relevant.add(entry.project);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
for (const entry of entries) {
|
|
278
|
+
if (filesUnderProject(changed, entry.project)) {
|
|
279
|
+
relevant.add(entry.project);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return relevant;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function relevantProjects(entries) {
|
|
287
|
+
if (hook === "pre-commit") {
|
|
288
|
+
const staged = stagedFiles();
|
|
289
|
+
return new Set(
|
|
290
|
+
entries
|
|
291
|
+
.filter((entry) => filesUnderProject(staged, entry.project))
|
|
292
|
+
.map((entry) => entry.project),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (hook === "pre-push") {
|
|
296
|
+
return prePushProjects(entries);
|
|
297
|
+
}
|
|
298
|
+
// Other hooks run for every registered project that is present.
|
|
299
|
+
return new Set(entries.map((entry) => entry.project));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const entries = readEntries();
|
|
303
|
+
if (entries.length === 0) process.exit(0);
|
|
304
|
+
|
|
305
|
+
let root;
|
|
306
|
+
try {
|
|
307
|
+
root = git(["rev-parse", "--show-toplevel"]);
|
|
308
|
+
} catch {
|
|
309
|
+
process.exit(0);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const relevant = relevantProjects(entries);
|
|
313
|
+
let failed = false;
|
|
314
|
+
|
|
315
|
+
for (const entry of entries) {
|
|
316
|
+
if (!relevant.has(entry.project)) continue;
|
|
317
|
+
const projectDir = path.join(root, entry.project);
|
|
318
|
+
if (!fs.existsSync(path.join(projectDir, "package.json"))) continue;
|
|
319
|
+
try {
|
|
320
|
+
execFileSync("sh", ["-c", entry.command], {
|
|
321
|
+
cwd: projectDir,
|
|
322
|
+
stdio: "inherit",
|
|
323
|
+
});
|
|
324
|
+
} catch {
|
|
325
|
+
failed = true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
process.exit(failed ? 1 : 0);
|
|
330
|
+
`;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Registers this project for every hook defined in its simple-git-hooks
|
|
334
|
+
* config, then installs the shared dispatcher and hook shims. Entries from
|
|
335
|
+
* other nested projects are left untouched. Returns which hooks were
|
|
336
|
+
* installed or skipped.
|
|
337
|
+
*/
|
|
338
|
+
export function installScopedHooks({
|
|
339
|
+
projectDir = PROJECT_DIR,
|
|
340
|
+
gitRoot = getGitRoot(projectDir),
|
|
341
|
+
hooksDir = getHooksDir(projectDir),
|
|
342
|
+
logger = console,
|
|
343
|
+
} = {}) {
|
|
344
|
+
if (!gitRoot || !hooksDir) {
|
|
345
|
+
return { installed: [], skipped: [], reason: "no-git-repo" };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (realpath(gitRoot) === realpath(projectDir)) {
|
|
349
|
+
// The project is the repository root - simple-git-hooks handles it.
|
|
350
|
+
return { installed: [], skipped: [], reason: "project-is-git-root" };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const config = readHookConfig(projectDir);
|
|
354
|
+
if (!config) {
|
|
355
|
+
return { installed: [], skipped: [], reason: "no-config" };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const relativeProjectPath = path
|
|
359
|
+
.relative(realpath(gitRoot), realpath(projectDir))
|
|
360
|
+
.split(path.sep)
|
|
361
|
+
.join("/");
|
|
362
|
+
|
|
363
|
+
const scopedHooksDir = path.join(hooksDir, SCOPED_HOOKS_DIR);
|
|
364
|
+
const entriesDir = path.join(scopedHooksDir, ENTRIES_DIR);
|
|
365
|
+
|
|
366
|
+
const configuredHooks = new Set(
|
|
367
|
+
Object.entries(config)
|
|
368
|
+
.filter(
|
|
369
|
+
([hook, command]) =>
|
|
370
|
+
VALID_HOOKS.has(hook) && typeof command === "string",
|
|
371
|
+
)
|
|
372
|
+
.map(([hook]) => hook),
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const installed = [];
|
|
376
|
+
const skipped = [];
|
|
377
|
+
|
|
378
|
+
// The dispatcher is shared by every project and every hook.
|
|
379
|
+
fs.mkdirSync(entriesDir, { recursive: true });
|
|
380
|
+
const dispatcherPath = path.join(scopedHooksDir, "dispatch.js");
|
|
381
|
+
let dispatcher = "";
|
|
382
|
+
try {
|
|
383
|
+
dispatcher = fs.readFileSync(dispatcherPath, "utf8");
|
|
384
|
+
} catch {
|
|
385
|
+
// Not installed yet.
|
|
386
|
+
}
|
|
387
|
+
if (dispatcher !== DISPATCHER_SOURCE) {
|
|
388
|
+
fs.writeFileSync(dispatcherPath, DISPATCHER_SOURCE);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for (const hook of configuredHooks) {
|
|
392
|
+
const hookPath = path.join(hooksDir, hook);
|
|
393
|
+
let existing = "";
|
|
394
|
+
try {
|
|
395
|
+
existing = fs.readFileSync(hookPath, "utf8");
|
|
396
|
+
} catch {
|
|
397
|
+
// No existing hook - safe to create.
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (existing && !isManagedHook(existing)) {
|
|
401
|
+
skipped.push(hook);
|
|
402
|
+
logger.warn(
|
|
403
|
+
`[quark] Skipping ${hook}: an existing hook not managed by Quark is present in ${hooksDir}`,
|
|
404
|
+
);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const entryPath = path.join(
|
|
409
|
+
entriesDir,
|
|
410
|
+
projectEntryName(relativeProjectPath, hook),
|
|
411
|
+
);
|
|
412
|
+
fs.writeFileSync(
|
|
413
|
+
entryPath,
|
|
414
|
+
`${JSON.stringify(
|
|
415
|
+
{ project: relativeProjectPath, command: config[hook] },
|
|
416
|
+
null,
|
|
417
|
+
"\t",
|
|
418
|
+
)}\n`,
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const shim = buildHookShim(hook);
|
|
422
|
+
if (existing !== shim) {
|
|
423
|
+
fs.writeFileSync(hookPath, shim);
|
|
424
|
+
}
|
|
425
|
+
fs.chmodSync(hookPath, 0o755);
|
|
426
|
+
installed.push(hook);
|
|
427
|
+
logger.log(
|
|
428
|
+
`[quark] Registered scoped git hook "${hook}" for ${relativeProjectPath}/`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Drop entries for hooks this project no longer configures, without
|
|
433
|
+
// touching entries that belong to other projects.
|
|
434
|
+
for (const name of fs.readdirSync(entriesDir)) {
|
|
435
|
+
const separator = name.lastIndexOf(".");
|
|
436
|
+
if (separator === -1) continue;
|
|
437
|
+
const hook = name.slice(separator + 1);
|
|
438
|
+
if (configuredHooks.has(hook)) continue;
|
|
439
|
+
|
|
440
|
+
const entryPath = path.join(entriesDir, name);
|
|
441
|
+
try {
|
|
442
|
+
const entry = JSON.parse(fs.readFileSync(entryPath, "utf8"));
|
|
443
|
+
if (entry.project === relativeProjectPath) {
|
|
444
|
+
fs.unlinkSync(entryPath);
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
// Malformed entry - leave it alone.
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return { installed, skipped, reason: null };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function main({ projectDir = PROJECT_DIR, logger = console } = {}) {
|
|
455
|
+
const gitRoot = getGitRoot(projectDir);
|
|
456
|
+
if (!gitRoot) {
|
|
457
|
+
// Not inside a git repo (e.g. npx install, CI without checkout) - skip.
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (gitRoot === realpath(projectDir)) {
|
|
462
|
+
try {
|
|
463
|
+
execSync("npx simple-git-hooks", { stdio: "inherit" });
|
|
464
|
+
} catch {
|
|
465
|
+
// Hooks setup failed - non-fatal, do not block installs.
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// A project nested in a larger repository must not take over hooks that
|
|
471
|
+
// repository already manages for its own root project.
|
|
472
|
+
if (readHookConfig(gitRoot)) {
|
|
473
|
+
logger.warn(
|
|
474
|
+
`[quark] The repository at ${gitRoot} manages its own git hooks - skipping hook setup for this nested project.`,
|
|
475
|
+
);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
installScopedHooks({ projectDir, gitRoot, logger });
|
|
481
|
+
} catch (error) {
|
|
482
|
+
logger.warn(
|
|
483
|
+
`[quark] Could not install scoped git hooks: ${error.message}`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === SCRIPT_PATH) {
|
|
489
|
+
main();
|
|
490
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const roots = [];
|
|
7
|
+
const excludes = [];
|
|
8
|
+
|
|
9
|
+
for (const arg of args) {
|
|
10
|
+
if (arg.startsWith("--exclude=")) {
|
|
11
|
+
excludes.push(arg.slice("--exclude=".length));
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
roots.push(arg);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (roots.length === 0) {
|
|
19
|
+
roots.push("src");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function collectTests(targetPath) {
|
|
23
|
+
const stats = [];
|
|
24
|
+
|
|
25
|
+
function walk(currentPath) {
|
|
26
|
+
for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
|
|
27
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
28
|
+
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
walk(fullPath);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!entry.isFile() || !entry.name.endsWith(".test.js")) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (excludes.some((exclude) => fullPath.endsWith(exclude))) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
stats.push(fullPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
walk(targetPath);
|
|
47
|
+
return stats;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const files = roots.flatMap((rootPath) => collectTests(path.resolve(rootPath)));
|
|
51
|
+
|
|
52
|
+
if (files.length === 0) {
|
|
53
|
+
console.error(`No test files found in: ${roots.join(", ")}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result = spawnSync(
|
|
58
|
+
process.execPath,
|
|
59
|
+
[
|
|
60
|
+
"--import",
|
|
61
|
+
"tsx/esm",
|
|
62
|
+
"--experimental-test-module-mocks",
|
|
63
|
+
"--test",
|
|
64
|
+
...files,
|
|
65
|
+
],
|
|
66
|
+
{
|
|
67
|
+
stdio: "inherit",
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-dashboard
|
|
3
|
+
description: Add a decision dashboard to the admin on Quark. Use when the user wants a dashboard, metrics, or admin overview page.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add Dashboard Skill
|
|
7
|
+
|
|
8
|
+
Add a decision dashboard in admin: metric cards showing decision-relevant values for a domain, backed by query helpers and a Server Action.
|
|
9
|
+
|
|
10
|
+
## Framework context (build on Quark)
|
|
11
|
+
|
|
12
|
+
- **Admin** is an operations shell — show decision-relevant values (counts, totals, statuses), not raw CRUD tables.
|
|
13
|
+
- **Server Actions** are `"use server"`, validate with Zod, and use `createLogger()`.
|
|
14
|
+
- **Data access** goes through query helpers in `packages/db/src/queries.js` — never call `prisma.*` directly.
|
|
15
|
+
|
|
16
|
+
## Workflow
|
|
17
|
+
|
|
18
|
+
1. Create `apps/web/src/app/admin/<feature>/page.js` with metric cards.
|
|
19
|
+
2. Create `apps/web/src/app/admin/<feature>/_actions/<feature>.js` (Server Actions that load the metrics).
|
|
20
|
+
3. Add a sibling `loading.js` using `<Skeleton>`.
|
|
21
|
+
4. Add `export const metadata` (title + description).
|
|
22
|
+
|
|
23
|
+
## Patterns to follow
|
|
24
|
+
|
|
25
|
+
- Metric cards show decision-relevant values (counts, totals, statuses), not raw tables.
|
|
26
|
+
- Server Actions are `"use server"`, validate with Zod, and use `createLogger()`.
|
|
27
|
+
- Every async page that fetches from the database needs a sibling `loading.js` using `<Skeleton>`.
|
|
28
|
+
- Add `export const metadata` (title + description) to the page.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-endpoint
|
|
3
|
+
description: Add a REST CRUD endpoint on Quark. Use when the user wants an API route, endpoint, or REST resource.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add Endpoint Skill
|
|
7
|
+
|
|
8
|
+
Add a REST CRUD endpoint for an existing model: list/create on the collection route, and read/update/delete on the `[id]` route.
|
|
9
|
+
|
|
10
|
+
## Framework context (build on Quark)
|
|
11
|
+
|
|
12
|
+
- **API routes** live in `apps/web/src/app/api/<resource>/`.
|
|
13
|
+
- **Data access** goes through query helpers in `packages/db/src/queries.js` — never call `prisma.*` directly.
|
|
14
|
+
- **Auth** via `requireRole` from `@/lib/auth-middleware`; **validation** via Zod `validateBody`; **CSRF** via `withCsrfProtection` from `@techstream/quark-core`.
|
|
15
|
+
|
|
16
|
+
## Workflow
|
|
17
|
+
|
|
18
|
+
1. Create `apps/web/src/app/api/<resource>/route.js` (GET list + POST create).
|
|
19
|
+
2. Create `apps/web/src/app/api/<resource>/[id]/route.js` (GET read + PATCH update + DELETE).
|
|
20
|
+
3. Add a test near the changed code.
|
|
21
|
+
|
|
22
|
+
## Patterns to follow
|
|
23
|
+
|
|
24
|
+
- Use `NextResponse` from `next/server` and `handleError` from `../error-handler`.
|
|
25
|
+
- Guard with `requireRole("admin")` (or the appropriate role).
|
|
26
|
+
- Validate the body with Zod via `validateBody(request, schema)`.
|
|
27
|
+
- Wrap mutations in `withCsrfProtection`.
|
|
28
|
+
- Return `404` when a record is not found; `201` on create.
|
|
29
|
+
- Follow `apps/web/src/app/api/users/route.js` and `apps/web/src/app/api/users/[id]/route.js` as reference.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-model
|
|
3
|
+
description: Add a new Prisma model and query helpers on Quark. Use when the user wants to add a data model, entity, or table.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add Model Skill
|
|
7
|
+
|
|
8
|
+
Add a new Prisma model plus the query helpers to read and write it. This is the foundation for any feature that stores data.
|
|
9
|
+
|
|
10
|
+
## Framework context (build on Quark)
|
|
11
|
+
|
|
12
|
+
- **Models** live in `packages/db/prisma/schema.prisma`.
|
|
13
|
+
- **Data access** goes through query helpers in `packages/db/src/queries.js` — never call `prisma.*` directly in pages/actions.
|
|
14
|
+
- After any schema change: `pnpm db:generate` then `pnpm db:migrate`.
|
|
15
|
+
|
|
16
|
+
## Workflow
|
|
17
|
+
|
|
18
|
+
1. Add the model to `packages/db/prisma/schema.prisma`.
|
|
19
|
+
2. Add query helpers to `packages/db/src/queries.js` — start with `findAll`, `findById`, `create`; add `update`/`delete` when needed.
|
|
20
|
+
3. Run `pnpm db:generate` then `pnpm db:migrate`.
|
|
21
|
+
4. Add a test near the changed code.
|
|
22
|
+
|
|
23
|
+
## Patterns to follow
|
|
24
|
+
|
|
25
|
+
- Every model includes `id String @id @default(cuid())`, `createdAt DateTime @default(now())`, and `updatedAt DateTime @updatedAt`.
|
|
26
|
+
- If the data belongs to a signed-in user, relate it to `User` with `onDelete: Cascade` (or `SetNull` when optional).
|
|
27
|
+
- Add indexes on fields you query by (`@@index([...])`).
|
|
28
|
+
- Do not call `prisma.*` directly in pages or Server Actions — use the query helpers.
|