prjct-cli 0.45.0 → 0.45.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.
- package/CHANGELOG.md +75 -0
- package/bin/prjct.ts +117 -10
- package/core/__tests__/agentic/memory-system.test.ts +39 -26
- package/core/__tests__/agentic/plan-mode.test.ts +64 -46
- package/core/__tests__/agentic/prompt-builder.test.ts +14 -14
- package/core/__tests__/services/project-index.test.ts +353 -0
- package/core/__tests__/types/fs.test.ts +3 -3
- package/core/__tests__/utils/date-helper.test.ts +10 -10
- package/core/__tests__/utils/output.test.ts +9 -6
- package/core/__tests__/utils/project-commands.test.ts +5 -6
- package/core/agentic/agent-router.ts +9 -10
- package/core/agentic/chain-of-thought.ts +16 -4
- package/core/agentic/command-executor.ts +66 -40
- package/core/agentic/context-builder.ts +8 -5
- package/core/agentic/ground-truth.ts +15 -9
- package/core/agentic/index.ts +145 -152
- package/core/agentic/loop-detector.ts +40 -11
- package/core/agentic/memory-system.ts +98 -35
- package/core/agentic/orchestrator-executor.ts +135 -71
- package/core/agentic/plan-mode.ts +46 -16
- package/core/agentic/prompt-builder.ts +108 -42
- package/core/agentic/services.ts +10 -9
- package/core/agentic/skill-loader.ts +9 -15
- package/core/agentic/smart-context.ts +129 -79
- package/core/agentic/template-executor.ts +13 -12
- package/core/agentic/template-loader.ts +7 -4
- package/core/agentic/tool-registry.ts +16 -13
- package/core/agents/index.ts +1 -1
- package/core/agents/performance.ts +10 -27
- package/core/ai-tools/formatters.ts +8 -6
- package/core/ai-tools/generator.ts +4 -4
- package/core/ai-tools/index.ts +1 -1
- package/core/ai-tools/registry.ts +21 -11
- package/core/bus/bus.ts +23 -16
- package/core/bus/index.ts +2 -2
- package/core/cli/linear.ts +3 -5
- package/core/cli/start.ts +28 -25
- package/core/commands/analysis.ts +58 -39
- package/core/commands/analytics.ts +52 -44
- package/core/commands/base.ts +15 -13
- package/core/commands/cleanup.ts +6 -13
- package/core/commands/command-data.ts +28 -4
- package/core/commands/commands.ts +57 -24
- package/core/commands/context.ts +4 -4
- package/core/commands/design.ts +3 -10
- package/core/commands/index.ts +5 -8
- package/core/commands/maintenance.ts +7 -4
- package/core/commands/planning.ts +179 -56
- package/core/commands/register.ts +13 -9
- package/core/commands/registry.ts +15 -14
- package/core/commands/setup.ts +26 -14
- package/core/commands/shipping.ts +11 -16
- package/core/commands/snapshots.ts +16 -32
- package/core/commands/uninstall.ts +541 -0
- package/core/commands/workflow.ts +24 -28
- package/core/constants/index.ts +10 -22
- package/core/context/generator.ts +82 -33
- package/core/context-tools/files-tool.ts +18 -19
- package/core/context-tools/imports-tool.ts +13 -33
- package/core/context-tools/index.ts +29 -54
- package/core/context-tools/recent-tool.ts +16 -22
- package/core/context-tools/signatures-tool.ts +17 -26
- package/core/context-tools/summary-tool.ts +20 -22
- package/core/context-tools/token-counter.ts +25 -20
- package/core/context-tools/types.ts +5 -5
- package/core/domain/agent-generator.ts +7 -5
- package/core/domain/agent-loader.ts +2 -2
- package/core/domain/analyzer.ts +19 -16
- package/core/domain/architecture-generator.ts +6 -3
- package/core/domain/context-estimator.ts +3 -4
- package/core/domain/snapshot-manager.ts +25 -22
- package/core/domain/task-stack.ts +24 -14
- package/core/errors.ts +1 -1
- package/core/events/events.ts +2 -4
- package/core/events/index.ts +1 -2
- package/core/index.ts +28 -16
- package/core/infrastructure/agent-detector.ts +3 -3
- package/core/infrastructure/ai-provider.ts +23 -20
- package/core/infrastructure/author-detector.ts +16 -10
- package/core/infrastructure/capability-installer.ts +2 -2
- package/core/infrastructure/claude-agent.ts +6 -6
- package/core/infrastructure/command-installer.ts +22 -17
- package/core/infrastructure/config-manager.ts +18 -14
- package/core/infrastructure/editors-config.ts +8 -4
- package/core/infrastructure/path-manager.ts +8 -6
- package/core/infrastructure/permission-manager.ts +20 -17
- package/core/infrastructure/setup.ts +42 -38
- package/core/infrastructure/update-checker.ts +5 -5
- package/core/integrations/issue-tracker/enricher.ts +8 -19
- package/core/integrations/issue-tracker/index.ts +2 -2
- package/core/integrations/issue-tracker/manager.ts +15 -15
- package/core/integrations/issue-tracker/types.ts +5 -22
- package/core/integrations/jira/client.ts +67 -59
- package/core/integrations/jira/index.ts +11 -14
- package/core/integrations/jira/mcp-adapter.ts +5 -10
- package/core/integrations/jira/service.ts +10 -10
- package/core/integrations/linear/client.ts +27 -18
- package/core/integrations/linear/index.ts +9 -12
- package/core/integrations/linear/service.ts +11 -11
- package/core/integrations/linear/sync.ts +8 -8
- package/core/outcomes/analyzer.ts +5 -18
- package/core/outcomes/index.ts +2 -2
- package/core/outcomes/recorder.ts +3 -3
- package/core/plugin/builtin/webhook.ts +19 -15
- package/core/plugin/hooks.ts +29 -21
- package/core/plugin/index.ts +7 -7
- package/core/plugin/loader.ts +19 -19
- package/core/plugin/registry.ts +12 -23
- package/core/schemas/agents.ts +1 -1
- package/core/schemas/analysis.ts +1 -1
- package/core/schemas/enriched-task.ts +62 -49
- package/core/schemas/ideas.ts +13 -13
- package/core/schemas/index.ts +17 -27
- package/core/schemas/issues.ts +40 -25
- package/core/schemas/metrics.ts +25 -25
- package/core/schemas/outcomes.ts +70 -62
- package/core/schemas/permissions.ts +15 -12
- package/core/schemas/prd.ts +27 -14
- package/core/schemas/project.ts +3 -3
- package/core/schemas/roadmap.ts +47 -34
- package/core/schemas/schemas.ts +3 -4
- package/core/schemas/shipped.ts +3 -3
- package/core/schemas/state.ts +43 -29
- package/core/server/index.ts +5 -6
- package/core/server/routes-extended.ts +68 -72
- package/core/server/routes.ts +3 -3
- package/core/server/server.ts +31 -26
- package/core/services/agent-generator.ts +237 -0
- package/core/services/agent-service.ts +2 -2
- package/core/services/breakdown-service.ts +2 -4
- package/core/services/context-generator.ts +299 -0
- package/core/services/context-selector.ts +420 -0
- package/core/services/doctor-service.ts +426 -0
- package/core/services/file-categorizer.ts +448 -0
- package/core/services/file-scorer.ts +270 -0
- package/core/services/git-analyzer.ts +267 -0
- package/core/services/index.ts +27 -10
- package/core/services/memory-service.ts +3 -4
- package/core/services/project-index.ts +911 -0
- package/core/services/project-service.ts +4 -4
- package/core/services/skill-installer.ts +14 -17
- package/core/services/skill-lock.ts +3 -3
- package/core/services/skill-service.ts +12 -6
- package/core/services/stack-detector.ts +245 -0
- package/core/services/sync-service.ts +87 -345
- package/core/services/watch-service.ts +294 -0
- package/core/session/compaction.ts +23 -31
- package/core/session/index.ts +11 -5
- package/core/session/log-migration.ts +3 -3
- package/core/session/metrics.ts +19 -14
- package/core/session/session-log-manager.ts +12 -17
- package/core/session/task-session-manager.ts +25 -25
- package/core/session/utils.ts +1 -1
- package/core/storage/ideas-storage.ts +41 -57
- package/core/storage/index-storage.ts +514 -0
- package/core/storage/index.ts +41 -17
- package/core/storage/metrics-storage.ts +39 -34
- package/core/storage/queue-storage.ts +35 -45
- package/core/storage/shipped-storage.ts +17 -20
- package/core/storage/state-storage.ts +50 -30
- package/core/storage/storage-manager.ts +6 -6
- package/core/storage/storage.ts +18 -15
- package/core/sync/auth-config.ts +3 -3
- package/core/sync/index.ts +13 -19
- package/core/sync/oauth-handler.ts +3 -3
- package/core/sync/sync-client.ts +4 -9
- package/core/sync/sync-manager.ts +12 -14
- package/core/types/commands.ts +42 -7
- package/core/types/index.ts +284 -305
- package/core/types/integrations.ts +3 -3
- package/core/types/storage.ts +14 -14
- package/core/types/utils.ts +3 -3
- package/core/utils/agent-stream.ts +3 -1
- package/core/utils/animations.ts +14 -11
- package/core/utils/branding.ts +7 -7
- package/core/utils/cache.ts +1 -3
- package/core/utils/collection-filters.ts +3 -15
- package/core/utils/date-helper.ts +2 -7
- package/core/utils/file-helper.ts +13 -8
- package/core/utils/jsonl-helper.ts +13 -10
- package/core/utils/keychain.ts +4 -8
- package/core/utils/logger.ts +1 -1
- package/core/utils/next-steps.ts +3 -3
- package/core/utils/output.ts +58 -11
- package/core/utils/project-commands.ts +6 -6
- package/core/utils/project-credentials.ts +5 -12
- package/core/utils/runtime.ts +2 -2
- package/core/utils/session-helper.ts +3 -4
- package/core/utils/version.ts +3 -3
- package/core/wizard/index.ts +13 -0
- package/core/wizard/onboarding.ts +633 -0
- package/core/workflow/state-machine.ts +7 -7
- package/dist/bin/prjct.mjs +18755 -15574
- package/dist/core/infrastructure/command-installer.js +86 -79
- package/dist/core/infrastructure/editors-config.js +6 -6
- package/dist/core/infrastructure/setup.js +246 -225
- package/dist/core/utils/version.js +9 -9
- package/package.json +11 -12
- package/scripts/build.js +3 -3
- package/scripts/postinstall.js +2 -2
- package/templates/mcp-config.json +6 -1
- package/templates/permissions/permissive.jsonc +1 -1
- package/templates/permissions/strict.jsonc +5 -9
- package/templates/global/docs/agents.md +0 -88
- package/templates/global/docs/architecture.md +0 -103
- package/templates/global/docs/commands.md +0 -96
- package/templates/global/docs/validation.md +0 -95
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* OPTIMIZED: Tests updated to match compressed prompt structure
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { beforeEach, describe, expect, it } from 'bun:test'
|
|
9
9
|
import promptBuilder from '../../agentic/prompt-builder'
|
|
10
10
|
|
|
11
11
|
describe('PromptBuilder', () => {
|
|
@@ -63,7 +63,7 @@ describe('PromptBuilder', () => {
|
|
|
63
63
|
next: '# NEXT\n\n## Priority Queue\n\n1. Task 1',
|
|
64
64
|
context: 'Project context information',
|
|
65
65
|
analysis: 'Stack: Node.js\nPatterns: ES6 modules',
|
|
66
|
-
metrics: 'Some metrics data'
|
|
66
|
+
metrics: 'Some metrics data',
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const filtered = builder.filterRelevantState(state)
|
|
@@ -79,7 +79,7 @@ describe('PromptBuilder', () => {
|
|
|
79
79
|
const largeContent = 'x'.repeat(2000)
|
|
80
80
|
const state = {
|
|
81
81
|
now: '# NOW\n\n**Task**',
|
|
82
|
-
largeFile: largeContent
|
|
82
|
+
largeFile: largeContent,
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const filtered = builder.filterRelevantState(state)
|
|
@@ -99,7 +99,7 @@ describe('PromptBuilder', () => {
|
|
|
99
99
|
it('should include patterns for code commands', () => {
|
|
100
100
|
const template = {
|
|
101
101
|
frontmatter: { description: 'Build feature', name: 'p:build' },
|
|
102
|
-
content: '## Flow\nBuild something'
|
|
102
|
+
content: '## Flow\nBuild something',
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const context = { projectPath: '/test', files: ['file1.js'] }
|
|
@@ -114,7 +114,7 @@ describe('PromptBuilder', () => {
|
|
|
114
114
|
it('should NOT include patterns for non-code commands', () => {
|
|
115
115
|
const template = {
|
|
116
116
|
frontmatter: { description: 'Show current task', name: 'p:now' },
|
|
117
|
-
content: '## Flow\nShow task'
|
|
117
|
+
content: '## Flow\nShow task',
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
const context = { projectPath: '/test', files: ['file1.js'] }
|
|
@@ -130,12 +130,12 @@ describe('PromptBuilder', () => {
|
|
|
130
130
|
it('should list available files when context has files', () => {
|
|
131
131
|
const template = {
|
|
132
132
|
frontmatter: { description: 'Test command' },
|
|
133
|
-
content: '## Flow\nDo something'
|
|
133
|
+
content: '## Flow\nDo something',
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
const context = {
|
|
137
137
|
projectPath: '/test',
|
|
138
|
-
files: ['src/file1.js', 'src/file2.js', 'tests/test1.js']
|
|
138
|
+
files: ['src/file1.js', 'src/file2.js', 'tests/test1.js'],
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
const state = {}
|
|
@@ -151,7 +151,7 @@ describe('PromptBuilder', () => {
|
|
|
151
151
|
it('should show project path when no files listed', () => {
|
|
152
152
|
const template = {
|
|
153
153
|
frontmatter: { description: 'Test command' },
|
|
154
|
-
content: '## Flow\nDo something'
|
|
154
|
+
content: '## Flow\nDo something',
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
const context = { projectPath: '/test/project' }
|
|
@@ -169,15 +169,15 @@ describe('PromptBuilder', () => {
|
|
|
169
169
|
const template = {
|
|
170
170
|
frontmatter: {
|
|
171
171
|
description: 'Test command',
|
|
172
|
-
'allowed-tools': ['Read', 'Write']
|
|
172
|
+
'allowed-tools': ['Read', 'Write'],
|
|
173
173
|
},
|
|
174
|
-
content: '## Flow\n1. Do step 1\n2. Do step 2'
|
|
174
|
+
content: '## Flow\n1. Do step 1\n2. Do step 2',
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
const context = {
|
|
178
178
|
projectPath: '/test',
|
|
179
179
|
params: { task: 'test task' },
|
|
180
|
-
files: ['file1.js']
|
|
180
|
+
files: ['file1.js'],
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
const state = { now: '# NOW\n\n**Current task**' }
|
|
@@ -194,7 +194,7 @@ describe('PromptBuilder', () => {
|
|
|
194
194
|
it('should be concise (under 2000 chars for simple prompt)', () => {
|
|
195
195
|
const template = {
|
|
196
196
|
frontmatter: { description: 'Test', 'allowed-tools': ['Read'] },
|
|
197
|
-
content: '## Flow\n1. Test'
|
|
197
|
+
content: '## Flow\n1. Test',
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
const context = { projectPath: '/test', files: ['a.js'] }
|
|
@@ -210,7 +210,7 @@ describe('PromptBuilder', () => {
|
|
|
210
210
|
it('should include compact plan mode instructions', () => {
|
|
211
211
|
const template = {
|
|
212
212
|
frontmatter: { description: 'Test' },
|
|
213
|
-
content: '## Flow\nTest'
|
|
213
|
+
content: '## Flow\nTest',
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
const context = { projectPath: '/test' }
|
|
@@ -227,7 +227,7 @@ describe('PromptBuilder', () => {
|
|
|
227
227
|
it('should include approval required section', () => {
|
|
228
228
|
const template = {
|
|
229
229
|
frontmatter: { description: 'Test' },
|
|
230
|
-
content: '## Flow\nTest'
|
|
230
|
+
content: '## Flow\nTest',
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
const context = { projectPath: '/test' }
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectIndex Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the persistent project scanner with scoring
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
|
8
|
+
import fs from 'node:fs/promises'
|
|
9
|
+
import os from 'node:os'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import pathManager from '../../infrastructure/path-manager'
|
|
12
|
+
import { FileScorer } from '../../services/file-scorer'
|
|
13
|
+
import { createProjectIndexer, RELEVANCE_THRESHOLD } from '../../services/project-index'
|
|
14
|
+
import { getDefaultIndex, indexStorage } from '../../storage/index-storage'
|
|
15
|
+
|
|
16
|
+
describe('FileScorer', () => {
|
|
17
|
+
describe('scoreFile', () => {
|
|
18
|
+
it('should give higher scores to recently modified files', () => {
|
|
19
|
+
const scorer = new FileScorer()
|
|
20
|
+
|
|
21
|
+
const recentFile = {
|
|
22
|
+
path: 'src/recent.ts',
|
|
23
|
+
size: 1000,
|
|
24
|
+
mtime: new Date(), // today
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const oldFile = {
|
|
28
|
+
path: 'src/old.ts',
|
|
29
|
+
size: 1000,
|
|
30
|
+
mtime: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), // 1 year ago
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const context = {
|
|
34
|
+
allFiles: new Map([
|
|
35
|
+
['src/recent.ts', recentFile],
|
|
36
|
+
['src/old.ts', oldFile],
|
|
37
|
+
]),
|
|
38
|
+
configFiles: new Set<string>(),
|
|
39
|
+
maxFileSize: 1000,
|
|
40
|
+
maxRecentCommits: 0,
|
|
41
|
+
now: new Date(),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const recentScore = scorer.scoreFile(recentFile, context)
|
|
45
|
+
const oldScore = scorer.scoreFile(oldFile, context)
|
|
46
|
+
|
|
47
|
+
expect(recentScore.factors.recency).toBeGreaterThan(oldScore.factors.recency)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should give higher scores to config files', () => {
|
|
51
|
+
const scorer = new FileScorer()
|
|
52
|
+
|
|
53
|
+
const configFile = {
|
|
54
|
+
path: 'package.json',
|
|
55
|
+
size: 500,
|
|
56
|
+
mtime: new Date(),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const sourceFile = {
|
|
60
|
+
path: 'src/utils.ts',
|
|
61
|
+
size: 500,
|
|
62
|
+
mtime: new Date(),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const context = {
|
|
66
|
+
allFiles: new Map([
|
|
67
|
+
['package.json', configFile],
|
|
68
|
+
['src/utils.ts', sourceFile],
|
|
69
|
+
]),
|
|
70
|
+
configFiles: new Set(['package.json']),
|
|
71
|
+
maxFileSize: 500,
|
|
72
|
+
maxRecentCommits: 0,
|
|
73
|
+
now: new Date(),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const configScore = scorer.scoreFile(configFile, context)
|
|
77
|
+
const sourceScore = scorer.scoreFile(sourceFile, context)
|
|
78
|
+
|
|
79
|
+
expect(configScore.factors.configRelevance).toBe(20)
|
|
80
|
+
expect(sourceScore.factors.configRelevance).toBe(0)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should give higher scores to index/main files', () => {
|
|
84
|
+
const scorer = new FileScorer()
|
|
85
|
+
|
|
86
|
+
const indexFile = {
|
|
87
|
+
path: 'src/index.ts',
|
|
88
|
+
size: 500,
|
|
89
|
+
mtime: new Date(),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const helperFile = {
|
|
93
|
+
path: 'src/random-helper.ts',
|
|
94
|
+
size: 500,
|
|
95
|
+
mtime: new Date(),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const context = {
|
|
99
|
+
allFiles: new Map([
|
|
100
|
+
['src/index.ts', indexFile],
|
|
101
|
+
['src/random-helper.ts', helperFile],
|
|
102
|
+
]),
|
|
103
|
+
configFiles: new Set<string>(),
|
|
104
|
+
maxFileSize: 500,
|
|
105
|
+
maxRecentCommits: 0,
|
|
106
|
+
now: new Date(),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const indexScore = scorer.scoreFile(indexFile, context)
|
|
110
|
+
const helperScore = scorer.scoreFile(helperFile, context)
|
|
111
|
+
|
|
112
|
+
expect(indexScore.factors.nameRelevance).toBe(15)
|
|
113
|
+
expect(helperScore.factors.nameRelevance).toBe(0)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should penalize very large files', () => {
|
|
117
|
+
const scorer = new FileScorer()
|
|
118
|
+
|
|
119
|
+
const normalFile = {
|
|
120
|
+
path: 'src/normal.ts',
|
|
121
|
+
size: 5000, // 5KB - optimal
|
|
122
|
+
mtime: new Date(),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const hugeFile = {
|
|
126
|
+
path: 'src/huge.ts',
|
|
127
|
+
size: 500000, // 500KB - too large
|
|
128
|
+
mtime: new Date(),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const context = {
|
|
132
|
+
allFiles: new Map([
|
|
133
|
+
['src/normal.ts', normalFile],
|
|
134
|
+
['src/huge.ts', hugeFile],
|
|
135
|
+
]),
|
|
136
|
+
configFiles: new Set<string>(),
|
|
137
|
+
maxFileSize: 500000,
|
|
138
|
+
maxRecentCommits: 0,
|
|
139
|
+
now: new Date(),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const normalScore = scorer.scoreFile(normalFile, context)
|
|
143
|
+
const hugeScore = scorer.scoreFile(hugeFile, context)
|
|
144
|
+
|
|
145
|
+
expect(normalScore.factors.sizeOptimal).toBeGreaterThan(hugeScore.factors.sizeOptimal)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('getRelevantFiles', () => {
|
|
150
|
+
it('should filter files below threshold', () => {
|
|
151
|
+
const scorer = new FileScorer()
|
|
152
|
+
|
|
153
|
+
// Config file should be relevant
|
|
154
|
+
const configFile = {
|
|
155
|
+
path: 'package.json',
|
|
156
|
+
size: 500,
|
|
157
|
+
mtime: new Date(),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Random old file should not be relevant
|
|
161
|
+
const randomFile = {
|
|
162
|
+
path: 'src/old-unused.ts',
|
|
163
|
+
size: 50, // very small
|
|
164
|
+
mtime: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), // old
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const context = {
|
|
168
|
+
allFiles: new Map([
|
|
169
|
+
['package.json', configFile],
|
|
170
|
+
['src/old-unused.ts', randomFile],
|
|
171
|
+
]),
|
|
172
|
+
configFiles: new Set(['package.json']),
|
|
173
|
+
maxFileSize: 500,
|
|
174
|
+
maxRecentCommits: 0,
|
|
175
|
+
now: new Date(),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const relevant = scorer.getRelevantFiles(context, RELEVANCE_THRESHOLD)
|
|
179
|
+
|
|
180
|
+
expect(relevant.length).toBeGreaterThanOrEqual(1)
|
|
181
|
+
expect(relevant[0].path).toBe('package.json')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
describe('IndexStorage', () => {
|
|
187
|
+
const testProjectId = `test-project-${Date.now()}`
|
|
188
|
+
|
|
189
|
+
beforeEach(async () => {
|
|
190
|
+
// Set up test directory
|
|
191
|
+
const testDir = path.join(os.tmpdir(), `prjct-test-${Date.now()}`)
|
|
192
|
+
pathManager.setGlobalBaseDir(testDir)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
afterEach(async () => {
|
|
196
|
+
// Cleanup is handled by temp directory
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe('readIndex/writeIndex', () => {
|
|
200
|
+
it('should return null for non-existent index', async () => {
|
|
201
|
+
const index = await indexStorage.readIndex(testProjectId)
|
|
202
|
+
expect(index).toBeNull()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should persist and retrieve index', async () => {
|
|
206
|
+
const testIndex = getDefaultIndex('/test/project')
|
|
207
|
+
testIndex.lastFullScan = new Date().toISOString()
|
|
208
|
+
testIndex.totalFiles = 100
|
|
209
|
+
|
|
210
|
+
await indexStorage.writeIndex(testProjectId, testIndex)
|
|
211
|
+
const retrieved = await indexStorage.readIndex(testProjectId)
|
|
212
|
+
|
|
213
|
+
expect(retrieved).not.toBeNull()
|
|
214
|
+
expect(retrieved!.totalFiles).toBe(100)
|
|
215
|
+
expect(retrieved!.lastFullScan).toBe(testIndex.lastFullScan)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('hasValidIndex', () => {
|
|
220
|
+
it('should return false for non-existent index', async () => {
|
|
221
|
+
const valid = await indexStorage.hasValidIndex(testProjectId)
|
|
222
|
+
expect(valid).toBe(false)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('should return true for index with lastFullScan', async () => {
|
|
226
|
+
const testIndex = getDefaultIndex('/test/project')
|
|
227
|
+
testIndex.lastFullScan = new Date().toISOString()
|
|
228
|
+
|
|
229
|
+
await indexStorage.writeIndex(testProjectId, testIndex)
|
|
230
|
+
const valid = await indexStorage.hasValidIndex(testProjectId)
|
|
231
|
+
|
|
232
|
+
expect(valid).toBe(true)
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('ProjectIndexer', () => {
|
|
238
|
+
const testProjectId = `test-indexer-${Date.now()}`
|
|
239
|
+
let testProjectPath: string
|
|
240
|
+
|
|
241
|
+
beforeEach(async () => {
|
|
242
|
+
// Create a temp project directory
|
|
243
|
+
testProjectPath = path.join(os.tmpdir(), `prjct-indexer-test-${Date.now()}`)
|
|
244
|
+
await fs.mkdir(testProjectPath, { recursive: true })
|
|
245
|
+
|
|
246
|
+
// Create some test files
|
|
247
|
+
await fs.mkdir(path.join(testProjectPath, 'src'), { recursive: true })
|
|
248
|
+
await fs.writeFile(
|
|
249
|
+
path.join(testProjectPath, 'src', 'index.ts'),
|
|
250
|
+
'export const main = () => {}'
|
|
251
|
+
)
|
|
252
|
+
await fs.writeFile(
|
|
253
|
+
path.join(testProjectPath, 'src', 'utils.ts'),
|
|
254
|
+
'export const helper = () => {}'
|
|
255
|
+
)
|
|
256
|
+
await fs.writeFile(
|
|
257
|
+
path.join(testProjectPath, 'package.json'),
|
|
258
|
+
JSON.stringify(
|
|
259
|
+
{
|
|
260
|
+
name: 'test-project',
|
|
261
|
+
version: '1.0.0',
|
|
262
|
+
dependencies: { express: '^4.0.0' },
|
|
263
|
+
devDependencies: { typescript: '^5.0.0' },
|
|
264
|
+
},
|
|
265
|
+
null,
|
|
266
|
+
2
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
// Set up test storage directory
|
|
271
|
+
const testStorageDir = path.join(os.tmpdir(), `prjct-storage-${Date.now()}`)
|
|
272
|
+
pathManager.setGlobalBaseDir(testStorageDir)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
afterEach(async () => {
|
|
276
|
+
// Cleanup
|
|
277
|
+
try {
|
|
278
|
+
await fs.rm(testProjectPath, { recursive: true })
|
|
279
|
+
} catch {
|
|
280
|
+
// Ignore cleanup errors
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
describe('fullScan', () => {
|
|
285
|
+
it('should scan project and create index', async () => {
|
|
286
|
+
const indexer = createProjectIndexer(testProjectPath, testProjectId)
|
|
287
|
+
const result = await indexer.fullScan()
|
|
288
|
+
|
|
289
|
+
expect(result.fromCache).toBe(false)
|
|
290
|
+
expect(result.index.totalFiles).toBeGreaterThanOrEqual(2)
|
|
291
|
+
expect(result.index.lastFullScan).not.toBe('')
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should detect TypeScript from config files', async () => {
|
|
295
|
+
// Add tsconfig.json
|
|
296
|
+
await fs.writeFile(
|
|
297
|
+
path.join(testProjectPath, 'tsconfig.json'),
|
|
298
|
+
JSON.stringify({
|
|
299
|
+
compilerOptions: { target: 'ES2020' },
|
|
300
|
+
})
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
const indexer = createProjectIndexer(testProjectPath, testProjectId)
|
|
304
|
+
const result = await indexer.fullScan()
|
|
305
|
+
|
|
306
|
+
expect(result.index.configFiles.some((cf) => cf.type === 'tsconfig.json')).toBe(true)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('should detect Express backend', async () => {
|
|
310
|
+
const indexer = createProjectIndexer(testProjectPath, testProjectId)
|
|
311
|
+
const result = await indexer.fullScan()
|
|
312
|
+
|
|
313
|
+
expect(result.index.detectedStack.frameworks).toContain('Express')
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
describe('loadOrScan', () => {
|
|
318
|
+
it('should use cached index if valid', async () => {
|
|
319
|
+
const indexer = createProjectIndexer(testProjectPath, testProjectId)
|
|
320
|
+
|
|
321
|
+
// First scan
|
|
322
|
+
const firstResult = await indexer.fullScan()
|
|
323
|
+
expect(firstResult.fromCache).toBe(false)
|
|
324
|
+
|
|
325
|
+
// Second load should use cache
|
|
326
|
+
const secondResult = await indexer.loadOrScan()
|
|
327
|
+
expect(secondResult.fromCache).toBe(true)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('should rescan if forceFullScan is true', async () => {
|
|
331
|
+
const indexer = createProjectIndexer(testProjectPath, testProjectId)
|
|
332
|
+
|
|
333
|
+
// First scan
|
|
334
|
+
await indexer.fullScan()
|
|
335
|
+
|
|
336
|
+
// Force rescan
|
|
337
|
+
const result = await indexer.loadOrScan({ forceFullScan: true })
|
|
338
|
+
expect(result.fromCache).toBe(false)
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
describe('getRelevantContext', () => {
|
|
343
|
+
it('should return files within token limit', async () => {
|
|
344
|
+
const indexer = createProjectIndexer(testProjectPath, testProjectId)
|
|
345
|
+
await indexer.fullScan()
|
|
346
|
+
|
|
347
|
+
const context = await indexer.getRelevantContext(10000)
|
|
348
|
+
|
|
349
|
+
expect(context.estimatedTokens).toBeLessThanOrEqual(10000)
|
|
350
|
+
expect(context.compressionRate).toBeGreaterThanOrEqual(0)
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
})
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* Tests for file system error utilities
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { describe,
|
|
6
|
+
import { describe, expect, it } from 'bun:test'
|
|
7
7
|
import {
|
|
8
|
-
isNotFoundError,
|
|
9
|
-
isPermissionError,
|
|
10
8
|
isDirNotEmptyError,
|
|
11
9
|
isFileExistsError,
|
|
12
10
|
isNodeError,
|
|
11
|
+
isNotFoundError,
|
|
12
|
+
isPermissionError,
|
|
13
13
|
} from '../../types/fs'
|
|
14
14
|
|
|
15
15
|
describe('FS Error Utilities', () => {
|
|
@@ -3,24 +3,24 @@
|
|
|
3
3
|
* Tests for centralized date operations and formatting
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, setSystemTime } from 'bun:test'
|
|
7
7
|
import {
|
|
8
|
+
calculateDuration,
|
|
8
9
|
formatDate,
|
|
10
|
+
formatDuration,
|
|
9
11
|
formatMonth,
|
|
10
|
-
getTodayKey,
|
|
11
12
|
getDateKey,
|
|
12
|
-
|
|
13
|
-
parseDate,
|
|
14
|
-
getTimestamp,
|
|
13
|
+
getDateRange,
|
|
15
14
|
getDaysAgo,
|
|
16
15
|
getDaysFromNow,
|
|
17
|
-
|
|
16
|
+
getEndOfDay,
|
|
17
|
+
getStartOfDay,
|
|
18
|
+
getTimestamp,
|
|
19
|
+
getTodayKey,
|
|
20
|
+
getYearMonthDay,
|
|
18
21
|
isToday,
|
|
19
22
|
isWithinLastDays,
|
|
20
|
-
|
|
21
|
-
calculateDuration,
|
|
22
|
-
getStartOfDay,
|
|
23
|
-
getEndOfDay,
|
|
23
|
+
parseDate,
|
|
24
24
|
} from '../../utils/date-helper'
|
|
25
25
|
|
|
26
26
|
describe('DateHelper', () => {
|
|
@@ -3,21 +3,24 @@
|
|
|
3
3
|
* Minimal output system for prjct-cli
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test'
|
|
7
7
|
import out from '../../utils/output'
|
|
8
8
|
|
|
9
9
|
describe('Output Module', () => {
|
|
10
10
|
let consoleLogSpy: ReturnType<typeof spyOn>
|
|
11
|
+
let consoleErrorSpy: ReturnType<typeof spyOn>
|
|
11
12
|
let stdoutWriteSpy: ReturnType<typeof spyOn>
|
|
12
13
|
|
|
13
14
|
beforeEach(() => {
|
|
14
15
|
consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
|
|
16
|
+
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
|
|
15
17
|
stdoutWriteSpy = spyOn(process.stdout, 'write').mockImplementation(() => true)
|
|
16
18
|
out.stop()
|
|
17
19
|
})
|
|
18
20
|
|
|
19
21
|
afterEach(() => {
|
|
20
22
|
consoleLogSpy.mockRestore()
|
|
23
|
+
consoleErrorSpy.mockRestore()
|
|
21
24
|
stdoutWriteSpy.mockRestore()
|
|
22
25
|
out.stop()
|
|
23
26
|
})
|
|
@@ -50,8 +53,8 @@ describe('Output Module', () => {
|
|
|
50
53
|
it('should output error message with X mark', () => {
|
|
51
54
|
out.fail('something failed')
|
|
52
55
|
|
|
53
|
-
expect(
|
|
54
|
-
const output =
|
|
56
|
+
expect(consoleErrorSpy).toHaveBeenCalledTimes(1)
|
|
57
|
+
const output = consoleErrorSpy.mock.calls[0][0]
|
|
55
58
|
expect(output).toContain('✗')
|
|
56
59
|
expect(output).toContain('something failed')
|
|
57
60
|
})
|
|
@@ -60,7 +63,7 @@ describe('Output Module', () => {
|
|
|
60
63
|
const longMessage = 'error '.repeat(50)
|
|
61
64
|
out.fail(longMessage)
|
|
62
65
|
|
|
63
|
-
const output =
|
|
66
|
+
const output = consoleErrorSpy.mock.calls[0][0]
|
|
64
67
|
expect(output.length).toBeLessThan(80)
|
|
65
68
|
})
|
|
66
69
|
|
|
@@ -90,7 +93,7 @@ describe('Output Module', () => {
|
|
|
90
93
|
it('should start spinner with message', async () => {
|
|
91
94
|
out.spin('loading')
|
|
92
95
|
|
|
93
|
-
await new Promise(resolve => setTimeout(resolve, 150))
|
|
96
|
+
await new Promise((resolve) => setTimeout(resolve, 150))
|
|
94
97
|
|
|
95
98
|
expect(stdoutWriteSpy).toHaveBeenCalled()
|
|
96
99
|
const output = stdoutWriteSpy.mock.calls[0][0]
|
|
@@ -109,7 +112,7 @@ describe('Output Module', () => {
|
|
|
109
112
|
describe('stop()', () => {
|
|
110
113
|
it('should stop spinner and clear line', async () => {
|
|
111
114
|
out.spin('loading')
|
|
112
|
-
await new Promise(resolve => setTimeout(resolve, 150))
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, 150))
|
|
113
116
|
|
|
114
117
|
stdoutWriteSpy.mockClear()
|
|
115
118
|
out.stop()
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* Ensures prjct-cli uses the repo's own test/lint tooling (not hardcoded).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import fs from 'fs/promises'
|
|
8
|
-
import
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
|
7
|
+
import fs from 'node:fs/promises'
|
|
8
|
+
import os from 'node:os'
|
|
9
|
+
import path from 'node:path'
|
|
9
10
|
|
|
10
11
|
import { detectProjectCommands } from '../../utils/project-commands'
|
|
11
12
|
|
|
@@ -23,7 +24,7 @@ async function writeText(filePath: string, content: string): Promise<void> {
|
|
|
23
24
|
|
|
24
25
|
describe('detectProjectCommands', () => {
|
|
25
26
|
beforeEach(async () => {
|
|
26
|
-
tmpRoot = await fs.mkdtemp(path.join(
|
|
27
|
+
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'prjct-detect-commands-'))
|
|
27
28
|
})
|
|
28
29
|
|
|
29
30
|
afterEach(async () => {
|
|
@@ -68,5 +69,3 @@ describe('detectProjectCommands', () => {
|
|
|
68
69
|
expect(detected.test?.command).toBe('cargo test')
|
|
69
70
|
})
|
|
70
71
|
})
|
|
71
|
-
|
|
72
|
-
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* @version 4.0.0
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import fs from 'fs/promises'
|
|
15
|
-
import path from 'path'
|
|
14
|
+
import fs from 'node:fs/promises'
|
|
15
|
+
import path from 'node:path'
|
|
16
16
|
import configManager from '../infrastructure/config-manager'
|
|
17
17
|
import pathManager from '../infrastructure/path-manager'
|
|
18
|
-
import { isNotFoundError } from '../types/fs'
|
|
19
18
|
import type { Agent, AssignmentContext } from '../types'
|
|
19
|
+
import { isNotFoundError } from '../types/fs'
|
|
20
20
|
|
|
21
21
|
// Re-export types for convenience
|
|
22
22
|
export type { Agent, AssignmentContext } from '../types'
|
|
@@ -130,13 +130,12 @@ class AgentRouter {
|
|
|
130
130
|
'agent-usage.jsonl'
|
|
131
131
|
)
|
|
132
132
|
|
|
133
|
-
const entry =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}) + '\n'
|
|
133
|
+
const entry = `${JSON.stringify({
|
|
134
|
+
timestamp: new Date().toISOString(),
|
|
135
|
+
task: typeof task === 'string' ? task : task.description,
|
|
136
|
+
agent: typeof agent === 'string' ? agent : agent.name,
|
|
137
|
+
projectId: this.projectId,
|
|
138
|
+
})}\n`
|
|
140
139
|
|
|
141
140
|
await fs.appendFile(logPath, entry)
|
|
142
141
|
} catch (error) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @version 1.0.0
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type { ContextState, ProjectContext } from '../types'
|
|
10
10
|
|
|
11
11
|
// Type aliases for compatibility with ProjectContext from contextBuilder.build()
|
|
12
12
|
type Context = Pick<ProjectContext, 'projectId' | 'projectPath' | 'params'>
|
|
@@ -44,7 +44,11 @@ function requiresReasoning(commandName: string): boolean {
|
|
|
44
44
|
/**
|
|
45
45
|
* Reason through a command before execution
|
|
46
46
|
*/
|
|
47
|
-
async function reason(
|
|
47
|
+
async function reason(
|
|
48
|
+
commandName: string,
|
|
49
|
+
context: Context,
|
|
50
|
+
state: State
|
|
51
|
+
): Promise<ReasoningResult> {
|
|
48
52
|
const steps: ReasoningStep[] = []
|
|
49
53
|
const plan: string[] = []
|
|
50
54
|
const criticalIssues: string[] = []
|
|
@@ -63,7 +67,11 @@ async function reason(commandName: string, context: Context, state: State): Prom
|
|
|
63
67
|
if (state.shipped) {
|
|
64
68
|
steps.push({ step: 'Shipped log accessible', passed: true })
|
|
65
69
|
} else {
|
|
66
|
-
steps.push({
|
|
70
|
+
steps.push({
|
|
71
|
+
step: 'Shipped log accessible',
|
|
72
|
+
passed: false,
|
|
73
|
+
details: 'shipped.md not found',
|
|
74
|
+
})
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
// Plan
|
|
@@ -79,7 +87,11 @@ async function reason(commandName: string, context: Context, state: State): Prom
|
|
|
79
87
|
if (context.params.description || context.params.feature) {
|
|
80
88
|
steps.push({ step: 'Has feature description', passed: true })
|
|
81
89
|
} else {
|
|
82
|
-
steps.push({
|
|
90
|
+
steps.push({
|
|
91
|
+
step: 'Has feature description',
|
|
92
|
+
passed: false,
|
|
93
|
+
details: 'No description provided',
|
|
94
|
+
})
|
|
83
95
|
criticalIssues.push('Missing feature description')
|
|
84
96
|
}
|
|
85
97
|
|