@rashidee/co2 1.2.5 → 1.3.1
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 +17149 -14962
- package/drizzle/0011_workflow_history_v152.sql +5 -0
- package/drizzle/0012_prd_sections_v160.sql +14 -0
- package/drizzle/0013_brainstorm_sessions_v170.sql +10 -0
- package/drizzle/0014_bug_module_v180.sql +4 -0
- package/drizzle/0015_project_config_v190.sql +23 -0
- package/drizzle/0016_cicd_v2100.sql +22 -0
- package/drizzle/meta/_journal.json +42 -0
- package/package.json +1 -1
- package/plugin/README.md +2 -0
- package/plugin/SKILLS.md +414 -350
- package/plugin/skills/conductor-feature-develop/SKILL.md +1 -1
- package/plugin/skills/conductor-feature-prepare/SKILL.md +1 -1
- package/plugin/skills/depgen-k8s/SKILL.md +1 -1
- package/plugin/skills/tracegen-matrix/SKILL.md +1 -1
- package/plugin/skills/util-gencicdscript/SKILL.md +91 -0
- package/plugin/skills/util-gencicdscript/references/cicd-app-template.md +414 -0
- package/plugin/skills/util-plancicd/SKILL.md +130 -0
- package/plugin/skills/util-projectinit/SKILL.md +1 -1
- package/plugin/skills/util-projectsync/SKILL.md +1 -1
- package/plugin/skills/util-updprd/SKILL.md +85 -15
- package/plugin/skills/util-usanalyzer/SKILL.md +1 -1
- package/plugin/skills/util-ustagger/SKILL.md +1 -1
- package/static/assets/{abnfDiagram-VRR7QNED-DvPuwyCV.js → abnfDiagram-VRR7QNED-pIypz8mS.js} +1 -1
- package/static/assets/{arc-DRKw7sjs.js → arc-DzTLPg59.js} +1 -1
- package/static/assets/{architectureDiagram-ZJ3FMSHR-CaNcxUbC.js → architectureDiagram-ZJ3FMSHR-CuC9-Bft.js} +1 -1
- package/static/assets/{blockDiagram-677ZJIJ3-Demd4AOK.js → blockDiagram-677ZJIJ3-DcerPwn1.js} +1 -1
- package/static/assets/{c4Diagram-LMCZKHZV-PVKUfc2x.js → c4Diagram-LMCZKHZV-aH--jsPu.js} +1 -1
- package/static/assets/channel-2AGSySJb.js +1 -0
- package/static/assets/{chunk-2Q5K7J3B-CExf8ww_.js → chunk-2Q5K7J3B-6kmvWwD5.js} +1 -1
- package/static/assets/{chunk-32BRIVSS-CFPaoONF.js → chunk-32BRIVSS-Dgk5Y2Hd.js} +1 -1
- package/static/assets/{chunk-5VM5RSS4-DE2esM67.js → chunk-5VM5RSS4-DISUWWIg.js} +1 -1
- package/static/assets/{chunk-EX3LRPZG-SON_YdDG.js → chunk-EX3LRPZG-CptcVUzS.js} +1 -1
- package/static/assets/{chunk-JWPE2WC7-Qxah8LNr.js → chunk-JWPE2WC7-DpOM2Td1.js} +1 -1
- package/static/assets/{chunk-MOJQB5TN-BcWGQV3j.js → chunk-MOJQB5TN-D0mE8XQJ.js} +1 -1
- package/static/assets/{chunk-RYQCIY6F-DkDj-OR5.js → chunk-RYQCIY6F-6tMpp_wE.js} +1 -1
- package/static/assets/{chunk-V7JOEXUC-BlI1Mrah.js → chunk-V7JOEXUC-DlhAClLX.js} +1 -1
- package/static/assets/{chunk-VR4S4FIN-Agf__duj.js → chunk-VR4S4FIN-CnRRTJhL.js} +1 -1
- package/static/assets/{chunk-XXDRQBXY-B5CfAh85.js → chunk-XXDRQBXY-BjHtDhhp.js} +1 -1
- package/static/assets/classDiagram-OUVF2IWQ-DfuKvXel.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DfuKvXel.js +1 -0
- package/static/assets/{cose-bilkent-JH36ORCC-DO0cQkcy.js → cose-bilkent-JH36ORCC-ywkSH5WV.js} +1 -1
- package/static/assets/{cynefin-VYW2F7L2-CfOnsnXc.js → cynefin-VYW2F7L2-rNCSrA_M.js} +1 -1
- package/static/assets/{cynefinDiagram-TSTJHNR4-BJ8tULAv.js → cynefinDiagram-TSTJHNR4-CTO-xyj0.js} +1 -1
- package/static/assets/{dagre-VKFMJZFB-DfjwzHqP.js → dagre-VKFMJZFB-DXWjc-x2.js} +1 -1
- package/static/assets/{diagram-FQU43EPY-CndSOIIM.js → diagram-FQU43EPY-B6Yg08kE.js} +1 -1
- package/static/assets/{diagram-G47NLZAW-Czf5hN0Z.js → diagram-G47NLZAW-FdzgI0QA.js} +1 -1
- package/static/assets/{diagram-NH7WQ7WH-COAyojGh.js → diagram-NH7WQ7WH-C7MRGBLI.js} +1 -1
- package/static/assets/{diagram-OA4YK3LP-H8PoiN2m.js → diagram-OA4YK3LP-xpJMJx9r.js} +1 -1
- package/static/assets/{diagram-WEI45ONY-DzcaVzjp.js → diagram-WEI45ONY-D5PmiZy-.js} +1 -1
- package/static/assets/{ebnfDiagram-CCIWWBDH-DPazGYLJ.js → ebnfDiagram-CCIWWBDH-DJzkD6Lj.js} +1 -1
- package/static/assets/{erDiagram-Q63AITRT-Bo-fubgF.js → erDiagram-Q63AITRT-D24quzu9.js} +1 -1
- package/static/assets/{flowDiagram-23GEKE2U-CSvxUDwh.js → flowDiagram-23GEKE2U-2wLf_cAi.js} +1 -1
- package/static/assets/{ganttDiagram-NO4QXBWP-DnBveWX6.js → ganttDiagram-NO4QXBWP-hjpg1Ww8.js} +1 -1
- package/static/assets/{gitGraphDiagram-IHSO6WYX-B3rVL8c-.js → gitGraphDiagram-IHSO6WYX-Zm2bxgq8.js} +1 -1
- package/static/assets/{index-y7XDSgMG.js → index-CMSll7Xn.js} +173 -172
- package/static/assets/{index-LZSQkLE2.css → index-Dn_JY-18.css} +1 -1
- package/static/assets/{infoDiagram-FWYZ7A6U-CvWV9yy2.js → infoDiagram-FWYZ7A6U-5eq5DfJX.js} +1 -1
- package/static/assets/{ishikawaDiagram-FXEZZL3T-DF28a1N6.js → ishikawaDiagram-FXEZZL3T-BqBn7GDQ.js} +1 -1
- package/static/assets/{journeyDiagram-5HDEW3XC-CB6CugYk.js → journeyDiagram-5HDEW3XC-C6AceyMG.js} +1 -1
- package/static/assets/{kanban-definition-HUTT4EX6-ResSLF6d.js → kanban-definition-HUTT4EX6-BeQ_o04F.js} +1 -1
- package/static/assets/{linear-CLO--ryk.js → linear-CX49V_yy.js} +1 -1
- package/static/assets/{mindmap-definition-LN4V7U3C-BvW3EbxX.js → mindmap-definition-LN4V7U3C-CVzByupH.js} +1 -1
- package/static/assets/{pegDiagram-2B236MQR-BHCgqrrM.js → pegDiagram-2B236MQR-GuHmU0j3.js} +1 -1
- package/static/assets/{pieDiagram-ENE6RG2P-DVmCQ5FA.js → pieDiagram-ENE6RG2P-BKOiteQS.js} +1 -1
- package/static/assets/{quadrantDiagram-ABIIQ3AL-B3e5KR52.js → quadrantDiagram-ABIIQ3AL-CNtd5BPA.js} +1 -1
- package/static/assets/{railroadDiagram-RFXS5EU6-Csv5ysLc.js → railroadDiagram-RFXS5EU6-D4gIQQiY.js} +1 -1
- package/static/assets/{requirementDiagram-TGXJPOKE-DOMVzvnB.js → requirementDiagram-TGXJPOKE-BYokGs63.js} +1 -1
- package/static/assets/{sankeyDiagram-HTMAVEWB-B1dcTOvy.js → sankeyDiagram-HTMAVEWB-5RLXfMzy.js} +1 -1
- package/static/assets/{sequenceDiagram-DBY2YBRQ-BY414yeO.js → sequenceDiagram-DBY2YBRQ-DugFTncH.js} +1 -1
- package/static/assets/{sizeCapture-X5ZJPWSS-CuJfrfBN.js → sizeCapture-X5ZJPWSS-cCLHi08w.js} +1 -1
- package/static/assets/{stateDiagram-2N3HPSRC-FX4rJkzV.js → stateDiagram-2N3HPSRC-DQnxouNu.js} +1 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-BsTf558I.js +1 -0
- package/static/assets/{swimlanes-5IMT3BWC-Sfy58mmv.js → swimlanes-5IMT3BWC-CET7P2RH.js} +2 -2
- package/static/assets/swimlanesDiagram-G3AALYLV-Crsci1-o.js +8 -0
- package/static/assets/{timeline-definition-FHXFAJF6-CmV7by8p.js → timeline-definition-FHXFAJF6-49T4daxW.js} +1 -1
- package/static/assets/{vennDiagram-L72KCM5P-FqIyFZFt.js → vennDiagram-L72KCM5P-FqAF-S_r.js} +1 -1
- package/static/assets/{wardleyDiagram-EHGQE667-BZ06O45i.js → wardleyDiagram-EHGQE667-C9Du1lj9.js} +1 -1
- package/static/assets/{xychartDiagram-FW5EYKEG-BOkffR06.js → xychartDiagram-FW5EYKEG-CY-hJxel.js} +1 -1
- package/static/index.html +2 -2
- package/plugin/skills/util-preparek8senv/SKILL.md +0 -422
- package/static/assets/channel-C2nVbuwv.js +0 -1
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +0 -1
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +0 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +0 -1
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +0 -8
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: util-gencicdscript
|
|
3
|
+
model: claude-sonnet-5
|
|
4
|
+
effort: high
|
|
5
|
+
description: >
|
|
6
|
+
Generate the self-contained CI/CD build/run/deploy app for a CO2 project from an APPROVED
|
|
7
|
+
cicd/CICD_PLAN.md. Refuses to run unless the plan has `**Status**: COMPLETED` AND an
|
|
8
|
+
`**Approved**:` line (stamped by Compound Context Studio's Approve button). Produces
|
|
9
|
+
cicd/app/ — a zero-npm-dependency node:http app (server.js + cicd.config.json) rendering a
|
|
10
|
+
bare light-themed page (no header/sidebar/footer, iframe-embeddable) with one nav tab per
|
|
11
|
+
application: a controls bar (status badge, clickable app URL opening a new tab, Build /
|
|
12
|
+
Start / Stop / Deploy buttons) above that application's log streamed live over a built-in
|
|
13
|
+
zero-dependency WebSocket (bounded tail + backpressure drop server-side, trimmed DOM buffer
|
|
14
|
+
client-side — no leaks/OOM). Build and Deploy commands come from the approved plan (inferred
|
|
15
|
+
from ENVIRONMENT.md/DEVTOOL.md — e.g. Spring Boot jar vs Docker image; deploy via web-server
|
|
16
|
+
copy, docker run or kubectl apply, or none). Controls are disabled for any application that
|
|
17
|
+
has never completed a conductor-develop workflow run (checked against the studio's
|
|
18
|
+
/api/cicd-guard endpoint using the spawn-env token). Health/port probing marks apps already
|
|
19
|
+
running — even ones started outside this app. No version selector: only the latest built
|
|
20
|
+
applications are operated. Writes cicd/CICD_GENERATION.md with `**Status**:` IN PROGRESS →
|
|
21
|
+
COMPLETED so the studio watcher detects completion. Idempotent — re-running regenerates
|
|
22
|
+
cicd/app in place. Trigger on keywords: "generate cicd", "gen cicd script", "generate ci/cd
|
|
23
|
+
app", "cicd app", "generate deployment app". Accepts one MANDATORY argument: the
|
|
24
|
+
project-relative plan path (e.g., /util-gencicdscript cicd/CICD_PLAN.md).
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# Util Generate CI/CD Script
|
|
28
|
+
|
|
29
|
+
## Inputs
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
/util-gencicdscript cicd/CICD_PLAN.md
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Phase 0 — Gate (hard requirements, check in order)
|
|
36
|
+
|
|
37
|
+
1. The argument file exists — else STOP: "run /util-plancicd first".
|
|
38
|
+
2. It contains a top-level `**Status**: COMPLETED` — else STOP: "the plan is not complete".
|
|
39
|
+
3. It contains an `**Approved**:` line — else STOP: "approve the plan in the studio's CI/CD
|
|
40
|
+
screen first". NEVER proceed without approval.
|
|
41
|
+
|
|
42
|
+
## Phase 1 — Write the generation tracking file
|
|
43
|
+
|
|
44
|
+
Write `cicd/CICD_GENERATION.md`:
|
|
45
|
+
|
|
46
|
+
```markdown
|
|
47
|
+
# CI/CD App Generation
|
|
48
|
+
|
|
49
|
+
**Started**: <ISO date>
|
|
50
|
+
**Status**: IN PROGRESS
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Phase 2 — Generate cicd/app/
|
|
54
|
+
|
|
55
|
+
Read `references/cicd-app-template.md` for the complete file templates. Produce:
|
|
56
|
+
|
|
57
|
+
1. `cicd/app/cicd.config.json` — from the plan's tables:
|
|
58
|
+
`{ "port": <CI/CD app port>, "apps": [{ "name", "folder", "method", "port", "build",
|
|
59
|
+
"start", "stop", "deployMethod", "deploy", "health" }] }` — `deployMethod` is the plan's
|
|
60
|
+
Deploy Method column (`none|web-server|docker|kubernetes`) and `deploy` its command
|
|
61
|
+
(`null` when the method is `none`).
|
|
62
|
+
2. `cicd/app/server.js` — the template verbatim (it is generic; all project specifics live in
|
|
63
|
+
cicd.config.json). Zero npm dependencies.
|
|
64
|
+
3. Ensure `cicd/logs/` and `cicd/state.json` are ignored: append `cicd/logs/` and
|
|
65
|
+
`cicd/state.json` to the project `.gitignore` if absent.
|
|
66
|
+
|
|
67
|
+
## Phase 3 — Complete
|
|
68
|
+
|
|
69
|
+
Flip `cicd/CICD_GENERATION.md` `**Status**:` to `COMPLETED`. Print a summary (app folder,
|
|
70
|
+
port, application count) and STOP — the studio starts/embeds the app.
|
|
71
|
+
|
|
72
|
+
## Important Rules
|
|
73
|
+
|
|
74
|
+
- Zero npm dependencies — plain node:http only (the WebSocket log stream is hand-rolled in the
|
|
75
|
+
template); no install step may be required.
|
|
76
|
+
- The page is bare: no header, sidebar or footer (it is iframed by the studio). Light theme,
|
|
77
|
+
one nav tab per application; the tab body is that application's live log.
|
|
78
|
+
- Build/Start/Stop/Deploy run the plan's commands verbatim, each with the APPLICATION FOLDER
|
|
79
|
+
(`<project>/<folder>`) as working directory — command paths are relative to the application
|
|
80
|
+
folder (e.g. `java -jar target\app.jar`), never to the project root. Deploy renders only
|
|
81
|
+
when the plan assigned a deploy method other than `none`. No version selector — only the
|
|
82
|
+
latest built applications are operated.
|
|
83
|
+
- Develop gate: controls stay disabled for an application that never completed a
|
|
84
|
+
conductor-develop run — checked via `${CO2_STUDIO_URL}/api/cicd-guard/${CO2_PROJECT_ID}`
|
|
85
|
+
with the CO2_GUARD_TOKEN from the spawn env (refreshed every 30 s, last answer cached).
|
|
86
|
+
- Log streaming must stay bounded: per-app log file on disk, bounded tail on connect, live
|
|
87
|
+
append broadcast only, slow subscribers dropped on backpressure, client DOM buffer trimmed.
|
|
88
|
+
- Already-running detection: `/api/state` probes each application's health URL (falling back
|
|
89
|
+
to its port) so an app started outside this page still shows as running; a Stop on an
|
|
90
|
+
externally started process is refused with a hint instead of killed.
|
|
91
|
+
- Never touch PRD.md/CLAUDE.md; never run builds or deploys yourself during generation.
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# CI/CD App Template
|
|
2
|
+
|
|
3
|
+
`cicd/app/server.js` — generic; reads `cicd.config.json`. Copy verbatim.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
// Generated by util-gencicdscript — zero-dependency CI/CD build/run/deploy app.
|
|
7
|
+
// Env (set by Compound Context Studio at spawn): PORT, CO2_STUDIO_URL, CO2_PROJECT_ID,
|
|
8
|
+
// CO2_GUARD_TOKEN. Bare page — designed to be embedded in the studio as an iframe.
|
|
9
|
+
// One nav tab per application; the tab body streams that application's log live over a
|
|
10
|
+
// hand-rolled WebSocket (server→client push only). Memory safety: logs live on disk, the
|
|
11
|
+
// server retains nothing per subscriber beyond the socket itself (bounded initial tail,
|
|
12
|
+
// slow clients are dropped on backpressure), and the client trims its DOM log buffer.
|
|
13
|
+
const http = require('node:http')
|
|
14
|
+
const { spawn, execFileSync } = require('node:child_process')
|
|
15
|
+
const { createHash } = require('node:crypto')
|
|
16
|
+
const { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, openSync, readSync, closeSync, createWriteStream } = require('node:fs')
|
|
17
|
+
const { join, resolve } = require('node:path')
|
|
18
|
+
|
|
19
|
+
const APP_DIR = __dirname
|
|
20
|
+
const PROJECT_DIR = resolve(APP_DIR, '..', '..')
|
|
21
|
+
const LOG_DIR = join(PROJECT_DIR, 'cicd', 'logs')
|
|
22
|
+
const STATE_FILE = join(PROJECT_DIR, 'cicd', 'state.json')
|
|
23
|
+
const CONFIG = JSON.parse(readFileSync(join(APP_DIR, 'cicd.config.json'), 'utf8'))
|
|
24
|
+
const PORT = Number(process.env.PORT || CONFIG.port || 4100)
|
|
25
|
+
const STUDIO = process.env.CO2_STUDIO_URL || 'http://127.0.0.1:3001'
|
|
26
|
+
const PROJECT_ID = process.env.CO2_PROJECT_ID || ''
|
|
27
|
+
const TOKEN = process.env.CO2_GUARD_TOKEN || ''
|
|
28
|
+
|
|
29
|
+
mkdirSync(LOG_DIR, { recursive: true })
|
|
30
|
+
const running = new Map() // app name -> ChildProcess (only processes THIS app spawned)
|
|
31
|
+
const activity = new Map() // app name -> 'building' | 'deploying' (transient, exclusive)
|
|
32
|
+
const failed = new Map() // app name -> true after a nonzero run exit
|
|
33
|
+
const stopping = new Set() // app names whose exit was requested — not a failure
|
|
34
|
+
const lastError = new Map() // app name -> string
|
|
35
|
+
|
|
36
|
+
function saveState() {
|
|
37
|
+
const s = {}
|
|
38
|
+
for (const a of CONFIG.apps) s[a.name] = running.has(a.name) ? 'running' : 'stopped'
|
|
39
|
+
try { writeFileSync(STATE_FILE, JSON.stringify(s)) } catch {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function currentBranch() {
|
|
43
|
+
try {
|
|
44
|
+
return execFileSync('git', ['branch', '--show-current'], { cwd: PROJECT_DIR }).toString().trim()
|
|
45
|
+
} catch { return null }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---- Develop gate (Compound Context Studio) --------------------------------------------
|
|
49
|
+
// An application that never completed a conductor-develop run has nothing to build or run —
|
|
50
|
+
// its tab controls are disabled. Refreshed every 30 s; the last known answer is kept when the
|
|
51
|
+
// studio is briefly unreachable, and `null` (never fetched) disables controls with a hint.
|
|
52
|
+
let guardApps = null // Map lower(name|folder) -> developedOnce
|
|
53
|
+
async function refreshGuard() {
|
|
54
|
+
if (!PROJECT_ID) return
|
|
55
|
+
try {
|
|
56
|
+
const r = await fetch(`${STUDIO}/api/cicd-guard/${PROJECT_ID}?token=${TOKEN}`, { signal: AbortSignal.timeout(3000) })
|
|
57
|
+
if (!r.ok) return
|
|
58
|
+
const g = await r.json()
|
|
59
|
+
if (Array.isArray(g.applications)) {
|
|
60
|
+
guardApps = new Map()
|
|
61
|
+
for (const a of g.applications) guardApps.set(String(a.name).toLowerCase(), !!a.developedOnce)
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
function developedOnce(app) {
|
|
66
|
+
if (!guardApps) return null // unknown — controls stay disabled until the studio answers
|
|
67
|
+
const byName = guardApps.get(app.name.toLowerCase())
|
|
68
|
+
if (byName !== undefined) return byName
|
|
69
|
+
const byFolder = guardApps.get(String(app.folder || '').toLowerCase())
|
|
70
|
+
return byFolder === undefined ? null : byFolder
|
|
71
|
+
}
|
|
72
|
+
refreshGuard()
|
|
73
|
+
setInterval(refreshGuard, 30_000).unref?.()
|
|
74
|
+
|
|
75
|
+
// ---- Log streaming ---------------------------------------------------------------------
|
|
76
|
+
// One log file per application (build + run + deploy share it — the tab is one stream).
|
|
77
|
+
// Subscribers get a bounded tail on connect, then live appends; nothing is buffered
|
|
78
|
+
// server-side, and a subscriber that cannot keep up is dropped instead of queuing forever.
|
|
79
|
+
const subscribers = new Map() // app name -> Set<net.Socket>
|
|
80
|
+
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
81
|
+
|
|
82
|
+
function wsFrame(data) {
|
|
83
|
+
const payload = Buffer.from(data)
|
|
84
|
+
const len = payload.length
|
|
85
|
+
let header
|
|
86
|
+
if (len < 126) header = Buffer.from([0x81, len])
|
|
87
|
+
else if (len < 65_536) { header = Buffer.alloc(4); header[0] = 0x81; header[1] = 126; header.writeUInt16BE(len, 2) }
|
|
88
|
+
else { header = Buffer.alloc(10); header[0] = 0x81; header[1] = 127; header.writeBigUInt64BE(BigInt(len), 2) }
|
|
89
|
+
return Buffer.concat([header, payload])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function broadcast(name, chunk) {
|
|
93
|
+
const subs = subscribers.get(name)
|
|
94
|
+
if (!subs || subs.size === 0) return
|
|
95
|
+
const frame = wsFrame(chunk)
|
|
96
|
+
for (const s of subs) {
|
|
97
|
+
if (s.writableLength > 1_000_000) { subs.delete(s); try { s.destroy() } catch {} ; continue }
|
|
98
|
+
s.write(frame)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function logPath(name) { return join(LOG_DIR, `${name}.log`) }
|
|
103
|
+
|
|
104
|
+
function logTail(name, bytes = 16_384) {
|
|
105
|
+
try {
|
|
106
|
+
const p = logPath(name)
|
|
107
|
+
const size = statSync(p).size
|
|
108
|
+
const start = Math.max(0, size - bytes)
|
|
109
|
+
const buf = Buffer.alloc(size - start)
|
|
110
|
+
const fd = openSync(p, 'r')
|
|
111
|
+
readSync(fd, buf, 0, buf.length, start)
|
|
112
|
+
closeSync(fd)
|
|
113
|
+
return buf.toString('utf8')
|
|
114
|
+
} catch { return '' }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function logLine(app, text) {
|
|
118
|
+
const line = `\n--- ${new Date().toISOString()} ${text}\n`
|
|
119
|
+
try { createWriteStream(logPath(app.name), { flags: 'a' }).end(line) } catch {}
|
|
120
|
+
broadcast(app.name, line)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function runLogged(app, command, onExit) {
|
|
124
|
+
const log = createWriteStream(logPath(app.name), { flags: 'a' })
|
|
125
|
+
log.write(`\n--- ${new Date().toISOString()} $ ${command}\n`)
|
|
126
|
+
broadcast(app.name, `\n--- ${new Date().toISOString()} $ ${command}\n`)
|
|
127
|
+
const child = spawn(command, { cwd: join(PROJECT_DIR, app.folder), shell: true, env: process.env })
|
|
128
|
+
const tap = (d) => { log.write(d); broadcast(app.name, d) }
|
|
129
|
+
child.stdout.on('data', tap)
|
|
130
|
+
child.stderr.on('data', tap)
|
|
131
|
+
child.on('exit', (code) => { log.end(); onExit(code) })
|
|
132
|
+
return child
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---- App lifecycle ---------------------------------------------------------------------
|
|
136
|
+
async function probe(app) {
|
|
137
|
+
const url = app.health || `http://127.0.0.1:${app.port}/`
|
|
138
|
+
try {
|
|
139
|
+
await fetch(url, { signal: AbortSignal.timeout(800) })
|
|
140
|
+
return true
|
|
141
|
+
} catch { return false }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildApp(app) {
|
|
145
|
+
if (activity.has(app.name)) return
|
|
146
|
+
activity.set(app.name, 'building'); lastError.delete(app.name)
|
|
147
|
+
runLogged(app, app.build, (code) => {
|
|
148
|
+
activity.delete(app.name)
|
|
149
|
+
if (code !== 0) lastError.set(app.name, `build exited ${code} — see log`)
|
|
150
|
+
else logLine(app, 'build OK')
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function deployApp(app) {
|
|
155
|
+
if (activity.has(app.name) || !app.deploy) return
|
|
156
|
+
activity.set(app.name, 'deploying'); lastError.delete(app.name)
|
|
157
|
+
runLogged(app, app.deploy, (code) => {
|
|
158
|
+
activity.delete(app.name)
|
|
159
|
+
if (code !== 0) lastError.set(app.name, `deploy exited ${code} — see log`)
|
|
160
|
+
else logLine(app, `deploy OK (${app.deployMethod})`)
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function startApp(app) {
|
|
165
|
+
if (running.get(app.name) || activity.has(app.name)) return
|
|
166
|
+
failed.delete(app.name); lastError.delete(app.name)
|
|
167
|
+
if (app.method === 'container') {
|
|
168
|
+
runLogged(app, app.start, () => {})
|
|
169
|
+
saveState(); return
|
|
170
|
+
}
|
|
171
|
+
const child = runLogged(app, app.start, (code) => {
|
|
172
|
+
running.delete(app.name)
|
|
173
|
+
// A user-requested Stop exits nonzero (killed) — that is not a failure.
|
|
174
|
+
if (code !== 0 && !stopping.delete(app.name)) {
|
|
175
|
+
failed.set(app.name, true)
|
|
176
|
+
lastError.set(app.name, `process exited ${code} — see log`)
|
|
177
|
+
} else stopping.delete(app.name)
|
|
178
|
+
saveState()
|
|
179
|
+
})
|
|
180
|
+
running.set(app.name, child)
|
|
181
|
+
saveState()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// shell:true spawns a shell whose grandchildren survive child.kill() on Windows — kill the
|
|
185
|
+
// whole tree there (taskkill /T); plain kill elsewhere.
|
|
186
|
+
function killTree(child) {
|
|
187
|
+
if (process.platform === 'win32' && child.pid) {
|
|
188
|
+
try { execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' }) } catch {}
|
|
189
|
+
} else {
|
|
190
|
+
try { child.kill() } catch {}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function stopApp(app) {
|
|
195
|
+
if (app.method === 'container' && app.stop && app.stop !== 'process-kill') {
|
|
196
|
+
try { execFileSync(app.stop.split(' ')[0], app.stop.split(' ').slice(1), { cwd: PROJECT_DIR }) } catch {}
|
|
197
|
+
} else {
|
|
198
|
+
const child = running.get(app.name)
|
|
199
|
+
if (!child) {
|
|
200
|
+
lastError.set(app.name, 'running outside this CI/CD app — stop it from where it was started')
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
stopping.add(app.name)
|
|
204
|
+
killTree(child)
|
|
205
|
+
}
|
|
206
|
+
running.delete(app.name)
|
|
207
|
+
saveState()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function json(res, code, body) {
|
|
211
|
+
res.writeHead(code, { 'content-type': 'application/json' })
|
|
212
|
+
res.end(JSON.stringify(body))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---- Page ------------------------------------------------------------------------------
|
|
216
|
+
const PAGE = `<!doctype html><html><head><meta charset="utf-8"><title>CI/CD</title><style>
|
|
217
|
+
body{font-family:system-ui,sans-serif;margin:0;background:#f6f7f9;color:#1c2128;display:flex;flex-direction:column;height:100vh}
|
|
218
|
+
.top{display:flex;gap:8px;align-items:center;padding:10px 12px 6px;font-size:13px}
|
|
219
|
+
.meta{font-size:11px;color:#5b6572}
|
|
220
|
+
.tabs{display:flex;gap:2px;padding:0 12px;border-bottom:1px solid #d9dee5;flex-wrap:wrap}
|
|
221
|
+
.tab{border:1px solid #d9dee5;border-bottom:none;border-radius:8px 8px 0 0;background:#eef1f5;padding:6px 14px;font-size:13px;cursor:pointer;display:flex;gap:6px;align-items:center}
|
|
222
|
+
.tab.active{background:#fff;font-weight:600}
|
|
223
|
+
.tab .dot{width:8px;height:8px;border-radius:99px;background:#8a94a0}
|
|
224
|
+
.dot.running{background:#1c8a4a}.dot.failed{background:#c23b3b}.dot.building,.dot.deploying{background:#c9922a}
|
|
225
|
+
.bar{display:flex;gap:8px;align-items:center;padding:10px 12px;background:#fff;border-bottom:1px solid #e1e5ea;flex-wrap:wrap}
|
|
226
|
+
.badge{font-size:10px;text-transform:uppercase;border-radius:99px;padding:2px 8px;color:#fff;background:#8a94a0}
|
|
227
|
+
.badge.running{background:#1c8a4a}.badge.failed{background:#c23b3b}.badge.building,.badge.deploying{background:#c9922a}
|
|
228
|
+
.url{font-size:12px;color:#1a66d0;text-decoration:underline;word-break:break-all}
|
|
229
|
+
button{background:#fff;color:#1c2128;border:1px solid #c6cdd6;border-radius:6px;padding:4px 12px;font-size:12px;cursor:pointer}
|
|
230
|
+
button:hover:enabled{background:#eef1f5}
|
|
231
|
+
button:disabled{opacity:.4;cursor:default}
|
|
232
|
+
.notice{padding:4px 12px;font-size:11px;color:#8a5a00;background:#fff7e6;display:none}
|
|
233
|
+
.err{padding:4px 12px;font-size:11px;color:#c23b3b;display:none;white-space:pre-wrap}
|
|
234
|
+
#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}
|
|
235
|
+
</style></head><body>
|
|
236
|
+
<div class="top"><strong>CI/CD</strong><span id="branch" class="meta"></span></div>
|
|
237
|
+
<div class="tabs" id="tabs"></div>
|
|
238
|
+
<div class="bar" id="bar"></div>
|
|
239
|
+
<div class="notice" id="notice"></div>
|
|
240
|
+
<div class="err" id="err"></div>
|
|
241
|
+
<pre id="log"></pre>
|
|
242
|
+
<script>
|
|
243
|
+
let state = { apps: [] }
|
|
244
|
+
let active = null
|
|
245
|
+
let ws = null, wsTimer = null
|
|
246
|
+
|
|
247
|
+
function appOf(name){ return state.apps.find(a => a.name === name) }
|
|
248
|
+
|
|
249
|
+
function renderTabs(){
|
|
250
|
+
document.getElementById('tabs').innerHTML = state.apps.map(a => \`
|
|
251
|
+
<div class="tab \${a.name===active?'active':''}" onclick="setActive('\${a.name}')">
|
|
252
|
+
<span class="dot \${a.phase}"></span>\${a.name}\${a.developedOnce===false?' 🔒':''}</div>\`).join('')
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renderBar(){
|
|
256
|
+
const a = appOf(active)
|
|
257
|
+
if (!a) return
|
|
258
|
+
const url = 'http://'+location.hostname+':'+a.port+'/'
|
|
259
|
+
const gate = a.developedOnce === true
|
|
260
|
+
const busy = a.phase==='building'||a.phase==='deploying'
|
|
261
|
+
document.getElementById('bar').innerHTML = \`
|
|
262
|
+
<span class="badge \${a.phase}">\${a.phase}\${a.external?' (external)':''}</span>
|
|
263
|
+
<span class="meta">\${a.method} · port \${a.port}</span>
|
|
264
|
+
<a class="url" href="\${url}" target="_blank" rel="noreferrer">\${url}</a>
|
|
265
|
+
<span style="flex:1"></span>
|
|
266
|
+
<button onclick="act('build')" \${!gate||busy||a.phase==='running'?'disabled':''}>Build</button>
|
|
267
|
+
<button onclick="act('start')" \${!gate||busy||a.phase==='running'?'disabled':''}>Start</button>
|
|
268
|
+
<button onclick="act('stop')" \${a.phase!=='running'||(a.external&&a.method!=='container')?'disabled':''}>Stop</button>
|
|
269
|
+
\${a.deploy?\`<button onclick="act('deploy')" \${!gate||busy?'disabled':''}>Deploy (\${a.deployMethod})</button>\`:''}\`
|
|
270
|
+
const notice = document.getElementById('notice')
|
|
271
|
+
if (a.developedOnce === false) {
|
|
272
|
+
notice.textContent = 'This application has not been developed through the workflow yet — run conductor-feature-develop first.'
|
|
273
|
+
notice.style.display = 'block'
|
|
274
|
+
} else if (a.developedOnce === null) {
|
|
275
|
+
notice.textContent = 'Develop status not verified yet (studio unreachable) — controls stay disabled.'
|
|
276
|
+
notice.style.display = 'block'
|
|
277
|
+
} else notice.style.display = 'none'
|
|
278
|
+
const err = document.getElementById('err')
|
|
279
|
+
if (a.error) { err.textContent = a.error; err.style.display = 'block' } else err.style.display = 'none'
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function setActive(name){
|
|
283
|
+
if (active === name) return
|
|
284
|
+
active = name
|
|
285
|
+
closeWs()
|
|
286
|
+
document.getElementById('log').textContent = ''
|
|
287
|
+
openWs(name)
|
|
288
|
+
renderTabs(); renderBar()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Live log stream. One socket at a time (closed on tab switch); auto-reconnect while the tab
|
|
292
|
+
// stays active; the DOM buffer is trimmed so long sessions never grow the page unbounded.
|
|
293
|
+
function openWs(name){
|
|
294
|
+
ws = new WebSocket('ws://'+location.host+'/ws?app='+encodeURIComponent(name))
|
|
295
|
+
ws.onmessage = e => appendLog(e.data)
|
|
296
|
+
ws.onclose = () => { ws = null; if (active === name) wsTimer = setTimeout(() => openWs(name), 2000) }
|
|
297
|
+
}
|
|
298
|
+
function closeWs(){
|
|
299
|
+
if (wsTimer) { clearTimeout(wsTimer); wsTimer = null }
|
|
300
|
+
if (ws) { ws.onclose = null; try { ws.close() } catch {} ws = null }
|
|
301
|
+
}
|
|
302
|
+
function appendLog(text){
|
|
303
|
+
const el = document.getElementById('log')
|
|
304
|
+
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 40
|
|
305
|
+
el.textContent += text
|
|
306
|
+
if (el.textContent.length > 400000) el.textContent = el.textContent.slice(-250000)
|
|
307
|
+
if (atBottom) el.scrollTop = el.scrollHeight
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function refresh(){
|
|
311
|
+
const s = await fetch('/api/state').then(r => r.json()).catch(() => null)
|
|
312
|
+
if (!s) return
|
|
313
|
+
state = s
|
|
314
|
+
document.getElementById('branch').textContent = s.currentBranch ? 'branch: '+s.currentBranch : ''
|
|
315
|
+
if (!active && s.apps.length) setActive(s.apps[0].name)
|
|
316
|
+
renderTabs(); renderBar()
|
|
317
|
+
}
|
|
318
|
+
async function act(op){
|
|
319
|
+
await fetch('/api/apps/'+encodeURIComponent(active)+'/'+op, { method: 'POST' })
|
|
320
|
+
refresh()
|
|
321
|
+
}
|
|
322
|
+
refresh(); setInterval(refresh, 3000)
|
|
323
|
+
</script></body></html>`
|
|
324
|
+
|
|
325
|
+
// ---- HTTP ------------------------------------------------------------------------------
|
|
326
|
+
const server = http.createServer(async (req, res) => {
|
|
327
|
+
if (req.method === 'GET' && req.url === '/') {
|
|
328
|
+
res.writeHead(200, { 'content-type': 'text/html' })
|
|
329
|
+
return res.end(PAGE)
|
|
330
|
+
}
|
|
331
|
+
if (req.method === 'GET' && req.url === '/api/state') {
|
|
332
|
+
const apps = await Promise.all(CONFIG.apps.map(async (a) => {
|
|
333
|
+
const healthy = await probe(a)
|
|
334
|
+
const phase = activity.get(a.name) || (healthy ? 'running' : failed.has(a.name) ? 'failed' : 'stopped')
|
|
335
|
+
return {
|
|
336
|
+
name: a.name, method: a.method, port: a.port, phase,
|
|
337
|
+
deploy: !!a.deploy, deployMethod: a.deployMethod || 'none',
|
|
338
|
+
external: healthy && !running.has(a.name),
|
|
339
|
+
developedOnce: developedOnce(a),
|
|
340
|
+
error: lastError.get(a.name) || null,
|
|
341
|
+
}
|
|
342
|
+
}))
|
|
343
|
+
return json(res, 200, { currentBranch: currentBranch(), apps })
|
|
344
|
+
}
|
|
345
|
+
const m = req.url && req.url.match(/^\/api\/apps\/([^/]+)\/(build|start|stop|deploy)$/)
|
|
346
|
+
if (m && req.method === 'POST') {
|
|
347
|
+
const app = CONFIG.apps.find((a) => a.name === decodeURIComponent(m[1]))
|
|
348
|
+
if (!app) return json(res, 404, { error: 'unknown app' })
|
|
349
|
+
if (m[2] !== 'stop' && developedOnce(app) === false) {
|
|
350
|
+
return json(res, 409, { error: 'application has not completed a develop workflow run' })
|
|
351
|
+
}
|
|
352
|
+
if (m[2] === 'build') buildApp(app)
|
|
353
|
+
else if (m[2] === 'start') startApp(app)
|
|
354
|
+
else if (m[2] === 'deploy') deployApp(app)
|
|
355
|
+
else stopApp(app)
|
|
356
|
+
return json(res, 200, { ok: true })
|
|
357
|
+
}
|
|
358
|
+
json(res, 404, { error: 'not found' })
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// WebSocket endpoint /ws?app=<name> — push-only log stream (see "Log streaming" above).
|
|
362
|
+
server.on('upgrade', (req, socket) => {
|
|
363
|
+
let name = ''
|
|
364
|
+
let isWsPath = false
|
|
365
|
+
try {
|
|
366
|
+
const url = new URL(req.url, 'http://x')
|
|
367
|
+
isWsPath = url.pathname === '/ws'
|
|
368
|
+
name = url.searchParams.get('app') || ''
|
|
369
|
+
} catch {}
|
|
370
|
+
const key = req.headers['sec-websocket-key']
|
|
371
|
+
const app = CONFIG.apps.find((a) => a.name === name)
|
|
372
|
+
if (!app || !key || !isWsPath) { socket.destroy(); return }
|
|
373
|
+
const accept = createHash('sha1').update(key + WS_GUID).digest('base64')
|
|
374
|
+
socket.write(
|
|
375
|
+
'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n' +
|
|
376
|
+
`Sec-WebSocket-Accept: ${accept}\r\n\r\n`,
|
|
377
|
+
)
|
|
378
|
+
let subs = subscribers.get(name)
|
|
379
|
+
if (!subs) { subs = new Set(); subscribers.set(name, subs) }
|
|
380
|
+
subs.add(socket)
|
|
381
|
+
const cleanup = () => { subs.delete(socket); try { socket.destroy() } catch {} }
|
|
382
|
+
socket.on('close', cleanup)
|
|
383
|
+
socket.on('error', cleanup)
|
|
384
|
+
// Client frames are only ever close (0x8) — anything else is ignored, never buffered.
|
|
385
|
+
socket.on('data', (buf) => { if ((buf[0] & 0x0f) === 0x8) cleanup() })
|
|
386
|
+
socket.write(wsFrame(logTail(name)))
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
server.listen(PORT, () => console.log(`cicd app on :${PORT}`))
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
`cicd/app/cicd.config.json` shape — one entry per application from the plan's Deployment
|
|
393
|
+
Decisions table. `deployMethod`/`deploy` come from the plan's Deploy columns (`deploy` is
|
|
394
|
+
`null` when the method is `none`):
|
|
395
|
+
|
|
396
|
+
```json
|
|
397
|
+
{
|
|
398
|
+
"port": 4100,
|
|
399
|
+
"apps": [
|
|
400
|
+
{
|
|
401
|
+
"name": "my-app",
|
|
402
|
+
"folder": "my-app",
|
|
403
|
+
"method": "process",
|
|
404
|
+
"port": 3000,
|
|
405
|
+
"build": "pnpm install && pnpm build",
|
|
406
|
+
"start": "node dist/index.js",
|
|
407
|
+
"stop": "process-kill",
|
|
408
|
+
"deployMethod": "kubernetes",
|
|
409
|
+
"deploy": "kubectl apply -f my-app/k8s/",
|
|
410
|
+
"health": "http://127.0.0.1:3000/"
|
|
411
|
+
}
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
```
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: util-plancicd
|
|
3
|
+
model: claude-sonnet-5
|
|
4
|
+
effort: high
|
|
5
|
+
description: >
|
|
6
|
+
Infer a local-deployment plan for every custom application of a CO2 project and write it to
|
|
7
|
+
cicd/CICD_PLAN.md for human review in Compound Context Studio. Reads the project's
|
|
8
|
+
ENVIRONMENT.md and DEVTOOL.md, CLAUDE.md (# Custom Applications, # Port Allocation) and each
|
|
9
|
+
application's context/specification/SPECIFICATION.md, then decides per application: deployment
|
|
10
|
+
method (container vs bare process), build command (e.g. Spring Boot jar vs Docker image),
|
|
11
|
+
start command, stop mechanism, deploy method + command (none / web-server copy / docker run /
|
|
12
|
+
kubectl apply — only when the environment calls for one), port and health-check URL, plus a
|
|
13
|
+
port for the generated CI/CD app itself. The plan carries a top-level
|
|
14
|
+
`**Status**: IN PROGRESS` header flipped to `COMPLETED` when done — the studio's watcher reads
|
|
15
|
+
it. This skill NEVER generates the CI/CD app (that is util-gencicdscript, which runs only
|
|
16
|
+
after the studio stamps an **Approved**: line into the plan). Trigger on keywords: "plan
|
|
17
|
+
cicd", "plan deployment", "cicd plan", "infer deployment", "deployment plan", "plan ci/cd".
|
|
18
|
+
Accepts one MANDATORY argument: the project-relative path of the plan-request markdown the
|
|
19
|
+
studio generated (e.g., /util-plancicd execution/cicd-plan-3-1753000000000.md).
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Util Plan CI/CD
|
|
23
|
+
|
|
24
|
+
## Inputs
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
/util-plancicd <path-to-plan-request-md>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Argument | Required | Description |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| path | yes | Project-relative path of the studio-generated plan-request markdown under `execution/` |
|
|
33
|
+
|
|
34
|
+
If the argument is missing or the file does not exist, STOP and tell the user to click
|
|
35
|
+
**Plan deployment** in the studio's CI/CD screen first.
|
|
36
|
+
|
|
37
|
+
### Auto-Resolved Paths
|
|
38
|
+
|
|
39
|
+
| Logical file | Path |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Plan request | `<argument>` |
|
|
42
|
+
| Environment | `ENVIRONMENT.md` (project root) |
|
|
43
|
+
| Dev tools | `DEVTOOL.md` (project root) |
|
|
44
|
+
| Project context | `CLAUDE.md` (project root; `source/CLAUDE.md` fallback) |
|
|
45
|
+
| App specification | `<app_folder>/context/specification/SPECIFICATION.md` |
|
|
46
|
+
| Output plan | `cicd/CICD_PLAN.md` |
|
|
47
|
+
|
|
48
|
+
## Phase 1 — Gather
|
|
49
|
+
|
|
50
|
+
1. Read the plan-request markdown; its `## Applications` list is the authoritative application set.
|
|
51
|
+
2. Read `ENVIRONMENT.md` (registries, credentials, external services) and `DEVTOOL.md`
|
|
52
|
+
(installed tools + paths: node/pnpm/docker/kubectl/python versions).
|
|
53
|
+
3. Read `CLAUDE.md`: `# Custom Applications` (folder names, Depends on) and `# Port Allocation`
|
|
54
|
+
(per-application ports).
|
|
55
|
+
4. For each application, read its SPECIFICATION.md tech-stack section if present; otherwise
|
|
56
|
+
infer the stack from build files (`package.json` → node, `pom.xml` → spring-boot,
|
|
57
|
+
`composer.json` → laravel).
|
|
58
|
+
|
|
59
|
+
## Phase 2 — Decide (per application)
|
|
60
|
+
|
|
61
|
+
- **Working directory (CRITICAL)**: the generated CI/CD app executes every command with the
|
|
62
|
+
**application folder** (`<project>/<folder>`) as its working directory — NEVER the project
|
|
63
|
+
root. All paths inside build/start/stop/deploy commands must therefore be relative to the
|
|
64
|
+
application folder (`mvn -q package`, `java -jar target\app.jar`) — never prefixed with the
|
|
65
|
+
application folder itself (`skolafund_middleware\target\...` is WRONG).
|
|
66
|
+
- **Method**: `container` only when Docker appears in DEVTOOL.md AND the stack has a sensible
|
|
67
|
+
container path; otherwise `process` (bare local process — the default for a dev VM).
|
|
68
|
+
- **Build command**: from the stack (e.g. `pnpm install && pnpm build`, `mvn -q package`,
|
|
69
|
+
`composer install`). Use tool paths from DEVTOOL.md when the tool is not on PATH.
|
|
70
|
+
- **Start command**: the stack's production-ish local start (e.g. `node dist/index.js`,
|
|
71
|
+
`java -jar target/<artifact>.jar`, `php artisan serve --port <port>`); `docker build`/
|
|
72
|
+
`docker run` pair when method=container. For jar-based stacks, resolve the EXACT artifact
|
|
73
|
+
filename — read the pom/gradle build config: a Maven `<finalName>` means
|
|
74
|
+
`target/<finalName>.jar` with NO version suffix (Spring Boot repackage keeps that name and
|
|
75
|
+
leaves a `.jar.original` beside it — never reference the `.original`). Never guess
|
|
76
|
+
`<app>-<version>.jar` and never use a `target/*.jar` glob (shell-dependent, breaks on
|
|
77
|
+
Windows cmd).
|
|
78
|
+
- **Stop**: `process-kill` for bare processes; `docker stop <name>` for containers.
|
|
79
|
+
- **Deploy** (method + command): only when ENVIRONMENT.md/DEVTOOL.md show a deployment target —
|
|
80
|
+
`kubernetes` (`kubectl apply -f <app>/k8s/` when kubectl + a cluster/kubeconfig appear),
|
|
81
|
+
`docker` (`docker run`/`docker compose up -d` when the image is the deliverable),
|
|
82
|
+
`web-server` (copy the built artifact into the configured web/app server, e.g. nginx html
|
|
83
|
+
root or Tomcat webapps, when one is configured). Default **`none`** — a bare local process
|
|
84
|
+
needs no deploy step beyond Start; never invent a target the config does not name.
|
|
85
|
+
- **Port**: from `# Port Allocation`; if absent, assign sequentially from 3000 avoiding
|
|
86
|
+
collisions with the studio (3001) and the CI/CD app port.
|
|
87
|
+
- **Health**: `http://127.0.0.1:<port>/` or a documented health endpoint from the spec.
|
|
88
|
+
- **CI/CD app port**: 4100 unless taken in `# Port Allocation`; record it in the plan.
|
|
89
|
+
|
|
90
|
+
## Phase 3 — Write the plan
|
|
91
|
+
|
|
92
|
+
Create `cicd/` if missing and write `cicd/CICD_PLAN.md`:
|
|
93
|
+
|
|
94
|
+
```markdown
|
|
95
|
+
# CI/CD Deployment Plan — <Project Name>
|
|
96
|
+
|
|
97
|
+
**Generated**: <ISO date>
|
|
98
|
+
**Status**: IN PROGRESS
|
|
99
|
+
|
|
100
|
+
## CI/CD App
|
|
101
|
+
|
|
102
|
+
- **Port**: <port>
|
|
103
|
+
- **Folder**: cicd/app
|
|
104
|
+
|
|
105
|
+
## Deployment Decisions
|
|
106
|
+
|
|
107
|
+
| Application | Folder | Method | Port | Build | Start | Stop | Deploy Method | Deploy | Health |
|
|
108
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
109
|
+
| <name> | <folder> | process|container | <port> | `<cmd>` | `<cmd>` | process-kill|docker stop | none|web-server|docker|kubernetes | `<cmd or ->` | <url> |
|
|
110
|
+
|
|
111
|
+
## Rationale
|
|
112
|
+
|
|
113
|
+
- <per-application: why this method/commands, citing ENVIRONMENT.md/DEVTOOL.md evidence>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then flip `**Status**: IN PROGRESS` to `**Status**: COMPLETED`. The studio watcher advances
|
|
117
|
+
only on COMPLETED.
|
|
118
|
+
|
|
119
|
+
## STOP — Hand Back to the Human
|
|
120
|
+
|
|
121
|
+
Print a one-paragraph summary and STOP. Never invoke util-gencicdscript or any other skill —
|
|
122
|
+
the human reviews and approves the plan in the studio's CI/CD screen first.
|
|
123
|
+
|
|
124
|
+
## Important Rules
|
|
125
|
+
|
|
126
|
+
- Never write the CI/CD app; never modify PRD.md/CLAUDE.md/BUG.md.
|
|
127
|
+
- Never delete an existing `**Approved**:` line if re-planning over a previous plan — overwrite
|
|
128
|
+
the whole file WITHOUT any Approved line (a re-plan always requires fresh approval).
|
|
129
|
+
- Credentials from ENVIRONMENT.md may be referenced in commands verbatim (dev-environment
|
|
130
|
+
plaintext rule) but never echoed into the Rationale narrative.
|