@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,108 @@
1
+ /** Applications for a workspace, from config and repo manifests. */
2
+ import { readDenConfig } from "../den-config.js";
3
+ import { resolveApplications, type Application } from "../applications.js";
4
+ import { readManifest } from "../manifest/read-manifest.js";
5
+ import { contributesTo, repoKind } from "../repo-kind.js";
6
+ import type { RepoKind } from "../types/index.js";
7
+ import { deriveGraph } from "./derivation/derive-graph.js";
8
+ import type { DerivedGraph } from "./derivation/types.js";
9
+ import { getWorkspaceServices } from "./service-runner.js";
10
+ import { readOrg, type Org } from "../org.js";
11
+ import {
12
+ rollupPlacement,
13
+ servicePlacement,
14
+ type Placement,
15
+ type RollupPlacement,
16
+ } from "../manifest/placement.js";
17
+
18
+ export function loadApplications(workspace?: string): Application[] {
19
+ const services = getWorkspaceServices(workspace);
20
+ const repos = [...new Set(services.map((s) => s.repoName))];
21
+ const manifestApplications: Record<string, string> = {};
22
+ const kinds: Record<string, RepoKind> = {};
23
+ const contributes: Record<string, string[]> = {};
24
+ // The graph only tells a module/library where it ships; derive it lazily.
25
+ let graph: DerivedGraph | undefined;
26
+ for (const s of services) {
27
+ const a = readManifest(s.repoPath)?.application;
28
+ if (a) manifestApplications[s.repoName] = a;
29
+ const kind = repoKind(s.repoPath);
30
+ if (kind !== "service") {
31
+ graph ??= deriveGraph({
32
+ scope: "all",
33
+ ...(workspace ? { workspace } : {}),
34
+ });
35
+ kinds[s.repoName] = kind;
36
+ contributes[s.repoName] = contributesTo(s.repoName, s.repoPath, graph);
37
+ }
38
+ }
39
+ return resolveApplications({
40
+ repos,
41
+ config: readDenConfig().applications,
42
+ manifestApplications,
43
+ kinds,
44
+ contributesTo: contributes,
45
+ });
46
+ }
47
+
48
+ /** An application with what the workspace runs of it. */
49
+ export interface ApplicationView extends Application {
50
+ /** repo → its service addresses in the workspace ("repo" or "repo/sub"). */
51
+ services: Record<string, string[]>;
52
+ /** Every service address in the application, for app-wide up/down. */
53
+ addresses: string[];
54
+ /** Repos the team config lists for it that are not in this workspace. */
55
+ absent: string[];
56
+ /** address → where that service runs locally, and where a remote one is. */
57
+ placements: Record<string, Placement>;
58
+ /** local | remote | hybrid | none, over its services (R13). */
59
+ placement: RollupPlacement;
60
+ /** "shop-frontend local · shop-core remote (dev)" */
61
+ summary: string;
62
+ }
63
+
64
+ /** Org > Application > Repo > Services for a workspace (CLI `apps`, web UI). */
65
+ export function loadApplicationTree(workspace?: string): {
66
+ org?: Org;
67
+ applications: ApplicationView[];
68
+ } {
69
+ const byRepo = new Map<string, string[]>();
70
+ const placed = new Map<
71
+ string,
72
+ Placement & { repo: string; service: string }
73
+ >();
74
+ for (const s of getWorkspaceServices(workspace)) {
75
+ if (!byRepo.has(s.repoName)) byRepo.set(s.repoName, []);
76
+ byRepo.get(s.repoName)!.push(s.address);
77
+ placed.set(s.address, {
78
+ repo: s.repoName,
79
+ service: s.subsystem,
80
+ ...servicePlacement(readManifest(s.repoPath), s.subsystem, "local"),
81
+ });
82
+ }
83
+ const config = readDenConfig().applications ?? {};
84
+ const org = readOrg();
85
+ return {
86
+ ...(org ? { org } : {}),
87
+ applications: loadApplications(workspace).map((a) => {
88
+ const services = Object.fromEntries(
89
+ a.repos.map((r) => [r, byRepo.get(r) ?? []]),
90
+ );
91
+ const addresses = Object.values(services).flat();
92
+ const rows = addresses.flatMap((addr) => placed.get(addr) ?? []);
93
+ return {
94
+ ...a,
95
+ services,
96
+ addresses,
97
+ absent: (config[a.name]?.repos ?? []).filter((r) => !byRepo.has(r)),
98
+ placements: Object.fromEntries(
99
+ addresses.flatMap((addr) => {
100
+ const p = placed.get(addr);
101
+ return p ? [[addr, { runs: p.runs, target: p.target }]] : [];
102
+ }),
103
+ ),
104
+ ...rollupPlacement(rows),
105
+ };
106
+ }),
107
+ };
108
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Live log tailing for a local service — the CLI-side counterpart of the web
3
+ * /api/service/taillog SSE endpoint. Source resolution, in order:
4
+ * 1. The service's manifest `logs.local` shell command (e.g. `docker logs -f`)
5
+ * 2. The service's running container → `docker logs -f`: a compose stack's
6
+ * `<stack>-<workspace>-<service>-1`, else an `opctl_<base>_<hash>` one —
7
+ * real runtime logs with zero manifest config
8
+ * 3. The den daemon startup log (~/.den/logs/<repo>/<subsystem>.log) —
9
+ * last resort, covers host-run processes with no container
10
+ * A source whose command exits within the quick-exit window (container not
11
+ * found, stale filter) is treated as broken and the next tier takes over.
12
+ */
13
+
14
+ import * as fs from "fs";
15
+ import { DAEMON_QUICK_EXIT_MS, LIVE_TAIL_LINES } from "../constants.js";
16
+ import { readManifest } from "../manifest/read-manifest.js";
17
+ import { findServiceContainer } from "./service-container.js";
18
+ import {
19
+ getServiceLogPath,
20
+ spawnStreaming,
21
+ } from "../process/spawn-streaming.js";
22
+ import type { LogSource, ResolvedService } from "../types/index.js";
23
+
24
+ export interface TailSource {
25
+ kind: "manifest" | "docker" | "file";
26
+ /** Executable (argv mode) or a full shell command (when args is empty). */
27
+ cmd: string;
28
+ /**
29
+ * Argv for cmd. Empty → cmd runs through the shell (manifest `logs.local`
30
+ * commands are arbitrary shell by design). Den-BUILT commands always use
31
+ * argv mode so container/subsystem names from manifests or docker output
32
+ * can never be shell-interpreted (`$(…)`, backticks, quotes).
33
+ */
34
+ args: string[];
35
+ /** Working directory for the command (manifest commands run in the repo). */
36
+ cwd?: string;
37
+ /** Human-readable description for the stream header. */
38
+ label: string;
39
+ }
40
+
41
+ export interface TailSourceInputs {
42
+ repoPath?: string;
43
+ /** The subsystem's manifest `logs.local` entry, if any. */
44
+ logSource?: LogSource;
45
+ /** A running conventional opctl container name, if any. */
46
+ container: string | null;
47
+ daemonLogPath: string;
48
+ daemonLogExists: boolean;
49
+ }
50
+
51
+ /** Ordered tail-source candidates (best source first). Pure — see resolveTailSources. */
52
+ export function tailSourceCandidates(
53
+ inputs: TailSourceInputs,
54
+ lines: number = LIVE_TAIL_LINES,
55
+ ): TailSource[] {
56
+ const candidates: TailSource[] = [];
57
+ if (inputs.logSource?.type === "shell" && inputs.logSource.cmd) {
58
+ candidates.push({
59
+ kind: "manifest",
60
+ cmd: inputs.logSource.cmd,
61
+ args: [],
62
+ cwd: inputs.repoPath,
63
+ label: `manifest log command: ${inputs.logSource.cmd}`,
64
+ });
65
+ }
66
+ if (inputs.container) {
67
+ candidates.push({
68
+ kind: "docker",
69
+ cmd: "docker",
70
+ args: ["logs", "-f", "--tail", String(lines), inputs.container],
71
+ label: `container logs: ${inputs.container}`,
72
+ });
73
+ }
74
+ if (inputs.daemonLogExists) {
75
+ candidates.push({
76
+ kind: "file",
77
+ cmd: "tail",
78
+ args: ["-F", "-n", String(lines), inputs.daemonLogPath],
79
+ label: `startup log: ${inputs.daemonLogPath}`,
80
+ });
81
+ }
82
+ return candidates;
83
+ }
84
+
85
+ /** Gather live inputs for `resolved` and build its tail-source candidates. */
86
+ export function resolveTailSources(resolved: ResolvedService): TailSource[] {
87
+ const manifest = readManifest(resolved.repoPath);
88
+ const logSource = manifest?.services?.[resolved.subsystem]?.logs?.local;
89
+ const container = findServiceContainer(
90
+ resolved.repoName,
91
+ resolved.repoPath,
92
+ resolved.subsystem,
93
+ );
94
+ const daemonLogPath = getServiceLogPath(
95
+ resolved.repoName,
96
+ resolved.subsystem,
97
+ );
98
+ return tailSourceCandidates({
99
+ repoPath: resolved.repoPath,
100
+ logSource,
101
+ container,
102
+ daemonLogPath,
103
+ daemonLogExists: fs.existsSync(daemonLogPath),
104
+ });
105
+ }
106
+
107
+ export interface FollowCallbacks {
108
+ onLine: (line: string) => void;
109
+ /** Stream headers / end-of-stream notices (rendered dim by the CLI). */
110
+ onMeta: (line: string) => void;
111
+ }
112
+
113
+ export interface FollowHandle {
114
+ /** Resolves with the final stream's exit code (0 after stop()). */
115
+ done: Promise<number>;
116
+ /** Stop tailing without stopping the service. */
117
+ stop: () => void;
118
+ }
119
+
120
+ export interface FollowDeps {
121
+ spawn?: typeof spawnStreaming;
122
+ quickExitMs?: number;
123
+ }
124
+
125
+ /**
126
+ * Follow the first working candidate, falling through the tiers when a
127
+ * command quick-exits. Lines seen before a source survives the quick-exit
128
+ * window are buffered (flushed on confirm, discarded on fallback) so a broken
129
+ * tier's error spew never reaches the caller.
130
+ */
131
+ export function followTailSources(
132
+ candidates: TailSource[],
133
+ { onLine, onMeta }: FollowCallbacks,
134
+ deps: FollowDeps = {},
135
+ ): FollowHandle {
136
+ const spawn = deps.spawn ?? spawnStreaming;
137
+ const quickExitMs = deps.quickExitMs ?? DAEMON_QUICK_EXIT_MS;
138
+
139
+ let stopped = false;
140
+ let current: ReturnType<typeof spawnStreaming> | null = null;
141
+ let resolveDone!: (code: number) => void;
142
+ const done = new Promise<number>((resolve) => (resolveDone = resolve));
143
+
144
+ const tryCandidate = (index: number): void => {
145
+ if (index >= candidates.length) {
146
+ onMeta("── no live log stream available ──");
147
+ resolveDone(1);
148
+ return;
149
+ }
150
+ const source = candidates[index];
151
+ const buffer: string[] = [];
152
+ let confirmed = false;
153
+ let quickTimer: ReturnType<typeof setTimeout> | null = setTimeout(() => {
154
+ quickTimer = null;
155
+ confirmed = true;
156
+ onMeta(`── ${source.label} ──`);
157
+ for (const line of buffer) onLine(line);
158
+ buffer.length = 0;
159
+ }, quickExitMs);
160
+
161
+ // Detached: the tail child leads its own process group, so stop() can
162
+ // SIGTERM the whole tree. Shell-wrapped commands (e.g. `docker logs -f
163
+ // $(docker ps …)`) keep `sh` as the parent — killing only its pid would
164
+ // orphan a live `docker logs -f`. The child's stdio pipes keep the event
165
+ // loop alive; killing it stops the stream, never the service.
166
+ current = spawn(
167
+ source.cmd,
168
+ source.args,
169
+ { ...process.env },
170
+ (line) => (confirmed ? onLine(line) : buffer.push(line)),
171
+ (exitCode) => {
172
+ if (quickTimer !== null) {
173
+ clearTimeout(quickTimer);
174
+ quickTimer = null;
175
+ }
176
+ if (stopped) {
177
+ resolveDone(0);
178
+ return;
179
+ }
180
+ if (!confirmed) {
181
+ buffer.length = 0;
182
+ tryCandidate(index + 1);
183
+ return;
184
+ }
185
+ onMeta(`── log stream ended (${exitCode}) ──`);
186
+ resolveDone(exitCode);
187
+ },
188
+ source.cwd,
189
+ true,
190
+ );
191
+ };
192
+
193
+ tryCandidate(0);
194
+
195
+ return {
196
+ done,
197
+ stop: (): void => {
198
+ stopped = true;
199
+ if (!current) return;
200
+ try {
201
+ // Negative pid → the child's whole process group (it is detached).
202
+ if (current.pid) process.kill(-current.pid, "SIGTERM");
203
+ else current.kill("SIGTERM");
204
+ } catch {
205
+ try {
206
+ current.kill("SIGTERM");
207
+ } catch {
208
+ /* already exited */
209
+ }
210
+ }
211
+ },
212
+ };
213
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Lightweight Makefile target discovery.
3
+ *
4
+ * `den graph`/`up` annotate each node with the make targets it exposes and
5
+ * warn when the ones den drives (`setup`/`up`/`down`) are missing. We parse
6
+ * the root `Makefile` plus any `include`d `*.mk` for target definitions —
7
+ * deliberately simple (no variable expansion), reading a fixed set of files.
8
+ */
9
+
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+
13
+ export interface MakeTargets {
14
+ /** Sorted, unique runnable target names. */
15
+ targets: string[];
16
+ /** Makefiles parsed, relative to repo root. */
17
+ files: string[];
18
+ }
19
+
20
+ const TARGET_LINE = /^([A-Za-z0-9][A-Za-z0-9_.-]*)[ \t]*:(?!=)/gm;
21
+ const INCLUDE_LINE = /^[ \t]*-?include[ \t]+(.+)$/gm;
22
+
23
+ function readSafe(abs: string): string | null {
24
+ try {
25
+ return fs.readFileSync(abs, "utf-8");
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function collectTargets(content: string, into: Set<string>): void {
32
+ for (const m of content.matchAll(TARGET_LINE)) {
33
+ const name = m[1];
34
+ if (name.startsWith(".")) continue; // .PHONY, .DEFAULT_GOAL, …
35
+ into.add(name);
36
+ }
37
+ }
38
+
39
+ /** Parse the root Makefile (+ statically-includable `*.mk`) for target names. */
40
+ export function listMakeTargets(repoPath: string): MakeTargets {
41
+ const rootMk = path.join(repoPath, "Makefile");
42
+ const rootContent = readSafe(rootMk);
43
+ if (rootContent === null) return { targets: [], files: [] };
44
+
45
+ const targets = new Set<string>();
46
+ const filesParsed: string[] = ["Makefile"];
47
+ collectTargets(rootContent, targets);
48
+
49
+ // Follow one level of `include`d makefiles with literal (non-variable) paths.
50
+ const seen = new Set<string>([rootMk]);
51
+ for (const inc of rootContent.matchAll(INCLUDE_LINE)) {
52
+ const spec = inc[1].trim();
53
+ if (spec.includes("$")) continue; // variable path — can't resolve statically
54
+ for (const rel of spec.split(/\s+/)) {
55
+ const abs = path.join(repoPath, rel);
56
+ if (seen.has(abs)) continue;
57
+ seen.add(abs);
58
+ const content = readSafe(abs);
59
+ if (content === null) continue;
60
+ filesParsed.push(rel);
61
+ collectTargets(content, targets);
62
+ }
63
+ }
64
+
65
+ return { targets: [...targets].sort(), files: filesParsed };
66
+ }
67
+
68
+ /** Split `required` into those present in the repo's Makefile(s) and those missing. */
69
+ export function requireTargets(
70
+ repoPath: string,
71
+ required: string[],
72
+ ): { present: string[]; missing: string[] } {
73
+ const have = new Set(listMakeTargets(repoPath).targets);
74
+ const present: string[] = [];
75
+ const missing: string[] = [];
76
+ for (const t of required) (have.has(t) ? present : missing).push(t);
77
+ return { present, missing };
78
+ }