@monster-orange/den 0.0.0 → 0.2.0

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 (255) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -1
  4. package/bin/den +68 -0
  5. package/docs/distributions.md +499 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +101 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +403 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +124 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +757 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/agent-actor.ts +77 -0
  91. package/src/shared/applications.ts +138 -0
  92. package/src/shared/auth/api-token.ts +416 -0
  93. package/src/shared/auth/auth0-helper.ts +284 -0
  94. package/src/shared/auth/headless-login.ts +265 -0
  95. package/src/shared/auth/jwt-helper.ts +186 -0
  96. package/src/shared/auth/sudo-vault.ts +69 -0
  97. package/src/shared/auth/test-accounts.ts +98 -0
  98. package/src/shared/auth/token-injection.ts +184 -0
  99. package/src/shared/auth/token-providers/auth0.ts +80 -0
  100. package/src/shared/auth/token-providers/index.ts +182 -0
  101. package/src/shared/auth/token-refresh.ts +273 -0
  102. package/src/shared/banner.ts +15 -0
  103. package/src/shared/brand.ts +79 -0
  104. package/src/shared/cli-tabs.ts +131 -0
  105. package/src/shared/constants.ts +128 -0
  106. package/src/shared/den-config.ts +688 -0
  107. package/src/shared/dev-metrics/emit.ts +354 -0
  108. package/src/shared/edge/config.ts +229 -0
  109. package/src/shared/edge/render.ts +221 -0
  110. package/src/shared/edge/runtime.ts +268 -0
  111. package/src/shared/edge/status.ts +74 -0
  112. package/src/shared/edge/types.ts +62 -0
  113. package/src/shared/engine.ts +67 -0
  114. package/src/shared/environment/aws-profiles.ts +49 -0
  115. package/src/shared/environment/env-selector.ts +161 -0
  116. package/src/shared/environment/playwright-env.ts +82 -0
  117. package/src/shared/environment-manager/catalog.ts +77 -0
  118. package/src/shared/environment-manager/clone-guard.ts +163 -0
  119. package/src/shared/environment-manager/dag.ts +286 -0
  120. package/src/shared/environment-manager/default-workspace.ts +182 -0
  121. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  122. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  123. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  124. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  125. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  126. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  127. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  128. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  129. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  130. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  131. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  132. package/src/shared/environment-manager/derivation/text.ts +40 -0
  133. package/src/shared/environment-manager/derivation/types.ts +69 -0
  134. package/src/shared/environment-manager/health.ts +635 -0
  135. package/src/shared/environment-manager/index.ts +6 -0
  136. package/src/shared/environment-manager/load-applications.ts +108 -0
  137. package/src/shared/environment-manager/log-tail.ts +213 -0
  138. package/src/shared/environment-manager/make-targets.ts +78 -0
  139. package/src/shared/environment-manager/opctl.ts +335 -0
  140. package/src/shared/environment-manager/readiness.ts +93 -0
  141. package/src/shared/environment-manager/register-local.ts +72 -0
  142. package/src/shared/environment-manager/secrets-check.ts +260 -0
  143. package/src/shared/environment-manager/service-container.ts +47 -0
  144. package/src/shared/environment-manager/service-env.ts +110 -0
  145. package/src/shared/environment-manager/service-runner.ts +1603 -0
  146. package/src/shared/environment-manager/sync-targets.ts +66 -0
  147. package/src/shared/environment-manager/up-plan.ts +326 -0
  148. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  149. package/src/shared/environment-manager/workspaces.ts +860 -0
  150. package/src/shared/manifest/deps.ts +27 -0
  151. package/src/shared/manifest/merge-override.ts +75 -0
  152. package/src/shared/manifest/migrate-manifest.ts +101 -0
  153. package/src/shared/manifest/placement.ts +321 -0
  154. package/src/shared/manifest/read-manifest.ts +278 -0
  155. package/src/shared/manifest/schema.ts +313 -0
  156. package/src/shared/manifest/service-links.ts +93 -0
  157. package/src/shared/manifest/validate-layers.ts +229 -0
  158. package/src/shared/manifest/validate-manifest.ts +200 -0
  159. package/src/shared/manifest/workspace-manifests.ts +92 -0
  160. package/src/shared/open-url.ts +122 -0
  161. package/src/shared/org.ts +44 -0
  162. package/src/shared/paths.ts +190 -0
  163. package/src/shared/perf/optional-fields.ts +105 -0
  164. package/src/shared/perf/report-normalizer.ts +404 -0
  165. package/src/shared/perf/report-paths.ts +19 -0
  166. package/src/shared/perf/report-store.ts +110 -0
  167. package/src/shared/perf/report-types.ts +70 -0
  168. package/src/shared/perf/test-paths.ts +25 -0
  169. package/src/shared/process/log-rotation.ts +147 -0
  170. package/src/shared/process/spawn-streaming.ts +229 -0
  171. package/src/shared/product.ts +92 -0
  172. package/src/shared/profile.ts +141 -0
  173. package/src/shared/redact.ts +107 -0
  174. package/src/shared/repo-kind.ts +32 -0
  175. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  176. package/src/shared/reporting/slack-reporter.ts +340 -0
  177. package/src/shared/results/result-reader.ts +266 -0
  178. package/src/shared/results/result-writer.ts +168 -0
  179. package/src/shared/secrets/doppler.ts +126 -0
  180. package/src/shared/secrets/env.ts +48 -0
  181. package/src/shared/secrets/index.ts +197 -0
  182. package/src/shared/secrets/keys.ts +33 -0
  183. package/src/shared/secrets/onepassword.ts +75 -0
  184. package/src/shared/secrets/types.ts +52 -0
  185. package/src/shared/service-url.ts +119 -0
  186. package/src/shared/smoke/branch-resolver.ts +141 -0
  187. package/src/shared/smoke/git-remote.ts +70 -0
  188. package/src/shared/smoke/repo-cloner.ts +151 -0
  189. package/src/shared/stacks/config.ts +137 -0
  190. package/src/shared/stacks/manifest-commands.ts +118 -0
  191. package/src/shared/stacks/render.ts +253 -0
  192. package/src/shared/stacks/runtime.ts +334 -0
  193. package/src/shared/stacks/status.ts +99 -0
  194. package/src/shared/stacks/types.ts +53 -0
  195. package/src/shared/terminal/tabs.ts +212 -0
  196. package/src/shared/test-discovery/module-scanner.ts +569 -0
  197. package/src/shared/types/index.ts +701 -0
  198. package/src/shared/ui-marker.ts +19 -0
  199. package/src/ui/cli/applications-command.ts +148 -0
  200. package/src/ui/cli/config-command.ts +68 -0
  201. package/src/ui/cli/create-main.ts +9 -0
  202. package/src/ui/cli/create.ts +360 -0
  203. package/src/ui/cli/edge-command.ts +81 -0
  204. package/src/ui/cli/flag-parser.ts +246 -0
  205. package/src/ui/cli/graph-command.ts +570 -0
  206. package/src/ui/cli/health-display.ts +63 -0
  207. package/src/ui/cli/index.ts +3742 -0
  208. package/src/ui/cli/init.ts +855 -0
  209. package/src/ui/cli/invocation-target.ts +175 -0
  210. package/src/ui/cli/logs-command.ts +104 -0
  211. package/src/ui/cli/mcp-main.ts +13 -0
  212. package/src/ui/cli/mcp-stdout-guard.ts +10 -0
  213. package/src/ui/cli/module-selection.ts +31 -0
  214. package/src/ui/cli/prompts.ts +767 -0
  215. package/src/ui/cli/results-viewer.ts +436 -0
  216. package/src/ui/cli/secrets-preflight.ts +91 -0
  217. package/src/ui/cli/stack-command.ts +117 -0
  218. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  219. package/src/ui/cli/test-runner.ts +1319 -0
  220. package/src/ui/cli/web-command.ts +1073 -0
  221. package/src/ui/mcp/api.ts +284 -0
  222. package/src/ui/mcp/guards.ts +141 -0
  223. package/src/ui/mcp/server.ts +147 -0
  224. package/src/ui/mcp/tools.ts +565 -0
  225. package/src/ui/server/audit.ts +71 -0
  226. package/src/ui/server/changelog-parser.ts +105 -0
  227. package/src/ui/server/daemon-reaper.ts +129 -0
  228. package/src/ui/server/editor-manager.ts +301 -0
  229. package/src/ui/server/github-service.ts +223 -0
  230. package/src/ui/server/graph-prefs.ts +89 -0
  231. package/src/ui/server/health-row.ts +15 -0
  232. package/src/ui/server/http-helpers.ts +98 -0
  233. package/src/ui/server/index.ts +3350 -0
  234. package/src/ui/server/log-history.ts +453 -0
  235. package/src/ui/server/perf-report.ts +42 -0
  236. package/src/ui/server/public/environments.html +496 -0
  237. package/src/ui/server/public/favicon.ico +0 -0
  238. package/src/ui/server/public/graph-model.js +416 -0
  239. package/src/ui/server/public/graph.css +976 -0
  240. package/src/ui/server/public/graph.js +986 -0
  241. package/src/ui/server/public/header.js +1373 -0
  242. package/src/ui/server/public/index.html +2595 -0
  243. package/src/ui/server/public/local-dev.html +5344 -0
  244. package/src/ui/server/public/logs.html +1496 -0
  245. package/src/ui/server/public/panel.js +787 -0
  246. package/src/ui/server/public/perf-report.js +359 -0
  247. package/src/ui/server/public/release.html +2111 -0
  248. package/src/ui/server/public/services.html +1103 -0
  249. package/src/ui/server/public/workspace-picker.js +69 -0
  250. package/src/ui/server/readonly-routes.ts +200 -0
  251. package/src/ui/server/release-deploy-session.ts +246 -0
  252. package/src/ui/server/release-service.ts +1125 -0
  253. package/src/ui/server/sudo-store.ts +111 -0
  254. package/src/ui/server/ui-assets.ts +73 -0
  255. package/src/ui/server/workspace-routes.ts +2185 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Changelog parser — parses Keep a Changelog format into structured entries.
3
+ *
4
+ * Extracted from server/index.ts. Used by the /api/changelog endpoint
5
+ * and the startup banner version detection.
6
+ */
7
+
8
+ export interface ChangelogBullet {
9
+ text: string;
10
+ sub: string[];
11
+ }
12
+
13
+ export interface ChangelogEntry {
14
+ version: string;
15
+ date: string;
16
+ /** Free-form text between the version header and the first ### section. */
17
+ note?: string;
18
+ sections: Record<string, ChangelogBullet[]>;
19
+ /** Raw markdown block for this version entry, suitable for sharing. */
20
+ raw: string;
21
+ }
22
+
23
+ /**
24
+ * Parse a Keep a Changelog-style CHANGELOG.md into structured entries.
25
+ * Each section starts with: ## [version] — YYYY-MM-DD
26
+ */
27
+ export function parseChangelog(raw: string): ChangelogEntry[] {
28
+ const entries: ChangelogEntry[] = [];
29
+ const versionPattern = /^##\s+\[([^\]]+)\]\s*[—–-]\s*(\S+)/;
30
+ const sectionPattern = /^###\s+(.+)/;
31
+
32
+ let current: ChangelogEntry | null = null;
33
+ let currentSection: string | null = null;
34
+ let currentLines: string[] = [];
35
+
36
+ const lines = raw.split("\n");
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const line = lines[i];
39
+ const vMatch = line.match(versionPattern);
40
+ if (vMatch) {
41
+ if (current) {
42
+ current.raw = currentLines.join("\n").trimEnd();
43
+ entries.push(current);
44
+ }
45
+ current = { version: vMatch[1], date: vMatch[2], sections: {}, raw: "" };
46
+ currentSection = null;
47
+ currentLines = [line];
48
+ continue;
49
+ }
50
+ if (!current) continue;
51
+
52
+ // Stop collecting raw lines when we hit the next version separator ("---")
53
+ if (line.trim() === "---") {
54
+ current.raw = currentLines.join("\n").trimEnd();
55
+ entries.push(current);
56
+ current = null;
57
+ currentLines = [];
58
+ continue;
59
+ }
60
+
61
+ currentLines.push(line);
62
+
63
+ const sMatch = line.match(sectionPattern);
64
+ if (sMatch) {
65
+ currentSection = sMatch[1];
66
+ current.sections[currentSection] = [];
67
+ continue;
68
+ }
69
+
70
+ // Capture free text before the first ### section as a note
71
+ if (!currentSection && line.trim() && !line.startsWith("#")) {
72
+ current.note = (current.note ? current.note + " " : "") + line.trim();
73
+ }
74
+
75
+ if (currentSection && line.startsWith("- ")) {
76
+ current.sections[currentSection].push({
77
+ text: line.slice(2).trim(),
78
+ sub: [],
79
+ });
80
+ } else if (currentSection && /^ {2}- /.test(line)) {
81
+ const bullets = current.sections[currentSection];
82
+ if (bullets.length > 0) {
83
+ bullets[bullets.length - 1].sub.push(
84
+ line.replace(/^ {2}- /, "").trim(),
85
+ );
86
+ }
87
+ } else if (currentSection && /^ {2,}\S/.test(line)) {
88
+ // A wrapped continuation line: it belongs to the bullet (or sub-bullet)
89
+ // above it. Dropping it cut every multi-line bullet mid-sentence.
90
+ const bullets = current.sections[currentSection];
91
+ const last = bullets[bullets.length - 1];
92
+ if (last) {
93
+ const text = line.trim();
94
+ if (/^ {4,}/.test(line) && last.sub.length > 0)
95
+ last.sub[last.sub.length - 1] += " " + text;
96
+ else last.text += " " + text;
97
+ }
98
+ }
99
+ }
100
+ if (current) {
101
+ current.raw = currentLines.join("\n").trimEnd();
102
+ entries.push(current);
103
+ }
104
+ return entries;
105
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Daemon-registry recovery sweep (Web-UI server startup).
3
+ *
4
+ * After a den restart, previously registered daemons are reloaded from
5
+ * disk and health-checked. Entries whose service is explicitly unhealthy
6
+ * (was stopped outside of den) are removed so the shutdown prompt
7
+ * doesn't list them.
8
+ *
9
+ * Removal requires STRONG evidence (review finding 11): a single
10
+ * transient timeout/5xx must never unregister a possibly-running daemon,
11
+ * so an "error" health result is only acted on after a SECOND
12
+ * consecutive "error" check separated by a retry delay. "unknown"
13
+ * results and thrown checks always keep the entry (err on the side of
14
+ * keeping). A daemon whose address no longer resolves to any
15
+ * workspace/catalog service is process-death-equivalent evidence and is
16
+ * removed directly.
17
+ */
18
+
19
+ import type { ServiceHealthResult } from "../../shared/environment-manager/health.js";
20
+ import type { DaemonRecord } from "../../shared/environment-manager/service-runner.js";
21
+ import type { ResolvedService } from "../../shared/types/index.js";
22
+
23
+ export interface DaemonReaperDeps {
24
+ listDaemons: () => DaemonRecord[];
25
+ /**
26
+ * Resolve an address, optionally against a named workspace. Omitting the
27
+ * workspace resolves against the focused one only, which is not enough with
28
+ * several workspaces active — see `resolveForDaemon`.
29
+ */
30
+ resolveService: (
31
+ address: string,
32
+ workspace?: string,
33
+ ) => ResolvedService | null;
34
+ /** Names of the currently active workspaces — see `resolveForDaemon`. */
35
+ getActiveWorkspaces: () => string[];
36
+ checkServiceHealth: (
37
+ service: ResolvedService,
38
+ ) => Promise<ServiceHealthResult>;
39
+ unregisterDaemon: (address: string) => void;
40
+ /** Injectable sleep between the two confirmation checks. */
41
+ delay: (ms: number) => Promise<void>;
42
+ /** Gap between the first "error" result and the confirming re-check. */
43
+ retryDelayMs: number;
44
+ }
45
+
46
+ export interface DaemonReapResult {
47
+ /** Addresses removed from the registry. */
48
+ removed: string[];
49
+ /** Addresses kept (healthy, unknown, transient failure, or check threw). */
50
+ kept: string[];
51
+ }
52
+
53
+ /**
54
+ * Resolve a daemon's service, trying the focus, then each workspace that owns
55
+ * it, then every other active workspace, before concluding the service is gone.
56
+ * The focused workspace alone would drop every daemon belonging to another
57
+ * active workspace the moment focus moves — and its containers keep running, so
58
+ * the record is exactly what's needed to stop them.
59
+ *
60
+ * The active set is the last resort because a legacy record carries no owners
61
+ * at all: without it, a running service defined only in a non-focused active
62
+ * workspace looks unresolvable and gets reaped. This mirrors
63
+ * `canResolveDaemonAddress`, which `pruneDeadDaemons` uses for the same call.
64
+ */
65
+ function resolveForDaemon(
66
+ deps: DaemonReaperDeps,
67
+ daemon: DaemonRecord,
68
+ ): ResolvedService | null {
69
+ const direct = deps.resolveService(daemon.address);
70
+ if (direct) return direct;
71
+ const candidates = [
72
+ ...new Set([...(daemon.workspaces ?? []), ...deps.getActiveWorkspaces()]),
73
+ ];
74
+ for (const workspace of candidates) {
75
+ const owned = deps.resolveService(daemon.address, workspace);
76
+ if (owned) return owned;
77
+ }
78
+ return null;
79
+ }
80
+
81
+ /**
82
+ * Sweep the daemon registry once. Unregisters an entry only when its
83
+ * service is gone from every owning workspace/catalog, or when TWO
84
+ * consecutive health checks report "error".
85
+ */
86
+ export async function reapDeadDaemons(
87
+ deps: DaemonReaperDeps,
88
+ ): Promise<DaemonReapResult> {
89
+ const removed: string[] = [];
90
+ const kept: string[] = [];
91
+
92
+ await Promise.allSettled(
93
+ deps.listDaemons().map(async (daemon) => {
94
+ try {
95
+ const service = resolveForDaemon(deps, daemon);
96
+ if (!service) {
97
+ // No longer in any owning workspace/catalog — nothing could restart it.
98
+ deps.unregisterDaemon(daemon.address);
99
+ removed.push(daemon.address);
100
+ return;
101
+ }
102
+
103
+ const first = await deps.checkServiceHealth(service);
104
+ if (first.health !== "error") {
105
+ // "ok" and "unknown" both keep the entry.
106
+ kept.push(daemon.address);
107
+ return;
108
+ }
109
+
110
+ // One "error" may be a transient timeout/5xx — confirm before
111
+ // unregistering a possibly-running daemon.
112
+ await deps.delay(deps.retryDelayMs);
113
+ const second = await deps.checkServiceHealth(service);
114
+ if (second.health === "error") {
115
+ deps.unregisterDaemon(daemon.address);
116
+ removed.push(daemon.address);
117
+ } else {
118
+ kept.push(daemon.address);
119
+ }
120
+ } catch {
121
+ // Err on the side of keeping the entry rather than wrongly
122
+ // removing it.
123
+ kept.push(daemon.address);
124
+ }
125
+ }),
126
+ );
127
+
128
+ return { removed, kept };
129
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Editor & Terminal Manager
3
+ *
4
+ * Extracted from server/index.ts. Handles detection of installed editors
5
+ * and terminal apps, path resolution, and availability listing.
6
+ */
7
+
8
+ import * as fs from "fs";
9
+ import * as os from "os";
10
+ import * as path from "path";
11
+ import { spawnSync } from "child_process";
12
+ import { detectStackConfig } from "../../ui/cli/init.js";
13
+
14
+ // ── Editor definitions ───────────────────────────────────────────────────────
15
+
16
+ interface EditorDef {
17
+ key: string;
18
+ name: string;
19
+ /** Binary name for `which` fallback (may differ from key). */
20
+ binName: string;
21
+ terminal: boolean;
22
+ paths: readonly string[];
23
+ }
24
+
25
+ const KNOWN_EDITORS: readonly EditorDef[] = [
26
+ {
27
+ key: "vscode",
28
+ name: "VS Code",
29
+ binName: "code",
30
+ terminal: false,
31
+ paths: [
32
+ "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
33
+ path.join(
34
+ os.homedir(),
35
+ "Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
36
+ ),
37
+ "/usr/local/bin/code",
38
+ "/opt/homebrew/bin/code",
39
+ ],
40
+ },
41
+ {
42
+ key: "cursor",
43
+ name: "Cursor",
44
+ binName: "cursor",
45
+ terminal: false,
46
+ paths: [
47
+ "/Applications/Cursor.app/Contents/Resources/app/bin/cursor",
48
+ path.join(
49
+ os.homedir(),
50
+ "Applications/Cursor.app/Contents/Resources/app/bin/cursor",
51
+ ),
52
+ "/usr/local/bin/cursor",
53
+ ],
54
+ },
55
+ {
56
+ key: "webstorm",
57
+ name: "WebStorm",
58
+ binName: "webstorm",
59
+ terminal: false,
60
+ paths: [
61
+ path.join(
62
+ os.homedir(),
63
+ "Library/Application Support/JetBrains/Toolbox/scripts/webstorm",
64
+ ),
65
+ "/usr/local/bin/webstorm",
66
+ ],
67
+ },
68
+ {
69
+ key: "idea",
70
+ name: "IntelliJ IDEA",
71
+ binName: "idea",
72
+ terminal: false,
73
+ paths: [
74
+ path.join(
75
+ os.homedir(),
76
+ "Library/Application Support/JetBrains/Toolbox/scripts/idea",
77
+ ),
78
+ "/usr/local/bin/idea",
79
+ ],
80
+ },
81
+ {
82
+ key: "zed",
83
+ name: "Zed",
84
+ binName: "zed",
85
+ terminal: false,
86
+ paths: [
87
+ "/Applications/Zed.app/Contents/MacOS/cli",
88
+ path.join(os.homedir(), "Applications/Zed.app/Contents/MacOS/cli"),
89
+ "/usr/local/bin/zed",
90
+ ],
91
+ },
92
+ {
93
+ key: "vim",
94
+ name: "vim",
95
+ binName: "vim",
96
+ terminal: true,
97
+ paths: ["/usr/bin/vim", "/opt/homebrew/bin/vim", "/usr/local/bin/vim"],
98
+ },
99
+ {
100
+ key: "nvim",
101
+ name: "Neovim",
102
+ binName: "nvim",
103
+ terminal: true,
104
+ paths: ["/opt/homebrew/bin/nvim", "/usr/local/bin/nvim", "/usr/bin/nvim"],
105
+ },
106
+ {
107
+ key: "emacs",
108
+ name: "Emacs",
109
+ binName: "emacs",
110
+ terminal: true,
111
+ paths: [
112
+ "/Applications/Emacs.app/Contents/MacOS/emacs",
113
+ "/opt/homebrew/bin/emacs",
114
+ "/usr/bin/emacs",
115
+ ],
116
+ },
117
+ ];
118
+
119
+ // ── Terminal definitions (macOS) ─────────────────────────────────────────────
120
+
121
+ interface TerminalDef {
122
+ key: string;
123
+ name: string;
124
+ appName: string;
125
+ appPaths: readonly string[];
126
+ }
127
+
128
+ const KNOWN_TERMINALS: readonly TerminalDef[] = [
129
+ {
130
+ key: "terminal",
131
+ name: "Terminal",
132
+ appName: "Terminal",
133
+ appPaths: ["/System/Applications/Utilities/Terminal.app"],
134
+ },
135
+ {
136
+ key: "iterm2",
137
+ name: "iTerm2",
138
+ appName: "iTerm",
139
+ appPaths: ["/Applications/iTerm.app"],
140
+ },
141
+ {
142
+ key: "ghostty",
143
+ name: "Ghostty",
144
+ appName: "Ghostty",
145
+ appPaths: [
146
+ "/Applications/Ghostty.app",
147
+ path.join(os.homedir(), "Applications/Ghostty.app"),
148
+ ],
149
+ },
150
+ {
151
+ key: "warp",
152
+ name: "Warp",
153
+ appName: "Warp",
154
+ appPaths: ["/Applications/Warp.app"],
155
+ },
156
+ {
157
+ key: "kitty",
158
+ name: "kitty",
159
+ appName: "kitty",
160
+ appPaths: [
161
+ "/Applications/kitty.app",
162
+ path.join(os.homedir(), "Applications/kitty.app"),
163
+ ],
164
+ },
165
+ {
166
+ key: "alacritty",
167
+ name: "Alacritty",
168
+ appName: "Alacritty",
169
+ appPaths: [
170
+ "/Applications/Alacritty.app",
171
+ path.join(os.homedir(), "Applications/Alacritty.app"),
172
+ ],
173
+ },
174
+ {
175
+ key: "hyper",
176
+ name: "Hyper",
177
+ appName: "Hyper",
178
+ appPaths: ["/Applications/Hyper.app"],
179
+ },
180
+ ];
181
+
182
+ /** JetBrains editors that open via CLI but need special `--line` arg handling. */
183
+ export const JETBRAINS_EDITORS = new Set(["webstorm", "idea"]);
184
+
185
+ // ── Supported languages for per-language IDE overrides ───────────────────────
186
+
187
+ export const SUPPORTED_LANGUAGES = [
188
+ { key: "go", name: "Go" },
189
+ { key: "python", name: "Python" },
190
+ { key: "typescript", name: "TypeScript" },
191
+ { key: "javascript", name: "JavaScript" },
192
+ { key: "java", name: "Java" },
193
+ { key: "kotlin", name: "Kotlin" },
194
+ { key: "rust", name: "Rust" },
195
+ { key: "ruby", name: "Ruby" },
196
+ { key: "swift", name: "Swift" },
197
+ { key: "csharp", name: "C#" },
198
+ ] as const;
199
+
200
+ // ── EditorManager ────────────────────────────────────────────────────────────
201
+
202
+ export class EditorManager {
203
+ /** Resolve the executable path for an editor key, or null if not installed. */
204
+ resolveEditorCmd(key: string): string | null {
205
+ const editor = KNOWN_EDITORS.find((e) => e.key === key);
206
+ if (!editor) return null;
207
+ for (const p of editor.paths) {
208
+ if (fs.existsSync(p)) return p;
209
+ }
210
+ try {
211
+ const result = spawnSync("which", [editor.binName], {
212
+ encoding: "utf-8",
213
+ });
214
+ const found = result.stdout?.trim();
215
+ if (found && fs.existsSync(found)) return found;
216
+ } catch {
217
+ // ignore
218
+ }
219
+ return null;
220
+ }
221
+
222
+ /** Resolve the macOS app name for a terminal key, or null if not installed. */
223
+ resolveTerminalApp(key: string): string | null {
224
+ const terminal = KNOWN_TERMINALS.find((t) => t.key === key);
225
+ if (!terminal) return null;
226
+ for (const p of terminal.appPaths) {
227
+ if (fs.existsSync(p)) return terminal.appName;
228
+ }
229
+ return null;
230
+ }
231
+
232
+ /** List all known editors with their availability status. */
233
+ listEditors(): Array<{
234
+ key: string;
235
+ name: string;
236
+ terminal: boolean;
237
+ available: boolean;
238
+ }> {
239
+ return KNOWN_EDITORS.map((e) => ({
240
+ key: e.key,
241
+ name: e.name,
242
+ terminal: e.terminal,
243
+ available: !!this.resolveEditorCmd(e.key),
244
+ }));
245
+ }
246
+
247
+ /** List all known terminals with their availability status. */
248
+ listTerminals(): Array<{ key: string; name: string; available: boolean }> {
249
+ return KNOWN_TERMINALS.map((t) => ({
250
+ key: t.key,
251
+ name: t.name,
252
+ available: !!this.resolveTerminalApp(t.key),
253
+ }));
254
+ }
255
+
256
+ /** True if the editor key is a terminal-based editor (vim, nvim, emacs). */
257
+ isTerminalEditor(key: string): boolean {
258
+ return KNOWN_EDITORS.find((e) => e.key === key)?.terminal ?? false;
259
+ }
260
+
261
+ /** True if the key matches a known editor. */
262
+ isKnownEditor(key: string): boolean {
263
+ return KNOWN_EDITORS.some((e) => e.key === key);
264
+ }
265
+
266
+ /** True if the key matches a known terminal. */
267
+ isKnownTerminal(key: string): boolean {
268
+ return KNOWN_TERMINALS.some((t) => t.key === key);
269
+ }
270
+
271
+ /**
272
+ * Resolve which editor to use for a path, considering overrides.
273
+ *
274
+ * Resolution order (first match wins):
275
+ * 1. Per-language override — repo has stack.yml with `language` field
276
+ * matching a key in `languageOverrides`
277
+ * 2. Workspace editor — the active workspace has an `editor` override
278
+ * 3. Global default — the user's default editor from settings
279
+ */
280
+ resolveEditorForRepo(
281
+ repoPath: string,
282
+ defaultEditor: string | undefined,
283
+ languageOverrides: Record<string, string> | undefined,
284
+ workspaceEditor?: string,
285
+ ): string | undefined {
286
+ // 1. Per-language override (repo-level)
287
+ if (languageOverrides && Object.keys(languageOverrides).length > 0) {
288
+ const stack = detectStackConfig(repoPath);
289
+ const lang = stack?.language;
290
+ if (lang && languageOverrides[lang]) {
291
+ return languageOverrides[lang];
292
+ }
293
+ }
294
+ // 2. Workspace editor override
295
+ if (workspaceEditor) {
296
+ return workspaceEditor;
297
+ }
298
+ // 3. Global default
299
+ return defaultEditor;
300
+ }
301
+ }