generatesaas 0.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.
- package/dist/index.js +551 -0
- package/dist/skill/content/SKILL.md +306 -0
- package/dist/skill/content/scripts/_helpers.js +85 -0
- package/dist/skill/content/scripts/apply-auto.js +101 -0
- package/dist/skill/content/scripts/classify-files.js +131 -0
- package/dist/skill/content/scripts/complete-update.js +134 -0
- package/dist/skill/content/scripts/prepare-update.js +155 -0
- package/package.json +35 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Complete the update process.
|
|
5
|
+
* Re-hashes all project files, updates template hashes from staging,
|
|
6
|
+
* updates .generatesaas/manifest.json, and cleans up staging + references.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
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");
|
|
12
|
+
|
|
13
|
+
const HASH_EXCLUSIONS = new Set([".git", "node_modules", ".pnpm-store", ".env", "data", INTERNAL_DIR]);
|
|
14
|
+
|
|
15
|
+
/** Check if a relative path should be excluded from hashing. */
|
|
16
|
+
function shouldExcludeFromHash(relativePath) {
|
|
17
|
+
const parts = relativePath.split("/");
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
if (HASH_EXCLUSIONS.has(part)) return true;
|
|
20
|
+
if (part.startsWith(".env") && !part.includes("example")) return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Recursively collect all file paths for project hashing. */
|
|
26
|
+
function collectProjectFiles(dir, baseDir) {
|
|
27
|
+
const files = [];
|
|
28
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
29
|
+
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const fullPath = path.join(dir, entry.name);
|
|
32
|
+
const rel = path.relative(baseDir, fullPath);
|
|
33
|
+
|
|
34
|
+
if (shouldExcludeFromHash(rel)) continue;
|
|
35
|
+
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
files.push(...collectProjectFiles(fullPath, baseDir));
|
|
38
|
+
} else if (entry.isFile()) {
|
|
39
|
+
files.push(fullPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return files;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function main() {
|
|
47
|
+
const root = findProjectRoot();
|
|
48
|
+
if (!root) {
|
|
49
|
+
console.error("Error: .generatesaas/manifest.json not found. Run this from a GenerateSaaS project.");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Determine the skill root
|
|
54
|
+
const scriptDir = __dirname;
|
|
55
|
+
const skillDir = path.dirname(scriptDir);
|
|
56
|
+
const refsDir = path.join(skillDir, "references");
|
|
57
|
+
|
|
58
|
+
const updateManifestPath = path.join(refsDir, "update-manifest.json");
|
|
59
|
+
if (!fs.existsSync(updateManifestPath)) {
|
|
60
|
+
console.error("Error: references/update-manifest.json not found. Run prepare-update.js first.");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const updateManifest = JSON.parse(fs.readFileSync(updateManifestPath, "utf-8"));
|
|
65
|
+
const targetVersion = updateManifest.targetVersion;
|
|
66
|
+
|
|
67
|
+
console.log(`Completing update to version ${targetVersion}...`);
|
|
68
|
+
|
|
69
|
+
// Compute and write new template hashes from staging BEFORE cleanup
|
|
70
|
+
const stagingDir = path.join(root, STAGING_DIR);
|
|
71
|
+
if (fs.existsSync(stagingDir)) {
|
|
72
|
+
console.log("Computing template hashes from staging...");
|
|
73
|
+
const stagingFiles = walkDir(stagingDir, stagingDir);
|
|
74
|
+
const templateHashes = {};
|
|
75
|
+
for (const rel of stagingFiles.sort()) {
|
|
76
|
+
templateHashes[rel] = hashFile(path.join(stagingDir, rel));
|
|
77
|
+
}
|
|
78
|
+
const templateHashesPath = path.join(root, TEMPLATE_HASHES_FILE);
|
|
79
|
+
fs.mkdirSync(path.dirname(templateHashesPath), { recursive: true });
|
|
80
|
+
fs.writeFileSync(templateHashesPath, JSON.stringify(templateHashes, null, "\t") + "\n", "utf-8");
|
|
81
|
+
console.log(`Tracked ${Object.keys(templateHashes).length} template hashes in ${TEMPLATE_HASHES_FILE}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Clean up staging
|
|
85
|
+
if (fs.existsSync(stagingDir)) {
|
|
86
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
87
|
+
console.log("Cleaned up staging directory");
|
|
88
|
+
}
|
|
89
|
+
const metaPath = path.join(root, STAGING_META_FILE);
|
|
90
|
+
if (fs.existsSync(metaPath)) {
|
|
91
|
+
fs.rmSync(metaPath);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Clean up references BEFORE hashing to avoid tracking temp files
|
|
95
|
+
if (fs.existsSync(refsDir)) {
|
|
96
|
+
fs.rmSync(refsDir, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
fs.mkdirSync(refsDir, { recursive: true });
|
|
99
|
+
fs.writeFileSync(path.join(refsDir, ".gitkeep"), "", "utf-8");
|
|
100
|
+
console.log("Cleaned up references/");
|
|
101
|
+
|
|
102
|
+
// Re-hash all project files
|
|
103
|
+
console.log("Hashing project files...");
|
|
104
|
+
const files = collectProjectFiles(root, root);
|
|
105
|
+
const fileHashes = {};
|
|
106
|
+
for (const file of files.sort()) {
|
|
107
|
+
const rel = path.relative(root, file);
|
|
108
|
+
fileHashes[rel] = hashFile(file);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Update version in manifest
|
|
112
|
+
const manifestPath = path.join(root, MANIFEST_FILE);
|
|
113
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
114
|
+
manifest.version = targetVersion;
|
|
115
|
+
|
|
116
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, "\t") + "\n", "utf-8");
|
|
117
|
+
console.log(`Updated manifest to version ${targetVersion}`);
|
|
118
|
+
|
|
119
|
+
// Write hashes to separate file
|
|
120
|
+
const hashesPath = path.join(root, HASHES_FILE);
|
|
121
|
+
fs.mkdirSync(path.dirname(hashesPath), { recursive: true });
|
|
122
|
+
fs.writeFileSync(hashesPath, JSON.stringify(fileHashes, null, "\t") + "\n", "utf-8");
|
|
123
|
+
console.log(`Tracked ${Object.keys(fileHashes).length} file hashes in ${HASHES_FILE}`);
|
|
124
|
+
|
|
125
|
+
console.log(`\nUpdate to ${targetVersion} complete!`);
|
|
126
|
+
console.log("Run 'pnpm install' if package.json was updated.");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
main();
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error("Error:", err.message);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prepare update data from the local staging directory.
|
|
5
|
+
* Computes diffs between the staged template and current project files.
|
|
6
|
+
* Produces the same output as the old fetch-update.js but without any API calls.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
const path = require("node:path");
|
|
11
|
+
const { execFileSync } = require("node:child_process");
|
|
12
|
+
const { findProjectRoot, ensureDir, hashFile, walkDir, TEMPLATE_HASHES_FILE, STAGING_DIR, STAGING_META_FILE } = require("./_helpers.js");
|
|
13
|
+
|
|
14
|
+
function main() {
|
|
15
|
+
const root = findProjectRoot();
|
|
16
|
+
if (!root) {
|
|
17
|
+
console.error("Error: .generatesaas/manifest.json not found. Run this from a GenerateSaaS project.");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Read staging metadata (written by the CLI `update` command)
|
|
22
|
+
const metaPath = path.join(root, STAGING_META_FILE);
|
|
23
|
+
if (!fs.existsSync(metaPath)) {
|
|
24
|
+
console.error("Error: No staged update found. Run 'generatesaas update' first.");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
28
|
+
const { currentVersion, targetVersion, changelog } = meta;
|
|
29
|
+
|
|
30
|
+
// Verify staging directory exists
|
|
31
|
+
const stagingDir = path.join(root, STAGING_DIR);
|
|
32
|
+
if (!fs.existsSync(stagingDir)) {
|
|
33
|
+
console.error("Error: Staging directory not found. Run 'generatesaas update' first.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(`Preparing update: ${currentVersion} → ${targetVersion}`);
|
|
38
|
+
|
|
39
|
+
// Read old template hashes
|
|
40
|
+
const templateHashesPath = path.join(root, TEMPLATE_HASHES_FILE);
|
|
41
|
+
const oldHashes = fs.existsSync(templateHashesPath)
|
|
42
|
+
? JSON.parse(fs.readFileSync(templateHashesPath, "utf-8"))
|
|
43
|
+
: {};
|
|
44
|
+
|
|
45
|
+
// Walk staging dir and compute new template hashes
|
|
46
|
+
const stagingFiles = walkDir(stagingDir, stagingDir);
|
|
47
|
+
const newHashes = {};
|
|
48
|
+
for (const rel of stagingFiles) {
|
|
49
|
+
newHashes[rel] = hashFile(path.join(stagingDir, rel));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Compare old vs new template hashes
|
|
53
|
+
const added = [];
|
|
54
|
+
const modified = [];
|
|
55
|
+
const removed = [];
|
|
56
|
+
|
|
57
|
+
// Files in new template
|
|
58
|
+
for (const filePath of Object.keys(newHashes)) {
|
|
59
|
+
if (!(filePath in oldHashes)) {
|
|
60
|
+
added.push(filePath);
|
|
61
|
+
} else if (newHashes[filePath] !== oldHashes[filePath]) {
|
|
62
|
+
modified.push(filePath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Files in old template but not in new
|
|
67
|
+
for (const filePath of Object.keys(oldHashes)) {
|
|
68
|
+
if (!(filePath in newHashes)) {
|
|
69
|
+
removed.push(filePath);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
added.sort();
|
|
74
|
+
modified.sort();
|
|
75
|
+
removed.sort();
|
|
76
|
+
|
|
77
|
+
console.log(` Added: ${added.length}, Modified: ${modified.length}, Removed: ${removed.length}`);
|
|
78
|
+
|
|
79
|
+
// Determine output directory
|
|
80
|
+
const scriptDir = __dirname;
|
|
81
|
+
const skillDir = path.dirname(scriptDir);
|
|
82
|
+
const refsDir = path.join(skillDir, "references");
|
|
83
|
+
ensureDir(refsDir);
|
|
84
|
+
|
|
85
|
+
// Write changelog
|
|
86
|
+
if (changelog) {
|
|
87
|
+
fs.writeFileSync(path.join(refsDir, "changelog.md"), changelog, "utf-8");
|
|
88
|
+
console.log("Written: references/changelog.md");
|
|
89
|
+
} else {
|
|
90
|
+
console.log("No changelog found for this version.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Generate and write diffs for modified files
|
|
94
|
+
const diffsDir = path.join(refsDir, "diffs");
|
|
95
|
+
ensureDir(diffsDir);
|
|
96
|
+
let diffCount = 0;
|
|
97
|
+
|
|
98
|
+
for (const filePath of modified) {
|
|
99
|
+
const userFile = path.join(root, filePath);
|
|
100
|
+
const stagingFile = path.join(stagingDir, filePath);
|
|
101
|
+
|
|
102
|
+
if (!fs.existsSync(userFile)) continue;
|
|
103
|
+
|
|
104
|
+
const diff = generateDiff(userFile, stagingFile, filePath);
|
|
105
|
+
if (diff) {
|
|
106
|
+
const diffPath = path.join(diffsDir, filePath + ".diff");
|
|
107
|
+
ensureDir(path.dirname(diffPath));
|
|
108
|
+
fs.writeFileSync(diffPath, diff, "utf-8");
|
|
109
|
+
diffCount++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
console.log(`Written: ${diffCount} diff files`);
|
|
113
|
+
|
|
114
|
+
// Write update manifest (same format as before)
|
|
115
|
+
const updateManifest = {
|
|
116
|
+
currentVersion,
|
|
117
|
+
targetVersion,
|
|
118
|
+
added,
|
|
119
|
+
modified,
|
|
120
|
+
removed,
|
|
121
|
+
};
|
|
122
|
+
fs.writeFileSync(
|
|
123
|
+
path.join(refsDir, "update-manifest.json"),
|
|
124
|
+
JSON.stringify(updateManifest, null, "\t") + "\n",
|
|
125
|
+
"utf-8"
|
|
126
|
+
);
|
|
127
|
+
console.log("Written: references/update-manifest.json");
|
|
128
|
+
|
|
129
|
+
console.log("\nNext: Review references/changelog.md, then run classify-files.js");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Generate a unified diff between two files. Returns null if diff is unavailable. */
|
|
133
|
+
function generateDiff(userFile, stagingFile, relativePath) {
|
|
134
|
+
try {
|
|
135
|
+
execFileSync("diff", ["-u", userFile, stagingFile], { encoding: "utf-8" });
|
|
136
|
+
return null; // Files are identical (exit code 0)
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (err.status === 1 && err.stdout) {
|
|
139
|
+
// Exit code 1 = files differ, stdout contains the diff
|
|
140
|
+
// Replace absolute paths with relative ones for readability
|
|
141
|
+
return err.stdout
|
|
142
|
+
.replaceAll(userFile, `a/${relativePath}`)
|
|
143
|
+
.replaceAll(stagingFile, `b/${relativePath}`);
|
|
144
|
+
}
|
|
145
|
+
// diff command not available or other error
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
main();
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error("Error:", err.message);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "generatesaas",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI for scaffolding and managing GenerateSaaS projects",
|
|
6
|
+
"bin": {
|
|
7
|
+
"generatesaas": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"postbuild": "rm -rf dist/skill/content && mkdir -p dist/skill && cp -r src/skill/content dist/skill/content",
|
|
15
|
+
"dev": "tsup --watch",
|
|
16
|
+
"check-types": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"prepublishOnly": "pnpm run build"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@clack/prompts": "^1.1.0",
|
|
22
|
+
"commander": "^14.0.3",
|
|
23
|
+
"picocolors": "^1.1.1",
|
|
24
|
+
"tar": "^7.5.11"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@repo/tsconfig": "workspace:*",
|
|
28
|
+
"tsup": "^8.5.1",
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
|
+
"vitest": "^4.1.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22"
|
|
34
|
+
}
|
|
35
|
+
}
|