@nebulit/embuilder 0.1.39

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.
Files changed (212) hide show
  1. package/README.md +254 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +138 -0
  4. package/package.json +49 -0
  5. package/templates/.claude/hooks/QUICKSTART.md +256 -0
  6. package/templates/.claude/hooks/README.md +533 -0
  7. package/templates/.claude/hooks/analyze-commit.sh +22 -0
  8. package/templates/.claude/hooks/analyze-commit.ts +518 -0
  9. package/templates/.claude/hooks/analyzers/README.md +198 -0
  10. package/templates/.claude/hooks/analyzers/code-quality-checker.ts +154 -0
  11. package/templates/.claude/hooks/analyzers/code-quality.md +54 -0
  12. package/templates/.claude/hooks/analyzers/commit-blocker-example.ts.disabled +110 -0
  13. package/templates/.claude/hooks/analyzers/commit-policy.md +49 -0
  14. package/templates/.claude/hooks/analyzers/event-model-validator.md +49 -0
  15. package/templates/.claude/hooks/analyzers/event-model-validator.ts +169 -0
  16. package/templates/.claude/hooks/analyzers/example-logger.ts +70 -0
  17. package/templates/.claude/hooks/analyzers/slice-scope-validator.md +81 -0
  18. package/templates/.claude/hooks/check-review-result.sh +47 -0
  19. package/templates/.claude/hooks/prepare-review.sh +34 -0
  20. package/templates/.claude/hooks/review-agent-prompt.md +42 -0
  21. package/templates/.claude/hooks/run-review-agent.sh +124 -0
  22. package/templates/.claude/settings.local.json +37 -0
  23. package/templates/.claude/skills/help/README.md +84 -0
  24. package/templates/.claude/skills/help/SKILL.md +393 -0
  25. package/templates/.claude/skills/help/templates/demo-config.json +6753 -0
  26. package/templates/.claude/skills/sample-slices/SKILL.md +8 -0
  27. package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/code-slice.json +124 -0
  28. package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/slice.json +255 -0
  29. package/templates/.claude/skills/sample-slices/templates/.slices/Library/availablebooks/slice.json +107 -0
  30. package/templates/.claude/skills/sample-slices/templates/.slices/index.json +20 -0
  31. package/templates/.claude/skills/sample-slices/templates/Cart/additem/slice.json +979 -0
  32. package/templates/.claude/skills/sample-slices/templates/Cart/archiveitem/slice.json +529 -0
  33. package/templates/.claude/skills/sample-slices/templates/Cart/cartitems/slice.json +1072 -0
  34. package/templates/.claude/skills/sample-slices/templates/Cart/cartwithproducts/slice.json +394 -0
  35. package/templates/.claude/skills/sample-slices/templates/Cart/changedprices/slice.json +88 -0
  36. package/templates/.claude/skills/sample-slices/templates/Cart/changeinventory/slice.json +264 -0
  37. package/templates/.claude/skills/sample-slices/templates/Cart/changeprice/slice.json +308 -0
  38. package/templates/.claude/skills/sample-slices/templates/Cart/clearcart/slice.json +358 -0
  39. package/templates/.claude/skills/sample-slices/templates/Cart/inventories/slice.json +203 -0
  40. package/templates/.claude/skills/sample-slices/templates/Cart/publishcart/slice.json +876 -0
  41. package/templates/.claude/skills/sample-slices/templates/Cart/removeitem/slice.json +560 -0
  42. package/templates/.claude/skills/sample-slices/templates/Cart/submitcart/slice.json +708 -0
  43. package/templates/.claude/skills/sample-slices/templates/Cart/submittedcartdata/slice.json +399 -0
  44. package/templates/.claude/skills/sample-slices/templates/index.json +108 -0
  45. package/templates/.claude/skills/slice-automation/SKILL.md +49 -0
  46. package/templates/.claude/skills/slice-state-change/SKILL.md +369 -0
  47. package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocation.test.ts.sample +76 -0
  48. package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocationCommand.ts.sample +84 -0
  49. package/templates/.claude/skills/slice-state-change/templates/AddLocation/routes.ts.sample +73 -0
  50. package/templates/.claude/skills/slice-state-change/templates/README.md +46 -0
  51. package/templates/.claude/skills/slice-state-view/SKILL.md +336 -0
  52. package/templates/.claude/skills/slice-state-view/templates/Locations/Locations.test.ts.sample +84 -0
  53. package/templates/.claude/skills/slice-state-view/templates/Locations/LocationsProjection.ts.sample +50 -0
  54. package/templates/.claude/skills/slice-state-view/templates/Locations/routes.ts.sample +46 -0
  55. package/templates/.claude/skills/slice-state-view/templates/README.md +109 -0
  56. package/templates/.claude/skills/slice-state-view/templates/Tables/Tables.test.ts.sample +104 -0
  57. package/templates/.claude/skills/slice-state-view/templates/Tables/TablesProjection.ts.sample +59 -0
  58. package/templates/.claude/skills/slice-state-view/templates/Tables/routes.ts.sample +46 -0
  59. package/templates/.claude/skills/slice-state-view/templates/V2__tables.sql +7 -0
  60. package/templates/.claude/skills/slice-state-view/templates/V8__locations.sql +7 -0
  61. package/templates/.claude/skills/test-analyzer/SKILL.md +373 -0
  62. package/templates/.claude/skills/test-analyzer/examples/specification-format.md +143 -0
  63. package/templates/.claude/skills/test-analyzer/examples/state-change-example.md +111 -0
  64. package/templates/.claude/skills/test-analyzer/examples/state-view-example.md +122 -0
  65. package/templates/AGENTS.md +110 -0
  66. package/templates/Claude.md +58 -0
  67. package/templates/README.md +178 -0
  68. package/templates/backend/.env +9 -0
  69. package/templates/backend/BACKEND_AUTH_SETUP.md +183 -0
  70. package/templates/backend/SWAGGER.md +213 -0
  71. package/templates/backend/eslint.config.mjs +31 -0
  72. package/templates/backend/flyway.conf +17 -0
  73. package/templates/backend/package.json +44 -0
  74. package/templates/backend/prd.json.example +64 -0
  75. package/templates/backend/public/assets/images/banner.png +0 -0
  76. package/templates/backend/public/assets/logo.png +0 -0
  77. package/templates/backend/public/file.svg +4 -0
  78. package/templates/backend/public/globe.svg +12 -0
  79. package/templates/backend/public/next.svg +6 -0
  80. package/templates/backend/public/vercel.svg +3 -0
  81. package/templates/backend/public/window.svg +5 -0
  82. package/templates/backend/server.ts +129 -0
  83. package/templates/backend/setup-env.sh +50 -0
  84. package/templates/backend/src/common/assertions.ts +6 -0
  85. package/templates/backend/src/common/db.ts +1 -0
  86. package/templates/backend/src/common/loadPostgresEventstore.ts +16 -0
  87. package/templates/backend/src/common/parseEndpoint.ts +51 -0
  88. package/templates/backend/src/common/replay.ts +9 -0
  89. package/templates/backend/src/common/routes.ts +19 -0
  90. package/templates/backend/src/common/testHelpers.ts +53 -0
  91. package/templates/backend/src/core/readmodel.ts +28 -0
  92. package/templates/backend/src/core/types.ts +26 -0
  93. package/templates/backend/src/process/process.ts +53 -0
  94. package/templates/backend/src/supabase/LoginHandler.ts +36 -0
  95. package/templates/backend/src/supabase/ProtectedPageProps.ts +21 -0
  96. package/templates/backend/src/supabase/README.md +171 -0
  97. package/templates/backend/src/supabase/api.ts +63 -0
  98. package/templates/backend/src/supabase/authMiddleware.ts +53 -0
  99. package/templates/backend/src/supabase/component.ts +12 -0
  100. package/templates/backend/src/supabase/requireUser.ts +72 -0
  101. package/templates/backend/src/supabase/serverProps.ts +25 -0
  102. package/templates/backend/src/supabase/staticProps.ts +10 -0
  103. package/templates/backend/src/swagger.ts +34 -0
  104. package/templates/backend/src/util/assertions.ts +6 -0
  105. package/templates/backend/supabase/config.toml +295 -0
  106. package/templates/backend/supabase/migrations/20260121155918593_catalogentries.sql.sample +23 -0
  107. package/templates/backend/supabase/seed.sql +1 -0
  108. package/templates/backend/tsconfig.json +31 -0
  109. package/templates/frontend/.env.development +3 -0
  110. package/templates/frontend/AGENTS.md +7 -0
  111. package/templates/frontend/README.md +73 -0
  112. package/templates/frontend/components.json +20 -0
  113. package/templates/frontend/eslint.config.js +26 -0
  114. package/templates/frontend/index.html +18 -0
  115. package/templates/frontend/package-lock.json +8347 -0
  116. package/templates/frontend/package.json +94 -0
  117. package/templates/frontend/postcss.config.js +6 -0
  118. package/templates/frontend/public/favicon.ico +0 -0
  119. package/templates/frontend/public/logo.png +0 -0
  120. package/templates/frontend/public/placeholder.svg +1 -0
  121. package/templates/frontend/public/robots.txt +14 -0
  122. package/templates/frontend/src/App.css +42 -0
  123. package/templates/frontend/src/App.tsx +47 -0
  124. package/templates/frontend/src/components/NavLink.tsx +28 -0
  125. package/templates/frontend/src/components/ProtectedRoute.tsx +24 -0
  126. package/templates/frontend/src/components/calendar/Calendar.tsx +302 -0
  127. package/templates/frontend/src/components/layout/DashboardLayout.tsx +21 -0
  128. package/templates/frontend/src/components/layout/Header.tsx +45 -0
  129. package/templates/frontend/src/components/layout/Sidebar.tsx +82 -0
  130. package/templates/frontend/src/components/tables/ReservationTemplates.tsx +189 -0
  131. package/templates/frontend/src/components/ui/accordion.tsx +52 -0
  132. package/templates/frontend/src/components/ui/alert-dialog.tsx +104 -0
  133. package/templates/frontend/src/components/ui/alert.tsx +43 -0
  134. package/templates/frontend/src/components/ui/aspect-ratio.tsx +5 -0
  135. package/templates/frontend/src/components/ui/avatar.tsx +38 -0
  136. package/templates/frontend/src/components/ui/badge.tsx +29 -0
  137. package/templates/frontend/src/components/ui/breadcrumb.tsx +90 -0
  138. package/templates/frontend/src/components/ui/button.tsx +47 -0
  139. package/templates/frontend/src/components/ui/calendar.tsx +54 -0
  140. package/templates/frontend/src/components/ui/card.tsx +43 -0
  141. package/templates/frontend/src/components/ui/carousel.tsx +224 -0
  142. package/templates/frontend/src/components/ui/chart.tsx +303 -0
  143. package/templates/frontend/src/components/ui/checkbox.tsx +26 -0
  144. package/templates/frontend/src/components/ui/collapsible.tsx +9 -0
  145. package/templates/frontend/src/components/ui/command.tsx +132 -0
  146. package/templates/frontend/src/components/ui/context-menu.tsx +178 -0
  147. package/templates/frontend/src/components/ui/dialog.tsx +95 -0
  148. package/templates/frontend/src/components/ui/drawer.tsx +87 -0
  149. package/templates/frontend/src/components/ui/dropdown-menu.tsx +179 -0
  150. package/templates/frontend/src/components/ui/form.tsx +129 -0
  151. package/templates/frontend/src/components/ui/hover-card.tsx +27 -0
  152. package/templates/frontend/src/components/ui/input-otp.tsx +61 -0
  153. package/templates/frontend/src/components/ui/input.tsx +22 -0
  154. package/templates/frontend/src/components/ui/label.tsx +17 -0
  155. package/templates/frontend/src/components/ui/menubar.tsx +207 -0
  156. package/templates/frontend/src/components/ui/navigation-menu.tsx +120 -0
  157. package/templates/frontend/src/components/ui/pagination.tsx +81 -0
  158. package/templates/frontend/src/components/ui/popover.tsx +29 -0
  159. package/templates/frontend/src/components/ui/progress.tsx +23 -0
  160. package/templates/frontend/src/components/ui/radio-group.tsx +36 -0
  161. package/templates/frontend/src/components/ui/resizable.tsx +37 -0
  162. package/templates/frontend/src/components/ui/scroll-area.tsx +38 -0
  163. package/templates/frontend/src/components/ui/select.tsx +143 -0
  164. package/templates/frontend/src/components/ui/separator.tsx +20 -0
  165. package/templates/frontend/src/components/ui/sheet.tsx +107 -0
  166. package/templates/frontend/src/components/ui/sidebar.tsx +637 -0
  167. package/templates/frontend/src/components/ui/skeleton.tsx +7 -0
  168. package/templates/frontend/src/components/ui/slider.tsx +23 -0
  169. package/templates/frontend/src/components/ui/sonner.tsx +27 -0
  170. package/templates/frontend/src/components/ui/stat-card.tsx +44 -0
  171. package/templates/frontend/src/components/ui/switch.tsx +27 -0
  172. package/templates/frontend/src/components/ui/table.tsx +72 -0
  173. package/templates/frontend/src/components/ui/tabs.tsx +53 -0
  174. package/templates/frontend/src/components/ui/textarea.tsx +21 -0
  175. package/templates/frontend/src/components/ui/toast.tsx +111 -0
  176. package/templates/frontend/src/components/ui/toaster.tsx +24 -0
  177. package/templates/frontend/src/components/ui/toggle-group.tsx +49 -0
  178. package/templates/frontend/src/components/ui/toggle.tsx +37 -0
  179. package/templates/frontend/src/components/ui/tooltip.tsx +28 -0
  180. package/templates/frontend/src/components/ui/use-toast.ts +3 -0
  181. package/templates/frontend/src/contexts/AuthContext.tsx +94 -0
  182. package/templates/frontend/src/contexts/RefreshContext.tsx +236 -0
  183. package/templates/frontend/src/hooks/api/index.ts +2 -0
  184. package/templates/frontend/src/hooks/api/useLocations.ts +15 -0
  185. package/templates/frontend/src/hooks/use-mobile.tsx +19 -0
  186. package/templates/frontend/src/hooks/use-toast.ts +186 -0
  187. package/templates/frontend/src/hooks/useApiContext.ts +11 -0
  188. package/templates/frontend/src/index.css +118 -0
  189. package/templates/frontend/src/integrations/supabase/client.ts +9 -0
  190. package/templates/frontend/src/lib/api-client.ts +136 -0
  191. package/templates/frontend/src/lib/api.ts +1028 -0
  192. package/templates/frontend/src/lib/utils.ts +6 -0
  193. package/templates/frontend/src/main.tsx +5 -0
  194. package/templates/frontend/src/pages/Auth.tsx +408 -0
  195. package/templates/frontend/src/pages/Dashboard.tsx +168 -0
  196. package/templates/frontend/src/pages/Menus.tsx +224 -0
  197. package/templates/frontend/src/pages/NotFound.tsx +24 -0
  198. package/templates/frontend/src/pages/Register.tsx +285 -0
  199. package/templates/frontend/src/test/example.test.ts +0 -0
  200. package/templates/frontend/src/test/setup.ts +15 -0
  201. package/templates/frontend/src/types/index.ts +8 -0
  202. package/templates/frontend/src/vite-env.d.ts +1 -0
  203. package/templates/frontend/tailwind.config.ts +101 -0
  204. package/templates/frontend/tsconfig.app.json +31 -0
  205. package/templates/frontend/tsconfig.json +16 -0
  206. package/templates/frontend/tsconfig.node.json +22 -0
  207. package/templates/frontend/vite.config.ts +21 -0
  208. package/templates/frontend/vitest.config.ts +16 -0
  209. package/templates/init.sh +1 -0
  210. package/templates/prompt.md +139 -0
  211. package/templates/ralph.sh +120 -0
  212. package/templates/server.mjs +505 -0
@@ -0,0 +1,505 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from 'http';
3
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, unlinkSync, mkdirSync } from 'fs';
4
+ import { join, dirname, relative } from 'path';
5
+ import { execSync } from 'child_process';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const ROOT = process.cwd();
11
+ const CONFIG_PATH = join(ROOT, 'config.json');
12
+ const SLICES_DIR = join(ROOT, '.slices');
13
+ const repoPath = process.env.WORKSPACE_PATH ?? ROOT;
14
+
15
+ // ---------------------------------------
16
+ // Helpers
17
+ // ---------------------------------------
18
+ function sendJSON(res, data, status = 200) {
19
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization' });
20
+ res.end(JSON.stringify(data, null, 2));
21
+ }
22
+
23
+ function parseIdWithRegex(filename) {
24
+ const match = filename.match(/screen-(\d+)\.png$/);
25
+ return match ? match[1] : null;
26
+ }
27
+
28
+ function slugify(text) {
29
+ return text
30
+ .toString()
31
+ .toLowerCase()
32
+ .trim()
33
+ .replace(/\s+/g, '-')
34
+ .replace(/[^\w\-]+/g, '')
35
+ .replace(/\-\-+/g, '-')
36
+ .replace(/^-+/, '')
37
+ .replace(/-+$/, '');
38
+ }
39
+
40
+ function findFilesRecursive(dir, filterFn, results = []) {
41
+ if (!existsSync(dir)) return results;
42
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
43
+ const fullPath = join(dir, entry.name);
44
+ if (entry.isDirectory()) findFilesRecursive(fullPath, filterFn, results);
45
+ else if (entry.isFile() && filterFn(entry.name)) results.push(fullPath);
46
+ }
47
+ return results;
48
+ }
49
+
50
+ function isGitRepo(path) {
51
+ try {
52
+ execSync('git rev-parse --git-dir', { cwd: path, stdio: 'ignore' });
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ function gitLsTree(path, revision, dir) {
60
+ try {
61
+ const output = execSync(`git ls-tree -r --name-only ${revision} -- ${dir}`, { cwd: path, encoding: 'utf-8' });
62
+ return output.split('\n').filter(Boolean);
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
67
+
68
+ function gitShow(path, ref) {
69
+ try {
70
+ return execSync(`git show ${ref}`, { cwd: path, encoding: 'utf-8' });
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ function gitInit(path) {
77
+ try {
78
+ execSync('git init', { cwd: path, stdio: 'ignore' });
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ function gitAdd(path, files) {
86
+ try {
87
+ execSync(`git add ${files}`, { cwd: path, stdio: 'ignore' });
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ function gitStatus(path) {
95
+ try {
96
+ const output = execSync('git status --porcelain', { cwd: path, encoding: 'utf-8' });
97
+ const lines = output.split('\n').filter(Boolean);
98
+ const staged = lines.filter(line => /^[MARC]/.test(line)).map(line => line.substring(3));
99
+ return { staged };
100
+ } catch {
101
+ return { staged: [] };
102
+ }
103
+ }
104
+
105
+ function gitCommit(path, message, files) {
106
+ try {
107
+ const output = execSync(`git commit -m "${message}" ${files}`, { cwd: path, encoding: 'utf-8' });
108
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: path, encoding: 'utf-8' }).trim();
109
+ const commit = execSync('git rev-parse HEAD', { cwd: path, encoding: 'utf-8' }).trim();
110
+ return { branch, commit, summary: output };
111
+ } catch (err) {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ // ---------------------------------------
117
+ // Server
118
+ // ---------------------------------------
119
+ const server = createServer(async (req, res) => {
120
+ // CORS preflight
121
+ if (req.method === 'OPTIONS') {
122
+ res.writeHead(200, {
123
+ 'Access-Control-Allow-Origin': '*',
124
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
125
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization'
126
+ });
127
+ return res.end();
128
+ }
129
+
130
+ const url = new URL(req.url, `http://${req.headers.host}`);
131
+ const pathname = url.pathname;
132
+
133
+ // ---------------------------------------
134
+ // /api/ping
135
+ // ---------------------------------------
136
+ if (pathname === '/api/ping' && req.method === 'GET') {
137
+ return sendJSON(res, { ok: true, message: 'pong' });
138
+ }
139
+
140
+ // ---------------------------------------
141
+ // /api/generate
142
+ // ---------------------------------------
143
+ if (pathname === '/api/generate' && req.method === 'POST') {
144
+ let body = '';
145
+ req.on('data', chunk => { body += chunk; });
146
+ req.on('end', () => {
147
+ try {
148
+ const config = JSON.parse(body);
149
+ const urlObj = new URL(req.url, `http://${req.headers.host}`);
150
+ const exportAsConfig = urlObj.searchParams.get("exportAsConfig") === "true";
151
+ let fileName = "config.json";
152
+
153
+ if (exportAsConfig) {
154
+ fileName = "config.json";
155
+ } else if (config.context) {
156
+ fileName = slugify(config.context) + ".json";
157
+ }
158
+
159
+ writeFileSync(join(ROOT, fileName), JSON.stringify(config, null, 2));
160
+ console.log(`✅ ${fileName} written to ${ROOT}`);
161
+
162
+ // Read or initialize index.json
163
+ const indexFile = join(SLICES_DIR, 'index.json');
164
+ let sliceIndices = { slices: [] };
165
+ if (existsSync(indexFile)) {
166
+ try {
167
+ sliceIndices = JSON.parse(readFileSync(indexFile, 'utf-8'));
168
+ } catch {
169
+ sliceIndices = { slices: [] };
170
+ }
171
+ }
172
+
173
+ // Write all slices
174
+ if (config.slices) {
175
+ config.slices.forEach((slice) => {
176
+ const baseFolder = join(SLICES_DIR, slice.context ?? "default");
177
+ const sliceFolder = slice.title?.replaceAll(" ", "")?.replaceAll("slice:", "")?.toLowerCase();
178
+ const folder = join(baseFolder, sliceFolder);
179
+
180
+ if (!existsSync(folder)) {
181
+ mkdirSync(folder, { recursive: true });
182
+ }
183
+
184
+ const filePath = join(folder, 'slice.json');
185
+ const sliceData = { ...slice };
186
+ delete sliceData.index;
187
+ writeFileSync(filePath, JSON.stringify(sliceData, null, 2));
188
+
189
+ const sliceIndex = {
190
+ id: slice.id,
191
+ slice: slice.title,
192
+ index: slice.index,
193
+ context: slice.context ?? "default",
194
+ folder: sliceFolder,
195
+ status: slice.status
196
+ };
197
+
198
+ const index = sliceIndices.slices.findIndex(it => it.id == slice.id);
199
+ if (index == -1) {
200
+ sliceIndices.slices.push(sliceIndex);
201
+ } else {
202
+ sliceIndices.slices[index] = sliceIndex;
203
+ }
204
+ });
205
+ writeFileSync(indexFile, JSON.stringify(sliceIndices, null, 2));
206
+ }
207
+
208
+ // Write slice images
209
+ if (config.sliceImages) {
210
+ config.sliceImages.forEach((sliceImage) => {
211
+ const sliceFolder = sliceImage.slice?.replaceAll(" ", "")?.replaceAll("slice:", "")?.toLowerCase();
212
+ const folder = join(SLICES_DIR, sliceImage.context ?? "default", sliceFolder);
213
+
214
+ if (!existsSync(folder)) {
215
+ mkdirSync(folder, { recursive: true });
216
+ }
217
+
218
+ const base64String = sliceImage.base64Image.replace(/^data:image\/[a-z]+;base64,/, '');
219
+ const buffer = Buffer.from(base64String, 'base64');
220
+ const filePath = join(folder, `screen-${sliceImage.id}.png`);
221
+ writeFileSync(filePath, buffer);
222
+ });
223
+ }
224
+
225
+ sendJSON(res, { success: true, path: join(ROOT, fileName) });
226
+ console.log('🛑 Shutting down server...');
227
+ server.close(() => process.exit(0));
228
+ } catch (err) {
229
+ sendJSON(res, { success: false, error: err.message }, 400);
230
+ }
231
+ });
232
+ return;
233
+ }
234
+
235
+ // ---------------------------------------
236
+ // /api/slice-info
237
+ // ---------------------------------------
238
+ if (pathname === '/api/slice-info' && req.method === 'GET') {
239
+ try {
240
+ const indexPath = join(SLICES_DIR, 'index.json');
241
+ if (!existsSync(indexPath)) return sendJSON(res, { error: 'index.json not found' }, 404);
242
+ const indexData = JSON.parse(readFileSync(indexPath, 'utf-8'));
243
+ const specificationsMap = new Map();
244
+
245
+ const codeFiles = findFilesRecursive(SLICES_DIR, name => name === 'code-slice.json');
246
+ for (const file of codeFiles) {
247
+ try {
248
+ const cs = JSON.parse(readFileSync(file, 'utf-8'));
249
+ if (cs.id && cs.specifications) specificationsMap.set(cs.id, cs.specifications);
250
+ } catch {}
251
+ }
252
+
253
+ const allSlices = indexData.slices.map(s => ({
254
+ title: s.slice,
255
+ status: s.status,
256
+ id: s.id,
257
+ specifications: specificationsMap.get(s.id)
258
+ }));
259
+
260
+ return sendJSON(res, { slices: allSlices });
261
+ } catch (err) {
262
+ return sendJSON(res, { error: 'Failed to load slice-info', message: err.message }, 500);
263
+ }
264
+ }
265
+
266
+ // ---------------------------------------
267
+ // /api/slicepath
268
+ // ---------------------------------------
269
+ if (pathname === '/api/slicepath' && req.method === 'GET') {
270
+ try {
271
+ const sliceFiles = findFilesRecursive(SLICES_DIR, name => name.endsWith('.slice.json'));
272
+ const slices = sliceFiles.map(f => ({
273
+ path: relative(ROOT, f),
274
+ name: f.split('/').pop()
275
+ }));
276
+ return sendJSON(res, { slices });
277
+ } catch (err) {
278
+ return sendJSON(res, { error: 'Failed to list slice paths', message: err.message }, 500);
279
+ }
280
+ }
281
+
282
+ // ---------------------------------------
283
+ // /api/slices
284
+ // ---------------------------------------
285
+ if (pathname === '/api/slices' && req.method === 'GET') {
286
+ try {
287
+ const includeImages = url.searchParams.get('includeImages') === 'true';
288
+ const revision = url.searchParams.get('revision') ?? 'HEAD';
289
+ const isHEAD = revision === 'HEAD';
290
+ const isRepo = isGitRepo(repoPath);
291
+
292
+ // slice.json files
293
+ const trackedFiles = isRepo ? gitLsTree(repoPath, revision, '.slices') : [];
294
+ const sliceFiles = trackedFiles.filter(f => f.endsWith('slice.json'));
295
+ const trackedScreens = trackedFiles.filter(f => f.match(/screen-\d+\.png$/));
296
+
297
+ // filesystem screens if HEAD
298
+ let allScreens = [];
299
+ if (isHEAD) {
300
+ const fsScreens = findFilesRecursive(SLICES_DIR, name => name.match(/screen-\d+\.png$/));
301
+ allScreens = [...new Set([...fsScreens, ...trackedScreens])];
302
+ } else allScreens = trackedScreens;
303
+
304
+ const slices = [];
305
+ for (const file of sliceFiles) {
306
+ if (isHEAD && existsSync(join(ROOT, file))) {
307
+ slices.push(JSON.parse(readFileSync(join(ROOT, file), 'utf-8')));
308
+ } else if (isRepo) {
309
+ const content = gitShow(repoPath, `${revision}:${file}`);
310
+ if (content) slices.push(JSON.parse(content));
311
+ }
312
+ }
313
+
314
+ const sliceImages = [];
315
+ if (includeImages) {
316
+ for (const screenPath of allScreens) {
317
+ try {
318
+ const buffer = isHEAD && existsSync(screenPath)
319
+ ? readFileSync(screenPath)
320
+ : execSync(`git show ${revision}:${screenPath}`, { cwd: ROOT, encoding: 'buffer' });
321
+
322
+ sliceImages.push({
323
+ id: parseIdWithRegex(screenPath.split('/').pop()) ?? '',
324
+ slice: '',
325
+ title: '',
326
+ base64Image: `data:image/png;base64,${buffer.toString('base64')}`
327
+ });
328
+ } catch {}
329
+ }
330
+ }
331
+
332
+ return sendJSON(res, { slices, sliceImages, meta: { revision, sliceCount: slices.length, screenCount: sliceImages.length } });
333
+ } catch (err) {
334
+ return sendJSON(res, { error: 'Failed to load slices', message: err.message }, 500);
335
+ }
336
+ }
337
+
338
+ // ---------------------------------------
339
+ // /api/config
340
+ // ---------------------------------------
341
+ if (pathname === '/api/config' && req.method === 'GET') {
342
+ const storedData = { version: 1.0 };
343
+ try {
344
+ const gitRepo = isGitRepo(repoPath);
345
+ if (storedData) storedData.gitRepo = gitRepo;
346
+ } catch (e) {}
347
+ return sendJSON(res, storedData);
348
+ }
349
+
350
+ if (pathname === '/api/config' && req.method === 'POST') {
351
+ let body = '';
352
+ req.on('data', chunk => { body += chunk; });
353
+ req.on('end', () => {
354
+ try {
355
+ JSON.parse(body);
356
+ return sendJSON(res, { success: 'Data stored successfully!' });
357
+ } catch (err) {
358
+ return sendJSON(res, { error: 'Failed to store data' }, 500);
359
+ }
360
+ });
361
+ return;
362
+ }
363
+
364
+ // ---------------------------------------
365
+ // /api/progress
366
+ // ---------------------------------------
367
+ if (pathname === '/api/progress' && req.method === 'GET') {
368
+ try {
369
+ const progressPath = join(repoPath, 'progress.txt');
370
+ if (!existsSync(progressPath)) {
371
+ return sendJSON(res, { available: false, progress: [] });
372
+ }
373
+ const content = readFileSync(progressPath, 'utf-8');
374
+ const paragraphs = content
375
+ .split(/(?=>>> Iteration \d+)/)
376
+ .map(p => p.trim())
377
+ .filter(p => p.length > 0);
378
+ return sendJSON(res, { available: true, progress: paragraphs });
379
+ } catch (err) {
380
+ return sendJSON(res, { error: 'Failed to load progress', message: err.message }, 500);
381
+ }
382
+ }
383
+
384
+ // ---------------------------------------
385
+ // /api/delete-slice
386
+ // ---------------------------------------
387
+ if (pathname === '/api/delete-slice' && req.method === 'POST') {
388
+ let body = '';
389
+ req.on('data', chunk => { body += chunk; });
390
+ req.on('end', () => {
391
+ try {
392
+ const data = JSON.parse(body);
393
+ const sliceId = data.id;
394
+
395
+ if (!sliceId) {
396
+ return sendJSON(res, { error: 'Missing required parameter: id' }, 400);
397
+ }
398
+
399
+ function findAndDeleteCodeSliceById(dir, targetId) {
400
+ if (!existsSync(dir)) return { found: false };
401
+ const entries = readdirSync(dir, { withFileTypes: true });
402
+
403
+ for (const entry of entries) {
404
+ const fullPath = join(dir, entry.name);
405
+ if (entry.isDirectory()) {
406
+ const result = findAndDeleteCodeSliceById(fullPath, targetId);
407
+ if (result.found) return result;
408
+ } else if (entry.name === 'code-slice.json') {
409
+ try {
410
+ const codeSliceContent = readFileSync(fullPath, 'utf-8');
411
+ const codeSlice = JSON.parse(codeSliceContent);
412
+ if (codeSlice.id === targetId) {
413
+ unlinkSync(fullPath);
414
+ return { found: true, deletedPath: fullPath };
415
+ }
416
+ } catch (error) {
417
+ console.error(`Error reading code-slice.json at ${fullPath}:`, error);
418
+ }
419
+ }
420
+ }
421
+ return { found: false };
422
+ }
423
+
424
+ if (!existsSync(SLICES_DIR)) {
425
+ return sendJSON(res, { error: '.slices directory not found' }, 404);
426
+ }
427
+
428
+ const result = findAndDeleteCodeSliceById(SLICES_DIR, sliceId);
429
+ if (result.found) {
430
+ return sendJSON(res, {
431
+ success: true,
432
+ message: `Successfully deleted code-slice.json with id: ${sliceId}`,
433
+ deletedPath: result.deletedPath
434
+ });
435
+ } else {
436
+ return sendJSON(res, { error: `No code-slice.json file found with id: ${sliceId}` }, 404);
437
+ }
438
+ } catch (err) {
439
+ return sendJSON(res, { error: 'Failed to delete code-slice.json', message: err.message }, 500);
440
+ }
441
+ });
442
+ return;
443
+ }
444
+
445
+ // ---------------------------------------
446
+ // /api/git
447
+ // ---------------------------------------
448
+ if (pathname === '/api/git' && req.method === 'GET') {
449
+ const storedData = { version: 1.0 };
450
+ try {
451
+ const gitRepo = isGitRepo(repoPath);
452
+ if (storedData) storedData.gitRepo = gitRepo;
453
+ } catch (e) {}
454
+ return sendJSON(res, storedData);
455
+ }
456
+
457
+ if (pathname === '/api/git' && req.method === 'POST') {
458
+ try {
459
+ let isRepo = isGitRepo(repoPath);
460
+ if (!isRepo) {
461
+ gitInit(repoPath);
462
+ }
463
+ gitAdd(repoPath, '.slices');
464
+ const statusResult = gitStatus(repoPath);
465
+
466
+ let commitResult = undefined;
467
+ if (statusResult.staged?.length > 0) {
468
+ commitResult = gitCommit(repoPath, '(chore) slices', '.slices');
469
+ }
470
+
471
+ return sendJSON(res, {
472
+ branch: commitResult?.branch,
473
+ revision: commitResult?.commit
474
+ });
475
+ } catch (error) {
476
+ console.error('Error committing:', error);
477
+ return sendJSON(res, {
478
+ error: 'Failed to commit slices',
479
+ message: error instanceof Error ? error.message : 'Unknown error'
480
+ }, 500);
481
+ }
482
+ }
483
+
484
+ // ---------------------------------------
485
+ // 404
486
+ // ---------------------------------------
487
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
488
+ res.end('Not Found');
489
+ });
490
+
491
+ // ---------------------------------------
492
+ // Listen
493
+ // ---------------------------------------
494
+ server.listen(3001, () => {
495
+ console.log('🚀 Server running at http://localhost:3001');
496
+ console.log('💓 GET /api/ping');
497
+ console.log('📥 POST /api/generate');
498
+ console.log('📄 GET /api/slice-info');
499
+ console.log('📂 GET /api/slicepath');
500
+ console.log('📦 GET /api/slices?includeImages=true');
501
+ console.log('⚙️ GET/POST /api/config');
502
+ console.log('📊 GET /api/progress');
503
+ console.log('🗑️ POST /api/delete-slice');
504
+ console.log('🔧 GET/POST /api/git');
505
+ });