@rashidee/co2 1.3.10 → 1.3.12

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 (74) hide show
  1. package/dist/.co2-dat/app.db +0 -0
  2. package/dist/.co2-dat/app.db-shm +0 -0
  3. package/dist/.co2-dat/app.db-wal +0 -0
  4. package/dist/index.js +408 -73
  5. package/package.json +41 -41
  6. package/plugin/skills/mockgen-shadcn/SKILL.md +11 -7
  7. package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +14 -3
  8. package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +98 -40
  9. package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +98 -40
  10. package/plugin/skills/util-gencicdscript/SKILL.md +34 -8
  11. package/plugin/skills/util-gencicdscript/references/cicd-app-template.md +798 -689
  12. package/plugin/skills/util-plancicd/SKILL.md +63 -2
  13. package/static/assets/{abnfDiagram-VRR7QNED-ChoWkBm-.js → abnfDiagram-VRR7QNED-Qm0Wk_H_.js} +1 -1
  14. package/static/assets/{arc-BOiutWhq.js → arc-Cfk6Kt0D.js} +1 -1
  15. package/static/assets/{architectureDiagram-ZJ3FMSHR-Dn4YKYfM.js → architectureDiagram-ZJ3FMSHR-DQTlZaUK.js} +1 -1
  16. package/static/assets/{blockDiagram-677ZJIJ3-BrXEfs29.js → blockDiagram-677ZJIJ3-D-4ibdoX.js} +1 -1
  17. package/static/assets/{c4Diagram-LMCZKHZV-ChaIzrlx.js → c4Diagram-LMCZKHZV-CHuqcdJm.js} +1 -1
  18. package/static/assets/channel-H9DRrduO.js +1 -0
  19. package/static/assets/{chunk-2Q5K7J3B-BOlnmyI2.js → chunk-2Q5K7J3B-MB9pWQe1.js} +1 -1
  20. package/static/assets/{chunk-32BRIVSS-BYtO1gsq.js → chunk-32BRIVSS-BttYlwU1.js} +1 -1
  21. package/static/assets/{chunk-5VM5RSS4-Cmx4Morj.js → chunk-5VM5RSS4-oyt0Tagd.js} +1 -1
  22. package/static/assets/{chunk-EX3LRPZG-BcgUofmA.js → chunk-EX3LRPZG-DaTxdTWl.js} +1 -1
  23. package/static/assets/{chunk-JWPE2WC7-CnNRXdvN.js → chunk-JWPE2WC7-CNgDxPAD.js} +1 -1
  24. package/static/assets/{chunk-MOJQB5TN-DGG6Q3Wn.js → chunk-MOJQB5TN-CoAudO3B.js} +1 -1
  25. package/static/assets/{chunk-RYQCIY6F-C8erKHls.js → chunk-RYQCIY6F-BrkDEEha.js} +1 -1
  26. package/static/assets/{chunk-V7JOEXUC-BNkIJqzu.js → chunk-V7JOEXUC-D14rGvBO.js} +1 -1
  27. package/static/assets/{chunk-VR4S4FIN-CGq-RseS.js → chunk-VR4S4FIN-CAtas2vF.js} +1 -1
  28. package/static/assets/{chunk-XXDRQBXY-DOVfEC7j.js → chunk-XXDRQBXY-DcVAnNUQ.js} +1 -1
  29. package/static/assets/classDiagram-OUVF2IWQ-DRNPM383.js +1 -0
  30. package/static/assets/classDiagram-v2-EOCWNBFH-DRNPM383.js +1 -0
  31. package/static/assets/{cose-bilkent-JH36ORCC-BrK3gCzK.js → cose-bilkent-JH36ORCC-B7y4MI6Z.js} +1 -1
  32. package/static/assets/{cynefin-VYW2F7L2-BTMYzKLg.js → cynefin-VYW2F7L2-D2mnk-iJ.js} +1 -1
  33. package/static/assets/{cynefinDiagram-TSTJHNR4-Df4k0_Hb.js → cynefinDiagram-TSTJHNR4-DNzQJhG7.js} +1 -1
  34. package/static/assets/{dagre-VKFMJZFB-0LnYnEqq.js → dagre-VKFMJZFB-NMSyTo8N.js} +1 -1
  35. package/static/assets/{diagram-FQU43EPY-B5nIVD5g.js → diagram-FQU43EPY-CdqS5Ike.js} +1 -1
  36. package/static/assets/{diagram-G47NLZAW-B2YLBzdb.js → diagram-G47NLZAW-Bsb4T5JI.js} +1 -1
  37. package/static/assets/{diagram-NH7WQ7WH-DPVumXIF.js → diagram-NH7WQ7WH-Z-PyzBSC.js} +1 -1
  38. package/static/assets/{diagram-OA4YK3LP-Da20vcLJ.js → diagram-OA4YK3LP-BZBTYbxg.js} +1 -1
  39. package/static/assets/{diagram-WEI45ONY-DQbTpuRZ.js → diagram-WEI45ONY-BL2LA5H_.js} +1 -1
  40. package/static/assets/{ebnfDiagram-CCIWWBDH-BEn1vEyb.js → ebnfDiagram-CCIWWBDH-BGtukMeo.js} +1 -1
  41. package/static/assets/{erDiagram-Q63AITRT-Ccg0DVBG.js → erDiagram-Q63AITRT-B6OJ4YCl.js} +1 -1
  42. package/static/assets/{flowDiagram-23GEKE2U-B2dMjEpa.js → flowDiagram-23GEKE2U-BBkMgJ1i.js} +1 -1
  43. package/static/assets/{ganttDiagram-NO4QXBWP-Kp1UsG59.js → ganttDiagram-NO4QXBWP-Cv8KHFBD.js} +1 -1
  44. package/static/assets/{gitGraphDiagram-IHSO6WYX-D6RB8sPf.js → gitGraphDiagram-IHSO6WYX-hYxDNjWx.js} +1 -1
  45. package/static/assets/{index-CM4GfOLV.css → index-DKI3BgkF.css} +1 -1
  46. package/static/assets/{index-GEUFVo96.js → index-DnYGyYET.js} +118 -120
  47. package/static/assets/{infoDiagram-FWYZ7A6U-C4ILACOF.js → infoDiagram-FWYZ7A6U-WuQIiedv.js} +1 -1
  48. package/static/assets/{ishikawaDiagram-FXEZZL3T-Q2Ef9fW7.js → ishikawaDiagram-FXEZZL3T-BJGOD-IY.js} +1 -1
  49. package/static/assets/{journeyDiagram-5HDEW3XC-B7ZV1i0L.js → journeyDiagram-5HDEW3XC-Ba_NXseS.js} +1 -1
  50. package/static/assets/{kanban-definition-HUTT4EX6-CjaPV_vw.js → kanban-definition-HUTT4EX6-C5RMm9uF.js} +1 -1
  51. package/static/assets/{linear-BELpVz92.js → linear-CLKppNoj.js} +1 -1
  52. package/static/assets/{mindmap-definition-LN4V7U3C-COhl-GrE.js → mindmap-definition-LN4V7U3C-ClSG_qmQ.js} +1 -1
  53. package/static/assets/{pegDiagram-2B236MQR-3FlTtWva.js → pegDiagram-2B236MQR-C_JEZqk3.js} +1 -1
  54. package/static/assets/{pieDiagram-ENE6RG2P-CLeOh_sh.js → pieDiagram-ENE6RG2P-D0lx7wDi.js} +1 -1
  55. package/static/assets/{quadrantDiagram-ABIIQ3AL-D2MnobFS.js → quadrantDiagram-ABIIQ3AL-BBKy0PVs.js} +1 -1
  56. package/static/assets/{railroadDiagram-RFXS5EU6-FjtL4fEj.js → railroadDiagram-RFXS5EU6-C1Badu5q.js} +1 -1
  57. package/static/assets/{requirementDiagram-TGXJPOKE-C5Y2uWRV.js → requirementDiagram-TGXJPOKE-1j59jBQH.js} +1 -1
  58. package/static/assets/{sankeyDiagram-HTMAVEWB-ITj9bISn.js → sankeyDiagram-HTMAVEWB-BLR5S8aZ.js} +1 -1
  59. package/static/assets/{sequenceDiagram-DBY2YBRQ-Ct2xTSbI.js → sequenceDiagram-DBY2YBRQ-BlqdK4qm.js} +1 -1
  60. package/static/assets/{sizeCapture-X5ZJPWSS-w9hGRpov.js → sizeCapture-X5ZJPWSS-C27ndGIT.js} +1 -1
  61. package/static/assets/{stateDiagram-2N3HPSRC-BlxDF1qr.js → stateDiagram-2N3HPSRC-DOu4536_.js} +1 -1
  62. package/static/assets/stateDiagram-v2-6OUMAXLB-DvP1ZCp8.js +1 -0
  63. package/static/assets/{swimlanes-5IMT3BWC-DFYm_SCW.js → swimlanes-5IMT3BWC-BpGitR9A.js} +2 -2
  64. package/static/assets/swimlanesDiagram-G3AALYLV-B5cf18Ey.js +8 -0
  65. package/static/assets/{timeline-definition-FHXFAJF6-johIMUxt.js → timeline-definition-FHXFAJF6-JIvdjH7z.js} +1 -1
  66. package/static/assets/{vennDiagram-L72KCM5P-BFk2-H5y.js → vennDiagram-L72KCM5P-DulnqLWm.js} +1 -1
  67. package/static/assets/{wardleyDiagram-EHGQE667-T7509MWq.js → wardleyDiagram-EHGQE667--MhZNL8M.js} +1 -1
  68. package/static/assets/{xychartDiagram-FW5EYKEG-DskgQ7uq.js → xychartDiagram-FW5EYKEG-CjnNgnMQ.js} +1 -1
  69. package/static/index.html +2 -2
  70. package/static/assets/channel-CKovdojM.js +0 -1
  71. package/static/assets/classDiagram-OUVF2IWQ-BRxqfb0_.js +0 -1
  72. package/static/assets/classDiagram-v2-EOCWNBFH-BRxqfb0_.js +0 -1
  73. package/static/assets/stateDiagram-v2-6OUMAXLB-CQVgnOlz.js +0 -1
  74. package/static/assets/swimlanesDiagram-G3AALYLV-DubMPSZe.js +0 -8
@@ -19,6 +19,13 @@ they contain NO server of their own.
19
19
  `mockup.config.json` and re-listens on the new port. Compound Context Studio's Mockup
20
20
  page reads its own topbar port input and passes it via the `PORT` env when its
21
21
  **Start Hub** button spawns this server.
22
+ - **Reverse-proxy aware** (v3): Compound Context Studio embeds the hub through its own
23
+ same-origin proxy (`/api/artifacts/mockup-proxy/<projectId>/`), which sends the mount
24
+ path in an `X-Forwarded-Prefix` header. The hub prefixes every redirect and rewrites
25
+ root-absolute URL attributes (`href`/`src`/`action`/`hx-*`) in the HTML it serves —
26
+ including per-app assets generated before v3 — so mockups work behind the studio (or
27
+ any prefix-forwarding reverse proxy) without exposing the hub's port separately.
28
+ Requests without the header behave exactly as before.
22
29
 
23
30
  The hub has **zero npm dependencies** — `npm start` (or `node server.js`) works with no
24
31
  `npm install`.
@@ -97,12 +104,15 @@ Example:
97
104
  #!/usr/bin/env node
98
105
  /*
99
106
  * CO2 Mockup Hub — one server for ALL application mockups.
100
- * HUB_VERSION: 2 (marker used by mockgen-* skills to decide upgrades — do not remove)
107
+ * HUB_VERSION: 3 (marker used by mockgen-* skills to decide upgrades — do not remove)
101
108
  *
102
109
  * Discovers every root-level application folder containing context/PRD.md.
103
110
  * Apps with a context/mockup/mockup-manifest.json are routable:
104
111
  * - stack "tailwind": pages assembled from partials + content fragments
105
112
  * - stack "shadcn": served from the built dist/ folder (SPA fallback)
113
+ * Reverse proxy: when a request carries X-Forwarded-Prefix (Compound Context Studio's
114
+ * same-origin proxy sends it), redirects are prefixed and root-absolute URL
115
+ * attributes in served HTML are rewritten under the prefix.
106
116
  * Port: PORT env var > mockup.config.json "port" > first unused port from 4000.
107
117
  * - A PORT env value is exact (Compound Context Studio's Start Hub passes it) — a busy
108
118
  * port fails fast so the caller sees the error.
@@ -118,10 +128,11 @@ const net = require('net');
118
128
  const fs = require('fs');
119
129
  const path = require('path');
120
130
 
121
- const HUB_VERSION = 2;
131
+ const HUB_VERSION = 3;
122
132
  const HUB_DIR = __dirname;
123
133
  const ROOT_DIR = path.resolve(HUB_DIR, '..');
124
134
  const SAFE_SEG = /^[A-Za-z0-9._-]+$/;
135
+ const SAFE_PREFIX = /^\/[A-Za-z0-9/_-]*$/; // root-relative, no dots/query/fragment
125
136
  const PORT_SCAN_START = 4000; // default: first unused port starting here
126
137
  const PORT_SCAN_LIMIT = 100;
127
138
 
@@ -229,11 +240,39 @@ function findApp(slug) {
229
240
 
230
241
  // ── Response helpers ─────────────────────────────────────────────────────────
231
242
 
243
+ /** The sanitized X-Forwarded-Prefix ('' when absent/unsafe), without trailing slash. */
244
+ function requestPrefix(req) {
245
+ var raw = String(req.headers['x-forwarded-prefix'] || '');
246
+ if (raw.endsWith('/')) raw = raw.slice(0, -1);
247
+ return SAFE_PREFIX.test(raw) ? raw : '';
248
+ }
249
+
250
+ /**
251
+ * Rewrite root-absolute URL attributes so they stay under the proxy prefix. Covers hub
252
+ * pages AND per-app assets (tailwind partials/content with hx-* attributes, shadcn dist
253
+ * index.html, MOCKUP.html, reports) — including assets generated before hub v3.
254
+ */
255
+ function applyPrefix(html, prefix) {
256
+ if (!prefix) return html;
257
+ return html.replace(
258
+ /(href|src|action|hx-get|hx-post|hx-put|hx-patch|hx-delete)="\//g,
259
+ '$1="' + prefix + '/'
260
+ );
261
+ }
262
+
232
263
  function send(res, code, body, type) {
233
264
  res.writeHead(code, { 'Content-Type': type || 'text/html; charset=utf-8' });
234
265
  res.end(body);
235
266
  }
236
267
 
268
+ /** All HTML goes out through this so root-absolute links are prefix-rewritten. The
269
+ * X-Hub-Prefix header tells the studio proxy the hub already rewrote its HTML, so the
270
+ * proxy skips its own legacy-hub rewrite pass (no double-prefixing). */
271
+ function sendHtml(res, code, html, prefix) {
272
+ if (prefix) res.setHeader('X-Hub-Prefix', prefix);
273
+ send(res, code, applyPrefix(html, prefix), 'text/html; charset=utf-8');
274
+ }
275
+
237
276
  function redirect(res, location) {
238
277
  res.writeHead(302, { Location: location });
239
278
  res.end();
@@ -252,12 +291,18 @@ function safeJoin(baseDir, segments) {
252
291
  return target;
253
292
  }
254
293
 
255
- function serveFile(res, filePath) {
294
+ function serveFile(res, filePath, prefix) {
256
295
  if (!filePath) return false;
257
296
  let stat;
258
297
  try { stat = fs.statSync(filePath); } catch (err) { return false; }
259
298
  if (!stat.isFile()) return false;
260
- const type = MIME[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
299
+ const ext = path.extname(filePath).toLowerCase();
300
+ const type = MIME[ext] || 'application/octet-stream';
301
+ // HTML files (MOCKUP.html, shadcn dist index.html, reports) get prefix-rewritten.
302
+ if (ext === '.html' && prefix) {
303
+ sendHtml(res, 200, fs.readFileSync(filePath, 'utf8'), prefix);
304
+ return true;
305
+ }
261
306
  send(res, 200, fs.readFileSync(filePath), type);
262
307
  return true;
263
308
  }
@@ -340,7 +385,9 @@ function renderAppCard(app) {
340
385
 
341
386
  // Port input (default: the current port — auto-picked as the first unused port from 4000
342
387
  // 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.
388
+ // and the hub re-listens on the new port; the page then follows it. Behind a proxy
389
+ // (HUB_PREFIX set) the page reloads the prefix root instead — the studio proxy reads
390
+ // the new port from mockup.config.json, so the same URL keeps working.
344
391
  const PORT_SCRIPT = `
345
392
  function applyPort() {
346
393
  var msg = document.getElementById('port-msg');
@@ -349,25 +396,28 @@ const PORT_SCRIPT = `
349
396
  msg.style.color = '#dc2626'; msg.textContent = 'Port must be 1024-65535'; return;
350
397
  }
351
398
  msg.style.color = '#64748b'; msg.textContent = 'Applying\\u2026';
352
- fetch('/api/port', { method: 'POST', headers: { 'Content-Type': 'application/json' },
399
+ fetch(HUB_PREFIX + '/api/port', { method: 'POST', headers: { 'Content-Type': 'application/json' },
353
400
  body: JSON.stringify({ port: port }) })
354
401
  .then(function (r) { return r.json(); })
355
402
  .then(function (res) {
356
403
  if (!res.ok) { msg.style.color = '#dc2626'; msg.textContent = res.error || 'Failed'; return; }
357
404
  msg.textContent = 'Restarting on port ' + res.port + '\\u2026';
358
405
  setTimeout(function () {
359
- location.href = location.protocol + '//' + location.hostname + ':' + res.port + '/';
406
+ location.href = HUB_PREFIX
407
+ ? HUB_PREFIX + '/'
408
+ : location.protocol + '//' + location.hostname + ':' + res.port + '/';
360
409
  }, 700);
361
410
  })
362
411
  .catch(function () { msg.style.color = '#dc2626'; msg.textContent = 'Request failed'; });
363
412
  }
364
413
  `;
365
414
 
366
- function renderLanding(apps, currentPort) {
415
+ function renderLanding(apps, currentPort, prefix) {
367
416
  return '<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n'
368
417
  + '<meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
369
418
  + '<title>Mockup Hub</title>\n<style>' + LANDING_CSS + '</style>\n'
370
- + '<script>' + PORT_SCRIPT + '</script>\n</head>\n<body>\n'
419
+ // prefix is SAFE_PREFIX-sanitized, so it embeds safely in the inline script.
420
+ + '<script>var HUB_PREFIX = "' + (prefix || '') + '";' + PORT_SCRIPT + '</script>\n</head>\n<body>\n'
371
421
  + '<div class="wrap">\n<h1>Mockup Hub</h1>\n'
372
422
  + '<p class="sub">All application mockups, served from one place. '
373
423
  + apps.length + ' application(s) discovered. Click a role to open its dashboard.</p>\n'
@@ -378,8 +428,8 @@ function renderLanding(apps, currentPort) {
378
428
  + '<span id="port-msg"></span></span></p>\n</div>\n</body>\n</html>';
379
429
  }
380
430
 
381
- function notReadyPage(app) {
382
- return renderLanding([app], CURRENT_PORT);
431
+ function notReadyPage(app, prefix) {
432
+ return renderLanding([app], CURRENT_PORT, prefix);
383
433
  }
384
434
 
385
435
  function notFoundPage(what) {
@@ -410,45 +460,46 @@ function assemblePage(app, role, page) {
410
460
 
411
461
  // ── Routing ──────────────────────────────────────────────────────────────────
412
462
 
413
- function routeTailwind(app, segs, res) {
463
+ function routeTailwind(app, segs, res, prefix) {
414
464
  if (segs.length === 1) {
415
- if (serveFile(res, path.join(app.mockupDir, 'MOCKUP.html'))) return;
465
+ if (serveFile(res, path.join(app.mockupDir, 'MOCKUP.html'), prefix)) return;
416
466
  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));
467
+ if (first) return redirect(res, prefix + '/' + app.slug + '/' + first.slug + '/home');
468
+ return sendHtml(res, 404, notFoundPage(app.slug), prefix);
419
469
  }
420
470
  if (segs[1] === 'static' || segs[1] === 'reports') {
421
471
  const file = safeJoin(path.join(app.mockupDir, segs[1]), segs.slice(2));
422
- if (serveFile(res, file)) return;
423
- return send(res, 404, notFoundPage());
472
+ if (serveFile(res, file, prefix)) return;
473
+ return sendHtml(res, 404, notFoundPage(), prefix);
424
474
  }
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');
475
+ if (!SAFE_SEG.test(segs[1])) return sendHtml(res, 400, notFoundPage(), prefix);
476
+ if (segs.length === 2) return redirect(res, prefix + '/' + app.slug + '/' + segs[1] + '/home');
427
477
  if (segs.length === 3 && SAFE_SEG.test(segs[2])) {
428
478
  const html = assemblePage(app, segs[1], segs[2]);
429
479
  if (html === null) {
430
480
  return send(res, 500, '<h1>Missing partials. Re-run the mockup generator.</h1>');
431
481
  }
432
- return send(res, 200, html);
482
+ return sendHtml(res, 200, html, prefix);
433
483
  }
434
- return send(res, 404, notFoundPage());
484
+ return sendHtml(res, 404, notFoundPage(), prefix);
435
485
  }
436
486
 
437
- function routeShadcn(app, segs, res) {
438
- if (segs.length === 1 && serveFile(res, path.join(app.mockupDir, 'MOCKUP.html'))) return;
487
+ function routeShadcn(app, segs, res, prefix) {
488
+ if (segs.length === 1 && serveFile(res, path.join(app.mockupDir, 'MOCKUP.html'), prefix)) return;
439
489
  if (segs[1] === 'reports') {
440
490
  const file = safeJoin(path.join(app.mockupDir, 'public', 'reports'), segs.slice(2));
441
- if (serveFile(res, file)) return;
442
- return send(res, 404, notFoundPage());
491
+ if (serveFile(res, file, prefix)) return;
492
+ return sendHtml(res, 404, notFoundPage(), prefix);
443
493
  }
444
494
  const distDir = path.join(app.mockupDir, 'dist');
445
495
  if (segs.length > 1) {
446
496
  const file = safeJoin(distDir, segs.slice(1));
447
- if (serveFile(res, file)) return;
497
+ if (serveFile(res, file, prefix)) return;
448
498
  }
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));
499
+ // SPA fallback — React Router handles the path client-side (the app derives its
500
+ // basename from the current URL, so it also works under a proxy prefix)
501
+ if (serveFile(res, path.join(distDir, 'index.html'), prefix)) return;
502
+ return sendHtml(res, 404, notFoundPage(app.slug), prefix);
452
503
  }
453
504
 
454
505
  // POST /api/port — persist a new port (from the landing page's port input) and re-listen.
@@ -473,8 +524,11 @@ function handlePortUpdate(req, res) {
473
524
  function route(req, res) {
474
525
  const urlPath = decodeURIComponent((req.url || '/').split('?')[0]);
475
526
  const segs = urlPath.split('/').filter(Boolean);
527
+ const prefix = requestPrefix(req); // '' unless behind a prefix-forwarding proxy
476
528
 
477
- if (segs.length === 0) return send(res, 200, renderLanding(discoverApps(), CURRENT_PORT));
529
+ if (segs.length === 0) {
530
+ return sendHtml(res, 200, renderLanding(discoverApps(), CURRENT_PORT, prefix), prefix);
531
+ }
478
532
  if (urlPath === '/favicon.ico') { res.writeHead(204); return res.end(); }
479
533
 
480
534
  if (segs[0] === 'api' && segs[1] === 'port' && segs.length === 2 && req.method === 'POST') {
@@ -483,26 +537,28 @@ function route(req, res) {
483
537
 
484
538
  // HTMX fragment endpoint (tailwind): /api/content/:app/:role/:page
485
539
  if (segs[0] === 'api' && segs[1] === 'content') {
486
- if (segs.length !== 5) return send(res, 404, notFoundPage());
540
+ if (segs.length !== 5) return sendHtml(res, 404, notFoundPage(), prefix);
487
541
  const app = findApp(segs[2]);
488
542
  if (!app || app.status !== 'ready' || app.stack !== 'tailwind') {
489
- return send(res, 404, notFoundPage(segs[2]));
543
+ return sendHtml(res, 404, notFoundPage(segs[2]), prefix);
544
+ }
545
+ if (!SAFE_SEG.test(segs[3]) || !SAFE_SEG.test(segs[4])) {
546
+ return sendHtml(res, 400, notFoundPage(), prefix);
490
547
  }
491
- if (!SAFE_SEG.test(segs[3]) || !SAFE_SEG.test(segs[4])) return send(res, 400, notFoundPage());
492
548
  const content = readText(path.join(app.mockupDir, segs[3], 'content', segs[4] + '.html'));
493
549
  if (content === null) {
494
550
  return send(res, 404, '<div class="p-6 text-red-500">Content not found.</div>');
495
551
  }
496
- return send(res, 200, content);
552
+ return sendHtml(res, 200, content, prefix);
497
553
  }
498
554
 
499
- if (!SAFE_SEG.test(segs[0])) return send(res, 400, notFoundPage());
555
+ if (!SAFE_SEG.test(segs[0])) return sendHtml(res, 400, notFoundPage(), prefix);
500
556
  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));
557
+ if (!app) return sendHtml(res, 404, notFoundPage(segs[0]), prefix);
558
+ if (app.status !== 'ready') return sendHtml(res, 200, notReadyPage(app, prefix), prefix);
503
559
 
504
- if (app.stack === 'shadcn') return routeShadcn(app, segs, res);
505
- return routeTailwind(app, segs, res);
560
+ if (app.stack === 'shadcn') return routeShadcn(app, segs, res, prefix);
561
+ return routeTailwind(app, segs, res, prefix);
506
562
  }
507
563
 
508
564
  // ── Start ────────────────────────────────────────────────────────────────────
@@ -627,5 +683,7 @@ Mockup Hub
627
683
  3. Open http://localhost:4000 in your browser
628
684
  (the hub picks the first unused port from 4000; the exact URL is printed on start.
629
685
  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.)
686
+ mockup.config.json — or use Compound Context Studio's Mockup page Start/Stop Hub.
687
+ Inside the studio the hub is embedded through its same-origin proxy — no need to
688
+ expose this port when the studio itself is reachable.)
631
689
  ```
@@ -21,8 +21,12 @@ description: >
21
21
  copy, docker run or kubectl apply, or none). Controls are disabled for any application that
22
22
  has never completed a conductor-develop workflow run (checked against the studio's
23
23
  /api/cicd-guard endpoint using the spawn-env token). Health/port probing marks apps already
24
- running — even ones started outside this app. No version selector: only the latest built
25
- applications are operated. When the approved plan declares an `## Infrastructure` list, the
24
+ running — even ones started outside this app. The top bar carries a version selector (fed by
25
+ the studio guard's versions, latest preselected) whose choice replaces the {VERSION} token in
26
+ commands — docker images are tagged <repo>/<app>:<version> when the plan's Docker section is
27
+ active (managed Dockerfiles materialized before Build when missing; Build never pushes) — and
28
+ Build All / Start All / Stop All buttons operating every eligible application at once
29
+ (sequential builds). When the approved plan declares an `## Infrastructure` list, the
26
30
  main page shows an "Infrastructure" link that opens a separate status-only page listing every
27
31
  supporting 3rd-party service with a live TCP-reachability indicator and a console link. Writes cicd/CICD_GENERATION.md with `**Status**:` IN PROGRESS →
28
32
  COMPLETED so the studio watcher detects completion. Idempotent — re-running regenerates
@@ -62,9 +66,17 @@ Write `cicd/CICD_GENERATION.md`:
62
66
  Read `references/cicd-app-template.md` for the complete file templates. Produce:
63
67
 
64
68
  1. `cicd/app/cicd.config.json` — from the plan's tables:
65
- `{ "port": <CI/CD app port>, "apps": [{ "name", "folder", "method", "port", "build",
66
- "start", "stop", "deployMethod", "deploy", "health", "configs" }], "infra": [{ "id", "name",
67
- "host", "port", "url", "description" }] }` — `deployMethod` is the plan's Deploy Method column
69
+ `{ "port": <CI/CD app port>, "docker": { "registry" } | null, "apps": [{ "name", "folder",
70
+ "method", "port", "build", "start", "stop", "deployMethod", "deploy", "health", "image",
71
+ "dockerfile", "configs" }], "infra": [{ "id", "name",
72
+ "host", "port", "url", "description" }] }` — `docker.registry` comes from the plan's
73
+ `## Docker` section (`null` when the plan says not dockerized); per-app `image` is that
74
+ section's Image column (`null` for non-dockerized apps) and `dockerfile` is
75
+ `{ "path": "Dockerfile", "content": "<body>" }` for a `managed` Dockerfile from the plan's
76
+ `## Dockerfiles` section, or `{ "path": "Dockerfile", "content": null }` for an `existing`
77
+ one (`null` entirely when the app has no image). Copy managed Dockerfile bodies verbatim.
78
+ Commands may carry the literal `{VERSION}` token — copy it verbatim; the generated app
79
+ substitutes it at execution. `deployMethod` is the plan's Deploy Method column
68
80
  (`none|web-server|docker|kubernetes`) and `deploy` its command (`null` when the method is
69
81
  `none`). The `infra` array comes from the plan's `## Infrastructure` table (one entry per
70
82
  row); `url` is `null` when the Console URL cell is `-`. Emit `"infra": []` when the plan's
@@ -104,11 +116,25 @@ port, application count) and STOP — the studio starts/embeds the app.
104
116
  3rd-party services, each with a live TCP-reachability dot (polled) and a console link when a
105
117
  `url` is set. Infrastructure is never built/started/stopped/deployed (no lifecycle controls,
106
118
  no develop gate); the link is hidden when `infra` is empty.
107
- - Build/Start/Stop/Deploy run the plan's commands verbatim, each with the APPLICATION FOLDER
119
+ - Build/Start/Stop/Deploy run the plan's commands, each with the APPLICATION FOLDER
108
120
  (`<project>/<folder>`) as working directory — command paths are relative to the application
109
121
  folder (e.g. `java -jar target\app.jar`), never to the project root. Deploy renders only
110
- when the plan assigned a deploy method other than `none`. No version selector — only the
111
- latest built applications are operated.
122
+ when the plan assigned a deploy method other than `none`.
123
+ - **Version selector (topbar)**: the page's top bar carries a version dropdown fed by the
124
+ studio guard response's `versions` array (already polled every 30 s; latest first —
125
+ preselected). When the studio is unreachable the selector holds a single `latest` entry.
126
+ The selected semver, minus its `v` prefix, replaces every literal `{VERSION}` in
127
+ build/start/stop/deploy commands at execution time (fallback `latest`). Tag-only
128
+ semantics: the code built is always the working tree — the selector never checks out code.
129
+ - **Dockerfile materialization**: before Build, when an app's `dockerfile.content` is set and
130
+ no file exists at its path in the application folder, the server writes it (never
131
+ overwriting an existing Dockerfile — mirror of the managed-config rule). Build never
132
+ pushes an image; pushing belongs to a `docker` deploy command.
133
+ - **All-apps controls**: the top bar carries **Build All**, **Start All** and **Stop All**
134
+ buttons. Build All builds eligible apps SEQUENTIALLY (one at a time — honest logs, no CPU
135
+ storm); Start All starts every eligible stopped app; Stop All stops every running app.
136
+ Eligible = develop-gate not failed, not busy, and (for build/start) not already running;
137
+ ineligible apps are skipped silently — per-tab badges show progress.
112
138
  - Develop gate: controls stay disabled for an application that never completed a
113
139
  conductor-develop run — checked via `${CO2_STUDIO_URL}/api/cicd-guard/${CO2_PROJECT_ID}`
114
140
  with the CO2_GUARD_TOKEN from the spawn env (refreshed every 30 s, last answer cached).