ai-xray 1.2.0 → 2.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.
@@ -1,105 +0,0 @@
1
- # Ana 功能建议 — ai-xray 增强方向
2
-
3
- > 来源:Ana 与用户讨论(2026-03-06)
4
- > 状态:待评估,后续归纳到 PRD
5
-
6
- ## 背景
7
- ai-xray 当前定位:**AI Agent 的上下文采集器**(Structured Context Gathering for AI Agents)。
8
- 以下建议基于通用 AI Agent 工作场景中的常见痛点,均为**通用需求**(非专项项目需求)。
9
-
10
- ---
11
-
12
- ## 建议 1: `hotspots` — 文件重要性排序
13
-
14
- **痛点**:Agent 进入陌生 repo 后不知道哪些文件最关键,容易在边缘文件上浪费 token。
15
-
16
- **功能**:
17
- ```
18
- ai-xray hotspots [dir] [--top=N] [--by=imports|commits|both]
19
- ```
20
-
21
- **输出**:按重要性排序的文件列表,指标包括:
22
- - **import 频率**:被其他文件引用最多的文件(核心模块)
23
- - **commit 频率**:最近 N 天改动最频繁的文件(活跃热点)
24
- - **综合评分**:两者加权
25
-
26
- **价值**:Agent 可以优先阅读最重要的文件,避免在不重要文件上浪费上下文。
27
-
28
- ---
29
-
30
- ## 建议 2: `coverage` — 测试覆盖快照
31
-
32
- **痛点**:Agent 修 bug 或加功能时,不知道哪些模块有测试保护、哪些是裸奔的。
33
-
34
- **功能**:
35
- ```
36
- ai-xray coverage [--format=summary|detail]
37
- ```
38
-
39
- **输出**:
40
- - 哪些源文件有对应的测试文件
41
- - 哪些模块完全没有测试(风险区域)
42
- - 测试文件与源文件的映射关系
43
-
44
- **注意**:不是跑真实 coverage(太重),而是通过文件名匹配 + 目录结构推断(轻量级)。
45
-
46
- ---
47
-
48
- ## 建议 3: `pulse` — 项目活跃度脉搏
49
-
50
- **痛点**:Agent(特别是做开源贡献的)需要判断项目是否还活跃、maintainer 是否还在。
51
-
52
- **功能**:
53
- ```
54
- ai-xray pulse [--days=90]
55
- ```
56
-
57
- **输出**:
58
- - 最近 N 天的 commit 趋势(每周/每月频率)
59
- - 最近一次 commit 距今天数
60
- - 活跃贡献者数量
61
- - issue/PR 响应速度(如果能从 git log 推断)
62
- - 一句话判断:`Active` / `Slow` / `Dormant` / `Abandoned`
63
-
64
- **价值**:帮 Agent 快速判断是否值得在这个项目上投入时间。对大荒的谛听角色特别有用。
65
-
66
- ---
67
-
68
- ## 建议 4: `deps` — 依赖关系图
69
-
70
- **痛点**:Agent 要修改某个文件时,不知道会影响多少其他文件。
71
-
72
- **功能**:
73
- ```
74
- ai-xray deps <file> [--depth=N] [--direction=in|out|both]
75
- ```
76
-
77
- **输出**:
78
- - **in**:谁引用了这个文件(影响范围)
79
- - **out**:这个文件引用了谁(依赖链)
80
- - 循环依赖检测
81
-
82
- **价值**:Agent 修改前先查影响范围,避免引入 breaking changes。
83
-
84
- ---
85
-
86
- ## 建议 5: `entrypoints` — 入口点发现
87
-
88
- **痛点**:Agent 需要快速定位项目的入口点(main、CLI entry、route handlers、exports)。
89
-
90
- **功能**:
91
- ```
92
- ai-xray entrypoints [--type=cli|web|lib|all]
93
- ```
94
-
95
- **输出**:
96
- - package.json 的 main/bin/exports
97
- - 框架入口(Next.js pages、Express routes 等)
98
- - CLI 命令注册点
99
-
100
- ---
101
-
102
- ## 后续
103
- - [ ] 与用户评估优先级
104
- - [ ] 选择 1-2 个最有价值的先实现
105
- - [ ] 归纳到正式 PRD
package/tests/cli.test.ts DELETED
@@ -1,172 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
-
3
- // We test the CLI routing logic by extracting the core routing behavior.
4
- // Since cli.ts calls main() on import and uses process.argv / process.exit,
5
- // we test the command routing indirectly through the individual handlers,
6
- // and verify the arg-parsing logic directly.
7
-
8
- // Mock all command handlers
9
- vi.mock('../src/commands/env', () => ({ handleEnvCommand: vi.fn(() => ({ mock: 'env' })) }));
10
- vi.mock('../src/commands/tree', () => ({ handleTreeCommand: vi.fn(() => ({ mock: 'tree' })) }));
11
- vi.mock('../src/commands/read', () => ({ handleReadCommand: vi.fn(() => ({ mock: 'read' })) }));
12
- vi.mock('../src/commands/diff', () => ({ handleDiffCommand: vi.fn(() => ({ mock: 'diff' })) }));
13
- vi.mock('../src/commands/scout', () => ({ handleScoutCommand: vi.fn(() => ({ mock: 'scout' })) }));
14
- vi.mock('../src/commands/init', () => ({ handleInitCommand: vi.fn(() => ({ mock: 'init' })) }));
15
-
16
- // Mock output to prevent process.exit calls
17
- vi.mock('../src/utils/output', () => ({
18
- outputSuccess: vi.fn(),
19
- outputError: vi.fn(),
20
- }));
21
-
22
- import { handleEnvCommand } from '../src/commands/env';
23
- import { handleTreeCommand } from '../src/commands/tree';
24
- import { handleReadCommand } from '../src/commands/read';
25
- import { handleDiffCommand } from '../src/commands/diff';
26
- import { handleScoutCommand } from '../src/commands/scout';
27
- import { handleInitCommand } from '../src/commands/init';
28
- import { outputSuccess, outputError } from '../src/utils/output';
29
-
30
- /**
31
- * Re-implements the CLI arg parsing and routing logic from src/cli.ts
32
- * to test it in isolation without triggering process.exit or auto-execution.
33
- */
34
- function routeCommand(args: string[]): { command: string; result?: any; error?: string; isHelp?: boolean } {
35
- if (args.length === 0 || args[0] === 'help' || args.includes('--help') || args.includes('-h')) {
36
- return { command: 'help', isHelp: true };
37
- }
38
-
39
- const command = args[0];
40
- const originalParams = args.slice(1);
41
-
42
- let budget: number | undefined;
43
- const commandParams: string[] = [];
44
-
45
- for (const p of originalParams) {
46
- if (p.startsWith('--budget=')) {
47
- budget = parseInt(p.split('=')[1], 10);
48
- if (isNaN(budget)) {
49
- return { command, error: 'Invalid --budget value. Must be a number.' };
50
- }
51
- } else if (p === '--pretty') {
52
- // handled globally
53
- } else {
54
- commandParams.push(p);
55
- }
56
- }
57
-
58
- let result: any;
59
- switch (command) {
60
- case 'scout':
61
- result = (handleScoutCommand as any)(budget);
62
- break;
63
- case 'env':
64
- result = (handleEnvCommand as any)();
65
- break;
66
- case 'tree':
67
- result = (handleTreeCommand as any)(commandParams, budget);
68
- break;
69
- case 'read':
70
- result = (handleReadCommand as any)(commandParams, budget);
71
- break;
72
- case 'diff':
73
- result = (handleDiffCommand as any)(commandParams, budget);
74
- break;
75
- case 'init':
76
- result = (handleInitCommand as any)(commandParams);
77
- break;
78
- default:
79
- return { command, error: `Unknown command: ${command}` };
80
- }
81
-
82
- return { command, result };
83
- }
84
-
85
- describe('CLI Routing', () => {
86
- beforeEach(() => {
87
- vi.clearAllMocks();
88
- });
89
-
90
- it('should show help when no arguments provided', () => {
91
- const res = routeCommand([]);
92
- expect(res.isHelp).toBe(true);
93
- expect(res.command).toBe('help');
94
- });
95
-
96
- it('should show help when "help" is the first argument (args[0] === "help" fix)', () => {
97
- const res = routeCommand(['help']);
98
- expect(res.isHelp).toBe(true);
99
- });
100
-
101
- it('should show help when --help flag is present anywhere', () => {
102
- const res = routeCommand(['env', '--help']);
103
- expect(res.isHelp).toBe(true);
104
- });
105
-
106
- it('should show help when -h flag is present', () => {
107
- const res = routeCommand(['-h']);
108
- expect(res.isHelp).toBe(true);
109
- });
110
-
111
- it('should route to env command', () => {
112
- const res = routeCommand(['env']);
113
- expect(res.command).toBe('env');
114
- expect(handleEnvCommand).toHaveBeenCalled();
115
- });
116
-
117
- it('should route to tree command with params', () => {
118
- routeCommand(['tree', 'src/', '--depth=5']);
119
- expect(handleTreeCommand).toHaveBeenCalledWith(['src/', '--depth=5'], undefined);
120
- });
121
-
122
- it('should route to read command', () => {
123
- routeCommand(['read', 'file.ts']);
124
- expect(handleReadCommand).toHaveBeenCalledWith(['file.ts'], undefined);
125
- });
126
-
127
- it('should route to diff command', () => {
128
- routeCommand(['diff', '--full']);
129
- expect(handleDiffCommand).toHaveBeenCalledWith(['--full'], undefined);
130
- });
131
-
132
- it('should route to scout command', () => {
133
- routeCommand(['scout']);
134
- expect(handleScoutCommand).toHaveBeenCalledWith(undefined);
135
- });
136
-
137
- it('should route to init command', () => {
138
- routeCommand(['init', '--format=claude']);
139
- expect(handleInitCommand).toHaveBeenCalledWith(['--format=claude']);
140
- });
141
-
142
- it('should return error for unknown commands', () => {
143
- const res = routeCommand(['foobar']);
144
- expect(res.error).toBe('Unknown command: foobar');
145
- });
146
-
147
- it('should extract --budget flag and pass as number', () => {
148
- routeCommand(['scout', '--budget=500']);
149
- expect(handleScoutCommand).toHaveBeenCalledWith(500);
150
- });
151
-
152
- it('should pass budget to tree command', () => {
153
- routeCommand(['tree', 'src/', '--budget=1000', '--depth=2']);
154
- expect(handleTreeCommand).toHaveBeenCalledWith(['src/', '--depth=2'], 1000);
155
- });
156
-
157
- it('should error on invalid budget value (NaN)', () => {
158
- const res = routeCommand(['env', '--budget=abc']);
159
- expect(res.error).toContain('Invalid --budget value');
160
- });
161
-
162
- it('should strip --pretty from command params', () => {
163
- routeCommand(['tree', 'src/', '--pretty']);
164
- // --pretty should not be passed to the command handler
165
- expect(handleTreeCommand).toHaveBeenCalledWith(['src/'], undefined);
166
- });
167
-
168
- it('should handle multiple flags together', () => {
169
- routeCommand(['read', 'file.ts', '--lines=1-10', '--budget=200', '--pretty']);
170
- expect(handleReadCommand).toHaveBeenCalledWith(['file.ts', '--lines=1-10'], 200);
171
- });
172
- });
@@ -1,169 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { handleDiffCommand } from '../src/commands/diff';
3
- import * as child_process from 'child_process';
4
- import * as fs from 'fs';
5
-
6
- vi.mock('fs', async () => {
7
- const actual = await vi.importActual<typeof import('fs')>('fs');
8
- return { ...actual, existsSync: vi.fn() };
9
- });
10
-
11
- vi.mock('child_process', () => ({
12
- execFileSync: vi.fn()
13
- }));
14
-
15
- const mockExistsSync = fs.existsSync as any;
16
- const mockExecFileSync = child_process.execFileSync as any;
17
-
18
- describe('Diff Command', () => {
19
- beforeEach(() => {
20
- vi.clearAllMocks();
21
- });
22
-
23
- it('should throw when not a git repository', () => {
24
- mockExistsSync.mockReturnValue(false);
25
- expect(() => handleDiffCommand([])).toThrow('Not a git repository');
26
- });
27
-
28
- it('should return branch, ahead/behind counts, staged/unstaged/untracked', () => {
29
- mockExistsSync.mockImplementation((p: string) => p.endsWith('.git'));
30
- mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
31
- if (args.includes('--abbrev-ref')) return 'feature-x\n';
32
- if (args.includes('--left-right')) return '3\t1\n';
33
- if (args.includes('--porcelain')) return 'M staged.ts\n M unstaged.ts\n?? new.ts\n';
34
- if (args.includes('--numstat') && args.includes('--cached')) return '10\t2\tstaged.ts\n';
35
- if (args.includes('--numstat')) return '5\t3\tunstaged.ts\n';
36
- return '';
37
- });
38
-
39
- const result = handleDiffCommand([]);
40
- expect(result.branch).toBe('feature-x');
41
- expect(result.ahead).toBe(3);
42
- expect(result.behind).toBe(1);
43
- expect(result.staged).toEqual([{ file: 'staged.ts', status: 'M', insertions: 10, deletions: 2 }]);
44
- expect(result.unstaged).toEqual([{ file: 'unstaged.ts', status: 'M', insertions: 5, deletions: 3 }]);
45
- expect(result.untracked).toEqual(['new.ts']);
46
- });
47
-
48
- it('should handle --full flag and attach raw diff output', () => {
49
- mockExistsSync.mockImplementation((p: string) => p.endsWith('.git'));
50
- mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
51
- if (args.includes('--abbrev-ref')) return 'main\n';
52
- if (args.includes('--left-right')) return null;
53
- if (args.includes('--porcelain')) return '';
54
- if (args[0] === 'diff' && args.includes('--cached')) return 'staged diff content\n';
55
- if (args[0] === 'diff' && !args.includes('--cached') && !args.includes('--numstat')) return 'unstaged diff content\n';
56
- return '';
57
- });
58
-
59
- const result = handleDiffCommand(['--full']);
60
- expect(result.stagedDiff).toBe('staged diff content');
61
- expect(result.unstagedDiff).toBe('unstaged diff content');
62
- });
63
-
64
- it('should filter by --file flag', () => {
65
- mockExistsSync.mockImplementation((p: string) => p.endsWith('.git'));
66
- mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
67
- if (args.includes('--abbrev-ref')) return 'main\n';
68
- if (args.includes('--left-right')) return '';
69
- if (args.includes('--porcelain')) return 'M target.ts\nM other.ts\n';
70
- return '';
71
- });
72
-
73
- const result = handleDiffCommand(['--file=target.ts']);
74
- expect(result.staged.length).toBe(1);
75
- expect(result.staged[0].file).toBe('target.ts');
76
- // other.ts should be filtered out
77
- expect(result.unstaged.length).toBe(0);
78
- });
79
-
80
- it('should use execFileSync (not execSync) to prevent shell injection', () => {
81
- // Verify the module uses execFileSync by checking the import
82
- // execFileSync takes command and args array separately — no shell interpretation
83
- mockExistsSync.mockImplementation((p: string) => p.endsWith('.git'));
84
- mockExecFileSync.mockImplementation(() => '');
85
-
86
- handleDiffCommand([]);
87
-
88
- // All calls should be to execFileSync (our mock), not execSync
89
- expect(mockExecFileSync).toHaveBeenCalled();
90
- // Verify args are passed as arrays (safe), not concatenated strings
91
- for (const call of mockExecFileSync.mock.calls) {
92
- expect(call[0]).toBe('git');
93
- expect(Array.isArray(call[1])).toBe(true);
94
- }
95
- });
96
-
97
- it('should handle no tracking info gracefully (ahead/behind = 0)', () => {
98
- mockExistsSync.mockImplementation((p: string) => p.endsWith('.git'));
99
- mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
100
- if (args.includes('--abbrev-ref')) return 'main\n';
101
- if (args.includes('--left-right')) return null; // no upstream
102
- if (args.includes('--porcelain')) return '';
103
- return '';
104
- });
105
-
106
- const result = handleDiffCommand([]);
107
- expect(result.ahead).toBe(0);
108
- expect(result.behind).toBe(0);
109
- });
110
-
111
- it('should handle empty git status (clean working directory)', () => {
112
- mockExistsSync.mockImplementation((p: string) => p.endsWith('.git'));
113
- mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
114
- if (args.includes('--abbrev-ref')) return 'main\n';
115
- if (args.includes('--left-right')) return '0\t0\n';
116
- if (args.includes('--porcelain')) return '';
117
- return null;
118
- });
119
-
120
- const result = handleDiffCommand([]);
121
- expect(result.staged).toEqual([]);
122
- expect(result.unstaged).toEqual([]);
123
- expect(result.untracked).toEqual([]);
124
- });
125
-
126
- it('should not attach stagedDiff/unstagedDiff without --full flag', () => {
127
- mockExistsSync.mockImplementation((p: string) => p.endsWith('.git'));
128
- mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
129
- if (args.includes('--abbrev-ref')) return 'main\n';
130
- if (args.includes('--left-right')) return '';
131
- if (args.includes('--porcelain')) return '';
132
- return '';
133
- });
134
-
135
- const result = handleDiffCommand([]);
136
- expect(result.stagedDiff).toBeUndefined();
137
- expect(result.unstagedDiff).toBeUndefined();
138
- });
139
-
140
- it('should handle --full with --file to scope diff', () => {
141
- mockExistsSync.mockImplementation((p: string) => p.endsWith('.git'));
142
- mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
143
- if (args.includes('--abbrev-ref')) return 'main\n';
144
- if (args.includes('--left-right')) return '';
145
- if (args.includes('--porcelain')) return '';
146
- // When --full + --file=target.ts, diff args should include the file
147
- if (args[0] === 'diff' && args.includes('target.ts')) return 'targeted diff\n';
148
- if (args[0] === 'diff') return '';
149
- return '';
150
- });
151
-
152
- const result = handleDiffCommand(['--full', '--file=target.ts']);
153
- // The --file flag should cause diff to be scoped to target.ts
154
- expect(result).toBeDefined();
155
- });
156
-
157
- it('should handle branch detection failure', () => {
158
- mockExistsSync.mockImplementation((p: string) => p.endsWith('.git'));
159
- mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
160
- if (args.includes('--abbrev-ref')) { throw new Error('git failed'); }
161
- if (args.includes('--left-right')) return null;
162
- if (args.includes('--porcelain')) return '';
163
- return null;
164
- });
165
-
166
- const result = handleDiffCommand([]);
167
- expect(result.branch).toBe('unknown');
168
- });
169
- });
package/tests/env.test.ts DELETED
@@ -1,69 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { handleEnvCommand } from '../src/commands/env';
3
- import * as child_process from 'child_process';
4
- import * as fs from 'fs';
5
-
6
- // We mock readFileSync and existsSync heavily for this test
7
- vi.mock('fs', async () => {
8
- const actualFs = await vi.importActual<typeof import('fs')>('fs');
9
- return {
10
- ...actualFs,
11
- existsSync: vi.fn(),
12
- readFileSync: vi.fn()
13
- };
14
- });
15
-
16
- vi.mock('child_process', () => ({
17
- execFileSync: vi.fn()
18
- }));
19
-
20
- const mockExistsSync = fs.existsSync as any;
21
- const mockReadFileSync = fs.readFileSync as any;
22
- const mockExecFileSync = child_process.execFileSync as any;
23
-
24
- describe('Env Command', () => {
25
- it('should detect pnpm and frameworks', () => {
26
- // Setup virtual filesystem
27
- mockExistsSync.mockImplementation((path: string) => {
28
- if (path.includes('package.json')) return true;
29
- if (path.includes('pnpm-lock.yaml')) return true;
30
- if (path.includes('.git')) return true;
31
- return false;
32
- });
33
-
34
- mockReadFileSync.mockImplementation((path: string) => {
35
- if (path.includes('package.json')) {
36
- return JSON.stringify({
37
- name: 'test-app',
38
- engines: { node: '>=18' },
39
- dependencies: { next: '14.0.0', react: '18.0.0' },
40
- devDependencies: { vitest: '1.0.0' }
41
- });
42
- }
43
- return '';
44
- });
45
-
46
- mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
47
- if (cmd === 'pnpm' && args.includes('--version')) return '9.1.0\n';
48
- if (cmd === 'git' && args.includes('rev-parse')) return 'main\n';
49
- if (cmd === 'git' && args.includes('--porcelain')) return ' M file.ts\n';
50
- return '';
51
- });
52
-
53
- const result = handleEnvCommand();
54
-
55
- // Assertions
56
- expect(result.packageManager.name).toBe('pnpm');
57
- expect(result.packageManager.version).toBe('9.1.0');
58
- expect(result.frameworks).toContain('next');
59
- expect(result.frameworks).toContain('react');
60
-
61
- // Test framework inference
62
- expect(result.testFramework?.name).toBe('vitest');
63
- expect(result.testFramework?.runCommand).toContain('vitest run');
64
-
65
- // Git detection
66
- expect(result.git.branch).toBe('main');
67
- expect(result.git.hasUncommitted).toBe(true);
68
- });
69
- });
@@ -1,164 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { handleInitCommand } from '../src/commands/init';
3
- import * as fs from 'fs';
4
-
5
- vi.mock('fs', async () => {
6
- const actual = await vi.importActual<typeof import('fs')>('fs');
7
- return {
8
- ...actual,
9
- existsSync: vi.fn(),
10
- readFileSync: vi.fn(),
11
- writeFileSync: vi.fn()
12
- };
13
- });
14
-
15
- const mockExistsSync = fs.existsSync as any;
16
- const mockReadFileSync = fs.readFileSync as any;
17
- const mockWriteFileSync = fs.writeFileSync as any;
18
-
19
- describe('Init Command', () => {
20
- beforeEach(() => {
21
- vi.clearAllMocks();
22
- });
23
-
24
- it('should create AGENTS.md by default when no rule files exist', () => {
25
- mockExistsSync.mockReturnValue(false);
26
-
27
- const result = handleInitCommand([]);
28
- expect(result.action).toBe('created');
29
- expect(result.target).toBe('AGENTS.md');
30
- expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
31
- // Check content includes the template
32
- const writtenContent = mockWriteFileSync.mock.calls[0][1];
33
- expect(writtenContent).toContain('ai-xray');
34
- expect(writtenContent).toContain('# AI Agent Rules');
35
- });
36
-
37
- it('should inject into existing file that does not have marker', () => {
38
- mockExistsSync.mockImplementation((p: string) => {
39
- const norm = p.replace(/\\/g, '/');
40
- if (norm.endsWith('AGENTS.md')) return true;
41
- return false;
42
- });
43
- mockReadFileSync.mockReturnValue('# Existing Rules\n\nSome rules here.');
44
-
45
- const result = handleInitCommand([]);
46
- expect(result.action).toBe('injected');
47
- expect(result.target).toBe('AGENTS.md');
48
-
49
- const writtenContent = mockWriteFileSync.mock.calls[0][1];
50
- expect(writtenContent).toContain('# Existing Rules');
51
- expect(writtenContent).toContain('ai-xray:start');
52
- });
53
-
54
- it('should skip injection if marker already exists', () => {
55
- mockExistsSync.mockImplementation((p: string) => {
56
- const norm = p.replace(/\\/g, '/');
57
- if (norm.endsWith('AGENTS.md')) return true;
58
- return false;
59
- });
60
- mockReadFileSync.mockReturnValue('# Rules\n<!-- ai-xray:start -->\nstuff\n<!-- ai-xray:end -->');
61
-
62
- const result = handleInitCommand([]);
63
- expect(result.action).toBe('skipped');
64
- expect(result.reason).toBe('Already injected');
65
- expect(mockWriteFileSync).not.toHaveBeenCalled();
66
- });
67
-
68
- it('should use --format=claude to target CLAUDE.md', () => {
69
- mockExistsSync.mockReturnValue(false);
70
-
71
- const result = handleInitCommand(['--format=claude']);
72
- expect(result.target).toBe('CLAUDE.md');
73
- expect(result.action).toBe('created');
74
- });
75
-
76
- it('should use --format=cursorrules to target .cursorrules', () => {
77
- mockExistsSync.mockReturnValue(false);
78
-
79
- const result = handleInitCommand(['--format=cursorrules']);
80
- expect(result.target).toBe('.cursorrules');
81
- expect(result.action).toBe('created');
82
- });
83
-
84
- it('should use --format=agents to target AGENTS.md', () => {
85
- mockExistsSync.mockReturnValue(false);
86
-
87
- const result = handleInitCommand(['--format=agents']);
88
- expect(result.target).toBe('AGENTS.md');
89
- expect(result.action).toBe('created');
90
- });
91
-
92
- it('should throw on unknown format', () => {
93
- expect(() => handleInitCommand(['--format=unknown'])).toThrow('Unknown format: unknown');
94
- });
95
-
96
- it('should auto-detect existing CLAUDE.md and inject there', () => {
97
- mockExistsSync.mockImplementation((p: string) => {
98
- const norm = p.replace(/\\/g, '/');
99
- // AGENTS.md doesn't exist, but CLAUDE.md does
100
- if (norm.endsWith('AGENTS.md')) return false;
101
- if (norm.endsWith('CLAUDE.md')) return true;
102
- return false;
103
- });
104
- mockReadFileSync.mockReturnValue('# Claude Rules\n\nSome content.');
105
-
106
- const result = handleInitCommand([]);
107
- expect(result.target).toBe('CLAUDE.md');
108
- expect(result.action).toBe('injected');
109
- });
110
-
111
- it('should auto-detect .cursorrules before CLAUDE.md', () => {
112
- // The search order is: AGENTS.md, CLAUDE.md, .cursorrules, copilot-instructions.md
113
- // But since AGENTS.md is checked first, if it exists it takes priority
114
- mockExistsSync.mockImplementation((p: string) => {
115
- const norm = p.replace(/\\/g, '/');
116
- if (norm.endsWith('AGENTS.md')) return true;
117
- if (norm.endsWith('CLAUDE.md')) return true;
118
- if (norm.endsWith('.cursorrules')) return true;
119
- return false;
120
- });
121
- mockReadFileSync.mockReturnValue('# Agent rules');
122
-
123
- const result = handleInitCommand([]);
124
- // AGENTS.md is first in the search list
125
- expect(result.target).toBe('AGENTS.md');
126
- });
127
-
128
- it('should include linesAdded count in result', () => {
129
- mockExistsSync.mockReturnValue(false);
130
-
131
- const result = handleInitCommand([]);
132
- expect(result.linesAdded).toBeGreaterThan(0);
133
- });
134
-
135
- it('should include marker info in result', () => {
136
- mockExistsSync.mockReturnValue(false);
137
-
138
- const result = handleInitCommand([]);
139
- expect(result.marker).toBe('ai-xray:start');
140
- });
141
-
142
- it('should write UTF-8 encoded files', () => {
143
- mockExistsSync.mockReturnValue(false);
144
-
145
- handleInitCommand([]);
146
- // Check that writeFileSync was called with utf-8 encoding
147
- expect(mockWriteFileSync).toHaveBeenCalledWith(
148
- expect.any(String),
149
- expect.any(String),
150
- 'utf-8'
151
- );
152
- });
153
-
154
- it('should inject template with correct content markers', () => {
155
- mockExistsSync.mockReturnValue(false);
156
-
157
- handleInitCommand([]);
158
- const content = mockWriteFileSync.mock.calls[0][1];
159
- expect(content).toContain('<!-- ai-xray:start -->');
160
- expect(content).toContain('<!-- ai-xray:end -->');
161
- expect(content).toContain('npx ai-xray scout --budget=2000');
162
- expect(content).toContain('npx ai-xray diff --full');
163
- });
164
- });