prjct-cli 0.10.0 → 0.10.2
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/CHANGELOG.md +31 -0
- package/core/__tests__/agentic/memory-system.test.js +263 -0
- package/core/__tests__/agentic/plan-mode.test.js +336 -0
- package/core/agentic/chain-of-thought.js +578 -0
- package/core/agentic/command-executor.js +238 -4
- package/core/agentic/context-builder.js +208 -8
- package/core/agentic/ground-truth.js +591 -0
- package/core/agentic/loop-detector.js +406 -0
- package/core/agentic/memory-system.js +850 -0
- package/core/agentic/parallel-tools.js +366 -0
- package/core/agentic/plan-mode.js +572 -0
- package/core/agentic/prompt-builder.js +76 -1
- package/core/agentic/response-templates.js +290 -0
- package/core/agentic/semantic-compression.js +517 -0
- package/core/agentic/think-blocks.js +657 -0
- package/core/agentic/tool-registry.js +32 -0
- package/core/agentic/validation-rules.js +380 -0
- package/core/command-registry.js +48 -0
- package/core/commands.js +43 -1
- package/core/context-sync.js +183 -0
- package/package.json +7 -15
- package/templates/commands/done.md +7 -0
- package/templates/commands/feature.md +8 -0
- package/templates/commands/ship.md +8 -0
- package/templates/commands/spec.md +128 -0
- package/templates/global/CLAUDE.md +17 -0
- package/core/__tests__/agentic/agent-router.test.js +0 -398
- package/core/__tests__/agentic/command-executor.test.js +0 -223
- package/core/__tests__/agentic/context-builder.test.js +0 -160
- package/core/__tests__/agentic/context-filter.test.js +0 -494
- package/core/__tests__/agentic/prompt-builder.test.js +0 -204
- package/core/__tests__/agentic/template-loader.test.js +0 -164
- package/core/__tests__/agentic/tool-registry.test.js +0 -243
- package/core/__tests__/domain/agent-generator.test.js +0 -289
- package/core/__tests__/domain/agent-loader.test.js +0 -179
- package/core/__tests__/domain/analyzer.test.js +0 -324
- package/core/__tests__/infrastructure/author-detector.test.js +0 -103
- package/core/__tests__/infrastructure/config-manager.test.js +0 -454
- package/core/__tests__/infrastructure/path-manager.test.js +0 -412
- package/core/__tests__/setup.test.js +0 -15
- package/core/__tests__/utils/date-helper.test.js +0 -169
- package/core/__tests__/utils/file-helper.test.js +0 -258
- package/core/__tests__/utils/jsonl-helper.test.js +0 -387
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
-
import { createRequire } from 'module'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import fs from 'fs/promises'
|
|
5
|
-
import os from 'os'
|
|
6
|
-
|
|
7
|
-
const require = createRequire(import.meta.url)
|
|
8
|
-
|
|
9
|
-
describe('Codebase Analyzer', () => {
|
|
10
|
-
let analyzer
|
|
11
|
-
let testProjectPath
|
|
12
|
-
let tempDir
|
|
13
|
-
|
|
14
|
-
beforeEach(async () => {
|
|
15
|
-
analyzer = require('../../domain/analyzer.js')
|
|
16
|
-
|
|
17
|
-
// Create temporary test directory
|
|
18
|
-
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'prjct-test-'))
|
|
19
|
-
testProjectPath = tempDir
|
|
20
|
-
|
|
21
|
-
analyzer.init(testProjectPath)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
afterEach(async () => {
|
|
25
|
-
if (tempDir) {
|
|
26
|
-
try {
|
|
27
|
-
await fs.rm(tempDir, { recursive: true, force: true })
|
|
28
|
-
} catch (error) {
|
|
29
|
-
// Ignore cleanup errors
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
describe('init()', () => {
|
|
35
|
-
it('should initialize with project path', () => {
|
|
36
|
-
analyzer.init('/test/path')
|
|
37
|
-
expect(analyzer.projectPath).toBe('/test/path')
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('should use current directory if no path provided', () => {
|
|
41
|
-
analyzer.init()
|
|
42
|
-
expect(analyzer.projectPath).toBeDefined()
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
describe('readPackageJson()', () => {
|
|
47
|
-
it('should read package.json when it exists', async () => {
|
|
48
|
-
const packageJson = {
|
|
49
|
-
name: 'test-project',
|
|
50
|
-
version: '1.0.0',
|
|
51
|
-
dependencies: { express: '^4.18.0' }
|
|
52
|
-
}
|
|
53
|
-
await fs.writeFile(
|
|
54
|
-
path.join(testProjectPath, 'package.json'),
|
|
55
|
-
JSON.stringify(packageJson, null, 2)
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
const result = await analyzer.readPackageJson()
|
|
59
|
-
|
|
60
|
-
expect(result).toBeDefined()
|
|
61
|
-
expect(result.name).toBe('test-project')
|
|
62
|
-
expect(result.dependencies.express).toBeDefined()
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('should return null when package.json does not exist', async () => {
|
|
66
|
-
const result = await analyzer.readPackageJson()
|
|
67
|
-
|
|
68
|
-
expect(result).toBeNull()
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('should return null for invalid JSON', async () => {
|
|
72
|
-
await fs.writeFile(path.join(testProjectPath, 'package.json'), 'invalid json{')
|
|
73
|
-
|
|
74
|
-
const result = await analyzer.readPackageJson()
|
|
75
|
-
|
|
76
|
-
expect(result).toBeNull()
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
describe('readCargoToml()', () => {
|
|
81
|
-
it('should read Cargo.toml when it exists', async () => {
|
|
82
|
-
const cargoContent = '[package]\nname = "test-project"\nversion = "0.1.0"'
|
|
83
|
-
await fs.writeFile(path.join(testProjectPath, 'Cargo.toml'), cargoContent)
|
|
84
|
-
|
|
85
|
-
const result = await analyzer.readCargoToml()
|
|
86
|
-
|
|
87
|
-
expect(result).toBeDefined()
|
|
88
|
-
expect(result).toContain('test-project')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('should return null when Cargo.toml does not exist', async () => {
|
|
92
|
-
const result = await analyzer.readCargoToml()
|
|
93
|
-
|
|
94
|
-
expect(result).toBeNull()
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
describe('readRequirements()', () => {
|
|
99
|
-
it('should read requirements.txt when it exists', async () => {
|
|
100
|
-
const requirements = 'flask==2.0.0\nrequests==2.28.0'
|
|
101
|
-
await fs.writeFile(path.join(testProjectPath, 'requirements.txt'), requirements)
|
|
102
|
-
|
|
103
|
-
const result = await analyzer.readRequirements()
|
|
104
|
-
|
|
105
|
-
expect(result).toBeDefined()
|
|
106
|
-
expect(result).toContain('flask')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('should return null when requirements.txt does not exist', async () => {
|
|
110
|
-
const result = await analyzer.readRequirements()
|
|
111
|
-
|
|
112
|
-
expect(result).toBeNull()
|
|
113
|
-
})
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
describe('readGoMod()', () => {
|
|
117
|
-
it('should read go.mod when it exists', async () => {
|
|
118
|
-
const goMod = 'module test-project\n\ngo 1.19'
|
|
119
|
-
await fs.writeFile(path.join(testProjectPath, 'go.mod'), goMod)
|
|
120
|
-
|
|
121
|
-
const result = await analyzer.readGoMod()
|
|
122
|
-
|
|
123
|
-
expect(result).toBeDefined()
|
|
124
|
-
expect(result).toContain('test-project')
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('should return null when go.mod does not exist', async () => {
|
|
128
|
-
const result = await analyzer.readGoMod()
|
|
129
|
-
|
|
130
|
-
expect(result).toBeNull()
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
describe('listDirectories()', () => {
|
|
135
|
-
it('should list directories in project root', async () => {
|
|
136
|
-
await fs.mkdir(path.join(testProjectPath, 'src'), { recursive: true })
|
|
137
|
-
await fs.mkdir(path.join(testProjectPath, 'tests'), { recursive: true })
|
|
138
|
-
await fs.writeFile(path.join(testProjectPath, 'file.txt'), 'content')
|
|
139
|
-
|
|
140
|
-
const directories = await analyzer.listDirectories()
|
|
141
|
-
|
|
142
|
-
expect(directories).toContain('src')
|
|
143
|
-
expect(directories).toContain('tests')
|
|
144
|
-
expect(directories).not.toContain('file.txt')
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('should exclude hidden directories', async () => {
|
|
148
|
-
await fs.mkdir(path.join(testProjectPath, '.git'), { recursive: true })
|
|
149
|
-
await fs.mkdir(path.join(testProjectPath, 'src'), { recursive: true })
|
|
150
|
-
|
|
151
|
-
const directories = await analyzer.listDirectories()
|
|
152
|
-
|
|
153
|
-
expect(directories).not.toContain('.git')
|
|
154
|
-
expect(directories).toContain('src')
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
it('should exclude node_modules', async () => {
|
|
158
|
-
await fs.mkdir(path.join(testProjectPath, 'node_modules'), { recursive: true })
|
|
159
|
-
await fs.mkdir(path.join(testProjectPath, 'src'), { recursive: true })
|
|
160
|
-
|
|
161
|
-
const directories = await analyzer.listDirectories()
|
|
162
|
-
|
|
163
|
-
expect(directories).not.toContain('node_modules')
|
|
164
|
-
expect(directories).toContain('src')
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('should return empty array for non-existent directory', async () => {
|
|
168
|
-
analyzer.init('/nonexistent/path')
|
|
169
|
-
|
|
170
|
-
const directories = await analyzer.listDirectories()
|
|
171
|
-
|
|
172
|
-
expect(directories).toEqual([])
|
|
173
|
-
})
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
describe('fileExists()', () => {
|
|
177
|
-
it('should return true for existing file', async () => {
|
|
178
|
-
await fs.writeFile(path.join(testProjectPath, 'test.txt'), 'content')
|
|
179
|
-
|
|
180
|
-
const exists = await analyzer.fileExists('test.txt')
|
|
181
|
-
|
|
182
|
-
expect(exists).toBe(true)
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('should return false for non-existent file', async () => {
|
|
186
|
-
const exists = await analyzer.fileExists('nonexistent.txt')
|
|
187
|
-
|
|
188
|
-
expect(exists).toBe(false)
|
|
189
|
-
})
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
describe('readFile()', () => {
|
|
193
|
-
it('should read file content', async () => {
|
|
194
|
-
const content = 'file content here'
|
|
195
|
-
await fs.writeFile(path.join(testProjectPath, 'test.txt'), content)
|
|
196
|
-
|
|
197
|
-
const result = await analyzer.readFile('test.txt')
|
|
198
|
-
|
|
199
|
-
expect(result).toBe(content)
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('should return null for non-existent file', async () => {
|
|
203
|
-
const result = await analyzer.readFile('nonexistent.txt')
|
|
204
|
-
|
|
205
|
-
expect(result).toBeNull()
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
it('should read nested files', async () => {
|
|
209
|
-
await fs.mkdir(path.join(testProjectPath, 'src'), { recursive: true })
|
|
210
|
-
await fs.writeFile(path.join(testProjectPath, 'src', 'app.js'), 'console.log("test")')
|
|
211
|
-
|
|
212
|
-
const result = await analyzer.readFile('src/app.js')
|
|
213
|
-
|
|
214
|
-
expect(result).toBe('console.log("test")')
|
|
215
|
-
})
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
describe('getGitLog()', () => {
|
|
219
|
-
it('should return git log when git repo exists', async () => {
|
|
220
|
-
// Initialize git repo for testing
|
|
221
|
-
try {
|
|
222
|
-
const { exec } = require('child_process')
|
|
223
|
-
const { promisify } = require('util')
|
|
224
|
-
const execAsync = promisify(exec)
|
|
225
|
-
|
|
226
|
-
await execAsync('git init', { cwd: testProjectPath })
|
|
227
|
-
await execAsync('git config user.email "test@test.com"', { cwd: testProjectPath })
|
|
228
|
-
await execAsync('git config user.name "Test User"', { cwd: testProjectPath })
|
|
229
|
-
await fs.writeFile(path.join(testProjectPath, 'test.txt'), 'content')
|
|
230
|
-
await execAsync('git add test.txt', { cwd: testProjectPath })
|
|
231
|
-
await execAsync('git commit -m "Initial commit"', { cwd: testProjectPath })
|
|
232
|
-
|
|
233
|
-
const log = await analyzer.getGitLog(10)
|
|
234
|
-
|
|
235
|
-
expect(typeof log).toBe('string')
|
|
236
|
-
} catch (error) {
|
|
237
|
-
// Git might not be available, skip test
|
|
238
|
-
expect(true).toBe(true)
|
|
239
|
-
}
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('should return empty string when git repo does not exist', async () => {
|
|
243
|
-
const log = await analyzer.getGitLog()
|
|
244
|
-
|
|
245
|
-
expect(log).toBe('')
|
|
246
|
-
})
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
describe('getGitStats()', () => {
|
|
250
|
-
it('should return git statistics when git repo exists', async () => {
|
|
251
|
-
try {
|
|
252
|
-
const { exec } = require('child_process')
|
|
253
|
-
const { promisify } = require('util')
|
|
254
|
-
const execAsync = promisify(exec)
|
|
255
|
-
|
|
256
|
-
await execAsync('git init', { cwd: testProjectPath })
|
|
257
|
-
await execAsync('git config user.email "test@test.com"', { cwd: testProjectPath })
|
|
258
|
-
await execAsync('git config user.name "Test User"', { cwd: testProjectPath })
|
|
259
|
-
await fs.writeFile(path.join(testProjectPath, 'test.txt'), 'content')
|
|
260
|
-
await execAsync('git add test.txt', { cwd: testProjectPath })
|
|
261
|
-
await execAsync('git commit -m "Initial commit"', { cwd: testProjectPath })
|
|
262
|
-
|
|
263
|
-
const stats = await analyzer.getGitStats()
|
|
264
|
-
|
|
265
|
-
expect(stats).toBeDefined()
|
|
266
|
-
expect(stats.totalCommits).toBeGreaterThanOrEqual(0)
|
|
267
|
-
expect(stats.contributors).toBeGreaterThanOrEqual(0)
|
|
268
|
-
} catch (error) {
|
|
269
|
-
// Git might not be available, skip test
|
|
270
|
-
expect(true).toBe(true)
|
|
271
|
-
}
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
it('should return default stats when git repo does not exist', async () => {
|
|
275
|
-
const stats = await analyzer.getGitStats()
|
|
276
|
-
|
|
277
|
-
expect(stats).toBeDefined()
|
|
278
|
-
expect(stats.totalCommits).toBe(0)
|
|
279
|
-
expect(stats.contributors).toBe(0)
|
|
280
|
-
expect(stats.age).toBe('unknown')
|
|
281
|
-
})
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
describe('countFiles()', () => {
|
|
285
|
-
it('should count files in project', async () => {
|
|
286
|
-
await fs.writeFile(path.join(testProjectPath, 'file1.txt'), 'content')
|
|
287
|
-
await fs.writeFile(path.join(testProjectPath, 'file2.txt'), 'content')
|
|
288
|
-
await fs.mkdir(path.join(testProjectPath, 'src'), { recursive: true })
|
|
289
|
-
await fs.writeFile(path.join(testProjectPath, 'src', 'app.js'), 'content')
|
|
290
|
-
|
|
291
|
-
const count = await analyzer.countFiles()
|
|
292
|
-
|
|
293
|
-
expect(count).toBeGreaterThan(0)
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
it('should return 0 for empty directory', async () => {
|
|
297
|
-
const count = await analyzer.countFiles()
|
|
298
|
-
|
|
299
|
-
// May have some files, so just check it's a number
|
|
300
|
-
expect(typeof count).toBe('number')
|
|
301
|
-
})
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
describe('findFiles()', () => {
|
|
305
|
-
it('should find files matching pattern', async () => {
|
|
306
|
-
await fs.writeFile(path.join(testProjectPath, 'app.js'), 'content')
|
|
307
|
-
await fs.writeFile(path.join(testProjectPath, 'test.js'), 'content')
|
|
308
|
-
await fs.mkdir(path.join(testProjectPath, 'src'), { recursive: true })
|
|
309
|
-
await fs.writeFile(path.join(testProjectPath, 'src', 'app.js'), 'content')
|
|
310
|
-
|
|
311
|
-
const files = await analyzer.findFiles('app.js')
|
|
312
|
-
|
|
313
|
-
expect(files.length).toBeGreaterThan(0)
|
|
314
|
-
expect(files.some(f => f.includes('app.js'))).toBe(true)
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
it('should return empty array when no files match', async () => {
|
|
318
|
-
const files = await analyzer.findFiles('nonexistent-pattern-xyz')
|
|
319
|
-
|
|
320
|
-
expect(files).toEqual([])
|
|
321
|
-
})
|
|
322
|
-
})
|
|
323
|
-
})
|
|
324
|
-
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
-
import { createRequire } from 'module'
|
|
3
|
-
|
|
4
|
-
const require = createRequire(import.meta.url)
|
|
5
|
-
|
|
6
|
-
describe('Author Detector', () => {
|
|
7
|
-
let AuthorDetector
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
AuthorDetector = require('../../infrastructure/author-detector.js')
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
describe('detect()', () => {
|
|
14
|
-
it('should return author object with all fields', async () => {
|
|
15
|
-
const author = await AuthorDetector.detect()
|
|
16
|
-
|
|
17
|
-
expect(author).toBeDefined()
|
|
18
|
-
expect(author).toHaveProperty('name')
|
|
19
|
-
expect(author).toHaveProperty('email')
|
|
20
|
-
expect(author).toHaveProperty('github')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('should have name field', async () => {
|
|
24
|
-
const author = await AuthorDetector.detect()
|
|
25
|
-
|
|
26
|
-
expect(author.name).toBeDefined()
|
|
27
|
-
expect(typeof author.name).toBe('string')
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('should use github as name fallback', async () => {
|
|
31
|
-
// This test verifies the fallback logic exists
|
|
32
|
-
// Actual behavior depends on system git config
|
|
33
|
-
const author = await AuthorDetector.detect()
|
|
34
|
-
|
|
35
|
-
if (!author.name && author.github) {
|
|
36
|
-
// Fallback should have been applied in detect()
|
|
37
|
-
expect(author.name).toBeDefined()
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('should default to Unknown if no name found', async () => {
|
|
42
|
-
// This test verifies the final fallback
|
|
43
|
-
const author = await AuthorDetector.detect()
|
|
44
|
-
|
|
45
|
-
// Should always have a name (at least 'Unknown')
|
|
46
|
-
expect(author.name).toBeDefined()
|
|
47
|
-
expect(author.name.length).toBeGreaterThan(0)
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
describe('detectGitHubUsername()', () => {
|
|
52
|
-
it('should attempt to detect GitHub username', async () => {
|
|
53
|
-
const username = await AuthorDetector.detectGitHubUsername()
|
|
54
|
-
|
|
55
|
-
// May return null if GitHub CLI not configured
|
|
56
|
-
expect(username === null || typeof username === 'string').toBe(true)
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
describe('detectGitName()', () => {
|
|
61
|
-
it('should attempt to detect git name', async () => {
|
|
62
|
-
const name = await AuthorDetector.detectGitName()
|
|
63
|
-
|
|
64
|
-
// May return null if git not configured
|
|
65
|
-
expect(name === null || typeof name === 'string').toBe(true)
|
|
66
|
-
})
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
describe('detectGitEmail()', () => {
|
|
70
|
-
it('should attempt to detect git email', async () => {
|
|
71
|
-
const email = await AuthorDetector.detectGitEmail()
|
|
72
|
-
|
|
73
|
-
// May return null if git not configured
|
|
74
|
-
expect(email === null || typeof email === 'string').toBe(true)
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
describe('execCommand()', () => {
|
|
79
|
-
it('should execute commands safely', async () => {
|
|
80
|
-
const result = await AuthorDetector.execCommand('echo "test"')
|
|
81
|
-
|
|
82
|
-
expect(result).toBeDefined()
|
|
83
|
-
expect(result).toHaveProperty('success')
|
|
84
|
-
expect(result).toHaveProperty('output')
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('should handle command failures gracefully', async () => {
|
|
88
|
-
const result = await AuthorDetector.execCommand('nonexistent-command-xyz-123')
|
|
89
|
-
|
|
90
|
-
expect(result.success).toBe(false)
|
|
91
|
-
expect(result.output).toBe('')
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('should return output for successful commands', async () => {
|
|
95
|
-
const result = await AuthorDetector.execCommand('echo "hello"')
|
|
96
|
-
|
|
97
|
-
if (result.success) {
|
|
98
|
-
expect(result.output).toContain('hello')
|
|
99
|
-
}
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
})
|
|
103
|
-
|