prjct-cli 0.9.1 → 0.9.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.
@@ -0,0 +1,387 @@
1
+ import { describe, it, expect, beforeEach, afterEach } 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('JSONL Helper', () => {
10
+ let jsonlHelper
11
+ let testFilePath
12
+ let tempDir
13
+
14
+ beforeEach(async () => {
15
+ jsonlHelper = require('../../utils/jsonl-helper.js')
16
+
17
+ // Create temporary test directory
18
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'prjct-test-'))
19
+ testFilePath = path.join(tempDir, 'test.jsonl')
20
+ })
21
+
22
+ afterEach(async () => {
23
+ if (tempDir) {
24
+ try {
25
+ await fs.rm(tempDir, { recursive: true, force: true })
26
+ } catch (error) {
27
+ // Ignore cleanup errors
28
+ }
29
+ }
30
+ })
31
+
32
+ describe('parseJsonLines()', () => {
33
+ it('should parse valid JSONL content', () => {
34
+ const content = '{"ts":"2025-10-04T14:30:00Z","type":"test"}\n{"ts":"2025-10-04T15:00:00Z","type":"test2"}'
35
+ const parsed = jsonlHelper.parseJsonLines(content)
36
+
37
+ expect(parsed.length).toBe(2)
38
+ expect(parsed[0].type).toBe('test')
39
+ expect(parsed[1].type).toBe('test2')
40
+ })
41
+
42
+ it('should skip malformed lines', () => {
43
+ const content = '{"valid":true}\ninvalid json\n{"another":true}'
44
+ const parsed = jsonlHelper.parseJsonLines(content)
45
+
46
+ expect(parsed.length).toBe(2)
47
+ expect(parsed[0].valid).toBe(true)
48
+ expect(parsed[1].another).toBe(true)
49
+ })
50
+
51
+ it('should handle empty content', () => {
52
+ const parsed = jsonlHelper.parseJsonLines('')
53
+ expect(parsed).toEqual([])
54
+ })
55
+
56
+ it('should filter empty lines', () => {
57
+ const content = '{"a":1}\n\n{"b":2}\n \n{"c":3}'
58
+ const parsed = jsonlHelper.parseJsonLines(content)
59
+
60
+ expect(parsed.length).toBe(3)
61
+ })
62
+ })
63
+
64
+ describe('stringifyJsonLines()', () => {
65
+ it('should convert array to JSONL format', () => {
66
+ const objects = [
67
+ { ts: '2025-10-04T14:30:00Z', type: 'test' },
68
+ { ts: '2025-10-04T15:00:00Z', type: 'test2' }
69
+ ]
70
+
71
+ const jsonl = jsonlHelper.stringifyJsonLines(objects)
72
+ const lines = jsonl.trim().split('\n')
73
+
74
+ expect(lines.length).toBe(2)
75
+ expect(JSON.parse(lines[0]).type).toBe('test')
76
+ expect(JSON.parse(lines[1]).type).toBe('test2')
77
+ })
78
+
79
+ it('should end with newline', () => {
80
+ const objects = [{ a: 1 }]
81
+ const jsonl = jsonlHelper.stringifyJsonLines(objects)
82
+
83
+ expect(jsonl.endsWith('\n')).toBe(true)
84
+ })
85
+
86
+ it('should handle empty array', () => {
87
+ const jsonl = jsonlHelper.stringifyJsonLines([])
88
+ expect(jsonl).toBe('\n')
89
+ })
90
+ })
91
+
92
+ describe('readJsonLines()', () => {
93
+ it('should read and parse JSONL file', async () => {
94
+ const content = '{"a":1}\n{"b":2}'
95
+ await fs.writeFile(testFilePath, content)
96
+
97
+ const parsed = await jsonlHelper.readJsonLines(testFilePath)
98
+
99
+ expect(parsed.length).toBe(2)
100
+ expect(parsed[0].a).toBe(1)
101
+ expect(parsed[1].b).toBe(2)
102
+ })
103
+
104
+ it('should return empty array for non-existent file', async () => {
105
+ const parsed = await jsonlHelper.readJsonLines(path.join(tempDir, 'nonexistent.jsonl'))
106
+
107
+ expect(parsed).toEqual([])
108
+ })
109
+
110
+ it('should throw for other errors', async () => {
111
+ // Create directory with same name to cause error
112
+ await fs.mkdir(testFilePath, { recursive: true })
113
+
114
+ await expect(jsonlHelper.readJsonLines(testFilePath)).rejects.toThrow()
115
+ })
116
+ })
117
+
118
+ describe('writeJsonLines()', () => {
119
+ it('should write objects to JSONL file', async () => {
120
+ const objects = [
121
+ { ts: '2025-10-04T14:30:00Z', type: 'test' },
122
+ { ts: '2025-10-04T15:00:00Z', type: 'test2' }
123
+ ]
124
+
125
+ await jsonlHelper.writeJsonLines(testFilePath, objects)
126
+
127
+ const content = await fs.readFile(testFilePath, 'utf-8')
128
+ const parsed = jsonlHelper.parseJsonLines(content)
129
+
130
+ expect(parsed.length).toBe(2)
131
+ expect(parsed[0].type).toBe('test')
132
+ })
133
+
134
+ it('should overwrite existing file', async () => {
135
+ await fs.writeFile(testFilePath, '{"old":true}')
136
+
137
+ await jsonlHelper.writeJsonLines(testFilePath, [{ new: true }])
138
+
139
+ const content = await fs.readFile(testFilePath, 'utf-8')
140
+ const parsed = jsonlHelper.parseJsonLines(content)
141
+
142
+ expect(parsed.length).toBe(1)
143
+ expect(parsed[0].old).toBeUndefined()
144
+ expect(parsed[0].new).toBe(true)
145
+ })
146
+ })
147
+
148
+ describe('appendJsonLine()', () => {
149
+ it('should append single object to file', async () => {
150
+ await fs.writeFile(testFilePath, '{"first":true}\n')
151
+
152
+ await jsonlHelper.appendJsonLine(testFilePath, { second: true })
153
+
154
+ const content = await fs.readFile(testFilePath, 'utf-8')
155
+ const parsed = jsonlHelper.parseJsonLines(content)
156
+
157
+ expect(parsed.length).toBe(2)
158
+ expect(parsed[0].first).toBe(true)
159
+ expect(parsed[1].second).toBe(true)
160
+ })
161
+
162
+ it('should create file if it does not exist', async () => {
163
+ await jsonlHelper.appendJsonLine(testFilePath, { new: true })
164
+
165
+ const content = await fs.readFile(testFilePath, 'utf-8')
166
+ const parsed = jsonlHelper.parseJsonLines(content)
167
+
168
+ expect(parsed.length).toBe(1)
169
+ expect(parsed[0].new).toBe(true)
170
+ })
171
+ })
172
+
173
+ describe('appendJsonLines()', () => {
174
+ it('should append multiple objects', async () => {
175
+ await fs.writeFile(testFilePath, '{"first":true}\n')
176
+
177
+ await jsonlHelper.appendJsonLines(testFilePath, [
178
+ { second: true },
179
+ { third: true }
180
+ ])
181
+
182
+ const content = await fs.readFile(testFilePath, 'utf-8')
183
+ const parsed = jsonlHelper.parseJsonLines(content)
184
+
185
+ expect(parsed.length).toBe(3)
186
+ })
187
+ })
188
+
189
+ describe('filterJsonLines()', () => {
190
+ it('should filter entries by predicate', async () => {
191
+ const objects = [
192
+ { type: 'test', value: 1 },
193
+ { type: 'other', value: 2 },
194
+ { type: 'test', value: 3 }
195
+ ]
196
+ await jsonlHelper.writeJsonLines(testFilePath, objects)
197
+
198
+ const filtered = await jsonlHelper.filterJsonLines(testFilePath, entry => entry.type === 'test')
199
+
200
+ expect(filtered.length).toBe(2)
201
+ expect(filtered.every(e => e.type === 'test')).toBe(true)
202
+ })
203
+
204
+ it('should return empty array for non-existent file', async () => {
205
+ const filtered = await jsonlHelper.filterJsonLines(
206
+ path.join(tempDir, 'nonexistent.jsonl'),
207
+ () => true
208
+ )
209
+
210
+ expect(filtered).toEqual([])
211
+ })
212
+ })
213
+
214
+ describe('countJsonLines()', () => {
215
+ it('should count valid lines in file', async () => {
216
+ await fs.writeFile(testFilePath, '{"a":1}\n{"b":2}\n{"c":3}')
217
+
218
+ const count = await jsonlHelper.countJsonLines(testFilePath)
219
+
220
+ expect(count).toBe(3)
221
+ })
222
+
223
+ it('should return 0 for non-existent file', async () => {
224
+ const count = await jsonlHelper.countJsonLines(path.join(tempDir, 'nonexistent.jsonl'))
225
+
226
+ expect(count).toBe(0)
227
+ })
228
+
229
+ it('should ignore empty lines', async () => {
230
+ await fs.writeFile(testFilePath, '{"a":1}\n\n{"b":2}\n \n')
231
+
232
+ const count = await jsonlHelper.countJsonLines(testFilePath)
233
+
234
+ expect(count).toBe(2)
235
+ })
236
+ })
237
+
238
+ describe('getLastJsonLines()', () => {
239
+ it('should return last N entries', async () => {
240
+ const objects = Array.from({ length: 10 }, (_, i) => ({ index: i }))
241
+ await jsonlHelper.writeJsonLines(testFilePath, objects)
242
+
243
+ const last3 = await jsonlHelper.getLastJsonLines(testFilePath, 3)
244
+
245
+ expect(last3.length).toBe(3)
246
+ expect(last3[0].index).toBe(7)
247
+ expect(last3[2].index).toBe(9)
248
+ })
249
+
250
+ it('should return all entries if N is larger than file', async () => {
251
+ const objects = [{ a: 1 }, { b: 2 }]
252
+ await jsonlHelper.writeJsonLines(testFilePath, objects)
253
+
254
+ const last = await jsonlHelper.getLastJsonLines(testFilePath, 10)
255
+
256
+ expect(last.length).toBe(2)
257
+ })
258
+ })
259
+
260
+ describe('getFirstJsonLines()', () => {
261
+ it('should return first N entries', async () => {
262
+ const objects = Array.from({ length: 10 }, (_, i) => ({ index: i }))
263
+ await jsonlHelper.writeJsonLines(testFilePath, objects)
264
+
265
+ const first3 = await jsonlHelper.getFirstJsonLines(testFilePath, 3)
266
+
267
+ expect(first3.length).toBe(3)
268
+ expect(first3[0].index).toBe(0)
269
+ expect(first3[2].index).toBe(2)
270
+ })
271
+ })
272
+
273
+ describe('mergeJsonLines()', () => {
274
+ it('should merge multiple files', async () => {
275
+ const file1 = path.join(tempDir, 'file1.jsonl')
276
+ const file2 = path.join(tempDir, 'file2.jsonl')
277
+
278
+ await jsonlHelper.writeJsonLines(file1, [{ file: 1 }])
279
+ await jsonlHelper.writeJsonLines(file2, [{ file: 2 }])
280
+
281
+ const merged = await jsonlHelper.mergeJsonLines([file1, file2])
282
+
283
+ expect(merged.length).toBe(2)
284
+ expect(merged[0].file).toBe(1)
285
+ expect(merged[1].file).toBe(2)
286
+ })
287
+
288
+ it('should handle non-existent files gracefully', async () => {
289
+ const file1 = path.join(tempDir, 'file1.jsonl')
290
+ await jsonlHelper.writeJsonLines(file1, [{ a: 1 }])
291
+
292
+ const merged = await jsonlHelper.mergeJsonLines([
293
+ file1,
294
+ path.join(tempDir, 'nonexistent.jsonl')
295
+ ])
296
+
297
+ expect(merged.length).toBe(1)
298
+ })
299
+ })
300
+
301
+ describe('isJsonLinesEmpty()', () => {
302
+ it('should return true for empty file', async () => {
303
+ await fs.writeFile(testFilePath, '')
304
+
305
+ const isEmpty = await jsonlHelper.isJsonLinesEmpty(testFilePath)
306
+
307
+ expect(isEmpty).toBe(true)
308
+ })
309
+
310
+ it('should return false for file with content', async () => {
311
+ await jsonlHelper.writeJsonLines(testFilePath, [{ a: 1 }])
312
+
313
+ const isEmpty = await jsonlHelper.isJsonLinesEmpty(testFilePath)
314
+
315
+ expect(isEmpty).toBe(false)
316
+ })
317
+
318
+ it('should return true for non-existent file', async () => {
319
+ const isEmpty = await jsonlHelper.isJsonLinesEmpty(path.join(tempDir, 'nonexistent.jsonl'))
320
+
321
+ expect(isEmpty).toBe(true)
322
+ })
323
+ })
324
+
325
+ describe('getFileSizeMB()', () => {
326
+ it('should return file size in MB', async () => {
327
+ const content = 'x'.repeat(1024 * 1024) // 1MB
328
+ await fs.writeFile(testFilePath, content)
329
+
330
+ const sizeMB = await jsonlHelper.getFileSizeMB(testFilePath)
331
+
332
+ expect(sizeMB).toBeGreaterThan(0.9)
333
+ expect(sizeMB).toBeLessThan(1.1)
334
+ })
335
+
336
+ it('should return 0 for non-existent file', async () => {
337
+ const sizeMB = await jsonlHelper.getFileSizeMB(path.join(tempDir, 'nonexistent.jsonl'))
338
+
339
+ expect(sizeMB).toBe(0)
340
+ })
341
+ })
342
+
343
+ describe('rotateJsonLinesIfNeeded()', () => {
344
+ it('should not rotate if file is small', async () => {
345
+ await jsonlHelper.writeJsonLines(testFilePath, [{ a: 1 }])
346
+
347
+ const rotated = await jsonlHelper.rotateJsonLinesIfNeeded(testFilePath, 10)
348
+
349
+ expect(rotated).toBe(false)
350
+ const exists = await fs.access(testFilePath).then(() => true).catch(() => false)
351
+ expect(exists).toBe(true)
352
+ })
353
+
354
+ it('should rotate if file exceeds size limit', async () => {
355
+ // Create a large file (simulate with many entries)
356
+ const largeContent = Array.from({ length: 10000 }, (_, i) => ({
357
+ index: i,
358
+ data: 'x'.repeat(100)
359
+ }))
360
+ await jsonlHelper.writeJsonLines(testFilePath, largeContent)
361
+
362
+ // Use very small limit to force rotation
363
+ const rotated = await jsonlHelper.rotateJsonLinesIfNeeded(testFilePath, 0.001)
364
+
365
+ // File should be rotated (moved to archive)
366
+ expect(rotated).toBe(true)
367
+ })
368
+ })
369
+
370
+ describe('appendJsonLineWithRotation()', () => {
371
+ it('should append and rotate if needed', async () => {
372
+ // Create large file
373
+ const largeContent = Array.from({ length: 10000 }, (_, i) => ({
374
+ index: i,
375
+ data: 'x'.repeat(100)
376
+ }))
377
+ await jsonlHelper.writeJsonLines(testFilePath, largeContent)
378
+
379
+ await jsonlHelper.appendJsonLineWithRotation(testFilePath, { new: true }, 0.001)
380
+
381
+ // File should exist (either original or after rotation)
382
+ const files = await fs.readdir(tempDir)
383
+ expect(files.length).toBeGreaterThan(0)
384
+ })
385
+ })
386
+ })
387
+
@@ -9,9 +9,7 @@
9
9
 
10
10
  const fs = require('fs').promises;
11
11
  const path = require('path');
12
- const glob = require('glob');
13
- const { promisify } = require('util');
14
- const globAsync = promisify(glob);
12
+ const { glob } = require('glob');
15
13
 
16
14
  class ContextFilter {
17
15
  constructor() {
@@ -445,14 +443,20 @@ class ContextFilter {
445
443
 
446
444
  // Execute glob searches
447
445
  for (const pattern of globPatterns) {
448
- const matches = await globAsync(pattern, {
446
+ const matches = await glob(pattern, {
449
447
  cwd: projectPath,
450
448
  ignore: patterns.exclude,
451
449
  nodir: true,
452
450
  follow: false
453
451
  });
454
452
 
455
- files.push(...matches);
453
+ // Ensure matches is always an array (glob v10+ returns array, but be defensive)
454
+ if (Array.isArray(matches)) {
455
+ files.push(...matches);
456
+ } else if (matches) {
457
+ // Convert iterable to array if needed
458
+ files.push(...Array.from(matches));
459
+ }
456
460
  }
457
461
 
458
462
  // Remove duplicates and sort
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
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": {
@@ -44,6 +44,7 @@
44
44
  "license": "MIT",
45
45
  "dependencies": {
46
46
  "chalk": "^4.1.2",
47
+ "glob": "^10.3.10",
47
48
  "prompts": "^2.4.2"
48
49
  },
49
50
  "devDependencies": {