generatesaas 1.0.0 → 1.1.0

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.
@@ -19,7 +19,7 @@ Update your project to the latest GenerateSaaS boilerplate version while preserv
19
19
 
20
20
  ```
21
21
  Step 0: Pre-flight safety checks
22
- Step 1: Prepare update data (script)
22
+ Step 1: Fetch + stage + prepare update data (you run the CLI, then the script)
23
23
  Step 2: Present changelog to user
24
24
  Step 3: Classify files (script)
25
25
  Step 4: Analyze dependencies
@@ -30,13 +30,26 @@ Step 8: Post-update validation
30
30
  Step 9: Complete update (script)
31
31
  ```
32
32
 
33
+ **You own the whole flow.** When the user says "update my GenerateSaaS project," do
34
+ everything end-to-end — including fetching and staging the new version (Step 1).
35
+ The user does not run any CLI command themselves.
36
+
37
+ **What the staged update contains (important):** `generatesaas update` re-shapes the
38
+ new version's template into THIS project's exact configuration before staging — same
39
+ frontend, payment provider, database, feature flags, etc. So everything in
40
+ `.generatesaas/staging/` and the `.generatesaas/template/` baseline is buyer-shaped:
41
+ you will never see another framework's files, a provider the project doesn't use, the
42
+ boilerplate's internal tooling, or dev-only secrets. Every diff you surface already
43
+ applies to this project. The 3-way merge baseline (`.generatesaas/template/`) is a
44
+ faithful pristine copy of what this project started from — trust it.
45
+
33
46
  ---
34
47
 
35
48
  ### Step 0: Pre-flight
36
49
 
37
50
  Before starting the update:
38
51
 
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:
52
+ 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
53
 
41
54
  > 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
55
  >
@@ -60,7 +73,27 @@ If the user chooses option 1, stop completely. Do not continue the update.
60
73
 
61
74
  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
75
 
63
- ### Step 1: Prepare Update Data
76
+ ### Step 1: Fetch, Stage, and Prepare Update Data
77
+
78
+ **1a. Stage the new version yourself.** Unless a staged update already exists
79
+ (`.generatesaas/staging.json` present), run the CLI to fetch the latest version and
80
+ stage it — re-shaped for this project's exact configuration:
81
+
82
+ ```bash
83
+ npx generatesaas@latest update
84
+ ```
85
+
86
+ This resolves the saved API key from `~/.generatesaas` (written at init), so it runs
87
+ non-interactively. It refreshes the skill files, downloads the new version, shapes it
88
+ into this project's configuration, and writes `.generatesaas/staging/` + `staging.json`.
89
+ If it reports "Already on the latest version," there is nothing to update — tell the
90
+ user and stop. If it fails for lack of an API key or network, ask the user to run
91
+ `npx generatesaas@latest update` themselves, then continue.
92
+
93
+ > Note: this command also rewrites the skill scripts to their latest version. That is
94
+ > expected — the scripts you run in the following steps are the freshly-installed ones.
95
+
96
+ **1b. Prepare the diff/classification data:**
64
97
 
65
98
  ```bash
66
99
  node __SKILL_ROOT__/generatesaas-update/scripts/prepare-update.js
@@ -68,19 +101,29 @@ node __SKILL_ROOT__/generatesaas-update/scripts/prepare-update.js
68
101
 
69
102
  Creates:
70
103
  - `references/changelog.md` — what changed and why
71
- - `references/diffs/*.diff` — unified diffs for each changed file
104
+ - `references/diffs/<path>.diff` — one unified diff per changed file, named by the
105
+ file's project-relative path with `.diff` appended (e.g.
106
+ `references/diffs/packages/config/src/index.ts.diff`). Diffs cover `modified` +
107
+ `added` files; `unmodified`, `deleted`, and `removed` files have none.
72
108
  - `references/update-manifest.json` — lists of added, modified, and removed files
73
109
 
74
110
  ### Step 2: Present Changelog
75
111
 
76
- Read `references/changelog.md` and present a clear, organized summary:
112
+ Read `references/changelog.md`. It may be one of two shapes:
113
+
114
+ - **Curated** — contains explicit `## Breaking`, `## Migration`, `## Features`, `## Fixes` (or similar) sections. Use those sections verbatim; they are authoritative.
115
+ - **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.
116
+
117
+ Present a clear, organized summary:
77
118
 
78
119
  > ## What's New in v{targetVersion}
79
120
  >
80
- > **Breaking changes:** {list or "None"}
121
+ > **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
122
  > **New features:** {list}
82
123
  > **Bug fixes:** {list}
83
- > **Migration steps required:** {list or "None"}
124
+ > **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."}
125
+
126
+ **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
127
 
85
128
  Then ask: *"Ready to proceed with the file analysis?"*
86
129
 
@@ -97,7 +140,8 @@ Read `references/classification.json`. It contains:
97
140
  - **unmodified** — file matches the original template, safe to replace
98
141
  - **modified** — user has customized this file, needs careful review
99
142
  - **deleted** — user deleted this file, skip update for it
100
- - **new** — file doesn't exist locally, safe to create
143
+ - **new** — file doesn't exist locally and its directory tree is genuinely new upstream, safe to create
144
+ - **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
145
  - **removed** — file was removed upstream, user decides whether to delete
102
146
  - **crossDependencies** — import relationships between categories (if detected)
103
147
 
@@ -110,9 +154,10 @@ This step prevents breaking the user's code through indirect effects.
110
154
  - `unmodifiedImportsModified` — auto-apply files that import from modified files
111
155
  - `newImportsModified` — new files that import from modified files
112
156
 
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
157
+ 2. **For each cross-dependency**, read the relevant diff at `references/diffs/<path>.diff`. Handle all three directions the classifier emits:
158
+ - **`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).
159
+ - **`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.
160
+ - **`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
161
 
117
162
  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
163
 
@@ -167,10 +212,13 @@ Then ask the user to choose their interaction level:
167
212
  Based on the user's chosen mode:
168
213
 
169
214
  **Recommended / Quick mode:**
215
+
216
+ Preview first (especially useful in Careful mode or when unsure), then apply:
170
217
  ```bash
218
+ node __SKILL_ROOT__/generatesaas-update/scripts/apply-auto.js --dry-run # lists every file it would update/create, changes nothing
171
219
  node __SKILL_ROOT__/generatesaas-update/scripts/apply-auto.js
172
220
  ```
173
- Report what was applied. Mark all auto-update and new file items as `[x]` in the plan.
221
+ `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
222
 
175
223
  **Careful mode:**
176
224
  Present the list of safe auto-updates and new files to the user:
@@ -248,6 +296,23 @@ If ANY files import the removed file, always warn before deletion and explain wh
248
296
 
249
297
  Files the user intentionally deleted — skip silently. Just note in the plan: "Skipped — user previously deleted this file."
250
298
 
299
+ #### For New Files In Deleted Trees (`newInDeletedTree`)
300
+
301
+ 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:
302
+
303
+ ```
304
+ ### New upstream files under `{deleted dir}/` (which you removed)
305
+
306
+ The new version adds {N} files under `{deleted dir}/` — a directory you previously deleted:
307
+ - `{path}` — {what it is}
308
+
309
+ > **What would you like to do?**
310
+ > 1. **Keep it deleted** — Skip these files (you removed this feature on purpose)
311
+ > 2. **Bring the tree back** — Create these files (you want this feature after all)
312
+ ```
313
+
314
+ 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).
315
+
251
316
  #### After Each File
252
317
 
253
318
  1. Apply the user's chosen action immediately
@@ -276,10 +341,11 @@ After all files are processed:
276
341
 
277
342
  1. **Check for obvious issues:**
278
343
  - 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.
344
+ - If the project has TypeScript, run the type check (`pnpm check-types`)
345
+ - **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).
346
+ - 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
347
 
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.
348
+ > 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
349
 
284
350
  2. If issues are found, present them:
285
351
 
@@ -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.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding and managing GenerateSaaS projects",
6
6
  "bin": {