@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,99 @@
1
+ /**
2
+ * Read-only stack status: what `stack ls` and `stack health` show, as data.
3
+ * The CLI prints it; the web server serves it at /api/stacks (and through it
4
+ * the MCP `stacks` tool).
5
+ */
6
+ import { readEdgeConfig } from "../edge/config.js";
7
+ import { routeUrl } from "../edge/render.js";
8
+ import {
9
+ getFocusedWorkspaceName,
10
+ getWorkspaceByName,
11
+ } from "../environment-manager/workspaces.js";
12
+ import { readStacks } from "./config.js";
13
+ import { projectName, stackWorkspace } from "./render.js";
14
+ import { stackHealth, type StackContext } from "./runtime.js";
15
+
16
+ export interface StackRoute {
17
+ name: string;
18
+ url: string;
19
+ /** `<service>:<container port>`. */
20
+ target: string;
21
+ }
22
+
23
+ export interface StackStatus {
24
+ name: string;
25
+ repo: string;
26
+ dir: string | null;
27
+ /** The Compose project: `<stack>-<workspace>`. */
28
+ project: string;
29
+ routes: StackRoute[];
30
+ uses: string[];
31
+ /** Container health (only when asked for: it runs `docker ps`). */
32
+ health?: { ok: boolean; detail: string };
33
+ }
34
+
35
+ const WS_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
36
+
37
+ /**
38
+ * The den workspace stacks read their manifests from, and the Compose
39
+ * project suffix they run under. `denWorkspace` defaults to the CLI's
40
+ * -w override, then the focused workspace, then `default`.
41
+ */
42
+ export function resolveStackWorkspaces(
43
+ denWorkspace?: string | null,
44
+ env: NodeJS.ProcessEnv = process.env,
45
+ ): { denWorkspace: string; stackWorkspace: string } {
46
+ const fkWs =
47
+ denWorkspace ||
48
+ env.DEN_WORKSPACE_OVERRIDE ||
49
+ getFocusedWorkspaceName() ||
50
+ "default";
51
+ const ws = env.DEN_STACK_WORKSPACE || stackWorkspace(fkWs);
52
+ if (!WS_RE.test(ws))
53
+ throw new Error(
54
+ `workspace "${ws}" must be lowercase letters, digits and dashes`,
55
+ );
56
+ return { denWorkspace: fkWs, stackWorkspace: ws };
57
+ }
58
+
59
+ /** The edge, the known stacks and the project suffix for one workspace. */
60
+ export function loadStackContext(denWorkspace?: string | null): StackContext {
61
+ const { denWorkspace: fkWs, stackWorkspace: ws } =
62
+ resolveStackWorkspaces(denWorkspace);
63
+ const edge = readEdgeConfig();
64
+ // Repo manifests come from that den workspace's checkouts.
65
+ const stacks = readStacks(undefined, {
66
+ workspace: getWorkspaceByName(fkWs) ? fkWs : undefined,
67
+ });
68
+ return { edge, stacks, workspace: ws };
69
+ }
70
+
71
+ /** Each stack's name, project, routes and (optionally) container health. */
72
+ export function stackStatuses(
73
+ ctx: StackContext,
74
+ opts: { health?: boolean; healthOf?: typeof stackHealth } = {},
75
+ ): StackStatus[] {
76
+ const healthOf = opts.healthOf ?? stackHealth;
77
+ return Object.values(ctx.stacks).map((s) => {
78
+ const status: StackStatus = {
79
+ name: s.name,
80
+ repo: s.repo,
81
+ dir: s.dir ?? null,
82
+ project: projectName(s.name, ctx.workspace),
83
+ routes: Object.entries(s.routes).map(([route, target]) => ({
84
+ name: route,
85
+ url: routeUrl(`${route}.${s.name}`, ctx.edge),
86
+ target,
87
+ })),
88
+ uses: [...s.uses],
89
+ };
90
+ if (opts.health) {
91
+ try {
92
+ status.health = healthOf(ctx, s);
93
+ } catch (e) {
94
+ status.health = { ok: false, detail: (e as Error).message };
95
+ }
96
+ }
97
+ return status;
98
+ });
99
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * A stack = one repo's Docker Compose project, run behind the edge.
3
+ *
4
+ * stacks:
5
+ * webapp:
6
+ * repo: sample-webapp
7
+ * files: [docker-compose.yml] # relative to repo/dir
8
+ * routes: { app: frontend:80 } # https://app.webapp.<domain>
9
+ * generate: [SECRET_KEY] # random once, kept in local state
10
+ * uses: [ragstack] # start first + private link network
11
+ * env: # compose ${VAR} substitution
12
+ * RAG_PROXY_TARGET: "http://{ragstack.ragstack-server}:80"
13
+ *
14
+ * `{stack.service}` expands to that service's container name.
15
+ */
16
+ export interface StackConfig {
17
+ name: string;
18
+ repo: string;
19
+ /** Absolute checkout path; when unset, `<repos_root>/<repo>`. */
20
+ repoPath?: string;
21
+ /** Subdirectory of the repo holding the compose files. */
22
+ dir?: string;
23
+ files: string[];
24
+ /** `<route>` → `<service>:<container port>`. */
25
+ routes: Record<string, string>;
26
+ generate: string[];
27
+ uses: string[];
28
+ env: Record<string, string>;
29
+ /** Compose profiles to enable. */
30
+ profiles: string[];
31
+ /** service → replacement healthcheck `test`, for a repo's broken check. */
32
+ healthchecks?: Record<string, string[]>;
33
+ /**
34
+ * service → environment variables set on that service (override wins by
35
+ * key). For values a repo's compose file hard-codes, which `env` (compose
36
+ * `${VAR}` substitution) cannot change.
37
+ */
38
+ serviceEnv?: Record<string, Record<string, string>>;
39
+ }
40
+
41
+ /** What `docker compose config --format json` tells us about a service. */
42
+ export interface ResolvedService {
43
+ container_name?: string;
44
+ ports?: unknown[];
45
+ networks?: Record<string, unknown> | null;
46
+ environment?: Record<string, string | null> | null;
47
+ }
48
+
49
+ export interface ResolvedProject {
50
+ name: string;
51
+ services: Record<string, ResolvedService>;
52
+ networks?: Record<string, { name?: string }>;
53
+ }
@@ -0,0 +1,212 @@
1
+ import * as fs from "fs";
2
+ import { execFileSync } from "child_process";
3
+
4
+ import { product } from "../product.js";
5
+ export type TerminalApp = "iterm2" | "terminal";
6
+
7
+ export interface OpenTabOptions {
8
+ cmd: string;
9
+ cwd: string;
10
+ label?: string;
11
+ terminalApp?: TerminalApp;
12
+ /**
13
+ * Variables to export in the tab before running `cmd`. A tab is a fresh
14
+ * login shell, so den-resolved values (the org config's service_env) are only
15
+ * present if we put them there — without them a consumer repo silently falls
16
+ * back to its `../<repo>` sibling default and fails in a nested checkout.
17
+ *
18
+ * Keep this to non-secret values: it lands in the tab's shell history.
19
+ */
20
+ env?: Record<string, string>;
21
+ }
22
+
23
+ export interface OpenedTab {
24
+ terminalApp: TerminalApp;
25
+ tabId: string;
26
+ }
27
+
28
+ const APPLE_TERMINAL_APP = fs.existsSync(
29
+ "/System/Applications/Utilities/Terminal.app",
30
+ )
31
+ ? "/System/Applications/Utilities/Terminal.app"
32
+ : "Terminal";
33
+
34
+ export function shellQuote(value: string): string {
35
+ return `'${value.replace(/'/g, `'\\''`)}'`;
36
+ }
37
+
38
+ function appleScriptString(value: string): string {
39
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
40
+ }
41
+
42
+ function automationHelp(appName: string): string {
43
+ return (
44
+ `Could not automate ${appName}. ` +
45
+ `Allow ${product().title}/your terminal to control Terminal, iTerm, and System Events in ` +
46
+ "System Settings > Privacy & Security > Automation."
47
+ );
48
+ }
49
+
50
+ function runAppleScript(script: string, appName: string): string {
51
+ try {
52
+ return execFileSync("osascript", ["-e", script], {
53
+ encoding: "utf-8",
54
+ stdio: ["ignore", "pipe", "pipe"],
55
+ }).trim();
56
+ } catch (error) {
57
+ const stderr =
58
+ typeof (error as { stderr?: unknown }).stderr === "string"
59
+ ? ((error as { stderr: string }).stderr ?? "")
60
+ : "";
61
+ const message = stderr.trim() || (error as Error).message;
62
+ throw new Error(`${automationHelp(appName)} ${message}`.trim());
63
+ }
64
+ }
65
+
66
+ function buildTabCommand(opts: OpenTabOptions): string {
67
+ const exports = Object.entries(opts.env ?? {})
68
+ .filter(([key, value]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) && value)
69
+ .map(([key, value]) => `export ${key}=${shellQuote(value)}`);
70
+ const prefix = exports.length > 0 ? `${exports.join(" ")} && ` : "";
71
+ return `${prefix}cd ${shellQuote(opts.cwd)} && ${opts.cmd}`;
72
+ }
73
+
74
+ function openITermTab(command: string): OpenedTab {
75
+ const script = `
76
+ tell application "iTerm"
77
+ activate
78
+ if (count of windows) = 0 then
79
+ set newWindow to (create window with default profile)
80
+ set newTab to current tab of newWindow
81
+ else
82
+ tell current window
83
+ set newTab to (create tab with default profile)
84
+ end tell
85
+ end if
86
+ tell current session of newTab
87
+ write text ${appleScriptString(command)}
88
+ end tell
89
+ return (id of current session of newTab as text)
90
+ end tell
91
+ `;
92
+ return { terminalApp: "iterm2", tabId: runAppleScript(script, "iTerm2") };
93
+ }
94
+
95
+ function openAppleTerminalTab(command: string): OpenedTab {
96
+ const script = `
97
+ tell application ${appleScriptString(APPLE_TERMINAL_APP)}
98
+ activate
99
+ if not (exists front window) then
100
+ do script ""
101
+ end if
102
+ end tell
103
+ delay 0.1
104
+ tell application "System Events"
105
+ tell process "Terminal"
106
+ keystroke "t" using command down
107
+ end tell
108
+ end tell
109
+ delay 0.1
110
+ tell application ${appleScriptString(APPLE_TERMINAL_APP)}
111
+ do script ${appleScriptString(command)} in selected tab of front window
112
+ return tty of selected tab of front window
113
+ end tell
114
+ `;
115
+ return {
116
+ terminalApp: "terminal",
117
+ tabId: runAppleScript(script, "Apple Terminal"),
118
+ };
119
+ }
120
+
121
+ export function openTab(opts: OpenTabOptions): OpenedTab {
122
+ const terminalApp = opts.terminalApp ?? detectDefaultTerminalApp();
123
+ const command = buildTabCommand(opts);
124
+ if (terminalApp === "iterm2") return openITermTab(command);
125
+ return openAppleTerminalTab(command);
126
+ }
127
+
128
+ export function closeTab(tabId: string, terminalApp: TerminalApp): void {
129
+ if (terminalApp === "iterm2") {
130
+ const script = `
131
+ tell application "iTerm"
132
+ repeat with w in windows
133
+ repeat with t in tabs of w
134
+ if (id of current session of t as text) is ${appleScriptString(tabId)} then
135
+ close t
136
+ return "closed"
137
+ end if
138
+ end repeat
139
+ end repeat
140
+ end tell
141
+ return "missing"
142
+ `;
143
+ runAppleScript(script, "iTerm2");
144
+ return;
145
+ }
146
+
147
+ const script = `
148
+ tell application ${appleScriptString(APPLE_TERMINAL_APP)}
149
+ repeat with w in windows
150
+ repeat with t in tabs of w
151
+ try
152
+ if (tty of t) is ${appleScriptString(tabId)} then
153
+ close t
154
+ return "closed"
155
+ end if
156
+ end try
157
+ end repeat
158
+ end repeat
159
+ end tell
160
+ return "missing"
161
+ `;
162
+ runAppleScript(script, "Apple Terminal");
163
+ }
164
+
165
+ export function tabExists(tabId: string, terminalApp: TerminalApp): boolean {
166
+ if (terminalApp === "iterm2") {
167
+ const script = `
168
+ tell application "iTerm"
169
+ repeat with w in windows
170
+ repeat with t in tabs of w
171
+ if (id of current session of t as text) is ${appleScriptString(tabId)} then return "yes"
172
+ end repeat
173
+ end repeat
174
+ end tell
175
+ return "no"
176
+ `;
177
+ return runAppleScript(script, "iTerm2") === "yes";
178
+ }
179
+
180
+ const script = `
181
+ tell application ${appleScriptString(APPLE_TERMINAL_APP)}
182
+ repeat with w in windows
183
+ repeat with t in tabs of w
184
+ try
185
+ if (tty of t) is ${appleScriptString(tabId)} then return "yes"
186
+ end try
187
+ end repeat
188
+ end repeat
189
+ end tell
190
+ return "no"
191
+ `;
192
+ return runAppleScript(script, "Apple Terminal") === "yes";
193
+ }
194
+
195
+ export function resolveTerminalApp(key: string | undefined): TerminalApp {
196
+ if (!key) return detectDefaultTerminalApp();
197
+ if (key === "iterm2") return "iterm2";
198
+ if (key === "terminal") return "terminal";
199
+ if (key === "ghostty") {
200
+ throw new Error(
201
+ `Tab mode does not support Ghostty yet. Set \`${product().cli} config terminal=iterm2\`, \`${product().cli} config terminal=terminal\`, or run with \`--no-tabs\`.`,
202
+ );
203
+ }
204
+ throw new Error(
205
+ `Tab mode only supports iTerm2 and Apple Terminal. Configured terminal "${key}" is not supported; run with --no-tabs to use daemon mode.`,
206
+ );
207
+ }
208
+
209
+ function detectDefaultTerminalApp(): TerminalApp {
210
+ if (fs.existsSync("/Applications/iTerm.app")) return "iterm2";
211
+ return "terminal";
212
+ }