@monster-orange/den 0.0.0-stage → 0.1.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.
- package/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +166 -2
- package/bin/den +61 -0
- package/docs/distributions.md +256 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +104 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +406 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +108 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +729 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +566 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +589 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +78 -0
- package/src/shared/environment-manager/log-tail.ts +212 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +334 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1588 -0
- package/src/shared/environment-manager/up-plan.ts +302 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/read-manifest.ts +160 -0
- package/src/shared/manifest/schema.ts +299 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-manifest.ts +140 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +178 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +88 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +678 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +116 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +91 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +524 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3656 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +144 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1312 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/server/changelog-parser.ts +94 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3236 -0
- package/src/ui/server/log-history.ts +452 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +494 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/header.js +1371 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +7405 -0
- package/src/ui/server/public/logs.html +1489 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1098 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- package/src/ui/server/workspace-routes.ts +2154 -0
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (current) {
|
|
90
|
+
current.raw = currentLines.join("\n").trimEnd();
|
|
91
|
+
entries.push(current);
|
|
92
|
+
}
|
|
93
|
+
return entries;
|
|
94
|
+
}
|
|
@@ -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
|
+
}
|