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.
@@ -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
+ }