@rehearsal-db/core 0.1.0-rc.9 → 1.0.1

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 (37) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/COMPATIBILITY.md +56 -8
  3. package/README.md +31 -12
  4. package/SECURITY.md +5 -3
  5. package/SUPPORT.md +4 -3
  6. package/dist/package.json +3 -3
  7. package/dist/scripts/install/create_root_config.mjs +9 -2
  8. package/dist/src/application/interactive_task.d.mts +2 -6
  9. package/dist/src/application/interactive_task.mjs +4 -13
  10. package/dist/src/application/session.d.mts +5 -8
  11. package/dist/src/application/session.mjs +44 -19
  12. package/dist/src/cli/arguments.mjs +8 -1
  13. package/dist/src/cli/rehearsal.mjs +27 -5
  14. package/dist/src/cli/runtime_commands.mjs +16 -17
  15. package/dist/src/cli/runtime_planning.mjs +6 -0
  16. package/dist/src/cli/terminal.mjs +24 -5
  17. package/dist/src/project/install_scaffold.mjs +7 -0
  18. package/dist/src/project/project_discovery_metadata.mjs +9 -11
  19. package/dist/src/runtime/persistence.mjs +6 -3
  20. package/dist/src/runtime/plan.mjs +26 -15
  21. package/dist/src/shared/cancellation.d.mts +3 -1
  22. package/dist/src/shared/cancellation.mjs +12 -4
  23. package/dist/src/shared/host_platform.d.mts +16 -0
  24. package/dist/src/shared/host_platform.mjs +109 -0
  25. package/dist/src/shared/process_environment.d.mts +2 -1
  26. package/dist/src/shared/process_environment.mjs +47 -5
  27. package/dist/src/shared/terminal_input.d.mts +25 -0
  28. package/dist/src/shared/terminal_input.mjs +129 -0
  29. package/docs/commands.md +17 -1
  30. package/docs/getting-started.md +48 -0
  31. package/docs/index.md +15 -9
  32. package/docs/releasing.md +50 -24
  33. package/docs/roadmap.md +8 -3
  34. package/docs/stability.md +48 -19
  35. package/docs/tutorial.md +1 -1
  36. package/docs/vscode.md +199 -41
  37. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -5,6 +5,50 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ### Platform compatibility
9
+
10
+ - Keep macOS Intel/Apple Silicon and Linux execution distinct from Windows desktop
11
+ access through WSL 2. Require the WSL project's Linux filesystem; document the
12
+ preview's actual component and runtime limits rather than claiming every OS works.
13
+ - Verify installed package version/discovery on real desktop OS runners and exercise
14
+ PostgreSQL/dependent targets in an owned Windows 2025 WSL2 Ubuntu distribution.
15
+ Provision checksum-verified official Node and signed Docker Engine packages without
16
+ Docker Desktop, a daemon TCP endpoint or new branch-protection requirements.
17
+ - Document native Windows runtime refusal and the Linux/Remote-WSL setup path; version,
18
+ help and project discovery remain distinct from database-runtime support.
19
+
20
+ ## [1.0.0] - 2026-10-17
21
+
22
+ ### Stable compatibility
23
+
24
+ - Establish the documented CLI, configuration, policy, baseline, TypeScript and
25
+ JSON contracts as the 1.x compatibility boundary. This preserves rc.10's database,
26
+ authentication and restore behavior.
27
+ - Keep the complete MIT CLI free. The separately distributed Free Starter editor,
28
+ future Pro plans and additional database families do not expand the core support
29
+ matrix or change its safety boundaries.
30
+
31
+ ### Fixed
32
+
33
+ - Report unknown CLI commands and options as invalid input (exit 2), rather than
34
+ letting words in the input or help text misclassify them as migration or baseline
35
+ failures. The command remains refused and supported-command help is retained.
36
+
37
+ ## [0.1.0-rc.10] - 2026-10-17
38
+
39
+ ### Fixed
40
+
41
+ - Interactive Ctrl+C and Ctrl+Z retire owned work without leaving an invoking npm
42
+ job suspended. Terminal mode and input consumption are restored after cleanup;
43
+ startup and unanswered migration confirmations cancel safely, and unrelated jobs
44
+ are untouched.
45
+
46
+ ### Documentation
47
+
48
+ - Stable promotion is evidence-based after owner review: complete the release and
49
+ consumer acceptance checks rather than waiting a fixed number of calendar days.
50
+ Exact-artifact, security, data-preservation and human publishing approvals remain.
51
+
8
52
  ## [0.1.0-rc.9] - 2026-10-17
9
53
 
10
54
  ### Fixed
@@ -740,7 +784,14 @@ not unattended cleanup, and container age is not proof of user inactivity.
740
784
  publication uses short-lived trusted OIDC, and every release tag must already exist on
741
785
  protected `main`.
742
786
 
743
- [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.4...HEAD
787
+ [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.0...HEAD
788
+ [1.0.0]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.10...v1.0.0
789
+ [0.1.0-rc.10]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.9...v0.1.0-rc.10
790
+ [0.1.0-rc.9]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.8...v0.1.0-rc.9
791
+ [0.1.0-rc.8]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.7...v0.1.0-rc.8
792
+ [0.1.0-rc.7]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.6...v0.1.0-rc.7
793
+ [0.1.0-rc.6]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.5...v0.1.0-rc.6
794
+ [0.1.0-rc.5]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.4...v0.1.0-rc.5
744
795
  [0.1.0-rc.4]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.3...v0.1.0-rc.4
745
796
  [0.1.0-rc.3]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.2...v0.1.0-rc.3
746
797
  [0.1.0-rc.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.1...v0.1.0-rc.2
package/COMPATIBILITY.md CHANGED
@@ -1,10 +1,15 @@
1
1
  # Rehearsal compatibility policy
2
2
 
3
- Rehearsal begins at `0.x`. Minor releases may contain breaking changes, but every
4
- intentional break must be called out in the changelog with a migration path. Patch
5
- releases must remain backward compatible within their minor line.
3
+ Starting with `1.0.0`, documented public contracts are backward compatible throughout
4
+ the `1.x` line. Minor releases may add compatible capabilities; patch releases fix
5
+ defects. An intentional breaking change requires a new major version, a changelog
6
+ entry and a migration path.
6
7
 
7
- The following are compatibility contracts for published 0.x releases:
8
+ Historical `0.x` releases allowed intentional breaking changes in minor versions,
9
+ with changelog and migration guidance. Their patch releases remain compatible within
10
+ their minor line. Existing artifacts and granted license rights are unchanged.
11
+
12
+ The following are compatibility contracts for published releases:
8
13
 
9
14
  - configuration property names, types, defaults, validation, and schema version;
10
15
  - baseline format and checksum meaning;
@@ -17,16 +22,59 @@ A change to any of those requires either a compatible extension or a deliberate
17
22
  transition. Rehearsal must never silently reinterpret an old config, baseline, digest,
18
23
  or successful verification receipt.
19
24
 
20
- The first `1.0.0` release requires external beta evidence, a support policy, and a settled
21
- public API. Supporting additional database families, package managers, or operating
22
- systems is not implied by the 0.x contract.
25
+ The first `1.0.0` release requires external beta evidence, a support policy and the
26
+ settled public API described in [the stability policy](docs/stability.md). Supporting
27
+ additional database families, package managers or operating systems is not implied.
23
28
 
24
29
  Rehearsal supports maintained even-numbered Node.js release lines that pass its CI
25
- matrix. Beta.13 supports Node.js 22, 24, and 26; Node.js 24 LTS is the recommended
30
+ matrix. Rehearsal 1.0 supports Node.js 22, 24, and 26; Node.js 24 LTS is the recommended
26
31
  default and the release-build runtime. End-of-life and odd-numbered Node.js releases are
27
32
  not supported. A future release may remove a Node.js line after its upstream end-of-life
28
33
  date, with that change called out in the changelog.
29
34
 
35
+ ## Operating systems and execution location
36
+
37
+ Core 1.0.1's platform boundary distinguishes the computer running VS Code from the
38
+ operating system actually executing Node, Docker and Rehearsal. The corresponding editor
39
+ changes are delivered with extension 0.1.21. A Windows desktop connected to WSL is not native
40
+ Windows execution.
41
+
42
+ | Execution environment | Runtime boundary |
43
+ | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
44
+ | macOS, Intel or Apple Silicon | POSIX runtime; supported Node and a local Docker-compatible engine required |
45
+ | Linux | POSIX runtime; Ubuntu 24.04 is the primary PostgreSQL/Supabase fixture environment |
46
+ | Windows with WSL 2 | Run Linux Node and Docker inside the distribution, with the project on its Linux filesystem; compatibility preview |
47
+ | Native Windows Node, PowerShell or Command Prompt | Version, help and read-only project discovery remain available; runtime/setup/database operations refuse with WSL guidance |
48
+ | WSL 1, a WSL project on `/mnt/c` or another Windows-mounted filesystem | Not a supported database-runtime environment |
49
+ | SSH, Dev Containers, Codespaces or browser-only VS Code | No end-to-end support claim; different host, filesystem and port-forwarding boundaries need their own verification |
50
+ | iOS, iPadOS, Android, BSD and other unlisted systems | Not supported; installing a JavaScript package does not establish a compatible database engine or desktop extension host |
51
+
52
+ Do not bypass native Windows refusal with permission bits or a shell wrapper. Native
53
+ Windows ACLs, process-tree retirement and baseline filesystem behavior are not certified
54
+ by Linux tests. Keep keys, baselines and working files in the WSL Linux home filesystem,
55
+ not a Windows share. The package does not move an existing project automatically.
56
+ See [Windows and WSL setup](docs/getting-started.md#windows-use-a-wsl-2-linux-workspace).
57
+
58
+ ### What platform verification proves
59
+
60
+ The separate `Platform compatibility` workflow preserves existing release/check names.
61
+ Desktop jobs install the packed package and exercise version/discovery from a path with
62
+ spaces and Unicode on real Linux, macOS and Windows runners. Windows also checks the
63
+ intentional runtime refusal; a passing Windows job is not a working Windows database.
64
+ The Windows 2025 WSL job provisions its own Ubuntu 24.04 distribution, verifies Linux
65
+ execution and filesystem location, then runs the existing real PostgreSQL/dependent-target
66
+ fixtures against a local Unix-socket Docker Engine. It does not install Docker Desktop
67
+ or expose the daemon over TCP. Node uses the official release checksum; Docker packages
68
+ use its signed apt repository. These integrity checks are not independent publisher
69
+ authentication.
70
+
71
+ A configured workflow is not passing evidence. Reports identify actual OS, architecture,
72
+ installed artifact and exercised boundary. The WSL fixture does not certify native
73
+ Windows runtime, WSL Supabase/Auth/Storage, a native Remote-WSL VS Code session, all Linux
74
+ distributions, assistive technology or every architecture. Editor Node/controller tests
75
+ are separate from real extension-host and browser walkthroughs. Intel/ARM labels and
76
+ Windows/WSL are never interchangeable evidence.
77
+
30
78
  The npm install hook may create project scaffolding only when it proves one application
31
79
  root. Ordinary npm application installs are covered. A workspace is automatic only when
32
80
  one declaring consumer is already visible in its manifest or lockfile; npm can save a
package/README.md CHANGED
@@ -16,6 +16,7 @@
16
16
  <p align="center">
17
17
  <a href="https://ddupasquier.github.io/rehearsal-db/">Read the documentation</a> ·
18
18
  <a href="https://www.npmjs.com/package/@rehearsal-db/core">View on npm</a> ·
19
+ <a href="https://ddupasquier.github.io/rehearsal-db/vscode/">Free VS Code Starter</a> ·
19
20
  <a href="https://github.com/Ddupasquier/rehearsal-db">Source on GitHub</a> ·
20
21
  <a href="https://github.com/Ddupasquier/rehearsal-db/issues">Report a problem</a>
21
22
  </p>
@@ -45,8 +46,14 @@ it through a reviewed privacy policy, and retire the access again. It is never p
45
46
  `run`, `reset`, `migrate`, or application launch. Rehearsal is not a backup system or a
46
47
  production deployment tool.
47
48
 
48
- > **Pre-1.0 safety note:** use Rehearsal on a branch and keep a working backup of your
49
- > project.
49
+ The CLI is free and MIT-licensed, with stable releases on
50
+ [npm](https://www.npmjs.com/package/@rehearsal-db/core). This guide targets **1.0.1**;
51
+ install only a version actually published there. Prefer a visual workflow?
52
+ [Install the free VS Code Starter preview](docs/vscode.md). Its included features are
53
+ free indefinitely for personal or commercial development—no account, card, or trial.
54
+
55
+ Work on a branch and keep a working backup of your project. Rehearsal's local safety
56
+ boundaries do not replace version control or backups.
50
57
 
51
58
  ## What you need
52
59
 
@@ -62,6 +69,12 @@ your migration is intended to change.
62
69
 
63
70
  ## Quick start
64
71
 
72
+ Prefer OS-specific files? Use the [Windows / WSL 2](installers/windows/README.md),
73
+ [macOS](installers/macos/README.md) or [Linux](installers/linux/README.md) setup folder.
74
+ These readable helpers require existing Node.js 24 or 26 and check prerequisites by
75
+ default; installation is an explicit option. They do not install OS tools or start a
76
+ database. See [their exact behavior and release availability](installers/README.md).
77
+
65
78
  From your project directory:
66
79
 
67
80
  ```bash
@@ -157,18 +170,24 @@ loads.
157
170
 
158
171
  ## Supported today
159
172
 
160
- | Environment | Support |
161
- | ------------------------------------------------ | ---------------------- |
162
- | Supabase CLI projects | Supported |
163
- | Ordinary PostgreSQL in local Docker | Supported |
164
- | macOS and Linux | Supported |
165
- | WSL | Experimental |
166
- | Native Windows | Not yet supported |
167
- | Hosted database URLs | Intentionally rejected |
168
- | Optional read-only PostgreSQL source preparation | Explicit opt-in beta |
169
- | MySQL, MongoDB, and other database families | Not yet supported |
173
+ | Environment | Support |
174
+ | ------------------------------------------------ | ---------------------------- |
175
+ | Supabase CLI projects | Supported |
176
+ | Ordinary PostgreSQL in local Docker | Supported |
177
+ | macOS and Linux | Supported |
178
+ | Windows via WSL 2 on its Linux filesystem | Compatibility preview |
179
+ | Native Windows runtime | Not yet supported; use WSL 2 |
180
+ | Hosted database URLs | Intentionally rejected |
181
+ | Optional read-only PostgreSQL source preparation | Explicit opt-in beta |
182
+ | MySQL, MongoDB, and other database families | Not yet supported |
170
183
 
171
184
  See [COMPATIBILITY.md](COMPATIBILITY.md) for the exact support contract.
185
+ Native Windows metadata commands are not database-runtime support. For Windows, follow
186
+ the [WSL 2 setup path](docs/getting-started.md#windows-use-a-wsl-2-linux-workspace):
187
+ Linux Node, project files and Docker must live in the Linux execution environment.
188
+ Core 1.0.1 and editor 0.1.21 add explicit platform guidance/refusal; older immutable
189
+ releases retain their original behavior. Mobile, browser-only and unlisted systems are
190
+ not implied by desktop or Linux compatibility.
172
191
 
173
192
  ## Documentation
174
193
 
package/SECURITY.md CHANGED
@@ -9,9 +9,11 @@ Report suspected vulnerabilities through the
9
9
  in this repository. A GitHub account is required. Do not open a
10
10
  public issue containing an exploit, secret, hosted target, or source record.
11
11
 
12
- Supported security updates cover the latest released 0.x minor during beta. A security
13
- fix may deliberately tighten configuration or refuse a workflow that an earlier beta
14
- accepted. During beta, reports apply to the latest published release and current `main`.
12
+ Security updates target the latest released 1.x version. Update to the latest 1.x
13
+ release to receive fixes; older release candidates are not a maintained security line.
14
+ Reports concerning current `main` and prereleases are also welcome. A security fix may
15
+ tighten validation or refuse an unsafe workflow; it must not silently reinterpret
16
+ existing data or claim that a refused operation succeeded.
15
17
 
16
18
  The threat boundary and remaining responsibilities live in
17
19
  [docs/security-model.md](docs/security-model.md). Every
package/SUPPORT.md CHANGED
@@ -3,8 +3,7 @@
3
3
  Use GitHub Issues for reproducible defects and setup problems that include a synthetic
4
4
  reproduction. Include operating system, architecture, Node version, Supabase CLI version,
5
5
  Docker engine, the failing command, safe diagnostics, and the smallest reproduction.
6
- The first beta does not yet maintain a separate public discussion or feature-request
7
- channel.
6
+ Search existing reports before opening a new issue.
8
7
 
9
8
  Run `npx rehearsal support` or choose **Get help** in the guide to collect the environment
10
9
  and readiness portion without assembling it by hand. The report intentionally excludes
@@ -15,6 +14,8 @@ Do not include credentials, connection strings, source rows, baseline artifacts,
15
14
  URLs, or proprietary migrations. Report security problems through private vulnerability
16
15
  reporting as described in [SECURITY.md](SECURITY.md).
17
16
 
18
- The package is pre-release. The supported matrix is defined in
17
+ The supported matrix and 1.x compatibility boundary are defined in
19
18
  [COMPATIBILITY.md](COMPATIBILITY.md); unsupported environments are welcome as clearly
20
19
  labelled experiments but cannot be treated as verified behavior.
20
+
21
+ Support is community-based; no response-time guarantee or production SLA is offered.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "0.1.0-rc.9",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "description": "Catch PostgreSQL and Supabase migrations that pass on empty databases but fail on real data—before deploy.",
6
6
  "repository": {
@@ -76,9 +76,9 @@
76
76
  "test:fixture:standalone": "npm run build && node scripts/verification/run_scenario.mjs standalone",
77
77
  "test:fixture:privacy": "npm run build && node scripts/verification/run_scenario.mjs privacy",
78
78
  "test:fixture:resources": "npm run build && node scripts/verification/run_scenario.mjs resources",
79
- "release:verify": "node scripts/verification/release_gate.mjs",
79
+ "release:verify": "npm run build && node scripts/verification/release_gate.mjs",
80
80
  "check": "npm run check:syntax && npm run typecheck && npm run test && npm run docs:build && npm run format:check && npm run package:audit",
81
- "check:syntax": "find scripts tests -type f -name '*.mjs' -exec node --check {} +",
81
+ "check:syntax": "node scripts/verification/check_syntax.mjs",
82
82
  "typecheck": "npm run build && tsc --project tsconfig.json",
83
83
  "format": "prettier --write .",
84
84
  "format:check": "prettier --check .",
@@ -2,10 +2,17 @@
2
2
  /** Create safe project-root Rehearsal scaffolding during a local install. */
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { createInstalledRehearsalScaffold, renderInstalledRehearsalScaffold, } from "../../src/project/install_scaffold.mjs";
5
+ import { inspectHostPlatform } from "../../src/shared/host_platform.mjs";
5
6
  const packageRoot = fileURLToPath(new URL("../../..", import.meta.url));
6
7
  try {
7
- const result = await createInstalledRehearsalScaffold({ packageRoot });
8
- console.log(renderInstalledRehearsalScaffold(result));
8
+ const host = await inspectHostPlatform();
9
+ if (!host.supported) {
10
+ console.log(`Rehearsal installed without creating project files. ${host.remediation}`);
11
+ }
12
+ else {
13
+ const result = await createInstalledRehearsalScaffold({ packageRoot });
14
+ console.log(renderInstalledRehearsalScaffold(result));
15
+ }
9
16
  }
10
17
  catch (error) {
11
18
  const message = error instanceof Error ? error.message : String(error);
@@ -1,12 +1,8 @@
1
1
  /** Run identity discovery while an owned local application remains available. */
2
2
  import type { EventEmitter } from "node:events";
3
+ import { type TerminalInput } from "../shared/terminal_input.mjs";
3
4
  import { type ApplicationDiagnostics, type ApplicationSession, type ApplicationSessionSignal } from "./session.mjs";
4
5
  type SignalTarget = Pick<EventEmitter, "prependListener" | "removeListener">;
5
- interface SessionInput {
6
- readonly isTTY?: boolean;
7
- prependListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
8
- removeListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
9
- }
10
6
  /**
11
7
  * A terminal interruption aborts the task and retires the owned process group.
12
8
  * A completed task leaves the session running so its caller can deliberately
@@ -16,7 +12,7 @@ export declare const runApplicationSessionTask: <Value>({ session, task, signalT
16
12
  session: ApplicationSession;
17
13
  task: (signal: AbortSignal) => Promise<Value>;
18
14
  signalTarget?: SignalTarget;
19
- input?: SessionInput;
15
+ input?: TerminalInput | null;
20
16
  onReady?: (ready: ApplicationSession["ready"]) => void;
21
17
  }) => Promise<Readonly<{
22
18
  completed: true;
@@ -1,4 +1,5 @@
1
1
  /** Run identity discovery while an owned local application remains available. */
2
+ import { acquireTerminalInput, } from "../shared/terminal_input.mjs";
2
3
  import { summarizeProjectCommandFailure, } from "./session.mjs";
3
4
  const SESSION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGTSTP"];
4
5
  /**
@@ -23,19 +24,10 @@ export const runApplicationSessionTask = async ({ session, task, signalTarget =
23
24
  for (const [signal, handler] of handlers) {
24
25
  signalTarget.prependListener(signal, handler);
25
26
  }
26
- const onInput = (chunk) => {
27
- const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
28
- if (bytes.includes(0x03)) {
29
- finishInterruption({ kind: "interrupted", signal: "SIGINT" });
30
- }
31
- else if (bytes.includes(0x1a)) {
32
- finishInterruption({ kind: "interrupted", signal: "SIGTSTP" });
33
- }
34
- };
35
- if (input?.isTTY)
36
- input.prependListener("data", onInput);
27
+ let releaseInput = () => undefined;
37
28
  const abortController = new AbortController();
38
29
  try {
30
+ releaseInput = acquireTerminalInput(input, (signal) => finishInterruption({ kind: "interrupted", signal }));
39
31
  onReady(session.ready);
40
32
  const outcome = await Promise.race([
41
33
  interruption,
@@ -77,7 +69,6 @@ export const runApplicationSessionTask = async ({ session, task, signalTarget =
77
69
  for (const [signal, handler] of handlers) {
78
70
  signalTarget.removeListener(signal, handler);
79
71
  }
80
- if (input?.isTTY)
81
- input.removeListener("data", onInput);
72
+ releaseInput();
82
73
  }
83
74
  };
@@ -3,7 +3,8 @@ import { spawn } from "node:child_process";
3
3
  import type { EventEmitter } from "node:events";
4
4
  import { waitForApplicationIdle } from "./activity_monitor.mjs";
5
5
  import type { ApplicationActivity } from "./activity_contract.mjs";
6
- declare const SESSION_SIGNALS: string[];
6
+ import { type TerminalInput } from "../shared/terminal_input.mjs";
7
+ declare const SESSION_SIGNALS: readonly ["SIGINT", "SIGTERM", "SIGHUP", "SIGTSTP"];
7
8
  type SessionSignal = (typeof SESSION_SIGNALS)[number];
8
9
  export type ApplicationSessionSignal = SessionSignal;
9
10
  export interface ApplicationReadiness {
@@ -29,11 +30,6 @@ interface ChildExit {
29
30
  readonly status: number | null;
30
31
  readonly signal: NodeJS.Signals | null;
31
32
  }
32
- interface SessionInput {
33
- readonly isTTY?: boolean;
34
- prependListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
35
- removeListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
36
- }
37
33
  interface EnvironmentInput {
38
34
  readonly files: Readonly<Record<string, string>> & {
39
35
  readonly primary: string;
@@ -66,7 +62,7 @@ export declare const summarizeProjectCommandFailure: ({ error, status, signal, s
66
62
  stdoutByteCount?: number;
67
63
  stderrByteCount?: number;
68
64
  }) => string;
69
- export declare const startApplicationSession: ({ command, cwd, files, mappings, readiness, inheritedEnvironment, spawnImplementation, fetchImplementation, signalTarget, portPreflight, }: EnvironmentInput & {
65
+ export declare const startApplicationSession: ({ command, cwd, files, mappings, readiness, inheritedEnvironment, spawnImplementation, fetchImplementation, signalTarget, input, portPreflight, }: EnvironmentInput & {
70
66
  command: string;
71
67
  cwd: string;
72
68
  readiness: ApplicationReadiness;
@@ -74,6 +70,7 @@ export declare const startApplicationSession: ({ command, cwd, files, mappings,
74
70
  spawnImplementation?: typeof spawn;
75
71
  fetchImplementation?: typeof fetch;
76
72
  signalTarget?: SignalTarget;
73
+ input?: TerminalInput | null;
77
74
  portPreflight?: (readiness: ApplicationReadiness) => Promise<void>;
78
75
  }) => Promise<ApplicationSession>;
79
76
  /**
@@ -83,7 +80,7 @@ export declare const startApplicationSession: ({ command, cwd, files, mappings,
83
80
  export declare const holdApplicationSession: ({ session, signalTarget, input, onReady, activity, idleTimeoutSeconds, idleMonitorImplementation, onActivityAvailability, }: {
84
81
  session: ApplicationSession;
85
82
  signalTarget?: SignalTarget;
86
- input?: SessionInput;
83
+ input?: TerminalInput | null;
87
84
  onReady?: (ready: ApplicationSession["ready"]) => void;
88
85
  activity?: ApplicationActivity | null;
89
86
  idleTimeoutSeconds?: number | null;
@@ -7,6 +7,8 @@ import { createCleanProcessEnvironment, isLoopbackUrl, } from "../shared/process
7
7
  import { assertApplicationPortAvailable } from "./port_preflight.mjs";
8
8
  import { waitForApplicationIdle } from "./activity_monitor.mjs";
9
9
  import { stopOwnedPosixProcessGroup } from "./process_group.mjs";
10
+ import { acquireTerminalInput, } from "../shared/terminal_input.mjs";
11
+ import { createOperationCancelledError, throwIfOperationCancelled, } from "../shared/cancellation.mjs";
10
12
  const ENV_KEY = /^[A-Z][A-Z0-9_]*$/u;
11
13
  const MAX_DIAGNOSTIC_BYTES = 64 * 1024;
12
14
  const SESSION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGTSTP"];
@@ -188,7 +190,7 @@ const stopOwnedChild = async (child) => {
188
190
  throw new Error("Owned application process group did not stop within its shutdown bound. No unrelated process was targeted.");
189
191
  }
190
192
  };
191
- export const startApplicationSession = async ({ command, cwd, files, mappings, readiness, inheritedEnvironment = process.env, spawnImplementation = spawn, fetchImplementation = fetch, signalTarget = process, portPreflight = (value) => assertApplicationPortAvailable({ url: value?.url }), }) => {
193
+ export const startApplicationSession = async ({ command, cwd, files, mappings, readiness, inheritedEnvironment = process.env, spawnImplementation = spawn, fetchImplementation = fetch, signalTarget = process, input = process.stdin, portPreflight = (value) => assertApplicationPortAvailable({ url: value?.url }), }) => {
192
194
  const mapped = await buildApplicationEnvironment({ files, mappings });
193
195
  const [program, ...args] = parseCommand(command);
194
196
  await portPreflight(readiness);
@@ -228,13 +230,22 @@ export const startApplicationSession = async ({ command, cwd, files, mappings, r
228
230
  });
229
231
  let interruptStartup = () => undefined;
230
232
  const interruption = new Promise((_, reject) => {
231
- interruptStartup = (signal) => reject(new Error(`Application startup was interrupted by ${signal}.`));
233
+ interruptStartup = (signal) => reject(createOperationCancelledError(signal));
232
234
  });
233
- const startupHandlers = new Map(SESSION_SIGNALS.map((signal) => [signal, () => interruptStartup(signal)]));
235
+ let releaseInput = null;
236
+ const startupHandlers = new Map(SESSION_SIGNALS.map((signal) => [
237
+ signal,
238
+ () => {
239
+ if (!releaseInput || releaseInput.isCurrentOwner())
240
+ interruptStartup(signal);
241
+ },
242
+ ]));
234
243
  for (const [signal, handler] of startupHandlers) {
235
244
  signalTarget.prependListener(signal, handler);
236
245
  }
237
246
  try {
247
+ releaseInput = acquireTerminalInput(input, interruptStartup);
248
+ throwIfOperationCancelled();
238
249
  const ready = await Promise.race([
239
250
  waitForReadiness({
240
251
  readiness,
@@ -255,13 +266,19 @@ export const startApplicationSession = async ({ command, cwd, files, mappings, r
255
266
  });
256
267
  }
257
268
  catch (error) {
258
- await stopApplication();
269
+ try {
270
+ await stopApplication();
271
+ }
272
+ catch (cleanupError) {
273
+ throw new AggregateError([error, cleanupError], "Application startup failed and its owned process could not be stopped.");
274
+ }
259
275
  throw error;
260
276
  }
261
277
  finally {
262
278
  for (const [signal, handler] of startupHandlers) {
263
279
  signalTarget.removeListener(signal, handler);
264
280
  }
281
+ releaseInput?.();
265
282
  }
266
283
  };
267
284
  /**
@@ -278,26 +295,23 @@ export const holdApplicationSession = async ({ session, signalTarget = process,
278
295
  const interruption = new Promise((resolve) => {
279
296
  finishInterruption = resolve;
280
297
  });
298
+ let releaseInput = null;
281
299
  const handlers = new Map(SESSION_SIGNALS.map((signal) => [
282
300
  signal,
283
- () => finishInterruption({ kind: "interrupted", signal }),
301
+ () => {
302
+ if (!releaseInput || releaseInput.isCurrentOwner())
303
+ finishInterruption({ kind: "interrupted", signal });
304
+ },
284
305
  ]));
285
306
  for (const [signal, handler] of handlers) {
286
307
  signalTarget.prependListener(signal, handler);
287
308
  }
288
- const onInput = (chunk) => {
289
- const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
290
- if (bytes.includes(0x03)) {
291
- finishInterruption({ kind: "interrupted", signal: "SIGINT" });
292
- }
293
- else if (bytes.includes(0x1a)) {
294
- finishInterruption({ kind: "interrupted", signal: "SIGTSTP" });
295
- }
296
- };
297
- if (input?.isTTY)
298
- input.prependListener("data", onInput);
299
309
  const activityController = new AbortController();
310
+ let operationFailed = false;
311
+ let operationError;
300
312
  try {
313
+ releaseInput = acquireTerminalInput(input, (signal) => finishInterruption({ kind: "interrupted", signal }));
314
+ throwIfOperationCancelled();
301
315
  if (idleTimeoutSeconds !== null &&
302
316
  (!activity ||
303
317
  !Number.isSafeInteger(idleTimeoutSeconds) ||
@@ -338,20 +352,31 @@ export const holdApplicationSession = async ({ session, signalTarget = process,
338
352
  },
339
353
  });
340
354
  }
355
+ catch (error) {
356
+ operationFailed = true;
357
+ operationError = error;
358
+ throw error;
359
+ }
341
360
  finally {
342
361
  activityController.abort();
343
362
  try {
344
363
  // npm can forward the terminal's SIGINT after the CLI receives it.
345
364
  // Keep our handlers installed until the owned process group is fully
346
365
  // stopped so a duplicate signal cannot terminate Rehearsal mid-cleanup.
347
- await session.stop();
366
+ try {
367
+ await session.stop();
368
+ }
369
+ catch (cleanupError) {
370
+ if (operationFailed)
371
+ throw new AggregateError([operationError, cleanupError], "Application session failed and its owned process could not be stopped.");
372
+ throw cleanupError;
373
+ }
348
374
  }
349
375
  finally {
350
376
  for (const [signal, handler] of handlers) {
351
377
  signalTarget.removeListener(signal, handler);
352
378
  }
353
- if (input?.isTTY)
354
- input.removeListener("data", onInput);
379
+ releaseInput?.();
355
380
  }
356
381
  }
357
382
  };
@@ -1,4 +1,5 @@
1
1
  /** Parse and classify the stable Rehearsal command-line contract. */
2
+ import { RehearsalError } from "../shared/diagnostics.mjs";
2
3
  export const parseArguments = (arguments_) => {
3
4
  const flags = {
4
5
  json: false,
@@ -86,7 +87,13 @@ export const parseArguments = (arguments_) => {
86
87
  else if (argument.startsWith("--image-id="))
87
88
  (flags.imageIds ??= []).push(argument.slice("--image-id=".length));
88
89
  else if (argument.startsWith("--"))
89
- throw new Error(`Unknown Rehearsal option: ${argument}.`);
90
+ throw new RehearsalError({
91
+ category: "configuration_invalid",
92
+ code: "CONFIGURATION_INVALID",
93
+ message: `Unknown Rehearsal option: ${argument}.`,
94
+ refused: "No Rehearsal command was executed.",
95
+ suggestions: ["Run rehearsal --help to list supported options."],
96
+ });
90
97
  else
91
98
  positionals.push(argument);
92
99
  }
@@ -8,7 +8,7 @@ import { join } from "node:path";
8
8
  import { performance } from "node:perf_hooks";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import * as prompts from "@clack/prompts";
11
- import { normalizeRehearsalError, renderHumanError, serializeRehearsalError, } from "../shared/diagnostics.mjs";
11
+ import { RehearsalError, normalizeRehearsalError, renderHumanError, serializeRehearsalError, } from "../shared/diagnostics.mjs";
12
12
  import { acquireProjectOperation, assertInstalledPackageFingerprint, createInstalledPackageFingerprint, } from "../shared/operation_guard.mjs";
13
13
  import { commandMutatesProject, parseArguments, resetGuidedFlags, } from "./arguments.mjs";
14
14
  import { usage } from "./basic_command_handlers.mjs";
@@ -22,6 +22,7 @@ import { buildRefreshWorkflowPlan, createRefreshWorkflow, } from "./source_comma
22
22
  import { installGuidedExitShortcut, useStyledPrompts } from "./terminal.mjs";
23
23
  import { runWithOperationCancellation, supportsOperationCancellation, } from "../shared/cancellation.mjs";
24
24
  import { withOperationProgress } from "../shared/operation_progress.mjs";
25
+ import { assertSupportedHostPlatform } from "../shared/host_platform.mjs";
25
26
  const packageRoot = fileURLToPath(new URL("../../..", import.meta.url));
26
27
  const projectRoot = process.cwd();
27
28
  const managerPath = join(packageRoot, "dist/scripts/runtime/manage_database.mjs");
@@ -70,7 +71,13 @@ const executeCommand = async ({ command, flags, planOptions, guided, }) => {
70
71
  state,
71
72
  });
72
73
  if (!result) {
73
- throw new Error(`Unknown Rehearsal command: ${command}.\n\n${usage()}`);
74
+ throw new RehearsalError({
75
+ category: "configuration_invalid",
76
+ code: "CONFIGURATION_INVALID",
77
+ message: `Unknown Rehearsal command: ${command}.\n\n${usage()}`,
78
+ refused: "No Rehearsal command was executed.",
79
+ suggestions: ["Run rehearsal --help to list supported commands."],
80
+ });
74
81
  }
75
82
  const exitCode = emitCommand({ command, flags, result });
76
83
  if (exitCode !== undefined)
@@ -88,8 +95,17 @@ const main = async () => {
88
95
  !flags.json &&
89
96
  process.stdin.isTTY &&
90
97
  process.stdout.isTTY;
91
- const guided = command === "guide" || wantsAutomaticGuide;
92
- const mutating = commandMutatesProject({ command, flags, guided });
98
+ const guided = !flags.help &&
99
+ !flags.version &&
100
+ (command === "guide" || wantsAutomaticGuide);
101
+ if (!flags.help &&
102
+ !flags.version &&
103
+ (guided || !["help", "version", "discover", "doctor"].includes(command))) {
104
+ await assertSupportedHostPlatform({ projectRoot });
105
+ }
106
+ const mutating = !flags.help &&
107
+ !flags.version &&
108
+ commandMutatesProject({ command, flags, guided });
93
109
  const operation = mutating
94
110
  ? await acquireProjectOperation({
95
111
  projectRoot,
@@ -141,12 +157,18 @@ const main = async () => {
141
157
  removeGuidedExitShortcut = null;
142
158
  }
143
159
  try {
144
- await executeCommand({
160
+ const executeSelected = () => executeCommand({
145
161
  command: selected,
146
162
  flags,
147
163
  planOptions,
148
164
  guided: true,
149
165
  });
166
+ if (opensApplication) {
167
+ await runWithOperationCancellation({ task: executeSelected });
168
+ }
169
+ else {
170
+ await executeSelected();
171
+ }
150
172
  }
151
173
  finally {
152
174
  if (opensApplication) {