create-raredays-app 0.1.12 → 0.3.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 CHANGED
@@ -37,9 +37,11 @@ Presets set the template, modules, and brand color. You can still customise disp
37
37
  ## Options
38
38
 
39
39
  - `--template <name>` -- `full-stack` (default), `client`, or `static`.
40
- - `--preset <name>` -- `courseware`, `internal-tool`, or `demo`. Overrides `--template` and `--modules`.
41
- - `--modules <list>` -- comma-separated modules to wire: `billing`, `jobs`, `notifications`, `email`, `telemetry`, `feature-flags`.
42
- - `--vendors <list>` -- comma-separated vendor preconfiguration: `stripe`, `resend`, `sentry`, `axiom`.
40
+ - `--preset <name>` -- `courseware`, `internal-tool`, or `demo`. **Overrides** `--template`, `--modules`, and `--vendors` (it does not extend them).
41
+ - `--modules <list>` -- comma-separated modules to wire. Discovered dynamically from `scaffold-fragments/`: `billing`, `cms`, `email`, `feature-flags`, `i18n`, `identity`, `jobs`, `notifications`, `organizations`, `telemetry`. Modules apply to `full-stack` and `client` templates only.
42
+ - `--vendors <list>` -- comma-separated **`module:vendor` pairs**, e.g. `billing:stripe,cms:hygraph,email:resend,identity:better-auth`. A bare vendor name (`stripe`) is not a valid pair and is skipped with a warning.
43
+ - `--harness <name>` -- wire `@raredays/mcp` into an MCP-aware IDE: `claude-code`, `cursor`, `windsurf`, or `none` (default). Adds `@raredays/mcp` to devDependencies and writes the IDE config (`.mcp.json` at root for Claude Code; `.cursor/mcp.json` / `.windsurf/mcp.json` otherwise).
44
+ - `--deploy <target>` -- scaffold deploy config: `vercel` or `none` (default). Writes `vercel.json`, `.env.example`, and `docs/DEPLOY.md`.
43
45
  - `--remote-url <url>` -- set git remote origin after scaffolding.
44
46
  - `--no-git` -- skip `git init`.
45
47
  - `--no-install` -- skip `pnpm install`.
@@ -49,27 +51,41 @@ Presets set the template, modules, and brand color. You can still customise disp
49
51
  ## What it does
50
52
 
51
53
  1. Copies `templates/<name>/` into the target directory.
52
- 2. Renames stub files (`_gitignore` -> `.gitignore`, `_github` -> `.github`).
54
+ 2. Renames stub files (`_gitignore` -> `.gitignore`, `_npmrc` -> `.npmrc`, `_claude` -> `.claude`, `_github` -> `.github`).
53
55
  3. Sets `package.json:name` to the project slug.
54
56
  4. Substitutes `__SLUG__` in `site.spec.ts` and applies display name, domain, and brand color.
55
- 5. Loads scaffold fragments for selected modules (imports, config, ports, file overlays).
56
- 6. Composes `app/lib/runtime.ts` with module wiring and `mocks/handlers.ts` with vendor mocks.
57
- 7. Merges fragment dependencies into `package.json`.
58
- 8. Writes `.env.example` if any vendor env vars are needed.
59
- 9. Optionally runs `git init` and `pnpm install`.
57
+ 5. Loads scaffold fragments for selected modules (imports, config, ports, file overlays) and applies vendor overlays for each `module:vendor` pair.
58
+ 6. Composes `app/lib/runtime.ts` with module wiring and `mocks/handlers.ts` with vendor mocks (skipped for the `static` template, which has no composition root).
59
+ 7. Merges fragment + vendor dependencies into `package.json`.
60
+ 8. Applies `--harness` (MCP config) and `--deploy` (deploy config) when requested.
61
+ 9. Writes `.env.example` **only** if any vendor or deploy env vars are needed.
62
+ 10. Optionally runs `git init` and `pnpm install`.
60
63
 
61
64
  ## Scaffold fragments
62
65
 
63
- The `scaffold-fragments/` directory contains per-module overlays:
64
-
65
- - `billing/` -- Stripe billing integration (checkout, portal, webhooks)
66
- - `email/` -- Resend email provider
67
- - `feature-flags/` -- Vercel Flags SDK wiring
68
- - `jobs/` -- Background job processing
69
- - `notifications/` -- Multi-channel notification delivery
70
- - `telemetry/` -- Sentry + Axiom telemetry
71
-
72
- Each fragment has a `module.json` manifest, runtime slot injections, and optional file overlays.
66
+ The `scaffold-fragments/` directory contains per-module overlays (10 total):
67
+
68
+ - `billing/` -- billing integration; vendor: `stripe`
69
+ - `cms/` -- CMS integration; vendor: `hygraph`
70
+ - `email/` -- transactional email; vendor: `resend`
71
+ - `feature-flags/` -- feature-flag wiring
72
+ - `i18n/` -- internationalisation
73
+ - `identity/` -- auth/sessions/roles; vendor: `better-auth`
74
+ - `jobs/` -- background job processing
75
+ - `notifications/` -- multi-channel notification delivery
76
+ - `organizations/` -- multi-tenant organizations
77
+ - `telemetry/` -- telemetry; vendors: `sentry`, `axiom`
78
+
79
+ Each fragment has a `module.json` manifest, runtime slot injections, and optional per-vendor file overlays.
80
+
81
+ ## Keeping template pins current
82
+
83
+ Every template and fragment pins `@raredays/*` deps and `MCP_VERSION`. These are
84
+ the framework's public surface — a non-`--dev` scaffold installs exactly what
85
+ they pin — and do not move automatically when the workspace releases. Run
86
+ `pnpm --filter create-raredays-app sync-versions` (also run as `prepublishOnly`)
87
+ to refresh them to `^<current workspace version>`. The guard test
88
+ `test/template-workspace-pins.test.ts` fails in CI when a pin drifts out of range.
73
89
 
74
90
  ## How it fits in the ecosystem
75
91
 
package/bin/index.js CHANGED
@@ -635,12 +635,14 @@ async function scaffold(opts) {
635
635
  // ─── MCP harness wiring ──────────────────────────────────────────────────
636
636
 
637
637
  const HARNESS_CONFIGS = {
638
- "claude-code": ".claude/mcp.json",
638
+ // Claude Code reads project-scope MCP servers from `.mcp.json` at the repo
639
+ // root (not `.claude/mcp.json`), as this monorepo's own `.mcp.json` does.
640
+ "claude-code": ".mcp.json",
639
641
  cursor: ".cursor/mcp.json",
640
642
  windsurf: ".windsurf/mcp.json",
641
643
  };
642
644
 
643
- const MCP_VERSION = "0.0.1";
645
+ const MCP_VERSION = "0.2.0";
644
646
 
645
647
  async function applyHarness(targetDir, pkg, harness, dev) {
646
648
  if (!harness || harness === "none") return;
@@ -944,26 +946,70 @@ async function main() {
944
946
  `Unknown preset "${values.preset}". Available presets: ${Object.keys(PRESETS).join(", ")}`,
945
947
  );
946
948
  }
949
+ const resolvedTemplate = preset ? preset.template : values.template;
947
950
  const requestedModules = preset ? preset.modules : parseCsv(values.modules);
948
951
  const availableModules = availableModuleKeys();
949
952
  const known = [];
950
953
  for (const m of requestedModules) {
951
- if (availableModules.includes(m)) {
952
- known.push(m);
953
- } else {
954
+ if (!availableModules.includes(m)) {
954
955
  console.warn(
955
956
  `warning: --modules "${m}" has no scaffold-fragment under scaffold-fragments/${m}/, skipping. Available: ${availableModules.join(", ")}`,
956
957
  );
958
+ continue;
957
959
  }
960
+ // Mirror the interactive path's template-compatibility filter: a module
961
+ // whose fragment does not declare the resolved template is skipped (its
962
+ // runtime/files assume a composition root that template lacks).
963
+ const fragment = allFragments.find((f) => f.key === m);
964
+ const supported = fragment?.templates ?? ["full-stack"];
965
+ if (!supported.includes(resolvedTemplate)) {
966
+ console.warn(
967
+ `warning: --modules "${m}" is not compatible with the "${resolvedTemplate}" template (supports: ${supported.join(", ")}), skipping.`,
968
+ );
969
+ continue;
970
+ }
971
+ known.push(m);
972
+ }
973
+
974
+ // Validate --vendors pairs (module:vendor) and warn on unknown ones, the way
975
+ // the module list already warns. A silently-dropped vendor is worse than a
976
+ // loud one.
977
+ const requestedVendors = preset ? preset.vendors : parseCsv(values.vendors);
978
+ const knownVendors = [];
979
+ for (const pair of requestedVendors) {
980
+ const [moduleKey, vendorKey] = pair.split(":");
981
+ if (!vendorKey) {
982
+ console.warn(
983
+ `warning: --vendors "${pair}" is not a "module:vendor" pair (e.g. "billing:stripe"), skipping.`,
984
+ );
985
+ continue;
986
+ }
987
+ const fragment = allFragments.find((f) => f.key === moduleKey);
988
+ const vendor = fragment?.vendors?.find((v) => v.key === vendorKey);
989
+ if (!known.includes(moduleKey)) {
990
+ console.warn(
991
+ `warning: --vendors "${pair}" names module "${moduleKey}", which is not in the selected/compatible module set, skipping.`,
992
+ );
993
+ continue;
994
+ }
995
+ if (!vendor) {
996
+ const vendorKeys = (fragment?.vendors ?? []).map((v) => v.key);
997
+ console.warn(
998
+ `warning: --vendors "${pair}" has no vendor "${vendorKey}" for module "${moduleKey}"${vendorKeys.length ? ` (available: ${vendorKeys.join(", ")})` : ""}, skipping.`,
999
+ );
1000
+ continue;
1001
+ }
1002
+ knownVendors.push(pair);
958
1003
  }
1004
+
959
1005
  opts = {
960
1006
  projectName: target,
961
- template: preset ? preset.template : values.template,
1007
+ template: resolvedTemplate,
962
1008
  displayName: undefined,
963
1009
  canonicalDomain: undefined,
964
1010
  brandPrimary: preset?.brandPrimary ?? undefined,
965
1011
  selectedModules: known,
966
- selectedVendors: preset ? preset.vendors : parseCsv(values.vendors),
1012
+ selectedVendors: knownVendors,
967
1013
  remoteUrl: values["remote-url"],
968
1014
  gitInit: !values["no-git"],
969
1015
  runInstall: !values["no-install"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-raredays-app",
3
- "version": "0.1.12",
3
+ "version": "0.3.0",
4
4
  "description": "Scaffold a new RareDays Next.js app.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Jeremy Harper <jeremy@raredays.com>",
@@ -22,8 +22,8 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "typescript": "5.9.3",
25
- "vitest": "4.1.9",
26
- "@raredays/lint": "0.2.1"
25
+ "vitest": "4.1.10",
26
+ "@raredays/lint": "0.2.2"
27
27
  },
28
28
  "engines": {
29
29
  "node": ">=20"
@@ -41,6 +41,8 @@
41
41
  "check": "raredays check",
42
42
  "test": "vitest run",
43
43
  "test:watch": "vitest",
44
+ "sync-versions": "node scripts/sync-template-versions.mjs",
45
+ "sync-versions:check": "node scripts/sync-template-versions.mjs --check",
44
46
  "dev": "mkdir -p .sandbox && cd .sandbox && node ../bin/index.js",
45
47
  "clean": "rm -rf .sandbox"
46
48
  }
@@ -3,14 +3,14 @@
3
3
  "label": "Billing (subscriptions + webhooks)",
4
4
  "description": "Plan registry, subscription state, webhook handler with idempotency + event-bus fan-out.",
5
5
  "deps": {
6
- "@raredays/billing": "^0.1.0"
6
+ "@raredays/billing": "^0.3.0"
7
7
  },
8
8
  "vendors": [
9
9
  {
10
10
  "key": "stripe",
11
11
  "label": "Stripe",
12
12
  "deps": {
13
- "@raredays/billing-stripe": "^0.1.0"
13
+ "@raredays/billing-stripe": "^0.1.1"
14
14
  },
15
15
  "env": [
16
16
  "STRIPE_SECRET_KEY",
@@ -3,14 +3,14 @@
3
3
  "label": "CMS (headless content + rich text)",
4
4
  "description": "Vendor-neutral CMS port. In-memory dev driver for development; swap to Hygraph (or another adapter) for production.",
5
5
  "deps": {
6
- "@raredays/cms": "^0.1.0"
6
+ "@raredays/cms": "^0.2.0"
7
7
  },
8
8
  "vendors": [
9
9
  {
10
10
  "key": "hygraph",
11
11
  "label": "Hygraph",
12
12
  "deps": {
13
- "@raredays/cms-hygraph": "^0.1.0"
13
+ "@raredays/cms-hygraph": "^0.1.1"
14
14
  },
15
15
  "env": [
16
16
  "HYGRAPH_ENDPOINT",
@@ -3,14 +3,14 @@
3
3
  "label": "Transactional email",
4
4
  "description": "Vendor-neutral email port. Console adapter for dev; swap to Resend for production.",
5
5
  "deps": {
6
- "@raredays/email": "^0.1.0"
6
+ "@raredays/email": "^0.2.0"
7
7
  },
8
8
  "vendors": [
9
9
  {
10
10
  "key": "resend",
11
11
  "label": "Resend",
12
12
  "deps": {
13
- "@raredays/email-resend": "^0.1.0"
13
+ "@raredays/email-resend": "^0.1.2"
14
14
  },
15
15
  "env": [
16
16
  "RESEND_API_KEY"
@@ -3,7 +3,10 @@
3
3
  "label": "Internationalisation (i18n)",
4
4
  "description": "Locale resolution, ICU message formatting, date/number/currency formatters, and React hooks.",
5
5
  "deps": {
6
- "@raredays/i18n": "^0.1.0"
6
+ "@raredays/i18n": "^1.0.1"
7
7
  },
8
- "templates": ["full-stack", "client", "static"]
8
+ "templates": [
9
+ "full-stack",
10
+ "client"
11
+ ]
9
12
  }
@@ -3,15 +3,15 @@
3
3
  "label": "Identity (auth, sessions, organizations, roles)",
4
4
  "description": "Identity port is always wired with an in-memory dev provider. Select a vendor to add the production adapter, env vars, mock handlers, and Next.js auth route stub.",
5
5
  "deps": {
6
- "@raredays/identity": "^0.1.0"
6
+ "@raredays/identity": "^0.1.1"
7
7
  },
8
8
  "vendors": [
9
9
  {
10
10
  "key": "better-auth",
11
11
  "label": "Better Auth (self-hosted)",
12
12
  "deps": {
13
- "@raredays/identity-better-auth": "^0.1.0",
14
- "better-auth": "^1.2.0"
13
+ "@raredays/identity-better-auth": "^0.2.0",
14
+ "better-auth": "1.6.11"
15
15
  },
16
16
  "env": [
17
17
  "BETTER_AUTH_SECRET",
@@ -14,7 +14,26 @@
14
14
  // See @raredays/identity-better-auth README for the full pattern.
15
15
 
16
16
  import { betterAuth } from "better-auth";
17
+ import { resolveRedirectorConfig } from "@raredays/identity-better-auth/oauth-redirector";
18
+
19
+ // Preview-deployment OAuth. Inert unless OAUTH_REDIRECTOR_URL + SECRET are set
20
+ // (Preview env only), so production, staging, and local dev use the direct flow
21
+ // unchanged. When set, previews with dynamic hostnames (per-branch, per-dev)
22
+ // complete OAuth against their OWN database by bouncing through this app's
23
+ // redirector (oauth.<zone>) — the single callback URL you register with the
24
+ // provider (@legoland/project-ops provisions this per project). Set the social
25
+ // provider's `redirectURI` to `redirector.redirectURI` too (see the commented
26
+ // Google block). @raredays/identity-better-auth "Preview OAuth".
27
+ const redirector = resolveRedirectorConfig();
17
28
 
18
29
  export const auth = betterAuth({
19
30
  emailAndPassword: { enabled: true },
31
+ // socialProviders: {
32
+ // google: {
33
+ // clientId: process.env.GOOGLE_CLIENT_ID!,
34
+ // clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
35
+ // ...(redirector.redirectURI && { redirectURI: redirector.redirectURI }),
36
+ // },
37
+ // },
38
+ plugins: [...redirector.plugins],
20
39
  });
@@ -3,7 +3,7 @@
3
3
  "label": "Background jobs (in-process runner)",
4
4
  "description": "Type-safe job definitions, retries, idempotency. In-process runner by default; swap to Inngest/Trigger.dev for durable execution.",
5
5
  "deps": {
6
- "@raredays/jobs": "^0.1.0"
6
+ "@raredays/jobs": "^0.2.0"
7
7
  },
8
8
  "vendors": [],
9
9
  "templates": [
@@ -3,7 +3,7 @@
3
3
  "label": "Notifications (multi-channel send)",
4
4
  "description": "Notification definitions, channels (email + in-app), user preferences, dedupe, delivery logging.",
5
5
  "deps": {
6
- "@raredays/notifications": "^0.1.0"
6
+ "@raredays/notifications": "^0.2.0"
7
7
  },
8
8
  "vendors": [],
9
9
  "templates": [
@@ -3,7 +3,7 @@
3
3
  "label": "Organizations (multi-tenancy)",
4
4
  "description": "Domain types + role-based permissions for organizations, memberships, and invitations. Pairs with a postgres provider for storage.",
5
5
  "deps": {
6
- "@raredays/organizations": "^0.1.0"
6
+ "@raredays/organizations": "^0.2.0"
7
7
  },
8
8
  "vendors": [],
9
9
  "templates": [
@@ -3,14 +3,14 @@
3
3
  "label": "Telemetry (logging, errors, metrics, events)",
4
4
  "description": "Vendor-neutral observability port. Console adapters for dev; swap to Sentry + Axiom for production.",
5
5
  "deps": {
6
- "@raredays/telemetry": "^0.1.0"
6
+ "@raredays/telemetry": "^0.2.0"
7
7
  },
8
8
  "vendors": [
9
9
  {
10
10
  "key": "sentry",
11
11
  "label": "Sentry (error reporting)",
12
12
  "deps": {
13
- "@raredays/telemetry-sentry": "^0.1.0"
13
+ "@raredays/telemetry-sentry": "^0.1.2"
14
14
  },
15
15
  "env": [
16
16
  "SENTRY_DSN"
@@ -24,7 +24,7 @@
24
24
  "key": "axiom",
25
25
  "label": "Axiom (logging)",
26
26
  "deps": {
27
- "@raredays/telemetry-axiom": "^0.1.0"
27
+ "@raredays/telemetry-axiom": "^0.1.1"
28
28
  },
29
29
  "env": [
30
30
  "AXIOM_TOKEN",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "Contributing to create-raredays-app"
3
- description: Deep guide to the app scaffolder — CLI flow, CRDA placeholder tokens, composeRuntime/composeHandlers internals, template file trees, customiseSiteSpec regex, fragment loading, env generation, --dev flag.
3
+ description: Deep guide to the app scaffolder — CLI flow, presets, CRDA placeholder tokens, composeRuntime/composeHandlers internals, template file trees, customiseSiteSpec regex, ten fragments + vendor overlays, harness/deploy wiring, env generation, --dev flag, version-pin sync script, and the test suite.
4
4
  topics: [scaffolding, contributing]
5
5
  tags: [scaffolding, cli, contributing]
6
6
  ---
@@ -42,16 +42,18 @@ Prompts in order via `@clack/prompts`:
42
42
 
43
43
  1. Error if target dir exists
44
44
  2. Copy `templates/{template}/` recursively
45
- 3. Rename stub files: `_gitignore` → `.gitignore`, `_npmrc` → `.npmrc`
45
+ 3. Rename stub files: `_gitignore` → `.gitignore`, `_npmrc` → `.npmrc`, `_claude` → `.claude`, `_github` → `.github`
46
46
  4. Derive slug: trim, lowercase, replace `[^a-z0-9]+` with `-`, strip leading/trailing `-`
47
47
  5. Substitute `__SLUG__` in `site.spec.ts`
48
48
  6. Apply `customiseSiteSpec()` regex replacements
49
- 7. Load fragments, merge deps, copy fragment `files/` dirs, compose runtime
49
+ 7. Load fragments (template-compatibility filtered), merge deps + vendor overlays, copy fragment `files/` dirs, compose runtime
50
50
  8. Compose mock handlers if template ≠ `static`
51
51
  9. Merge deps into `package.json`, apply `--dev` rewrite if flagged
52
- 10. Write `.env.example` if any env vars collected
53
- 11. `git init --quiet --initial-branch=main` (+ remote if specified)
54
- 12. `pnpm install` with `stdio: "inherit"`
52
+ 10. `applyHarness()` (`--harness`) — add `@raredays/mcp` devDep + write the IDE MCP config (`.mcp.json` at root for Claude Code)
53
+ 11. `applyDeploy()` (`--deploy vercel`) — write `vercel.json` + `docs/DEPLOY.md`, collect deploy env vars
54
+ 12. Write `.env.example` only if any vendor/deploy env vars were collected
55
+ 13. `git init --quiet --initial-branch=main` (+ remote if specified)
56
+ 14. `pnpm install` with `stdio: "inherit"`
55
57
 
56
58
  ## CRDA Placeholder Tokens
57
59
 
@@ -135,12 +137,12 @@ scaffold-fragments/{key}/
135
137
  "key": "billing",
136
138
  "label": "Billing (subscriptions + webhooks)",
137
139
  "description": "...",
138
- "deps": { "@raredays/billing": "0.0.1" },
140
+ "deps": { "@raredays/billing": "^0.2.0" },
139
141
  "vendors": [
140
142
  {
141
143
  "key": "stripe",
142
144
  "label": "Stripe",
143
- "deps": { "@raredays/billing-stripe": "0.0.1" },
145
+ "deps": { "@raredays/billing-stripe": "^0.1.0" },
144
146
  "env": ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"],
145
147
  "mocks": { "package": "@raredays/billing-stripe/mocks", "named": "stripeMocks" }
146
148
  }
@@ -149,16 +151,24 @@ scaffold-fragments/{key}/
149
151
  }
150
152
  ```
151
153
 
152
- ### Available Fragments
154
+ ### Available Fragments (10)
153
155
 
154
156
  | Fragment | Templates | Vendors | Env Vars |
155
157
  | --------------- | ------------------ | ------------- | -------------------------------------------- |
156
158
  | `billing` | full-stack | Stripe | `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` |
159
+ | `cms` | full-stack, client | Hygraph | `HYGRAPH_ENDPOINT`, `HYGRAPH_TOKEN` |
157
160
  | `email` | full-stack | Resend | `RESEND_API_KEY` |
161
+ | `feature-flags` | full-stack, client | None | — |
162
+ | `i18n` | full-stack, client | None | — |
163
+ | `identity` | full-stack, client | Better Auth | `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL` |
158
164
  | `jobs` | full-stack | None | — |
159
165
  | `notifications` | full-stack | None | — |
166
+ | `organizations` | full-stack | None | — |
160
167
  | `telemetry` | full-stack, client | Sentry, Axiom | `SENTRY_DSN`, `AXIOM_TOKEN`, `AXIOM_DATASET` |
161
168
 
169
+ Pins in `deps`/`vendors[].deps` are `^<workspace version>` for `@raredays/*`
170
+ packages, kept current by `scripts/sync-template-versions.mjs` (see below).
171
+
162
172
  ## .env.example Generation
163
173
 
164
174
  Only written if `envVars.length > 0`. Format:
@@ -178,12 +188,31 @@ STRIPE_WEBHOOK_SECRET=
178
188
 
179
189
  Rewrites every `@raredays/*` in both `dependencies` and `devDependencies` to `workspace:*`. Used for in-repo dogfooding (playground scaffolds).
180
190
 
191
+ ## Version pins (`scripts/sync-template-versions.mjs`)
192
+
193
+ Templates + fragments pin `@raredays/*` and `bin/index.js` holds `MCP_VERSION`.
194
+ These are the framework's public surface and do NOT move when the workspace
195
+ releases. Run `pnpm --filter create-raredays-app sync-versions` (also
196
+ `prepublishOnly`) to refresh them to `^<workspace version>`; `sync-versions:check`
197
+ is the CI dry-run. The guard test `test/template-workspace-pins.test.ts` fails
198
+ when a pin's range excludes the current workspace version.
199
+
181
200
  ## Testing
182
201
 
183
- No unit tests. Validated by:
202
+ The package ships Vitest guard/unit tests under `test/`:
203
+
204
+ - **Template lint guards**: every-import-is-declared, secure next/react floors,
205
+ pnpm-11 build approvals, registry auth, `.ts`-import specifiers, skill
206
+ frontmatter (v4), workflow lint, no duplicate migration workflow.
207
+ - **`template-workspace-pins.test.ts`**: `@raredays/*` pins (incl. `MCP_VERSION`)
208
+ resolve the current workspace version.
209
+ - **`scaffold-smoke.test.ts`**: runs `bin/index.js` into a temp dir
210
+ (`--no-git --no-install --dev`) and asserts the harness config path, merged
211
+ deps, and composed runtime.
212
+ - **`compose-allow-builds.test.ts`**: `mergeAllowBuilds` unit tests.
184
213
 
185
- - E2E scenarios in `/e2e/` (scaffold → build → serve → test)
186
- - Manual: `pnpm playground:reset && pnpm playground`
214
+ Full scaffold-build-serve coverage lives in the e2e scenarios (`/e2e/`); use
215
+ `pnpm playground:reset && pnpm playground` for manual checks.
187
216
 
188
217
  ## Build and Test
189
218
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "Consuming create-raredays-app"
3
- description: How to scaffold new apps — interactive prompts sequence, CLI flags, three templates (full-stack/client/static) with output files, five available modules with vendor options, --dev flag for monorepo dogfooding.
3
+ description: How to scaffold new apps — presets, interactive prompts sequence, CLI flags (including --preset/--harness/--deploy), three templates (full-stack/client/static) with output files, ten available modules with vendor options, --dev flag for monorepo dogfooding.
4
4
  topics: [scaffolding, consuming]
5
5
  tags: [scaffolding, cli, consuming]
6
6
  ---
@@ -13,18 +13,21 @@ tags: [scaffolding, cli, consuming]
13
13
  pnpm create raredays-app
14
14
  ```
15
15
 
16
- Prompts in order:
16
+ Prompts in roughly this order:
17
17
 
18
- 1. **Project name**directory name and package slug (kebab-case, 1–64 chars)
19
- 2. **Template** — `full-stack`, `client`, or `static`
20
- 3. **Display name** — human-readable (defaults to project name)
21
- 4. **Canonical domain** — optional, hostname only (e.g., `example.com`)
22
- 5. **Brand primary color** — hex value (defaults to `#10b981`)
23
- 6. **Modules** — multiselect (not shown for `static`)
24
- 7. **Vendors** — multiselect for selected modules
25
- 8. **Git remote URL** — optional
26
- 9. **Initialize git?**default yes
27
- 10. **Run pnpm install?**default yes
18
+ 1. **What are you building?** pick a preset (courseware / internal-tool / demo) or "custom"
19
+ 2. **Project name** — directory name and package slug (kebab-case, 1–64 chars)
20
+ 3. **Template** — `full-stack`, `client`, or `static` (skipped when a preset is chosen)
21
+ 4. **Display name** — human-readable (defaults to project name)
22
+ 5. **Canonical domain** — optional, hostname only (e.g., `example.com`)
23
+ 6. **Brand primary color** — hex value (defaults to `#10b981`)
24
+ 7. **Modules** — multiselect (not shown for `static` or when a preset is chosen)
25
+ 8. **Vendors** — multiselect for selected modules
26
+ 9. **Deploy target**optional (`vercel` / none)
27
+ 10. **Git remote URL**optional
28
+ 11. **Initialize git? / Run pnpm install?** — default yes
29
+
30
+ `--harness` (MCP/IDE wiring) is flag-only today; it is not offered interactively.
28
31
 
29
32
  ## Non-Interactive Mode
30
33
 
@@ -37,15 +40,18 @@ pnpm create raredays-app my-app \
37
40
 
38
41
  ## CLI Flags
39
42
 
40
- | Flag | Description |
41
- | -------------- | --------------------------------------------------------------------- |
42
- | `--template` | `full-stack` (default), `client`, or `static` |
43
- | `--modules` | Comma-separated module keys |
44
- | `--vendors` | Comma-separated `MODULE:VENDOR` selectors |
45
- | `--remote-url` | Git remote origin URL |
46
- | `--no-git` | Skip git initialization |
47
- | `--no-install` | Skip `pnpm install` |
48
- | `--dev` | Rewrite all `@raredays/*` deps to `workspace:*` (monorepo dogfooding) |
43
+ | Flag | Description |
44
+ | -------------- | ----------------------------------------------------------------------------------- |
45
+ | `--template` | `full-stack` (default), `client`, or `static` |
46
+ | `--preset` | `courseware`, `internal-tool`, `demo` — **overrides** template/modules/vendors |
47
+ | `--modules` | Comma-separated module keys (ignored when `--preset` is given) |
48
+ | `--vendors` | Comma-separated `MODULE:VENDOR` selectors (a bare vendor is skipped with a warning) |
49
+ | `--harness` | `claude-code` / `cursor` / `windsurf` / none — wire `@raredays/mcp` into the IDE |
50
+ | `--deploy` | `vercel` / none — scaffold deploy config (`vercel.json`, `docs/DEPLOY.md`) |
51
+ | `--remote-url` | Git remote origin URL |
52
+ | `--no-git` | Skip git initialization |
53
+ | `--no-install` | Skip `pnpm install` |
54
+ | `--dev` | Rewrite all `@raredays/*` deps to `workspace:*` (monorepo dogfooding) |
49
55
 
50
56
  ## Templates
51
57
 
@@ -89,14 +95,21 @@ Key output files:
89
95
 
90
96
  ## Available Modules
91
97
 
92
- | Module | Label | Template | Vendor Options |
98
+ Ten fragments live under `scaffold-fragments/`. Each declares which templates it
99
+ supports (`module.json:templates`); a module incompatible with the chosen
100
+ template is skipped with a warning.
101
+
102
+ | Module | Label | Templates | Vendor Options |
93
103
  | --------------- | --------------------------- | ------------------ | -------------- |
94
104
  | `billing` | Subscriptions + webhooks | full-stack | Stripe |
95
105
  | `cms` | Headless CMS + rich text | full-stack, client | Hygraph |
96
106
  | `email` | Transactional email | full-stack | Resend |
107
+ | `feature-flags` | Feature-flag wiring | full-stack, client | — |
108
+ | `i18n` | Internationalisation | full-stack, client | — |
97
109
  | `identity` | Auth, sessions, orgs, roles | full-stack, client | Better Auth |
98
110
  | `jobs` | Background jobs | full-stack | — (in-memory) |
99
111
  | `notifications` | Multi-channel notifications | full-stack | — (in-memory) |
112
+ | `organizations` | Multi-tenant organizations | full-stack | — (in-memory) |
100
113
  | `telemetry` | Logging, errors, metrics | full-stack, client | Sentry, Axiom |
101
114
 
102
115
  Identity is always wired with an in-memory dev provider (the template hard-codes
@@ -140,11 +153,13 @@ A fully configured project with:
140
153
 
141
154
  - `site.spec.ts` — SiteSpec with your name, domain, and brand color substituted
142
155
  - `app/lib/runtime.ts` — composition root with selected modules wired via slot injection
143
- - `app/theme.css` — generated `@theme` overrides from branding config
156
+ - `app/theme.override.css` — build-time `@theme` overrides from branding config
144
157
  - `next.config.ts` — wraps `withRaredays()`
145
158
  - `postcss.config.mjs` — re-exports from `@raredays/style/postcss`
146
159
  - `mocks/` — MSW setup with vendor-specific handlers (client/full-stack)
147
- - `.env.example` — required env vars for selected vendors
160
+ - `.env.example` — written only when a selected vendor or `--deploy` target needs env vars
161
+ - With `--harness`: `.mcp.json` (Claude Code, at root) / `.cursor/mcp.json` / `.windsurf/mcp.json` + `@raredays/mcp` devDep
162
+ - With `--deploy vercel`: `vercel.json` + `docs/DEPLOY.md`
148
163
 
149
164
  ## After Scaffolding
150
165
 
@@ -1,5 +1,7 @@
1
- /*
2
- * Auto-generated by @raredays/theme-build. Do not edit by hand.
1
+ /**
2
+ * AUTO-GENERATED DO NOT EDIT.
3
+ * Regenerated from site.spec.ts by `pnpm theme:build`.
4
+ * For per-app CSS overrides, edit app/globals.css instead.
3
5
  * Source: site.spec.ts
4
6
  * To regenerate: pnpm theme:build
5
7
  */
@@ -26,11 +26,11 @@
26
26
  "check": "raredays check && tsc --noEmit"
27
27
  },
28
28
  "dependencies": {
29
- "@raredays/components": "^0.1.0",
30
- "@raredays/core": "^0.1.0",
31
- "@raredays/identity": "^0.1.0",
32
- "@raredays/mocks": "^0.1.0",
33
- "@raredays/style": "^0.1.0",
29
+ "@raredays/components": "^0.3.0",
30
+ "@raredays/core": "^0.3.0",
31
+ "@raredays/identity": "^0.1.1",
32
+ "@raredays/mocks": "^0.2.0",
33
+ "@raredays/style": "^0.3.0",
34
34
  "lucide-react": "1.14.0",
35
35
  "msw": "2.11.5",
36
36
  "next": "16.2.9",
@@ -39,12 +39,12 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@atheory-ai/skillex": "0.7.2",
42
- "@raredays/dev": "^0.1.0",
43
- "@raredays/lint": "^0.1.0",
44
- "@raredays/site-config": "^0.1.0",
45
- "@raredays/skills": "^0.1.0",
46
- "@raredays/theme-build": "^0.1.0",
47
- "@raredays/typescript": "^0.1.0",
42
+ "@raredays/dev": "^0.2.0",
43
+ "@raredays/lint": "^0.2.2",
44
+ "@raredays/site-config": "^0.3.0",
45
+ "@raredays/skills": "^0.2.0",
46
+ "@raredays/theme-build": "^0.3.0",
47
+ "@raredays/typescript": "^0.3.0",
48
48
  "@storybook/react-vite": "10.3.6",
49
49
  "@tailwindcss/postcss": "4.2.4",
50
50
  "@tailwindcss/vite": "4.2.4",
@@ -52,13 +52,13 @@
52
52
  "@types/react": "19.2.14",
53
53
  "@types/react-dom": "19.2.3",
54
54
  "@vitejs/plugin-react": "6.0.1",
55
- "@vitest/coverage-v8": "4.1.5",
55
+ "@vitest/coverage-v8": "4.1.9",
56
56
  "happy-dom": "15.11.7",
57
57
  "storybook": "10.3.6",
58
58
  "tailwindcss": "4.2.4",
59
59
  "typescript": "5.9.3",
60
60
  "vite": "8.0.10",
61
- "vitest": "4.1.5"
61
+ "vitest": "4.1.9"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=24"
@@ -1,5 +1,7 @@
1
- /*
2
- * Auto-generated by @raredays/theme-build. Do not edit by hand.
1
+ /**
2
+ * AUTO-GENERATED DO NOT EDIT.
3
+ * Regenerated from site.spec.ts by `pnpm theme:build`.
4
+ * For per-app CSS overrides, edit app/globals.css instead.
3
5
  * Source: site.spec.ts
4
6
  * To regenerate: pnpm theme:build
5
7
  */
@@ -36,8 +36,7 @@ const hasDatabaseUrl = Boolean(process.env.DATABASE_URL);
36
36
  // Dev fallback: bootstrap PGlite eagerly so providers can register
37
37
  // tables at composition-root eval. In prod, or when DATABASE_URL is
38
38
  // set, leave this null and fall through to the lazy Proxy below.
39
- const eagerPglite: Db | null =
40
- !isProduction && !hasDatabaseUrl ? await bootstrapPglite() : null;
39
+ const eagerPglite: Db | null = !isProduction && !hasDatabaseUrl ? await bootstrapPglite() : null;
41
40
 
42
41
  let cached: Db | undefined = eagerPglite ?? undefined;
43
42
 
@@ -60,15 +59,28 @@ async function bootstrapPglite(): Promise<Db> {
60
59
  // you need the full migration set in dev, point DATABASE_URL at a
61
60
  // local Postgres and run `pnpm db:migrate`.
62
61
  const statements: readonly string[] = [
63
- `CREATE TABLE IF NOT EXISTS audit_events (
64
- id text PRIMARY KEY,
65
- organization_id text NOT NULL,
66
- actor_id text,
67
- action text NOT NULL,
68
- resource_type text NOT NULL,
69
- resource_id text,
70
- payload jsonb NOT NULL DEFAULT '{}'::jsonb,
71
- created_at timestamptz NOT NULL DEFAULT now()
62
+ // Audit substrate — `audit_entries` is the ONE audit table (S2a / D3). The
63
+ // old `audit_events` table was retired: `@raredays/audit-drizzle` persists
64
+ // `audit_entries`, which matches the core `AuditProvider` port. If you hold
65
+ // legacy `audit_events` rows, backfill them into `audit_entries` before
66
+ // dropping (see the @raredays/postgres CHANGELOG / migration M8-backfill).
67
+ `DO $$ BEGIN
68
+ CREATE TYPE audit_actor_type AS ENUM ('user', 'system', 'api-key');
69
+ EXCEPTION WHEN duplicate_object THEN null; END $$`,
70
+ `CREATE TABLE IF NOT EXISTS audit_entries (
71
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
72
+ organization_id uuid NOT NULL,
73
+ actor_id text NOT NULL,
74
+ actor_type audit_actor_type NOT NULL,
75
+ action varchar(255) NOT NULL,
76
+ target_type varchar(255) NOT NULL,
77
+ target_id text NOT NULL,
78
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
79
+ ip_address varchar(45),
80
+ user_agent text,
81
+ timestamp timestamptz NOT NULL DEFAULT now(),
82
+ created_at timestamptz NOT NULL DEFAULT now(),
83
+ updated_at timestamptz NOT NULL DEFAULT now()
72
84
  )`,
73
85
  `CREATE TABLE IF NOT EXISTS webhook_deliveries (
74
86
  id text PRIMARY KEY,
@@ -82,17 +94,35 @@ async function bootstrapPglite(): Promise<Db> {
82
94
  created_at timestamptz NOT NULL DEFAULT now(),
83
95
  updated_at timestamptz NOT NULL DEFAULT now()
84
96
  )`,
97
+ // Jobs substrate — append-only log + tenant-scoped dedup markers (S2a / D2).
98
+ // `job_executions` carries NO unique index (every status transition is an
99
+ // insert); dedup-of-record lives in `job_dedup_markers`.
100
+ `DO $$ BEGIN
101
+ CREATE TYPE job_execution_status AS ENUM
102
+ ('enqueued', 'running', 'succeeded', 'failed', 'skipped');
103
+ EXCEPTION WHEN duplicate_object THEN null; END $$`,
85
104
  `CREATE TABLE IF NOT EXISTS job_executions (
86
- id text PRIMARY KEY,
87
- organization_id text NOT NULL,
88
- job_name text NOT NULL,
89
- status text NOT NULL DEFAULT 'pending',
90
- payload jsonb NOT NULL DEFAULT '{}'::jsonb,
91
- result jsonb,
92
- error text,
93
- scheduled_at timestamptz NOT NULL DEFAULT now(),
94
- started_at timestamptz,
95
- completed_at timestamptz
105
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
106
+ organization_id uuid NOT NULL,
107
+ created_at timestamptz NOT NULL DEFAULT now(),
108
+ updated_at timestamptz NOT NULL DEFAULT now(),
109
+ job_name varchar(64) NOT NULL,
110
+ idempotency_key varchar(128),
111
+ status job_execution_status NOT NULL,
112
+ attempts integer NOT NULL DEFAULT 0,
113
+ payload jsonb NOT NULL,
114
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
115
+ reason text,
116
+ error text
117
+ )`,
118
+ `CREATE TABLE IF NOT EXISTS job_dedup_markers (
119
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
120
+ organization_id uuid NOT NULL,
121
+ created_at timestamptz NOT NULL DEFAULT now(),
122
+ updated_at timestamptz NOT NULL DEFAULT now(),
123
+ job_name varchar(64) NOT NULL,
124
+ idempotency_key varchar(128) NOT NULL,
125
+ CONSTRAINT job_dedup_markers_key_idx UNIQUE (organization_id, job_name, idempotency_key)
96
126
  )`,
97
127
  ];
98
128
  for (const stmt of statements) {
@@ -32,14 +32,14 @@
32
32
  "check": "raredays check && tsc --noEmit"
33
33
  },
34
34
  "dependencies": {
35
- "@raredays/components": "^0.1.0",
36
- "@raredays/core": "^0.1.0",
37
- "@raredays/identity": "^0.1.0",
38
- "@raredays/mocks": "^0.1.0",
39
- "@raredays/postgres": "^0.1.0",
40
- "@raredays/postgres-neon": "^0.1.0",
41
- "@raredays/postgres-pglite": "^0.1.0",
42
- "@raredays/style": "^0.1.0",
35
+ "@raredays/components": "^0.3.0",
36
+ "@raredays/core": "^0.3.0",
37
+ "@raredays/identity": "^0.1.1",
38
+ "@raredays/mocks": "^0.2.0",
39
+ "@raredays/postgres": "^0.3.0",
40
+ "@raredays/postgres-neon": "^0.2.1",
41
+ "@raredays/postgres-pglite": "^0.2.1",
42
+ "@raredays/style": "^0.3.0",
43
43
  "drizzle-orm": "0.45.2",
44
44
  "lucide-react": "1.14.0",
45
45
  "msw": "2.11.5",
@@ -49,12 +49,12 @@
49
49
  },
50
50
  "devDependencies": {
51
51
  "@atheory-ai/skillex": "0.7.2",
52
- "@raredays/dev": "^0.1.0",
53
- "@raredays/lint": "^0.1.0",
54
- "@raredays/site-config": "^0.1.0",
55
- "@raredays/skills": "^0.1.0",
56
- "@raredays/theme-build": "^0.1.0",
57
- "@raredays/typescript": "^0.1.0",
52
+ "@raredays/dev": "^0.2.0",
53
+ "@raredays/lint": "^0.2.2",
54
+ "@raredays/site-config": "^0.3.0",
55
+ "@raredays/skills": "^0.2.0",
56
+ "@raredays/theme-build": "^0.3.0",
57
+ "@raredays/typescript": "^0.3.0",
58
58
  "@tailwindcss/postcss": "4.2.4",
59
59
  "@types/node": "22.19.17",
60
60
  "@types/react": "19.2.14",
@@ -62,14 +62,14 @@
62
62
  "@storybook/react-vite": "10.3.6",
63
63
  "@tailwindcss/vite": "4.2.4",
64
64
  "@vitejs/plugin-react": "6.0.1",
65
- "@vitest/coverage-v8": "4.1.5",
65
+ "@vitest/coverage-v8": "4.1.9",
66
66
  "drizzle-kit": "0.31.10",
67
67
  "happy-dom": "15.11.7",
68
68
  "storybook": "10.3.6",
69
69
  "tailwindcss": "4.2.4",
70
70
  "typescript": "5.9.3",
71
71
  "vite": "8.0.10",
72
- "vitest": "4.1.5"
72
+ "vitest": "4.1.9"
73
73
  },
74
74
  "engines": {
75
75
  "node": ">=24"
@@ -1,5 +1,7 @@
1
- /*
2
- * Auto-generated by @raredays/theme-build. Do not edit by hand.
1
+ /**
2
+ * AUTO-GENERATED DO NOT EDIT.
3
+ * Regenerated from site.spec.ts by `pnpm theme:build`.
4
+ * For per-app CSS overrides, edit app/globals.css instead.
3
5
  * Source: site.spec.ts
4
6
  * To regenerate: pnpm theme:build
5
7
  */
@@ -22,9 +22,9 @@
22
22
  "check": "raredays check && tsc --noEmit"
23
23
  },
24
24
  "dependencies": {
25
- "@raredays/components": "^0.1.0",
26
- "@raredays/core": "^0.1.0",
27
- "@raredays/style": "^0.1.0",
25
+ "@raredays/components": "^0.3.0",
26
+ "@raredays/core": "^0.3.0",
27
+ "@raredays/style": "^0.3.0",
28
28
  "lucide-react": "1.14.0",
29
29
  "next": "16.2.9",
30
30
  "react": "19.2.7",
@@ -32,12 +32,12 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@atheory-ai/skillex": "0.7.2",
35
- "@raredays/dev": "^0.1.0",
36
- "@raredays/lint": "^0.1.0",
37
- "@raredays/site-config": "^0.1.0",
38
- "@raredays/skills": "^0.1.0",
39
- "@raredays/theme-build": "^0.1.0",
40
- "@raredays/typescript": "^0.1.0",
35
+ "@raredays/dev": "^0.2.0",
36
+ "@raredays/lint": "^0.2.2",
37
+ "@raredays/site-config": "^0.3.0",
38
+ "@raredays/skills": "^0.2.0",
39
+ "@raredays/theme-build": "^0.3.0",
40
+ "@raredays/typescript": "^0.3.0",
41
41
  "@storybook/react-vite": "10.3.6",
42
42
  "@tailwindcss/postcss": "4.2.4",
43
43
  "@tailwindcss/vite": "4.2.4",