aiseerr 1.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.
@@ -0,0 +1,248 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { handleScoutCommand } from '../src/commands/scout';
3
+ import * as fs from 'fs';
4
+ import * as child_process from 'child_process';
5
+
6
+ vi.mock('fs', async () => {
7
+ const actual = await vi.importActual<typeof import('fs')>('fs');
8
+ return {
9
+ ...actual,
10
+ existsSync: vi.fn(),
11
+ readFileSync: vi.fn(),
12
+ readdirSync: vi.fn(),
13
+ statSync: vi.fn()
14
+ };
15
+ });
16
+
17
+ vi.mock('child_process', () => ({
18
+ execFileSync: vi.fn()
19
+ }));
20
+
21
+ const mockExistsSync = fs.existsSync as any;
22
+ const mockReadFileSync = fs.readFileSync as any;
23
+ const mockReaddirSync = fs.readdirSync as any;
24
+ const mockStatSync = fs.statSync as any;
25
+ const mockExecFileSync = child_process.execFileSync as any;
26
+
27
+ function setupBasicProject() {
28
+ mockExistsSync.mockImplementation((p: string) => {
29
+ const norm = p.replace(/\\/g, '/');
30
+ if (norm.endsWith('package.json')) return true;
31
+ if (norm.endsWith('.git')) return true;
32
+ if (norm.endsWith('README.md')) return true;
33
+ if (norm.endsWith('.gitignore')) return false;
34
+ if (norm.endsWith('.cursorrules')) return false;
35
+ if (norm.endsWith('CLAUDE.md')) return false;
36
+ if (norm.endsWith('AGENTS.md')) return false;
37
+ if (norm.endsWith('copilot-instructions.md')) return false;
38
+ if (norm.endsWith('Readme.md')) return false;
39
+ if (norm.endsWith('readme.md')) return false;
40
+ return true;
41
+ });
42
+
43
+ mockReadFileSync.mockImplementation((p: string) => {
44
+ const norm = p.replace(/\\/g, '/');
45
+ if (norm.endsWith('package.json')) {
46
+ return JSON.stringify({
47
+ name: 'test-project',
48
+ dependencies: { react: '18.0.0' },
49
+ devDependencies: { vitest: '1.0.0' }
50
+ });
51
+ }
52
+ if (norm.endsWith('README.md')) return '# Test Project\n\nA test project.';
53
+ return '';
54
+ });
55
+
56
+ mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
57
+ if (args.includes('--version')) return '9.0.0\n';
58
+ if (args.includes('--abbrev-ref')) return 'main\n';
59
+ if (args.includes('--porcelain')) return '';
60
+ if (args.includes('remote.origin.url')) return 'https://github.com/test/test.git\n';
61
+ if (args.includes('--left-right')) return '0\t0\n';
62
+ return '';
63
+ });
64
+
65
+ mockReaddirSync.mockReturnValue(['src', 'package.json', 'README.md']);
66
+ mockStatSync.mockImplementation((p: string, opts?: any) => {
67
+ const norm = p.replace(/\\/g, '/');
68
+ if (norm.endsWith('src')) return { isDirectory: () => true };
69
+ return { isDirectory: () => false };
70
+ });
71
+ }
72
+
73
+ describe('Scout Command', () => {
74
+ beforeEach(() => {
75
+ vi.clearAllMocks();
76
+ });
77
+
78
+ it('should return a combined result with env, tree, readme, diff sections', () => {
79
+ setupBasicProject();
80
+
81
+ const result = handleScoutCommand();
82
+ expect(result.env).toBeDefined();
83
+ expect(result.tree).toBeDefined();
84
+ expect(result.readme).toBeDefined();
85
+ expect(result.diff).toBeDefined();
86
+ });
87
+
88
+ it('should include readme content (truncated to 60 lines)', () => {
89
+ setupBasicProject();
90
+
91
+ const result = handleScoutCommand();
92
+ expect(result.readme).toContain('# Test Project');
93
+ });
94
+
95
+ it('should truncate long readmes to 60 lines', () => {
96
+ setupBasicProject();
97
+ // Override README to be very long
98
+ const longReadme = Array.from({ length: 100 }, (_, i) => `Line ${i + 1}`).join('\n');
99
+ mockReadFileSync.mockImplementation((p: string) => {
100
+ const norm = p.replace(/\\/g, '/');
101
+ if (norm.endsWith('package.json')) {
102
+ return JSON.stringify({ name: 'test', dependencies: {} });
103
+ }
104
+ if (norm.endsWith('README.md')) return longReadme;
105
+ return '';
106
+ });
107
+
108
+ const result = handleScoutCommand();
109
+ expect(result.readme).toContain('[TRUNCATED TO 60 LINES]');
110
+ });
111
+
112
+ it('should handle missing env gracefully (error in env)', () => {
113
+ // No package.json, no git
114
+ mockExistsSync.mockReturnValue(false);
115
+ mockReadFileSync.mockReturnValue('');
116
+ mockReaddirSync.mockReturnValue([]);
117
+ mockStatSync.mockReturnValue({ isDirectory: () => false });
118
+
119
+ const result = handleScoutCommand();
120
+ // env should still be present (returns a "not node project" result)
121
+ expect(result.env).toBeDefined();
122
+ });
123
+
124
+ it('should handle missing readme', () => {
125
+ mockExistsSync.mockImplementation((p: string) => {
126
+ const norm = p.replace(/\\/g, '/');
127
+ if (norm.endsWith('README.md') || norm.endsWith('Readme.md') || norm.endsWith('readme.md')) return false;
128
+ if (norm.endsWith('package.json')) return true;
129
+ if (norm.endsWith('.git')) return false;
130
+ if (norm.endsWith('.gitignore')) return false;
131
+ if (norm.endsWith('.cursorrules')) return false;
132
+ if (norm.endsWith('CLAUDE.md')) return false;
133
+ if (norm.endsWith('AGENTS.md')) return false;
134
+ if (norm.endsWith('copilot-instructions.md')) return false;
135
+ return true;
136
+ });
137
+ mockReadFileSync.mockImplementation((p: string) => {
138
+ if (p.includes('package.json')) return JSON.stringify({ name: 'test' });
139
+ return '';
140
+ });
141
+ mockReaddirSync.mockReturnValue([]);
142
+ mockStatSync.mockReturnValue({ isDirectory: () => false });
143
+
144
+ const result = handleScoutCommand();
145
+ expect(result.readme).toBeNull();
146
+ });
147
+
148
+ it('should set diff to null when not a git repo', () => {
149
+ mockExistsSync.mockImplementation((p: string) => {
150
+ const norm = p.replace(/\\/g, '/');
151
+ if (norm.endsWith('.git')) return false;
152
+ if (norm.endsWith('package.json')) return true;
153
+ if (norm.endsWith('.gitignore')) return false;
154
+ if (norm.endsWith('.cursorrules')) return false;
155
+ if (norm.endsWith('CLAUDE.md')) return false;
156
+ if (norm.endsWith('AGENTS.md')) return false;
157
+ if (norm.endsWith('copilot-instructions.md')) return false;
158
+ if (norm.endsWith('README.md') || norm.endsWith('Readme.md') || norm.endsWith('readme.md')) return false;
159
+ return true;
160
+ });
161
+ mockReadFileSync.mockImplementation((p: string) => {
162
+ if (p.includes('package.json')) return JSON.stringify({ name: 'test' });
163
+ return '';
164
+ });
165
+ mockReaddirSync.mockReturnValue([]);
166
+ mockStatSync.mockReturnValue({ isDirectory: () => false });
167
+
168
+ const result = handleScoutCommand();
169
+ expect(result.diff).toBeNull();
170
+ });
171
+
172
+ it('should strip full diffs from scout diff output (stagedDiff/unstagedDiff removed)', () => {
173
+ setupBasicProject();
174
+ // Even if handleDiffCommand returned these, scout should delete them
175
+ const result = handleScoutCommand();
176
+ if (result.diff) {
177
+ expect(result.diff.stagedDiff).toBeUndefined();
178
+ expect(result.diff.unstagedDiff).toBeUndefined();
179
+ }
180
+ });
181
+
182
+ it('should set agentRules to null when no rule files exist', () => {
183
+ mockExistsSync.mockImplementation((p: string) => {
184
+ const norm = p.replace(/\\/g, '/');
185
+ if (norm.endsWith('.cursorrules')) return false;
186
+ if (norm.endsWith('CLAUDE.md')) return false;
187
+ if (norm.endsWith('AGENTS.md')) return false;
188
+ if (norm.endsWith('copilot-instructions.md')) return false;
189
+ if (norm.endsWith('README.md') || norm.endsWith('Readme.md') || norm.endsWith('readme.md')) return false;
190
+ if (norm.endsWith('package.json')) return true;
191
+ if (norm.endsWith('.git')) return false;
192
+ if (norm.endsWith('.gitignore')) return false;
193
+ return true;
194
+ });
195
+ mockReadFileSync.mockImplementation((p: string) => {
196
+ if (p.includes('package.json')) return JSON.stringify({ name: 'test' });
197
+ return '';
198
+ });
199
+ mockReaddirSync.mockReturnValue([]);
200
+ mockStatSync.mockReturnValue({ isDirectory: () => false });
201
+
202
+ const result = handleScoutCommand();
203
+ expect(result.agentRules).toBeNull();
204
+ });
205
+
206
+ it('should detect agent rules from .cursorrules', () => {
207
+ setupBasicProject();
208
+ mockExistsSync.mockImplementation((p: string) => {
209
+ const norm = p.replace(/\\/g, '/');
210
+ if (norm.endsWith('.cursorrules')) return true;
211
+ if (norm.endsWith('package.json')) return true;
212
+ if (norm.endsWith('.git')) return true;
213
+ if (norm.endsWith('README.md')) return true;
214
+ if (norm.endsWith('.gitignore')) return false;
215
+ if (norm.endsWith('CLAUDE.md')) return false;
216
+ if (norm.endsWith('AGENTS.md')) return false;
217
+ if (norm.endsWith('copilot-instructions.md')) return false;
218
+ if (norm.endsWith('Readme.md') || norm.endsWith('readme.md')) return false;
219
+ return true;
220
+ });
221
+ mockReadFileSync.mockImplementation((p: string) => {
222
+ const norm = p.replace(/\\/g, '/');
223
+ if (norm.endsWith('.cursorrules')) return 'Use TypeScript strict mode';
224
+ if (norm.endsWith('package.json')) return JSON.stringify({ name: 'test', dependencies: {} });
225
+ if (norm.endsWith('README.md')) return '# Test';
226
+ return '';
227
+ });
228
+
229
+ const result = handleScoutCommand();
230
+ expect(result.agentRules).toBeDefined();
231
+ expect(result.agentRules.source).toBe('.cursorrules');
232
+ expect(result.agentRules.content).toContain('TypeScript strict mode');
233
+ });
234
+
235
+ it('should pass budget allocation to tree (approx 50%)', () => {
236
+ setupBasicProject();
237
+ // We verify that tree gets called with a budget argument
238
+ // by checking the result completes without error with a budget
239
+ const result = handleScoutCommand(1000);
240
+ expect(result.tree).toBeDefined();
241
+ });
242
+
243
+ it('should set workspaces to null when not present', () => {
244
+ setupBasicProject();
245
+ const result = handleScoutCommand();
246
+ expect(result.workspaces).toBeNull();
247
+ });
248
+ });
@@ -0,0 +1,222 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { handleTreeCommand } from '../src/commands/tree';
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
+ readdirSync: vi.fn(),
12
+ statSync: vi.fn()
13
+ };
14
+ });
15
+
16
+ const mockExistsSync = fs.existsSync as any;
17
+ const mockReadFileSync = fs.readFileSync as any;
18
+ const mockReaddirSync = fs.readdirSync as any;
19
+ const mockStatSync = fs.statSync as any;
20
+
21
+ // Helper to build a virtual filesystem
22
+ function setupVirtualFS(tree: Record<string, 'dir' | 'file' | null>) {
23
+ mockExistsSync.mockImplementation((p: string) => {
24
+ // Normalize path separators for matching
25
+ const normalized = p.replace(/\\/g, '/');
26
+ // Check .gitignore existence
27
+ if (normalized.endsWith('.gitignore')) {
28
+ return tree['.gitignore'] !== undefined;
29
+ }
30
+ return true; // default: exists
31
+ });
32
+
33
+ mockReaddirSync.mockImplementation((dirPath: string) => {
34
+ const normalized = dirPath.replace(/\\/g, '/');
35
+ const entries: string[] = [];
36
+ for (const [key, type] of Object.entries(tree)) {
37
+ // Get entries that are direct children of dirPath
38
+ const parent = key.substring(0, key.lastIndexOf('/'));
39
+ const name = key.substring(key.lastIndexOf('/') + 1);
40
+ if (!key.includes('/') && type !== null) {
41
+ // Root-level entries
42
+ entries.push(key);
43
+ }
44
+ }
45
+ return entries;
46
+ });
47
+
48
+ mockStatSync.mockImplementation((p: string, opts?: any) => {
49
+ const normalized = p.replace(/\\/g, '/');
50
+ const name = normalized.substring(normalized.lastIndexOf('/') + 1);
51
+ const entry = tree[name];
52
+ if (entry === 'dir') return { isDirectory: () => true };
53
+ if (entry === 'file') return { isDirectory: () => false };
54
+ // Default: file
55
+ return { isDirectory: () => false };
56
+ });
57
+ }
58
+
59
+ describe('Tree Command', () => {
60
+ beforeEach(() => {
61
+ vi.clearAllMocks();
62
+ // Default: no .gitignore
63
+ mockReadFileSync.mockReturnValue('');
64
+ });
65
+
66
+ it('should throw when target directory does not exist', () => {
67
+ mockExistsSync.mockReturnValue(false);
68
+ expect(() => handleTreeCommand(['nonexistent'])).toThrow('Directory not found: nonexistent');
69
+ });
70
+
71
+ it('should return tree structure with files and directories', () => {
72
+ mockExistsSync.mockReturnValue(true);
73
+ mockReadFileSync.mockReturnValue(''); // empty .gitignore
74
+
75
+ // Root has: src/ (dir), README.md (file), package.json (file)
76
+ mockReaddirSync.mockImplementation((dirPath: string) => {
77
+ const normalized = dirPath.replace(/\\/g, '/');
78
+ if (normalized.endsWith('src')) return ['index.ts', 'utils.ts'];
79
+ return ['src', 'README.md', 'package.json'];
80
+ });
81
+
82
+ mockStatSync.mockImplementation((p: string, opts?: any) => {
83
+ const normalized = p.replace(/\\/g, '/');
84
+ if (normalized.endsWith('src')) return { isDirectory: () => true };
85
+ return { isDirectory: () => false };
86
+ });
87
+
88
+ const result = handleTreeCommand([]);
89
+ expect(result.root).toBe('.');
90
+ expect(result.depth).toBe(3);
91
+ expect(result.structure).toBeDefined();
92
+ expect(result.stats.totalFiles).toBeGreaterThan(0);
93
+ });
94
+
95
+ it('should respect --depth flag', () => {
96
+ mockExistsSync.mockReturnValue(true);
97
+ mockReadFileSync.mockReturnValue('');
98
+
99
+ mockReaddirSync.mockImplementation((dirPath: string) => {
100
+ const norm = dirPath.replace(/\\/g, '/');
101
+ if (norm.endsWith('deep')) return ['deeper'];
102
+ if (norm.endsWith('deeper')) return ['deepest.txt'];
103
+ return ['deep'];
104
+ });
105
+
106
+ mockStatSync.mockImplementation((p: string) => {
107
+ const norm = p.replace(/\\/g, '/');
108
+ if (norm.endsWith('deep') || norm.endsWith('deeper')) return { isDirectory: () => true };
109
+ return { isDirectory: () => false };
110
+ });
111
+
112
+ // depth=1 should not traverse into subdirs beyond first level
113
+ const result = handleTreeCommand(['--depth=1']);
114
+ expect(result.depth).toBe(1);
115
+ // 'deep/' at depth 1, its children are at depth 2 which exceeds limit
116
+ expect(result.structure['deep/']).toEqual({ _more: true });
117
+ });
118
+
119
+ it('should ignore default directories (node_modules, .git, dist)', () => {
120
+ mockExistsSync.mockReturnValue(true);
121
+ mockReadFileSync.mockReturnValue('');
122
+
123
+ mockReaddirSync.mockReturnValue(['src', 'node_modules', '.git', 'dist', 'index.ts']);
124
+
125
+ mockStatSync.mockImplementation((p: string) => {
126
+ const name = p.replace(/\\/g, '/').split('/').pop();
127
+ if (['src', 'node_modules', '.git', 'dist'].includes(name!)) {
128
+ return { isDirectory: () => true };
129
+ }
130
+ return { isDirectory: () => false };
131
+ });
132
+
133
+ const result = handleTreeCommand([]);
134
+ // node_modules, .git, dist should be in ignored list
135
+ expect(result.stats.ignored).toContain('node_modules');
136
+ expect(result.stats.ignored).toContain('.git');
137
+ expect(result.stats.ignored).toContain('dist');
138
+ });
139
+
140
+ it('should respect .gitignore patterns', () => {
141
+ mockExistsSync.mockReturnValue(true);
142
+ // .gitignore ignores "*.log" and "build/"
143
+ mockReadFileSync.mockReturnValue('*.log\nbuild/\n');
144
+
145
+ mockReaddirSync.mockReturnValue(['src', 'build', 'app.ts', 'debug.log']);
146
+
147
+ mockStatSync.mockImplementation((p: string) => {
148
+ const name = p.replace(/\\/g, '/').split('/').pop();
149
+ if (['src', 'build'].includes(name!)) return { isDirectory: () => true };
150
+ return { isDirectory: () => false };
151
+ });
152
+
153
+ const result = handleTreeCommand([]);
154
+ // build should be ignored (from .gitignore), debug.log should be ignored (*.log pattern)
155
+ expect(result.stats.ignored).toContain('build');
156
+ });
157
+
158
+ it('should use _files compression for directories with only files', () => {
159
+ mockExistsSync.mockReturnValue(true);
160
+ mockReadFileSync.mockReturnValue('');
161
+
162
+ // Root has only files
163
+ mockReaddirSync.mockReturnValue(['a.ts', 'b.ts', 'c.ts']);
164
+ mockStatSync.mockReturnValue({ isDirectory: () => false });
165
+
166
+ const result = handleTreeCommand([]);
167
+ // When a dir has only files, it uses the _files array optimization
168
+ expect(result.structure._files).toEqual(['a.ts', 'b.ts', 'c.ts']);
169
+ });
170
+
171
+ it('should handle empty directories', () => {
172
+ mockExistsSync.mockReturnValue(true);
173
+ mockReadFileSync.mockReturnValue('');
174
+ mockReaddirSync.mockReturnValue([]);
175
+
176
+ const result = handleTreeCommand([]);
177
+ expect(result.structure).toEqual({});
178
+ expect(result.stats.totalFiles).toBe(0);
179
+ });
180
+
181
+ it('should accept custom target directory', () => {
182
+ mockExistsSync.mockReturnValue(true);
183
+ mockReadFileSync.mockReturnValue('');
184
+ mockReaddirSync.mockReturnValue(['app.ts']);
185
+ mockStatSync.mockReturnValue({ isDirectory: () => false });
186
+
187
+ const result = handleTreeCommand(['src']);
188
+ expect(result.root).toBe('src');
189
+ });
190
+
191
+ it('should handle permission errors on directories', () => {
192
+ mockExistsSync.mockReturnValue(true);
193
+ mockReadFileSync.mockReturnValue('');
194
+ mockReaddirSync.mockImplementation(() => { throw new Error('EACCES'); });
195
+
196
+ const result = handleTreeCommand([]);
197
+ // Should return empty structure, not throw
198
+ expect(result.structure).toEqual({});
199
+ });
200
+
201
+ it('should handle mixed directories and files', () => {
202
+ mockExistsSync.mockReturnValue(true);
203
+ mockReadFileSync.mockReturnValue('');
204
+
205
+ mockReaddirSync.mockImplementation((dirPath: string) => {
206
+ const norm = dirPath.replace(/\\/g, '/');
207
+ if (norm.endsWith('utils')) return ['helper.ts'];
208
+ return ['utils', 'index.ts', 'config.ts'];
209
+ });
210
+
211
+ mockStatSync.mockImplementation((p: string) => {
212
+ const norm = p.replace(/\\/g, '/');
213
+ if (norm.endsWith('utils')) return { isDirectory: () => true };
214
+ return { isDirectory: () => false };
215
+ });
216
+
217
+ const result = handleTreeCommand([]);
218
+ // Mixed dir: files should be individual keys, not in _files
219
+ expect(result.structure['utils/']).toBeDefined();
220
+ expect(result.structure['index.ts']).toBeDefined();
221
+ });
222
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "node",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "outDir": "./dist"
11
+ },
12
+ "include": [
13
+ "src/**/*"
14
+ ]
15
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/cli.ts'],
5
+ format: ['cjs'],
6
+ target: 'node18',
7
+ clean: true,
8
+ minify: true,
9
+ // We bundle everything so there are zero dependencies at runtime
10
+ noExternal: [/(.*)/],
11
+ });