@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.
Files changed (69) hide show
  1. package/dist/.co2-dat/app.db +0 -0
  2. package/dist/.co2-dat/app.db-shm +0 -0
  3. package/dist/.co2-dat/app.db-wal +0 -0
  4. package/dist/index.js +24 -1
  5. package/package.json +1 -1
  6. package/plugin/skills/util-gencicdscript/SKILL.md +13 -3
  7. package/plugin/skills/util-gencicdscript/references/cicd-app-template.md +82 -19
  8. package/plugin/skills/util-plancicd/SKILL.md +15 -1
  9. package/static/assets/{abnfDiagram-VRR7QNED-BwxjNDj8.js → abnfDiagram-VRR7QNED-DPeOXHkD.js} +1 -1
  10. package/static/assets/{arc-Bf00Zylw.js → arc-Bb2ZEtXe.js} +1 -1
  11. package/static/assets/{architectureDiagram-ZJ3FMSHR-CRUg8Wt3.js → architectureDiagram-ZJ3FMSHR-D4QooL3I.js} +1 -1
  12. package/static/assets/{blockDiagram-677ZJIJ3-Dy-JP_0N.js → blockDiagram-677ZJIJ3-BBGaxcqB.js} +1 -1
  13. package/static/assets/{c4Diagram-LMCZKHZV-obM4PBxI.js → c4Diagram-LMCZKHZV-BuFUMezL.js} +1 -1
  14. package/static/assets/channel-8qA4Tgiu.js +1 -0
  15. package/static/assets/{chunk-2Q5K7J3B-DoXzquQ1.js → chunk-2Q5K7J3B-MrR2dFOK.js} +1 -1
  16. package/static/assets/{chunk-32BRIVSS-B6VgB7K8.js → chunk-32BRIVSS-KMxu8q-J.js} +1 -1
  17. package/static/assets/{chunk-5VM5RSS4-BmZS0Ftn.js → chunk-5VM5RSS4-Dbm1PUMI.js} +1 -1
  18. package/static/assets/{chunk-EX3LRPZG-DdOLrDUP.js → chunk-EX3LRPZG-C0dIkyxF.js} +1 -1
  19. package/static/assets/{chunk-JWPE2WC7-RVdgT8k5.js → chunk-JWPE2WC7-DbWRh-D_.js} +1 -1
  20. package/static/assets/{chunk-MOJQB5TN-BbMHSSXk.js → chunk-MOJQB5TN-Do9cetyI.js} +1 -1
  21. package/static/assets/{chunk-RYQCIY6F-CBOyejV-.js → chunk-RYQCIY6F-CyFiM1A0.js} +1 -1
  22. package/static/assets/{chunk-V7JOEXUC-DVvl5ZDu.js → chunk-V7JOEXUC-CoFBSdV8.js} +1 -1
  23. package/static/assets/{chunk-VR4S4FIN-TKs-_Nin.js → chunk-VR4S4FIN-ZJvwfHHK.js} +1 -1
  24. package/static/assets/{chunk-XXDRQBXY-BuwUqVAV.js → chunk-XXDRQBXY-D8HPek0x.js} +1 -1
  25. package/static/assets/classDiagram-OUVF2IWQ-Q3gj4hh8.js +1 -0
  26. package/static/assets/classDiagram-v2-EOCWNBFH-Q3gj4hh8.js +1 -0
  27. package/static/assets/{cose-bilkent-JH36ORCC-bVLOVjVk.js → cose-bilkent-JH36ORCC-Cirxlh75.js} +1 -1
  28. package/static/assets/{cynefin-VYW2F7L2-B7HPqh-Z.js → cynefin-VYW2F7L2-DJsuSCkt.js} +1 -1
  29. package/static/assets/{cynefinDiagram-TSTJHNR4-0d2yxXQG.js → cynefinDiagram-TSTJHNR4-uBfDZheC.js} +1 -1
  30. package/static/assets/{dagre-VKFMJZFB-D7ErrCay.js → dagre-VKFMJZFB-BI0z1jT0.js} +1 -1
  31. package/static/assets/{diagram-FQU43EPY-C36bU8jh.js → diagram-FQU43EPY-EHIThO2P.js} +1 -1
  32. package/static/assets/{diagram-G47NLZAW-BzaLh10R.js → diagram-G47NLZAW-Dy2XDYy0.js} +1 -1
  33. package/static/assets/{diagram-NH7WQ7WH-v-Zsf5gF.js → diagram-NH7WQ7WH-C4g4puV-.js} +1 -1
  34. package/static/assets/{diagram-OA4YK3LP-CRbJBq_k.js → diagram-OA4YK3LP-CQ16Gfax.js} +1 -1
  35. package/static/assets/{diagram-WEI45ONY-8fV5Plo4.js → diagram-WEI45ONY-B4FV_zLb.js} +1 -1
  36. package/static/assets/{ebnfDiagram-CCIWWBDH-5gIe63LE.js → ebnfDiagram-CCIWWBDH-CHmsZ-vG.js} +1 -1
  37. package/static/assets/{erDiagram-Q63AITRT-C3IcAHjG.js → erDiagram-Q63AITRT-DCRi5XIy.js} +1 -1
  38. package/static/assets/{flowDiagram-23GEKE2U-KQjlOZK9.js → flowDiagram-23GEKE2U-BtIBkFJl.js} +1 -1
  39. package/static/assets/{ganttDiagram-NO4QXBWP-Dq3c2GZf.js → ganttDiagram-NO4QXBWP-CL02h1xy.js} +1 -1
  40. package/static/assets/{gitGraphDiagram-IHSO6WYX-BYtnaU27.js → gitGraphDiagram-IHSO6WYX-v3ovWEkW.js} +1 -1
  41. package/static/assets/{index-CgNepcKp.js → index-BeEg-X0n.js} +130 -130
  42. package/static/assets/{infoDiagram-FWYZ7A6U-CxqBig2C.js → infoDiagram-FWYZ7A6U-CQqDjZFw.js} +1 -1
  43. package/static/assets/{ishikawaDiagram-FXEZZL3T-0HfzC1wj.js → ishikawaDiagram-FXEZZL3T-BbtSBTuq.js} +1 -1
  44. package/static/assets/{journeyDiagram-5HDEW3XC-umkaBeJl.js → journeyDiagram-5HDEW3XC-BYiMuolz.js} +1 -1
  45. package/static/assets/{kanban-definition-HUTT4EX6-DsBos3y7.js → kanban-definition-HUTT4EX6-ok10yIH4.js} +1 -1
  46. package/static/assets/{linear-DtNdgaOB.js → linear-BzIhXjex.js} +1 -1
  47. package/static/assets/{mindmap-definition-LN4V7U3C-BThlwYQu.js → mindmap-definition-LN4V7U3C-ByHM6OsT.js} +1 -1
  48. package/static/assets/{pegDiagram-2B236MQR-DoxrNvw3.js → pegDiagram-2B236MQR-36ZAozwu.js} +1 -1
  49. package/static/assets/{pieDiagram-ENE6RG2P-BqCJUG4Q.js → pieDiagram-ENE6RG2P-CiI5CYaa.js} +1 -1
  50. package/static/assets/{quadrantDiagram-ABIIQ3AL-4GVp_BSP.js → quadrantDiagram-ABIIQ3AL-CqfESErc.js} +1 -1
  51. package/static/assets/{railroadDiagram-RFXS5EU6-CU341ElP.js → railroadDiagram-RFXS5EU6-n-HvWpNy.js} +1 -1
  52. package/static/assets/{requirementDiagram-TGXJPOKE-DoBSr-cX.js → requirementDiagram-TGXJPOKE-a4x2DB1M.js} +1 -1
  53. package/static/assets/{sankeyDiagram-HTMAVEWB-CJ__IqT6.js → sankeyDiagram-HTMAVEWB-s-Q6aijB.js} +1 -1
  54. package/static/assets/{sequenceDiagram-DBY2YBRQ-BkNwP7Io.js → sequenceDiagram-DBY2YBRQ-DNZkVRTf.js} +1 -1
  55. package/static/assets/{sizeCapture-X5ZJPWSS-B-2Xu5n9.js → sizeCapture-X5ZJPWSS-B5Ciccrp.js} +1 -1
  56. package/static/assets/{stateDiagram-2N3HPSRC-DPbpX5_5.js → stateDiagram-2N3HPSRC-BlFZbi2g.js} +1 -1
  57. package/static/assets/stateDiagram-v2-6OUMAXLB-BclL0t8p.js +1 -0
  58. package/static/assets/{swimlanes-5IMT3BWC-CnBZNoN8.js → swimlanes-5IMT3BWC-BXRFSwye.js} +2 -2
  59. package/static/assets/swimlanesDiagram-G3AALYLV-CuNyWLnI.js +8 -0
  60. package/static/assets/{timeline-definition-FHXFAJF6-iVUc1pZx.js → timeline-definition-FHXFAJF6-D9DwlOxs.js} +1 -1
  61. package/static/assets/{vennDiagram-L72KCM5P-CL66_DTK.js → vennDiagram-L72KCM5P-C80mLh7C.js} +1 -1
  62. package/static/assets/{wardleyDiagram-EHGQE667-DQHcCKUc.js → wardleyDiagram-EHGQE667-ESSoASoQ.js} +1 -1
  63. package/static/assets/{xychartDiagram-FW5EYKEG-B8thIce8.js → xychartDiagram-FW5EYKEG-BHN68FBi.js} +1 -1
  64. package/static/index.html +1 -1
  65. package/static/assets/channel-BeHa2ubV.js +0 -1
  66. package/static/assets/classDiagram-OUVF2IWQ-B0BHpjWc.js +0 -1
  67. package/static/assets/classDiagram-v2-EOCWNBFH-B0BHpjWc.js +0 -1
  68. package/static/assets/stateDiagram-v2-6OUMAXLB-1Opbis3l.js +0 -1
  69. package/static/assets/swimlanesDiagram-G3AALYLV-aoTIjn7w.js +0 -8
Binary file
Binary file
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.4",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rashidee/co2",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "description": "Compound Context Studio — self-hosted team context authoring for CO2 projects",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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` probes each application's health URL (falling back
99
- to its port) so an app started outside this page still shows as running; a Stop on an
100
- externally started process is refused with a hint instead of killed.
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 CONFIG = JSON.parse(readFileSync(join(APP_DIR, 'cicd.config.json'), 'utf8'))
25
- const PORT = Number(process.env.PORT || CONFIG.port || 4100)
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 CONFIG.apps) s[a.name] = running.has(a.name) ? 'running' : 'stopped'
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 url = app.health || `http://127.0.0.1:${app.port}/`
139
- try {
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 (!child) {
216
- lastError.set(app.name, 'running outside this CI/CD app — stop it from where it was started')
217
- return
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'||(a.external&&a.method!=='container')?'disabled':''}>Stop</button>
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((CONFIG.infra || []).map(async (i) => ({
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(CONFIG.apps.map(async (a) => {
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: (CONFIG.infra || []).length > 0, apps })
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 = CONFIG.apps.find((a) => a.name === decodeURIComponent(m[1]))
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 = CONFIG.apps.find((a) => a.name === name)
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**: `http://127.0.0.1:<port>/` or a documented health endpoint from the spec.
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-BbMHSSXk.js";import{p as f}from"./chunk-JWPE2WC7-RVdgT8k5.js";import{_ as n,l as o}from"./index-CgNepcKp.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-B7HPqh-Z.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
+ 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-CgNepcKp.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};
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};