@techstream/quark-create-app 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +3 -3
- package/src/index.js +415 -150
- package/src/utils.js +36 -0
- package/src/utils.test.js +63 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.github/copilot-instructions.md +55 -0
- package/templates/base-project/.github/workflows/release.yml +37 -8
- package/templates/base-project/CLAUDE.md +273 -0
- package/templates/base-project/README.md +72 -30
- package/templates/base-project/apps/web/next.config.js +5 -1
- package/templates/base-project/apps/web/package.json +7 -5
- package/templates/base-project/apps/web/public/quark.svg +46 -0
- package/templates/base-project/apps/web/railway.json +2 -2
- 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/api/health/route.js +56 -17
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +53 -0
- package/templates/base-project/apps/web/src/app/globals.css +121 -15
- package/templates/base-project/apps/web/src/app/icon.svg +46 -0
- package/templates/base-project/apps/web/src/app/layout.js +1 -0
- package/templates/base-project/apps/web/src/app/not-found.js +35 -0
- package/templates/base-project/apps/web/src/app/page.js +38 -5
- package/templates/base-project/apps/web/src/lib/theme.js +23 -0
- package/templates/base-project/apps/web/src/proxy.js +10 -2
- package/templates/base-project/package.json +16 -1
- package/templates/base-project/packages/db/package.json +4 -4
- package/templates/base-project/packages/db/src/client.js +6 -1
- package/templates/base-project/packages/db/src/index.js +1 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/scripts/doctor.js +261 -0
- package/templates/base-project/turbo.json +2 -1
- package/templates/config/package.json +1 -0
- package/templates/config/src/index.js +1 -3
- package/templates/config/src/validate-env.js +79 -3
- package/templates/jobs/package.json +2 -1
- package/templates/ui/README.md +67 -0
- package/templates/ui/package.json +1 -0
- package/templates/ui/src/badge.js +32 -0
- package/templates/ui/src/badge.test.js +42 -0
- package/templates/ui/src/button.js +64 -15
- package/templates/ui/src/button.test.js +34 -5
- package/templates/ui/src/card.js +58 -0
- package/templates/ui/src/card.test.js +59 -0
- package/templates/ui/src/checkbox.js +35 -0
- package/templates/ui/src/checkbox.test.js +35 -0
- package/templates/ui/src/dialog.js +139 -0
- package/templates/ui/src/dialog.test.js +15 -0
- package/templates/ui/src/index.js +16 -0
- package/templates/ui/src/input.js +15 -0
- package/templates/ui/src/input.test.js +27 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/label.test.js +22 -0
- package/templates/ui/src/select.js +42 -0
- package/templates/ui/src/select.test.js +27 -0
- package/templates/ui/src/skeleton.js +14 -0
- package/templates/ui/src/skeleton.test.js +22 -0
- package/templates/ui/src/table.js +75 -0
- package/templates/ui/src/table.test.js +69 -0
- package/templates/ui/src/textarea.js +15 -0
- package/templates/ui/src/textarea.test.js +27 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +132 -0
- package/templates/ui/src/toast.js +229 -0
- package/templates/ui/src/toast.test.js +23 -0
- package/templates/{base-project/apps/worker → worker}/package.json +2 -2
- package/templates/{base-project/apps/worker → worker}/src/index.js +38 -23
- package/templates/{base-project/apps/worker → worker}/src/index.test.js +19 -20
- package/templates/base-project/apps/web/public/file.svg +0 -1
- package/templates/base-project/apps/web/public/globe.svg +0 -1
- package/templates/base-project/apps/web/public/next.svg +0 -1
- package/templates/base-project/apps/web/public/vercel.svg +0 -1
- package/templates/base-project/apps/web/public/window.svg +0 -1
- /package/templates/{base-project/apps/worker → worker}/README.md +0 -0
- /package/templates/{base-project/apps/worker → worker}/railway.json +0 -0
- /package/templates/{base-project/apps/worker → worker}/src/handlers/email.js +0 -0
- /package/templates/{base-project/apps/worker → worker}/src/handlers/files.js +0 -0
- /package/templates/{base-project/apps/worker → worker}/src/handlers/index.js +0 -0
package/src/utils.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for @techstream/quark-create-app
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format a project slug into a human-friendly application name.
|
|
7
|
+
*
|
|
8
|
+
* - Hyphens, underscores, and dots are treated as word separators
|
|
9
|
+
* - Each word is title-cased
|
|
10
|
+
* - Degenerate inputs (all separators) fall back to "Quark App"
|
|
11
|
+
*
|
|
12
|
+
* @param {string} projectName - Raw project slug (e.g. "my-cool-app")
|
|
13
|
+
* @returns {string} Title-cased display name (e.g. "My Cool App")
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* formatProjectDisplayName("my-cool-app") // "My Cool App"
|
|
17
|
+
* formatProjectDisplayName("my.app") // "My App"
|
|
18
|
+
* formatProjectDisplayName("my_app_v2") // "My App V2"
|
|
19
|
+
* formatProjectDisplayName("myapp") // "Myapp"
|
|
20
|
+
* formatProjectDisplayName("---") // "Quark App"
|
|
21
|
+
*/
|
|
22
|
+
export function formatProjectDisplayName(projectName) {
|
|
23
|
+
const normalized = projectName
|
|
24
|
+
.replace(/[-_.]+/g, " ")
|
|
25
|
+
.replace(/\s+/g, " ")
|
|
26
|
+
.trim();
|
|
27
|
+
|
|
28
|
+
if (!normalized) {
|
|
29
|
+
return "Quark App";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return normalized
|
|
33
|
+
.split(" ")
|
|
34
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
35
|
+
.join(" ");
|
|
36
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { formatProjectDisplayName } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Happy path — common slug patterns
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
test("formatProjectDisplayName - hyphen-separated slug", () => {
|
|
10
|
+
assert.strictEqual(formatProjectDisplayName("my-cool-app"), "My Cool App");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("formatProjectDisplayName - underscore-separated slug", () => {
|
|
14
|
+
assert.strictEqual(formatProjectDisplayName("my_app_v2"), "My App V2");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("formatProjectDisplayName - dot-separated slug", () => {
|
|
18
|
+
assert.strictEqual(formatProjectDisplayName("my.app"), "My App");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("formatProjectDisplayName - single word (no separators)", () => {
|
|
22
|
+
assert.strictEqual(formatProjectDisplayName("myapp"), "Myapp");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("formatProjectDisplayName - already title-cased input", () => {
|
|
26
|
+
// slice(1) preserves remaining chars as-is — only the first char is forced uppercase
|
|
27
|
+
assert.strictEqual(formatProjectDisplayName("MyApp"), "MyApp");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("formatProjectDisplayName - mixed separators", () => {
|
|
31
|
+
assert.strictEqual(formatProjectDisplayName("my-app_v2.0"), "My App V2 0");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("formatProjectDisplayName - consecutive separators collapse to single space", () => {
|
|
35
|
+
assert.strictEqual(formatProjectDisplayName("my--app"), "My App");
|
|
36
|
+
assert.strictEqual(formatProjectDisplayName("my___app"), "My App");
|
|
37
|
+
assert.strictEqual(formatProjectDisplayName("my-._app"), "My App");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("formatProjectDisplayName - numbers preserved in words", () => {
|
|
41
|
+
assert.strictEqual(formatProjectDisplayName("app-v2"), "App V2");
|
|
42
|
+
assert.strictEqual(formatProjectDisplayName("project-123"), "Project 123");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Edge cases — degenerate / boundary inputs
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
test("formatProjectDisplayName - all separators returns fallback", () => {
|
|
50
|
+
assert.strictEqual(formatProjectDisplayName("---"), "Quark App");
|
|
51
|
+
assert.strictEqual(formatProjectDisplayName("___"), "Quark App");
|
|
52
|
+
assert.strictEqual(formatProjectDisplayName("..."), "Quark App");
|
|
53
|
+
assert.strictEqual(formatProjectDisplayName("-._-"), "Quark App");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("formatProjectDisplayName - preserves casing of rest of word", () => {
|
|
57
|
+
// The function only forces the first char uppercase; the remainder is untouched
|
|
58
|
+
assert.strictEqual(formatProjectDisplayName("myApp"), "MyApp");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("formatProjectDisplayName - short single-char name", () => {
|
|
62
|
+
assert.strictEqual(formatProjectDisplayName("a"), "A");
|
|
63
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Quark project conventions for __QUARK_PROJECT_NAME__. Covers all layers: UI, backend, database, auth, jobs, testing, and deployment.
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Quark Project: __QUARK_PROJECT_NAME__
|
|
7
|
+
|
|
8
|
+
Package scope: `@__QUARK_SCOPE__` — use this for ALL workspace packages.
|
|
9
|
+
Scaffolded: __QUARK_SCAFFOLD_DATE__
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Non-Negotiable Rules
|
|
14
|
+
|
|
15
|
+
1. **ESM only.** Always `import`/`export`. Never `require()` or `module.exports`.
|
|
16
|
+
2. **No TypeScript.** Plain `.js` and `.jsx` files only — no `.ts`, `.tsx`, or type annotations.
|
|
17
|
+
3. **Validate everything at boundaries.** Server Actions and API routes must use Zod. No raw `req.body` access without parsing.
|
|
18
|
+
4. **Use AppError — not Error.** Throw `AppError` / `ValidationError` / `NotFoundError` from `@techstream/quark-core/errors`. Never `throw new Error()` in application code.
|
|
19
|
+
5. **Log — never console.** Use `createLogger(name)` from `@techstream/quark-core`. Zero `console.log` / `console.error` in non-test code.
|
|
20
|
+
6. **DB models always have timestamps.** Every Prisma model: `createdAt DateTime @default(now())` + `updatedAt DateTime @updatedAt`.
|
|
21
|
+
7. **Workspace import scope.** Use `@__QUARK_SCOPE__/*` for local packages. Never `@techstream/quark-db`, `@techstream/quark-config`, etc.
|
|
22
|
+
8. **Co-located tests.** `feature.test.js` lives next to `feature.js`. Tests use `node --test` — no Jest, no Vitest.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Import Reference
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
// ✅ Workspace packages — always @__QUARK_SCOPE__/*:
|
|
30
|
+
import { prisma, user, post } from "@__QUARK_SCOPE__/db";
|
|
31
|
+
import { loadConfig } from "@__QUARK_SCOPE__/config";
|
|
32
|
+
import { Button, Card, Input } from "@__QUARK_SCOPE__/ui";
|
|
33
|
+
import { JOB_TYPES } from "@__QUARK_SCOPE__/jobs";
|
|
34
|
+
|
|
35
|
+
// ✅ Published runtime — @techstream/quark-core:
|
|
36
|
+
import { AppError, ValidationError, NotFoundError } from "@techstream/quark-core/errors";
|
|
37
|
+
import { createLogger, getCurrentSession } from "@techstream/quark-core";
|
|
38
|
+
import { createQueue, addJob } from "@techstream/quark-core";
|
|
39
|
+
import { metrics, cache, storage } from "@techstream/quark-core";
|
|
40
|
+
|
|
41
|
+
// ❌ Never:
|
|
42
|
+
import { Button } from "@/components/ui/button"; // wrong path convention
|
|
43
|
+
import { prisma } from "@techstream/quark-db"; // wrong scope for workspace pkg
|
|
44
|
+
import something from "@techstream/quark-config"; // wrong scope for workspace pkg
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## UI & Design System
|
|
50
|
+
|
|
51
|
+
- Styling: **Tailwind CSS utility classes only**. No inline styles, no CSS modules.
|
|
52
|
+
- Components from `@__QUARK_SCOPE__/ui`. All accept `className` for overrides.
|
|
53
|
+
- Available: `Button`, `Input`, `Label`, `Textarea`, `Select`, `Checkbox`, `Badge`, `Card`/`CardHeader`/`CardTitle`/`CardContent`/`CardFooter`, `Table`/`TableHeader`/`TableBody`/`TableRow`/`TableHead`/`TableCell`, `Skeleton`, `Dialog` *(client)*, `Toast`/`useToast` *(client)*.
|
|
54
|
+
- Loading states → `<Skeleton>` + `<Suspense>` boundaries.
|
|
55
|
+
- User feedback → `useToast()` (mark component `"use client"`).
|
|
56
|
+
- Modals → `<Dialog>` (mark parent `"use client"`).
|
|
57
|
+
- Default to **Server Components**. Only add `"use client"` when necessary.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Server Action Pattern
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
"use server";
|
|
65
|
+
import { z } from "zod";
|
|
66
|
+
import { ValidationError, AppError } from "@techstream/quark-core/errors";
|
|
67
|
+
import { getCurrentSession, createLogger } from "@techstream/quark-core";
|
|
68
|
+
import { prisma } from "@__QUARK_SCOPE__/db";
|
|
69
|
+
|
|
70
|
+
const log = createLogger("action:example");
|
|
71
|
+
const schema = z.object({ title: z.string().min(1).max(255) });
|
|
72
|
+
|
|
73
|
+
export async function createExample(formData) {
|
|
74
|
+
const session = await getCurrentSession();
|
|
75
|
+
if (!session) throw new AppError("Unauthorized", 401);
|
|
76
|
+
|
|
77
|
+
const parsed = schema.safeParse(Object.fromEntries(formData));
|
|
78
|
+
if (!parsed.success) throw new ValidationError(parsed.error.flatten());
|
|
79
|
+
|
|
80
|
+
const result = await prisma.example.create({ data: parsed.data });
|
|
81
|
+
log.info("created", { id: result.id });
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## API Route Pattern
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
import { NextResponse } from "next/server";
|
|
92
|
+
import { AppError } from "@techstream/quark-core/errors";
|
|
93
|
+
import { createLogger } from "@techstream/quark-core";
|
|
94
|
+
|
|
95
|
+
const log = createLogger("api:example");
|
|
96
|
+
|
|
97
|
+
export async function GET(request) {
|
|
98
|
+
try {
|
|
99
|
+
return NextResponse.json({ data });
|
|
100
|
+
} catch (error) {
|
|
101
|
+
log.error("request failed", { error });
|
|
102
|
+
if (error instanceof AppError)
|
|
103
|
+
return NextResponse.json({ error: error.message }, { status: error.statusCode });
|
|
104
|
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Database
|
|
112
|
+
|
|
113
|
+
- Schema: `packages/db/prisma/schema.prisma`
|
|
114
|
+
- Query helpers: `packages/db/src/queries.js` — add functions here, don't call `prisma.*` directly in components or actions.
|
|
115
|
+
- Always run `pnpm db:generate` after schema changes, then `pnpm db:migrate`.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Auth
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
import { getCurrentSession } from "@techstream/quark-core";
|
|
123
|
+
|
|
124
|
+
const session = await getCurrentSession(); // null if unauthenticated
|
|
125
|
+
if (!session) redirect("/login"); // Server Component
|
|
126
|
+
if (!session) throw new AppError("Unauthorized", 401); // Server Action / API
|
|
127
|
+
if (session.user.role !== "ADMIN") throw new AppError("Forbidden", 403);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Background Jobs
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
// Dispatch (from Server Action or API route):
|
|
136
|
+
import { createQueue, addJob } from "@techstream/quark-core";
|
|
137
|
+
import { JOB_TYPES } from "@__QUARK_SCOPE__/jobs";
|
|
138
|
+
const queue = createQueue("default");
|
|
139
|
+
await addJob(queue, JOB_TYPES.MY_JOB, { userId });
|
|
140
|
+
|
|
141
|
+
// Handle (apps/worker/src/handlers/):
|
|
142
|
+
import { createLogger } from "@techstream/quark-core";
|
|
143
|
+
const log = createLogger("job:my-job");
|
|
144
|
+
export async function handleMyJob({ data }) { log.info("processing", data); }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Testing
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
import { test } from "node:test";
|
|
153
|
+
import assert from "node:assert";
|
|
154
|
+
|
|
155
|
+
test("feature", async (t) => {
|
|
156
|
+
await t.test("does X", async () => {
|
|
157
|
+
assert.strictEqual(actual, expected);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Environment
|
|
165
|
+
|
|
166
|
+
New env vars must be registered in `packages/config/src/validate-env.js` before use. Load config via `loadConfig()` from `@__QUARK_SCOPE__/config`.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Deployment
|
|
171
|
+
|
|
172
|
+
Railway: two services (`web` + `worker`). Migrations auto-run on deploy via `releaseCommand` in `apps/web/railway.json`. Env vars managed in Railway dashboard — never committed.
|
|
@@ -5,3 +5,58 @@
|
|
|
5
5
|
<file>.github/skills/project-context/SKILL.md</file>
|
|
6
6
|
</skill>
|
|
7
7
|
</skills>
|
|
8
|
+
|
|
9
|
+
# __QUARK_PROJECT_NAME__ — Project Conventions
|
|
10
|
+
|
|
11
|
+
> Scaffolded with Quark on __QUARK_SCAFFOLD_DATE__. Package scope: `@__QUARK_SCOPE__`
|
|
12
|
+
> Also see `CLAUDE.md` at the project root for the full context reference.
|
|
13
|
+
|
|
14
|
+
## Non-Negotiable Rules
|
|
15
|
+
|
|
16
|
+
- **ESM only** — `import`/`export` everywhere. Never `require()`.
|
|
17
|
+
- **No TypeScript** — `.js` and `.jsx` only.
|
|
18
|
+
- **Validate at every boundary** — Zod schemas on all Server Actions and API routes.
|
|
19
|
+
- **Errors** — `AppError` / `ValidationError` from `@techstream/quark-core/errors`. Never `throw new Error()`.
|
|
20
|
+
- **Logging** — `createLogger(name)` from `@techstream/quark-core`. No `console.log` in production code.
|
|
21
|
+
- **DB models** — Always add `createdAt` and `updatedAt` to every Prisma model.
|
|
22
|
+
- **Workspace imports** — Use `@__QUARK_SCOPE__/*` for local packages (`db`, `config`, `ui`, `jobs`). Never `@techstream/quark-db` etc.
|
|
23
|
+
- **Tests** — Co-located `*.test.js`, run with `node --test`.
|
|
24
|
+
|
|
25
|
+
## Import Patterns
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
// Workspace packages:
|
|
29
|
+
import { prisma, user } from "@__QUARK_SCOPE__/db";
|
|
30
|
+
import { loadConfig } from "@__QUARK_SCOPE__/config";
|
|
31
|
+
import { Button, Card, Input } from "@__QUARK_SCOPE__/ui";
|
|
32
|
+
import { JOB_TYPES } from "@__QUARK_SCOPE__/jobs";
|
|
33
|
+
|
|
34
|
+
// Published runtime:
|
|
35
|
+
import { AppError, ValidationError } from "@techstream/quark-core/errors";
|
|
36
|
+
import { createLogger, getCurrentSession, createQueue, addJob } from "@techstream/quark-core";
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## UI & Design System
|
|
40
|
+
|
|
41
|
+
Components from `@__QUARK_SCOPE__/ui` — Tailwind-only, Server Component safe:
|
|
42
|
+
`Button`, `Input`, `Label`, `Textarea`, `Select`, `Checkbox`, `Badge`,
|
|
43
|
+
`Card`/`CardHeader`/`CardTitle`/`CardContent`/`CardFooter`,
|
|
44
|
+
`Table`/`TableHeader`/`TableBody`/`TableRow`/`TableHead`/`TableCell`,
|
|
45
|
+
`Skeleton`, `Dialog` *(client)*, `Toast`/`useToast` *(client)*.
|
|
46
|
+
|
|
47
|
+
All accept `className`. Never import from `@/components/ui/*`.
|
|
48
|
+
|
|
49
|
+
## Standard Patterns
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
// Server Action:
|
|
53
|
+
"use server";
|
|
54
|
+
const parsed = schema.safeParse(Object.fromEntries(formData));
|
|
55
|
+
if (!parsed.success) throw new ValidationError(parsed.error.flatten());
|
|
56
|
+
const session = await getCurrentSession();
|
|
57
|
+
if (!session) throw new AppError("Unauthorized", 401);
|
|
58
|
+
|
|
59
|
+
// New env var: register in packages/config/src/validate-env.js first.
|
|
60
|
+
// New DB model: schema.prisma → pnpm db:generate → pnpm db:migrate.
|
|
61
|
+
// New job: define type in packages/jobs/src/index.js, handler in apps/worker/src/handlers/.
|
|
62
|
+
```
|
|
@@ -3,9 +3,13 @@ name: Release
|
|
|
3
3
|
on:
|
|
4
4
|
workflow_dispatch:
|
|
5
5
|
inputs:
|
|
6
|
-
|
|
7
|
-
description: "
|
|
6
|
+
prerelease:
|
|
7
|
+
description: "Tag as pre-release (staging-only, skips production deploy)"
|
|
8
8
|
required: false
|
|
9
|
+
default: false
|
|
10
|
+
type: boolean
|
|
11
|
+
|
|
12
|
+
concurrency: ${{ github.workflow }}
|
|
9
13
|
|
|
10
14
|
permissions:
|
|
11
15
|
contents: write
|
|
@@ -17,22 +21,47 @@ jobs:
|
|
|
17
21
|
steps:
|
|
18
22
|
- uses: actions/checkout@v4
|
|
19
23
|
with:
|
|
24
|
+
ref: main
|
|
20
25
|
fetch-depth: 0
|
|
21
26
|
|
|
22
|
-
- name:
|
|
27
|
+
- name: Configure git
|
|
28
|
+
run: |
|
|
29
|
+
git config user.name "github-actions[bot]"
|
|
30
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
31
|
+
|
|
32
|
+
- name: Push to production branch
|
|
33
|
+
if: ${{ !inputs.prerelease }}
|
|
34
|
+
run: |
|
|
35
|
+
git fetch origin
|
|
36
|
+
if git ls-remote --exit-code --heads origin production > /dev/null; then
|
|
37
|
+
git checkout production && git merge --ff-only origin/main
|
|
38
|
+
else
|
|
39
|
+
git checkout -b production origin/main
|
|
40
|
+
fi
|
|
41
|
+
git push origin production
|
|
42
|
+
git checkout main
|
|
43
|
+
|
|
44
|
+
- name: Generate tag
|
|
23
45
|
id: tag
|
|
24
46
|
run: |
|
|
25
47
|
BASE="v$(date +%Y.%m.%d)"
|
|
26
|
-
|
|
48
|
+
SUFFIX="${{ inputs.prerelease && '-rc' || '' }}"
|
|
49
|
+
# Count only exact-format matches to avoid rc tags polluting production counts
|
|
50
|
+
EXISTING=$(git tag -l | grep -cE "^${BASE}${SUFFIX}(\.[0-9]+)?$" || true)
|
|
27
51
|
if [ "$EXISTING" -eq "0" ]; then
|
|
28
|
-
echo "tag=${BASE}" >> "$GITHUB_OUTPUT"
|
|
52
|
+
echo "tag=${BASE}${SUFFIX}" >> "$GITHUB_OUTPUT"
|
|
29
53
|
else
|
|
30
|
-
echo "tag=${BASE}.${EXISTING}" >> "$GITHUB_OUTPUT"
|
|
54
|
+
echo "tag=${BASE}${SUFFIX}.${EXISTING}" >> "$GITHUB_OUTPUT"
|
|
31
55
|
fi
|
|
32
56
|
|
|
57
|
+
- name: Tag release
|
|
58
|
+
run: |
|
|
59
|
+
git tag -a "${{ steps.tag.outputs.tag }}" -m "Release ${{ steps.tag.outputs.tag }}"
|
|
60
|
+
git push origin "${{ steps.tag.outputs.tag }}"
|
|
61
|
+
|
|
33
62
|
- name: Create GitHub Release
|
|
34
63
|
uses: softprops/action-gh-release@v2
|
|
35
64
|
with:
|
|
36
65
|
tag_name: ${{ steps.tag.outputs.tag }}
|
|
37
|
-
generate_release_notes:
|
|
38
|
-
|
|
66
|
+
generate_release_notes: true
|
|
67
|
+
prerelease: ${{ inputs.prerelease }}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# __QUARK_PROJECT_NAME__ — AI Context
|
|
2
|
+
|
|
3
|
+
> Scaffolded with [Quark](https://github.com/Bobnoddle/quark) on __QUARK_SCAFFOLD_DATE__.
|
|
4
|
+
> **Keep this file updated** as your project grows — it's what Claude Code, Cursor, and other AI tools read first.
|
|
5
|
+
|
|
6
|
+
## Quick Start
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
docker compose up -d # Start PostgreSQL, Redis, Mailpit
|
|
10
|
+
pnpm install # Install dependencies
|
|
11
|
+
pnpm db:generate # Generate Prisma client
|
|
12
|
+
pnpm db:migrate # Apply database migrations
|
|
13
|
+
pnpm db:seed # Seed development data
|
|
14
|
+
pnpm dev # Start web + worker
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Scaffolding Another Project (non-interactive)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# With default features (ui + jobs):
|
|
21
|
+
npx @techstream/quark-create-app my-app --no-prompts
|
|
22
|
+
|
|
23
|
+
# With specific features:
|
|
24
|
+
npx @techstream/quark-create-app my-app --no-prompts --features ui,jobs
|
|
25
|
+
npx @techstream/quark-create-app my-app --no-prompts --features ui,jobs,admin
|
|
26
|
+
npx @techstream/quark-create-app my-app --no-prompts --features "" # minimal (db + config only)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Project Structure
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
__QUARK_PROJECT_NAME__/
|
|
33
|
+
├── apps/
|
|
34
|
+
│ ├── web/ # Next.js 16 (App Router, Server Actions)
|
|
35
|
+
│ └── worker/ # BullMQ background worker
|
|
36
|
+
├── packages/
|
|
37
|
+
│ ├── db/ # Prisma schema, client, query functions
|
|
38
|
+
│ ├── config/ # Environment validation & shared config
|
|
39
|
+
__QUARK_OPTIONAL_PACKAGES__├── docker-compose.yml
|
|
40
|
+
├── .env # Local environment secrets (never commit)
|
|
41
|
+
└── .env.example # Template — copy to .env to get started
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Tech Stack
|
|
45
|
+
|
|
46
|
+
| Layer | Technology |
|
|
47
|
+
|---|---|
|
|
48
|
+
| Framework | Next.js 16 (App Router) |
|
|
49
|
+
| Language | JavaScript — ESM only, no TypeScript |
|
|
50
|
+
| Database | PostgreSQL 16 + Prisma 7 |
|
|
51
|
+
| Queue | BullMQ + Redis 7 |
|
|
52
|
+
| Auth | NextAuth v5 |
|
|
53
|
+
| Validation | Zod 4 |
|
|
54
|
+
| UI | Tailwind CSS + `@__QUARK_SCOPE__/ui` components |
|
|
55
|
+
| Email | Nodemailer (Mailpit local / Resend or Zeptomail prod) |
|
|
56
|
+
| Storage | Local filesystem or S3/R2 (pluggable via `STORAGE_PROVIDER`) |
|
|
57
|
+
| Logging | Structured logger via `createLogger()` from `@techstream/quark-core` |
|
|
58
|
+
| Metrics | Prometheus via `metrics` singleton from `@techstream/quark-core` |
|
|
59
|
+
| Testing | Node.js built-in `node --test` |
|
|
60
|
+
| Linting | Biome |
|
|
61
|
+
| Monorepo | Turborepo + pnpm workspaces |
|
|
62
|
+
| Deployment | Railway (web service + worker service) |
|
|
63
|
+
|
|
64
|
+
## Package Imports
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
// Published (installed from npm) — use @techstream/ prefix:
|
|
68
|
+
import { AppError, ValidationError, NotFoundError, UnauthorizedError } from "@techstream/quark-core/errors";
|
|
69
|
+
import { createLogger } from "@techstream/quark-core";
|
|
70
|
+
import { getCurrentSession } from "@techstream/quark-core";
|
|
71
|
+
import { createQueue, addJob } from "@techstream/quark-core";
|
|
72
|
+
import { metrics } from "@techstream/quark-core";
|
|
73
|
+
import { cache } from "@techstream/quark-core";
|
|
74
|
+
import { rateLimit } from "@techstream/quark-core";
|
|
75
|
+
import { storage } from "@techstream/quark-core";
|
|
76
|
+
|
|
77
|
+
// Workspace packages — ALWAYS use @__QUARK_SCOPE__/* (never @techstream/quark-*):
|
|
78
|
+
import { prisma, user } from "@__QUARK_SCOPE__/db"; // add more as you create query helpers
|
|
79
|
+
import { loadConfig } from "@__QUARK_SCOPE__/config";
|
|
80
|
+
import { Button, Card, Input } from "@__QUARK_SCOPE__/ui"; // if ui package selected
|
|
81
|
+
import { JOB_TYPES } from "@__QUARK_SCOPE__/jobs"; // if jobs package selected
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## UI & Design System
|
|
85
|
+
|
|
86
|
+
All UI components come from `@__QUARK_SCOPE__/ui`. They are Tailwind-only, dependency-free, and Server Component-safe.
|
|
87
|
+
|
|
88
|
+
**Available components:**
|
|
89
|
+
- `Button`, `Input`, `Label`, `Textarea`, `Select`, `Checkbox` — form primitives
|
|
90
|
+
- `Badge` — status labels
|
|
91
|
+
- `Card` / `CardHeader` / `CardTitle` / `CardContent` / `CardFooter` — content containers
|
|
92
|
+
- `Table` / `TableHeader` / `TableBody` / `TableRow` / `TableHead` / `TableCell` — data tables
|
|
93
|
+
- `Skeleton` — loading placeholders
|
|
94
|
+
- `Dialog` *(client)* — modal dialogs
|
|
95
|
+
- `Toast` / `useToast` *(client)* — notifications
|
|
96
|
+
|
|
97
|
+
**Rules:**
|
|
98
|
+
- All styling via Tailwind CSS utility classes. No inline styles, no CSS modules.
|
|
99
|
+
- Import from `@__QUARK_SCOPE__/ui` — never from `@/components/ui/*` or direct paths.
|
|
100
|
+
- Every component accepts `className` for Tailwind overrides.
|
|
101
|
+
- Loading states → `<Skeleton>` / `<Suspense>`.
|
|
102
|
+
- User feedback → `useToast()` hook (client component).
|
|
103
|
+
- Modals → `<Dialog>` (mark parent as `"use client"`).
|
|
104
|
+
- Server Components are the default — only add `"use client"` when the component uses hooks, browser APIs, or the marked client components above.
|
|
105
|
+
|
|
106
|
+
## Server Actions (preferred for all mutations)
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
"use server";
|
|
110
|
+
import { z } from "zod";
|
|
111
|
+
import { ValidationError, AppError } from "@techstream/quark-core/errors";
|
|
112
|
+
import { getCurrentSession } from "@techstream/quark-core";
|
|
113
|
+
import { createLogger } from "@techstream/quark-core";
|
|
114
|
+
import { prisma } from "@__QUARK_SCOPE__/db";
|
|
115
|
+
|
|
116
|
+
const log = createLogger("action:example");
|
|
117
|
+
|
|
118
|
+
const schema = z.object({
|
|
119
|
+
title: z.string().min(1).max(255),
|
|
120
|
+
body: z.string().optional(),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export async function createExample(formData) {
|
|
124
|
+
const session = await getCurrentSession();
|
|
125
|
+
if (!session) throw new AppError("Unauthorized", 401);
|
|
126
|
+
|
|
127
|
+
const parsed = schema.safeParse(Object.fromEntries(formData));
|
|
128
|
+
if (!parsed.success) throw new ValidationError(parsed.error.flatten());
|
|
129
|
+
|
|
130
|
+
const result = await prisma.example.create({ data: parsed.data });
|
|
131
|
+
log.info("created", { id: result.id });
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## API Routes
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
// apps/web/src/app/api/example/route.js
|
|
140
|
+
import { NextResponse } from "next/server";
|
|
141
|
+
import { AppError } from "@techstream/quark-core/errors";
|
|
142
|
+
import { createLogger } from "@techstream/quark-core";
|
|
143
|
+
|
|
144
|
+
const log = createLogger("api:example");
|
|
145
|
+
|
|
146
|
+
export async function GET(request) {
|
|
147
|
+
try {
|
|
148
|
+
// ... logic
|
|
149
|
+
return NextResponse.json({ data });
|
|
150
|
+
} catch (error) {
|
|
151
|
+
log.error("request failed", { error });
|
|
152
|
+
if (error instanceof AppError) {
|
|
153
|
+
return NextResponse.json({ error: error.message }, { status: error.statusCode });
|
|
154
|
+
}
|
|
155
|
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Database
|
|
161
|
+
|
|
162
|
+
- Schema: `packages/db/prisma/schema.prisma`
|
|
163
|
+
- Query helpers: `packages/db/src/queries.js`
|
|
164
|
+
- Every model **must** include `createdAt DateTime @default(now())` and `updatedAt DateTime @updatedAt`.
|
|
165
|
+
- After any schema change: `pnpm db:generate` then `pnpm db:migrate`.
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
// Add query functions in packages/db/src/queries.js:
|
|
169
|
+
export const example = {
|
|
170
|
+
findMany: (args) => prisma.example.findMany(args),
|
|
171
|
+
findById: (id) => prisma.example.findUnique({ where: { id } }),
|
|
172
|
+
create: (data) => prisma.example.create({ data }),
|
|
173
|
+
update: (id, data) => prisma.example.update({ where: { id }, data }),
|
|
174
|
+
delete: (id) => prisma.example.delete({ where: { id } }),
|
|
175
|
+
};
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Do not call `prisma.*` directly inside pages or Server Actions — use the query helper functions.
|
|
179
|
+
|
|
180
|
+
## Auth & Authorization
|
|
181
|
+
|
|
182
|
+
```javascript
|
|
183
|
+
import { getCurrentSession } from "@techstream/quark-core";
|
|
184
|
+
|
|
185
|
+
// Server Component — check session:
|
|
186
|
+
const session = await getCurrentSession();
|
|
187
|
+
if (!session) redirect("/login");
|
|
188
|
+
|
|
189
|
+
// Server Action — guard:
|
|
190
|
+
const session = await getCurrentSession();
|
|
191
|
+
if (!session) throw new AppError("Unauthorized", 401);
|
|
192
|
+
|
|
193
|
+
// Role check (ADMIN | VIEWER defined in Prisma schema):
|
|
194
|
+
if (session.user.role !== "ADMIN") throw new AppError("Forbidden", 403);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Background Jobs (if jobs package selected)
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
// 1. Dispatch from web (Server Action or API route):
|
|
201
|
+
import { createQueue, addJob } from "@techstream/quark-core";
|
|
202
|
+
import { JOB_TYPES } from "@__QUARK_SCOPE__/jobs";
|
|
203
|
+
const queue = createQueue("default");
|
|
204
|
+
await addJob(queue, JOB_TYPES.SEND_EMAIL, { userId: session.user.id, template: "welcome" });
|
|
205
|
+
|
|
206
|
+
// 2. Handle in apps/worker/src/handlers/<job-name>.js:
|
|
207
|
+
import { createLogger } from "@techstream/quark-core";
|
|
208
|
+
const log = createLogger("job:send-email");
|
|
209
|
+
export async function handleSendEmail({ data }) {
|
|
210
|
+
log.info("processing", data);
|
|
211
|
+
// ... job logic
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Environment Variables
|
|
216
|
+
|
|
217
|
+
Validated at startup in `packages/config/src/validate-env.js`. **Always add new env vars there before using them.**
|
|
218
|
+
|
|
219
|
+
Config loaded via `loadConfig()` from `@__QUARK_SCOPE__/config`.
|
|
220
|
+
|
|
221
|
+
Key variables:
|
|
222
|
+
| Variable | Purpose |
|
|
223
|
+
|---|---|
|
|
224
|
+
| `DATABASE_URL` / `POSTGRES_*` | PostgreSQL connection |
|
|
225
|
+
| `REDIS_URL` / `REDIS_HOST` + `REDIS_PORT` | Redis connection |
|
|
226
|
+
| `NEXTAUTH_SECRET` | Auth JWT encryption (`openssl rand -base64 32`) |
|
|
227
|
+
| `APP_URL` | Production domain (auto-derived from `PORT` in dev) |
|
|
228
|
+
| `STORAGE_PROVIDER` | `local` (default) or `s3` |
|
|
229
|
+
| `APP_NAME` / `APP_DESCRIPTION` | Used in metadata, emails, page titles |
|
|
230
|
+
|
|
231
|
+
## Testing
|
|
232
|
+
|
|
233
|
+
```javascript
|
|
234
|
+
import { test } from "node:test";
|
|
235
|
+
import assert from "node:assert";
|
|
236
|
+
|
|
237
|
+
test("feature name", async (t) => {
|
|
238
|
+
await t.test("does the thing", async () => {
|
|
239
|
+
// arrange → act → assert
|
|
240
|
+
assert.strictEqual(actual, expected);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
- Tests are co-located: `feature.test.js` lives next to `feature.js`.
|
|
246
|
+
- Run all: `pnpm test` | Run one package: `pnpm --filter @__QUARK_SCOPE__/web test`.
|
|
247
|
+
- Test utilities and model factories: `@techstream/quark-core/testing`.
|
|
248
|
+
|
|
249
|
+
## Deployment
|
|
250
|
+
|
|
251
|
+
Railway with two services: **web** (`apps/web`) and **worker** (`apps/worker`).
|
|
252
|
+
|
|
253
|
+
- Migrations run automatically on every deploy via `releaseCommand` in `apps/web/railway.json`.
|
|
254
|
+
- Environment variables live in Railway dashboard — never in committed files.
|
|
255
|
+
- Staging: auto-deploys on push to `main`. Production: deploy from a version tag.
|
|
256
|
+
- Generate a production seed: `SEED_PROFILE=minimal pnpm db:seed`.
|
|
257
|
+
|
|
258
|
+
## Key Files
|
|
259
|
+
|
|
260
|
+
| File | Purpose |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `packages/db/prisma/schema.prisma` | Database schema — edit this to add models |
|
|
263
|
+
| `packages/db/src/queries.js` | Database query functions — add helpers here |
|
|
264
|
+
| `packages/config/src/validate-env.js` | Environment variable validation — register new vars here |
|
|
265
|
+
| `apps/web/src/app/` | Next.js pages and API routes |
|
|
266
|
+
| `apps/web/src/lib/auth.js` | NextAuth configuration |
|
|
267
|
+
| `apps/worker/src/handlers/` | Background job handler functions |
|
|
268
|
+
| `packages/ui/src/` | UI component source |
|
|
269
|
+
| `.github/skills/project-context/SKILL.md` | VS Code Copilot skill context |
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
*Update this file whenever you make structural changes: new packages, deployment targets, major new conventions, or significant architectural decisions. The AI tools you use daily depend on it being accurate.*
|