@sidebase/base-config 0.3.0 → 0.3.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.
package/README.md CHANGED
@@ -1,237 +1,161 @@
1
1
  # @sidebase/base-config
2
2
 
3
- Shared `@sidebase` Nuxt configuration, delivered over two channels in one package:
3
+ Shared setup for sidebase Nuxt 4 apps. [`@sidebase/streamctl`](https://github.com/sidebase/streamctl)
4
+ syncs this package's config files (CI, Dockerfile, tsconfig, lint config, editor settings, ...) into your
5
+ repo and keeps them up to date. The package also exports ESLint, Prisma and tsconfig helpers.
4
6
 
5
- - npm channel: the root export `@sidebase/base-config` plus the subpath exports
6
- `@sidebase/base-config/config`, `@sidebase/base-config/eslint`,
7
- `@sidebase/base-config/prisma`, and `@sidebase/base-config/tsconfig.base`.
8
- - file-sync channel: the bundled `presets/` payload read by the [`@sidebase/streamctl`](https://github.com/sidebase/streamctl) CLI.
7
+ ## Quick start
9
8
 
10
- ## Install
11
-
12
- For the npm channel, install the package plus what the subpaths you use require:
13
-
14
- ```sh
15
- pnpm add -D @sidebase/base-config eslint jiti
16
- ```
17
-
18
- `eslint` (`^10.5.0`) and `jiti` (`>= 2.2.0`) are what `/eslint` needs; jiti is only
19
- required because the config file is TypeScript. `prisma` and `@prisma/client` (`^6.19`)
20
- are optional peers, install them only if you use `/prisma`.
21
-
22
- **`jiti` is not a declared peer.** `peerDependencies` is `eslint`, `prisma` and
23
- `@prisma/client`. It is in the command above because `/eslint` genuinely needs it. Since
24
- nothing declares it, pnpm emits no missing-peer warning if you leave it out. This README is
25
- the only thing that will tell you. See Requirements below.
26
-
27
- For the file-sync channel, see Adopting below. The CLI wires the dependency itself.
28
-
29
- ## Adopting
30
-
31
- The payload *is* the install: [`@sidebase/streamctl`](https://github.com/sidebase/streamctl)
32
- reads `presets/` straight from this package in a consuming repo's `node_modules`.
9
+ Requires Nuxt 4, Node >= 24.21 and pnpm.
33
10
 
34
11
  ```sh
35
- pnpm streamctl init --package @sidebase/base-config # scaffold + first sync
36
- pnpm streamctl check # CI drift gate
37
- pnpm streamctl upgrade --to <version> # move the pin forward
12
+ pnpm add -D @sidebase/streamctl
13
+ pnpm streamctl init --package @sidebase/base-config
38
14
  ```
39
15
 
40
- Neither package needs a registry token or private-registry setup: both target the public
41
- `@sidebase` scope on npm.
42
-
43
- Coming from `@sidestream-tech/nuxt-config`? That rename is a manual migration, not an
44
- upgrade: `streamctl upgrade` moves the version pin and never rewrites `package:`. Follow
45
- [`docs/migration.md`](docs/migration.md), and note that the step order is load-bearing.
46
-
47
- ### Presets and profiles
48
-
49
- Two separate axes in `streamctl.config.ts`:
50
-
51
- - `base` is the preset, the set of managed files you get. `nuxt-app` names the repo
52
- *type*: it ships a Dockerfile, a postgres/prisma CI pipeline, and PR-preview cleanup.
53
- - `profile` is the version baseline, the dependency pins reconciled into your
54
- `package.json`. It names the framework and its major: `nuxt-4`.
55
-
56
- The major is in the profile name on purpose. Each profile carries its own
57
- `detect.majorIs`, so `init` picks the right one from your `package.json` and a future
58
- `nuxt-5` profile can sit alongside `nuxt-4` while repos migrate one at a time.
16
+ `init` adds `@sidebase/base-config`, writes `streamctl.config.ts`, and runs the first sync.
17
+ Review the changes and commit them.
59
18
 
60
- `nuxt-4` is the only profile shipped today, so this payload is Nuxt 4 only. There is no
61
- `nuxt-3` profile and none is planned. Running `init` in a Nuxt 3 repo fails with
62
- "No version profile could be detected", because no profile's `detect.majorIs` matches the
63
- `nuxt` version in your `package.json`. Move the repo to Nuxt 4 before adopting.
19
+ Coming from `@sidestream-tech/nuxt-config`? Follow [`docs/migration.md`](docs/migration.md)
20
+ instead.
64
21
 
65
- A profile name must match a `versionProfiles` key in the preset chain. If it does not, the
66
- CLI fails with `CONFIG_INVALID`: `profile "..." is not declared in presets/manifest.json
67
- profiles[]`. `pnpm validate:presets` catches the same mismatch at build time, so a broken
68
- profile name cannot ship in the first place.
22
+ ## What you get
69
23
 
70
- #### Reconciled `scripts.lint`
24
+ | File | Managed as | What it is |
25
+ | ---- | ---------- | ---------- |
26
+ | `.github/workflows/ci.yml` | owned | Lint, typecheck and build, plus optional test and e2e jobs |
27
+ | `.github/workflows/pr-preview-cleanup.yml` | owned | Removes the `preview-deployed` label when `preview` is removed |
28
+ | `.github/workflows/streamctl-upgrade.yml` | owned, opt-in | Weekly upgrade PRs (see [Upgrades](#upgrades)) |
29
+ | `Dockerfile` | owned | Production image, runs `prisma migrate deploy` and then the server |
30
+ | `tsconfig.json` | owned | Extends the shared strict base and Nuxt's generated config |
31
+ | `pnpm-workspace.yaml` | owned | 7-day release cooldown and the install-script allowlist |
32
+ | `.oxlintrc.json` | owned | oxlint rules, run before ESLint |
33
+ | `AGENTS.md` | owned | Conventions for coding agents |
34
+ | `.editorconfig`, `.gitignore`, `.dockerignore` | block | A marked block; the rest of the file is yours |
35
+ | `.vscode/settings.json`, `.vscode/extensions.json` | merged | Only the keys the payload sets; your other keys stay |
36
+ | `eslint.config.ts`, `prisma.config.ts` | scaffolded | Written once, then yours |
71
37
 
72
- The `nuxt-4` profile reconciles your `package.json` `scripts.lint` to
73
- `oxlint -c .oxlintrc.json --deny-warnings && eslint --max-warnings 0 .`, so the fleet
74
- runs `oxlint` (already pinned, with the shipped `.oxlintrc.json`) before ESLint.
38
+ - **owned:** streamctl writes the whole file. Local edits show up as drift in `check`.
39
+ - **block:** only the region between the `BEGIN`/`END streamctl MANAGED BLOCK` markers is
40
+ managed.
75
41
 
76
- Unlike a version pin, a script has no forward-version floor to protect it, because a
77
- script is not a semver, so this is a plain **overwrite**: every `sync` replaces whatever
78
- your `scripts.lint` currently is. A repo that wants its own (for example
79
- `... && LINT_TYPEAWARE=true eslint --max-warnings 0 .` to opt into type-aware linting)
80
- keeps it by excluding the key:
42
+ `sync` also updates `package.json`:
43
+ - `engines`, `packageManager`, and the version ranges of nuxt, typescript, eslint, oxlint,
44
+ prisma, `@prisma/client`, jiti, json-sort-cli and vitest. A range is only raised, never
45
+ lowered.
46
+ - `scripts.postinstall` (`nuxt prepare`) and `scripts.lint` (oxlint, then ESLint).
81
47
 
82
- ```ts
83
- export default {
84
- // ...
85
- versionSyncExclude: ["scripts.lint"],
86
- };
87
- ```
88
-
89
- #### Overriding a managed file
90
-
91
- `.editorconfig` is **block**-managed: the payload owns a marked region, delimited by
92
- `# BEGIN streamctl MANAGED BLOCK editorconfig` / `# END ...`, and preserves everything
93
- outside it byte-for-byte. On first sync the managed block is appended to the end of your
94
- file. Because EditorConfig applies later matching sections over earlier ones, you keep a
95
- project override by placing it **below** the managed block:
48
+ ## Everyday use
96
49
 
97
- ```ini
98
- # BEGIN streamctl MANAGED BLOCK editorconfig
99
- [*]
100
- indent_size = 2
101
- # END streamctl MANAGED BLOCK editorconfig
102
-
103
- # your override wins because it comes after the managed block
104
- [*]
105
- indent_size = 4
50
+ ```sh
51
+ pnpm streamctl sync # apply the payload after changing the config
52
+ pnpm streamctl check # CI gate: fails when managed files drifted
53
+ pnpm streamctl upgrade # move to the latest payload version and re-sync
106
54
  ```
107
55
 
108
- That way the payload keeps updating its region on every sync while your `indent_size = 4`
109
- survives. (A section placed *above* the block would be overridden by the block's
110
- `indent_size = 2`, so keep overrides below the `END` marker.)
56
+ `sync` stops on conflicts, e.g. a file you edited by hand. Resolve them with
57
+ `pnpm streamctl sync --interactive`.
111
58
 
112
- **First-sync hazard.** If your repo already has an `.editorconfig`, the block writer appends
113
- without looking at what is there, so all of your existing content ends up *above* the block
114
- and therefore loses. `streamctl check` exits 0 either way, because the block strategy only
115
- inspects its own markers. This applies to a fresh `streamctl init` just as much as to a
116
- migration, and you have to fix it by hand once. Step 8 of
117
- [`docs/migration.md`](docs/migration.md) has the detector and the two-case fix.
59
+ ### Upgrades
118
60
 
119
- If you instead want to stop managing a file entirely, opt it out per-path:
61
+ Upgrade by hand with `pnpm streamctl upgrade`, or let a weekly workflow open the PRs:
120
62
 
121
63
  ```ts
122
- files: { ".editorconfig": "off" },
64
+ automation: { upgradePr: true },
123
65
  ```
124
66
 
125
- The cost of `off` is total: streamctl no longer touches that file, so **every future
126
- payload fix to it stops reaching your repo**, including new rules, security bumps, and
127
- format corrections. Prefer the block override above when you only need to change part of
128
- a file.
129
-
130
- ## This repo's CLI dependency
67
+ The workflow runs `streamctl check`, lint, typecheck, test and build before it opens the PR.
68
+ Files that need manual work end up in a draft PR with instructions.
131
69
 
132
- The payload is validated and dogfooded against the CLI, so `@sidebase/streamctl` is a
133
- `devDependency` here, pinned to the published semver range.
134
- `validate:presets` imports the CLI's exported manifest schema and the e2e dry-run drives the
135
- real CLI binary, so both resolve `@sidebase/streamctl` from the registry.
70
+ By default it uses `GITHUB_TOKEN`, which may not change `.github/workflows/**`, so
71
+ workflow changes are left out of the PR. To include them, set
72
+ `automation.allowWorkflowUpdates: true` and add a `STREAMCTL_PR_TOKEN` secret (a
73
+ fine-grained PAT or GitHub App token with Contents, Pull requests and Workflows write).
74
+ That token also makes the PR trigger your normal CI.
136
75
 
137
- This payload needs **streamctl >= 0.3.0**: its manifest declares `schemaVersion` 3 (the
138
- `fromDependency` placeholder behind the derived Prisma ARG), which a 0.2.x CLI rejects with
139
- `SCHEMA_UNSUPPORTED`. The root `streamctl.config.ts` location it documents arrived in
140
- 0.2.0. See "Config location" below.
76
+ ## Configuration
141
77
 
142
- ## Typed config
143
-
144
- The root export `@sidebase/base-config` and the subpath `@sidebase/base-config/config`
145
- are the same module. Both provide `defineNuxtBaseConfig` and the `NuxtBaseConfig` type,
146
- which is what makes a `streamctl.config.ts` typed. Every consuming repo has this file,
147
- and `streamctl init` scaffolds it importing from the root export:
78
+ Everything is configured in `streamctl.config.ts`. `defineNuxtBaseConfig` gives you
79
+ completion and type checking for every option:
148
80
 
149
81
  ```ts
150
- // streamctl.config.ts
151
82
  import { defineNuxtBaseConfig } from "@sidebase/base-config";
152
83
 
153
84
  export default defineNuxtBaseConfig({
154
85
  package: "@sidebase/base-config",
155
86
  base: "nuxt-app",
156
- version: "0.1.0",
87
+ version: "0.3.0",
157
88
  profile: "nuxt-4",
158
- ci: { unitTests: true },
89
+
90
+ ci: { unitTests: true, e2e: true },
91
+ docker: { aptPackages: ["ffmpeg"] },
92
+ automation: { upgradePr: true },
93
+ editor: { i18nSourceLanguage: "en" },
159
94
  });
160
95
  ```
161
96
 
162
- ### Config location
163
-
164
- streamctl >= 0.2.0 reads `streamctl.config.ts` from the repo root, and `init` scaffolds it
165
- there. The older `.streamctl/config.ts` is still resolved, so an existing repo keeps working
166
- untouched and can move the file whenever it likes.
167
-
168
- Move it and nothing else changes: the contents are identical, and the CLI warns rather
169
- than guesses if both exist, naming the root file as the one it read. Keeping the legacy
170
- path is fine too. The one thing not to do is leave both in place, since only the root file
171
- is read and edits to the other will look like they do nothing.
172
-
173
- If you set `ignoresTypeAware` yourself, list whichever path your repo uses. The default
174
- covers both.
175
-
176
- `defineNuxtBaseConfig` returns the config unchanged. Its job is typed editor inference
177
- for this payload's knobs: completion and type checking on `ci`, `versions`, `docker`,
178
- `automation`, `security`, `pnpm` and `editor`, plus `profile` narrowed to the profile
179
- names that actually exist. Without it the object is an untyped literal, so a typo such as
180
- `ci: { unitTest: true }` stays silent in your editor.
181
-
182
- The CLI shape-checks the config against the manifest when it loads it, so a sync still
183
- works without the helper. The helper is what moves that feedback into your editor.
97
+ ### Options
184
98
 
185
- Import `NuxtBaseConfig` directly when you need the type on its own:
99
+ | Option | Default | Effect |
100
+ | ------ | ------- | ------ |
101
+ | `ci.unitTests` | `false` | Add a `test` job (`pnpm test`) |
102
+ | `ci.e2e` | `false` | Add an `e2e` job with a postgres service |
103
+ | `ci.aptPackages` | `[]` | apt packages for the `test`/`e2e` jobs and upgrade validation |
104
+ | `docker.aptPackages` | `[]` | apt packages for the image, on top of `openssl` |
105
+ | `docker.preInstall` | `""` | Dockerfile lines before `pnpm install` |
106
+ | `docker.buildArgs` | `""` | Dockerfile lines before `COPY . .` |
107
+ | `docker.buildSteps` | `nuxi prepare`, `prisma generate`, `build` | The build commands |
108
+ | `docker.finalStage` | `""` | Dockerfile lines in the final stage, before `CMD` |
109
+ | `docker.prismaRuntime` | copy schema, install Prisma CLI | Prisma setup in the final stage |
110
+ | `docker.prismaVersion` | your `prisma` version | Prisma CLI version in the image |
111
+ | `docker.startCommand` | `npm exec prisma migrate deploy && exec node ...` | The `CMD`, one line |
112
+ | `versions.node` | `24.21.0` | Node in CI, the Dockerfile and the upgrade workflow |
113
+ | `versions.pnpm` | `10.34.5` | pnpm in the Dockerfile (CI uses `packageManager`) |
114
+ | `automation.upgradePr` | `false` | Add the weekly upgrade workflow |
115
+ | `automation.allowWorkflowUpdates` | `false` | Include workflow changes in upgrade PRs (needs a token) |
116
+ | `automation.prTokenSecret` | `"STREAMCTL_PR_TOKEN"` | Secret name for that token |
117
+ | `automation.prepare` | `pnpm prisma generate` if Prisma is installed | Command run before validating an upgrade; `""` skips |
118
+ | `security.minimumReleaseAge` | `"10080"` | Minutes a release must be public before pnpm installs it; `"0"` disables |
119
+ | `pnpm.onlyBuiltDependencies` | `[]` | Extra packages allowed to run install scripts |
120
+ | `editor.i18nSourceLanguage` | `"de"` | i18n-ally source language |
121
+
122
+ Things to know:
123
+
124
+ - **A `docker.*` text option replaces the default.** It does not append, so a `buildSteps`
125
+ override must list every build command you still need.
126
+ - **`docker.prismaRuntime` and `docker.startCommand` go together.** Without Prisma, set
127
+ `prismaRuntime: ""` and a `startCommand` without `migrate deploy`, e.g.
128
+ `exec node .output/server/index.mjs`. Keep the `exec`, so node gets SIGTERM.
129
+ - **Repos with English source strings must set `editor.i18nSourceLanguage: "en"`.**
130
+ Otherwise every sync writes `"de"` back.
131
+ - **Adopting `pnpm-workspace.yaml` replaces your existing file**, including any
132
+ `onlyBuiltDependencies`. Move those entries to `pnpm.onlyBuiltDependencies` instead of
133
+ running `pnpm approve-builds`, which the next sync reverts.
134
+
135
+ ### Customizing managed files
136
+
137
+ **Block files:** put your own settings below the `END` marker. For `.editorconfig`,
138
+ later sections win, so an override below the block takes precedence. On the first sync,
139
+ the block is appended *after* your existing content, so move that content below the
140
+ block once.
141
+
142
+ **Opt a file out** when the options are not enough:
186
143
 
187
144
  ```ts
188
- import type { NuxtBaseConfig } from "@sidebase/base-config/config";
145
+ files: { "Dockerfile": "off" },
189
146
  ```
190
147
 
191
- ## Shared tsconfig
148
+ streamctl then leaves the file alone, including all future fixes to it.
192
149
 
193
- `@sidebase/base-config/tsconfig.base` is the shared strict TypeScript base, exported as a
194
- JSON file so a `tsconfig.json` can extend it by package name. The `nuxt-app` preset ships
195
- a managed `tsconfig.json` that chains it with Nuxt's generated config:
150
+ **Keep your own `scripts.lint`** (or any other synced `package.json` field):
196
151
 
197
- ```json
198
- {
199
- "extends": [
200
- "@sidebase/base-config/tsconfig.base",
201
- "./.nuxt/tsconfig.json"
202
- ]
203
- }
152
+ ```ts
153
+ versionSyncExclude: ["scripts.lint"],
204
154
  ```
205
155
 
206
- Order matters: the shared base comes first, so Nuxt's generated config applies over it.
207
-
208
- ## Naming convention
209
-
210
- Two rules cover every exported name, so you can predict them:
156
+ ## Helpers
211
157
 
212
- - Types describing **this payload's config shape** are `NuxtBase*`: `NuxtBaseConfig`,
213
- `NuxtBaseCiConfig`, `defineNuxtBaseConfig`, and the `NUXT_BASE_*_KEYS` lists. The prefix
214
- is doing real work here, because these types describe the config of this specific
215
- payload rather than anything generic.
216
- - **Everything else is domain-named with no package prefix**: `buildPrismaConfig`,
217
- `applyPrismaDevEnv`, `createSidebaseEslint`, `resolveEslintOptions`. The import path
218
- already states where a symbol comes from, so repeating it in the identifier adds
219
- nothing.
220
-
221
- `streamctl` is reserved for the CLI (`@sidebase/streamctl`) and the files it owns:
222
- `streamctl.config.ts` in the repo root, and the legacy `.streamctl/` directory it still
223
- reads. Nothing exported from this package carries that name, because nothing
224
- exported from this package comes from the CLI. The ESLint layers this package builds are
225
- named `sidebase/*` (`sidebase/defaults`, `sidebase/console`, and so on), which is what you
226
- see in lint output and what you target if you `.override()` a layer by name. They are
227
- built at runtime by this package; the CLI never sees them.
228
-
229
- ## ESLint factory
230
-
231
- `@sidebase/base-config/eslint` exports `createSidebaseEslint(options?)`, which wraps
232
- [`@antfu/eslint-config`](https://github.com/antfu/eslint-config) (a dependency of this
233
- package, so **do not add `@antfu/eslint-config` directly**) and returns its
234
- `FlatConfigComposer`, so you chain `.append()` / `.override()` for local rules:
158
+ ### ESLint
235
159
 
236
160
  ```ts
237
161
  // eslint.config.ts
@@ -241,432 +165,82 @@ export default createSidebaseEslint({ zod: "full", trpcGuard: true })
241
165
  .append({ rules: { "vue/multi-word-component-names": "off" } });
242
166
  ```
243
167
 
244
- ### Options
168
+ Wraps [`@antfu/eslint-config`](https://github.com/antfu/eslint-config), which is a
169
+ dependency of this package; don't install it yourself. Needs `eslint` and `jiti` (for the
170
+ TypeScript config file) in your devDependencies; `sync` adds both.
245
171
 
246
172
  | Option | Default | Effect |
247
173
  | ------ | ------- | ------ |
248
- | `zod` | `"none"` | `"full"` = ban `.extend()/.merge()/.passthrough()` + enforce `import * as z`; `"import-style"` = import style only; `"none"` = off |
249
- | `console` | `"error"` | `"error"` bans all `console.*`; an array is the allow-list (e.g. `["warn", "error"]`) |
174
+ | `zod` | `"none"` | `"import-style"`: enforce `import * as z`. `"full"`: also ban `.extend()`, `.merge()`, `.passthrough()` |
175
+ | `console` | `"error"` | Ban all `console.*`, or pass allowed methods, e.g. `["warn", "error"]` |
250
176
  | `trpcGuard` | `false` | Ban `publicProcedure` in `server/trpc/routers/**` |
251
- | `prismaImportGuard` | `false` | Ban `~~/prisma/` client imports on the app side |
252
- | `typeDefStyle` | `"interface"` | `ts/consistent-type-definitions` |
253
- | `autoImportPaths` | `["utils/", "composables/", "~~/shared/types/"]` | Paths banned from direct import (Nuxt auto-imports) |
254
- | `autoImportTypeOnly` | `[]` | Subset of `autoImportPaths` where `import type { ... }` stays allowed (value imports still banned) |
255
- | `ignoresTypeAware` | `["prisma.config.ts", "eslint.config.ts", "streamctl.config.ts", ".streamctl/**/*.ts"]` | Files excluded from antfu's type-aware program (only relevant when type-aware). Covers both config locations, since `.streamctl/config.ts` stays supported after the root default lands |
256
- | `testFilePattern` | `["**/*.{test,spec}.{ts,tsx}", "**/*.stories.{ts,tsx}"]` | Test/spec/story globs where the `process.env` + auto-import bans are relaxed |
177
+ | `prismaImportGuard` | `false` | Ban `~~/prisma/` client imports outside server code |
178
+ | `typeDefStyle` | `"interface"` | Prefer `interface` or `type` |
179
+ | `autoImportPaths` | `["utils/", "composables/", "~~/shared/types/"]` | Paths Nuxt auto-imports, so direct imports are banned |
180
+ | `autoImportTypeOnly` | `[]` | Of those, paths where `import type` stays allowed |
181
+ | `ignoresTypeAware` | config files | Files excluded from type-aware linting |
182
+ | `testFilePattern` | `*.test`, `*.spec`, `*.stories` | Files where the `process.env` and auto-import bans are off |
257
183
 
258
- Rare per-repo rules belong in your local `.append(...)`, not in the option surface.
184
+ Type-aware linting is off by default; run with `LINT_TYPEAWARE=true` to enable it.
259
185
 
260
- ### Rule IDs & inline suppression
261
-
262
- The custom bans map to these ESLint rule IDs. Use the exact ID in an
263
- `// eslint-disable-next-line ...` directive to suppress one line:
186
+ To silence a single line, use the rule ID:
264
187
 
265
188
  | Ban | Rule ID |
266
189
  | --- | ------- |
267
- | Direct `process.env` access, as a member expression (`process.env.X`, `const { env } = process`) | `no-restricted-properties` |
268
- | ANY import **or re-export** of `node:process` or `process` that can reach `env` (`import { env }`, `import { env as e }`, `import * as proc`, `import proc from "node:process"`, `export { env } from "node:process"`, `export * from "node:process"`) | `ts/no-restricted-imports` |
269
- | Auto-import path imports (`autoImportPaths`) | `ts/no-restricted-imports` |
270
- | App-side Prisma client import (`prismaImportGuard`) | `ts/no-restricted-imports` |
271
- | Zod named-`z` import style (`zod: "import-style"`/`"full"`) | `no-restricted-syntax` |
272
- | Zod `.extend()/.merge()/.passthrough()` (`zod: "full"`) | `no-restricted-syntax` |
273
- | `publicProcedure` in routers (`trpcGuard`) | `no-restricted-syntax` |
274
-
275
- "Re-export" is not a figure of speech: a barrel file doing `export * from "node:process"`
276
- reports, as do `export { env } from "node:process"` and `export { default as p } from
277
- "node:process"`. The one shape that does **not** report is a bare side-effect import with
278
- no bindings, `import "node:process"`. It cannot reach `env`, so there is nothing to ban.
279
-
280
- The `process.env` ban is enforced by **two** rules, and which one reports depends on the
281
- shape, so suppressing the wrong one fails twice over: the directive does not suppress
282
- anything, and ESLint additionally warns that it was unused.
283
-
284
- ```
285
- // eslint-disable-next-line no-restricted-properties
286
- import { env } from 'node:process'
287
-
288
- 1:1 warning Unused eslint-disable directive (no problems were reported from 'no-restricted-properties')
289
- 2:10 error 'env' import from 'node:process' is restricted... ts/no-restricted-imports
290
- ```
291
-
292
- ### Requirements
293
-
294
- `eslint` is a peer dependency (`^10.5.0`), so consumers of `/eslint` must supply ESLint
295
- themselves. The peer range matches the version baseline, so `streamctl` version-sync
296
- keeps a repo's ESLint in step with what this package is tested against.
297
-
298
- Flat config only. To load a TypeScript `eslint.config.ts` on Node, consumers add
299
- `jiti` (`>= 2.2.0`) as a devDependency: ESLint has no native TS loader, and ESLint 10
300
- rejects jiti below 2.2.0. Writing `eslint.config.mjs` instead drops the jiti requirement
301
- entirely, as does running on Deno or Bun, which import TypeScript directly.
302
-
303
- #### Type-aware linting prerequisite
304
-
305
- Type-aware linting is gated behind `LINT_TYPEAWARE=true` (off by default), so this only
306
- matters on the opt-in and CI path. When type-aware lint is on, the Prisma client and Nuxt
307
- types must be generated **before** `lint` runs, otherwise type-aware rules fail on missing
308
- generated types. Ordering satisfies this, not a preflight check:
309
-
310
- - the managed `ci.yml` runs `prisma generate` (and `nuxi prepare`) before `pnpm lint`, and
311
- - `scripts.postinstall: "nuxt prepare"` regenerates Nuxt types on install.
312
-
313
- A standard `pnpm install` plus the managed CI ordering covers the prerequisite. No separate
314
- check runs.
315
-
316
- ## Prisma factory
317
-
318
- `@sidebase/base-config/prisma` exports `buildPrismaConfig({ views?, typedSql? })`,
319
- which returns a Prisma 6.19 config object you wrap in `defineConfig`:
320
-
321
- ```ts
322
- // prisma.config.ts
323
- import { defineConfig } from "prisma/config";
324
- import { buildPrismaConfig } from "@sidebase/base-config/prisma";
325
-
326
- export default defineConfig(buildPrismaConfig({ views: true, typedSql: true }));
327
- ```
328
-
329
- It reads `DATABASE_URL` / `DIRECT_DATABASE_URL` / `SHADOW_DATABASE_URL` from the
330
- environment (Pattern B): the schema engine gets a pgbouncer-free direct
331
- connection: `DIRECT_DATABASE_URL` if set, otherwise `DATABASE_URL` with
332
- pooler-only query params (`pgbouncer`, `connection_limit`, and so on) stripped via `ufo`.
333
- `SHADOW_DATABASE_URL` is wired only when present; with no database URL the engine
334
- block is omitted so the schema's own `datasource` applies.
335
-
336
- | Option | Default | Effect |
337
- | ------ | ------- | ------ |
338
- | `views` | `false` | Configure the `views` feature (`prisma/views`) |
339
- | `typedSql` | `false` | Configure the `typedSql` preview feature (`prisma/sql`) |
340
-
341
- ### `applyPrismaDevEnv`
190
+ | `process.env.X` | `no-restricted-properties` |
191
+ | `import { env } from "node:process"` and similar | `ts/no-restricted-imports` |
192
+ | Auto-import paths, app-side Prisma client | `ts/no-restricted-imports` |
193
+ | Zod import style and methods, `publicProcedure` | `no-restricted-syntax` |
342
194
 
343
- The same subpath exports `applyPrismaDevEnv(env?)`, the companion for local development.
344
- It seeds `DIRECT_DATABASE_URL`, `DATABASE_URL`, and `SHADOW_DATABASE_URL` with localhost
345
- defaults, but only when they are unset, so a real `.env` or shell value always wins. It
346
- writes to the environment record you pass and defaults to `process.env`, which is a
347
- deliberate side effect.
348
-
349
- You need it because once a `prisma.config.ts` exists, Prisma stops auto-loading `.env`.
350
- A schema that reads `env("DATABASE_URL")` then fails with no environment at all. Call it
351
- in the config file, after loading dotenv, and the Prisma CLI runs against local postgres
352
- with zero setup:
195
+ ### Prisma
353
196
 
354
197
  ```ts
355
198
  // prisma.config.ts
356
199
  import "dotenv/config";
357
- import { defineConfig } from "prisma/config";
358
200
  import { applyPrismaDevEnv, buildPrismaConfig } from "@sidebase/base-config/prisma";
201
+ import { defineConfig } from "prisma/config";
359
202
 
360
- applyPrismaDevEnv();
361
-
362
- export default defineConfig(buildPrismaConfig());
363
- ```
364
-
365
- `buildPrismaConfig` alone is enough wherever the connection variables are already set,
366
- which is every deployed environment and CI. Add `applyPrismaDevEnv` when you want
367
- `prisma migrate dev` and friends to work on a fresh checkout without a `.env` file. The
368
- defaults are direct = local postgres, pooled = the direct URL plus `pgbouncer=1`, and
369
- shadow = the direct URL on a `/prisma-shadow` database.
370
-
371
- The URL helpers never log a connection string and never put one in an error message, so a
372
- password cannot reach your terminal or CI output through them. Keep that property if you
373
- wrap them.
374
-
375
- `prisma` / `@prisma/client` are peer dependencies (`^6.19`).
376
-
377
- > Prisma 7 is out of scope for now. v7 changes the config shape (`env()` helper,
378
- > `directUrl` becomes `url`, no `package.json` prisma block); a `buildPrismaConfig` v7
379
- > variant is a deliberate later bump.
380
-
381
- ## Supply-chain cooldown (`pnpm-workspace.yaml`)
382
-
383
- The base preset fully owns `pnpm-workspace.yaml`, pnpm's settings file. It ships two
384
- supply-chain controls, both on by default for every repo:
385
-
386
- ```yaml
387
- packages: []
388
- minimumReleaseAge: 10080 # 7 days, in minutes
389
- minimumReleaseAgeExclude:
390
- - "@sidebase/*"
391
- onlyBuiltDependencies: # the postinstall-script allowlist
392
- - "@prisma/client"
393
- - "@prisma/engines"
394
- - "esbuild"
395
- - "prisma"
396
- ```
397
-
398
- `minimumReleaseAge` refuses to resolve a version until it has been published for
399
- 7 days, so a compromised release has time to be caught and yanked before the fleet
400
- installs it. It needs pnpm 10.16 or newer and gates *fresh resolution only*, so
401
- `pnpm install --frozen-lockfile` replays the lockfile untouched and CI is unaffected.
402
-
403
- The pnpm baseline shipped to consumers is 10.34.5 (`presets/*/preset.json`). This repo's
404
- own `packageManager` pin tracks separately and is usually newer; the two are independent
405
- by design, not drift.
406
-
407
- Exclusions match **package names, not dependency trees**: an excluded package's own
408
- dependencies still face the cooldown, and pnpm resolves the newest *eligible* older
409
- version in range. Only when nothing in range is old enough does resolution fail, with
410
- a misleading `ERR_PNPM_NO_MATCHING_VERSION` ([pnpm#9998](https://github.com/pnpm/pnpm/issues/9998)).
411
-
412
- `@sidebase/*` is exempt: the first-party scope covering both this payload and the
413
- `streamctl` CLI, published by the org itself, so a payload release reaches the fleet
414
- the same day instead of waiting out its own cooldown.
415
-
416
- **`packages: []` is load-bearing.** pnpm defaults it to `**` whenever a
417
- `pnpm-workspace.yaml` exists, which would silently promote every nested `package.json`
418
- (test fixtures, examples) to a workspace project and break `--frozen-lockfile`.
419
-
420
- `onlyBuiltDependencies` is the install-script allowlist. Everything else installs
421
- with its lifecycle scripts blocked. Add extras through the config, **not** through
422
- `pnpm approve-builds` (which writes to the managed file and is reverted on the next
423
- sync):
424
-
425
- ```ts
426
- export default {
427
- // ...
428
- security: { minimumReleaseAge: "10080" }, // minutes; "0" disables the cooldown
429
- pnpm: { onlyBuiltDependencies: ["sharp"] }, // on top of the baked-in baseline
430
- };
431
- ```
432
-
433
- | Option | Default | Effect |
434
- | ------ | ------- | ------ |
435
- | `security.minimumReleaseAge` | `"10080"` | Minutes a release must age before pnpm installs it. A string, because the manifest's `configKeys` has no number type, same as the `versions.*` pins. |
436
- | `pnpm.onlyBuiltDependencies` | `[]` | Extra packages allowed to run install scripts, appended below the baked-in baseline |
437
-
438
- Because the file is `full`-owned, a repo that already has a `pnpm-workspace.yaml`
439
- raises a one-time adoption conflict on the first sync; reconcile it with
440
- `streamctl sync --interactive`.
441
-
442
- **Adopting it discards your existing keys.** The rendered file is the payload's, so an
443
- existing `onlyBuiltDependencies` allowlist and `ignoredBuiltDependencies` are both replaced
444
- by the baked-in baseline, and **nothing warns about it**. Most affected packages ship a
445
- prebuilt native binary and keep working either way, so the practical breakage is limited to
446
- architectures with no prebuild and to postinstalls doing essential non-native work. Capture
447
- the old list before you adopt and re-add it via `pnpm.onlyBuiltDependencies` above. A repo
448
- that genuinely needs its own `packages:` list (a real monorepo) or its own exclude list
449
- should opt the file out entirely instead:
450
-
451
- ```ts
452
- files: { "pnpm-workspace.yaml": "off" },
453
- ```
454
-
455
- ## CI knobs
456
-
457
- The `nuxt-app` preset ships a managed `.github/workflows/ci.yml`. Its `lint-typecheck` and
458
- `build` jobs always run; the rest is knobs:
459
-
460
- | Knob | Default | Effect |
461
- | ---- | ------- | ------ |
462
- | `ci.unitTests` | `false` | Add the `test` job (`pnpm test`) |
463
- | `ci.e2e` | `false` | Add the `e2e` job, with a health-checked postgres service |
464
- | `ci.aptPackages` | `[]` | System packages `apt-get`-installed in the `test` and `e2e` jobs and the upgrade validation |
465
-
466
- `ci.aptPackages` is the CI counterpart to `docker.aptPackages` below: extra system packages
467
- on top of what the runner image already carries, for a binary the suite needs and
468
- `ubuntu-latest` does not ship. The motivating case is `libxml2-utils`, for `xmllint`.
469
-
470
- It applies to the `test` and `e2e` jobs only, never `lint-typecheck` or `build`. The step
471
- installs directly after `checkout`, before `pnpm install`, so a package needed by an install
472
- lifecycle script is covered as well as one needed by the tests. Leave it unset and the step
473
- is skipped entirely: it renders with a guard on the value being non-empty, so the default
474
- costs a no-op step rather than an `apt-get` call.
475
-
476
- ```ts
477
- export default {
478
- // ...
479
- ci: { unitTests: true, e2e: true, aptPackages: ["libxml2-utils"] },
480
- };
481
- ```
482
-
483
- Package names only. Every element is rejected if it contains a shell metacharacter, and the
484
- joined string is then re-validated against the Debian package-name character set (lowercase,
485
- digits, `+`, `-`, `.`). Both checks exist because the rendered command deliberately leaves
486
- the value unquoted, which is how several packages word-split into one `apt-get` call.
487
-
488
- ## Dockerfile knobs
489
-
490
- The `nuxt-app` preset ships a managed `Dockerfile`. Beyond `docker.aptPackages`
491
- (an `openssl`-plus allowlist), six `docker.*` knobs inject raw text at fixed
492
- positions so every fleet repo can express its own build without opting the file
493
- out:
494
-
495
- | Knob | Position | Default |
496
- | ---- | -------- | ------- |
497
- | `docker.preInstall` | build stage, before `pnpm install` | `""` |
498
- | `docker.buildArgs` | build stage, before `COPY . .` | `""` |
499
- | `docker.buildSteps` | the build-command block | `nuxi prepare` / `prisma generate` / `run build` |
500
- | `docker.finalStage` | final stage, before `CMD` | `""` |
501
- | `docker.prismaRuntime` | final stage | copy the schema + install the `migrate deploy` CLI |
502
- | `docker.startCommand` | shell-form `CMD`, one line | `prisma migrate deploy` then `exec node ...` |
503
-
504
- A seventh knob is different in kind: a validated version, not raw text.
505
-
506
- | Knob | Position | Default |
507
- | ---- | -------- | ------- |
508
- | `docker.prismaVersion` | the `ARG PRISMA_VERSION=` default inside `docker.prismaRuntime` | derived from the repo's `prisma` pin (its range floor); the baseline only when no full `x.y.z` pin exists |
509
-
510
- It is pattern-checked (`x.y.z`, optional prerelease), so unlike the six raw-text
511
- knobs it cannot inject arbitrary Dockerfile content. `--build-arg PRISMA_VERSION=`
512
- still overrides at build time.
513
-
514
- ```ts
515
- export default {
516
- // ...
517
- docker: {
518
- preInstall: "COPY ./vendor ./vendor", // vendored dep needed at install time
519
- buildSteps: "RUN pnpm nuxi prepare\nRUN pnpm run build", // a repo with no Prisma
520
- prismaRuntime: "", // drop Prisma entirely
521
- startCommand: "exec node .output/server/index.mjs",
522
- },
523
- };
203
+ applyPrismaDevEnv(); // optional: localhost defaults for unset DB URLs
204
+ export default defineConfig(buildPrismaConfig({ views: true, typedSql: true }));
524
205
  ```
525
206
 
526
- > **These six values land verbatim.** They are `string`, not `string[]`, so order
527
- > is preserved (a sorted array would scramble ordered `RUN` steps), and no
528
- > shell-metacharacter check runs on them. The trust level is exactly that of
529
- > `files: { "Dockerfile": "off" }`, which any consumer can already set: whoever
530
- > can edit `streamctl.config.ts` can already replace the whole file. The
531
- > `.output` ownership stays FIXED outside every knob. The container itself runs
532
- > as **root**: a fixed `USER node` broke every deployment that overrides the image
533
- > CMD with a root-requiring command, so a repo that wants an unprivileged runtime
534
- > opts in with `docker.finalStage: "USER node"`.
535
-
536
- Setting a knob replaces its default outright, it does not append. So a `docker.buildSteps`
537
- override must restate every build command you still want, and `docker.prismaRuntime: ""`
538
- removes the Prisma runtime block entirely. The three knobs that default to real content
539
- (`buildSteps`, `prismaRuntime`, `startCommand`) are the ones where this matters; the other
540
- three default to `""`.
541
-
542
- **`prismaRuntime` and `startCommand` are coupled.** Override one and you must override the
543
- other. `prismaRuntime` is what installs the Prisma CLI into the final stage, and the
544
- DEFAULT `startCommand` is `npm exec prisma migrate deploy && exec node ...`. Setting only
545
- `prismaRuntime: ""` therefore produces an image whose `CMD` invokes a CLI that is no longer
546
- installed, and the container fails to start. The example above overrides both, which is why
547
- it is safe to copy; a single-knob change is not. Drop the `migrate deploy` half of
548
- `startCommand` at the same time.
549
-
550
- Global installs in `docker.preInstall` must use `npm i -g <tool>`, not
551
- `pnpm add -g`: the image sets no `PNPM_HOME`, so pnpm's global bin dir is
552
- undefined.
553
-
554
- ## Editor settings (`.vscode/settings.json`)
555
-
556
- The base preset owns its own keys in `.vscode/settings.json` and leaves the rest of the file
557
- to the project. Among them is i18n-ally, configured for the fleet's standard setup:
558
- `@nuxtjs/i18n` with flat dotted keys under `i18n/locales`.
559
-
560
- Everything the payload does not write is yours and survives a sync untouched: your
561
- `editor.fontSize`, your `files.exclude`, any unrelated key at all. Six of those are named in
562
- the manifest as project-owned on top of that (`editor.fontSize`, `editor.rulers`,
563
- `editor.formatOnSave`, `files.exclude`, `search.exclude`, `files.watcherExclude`), which
564
- keeps them yours even if a later payload version starts setting them. Only the keys the
565
- payload writes are reverted, and they are the ones below.
566
-
567
- | Knob | Default | Effect |
568
- | ---- | ------- | ------ |
569
- | `editor.i18nSourceLanguage` | `"de"` | The locale i18n-ally translates FROM (`i18n-ally.sourceLanguage`). Payload-enforced; see below |
207
+ - `buildPrismaConfig` reads `DATABASE_URL`, `DIRECT_DATABASE_URL` and `SHADOW_DATABASE_URL`.
208
+ Migrations use `DIRECT_DATABASE_URL`, or `DATABASE_URL` with the pooler parameters removed.
209
+ `views` and `typedSql` enable those Prisma features.
210
+ - `applyPrismaDevEnv` sets localhost defaults for the URLs that are unset, so
211
+ `prisma migrate dev` works on a fresh checkout without a `.env`.
570
212
 
571
- ```ts
572
- export default {
573
- // ...
574
- editor: { i18nSourceLanguage: "en" },
575
- };
576
- ```
213
+ Prisma 6.19 only; `prisma` and `@prisma/client` are optional peer dependencies.
577
214
 
578
- **If your repo authors its source strings in English, set this.** The source language is
579
- payload-owned, so leaving it unset does not mean "keep what I have". Every sync writes
580
- `"de"` into the file, over the top of an `"en"` you put there by hand. Nothing warns, and
581
- `streamctl check` stays green afterwards, because the value it finds is the value the payload
582
- intends. Setting the knob is the only thing that makes `"en"` survive a sync.
215
+ ### tsconfig
583
216
 
584
- `i18n-ally.localesPaths` and `i18n-ally.keystyle` are deliberately NOT configurable: the
585
- locales path and the flat key style are fleet-wide conventions, so they are enforced rather
586
- than offered. Editing either one in your own `.vscode/settings.json` is reverted on the next
587
- sync. A repo that genuinely needs a different layout should opt the file out
588
- (`files: { ".vscode/settings.json": "off" }`) rather than fight the merge.
217
+ The managed `tsconfig.json` extends `@sidebase/base-config/tsconfig.base`, the shared
218
+ strict config, before Nuxt's generated one. You can extend it directly as well.
589
219
 
590
- ## Upgrade-PR workflow
220
+ ## Troubleshooting
591
221
 
592
- The base preset ships `.github/workflows/streamctl-upgrade.yml`, a scheduled workflow
593
- that opens a PR when a payload update is available. It is an `enabledBy`-gated managed
594
- file, off by default. It only lands once a repo opts in.
222
+ - **`No version profile could be detected`:** the repo is not on Nuxt 4. Nuxt 3 is not
223
+ supported.
224
+ - **`pnpm approve-builds` changes disappear:** use `pnpm.onlyBuiltDependencies`.
225
+ - **`.editorconfig` settings ignored after the first sync:** your old content sits above the
226
+ managed block. Move it below.
227
+ - **`eslint.config.ts` fails to load:** add `jiti` to devDependencies.
228
+ - **Repo with private npm dependencies:** the upgrade workflow installs without registry
229
+ credentials. Opt it out and use your own.
595
230
 
596
- Enable it in `streamctl.config.ts`, then `streamctl sync`:
231
+ ## Developing this package
597
232
 
598
- ```ts
599
- export default {
600
- // ...
601
- automation: {
602
- upgradePr: true,
603
- // allowWorkflowUpdates: true,
604
- // prTokenSecret: "STREAMCTL_PR_TOKEN",
605
- },
606
- // optional: version pins, default to the payload's baseline. `node` feeds
607
- // ci.yml, the Dockerfile and this workflow; `pnpm` feeds the Dockerfile only
608
- // (CI pins pnpm via package.json#packageManager):
609
- // versions: { node: "24.21.0", pnpm: "10.34.5" },
610
- };
611
- ```
612
-
613
- By default, the workflow uses `GITHUB_TOKEN` and omits changes under
614
- `.github/workflows/**`. If an update contains workflow changes, it opens a draft PR
615
- with every other safe change and lists the omitted files and completion commands.
616
-
617
- Set `automation.allowWorkflowUpdates` to `true` to include workflow changes. Add a
618
- `STREAMCTL_PR_TOKEN` repository secret with `Contents`, `Pull requests`, and
619
- `Workflows` write access. Change `automation.prTokenSecret` when the secret has another
620
- name. A fine-grained PAT or GitHub App token works. The built-in `GITHUB_TOKEN` cannot
621
- receive `Workflows` permission.
622
-
623
- | Knob | Default | Effect |
624
- | ---- | ------- | ------ |
625
- | `automation.upgradePr` | `false` | Sync the scheduled upgrade workflow |
626
- | `automation.allowWorkflowUpdates` | `false` | Include `.github/workflows/**` changes in upgrade PRs |
627
- | `automation.prTokenSecret` | `"STREAMCTL_PR_TOKEN"` | Secret used when workflow updates are enabled |
628
- | `automation.prepare` | `"pnpm prisma generate"` | One-line command run before validation |
629
-
630
- `automation.prepare` generates what `pnpm install` does not, before validation. One line;
631
- chain with `&&`. Set `""` for a repo without Prisma.
632
-
633
- The upgrade job runs `streamctl check` and every available lint, typecheck, test, and
634
- build script before opening a ready PR. PRs created with `GITHUB_TOKEN` do not trigger
635
- their own `pull_request` workflows, so these inline checks are required. The external
636
- token allows the new PR to trigger them normally.
637
-
638
- The workflow handles CLI results as follows:
639
-
640
- - `check` exit 0: stop without a PR.
641
- - `check` exit 3: open or update one drift issue.
642
- - `check` exit 4: attempt the upgrade.
643
- - `upgrade` exit 0: validate and open a ready PR, or a draft when workflow files were omitted.
644
- - `upgrade` exit 2: inspect conflicts using the pre-upgrade status.
645
- - Any other exit: fail the workflow.
646
-
647
- A conflict is safe to apply when the same path was previously `full` managed and
648
- `in-sync`. The workflow retries those generated replacements with `--force`. Any new,
649
- modified, malformed, or structurally conflicting path produces a draft PR instead.
650
- Workflow changes also produce a draft unless explicitly allowed. Drafts contain every
651
- safe change, keep conflicting and omitted workflow files at their current content, list
652
- what remains, and give the commands needed to finish the upgrade. When a malformed or
653
- structurally invalid file blocks `--force`, the draft only updates the payload version
654
- and lockfile.
655
-
656
- Diagnostic JSON stays under the runner's temporary directory. Upgrade branches include
657
- the target version, so a later release does not overwrite an older open PR.
658
-
659
- The payload is public npm. A repo with other private dependencies must opt this workflow
660
- out and provide its own install authentication.
661
-
662
- ### Action pinning policy
663
-
664
- Every action in every managed workflow and CI job fragment is pinned by full commit SHA,
665
- with the version tag in a trailing comment. Moving tags can be re-pointed, so tags are
666
- never trusted, secrets or not. `test/payload.test.ts` renders every managed workflow
667
- and fails on any `uses:` that is not a 40-hex SHA.
668
-
669
- The shipped pins track `actions/checkout` v6, `actions/setup-node` v6, and
670
- `pnpm/action-setup` v6. These majors run on the node24 action runtime, which needs a
671
- GitHub Actions runner `>= v2.327.1`. GitHub-hosted `ubuntu-latest` is far past that; only
672
- a repo that adds its own **self-hosted** runner needs to keep it above that floor.
233
+ ```sh
234
+ pnpm lint && pnpm typecheck && pnpm test
235
+ pnpm validate:presets # manifests and cross-file invariants
236
+ pnpm e2e:dry-run # pack, install into a scratch consumer, sync and check
237
+ pnpm e2e:docker # build and start the rendered Dockerfile (needs Docker)
238
+ ```
239
+
240
+ - The payload lives in `presets/` (`base`, and `nuxt-app` on top of it). The manifests in
241
+ `preset.json` define files, options and version baselines.
242
+ - Tests check contracts derived from the manifests, so changing a preset file rarely needs
243
+ a test change.
244
+ - Actions in managed workflows are pinned by commit SHA; the tests enforce it.
245
+ - Requires streamctl >= 0.3.0 (manifest schema version 3).
246
+ - Releases: [`docs/release.md`](docs/release.md). Agent conventions: [`AGENTS.md`](AGENTS.md).
package/dist/config.d.mts CHANGED
@@ -100,7 +100,7 @@ interface NuxtBaseAutomationConfig {
100
100
  allowWorkflowUpdates?: boolean;
101
101
  /** Repository secret used when workflow updates are allowed. */
102
102
  prTokenSecret?: string;
103
- /** One-line shell command run before validation (default: `pnpm prisma generate`, `""` skips). */
103
+ /** One-line shell command run before validation (default: `pnpm prisma generate` when Prisma is installed, `""` skips). */
104
104
  prepare?: string;
105
105
  }
106
106
  /**
package/dist/config.d.ts CHANGED
@@ -100,7 +100,7 @@ interface NuxtBaseAutomationConfig {
100
100
  allowWorkflowUpdates?: boolean;
101
101
  /** Repository secret used when workflow updates are allowed. */
102
102
  prTokenSecret?: string;
103
- /** One-line shell command run before validation (default: `pnpm prisma generate`, `""` skips). */
103
+ /** One-line shell command run before validation (default: `pnpm prisma generate` when Prisma is installed, `""` skips). */
104
104
  prepare?: string;
105
105
  }
106
106
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sidebase/base-config",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Shared @sidebase base configuration for Nuxt repos: ESLint / Prisma / tsconfig factories on npm, plus the streamctl file-sync preset payload",
5
5
  "keywords": [
6
6
  "sidebase",
@@ -332,7 +332,8 @@ jobs:
332
332
  {
333
333
  echo "## Status"
334
334
  echo
335
- echo "This update omits GitHub Actions workflow changes. Do not merge this pull request yet."
335
+ echo "> [!CAUTION]"
336
+ echo "> This update omits GitHub Actions workflow changes. Do not merge this pull request yet."
336
337
  echo
337
338
  printf -- '- Updated the streamctl payload from `%s` to `%s`.\n' "$from" "$TARGET"
338
339
  echo "- Applied every other safe managed-file change."
@@ -369,7 +370,8 @@ jobs:
369
370
  {
370
371
  echo "## Status"
371
372
  echo
372
- echo "This update needs manual conflict resolution. Do not merge this pull request yet."
373
+ echo "> [!CAUTION]"
374
+ echo "> This update needs manual conflict resolution. Do not merge this pull request yet."
373
375
  echo
374
376
  printf -- '- Updated the streamctl payload from `%s` to `%s`.\n' "$from" "$TARGET"
375
377
  if [[ "$APPLIED" == "true" ]]; then
@@ -19,7 +19,7 @@
19
19
  "CI_APT_PACKAGES": { "configPath": "ci.aptPackages", "default": "", "pattern": "^$|^[a-z0-9][a-z0-9 +.-]*$", "join": "space" },
20
20
  "NODE_VERSION": { "configPath": "versions.node", "default": "24.21.0", "pattern": "^[\\w.+-]+$" },
21
21
  "PR_TOKEN_SECRET": { "configPath": "automation.prTokenSecret", "default": "STREAMCTL_PR_TOKEN", "pattern": "^(?![Gg][Ii][Tt][Hh][Uu][Bb]_)[A-Za-z_][A-Za-z0-9_]*$" },
22
- "PREPARE_COMMAND": { "configPath": "automation.prepare", "default": "pnpm prisma generate", "pattern": "^[^\\r\\n]*$" }
22
+ "PREPARE_COMMAND": { "configPath": "automation.prepare", "default": "if [ -e node_modules/.bin/prisma ]; then pnpm prisma generate; fi", "pattern": "^[^\\r\\n]*$" }
23
23
  }
24
24
  },
25
25
  "editor": {