@sentry/warden 0.0.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/.agents/skills/find-bugs/SKILL.md +75 -0
- package/.agents/skills/vercel-react-best-practices/AGENTS.md +2934 -0
- package/.agents/skills/vercel-react-best-practices/SKILL.md +136 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.claude/settings.json +57 -0
- package/.claude/settings.local.json +88 -0
- package/.claude/skills/agent-prompt/SKILL.md +54 -0
- package/.claude/skills/agent-prompt/references/agentic-patterns.md +94 -0
- package/.claude/skills/agent-prompt/references/anti-patterns.md +140 -0
- package/.claude/skills/agent-prompt/references/context-design.md +124 -0
- package/.claude/skills/agent-prompt/references/core-principles.md +75 -0
- package/.claude/skills/agent-prompt/references/model-guidance.md +118 -0
- package/.claude/skills/agent-prompt/references/output-formats.md +98 -0
- package/.claude/skills/agent-prompt/references/skill-structure.md +115 -0
- package/.claude/skills/agent-prompt/references/system-prompts.md +115 -0
- package/.claude/skills/notseer/SKILL.md +131 -0
- package/.claude/skills/skill-writer/SKILL.md +140 -0
- package/.claude/skills/testing-guidelines/SKILL.md +132 -0
- package/.claude/skills/warden-skill/SKILL.md +250 -0
- package/.claude/skills/warden-skill/references/config-schema.md +133 -0
- package/.dex/config.toml +2 -0
- package/.github/workflows/ci.yml +33 -0
- package/.github/workflows/release.yml +54 -0
- package/.github/workflows/warden.yml +40 -0
- package/AGENTS.md +89 -0
- package/CONTRIBUTING.md +60 -0
- package/LICENSE +105 -0
- package/README.md +43 -0
- package/SPEC.md +263 -0
- package/action.yml +87 -0
- package/assets/favicon.png +0 -0
- package/assets/warden-icon-bw.svg +5 -0
- package/assets/warden-icon-purple.png +0 -0
- package/assets/warden-icon-purple.svg +5 -0
- package/docs/.claude/settings.local.json +11 -0
- package/docs/astro.config.mjs +43 -0
- package/docs/package.json +19 -0
- package/docs/pnpm-lock.yaml +4000 -0
- package/docs/public/favicon.svg +5 -0
- package/docs/src/components/Code.astro +141 -0
- package/docs/src/components/PackageManagerTabs.astro +183 -0
- package/docs/src/components/Terminal.astro +212 -0
- package/docs/src/layouts/Base.astro +380 -0
- package/docs/src/pages/cli.astro +167 -0
- package/docs/src/pages/config.astro +394 -0
- package/docs/src/pages/guide.astro +449 -0
- package/docs/src/pages/index.astro +490 -0
- package/docs/src/styles/global.css +551 -0
- package/docs/tsconfig.json +3 -0
- package/docs/vercel.json +5 -0
- package/eslint.config.js +33 -0
- package/package.json +73 -0
- package/src/action/index.ts +1 -0
- package/src/action/main.ts +868 -0
- package/src/cli/args.test.ts +477 -0
- package/src/cli/args.ts +415 -0
- package/src/cli/commands/add.ts +447 -0
- package/src/cli/commands/init.test.ts +136 -0
- package/src/cli/commands/init.ts +132 -0
- package/src/cli/commands/setup-app/browser.ts +38 -0
- package/src/cli/commands/setup-app/credentials.ts +45 -0
- package/src/cli/commands/setup-app/manifest.ts +48 -0
- package/src/cli/commands/setup-app/server.ts +172 -0
- package/src/cli/commands/setup-app.ts +156 -0
- package/src/cli/commands/sync.ts +114 -0
- package/src/cli/context.ts +131 -0
- package/src/cli/files.test.ts +155 -0
- package/src/cli/files.ts +89 -0
- package/src/cli/fix.test.ts +310 -0
- package/src/cli/fix.ts +387 -0
- package/src/cli/git.test.ts +119 -0
- package/src/cli/git.ts +318 -0
- package/src/cli/index.ts +14 -0
- package/src/cli/main.ts +672 -0
- package/src/cli/output/box.ts +235 -0
- package/src/cli/output/formatters.test.ts +187 -0
- package/src/cli/output/formatters.ts +269 -0
- package/src/cli/output/icons.ts +13 -0
- package/src/cli/output/index.ts +44 -0
- package/src/cli/output/ink-runner.tsx +337 -0
- package/src/cli/output/jsonl.test.ts +347 -0
- package/src/cli/output/jsonl.ts +126 -0
- package/src/cli/output/reporter.ts +435 -0
- package/src/cli/output/tasks.ts +374 -0
- package/src/cli/output/tty.test.ts +117 -0
- package/src/cli/output/tty.ts +60 -0
- package/src/cli/output/verbosity.test.ts +40 -0
- package/src/cli/output/verbosity.ts +31 -0
- package/src/cli/terminal.test.ts +148 -0
- package/src/cli/terminal.ts +301 -0
- package/src/config/index.ts +3 -0
- package/src/config/loader.test.ts +313 -0
- package/src/config/loader.ts +103 -0
- package/src/config/schema.ts +168 -0
- package/src/config/writer.test.ts +119 -0
- package/src/config/writer.ts +84 -0
- package/src/diff/classify.test.ts +162 -0
- package/src/diff/classify.ts +92 -0
- package/src/diff/coalesce.test.ts +208 -0
- package/src/diff/coalesce.ts +133 -0
- package/src/diff/context.test.ts +226 -0
- package/src/diff/context.ts +201 -0
- package/src/diff/index.ts +4 -0
- package/src/diff/parser.test.ts +212 -0
- package/src/diff/parser.ts +149 -0
- package/src/event/context.ts +132 -0
- package/src/event/index.ts +2 -0
- package/src/event/schedule-context.ts +101 -0
- package/src/examples/examples.integration.test.ts +66 -0
- package/src/examples/index.test.ts +101 -0
- package/src/examples/index.ts +122 -0
- package/src/examples/setup.ts +25 -0
- package/src/index.ts +115 -0
- package/src/output/dedup.test.ts +419 -0
- package/src/output/dedup.ts +607 -0
- package/src/output/github-checks.test.ts +300 -0
- package/src/output/github-checks.ts +476 -0
- package/src/output/github-issues.ts +329 -0
- package/src/output/index.ts +5 -0
- package/src/output/issue-renderer.ts +197 -0
- package/src/output/renderer.test.ts +727 -0
- package/src/output/renderer.ts +217 -0
- package/src/output/stale.test.ts +375 -0
- package/src/output/stale.ts +155 -0
- package/src/output/types.ts +34 -0
- package/src/sdk/index.ts +1 -0
- package/src/sdk/runner.test.ts +806 -0
- package/src/sdk/runner.ts +1232 -0
- package/src/skills/index.ts +36 -0
- package/src/skills/loader.test.ts +300 -0
- package/src/skills/loader.ts +423 -0
- package/src/skills/remote.test.ts +704 -0
- package/src/skills/remote.ts +604 -0
- package/src/triggers/matcher.test.ts +277 -0
- package/src/triggers/matcher.ts +152 -0
- package/src/types/index.ts +194 -0
- package/src/utils/async.ts +18 -0
- package/src/utils/index.test.ts +84 -0
- package/src/utils/index.ts +50 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +8 -0
- package/vitest.integration.config.ts +11 -0
- package/warden.toml +19 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import type { EventContext, FileChange } from '../types/index.js';
|
|
3
|
+
import { pluralize } from './output/index.js';
|
|
4
|
+
import { expandAndCreateFileChanges } from './files.js';
|
|
5
|
+
import {
|
|
6
|
+
getChangedFilesWithPatches,
|
|
7
|
+
getCurrentBranch,
|
|
8
|
+
getHeadSha,
|
|
9
|
+
getDefaultBranch,
|
|
10
|
+
getRepoRoot,
|
|
11
|
+
getRepoName,
|
|
12
|
+
getCommitMessage,
|
|
13
|
+
type GitFileChange,
|
|
14
|
+
} from './git.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert git file change to EventContext FileChange format.
|
|
18
|
+
*/
|
|
19
|
+
function toFileChange(file: GitFileChange): FileChange {
|
|
20
|
+
return {
|
|
21
|
+
filename: file.filename,
|
|
22
|
+
status: file.status,
|
|
23
|
+
additions: file.additions,
|
|
24
|
+
deletions: file.deletions,
|
|
25
|
+
patch: file.patch,
|
|
26
|
+
chunks: file.chunks,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LocalContextOptions {
|
|
31
|
+
base?: string;
|
|
32
|
+
head?: string;
|
|
33
|
+
cwd?: string;
|
|
34
|
+
/** Override auto-detected default branch (from config) */
|
|
35
|
+
defaultBranch?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build an EventContext from local git repository state.
|
|
40
|
+
* Creates a synthetic pull_request event from git diff.
|
|
41
|
+
*
|
|
42
|
+
* When analyzing a specific commit (head is set), uses the actual commit
|
|
43
|
+
* message as title/body to provide intent context to the LLM.
|
|
44
|
+
*/
|
|
45
|
+
export function buildLocalEventContext(options: LocalContextOptions = {}): EventContext {
|
|
46
|
+
const cwd = options.cwd ?? process.cwd();
|
|
47
|
+
const repoPath = getRepoRoot(cwd);
|
|
48
|
+
const { owner, name } = getRepoName(cwd);
|
|
49
|
+
const defaultBranch = options.defaultBranch ?? getDefaultBranch(cwd);
|
|
50
|
+
|
|
51
|
+
const base = options.base ?? defaultBranch;
|
|
52
|
+
const head = options.head; // undefined means working tree
|
|
53
|
+
const currentBranch = getCurrentBranch(cwd);
|
|
54
|
+
const headSha = head ? head : getHeadSha(cwd);
|
|
55
|
+
|
|
56
|
+
const changedFiles = getChangedFilesWithPatches(base, head, cwd);
|
|
57
|
+
const files = changedFiles.map(toFileChange);
|
|
58
|
+
|
|
59
|
+
// Use actual commit message when analyzing a specific commit
|
|
60
|
+
let title: string;
|
|
61
|
+
let body: string;
|
|
62
|
+
if (head) {
|
|
63
|
+
const commitMsg = getCommitMessage(head, cwd);
|
|
64
|
+
title = commitMsg.subject || `Commit ${head}`;
|
|
65
|
+
body = commitMsg.body || `Analyzing changes in ${head}`;
|
|
66
|
+
} else {
|
|
67
|
+
title = `Local changes: ${currentBranch}`;
|
|
68
|
+
body = `Analyzing local changes from ${base} to working tree`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
eventType: 'pull_request',
|
|
73
|
+
action: 'opened',
|
|
74
|
+
repository: {
|
|
75
|
+
owner,
|
|
76
|
+
name,
|
|
77
|
+
fullName: `${owner}/${name}`,
|
|
78
|
+
defaultBranch,
|
|
79
|
+
},
|
|
80
|
+
pullRequest: {
|
|
81
|
+
number: 0, // Local run, no real PR number
|
|
82
|
+
title,
|
|
83
|
+
body,
|
|
84
|
+
author: 'local',
|
|
85
|
+
baseBranch: base,
|
|
86
|
+
headBranch: currentBranch,
|
|
87
|
+
headSha,
|
|
88
|
+
files,
|
|
89
|
+
},
|
|
90
|
+
repoPath,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface FileContextOptions {
|
|
95
|
+
patterns: string[];
|
|
96
|
+
cwd?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build an EventContext from a list of files or glob patterns.
|
|
101
|
+
* Creates a synthetic pull_request event treating files as newly added.
|
|
102
|
+
* This allows analysis without requiring git or a warden.toml config.
|
|
103
|
+
*/
|
|
104
|
+
export async function buildFileEventContext(options: FileContextOptions): Promise<EventContext> {
|
|
105
|
+
const cwd = options.cwd ?? process.cwd();
|
|
106
|
+
const dirName = basename(cwd);
|
|
107
|
+
|
|
108
|
+
const files = await expandAndCreateFileChanges(options.patterns, cwd);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
eventType: 'pull_request',
|
|
112
|
+
action: 'opened',
|
|
113
|
+
repository: {
|
|
114
|
+
owner: 'local',
|
|
115
|
+
name: dirName,
|
|
116
|
+
fullName: `local/${dirName}`,
|
|
117
|
+
defaultBranch: 'main',
|
|
118
|
+
},
|
|
119
|
+
pullRequest: {
|
|
120
|
+
number: 0,
|
|
121
|
+
title: 'File analysis',
|
|
122
|
+
body: `Analyzing ${files.length} ${pluralize(files.length, 'file')}`,
|
|
123
|
+
author: 'local',
|
|
124
|
+
baseBranch: 'main',
|
|
125
|
+
headBranch: 'file-analysis',
|
|
126
|
+
headSha: 'file-analysis',
|
|
127
|
+
files,
|
|
128
|
+
},
|
|
129
|
+
repoPath: cwd,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import {
|
|
6
|
+
createPatchFromContent,
|
|
7
|
+
createSyntheticFileChange,
|
|
8
|
+
expandFileGlobs,
|
|
9
|
+
expandAndCreateFileChanges,
|
|
10
|
+
} from './files.js';
|
|
11
|
+
|
|
12
|
+
describe('createPatchFromContent', () => {
|
|
13
|
+
it('creates patch for single line content', () => {
|
|
14
|
+
const patch = createPatchFromContent('hello world');
|
|
15
|
+
expect(patch).toBe('@@ -0,0 +1,1 @@\n+hello world');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('creates patch for multi-line content', () => {
|
|
19
|
+
const patch = createPatchFromContent('line1\nline2\nline3');
|
|
20
|
+
expect(patch).toBe('@@ -0,0 +1,3 @@\n+line1\n+line2\n+line3');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('handles empty file', () => {
|
|
24
|
+
const patch = createPatchFromContent('');
|
|
25
|
+
expect(patch).toBe('@@ -0,0 +0,0 @@\n');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('handles file ending with newline', () => {
|
|
29
|
+
const patch = createPatchFromContent('line1\nline2\n');
|
|
30
|
+
expect(patch).toBe('@@ -0,0 +1,3 @@\n+line1\n+line2\n+');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('createSyntheticFileChange', () => {
|
|
35
|
+
let tempDir: string;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
tempDir = join(tmpdir(), `warden-test-${Date.now()}`);
|
|
39
|
+
mkdirSync(tempDir, { recursive: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('creates FileChange from file', () => {
|
|
47
|
+
const filePath = join(tempDir, 'test.ts');
|
|
48
|
+
writeFileSync(filePath, 'const x = 1;\nconst y = 2;');
|
|
49
|
+
|
|
50
|
+
const change = createSyntheticFileChange(filePath, tempDir);
|
|
51
|
+
|
|
52
|
+
expect(change.filename).toBe('test.ts');
|
|
53
|
+
expect(change.status).toBe('added');
|
|
54
|
+
expect(change.additions).toBe(2);
|
|
55
|
+
expect(change.deletions).toBe(0);
|
|
56
|
+
expect(change.patch).toContain('+const x = 1;');
|
|
57
|
+
expect(change.patch).toContain('+const y = 2;');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('handles nested files', () => {
|
|
61
|
+
const subDir = join(tempDir, 'src', 'utils');
|
|
62
|
+
mkdirSync(subDir, { recursive: true });
|
|
63
|
+
const filePath = join(subDir, 'helper.ts');
|
|
64
|
+
writeFileSync(filePath, 'export const helper = () => {};\n');
|
|
65
|
+
|
|
66
|
+
const change = createSyntheticFileChange(filePath, tempDir);
|
|
67
|
+
|
|
68
|
+
expect(change.filename).toBe('src/utils/helper.ts');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('expandFileGlobs', () => {
|
|
73
|
+
let tempDir: string;
|
|
74
|
+
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
tempDir = join(tmpdir(), `warden-test-${Date.now()}`);
|
|
77
|
+
mkdirSync(tempDir, { recursive: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('expands glob pattern', async () => {
|
|
85
|
+
writeFileSync(join(tempDir, 'file1.ts'), 'content1');
|
|
86
|
+
writeFileSync(join(tempDir, 'file2.ts'), 'content2');
|
|
87
|
+
writeFileSync(join(tempDir, 'file.js'), 'content3');
|
|
88
|
+
|
|
89
|
+
const files = await expandFileGlobs(['*.ts'], tempDir);
|
|
90
|
+
|
|
91
|
+
expect(files).toHaveLength(2);
|
|
92
|
+
expect(files.some(f => f.endsWith('file1.ts'))).toBe(true);
|
|
93
|
+
expect(files.some(f => f.endsWith('file2.ts'))).toBe(true);
|
|
94
|
+
expect(files.some(f => f.endsWith('file.js'))).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('expands nested glob pattern', async () => {
|
|
98
|
+
const srcDir = join(tempDir, 'src');
|
|
99
|
+
mkdirSync(srcDir, { recursive: true });
|
|
100
|
+
writeFileSync(join(tempDir, 'root.ts'), 'root');
|
|
101
|
+
writeFileSync(join(srcDir, 'nested.ts'), 'nested');
|
|
102
|
+
|
|
103
|
+
const files = await expandFileGlobs(['**/*.ts'], tempDir);
|
|
104
|
+
|
|
105
|
+
expect(files).toHaveLength(2);
|
|
106
|
+
expect(files.some(f => f.endsWith('root.ts'))).toBe(true);
|
|
107
|
+
expect(files.some(f => f.includes('src/nested.ts'))).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('handles specific file path', async () => {
|
|
111
|
+
const filePath = join(tempDir, 'specific.ts');
|
|
112
|
+
writeFileSync(filePath, 'content');
|
|
113
|
+
|
|
114
|
+
const files = await expandFileGlobs(['specific.ts'], tempDir);
|
|
115
|
+
|
|
116
|
+
expect(files).toHaveLength(1);
|
|
117
|
+
expect(files[0]).toContain('specific.ts');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns empty for no matches', async () => {
|
|
121
|
+
const files = await expandFileGlobs(['*.nonexistent'], tempDir);
|
|
122
|
+
expect(files).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('expandAndCreateFileChanges', () => {
|
|
127
|
+
let tempDir: string;
|
|
128
|
+
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
tempDir = join(tmpdir(), `warden-test-${Date.now()}`);
|
|
131
|
+
mkdirSync(tempDir, { recursive: true });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('combines glob expansion and file change creation', async () => {
|
|
139
|
+
writeFileSync(join(tempDir, 'file1.ts'), 'const a = 1;');
|
|
140
|
+
writeFileSync(join(tempDir, 'file2.ts'), 'const b = 2;\nconst c = 3;');
|
|
141
|
+
|
|
142
|
+
const changes = await expandAndCreateFileChanges(['*.ts'], tempDir);
|
|
143
|
+
|
|
144
|
+
expect(changes).toHaveLength(2);
|
|
145
|
+
expect(changes.every(c => c.status === 'added')).toBe(true);
|
|
146
|
+
|
|
147
|
+
const file1 = changes.find(c => c.filename === 'file1.ts');
|
|
148
|
+
expect(file1).toBeDefined();
|
|
149
|
+
expect(file1?.additions).toBe(1);
|
|
150
|
+
|
|
151
|
+
const file2 = changes.find(c => c.filename === 'file2.ts');
|
|
152
|
+
expect(file2).toBeDefined();
|
|
153
|
+
expect(file2?.additions).toBe(2);
|
|
154
|
+
});
|
|
155
|
+
});
|
package/src/cli/files.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, relative } from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { countPatchChunks } from '../types/index.js';
|
|
5
|
+
import type { FileChange } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Expand glob patterns to a list of file paths.
|
|
9
|
+
*/
|
|
10
|
+
export async function expandFileGlobs(
|
|
11
|
+
patterns: string[],
|
|
12
|
+
cwd: string = process.cwd()
|
|
13
|
+
): Promise<string[]> {
|
|
14
|
+
const files = await fg(patterns, {
|
|
15
|
+
cwd,
|
|
16
|
+
onlyFiles: true,
|
|
17
|
+
absolute: true,
|
|
18
|
+
dot: false,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return files.sort();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a unified diff patch for a file, treating entire content as added.
|
|
26
|
+
*/
|
|
27
|
+
export function createPatchFromContent(content: string): string {
|
|
28
|
+
const lines = content.split('\n');
|
|
29
|
+
const lineCount = lines.length;
|
|
30
|
+
|
|
31
|
+
// Handle empty files
|
|
32
|
+
if (lineCount === 0 || (lineCount === 1 && lines[0] === '')) {
|
|
33
|
+
return '@@ -0,0 +0,0 @@\n';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Create patch header showing all lines as additions
|
|
37
|
+
const patchLines = [`@@ -0,0 +1,${lineCount} @@`];
|
|
38
|
+
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
patchLines.push(`+${line}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return patchLines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read a file and create a synthetic FileChange treating it as newly added.
|
|
48
|
+
*/
|
|
49
|
+
export function createSyntheticFileChange(
|
|
50
|
+
absolutePath: string,
|
|
51
|
+
basePath: string
|
|
52
|
+
): FileChange {
|
|
53
|
+
const content = readFileSync(absolutePath, 'utf-8');
|
|
54
|
+
const lines = content.split('\n');
|
|
55
|
+
const lineCount = lines.length;
|
|
56
|
+
const relativePath = relative(basePath, absolutePath);
|
|
57
|
+
const patch = createPatchFromContent(content);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
filename: relativePath,
|
|
61
|
+
status: 'added',
|
|
62
|
+
additions: lineCount,
|
|
63
|
+
deletions: 0,
|
|
64
|
+
patch,
|
|
65
|
+
chunks: countPatchChunks(patch),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Process a list of file paths into FileChange objects.
|
|
71
|
+
*/
|
|
72
|
+
export function createSyntheticFileChanges(
|
|
73
|
+
absolutePaths: string[],
|
|
74
|
+
basePath: string
|
|
75
|
+
): FileChange[] {
|
|
76
|
+
return absolutePaths.map((filePath) => createSyntheticFileChange(filePath, basePath));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Expand glob patterns and create FileChange objects for all matching files.
|
|
81
|
+
*/
|
|
82
|
+
export async function expandAndCreateFileChanges(
|
|
83
|
+
patterns: string[],
|
|
84
|
+
cwd: string = process.cwd()
|
|
85
|
+
): Promise<FileChange[]> {
|
|
86
|
+
const resolvedCwd = resolve(cwd);
|
|
87
|
+
const files = await expandFileGlobs(patterns, resolvedCwd);
|
|
88
|
+
return createSyntheticFileChanges(files, resolvedCwd);
|
|
89
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import {
|
|
6
|
+
applyUnifiedDiff,
|
|
7
|
+
collectFixableFindings,
|
|
8
|
+
applyAllFixes,
|
|
9
|
+
} from './fix.js';
|
|
10
|
+
import type { Finding, SkillReport } from '../types/index.js';
|
|
11
|
+
|
|
12
|
+
describe('applyUnifiedDiff', () => {
|
|
13
|
+
let testDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
testDir = join(tmpdir(), `warden-fix-test-${Date.now()}`);
|
|
17
|
+
mkdirSync(testDir, { recursive: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('applies a single-line replacement', () => {
|
|
25
|
+
const filePath = join(testDir, 'test.ts');
|
|
26
|
+
writeFileSync(filePath, 'const x = "hello";\nconst y = "world";\n');
|
|
27
|
+
|
|
28
|
+
const diff = `@@ -1,2 +1,2 @@
|
|
29
|
+
-const x = "hello";
|
|
30
|
+
+const x = "goodbye";
|
|
31
|
+
const y = "world";`;
|
|
32
|
+
|
|
33
|
+
applyUnifiedDiff(filePath, diff);
|
|
34
|
+
|
|
35
|
+
const result = readFileSync(filePath, 'utf-8');
|
|
36
|
+
expect(result).toBe('const x = "goodbye";\nconst y = "world";\n');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('applies a multi-line addition', () => {
|
|
40
|
+
const filePath = join(testDir, 'test.ts');
|
|
41
|
+
writeFileSync(filePath, 'function foo() {\n return 1;\n}\n');
|
|
42
|
+
|
|
43
|
+
const diff = `@@ -1,3 +1,5 @@
|
|
44
|
+
function foo() {
|
|
45
|
+
+ // Added comment
|
|
46
|
+
+ const x = 1;
|
|
47
|
+
return 1;
|
|
48
|
+
}`;
|
|
49
|
+
|
|
50
|
+
applyUnifiedDiff(filePath, diff);
|
|
51
|
+
|
|
52
|
+
const result = readFileSync(filePath, 'utf-8');
|
|
53
|
+
expect(result).toBe('function foo() {\n // Added comment\n const x = 1;\n return 1;\n}\n');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('applies a multi-line deletion', () => {
|
|
57
|
+
const filePath = join(testDir, 'test.ts');
|
|
58
|
+
writeFileSync(filePath, 'line1\nline2\nline3\nline4\n');
|
|
59
|
+
|
|
60
|
+
const diff = `@@ -1,4 +1,2 @@
|
|
61
|
+
line1
|
|
62
|
+
-line2
|
|
63
|
+
-line3
|
|
64
|
+
line4`;
|
|
65
|
+
|
|
66
|
+
applyUnifiedDiff(filePath, diff);
|
|
67
|
+
|
|
68
|
+
const result = readFileSync(filePath, 'utf-8');
|
|
69
|
+
expect(result).toBe('line1\nline4\n');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('applies multiple hunks in reverse order', () => {
|
|
73
|
+
const filePath = join(testDir, 'test.ts');
|
|
74
|
+
writeFileSync(filePath, 'a\nb\nc\nd\ne\nf\ng\n');
|
|
75
|
+
|
|
76
|
+
// Two hunks: one at line 2, one at line 6
|
|
77
|
+
const diff = `@@ -2,1 +2,1 @@
|
|
78
|
+
-b
|
|
79
|
+
+B
|
|
80
|
+
@@ -6,1 +6,1 @@
|
|
81
|
+
-f
|
|
82
|
+
+F`;
|
|
83
|
+
|
|
84
|
+
applyUnifiedDiff(filePath, diff);
|
|
85
|
+
|
|
86
|
+
const result = readFileSync(filePath, 'utf-8');
|
|
87
|
+
expect(result).toBe('a\nB\nc\nd\ne\nF\ng\n');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('throws error for non-existent file', () => {
|
|
91
|
+
const filePath = join(testDir, 'nonexistent.ts');
|
|
92
|
+
const diff = `@@ -1,1 +1,1 @@
|
|
93
|
+
-old
|
|
94
|
+
+new`;
|
|
95
|
+
|
|
96
|
+
expect(() => applyUnifiedDiff(filePath, diff)).toThrow('File not found');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('throws error for context mismatch', () => {
|
|
100
|
+
const filePath = join(testDir, 'test.ts');
|
|
101
|
+
writeFileSync(filePath, 'actual content\n');
|
|
102
|
+
|
|
103
|
+
const diff = `@@ -1,1 +1,1 @@
|
|
104
|
+
-expected content
|
|
105
|
+
+new content`;
|
|
106
|
+
|
|
107
|
+
expect(() => applyUnifiedDiff(filePath, diff)).toThrow('context mismatch');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('throws error for empty diff', () => {
|
|
111
|
+
const filePath = join(testDir, 'test.ts');
|
|
112
|
+
writeFileSync(filePath, 'content\n');
|
|
113
|
+
|
|
114
|
+
expect(() => applyUnifiedDiff(filePath, '')).toThrow('No valid hunks');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('collectFixableFindings', () => {
|
|
119
|
+
it('returns empty array when no findings have fixes', () => {
|
|
120
|
+
const reports: SkillReport[] = [
|
|
121
|
+
{
|
|
122
|
+
skill: 'test',
|
|
123
|
+
summary: 'Test summary',
|
|
124
|
+
findings: [
|
|
125
|
+
{
|
|
126
|
+
id: '1',
|
|
127
|
+
severity: 'high',
|
|
128
|
+
title: 'Test finding',
|
|
129
|
+
description: 'No fix available',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const result = collectFixableFindings(reports);
|
|
136
|
+
expect(result).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('returns only findings with both diff and location', () => {
|
|
140
|
+
const fixableFinding: Finding = {
|
|
141
|
+
id: '1',
|
|
142
|
+
severity: 'high',
|
|
143
|
+
title: 'Fixable',
|
|
144
|
+
description: 'Has fix',
|
|
145
|
+
location: { path: 'test.ts', startLine: 10 },
|
|
146
|
+
suggestedFix: { description: 'Fix it', diff: '@@ -10,1 +10,1 @@\n-old\n+new' },
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const noLocation: Finding = {
|
|
150
|
+
id: '2',
|
|
151
|
+
severity: 'high',
|
|
152
|
+
title: 'No location',
|
|
153
|
+
description: 'Missing location',
|
|
154
|
+
suggestedFix: { description: 'Fix it', diff: '@@ -1,1 +1,1 @@\n-old\n+new' },
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const noDiff: Finding = {
|
|
158
|
+
id: '3',
|
|
159
|
+
severity: 'high',
|
|
160
|
+
title: 'No diff',
|
|
161
|
+
description: 'Missing diff',
|
|
162
|
+
location: { path: 'test.ts', startLine: 5 },
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const reports: SkillReport[] = [
|
|
166
|
+
{
|
|
167
|
+
skill: 'test',
|
|
168
|
+
summary: 'Test',
|
|
169
|
+
findings: [fixableFinding, noLocation, noDiff],
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const result = collectFixableFindings(reports);
|
|
174
|
+
expect(result).toHaveLength(1);
|
|
175
|
+
expect(result[0]?.id).toBe('1');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('sorts findings by file then by line (descending)', () => {
|
|
179
|
+
const finding1: Finding = {
|
|
180
|
+
id: '1',
|
|
181
|
+
severity: 'high',
|
|
182
|
+
title: 'F1',
|
|
183
|
+
description: 'D1',
|
|
184
|
+
location: { path: 'b.ts', startLine: 10 },
|
|
185
|
+
suggestedFix: { description: 'Fix', diff: '@@ -10,1 +10,1 @@\n-x\n+y' },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const finding2: Finding = {
|
|
189
|
+
id: '2',
|
|
190
|
+
severity: 'high',
|
|
191
|
+
title: 'F2',
|
|
192
|
+
description: 'D2',
|
|
193
|
+
location: { path: 'a.ts', startLine: 20 },
|
|
194
|
+
suggestedFix: { description: 'Fix', diff: '@@ -20,1 +20,1 @@\n-x\n+y' },
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const finding3: Finding = {
|
|
198
|
+
id: '3',
|
|
199
|
+
severity: 'high',
|
|
200
|
+
title: 'F3',
|
|
201
|
+
description: 'D3',
|
|
202
|
+
location: { path: 'a.ts', startLine: 5 },
|
|
203
|
+
suggestedFix: { description: 'Fix', diff: '@@ -5,1 +5,1 @@\n-x\n+y' },
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const reports: SkillReport[] = [
|
|
207
|
+
{ skill: 'test', summary: 'Test', findings: [finding1, finding2, finding3] },
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
const result = collectFixableFindings(reports);
|
|
211
|
+
expect(result).toHaveLength(3);
|
|
212
|
+
// Sorted: a.ts:20, a.ts:5, b.ts:10
|
|
213
|
+
expect(result[0]?.id).toBe('2'); // a.ts:20
|
|
214
|
+
expect(result[1]?.id).toBe('3'); // a.ts:5
|
|
215
|
+
expect(result[2]?.id).toBe('1'); // b.ts:10
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('applyAllFixes', () => {
|
|
220
|
+
let testDir: string;
|
|
221
|
+
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
testDir = join(tmpdir(), `warden-fix-test-${Date.now()}`);
|
|
224
|
+
mkdirSync(testDir, { recursive: true });
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
afterEach(() => {
|
|
228
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('applies all valid fixes', () => {
|
|
232
|
+
const filePath = join(testDir, 'test.ts');
|
|
233
|
+
writeFileSync(filePath, 'line1\nline2\nline3\n');
|
|
234
|
+
|
|
235
|
+
const findings: Finding[] = [
|
|
236
|
+
{
|
|
237
|
+
id: '1',
|
|
238
|
+
severity: 'high',
|
|
239
|
+
title: 'Fix line 2',
|
|
240
|
+
description: 'Change line 2',
|
|
241
|
+
location: { path: filePath, startLine: 2 },
|
|
242
|
+
suggestedFix: { description: 'Fix', diff: '@@ -2,1 +2,1 @@\n-line2\n+LINE2' },
|
|
243
|
+
},
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
const summary = applyAllFixes(findings);
|
|
247
|
+
|
|
248
|
+
expect(summary.applied).toBe(1);
|
|
249
|
+
expect(summary.failed).toBe(0);
|
|
250
|
+
expect(summary.skipped).toBe(0);
|
|
251
|
+
|
|
252
|
+
const result = readFileSync(filePath, 'utf-8');
|
|
253
|
+
expect(result).toBe('line1\nLINE2\nline3\n');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('handles failed fixes and continues', () => {
|
|
257
|
+
const filePath = join(testDir, 'test.ts');
|
|
258
|
+
writeFileSync(filePath, 'actual\n');
|
|
259
|
+
|
|
260
|
+
const findings: Finding[] = [
|
|
261
|
+
{
|
|
262
|
+
id: '1',
|
|
263
|
+
severity: 'high',
|
|
264
|
+
title: 'Bad fix',
|
|
265
|
+
description: 'Wrong context',
|
|
266
|
+
location: { path: filePath, startLine: 1 },
|
|
267
|
+
suggestedFix: { description: 'Fix', diff: '@@ -1,1 +1,1 @@\n-wrong\n+new' },
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const summary = applyAllFixes(findings);
|
|
272
|
+
|
|
273
|
+
expect(summary.applied).toBe(0);
|
|
274
|
+
expect(summary.failed).toBe(1);
|
|
275
|
+
expect(summary.results[0]?.error).toContain('mismatch');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('applies multiple fixes to same file in correct order', () => {
|
|
279
|
+
const filePath = join(testDir, 'test.ts');
|
|
280
|
+
writeFileSync(filePath, 'a\nb\nc\nd\ne\n');
|
|
281
|
+
|
|
282
|
+
// Findings already sorted by line descending
|
|
283
|
+
const findings: Finding[] = [
|
|
284
|
+
{
|
|
285
|
+
id: '1',
|
|
286
|
+
severity: 'high',
|
|
287
|
+
title: 'Fix line 4',
|
|
288
|
+
description: 'Change d',
|
|
289
|
+
location: { path: filePath, startLine: 4 },
|
|
290
|
+
suggestedFix: { description: 'Fix', diff: '@@ -4,1 +4,1 @@\n-d\n+D' },
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: '2',
|
|
294
|
+
severity: 'high',
|
|
295
|
+
title: 'Fix line 2',
|
|
296
|
+
description: 'Change b',
|
|
297
|
+
location: { path: filePath, startLine: 2 },
|
|
298
|
+
suggestedFix: { description: 'Fix', diff: '@@ -2,1 +2,1 @@\n-b\n+B' },
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const summary = applyAllFixes(findings);
|
|
303
|
+
|
|
304
|
+
expect(summary.applied).toBe(2);
|
|
305
|
+
expect(summary.failed).toBe(0);
|
|
306
|
+
|
|
307
|
+
const result = readFileSync(filePath, 'utf-8');
|
|
308
|
+
expect(result).toBe('a\nB\nc\nD\ne\n');
|
|
309
|
+
});
|
|
310
|
+
});
|