generatesaas 1.0.1 → 1.1.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.
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: update
3
- description: Update GenerateSaaS project to latest boilerplate version. Interactive process that classifies files, analyzes dependencies, and applies changes while preserving user customizations — with the user in control of every decision.
4
- disable-model-invocation: true
3
+ description: Update a GenerateSaaS project to the latest boilerplate version. Use this whenever the user asks to "update my GenerateSaaS project", update their GenerateSaaS boilerplate/template, or pull the latest GenerateSaaS changes. Fetches and stages the new version, classifies files, analyzes dependencies, and applies changes while preserving user customizations — the user controls every decision.
5
4
  ---
6
5
 
7
6
  # GenerateSaaS Update
@@ -19,7 +18,7 @@ Update your project to the latest GenerateSaaS boilerplate version while preserv
19
18
 
20
19
  ```
21
20
  Step 0: Pre-flight safety checks
22
- Step 1: Prepare update data (script)
21
+ Step 1: Fetch + stage + prepare update data (you run the CLI, then the script)
23
22
  Step 2: Present changelog to user
24
23
  Step 3: Classify files (script)
25
24
  Step 4: Analyze dependencies
@@ -30,13 +29,26 @@ Step 8: Post-update validation
30
29
  Step 9: Complete update (script)
31
30
  ```
32
31
 
32
+ **You own the whole flow.** When the user says "update my GenerateSaaS project," do
33
+ everything end-to-end — including fetching and staging the new version (Step 1).
34
+ The user does not run any CLI command themselves.
35
+
36
+ **What the staged update contains (important):** `generatesaas update` re-shapes the
37
+ new version's template into THIS project's exact configuration before staging — same
38
+ frontend, payment provider, database, feature flags, etc. So everything in
39
+ `.generatesaas/staging/` and the `.generatesaas/template/` baseline is buyer-shaped:
40
+ you will never see another framework's files, a provider the project doesn't use, the
41
+ boilerplate's internal tooling, or dev-only secrets. Every diff you surface already
42
+ applies to this project. The 3-way merge baseline (`.generatesaas/template/`) is a
43
+ faithful pristine copy of what this project started from — trust it.
44
+
33
45
  ---
34
46
 
35
47
  ### Step 0: Pre-flight
36
48
 
37
49
  Before starting the update:
38
50
 
39
- 1. **Check git status.** Run `git status`. If there are ANY uncommitted changes (staged, unstaged, or untracked files in tracked directories), **STOP immediately**. Do NOT proceed with the update. Tell the user:
51
+ 1. **Check git status.** Run `git status --porcelain -- . ':(exclude).generatesaas'` (or otherwise ignore changes under `.generatesaas/`). The update process writes its own artifacts there — `staging.json`, `manifest.json` (license refresh), `held-back.json`, hash files — so those must NOT count as a dirty tree. If, ignoring `.generatesaas/`, there are ANY uncommitted changes (staged, unstaged, or untracked files in tracked directories), **STOP immediately**. Do NOT proceed with the update. Tell the user:
40
52
 
41
53
  > You have uncommitted changes. The update cannot proceed until your working tree is clean — this ensures you have a safe rollback point if anything needs to be reverted.
42
54
  >
@@ -60,7 +72,27 @@ If the user chooses option 1, stop completely. Do not continue the update.
60
72
 
61
73
  If starting fresh, delete the old plan file. Note: files that were auto-applied in the previous attempt may now have V2 content but template-hashes.json still has V1 hashes — these will be correctly classified as "modified" (since they differ from the V1 template hash) and the AI will see they match the staging file, making them trivial merges.
62
74
 
63
- ### Step 1: Prepare Update Data
75
+ ### Step 1: Fetch, Stage, and Prepare Update Data
76
+
77
+ **1a. Stage the new version yourself.** Unless a staged update already exists
78
+ (`.generatesaas/staging.json` present), run the CLI to fetch the latest version and
79
+ stage it — re-shaped for this project's exact configuration:
80
+
81
+ ```bash
82
+ npx generatesaas@latest update
83
+ ```
84
+
85
+ This resolves the saved API key from `~/.generatesaas` (written at init), so it runs
86
+ non-interactively. It refreshes the skill files, downloads the new version, shapes it
87
+ into this project's configuration, and writes `.generatesaas/staging/` + `staging.json`.
88
+ If it reports "Already on the latest version," there is nothing to update — tell the
89
+ user and stop. If it fails for lack of an API key or network, ask the user to run
90
+ `npx generatesaas@latest update` themselves, then continue.
91
+
92
+ > Note: this command also rewrites the skill scripts to their latest version. That is
93
+ > expected — the scripts you run in the following steps are the freshly-installed ones.
94
+
95
+ **1b. Prepare the diff/classification data:**
64
96
 
65
97
  ```bash
66
98
  node __SKILL_ROOT__/generatesaas-update/scripts/prepare-update.js
@@ -68,19 +100,29 @@ node __SKILL_ROOT__/generatesaas-update/scripts/prepare-update.js
68
100
 
69
101
  Creates:
70
102
  - `references/changelog.md` — what changed and why
71
- - `references/diffs/*.diff` — unified diffs for each changed file
103
+ - `references/diffs/<path>.diff` — one unified diff per changed file, named by the
104
+ file's project-relative path with `.diff` appended (e.g.
105
+ `references/diffs/packages/config/src/index.ts.diff`). Diffs cover `modified` +
106
+ `added` files; `unmodified`, `deleted`, and `removed` files have none.
72
107
  - `references/update-manifest.json` — lists of added, modified, and removed files
73
108
 
74
109
  ### Step 2: Present Changelog
75
110
 
76
- Read `references/changelog.md` and present a clear, organized summary:
111
+ Read `references/changelog.md`. It may be one of two shapes:
112
+
113
+ - **Curated** — contains explicit `## Breaking`, `## Migration`, `## Features`, `## Fixes` (or similar) sections. Use those sections verbatim; they are authoritative.
114
+ - **Raw** — auto-generated release notes (a flat list of PR titles, no sections). This is the common case. You can group PR titles into features/fixes by reading them, but you **cannot** reliably infer breaking changes or migration steps from PR titles.
115
+
116
+ Present a clear, organized summary:
77
117
 
78
118
  > ## What's New in v{targetVersion}
79
119
  >
80
- > **Breaking changes:** {list or "None"}
120
+ > **Breaking changes:** {from a Breaking section, or "⚠️ Not specified in the changelog — review the diffs in Step 7 carefully, especially database/schema and config files"}
81
121
  > **New features:** {list}
82
122
  > **Bug fixes:** {list}
83
- > **Migration steps required:** {list or "None"}
123
+ > **Migration steps required:** {from a Migration section, or "⚠️ None listed — if this update changes the database schema or required env vars, you may still need to migrate. I'll flag schema/config changes as we review them."}
124
+
125
+ **Be honest about uncertainty.** Never present an inferred "Breaking changes: None" or "Migration steps: None" as if it were authoritative when the changelog is raw PR titles — say it's not specified and that you'll watch for schema/config/env changes during the file review. A silently-missed migration (e.g. a new DB column) is the worst failure mode.
84
126
 
85
127
  Then ask: *"Ready to proceed with the file analysis?"*
86
128
 
@@ -97,7 +139,8 @@ Read `references/classification.json`. It contains:
97
139
  - **unmodified** — file matches the original template, safe to replace
98
140
  - **modified** — user has customized this file, needs careful review
99
141
  - **deleted** — user deleted this file, skip update for it
100
- - **new** — file doesn't exist locally, safe to create
142
+ - **new** — file doesn't exist locally and its directory tree is genuinely new upstream, safe to create
143
+ - **newInDeletedTree** — file is new upstream BUT lives inside a directory tree the user intentionally deleted (the tree existed in the baseline and is now gone on disk). **Never auto-create these.** Ask the user whether to bring the tree back (see "For New Files In Deleted Trees" in Step 7).
101
144
  - **removed** — file was removed upstream, user decides whether to delete
102
145
  - **crossDependencies** — import relationships between categories (if detected)
103
146
 
@@ -110,9 +153,10 @@ This step prevents breaking the user's code through indirect effects.
110
153
  - `unmodifiedImportsModified` — auto-apply files that import from modified files
111
154
  - `newImportsModified` — new files that import from modified files
112
155
 
113
- 2. **For each cross-dependency**, read the relevant diff at `references/diffs/<path>.diff`:
114
- - If an auto-apply file's diff changes **exports, function signatures, types, or interfaces** that a modified file uses → promote it from `unmodified` to `modified` (it needs user review alongside its dependent)
115
- - If an auto-apply file imports from a modified file flag it as needing review after the modified file is handled
156
+ 2. **For each cross-dependency**, read the relevant diff at `references/diffs/<path>.diff`. Handle all three directions the classifier emits:
157
+ - **`modifiedImportsUnmodified`** — a modified file imports from an auto-apply (unmodified) file. If the auto-apply file's diff changes **exports, function signatures, types, or interfaces** that the modified file uses → promote it from `unmodified` to `modified` (it needs user review alongside its dependent, so the merge accounts for the new API).
158
+ - **`unmodifiedImportsModified`** an auto-apply file imports from a modified file. After the modified file is handled in Step 7, re-check this auto-apply file: if the user kept their version of the modified file but the auto-apply file's new version expects the upstream API, the auto-apply file may not be compatible. Flag it and revisit in the Step 7 cascade-awareness check.
159
+ - **`newImportsModified`** — a new file imports from a modified file. The new file will be auto-created in Step 6. After the modified file is handled, verify the new file's imports resolve against whatever version of the modified file the user ended up with (their merge, their kept version, or upstream). If the new file expects the upstream API but the user kept their version, surface it.
116
160
 
117
161
  3. **Update `references/classification.json`**: before making any changes, copy the file to `references/classification-original.json` as a backup. Then move any promoted files from the `unmodified` array to the `modified` array. This ensures `apply-auto.js` only touches truly safe files. If you need to redo the analysis, restore from the backup.
118
162
 
@@ -167,10 +211,13 @@ Then ask the user to choose their interaction level:
167
211
  Based on the user's chosen mode:
168
212
 
169
213
  **Recommended / Quick mode:**
214
+
215
+ Preview first (especially useful in Careful mode or when unsure), then apply:
170
216
  ```bash
217
+ node __SKILL_ROOT__/generatesaas-update/scripts/apply-auto.js --dry-run # lists every file it would update/create, changes nothing
171
218
  node __SKILL_ROOT__/generatesaas-update/scripts/apply-auto.js
172
219
  ```
173
- Report what was applied. Mark all auto-update and new file items as `[x]` in the plan.
220
+ `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.
174
221
 
175
222
  **Careful mode:**
176
223
  Present the list of safe auto-updates and new files to the user:
@@ -248,6 +295,23 @@ If ANY files import the removed file, always warn before deletion and explain wh
248
295
 
249
296
  Files the user intentionally deleted — skip silently. Just note in the plan: "Skipped — user previously deleted this file."
250
297
 
298
+ #### For New Files In Deleted Trees (`newInDeletedTree`)
299
+
300
+ These are files the new version adds inside a directory the user previously deleted (e.g. a feature package they removed). Auto-creating them would silently resurrect the deleted tree. Group them by their deleted ancestor directory and ask once per tree:
301
+
302
+ ```
303
+ ### New upstream files under `{deleted dir}/` (which you removed)
304
+
305
+ The new version adds {N} files under `{deleted dir}/` — a directory you previously deleted:
306
+ - `{path}` — {what it is}
307
+
308
+ > **What would you like to do?**
309
+ > 1. **Keep it deleted** — Skip these files (you removed this feature on purpose)
310
+ > 2. **Bring the tree back** — Create these files (you want this feature after all)
311
+ ```
312
+
313
+ Default to **keep deleted** unless the user says otherwise. If they keep it deleted, note in the plan and do nothing (the completion script handles the hashes correctly — these files simply won't exist).
314
+
251
315
  #### After Each File
252
316
 
253
317
  1. Apply the user's chosen action immediately
@@ -276,10 +340,11 @@ After all files are processed:
276
340
 
277
341
  1. **Check for obvious issues:**
278
342
  - Scan merged files for syntax errors or broken imports
279
- - If the project has TypeScript, suggest running type check
280
- - Note: auto-applied files may have changed their exports/API in ways that affect **user-created files** (files not part of the template). The classification script only scans template files for cross-dependencies. A type check is the best way to catch breakage in user-created files.
343
+ - If the project has TypeScript, run the type check (`pnpm check-types`)
344
+ - **Run the test suite (`pnpm test`) too — do not rely on the type check alone.** Most packages scope `check-types` to `src/**` and exclude `tests/**`, so a type error introduced into a **test file** during a merge will pass `check-types` silently. If any of your merges landed in a `tests/` file, the test suite is the only gate that exercises it. When in doubt, also type-check the specific merged test files directly (e.g. `npx tsc --noEmit <file>` with the package's tsconfig settings).
345
+ - Note: auto-applied files may have changed their exports/API in ways that affect **user-created files** (files not part of the template). The classification script only scans template files for cross-dependencies. A type check + test run is the best way to catch breakage in user-created files.
281
346
 
282
- > All changes have been applied. Would you like me to run a type check / build to verify nothing is broken? This is especially important since some auto-applied files may have changed their API, which could affect your custom files.
347
+ > All changes have been applied. I'll run the type check **and** the test suite to verify nothing is broken the type check alone skips test files, and some auto-applied files may have changed their API in ways that affect your custom code or tests.
283
348
 
284
349
  2. If issues are found, present them:
285
350
 
@@ -36,6 +36,8 @@ function main() {
36
36
  process.exit(1);
37
37
  }
38
38
 
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).
39
41
  const filesToCopy = [...classification.unmodified, ...classification.new];
40
42
 
41
43
  if (filesToCopy.length === 0) {
@@ -43,25 +45,42 @@ function main() {
43
45
  return;
44
46
  }
45
47
 
46
- console.log(`Copying ${filesToCopy.length} files from staging...`);
47
-
48
+ const dryRun = process.argv.includes("--dry-run");
48
49
  const unmodifiedSet = new Set(classification.unmodified);
50
+
51
+ // Pre-flight: every source must exist in staging. Abort BEFORE touching the
52
+ // project if any are missing — never leave a half-applied tree (there is no
53
+ // rollback besides git). Buyer-shaped staging should always contain them; a
54
+ // miss means staging is stale or incomplete.
55
+ const missing = filesToCopy.filter((f) => !fs.existsSync(path.join(stagingDir, f)));
56
+ if (missing.length > 0) {
57
+ console.error(`Error: ${missing.length} file(s) not found in staging — aborting without changes:`);
58
+ for (const f of missing) console.error(` - ${f}`);
59
+ console.error("Re-stage the update (run 'npx generatesaas@latest update') and retry.");
60
+ process.exit(1);
61
+ }
62
+
63
+ const willUpdate = filesToCopy.filter((f) => unmodifiedSet.has(f)).length;
64
+ const willCreate = filesToCopy.length - willUpdate;
65
+
66
+ if (dryRun) {
67
+ console.log(`[dry run] Would update ${willUpdate} file(s) and create ${willCreate} new file(s):`);
68
+ for (const filePath of filesToCopy) {
69
+ console.log(` ${unmodifiedSet.has(filePath) ? "update" : "create"} ${filePath}`);
70
+ }
71
+ console.log("\n[dry run] No files were modified.");
72
+ return;
73
+ }
74
+
75
+ console.log(`Copying ${filesToCopy.length} files from staging...`);
49
76
  let updated = 0;
50
77
  let created = 0;
51
- const failed = [];
52
78
 
53
79
  for (const filePath of filesToCopy) {
54
80
  const stagingPath = path.join(stagingDir, filePath);
55
81
  const fullPath = path.join(root, filePath);
56
-
57
- if (!fs.existsSync(stagingPath)) {
58
- failed.push(filePath);
59
- continue;
60
- }
61
-
62
82
  ensureDir(path.dirname(fullPath));
63
83
  fs.copyFileSync(stagingPath, fullPath);
64
-
65
84
  if (unmodifiedSet.has(filePath)) {
66
85
  updated++;
67
86
  } else {
@@ -71,13 +90,6 @@ function main() {
71
90
 
72
91
  console.log(`\nUpdated ${updated} files, created ${created} new files.`);
73
92
 
74
- if (failed.length > 0) {
75
- console.log(`\nWarning: ${failed.length} files not found in staging:`);
76
- for (const f of failed) {
77
- console.log(` - ${f}`);
78
- }
79
- }
80
-
81
93
  if (classification.modified.length > 0) {
82
94
  console.log(`\n${classification.modified.length} files still need manual merge.`);
83
95
  console.log("Review diffs in references/diffs/ and apply changes preserving your customizations.");
@@ -84,6 +84,29 @@ function matchImportToFile(importPath, fromFilePath, knownFileSet) {
84
84
  return null;
85
85
  }
86
86
 
87
+ /**
88
+ * Decide whether an upstream-added file (absent on disk) sits inside a directory
89
+ * tree the buyer intentionally DELETED, versus a brand-new tree upstream just
90
+ * introduced.
91
+ *
92
+ * Walks the file's ancestor directories shallow→deep. The first ancestor that is
93
+ * missing on disk is the boundary: if that directory HAD files in the old
94
+ * template baseline, the buyer removed a tree that used to exist → return it
95
+ * (don't silently recreate). If it never existed in the baseline, it's a
96
+ * legitimately new tree → return null (safe to auto-create).
97
+ */
98
+ function deletedTreeAncestor(filePath, root, baselineFiles) {
99
+ const parts = filePath.split("/");
100
+ for (let i = 1; i < parts.length; i++) {
101
+ const dir = parts.slice(0, i).join("/");
102
+ if (fs.existsSync(path.join(root, dir))) continue;
103
+ // First missing ancestor. Did it exist in the old template?
104
+ const existedInTemplate = baselineFiles.some((f) => f === dir || f.startsWith(dir + "/"));
105
+ return existedInTemplate ? dir : null;
106
+ }
107
+ return null;
108
+ }
109
+
87
110
  /**
88
111
  * Scan files for imports and build cross-dependency map between categories.
89
112
  */
@@ -96,6 +119,7 @@ function buildCrossDependencies(root, classification, stagingDir) {
96
119
  ...classification.unmodified,
97
120
  ...classification.modified,
98
121
  ...classification.new,
122
+ ...classification.newInDeletedTree,
99
123
  ...classification.deleted,
100
124
  ...classification.removed,
101
125
  ]);
@@ -212,9 +236,12 @@ function main() {
212
236
  modified: [],
213
237
  deleted: [],
214
238
  new: [],
239
+ newInDeletedTree: [],
215
240
  removed: [],
216
241
  };
217
242
 
243
+ const baselineFiles = Object.keys(baselineHashes);
244
+
218
245
  // Classify modified files (upstream changed, check if user also changed)
219
246
  for (const filePath of updateManifest.modified) {
220
247
  const fullPath = path.join(root, filePath);
@@ -242,6 +269,10 @@ function main() {
242
269
  const fullPath = path.join(root, filePath);
243
270
  if (fs.existsSync(fullPath)) {
244
271
  classification.modified.push(filePath);
272
+ } else if (deletedTreeAncestor(filePath, root, baselineFiles)) {
273
+ // Upstream added a file inside a directory tree the buyer deleted on
274
+ // purpose. Don't silently recreate the tree — surface for a decision.
275
+ classification.newInDeletedTree.push(filePath);
245
276
  } else {
246
277
  classification.new.push(filePath);
247
278
  }
@@ -274,13 +305,22 @@ function main() {
274
305
  console.log("File Classification Summary");
275
306
  console.log("===========================");
276
307
  console.log(`Target version: ${classification.targetVersion}`);
277
- console.log(` Unmodified (auto-update): ${classification.unmodified.length}`);
278
- console.log(` Modified (manual merge): ${classification.modified.length}`);
279
- console.log(` Deleted by user (skip): ${classification.deleted.length}`);
280
- console.log(` New files (auto-create): ${classification.new.length}`);
281
- console.log(` Removed upstream: ${classification.removed.length}`);
308
+ console.log(` Unmodified (auto-update): ${classification.unmodified.length}`);
309
+ console.log(` Modified (manual merge): ${classification.modified.length}`);
310
+ console.log(` Deleted by user (skip): ${classification.deleted.length}`);
311
+ console.log(` New files (auto-create): ${classification.new.length}`);
312
+ console.log(` New in deleted tree (ask): ${classification.newInDeletedTree.length}`);
313
+ console.log(` Removed upstream: ${classification.removed.length}`);
282
314
  console.log("");
283
315
 
316
+ if (classification.newInDeletedTree.length > 0) {
317
+ console.log("New files inside trees you deleted (do NOT auto-create — ask the user):");
318
+ for (const f of classification.newInDeletedTree) {
319
+ console.log(` - ${f}`);
320
+ }
321
+ console.log("");
322
+ }
323
+
284
324
  if (classification.modified.length > 0) {
285
325
  console.log("Files needing manual merge:");
286
326
  for (const f of classification.modified) {
@@ -96,6 +96,23 @@ function main() {
96
96
  console.log("Updating template directory from staging...");
97
97
  const stagingFiles = new Set(walkDir(stagingDir, stagingDir));
98
98
 
99
+ // Validate held-back entries (non-fatal). A held-back file is supposed to be
100
+ // a real upstream change the user chose to skip — preserving its old template
101
+ // version so the next update re-surfaces the missed change. Warn on entries
102
+ // that can't be that: a path not in staging (nothing to hold back), or a file
103
+ // identical between old template and staging (no upstream change to preserve).
104
+ // A wrong held-back entry silently corrupts the next update's diff baseline.
105
+ for (const rel of heldBack) {
106
+ if (!stagingFiles.has(rel)) {
107
+ console.warn(` Warning: held-back "${rel}" is not in staging — nothing to hold back. Check .generatesaas/held-back.json.`);
108
+ continue;
109
+ }
110
+ const oldTemplateFile = path.join(templateDir, rel);
111
+ if (fs.existsSync(oldTemplateFile) && hashFile(oldTemplateFile) === hashFile(path.join(stagingDir, rel))) {
112
+ console.warn(` Warning: held-back "${rel}" is identical in the old template and staging — no upstream change to preserve.`);
113
+ }
114
+ }
115
+
99
116
  // Copy staging files to template dir (skip held-back — old version stays)
100
117
  let copied = 0;
101
118
  let skipped = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generatesaas",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding and managing GenerateSaaS projects",
6
6
  "bin": {