@rashidee/co2 1.3.7 → 1.3.9
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/.co2-dat/app.db +0 -0
- package/dist/.co2-dat/app.db-shm +0 -0
- package/dist/.co2-dat/app.db-wal +0 -0
- package/dist/index.js +204 -75
- package/package.json +41 -41
- package/plugin/skills/conductor-feature-develop/SKILL.md +1384 -1383
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +225 -224
- package/plugin/skills/mockgen-shadcn/SKILL.md +1073 -1067
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +5 -3
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +631 -498
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +2 -2
- package/plugin/skills/mockgen-tailwind/SKILL.md +913 -904
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +722 -720
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +631 -498
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -190
- package/static/assets/{abnfDiagram-VRR7QNED-CsyqZblo.js → abnfDiagram-VRR7QNED-C0CrqPXX.js} +1 -1
- package/static/assets/{arc-it3yvCvj.js → arc-BFSRYIcI.js} +1 -1
- package/static/assets/{architectureDiagram-ZJ3FMSHR-sjNv2MOQ.js → architectureDiagram-ZJ3FMSHR-DuwTK2W9.js} +1 -1
- package/static/assets/{blockDiagram-677ZJIJ3-DW9ZjtwO.js → blockDiagram-677ZJIJ3-Bn9tAB0n.js} +1 -1
- package/static/assets/{c4Diagram-LMCZKHZV-DP3gJkhN.js → c4Diagram-LMCZKHZV-CDiCEjY7.js} +1 -1
- package/static/assets/channel-CZGeGYqv.js +1 -0
- package/static/assets/{chunk-2Q5K7J3B-ByxTr7KB.js → chunk-2Q5K7J3B-BMwS7NgW.js} +1 -1
- package/static/assets/{chunk-32BRIVSS-vNETwpX4.js → chunk-32BRIVSS-8l0XBCjq.js} +1 -1
- package/static/assets/{chunk-5VM5RSS4-DbdsRtpE.js → chunk-5VM5RSS4-C6Cv-67E.js} +1 -1
- package/static/assets/{chunk-EX3LRPZG-DPkDGikN.js → chunk-EX3LRPZG-DjWs4lB9.js} +1 -1
- package/static/assets/{chunk-JWPE2WC7-Ccpx-Rog.js → chunk-JWPE2WC7-DuK_Mu9c.js} +1 -1
- package/static/assets/{chunk-MOJQB5TN-CnRPtHb3.js → chunk-MOJQB5TN-K-bFFmpr.js} +1 -1
- package/static/assets/{chunk-RYQCIY6F-C--_KRGR.js → chunk-RYQCIY6F-B-b4TyDb.js} +1 -1
- package/static/assets/{chunk-V7JOEXUC-BJQ5fwNa.js → chunk-V7JOEXUC-BZRKjyVr.js} +1 -1
- package/static/assets/{chunk-VR4S4FIN-BPcVRYqJ.js → chunk-VR4S4FIN-BcvbM_n3.js} +1 -1
- package/static/assets/{chunk-XXDRQBXY-BoC3DCKQ.js → chunk-XXDRQBXY-Z2nhcpRg.js} +1 -1
- package/static/assets/classDiagram-OUVF2IWQ-DV5beYlE.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DV5beYlE.js +1 -0
- package/static/assets/{cose-bilkent-JH36ORCC-DlKrOw_E.js → cose-bilkent-JH36ORCC-CPBw8Z2X.js} +1 -1
- package/static/assets/{cynefin-VYW2F7L2-CZQhaPM1.js → cynefin-VYW2F7L2-DeJ1X-1F.js} +1 -1
- package/static/assets/{cynefinDiagram-TSTJHNR4-cZP74_0I.js → cynefinDiagram-TSTJHNR4-BPs3jIZR.js} +1 -1
- package/static/assets/{dagre-VKFMJZFB-tB2cBd_e.js → dagre-VKFMJZFB-Bn5Xp63O.js} +1 -1
- package/static/assets/{diagram-FQU43EPY-QwoADT9c.js → diagram-FQU43EPY-DWIlVMUR.js} +1 -1
- package/static/assets/{diagram-G47NLZAW-Bjz2rwUz.js → diagram-G47NLZAW-BWxpunVI.js} +1 -1
- package/static/assets/{diagram-NH7WQ7WH-DS9j6K7F.js → diagram-NH7WQ7WH-CpoAYUis.js} +1 -1
- package/static/assets/{diagram-OA4YK3LP-CUPwlGEi.js → diagram-OA4YK3LP-Dv1XfQGK.js} +1 -1
- package/static/assets/{diagram-WEI45ONY-iejOZOdV.js → diagram-WEI45ONY-DSk6wZtO.js} +1 -1
- package/static/assets/{ebnfDiagram-CCIWWBDH-Bg3puNXE.js → ebnfDiagram-CCIWWBDH-DIDhI2dD.js} +1 -1
- package/static/assets/{erDiagram-Q63AITRT-DTxdGEtK.js → erDiagram-Q63AITRT-CGO6ep6G.js} +1 -1
- package/static/assets/{flowDiagram-23GEKE2U-CRl-AJlj.js → flowDiagram-23GEKE2U-D8nzLjfG.js} +1 -1
- package/static/assets/{ganttDiagram-NO4QXBWP-wCOJ8cfC.js → ganttDiagram-NO4QXBWP-Usz8f26d.js} +1 -1
- package/static/assets/{gitGraphDiagram-IHSO6WYX-CnVtKot-.js → gitGraphDiagram-IHSO6WYX-WEJUXa7P.js} +1 -1
- package/static/assets/{index-xLqMMC0F.css → index-CM4GfOLV.css} +1 -1
- package/static/assets/{index-DAL1vVaa.js → index-j3pCmOym.js} +194 -194
- package/static/assets/{infoDiagram-FWYZ7A6U-DmvEl4fi.js → infoDiagram-FWYZ7A6U-BRPwqRvz.js} +1 -1
- package/static/assets/{ishikawaDiagram-FXEZZL3T-CjKerAXn.js → ishikawaDiagram-FXEZZL3T-CrntEiqz.js} +1 -1
- package/static/assets/{journeyDiagram-5HDEW3XC-Bmc4ARKl.js → journeyDiagram-5HDEW3XC-BmhRsz4F.js} +1 -1
- package/static/assets/{kanban-definition-HUTT4EX6-BZ9I3imN.js → kanban-definition-HUTT4EX6-CWxUXdc3.js} +1 -1
- package/static/assets/{linear-CrHKLpp_.js → linear-BGjzqzT6.js} +1 -1
- package/static/assets/{mindmap-definition-LN4V7U3C-SUk59XXF.js → mindmap-definition-LN4V7U3C-CoVNmO4b.js} +1 -1
- package/static/assets/{pegDiagram-2B236MQR-CknFYdh_.js → pegDiagram-2B236MQR-hBHOzL7x.js} +1 -1
- package/static/assets/{pieDiagram-ENE6RG2P-B3VlLXXT.js → pieDiagram-ENE6RG2P-Cpv7VZPG.js} +1 -1
- package/static/assets/{quadrantDiagram-ABIIQ3AL-BUkZjvW_.js → quadrantDiagram-ABIIQ3AL-BIQAB21D.js} +1 -1
- package/static/assets/{railroadDiagram-RFXS5EU6-LklmPimD.js → railroadDiagram-RFXS5EU6-DX6hST3S.js} +1 -1
- package/static/assets/{requirementDiagram-TGXJPOKE-DnRFaZbz.js → requirementDiagram-TGXJPOKE-Gas2cE_l.js} +1 -1
- package/static/assets/{sankeyDiagram-HTMAVEWB-D242DUlQ.js → sankeyDiagram-HTMAVEWB-BpwoyJDe.js} +1 -1
- package/static/assets/{sequenceDiagram-DBY2YBRQ-DmiiStqo.js → sequenceDiagram-DBY2YBRQ-CzBxna5k.js} +1 -1
- package/static/assets/{sizeCapture-X5ZJPWSS-cbMvR047.js → sizeCapture-X5ZJPWSS-Cn-5NsLr.js} +1 -1
- package/static/assets/{stateDiagram-2N3HPSRC-YIIylI9N.js → stateDiagram-2N3HPSRC-BiW03iCW.js} +1 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-CmQ_Kjkj.js +1 -0
- package/static/assets/{swimlanes-5IMT3BWC-Ba309Ysu.js → swimlanes-5IMT3BWC-3jPuufWt.js} +2 -2
- package/static/assets/swimlanesDiagram-G3AALYLV-OaMW2HRF.js +8 -0
- package/static/assets/{timeline-definition-FHXFAJF6-BdpW3kGY.js → timeline-definition-FHXFAJF6-CzThWt1O.js} +1 -1
- package/static/assets/{vennDiagram-L72KCM5P-BiSgQ9Lf.js → vennDiagram-L72KCM5P-D1Axj0CM.js} +1 -1
- package/static/assets/{wardleyDiagram-EHGQE667-Clr8YWGW.js → wardleyDiagram-EHGQE667-mKj7UT6Q.js} +1 -1
- package/static/assets/{xychartDiagram-FW5EYKEG-LsC066MO.js → xychartDiagram-FW5EYKEG-CSAkvAer.js} +1 -1
- package/static/index.html +2 -2
- package/static/assets/channel-C_J_aLZ7.js +0 -1
- package/static/assets/classDiagram-OUVF2IWQ-yUfBrCjx.js +0 -1
- package/static/assets/classDiagram-v2-EOCWNBFH-yUfBrCjx.js +0 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-m0MSGh0v.js +0 -1
- package/static/assets/swimlanesDiagram-G3AALYLV-DY_J_ih_.js +0 -8
|
@@ -1,498 +1,631 @@
|
|
|
1
|
-
# Mockup Hub — Single Node Server for ALL Application Mockups
|
|
2
|
-
|
|
3
|
-
The Mockup Hub is ONE zero-dependency Node.js application at `<root>/mockup/` that serves
|
|
4
|
-
the mockups of EVERY application in the project. Per-application mockup folders
|
|
5
|
-
(`<app_folder>/context/mockup/`) contain ONLY assets (HTML/CSS/JS/images + a manifest) —
|
|
6
|
-
they contain NO server of their own.
|
|
7
|
-
|
|
8
|
-
- **Tailwind mockups** (`mockgen-tailwind`): the hub assembles shell + header + sidebar +
|
|
9
|
-
content + footer server-side at `/{app_slug}/{role}/{page}`.
|
|
10
|
-
- **shadcn mockups** (`mockgen-shadcn`): the hub serves the app's built `dist/` folder at
|
|
11
|
-
`/{app_slug}/...` with SPA fallback to `dist/index.html`.
|
|
12
|
-
- **Landing page** at `/`: dynamically lists ALL applications and their roles. Roles are
|
|
13
|
-
clickable only when the app's mockup is ready; otherwise they render as disabled
|
|
14
|
-
(grayed, no `href`) chips with a status hint ("Mockups not generated yet" /
|
|
15
|
-
"Build required").
|
|
16
|
-
- **Port** is configurable: `PORT` env var → `mockup.config.json` `port` →
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
`
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
1
|
+
# Mockup Hub — Single Node Server for ALL Application Mockups
|
|
2
|
+
|
|
3
|
+
The Mockup Hub is ONE zero-dependency Node.js application at `<root>/mockup/` that serves
|
|
4
|
+
the mockups of EVERY application in the project. Per-application mockup folders
|
|
5
|
+
(`<app_folder>/context/mockup/`) contain ONLY assets (HTML/CSS/JS/images + a manifest) —
|
|
6
|
+
they contain NO server of their own.
|
|
7
|
+
|
|
8
|
+
- **Tailwind mockups** (`mockgen-tailwind`): the hub assembles shell + header + sidebar +
|
|
9
|
+
content + footer server-side at `/{app_slug}/{role}/{page}`.
|
|
10
|
+
- **shadcn mockups** (`mockgen-shadcn`): the hub serves the app's built `dist/` folder at
|
|
11
|
+
`/{app_slug}/...` with SPA fallback to `dist/index.html`.
|
|
12
|
+
- **Landing page** at `/`: dynamically lists ALL applications and their roles. Roles are
|
|
13
|
+
clickable only when the app's mockup is ready; otherwise they render as disabled
|
|
14
|
+
(grayed, no `href`) chips with a status hint ("Mockups not generated yet" /
|
|
15
|
+
"Build required").
|
|
16
|
+
- **Port** is configurable: `PORT` env var → `mockup.config.json` `port` → auto-pick the
|
|
17
|
+
first **unused port starting from 4000**. The landing page carries a **port input text**
|
|
18
|
+
(defaulting to the current port) whose Apply action persists the choice to
|
|
19
|
+
`mockup.config.json` and re-listens on the new port. Compound Context Studio's Mockup
|
|
20
|
+
page reads its own topbar port input and passes it via the `PORT` env when its
|
|
21
|
+
**Start Hub** button spawns this server.
|
|
22
|
+
|
|
23
|
+
The hub has **zero npm dependencies** — `npm start` (or `node server.js`) works with no
|
|
24
|
+
`npm install`.
|
|
25
|
+
|
|
26
|
+
## File Structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
<root>/
|
|
30
|
+
mockup/ # ← the hub (this template)
|
|
31
|
+
server.js # zero-dependency Node HTTP server
|
|
32
|
+
package.json # no dependencies; "start": "node server.js"
|
|
33
|
+
mockup.config.json # { "port": 4000 } — user-editable (first unused port ≥ 4000)
|
|
34
|
+
.gitignore
|
|
35
|
+
1_first_app/
|
|
36
|
+
context/mockup/ # assets only (+ mockup-manifest.json)
|
|
37
|
+
2_second_app/
|
|
38
|
+
context/mockup/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Ensure-Hub Rules (run by every mockgen-* execution, including module-filtered runs)
|
|
42
|
+
|
|
43
|
+
1. If `<root>/mockup/server.js` does NOT exist: create ALL four hub files from the
|
|
44
|
+
templates below.
|
|
45
|
+
2. If it exists: read the `HUB_VERSION` constant in the existing `server.js`.
|
|
46
|
+
- Existing version **< template version** → overwrite `server.js` and `package.json`
|
|
47
|
+
with the templates (upgrade).
|
|
48
|
+
- Existing version **>= template version**, or no marker found (user-customized) →
|
|
49
|
+
leave `server.js` and `package.json` untouched.
|
|
50
|
+
3. `mockup.config.json`: create ONLY if missing. NEVER overwrite an existing one — it
|
|
51
|
+
holds the user's port choice. It is per-machine and gitignored. When creating it, set
|
|
52
|
+
`port` to the **first unused port starting from 4000** (default `4000`; if you cannot
|
|
53
|
+
check, just write `4000` — the server auto-scans upward from a busy config port at
|
|
54
|
+
startup and writes the effective port back).
|
|
55
|
+
4. `.gitignore`: create ONLY if missing.
|
|
56
|
+
5. Never place per-application assets inside `<root>/mockup/`. Conversely, everything the
|
|
57
|
+
hub generates or needs (config, logs, any `node_modules/`) stays inside `<root>/mockup/`
|
|
58
|
+
and is covered by its `.gitignore`.
|
|
59
|
+
|
|
60
|
+
## mockup-manifest.json Schema (written to `<app_folder>/context/mockup/`)
|
|
61
|
+
|
|
62
|
+
Every mockgen-* run writes/updates this file. The hub uses it for discovery, routing and
|
|
63
|
+
the landing page.
|
|
64
|
+
|
|
65
|
+
| Field | Type | Description |
|
|
66
|
+
|-------|------|-------------|
|
|
67
|
+
| `app` | string | App slug used in URLs = app folder name minus leading `<number>_` (e.g. `hub_middleware`) |
|
|
68
|
+
| `appName` | string | Display name (e.g. "Hub Middleware") |
|
|
69
|
+
| `description` | string | Short app description from PRD.md |
|
|
70
|
+
| `stack` | string | `"tailwind"` or `"shadcn"` |
|
|
71
|
+
| `version` | string | Target version the mockups were generated for (e.g. `v1.0.3`) |
|
|
72
|
+
| `generatedAt` | string | ISO date of the last generation |
|
|
73
|
+
| `roles` | array | One entry per role: `{ "name", "slug", "screens" }`. `slug` uses the stack's convention — snake_case (tailwind) or kebab-case (shadcn). `screens` = number of screens generated for the role |
|
|
74
|
+
| `reports` | array (optional) | Report layout file names, if any |
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"app": "hub_middleware",
|
|
81
|
+
"appName": "Hub Middleware",
|
|
82
|
+
"description": "Central middleware for labour mobility data exchange.",
|
|
83
|
+
"stack": "tailwind",
|
|
84
|
+
"version": "v1.0.3",
|
|
85
|
+
"generatedAt": "2026-07-12",
|
|
86
|
+
"roles": [
|
|
87
|
+
{ "name": "Hub Administrator", "slug": "hub_administrator", "screens": 12 },
|
|
88
|
+
{ "name": "Hub Operation Support", "slug": "hub_operation_support", "screens": 9 }
|
|
89
|
+
],
|
|
90
|
+
"reports": ["staff_allocation_summary.html"]
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## server.js Template
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
#!/usr/bin/env node
|
|
98
|
+
/*
|
|
99
|
+
* CO2 Mockup Hub — one server for ALL application mockups.
|
|
100
|
+
* HUB_VERSION: 2 (marker used by mockgen-* skills to decide upgrades — do not remove)
|
|
101
|
+
*
|
|
102
|
+
* Discovers every root-level application folder containing context/PRD.md.
|
|
103
|
+
* Apps with a context/mockup/mockup-manifest.json are routable:
|
|
104
|
+
* - stack "tailwind": pages assembled from partials + content fragments
|
|
105
|
+
* - stack "shadcn": served from the built dist/ folder (SPA fallback)
|
|
106
|
+
* Port: PORT env var > mockup.config.json "port" > first unused port from 4000.
|
|
107
|
+
* - A PORT env value is exact (Compound Context Studio's Start Hub passes it) — a busy
|
|
108
|
+
* port fails fast so the caller sees the error.
|
|
109
|
+
* - A config/auto port scans upward from a busy port; the effective port is written
|
|
110
|
+
* back to mockup.config.json so probes and the next start agree.
|
|
111
|
+
* - The landing page has a port input (POST /api/port) that persists a new port and
|
|
112
|
+
* re-listens on it.
|
|
113
|
+
*/
|
|
114
|
+
'use strict';
|
|
115
|
+
|
|
116
|
+
const http = require('http');
|
|
117
|
+
const net = require('net');
|
|
118
|
+
const fs = require('fs');
|
|
119
|
+
const path = require('path');
|
|
120
|
+
|
|
121
|
+
const HUB_VERSION = 2;
|
|
122
|
+
const HUB_DIR = __dirname;
|
|
123
|
+
const ROOT_DIR = path.resolve(HUB_DIR, '..');
|
|
124
|
+
const SAFE_SEG = /^[A-Za-z0-9._-]+$/;
|
|
125
|
+
const PORT_SCAN_START = 4000; // default: first unused port starting here
|
|
126
|
+
const PORT_SCAN_LIMIT = 100;
|
|
127
|
+
|
|
128
|
+
/** { port, source: 'env' | 'config' | 'auto' } — env is exact, config/auto may scan. */
|
|
129
|
+
function resolvePort() {
|
|
130
|
+
const envPort = parseInt(process.env.PORT || '', 10);
|
|
131
|
+
if (Number.isInteger(envPort) && envPort > 0) return { port: envPort, source: 'env' };
|
|
132
|
+
try {
|
|
133
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(HUB_DIR, 'mockup.config.json'), 'utf8'));
|
|
134
|
+
const cfgPort = parseInt(cfg.port, 10);
|
|
135
|
+
if (Number.isInteger(cfgPort) && cfgPort > 0) return { port: cfgPort, source: 'config' };
|
|
136
|
+
} catch (err) { /* missing/invalid config — auto-pick */ }
|
|
137
|
+
return { port: PORT_SCAN_START, source: 'auto' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function persistPort(port) {
|
|
141
|
+
try {
|
|
142
|
+
fs.writeFileSync(
|
|
143
|
+
path.join(HUB_DIR, 'mockup.config.json'),
|
|
144
|
+
JSON.stringify({ port: port }, null, 2) + '\n'
|
|
145
|
+
);
|
|
146
|
+
} catch (err) { /* read-only folder — the port still applies for this run */ }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const MIME = {
|
|
150
|
+
'.html': 'text/html; charset=utf-8',
|
|
151
|
+
'.css': 'text/css; charset=utf-8',
|
|
152
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
153
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
154
|
+
'.json': 'application/json; charset=utf-8',
|
|
155
|
+
'.svg': 'image/svg+xml',
|
|
156
|
+
'.png': 'image/png',
|
|
157
|
+
'.jpg': 'image/jpeg',
|
|
158
|
+
'.jpeg': 'image/jpeg',
|
|
159
|
+
'.gif': 'image/gif',
|
|
160
|
+
'.webp': 'image/webp',
|
|
161
|
+
'.ico': 'image/x-icon',
|
|
162
|
+
'.woff': 'font/woff',
|
|
163
|
+
'.woff2': 'font/woff2',
|
|
164
|
+
'.ttf': 'font/ttf',
|
|
165
|
+
'.map': 'application/json',
|
|
166
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
167
|
+
'.pdf': 'application/pdf',
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// ── Discovery ────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
function titleCase(slug) {
|
|
173
|
+
return slug.replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function rolesFromPrd(appDir) {
|
|
177
|
+
try {
|
|
178
|
+
const prd = fs.readFileSync(path.join(appDir, 'context', 'PRD.md'), 'utf8');
|
|
179
|
+
const names = new Set();
|
|
180
|
+
for (const line of prd.split('\n')) {
|
|
181
|
+
if (line.includes('~~')) continue; // skip strikethrough (deprecated) items
|
|
182
|
+
const m = line.match(/As an? (.+?) user/);
|
|
183
|
+
if (m) names.add(m[1].trim());
|
|
184
|
+
}
|
|
185
|
+
return Array.from(names).map((name) => ({ name, slug: null, screens: 0 }));
|
|
186
|
+
} catch (err) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function discoverApps() {
|
|
192
|
+
let entries;
|
|
193
|
+
try { entries = fs.readdirSync(ROOT_DIR, { withFileTypes: true }); } catch (err) { return []; }
|
|
194
|
+
const apps = [];
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
if (!entry.isDirectory()) continue;
|
|
197
|
+
const appDir = path.join(ROOT_DIR, entry.name);
|
|
198
|
+
if (!fs.existsSync(path.join(appDir, 'context', 'PRD.md'))) continue;
|
|
199
|
+
const slug = entry.name.replace(/^\d+_/, '');
|
|
200
|
+
const mockupDir = path.join(appDir, 'context', 'mockup');
|
|
201
|
+
let manifest = null;
|
|
202
|
+
try {
|
|
203
|
+
manifest = JSON.parse(fs.readFileSync(path.join(mockupDir, 'mockup-manifest.json'), 'utf8'));
|
|
204
|
+
} catch (err) { /* mockups not generated yet */ }
|
|
205
|
+
let status = 'not-generated';
|
|
206
|
+
if (manifest && manifest.stack === 'shadcn') {
|
|
207
|
+
status = fs.existsSync(path.join(mockupDir, 'dist', 'index.html')) ? 'ready' : 'build-required';
|
|
208
|
+
} else if (manifest) {
|
|
209
|
+
status = fs.existsSync(path.join(mockupDir, 'partials', 'shell.html')) ? 'ready' : 'not-generated';
|
|
210
|
+
}
|
|
211
|
+
apps.push({
|
|
212
|
+
slug,
|
|
213
|
+
folder: entry.name,
|
|
214
|
+
appName: (manifest && manifest.appName) || titleCase(slug),
|
|
215
|
+
description: (manifest && manifest.description) || '',
|
|
216
|
+
version: (manifest && manifest.version) || '',
|
|
217
|
+
stack: manifest ? manifest.stack : null,
|
|
218
|
+
status,
|
|
219
|
+
mockupDir,
|
|
220
|
+
roles: manifest ? (manifest.roles || []) : rolesFromPrd(appDir),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return apps.sort((a, b) => a.folder.localeCompare(b.folder));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function findApp(slug) {
|
|
227
|
+
return discoverApps().find((a) => a.slug === slug) || null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Response helpers ─────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
function send(res, code, body, type) {
|
|
233
|
+
res.writeHead(code, { 'Content-Type': type || 'text/html; charset=utf-8' });
|
|
234
|
+
res.end(body);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function redirect(res, location) {
|
|
238
|
+
res.writeHead(302, { Location: location });
|
|
239
|
+
res.end();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function escapeHtml(s) {
|
|
243
|
+
return String(s)
|
|
244
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
245
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function safeJoin(baseDir, segments) {
|
|
249
|
+
if (!segments.every((s) => SAFE_SEG.test(s))) return null;
|
|
250
|
+
const target = path.normalize(path.join(baseDir, segments.join(path.sep)));
|
|
251
|
+
if (target !== baseDir && !target.startsWith(baseDir + path.sep)) return null;
|
|
252
|
+
return target;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function serveFile(res, filePath) {
|
|
256
|
+
if (!filePath) return false;
|
|
257
|
+
let stat;
|
|
258
|
+
try { stat = fs.statSync(filePath); } catch (err) { return false; }
|
|
259
|
+
if (!stat.isFile()) return false;
|
|
260
|
+
const type = MIME[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
|
|
261
|
+
send(res, 200, fs.readFileSync(filePath), type);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readText(filePath) {
|
|
266
|
+
try { return fs.readFileSync(filePath, 'utf8'); } catch (err) { return null; }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Landing page ─────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
const LANDING_CSS = `
|
|
272
|
+
* { box-sizing: border-box; margin: 0; }
|
|
273
|
+
body { font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
274
|
+
background: #f1f5f9; color: #0f172a; padding: 40px 20px; }
|
|
275
|
+
.wrap { max-width: 960px; margin: 0 auto; }
|
|
276
|
+
h1 { font-size: 26px; margin-bottom: 4px; }
|
|
277
|
+
.sub { color: #64748b; font-size: 14px; margin-bottom: 28px; }
|
|
278
|
+
.card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px;
|
|
279
|
+
padding: 20px; margin-bottom: 16px; }
|
|
280
|
+
.card h2 { font-size: 18px; display: inline; }
|
|
281
|
+
.meta { color: #94a3b8; font-size: 12px; margin-left: 8px; }
|
|
282
|
+
.badge { display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 999px;
|
|
283
|
+
margin-left: 8px; vertical-align: 2px; }
|
|
284
|
+
.badge.ready { background: #dcfce7; color: #166534; }
|
|
285
|
+
.badge.pending { background: #fef3c7; color: #92400e; }
|
|
286
|
+
.hint { color: #94a3b8; font-size: 12px; margin-top: 10px; }
|
|
287
|
+
.hint code { background: #f1f5f9; padding: 1px 5px; border-radius: 4px; }
|
|
288
|
+
.roles { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 8px; }
|
|
289
|
+
.role { display: inline-block; font-size: 13px; padding: 6px 14px; border-radius: 8px;
|
|
290
|
+
border: 1px solid #cbd5e1; text-decoration: none; color: #0f172a; }
|
|
291
|
+
a.role:hover { border-color: #2563eb; color: #2563eb; }
|
|
292
|
+
.role.disabled { color: #cbd5e1; border-color: #e2e8f0; cursor: not-allowed; }
|
|
293
|
+
.index-link { font-size: 12px; margin-left: 8px; }
|
|
294
|
+
.foot { color: #94a3b8; font-size: 12px; text-align: center; margin-top: 28px; }
|
|
295
|
+
.portbox { display: inline-flex; align-items: center; gap: 6px; margin-left: 10px; }
|
|
296
|
+
.portbox input { width: 64px; font-size: 12px; padding: 3px 6px; border: 1px solid #cbd5e1;
|
|
297
|
+
border-radius: 6px; color: #0f172a; }
|
|
298
|
+
.portbox button { font-size: 12px; padding: 3px 10px; border: 1px solid #cbd5e1;
|
|
299
|
+
border-radius: 6px; background: #fff; cursor: pointer; }
|
|
300
|
+
.portbox button:hover { border-color: #2563eb; color: #2563eb; }
|
|
301
|
+
#port-msg { font-size: 12px; margin-left: 6px; }
|
|
302
|
+
`;
|
|
303
|
+
|
|
304
|
+
function renderRoleChips(app) {
|
|
305
|
+
if (!app.roles.length) {
|
|
306
|
+
return '<span class="role disabled">No roles found</span>';
|
|
307
|
+
}
|
|
308
|
+
return app.roles.map((role) => {
|
|
309
|
+
if (app.status === 'ready' && role.slug) {
|
|
310
|
+
return '<a class="role" href="/' + app.slug + '/' + role.slug + '/home">'
|
|
311
|
+
+ escapeHtml(role.name) + ' →</a>';
|
|
312
|
+
}
|
|
313
|
+
return '<span class="role disabled" aria-disabled="true">' + escapeHtml(role.name) + '</span>';
|
|
314
|
+
}).join('\n ');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function renderAppCard(app) {
|
|
318
|
+
const badge = app.status === 'ready'
|
|
319
|
+
? '<span class="badge ready">Ready' + (app.stack ? ' · ' + escapeHtml(app.stack) : '') + '</span>'
|
|
320
|
+
: (app.status === 'build-required'
|
|
321
|
+
? '<span class="badge pending">Build required</span>'
|
|
322
|
+
: '<span class="badge pending">Not generated yet</span>');
|
|
323
|
+
const indexLink = app.status === 'ready'
|
|
324
|
+
? '<a class="index-link" href="/' + app.slug + '">screen index</a>' : '';
|
|
325
|
+
let hint = '';
|
|
326
|
+
if (app.status === 'build-required') {
|
|
327
|
+
hint = '<p class="hint">Run <code>cd ' + escapeHtml(app.folder)
|
|
328
|
+
+ '/context/mockup && npm install && npm run build</code> to make this app viewable.</p>';
|
|
329
|
+
} else if (app.status === 'not-generated') {
|
|
330
|
+
hint = '<p class="hint">Run a <code>mockgen-*</code> skill for this application to generate its mockups.</p>';
|
|
331
|
+
}
|
|
332
|
+
return '<div class="card">\n'
|
|
333
|
+
+ ' <h2>' + escapeHtml(app.appName) + '</h2>'
|
|
334
|
+
+ (app.version ? '<span class="meta">' + escapeHtml(app.version) + '</span>' : '')
|
|
335
|
+
+ badge + indexLink + '\n'
|
|
336
|
+
+ (app.description ? ' <p class="sub" style="margin:6px 0 0">' + escapeHtml(app.description) + '</p>\n' : '')
|
|
337
|
+
+ ' <div class="roles">\n ' + renderRoleChips(app) + '\n </div>\n'
|
|
338
|
+
+ hint + '\n</div>';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Port input (default: the current port — auto-picked as the first unused port from 4000
|
|
342
|
+
// when nothing was configured). Apply persists to mockup.config.json via POST /api/port
|
|
343
|
+
// and the hub re-listens on the new port; the page then follows it.
|
|
344
|
+
const PORT_SCRIPT = `
|
|
345
|
+
function applyPort() {
|
|
346
|
+
var msg = document.getElementById('port-msg');
|
|
347
|
+
var port = parseInt(document.getElementById('hub-port').value, 10);
|
|
348
|
+
if (!port || port < 1024 || port > 65535) {
|
|
349
|
+
msg.style.color = '#dc2626'; msg.textContent = 'Port must be 1024-65535'; return;
|
|
350
|
+
}
|
|
351
|
+
msg.style.color = '#64748b'; msg.textContent = 'Applying\\u2026';
|
|
352
|
+
fetch('/api/port', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
353
|
+
body: JSON.stringify({ port: port }) })
|
|
354
|
+
.then(function (r) { return r.json(); })
|
|
355
|
+
.then(function (res) {
|
|
356
|
+
if (!res.ok) { msg.style.color = '#dc2626'; msg.textContent = res.error || 'Failed'; return; }
|
|
357
|
+
msg.textContent = 'Restarting on port ' + res.port + '\\u2026';
|
|
358
|
+
setTimeout(function () {
|
|
359
|
+
location.href = location.protocol + '//' + location.hostname + ':' + res.port + '/';
|
|
360
|
+
}, 700);
|
|
361
|
+
})
|
|
362
|
+
.catch(function () { msg.style.color = '#dc2626'; msg.textContent = 'Request failed'; });
|
|
363
|
+
}
|
|
364
|
+
`;
|
|
365
|
+
|
|
366
|
+
function renderLanding(apps, currentPort) {
|
|
367
|
+
return '<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n'
|
|
368
|
+
+ '<meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
|
|
369
|
+
+ '<title>Mockup Hub</title>\n<style>' + LANDING_CSS + '</style>\n'
|
|
370
|
+
+ '<script>' + PORT_SCRIPT + '</script>\n</head>\n<body>\n'
|
|
371
|
+
+ '<div class="wrap">\n<h1>Mockup Hub</h1>\n'
|
|
372
|
+
+ '<p class="sub">All application mockups, served from one place. '
|
|
373
|
+
+ apps.length + ' application(s) discovered. Click a role to open its dashboard.</p>\n'
|
|
374
|
+
+ apps.map(renderAppCard).join('\n')
|
|
375
|
+
+ '\n<p class="foot">CO2 Mockup Hub v' + HUB_VERSION
|
|
376
|
+
+ '<span class="portbox">Port <input id="hub-port" type="text" inputmode="numeric" value="'
|
|
377
|
+
+ (currentPort || '') + '"> <button type="button" onclick="applyPort()">Apply</button>'
|
|
378
|
+
+ '<span id="port-msg"></span></span></p>\n</div>\n</body>\n</html>';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function notReadyPage(app) {
|
|
382
|
+
return renderLanding([app], CURRENT_PORT);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function notFoundPage(what) {
|
|
386
|
+
return '<!DOCTYPE html><html><body style="font-family:sans-serif;padding:40px">'
|
|
387
|
+
+ '<h1>404 — Not found' + (what ? ': ' + escapeHtml(what) : '') + '</h1>'
|
|
388
|
+
+ '<p><a href="/">Back to Mockup Hub</a></p></body></html>';
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── Tailwind assembly ────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
function assemblePage(app, role, page) {
|
|
394
|
+
const dir = app.mockupDir;
|
|
395
|
+
const shell = readText(path.join(dir, 'partials', 'shell.html'));
|
|
396
|
+
let header = readText(path.join(dir, 'partials', 'header.html'));
|
|
397
|
+
const sidebar = readText(path.join(dir, 'partials', 'sidebar-' + role + '.html'));
|
|
398
|
+
const footer = readText(path.join(dir, 'partials', 'footer.html'));
|
|
399
|
+
const content = readText(path.join(dir, role, 'content', page + '.html'));
|
|
400
|
+
if (!shell || !header || !sidebar || !footer) return null;
|
|
401
|
+
header = header.replace(/\{\{ROLE\}\}/g, role).replace(/\{\{APP_SLUG\}\}/g, app.slug);
|
|
402
|
+
const body = content
|
|
403
|
+
|| '<div class="p-6 text-red-500">Page not found: ' + escapeHtml(page) + '</div>';
|
|
404
|
+
return shell
|
|
405
|
+
.replace('{{HEADER}}', header)
|
|
406
|
+
.replace('{{SIDEBAR}}', sidebar.replace(/\{\{APP_SLUG\}\}/g, app.slug))
|
|
407
|
+
.replace('{{CONTENT}}', body)
|
|
408
|
+
.replace('{{FOOTER}}', footer);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Routing ──────────────────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
function routeTailwind(app, segs, res) {
|
|
414
|
+
if (segs.length === 1) {
|
|
415
|
+
if (serveFile(res, path.join(app.mockupDir, 'MOCKUP.html'))) return;
|
|
416
|
+
const first = app.roles.find((r) => r.slug);
|
|
417
|
+
if (first) return redirect(res, '/' + app.slug + '/' + first.slug + '/home');
|
|
418
|
+
return send(res, 404, notFoundPage(app.slug));
|
|
419
|
+
}
|
|
420
|
+
if (segs[1] === 'static' || segs[1] === 'reports') {
|
|
421
|
+
const file = safeJoin(path.join(app.mockupDir, segs[1]), segs.slice(2));
|
|
422
|
+
if (serveFile(res, file)) return;
|
|
423
|
+
return send(res, 404, notFoundPage());
|
|
424
|
+
}
|
|
425
|
+
if (!SAFE_SEG.test(segs[1])) return send(res, 400, notFoundPage());
|
|
426
|
+
if (segs.length === 2) return redirect(res, '/' + app.slug + '/' + segs[1] + '/home');
|
|
427
|
+
if (segs.length === 3 && SAFE_SEG.test(segs[2])) {
|
|
428
|
+
const html = assemblePage(app, segs[1], segs[2]);
|
|
429
|
+
if (html === null) {
|
|
430
|
+
return send(res, 500, '<h1>Missing partials. Re-run the mockup generator.</h1>');
|
|
431
|
+
}
|
|
432
|
+
return send(res, 200, html);
|
|
433
|
+
}
|
|
434
|
+
return send(res, 404, notFoundPage());
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function routeShadcn(app, segs, res) {
|
|
438
|
+
if (segs.length === 1 && serveFile(res, path.join(app.mockupDir, 'MOCKUP.html'))) return;
|
|
439
|
+
if (segs[1] === 'reports') {
|
|
440
|
+
const file = safeJoin(path.join(app.mockupDir, 'public', 'reports'), segs.slice(2));
|
|
441
|
+
if (serveFile(res, file)) return;
|
|
442
|
+
return send(res, 404, notFoundPage());
|
|
443
|
+
}
|
|
444
|
+
const distDir = path.join(app.mockupDir, 'dist');
|
|
445
|
+
if (segs.length > 1) {
|
|
446
|
+
const file = safeJoin(distDir, segs.slice(1));
|
|
447
|
+
if (serveFile(res, file)) return;
|
|
448
|
+
}
|
|
449
|
+
// SPA fallback — React Router (basename /{app_slug}) handles the path client-side
|
|
450
|
+
if (serveFile(res, path.join(distDir, 'index.html'))) return;
|
|
451
|
+
return send(res, 404, notFoundPage(app.slug));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// POST /api/port — persist a new port (from the landing page's port input) and re-listen.
|
|
455
|
+
function handlePortUpdate(req, res) {
|
|
456
|
+
let body = '';
|
|
457
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
458
|
+
req.on('end', () => {
|
|
459
|
+
let port = NaN;
|
|
460
|
+
try { port = parseInt(JSON.parse(body).port, 10); } catch (err) { /* invalid JSON */ }
|
|
461
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
462
|
+
return send(res, 400, JSON.stringify({ ok: false, error: 'Port must be 1024-65535' }),
|
|
463
|
+
'application/json; charset=utf-8');
|
|
464
|
+
}
|
|
465
|
+
persistPort(port);
|
|
466
|
+
send(res, 200, JSON.stringify({ ok: true, port: port }), 'application/json; charset=utf-8');
|
|
467
|
+
if (port !== CURRENT_PORT) {
|
|
468
|
+
setTimeout(() => { server.close(() => listenOn(port, false)); }, 200);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function route(req, res) {
|
|
474
|
+
const urlPath = decodeURIComponent((req.url || '/').split('?')[0]);
|
|
475
|
+
const segs = urlPath.split('/').filter(Boolean);
|
|
476
|
+
|
|
477
|
+
if (segs.length === 0) return send(res, 200, renderLanding(discoverApps(), CURRENT_PORT));
|
|
478
|
+
if (urlPath === '/favicon.ico') { res.writeHead(204); return res.end(); }
|
|
479
|
+
|
|
480
|
+
if (segs[0] === 'api' && segs[1] === 'port' && segs.length === 2 && req.method === 'POST') {
|
|
481
|
+
return handlePortUpdate(req, res);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// HTMX fragment endpoint (tailwind): /api/content/:app/:role/:page
|
|
485
|
+
if (segs[0] === 'api' && segs[1] === 'content') {
|
|
486
|
+
if (segs.length !== 5) return send(res, 404, notFoundPage());
|
|
487
|
+
const app = findApp(segs[2]);
|
|
488
|
+
if (!app || app.status !== 'ready' || app.stack !== 'tailwind') {
|
|
489
|
+
return send(res, 404, notFoundPage(segs[2]));
|
|
490
|
+
}
|
|
491
|
+
if (!SAFE_SEG.test(segs[3]) || !SAFE_SEG.test(segs[4])) return send(res, 400, notFoundPage());
|
|
492
|
+
const content = readText(path.join(app.mockupDir, segs[3], 'content', segs[4] + '.html'));
|
|
493
|
+
if (content === null) {
|
|
494
|
+
return send(res, 404, '<div class="p-6 text-red-500">Content not found.</div>');
|
|
495
|
+
}
|
|
496
|
+
return send(res, 200, content);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!SAFE_SEG.test(segs[0])) return send(res, 400, notFoundPage());
|
|
500
|
+
const app = findApp(segs[0]);
|
|
501
|
+
if (!app) return send(res, 404, notFoundPage(segs[0]));
|
|
502
|
+
if (app.status !== 'ready') return send(res, 200, notReadyPage(app));
|
|
503
|
+
|
|
504
|
+
if (app.stack === 'shadcn') return routeShadcn(app, segs, res);
|
|
505
|
+
return routeTailwind(app, segs, res);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── Start ────────────────────────────────────────────────────────────────────
|
|
509
|
+
|
|
510
|
+
let CURRENT_PORT = 0;
|
|
511
|
+
const server = http.createServer((req, res) => {
|
|
512
|
+
try {
|
|
513
|
+
route(req, res);
|
|
514
|
+
} catch (err) {
|
|
515
|
+
send(res, 500, '<h1>500 — Hub error</h1><pre>' + escapeHtml(err && err.stack || err) + '</pre>');
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
/** True when a TCP connect to 127.0.0.1:<port> is answered (something listens there).
|
|
520
|
+
* Needed besides the bind error: Windows lets a wildcard bind coexist with another
|
|
521
|
+
* process's 127.0.0.1-specific bind, so a plain listen() would "succeed" while
|
|
522
|
+
* http://localhost:<port> still hits the other app. */
|
|
523
|
+
function tcpBusy(port, cb) {
|
|
524
|
+
const sock = net.connect({ host: '127.0.0.1', port: port, timeout: 400 });
|
|
525
|
+
sock.once('connect', function () { sock.destroy(); cb(true); });
|
|
526
|
+
sock.once('timeout', function () { sock.destroy(); cb(false); });
|
|
527
|
+
sock.once('error', function () { cb(false); });
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Listen on `port`; when `scan` is true a busy port tries the next one (bounded). The
|
|
532
|
+
* effective port is written back to mockup.config.json so probes and restarts agree.
|
|
533
|
+
* An exact (PORT env) port never scans — the caller asked for THAT port.
|
|
534
|
+
*/
|
|
535
|
+
function listenOn(port, scan) {
|
|
536
|
+
tcpBusy(port, (busy) => {
|
|
537
|
+
if (busy) {
|
|
538
|
+
if (scan && port < PORT_SCAN_START + PORT_SCAN_LIMIT) return listenOn(port + 1, true);
|
|
539
|
+
console.error(' CO2 Mockup Hub failed to listen on port ' + port + ': already in use');
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
server.removeAllListeners('error'); // drop stale handlers from prior attempts/restarts
|
|
543
|
+
server.once('error', (err) => {
|
|
544
|
+
if (scan && err && err.code === 'EADDRINUSE' && port < PORT_SCAN_START + PORT_SCAN_LIMIT) {
|
|
545
|
+
return listenOn(port + 1, true);
|
|
546
|
+
}
|
|
547
|
+
console.error(' CO2 Mockup Hub failed to listen on port ' + port + ': ' + (err && err.code || err));
|
|
548
|
+
process.exit(1);
|
|
549
|
+
});
|
|
550
|
+
// Bind the IPv4 wildcard explicitly — a bare listen(port) grabs the IPv6 wildcard on
|
|
551
|
+
// some hosts and would miss IPv4 conflicts entirely.
|
|
552
|
+
server.listen(port, '0.0.0.0', () => {
|
|
553
|
+
CURRENT_PORT = port;
|
|
554
|
+
persistPort(port);
|
|
555
|
+
console.log('');
|
|
556
|
+
console.log(' CO2 Mockup Hub v' + HUB_VERSION + ' running at:');
|
|
557
|
+
console.log(' -> http://localhost:' + port);
|
|
558
|
+
console.log('');
|
|
559
|
+
console.log(' Serving mockups for all applications under: ' + ROOT_DIR);
|
|
560
|
+
console.log(' Change port: the landing page port input, PORT env var, or "port" in mockup.config.json');
|
|
561
|
+
console.log('');
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const resolved = resolvePort();
|
|
567
|
+
// env = exact (fail fast when busy); config/auto = scan upward from the resolved port.
|
|
568
|
+
listenOn(resolved.port, resolved.source !== 'env');
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
## package.json Template
|
|
572
|
+
|
|
573
|
+
```json
|
|
574
|
+
{
|
|
575
|
+
"name": "co2-mockup-hub",
|
|
576
|
+
"private": true,
|
|
577
|
+
"version": "1.0.0",
|
|
578
|
+
"description": "CO2 Mockup Hub — single server for all application mockups",
|
|
579
|
+
"main": "server.js",
|
|
580
|
+
"scripts": {
|
|
581
|
+
"start": "node server.js"
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
## mockup.config.json Template
|
|
587
|
+
|
|
588
|
+
Create only if missing — never overwrite (it stores the user's port choice). When
|
|
589
|
+
creating, set `port` to the first unused port starting from 4000 (write `4000` if you
|
|
590
|
+
cannot check — the server scans upward from a busy config port and writes the effective
|
|
591
|
+
port back).
|
|
592
|
+
|
|
593
|
+
```json
|
|
594
|
+
{
|
|
595
|
+
"port": 4000
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
Port precedence at startup: `PORT` env var (e.g. `PORT=4100 npm start` — exact, fails
|
|
600
|
+
fast when busy; Compound Context Studio's Start Hub passes the port this way) →
|
|
601
|
+
`mockup.config.json` `port` → first unused port starting from `4000`. The landing page's
|
|
602
|
+
port input (POST `/api/port`) persists a new port and re-listens on it.
|
|
603
|
+
|
|
604
|
+
## .gitignore Template
|
|
605
|
+
|
|
606
|
+
Everything npm- or machine-generated inside `<root>/mockup/` stays inside `<root>/mockup/`
|
|
607
|
+
and is gitignored there. The hub has zero dependencies, so `node_modules/` normally never
|
|
608
|
+
exists — the rule is a safety net in case a user installs packages. `mockup.config.json`
|
|
609
|
+
is per-machine (each machine keeps its own port choice; the skills recreate it if missing),
|
|
610
|
+
so it is gitignored too.
|
|
611
|
+
|
|
612
|
+
```
|
|
613
|
+
node_modules/
|
|
614
|
+
package-lock.json
|
|
615
|
+
mockup.config.json
|
|
616
|
+
.DS_Store
|
|
617
|
+
*.log
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
## Quick Start (for output summaries)
|
|
621
|
+
|
|
622
|
+
```
|
|
623
|
+
Mockup Hub
|
|
624
|
+
==========
|
|
625
|
+
1. cd <root>/mockup
|
|
626
|
+
2. npm start (zero dependencies — no npm install required)
|
|
627
|
+
3. Open http://localhost:4000 in your browser
|
|
628
|
+
(the hub picks the first unused port from 4000; the exact URL is printed on start.
|
|
629
|
+
Change port: the landing page's Port input, PORT=4100 npm start, or edit
|
|
630
|
+
mockup.config.json — or use Compound Context Studio's Mockup page Start/Stop Hub.)
|
|
631
|
+
```
|