prjct-cli 0.7.2 → 0.8.0

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.
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ describe('Vitest Setup', () => {
4
+ it('should pass basic assertion', () => {
5
+ expect(true).toBe(true)
6
+ })
7
+
8
+ it('should handle numbers', () => {
9
+ expect(2 + 2).toBe(4)
10
+ })
11
+
12
+ it('should handle strings', () => {
13
+ expect('prjct-cli').toContain('prjct')
14
+ })
15
+ })
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import * as dateHelper from '../../utils/date-helper.js'
3
+
4
+ describe('Date Helper', () => {
5
+ describe('formatDate()', () => {
6
+ it('should format date to YYYY-MM-DD', () => {
7
+ const date = new Date(2025, 9, 4, 12, 0, 0) // October 4, 2025
8
+ const formatted = dateHelper.formatDate(date)
9
+
10
+ expect(formatted).toMatch(/^\d{4}-\d{2}-\d{2}$/)
11
+ })
12
+
13
+ it('should pad single digit month', () => {
14
+ const date = new Date(2025, 2, 15) // March 15, 2025
15
+ const formatted = dateHelper.formatDate(date)
16
+
17
+ expect(formatted).toContain('-03-')
18
+ })
19
+
20
+ it('should pad single digit day', () => {
21
+ const date = new Date(2025, 9, 5) // October 5, 2025
22
+ const formatted = dateHelper.formatDate(date)
23
+
24
+ expect(formatted).toContain('-05')
25
+ })
26
+
27
+ it('should handle edge case dates', () => {
28
+ const date = new Date(2025, 0, 1) // January 1, 2025
29
+ const formatted = dateHelper.formatDate(date)
30
+
31
+ expect(formatted).toBe('2025-01-01')
32
+ })
33
+
34
+ it('should handle end of year', () => {
35
+ const date = new Date(2025, 11, 31) // December 31, 2025
36
+ const formatted = dateHelper.formatDate(date)
37
+
38
+ expect(formatted).toBe('2025-12-31')
39
+ })
40
+ })
41
+
42
+ describe('formatMonth()', () => {
43
+ it('should format date to YYYY-MM', () => {
44
+ const date = new Date('2025-10-04')
45
+ const formatted = dateHelper.formatMonth(date)
46
+
47
+ expect(formatted).toMatch(/^\d{4}-\d{2}$/)
48
+ expect(formatted).toBe('2025-10')
49
+ })
50
+
51
+ it('should pad single digit month', () => {
52
+ const date = new Date('2025-03-15')
53
+ const formatted = dateHelper.formatMonth(date)
54
+
55
+ expect(formatted).toBe('2025-03')
56
+ })
57
+
58
+ it('should handle January', () => {
59
+ const date = new Date('2025-01-15')
60
+ const formatted = dateHelper.formatMonth(date)
61
+
62
+ expect(formatted).toBe('2025-01')
63
+ })
64
+
65
+ it('should handle December', () => {
66
+ const date = new Date('2025-12-25')
67
+ const formatted = dateHelper.formatMonth(date)
68
+
69
+ expect(formatted).toBe('2025-12')
70
+ })
71
+ })
72
+
73
+ describe('getTodayKey()', () => {
74
+ it('should return today in YYYY-MM-DD format', () => {
75
+ const today = dateHelper.getTodayKey()
76
+
77
+ expect(today).toMatch(/^\d{4}-\d{2}-\d{2}$/)
78
+ })
79
+
80
+ it('should match formatDate for current date', () => {
81
+ const today = dateHelper.getTodayKey()
82
+ const now = dateHelper.formatDate(new Date())
83
+
84
+ expect(today).toBe(now)
85
+ })
86
+ })
87
+
88
+ describe('getDateKey()', () => {
89
+ it('should return date key in YYYY-MM-DD format', () => {
90
+ const date = new Date(2025, 9, 4) // October 4, 2025
91
+ const key = dateHelper.getDateKey(date)
92
+
93
+ expect(key).toBe('2025-10-04')
94
+ })
95
+
96
+ it('should be alias for formatDate', () => {
97
+ const date = new Date(2025, 9, 4) // October 4, 2025
98
+
99
+ expect(dateHelper.getDateKey(date)).toBe(dateHelper.formatDate(date))
100
+ })
101
+ })
102
+
103
+ describe('getTimestamp()', () => {
104
+ it('should return ISO timestamp', () => {
105
+ const timestamp = dateHelper.getTimestamp()
106
+
107
+ expect(timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
108
+ })
109
+
110
+ it('should be valid date string', () => {
111
+ const timestamp = dateHelper.getTimestamp()
112
+ const date = new Date(timestamp)
113
+
114
+ expect(date.toString()).not.toBe('Invalid Date')
115
+ })
116
+ })
117
+
118
+ describe('calculateDuration()', () => {
119
+ it('should calculate duration in minutes', () => {
120
+ const start = new Date('2025-10-04T10:00:00Z')
121
+ const end = new Date('2025-10-04T10:30:00Z')
122
+ const duration = dateHelper.calculateDuration(start, end)
123
+
124
+ expect(duration).toContain('30m')
125
+ })
126
+
127
+ it('should calculate duration in hours and minutes', () => {
128
+ const start = new Date('2025-10-04T10:00:00Z')
129
+ const end = new Date('2025-10-04T12:30:00Z')
130
+ const duration = dateHelper.calculateDuration(start, end)
131
+
132
+ expect(duration).toContain('2h')
133
+ expect(duration).toContain('30m')
134
+ })
135
+
136
+ it('should handle exact hours', () => {
137
+ const start = new Date('2025-10-04T10:00:00Z')
138
+ const end = new Date('2025-10-04T13:00:00Z')
139
+ const duration = dateHelper.calculateDuration(start, end)
140
+
141
+ expect(duration).toContain('3h')
142
+ })
143
+
144
+ it('should handle less than 1 minute', () => {
145
+ const start = new Date('2025-10-04T10:00:00Z')
146
+ const end = new Date('2025-10-04T10:00:30Z')
147
+ const duration = dateHelper.calculateDuration(start, end)
148
+
149
+ expect(duration).toContain('30s')
150
+ })
151
+ })
152
+
153
+ describe('Edge Cases', () => {
154
+ it('should handle leap year', () => {
155
+ const date = new Date(2024, 1, 29) // February 29, 2024
156
+ const formatted = dateHelper.formatDate(date)
157
+
158
+ expect(formatted).toBe('2024-02-29')
159
+ })
160
+
161
+ it('should handle different years', () => {
162
+ const date2024 = new Date(2024, 9, 4) // October 4, 2024
163
+ const date2025 = new Date(2025, 9, 4) // October 4, 2025
164
+
165
+ expect(dateHelper.formatDate(date2024)).toBe('2024-10-04')
166
+ expect(dateHelper.formatDate(date2025)).toBe('2025-10-04')
167
+ })
168
+ })
169
+ })
@@ -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
+ })
@@ -8,11 +8,11 @@
8
8
  * - CLAUDE.md (AI assistant instructions)
9
9
  * - scripts/validate-commands.js (validation)
10
10
  *
11
- * @version 0.6.0 - Simplified workflow with 9 core commands
11
+ * @version 0.8.0 - Conversational interface with zero memorization
12
12
  */
13
13
 
14
14
  const COMMANDS = [
15
- // ===== CORE WORKFLOW COMMANDS (9 essential) =====
15
+ // ===== CORE WORKFLOW COMMANDS (13 essential) =====
16
16
 
17
17
  // 1. Initialize
18
18
  {
@@ -267,7 +267,55 @@ const COMMANDS = [
267
267
  ],
268
268
  },
269
269
 
270
- // 11. Architect Execute
270
+ // 11. Ask - Intent to Action
271
+ {
272
+ name: 'ask',
273
+ category: 'core',
274
+ description: 'Conversational intent to action translator',
275
+ usage: {
276
+ claude: '/p:ask "what you want to do"',
277
+ terminal: 'prjct ask "what you want to do"',
278
+ },
279
+ params: '<description>',
280
+ implemented: true,
281
+ hasTemplate: true,
282
+ icon: 'MessageCircle',
283
+ requiresInit: false, // Can work before init to guide setup
284
+ blockingRules: null,
285
+ features: [
286
+ 'Natural language understanding',
287
+ 'Recommends command flow',
288
+ 'Educational explanations',
289
+ 'Interactive confirmation',
290
+ 'Works in any language',
291
+ ],
292
+ },
293
+
294
+ // 12. Suggest - Context-Aware Recommendations
295
+ {
296
+ name: 'suggest',
297
+ category: 'core',
298
+ description: 'Context-aware next steps suggestions',
299
+ usage: {
300
+ claude: '/p:suggest',
301
+ terminal: 'prjct suggest',
302
+ },
303
+ params: null,
304
+ implemented: true,
305
+ hasTemplate: true,
306
+ icon: 'Lightbulb',
307
+ requiresInit: true,
308
+ blockingRules: null,
309
+ features: [
310
+ 'Analyzes project state',
311
+ 'Recommends actions',
312
+ 'Urgency detection',
313
+ 'Momentum tracking',
314
+ 'Personalized suggestions',
315
+ ],
316
+ },
317
+
318
+ // 13. Architect Execute
271
319
  {
272
320
  name: 'architect',
273
321
  category: 'core',
@@ -446,7 +494,7 @@ const CATEGORIES = {
446
494
  core: {
447
495
  title: 'Core Workflow',
448
496
  icon: 'Zap',
449
- description: '9 essential commands for daily development workflow',
497
+ description: '13 essential commands for daily development workflow',
450
498
  order: 1,
451
499
  },
452
500
  optional: {
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')
@@ -1,4 +1,3 @@
1
- const fs = require('fs').promises
2
1
  const path = require('path')
3
2
  const pathManager = require('./path-manager')
4
3
  const { VERSION } = require('../utils/version')
@@ -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.2",
3
+ "version": "0.8.0",
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": "echo 'No tests configured'",
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",