@wrongstack/tools 0.293.0 → 0.295.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/dist/_env.d.ts +1 -1
- package/dist/_env.d.ts.map +1 -1
- package/dist/_fetch-guard.d.ts.map +1 -1
- package/dist/_spawn-stream.d.ts +1 -1
- package/dist/_spawn-stream.d.ts.map +1 -1
- package/dist/_util.d.ts +1 -1
- package/dist/_util.d.ts.map +1 -1
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +81 -16
- package/dist/audit.js.map +2 -2
- package/dist/bash.d.ts +1 -1
- package/dist/bash.d.ts.map +1 -1
- package/dist/bash.js +66 -52
- package/dist/bash.js.map +3 -3
- package/dist/batch-tool-use.d.ts +1 -1
- package/dist/batch-tool-use.d.ts.map +1 -1
- package/dist/batch-tool-use.js +1 -1
- package/dist/batch-tool-use.js.map +1 -1
- package/dist/browser/index.js +10 -1
- package/dist/browser/index.js.map +2 -2
- package/dist/browser/manager.d.ts +2 -0
- package/dist/browser/manager.d.ts.map +1 -1
- package/dist/browser/tools.d.ts +1 -1
- package/dist/browser/tools.d.ts.map +1 -1
- package/dist/builtin.d.ts +1 -1
- package/dist/builtin.d.ts.map +1 -1
- package/dist/builtin.js +1394 -553
- package/dist/builtin.js.map +4 -4
- package/dist/codebase-index/background-indexer.d.ts +2 -2
- package/dist/codebase-index/background-indexer.d.ts.map +1 -1
- package/dist/codebase-index/bm25.d.ts +3 -1
- package/dist/codebase-index/bm25.d.ts.map +1 -1
- package/dist/codebase-index/codebase-index-tool.d.ts +1 -1
- package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
- package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts +1 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
- package/dist/codebase-index/generic-parser.d.ts +34 -0
- package/dist/codebase-index/generic-parser.d.ts.map +1 -0
- package/dist/codebase-index/go-parser.d.ts +1 -1
- package/dist/codebase-index/go-parser.d.ts.map +1 -1
- package/dist/codebase-index/index.d.ts +4 -2
- package/dist/codebase-index/index.d.ts.map +1 -1
- package/dist/codebase-index/index.js +1036 -341
- package/dist/codebase-index/index.js.map +4 -4
- package/dist/codebase-index/indexer.d.ts +6 -1
- package/dist/codebase-index/indexer.d.ts.map +1 -1
- package/dist/codebase-index/json-parser.d.ts +1 -1
- package/dist/codebase-index/languages.d.ts +25 -0
- package/dist/codebase-index/languages.d.ts.map +1 -0
- package/dist/codebase-index/py-parser.d.ts +9 -1
- package/dist/codebase-index/py-parser.d.ts.map +1 -1
- package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
- package/dist/codebase-index/rs-parser.d.ts +1 -1
- package/dist/codebase-index/rs-parser.d.ts.map +1 -1
- package/dist/codebase-index/schema.d.ts +9 -2
- package/dist/codebase-index/schema.d.ts.map +1 -1
- package/dist/codebase-index/spawn-gate.d.ts +15 -0
- package/dist/codebase-index/spawn-gate.d.ts.map +1 -0
- package/dist/codebase-index/ts-parser.d.ts +2 -2
- package/dist/codebase-index/ts-parser.d.ts.map +1 -1
- package/dist/codebase-index/worker.js +961 -317
- package/dist/codebase-index/worker.js.map +4 -4
- package/dist/codebase-index/writer.d.ts +83 -4
- package/dist/codebase-index/writer.d.ts.map +1 -1
- package/dist/codebase-index/yaml-parser.d.ts +1 -1
- package/dist/design.d.ts +5 -2
- package/dist/design.d.ts.map +1 -1
- package/dist/design.js +36 -3
- package/dist/design.js.map +2 -2
- package/dist/diff.d.ts +1 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +2 -2
- package/dist/diff.js.map +1 -1
- package/dist/document.d.ts +1 -1
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +1 -1
- package/dist/document.js.map +1 -1
- package/dist/e2e.d.ts +1 -1
- package/dist/e2e.d.ts.map +1 -1
- package/dist/e2e.js +1 -1
- package/dist/e2e.js.map +1 -1
- package/dist/edit.d.ts +1 -1
- package/dist/edit.d.ts.map +1 -1
- package/dist/edit.js +3 -3
- package/dist/edit.js.map +2 -2
- package/dist/exec.d.ts +1 -1
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +60 -21
- package/dist/exec.js.map +3 -3
- package/dist/fetch.d.ts +1 -1
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +4 -5
- package/dist/fetch.js.map +2 -2
- package/dist/format.d.ts +1 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +81 -16
- package/dist/format.js.map +2 -2
- package/dist/git.d.ts +1 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +3 -2
- package/dist/git.js.map +2 -2
- package/dist/glob.d.ts +1 -1
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +2 -2
- package/dist/glob.js.map +1 -1
- package/dist/grep.d.ts +1 -1
- package/dist/grep.d.ts.map +1 -1
- package/dist/grep.js +4 -3
- package/dist/grep.js.map +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1657 -705
- package/dist/index.js.map +4 -4
- package/dist/install.d.ts +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +85 -17
- package/dist/install.js.map +2 -2
- package/dist/json.d.ts +1 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +2 -2
- package/dist/json.js.map +2 -2
- package/dist/kanban.d.ts +4 -3
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +94 -27
- package/dist/kanban.js.map +2 -2
- package/dist/languages/execute-tool.d.ts +1 -1
- package/dist/languages/execute-tool.d.ts.map +1 -1
- package/dist/languages/execute.d.ts +1 -1
- package/dist/languages/execute.d.ts.map +1 -1
- package/dist/languages/index.d.ts +1 -1
- package/dist/languages/index.d.ts.map +1 -1
- package/dist/languages/index.js +99 -17
- package/dist/languages/index.js.map +2 -2
- package/dist/languages/package-tool.d.ts +3 -4
- package/dist/languages/package-tool.d.ts.map +1 -1
- package/dist/languages/plan.d.ts.map +1 -1
- package/dist/languages/profiles/additional.d.ts.map +1 -1
- package/dist/languages/profiles/primary.d.ts.map +1 -1
- package/dist/languages/tool.d.ts +1 -1
- package/dist/languages/tool.d.ts.map +1 -1
- package/dist/lint.d.ts +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +81 -16
- package/dist/lint.js.map +2 -2
- package/dist/logs.d.ts +1 -1
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +2 -2
- package/dist/logs.js.map +1 -1
- package/dist/memory.d.ts +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +1 -1
- package/dist/memory.js.map +1 -1
- package/dist/mode.d.ts +1 -1
- package/dist/mode.d.ts.map +1 -1
- package/dist/mode.js.map +1 -1
- package/dist/outdated.d.ts +1 -1
- package/dist/outdated.d.ts.map +1 -1
- package/dist/outdated.js +101 -19
- package/dist/outdated.js.map +2 -2
- package/dist/pack.d.ts +1 -1
- package/dist/pack.js +1394 -553
- package/dist/pack.js.map +4 -4
- package/dist/patch.d.ts +1 -1
- package/dist/patch.d.ts.map +1 -1
- package/dist/patch.js +2 -2
- package/dist/patch.js.map +1 -1
- package/dist/plan.d.ts +1 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +7 -7
- package/dist/plan.js.map +2 -2
- package/dist/process-guardian.d.ts +7 -0
- package/dist/process-guardian.d.ts.map +1 -1
- package/dist/process-registry-persistent.d.ts +5 -0
- package/dist/process-registry-persistent.d.ts.map +1 -1
- package/dist/process-registry.d.ts +9 -1
- package/dist/process-registry.d.ts.map +1 -1
- package/dist/process-registry.js +18 -3
- package/dist/process-registry.js.map +2 -2
- package/dist/ps-slash.js +54 -15
- package/dist/ps-slash.js.map +2 -2
- package/dist/read.d.ts +1 -1
- package/dist/read.d.ts.map +1 -1
- package/dist/read.js +3 -2
- package/dist/read.js.map +2 -2
- package/dist/replace.d.ts +1 -1
- package/dist/replace.d.ts.map +1 -1
- package/dist/replace.js +4 -4
- package/dist/replace.js.map +2 -2
- package/dist/scaffold.d.ts +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +2 -2
- package/dist/scaffold.js.map +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +4 -4
- package/dist/search.js.map +2 -2
- package/dist/session-kanban.d.ts +4 -1
- package/dist/session-kanban.d.ts.map +1 -1
- package/dist/session-kanban.js +23 -15
- package/dist/session-kanban.js.map +2 -2
- package/dist/set-working-dir.d.ts +1 -1
- package/dist/set-working-dir.d.ts.map +1 -1
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +2 -5
- package/dist/skill.js.map +2 -2
- package/dist/spawn-background.d.ts.map +1 -1
- package/dist/task.d.ts +3 -2
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +6 -8
- package/dist/task.js.map +2 -2
- package/dist/test.d.ts +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +81 -16
- package/dist/test.js.map +2 -2
- package/dist/todo.d.ts +2 -1
- package/dist/todo.d.ts.map +1 -1
- package/dist/todo.js +4 -4
- package/dist/todo.js.map +2 -2
- package/dist/tool-help.d.ts +1 -1
- package/dist/tool-help.d.ts.map +1 -1
- package/dist/tool-help.js.map +1 -1
- package/dist/tool-search.d.ts +1 -1
- package/dist/tool-search.d.ts.map +1 -1
- package/dist/tool-search.js.map +1 -1
- package/dist/tool-tier.d.ts +16 -0
- package/dist/tool-tier.d.ts.map +1 -0
- package/dist/tool-tier.js +22233 -0
- package/dist/tool-tier.js.map +7 -0
- package/dist/tool-use.d.ts +1 -1
- package/dist/tool-use.d.ts.map +1 -1
- package/dist/tool-use.js.map +1 -1
- package/dist/tree.d.ts +1 -1
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +2 -2
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.d.ts +1 -1
- package/dist/typecheck.d.ts.map +1 -1
- package/dist/typecheck.js +81 -16
- package/dist/typecheck.js.map +2 -2
- package/dist/write.d.ts +1 -1
- package/dist/write.d.ts.map +1 -1
- package/dist/write.js +3 -2
- package/dist/write.js.map +2 -2
- package/package.json +7 -3
package/dist/document.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/document.ts", "../src/_util.ts"],
|
|
4
|
-
"sourcesContent": ["import * as fs from 'node:fs/promises';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\n\ninterface DocumentInput {\n target: 'file' | 'function' | 'class' | 'type' | 'all';\n path?: string | undefined;\n files?: string | string[] | undefined;\n style?: 'jsdoc' | 'tsdoc' | 'block' | undefined;\n overwrite?: boolean | undefined;\n cwd?: string | undefined;\n}\n\ninterface DocumentedItem {\n path: string;\n name: string;\n signature: string;\n docstring: string;\n status: 'documented' | 'skipped' | 'error';\n error?: string | undefined;\n}\n\ninterface DocumentOutput {\n files_processed: number;\n items_documented: number;\n results: DocumentedItem[];\n style: string;\n}\n\nexport const documentTool: Tool<DocumentInput, DocumentOutput> = {\n name: 'document',\n category: 'Project',\n description:\n 'DEPRECATED \u2014 use the `auto_doc` tool with `dryRun: true` instead. ' +\n 'This tool is a read-only preview stub that returns `skipped` candidates without generating real docstrings.',\n usageHint:\n 'Deprecated: prefer `auto_doc` with `dryRun: true` for previewing, or `auto_doc` without dryRun for writing. ' +\n 'This tool only lists undocumented symbols with placeholder comments \u2014 it does not generate real JSDoc/TSDoc.',\n permission: 'auto',\n mutating: false,\n timeoutMs: 30_000,\n capabilities: ['fs.read'],\n icon: 'document',\n inputSchema: {\n type: 'object',\n properties: {\n target: {\n type: 'string',\n enum: ['file', 'function', 'class', 'type', 'all'],\n description: 'What to document',\n },\n path: {\n type: 'string',\n description: 'Specific file path to document',\n },\n files: {\n type: 'string',\n description: 'File(s) to process: single path, comma-separated list, or glob',\n },\n style: {\n type: 'string',\n enum: ['jsdoc', 'tsdoc', 'block'],\n description: 'Documentation style (default: jsdoc)',\n },\n\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n },\n },\n async execute(input, ctx) {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const style = input.style ?? 'jsdoc';\n const results: DocumentedItem[] = [];\n let filesProcessed = 0;\n let itemsDocumented = 0;\n\n const fileList = input.files\n ? await resolveFiles(Array.isArray(input.files) ? input.files.join(',') : input.files, cwd)\n : input.path\n ? [safeResolve(input.path, ctx)]\n : [];\n\n for (const absPath of fileList) {\n try {\n const content = await fs.readFile(absPath, 'utf8');\n filesProcessed++;\n const processed = processFile(\n content,\n absPath,\n style,\n input.overwrite ?? false,\n input.target ?? 'all',\n );\n results.push(...processed);\n itemsDocumented += processed.filter((r) => r.status === 'documented').length;\n } catch (e) {\n results.push({\n path: absPath,\n name: absPath.split('/').pop() ?? absPath,\n signature: '',\n docstring: '',\n status: 'error',\n error: e instanceof Error ? e.message : String(e),\n });\n }\n }\n\n return {\n files_processed: filesProcessed,\n items_documented: itemsDocumented,\n results,\n style,\n };\n },\n};\n\nasync function resolveFiles(filesInput: string, cwd: string): Promise<string[]> {\n const files = Array.isArray(filesInput) ? filesInput : filesInput.split(',');\n const resolved: string[] = [];\n\n for (const f of files) {\n const absPath = f.trim().startsWith('/') ? f.trim() : `${cwd}/${f.trim()}`;\n try {\n const stat = await fs.stat(absPath);\n if (stat.isFile()) resolved.push(absPath);\n } catch {\n // skip\n }\n }\n\n return resolved;\n}\n\nfunction processFile(\n content: string,\n absPath: string,\n _style: string,\n _overwrite: boolean,\n target: string,\n): DocumentedItem[] {\n const results: DocumentedItem[] = [];\n // These must be global \u2014 `String.prototype.matchAll` throws on a non-global\n // RegExp, which previously made processFile throw and report every file as an\n // error instead of returning documentation candidates.\n const functionRegex = /(?:async\\s+)?function\\s+(\\w+)\\s*\\(([^)]*)\\)/g;\n const arrowRegex = /(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:async\\s+)?\\(([^)]*)\\)\\s*=>/g;\n const classRegex = /class\\s+(\\w+)/g;\n const typeRegex = /(?:type|interface)\\s+(\\w+)\\s*[=<]/g;\n\n const allMatches: { name: string; sig: string; type: string; line: number }[] = [];\n\n if (target === 'all' || target === 'function') {\n for (const m of content.matchAll(functionRegex)) {\n /* v8 ignore next -- capture group (\\w+) is mandatory, so m[1] is always defined; defensive. */\n if (!m[1]) continue;\n allMatches.push({\n name: m[1],\n sig: m[2] ?? '',\n type: 'function',\n line: content.slice(0, m.index).split('\\n').length,\n });\n }\n for (const m of content.matchAll(arrowRegex)) {\n /* v8 ignore next -- capture group (\\w+) is mandatory, so m[1] is always defined; defensive. */\n if (!m[1]) continue;\n allMatches.push({\n name: m[1],\n sig: m[2] ?? '',\n type: 'arrow',\n line: content.slice(0, m.index).split('\\n').length,\n });\n }\n }\n\n if (target === 'all' || target === 'class') {\n for (const m of content.matchAll(classRegex)) {\n /* v8 ignore next -- capture group (\\w+) is mandatory, so m[1] is always defined; defensive. */\n if (!m[1]) continue;\n allMatches.push({\n name: m[1],\n sig: '',\n type: 'class',\n line: content.slice(0, m.index).split('\\n').length,\n });\n }\n }\n\n if (target === 'all' || target === 'type') {\n for (const m of content.matchAll(typeRegex)) {\n /* v8 ignore next -- capture group (\\w+) is mandatory, so m[1] is always defined; defensive. */\n if (!m[1]) continue;\n allMatches.push({\n name: m[1],\n sig: m[0] ?? '',\n type: 'type',\n line: content.slice(0, m.index).split('\\n').length,\n });\n }\n }\n\n for (const m of allMatches) {\n results.push({\n path: absPath,\n name: m.name,\n signature: m.sig,\n docstring: `/** ${m.name} - documented at line ${m.line} */`,\n status: 'skipped',\n });\n }\n\n return results;\n}\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
|
|
4
|
+
"sourcesContent": ["import * as fs from 'node:fs/promises';\nimport type { Tool } from '@wrongstack/core/types';\nimport { safeResolve } from './_util.js';\n\ninterface DocumentInput {\n target: 'file' | 'function' | 'class' | 'type' | 'all';\n path?: string | undefined;\n files?: string | string[] | undefined;\n style?: 'jsdoc' | 'tsdoc' | 'block' | undefined;\n overwrite?: boolean | undefined;\n cwd?: string | undefined;\n}\n\ninterface DocumentedItem {\n path: string;\n name: string;\n signature: string;\n docstring: string;\n status: 'documented' | 'skipped' | 'error';\n error?: string | undefined;\n}\n\ninterface DocumentOutput {\n files_processed: number;\n items_documented: number;\n results: DocumentedItem[];\n style: string;\n}\n\nexport const documentTool: Tool<DocumentInput, DocumentOutput> = {\n name: 'document',\n category: 'Project',\n description:\n 'DEPRECATED \u2014 use the `auto_doc` tool with `dryRun: true` instead. ' +\n 'This tool is a read-only preview stub that returns `skipped` candidates without generating real docstrings.',\n usageHint:\n 'Deprecated: prefer `auto_doc` with `dryRun: true` for previewing, or `auto_doc` without dryRun for writing. ' +\n 'This tool only lists undocumented symbols with placeholder comments \u2014 it does not generate real JSDoc/TSDoc.',\n permission: 'auto',\n mutating: false,\n timeoutMs: 30_000,\n capabilities: ['fs.read'],\n icon: 'document',\n inputSchema: {\n type: 'object',\n properties: {\n target: {\n type: 'string',\n enum: ['file', 'function', 'class', 'type', 'all'],\n description: 'What to document',\n },\n path: {\n type: 'string',\n description: 'Specific file path to document',\n },\n files: {\n type: 'string',\n description: 'File(s) to process: single path, comma-separated list, or glob',\n },\n style: {\n type: 'string',\n enum: ['jsdoc', 'tsdoc', 'block'],\n description: 'Documentation style (default: jsdoc)',\n },\n\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n },\n },\n async execute(input, ctx) {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const style = input.style ?? 'jsdoc';\n const results: DocumentedItem[] = [];\n let filesProcessed = 0;\n let itemsDocumented = 0;\n\n const fileList = input.files\n ? await resolveFiles(Array.isArray(input.files) ? input.files.join(',') : input.files, cwd)\n : input.path\n ? [safeResolve(input.path, ctx)]\n : [];\n\n for (const absPath of fileList) {\n try {\n const content = await fs.readFile(absPath, 'utf8');\n filesProcessed++;\n const processed = processFile(\n content,\n absPath,\n style,\n input.overwrite ?? false,\n input.target ?? 'all',\n );\n results.push(...processed);\n itemsDocumented += processed.filter((r) => r.status === 'documented').length;\n } catch (e) {\n results.push({\n path: absPath,\n name: absPath.split('/').pop() ?? absPath,\n signature: '',\n docstring: '',\n status: 'error',\n error: e instanceof Error ? e.message : String(e),\n });\n }\n }\n\n return {\n files_processed: filesProcessed,\n items_documented: itemsDocumented,\n results,\n style,\n };\n },\n};\n\nasync function resolveFiles(filesInput: string, cwd: string): Promise<string[]> {\n const files = Array.isArray(filesInput) ? filesInput : filesInput.split(',');\n const resolved: string[] = [];\n\n for (const f of files) {\n const absPath = f.trim().startsWith('/') ? f.trim() : `${cwd}/${f.trim()}`;\n try {\n const stat = await fs.stat(absPath);\n if (stat.isFile()) resolved.push(absPath);\n } catch {\n // skip\n }\n }\n\n return resolved;\n}\n\nfunction processFile(\n content: string,\n absPath: string,\n _style: string,\n _overwrite: boolean,\n target: string,\n): DocumentedItem[] {\n const results: DocumentedItem[] = [];\n // These must be global \u2014 `String.prototype.matchAll` throws on a non-global\n // RegExp, which previously made processFile throw and report every file as an\n // error instead of returning documentation candidates.\n const functionRegex = /(?:async\\s+)?function\\s+(\\w+)\\s*\\(([^)]*)\\)/g;\n const arrowRegex = /(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:async\\s+)?\\(([^)]*)\\)\\s*=>/g;\n const classRegex = /class\\s+(\\w+)/g;\n const typeRegex = /(?:type|interface)\\s+(\\w+)\\s*[=<]/g;\n\n const allMatches: { name: string; sig: string; type: string; line: number }[] = [];\n\n if (target === 'all' || target === 'function') {\n for (const m of content.matchAll(functionRegex)) {\n /* v8 ignore next -- capture group (\\w+) is mandatory, so m[1] is always defined; defensive. */\n if (!m[1]) continue;\n allMatches.push({\n name: m[1],\n sig: m[2] ?? '',\n type: 'function',\n line: content.slice(0, m.index).split('\\n').length,\n });\n }\n for (const m of content.matchAll(arrowRegex)) {\n /* v8 ignore next -- capture group (\\w+) is mandatory, so m[1] is always defined; defensive. */\n if (!m[1]) continue;\n allMatches.push({\n name: m[1],\n sig: m[2] ?? '',\n type: 'arrow',\n line: content.slice(0, m.index).split('\\n').length,\n });\n }\n }\n\n if (target === 'all' || target === 'class') {\n for (const m of content.matchAll(classRegex)) {\n /* v8 ignore next -- capture group (\\w+) is mandatory, so m[1] is always defined; defensive. */\n if (!m[1]) continue;\n allMatches.push({\n name: m[1],\n sig: '',\n type: 'class',\n line: content.slice(0, m.index).split('\\n').length,\n });\n }\n }\n\n if (target === 'all' || target === 'type') {\n for (const m of content.matchAll(typeRegex)) {\n /* v8 ignore next -- capture group (\\w+) is mandatory, so m[1] is always defined; defensive. */\n if (!m[1]) continue;\n allMatches.push({\n name: m[1],\n sig: m[0] ?? '',\n type: 'type',\n line: content.slice(0, m.index).split('\\n').length,\n });\n }\n }\n\n for (const m of allMatches) {\n results.push({\n path: absPath,\n name: m.name,\n signature: m.sig,\n docstring: `/** ${m.name} - documented at line ${m.line} */`,\n status: 'skipped',\n });\n }\n\n return results;\n}\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core/utils';\nimport type { Context } from '@wrongstack/core/agent';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["stat"]
|
|
7
7
|
}
|
package/dist/e2e.d.ts
CHANGED
package/dist/e2e.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"e2e.d.ts","sourceRoot":"","sources":["../src/e2e.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"e2e.d.ts","sourceRoot":"","sources":["../src/e2e.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGnD,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;AACpD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;AAEhE,UAAU,YAAY;IACpB,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,SAAS,CAAC,EAAE,YAAY,GAAG,KAAK,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,eAAe,CAAC;IACxB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,gBAAgB,GAAG,mBAAmB,CAAC;IAC/C,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,YAAY,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,EAAE,iBAAiB,CAAC;IAClC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,EAAE,aAAa,EAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA0XD,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IACP,SAAS,CAAC,EAAE,YAAY,GAAG,KAAK,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,MAAM,EAAE,WAAW,CAAC;CACrB,GACA,OAAO,CAAC,aAAa,CAAC,CAuHxB;AAED,eAAO,MAAM,WAAW,EAAE,IAAI,CAAC,YAAY,EAAE,aAAa,CA4CzD,CAAC"}
|
package/dist/e2e.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as path2 from "node:path";
|
|
|
5
5
|
// src/_util.ts
|
|
6
6
|
import * as fsp from "node:fs/promises";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
-
import * as Core from "@wrongstack/core";
|
|
8
|
+
import * as Core from "@wrongstack/core/utils";
|
|
9
9
|
function resolvePath(input, ctx) {
|
|
10
10
|
return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
11
11
|
}
|
package/dist/e2e.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/e2e.ts", "../src/_util.ts"],
|
|
4
|
-
"sourcesContent": ["import { open, readdir } from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolveReal } from './_util.js';\n\nexport type E2EFramework = 'playwright' | 'cypress';\nexport type E2EPackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';\n\ninterface E2EPlanInput {\n cwd?: string | undefined;\n framework?: E2EFramework | 'all' | undefined;\n maxDepth?: number | undefined;\n includeSpecs?: boolean | undefined;\n}\n\nexport interface E2EServerHint {\n kind: 'managed' | 'attach';\n command?: string | undefined;\n url?: string | undefined;\n port?: number | undefined;\n source: 'static-config';\n incomplete: boolean;\n}\n\nexport interface E2EExecutionPlan {\n command: string;\n args: string[];\n cwd: string;\n source: 'package-script' | 'framework-default';\n script?: string | undefined;\n}\n\nexport interface E2EProjectPlan {\n framework: E2EFramework;\n root: string;\n configPath?: string | undefined;\n packagePath?: string | undefined;\n packageManager: E2EPackageManager;\n evidence: string[];\n scripts: string[];\n serverHints: E2EServerHint[];\n specPattern: string;\n specCount: number;\n specSamples: string[];\n specsTruncated: boolean;\n execution: E2EExecutionPlan;\n warnings: string[];\n}\n\nexport interface E2EPlanOutput {\n root: string;\n projects: E2EProjectPlan[];\n scannedDirectories: number;\n scanTruncated: boolean;\n warnings: string[];\n}\n\ninterface ScanResult {\n configs: Array<{ framework: E2EFramework; absolutePath: string }>;\n packageFiles: string[];\n scannedDirectories: number;\n truncated: boolean;\n}\n\ninterface PackageInfo {\n path: string;\n scripts: Record<string, string>;\n dependencies: Set<string>;\n packageManager?: E2EPackageManager | undefined;\n}\n\nasync function readBoundedText(filePath: string, maxBytes: number): Promise<string | undefined> {\n let handle;\n try {\n handle = await open(filePath, 'r');\n const info = await handle.stat();\n if (!info.isFile() || info.size > maxBytes) return undefined;\n const buffer = Buffer.alloc(info.size);\n const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);\n return buffer.subarray(0, bytesRead).toString('utf8');\n } catch {\n return undefined;\n } finally {\n await handle?.close().catch(() => undefined);\n }\n}\n\nconst CONFIG_NAMES = new Map<string, E2EFramework>([\n ['playwright.config.ts', 'playwright'],\n ['playwright.config.js', 'playwright'],\n ['playwright.config.mts', 'playwright'],\n ['playwright.config.mjs', 'playwright'],\n ['playwright.config.cts', 'playwright'],\n ['playwright.config.cjs', 'playwright'],\n ['cypress.config.ts', 'cypress'],\n ['cypress.config.js', 'cypress'],\n ['cypress.config.mts', 'cypress'],\n ['cypress.config.mjs', 'cypress'],\n ['cypress.config.cts', 'cypress'],\n ['cypress.config.cjs', 'cypress'],\n ['cypress.json', 'cypress'],\n]);\n\nconst SKIP_DIRECTORIES = new Set([\n '.git',\n '.hg',\n '.svn',\n '.wrongstack',\n 'node_modules',\n 'dist',\n 'build',\n 'coverage',\n 'coverage-html',\n 'playwright-report',\n 'test-results',\n '.next',\n]);\n\nconst MAX_SCAN_DIRECTORIES = 4_000;\nconst MAX_PACKAGE_BYTES = 512 * 1024;\nconst MAX_CONFIG_BYTES = 512 * 1024;\nconst MAX_SPEC_SAMPLES = 20;\n\nfunction relativePath(root: string, target: string): string {\n const value = path.relative(root, target) || '.';\n return value.split(path.sep).join('/');\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction staticString(source: string, property: string): string | undefined {\n const key = escapeRegExp(property);\n const direct = source.match(\n new RegExp(`(?:['\"]?${key}['\"]?)\\\\s*:\\\\s*(['\"\\\\x60])([^'\"\\\\x60\\\\r\\\\n]*)\\\\1`),\n );\n if (direct?.[2] && !direct[2].includes('${')) return direct[2];\n\n const sameLineFallback = source.match(\n new RegExp(`(?:['\"]?${key}['\"]?)\\\\s*:[^\\\\r\\\\n]{0,300}?(['\"\\\\x60])([^'\"\\\\x60\\\\r\\\\n]*)\\\\1`),\n );\n return sameLineFallback?.[2] && !sameLineFallback[2].includes('${')\n ? sameLineFallback[2]\n : undefined;\n}\n\nfunction staticNumber(source: string, property: string): number | undefined {\n const key = escapeRegExp(property);\n const match = source.match(new RegExp(`(?:['\"]?${key}['\"]?)\\\\s*:\\\\s*(\\\\d{2,5})\\\\b`));\n return match?.[1] ? Number.parseInt(match[1], 10) : undefined;\n}\n\nfunction serverHints(framework: E2EFramework, source: string): E2EServerHint[] {\n if (framework === 'playwright' && /\\bwebServer\\s*:/.test(source)) {\n const command = staticString(source, 'command');\n const url = staticString(source, 'url');\n const port = staticNumber(source, 'port');\n return [\n {\n kind: 'managed',\n ...(command ? { command } : {}),\n ...(url ? { url } : {}),\n ...(port ? { port } : {}),\n source: 'static-config',\n incomplete: !command || (!url && !port),\n },\n ];\n }\n\n const baseUrl = staticString(source, 'baseURL') ?? staticString(source, 'baseUrl');\n return baseUrl\n ? [{ kind: 'attach', url: baseUrl, source: 'static-config', incomplete: false }]\n : [];\n}\n\nfunction configuredSpecPattern(framework: E2EFramework, source: string): string {\n if (framework === 'playwright') {\n const testDir = staticString(source, 'testDir') ?? './tests';\n return `${testDir.replace(/\\/$/, '')}/**/*.{spec,test}.{js,jsx,ts,tsx,mjs,cjs}`;\n }\n return (\n staticString(source, 'specPattern') ??\n 'cypress/{e2e,integration}/**/*.cy.{js,jsx,ts,tsx,mjs,cjs}'\n );\n}\n\nasync function scanWorkspace(\n root: string,\n maxDepth: number,\n signal: AbortSignal,\n): Promise<ScanResult> {\n const result: ScanResult = {\n configs: [],\n packageFiles: [],\n scannedDirectories: 0,\n truncated: false,\n };\n const queue: Array<{ directory: string; depth: number }> = [{ directory: root, depth: 0 }];\n\n while (queue.length > 0) {\n signal.throwIfAborted();\n const current = queue.shift();\n if (!current) break;\n result.scannedDirectories += 1;\n if (result.scannedDirectories > MAX_SCAN_DIRECTORIES) {\n result.truncated = true;\n break;\n }\n\n let entries;\n try {\n entries = await readdir(current.directory, { withFileTypes: true });\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n signal.throwIfAborted();\n const absolutePath = path.join(current.directory, entry.name);\n if (entry.isFile()) {\n if (entry.name === 'package.json') result.packageFiles.push(absolutePath);\n const framework = CONFIG_NAMES.get(entry.name);\n if (framework) result.configs.push({ framework, absolutePath });\n } else if (\n entry.isDirectory() &&\n current.depth < maxDepth &&\n !SKIP_DIRECTORIES.has(entry.name) &&\n !entry.name.startsWith('coverage-') &&\n !entry.name.startsWith('cov-')\n ) {\n queue.push({ directory: absolutePath, depth: current.depth + 1 });\n }\n }\n }\n\n return result;\n}\n\nasync function readPackageInfo(packagePath: string): Promise<PackageInfo | undefined> {\n try {\n const raw = await readBoundedText(packagePath, MAX_PACKAGE_BYTES);\n if (raw === undefined) return undefined;\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n const scripts =\n parsed.scripts && typeof parsed.scripts === 'object'\n ? Object.fromEntries(\n Object.entries(parsed.scripts as Record<string, unknown>).filter(\n (entry): entry is [string, string] => typeof entry[1] === 'string',\n ),\n )\n : {};\n const dependencies = new Set<string>();\n for (const field of ['dependencies', 'devDependencies', 'optionalDependencies']) {\n const value = parsed[field];\n if (!value || typeof value !== 'object') continue;\n for (const name of Object.keys(value as Record<string, unknown>)) dependencies.add(name);\n }\n const declared =\n typeof parsed.packageManager === 'string' ? (parsed.packageManager.split('@')[0] ?? '') : '';\n const packageManager = ['pnpm', 'npm', 'yarn', 'bun'].includes(declared)\n ? (declared as E2EPackageManager)\n : undefined;\n return { path: packagePath, scripts, dependencies, packageManager };\n } catch {\n return undefined;\n }\n}\n\nfunction frameworkFromPackage(info: PackageInfo): E2EFramework[] {\n const found = new Set<E2EFramework>();\n if (info.dependencies.has('@playwright/test') || info.dependencies.has('playwright')) {\n found.add('playwright');\n }\n if (info.dependencies.has('cypress')) found.add('cypress');\n for (const command of Object.values(info.scripts)) {\n if (/\\bplaywright(?:\\.cmd)?\\b/i.test(command)) found.add('playwright');\n if (/\\bcypress(?:\\.cmd)?\\b/i.test(command)) found.add('cypress');\n }\n return [...found];\n}\n\nasync function detectPackageManager(\n projectRoot: string,\n scanRoot: string,\n declared?: E2EPackageManager,\n): Promise<E2EPackageManager> {\n if (declared) return declared;\n let directory = projectRoot;\n while (true) {\n const names = new Set<string>();\n try {\n for (const entry of await readdir(directory)) names.add(entry);\n } catch {\n // Continue toward the scan root.\n }\n if (names.has('pnpm-lock.yaml')) return 'pnpm';\n if (names.has('yarn.lock')) return 'yarn';\n if (names.has('bun.lock') || names.has('bun.lockb')) return 'bun';\n if (names.has('package-lock.json') || names.has('npm-shrinkwrap.json')) return 'npm';\n if (directory === scanRoot) break;\n const parent = path.dirname(directory);\n const relativeParent = path.relative(scanRoot, parent);\n if (\n parent === directory ||\n relativeParent.startsWith('..') ||\n path.isAbsolute(relativeParent)\n ) {\n break;\n }\n directory = parent;\n }\n return 'npm';\n}\n\nfunction matchingScripts(info: PackageInfo | undefined, framework: E2EFramework): string[] {\n if (!info) return [];\n const matcher =\n framework === 'playwright' ? /\\bplaywright(?:\\.cmd)?\\b/i : /\\bcypress(?:\\.cmd)?\\b/i;\n return Object.entries(info.scripts)\n .filter(([, command]) => matcher.test(command))\n .map(([name]) => name)\n .sort((a, b) => {\n const preferred = ['test:e2e', 'e2e', 'e2e:test'];\n const ai = preferred.indexOf(a);\n const bi = preferred.indexOf(b);\n if (ai !== -1 || bi !== -1) return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);\n return a.localeCompare(b);\n });\n}\n\nfunction executionPlan(\n framework: E2EFramework,\n manager: E2EPackageManager,\n root: string,\n configPath: string | undefined,\n scripts: string[],\n): E2EExecutionPlan {\n const script = scripts[0];\n if (script) {\n return {\n command: manager,\n args: ['run', script],\n cwd: root,\n source: 'package-script',\n script,\n };\n }\n\n const runnerArgs = framework === 'playwright' ? ['playwright', 'test'] : ['cypress', 'run'];\n if (configPath) {\n runnerArgs.push(framework === 'playwright' ? '--config' : '--config-file', configPath);\n }\n return {\n command: manager === 'bun' ? 'bunx' : manager,\n args:\n manager === 'bun'\n ? runnerArgs\n : manager === 'npm'\n ? ['exec', '--', ...runnerArgs]\n : ['exec', ...runnerArgs],\n cwd: root,\n source: 'framework-default',\n };\n}\n\nfunction isSpec(framework: E2EFramework, filename: string): boolean {\n if (framework === 'cypress') return /\\.cy\\.(?:[cm]?[jt]sx?)$/i.test(filename);\n return /\\.(?:spec|test)\\.(?:[cm]?[jt]sx?)$/i.test(filename);\n}\n\nasync function collectSpecs(\n root: string,\n framework: E2EFramework,\n testDirectory: string | undefined,\n signal: AbortSignal,\n): Promise<{ count: number; samples: string[]; truncated: boolean }> {\n const roots = testDirectory\n ? [path.resolve(root, testDirectory)]\n : framework === 'cypress'\n ? [path.join(root, 'cypress', 'e2e'), path.join(root, 'cypress', 'integration')]\n : [path.join(root, 'tests')];\n const samples: string[] = [];\n let count = 0;\n let scanned = 0;\n const queue = [...roots];\n\n while (queue.length > 0) {\n signal.throwIfAborted();\n const directory = queue.shift();\n if (!directory) break;\n scanned += 1;\n if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };\n let entries;\n try {\n entries = await readdir(directory, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const entry of entries) {\n signal.throwIfAborted();\n const target = path.join(directory, entry.name);\n if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);\n else if (entry.isFile() && isSpec(framework, entry.name)) {\n count += 1;\n if (samples.length < MAX_SPEC_SAMPLES) samples.push(relativePath(root, target));\n }\n }\n }\n return { count, samples, truncated: count > samples.length };\n}\n\nasync function readConfig(configPath: string | undefined): Promise<string> {\n if (!configPath) return '';\n return (await readBoundedText(configPath, MAX_CONFIG_BYTES)) ?? '';\n}\n\nfunction nearestPackage(\n projectRoot: string,\n packagesByDirectory: Map<string, PackageInfo>,\n scanRoot: string,\n): PackageInfo | undefined {\n let directory = projectRoot;\n while (true) {\n const found = packagesByDirectory.get(directory);\n if (found) return found;\n if (directory === scanRoot) return undefined;\n const parent = path.dirname(directory);\n if (parent === directory || relativePath(scanRoot, parent).startsWith('..')) return undefined;\n directory = parent;\n }\n}\n\nexport async function discoverE2EProjects(\n root: string,\n options: {\n framework?: E2EFramework | 'all' | undefined;\n maxDepth?: number | undefined;\n includeSpecs?: boolean | undefined;\n signal: AbortSignal;\n },\n): Promise<E2EPlanOutput> {\n const maxDepth = Math.max(0, Math.min(options.maxDepth ?? 5, 8));\n const scan = await scanWorkspace(root, maxDepth, options.signal);\n const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(\n (info): info is PackageInfo => Boolean(info),\n );\n const packagesByDirectory = new Map(packages.map((info) => [path.dirname(info.path), info]));\n const candidates = new Map<\n string,\n { framework: E2EFramework; root: string; configPath?: string }\n >();\n\n for (const config of scan.configs) {\n if (\n options.framework &&\n options.framework !== 'all' &&\n config.framework !== options.framework\n ) {\n continue;\n }\n const projectRoot = path.dirname(config.absolutePath);\n candidates.set(`${config.framework}:${projectRoot}`, {\n framework: config.framework,\n root: projectRoot,\n configPath: config.absolutePath,\n });\n }\n\n for (const info of packages) {\n const projectRoot = path.dirname(info.path);\n for (const framework of frameworkFromPackage(info)) {\n if (options.framework && options.framework !== 'all' && framework !== options.framework)\n continue;\n const key = `${framework}:${projectRoot}`;\n if (!candidates.has(key)) candidates.set(key, { framework, root: projectRoot });\n }\n }\n\n const projects: E2EProjectPlan[] = [];\n for (const candidate of candidates.values()) {\n options.signal.throwIfAborted();\n const info = nearestPackage(candidate.root, packagesByDirectory, root);\n const source = await readConfig(candidate.configPath);\n const scripts = matchingScripts(info, candidate.framework);\n const manager = await detectPackageManager(candidate.root, root, info?.packageManager);\n const testDirectory =\n candidate.framework === 'playwright' ? staticString(source, 'testDir') : undefined;\n const resolvedTestDirectory = testDirectory\n ? path.resolve(candidate.root, testDirectory)\n : undefined;\n const relativeTestDirectory = resolvedTestDirectory\n ? path.relative(root, resolvedTestDirectory)\n : undefined;\n const unsafeTestDirectory = Boolean(\n relativeTestDirectory &&\n (relativeTestDirectory.startsWith('..') || path.isAbsolute(relativeTestDirectory)),\n );\n const specs =\n options.includeSpecs === false || unsafeTestDirectory\n ? { count: 0, samples: [], truncated: false }\n : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);\n const warnings: string[] = [];\n if (!candidate.configPath)\n warnings.push('Framework dependency or script found without a config file.');\n if (candidate.configPath && !source) {\n warnings.push('Config could not be read or exceeded the static inspection limit.');\n }\n if (unsafeTestDirectory) {\n warnings.push(\n 'Configured testDir resolves outside the requested discovery root and was not scanned.',\n );\n }\n const hints = serverHints(candidate.framework, source);\n if (hints.some((hint) => hint.incomplete)) {\n warnings.push('Configured server contains dynamic values; only static hints are shown.');\n }\n\n projects.push({\n framework: candidate.framework,\n root: relativePath(root, candidate.root),\n ...(candidate.configPath ? { configPath: relativePath(root, candidate.configPath) } : {}),\n ...(info ? { packagePath: relativePath(root, info.path) } : {}),\n packageManager: manager,\n evidence: [\n ...(candidate.configPath ? [`config:${relativePath(root, candidate.configPath)}`] : []),\n ...(info?.dependencies.has(\n candidate.framework === 'playwright' ? '@playwright/test' : 'cypress',\n )\n ? [`dependency:${candidate.framework === 'playwright' ? '@playwright/test' : 'cypress'}`]\n : []),\n ...scripts.map((script) => `script:${script}`),\n ],\n scripts,\n serverHints: hints,\n specPattern: configuredSpecPattern(candidate.framework, source),\n specCount: specs.count,\n specSamples: specs.samples,\n specsTruncated: specs.truncated,\n execution: executionPlan(\n candidate.framework,\n manager,\n relativePath(root, candidate.root),\n candidate.configPath ? relativePath(candidate.root, candidate.configPath) : undefined,\n scripts,\n ),\n warnings,\n });\n }\n\n projects.sort((a, b) => a.root.localeCompare(b.root) || a.framework.localeCompare(b.framework));\n return {\n root,\n projects,\n scannedDirectories: scan.scannedDirectories,\n scanTruncated: scan.truncated,\n warnings: scan.truncated\n ? [`Discovery stopped after ${MAX_SCAN_DIRECTORIES} directories; narrow cwd or maxDepth.`]\n : [],\n };\n}\n\nexport const e2ePlanTool: Tool<E2EPlanInput, E2EPlanOutput> = {\n name: 'e2e_plan',\n category: 'Code Quality',\n description:\n 'Discover Playwright and Cypress projects and preview bounded E2E execution plans without loading configs or starting processes.',\n usageHint:\n 'Use before running browser E2E tests. The result identifies authoritative configs, package scripts, ' +\n 'static server hints, specs, and exact argv. This tool never imports project config or executes commands.',\n permission: 'auto',\n mutating: false,\n riskTier: 'safe',\n capabilities: ['fs.read'],\n icon: 'test',\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n cwd: { type: 'string', description: 'Directory inside the project to inspect.' },\n framework: {\n type: 'string',\n enum: ['all', 'playwright', 'cypress'],\n description: 'Optional framework filter; defaults to all.',\n },\n maxDepth: {\n type: 'integer',\n minimum: 0,\n maximum: 8,\n description: 'Maximum workspace discovery depth; defaults to 5.',\n },\n includeSpecs: {\n type: 'boolean',\n description: 'Count and sample specs; defaults to true.',\n },\n },\n },\n async execute(input, ctx, opts) {\n const root = await safeResolveReal(input.cwd ?? '.', ctx);\n return discoverE2EProjects(root, {\n framework: input.framework,\n maxDepth: input.maxDepth,\n includeSpecs: input.includeSpecs,\n signal: opts.signal,\n });\n },\n};\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { open, readdir } from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { Tool } from '@wrongstack/core/types';\nimport { safeResolveReal } from './_util.js';\n\nexport type E2EFramework = 'playwright' | 'cypress';\nexport type E2EPackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';\n\ninterface E2EPlanInput {\n cwd?: string | undefined;\n framework?: E2EFramework | 'all' | undefined;\n maxDepth?: number | undefined;\n includeSpecs?: boolean | undefined;\n}\n\nexport interface E2EServerHint {\n kind: 'managed' | 'attach';\n command?: string | undefined;\n url?: string | undefined;\n port?: number | undefined;\n source: 'static-config';\n incomplete: boolean;\n}\n\nexport interface E2EExecutionPlan {\n command: string;\n args: string[];\n cwd: string;\n source: 'package-script' | 'framework-default';\n script?: string | undefined;\n}\n\nexport interface E2EProjectPlan {\n framework: E2EFramework;\n root: string;\n configPath?: string | undefined;\n packagePath?: string | undefined;\n packageManager: E2EPackageManager;\n evidence: string[];\n scripts: string[];\n serverHints: E2EServerHint[];\n specPattern: string;\n specCount: number;\n specSamples: string[];\n specsTruncated: boolean;\n execution: E2EExecutionPlan;\n warnings: string[];\n}\n\nexport interface E2EPlanOutput {\n root: string;\n projects: E2EProjectPlan[];\n scannedDirectories: number;\n scanTruncated: boolean;\n warnings: string[];\n}\n\ninterface ScanResult {\n configs: Array<{ framework: E2EFramework; absolutePath: string }>;\n packageFiles: string[];\n scannedDirectories: number;\n truncated: boolean;\n}\n\ninterface PackageInfo {\n path: string;\n scripts: Record<string, string>;\n dependencies: Set<string>;\n packageManager?: E2EPackageManager | undefined;\n}\n\nasync function readBoundedText(filePath: string, maxBytes: number): Promise<string | undefined> {\n let handle;\n try {\n handle = await open(filePath, 'r');\n const info = await handle.stat();\n if (!info.isFile() || info.size > maxBytes) return undefined;\n const buffer = Buffer.alloc(info.size);\n const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);\n return buffer.subarray(0, bytesRead).toString('utf8');\n } catch {\n return undefined;\n } finally {\n await handle?.close().catch(() => undefined);\n }\n}\n\nconst CONFIG_NAMES = new Map<string, E2EFramework>([\n ['playwright.config.ts', 'playwright'],\n ['playwright.config.js', 'playwright'],\n ['playwright.config.mts', 'playwright'],\n ['playwright.config.mjs', 'playwright'],\n ['playwright.config.cts', 'playwright'],\n ['playwright.config.cjs', 'playwright'],\n ['cypress.config.ts', 'cypress'],\n ['cypress.config.js', 'cypress'],\n ['cypress.config.mts', 'cypress'],\n ['cypress.config.mjs', 'cypress'],\n ['cypress.config.cts', 'cypress'],\n ['cypress.config.cjs', 'cypress'],\n ['cypress.json', 'cypress'],\n]);\n\nconst SKIP_DIRECTORIES = new Set([\n '.git',\n '.hg',\n '.svn',\n '.wrongstack',\n 'node_modules',\n 'dist',\n 'build',\n 'coverage',\n 'coverage-html',\n 'playwright-report',\n 'test-results',\n '.next',\n]);\n\nconst MAX_SCAN_DIRECTORIES = 4_000;\nconst MAX_PACKAGE_BYTES = 512 * 1024;\nconst MAX_CONFIG_BYTES = 512 * 1024;\nconst MAX_SPEC_SAMPLES = 20;\n\nfunction relativePath(root: string, target: string): string {\n const value = path.relative(root, target) || '.';\n return value.split(path.sep).join('/');\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction staticString(source: string, property: string): string | undefined {\n const key = escapeRegExp(property);\n const direct = source.match(\n new RegExp(`(?:['\"]?${key}['\"]?)\\\\s*:\\\\s*(['\"\\\\x60])([^'\"\\\\x60\\\\r\\\\n]*)\\\\1`),\n );\n if (direct?.[2] && !direct[2].includes('${')) return direct[2];\n\n const sameLineFallback = source.match(\n new RegExp(`(?:['\"]?${key}['\"]?)\\\\s*:[^\\\\r\\\\n]{0,300}?(['\"\\\\x60])([^'\"\\\\x60\\\\r\\\\n]*)\\\\1`),\n );\n return sameLineFallback?.[2] && !sameLineFallback[2].includes('${')\n ? sameLineFallback[2]\n : undefined;\n}\n\nfunction staticNumber(source: string, property: string): number | undefined {\n const key = escapeRegExp(property);\n const match = source.match(new RegExp(`(?:['\"]?${key}['\"]?)\\\\s*:\\\\s*(\\\\d{2,5})\\\\b`));\n return match?.[1] ? Number.parseInt(match[1], 10) : undefined;\n}\n\nfunction serverHints(framework: E2EFramework, source: string): E2EServerHint[] {\n if (framework === 'playwright' && /\\bwebServer\\s*:/.test(source)) {\n const command = staticString(source, 'command');\n const url = staticString(source, 'url');\n const port = staticNumber(source, 'port');\n return [\n {\n kind: 'managed',\n ...(command ? { command } : {}),\n ...(url ? { url } : {}),\n ...(port ? { port } : {}),\n source: 'static-config',\n incomplete: !command || (!url && !port),\n },\n ];\n }\n\n const baseUrl = staticString(source, 'baseURL') ?? staticString(source, 'baseUrl');\n return baseUrl\n ? [{ kind: 'attach', url: baseUrl, source: 'static-config', incomplete: false }]\n : [];\n}\n\nfunction configuredSpecPattern(framework: E2EFramework, source: string): string {\n if (framework === 'playwright') {\n const testDir = staticString(source, 'testDir') ?? './tests';\n return `${testDir.replace(/\\/$/, '')}/**/*.{spec,test}.{js,jsx,ts,tsx,mjs,cjs}`;\n }\n return (\n staticString(source, 'specPattern') ??\n 'cypress/{e2e,integration}/**/*.cy.{js,jsx,ts,tsx,mjs,cjs}'\n );\n}\n\nasync function scanWorkspace(\n root: string,\n maxDepth: number,\n signal: AbortSignal,\n): Promise<ScanResult> {\n const result: ScanResult = {\n configs: [],\n packageFiles: [],\n scannedDirectories: 0,\n truncated: false,\n };\n const queue: Array<{ directory: string; depth: number }> = [{ directory: root, depth: 0 }];\n\n while (queue.length > 0) {\n signal.throwIfAborted();\n const current = queue.shift();\n if (!current) break;\n result.scannedDirectories += 1;\n if (result.scannedDirectories > MAX_SCAN_DIRECTORIES) {\n result.truncated = true;\n break;\n }\n\n let entries;\n try {\n entries = await readdir(current.directory, { withFileTypes: true });\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n signal.throwIfAborted();\n const absolutePath = path.join(current.directory, entry.name);\n if (entry.isFile()) {\n if (entry.name === 'package.json') result.packageFiles.push(absolutePath);\n const framework = CONFIG_NAMES.get(entry.name);\n if (framework) result.configs.push({ framework, absolutePath });\n } else if (\n entry.isDirectory() &&\n current.depth < maxDepth &&\n !SKIP_DIRECTORIES.has(entry.name) &&\n !entry.name.startsWith('coverage-') &&\n !entry.name.startsWith('cov-')\n ) {\n queue.push({ directory: absolutePath, depth: current.depth + 1 });\n }\n }\n }\n\n return result;\n}\n\nasync function readPackageInfo(packagePath: string): Promise<PackageInfo | undefined> {\n try {\n const raw = await readBoundedText(packagePath, MAX_PACKAGE_BYTES);\n if (raw === undefined) return undefined;\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n const scripts =\n parsed.scripts && typeof parsed.scripts === 'object'\n ? Object.fromEntries(\n Object.entries(parsed.scripts as Record<string, unknown>).filter(\n (entry): entry is [string, string] => typeof entry[1] === 'string',\n ),\n )\n : {};\n const dependencies = new Set<string>();\n for (const field of ['dependencies', 'devDependencies', 'optionalDependencies']) {\n const value = parsed[field];\n if (!value || typeof value !== 'object') continue;\n for (const name of Object.keys(value as Record<string, unknown>)) dependencies.add(name);\n }\n const declared =\n typeof parsed.packageManager === 'string' ? (parsed.packageManager.split('@')[0] ?? '') : '';\n const packageManager = ['pnpm', 'npm', 'yarn', 'bun'].includes(declared)\n ? (declared as E2EPackageManager)\n : undefined;\n return { path: packagePath, scripts, dependencies, packageManager };\n } catch {\n return undefined;\n }\n}\n\nfunction frameworkFromPackage(info: PackageInfo): E2EFramework[] {\n const found = new Set<E2EFramework>();\n if (info.dependencies.has('@playwright/test') || info.dependencies.has('playwright')) {\n found.add('playwright');\n }\n if (info.dependencies.has('cypress')) found.add('cypress');\n for (const command of Object.values(info.scripts)) {\n if (/\\bplaywright(?:\\.cmd)?\\b/i.test(command)) found.add('playwright');\n if (/\\bcypress(?:\\.cmd)?\\b/i.test(command)) found.add('cypress');\n }\n return [...found];\n}\n\nasync function detectPackageManager(\n projectRoot: string,\n scanRoot: string,\n declared?: E2EPackageManager,\n): Promise<E2EPackageManager> {\n if (declared) return declared;\n let directory = projectRoot;\n while (true) {\n const names = new Set<string>();\n try {\n for (const entry of await readdir(directory)) names.add(entry);\n } catch {\n // Continue toward the scan root.\n }\n if (names.has('pnpm-lock.yaml')) return 'pnpm';\n if (names.has('yarn.lock')) return 'yarn';\n if (names.has('bun.lock') || names.has('bun.lockb')) return 'bun';\n if (names.has('package-lock.json') || names.has('npm-shrinkwrap.json')) return 'npm';\n if (directory === scanRoot) break;\n const parent = path.dirname(directory);\n const relativeParent = path.relative(scanRoot, parent);\n if (\n parent === directory ||\n relativeParent.startsWith('..') ||\n path.isAbsolute(relativeParent)\n ) {\n break;\n }\n directory = parent;\n }\n return 'npm';\n}\n\nfunction matchingScripts(info: PackageInfo | undefined, framework: E2EFramework): string[] {\n if (!info) return [];\n const matcher =\n framework === 'playwright' ? /\\bplaywright(?:\\.cmd)?\\b/i : /\\bcypress(?:\\.cmd)?\\b/i;\n return Object.entries(info.scripts)\n .filter(([, command]) => matcher.test(command))\n .map(([name]) => name)\n .sort((a, b) => {\n const preferred = ['test:e2e', 'e2e', 'e2e:test'];\n const ai = preferred.indexOf(a);\n const bi = preferred.indexOf(b);\n if (ai !== -1 || bi !== -1) return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);\n return a.localeCompare(b);\n });\n}\n\nfunction executionPlan(\n framework: E2EFramework,\n manager: E2EPackageManager,\n root: string,\n configPath: string | undefined,\n scripts: string[],\n): E2EExecutionPlan {\n const script = scripts[0];\n if (script) {\n return {\n command: manager,\n args: ['run', script],\n cwd: root,\n source: 'package-script',\n script,\n };\n }\n\n const runnerArgs = framework === 'playwright' ? ['playwright', 'test'] : ['cypress', 'run'];\n if (configPath) {\n runnerArgs.push(framework === 'playwright' ? '--config' : '--config-file', configPath);\n }\n return {\n command: manager === 'bun' ? 'bunx' : manager,\n args:\n manager === 'bun'\n ? runnerArgs\n : manager === 'npm'\n ? ['exec', '--', ...runnerArgs]\n : ['exec', ...runnerArgs],\n cwd: root,\n source: 'framework-default',\n };\n}\n\nfunction isSpec(framework: E2EFramework, filename: string): boolean {\n if (framework === 'cypress') return /\\.cy\\.(?:[cm]?[jt]sx?)$/i.test(filename);\n return /\\.(?:spec|test)\\.(?:[cm]?[jt]sx?)$/i.test(filename);\n}\n\nasync function collectSpecs(\n root: string,\n framework: E2EFramework,\n testDirectory: string | undefined,\n signal: AbortSignal,\n): Promise<{ count: number; samples: string[]; truncated: boolean }> {\n const roots = testDirectory\n ? [path.resolve(root, testDirectory)]\n : framework === 'cypress'\n ? [path.join(root, 'cypress', 'e2e'), path.join(root, 'cypress', 'integration')]\n : [path.join(root, 'tests')];\n const samples: string[] = [];\n let count = 0;\n let scanned = 0;\n const queue = [...roots];\n\n while (queue.length > 0) {\n signal.throwIfAborted();\n const directory = queue.shift();\n if (!directory) break;\n scanned += 1;\n if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };\n let entries;\n try {\n entries = await readdir(directory, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const entry of entries) {\n signal.throwIfAborted();\n const target = path.join(directory, entry.name);\n if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);\n else if (entry.isFile() && isSpec(framework, entry.name)) {\n count += 1;\n if (samples.length < MAX_SPEC_SAMPLES) samples.push(relativePath(root, target));\n }\n }\n }\n return { count, samples, truncated: count > samples.length };\n}\n\nasync function readConfig(configPath: string | undefined): Promise<string> {\n if (!configPath) return '';\n return (await readBoundedText(configPath, MAX_CONFIG_BYTES)) ?? '';\n}\n\nfunction nearestPackage(\n projectRoot: string,\n packagesByDirectory: Map<string, PackageInfo>,\n scanRoot: string,\n): PackageInfo | undefined {\n let directory = projectRoot;\n while (true) {\n const found = packagesByDirectory.get(directory);\n if (found) return found;\n if (directory === scanRoot) return undefined;\n const parent = path.dirname(directory);\n if (parent === directory || relativePath(scanRoot, parent).startsWith('..')) return undefined;\n directory = parent;\n }\n}\n\nexport async function discoverE2EProjects(\n root: string,\n options: {\n framework?: E2EFramework | 'all' | undefined;\n maxDepth?: number | undefined;\n includeSpecs?: boolean | undefined;\n signal: AbortSignal;\n },\n): Promise<E2EPlanOutput> {\n const maxDepth = Math.max(0, Math.min(options.maxDepth ?? 5, 8));\n const scan = await scanWorkspace(root, maxDepth, options.signal);\n const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(\n (info): info is PackageInfo => Boolean(info),\n );\n const packagesByDirectory = new Map(packages.map((info) => [path.dirname(info.path), info]));\n const candidates = new Map<\n string,\n { framework: E2EFramework; root: string; configPath?: string }\n >();\n\n for (const config of scan.configs) {\n if (\n options.framework &&\n options.framework !== 'all' &&\n config.framework !== options.framework\n ) {\n continue;\n }\n const projectRoot = path.dirname(config.absolutePath);\n candidates.set(`${config.framework}:${projectRoot}`, {\n framework: config.framework,\n root: projectRoot,\n configPath: config.absolutePath,\n });\n }\n\n for (const info of packages) {\n const projectRoot = path.dirname(info.path);\n for (const framework of frameworkFromPackage(info)) {\n if (options.framework && options.framework !== 'all' && framework !== options.framework)\n continue;\n const key = `${framework}:${projectRoot}`;\n if (!candidates.has(key)) candidates.set(key, { framework, root: projectRoot });\n }\n }\n\n const projects: E2EProjectPlan[] = [];\n for (const candidate of candidates.values()) {\n options.signal.throwIfAborted();\n const info = nearestPackage(candidate.root, packagesByDirectory, root);\n const source = await readConfig(candidate.configPath);\n const scripts = matchingScripts(info, candidate.framework);\n const manager = await detectPackageManager(candidate.root, root, info?.packageManager);\n const testDirectory =\n candidate.framework === 'playwright' ? staticString(source, 'testDir') : undefined;\n const resolvedTestDirectory = testDirectory\n ? path.resolve(candidate.root, testDirectory)\n : undefined;\n const relativeTestDirectory = resolvedTestDirectory\n ? path.relative(root, resolvedTestDirectory)\n : undefined;\n const unsafeTestDirectory = Boolean(\n relativeTestDirectory &&\n (relativeTestDirectory.startsWith('..') || path.isAbsolute(relativeTestDirectory)),\n );\n const specs =\n options.includeSpecs === false || unsafeTestDirectory\n ? { count: 0, samples: [], truncated: false }\n : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);\n const warnings: string[] = [];\n if (!candidate.configPath)\n warnings.push('Framework dependency or script found without a config file.');\n if (candidate.configPath && !source) {\n warnings.push('Config could not be read or exceeded the static inspection limit.');\n }\n if (unsafeTestDirectory) {\n warnings.push(\n 'Configured testDir resolves outside the requested discovery root and was not scanned.',\n );\n }\n const hints = serverHints(candidate.framework, source);\n if (hints.some((hint) => hint.incomplete)) {\n warnings.push('Configured server contains dynamic values; only static hints are shown.');\n }\n\n projects.push({\n framework: candidate.framework,\n root: relativePath(root, candidate.root),\n ...(candidate.configPath ? { configPath: relativePath(root, candidate.configPath) } : {}),\n ...(info ? { packagePath: relativePath(root, info.path) } : {}),\n packageManager: manager,\n evidence: [\n ...(candidate.configPath ? [`config:${relativePath(root, candidate.configPath)}`] : []),\n ...(info?.dependencies.has(\n candidate.framework === 'playwright' ? '@playwright/test' : 'cypress',\n )\n ? [`dependency:${candidate.framework === 'playwright' ? '@playwright/test' : 'cypress'}`]\n : []),\n ...scripts.map((script) => `script:${script}`),\n ],\n scripts,\n serverHints: hints,\n specPattern: configuredSpecPattern(candidate.framework, source),\n specCount: specs.count,\n specSamples: specs.samples,\n specsTruncated: specs.truncated,\n execution: executionPlan(\n candidate.framework,\n manager,\n relativePath(root, candidate.root),\n candidate.configPath ? relativePath(candidate.root, candidate.configPath) : undefined,\n scripts,\n ),\n warnings,\n });\n }\n\n projects.sort((a, b) => a.root.localeCompare(b.root) || a.framework.localeCompare(b.framework));\n return {\n root,\n projects,\n scannedDirectories: scan.scannedDirectories,\n scanTruncated: scan.truncated,\n warnings: scan.truncated\n ? [`Discovery stopped after ${MAX_SCAN_DIRECTORIES} directories; narrow cwd or maxDepth.`]\n : [],\n };\n}\n\nexport const e2ePlanTool: Tool<E2EPlanInput, E2EPlanOutput> = {\n name: 'e2e_plan',\n category: 'Code Quality',\n description:\n 'Discover Playwright and Cypress projects and preview bounded E2E execution plans without loading configs or starting processes.',\n usageHint:\n 'Use before running browser E2E tests. The result identifies authoritative configs, package scripts, ' +\n 'static server hints, specs, and exact argv. This tool never imports project config or executes commands.',\n permission: 'auto',\n mutating: false,\n riskTier: 'safe',\n capabilities: ['fs.read'],\n icon: 'test',\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n cwd: { type: 'string', description: 'Directory inside the project to inspect.' },\n framework: {\n type: 'string',\n enum: ['all', 'playwright', 'cypress'],\n description: 'Optional framework filter; defaults to all.',\n },\n maxDepth: {\n type: 'integer',\n minimum: 0,\n maximum: 8,\n description: 'Maximum workspace discovery depth; defaults to 5.',\n },\n includeSpecs: {\n type: 'boolean',\n description: 'Count and sample specs; defaults to true.',\n },\n },\n },\n async execute(input, ctx, opts) {\n const root = await safeResolveReal(input.cwd ?? '.', ctx);\n return discoverE2EProjects(root, {\n framework: input.framework,\n maxDepth: input.maxDepth,\n includeSpecs: input.includeSpecs,\n signal: opts.signal,\n });\n },\n};\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core/utils';\nimport type { Context } from '@wrongstack/core/agent';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["path"]
|
|
7
7
|
}
|
package/dist/edit.d.ts
CHANGED
package/dist/edit.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../src/edit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../src/edit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AASnD,OAAO,EAIL,KAAK,SAAS,EAIf,MAAM,kBAAkB,CAAC;AAI1B,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACnC;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACrC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAsRhD,CAAC"}
|
package/dist/edit.js
CHANGED
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
atomicWrite,
|
|
5
5
|
detectNewlineStyle,
|
|
6
6
|
normalizeToLf,
|
|
7
|
-
ToolValidationError,
|
|
8
7
|
toStyle,
|
|
9
8
|
unifiedDiff
|
|
10
|
-
} from "@wrongstack/core";
|
|
9
|
+
} from "@wrongstack/core/utils";
|
|
10
|
+
import { ToolValidationError } from "@wrongstack/core/types";
|
|
11
11
|
|
|
12
12
|
// src/_edit-match.ts
|
|
13
13
|
var TIER_LABEL = {
|
|
@@ -323,7 +323,7 @@ function formatDiag(ts, diag, content, sourceFile) {
|
|
|
323
323
|
import { createHash } from "node:crypto";
|
|
324
324
|
import * as fsp from "node:fs/promises";
|
|
325
325
|
import * as path2 from "node:path";
|
|
326
|
-
import * as Core from "@wrongstack/core";
|
|
326
|
+
import * as Core from "@wrongstack/core/utils";
|
|
327
327
|
function sha256hex(content) {
|
|
328
328
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
329
329
|
}
|