@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.
@@ -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.setLabelStatus('rp', 'Train AI', isRpTraining)
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 askForTrainingToken() {
374
- if (this.trainingTokenPopup) {
375
- return
376
- }
380
+ public async showAlert(options: { title: string; message: string }) {
381
+ const { title, message } = options
377
382
 
378
- this.trainingTokenPopup = this.widgets.Widget('popup', {
379
- parent: this.window,
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
- this.widgets.Widget('text', {
387
- parent: this.trainingTokenPopup,
388
- left: 4,
389
- top: 3,
390
- height: 4,
391
- width: this.trainingTokenPopup.getFrame().width - 2,
392
- text: 'Coming soon...',
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
- const button = this.widgets.Widget('button', {
396
- parent: this.trainingTokenPopup,
397
- left: 20,
398
- top: 7,
399
- text: ' Ok ',
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
- await button.on('click', async () => {
403
- await this.trainingTokenPopup?.destroy()
404
- delete this.trainingTokenPopup
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 command = `node --experimental-vm-modules --unhandled-rejections=strict ${debugArgs} ${jestPath} --reporters="@sprucelabs/jest-json-reporter" --testRunner="jest-circus/runner" --passWithNoTests ${
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
- // this.isRpTraining = !this.isRpTraining
251
- // this.testReporter?.setIsRpTraining(this.isRpTraining)
252
- // if (this.isRpTraining) {
253
- await this.testReporter?.askForTrainingToken()
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
- return this.packageManager.installDependencies(pkg, options)
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() {
@@ -138,6 +138,7 @@ export default abstract class AbstractCliTest extends AbstractSpruceTest {
138
138
  }
139
139
 
140
140
  if (
141
+ this.homeDir &&
141
142
  diskUtil.doesDirExist(this.homeDir) &&
142
143
  testUtil.shouldClearCache()
143
144
  ) {