@meistrari/tela-cli 0.7.0 → 0.7.4
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/README.md +56 -106
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/doctor-auth.d.ts +2 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/credentials.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7027 -0
- package/dist/output.d.ts +13 -0
- package/dist/program.d.ts +2 -0
- package/package.json +17 -24
- package/scripts/install-skills.cjs +127 -0
- package/skills/tela-cli/SKILL.md +20 -0
- package/skills/tela-cli/references/agent.md +111 -0
- package/cli.ts +0 -25
- package/postinstall.sh +0 -19
- package/src/agent-api.ts +0 -300
- package/src/assets.d.ts +0 -4
- package/src/auth.ts +0 -432
- package/src/claude.ts +0 -101
- package/src/commands/doctor.ts +0 -35
- package/src/commands/feedback.ts +0 -701
- package/src/commands/metadata.ts +0 -242
- package/src/commands/push.ts +0 -208
- package/src/commands/run.ts +0 -1699
- package/src/commands/status.ts +0 -109
- package/src/commands/sync.ts +0 -245
- package/src/commit-message.ts +0 -69
- package/src/config.ts +0 -799
- package/src/diagnostics.ts +0 -813
- package/src/errors.ts +0 -122
- package/src/forgejo.ts +0 -233
- package/src/git.ts +0 -457
- package/src/ink/assets/160092__jorickhoofd__dog-bark-1.mp3 +0 -0
- package/src/ink/bark.tsx +0 -241
- package/src/ink/components.tsx +0 -178
- package/src/ink/runtime.tsx +0 -90
- package/src/ink/screens/feedback-screen.tsx +0 -395
- package/src/ink/screens/push-screen.tsx +0 -232
- package/src/ink/screens/run-screen.tsx +0 -292
- package/src/ink/screens/setup/breadcrumb.tsx +0 -24
- package/src/ink/screens/setup/setup-screen.tsx +0 -612
- package/src/ink/screens/setup/step-frame.tsx +0 -31
- package/src/ink/screens/setup/steps/done-step.tsx +0 -29
- package/src/ink/screens/setup/steps/environment-step.tsx +0 -23
- package/src/ink/screens/setup/steps/organization-step.tsx +0 -35
- package/src/ink/screens/setup/steps/review-step.tsx +0 -78
- package/src/ink/screens/setup/steps/running-step.tsx +0 -37
- package/src/ink/screens/setup/steps/ssh-key-step.tsx +0 -48
- package/src/ink/screens/setup/steps/text-field-step.tsx +0 -18
- package/src/ink/screens/setup/steps.ts +0 -156
- package/src/ink/screens/setup/types.ts +0 -65
- package/src/ink/screens/sync-screen.tsx +0 -283
- package/src/process.ts +0 -251
- package/src/program.ts +0 -2105
- package/src/runtime.ts +0 -147
- package/src/types.ts +0 -165
- package/src/ui.ts +0 -295
- package/src/version.ts +0 -3
package/src/commands/feedback.ts
DELETED
|
@@ -1,701 +0,0 @@
|
|
|
1
|
-
import { basename } from 'node:path'
|
|
2
|
-
import type { AgentFeedbackItem, TelaCliConfig } from '../types.ts'
|
|
3
|
-
import { fetchCommitFeedback, fetchRepositoryLookup } from '../agent-api.ts'
|
|
4
|
-
import {
|
|
5
|
-
ensureConfigInteractive,
|
|
6
|
-
type Option,
|
|
7
|
-
type PromptModule as ConfigPromptModule,
|
|
8
|
-
} from '../config.ts'
|
|
9
|
-
import { createUserFacingError } from '../errors.ts'
|
|
10
|
-
import { getDiffStatAgainstHead, getHeadCommitHash, getOriginUrl } from '../git.ts'
|
|
11
|
-
import { CommandError, commandExists, runInteractiveCommand } from '../process.ts'
|
|
12
|
-
|
|
13
|
-
export interface ConfirmOptions {
|
|
14
|
-
message: string
|
|
15
|
-
active?: string
|
|
16
|
-
inactive?: string
|
|
17
|
-
initialValue?: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface GroupMultiSelectOptions<T> {
|
|
21
|
-
message: string
|
|
22
|
-
options: Record<string, Array<Option<T>>>
|
|
23
|
-
cursorAt?: T
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface PromptModule extends ConfigPromptModule {
|
|
27
|
-
log: {
|
|
28
|
-
success(message: string): void
|
|
29
|
-
}
|
|
30
|
-
note(message: string, title?: string): void
|
|
31
|
-
confirm(options: ConfirmOptions): Promise<boolean | symbol>
|
|
32
|
-
groupMultiselect<T>(options: GroupMultiSelectOptions<T>): Promise<T[] | symbol>
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface FeedbackCommandFlags {
|
|
36
|
-
list?: boolean
|
|
37
|
-
apply?: 'all' | 'select'
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface FeedbackEntry {
|
|
41
|
-
id: string
|
|
42
|
-
number: number
|
|
43
|
-
feedback: AgentFeedbackItem
|
|
44
|
-
actionable: boolean
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface FeedbackData {
|
|
48
|
-
repoRoot: string
|
|
49
|
-
organizationName: string
|
|
50
|
-
repository: string
|
|
51
|
-
commitHash: string
|
|
52
|
-
entries: FeedbackEntry[]
|
|
53
|
-
actionableEntries: FeedbackEntry[]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type FeedbackEmptyState = 'none' | 'no_feedback' | 'informational_only'
|
|
57
|
-
export type FeedbackSelectionMode = 'list' | 'all' | 'select' | 'default'
|
|
58
|
-
|
|
59
|
-
export interface FeedbackSummary {
|
|
60
|
-
organizationName: string
|
|
61
|
-
repository: string
|
|
62
|
-
commitHash: string
|
|
63
|
-
totalFeedbacks: number
|
|
64
|
-
actionableCount: number
|
|
65
|
-
informationalCount: number
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface FeedbackGroupedEntries {
|
|
69
|
-
actionable: FeedbackEntry[]
|
|
70
|
-
informational: FeedbackEntry[]
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface FeedbackReviewModel {
|
|
74
|
-
summary: FeedbackSummary
|
|
75
|
-
groupedEntries: FeedbackGroupedEntries
|
|
76
|
-
emptyState: FeedbackEmptyState
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface FeedbackRunResult {
|
|
80
|
-
ok: true
|
|
81
|
-
organizationName: string
|
|
82
|
-
repository: string
|
|
83
|
-
commitHash: string
|
|
84
|
-
summary: FeedbackSummary
|
|
85
|
-
emptyState: FeedbackEmptyState
|
|
86
|
-
selectionMode: FeedbackSelectionMode
|
|
87
|
-
feedbacks: Array<{
|
|
88
|
-
id: string
|
|
89
|
-
number: number
|
|
90
|
-
status: AgentFeedbackItem['status']
|
|
91
|
-
reason: string
|
|
92
|
-
suggestion: string
|
|
93
|
-
actionable: boolean
|
|
94
|
-
}>
|
|
95
|
-
selectedIds?: string[]
|
|
96
|
-
launchedClaude: boolean
|
|
97
|
-
diffStat?: string
|
|
98
|
-
nextActions: string[]
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface RunFeedbackCommandOptions extends FeedbackCommandFlags {
|
|
102
|
-
cwd?: string
|
|
103
|
-
prompts: PromptModule
|
|
104
|
-
ensureConfigInteractive?: typeof ensureConfigInteractive
|
|
105
|
-
fetchRepositoryLookup?: typeof fetchRepositoryLookup
|
|
106
|
-
fetchCommitFeedback?: typeof fetchCommitFeedback
|
|
107
|
-
getOriginUrl?: typeof getOriginUrl
|
|
108
|
-
getHeadCommitHash?: typeof getHeadCommitHash
|
|
109
|
-
getDiffStatAgainstHead?: typeof getDiffStatAgainstHead
|
|
110
|
-
commandExists?: typeof commandExists
|
|
111
|
-
runInteractiveCommand?: typeof runInteractiveCommand
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function promptValue<T>(value: T | symbol): T {
|
|
115
|
-
return value as T
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function truncate(value: string, maxLength: number): string {
|
|
119
|
-
if (value.length <= maxLength) {
|
|
120
|
-
return value
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return `${value.slice(0, Math.max(0, maxLength - 1)).trimEnd()}...`
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function firstMeaningfulLine(value: string): string {
|
|
127
|
-
return value
|
|
128
|
-
.split(/\r?\n/)
|
|
129
|
-
.map(line => line.trim())
|
|
130
|
-
.find(Boolean) ?? ''
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function parseRepositoryNameFromOrigin(originUrl: string): string | null {
|
|
134
|
-
const trimmed = originUrl.trim()
|
|
135
|
-
if (!trimmed) {
|
|
136
|
-
return null
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (trimmed.includes('://')) {
|
|
140
|
-
try {
|
|
141
|
-
const url = new URL(trimmed)
|
|
142
|
-
const segments = url.pathname.split('/').filter(Boolean)
|
|
143
|
-
const lastSegment = segments.at(-1)
|
|
144
|
-
return lastSegment ? lastSegment.replace(/\.git$/i, '') : null
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
return null
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const scpMatch = trimmed.match(/^[^@]+@[^:]+:(.+)$/)
|
|
152
|
-
if (scpMatch?.[1]) {
|
|
153
|
-
return basename(scpMatch[1]).replace(/\.git$/i, '') || null
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return basename(trimmed).replace(/\.git$/i, '') || null
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function resolveRepositoryName(
|
|
160
|
-
cwd: string,
|
|
161
|
-
getOriginUrlFn: typeof getOriginUrl,
|
|
162
|
-
): Promise<string> {
|
|
163
|
-
try {
|
|
164
|
-
const originUrl = await getOriginUrlFn(cwd)
|
|
165
|
-
return parseRepositoryNameFromOrigin(originUrl) ?? basename(cwd)
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
return basename(cwd)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function buildFeedbackEntries(feedbacks: AgentFeedbackItem[]): FeedbackEntry[] {
|
|
173
|
-
return feedbacks.map((feedback, index) => ({
|
|
174
|
-
id: String(index + 1),
|
|
175
|
-
number: index + 1,
|
|
176
|
-
feedback,
|
|
177
|
-
actionable: feedback.suggestion.trim().length > 0,
|
|
178
|
-
}))
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function getFeedbackEntryTitle(entry: FeedbackEntry): string {
|
|
182
|
-
return firstMeaningfulLine(entry.feedback.suggestion)
|
|
183
|
-
|| firstMeaningfulLine(entry.feedback.reason)
|
|
184
|
-
|| `Feedback #${entry.number}`
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function describeFeedbackSelectionMode(mode: FeedbackSelectionMode): string {
|
|
188
|
-
switch (mode) {
|
|
189
|
-
case 'list':
|
|
190
|
-
return 'Review only'
|
|
191
|
-
case 'all':
|
|
192
|
-
return 'Apply all actionable suggestions'
|
|
193
|
-
case 'select':
|
|
194
|
-
return 'Choose actionable suggestions'
|
|
195
|
-
case 'default':
|
|
196
|
-
return 'Review and edit the actionable selection'
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function describeFeedbackEmptyState(emptyState: FeedbackEmptyState): string {
|
|
201
|
-
switch (emptyState) {
|
|
202
|
-
case 'no_feedback':
|
|
203
|
-
return 'No feedback was returned for this commit.'
|
|
204
|
-
case 'informational_only':
|
|
205
|
-
return 'Only informational feedback is available for this commit.'
|
|
206
|
-
case 'none':
|
|
207
|
-
return 'Actionable feedback is available.'
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function getFeedbackSelectionMode(options: FeedbackCommandFlags): FeedbackSelectionMode {
|
|
212
|
-
if (options.list) {
|
|
213
|
-
return 'list'
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (options.apply === 'all') {
|
|
217
|
-
return 'all'
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (options.apply === 'select') {
|
|
221
|
-
return 'select'
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return 'default'
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export function buildFeedbackReviewModel(data: FeedbackData): FeedbackReviewModel {
|
|
228
|
-
const groupedEntries: FeedbackGroupedEntries = {
|
|
229
|
-
actionable: data.entries.filter(entry => entry.actionable),
|
|
230
|
-
informational: data.entries.filter(entry => !entry.actionable),
|
|
231
|
-
}
|
|
232
|
-
const summary: FeedbackSummary = {
|
|
233
|
-
organizationName: data.organizationName,
|
|
234
|
-
repository: data.repository,
|
|
235
|
-
commitHash: data.commitHash,
|
|
236
|
-
totalFeedbacks: data.entries.length,
|
|
237
|
-
actionableCount: groupedEntries.actionable.length,
|
|
238
|
-
informationalCount: groupedEntries.informational.length,
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return {
|
|
242
|
-
summary,
|
|
243
|
-
groupedEntries,
|
|
244
|
-
emptyState: summary.totalFeedbacks === 0
|
|
245
|
-
? 'no_feedback'
|
|
246
|
-
: summary.actionableCount === 0
|
|
247
|
-
? 'informational_only'
|
|
248
|
-
: 'none',
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function formatFeedbackEntryLine(entry: FeedbackEntry): string {
|
|
253
|
-
return `#${entry.number} [${entry.feedback.status}] ${truncate(getFeedbackEntryTitle(entry), 88)}`
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export function formatFeedbackSummary(summary: FeedbackSummary, emptyState: FeedbackEmptyState): string {
|
|
257
|
-
return [
|
|
258
|
-
`Organization: ${summary.organizationName}`,
|
|
259
|
-
`Repository: ${summary.repository}`,
|
|
260
|
-
`Commit: ${summary.commitHash}`,
|
|
261
|
-
`Feedbacks: ${summary.totalFeedbacks}`,
|
|
262
|
-
`Actionable suggestions: ${summary.actionableCount}`,
|
|
263
|
-
`Informational items: ${summary.informationalCount}`,
|
|
264
|
-
`Review state: ${describeFeedbackEmptyState(emptyState)}`,
|
|
265
|
-
].join('\n')
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function formatFeedbackCatalog(entries: FeedbackEntry[]): string {
|
|
269
|
-
if (entries.length === 0) {
|
|
270
|
-
return 'No feedback returned for this commit.'
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const groupedEntries = {
|
|
274
|
-
actionable: entries.filter(entry => entry.actionable),
|
|
275
|
-
informational: entries.filter(entry => !entry.actionable),
|
|
276
|
-
}
|
|
277
|
-
const lines: string[] = []
|
|
278
|
-
|
|
279
|
-
if (groupedEntries.actionable.length > 0) {
|
|
280
|
-
lines.push('Actionable')
|
|
281
|
-
for (const entry of groupedEntries.actionable) {
|
|
282
|
-
lines.push(`- ${formatFeedbackEntryLine(entry)}`)
|
|
283
|
-
lines.push(` reason: ${entry.feedback.reason || '<empty>'}`)
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (groupedEntries.informational.length > 0) {
|
|
288
|
-
if (lines.length > 0) {
|
|
289
|
-
lines.push('')
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
lines.push('Informational')
|
|
293
|
-
for (const entry of groupedEntries.informational) {
|
|
294
|
-
lines.push(`- ${formatFeedbackEntryLine(entry)}`)
|
|
295
|
-
lines.push(` reason: ${entry.feedback.reason || '<empty>'}`)
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return lines.join('\n')
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function buildFeedbackSelectionGroups(entries: FeedbackEntry[]): Record<string, Array<Option<string>>> {
|
|
303
|
-
const groupedEntries = {
|
|
304
|
-
actionable: entries.filter(entry => entry.actionable),
|
|
305
|
-
informational: entries.filter(entry => !entry.actionable),
|
|
306
|
-
}
|
|
307
|
-
const groups: Record<string, Array<Option<string>>> = {}
|
|
308
|
-
|
|
309
|
-
if (groupedEntries.actionable.length > 0) {
|
|
310
|
-
groups.Actionable = groupedEntries.actionable.map(entry => ({
|
|
311
|
-
value: entry.id,
|
|
312
|
-
label: `#${entry.number} ${truncate(getFeedbackEntryTitle(entry), 80)}`,
|
|
313
|
-
hint: truncate(entry.feedback.reason || '<empty>', 60),
|
|
314
|
-
}))
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (groupedEntries.informational.length > 0) {
|
|
318
|
-
groups.Informational = groupedEntries.informational.map(entry => ({
|
|
319
|
-
value: entry.id,
|
|
320
|
-
label: `#${entry.number} ${truncate(getFeedbackEntryTitle(entry), 80)}`,
|
|
321
|
-
hint: truncate(entry.feedback.reason || 'No suggestion text', 60),
|
|
322
|
-
}))
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return groups
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function formatFeedbackSelectionSummary(input: {
|
|
329
|
-
selectionMode: FeedbackSelectionMode
|
|
330
|
-
selectedFeedbacks: FeedbackEntry[]
|
|
331
|
-
}): string {
|
|
332
|
-
return [
|
|
333
|
-
`Mode: ${describeFeedbackSelectionMode(input.selectionMode)}`,
|
|
334
|
-
`Selected suggestions: ${input.selectedFeedbacks.length}`,
|
|
335
|
-
`Selected IDs: ${input.selectedFeedbacks.length > 0 ? input.selectedFeedbacks.map(entry => `#${entry.number}`).join(', ') : '<none>'}`,
|
|
336
|
-
...input.selectedFeedbacks.map(entry => `- ${formatFeedbackEntryLine(entry)}`),
|
|
337
|
-
].join('\n')
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export function formatFeedbackOutcomeSummary(input: {
|
|
341
|
-
commitHash: string
|
|
342
|
-
selectionMode: FeedbackSelectionMode
|
|
343
|
-
selectedFeedbacks: FeedbackEntry[]
|
|
344
|
-
}): string {
|
|
345
|
-
return [
|
|
346
|
-
`Commit: ${input.commitHash}`,
|
|
347
|
-
`Mode: ${describeFeedbackSelectionMode(input.selectionMode)}`,
|
|
348
|
-
`Selected suggestions: ${input.selectedFeedbacks.length}`,
|
|
349
|
-
...input.selectedFeedbacks.map(entry => `- ${formatFeedbackEntryLine(entry)}`),
|
|
350
|
-
].join('\n')
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export function buildClaudeFeedbackPrompt(input: {
|
|
354
|
-
repoRoot: string
|
|
355
|
-
organizationName: string
|
|
356
|
-
repository: string
|
|
357
|
-
commitHash: string
|
|
358
|
-
selectedFeedbacks: FeedbackEntry[]
|
|
359
|
-
}): string {
|
|
360
|
-
const feedbackBlocks = input.selectedFeedbacks
|
|
361
|
-
.map(entry => [
|
|
362
|
-
`Feedback #${entry.number}`,
|
|
363
|
-
`Status: ${entry.feedback.status}`,
|
|
364
|
-
`Reason: ${entry.feedback.reason}`,
|
|
365
|
-
`Suggestion: ${entry.feedback.suggestion}`,
|
|
366
|
-
].join('\n'))
|
|
367
|
-
.join('\n\n')
|
|
368
|
-
|
|
369
|
-
return [
|
|
370
|
-
'You are working inside a local git repository.',
|
|
371
|
-
'',
|
|
372
|
-
`Repository root: ${input.repoRoot}`,
|
|
373
|
-
`Repository: ${input.organizationName}/${input.repository}`,
|
|
374
|
-
`Feedback commit hash: ${input.commitHash}`,
|
|
375
|
-
'',
|
|
376
|
-
'Address the selected feedback suggestions below.',
|
|
377
|
-
'- Inspect the repository and make the code changes that best address the selected suggestions.',
|
|
378
|
-
'- Run relevant verification when practical and mention what you checked.',
|
|
379
|
-
'- If a suggestion should not be implemented as written, explain why in your final summary.',
|
|
380
|
-
'- Do not create a git commit or push changes unless explicitly asked.',
|
|
381
|
-
'',
|
|
382
|
-
'Selected feedback:',
|
|
383
|
-
feedbackBlocks,
|
|
384
|
-
].join('\n')
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function serializeFeedbackEntries(entries: FeedbackEntry[]): FeedbackRunResult['feedbacks'] {
|
|
388
|
-
return entries.map(entry => ({
|
|
389
|
-
id: entry.id,
|
|
390
|
-
number: entry.number,
|
|
391
|
-
status: entry.feedback.status,
|
|
392
|
-
reason: entry.feedback.reason,
|
|
393
|
-
suggestion: entry.feedback.suggestion,
|
|
394
|
-
actionable: entry.actionable,
|
|
395
|
-
}))
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
export function buildFeedbackNextActions(input: {
|
|
399
|
-
launchedClaude: boolean
|
|
400
|
-
actionableCount: number
|
|
401
|
-
emptyState: FeedbackEmptyState
|
|
402
|
-
}): string[] {
|
|
403
|
-
if (!input.launchedClaude) {
|
|
404
|
-
if (input.emptyState === 'no_feedback') {
|
|
405
|
-
return ['Push a feedback-producing commit or choose another checkout, then rerun `tela feedback --list`.']
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (input.emptyState === 'informational_only') {
|
|
409
|
-
return [
|
|
410
|
-
'Review the informational feedback above for context.',
|
|
411
|
-
'Rerun `tela feedback --list` after a later push if you want a fresh snapshot.',
|
|
412
|
-
]
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return input.actionableCount > 0
|
|
416
|
-
? ['Run `tela feedback --apply all` or `tela feedback --apply select` when you are ready to address suggestions.']
|
|
417
|
-
: ['No follow-up action is required for this commit.']
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return [
|
|
421
|
-
'Review the local diff before committing the Claude changes.',
|
|
422
|
-
'Run `tela push` after the feedback-driven changes look correct.',
|
|
423
|
-
'Rerun `tela feedback --list` after the next push if you want another feedback snapshot.',
|
|
424
|
-
]
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
export function hasActionableFeedback(entries: FeedbackEntry[]): boolean {
|
|
428
|
-
return entries.some(entry => entry.actionable)
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
export function buildFeedbackRunResult(input: {
|
|
432
|
-
data: FeedbackData
|
|
433
|
-
selectionMode: FeedbackSelectionMode
|
|
434
|
-
launchedClaude: boolean
|
|
435
|
-
selectedFeedbacks?: FeedbackEntry[]
|
|
436
|
-
diffStat?: string
|
|
437
|
-
}): FeedbackRunResult {
|
|
438
|
-
const review = buildFeedbackReviewModel(input.data)
|
|
439
|
-
const selectedFeedbacks = input.selectedFeedbacks ?? []
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
ok: true,
|
|
443
|
-
organizationName: input.data.organizationName,
|
|
444
|
-
repository: input.data.repository,
|
|
445
|
-
commitHash: input.data.commitHash,
|
|
446
|
-
summary: review.summary,
|
|
447
|
-
emptyState: review.emptyState,
|
|
448
|
-
selectionMode: input.selectionMode,
|
|
449
|
-
feedbacks: serializeFeedbackEntries(input.data.entries),
|
|
450
|
-
selectedIds: selectedFeedbacks.length > 0 ? selectedFeedbacks.map(entry => entry.id) : undefined,
|
|
451
|
-
launchedClaude: input.launchedClaude,
|
|
452
|
-
diffStat: input.diffStat,
|
|
453
|
-
nextActions: buildFeedbackNextActions({
|
|
454
|
-
launchedClaude: input.launchedClaude,
|
|
455
|
-
actionableCount: review.summary.actionableCount,
|
|
456
|
-
emptyState: review.emptyState,
|
|
457
|
-
}),
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export async function loadFeedbackData(options: {
|
|
462
|
-
cwd?: string
|
|
463
|
-
config: TelaCliConfig
|
|
464
|
-
fetchRepositoryLookup?: typeof fetchRepositoryLookup
|
|
465
|
-
fetchCommitFeedback?: typeof fetchCommitFeedback
|
|
466
|
-
getOriginUrl?: typeof getOriginUrl
|
|
467
|
-
getHeadCommitHash?: typeof getHeadCommitHash
|
|
468
|
-
}): Promise<FeedbackData> {
|
|
469
|
-
const repoRoot = options.cwd ?? process.cwd()
|
|
470
|
-
const fetchRepositoryLookupFn = options.fetchRepositoryLookup ?? fetchRepositoryLookup
|
|
471
|
-
const fetchCommitFeedbackFn = options.fetchCommitFeedback ?? fetchCommitFeedback
|
|
472
|
-
const getOriginUrlFn = options.getOriginUrl ?? getOriginUrl
|
|
473
|
-
const getHeadCommitHashFn = options.getHeadCommitHash ?? getHeadCommitHash
|
|
474
|
-
|
|
475
|
-
const derivedRepository = await resolveRepositoryName(repoRoot, getOriginUrlFn)
|
|
476
|
-
const repoDetails = await fetchRepositoryLookupFn(derivedRepository, options.config)
|
|
477
|
-
const commitHash = await getHeadCommitHashFn(repoRoot)
|
|
478
|
-
const feedbackResponse = await fetchCommitFeedbackFn(
|
|
479
|
-
repoDetails.organization.name,
|
|
480
|
-
repoDetails.agentRepo.repository,
|
|
481
|
-
commitHash,
|
|
482
|
-
options.config,
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
const entries = buildFeedbackEntries(feedbackResponse.feedbacks)
|
|
486
|
-
const actionableEntries = entries.filter(entry => entry.actionable)
|
|
487
|
-
|
|
488
|
-
return {
|
|
489
|
-
repoRoot,
|
|
490
|
-
organizationName: repoDetails.organization.name,
|
|
491
|
-
repository: repoDetails.agentRepo.repository,
|
|
492
|
-
commitHash: feedbackResponse.commitHash,
|
|
493
|
-
entries,
|
|
494
|
-
actionableEntries,
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
export async function launchFeedbackClaude(options: {
|
|
499
|
-
repoRoot: string
|
|
500
|
-
prompt: string
|
|
501
|
-
runInteractiveCommand?: typeof runInteractiveCommand
|
|
502
|
-
}): Promise<void> {
|
|
503
|
-
const runInteractiveCommandFn = options.runInteractiveCommand ?? runInteractiveCommand
|
|
504
|
-
|
|
505
|
-
try {
|
|
506
|
-
await runInteractiveCommandFn(['claude', options.prompt], { cwd: options.repoRoot })
|
|
507
|
-
}
|
|
508
|
-
catch (error) {
|
|
509
|
-
if (error instanceof CommandError) {
|
|
510
|
-
const stdout = error.result.stdout.trim()
|
|
511
|
-
const stderr = error.result.stderr.trim()
|
|
512
|
-
throw createUserFacingError({
|
|
513
|
-
code: 'feedback_claude_failed',
|
|
514
|
-
summary: 'Claude exited before completing the feedback review.',
|
|
515
|
-
details: stderr || stdout || `Claude exited with code ${error.result.exitCode}.`,
|
|
516
|
-
fixes: ['Review the Claude session output above, then rerun `tela feedback` when you are ready.'],
|
|
517
|
-
evidence: [
|
|
518
|
-
`Command: ${error.result.command.join(' ')}`,
|
|
519
|
-
stdout ? `stdout: ${stdout}` : '',
|
|
520
|
-
stderr ? `stderr: ${stderr}` : '',
|
|
521
|
-
].filter(Boolean),
|
|
522
|
-
cause: error,
|
|
523
|
-
})
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
throw error
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
export async function runFeedbackCommand(options: RunFeedbackCommandOptions): Promise<FeedbackRunResult> {
|
|
531
|
-
const repoRoot = options.cwd ?? process.cwd()
|
|
532
|
-
const prompts = options.prompts
|
|
533
|
-
const ensureConfig = options.ensureConfigInteractive ?? ensureConfigInteractive
|
|
534
|
-
const fetchRepositoryLookupFn = options.fetchRepositoryLookup ?? fetchRepositoryLookup
|
|
535
|
-
const fetchCommitFeedbackFn = options.fetchCommitFeedback ?? fetchCommitFeedback
|
|
536
|
-
const getOriginUrlFn = options.getOriginUrl ?? getOriginUrl
|
|
537
|
-
const getHeadCommitHashFn = options.getHeadCommitHash ?? getHeadCommitHash
|
|
538
|
-
const getDiffStatAgainstHeadFn = options.getDiffStatAgainstHead ?? getDiffStatAgainstHead
|
|
539
|
-
const commandExistsFn = options.commandExists ?? commandExists
|
|
540
|
-
const runInteractiveCommandFn = options.runInteractiveCommand ?? runInteractiveCommand
|
|
541
|
-
const selectionMode = getFeedbackSelectionMode(options)
|
|
542
|
-
|
|
543
|
-
const config: TelaCliConfig = await ensureConfig()
|
|
544
|
-
const data = await loadFeedbackData({
|
|
545
|
-
cwd: repoRoot,
|
|
546
|
-
config,
|
|
547
|
-
fetchRepositoryLookup: fetchRepositoryLookupFn,
|
|
548
|
-
fetchCommitFeedback: fetchCommitFeedbackFn,
|
|
549
|
-
getOriginUrl: getOriginUrlFn,
|
|
550
|
-
getHeadCommitHash: getHeadCommitHashFn,
|
|
551
|
-
})
|
|
552
|
-
const review = buildFeedbackReviewModel(data)
|
|
553
|
-
const feedbackEntries = data.entries
|
|
554
|
-
const actionableFeedbacks = data.actionableEntries
|
|
555
|
-
|
|
556
|
-
prompts.note(formatFeedbackSummary(review.summary, review.emptyState), 'Feedback summary')
|
|
557
|
-
prompts.note(formatFeedbackCatalog(feedbackEntries), 'Feedback catalog')
|
|
558
|
-
|
|
559
|
-
if (selectionMode === 'list' || review.emptyState !== 'none') {
|
|
560
|
-
return buildFeedbackRunResult({
|
|
561
|
-
data,
|
|
562
|
-
selectionMode,
|
|
563
|
-
launchedClaude: false,
|
|
564
|
-
})
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
if (!await commandExistsFn('claude')) {
|
|
568
|
-
throw createUserFacingError({
|
|
569
|
-
code: 'feedback_claude_missing',
|
|
570
|
-
summary: 'Claude CLI is not available on PATH.',
|
|
571
|
-
fixes: ['Install the Claude CLI before using `tela feedback --apply ...`.'],
|
|
572
|
-
})
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
let selectedFeedbacks = selectionMode === 'select' ? [] : actionableFeedbacks
|
|
576
|
-
if (selectionMode === 'select') {
|
|
577
|
-
const selectedIds = await prompts.groupMultiselect({
|
|
578
|
-
message: 'Select the suggestions to address with Claude',
|
|
579
|
-
options: buildFeedbackSelectionGroups(feedbackEntries),
|
|
580
|
-
cursorAt: actionableFeedbacks[0]?.id,
|
|
581
|
-
})
|
|
582
|
-
|
|
583
|
-
if (prompts.isCancel(selectedIds)) {
|
|
584
|
-
prompts.cancel('Feedback selection cancelled.')
|
|
585
|
-
return buildFeedbackRunResult({
|
|
586
|
-
data,
|
|
587
|
-
selectionMode,
|
|
588
|
-
launchedClaude: false,
|
|
589
|
-
})
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
const selectedIdSet = new Set(promptValue(selectedIds))
|
|
593
|
-
selectedFeedbacks = actionableFeedbacks.filter(entry => selectedIdSet.has(entry.id))
|
|
594
|
-
}
|
|
595
|
-
else if (selectionMode === 'default') {
|
|
596
|
-
const selectionChoice = await prompts.select<'all' | 'manual' | 'cancel'>({
|
|
597
|
-
message: 'Which suggestions do you want to review with Claude?',
|
|
598
|
-
options: [
|
|
599
|
-
{ value: 'all', label: 'Apply all actionable suggestions' },
|
|
600
|
-
{ value: 'manual', label: 'Choose suggestions individually' },
|
|
601
|
-
{ value: 'cancel', label: 'Finish review without launching Claude' },
|
|
602
|
-
],
|
|
603
|
-
initialValue: 'all',
|
|
604
|
-
})
|
|
605
|
-
|
|
606
|
-
if (prompts.isCancel(selectionChoice) || promptValue(selectionChoice) === 'cancel') {
|
|
607
|
-
prompts.cancel('Feedback application cancelled.')
|
|
608
|
-
return buildFeedbackRunResult({
|
|
609
|
-
data,
|
|
610
|
-
selectionMode,
|
|
611
|
-
launchedClaude: false,
|
|
612
|
-
})
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (promptValue(selectionChoice) === 'manual') {
|
|
616
|
-
const selectedIds = await prompts.groupMultiselect({
|
|
617
|
-
message: 'Select the suggestions to address with Claude',
|
|
618
|
-
options: buildFeedbackSelectionGroups(feedbackEntries),
|
|
619
|
-
cursorAt: actionableFeedbacks[0]?.id,
|
|
620
|
-
})
|
|
621
|
-
|
|
622
|
-
if (prompts.isCancel(selectedIds)) {
|
|
623
|
-
prompts.cancel('Feedback selection cancelled.')
|
|
624
|
-
return buildFeedbackRunResult({
|
|
625
|
-
data,
|
|
626
|
-
selectionMode,
|
|
627
|
-
launchedClaude: false,
|
|
628
|
-
})
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
const selectedIdSet = new Set(promptValue(selectedIds))
|
|
632
|
-
selectedFeedbacks = actionableFeedbacks.filter(entry => selectedIdSet.has(entry.id))
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
prompts.note(formatFeedbackSelectionSummary({
|
|
637
|
-
selectionMode,
|
|
638
|
-
selectedFeedbacks,
|
|
639
|
-
}), 'Selection summary')
|
|
640
|
-
|
|
641
|
-
if (selectedFeedbacks.length === 0) {
|
|
642
|
-
prompts.cancel('Feedback review finished without launching Claude.')
|
|
643
|
-
return buildFeedbackRunResult({
|
|
644
|
-
data,
|
|
645
|
-
selectionMode,
|
|
646
|
-
launchedClaude: false,
|
|
647
|
-
})
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
const prompt = buildClaudeFeedbackPrompt({
|
|
651
|
-
repoRoot: data.repoRoot,
|
|
652
|
-
organizationName: data.organizationName,
|
|
653
|
-
repository: data.repository,
|
|
654
|
-
commitHash: data.commitHash,
|
|
655
|
-
selectedFeedbacks,
|
|
656
|
-
})
|
|
657
|
-
|
|
658
|
-
prompts.note(prompt, 'Claude task')
|
|
659
|
-
|
|
660
|
-
if (selectionMode === 'default') {
|
|
661
|
-
const confirmation = await prompts.confirm({
|
|
662
|
-
message: `Launch Claude to address ${selectedFeedbacks.length} suggestion${selectedFeedbacks.length === 1 ? '' : 's'}?`,
|
|
663
|
-
active: 'apply',
|
|
664
|
-
inactive: 'cancel',
|
|
665
|
-
initialValue: true,
|
|
666
|
-
})
|
|
667
|
-
|
|
668
|
-
if (prompts.isCancel(confirmation) || !promptValue(confirmation)) {
|
|
669
|
-
prompts.cancel('Feedback application cancelled.')
|
|
670
|
-
return buildFeedbackRunResult({
|
|
671
|
-
data,
|
|
672
|
-
selectionMode,
|
|
673
|
-
launchedClaude: false,
|
|
674
|
-
selectedFeedbacks,
|
|
675
|
-
})
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
await launchFeedbackClaude({
|
|
680
|
-
repoRoot: data.repoRoot,
|
|
681
|
-
prompt,
|
|
682
|
-
runInteractiveCommand: runInteractiveCommandFn,
|
|
683
|
-
})
|
|
684
|
-
|
|
685
|
-
const diffStat = (await getDiffStatAgainstHeadFn(repoRoot)) || '<no local diff detected>'
|
|
686
|
-
prompts.note(formatFeedbackOutcomeSummary({
|
|
687
|
-
commitHash: data.commitHash,
|
|
688
|
-
selectionMode,
|
|
689
|
-
selectedFeedbacks,
|
|
690
|
-
}), 'Review outcome')
|
|
691
|
-
prompts.note(diffStat, 'Local diff summary')
|
|
692
|
-
prompts.log.success(`Claude finished reviewing ${selectedFeedbacks.length} suggestion${selectedFeedbacks.length === 1 ? '' : 's'}`)
|
|
693
|
-
|
|
694
|
-
return buildFeedbackRunResult({
|
|
695
|
-
data,
|
|
696
|
-
selectionMode,
|
|
697
|
-
launchedClaude: true,
|
|
698
|
-
selectedFeedbacks,
|
|
699
|
-
diffStat,
|
|
700
|
-
})
|
|
701
|
-
}
|