@webpresso/agent-kit 0.24.0 → 0.26.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -2
- package/dist/esm/audit/toolchain-isolation.d.ts +3 -0
- package/dist/esm/audit/toolchain-isolation.js +134 -0
- package/dist/esm/cli/cli.d.ts +1 -1
- package/dist/esm/cli/cli.js +6 -0
- package/dist/esm/cli/commands/audit-core.d.ts +1 -1
- package/dist/esm/cli/commands/audit.js +1 -0
- package/dist/esm/cli/commands/deploy.d.ts +4 -0
- package/dist/esm/cli/commands/deploy.js +35 -0
- package/dist/esm/cli/commands/init/gitignore-patcher.d.ts +17 -0
- package/dist/esm/cli/commands/init/gitignore-patcher.js +42 -0
- package/dist/esm/cli/commands/init/index.js +11 -1
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +44 -17
- package/dist/esm/deploy/index.d.ts +5 -0
- package/dist/esm/deploy/index.js +5 -0
- package/dist/esm/deploy/load-adapter.d.ts +6 -0
- package/dist/esm/deploy/load-adapter.js +57 -0
- package/dist/esm/deploy/run.d.ts +12 -0
- package/dist/esm/deploy/run.js +52 -0
- package/dist/esm/deploy/schema.d.ts +6 -0
- package/dist/esm/deploy/schema.js +61 -0
- package/dist/esm/deploy/types.d.ts +43 -0
- package/dist/esm/deploy/types.js +2 -0
- package/dist/esm/e2e/command-builder.js +37 -18
- package/dist/esm/e2e/config.d.ts +2 -0
- package/dist/esm/e2e/config.js +6 -1
- package/dist/esm/e2e/load-host-adapter.d.ts +0 -4
- package/dist/esm/e2e/load-host-adapter.js +5 -14
- package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +6 -0
- package/dist/esm/hooks/pretool-guard/dev-routing.js +3 -2
- package/dist/esm/hooks/pretool-guard/runner.js +11 -6
- package/dist/esm/mcp/tools/_shared/runner-failure.d.ts +30 -0
- package/dist/esm/mcp/tools/_shared/runner-failure.js +45 -0
- package/dist/esm/mcp/tools/typecheck.js +20 -3
- package/dist/esm/package.json +3 -1
- package/dist/esm/test/command-builder.d.ts +1 -0
- package/dist/esm/test/command-builder.js +9 -5
- package/dist/esm/tool-runtime/resolve-runner.js +38 -10
- package/package.json +32 -18
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const DEPLOY_PLAN_SCHEMA_VERSION = 1;
|
|
3
|
+
const deployLaneSchema = z.union([
|
|
4
|
+
z.literal('dev'),
|
|
5
|
+
z.literal('preview_main'),
|
|
6
|
+
z.literal('prd'),
|
|
7
|
+
z.string().regex(/^preview_pr_\d+$/u, 'preview PR lanes must be preview_pr_<n>'),
|
|
8
|
+
]);
|
|
9
|
+
const envSchema = z.record(z.string(), z.string().optional());
|
|
10
|
+
const commandStepSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
kind: z.literal('command'),
|
|
13
|
+
id: z.string().min(1),
|
|
14
|
+
label: z.string().min(1).optional(),
|
|
15
|
+
command: z.string().min(1),
|
|
16
|
+
args: z.array(z.string()).optional(),
|
|
17
|
+
cwd: z.string().min(1).optional(),
|
|
18
|
+
env: envSchema.optional(),
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
const managedToolStepSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
kind: z.literal('managed-tool'),
|
|
24
|
+
id: z.string().min(1),
|
|
25
|
+
label: z.string().min(1).optional(),
|
|
26
|
+
tool: z.string().min(1),
|
|
27
|
+
args: z.array(z.string()).optional(),
|
|
28
|
+
cwd: z.string().min(1).optional(),
|
|
29
|
+
env: envSchema.optional(),
|
|
30
|
+
})
|
|
31
|
+
.strict();
|
|
32
|
+
const deployPlanSchema = z
|
|
33
|
+
.object({
|
|
34
|
+
schemaVersion: z.literal(DEPLOY_PLAN_SCHEMA_VERSION),
|
|
35
|
+
lane: deployLaneSchema,
|
|
36
|
+
provider: z.string().min(1),
|
|
37
|
+
requiredCredentials: z.array(z.string().min(1)),
|
|
38
|
+
steps: z.array(z.union([commandStepSchema, managedToolStepSchema])),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
export function isDeployLane(value) {
|
|
42
|
+
return deployLaneSchema.safeParse(value).success;
|
|
43
|
+
}
|
|
44
|
+
export function parseDeployLane(value) {
|
|
45
|
+
const result = deployLaneSchema.safeParse(value);
|
|
46
|
+
if (!result.success) {
|
|
47
|
+
throw new Error(`Invalid deploy lane "${value}". Use dev, preview_main, preview_pr_<n>, or prd.`);
|
|
48
|
+
}
|
|
49
|
+
return result.data;
|
|
50
|
+
}
|
|
51
|
+
export function validateDeployPlan(plan) {
|
|
52
|
+
const result = deployPlanSchema.safeParse(plan);
|
|
53
|
+
if (!result.success) {
|
|
54
|
+
const issues = result.error.issues
|
|
55
|
+
.map((issue) => ` - ${issue.path.join('.') || '<root>'}: ${issue.message}`)
|
|
56
|
+
.join('\n');
|
|
57
|
+
throw new Error(`Invalid deploy plan:\n${issues}`);
|
|
58
|
+
}
|
|
59
|
+
return result.data;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type DeployLane = 'dev' | 'preview_main' | `preview_pr_${number}` | 'prd';
|
|
2
|
+
export type DeployStep = {
|
|
3
|
+
readonly kind: 'command';
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly label?: string;
|
|
6
|
+
readonly command: string;
|
|
7
|
+
readonly args?: readonly string[];
|
|
8
|
+
readonly cwd?: string;
|
|
9
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: 'managed-tool';
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly label?: string;
|
|
14
|
+
readonly tool: string;
|
|
15
|
+
readonly args?: readonly string[];
|
|
16
|
+
readonly cwd?: string;
|
|
17
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
18
|
+
};
|
|
19
|
+
export interface DeployPlan {
|
|
20
|
+
readonly schemaVersion: 1;
|
|
21
|
+
readonly lane: DeployLane;
|
|
22
|
+
readonly provider: string;
|
|
23
|
+
readonly requiredCredentials: readonly string[];
|
|
24
|
+
readonly steps: readonly DeployStep[];
|
|
25
|
+
}
|
|
26
|
+
export interface DeployRequest {
|
|
27
|
+
readonly cwd: string;
|
|
28
|
+
readonly lane: DeployLane;
|
|
29
|
+
readonly dryRun: boolean;
|
|
30
|
+
readonly env: NodeJS.ProcessEnv;
|
|
31
|
+
readonly cloudflare?: unknown;
|
|
32
|
+
}
|
|
33
|
+
export interface DeployAdapter {
|
|
34
|
+
readonly createPlan: (request: DeployRequest) => DeployPlan | Promise<DeployPlan>;
|
|
35
|
+
}
|
|
36
|
+
export interface LoadedDeployAdapter {
|
|
37
|
+
readonly adapter: DeployAdapter;
|
|
38
|
+
readonly config: import('#e2e/config.js').WebpressoConfig;
|
|
39
|
+
readonly configPath: string;
|
|
40
|
+
readonly moduleSpecifier: string;
|
|
41
|
+
readonly exportName: string;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -15,13 +15,18 @@ function buildPlaywrightCommand(options) {
|
|
|
15
15
|
if (!step.configPath) {
|
|
16
16
|
throw new Error(`Step ${step.logName} uses runner "playwright" but does not define configPath.`);
|
|
17
17
|
}
|
|
18
|
-
const
|
|
18
|
+
const paths = resolveRunnerPaths(step.configPath, options.files ?? []);
|
|
19
19
|
const resolution = withBaseDir(getManagedRunner('playwright', {
|
|
20
20
|
outputPolicy: resolveOutputPolicy(options.outputPolicy, options.filterOutput),
|
|
21
|
-
}), baseDir);
|
|
22
|
-
const args = [
|
|
21
|
+
}), paths.baseDir);
|
|
22
|
+
const args = [
|
|
23
|
+
...resolution.args,
|
|
24
|
+
'test',
|
|
25
|
+
'--config',
|
|
26
|
+
resolution.usesBaseDir ? paths.relativeConfigArg : paths.rootConfigArg,
|
|
27
|
+
];
|
|
23
28
|
appendPlaywrightFlags(args, options);
|
|
24
|
-
args.push(...(step.fixedArgs ?? []), ...
|
|
29
|
+
args.push(...(step.fixedArgs ?? []), ...(resolution.usesBaseDir ? paths.relativeFiles : paths.rootFiles), ...(options.passthrough ?? []));
|
|
25
30
|
return { command: resolution.command, args };
|
|
26
31
|
}
|
|
27
32
|
function buildVitestE2eCommand(options) {
|
|
@@ -29,15 +34,20 @@ function buildVitestE2eCommand(options) {
|
|
|
29
34
|
if (!step.configPath) {
|
|
30
35
|
throw new Error(`Step ${step.logName} uses runner "vitest" but does not define configPath.`);
|
|
31
36
|
}
|
|
32
|
-
const
|
|
37
|
+
const paths = resolveRunnerPaths(step.configPath, options.files ?? []);
|
|
33
38
|
const resolution = withBaseDir(getManagedRunner('vitest', {
|
|
34
39
|
outputPolicy: resolveOutputPolicy(options.outputPolicy, options.filterOutput),
|
|
35
|
-
}), baseDir);
|
|
36
|
-
const args = [
|
|
40
|
+
}), paths.baseDir);
|
|
41
|
+
const args = [
|
|
42
|
+
...resolution.args,
|
|
43
|
+
'run',
|
|
44
|
+
'--config',
|
|
45
|
+
resolution.usesBaseDir ? paths.relativeConfigArg : paths.rootConfigArg,
|
|
46
|
+
];
|
|
37
47
|
if (options.workers !== undefined) {
|
|
38
48
|
args.push('--poolOptions.threads.maxThreads', String(options.workers));
|
|
39
49
|
}
|
|
40
|
-
args.push(...(step.fixedArgs ?? []), ...
|
|
50
|
+
args.push(...(step.fixedArgs ?? []), ...(resolution.usesBaseDir ? paths.relativeFiles : paths.rootFiles), ...(options.passthrough ?? []));
|
|
41
51
|
return { command: resolution.command, args };
|
|
42
52
|
}
|
|
43
53
|
function buildCustomCommand(options) {
|
|
@@ -79,19 +89,27 @@ function appendPlaywrightFlags(args, options) {
|
|
|
79
89
|
function resolveRunnerPaths(configPath, files) {
|
|
80
90
|
const normalizedConfigPath = configPath.replace(/\\/gu, '/');
|
|
81
91
|
const baseDir = path.posix.dirname(normalizedConfigPath);
|
|
82
|
-
const
|
|
92
|
+
const normalizedFiles = files.map((file) => file.replace(/\\/gu, '/'));
|
|
83
93
|
if (baseDir === '.') {
|
|
84
94
|
return {
|
|
85
95
|
baseDir,
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
rootConfigArg: normalizedConfigPath,
|
|
97
|
+
rootFiles: normalizedFiles,
|
|
98
|
+
relativeConfigArg: normalizedConfigPath,
|
|
99
|
+
relativeFiles: normalizedFiles,
|
|
88
100
|
};
|
|
89
101
|
}
|
|
90
102
|
return {
|
|
91
103
|
baseDir,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
rootConfigArg: normalizedConfigPath,
|
|
105
|
+
rootFiles: normalizedFiles.map((normalizedFile) => {
|
|
106
|
+
if (path.posix.isAbsolute(normalizedFile) || normalizedFile.startsWith(`${baseDir}/`)) {
|
|
107
|
+
return normalizedFile;
|
|
108
|
+
}
|
|
109
|
+
return `${baseDir}/${normalizedFile}`;
|
|
110
|
+
}),
|
|
111
|
+
relativeConfigArg: path.posix.basename(normalizedConfigPath),
|
|
112
|
+
relativeFiles: normalizedFiles.map((normalizedFile) => {
|
|
95
113
|
if (path.posix.isAbsolute(normalizedFile) || normalizedFile.startsWith(`${baseDir}/`)) {
|
|
96
114
|
return path.posix.relative(baseDir, normalizedFile);
|
|
97
115
|
}
|
|
@@ -100,13 +118,13 @@ function resolveRunnerPaths(configPath, files) {
|
|
|
100
118
|
};
|
|
101
119
|
}
|
|
102
120
|
function withBaseDir(resolution, baseDir) {
|
|
103
|
-
if (baseDir === '.')
|
|
104
|
-
return { command: resolution.command, args: [...resolution.args] };
|
|
105
|
-
}
|
|
121
|
+
if (baseDir === '.')
|
|
122
|
+
return { command: resolution.command, args: [...resolution.args], usesBaseDir: true };
|
|
106
123
|
if (resolution.command === 'vp') {
|
|
107
124
|
return {
|
|
108
125
|
command: resolution.command,
|
|
109
126
|
args: ['--dir', baseDir, ...resolution.args],
|
|
127
|
+
usesBaseDir: true,
|
|
110
128
|
};
|
|
111
129
|
}
|
|
112
130
|
const [wrappedCommand, ...wrappedArgs] = resolution.args;
|
|
@@ -114,9 +132,10 @@ function withBaseDir(resolution, baseDir) {
|
|
|
114
132
|
return {
|
|
115
133
|
command: resolution.command,
|
|
116
134
|
args: ['vp', '--dir', baseDir, ...wrappedArgs],
|
|
135
|
+
usesBaseDir: true,
|
|
117
136
|
};
|
|
118
137
|
}
|
|
119
|
-
return { command: resolution.command, args: [...resolution.args] };
|
|
138
|
+
return { command: resolution.command, args: [...resolution.args], usesBaseDir: false };
|
|
120
139
|
}
|
|
121
140
|
function resolveOutputPolicy(outputPolicy, filterOutput) {
|
|
122
141
|
if (outputPolicy)
|
package/dist/esm/e2e/config.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ declare const webpressoConfigSchema: z.ZodObject<{
|
|
|
16
16
|
hostAdapterExport: z.ZodOptional<z.ZodString>;
|
|
17
17
|
}, z.core.$strict>>;
|
|
18
18
|
deploy: z.ZodOptional<z.ZodObject<{
|
|
19
|
+
adapterModule: z.ZodOptional<z.ZodString>;
|
|
20
|
+
adapterExport: z.ZodOptional<z.ZodString>;
|
|
19
21
|
cloudflare: z.ZodOptional<z.ZodObject<{
|
|
20
22
|
lanes: z.ZodObject<{
|
|
21
23
|
dev: z.ZodObject<{
|
package/dist/esm/e2e/config.js
CHANGED
|
@@ -51,7 +51,10 @@ const cloudflareDurableObjectBindingSchema = z
|
|
|
51
51
|
.object({
|
|
52
52
|
name: z.string().min(1, 'durableObjectBindings[].name must not be empty.'),
|
|
53
53
|
className: z.string().min(1, 'durableObjectBindings[].className must not be empty.'),
|
|
54
|
-
scriptName: z
|
|
54
|
+
scriptName: z
|
|
55
|
+
.string()
|
|
56
|
+
.min(1, 'durableObjectBindings[].scriptName must not be empty.')
|
|
57
|
+
.optional(),
|
|
55
58
|
})
|
|
56
59
|
.strict();
|
|
57
60
|
const cloudflareTargetSchema = z
|
|
@@ -129,6 +132,8 @@ const cloudflareDeployConfigSchema = z
|
|
|
129
132
|
.strict();
|
|
130
133
|
const deployWebpressoConfigSchema = z
|
|
131
134
|
.object({
|
|
135
|
+
adapterModule: z.string().min(1, 'deploy.adapterModule must not be empty.').optional(),
|
|
136
|
+
adapterExport: z.string().min(1, 'deploy.adapterExport must not be empty.').optional(),
|
|
132
137
|
cloudflare: cloudflareDeployConfigSchema.optional(),
|
|
133
138
|
})
|
|
134
139
|
.strict();
|
|
@@ -22,10 +22,6 @@ export declare class WebpressoConfigExportError extends Error {
|
|
|
22
22
|
readonly exportName: string;
|
|
23
23
|
constructor(configPath: string, exportName?: string);
|
|
24
24
|
}
|
|
25
|
-
export declare class WebpressoConfigAmbiguousError extends Error {
|
|
26
|
-
readonly configPaths: readonly string[];
|
|
27
|
-
constructor(configPaths: readonly string[]);
|
|
28
|
-
}
|
|
29
25
|
export declare class HostAdapterModuleLoadError extends Error {
|
|
30
26
|
readonly moduleSpecifier: string;
|
|
31
27
|
readonly configPath: string;
|
|
@@ -23,14 +23,6 @@ export class WebpressoConfigExportError extends Error {
|
|
|
23
23
|
this.name = 'WebpressoConfigExportError';
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
export class WebpressoConfigAmbiguousError extends Error {
|
|
27
|
-
configPaths;
|
|
28
|
-
constructor(configPaths) {
|
|
29
|
-
super(`Multiple Webpresso config files found: ${configPaths.join(', ')}`);
|
|
30
|
-
this.configPaths = configPaths;
|
|
31
|
-
this.name = 'WebpressoConfigAmbiguousError';
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
26
|
export class HostAdapterModuleLoadError extends Error {
|
|
35
27
|
moduleSpecifier;
|
|
36
28
|
configPath;
|
|
@@ -65,12 +57,11 @@ export function resolveWebpressoConfigPath(cwd = process.cwd()) {
|
|
|
65
57
|
}
|
|
66
58
|
export function findWebpressoConfigPath(cwd = process.cwd()) {
|
|
67
59
|
for (const searchDir of getSearchDirectories(cwd)) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return configPaths[0];
|
|
60
|
+
for (const candidate of WEBPRESSO_CONFIG_CANDIDATES) {
|
|
61
|
+
const configPath = resolve(searchDir, candidate.fileName);
|
|
62
|
+
if (existsSync(configPath)) {
|
|
63
|
+
return configPath;
|
|
64
|
+
}
|
|
74
65
|
}
|
|
75
66
|
}
|
|
76
67
|
return null;
|
|
@@ -15,7 +15,13 @@ export interface RouteDecision {
|
|
|
15
15
|
export declare function normalizeCommandForRouting(command: string): string;
|
|
16
16
|
export declare function extractRoutableCommandsFromToolInput(input: {
|
|
17
17
|
tool_name?: string;
|
|
18
|
+
toolName?: string;
|
|
19
|
+
tool?: string;
|
|
20
|
+
name?: string;
|
|
18
21
|
tool_input?: Record<string, unknown>;
|
|
22
|
+
toolInput?: Record<string, unknown>;
|
|
23
|
+
input?: Record<string, unknown>;
|
|
24
|
+
arguments?: Record<string, unknown>;
|
|
19
25
|
}): string[];
|
|
20
26
|
export declare function routeCommand(command: string, _sessionId?: string): RouteDecision | null;
|
|
21
27
|
//# sourceMappingURL=dev-routing.d.ts.map
|
|
@@ -585,9 +585,10 @@ function isContextModeTool(toolName) {
|
|
|
585
585
|
return /(?:^|[._-]|__)ctx_(?:batch_)?execute$/u.test(toolName);
|
|
586
586
|
}
|
|
587
587
|
export function extractRoutableCommandsFromToolInput(input) {
|
|
588
|
-
|
|
588
|
+
const toolName = input.tool_name ?? input.toolName ?? input.tool ?? input.name;
|
|
589
|
+
if (!isContextModeTool(toolName))
|
|
589
590
|
return [];
|
|
590
|
-
const toolInput = input.tool_input;
|
|
591
|
+
const toolInput = input.tool_input ?? input.toolInput ?? input.input ?? input.arguments;
|
|
591
592
|
if (!toolInput || typeof toolInput !== 'object')
|
|
592
593
|
return [];
|
|
593
594
|
const commands = [];
|
|
@@ -101,23 +101,28 @@ export function processValidation(inputJson) {
|
|
|
101
101
|
const input = parseToolInput(inputJson);
|
|
102
102
|
const command = isBashInput(input) ? getCommand(input) : null;
|
|
103
103
|
const routableCommands = [
|
|
104
|
-
...(command ? [command] : []),
|
|
105
|
-
...extractRoutableCommandsFromToolInput(input)
|
|
104
|
+
...(command ? [{ command, alreadySandboxed: false }] : []),
|
|
105
|
+
...extractRoutableCommandsFromToolInput(input).map((routedCommand) => ({
|
|
106
|
+
command: routedCommand,
|
|
107
|
+
alreadySandboxed: true,
|
|
108
|
+
})),
|
|
106
109
|
];
|
|
107
110
|
for (const routedCommand of routableCommands) {
|
|
108
|
-
const decision = routeCommand(routedCommand);
|
|
111
|
+
const decision = routeCommand(routedCommand.command);
|
|
109
112
|
if (decision !== null) {
|
|
110
113
|
if (decision.action.action === 'deny') {
|
|
111
114
|
// Phase 1: Dev-workflow routing — always authoritative (MCP-first)
|
|
112
115
|
writeDenyDecision(decision.action.guidance);
|
|
113
116
|
process.exit(0);
|
|
114
117
|
}
|
|
115
|
-
else if (decision.action.action === 'sandbox') {
|
|
116
|
-
// Phase 2: Context-mode sandbox routing —
|
|
118
|
+
else if (decision.action.action === 'sandbox' && !routedCommand.alreadySandboxed) {
|
|
119
|
+
// Phase 2: Context-mode sandbox routing — fire only for raw tool calls.
|
|
120
|
+
// Commands already inside ctx_execute/ctx_batch_execute are already in
|
|
121
|
+
// the requested sandbox; re-denying them creates a ctx_execute loop.
|
|
117
122
|
writeDenyDecision(decision.action.guidance);
|
|
118
123
|
process.exit(0);
|
|
119
124
|
}
|
|
120
|
-
// 'passthrough'
|
|
125
|
+
// 'passthrough' or already-sandboxed sandbox redirects → Phase 3
|
|
121
126
|
}
|
|
122
127
|
}
|
|
123
128
|
// Phase 3: Security validators (existing pipeline)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared runner-failure bounding for the check tools (`wp_typecheck`, `wp_lint`,
|
|
3
|
+
* `wp_test`).
|
|
4
|
+
*
|
|
5
|
+
* A check that exits non-zero but yields ZERO parseable structured results is a
|
|
6
|
+
* runner/launcher failure — e.g. a missing `vp`/`tsc`/test-runner binary
|
|
7
|
+
* printing a Node `MODULE_NOT_FOUND` stack — not a genuine type/lint/test
|
|
8
|
+
* failure. Left alone, that raw output falls through each tool's transform to
|
|
9
|
+
* the generic passthrough (clipped only at 4000 chars), so it can become the
|
|
10
|
+
* leaf's measured `bytes` and blow the compact QA evidence budget (≤800/leaf),
|
|
11
|
+
* shipping an unbounded blob masquerading as tool output.
|
|
12
|
+
*
|
|
13
|
+
* `isRunnerFailure` detects the case from a count the caller already has (tsc
|
|
14
|
+
* errors / oxlint issues / parsed test failures), so it never misclassifies a
|
|
15
|
+
* real failure that produced structured results. `boundRunnerFailureEvidence`
|
|
16
|
+
* clips the evidence well under the budget and persists the full output to a log
|
|
17
|
+
* (truthful, not dropped). The caller keeps `passed: false` so the failure stays
|
|
18
|
+
* loud.
|
|
19
|
+
*/
|
|
20
|
+
import type { TransformResult } from '#output-transforms/index';
|
|
21
|
+
export declare function stripTransform(result: TransformResult): Omit<TransformResult, 'transform'>;
|
|
22
|
+
export declare function isRunnerFailure(input: {
|
|
23
|
+
passed: boolean;
|
|
24
|
+
timedOut: boolean;
|
|
25
|
+
aborted: boolean;
|
|
26
|
+
parsedCount: number;
|
|
27
|
+
output: string;
|
|
28
|
+
}): boolean;
|
|
29
|
+
export declare function boundRunnerFailureEvidence(output: string, toolName: string): Omit<TransformResult, 'transform'>;
|
|
30
|
+
//# sourceMappingURL=runner-failure.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared runner-failure bounding for the check tools (`wp_typecheck`, `wp_lint`,
|
|
3
|
+
* `wp_test`).
|
|
4
|
+
*
|
|
5
|
+
* A check that exits non-zero but yields ZERO parseable structured results is a
|
|
6
|
+
* runner/launcher failure — e.g. a missing `vp`/`tsc`/test-runner binary
|
|
7
|
+
* printing a Node `MODULE_NOT_FOUND` stack — not a genuine type/lint/test
|
|
8
|
+
* failure. Left alone, that raw output falls through each tool's transform to
|
|
9
|
+
* the generic passthrough (clipped only at 4000 chars), so it can become the
|
|
10
|
+
* leaf's measured `bytes` and blow the compact QA evidence budget (≤800/leaf),
|
|
11
|
+
* shipping an unbounded blob masquerading as tool output.
|
|
12
|
+
*
|
|
13
|
+
* `isRunnerFailure` detects the case from a count the caller already has (tsc
|
|
14
|
+
* errors / oxlint issues / parsed test failures), so it never misclassifies a
|
|
15
|
+
* real failure that produced structured results. `boundRunnerFailureEvidence`
|
|
16
|
+
* clips the evidence well under the budget and persists the full output to a log
|
|
17
|
+
* (truthful, not dropped). The caller keeps `passed: false` so the failure stays
|
|
18
|
+
* loud.
|
|
19
|
+
*/
|
|
20
|
+
import { clipRawOutput } from './result.js';
|
|
21
|
+
const RUNNER_FAILURE_EVIDENCE_BUDGET = 600;
|
|
22
|
+
export function stripTransform(result) {
|
|
23
|
+
const { transform: _transform, ...rest } = result;
|
|
24
|
+
return rest;
|
|
25
|
+
}
|
|
26
|
+
export function isRunnerFailure(input) {
|
|
27
|
+
return (!input.passed &&
|
|
28
|
+
!input.timedOut &&
|
|
29
|
+
!input.aborted &&
|
|
30
|
+
input.parsedCount === 0 &&
|
|
31
|
+
input.output.trim().length > 0);
|
|
32
|
+
}
|
|
33
|
+
export function boundRunnerFailureEvidence(output, toolName) {
|
|
34
|
+
const clipped = clipRawOutput(output, RUNNER_FAILURE_EVIDENCE_BUDGET, { toolName });
|
|
35
|
+
const rawBytes = Buffer.byteLength(output);
|
|
36
|
+
const bytes = Buffer.byteLength(clipped.rawOutput ?? '');
|
|
37
|
+
return {
|
|
38
|
+
...clipped,
|
|
39
|
+
failures: [],
|
|
40
|
+
tier: 3,
|
|
41
|
+
bytes,
|
|
42
|
+
tokensSaved: Math.max(0, rawBytes - bytes),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=runner-failure.js.map
|
|
@@ -17,6 +17,7 @@ import { applyOutputTransform } from '#output-transforms/index';
|
|
|
17
17
|
import { getManagedRunner } from '#tool-runtime';
|
|
18
18
|
import { resolveProjectRoot } from './_shared/project-root.js';
|
|
19
19
|
import { createSummaryOutputSchema, createSummaryResult } from './_shared/result.js';
|
|
20
|
+
import { boundRunnerFailureEvidence, isRunnerFailure, stripTransform, } from './_shared/runner-failure.js';
|
|
20
21
|
import { isRunFailure, runCommand } from './_shared/run-command.js';
|
|
21
22
|
const inputSchema = z.object({
|
|
22
23
|
cwd: z.string().optional(),
|
|
@@ -115,6 +116,8 @@ function summarizeTypecheckResult(options) {
|
|
|
115
116
|
return 'typecheck timed out';
|
|
116
117
|
if (options.aborted)
|
|
117
118
|
return 'typecheck aborted';
|
|
119
|
+
if (options.failedWithoutDiagnostics)
|
|
120
|
+
return 'typecheck failed to run (no diagnostics parsed)';
|
|
118
121
|
if (options.passed)
|
|
119
122
|
return 'typecheck passed';
|
|
120
123
|
return `typecheck failed with ${options.errorCount} error${options.errorCount === 1 ? '' : 's'}`;
|
|
@@ -169,12 +172,26 @@ const tool = {
|
|
|
169
172
|
const passed = runs.every((r) => r.exitCode === 0);
|
|
170
173
|
const timedOut = runs.some((r) => r.timedOut);
|
|
171
174
|
const aborted = runs.some((r) => r.aborted);
|
|
172
|
-
const
|
|
173
|
-
|
|
175
|
+
const combinedOutput = [combinedStdout, combinedStderr].filter(Boolean).join('');
|
|
176
|
+
const failedWithoutDiagnostics = isRunnerFailure({
|
|
177
|
+
passed,
|
|
178
|
+
timedOut,
|
|
179
|
+
aborted,
|
|
180
|
+
parsedCount: errors.length,
|
|
181
|
+
output: combinedOutput,
|
|
174
182
|
});
|
|
183
|
+
const compact = failedWithoutDiagnostics
|
|
184
|
+
? boundRunnerFailureEvidence(combinedOutput, 'wp_typecheck')
|
|
185
|
+
: stripTransform(applyOutputTransform(combinedOutput, { toolName: 'wp_typecheck' }));
|
|
175
186
|
const payload = {
|
|
176
187
|
passed,
|
|
177
|
-
summary: summarizeTypecheckResult({
|
|
188
|
+
summary: summarizeTypecheckResult({
|
|
189
|
+
passed,
|
|
190
|
+
errorCount: errors.length,
|
|
191
|
+
timedOut,
|
|
192
|
+
aborted,
|
|
193
|
+
failedWithoutDiagnostics,
|
|
194
|
+
}),
|
|
178
195
|
counts: { errorCount: errors.length },
|
|
179
196
|
details: { errors },
|
|
180
197
|
...compact,
|
package/dist/esm/package.json
CHANGED
|
@@ -95,6 +95,8 @@
|
|
|
95
95
|
"#types/execution-backend": "./blueprint/types/execution-backend.js",
|
|
96
96
|
"#types/execution-backend.js": "./blueprint/types/execution-backend.js",
|
|
97
97
|
"#*.js": "./blueprint/*.js",
|
|
98
|
-
"#*": "./blueprint/*.js"
|
|
98
|
+
"#*": "./blueprint/*.js",
|
|
99
|
+
"#deploy/*.js": "./deploy/*.js",
|
|
100
|
+
"#deploy/*": "./deploy/*.js"
|
|
99
101
|
}
|
|
100
102
|
}
|
|
@@ -25,5 +25,6 @@ export interface TestCommandOptions {
|
|
|
25
25
|
export declare function buildTestCommand(target: ResolvedTestTarget, options?: TestCommandOptions): CommandConfig;
|
|
26
26
|
export declare function buildVpTestCommand(filters: readonly string[], options?: TestCommandOptions): CommandConfig;
|
|
27
27
|
export declare function buildVitestCommand(files: readonly string[], options?: TestCommandOptions): CommandConfig;
|
|
28
|
+
export declare function buildStrykerCommand(options?: TestCommandOptions): CommandConfig;
|
|
28
29
|
export declare function getVpTestTask(options: Pick<TestCommandOptions, 'mutation' | 'workers' | 'watch'>): string;
|
|
29
30
|
//# sourceMappingURL=command-builder.d.ts.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getManagedRunner } from '#tool-runtime';
|
|
2
|
-
import { getPackageScript, isRecursiveWpScript
|
|
2
|
+
import { getPackageScript, isRecursiveWpScript } from '#cli/package-scripts.js';
|
|
3
3
|
export function buildTestCommand(target, options = {}) {
|
|
4
4
|
if (target.type === 'all' && shouldBypassRecursiveWpTest(options.cwd ?? process.cwd())) {
|
|
5
|
-
return buildVitestCommand([], options);
|
|
5
|
+
return options.mutation ? buildStrykerCommand(options) : buildVitestCommand([], options);
|
|
6
6
|
}
|
|
7
7
|
if (target.type === 'file') {
|
|
8
8
|
return buildVitestCommand(target.values, options);
|
|
@@ -56,6 +56,12 @@ export function buildVitestCommand(files, options = {}) {
|
|
|
56
56
|
});
|
|
57
57
|
return { command: resolution.command, args: [...resolution.args, ...args] };
|
|
58
58
|
}
|
|
59
|
+
export function buildStrykerCommand(options = {}) {
|
|
60
|
+
const resolution = getManagedRunner('stryker', {
|
|
61
|
+
outputPolicy: resolveOutputPolicy(options.outputPolicy, options.filterOutput),
|
|
62
|
+
});
|
|
63
|
+
return { command: resolution.command, args: [...resolution.args, 'run', 'stryker.config.ts'] };
|
|
64
|
+
}
|
|
59
65
|
export function getVpTestTask(options) {
|
|
60
66
|
if (options.mutation)
|
|
61
67
|
return 'test:mutation';
|
|
@@ -116,8 +122,6 @@ function resolveOutputPolicy(outputPolicy, filterOutput) {
|
|
|
116
122
|
}
|
|
117
123
|
function shouldBypassRecursiveWpTest(cwd) {
|
|
118
124
|
const testScript = getPackageScript(cwd, 'test');
|
|
119
|
-
|
|
120
|
-
return false;
|
|
121
|
-
return packageUsesVitest(cwd);
|
|
125
|
+
return Boolean(testScript && isRecursiveWpScript(testScript, 'test'));
|
|
122
126
|
}
|
|
123
127
|
//# sourceMappingURL=command-builder.js.map
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
1
5
|
const MANAGED_TOOL_PREFIX = {
|
|
2
|
-
oxfmt: {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
oxfmt: { packageName: 'oxfmt', binName: 'oxfmt' },
|
|
7
|
+
oxlint: { packageName: 'oxlint', binName: 'oxlint' },
|
|
8
|
+
playwright: { packageName: '@playwright/test', binName: 'playwright' },
|
|
9
|
+
stryker: { packageName: '@stryker-mutator/core', binName: 'stryker' },
|
|
10
|
+
tsc: { packageName: 'typescript', binName: 'tsc' },
|
|
11
|
+
tsx: { packageName: 'tsx', binName: 'tsx' },
|
|
12
|
+
vite: { packageName: 'vite', binName: 'vite' },
|
|
13
|
+
vitest: { packageName: 'vitest', binName: 'vitest' },
|
|
6
14
|
vp: { command: 'vp', args: [] },
|
|
15
|
+
wrangler: { packageName: 'wrangler', binName: 'wrangler' },
|
|
7
16
|
};
|
|
8
17
|
function withOptionalRtk(resolution, outputPolicy) {
|
|
9
18
|
if (outputPolicy !== 'rtk-filtered')
|
|
@@ -22,12 +31,7 @@ export function resolveRunner(tool, options = {}) {
|
|
|
22
31
|
const outputPolicy = options.outputPolicy ?? (options.filterOutput === false ? 'structured' : 'rtk-filtered');
|
|
23
32
|
const managed = MANAGED_TOOL_PREFIX[normalized];
|
|
24
33
|
if (managed) {
|
|
25
|
-
return withOptionalRtk(
|
|
26
|
-
tool: normalized,
|
|
27
|
-
command: managed.command,
|
|
28
|
-
args: [...managed.args],
|
|
29
|
-
source: 'managed',
|
|
30
|
-
}, outputPolicy);
|
|
34
|
+
return withOptionalRtk(resolveManagedTool(normalized, managed), outputPolicy);
|
|
31
35
|
}
|
|
32
36
|
if (options.fallbackCommand) {
|
|
33
37
|
return withOptionalRtk({
|
|
@@ -39,4 +43,28 @@ export function resolveRunner(tool, options = {}) {
|
|
|
39
43
|
}
|
|
40
44
|
throw new Error(`No managed runtime runner is defined for tool "${normalized}"`);
|
|
41
45
|
}
|
|
46
|
+
function resolveManagedTool(tool, spec) {
|
|
47
|
+
if ('command' in spec) {
|
|
48
|
+
return { tool, command: spec.command, args: [...spec.args], source: 'managed' };
|
|
49
|
+
}
|
|
50
|
+
const binPath = resolvePackageBin(spec.packageName, spec.binName);
|
|
51
|
+
if (binPath) {
|
|
52
|
+
return { tool, command: binPath, args: [...(spec.fallbackArgs ?? [])], source: 'managed' };
|
|
53
|
+
}
|
|
54
|
+
return { tool, command: 'vp', args: ['exec', spec.binName], source: 'fallback' };
|
|
55
|
+
}
|
|
56
|
+
function resolvePackageBin(packageName, binName) {
|
|
57
|
+
try {
|
|
58
|
+
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
|
59
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
60
|
+
const relativeBin = typeof packageJson.bin === 'string' ? packageJson.bin : packageJson.bin?.[binName];
|
|
61
|
+
if (!relativeBin)
|
|
62
|
+
return null;
|
|
63
|
+
const absoluteBin = resolve(dirname(packageJsonPath), relativeBin);
|
|
64
|
+
return existsSync(absoluteBin) ? absoluteBin : null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
42
70
|
//# sourceMappingURL=resolve-runner.js.map
|