generatesaas 1.9.3 → 1.11.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.
|
@@ -105,15 +105,22 @@ Creates:
|
|
|
105
105
|
file's project-relative path with `.diff` appended (e.g.
|
|
106
106
|
`references/diffs/packages/config/src/index.ts.diff`). Diffs cover `modified` +
|
|
107
107
|
`added` files; `unmodified`, `deleted`, and `removed` files have none.
|
|
108
|
-
- `references/update-manifest.json` - lists of added, modified, and removed files
|
|
108
|
+
- `references/update-manifest.json` - lists of added, modified, and removed files,
|
|
109
|
+
plus `renamed` (upstream renames detected by identical content: `[{from, to}]`)
|
|
110
|
+
and `sensitive` (database schema and `.env.example` changes that carry
|
|
111
|
+
follow-up work: migrations, new env vars)
|
|
109
112
|
|
|
110
113
|
### Step 2: Present Changelog
|
|
111
114
|
|
|
112
|
-
Read `references/changelog.md`.
|
|
115
|
+
Read `references/changelog.md`. When the update spans multiple releases, it contains one `# vX.Y.Z` section per release, oldest first - read ALL of them, not just the last; a breaking change can sit in any intermediate release. A `[BREAKING]` marker on a section header is authoritative (it comes from the version index: a major bump or release notes flagged BREAKING) - always call those releases out explicitly.
|
|
116
|
+
|
|
117
|
+
Each release's notes may be one of two shapes:
|
|
113
118
|
|
|
114
119
|
- **Curated** - contains explicit `## Breaking`, `## Migration`, `## Features`, `## Fixes` (or similar) sections. Use those sections verbatim; they are authoritative.
|
|
115
120
|
- **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
121
|
|
|
122
|
+
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.
|
|
123
|
+
|
|
117
124
|
Present a clear, organized summary:
|
|
118
125
|
|
|
119
126
|
> ## What's New in v{targetVersion}
|
|
@@ -276,6 +283,12 @@ If `.generatesaas/template/<path>` does not exist (backward compat), fall back t
|
|
|
276
283
|
|
|
277
284
|
#### For Removed Files
|
|
278
285
|
|
|
286
|
+
**Check for renames first.** Look up the file in `renamed` in `references/update-manifest.json`. If it is the `from` of a rename, the same upstream content now lives at the `to` path (auto-created in Step 6). Present it as a rename, not an unrelated removal:
|
|
287
|
+
|
|
288
|
+
- If the user never modified the old file: delete it and note the rename - nothing is lost.
|
|
289
|
+
- If the user **modified** the old file: their customizations exist nowhere in the new version. Port them to the `to` path (show the proposed result) before deleting the old file. Never let a rename silently drop customizations.
|
|
290
|
+
- Imports of the old path elsewhere in the project must be updated to the new path - run the import search below for the old path either way.
|
|
291
|
+
|
|
279
292
|
Before presenting each removed file, **search the entire project** for imports of it. Use grep to find `import ... from` or `require(...)` statements referencing the file's path (check relative paths, aliases, and package paths). This is critical - the classification script only scans files in the update, not user-created files that may also depend on the removed file.
|
|
280
293
|
|
|
281
294
|
```
|
|
@@ -377,6 +390,13 @@ After all files are processed:
|
|
|
377
390
|
|
|
378
391
|
If no files were held back, skip creating this file.
|
|
379
392
|
|
|
393
|
+
> Note: the completion script runs a safety net that auto-holds-back upstream
|
|
394
|
+
> changes which were demonstrably never applied (the file was not touched during
|
|
395
|
+
> the entire update). Do NOT rely on it - it cannot detect a file you edited
|
|
396
|
+
> during a merge or that the user customized between updates - always write
|
|
397
|
+
> held-back.json yourself. Treat its warnings as a signal that your bookkeeping
|
|
398
|
+
> missed something.
|
|
399
|
+
|
|
380
400
|
2. **Run the completion script:**
|
|
381
401
|
|
|
382
402
|
```bash
|
|
@@ -39,14 +39,17 @@ function hashFile(filePath) {
|
|
|
39
39
|
|
|
40
40
|
// ── File Walking ──
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
42
|
+
// MUST stay in sync with the CLI's template/exclusions.ts: exactly
|
|
43
|
+
// EXCLUDED_NAMES + SNAPSHOT_EXCLUDED_NAMES plus the two project-root extras
|
|
44
|
+
// ("data", INTERNAL_DIR). A drift silently corrupts classification - a path the
|
|
45
|
+
// CLI stages but this walker skips never gets diffed or hashed. The
|
|
46
|
+
// exclusions-parity test in the CLI repo asserts this set equality.
|
|
45
47
|
const WALK_EXCLUSIONS = new Set([
|
|
46
48
|
".git", "node_modules", ".pnpm-store", ".env", ".env.test",
|
|
47
49
|
".turbo", ".nuxt", ".output", ".data", "dist", "data",
|
|
48
50
|
".next", ".svelte-kit", ".wrangler",
|
|
49
51
|
".devcontainer", "playwright-report", "test-results",
|
|
52
|
+
"pnpm-lock.yaml",
|
|
50
53
|
INTERNAL_DIR,
|
|
51
54
|
]);
|
|
52
55
|
|
|
@@ -86,6 +89,8 @@ module.exports = {
|
|
|
86
89
|
ensureDir,
|
|
87
90
|
hashFile,
|
|
88
91
|
walkDir,
|
|
92
|
+
shouldExcludeWalk,
|
|
93
|
+
WALK_EXCLUSIONS,
|
|
89
94
|
INTERNAL_DIR,
|
|
90
95
|
MANIFEST_FILE,
|
|
91
96
|
HASHES_FILE,
|
|
@@ -15,43 +15,43 @@ const fs = require("node:fs");
|
|
|
15
15
|
const path = require("node:path");
|
|
16
16
|
const { findProjectRoot, hashFile, walkDir, ensureDir, MANIFEST_FILE, HASHES_FILE, TEMPLATE_HASHES_FILE, TEMPLATE_DIR, STAGING_DIR, STAGING_META_FILE, INTERNAL_DIR } = require("./_helpers.js");
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Safety net for the held-back list. An upstream change was demonstrably never
|
|
20
|
+
* applied when the file (a) really changed upstream (old template hash differs
|
|
21
|
+
* from staging), (b) still exists on disk, and (c) was not touched during the
|
|
22
|
+
* entire update (its disk hash still equals the previous hashes.json entry).
|
|
23
|
+
* Such files MUST be held back - otherwise the template baseline advances past
|
|
24
|
+
* a change the project never received and the next update's diff silently
|
|
25
|
+
* drops it. Returns the detected paths (sorted) and adds them to `heldBack`.
|
|
26
|
+
*
|
|
27
|
+
* This is a net, not a replacement for the AI-written held-back.json: a file
|
|
28
|
+
* the user customized since the last update and then kept during this one
|
|
29
|
+
* cannot be distinguished from a merged file by hashes alone.
|
|
30
|
+
*/
|
|
31
|
+
function applyHeldBackSafetyNet(root, stagingDir, stagingFiles, heldBack) {
|
|
32
|
+
const templateHashesPath = path.join(root, TEMPLATE_HASHES_FILE);
|
|
33
|
+
const hashesPath = path.join(root, HASHES_FILE);
|
|
34
|
+
const oldTemplateHashes = fs.existsSync(templateHashesPath)
|
|
35
|
+
? JSON.parse(fs.readFileSync(templateHashesPath, "utf-8"))
|
|
36
|
+
: {};
|
|
37
|
+
const prevProjectHashes = fs.existsSync(hashesPath)
|
|
38
|
+
? JSON.parse(fs.readFileSync(hashesPath, "utf-8"))
|
|
39
|
+
: {};
|
|
40
|
+
|
|
41
|
+
const netted = [];
|
|
42
|
+
for (const rel of stagingFiles) {
|
|
43
|
+
if (heldBack.has(rel)) continue;
|
|
44
|
+
const oldHash = oldTemplateHashes[rel];
|
|
45
|
+
const prevHash = prevProjectHashes[rel];
|
|
46
|
+
if (!oldHash || !prevHash) continue;
|
|
47
|
+
const diskPath = path.join(root, rel);
|
|
48
|
+
if (!fs.existsSync(diskPath)) continue;
|
|
49
|
+
if (hashFile(path.join(stagingDir, rel)) === oldHash) continue;
|
|
50
|
+
if (hashFile(diskPath) !== prevHash) continue;
|
|
51
|
+
heldBack.add(rel);
|
|
52
|
+
netted.push(rel);
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
return files;
|
|
54
|
+
return netted.sort();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -133,6 +133,15 @@ function main() {
|
|
|
133
133
|
console.log("Updating template directory from staging...");
|
|
134
134
|
const stagingFiles = new Set(walkDir(stagingDir, stagingDir));
|
|
135
135
|
|
|
136
|
+
// Safety net: auto-hold-back upstream changes that were demonstrably
|
|
137
|
+
// never applied but are missing from held-back.json.
|
|
138
|
+
const netted = applyHeldBackSafetyNet(root, stagingDir, stagingFiles, heldBack);
|
|
139
|
+
if (netted.length > 0) {
|
|
140
|
+
console.warn(`Safety net: ${netted.length} upstream change(s) were never applied and were missing from held-back.json.`);
|
|
141
|
+
console.warn("Holding them back so the next update re-surfaces them:");
|
|
142
|
+
for (const rel of netted) console.warn(` - ${rel}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
136
145
|
// Validate held-back entries (non-fatal). A held-back file is supposed to be
|
|
137
146
|
// a real upstream change the user chose to skip - preserving its old template
|
|
138
147
|
// version so the next update re-surfaces the missed change. Warn on entries
|
|
@@ -219,11 +228,10 @@ function main() {
|
|
|
219
228
|
|
|
220
229
|
// Re-hash all project files
|
|
221
230
|
console.log("Hashing project files...");
|
|
222
|
-
const files =
|
|
231
|
+
const files = walkDir(root, root);
|
|
223
232
|
const fileHashes = {};
|
|
224
|
-
for (const
|
|
225
|
-
|
|
226
|
-
fileHashes[rel] = hashFile(file);
|
|
233
|
+
for (const rel of files.sort()) {
|
|
234
|
+
fileHashes[rel] = hashFile(path.join(root, rel));
|
|
227
235
|
}
|
|
228
236
|
|
|
229
237
|
// Update version in manifest
|
|
@@ -9,7 +9,15 @@
|
|
|
9
9
|
const fs = require("node:fs");
|
|
10
10
|
const path = require("node:path");
|
|
11
11
|
const { execFileSync } = require("node:child_process");
|
|
12
|
-
const { findProjectRoot, ensureDir, hashFile, walkDir, TEMPLATE_HASHES_FILE, TEMPLATE_DIR, STAGING_DIR, STAGING_META_FILE } = require("./_helpers.js");
|
|
12
|
+
const { findProjectRoot, ensureDir, hashFile, walkDir, shouldExcludeWalk, TEMPLATE_HASHES_FILE, TEMPLATE_DIR, STAGING_DIR, STAGING_META_FILE } = require("./_helpers.js");
|
|
13
|
+
|
|
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.
|
|
17
|
+
const SENSITIVE_PATTERNS = [
|
|
18
|
+
/^packages\/database\/src\/db\//,
|
|
19
|
+
/(^|\/)\.env\.example$/,
|
|
20
|
+
];
|
|
13
21
|
|
|
14
22
|
function main() {
|
|
15
23
|
const root = findProjectRoot();
|
|
@@ -36,11 +44,17 @@ function main() {
|
|
|
36
44
|
|
|
37
45
|
console.log(`Preparing update: ${currentVersion} → ${targetVersion}`);
|
|
38
46
|
|
|
39
|
-
// Read old template hashes
|
|
47
|
+
// Read old template hashes. Entries that are no longer part of the template
|
|
48
|
+
// universe (e.g. pnpm-lock.yaml after an exclusions update) are dropped so
|
|
49
|
+
// they don't surface as phantom "removed upstream" files.
|
|
40
50
|
const templateHashesPath = path.join(root, TEMPLATE_HASHES_FILE);
|
|
41
|
-
const
|
|
51
|
+
const oldHashesRaw = fs.existsSync(templateHashesPath)
|
|
42
52
|
? JSON.parse(fs.readFileSync(templateHashesPath, "utf-8"))
|
|
43
53
|
: {};
|
|
54
|
+
const oldHashes = {};
|
|
55
|
+
for (const [filePath, hash] of Object.entries(oldHashesRaw)) {
|
|
56
|
+
if (!shouldExcludeWalk(filePath)) oldHashes[filePath] = hash;
|
|
57
|
+
}
|
|
44
58
|
|
|
45
59
|
// Walk staging dir and compute new template hashes
|
|
46
60
|
const stagingFiles = walkDir(stagingDir, stagingDir);
|
|
@@ -74,7 +88,25 @@ function main() {
|
|
|
74
88
|
modified.sort();
|
|
75
89
|
removed.sort();
|
|
76
90
|
|
|
91
|
+
// Detect upstream renames: a removed file whose baseline content reappears
|
|
92
|
+
// verbatim at exactly one added path (and vice versa). Surfaced so user
|
|
93
|
+
// customizations on the old path can be ported before it is deleted -
|
|
94
|
+
// otherwise a rename reads as an unrelated add + remove and customizations
|
|
95
|
+
// on the old path are silently lost.
|
|
96
|
+
const renamed = detectRenames(oldHashes, newHashes, added, removed);
|
|
97
|
+
|
|
98
|
+
// Flag changes that always carry follow-up work (migrations, new env vars).
|
|
99
|
+
const sensitive = [...modified, ...added]
|
|
100
|
+
.filter((filePath) => SENSITIVE_PATTERNS.some((pattern) => pattern.test(filePath)))
|
|
101
|
+
.sort();
|
|
102
|
+
|
|
77
103
|
console.log(` Added: ${added.length}, Modified: ${modified.length}, Removed: ${removed.length}`);
|
|
104
|
+
if (renamed.length > 0) {
|
|
105
|
+
console.log(` Renamed: ${renamed.length} (see update-manifest.json - port customizations before deleting the old paths)`);
|
|
106
|
+
}
|
|
107
|
+
if (sensitive.length > 0) {
|
|
108
|
+
console.log(` Sensitive (schema / env example): ${sensitive.length}`);
|
|
109
|
+
}
|
|
78
110
|
|
|
79
111
|
// Determine output directory
|
|
80
112
|
const scriptDir = __dirname;
|
|
@@ -124,13 +156,15 @@ function main() {
|
|
|
124
156
|
}
|
|
125
157
|
console.log(`Written: ${diffCount} diff files`);
|
|
126
158
|
|
|
127
|
-
// Write update manifest (same format as before)
|
|
159
|
+
// Write update manifest (same format as before, plus renamed + sensitive)
|
|
128
160
|
const updateManifest = {
|
|
129
161
|
currentVersion,
|
|
130
162
|
targetVersion,
|
|
131
163
|
added,
|
|
132
164
|
modified,
|
|
133
165
|
removed,
|
|
166
|
+
renamed,
|
|
167
|
+
sensitive,
|
|
134
168
|
};
|
|
135
169
|
fs.writeFileSync(
|
|
136
170
|
path.join(refsDir, "update-manifest.json"),
|
|
@@ -142,6 +176,35 @@ function main() {
|
|
|
142
176
|
console.log("\nNext: Review references/changelog.md, then run classify-files.js");
|
|
143
177
|
}
|
|
144
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Detect upstream renames by exact content hash: a removed path whose baseline
|
|
181
|
+
* hash matches exactly one added path's staging hash (1:1 both ways - ambiguous
|
|
182
|
+
* matches are skipped). Returns [{ from, to }] sorted by old path.
|
|
183
|
+
*/
|
|
184
|
+
function detectRenames(oldHashes, newHashes, added, removed) {
|
|
185
|
+
if (added.length === 0 || removed.length === 0) return [];
|
|
186
|
+
|
|
187
|
+
const addedByHash = new Map();
|
|
188
|
+
for (const filePath of added) {
|
|
189
|
+
const hash = newHashes[filePath];
|
|
190
|
+
addedByHash.set(hash, (addedByHash.get(hash) || []).concat(filePath));
|
|
191
|
+
}
|
|
192
|
+
const removedByHash = new Map();
|
|
193
|
+
for (const filePath of removed) {
|
|
194
|
+
const hash = oldHashes[filePath];
|
|
195
|
+
removedByHash.set(hash, (removedByHash.get(hash) || []).concat(filePath));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const renamed = [];
|
|
199
|
+
for (const [hash, fromPaths] of removedByHash) {
|
|
200
|
+
const toPaths = addedByHash.get(hash);
|
|
201
|
+
if (fromPaths.length === 1 && toPaths && toPaths.length === 1) {
|
|
202
|
+
renamed.push({ from: fromPaths[0], to: toPaths[0] });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return renamed.sort((a, b) => a.from.localeCompare(b.from));
|
|
206
|
+
}
|
|
207
|
+
|
|
145
208
|
/** Generate a unified diff between two files. Returns null if diff is unavailable. */
|
|
146
209
|
function generateDiff(userFile, stagingFile, relativePath) {
|
|
147
210
|
try {
|