@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.
Files changed (231) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -2
  4. package/bin/den +61 -0
  5. package/docs/distributions.md +256 -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 +104 -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 +406 -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 +108 -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 +729 -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/applications.ts +138 -0
  91. package/src/shared/auth/api-token.ts +416 -0
  92. package/src/shared/auth/auth0-helper.ts +284 -0
  93. package/src/shared/auth/headless-login.ts +265 -0
  94. package/src/shared/auth/jwt-helper.ts +186 -0
  95. package/src/shared/auth/sudo-vault.ts +69 -0
  96. package/src/shared/auth/test-accounts.ts +98 -0
  97. package/src/shared/auth/token-injection.ts +184 -0
  98. package/src/shared/auth/token-providers/auth0.ts +80 -0
  99. package/src/shared/auth/token-providers/index.ts +182 -0
  100. package/src/shared/auth/token-refresh.ts +273 -0
  101. package/src/shared/banner.ts +15 -0
  102. package/src/shared/brand.ts +79 -0
  103. package/src/shared/cli-tabs.ts +131 -0
  104. package/src/shared/constants.ts +128 -0
  105. package/src/shared/den-config.ts +566 -0
  106. package/src/shared/dev-metrics/emit.ts +354 -0
  107. package/src/shared/edge/config.ts +229 -0
  108. package/src/shared/edge/render.ts +221 -0
  109. package/src/shared/edge/runtime.ts +268 -0
  110. package/src/shared/edge/types.ts +62 -0
  111. package/src/shared/engine.ts +67 -0
  112. package/src/shared/environment/aws-profiles.ts +49 -0
  113. package/src/shared/environment/env-selector.ts +161 -0
  114. package/src/shared/environment/playwright-env.ts +82 -0
  115. package/src/shared/environment-manager/catalog.ts +77 -0
  116. package/src/shared/environment-manager/clone-guard.ts +163 -0
  117. package/src/shared/environment-manager/dag.ts +286 -0
  118. package/src/shared/environment-manager/default-workspace.ts +182 -0
  119. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  120. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  121. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  122. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  123. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  124. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  125. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  126. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  127. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  128. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  129. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  130. package/src/shared/environment-manager/derivation/text.ts +40 -0
  131. package/src/shared/environment-manager/derivation/types.ts +69 -0
  132. package/src/shared/environment-manager/health.ts +589 -0
  133. package/src/shared/environment-manager/index.ts +6 -0
  134. package/src/shared/environment-manager/load-applications.ts +78 -0
  135. package/src/shared/environment-manager/log-tail.ts +212 -0
  136. package/src/shared/environment-manager/make-targets.ts +78 -0
  137. package/src/shared/environment-manager/opctl.ts +334 -0
  138. package/src/shared/environment-manager/readiness.ts +93 -0
  139. package/src/shared/environment-manager/register-local.ts +72 -0
  140. package/src/shared/environment-manager/secrets-check.ts +260 -0
  141. package/src/shared/environment-manager/service-env.ts +110 -0
  142. package/src/shared/environment-manager/service-runner.ts +1588 -0
  143. package/src/shared/environment-manager/up-plan.ts +302 -0
  144. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  145. package/src/shared/environment-manager/workspaces.ts +860 -0
  146. package/src/shared/manifest/deps.ts +27 -0
  147. package/src/shared/manifest/migrate-manifest.ts +101 -0
  148. package/src/shared/manifest/read-manifest.ts +160 -0
  149. package/src/shared/manifest/schema.ts +299 -0
  150. package/src/shared/manifest/service-links.ts +93 -0
  151. package/src/shared/manifest/validate-manifest.ts +140 -0
  152. package/src/shared/manifest/workspace-manifests.ts +92 -0
  153. package/src/shared/open-url.ts +122 -0
  154. package/src/shared/org.ts +44 -0
  155. package/src/shared/paths.ts +178 -0
  156. package/src/shared/perf/optional-fields.ts +105 -0
  157. package/src/shared/perf/report-normalizer.ts +404 -0
  158. package/src/shared/perf/report-paths.ts +19 -0
  159. package/src/shared/perf/report-store.ts +110 -0
  160. package/src/shared/perf/report-types.ts +70 -0
  161. package/src/shared/perf/test-paths.ts +25 -0
  162. package/src/shared/process/log-rotation.ts +147 -0
  163. package/src/shared/process/spawn-streaming.ts +229 -0
  164. package/src/shared/product.ts +92 -0
  165. package/src/shared/profile.ts +141 -0
  166. package/src/shared/repo-kind.ts +32 -0
  167. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  168. package/src/shared/reporting/slack-reporter.ts +340 -0
  169. package/src/shared/results/result-reader.ts +266 -0
  170. package/src/shared/results/result-writer.ts +168 -0
  171. package/src/shared/secrets/doppler.ts +126 -0
  172. package/src/shared/secrets/env.ts +48 -0
  173. package/src/shared/secrets/index.ts +197 -0
  174. package/src/shared/secrets/keys.ts +33 -0
  175. package/src/shared/secrets/onepassword.ts +75 -0
  176. package/src/shared/secrets/types.ts +52 -0
  177. package/src/shared/service-url.ts +119 -0
  178. package/src/shared/smoke/branch-resolver.ts +141 -0
  179. package/src/shared/smoke/git-remote.ts +70 -0
  180. package/src/shared/smoke/repo-cloner.ts +151 -0
  181. package/src/shared/stacks/config.ts +137 -0
  182. package/src/shared/stacks/manifest-commands.ts +88 -0
  183. package/src/shared/stacks/render.ts +253 -0
  184. package/src/shared/stacks/runtime.ts +334 -0
  185. package/src/shared/stacks/types.ts +53 -0
  186. package/src/shared/terminal/tabs.ts +212 -0
  187. package/src/shared/test-discovery/module-scanner.ts +569 -0
  188. package/src/shared/types/index.ts +678 -0
  189. package/src/shared/ui-marker.ts +19 -0
  190. package/src/ui/cli/applications-command.ts +116 -0
  191. package/src/ui/cli/config-command.ts +68 -0
  192. package/src/ui/cli/create-main.ts +9 -0
  193. package/src/ui/cli/create.ts +360 -0
  194. package/src/ui/cli/edge-command.ts +91 -0
  195. package/src/ui/cli/flag-parser.ts +246 -0
  196. package/src/ui/cli/graph-command.ts +524 -0
  197. package/src/ui/cli/health-display.ts +63 -0
  198. package/src/ui/cli/index.ts +3656 -0
  199. package/src/ui/cli/init.ts +855 -0
  200. package/src/ui/cli/invocation-target.ts +175 -0
  201. package/src/ui/cli/logs-command.ts +104 -0
  202. package/src/ui/cli/module-selection.ts +31 -0
  203. package/src/ui/cli/prompts.ts +767 -0
  204. package/src/ui/cli/results-viewer.ts +436 -0
  205. package/src/ui/cli/secrets-preflight.ts +91 -0
  206. package/src/ui/cli/stack-command.ts +144 -0
  207. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  208. package/src/ui/cli/test-runner.ts +1312 -0
  209. package/src/ui/cli/web-command.ts +1073 -0
  210. package/src/ui/server/changelog-parser.ts +94 -0
  211. package/src/ui/server/daemon-reaper.ts +129 -0
  212. package/src/ui/server/editor-manager.ts +301 -0
  213. package/src/ui/server/github-service.ts +223 -0
  214. package/src/ui/server/http-helpers.ts +98 -0
  215. package/src/ui/server/index.ts +3236 -0
  216. package/src/ui/server/log-history.ts +452 -0
  217. package/src/ui/server/perf-report.ts +42 -0
  218. package/src/ui/server/public/environments.html +494 -0
  219. package/src/ui/server/public/favicon.ico +0 -0
  220. package/src/ui/server/public/header.js +1371 -0
  221. package/src/ui/server/public/index.html +2595 -0
  222. package/src/ui/server/public/local-dev.html +7405 -0
  223. package/src/ui/server/public/logs.html +1489 -0
  224. package/src/ui/server/public/perf-report.js +359 -0
  225. package/src/ui/server/public/release.html +2111 -0
  226. package/src/ui/server/public/services.html +1098 -0
  227. package/src/ui/server/public/workspace-picker.js +69 -0
  228. package/src/ui/server/release-deploy-session.ts +246 -0
  229. package/src/ui/server/release-service.ts +1125 -0
  230. package/src/ui/server/sudo-store.ts +111 -0
  231. package/src/ui/server/workspace-routes.ts +2154 -0
@@ -0,0 +1,302 @@
1
+ /**
2
+ * `den up` planning + classification.
3
+ *
4
+ * Pure discovery: for each node in a derived start-set, resolve HOW den would
5
+ * bring it up (Makefile target / opctl dev op / `.den.yml` override),
6
+ * determine its health signal and container identity, and CLASSIFY it so the up
7
+ * flow can treat the well-behaved majority headlessly while surfacing the few
8
+ * outliers (optional leaves, unconditionally-interactive ups) for a human
9
+ * decision. The same structured plan feeds the CLI (`den up --plan` /
10
+ * pre-flight prompt), the web UX (outline + start toggle), and CI (defaults).
11
+ *
12
+ * Conventions are PRIORS verified here, never trusted blind:
13
+ * - command: Makefile `up` is the default; `.den.yml up:` overrides; the
14
+ * opctl `dev` op is the fallback.
15
+ * - health: `.den.yml` health if declared, else `http://<container>/healthz`
16
+ * (the port-80 + container-name convention).
17
+ * - optional: a node nothing else HARD-depends on (only soft/event inbound).
18
+ * - interactive: the up recipe (or its direct entry script) reaches an
19
+ * UNCONDITIONAL tab/login/prompt (osascript / `stripe login` /
20
+ * `read`) that isn't guarded by a TTY / USE_TABS check. (TTY-gated
21
+ * tabs are safe — den runs without a TTY.)
22
+ */
23
+
24
+ import * as fs from "fs";
25
+ import * as path from "path";
26
+ import type { DerivedGraph } from "./derivation/types.js";
27
+ import type { ResolvedService } from "../types/index.js";
28
+ import { resolveService } from "./service-runner.js";
29
+ import { readManifest } from "../manifest/read-manifest.js";
30
+ import { effectiveLocalCommands } from "../stacks/manifest-commands.js";
31
+ import { getFocusedWorkspaceName } from "./workspaces.js";
32
+ import { listMakeTargets } from "./make-targets.js";
33
+ import { devOpContainerNames } from "./opctl.js";
34
+
35
+ import { product } from "../product.js";
36
+ export type UpFlag = "auto-start off" | "interactive" | "no-command";
37
+
38
+ export interface NodePlan {
39
+ repo: string;
40
+ /** Concrete command den would run for `up`, or null if none was found. */
41
+ upCommand: string | null;
42
+ /** Where the command came from. */
43
+ upSource: "manifest" | "make" | "opctl-dev" | "none";
44
+ /** opctl container names declared by the repo's dev/up op (for down/health). */
45
+ containerNames: string[];
46
+ /** URL den would poll for readiness, or null. */
47
+ healthUrl: string | null;
48
+ /** The user toggled this system's auto-start off in the workspace. */
49
+ noAutostart: boolean;
50
+ /** `up` may open a tab / prompt unattended (not just a one-touch op-signin). */
51
+ interactive: boolean;
52
+ interactiveReason: string;
53
+ /** Short label for the interactivity kind: tabs | stripe | 1pwd | prompt. */
54
+ interactiveKind: string;
55
+ /** What den does by default with no human input (CI / no-TTY). */
56
+ defaultAction: "start" | "skip";
57
+ flags: UpFlag[];
58
+ notes: string[];
59
+ }
60
+
61
+ /**
62
+ * Resolve the up command for a service. Makefile `up` is the default (the
63
+ * discovered convention); a `.den.yml` `up:` override wins when present; the
64
+ * opctl `dev` op is the fallback when there is no make target.
65
+ */
66
+ export function resolveUpCommand(svc: ResolvedService): {
67
+ command: string | null;
68
+ source: NodePlan["upSource"];
69
+ overrideAvailable?: string;
70
+ } {
71
+ const manifest = readManifest(svc.repoPath);
72
+ const manifestUp = manifest
73
+ ? effectiveLocalCommands(
74
+ manifest,
75
+ svc.subsystem,
76
+ svc.repoName,
77
+ getFocusedWorkspaceName() ?? "default",
78
+ )["up"]
79
+ : undefined;
80
+ // A compose stack is run by den behind the edge, never by `make up`
81
+ // (which would publish host ports).
82
+ if (manifest?.compose && manifestUp) {
83
+ const cmd = typeof manifestUp === "string" ? manifestUp : manifestUp.cmd;
84
+ return { command: cmd, source: "manifest" };
85
+ }
86
+ const manifestUpCmd =
87
+ typeof manifestUp === "string" ? manifestUp : manifestUp?.cmd;
88
+
89
+ const hasMakeUp = listMakeTargets(svc.repoPath).targets.includes("up");
90
+ if (hasMakeUp) {
91
+ return {
92
+ command: "make up",
93
+ source: "make",
94
+ ...(manifestUpCmd && manifestUpCmd !== "make up"
95
+ ? { overrideAvailable: manifestUpCmd }
96
+ : {}),
97
+ };
98
+ }
99
+ if (devOpContainerNames(svc.repoPath).length > 0) {
100
+ return { command: "opctl run dev", source: "opctl-dev" };
101
+ }
102
+ if (manifestUpCmd) return { command: manifestUpCmd, source: "manifest" };
103
+ return { command: null, source: "none" };
104
+ }
105
+
106
+ /** The readiness URL: declared `.den.yml` health, else container:80/healthz. */
107
+ export function resolveHealthUrl(
108
+ svc: ResolvedService,
109
+ containerNames: string[],
110
+ ): string | null {
111
+ const manifest = readManifest(svc.repoPath);
112
+ const health = manifest?.services?.[svc.subsystem]?.health?.local as
113
+ | { url?: string; cmd?: string }
114
+ | undefined;
115
+ if (health?.url) return health.url;
116
+ if (containerNames.length > 0) return `http://${containerNames[0]}/healthz`;
117
+ return null;
118
+ }
119
+
120
+ const INTERACTIVE_MARKERS =
121
+ /\bosascript\b|\bstripe\s+(login|listen)\b|\bread\s+-[rp]\b/;
122
+ const TTY_GUARD = /-t\s+1\b|USE_TABS|\bCI\b|\[\[\s*-t/;
123
+
124
+ /**
125
+ * Recipe text for a Makefile target PLUS the recipes of its direct
126
+ * prerequisites (one level). Following prereqs matters because an `up` target is
127
+ * often just an alias — e.g. billing's `up: dev`, where the real (interactive)
128
+ * work lives in the `dev` recipe.
129
+ */
130
+ function makeRecipe(
131
+ makefilePath: string,
132
+ target: string,
133
+ seen = new Set<string>(),
134
+ ): string {
135
+ if (seen.has(target)) return "";
136
+ seen.add(target);
137
+ let text: string;
138
+ try {
139
+ text = fs.readFileSync(makefilePath, "utf-8");
140
+ } catch {
141
+ return "";
142
+ }
143
+ const lines = text.split("\n");
144
+ const recipe: string[] = [];
145
+ const prereqs: string[] = [];
146
+ let capturing = false;
147
+ for (const line of lines) {
148
+ const header = new RegExp(`^${target}:(.*)$`).exec(line);
149
+ if (header) {
150
+ capturing = true;
151
+ // Prerequisite targets are the space-separated tokens after the colon
152
+ // (drop ## comments and inline vars).
153
+ for (const tok of header[1].replace(/##.*/, "").trim().split(/\s+/)) {
154
+ if (tok && /^[\w.-]+$/.test(tok)) prereqs.push(tok);
155
+ }
156
+ continue;
157
+ }
158
+ if (capturing) {
159
+ if (line.startsWith("\t") || line.trim() === "") recipe.push(line);
160
+ else capturing = false; // next rule starts
161
+ }
162
+ }
163
+ const prereqRecipes = prereqs.map((p) => makeRecipe(makefilePath, p, seen));
164
+ return [recipe.join("\n"), ...prereqRecipes].join("\n");
165
+ }
166
+
167
+ /** Scripts the recipe directly invokes (one level — not transitive). */
168
+ function entryScripts(recipe: string, repoPath: string): string[] {
169
+ const out: string[] = [];
170
+ for (const m of recipe.matchAll(/(?:\.\/)?([\w./-]+\.sh)\b/g)) {
171
+ const rel = m[1].replace(/^\.\//, "");
172
+ const abs = path.join(repoPath, rel);
173
+ if (fs.existsSync(abs)) out.push(abs);
174
+ }
175
+ return out;
176
+ }
177
+
178
+ /**
179
+ * Whether `up` reaches an UNCONDITIONAL interactive tab/login/prompt. We scan
180
+ * the recipe and its DIRECT entry scripts only (not transitively): an
181
+ * interactive marker one level deeper, behind a TTY/USE_TABS gate (e.g. scribe's
182
+ * run_in_tab.sh), is safe because den runs without a TTY — only inline,
183
+ * unguarded markers (e.g. billing's `osascript` in the recipe itself) count.
184
+ */
185
+ /** A short label for the kind of interactivity (for inline plan tags). */
186
+ function interactiveKindOf(hit: string): string {
187
+ if (/osascript|new tab/i.test(hit)) return "tabs";
188
+ if (/stripe/i.test(hit)) return "stripe";
189
+ if (/op\s+signin/i.test(hit)) return "1pwd";
190
+ return "prompt";
191
+ }
192
+
193
+ export function classifyInteractive(
194
+ svc: ResolvedService,
195
+ upCommand: string | null,
196
+ ): { interactive: boolean; reason: string; kind: string } {
197
+ if (!upCommand) return { interactive: false, reason: "", kind: "" };
198
+
199
+ const makeTarget = upCommand.match(/^make\s+([\w-]+)/)?.[1];
200
+ const sources: { label: string; content: string }[] = [];
201
+ if (makeTarget) {
202
+ const recipe = makeRecipe(path.join(svc.repoPath, "Makefile"), makeTarget);
203
+ sources.push({ label: `Makefile:${makeTarget}`, content: recipe });
204
+ for (const script of entryScripts(recipe, svc.repoPath)) {
205
+ try {
206
+ sources.push({
207
+ label: path.relative(svc.repoPath, script),
208
+ content: fs.readFileSync(script, "utf-8"),
209
+ });
210
+ } catch {
211
+ /* unreadable — skip */
212
+ }
213
+ }
214
+ }
215
+
216
+ for (const s of sources) {
217
+ if (INTERACTIVE_MARKERS.test(s.content) && !TTY_GUARD.test(s.content)) {
218
+ const hit = s.content.match(INTERACTIVE_MARKERS)?.[0] ?? "prompt";
219
+ return {
220
+ interactive: true,
221
+ reason: `${s.label} runs an unguarded ${hit.trim()}`,
222
+ kind: interactiveKindOf(hit),
223
+ };
224
+ }
225
+ }
226
+ return { interactive: false, reason: "", kind: "" };
227
+ }
228
+
229
+ export interface UpPlanOptions {
230
+ /**
231
+ * Repos whose auto-start is toggled OFF in the workspace (`autostart: false`
232
+ * on the service entry). Not derivable from the graph — a node with only soft
233
+ * inbound edges is usually still a dependency you want — so it's declared
234
+ * per workspace, not guessed.
235
+ */
236
+ noAutostart?: Set<string>;
237
+ }
238
+
239
+ /** Build the per-node plan for an ordered start-set over a derived graph. */
240
+ export function buildUpPlan(
241
+ graph: DerivedGraph,
242
+ order: string[],
243
+ opts: UpPlanOptions = {},
244
+ ): NodePlan[] {
245
+ const noAutostartSet = opts.noAutostart ?? new Set<string>();
246
+ void graph; // reserved for future graph-derived hints (leaf candidates, etc.)
247
+ return order.map((repo) => {
248
+ const svc = resolveService(repo);
249
+ const notes: string[] = [];
250
+ if (!svc) {
251
+ return {
252
+ repo,
253
+ upCommand: null,
254
+ upSource: "none",
255
+ containerNames: [],
256
+ healthUrl: null,
257
+ noAutostart: noAutostartSet.has(repo),
258
+ interactive: false,
259
+ interactiveReason: "",
260
+ interactiveKind: "",
261
+ defaultAction: "skip",
262
+ flags: ["no-command"],
263
+ notes: ["not resolvable in the active workspace"],
264
+ };
265
+ }
266
+
267
+ const { command, source, overrideAvailable } = resolveUpCommand(svc);
268
+ if (overrideAvailable) {
269
+ notes.push(
270
+ `${product().manifestYml} up override available: ${overrideAvailable}`,
271
+ );
272
+ }
273
+ const containerNames = devOpContainerNames(svc.repoPath);
274
+ const healthUrl = resolveHealthUrl(svc, containerNames);
275
+ const noAutostart = noAutostartSet.has(repo);
276
+ const { interactive, reason, kind } = classifyInteractive(svc, command);
277
+
278
+ const flags: UpFlag[] = [];
279
+ if (noAutostart) flags.push("auto-start off");
280
+ if (interactive) flags.push("interactive");
281
+ if (!command) flags.push("no-command");
282
+
283
+ // No human input (CI / no-TTY): skip flagged nodes, start the rest.
284
+ const defaultAction: NodePlan["defaultAction"] =
285
+ noAutostart || interactive || !command ? "skip" : "start";
286
+
287
+ return {
288
+ repo,
289
+ upCommand: command,
290
+ upSource: source,
291
+ containerNames,
292
+ healthUrl,
293
+ noAutostart,
294
+ interactive,
295
+ interactiveReason: reason,
296
+ interactiveKind: kind,
297
+ defaultAction,
298
+ flags,
299
+ notes,
300
+ };
301
+ });
302
+ }