@sprucelabs/spruce-cli 29.1.2 → 29.2.1
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 +19 -0
- package/build/__tests__/behavioral/upgrading/UpgradingAMonorepo.test.d.ts +26 -0
- package/build/__tests__/behavioral/upgrading/UpgradingAMonorepo.test.js +135 -0
- package/build/__tests__/behavioral/upgrading/UpgradingAMonorepo.test.js.map +1 -0
- package/build/features/test/TestReporter.d.ts +6 -2
- package/build/features/test/TestReporter.js +159 -51
- package/build/features/test/TestReporter.js.map +1 -1
- package/build/features/test/TestRunner.d.ts +1 -0
- package/build/features/test/TestRunner.js +4 -1
- package/build/features/test/TestRunner.js.map +1 -1
- package/build/features/test/actions/TestAction.d.ts +1 -0
- package/build/features/test/actions/TestAction.js +63 -6
- package/build/features/test/actions/TestAction.js.map +1 -1
- package/build/services/PkgService.d.ts +4 -0
- package/build/services/PkgService.js +39 -1
- package/build/services/PkgService.js.map +1 -1
- package/build/tests/AbstractCliTest.js +2 -1
- package/build/tests/AbstractCliTest.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/behavioral/upgrading/UpgradingAMonorepo.test.ts +166 -0
- package/src/features/test/TestReporter.ts +143 -52
- package/src/features/test/TestRunner.ts +5 -1
- package/src/features/test/actions/TestAction.ts +88 -6
- package/src/services/PkgService.ts +47 -1
- package/src/tests/AbstractCliTest.ts +1 -0
|
@@ -53,10 +53,7 @@ export default class TestReporter {
|
|
|
53
53
|
private handleToggleSmartWatch?: () => any
|
|
54
54
|
private minWidth = 50
|
|
55
55
|
private isRpTraining: boolean
|
|
56
|
-
private trainingTokenPopup?: PopupWidget
|
|
57
56
|
private shouldStripCwdFromErrors = true
|
|
58
|
-
// private orientationWhenErrorLogWasShown: TestReporterOrientation =
|
|
59
|
-
// 'landscape'
|
|
60
57
|
|
|
61
58
|
public constructor(options?: TestReporterOptions) {
|
|
62
59
|
this.cwd = options?.cwd
|
|
@@ -92,10 +89,20 @@ export default class TestReporter {
|
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
public setIsRpTraining(isRpTraining: boolean) {
|
|
95
|
-
this.
|
|
92
|
+
this.setRpTrainingStatus(isRpTraining ? 'on' : 'off')
|
|
96
93
|
this.isRpTraining = isRpTraining
|
|
97
94
|
}
|
|
98
95
|
|
|
96
|
+
public setRpTrainingStatus(status: 'off' | 'installing' | 'on') {
|
|
97
|
+
const colors: Record<string, { fg: string; bg: string }> = {
|
|
98
|
+
off: { fg: 'w', bg: 'r' },
|
|
99
|
+
installing: { fg: 'k', bg: 'y' },
|
|
100
|
+
on: { fg: 'k', bg: 'g' },
|
|
101
|
+
}
|
|
102
|
+
const { fg, bg } = colors[status]
|
|
103
|
+
this.menu.setTextForItem('rp', `Train AI ^${fg}^#^${bg} • ^`)
|
|
104
|
+
}
|
|
105
|
+
|
|
99
106
|
public startCountdownTimer(durationSec: number) {
|
|
100
107
|
clearInterval(this.countDownTimeInterval)
|
|
101
108
|
this.countDownTimeInterval = undefined
|
|
@@ -370,38 +377,142 @@ export default class TestReporter {
|
|
|
370
377
|
}
|
|
371
378
|
}
|
|
372
379
|
|
|
373
|
-
public async
|
|
374
|
-
|
|
375
|
-
return
|
|
376
|
-
}
|
|
380
|
+
public async showAlert(options: { title: string; message: string }) {
|
|
381
|
+
const { title, message } = options
|
|
377
382
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
top: 10,
|
|
381
|
-
left: 10,
|
|
382
|
-
width: 50,
|
|
383
|
-
height: 10,
|
|
384
|
-
})
|
|
383
|
+
const windowFrame = this.window.getFrame()
|
|
384
|
+
const popupHeight = Math.min(windowFrame.height - 4, 25)
|
|
385
385
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
386
|
+
return new Promise<void>((resolve) => {
|
|
387
|
+
const popup = this.widgets.Widget('popup', {
|
|
388
|
+
parent: this.window,
|
|
389
|
+
top: 2,
|
|
390
|
+
left: 4,
|
|
391
|
+
width: Math.min(windowFrame.width - 8, 80),
|
|
392
|
+
height: popupHeight,
|
|
393
|
+
})
|
|
394
394
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
395
|
+
this.widgets.Widget('text', {
|
|
396
|
+
parent: popup,
|
|
397
|
+
left: 2,
|
|
398
|
+
top: 1,
|
|
399
|
+
height: 1,
|
|
400
|
+
width: popup.getFrame().width - 4,
|
|
401
|
+
text: title,
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
this.widgets.Widget('text', {
|
|
405
|
+
parent: popup,
|
|
406
|
+
left: 2,
|
|
407
|
+
top: 3,
|
|
408
|
+
height: popupHeight - 7,
|
|
409
|
+
width: popup.getFrame().width - 4,
|
|
410
|
+
text: message,
|
|
411
|
+
isScrollEnabled: true,
|
|
412
|
+
wordWrap: true,
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const okButton = this.widgets.Widget('button', {
|
|
416
|
+
parent: popup,
|
|
417
|
+
left: Math.floor(popup.getFrame().width / 2) - 4,
|
|
418
|
+
top: popupHeight - 3,
|
|
419
|
+
text: ' OK ',
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
void okButton.on('click', () => {
|
|
423
|
+
void popup.destroy()
|
|
424
|
+
resolve()
|
|
425
|
+
})
|
|
400
426
|
})
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
public async askForProjectName(
|
|
430
|
+
defaultName: string
|
|
431
|
+
): Promise<string | undefined> {
|
|
432
|
+
return new Promise((resolve) => {
|
|
433
|
+
const popup = this.widgets.Widget('popup', {
|
|
434
|
+
parent: this.window,
|
|
435
|
+
top: 5,
|
|
436
|
+
left: 8,
|
|
437
|
+
width: 65,
|
|
438
|
+
height: 15,
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
this.widgets.Widget('text', {
|
|
442
|
+
parent: popup,
|
|
443
|
+
left: 2,
|
|
444
|
+
top: 1,
|
|
445
|
+
height: 1,
|
|
446
|
+
width: popup.getFrame().width - 4,
|
|
447
|
+
text: 'regressionproof.ai',
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
this.widgets.Widget('text', {
|
|
451
|
+
parent: popup,
|
|
452
|
+
left: 2,
|
|
453
|
+
top: 3,
|
|
454
|
+
height: 4,
|
|
455
|
+
width: popup.getFrame().width - 4,
|
|
456
|
+
text: "Help Spruce train coding agents on how to do proper TDD.\nIf you contribute to this effort, you'll get a lifetime\nlicense to the agents for free.",
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
const input = this.widgets.Widget('input', {
|
|
460
|
+
parent: popup,
|
|
461
|
+
left: 2,
|
|
462
|
+
top: 8,
|
|
463
|
+
label: 'Name',
|
|
464
|
+
width: popup.getFrame().width - 6,
|
|
465
|
+
height: 1,
|
|
466
|
+
value: defaultName,
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
const learnMoreButton = this.widgets.Widget('button', {
|
|
470
|
+
parent: popup,
|
|
471
|
+
left: 2,
|
|
472
|
+
top: 11,
|
|
473
|
+
text: ' Learn more ',
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
const cancelButton = this.widgets.Widget('button', {
|
|
477
|
+
parent: popup,
|
|
478
|
+
left: 40,
|
|
479
|
+
top: 11,
|
|
480
|
+
text: ' Cancel ',
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
const enableButton = this.widgets.Widget('button', {
|
|
484
|
+
parent: popup,
|
|
485
|
+
left: 50,
|
|
486
|
+
top: 11,
|
|
487
|
+
text: ' Enable ',
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
void learnMoreButton.on('click', async () => {
|
|
491
|
+
const open = await import('open')
|
|
492
|
+
void open.default('https://regressionproof.ai')
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
void enableButton.on('click', () => {
|
|
496
|
+
const value = input.getValue()
|
|
497
|
+
void popup.destroy()
|
|
498
|
+
resolve(value || undefined)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
void cancelButton.on('click', () => {
|
|
502
|
+
void popup.destroy()
|
|
503
|
+
resolve(undefined)
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
void input.on('submit', () => {
|
|
507
|
+
const value = input.getValue()
|
|
508
|
+
void popup.destroy()
|
|
509
|
+
resolve(value || undefined)
|
|
510
|
+
})
|
|
401
511
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
512
|
+
void input.on('cancel', () => {
|
|
513
|
+
void popup.destroy()
|
|
514
|
+
resolve(undefined)
|
|
515
|
+
})
|
|
405
516
|
})
|
|
406
517
|
}
|
|
407
518
|
|
|
@@ -638,7 +749,6 @@ export default class TestReporter {
|
|
|
638
749
|
this.testLog.setText(logContent)
|
|
639
750
|
|
|
640
751
|
if (!errorContent) {
|
|
641
|
-
// this.errorLog && this.destroyErrorLog()
|
|
642
752
|
this.errorLog?.setText(' Nothing to report...')
|
|
643
753
|
} else {
|
|
644
754
|
!this.errorLog && this.dropInErrorLog()
|
|
@@ -678,8 +788,6 @@ export default class TestReporter {
|
|
|
678
788
|
}
|
|
679
789
|
|
|
680
790
|
private dropInErrorLog() {
|
|
681
|
-
// this.orientationWhenErrorLogWasShown = this.orientation
|
|
682
|
-
|
|
683
791
|
if (this.bottomLayout.getRows().length === 1) {
|
|
684
792
|
if (this.orientation === 'portrait') {
|
|
685
793
|
this.bottomLayout.addRow({
|
|
@@ -719,24 +827,7 @@ export default class TestReporter {
|
|
|
719
827
|
}
|
|
720
828
|
}
|
|
721
829
|
|
|
722
|
-
private destroyErrorLog() {
|
|
723
|
-
// if (this.errorLog) {
|
|
724
|
-
// void this.errorLog?.destroy()
|
|
725
|
-
// this.errorLog = undefined
|
|
726
|
-
// if (this.orientationWhenErrorLogWasShown === 'landscape') {
|
|
727
|
-
// this.bottomLayout.removeColumn(0, 1)
|
|
728
|
-
// this.bottomLayout.setColumnWidth({
|
|
729
|
-
// rowIdx: 0,
|
|
730
|
-
// columnIdx: 0,
|
|
731
|
-
// width: '100%',
|
|
732
|
-
// })
|
|
733
|
-
// } else {
|
|
734
|
-
// this.bottomLayout.removeRow(1)
|
|
735
|
-
// this.bottomLayout.setRowHeight(0, '100%')
|
|
736
|
-
// }
|
|
737
|
-
// this.bottomLayout.updateLayout()
|
|
738
|
-
// }
|
|
739
|
-
}
|
|
830
|
+
private destroyErrorLog() {}
|
|
740
831
|
|
|
741
832
|
private updateProgressBar(results: SpruceTestResults) {
|
|
742
833
|
if (results.totalTestFilesComplete ?? 0 > 0) {
|
|
@@ -29,6 +29,7 @@ export default class TestRunner extends AbstractEventEmitter<TestRunnerContract>
|
|
|
29
29
|
public async run(options?: {
|
|
30
30
|
pattern?: string | null
|
|
31
31
|
debugPort?: number | null
|
|
32
|
+
isRpTraining?: boolean
|
|
32
33
|
}): Promise<SpruceTestResults & { wasKilled: boolean }> {
|
|
33
34
|
this.wasKilled = false
|
|
34
35
|
|
|
@@ -45,7 +46,10 @@ export default class TestRunner extends AbstractEventEmitter<TestRunnerContract>
|
|
|
45
46
|
'"'
|
|
46
47
|
)
|
|
47
48
|
}
|
|
48
|
-
const
|
|
49
|
+
const rpReporter = options?.isRpTraining
|
|
50
|
+
? ' --reporters="@regressionproof/jest-reporter"'
|
|
51
|
+
: ''
|
|
52
|
+
const command = `node --experimental-vm-modules --unhandled-rejections=strict ${debugArgs} ${jestPath} --reporters="@sprucelabs/jest-json-reporter"${rpReporter} --testRunner="jest-circus/runner" --passWithNoTests ${
|
|
49
53
|
pattern ? escapeShell(pattern) : ''
|
|
50
54
|
}`
|
|
51
55
|
|
|
@@ -50,11 +50,17 @@ export default class TestAction extends AbstractAction<OptionsSchema> {
|
|
|
50
50
|
public async execute(
|
|
51
51
|
options: SchemaValues<OptionsSchema>
|
|
52
52
|
): Promise<FeatureActionResponse> {
|
|
53
|
+
const settings = this.Service('settings')
|
|
54
|
+
|
|
53
55
|
if (!options.watchMode) {
|
|
54
|
-
const settings = this.Service('settings')
|
|
55
56
|
options.watchMode = settings.get('test.watchMode') ?? 'off'
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
const rpSettings = settings.get('regressionproof') as
|
|
60
|
+
| { enabled: boolean; projectName: string }
|
|
61
|
+
| undefined
|
|
62
|
+
this.isRpTraining = rpSettings?.enabled ?? false
|
|
63
|
+
|
|
58
64
|
const normalizedOptions = this.validateAndNormalizeOptions(options)
|
|
59
65
|
|
|
60
66
|
const {
|
|
@@ -247,11 +253,86 @@ export default class TestAction extends AbstractAction<OptionsSchema> {
|
|
|
247
253
|
}
|
|
248
254
|
|
|
249
255
|
private async handleToggleRpTraining() {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
const settings = this.Service('settings')
|
|
257
|
+
const rpSettings = settings.get('regressionproof') as
|
|
258
|
+
| { enabled: boolean; projectName: string }
|
|
259
|
+
| undefined
|
|
260
|
+
|
|
261
|
+
if (!rpSettings) {
|
|
262
|
+
const defaultName = await this.getDefaultProjectName()
|
|
263
|
+
const projectName =
|
|
264
|
+
await this.testReporter?.askForProjectName(defaultName)
|
|
265
|
+
|
|
266
|
+
if (projectName) {
|
|
267
|
+
try {
|
|
268
|
+
this.testReporter?.setRpTrainingStatus('installing')
|
|
269
|
+
this.testReporter?.setStatusLabel(
|
|
270
|
+
'Installing regressionproof packages...'
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
const pkgService = this.Service('pkg')
|
|
274
|
+
await pkgService.install(
|
|
275
|
+
[
|
|
276
|
+
'@regressionproof/cli',
|
|
277
|
+
'@regressionproof/jest-reporter',
|
|
278
|
+
],
|
|
279
|
+
{ isDev: true }
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
this.testReporter?.setStatusLabel(
|
|
283
|
+
'Initializing regressionproof...'
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
const commandService = this.Service('command')
|
|
287
|
+
await commandService.execute(
|
|
288
|
+
`node node_modules/@regressionproof/cli/build/cli.js init ${projectName}`,
|
|
289
|
+
{ ignoreErrors: true }
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
settings.set('regressionproof', {
|
|
293
|
+
enabled: true,
|
|
294
|
+
projectName: projectName.trim(),
|
|
295
|
+
})
|
|
296
|
+
this.isRpTraining = true
|
|
297
|
+
this.testReporter?.setIsRpTraining(true)
|
|
298
|
+
this.testReporter?.setStatusLabel('')
|
|
299
|
+
} catch (err: any) {
|
|
300
|
+
this.testReporter?.setRpTrainingStatus('off')
|
|
301
|
+
const message = err?.message ?? 'Unknown error'
|
|
302
|
+
this.testReporter?.setStatusLabel('')
|
|
303
|
+
await this.testReporter?.showAlert({
|
|
304
|
+
title: 'Train AI Setup Failed',
|
|
305
|
+
message: `Could not set up regressionproof:\n\n${message}`,
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
const newEnabled = !rpSettings.enabled
|
|
311
|
+
settings.set('regressionproof', {
|
|
312
|
+
...rpSettings,
|
|
313
|
+
enabled: newEnabled,
|
|
314
|
+
})
|
|
315
|
+
this.isRpTraining = newEnabled
|
|
316
|
+
this.testReporter?.setIsRpTraining(newEnabled)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private async getDefaultProjectName(): Promise<string> {
|
|
321
|
+
try {
|
|
322
|
+
const commandService = this.Service('command')
|
|
323
|
+
const result = await commandService.execute(
|
|
324
|
+
'git remote get-url origin',
|
|
325
|
+
{ ignoreErrors: true }
|
|
326
|
+
)
|
|
327
|
+
if (result.stdout) {
|
|
328
|
+
const match = result.stdout.match(/\/([^/]+?)(?:\.git)?$/)
|
|
329
|
+
if (match) {
|
|
330
|
+
return match[1].replace('.git', '')
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} catch {}
|
|
334
|
+
|
|
335
|
+
return pathUtil.basename(this.cwd)
|
|
255
336
|
}
|
|
256
337
|
|
|
257
338
|
public setWatchMode(mode: WatchMode) {
|
|
@@ -381,6 +462,7 @@ export default class TestAction extends AbstractAction<OptionsSchema> {
|
|
|
381
462
|
let testResults: SpruceTestResults = await this.testRunner.run({
|
|
382
463
|
pattern: this.pattern,
|
|
383
464
|
debugPort: this.inspect,
|
|
465
|
+
isRpTraining: this.isRpTraining,
|
|
384
466
|
})
|
|
385
467
|
|
|
386
468
|
if (
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fsUtil from 'fs'
|
|
2
|
+
import pathUtil from 'path'
|
|
1
3
|
import {
|
|
2
4
|
diskUtil,
|
|
3
5
|
PkgService as BasePkgService,
|
|
@@ -25,7 +27,51 @@ export default class PkgService extends BasePkgService {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
public async install(pkg?: string[] | string, options?: AddOptions) {
|
|
28
|
-
|
|
30
|
+
const filtered = this.filterOutWorkspaceSiblings(pkg)
|
|
31
|
+
if (Array.isArray(filtered) && filtered.length === 0) {
|
|
32
|
+
return { totalInstalled: 0 }
|
|
33
|
+
}
|
|
34
|
+
return this.packageManager.installDependencies(filtered, options)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private filterOutWorkspaceSiblings(
|
|
38
|
+
pkg?: string[] | string
|
|
39
|
+
): string[] | string | undefined {
|
|
40
|
+
if (!pkg) {
|
|
41
|
+
return pkg
|
|
42
|
+
}
|
|
43
|
+
const siblings = this.getWorkspaceSiblingNames()
|
|
44
|
+
const packages = Array.isArray(pkg) ? pkg : [pkg]
|
|
45
|
+
const filtered = packages.filter((p) => !siblings.includes(p))
|
|
46
|
+
return Array.isArray(pkg) ? filtered : filtered[0]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private getWorkspaceSiblingNames(): string[] {
|
|
50
|
+
const packagesDir = pathUtil.dirname(this.cwd)
|
|
51
|
+
if (pathUtil.basename(packagesDir) !== 'packages') {
|
|
52
|
+
return []
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const monorepoRoot = pathUtil.dirname(packagesDir)
|
|
56
|
+
const rootPkgPath = pathUtil.join(monorepoRoot, 'package.json')
|
|
57
|
+
if (!diskUtil.doesFileExist(rootPkgPath)) {
|
|
58
|
+
return []
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const siblingDirs = fsUtil.readdirSync(packagesDir)
|
|
62
|
+
const names: string[] = []
|
|
63
|
+
|
|
64
|
+
for (const dir of siblingDirs) {
|
|
65
|
+
const pkgPath = pathUtil.join(packagesDir, dir, 'package.json')
|
|
66
|
+
if (diskUtil.doesFileExist(pkgPath)) {
|
|
67
|
+
const siblingPkg = JSON.parse(diskUtil.readFile(pkgPath))
|
|
68
|
+
if (siblingPkg.name) {
|
|
69
|
+
names.push(siblingPkg.name)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return names
|
|
29
75
|
}
|
|
30
76
|
|
|
31
77
|
public getSkillNamespace() {
|