@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.
- package/CHANGELOG.md +204 -0
- package/LICENSE +21 -0
- package/README.md +165 -1
- package/bin/den +68 -0
- package/docs/distributions.md +499 -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 +101 -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 +403 -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 +124 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +757 -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/agent-actor.ts +77 -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 +688 -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/status.ts +74 -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 +635 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +108 -0
- package/src/shared/environment-manager/log-tail.ts +213 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +335 -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-container.ts +47 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1603 -0
- package/src/shared/environment-manager/sync-targets.ts +66 -0
- package/src/shared/environment-manager/up-plan.ts +326 -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/merge-override.ts +75 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/placement.ts +321 -0
- package/src/shared/manifest/read-manifest.ts +278 -0
- package/src/shared/manifest/schema.ts +313 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-layers.ts +229 -0
- package/src/shared/manifest/validate-manifest.ts +200 -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 +190 -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/redact.ts +107 -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 +118 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/status.ts +99 -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 +701 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +148 -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 +81 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +570 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3742 -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/mcp-main.ts +13 -0
- package/src/ui/cli/mcp-stdout-guard.ts +10 -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 +117 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1319 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/mcp/api.ts +284 -0
- package/src/ui/mcp/guards.ts +141 -0
- package/src/ui/mcp/server.ts +147 -0
- package/src/ui/mcp/tools.ts +565 -0
- package/src/ui/server/audit.ts +71 -0
- package/src/ui/server/changelog-parser.ts +105 -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/graph-prefs.ts +89 -0
- package/src/ui/server/health-row.ts +15 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3350 -0
- package/src/ui/server/log-history.ts +453 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +496 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/graph-model.js +416 -0
- package/src/ui/server/public/graph.css +976 -0
- package/src/ui/server/public/graph.js +986 -0
- package/src/ui/server/public/header.js +1373 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +5344 -0
- package/src/ui/server/public/logs.html +1496 -0
- package/src/ui/server/public/panel.js +787 -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 +1103 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/readonly-routes.ts +200 -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/ui-assets.ts +73 -0
- package/src/ui/server/workspace-routes.ts +2185 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `den graph` — derived system dependency analysis.
|
|
3
|
+
*
|
|
4
|
+
* Builds the dependency graph by deriving real signals from each repo (SDK
|
|
5
|
+
* imports, service URLs, IaC stack refs, EventBridge rules) rather than trusting
|
|
6
|
+
* each repo's `.den.yml` `depends_on`, and renders / diffs / annotates it.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* den graph [repo] graph for repo's dependency closure (defaults to cwd repo, else --all)
|
|
10
|
+
* den graph --all the full known-systems graph
|
|
11
|
+
* den graph --format=json|dot|text
|
|
12
|
+
* den graph --source=derived|declared|union
|
|
13
|
+
* den graph --kind=all|hard|infra|http|event
|
|
14
|
+
* den graph --diff compare derived deps vs .den.yml depends_on
|
|
15
|
+
* den graph --refresh ignore the per-repo cache
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
import {
|
|
20
|
+
parseAddress,
|
|
21
|
+
getActiveWorkspace,
|
|
22
|
+
noAutostartRepos,
|
|
23
|
+
} from "../../shared/environment-manager/workspaces.js";
|
|
24
|
+
import { readCatalog } from "../../shared/environment-manager/catalog.js";
|
|
25
|
+
import { readManifest } from "../../shared/manifest/read-manifest.js";
|
|
26
|
+
import {
|
|
27
|
+
repoPlacement,
|
|
28
|
+
type RollupPlacement,
|
|
29
|
+
} from "../../shared/manifest/placement.js";
|
|
30
|
+
import type { AliasResolver } from "../../shared/environment-manager/derivation/alias.js";
|
|
31
|
+
import { loadAliasResolver } from "../../shared/environment-manager/derivation/load-alias.js";
|
|
32
|
+
import {
|
|
33
|
+
deriveGraph,
|
|
34
|
+
resolveRepoPath,
|
|
35
|
+
repoSource,
|
|
36
|
+
buildContext,
|
|
37
|
+
} from "../../shared/environment-manager/derivation/derive-graph.js";
|
|
38
|
+
import { listMakeTargets } from "../../shared/environment-manager/make-targets.js";
|
|
39
|
+
import {
|
|
40
|
+
orderForUp,
|
|
41
|
+
orderForDown,
|
|
42
|
+
} from "../../shared/environment-manager/derivation/derived-up.js";
|
|
43
|
+
import type {
|
|
44
|
+
DerivedEdge,
|
|
45
|
+
DerivedGraph,
|
|
46
|
+
} from "../../shared/environment-manager/derivation/types.js";
|
|
47
|
+
import { findInvocationRepoRoot, resolveCwdRepo } from "./invocation-target.js";
|
|
48
|
+
import { getDistRoot } from "../../shared/paths.js";
|
|
49
|
+
import * as path from "path";
|
|
50
|
+
import * as os from "os";
|
|
51
|
+
|
|
52
|
+
import { product } from "../../shared/product.js";
|
|
53
|
+
type Source = "derived" | "declared" | "union";
|
|
54
|
+
type KindFilter = "all" | "hard" | "infra" | "http" | "event";
|
|
55
|
+
type Format = "text" | "json" | "dot";
|
|
56
|
+
type Order = "up" | "down";
|
|
57
|
+
|
|
58
|
+
export interface GraphFlags {
|
|
59
|
+
repo?: string;
|
|
60
|
+
all: boolean;
|
|
61
|
+
source: Source;
|
|
62
|
+
kind: KindFilter;
|
|
63
|
+
format: Format;
|
|
64
|
+
diff: boolean;
|
|
65
|
+
refresh: boolean;
|
|
66
|
+
order: Order;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const REQUIRED_TARGETS = ["setup", "up", "down"];
|
|
70
|
+
|
|
71
|
+
// ── pure helpers (unit-tested) ───────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
export function edgeMatchesKind(edge: DerivedEdge, kind: KindFilter): boolean {
|
|
74
|
+
switch (kind) {
|
|
75
|
+
case "all":
|
|
76
|
+
return true;
|
|
77
|
+
case "hard":
|
|
78
|
+
return edge.blocking === "hard";
|
|
79
|
+
case "infra":
|
|
80
|
+
return edge.kind === "infra";
|
|
81
|
+
case "http":
|
|
82
|
+
return edge.kind === "http" || edge.kind === "jwks";
|
|
83
|
+
case "event":
|
|
84
|
+
return edge.kind === "event";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Cross-service `depends_on` edges declared in each node's `.den.yml`. */
|
|
89
|
+
export function collectDeclaredEdges(
|
|
90
|
+
nodes: string[],
|
|
91
|
+
alias: AliasResolver,
|
|
92
|
+
): DerivedEdge[] {
|
|
93
|
+
const catalog = readCatalog();
|
|
94
|
+
const out: DerivedEdge[] = [];
|
|
95
|
+
for (const node of nodes) {
|
|
96
|
+
const repoPath = resolveRepoPath(node);
|
|
97
|
+
if (!repoPath) continue;
|
|
98
|
+
const manifest = readManifest(repoPath);
|
|
99
|
+
for (const svc of Object.values(manifest?.services ?? {})) {
|
|
100
|
+
for (const dep of svc.depends_on ?? []) {
|
|
101
|
+
const service = typeof dep === "string" ? dep : dep.service;
|
|
102
|
+
const wait = typeof dep === "string" ? false : dep.wait === true;
|
|
103
|
+
const repoName = parseAddress(service, catalog).repoName;
|
|
104
|
+
const to = alias.resolve(repoName);
|
|
105
|
+
if (!to || to === node || !nodes.includes(to)) continue;
|
|
106
|
+
out.push({
|
|
107
|
+
from: node,
|
|
108
|
+
to,
|
|
109
|
+
kind: "http",
|
|
110
|
+
blocking: wait ? "hard" : "soft",
|
|
111
|
+
signal: "declared:depends_on",
|
|
112
|
+
evidence: `${product().manifestYml} depends_on: ${service}`,
|
|
113
|
+
confidence: "high",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const edgeKey = (e: DerivedEdge): string => `${e.from}->${e.to}`;
|
|
122
|
+
|
|
123
|
+
export interface GraphDiff {
|
|
124
|
+
repo: string;
|
|
125
|
+
/** Derived (real) but not declared in `.den.yml`. */
|
|
126
|
+
undeclared: string[];
|
|
127
|
+
/** Declared in `.den.yml` but no derived signal found. */
|
|
128
|
+
stale: string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function diffEdges(
|
|
132
|
+
derived: DerivedEdge[],
|
|
133
|
+
declared: DerivedEdge[],
|
|
134
|
+
nodes: string[],
|
|
135
|
+
): GraphDiff[] {
|
|
136
|
+
// Compare on synchronous edges only (events are never declared as depends_on).
|
|
137
|
+
const sync = (e: DerivedEdge) => e.kind !== "event";
|
|
138
|
+
const derivedByRepo = new Map<string, Set<string>>();
|
|
139
|
+
const declaredByRepo = new Map<string, Set<string>>();
|
|
140
|
+
for (const e of derived.filter(sync))
|
|
141
|
+
(
|
|
142
|
+
derivedByRepo.get(e.from) ??
|
|
143
|
+
derivedByRepo.set(e.from, new Set()).get(e.from)!
|
|
144
|
+
).add(e.to);
|
|
145
|
+
for (const e of declared)
|
|
146
|
+
(
|
|
147
|
+
declaredByRepo.get(e.from) ??
|
|
148
|
+
declaredByRepo.set(e.from, new Set()).get(e.from)!
|
|
149
|
+
).add(e.to);
|
|
150
|
+
|
|
151
|
+
const out: GraphDiff[] = [];
|
|
152
|
+
for (const repo of nodes) {
|
|
153
|
+
const d = derivedByRepo.get(repo) ?? new Set();
|
|
154
|
+
const c = declaredByRepo.get(repo) ?? new Set();
|
|
155
|
+
const undeclared = [...d].filter((t) => !c.has(t)).sort();
|
|
156
|
+
const stale = [...c].filter((t) => !d.has(t)).sort();
|
|
157
|
+
if (undeclared.length || stale.length)
|
|
158
|
+
out.push({ repo, undeclared, stale });
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const ARROW: Record<DerivedEdge["kind"], string> = {
|
|
164
|
+
infra: "→",
|
|
165
|
+
http: "→",
|
|
166
|
+
jwks: "→",
|
|
167
|
+
event: "⇢",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export function renderText(
|
|
171
|
+
graph: DerivedGraph,
|
|
172
|
+
opts: {
|
|
173
|
+
showEvidence?: boolean;
|
|
174
|
+
annotations?: Map<string, string>;
|
|
175
|
+
/** Node for the repo the command was run from — marked with a leading `*`. */
|
|
176
|
+
currentRepo?: string;
|
|
177
|
+
/** repo → placement (R13): remote and hybrid nodes are marked. */
|
|
178
|
+
placements?: Map<string, NodePlacement>;
|
|
179
|
+
} = {},
|
|
180
|
+
): string {
|
|
181
|
+
const showEvidence = opts.showEvidence ?? true;
|
|
182
|
+
const byFrom = new Map<string, DerivedEdge[]>();
|
|
183
|
+
for (const e of graph.edges)
|
|
184
|
+
(byFrom.get(e.from) ?? byFrom.set(e.from, []).get(e.from)!).push(e);
|
|
185
|
+
|
|
186
|
+
// Pad node names to the longest so the annotations (den managed / sibling
|
|
187
|
+
// repo …) line up in a single column. Pad the PLAIN name then colorize, so the
|
|
188
|
+
// ANSI codes don't throw off the width.
|
|
189
|
+
const nameWidth = graph.nodes.reduce((m, n) => Math.max(m, n.length), 0);
|
|
190
|
+
// Pad the `[kind/blocking]` tag to the widest so the evidence ("reason")
|
|
191
|
+
// column lines up across all bullets.
|
|
192
|
+
const tagText = (e: DerivedEdge): string =>
|
|
193
|
+
e.kind === "event" ? "[event]" : `[${e.kind}/${e.blocking}]`;
|
|
194
|
+
const tagWidth = graph.edges.reduce(
|
|
195
|
+
(m, e) => Math.max(m, tagText(e).length),
|
|
196
|
+
0,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const lines: string[] = [];
|
|
200
|
+
for (const node of graph.nodes) {
|
|
201
|
+
const ann = opts.annotations?.get(node);
|
|
202
|
+
// System (node) rows: auto-start-off ones in yellow, the rest in light
|
|
203
|
+
// green — both bold so they stand out from the plain-white edge bullets.
|
|
204
|
+
const isOff = !!ann && ann.includes("auto-start off");
|
|
205
|
+
const paint = isOff ? chalk.yellow : chalk.greenBright;
|
|
206
|
+
const placed = placementMark(opts.placements?.get(node));
|
|
207
|
+
const styledAnn =
|
|
208
|
+
(placed ? " " + chalk.cyan(placed) : "") +
|
|
209
|
+
(ann ? " " + paint(ann) : "");
|
|
210
|
+
// The repo the command was run from gets a leading `▸`; others a 2-space
|
|
211
|
+
// gutter so node names stay aligned. A single-width glyph (not an emoji,
|
|
212
|
+
// which is double-width + terminal-dependent) keeps the column stable.
|
|
213
|
+
const gutter =
|
|
214
|
+
node === opts.currentRepo ? chalk.bold.yellow.bgBlue("▸ ") : " ";
|
|
215
|
+
const pad = " ".repeat(nameWidth - node.length);
|
|
216
|
+
lines.push(`${gutter}${paint.bold(node)}${pad}${styledAnn}`);
|
|
217
|
+
for (const e of byFrom.get(node) ?? []) {
|
|
218
|
+
const tagPlain = tagText(e);
|
|
219
|
+
const tagColored =
|
|
220
|
+
e.kind === "event"
|
|
221
|
+
? chalk.magenta(tagPlain)
|
|
222
|
+
: e.blocking === "hard"
|
|
223
|
+
? chalk.red(tagPlain)
|
|
224
|
+
: chalk.white(tagPlain);
|
|
225
|
+
const tagPad = " ".repeat(tagWidth - tagPlain.length);
|
|
226
|
+
const ev = showEvidence ? ` ${e.evidence}` : "";
|
|
227
|
+
// Bullet rows use plain white (not dim/gray) so they stay readable — they
|
|
228
|
+
// recede from the bold colored system headers by weight, not by dimming.
|
|
229
|
+
// Tag is padded (plain, then colorized) so the evidence column aligns.
|
|
230
|
+
lines.push(
|
|
231
|
+
chalk.white(` ${ARROW[e.kind]} ${e.to.padEnd(18)} `) +
|
|
232
|
+
tagColored +
|
|
233
|
+
tagPad +
|
|
234
|
+
chalk.white(ev),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return lines.join("\n");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** A repo's placement on the graph: local, remote (with envs), or hybrid. */
|
|
242
|
+
export interface NodePlacement {
|
|
243
|
+
placement: RollupPlacement;
|
|
244
|
+
envs: string[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** "☁ remote (dev)", "⇄ hybrid", or "" for a local (or runtime-less) node. */
|
|
248
|
+
export function placementMark(p: NodePlacement | undefined): string {
|
|
249
|
+
if (!p) return "";
|
|
250
|
+
if (p.placement === "remote") return `☁ remote (${p.envs.join(", ")})`;
|
|
251
|
+
if (p.placement === "hybrid") return "⇄ hybrid";
|
|
252
|
+
return "";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Each node's placement from its manifest, viewed locally. */
|
|
256
|
+
export function nodePlacements(nodes: string[]): Map<string, NodePlacement> {
|
|
257
|
+
const ctx = buildContext();
|
|
258
|
+
const out = new Map<string, NodePlacement>();
|
|
259
|
+
for (const node of nodes) {
|
|
260
|
+
const src = repoSource(node, ctx);
|
|
261
|
+
if (!src?.exists) continue;
|
|
262
|
+
out.set(node, repoPlacement(readManifest(src.path), "local"));
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function renderDot(
|
|
268
|
+
graph: DerivedGraph,
|
|
269
|
+
placements?: Map<string, NodePlacement>,
|
|
270
|
+
): string {
|
|
271
|
+
const lines = [`digraph ${product().cli} {`, " rankdir=LR;"];
|
|
272
|
+
for (const node of graph.nodes) {
|
|
273
|
+
const p = placements?.get(node);
|
|
274
|
+
lines.push(
|
|
275
|
+
p?.placement === "remote"
|
|
276
|
+
? ` "${node}" [style=dashed, label="${node} ☁ ${p.envs.join(", ")}"];`
|
|
277
|
+
: ` "${node}";`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
for (const e of graph.edges) {
|
|
281
|
+
const style = e.kind === "event" ? "dashed" : "solid";
|
|
282
|
+
const color = e.blocking === "hard" ? "red" : "black";
|
|
283
|
+
lines.push(
|
|
284
|
+
` "${e.from}" -> "${e.to}" [style=${style}, color=${color}, label="${e.kind}"];`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
lines.push("}");
|
|
288
|
+
return lines.join("\n");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function renderDiff(diffs: GraphDiff[]): string {
|
|
292
|
+
if (diffs.length === 0) {
|
|
293
|
+
return `✓ Derived graph matches declared ${product().manifestYml} depends_on.`;
|
|
294
|
+
}
|
|
295
|
+
const lines: string[] = [];
|
|
296
|
+
for (const d of diffs) {
|
|
297
|
+
lines.push(d.repo);
|
|
298
|
+
for (const t of d.undeclared)
|
|
299
|
+
lines.push(` + ${t} (derived, not declared)`);
|
|
300
|
+
for (const t of d.stale)
|
|
301
|
+
lines.push(` - ${t} (declared, no derived signal)`);
|
|
302
|
+
}
|
|
303
|
+
return lines.join("\n");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── flag parsing ─────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
const SOURCES: readonly Source[] = ["derived", "declared", "union"];
|
|
309
|
+
const KINDS: readonly KindFilter[] = ["all", "hard", "infra", "http", "event"];
|
|
310
|
+
const FORMATS: readonly Format[] = ["text", "json", "dot"];
|
|
311
|
+
|
|
312
|
+
/** Value of `--<flag>=<value>` validated against its allowed set — a typo must
|
|
313
|
+
* error out loudly, not silently degrade to the default rendering. */
|
|
314
|
+
function parseChoice<T extends string>(
|
|
315
|
+
flag: string,
|
|
316
|
+
value: string,
|
|
317
|
+
allowed: readonly T[],
|
|
318
|
+
): T {
|
|
319
|
+
if (!(allowed as readonly string[]).includes(value)) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Invalid value for ${flag}: "${value}" (valid: ${allowed.join(" | ")})`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return value as T;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function parseGraphFlags(args: string[]): GraphFlags {
|
|
328
|
+
const flags: GraphFlags = {
|
|
329
|
+
all: false,
|
|
330
|
+
source: "derived",
|
|
331
|
+
kind: "all",
|
|
332
|
+
format: "text",
|
|
333
|
+
diff: false,
|
|
334
|
+
refresh: false,
|
|
335
|
+
order: "up",
|
|
336
|
+
};
|
|
337
|
+
for (const arg of args) {
|
|
338
|
+
if (arg === "--all") flags.all = true;
|
|
339
|
+
else if (arg === "--diff") flags.diff = true;
|
|
340
|
+
else if (arg === "--refresh" || arg === "--no-cache") flags.refresh = true;
|
|
341
|
+
else if (arg === "-u" || arg === "--up") flags.order = "up";
|
|
342
|
+
else if (arg === "-d" || arg === "--down") flags.order = "down";
|
|
343
|
+
else if (arg.startsWith("--source="))
|
|
344
|
+
flags.source = parseChoice("--source", arg.slice(9), SOURCES);
|
|
345
|
+
else if (arg.startsWith("--kind="))
|
|
346
|
+
flags.kind = parseChoice("--kind", arg.slice(7), KINDS);
|
|
347
|
+
else if (arg.startsWith("--format="))
|
|
348
|
+
flags.format = parseChoice("--format", arg.slice(9), FORMATS);
|
|
349
|
+
else if (!arg.startsWith("-")) flags.repo = arg;
|
|
350
|
+
}
|
|
351
|
+
return flags;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Resolve the seed repo from an explicit arg, else the cwd repo. */
|
|
355
|
+
function resolveSeed(explicit: string | undefined): string | undefined {
|
|
356
|
+
if (explicit) return explicit;
|
|
357
|
+
const root = findInvocationRepoRoot();
|
|
358
|
+
return root ? path.basename(root) : undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The connected component containing `seed`, traversed UNDIRECTED over ALL edge
|
|
363
|
+
* kinds (including events). Unlike the startup closure (`den up`, sync + outgoing
|
|
364
|
+
* only), this surfaces the full neighborhood — event producers/consumers like
|
|
365
|
+
* control-plane and jobs-runtime included — which is what you want when reading
|
|
366
|
+
* the graph.
|
|
367
|
+
*/
|
|
368
|
+
export function connectedComponent(
|
|
369
|
+
graph: DerivedGraph,
|
|
370
|
+
seed: string,
|
|
371
|
+
): DerivedGraph {
|
|
372
|
+
const adj = new Map<string, Set<string>>();
|
|
373
|
+
const link = (a: string, b: string) =>
|
|
374
|
+
(adj.get(a) ?? adj.set(a, new Set()).get(a)!).add(b);
|
|
375
|
+
for (const e of graph.edges) {
|
|
376
|
+
link(e.from, e.to);
|
|
377
|
+
link(e.to, e.from);
|
|
378
|
+
}
|
|
379
|
+
const seen = new Set([seed]);
|
|
380
|
+
const queue = [seed];
|
|
381
|
+
while (queue.length > 0) {
|
|
382
|
+
const cur = queue.shift()!;
|
|
383
|
+
for (const n of adj.get(cur) ?? []) {
|
|
384
|
+
if (!seen.has(n)) {
|
|
385
|
+
seen.add(n);
|
|
386
|
+
queue.push(n);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
nodes: graph.nodes.filter((n) => seen.has(n)),
|
|
392
|
+
edges: graph.edges.filter((e) => seen.has(e.from) && seen.has(e.to)),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Per-node "start: den|sibling (path)" annotations showing the boot location. */
|
|
397
|
+
export function bootAnnotations(nodes: string[]): Map<string, string> {
|
|
398
|
+
const distRoot = getDistRoot();
|
|
399
|
+
const home = os.homedir();
|
|
400
|
+
// Systems with auto-start toggled OFF in the active workspace — workspace-wide
|
|
401
|
+
// up won't start these; toggle in the web ⋮ menu or start explicitly.
|
|
402
|
+
const ws = getActiveWorkspace();
|
|
403
|
+
const noAutostart = ws
|
|
404
|
+
? noAutostartRepos(ws.config, readCatalog())
|
|
405
|
+
: new Set<string>();
|
|
406
|
+
// Build the derive context once and reuse it for every node — repoSource()
|
|
407
|
+
// otherwise rebuilds it per call, re-reading/parsing den.yml + catalog +
|
|
408
|
+
// workspaces from disk N times for an N-node graph.
|
|
409
|
+
const ctx = buildContext();
|
|
410
|
+
const ann = new Map<string, string>();
|
|
411
|
+
for (const node of nodes) {
|
|
412
|
+
const opt = noAutostart.has(node) ? " ⏸ auto-start off" : "";
|
|
413
|
+
const src = repoSource(node, ctx);
|
|
414
|
+
if (!src) {
|
|
415
|
+
ann.set(node, `(not found on disk)${opt}`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
// Managed clones live under the den repo → home-relative (~/…); sibling
|
|
419
|
+
// repos → relative to the den dir (../…). Full path otherwise.
|
|
420
|
+
const rel = src.managed
|
|
421
|
+
? src.path.startsWith(home + path.sep)
|
|
422
|
+
? `~${src.path.slice(home.length)}`
|
|
423
|
+
: src.path
|
|
424
|
+
: path.relative(distRoot, src.path) || src.path;
|
|
425
|
+
// A managed repo may be configured but not cloned yet — flag it so the
|
|
426
|
+
// annotation reflects the configured mode, not a silent sibling fallback.
|
|
427
|
+
const note = !src.exists
|
|
428
|
+
? src.managed
|
|
429
|
+
? " — not cloned"
|
|
430
|
+
: " — missing"
|
|
431
|
+
: "";
|
|
432
|
+
ann.set(
|
|
433
|
+
node,
|
|
434
|
+
`${src.managed ? `${product().cli} managed` : "sibling repo"} (${rel}${note})${opt}`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return ann;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── command ──────────────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
export async function cmdGraph(args: string[]): Promise<void> {
|
|
443
|
+
const flags = parseGraphFlags(args);
|
|
444
|
+
const alias = loadAliasResolver();
|
|
445
|
+
|
|
446
|
+
const seed = flags.all ? undefined : resolveSeed(flags.repo);
|
|
447
|
+
const scoped = seed !== undefined && alias.knownRepos.has(seed);
|
|
448
|
+
|
|
449
|
+
// Always derive the full graph; for a seed, narrow to its connected component
|
|
450
|
+
// (full neighborhood, both directions, incl. events) — NOT the strict startup
|
|
451
|
+
// closure that `den up` uses.
|
|
452
|
+
const full = deriveGraph({ scope: "all", refresh: flags.refresh });
|
|
453
|
+
|
|
454
|
+
// The repo the command is run from (independent of --all / explicit arg) —
|
|
455
|
+
// marked with a leading `*`. Identified by repo IDENTITY (manifest name /
|
|
456
|
+
// folder name), so it resolves from a sibling checkout or a managed clone.
|
|
457
|
+
const cwd = resolveCwdRepo(alias);
|
|
458
|
+
const currentRepo = cwd?.repo;
|
|
459
|
+
const graph = scoped ? connectedComponent(full, seed) : full;
|
|
460
|
+
|
|
461
|
+
if (flags.diff) {
|
|
462
|
+
const declared = collectDeclaredEdges(graph.nodes, alias);
|
|
463
|
+
const diffs = diffEdges(graph.edges, declared, graph.nodes);
|
|
464
|
+
console.log(renderDiff(diffs));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Choose the edge set per --source, then filter by --kind.
|
|
469
|
+
let edges = graph.edges;
|
|
470
|
+
if (flags.source !== "derived") {
|
|
471
|
+
const declared = collectDeclaredEdges(graph.nodes, alias);
|
|
472
|
+
edges =
|
|
473
|
+
flags.source === "declared"
|
|
474
|
+
? declared
|
|
475
|
+
: dedupeUnion(graph.edges, declared);
|
|
476
|
+
}
|
|
477
|
+
// Render nodes in the order `den up` (deps-first) or `down` (reverse) runs.
|
|
478
|
+
const orderedNodes =
|
|
479
|
+
flags.order === "down" ? orderForDown(graph) : orderForUp(graph);
|
|
480
|
+
const filtered: DerivedGraph = {
|
|
481
|
+
nodes: orderedNodes,
|
|
482
|
+
edges: edges.filter((e) => edgeMatchesKind(e, flags.kind)),
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
if (flags.format === "json") {
|
|
486
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (flags.format === "dot") {
|
|
490
|
+
console.log(renderDot(filtered, nodePlacements(filtered.nodes)));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const scopeLabel = scoped ? `neighborhood: ${seed}` : "all";
|
|
495
|
+
const cwdNote =
|
|
496
|
+
currentRepo && filtered.nodes.includes(currentRepo)
|
|
497
|
+
? ` · ${chalk.bold.yellow.bgBlue("▸")} = current repo`
|
|
498
|
+
: "";
|
|
499
|
+
console.log(
|
|
500
|
+
`${product().cli} dependency graph (source: ${flags.source} · ${scopeLabel} · order: ${flags.order})${cwdNote}\n`,
|
|
501
|
+
);
|
|
502
|
+
console.log(
|
|
503
|
+
renderText(filtered, {
|
|
504
|
+
annotations: bootAnnotations(graph.nodes),
|
|
505
|
+
currentRepo,
|
|
506
|
+
placements: nodePlacements(graph.nodes),
|
|
507
|
+
}),
|
|
508
|
+
);
|
|
509
|
+
console.log("\n" + renderTargetWarnings(graph.nodes));
|
|
510
|
+
|
|
511
|
+
// Non-intrusive hint (stderr, pipe-safe): you're in a sibling checkout of a
|
|
512
|
+
// managed repo, so den boots a different clone than the code you're
|
|
513
|
+
// editing. Suggest switching — don't prompt or mutate from a read-only command.
|
|
514
|
+
if (currentRepo && cwd) {
|
|
515
|
+
const src = repoSource(currentRepo);
|
|
516
|
+
if (src?.managed && path.resolve(src.path) !== path.resolve(cwd.root)) {
|
|
517
|
+
console.error(
|
|
518
|
+
chalk.gray(
|
|
519
|
+
`\nℹ ${chalk.bold(currentRepo)} is managed (boots from ${src.path}),\n` +
|
|
520
|
+
` but you're in a sibling checkout (${cwd.root}).\n` +
|
|
521
|
+
` Run ${chalk.cyan(`${product().cli} use-local`)} to point ${product().cli} at this checkout` +
|
|
522
|
+
` (or ${chalk.cyan(`${product().cli} use-managed`)} to keep the managed clone).`,
|
|
523
|
+
),
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function dedupeUnion(
|
|
530
|
+
derived: DerivedEdge[],
|
|
531
|
+
declared: DerivedEdge[],
|
|
532
|
+
): DerivedEdge[] {
|
|
533
|
+
const seen = new Set(derived.map((e) => `${edgeKey(e)}:${e.kind}`));
|
|
534
|
+
const extra = declared.filter((e) => !seen.has(`${edgeKey(e)}:${e.kind}`));
|
|
535
|
+
return [...derived, ...extra];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Commands a repo exposes: Makefile targets ∪ `.den.yml` commands.local keys. */
|
|
539
|
+
function availableCommands(repoPath: string): Set<string> {
|
|
540
|
+
const cmds = new Set(listMakeTargets(repoPath).targets);
|
|
541
|
+
const manifest = readManifest(repoPath);
|
|
542
|
+
for (const svc of Object.values(manifest?.services ?? {})) {
|
|
543
|
+
for (const env of Object.values(svc.commands ?? {})) {
|
|
544
|
+
for (const name of Object.keys(env ?? {})) cmds.add(name);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return cmds;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Flag any node missing a den-driven command. A command counts as available
|
|
552
|
+
* if it's a Makefile target OR declared in `.den.yml` commands.local (so
|
|
553
|
+
* `down: opctl kill` satisfies "down" without a `make down` target).
|
|
554
|
+
*/
|
|
555
|
+
export function renderTargetWarnings(nodes: string[]): string {
|
|
556
|
+
const warnings: string[] = [];
|
|
557
|
+
for (const node of nodes) {
|
|
558
|
+
const repoPath = resolveRepoPath(node);
|
|
559
|
+
if (!repoPath) continue;
|
|
560
|
+
const have = availableCommands(repoPath);
|
|
561
|
+
const missing = REQUIRED_TARGETS.filter((t) => !have.has(t));
|
|
562
|
+
if (missing.length)
|
|
563
|
+
warnings.push(
|
|
564
|
+
` ${node}: missing ${missing.join(", ")} (no make target or ${product().manifestYml} command)`,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
return warnings.length
|
|
568
|
+
? `command gaps:\n${warnings.join("\n")}`
|
|
569
|
+
: "commands: all nodes expose setup/up/down ✓";
|
|
570
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { ServiceHealthResult } from "../../shared/environment-manager/health.js";
|
|
3
|
+
|
|
4
|
+
import { product } from "../../shared/product.js";
|
|
5
|
+
type HealthState = ServiceHealthResult["health"];
|
|
6
|
+
type HealthReasonInput = Pick<ServiceHealthResult, "health" | "healthDetail">;
|
|
7
|
+
|
|
8
|
+
export function healthLabelText(health: HealthState): string {
|
|
9
|
+
if (health === "error") return "down";
|
|
10
|
+
return health;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatHealthLabel(health: HealthState): string {
|
|
14
|
+
if (health === "ok") return chalk.green("● ok");
|
|
15
|
+
if (health === "error") return chalk.red("● down");
|
|
16
|
+
return chalk.gray("● unknown");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatHealthDot(health: HealthState): string {
|
|
20
|
+
if (health === "ok") return chalk.green("●");
|
|
21
|
+
if (health === "error") return chalk.red("●");
|
|
22
|
+
return chalk.gray("●");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function healthReasonText(result: HealthReasonInput): string {
|
|
26
|
+
if (result.health === "ok") return "";
|
|
27
|
+
|
|
28
|
+
const detail = result.healthDetail?.trim();
|
|
29
|
+
if (!detail) return result.health === "error" ? "not running" : "";
|
|
30
|
+
|
|
31
|
+
if (detail === "no health check configured") return "missing health check";
|
|
32
|
+
if (detail === `no ${product().manifestYml}`)
|
|
33
|
+
return `missing ${product().manifestYml}`;
|
|
34
|
+
if (detail.startsWith("repo path does not exist")) return "repo path missing";
|
|
35
|
+
if (detail === "timeout") return "health URL timeout";
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
detail === "fetch failed" ||
|
|
39
|
+
/^(connect )?ECONNREFUSED\b/i.test(detail) ||
|
|
40
|
+
/^ENOTFOUND\b/i.test(detail) ||
|
|
41
|
+
/^EAI_AGAIN\b/i.test(detail)
|
|
42
|
+
) {
|
|
43
|
+
return "health URL unreachable";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const httpMatch = detail.match(/^http (\d{3})$/i);
|
|
47
|
+
if (httpMatch) return `health URL returned HTTP ${httpMatch[1]}`;
|
|
48
|
+
|
|
49
|
+
const exitMatch = detail.match(/^exit (\d+|null)$/);
|
|
50
|
+
if (exitMatch) {
|
|
51
|
+
if (exitMatch[1] === "127") {
|
|
52
|
+
return "health command missing dependency (exit 127)";
|
|
53
|
+
}
|
|
54
|
+
return `health command failed: exit ${exitMatch[1]}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return detail;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatHealthReason(result: HealthReasonInput): string {
|
|
61
|
+
const reason = healthReasonText(result);
|
|
62
|
+
return reason ? chalk.gray(reason.slice(0, 48)) : "";
|
|
63
|
+
}
|