@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,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
|
+
}
|