machine-bridge-mcp 0.6.0 → 0.7.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 +83 -0
- package/CONTRIBUTING.md +23 -0
- package/README.md +32 -12
- package/SECURITY.md +20 -10
- package/docs/ARCHITECTURE.md +16 -8
- package/docs/CLIENTS.md +11 -1
- package/docs/LOGGING.md +13 -18
- package/docs/MANAGED_JOBS.md +20 -11
- package/docs/OPERATIONS.md +31 -10
- package/docs/PRIVACY.md +37 -0
- package/docs/RELEASING.md +9 -4
- package/docs/TESTING.md +22 -2
- package/package.json +39 -10
- package/scripts/network-retry.mjs +47 -0
- package/scripts/privacy-check.mjs +177 -0
- package/scripts/release-impact-check.mjs +78 -0
- package/src/local/atomic-fs.mjs +37 -0
- package/src/local/cli.mjs +113 -15
- package/src/local/daemon.mjs +114 -15
- package/src/local/full-access-test.mjs +206 -0
- package/src/local/job-runner.mjs +73 -15
- package/src/local/log.mjs +26 -2
- package/src/local/managed-jobs.mjs +165 -75
- package/src/local/process-sessions.mjs +5 -5
- package/src/local/resource-operations.mjs +66 -0
- package/src/local/service.mjs +128 -30
- package/src/local/shell.mjs +1 -1
- package/src/local/ssh-key.mjs +127 -0
- package/src/local/state.mjs +12 -9
- package/src/local/stdio.mjs +78 -20
- package/src/local/tools.mjs +37 -3
- package/src/shared/server-metadata.json +4 -1
- package/src/shared/tool-catalog.json +37 -0
- package/src/worker/index.ts +41 -18
- package/wrangler.jsonc +1 -1
package/docs/OPERATIONS.md
CHANGED
|
@@ -14,6 +14,7 @@ machine-mcp service status
|
|
|
14
14
|
|
|
15
15
|
| Result | Interpretation |
|
|
16
16
|
|---|---|
|
|
17
|
+
| `server_info` reports full and all relay tools, but the current session UI exposes fewer tools | Host/connector post-relay filtering; Machine Bridge cannot enumerate or override that subset |
|
|
17
18
|
| No structured result because the host rejects the call | Host/connector approval or safety layer, or transport before daemon delivery |
|
|
18
19
|
| `mcp-host-to-daemon` passes but `local-filesystem` fails | Local state/runtime permissions, disk policy, sandbox, or endpoint security |
|
|
19
20
|
| Filesystem passes but `local-process-spawn` fails | Local executable policy, endpoint security, OS permissions, or damaged Node runtime |
|
|
@@ -21,19 +22,19 @@ machine-mcp service status
|
|
|
21
22
|
| `managed-job-storage` fails | Owner-only profile/job directory cannot be used |
|
|
22
23
|
| Registered resource is unavailable | File moved, permissions changed, size exceeded, or local access denied |
|
|
23
24
|
|
|
24
|
-
A successful diagnostic result applies only to that probe. An MCP host can still deny a later call based on its own request context.
|
|
25
|
+
A successful diagnostic result applies only to that probe. An MCP host can still deny a later call based on its own request context. This is expected layering, not a defect in the `full` profile: `full` removes Machine Bridge's own denials, while host delivery remains independent.
|
|
25
26
|
|
|
26
27
|
## Logs
|
|
27
28
|
|
|
28
|
-
Remote autostart logs are stored under the state root in `logs/daemon.out.log` and `logs/daemon.err.log`. Files are owner-only where supported and tail-trimmed before daemon startup.
|
|
29
|
+
Remote autostart definitions prefer a stable PATH alias that resolves to the currently running Node executable and persist a sanitized absolute-only service `PATH` containing the Node/CLI directories, the installer's absolute PATH entries, and platform defaults. This avoids versioned Homebrew-style paths becoming invalid after upgrades and prevents launchd/systemd from falling back to a minimal system-only PATH. Re-run `machine-mcp service install` after changing Node installation families or PATH layout. Autostart logs are stored under the state root in `logs/daemon.out.log` and `logs/daemon.err.log`. Files are owner-only where supported and tail-trimmed before daemon startup.
|
|
29
30
|
|
|
30
31
|
Logging is level-based:
|
|
31
32
|
|
|
32
33
|
```text
|
|
33
|
-
error unrecoverable local/transport failures
|
|
34
|
-
warn
|
|
35
|
-
info startup/deploy/connect transitions
|
|
36
|
-
debug
|
|
34
|
+
error unrecoverable local/transport/service failures
|
|
35
|
+
warn relay disconnect/send failures, malformed relay events, supersession and service problems
|
|
36
|
+
info startup/deploy/connect transitions
|
|
37
|
+
debug all per-tool starts/successes/failures/cancellations/timing, correlation and reconnect details
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
Foreground mode defaults to `info`; autostart uses `warn`. Use `--verbose` or `--log-level debug` only for diagnosis. `--quiet` is an alias for `--log-level error`.
|
|
@@ -42,13 +43,32 @@ Normal logs intentionally omit tool arguments, file/patch/image content, command
|
|
|
42
43
|
|
|
43
44
|
See [LOGGING.md](LOGGING.md) for the event contract and MCP-host boundary. Cloudflare observability is sampled and is not a complete audit log.
|
|
44
45
|
|
|
46
|
+
## Full capability acceptance
|
|
47
|
+
|
|
48
|
+
Run:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
machine-mcp full-test --workspace /path/to/project
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The command uses disposable local directories and performs actual read/write, process, shell, environment, SSH-key, sandbox authorized-key, SSH-client, managed-job and finally-cleanup operations. It also checks whether the Google Cloud OS Login command exists and whether `sudo -n true` is currently permitted, without changing either system. `ok` covers core Machine Bridge functionality; `operator_workflow_ready` additionally reports the local SSH/Google CLI prerequisites. No external cloud, account, or server change is made.
|
|
55
|
+
|
|
56
|
+
Generate and register an operator key locally:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
machine-mcp resource generate-ssh-key NAME [PRIVATE_KEY_PATH]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
An authorized canonical-full MCP client can use `generate_ssh_key_resource`. Both paths validate the key pair and return only metadata and the bare public fingerprint. Local paths are omitted unless `--show-paths` or `expose_paths=true` is explicitly requested; public-key comments are not included in the returned fingerprint. They do not install the public key in Google, modify `authorized_keys`, or grant remote `sudo`; those remain explicit managed-job/local-operator operations.
|
|
63
|
+
|
|
45
64
|
## Managed jobs and local recovery
|
|
46
65
|
|
|
47
66
|
Register local-only resources from the terminal:
|
|
48
67
|
|
|
49
68
|
```sh
|
|
50
69
|
machine-mcp resource add NAME FILE_PATH
|
|
51
|
-
machine-mcp resource list
|
|
70
|
+
machine-mcp resource list # paths omitted by default
|
|
71
|
+
machine-mcp resource list --show-paths # explicit local-only disclosure
|
|
52
72
|
machine-mcp resource check NAME
|
|
53
73
|
machine-mcp resource remove NAME
|
|
54
74
|
```
|
|
@@ -84,8 +104,9 @@ Pending calls are bound to the socket that received them. Results from another s
|
|
|
84
104
|
Defense-in-depth limits include:
|
|
85
105
|
|
|
86
106
|
- Worker MCP body: 8 MiB by default, hard cap 16 MiB;
|
|
107
|
+
- stdio JSON-RPC line: 8 MiB, enforced incrementally while reading;
|
|
87
108
|
- OAuth body: 64 KiB;
|
|
88
|
-
- daemon WebSocket message: 8 MiB;
|
|
109
|
+
- daemon WebSocket message: 8 MiB, enforced by the local WebSocket parser before string conversion;
|
|
89
110
|
- text writes and patch envelopes: 5 MiB;
|
|
90
111
|
- images: 4 MiB before base64 encoding;
|
|
91
112
|
- shell/argv envelope: 64 KiB;
|
|
@@ -108,9 +129,9 @@ Defense-in-depth limits include:
|
|
|
108
129
|
|
|
109
130
|
## Upgrade behavior
|
|
110
131
|
|
|
111
|
-
|
|
132
|
+
Policy revision 3 makes named profiles canonical. A state entry labelled `full` is repaired to writes, direct processes, process sessions, shell execution, unrestricted direct filesystem paths, absolute path output, the complete parent environment, and the complete tool catalog. CLI capability overrides are stored as `custom`. The exact pre-0.4 implicit-default shape is still migrated to the current `full` default; explicit restrictive and identified custom profiles remain preserved.
|
|
112
133
|
|
|
113
|
-
`full`
|
|
134
|
+
`full` removes Machine Bridge's own profile/path/environment/shell denials and makes the complete catalog available to the relay. It does not force a connector host to expose every relayed tool, and the server cannot see the host's final subset. It also does not override operating-system access controls, endpoint security, remote authentication, cloud IAM, `sudo`, or independent MCP-host/platform policy.
|
|
114
135
|
|
|
115
136
|
Inspect effective policy with:
|
|
116
137
|
|
package/docs/PRIVACY.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Repository privacy hygiene
|
|
2
|
+
|
|
3
|
+
Source code, tests, examples, release notes, and documentation are publication surfaces. Examples must use synthetic identifiers such as `maintenance-key`, `admin@server.example`, reserved example domains, and generic filesystem paths. Do not copy a real server alias, username, hostname, account name, workspace path, key filename, customer name, or internal codename into a fixture merely because the value is not itself a credential.
|
|
4
|
+
|
|
5
|
+
## Automated check
|
|
6
|
+
|
|
7
|
+
Run:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm run privacy:check
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The check scans tracked and unignored new UTF-8 files and their relative names for private-key material, common live-token forms, user-home paths, non-example `user@host` identifiers, and locally configured private identifiers. Publication-surface symbolic links are rejected rather than followed. Binary, invalid UTF-8, and files above the bounded scanner limit fail closed and require explicit manual review instead of being silently skipped. It reports only the file, line, and rule; it does not print the matched value.
|
|
14
|
+
|
|
15
|
+
Maintain machine-specific names in an ignored owner-only file:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
cp .privacy-denylist.example .privacy-denylist
|
|
19
|
+
chmod 600 .privacy-denylist
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Add one identifier per line. The denylist is deliberately local and must never be committed. CI still runs the built-in generic checks; a developer's local check adds their private vocabulary.
|
|
23
|
+
|
|
24
|
+
## Review rules
|
|
25
|
+
|
|
26
|
+
Before committing or publishing:
|
|
27
|
+
|
|
28
|
+
- inspect the complete staged diff, including tests, snapshots, examples, release notes, and generated metadata;
|
|
29
|
+
- use reserved example domains and neutral aliases;
|
|
30
|
+
- run `npm run privacy:check`, `npm run check`, and `npm pack --dry-run`;
|
|
31
|
+
- treat paths, host aliases, usernames, and codenames as private metadata even when they are not authentication secrets.
|
|
32
|
+
|
|
33
|
+
The scanner is heuristic. It cannot identify every personal or organizational name, transformed value, image, archive, binary fixture, or data already present in Git history.
|
|
34
|
+
|
|
35
|
+
## Incident response
|
|
36
|
+
|
|
37
|
+
For an accidental publication, remove the value from the current tree and release artifacts, determine whether it is merely identifying metadata or an active credential, and rotate/revoke any credential immediately. Public Git and npm history are immutable in ordinary workflows: replacing the current file does not erase old commits or a published package. A coordinated history rewrite, cache invalidation request, or replacement release may be appropriate, but those actions are disruptive and require an explicit repository-owner decision.
|
package/docs/RELEASING.md
CHANGED
|
@@ -7,6 +7,8 @@ The release invariant is:
|
|
|
7
7
|
- A final GitHub Release exists for the tag.
|
|
8
8
|
- The GitHub Release contains the npm tarball generated from that commit.
|
|
9
9
|
- `package.json`, `package-lock.json`, and the Worker-reported version agree.
|
|
10
|
+
- Every release-relevant change since the prior version tag has a higher package version and matching CHANGELOG section.
|
|
11
|
+
- The same reviewed change is present on GitHub and in a new npm version.
|
|
10
12
|
|
|
11
13
|
`npm publish` runs `release:check` through `prepublishOnly`, so npm publication is blocked until all GitHub state is synchronized.
|
|
12
14
|
|
|
@@ -18,8 +20,11 @@ The release invariant is:
|
|
|
18
20
|
npm version <version> --no-git-tag-version
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
2. Add the matching `CHANGELOG.md` section.
|
|
22
|
-
3. Run `npm run check`,
|
|
23
|
+
2. Add the matching dated `CHANGELOG.md` section.
|
|
24
|
+
3. Run `npm run release-impact:check`, `npm run privacy:check`, `npm run check`, both dependency audits, `npm audit signatures`, and generate a CycloneDX `npm sbom`.
|
|
25
|
+
4. Inspect the complete diff and `npm pack --dry-run`, then commit and push all release changes to `main`.
|
|
26
|
+
|
|
27
|
+
A privacy/security documentation correction is not “docs only” for release purposes. It requires a replacement npm version and, when appropriate, deprecation or unpublication of the affected version.
|
|
23
28
|
|
|
24
29
|
## Publish GitHub source and release
|
|
25
30
|
|
|
@@ -29,7 +34,7 @@ From a clean `main` worktree:
|
|
|
29
34
|
npm run release:publish
|
|
30
35
|
```
|
|
31
36
|
|
|
32
|
-
The command validates the project, fast-forwards `origin/main`, creates or verifies the annotated version tag, pushes it, builds the npm tarball, creates or updates the GitHub Release, uploads the tarball, and verifies the resulting state.
|
|
37
|
+
The command validates the project, including repository privacy checks, fast-forwards `origin/main`, creates or verifies the annotated version tag, pushes it, builds the npm tarball, creates or updates the GitHub Release, uploads the tarball, and verifies the resulting state. Read-only GitHub operations, Git pushes, and idempotent release updates use bounded retries only for classified transient network failures; ambiguous Release creation responses are resolved by querying server state before continuing.
|
|
33
38
|
|
|
34
39
|
To verify without changing anything:
|
|
35
40
|
|
|
@@ -51,7 +56,7 @@ Only after `release:check` succeeds:
|
|
|
51
56
|
npm publish --access public
|
|
52
57
|
```
|
|
53
58
|
|
|
54
|
-
The npm lifecycle repeats the full project checks and the GitHub synchronization check before upload.
|
|
59
|
+
The npm lifecycle repeats the full project checks and the GitHub synchronization check before upload. Do not leave a code or documentation fix only on GitHub; the corresponding npm version must be published, and the operator should be explicitly reminded until registry publication is confirmed.
|
|
55
60
|
|
|
56
61
|
## Authentication requirements
|
|
57
62
|
|
package/docs/TESTING.md
CHANGED
|
@@ -8,8 +8,11 @@ The project treats transport, authorization, local authority, and state removal
|
|
|
8
8
|
npm run check
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
The repository requires Node.js 26 and npm 12. `.node-version`, `.nvmrc`, `packageManager`, `devEngines`, and strict engine checks keep local and CI execution on the same baseline.
|
|
12
|
+
|
|
11
13
|
The suite includes:
|
|
12
14
|
|
|
15
|
+
- release-impact enforcement requiring a new package version and CHANGELOG section for release-relevant changes;
|
|
13
16
|
- generated Cloudflare Worker types and strict TypeScript checking;
|
|
14
17
|
- syntax validation for every shipped JavaScript entry point;
|
|
15
18
|
- shared tool-catalog schema, annotation, and profile-inventory checks;
|
|
@@ -25,11 +28,15 @@ The suite includes:
|
|
|
25
28
|
- one-shot timeout, descendant termination, cancellation, and process-session interaction;
|
|
26
29
|
- layered fixed runtime diagnostics for filesystem, direct process, shell, managed-job storage, and resource availability;
|
|
27
30
|
- local resource CLI registration, permission checks, dynamic reload, state-path redaction, and content non-disclosure;
|
|
31
|
+
- real Ed25519 and RSA generation, idempotent reuse, public/private correspondence, mode enforcement, incomplete/mismatched/symlink rejection, and private-content non-disclosure;
|
|
32
|
+
- real-machine canonical-full sandbox acceptance for outside-workspace I/O, direct/shell execution, full environment inheritance, SSH prerequisites, temporary authorized-key writing, and detached cleanup without external state changes;
|
|
33
|
+
- deterministic injected atomic-replace failures and repeated Windows full-sandbox runs to catch transient file-sharing races;
|
|
34
|
+
- canonical named-profile repair and full-only tool exposure parity between local and Worker policy filters;
|
|
28
35
|
- managed-job staging/local approval/cancel-before-start, detachment, job-scoped temporary files, resource hash verification/redaction, discard capture, finally execution, cancellation escalation, plan scrubbing, and dead-runner recovery;
|
|
29
36
|
- daemon/startup locking and state corruption recovery;
|
|
30
37
|
- guarded state-root removal, schema migration, policy-origin persistence, and legacy implicit-default migration;
|
|
31
38
|
- no filename-based sensitive-file denial under unrestricted policy;
|
|
32
|
-
- log redaction, control-character handling, message/field bounds,
|
|
39
|
+
- log redaction, control-character handling, message/field bounds, suppression of both successful and failed per-tool events outside debug, and service warning-level configuration;
|
|
33
40
|
- CLI parsing, policy profiles, and client configuration boundaries;
|
|
34
41
|
- live stdio MCP initialization, discovery, calls, rich content, sessions, cancellation, managed-job acceptance, and a detached job/finally phase that survives stdio shutdown;
|
|
35
42
|
- live local Worker OAuth registration, consent, PKCE, token replay rejection, throttling, CORS, protocol negotiation, dynamic tool advertisement, rich content, daemon replacement, and cancellation.
|
|
@@ -40,11 +47,14 @@ The suite includes:
|
|
|
40
47
|
npm run worker:dry-run
|
|
41
48
|
npm audit --audit-level=high
|
|
42
49
|
npm audit --omit=dev --audit-level=high
|
|
50
|
+
npm audit signatures
|
|
51
|
+
npm sbom --sbom-format cyclonedx
|
|
43
52
|
npm pack --dry-run
|
|
44
53
|
npm run version:check
|
|
54
|
+
npm run release-impact:check
|
|
45
55
|
```
|
|
46
56
|
|
|
47
|
-
GitHub Actions executes the main suite on Linux, macOS, and Windows. Node
|
|
57
|
+
GitHub Actions executes the main suite on Linux, macOS, and Windows using the pinned Node 26/npm 12 baseline. Because Node 26 currently bundles npm 11, CI explicitly disables setup-node's automatic package-manager cache and upgrades npm from the runner temporary directory before any project-local npm command can trigger strict `devEngines`. Checkout fetches version tags so the release-impact gate can compare the branch with the latest release. A separate package-audit job audits both the complete dependency graph and the production-only graph, verifies registry signatures and attestations, validates a CycloneDX SBOM written under the runner temporary directory, then performs a dry-run package build. Dependency and GitHub Actions updates are monitored by Dependabot.
|
|
48
58
|
|
|
49
59
|
## Test design rules
|
|
50
60
|
|
|
@@ -57,3 +67,13 @@ GitHub Actions executes the main suite on Linux, macOS, and Windows. Node 22 and
|
|
|
57
67
|
- Secret-bearing resource tests must assert absence of raw, path, base64, and hex forms from MCP-visible results.
|
|
58
68
|
- Logs and public metadata should be tested for absence of sensitive fields, arguments, outputs, and routine success noise—not only presence of expected fields.
|
|
59
69
|
- Cross-platform tests must avoid shell syntax, URL-path conversion, and executable-shim assumptions specific to one operating system.
|
|
70
|
+
|
|
71
|
+
## Privacy hygiene
|
|
72
|
+
|
|
73
|
+
Run `npm run privacy:check` before committing and before packaging. Developers should maintain an ignored owner-only `.privacy-denylist` for machine aliases, usernames, internal codenames, and other private identifiers that a generic scanner cannot know. See [Repository privacy hygiene](PRIVACY.md).
|
|
74
|
+
|
|
75
|
+
## Package manifest
|
|
76
|
+
|
|
77
|
+
`npm run package:test` executes a real silent `npm pack --dry-run --json`, requires clean parseable JSON, rejects sensitive local artifacts and credential-like file classes, and verifies that privacy guidance, contribution/release discipline, and both privacy/release-impact checkers are present in the published package.
|
|
78
|
+
|
|
79
|
+
The stdio integration test also sends an oversized line, verifies bounded rejection, and confirms that the next valid request is still processed.
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Cross-client MCP bridge for
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "Cross-client MCP bridge for canonical local automation, files, Git, SSH resources, managed jobs, and processes over stdio or an OAuth-protected remote relay.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=26.0.0"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
11
|
"machine-bridge-mcp": "bin/machine-mcp.mjs",
|
|
@@ -20,11 +20,15 @@
|
|
|
20
20
|
"src/shared",
|
|
21
21
|
"src/worker/index.ts",
|
|
22
22
|
"scripts/sync-worker-version.mjs",
|
|
23
|
+
"scripts/privacy-check.mjs",
|
|
24
|
+
"scripts/release-impact-check.mjs",
|
|
25
|
+
"scripts/network-retry.mjs",
|
|
23
26
|
"wrangler.jsonc",
|
|
24
27
|
"tsconfig.json",
|
|
25
28
|
"README.md",
|
|
26
29
|
"SECURITY.md",
|
|
27
30
|
"CHANGELOG.md",
|
|
31
|
+
"CONTRIBUTING.md",
|
|
28
32
|
"docs",
|
|
29
33
|
"LICENSE"
|
|
30
34
|
],
|
|
@@ -35,12 +39,12 @@
|
|
|
35
39
|
"version:sync": "node scripts/sync-worker-version.mjs",
|
|
36
40
|
"version:check": "node scripts/sync-worker-version.mjs --check",
|
|
37
41
|
"version": "node scripts/sync-worker-version.mjs && git add package.json package-lock.json src/worker/index.ts",
|
|
38
|
-
"prepack": "npm run version:check",
|
|
42
|
+
"prepack": "npm run version:check && npm run privacy:check && npm run release-impact:check",
|
|
39
43
|
"prepublishOnly": "npm run check && npm run version:check && npm run release:check",
|
|
40
44
|
"worker:types": "wrangler types src/worker/worker-configuration.d.ts",
|
|
41
45
|
"typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit",
|
|
42
|
-
"syntax": "sh -n mbm && node --check bin/machine-mcp.mjs && node --check src/local/cli.mjs && node --check src/local/daemon.mjs && node --check src/local/patch.mjs && node --check src/local/process-sessions.mjs && node --check src/local/tools.mjs && node --check src/local/stdio.mjs && node --check src/local/state.mjs && node --check src/local/shell.mjs && node --check src/local/service.mjs && node --check src/local/log.mjs && node --check src/local/managed-jobs.mjs && node --check src/local/job-runner.mjs && node --check scripts/sync-worker-version.mjs && node --check scripts/github-release.mjs && node --check tests/worker-integration-test.mjs && node --check tests/stdio-integration-test.mjs && node --check tests/catalog-test.mjs && node --check tests/local-self-test.mjs && node --check tests/daemon-self-test.mjs && node --check tests/managed-jobs-test.mjs",
|
|
43
|
-
"check": "npm run typecheck && npm run syntax && npm run catalog:test && npm run self-test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
|
|
46
|
+
"syntax": "sh -n mbm && node --check bin/machine-mcp.mjs && node --check src/local/cli.mjs && node --check src/local/daemon.mjs && node --check src/local/patch.mjs && node --check src/local/process-sessions.mjs && node --check src/local/tools.mjs && node --check src/local/stdio.mjs && node --check src/local/state.mjs && node --check src/local/shell.mjs && node --check src/local/service.mjs && node --check src/local/log.mjs && node --check src/local/atomic-fs.mjs && node --check src/local/ssh-key.mjs && node --check src/local/resource-operations.mjs && node --check src/local/full-access-test.mjs && node --check src/local/managed-jobs.mjs && node --check src/local/job-runner.mjs && node --check scripts/sync-worker-version.mjs && node --check scripts/privacy-check.mjs && node --check scripts/release-impact-check.mjs && node --check scripts/network-retry.mjs && node --check scripts/github-release.mjs && node --check tests/worker-integration-test.mjs && node --check tests/stdio-integration-test.mjs && node --check tests/catalog-test.mjs && node --check tests/local-self-test.mjs && node --check tests/daemon-self-test.mjs && node --check tests/managed-jobs-test.mjs && node --check tests/ssh-key-test.mjs && node --check tests/full-access-test.mjs && node --check tests/atomic-fs-test.mjs && node --check tests/package-test.mjs && node --check tests/release-impact-test.mjs && node --check tests/privacy-test.mjs && node --check tests/network-retry-test.mjs",
|
|
47
|
+
"check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run network-retry:test && npm run typecheck && npm run syntax && npm run catalog:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
|
|
44
48
|
"worker:dry-run": "wrangler deploy --dry-run",
|
|
45
49
|
"worker:integration-test": "node tests/worker-integration-test.mjs",
|
|
46
50
|
"release:check": "node scripts/github-release.mjs --check",
|
|
@@ -48,15 +52,24 @@
|
|
|
48
52
|
"release:backfill": "node scripts/github-release.mjs --backfill",
|
|
49
53
|
"stdio:integration-test": "node tests/stdio-integration-test.mjs",
|
|
50
54
|
"catalog:test": "node tests/catalog-test.mjs",
|
|
51
|
-
"managed-jobs:test": "node tests/managed-jobs-test.mjs"
|
|
55
|
+
"managed-jobs:test": "node tests/managed-jobs-test.mjs",
|
|
56
|
+
"ssh-key:test": "node tests/ssh-key-test.mjs",
|
|
57
|
+
"full-access:test": "node tests/full-access-test.mjs",
|
|
58
|
+
"atomic-fs:test": "node tests/atomic-fs-test.mjs",
|
|
59
|
+
"privacy:check": "node scripts/privacy-check.mjs",
|
|
60
|
+
"package:test": "node tests/package-test.mjs",
|
|
61
|
+
"release-impact:check": "node scripts/release-impact-check.mjs",
|
|
62
|
+
"release-impact:test": "node tests/release-impact-test.mjs",
|
|
63
|
+
"privacy:test": "node tests/privacy-test.mjs",
|
|
64
|
+
"network-retry:test": "node tests/network-retry-test.mjs"
|
|
52
65
|
},
|
|
53
66
|
"dependencies": {
|
|
54
67
|
"wrangler": "4.110.0",
|
|
55
68
|
"ws": "8.21.0"
|
|
56
69
|
},
|
|
57
70
|
"devDependencies": {
|
|
58
|
-
"@types/node": "
|
|
59
|
-
"typescript": "
|
|
71
|
+
"@types/node": "26.1.1",
|
|
72
|
+
"typescript": "7.0.2"
|
|
60
73
|
},
|
|
61
74
|
"keywords": [
|
|
62
75
|
"mcp",
|
|
@@ -68,7 +81,10 @@
|
|
|
68
81
|
"stdio",
|
|
69
82
|
"coding-agent",
|
|
70
83
|
"oauth",
|
|
71
|
-
"cross-platform"
|
|
84
|
+
"cross-platform",
|
|
85
|
+
"ssh",
|
|
86
|
+
"automation",
|
|
87
|
+
"managed-jobs"
|
|
72
88
|
],
|
|
73
89
|
"repository": {
|
|
74
90
|
"type": "git",
|
|
@@ -83,5 +99,18 @@
|
|
|
83
99
|
"sharp@0.34.5": true,
|
|
84
100
|
"workerd@1.20260708.1": true,
|
|
85
101
|
"fsevents": false
|
|
102
|
+
},
|
|
103
|
+
"packageManager": "npm@12.0.1",
|
|
104
|
+
"devEngines": {
|
|
105
|
+
"runtime": {
|
|
106
|
+
"name": "node",
|
|
107
|
+
"version": ">=26.0.0",
|
|
108
|
+
"onFail": "error"
|
|
109
|
+
},
|
|
110
|
+
"packageManager": {
|
|
111
|
+
"name": "npm",
|
|
112
|
+
"version": ">=12.0.0",
|
|
113
|
+
"onFail": "error"
|
|
114
|
+
}
|
|
86
115
|
}
|
|
87
116
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
4
|
+
const TRANSIENT_NETWORK_FAILURE = /(?:SSL_ERROR_SYSCALL|SSL_connect|TLS handshake timeout|Client network socket disconnected before secure TLS connection was established|unexpected EOF|unexpected eof while reading|\bEOF\b|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|ECONNREFUSED|Could not resolve host|Failed to connect|connection (?:was )?reset|remote end hung up|unexpected disconnect|HTTP (?:429|5\d\d)|(?:502|503|504) (?:Bad Gateway|Service Unavailable|Gateway Timeout))/i;
|
|
5
|
+
|
|
6
|
+
export function runNetworkCommand(command, args, options = {}) {
|
|
7
|
+
const attempts = clampInteger(options.attempts, 3, 1, 5);
|
|
8
|
+
const baseDelayMs = clampInteger(options.baseDelayMs, 750, 0, 10_000);
|
|
9
|
+
const actualArgs = networkCommandArgs(command, args);
|
|
10
|
+
let result;
|
|
11
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
12
|
+
result = spawnSync(command, actualArgs, {
|
|
13
|
+
cwd: options.cwd,
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
stdio: "pipe",
|
|
16
|
+
env: options.env,
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
});
|
|
19
|
+
result.attempts = attempt;
|
|
20
|
+
if (!isTransientNetworkFailure(result) || attempt === attempts) return result;
|
|
21
|
+
sleepSync(Math.min(baseDelayMs * attempt, 5_000));
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function networkCommandArgs(command, args) {
|
|
27
|
+
return command === "git" ? ["-c", "http.version=HTTP/1.1", ...args] : [...args];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isTransientNetworkFailure(result) {
|
|
31
|
+
if (result?.error) {
|
|
32
|
+
const code = String(result.error.code || "");
|
|
33
|
+
if (["ECONNRESET", "ETIMEDOUT", "EAI_AGAIN", "ENETUNREACH", "ECONNREFUSED"].includes(code)) return true;
|
|
34
|
+
}
|
|
35
|
+
const detail = `${result?.stdout ?? ""}\n${result?.stderr ?? ""}\n${result?.error?.message ?? ""}`;
|
|
36
|
+
return TRANSIENT_NETWORK_FAILURE.test(detail);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sleepSync(milliseconds) {
|
|
40
|
+
if (milliseconds > 0) Atomics.wait(WAIT_BUFFER, 0, 0, milliseconds);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function clampInteger(value, fallback, minimum, maximum) {
|
|
44
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
45
|
+
const number = Number.isFinite(parsed) ? parsed : fallback;
|
|
46
|
+
return Math.min(Math.max(number, minimum), maximum);
|
|
47
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const selfPath = "scripts/privacy-check.mjs";
|
|
8
|
+
const candidates = collectCandidateFiles(root);
|
|
9
|
+
const denylist = loadDenylist(path.join(root, ".privacy-denylist"));
|
|
10
|
+
const findings = [];
|
|
11
|
+
|
|
12
|
+
for (const relativePath of candidates) {
|
|
13
|
+
if (relativePath === ".privacy-denylist") continue;
|
|
14
|
+
scanDenylistPath(relativePath, denylist, findings);
|
|
15
|
+
const fullPath = path.join(root, relativePath);
|
|
16
|
+
let info;
|
|
17
|
+
try { info = lstatSync(fullPath); } catch { continue; }
|
|
18
|
+
if (info.isSymbolicLink()) {
|
|
19
|
+
findings.push({ path: relativePath, line: 1, rule: "symbolic link in publication surface" });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!info.isFile()) continue;
|
|
23
|
+
let buffer;
|
|
24
|
+
try { buffer = readFileSync(fullPath); } catch { continue; }
|
|
25
|
+
if (buffer.length > 5 * 1024 * 1024) {
|
|
26
|
+
findings.push({ path: relativePath, line: 1, rule: "file exceeds privacy scanner size limit and requires manual review" });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (buffer.includes(0)) {
|
|
30
|
+
findings.push({ path: relativePath, line: 1, rule: "binary file in publication surface requires manual review" });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
let text;
|
|
34
|
+
try { text = new TextDecoder("utf-8", { fatal: true }).decode(buffer); } catch {
|
|
35
|
+
findings.push({ path: relativePath, line: 1, rule: "non-UTF-8 file in publication surface requires manual review" });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (relativePath !== selfPath) scanBuiltIn(relativePath, text, findings);
|
|
39
|
+
scanDenylist(relativePath, text, denylist, findings);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (findings.length) {
|
|
43
|
+
for (const finding of findings.slice(0, 100)) {
|
|
44
|
+
process.stderr.write(`${redactReportPath(finding.path, denylist)}:${finding.line}: ${finding.rule}\n`);
|
|
45
|
+
}
|
|
46
|
+
if (findings.length > 100) process.stderr.write(`... ${findings.length - 100} additional findings omitted\n`);
|
|
47
|
+
process.stderr.write("Privacy check failed. Replace private identifiers with synthetic examples or review the local denylist.\n");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
process.stderr.write(`privacy check ok (${candidates.length} tracked/unignored files; ${denylist.length} local denylist entries)\n`);
|
|
52
|
+
|
|
53
|
+
function collectCandidateFiles(directory) {
|
|
54
|
+
try {
|
|
55
|
+
return execFileSync("git", ["-C", directory, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
56
|
+
encoding: "buffer",
|
|
57
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
58
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
59
|
+
}).toString("utf8").split("\0").filter(Boolean).sort();
|
|
60
|
+
} catch {
|
|
61
|
+
const excluded = new Set([".git", ".wrangler", "node_modules"]);
|
|
62
|
+
const files = [];
|
|
63
|
+
const stack = [""];
|
|
64
|
+
while (stack.length) {
|
|
65
|
+
const relative = stack.pop();
|
|
66
|
+
const absolute = path.join(directory, relative);
|
|
67
|
+
let entries;
|
|
68
|
+
try { entries = readdirSync(absolute, { withFileTypes: true }); } catch { continue; }
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (excluded.has(entry.name) || entry.name === ".privacy-denylist") continue;
|
|
71
|
+
const child = relative ? path.join(relative, entry.name) : entry.name;
|
|
72
|
+
if (entry.isDirectory()) stack.push(child);
|
|
73
|
+
else if (entry.isFile() || entry.isSymbolicLink()) files.push(child.split(path.sep).join("/"));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return files.sort();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function redactReportPath(relativePath, entries) {
|
|
81
|
+
let shown = String(relativePath);
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const expression = new RegExp(escapeRegExp(entry), "gi");
|
|
84
|
+
shown = shown.replace(expression, "<private>");
|
|
85
|
+
}
|
|
86
|
+
return shown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function escapeRegExp(value) {
|
|
90
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function scanBuiltIn(relativePath, text, out) {
|
|
94
|
+
const rules = [
|
|
95
|
+
["private key material", /-----BEGIN\s+(?:OPENSSH|RSA|EC|DSA)\s+PRIVATE\s+KEY-----/g],
|
|
96
|
+
["AWS access key", /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g],
|
|
97
|
+
["GitHub access token", /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g],
|
|
98
|
+
["API secret token", /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g],
|
|
99
|
+
["absolute macOS/Linux home path", /(?:^|[\s"'=(:,])\/(?:Users|home)\/([^/\s"'<>]+)\//gm],
|
|
100
|
+
["absolute Windows home path", /(?:^|[\s"'=(:,])\b[A-Za-z]:\\Users\\([^\\\s"'<>]+)\\/gm],
|
|
101
|
+
];
|
|
102
|
+
for (const [rule, expression] of rules) {
|
|
103
|
+
for (const match of text.matchAll(expression)) {
|
|
104
|
+
if (rule.includes("home path") && isSyntheticUser(String(match[1] || ""))) continue;
|
|
105
|
+
out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const sshLike = /\b[A-Za-z_][A-Za-z0-9._-]*@([A-Za-z0-9.-]+)\b/g;
|
|
109
|
+
for (const match of text.matchAll(sshLike)) {
|
|
110
|
+
if (isReservedExampleHost(match[1]) || !hasNearbySshContext(text, match.index || 0)) continue;
|
|
111
|
+
out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule: "non-example SSH user@host identifier" });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function scanDenylistPath(relativePath, entries, out) {
|
|
116
|
+
const lower = relativePath.toLocaleLowerCase("en-US");
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (lower.includes(entry)) out.push({ path: relativePath, line: 1, rule: "local private-identifier denylist match in file path" });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function scanDenylist(relativePath, text, entries, out) {
|
|
123
|
+
const lower = text.toLocaleLowerCase("en-US");
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
let offset = 0;
|
|
126
|
+
while ((offset = lower.indexOf(entry, offset)) !== -1) {
|
|
127
|
+
out.push({ path: relativePath, line: lineNumber(text, offset), rule: "local private-identifier denylist match" });
|
|
128
|
+
offset += Math.max(1, entry.length);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function loadDenylist(file) {
|
|
134
|
+
if (!existsSync(file)) return [];
|
|
135
|
+
let text;
|
|
136
|
+
try { text = readFileSync(file, "utf8"); } catch { return []; }
|
|
137
|
+
return [...new Set(text.split(/\r?\n/)
|
|
138
|
+
.map((line) => line.trim())
|
|
139
|
+
.filter((line) => line && !line.startsWith("#") && line.length >= 3)
|
|
140
|
+
.map((line) => line.toLocaleLowerCase("en-US")))];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isSyntheticUser(value) {
|
|
144
|
+
return /^(?:user|username|example|test|name|home|runner|workspace|tmp|private|path|package)$/i.test(value)
|
|
145
|
+
|| /^<[^>]+>$/.test(value);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function hasNearbySshContext(text, offset) {
|
|
149
|
+
const lineStart = Math.max(0, text.lastIndexOf("\n", offset - 1));
|
|
150
|
+
let start = lineStart;
|
|
151
|
+
for (let count = 0; count < 4 && start > 0; count += 1) start = Math.max(0, text.lastIndexOf("\n", start - 1));
|
|
152
|
+
let end = offset;
|
|
153
|
+
for (let count = 0; count < 4 && end < text.length; count += 1) {
|
|
154
|
+
const next = text.indexOf("\n", end + 1);
|
|
155
|
+
end = next === -1 ? text.length : next;
|
|
156
|
+
}
|
|
157
|
+
return /\b(?:ssh|scp|sftp)\b/i.test(text.slice(start, end));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isReservedExampleHost(host) {
|
|
161
|
+
const value = String(host || "").toLowerCase().replace(/\.$/, "");
|
|
162
|
+
return value === "localhost"
|
|
163
|
+
|| value === "127.0.0.1"
|
|
164
|
+
|| value === "::1"
|
|
165
|
+
|| value === "example"
|
|
166
|
+
|| value.endsWith(".example")
|
|
167
|
+
|| value.endsWith(".example.com")
|
|
168
|
+
|| value.endsWith(".example.net")
|
|
169
|
+
|| value.endsWith(".example.org")
|
|
170
|
+
|| value.endsWith(".invalid");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function lineNumber(text, offset) {
|
|
174
|
+
let line = 1;
|
|
175
|
+
for (let index = 0; index < offset; index += 1) if (text.charCodeAt(index) === 10) line += 1;
|
|
176
|
+
return line;
|
|
177
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
const VERSION_TAG = /^v(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
7
|
+
|
|
8
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
9
|
+
const current = parseVersion(String(pkg.version || ""));
|
|
10
|
+
if (!current) fail("package.json contains an invalid version");
|
|
11
|
+
|
|
12
|
+
const tags = git(["tag", "--merged", "HEAD", "--sort=-version:refname", "--list", "v[0-9]*"])
|
|
13
|
+
.split("\n")
|
|
14
|
+
.map((value) => value.trim())
|
|
15
|
+
.filter((value) => VERSION_TAG.test(value));
|
|
16
|
+
if (!tags.length) {
|
|
17
|
+
process.stderr.write("release impact check skipped: no reachable version tag\n");
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const latestTag = tags[0];
|
|
22
|
+
const latest = parseVersion(latestTag.slice(1));
|
|
23
|
+
const changed = new Set([
|
|
24
|
+
...lines(git(["diff", "--name-only", latestTag, "--"])),
|
|
25
|
+
...lines(git(["ls-files", "--others", "--exclude-standard"])),
|
|
26
|
+
]);
|
|
27
|
+
const relevant = [...changed].sort();
|
|
28
|
+
|
|
29
|
+
if (!relevant.length) {
|
|
30
|
+
process.stderr.write(`release impact check ok: no release-relevant changes since ${latestTag}\n`);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
if (compareVersions(current, latest) <= 0) {
|
|
34
|
+
fail(`release-relevant changes exist since ${latestTag}, but package.json is still ${pkg.version}; bump the npm version and add a CHANGELOG section before merging`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const changelog = readFileSync(new URL("../CHANGELOG.md", import.meta.url), "utf8");
|
|
38
|
+
const heading = new RegExp(`^## ${escapeRegExp(pkg.version)}(?:\\s+-[^\\n]*)?$`, "m");
|
|
39
|
+
if (!heading.test(changelog)) fail(`CHANGELOG.md has no section for ${pkg.version}`);
|
|
40
|
+
|
|
41
|
+
process.stderr.write(`release impact check ok: ${relevant.length} release-relevant file(s) since ${latestTag}; next npm version ${pkg.version}\n`);
|
|
42
|
+
|
|
43
|
+
function git(args) {
|
|
44
|
+
try {
|
|
45
|
+
return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const detail = String(error?.stderr || error?.message || error).trim();
|
|
48
|
+
fail(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function lines(value) {
|
|
53
|
+
return value ? value.split("\n").map((item) => item.trim()).filter(Boolean) : [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseVersion(value) {
|
|
57
|
+
const match = String(value).match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
58
|
+
return match ? { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] || "" } : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function compareVersions(left, right) {
|
|
62
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
63
|
+
if (left[key] !== right[key]) return left[key] - right[key];
|
|
64
|
+
}
|
|
65
|
+
if (left.prerelease === right.prerelease) return 0;
|
|
66
|
+
if (!left.prerelease) return 1;
|
|
67
|
+
if (!right.prerelease) return -1;
|
|
68
|
+
return left.prerelease.localeCompare(right.prerelease);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function escapeRegExp(value) {
|
|
72
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function fail(message) {
|
|
76
|
+
process.stderr.write(`release impact check failed: ${message}\n`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|