@blockspool/cli 0.4.1 → 0.4.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.
Files changed (149) hide show
  1. package/dist/bin/blockspool.d.ts +16 -0
  2. package/dist/bin/blockspool.d.ts.map +1 -0
  3. package/dist/bin/blockspool.js +45 -0
  4. package/dist/bin/blockspool.js.map +1 -0
  5. package/dist/commands/solo-auto.d.ts +6 -0
  6. package/dist/commands/solo-auto.d.ts.map +1 -0
  7. package/dist/commands/solo-auto.js +418 -0
  8. package/dist/commands/solo-auto.js.map +1 -0
  9. package/dist/commands/solo-exec.d.ts +6 -0
  10. package/dist/commands/solo-exec.d.ts.map +1 -0
  11. package/dist/commands/solo-exec.js +656 -0
  12. package/dist/commands/solo-exec.js.map +1 -0
  13. package/dist/commands/solo-inspect.d.ts +6 -0
  14. package/dist/commands/solo-inspect.d.ts.map +1 -0
  15. package/dist/commands/solo-inspect.js +690 -0
  16. package/dist/commands/solo-inspect.js.map +1 -0
  17. package/dist/commands/solo-lifecycle.d.ts +6 -0
  18. package/dist/commands/solo-lifecycle.d.ts.map +1 -0
  19. package/dist/commands/solo-lifecycle.js +188 -0
  20. package/dist/commands/solo-lifecycle.js.map +1 -0
  21. package/dist/commands/solo-nudge.d.ts +6 -0
  22. package/dist/commands/solo-nudge.d.ts.map +1 -0
  23. package/dist/commands/solo-nudge.js +49 -0
  24. package/dist/commands/solo-nudge.js.map +1 -0
  25. package/dist/commands/solo-qa.d.ts +6 -0
  26. package/dist/commands/solo-qa.d.ts.map +1 -0
  27. package/dist/commands/solo-qa.js +254 -0
  28. package/dist/commands/solo-qa.js.map +1 -0
  29. package/dist/commands/solo.d.ts +11 -0
  30. package/dist/commands/solo.d.ts.map +1 -0
  31. package/dist/commands/solo.js +43 -0
  32. package/dist/commands/solo.js.map +1 -0
  33. package/dist/index.d.ts +18 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +18 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/lib/artifacts.d.ts +136 -0
  38. package/dist/lib/artifacts.d.ts.map +1 -0
  39. package/dist/lib/artifacts.js +146 -0
  40. package/dist/lib/artifacts.js.map +1 -0
  41. package/dist/lib/doctor.d.ts +45 -0
  42. package/dist/lib/doctor.d.ts.map +1 -0
  43. package/dist/lib/doctor.js +383 -0
  44. package/dist/lib/doctor.js.map +1 -0
  45. package/dist/lib/exec.d.ts +24 -0
  46. package/dist/lib/exec.d.ts.map +1 -0
  47. package/dist/lib/exec.js +295 -0
  48. package/dist/lib/exec.js.map +1 -0
  49. package/dist/lib/formulas.d.ts +78 -0
  50. package/dist/lib/formulas.d.ts.map +1 -0
  51. package/dist/lib/formulas.js +295 -0
  52. package/dist/lib/formulas.js.map +1 -0
  53. package/dist/lib/git.d.ts +9 -0
  54. package/dist/lib/git.d.ts.map +1 -0
  55. package/dist/lib/git.js +60 -0
  56. package/dist/lib/git.js.map +1 -0
  57. package/dist/lib/guidelines.d.ts +43 -0
  58. package/dist/lib/guidelines.d.ts.map +1 -0
  59. package/dist/lib/guidelines.js +195 -0
  60. package/dist/lib/guidelines.js.map +1 -0
  61. package/dist/lib/logger.d.ts +17 -0
  62. package/dist/lib/logger.d.ts.map +1 -0
  63. package/dist/lib/logger.js +42 -0
  64. package/dist/lib/logger.js.map +1 -0
  65. package/dist/lib/retention.d.ts +62 -0
  66. package/dist/lib/retention.d.ts.map +1 -0
  67. package/dist/lib/retention.js +285 -0
  68. package/dist/lib/retention.js.map +1 -0
  69. package/dist/lib/run-history.d.ts +52 -0
  70. package/dist/lib/run-history.d.ts.map +1 -0
  71. package/dist/lib/run-history.js +116 -0
  72. package/dist/lib/run-history.js.map +1 -0
  73. package/dist/lib/run-state.d.ts +58 -0
  74. package/dist/lib/run-state.d.ts.map +1 -0
  75. package/dist/lib/run-state.js +119 -0
  76. package/dist/lib/run-state.js.map +1 -0
  77. package/dist/lib/scope.d.ts +95 -0
  78. package/dist/lib/scope.d.ts.map +1 -0
  79. package/dist/lib/scope.js +291 -0
  80. package/dist/lib/scope.js.map +1 -0
  81. package/dist/lib/selection.d.ts +35 -0
  82. package/dist/lib/selection.d.ts.map +1 -0
  83. package/dist/lib/selection.js +110 -0
  84. package/dist/lib/selection.js.map +1 -0
  85. package/dist/lib/solo-auto.d.ts +87 -0
  86. package/dist/lib/solo-auto.d.ts.map +1 -0
  87. package/dist/lib/solo-auto.js +1230 -0
  88. package/dist/lib/solo-auto.js.map +1 -0
  89. package/dist/lib/solo-ci.d.ts +84 -0
  90. package/dist/lib/solo-ci.d.ts.map +1 -0
  91. package/dist/lib/solo-ci.js +300 -0
  92. package/dist/lib/solo-ci.js.map +1 -0
  93. package/dist/lib/solo-config.d.ts +155 -0
  94. package/dist/lib/solo-config.d.ts.map +1 -0
  95. package/dist/lib/solo-config.js +236 -0
  96. package/dist/lib/solo-config.js.map +1 -0
  97. package/dist/lib/solo-git.d.ts +44 -0
  98. package/dist/lib/solo-git.d.ts.map +1 -0
  99. package/dist/lib/solo-git.js +174 -0
  100. package/dist/lib/solo-git.js.map +1 -0
  101. package/dist/lib/solo-hints.d.ts +32 -0
  102. package/dist/lib/solo-hints.d.ts.map +1 -0
  103. package/dist/lib/solo-hints.js +98 -0
  104. package/dist/lib/solo-hints.js.map +1 -0
  105. package/dist/lib/solo-remote.d.ts +14 -0
  106. package/dist/lib/solo-remote.d.ts.map +1 -0
  107. package/dist/lib/solo-remote.js +48 -0
  108. package/dist/lib/solo-remote.js.map +1 -0
  109. package/dist/lib/solo-stdin.d.ts +13 -0
  110. package/dist/lib/solo-stdin.d.ts.map +1 -0
  111. package/dist/lib/solo-stdin.js +33 -0
  112. package/dist/lib/solo-stdin.js.map +1 -0
  113. package/dist/lib/solo-ticket.d.ts +213 -0
  114. package/dist/lib/solo-ticket.d.ts.map +1 -0
  115. package/dist/lib/solo-ticket.js +850 -0
  116. package/dist/lib/solo-ticket.js.map +1 -0
  117. package/dist/lib/solo-utils.d.ts +133 -0
  118. package/dist/lib/solo-utils.d.ts.map +1 -0
  119. package/dist/lib/solo-utils.js +300 -0
  120. package/dist/lib/solo-utils.js.map +1 -0
  121. package/dist/lib/spindle.d.ts +144 -0
  122. package/dist/lib/spindle.d.ts.map +1 -0
  123. package/dist/lib/spindle.js +388 -0
  124. package/dist/lib/spindle.js.map +1 -0
  125. package/dist/tui/app.d.ts +17 -0
  126. package/dist/tui/app.d.ts.map +1 -0
  127. package/dist/tui/app.js +139 -0
  128. package/dist/tui/app.js.map +1 -0
  129. package/dist/tui/index.d.ts +8 -0
  130. package/dist/tui/index.d.ts.map +1 -0
  131. package/dist/tui/index.js +7 -0
  132. package/dist/tui/index.js.map +1 -0
  133. package/dist/tui/poller.d.ts +42 -0
  134. package/dist/tui/poller.d.ts.map +1 -0
  135. package/dist/tui/poller.js +62 -0
  136. package/dist/tui/poller.js.map +1 -0
  137. package/dist/tui/screens/overview.d.ts +9 -0
  138. package/dist/tui/screens/overview.d.ts.map +1 -0
  139. package/dist/tui/screens/overview.js +189 -0
  140. package/dist/tui/screens/overview.js.map +1 -0
  141. package/dist/tui/state.d.ts +93 -0
  142. package/dist/tui/state.d.ts.map +1 -0
  143. package/dist/tui/state.js +169 -0
  144. package/dist/tui/state.js.map +1 -0
  145. package/dist/tui/types.d.ts +18 -0
  146. package/dist/tui/types.d.ts.map +1 -0
  147. package/dist/tui/types.js +5 -0
  148. package/dist/tui/types.js.map +1 -0
  149. package/package.json +1 -1
@@ -0,0 +1,690 @@
1
+ /**
2
+ * Solo inspect commands: scout, status, history, formulas, export, artifacts, approve
3
+ */
4
+ import * as path from 'node:path';
5
+ import * as fs from 'node:fs';
6
+ import chalk from 'chalk';
7
+ import { scoutRepo, approveProposals, } from '@blockspool/core/services';
8
+ import { projects, tickets, runs } from '@blockspool/core/repos';
9
+ import { createGitService } from '../lib/git.js';
10
+ import { writeJsonArtifact, getLatestArtifact, getAllArtifacts, getArtifactsForRun, getArtifactByRunId, } from '../lib/artifacts.js';
11
+ import { parseSelection } from '../lib/selection.js';
12
+ import { getBlockspoolDir, getDbPath, isInitialized, initSolo, getAdapter, createScoutDeps, formatProgress, displayProposal, } from '../lib/solo-config.js';
13
+ import { formatDuration, formatRelativeTime, } from '../lib/solo-utils.js';
14
+ export function registerInspectCommands(solo) {
15
+ /**
16
+ * solo scout - Scan codebase and create tickets
17
+ */
18
+ solo
19
+ .command('scout [path]')
20
+ .description('Scan a codebase and create improvement tickets')
21
+ .option('-v, --verbose', 'Show detailed output')
22
+ .option('-q, --quiet', 'Suppress non-essential output')
23
+ .option('--json', 'Output as JSON')
24
+ .option('-s, --scope <pattern>', 'Glob pattern for files to scan', 'src/**')
25
+ .option('-t, --types <categories>', 'Comma-separated categories: refactor,docs,test,perf,security')
26
+ .option('-m, --max <count>', 'Maximum proposals to generate', '10')
27
+ .option('-c, --min-confidence <percent>', 'Minimum confidence threshold', '50')
28
+ .option('--model <model>', 'Model to use: haiku, sonnet, opus', 'opus')
29
+ .option('--auto-approve', 'Automatically create tickets for all proposals')
30
+ .option('--dry-run', 'Show proposals without saving')
31
+ .option('--max-files <count>', 'Maximum files to scan (default: 500)')
32
+ .action(async (targetPath, options) => {
33
+ const isJsonMode = options.json;
34
+ const isQuiet = options.quiet || isJsonMode;
35
+ const scope = targetPath ?? options.scope ?? 'src/**';
36
+ if (!isQuiet) {
37
+ console.log(chalk.blue('🔍 BlockSpool Solo Scout'));
38
+ console.log();
39
+ }
40
+ const git = createGitService();
41
+ const repoRoot = await git.findRepoRoot('.');
42
+ if (!repoRoot) {
43
+ if (isJsonMode) {
44
+ console.log(JSON.stringify({ success: false, error: 'Not a git repository' }));
45
+ }
46
+ else {
47
+ console.error(chalk.red('✗ Not a git repository'));
48
+ console.error(' Run this command from within a git repository');
49
+ }
50
+ process.exit(1);
51
+ }
52
+ if (!isInitialized(repoRoot)) {
53
+ if (!isQuiet) {
54
+ console.log(chalk.gray('Initializing local state...'));
55
+ }
56
+ await initSolo(repoRoot);
57
+ }
58
+ if (!isQuiet) {
59
+ console.log(chalk.gray(`Project: ${path.basename(repoRoot)}`));
60
+ console.log(chalk.gray(`Scope: ${scope}`));
61
+ console.log();
62
+ }
63
+ const adapter = await getAdapter(repoRoot);
64
+ const deps = createScoutDeps(adapter, options);
65
+ try {
66
+ const types = options.types?.split(',').map(t => t.trim());
67
+ const maxProposals = parseInt(options.max || '10', 10);
68
+ const minConfidence = parseInt(options.minConfidence || '50', 10);
69
+ const model = (options.model || 'opus');
70
+ const maxFiles = options.maxFiles ? parseInt(options.maxFiles, 10) : undefined;
71
+ // eslint-disable-next-line no-undef
72
+ const controller = new AbortController();
73
+ process.on('SIGINT', () => {
74
+ if (!isQuiet) {
75
+ console.log(chalk.yellow('\n\nAborting scan...'));
76
+ }
77
+ controller.abort();
78
+ });
79
+ let lastProgress = '';
80
+ const scoutOptions = {
81
+ path: repoRoot,
82
+ scope,
83
+ types,
84
+ maxProposals,
85
+ minConfidence,
86
+ model,
87
+ signal: controller.signal,
88
+ autoApprove: options.autoApprove && !options.dryRun,
89
+ onProgress: (progress) => {
90
+ if (!isQuiet) {
91
+ const formatted = formatProgress(progress);
92
+ if (formatted !== lastProgress) {
93
+ process.stdout.write(`\r${formatted.padEnd(80)}`);
94
+ lastProgress = formatted;
95
+ }
96
+ }
97
+ },
98
+ ...(maxFiles !== undefined && { maxFiles }),
99
+ };
100
+ const result = await scoutRepo(deps, scoutOptions);
101
+ if (!isQuiet) {
102
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
103
+ }
104
+ let proposalsArtifactPath = null;
105
+ if (result.proposals.length > 0) {
106
+ proposalsArtifactPath = writeJsonArtifact({
107
+ baseDir: getBlockspoolDir(repoRoot),
108
+ type: 'proposals',
109
+ id: result.run.id,
110
+ data: {
111
+ runId: result.run.id,
112
+ projectId: result.project.id,
113
+ projectName: result.project.name,
114
+ createdAt: new Date().toISOString(),
115
+ proposals: result.proposals,
116
+ },
117
+ });
118
+ }
119
+ if (isJsonMode) {
120
+ const output = {
121
+ success: result.success,
122
+ project: path.basename(repoRoot),
123
+ scannedFiles: result.scannedFiles,
124
+ durationMs: result.durationMs,
125
+ proposals: result.proposals.map(p => ({
126
+ title: p.title,
127
+ category: p.category,
128
+ description: p.description,
129
+ files: p.files,
130
+ estimated_complexity: p.estimated_complexity,
131
+ confidence: p.confidence,
132
+ })),
133
+ tickets: result.tickets.map(t => ({
134
+ id: t.id,
135
+ title: t.title,
136
+ status: t.status,
137
+ })),
138
+ errors: result.errors,
139
+ };
140
+ console.log(JSON.stringify(output, null, 2));
141
+ return;
142
+ }
143
+ if (!result.success) {
144
+ console.error(chalk.red('✗ Scout failed'));
145
+ for (const error of result.errors) {
146
+ console.error(chalk.red(` ${error}`));
147
+ }
148
+ process.exit(1);
149
+ }
150
+ console.log(chalk.green(`✓ Scanned ${result.scannedFiles} files in ` +
151
+ `${(result.durationMs / 1000).toFixed(1)}s`));
152
+ if (result.proposals.length === 0) {
153
+ console.log(chalk.yellow('\nNo improvement opportunities found.'));
154
+ console.log(chalk.gray('Try broadening your scope or lowering the confidence threshold.'));
155
+ return;
156
+ }
157
+ console.log(chalk.blue(`\nFound ${result.proposals.length} proposals:`));
158
+ for (let i = 0; i < result.proposals.length; i++) {
159
+ displayProposal(result.proposals[i], i);
160
+ }
161
+ if (options.dryRun) {
162
+ console.log(chalk.yellow('\n--dry-run: Proposals not saved'));
163
+ return;
164
+ }
165
+ if (result.tickets.length > 0) {
166
+ console.log(chalk.green(`\n✓ Created ${result.tickets.length} tickets`));
167
+ console.log(chalk.gray(` IDs: ${result.tickets.map(t => t.id).join(', ')}`));
168
+ }
169
+ else if (!options.autoApprove && result.proposals.length > 0) {
170
+ console.log(chalk.blue('\nNext steps:'));
171
+ console.log(' blockspool solo approve 1-3 # Approve proposals 1-3');
172
+ console.log(' blockspool solo approve all # Approve all proposals');
173
+ if (proposalsArtifactPath) {
174
+ console.log(chalk.gray(`\n Proposals saved: ${proposalsArtifactPath}`));
175
+ }
176
+ }
177
+ if (result.errors.length > 0) {
178
+ console.log(chalk.yellow('\nWarnings:'));
179
+ for (const error of result.errors) {
180
+ console.log(chalk.yellow(` ${error}`));
181
+ }
182
+ }
183
+ }
184
+ finally {
185
+ await adapter.close();
186
+ }
187
+ });
188
+ /**
189
+ * solo status - Show current state
190
+ */
191
+ solo
192
+ .command('status')
193
+ .description('Show local state and active tickets')
194
+ .option('-v, --verbose', 'Show detailed output')
195
+ .option('--json', 'Output as JSON')
196
+ .action(async (options) => {
197
+ const git = createGitService();
198
+ const repoRoot = await git.findRepoRoot(process.cwd());
199
+ if (!repoRoot) {
200
+ if (options.json) {
201
+ console.log(JSON.stringify({ error: 'Not a git repository' }));
202
+ }
203
+ else {
204
+ console.error(chalk.red('✗ Not a git repository'));
205
+ }
206
+ process.exit(1);
207
+ }
208
+ const dbPath = getDbPath(repoRoot);
209
+ const adapter = await getAdapter(repoRoot);
210
+ try {
211
+ const projectList = await projects.list(adapter);
212
+ if (options.json) {
213
+ const output = {
214
+ dbPath,
215
+ projects: [],
216
+ };
217
+ for (const project of projectList) {
218
+ const counts = await tickets.countByStatus(adapter, project.id);
219
+ const summary = await runs.getSummary(adapter, project.id);
220
+ let lastExecuteCompletionOutcome = null;
221
+ if (summary.lastExecute?.id) {
222
+ const fullRun = await runs.getById(adapter, summary.lastExecute.id);
223
+ if (fullRun?.metadata?.completionOutcome) {
224
+ lastExecuteCompletionOutcome = fullRun.metadata.completionOutcome;
225
+ }
226
+ }
227
+ output.projects.push({
228
+ id: project.id,
229
+ name: project.name,
230
+ ticketCounts: counts,
231
+ lastScout: summary.lastScout ? {
232
+ ...summary.lastScout,
233
+ completedAt: summary.lastScout.completedAt?.toISOString() ?? null,
234
+ } : null,
235
+ lastQa: summary.lastQa ? {
236
+ ...summary.lastQa,
237
+ completedAt: summary.lastQa.completedAt?.toISOString() ?? null,
238
+ } : null,
239
+ lastExecute: summary.lastExecute ? {
240
+ ...summary.lastExecute,
241
+ completedAt: summary.lastExecute.completedAt?.toISOString() ?? null,
242
+ completionOutcome: lastExecuteCompletionOutcome,
243
+ } : null,
244
+ activeRuns: summary.activeRuns,
245
+ });
246
+ }
247
+ console.log(JSON.stringify(output, null, 2));
248
+ return;
249
+ }
250
+ console.log(chalk.blue('📊 BlockSpool Solo Status'));
251
+ console.log();
252
+ console.log(chalk.gray(`Database: ${dbPath}`));
253
+ console.log();
254
+ console.log(`Projects: ${projectList.length}`);
255
+ if (projectList.length === 0) {
256
+ console.log(chalk.yellow('\nNo projects found. Run: blockspool solo scout .'));
257
+ return;
258
+ }
259
+ for (const project of projectList) {
260
+ console.log();
261
+ console.log(chalk.bold(project.name));
262
+ const summary = await runs.getSummary(adapter, project.id);
263
+ if (summary.lastScout) {
264
+ const scout = summary.lastScout;
265
+ const statusColor = scout.status === 'success' ? chalk.green :
266
+ scout.status === 'failure' ? chalk.red : chalk.yellow;
267
+ const timeAgo = scout.completedAt ? formatRelativeTime(scout.completedAt) : 'running';
268
+ console.log();
269
+ console.log(` ${chalk.cyan('Last Scout:')}`);
270
+ console.log(` ${statusColor(scout.status)} | ${timeAgo}`);
271
+ console.log(` ${scout.scannedFiles} files scanned, ${scout.proposalCount} proposals, ${scout.ticketCount} tickets`);
272
+ if (scout.durationMs > 0) {
273
+ console.log(` Duration: ${formatDuration(scout.durationMs)}`);
274
+ }
275
+ }
276
+ if (summary.lastQa) {
277
+ const qa = summary.lastQa;
278
+ const statusColor = qa.status === 'success' ? chalk.green :
279
+ qa.status === 'failure' ? chalk.red : chalk.yellow;
280
+ const timeAgo = qa.completedAt ? formatRelativeTime(qa.completedAt) : 'running';
281
+ const passFailText = `${qa.stepsPassed} passed, ${qa.stepsFailed} failed`;
282
+ console.log();
283
+ console.log(` ${chalk.cyan('Last QA:')}`);
284
+ console.log(` ${statusColor(qa.status)} | ${timeAgo}`);
285
+ console.log(` ${passFailText}`);
286
+ if (qa.durationMs > 0) {
287
+ console.log(` Duration: ${formatDuration(qa.durationMs)}`);
288
+ }
289
+ }
290
+ if (summary.lastExecute) {
291
+ const exec = summary.lastExecute;
292
+ const baseDir = getBlockspoolDir(repoRoot);
293
+ let spindleInfo = null;
294
+ if (exec.status === 'failure' && exec.id) {
295
+ const spindleArtifact = getArtifactByRunId(baseDir, exec.id, 'spindle');
296
+ if (spindleArtifact) {
297
+ const data = spindleArtifact.data;
298
+ spindleInfo = {
299
+ reason: data.reason,
300
+ artifactPath: spindleArtifact.path,
301
+ };
302
+ }
303
+ }
304
+ let completionOutcome = null;
305
+ if (exec.status === 'success' && exec.id) {
306
+ const fullRun = await runs.getById(adapter, exec.id);
307
+ if (fullRun?.metadata?.completionOutcome) {
308
+ completionOutcome = fullRun.metadata.completionOutcome;
309
+ }
310
+ }
311
+ const isNoChangesNeeded = completionOutcome === 'no_changes_needed';
312
+ const isSpindleFailure = exec.status === 'failure' && spindleInfo?.reason;
313
+ const statusColor = exec.status === 'success' ? chalk.green :
314
+ isSpindleFailure ? chalk.yellow :
315
+ exec.status === 'failure' ? chalk.red : chalk.yellow;
316
+ const timeAgo = exec.completedAt ? formatRelativeTime(exec.completedAt) : 'running';
317
+ console.log();
318
+ console.log(` ${chalk.cyan('Last Execute:')}`);
319
+ if (isSpindleFailure) {
320
+ console.log(` ${statusColor('failed')} (Spindle: ${spindleInfo.reason}) | ${timeAgo}`);
321
+ console.log(chalk.gray(` See artifacts: ${spindleInfo.artifactPath}`));
322
+ }
323
+ else if (isNoChangesNeeded) {
324
+ console.log(` ${statusColor('success')} (no changes needed) | ${timeAgo}`);
325
+ }
326
+ else {
327
+ console.log(` ${statusColor(exec.status)} | ${timeAgo}`);
328
+ }
329
+ if (exec.ticketId) {
330
+ console.log(` Ticket: ${exec.ticketId}`);
331
+ }
332
+ if (exec.branchName) {
333
+ console.log(` Branch: ${exec.branchName}`);
334
+ }
335
+ if (exec.prUrl) {
336
+ console.log(` PR: ${chalk.cyan(exec.prUrl)}`);
337
+ }
338
+ if (exec.durationMs > 0) {
339
+ console.log(` Duration: ${formatDuration(exec.durationMs)}`);
340
+ }
341
+ }
342
+ const counts = await tickets.countByStatus(adapter, project.id);
343
+ const total = Object.values(counts).reduce((a, b) => a + b, 0);
344
+ console.log();
345
+ console.log(` ${chalk.cyan('Tickets:')}`);
346
+ if (total === 0) {
347
+ console.log(chalk.gray(' No tickets'));
348
+ }
349
+ else {
350
+ for (const [status, count] of Object.entries(counts)) {
351
+ if (count === 0)
352
+ continue;
353
+ const color = status === 'done' ? chalk.green :
354
+ status === 'blocked' || status === 'aborted' ? chalk.red :
355
+ status === 'in_progress' || status === 'leased' ? chalk.yellow :
356
+ chalk.gray;
357
+ console.log(` ${color(status)}: ${count}`);
358
+ }
359
+ }
360
+ if (summary.activeRuns > 0) {
361
+ console.log(` ${chalk.cyan('active runs')}: ${summary.activeRuns}`);
362
+ }
363
+ }
364
+ }
365
+ finally {
366
+ await adapter.close();
367
+ }
368
+ });
369
+ /**
370
+ * solo history - View auto run history
371
+ */
372
+ solo
373
+ .command('history')
374
+ .description('View auto run history')
375
+ .option('-n, --limit <n>', 'Number of entries to show', '10')
376
+ .option('--json', 'Output as JSON')
377
+ .action(async (options) => {
378
+ const { readRunHistory, formatHistoryEntry } = await import('../lib/run-history.js');
379
+ const git = createGitService();
380
+ const repoRoot = await git.findRepoRoot(process.cwd());
381
+ const entries = readRunHistory(repoRoot || undefined, parseInt(options.limit || '10', 10));
382
+ if (entries.length === 0) {
383
+ console.log(chalk.gray('No history yet. Run `blockspool solo auto` to get started.'));
384
+ return;
385
+ }
386
+ if (options.json) {
387
+ console.log(JSON.stringify(entries, null, 2));
388
+ return;
389
+ }
390
+ console.log(chalk.bold(`Run History (${entries.length} entries):\n`));
391
+ for (const entry of entries) {
392
+ console.log(formatHistoryEntry(entry));
393
+ console.log();
394
+ }
395
+ });
396
+ /**
397
+ * solo formulas - List available formulas
398
+ */
399
+ solo
400
+ .command('formulas')
401
+ .description('List available auto formulas')
402
+ .action(async () => {
403
+ const { listFormulas } = await import('../lib/formulas.js');
404
+ const git = createGitService();
405
+ const repoRoot = await git.findRepoRoot(process.cwd());
406
+ const formulas = listFormulas(repoRoot || undefined);
407
+ if (formulas.length === 0) {
408
+ console.log(chalk.gray('No formulas available'));
409
+ return;
410
+ }
411
+ console.log(chalk.bold('Available formulas:\n'));
412
+ for (const formula of formulas) {
413
+ const tags = formula.tags?.length ? chalk.gray(` [${formula.tags.join(', ')}]`) : '';
414
+ console.log(` ${chalk.cyan(formula.name)}${tags}`);
415
+ console.log(` ${formula.description}`);
416
+ if (formula.categories?.length) {
417
+ console.log(chalk.gray(` Categories: ${formula.categories.join(', ')}`));
418
+ }
419
+ if (formula.minConfidence) {
420
+ console.log(chalk.gray(` Min confidence: ${formula.minConfidence}%`));
421
+ }
422
+ console.log();
423
+ }
424
+ console.log(chalk.gray('Usage: blockspool solo auto --formula <name>'));
425
+ console.log(chalk.gray('Custom: Create .blockspool/formulas/<name>.yaml'));
426
+ });
427
+ /**
428
+ * solo export - Export state for debugging
429
+ */
430
+ solo
431
+ .command('export')
432
+ .description('Export local state for debugging or migration')
433
+ .option('-o, --output <file>', 'Output file', 'blockspool-export.json')
434
+ .action(async (options) => {
435
+ const git = createGitService();
436
+ const repoRoot = await git.findRepoRoot(process.cwd());
437
+ if (!repoRoot) {
438
+ console.error(chalk.red('✗ Not a git repository'));
439
+ process.exit(1);
440
+ }
441
+ const adapter = await getAdapter(repoRoot);
442
+ try {
443
+ const projectList = await projects.list(adapter);
444
+ const data = {
445
+ exportedAt: new Date().toISOString(),
446
+ version: 1,
447
+ projects: [],
448
+ };
449
+ for (const project of projectList) {
450
+ const projectTickets = await tickets.listByProject(adapter, project.id);
451
+ const projectRuns = await runs.listByProject(adapter, project.id);
452
+ data.projects.push({
453
+ ...project,
454
+ tickets: projectTickets,
455
+ runs: projectRuns,
456
+ });
457
+ }
458
+ fs.writeFileSync(options.output, JSON.stringify(data, null, 2));
459
+ console.log(chalk.green(`✓ Exported to ${options.output}`));
460
+ }
461
+ finally {
462
+ await adapter.close();
463
+ }
464
+ });
465
+ /**
466
+ * solo artifacts - List and view run artifacts
467
+ */
468
+ solo
469
+ .command('artifacts')
470
+ .description('List and view run artifacts')
471
+ .option('--run <runId>', 'Show artifacts for a specific run')
472
+ .option('--type <type>', 'Filter by artifact type (proposals, executions, diffs, runs, violations)')
473
+ .option('--show <path>', 'Display contents of a specific artifact file')
474
+ .option('--json', 'Output in JSON format')
475
+ .action(async (options) => {
476
+ const repoRoot = process.cwd();
477
+ const baseDir = getBlockspoolDir(repoRoot);
478
+ const artifactsDir = path.join(baseDir, 'artifacts');
479
+ if (options.show) {
480
+ const filePath = options.show.startsWith('/') ? options.show : path.join(process.cwd(), options.show);
481
+ if (!fs.existsSync(filePath)) {
482
+ console.error(chalk.red(`Artifact not found: ${filePath}`));
483
+ process.exit(1);
484
+ }
485
+ const content = fs.readFileSync(filePath, 'utf-8');
486
+ if (options.json) {
487
+ console.log(content);
488
+ }
489
+ else {
490
+ console.log(chalk.cyan(`\n─── ${path.basename(filePath)} ───\n`));
491
+ try {
492
+ const data = JSON.parse(content);
493
+ console.log(JSON.stringify(data, null, 2));
494
+ }
495
+ catch {
496
+ console.log(content);
497
+ }
498
+ }
499
+ return;
500
+ }
501
+ if (options.run) {
502
+ const artifacts = getArtifactsForRun(baseDir, options.run);
503
+ const found = Object.entries(artifacts).filter(([, v]) => v !== null);
504
+ if (options.json) {
505
+ console.log(JSON.stringify({
506
+ runId: options.run,
507
+ artifacts: Object.fromEntries(found.map(([type, artifact]) => [type, artifact?.path])),
508
+ }, null, 2));
509
+ return;
510
+ }
511
+ if (found.length === 0) {
512
+ console.log(chalk.yellow(`No artifacts found for run: ${options.run}`));
513
+ return;
514
+ }
515
+ console.log(chalk.cyan(`\nArtifacts for run ${options.run}:\n`));
516
+ for (const [type, artifact] of found) {
517
+ if (artifact) {
518
+ console.log(` ${chalk.bold(type)}: ${artifact.path}`);
519
+ }
520
+ }
521
+ console.log();
522
+ return;
523
+ }
524
+ if (!fs.existsSync(artifactsDir)) {
525
+ console.log(chalk.yellow('No artifacts found. Run a ticket to generate artifacts.'));
526
+ return;
527
+ }
528
+ const allArtifacts = getAllArtifacts(baseDir);
529
+ const types = options.type
530
+ ? [options.type]
531
+ : ['runs', 'executions', 'diffs', 'violations', 'proposals', 'spindle'];
532
+ if (options.json) {
533
+ const output = {};
534
+ for (const type of types) {
535
+ output[type] = allArtifacts[type] ?? [];
536
+ }
537
+ console.log(JSON.stringify(output, null, 2));
538
+ return;
539
+ }
540
+ let totalCount = 0;
541
+ for (const type of types) {
542
+ const artifacts = allArtifacts[type] ?? [];
543
+ if (artifacts.length === 0)
544
+ continue;
545
+ console.log(chalk.cyan(`\n${type.toUpperCase()} (${artifacts.length}):`));
546
+ for (const artifact of artifacts.slice(0, 10)) {
547
+ const date = new Date(artifact.timestamp).toISOString().slice(0, 19).replace('T', ' ');
548
+ console.log(` ${chalk.gray(date)} ${artifact.id}`);
549
+ console.log(` ${chalk.dim(artifact.path)}`);
550
+ totalCount++;
551
+ }
552
+ if (artifacts.length > 10) {
553
+ console.log(chalk.dim(` ... and ${artifacts.length - 10} more`));
554
+ }
555
+ }
556
+ if (totalCount === 0) {
557
+ console.log(chalk.yellow('No artifacts found. Run a ticket to generate artifacts.'));
558
+ }
559
+ else {
560
+ console.log(chalk.dim(`\nUse --show <path> to view an artifact, or --run <id> to see all artifacts for a run.\n`));
561
+ }
562
+ });
563
+ /**
564
+ * solo approve - Convert proposals to tickets
565
+ */
566
+ solo
567
+ .command('approve <selection>')
568
+ .description('Approve proposals and create tickets')
569
+ .addHelpText('after', `
570
+ Selection formats:
571
+ 1 Single proposal (1-indexed)
572
+ 1,3,5 Multiple specific proposals
573
+ 1-3 Range of proposals
574
+ 1-3,5,7 Mixed selection
575
+ all All proposals
576
+
577
+ Examples:
578
+ blockspool solo approve 1 # Approve first proposal
579
+ blockspool solo approve 1-3 # Approve proposals 1, 2, 3
580
+ blockspool solo approve all # Approve all proposals
581
+ `)
582
+ .option('-v, --verbose', 'Show detailed output')
583
+ .option('--json', 'Output as JSON')
584
+ .action(async (selection, options) => {
585
+ const isJsonMode = options.json;
586
+ const git = createGitService();
587
+ const repoRoot = await git.findRepoRoot(process.cwd());
588
+ if (!repoRoot) {
589
+ if (isJsonMode) {
590
+ console.log(JSON.stringify({ success: false, error: 'Not a git repository' }));
591
+ }
592
+ else {
593
+ console.error(chalk.red('✗ Not a git repository'));
594
+ }
595
+ process.exit(1);
596
+ }
597
+ const baseDir = getBlockspoolDir(repoRoot);
598
+ const artifact = getLatestArtifact(baseDir, 'proposals');
599
+ if (!artifact) {
600
+ if (isJsonMode) {
601
+ console.log(JSON.stringify({
602
+ success: false,
603
+ error: 'No proposals found. Run: blockspool solo scout .',
604
+ }));
605
+ }
606
+ else {
607
+ console.error(chalk.red('✗ No proposals found'));
608
+ console.log(chalk.gray(' Run: blockspool solo scout .'));
609
+ }
610
+ process.exit(1);
611
+ }
612
+ const { proposals } = artifact.data;
613
+ if (proposals.length === 0) {
614
+ if (isJsonMode) {
615
+ console.log(JSON.stringify({ success: false, error: 'No proposals in artifact' }));
616
+ }
617
+ else {
618
+ console.error(chalk.red('✗ No proposals in artifact'));
619
+ }
620
+ process.exit(1);
621
+ }
622
+ let selectedIndices;
623
+ try {
624
+ selectedIndices = parseSelection(selection, proposals.length);
625
+ }
626
+ catch (err) {
627
+ if (isJsonMode) {
628
+ console.log(JSON.stringify({
629
+ success: false,
630
+ error: err instanceof Error ? err.message : String(err),
631
+ }));
632
+ }
633
+ else {
634
+ console.error(chalk.red(`✗ Invalid selection: ${err instanceof Error ? err.message : err}`));
635
+ console.log(chalk.gray(` Valid range: 1-${proposals.length}`));
636
+ }
637
+ process.exit(1);
638
+ }
639
+ if (selectedIndices.length === 0) {
640
+ if (isJsonMode) {
641
+ console.log(JSON.stringify({ success: false, error: 'No proposals selected' }));
642
+ }
643
+ else {
644
+ console.error(chalk.red('✗ No proposals selected'));
645
+ console.log(chalk.gray(` Valid range: 1-${proposals.length}`));
646
+ }
647
+ process.exit(1);
648
+ }
649
+ const selectedProposals = selectedIndices.map(i => proposals[i]);
650
+ if (!isJsonMode) {
651
+ console.log(chalk.blue('📋 BlockSpool Solo Approve'));
652
+ console.log();
653
+ console.log(`Selected ${selectedProposals.length} proposal(s):`);
654
+ for (const idx of selectedIndices) {
655
+ const p = proposals[idx];
656
+ console.log(` ${chalk.bold(idx + 1)}. ${p.title}`);
657
+ }
658
+ console.log();
659
+ }
660
+ const adapter = await getAdapter(repoRoot);
661
+ try {
662
+ const deps = createScoutDeps(adapter, options);
663
+ const createdTickets = await approveProposals(deps, artifact.data.projectId, selectedProposals);
664
+ if (isJsonMode) {
665
+ console.log(JSON.stringify({
666
+ success: true,
667
+ tickets: createdTickets.map(t => ({
668
+ id: t.id,
669
+ title: t.title,
670
+ status: t.status,
671
+ })),
672
+ }));
673
+ }
674
+ else {
675
+ console.log(chalk.green(`✓ Created ${createdTickets.length} ticket(s)`));
676
+ for (const t of createdTickets) {
677
+ console.log(` ${chalk.gray(t.id)} ${t.title}`);
678
+ }
679
+ console.log();
680
+ console.log(chalk.blue('Next steps:'));
681
+ console.log(` blockspool solo run ${createdTickets[0]?.id} # Run a ticket`);
682
+ console.log(' blockspool solo status # View all tickets');
683
+ }
684
+ }
685
+ finally {
686
+ await adapter.close();
687
+ }
688
+ });
689
+ }
690
+ //# sourceMappingURL=solo-inspect.js.map