generatesaas 3.6.0 → 3.8.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.
- package/dist/generators/content/AGENTS.template.md +1 -1
- package/dist/index.js +392 -168
- package/dist/skill/content/generatesaas-update/SKILL.md +121 -246
- package/dist/skill/content/generatesaas-update/scripts/classify-files.js +43 -14
- package/dist/skill/content/generatesaas-update/scripts/complete-update.js +40 -13
- package/dist/skill/content/generatesaas-update/scripts/prepare-update.js +21 -7
- package/package.json +4 -1
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
const fs = require("node:fs");
|
|
11
11
|
const path = require("node:path");
|
|
12
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
findProjectRoot,
|
|
14
|
+
hashFile,
|
|
15
|
+
TEMPLATE_HASHES_FILE,
|
|
16
|
+
HASHES_FILE,
|
|
17
|
+
STAGING_DIR
|
|
18
|
+
} = require("./_helpers.js");
|
|
13
19
|
|
|
14
20
|
// ── Import Scanning ──
|
|
15
21
|
|
|
@@ -28,16 +34,17 @@ function extractImports(content, filePath) {
|
|
|
28
34
|
|
|
29
35
|
const patterns = [
|
|
30
36
|
/(?:import|export)\s[\s\S]*?from\s+['"]([^'"]+)['"]/g,
|
|
31
|
-
/(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g
|
|
37
|
+
/(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g
|
|
32
38
|
];
|
|
33
39
|
|
|
34
40
|
for (const pattern of patterns) {
|
|
35
|
-
let m;
|
|
36
|
-
while (
|
|
41
|
+
let m = pattern.exec(content);
|
|
42
|
+
while (m !== null) {
|
|
37
43
|
const p = m[1];
|
|
38
44
|
if (p.startsWith(".") || p.startsWith("~/") || p.startsWith("@/") || p.startsWith("#")) {
|
|
39
45
|
imports.add(p);
|
|
40
46
|
}
|
|
47
|
+
m = pattern.exec(content);
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
50
|
|
|
@@ -53,7 +60,11 @@ function matchImportToFile(importPath, fromFilePath, knownFileSet) {
|
|
|
53
60
|
|
|
54
61
|
if (importPath.startsWith("./") || importPath.startsWith("../")) {
|
|
55
62
|
basePath = path.normalize(path.join(path.dirname(fromFilePath), importPath));
|
|
56
|
-
} else if (
|
|
63
|
+
} else if (
|
|
64
|
+
importPath.startsWith("~/") ||
|
|
65
|
+
importPath.startsWith("@/") ||
|
|
66
|
+
importPath.startsWith("#")
|
|
67
|
+
) {
|
|
57
68
|
// Alias - strip prefix. ~ and @ typically resolve from the app root.
|
|
58
69
|
basePath = importPath.replace(/^[~@#]\//, "");
|
|
59
70
|
} else {
|
|
@@ -64,7 +75,19 @@ function matchImportToFile(importPath, fromFilePath, knownFileSet) {
|
|
|
64
75
|
basePath = basePath.split(path.sep).join("/");
|
|
65
76
|
if (basePath.startsWith("./")) basePath = basePath.slice(2);
|
|
66
77
|
|
|
67
|
-
const extensions = [
|
|
78
|
+
const extensions = [
|
|
79
|
+
"",
|
|
80
|
+
".ts",
|
|
81
|
+
".js",
|
|
82
|
+
".vue",
|
|
83
|
+
".tsx",
|
|
84
|
+
".jsx",
|
|
85
|
+
"/index.ts",
|
|
86
|
+
"/index.js",
|
|
87
|
+
"/index.tsx",
|
|
88
|
+
"/index.jsx",
|
|
89
|
+
"/index.vue"
|
|
90
|
+
];
|
|
68
91
|
for (const ext of extensions) {
|
|
69
92
|
const candidate = basePath + ext;
|
|
70
93
|
if (knownFileSet.has(candidate)) return candidate;
|
|
@@ -72,7 +95,7 @@ function matchImportToFile(importPath, fromFilePath, knownFileSet) {
|
|
|
72
95
|
|
|
73
96
|
// For alias imports, also try matching as a suffix of known files
|
|
74
97
|
if (!importPath.startsWith("./") && !importPath.startsWith("../")) {
|
|
75
|
-
const suffix =
|
|
98
|
+
const suffix = `/${basePath}`;
|
|
76
99
|
for (const ext of extensions) {
|
|
77
100
|
const needle = suffix + ext;
|
|
78
101
|
for (const known of knownFileSet) {
|
|
@@ -101,7 +124,7 @@ function deletedTreeAncestor(filePath, root, baselineFiles) {
|
|
|
101
124
|
const dir = parts.slice(0, i).join("/");
|
|
102
125
|
if (fs.existsSync(path.join(root, dir))) continue;
|
|
103
126
|
// First missing ancestor. Did it exist in the old template?
|
|
104
|
-
const existedInTemplate = baselineFiles.some((f) => f === dir || f.startsWith(dir
|
|
127
|
+
const existedInTemplate = baselineFiles.some((f) => f === dir || f.startsWith(`${dir}/`));
|
|
105
128
|
return existedInTemplate ? dir : null;
|
|
106
129
|
}
|
|
107
130
|
return null;
|
|
@@ -121,13 +144,13 @@ function buildCrossDependencies(root, classification, stagingDir) {
|
|
|
121
144
|
...classification.new,
|
|
122
145
|
...classification.newInDeletedTree,
|
|
123
146
|
...classification.deleted,
|
|
124
|
-
...classification.removed
|
|
147
|
+
...classification.removed
|
|
125
148
|
]);
|
|
126
149
|
|
|
127
150
|
const crossDeps = {
|
|
128
151
|
modifiedImportsUnmodified: {},
|
|
129
152
|
unmodifiedImportsModified: {},
|
|
130
|
-
newImportsModified: {}
|
|
153
|
+
newImportsModified: {}
|
|
131
154
|
};
|
|
132
155
|
|
|
133
156
|
function addDep(bucket, key, value) {
|
|
@@ -154,7 +177,11 @@ function buildCrossDependencies(root, classification, stagingDir) {
|
|
|
154
177
|
}
|
|
155
178
|
|
|
156
179
|
function readSafe(fullPath) {
|
|
157
|
-
try {
|
|
180
|
+
try {
|
|
181
|
+
return fs.readFileSync(fullPath, "utf-8");
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
158
185
|
}
|
|
159
186
|
|
|
160
187
|
// Scan modified files (user's current versions)
|
|
@@ -192,7 +219,9 @@ function buildCrossDependencies(root, classification, stagingDir) {
|
|
|
192
219
|
function main() {
|
|
193
220
|
const root = findProjectRoot();
|
|
194
221
|
if (!root) {
|
|
195
|
-
console.error(
|
|
222
|
+
console.error(
|
|
223
|
+
"Error: .generatesaas/manifest.json not found. Run this from a GenerateSaaS project."
|
|
224
|
+
);
|
|
196
225
|
process.exit(1);
|
|
197
226
|
}
|
|
198
227
|
|
|
@@ -237,7 +266,7 @@ function main() {
|
|
|
237
266
|
deleted: [],
|
|
238
267
|
new: [],
|
|
239
268
|
newInDeletedTree: [],
|
|
240
|
-
removed: []
|
|
269
|
+
removed: []
|
|
241
270
|
};
|
|
242
271
|
|
|
243
272
|
const baselineFiles = Object.keys(baselineHashes);
|
|
@@ -296,7 +325,7 @@ function main() {
|
|
|
296
325
|
// Write classification
|
|
297
326
|
fs.writeFileSync(
|
|
298
327
|
path.join(refsDir, "classification.json"),
|
|
299
|
-
JSON.stringify(classification, null, "\t")
|
|
328
|
+
`${JSON.stringify(classification, null, "\t")}\n`,
|
|
300
329
|
"utf-8"
|
|
301
330
|
);
|
|
302
331
|
|
|
@@ -13,7 +13,19 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require("node:fs");
|
|
15
15
|
const path = require("node:path");
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
findProjectRoot,
|
|
18
|
+
hashFile,
|
|
19
|
+
walkDir,
|
|
20
|
+
ensureDir,
|
|
21
|
+
MANIFEST_FILE,
|
|
22
|
+
HASHES_FILE,
|
|
23
|
+
TEMPLATE_HASHES_FILE,
|
|
24
|
+
TEMPLATE_DIR,
|
|
25
|
+
STAGING_DIR,
|
|
26
|
+
STAGING_META_FILE,
|
|
27
|
+
INTERNAL_DIR
|
|
28
|
+
} = require("./_helpers.js");
|
|
17
29
|
|
|
18
30
|
/**
|
|
19
31
|
* Safety net for the held-back list. An upstream change was demonstrably never
|
|
@@ -94,7 +106,9 @@ function archivePlanFile(root) {
|
|
|
94
106
|
function main() {
|
|
95
107
|
const root = findProjectRoot();
|
|
96
108
|
if (!root) {
|
|
97
|
-
console.error(
|
|
109
|
+
console.error(
|
|
110
|
+
"Error: .generatesaas/manifest.json not found. Run this from a GenerateSaaS project."
|
|
111
|
+
);
|
|
98
112
|
process.exit(1);
|
|
99
113
|
}
|
|
100
114
|
|
|
@@ -116,9 +130,7 @@ function main() {
|
|
|
116
130
|
// Read held-back files list (created by the AI for skipped/held-back files)
|
|
117
131
|
const heldBackPath = path.join(root, INTERNAL_DIR, "held-back.json");
|
|
118
132
|
const heldBack = new Set(
|
|
119
|
-
fs.existsSync(heldBackPath)
|
|
120
|
-
? JSON.parse(fs.readFileSync(heldBackPath, "utf-8"))
|
|
121
|
-
: []
|
|
133
|
+
fs.existsSync(heldBackPath) ? JSON.parse(fs.readFileSync(heldBackPath, "utf-8")) : []
|
|
122
134
|
);
|
|
123
135
|
|
|
124
136
|
if (heldBack.size > 0) {
|
|
@@ -137,7 +149,9 @@ function main() {
|
|
|
137
149
|
// never applied but are missing from held-back.json.
|
|
138
150
|
const netted = applyHeldBackSafetyNet(root, stagingDir, stagingFiles, heldBack);
|
|
139
151
|
if (netted.length > 0) {
|
|
140
|
-
console.warn(
|
|
152
|
+
console.warn(
|
|
153
|
+
`Safety net: ${netted.length} upstream change(s) were never applied and were missing from held-back.json.`
|
|
154
|
+
);
|
|
141
155
|
console.warn("Holding them back so the next update re-surfaces them:");
|
|
142
156
|
for (const rel of netted) console.warn(` - ${rel}`);
|
|
143
157
|
}
|
|
@@ -150,12 +164,19 @@ function main() {
|
|
|
150
164
|
// A wrong held-back entry silently corrupts the next update's diff baseline.
|
|
151
165
|
for (const rel of heldBack) {
|
|
152
166
|
if (!stagingFiles.has(rel)) {
|
|
153
|
-
console.warn(
|
|
167
|
+
console.warn(
|
|
168
|
+
` Warning: held-back "${rel}" is not in staging - nothing to hold back. Check .generatesaas/held-back.json.`
|
|
169
|
+
);
|
|
154
170
|
continue;
|
|
155
171
|
}
|
|
156
172
|
const oldTemplateFile = path.join(templateDir, rel);
|
|
157
|
-
if (
|
|
158
|
-
|
|
173
|
+
if (
|
|
174
|
+
fs.existsSync(oldTemplateFile) &&
|
|
175
|
+
hashFile(oldTemplateFile) === hashFile(path.join(stagingDir, rel))
|
|
176
|
+
) {
|
|
177
|
+
console.warn(
|
|
178
|
+
` Warning: held-back "${rel}" is identical in the old template and staging - no upstream change to preserve.`
|
|
179
|
+
);
|
|
159
180
|
}
|
|
160
181
|
}
|
|
161
182
|
|
|
@@ -199,8 +220,14 @@ function main() {
|
|
|
199
220
|
templateHashes[rel] = hashFile(path.join(templateDir, rel));
|
|
200
221
|
}
|
|
201
222
|
fs.mkdirSync(path.dirname(templateHashesPath), { recursive: true });
|
|
202
|
-
fs.writeFileSync(
|
|
203
|
-
|
|
223
|
+
fs.writeFileSync(
|
|
224
|
+
templateHashesPath,
|
|
225
|
+
`${JSON.stringify(templateHashes, null, "\t")}\n`,
|
|
226
|
+
"utf-8"
|
|
227
|
+
);
|
|
228
|
+
console.log(
|
|
229
|
+
`Tracked ${Object.keys(templateHashes).length} template hashes in ${TEMPLATE_HASHES_FILE}`
|
|
230
|
+
);
|
|
204
231
|
}
|
|
205
232
|
|
|
206
233
|
// Clean up staging
|
|
@@ -239,13 +266,13 @@ function main() {
|
|
|
239
266
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
240
267
|
manifest.version = targetVersion;
|
|
241
268
|
|
|
242
|
-
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, "\t")
|
|
269
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, "\t")}\n`, "utf-8");
|
|
243
270
|
console.log(`Updated manifest to version ${targetVersion}`);
|
|
244
271
|
|
|
245
272
|
// Write hashes to separate file
|
|
246
273
|
const hashesPath = path.join(root, HASHES_FILE);
|
|
247
274
|
fs.mkdirSync(path.dirname(hashesPath), { recursive: true });
|
|
248
|
-
fs.writeFileSync(hashesPath, JSON.stringify(fileHashes, null, "\t")
|
|
275
|
+
fs.writeFileSync(hashesPath, `${JSON.stringify(fileHashes, null, "\t")}\n`, "utf-8");
|
|
249
276
|
console.log(`Tracked ${Object.keys(fileHashes).length} file hashes in ${HASHES_FILE}`);
|
|
250
277
|
|
|
251
278
|
// Archive the finished plan file so the next update's pre-flight check doesn't
|
|
@@ -9,7 +9,17 @@
|
|
|
9
9
|
const fs = require("node:fs");
|
|
10
10
|
const path = require("node:path");
|
|
11
11
|
const { execFileSync } = require("node:child_process");
|
|
12
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
findProjectRoot,
|
|
14
|
+
ensureDir,
|
|
15
|
+
hashFile,
|
|
16
|
+
walkDir,
|
|
17
|
+
shouldExcludeWalk,
|
|
18
|
+
TEMPLATE_HASHES_FILE,
|
|
19
|
+
TEMPLATE_DIR,
|
|
20
|
+
STAGING_DIR,
|
|
21
|
+
STAGING_META_FILE
|
|
22
|
+
} = require("./_helpers.js");
|
|
13
23
|
|
|
14
24
|
// Paths whose upstream changes always carry follow-up work for the user:
|
|
15
25
|
// database schema changes may need a migration, incoming migrations must be
|
|
@@ -19,13 +29,15 @@ const { findProjectRoot, ensureDir, hashFile, walkDir, shouldExcludeWalk, TEMPLA
|
|
|
19
29
|
const SENSITIVE_PATTERNS = [
|
|
20
30
|
/^packages\/database\/src\/db\//,
|
|
21
31
|
/^packages\/database\/drizzle\//,
|
|
22
|
-
/(^|\/)\.env\.example
|
|
32
|
+
/(^|\/)\.env\.example$/
|
|
23
33
|
];
|
|
24
34
|
|
|
25
35
|
function main() {
|
|
26
36
|
const root = findProjectRoot();
|
|
27
37
|
if (!root) {
|
|
28
|
-
console.error(
|
|
38
|
+
console.error(
|
|
39
|
+
"Error: .generatesaas/manifest.json not found. Run this from a GenerateSaaS project."
|
|
40
|
+
);
|
|
29
41
|
process.exit(1);
|
|
30
42
|
}
|
|
31
43
|
|
|
@@ -105,7 +117,9 @@ function main() {
|
|
|
105
117
|
|
|
106
118
|
console.log(` Added: ${added.length}, Modified: ${modified.length}, Removed: ${removed.length}`);
|
|
107
119
|
if (renamed.length > 0) {
|
|
108
|
-
console.log(
|
|
120
|
+
console.log(
|
|
121
|
+
` Renamed: ${renamed.length} (see update-manifest.json - port customizations before deleting the old paths)`
|
|
122
|
+
);
|
|
109
123
|
}
|
|
110
124
|
if (sensitive.length > 0) {
|
|
111
125
|
console.log(` Sensitive (schema / migrations / env example): ${sensitive.length}`);
|
|
@@ -151,7 +165,7 @@ function main() {
|
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
if (diff) {
|
|
154
|
-
const diffPath = path.join(diffsDir, filePath
|
|
168
|
+
const diffPath = path.join(diffsDir, `${filePath}.diff`);
|
|
155
169
|
ensureDir(path.dirname(diffPath));
|
|
156
170
|
fs.writeFileSync(diffPath, diff, "utf-8");
|
|
157
171
|
diffCount++;
|
|
@@ -167,11 +181,11 @@ function main() {
|
|
|
167
181
|
modified,
|
|
168
182
|
removed,
|
|
169
183
|
renamed,
|
|
170
|
-
sensitive
|
|
184
|
+
sensitive
|
|
171
185
|
};
|
|
172
186
|
fs.writeFileSync(
|
|
173
187
|
path.join(refsDir, "update-manifest.json"),
|
|
174
|
-
JSON.stringify(updateManifest, null, "\t")
|
|
188
|
+
`${JSON.stringify(updateManifest, null, "\t")}\n`,
|
|
175
189
|
"utf-8"
|
|
176
190
|
);
|
|
177
191
|
console.log("Written: references/update-manifest.json");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "generatesaas",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI for scaffolding and managing GenerateSaaS projects",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
"tar": "^7.5.22"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
+
"@repo/eslint-config": "0.0.0",
|
|
22
23
|
"@repo/tsconfig": "1.0.0",
|
|
23
24
|
"@types/node": "26.2.0",
|
|
25
|
+
"eslint": "^9.39.5",
|
|
24
26
|
"tsup": "^8.5.1",
|
|
25
27
|
"typescript": "6.0.3",
|
|
26
28
|
"vitest": "4.1.10"
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
"postbuild": "node -e \"const fs=require('node:fs');fs.rmSync('dist/skill/content',{recursive:true,force:true});fs.mkdirSync('dist/skill',{recursive:true});fs.cpSync('src/skill/content','dist/skill/content',{recursive:true});fs.rmSync('dist/generators/content',{recursive:true,force:true});fs.mkdirSync('dist/generators',{recursive:true});fs.cpSync('src/generators/content','dist/generators/content',{recursive:true})\"",
|
|
34
36
|
"dev": "tsup --watch",
|
|
35
37
|
"check-types": "tsc --noEmit",
|
|
38
|
+
"lint": "eslint --cache .",
|
|
36
39
|
"test": "vitest run",
|
|
37
40
|
"test:playground": "vitest run --config vitest.playground.config.ts"
|
|
38
41
|
}
|