@rashidee/co2 1.3.18 → 1.3.20
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 +78 -29
- package/package.json +1 -1
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/util-gencicdscript/SKILL.md +5 -2
- package/plugin/skills/util-gencicdscript/references/cicd-app-template.md +156 -54
- package/plugin/skills/util-plancicd/SKILL.md +319 -305
- package/static/assets/{abnfDiagram-VRR7QNED-kjS3baPt.js → abnfDiagram-VRR7QNED-cQAiKjYG.js} +1 -1
- package/static/assets/{arc-CXd0xnDj.js → arc-C4ZlEQ3S.js} +1 -1
- package/static/assets/{architectureDiagram-ZJ3FMSHR-5QfnAz_Q.js → architectureDiagram-ZJ3FMSHR-C_31NGnA.js} +1 -1
- package/static/assets/{blockDiagram-677ZJIJ3-ByICFNDb.js → blockDiagram-677ZJIJ3-DDQQ2PAJ.js} +1 -1
- package/static/assets/{c4Diagram-LMCZKHZV-C5a0Ubby.js → c4Diagram-LMCZKHZV-BPbCZxQJ.js} +1 -1
- package/static/assets/channel-CufY_jLp.js +1 -0
- package/static/assets/{chunk-2Q5K7J3B-UucADWxr.js → chunk-2Q5K7J3B-B3LoONkc.js} +1 -1
- package/static/assets/{chunk-32BRIVSS-WAe17d3N.js → chunk-32BRIVSS-Buim75a9.js} +1 -1
- package/static/assets/{chunk-5VM5RSS4-BQBMOb0f.js → chunk-5VM5RSS4-BjHqxxbw.js} +1 -1
- package/static/assets/{chunk-EX3LRPZG-BzaET0n4.js → chunk-EX3LRPZG-Dn0_qUTW.js} +1 -1
- package/static/assets/{chunk-JWPE2WC7-D_tBvbZg.js → chunk-JWPE2WC7-DpYEF0BV.js} +1 -1
- package/static/assets/{chunk-MOJQB5TN-CsYb9JBc.js → chunk-MOJQB5TN-CbNDWijQ.js} +1 -1
- package/static/assets/{chunk-RYQCIY6F-D5xRlnc7.js → chunk-RYQCIY6F-DOhXYkGG.js} +1 -1
- package/static/assets/{chunk-V7JOEXUC-y0cEgq6c.js → chunk-V7JOEXUC-CfASUuSJ.js} +1 -1
- package/static/assets/{chunk-VR4S4FIN-CSXJHPXT.js → chunk-VR4S4FIN-vgVNnFtp.js} +1 -1
- package/static/assets/{chunk-XXDRQBXY-QFNFGLYZ.js → chunk-XXDRQBXY-CVjoYUTB.js} +1 -1
- package/static/assets/classDiagram-OUVF2IWQ-CVQD_QZa.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-CVQD_QZa.js +1 -0
- package/static/assets/{cose-bilkent-JH36ORCC-ValIntpZ.js → cose-bilkent-JH36ORCC-EqjuuBpY.js} +1 -1
- package/static/assets/{cynefin-VYW2F7L2-BgHVKyAR.js → cynefin-VYW2F7L2-DioyO_Cm.js} +1 -1
- package/static/assets/{cynefinDiagram-TSTJHNR4-LIZzpkLM.js → cynefinDiagram-TSTJHNR4-CuPa5A8w.js} +1 -1
- package/static/assets/{dagre-VKFMJZFB-DY9b36A5.js → dagre-VKFMJZFB-MIf1Dtbk.js} +1 -1
- package/static/assets/{diagram-FQU43EPY-aeuYUUxe.js → diagram-FQU43EPY-5OBJqg6R.js} +1 -1
- package/static/assets/{diagram-G47NLZAW-BWwcYYCF.js → diagram-G47NLZAW-B2ZGPKp3.js} +1 -1
- package/static/assets/{diagram-NH7WQ7WH-BObFlRPR.js → diagram-NH7WQ7WH-B5YEN-B-.js} +1 -1
- package/static/assets/{diagram-OA4YK3LP-BUnc8m1-.js → diagram-OA4YK3LP-BXg_CQ2m.js} +1 -1
- package/static/assets/{diagram-WEI45ONY-DsNxBIuJ.js → diagram-WEI45ONY-DVbqehF6.js} +1 -1
- package/static/assets/{ebnfDiagram-CCIWWBDH-DDL2Id1r.js → ebnfDiagram-CCIWWBDH-hmnXWJuZ.js} +1 -1
- package/static/assets/{erDiagram-Q63AITRT-DZZcG2QU.js → erDiagram-Q63AITRT-CLYATrvi.js} +1 -1
- package/static/assets/{flowDiagram-23GEKE2U-C6GvkBFb.js → flowDiagram-23GEKE2U-cs1YvuVH.js} +1 -1
- package/static/assets/{ganttDiagram-NO4QXBWP-C7ydCTnF.js → ganttDiagram-NO4QXBWP-CH5rfioY.js} +1 -1
- package/static/assets/{gitGraphDiagram-IHSO6WYX-C60P6bwf.js → gitGraphDiagram-IHSO6WYX-DjAOB5L7.js} +1 -1
- package/static/assets/{index-CfGMfAGO.js → index-B4Y8oBUt.js} +4 -4
- package/static/assets/{infoDiagram-FWYZ7A6U-CnH7Q2nX.js → infoDiagram-FWYZ7A6U-De2-PVxo.js} +1 -1
- package/static/assets/{ishikawaDiagram-FXEZZL3T-BNEvOQYX.js → ishikawaDiagram-FXEZZL3T-CDikxFR6.js} +1 -1
- package/static/assets/{journeyDiagram-5HDEW3XC-BOwGZTfX.js → journeyDiagram-5HDEW3XC-Bo6isp1w.js} +1 -1
- package/static/assets/{kanban-definition-HUTT4EX6-BEfHVl-l.js → kanban-definition-HUTT4EX6-jM1azdC7.js} +1 -1
- package/static/assets/{linear-BOHPNxBi.js → linear-CUk0_yzf.js} +1 -1
- package/static/assets/{mindmap-definition-LN4V7U3C-B84wfT_w.js → mindmap-definition-LN4V7U3C-l-cgg6PS.js} +1 -1
- package/static/assets/{pegDiagram-2B236MQR-BgEa7yRL.js → pegDiagram-2B236MQR-B-8nGU3G.js} +1 -1
- package/static/assets/{pieDiagram-ENE6RG2P-DgA9LQlc.js → pieDiagram-ENE6RG2P-Di1V86Ag.js} +1 -1
- package/static/assets/{quadrantDiagram-ABIIQ3AL-C5K_r0Mx.js → quadrantDiagram-ABIIQ3AL-BknVxapP.js} +1 -1
- package/static/assets/{railroadDiagram-RFXS5EU6-Ckh_mLXW.js → railroadDiagram-RFXS5EU6-D7Vohrlg.js} +1 -1
- package/static/assets/{requirementDiagram-TGXJPOKE-CtII7Obm.js → requirementDiagram-TGXJPOKE-BlFXj6Li.js} +1 -1
- package/static/assets/{sankeyDiagram-HTMAVEWB-DdkySwbL.js → sankeyDiagram-HTMAVEWB-Ck_A1wxA.js} +1 -1
- package/static/assets/{sequenceDiagram-DBY2YBRQ-DEIBg3kY.js → sequenceDiagram-DBY2YBRQ-D4mu07Ol.js} +1 -1
- package/static/assets/{sizeCapture-X5ZJPWSS-D2MM9f6M.js → sizeCapture-X5ZJPWSS-BwempBoZ.js} +1 -1
- package/static/assets/{stateDiagram-2N3HPSRC-DHmJsAte.js → stateDiagram-2N3HPSRC-DSh1YMwZ.js} +1 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-DrMkFhr2.js +1 -0
- package/static/assets/{swimlanes-5IMT3BWC-BBPvUQvZ.js → swimlanes-5IMT3BWC-4EE1KLA5.js} +2 -2
- package/static/assets/swimlanesDiagram-G3AALYLV-DoZQ3XaY.js +8 -0
- package/static/assets/{timeline-definition-FHXFAJF6-CG_Q0YTD.js → timeline-definition-FHXFAJF6-DVZepywq.js} +1 -1
- package/static/assets/{vennDiagram-L72KCM5P-DlFSe4is.js → vennDiagram-L72KCM5P-YqnYvNJT.js} +1 -1
- package/static/assets/{wardleyDiagram-EHGQE667-BTlvtp2f.js → wardleyDiagram-EHGQE667-Dc6PvPZE.js} +1 -1
- package/static/assets/{xychartDiagram-FW5EYKEG-Bm14zLxj.js → xychartDiagram-FW5EYKEG-DLbY5Kg1.js} +1 -1
- package/static/index.html +1 -1
- package/static/assets/channel-CrYjYK1f.js +0 -1
- package/static/assets/classDiagram-OUVF2IWQ-DoLCCqXD.js +0 -1
- package/static/assets/classDiagram-v2-EOCWNBFH-DoLCCqXD.js +0 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-B6sumoG6.js +0 -1
- package/static/assets/swimlanesDiagram-G3AALYLV-DH2ts1oT.js +0 -8
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.20",
|
|
6813
6813
|
description: "Compound Context Studio \u2014 self-hosted team context authoring for CO2 projects",
|
|
6814
6814
|
type: "module",
|
|
6815
6815
|
license: "MIT",
|
|
@@ -21875,6 +21875,7 @@ import { spawn as spawn2 } from "child_process";
|
|
|
21875
21875
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
21876
21876
|
import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
21877
21877
|
import { request as httpRequest2 } from "http";
|
|
21878
|
+
import { createServer as createNetServer } from "net";
|
|
21878
21879
|
import { join as join9 } from "path";
|
|
21879
21880
|
function svcError6(code, status) {
|
|
21880
21881
|
return Object.assign(new Error(code), { code, status });
|
|
@@ -21896,8 +21897,22 @@ process.once("SIGINT", () => {
|
|
|
21896
21897
|
killSpawnedApps();
|
|
21897
21898
|
process.exit(130);
|
|
21898
21899
|
});
|
|
21899
|
-
function
|
|
21900
|
-
|
|
21900
|
+
function persistGuardToken(db, projectId, token) {
|
|
21901
|
+
try {
|
|
21902
|
+
db.update(cicdSetups).set({ guardToken: token, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(cicdSetups.projectId, projectId)).run();
|
|
21903
|
+
} catch {
|
|
21904
|
+
}
|
|
21905
|
+
}
|
|
21906
|
+
function cicdGuardTokenValid(db, projectId, token) {
|
|
21907
|
+
if (!token) return false;
|
|
21908
|
+
const mem = guardTokens.get(projectId);
|
|
21909
|
+
if (mem !== void 0) return mem === token;
|
|
21910
|
+
const row = db.select({ t: cicdSetups.guardToken }).from(cicdSetups).where(eq(cicdSetups.projectId, projectId)).get();
|
|
21911
|
+
if (row?.t) {
|
|
21912
|
+
guardTokens.set(projectId, row.t);
|
|
21913
|
+
return row.t === token;
|
|
21914
|
+
}
|
|
21915
|
+
return false;
|
|
21901
21916
|
}
|
|
21902
21917
|
function resolveCicdApp(db, workspacePath, projectId) {
|
|
21903
21918
|
const project = db.select().from(projects).where(eq(projects.id, projectId)).get();
|
|
@@ -21930,6 +21945,49 @@ function probeApp(port, timeoutMs = 1e3) {
|
|
|
21930
21945
|
req.end();
|
|
21931
21946
|
});
|
|
21932
21947
|
}
|
|
21948
|
+
function portFree(port) {
|
|
21949
|
+
return new Promise((resolve5) => {
|
|
21950
|
+
const srv = createNetServer();
|
|
21951
|
+
srv.once("error", () => resolve5(false));
|
|
21952
|
+
srv.once("listening", () => srv.close(() => resolve5(true)));
|
|
21953
|
+
srv.listen(port, "127.0.0.1");
|
|
21954
|
+
});
|
|
21955
|
+
}
|
|
21956
|
+
function ephemeralPort() {
|
|
21957
|
+
return new Promise((resolve5) => {
|
|
21958
|
+
const srv = createNetServer();
|
|
21959
|
+
srv.once("error", () => resolve5(0));
|
|
21960
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
21961
|
+
const addr = srv.address();
|
|
21962
|
+
const p = addr && typeof addr === "object" ? addr.port : 0;
|
|
21963
|
+
srv.close(() => resolve5(p));
|
|
21964
|
+
});
|
|
21965
|
+
});
|
|
21966
|
+
}
|
|
21967
|
+
async function pickFreePort(preferred, avoid) {
|
|
21968
|
+
if (preferred && preferred !== avoid && await portFree(preferred)) return preferred;
|
|
21969
|
+
for (let i = 0; i < 20; i++) {
|
|
21970
|
+
const p = await ephemeralPort();
|
|
21971
|
+
if (p && p !== avoid) return p;
|
|
21972
|
+
}
|
|
21973
|
+
return preferred || DEFAULT_CICD_APP_PORT;
|
|
21974
|
+
}
|
|
21975
|
+
function persistConfigPort(location, newPort) {
|
|
21976
|
+
if (newPort === location.port) return;
|
|
21977
|
+
const configPath = join9(location.appDir, "cicd.config.json");
|
|
21978
|
+
let config = {};
|
|
21979
|
+
try {
|
|
21980
|
+
config = JSON.parse(readFileSync6(configPath, "utf8"));
|
|
21981
|
+
} catch {
|
|
21982
|
+
}
|
|
21983
|
+
config.port = newPort;
|
|
21984
|
+
try {
|
|
21985
|
+
writeFileSync5(configPath, `${JSON.stringify(config, null, 2)}
|
|
21986
|
+
`);
|
|
21987
|
+
} catch {
|
|
21988
|
+
}
|
|
21989
|
+
location.port = newPort;
|
|
21990
|
+
}
|
|
21933
21991
|
function toStatus2(location, running, projectId) {
|
|
21934
21992
|
return {
|
|
21935
21993
|
present: location.present,
|
|
@@ -21948,26 +22006,13 @@ async function getCicdAppStatus(db, workspacePath, projectId) {
|
|
|
21948
22006
|
async function startCicdApp(db, workspacePath, projectId, studioPort, port) {
|
|
21949
22007
|
const location = resolveCicdApp(db, workspacePath, projectId);
|
|
21950
22008
|
if (!location.present) throw svcError6("CICD_APP_MISSING", 404);
|
|
21951
|
-
const
|
|
21952
|
-
if (
|
|
21953
|
-
|
|
21954
|
-
let config = {};
|
|
21955
|
-
try {
|
|
21956
|
-
config = JSON.parse(readFileSync6(configPath, "utf8"));
|
|
21957
|
-
} catch {
|
|
21958
|
-
}
|
|
21959
|
-
config.port = targetPort;
|
|
21960
|
-
try {
|
|
21961
|
-
writeFileSync5(configPath, `${JSON.stringify(config, null, 2)}
|
|
21962
|
-
`);
|
|
21963
|
-
} catch {
|
|
21964
|
-
}
|
|
21965
|
-
location.port = targetPort;
|
|
21966
|
-
}
|
|
21967
|
-
if (await probeApp(targetPort)) {
|
|
21968
|
-
location.port = targetPort;
|
|
22009
|
+
const savedPort = port ?? location.port;
|
|
22010
|
+
if (await probeApp(savedPort)) {
|
|
22011
|
+
persistConfigPort(location, savedPort);
|
|
21969
22012
|
return toStatus2(location, true, projectId);
|
|
21970
22013
|
}
|
|
22014
|
+
const targetPort = port !== void 0 ? port : await pickFreePort(savedPort, studioPort);
|
|
22015
|
+
persistConfigPort(location, targetPort);
|
|
21971
22016
|
const existing = spawnedApps.get(projectId);
|
|
21972
22017
|
if (existing && existing.exitCode === null) {
|
|
21973
22018
|
try {
|
|
@@ -21993,20 +22038,22 @@ async function startCicdApp(db, workspacePath, projectId, studioPort, port) {
|
|
|
21993
22038
|
child.on("error", () => {
|
|
21994
22039
|
spawnedApps.delete(projectId);
|
|
21995
22040
|
guardTokens.delete(projectId);
|
|
22041
|
+
persistGuardToken(db, projectId, null);
|
|
21996
22042
|
});
|
|
21997
22043
|
child.on("exit", () => {
|
|
21998
22044
|
spawnedApps.delete(projectId);
|
|
21999
22045
|
guardTokens.delete(projectId);
|
|
22046
|
+
persistGuardToken(db, projectId, null);
|
|
22000
22047
|
});
|
|
22001
22048
|
spawnedApps.set(projectId, child);
|
|
22002
|
-
db.update(cicdSetups).set({ appPort: location.port, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(cicdSetups.projectId, projectId)).run();
|
|
22049
|
+
db.update(cicdSetups).set({ appPort: location.port, guardToken: token, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(cicdSetups.projectId, projectId)).run();
|
|
22003
22050
|
for (let attempt = 0; attempt < 20; attempt++) {
|
|
22004
22051
|
if (await probeApp(location.port, 500)) return toStatus2(location, true, projectId);
|
|
22005
22052
|
await new Promise((r) => setTimeout(r, 250));
|
|
22006
22053
|
}
|
|
22007
22054
|
return toStatus2(location, false, projectId);
|
|
22008
22055
|
}
|
|
22009
|
-
function stopCicdApp(projectId) {
|
|
22056
|
+
function stopCicdApp(db, projectId) {
|
|
22010
22057
|
const child = spawnedApps.get(projectId);
|
|
22011
22058
|
if (!child) return false;
|
|
22012
22059
|
try {
|
|
@@ -22015,6 +22062,7 @@ function stopCicdApp(projectId) {
|
|
|
22015
22062
|
}
|
|
22016
22063
|
spawnedApps.delete(projectId);
|
|
22017
22064
|
guardTokens.delete(projectId);
|
|
22065
|
+
persistGuardToken(db, projectId, null);
|
|
22018
22066
|
return true;
|
|
22019
22067
|
}
|
|
22020
22068
|
|
|
@@ -22701,6 +22749,7 @@ function pollCicdTracking(db, workspacePath, setupId, kind, notifyUserId) {
|
|
|
22701
22749
|
relatedEntityType: "cicd_setup",
|
|
22702
22750
|
relatedEntityId: setupId
|
|
22703
22751
|
});
|
|
22752
|
+
stopCicdApp(db, setup.projectId);
|
|
22704
22753
|
try {
|
|
22705
22754
|
runProjectGit(
|
|
22706
22755
|
projectDir2,
|
|
@@ -22854,7 +22903,7 @@ function cicdRoutes(db, workspacePath, studioPort) {
|
|
|
22854
22903
|
const user = c.get("user");
|
|
22855
22904
|
try {
|
|
22856
22905
|
const result = requestPlan(db, workspacePath, projectId, user.id);
|
|
22857
|
-
stopCicdApp(projectId);
|
|
22906
|
+
stopCicdApp(db, projectId);
|
|
22858
22907
|
return c.json(result, 201);
|
|
22859
22908
|
} catch (e) {
|
|
22860
22909
|
return errorResponse3(c, e);
|
|
@@ -22910,7 +22959,7 @@ function cicdRoutes(db, workspacePath, studioPort) {
|
|
|
22910
22959
|
const projectId = parseProjectId(c);
|
|
22911
22960
|
if (projectId === null) return c.json({ code: "INVALID_ID" }, 400);
|
|
22912
22961
|
try {
|
|
22913
|
-
stopCicdApp(projectId);
|
|
22962
|
+
stopCicdApp(db, projectId);
|
|
22914
22963
|
return c.json(await getCicdAppStatus(db, workspacePath, projectId));
|
|
22915
22964
|
} catch (e) {
|
|
22916
22965
|
return errorResponse3(c, e);
|
|
@@ -22943,7 +22992,7 @@ function cicdGuardRoutes(db, workspacePath) {
|
|
|
22943
22992
|
if (Number.isNaN(projectId)) return c.json({ code: "INVALID_ID" }, 400);
|
|
22944
22993
|
const bearer = c.req.header("authorization")?.replace(/^Bearer\s+/i, "");
|
|
22945
22994
|
const token = c.req.query("token") ?? bearer;
|
|
22946
|
-
if (!cicdGuardTokenValid(projectId, token) && !c.get("user")) {
|
|
22995
|
+
if (!cicdGuardTokenValid(db, projectId, token) && !c.get("user")) {
|
|
22947
22996
|
return c.json({ code: "UNAUTHORIZED" }, 401);
|
|
22948
22997
|
}
|
|
22949
22998
|
const rawVersion = c.req.query("versionId");
|
|
@@ -25518,7 +25567,7 @@ function isPendingForConductor(s, conductorType) {
|
|
|
25518
25567
|
if (conductorType === "prepare") return s.prepareStatus !== "completed";
|
|
25519
25568
|
if (conductorType === "develop")
|
|
25520
25569
|
return s.prepareStatus === "completed" && s.developStatus !== "completed";
|
|
25521
|
-
return s.developStatus === "completed"
|
|
25570
|
+
return s.developStatus === "completed";
|
|
25522
25571
|
}
|
|
25523
25572
|
function getVersionAffectedAppIds(db, projectId, prdVersionId) {
|
|
25524
25573
|
const version2 = db.select({ id: prdVersions.id, semver: prdVersions.semver }).from(prdVersions).where(and(eq(prdVersions.id, prdVersionId), eq(prdVersions.projectId, projectId))).limit(1).get();
|
|
@@ -25568,7 +25617,7 @@ function getVersionAffectedAppIds(db, projectId, prdVersionId) {
|
|
|
25568
25617
|
}
|
|
25569
25618
|
function getPendingApps(db, projectId, conductorType, prdVersionId) {
|
|
25570
25619
|
const apps = db.select({ id: applications.id, name: applications.name }).from(applications).where(and(eq(applications.projectId, projectId), eq(applications.isDeleted, 0))).all();
|
|
25571
|
-
const affected = prdVersionId === void 0 ? null : getVersionAffectedAppIds(db, projectId, prdVersionId);
|
|
25620
|
+
const affected = prdVersionId === void 0 || conductorType === "quality" ? null : getVersionAffectedAppIds(db, projectId, prdVersionId);
|
|
25572
25621
|
const result = [];
|
|
25573
25622
|
for (const app of apps) {
|
|
25574
25623
|
if (affected !== null && !affected.has(app.id)) continue;
|
|
@@ -25581,7 +25630,7 @@ function getPendingApps(db, projectId, conductorType, prdVersionId) {
|
|
|
25581
25630
|
}
|
|
25582
25631
|
function getPhaseCompletion(db, projectId, conductorType, prdVersionId) {
|
|
25583
25632
|
const allApps = db.select({ id: applications.id }).from(applications).where(and(eq(applications.projectId, projectId), eq(applications.isDeleted, 0))).all();
|
|
25584
|
-
const affected = prdVersionId === void 0 ? null : getVersionAffectedAppIds(db, projectId, prdVersionId);
|
|
25633
|
+
const affected = prdVersionId === void 0 || conductorType === "quality" ? null : getVersionAffectedAppIds(db, projectId, prdVersionId);
|
|
25585
25634
|
const apps = affected === null ? allApps : allApps.filter((a) => affected.has(a.id));
|
|
25586
25635
|
let completed = 0;
|
|
25587
25636
|
for (const app of apps) {
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"name": "co2-skills",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Compound Context Skills — generates data models, HTML mockups, technical specifications, test specifications, and orchestrates full-stack development from Agile user stories.",
|
|
15
|
-
"version": "1.
|
|
15
|
+
"version": "1.8.2",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "Rashidee M.Rashid"
|
|
18
18
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "co2-skills",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "Compound Context Skills — an opinionated context engineering methodology for AI-driven software development. Generates data models, HTML mockups, technical specifications, test specifications, and orchestrates full-stack development from Agile user stories.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rashidee M.Rashid"
|
|
@@ -66,10 +66,13 @@ Write `cicd/CICD_GENERATION.md`:
|
|
|
66
66
|
Read `references/cicd-app-template.md` for the complete file templates. Produce:
|
|
67
67
|
|
|
68
68
|
1. `cicd/app/cicd.config.json` — from the plan's tables:
|
|
69
|
-
`{ "port": <CI/CD app port>, "docker": { "registry" } | null, "apps": [{ "name", "folder",
|
|
69
|
+
`{ "port": <CI/CD app port>, "generatedAt": "<current ISO-8601 UTC>", "docker": { "registry" } | null, "apps": [{ "name", "folder",
|
|
70
70
|
"method", "port", "build", "start", "stop", "deployMethod", "deploy", "health", "image",
|
|
71
71
|
"dockerfile", "configs" }], "infra": [{ "id", "name",
|
|
72
|
-
"host", "port", "url", "description" }] }` — `
|
|
72
|
+
"host", "port", "url", "description" }] }` — `generatedAt` MUST be the actual time you generate
|
|
73
|
+
this file (a fresh value on EVERY run — never copy a previous one): the running server pins the
|
|
74
|
+
value it booted with and shows a "restart required" banner when a later regeneration moves it
|
|
75
|
+
forward, so an in-place-replaced-but-still-running app is self-diagnosing. `docker.registry` comes from the plan's
|
|
73
76
|
`## Docker` section (`null` when the plan says not dockerized); per-app `image` is that
|
|
74
77
|
section's Image column (`null` for non-dockerized apps) and `dockerfile` is
|
|
75
78
|
`{ "path": "Dockerfile", "content": "<body>" }` for a `managed` Dockerfile from the plan's
|
|
@@ -41,6 +41,11 @@ const PORT = Number(process.env.PORT || loadConfig().port || 5000)
|
|
|
41
41
|
const STUDIO = process.env.CO2_STUDIO_URL || 'http://127.0.0.1:3001'
|
|
42
42
|
const PROJECT_ID = process.env.CO2_PROJECT_ID || ''
|
|
43
43
|
const TOKEN = process.env.CO2_GUARD_TOKEN || ''
|
|
44
|
+
// Defect 1 — the build id stamped into cicd.config.json at generation. Pinned at boot; when a
|
|
45
|
+
// regeneration rewrites the config with a newer value while this process keeps running, /api/state
|
|
46
|
+
// reports the drift and the page shows a "running older code — restart" banner. Missing (older
|
|
47
|
+
// configs) → null → never a false positive.
|
|
48
|
+
const BOOT_GENERATED_AT = loadConfig().generatedAt || null
|
|
44
49
|
|
|
45
50
|
mkdirSync(LOG_DIR, { recursive: true })
|
|
46
51
|
const running = new Map() // app name -> ChildProcess (only processes THIS app spawned)
|
|
@@ -63,27 +68,60 @@ function currentBranch() {
|
|
|
63
68
|
|
|
64
69
|
// ---- Develop gate (Compound Context Studio) --------------------------------------------
|
|
65
70
|
// An application that never completed a conductor-develop run has nothing to build or run —
|
|
66
|
-
// its tab controls are disabled.
|
|
67
|
-
//
|
|
68
|
-
|
|
71
|
+
// its tab controls are disabled. The studio build guard is authoritative; when it is
|
|
72
|
+
// unreachable we fall back to the on-disk develop tracking file, and ONLY a definitive
|
|
73
|
+
// "never developed" (false) disables controls — unknown fails OPEN (Defect 2c/4).
|
|
74
|
+
//
|
|
75
|
+
// Identity differs across the two sides: the studio keys by the CLAUDE.md application name
|
|
76
|
+
// (e.g. `skolafund-web`), while the plan/config carry a display name (`SkolaFund Web`) and a
|
|
77
|
+
// folder (`skolafund_web`). Match on a separator-insensitive key so all three collapse to the
|
|
78
|
+
// same value (Defect 2b) — exact-lowercase matching silently missed every app.
|
|
79
|
+
function norm(s) { return String(s == null ? '' : s).toLowerCase().replace(/[^a-z0-9]+/g, '') }
|
|
80
|
+
let guardApps = null // Map norm(name) -> developedOnce (last good answer, kept across blips)
|
|
81
|
+
// ok | unauthorized | unreachable | http-<code> | not-configured | init — surfaced in the notice
|
|
82
|
+
// so a disabled/unknown control says WHAT actually happened instead of a hard-coded guess.
|
|
83
|
+
let guardStatus = PROJECT_ID ? 'init' : 'not-configured'
|
|
69
84
|
async function refreshGuard() {
|
|
70
|
-
if (!PROJECT_ID) return
|
|
85
|
+
if (!PROJECT_ID) { guardStatus = 'not-configured'; return }
|
|
71
86
|
try {
|
|
72
87
|
const r = await fetch(`${STUDIO}/api/cicd-guard/${PROJECT_ID}?token=${TOKEN}`, { signal: AbortSignal.timeout(3000) })
|
|
73
|
-
if (
|
|
88
|
+
if (r.status === 401) { guardStatus = 'unauthorized'; return } // keep last good guardApps
|
|
89
|
+
if (!r.ok) { guardStatus = 'http-' + r.status; return }
|
|
74
90
|
const g = await r.json()
|
|
75
91
|
if (Array.isArray(g.applications)) {
|
|
76
|
-
|
|
77
|
-
for (const a of g.applications)
|
|
92
|
+
const next = new Map()
|
|
93
|
+
for (const a of g.applications) next.set(norm(a.name), !!a.developedOnce)
|
|
94
|
+
guardApps = next
|
|
95
|
+
guardStatus = 'ok'
|
|
78
96
|
}
|
|
79
|
-
} catch {}
|
|
97
|
+
} catch { guardStatus = 'unreachable' } // keep last good guardApps
|
|
98
|
+
}
|
|
99
|
+
// On-disk fallback (Defect 2 fix 3) — conductor-feature-develop writes this app's
|
|
100
|
+
// context/develop/IMPLEMENTATION_MASTER.md with `**Status**: COMPLETED` when development
|
|
101
|
+
// finishes. Read it (mtime-cached) so the gate is correct even with the studio down, restarted
|
|
102
|
+
// or renamed. Mirrors the studio's TRACKING_FILE_MAP.develop.
|
|
103
|
+
const developMarkerCache = new Map() // abs path -> { mtimeMs, done }
|
|
104
|
+
function localDeveloped(app) {
|
|
105
|
+
const p = join(PROJECT_DIR, app.folder || '', 'context', 'develop', 'IMPLEMENTATION_MASTER.md')
|
|
106
|
+
try {
|
|
107
|
+
const mtimeMs = statSync(p).mtimeMs
|
|
108
|
+
const hit = developMarkerCache.get(p)
|
|
109
|
+
if (hit && hit.mtimeMs === mtimeMs) return hit.done
|
|
110
|
+
const done = /^\s*\**\s*Status\s*\**\s*:\s*COMPLETED\b/im.test(readFileSync(p, 'utf8'))
|
|
111
|
+
developMarkerCache.set(p, { mtimeMs, done })
|
|
112
|
+
return done
|
|
113
|
+
} catch { return false }
|
|
80
114
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
// { val: true|false|null, src: 'studio'|'local'|'none' }. Studio wins; local marker fills the gap
|
|
116
|
+
// when the studio has no answer; null only when nobody knows (controls then stay ENABLED).
|
|
117
|
+
function developInfo(app) {
|
|
118
|
+
if (guardApps) {
|
|
119
|
+
let v = guardApps.get(norm(app.name))
|
|
120
|
+
if (v === undefined) v = guardApps.get(norm(app.folder))
|
|
121
|
+
if (v !== undefined) return { val: v, src: 'studio' }
|
|
122
|
+
}
|
|
123
|
+
if (localDeveloped(app)) return { val: true, src: 'local' }
|
|
124
|
+
return { val: null, src: 'none' }
|
|
87
125
|
}
|
|
88
126
|
refreshGuard()
|
|
89
127
|
setInterval(refreshGuard, 30_000).unref?.()
|
|
@@ -140,7 +178,11 @@ function runLogged(app, command, onExit) {
|
|
|
140
178
|
const log = createWriteStream(logPath(app.name), { flags: 'a' })
|
|
141
179
|
log.write(`\n--- ${new Date().toISOString()} $ ${command}\n`)
|
|
142
180
|
broadcast(app.name, `\n--- ${new Date().toISOString()} $ ${command}\n`)
|
|
143
|
-
|
|
181
|
+
// detached on POSIX so the shell AND everything it forks (a compound `a && b` start command,
|
|
182
|
+
// pnpm → vite, etc.) share ONE process group we can signal as a unit on Stop. Without it, `sh`
|
|
183
|
+
// running `a && b` cannot exec into `b`, so killing the shell alone leaves the real server
|
|
184
|
+
// reparented to init and still holding the port (Defect 3). Windows kills the tree via taskkill.
|
|
185
|
+
const child = spawn(command, { cwd: join(PROJECT_DIR, app.folder), shell: true, env: process.env, detached: process.platform !== 'win32' })
|
|
144
186
|
const tap = (d) => { log.write(d); broadcast(app.name, d) }
|
|
145
187
|
child.stdout.on('data', tap)
|
|
146
188
|
child.stderr.on('data', tap)
|
|
@@ -282,20 +324,27 @@ function startApp(app) {
|
|
|
282
324
|
saveState()
|
|
283
325
|
}
|
|
284
326
|
|
|
285
|
-
|
|
286
|
-
//
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
327
|
+
const delay = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
328
|
+
// A signal-0 probe: alive if it succeeds OR fails with EPERM (exists, not ours to signal).
|
|
329
|
+
function pidAlive(pid) { try { process.kill(Number(pid), 0); return true } catch (e) { return e.code === 'EPERM' } }
|
|
330
|
+
|
|
331
|
+
// shell:true (and a compound start command) spawns a tree whose grandchildren survive a bare
|
|
332
|
+
// child.kill(). Signal the whole PROCESS GROUP (detached → pgid === the shell's pid), escalating
|
|
333
|
+
// SIGTERM → SIGKILL. Windows: taskkill /T /F kills the tree. Async — the caller awaits it.
|
|
334
|
+
async function killTree(child) {
|
|
335
|
+
const pid = child && child.pid
|
|
336
|
+
if (!pid) return
|
|
337
|
+
if (process.platform === 'win32') {
|
|
338
|
+
try { execFileSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' }) } catch {}
|
|
339
|
+
return
|
|
292
340
|
}
|
|
341
|
+
try { process.kill(-pid, 'SIGTERM') } catch { try { child.kill('SIGTERM') } catch {} }
|
|
342
|
+
for (let i = 0; i < 40 && pidAlive(pid); i++) await delay(100) // up to ~4s for a graceful exit
|
|
343
|
+
if (pidAlive(pid)) { try { process.kill(-pid, 'SIGKILL') } catch { try { child.kill('SIGKILL') } catch {} } }
|
|
293
344
|
}
|
|
294
345
|
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
// for any running app, not only ones this CI/CD app spawned. Returns true if a PID was killed.
|
|
298
|
-
function killByPort(port) {
|
|
346
|
+
// PIDs listening on a TCP port. POSIX: lsof, then fuser — both tolerated absent. Windows: netstat.
|
|
347
|
+
function portListeners(port) {
|
|
299
348
|
try {
|
|
300
349
|
if (process.platform === 'win32') {
|
|
301
350
|
const out = execFileSync('netstat', ['-ano', '-p', 'tcp'], { encoding: 'utf8' })
|
|
@@ -304,36 +353,60 @@ function killByPort(port) {
|
|
|
304
353
|
const m = line.match(/:(\d+)\s+\S+\s+LISTENING\s+(\d+)/)
|
|
305
354
|
if (m && Number(m[1]) === Number(port)) pids.add(m[2])
|
|
306
355
|
}
|
|
307
|
-
|
|
308
|
-
try { execFileSync('taskkill', ['/pid', pid, '/T', '/F'], { stdio: 'ignore' }) } catch {}
|
|
309
|
-
}
|
|
310
|
-
return pids.size > 0
|
|
356
|
+
return [...pids]
|
|
311
357
|
}
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
358
|
+
for (const cmd of [`lsof -ti tcp:${port}`, `fuser -n tcp ${port} 2>/dev/null`]) {
|
|
359
|
+
const out = execFileSync('sh', ['-c', `${cmd} || true`], { encoding: 'utf8' })
|
|
360
|
+
const pids = [...new Set(out.split(/\s+/).map((s) => s.trim()).filter((s) => /^\d+$/.test(s)))]
|
|
361
|
+
if (pids.length) return pids
|
|
362
|
+
}
|
|
363
|
+
} catch {}
|
|
364
|
+
return []
|
|
365
|
+
}
|
|
366
|
+
// Signal a pid's whole process group (falls back to the bare pid if the group is already gone).
|
|
367
|
+
function killGroup(pid, sig) {
|
|
368
|
+
let pgid = 0
|
|
369
|
+
if (process.platform !== 'win32') {
|
|
370
|
+
try { pgid = Number(execFileSync('ps', ['-o', 'pgid=', '-p', String(pid)], { encoding: 'utf8' }).trim()) } catch {}
|
|
371
|
+
}
|
|
372
|
+
try { process.kill(pgid ? -pgid : Number(pid), sig) } catch { try { process.kill(Number(pid), sig) } catch {} }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Stop whatever is listening on a port even when we hold no child handle for it (started outside
|
|
376
|
+
// this app, or before this app last restarted) — group-kill each listener, escalating
|
|
377
|
+
// SIGTERM → SIGKILL. Returns true when the port ended up FREE.
|
|
378
|
+
async function killByPort(port) {
|
|
379
|
+
let pids = portListeners(port)
|
|
380
|
+
if (!pids.length) return true // nothing listening — already free
|
|
381
|
+
if (process.platform === 'win32') {
|
|
382
|
+
for (const pid of pids) { try { execFileSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' }) } catch {} }
|
|
383
|
+
for (let i = 0; i < 20 && portListeners(port).length; i++) await delay(100)
|
|
384
|
+
return portListeners(port).length === 0
|
|
385
|
+
}
|
|
386
|
+
for (const pid of pids) killGroup(pid, 'SIGTERM')
|
|
387
|
+
for (let i = 0; i < 30 && portListeners(port).length; i++) await delay(100) // ~3s grace
|
|
388
|
+
for (const pid of portListeners(port)) killGroup(pid, 'SIGKILL')
|
|
389
|
+
await delay(200)
|
|
390
|
+
return portListeners(port).length === 0
|
|
317
391
|
}
|
|
318
392
|
|
|
319
|
-
function stopApp(app) {
|
|
393
|
+
async function stopApp(app) {
|
|
320
394
|
if (app.method === 'container' && app.stop && app.stop !== 'process-kill') {
|
|
321
395
|
const stopCmd = app.stop
|
|
322
396
|
try { execFileSync(stopCmd.split(' ')[0], stopCmd.split(' ').slice(1), { cwd: PROJECT_DIR }) } catch {}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
stopping.add(app.name)
|
|
327
|
-
killTree(child)
|
|
328
|
-
} else {
|
|
329
|
-
// No child handle — kill by port so an externally/previously started app still stops.
|
|
330
|
-
lastError.delete(app.name)
|
|
331
|
-
if (!killByPort(app.port)) {
|
|
332
|
-
lastError.set(app.name, `could not stop — nothing found listening on port ${app.port}`)
|
|
333
|
-
}
|
|
334
|
-
}
|
|
397
|
+
running.delete(app.name)
|
|
398
|
+
saveState()
|
|
399
|
+
return
|
|
335
400
|
}
|
|
401
|
+
lastError.delete(app.name)
|
|
402
|
+
const child = running.get(app.name)
|
|
403
|
+
if (child) { stopping.add(app.name); await killTree(child) }
|
|
336
404
|
running.delete(app.name)
|
|
405
|
+
// ALWAYS sweep the port after killing our handle — the child dying is NOT proof the server did
|
|
406
|
+
// (its reparented grandchildren, or an app started outside this app, can still hold the port).
|
|
407
|
+
// Report an honest failure if it never frees, instead of the old silent "success" (Defect 3).
|
|
408
|
+
const free = await killByPort(app.port)
|
|
409
|
+
if (!free) lastError.set(app.name, `stop failed — port ${app.port} is still in use`)
|
|
337
410
|
saveState()
|
|
338
411
|
}
|
|
339
412
|
|
|
@@ -360,6 +433,7 @@ button{background:#fff;color:#1c2128;border:1px solid #c6cdd6;border-radius:6px;
|
|
|
360
433
|
button:hover:enabled{background:#eef1f5}
|
|
361
434
|
button:disabled{opacity:.4;cursor:default}
|
|
362
435
|
.notice{padding:4px 12px;font-size:11px;color:#8a5a00;background:#fff7e6;display:none}
|
|
436
|
+
.stale{padding:6px 12px;font-size:12px;color:#fff;background:#c23b3b;font-weight:600;display:none}
|
|
363
437
|
.err{padding:4px 12px;font-size:11px;color:#c23b3b;display:none;white-space:pre-wrap}
|
|
364
438
|
#log{flex:1;margin:0;padding:10px 12px;background:#0f1216;color:#d7dde4;font-size:11px;line-height:1.5;overflow:auto;white-space:pre-wrap}
|
|
365
439
|
.muted{color:#5b6572;font-size:12px}
|
|
@@ -378,6 +452,7 @@ button:disabled{opacity:.4;cursor:default}
|
|
|
378
452
|
</style></head><body>
|
|
379
453
|
<div class="top"><strong>CI/CD</strong><span id="branch" class="meta"></span>
|
|
380
454
|
<span style="flex:1"></span><a id="infralink" class="url" href="infra" target="_blank" rel="noreferrer" style="display:none">Infrastructure ↗</a></div>
|
|
455
|
+
<div class="stale" id="stale"></div>
|
|
381
456
|
<div class="tabs" id="tabs"></div>
|
|
382
457
|
<div class="bar" id="bar"></div>
|
|
383
458
|
<div class="notice" id="notice"></div>
|
|
@@ -403,11 +478,19 @@ function renderTabs(){
|
|
|
403
478
|
<span class="dot \${a.phase}"></span>\${a.name}\${a.developedOnce===false?' 🔒':''}</div>\`).join('')
|
|
404
479
|
}
|
|
405
480
|
|
|
481
|
+
// A guard answer we could not get, said plainly — the notice names the cause it observed instead
|
|
482
|
+
// of always blaming an unreachable studio (Defect 2c/4).
|
|
483
|
+
function guardReason(s){
|
|
484
|
+
return ({unauthorized:'studio auth failed',unreachable:'studio unreachable','not-configured':'studio link not set',init:'checking…',ok:'not listed by the studio'})[s]
|
|
485
|
+
|| ('studio error '+String(s||'').replace(/^http-/,''))
|
|
486
|
+
}
|
|
406
487
|
function renderBar(){
|
|
407
488
|
const a = appOf(active)
|
|
408
489
|
if (!a) return
|
|
409
490
|
const url = 'http://'+location.hostname+':'+a.port+'/'
|
|
410
|
-
|
|
491
|
+
// Fail OPEN — only a definitive "never developed" (false) disables controls; unknown (null)
|
|
492
|
+
// leaves them enabled so an unverifiable studio never locks the user out (Defect 2c/4).
|
|
493
|
+
const gate = a.developedOnce !== false
|
|
411
494
|
const busy = a.phase==='building'||a.phase==='deploying'
|
|
412
495
|
document.getElementById('bar').innerHTML = \`
|
|
413
496
|
<span class="badge \${a.phase}">\${a.phase}\${a.external?' (external)':''}</span>
|
|
@@ -424,7 +507,10 @@ function renderBar(){
|
|
|
424
507
|
notice.textContent = 'This application has not been developed through the workflow yet — run conductor-feature-develop first.'
|
|
425
508
|
notice.style.display = 'block'
|
|
426
509
|
} else if (a.developedOnce === null) {
|
|
427
|
-
notice.textContent = 'Develop status
|
|
510
|
+
notice.textContent = 'Develop status unverified (' + guardReason(state.guardStatus) + ') — controls left enabled.'
|
|
511
|
+
notice.style.display = 'block'
|
|
512
|
+
} else if (a.developSource === 'local') {
|
|
513
|
+
notice.textContent = 'Develop status read from this app’s local tracking file (studio guard unavailable).'
|
|
428
514
|
notice.style.display = 'block'
|
|
429
515
|
} else notice.style.display = 'none'
|
|
430
516
|
const err = document.getElementById('err')
|
|
@@ -494,6 +580,10 @@ async function refresh(){
|
|
|
494
580
|
state = s
|
|
495
581
|
document.getElementById('branch').textContent = s.currentBranch ? 'branch: '+s.currentBranch : ''
|
|
496
582
|
document.getElementById('infralink').style.display = s.hasInfra ? 'inline' : 'none'
|
|
583
|
+
// Defect 1 — the app was regenerated in place while this process kept serving the old code.
|
|
584
|
+
const stale = document.getElementById('stale')
|
|
585
|
+
if (s.regenerated) { stale.textContent = 'This CI/CD app is running older code than was generated — restart it (Stop, then Start).'; stale.style.display = 'block' }
|
|
586
|
+
else stale.style.display = 'none'
|
|
497
587
|
if (!active && s.apps.length) setActive(s.apps[0].name)
|
|
498
588
|
renderTabs(); renderBar()
|
|
499
589
|
}
|
|
@@ -571,18 +661,24 @@ const server = http.createServer(async (req, res) => {
|
|
|
571
661
|
const apps = await Promise.all(loadConfig().apps.map(async (a) => {
|
|
572
662
|
const healthy = await probe(a)
|
|
573
663
|
const phase = activity.get(a.name) || (healthy ? 'running' : failed.has(a.name) ? 'failed' : 'stopped')
|
|
664
|
+
const dev = developInfo(a)
|
|
574
665
|
return {
|
|
575
666
|
name: a.name, method: a.method, port: a.port, phase,
|
|
576
667
|
deploy: !!a.deploy, deployMethod: a.deployMethod || 'none',
|
|
577
668
|
image: a.image || null,
|
|
578
669
|
external: healthy && !running.has(a.name),
|
|
579
|
-
developedOnce:
|
|
670
|
+
developedOnce: dev.val, developSource: dev.src,
|
|
580
671
|
error: lastError.get(a.name) || null,
|
|
581
672
|
}
|
|
582
673
|
}))
|
|
674
|
+
// Defect 1 — regenerated in place while still running the old code (config's generatedAt
|
|
675
|
+
// moved past the value pinned at boot).
|
|
676
|
+
const nowGen = loadConfig().generatedAt || null
|
|
583
677
|
return json(res, 200, {
|
|
584
678
|
currentBranch: currentBranch(),
|
|
585
679
|
hasInfra: (loadConfig().infra || []).length > 0,
|
|
680
|
+
guardStatus,
|
|
681
|
+
regenerated: !!(BOOT_GENERATED_AT && nowGen !== BOOT_GENERATED_AT),
|
|
586
682
|
apps,
|
|
587
683
|
})
|
|
588
684
|
}
|
|
@@ -597,13 +693,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
597
693
|
if (m && req.method === 'POST') {
|
|
598
694
|
const app = loadConfig().apps.find((a) => a.name === decodeURIComponent(m[1]))
|
|
599
695
|
if (!app) return json(res, 404, { error: 'unknown app' })
|
|
600
|
-
|
|
696
|
+
// Only a DEFINITIVE "never developed" blocks — unknown (null) fails open (Defect 2c/4).
|
|
697
|
+
if (m[2] !== 'stop' && developInfo(app).val === false) {
|
|
601
698
|
return json(res, 409, { error: 'application has not completed a develop workflow run' })
|
|
602
699
|
}
|
|
603
700
|
if (m[2] === 'build') buildApp(app)
|
|
604
701
|
else if (m[2] === 'start') startApp(app)
|
|
605
702
|
else if (m[2] === 'deploy') deployApp(app)
|
|
606
|
-
else stopApp(app)
|
|
703
|
+
else await stopApp(app)
|
|
607
704
|
return json(res, 200, { ok: true })
|
|
608
705
|
}
|
|
609
706
|
json(res, 404, { error: 'not found' })
|
|
@@ -642,6 +739,10 @@ server.listen(PORT, () => console.log(`cicd app on :${PORT}`))
|
|
|
642
739
|
|
|
643
740
|
`cicd/app/cicd.config.json` shape — one `apps` entry per application from the plan's Deployment
|
|
644
741
|
Decisions table, one `infra` entry per row of the plan's `## Infrastructure` table.
|
|
742
|
+
Top-level `"generatedAt"` is an ISO-8601 UTC timestamp **stamped fresh on every generation**
|
|
743
|
+
(Defect 1): the running server pins the value it booted with and flags a "restart required" banner
|
|
744
|
+
once a regeneration moves it forward, so a stale in-place-replaced process is self-diagnosing. It
|
|
745
|
+
MUST change on each run — use the actual generation time (e.g. `new Date().toISOString()`).
|
|
645
746
|
`deployMethod`/`deploy` come from the plan's Deploy columns (`deploy` is `null` when the method
|
|
646
747
|
is `none`). `infra[].url` is `null` for a headless service (Console URL cell `-`); emit
|
|
647
748
|
`"infra": []` when the plan declares no supporting services.
|
|
@@ -669,6 +770,7 @@ verbatim (this is a local dev CI/CD app).
|
|
|
669
770
|
```json
|
|
670
771
|
{
|
|
671
772
|
"port": 5000,
|
|
773
|
+
"generatedAt": "2026-07-24T09:15:00.000Z",
|
|
672
774
|
"docker": { "registry": "registry.example.com/team" },
|
|
673
775
|
"apps": [
|
|
674
776
|
{
|
|
@@ -676,7 +778,7 @@ verbatim (this is a local dev CI/CD app).
|
|
|
676
778
|
"folder": "my-app",
|
|
677
779
|
"method": "process",
|
|
678
780
|
"port": 3000,
|
|
679
|
-
"build": "pnpm install && pnpm build && docker build -t registry.example.com/team/my-app:latest .",
|
|
781
|
+
"build": "rm -rf dist && pnpm install && pnpm build && docker build --no-cache -t registry.example.com/team/my-app:latest .",
|
|
680
782
|
"start": "node dist/index.js",
|
|
681
783
|
"stop": "process-kill",
|
|
682
784
|
"deployMethod": "kubernetes",
|