prjct-cli 0.9.2 → 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.
Files changed (53) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/core/__tests__/agentic/memory-system.test.js +263 -0
  3. package/core/__tests__/agentic/plan-mode.test.js +336 -0
  4. package/core/agentic/agent-router.js +253 -186
  5. package/core/agentic/chain-of-thought.js +578 -0
  6. package/core/agentic/command-executor.js +299 -17
  7. package/core/agentic/context-builder.js +208 -8
  8. package/core/agentic/context-filter.js +83 -83
  9. package/core/agentic/ground-truth.js +591 -0
  10. package/core/agentic/loop-detector.js +406 -0
  11. package/core/agentic/memory-system.js +850 -0
  12. package/core/agentic/parallel-tools.js +366 -0
  13. package/core/agentic/plan-mode.js +572 -0
  14. package/core/agentic/prompt-builder.js +127 -2
  15. package/core/agentic/response-templates.js +290 -0
  16. package/core/agentic/semantic-compression.js +517 -0
  17. package/core/agentic/think-blocks.js +657 -0
  18. package/core/agentic/tool-registry.js +32 -0
  19. package/core/agentic/validation-rules.js +380 -0
  20. package/core/command-registry.js +48 -0
  21. package/core/commands.js +128 -60
  22. package/core/context-sync.js +183 -0
  23. package/core/domain/agent-generator.js +77 -46
  24. package/core/domain/agent-loader.js +183 -0
  25. package/core/domain/agent-matcher.js +217 -0
  26. package/core/domain/agent-validator.js +217 -0
  27. package/core/domain/context-estimator.js +175 -0
  28. package/core/domain/product-standards.js +92 -0
  29. package/core/domain/smart-cache.js +157 -0
  30. package/core/domain/task-analyzer.js +353 -0
  31. package/core/domain/tech-detector.js +365 -0
  32. package/package.json +8 -16
  33. package/templates/commands/done.md +7 -0
  34. package/templates/commands/feature.md +8 -0
  35. package/templates/commands/ship.md +8 -0
  36. package/templates/commands/spec.md +128 -0
  37. package/templates/global/CLAUDE.md +17 -0
  38. package/core/__tests__/agentic/agent-router.test.js +0 -398
  39. package/core/__tests__/agentic/command-executor.test.js +0 -223
  40. package/core/__tests__/agentic/context-builder.test.js +0 -160
  41. package/core/__tests__/agentic/context-filter.test.js +0 -494
  42. package/core/__tests__/agentic/prompt-builder.test.js +0 -212
  43. package/core/__tests__/agentic/template-loader.test.js +0 -164
  44. package/core/__tests__/agentic/tool-registry.test.js +0 -243
  45. package/core/__tests__/domain/agent-generator.test.js +0 -296
  46. package/core/__tests__/domain/analyzer.test.js +0 -324
  47. package/core/__tests__/infrastructure/author-detector.test.js +0 -103
  48. package/core/__tests__/infrastructure/config-manager.test.js +0 -454
  49. package/core/__tests__/infrastructure/path-manager.test.js +0 -412
  50. package/core/__tests__/setup.test.js +0 -15
  51. package/core/__tests__/utils/date-helper.test.js +0 -169
  52. package/core/__tests__/utils/file-helper.test.js +0 -258
  53. package/core/__tests__/utils/jsonl-helper.test.js +0 -387
@@ -1,454 +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('Config Manager', () => {
10
- let configManager
11
- let pathManager
12
- let testProjectPath
13
- let tempDir
14
-
15
- beforeEach(async () => {
16
- configManager = require('../../infrastructure/config-manager.js')
17
- pathManager = require('../../infrastructure/path-manager.js')
18
-
19
- // Create temporary test directory
20
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'prjct-test-'))
21
- testProjectPath = tempDir
22
-
23
- // Create .prjct directory
24
- await fs.mkdir(path.join(testProjectPath, '.prjct'), { recursive: true })
25
- })
26
-
27
- afterEach(async () => {
28
- // Cleanup temp directory
29
- if (tempDir) {
30
- try {
31
- await fs.rm(tempDir, { recursive: true, force: true })
32
- } catch (error) {
33
- // Ignore cleanup errors
34
- }
35
- }
36
- })
37
-
38
- describe('readConfig()', () => {
39
- it('should read existing config', async () => {
40
- const config = {
41
- projectId: 'test-id-123',
42
- dataPath: '~/.prjct-cli/projects/test-id-123'
43
- }
44
- const configPath = pathManager.getLocalConfigPath(testProjectPath)
45
- await fs.writeFile(configPath, JSON.stringify(config, null, 2))
46
-
47
- const result = await configManager.readConfig(testProjectPath)
48
-
49
- expect(result).toBeDefined()
50
- expect(result.projectId).toBe('test-id-123')
51
- expect(result.dataPath).toBeDefined()
52
- })
53
-
54
- it('should return null for non-existent config', async () => {
55
- const result = await configManager.readConfig(testProjectPath)
56
-
57
- expect(result).toBeNull()
58
- })
59
-
60
- it('should return null for invalid JSON', async () => {
61
- const configPath = pathManager.getLocalConfigPath(testProjectPath)
62
- await fs.writeFile(configPath, 'invalid json{')
63
-
64
- const result = await configManager.readConfig(testProjectPath)
65
-
66
- expect(result).toBeNull()
67
- })
68
- })
69
-
70
- describe('writeConfig()', () => {
71
- it('should write config file', async () => {
72
- const config = {
73
- projectId: 'test-id-456',
74
- dataPath: '~/.prjct-cli/projects/test-id-456'
75
- }
76
-
77
- await configManager.writeConfig(testProjectPath, config)
78
-
79
- const configPath = pathManager.getLocalConfigPath(testProjectPath)
80
- const content = await fs.readFile(configPath, 'utf-8')
81
- const parsed = JSON.parse(content)
82
-
83
- expect(parsed.projectId).toBe('test-id-456')
84
- })
85
-
86
- it('should create .prjct directory if it does not exist', async () => {
87
- const newPath = path.join(tempDir, 'new-project')
88
- const config = { projectId: 'test', dataPath: '~/.prjct-cli/projects/test' }
89
-
90
- await configManager.writeConfig(newPath, config)
91
-
92
- const configPath = pathManager.getLocalConfigPath(newPath)
93
- const exists = await fs.access(configPath).then(() => true).catch(() => false)
94
-
95
- expect(exists).toBe(true)
96
- })
97
- })
98
-
99
- describe('readGlobalConfig()', () => {
100
- it('should read global config', async () => {
101
- const projectId = 'test-global-123'
102
- const globalConfig = {
103
- projectId,
104
- authors: [],
105
- version: '0.9.1',
106
- lastSync: new Date().toISOString()
107
- }
108
-
109
- await configManager.writeGlobalConfig(projectId, globalConfig)
110
- const result = await configManager.readGlobalConfig(projectId)
111
-
112
- expect(result).toBeDefined()
113
- expect(result.projectId).toBe(projectId)
114
- expect(result.authors).toBeDefined()
115
- })
116
-
117
- it('should return null for non-existent global config', async () => {
118
- const result = await configManager.readGlobalConfig('non-existent-id')
119
-
120
- expect(result).toBeNull()
121
- })
122
- })
123
-
124
- describe('writeGlobalConfig()', () => {
125
- it('should write global config file', async () => {
126
- const projectId = 'test-global-456'
127
- const globalConfig = {
128
- projectId,
129
- authors: [],
130
- version: '0.9.1'
131
- }
132
-
133
- await configManager.writeGlobalConfig(projectId, globalConfig)
134
-
135
- const result = await configManager.readGlobalConfig(projectId)
136
- expect(result.projectId).toBe(projectId)
137
- })
138
-
139
- it('should create directory structure if needed', async () => {
140
- const projectId = 'test-new-global'
141
- const globalConfig = { projectId, authors: [] }
142
-
143
- await configManager.writeGlobalConfig(projectId, globalConfig)
144
-
145
- const result = await configManager.readGlobalConfig(projectId)
146
- expect(result).toBeDefined()
147
- })
148
- })
149
-
150
- describe('ensureGlobalConfig()', () => {
151
- it('should return existing global config', async () => {
152
- const projectId = 'test-ensure-123'
153
- const existingConfig = {
154
- projectId,
155
- authors: [],
156
- version: '0.9.1'
157
- }
158
-
159
- await configManager.writeGlobalConfig(projectId, existingConfig)
160
- const result = await configManager.ensureGlobalConfig(projectId)
161
-
162
- expect(result.projectId).toBe(projectId)
163
- })
164
-
165
- it('should create new global config if not exists', async () => {
166
- const projectId = 'test-ensure-new'
167
-
168
- const result = await configManager.ensureGlobalConfig(projectId)
169
-
170
- expect(result).toBeDefined()
171
- expect(result.projectId).toBe(projectId)
172
- expect(result.authors).toEqual([])
173
- expect(result.version).toBeDefined()
174
- expect(result.lastSync).toBeDefined()
175
- })
176
- })
177
-
178
- describe('createConfig()', () => {
179
- it('should create both local and global config', async () => {
180
- const author = {
181
- name: 'Test User',
182
- email: 'test@example.com',
183
- github: 'testuser'
184
- }
185
-
186
- const localConfig = await configManager.createConfig(testProjectPath, author)
187
-
188
- expect(localConfig).toBeDefined()
189
- expect(localConfig.projectId).toBeDefined()
190
- expect(localConfig.dataPath).toBeDefined()
191
-
192
- // Verify local config was written
193
- const readLocal = await configManager.readConfig(testProjectPath)
194
- expect(readLocal.projectId).toBe(localConfig.projectId)
195
-
196
- // Verify global config was written
197
- const globalConfig = await configManager.readGlobalConfig(localConfig.projectId)
198
- expect(globalConfig).toBeDefined()
199
- expect(globalConfig.authors.length).toBe(1)
200
- expect(globalConfig.authors[0].github).toBe('testuser')
201
- })
202
-
203
- it('should handle missing author fields', async () => {
204
- const author = {}
205
-
206
- const localConfig = await configManager.createConfig(testProjectPath, author)
207
-
208
- expect(localConfig).toBeDefined()
209
- const globalConfig = await configManager.readGlobalConfig(localConfig.projectId)
210
- expect(globalConfig.authors[0].name).toBe('Unknown')
211
- })
212
- })
213
-
214
- describe('validateConfig()', () => {
215
- it('should validate correct config', () => {
216
- const config = {
217
- projectId: 'test-123',
218
- dataPath: '~/.prjct-cli/projects/test-123'
219
- }
220
-
221
- expect(configManager.validateConfig(config)).toBe(true)
222
- })
223
-
224
- it('should reject config without projectId', () => {
225
- const config = { dataPath: '~/.prjct-cli/projects/test' }
226
-
227
- expect(configManager.validateConfig(config)).toBe(false)
228
- })
229
-
230
- it('should reject config without dataPath', () => {
231
- const config = { projectId: 'test-123' }
232
-
233
- expect(configManager.validateConfig(config)).toBe(false)
234
- })
235
-
236
- it('should reject null config', () => {
237
- expect(configManager.validateConfig(null)).toBe(false)
238
- })
239
-
240
- it('should reject undefined config', () => {
241
- expect(configManager.validateConfig(undefined)).toBe(false)
242
- })
243
- })
244
-
245
- describe('getProjectId()', () => {
246
- it('should return projectId from config', async () => {
247
- const config = {
248
- projectId: 'test-from-config',
249
- dataPath: '~/.prjct-cli/projects/test-from-config'
250
- }
251
- await configManager.writeConfig(testProjectPath, config)
252
-
253
- const projectId = await configManager.getProjectId(testProjectPath)
254
-
255
- expect(projectId).toBe('test-from-config')
256
- })
257
-
258
- it('should generate projectId if config does not exist', async () => {
259
- const projectId = await configManager.getProjectId(testProjectPath)
260
-
261
- expect(projectId).toBeDefined()
262
- expect(typeof projectId).toBe('string')
263
- expect(projectId.length).toBeGreaterThan(0)
264
- })
265
- })
266
-
267
- describe('isConfigured()', () => {
268
- it('should return true for valid config', async () => {
269
- const config = {
270
- projectId: 'test-123',
271
- dataPath: '~/.prjct-cli/projects/test-123'
272
- }
273
- await configManager.writeConfig(testProjectPath, config)
274
-
275
- const isConfigured = await configManager.isConfigured(testProjectPath)
276
-
277
- expect(isConfigured).toBe(true)
278
- })
279
-
280
- it('should return false for missing config', async () => {
281
- const isConfigured = await configManager.isConfigured(testProjectPath)
282
-
283
- expect(isConfigured).toBe(false)
284
- })
285
-
286
- it('should return false for invalid config', async () => {
287
- const config = { projectId: 'test' } // Missing dataPath
288
- await configManager.writeConfig(testProjectPath, config)
289
-
290
- const isConfigured = await configManager.isConfigured(testProjectPath)
291
-
292
- expect(isConfigured).toBe(false)
293
- })
294
- })
295
-
296
- describe('Author Management', () => {
297
- const projectId = 'test-authors'
298
-
299
- beforeEach(async () => {
300
- await configManager.ensureGlobalConfig(projectId)
301
- })
302
-
303
- describe('addAuthor()', () => {
304
- it('should add new author', async () => {
305
- const author = {
306
- name: 'New Author',
307
- email: 'new@example.com',
308
- github: 'newauthor'
309
- }
310
-
311
- await configManager.addAuthor(projectId, author)
312
-
313
- const globalConfig = await configManager.readGlobalConfig(projectId)
314
- expect(globalConfig.authors.length).toBe(1)
315
- expect(globalConfig.authors[0].github).toBe('newauthor')
316
- })
317
-
318
- it('should not add duplicate author', async () => {
319
- const author = {
320
- name: 'Duplicate',
321
- github: 'duplicate'
322
- }
323
-
324
- await configManager.addAuthor(projectId, author)
325
- await configManager.addAuthor(projectId, author)
326
-
327
- const globalConfig = await configManager.readGlobalConfig(projectId)
328
- expect(globalConfig.authors.length).toBe(1)
329
- })
330
-
331
- it('should set timestamps for new author', async () => {
332
- const author = { github: 'timestamp-test' }
333
-
334
- await configManager.addAuthor(projectId, author)
335
-
336
- const globalConfig = await configManager.readGlobalConfig(projectId)
337
- const addedAuthor = globalConfig.authors[0]
338
- expect(addedAuthor.firstContribution).toBeDefined()
339
- expect(addedAuthor.lastActivity).toBeDefined()
340
- })
341
- })
342
-
343
- describe('findAuthor()', () => {
344
- it('should find existing author', async () => {
345
- const author = { github: 'findme' }
346
- await configManager.addAuthor(projectId, author)
347
-
348
- const found = await configManager.findAuthor(projectId, 'findme')
349
-
350
- expect(found).toBeDefined()
351
- expect(found.github).toBe('findme')
352
- })
353
-
354
- it('should return null for non-existent author', async () => {
355
- const found = await configManager.findAuthor(projectId, 'notfound')
356
-
357
- expect(found).toBeNull()
358
- })
359
-
360
- it('should return null for non-existent project', async () => {
361
- const found = await configManager.findAuthor('non-existent', 'anyone')
362
-
363
- expect(found).toBeNull()
364
- })
365
- })
366
-
367
- describe('updateAuthorActivity()', () => {
368
- it('should update last activity timestamp', async () => {
369
- const author = { github: 'activity-test' }
370
- await configManager.addAuthor(projectId, author)
371
-
372
- const before = await configManager.findAuthor(projectId, 'activity-test')
373
- const beforeTime = before.lastActivity
374
-
375
- // Wait a bit to ensure different timestamp
376
- await new Promise(resolve => setTimeout(resolve, 10))
377
-
378
- await configManager.updateAuthorActivity(projectId, 'activity-test')
379
-
380
- const after = await configManager.findAuthor(projectId, 'activity-test')
381
- expect(after.lastActivity).not.toBe(beforeTime)
382
- })
383
-
384
- it('should not fail for non-existent author', async () => {
385
- await expect(
386
- configManager.updateAuthorActivity(projectId, 'nonexistent')
387
- ).resolves.not.toThrow()
388
- })
389
- })
390
- })
391
-
392
- describe('getConfigWithDefaults()', () => {
393
- it('should return existing config', async () => {
394
- const config = {
395
- projectId: 'test-defaults',
396
- dataPath: '~/.prjct-cli/projects/test-defaults'
397
- }
398
- await configManager.writeConfig(testProjectPath, config)
399
-
400
- const result = await configManager.getConfigWithDefaults(testProjectPath)
401
-
402
- expect(result.projectId).toBe('test-defaults')
403
- })
404
-
405
- it('should return defaults if config does not exist', async () => {
406
- const result = await configManager.getConfigWithDefaults(testProjectPath)
407
-
408
- expect(result).toBeDefined()
409
- expect(result.projectId).toBeDefined()
410
- expect(result.dataPath).toBeDefined()
411
- })
412
- })
413
-
414
- describe('updateLastSync()', () => {
415
- it('should update lastSync timestamp', async () => {
416
- const projectId = 'test-sync'
417
- await configManager.ensureGlobalConfig(projectId)
418
-
419
- const before = await configManager.readGlobalConfig(projectId)
420
- const beforeTime = before.lastSync
421
-
422
- await new Promise(resolve => setTimeout(resolve, 10))
423
- await configManager.updateLastSync(testProjectPath)
424
-
425
- // Need to set projectId in local config for updateLastSync to work
426
- await configManager.writeConfig(testProjectPath, { projectId, dataPath: '~/.prjct-cli/projects/test-sync' })
427
- await configManager.updateLastSync(testProjectPath)
428
-
429
- const after = await configManager.readGlobalConfig(projectId)
430
- expect(after.lastSync).not.toBe(beforeTime)
431
- })
432
- })
433
-
434
- describe('needsMigration()', () => {
435
- it('should return false for new project', async () => {
436
- const needs = await configManager.needsMigration(testProjectPath)
437
-
438
- expect(needs).toBe(false)
439
- })
440
-
441
- it('should detect when migration is needed', async () => {
442
- // Create legacy structure
443
- const legacyPath = path.join(testProjectPath, '.prjct')
444
- await fs.mkdir(legacyPath, { recursive: true })
445
- await fs.writeFile(path.join(legacyPath, 'old-file.md'), 'content')
446
-
447
- const needs = await configManager.needsMigration(testProjectPath)
448
-
449
- // Should be true if legacy exists but no proper config/structure
450
- expect(typeof needs).toBe('boolean')
451
- })
452
- })
453
- })
454
-