generatesaas 1.0.1 → 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:
|
|
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
|
|
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
|
|
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:** {
|
|
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:** {
|
|
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
|
-
-
|
|
115
|
-
-
|
|
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,
|
|
280
|
-
-
|
|
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.
|
|
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
|
-
|
|
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):
|
|
278
|
-
console.log(` Modified (manual merge):
|
|
279
|
-
console.log(` Deleted by user (skip):
|
|
280
|
-
console.log(` New files (auto-create):
|
|
281
|
-
console.log(`
|
|
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;
|