prjct-cli 0.7.1 → 0.7.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 +111 -0
- package/README.md +32 -0
- package/core/__tests__/agentic/command-executor.test.js +223 -0
- package/core/__tests__/agentic/context-builder.test.js +160 -0
- package/core/__tests__/agentic/prompt-builder.test.js +212 -0
- package/core/__tests__/agentic/template-loader.test.js +164 -0
- package/core/__tests__/agentic/tool-registry.test.js +243 -0
- package/core/__tests__/domain/agent-generator.test.js +296 -0
- package/core/__tests__/setup.test.js +15 -0
- package/core/__tests__/utils/date-helper.test.js +169 -0
- package/core/__tests__/utils/file-helper.test.js +258 -0
- package/core/commands.js +0 -3
- package/core/infrastructure/agent-detector.js +2 -0
- package/core/infrastructure/session-manager.js +0 -1
- package/core/utils/file-helper.js +2 -0
- package/package.json +8 -3
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import * as fileHelper from '../../utils/file-helper.js'
|
|
3
|
+
import fs from 'fs/promises'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import os from 'os'
|
|
6
|
+
|
|
7
|
+
describe('File Helper', () => {
|
|
8
|
+
const testDir = path.join(os.tmpdir(), 'prjct-file-helper-test-' + Date.now())
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
await fs.mkdir(testDir, { recursive: true })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
try {
|
|
16
|
+
await fs.rm(testDir, { recursive: true, force: true })
|
|
17
|
+
} catch (error) {
|
|
18
|
+
// Ignore cleanup errors
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('readJson()', () => {
|
|
23
|
+
it('should read and parse JSON file', async () => {
|
|
24
|
+
const testFile = path.join(testDir, 'test.json')
|
|
25
|
+
const data = { name: 'test', value: 123 }
|
|
26
|
+
await fs.writeFile(testFile, JSON.stringify(data))
|
|
27
|
+
|
|
28
|
+
const result = await fileHelper.readJson(testFile)
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual(data)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should return default value for non-existent file', async () => {
|
|
34
|
+
const result = await fileHelper.readJson('/nonexistent/file.json', { default: true })
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual({ default: true })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should return null by default for non-existent file', async () => {
|
|
40
|
+
const result = await fileHelper.readJson('/nonexistent/file.json')
|
|
41
|
+
|
|
42
|
+
expect(result).toBeNull()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should handle complex JSON structures', async () => {
|
|
46
|
+
const testFile = path.join(testDir, 'complex.json')
|
|
47
|
+
const data = {
|
|
48
|
+
nested: {
|
|
49
|
+
array: [1, 2, 3],
|
|
50
|
+
object: { key: 'value' },
|
|
51
|
+
},
|
|
52
|
+
boolean: true,
|
|
53
|
+
number: 42,
|
|
54
|
+
}
|
|
55
|
+
await fs.writeFile(testFile, JSON.stringify(data))
|
|
56
|
+
|
|
57
|
+
const result = await fileHelper.readJson(testFile)
|
|
58
|
+
|
|
59
|
+
expect(result).toEqual(data)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('writeJson()', () => {
|
|
64
|
+
it('should write JSON file with pretty formatting', async () => {
|
|
65
|
+
const testFile = path.join(testDir, 'write.json')
|
|
66
|
+
const data = { name: 'test', value: 123 }
|
|
67
|
+
|
|
68
|
+
await fileHelper.writeJson(testFile, data)
|
|
69
|
+
|
|
70
|
+
const content = await fs.readFile(testFile, 'utf-8')
|
|
71
|
+
expect(JSON.parse(content)).toEqual(data)
|
|
72
|
+
expect(content).toContain('\n') // Should be pretty-printed
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should use custom indentation', async () => {
|
|
76
|
+
const testFile = path.join(testDir, 'indent.json')
|
|
77
|
+
const data = { key: 'value' }
|
|
78
|
+
|
|
79
|
+
await fileHelper.writeJson(testFile, data, 4)
|
|
80
|
+
|
|
81
|
+
const content = await fs.readFile(testFile, 'utf-8')
|
|
82
|
+
expect(content).toContain(' ') // 4 spaces
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should handle nested objects', async () => {
|
|
86
|
+
const testFile = path.join(testDir, 'nested.json')
|
|
87
|
+
const data = {
|
|
88
|
+
level1: {
|
|
89
|
+
level2: {
|
|
90
|
+
level3: 'deep',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await fileHelper.writeJson(testFile, data)
|
|
96
|
+
|
|
97
|
+
const result = await fileHelper.readJson(testFile)
|
|
98
|
+
expect(result).toEqual(data)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('readFile()', () => {
|
|
103
|
+
it('should read text file', async () => {
|
|
104
|
+
const testFile = path.join(testDir, 'text.txt')
|
|
105
|
+
const content = 'Hello, World!'
|
|
106
|
+
await fs.writeFile(testFile, content)
|
|
107
|
+
|
|
108
|
+
const result = await fileHelper.readFile(testFile)
|
|
109
|
+
|
|
110
|
+
expect(result).toBe(content)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should return default value for non-existent file', async () => {
|
|
114
|
+
const result = await fileHelper.readFile('/nonexistent/file.txt', 'default content')
|
|
115
|
+
|
|
116
|
+
expect(result).toBe('default content')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should return empty string by default', async () => {
|
|
120
|
+
const result = await fileHelper.readFile('/nonexistent/file.txt')
|
|
121
|
+
|
|
122
|
+
expect(result).toBe('')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should handle multi-line content', async () => {
|
|
126
|
+
const testFile = path.join(testDir, 'multiline.txt')
|
|
127
|
+
const content = 'Line 1\nLine 2\nLine 3'
|
|
128
|
+
await fs.writeFile(testFile, content)
|
|
129
|
+
|
|
130
|
+
const result = await fileHelper.readFile(testFile)
|
|
131
|
+
|
|
132
|
+
expect(result).toBe(content)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('writeFile()', () => {
|
|
137
|
+
it('should write text file', async () => {
|
|
138
|
+
const testFile = path.join(testDir, 'write.txt')
|
|
139
|
+
const content = 'Test content'
|
|
140
|
+
|
|
141
|
+
await fileHelper.writeFile(testFile, content)
|
|
142
|
+
|
|
143
|
+
const result = await fs.readFile(testFile, 'utf-8')
|
|
144
|
+
expect(result).toBe(content)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should overwrite existing file', async () => {
|
|
148
|
+
const testFile = path.join(testDir, 'overwrite.txt')
|
|
149
|
+
|
|
150
|
+
await fileHelper.writeFile(testFile, 'First')
|
|
151
|
+
await fileHelper.writeFile(testFile, 'Second')
|
|
152
|
+
|
|
153
|
+
const result = await fs.readFile(testFile, 'utf-8')
|
|
154
|
+
expect(result).toBe('Second')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should create directory if needed', async () => {
|
|
158
|
+
const nestedFile = path.join(testDir, 'nested', 'dir', 'file.txt')
|
|
159
|
+
|
|
160
|
+
await fileHelper.writeFile(nestedFile, 'content')
|
|
161
|
+
|
|
162
|
+
const exists = await fs
|
|
163
|
+
.access(nestedFile)
|
|
164
|
+
.then(() => true)
|
|
165
|
+
.catch(() => false)
|
|
166
|
+
expect(exists).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('fileExists()', () => {
|
|
171
|
+
it('should return true for existing file', async () => {
|
|
172
|
+
const testFile = path.join(testDir, 'exists.txt')
|
|
173
|
+
await fs.writeFile(testFile, 'content')
|
|
174
|
+
|
|
175
|
+
const exists = await fileHelper.fileExists(testFile)
|
|
176
|
+
|
|
177
|
+
expect(exists).toBe(true)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should return false for non-existent file', async () => {
|
|
181
|
+
const exists = await fileHelper.fileExists('/nonexistent/file.txt')
|
|
182
|
+
|
|
183
|
+
expect(exists).toBe(false)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should work with directories', async () => {
|
|
187
|
+
const exists = await fileHelper.fileExists(testDir)
|
|
188
|
+
|
|
189
|
+
expect(exists).toBe(true)
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('ensureDir()', () => {
|
|
194
|
+
it('should create directory if not exists', async () => {
|
|
195
|
+
const newDir = path.join(testDir, 'new', 'nested', 'dir')
|
|
196
|
+
|
|
197
|
+
await fileHelper.ensureDir(newDir)
|
|
198
|
+
|
|
199
|
+
const exists = await fs
|
|
200
|
+
.access(newDir)
|
|
201
|
+
.then(() => true)
|
|
202
|
+
.catch(() => false)
|
|
203
|
+
expect(exists).toBe(true)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should not fail if directory exists', async () => {
|
|
207
|
+
await fileHelper.ensureDir(testDir)
|
|
208
|
+
|
|
209
|
+
// Should not throw
|
|
210
|
+
await expect(fileHelper.ensureDir(testDir)).resolves.not.toThrow()
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('Integration', () => {
|
|
215
|
+
it('should read and write JSON files', async () => {
|
|
216
|
+
const testFile = path.join(testDir, 'integration.json')
|
|
217
|
+
const data = { test: 'data', number: 42 }
|
|
218
|
+
|
|
219
|
+
await fileHelper.writeJson(testFile, data)
|
|
220
|
+
const result = await fileHelper.readJson(testFile)
|
|
221
|
+
|
|
222
|
+
expect(result).toEqual(data)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('should handle file operations pipeline', async () => {
|
|
226
|
+
const testFile = path.join(testDir, 'pipeline.txt')
|
|
227
|
+
|
|
228
|
+
// Write
|
|
229
|
+
await fileHelper.writeFile(testFile, 'original')
|
|
230
|
+
|
|
231
|
+
// Check exists
|
|
232
|
+
const exists = await fileHelper.fileExists(testFile)
|
|
233
|
+
expect(exists).toBe(true)
|
|
234
|
+
|
|
235
|
+
// Read
|
|
236
|
+
const content = await fileHelper.readFile(testFile)
|
|
237
|
+
expect(content).toBe('original')
|
|
238
|
+
|
|
239
|
+
// Update
|
|
240
|
+
await fileHelper.writeFile(testFile, 'updated')
|
|
241
|
+
|
|
242
|
+
// Read again
|
|
243
|
+
const updated = await fileHelper.readFile(testFile)
|
|
244
|
+
expect(updated).toBe('updated')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('should create nested structure and write files', async () => {
|
|
248
|
+
const nestedDir = path.join(testDir, 'level1', 'level2', 'level3')
|
|
249
|
+
const nestedFile = path.join(nestedDir, 'deep.json')
|
|
250
|
+
|
|
251
|
+
await fileHelper.ensureDir(nestedDir)
|
|
252
|
+
await fileHelper.writeJson(nestedFile, { deep: true })
|
|
253
|
+
|
|
254
|
+
const result = await fileHelper.readJson(nestedFile)
|
|
255
|
+
expect(result).toEqual({ deep: true })
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
})
|
package/core/commands.js
CHANGED
|
@@ -2632,8 +2632,6 @@ Agent: ${agent}
|
|
|
2632
2632
|
const skipped = []
|
|
2633
2633
|
|
|
2634
2634
|
for (const projectId of projectIds) {
|
|
2635
|
-
const globalProjectPath = path.join(globalRoot, projectId)
|
|
2636
|
-
|
|
2637
2635
|
// Read global config to get project path
|
|
2638
2636
|
const globalConfig = await configManager.readGlobalConfig(projectId)
|
|
2639
2637
|
if (!globalConfig || !globalConfig.projectPath) {
|
|
@@ -2704,7 +2702,6 @@ Agent: ${agent}
|
|
|
2704
2702
|
console.log('🏗️ Architect Mode - Code Generation\n')
|
|
2705
2703
|
|
|
2706
2704
|
const globalPath = await this.getGlobalProjectPath(projectPath)
|
|
2707
|
-
const architectSession = require('./domain/architect-session')
|
|
2708
2705
|
|
|
2709
2706
|
// Check if there's a completed plan
|
|
2710
2707
|
const planPath = path.join(globalPath, 'planning', 'architect-session.md')
|
|
@@ -79,6 +79,7 @@ class AgentDetector {
|
|
|
79
79
|
return {
|
|
80
80
|
type: 'claude',
|
|
81
81
|
name: 'Claude (Code + Desktop)',
|
|
82
|
+
isSupported: true,
|
|
82
83
|
capabilities: {
|
|
83
84
|
mcp: true,
|
|
84
85
|
filesystem: 'mcp',
|
|
@@ -112,6 +113,7 @@ class AgentDetector {
|
|
|
112
113
|
return {
|
|
113
114
|
type: 'terminal',
|
|
114
115
|
name: 'Terminal/CLI',
|
|
116
|
+
isSupported: true,
|
|
115
117
|
capabilities: {
|
|
116
118
|
mcp: false,
|
|
117
119
|
filesystem: 'native',
|
|
@@ -70,6 +70,8 @@ async function readFile(filePath, defaultValue = '') {
|
|
|
70
70
|
* @returns {Promise<void>}
|
|
71
71
|
*/
|
|
72
72
|
async function writeFile(filePath, content) {
|
|
73
|
+
const dir = path.dirname(filePath)
|
|
74
|
+
await fs.mkdir(dir, { recursive: true })
|
|
73
75
|
await fs.writeFile(filePath, content, 'utf-8')
|
|
74
76
|
}
|
|
75
77
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prjct-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Built for Claude - Ship fast, track progress, stay focused. Developer momentum tool for indie hackers.",
|
|
5
5
|
"main": "core/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"install-global": "./scripts/install.sh",
|
|
15
|
-
"test": "
|
|
15
|
+
"test": "vitest run --workspace=vitest.workspace.js",
|
|
16
|
+
"test:watch": "vitest --workspace=vitest.workspace.js",
|
|
17
|
+
"test:coverage": "vitest run --coverage --workspace=vitest.workspace.js",
|
|
16
18
|
"validate": "node scripts/validate-commands.js",
|
|
17
19
|
"lint": "eslint \"**/*.js\" --ignore-pattern \"node_modules/**\" --ignore-pattern \"website/**\"",
|
|
18
20
|
"lint:fix": "eslint \"**/*.js\" --fix --ignore-pattern \"node_modules/**\" --ignore-pattern \"website/**\"",
|
|
@@ -43,15 +45,18 @@
|
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
47
|
"@types/node": "^20.0.0",
|
|
48
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
46
49
|
"eslint": "^8.57.1",
|
|
47
50
|
"eslint-config-prettier": "^10.1.8",
|
|
48
51
|
"eslint-config-standard": "^17.1.0",
|
|
49
52
|
"eslint-plugin-import": "^2.32.0",
|
|
50
53
|
"eslint-plugin-n": "^16.6.2",
|
|
51
54
|
"eslint-plugin-promise": "^6.6.0",
|
|
55
|
+
"jsdom": "^27.0.0",
|
|
52
56
|
"prettier": "^3.6.2",
|
|
53
57
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
54
|
-
"typescript": "^5.0.0"
|
|
58
|
+
"typescript": "^5.0.0",
|
|
59
|
+
"vitest": "^3.2.4"
|
|
55
60
|
},
|
|
56
61
|
"repository": {
|
|
57
62
|
"type": "git",
|