---
name: "Contributing to create-raredays-app"
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.
topics: [scaffolding, contributing]
tags: [scaffolding, cli, contributing]
---

# Contributing to create-raredays-app

Location: `workspaces/app/cli/create-raredays-app/`

## CLI Entry (`bin/index.js`)

Plain Node.js script (no build step). Uses `parseArgs` for flags:

| Flag                 | Default        | Purpose                                                                   |
| -------------------- | -------------- | ------------------------------------------------------------------------- |
| `--template <name>`  | `"full-stack"` | Template to scaffold                                                      |
| `--id-strategy <s>`  | required       | `uuidv7`, `uuidv4` or `text` (ADR-0028)                                   |
| `--preset <name>`    | —              | `courseware`, `internal-tool`, `demo`; overrides `--template`/`--modules` |
| `--modules <list>`   | —              | Comma-separated module keys                                               |
| `--vendors <list>`   | —              | Comma-separated `module:vendor` pairs                                     |
| `--remote-url <url>` | —              | Git remote origin URL                                                     |
| `--no-git`           | —              | Skip git init                                                             |
| `--no-install`       | —              | Skip pnpm install                                                         |
| `--dev`              | —              | Rewrite `@raredays/*` deps to `workspace:*`                               |
| `--harness <name>`   | `none`         | `claude-code`, `cursor`, `windsurf`: wire `@raredays/mcp` into the IDE    |
| `--deploy <target>`  | `none`         | `vercel`: write `vercel.json` and `docs/DEPLOY.md`                        |

### Interactive Mode (no positional arg)

Prompts in order via `@clack/prompts`:

1. Project name (validates non-empty, no existing dir)
2. "What are you building?": a preset (`PRESETS`: courseware / internal-tool / demo) or "Custom". A preset then asks only display name, id strategy (unless its template is `static` or `--id-strategy` is given), deploy target, init git and run install, and takes template, modules, vendors and brand color from the preset.
3. Template selection: `static` | `client` | `full-stack`
4. Id strategy (`promptIdStrategy`, skipped for `static` or when `--id-strategy` is given)
5. Display name (defaults to project name)
6. Canonical domain (validates `/^[a-z0-9.-]+\.[a-z]{2,}$/i`)
7. Brand primary color (hex, defaults `#10b981`, validates `/^#[0-9a-fA-F]{3,8}$/`)
8. Module selection (multiselect, filtered by template's `templates` field)
9. Vendor selection per module (format: `billing:stripe`)
10. Deploy target (`promptDeploy`)
11. Git remote URL (optional)
12. Init git repo (default: true)
13. Run `pnpm install` (default: true)

Non-interactive mode fails before writing anything when a non-`static`
template has no `--id-strategy`, or when its value is not one of
`ID_STRATEGIES` (framework ADR-0028: no default). `ID_STRATEGIES` in
`bin/index.js` must equal `@raredays/core`'s and `@raredays/site-config`'s
`idStrategySchema`; `test/scaffold-id-strategy.test.ts` holds them equal.

### Scaffold Flow

1. Error if target dir exists
2. Copy `templates/{template}/` recursively
3. Rename stub files: `_gitignore` → `.gitignore`, `_npmrc` → `.npmrc`, `_claude` → `.claude`, `_github` → `.github`
4. Derive slug: trim, lowercase, replace `[^a-z0-9]+` with `-`, strip leading/trailing `-`
5. Substitute `__SLUG__` in `site.spec.ts`
6. Apply `customiseSiteSpec()` regex replacements (display name, domain, `idStrategy`, brand color)
7. Load fragments (template-compatibility filtered), merge deps + vendor overlays, copy fragment `files/` dirs, compose runtime
8. Compose mock handlers if template ≠ `static`
9. Merge deps, devDeps and scripts into `package.json`, apply `--dev` rewrite if flagged
10. `applyHarness()` (`--harness`) — add `@raredays/mcp` devDep + write the IDE MCP config (`.mcp.json` at root for Claude Code)
11. `applyDeploy()` (`--deploy vercel`) — write `vercel.json` + `docs/DEPLOY.md`, collect deploy env vars
12. Write `.env.example` only if any vendor/deploy env vars were collected
13. `git init --quiet --initial-branch=main` (+ remote if specified)
14. `pnpm install` with `stdio: "inherit"`

## CRDA Placeholder Tokens

### In `app/lib/runtime.ts`:

| Token (exact string) | Replaced With                                                           | Empty Behavior                                          |
| -------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------- |
| `// CRDA:imports`    | Fragment `runtime/imports.ts` concatenated with newlines                | Kept as-is                                              |
| `// CRDA:config`     | Fragment `runtime/config.ts` concatenated with double newlines          | Comment: `// CRDA:config (no additional modules wired)` |
| `  // CRDA:ports`    | Fragment `runtime/ports.ts` concatenated, trimmed (note 2-space indent) | Kept as-is                                              |
| `// CRDA:exports`    | Fragment `runtime/exports.ts` concatenated with double newlines         | Kept as-is                                              |

### In `mocks/handlers.ts`:

| Token (exact string)     | Replaced With                                           |
| ------------------------ | ------------------------------------------------------- |
| `// CRDA:mocks-imports`  | Import statements: `import { <named> } from <package>;` |
| `  // CRDA:mocks-spread` | Spread entries: `  <named>,` (2-space indent)           |

### In `knip.config.ts`:

| Token (exact string)                    | Replaced With                                         |
| --------------------------------------- | ----------------------------------------------------- |
| `      // CRDA:knip-entry`              | `// <why>` + `"<file>",` per `knip.entry` item        |
| `      // CRDA:knip-ignoreDependencies` | `// <why>` + `"<pkg>",` per `knip.ignoreDependencies` |

Composed right after `applyHarness()`, from the fragment, vendor and harness `knip`
contributions. Kept as-is when nothing adds to that array; `composeKnip` throws if a module adds
to an array whose marker the template lacks (the static template has no entry marker).

## composeRuntime(source, fragments)

Iterates fragments in order, collects all slot code per token, replaces all four tokens in a single pass.

## composeHandlers(source, vendorMocks)

Builds import list + spread list from vendor mock metadata, replaces both tokens.

## customiseSiteSpec(filePath, opts)

Four regex replacements on `site.spec.ts`, each applied only when its option is set:

1. Display name: `/name:\s*"[^"]*"/` → `name: ${JSON.stringify(opts.displayName)}`
2. Canonical domain: `/canonicalDomain:\s*"[^"]*"/` → `canonicalDomain: ${JSON.stringify(opts.canonicalDomain)}`
3. Id strategy: `/idStrategy:\s*"[^"]*"/` → `idStrategy: ${JSON.stringify(opts.idStrategy)}`
4. Brand primary: `/primary:\s*"#[0-9a-fA-F]+"/` → `primary: ${JSON.stringify(opts.brandPrimary)}`

`__SLUG__` token is a separate find-replace earlier in the flow.

## Template File Trees

### `full-stack`

```
app/api/dev/{login,logout}/route.ts, app/api/widgets/route.ts
app/demo/widgets/page.tsx, app/protected/page.tsx
app/globals.css, app/home-auth-controls.tsx, app/layout.tsx, app/page.tsx
app/lib/runtime.ts (CRDA placeholders), app/lib/runtime.test.ts
app/theme.override.css
db/index.ts, db/schema.ts, drizzle.config.ts
instrumentation.ts
mocks/browser.tsx, mocks/handlers.ts (CRDA placeholders), mocks/node.ts
next.config.ts, oxfmt.config.ts, oxlint.config.ts, package.json
postcss.config.mjs, scripts/build-theme.mjs, site.spec.ts
stylelint.config.mjs, tsconfig.json, vitest.config.ts, vitest.setup.ts
```

### `client`

Same as full-stack minus `db/`, `drizzle.config.ts`, `app/api/widgets/`. Adds `app/api/backend/[...path]/route.ts` for upstream proxying and `app/lib/backend-client.ts`.

### `static`

Minimal: no `app/lib/`, no `app/api/`, no `mocks/`, no `instrumentation.ts`, no tests, no DB. Just layout, page, styling, and build config.

## Fragment Structure

```
scaffold-fragments/{key}/
├── module.json          # Manifest
├── runtime/
│   ├── imports.ts       # Injected at // CRDA:imports
│   ├── config.ts        # Injected at // CRDA:config
│   ├── ports.ts         # Injected at   // CRDA:ports
│   └── exports.ts       # Injected at // CRDA:exports
└── files/               # Overlay into target app
```

### module.json Schema

```json
{
  "key": "billing",
  "label": "Billing (subscriptions + webhooks)",
  "description": "...",
  "deps": { "@raredays/billing": "^0.2.0" },
  "vendors": [
    {
      "key": "stripe",
      "label": "Stripe",
      "deps": { "@raredays/billing-stripe": "^0.1.0" },
      "env": ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"],
      "mocks": { "package": "@raredays/billing-stripe/mocks", "named": "stripeMocks" }
    }
  ],
  "templates": ["full-stack"]
}
```

Optional `devDeps` and `scripts` merge into the app's `devDependencies` and
`scripts` (the `email` fragment uses them for `react-email`, `@react-email/ui` and
`email:dev`).

Optional `knip` (on the module or a vendor) lists `entry` files and
`ignoreDependencies` packages with the `why` written above each, e.g.
`"knip": { "why": "...", "entry": ["app/lib/flags.ts"] }`. Arrays, not
`@raredays/*` keys, so the version-pin scripts never rewrite them. Every template runs
`raredays knip` in `pnpm check` and CI, so a fragment that ships a file nothing
imports yet, or a package no shipped file imports, must declare it here or the
new app fails its own gate. The harness adds `@raredays/mcp` the same way.
Nothing runs knip on each module combination: `scaffold-knip.test.ts` checks only
that the contributions land, and the reference run (`scripts/reference.mjs`) runs
knip only on the reference apps. Scaffold a new or changed fragment and run
`pnpm knip` in it before relying on its `knip` field.

### Available Fragments (10)

| Fragment        | Templates          | Vendors       | Env Vars                                     |
| --------------- | ------------------ | ------------- | -------------------------------------------- |
| `billing`       | full-stack         | Stripe        | `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` |
| `cms`           | full-stack, client | Hygraph       | `HYGRAPH_ENDPOINT`, `HYGRAPH_TOKEN`          |
| `email`         | full-stack         | Resend        | `RESEND_API_KEY`                             |
| `feature-flags` | full-stack, client | None          | —                                            |
| `i18n`          | full-stack, client | None          | —                                            |
| `identity`      | full-stack, client | Better Auth   | `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`      |
| `jobs`          | full-stack         | None          | —                                            |
| `notifications` | full-stack         | None          | —                                            |
| `organizations` | full-stack         | None          | —                                            |
| `telemetry`     | full-stack, client | Sentry, Axiom | `SENTRY_DSN`, `AXIOM_TOKEN`, `AXIOM_DATASET` |

Pins in `deps`/`vendors[].deps` are `^<workspace version>` for `@raredays/*`
packages, kept current by `scripts/sync-template-versions.mjs` (see below).

## .env.example Generation

Only written if `envVars.length > 0`. Format:

```
# Environment variables required by the modules you selected.
# Copy to .env.local and fill in real values before running.

# Database (uncomment when wiring Drizzle queries)
# DATABASE_URL=

STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
```

## --dev Flag

Rewrites every `@raredays/*` in both `dependencies` and `devDependencies` to `workspace:*`. Used for in-repo dogfooding (playground scaffolds).

## Version pins (`scripts/sync-template-versions.mjs`)

Templates + fragments pin `@raredays/*` and `bin/index.js` holds `MCP_VERSION`.
These are the framework's public surface and do NOT move when the workspace
releases. Run `pnpm --filter create-raredays-app sync-versions` (also
`prepublishOnly`) to refresh them to `^<workspace version>`; `sync-versions:check`
is the CI dry-run. The guard test `test/template-workspace-pins.test.ts` fails
when a change leaves a pin whose range excludes the current workspace version.
Offenders already present at the merge base with `origin/main` (a release the
sync bot has not re-pinned yet) are tolerated, unless a pending changeset
releases `create-raredays-app`: then every offender fails, so the CLI never
publishes past a stale scaffold except through a re-pin. The package's Nx `test`
target lists `.changeset/*.md` as an input so that rule is never cached away.

The repo-level `scripts/sync-scaffold-versions.mjs` bot re-pins them EXACT on
every release so it always opens a review PR. Exact is an intermediate only:
`prepublishOnly` converts to caret (ADR-0017), and
`test/publish-caret-pins.test.ts` runs that step on a copy, lists the tarball
with `npm pack --dry-run`, and fails if a shipped manifest keeps a non-caret pin.

## Testing

The package ships Vitest guard/unit tests under `test/`:

- **Template lint guards**: every-import-is-declared, secure next/react floors,
  pnpm-11 build approvals, registry auth, `.ts`-import specifiers, skill
  frontmatter (v4), workflow lint, no duplicate migration workflow.
- **`package-skill-frontmatter.test.ts`**: the frontmatter of every package skill
  (`workspaces/**/skillex/**/*.md`, not `*.test.md`) parses as YAML, since skillex
  skips a skill whose frontmatter does not. Those files are `test` inputs.
- **`template-workspace-pins.test.ts`**: `@raredays/*` pins (incl. `MCP_VERSION`)
  resolve the current workspace version; only drift the change introduces fails,
  unless a pending changeset releases the CLI.
- **`publish-caret-pins.test.ts`**: after `prepublishOnly`, every `@raredays/*`
  pin in the packed tarball's template and fragment manifests is caret.
- **`scaffold-email.test.ts`**: the `email` fragment adds `@raredays/email-templates`,
  the `email:dev` previewer, `emails/` and the template-backed senders.
- **`scaffold-smoke.test.ts`**: runs `bin/index.js` into a temp dir
  (`--no-git --no-install --dev`) and asserts the harness config path, merged
  deps, and composed runtime.
- **`scaffold-knip.test.ts`**: modules, vendors and the harness land their `knip`
  contributions in `knip.config.ts`, and a plain scaffold's config stays the template's.
  It does not run knip.
- **`compose-allow-builds.test.ts`**: `mergeAllowBuilds` unit tests.
- **`template-typescript.test.ts`**: the templates pin this package's TypeScript
  (7), set no `baseUrl` (TS5102), pin a Next that type-checks through the tsc CLI
  (16.3+), pin the `next`, `react`, `react-dom` and `@types/node` of
  `@raredays/core`'s devDependencies, and keep the root's `packageExtensions`
  entry (TypeScript 6's compiler API for Storybook docgen) above `allowBuilds`.
  The E2E and reference runners link scaffolds to core's source, so a different
  peer set gives them two Next copies whose config types do not match. Bump
  those pins together with core's.

Full scaffold-build-serve coverage lives in the e2e scenarios (`/e2e/`); use
`pnpm playground:reset && pnpm playground` for manual checks.

## Build and Test

```sh
# No build step needed — plain Node.js script
pnpm --filter create-raredays-app test  # if tests exist
```
