generatesaas 2.3.0 → 3.0.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.
@@ -103,6 +103,11 @@ key once and saves it), then re-run the update. For a network error, ask them to
103
103
  > Note: this command also rewrites the skill scripts to their latest version. That is
104
104
  > expected - the scripts you run in the following steps are the freshly-installed ones.
105
105
 
106
+ **Re-read this SKILL.md now**, from `__SKILL_ROOT__/generatesaas-update/SKILL.md`. The command
107
+ just overwrote it along with the scripts, so the copy in your context is the PREVIOUS release's
108
+ instructions while every script you are about to run is the new one. Follow the file on disk
109
+ wherever the two disagree.
110
+
106
111
  **1b. Prepare the diff/classification data:**
107
112
 
108
113
  ```bash
@@ -117,8 +122,8 @@ Creates:
117
122
  `added` files; `unmodified`, `deleted`, and `removed` files have none.
118
123
  - `references/update-manifest.json` - lists of added, modified, and removed files,
119
124
  plus `renamed` (upstream renames detected by identical content: `[{from, to}]`)
120
- and `sensitive` (database schema and `.env.example` changes that carry
121
- follow-up work: migrations, new env vars)
125
+ and `sensitive` (database schema, `packages/database/drizzle/`, and
126
+ `.env.example` changes that carry follow-up work: migrations, new env vars)
122
127
 
123
128
  ### Step 2: Present Changelog
124
129
 
@@ -129,7 +134,7 @@ Each release's notes may be one of two shapes:
129
134
  - **Curated** - contains explicit `## Breaking`, `## Migration`, `## Features`, `## Fixes` (or similar) sections. Use those sections verbatim; they are authoritative.
130
135
  - **Raw** - an auto-generated flat list of change titles (one line per commit or PR, no sections). This is the common case. You can group the titles into features/fixes by reading them, but you **cannot** reliably infer breaking changes or migration steps from titles alone.
131
136
 
132
- Also read `sensitive` in `references/update-manifest.json`: database schema changes mean a migration is likely required, and `.env.example` changes mean new or changed environment variables. Mention both in the summary and again in the final post-update steps.
137
+ Also read `sensitive` in `references/update-manifest.json`: database schema changes mean a migration is likely required, anything under `packages/database/drizzle/` means upstream migrations that must be regenerated into the project's own history before they will run (see [Database](#database-schema-migrations)), and `.env.example` changes mean new or changed environment variables. Mention all three in the summary and again in the final post-update steps.
133
138
 
134
139
  Present a clear, organized summary:
135
140
 
@@ -154,7 +159,7 @@ it stays complete). Read `newOptions` from `.generatesaas/staging.json`. **If it
154
159
  empty, skip this step entirely.**
155
160
 
156
161
  Otherwise, present each option so the user can decide whether to adopt it. Each entry has
157
- `{ key, label, hint, default, impact, requiresLabel? }`. For each one:
162
+ `{ key, label, hint, default, impact, choices?, requiresLabel? }`. For each one:
158
163
 
159
164
  ```
160
165
  ### New since your version: {label}
@@ -172,14 +177,32 @@ it is a large surface you didn't originally choose" or "Safe to adopt - it is pu
172
177
  > 2. **Adopt it** - turn this feature on
173
178
  ```
174
179
 
180
+ **An entry with `choices` is not a yes/no.** It takes one of several values, so the two-way block
181
+ above cannot express it - offer the values as a numbered pick list instead, one line per choice,
182
+ with the entry's `default` marked as the current behavior:
183
+
184
+ ```
185
+ **Default:** `{default}` (your project's current behavior is unchanged).
186
+ **My recommendation:** {your recommendation, naming one of the choices}
187
+
188
+ > **Which would you like?**
189
+ > 1. **`{choices[0]}`** {"(current)" on the one equal to default}
190
+ > 2. **`{choices[1]}`**
191
+ > 3. **`{choices[2]}`** ...one line per choice, however many there are
192
+ ```
193
+
175
194
  **Ask the user about every new option, in every mode - including Automatic.** Surfacing a new
176
195
  feature is exactly the kind of decision the update must never assume. Give your recommendation,
177
196
  but the user decides.
178
197
 
179
198
  **Applying the decisions:**
180
199
 
181
- 1. For each option the user **adopts**, write its enabling value into `.generatesaas/manifest.json`
182
- (e.g. set `"desktop": true`). The manifest is the single shaping input.
200
+ 1. For each option the user **adopts**, write the value they picked into
201
+ `.generatesaas/manifest.json` under the entry's `key`. For an option with no `choices` that value
202
+ is `true` (e.g. set `"desktop": true`); for one with `choices` it is the choice they named,
203
+ verbatim (e.g. set `"errorTracking": "sentry"`). Never write `true` for an option that has
204
+ `choices` - the CLI refuses any value outside them and stages nothing at all. The manifest is the
205
+ single shaping input.
183
206
  2. **If the user adopted at least one option, re-stage once** so re-shaping produces the
184
207
  feature's files. Re-run the CLI (append `auto` if you are in Automatic mode, so the staged
185
208
  metadata keeps the unattended signal):
@@ -318,13 +341,15 @@ Preview first (especially useful in Careful mode or when unsure), then apply:
318
341
  node __SKILL_ROOT__/generatesaas-update/scripts/apply-auto.js --dry-run # lists every file it would update/create, changes nothing
319
342
  node __SKILL_ROOT__/generatesaas-update/scripts/apply-auto.js
320
343
  ```
321
- `apply-auto.js` copies only `unmodified` (safe replace) + `new` (genuinely-new trees). It will **never** touch `newInDeletedTree` files, and it aborts without writing anything if any source is missing from staging (no half-applied tree). Report what was applied. Mark all auto-update and new file items as `[x]` in the plan.
344
+ `apply-auto.js` copies only `unmodified` (safe replace) + `new` (any upstream-added file whose tree the buyer has not deleted). It will **never** touch `newInDeletedTree` files, and it aborts without writing anything if any source is missing from staging (no half-applied tree). Report what was applied. Mark all auto-update and new file items as `[x]` in the plan - **except any file the script reports as held back**, which was never applied and moves to **Skipped** with the regenerate reason. Marking a held-back migration `[x]` records a table that does not exist as delivered, in the one artifact that outlives the session.
345
+
346
+ It also holds back everything under `packages/database/drizzle/` and lists what it skipped. Those are NOT pending work you can copy in afterwards - migrations must be regenerated into the project's own history or they never run. See [Database](#database-schema-migrations).
322
347
 
323
348
  **Careful mode:**
324
349
  Present the list of safe auto-updates and new files to the user:
325
350
  > These {N} files haven't been customized and can be safely replaced with the new version. {N} new files will be created. Want me to apply all of these, or would you like to review any specific ones first?
326
351
 
327
- If the user wants to review specific files, show them the diff for each requested file. Apply only after approval.
352
+ If the user wants to review specific files, show them the diff for each requested file. Apply only after approval. Do not count held-back migrations in either number - they are not being created, and presenting them as applied is the same error as marking them `[x]`.
328
353
 
329
354
  ### Step 7: Review Modified Files
330
355
 
@@ -476,7 +501,7 @@ After all files are processed:
476
501
  **Do NOT include:**
477
502
  - Files where the user accepted a merge or chose "Use upstream version" (template changes were applied)
478
503
  - Removed files (not in staging - template-hashes handles these automatically)
479
- - New files, whether created or rejected (handled correctly by staging presence)
504
+ - New files, whether created or rejected. NB: that is only right for `newInDeletedTree`, where declining is a standing choice. A new file rejected for any other reason also stops being offered once the baseline advances, so say so when you record it as skipped
480
505
  - Any file where the template's intended change was successfully applied
481
506
 
482
507
  ```json
@@ -554,9 +579,26 @@ Config files are the most commonly customized.
554
579
  ### Database (schema, migrations)
555
580
 
556
581
  - NEVER auto-merge schema files - always present for user review
557
- - New migration files can be added directly
558
582
  - Warn about schema changes that may require new migrations
559
583
 
584
+ **Never copy an upstream migration into `packages/database/drizzle/` as-is, and never overwrite the project's `drizzle/meta/_journal.json` or `drizzle/meta/*_snapshot.json` with the upstream versions.** A project's migration history is its own: upstream changes get re-derived into it, never pasted over it. Regenerate instead:
585
+
586
+ 1. Merge the upstream schema change into `packages/database/src/db/*.ts` (user review first, as above).
587
+ 2. Run `pnpm --filter @repo/database generate`. Drizzle diffs the project's own latest snapshot against the merged schema and writes a fresh migration carrying a current timestamp, the next `idx`, a correct `prevId`, and a snapshot that describes the project's real schema.
588
+ 3. Open the upstream `.sql` under `.generatesaas/staging/packages/database/drizzle/` and copy any hand-written statements into the generated file: seeds, backfills, data migrations, `ON CONFLICT` guards. `generate` infers structure only, so those are lost otherwise.
589
+ 4. Run `pnpm --filter @repo/database migrate`.
590
+
591
+ **Why the upstream file cannot just be added.** Drizzle applies a migration only when its journal `when` value is greater than the `created_at` of the last migration already applied to the database. A project that generated its own migration after the upstream one was authored sits above that mark, so a copied-in upstream file is **skipped silently and permanently**: no error, no warning, no log line, and `migrate` still exits 0. The table or column it was supposed to create never exists, and the miss surfaces later as runtime failures on whatever code path needs it. Taking the upstream `_journal.json` and snapshots instead of the project's own is the second half of the same trap: the final snapshot then knows nothing about the project's own tables, so the next `generate` re-emits `CREATE TABLE` for tables that already exist.
592
+
593
+ **If step 2 reports no schema changes**, the upstream migration is data-only and there is nothing to regenerate. This is the ONLY case where the `.sql` is copied in by hand. Copying the file is not enough on its own: drizzle reads `_journal.json` and runs only the entries listed there, so a `.sql` sitting in the folder with no entry is never opened. Do all four, in this order:
594
+
595
+ 1. **Append an entry** to the project's own `_journal.json`, copied from the upstream entry. This is the load-bearing step - without it nothing else matters.
596
+ 2. Set that entry's `when` **greater than the largest `when` already applied** to the database, never the upstream value. Drizzle takes the highest `created_at` in `drizzle.__drizzle_migrations` as its floor and skips everything at or below it.
597
+ 3. Set that entry's `idx` to continue the project's own sequence (last `idx` + 1).
598
+ 4. Rename the `.sql` file so its name matches the entry's `tag`, or set the `tag` to the file's existing name. Drizzle loads the file **by tag** and throws if the two disagree.
599
+
600
+ Still do not bring the upstream `meta/*_snapshot.json`, here least of all. Nothing needs it: `migrate` never reads snapshots, and `generate` needs only ONE previous snapshot, which must be the project's own. Adding an upstream one breaks `generate` whichever way it sorts - `meta/` is read in filename order, so an upstream snapshot that sorts LAST silently becomes the diff base (re-emitting `CREATE TABLE` for the project's own tables, and `DROP TABLE` for anything upstream carries that the project removed), while one that sorts earlier collides on `prevId` with the project's own, which prints a warning, writes no migration, and still exits 0.
601
+
560
602
  ### Package Dependencies (`package.json`)
561
603
 
562
604
  - Add new dependencies from upstream
@@ -46,7 +46,7 @@ function hashFile(filePath) {
46
46
  // asserts this set equality and the root-only matching below.
47
47
  const WALK_EXCLUSIONS = new Set([
48
48
  ".git", "node_modules", ".pnpm-store", ".env", ".env.test",
49
- ".turbo", ".nuxt", ".output", ".data", "dist",
49
+ ".turbo", ".data", "dist",
50
50
  ".next", ".svelte-kit", ".wrangler",
51
51
  ".devcontainer", "playwright-report", "test-results",
52
52
  "pnpm-lock.yaml",
@@ -10,6 +10,9 @@ const fs = require("node:fs");
10
10
  const path = require("node:path");
11
11
  const { findProjectRoot, ensureDir, STAGING_DIR } = require("./_helpers.js");
12
12
 
13
+ /** Drizzle migration history. Held back from every auto-copy - see filesToCopy. */
14
+ const MIGRATION_DIR = "packages/database/drizzle/";
15
+
13
16
  function main() {
14
17
  const root = findProjectRoot();
15
18
  if (!root) {
@@ -36,12 +39,24 @@ function main() {
36
39
  process.exit(1);
37
40
  }
38
41
 
39
- // `new` only ever contains genuinely-new trees - files inside buyer-deleted
40
- // trees land in `newInDeletedTree`, which the AI handles by prompt (never here).
41
- const filesToCopy = [...classification.unmodified, ...classification.new];
42
+ // `new` is any upstream-added file whose tree the buyer has NOT deleted -
43
+ // files inside buyer-deleted trees land in `newInDeletedTree`, which the AI
44
+ // handles by prompt (never here).
45
+ const candidates = [...classification.unmodified, ...classification.new];
46
+
47
+ // Migrations are NEVER auto-applied, and both failure modes are silent.
48
+ // Drizzle runs a migration only when its journal `when` beats the newest one
49
+ // already applied, so an upstream .sql copied in below that mark never runs
50
+ // and `migrate` still exits 0; and an upstream meta snapshot becomes the diff
51
+ // base for the next `generate`, which then re-emits the buyer's own tables.
52
+ // The skill's Database section regenerates them into the project's own
53
+ // history instead, which is only possible if this step leaves them alone.
54
+ const heldMigrations = candidates.filter((f) => f.startsWith(MIGRATION_DIR));
55
+ const filesToCopy = candidates.filter((f) => !f.startsWith(MIGRATION_DIR));
42
56
 
43
57
  if (filesToCopy.length === 0) {
44
58
  console.log("No files to auto-update. All changed files need manual merge.");
59
+ reportHeldMigrations(heldMigrations);
45
60
  return;
46
61
  }
47
62
 
@@ -69,6 +84,7 @@ function main() {
69
84
  console.log(` ${unmodifiedSet.has(filePath) ? "update" : "create"} ${filePath}`);
70
85
  }
71
86
  console.log("\n[dry run] No files were modified.");
87
+ reportHeldMigrations(heldMigrations);
72
88
  return;
73
89
  }
74
90
 
@@ -102,9 +118,37 @@ function main() {
102
118
  }
103
119
  }
104
120
 
121
+ reportHeldMigrations(heldMigrations);
122
+
105
123
  console.log("\nAfter merging all files, run complete-update.js to finalize.");
106
124
  }
107
125
 
126
+ /**
127
+ * Report migration files deliberately NOT copied, so the AI knows they are
128
+ * waiting rather than assuming the tree is settled. Silent when there are none.
129
+ *
130
+ * States the whole procedure rather than pointing at SKILL.md. The AI loads
131
+ * SKILL.md at the start of the update, BEFORE step 1a refreshes the bundle, so
132
+ * for one release after this ships it is following the previous release's prose
133
+ * - which says migrations can be added directly. This output has to stand alone
134
+ * against that reader, or the fix reads as an obstacle to work around.
135
+ */
136
+ function reportHeldMigrations(heldMigrations) {
137
+ if (heldMigrations.length === 0) return;
138
+ console.log(`\n${heldMigrations.length} migration file(s) held back - NOT copied into the project:`);
139
+ for (const f of heldMigrations) console.log(` - ${f}`);
140
+ console.log("\nDo NOT copy these in by hand, and do not take the upstream meta/ snapshots either.");
141
+ console.log("Drizzle runs a migration only when its journal entry is newer than the newest one");
142
+ console.log("already applied, so a pasted-in file never runs, `migrate` still exits 0, and the");
143
+ console.log("table it should have created silently does not exist. Regenerate instead:");
144
+ console.log(" 1. Merge the upstream schema change into packages/database/src/db/");
145
+ console.log(" 2. Run: pnpm --filter @repo/database generate");
146
+ console.log(" 3. Copy any hand-written statements (seeds, backfills, data migrations) out of the");
147
+ console.log(" staged .sql at .generatesaas/staging/<path above> into the file step 2 wrote -");
148
+ console.log(" `generate` infers structure only, so those statements are lost otherwise");
149
+ console.log(" 4. Run: pnpm --filter @repo/database migrate");
150
+ }
151
+
108
152
  try {
109
153
  main();
110
154
  } catch (err) {
@@ -12,10 +12,13 @@ const { execFileSync } = require("node:child_process");
12
12
  const { findProjectRoot, ensureDir, hashFile, walkDir, shouldExcludeWalk, TEMPLATE_HASHES_FILE, TEMPLATE_DIR, STAGING_DIR, STAGING_META_FILE } = require("./_helpers.js");
13
13
 
14
14
  // Paths whose upstream changes always carry follow-up work for the user:
15
- // database schema changes may need a migration, .env.example changes may need
16
- // new environment variables. Surfaced as `sensitive` in update-manifest.json.
15
+ // database schema changes may need a migration, incoming migrations must be
16
+ // regenerated into the project's own history before drizzle will run them, and
17
+ // .env.example changes may need new environment variables. Surfaced as
18
+ // `sensitive` in update-manifest.json.
17
19
  const SENSITIVE_PATTERNS = [
18
20
  /^packages\/database\/src\/db\//,
21
+ /^packages\/database\/drizzle\//,
19
22
  /(^|\/)\.env\.example$/,
20
23
  ];
21
24
 
@@ -105,7 +108,7 @@ function main() {
105
108
  console.log(` Renamed: ${renamed.length} (see update-manifest.json - port customizations before deleting the old paths)`);
106
109
  }
107
110
  if (sensitive.length > 0) {
108
- console.log(` Sensitive (schema / env example): ${sensitive.length}`);
111
+ console.log(` Sensitive (schema / migrations / env example): ${sensitive.length}`);
109
112
  }
110
113
 
111
114
  // Determine output directory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generatesaas",
3
- "version": "2.3.0",
3
+ "version": "3.0.1",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding and managing GenerateSaaS projects",
6
6
  "license": "UNLICENSED",
@@ -26,7 +26,7 @@
26
26
  "@repo/tsconfig": "1.0.0"
27
27
  },
28
28
  "engines": {
29
- "node": ">=22"
29
+ "node": ">=22.18"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsup",