@rashidee/co2 1.3.10 → 1.3.11
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-shm +0 -0
- package/dist/.co2-dat/app.db-wal +0 -0
- package/dist/index.js +155 -1
- package/package.json +1 -1
- package/plugin/skills/mockgen-shadcn/SKILL.md +11 -7
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +14 -3
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +98 -40
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +98 -40
- package/plugin/skills/util-gencicdscript/SKILL.md +34 -8
- package/plugin/skills/util-gencicdscript/references/cicd-app-template.md +127 -20
- package/plugin/skills/util-plancicd/SKILL.md +62 -1
- package/static/assets/{abnfDiagram-VRR7QNED-ChoWkBm-.js → abnfDiagram-VRR7QNED-DBzgA_2G.js} +1 -1
- package/static/assets/{arc-BOiutWhq.js → arc-Bc02_4l6.js} +1 -1
- package/static/assets/{architectureDiagram-ZJ3FMSHR-Dn4YKYfM.js → architectureDiagram-ZJ3FMSHR-ixN9wSDW.js} +1 -1
- package/static/assets/{blockDiagram-677ZJIJ3-BrXEfs29.js → blockDiagram-677ZJIJ3-Ob4lZkCY.js} +1 -1
- package/static/assets/{c4Diagram-LMCZKHZV-ChaIzrlx.js → c4Diagram-LMCZKHZV-BeJY3Zks.js} +1 -1
- package/static/assets/channel-CJFxgMC1.js +1 -0
- package/static/assets/{chunk-2Q5K7J3B-BOlnmyI2.js → chunk-2Q5K7J3B-DTFIWNGb.js} +1 -1
- package/static/assets/{chunk-32BRIVSS-BYtO1gsq.js → chunk-32BRIVSS-DLTvoPpT.js} +1 -1
- package/static/assets/{chunk-5VM5RSS4-Cmx4Morj.js → chunk-5VM5RSS4-BKlvf6hI.js} +1 -1
- package/static/assets/{chunk-EX3LRPZG-BcgUofmA.js → chunk-EX3LRPZG-B9ej8_ma.js} +1 -1
- package/static/assets/{chunk-JWPE2WC7-CnNRXdvN.js → chunk-JWPE2WC7-DAYaiy17.js} +1 -1
- package/static/assets/{chunk-MOJQB5TN-DGG6Q3Wn.js → chunk-MOJQB5TN-D6rRKzWc.js} +1 -1
- package/static/assets/{chunk-RYQCIY6F-C8erKHls.js → chunk-RYQCIY6F-CE4CHN6p.js} +1 -1
- package/static/assets/{chunk-V7JOEXUC-BNkIJqzu.js → chunk-V7JOEXUC-BR-Kl6S2.js} +1 -1
- package/static/assets/{chunk-VR4S4FIN-CGq-RseS.js → chunk-VR4S4FIN-Bo9J3WGe.js} +1 -1
- package/static/assets/{chunk-XXDRQBXY-DOVfEC7j.js → chunk-XXDRQBXY-DjI5dJHR.js} +1 -1
- package/static/assets/classDiagram-OUVF2IWQ-B5OYUyBf.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-B5OYUyBf.js +1 -0
- package/static/assets/{cose-bilkent-JH36ORCC-BrK3gCzK.js → cose-bilkent-JH36ORCC-DLciz-3w.js} +1 -1
- package/static/assets/{cynefin-VYW2F7L2-BTMYzKLg.js → cynefin-VYW2F7L2-DqfVJKpm.js} +1 -1
- package/static/assets/{cynefinDiagram-TSTJHNR4-Df4k0_Hb.js → cynefinDiagram-TSTJHNR4-D14oLdNM.js} +1 -1
- package/static/assets/{dagre-VKFMJZFB-0LnYnEqq.js → dagre-VKFMJZFB-BduMbW80.js} +1 -1
- package/static/assets/{diagram-FQU43EPY-B5nIVD5g.js → diagram-FQU43EPY-CLQg7RWT.js} +1 -1
- package/static/assets/{diagram-G47NLZAW-B2YLBzdb.js → diagram-G47NLZAW-Dq3Ymr1b.js} +1 -1
- package/static/assets/{diagram-NH7WQ7WH-DPVumXIF.js → diagram-NH7WQ7WH-BPjov8S5.js} +1 -1
- package/static/assets/{diagram-OA4YK3LP-Da20vcLJ.js → diagram-OA4YK3LP-BgQgpiJD.js} +1 -1
- package/static/assets/{diagram-WEI45ONY-DQbTpuRZ.js → diagram-WEI45ONY-BeOPSl27.js} +1 -1
- package/static/assets/{ebnfDiagram-CCIWWBDH-BEn1vEyb.js → ebnfDiagram-CCIWWBDH-DFsTw8OM.js} +1 -1
- package/static/assets/{erDiagram-Q63AITRT-Ccg0DVBG.js → erDiagram-Q63AITRT-scBEBawL.js} +1 -1
- package/static/assets/{flowDiagram-23GEKE2U-B2dMjEpa.js → flowDiagram-23GEKE2U-BfX-2ZO-.js} +1 -1
- package/static/assets/{ganttDiagram-NO4QXBWP-Kp1UsG59.js → ganttDiagram-NO4QXBWP-BR5IJ1mv.js} +1 -1
- package/static/assets/{gitGraphDiagram-IHSO6WYX-D6RB8sPf.js → gitGraphDiagram-IHSO6WYX-zPu_QDUM.js} +1 -1
- package/static/assets/{index-CM4GfOLV.css → index-DKI3BgkF.css} +1 -1
- package/static/assets/{index-GEUFVo96.js → index-Dnp-sAA_.js} +111 -115
- package/static/assets/{infoDiagram-FWYZ7A6U-C4ILACOF.js → infoDiagram-FWYZ7A6U-CpyKTQb9.js} +1 -1
- package/static/assets/{ishikawaDiagram-FXEZZL3T-Q2Ef9fW7.js → ishikawaDiagram-FXEZZL3T-C2SXt-2Y.js} +1 -1
- package/static/assets/{journeyDiagram-5HDEW3XC-B7ZV1i0L.js → journeyDiagram-5HDEW3XC-DJavk0C4.js} +1 -1
- package/static/assets/{kanban-definition-HUTT4EX6-CjaPV_vw.js → kanban-definition-HUTT4EX6-xN4pVGL7.js} +1 -1
- package/static/assets/{linear-BELpVz92.js → linear-BMWKojRX.js} +1 -1
- package/static/assets/{mindmap-definition-LN4V7U3C-COhl-GrE.js → mindmap-definition-LN4V7U3C-DOaDf7u0.js} +1 -1
- package/static/assets/{pegDiagram-2B236MQR-3FlTtWva.js → pegDiagram-2B236MQR-B1px906A.js} +1 -1
- package/static/assets/{pieDiagram-ENE6RG2P-CLeOh_sh.js → pieDiagram-ENE6RG2P-s0vEagFS.js} +1 -1
- package/static/assets/{quadrantDiagram-ABIIQ3AL-D2MnobFS.js → quadrantDiagram-ABIIQ3AL-BEhIl8IZ.js} +1 -1
- package/static/assets/{railroadDiagram-RFXS5EU6-FjtL4fEj.js → railroadDiagram-RFXS5EU6-fFsin92S.js} +1 -1
- package/static/assets/{requirementDiagram-TGXJPOKE-C5Y2uWRV.js → requirementDiagram-TGXJPOKE-Cf9J7tii.js} +1 -1
- package/static/assets/{sankeyDiagram-HTMAVEWB-ITj9bISn.js → sankeyDiagram-HTMAVEWB-Cy5gaZqM.js} +1 -1
- package/static/assets/{sequenceDiagram-DBY2YBRQ-Ct2xTSbI.js → sequenceDiagram-DBY2YBRQ-DI61Vnk1.js} +1 -1
- package/static/assets/{sizeCapture-X5ZJPWSS-w9hGRpov.js → sizeCapture-X5ZJPWSS-CmUBUW3B.js} +1 -1
- package/static/assets/{stateDiagram-2N3HPSRC-BlxDF1qr.js → stateDiagram-2N3HPSRC-CBMhhwuE.js} +1 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-BD1I889C.js +1 -0
- package/static/assets/{swimlanes-5IMT3BWC-DFYm_SCW.js → swimlanes-5IMT3BWC-CtZlr9wD.js} +2 -2
- package/static/assets/swimlanesDiagram-G3AALYLV-BfEZlw_9.js +8 -0
- package/static/assets/{timeline-definition-FHXFAJF6-johIMUxt.js → timeline-definition-FHXFAJF6-BC69PWR-.js} +1 -1
- package/static/assets/{vennDiagram-L72KCM5P-BFk2-H5y.js → vennDiagram-L72KCM5P-jEaqKPSP.js} +1 -1
- package/static/assets/{wardleyDiagram-EHGQE667-T7509MWq.js → wardleyDiagram-EHGQE667-CKFWi3EK.js} +1 -1
- package/static/assets/{xychartDiagram-FW5EYKEG-DskgQ7uq.js → xychartDiagram-FW5EYKEG-CjyCKfOu.js} +1 -1
- package/static/index.html +2 -2
- package/static/assets/channel-CKovdojM.js +0 -1
- package/static/assets/classDiagram-OUVF2IWQ-BRxqfb0_.js +0 -1
- package/static/assets/classDiagram-v2-EOCWNBFH-BRxqfb0_.js +0 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-CQVgnOlz.js +0 -1
- package/static/assets/swimlanesDiagram-G3AALYLV-DubMPSZe.js +0 -8
package/dist/.co2-dat/app.db-shm
CHANGED
|
Binary file
|
package/dist/.co2-dat/app.db-wal
CHANGED
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -6809,7 +6809,7 @@ var {
|
|
|
6809
6809
|
// package.json
|
|
6810
6810
|
var package_default = {
|
|
6811
6811
|
name: "@rashidee/co2",
|
|
6812
|
-
version: "1.3.
|
|
6812
|
+
version: "1.3.11",
|
|
6813
6813
|
description: "Compound Context Studio \u2014 self-hosted team context authoring for CO2 projects",
|
|
6814
6814
|
type: "module",
|
|
6815
6815
|
license: "MIT",
|
|
@@ -19151,6 +19151,14 @@ var gitResolveRequestSchema = external_exports.object({
|
|
|
19151
19151
|
message: "Manual resolution requires the merged content",
|
|
19152
19152
|
path: ["content"]
|
|
19153
19153
|
});
|
|
19154
|
+
var gitPullResponseSchema = external_exports.object({
|
|
19155
|
+
status: external_exports.enum(["up_to_date", "merged", "conflicts"]),
|
|
19156
|
+
branch: external_exports.string(),
|
|
19157
|
+
/** New short HEAD hash — present only for `merged`. */
|
|
19158
|
+
commitHash: external_exports.string().optional(),
|
|
19159
|
+
/** Git's own summary (e.g. the conflict list) — display verbatim. */
|
|
19160
|
+
message: external_exports.string().optional()
|
|
19161
|
+
});
|
|
19154
19162
|
|
|
19155
19163
|
// ../core/src/schemas/import.ts
|
|
19156
19164
|
var importCandidateSchema = external_exports.object({
|
|
@@ -19905,6 +19913,64 @@ async function startMockupHub(db, workspacePath, projectId, requestHost, port) {
|
|
|
19905
19913
|
}
|
|
19906
19914
|
return toStatus({ location, running: false, activePort: targetPort }, requestHost, projectId);
|
|
19907
19915
|
}
|
|
19916
|
+
var STRIP_HEADERS = /* @__PURE__ */ new Set([
|
|
19917
|
+
"connection",
|
|
19918
|
+
"keep-alive",
|
|
19919
|
+
"proxy-authenticate",
|
|
19920
|
+
"proxy-authorization",
|
|
19921
|
+
"te",
|
|
19922
|
+
"trailer",
|
|
19923
|
+
"transfer-encoding",
|
|
19924
|
+
"upgrade",
|
|
19925
|
+
"host",
|
|
19926
|
+
"content-length",
|
|
19927
|
+
"content-encoding",
|
|
19928
|
+
"accept-encoding"
|
|
19929
|
+
]);
|
|
19930
|
+
async function proxyHubRequest(db, workspacePath, projectId, prefix, subPath, req) {
|
|
19931
|
+
const location = resolveHub(db, workspacePath, projectId);
|
|
19932
|
+
if (!location.present) throw svcError("MOCKUP_HUB_MISSING", 404);
|
|
19933
|
+
if (!location.configPort) throw svcError("MOCKUP_HUB_DOWN", 502);
|
|
19934
|
+
const url = new URL(req.url);
|
|
19935
|
+
const target = `http://127.0.0.1:${location.configPort}${subPath}${url.search}`;
|
|
19936
|
+
const headers = new Headers();
|
|
19937
|
+
req.headers.forEach((value, name) => {
|
|
19938
|
+
if (!STRIP_HEADERS.has(name.toLowerCase())) headers.set(name, value);
|
|
19939
|
+
});
|
|
19940
|
+
headers.set("x-forwarded-prefix", prefix);
|
|
19941
|
+
headers.set("x-forwarded-host", url.host);
|
|
19942
|
+
headers.set("x-forwarded-proto", url.protocol.replace(/:$/, ""));
|
|
19943
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
19944
|
+
let upstream;
|
|
19945
|
+
try {
|
|
19946
|
+
upstream = await fetch(target, {
|
|
19947
|
+
method: req.method,
|
|
19948
|
+
headers,
|
|
19949
|
+
body: hasBody ? await req.arrayBuffer() : void 0,
|
|
19950
|
+
redirect: "manual"
|
|
19951
|
+
});
|
|
19952
|
+
} catch {
|
|
19953
|
+
throw svcError("MOCKUP_HUB_DOWN", 502);
|
|
19954
|
+
}
|
|
19955
|
+
const responseHeaders = new Headers();
|
|
19956
|
+
upstream.headers.forEach((value, name) => {
|
|
19957
|
+
if (!STRIP_HEADERS.has(name.toLowerCase())) responseHeaders.set(name, value);
|
|
19958
|
+
});
|
|
19959
|
+
const redirectTo = responseHeaders.get("location");
|
|
19960
|
+
if (redirectTo?.startsWith("/") && redirectTo !== prefix && !redirectTo.startsWith(`${prefix}/`)) {
|
|
19961
|
+
responseHeaders.set("location", `${prefix}${redirectTo}`);
|
|
19962
|
+
}
|
|
19963
|
+
const prefixAware = upstream.headers.get("x-hub-prefix") !== null;
|
|
19964
|
+
responseHeaders.delete("x-hub-prefix");
|
|
19965
|
+
if (!prefixAware && responseHeaders.get("content-type")?.includes("text/html")) {
|
|
19966
|
+
const html = (await upstream.text()).replace(
|
|
19967
|
+
/(href|src|action|hx-get|hx-post|hx-put|hx-patch|hx-delete)="(\/[^"]*)"/g,
|
|
19968
|
+
(match2, attr, url2) => url2 === prefix || url2.startsWith(`${prefix}/`) ? match2 : `${attr}="${prefix}${url2}"`
|
|
19969
|
+
);
|
|
19970
|
+
return new Response(html, { status: upstream.status, headers: responseHeaders });
|
|
19971
|
+
}
|
|
19972
|
+
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
|
|
19973
|
+
}
|
|
19908
19974
|
function stopMockupHub(projectId) {
|
|
19909
19975
|
const child = spawnedHubs.get(projectId);
|
|
19910
19976
|
if (!child) return false;
|
|
@@ -20201,6 +20267,24 @@ function artifactRoutes(db, workspacePath) {
|
|
|
20201
20267
|
return errorResponse(c, e);
|
|
20202
20268
|
}
|
|
20203
20269
|
});
|
|
20270
|
+
app.all(
|
|
20271
|
+
"/mockup-proxy/:projectId",
|
|
20272
|
+
(c) => (
|
|
20273
|
+
// Normalize to the trailing-slash form so the prefix always maps to the hub root.
|
|
20274
|
+
c.redirect(`${c.req.path}/`, 301)
|
|
20275
|
+
)
|
|
20276
|
+
);
|
|
20277
|
+
app.all("/mockup-proxy/:projectId/*", async (c) => {
|
|
20278
|
+
const projectId = Number(c.req.param("projectId"));
|
|
20279
|
+
if (!Number.isInteger(projectId) || projectId <= 0) return c.json({ code: "INVALID_ID" }, 400);
|
|
20280
|
+
const prefix = `/api/artifacts/mockup-proxy/${projectId}`;
|
|
20281
|
+
const subPath = c.req.path.slice(prefix.length) || "/";
|
|
20282
|
+
try {
|
|
20283
|
+
return await proxyHubRequest(db, workspacePath, projectId, prefix, subPath, c.req.raw);
|
|
20284
|
+
} catch (e) {
|
|
20285
|
+
return errorResponse(c, e);
|
|
20286
|
+
}
|
|
20287
|
+
});
|
|
20204
20288
|
app.get("/:id/content", (c) => {
|
|
20205
20289
|
const id = Number(c.req.param("id"));
|
|
20206
20290
|
if (Number.isNaN(id)) return c.json({ code: "INVALID_ID" }, 400);
|
|
@@ -24404,6 +24488,67 @@ function resolveConflict(db, workspacePath, projectId, input) {
|
|
|
24404
24488
|
throw new GitServiceError("GIT_RESOLVE_FAILED", 409, detail(e));
|
|
24405
24489
|
}
|
|
24406
24490
|
}
|
|
24491
|
+
function pullChanges(db, workspacePath, projectId, userId) {
|
|
24492
|
+
const dir = projectDir(db, workspacePath, projectId);
|
|
24493
|
+
if (!existsSync18(dir) || !isGitRepo(dir)) {
|
|
24494
|
+
throw new GitServiceError("NO_GIT_REPO", 404, "No git repository for this project");
|
|
24495
|
+
}
|
|
24496
|
+
const status = getGitStatus(db, workspacePath, projectId);
|
|
24497
|
+
if (!status.clean) {
|
|
24498
|
+
throw new GitServiceError(
|
|
24499
|
+
"UNCOMMITTED_CHANGES",
|
|
24500
|
+
409,
|
|
24501
|
+
"Commit or discard your local changes before pulling"
|
|
24502
|
+
);
|
|
24503
|
+
}
|
|
24504
|
+
const branch = safe(() => git3(dir, ["branch", "--show-current"])) ?? "";
|
|
24505
|
+
if (branch === "") {
|
|
24506
|
+
throw new GitServiceError(
|
|
24507
|
+
"DETACHED_HEAD",
|
|
24508
|
+
409,
|
|
24509
|
+
"The repository is in detached HEAD state \u2014 check out a branch before pulling"
|
|
24510
|
+
);
|
|
24511
|
+
}
|
|
24512
|
+
const ctx = resolveGitPushContext(db, projectId, userId);
|
|
24513
|
+
if (ctx.remoteUrl) ensureOrigin(dir, ctx.remoteUrl);
|
|
24514
|
+
const remotes = safe(() => git3(dir, ["remote"])) ?? "";
|
|
24515
|
+
if (!remotes.split("\n").includes("origin")) {
|
|
24516
|
+
throw new GitServiceError("NO_REMOTE", 400, "No git remote configured for this project");
|
|
24517
|
+
}
|
|
24518
|
+
const headBefore = safe(() => git3(dir, ["rev-parse", "--short", "HEAD"])) ?? "";
|
|
24519
|
+
const authB64 = ctx.auth ? Buffer.from(`${ctx.auth.username}:${ctx.auth.token}`).toString("base64") : null;
|
|
24520
|
+
try {
|
|
24521
|
+
execFileSync3(
|
|
24522
|
+
"git",
|
|
24523
|
+
[
|
|
24524
|
+
...buildPushAuthArgs(ctx.auth),
|
|
24525
|
+
...identityArgs(db, userId),
|
|
24526
|
+
"pull",
|
|
24527
|
+
"--no-rebase",
|
|
24528
|
+
"--no-edit",
|
|
24529
|
+
"origin",
|
|
24530
|
+
branch
|
|
24531
|
+
],
|
|
24532
|
+
{
|
|
24533
|
+
cwd: dir,
|
|
24534
|
+
stdio: "pipe",
|
|
24535
|
+
timeout: 12e4,
|
|
24536
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
24537
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
24538
|
+
}
|
|
24539
|
+
);
|
|
24540
|
+
} catch (e) {
|
|
24541
|
+
if (authB64) redactAuthHeaderFromError(e, authB64);
|
|
24542
|
+
const after = getGitStatus(db, workspacePath, projectId);
|
|
24543
|
+
if (after.hasConflicts) {
|
|
24544
|
+
return { status: "conflicts", branch, message: gitErrorDetail(e) };
|
|
24545
|
+
}
|
|
24546
|
+
throw new GitServiceError("GIT_PULL_FAILED", 409, withIdentityHint(gitErrorDetail(e)));
|
|
24547
|
+
}
|
|
24548
|
+
const headAfter = safe(() => git3(dir, ["rev-parse", "--short", "HEAD"])) ?? "";
|
|
24549
|
+
if (headAfter === headBefore) return { status: "up_to_date", branch };
|
|
24550
|
+
return { status: "merged", branch, commitHash: headAfter };
|
|
24551
|
+
}
|
|
24407
24552
|
function detail(e) {
|
|
24408
24553
|
const stderr = e.stderr?.toString().trim();
|
|
24409
24554
|
return stderr || e.message || String(e);
|
|
@@ -24475,6 +24620,15 @@ function gitRoutes(db, workspacePath) {
|
|
|
24475
24620
|
return errorResponse7(c, e);
|
|
24476
24621
|
}
|
|
24477
24622
|
});
|
|
24623
|
+
app.post("/pull/:projectId", (c) => {
|
|
24624
|
+
const projectId = Number(c.req.param("projectId"));
|
|
24625
|
+
if (Number.isNaN(projectId)) return c.json({ code: "INVALID_ID" }, 400);
|
|
24626
|
+
try {
|
|
24627
|
+
return c.json(pullChanges(db, workspacePath, projectId, c.get("user")?.id));
|
|
24628
|
+
} catch (e) {
|
|
24629
|
+
return errorResponse7(c, e);
|
|
24630
|
+
}
|
|
24631
|
+
});
|
|
24478
24632
|
return app;
|
|
24479
24633
|
}
|
|
24480
24634
|
|
package/package.json
CHANGED
|
@@ -36,7 +36,8 @@ designer review. Layout uses React components (header, sidebar, footer) composed
|
|
|
36
36
|
layout route. Pages are React Router routes rendered inside the layout. All navigation is
|
|
37
37
|
client-side via React Router `<Link>` and `useNavigate`.
|
|
38
38
|
|
|
39
|
-
The mockup is generated with `base
|
|
39
|
+
The mockup is generated with Vite `base` set to `/{app_slug}/` (router basename derived
|
|
40
|
+
from the current URL so it also works behind the studio's proxy prefix), **built** with
|
|
40
41
|
`npm run build`, and served from its `dist/` folder by the **shared Mockup Hub** — a single
|
|
41
42
|
zero-dependency Node.js server at `<root>/mockup/` that serves ALL applications' mockups
|
|
42
43
|
(see [references/mockup-hub-template.md](references/mockup-hub-template.md)). The hub renders
|
|
@@ -90,8 +91,8 @@ The application name is matched against root-level application folders:
|
|
|
90
91
|
|
|
91
92
|
**App slug** (used as the URL base path): the application folder name with the leading
|
|
92
93
|
`<number>_` prefix stripped (e.g., `1_hub_middleware` → `hub_middleware`). Record it during
|
|
93
|
-
input resolution — it is baked into `vite.config.ts` (`base`), `main.tsx`
|
|
94
|
-
|
|
94
|
+
input resolution — it is baked into `vite.config.ts` (`base`), `main.tsx` (the runtime
|
|
95
|
+
basename derivation always ends in `/{app_slug}`), and all MOCKUP.html links.
|
|
95
96
|
|
|
96
97
|
### Example Invocations
|
|
97
98
|
|
|
@@ -528,8 +529,10 @@ Key project setup:
|
|
|
528
529
|
- `postcss.config.js` — PostCSS with Tailwind and autoprefixer
|
|
529
530
|
- `components.json` — shadcn/ui configuration (New York style, zinc base)
|
|
530
531
|
- `index.html` — Vite entry HTML with Google Fonts
|
|
531
|
-
- `src/main.tsx` — React root render with `<BrowserRouter basename=
|
|
532
|
-
|
|
532
|
+
- `src/main.tsx` — React root render with a `<BrowserRouter basename={...}>` derived at
|
|
533
|
+
runtime from the current URL — it always ends in `/{app_slug}` (matching the Vite
|
|
534
|
+
`base`) and preserves any proxy prefix (Compound Context Studio embeds the hub through
|
|
535
|
+
its same-origin proxy); see the main.tsx template
|
|
533
536
|
- `src/App.tsx` — React Router configuration with all role/page routes
|
|
534
537
|
- `src/index.css` — Tailwind directives + shadcn/ui CSS variables (light and dark)
|
|
535
538
|
- `src/lib/utils.ts` — `cn()` utility (clsx + tailwind-merge)
|
|
@@ -1009,8 +1012,9 @@ After all mockup files are successfully generated, append an entry to `CHANGELOG
|
|
|
1009
1012
|
v1.0.2` means "generate Employer module pages as they exist at v1.0.2". Version filtering
|
|
1010
1013
|
and module filtering each apply independently; a story must satisfy BOTH to be included.
|
|
1011
1014
|
- **Base path is mandatory**: `vite.config.ts` MUST set `base: "/{app_slug}/"` and
|
|
1012
|
-
`main.tsx` MUST
|
|
1013
|
-
|
|
1015
|
+
`main.tsx` MUST derive the router basename from the current URL per the main.tsx
|
|
1016
|
+
template (it always ends in `/{app_slug}`) — without them the built app cannot be
|
|
1017
|
+
served by the hub at `/{app_slug}`, directly or behind the studio's proxy prefix.
|
|
1014
1018
|
- **No per-app server for viewing**: reviewers use ONLY the shared hub at `<root>/mockup/`
|
|
1015
1019
|
(ensure it per Step 4d). The Vite dev server is for design iteration only.
|
|
1016
1020
|
- **Manifest is mandatory**: every run writes/updates `mockup-manifest.json` — without it
|
|
@@ -461,9 +461,19 @@ import { ThemeProvider } from "next-themes";
|
|
|
461
461
|
import App from "./App";
|
|
462
462
|
import "./index.css";
|
|
463
463
|
|
|
464
|
+
// The app is served at /{{APP_SLUG}} by the Mockup Hub — either directly, or behind a
|
|
465
|
+
// prefix-forwarding proxy (Compound Context Studio embeds the hub at
|
|
466
|
+
// /api/artifacts/mockup-proxy/<id>/). Derive the basename from the current URL so
|
|
467
|
+
// client-side routing works in both cases.
|
|
468
|
+
const APP_BASE = "/{{APP_SLUG}}";
|
|
469
|
+
const path = window.location.pathname;
|
|
470
|
+
const cut = path.indexOf(APP_BASE + "/");
|
|
471
|
+
const basename =
|
|
472
|
+
cut >= 0 ? path.slice(0, cut) + APP_BASE : path.endsWith(APP_BASE) ? path : APP_BASE;
|
|
473
|
+
|
|
464
474
|
createRoot(document.getElementById("root")!).render(
|
|
465
475
|
<StrictMode>
|
|
466
|
-
<BrowserRouter basename=
|
|
476
|
+
<BrowserRouter basename={basename}>
|
|
467
477
|
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
|
|
468
478
|
<App />
|
|
469
479
|
</ThemeProvider>
|
|
@@ -472,8 +482,9 @@ createRoot(document.getElementById("root")!).render(
|
|
|
472
482
|
);
|
|
473
483
|
```
|
|
474
484
|
|
|
475
|
-
> The
|
|
476
|
-
> paths stay unchanged; React Router resolves them under
|
|
485
|
+
> The basename always ends in `/{{APP_SLUG}}` (matching the Vite `base`) — all internal
|
|
486
|
+
> `<Link to="/{role}/...">` paths stay unchanged; React Router resolves them under it
|
|
487
|
+
> automatically, with any proxy prefix preserved.
|
|
477
488
|
|
|
478
489
|
---
|
|
479
490
|
|
|
@@ -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:
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
472
|
+
if (serveFile(res, file, prefix)) return;
|
|
473
|
+
return sendHtml(res, 404, notFoundPage(), prefix);
|
|
424
474
|
}
|
|
425
|
-
if (!SAFE_SEG.test(segs[1])) return
|
|
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
|
|
482
|
+
return sendHtml(res, 200, html, prefix);
|
|
433
483
|
}
|
|
434
|
-
return
|
|
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
|
|
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
|
|
450
|
-
|
|
451
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
552
|
+
return sendHtml(res, 200, content, prefix);
|
|
497
553
|
}
|
|
498
554
|
|
|
499
|
-
if (!SAFE_SEG.test(segs[0])) return
|
|
555
|
+
if (!SAFE_SEG.test(segs[0])) return sendHtml(res, 400, notFoundPage(), prefix);
|
|
500
556
|
const app = findApp(segs[0]);
|
|
501
|
-
if (!app) return
|
|
502
|
-
if (app.status !== 'ready') return
|
|
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
|
```
|