prjct-cli 0.10.0 → 0.10.3

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 (43) hide show
  1. package/CHANGELOG.md +41 -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/chain-of-thought.js +578 -0
  5. package/core/agentic/command-executor.js +238 -4
  6. package/core/agentic/context-builder.js +208 -8
  7. package/core/agentic/ground-truth.js +591 -0
  8. package/core/agentic/loop-detector.js +406 -0
  9. package/core/agentic/memory-system.js +850 -0
  10. package/core/agentic/parallel-tools.js +366 -0
  11. package/core/agentic/plan-mode.js +572 -0
  12. package/core/agentic/prompt-builder.js +76 -1
  13. package/core/agentic/response-templates.js +290 -0
  14. package/core/agentic/semantic-compression.js +517 -0
  15. package/core/agentic/think-blocks.js +657 -0
  16. package/core/agentic/tool-registry.js +32 -0
  17. package/core/agentic/validation-rules.js +380 -0
  18. package/core/command-registry.js +48 -0
  19. package/core/commands.js +65 -1
  20. package/core/context-sync.js +183 -0
  21. package/package.json +7 -15
  22. package/templates/commands/done.md +7 -0
  23. package/templates/commands/feature.md +8 -0
  24. package/templates/commands/ship.md +8 -0
  25. package/templates/commands/spec.md +128 -0
  26. package/templates/global/CLAUDE.md +17 -0
  27. package/core/__tests__/agentic/agent-router.test.js +0 -398
  28. package/core/__tests__/agentic/command-executor.test.js +0 -223
  29. package/core/__tests__/agentic/context-builder.test.js +0 -160
  30. package/core/__tests__/agentic/context-filter.test.js +0 -494
  31. package/core/__tests__/agentic/prompt-builder.test.js +0 -204
  32. package/core/__tests__/agentic/template-loader.test.js +0 -164
  33. package/core/__tests__/agentic/tool-registry.test.js +0 -243
  34. package/core/__tests__/domain/agent-generator.test.js +0 -289
  35. package/core/__tests__/domain/agent-loader.test.js +0 -179
  36. package/core/__tests__/domain/analyzer.test.js +0 -324
  37. package/core/__tests__/infrastructure/author-detector.test.js +0 -103
  38. package/core/__tests__/infrastructure/config-manager.test.js +0 -454
  39. package/core/__tests__/infrastructure/path-manager.test.js +0 -412
  40. package/core/__tests__/setup.test.js +0 -15
  41. package/core/__tests__/utils/date-helper.test.js +0 -169
  42. package/core/__tests__/utils/file-helper.test.js +0 -258
  43. 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
-