@webpresso/agent-kit 0.25.0 → 0.26.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/esm/audit/toolchain-isolation.d.ts +3 -0
- package/dist/esm/audit/toolchain-isolation.js +139 -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 +30 -2
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +44 -17
- package/dist/esm/cli/commands/init/scaffolders/codex-cli/index.js +5 -1
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +29 -1
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +72 -2
- 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/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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Webpresso agent-kit Claude Code plugin: blueprints, skills, hooks, MCP server",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.26.1"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -23,5 +23,5 @@
|
|
|
23
23
|
]
|
|
24
24
|
}
|
|
25
25
|
],
|
|
26
|
-
"version": "0.
|
|
26
|
+
"version": "0.26.1"
|
|
27
27
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const FORBIDDEN_DEPENDENCY_PATTERNS = [
|
|
4
|
+
/^typescript$/u,
|
|
5
|
+
/^vite$/u,
|
|
6
|
+
/^vitest$/u,
|
|
7
|
+
/^@stryker-mutator\//u,
|
|
8
|
+
/^@playwright\/test$/u,
|
|
9
|
+
/^wrangler$/u,
|
|
10
|
+
/^oxlint$/u,
|
|
11
|
+
/^oxfmt$/u,
|
|
12
|
+
/^tsx$/u,
|
|
13
|
+
];
|
|
14
|
+
const FORBIDDEN_SCRIPT_PATTERNS = [
|
|
15
|
+
/(^|\s)(tsc|vite|vitest|stryker|playwright|wrangler|oxlint|oxfmt|tsx)(\s|$)/u,
|
|
16
|
+
/node\s+\.\/node_modules\/(typescript|vite|vitest|wrangler|oxlint|tsx)\//u,
|
|
17
|
+
];
|
|
18
|
+
const ALLOWED_SCRIPT_PREFIXES = ['wp ', 'vp run ', 'vp run -r '];
|
|
19
|
+
export function auditToolchainIsolation(root) {
|
|
20
|
+
const packagePaths = findPackageJsonFiles(root);
|
|
21
|
+
const violations = [];
|
|
22
|
+
for (const packagePath of packagePaths) {
|
|
23
|
+
const pkg = readPackageJson(packagePath);
|
|
24
|
+
if (!pkg) {
|
|
25
|
+
violations.push({ file: packagePath, message: 'package.json must be valid JSON' });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (isExemptPackage(root, packagePath, pkg))
|
|
29
|
+
continue;
|
|
30
|
+
for (const field of [
|
|
31
|
+
'dependencies',
|
|
32
|
+
'devDependencies',
|
|
33
|
+
'optionalDependencies',
|
|
34
|
+
'peerDependencies',
|
|
35
|
+
]) {
|
|
36
|
+
for (const depName of Object.keys(pkg[field] ?? {})) {
|
|
37
|
+
if (!isForbiddenDependency(depName))
|
|
38
|
+
continue;
|
|
39
|
+
violations.push({
|
|
40
|
+
file: packagePath,
|
|
41
|
+
message: `${field}.${depName} is toolchain-owned; route it through @webpresso/agent-kit/wp instead of declaring it directly`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const [scriptName, scriptValue] of Object.entries(pkg.scripts ?? {})) {
|
|
46
|
+
if (typeof scriptValue !== 'string')
|
|
47
|
+
continue;
|
|
48
|
+
if (isAllowedScript(scriptValue))
|
|
49
|
+
continue;
|
|
50
|
+
if (!FORBIDDEN_SCRIPT_PATTERNS.some((pattern) => pattern.test(scriptValue)))
|
|
51
|
+
continue;
|
|
52
|
+
violations.push({
|
|
53
|
+
file: packagePath,
|
|
54
|
+
message: `script "${scriptName}" invokes a toolchain binary directly; use wp-managed commands instead`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
ok: violations.length === 0,
|
|
60
|
+
title: 'Toolchain isolation',
|
|
61
|
+
checked: packagePaths.length,
|
|
62
|
+
violations,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function isExemptPackage(root, packagePath, pkg) {
|
|
66
|
+
if (pkg.name === '@webpresso/agent-kit')
|
|
67
|
+
return true;
|
|
68
|
+
const relativePath = path.relative(root, packagePath).split(path.sep).join('/');
|
|
69
|
+
// Catalog skill templates are sample project manifests shipped by agent-kit,
|
|
70
|
+
// not live consumer package manifests. They intentionally demonstrate raw
|
|
71
|
+
// framework CLIs in generated starter content and should not make agent-kit's
|
|
72
|
+
// own audit fail.
|
|
73
|
+
const unpackedPath = relativePath.replace(/^\.webpresso-packed-surface\//u, '');
|
|
74
|
+
if (unpackedPath.startsWith('catalog/') && unpackedPath.includes('/templates/'))
|
|
75
|
+
return true;
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
function findPackageJsonFiles(root) {
|
|
79
|
+
const files = [];
|
|
80
|
+
walk(root, files);
|
|
81
|
+
return files.sort();
|
|
82
|
+
}
|
|
83
|
+
function walk(dir, files) {
|
|
84
|
+
for (const entry of safeReadDir(dir)) {
|
|
85
|
+
const absolute = path.join(dir, entry.name);
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
if (!shouldSkipDirectory(entry.name))
|
|
88
|
+
walk(absolute, files);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (entry.isFile() && entry.name === 'package.json')
|
|
92
|
+
files.push(absolute);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function safeReadDir(dir) {
|
|
96
|
+
try {
|
|
97
|
+
return existsSync(dir) ? readdirSync(dir, { withFileTypes: true }) : [];
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function shouldSkipDirectory(name) {
|
|
104
|
+
return [
|
|
105
|
+
'.git',
|
|
106
|
+
'node_modules',
|
|
107
|
+
'dist',
|
|
108
|
+
'build',
|
|
109
|
+
'.turbo',
|
|
110
|
+
'.next',
|
|
111
|
+
'.wrangler',
|
|
112
|
+
'.agent',
|
|
113
|
+
'.agents',
|
|
114
|
+
'.omx',
|
|
115
|
+
'.omc',
|
|
116
|
+
'.codex',
|
|
117
|
+
// Gitignored Claude Code agent surface — agent worktree scratch under
|
|
118
|
+
// .claude/worktrees/* carries vendored package manifests that are not the
|
|
119
|
+
// repo's own packages; walking it produces false positives on local dev
|
|
120
|
+
// machines and in consumer repos that run agent worktrees.
|
|
121
|
+
'.claude',
|
|
122
|
+
].includes(name);
|
|
123
|
+
}
|
|
124
|
+
function readPackageJson(file) {
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function isForbiddenDependency(depName) {
|
|
133
|
+
return FORBIDDEN_DEPENDENCY_PATTERNS.some((pattern) => pattern.test(depName));
|
|
134
|
+
}
|
|
135
|
+
function isAllowedScript(script) {
|
|
136
|
+
const trimmed = script.trim();
|
|
137
|
+
return ALLOWED_SCRIPT_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=toolchain-isolation.js.map
|
package/dist/esm/cli/cli.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Lazy-loads subcommand modules based on the first argv to keep startup
|
|
6
6
|
* cheap. Modeled on apps/cli-wp/src/cli.ts.
|
|
7
7
|
*/
|
|
8
|
-
declare const SUPPORTED_COMMANDS: readonly ["blueprint", "config", "roadmap", "sync", "audit", "compile", "rule", "skill", "skills", "docs", "setup", "init", "dev", "doctor", "err", "test", "e2e", "ci", "typecheck", "lint", "format", "tech-debt", "worktree", "mcp", "hook", "hooks", "gain", "bench", "install", "add", "remove", "update", "exec", "run"];
|
|
8
|
+
declare const SUPPORTED_COMMANDS: readonly ["blueprint", "config", "roadmap", "sync", "audit", "compile", "rule", "skill", "skills", "docs", "setup", "init", "dev", "deploy", "doctor", "err", "test", "e2e", "ci", "typecheck", "lint", "format", "tech-debt", "worktree", "mcp", "hook", "hooks", "gain", "bench", "install", "add", "remove", "update", "exec", "run"];
|
|
9
9
|
export { SUPPORTED_COMMANDS };
|
|
10
10
|
export declare function main(): Promise<number>;
|
|
11
11
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/esm/cli/cli.js
CHANGED
|
@@ -27,6 +27,7 @@ const SUPPORTED_COMMANDS = [
|
|
|
27
27
|
'setup',
|
|
28
28
|
'init',
|
|
29
29
|
'dev',
|
|
30
|
+
'deploy',
|
|
30
31
|
'doctor',
|
|
31
32
|
'err',
|
|
32
33
|
'test',
|
|
@@ -209,6 +210,11 @@ export async function main() {
|
|
|
209
210
|
registerDevCommand(cli);
|
|
210
211
|
break;
|
|
211
212
|
}
|
|
213
|
+
case 'deploy': {
|
|
214
|
+
const { registerDeployCommand } = await import('./commands/deploy.js');
|
|
215
|
+
registerDeployCommand(cli);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
212
218
|
case 'doctor': {
|
|
213
219
|
const { registerDoctorCommand } = await import('./commands/doctor.js');
|
|
214
220
|
registerDoctorCommand(cli);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RepoAuditResult } from '#audit/repo-guardrails';
|
|
2
|
-
export type AuditKind = 'tph' | 'tph-e2e' | 'bundle-budget' | 'commit-message' | 'blueprint-lifecycle' | 'roadmap-links' | 'docs-frontmatter' | 'catalog-drift' | 'package-surface' | 'agents' | 'tech-debt' | 'no-relative-parent-imports' | 'no-link-protocol' | 'vision' | 'bucket-boundary' | 'skill-sizes' | 'broken-refs' | 'memory-rotation' | 'gitignore-agent-surfaces' | 'memory-unified' | 'compile-drift' | 'architecture-drift' | 'cloudflare-deploy-contract' | 'absolute-path-policy' | 'agent-cost' | 'blueprint-db-consistency' | 'blueprint-lifecycle-sql' | 'tech-debt-cadence' | 'cross-repo-correlation' | 'ai-contracts' | 'mutation' | 'quality' | 'guardrails' | 'hook-surface' | 'no-relative-package-scripts';
|
|
2
|
+
export type AuditKind = 'tph' | 'tph-e2e' | 'bundle-budget' | 'commit-message' | 'blueprint-lifecycle' | 'roadmap-links' | 'docs-frontmatter' | 'catalog-drift' | 'package-surface' | 'agents' | 'tech-debt' | 'no-relative-parent-imports' | 'no-link-protocol' | 'vision' | 'bucket-boundary' | 'skill-sizes' | 'broken-refs' | 'memory-rotation' | 'gitignore-agent-surfaces' | 'memory-unified' | 'compile-drift' | 'architecture-drift' | 'cloudflare-deploy-contract' | 'toolchain-isolation' | 'absolute-path-policy' | 'agent-cost' | 'blueprint-db-consistency' | 'blueprint-lifecycle-sql' | 'tech-debt-cadence' | 'cross-repo-correlation' | 'ai-contracts' | 'mutation' | 'quality' | 'guardrails' | 'hook-surface' | 'no-relative-package-scripts';
|
|
3
3
|
export type AuditOutcome = {
|
|
4
4
|
kind: 'invalid-usage';
|
|
5
5
|
message: string;
|
|
@@ -59,6 +59,7 @@ const REPO_AUDIT_REGISTRY = {
|
|
|
59
59
|
'no-legacy-cli-bin': async (root) => (await import('#audit/no-legacy-cli-bin')).auditNoLegacyCliBin(root),
|
|
60
60
|
'architecture-drift': async (root) => (await import('#audit/architecture-drift')).auditArchitectureDrift(root),
|
|
61
61
|
'cloudflare-deploy-contract': async (root) => (await import('#audit/cloudflare-deploy-contract')).auditCloudflareDeployContract(root),
|
|
62
|
+
'toolchain-isolation': async (root) => (await import('#audit/toolchain-isolation')).auditToolchainIsolation(root),
|
|
62
63
|
'absolute-path-policy': async (root) => (await import('#audit/absolute-path-policy')).auditAbsolutePathPolicy(root),
|
|
63
64
|
'agent-cost': async (root) => (await import('#audit/agent-cost')).auditAgentCost(root),
|
|
64
65
|
'blueprint-db-consistency': async (root) => (await import('#audit/blueprint-db-consistency')).auditBlueprintDbConsistency(root),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { runDeployPlan } from '#deploy/run.js';
|
|
2
|
+
export const DEPLOY_COMMAND_HELP = [
|
|
3
|
+
'Run a consumer-owned deploy adapter through the managed wp deploy surface.',
|
|
4
|
+
'',
|
|
5
|
+
'Examples:',
|
|
6
|
+
' wp deploy --lane prd --dry-run',
|
|
7
|
+
' wp deploy --lane preview_pr_123 --plan-json',
|
|
8
|
+
].join('\n');
|
|
9
|
+
export function registerDeployCommand(cli) {
|
|
10
|
+
cli
|
|
11
|
+
.command('deploy', DEPLOY_COMMAND_HELP)
|
|
12
|
+
.option('--lane <lane>', 'Deploy lane: dev, preview_main, preview_pr_<n>, or prd')
|
|
13
|
+
.option('--dry-run', 'Ask the adapter for its dry-run deploy plan')
|
|
14
|
+
.option('--plan-json', 'Print the validated deploy plan JSON without executing steps')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
const lane = typeof options.lane === 'string' ? options.lane : undefined;
|
|
17
|
+
if (!lane) {
|
|
18
|
+
console.error('Usage: wp deploy --lane <dev|preview_main|preview_pr_<n>|prd>');
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return await runDeployPlan({
|
|
23
|
+
cwd: process.cwd(),
|
|
24
|
+
lane,
|
|
25
|
+
dryRun: Boolean(options.dryRun),
|
|
26
|
+
planJson: Boolean(options.planJson),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=deploy.js.map
|
|
@@ -3,7 +3,24 @@ export interface GitignoreBlock {
|
|
|
3
3
|
id: string;
|
|
4
4
|
patterns: readonly string[];
|
|
5
5
|
}
|
|
6
|
+
export type GeneratedIndexCleanupResult = {
|
|
7
|
+
kind: 'skipped-dry-run';
|
|
8
|
+
pathspecs: readonly string[];
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'skipped-not-git';
|
|
11
|
+
pathspecs: readonly string[];
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'ok';
|
|
14
|
+
pathspecs: readonly string[];
|
|
15
|
+
removedPaths: readonly string[];
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'failed';
|
|
18
|
+
pathspecs: readonly string[];
|
|
19
|
+
exitCode: number | null;
|
|
20
|
+
stderr: string;
|
|
21
|
+
};
|
|
6
22
|
/** Canonical gitignore block for webpresso generated/transient paths. */
|
|
7
23
|
export declare const GENERATED_PATHS_BLOCK: GitignoreBlock;
|
|
24
|
+
export declare function untrackGeneratedGitignoredPaths(repoRoot: string, block?: GitignoreBlock, opts?: Pick<MergeOptions, 'dryRun'>): GeneratedIndexCleanupResult;
|
|
8
25
|
export declare function patchGitignore(targetPath: string, block: GitignoreBlock, opts?: MergeOptions): MergeResult;
|
|
9
26
|
//# sourceMappingURL=gitignore-patcher.d.ts.map
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* drift). Other content in `.gitignore` — including unrelated managed blocks
|
|
12
12
|
* from other scaffolders — is preserved verbatim.
|
|
13
13
|
*/
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
14
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
16
|
import { dirname } from 'node:path';
|
|
16
17
|
const BEGIN = (id) => `# >>> managed by webpresso (${id})`;
|
|
@@ -55,6 +56,7 @@ export const GENERATED_PATHS_BLOCK = {
|
|
|
55
56
|
'.opencode/',
|
|
56
57
|
'.codex/',
|
|
57
58
|
'.claude/settings.json',
|
|
59
|
+
'.claude/settings.local.json',
|
|
58
60
|
'.claude/agents/',
|
|
59
61
|
'.claude/hooks/',
|
|
60
62
|
'.claude/rules/',
|
|
@@ -77,6 +79,46 @@ export const GENERATED_PATHS_BLOCK = {
|
|
|
77
79
|
'.agent/.tail-hint-history.jsonl',
|
|
78
80
|
],
|
|
79
81
|
};
|
|
82
|
+
function generatedPathspecs(block) {
|
|
83
|
+
return block.patterns
|
|
84
|
+
.map((pattern) => pattern.trim())
|
|
85
|
+
.filter((pattern) => pattern.length > 0)
|
|
86
|
+
.filter((pattern) => !pattern.startsWith('#'))
|
|
87
|
+
.filter((pattern) => !pattern.startsWith('!'));
|
|
88
|
+
}
|
|
89
|
+
function parseGitRmStdout(stdout) {
|
|
90
|
+
return stdout
|
|
91
|
+
.split('\n')
|
|
92
|
+
.map((line) => line.trim())
|
|
93
|
+
.filter((line) => line.startsWith('rm '))
|
|
94
|
+
.map((line) => line.replace(/^rm ['"]?/, '').replace(/['"]?$/, ''));
|
|
95
|
+
}
|
|
96
|
+
export function untrackGeneratedGitignoredPaths(repoRoot, block = GENERATED_PATHS_BLOCK, opts = {}) {
|
|
97
|
+
const pathspecs = generatedPathspecs(block);
|
|
98
|
+
if (opts.dryRun)
|
|
99
|
+
return { kind: 'skipped-dry-run', pathspecs };
|
|
100
|
+
if (pathspecs.length === 0)
|
|
101
|
+
return { kind: 'ok', pathspecs, removedPaths: [] };
|
|
102
|
+
const gitProbe = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
103
|
+
cwd: repoRoot,
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
});
|
|
106
|
+
if (gitProbe.status !== 0)
|
|
107
|
+
return { kind: 'skipped-not-git', pathspecs };
|
|
108
|
+
const gitRm = spawnSync('git', ['rm', '--cached', '-r', '--ignore-unmatch', '--', ...pathspecs], {
|
|
109
|
+
cwd: repoRoot,
|
|
110
|
+
encoding: 'utf8',
|
|
111
|
+
});
|
|
112
|
+
if (gitRm.status !== 0) {
|
|
113
|
+
return {
|
|
114
|
+
kind: 'failed',
|
|
115
|
+
pathspecs,
|
|
116
|
+
exitCode: gitRm.status,
|
|
117
|
+
stderr: String(gitRm.stderr ?? '').trim(),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { kind: 'ok', pathspecs, removedPaths: parseGitRmStdout(String(gitRm.stdout ?? '')) };
|
|
121
|
+
}
|
|
80
122
|
export function patchGitignore(targetPath, block, opts = {}) {
|
|
81
123
|
const exists = existsSync(targetPath);
|
|
82
124
|
const original = exists ? readFileSync(targetPath, 'utf8') : '';
|
|
@@ -22,7 +22,7 @@ import { scaffoldAgent, RENDERED_SKILLS, TIER1_SKILLS, TIER2_SKILLS } from './sc
|
|
|
22
22
|
import { scaffoldAgentRules } from './scaffold-agent-rules.js';
|
|
23
23
|
import { scaffoldAgentSkills } from './scaffold-agent-skills.js';
|
|
24
24
|
import { scaffoldCatalogIgnore } from './scaffold-catalog-ignore.js';
|
|
25
|
-
import { GENERATED_PATHS_BLOCK, patchGitignore } from './gitignore-patcher.js';
|
|
25
|
+
import { GENERATED_PATHS_BLOCK, patchGitignore, untrackGeneratedGitignoredPaths, } from './gitignore-patcher.js';
|
|
26
26
|
import { scaffoldAgentsMd } from './scaffold-agents-md.js';
|
|
27
27
|
import { scaffoldBlueprints } from './scaffold-blueprints.js';
|
|
28
28
|
import { scaffoldDocs } from './scaffold-docs.js';
|
|
@@ -35,7 +35,7 @@ import { scaffoldAuditHooks } from './scaffolders/audit-hooks/index.js';
|
|
|
35
35
|
import { ensureClaudeCodeUserPlugin } from './scaffolders/claude-plugin/index.js';
|
|
36
36
|
import { scaffoldClaudeRules } from './scaffolders/claude-rules/index.js';
|
|
37
37
|
import { ensureCodexCli } from './scaffolders/codex-cli/index.js';
|
|
38
|
-
import { ensureCodexWebpressoMcp, ensureCodexPlaywrightMcp } from './scaffolders/codex-mcp/index.js';
|
|
38
|
+
import { ensureCodexWebpressoMcp, ensureCodexPlaywrightMcp, ensureClaudePlaywrightMcp, } from './scaffolders/codex-mcp/index.js';
|
|
39
39
|
import { scaffoldExampleSkill } from './scaffolders/example-skill/index.js';
|
|
40
40
|
import { ensureGstack } from './scaffolders/gstack/index.js';
|
|
41
41
|
import { scaffoldLoreCommits } from './scaffolders/lore-commits/index.js';
|
|
@@ -244,6 +244,7 @@ export async function runInit(flags) {
|
|
|
244
244
|
overwrite: options.overwrite,
|
|
245
245
|
});
|
|
246
246
|
const generatedSurfaceIgnoreResult = patchGitignore(join(consumer.repoRoot, '.gitignore'), GENERATED_PATHS_BLOCK, { dryRun: options.dryRun, overwrite: true });
|
|
247
|
+
const generatedIndexCleanupResult = untrackGeneratedGitignoredPaths(consumer.repoRoot, GENERATED_PATHS_BLOCK, { dryRun: options.dryRun });
|
|
247
248
|
const baseKitResults = tier3Selection.includes('base-kit')
|
|
248
249
|
? scaffoldBaseKit({
|
|
249
250
|
catalogDir,
|
|
@@ -436,6 +437,24 @@ export async function runInit(flags) {
|
|
|
436
437
|
console.log(' codex playwright mcp: skipped (--dry-run)');
|
|
437
438
|
break;
|
|
438
439
|
}
|
|
440
|
+
const claudePlaywrightMcpResult = ensureClaudePlaywrightMcp({
|
|
441
|
+
options,
|
|
442
|
+
repoRoot: consumer.repoRoot,
|
|
443
|
+
});
|
|
444
|
+
switch (claudePlaywrightMcpResult.kind) {
|
|
445
|
+
case 'claude-playwright-mcp-written':
|
|
446
|
+
console.log(` claude playwright mcp: ✓ ${claudePlaywrightMcpResult.path}`);
|
|
447
|
+
break;
|
|
448
|
+
case 'claude-playwright-mcp-unchanged':
|
|
449
|
+
console.log(` claude playwright mcp: already configured at ${claudePlaywrightMcpResult.path}`);
|
|
450
|
+
break;
|
|
451
|
+
case 'claude-playwright-mcp-skipped-dry-run':
|
|
452
|
+
console.log(' claude playwright mcp: skipped (--dry-run)');
|
|
453
|
+
break;
|
|
454
|
+
case 'claude-playwright-mcp-invalid-json':
|
|
455
|
+
console.warn(` claude playwright mcp: ⚠ ${claudePlaywrightMcpResult.path} is not valid JSON; left unchanged`);
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
439
458
|
}
|
|
440
459
|
if (presets.includes('omx')) {
|
|
441
460
|
agentHooksResult = await scaffoldAgentHooks({
|
|
@@ -669,6 +688,15 @@ export async function runInit(flags) {
|
|
|
669
688
|
console.log(` drifted: ${summary.drifted}`);
|
|
670
689
|
if (options.dryRun)
|
|
671
690
|
console.log(` would-change: ${summary['skipped-dry']}`);
|
|
691
|
+
if (generatedIndexCleanupResult.kind === 'ok') {
|
|
692
|
+
console.log(` git index cleanup: ${generatedIndexCleanupResult.removedPaths.length} untracked`);
|
|
693
|
+
}
|
|
694
|
+
else if (generatedIndexCleanupResult.kind === 'failed') {
|
|
695
|
+
console.warn(` git index cleanup: failed (git rm --cached exited ${generatedIndexCleanupResult.exitCode})`);
|
|
696
|
+
if (generatedIndexCleanupResult.stderr.length > 0) {
|
|
697
|
+
console.warn(` git index cleanup stderr: ${generatedIndexCleanupResult.stderr}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
672
700
|
if (tier3Selection.includes('base-kit')) {
|
|
673
701
|
const qualityTargets = new Set(BASE_KIT_QUALITY_TARGETS);
|
|
674
702
|
const qualityResults = baseKitResults.filter((result) => qualityTargets.has(relative(consumer.repoRoot, result.targetPath).replaceAll('\\', '/')));
|
|
@@ -138,7 +138,7 @@ function mergePackageJson(repoRoot, options, globalInstall = false) {
|
|
|
138
138
|
const devDeps = (pkg['devDependencies'] ?? {});
|
|
139
139
|
const hasAgentKitDevDep = typeof devDeps['@webpresso/agent-kit'] === 'string';
|
|
140
140
|
const hasLegacyAgentKitDevDep = typeof devDeps['webpresso'] === 'string';
|
|
141
|
-
const shouldSkipSelfInstall = packageName
|
|
141
|
+
const shouldSkipSelfInstall = isSelfPackageName(packageName);
|
|
142
142
|
const shouldManageAgentKitAsGlobal = globalInstall && !shouldSkipSelfInstall;
|
|
143
143
|
const requiredAuthoringDeps = {
|
|
144
144
|
'@playwright/test': 'latest',
|
|
@@ -236,10 +236,35 @@ function mergePackageJson(repoRoot, options, globalInstall = false) {
|
|
|
236
236
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
237
237
|
return { targetPath: pkgPath, action: 'overwritten' };
|
|
238
238
|
}
|
|
239
|
+
/** agent-kit's own package identities: scoped canonical + legacy unscoped. */
|
|
240
|
+
const SELF_PACKAGE_NAMES = ['@webpresso/agent-kit', 'webpresso'];
|
|
241
|
+
/** True when `name` is one of agent-kit's own package identities. */
|
|
242
|
+
function isSelfPackageName(name) {
|
|
243
|
+
return name !== undefined && SELF_PACKAGE_NAMES.includes(name);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* True when `repoRoot` is agent-kit's own source repo (by package.json name).
|
|
247
|
+
* agent-kit dogfoods base-kit's scripts and shared templates, but the QUALITY
|
|
248
|
+
* starter samples (quality-sample.ts, e2e/smoke, sample configs) are teaching
|
|
249
|
+
* artifacts for FRESH consumer repos — scaffolding them into agent-kit's own
|
|
250
|
+
* source tree is pollution. This flag skips ONLY those samples.
|
|
251
|
+
*/
|
|
252
|
+
function isAgentKitSelfRepo(repoRoot) {
|
|
253
|
+
try {
|
|
254
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'));
|
|
255
|
+
return isSelfPackageName(pkg.name);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
239
261
|
export function scaffoldBaseKit(input) {
|
|
240
262
|
const { catalogDir, repoRoot, options, globalInstall = false } = input;
|
|
241
263
|
const baseKitDir = join(catalogDir, 'base-kit');
|
|
242
264
|
const results = [];
|
|
265
|
+
// Dogfooding boundary: agent-kit gets base-kit's scripts/templates but not the
|
|
266
|
+
// starter quality samples scaffolded into its own source tree.
|
|
267
|
+
const skipStarterSamples = isAgentKitSelfRepo(repoRoot);
|
|
243
268
|
for (const [tmplRel, targetRel] of TEMPLATE_MAP) {
|
|
244
269
|
const tmplPath = join(baseKitDir, tmplRel);
|
|
245
270
|
if (!existsSync(tmplPath))
|
|
@@ -269,23 +294,25 @@ export function scaffoldBaseKit(input) {
|
|
|
269
294
|
writeFileSync(targetPath, content);
|
|
270
295
|
results.push({ targetPath, action: 'created' });
|
|
271
296
|
}
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
297
|
+
if (!skipStarterSamples) {
|
|
298
|
+
for (const [tmplRel, targetRel] of QUALITY_BOOTSTRAP_ONLY_MAP) {
|
|
299
|
+
const tmplPath = join(baseKitDir, tmplRel);
|
|
300
|
+
if (!existsSync(tmplPath))
|
|
301
|
+
continue;
|
|
302
|
+
const targetPath = join(repoRoot, targetRel);
|
|
303
|
+
if (existsSync(targetPath)) {
|
|
304
|
+
results.push({ targetPath, action: 'identical' });
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const content = readFileSync(tmplPath, 'utf8');
|
|
308
|
+
if (options.dryRun) {
|
|
309
|
+
results.push({ targetPath, action: 'skipped-dry' });
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
313
|
+
writeFileSync(targetPath, content);
|
|
314
|
+
results.push({ targetPath, action: 'created' });
|
|
285
315
|
}
|
|
286
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
287
|
-
writeFileSync(targetPath, content);
|
|
288
|
-
results.push({ targetPath, action: 'created' });
|
|
289
316
|
}
|
|
290
317
|
// Make husky hook files executable
|
|
291
318
|
if (!options.dryRun) {
|
|
@@ -20,7 +20,11 @@ export function ensureCodexCli(input) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
else if (!shouldSkipCodexRefresh()) {
|
|
23
|
-
|
|
23
|
+
// `--latest` ignores the recorded semver range so the global is pulled to
|
|
24
|
+
// the absolute newest published release, matching the force-to-latest
|
|
25
|
+
// guarantee `vp install -g <bare>` gives the agent-kit self-update. Plain
|
|
26
|
+
// `vp update -g` is range-bound and can strand the global on an old major.
|
|
27
|
+
spawn('vp', ['update', '-g', '--latest', '@openai/codex'], { stdio: 'inherit' });
|
|
24
28
|
}
|
|
25
29
|
return { kind: 'codex-cli-ok', installed };
|
|
26
30
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MergeOptions } from '#cli/commands/init/merge';
|
|
2
2
|
export declare const PLAYWRIGHT_MCP_SERVER_NAME = "playwright";
|
|
3
3
|
export declare const PLAYWRIGHT_MCP_HEADER = "[mcp_servers.playwright]";
|
|
4
|
-
export declare const PLAYWRIGHT_MCP_BLOCK
|
|
4
|
+
export declare const PLAYWRIGHT_MCP_BLOCK: string;
|
|
5
5
|
export declare const WEBPRESSO_MCP_SERVER_NAME = "webpresso";
|
|
6
6
|
export declare const WEBPRESSO_MCP_HEADER = "[mcp_servers.webpresso]";
|
|
7
7
|
export interface EnsureCodexPlaywrightMcpInput {
|
|
@@ -21,6 +21,34 @@ export type EnsureCodexPlaywrightMcpResult = {
|
|
|
21
21
|
};
|
|
22
22
|
export declare function upsertPlaywrightMcpServer(raw: string): string;
|
|
23
23
|
export declare function ensureCodexPlaywrightMcp(input: EnsureCodexPlaywrightMcpInput): EnsureCodexPlaywrightMcpResult;
|
|
24
|
+
/**
|
|
25
|
+
* Upsert the `playwright` server into a `.mcp.json` document, preserving every
|
|
26
|
+
* other server (e.g. `context7`, `exa`) and any non-server top-level keys.
|
|
27
|
+
* Output is normalized to 2-space JSON with a trailing newline so repeated runs
|
|
28
|
+
* converge — idempotent after the first write.
|
|
29
|
+
*/
|
|
30
|
+
export declare function upsertClaudePlaywrightMcpServer(raw: string): string;
|
|
31
|
+
export interface EnsureClaudePlaywrightMcpInput {
|
|
32
|
+
options: MergeOptions;
|
|
33
|
+
/** Project root whose `.mcp.json` is managed. */
|
|
34
|
+
repoRoot: string;
|
|
35
|
+
/** Test seam. Defaults to `<repoRoot>/.mcp.json`. */
|
|
36
|
+
configPath?: string;
|
|
37
|
+
}
|
|
38
|
+
export type EnsureClaudePlaywrightMcpResult = {
|
|
39
|
+
kind: 'claude-playwright-mcp-written';
|
|
40
|
+
path: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'claude-playwright-mcp-unchanged';
|
|
43
|
+
path: string;
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'claude-playwright-mcp-skipped-dry-run';
|
|
46
|
+
path: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'claude-playwright-mcp-invalid-json';
|
|
49
|
+
path: string;
|
|
50
|
+
};
|
|
51
|
+
export declare function ensureClaudePlaywrightMcp(input: EnsureClaudePlaywrightMcpInput): EnsureClaudePlaywrightMcpResult;
|
|
24
52
|
export interface WebpressoInstallProbe {
|
|
25
53
|
/** Test seam — override the candidate roots. Default: probe in fixed order. */
|
|
26
54
|
candidates?: readonly string[];
|
|
@@ -22,10 +22,27 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
22
22
|
import { homedir } from 'node:os';
|
|
23
23
|
import { dirname, join } from 'node:path';
|
|
24
24
|
export const PLAYWRIGHT_MCP_SERVER_NAME = 'playwright';
|
|
25
|
+
/**
|
|
26
|
+
* Single source of truth for how the Playwright MCP server is launched. Both
|
|
27
|
+
* the Codex TOML block and the Claude Code `.mcp.json` block render from these.
|
|
28
|
+
* The portable `vp dlx` facade fetches the npm-published server on demand, so
|
|
29
|
+
* there is no machine-specific bin path to rot — the failure mode a hand-
|
|
30
|
+
* authored `~/.bun/bin/playwright-mcp` entry hits the moment that global bin
|
|
31
|
+
* disappears (ENOENT on spawn).
|
|
32
|
+
*/
|
|
33
|
+
const PLAYWRIGHT_MCP_COMMAND = 'vp';
|
|
34
|
+
const PLAYWRIGHT_MCP_ARGS = [
|
|
35
|
+
'dlx',
|
|
36
|
+
'@playwright/mcp@latest',
|
|
37
|
+
'--caps=testing,storage,network,devtools',
|
|
38
|
+
];
|
|
39
|
+
function tomlStringArray(values) {
|
|
40
|
+
return `[${values.map((value) => `"${value}"`).join(', ')}]`;
|
|
41
|
+
}
|
|
25
42
|
export const PLAYWRIGHT_MCP_HEADER = `[mcp_servers.${PLAYWRIGHT_MCP_SERVER_NAME}]`;
|
|
26
43
|
export const PLAYWRIGHT_MCP_BLOCK = `${PLAYWRIGHT_MCP_HEADER}
|
|
27
|
-
command = "
|
|
28
|
-
args =
|
|
44
|
+
command = "${PLAYWRIGHT_MCP_COMMAND}"
|
|
45
|
+
args = ${tomlStringArray(PLAYWRIGHT_MCP_ARGS)}
|
|
29
46
|
enabled = true
|
|
30
47
|
startup_timeout_sec = 30
|
|
31
48
|
`;
|
|
@@ -70,6 +87,59 @@ export function ensureCodexPlaywrightMcp(input) {
|
|
|
70
87
|
writeFileSync(configPath, next, 'utf8');
|
|
71
88
|
return { kind: 'codex-playwright-mcp-written', path: configPath };
|
|
72
89
|
}
|
|
90
|
+
function claudePlaywrightServer() {
|
|
91
|
+
return { command: PLAYWRIGHT_MCP_COMMAND, args: [...PLAYWRIGHT_MCP_ARGS] };
|
|
92
|
+
}
|
|
93
|
+
function isJsonRecord(value) {
|
|
94
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
95
|
+
}
|
|
96
|
+
function parseJson(raw) {
|
|
97
|
+
try {
|
|
98
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return { ok: false };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Upsert the `playwright` server into a `.mcp.json` document, preserving every
|
|
106
|
+
* other server (e.g. `context7`, `exa`) and any non-server top-level keys.
|
|
107
|
+
* Output is normalized to 2-space JSON with a trailing newline so repeated runs
|
|
108
|
+
* converge — idempotent after the first write.
|
|
109
|
+
*/
|
|
110
|
+
export function upsertClaudePlaywrightMcpServer(raw) {
|
|
111
|
+
const parsed = raw.trim().length > 0 ? parseJson(raw) : { ok: true, value: {} };
|
|
112
|
+
if (!parsed.ok) {
|
|
113
|
+
throw new Error('cannot upsert playwright into .mcp.json: existing file is not valid JSON');
|
|
114
|
+
}
|
|
115
|
+
const root = isJsonRecord(parsed.value) ? parsed.value : {};
|
|
116
|
+
const servers = isJsonRecord(root.mcpServers) ? root.mcpServers : {};
|
|
117
|
+
const next = {
|
|
118
|
+
...root,
|
|
119
|
+
mcpServers: {
|
|
120
|
+
...servers,
|
|
121
|
+
[PLAYWRIGHT_MCP_SERVER_NAME]: claudePlaywrightServer(),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
125
|
+
}
|
|
126
|
+
export function ensureClaudePlaywrightMcp(input) {
|
|
127
|
+
const configPath = input.configPath ?? join(input.repoRoot, '.mcp.json');
|
|
128
|
+
if (input.options.dryRun) {
|
|
129
|
+
return { kind: 'claude-playwright-mcp-skipped-dry-run', path: configPath };
|
|
130
|
+
}
|
|
131
|
+
const existing = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
|
|
132
|
+
if (existing.trim().length > 0 && !parseJson(existing).ok) {
|
|
133
|
+
return { kind: 'claude-playwright-mcp-invalid-json', path: configPath };
|
|
134
|
+
}
|
|
135
|
+
const next = upsertClaudePlaywrightMcpServer(existing);
|
|
136
|
+
if (next === existing) {
|
|
137
|
+
return { kind: 'claude-playwright-mcp-unchanged', path: configPath };
|
|
138
|
+
}
|
|
139
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
140
|
+
writeFileSync(configPath, next, 'utf8');
|
|
141
|
+
return { kind: 'claude-playwright-mcp-written', path: configPath };
|
|
142
|
+
}
|
|
73
143
|
// ────────────────────────────────────────────────────────────────────────────
|
|
74
144
|
// Agent-kit MCP server registration
|
|
75
145
|
// ────────────────────────────────────────────────────────────────────────────
|