generatesaas 0.7.1 → 0.8.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.
- package/dist/index.js +404 -109
- package/dist/skill/content/SKILL.md +289 -173
- package/dist/skill/content/scripts/_helpers.js +3 -1
- package/dist/skill/content/scripts/classify-files.js +218 -17
- package/dist/skill/content/scripts/complete-update.js +69 -11
- package/dist/skill/content/scripts/prepare-update.js +17 -4
- package/package.json +1 -1
|
@@ -2,13 +2,168 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Classify project files for update.
|
|
5
|
-
* Compares local file hashes against the
|
|
6
|
-
* the user has customized and which are safe to auto-update.
|
|
5
|
+
* Compares local file hashes against the TEMPLATE hashes (not project hashes)
|
|
6
|
+
* to determine which files the user has customized and which are safe to auto-update.
|
|
7
|
+
* Also scans imports to detect cross-dependencies between categories.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
const fs = require("node:fs");
|
|
10
11
|
const path = require("node:path");
|
|
11
|
-
const { findProjectRoot, hashFile, HASHES_FILE } = require("./_helpers.js");
|
|
12
|
+
const { findProjectRoot, hashFile, TEMPLATE_HASHES_FILE, HASHES_FILE, STAGING_DIR } = require("./_helpers.js");
|
|
13
|
+
|
|
14
|
+
// ── Import Scanning ──
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract local import paths from file content.
|
|
18
|
+
* Only captures relative (./, ../), alias (~/, @/), and # imports — skips packages.
|
|
19
|
+
*/
|
|
20
|
+
function extractImports(content, filePath) {
|
|
21
|
+
// For Vue SFCs, extract all script block contents (both <script> and <script setup>)
|
|
22
|
+
if (filePath.endsWith(".vue")) {
|
|
23
|
+
const blocks = [...content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)];
|
|
24
|
+
content = blocks.map((m) => m[1]).join("\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const imports = new Set();
|
|
28
|
+
|
|
29
|
+
const patterns = [
|
|
30
|
+
/(?:import|export)\s[\s\S]*?from\s+['"]([^'"]+)['"]/g,
|
|
31
|
+
/(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const pattern of patterns) {
|
|
35
|
+
let m;
|
|
36
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
37
|
+
const p = m[1];
|
|
38
|
+
if (p.startsWith(".") || p.startsWith("~/") || p.startsWith("@/") || p.startsWith("#")) {
|
|
39
|
+
imports.add(p);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [...imports];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Try to match an import specifier to a file path in the known set.
|
|
49
|
+
* Returns the matched path or null.
|
|
50
|
+
*/
|
|
51
|
+
function matchImportToFile(importPath, fromFilePath, knownFileSet) {
|
|
52
|
+
let basePath;
|
|
53
|
+
|
|
54
|
+
if (importPath.startsWith("./") || importPath.startsWith("../")) {
|
|
55
|
+
basePath = path.normalize(path.join(path.dirname(fromFilePath), importPath));
|
|
56
|
+
} else if (importPath.startsWith("~/") || importPath.startsWith("@/") || importPath.startsWith("#")) {
|
|
57
|
+
// Alias — strip prefix. ~ and @ typically resolve from the app root.
|
|
58
|
+
basePath = importPath.replace(/^[~@#]\//, "");
|
|
59
|
+
} else {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Normalize separators and strip leading ./
|
|
64
|
+
basePath = basePath.split(path.sep).join("/");
|
|
65
|
+
if (basePath.startsWith("./")) basePath = basePath.slice(2);
|
|
66
|
+
|
|
67
|
+
const extensions = ["", ".ts", ".js", ".vue", ".tsx", ".jsx", "/index.ts", "/index.js", "/index.vue"];
|
|
68
|
+
for (const ext of extensions) {
|
|
69
|
+
const candidate = basePath + ext;
|
|
70
|
+
if (knownFileSet.has(candidate)) return candidate;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// For alias imports, also try matching as a suffix of known files
|
|
74
|
+
if (!importPath.startsWith("./") && !importPath.startsWith("../")) {
|
|
75
|
+
const suffix = "/" + basePath;
|
|
76
|
+
for (const ext of extensions) {
|
|
77
|
+
const needle = suffix + ext;
|
|
78
|
+
for (const known of knownFileSet) {
|
|
79
|
+
if (known.endsWith(needle)) return known;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Scan files for imports and build cross-dependency map between categories.
|
|
89
|
+
*/
|
|
90
|
+
function buildCrossDependencies(root, classification, stagingDir) {
|
|
91
|
+
const unmodifiedSet = new Set(classification.unmodified);
|
|
92
|
+
const modifiedSet = new Set(classification.modified);
|
|
93
|
+
|
|
94
|
+
// All files involved in the update
|
|
95
|
+
const allUpdateFiles = new Set([
|
|
96
|
+
...classification.unmodified,
|
|
97
|
+
...classification.modified,
|
|
98
|
+
...classification.new,
|
|
99
|
+
...classification.deleted,
|
|
100
|
+
...classification.removed,
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
const crossDeps = {
|
|
104
|
+
modifiedImportsUnmodified: {},
|
|
105
|
+
unmodifiedImportsModified: {},
|
|
106
|
+
newImportsModified: {},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function addDep(bucket, key, value) {
|
|
110
|
+
if (!bucket[key]) bucket[key] = [];
|
|
111
|
+
if (!bucket[key].includes(value)) bucket[key].push(value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function scanFileContent(filePath, content, sourceCategory) {
|
|
115
|
+
const imports = extractImports(content, filePath);
|
|
116
|
+
for (const imp of imports) {
|
|
117
|
+
const resolved = matchImportToFile(imp, filePath, allUpdateFiles);
|
|
118
|
+
if (!resolved) continue;
|
|
119
|
+
|
|
120
|
+
if (sourceCategory === "modified" && unmodifiedSet.has(resolved)) {
|
|
121
|
+
addDep(crossDeps.modifiedImportsUnmodified, filePath, resolved);
|
|
122
|
+
}
|
|
123
|
+
if (sourceCategory === "unmodified" && modifiedSet.has(resolved)) {
|
|
124
|
+
addDep(crossDeps.unmodifiedImportsModified, filePath, resolved);
|
|
125
|
+
}
|
|
126
|
+
if (sourceCategory === "new" && modifiedSet.has(resolved)) {
|
|
127
|
+
addDep(crossDeps.newImportsModified, filePath, resolved);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function readSafe(fullPath) {
|
|
133
|
+
try { return fs.readFileSync(fullPath, "utf-8"); } catch { return null; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Scan modified files (user's current versions)
|
|
137
|
+
for (const f of classification.modified) {
|
|
138
|
+
const content = readSafe(path.join(root, f));
|
|
139
|
+
if (content) scanFileContent(f, content, "modified");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Scan unmodified files — both current AND staging versions
|
|
143
|
+
// (staging is what will actually be placed, so its imports matter)
|
|
144
|
+
for (const f of classification.unmodified) {
|
|
145
|
+
const content = readSafe(path.join(root, f));
|
|
146
|
+
if (content) scanFileContent(f, content, "unmodified");
|
|
147
|
+
|
|
148
|
+
const stagingContent = readSafe(path.join(stagingDir, f));
|
|
149
|
+
if (stagingContent) scanFileContent(f, stagingContent, "unmodified");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Scan new files from staging
|
|
153
|
+
for (const f of classification.new) {
|
|
154
|
+
const stagingContent = readSafe(path.join(stagingDir, f));
|
|
155
|
+
if (stagingContent) scanFileContent(f, stagingContent, "new");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Remove empty entries
|
|
159
|
+
for (const key of Object.keys(crossDeps)) {
|
|
160
|
+
if (Object.keys(crossDeps[key]).length === 0) delete crossDeps[key];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return Object.keys(crossDeps).length > 0 ? crossDeps : undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Main ──
|
|
12
167
|
|
|
13
168
|
function main() {
|
|
14
169
|
const root = findProjectRoot();
|
|
@@ -17,7 +172,6 @@ function main() {
|
|
|
17
172
|
process.exit(1);
|
|
18
173
|
}
|
|
19
174
|
|
|
20
|
-
// Determine the skill root
|
|
21
175
|
const scriptDir = __dirname;
|
|
22
176
|
const skillDir = path.dirname(scriptDir);
|
|
23
177
|
const refsDir = path.join(skillDir, "references");
|
|
@@ -29,10 +183,28 @@ function main() {
|
|
|
29
183
|
}
|
|
30
184
|
|
|
31
185
|
const updateManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
186
|
+
|
|
187
|
+
// Use template hashes (what the template looked like) for classification.
|
|
188
|
+
// This correctly detects user modifications even after previous merges,
|
|
189
|
+
// because merged files differ from the template while unmodified files match it.
|
|
190
|
+
// Falls back to project hashes for backward compatibility.
|
|
191
|
+
const templateHashesPath = path.join(root, TEMPLATE_HASHES_FILE);
|
|
32
192
|
const hashesPath = path.join(root, HASHES_FILE);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
193
|
+
|
|
194
|
+
let baselineHashes;
|
|
195
|
+
let hashSource;
|
|
196
|
+
if (fs.existsSync(templateHashesPath)) {
|
|
197
|
+
baselineHashes = JSON.parse(fs.readFileSync(templateHashesPath, "utf-8"));
|
|
198
|
+
hashSource = "template-hashes.json";
|
|
199
|
+
} else if (fs.existsSync(hashesPath)) {
|
|
200
|
+
baselineHashes = JSON.parse(fs.readFileSync(hashesPath, "utf-8"));
|
|
201
|
+
hashSource = "hashes.json (fallback)";
|
|
202
|
+
} else {
|
|
203
|
+
baselineHashes = {};
|
|
204
|
+
hashSource = "none (treating all as modified)";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log(`Using ${hashSource} for classification baseline.`);
|
|
36
208
|
|
|
37
209
|
const classification = {
|
|
38
210
|
targetVersion: updateManifest.targetVersion,
|
|
@@ -46,21 +218,20 @@ function main() {
|
|
|
46
218
|
// Classify modified files (upstream changed, check if user also changed)
|
|
47
219
|
for (const filePath of updateManifest.modified) {
|
|
48
220
|
const fullPath = path.join(root, filePath);
|
|
49
|
-
const
|
|
221
|
+
const baselineHash = baselineHashes[filePath];
|
|
50
222
|
|
|
51
223
|
if (!fs.existsSync(fullPath)) {
|
|
52
|
-
// User deleted this file
|
|
53
224
|
classification.deleted.push(filePath);
|
|
54
|
-
} else if (!
|
|
55
|
-
// No
|
|
225
|
+
} else if (!baselineHash) {
|
|
226
|
+
// No baseline hash — treat as modified to be safe
|
|
56
227
|
classification.modified.push(filePath);
|
|
57
228
|
} else {
|
|
58
229
|
const currentHash = hashFile(fullPath);
|
|
59
|
-
if (currentHash ===
|
|
60
|
-
// File
|
|
230
|
+
if (currentHash === baselineHash) {
|
|
231
|
+
// File matches the original template — safe to auto-update
|
|
61
232
|
classification.unmodified.push(filePath);
|
|
62
233
|
} else {
|
|
63
|
-
//
|
|
234
|
+
// File differs from template — user has customized
|
|
64
235
|
classification.modified.push(filePath);
|
|
65
236
|
}
|
|
66
237
|
}
|
|
@@ -70,7 +241,6 @@ function main() {
|
|
|
70
241
|
for (const filePath of updateManifest.added) {
|
|
71
242
|
const fullPath = path.join(root, filePath);
|
|
72
243
|
if (fs.existsSync(fullPath)) {
|
|
73
|
-
// File already exists locally — treat as modified for safety
|
|
74
244
|
classification.modified.push(filePath);
|
|
75
245
|
} else {
|
|
76
246
|
classification.new.push(filePath);
|
|
@@ -83,7 +253,13 @@ function main() {
|
|
|
83
253
|
if (fs.existsSync(fullPath)) {
|
|
84
254
|
classification.removed.push(filePath);
|
|
85
255
|
}
|
|
86
|
-
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Scan for cross-category dependencies
|
|
259
|
+
const stagingDir = path.join(root, STAGING_DIR);
|
|
260
|
+
const crossDeps = buildCrossDependencies(root, classification, stagingDir);
|
|
261
|
+
if (crossDeps) {
|
|
262
|
+
classification.crossDependencies = crossDeps;
|
|
87
263
|
}
|
|
88
264
|
|
|
89
265
|
// Write classification
|
|
@@ -94,6 +270,7 @@ function main() {
|
|
|
94
270
|
);
|
|
95
271
|
|
|
96
272
|
// Print summary
|
|
273
|
+
console.log("");
|
|
97
274
|
console.log("File Classification Summary");
|
|
98
275
|
console.log("===========================");
|
|
99
276
|
console.log(`Target version: ${classification.targetVersion}`);
|
|
@@ -120,7 +297,31 @@ function main() {
|
|
|
120
297
|
console.log("");
|
|
121
298
|
}
|
|
122
299
|
|
|
123
|
-
|
|
300
|
+
if (crossDeps) {
|
|
301
|
+
if (crossDeps.modifiedImportsUnmodified) {
|
|
302
|
+
console.log("Cross-dependencies: modified files import from unmodified (auto-apply) files:");
|
|
303
|
+
for (const [mod, deps] of Object.entries(crossDeps.modifiedImportsUnmodified)) {
|
|
304
|
+
console.log(` ${mod} imports: ${deps.join(", ")}`);
|
|
305
|
+
}
|
|
306
|
+
console.log("");
|
|
307
|
+
}
|
|
308
|
+
if (crossDeps.unmodifiedImportsModified) {
|
|
309
|
+
console.log("Cross-dependencies: unmodified (auto-apply) files import from modified files:");
|
|
310
|
+
for (const [unmod, deps] of Object.entries(crossDeps.unmodifiedImportsModified)) {
|
|
311
|
+
console.log(` ${unmod} imports: ${deps.join(", ")}`);
|
|
312
|
+
}
|
|
313
|
+
console.log("");
|
|
314
|
+
}
|
|
315
|
+
if (crossDeps.newImportsModified) {
|
|
316
|
+
console.log("Cross-dependencies: new files import from modified files:");
|
|
317
|
+
for (const [newFile, deps] of Object.entries(crossDeps.newImportsModified)) {
|
|
318
|
+
console.log(` ${newFile} imports: ${deps.join(", ")}`);
|
|
319
|
+
}
|
|
320
|
+
console.log("");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
console.log("Next: Review classification and cross-dependencies, then generate the update plan.");
|
|
124
325
|
}
|
|
125
326
|
|
|
126
327
|
try {
|
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Complete the update process.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Updates the stored template directory from staging, re-hashes all project
|
|
6
|
+
* files, derives template-hashes.json from the template directory, updates
|
|
7
|
+
* .generatesaas/manifest.json, and cleans up staging + references.
|
|
8
|
+
*
|
|
9
|
+
* Reads .generatesaas/held-back.json (if present) to skip updating template
|
|
10
|
+
* files that were held back — their old template version is preserved so the
|
|
11
|
+
* next update generates correct diffs that include the missed changes.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
const fs = require("node:fs");
|
|
10
15
|
const path = require("node:path");
|
|
11
|
-
const { findProjectRoot, hashFile, walkDir, MANIFEST_FILE, HASHES_FILE, TEMPLATE_HASHES_FILE, STAGING_DIR, STAGING_META_FILE, INTERNAL_DIR } = require("./_helpers.js");
|
|
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");
|
|
12
17
|
|
|
13
18
|
const HASH_EXCLUSIONS = new Set([
|
|
14
19
|
".git", "node_modules", ".pnpm-store", ".env", ".env.test",
|
|
@@ -56,7 +61,6 @@ function main() {
|
|
|
56
61
|
process.exit(1);
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
// Determine the skill root
|
|
60
64
|
const scriptDir = __dirname;
|
|
61
65
|
const skillDir = path.dirname(scriptDir);
|
|
62
66
|
const refsDir = path.join(skillDir, "references");
|
|
@@ -72,16 +76,65 @@ function main() {
|
|
|
72
76
|
|
|
73
77
|
console.log(`Completing update to version ${targetVersion}...`);
|
|
74
78
|
|
|
75
|
-
//
|
|
79
|
+
// Read held-back files list (created by the AI for skipped/held-back files)
|
|
80
|
+
const heldBackPath = path.join(root, INTERNAL_DIR, "held-back.json");
|
|
81
|
+
const heldBack = new Set(
|
|
82
|
+
fs.existsSync(heldBackPath)
|
|
83
|
+
? JSON.parse(fs.readFileSync(heldBackPath, "utf-8"))
|
|
84
|
+
: []
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (heldBack.size > 0) {
|
|
88
|
+
console.log(`${heldBack.size} files were held back — preserving their old template versions.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Update template directory from staging ──
|
|
76
92
|
const stagingDir = path.join(root, STAGING_DIR);
|
|
93
|
+
const templateDir = path.join(root, TEMPLATE_DIR);
|
|
94
|
+
|
|
77
95
|
if (fs.existsSync(stagingDir)) {
|
|
78
|
-
console.log("
|
|
79
|
-
const stagingFiles = walkDir(stagingDir, stagingDir);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
console.log("Updating template directory from staging...");
|
|
97
|
+
const stagingFiles = new Set(walkDir(stagingDir, stagingDir));
|
|
98
|
+
|
|
99
|
+
// Copy staging files to template dir (skip held-back — old version stays)
|
|
100
|
+
let copied = 0;
|
|
101
|
+
let skipped = 0;
|
|
102
|
+
for (const rel of stagingFiles) {
|
|
103
|
+
if (heldBack.has(rel)) {
|
|
104
|
+
skipped++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const src = path.join(stagingDir, rel);
|
|
108
|
+
const dest = path.join(templateDir, rel);
|
|
109
|
+
ensureDir(path.dirname(dest));
|
|
110
|
+
fs.copyFileSync(src, dest);
|
|
111
|
+
copied++;
|
|
112
|
+
}
|
|
113
|
+
console.log(` Copied ${copied} files to template dir (${skipped} held back).`);
|
|
114
|
+
|
|
115
|
+
// Remove files from template dir that were removed upstream
|
|
116
|
+
if (fs.existsSync(templateDir)) {
|
|
117
|
+
const templateFiles = walkDir(templateDir, templateDir);
|
|
118
|
+
let removed = 0;
|
|
119
|
+
for (const rel of templateFiles) {
|
|
120
|
+
if (!stagingFiles.has(rel) && !heldBack.has(rel)) {
|
|
121
|
+
fs.rmSync(path.join(templateDir, rel));
|
|
122
|
+
removed++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (removed > 0) {
|
|
126
|
+
console.log(` Removed ${removed} files no longer in template.`);
|
|
127
|
+
}
|
|
83
128
|
}
|
|
129
|
+
|
|
130
|
+
// Derive template-hashes.json from the template directory (source of truth)
|
|
131
|
+
console.log("Computing template hashes from template directory...");
|
|
84
132
|
const templateHashesPath = path.join(root, TEMPLATE_HASHES_FILE);
|
|
133
|
+
const allTemplateFiles = walkDir(templateDir, templateDir);
|
|
134
|
+
const templateHashes = {};
|
|
135
|
+
for (const rel of allTemplateFiles.sort()) {
|
|
136
|
+
templateHashes[rel] = hashFile(path.join(templateDir, rel));
|
|
137
|
+
}
|
|
85
138
|
fs.mkdirSync(path.dirname(templateHashesPath), { recursive: true });
|
|
86
139
|
fs.writeFileSync(templateHashesPath, JSON.stringify(templateHashes, null, "\t") + "\n", "utf-8");
|
|
87
140
|
console.log(`Tracked ${Object.keys(templateHashes).length} template hashes in ${TEMPLATE_HASHES_FILE}`);
|
|
@@ -90,13 +143,18 @@ function main() {
|
|
|
90
143
|
// Clean up staging
|
|
91
144
|
if (fs.existsSync(stagingDir)) {
|
|
92
145
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
93
|
-
console.log("Cleaned up staging directory");
|
|
146
|
+
console.log("Cleaned up staging directory.");
|
|
94
147
|
}
|
|
95
148
|
const metaPath = path.join(root, STAGING_META_FILE);
|
|
96
149
|
if (fs.existsSync(metaPath)) {
|
|
97
150
|
fs.rmSync(metaPath);
|
|
98
151
|
}
|
|
99
152
|
|
|
153
|
+
// Clean up held-back file
|
|
154
|
+
if (fs.existsSync(heldBackPath)) {
|
|
155
|
+
fs.rmSync(heldBackPath);
|
|
156
|
+
}
|
|
157
|
+
|
|
100
158
|
// Clean up references BEFORE hashing to avoid tracking temp files
|
|
101
159
|
if (fs.existsSync(refsDir)) {
|
|
102
160
|
fs.rmSync(refsDir, { recursive: true, force: true });
|
|
@@ -9,7 +9,7 @@
|
|
|
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, STAGING_DIR, STAGING_META_FILE } = require("./_helpers.js");
|
|
12
|
+
const { findProjectRoot, ensureDir, hashFile, walkDir, TEMPLATE_HASHES_FILE, TEMPLATE_DIR, STAGING_DIR, STAGING_META_FILE } = require("./_helpers.js");
|
|
13
13
|
|
|
14
14
|
function main() {
|
|
15
15
|
const root = findProjectRoot();
|
|
@@ -91,17 +91,30 @@ function main() {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// Generate and write diffs for modified files
|
|
94
|
+
// Uses stored template files when available for pure upstream diffs (old template → new template).
|
|
95
|
+
// Falls back to user file → staging if no stored template exists (backward compat).
|
|
94
96
|
const diffsDir = path.join(refsDir, "diffs");
|
|
95
97
|
ensureDir(diffsDir);
|
|
96
98
|
let diffCount = 0;
|
|
97
99
|
|
|
100
|
+
const templateDir = path.join(root, TEMPLATE_DIR);
|
|
101
|
+
|
|
98
102
|
for (const filePath of [...modified, ...added]) {
|
|
99
|
-
const userFile = path.join(root, filePath);
|
|
100
103
|
const stagingFile = path.join(stagingDir, filePath);
|
|
104
|
+
const templateFile = path.join(templateDir, filePath);
|
|
105
|
+
const userFile = path.join(root, filePath);
|
|
101
106
|
|
|
102
|
-
if (!fs.existsSync(
|
|
107
|
+
if (!fs.existsSync(stagingFile)) continue;
|
|
108
|
+
|
|
109
|
+
let diff;
|
|
110
|
+
if (fs.existsSync(templateFile)) {
|
|
111
|
+
// Three-way capable: diff old template vs new template (pure upstream changes)
|
|
112
|
+
diff = generateDiff(templateFile, stagingFile, filePath);
|
|
113
|
+
} else if (fs.existsSync(userFile)) {
|
|
114
|
+
// Fallback: diff user file vs staging (old behavior)
|
|
115
|
+
diff = generateDiff(userFile, stagingFile, filePath);
|
|
116
|
+
}
|
|
103
117
|
|
|
104
|
-
const diff = generateDiff(userFile, stagingFile, filePath);
|
|
105
118
|
if (diff) {
|
|
106
119
|
const diffPath = path.join(diffsDir, filePath + ".diff");
|
|
107
120
|
ensureDir(path.dirname(diffPath));
|