humanish 0.110.2 → 0.110.4
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/ARCHITECTURE.md +10 -15
- package/CHANGELOG.md +27 -0
- package/CONTEXT.md +1 -1
- package/dist/actors/computer-use/actor.js +2 -2
- package/dist/actors/computer-use/loop/ending.js +1 -1
- package/dist/actors/registry.js +1 -1
- package/dist/actors/scripted-browser/actor.js +17 -8
- package/dist/actors/scripted-browser/launch-env.d.ts +12 -0
- package/dist/actors/scripted-browser/launch-env.js +50 -0
- package/dist/analysis/automatic-completion.d.ts +2 -1
- package/dist/analysis/automatic-completion.js +15 -2
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/observe.js +3 -2
- package/dist/cli/commands/runs.js +5 -2
- package/dist/cli/commands/study-route-computer-use.js +3 -2
- package/dist/cli/commands/study-route-preview.js +2 -1
- package/dist/cli/commands/study-route-shared-world.js +3 -3
- package/dist/cli/commands/study-run.js +9 -19
- package/dist/cli/commands/study.js +4 -1
- package/dist/cli/findings.d.ts +5 -0
- package/dist/cli/findings.js +21 -0
- package/dist/cli/io.d.ts +2 -1
- package/dist/cli/io.js +3 -2
- package/dist/cli/observer-follow.d.ts +6 -2
- package/dist/cli/observer-follow.js +15 -5
- package/dist/evidence/redaction.js +15 -4
- package/dist/routes/computer-use/live-phase.js +8 -1
- package/dist/routes/computer-use/local-vm.js +2 -1
- package/dist/routes/computer-use/participant-runs.d.ts +6 -9
- package/dist/routes/computer-use/participant-runs.js +16 -36
- package/dist/routes/computer-use/plan.d.ts +2 -2
- package/dist/routes/computer-use/plan.js +7 -6
- package/dist/routes/computer-use/route.d.ts +5 -18
- package/dist/routes/computer-use/route.js +19 -79
- package/dist/routes/computer-use/setup.d.ts +3 -2
- package/dist/routes/computer-use/setup.js +2 -1
- package/dist/routes/computer-use/types.d.ts +6 -10
- package/dist/routes/scripted/plan.js +5 -4
- package/dist/routes/scripted/route.d.ts +4 -17
- package/dist/routes/scripted/route.js +15 -43
- package/dist/routes/scripted/surface-sessions.d.ts +2 -0
- package/dist/routes/scripted/surface-sessions.js +2 -1
- package/dist/routes/scripted/types.d.ts +1 -6
- package/dist/routes/shared-world/participant-specs.js +5 -1
- package/dist/routes/shared-world/plan.js +7 -6
- package/dist/routes/shared-world/route.d.ts +5 -11
- package/dist/routes/shared-world/route.js +27 -66
- package/dist/routes/shared-world/types.d.ts +2 -6
- package/dist/routes/terminal/plan.js +4 -3
- package/dist/routes/terminal/route.d.ts +3 -13
- package/dist/routes/terminal/route.js +21 -60
- package/dist/routes/terminal/session.d.ts +2 -3
- package/dist/routes/terminal/session.js +3 -3
- package/dist/routes/terminal/types.d.ts +2 -7
- package/dist/run/display.d.ts +10 -0
- package/dist/run/display.js +22 -0
- package/dist/run/reclaim.d.ts +4 -2
- package/dist/run/reclaim.js +61 -18
- package/dist/run/route-shell.d.ts +60 -0
- package/dist/run/route-shell.js +51 -0
- package/dist/run/run.d.ts +16 -1
- package/dist/run/run.js +33 -5
- package/dist/run/status.d.ts +8 -0
- package/dist/run/status.js +6 -0
- package/dist/run/stored-runs.js +26 -11
- package/dist/study/composition-rules.js +33 -33
- package/dist/study/config.d.ts +9 -2
- package/dist/study/config.js +14 -5
- package/dist/study/doctor.js +5 -4
- package/dist/study/init-templates.d.ts +1 -1
- package/dist/study/init-templates.js +43 -34
- package/dist/study/init.js +1 -1
- package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
- package/dist/study/{convert.js → migrate/convert.js} +29 -31
- package/dist/study/{migrate.d.ts → migrate/migrate.d.ts} +1 -1
- package/dist/study/{migrate.js → migrate/migrate.js} +2 -2
- package/dist/study/{owned-files.js → migrate/owned-files.js} +1 -1
- package/dist/study/migrate/v2.d.ts +68 -0
- package/dist/study/migrate/v2.js +480 -0
- package/dist/study/parse/actors.d.ts +46 -1
- package/dist/study/parse/actors.js +116 -69
- package/dist/study/parse/execution.js +2 -2
- package/dist/study/parse/study-v3.d.ts +26 -0
- package/dist/study/parse/study-v3.js +53 -35
- package/dist/study/parse/study.d.ts +38 -0
- package/dist/study/parse/study.js +225 -0
- package/dist/study/parse/subject.js +1 -1
- package/dist/study/persona-resolve.d.ts +2 -4
- package/dist/study/persona-resolve.js +2 -1
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +5 -4
- package/dist/study/plan-participants.js +12 -11
- package/dist/study/plan.js +4 -3
- package/dist/study/preflight-probes.js +2 -2
- package/dist/study/preflight.js +2 -2
- package/dist/study/routing.js +12 -15
- package/dist/study/run-study-options.js +3 -3
- package/dist/study/study-fields.d.ts +28 -0
- package/dist/study/study-fields.js +51 -0
- package/dist/study/summary.js +13 -17
- package/dist/study/types.d.ts +40 -0
- package/dist/study/url-credentials.js +2 -1
- package/dist/study/validation.d.ts +2 -0
- package/dist/study/validation.js +46 -41
- package/dist/study/warnings.js +5 -4
- package/dist/substrates/local/runtime-config.js +9 -9
- package/dist/tui-app.js +3 -3
- package/docs/architecture/actor-contract.md +1 -1
- package/docs/architecture/external-public-shared-world.md +1 -1
- package/docs/architecture/terminal-product-route.md +13 -13
- package/docs/contracts/schemas.md +13 -4
- package/docs/contracts/study-analysis.md +5 -2
- package/docs/product/automatic-analysis.md +1 -1
- package/package.json +1 -1
- /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -9,25 +9,20 @@ longer declares that name. [CONTEXT.md](CONTEXT.md) defines the domain terms, an
|
|
|
9
9
|
|
|
10
10
|
The steps follow a live computer-use study on a hosted E2B desktop. Every route shares steps 1, 2, 7
|
|
11
11
|
and 8. Steps 3 to 6 run in the route's admit function and the `run()` it returns, which `runStudyWith`
|
|
12
|
-
reaches through `admitPlan` (`src/run-study.ts`)
|
|
12
|
+
reaches through `admitPlan` (`src/run-study.ts`). Four of the admit functions hand their checks and
|
|
13
|
+
the body they run inside the run scope to `admitRoute` (`src/run/route-shell.ts`). It completes the
|
|
14
|
+
automatic analysis of a refusal, and its `run()` opens the run scope, runs the body and completes
|
|
15
|
+
the analysis of the run:
|
|
13
16
|
|
|
14
|
-
- computer-use: `admitComputerUsePlan` (`src/routes/computer-use/route.ts`)
|
|
15
|
-
`
|
|
16
|
-
- scripted: `admitScriptedPlan` (`src/routes/scripted/route.ts`)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- shared-world: `admitSharedWorldPlan` (`src/routes/shared-world/route.ts`); its `run()` calls
|
|
21
|
-
`runAdmittedSharedWorldRun` (`src/routes/shared-world/route.ts`).
|
|
17
|
+
- computer-use: `admitComputerUsePlan` (`src/routes/computer-use/route.ts`), with the checks in
|
|
18
|
+
`admitCuaRun` (`src/routes/computer-use/setup.ts`).
|
|
19
|
+
- scripted: `admitScriptedPlan` (`src/routes/scripted/route.ts`), whose checks run inside the run.
|
|
20
|
+
- terminal: `admitTerminalPlan` (`src/routes/terminal/route.ts`), with a live plan's checks in
|
|
21
|
+
`checkLiveTerminalMachine` (`src/routes/terminal/session.ts`).
|
|
22
|
+
- shared-world: `admitSharedWorldPlan` (`src/routes/shared-world/route.ts`).
|
|
22
23
|
- preview: `admitPreviewPlan` (`src/routes/preview.ts`); its `run()` calls `runPreviewPlan`
|
|
23
24
|
(`src/routes/preview.ts`), which publishes a fixture bundle.
|
|
24
25
|
|
|
25
|
-
`runComputerUsePlan` (`src/routes/computer-use/route.ts`), `runTerminalPlan`
|
|
26
|
-
(`src/routes/terminal/route.ts`) and `runSharedWorldPlan` (`src/routes/shared-world/route.ts`)
|
|
27
|
-
admit and run in one call. Only tests call them, directly or through the route modules'
|
|
28
|
-
`runCuaActorStudy`, `runTerminalProductStudy` and `runConcurrentSharedWorld`. `runStudyWith` and the CLI do
|
|
29
|
-
not.
|
|
30
|
-
|
|
31
26
|
1. **Parse.** `runStudyCommand` (`src/cli/commands/study-run.ts`) calls `resolveStudyManifest`
|
|
32
27
|
(`src/study/discover.ts`), which calls `parseStudy` (`src/study/config.ts`). It rejects unknown
|
|
33
28
|
keys and the compositions the
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,33 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.110.4: Scripted runs reclaim clean, review says interrupted, Chrome under a long TMPDIR (2026-10-05)
|
|
12
|
+
|
|
13
|
+
humanish 0.110.4 fixes four findings of the 0.110.3 live smoke. `humanish reclaim --check` reports
|
|
14
|
+
a live scripted run against an `app-url` subject `clean`, with reason `no-sandbox`, and exits 0.
|
|
15
|
+
`humanish review` on a computer-use run stopped by Ctrl-C after its desktop started says the run
|
|
16
|
+
was interrupted. A run that failed before any participant started no longer prints "Participants
|
|
17
|
+
finished; preparing analysis…". The scripted route launches Chrome when `TMPDIR` is too long for
|
|
18
|
+
Chrome's socket path. Planner refusals for a library config built without `parseStudy` name v3
|
|
19
|
+
keys, and so does `humanish migrate` when one of those checks refuses a v2 file. Inside, a v3
|
|
20
|
+
parser runs beside `parseStudy`, src reads the study through accessors, and `humanish migrate`
|
|
21
|
+
reads v2 files through its own front end; the library API is unchanged.
|
|
22
|
+
|
|
23
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.110.4)
|
|
24
|
+
|
|
25
|
+
## 0.110.3: Ctrl-C at the watch prompt, review says analysis is off, E2B docs links kept (2026-10-05)
|
|
26
|
+
|
|
27
|
+
humanish 0.110.3 runs the `watch` and `observe` shutdown, and prints `watch stopped` or `observe
|
|
28
|
+
stopped`, when a Ctrl-C lands right at their prompt. It starts no automatic analysis for a run a
|
|
29
|
+
signal interrupted. `humanish review` says when a study's `review.analysis: false` is why a live
|
|
30
|
+
run has no findings. `humanish verify` no longer blocks a run whose text holds a URL followed by an
|
|
31
|
+
escaped line break and an `E2B_` variable, and redacted output keeps the E2B docs link that E2B's
|
|
32
|
+
401 names. The `--env-file` alias is now removed in 0.112.0. Inside, the computer-use, scripted,
|
|
33
|
+
terminal and shared-world routes run through one shell, `admitRoute` in `src/run/route-shell.ts`;
|
|
34
|
+
the live smoke ran each route on it.
|
|
35
|
+
|
|
36
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.110.3)
|
|
37
|
+
|
|
11
38
|
## 0.110.2: Credential URLs refused, terminal key proxied, watch exits for agents (2026-10-05)
|
|
12
39
|
|
|
13
40
|
humanish 0.110.2 refuses a study URL that carries a user name, password or credential parameter,
|
package/CONTEXT.md
CHANGED
|
@@ -28,7 +28,7 @@ bundle and output fields keep older spellings; the second table maps them.
|
|
|
28
28
|
| computer use | A model drives a participant by reading screenshots of a desktop and acting with mouse and keyboard. `openai-computer-use` and `local-agent` are the computer-use actors. CUA is the short form in code names such as `CuaExecutor` and in the older spellings listed under `cua` below. | `src/routes/computer-use/`, `src/actors/computer-use/` |
|
|
29
29
|
| fan-out | A computer-use run with more than one participant, each on its own desktop, or any rerun. It writes the fan-out bundle shape and passes only when every expected participant passes. One participant without a rerun keeps the single-participant shape. | `buildCuaFanoutBundle` in `src/routes/computer-use/fanout-bundle.ts`; `judgeComputerUseRun` in `src/routes/computer-use/bundle.ts` |
|
|
30
30
|
| route | The execution path for a study: `computer-use`, `shared-world`, `scripted`, `terminal` or `preview`. `preview` writes a dry-run bundle for `this-repo` subjects with no live participant. | `routeOf` and `StudyRoute` in `src/study/plan.ts`; predicates in `src/study/routing.ts`; one folder or file per route in `src/routes/` |
|
|
31
|
-
| plan | What `planStudy` decides about a study before anything runs: the study id, whether it is a dry run, the analysis settings and the keys, connections and env vars the run needs, plus the route's participants, caps and own fields. There is one variant per route. `planStudy` returns a plan, or a refusal with the route's error code, and the route's
|
|
31
|
+
| plan | What `planStudy` decides about a study before anything runs: the study id, whether it is a dry run, the analysis settings and the keys, connections and env vars the run needs, plus the route's participants, caps and own fields. There is one variant per route. `planStudy` returns a plan, or a refusal with the route's error code, and the route's admit function (`admitComputerUsePlan` and the others) takes the plan. | `StudyPlan` in `src/study/plan-types.ts`; `planStudy` in `src/study/plan.ts`; each route's planner in its `plan.ts` |
|
|
32
32
|
| substrate | Where a participant's desktop or shell runs: a hosted E2B sandbox, or a local Firecracker VM (inside Lima on macOS). | `src/substrates/e2b/`, `src/substrates/local/` |
|
|
33
33
|
| participant desktop | The desktop one computer-use or shared-world participant drives: a substrate plus the route's concerns, such as subject provisioning, comms and the participant's plan. It lives in the route, so `src/substrates/` holds only provider primitives ([ADR 0004](docs/decisions/0004-participant-desktops-stay-in-the-route.md)). | `ParticipantDesktop` in `src/routes/computer-use/participant-desktop.ts`; `createE2BParticipantDesktop` in `src/routes/computer-use/e2b-desktop/desktop.ts`; the local VM's in `src/routes/computer-use/local-vm.ts` |
|
|
34
34
|
| receipt | A line in a run's `sandbox-receipts.ndjson` that records a sandbox's id, provider, the participant or subject it belongs to (`laneId`), and its time limit as soon as the sandbox is created, before any work runs in it. `humanish reclaim` kills sandboxes by these ids after an interrupted run. | `SandboxReceipt` in `src/run/sandbox-receipts.ts`; `reclaimRunSandboxes` in `src/run/reclaim.ts` |
|
|
@@ -63,7 +63,7 @@ export async function runCuaActorSession(options) {
|
|
|
63
63
|
}
|
|
64
64
|
function defaultProvider(openai, strictSpend = false) {
|
|
65
65
|
if (!openai) {
|
|
66
|
-
throw new Error("
|
|
66
|
+
throw new Error("A computer-use session requires either `provider` (injected) or `openai` provider options.");
|
|
67
67
|
}
|
|
68
68
|
return createOpenAiResponsesProvider({
|
|
69
69
|
...openai,
|
|
@@ -72,7 +72,7 @@ function defaultProvider(openai, strictSpend = false) {
|
|
|
72
72
|
}
|
|
73
73
|
function defaultExecutor(desktop, executorOptions) {
|
|
74
74
|
if (!desktop) {
|
|
75
|
-
throw new Error("
|
|
75
|
+
throw new Error("A computer-use session requires either `executor` (injected) or `desktop` to build one.");
|
|
76
76
|
}
|
|
77
77
|
return createE2BDesktopExecutor(desktop, executorOptions ?? {});
|
|
78
78
|
}
|
|
@@ -96,7 +96,7 @@ export function spendLimit(session, estimate, maxUsd) {
|
|
|
96
96
|
// trip.
|
|
97
97
|
export const nonFiniteEstimate = {
|
|
98
98
|
completionReason: "harness_error",
|
|
99
|
-
reason: "the injected estimateTurnCostUsd returned a non-finite estimate while caps.maxUsd is set; the estimator receives one
|
|
99
|
+
reason: "the injected estimateTurnCostUsd returned a non-finite estimate while caps.maxUsd is set; the estimator receives one object with the running token totals. Failing closed instead of running uncapped.",
|
|
100
100
|
};
|
|
101
101
|
/** A study-level stop is a recruiting decision hitting its limit, not this participant's runaway. */
|
|
102
102
|
export function runSpendLimit(reason) {
|
package/dist/actors/registry.js
CHANGED
|
@@ -74,7 +74,7 @@ export const actorRegistry = {
|
|
|
74
74
|
},
|
|
75
75
|
// The ActorId names the terminal-product dispatch slot; the live route records the concrete
|
|
76
76
|
// provider as "codex". keyPlacement "in-sandbox-command-scoped" is registry-declared and
|
|
77
|
-
// enforced by
|
|
77
|
+
// enforced by the terminal route before it creates a sandbox, which is where this actor runs.
|
|
78
78
|
"codex-exec": {
|
|
79
79
|
id: "codex-exec",
|
|
80
80
|
label: "Codex Exec (autonomous terminal agent, in-sandbox)",
|
|
@@ -23,17 +23,26 @@ import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, pre
|
|
|
23
23
|
import { ACTOR_TRACE_SCHEMA, SCRIPTED_BROWSER_CAPABILITIES, } from "../contract.js";
|
|
24
24
|
import { buildBlockedBrowserPersonaSteps, buildBrowserTrace, captureBlockedStepScreenshot, compactBrowserError, executeBrowserPersonaStep, probeAppUrl, sanitizeBrowserEvidenceUrl, screenshotPathForBrowserStep, surfaceScreenshotPath, tracePathForBrowserSurface, } from "./steps.js";
|
|
25
25
|
import { LOOPBACK_EVIDENCE_URL_POLICY, } from "./types.js";
|
|
26
|
+
import { chromeLaunchEnv, chromeLaunchError } from "./launch-env.js";
|
|
26
27
|
/** Production default: lazy playwright-core import + chromium.launch, exactly as the driver
|
|
27
|
-
* always did. Kept in one place so the optional peer is touched by exactly one code path.
|
|
28
|
+
* always did. Kept in one place so the optional peer is touched by exactly one code path. A
|
|
29
|
+
* `TMPDIR` too long for Chrome's singleton socket is replaced for Chrome (chromeLaunchEnv). */
|
|
28
30
|
async function launchPlaywrightChromium(args) {
|
|
29
31
|
const { chromium } = await import("playwright-core");
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const env = await chromeLaunchEnv();
|
|
33
|
+
try {
|
|
34
|
+
const browser = await chromium.launch({
|
|
35
|
+
executablePath: args.browserCommand,
|
|
36
|
+
headless: true,
|
|
37
|
+
args: [...CHROMIUM_EVIDENCE_HYGIENE_FLAGS, "--disable-gpu", "--disable-dev-shm-usage"],
|
|
38
|
+
timeout: args.timeoutMs,
|
|
39
|
+
...(env === undefined ? {} : { env }),
|
|
40
|
+
});
|
|
41
|
+
return browser;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw chromeLaunchError(error, env);
|
|
45
|
+
}
|
|
37
46
|
}
|
|
38
47
|
function assertScriptedSessionPathIds(options) {
|
|
39
48
|
assertSafeOutputPathSegment(options.surface.id, "Browser surface id");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment to launch Chrome with: `env` with `TMPDIR` set to /tmp when its own `TMPDIR` is
|
|
3
|
+
* too long for the singleton socket and /tmp is a writable directory. Undefined when Chrome can
|
|
4
|
+
* launch with `env` as it is, or when /tmp cannot be used; chromeLaunchError then names the cause.
|
|
5
|
+
*/
|
|
6
|
+
export declare function chromeLaunchEnv(env?: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv | undefined>;
|
|
7
|
+
/**
|
|
8
|
+
* The launch error to report: one that names the cause and the fix when Chrome aborted because its
|
|
9
|
+
* singleton socket path was too long, and `error` itself otherwise. `env` is the environment
|
|
10
|
+
* Chrome was launched with.
|
|
11
|
+
*/
|
|
12
|
+
export declare function chromeLaunchError(error: unknown, env?: NodeJS.ProcessEnv): unknown;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Chrome's process singleton listens on a Unix socket at `$TMPDIR/<product>.XXXXXX/SingletonSocket`,
|
|
2
|
+
// where the product is com.google.Chrome, or org.chromium.Chromium for Chromium and Chrome for
|
|
3
|
+
// Testing. A Unix socket path holds at most 107 bytes on Linux and 103 on macOS, so under a longer
|
|
4
|
+
// `TMPDIR`, such as a sandboxed agent's private temp directory, Chrome aborts at launch with
|
|
5
|
+
// "Socket path too long", and Playwright reports only that the browser closed. The scripted
|
|
6
|
+
// browser gives Chrome /tmp in that case; the browser profile stays where Playwright puts it.
|
|
7
|
+
import { constants } from "node:fs";
|
|
8
|
+
import { access } from "node:fs/promises";
|
|
9
|
+
/** `/org.chromium.Chromium.XXXXXX/SingletonSocket`: the longest path Chrome adds to `TMPDIR`. */
|
|
10
|
+
const SOCKET_SUFFIX_BYTES = 45;
|
|
11
|
+
/** The longest socket path, without its NUL: `sun_path` holds 104 bytes on macOS, 108 elsewhere. */
|
|
12
|
+
const SOCKET_PATH_MAX_BYTES = process.platform === "darwin" ? 103 : 107;
|
|
13
|
+
/** Where Chrome makes its socket directory when `TMPDIR` is unset. */
|
|
14
|
+
const DEFAULT_TMPDIR = "/tmp";
|
|
15
|
+
/** The byte length of `TMPDIR` when Chrome's singleton socket path under it would be too long. */
|
|
16
|
+
function overlongTmpdirBytes(env) {
|
|
17
|
+
const tmpdir = env.TMPDIR?.replace(/\/+$/, "");
|
|
18
|
+
if (tmpdir === undefined || tmpdir === "")
|
|
19
|
+
return undefined;
|
|
20
|
+
const bytes = Buffer.byteLength(tmpdir);
|
|
21
|
+
return bytes + SOCKET_SUFFIX_BYTES > SOCKET_PATH_MAX_BYTES ? bytes : undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The environment to launch Chrome with: `env` with `TMPDIR` set to /tmp when its own `TMPDIR` is
|
|
25
|
+
* too long for the singleton socket and /tmp is a writable directory. Undefined when Chrome can
|
|
26
|
+
* launch with `env` as it is, or when /tmp cannot be used; chromeLaunchError then names the cause.
|
|
27
|
+
*/
|
|
28
|
+
export async function chromeLaunchEnv(env = process.env) {
|
|
29
|
+
if (process.platform === "win32" || overlongTmpdirBytes(env) === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
try {
|
|
32
|
+
await access(DEFAULT_TMPDIR, constants.W_OK | constants.X_OK);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return { ...env, TMPDIR: DEFAULT_TMPDIR };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The launch error to report: one that names the cause and the fix when Chrome aborted because its
|
|
41
|
+
* singleton socket path was too long, and `error` itself otherwise. `env` is the environment
|
|
42
|
+
* Chrome was launched with.
|
|
43
|
+
*/
|
|
44
|
+
export function chromeLaunchError(error, env = process.env) {
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
if (!message.includes("Socket path too long"))
|
|
47
|
+
return error;
|
|
48
|
+
const bytes = overlongTmpdirBytes(env);
|
|
49
|
+
return new Error(`Chrome could not start: its singleton socket path under \`TMPDIR\`${bytes === undefined ? "" : ` (${bytes} bytes)`} is longer than the ${SOCKET_PATH_MAX_BYTES} bytes a Unix socket path allows. Set \`TMPDIR\` to a directory of at most ${SOCKET_PATH_MAX_BYTES - SOCKET_SUFFIX_BYTES} bytes, such as /tmp, and run again.`);
|
|
50
|
+
}
|
|
@@ -9,7 +9,8 @@ export interface AnalysisInput {
|
|
|
9
9
|
readonly deps?: Pick<StudyDeps, "analysis">;
|
|
10
10
|
/** Cancels the analysis only. */
|
|
11
11
|
readonly analysisSignal?: AbortSignal;
|
|
12
|
-
/** Receives analysis-started and analysis-finished
|
|
12
|
+
/** Receives analysis-started and analysis-finished, around the analysis of a run where a
|
|
13
|
+
* participant's session started. */
|
|
13
14
|
readonly emit?: (event: StudyEvent) => void;
|
|
14
15
|
}
|
|
15
16
|
export interface AutomaticAnalysisResult {
|
|
@@ -25,13 +25,26 @@ export async function completeAutomaticAnalysis(result, finished, config, input,
|
|
|
25
25
|
automaticAnalysis: { state: "skipped", reason: "AUTOMATIC_ANALYSIS_SOURCE_UNAVAILABLE" },
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
// A signal is stopping this process: the run is recorded interrupted, and the process exits
|
|
29
|
+
// once its sandboxes are reclaimed, so no analysis starts.
|
|
30
|
+
if (finished.interrupted)
|
|
31
|
+
return {
|
|
32
|
+
...result,
|
|
33
|
+
...origin,
|
|
34
|
+
automaticAnalysis: { state: "skipped", reason: "AUTOMATIC_ANALYSIS_ACTOR_CANCELLED" },
|
|
35
|
+
};
|
|
28
36
|
const prepared = finished.paths;
|
|
29
37
|
const seams = input?.deps?.analysis;
|
|
30
38
|
const signal = input?.analysisSignal;
|
|
31
39
|
let started = false;
|
|
32
40
|
try {
|
|
33
|
-
|
|
34
|
-
started
|
|
41
|
+
// The CLI prints "Participants finished" on analysis-started. A run where no participant's
|
|
42
|
+
// session started (it failed before, such as at E2B login) still records its analysis
|
|
43
|
+
// outcome, but without the two events.
|
|
44
|
+
if (finished.participantsRan) {
|
|
45
|
+
input?.emit?.({ type: "analysis-started" });
|
|
46
|
+
started = true;
|
|
47
|
+
}
|
|
35
48
|
try {
|
|
36
49
|
await validatePreparedRunRootIdentity(prepared);
|
|
37
50
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { migrateStudies } from "../../study/migrate.js";
|
|
1
|
+
import { migrateStudies, } from "../../study/migrate/migrate.js";
|
|
2
2
|
import { CWD_OPTION_DESCRIPTION, JSON_OPTION_DESCRIPTION, wantsJson, writeResult, } from "../io.js";
|
|
3
3
|
function formatValue(value) {
|
|
4
4
|
const text = JSON.stringify(value);
|
|
@@ -164,8 +164,6 @@ async function serveObserveUntilSignal(io, server, options) {
|
|
|
164
164
|
// Keep the JSON envelope on stdout clean: route attach/stop chatter to stderr
|
|
165
165
|
// for machine output, and to stdout for humans.
|
|
166
166
|
const note = options.json ? io.writeErr : io.writeOut;
|
|
167
|
-
note(`serving: ${server.url}\n`);
|
|
168
|
-
note("serving: press Ctrl-C to stop\n");
|
|
169
167
|
await new Promise((resolveWait) => {
|
|
170
168
|
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
171
169
|
const handlers = new Map();
|
|
@@ -195,6 +193,9 @@ async function serveObserveUntilSignal(io, server, options) {
|
|
|
195
193
|
handlers.set(signal, handler);
|
|
196
194
|
process.once(signal, handler);
|
|
197
195
|
}
|
|
196
|
+
// The prompt follows the handlers, as in followObserver.
|
|
197
|
+
note(`serving: ${server.url}\n`);
|
|
198
|
+
note("serving: press Ctrl-C to stop\n");
|
|
198
199
|
});
|
|
199
200
|
}
|
|
200
201
|
async function handleServe(io, options, command) {
|
|
@@ -217,9 +217,12 @@ function formatReclaimHuman(result, cwd, preflight) {
|
|
|
217
217
|
const search = result.tagSearch.status === "done"
|
|
218
218
|
? `E2B lists ${plural(result.tagSearch.found, "more sandbox", "more sandboxes")} tagged with it`
|
|
219
219
|
: `E2B tag search ${result.tagSearch.status}`;
|
|
220
|
+
const head = `${check ? "Reclaim check" : "Reclaim"} ${result.runId}: ${result.state}.`;
|
|
220
221
|
lines.push(result.reason === "dry-run"
|
|
221
|
-
? `${
|
|
222
|
-
:
|
|
222
|
+
? `${head} It was a dry run, which creates no sandboxes, so E2B was not contacted.`
|
|
223
|
+
: result.reason === "no-sandbox"
|
|
224
|
+
? `${head} Its status.json records that its route created no sandbox, so E2B was not contacted.`
|
|
225
|
+
: `${head} ${plural(result.receiptCount, "sandbox receipt")}; ${search}.`);
|
|
223
226
|
for (const outcome of result.outcomes) {
|
|
224
227
|
lines.push(` sandbox ${outcome.sandboxIdDigest} (${outcome.laneId}, from ${outcome.source}): ${outcome.state}${outcome.detail ? `: ${outcome.detail}` : ""}`);
|
|
225
228
|
}
|
|
@@ -13,6 +13,7 @@ import { writeRunFindings } from "../findings.js";
|
|
|
13
13
|
import { formatCuaStudyHuman } from "./study-format.js";
|
|
14
14
|
import { resolveRouteShouldOpen } from "./study-route-open.js";
|
|
15
15
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
16
|
+
import { actorOf, declaredParticipantCount } from "../../study/study-fields.js";
|
|
16
17
|
/**
|
|
17
18
|
* The computer-use route's CLI setup: its settings, the watch and exposure plan, the runStudyWith options
|
|
18
19
|
* with the live Observer hook, and how it presents the outcome or a run error. Undefined when
|
|
@@ -54,7 +55,7 @@ function refuseCua(args, dryRun, code, message) {
|
|
|
54
55
|
...studyResultIdentity("computer-use", args.config.id),
|
|
55
56
|
ok: false,
|
|
56
57
|
cwd: args.options.cwd,
|
|
57
|
-
actor: args.config
|
|
58
|
+
actor: actorOf(args.config)?.type ?? "",
|
|
58
59
|
appUrl: "",
|
|
59
60
|
dryRun,
|
|
60
61
|
runId: args.options.runId ?? "not-created",
|
|
@@ -79,7 +80,7 @@ function resolveCuaSettings(args) {
|
|
|
79
80
|
args.io.setExitCode(2);
|
|
80
81
|
return undefined;
|
|
81
82
|
}
|
|
82
|
-
const count = parseStudyCount(args.options.count, args.config
|
|
83
|
+
const count = parseStudyCount(args.options.count, declaredParticipantCount(args.config) ?? 1);
|
|
83
84
|
if (count === null) {
|
|
84
85
|
args.io.writeErr("error: --count must be a positive integer.\n");
|
|
85
86
|
args.io.setExitCode(2);
|
|
@@ -2,12 +2,13 @@ import { Command } from "commander";
|
|
|
2
2
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
3
3
|
import { formatRunHuman, parseStudyCount, writeResult, } from "../io.js";
|
|
4
4
|
import { planObserver, showObserver, staticObserverOpen } from "../observer-follow.js";
|
|
5
|
+
import { declaredParticipantCount } from "../../study/study-fields.js";
|
|
5
6
|
/**
|
|
6
7
|
* The preview route's CLI setup: the participant count and Observer plan, its runStudyWith options, and how it
|
|
7
8
|
* presents the outcome. Undefined when setup has already written its own result.
|
|
8
9
|
*/
|
|
9
10
|
export function previewRouteRun(args) {
|
|
10
|
-
const participantCount = parseStudyCount(args.options.count, args.config
|
|
11
|
+
const participantCount = parseStudyCount(args.options.count, declaredParticipantCount(args.config) ?? 4);
|
|
11
12
|
if (participantCount === null) {
|
|
12
13
|
const result = {
|
|
13
14
|
...studyResultIdentity("preview", args.config.id),
|
|
@@ -9,8 +9,8 @@ import { followDecision, followObserver, showObserver, staticObserverOpen, withO
|
|
|
9
9
|
import { writeRunFindings } from "../findings.js";
|
|
10
10
|
import { formatConcurrentSharedWorldStudyHuman } from "./study-format.js";
|
|
11
11
|
import { resolveRouteShouldOpen, watchFinishedPlan } from "./study-route-open.js";
|
|
12
|
-
import { rosterOf } from "../../study/parse/actors.js";
|
|
13
12
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
13
|
+
import { actorOf, participantList } from "../../study/study-fields.js";
|
|
14
14
|
/**
|
|
15
15
|
* The shared-world route's CLI setup: dry run, open and follow semantics, the live Observer server
|
|
16
16
|
* the run attaches, and how it presents the outcome. Undefined when setup has already written its
|
|
@@ -34,10 +34,10 @@ export function sharedWorldRouteRun(args) {
|
|
|
34
34
|
...studyResultIdentity("shared-world", args.config.id),
|
|
35
35
|
ok: false,
|
|
36
36
|
cwd: args.options.cwd,
|
|
37
|
-
actor: args.config
|
|
37
|
+
actor: actorOf(args.config)?.type ?? "",
|
|
38
38
|
topology: "shared-world",
|
|
39
39
|
topologyMode: "concurrent",
|
|
40
|
-
roleCount:
|
|
40
|
+
roleCount: participantList(args.config)?.length ?? 0,
|
|
41
41
|
concurrency: args.config.execution?.concurrency ?? 1,
|
|
42
42
|
dryRun,
|
|
43
43
|
runId: runId ?? args.options.runId ?? "not-created",
|
|
@@ -94,7 +94,7 @@ export async function runStudyCommand(args) {
|
|
|
94
94
|
// checks (keys, runtime auth, subject env, the local agent, caps) refuse this machine, all before
|
|
95
95
|
// the scorer loads, so none imports the scorer's host code. The scripted route's checks and the
|
|
96
96
|
// run-id claim still come after it; the scripted route takes no scorer.
|
|
97
|
-
const routeRun =
|
|
97
|
+
const routeRun = ROUTE_RUNS[route]({ ...args, config });
|
|
98
98
|
if (routeRun === undefined)
|
|
99
99
|
return;
|
|
100
100
|
// Every route's runStudyWith options carry the study's provenance, from here only.
|
|
@@ -155,24 +155,14 @@ function announcedKeyNames(config, options) {
|
|
|
155
155
|
const planned = planStudy(config, { cwd: options.cwd, dryRun: false });
|
|
156
156
|
return planned.ok ? keyNamesOf(planned.planned.plan) : undefined;
|
|
157
157
|
}
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return scriptedRouteRun(args);
|
|
167
|
-
case "terminal":
|
|
168
|
-
return terminalRouteRun(args);
|
|
169
|
-
case "shared-world":
|
|
170
|
-
return sharedWorldRouteRun(args);
|
|
171
|
-
default:
|
|
172
|
-
// Compile-time exhaustiveness: a future route must be handled here, not silently no-op.
|
|
173
|
-
throw new Error(`Unhandled study route: ${String(route)}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
158
|
+
/** Each route's CLI setup. It returns undefined when it has already written its own result. */
|
|
159
|
+
const ROUTE_RUNS = {
|
|
160
|
+
preview: previewRouteRun,
|
|
161
|
+
"computer-use": computerUseRouteRun,
|
|
162
|
+
scripted: scriptedRouteRun,
|
|
163
|
+
terminal: terminalRouteRun,
|
|
164
|
+
"shared-world": sharedWorldRouteRun,
|
|
165
|
+
};
|
|
176
166
|
function studyRerunFlagsRequested(options) {
|
|
177
167
|
return options.rerunFailedFrom !== undefined || options.participants !== undefined;
|
|
178
168
|
}
|
|
@@ -4,6 +4,7 @@ import { Command, Option } from "commander";
|
|
|
4
4
|
import { inspectStudyManifest, listStudyManifests } from "../../study/discover.js";
|
|
5
5
|
import { runStudyPreflight, } from "../../study/preflight.js";
|
|
6
6
|
import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, JSON_OPTION_DESCRIPTION, parsePositiveInteger, writeResult, humanError, } from "../io.js";
|
|
7
|
+
import { actorsOf } from "../../study/study-fields.js";
|
|
7
8
|
export function registerStudyCommands(parent, io) {
|
|
8
9
|
const study = parent
|
|
9
10
|
.command("study")
|
|
@@ -114,7 +115,9 @@ function formatStudyShowHuman(result) {
|
|
|
114
115
|
`id: ${config.id}`,
|
|
115
116
|
`subject: ${config.subject.source}`,
|
|
116
117
|
...(config.execution?.target ? [`execution: ${config.execution.target}`] : []),
|
|
117
|
-
`actors: ${config
|
|
118
|
+
`actors: ${actorsOf(config)
|
|
119
|
+
.map((actor) => actor.type)
|
|
120
|
+
.join(", ")}`,
|
|
118
121
|
...(config.title ? [`title: ${config.title}`] : []),
|
|
119
122
|
...(config.description ? [`description: ${config.description}`] : []),
|
|
120
123
|
...(result.path ? [`path: ${result.path}`] : []),
|
package/dist/cli/findings.d.ts
CHANGED
|
@@ -75,6 +75,11 @@ interface FindingsSource {
|
|
|
75
75
|
loaded: LoadedAnalysis;
|
|
76
76
|
/** ` --cwd <dir>` when the commands this view names need it, or "". */
|
|
77
77
|
cwdFlag: string;
|
|
78
|
+
/**
|
|
79
|
+
* The project-relative path of the run's study file when that file sets `review.analysis:
|
|
80
|
+
* false`, read when the view is built. Undefined when it does not, or cannot be read.
|
|
81
|
+
*/
|
|
82
|
+
analysisOffIn?: string;
|
|
78
83
|
}
|
|
79
84
|
/** The findings view of a run's selected analysis, or why it has none. */
|
|
80
85
|
export declare function analysisFindings(source: FindingsSource): AnalysisFindings;
|
package/dist/cli/findings.js
CHANGED
|
@@ -7,6 +7,8 @@ import { loadAnalysis } from "../analysis/load.js";
|
|
|
7
7
|
import { projectShareCheckedAnalysis } from "../analysis/sharing.js";
|
|
8
8
|
import { loadRunBundlePrepared, resolveRunPath } from "../run/locate.js";
|
|
9
9
|
import { resolvePhysicalCwd, runIdOf } from "../run/paths.js";
|
|
10
|
+
import { studyProvenanceOf } from "../run/study-provenance.js";
|
|
11
|
+
import { resolveStudyManifest } from "../study/discover.js";
|
|
10
12
|
import { plural } from "../run/text.js";
|
|
11
13
|
import { shellQuote } from "../substrates/shell.js";
|
|
12
14
|
import { analysisOutcomeText, wantsJson } from "./io.js";
|
|
@@ -151,8 +153,23 @@ export function analysisFindings(source) {
|
|
|
151
153
|
}
|
|
152
154
|
if (job !== undefined || loaded.state === "invalid")
|
|
153
155
|
return withoutFindings(source, "unavailable", job?.reason ?? loaded.warnings[0] ?? null, "This run's analysis records could not be read or checked, so no findings are shown.", cli(`analyze list --run ${source.runId}${source.cwdFlag}`));
|
|
156
|
+
if (source.analysisOffIn !== undefined)
|
|
157
|
+
return withoutFindings(source, "skipped", "AUTOMATIC_ANALYSIS_DISABLED", `Analysis is turned off in this run's study (review.analysis: false in ${source.analysisOffIn}), so none ran after the run. To analyze this run anyway, run the next command.`, analyzeCommand(source, undefined));
|
|
154
158
|
return withoutFindings(source, "none", null, "No analysis has run for this run.", analyzeCommand(source, undefined));
|
|
155
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* The run's study file, project-relative, when it still names the same study and sets
|
|
162
|
+
* `review.analysis: false`. A study that turns analysis off leaves no analysis record, so the file
|
|
163
|
+
* is the only place that says why a live run has none.
|
|
164
|
+
*/
|
|
165
|
+
async function studyAnalysisOff(cwd, study) {
|
|
166
|
+
if (study?.path === undefined)
|
|
167
|
+
return undefined;
|
|
168
|
+
const resolved = await resolveStudyManifest(cwd, study.path).catch(() => null);
|
|
169
|
+
if (!resolved?.ok || resolved.config.id !== study.id)
|
|
170
|
+
return undefined;
|
|
171
|
+
return resolved.config.review?.analysis === false ? study.path : undefined;
|
|
172
|
+
}
|
|
156
173
|
/**
|
|
157
174
|
* The run's bundle mode and its selected analysis: `analysisId`, or the version `analyze show`
|
|
158
175
|
* defaults to. Null when the run is missing or its storage is unsafe.
|
|
@@ -163,12 +180,16 @@ export async function readRunAnalysis(cwd, run, analysisId) {
|
|
|
163
180
|
if (!prepared)
|
|
164
181
|
return null;
|
|
165
182
|
const bundle = await loadRunBundlePrepared(physical, prepared).catch(() => null);
|
|
183
|
+
const analysisOffIn = bundle?.bundle.mode === "live"
|
|
184
|
+
? await studyAnalysisOff(physical, studyProvenanceOf(bundle.bundle))
|
|
185
|
+
: undefined;
|
|
166
186
|
return {
|
|
167
187
|
runId: runIdOf(prepared),
|
|
168
188
|
mode: bundle?.bundle.mode,
|
|
169
189
|
runRoot: prepared.relativeRunRoot,
|
|
170
190
|
loaded: await loadAnalysis(prepared, analysisId),
|
|
171
191
|
cwdFlag: cwdFlag(cwd),
|
|
192
|
+
...(analysisOffIn === undefined ? {} : { analysisOffIn }),
|
|
172
193
|
};
|
|
173
194
|
}
|
|
174
195
|
/** The view of a run that could not be found. */
|
package/dist/cli/io.d.ts
CHANGED
|
@@ -17,7 +17,8 @@ export declare const DOTENV_OPTION_DESCRIPTION = "Load unset variables from this
|
|
|
17
17
|
/**
|
|
18
18
|
* The hidden `--env-file <path>` that `--dotenv` replaced. Node scans the whole argv for
|
|
19
19
|
* `--env-file` before a script runs, and stops only at `--` (src/node_dotenv.cc), so a missing file
|
|
20
|
-
* exits 9 through Node and never reaches humanish. `--dotenv` does not collide.
|
|
20
|
+
* exits 9 through Node and never reaches humanish. `--dotenv` does not collide.
|
|
21
|
+
* Removed in 0.112.0: deprecated in 0.110.0 on 2026-10-03, it stays 30 days and one minor release.
|
|
21
22
|
*/
|
|
22
23
|
export declare function envFileAliasOption(): Option;
|
|
23
24
|
/** The dotenv path a command was given: `--dotenv`, or the `--env-file` alias with its warning. */
|
package/dist/cli/io.js
CHANGED
|
@@ -15,11 +15,12 @@ export const JSON_OPTION_DESCRIPTION = "Print a machine-readable JSON response."
|
|
|
15
15
|
export const CWD_OPTION_DESCRIPTION = "Project directory.";
|
|
16
16
|
export const RUN_OPTION_DESCRIPTION = "Run id, or latest.";
|
|
17
17
|
export const DOTENV_OPTION_DESCRIPTION = "Load unset variables from this dotenv file. Values are never printed or saved.";
|
|
18
|
-
const ENV_FILE_RENAMED = "`--env-file` is now `--dotenv`; `--env-file` is removed in 0.
|
|
18
|
+
const ENV_FILE_RENAMED = "`--env-file` is now `--dotenv`; `--env-file` is removed in 0.112.0.";
|
|
19
19
|
/**
|
|
20
20
|
* The hidden `--env-file <path>` that `--dotenv` replaced. Node scans the whole argv for
|
|
21
21
|
* `--env-file` before a script runs, and stops only at `--` (src/node_dotenv.cc), so a missing file
|
|
22
|
-
* exits 9 through Node and never reaches humanish. `--dotenv` does not collide.
|
|
22
|
+
* exits 9 through Node and never reaches humanish. `--dotenv` does not collide.
|
|
23
|
+
* Removed in 0.112.0: deprecated in 0.110.0 on 2026-10-03, it stays 30 days and one minor release.
|
|
23
24
|
*/
|
|
24
25
|
export function envFileAliasOption() {
|
|
25
26
|
return new Option("--env-file <path>").hideHelp().conflicts("dotenv");
|
|
@@ -26,8 +26,12 @@ export declare function followDecision(args: {
|
|
|
26
26
|
follow: boolean;
|
|
27
27
|
unattended: boolean;
|
|
28
28
|
};
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The warning a command adds when it printed the Observer and exited because nobody was attached.
|
|
31
|
+
* It names the reason personAtTerminal found: an agent runner's marker, which can sit beside a real
|
|
32
|
+
* terminal, or no terminal at all.
|
|
33
|
+
*/
|
|
34
|
+
export declare function unattendedObserverWarning(command: "watch" | "observe", env?: NodeJS.ProcessEnv): string;
|
|
31
35
|
/**
|
|
32
36
|
* Parse --port and decide open and follow. On an invalid port it writes the failure, sets exit
|
|
33
37
|
* code 2 and returns null, so a caller refuses before starting a run.
|
|
@@ -23,9 +23,17 @@ export function followDecision(args) {
|
|
|
23
23
|
const attended = personAtTerminal();
|
|
24
24
|
return { follow: attended, unattended: !attended };
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The warning a command adds when it printed the Observer and exited because nobody was attached.
|
|
28
|
+
* It names the reason personAtTerminal found: an agent runner's marker, which can sit beside a real
|
|
29
|
+
* terminal, or no terminal at all.
|
|
30
|
+
*/
|
|
31
|
+
export function unattendedObserverWarning(command, env = process.env) {
|
|
32
|
+
const agent = detectAgentSession(env);
|
|
33
|
+
const reason = agent === undefined
|
|
34
|
+
? "No interactive terminal"
|
|
35
|
+
: `An agent environment was detected (${agent.marker} is set, which ${agent.runner} sets)`;
|
|
36
|
+
return `${reason}, so ${command} printed the Observer path and exited instead of serving it until Ctrl-C. Pass --serve to keep serving, or open the file.`;
|
|
29
37
|
}
|
|
30
38
|
/**
|
|
31
39
|
* Parse --port and decide open and follow. On an invalid port it writes the failure, sets exit
|
|
@@ -112,8 +120,6 @@ export function formatObserverHuman(result) {
|
|
|
112
120
|
].join("\n") + "\n");
|
|
113
121
|
}
|
|
114
122
|
export async function followObserver(io, result, server, options = {}) {
|
|
115
|
-
io.writeOut(`watching: ${result.serverUrl ?? result.observerUrl ?? result.observerPath}\n`);
|
|
116
|
-
io.writeOut("watching: press Ctrl-C to stop\n");
|
|
117
123
|
await new Promise((resolve) => {
|
|
118
124
|
const signalTarget = options.signalTarget ?? process;
|
|
119
125
|
const signals = options.signals ?? ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
@@ -155,6 +161,10 @@ export async function followObserver(io, result, server, options = {}) {
|
|
|
155
161
|
handlers.set(signal, handler);
|
|
156
162
|
signalTarget.once(signal, handler);
|
|
157
163
|
}
|
|
164
|
+
// Print the prompt only once the handlers are in place: a Ctrl-C that arrives sooner would
|
|
165
|
+
// take Node's default action and end the process without closing the server.
|
|
166
|
+
io.writeOut(`watching: ${result.serverUrl ?? result.observerUrl ?? result.observerPath}\n`);
|
|
167
|
+
io.writeOut("watching: press Ctrl-C to stop\n");
|
|
158
168
|
});
|
|
159
169
|
}
|
|
160
170
|
export function exitCodeForSignal(signal) {
|