@akira-tl/forgerelay 0.10.0 → 0.10.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 +7 -0
- package/README.md +3 -2
- package/capabilities/shell-processes/GUIDE.md +1 -1
- package/dist/cli/init.js +14 -0
- package/dist/mcp/process/process-platform.js +12 -2
- package/dist/mcp/process/process-sessions.js +1 -1
- package/dist/runtime/instructions/powershell-skill.js +65 -0
- package/dist/runtime/shell/command-shell-runtime.js +54 -11
- package/docs/gotchas.md +5 -3
- package/package.json +3 -2
- package/scripts/ci/pwsh-acceptance.mjs +308 -0
- package/scripts/ci/verify.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.10.1] - 2026-09-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added native PowerShell 7 (`pwsh`) execution for Agent commands, Hooks, and PTY sessions with one versioned Command Shell Runtime, no-profile invocation, PowerShell-native command semantics, and Windows packaged-shim acceptance in cloud CI.
|
|
12
|
+
- Added an exact-release-tag PowerShell guidance Skill seeded into the ForgeRelay config Skills directory without bundling prompt templates into the npm package or overwriting user edits.
|
|
13
|
+
|
|
7
14
|
## [0.10.0] - 2026-09-05
|
|
8
15
|
|
|
9
16
|
### Added
|
package/README.md
CHANGED
|
@@ -295,8 +295,9 @@ See [Security Model](docs/security.md) for the full boundary and threat model.
|
|
|
295
295
|
| --- | --- | --- |
|
|
296
296
|
| Linux | Supported | Requires Node, npm, Git, and Bash. |
|
|
297
297
|
| macOS | Supported | Requires Node, npm, Git, and Bash. |
|
|
298
|
-
| Windows with
|
|
299
|
-
| Windows
|
|
298
|
+
| Windows with PowerShell 7 (`pwsh`) | Supported | Agent commands and Hooks use one native `pwsh` runtime without loading the user profile. |
|
|
299
|
+
| Windows with Git Bash, WSL, MSYS2, or Cygwin Bash | Supported | Bash remains available as a compatibility path. |
|
|
300
|
+
| Windows PowerShell 5.1 (`powershell.exe`) or `cmd.exe` only | Not supported yet | These runtimes are separate follow-up stages. |
|
|
300
301
|
|
|
301
302
|
You can check the local runtime with:
|
|
302
303
|
|
|
@@ -48,6 +48,6 @@ PTY 依赖 optional `node-pty`。缺少该依赖时 ForgeRelay 会明确报错
|
|
|
48
48
|
|
|
49
49
|
## Platform notes
|
|
50
50
|
|
|
51
|
-
ForgeRelay
|
|
51
|
+
ForgeRelay 在 Windows 上支持 PowerShell 7 (`pwsh`) 作为原生 Command Shell Runtime;非 TTY Agent 命令与 Hooks 使用同一个 `pwsh`,禁止加载用户 profile,PTY 也复用同一 executable。公共工具名仍保留为 `bash` 以兼容 Host contract,实际命令语言以 ForgeRelay 报告的 Command Shell Runtime 为准。Windows PowerShell 5.1 (`powershell.exe`) 与 `cmd.exe` 仍属于后续 runtime 阶段;Git Bash、WSL、MSYS2 或 Cygwin Bash 仍可作为 Bash 兼容路径。用 `forgerelay doctor` 检查当前 shell identity、version 与 executable。
|
|
52
52
|
|
|
53
53
|
Shell 可以作为用户开发任务的一部分修改普通项目文件,但始终受 ForgeRelay core mutation/safety contract 约束。涉及 privileged OS files、credentials、configuration 或外部硬件持久写入时,不要用本指南替代 core authorization/safety 规则。
|
package/dist/cli/init.js
CHANGED
|
@@ -4,6 +4,7 @@ import { publicEndpointUrl } from "../mcp/oauth/public-url.js";
|
|
|
4
4
|
import { expandHomePath } from "../mcp/filesystem/roots.js";
|
|
5
5
|
import { detectLauncherCommandShell, } from "../runtime/shell/command-shell-runtime.js";
|
|
6
6
|
import { seedShellInstructionFiles, shellInstructionFamiliesToSeed, } from "../runtime/instructions/shell-instructions.js";
|
|
7
|
+
import { seedPowerShellSkill, } from "../runtime/instructions/powershell-skill.js";
|
|
7
8
|
import { installManagedLanguageServers, installedManagedLanguageServers, managedLanguageServerOptions, } from "../lsp/runtime/managed-language-servers.js";
|
|
8
9
|
import { generateInstanceId, generateOwnerToken, loadForgeRelayFiles, resolveSubagentsFlag, writeForgeRelayAuth, writeForgeRelayConfig, } from "../runtime/config/user-config.js";
|
|
9
10
|
import { classifyClientFacingBaseUrl, compactPublicBaseUrlConfig, hasInsecureLanBaseUrl, isLoopbackBindAddress, normalizePublicBaseUrlsInput, setupBindAddress, SetupCancelledError, textPrompt, validateHttpsProxyBaseUrls, validateLanClientFacingBaseUrls, validatePort, } from "./setup-support.js";
|
|
@@ -159,6 +160,7 @@ export async function runInit({ force, version }) {
|
|
|
159
160
|
const shellInstructionFamilies = shellInstructionFamiliesToSeed(process.platform, commandShell.family);
|
|
160
161
|
let shellInstructions = files.config.shellInstructions;
|
|
161
162
|
let shellInstructionSeedResults = [];
|
|
163
|
+
let powerShellSkillSeedResult;
|
|
162
164
|
if (shellInstructionFamilies.length > 0) {
|
|
163
165
|
const shellInstructionsAnswer = await prompts.confirm({
|
|
164
166
|
message: "Load ForgeRelay-provided extra Instructions for this command shell?",
|
|
@@ -177,6 +179,15 @@ export async function runInit({ force, version }) {
|
|
|
177
179
|
prompts.note(failedShellInstructions.map((result) => `${result.family}: ${result.error ?? "download failed"}\n${result.sourceUrl}`).join("\n\n"), "Shell Instructions unavailable");
|
|
178
180
|
}
|
|
179
181
|
}
|
|
182
|
+
if (commandShell.family === "pwsh") {
|
|
183
|
+
powerShellSkillSeedResult = await seedPowerShellSkill({
|
|
184
|
+
configDir: files.dir,
|
|
185
|
+
version,
|
|
186
|
+
});
|
|
187
|
+
if (powerShellSkillSeedResult.status === "failed") {
|
|
188
|
+
prompts.note(`${powerShellSkillSeedResult.error ?? "download failed"}\n${powerShellSkillSeedResult.sourceUrl}`, "PowerShell Skill unavailable");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
180
191
|
const installedManaged = installedManagedLanguageServers(files.dir);
|
|
181
192
|
const selectedManaged = await prompts.multiselect({
|
|
182
193
|
message: "Which Language Servers should ForgeRelay manage with npm?",
|
|
@@ -244,6 +255,9 @@ export async function runInit({ force, version }) {
|
|
|
244
255
|
...shellInstructionSeedResults.map((result) => `Shell Instructions ${result.family}: ${result.status} (${result.path})`),
|
|
245
256
|
]
|
|
246
257
|
: []),
|
|
258
|
+
...(powerShellSkillSeedResult
|
|
259
|
+
? [`PowerShell Skill: ${powerShellSkillSeedResult.status} (${powerShellSkillSeedResult.path})`]
|
|
260
|
+
: []),
|
|
247
261
|
...clientFacingBaseUrls.map((baseUrl, index) => `${index === 0 ? "Client-facing MCP URL" : "Client-facing MCP alias"}: ${publicEndpointUrl(baseUrl, "mcp").toString()}`),
|
|
248
262
|
];
|
|
249
263
|
if (networkMode === "ssh") {
|
|
@@ -14,7 +14,7 @@ const defaultProcessTreeRuntime = {
|
|
|
14
14
|
export function resolveShellCommand(command, platform = process.platform, environment = process.env) {
|
|
15
15
|
return resolveShellCommandForRuntime(command, resolveCompatibilityCommandShellRuntime(platform, environment));
|
|
16
16
|
}
|
|
17
|
-
export function resolveShellCommandForRuntime(command, runtime) {
|
|
17
|
+
export function resolveShellCommandForRuntime(command, runtime, options = {}) {
|
|
18
18
|
// Agent and Hook commands must not source the user's interactive/login shell
|
|
19
19
|
// configuration. ForgeRelay already inherits PATH and other environment from
|
|
20
20
|
// the process that launched the server; re-running zsh/bash as a login shell
|
|
@@ -35,8 +35,18 @@ export function resolveShellCommandForRuntime(command, runtime) {
|
|
|
35
35
|
return { executable: runtime.executable, args: ["-f", "-c", command] };
|
|
36
36
|
case "sh":
|
|
37
37
|
return { executable: runtime.executable, args: ["-c", command] };
|
|
38
|
-
case "fish":
|
|
39
38
|
case "pwsh":
|
|
39
|
+
return {
|
|
40
|
+
executable: runtime.executable,
|
|
41
|
+
args: [
|
|
42
|
+
"-NoLogo",
|
|
43
|
+
"-NoProfile",
|
|
44
|
+
...(options.interactive ? [] : ["-NonInteractive"]),
|
|
45
|
+
"-Command",
|
|
46
|
+
command,
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
case "fish":
|
|
40
50
|
case "powershell":
|
|
41
51
|
throw new Error(`Command shell runtime ${runtime.family} is identified but native execution support is not enabled in this release stage. ForgeRelay will not silently execute the command through another shell.`);
|
|
42
52
|
}
|
|
@@ -470,7 +470,7 @@ export class ProcessManager {
|
|
|
470
470
|
catch {
|
|
471
471
|
throw new Error("PTY support requires the optional node-pty dependency.");
|
|
472
472
|
}
|
|
473
|
-
const shell = resolveShellCommandForRuntime(input.command, this.commandShellRuntime);
|
|
473
|
+
const shell = resolveShellCommandForRuntime(input.command, this.commandShellRuntime, { interactive: true });
|
|
474
474
|
let pty;
|
|
475
475
|
try {
|
|
476
476
|
pty = nodePty.spawn(shell.executable, shell.args, {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
const TEMPLATE_REPOSITORY_RAW = "https://raw.githubusercontent.com/Akira-TL/forgerelay";
|
|
5
|
+
const POWERSHELL_SKILL_PATH = "templates/skills/powershell/SKILL.md";
|
|
6
|
+
export function powerShellSkillPath(configDir) {
|
|
7
|
+
return join(resolve(configDir), "skills", "powershell", "SKILL.md");
|
|
8
|
+
}
|
|
9
|
+
export function powerShellSkillTemplateUrl(version) {
|
|
10
|
+
const normalizedVersion = version.trim().replace(/^v/, "");
|
|
11
|
+
if (!normalizedVersion)
|
|
12
|
+
throw new Error("ForgeRelay version is required to fetch the PowerShell Skill.");
|
|
13
|
+
return `${TEMPLATE_REPOSITORY_RAW}/v${encodeURIComponent(normalizedVersion)}/${POWERSHELL_SKILL_PATH}`;
|
|
14
|
+
}
|
|
15
|
+
export async function seedPowerShellSkill(options) {
|
|
16
|
+
const path = powerShellSkillPath(options.configDir);
|
|
17
|
+
const sourceUrl = powerShellSkillTemplateUrl(options.version);
|
|
18
|
+
if (!options.reset && await fileExists(path)) {
|
|
19
|
+
return { path, status: "preserved", sourceUrl };
|
|
20
|
+
}
|
|
21
|
+
await mkdir(join(resolve(options.configDir), "skills", "powershell"), {
|
|
22
|
+
recursive: true,
|
|
23
|
+
mode: 0o700,
|
|
24
|
+
});
|
|
25
|
+
const fetchText = options.fetchText ?? fetchSkillText;
|
|
26
|
+
try {
|
|
27
|
+
const content = await fetchText(sourceUrl);
|
|
28
|
+
if (!content.trim())
|
|
29
|
+
throw new Error("template response was empty");
|
|
30
|
+
await writeFile(path, content.endsWith("\n") ? content : `${content}\n`, {
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
mode: 0o600,
|
|
33
|
+
flag: options.reset ? "w" : "wx",
|
|
34
|
+
});
|
|
35
|
+
return { path, status: "created", sourceUrl };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (!options.reset && await fileExists(path)) {
|
|
39
|
+
return { path, status: "preserved", sourceUrl };
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
path,
|
|
43
|
+
status: "failed",
|
|
44
|
+
sourceUrl,
|
|
45
|
+
error: error instanceof Error ? error.message : String(error),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function fetchSkillText(url) {
|
|
50
|
+
const response = await fetch(url, {
|
|
51
|
+
headers: { "user-agent": "forgerelay-powershell-skill" },
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok)
|
|
54
|
+
throw new Error(`HTTP ${response.status} while fetching ${url}`);
|
|
55
|
+
return response.text();
|
|
56
|
+
}
|
|
57
|
+
async function fileExists(path) {
|
|
58
|
+
try {
|
|
59
|
+
await access(path, fsConstants.F_OK);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -21,23 +21,31 @@ export function resolveCommandShellRuntime(input) {
|
|
|
21
21
|
* strict precedence resolver above: new configuration should call
|
|
22
22
|
* resolveCommandShellRuntime instead of inventing another fallback.
|
|
23
23
|
*/
|
|
24
|
-
export function resolveConfiguredCommandShellRuntime(preference, platform = process.platform, environment = process.env) {
|
|
24
|
+
export function resolveConfiguredCommandShellRuntime(preference, platform = process.platform, environment = process.env, metadata = {}) {
|
|
25
25
|
const legacyExplicit = environment.FORGERELAY_COMMAND_SHELL?.trim();
|
|
26
|
+
let runtime;
|
|
26
27
|
if (legacyExplicit) {
|
|
27
28
|
const explicit = normalizeCommandShellSelection({ executable: legacyExplicit }, platform, environment, true);
|
|
28
|
-
|
|
29
|
+
runtime = resolveCommandShellRuntime({ explicit });
|
|
29
30
|
}
|
|
30
|
-
if (!preference)
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
else if (!preference) {
|
|
32
|
+
runtime = resolveCompatibilityCommandShellRuntime(platform, environment);
|
|
33
|
+
}
|
|
34
|
+
else if (preference.mode === "pinned") {
|
|
33
35
|
const explicit = normalizeCommandShellSelection(preference, platform, environment, true);
|
|
34
|
-
|
|
36
|
+
runtime = resolveCommandShellRuntime({ explicit });
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const launcher = detectLauncherCommandShell({ platform, environment });
|
|
40
|
+
if (launcher) {
|
|
41
|
+
runtime = resolveCommandShellRuntime({ launcher });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const recordedFallback = normalizeCommandShellSelection(preference, platform, environment, true);
|
|
45
|
+
runtime = resolveCommandShellRuntime({ recordedFallback });
|
|
46
|
+
}
|
|
35
47
|
}
|
|
36
|
-
|
|
37
|
-
if (launcher)
|
|
38
|
-
return resolveCommandShellRuntime({ launcher });
|
|
39
|
-
const recordedFallback = normalizeCommandShellSelection(preference, platform, environment, true);
|
|
40
|
-
return resolveCommandShellRuntime({ recordedFallback });
|
|
48
|
+
return enrichConfiguredCommandShellRuntime(runtime, metadata.probePowerShell7Version ?? probePowerShell7Version);
|
|
41
49
|
}
|
|
42
50
|
export function resolveCompatibilityCommandShellRuntime(platform = process.platform, environment = process.env) {
|
|
43
51
|
if (platform === "win32") {
|
|
@@ -121,6 +129,41 @@ export function formatCommandShellRuntime(runtime) {
|
|
|
121
129
|
export function snapshotCommandShellRuntime(runtime) {
|
|
122
130
|
return { ...runtime, capabilities: [...runtime.capabilities] };
|
|
123
131
|
}
|
|
132
|
+
function enrichConfiguredCommandShellRuntime(runtime, powerShell7VersionProbe) {
|
|
133
|
+
if (runtime.family !== "pwsh")
|
|
134
|
+
return runtime;
|
|
135
|
+
const version = powerShell7VersionProbe(runtime.executable).trim();
|
|
136
|
+
const major = Number.parseInt(version.split(".", 1)[0] ?? "", 10);
|
|
137
|
+
if (!version || !Number.isInteger(major)) {
|
|
138
|
+
throw new Error(`Unable to determine PowerShell 7 version from ${runtime.executable}.`);
|
|
139
|
+
}
|
|
140
|
+
if (major < 7) {
|
|
141
|
+
throw new Error(`Configured pwsh runtime must be PowerShell 7 or newer; detected ${version} at ${runtime.executable}.`);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
...runtime,
|
|
145
|
+
version,
|
|
146
|
+
capabilities: Array.from(new Set([
|
|
147
|
+
...runtime.capabilities,
|
|
148
|
+
"profile-isolation",
|
|
149
|
+
"pipeline-chain-operators",
|
|
150
|
+
])),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function probePowerShell7Version(executable) {
|
|
154
|
+
const result = spawnSync(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "$PSVersionTable.PSVersion.ToString()"], { encoding: "utf8", windowsHide: true, timeout: 5_000 });
|
|
155
|
+
if (result.error) {
|
|
156
|
+
throw new Error(`Unable to query PowerShell 7 version from ${executable}: ${result.error.message}`);
|
|
157
|
+
}
|
|
158
|
+
if (result.status !== 0) {
|
|
159
|
+
const detail = result.stderr?.trim();
|
|
160
|
+
throw new Error(`Unable to query PowerShell 7 version from ${executable}: exited with code ${result.status ?? "unknown"}${detail ? `: ${detail}` : ""}.`);
|
|
161
|
+
}
|
|
162
|
+
const version = result.stdout?.trim();
|
|
163
|
+
if (!version)
|
|
164
|
+
throw new Error(`Unable to query PowerShell 7 version from ${executable}: no version was reported.`);
|
|
165
|
+
return version;
|
|
166
|
+
}
|
|
124
167
|
export function commandShellAgentInstruction(runtime) {
|
|
125
168
|
if (runtime.family === "bash")
|
|
126
169
|
return "";
|
package/docs/gotchas.md
CHANGED
|
@@ -162,10 +162,12 @@ ForgeRelay does not intentionally leave the source checkout in a merge-conflict
|
|
|
162
162
|
|
|
163
163
|
## Windows shell commands fail
|
|
164
164
|
|
|
165
|
-
ForgeRelay
|
|
166
|
-
|
|
165
|
+
ForgeRelay supports PowerShell 7 (`pwsh`) as a native Windows Command Shell Runtime.
|
|
166
|
+
Agent commands and Hooks share that runtime and do not load the user PowerShell profile.
|
|
167
|
+
Windows PowerShell 5.1 (`powershell.exe`) and `cmd.exe` remain separate follow-up runtimes.
|
|
168
|
+
Git Bash, WSL, MSYS2, and Cygwin Bash also remain supported compatibility paths.
|
|
167
169
|
|
|
168
|
-
|
|
170
|
+
Check the selected runtime with:
|
|
169
171
|
|
|
170
172
|
```bash
|
|
171
173
|
forgerelay doctor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Local development control plane for MCP coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Akira-TL/forgerelay#readme",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"debug:accept:relay": "node scripts/debug/relay-accept.mjs",
|
|
43
43
|
"traffic:audit": "node --import tsx scripts/debug/traffic/traffic-audit.mjs",
|
|
44
44
|
"lsp:interop": "node scripts/lsp/interop.mjs",
|
|
45
|
+
"pwsh:accept": "node scripts/ci/pwsh-acceptance.mjs",
|
|
45
46
|
"wiki:check": "node scripts/wiki/sync.mjs check",
|
|
46
47
|
"wiki:publish": "node scripts/wiki/sync.mjs publish",
|
|
47
48
|
"architecture:check": "node scripts/ci/architecture.mjs",
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"release:push-ready": "node scripts/release/push-ready.mjs",
|
|
53
54
|
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
|
|
54
55
|
"start": "node dist/cli.js serve",
|
|
55
|
-
"test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/workspaces/relay/tests/recovery.test.ts && tsx src/workspaces/relay/tests/checkpoint.test.ts && tsx src/runtime/config/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/runtime/state/runtime-lease.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/mcp/server/workspace/workspace-recovery.test.ts && tsx src/mcp/server/workspace/workspace-checkpoint.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/maintenance.test.ts && tsx src/cli/maintenance-prune.test.ts && tsx src/cli/cli.test.ts",
|
|
56
|
+
"test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/workspaces/relay/tests/recovery.test.ts && tsx src/workspaces/relay/tests/checkpoint.test.ts && tsx src/runtime/config/config.test.ts && tsx src/runtime/instructions/powershell-skill.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/runtime/state/runtime-lease.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/mcp/server/workspace/workspace-recovery.test.ts && tsx src/mcp/server/workspace/workspace-checkpoint.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/maintenance.test.ts && tsx src/cli/maintenance-prune.test.ts && tsx src/cli/cli.test.ts",
|
|
56
57
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
57
58
|
"release:check": "node scripts/release-version.mjs check",
|
|
58
59
|
"release:tag-check": "node scripts/release-version.mjs tag",
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
if (process.platform !== "win32") {
|
|
11
|
+
console.log("PowerShell 7 packaged acceptance skipped outside Windows.");
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const npmCli = process.env.npm_execpath;
|
|
16
|
+
if (!npmCli) throw new Error("pwsh acceptance must run through npm so npm_execpath is available");
|
|
17
|
+
|
|
18
|
+
const root = await mkdtemp(join(tmpdir(), "forgerelay-pwsh-acceptance-"));
|
|
19
|
+
try {
|
|
20
|
+
const pwsh = resolvePowerShell7();
|
|
21
|
+
const version = powerShellVersion(pwsh);
|
|
22
|
+
const major = Number.parseInt(version.split(".", 1)[0] ?? "", 10);
|
|
23
|
+
assert.ok(Number.isInteger(major) && major >= 7, `PowerShell 7 acceptance requires pwsh >= 7; got ${version}`);
|
|
24
|
+
|
|
25
|
+
const runtime = {
|
|
26
|
+
family: "pwsh",
|
|
27
|
+
executable: pwsh,
|
|
28
|
+
source: "explicit",
|
|
29
|
+
version,
|
|
30
|
+
capabilities: [
|
|
31
|
+
"powershell-command-language",
|
|
32
|
+
"powershell-core",
|
|
33
|
+
"profile-isolation",
|
|
34
|
+
"pipeline-chain-operators",
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
await exerciseAgentRuntime(runtime);
|
|
39
|
+
await exerciseHookRuntime(runtime);
|
|
40
|
+
await exercisePackagedPowerShellShim(pwsh, version);
|
|
41
|
+
|
|
42
|
+
console.log(`PowerShell 7 acceptance passed with ${pwsh} (${version}).`);
|
|
43
|
+
} finally {
|
|
44
|
+
await rm(root, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolvePowerShell7() {
|
|
48
|
+
const result = spawnSync("where.exe", ["pwsh.exe"], {
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
windowsHide: true,
|
|
51
|
+
});
|
|
52
|
+
if (result.error || result.status !== 0) {
|
|
53
|
+
throw new Error("Windows release acceptance requires PowerShell 7 (pwsh.exe) on PATH.");
|
|
54
|
+
}
|
|
55
|
+
const executable = result.stdout?.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
56
|
+
if (!executable) throw new Error("where.exe reported no pwsh.exe path.");
|
|
57
|
+
return executable;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function powerShellVersion(executable) {
|
|
61
|
+
const result = spawnSync(
|
|
62
|
+
executable,
|
|
63
|
+
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "$PSVersionTable.PSVersion.ToString()"],
|
|
64
|
+
{ encoding: "utf8", windowsHide: true },
|
|
65
|
+
);
|
|
66
|
+
if (result.error || result.status !== 0) {
|
|
67
|
+
throw new Error(`Unable to query ${executable} version: ${result.error?.message ?? result.stderr ?? result.status}`);
|
|
68
|
+
}
|
|
69
|
+
const version = result.stdout?.trim();
|
|
70
|
+
if (!version) throw new Error(`${executable} did not report a version.`);
|
|
71
|
+
return version;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function exerciseAgentRuntime(runtime) {
|
|
75
|
+
const { ProcessManager } = await import("../../dist/mcp/process/process-sessions.js");
|
|
76
|
+
const manager = new ProcessManager({ commandShellRuntime: runtime });
|
|
77
|
+
const originalMarker = process.env.FORGERELAY_PWSH_ACCEPTANCE;
|
|
78
|
+
process.env.FORGERELAY_PWSH_ACCEPTANCE = "inherited environment";
|
|
79
|
+
try {
|
|
80
|
+
const node = powerShellLiteral(process.execPath);
|
|
81
|
+
const semantics = await manager.start({
|
|
82
|
+
workspaceId: "pwsh-agent",
|
|
83
|
+
cwd: process.cwd(),
|
|
84
|
+
command: [
|
|
85
|
+
'Write-Output "env=$env:FORGERELAY_PWSH_ACCEPTANCE"',
|
|
86
|
+
'1,2,3 | Measure-Object -Sum | ForEach-Object { Write-Output "sum=$($_.Sum)" }',
|
|
87
|
+
`$exe = ${node}`,
|
|
88
|
+
"& $exe -e 'console.log(JSON.stringify(process.argv.slice(1)))' 'native arg with spaces' 'quote\"inside' 'unicode-雪'",
|
|
89
|
+
"if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }",
|
|
90
|
+
"$redirect = Join-Path $env:TEMP 'forgerelay-pwsh-redirection.txt'",
|
|
91
|
+
"'redirected-through-pwsh' > $redirect",
|
|
92
|
+
"Get-Content $redirect",
|
|
93
|
+
"Remove-Item $redirect -Force",
|
|
94
|
+
].join("; "),
|
|
95
|
+
yieldTimeMs: 10_000,
|
|
96
|
+
});
|
|
97
|
+
assert.equal(semantics.running, false);
|
|
98
|
+
assert.equal(semantics.exitCode, 0);
|
|
99
|
+
assert.match(semantics.output, /env=inherited environment/);
|
|
100
|
+
assert.match(semantics.output, /sum=6/);
|
|
101
|
+
assert.match(semantics.output, /\["native arg with spaces","quote\\\"inside","unicode-雪"\]/);
|
|
102
|
+
assert.match(semantics.output, /redirected-through-pwsh/);
|
|
103
|
+
|
|
104
|
+
const errorSemantics = await manager.start({
|
|
105
|
+
workspaceId: "pwsh-agent",
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
command: [
|
|
108
|
+
"Write-Error 'nonterminating-pwsh-error'",
|
|
109
|
+
"Write-Output 'continued-after-nonterminating-error'",
|
|
110
|
+
"try { Get-Item 'forgerelay-definitely-missing-item' -ErrorAction Stop; exit 31 } catch { Write-Output 'terminating-error-caught' }",
|
|
111
|
+
].join("; "),
|
|
112
|
+
yieldTimeMs: 10_000,
|
|
113
|
+
});
|
|
114
|
+
assert.equal(errorSemantics.exitCode, 0);
|
|
115
|
+
assert.match(errorSemantics.output, /nonterminating-pwsh-error/);
|
|
116
|
+
assert.match(errorSemantics.output, /continued-after-nonterminating-error/);
|
|
117
|
+
assert.match(errorSemantics.output, /terminating-error-caught/);
|
|
118
|
+
|
|
119
|
+
const nativeExit = await manager.start({
|
|
120
|
+
workspaceId: "pwsh-agent",
|
|
121
|
+
cwd: process.cwd(),
|
|
122
|
+
command: `$exe = ${node}; & $exe -e 'process.exit(7)'; exit $LASTEXITCODE`,
|
|
123
|
+
yieldTimeMs: 10_000,
|
|
124
|
+
});
|
|
125
|
+
assert.equal(nativeExit.exitCode, 7);
|
|
126
|
+
|
|
127
|
+
const background = await manager.start({
|
|
128
|
+
workspaceId: "pwsh-agent",
|
|
129
|
+
cwd: process.cwd(),
|
|
130
|
+
command: "Write-Output 'pwsh-background-start'; Start-Sleep -Milliseconds 250; Write-Output 'pwsh-background-done'",
|
|
131
|
+
yieldTimeMs: 5,
|
|
132
|
+
});
|
|
133
|
+
assert.equal(background.running, true);
|
|
134
|
+
assert.ok(background.processId);
|
|
135
|
+
const completed = await manager.write({
|
|
136
|
+
workspaceId: "pwsh-agent",
|
|
137
|
+
processId: background.processId,
|
|
138
|
+
yieldTimeMs: 5_000,
|
|
139
|
+
});
|
|
140
|
+
assert.equal(completed.running, false);
|
|
141
|
+
assert.equal(completed.exitCode, 0);
|
|
142
|
+
assert.match(completed.output, /pwsh-background-done/);
|
|
143
|
+
|
|
144
|
+
const timedOut = await manager.start({
|
|
145
|
+
workspaceId: "pwsh-agent",
|
|
146
|
+
cwd: process.cwd(),
|
|
147
|
+
command: "Start-Sleep -Seconds 30",
|
|
148
|
+
yieldTimeMs: 5_000,
|
|
149
|
+
timeoutMs: 100,
|
|
150
|
+
});
|
|
151
|
+
assert.equal(timedOut.running, false);
|
|
152
|
+
assert.equal(timedOut.timedOut, true);
|
|
153
|
+
|
|
154
|
+
const interruptible = await manager.start({
|
|
155
|
+
workspaceId: "pwsh-agent",
|
|
156
|
+
cwd: process.cwd(),
|
|
157
|
+
command: "Write-Output 'pwsh-interrupt-ready'; Start-Sleep -Seconds 30",
|
|
158
|
+
yieldTimeMs: 5,
|
|
159
|
+
});
|
|
160
|
+
assert.equal(interruptible.running, true);
|
|
161
|
+
assert.ok(interruptible.processId);
|
|
162
|
+
const interrupted = await manager.write({
|
|
163
|
+
workspaceId: "pwsh-agent",
|
|
164
|
+
processId: interruptible.processId,
|
|
165
|
+
chars: "\u0003",
|
|
166
|
+
yieldTimeMs: 5_000,
|
|
167
|
+
});
|
|
168
|
+
assert.equal(interrupted.running, false);
|
|
169
|
+
|
|
170
|
+
const pty = await manager.start({
|
|
171
|
+
workspaceId: "pwsh-agent",
|
|
172
|
+
cwd: process.cwd(),
|
|
173
|
+
command: "Write-Output 'pwsh-pty-ok'",
|
|
174
|
+
tty: true,
|
|
175
|
+
yieldTimeMs: 10_000,
|
|
176
|
+
});
|
|
177
|
+
assert.equal(pty.running, false);
|
|
178
|
+
assert.equal(pty.exitCode, 0);
|
|
179
|
+
assert.match(pty.output, /pwsh-pty-ok/);
|
|
180
|
+
} finally {
|
|
181
|
+
if (originalMarker === undefined) delete process.env.FORGERELAY_PWSH_ACCEPTANCE;
|
|
182
|
+
else process.env.FORGERELAY_PWSH_ACCEPTANCE = originalMarker;
|
|
183
|
+
manager.shutdown();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function exerciseHookRuntime(runtime) {
|
|
188
|
+
const { HookRunner, parseHookConfig } = await import("../../dist/mcp/hooks/hooks.js");
|
|
189
|
+
const logging = {
|
|
190
|
+
level: "silent",
|
|
191
|
+
format: "json",
|
|
192
|
+
requests: false,
|
|
193
|
+
assets: false,
|
|
194
|
+
toolCalls: false,
|
|
195
|
+
shellCommands: false,
|
|
196
|
+
trustProxy: false,
|
|
197
|
+
};
|
|
198
|
+
const runner = new HookRunner(
|
|
199
|
+
parseHookConfig({
|
|
200
|
+
BeforeTool: [{
|
|
201
|
+
handlers: [{
|
|
202
|
+
name: "PowerShell policy",
|
|
203
|
+
command: "if ($env:FORGERELAY_WORKSPACE_ID -ne 'pwsh-hook') { exit 19 }; Write-Error 'pwsh policy denied'; exit 13",
|
|
204
|
+
}],
|
|
205
|
+
}],
|
|
206
|
+
}),
|
|
207
|
+
logging,
|
|
208
|
+
process.env,
|
|
209
|
+
undefined,
|
|
210
|
+
runtime,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
await assert.rejects(
|
|
214
|
+
() => runner.run("BeforeTool", {
|
|
215
|
+
workspaceId: "pwsh-hook",
|
|
216
|
+
workspaceRoot: process.cwd(),
|
|
217
|
+
workspaceMode: "checkout",
|
|
218
|
+
payload: { tool: "bash", command: "Write-Output 'agent command'" },
|
|
219
|
+
}),
|
|
220
|
+
/PowerShell policy exited with code 13: .*pwsh policy denied/i,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function exercisePackagedPowerShellShim(pwsh, expectedVersion) {
|
|
225
|
+
const artifactDir = join(root, "artifact");
|
|
226
|
+
const prefix = join(root, "prefix");
|
|
227
|
+
const configDir = join(root, "config");
|
|
228
|
+
const stateDir = join(root, "state");
|
|
229
|
+
await Promise.all([
|
|
230
|
+
mkdir(artifactDir, { recursive: true }),
|
|
231
|
+
mkdir(prefix, { recursive: true }),
|
|
232
|
+
mkdir(configDir, { recursive: true }),
|
|
233
|
+
mkdir(stateDir, { recursive: true }),
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
const packed = runNpm(["pack", "--json", "--pack-destination", artifactDir]);
|
|
237
|
+
const packResult = JSON.parse(packed.stdout);
|
|
238
|
+
const filename = packResult?.[0]?.filename;
|
|
239
|
+
if (!filename) throw new Error(`npm pack did not report a package filename: ${packed.stdout}`);
|
|
240
|
+
const tarball = join(artifactDir, filename);
|
|
241
|
+
assert.ok(existsSync(tarball), `packed artifact is missing: ${tarball}`);
|
|
242
|
+
|
|
243
|
+
runNpm(["install", "--global", "--prefix", prefix, tarball]);
|
|
244
|
+
const shim = join(prefix, "forgerelay.ps1");
|
|
245
|
+
assert.ok(existsSync(shim), `npm did not create the PowerShell launcher shim: ${shim}`);
|
|
246
|
+
|
|
247
|
+
await writeFile(
|
|
248
|
+
join(configDir, "config.json"),
|
|
249
|
+
JSON.stringify({
|
|
250
|
+
host: "127.0.0.1",
|
|
251
|
+
port: 7678,
|
|
252
|
+
allowedRoots: [process.cwd()],
|
|
253
|
+
stateDir,
|
|
254
|
+
worktreeRoot: join(root, "worktrees"),
|
|
255
|
+
commandShell: {
|
|
256
|
+
mode: "follow-launcher",
|
|
257
|
+
family: "pwsh",
|
|
258
|
+
executable: pwsh,
|
|
259
|
+
},
|
|
260
|
+
shellInstructions: false,
|
|
261
|
+
}, null, 2),
|
|
262
|
+
"utf8",
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const launcherEnv = {
|
|
266
|
+
...process.env,
|
|
267
|
+
FORGERELAY_CONFIG_DIR: configDir,
|
|
268
|
+
FORGERELAY_OAUTH_OWNER_TOKEN: "pwsh-acceptance-owner-token-that-is-long-enough",
|
|
269
|
+
};
|
|
270
|
+
delete launcherEnv.npm_lifecycle_event;
|
|
271
|
+
delete launcherEnv.FORGERELAY_COMMAND_SHELL;
|
|
272
|
+
|
|
273
|
+
const result = spawnSync(
|
|
274
|
+
pwsh,
|
|
275
|
+
["-NoLogo", "-NoProfile", "-NonInteractive", "-File", shim, "doctor"],
|
|
276
|
+
{
|
|
277
|
+
cwd: process.cwd(),
|
|
278
|
+
env: launcherEnv,
|
|
279
|
+
encoding: "utf8",
|
|
280
|
+
windowsHide: true,
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
if (result.error || result.status !== 0) {
|
|
284
|
+
throw new Error(`Packaged PowerShell launcher failed: ${result.error?.message ?? result.stderr ?? result.status}`);
|
|
285
|
+
}
|
|
286
|
+
assert.match(result.stdout ?? "", new RegExp(`Command shell: pwsh ${escapeRegExp(expectedVersion)} \\(.+; launcher\\)`));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function runNpm(args) {
|
|
290
|
+
const result = spawnSync(process.execPath, [npmCli, ...args], {
|
|
291
|
+
cwd: process.cwd(),
|
|
292
|
+
env: process.env,
|
|
293
|
+
encoding: "utf8",
|
|
294
|
+
windowsHide: true,
|
|
295
|
+
});
|
|
296
|
+
if (result.error || result.status !== 0) {
|
|
297
|
+
throw new Error(`npm ${args.join(" ")} failed: ${result.error?.message ?? result.stderr ?? result.status}`);
|
|
298
|
+
}
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function powerShellLiteral(value) {
|
|
303
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function escapeRegExp(value) {
|
|
307
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
308
|
+
}
|
package/scripts/ci/verify.mjs
CHANGED
|
@@ -14,6 +14,7 @@ runNpm(["run", "release:check"], "Release metadata");
|
|
|
14
14
|
runNpm(["run", "typecheck"], "Typecheck");
|
|
15
15
|
runNpm(["test"], "Full test suite");
|
|
16
16
|
runNpm(["run", "build"], "Build");
|
|
17
|
+
if (process.platform === "win32") runNpm(["run", "pwsh:accept"], "PowerShell 7 packaged acceptance");
|
|
17
18
|
// The traffic audit is an end-to-end wire-budget check, not a platform feature
|
|
18
19
|
// matrix. Run it once on Linux so regressions are gated without tripling the
|
|
19
20
|
// 7677/7678 server exercise across every CI operating system.
|