@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
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# @techstream/quark-create-app CLI
|
|
2
|
+
|
|
3
|
+
Scaffold a new Quark project with sensible defaults for full-stack JavaScript development.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Interactive — prompts for project name, description, and options
|
|
9
|
+
npx @techstream/quark-create-app@latest
|
|
10
|
+
|
|
11
|
+
# Non-interactive — provide project name as argument
|
|
12
|
+
npx @techstream/quark-create-app@latest my-awesome-app
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The CLI scaffolds a complete project structure with:
|
|
16
|
+
- **Next.js** web application
|
|
17
|
+
- **Prisma** database schema and migrations
|
|
18
|
+
- **BullMQ** job queues
|
|
19
|
+
- **Docker Compose** setup (PostgreSQL, Redis, Mailpit)
|
|
20
|
+
- **JavaScript** monorepo with `pnpm` workspaces
|
|
21
|
+
|
|
22
|
+
## Quick Setup
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd my-awesome-app
|
|
26
|
+
docker compose up -d
|
|
27
|
+
pnpm db:migrate
|
|
28
|
+
pnpm dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Scaffolding into an existing repository
|
|
32
|
+
|
|
33
|
+
The scaffolder creates a new project folder and initialises a git repository inside it. Run it from inside an existing git repository and the project lands one level below that repository's root — where pnpm workspaces, turbo and GitHub Actions cannot see `package.json`, breaking installs and CI.
|
|
34
|
+
|
|
35
|
+
The CLI detects this, prints a warning, and skips git initialisation so a nested repository is never created. Recommended alternatives:
|
|
36
|
+
|
|
37
|
+
- Scaffold outside the repository, then push the project as its own repository.
|
|
38
|
+
- Scaffold into a temporary folder, then move the files to the repository root before the first commit.
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Create a new project (interactive or with name argument)
|
|
44
|
+
npx @techstream/quark-create-app@latest
|
|
45
|
+
npx @techstream/quark-create-app@latest my-app
|
|
46
|
+
|
|
47
|
+
# Add a package to an existing project
|
|
48
|
+
npx @techstream/quark-create-app add jobs
|
|
49
|
+
|
|
50
|
+
# Print an embedded skill for a feature
|
|
51
|
+
npx @techstream/quark-create-app skill bookings
|
|
52
|
+
|
|
53
|
+
# Update Quark packages in an existing project
|
|
54
|
+
npx @techstream/quark-create-app update
|
|
55
|
+
|
|
56
|
+
# Check for available updates without applying them
|
|
57
|
+
npx @techstream/quark-create-app update --check
|
|
58
|
+
|
|
59
|
+
# Report drift in scaffold-managed files without overwriting anything
|
|
60
|
+
npx @techstream/quark-create-app update --scaffold-check
|
|
61
|
+
|
|
62
|
+
# Fail CI when scaffold drift is detected
|
|
63
|
+
npx @techstream/quark-create-app update --scaffold-check --fail-on-drift
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Aliases:
|
|
67
|
+
- `quark-create-app`
|
|
68
|
+
- `create-quark-app`
|
|
69
|
+
- `quark-update`
|
|
70
|
+
|
|
71
|
+
## Interactive Mode
|
|
72
|
+
|
|
73
|
+
When run without arguments, the CLI prompts for:
|
|
74
|
+
|
|
75
|
+
1. **Project name** — becomes the directory name, npm scope, and env vars
|
|
76
|
+
2. **Description** — seeds `MAIN.md` and `.env` `APP_NAME`/`APP_DESCRIPTION`
|
|
77
|
+
3. **Background jobs** — include BullMQ worker for emails, webhooks, scheduled tasks
|
|
78
|
+
4. **Public signup** — allow self-service user registration
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx @techstream/quark-create-app@latest
|
|
82
|
+
# ? Project name: my-app
|
|
83
|
+
# ? Describe your app: A platform for booking salon appointments
|
|
84
|
+
# ? Include background jobs (emails, webhooks, scheduled tasks)? Yes
|
|
85
|
+
# ? Allow public self-service signup? Yes
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Non-Interactive Mode
|
|
89
|
+
|
|
90
|
+
Skip all prompts with flags:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx @techstream/quark-create-app@latest my-app --no-prompts
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Packages
|
|
97
|
+
|
|
98
|
+
Specify which optional packages to include (default: `ui,jobs`; valid: `ui`, `jobs`):
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Only include UI package
|
|
102
|
+
npx @techstream/quark-create-app@latest my-app --no-prompts --packages ui
|
|
103
|
+
|
|
104
|
+
# Include both UI and Jobs
|
|
105
|
+
npx @techstream/quark-create-app@latest my-app --no-prompts --packages ui,jobs
|
|
106
|
+
|
|
107
|
+
# Minimal setup (no optional packages)
|
|
108
|
+
npx @techstream/quark-create-app@latest my-app --no-prompts --packages ""
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`ui` is always included automatically as a required dependency.
|
|
112
|
+
|
|
113
|
+
### Product brief
|
|
114
|
+
|
|
115
|
+
Seed the `MAIN.md` brief from a flag:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx @techstream/quark-create-app@latest my-app --no-prompts --prompt "A booking platform for salons"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Signup
|
|
122
|
+
|
|
123
|
+
Control public self-service signup (default: `enabled`):
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npx @techstream/quark-create-app@latest my-app --no-prompts --signup disabled
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### skill command
|
|
130
|
+
|
|
131
|
+
Print an embedded skill for a feature:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Print the bookings skill
|
|
135
|
+
npx @techstream/quark-create-app@latest skill bookings
|
|
136
|
+
|
|
137
|
+
# Print a core skill (model, endpoint, dashboard)
|
|
138
|
+
npx @techstream/quark-create-app@latest skill model
|
|
139
|
+
npx @techstream/quark-create-app@latest skill endpoint
|
|
140
|
+
npx @techstream/quark-create-app@latest skill dashboard
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Available skills: `model`, `endpoint`, `dashboard`, `bookings`, `crm`, `cms`, `ai`.
|
|
144
|
+
|
|
145
|
+
### Skip Installation Steps
|
|
146
|
+
|
|
147
|
+
Create the project structure without running package installation:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Create project but skip pnpm install
|
|
151
|
+
npx @techstream/quark-create-app@latest my-app --no-prompts --skip-install
|
|
152
|
+
|
|
153
|
+
# Useful for CI/CD where you'll install dependencies separately
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Docker Cleanup
|
|
157
|
+
|
|
158
|
+
Control whether to remove Docker volumes from previous cleanup:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Keep Docker working directories (useful in CI/CD)
|
|
162
|
+
npx @techstream/quark-create-app@latest my-app --no-prompts --skip-docker
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Complete Example: Full Automation
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Create, install, and setup everything automatically
|
|
169
|
+
npx @techstream/quark-create-app@latest my-app \
|
|
170
|
+
--no-prompts \
|
|
171
|
+
--packages ui,jobs \
|
|
172
|
+
&& cd my-app \
|
|
173
|
+
&& docker compose up -d \
|
|
174
|
+
&& pnpm db:migrate \
|
|
175
|
+
&& pnpm dev
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Common Tasks
|
|
179
|
+
|
|
180
|
+
- **Update Quark packages**: `quark-update` or `pnpm update @techstream/quark-*`
|
|
181
|
+
- **Check for updates**: `quark-update --check`
|
|
182
|
+
- **Review scaffold drift**: `quark-update --scaffold-check`
|
|
183
|
+
- **Fail CI on scaffold drift**: `quark-update --scaffold-check --fail-on-drift`
|
|
184
|
+
- **Configure environment**: Edit `.env` file (see `.env.example`)
|
|
185
|
+
|
|
186
|
+
## CLI Testing
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Lightweight template checks
|
|
190
|
+
pnpm test
|
|
191
|
+
|
|
192
|
+
# E2E scaffold simulation
|
|
193
|
+
pnpm test:e2e
|
|
194
|
+
|
|
195
|
+
# Full build verification (opt-in)
|
|
196
|
+
QUARK_CLI_BUILD_TEST=1 pnpm test:build
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Troubleshooting
|
|
200
|
+
|
|
201
|
+
- **pnpm install fails**: Ensure `pnpm` is installed and Node.js >= 22.
|
|
202
|
+
- **Prisma generate fails**: Run `pnpm --filter db db:generate` inside the project.
|
|
203
|
+
- **Docker ports conflict**: The CLI auto-selects free ports. Check `.env` for assigned values.
|
|
204
|
+
- **Missing env vars**: Copy `.env.example` to `.env` and fill required values.
|
|
205
|
+
|
|
206
|
+
## Support
|
|
207
|
+
|
|
208
|
+
For issues, questions, and discussions:
|
|
209
|
+
- [Issue Tracker](https://github.com/usequark/quark/issues)
|
|
210
|
+
- [Discussions](https://github.com/usequark/quark/discussions)
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usequark/quark-create-app",
|
|
3
|
+
"version": "1.23.11",
|
|
4
|
+
"description": "CLI to scaffold new Quark projects with Next.js, Prisma, BullMQ, and Railway deployment",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"homepage": "https://github.com/usequark/quark#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/usequark/quark.git",
|
|
10
|
+
"directory": "packages/cli"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/usequark/quark/issues"
|
|
14
|
+
},
|
|
15
|
+
"author": "Quark Contributors",
|
|
16
|
+
"bin": {
|
|
17
|
+
"quark": "src/index.js",
|
|
18
|
+
"quark-create-app": "src/index.js",
|
|
19
|
+
"create-quark-app": "src/index.js",
|
|
20
|
+
"quark-update": "src/index.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"templates",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"sync-templates": "node scripts/sync-templates.js",
|
|
29
|
+
"sync-templates:check": "node scripts/sync-templates.js --check",
|
|
30
|
+
"regen-migration": "node scripts/regenerate-template-migration.js",
|
|
31
|
+
"smoke:published": "node scripts/smoke-published.js",
|
|
32
|
+
"test": "node test-cli.js && node --test src/utils.test.js && node --test src/prepare-hooks.test.js && node --test src/scaffold-output.test.js && node --test src/template-scripts.test.js && node --test src/scaffold-guards.test.js && node --test src/worker-dockerfile.test.js && node --test src/dependabot-config.test.js && node --test 'src/deploy/**/*.test.js' && node --test --test-concurrency=1 'src/deploy/**/*.integration.test.js'",
|
|
33
|
+
"test:build": "node test-build.js",
|
|
34
|
+
"test:e2e": "node test-e2e.js",
|
|
35
|
+
"test:e2e:full": "node test-e2e-full.js",
|
|
36
|
+
"test:integration": "node test-integration.js",
|
|
37
|
+
"test:flags": "node --test test-flags.js",
|
|
38
|
+
"test:all": "node test-all.js",
|
|
39
|
+
"check:perf": "node scripts/check-e2e-perf.js"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"chalk": "^6.0.0",
|
|
43
|
+
"commander": "^15.0.0",
|
|
44
|
+
"execa": "^10.0.1",
|
|
45
|
+
"fs-extra": "^11.4.1",
|
|
46
|
+
"prompts": "^2.4.2"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"registry": "https://registry.npmjs.org",
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=22.13.0"
|
|
54
|
+
},
|
|
55
|
+
"license": "MIT"
|
|
56
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Guard the scaffold template's dependabot config.
|
|
5
|
+
*
|
|
6
|
+
* The template used to group by `dependency-type` alone, which produced one
|
|
7
|
+
* PR bundling patch, minor and major bumps together. That is how several
|
|
8
|
+
* breaking changes reached this repo unnoticed - bullmq 6 removed
|
|
9
|
+
* `queue.client` and shipped two silent production regressions inside a
|
|
10
|
+
* 35-package diff.
|
|
11
|
+
*
|
|
12
|
+
* This checks the shipped template still separates majors, so a scaffolded
|
|
13
|
+
* project does not inherit the problem.
|
|
14
|
+
*
|
|
15
|
+
* Run with: node --test packages/cli/src/dependabot-config.test.js
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import assert from "node:assert/strict";
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { describe, it } from "node:test";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const REPO_ROOT = path.resolve(__dirname, "../../..");
|
|
26
|
+
|
|
27
|
+
const CONFIGS = [
|
|
28
|
+
["monorepo", ".github/dependabot.yml"],
|
|
29
|
+
[
|
|
30
|
+
"scaffold template",
|
|
31
|
+
"packages/cli/templates/base-project/.github/dependabot.yml",
|
|
32
|
+
],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function readConfig(relativePath) {
|
|
36
|
+
return fs.readFileSync(path.join(REPO_ROOT, relativePath), "utf8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("dependabot config", () => {
|
|
40
|
+
for (const [label, relativePath] of CONFIGS) {
|
|
41
|
+
it(`${label} isolates major bumps into their own group`, () => {
|
|
42
|
+
const text = readConfig(relativePath);
|
|
43
|
+
|
|
44
|
+
assert.match(
|
|
45
|
+
text,
|
|
46
|
+
/production-majors:[\s\S]*?dependency-type:\s*production[\s\S]*?update-types:\s*\["major"\]/,
|
|
47
|
+
"production majors must be grouped separately so a breaking change is reviewed alone",
|
|
48
|
+
);
|
|
49
|
+
assert.match(
|
|
50
|
+
text,
|
|
51
|
+
/development-majors:[\s\S]*?dependency-type:\s*development[\s\S]*?update-types:\s*\["major"\]/,
|
|
52
|
+
"development majors must be grouped separately too",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it(`${label} does not lump every production bump into one group`, () => {
|
|
57
|
+
const text = readConfig(relativePath);
|
|
58
|
+
|
|
59
|
+
// A bare `production:` group with no update-types collects majors,
|
|
60
|
+
// minors and patches into a single reviewable-looking PR.
|
|
61
|
+
assert.doesNotMatch(
|
|
62
|
+
text,
|
|
63
|
+
/\n {6}production:\n {8}dependency-type:\s*production\n/,
|
|
64
|
+
"a catch-all production group re-creates the 35-package PR problem",
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it(`${label} keeps a PR limit that can hold the extra groups`, () => {
|
|
69
|
+
const text = readConfig(relativePath);
|
|
70
|
+
const limit = Number(
|
|
71
|
+
(text.match(/open-pull-requests-limit:\s*(\d+)/) || [])[1],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
assert.ok(
|
|
75
|
+
Number.isInteger(limit) && limit >= 6,
|
|
76
|
+
`open-pull-requests-limit should be at least 6 for the split groups (got ${limit})`,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The auto-merge workflow is the other half of the split, and the only part
|
|
84
|
+
* that actually merges anything.
|
|
85
|
+
*
|
|
86
|
+
* Splitting dependabot into per-severity groups changes what a human sees. It
|
|
87
|
+
* does not change what merges unattended - that is gated solely by
|
|
88
|
+
* `update-type == 'version-update:semver-patch'`. A dependency that ships a
|
|
89
|
+
* breaking change in a *patch* passes straight through, and that is exactly how
|
|
90
|
+
* bullmq 6's `queue.client` removal reached this repo. Majors already need a
|
|
91
|
+
* human by virtue of not matching; this test keeps it that way.
|
|
92
|
+
*
|
|
93
|
+
* The monorepo runs this workflow; the template ships an equivalent one, so a
|
|
94
|
+
* scaffolded project inherits the same gate.
|
|
95
|
+
*/
|
|
96
|
+
const AUTO_MERGE_WORKFLOWS = [
|
|
97
|
+
["monorepo", ".github/workflows/dependabot-auto-merge.yml"],
|
|
98
|
+
[
|
|
99
|
+
"scaffold template",
|
|
100
|
+
"packages/cli/templates/base-project/.github/workflows/dependabot-auto-merge.yml",
|
|
101
|
+
],
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
describe("dependabot auto-merge gate", () => {
|
|
105
|
+
for (const [label, relativePath] of AUTO_MERGE_WORKFLOWS) {
|
|
106
|
+
it(`${label} gates auto-merge on the semver update type`, () => {
|
|
107
|
+
const text = readConfig(relativePath);
|
|
108
|
+
|
|
109
|
+
assert.match(
|
|
110
|
+
text,
|
|
111
|
+
/version-update:semver-patch/,
|
|
112
|
+
"the auto-merge gate must key on the semver patch update type, so minors and majors still need a human",
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it(`${label} does not merge every dependabot update`, () => {
|
|
117
|
+
const text = readConfig(relativePath);
|
|
118
|
+
|
|
119
|
+
// An unconditional `gh pr merge` would auto-merge majors unattended,
|
|
120
|
+
// which is precisely how the bullmq break nearly landed.
|
|
121
|
+
assert.doesNotMatch(
|
|
122
|
+
text,
|
|
123
|
+
/if:\s*github\.actor\s*==\s*'dependabot\[bot\]'\n\s*steps:/,
|
|
124
|
+
"auto-merge must be gated on the update type, not just the actor",
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
|
|
3
|
+
const RAILWAY = "railway";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Escapes a string for safe embedding in a TypeScript string literal.
|
|
7
|
+
* Handles backslashes, quotes, newlines, and template literal interpolation.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} value
|
|
10
|
+
* @returns {string} The escaped string (without surrounding quotes)
|
|
11
|
+
*/
|
|
12
|
+
export function escapeTsString(value) {
|
|
13
|
+
return String(value)
|
|
14
|
+
.replace(/\\/g, "\\\\")
|
|
15
|
+
.replace(/"/g, '\\"')
|
|
16
|
+
.replace(/\n/g, "\\n")
|
|
17
|
+
.replace(/\r/g, "\\r")
|
|
18
|
+
.replace(/\$/g, "\\$");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Converts an arbitrary string into a valid JavaScript identifier.
|
|
23
|
+
* Replaces non-alphanumeric characters with underscores, prefixes
|
|
24
|
+
* with `_` if it starts with a digit, and deduplicates underscores.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} value
|
|
27
|
+
* @returns {string} A valid JS identifier (e.g. `my__web__app`)
|
|
28
|
+
*/
|
|
29
|
+
function toIdentifier(value) {
|
|
30
|
+
return (
|
|
31
|
+
String(value)
|
|
32
|
+
.replace(/[^a-zA-Z0-9]/g, "_")
|
|
33
|
+
.replace(/_+/g, "_")
|
|
34
|
+
.replace(/^(\d)/, "_$1") || "_service"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generates a Railway IaC file (.railway/railway.ts) from service contracts.
|
|
40
|
+
*
|
|
41
|
+
* Uses `preserve()` for secrets so Railway keeps existing values.
|
|
42
|
+
* Uses `${{ServiceName.VARIABLE}}` references for provisioned databases.
|
|
43
|
+
* Omits `source` so the CLI deploy flow (railway config apply) manages
|
|
44
|
+
* settings without overwriting a linked GitHub/Docker source.
|
|
45
|
+
*
|
|
46
|
+
* All user-supplied strings (service names, project names, command strings,
|
|
47
|
+
* variable values) are escaped before embedding in the generated TypeScript.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} options
|
|
50
|
+
* @param {string} options.iacPath - Absolute path to .railway/railway.ts
|
|
51
|
+
* @param {Array<object>} options.services - Discovered services from contract.js
|
|
52
|
+
* @param {object} options.secrets - Map of secret key → preserve() or literal value
|
|
53
|
+
* @param {object} options.variableRefs - Map of variable key → Railway reference string
|
|
54
|
+
* @param {object} options.serviceVars - Map of service name → extra env vars
|
|
55
|
+
* @param {string} [options.projectName] - Railway project name (defaults to "quark-app")
|
|
56
|
+
*/
|
|
57
|
+
export async function generateIacFile({
|
|
58
|
+
iacPath,
|
|
59
|
+
services,
|
|
60
|
+
secrets = {},
|
|
61
|
+
variableRefs = {},
|
|
62
|
+
serviceVars = {},
|
|
63
|
+
projectName = "quark-app",
|
|
64
|
+
}) {
|
|
65
|
+
const { default: fs } = await import("fs-extra");
|
|
66
|
+
const { dirname } = await import("node:path");
|
|
67
|
+
|
|
68
|
+
const imports = new Set();
|
|
69
|
+
imports.add(`import { defineRailway, project, service } from "railway/iac";`);
|
|
70
|
+
|
|
71
|
+
const resourceLines = [];
|
|
72
|
+
|
|
73
|
+
for (const svc of services) {
|
|
74
|
+
const svcIdentifier = toIdentifier(svc.name);
|
|
75
|
+
const svcLabel = escapeTsString(svc.name);
|
|
76
|
+
|
|
77
|
+
// Build config
|
|
78
|
+
const buildCmd =
|
|
79
|
+
svc.kind === "web"
|
|
80
|
+
? "pnpm install --frozen-lockfile && pnpm db:generate && pnpm --dir apps/web build:deploy"
|
|
81
|
+
: "pnpm install --frozen-lockfile && pnpm db:generate";
|
|
82
|
+
|
|
83
|
+
// Deploy config
|
|
84
|
+
const startCmd =
|
|
85
|
+
svc.kind === "web"
|
|
86
|
+
? "HOSTNAME=0.0.0.0 pnpm --dir apps/web start:deploy"
|
|
87
|
+
: "pnpm --dir apps/worker start:deploy";
|
|
88
|
+
|
|
89
|
+
const preDeploy = "pnpm db:migrate:deploy";
|
|
90
|
+
|
|
91
|
+
// Healthcheck (web only)
|
|
92
|
+
const healthcheck =
|
|
93
|
+
svc.kind === "web"
|
|
94
|
+
? `\n\t\thealthcheck: "/api/health",\n\t\thealthcheckTimeout: 120,`
|
|
95
|
+
: "";
|
|
96
|
+
|
|
97
|
+
// Build env vars — escape all string values
|
|
98
|
+
const extraVars = serviceVars[svc.name] || {};
|
|
99
|
+
const envEntries = [
|
|
100
|
+
...Object.entries(variableRefs),
|
|
101
|
+
...Object.entries(secrets),
|
|
102
|
+
...Object.entries(extraVars),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const envBlock =
|
|
106
|
+
envEntries.length > 0
|
|
107
|
+
? `,\n\t\tenv: {\n${envEntries.map(([k, v]) => `\t\t\t${escapeTsString(k)}: ${v},`).join("\n")}\n\t\t}`
|
|
108
|
+
: "";
|
|
109
|
+
|
|
110
|
+
resourceLines.push(`\tconst ${svcIdentifier} = service("${svcLabel}", {
|
|
111
|
+
\t\tbuild: "${escapeTsString(buildCmd)}",
|
|
112
|
+
\t\tstart: "${escapeTsString(startCmd)}",
|
|
113
|
+
\t\tpreDeploy: "${escapeTsString(preDeploy)}",${healthcheck}${envBlock}
|
|
114
|
+
\t});`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const resources = services.map((s) => toIdentifier(s.name)).join(", ");
|
|
118
|
+
|
|
119
|
+
const content = `${Array.from(imports).join("\n")}
|
|
120
|
+
|
|
121
|
+
export default defineRailway(() => {
|
|
122
|
+
${resourceLines.join("\n\n")}
|
|
123
|
+
|
|
124
|
+
\treturn project("${escapeTsString(projectName)}", {
|
|
125
|
+
\t\tresources: [${resources}],
|
|
126
|
+
\t});
|
|
127
|
+
});
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
await fs.ensureDir(dirname(iacPath));
|
|
131
|
+
await fs.writeFile(iacPath, content, "utf8");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Runs `railway config apply --yes` to apply IaC configuration.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} options
|
|
138
|
+
* @param {string} options.cwd - Project root directory
|
|
139
|
+
* @param {number} [options.timeout=120000] - Timeout in ms
|
|
140
|
+
* @returns {Promise<{success: boolean, output: string}>}
|
|
141
|
+
*/
|
|
142
|
+
export async function applyIacConfig({ cwd, timeout = 120_000 } = {}) {
|
|
143
|
+
try {
|
|
144
|
+
const { stdout, stderr } = await execa(
|
|
145
|
+
RAILWAY,
|
|
146
|
+
["config", "apply", "--yes"],
|
|
147
|
+
{ cwd, timeout, reject: true },
|
|
148
|
+
);
|
|
149
|
+
return { success: true, output: stdout || stderr };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const output = error.stdout || error.stderr || error.message;
|
|
152
|
+
return { success: false, output };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Installs the Railway SDK (required for TypeScript IaC evaluation).
|
|
158
|
+
*
|
|
159
|
+
* @param {object} options
|
|
160
|
+
* @param {string} options.cwd - Project root directory
|
|
161
|
+
* @param {string} [options.packageManager="pnpm"] - Package manager to use
|
|
162
|
+
*/
|
|
163
|
+
export async function installRailwaySdk({ cwd, packageManager = "pnpm" } = {}) {
|
|
164
|
+
await execa(packageManager, ["add", "-D", "railway"], {
|
|
165
|
+
cwd,
|
|
166
|
+
timeout: 60_000,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Checks the latest deployment status for a service.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} serviceName
|
|
174
|
+
* @param {object} options
|
|
175
|
+
* @param {string} options.cwd - Project root directory
|
|
176
|
+
* @param {string} [options.environment] - Railway environment name
|
|
177
|
+
* @returns {Promise<{status: string|null, deploymentId: string|null}>}
|
|
178
|
+
*/
|
|
179
|
+
export async function getDeploymentStatus(
|
|
180
|
+
serviceName,
|
|
181
|
+
{ cwd, environment } = {},
|
|
182
|
+
) {
|
|
183
|
+
const args = ["deployment", "list", "--json", "--service", serviceName];
|
|
184
|
+
if (environment) {
|
|
185
|
+
args.push("--environment", environment);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const { stdout } = await execa(RAILWAY, args, {
|
|
190
|
+
cwd,
|
|
191
|
+
timeout: 15_000,
|
|
192
|
+
});
|
|
193
|
+
const deployments = JSON.parse(stdout);
|
|
194
|
+
const latest = Array.isArray(deployments) ? deployments[0] : null;
|
|
195
|
+
if (!latest) return { status: null, deploymentId: null };
|
|
196
|
+
return {
|
|
197
|
+
status: (latest.status || "").toUpperCase(),
|
|
198
|
+
deploymentId: latest.id || null,
|
|
199
|
+
};
|
|
200
|
+
} catch {
|
|
201
|
+
return { status: null, deploymentId: null };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export {
|
|
2
|
+
applyIacConfig,
|
|
3
|
+
escapeTsString,
|
|
4
|
+
generateIacFile,
|
|
5
|
+
getDeploymentStatus,
|
|
6
|
+
installRailwaySdk,
|
|
7
|
+
} from "./iac.js";
|
|
8
|
+
export {
|
|
9
|
+
checkRailwayCLI,
|
|
10
|
+
checkRailwayLogin,
|
|
11
|
+
DIAGNOSTIC_CODES as RAILWAY_DIAGNOSTIC_CODES,
|
|
12
|
+
deleteService,
|
|
13
|
+
deployService,
|
|
14
|
+
ensurePlugin,
|
|
15
|
+
ensureRailwayProject,
|
|
16
|
+
ensureService,
|
|
17
|
+
getExistingVariable,
|
|
18
|
+
getProjectDomains,
|
|
19
|
+
getServiceUrl,
|
|
20
|
+
isProjectLinked,
|
|
21
|
+
listProjects,
|
|
22
|
+
RAILWAY,
|
|
23
|
+
RailwayError,
|
|
24
|
+
removeServiceDomain,
|
|
25
|
+
setProjectVariables,
|
|
26
|
+
tryLinkProject,
|
|
27
|
+
} from "./railway.js";
|