@rashidee/co2 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +25449 -0
- package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
- package/drizzle/0001_wise_wolf_cub.sql +13 -0
- package/drizzle/0002_secret_spyke.sql +16 -0
- package/drizzle/0003_workspace_config_singleton.sql +16 -0
- package/drizzle/0004_terminal_claim_v120.sql +11 -0
- package/drizzle/0005_brainstorm_v124.sql +10 -0
- package/drizzle/0006_project_v140.sql +6 -0
- package/drizzle/0007_prd_wizard_v124.sql +14 -0
- package/drizzle/0008_workflow_artifact_v126.sql +24 -0
- package/drizzle/0009_project_import_v150.sql +7 -0
- package/drizzle/0010_quality_v151.sql +3 -0
- package/drizzle/meta/0000_snapshot.json +1335 -0
- package/drizzle/meta/0001_snapshot.json +1416 -0
- package/drizzle/meta/0002_snapshot.json +1510 -0
- package/drizzle/meta/_journal.json +83 -0
- package/package.json +41 -0
- package/plugin/.claude-plugin/marketplace.json +36 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/README.md +625 -0
- package/plugin/SKILLS.md +350 -0
- package/plugin/co2.svg +5 -0
- package/plugin/commands/brainstorm-loop.md +129 -0
- package/plugin/skills/conductor-defect/SKILL.md +898 -0
- package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
- package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
- package/plugin/skills/depgen-k8s/SKILL.md +468 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
- package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
- package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
- package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
- package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
- package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
- package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
- package/plugin/skills/modelgen-relational/SKILL.md +707 -0
- package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
- package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
- package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
- package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
- package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
- package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
- package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
- package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
- package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
- package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
- package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
- package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
- package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
- package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
- package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
- package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
- package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
- package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
- package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
- package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
- package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
- package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
- package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
- package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
- package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
- package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
- package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
- package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
- package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
- package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
- package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
- package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
- package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
- package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
- package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
- package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
- package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
- package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
- package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
- package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
- package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
- package/plugin/skills/testgen-functional/SKILL.md +1381 -0
- package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
- package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
- package/plugin/skills/util-projectinit/SKILL.md +307 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
- package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
- package/plugin/skills/util-projectsync/SKILL.md +501 -0
- package/plugin/skills/util-updprd/SKILL.md +324 -0
- package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
- package/plugin/skills/util-ustagger/SKILL.md +154 -0
- package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
- package/static/assets/arc-DRKw7sjs.js +1 -0
- package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
- package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
- package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
- package/static/assets/channel-C2nVbuwv.js +1 -0
- package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
- package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
- package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
- package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
- package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
- package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
- package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
- package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
- package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
- package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
- package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
- package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
- package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
- package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
- package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
- package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
- package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
- package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
- package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
- package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
- package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
- package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
- package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
- package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
- package/static/assets/graph-C9eacEi8.js +1 -0
- package/static/assets/index-LZSQkLE2.css +41 -0
- package/static/assets/index-y7XDSgMG.js +495 -0
- package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
- package/static/assets/init-Gi6I4Gst.js +1 -0
- package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
- package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
- package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
- package/static/assets/katex-C5jXJg4s.js +257 -0
- package/static/assets/layout-DEXfKzaS.js +1 -0
- package/static/assets/linear-CLO--ryk.js +1 -0
- package/static/assets/map-Czzmt4hB.js +1 -0
- package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
- package/static/assets/ordinal-Cboi1Yqb.js +1 -0
- package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
- package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
- package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
- package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
- package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
- package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
- package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
- package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
- package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
- package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
- package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
- package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
- package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
- package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
- package/static/index.html +24 -0
|
@@ -0,0 +1,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` → default `3000`.
|
|
17
|
+
|
|
18
|
+
The hub has **zero npm dependencies** — `npm start` (or `node server.js`) works with no
|
|
19
|
+
`npm install`.
|
|
20
|
+
|
|
21
|
+
## File Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
<root>/
|
|
25
|
+
mockup/ # ← the hub (this template)
|
|
26
|
+
server.js # zero-dependency Node HTTP server
|
|
27
|
+
package.json # no dependencies; "start": "node server.js"
|
|
28
|
+
mockup.config.json # { "port": 3000 } — user-editable
|
|
29
|
+
.gitignore
|
|
30
|
+
1_first_app/
|
|
31
|
+
context/mockup/ # assets only (+ mockup-manifest.json)
|
|
32
|
+
2_second_app/
|
|
33
|
+
context/mockup/
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Ensure-Hub Rules (run by every mockgen-* execution, including module-filtered runs)
|
|
37
|
+
|
|
38
|
+
1. If `<root>/mockup/server.js` does NOT exist: create ALL four hub files from the
|
|
39
|
+
templates below.
|
|
40
|
+
2. If it exists: read the `HUB_VERSION` constant in the existing `server.js`.
|
|
41
|
+
- Existing version **< template version** → overwrite `server.js` and `package.json`
|
|
42
|
+
with the templates (upgrade).
|
|
43
|
+
- Existing version **>= template version**, or no marker found (user-customized) →
|
|
44
|
+
leave `server.js` and `package.json` untouched.
|
|
45
|
+
3. `mockup.config.json`: create ONLY if missing. NEVER overwrite an existing one — it
|
|
46
|
+
holds the user's port choice. It is per-machine and gitignored.
|
|
47
|
+
4. `.gitignore`: create ONLY if missing.
|
|
48
|
+
5. Never place per-application assets inside `<root>/mockup/`. Conversely, everything the
|
|
49
|
+
hub generates or needs (config, logs, any `node_modules/`) stays inside `<root>/mockup/`
|
|
50
|
+
and is covered by its `.gitignore`.
|
|
51
|
+
|
|
52
|
+
## mockup-manifest.json Schema (written to `<app_folder>/context/mockup/`)
|
|
53
|
+
|
|
54
|
+
Every mockgen-* run writes/updates this file. The hub uses it for discovery, routing and
|
|
55
|
+
the landing page.
|
|
56
|
+
|
|
57
|
+
| Field | Type | Description |
|
|
58
|
+
|-------|------|-------------|
|
|
59
|
+
| `app` | string | App slug used in URLs = app folder name minus leading `<number>_` (e.g. `hub_middleware`) |
|
|
60
|
+
| `appName` | string | Display name (e.g. "Hub Middleware") |
|
|
61
|
+
| `description` | string | Short app description from PRD.md |
|
|
62
|
+
| `stack` | string | `"tailwind"` or `"shadcn"` |
|
|
63
|
+
| `version` | string | Target version the mockups were generated for (e.g. `v1.0.3`) |
|
|
64
|
+
| `generatedAt` | string | ISO date of the last generation |
|
|
65
|
+
| `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 |
|
|
66
|
+
| `reports` | array (optional) | Report layout file names, if any |
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"app": "hub_middleware",
|
|
73
|
+
"appName": "Hub Middleware",
|
|
74
|
+
"description": "Central middleware for labour mobility data exchange.",
|
|
75
|
+
"stack": "tailwind",
|
|
76
|
+
"version": "v1.0.3",
|
|
77
|
+
"generatedAt": "2026-07-12",
|
|
78
|
+
"roles": [
|
|
79
|
+
{ "name": "Hub Administrator", "slug": "hub_administrator", "screens": 12 },
|
|
80
|
+
{ "name": "Hub Operation Support", "slug": "hub_operation_support", "screens": 9 }
|
|
81
|
+
],
|
|
82
|
+
"reports": ["staff_allocation_summary.html"]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## server.js Template
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
#!/usr/bin/env node
|
|
90
|
+
/*
|
|
91
|
+
* CO2 Mockup Hub — one server for ALL application mockups.
|
|
92
|
+
* HUB_VERSION: 1 (marker used by mockgen-* skills to decide upgrades — do not remove)
|
|
93
|
+
*
|
|
94
|
+
* Discovers every root-level application folder containing context/PRD.md.
|
|
95
|
+
* Apps with a context/mockup/mockup-manifest.json are routable:
|
|
96
|
+
* - stack "tailwind": pages assembled from partials + content fragments
|
|
97
|
+
* - stack "shadcn": served from the built dist/ folder (SPA fallback)
|
|
98
|
+
* Port: PORT env var > mockup.config.json "port" > 3000.
|
|
99
|
+
*/
|
|
100
|
+
'use strict';
|
|
101
|
+
|
|
102
|
+
const http = require('http');
|
|
103
|
+
const fs = require('fs');
|
|
104
|
+
const path = require('path');
|
|
105
|
+
|
|
106
|
+
const HUB_VERSION = 1;
|
|
107
|
+
const HUB_DIR = __dirname;
|
|
108
|
+
const ROOT_DIR = path.resolve(HUB_DIR, '..');
|
|
109
|
+
const SAFE_SEG = /^[A-Za-z0-9._-]+$/;
|
|
110
|
+
|
|
111
|
+
function resolvePort() {
|
|
112
|
+
const envPort = parseInt(process.env.PORT || '', 10);
|
|
113
|
+
if (Number.isInteger(envPort) && envPort > 0) return envPort;
|
|
114
|
+
try {
|
|
115
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(HUB_DIR, 'mockup.config.json'), 'utf8'));
|
|
116
|
+
const cfgPort = parseInt(cfg.port, 10);
|
|
117
|
+
if (Number.isInteger(cfgPort) && cfgPort > 0) return cfgPort;
|
|
118
|
+
} catch (err) { /* missing/invalid config — use default */ }
|
|
119
|
+
return 3000;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const MIME = {
|
|
123
|
+
'.html': 'text/html; charset=utf-8',
|
|
124
|
+
'.css': 'text/css; charset=utf-8',
|
|
125
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
126
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
127
|
+
'.json': 'application/json; charset=utf-8',
|
|
128
|
+
'.svg': 'image/svg+xml',
|
|
129
|
+
'.png': 'image/png',
|
|
130
|
+
'.jpg': 'image/jpeg',
|
|
131
|
+
'.jpeg': 'image/jpeg',
|
|
132
|
+
'.gif': 'image/gif',
|
|
133
|
+
'.webp': 'image/webp',
|
|
134
|
+
'.ico': 'image/x-icon',
|
|
135
|
+
'.woff': 'font/woff',
|
|
136
|
+
'.woff2': 'font/woff2',
|
|
137
|
+
'.ttf': 'font/ttf',
|
|
138
|
+
'.map': 'application/json',
|
|
139
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
140
|
+
'.pdf': 'application/pdf',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// ── Discovery ────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
function titleCase(slug) {
|
|
146
|
+
return slug.replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function rolesFromPrd(appDir) {
|
|
150
|
+
try {
|
|
151
|
+
const prd = fs.readFileSync(path.join(appDir, 'context', 'PRD.md'), 'utf8');
|
|
152
|
+
const names = new Set();
|
|
153
|
+
for (const line of prd.split('\n')) {
|
|
154
|
+
if (line.includes('~~')) continue; // skip strikethrough (deprecated) items
|
|
155
|
+
const m = line.match(/As an? (.+?) user/);
|
|
156
|
+
if (m) names.add(m[1].trim());
|
|
157
|
+
}
|
|
158
|
+
return Array.from(names).map((name) => ({ name, slug: null, screens: 0 }));
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function discoverApps() {
|
|
165
|
+
let entries;
|
|
166
|
+
try { entries = fs.readdirSync(ROOT_DIR, { withFileTypes: true }); } catch (err) { return []; }
|
|
167
|
+
const apps = [];
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
if (!entry.isDirectory()) continue;
|
|
170
|
+
const appDir = path.join(ROOT_DIR, entry.name);
|
|
171
|
+
if (!fs.existsSync(path.join(appDir, 'context', 'PRD.md'))) continue;
|
|
172
|
+
const slug = entry.name.replace(/^\d+_/, '');
|
|
173
|
+
const mockupDir = path.join(appDir, 'context', 'mockup');
|
|
174
|
+
let manifest = null;
|
|
175
|
+
try {
|
|
176
|
+
manifest = JSON.parse(fs.readFileSync(path.join(mockupDir, 'mockup-manifest.json'), 'utf8'));
|
|
177
|
+
} catch (err) { /* mockups not generated yet */ }
|
|
178
|
+
let status = 'not-generated';
|
|
179
|
+
if (manifest && manifest.stack === 'shadcn') {
|
|
180
|
+
status = fs.existsSync(path.join(mockupDir, 'dist', 'index.html')) ? 'ready' : 'build-required';
|
|
181
|
+
} else if (manifest) {
|
|
182
|
+
status = fs.existsSync(path.join(mockupDir, 'partials', 'shell.html')) ? 'ready' : 'not-generated';
|
|
183
|
+
}
|
|
184
|
+
apps.push({
|
|
185
|
+
slug,
|
|
186
|
+
folder: entry.name,
|
|
187
|
+
appName: (manifest && manifest.appName) || titleCase(slug),
|
|
188
|
+
description: (manifest && manifest.description) || '',
|
|
189
|
+
version: (manifest && manifest.version) || '',
|
|
190
|
+
stack: manifest ? manifest.stack : null,
|
|
191
|
+
status,
|
|
192
|
+
mockupDir,
|
|
193
|
+
roles: manifest ? (manifest.roles || []) : rolesFromPrd(appDir),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return apps.sort((a, b) => a.folder.localeCompare(b.folder));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function findApp(slug) {
|
|
200
|
+
return discoverApps().find((a) => a.slug === slug) || null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Response helpers ─────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
function send(res, code, body, type) {
|
|
206
|
+
res.writeHead(code, { 'Content-Type': type || 'text/html; charset=utf-8' });
|
|
207
|
+
res.end(body);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function redirect(res, location) {
|
|
211
|
+
res.writeHead(302, { Location: location });
|
|
212
|
+
res.end();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function escapeHtml(s) {
|
|
216
|
+
return String(s)
|
|
217
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
218
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function safeJoin(baseDir, segments) {
|
|
222
|
+
if (!segments.every((s) => SAFE_SEG.test(s))) return null;
|
|
223
|
+
const target = path.normalize(path.join(baseDir, segments.join(path.sep)));
|
|
224
|
+
if (target !== baseDir && !target.startsWith(baseDir + path.sep)) return null;
|
|
225
|
+
return target;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function serveFile(res, filePath) {
|
|
229
|
+
if (!filePath) return false;
|
|
230
|
+
let stat;
|
|
231
|
+
try { stat = fs.statSync(filePath); } catch (err) { return false; }
|
|
232
|
+
if (!stat.isFile()) return false;
|
|
233
|
+
const type = MIME[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
|
|
234
|
+
send(res, 200, fs.readFileSync(filePath), type);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function readText(filePath) {
|
|
239
|
+
try { return fs.readFileSync(filePath, 'utf8'); } catch (err) { return null; }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Landing page ─────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
const LANDING_CSS = `
|
|
245
|
+
* { box-sizing: border-box; margin: 0; }
|
|
246
|
+
body { font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
247
|
+
background: #f1f5f9; color: #0f172a; padding: 40px 20px; }
|
|
248
|
+
.wrap { max-width: 960px; margin: 0 auto; }
|
|
249
|
+
h1 { font-size: 26px; margin-bottom: 4px; }
|
|
250
|
+
.sub { color: #64748b; font-size: 14px; margin-bottom: 28px; }
|
|
251
|
+
.card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px;
|
|
252
|
+
padding: 20px; margin-bottom: 16px; }
|
|
253
|
+
.card h2 { font-size: 18px; display: inline; }
|
|
254
|
+
.meta { color: #94a3b8; font-size: 12px; margin-left: 8px; }
|
|
255
|
+
.badge { display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 999px;
|
|
256
|
+
margin-left: 8px; vertical-align: 2px; }
|
|
257
|
+
.badge.ready { background: #dcfce7; color: #166534; }
|
|
258
|
+
.badge.pending { background: #fef3c7; color: #92400e; }
|
|
259
|
+
.hint { color: #94a3b8; font-size: 12px; margin-top: 10px; }
|
|
260
|
+
.hint code { background: #f1f5f9; padding: 1px 5px; border-radius: 4px; }
|
|
261
|
+
.roles { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 8px; }
|
|
262
|
+
.role { display: inline-block; font-size: 13px; padding: 6px 14px; border-radius: 8px;
|
|
263
|
+
border: 1px solid #cbd5e1; text-decoration: none; color: #0f172a; }
|
|
264
|
+
a.role:hover { border-color: #2563eb; color: #2563eb; }
|
|
265
|
+
.role.disabled { color: #cbd5e1; border-color: #e2e8f0; cursor: not-allowed; }
|
|
266
|
+
.index-link { font-size: 12px; margin-left: 8px; }
|
|
267
|
+
.foot { color: #94a3b8; font-size: 12px; text-align: center; margin-top: 28px; }
|
|
268
|
+
`;
|
|
269
|
+
|
|
270
|
+
function renderRoleChips(app) {
|
|
271
|
+
if (!app.roles.length) {
|
|
272
|
+
return '<span class="role disabled">No roles found</span>';
|
|
273
|
+
}
|
|
274
|
+
return app.roles.map((role) => {
|
|
275
|
+
if (app.status === 'ready' && role.slug) {
|
|
276
|
+
return '<a class="role" href="/' + app.slug + '/' + role.slug + '/home">'
|
|
277
|
+
+ escapeHtml(role.name) + ' →</a>';
|
|
278
|
+
}
|
|
279
|
+
return '<span class="role disabled" aria-disabled="true">' + escapeHtml(role.name) + '</span>';
|
|
280
|
+
}).join('\n ');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function renderAppCard(app) {
|
|
284
|
+
const badge = app.status === 'ready'
|
|
285
|
+
? '<span class="badge ready">Ready' + (app.stack ? ' · ' + escapeHtml(app.stack) : '') + '</span>'
|
|
286
|
+
: (app.status === 'build-required'
|
|
287
|
+
? '<span class="badge pending">Build required</span>'
|
|
288
|
+
: '<span class="badge pending">Not generated yet</span>');
|
|
289
|
+
const indexLink = app.status === 'ready'
|
|
290
|
+
? '<a class="index-link" href="/' + app.slug + '">screen index</a>' : '';
|
|
291
|
+
let hint = '';
|
|
292
|
+
if (app.status === 'build-required') {
|
|
293
|
+
hint = '<p class="hint">Run <code>cd ' + escapeHtml(app.folder)
|
|
294
|
+
+ '/context/mockup && npm install && npm run build</code> to make this app viewable.</p>';
|
|
295
|
+
} else if (app.status === 'not-generated') {
|
|
296
|
+
hint = '<p class="hint">Run a <code>mockgen-*</code> skill for this application to generate its mockups.</p>';
|
|
297
|
+
}
|
|
298
|
+
return '<div class="card">\n'
|
|
299
|
+
+ ' <h2>' + escapeHtml(app.appName) + '</h2>'
|
|
300
|
+
+ (app.version ? '<span class="meta">' + escapeHtml(app.version) + '</span>' : '')
|
|
301
|
+
+ badge + indexLink + '\n'
|
|
302
|
+
+ (app.description ? ' <p class="sub" style="margin:6px 0 0">' + escapeHtml(app.description) + '</p>\n' : '')
|
|
303
|
+
+ ' <div class="roles">\n ' + renderRoleChips(app) + '\n </div>\n'
|
|
304
|
+
+ hint + '\n</div>';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function renderLanding(apps) {
|
|
308
|
+
return '<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n'
|
|
309
|
+
+ '<meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
|
|
310
|
+
+ '<title>Mockup Hub</title>\n<style>' + LANDING_CSS + '</style>\n</head>\n<body>\n'
|
|
311
|
+
+ '<div class="wrap">\n<h1>Mockup Hub</h1>\n'
|
|
312
|
+
+ '<p class="sub">All application mockups, served from one place. '
|
|
313
|
+
+ apps.length + ' application(s) discovered. Click a role to open its dashboard.</p>\n'
|
|
314
|
+
+ apps.map(renderAppCard).join('\n')
|
|
315
|
+
+ '\n<p class="foot">CO2 Mockup Hub v' + HUB_VERSION
|
|
316
|
+
+ ' · port via PORT env or mockup.config.json</p>\n</div>\n</body>\n</html>';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function notReadyPage(app) {
|
|
320
|
+
return renderLanding([app]);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function notFoundPage(what) {
|
|
324
|
+
return '<!DOCTYPE html><html><body style="font-family:sans-serif;padding:40px">'
|
|
325
|
+
+ '<h1>404 — Not found' + (what ? ': ' + escapeHtml(what) : '') + '</h1>'
|
|
326
|
+
+ '<p><a href="/">Back to Mockup Hub</a></p></body></html>';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── Tailwind assembly ────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
function assemblePage(app, role, page) {
|
|
332
|
+
const dir = app.mockupDir;
|
|
333
|
+
const shell = readText(path.join(dir, 'partials', 'shell.html'));
|
|
334
|
+
let header = readText(path.join(dir, 'partials', 'header.html'));
|
|
335
|
+
const sidebar = readText(path.join(dir, 'partials', 'sidebar-' + role + '.html'));
|
|
336
|
+
const footer = readText(path.join(dir, 'partials', 'footer.html'));
|
|
337
|
+
const content = readText(path.join(dir, role, 'content', page + '.html'));
|
|
338
|
+
if (!shell || !header || !sidebar || !footer) return null;
|
|
339
|
+
header = header.replace(/\{\{ROLE\}\}/g, role).replace(/\{\{APP_SLUG\}\}/g, app.slug);
|
|
340
|
+
const body = content
|
|
341
|
+
|| '<div class="p-6 text-red-500">Page not found: ' + escapeHtml(page) + '</div>';
|
|
342
|
+
return shell
|
|
343
|
+
.replace('{{HEADER}}', header)
|
|
344
|
+
.replace('{{SIDEBAR}}', sidebar.replace(/\{\{APP_SLUG\}\}/g, app.slug))
|
|
345
|
+
.replace('{{CONTENT}}', body)
|
|
346
|
+
.replace('{{FOOTER}}', footer);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── Routing ──────────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
function routeTailwind(app, segs, res) {
|
|
352
|
+
if (segs.length === 1) {
|
|
353
|
+
if (serveFile(res, path.join(app.mockupDir, 'MOCKUP.html'))) return;
|
|
354
|
+
const first = app.roles.find((r) => r.slug);
|
|
355
|
+
if (first) return redirect(res, '/' + app.slug + '/' + first.slug + '/home');
|
|
356
|
+
return send(res, 404, notFoundPage(app.slug));
|
|
357
|
+
}
|
|
358
|
+
if (segs[1] === 'static' || segs[1] === 'reports') {
|
|
359
|
+
const file = safeJoin(path.join(app.mockupDir, segs[1]), segs.slice(2));
|
|
360
|
+
if (serveFile(res, file)) return;
|
|
361
|
+
return send(res, 404, notFoundPage());
|
|
362
|
+
}
|
|
363
|
+
if (!SAFE_SEG.test(segs[1])) return send(res, 400, notFoundPage());
|
|
364
|
+
if (segs.length === 2) return redirect(res, '/' + app.slug + '/' + segs[1] + '/home');
|
|
365
|
+
if (segs.length === 3 && SAFE_SEG.test(segs[2])) {
|
|
366
|
+
const html = assemblePage(app, segs[1], segs[2]);
|
|
367
|
+
if (html === null) {
|
|
368
|
+
return send(res, 500, '<h1>Missing partials. Re-run the mockup generator.</h1>');
|
|
369
|
+
}
|
|
370
|
+
return send(res, 200, html);
|
|
371
|
+
}
|
|
372
|
+
return send(res, 404, notFoundPage());
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function routeShadcn(app, segs, res) {
|
|
376
|
+
if (segs.length === 1 && serveFile(res, path.join(app.mockupDir, 'MOCKUP.html'))) return;
|
|
377
|
+
if (segs[1] === 'reports') {
|
|
378
|
+
const file = safeJoin(path.join(app.mockupDir, 'public', 'reports'), segs.slice(2));
|
|
379
|
+
if (serveFile(res, file)) return;
|
|
380
|
+
return send(res, 404, notFoundPage());
|
|
381
|
+
}
|
|
382
|
+
const distDir = path.join(app.mockupDir, 'dist');
|
|
383
|
+
if (segs.length > 1) {
|
|
384
|
+
const file = safeJoin(distDir, segs.slice(1));
|
|
385
|
+
if (serveFile(res, file)) return;
|
|
386
|
+
}
|
|
387
|
+
// SPA fallback — React Router (basename /{app_slug}) handles the path client-side
|
|
388
|
+
if (serveFile(res, path.join(distDir, 'index.html'))) return;
|
|
389
|
+
return send(res, 404, notFoundPage(app.slug));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function route(req, res) {
|
|
393
|
+
const urlPath = decodeURIComponent((req.url || '/').split('?')[0]);
|
|
394
|
+
const segs = urlPath.split('/').filter(Boolean);
|
|
395
|
+
|
|
396
|
+
if (segs.length === 0) return send(res, 200, renderLanding(discoverApps()));
|
|
397
|
+
if (urlPath === '/favicon.ico') { res.writeHead(204); return res.end(); }
|
|
398
|
+
|
|
399
|
+
// HTMX fragment endpoint (tailwind): /api/content/:app/:role/:page
|
|
400
|
+
if (segs[0] === 'api' && segs[1] === 'content') {
|
|
401
|
+
if (segs.length !== 5) return send(res, 404, notFoundPage());
|
|
402
|
+
const app = findApp(segs[2]);
|
|
403
|
+
if (!app || app.status !== 'ready' || app.stack !== 'tailwind') {
|
|
404
|
+
return send(res, 404, notFoundPage(segs[2]));
|
|
405
|
+
}
|
|
406
|
+
if (!SAFE_SEG.test(segs[3]) || !SAFE_SEG.test(segs[4])) return send(res, 400, notFoundPage());
|
|
407
|
+
const content = readText(path.join(app.mockupDir, segs[3], 'content', segs[4] + '.html'));
|
|
408
|
+
if (content === null) {
|
|
409
|
+
return send(res, 404, '<div class="p-6 text-red-500">Content not found.</div>');
|
|
410
|
+
}
|
|
411
|
+
return send(res, 200, content);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!SAFE_SEG.test(segs[0])) return send(res, 400, notFoundPage());
|
|
415
|
+
const app = findApp(segs[0]);
|
|
416
|
+
if (!app) return send(res, 404, notFoundPage(segs[0]));
|
|
417
|
+
if (app.status !== 'ready') return send(res, 200, notReadyPage(app));
|
|
418
|
+
|
|
419
|
+
if (app.stack === 'shadcn') return routeShadcn(app, segs, res);
|
|
420
|
+
return routeTailwind(app, segs, res);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── Start ────────────────────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
const PORT = resolvePort();
|
|
426
|
+
const server = http.createServer((req, res) => {
|
|
427
|
+
try {
|
|
428
|
+
route(req, res);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
send(res, 500, '<h1>500 — Hub error</h1><pre>' + escapeHtml(err && err.stack || err) + '</pre>');
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
server.listen(PORT, () => {
|
|
435
|
+
console.log('');
|
|
436
|
+
console.log(' CO2 Mockup Hub v' + HUB_VERSION + ' running at:');
|
|
437
|
+
console.log(' -> http://localhost:' + PORT);
|
|
438
|
+
console.log('');
|
|
439
|
+
console.log(' Serving mockups for all applications under: ' + ROOT_DIR);
|
|
440
|
+
console.log(' Change port: PORT env var, or "port" in mockup.config.json');
|
|
441
|
+
console.log('');
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## package.json Template
|
|
446
|
+
|
|
447
|
+
```json
|
|
448
|
+
{
|
|
449
|
+
"name": "co2-mockup-hub",
|
|
450
|
+
"private": true,
|
|
451
|
+
"version": "1.0.0",
|
|
452
|
+
"description": "CO2 Mockup Hub — single server for all application mockups",
|
|
453
|
+
"main": "server.js",
|
|
454
|
+
"scripts": {
|
|
455
|
+
"start": "node server.js"
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## mockup.config.json Template
|
|
461
|
+
|
|
462
|
+
Create only if missing — never overwrite (it stores the user's port choice).
|
|
463
|
+
|
|
464
|
+
```json
|
|
465
|
+
{
|
|
466
|
+
"port": 3000
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Port precedence at startup: `PORT` env var (e.g. `PORT=4000 npm start`) →
|
|
471
|
+
`mockup.config.json` `port` → `3000`.
|
|
472
|
+
|
|
473
|
+
## .gitignore Template
|
|
474
|
+
|
|
475
|
+
Everything npm- or machine-generated inside `<root>/mockup/` stays inside `<root>/mockup/`
|
|
476
|
+
and is gitignored there. The hub has zero dependencies, so `node_modules/` normally never
|
|
477
|
+
exists — the rule is a safety net in case a user installs packages. `mockup.config.json`
|
|
478
|
+
is per-machine (each machine keeps its own port choice; the skills recreate it if missing),
|
|
479
|
+
so it is gitignored too.
|
|
480
|
+
|
|
481
|
+
```
|
|
482
|
+
node_modules/
|
|
483
|
+
package-lock.json
|
|
484
|
+
mockup.config.json
|
|
485
|
+
.DS_Store
|
|
486
|
+
*.log
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Quick Start (for output summaries)
|
|
490
|
+
|
|
491
|
+
```
|
|
492
|
+
Mockup Hub
|
|
493
|
+
==========
|
|
494
|
+
1. cd <root>/mockup
|
|
495
|
+
2. npm start (zero dependencies — no npm install required)
|
|
496
|
+
3. Open http://localhost:3000 in your browser
|
|
497
|
+
(change port: PORT=4000 npm start, or edit mockup.config.json)
|
|
498
|
+
```
|