@rashidee/co2 1.3.3 → 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 (70) 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/.co2-dat/config.json +4 -0
  5. package/dist/index.js +24 -1
  6. package/package.json +1 -1
  7. package/plugin/skills/util-gencicdscript/SKILL.md +27 -7
  8. package/plugin/skills/util-gencicdscript/references/cicd-app-template.md +171 -22
  9. package/plugin/skills/util-plancicd/SKILL.md +55 -5
  10. package/static/assets/{abnfDiagram-VRR7QNED-BwxjNDj8.js → abnfDiagram-VRR7QNED-DPeOXHkD.js} +1 -1
  11. package/static/assets/{arc-Bf00Zylw.js → arc-Bb2ZEtXe.js} +1 -1
  12. package/static/assets/{architectureDiagram-ZJ3FMSHR-CRUg8Wt3.js → architectureDiagram-ZJ3FMSHR-D4QooL3I.js} +1 -1
  13. package/static/assets/{blockDiagram-677ZJIJ3-Dy-JP_0N.js → blockDiagram-677ZJIJ3-BBGaxcqB.js} +1 -1
  14. package/static/assets/{c4Diagram-LMCZKHZV-obM4PBxI.js → c4Diagram-LMCZKHZV-BuFUMezL.js} +1 -1
  15. package/static/assets/channel-8qA4Tgiu.js +1 -0
  16. package/static/assets/{chunk-2Q5K7J3B-DoXzquQ1.js → chunk-2Q5K7J3B-MrR2dFOK.js} +1 -1
  17. package/static/assets/{chunk-32BRIVSS-B6VgB7K8.js → chunk-32BRIVSS-KMxu8q-J.js} +1 -1
  18. package/static/assets/{chunk-5VM5RSS4-BmZS0Ftn.js → chunk-5VM5RSS4-Dbm1PUMI.js} +1 -1
  19. package/static/assets/{chunk-EX3LRPZG-DdOLrDUP.js → chunk-EX3LRPZG-C0dIkyxF.js} +1 -1
  20. package/static/assets/{chunk-JWPE2WC7-RVdgT8k5.js → chunk-JWPE2WC7-DbWRh-D_.js} +1 -1
  21. package/static/assets/{chunk-MOJQB5TN-BbMHSSXk.js → chunk-MOJQB5TN-Do9cetyI.js} +1 -1
  22. package/static/assets/{chunk-RYQCIY6F-CBOyejV-.js → chunk-RYQCIY6F-CyFiM1A0.js} +1 -1
  23. package/static/assets/{chunk-V7JOEXUC-DVvl5ZDu.js → chunk-V7JOEXUC-CoFBSdV8.js} +1 -1
  24. package/static/assets/{chunk-VR4S4FIN-TKs-_Nin.js → chunk-VR4S4FIN-ZJvwfHHK.js} +1 -1
  25. package/static/assets/{chunk-XXDRQBXY-BuwUqVAV.js → chunk-XXDRQBXY-D8HPek0x.js} +1 -1
  26. package/static/assets/classDiagram-OUVF2IWQ-Q3gj4hh8.js +1 -0
  27. package/static/assets/classDiagram-v2-EOCWNBFH-Q3gj4hh8.js +1 -0
  28. package/static/assets/{cose-bilkent-JH36ORCC-bVLOVjVk.js → cose-bilkent-JH36ORCC-Cirxlh75.js} +1 -1
  29. package/static/assets/{cynefin-VYW2F7L2-B7HPqh-Z.js → cynefin-VYW2F7L2-DJsuSCkt.js} +1 -1
  30. package/static/assets/{cynefinDiagram-TSTJHNR4-0d2yxXQG.js → cynefinDiagram-TSTJHNR4-uBfDZheC.js} +1 -1
  31. package/static/assets/{dagre-VKFMJZFB-D7ErrCay.js → dagre-VKFMJZFB-BI0z1jT0.js} +1 -1
  32. package/static/assets/{diagram-FQU43EPY-C36bU8jh.js → diagram-FQU43EPY-EHIThO2P.js} +1 -1
  33. package/static/assets/{diagram-G47NLZAW-BzaLh10R.js → diagram-G47NLZAW-Dy2XDYy0.js} +1 -1
  34. package/static/assets/{diagram-NH7WQ7WH-v-Zsf5gF.js → diagram-NH7WQ7WH-C4g4puV-.js} +1 -1
  35. package/static/assets/{diagram-OA4YK3LP-CRbJBq_k.js → diagram-OA4YK3LP-CQ16Gfax.js} +1 -1
  36. package/static/assets/{diagram-WEI45ONY-8fV5Plo4.js → diagram-WEI45ONY-B4FV_zLb.js} +1 -1
  37. package/static/assets/{ebnfDiagram-CCIWWBDH-5gIe63LE.js → ebnfDiagram-CCIWWBDH-CHmsZ-vG.js} +1 -1
  38. package/static/assets/{erDiagram-Q63AITRT-C3IcAHjG.js → erDiagram-Q63AITRT-DCRi5XIy.js} +1 -1
  39. package/static/assets/{flowDiagram-23GEKE2U-KQjlOZK9.js → flowDiagram-23GEKE2U-BtIBkFJl.js} +1 -1
  40. package/static/assets/{ganttDiagram-NO4QXBWP-Dq3c2GZf.js → ganttDiagram-NO4QXBWP-CL02h1xy.js} +1 -1
  41. package/static/assets/{gitGraphDiagram-IHSO6WYX-BYtnaU27.js → gitGraphDiagram-IHSO6WYX-v3ovWEkW.js} +1 -1
  42. package/static/assets/{index-CgNepcKp.js → index-BeEg-X0n.js} +130 -130
  43. package/static/assets/{infoDiagram-FWYZ7A6U-CxqBig2C.js → infoDiagram-FWYZ7A6U-CQqDjZFw.js} +1 -1
  44. package/static/assets/{ishikawaDiagram-FXEZZL3T-0HfzC1wj.js → ishikawaDiagram-FXEZZL3T-BbtSBTuq.js} +1 -1
  45. package/static/assets/{journeyDiagram-5HDEW3XC-umkaBeJl.js → journeyDiagram-5HDEW3XC-BYiMuolz.js} +1 -1
  46. package/static/assets/{kanban-definition-HUTT4EX6-DsBos3y7.js → kanban-definition-HUTT4EX6-ok10yIH4.js} +1 -1
  47. package/static/assets/{linear-DtNdgaOB.js → linear-BzIhXjex.js} +1 -1
  48. package/static/assets/{mindmap-definition-LN4V7U3C-BThlwYQu.js → mindmap-definition-LN4V7U3C-ByHM6OsT.js} +1 -1
  49. package/static/assets/{pegDiagram-2B236MQR-DoxrNvw3.js → pegDiagram-2B236MQR-36ZAozwu.js} +1 -1
  50. package/static/assets/{pieDiagram-ENE6RG2P-BqCJUG4Q.js → pieDiagram-ENE6RG2P-CiI5CYaa.js} +1 -1
  51. package/static/assets/{quadrantDiagram-ABIIQ3AL-4GVp_BSP.js → quadrantDiagram-ABIIQ3AL-CqfESErc.js} +1 -1
  52. package/static/assets/{railroadDiagram-RFXS5EU6-CU341ElP.js → railroadDiagram-RFXS5EU6-n-HvWpNy.js} +1 -1
  53. package/static/assets/{requirementDiagram-TGXJPOKE-DoBSr-cX.js → requirementDiagram-TGXJPOKE-a4x2DB1M.js} +1 -1
  54. package/static/assets/{sankeyDiagram-HTMAVEWB-CJ__IqT6.js → sankeyDiagram-HTMAVEWB-s-Q6aijB.js} +1 -1
  55. package/static/assets/{sequenceDiagram-DBY2YBRQ-BkNwP7Io.js → sequenceDiagram-DBY2YBRQ-DNZkVRTf.js} +1 -1
  56. package/static/assets/{sizeCapture-X5ZJPWSS-B-2Xu5n9.js → sizeCapture-X5ZJPWSS-B5Ciccrp.js} +1 -1
  57. package/static/assets/{stateDiagram-2N3HPSRC-DPbpX5_5.js → stateDiagram-2N3HPSRC-BlFZbi2g.js} +1 -1
  58. package/static/assets/stateDiagram-v2-6OUMAXLB-BclL0t8p.js +1 -0
  59. package/static/assets/{swimlanes-5IMT3BWC-CnBZNoN8.js → swimlanes-5IMT3BWC-BXRFSwye.js} +2 -2
  60. package/static/assets/swimlanesDiagram-G3AALYLV-CuNyWLnI.js +8 -0
  61. package/static/assets/{timeline-definition-FHXFAJF6-iVUc1pZx.js → timeline-definition-FHXFAJF6-D9DwlOxs.js} +1 -1
  62. package/static/assets/{vennDiagram-L72KCM5P-CL66_DTK.js → vennDiagram-L72KCM5P-C80mLh7C.js} +1 -1
  63. package/static/assets/{wardleyDiagram-EHGQE667-DQHcCKUc.js → wardleyDiagram-EHGQE667-ESSoASoQ.js} +1 -1
  64. package/static/assets/{xychartDiagram-FW5EYKEG-B8thIce8.js → xychartDiagram-FW5EYKEG-BHN68FBi.js} +1 -1
  65. package/static/index.html +1 -1
  66. package/static/assets/channel-BeHa2ubV.js +0 -1
  67. package/static/assets/classDiagram-OUVF2IWQ-B0BHpjWc.js +0 -1
  68. package/static/assets/classDiagram-v2-EOCWNBFH-B0BHpjWc.js +0 -1
  69. package/static/assets/stateDiagram-v2-6OUMAXLB-1Opbis3l.js +0 -1
  70. package/static/assets/swimlanesDiagram-G3AALYLV-aoTIjn7w.js +0 -8
Binary file
Binary file
Binary file
@@ -0,0 +1,4 @@
1
+ {
2
+ "port": 3001,
3
+ "workspacePath": "C:\\Users\\vedee\\workspace"
4
+ }
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.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rashidee/co2",
3
- "version": "1.3.3",
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",
@@ -17,7 +17,9 @@ description: >
17
17
  has never completed a conductor-develop workflow run (checked against the studio's
18
18
  /api/cicd-guard endpoint using the spawn-env token). Health/port probing marks apps already
19
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
20
+ applications are operated. When the approved plan declares an `## Infrastructure` list, the
21
+ main page shows an "Infrastructure" link that opens a separate status-only page listing every
22
+ supporting 3rd-party service with a live TCP-reachability indicator and a console link. Writes cicd/CICD_GENERATION.md with `**Status**:` IN PROGRESS →
21
23
  COMPLETED so the studio watcher detects completion. Idempotent — re-running regenerates
22
24
  cicd/app in place. Trigger on keywords: "generate cicd", "gen cicd script", "generate ci/cd
23
25
  app", "cicd app", "generate deployment app". Accepts one MANDATORY argument: the
@@ -56,9 +58,12 @@ Read `references/cicd-app-template.md` for the complete file templates. Produce:
56
58
 
57
59
  1. `cicd/app/cicd.config.json` — from the plan's tables:
58
60
  `{ "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`).
61
+ "start", "stop", "deployMethod", "deploy", "health" }], "infra": [{ "id", "name", "host",
62
+ "port", "url", "description" }] }` `deployMethod` is the plan's Deploy Method column
63
+ (`none|web-server|docker|kubernetes`) and `deploy` its command (`null` when the method is
64
+ `none`). The `infra` array comes from the plan's `## Infrastructure` table (one entry per
65
+ row); `url` is `null` when the Console URL cell is `-`. Emit `"infra": []` when the plan's
66
+ Infrastructure table is empty — never invent services the plan does not list.
62
67
  2. `cicd/app/server.js` — the template verbatim (it is generic; all project specifics live in
63
68
  cicd.config.json). Zero npm dependencies.
64
69
  3. Ensure `cicd/logs/` and `cicd/state.json` are ignored: append `cicd/logs/` and
@@ -73,8 +78,17 @@ port, application count) and STOP — the studio starts/embeds the app.
73
78
 
74
79
  - Zero npm dependencies — plain node:http only (the WebSocket log stream is hand-rolled in the
75
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`.
76
85
  - The page is bare: no header, sidebar or footer (it is iframed by the studio). Light theme,
77
86
  one nav tab per application; the tab body is that application's live log.
87
+ - Infrastructure page: when `infra` is non-empty the main page shows an "Infrastructure" link
88
+ that opens the separate `/infra` page in a new tab — a status-only list of supporting
89
+ 3rd-party services, each with a live TCP-reachability dot (polled) and a console link when a
90
+ `url` is set. Infrastructure is never built/started/stopped/deployed (no lifecycle controls,
91
+ no develop gate); the link is hidden when `infra` is empty.
78
92
  - Build/Start/Stop/Deploy run the plan's commands verbatim, each with the APPLICATION FOLDER
79
93
  (`<project>/<folder>`) as working directory — command paths are relative to the application
80
94
  folder (e.g. `java -jar target\app.jar`), never to the project root. Deploy renders only
@@ -85,7 +99,13 @@ port, application count) and STOP — the studio starts/embeds the app.
85
99
  with the CO2_GUARD_TOKEN from the spawn env (refreshed every 30 s, last answer cached).
86
100
  - Log streaming must stay bounded: per-app log file on disk, bounded tail on connect, live
87
101
  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.
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.
91
111
  - Never touch PRD.md/CLAUDE.md; never run builds or deploys yourself during generation.
@@ -11,6 +11,7 @@
11
11
  // server retains nothing per subscriber beyond the socket itself (bounded initial tail,
12
12
  // slow clients are dropped on backpressure), and the client trims its DOM log buffer.
13
13
  const http = require('node:http')
14
+ const net = require('node:net')
14
15
  const { spawn, execFileSync } = require('node:child_process')
15
16
  const { createHash } = require('node:crypto')
16
17
  const { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, openSync, readSync, closeSync, createWriteStream } = require('node:fs')
@@ -20,8 +21,23 @@ const APP_DIR = __dirname
20
21
  const PROJECT_DIR = resolve(APP_DIR, '..', '..')
21
22
  const LOG_DIR = join(PROJECT_DIR, 'cicd', 'logs')
22
23
  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)
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)
25
41
  const STUDIO = process.env.CO2_STUDIO_URL || 'http://127.0.0.1:3001'
26
42
  const PROJECT_ID = process.env.CO2_PROJECT_ID || ''
27
43
  const TOKEN = process.env.CO2_GUARD_TOKEN || ''
@@ -35,7 +51,7 @@ const lastError = new Map() // app name -> string
35
51
 
36
52
  function saveState() {
37
53
  const s = {}
38
- 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'
39
55
  try { writeFileSync(STATE_FILE, JSON.stringify(s)) } catch {}
40
56
  }
41
57
 
@@ -133,12 +149,39 @@ function runLogged(app, command, onExit) {
133
149
  }
134
150
 
135
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.
136
167
  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 }
168
+ const results = await Promise.all(probeHosts(app).map((h) => tcpProbe(h, app.port, 800)))
169
+ return results.some(Boolean)
170
+ }
171
+
172
+ // Infrastructure reachability a raw TCP connect, so it works for non-HTTP services
173
+ // (PostgreSQL, Redis) as well as web consoles. Status-only: infra is never controlled here.
174
+ function tcpProbe(host, port, timeout = 1000) {
175
+ return new Promise((resolve) => {
176
+ const socket = new net.Socket()
177
+ let done = false
178
+ const finish = (ok) => { if (done) return; done = true; try { socket.destroy() } catch {} ; resolve(ok) }
179
+ socket.setTimeout(timeout)
180
+ socket.once('connect', () => finish(true))
181
+ socket.once('timeout', () => finish(false))
182
+ socket.once('error', () => finish(false))
183
+ try { socket.connect(Number(port), host || '127.0.0.1') } catch { finish(false) }
184
+ })
142
185
  }
143
186
 
144
187
  function buildApp(app) {
@@ -191,17 +234,45 @@ function killTree(child) {
191
234
  }
192
235
  }
193
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
+
194
261
  function stopApp(app) {
195
262
  if (app.method === 'container' && app.stop && app.stop !== 'process-kill') {
196
263
  try { execFileSync(app.stop.split(' ')[0], app.stop.split(' ').slice(1), { cwd: PROJECT_DIR }) } catch {}
197
264
  } else {
198
265
  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
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
+ }
202
275
  }
203
- stopping.add(app.name)
204
- killTree(child)
205
276
  }
206
277
  running.delete(app.name)
207
278
  saveState()
@@ -233,7 +304,7 @@ button:disabled{opacity:.4;cursor:default}
233
304
  .err{padding:4px 12px;font-size:11px;color:#c23b3b;display:none;white-space:pre-wrap}
234
305
  #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
306
  </style></head><body>
236
- <div class="top"><strong>CI/CD</strong><span id="branch" class="meta"></span></div>
307
+ <div class="top"><strong>CI/CD</strong><span id="branch" class="meta"></span><span style="flex:1"></span><a id="infralink" class="url" href="/infra" target="_blank" rel="noreferrer" style="display:none">Infrastructure ↗</a></div>
237
308
  <div class="tabs" id="tabs"></div>
238
309
  <div class="bar" id="bar"></div>
239
310
  <div class="notice" id="notice"></div>
@@ -265,7 +336,7 @@ function renderBar(){
265
336
  <span style="flex:1"></span>
266
337
  <button onclick="act('build')" \${!gate||busy||a.phase==='running'?'disabled':''}>Build</button>
267
338
  <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>
339
+ <button onclick="act('stop')" \${a.phase!=='running'?'disabled':''}>Stop</button>
269
340
  \${a.deploy?\`<button onclick="act('deploy')" \${!gate||busy?'disabled':''}>Deploy (\${a.deployMethod})</button>\`:''}\`
270
341
  const notice = document.getElementById('notice')
271
342
  if (a.developedOnce === false) {
@@ -312,6 +383,7 @@ async function refresh(){
312
383
  if (!s) return
313
384
  state = s
314
385
  document.getElementById('branch').textContent = s.currentBranch ? 'branch: '+s.currentBranch : ''
386
+ document.getElementById('infralink').style.display = s.hasInfra ? 'inline' : 'none'
315
387
  if (!active && s.apps.length) setActive(s.apps[0].name)
316
388
  renderTabs(); renderBar()
317
389
  }
@@ -319,6 +391,51 @@ async function act(op){
319
391
  await fetch('/api/apps/'+encodeURIComponent(active)+'/'+op, { method: 'POST' })
320
392
  refresh()
321
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)
398
+ refresh(); setInterval(refresh, 3000)
399
+ </script></body></html>`
400
+
401
+ // ---- Infrastructure page ---------------------------------------------------------------
402
+ // Standalone status-only page (opened in a new tab from the main page's Infrastructure link).
403
+ // Lists every supporting 3rd-party service with a live TCP-reachability dot and a console link
404
+ // when the service exposes a web UI. No lifecycle controls — infra is externally managed.
405
+ const INFRA_PAGE = `<!doctype html><html><head><meta charset="utf-8"><title>Infrastructure</title><style>
406
+ body{font-family:system-ui,sans-serif;margin:0;background:#f6f7f9;color:#1c2128;padding:16px}
407
+ h1{font-size:16px;margin:0 0 12px}
408
+ table{border-collapse:collapse;width:100%;max-width:820px;font-size:13px}
409
+ th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #e1e5ea}
410
+ th{font-size:11px;text-transform:uppercase;color:#5b6572}
411
+ .dot{display:inline-block;width:9px;height:9px;border-radius:99px;background:#8a94a0;margin-right:6px;vertical-align:middle}
412
+ .dot.up{background:#1c8a4a}.dot.down{background:#c23b3b}
413
+ .url{color:#1a66d0;text-decoration:underline}
414
+ .muted{color:#5b6572;font-size:12px}
415
+ .legend{margin-top:12px;font-size:11px;color:#5b6572}
416
+ </style></head><body>
417
+ <h1>Infrastructure</h1>
418
+ <table><thead><tr><th>Status</th><th>Service</th><th>Endpoint</th><th>Console</th></tr></thead>
419
+ <tbody id="rows"><tr><td colspan="4" class="muted">Loading…</td></tr></tbody></table>
420
+ <div class="legend">● up ○ down — TCP reachability, polled every 3s</div>
421
+ <script>
422
+ function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])) }
423
+ async function refresh(){
424
+ const s = await fetch('/api/infra').then(r=>r.json()).catch(()=>null)
425
+ if(!s) return
426
+ const rows = (s.infra||[]).map(i => \`
427
+ <tr>
428
+ <td><span class="dot \${i.up?'up':'down'}"></span>\${i.up?'up':'down'}</td>
429
+ <td>\${esc(i.name)}\${i.description?' <span class="muted">— '+esc(i.description)+'</span>':''}</td>
430
+ <td class="muted">\${esc(i.host)}:\${esc(i.port)}</td>
431
+ <td>\${i.url?'<a class="url" href="'+esc(i.url)+'" target="_blank" rel="noreferrer">open ↗</a>':'—'}</td>
432
+ </tr>\`).join('')
433
+ document.getElementById('rows').innerHTML = rows || '<tr><td colspan="4" class="muted">No infrastructure declared.</td></tr>'
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)
322
439
  refresh(); setInterval(refresh, 3000)
323
440
  </script></body></html>`
324
441
 
@@ -328,8 +445,20 @@ const server = http.createServer(async (req, res) => {
328
445
  res.writeHead(200, { 'content-type': 'text/html' })
329
446
  return res.end(PAGE)
330
447
  }
448
+ if (req.method === 'GET' && req.url === '/infra') {
449
+ res.writeHead(200, { 'content-type': 'text/html' })
450
+ return res.end(INFRA_PAGE)
451
+ }
452
+ if (req.method === 'GET' && req.url === '/api/infra') {
453
+ const infra = await Promise.all((loadConfig().infra || []).map(async (i) => ({
454
+ id: i.id, name: i.name, host: i.host || '127.0.0.1', port: i.port,
455
+ url: i.url || null, description: i.description || '',
456
+ up: await tcpProbe(i.host || '127.0.0.1', i.port),
457
+ })))
458
+ return json(res, 200, { infra })
459
+ }
331
460
  if (req.method === 'GET' && req.url === '/api/state') {
332
- const apps = await Promise.all(CONFIG.apps.map(async (a) => {
461
+ const apps = await Promise.all(loadConfig().apps.map(async (a) => {
333
462
  const healthy = await probe(a)
334
463
  const phase = activity.get(a.name) || (healthy ? 'running' : failed.has(a.name) ? 'failed' : 'stopped')
335
464
  return {
@@ -340,11 +469,11 @@ const server = http.createServer(async (req, res) => {
340
469
  error: lastError.get(a.name) || null,
341
470
  }
342
471
  }))
343
- return json(res, 200, { currentBranch: currentBranch(), apps })
472
+ return json(res, 200, { currentBranch: currentBranch(), hasInfra: (loadConfig().infra || []).length > 0, apps })
344
473
  }
345
474
  const m = req.url && req.url.match(/^\/api\/apps\/([^/]+)\/(build|start|stop|deploy)$/)
346
475
  if (m && req.method === 'POST') {
347
- const app = CONFIG.apps.find((a) => a.name === decodeURIComponent(m[1]))
476
+ const app = loadConfig().apps.find((a) => a.name === decodeURIComponent(m[1]))
348
477
  if (!app) return json(res, 404, { error: 'unknown app' })
349
478
  if (m[2] !== 'stop' && developedOnce(app) === false) {
350
479
  return json(res, 409, { error: 'application has not completed a develop workflow run' })
@@ -368,7 +497,7 @@ server.on('upgrade', (req, socket) => {
368
497
  name = url.searchParams.get('app') || ''
369
498
  } catch {}
370
499
  const key = req.headers['sec-websocket-key']
371
- const app = CONFIG.apps.find((a) => a.name === name)
500
+ const app = loadConfig().apps.find((a) => a.name === name)
372
501
  if (!app || !key || !isWsPath) { socket.destroy(); return }
373
502
  const accept = createHash('sha1').update(key + WS_GUID).digest('base64')
374
503
  socket.write(
@@ -389,9 +518,11 @@ server.on('upgrade', (req, socket) => {
389
518
  server.listen(PORT, () => console.log(`cicd app on :${PORT}`))
390
519
  ```
391
520
 
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`):
521
+ `cicd/app/cicd.config.json` shape — one `apps` entry per application from the plan's Deployment
522
+ Decisions table, one `infra` entry per row of the plan's `## Infrastructure` table.
523
+ `deployMethod`/`deploy` come from the plan's Deploy columns (`deploy` is `null` when the method
524
+ is `none`). `infra[].url` is `null` for a headless service (Console URL cell `-`); emit
525
+ `"infra": []` when the plan declares no supporting services:
395
526
 
396
527
  ```json
397
528
  {
@@ -409,6 +540,24 @@ Decisions table. `deployMethod`/`deploy` come from the plan's Deploy columns (`d
409
540
  "deploy": "kubectl apply -f my-app/k8s/",
410
541
  "health": "http://127.0.0.1:3000/"
411
542
  }
543
+ ],
544
+ "infra": [
545
+ {
546
+ "id": "postgres-main",
547
+ "name": "PostgreSQL (app db)",
548
+ "host": "127.0.0.1",
549
+ "port": 5432,
550
+ "url": null,
551
+ "description": "Shared application database"
552
+ },
553
+ {
554
+ "id": "keycloak",
555
+ "name": "Keycloak",
556
+ "host": "127.0.0.1",
557
+ "port": 8180,
558
+ "url": "http://127.0.0.1:8180",
559
+ "description": "Single sign-on"
560
+ }
412
561
  ]
413
562
  }
414
563
  ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: util-plancicd
3
- model: claude-sonnet-5
3
+ model: claude-opus-4-8
4
4
  effort: high
5
5
  description: >
6
6
  Infer a local-deployment plan for every custom application of a CO2 project and write it to
@@ -10,7 +10,11 @@ description: >
10
10
  method (container vs bare process), build command (e.g. Spring Boot jar vs Docker image),
11
11
  start command, stop mechanism, deploy method + command (none / web-server copy / docker run /
12
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
13
+ port for the generated CI/CD app itself. It also enumerates the project's supporting
14
+ infrastructure (3rd-party services from CLAUDE.md's `# Supporting 3rd Party Applications` and
15
+ ENVIRONMENT.md — host, port and optional console URL) into an `## Infrastructure` section so
16
+ the generated CI/CD app can surface a status-only Infrastructure page. The plan carries a
17
+ top-level
14
18
  `**Status**: IN PROGRESS` header flipped to `COMPLETED` when done — the studio's watcher reads
15
19
  it. This skill NEVER generates the CI/CD app (that is util-gencicdscript, which runs only
16
20
  after the studio stamps an **Approved**: line into the plan). Trigger on keywords: "plan
@@ -43,6 +47,7 @@ If the argument is missing or the file does not exist, STOP and tell the user to
43
47
  | Dev tools | `DEVTOOL.md` (project root) |
44
48
  | Project context | `CLAUDE.md` (project root; `source/CLAUDE.md` fallback) |
45
49
  | App specification | `<app_folder>/context/specification/SPECIFICATION.md` |
50
+ | Infrastructure | `CLAUDE.md` `# Supporting 3rd Party Applications` + `ENVIRONMENT.md` |
46
51
  | Output plan | `cicd/CICD_PLAN.md` |
47
52
 
48
53
  ## Phase 1 — Gather
@@ -50,11 +55,18 @@ If the argument is missing or the file does not exist, STOP and tell the user to
50
55
  1. Read the plan-request markdown; its `## Applications` list is the authoritative application set.
51
56
  2. Read `ENVIRONMENT.md` (registries, credentials, external services) and `DEVTOOL.md`
52
57
  (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).
58
+ 3. Read `CLAUDE.md`: `# Custom Applications` (folder names, Depends on), `# Port Allocation`
59
+ (per-application ports), and `# Supporting 3rd Party Applications` (the infrastructure the
60
+ applications depend on — databases, caches, object storage, SSO, mail, message brokers, etc.).
55
61
  4. For each application, read its SPECIFICATION.md tech-stack section if present; otherwise
56
62
  infer the stack from build files (`package.json` → node, `pom.xml` → spring-boot,
57
63
  `composer.json` → laravel).
64
+ 5. Build the **infrastructure list**: one entry per supporting 3rd-party service the project
65
+ declares. For each, resolve `host` (default `127.0.0.1`), `port` and — when it exposes a web
66
+ console/UI — a `url`, cross-referencing ENVIRONMENT.md for the actual host/port/endpoint. A
67
+ service with no host:port to probe (or a purely external SaaS with no local endpoint) is
68
+ omitted. If the project declares no supporting 3rd-party services, the infrastructure list is
69
+ empty and the plan's `## Infrastructure` section is written empty.
58
70
 
59
71
  ## Phase 2 — Decide (per application)
60
72
 
@@ -84,9 +96,33 @@ If the argument is missing or the file does not exist, STOP and tell the user to
84
96
  needs no deploy step beyond Start; never invent a target the config does not name.
85
97
  - **Port**: from `# Port Allocation`; if absent, assign sequentially from 3000 avoiding
86
98
  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.
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).
88
109
  - **CI/CD app port**: 4100 unless taken in `# Port Allocation`; record it in the plan.
89
110
 
111
+ ### Infrastructure (status-only)
112
+
113
+ Supporting 3rd-party services are **not** built/started/deployed by the CI/CD app — they are
114
+ surfaced status-only. For each service resolved in Gather step 5, decide:
115
+
116
+ - **id**: a stable kebab-case key unique within the plan (e.g. `postgres-main`, `keycloak`).
117
+ - **name**: display name (e.g. `PostgreSQL (skf_main)`, `Keycloak`).
118
+ - **host** / **port**: the address the CI/CD app TCP-probes for reachability; default host
119
+ `127.0.0.1`. Use the values ENVIRONMENT.md documents.
120
+ - **url**: the web console/UI opened in a new tab, when the service has one (Keycloak admin,
121
+ MinIO console, Mailcatcher inbox); `-` for headless services (PostgreSQL, Redis).
122
+ - **description**: a short blurb (e.g. `Shared app database`, `S3-compatible object storage`).
123
+
124
+ Never emit credentials into the infrastructure entries — status and links only.
125
+
90
126
  ## Phase 3 — Write the plan
91
127
 
92
128
  Create `cicd/` if missing and write `cicd/CICD_PLAN.md`:
@@ -108,6 +144,20 @@ Create `cicd/` if missing and write `cicd/CICD_PLAN.md`:
108
144
  |---|---|---|---|---|---|---|---|---|---|
109
145
  | <name> | <folder> | process|container | <port> | `<cmd>` | `<cmd>` | process-kill|docker stop | none|web-server|docker|kubernetes | `<cmd or ->` | <url> |
110
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
+
152
+ ## Infrastructure
153
+
154
+ Supporting 3rd-party services shown status-only (TCP reachability) on the CI/CD app's
155
+ Infrastructure page. Leave the table body empty when the project declares no supporting services.
156
+
157
+ | Id | Service | Host | Port | Console URL | Description |
158
+ |---|---|---|---|---|---|
159
+ | <id> | <name> | <host> | <port> | `<url or ->` | <blurb> |
160
+
111
161
  ## Rationale
112
162
 
113
163
  - <per-application: why this method/commands, citing ENVIRONMENT.md/DEVTOOL.md evidence>
@@ -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};