saasaloy 0.5.2 → 0.6.1

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.
Files changed (84) hide show
  1. package/README.md +7 -7
  2. package/dist/index.js +637 -522
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/schemas/registry-item.schema.json +31 -1
  6. package/templates/base/AGENTS.md +204 -89
  7. package/templates/base/README.md +20 -8
  8. package/templates/base/_agents/skills/saasaloy-design/SKILL.md +38 -17
  9. package/templates/base/_agents/skills/saasaloy-env/SKILL.md +130 -0
  10. package/templates/base/_agents/skills/saasaloy-landing-copy/SKILL.md +63 -46
  11. package/templates/base/_agents/skills/saasaloy-setup/SKILL.md +1 -1
  12. package/templates/base/_gitignore +0 -3
  13. package/templates/base/_saasaloy-base.json +8 -1
  14. package/templates/base/{DESIGN.md → apps/web/DESIGN.md} +36 -12
  15. package/templates/base/apps/web/astro.config.mjs +36 -6
  16. package/templates/base/apps/web/package.json +2 -1
  17. package/templates/base/apps/web/src/env.ts +41 -0
  18. package/templates/base/apps/web/src/layouts/Layout.astro +39 -11
  19. package/templates/base/apps/web/src/pages/index.astro +33 -24
  20. package/templates/base/apps/web/src/pages/privacy.astro +10 -2
  21. package/templates/base/apps/web/src/pages/terms.astro +10 -2
  22. package/templates/base/oxlint.config.mjs +125 -4
  23. package/templates/base/package.json +7 -3
  24. package/templates/base/packages/env/_env.example +54 -0
  25. package/templates/base/packages/env/package.json +35 -0
  26. package/templates/base/packages/env/scripts/env-setup.ts +87 -0
  27. package/templates/base/packages/env/src/check.test.ts +41 -0
  28. package/templates/base/packages/env/src/check.ts +50 -0
  29. package/templates/base/packages/env/src/define.test.ts +206 -0
  30. package/templates/base/packages/env/src/define.ts +315 -0
  31. package/templates/base/packages/env/src/deploy.test.ts +438 -0
  32. package/templates/base/packages/env/src/deploy.ts +518 -0
  33. package/templates/base/packages/env/src/dotenv.test.ts +179 -0
  34. package/templates/base/packages/env/src/dotenv.ts +173 -0
  35. package/templates/base/packages/env/src/env-file.ts +51 -0
  36. package/templates/base/packages/env/src/error.ts +38 -0
  37. package/templates/base/packages/env/src/example.test.ts +86 -0
  38. package/templates/base/packages/env/src/example.ts +90 -0
  39. package/templates/base/packages/env/src/index.ts +15 -0
  40. package/templates/base/packages/env/src/schema.ts +63 -0
  41. package/templates/base/packages/env/src/services.test.ts +83 -0
  42. package/templates/base/packages/env/src/services.ts +110 -0
  43. package/templates/base/packages/env/src/setup.test.ts +254 -0
  44. package/templates/base/packages/env/src/setup.ts +177 -0
  45. package/templates/base/packages/env/src/source.ts +20 -0
  46. package/templates/base/packages/env/src/sources.test.ts +137 -0
  47. package/templates/base/packages/env/src/sources.ts +93 -0
  48. package/templates/base/packages/env/src/zod.ts +8 -0
  49. package/templates/base/packages/env/tsconfig.json +13 -0
  50. package/templates/base/packages/tsconfig/base.json +7 -0
  51. package/templates/base/packages/ui/components.json +1 -1
  52. package/templates/base/packages/ui/package.json +4 -2
  53. package/templates/base/packages/ui/src/blocks/audience.tsx +73 -0
  54. package/templates/base/packages/ui/src/blocks/cta.tsx +41 -30
  55. package/templates/base/packages/ui/src/blocks/error-state.tsx +2 -0
  56. package/templates/base/packages/ui/src/blocks/faq.tsx +64 -36
  57. package/templates/base/packages/ui/src/blocks/feature-grid.tsx +48 -146
  58. package/templates/base/packages/ui/src/blocks/footer.tsx +129 -70
  59. package/templates/base/packages/ui/src/blocks/hero.tsx +183 -26
  60. package/templates/base/packages/ui/src/blocks/how-it-works.tsx +75 -0
  61. package/templates/base/packages/ui/src/blocks/logo-cloud.tsx +118 -0
  62. package/templates/base/packages/ui/src/blocks/navbar.tsx +204 -88
  63. package/templates/base/packages/ui/src/blocks/pain-points.tsx +131 -0
  64. package/templates/base/packages/ui/src/blocks/pricing-table.tsx +112 -94
  65. package/templates/base/packages/ui/src/blocks/showcase.tsx +185 -0
  66. package/templates/base/packages/ui/src/blocks/testimonials.tsx +85 -0
  67. package/templates/base/packages/ui/src/blocks/theme-toggle.tsx +16 -12
  68. package/templates/base/packages/ui/src/components/badge.tsx +1 -1
  69. package/templates/base/packages/ui/src/components/button.tsx +2 -2
  70. package/templates/base/packages/ui/src/components/tabs.tsx +1 -1
  71. package/templates/base/packages/ui/src/containers/README.md +6 -5
  72. package/templates/base/packages/ui/src/content/landing.ts +590 -125
  73. package/templates/base/packages/ui/src/index.ts +6 -2
  74. package/templates/base/packages/ui/src/lib/external-link.ts +36 -0
  75. package/templates/base/packages/ui/src/lib/icons.ts +131 -0
  76. package/templates/base/packages/ui/src/lib/inline-code.ts +29 -0
  77. package/templates/base/packages/ui/src/lib/theme.ts +140 -56
  78. package/templates/base/packages/ui/src/styles/globals.css +47 -125
  79. package/templates/base/packages/ui/src/styles/web.css +359 -0
  80. package/templates/base/pnpm-workspace.yaml +0 -6
  81. package/templates/base/saasaloy.json +1 -0
  82. package/templates/base/stylelint.config.js +3 -3
  83. package/templates/base/turbo.json +13 -0
  84. package/templates/base/vitest.config.ts +21 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: saasaloy-design
3
- description: Keep DESIGN.md true of the Saasaloy design layer. Use theme to choose and apply a registry style, update after packages/ui changes, or audit the design contract for token drift.
3
+ description: Keep apps/web/DESIGN.md, and apps/admin/DESIGN.md when the admin app is installed, true of the Saasaloy design layer. Use theme to choose and apply a registry style to the web app, update after packages/ui changes, or audit a design contract for token drift.
4
4
  derived-from: "designkit (MIT), narrowed 2026-08-09"
5
5
  ---
6
6
 
@@ -8,7 +8,16 @@ derived-from: "designkit (MIT), narrowed 2026-08-09"
8
8
 
9
9
  This skill records the design system that the project already uses. It writes no components and no pages. Use `uikit` for that work.
10
10
 
11
- The token home is `packages/ui/src/styles/globals.css`. The design contract is `DESIGN.md` at the project root.
11
+ Each app has its own token home and its own contract:
12
+
13
+ | App | Token home | Contract |
14
+ |-----|------------|----------|
15
+ | web | `packages/ui/src/styles/web.css` | `apps/web/DESIGN.md` |
16
+ | admin, when installed | `packages/ui/src/styles/admin.css` | `apps/admin/DESIGN.md` |
17
+
18
+ Both theme files import `packages/ui/src/styles/globals.css`, which holds the shared Tailwind setup and no token values. Ask which app when the user does not say, and work on one contract at a time. Never read one app's tokens into the other app's contract.
19
+
20
+ The admin contract also covers the admin primitive set in `packages/ui/src/components/admin/`, its blocks in `packages/ui/src/blocks/admin/` and its containers in `packages/ui/src/containers/admin/`. The web contract covers the base `components/`, the other `blocks/` and the web app.
12
21
 
13
22
  The grounding rule controls every mode. Every token value must exist in the project code or in the Tailwind utility that the project uses. Do not choose a nearby value because it looks cleaner.
14
23
 
@@ -24,15 +33,17 @@ If the user does not name a mode, ask which mode to use. Do not choose between a
24
33
 
25
34
  | Mode | Files |
26
35
  |------|-------|
27
- | `theme` | `packages/ui/src/styles/globals.css`, `DESIGN.md`, and `docs/product-brief.md` only when the brief is absent |
28
- | `update` | `DESIGN.md` only |
36
+ | `theme` | web only: `packages/ui/src/styles/web.css`, `apps/web/DESIGN.md`, and `docs/product-brief.md` only when the brief is absent |
37
+ | `update` | the chosen app's `DESIGN.md` only |
29
38
  | `audit` | none |
30
39
 
40
+ `theme` never runs for the admin app. A preset targets `web.css` through `components.json`, and nothing writes to `admin.css` but a person. When the user asks to restyle the admin app, say so, and offer `update` after they edit `admin.css`.
41
+
31
42
  Do not edit `components.json` during a theme change. Do not add a dependency, script, component, page, or Tailwind class.
32
43
 
33
44
  ## Shared rules
34
45
 
35
- Read `DESIGN.md`, `packages/ui/src/styles/globals.css`, and all files under `packages/ui/src/` before you derive tokens. Read the application files that use `@repo/ui` when you need to confirm usage.
46
+ Read the chosen app's `DESIGN.md` and theme file, `packages/ui/src/styles/globals.css`, and the files under `packages/ui/src/` that the app renders before you derive tokens. Read the application files that use `@repo/ui` when you need to confirm usage.
36
47
 
37
48
  Find the system in this order. Stop at the first match.
38
49
 
@@ -50,42 +61,52 @@ Write elevation as prose under `## Elevation & Depth`. Never add `elevation` to
50
61
 
51
62
  Write motion as prose under `## Motion`. The alpha schema has no motion token group and no component transition token.
52
63
 
64
+ The landing page's layout, type and motion vocabulary lives at the end of `web.css`, after the colour tokens, and `DESIGN.md` must name all of it:
65
+
66
+ - The type ramp in `@theme`: `--text-display`, `--text-section-title` and `--text-lead`. Record the largest size of each fluid `clamp()` step as the token's `fontSize`, and describe the fluid range in the Typography prose.
67
+ - The container widths in `@theme` (`--container-page`, `--container-narrow`, `--container-wide`) and the `section-container*` and `section-y` utilities. Describe them under `## Layout`. Spacing stays omitted, because these are named widths and one section rhythm, not a spacing scale.
68
+ - The motion utilities: `blur-in`, `beam-in`, `reveal`, `marquee`, `shine-text`, `nav-float` and `hover-lift`. Describe each under `## Motion`, and say that each one stops under reduced motion.
69
+ - The decoration utility `grid-pattern`. Describe it under `## Components` with the landing blocks it decorates.
70
+
71
+ A theme preset replaces the colour tokens and leaves this vocabulary alone. After a `theme` run, check that the vocabulary survived and that `DESIGN.md` still describes it.
72
+
53
73
  Use only these component token keys: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, and `width`.
54
74
 
55
75
  Keep the sections in this order: Overview, Colors, Typography, Layout, Elevation & Depth, Shapes, Components, Do's and Don'ts, Motion, Dark Mode.
56
76
 
57
- The seed is a one-time base file. ADR 0022 means Saasaloy has no update path for it. The fingerprint detects drift, and this skill repairs the contract.
77
+ The web contract is a one-time base seed. ADR 0022 means Saasaloy has no update path for it. The admin contract is an admin module file, so `saasaloy update` lists an edited copy in its merge plan. In both cases the fingerprint detects drift, and this skill repairs the contract.
58
78
 
59
79
  ## Fingerprint
60
80
 
61
- The fingerprint covers only `packages/ui/src/styles/globals.css`. It does not cover components, blocks, or other files.
81
+ The fingerprint covers only the chosen app's theme file: `packages/ui/src/styles/web.css` for the web contract, `packages/ui/src/styles/admin.css` for the admin contract. It does not cover `globals.css`, components, blocks, or other files. The commands and stamps below name `web.css`; substitute `admin.css` for the admin contract.
62
82
 
63
83
  Compute the fingerprint from the file bytes. Use the first 12 lowercase hexadecimal characters of SHA-256.
64
84
 
65
85
  ```sh
66
- node --input-type=module -e "import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; process.stdout.write(createHash('sha256').update(readFileSync('packages/ui/src/styles/globals.css')).digest('hex').slice(0, 12))"
86
+ node --input-type=module -e "import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; process.stdout.write(createHash('sha256').update(readFileSync('packages/ui/src/styles/web.css')).digest('hex').slice(0, 12))"
67
87
  ```
68
88
 
69
89
  The scaffold seed ends with this stamp.
70
90
 
71
91
  ```markdown
72
- _Seeded from the saasaloy base template · CLI <version> · tokens sha256:<12 hex> of packages/ui/src/styles/globals.css_
92
+ _Seeded from the saasaloy base template · CLI <version> · tokens sha256:<12 hex> of packages/ui/src/styles/web.css_
73
93
  ```
74
94
 
75
95
  After `theme` or `update`, replace the seed stamp with this stamp.
76
96
 
77
97
  ```markdown
78
- _Updated from packages/ui on YYYY-MM-DD · tokens sha256:<12 hex> of packages/ui/src/styles/globals.css_
98
+ _Updated from packages/ui on YYYY-MM-DD · tokens sha256:<12 hex> of packages/ui/src/styles/web.css_
79
99
  ```
80
100
 
81
101
  Do not change the fingerprint during `audit`.
82
102
 
83
103
  ## Official linter
84
104
 
85
- Run the official linter after each write.
105
+ Run the official linter after each write, on the contract you wrote.
86
106
 
87
107
  ```sh
88
- pnpm dlx @google/design.md lint DESIGN.md
108
+ pnpm dlx @google/design.md lint apps/web/DESIGN.md
109
+ pnpm dlx @google/design.md lint apps/admin/DESIGN.md
89
110
  ```
90
111
 
91
112
  The format is alpha. Read the current schema when a lint finding conflicts with this skill.
@@ -126,13 +147,13 @@ Use the package-local executable from the project root.
126
147
  pnpm --filter @repo/ui exec shadcn add <registry-style-url>
127
148
  ```
128
149
 
129
- Confirm that the `@source` rules, `@custom-variant dark`, and `@layer base` remain in `globals.css`. Confirm that `components.json` did not change.
150
+ `components.json` points shadcn at `web.css`, so the preset merges into the `:root`, `.dark` and `@theme inline` blocks there. Confirm that `globals.css` did not change and still holds the `@source` rules and `@layer base`. Confirm that `web.css` and `globals.css` hold one `@theme inline` and one `@custom-variant dark` between them. Confirm that `components.json` did not change.
130
151
 
131
152
  Stop and report the failure when the preset removes a required rule or changes `components.json`. Do not repair an unknown preset merge without approval.
132
153
 
133
154
  ### 4. Re-derive the contract
134
155
 
135
- Run the shared extraction process against the merged `globals.css` and its usage. Update the token front matter and only the prose sections that the new tokens affect.
156
+ Run the shared extraction process against the merged `web.css` and its usage. Update the token front matter and only the prose sections that the new tokens affect.
136
157
 
137
158
  Use the product brief to update Overview and Do's and Don'ts. Do not add product claims that the brief does not support.
138
159
 
@@ -168,7 +189,7 @@ Audit is read-only. Do not edit or format any file.
168
189
 
169
190
  ### 1. Check the fingerprint offline
170
191
 
171
- Read the recorded 12-character fingerprint from the final stamp. Compute the current fingerprint from `globals.css`.
192
+ Read the recorded 12-character fingerprint from the final stamp. Compute the current fingerprint from the chosen app's theme file, `web.css` or `admin.css`.
172
193
 
173
194
  - `current` means the two fingerprints match.
174
195
  - `stale` means they differ.
@@ -182,7 +203,7 @@ Use `orphaned` for a documented token with no current source. Use `uncovered` fo
182
203
 
183
204
  ### 3. Lint when possible
184
205
 
185
- Run `pnpm dlx @google/design.md lint DESIGN.md` when the tool and network are available. If it cannot run, state that the fingerprint check ran offline and structural lint did not run.
206
+ Run `pnpm dlx @google/design.md lint apps/<app>/DESIGN.md` for the chosen app when the tool and network are available. If it cannot run, state that the fingerprint check ran offline and structural lint did not run.
186
207
 
187
208
  ### 4. Report
188
209
 
@@ -192,7 +213,7 @@ Open with the file count that the audit scanned. Give one verdict for the finger
192
213
 
193
214
  - Never write a component or a page. Use `uikit` for that work.
194
215
  - Never add a token value that the project does not contain.
195
- - Never change `globals.css` in `update` or `audit`.
216
+ - Never change `globals.css` in any mode, or `web.css` in `update` or `audit`.
196
217
  - Never write during `audit`.
197
218
  - Never claim that lint passed when the linter did not run.
198
219
  - Never treat a matching fingerprint as proof that component usage has no drift.
@@ -0,0 +1,130 @@
1
+ ---
2
+ name: saasaloy-env
3
+ description: Runbook for the env capability in packages/env — the one tracked key list, the pnpm env:setup distribution command, the pnpm deploy:production deploy with env-deploy, and the typed createEnv gate each service reads its values through. Use when deploying to Cloudflare, when a deploy stops naming a blank production key, adding an environment key, when a build or a request fails naming an unset key, when choosing which services read a key, when a value must stay blank, when a key is checkout-owned, when picking a value source, or when a leftover .dev.vars is hiding .env from wrangler.
4
+ ---
5
+
6
+ # env — one key list, one command, one typed gate
7
+
8
+ `packages/env` (`@repo/env`) owns both halves of this project's environment story.
9
+
10
+ The **node half** is a tracked key list (`packages/env/.env.example`), a gitignored value file (`packages/env/.env`), and `pnpm env:setup`, which writes every service's own `.env` from the two. The **runtime half** is `createEnv`, which validates the values a service actually got and hands back typed ones.
11
+
12
+ A *service* is a workspace that reads a `.env`: `api`, `web`, `admin`. The name is the workspace directory. `packages/env/src/services.ts` holds the table, and `saasaloy add api` appends a row to it. One row has no workspace and no file: `deploy` holds `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`, which a deploy loads in memory.
13
+
14
+ ## Adding a key, in two steps
15
+
16
+ **1. Add its line to `packages/env/.env.example`**, under the `# @services` section naming every service that reads it. Write a comment above it saying where the value comes from and what unset looks like. Put the local development default on the line.
17
+
18
+ ```
19
+ # @services api
20
+
21
+ # Sender address every outbound message uses. The domain must be one the selected
22
+ # provider may send from. Unset, the first send throws naming this key.
23
+ EMAIL_FROM=dev@localhost
24
+ ```
25
+
26
+ A key with no `# @services` line above it fails `pnpm env:setup`. That is deliberate: a key that reaches no service is a key the Worker throws on, and failing here is the cheaper place to find out.
27
+
28
+ **2. Declare it in the preset** of the package that reads it, so `createEnv` validates it and the type reaches the calling code.
29
+
30
+ ```ts
31
+ // packages/email/src/env-preset.ts
32
+ export const emailPreset = definePreset({
33
+ module: "@repo/email",
34
+ extends: [],
35
+ server: {
36
+ EMAIL_FROM: z.string().min(1).describe("Sender address every message uses."),
37
+ },
38
+ });
39
+ ```
40
+
41
+ The `.describe()` text is what the aggregate error prints, so write it for the person reading a failed deploy.
42
+
43
+ ## Reading a key
44
+
45
+ On the server, pass the Worker's `env`:
46
+
47
+ ```ts
48
+ import { apiEnv } from "@api/env";
49
+
50
+ app.get("/x", (c) => {
51
+ const { EMAIL_FROM } = apiEnv(c.env);
52
+ });
53
+ ```
54
+
55
+ In a frontend, call it with nothing — the values are inlined at build time:
56
+
57
+ ```ts
58
+ import { webEnv } from "@web/env";
59
+
60
+ const base = webEnv().PUBLIC_API_URL;
61
+ ```
62
+
63
+ `createEnv` validates on the **first call per `env` object** and memoizes the result in a `WeakMap`. A Worker cannot see its `env` at module scope, so the first request is the earliest honest point to check. One throw then names every failing key at once, with the module that declared it and that module's own wording, so a deploy with three unset keys takes one round trip to fix.
64
+
65
+ ## Server, client, shared
66
+
67
+ - `server` — read only where `isServer` is true. A Worker, a build script.
68
+ - `client` — inlined into a public bundle. **Must** start with `PUBLIC_`, and the type system says so: a mis-prefixed key does not compile.
69
+ - `shared` — read on both sides.
70
+
71
+ `apps/web/src/env.ts` and `apps/admin/src/env.ts` set `isServer: false`, which arms a Proxy: reading a server key from client code throws by name. `typeof window === "undefined"` is not used, because it is true in a Worker *and* in a browser bundle before hydration.
72
+
73
+ The `runtimeEnvStrict` map in those files is hand-written, one literal `import.meta.env.PUBLIC_X` per key. Vite inlines a literal member expression and nothing else, so a spread, a loop or a computed key compiles to `undefined` in the built bundle and fails only in production. **Add the line by hand when you add a client key.**
74
+
75
+ ## Blank on purpose
76
+
77
+ `pnpm env:setup` fails on a key the list carries with no value. When a key is meant to stay empty, say so in its comment block:
78
+
79
+ ```
80
+ # Blank on purpose: unset means no proxy origin, and a link is root-relative.
81
+ STORAGE_PROXY_URL=
82
+ ```
83
+
84
+ ## Checkout-owned keys
85
+
86
+ `CHECKOUT_OWNED_KEYS` in `packages/env/src/services.ts` lists keys this checkout sets for itself. A key named there keeps whatever value is already on disk, whoever wrote it, so a per-branch `DATABASE_URL` survives every `pnpm env:setup`. `packages/env` never learns what a database is — only that some key is the checkout's to set.
87
+
88
+ ## Value sources
89
+
90
+ `pnpm env:setup` reads its values from a source, selected by `ENV_SOURCE`:
91
+
92
+ - **`local`** (the default, always installed) reads the gitignored `packages/env/.env`. `saasaloy env` prompts for each unset key and writes that file, so a project develops with no vendor account and no network.
93
+ - **`infisical`** arrives with `saasaloy add env-infisical`.
94
+
95
+ A source that does not answer at all — no CLI, no network — is treated as offline: `env:setup` keeps every service file that is already complete and warns, and fails writing nothing when one is incomplete. A refusal the source *answered* with, such as no access, always fails.
96
+
97
+ ## Production and `--env prod`
98
+
99
+ Each service row declares `dev` or `prod`. `pnpm env:setup --env prod` refuses to act without `--only <service>`, and refuses outright to write production values into a workspace under `apps/`. Two independent guards on the one command that can put a live credential on disk.
100
+
101
+ `saasaloy env` prints the `wrangler secret put` lines for a deploy. It never runs them, and `pnpm deploy:production` makes them unnecessary.
102
+
103
+ ## Deploying: `pnpm deploy:production` and `env-deploy`
104
+
105
+ `pnpm deploy:production` deploys every Worker in the project with wrangler. It runs in two steps:
106
+
107
+ 1. `node packages/env/src/deploy.ts --check` loads every service's `prod` values from the selected source and exits non-zero, naming each blank key, when one is missing. It also stops when `CLOUDFLARE_API_TOKEN` is blank and wrangler holds no `wrangler login` session. Nothing builds before this passes.
108
+ 2. `turbo run deploy` runs each workspace's `deploy` script, which is `env-deploy <service>`. A workspace whose package depends on `@repo/db` waits for `db:migrate:prod` first.
109
+
110
+ `env-deploy <service>` loads that service's `prod` values and the `deploy` credentials, runs `pnpm run build` with them in the environment, and runs `wrangler deploy --secrets-file <file>`. Code and secrets land in one Worker version, so a Worker that throws at module scope on a missing secret deploys cleanly the first time. The secrets file is JSON, mode `0600`, under the OS temp directory, and deleted when the deploy ends, passed or failed. It holds the service's values minus every `CLOUDFLARE_*` key, minus each key the Worker's `wrangler.jsonc` already sets in `vars`, minus empty values. An assets-only Worker, one with no `main`, gets no secrets file.
111
+
112
+ **Where the production values come from.** The `local` source reads the gitignored `packages/env/.env.prod`, never `packages/env/.env`, so a deploy cannot ship a `localhost` URL by accident. Copy the keys from `packages/env/.env.example` into it and set each one. `env-infisical` reads its `prod` environment. A variable the shell already exports wins over both, which is how CI passes values. The base `turbo.json` sets `passThroughEnv` on `deploy`, because turbo's strict env mode would drop the exported variables otherwise.
113
+
114
+ **Why the dev `.env` stays out of the build.** Vite gives the environment priority over a `.env` file, but the Cloudflare adapters prerender inside workerd, and wrangler hands workerd the `.env` beside the workspace as its vars. So `env-deploy` builds with `CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV=false`, and it sets a key the source leaves out to an empty string rather than leaving it unset. `apps/web/.env` keeps its dev values, and nothing writes a production value under `apps/`.
115
+
116
+ Three more forms:
117
+
118
+ - `pnpm --filter @repo/api run deploy` deploys one Worker. Pass wrangler flags after `--`: `pnpm --filter @repo/web run deploy -- --dry-run` builds with the production values and uploads nothing.
119
+ - `env-deploy --run <service> -- <command> [args...]` runs one command with that service's `prod` values and the credentials. `database-d1`'s `db:migrate:prod` uses it.
120
+ - `env-deploy --check` is step 1 on its own.
121
+
122
+ Two things the flow does not do. `--secrets-file` only adds: a secret you removed from the value source stays on the Worker until you run `wrangler secret delete <KEY>` in its workspace. And a key marked `# Blank on purpose` passes the check even where production needs it, such as `BETTER_AUTH_SECRET`; the Worker's own `createEnv` gate is what refuses it.
123
+
124
+ ## `.dev.vars` is gone
125
+
126
+ This project writes `.env` and nothing else. A leftover `.dev.vars` **hides** `.env` from wrangler — wrangler loads `.env` only while no `.dev.vars` sits beside it, and it does not warn — so `saasaloy doctor` reports one and `pnpm env:setup` carries its values across and deletes it.
127
+
128
+ ## `process.env`
129
+
130
+ No file a Worker imports reads it. The Worker's `env` is the only source, and it arrives through `createEnv`. Node tooling — `scripts/`, `env-deploy`, a value source — reads it freely.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: saasaloy-landing-copy
3
- description: Write the scaffolded landing page's copy from docs/product-brief.md — as a markdown draft the owner reviews first, then into the content module. Use when the landing page still says "Acme" or "The SaaS you meant to build", when the owner asks to write, rewrite, improve or update the landing copy, headline, features, pricing or FAQ, and when re-running after the brief changed. Needs docs/product-brief.md; run saasaloy-setup first if there isn't one.
3
+ description: Write the scaffolded landing page's copy from docs/product-brief.md — as a markdown draft the owner reviews first, then into the content module. Use when the landing page still describes Saasaloy ("Ship the SaaS, not the scaffolding"), when the owner asks to write, rewrite, improve or update the landing copy, headline, features, pricing or FAQ, and when re-running after the brief changed. Needs docs/product-brief.md; run saasaloy-setup first if there isn't one.
4
4
  ---
5
5
 
6
6
  # saasaloy-landing-copy — draft it in markdown, then write the page
@@ -41,12 +41,12 @@ says what to do when they disagree with the brief.
41
41
  an owner ends up answering the same thing twice and getting two different pages.
42
42
  2. **Read `packages/ui/src/content/landing.ts`.** You need the current copy to diff against,
43
43
  and its comments carry the shape rules you have to keep.
44
- 3. **Read the icon registry** at the top of `packages/ui/src/blocks/feature-grid.tsx`.
44
+ 3. **Read the icon registry** in `packages/ui/src/lib/icons.ts`.
45
45
  Reading a block is fine; editing one is not. Its keys are the whole vocabulary available
46
46
  to `landing.features.items[].icon`, and they are the actual list rather than one copied
47
47
  into this file that went stale.
48
48
  4. **Check the two facts you do not own.** If `siteName` in `packages/ui/src/index.ts` is
49
- still the scaffold's directory slug, or `lang` in `apps/web/src/layouts/Layout.astro` is
49
+ still the shipped `"Saasaloy"` or a directory slug, or `lang` in `apps/web/src/layouts/Layout.astro` is
50
50
  `en` while the brief names another language, say so now and carry both into the draft's
51
51
  **Not mine to fix** list. Writing Bangla copy into a page that declares itself English is
52
52
  a real defect, and one attribute long. Do not fix it yourself.
@@ -113,29 +113,29 @@ This file is deleted once the copy lands in `packages/ui/src/content/landing.ts`
113
113
  ## Hero
114
114
  | Key | Now | Proposed |
115
115
  |-----|-----|----------|
116
- | `hero.eyebrow` | Now in early access | In beta with two firms |
116
+ | `hero.announcement` | See what's new | In beta with two firms |
117
117
  | `hero.primaryActionLabel` | Get started | Join the beta |
118
118
 
119
119
  `hero.title`:
120
- > ~~The SaaS you meant to build, already scaffolded.~~
120
+ > ~~Ship the SaaS, not the scaffolding.~~
121
121
  > **Close the month without four spreadsheets per client.**
122
122
 
123
123
  *From the brief: the problem line, and "Excel plus a shared Dropbox folder" as the
124
- alternative. The eyebrow uses the two named beta firms rather than a stage name.*
124
+ alternative. The announcement uses the two named beta firms rather than a stage name.*
125
125
 
126
126
  ## Features
127
127
  | # | Icon | Title | About |
128
128
  |---|------|-------|-------|
129
- | 1 | `list-checks` (was `zap`) | One close, one checklist | … |
129
+ | 1 | `list-checks` (was `terminal`) | One close, one checklist | … |
130
130
 
131
131
  ## Where the buttons go
132
132
  | Key | Now | Proposed |
133
133
  |-----|-----|----------|
134
- | `navbar.ctaHref` | `#cta` | https://ledgerly.com.bd/waitlist |
135
- | `cta.primaryActionHref` | `/` | https://ledgerly.com.bd/waitlist |
134
+ | `navbar.ctaHref` | Saasaloy's getting started guide | https://ledgerly.com.bd/waitlist |
135
+ | `cta.primaryActionHref` | Saasaloy's getting started guide | https://ledgerly.com.bd/waitlist |
136
136
 
137
137
  ## Least sure about
138
- - The eyebrow names your two beta firms. Fine to say publicly?
138
+ - The announcement names your two beta firms. Fine to say publicly?
139
139
  - FAQ 4 says data exports as CSV. The brief doesn't mention exports; I inferred it.
140
140
 
141
141
  ## Not mine to fix
@@ -170,57 +170,70 @@ than doubling it.
170
170
 
171
171
  | Key | What goes there |
172
172
  |-----|-----------------|
173
- | `meta.title` | Browser tab. `{siteName} — <what it does>`, under 60 characters. |
173
+ | `meta.title` | Browser tab and link preview. `{siteName}: <what it does>`, under 60 characters. |
174
174
  | `meta.description` | Search result and link preview. One sentence, under 155 characters. |
175
- | `navbar.linkFeatures` `.linkPricing` `.linkFaq` | Nav labels. One or two words. They point at the sections of the same name — keep the meaning, or the anchor lies. |
175
+ | `navbar.linkFeatures` `.linkHowItWorks` `.linkShowcase` `.linkPricing` `.linkFaq` | Nav labels. One or two words. They point at the sections of the same name — keep the meaning, or the anchor lies. An empty label hides the link. |
176
+ | `navbar.secondaryLabel` `.secondaryHref` | The outline button beside the main one, usually docs, source or sign-in. Empty `secondaryHref` hides it. See [Destinations](#destinations). |
176
177
  | `navbar.ctaLabel` `.ctaHref` | Two or three words, a verb first, and where it goes. See [Destinations](#destinations). |
177
- | `hero.eyebrow` | The one-line status above the headline ("Now in early access"). Empty string hides it. |
178
+ | `hero.announcement` `.announcementHref` | The pill above the headline ("Now in early access") and where it links, usually a changelog or launch post. Empty `announcement` hides it. |
178
179
  | `hero.title` | The headline. Under ten words, one idea, concrete. This is the sentence the whole page is judged on. |
179
180
  | `hero.description` | One or two sentences saying what it does for whom. |
180
- | `hero.primaryActionLabel` `.secondaryActionLabel` | Button labels, a verb first. Both scroll down the page; their hrefs are anchors and stay in the block. |
181
+ | `hero.primaryActionLabel` `.primaryActionHref` `.secondaryActionLabel` `.secondaryActionHref` | Button labels, a verb first, and where each goes. See [Destinations](#destinations). |
182
+ | `hero.terminalLabel` `.terminalTitle` `.terminalLines[]` `.treeTitle` `.tree[]` | The hero's product visual: a terminal and a file tree. For a product that is not a developer tool, keep the shipped lines and tell the owner in the draft to replace the visual with a screenshot (the block's header comment shows how). Command lines and paths are code, not prose. |
183
+ | `logos.label` `.items[]` | One line beside the logo row, and `{ id, name, mark }` per logo. `name` is the accessible label; `mark` names an SVG the owner adds to `logo-cloud.tsx`, and an empty `mark` renders `name` as text. Never invent a customer: no logos in the brief means offer to drop the block. |
184
+ | `painPoints.title` `.description` `.intro` `.items[]` | The problem the product solves. Four items by default: `{ id, icon, title, description }`, each a pain the brief's customer actually has. See [Icons](#icons). |
185
+ | `howItWorks.title` `.description` `.steps[]` | Three ordered steps: `{ id, title, description }`. Only for a real sequence a customer follows. |
181
186
  | `features.title` `.description` | The section's heading and one supporting line. |
182
187
  | `features.items[]` | Six by default: `{ id, icon, title, description }`. Title 2–4 words; description one sentence about what the owner can *do*, not about the technology. See [Icons](#icons). |
188
+ | `showcase.title` `.description` `.rows[]` | Alternating rows: `{ id, title, description, points[], visual, imageSrc, imageAlt }`. With real screenshots in `apps/web/public`, set `imageSrc` and a real `imageAlt`; otherwise keep `visual` and its lists, or offer to drop the block. |
189
+ | `audience.title` `.description` `.items[]` | Who it is for. Four items: `{ id, icon, title, description }`, from the brief's customer section. |
190
+ | `testimonials.title` `.description` `.items[]` | `{ id, quote, name, role }`. **Real quotes only**, with the person's permission. With none in the brief, keep the shipped placeholders and offer to drop the block. Never write a quote. |
183
191
  | `pricing.title` `.description` | Heading plus one line. |
184
192
  | `pricing.annualNote` `.currencySymbol` | See [Pricing](#pricing). |
185
- | `pricing.tiers[]` | `{ id, name, description, monthlyPrice, annualPrice, features, ctaLabel, ctaHref }`. Prices are whole units; `null` renders "Custom". Set `featured` on at most one. A tier's `ctaHref` stays `#cta` unless the brief gives that tier its own destination. |
193
+ | `pricing.tiers[]` | `{ id, name, description, monthlyPrice, annualPrice, priceSuffix, features, ctaLabel, ctaHref }`, plus `featured` and `badge` on at most one. Prices are whole units; `null` renders "Custom". An empty `priceSuffix` shows "/month". |
186
194
  | `faq.items[]` | Five by default: `{ id, question, answer }`. Write the questions a buyer asks before paying — pricing, migration, lock-in, data, what happens when they outgrow it. One to three sentences, and answer the question. |
195
+ | `faq.moreLabel` `.moreLinkLabel` `.moreLinkHref` | The line under the list. Empty `moreLinkHref` hides it. |
187
196
  | `cta.title` `.description` | The closing ask, plus one line removing a reason to hesitate. |
188
- | `cta.primaryActionHref` `.secondaryActionHref` | See [Destinations](#destinations). |
197
+ | `cta.command` | A command shown as a code chip. Empty for a product that is not installed from a terminal. |
198
+ | `cta.primaryActionLabel` `.primaryActionHref` `.secondaryActionLabel` `.secondaryActionHref` | See [Destinations](#destinations). |
189
199
  | `footer.tagline` | One line under the brand. |
190
- | `footer.groupProduct` `.groupLegal` `.link*` | Footer labels. One or two words each. |
200
+ | `footer.links[]` `.linkTerms` `.linkPrivacy` | `{ id, label, href }` per outbound link, then the two legal labels. Empty drops a link. |
201
+ | `footer.githubHref` `.xHref` | Social profiles. Empty hides the icon. |
202
+ | `footer.copyright` | Keep `{year}` and `{siteName}` in it. |
191
203
 
192
204
  Three mechanical rules the code depends on:
193
205
 
194
- - **`{siteName}` is the only placeholder, and it works in exactly four strings**:
195
- `meta.title`, `meta.description`, `hero.description`, `cta.description`. Anywhere else it
196
- renders as the literal text `{siteName}`. Never a template literal — copy is data.
197
- - **Feature and FAQ `id`s are stable keys**, not positions. Rewriting a `title` under an
206
+ - **`{siteName}` is the placeholder**, and the blocks fill it in titles, descriptions,
207
+ answers and tier names. Never a template literal — copy is data. `{year}` works only in
208
+ `footer.copyright`. A pair of backticks in a description or answer renders as `<code>`.
209
+ - **Every list item's `id` is a stable key**, not a position. Rewriting a `title` under an
198
210
  existing id is free. Renaming an id is allowed but it is a new string to a translation
199
211
  layer, so do it only when the item genuinely became a different item.
200
- - **Deleting an item is fine**, in any of the three lists. Fewer than six features and fewer
201
- than five questions both lay out correctly.
212
+ - **Deleting an item is fine**, in any list. Fewer features, questions, steps or quotes all
213
+ lay out correctly.
202
214
 
203
215
  ### Icons
204
216
 
205
- Each feature carries an `icon` naming a glyph from the registry at the top of
206
- `packages/ui/src/blocks/feature-grid.tsx`. Rewrite what a feature is about and move its
207
- icon in the same edit — that is the whole reason the name lives in content. A page whose
208
- "practice all four modules" card renders a terminal prompt reads as machine-assembled,
209
- because it was.
217
+ Feature, pain point and audience items carry an `icon` naming a glyph from the registry in
218
+ `packages/ui/src/lib/icons.ts`. Rewrite what an item is about and move its icon in the same
219
+ edit — that is the whole reason the name lives in content. A page whose "practice all four
220
+ modules" card renders a terminal prompt reads as machine-assembled, because it was.
210
221
 
211
222
  - Use a key the registry actually has. Read the map; do not guess from memory. An unknown
212
223
  name silently renders the fallback sparkle, which looks like a choice and is not.
213
224
  - If nothing in the registry fits, say so in the draft and name the lucide icon you would
214
- want. Widening the map is a two-line edit **the owner makes**, in a block, which is not
215
- yours.
225
+ want. Widening the map is a two-line edit **the owner makes**, in a library file, which is
226
+ not yours.
216
227
 
217
228
  ### Destinations
218
229
 
219
- Three of the page's hrefs are content, and they are the ones that leave the page:
220
- `navbar.ctaHref`, `cta.primaryActionHref`, `cta.secondaryActionHref`. The brief's
221
- **Where "sign up" goes** section is where the answer lives.
230
+ Every href that leaves the page is content: the navbar's two buttons, the hero's two
231
+ actions and its announcement, the CTA's two actions, each tier's `ctaHref`, the FAQ's
232
+ contact link and the footer's links and social profiles. The brief's **Where "sign up"
233
+ goes** section is where the main answer lives.
222
234
 
223
- - **A URL in the brief.** Write it into `navbar.ctaHref` and `cta.primaryActionHref`.
235
+ - **A URL in the brief.** Write it into `navbar.ctaHref`, `hero.primaryActionHref` and
236
+ `cta.primaryActionHref`.
224
237
  - **Nothing yet.** Leave them as shipped (`#cta` and `/`) and write labels the page can
225
238
  honestly satisfy — "See how it works" rather than "Start your free trial". Note it in the
226
239
  draft and in the brief's **Known gaps**. A primary button that reloads the page is the
@@ -229,8 +242,12 @@ Three of the page's hrefs are content, and they are the ones that leave the page
229
242
  - **`cta.secondaryActionHref`** is usually docs or an about page. `/` is honest for a
230
243
  one-page site; a label promising docs that do not exist is not.
231
244
 
232
- Everything else stays put. `#features`, `#pricing`, `#faq` and `#cta` are anchors matching
233
- section ids, they live in the blocks, and changing one breaks a link.
245
+ - **The shipped values point at Saasaloy's own repo and docs.** Replace every one. An href
246
+ you cannot fill gets an empty string where the key allows it, which hides the link.
247
+
248
+ Everything else stays put. `#features`, `#how-it-works`, `#showcase`, `#pricing` and
249
+ `#faq` are anchors matching section ids, they live in the blocks, and changing one breaks a
250
+ link.
234
251
 
235
252
  ### Pricing
236
253
 
@@ -324,22 +341,22 @@ sure about, and anything still on the **Not mine to fix** list.
324
341
 
325
342
  ### Dropping a block is its own confirmation
326
343
 
327
- When the brief yields nothing a block can honestly carry — no pricing, no proof for the FAQ
328
- — you may propose removing that block. **Separately.** Never bundled into the copy write,
344
+ When the brief yields nothing a block can honestly carry — no pricing, no customer quotes,
345
+ no customer logos, no screenshots for the showcase, no real sequence for how it works — you
346
+ may propose removing that block. **Separately.** Never bundled into the copy write,
329
347
  never silent.
330
348
 
331
349
  It is three edits, and all three are allowed:
332
350
 
333
- 1. Delete that block's line from `apps/web/src/pages/index.astro`.
351
+ 1. Delete that block's line and its import from `apps/web/src/pages/index.astro`. Tell
352
+ the owner they can delete the block file in `packages/ui/src/blocks/` too; that file is
353
+ not yours to delete.
334
354
  2. Blank the matching label in content (`landing.navbar.linkPricing = ""`,
335
355
  `landing.footer.linkPricing = ""`). A blank label drops the link, so the page keeps no
336
356
  nav entry pointing at a section that is gone.
337
- 3. **Retarget or drop any hero action pointing at the removed section.** A blank label
338
- does not save you here: the hero's `href` lives in the block, so it survives an empty
339
- label and the button still points at nothing. The hero's secondary action ships as
340
- `href: "#pricing"`, so when you remove the pricing block, pass `secondaryAction={null}`
341
- to `<Hero>` in `index.astro` to drop the button, or pass a whole action pointing at a
342
- section that still exists. Confirm which one with the owner along with the removal.
357
+ 3. **Retarget any action pointing at the removed section.** The hero's and the CTA's
358
+ hrefs are content, so check each one in `landing.ts` and point it somewhere that still
359
+ exists. Confirm the new target with the owner along with the removal.
343
360
 
344
361
  Removing a block is not redesigning one. You are editing lines in a page, not editing
345
362
  `packages/ui/src/blocks/`.
@@ -371,7 +388,7 @@ Copy on disk plus a brief means this has run before.
371
388
  - **Never edit a block.** `packages/ui/src/blocks/*.tsx` is off limits: no markup, no
372
389
  classes, no structure, no widening the icon registry. Read them freely. If copy will not
373
390
  fit a block, say so and let the owner decide.
374
- - **Never touch the design layer** — `packages/ui/src/styles/globals.css`,
391
+ - **Never touch the design layer** — `packages/ui/src/styles/*`,
375
392
  `components.json`, `packages/ui/src/components/*`, or any Tailwind class anywhere.
376
393
  - **Never rewrite `ui.*`**, in any language. Translate it or leave it.
377
394
  - **Leave anchors alone.** `#features`, `#pricing`, `#faq`, `#cta` and a tier's default
@@ -262,7 +262,7 @@ can see:
262
262
  - **Never write landing copy.** Not into `packages/ui/src/content/landing.ts`, not into a
263
263
  block, not "just the headline while we're here". The brief is your output.
264
264
  - **Never edit a block or the design layer** — `packages/ui/src/blocks/*`,
265
- `packages/ui/src/styles/globals.css`, `packages/ui/src/components/*`, `components.json`, or a Tailwind class
265
+ `packages/ui/src/styles/*`, `packages/ui/src/components/*`, `components.json`, or a Tailwind class
266
266
  anywhere.
267
267
  - **Never invent pricing, proof, a customer name, or a URL.** Not in the brief, and not in
268
268
  a sample answer.
@@ -14,9 +14,6 @@ pnpm-debug.log*
14
14
  .env
15
15
  .env.*
16
16
  !.env.example
17
- .dev.vars
18
- .dev.vars.*
19
- !.dev.vars.example
20
17
  *.pem
21
18
 
22
19
  # OS / editor
@@ -1,18 +1,25 @@
1
1
  {
2
2
  "seedFiles": [
3
- "DESIGN.md",
3
+ "apps/web/DESIGN.md",
4
4
  "README.md",
5
5
  "saasaloy.json",
6
6
  "apps/web/src/pages/index.astro",
7
7
  "packages/ui/src/content/landing.ts"
8
8
  ],
9
+ "retiredSeedFiles": ["DESIGN.md"],
9
10
  "ownedFiles": [
11
+ "packages/env/.env.example",
12
+ "packages/env/src/services.ts",
13
+ "packages/env/src/sources.ts",
14
+ "apps/web/src/env.ts",
10
15
  "packages/ui/src/styles/globals.css",
16
+ "packages/ui/src/styles/web.css",
11
17
  "packages/ui/src/blocks/**",
12
18
  "packages/ui/src/components/**",
13
19
  "packages/ui/src/containers/**",
14
20
  "packages/ui/src/content/**",
15
21
  "packages/ui/src/index.ts",
22
+ "packages/ui/src/lib/icons.ts",
16
23
  "apps/web/src/layouts/Layout.astro",
17
24
  "apps/web/public/favicon.*"
18
25
  ]