@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.
- package/CHANGELOG.md +52 -1
- package/COMPATIBILITY.md +56 -8
- package/README.md +31 -12
- package/SECURITY.md +5 -3
- package/SUPPORT.md +4 -3
- package/dist/package.json +3 -3
- package/dist/scripts/install/create_root_config.mjs +9 -2
- package/dist/src/application/interactive_task.d.mts +2 -6
- package/dist/src/application/interactive_task.mjs +4 -13
- package/dist/src/application/session.d.mts +5 -8
- package/dist/src/application/session.mjs +44 -19
- package/dist/src/cli/arguments.mjs +8 -1
- package/dist/src/cli/rehearsal.mjs +27 -5
- package/dist/src/cli/runtime_commands.mjs +16 -17
- package/dist/src/cli/runtime_planning.mjs +6 -0
- package/dist/src/cli/terminal.mjs +24 -5
- package/dist/src/project/install_scaffold.mjs +7 -0
- package/dist/src/project/project_discovery_metadata.mjs +9 -11
- package/dist/src/runtime/persistence.mjs +6 -3
- package/dist/src/runtime/plan.mjs +26 -15
- package/dist/src/shared/cancellation.d.mts +3 -1
- package/dist/src/shared/cancellation.mjs +12 -4
- package/dist/src/shared/host_platform.d.mts +16 -0
- package/dist/src/shared/host_platform.mjs +109 -0
- package/dist/src/shared/process_environment.d.mts +2 -1
- package/dist/src/shared/process_environment.mjs +47 -5
- package/dist/src/shared/terminal_input.d.mts +25 -0
- package/dist/src/shared/terminal_input.mjs +129 -0
- package/docs/commands.md +17 -1
- package/docs/getting-started.md +48 -0
- package/docs/index.md +15 -9
- package/docs/releasing.md +50 -24
- package/docs/roadmap.md +8 -3
- package/docs/stability.md +48 -19
- package/docs/tutorial.md +1 -1
- package/docs/vscode.md +199 -41
- 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/
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
21
|
-
public API
|
|
22
|
-
systems is not implied
|
|
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.
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
166
|
-
| Native Windows
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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": "
|
|
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": "
|
|
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
|
|
8
|
-
|
|
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?:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
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(
|
|
233
|
+
interruptStartup = (signal) => reject(createOperationCancelledError(signal));
|
|
232
234
|
});
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
() =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
92
|
-
|
|
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
|
-
|
|
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) {
|