@rashidee/co2 1.3.4 → 1.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.co2-dat/app.db +0 -0
- package/dist/.co2-dat/app.db-shm +0 -0
- package/dist/.co2-dat/app.db-wal +0 -0
- package/dist/index.js +24 -1
- package/package.json +1 -1
- package/plugin/skills/util-gencicdscript/SKILL.md +13 -3
- package/plugin/skills/util-gencicdscript/references/cicd-app-template.md +82 -19
- package/plugin/skills/util-plancicd/SKILL.md +15 -1
- package/static/assets/{abnfDiagram-VRR7QNED-BwxjNDj8.js → abnfDiagram-VRR7QNED-DPeOXHkD.js} +1 -1
- package/static/assets/{arc-Bf00Zylw.js → arc-Bb2ZEtXe.js} +1 -1
- package/static/assets/{architectureDiagram-ZJ3FMSHR-CRUg8Wt3.js → architectureDiagram-ZJ3FMSHR-D4QooL3I.js} +1 -1
- package/static/assets/{blockDiagram-677ZJIJ3-Dy-JP_0N.js → blockDiagram-677ZJIJ3-BBGaxcqB.js} +1 -1
- package/static/assets/{c4Diagram-LMCZKHZV-obM4PBxI.js → c4Diagram-LMCZKHZV-BuFUMezL.js} +1 -1
- package/static/assets/channel-8qA4Tgiu.js +1 -0
- package/static/assets/{chunk-2Q5K7J3B-DoXzquQ1.js → chunk-2Q5K7J3B-MrR2dFOK.js} +1 -1
- package/static/assets/{chunk-32BRIVSS-B6VgB7K8.js → chunk-32BRIVSS-KMxu8q-J.js} +1 -1
- package/static/assets/{chunk-5VM5RSS4-BmZS0Ftn.js → chunk-5VM5RSS4-Dbm1PUMI.js} +1 -1
- package/static/assets/{chunk-EX3LRPZG-DdOLrDUP.js → chunk-EX3LRPZG-C0dIkyxF.js} +1 -1
- package/static/assets/{chunk-JWPE2WC7-RVdgT8k5.js → chunk-JWPE2WC7-DbWRh-D_.js} +1 -1
- package/static/assets/{chunk-MOJQB5TN-BbMHSSXk.js → chunk-MOJQB5TN-Do9cetyI.js} +1 -1
- package/static/assets/{chunk-RYQCIY6F-CBOyejV-.js → chunk-RYQCIY6F-CyFiM1A0.js} +1 -1
- package/static/assets/{chunk-V7JOEXUC-DVvl5ZDu.js → chunk-V7JOEXUC-CoFBSdV8.js} +1 -1
- package/static/assets/{chunk-VR4S4FIN-TKs-_Nin.js → chunk-VR4S4FIN-ZJvwfHHK.js} +1 -1
- package/static/assets/{chunk-XXDRQBXY-BuwUqVAV.js → chunk-XXDRQBXY-D8HPek0x.js} +1 -1
- package/static/assets/classDiagram-OUVF2IWQ-Q3gj4hh8.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-Q3gj4hh8.js +1 -0
- package/static/assets/{cose-bilkent-JH36ORCC-bVLOVjVk.js → cose-bilkent-JH36ORCC-Cirxlh75.js} +1 -1
- package/static/assets/{cynefin-VYW2F7L2-B7HPqh-Z.js → cynefin-VYW2F7L2-DJsuSCkt.js} +1 -1
- package/static/assets/{cynefinDiagram-TSTJHNR4-0d2yxXQG.js → cynefinDiagram-TSTJHNR4-uBfDZheC.js} +1 -1
- package/static/assets/{dagre-VKFMJZFB-D7ErrCay.js → dagre-VKFMJZFB-BI0z1jT0.js} +1 -1
- package/static/assets/{diagram-FQU43EPY-C36bU8jh.js → diagram-FQU43EPY-EHIThO2P.js} +1 -1
- package/static/assets/{diagram-G47NLZAW-BzaLh10R.js → diagram-G47NLZAW-Dy2XDYy0.js} +1 -1
- package/static/assets/{diagram-NH7WQ7WH-v-Zsf5gF.js → diagram-NH7WQ7WH-C4g4puV-.js} +1 -1
- package/static/assets/{diagram-OA4YK3LP-CRbJBq_k.js → diagram-OA4YK3LP-CQ16Gfax.js} +1 -1
- package/static/assets/{diagram-WEI45ONY-8fV5Plo4.js → diagram-WEI45ONY-B4FV_zLb.js} +1 -1
- package/static/assets/{ebnfDiagram-CCIWWBDH-5gIe63LE.js → ebnfDiagram-CCIWWBDH-CHmsZ-vG.js} +1 -1
- package/static/assets/{erDiagram-Q63AITRT-C3IcAHjG.js → erDiagram-Q63AITRT-DCRi5XIy.js} +1 -1
- package/static/assets/{flowDiagram-23GEKE2U-KQjlOZK9.js → flowDiagram-23GEKE2U-BtIBkFJl.js} +1 -1
- package/static/assets/{ganttDiagram-NO4QXBWP-Dq3c2GZf.js → ganttDiagram-NO4QXBWP-CL02h1xy.js} +1 -1
- package/static/assets/{gitGraphDiagram-IHSO6WYX-BYtnaU27.js → gitGraphDiagram-IHSO6WYX-v3ovWEkW.js} +1 -1
- package/static/assets/{index-CgNepcKp.js → index-BeEg-X0n.js} +130 -130
- package/static/assets/{infoDiagram-FWYZ7A6U-CxqBig2C.js → infoDiagram-FWYZ7A6U-CQqDjZFw.js} +1 -1
- package/static/assets/{ishikawaDiagram-FXEZZL3T-0HfzC1wj.js → ishikawaDiagram-FXEZZL3T-BbtSBTuq.js} +1 -1
- package/static/assets/{journeyDiagram-5HDEW3XC-umkaBeJl.js → journeyDiagram-5HDEW3XC-BYiMuolz.js} +1 -1
- package/static/assets/{kanban-definition-HUTT4EX6-DsBos3y7.js → kanban-definition-HUTT4EX6-ok10yIH4.js} +1 -1
- package/static/assets/{linear-DtNdgaOB.js → linear-BzIhXjex.js} +1 -1
- package/static/assets/{mindmap-definition-LN4V7U3C-BThlwYQu.js → mindmap-definition-LN4V7U3C-ByHM6OsT.js} +1 -1
- package/static/assets/{pegDiagram-2B236MQR-DoxrNvw3.js → pegDiagram-2B236MQR-36ZAozwu.js} +1 -1
- package/static/assets/{pieDiagram-ENE6RG2P-BqCJUG4Q.js → pieDiagram-ENE6RG2P-CiI5CYaa.js} +1 -1
- package/static/assets/{quadrantDiagram-ABIIQ3AL-4GVp_BSP.js → quadrantDiagram-ABIIQ3AL-CqfESErc.js} +1 -1
- package/static/assets/{railroadDiagram-RFXS5EU6-CU341ElP.js → railroadDiagram-RFXS5EU6-n-HvWpNy.js} +1 -1
- package/static/assets/{requirementDiagram-TGXJPOKE-DoBSr-cX.js → requirementDiagram-TGXJPOKE-a4x2DB1M.js} +1 -1
- package/static/assets/{sankeyDiagram-HTMAVEWB-CJ__IqT6.js → sankeyDiagram-HTMAVEWB-s-Q6aijB.js} +1 -1
- package/static/assets/{sequenceDiagram-DBY2YBRQ-BkNwP7Io.js → sequenceDiagram-DBY2YBRQ-DNZkVRTf.js} +1 -1
- package/static/assets/{sizeCapture-X5ZJPWSS-B-2Xu5n9.js → sizeCapture-X5ZJPWSS-B5Ciccrp.js} +1 -1
- package/static/assets/{stateDiagram-2N3HPSRC-DPbpX5_5.js → stateDiagram-2N3HPSRC-BlFZbi2g.js} +1 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-BclL0t8p.js +1 -0
- package/static/assets/{swimlanes-5IMT3BWC-CnBZNoN8.js → swimlanes-5IMT3BWC-BXRFSwye.js} +2 -2
- package/static/assets/swimlanesDiagram-G3AALYLV-CuNyWLnI.js +8 -0
- package/static/assets/{timeline-definition-FHXFAJF6-iVUc1pZx.js → timeline-definition-FHXFAJF6-D9DwlOxs.js} +1 -1
- package/static/assets/{vennDiagram-L72KCM5P-CL66_DTK.js → vennDiagram-L72KCM5P-C80mLh7C.js} +1 -1
- package/static/assets/{wardleyDiagram-EHGQE667-DQHcCKUc.js → wardleyDiagram-EHGQE667-ESSoASoQ.js} +1 -1
- package/static/assets/{xychartDiagram-FW5EYKEG-B8thIce8.js → xychartDiagram-FW5EYKEG-BHN68FBi.js} +1 -1
- package/static/index.html +1 -1
- package/static/assets/channel-BeHa2ubV.js +0 -1
- package/static/assets/classDiagram-OUVF2IWQ-B0BHpjWc.js +0 -1
- package/static/assets/classDiagram-v2-EOCWNBFH-B0BHpjWc.js +0 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-1Opbis3l.js +0 -1
- package/static/assets/swimlanesDiagram-G3AALYLV-aoTIjn7w.js +0 -8
package/dist/.co2-dat/app.db
CHANGED
|
Binary file
|
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.5",
|
|
6813
6813
|
description: "Compound Context Studio \u2014 self-hosted team context authoring for CO2 projects",
|
|
6814
6814
|
type: "module",
|
|
6815
6815
|
license: "MIT",
|
|
@@ -22248,6 +22248,29 @@ function cicdRoutes(db, workspacePath, studioPort) {
|
|
|
22248
22248
|
return errorResponse3(c, e);
|
|
22249
22249
|
}
|
|
22250
22250
|
});
|
|
22251
|
+
app.post("/:projectId/rescan", async (c) => {
|
|
22252
|
+
const projectId = parseProjectId(c);
|
|
22253
|
+
if (projectId === null) return c.json({ code: "INVALID_ID" }, 400);
|
|
22254
|
+
const user = c.get("user");
|
|
22255
|
+
try {
|
|
22256
|
+
const setup = getOrCreateSetup(db, projectId, user.id);
|
|
22257
|
+
if (setup.status === "planning") {
|
|
22258
|
+
pollCicdTracking(db, workspacePath, setup.id, "plan", user.id);
|
|
22259
|
+
} else if (setup.status === "generating") {
|
|
22260
|
+
pollCicdTracking(db, workspacePath, setup.id, "generation", user.id);
|
|
22261
|
+
}
|
|
22262
|
+
const fresh = getOrCreateSetup(db, projectId, user.id);
|
|
22263
|
+
if (fresh.status === "planning" && !hasCicdWatcher(fresh.id, "plan")) {
|
|
22264
|
+
startCicdWatcher(db, workspacePath, fresh.id, "plan", user.id);
|
|
22265
|
+
}
|
|
22266
|
+
if (fresh.status === "generating" && !hasCicdWatcher(fresh.id, "generation")) {
|
|
22267
|
+
startCicdWatcher(db, workspacePath, fresh.id, "generation", user.id);
|
|
22268
|
+
}
|
|
22269
|
+
return c.json(await toDto8(c, fresh));
|
|
22270
|
+
} catch (e) {
|
|
22271
|
+
return errorResponse3(c, e);
|
|
22272
|
+
}
|
|
22273
|
+
});
|
|
22251
22274
|
app.post("/:projectId/plan", (c) => {
|
|
22252
22275
|
const projectId = parseProjectId(c);
|
|
22253
22276
|
if (projectId === null) return c.json({ code: "INVALID_ID" }, 400);
|
package/package.json
CHANGED
|
@@ -78,6 +78,10 @@ port, application count) and STOP — the studio starts/embeds the app.
|
|
|
78
78
|
|
|
79
79
|
- Zero npm dependencies — plain node:http only (the WebSocket log stream is hand-rolled in the
|
|
80
80
|
template); no install step may be required.
|
|
81
|
+
- Live config: the generated server re-reads `cicd.config.json` on demand (mtime-cached), so a
|
|
82
|
+
regenerated plan — corrected build/start commands, a renamed build artifact, added apps or
|
|
83
|
+
changed infra — is applied WITHOUT restarting the running app (only the listen port is bound
|
|
84
|
+
once at boot). Never cache the parsed config in a top-level `const`.
|
|
81
85
|
- The page is bare: no header, sidebar or footer (it is iframed by the studio). Light theme,
|
|
82
86
|
one nav tab per application; the tab body is that application's live log.
|
|
83
87
|
- Infrastructure page: when `infra` is non-empty the main page shows an "Infrastructure" link
|
|
@@ -95,7 +99,13 @@ port, application count) and STOP — the studio starts/embeds the app.
|
|
|
95
99
|
with the CO2_GUARD_TOKEN from the spawn env (refreshed every 30 s, last answer cached).
|
|
96
100
|
- Log streaming must stay bounded: per-app log file on disk, bounded tail on connect, live
|
|
97
101
|
append broadcast only, slow subscribers dropped on backpressure, client DOM buffer trimmed.
|
|
98
|
-
- Already-running detection: `/api/state`
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
- Already-running detection: `/api/state` decides "is it running?" by a raw TCP connect to the
|
|
103
|
+
application's `port` on BOTH IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback plus the host named
|
|
104
|
+
in its `health` URL — up if any connects. This is family-agnostic (Spring binds `127.0.0.1`,
|
|
105
|
+
Vite/Next preview bind IPv6 `::1` only) and reliable where an HTTP fetch is not, so an app
|
|
106
|
+
started outside this page still shows as running. Carry `health` and `port` from the plan
|
|
107
|
+
verbatim into `cicd.config.json`.
|
|
108
|
+
- Stop works for any running app: if the app was started outside this CI/CD app (no child
|
|
109
|
+
handle), Stop kills whatever process tree is listening on its `port` (kill-by-port), so the
|
|
110
|
+
Stop control is enabled/usable whenever an app is detected running.
|
|
101
111
|
- Never touch PRD.md/CLAUDE.md; never run builds or deploys yourself during generation.
|
|
@@ -21,8 +21,23 @@ const APP_DIR = __dirname
|
|
|
21
21
|
const PROJECT_DIR = resolve(APP_DIR, '..', '..')
|
|
22
22
|
const LOG_DIR = join(PROJECT_DIR, 'cicd', 'logs')
|
|
23
23
|
const STATE_FILE = join(PROJECT_DIR, 'cicd', 'state.json')
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
const CONFIG_PATH = join(APP_DIR, 'cicd.config.json')
|
|
25
|
+
// Config is re-read on demand (mtime-cached), so a regenerated plan — corrected build/start
|
|
26
|
+
// commands, renamed artifacts, added apps or changed infra — is picked up WITHOUT restarting
|
|
27
|
+
// this app. Only the listen PORT is bound once at boot (a port change still needs a respawn).
|
|
28
|
+
let configCache = { mtimeMs: -1, data: { port: 4100, apps: [], infra: [] } }
|
|
29
|
+
function loadConfig() {
|
|
30
|
+
try {
|
|
31
|
+
const mtimeMs = statSync(CONFIG_PATH).mtimeMs
|
|
32
|
+
if (mtimeMs !== configCache.mtimeMs) {
|
|
33
|
+
configCache = { mtimeMs, data: JSON.parse(readFileSync(CONFIG_PATH, 'utf8')) }
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
/* unreadable or mid-write — keep the last good config */
|
|
37
|
+
}
|
|
38
|
+
return configCache.data
|
|
39
|
+
}
|
|
40
|
+
const PORT = Number(process.env.PORT || loadConfig().port || 4100)
|
|
26
41
|
const STUDIO = process.env.CO2_STUDIO_URL || 'http://127.0.0.1:3001'
|
|
27
42
|
const PROJECT_ID = process.env.CO2_PROJECT_ID || ''
|
|
28
43
|
const TOKEN = process.env.CO2_GUARD_TOKEN || ''
|
|
@@ -36,7 +51,7 @@ const lastError = new Map() // app name -> string
|
|
|
36
51
|
|
|
37
52
|
function saveState() {
|
|
38
53
|
const s = {}
|
|
39
|
-
for (const a of
|
|
54
|
+
for (const a of loadConfig().apps) s[a.name] = running.has(a.name) ? 'running' : 'stopped'
|
|
40
55
|
try { writeFileSync(STATE_FILE, JSON.stringify(s)) } catch {}
|
|
41
56
|
}
|
|
42
57
|
|
|
@@ -134,12 +149,24 @@ function runLogged(app, command, onExit) {
|
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
// ---- App lifecycle ---------------------------------------------------------------------
|
|
152
|
+
// Candidate loopback hosts to check for liveness: IPv4 (127.0.0.1) AND IPv6 (::1) loopback
|
|
153
|
+
// always, plus whatever host the plan's health URL names. Frameworks bind different families —
|
|
154
|
+
// Spring Boot listens on 127.0.0.1, but Vite/Next preview dev servers bind IPv6 ::1 only — so
|
|
155
|
+
// probing a single family misses half of them. `localhost` in a health URL is included as-is
|
|
156
|
+
// and resolved by the OS.
|
|
157
|
+
function probeHosts(app) {
|
|
158
|
+
const hosts = new Set(['127.0.0.1', '::1'])
|
|
159
|
+
try { if (app.health) hosts.add(new URL(app.health).hostname) } catch {}
|
|
160
|
+
return [...hosts]
|
|
161
|
+
}
|
|
162
|
+
// Liveness = a raw TCP connect to the app's listen port on ANY candidate host, NOT an HTTP
|
|
163
|
+
// fetch of a health URL. A listening port means the app is up regardless of auth (Spring
|
|
164
|
+
// Security 401s), path (actuator disabled), or protocol — and it is reliable where Node's fetch
|
|
165
|
+
// is not (undici can stall on localhost keep-alive and time out against a server curl reaches
|
|
166
|
+
// in 20 ms). This also detects apps started outside this CI/CD app, so their state shows right.
|
|
137
167
|
async function probe(app) {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
await fetch(url, { signal: AbortSignal.timeout(800) })
|
|
141
|
-
return true
|
|
142
|
-
} catch { return false }
|
|
168
|
+
const results = await Promise.all(probeHosts(app).map((h) => tcpProbe(h, app.port, 800)))
|
|
169
|
+
return results.some(Boolean)
|
|
143
170
|
}
|
|
144
171
|
|
|
145
172
|
// Infrastructure reachability — a raw TCP connect, so it works for non-HTTP services
|
|
@@ -207,17 +234,45 @@ function killTree(child) {
|
|
|
207
234
|
}
|
|
208
235
|
}
|
|
209
236
|
|
|
237
|
+
// Stop an app we DON'T hold a child handle for (started outside this app, or before this app
|
|
238
|
+
// last restarted) by killing whatever process tree is listening on its port — so Stop works
|
|
239
|
+
// for any running app, not only ones this CI/CD app spawned. Returns true if a PID was killed.
|
|
240
|
+
function killByPort(port) {
|
|
241
|
+
try {
|
|
242
|
+
if (process.platform === 'win32') {
|
|
243
|
+
const out = execFileSync('netstat', ['-ano', '-p', 'tcp'], { encoding: 'utf8' })
|
|
244
|
+
const pids = new Set()
|
|
245
|
+
for (const line of out.split(/\r?\n/)) {
|
|
246
|
+
const m = line.match(/:(\d+)\s+\S+\s+LISTENING\s+(\d+)/)
|
|
247
|
+
if (m && Number(m[1]) === Number(port)) pids.add(m[2])
|
|
248
|
+
}
|
|
249
|
+
for (const pid of pids) {
|
|
250
|
+
try { execFileSync('taskkill', ['/pid', pid, '/T', '/F'], { stdio: 'ignore' }) } catch {}
|
|
251
|
+
}
|
|
252
|
+
return pids.size > 0
|
|
253
|
+
}
|
|
254
|
+
const out = execFileSync('sh', ['-c', `lsof -ti tcp:${port} || true`], { encoding: 'utf8' })
|
|
255
|
+
const pids = out.split(/\s+/).filter(Boolean)
|
|
256
|
+
for (const pid of pids) { try { process.kill(Number(pid)) } catch {} }
|
|
257
|
+
return pids.length > 0
|
|
258
|
+
} catch { return false }
|
|
259
|
+
}
|
|
260
|
+
|
|
210
261
|
function stopApp(app) {
|
|
211
262
|
if (app.method === 'container' && app.stop && app.stop !== 'process-kill') {
|
|
212
263
|
try { execFileSync(app.stop.split(' ')[0], app.stop.split(' ').slice(1), { cwd: PROJECT_DIR }) } catch {}
|
|
213
264
|
} else {
|
|
214
265
|
const child = running.get(app.name)
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
266
|
+
if (child) {
|
|
267
|
+
stopping.add(app.name)
|
|
268
|
+
killTree(child)
|
|
269
|
+
} else {
|
|
270
|
+
// No child handle — kill by port so an externally/previously started app still stops.
|
|
271
|
+
lastError.delete(app.name)
|
|
272
|
+
if (!killByPort(app.port)) {
|
|
273
|
+
lastError.set(app.name, `could not stop — nothing found listening on port ${app.port}`)
|
|
274
|
+
}
|
|
218
275
|
}
|
|
219
|
-
stopping.add(app.name)
|
|
220
|
-
killTree(child)
|
|
221
276
|
}
|
|
222
277
|
running.delete(app.name)
|
|
223
278
|
saveState()
|
|
@@ -281,7 +336,7 @@ function renderBar(){
|
|
|
281
336
|
<span style="flex:1"></span>
|
|
282
337
|
<button onclick="act('build')" \${!gate||busy||a.phase==='running'?'disabled':''}>Build</button>
|
|
283
338
|
<button onclick="act('start')" \${!gate||busy||a.phase==='running'?'disabled':''}>Start</button>
|
|
284
|
-
<button onclick="act('stop')" \${a.phase!=='running'
|
|
339
|
+
<button onclick="act('stop')" \${a.phase!=='running'?'disabled':''}>Stop</button>
|
|
285
340
|
\${a.deploy?\`<button onclick="act('deploy')" \${!gate||busy?'disabled':''}>Deploy (\${a.deployMethod})</button>\`:''}\`
|
|
286
341
|
const notice = document.getElementById('notice')
|
|
287
342
|
if (a.developedOnce === false) {
|
|
@@ -336,6 +391,10 @@ async function act(op){
|
|
|
336
391
|
await fetch('/api/apps/'+encodeURIComponent(active)+'/'+op, { method: 'POST' })
|
|
337
392
|
refresh()
|
|
338
393
|
}
|
|
394
|
+
// Rescan immediately when the user returns to this page (tab focus / regained visibility), so
|
|
395
|
+
// coming back from another screen reflects the true app + infra state without waiting the poll.
|
|
396
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh() })
|
|
397
|
+
window.addEventListener('focus', refresh)
|
|
339
398
|
refresh(); setInterval(refresh, 3000)
|
|
340
399
|
</script></body></html>`
|
|
341
400
|
|
|
@@ -373,6 +432,10 @@ async function refresh(){
|
|
|
373
432
|
</tr>\`).join('')
|
|
374
433
|
document.getElementById('rows').innerHTML = rows || '<tr><td colspan="4" class="muted">No infrastructure declared.</td></tr>'
|
|
375
434
|
}
|
|
435
|
+
// Rescan immediately when the user returns to this page (tab focus / regained visibility), so
|
|
436
|
+
// coming back from another screen reflects the true app + infra state without waiting the poll.
|
|
437
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh() })
|
|
438
|
+
window.addEventListener('focus', refresh)
|
|
376
439
|
refresh(); setInterval(refresh, 3000)
|
|
377
440
|
</script></body></html>`
|
|
378
441
|
|
|
@@ -387,7 +450,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
387
450
|
return res.end(INFRA_PAGE)
|
|
388
451
|
}
|
|
389
452
|
if (req.method === 'GET' && req.url === '/api/infra') {
|
|
390
|
-
const infra = await Promise.all((
|
|
453
|
+
const infra = await Promise.all((loadConfig().infra || []).map(async (i) => ({
|
|
391
454
|
id: i.id, name: i.name, host: i.host || '127.0.0.1', port: i.port,
|
|
392
455
|
url: i.url || null, description: i.description || '',
|
|
393
456
|
up: await tcpProbe(i.host || '127.0.0.1', i.port),
|
|
@@ -395,7 +458,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
395
458
|
return json(res, 200, { infra })
|
|
396
459
|
}
|
|
397
460
|
if (req.method === 'GET' && req.url === '/api/state') {
|
|
398
|
-
const apps = await Promise.all(
|
|
461
|
+
const apps = await Promise.all(loadConfig().apps.map(async (a) => {
|
|
399
462
|
const healthy = await probe(a)
|
|
400
463
|
const phase = activity.get(a.name) || (healthy ? 'running' : failed.has(a.name) ? 'failed' : 'stopped')
|
|
401
464
|
return {
|
|
@@ -406,11 +469,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
406
469
|
error: lastError.get(a.name) || null,
|
|
407
470
|
}
|
|
408
471
|
}))
|
|
409
|
-
return json(res, 200, { currentBranch: currentBranch(), hasInfra: (
|
|
472
|
+
return json(res, 200, { currentBranch: currentBranch(), hasInfra: (loadConfig().infra || []).length > 0, apps })
|
|
410
473
|
}
|
|
411
474
|
const m = req.url && req.url.match(/^\/api\/apps\/([^/]+)\/(build|start|stop|deploy)$/)
|
|
412
475
|
if (m && req.method === 'POST') {
|
|
413
|
-
const app =
|
|
476
|
+
const app = loadConfig().apps.find((a) => a.name === decodeURIComponent(m[1]))
|
|
414
477
|
if (!app) return json(res, 404, { error: 'unknown app' })
|
|
415
478
|
if (m[2] !== 'stop' && developedOnce(app) === false) {
|
|
416
479
|
return json(res, 409, { error: 'application has not completed a develop workflow run' })
|
|
@@ -434,7 +497,7 @@ server.on('upgrade', (req, socket) => {
|
|
|
434
497
|
name = url.searchParams.get('app') || ''
|
|
435
498
|
} catch {}
|
|
436
499
|
const key = req.headers['sec-websocket-key']
|
|
437
|
-
const app =
|
|
500
|
+
const app = loadConfig().apps.find((a) => a.name === name)
|
|
438
501
|
if (!app || !key || !isWsPath) { socket.destroy(); return }
|
|
439
502
|
const accept = createHash('sha1').update(key + WS_GUID).digest('base64')
|
|
440
503
|
socket.write(
|
|
@@ -96,7 +96,16 @@ If the argument is missing or the file does not exist, STOP and tell the user to
|
|
|
96
96
|
needs no deploy step beyond Start; never invent a target the config does not name.
|
|
97
97
|
- **Port**: from `# Port Allocation`; if absent, assign sequentially from 3000 avoiding
|
|
98
98
|
collisions with the studio (3001) and the CI/CD app port.
|
|
99
|
-
- **Health**:
|
|
99
|
+
- **Health / running-detection**: the endpoint the generated CI/CD app uses to tell whether the
|
|
100
|
+
application is already running. Detection is a **raw TCP connect to the app's port on loopback**
|
|
101
|
+
— the generated app probes BOTH IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback, plus the host
|
|
102
|
+
named in this Health value — so it does not matter which family the framework binds. This
|
|
103
|
+
matters because dev servers differ: Spring Boot binds `127.0.0.1`, but **Vite / Next preview
|
|
104
|
+
servers bind IPv6 `::1` (localhost) only** and are invisible to an IPv4-only probe. Therefore
|
|
105
|
+
set the Health host to **`localhost`** (not `127.0.0.1`) for any Node/Vite/Next dev-server app
|
|
106
|
+
so the detection host set includes the interface it actually binds; use `127.0.0.1` for JVM
|
|
107
|
+
apps. Value form: `http://localhost:<port>/` or a documented health path from the spec (the
|
|
108
|
+
path is informational — detection keys on the port, not the path).
|
|
100
109
|
- **CI/CD app port**: 4100 unless taken in `# Port Allocation`; record it in the plan.
|
|
101
110
|
|
|
102
111
|
### Infrastructure (status-only)
|
|
@@ -135,6 +144,11 @@ Create `cicd/` if missing and write `cicd/CICD_PLAN.md`:
|
|
|
135
144
|
|---|---|---|---|---|---|---|---|---|---|
|
|
136
145
|
| <name> | <folder> | process|container | <port> | `<cmd>` | `<cmd>` | process-kill|docker stop | none|web-server|docker|kubernetes | `<cmd or ->` | <url> |
|
|
137
146
|
|
|
147
|
+
> **Running-detection**: the generated CI/CD app determines "is it running?" by a TCP connect to
|
|
148
|
+
> each application's **Port** on both IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback plus the
|
|
149
|
+
> **Health** host — so an app is detected whichever family it binds. Use a `localhost` Health host
|
|
150
|
+
> for Node/Vite/Next apps (they bind IPv6 `::1`) and `127.0.0.1` for JVM apps.
|
|
151
|
+
|
|
138
152
|
## Infrastructure
|
|
139
153
|
|
|
140
154
|
Supporting 3rd-party services shown status-only (TCP reachability) on the CI/CD app's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-
|
|
1
|
+
import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-Do9cetyI.js";import{p as f}from"./chunk-JWPE2WC7-DbWRh-D_.js";import{_ as n,l as o}from"./index-BeEg-X0n.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-DJsuSCkt.js";var v=d().RailroadAbnf.parser.LangiumParser,l=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),i=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:i,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return l(e.element);case"AbnfOptionalGroup":return{type:"optional",element:l(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:l(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},w={parser:R,db:a,renderer:u,styles:p};export{w as diagram};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Y as ln,$ as an,a0 as y,a1 as tn,a2 as F,a3 as q,a4 as _,a5 as un,a6 as rn,a7 as J,a8 as o,a9 as j,aa as sn,ab as on,ac as fn}from"./index-
|
|
1
|
+
import{Y as ln,$ as an,a0 as y,a1 as tn,a2 as F,a3 as q,a4 as _,a5 as un,a6 as rn,a7 as J,a8 as o,a9 as j,aa as sn,ab as on,ac as fn}from"./index-BeEg-X0n.js";function cn(l){return l.innerRadius}function yn(l){return l.outerRadius}function gn(l){return l.startAngle}function dn(l){return l.endAngle}function mn(l){return l&&l.padAngle}function pn(l,h,I,D,v,A,z,a){var O=I-l,i=D-h,n=z-v,d=a-A,u=d*O-n*i;if(!(u*u<y))return u=(n*(h-A)-d*(l-v))/u,[l+u*O,h+u*i]}function U(l,h,I,D,v,A,z){var a=l-I,O=h-D,i=(z?A:-A)/J(a*a+O*O),n=i*O,d=-i*a,u=l+n,s=h+d,f=I+n,c=D+d,B=(u+f)/2,t=(s+c)/2,m=f-u,g=c-s,R=m*m+g*g,T=v-A,P=u*c-f*s,S=(g<0?-1:1)*J(fn(0,T*T*R-P*P)),Y=(P*g-m*S)/R,$=(-P*m-g*S)/R,w=(P*g+m*S)/R,p=(-P*m+g*S)/R,x=Y-B,e=$-t,r=w-B,C=p-t;return x*x+e*e>r*r+C*C&&(Y=w,$=p),{cx:Y,cy:$,x01:-n,y01:-d,x11:Y*(v/T-1),y11:$*(v/T-1)}}function hn(){var l=cn,h=yn,I=j(0),D=null,v=gn,A=dn,z=mn,a=null,O=ln(i);function i(){var n,d,u=+l.apply(this,arguments),s=+h.apply(this,arguments),f=v.apply(this,arguments)-an,c=A.apply(this,arguments)-an,B=un(c-f),t=c>f;if(a||(a=n=O()),s<u&&(d=s,s=u,u=d),!(s>y))a.moveTo(0,0);else if(B>tn-y)a.moveTo(s*F(f),s*q(f)),a.arc(0,0,s,f,c,!t),u>y&&(a.moveTo(u*F(c),u*q(c)),a.arc(0,0,u,c,f,t));else{var m=f,g=c,R=f,T=c,P=B,S=B,Y=z.apply(this,arguments)/2,$=Y>y&&(D?+D.apply(this,arguments):J(u*u+s*s)),w=_(un(s-u)/2,+I.apply(this,arguments)),p=w,x=w,e,r;if($>y){var C=sn($/u*q(Y)),K=sn($/s*q(Y));(P-=C*2)>y?(C*=t?1:-1,R+=C,T-=C):(P=0,R=T=(f+c)/2),(S-=K*2)>y?(K*=t?1:-1,m+=K,g-=K):(S=0,m=g=(f+c)/2)}var G=s*F(m),H=s*q(m),L=u*F(T),M=u*q(T);if(w>y){var N=s*F(g),Q=s*q(g),V=u*F(R),W=u*q(R),E;if(B<rn)if(E=pn(G,H,V,W,N,Q,L,M)){var X=G-E[0],Z=H-E[1],b=N-E[0],k=Q-E[1],nn=1/q(on((X*b+Z*k)/(J(X*X+Z*Z)*J(b*b+k*k)))/2),en=J(E[0]*E[0]+E[1]*E[1]);p=_(w,(u-en)/(nn-1)),x=_(w,(s-en)/(nn+1))}else p=x=0}S>y?x>y?(e=U(V,W,G,H,s,x,t),r=U(N,Q,L,M,s,x,t),a.moveTo(e.cx+e.x01,e.cy+e.y01),x<w?a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,s,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),!t),a.arc(r.cx,r.cy,x,o(r.y11,r.x11),o(r.y01,r.x01),!t))):(a.moveTo(G,H),a.arc(0,0,s,m,g,!t)):a.moveTo(G,H),!(u>y)||!(P>y)?a.lineTo(L,M):p>y?(e=U(L,M,N,Q,u,-p,t),r=U(G,H,V,W,u,-p,t),a.lineTo(e.cx+e.x01,e.cy+e.y01),p<w?a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,u,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),t),a.arc(r.cx,r.cy,p,o(r.y11,r.x11),o(r.y01,r.x01),!t))):a.arc(0,0,u,T,R,t)}if(a.closePath(),n)return a=null,n+""||null}return i.centroid=function(){var n=(+l.apply(this,arguments)+ +h.apply(this,arguments))/2,d=(+v.apply(this,arguments)+ +A.apply(this,arguments))/2-rn/2;return[F(d)*n,q(d)*n]},i.innerRadius=function(n){return arguments.length?(l=typeof n=="function"?n:j(+n),i):l},i.outerRadius=function(n){return arguments.length?(h=typeof n=="function"?n:j(+n),i):h},i.cornerRadius=function(n){return arguments.length?(I=typeof n=="function"?n:j(+n),i):I},i.padRadius=function(n){return arguments.length?(D=n==null?null:typeof n=="function"?n:j(+n),i):D},i.startAngle=function(n){return arguments.length?(v=typeof n=="function"?n:j(+n),i):v},i.endAngle=function(n){return arguments.length?(A=typeof n=="function"?n:j(+n),i):A},i.padAngle=function(n){return arguments.length?(z=typeof n=="function"?n:j(+n),i):z},i.context=function(n){return arguments.length?(a=n??null,i):a},i}export{hn as d};
|