@wix/pathgrade 0.29.0
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/bin/pathgrade.js +2 -0
- package/dist/affected/anchor.d.ts +18 -0
- package/dist/affected/anchor.js +42 -0
- package/dist/affected/config.d.ts +25 -0
- package/dist/affected/config.js +98 -0
- package/dist/affected/format.d.ts +45 -0
- package/dist/affected/format.js +87 -0
- package/dist/affected/git.d.ts +33 -0
- package/dist/affected/git.js +68 -0
- package/dist/affected/glob.d.ts +17 -0
- package/dist/affected/glob.js +25 -0
- package/dist/affected/meta.d.ts +26 -0
- package/dist/affected/meta.js +158 -0
- package/dist/affected/select.d.ts +30 -0
- package/dist/affected/select.js +101 -0
- package/dist/affected/sidecar.d.ts +32 -0
- package/dist/affected/sidecar.js +86 -0
- package/dist/affected/types.d.ts +43 -0
- package/dist/affected/types.js +7 -0
- package/dist/agents/claude/ask-user-answer-store.d.ts +28 -0
- package/dist/agents/claude/ask-user-answer-store.js +29 -0
- package/dist/agents/claude/ask-user-bridge.d.ts +56 -0
- package/dist/agents/claude/ask-user-bridge.js +162 -0
- package/dist/agents/claude/sdk-message-projector.d.ts +43 -0
- package/dist/agents/claude/sdk-message-projector.js +208 -0
- package/dist/agents/claude/sdk-options.d.ts +52 -0
- package/dist/agents/claude/sdk-options.js +79 -0
- package/dist/agents/claude.d.ts +56 -0
- package/dist/agents/claude.js +143 -0
- package/dist/agents/codex-app-server/agent.d.ts +35 -0
- package/dist/agents/codex-app-server/agent.js +388 -0
- package/dist/agents/codex-app-server/fixtures/app-server-client.d.ts +19 -0
- package/dist/agents/codex-app-server/fixtures/app-server-client.js +104 -0
- package/dist/agents/codex-app-server/fixtures/run-gate.d.ts +25 -0
- package/dist/agents/codex-app-server/fixtures/run-gate.js +48 -0
- package/dist/agents/codex-app-server/protocol/ClientRequest.d.ts +27 -0
- package/dist/agents/codex-app-server/protocol/ClientRequest.js +7 -0
- package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.d.ts +9 -0
- package/dist/agents/codex-app-server/protocol/DynamicToolCallParams.js +4 -0
- package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.d.ts +6 -0
- package/dist/agents/codex-app-server/protocol/GrantedPermissionProfile.js +7 -0
- package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.d.ts +23 -0
- package/dist/agents/codex-app-server/protocol/McpElicitationRequestParams.js +8 -0
- package/dist/agents/codex-app-server/protocol/Op.d.ts +11 -0
- package/dist/agents/codex-app-server/protocol/Op.js +5 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.d.ts +10 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalParams.js +6 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.d.ts +15 -0
- package/dist/agents/codex-app-server/protocol/PermissionsRequestApprovalResponse.js +4 -0
- package/dist/agents/codex-app-server/protocol/SandboxMode.d.ts +1 -0
- package/dist/agents/codex-app-server/protocol/SandboxMode.js +4 -0
- package/dist/agents/codex-app-server/protocol/ServerRequest.d.ts +42 -0
- package/dist/agents/codex-app-server/protocol/ServerRequest.js +7 -0
- package/dist/agents/codex-app-server/protocol/ThreadStartParams.d.ts +45 -0
- package/dist/agents/codex-app-server/protocol/ThreadStartParams.js +10 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.d.ts +6 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputAnswer.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.d.ts +7 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputOption.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.d.ts +10 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputParams.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.d.ts +12 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputQuestion.js +4 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.d.ts +9 -0
- package/dist/agents/codex-app-server/protocol/ToolRequestUserInputResponse.js +4 -0
- package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.d.ts +5 -0
- package/dist/agents/codex-app-server/protocol/TurnCompletedNotification.js +4 -0
- package/dist/agents/codex-app-server/protocol/index.d.ts +16 -0
- package/dist/agents/codex-app-server/protocol/index.js +11 -0
- package/dist/agents/codex-app-server/transport.d.ts +84 -0
- package/dist/agents/codex-app-server/transport.js +246 -0
- package/dist/agents/codex-app-server/wire-translators.d.ts +19 -0
- package/dist/agents/codex-app-server/wire-translators.js +52 -0
- package/dist/agents/codex.d.ts +7 -0
- package/dist/agents/codex.js +164 -0
- package/dist/agents/cursor.d.ts +39 -0
- package/dist/agents/cursor.js +233 -0
- package/dist/agents/registry.d.ts +17 -0
- package/dist/agents/registry.js +27 -0
- package/dist/agents/transcript-agent.d.ts +14 -0
- package/dist/agents/transcript-agent.js +59 -0
- package/dist/analytics/engine.d.ts +18 -0
- package/dist/analytics/engine.js +66 -0
- package/dist/commands/affected.d.ts +31 -0
- package/dist/commands/affected.js +150 -0
- package/dist/commands/analyze.d.ts +21 -0
- package/dist/commands/analyze.js +167 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +242 -0
- package/dist/commands/preview-reactions.d.ts +10 -0
- package/dist/commands/preview-reactions.js +88 -0
- package/dist/commands/preview.d.ts +5 -0
- package/dist/commands/preview.js +17 -0
- package/dist/commands/report.d.ts +39 -0
- package/dist/commands/report.js +108 -0
- package/dist/commands/run-args.d.ts +22 -0
- package/dist/commands/run-args.js +69 -0
- package/dist/commands/run-changed.d.ts +26 -0
- package/dist/commands/run-changed.js +138 -0
- package/dist/commands/validate.d.ts +13 -0
- package/dist/commands/validate.js +233 -0
- package/dist/core/mcp-mock.d.ts +3 -0
- package/dist/core/mcp-mock.js +23 -0
- package/dist/core/mcp-mock.types.d.ts +15 -0
- package/dist/core/mcp-mock.types.js +1 -0
- package/dist/core/skills.d.ts +15 -0
- package/dist/core/skills.js +76 -0
- package/dist/evals/discovery.d.ts +7 -0
- package/dist/evals/discovery.js +69 -0
- package/dist/mcp-mock-server.d.ts +1 -0
- package/dist/mcp-mock-server.js +116 -0
- package/dist/pathgrade.d.ts +10 -0
- package/dist/pathgrade.js +243 -0
- package/dist/plugin/index.d.ts +9 -0
- package/dist/plugin/index.js +81 -0
- package/dist/plugin/lifecycle.d.ts +45 -0
- package/dist/plugin/lifecycle.js +146 -0
- package/dist/plugin/reporter.d.ts +36 -0
- package/dist/plugin/reporter.js +275 -0
- package/dist/plugin/setup.d.ts +1 -0
- package/dist/plugin/setup.js +3 -0
- package/dist/providers/copy-filter.d.ts +18 -0
- package/dist/providers/copy-filter.js +66 -0
- package/dist/providers/credentials.d.ts +36 -0
- package/dist/providers/credentials.js +202 -0
- package/dist/providers/mcp-config.d.ts +34 -0
- package/dist/providers/mcp-config.js +55 -0
- package/dist/providers/sandbox-exec.d.ts +8 -0
- package/dist/providers/sandbox-exec.js +82 -0
- package/dist/providers/sandbox.d.ts +22 -0
- package/dist/providers/sandbox.js +158 -0
- package/dist/providers/sandboxed-claude-spawn.d.ts +24 -0
- package/dist/providers/sandboxed-claude-spawn.js +57 -0
- package/dist/providers/workspace.d.ts +15 -0
- package/dist/providers/workspace.js +73 -0
- package/dist/reporters/browser.d.ts +2 -0
- package/dist/reporters/browser.js +85 -0
- package/dist/reporters/cli.d.ts +2 -0
- package/dist/reporters/cli.js +131 -0
- package/dist/reporters/diagnostics.d.ts +46 -0
- package/dist/reporters/diagnostics.js +147 -0
- package/dist/reporters/github-comment.d.ts +67 -0
- package/dist/reporters/github-comment.js +252 -0
- package/dist/reporters/loader.d.ts +8 -0
- package/dist/reporters/loader.js +36 -0
- package/dist/reporters/results-path.d.ts +2 -0
- package/dist/reporters/results-path.js +10 -0
- package/dist/reporters/verbose-emitter.d.ts +69 -0
- package/dist/reporters/verbose-emitter.js +103 -0
- package/dist/sdk/agent-crash.d.ts +19 -0
- package/dist/sdk/agent-crash.js +20 -0
- package/dist/sdk/agent-resolution.d.ts +12 -0
- package/dist/sdk/agent-resolution.js +21 -0
- package/dist/sdk/agent-result-log.d.ts +19 -0
- package/dist/sdk/agent-result-log.js +45 -0
- package/dist/sdk/agent.d.ts +4 -0
- package/dist/sdk/agent.js +354 -0
- package/dist/sdk/ask-bus/bus.d.ts +21 -0
- package/dist/sdk/ask-bus/bus.js +129 -0
- package/dist/sdk/ask-bus/handler.d.ts +31 -0
- package/dist/sdk/ask-bus/handler.js +244 -0
- package/dist/sdk/ask-bus/parsers.d.ts +9 -0
- package/dist/sdk/ask-bus/parsers.js +30 -0
- package/dist/sdk/ask-bus/projection.d.ts +26 -0
- package/dist/sdk/ask-bus/projection.js +45 -0
- package/dist/sdk/ask-bus/types.d.ts +69 -0
- package/dist/sdk/ask-bus/types.js +1 -0
- package/dist/sdk/chat.d.ts +29 -0
- package/dist/sdk/chat.js +93 -0
- package/dist/sdk/conversation-window.d.ts +14 -0
- package/dist/sdk/conversation-window.js +38 -0
- package/dist/sdk/converse.d.ts +42 -0
- package/dist/sdk/converse.js +329 -0
- package/dist/sdk/eval-runtime.d.ts +10 -0
- package/dist/sdk/eval-runtime.js +17 -0
- package/dist/sdk/evaluate.d.ts +13 -0
- package/dist/sdk/evaluate.js +325 -0
- package/dist/sdk/index.d.ts +31 -0
- package/dist/sdk/index.js +20 -0
- package/dist/sdk/judge-pipeline.d.ts +7 -0
- package/dist/sdk/judge-pipeline.js +178 -0
- package/dist/sdk/judge-prompt-builder.d.ts +7 -0
- package/dist/sdk/judge-prompt-builder.js +76 -0
- package/dist/sdk/judge-tool-runner.d.ts +19 -0
- package/dist/sdk/judge-tool-runner.js +51 -0
- package/dist/sdk/judge-tool-session.d.ts +47 -0
- package/dist/sdk/judge-tool-session.js +213 -0
- package/dist/sdk/judge-tools.d.ts +29 -0
- package/dist/sdk/judge-tools.js +283 -0
- package/dist/sdk/managed-session.d.ts +50 -0
- package/dist/sdk/managed-session.js +110 -0
- package/dist/sdk/persona.d.ts +5 -0
- package/dist/sdk/persona.js +33 -0
- package/dist/sdk/reaction-loader.d.ts +4 -0
- package/dist/sdk/reaction-loader.js +119 -0
- package/dist/sdk/reaction-preview.d.ts +6 -0
- package/dist/sdk/reaction-preview.js +94 -0
- package/dist/sdk/run-scorer.d.ts +10 -0
- package/dist/sdk/run-scorer.js +124 -0
- package/dist/sdk/runtime-policy.d.ts +21 -0
- package/dist/sdk/runtime-policy.js +89 -0
- package/dist/sdk/scorer-utils.d.ts +6 -0
- package/dist/sdk/scorer-utils.js +39 -0
- package/dist/sdk/scorers.d.ts +64 -0
- package/dist/sdk/scorers.js +80 -0
- package/dist/sdk/snapshots.d.ts +41 -0
- package/dist/sdk/snapshots.js +148 -0
- package/dist/sdk/types.d.ts +416 -0
- package/dist/sdk/types.js +17 -0
- package/dist/sdk/visible-turn.d.ts +4 -0
- package/dist/sdk/visible-turn.js +23 -0
- package/dist/tool-events.d.ts +45 -0
- package/dist/tool-events.js +167 -0
- package/dist/types.d.ts +372 -0
- package/dist/types.js +37 -0
- package/dist/utils/cli.d.ts +55 -0
- package/dist/utils/cli.js +131 -0
- package/dist/utils/env.d.ts +8 -0
- package/dist/utils/env.js +26 -0
- package/dist/utils/llm-mocks.d.ts +43 -0
- package/dist/utils/llm-mocks.js +108 -0
- package/dist/utils/llm-providers/anthropic.d.ts +2 -0
- package/dist/utils/llm-providers/anthropic.js +132 -0
- package/dist/utils/llm-providers/cli.d.ts +20 -0
- package/dist/utils/llm-providers/cli.js +162 -0
- package/dist/utils/llm-providers/openai.d.ts +2 -0
- package/dist/utils/llm-providers/openai.js +56 -0
- package/dist/utils/llm-types.d.ts +107 -0
- package/dist/utils/llm-types.js +1 -0
- package/dist/utils/llm.d.ts +36 -0
- package/dist/utils/llm.js +209 -0
- package/dist/utils/shutdown.d.ts +15 -0
- package/dist/utils/shutdown.js +41 -0
- package/dist/utils/timeout.d.ts +8 -0
- package/dist/utils/timeout.js +32 -0
- package/dist/viewer.html +1178 -0
- package/package.json +88 -0
- package/templates/eval.ts.template +28 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
const MCP_CONFIG_FILENAME = '.pathgrade-mcp.json';
|
|
4
|
+
export async function writeMcpConfig(workspacePath, mcp) {
|
|
5
|
+
if (!mcp)
|
|
6
|
+
return { mcpConfigPath: undefined };
|
|
7
|
+
const mcpConfigPath = MCP_CONFIG_FILENAME;
|
|
8
|
+
if ('configFile' in mcp) {
|
|
9
|
+
const mcpSrc = path.resolve(mcp.configFile);
|
|
10
|
+
if (!await fs.pathExists(mcpSrc)) {
|
|
11
|
+
throw new Error(`MCP config file not found: ${mcpSrc}`);
|
|
12
|
+
}
|
|
13
|
+
await fs.copy(mcpSrc, path.join(workspacePath, mcpConfigPath));
|
|
14
|
+
}
|
|
15
|
+
else if ('mock' in mcp) {
|
|
16
|
+
const mocks = Array.isArray(mcp.mock) ? mcp.mock : [mcp.mock];
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
for (const mock of mocks) {
|
|
19
|
+
if (seen.has(mock.config.name)) {
|
|
20
|
+
throw new Error(`Duplicate mock MCP server name: "${mock.config.name}"`);
|
|
21
|
+
}
|
|
22
|
+
seen.add(mock.config.name);
|
|
23
|
+
}
|
|
24
|
+
const mcpServers = {};
|
|
25
|
+
for (const mock of mocks) {
|
|
26
|
+
const sanitizedName = mock.config.name.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
27
|
+
const fixturePath = path.join(workspacePath, `.pathgrade-mcp-mock-${sanitizedName}.json`);
|
|
28
|
+
await fs.writeJson(fixturePath, mock.config, { spaces: 2 });
|
|
29
|
+
const mockServerScript = path.resolve(import.meta.dirname, '../mcp-mock-server.js');
|
|
30
|
+
mcpServers[mock.config.name] = {
|
|
31
|
+
command: 'node',
|
|
32
|
+
args: [mockServerScript, fixturePath],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
await fs.writeJson(path.join(workspacePath, mcpConfigPath), { mcpServers }, { spaces: 2 });
|
|
36
|
+
}
|
|
37
|
+
return { mcpConfigPath };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read the JSON `writeMcpConfig` writes into `<workspace>/.pathgrade-mcp.json`
|
|
41
|
+
* and return the inner `mcpServers` object the Claude SDK driver hands to
|
|
42
|
+
* `Options.mcpServers`. Returns `undefined` when the file is absent (the
|
|
43
|
+
* "no MCP" path through the driver — the fixture didn't declare an `mcp` spec).
|
|
44
|
+
*
|
|
45
|
+
* The on-disk path is preserved because the Cursor agent driver still
|
|
46
|
+
* consumes it via `mcpConfigPath` (`src/agents/cursor.ts`) — Claude moves to
|
|
47
|
+
* the object form, Cursor stays on the file.
|
|
48
|
+
*/
|
|
49
|
+
export async function loadMcpServersForSdk(workspacePath) {
|
|
50
|
+
const configPath = path.join(workspacePath, MCP_CONFIG_FILENAME);
|
|
51
|
+
if (!(await fs.pathExists(configPath)))
|
|
52
|
+
return undefined;
|
|
53
|
+
const parsed = (await fs.readJson(configPath));
|
|
54
|
+
return parsed.mcpServers;
|
|
55
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CommandResult } from '../types.js';
|
|
2
|
+
export interface ExecContext {
|
|
3
|
+
cwd: string;
|
|
4
|
+
env: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
export declare function sandboxExec(command: string, ctx: ExecContext, opts?: {
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}): Promise<CommandResult>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
export function sandboxExec(command, ctx, opts) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
const child = spawn(command, {
|
|
5
|
+
shell: true,
|
|
6
|
+
detached: process.platform !== 'win32',
|
|
7
|
+
cwd: ctx.cwd,
|
|
8
|
+
env: { ...ctx.env },
|
|
9
|
+
});
|
|
10
|
+
let stdout = '';
|
|
11
|
+
let stderr = '';
|
|
12
|
+
let settled = false;
|
|
13
|
+
let killTimer;
|
|
14
|
+
const finish = (result) => {
|
|
15
|
+
if (settled)
|
|
16
|
+
return;
|
|
17
|
+
settled = true;
|
|
18
|
+
if (killTimer)
|
|
19
|
+
clearTimeout(killTimer);
|
|
20
|
+
if (opts?.signal && abortHandler) {
|
|
21
|
+
opts.signal.removeEventListener('abort', abortHandler);
|
|
22
|
+
}
|
|
23
|
+
resolve(result);
|
|
24
|
+
};
|
|
25
|
+
const abortHandler = () => {
|
|
26
|
+
if (settled)
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
if (process.platform !== 'win32' && child.pid) {
|
|
30
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
child.kill('SIGTERM');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
child.kill('SIGTERM');
|
|
38
|
+
}
|
|
39
|
+
killTimer = setTimeout(() => {
|
|
40
|
+
try {
|
|
41
|
+
if (process.platform !== 'win32' && child.pid) {
|
|
42
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
43
|
+
}
|
|
44
|
+
else if (!child.killed) {
|
|
45
|
+
child.kill('SIGKILL');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
if (!child.killed)
|
|
50
|
+
child.kill('SIGKILL');
|
|
51
|
+
}
|
|
52
|
+
}, 250);
|
|
53
|
+
};
|
|
54
|
+
if (opts?.signal) {
|
|
55
|
+
if (opts.signal.aborted) {
|
|
56
|
+
abortHandler();
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
opts.signal.addEventListener('abort', abortHandler, { once: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
63
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
64
|
+
child.on('close', (code, signal) => {
|
|
65
|
+
finish({
|
|
66
|
+
stdout,
|
|
67
|
+
stderr,
|
|
68
|
+
exitCode: code ?? (opts?.signal?.aborted ? 124 : 1),
|
|
69
|
+
timedOut: opts?.signal?.aborted || undefined,
|
|
70
|
+
killed: signal != null || undefined,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
child.on('error', () => {
|
|
74
|
+
finish({
|
|
75
|
+
stdout,
|
|
76
|
+
stderr,
|
|
77
|
+
exitCode: opts?.signal?.aborted ? 124 : 1,
|
|
78
|
+
timedOut: opts?.signal?.aborted || undefined,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface SandboxConfig {
|
|
2
|
+
agent: import('../sdk/types.js').AgentName;
|
|
3
|
+
workspace?: string;
|
|
4
|
+
skillDir?: string;
|
|
5
|
+
copyFromHome?: string[];
|
|
6
|
+
env?: Record<string, string>;
|
|
7
|
+
mcp?: import('./mcp-config.js').McpSpec;
|
|
8
|
+
/**
|
|
9
|
+
* Glob patterns to ignore when copying workspace and skill directories.
|
|
10
|
+
* Replaces the default ignore list entirely. Pass `[]` to disable filtering.
|
|
11
|
+
* When omitted, DEFAULT_COPY_IGNORE is used.
|
|
12
|
+
*/
|
|
13
|
+
copyIgnore?: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface Sandbox {
|
|
16
|
+
rootDir: string;
|
|
17
|
+
workspacePath: string;
|
|
18
|
+
homePath: string;
|
|
19
|
+
env: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
export declare const SAFE_HOST_VARS: string[];
|
|
22
|
+
export declare function createSandbox(spec: SandboxConfig): Promise<Sandbox>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import { DEFAULT_COPY_IGNORE, createCopyFilter } from './copy-filter.js';
|
|
5
|
+
export const SAFE_HOST_VARS = [
|
|
6
|
+
'PATH', 'SHELL', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'USER', 'LOGNAME',
|
|
7
|
+
];
|
|
8
|
+
export async function createSandbox(spec) {
|
|
9
|
+
const rootDir = path.join(os.tmpdir(), `pathgrade-${Math.random().toString(36).substring(7)}`);
|
|
10
|
+
const workspacePath = path.join(rootDir, 'workspace');
|
|
11
|
+
const homePath = path.join(rootDir, 'home');
|
|
12
|
+
const tmpPath = path.join(rootDir, 'tmp');
|
|
13
|
+
await fs.ensureDir(workspacePath);
|
|
14
|
+
await fs.ensureDir(homePath);
|
|
15
|
+
await fs.ensureDir(tmpPath);
|
|
16
|
+
// Build copy filter for workspace and skill directories
|
|
17
|
+
const ignorePatterns = spec.copyIgnore !== undefined ? spec.copyIgnore : DEFAULT_COPY_IGNORE;
|
|
18
|
+
const copyFilter = createCopyFilter(ignorePatterns);
|
|
19
|
+
// Copy fixture directory contents into workspace
|
|
20
|
+
if (spec.workspace) {
|
|
21
|
+
const fixturePath = path.resolve(spec.workspace);
|
|
22
|
+
if (await fs.pathExists(fixturePath)) {
|
|
23
|
+
await fs.copy(fixturePath, workspacePath, { filter: copyFilter });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Stage skill to both discovery paths so either convention resolves
|
|
27
|
+
if (spec.skillDir) {
|
|
28
|
+
const skillPath = path.resolve(spec.skillDir);
|
|
29
|
+
const skillName = path.basename(skillPath);
|
|
30
|
+
const skillFilter = (src) => {
|
|
31
|
+
const rel = path.relative(skillPath, src);
|
|
32
|
+
if (rel === 'test' || rel.startsWith(`test${path.sep}`)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return copyFilter(src);
|
|
36
|
+
};
|
|
37
|
+
const claudeDir = path.join(workspacePath, '.claude', 'skills', skillName);
|
|
38
|
+
const agentsDir = path.join(workspacePath, '.agents', 'skills', skillName);
|
|
39
|
+
const primaryDir = spec.agent === 'claude' ? claudeDir : agentsDir;
|
|
40
|
+
const secondaryDir = spec.agent === 'claude' ? agentsDir : claudeDir;
|
|
41
|
+
await fs.copy(skillPath, primaryDir, { filter: skillFilter });
|
|
42
|
+
await fs.copy(skillPath, secondaryDir, { filter: skillFilter });
|
|
43
|
+
// Verified against cursor-agent 2026.04.17-787b533: the rule loader
|
|
44
|
+
// reads SKILL.md body from the .mdc file directly; a plain pointer
|
|
45
|
+
// ("see .agents/skills/<name>/SKILL.md") is not honored, so inline
|
|
46
|
+
// the content. Emitted unconditionally across all agents — harness-
|
|
47
|
+
// idempotent, same posture as the .claude/.agents dual-stage above.
|
|
48
|
+
await stageCursorRule(workspacePath, skillName, skillPath);
|
|
49
|
+
}
|
|
50
|
+
// Copy specified paths from real host HOME into sandbox HOME
|
|
51
|
+
if (spec.copyFromHome) {
|
|
52
|
+
const realHome = os.homedir();
|
|
53
|
+
for (const relPath of spec.copyFromHome) {
|
|
54
|
+
const srcPath = path.join(realHome, relPath);
|
|
55
|
+
if (await fs.pathExists(srcPath)) {
|
|
56
|
+
const destPath = path.join(homePath, relPath);
|
|
57
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
58
|
+
await fs.copy(srcPath, destPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Create HOME → workspace symlinks for skill directories that exist
|
|
63
|
+
const skillSymlinks = [
|
|
64
|
+
['.agents', 'skills'],
|
|
65
|
+
['.claude', 'skills'],
|
|
66
|
+
];
|
|
67
|
+
for (const segments of skillSymlinks) {
|
|
68
|
+
const wsSkillDir = path.join(workspacePath, ...segments);
|
|
69
|
+
if (await fs.pathExists(wsSkillDir)) {
|
|
70
|
+
const homeSkillDir = path.join(homePath, ...segments);
|
|
71
|
+
await fs.ensureDir(path.dirname(homeSkillDir));
|
|
72
|
+
await fs.symlink(wsSkillDir, homeSkillDir);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Build env: safe host vars, then sandbox defaults, then explicit overrides.
|
|
76
|
+
// API keys are NOT forwarded here — resolveCredentials() handles credentials
|
|
77
|
+
// per-agent so Claude CLI can use native OAuth when no key is needed.
|
|
78
|
+
const env = {};
|
|
79
|
+
for (const key of SAFE_HOST_VARS) {
|
|
80
|
+
if (process.env[key])
|
|
81
|
+
env[key] = process.env[key];
|
|
82
|
+
}
|
|
83
|
+
env.HOME = homePath;
|
|
84
|
+
env.TMPDIR = tmpPath;
|
|
85
|
+
env.TMP = tmpPath;
|
|
86
|
+
env.TEMP = tmpPath;
|
|
87
|
+
// Explicit overrides take precedence
|
|
88
|
+
if (spec.env) {
|
|
89
|
+
Object.assign(env, spec.env);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
rootDir,
|
|
93
|
+
workspacePath,
|
|
94
|
+
homePath,
|
|
95
|
+
env,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const FRONTMATTER_DELIM = /^---\s*$/;
|
|
99
|
+
async function stageCursorRule(workspacePath, skillName, skillPath) {
|
|
100
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
101
|
+
if (!(await fs.pathExists(skillMdPath)))
|
|
102
|
+
return;
|
|
103
|
+
const raw = await fs.readFile(skillMdPath, 'utf8');
|
|
104
|
+
const parsed = splitFrontmatter(raw);
|
|
105
|
+
const description = parsed.frontmatter.description ?? deriveFallbackDescription(parsed.body, skillName);
|
|
106
|
+
const mdc = [
|
|
107
|
+
'---',
|
|
108
|
+
`description: ${escapeYamlScalar(description)}`,
|
|
109
|
+
'alwaysApply: true',
|
|
110
|
+
'---',
|
|
111
|
+
'',
|
|
112
|
+
parsed.body.trimStart(),
|
|
113
|
+
].join('\n');
|
|
114
|
+
const rulesDir = path.join(workspacePath, '.cursor', 'rules');
|
|
115
|
+
await fs.ensureDir(rulesDir);
|
|
116
|
+
await fs.writeFile(path.join(rulesDir, `${skillName}.mdc`), mdc, 'utf8');
|
|
117
|
+
}
|
|
118
|
+
function splitFrontmatter(raw) {
|
|
119
|
+
const lines = raw.split('\n');
|
|
120
|
+
if (lines.length === 0 || !FRONTMATTER_DELIM.test(lines[0])) {
|
|
121
|
+
return { frontmatter: {}, body: raw };
|
|
122
|
+
}
|
|
123
|
+
const endIdx = lines.slice(1).findIndex((line) => FRONTMATTER_DELIM.test(line));
|
|
124
|
+
if (endIdx === -1) {
|
|
125
|
+
return { frontmatter: {}, body: raw };
|
|
126
|
+
}
|
|
127
|
+
const frontmatterLines = lines.slice(1, endIdx + 1);
|
|
128
|
+
const bodyLines = lines.slice(endIdx + 2);
|
|
129
|
+
const frontmatter = {};
|
|
130
|
+
for (const line of frontmatterLines) {
|
|
131
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
132
|
+
if (match) {
|
|
133
|
+
frontmatter[match[1]] = match[2].replace(/^['"]|['"]$/g, '').trim();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { frontmatter, body: bodyLines.join('\n') };
|
|
137
|
+
}
|
|
138
|
+
function deriveFallbackDescription(body, skillName) {
|
|
139
|
+
for (const line of body.split('\n')) {
|
|
140
|
+
const trimmed = line.trim();
|
|
141
|
+
if (!trimmed)
|
|
142
|
+
continue;
|
|
143
|
+
if (trimmed.startsWith('#')) {
|
|
144
|
+
const headingText = trimmed.replace(/^#+\s*/, '').trim();
|
|
145
|
+
if (headingText)
|
|
146
|
+
return headingText;
|
|
147
|
+
}
|
|
148
|
+
return trimmed;
|
|
149
|
+
}
|
|
150
|
+
return `Skill ${skillName}`;
|
|
151
|
+
}
|
|
152
|
+
function escapeYamlScalar(value) {
|
|
153
|
+
const single = value.replace(/[\r\n]+/g, ' ').trim();
|
|
154
|
+
if (/[:#&*!|>'"%@`]/.test(single) || single !== value) {
|
|
155
|
+
return `"${single.replace(/"/g, '\\"')}"`;
|
|
156
|
+
}
|
|
157
|
+
return single;
|
|
158
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SpawnOptions as SdkSpawnOptions, SpawnedProcess } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
/** Injected child_process spawn delegate. Real callers pass node's
|
|
3
|
+
* `child_process.spawn`; tests pass a recorder. */
|
|
4
|
+
export type ChildSpawnFn = (command: string, args: string[], opts: {
|
|
5
|
+
cwd?: string;
|
|
6
|
+
env: NodeJS.ProcessEnv;
|
|
7
|
+
signal: AbortSignal;
|
|
8
|
+
}) => SpawnedProcess;
|
|
9
|
+
export interface SandboxedClaudeSpawnDeps {
|
|
10
|
+
/** Host platform. macOS branches on this when `sandboxProfile` is set. */
|
|
11
|
+
platform: NodeJS.Platform;
|
|
12
|
+
/** Host process env to filter through `SAFE_HOST_VARS`. */
|
|
13
|
+
hostEnv: NodeJS.ProcessEnv;
|
|
14
|
+
/** Spawner. Defaults to `child_process.spawn`. */
|
|
15
|
+
spawn?: ChildSpawnFn;
|
|
16
|
+
/**
|
|
17
|
+
* Inline macOS sandbox-exec profile. When set on darwin, argv is wrapped
|
|
18
|
+
* with `sandbox-exec -p <profile> --`. When unset (today's posture),
|
|
19
|
+
* argv passes through untouched.
|
|
20
|
+
*/
|
|
21
|
+
sandboxProfile?: string;
|
|
22
|
+
}
|
|
23
|
+
export type SandboxedClaudeSpawn = (opts: SdkSpawnOptions) => SpawnedProcess;
|
|
24
|
+
export declare function createSandboxedClaudeSpawn(deps: SandboxedClaudeSpawnDeps): SandboxedClaudeSpawn;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapts the Claude Agent SDK's `spawnClaudeCodeProcess` hook into pathgrade's
|
|
3
|
+
* sandbox model. Knows nothing about Claude, the SDK, or evals — it just
|
|
4
|
+
* receives a `SpawnOptions` and returns a `SpawnedProcess`.
|
|
5
|
+
*
|
|
6
|
+
* Two enforcement points:
|
|
7
|
+
*
|
|
8
|
+
* 1. Subprocess env. The host's ambient env is intersected with
|
|
9
|
+
* `SAFE_HOST_VARS` (`PATH`, `SHELL`, `LANG`, ...) and the SDK-supplied
|
|
10
|
+
* `Options.env` is layered on top. Anything else stays out — the
|
|
11
|
+
* `Options.env` declaration in the driver is the single typed channel
|
|
12
|
+
* for whatever the SDK subprocess intentionally needs (auth, scratch
|
|
13
|
+
* config dir, etc.).
|
|
14
|
+
*
|
|
15
|
+
* 2. Argv. macOS optionally wraps with `sandbox-exec -p <profile> --` when
|
|
16
|
+
* a profile is supplied; non-darwin always passes through. Today no
|
|
17
|
+
* profile is staged, so production usage is passthrough on every
|
|
18
|
+
* platform — the seam exists for the wrap preserved from the pre-SDK
|
|
19
|
+
* driver.
|
|
20
|
+
*/
|
|
21
|
+
import { spawn as nodeSpawn } from 'child_process';
|
|
22
|
+
import { SAFE_HOST_VARS } from './sandbox.js';
|
|
23
|
+
export function createSandboxedClaudeSpawn(deps) {
|
|
24
|
+
const spawn = deps.spawn ?? nodeSpawn;
|
|
25
|
+
return (sdkOpts) => {
|
|
26
|
+
const env = buildSubprocessEnv(deps.hostEnv, sdkOpts.env);
|
|
27
|
+
const { command, args } = buildArgv(deps.platform, deps.sandboxProfile, sdkOpts.command, sdkOpts.args);
|
|
28
|
+
return spawn(command, args, {
|
|
29
|
+
cwd: sdkOpts.cwd,
|
|
30
|
+
env,
|
|
31
|
+
signal: sdkOpts.signal,
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function buildSubprocessEnv(hostEnv, sdkEnv) {
|
|
36
|
+
const env = {};
|
|
37
|
+
for (const key of SAFE_HOST_VARS) {
|
|
38
|
+
const value = hostEnv[key];
|
|
39
|
+
if (value !== undefined)
|
|
40
|
+
env[key] = value;
|
|
41
|
+
}
|
|
42
|
+
for (const [key, value] of Object.entries(sdkEnv)) {
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
env[key] = value;
|
|
46
|
+
}
|
|
47
|
+
return env;
|
|
48
|
+
}
|
|
49
|
+
function buildArgv(platform, sandboxProfile, command, args) {
|
|
50
|
+
if (platform === 'darwin' && sandboxProfile !== undefined) {
|
|
51
|
+
return {
|
|
52
|
+
command: 'sandbox-exec',
|
|
53
|
+
args: ['-p', sandboxProfile, '--', command, ...args],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { command, args };
|
|
57
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type SandboxConfig } from './sandbox.js';
|
|
2
|
+
import type { CommandResult } from '../types.js';
|
|
3
|
+
export type { McpSpec } from './mcp-config.js';
|
|
4
|
+
export interface Workspace {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly mcpConfigPath: string | undefined;
|
|
7
|
+
readonly env: Record<string, string>;
|
|
8
|
+
readonly setupCommands: string[];
|
|
9
|
+
exec(command: string, opts?: {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}): Promise<CommandResult>;
|
|
12
|
+
dispose(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function linkPathsFromHostHome(pathsToLink: string[], sandboxHomePath: string): Promise<void>;
|
|
15
|
+
export declare function prepareWorkspace(spec: SandboxConfig): Promise<Workspace>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { createSandbox } from './sandbox.js';
|
|
5
|
+
import { writeMcpConfig } from './mcp-config.js';
|
|
6
|
+
import { sandboxExec } from './sandbox-exec.js';
|
|
7
|
+
import { resolveCredentials } from './credentials.js';
|
|
8
|
+
async function copyPathsFromHostHome(pathsToCopy, sandboxHomePath) {
|
|
9
|
+
const realHome = os.homedir();
|
|
10
|
+
for (const relPath of pathsToCopy) {
|
|
11
|
+
const srcPath = path.join(realHome, relPath);
|
|
12
|
+
if (!await fs.pathExists(srcPath))
|
|
13
|
+
continue;
|
|
14
|
+
const destPath = path.join(sandboxHomePath, relPath);
|
|
15
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
16
|
+
await fs.copy(srcPath, destPath);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function linkPathsFromHostHome(pathsToLink, sandboxHomePath) {
|
|
20
|
+
const realHome = os.homedir();
|
|
21
|
+
for (const relPath of pathsToLink) {
|
|
22
|
+
const srcPath = path.join(realHome, relPath);
|
|
23
|
+
if (!await fs.pathExists(srcPath))
|
|
24
|
+
continue;
|
|
25
|
+
const destPath = path.join(sandboxHomePath, relPath);
|
|
26
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
27
|
+
await fs.symlink(srcPath, destPath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function prepareWorkspace(spec) {
|
|
31
|
+
const { mcp, ...sandboxSpec } = spec;
|
|
32
|
+
const sandbox = await createSandbox(sandboxSpec);
|
|
33
|
+
const { workspacePath, homePath, env: sandboxEnv, rootDir } = sandbox;
|
|
34
|
+
try {
|
|
35
|
+
// Resolve credentials: pass user's original env (not sandboxEnv) so
|
|
36
|
+
// the resolver can distinguish explicit user intent from auto-resolved values.
|
|
37
|
+
const creds = await resolveCredentials(spec.agent, spec.env ?? {});
|
|
38
|
+
Object.assign(sandboxEnv, creds.env);
|
|
39
|
+
await copyPathsFromHostHome(creds.copyFromHome, homePath);
|
|
40
|
+
await linkPathsFromHostHome(creds.linkFromHome ?? [], homePath);
|
|
41
|
+
const { mcpConfigPath } = await writeMcpConfig(workspacePath, mcp);
|
|
42
|
+
let disposed = false;
|
|
43
|
+
return {
|
|
44
|
+
path: workspacePath,
|
|
45
|
+
mcpConfigPath,
|
|
46
|
+
env: sandboxEnv,
|
|
47
|
+
setupCommands: creds.setupCommands,
|
|
48
|
+
exec: (command, opts) => sandboxExec(command, { cwd: workspacePath, env: sandboxEnv }, opts),
|
|
49
|
+
async dispose() {
|
|
50
|
+
if (disposed)
|
|
51
|
+
return;
|
|
52
|
+
disposed = true;
|
|
53
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
54
|
+
try {
|
|
55
|
+
await fs.remove(rootDir);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const code = error.code;
|
|
60
|
+
const retryable = code === 'ENOTEMPTY' || code === 'EBUSY' || code === 'EPERM';
|
|
61
|
+
if (!retryable || attempt === 3)
|
|
62
|
+
throw error;
|
|
63
|
+
await new Promise((r) => setTimeout(r, 50 * (attempt + 1)));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
await fs.remove(rootDir).catch(() => { });
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { loadReports } from './loader.js';
|
|
5
|
+
export function runBrowserPreview(resultsDir, port = 3847) {
|
|
6
|
+
const resolved = path.resolve(resultsDir);
|
|
7
|
+
const htmlPath = path.join(import.meta.dirname, '..', 'viewer.html');
|
|
8
|
+
const server = http.createServer(async (req, res) => {
|
|
9
|
+
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
10
|
+
if (url.pathname === '/api/reports') {
|
|
11
|
+
const reports = await loadReports(resolved, { skipTraces: true });
|
|
12
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
13
|
+
res.end(JSON.stringify(reports));
|
|
14
|
+
}
|
|
15
|
+
else if (url.pathname === '/api/report') {
|
|
16
|
+
const file = url.searchParams.get('file');
|
|
17
|
+
if (!file) {
|
|
18
|
+
res.writeHead(400);
|
|
19
|
+
res.end('Missing file param');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const filePath = path.resolve(resolved, file);
|
|
23
|
+
if (!filePath.startsWith(resolved + path.sep) && filePath !== resolved) {
|
|
24
|
+
res.writeHead(403);
|
|
25
|
+
res.end('Forbidden');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (req.method === 'DELETE') {
|
|
29
|
+
if (await fs.pathExists(filePath)) {
|
|
30
|
+
await fs.remove(filePath);
|
|
31
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
32
|
+
res.end(JSON.stringify({ deleted: file }));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
res.writeHead(404);
|
|
36
|
+
res.end('Not found');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
if (await fs.pathExists(filePath)) {
|
|
41
|
+
const raw = await fs.readJSON(filePath);
|
|
42
|
+
const groupIdx = parseInt(url.searchParams.get('group') || '0', 10);
|
|
43
|
+
let report = raw;
|
|
44
|
+
if (raw.version && Array.isArray(raw.groups)) {
|
|
45
|
+
report = { timestamp: raw.timestamp, ...raw.groups[groupIdx] };
|
|
46
|
+
}
|
|
47
|
+
// Merge trace data (session_log, conversation) back into trials
|
|
48
|
+
if (report.trace_file) {
|
|
49
|
+
const tracePath = path.resolve(resolved, report.trace_file);
|
|
50
|
+
if (await fs.pathExists(tracePath)) {
|
|
51
|
+
const traceTrials = await fs.readJSON(tracePath);
|
|
52
|
+
if (Array.isArray(traceTrials) && Array.isArray(report.trials)) {
|
|
53
|
+
report.trials = report.trials.map((t, i) => ({
|
|
54
|
+
...t,
|
|
55
|
+
...traceTrials[i],
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
61
|
+
res.end(JSON.stringify(report));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
res.writeHead(404);
|
|
65
|
+
res.end('Not found');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const html = await fs.readFile(htmlPath, 'utf-8');
|
|
71
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
72
|
+
res.end(html);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
server.listen(port, () => {
|
|
77
|
+
const addr = server.address();
|
|
78
|
+
const actualPort = typeof addr === 'object' && addr ? addr.port : port;
|
|
79
|
+
console.log(`\npathgrade preview`);
|
|
80
|
+
console.log(`\n url http://localhost:${actualPort}`);
|
|
81
|
+
console.log(` results ${resolved}\n`);
|
|
82
|
+
resolve(server);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|