@meistrari/tela-cli 0.1.0
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 +95 -0
- package/cli.ts +25 -0
- package/package.json +30 -0
- package/src/agent-api.ts +124 -0
- package/src/auth.ts +16 -0
- package/src/commands/doctor.ts +35 -0
- package/src/commands/feedback.ts +701 -0
- package/src/commands/metadata.ts +208 -0
- package/src/commands/push.ts +208 -0
- package/src/commands/run.ts +1601 -0
- package/src/commands/status.ts +87 -0
- package/src/commands/sync.ts +245 -0
- package/src/commit-message.ts +69 -0
- package/src/config.ts +384 -0
- package/src/diagnostics.ts +578 -0
- package/src/errors.ts +122 -0
- package/src/git.ts +250 -0
- package/src/ink/bark.tsx +288 -0
- package/src/ink/components.tsx +175 -0
- package/src/ink/runtime.tsx +89 -0
- package/src/ink/screens/feedback-screen.tsx +394 -0
- package/src/ink/screens/push-screen.tsx +231 -0
- package/src/ink/screens/run-screen.tsx +291 -0
- package/src/ink/screens/setup-screen.tsx +352 -0
- package/src/ink/screens/sync-screen.tsx +282 -0
- package/src/process.ts +251 -0
- package/src/program.ts +1872 -0
- package/src/runtime.ts +147 -0
- package/src/types.ts +80 -0
- package/src/ui.ts +297 -0
package/src/program.ts
ADDED
|
@@ -0,0 +1,1872 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, statSync } from 'node:fs'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { Command } from 'commander'
|
|
5
|
+
import {
|
|
6
|
+
createInkBarkSession,
|
|
7
|
+
shouldShowBarkIntro,
|
|
8
|
+
type BarkIntroSession,
|
|
9
|
+
} from './ink/bark.tsx'
|
|
10
|
+
import {
|
|
11
|
+
FeedbackClaudeHandoffScreen,
|
|
12
|
+
FeedbackOutcomeScreen,
|
|
13
|
+
FeedbackSelectionScreen,
|
|
14
|
+
} from './ink/screens/feedback-screen.tsx'
|
|
15
|
+
import { PushScreen } from './ink/screens/push-screen.tsx'
|
|
16
|
+
import { RunChatHandoffScreen, RunScreen } from './ink/screens/run-screen.tsx'
|
|
17
|
+
import { SetupScreen, type SetupScreenResult } from './ink/screens/setup-screen.tsx'
|
|
18
|
+
import { SyncScreen, type SyncProgressItem } from './ink/screens/sync-screen.tsx'
|
|
19
|
+
import { renderCommandScreen } from './ink/runtime.tsx'
|
|
20
|
+
import {
|
|
21
|
+
checkAgentApiReachability,
|
|
22
|
+
getAgentApiOriginCheckStatus,
|
|
23
|
+
getAgentApiOriginWarning,
|
|
24
|
+
} from './diagnostics.ts'
|
|
25
|
+
import { createUserFacingError } from './errors.ts'
|
|
26
|
+
import {
|
|
27
|
+
getCommandMetadata,
|
|
28
|
+
listCommandMetadata,
|
|
29
|
+
type CommandMetadata,
|
|
30
|
+
} from './commands/metadata.ts'
|
|
31
|
+
import {
|
|
32
|
+
buildFeedbackRunResult,
|
|
33
|
+
buildFeedbackNextActions,
|
|
34
|
+
buildFeedbackReviewModel,
|
|
35
|
+
buildClaudeFeedbackPrompt,
|
|
36
|
+
describeFeedbackEmptyState,
|
|
37
|
+
describeFeedbackSelectionMode,
|
|
38
|
+
getFeedbackSelectionMode,
|
|
39
|
+
launchFeedbackClaude,
|
|
40
|
+
loadFeedbackData,
|
|
41
|
+
runFeedbackCommand,
|
|
42
|
+
type FeedbackCommandFlags,
|
|
43
|
+
type FeedbackData,
|
|
44
|
+
type FeedbackRunResult,
|
|
45
|
+
type PromptModule as FeedbackPromptModule,
|
|
46
|
+
} from './commands/feedback.ts'
|
|
47
|
+
import {
|
|
48
|
+
previewPush,
|
|
49
|
+
pushRepository,
|
|
50
|
+
type PushPreview,
|
|
51
|
+
} from './commands/push.ts'
|
|
52
|
+
import {
|
|
53
|
+
createInitialRunSnapshot,
|
|
54
|
+
prepareLocalAgentRun,
|
|
55
|
+
renderRunArtifactCard,
|
|
56
|
+
renderRunExecutionRecap,
|
|
57
|
+
runLocalAgentCommand,
|
|
58
|
+
type RunCommandResult,
|
|
59
|
+
} from './commands/run.ts'
|
|
60
|
+
import {
|
|
61
|
+
categorizeSyncError,
|
|
62
|
+
listCloneRootRepositories,
|
|
63
|
+
syncAllRepositories,
|
|
64
|
+
syncCurrentRepository,
|
|
65
|
+
syncRepository,
|
|
66
|
+
type SyncAllProgressEvent,
|
|
67
|
+
type SyncFailureCategory,
|
|
68
|
+
} from './commands/sync.ts'
|
|
69
|
+
import { getDoctorReport, renderDoctorReport } from './commands/doctor.ts'
|
|
70
|
+
import { getStatusReport, renderStatusReport } from './commands/status.ts'
|
|
71
|
+
import {
|
|
72
|
+
AGENT_API_BROWSER_HELP,
|
|
73
|
+
DEFAULT_AGENT_API_URL,
|
|
74
|
+
ensureConfigInteractive,
|
|
75
|
+
getDefaultCloneRoot,
|
|
76
|
+
inspectSavedConfig,
|
|
77
|
+
redactApiKey,
|
|
78
|
+
saveConfig,
|
|
79
|
+
validateAgentApiUrl,
|
|
80
|
+
validateApiKey,
|
|
81
|
+
validateCloneRoot,
|
|
82
|
+
setupConfigInteractive,
|
|
83
|
+
type ConfigInspection,
|
|
84
|
+
} from './config.ts'
|
|
85
|
+
import { findRepositoryRoot, getDiffStatAgainstHead } from './git.ts'
|
|
86
|
+
import { commandExists, runInteractiveCommand, runStreamingCommand } from './process.ts'
|
|
87
|
+
import { CommandRuntime } from './runtime.ts'
|
|
88
|
+
import type {
|
|
89
|
+
BatchSyncFailure,
|
|
90
|
+
SyncResult,
|
|
91
|
+
TelaCliConfig,
|
|
92
|
+
} from './types.ts'
|
|
93
|
+
import {
|
|
94
|
+
bulletList,
|
|
95
|
+
bootstrapPane,
|
|
96
|
+
joinBlocks,
|
|
97
|
+
kvList,
|
|
98
|
+
nextActions,
|
|
99
|
+
resultCard,
|
|
100
|
+
section,
|
|
101
|
+
stepList,
|
|
102
|
+
table,
|
|
103
|
+
} from './ui.ts'
|
|
104
|
+
|
|
105
|
+
type PromptModule = FeedbackPromptModule
|
|
106
|
+
|
|
107
|
+
function promptValue<T>(value: T | symbol): T {
|
|
108
|
+
return value as T
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface CliDeps {
|
|
112
|
+
prompts?: PromptModule
|
|
113
|
+
ensureConfigInteractive?: typeof ensureConfigInteractive
|
|
114
|
+
setupConfigInteractive?: typeof setupConfigInteractive
|
|
115
|
+
inspectSavedConfig?: typeof inspectSavedConfig
|
|
116
|
+
findRepositoryRoot?: typeof findRepositoryRoot
|
|
117
|
+
syncRepository?: typeof syncRepository
|
|
118
|
+
syncAllRepositories?: typeof syncAllRepositories
|
|
119
|
+
syncCurrentRepository?: typeof syncCurrentRepository
|
|
120
|
+
listCloneRootRepositories?: typeof listCloneRootRepositories
|
|
121
|
+
previewPush?: typeof previewPush
|
|
122
|
+
pushRepository?: typeof pushRepository
|
|
123
|
+
runLocalAgentCommand?: typeof runLocalAgentCommand
|
|
124
|
+
runFeedbackCommand?: typeof runFeedbackCommand
|
|
125
|
+
loadFeedbackData?: typeof loadFeedbackData
|
|
126
|
+
launchFeedbackClaude?: typeof launchFeedbackClaude
|
|
127
|
+
prepareLocalAgentRun?: typeof prepareLocalAgentRun
|
|
128
|
+
getDiffStatAgainstHead?: typeof getDiffStatAgainstHead
|
|
129
|
+
getStatusReport?: typeof getStatusReport
|
|
130
|
+
getDoctorReport?: typeof getDoctorReport
|
|
131
|
+
createInkBarkSession?: typeof createInkBarkSession
|
|
132
|
+
commandExists?: typeof commandExists
|
|
133
|
+
runInteractiveCommand?: typeof runInteractiveCommand
|
|
134
|
+
runStreamingCommand?: typeof runStreamingCommand
|
|
135
|
+
fetchFn?: typeof fetch
|
|
136
|
+
writeStdout?: (message: string) => void
|
|
137
|
+
writeStderr?: (message: string) => void
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface GlobalCliOptions {
|
|
141
|
+
json?: boolean
|
|
142
|
+
verbose?: boolean
|
|
143
|
+
silentBark?: boolean
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface SyncCommandOptions extends GlobalCliOptions {
|
|
147
|
+
all?: boolean
|
|
148
|
+
current?: boolean
|
|
149
|
+
dryRun?: boolean
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface PushCommandOptions extends GlobalCliOptions {
|
|
153
|
+
message?: string
|
|
154
|
+
claude?: boolean
|
|
155
|
+
manual?: boolean
|
|
156
|
+
dryRun?: boolean
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface SetupCommandResult {
|
|
160
|
+
ok: true
|
|
161
|
+
config: TelaCliConfig
|
|
162
|
+
warnings: string[]
|
|
163
|
+
nextActions: string[]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface SyncCommandResult {
|
|
167
|
+
ok: boolean
|
|
168
|
+
mode: 'current' | 'repository' | 'all'
|
|
169
|
+
successes: SyncResult[]
|
|
170
|
+
failures: BatchSyncFailure[]
|
|
171
|
+
nextActions: string[]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface PushCommandResult {
|
|
175
|
+
ok: boolean
|
|
176
|
+
branch?: string
|
|
177
|
+
commitMessage?: string
|
|
178
|
+
commitSha?: string
|
|
179
|
+
retriedCommit?: boolean
|
|
180
|
+
nextActions: string[]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
type StatusCommandOptions = GlobalCliOptions
|
|
184
|
+
type DoctorCommandOptions = GlobalCliOptions
|
|
185
|
+
interface RunCommandOptions extends GlobalCliOptions {
|
|
186
|
+
plain?: boolean
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function renderOptionsForStdout(): { decorate: boolean } {
|
|
190
|
+
return { decorate: Boolean(process.stdout.isTTY) }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isInteractiveInkSession(options: GlobalCliOptions = {}): boolean {
|
|
194
|
+
return !options.json && Boolean(process.stdout.isTTY && process.stdin.isTTY)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function resolveRunSurface(options: RunCommandOptions = {}): 'plain' | 'viewer' | 'json' {
|
|
198
|
+
if (options.json) {
|
|
199
|
+
return 'json'
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (options.plain || !isInteractiveInkSession(options)) {
|
|
203
|
+
return 'plain'
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return 'viewer'
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getPromptModule(deps: CliDeps): PromptModule {
|
|
210
|
+
if (!deps.prompts) {
|
|
211
|
+
throw createUserFacingError({
|
|
212
|
+
code: 'interactive_prompt_unavailable',
|
|
213
|
+
summary: 'This command requires an interactive TTY for the requested flow.',
|
|
214
|
+
fixes: [
|
|
215
|
+
'Run the command in a terminal, or provide the flags needed for non-interactive execution.',
|
|
216
|
+
],
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return deps.prompts
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function createRuntime(deps: CliDeps, options: GlobalCliOptions = {}): CommandRuntime {
|
|
224
|
+
return new CommandRuntime({
|
|
225
|
+
json: options.json,
|
|
226
|
+
verbose: options.verbose,
|
|
227
|
+
prompts: deps.prompts,
|
|
228
|
+
writeStdout: deps.writeStdout,
|
|
229
|
+
writeStderr: deps.writeStderr,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function renderExamples(command: CommandMetadata): string {
|
|
234
|
+
return command.examples
|
|
235
|
+
.map(example => `${example.command.padEnd(36, ' ')} ${example.description}`)
|
|
236
|
+
.join('\n')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function renderRootHelp(program: Command): string {
|
|
240
|
+
const renderOptions = renderOptionsForStdout()
|
|
241
|
+
const commands = listCommandMetadata().map(command => [
|
|
242
|
+
`tela ${command.name}${command.argumentHint ? ` ${command.argumentHint}` : ''}`,
|
|
243
|
+
command.summary,
|
|
244
|
+
])
|
|
245
|
+
|
|
246
|
+
return joinBlocks([
|
|
247
|
+
bootstrapPane('Tela CLI', [
|
|
248
|
+
'One-shot workflows for setting up Tela, syncing repositories, running local agents, preparing pushes, and applying commit feedback.',
|
|
249
|
+
'',
|
|
250
|
+
'First successful path:',
|
|
251
|
+
stepList([
|
|
252
|
+
'tela setup',
|
|
253
|
+
'tela status',
|
|
254
|
+
'tela sync <repo> or tela sync --current',
|
|
255
|
+
'tela run',
|
|
256
|
+
'tela push',
|
|
257
|
+
'tela feedback --list',
|
|
258
|
+
]),
|
|
259
|
+
], renderOptions),
|
|
260
|
+
section('Commands', table(['Command', 'Summary'], commands, renderOptions), renderOptions),
|
|
261
|
+
section('Global flags', table(
|
|
262
|
+
['Flag', 'Description'],
|
|
263
|
+
[
|
|
264
|
+
['--silent-bark', 'Disable bark audio during interactive Ink startup'],
|
|
265
|
+
['--verbose', 'Include raw evidence for failures where available'],
|
|
266
|
+
['-h, --help', 'Show command help'],
|
|
267
|
+
],
|
|
268
|
+
renderOptions,
|
|
269
|
+
), renderOptions),
|
|
270
|
+
])
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function renderCommandHelp(command: Command): string {
|
|
274
|
+
const renderOptions = renderOptionsForStdout()
|
|
275
|
+
const metadata = getCommandMetadata(command.name())
|
|
276
|
+
if (!metadata) {
|
|
277
|
+
return renderRootHelp(command.parent ?? command)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const usage = `tela ${metadata.name}${metadata.argumentHint ? ` ${metadata.argumentHint}` : ''}`
|
|
281
|
+
const localOptionRows = command.options
|
|
282
|
+
.filter(option => option.long !== '--help')
|
|
283
|
+
.map(option => [option.flags, option.description || ''])
|
|
284
|
+
const globalOptionRows = (command.parent?.options ?? [])
|
|
285
|
+
.filter(option => option.long !== '--help')
|
|
286
|
+
.map(option => [option.flags, option.description || ''])
|
|
287
|
+
const optionRows = [...localOptionRows, ...globalOptionRows]
|
|
288
|
+
|
|
289
|
+
return joinBlocks([
|
|
290
|
+
bootstrapPane(`tela ${metadata.name}`, [
|
|
291
|
+
metadata.summary,
|
|
292
|
+
'',
|
|
293
|
+
`Usage: ${usage}`,
|
|
294
|
+
'',
|
|
295
|
+
metadata.whenToUse,
|
|
296
|
+
], renderOptions),
|
|
297
|
+
optionRows.length > 0
|
|
298
|
+
? section('Flags', table(['Flag', 'Description'], optionRows, renderOptions), renderOptions)
|
|
299
|
+
: undefined,
|
|
300
|
+
section('Examples', renderExamples(metadata), renderOptions),
|
|
301
|
+
section('Related', metadata.relatedCommands.map(name => `tela ${name}`).join('\n'), renderOptions),
|
|
302
|
+
nextActions([
|
|
303
|
+
`Run \`${usage}\` when the command inputs are ready.`,
|
|
304
|
+
`Use \`tela doctor\` if the command fails unexpectedly.`,
|
|
305
|
+
], renderOptions),
|
|
306
|
+
])
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function installCustomHelp(program: Command): void {
|
|
310
|
+
program.configureHelp({
|
|
311
|
+
formatHelp: (command) => command.parent ? renderCommandHelp(command) : renderRootHelp(command),
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function createBarkSession(
|
|
316
|
+
deps: CliDeps,
|
|
317
|
+
options: GlobalCliOptions,
|
|
318
|
+
commandTitle: string,
|
|
319
|
+
): BarkIntroSession {
|
|
320
|
+
const createSession = deps.createInkBarkSession ?? createInkBarkSession
|
|
321
|
+
return createSession({
|
|
322
|
+
commandTitle,
|
|
323
|
+
enabled: shouldShowBarkIntro({
|
|
324
|
+
json: options.json,
|
|
325
|
+
stdinIsTTY: process.stdin.isTTY,
|
|
326
|
+
stdoutIsTTY: process.stdout.isTTY,
|
|
327
|
+
}),
|
|
328
|
+
silentBark: options.silentBark,
|
|
329
|
+
env: process.env,
|
|
330
|
+
platform: process.platform,
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function emitBootstrap(runtime: CommandRuntime, title: string, summary: string): void {
|
|
335
|
+
runtime.emit(bootstrapPane(title, summary, { decorate: runtime.decorate }))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function ensureNotCancelled(prompts: PromptModule, value: unknown, message: string): void {
|
|
339
|
+
if (!prompts.isCancel(value)) {
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
prompts.cancel(message)
|
|
344
|
+
throw createUserFacingError({
|
|
345
|
+
code: 'cancelled',
|
|
346
|
+
summary: message,
|
|
347
|
+
fixes: ['Run the command again when you are ready.'],
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function summarizeConfigInspection(inspection: ConfigInspection): string {
|
|
352
|
+
if (!inspection.exists) {
|
|
353
|
+
return kvList([
|
|
354
|
+
{ label: 'Config path', value: inspection.path },
|
|
355
|
+
{ label: 'Status', value: 'missing' },
|
|
356
|
+
{ label: 'Clone root', value: getDefaultCloneRoot() },
|
|
357
|
+
])
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!inspection.valid || !inspection.config) {
|
|
361
|
+
return kvList([
|
|
362
|
+
{ label: 'Config path', value: inspection.path },
|
|
363
|
+
{ label: 'Status', value: 'invalid' },
|
|
364
|
+
{ label: 'Error', value: inspection.error ?? 'Unknown config error' },
|
|
365
|
+
])
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return kvList([
|
|
369
|
+
{ label: 'Config path', value: inspection.path },
|
|
370
|
+
{ label: 'Agent API origin', value: inspection.config.agentApiUrl },
|
|
371
|
+
{ label: 'Clone root', value: inspection.config.cloneRoot },
|
|
372
|
+
{ label: 'API key', value: redactApiKey(inspection.config.apiKey) },
|
|
373
|
+
])
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function buildSetupChecksRows(checks: Array<{ label: string, status: string, detail: string }>): string[][] {
|
|
377
|
+
return checks.map(check => [check.label, check.status, check.detail])
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function ensureCloneRootReady(prompts: PromptModule, cloneRoot: string): { created: boolean, warning?: string } {
|
|
381
|
+
if (!existsSync(cloneRoot)) {
|
|
382
|
+
return { created: false }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const stats = statSync(cloneRoot)
|
|
386
|
+
if (!stats.isDirectory()) {
|
|
387
|
+
throw createUserFacingError({
|
|
388
|
+
code: 'clone_root_invalid',
|
|
389
|
+
summary: `Clone root exists but is not a directory: ${cloneRoot}`,
|
|
390
|
+
fixes: [
|
|
391
|
+
`Move or remove \`${cloneRoot}\`, then rerun \`tela setup\`.`,
|
|
392
|
+
],
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return { created: false }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function executeSetupFlow(
|
|
400
|
+
input: {
|
|
401
|
+
agentApiUrl: string
|
|
402
|
+
cloneRoot: string
|
|
403
|
+
apiKey: string
|
|
404
|
+
createCloneRoot: boolean
|
|
405
|
+
},
|
|
406
|
+
deps: CliDeps = {},
|
|
407
|
+
): Promise<SetupScreenResult> {
|
|
408
|
+
const commandExistsFn = deps.commandExists ?? commandExists
|
|
409
|
+
const fetchFn = deps.fetchFn ?? fetch
|
|
410
|
+
const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
|
|
411
|
+
const config = {
|
|
412
|
+
agentApiUrl: input.agentApiUrl,
|
|
413
|
+
cloneRoot: input.cloneRoot,
|
|
414
|
+
apiKey: input.apiKey.trim() || inspection.config?.apiKey || '',
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const cloneRootExists = existsSync(config.cloneRoot)
|
|
418
|
+
if (!cloneRootExists && input.createCloneRoot) {
|
|
419
|
+
mkdirSync(config.cloneRoot, { recursive: true, mode: 0o755 })
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
saveConfig(config)
|
|
423
|
+
|
|
424
|
+
const [gitAvailable, claudeAvailable, reachability] = await Promise.all([
|
|
425
|
+
commandExistsFn('git'),
|
|
426
|
+
commandExistsFn('claude'),
|
|
427
|
+
checkAgentApiReachability(config.agentApiUrl, fetchFn),
|
|
428
|
+
])
|
|
429
|
+
|
|
430
|
+
const warnings = [
|
|
431
|
+
!existsSync(config.cloneRoot) ? `Clone root does not exist yet: ${config.cloneRoot}` : undefined,
|
|
432
|
+
gitAvailable ? undefined : '`git` is not available on PATH.',
|
|
433
|
+
claudeAvailable ? undefined : '`claude` is not available on PATH.',
|
|
434
|
+
getAgentApiOriginWarning(reachability),
|
|
435
|
+
].filter((warning): warning is string => Boolean(warning))
|
|
436
|
+
const checks = [
|
|
437
|
+
{ label: 'clone-root', status: existsSync(config.cloneRoot) ? 'ready' : 'missing', detail: config.cloneRoot },
|
|
438
|
+
{ label: 'git', status: gitAvailable ? 'available' : 'missing', detail: gitAvailable ? 'ready' : 'install git' },
|
|
439
|
+
{ label: 'claude', status: claudeAvailable ? 'available' : 'missing', detail: claudeAvailable ? 'ready' : 'install Claude CLI' },
|
|
440
|
+
{ label: 'agent-api-origin', status: getAgentApiOriginCheckStatus(reachability), detail: reachability.detail },
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
ok: true,
|
|
445
|
+
config,
|
|
446
|
+
warnings,
|
|
447
|
+
nextActions: [
|
|
448
|
+
'Run `tela status` to review the saved environment.',
|
|
449
|
+
existsSync(config.cloneRoot)
|
|
450
|
+
? 'Run `tela sync <repo>` or `tela sync --current` when you need a checkout.'
|
|
451
|
+
: 'Create the clone root or rerun setup before the first repository sync.',
|
|
452
|
+
claudeAvailable
|
|
453
|
+
? 'Use `tela run`, `tela push`, and `tela feedback` when you want Claude-assisted workflows.'
|
|
454
|
+
: 'Install the Claude CLI before using `tela run`, `tela feedback`, or Claude-generated commit messages.',
|
|
455
|
+
],
|
|
456
|
+
checks,
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function runSetupInk(
|
|
461
|
+
deps: CliDeps = {},
|
|
462
|
+
options: GlobalCliOptions = {},
|
|
463
|
+
barkSession = createBarkSession(deps, options, 'Setup'),
|
|
464
|
+
): Promise<SetupScreenResult> {
|
|
465
|
+
const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
|
|
466
|
+
await barkSession.playOnce()
|
|
467
|
+
return await renderCommandScreen<SetupScreenResult>((controller) => React.createElement(SetupScreen, {
|
|
468
|
+
controller,
|
|
469
|
+
currentConfigSummary: summarizeConfigInspection(inspection).split('\n'),
|
|
470
|
+
defaults: {
|
|
471
|
+
agentApiUrl: inspection.config?.agentApiUrl ?? DEFAULT_AGENT_API_URL,
|
|
472
|
+
cloneRoot: inspection.config?.cloneRoot ?? getDefaultCloneRoot(),
|
|
473
|
+
apiKey: inspection.config?.apiKey ?? '',
|
|
474
|
+
},
|
|
475
|
+
hasExistingApiKey: Boolean(inspection.config?.apiKey),
|
|
476
|
+
validateAgentApiUrl,
|
|
477
|
+
validateCloneRoot,
|
|
478
|
+
validateApiKey,
|
|
479
|
+
inspectCloneRoot: (path: string) => {
|
|
480
|
+
if (!existsSync(path)) {
|
|
481
|
+
return { exists: false, isDirectory: false }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
exists: true,
|
|
486
|
+
isDirectory: statSync(path).isDirectory(),
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
execute: async (input: {
|
|
490
|
+
agentApiUrl: string
|
|
491
|
+
cloneRoot: string
|
|
492
|
+
apiKey: string
|
|
493
|
+
createCloneRoot: boolean
|
|
494
|
+
}) => await executeSetupFlow(input, deps),
|
|
495
|
+
}))
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function ensureConfigForCommand(
|
|
499
|
+
deps: CliDeps,
|
|
500
|
+
options: GlobalCliOptions,
|
|
501
|
+
barkSession?: BarkIntroSession,
|
|
502
|
+
): Promise<TelaCliConfig> {
|
|
503
|
+
const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
|
|
504
|
+
if (inspection.valid && inspection.config) {
|
|
505
|
+
return inspection.config
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (isInteractiveInkSession(options)) {
|
|
509
|
+
const setupResult = await runSetupInk(
|
|
510
|
+
deps,
|
|
511
|
+
options,
|
|
512
|
+
barkSession ?? createBarkSession(deps, options, 'Setup'),
|
|
513
|
+
)
|
|
514
|
+
return setupResult.config
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (deps.prompts) {
|
|
518
|
+
const ensureConfig = deps.ensureConfigInteractive ?? ensureConfigInteractive
|
|
519
|
+
return await ensureConfig({ prompts: deps.prompts })
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
throw createUserFacingError({
|
|
523
|
+
code: 'config_required',
|
|
524
|
+
summary: 'Tela CLI is not configured.',
|
|
525
|
+
fixes: [
|
|
526
|
+
'Run `tela setup` in an interactive terminal first.',
|
|
527
|
+
],
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export async function runSetup(
|
|
532
|
+
deps: CliDeps = {},
|
|
533
|
+
options: GlobalCliOptions = {},
|
|
534
|
+
): Promise<SetupCommandResult> {
|
|
535
|
+
if (isInteractiveInkSession(options)) {
|
|
536
|
+
return await runSetupInk(deps, options)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const prompts = getPromptModule(deps)
|
|
540
|
+
const runtime = createRuntime(deps, options)
|
|
541
|
+
const setup = deps.setupConfigInteractive ?? setupConfigInteractive
|
|
542
|
+
const inspect = deps.inspectSavedConfig ?? inspectSavedConfig
|
|
543
|
+
const commandExistsFn = deps.commandExists ?? commandExists
|
|
544
|
+
const fetchFn = deps.fetchFn ?? fetch
|
|
545
|
+
const inspection = inspect()
|
|
546
|
+
|
|
547
|
+
prompts.intro('Tela CLI setup')
|
|
548
|
+
emitBootstrap(runtime, 'Tela setup', 'Bootstrap the local Tela environment before the first sync or push.')
|
|
549
|
+
runtime.emit(resultCard('Setup preflight', [
|
|
550
|
+
section('Current config', summarizeConfigInspection(inspection), { decorate: runtime.decorate }),
|
|
551
|
+
section('Agent API', AGENT_API_BROWSER_HELP, { decorate: runtime.decorate }),
|
|
552
|
+
section('What setup updates', bulletList([
|
|
553
|
+
'Agent API target and origin URL',
|
|
554
|
+
'Clone root directory',
|
|
555
|
+
'API key',
|
|
556
|
+
]), { decorate: runtime.decorate }),
|
|
557
|
+
], { decorate: runtime.decorate }))
|
|
558
|
+
|
|
559
|
+
const config = await setup({
|
|
560
|
+
prompts,
|
|
561
|
+
existingConfig: inspection.config ?? null,
|
|
562
|
+
skipLifecycle: true,
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
let cloneRootCreated = false
|
|
566
|
+
let cloneRootWarning: string | undefined
|
|
567
|
+
const cloneRootStatus = ensureCloneRootReady(prompts, config.cloneRoot)
|
|
568
|
+
cloneRootWarning = cloneRootStatus.warning
|
|
569
|
+
|
|
570
|
+
if (!existsSync(config.cloneRoot)) {
|
|
571
|
+
const createCloneRoot = await prompts.confirm({
|
|
572
|
+
message: `Create clone root directory at ${config.cloneRoot}?`,
|
|
573
|
+
active: 'create',
|
|
574
|
+
inactive: 'skip',
|
|
575
|
+
initialValue: true,
|
|
576
|
+
})
|
|
577
|
+
ensureNotCancelled(prompts, createCloneRoot, 'Setup cancelled.')
|
|
578
|
+
|
|
579
|
+
if (promptValue(createCloneRoot)) {
|
|
580
|
+
mkdirSync(config.cloneRoot, { recursive: true, mode: 0o755 })
|
|
581
|
+
cloneRootCreated = true
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
cloneRootWarning = `Clone root does not exist yet: ${config.cloneRoot}`
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const [gitAvailable, claudeAvailable, reachability] = await Promise.all([
|
|
589
|
+
commandExistsFn('git'),
|
|
590
|
+
commandExistsFn('claude'),
|
|
591
|
+
checkAgentApiReachability(config.agentApiUrl, fetchFn),
|
|
592
|
+
])
|
|
593
|
+
|
|
594
|
+
const warnings = [
|
|
595
|
+
cloneRootWarning,
|
|
596
|
+
gitAvailable ? undefined : '`git` is not available on PATH.',
|
|
597
|
+
claudeAvailable ? undefined : '`claude` is not available on PATH.',
|
|
598
|
+
getAgentApiOriginWarning(reachability),
|
|
599
|
+
].filter((warning): warning is string => Boolean(warning))
|
|
600
|
+
|
|
601
|
+
const nextSetupActions = [
|
|
602
|
+
'Run `tela status` to review the saved environment.',
|
|
603
|
+
cloneRootCreated
|
|
604
|
+
? 'Run `tela sync <repo>` to clone a repository into the new clone root.'
|
|
605
|
+
: 'Run `tela sync <repo>` or `tela sync --current` when you need a checkout.',
|
|
606
|
+
claudeAvailable
|
|
607
|
+
? 'Use `tela run`, `tela push`, and `tela feedback` when you want Claude-assisted workflows.'
|
|
608
|
+
: 'Install the Claude CLI before using `tela run`, `tela feedback`, or Claude-generated commit messages.',
|
|
609
|
+
]
|
|
610
|
+
|
|
611
|
+
runtime.emit(resultCard('Setup complete', [
|
|
612
|
+
section('Saved config', kvList([
|
|
613
|
+
{ label: 'Agent API origin', value: config.agentApiUrl },
|
|
614
|
+
{ label: 'Clone root', value: config.cloneRoot },
|
|
615
|
+
{ label: 'API key', value: redactApiKey(config.apiKey) },
|
|
616
|
+
]), { decorate: runtime.decorate }),
|
|
617
|
+
section('Agent API', AGENT_API_BROWSER_HELP, { decorate: runtime.decorate }),
|
|
618
|
+
section('Checks', table(
|
|
619
|
+
['Check', 'Status', 'Detail'],
|
|
620
|
+
buildSetupChecksRows([
|
|
621
|
+
{ label: 'clone-root', status: cloneRootCreated ? 'created' : (existsSync(config.cloneRoot) ? 'ready' : 'missing'), detail: config.cloneRoot },
|
|
622
|
+
{ label: 'git', status: gitAvailable ? 'available' : 'missing', detail: gitAvailable ? 'ready' : 'install git' },
|
|
623
|
+
{ label: 'claude', status: claudeAvailable ? 'available' : 'missing', detail: claudeAvailable ? 'ready' : 'install Claude CLI' },
|
|
624
|
+
{ label: 'agent-api-origin', status: getAgentApiOriginCheckStatus(reachability), detail: reachability.detail },
|
|
625
|
+
]),
|
|
626
|
+
{ decorate: runtime.decorate },
|
|
627
|
+
), { decorate: runtime.decorate }),
|
|
628
|
+
warnings.length > 0
|
|
629
|
+
? section('Warnings', bulletList(warnings), { decorate: runtime.decorate })
|
|
630
|
+
: undefined,
|
|
631
|
+
nextActions(nextSetupActions, { decorate: runtime.decorate }),
|
|
632
|
+
], { decorate: runtime.decorate }))
|
|
633
|
+
prompts.outro('Tela CLI configured')
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
ok: true,
|
|
637
|
+
config,
|
|
638
|
+
warnings,
|
|
639
|
+
nextActions: nextSetupActions,
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function syncCategoryFixes(category: SyncFailureCategory): string[] {
|
|
644
|
+
switch (category) {
|
|
645
|
+
case 'dirty_worktree':
|
|
646
|
+
return ['Review or stash local changes before syncing again.']
|
|
647
|
+
case 'detached_head':
|
|
648
|
+
return ['Check out a branch before syncing.']
|
|
649
|
+
case 'origin_mismatch':
|
|
650
|
+
return ['Verify the repository remote or reclone the checkout with `tela sync <repo>`.']
|
|
651
|
+
case 'missing_upstream':
|
|
652
|
+
return ['Push the branch once or set an upstream before syncing the current checkout.']
|
|
653
|
+
case 'missing_default_branch':
|
|
654
|
+
return ['Check the remote repository defaults or clone a fresh checkout.']
|
|
655
|
+
case 'not_git_repository':
|
|
656
|
+
return ['Remove the path and rerun `tela sync <repo>`, or initialize a valid repository there.']
|
|
657
|
+
default:
|
|
658
|
+
return ['Rerun `tela doctor` for more detailed remediation steps.']
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function renderSyncPlan(input: {
|
|
663
|
+
mode: SyncCommandResult['mode']
|
|
664
|
+
target: string
|
|
665
|
+
cloneRoot?: string
|
|
666
|
+
repositories?: string[]
|
|
667
|
+
dryRun?: boolean
|
|
668
|
+
}, decorate: boolean): string {
|
|
669
|
+
return resultCard(input.dryRun ? 'Sync dry run' : 'Sync plan', [
|
|
670
|
+
section('What will happen', kvList([
|
|
671
|
+
{ label: 'Mode', value: input.mode },
|
|
672
|
+
{ label: 'Target', value: input.target },
|
|
673
|
+
...(input.cloneRoot ? [{ label: 'Clone root', value: input.cloneRoot }] : []),
|
|
674
|
+
]), { decorate }),
|
|
675
|
+
input.repositories && input.repositories.length > 0
|
|
676
|
+
? section('Repositories', input.repositories.map(repo => `- ${repo}`).join('\n'), { decorate })
|
|
677
|
+
: undefined,
|
|
678
|
+
section('Steps', stepList([
|
|
679
|
+
'Fetch remote updates or clone a missing checkout',
|
|
680
|
+
'Validate the local repository state before pulling',
|
|
681
|
+
'Fast-forward the branch when the checkout is clean',
|
|
682
|
+
]), { decorate }),
|
|
683
|
+
], { decorate })
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function groupSyncFailures(failures: BatchSyncFailure[]): string {
|
|
687
|
+
const grouped = new Map<string, BatchSyncFailure[]>()
|
|
688
|
+
for (const failure of failures) {
|
|
689
|
+
const key = failure.category ?? 'other'
|
|
690
|
+
const current = grouped.get(key) ?? []
|
|
691
|
+
current.push(failure)
|
|
692
|
+
grouped.set(key, current)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return Array.from(grouped.entries())
|
|
696
|
+
.map(([category, entries]) => {
|
|
697
|
+
const fixes = syncCategoryFixes(category as SyncFailureCategory)
|
|
698
|
+
.map(fix => ` remedy: ${fix}`)
|
|
699
|
+
.join('\n')
|
|
700
|
+
const lines = entries.map(entry => `- ${basename(entry.targetDir)}: ${entry.error}`)
|
|
701
|
+
return [`${category}:`, ...lines, fixes].join('\n')
|
|
702
|
+
})
|
|
703
|
+
.join('\n\n')
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function renderSyncResult(result: SyncCommandResult, decorate: boolean): string {
|
|
707
|
+
const successLines = result.successes.map(entry => `- ${basename(entry.targetDir)} (${entry.branch} @ ${entry.head})`)
|
|
708
|
+
return resultCard(result.ok ? 'Sync complete' : 'Sync finished with recoverable failures', [
|
|
709
|
+
section('Summary', kvList([
|
|
710
|
+
{ label: 'Mode', value: result.mode },
|
|
711
|
+
{ label: 'Succeeded', value: String(result.successes.length) },
|
|
712
|
+
{ label: 'Failed', value: String(result.failures.length) },
|
|
713
|
+
]), { decorate }),
|
|
714
|
+
successLines.length > 0
|
|
715
|
+
? section('Successful repositories', successLines.join('\n'), { decorate })
|
|
716
|
+
: undefined,
|
|
717
|
+
result.failures.length > 0
|
|
718
|
+
? section('Failures by fix path', groupSyncFailures(result.failures), { decorate })
|
|
719
|
+
: undefined,
|
|
720
|
+
nextActions(result.nextActions, { decorate }),
|
|
721
|
+
], { decorate })
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function resolveSyncRepositoryName(
|
|
725
|
+
prompts: PromptModule,
|
|
726
|
+
cloneRoot: string,
|
|
727
|
+
listRepositories: typeof listCloneRootRepositories,
|
|
728
|
+
json: boolean,
|
|
729
|
+
): Promise<string> {
|
|
730
|
+
const repositories = listRepositories(cloneRoot)
|
|
731
|
+
|
|
732
|
+
if (json) {
|
|
733
|
+
throw createUserFacingError({
|
|
734
|
+
code: 'sync_repo_required',
|
|
735
|
+
summary: 'Repository name is required when --json is used outside a git repository.',
|
|
736
|
+
fixes: [
|
|
737
|
+
'Pass an explicit repository name, or run `tela sync --current` from inside a repository.',
|
|
738
|
+
],
|
|
739
|
+
})
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (repositories.length > 0) {
|
|
743
|
+
const selection = await prompts.select<string>({
|
|
744
|
+
message: 'Which repository do you want to sync?',
|
|
745
|
+
options: [
|
|
746
|
+
...repositories.map(repo => ({ value: repo, label: repo })),
|
|
747
|
+
{ value: '__manual__', label: 'Enter a repository name manually' },
|
|
748
|
+
],
|
|
749
|
+
initialValue: repositories[0],
|
|
750
|
+
})
|
|
751
|
+
ensureNotCancelled(prompts, selection, 'Sync cancelled.')
|
|
752
|
+
|
|
753
|
+
if (promptValue(selection) !== '__manual__') {
|
|
754
|
+
return promptValue(selection)
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const manual = await prompts.text({
|
|
759
|
+
message: 'Repository name',
|
|
760
|
+
placeholder: 'agent-api',
|
|
761
|
+
})
|
|
762
|
+
ensureNotCancelled(prompts, manual, 'Sync cancelled.')
|
|
763
|
+
|
|
764
|
+
const repository = promptValue(manual).trim()
|
|
765
|
+
if (!repository) {
|
|
766
|
+
throw createUserFacingError({
|
|
767
|
+
code: 'sync_repo_required',
|
|
768
|
+
summary: 'Repository name is required when not running inside a git repository.',
|
|
769
|
+
fixes: [
|
|
770
|
+
'Pass a repository name like `tela sync agent-api`, or run `tela sync --current` from inside a repository.',
|
|
771
|
+
],
|
|
772
|
+
})
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return repository
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function buildSyncNextActions(result: SyncCommandResult): string[] {
|
|
779
|
+
if (result.failures.length > 0) {
|
|
780
|
+
return [
|
|
781
|
+
'Fix the repositories listed in the failure section, then rerun `tela sync`.',
|
|
782
|
+
'Run `tela doctor` if the same checkout keeps failing.',
|
|
783
|
+
]
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return [
|
|
787
|
+
result.mode === 'all'
|
|
788
|
+
? 'Change into one of the synced repositories and start working.'
|
|
789
|
+
: 'Run `tela push` after you review or update the synced checkout.',
|
|
790
|
+
'Use `tela status` if you want a fresh environment snapshot.',
|
|
791
|
+
]
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function validateSyncOptions(
|
|
795
|
+
repository: string | undefined,
|
|
796
|
+
options: SyncCommandOptions,
|
|
797
|
+
): void {
|
|
798
|
+
if (options.all && options.current) {
|
|
799
|
+
throw createUserFacingError({
|
|
800
|
+
code: 'sync_flag_conflict',
|
|
801
|
+
summary: '`--all` cannot be combined with `--current`.',
|
|
802
|
+
fixes: ['Choose either `tela sync --all` or `tela sync --current`.'],
|
|
803
|
+
})
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (options.all && repository) {
|
|
807
|
+
throw createUserFacingError({
|
|
808
|
+
code: 'sync_flag_conflict',
|
|
809
|
+
summary: 'Repository name cannot be combined with --all.',
|
|
810
|
+
fixes: ['Use `tela sync <repo>` for one repository, or `tela sync --all` for every checkout.'],
|
|
811
|
+
})
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (options.current && repository) {
|
|
815
|
+
throw createUserFacingError({
|
|
816
|
+
code: 'sync_flag_conflict',
|
|
817
|
+
summary: 'Repository name cannot be combined with --current.',
|
|
818
|
+
fixes: ['Use `tela sync --current`, or remove `--current` and pass a repository name.'],
|
|
819
|
+
})
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function resolveSyncMode(
|
|
824
|
+
repository: string | undefined,
|
|
825
|
+
options: SyncCommandOptions,
|
|
826
|
+
repoRoot: string | null,
|
|
827
|
+
): SyncCommandResult['mode'] {
|
|
828
|
+
if (options.all) {
|
|
829
|
+
return 'all'
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (options.current || (!repository && repoRoot)) {
|
|
833
|
+
return 'current'
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return 'repository'
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async function runSyncInk(
|
|
840
|
+
repository: string | undefined,
|
|
841
|
+
deps: CliDeps,
|
|
842
|
+
options: SyncCommandOptions,
|
|
843
|
+
): Promise<SyncCommandResult> {
|
|
844
|
+
const barkSession = createBarkSession(deps, options, 'Sync')
|
|
845
|
+
await barkSession.playOnce()
|
|
846
|
+
const findRepo = deps.findRepositoryRoot ?? findRepositoryRoot
|
|
847
|
+
const listRepositories = deps.listCloneRootRepositories ?? listCloneRootRepositories
|
|
848
|
+
const syncAll = deps.syncAllRepositories ?? syncAllRepositories
|
|
849
|
+
const syncLocal = deps.syncCurrentRepository ?? syncCurrentRepository
|
|
850
|
+
const syncNamed = deps.syncRepository ?? syncRepository
|
|
851
|
+
const repoRoot = await findRepo(process.cwd())
|
|
852
|
+
const mode = resolveSyncMode(repository, options, repoRoot)
|
|
853
|
+
const config = mode === 'current'
|
|
854
|
+
? undefined
|
|
855
|
+
: await ensureConfigForCommand(deps, options, barkSession)
|
|
856
|
+
const availableRepositories = config ? listRepositories(config.cloneRoot) : undefined
|
|
857
|
+
|
|
858
|
+
return await renderCommandScreen<SyncCommandResult>((controller) => React.createElement(SyncScreen, {
|
|
859
|
+
controller,
|
|
860
|
+
mode,
|
|
861
|
+
dryRun: options.dryRun,
|
|
862
|
+
targetLabel: mode === 'current'
|
|
863
|
+
? (repoRoot ?? process.cwd())
|
|
864
|
+
: mode === 'all'
|
|
865
|
+
? `${availableRepositories?.length ?? 0} repositories`
|
|
866
|
+
: (repository ?? 'selected repository'),
|
|
867
|
+
cloneRoot: config?.cloneRoot,
|
|
868
|
+
initialRepository: repository,
|
|
869
|
+
availableRepositories,
|
|
870
|
+
execute: async (selectedRepository, onProgress) => {
|
|
871
|
+
if (mode === 'all') {
|
|
872
|
+
const syncResult = await syncAll(config!.cloneRoot, {
|
|
873
|
+
onProgress: (event: SyncAllProgressEvent) => {
|
|
874
|
+
onProgress({
|
|
875
|
+
id: event.targetDir,
|
|
876
|
+
label: basename(event.targetDir),
|
|
877
|
+
status: event.status,
|
|
878
|
+
detail: event.result
|
|
879
|
+
? `${event.result.branch} @ ${event.result.head}`
|
|
880
|
+
: event.error,
|
|
881
|
+
})
|
|
882
|
+
},
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
const result: SyncCommandResult = {
|
|
886
|
+
ok: syncResult.failures.length === 0,
|
|
887
|
+
mode,
|
|
888
|
+
successes: syncResult.successes,
|
|
889
|
+
failures: syncResult.failures,
|
|
890
|
+
nextActions: buildSyncNextActions({
|
|
891
|
+
ok: syncResult.failures.length === 0,
|
|
892
|
+
mode,
|
|
893
|
+
successes: syncResult.successes,
|
|
894
|
+
failures: syncResult.failures,
|
|
895
|
+
nextActions: [],
|
|
896
|
+
}),
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (!result.ok) {
|
|
900
|
+
process.exitCode = 1
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return result
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
try {
|
|
907
|
+
const syncResult = mode === 'current'
|
|
908
|
+
? await syncLocal(repoRoot ?? process.cwd())
|
|
909
|
+
: await syncNamed(selectedRepository!, config!)
|
|
910
|
+
|
|
911
|
+
return {
|
|
912
|
+
ok: true,
|
|
913
|
+
mode,
|
|
914
|
+
successes: [syncResult],
|
|
915
|
+
failures: [],
|
|
916
|
+
nextActions: buildSyncNextActions({
|
|
917
|
+
ok: true,
|
|
918
|
+
mode,
|
|
919
|
+
successes: [syncResult],
|
|
920
|
+
failures: [],
|
|
921
|
+
nextActions: [],
|
|
922
|
+
}),
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
const category = categorizeSyncError(error)
|
|
927
|
+
throw createUserFacingError({
|
|
928
|
+
code: `sync_${category}`,
|
|
929
|
+
summary: error instanceof Error ? error.message : String(error),
|
|
930
|
+
fixes: syncCategoryFixes(category),
|
|
931
|
+
cause: error,
|
|
932
|
+
})
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
}))
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export async function runSync(
|
|
939
|
+
repository: string | undefined,
|
|
940
|
+
deps: CliDeps = {},
|
|
941
|
+
options: SyncCommandOptions = {},
|
|
942
|
+
): Promise<SyncCommandResult> {
|
|
943
|
+
validateSyncOptions(repository, options)
|
|
944
|
+
|
|
945
|
+
if (isInteractiveInkSession(options)) {
|
|
946
|
+
return await runSyncInk(repository, deps, options)
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const prompts = deps.prompts
|
|
950
|
+
const runtime = createRuntime(deps, options)
|
|
951
|
+
const findRepo = deps.findRepositoryRoot ?? findRepositoryRoot
|
|
952
|
+
const ensureConfig = deps.ensureConfigInteractive ?? ensureConfigInteractive
|
|
953
|
+
const syncAll = deps.syncAllRepositories ?? syncAllRepositories
|
|
954
|
+
const syncLocal = deps.syncCurrentRepository ?? syncCurrentRepository
|
|
955
|
+
const syncNamed = deps.syncRepository ?? syncRepository
|
|
956
|
+
const listRepositories = deps.listCloneRootRepositories ?? listCloneRootRepositories
|
|
957
|
+
|
|
958
|
+
emitBootstrap(runtime, 'Tela sync', 'Refresh a checkout or clone a repository into the configured Tela workspace.')
|
|
959
|
+
|
|
960
|
+
const repoRoot = await findRepo(process.cwd())
|
|
961
|
+
const config = options.all || !repoRoot || Boolean(repository)
|
|
962
|
+
? await ensureConfig({ prompts })
|
|
963
|
+
: undefined
|
|
964
|
+
|
|
965
|
+
if (options.all) {
|
|
966
|
+
const repositories = listRepositories(config!.cloneRoot)
|
|
967
|
+
runtime.emit(renderSyncPlan({
|
|
968
|
+
mode: 'all',
|
|
969
|
+
target: `${repositories.length} repositories under ${config!.cloneRoot}`,
|
|
970
|
+
cloneRoot: config!.cloneRoot,
|
|
971
|
+
repositories,
|
|
972
|
+
dryRun: options.dryRun,
|
|
973
|
+
}, runtime.decorate))
|
|
974
|
+
|
|
975
|
+
if (options.dryRun) {
|
|
976
|
+
const dryRunResult: SyncCommandResult = {
|
|
977
|
+
ok: true,
|
|
978
|
+
mode: 'all',
|
|
979
|
+
successes: [],
|
|
980
|
+
failures: [],
|
|
981
|
+
nextActions: ['Run `tela sync --all` without `--dry-run` when you are ready to refresh every checkout.'],
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (options.json) {
|
|
985
|
+
runtime.emitJson(dryRunResult)
|
|
986
|
+
}
|
|
987
|
+
return dryRunResult
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const syncResult = await syncAll(config!.cloneRoot)
|
|
991
|
+
const result: SyncCommandResult = {
|
|
992
|
+
ok: syncResult.failures.length === 0,
|
|
993
|
+
mode: 'all',
|
|
994
|
+
successes: syncResult.successes,
|
|
995
|
+
failures: syncResult.failures,
|
|
996
|
+
nextActions: buildSyncNextActions({
|
|
997
|
+
ok: syncResult.failures.length === 0,
|
|
998
|
+
mode: 'all',
|
|
999
|
+
successes: syncResult.successes,
|
|
1000
|
+
failures: syncResult.failures,
|
|
1001
|
+
nextActions: [],
|
|
1002
|
+
}),
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (options.json) {
|
|
1006
|
+
runtime.emitJson(result)
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
runtime.emit(renderSyncResult(result, runtime.decorate))
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (!result.ok) {
|
|
1013
|
+
process.exitCode = 1
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return result
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
let mode: SyncCommandResult['mode']
|
|
1020
|
+
let targetRepository = repository
|
|
1021
|
+
|
|
1022
|
+
if (options.current || (!repository && repoRoot)) {
|
|
1023
|
+
mode = 'current'
|
|
1024
|
+
}
|
|
1025
|
+
else {
|
|
1026
|
+
mode = 'repository'
|
|
1027
|
+
if (!targetRepository) {
|
|
1028
|
+
if (options.json) {
|
|
1029
|
+
throw createUserFacingError({
|
|
1030
|
+
code: 'sync_repo_required',
|
|
1031
|
+
summary: 'Repository name is required when --json is used outside a git repository.',
|
|
1032
|
+
fixes: [
|
|
1033
|
+
'Pass an explicit repository name, or run `tela sync --current` from inside a repository.',
|
|
1034
|
+
],
|
|
1035
|
+
})
|
|
1036
|
+
}
|
|
1037
|
+
const promptModule = prompts ?? getPromptModule(deps)
|
|
1038
|
+
targetRepository = await resolveSyncRepositoryName(
|
|
1039
|
+
promptModule,
|
|
1040
|
+
config!.cloneRoot,
|
|
1041
|
+
listRepositories,
|
|
1042
|
+
false,
|
|
1043
|
+
)
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
runtime.emit(renderSyncPlan({
|
|
1048
|
+
mode,
|
|
1049
|
+
target: mode === 'current'
|
|
1050
|
+
? (repoRoot ?? process.cwd())
|
|
1051
|
+
: targetRepository!,
|
|
1052
|
+
cloneRoot: config?.cloneRoot,
|
|
1053
|
+
dryRun: options.dryRun,
|
|
1054
|
+
}, runtime.decorate))
|
|
1055
|
+
|
|
1056
|
+
if (options.dryRun) {
|
|
1057
|
+
const dryRunResult: SyncCommandResult = {
|
|
1058
|
+
ok: true,
|
|
1059
|
+
mode,
|
|
1060
|
+
successes: [],
|
|
1061
|
+
failures: [],
|
|
1062
|
+
nextActions: [
|
|
1063
|
+
mode === 'current'
|
|
1064
|
+
? 'Run `tela sync --current` without `--dry-run` to update this checkout.'
|
|
1065
|
+
: `Run \`tela sync ${targetRepository}\` without \`--dry-run\` to clone or update the repository.`,
|
|
1066
|
+
],
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (options.json) {
|
|
1070
|
+
runtime.emitJson(dryRunResult)
|
|
1071
|
+
}
|
|
1072
|
+
return dryRunResult
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
try {
|
|
1076
|
+
const syncResult = mode === 'current'
|
|
1077
|
+
? await syncLocal(repoRoot ?? process.cwd())
|
|
1078
|
+
: await syncNamed(targetRepository!, config!)
|
|
1079
|
+
|
|
1080
|
+
const result: SyncCommandResult = {
|
|
1081
|
+
ok: true,
|
|
1082
|
+
mode,
|
|
1083
|
+
successes: [syncResult],
|
|
1084
|
+
failures: [],
|
|
1085
|
+
nextActions: buildSyncNextActions({
|
|
1086
|
+
ok: true,
|
|
1087
|
+
mode,
|
|
1088
|
+
successes: [syncResult],
|
|
1089
|
+
failures: [],
|
|
1090
|
+
nextActions: [],
|
|
1091
|
+
}),
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (options.json) {
|
|
1095
|
+
runtime.emitJson(result)
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
runtime.emit(renderSyncResult(result, runtime.decorate))
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
return result
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
const category = categorizeSyncError(error)
|
|
1105
|
+
throw createUserFacingError({
|
|
1106
|
+
code: `sync_${category}`,
|
|
1107
|
+
summary: error instanceof Error ? error.message : String(error),
|
|
1108
|
+
fixes: syncCategoryFixes(category),
|
|
1109
|
+
cause: error,
|
|
1110
|
+
})
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function resolvePushMode(options: PushCommandOptions): 'manual' | 'claude' {
|
|
1115
|
+
const explicit = [Boolean(options.message), Boolean(options.claude), Boolean(options.manual)].filter(Boolean)
|
|
1116
|
+
if (explicit.length > 1) {
|
|
1117
|
+
throw createUserFacingError({
|
|
1118
|
+
code: 'push_flag_conflict',
|
|
1119
|
+
summary: 'Only one of --message, --claude, or --manual can be used at a time.',
|
|
1120
|
+
fixes: ['Choose one commit-message mode for `tela push`.'],
|
|
1121
|
+
})
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (options.message || options.manual) {
|
|
1125
|
+
return 'manual'
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (options.claude) {
|
|
1129
|
+
return 'claude'
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return 'manual'
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function renderPushPreview(preview: PushPreview, decorate: boolean): string {
|
|
1136
|
+
const plan = [
|
|
1137
|
+
`Stage all changes in ${preview.repoRoot}`,
|
|
1138
|
+
`Create a commit on ${preview.detached ? '<detached>' : (preview.branch ?? '<unknown>')}`,
|
|
1139
|
+
preview.upstream
|
|
1140
|
+
? 'Push the branch to its existing upstream'
|
|
1141
|
+
: 'Push the branch and set the upstream on origin',
|
|
1142
|
+
]
|
|
1143
|
+
|
|
1144
|
+
return resultCard('Push preview', [
|
|
1145
|
+
section('Repository', kvList([
|
|
1146
|
+
{ label: 'Repo root', value: preview.repoRoot },
|
|
1147
|
+
{ label: 'Branch', value: preview.detached ? '<detached>' : (preview.branch ?? '<unknown>') },
|
|
1148
|
+
{ label: 'Upstream', value: preview.upstream ?? '<none>' },
|
|
1149
|
+
{ label: 'Changed files', value: String(preview.statusCounts.total) },
|
|
1150
|
+
{ label: 'Staged', value: String(preview.statusCounts.staged) },
|
|
1151
|
+
{ label: 'Unstaged', value: String(preview.statusCounts.unstaged) },
|
|
1152
|
+
{ label: 'Untracked', value: String(preview.statusCounts.untracked) },
|
|
1153
|
+
]), { decorate }),
|
|
1154
|
+
preview.worktreeStatus
|
|
1155
|
+
? section('Worktree', preview.worktreeStatus, { decorate })
|
|
1156
|
+
: undefined,
|
|
1157
|
+
preview.suggestedMessage
|
|
1158
|
+
? section('Commit message', [
|
|
1159
|
+
preview.suggestedMessage,
|
|
1160
|
+
preview.messageSource === 'fallback'
|
|
1161
|
+
? `source: deterministic fallback (${preview.fallbackReason ?? 'Claude did not produce a usable message'})`
|
|
1162
|
+
: `source: ${preview.messageSource}`,
|
|
1163
|
+
].join('\n'), { decorate })
|
|
1164
|
+
: undefined,
|
|
1165
|
+
section('Plan', stepList(plan), { decorate }),
|
|
1166
|
+
], { decorate })
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async function runPushInk(
|
|
1170
|
+
deps: CliDeps,
|
|
1171
|
+
options: PushCommandOptions,
|
|
1172
|
+
): Promise<PushCommandResult> {
|
|
1173
|
+
const barkSession = createBarkSession(deps, options, 'Push')
|
|
1174
|
+
await barkSession.playOnce()
|
|
1175
|
+
const findRepo = deps.findRepositoryRoot ?? findRepositoryRoot
|
|
1176
|
+
const preview = deps.previewPush ?? previewPush
|
|
1177
|
+
const push = deps.pushRepository ?? pushRepository
|
|
1178
|
+
const repoRoot = await findRepo(process.cwd())
|
|
1179
|
+
|
|
1180
|
+
if (!repoRoot) {
|
|
1181
|
+
throw createUserFacingError({
|
|
1182
|
+
code: 'push_outside_repo',
|
|
1183
|
+
summary: 'Push must be run from inside a git repository.',
|
|
1184
|
+
fixes: ['Change into a repository, or run `tela sync <repo>` first.'],
|
|
1185
|
+
})
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const initialMode = (options.message || options.claude || options.manual)
|
|
1189
|
+
? resolvePushMode(options)
|
|
1190
|
+
: undefined
|
|
1191
|
+
const initialManualMessage = options.message?.trim() || undefined
|
|
1192
|
+
|
|
1193
|
+
return await renderCommandScreen<PushCommandResult>((controller) => React.createElement(PushScreen, {
|
|
1194
|
+
controller,
|
|
1195
|
+
initialMode,
|
|
1196
|
+
initialManualMessage,
|
|
1197
|
+
dryRun: options.dryRun,
|
|
1198
|
+
loadPreview: async (input) => {
|
|
1199
|
+
const previewResult = await preview({
|
|
1200
|
+
cwd: repoRoot,
|
|
1201
|
+
mode: input.mode,
|
|
1202
|
+
manualMessage: input.manualMessage,
|
|
1203
|
+
})
|
|
1204
|
+
|
|
1205
|
+
if (!previewResult.hasChanges) {
|
|
1206
|
+
throw createUserFacingError({
|
|
1207
|
+
code: 'push_no_changes',
|
|
1208
|
+
summary: 'No changes to push.',
|
|
1209
|
+
fixes: ['Modify files first, or review the worktree with `git status`.'],
|
|
1210
|
+
})
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (previewResult.detached) {
|
|
1214
|
+
throw createUserFacingError({
|
|
1215
|
+
code: 'push_detached_head',
|
|
1216
|
+
summary: 'Detached HEAD; check out a branch before pushing.',
|
|
1217
|
+
fixes: ['Check out a branch before running `tela push`.'],
|
|
1218
|
+
})
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return previewResult
|
|
1222
|
+
},
|
|
1223
|
+
execute: async (input) => {
|
|
1224
|
+
const pushResult = await push({
|
|
1225
|
+
cwd: repoRoot,
|
|
1226
|
+
mode: input.messageSource === 'manual' ? 'manual' : 'claude',
|
|
1227
|
+
manualMessage: input.messageSource === 'manual' ? input.commitMessage : undefined,
|
|
1228
|
+
resolvedMessage: {
|
|
1229
|
+
message: input.commitMessage,
|
|
1230
|
+
source: input.messageSource,
|
|
1231
|
+
fallbackReason: input.fallbackReason,
|
|
1232
|
+
},
|
|
1233
|
+
})
|
|
1234
|
+
|
|
1235
|
+
return pushResult
|
|
1236
|
+
},
|
|
1237
|
+
}))
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
export async function runPush(
|
|
1241
|
+
deps: CliDeps = {},
|
|
1242
|
+
options: PushCommandOptions = {},
|
|
1243
|
+
): Promise<PushCommandResult> {
|
|
1244
|
+
if (isInteractiveInkSession(options)) {
|
|
1245
|
+
return await runPushInk(deps, options)
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const prompts = deps.prompts
|
|
1249
|
+
const runtime = createRuntime(deps, options)
|
|
1250
|
+
const findRepo = deps.findRepositoryRoot ?? findRepositoryRoot
|
|
1251
|
+
const preview = deps.previewPush ?? previewPush
|
|
1252
|
+
const push = deps.pushRepository ?? pushRepository
|
|
1253
|
+
const repoRoot = await findRepo(process.cwd())
|
|
1254
|
+
|
|
1255
|
+
emitBootstrap(runtime, 'Tela push', 'Stage, preview, and publish the current repository with a reviewed commit message.')
|
|
1256
|
+
|
|
1257
|
+
if (!repoRoot) {
|
|
1258
|
+
throw createUserFacingError({
|
|
1259
|
+
code: 'push_outside_repo',
|
|
1260
|
+
summary: 'Push must be run from inside a git repository.',
|
|
1261
|
+
fixes: ['Change into a repository, or run `tela sync <repo>` first.'],
|
|
1262
|
+
})
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
let mode = resolvePushMode(options)
|
|
1266
|
+
let manualMessage = options.message?.trim() || undefined
|
|
1267
|
+
|
|
1268
|
+
if (!options.message && !options.claude && !options.manual && !options.json && prompts) {
|
|
1269
|
+
const modeResult = await prompts.select<'manual' | 'claude'>({
|
|
1270
|
+
message: 'How do you want to create the commit message?',
|
|
1271
|
+
options: [
|
|
1272
|
+
{ value: 'manual', label: 'Write it myself' },
|
|
1273
|
+
{ value: 'claude', label: 'Generate with Claude' },
|
|
1274
|
+
],
|
|
1275
|
+
initialValue: 'manual',
|
|
1276
|
+
})
|
|
1277
|
+
ensureNotCancelled(prompts, modeResult, 'Push cancelled.')
|
|
1278
|
+
mode = promptValue(modeResult)
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if (mode === 'manual' && !manualMessage && !options.json && prompts) {
|
|
1282
|
+
const messageResult = await prompts.text({
|
|
1283
|
+
message: 'Commit message',
|
|
1284
|
+
placeholder: 'Leave blank to generate from Claude or the deterministic fallback',
|
|
1285
|
+
})
|
|
1286
|
+
ensureNotCancelled(prompts, messageResult, 'Push cancelled.')
|
|
1287
|
+
manualMessage = promptValue(messageResult).trim() || undefined
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const previewResult = await preview({
|
|
1291
|
+
cwd: repoRoot,
|
|
1292
|
+
mode,
|
|
1293
|
+
manualMessage,
|
|
1294
|
+
})
|
|
1295
|
+
|
|
1296
|
+
if (!previewResult.hasChanges) {
|
|
1297
|
+
throw createUserFacingError({
|
|
1298
|
+
code: 'push_no_changes',
|
|
1299
|
+
summary: 'No changes to push.',
|
|
1300
|
+
fixes: ['Modify files first, or review the worktree with `git status`.'],
|
|
1301
|
+
})
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (previewResult.detached) {
|
|
1305
|
+
throw createUserFacingError({
|
|
1306
|
+
code: 'push_detached_head',
|
|
1307
|
+
summary: 'Detached HEAD; check out a branch before pushing.',
|
|
1308
|
+
fixes: ['Check out a branch before running `tela push`.'],
|
|
1309
|
+
})
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
runtime.emit(renderPushPreview(previewResult, runtime.decorate))
|
|
1313
|
+
|
|
1314
|
+
let resolvedMessage = previewResult.suggestedMessage?.trim() || manualMessage
|
|
1315
|
+
if (!resolvedMessage) {
|
|
1316
|
+
throw createUserFacingError({
|
|
1317
|
+
code: 'push_message_missing',
|
|
1318
|
+
summary: 'Unable to determine a commit message for the current worktree.',
|
|
1319
|
+
fixes: ['Pass `--message`, or retry with `--claude` after reviewing the current diff.'],
|
|
1320
|
+
})
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
let messageSource = previewResult.messageSource ?? (manualMessage ? 'manual' : 'fallback')
|
|
1324
|
+
let fallbackReason = previewResult.fallbackReason
|
|
1325
|
+
|
|
1326
|
+
if (mode === 'claude' && !options.json && prompts) {
|
|
1327
|
+
const editedMessage = await prompts.text({
|
|
1328
|
+
message: 'Confirm or edit the commit message',
|
|
1329
|
+
defaultValue: resolvedMessage,
|
|
1330
|
+
})
|
|
1331
|
+
ensureNotCancelled(prompts, editedMessage, 'Push cancelled.')
|
|
1332
|
+
resolvedMessage = promptValue(editedMessage).trim() || resolvedMessage
|
|
1333
|
+
messageSource = previewResult.messageSource ?? 'claude'
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (options.dryRun) {
|
|
1337
|
+
const dryRunResult: PushCommandResult = {
|
|
1338
|
+
ok: true,
|
|
1339
|
+
branch: previewResult.branch,
|
|
1340
|
+
commitMessage: resolvedMessage,
|
|
1341
|
+
nextActions: ['Run `tela push` without `--dry-run` when the plan looks correct.'],
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (options.json) {
|
|
1345
|
+
runtime.emitJson(dryRunResult)
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
return dryRunResult
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const pushResult = await push({
|
|
1352
|
+
cwd: repoRoot,
|
|
1353
|
+
mode: 'manual',
|
|
1354
|
+
manualMessage: resolvedMessage,
|
|
1355
|
+
resolvedMessage: {
|
|
1356
|
+
message: resolvedMessage,
|
|
1357
|
+
source: messageSource,
|
|
1358
|
+
fallbackReason,
|
|
1359
|
+
},
|
|
1360
|
+
})
|
|
1361
|
+
|
|
1362
|
+
const result: PushCommandResult = {
|
|
1363
|
+
ok: true,
|
|
1364
|
+
branch: pushResult.branch,
|
|
1365
|
+
commitMessage: pushResult.message,
|
|
1366
|
+
commitSha: pushResult.commitSha,
|
|
1367
|
+
retriedCommit: pushResult.retriedCommit,
|
|
1368
|
+
nextActions: [
|
|
1369
|
+
'Run `tela feedback --list` to inspect feedback for the pushed commit.',
|
|
1370
|
+
'Use `tela status` if you want to verify the repository is clean afterward.',
|
|
1371
|
+
],
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
if (options.json) {
|
|
1375
|
+
runtime.emitJson(result)
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
runtime.emit(resultCard('Push complete', [
|
|
1379
|
+
section('Commit', kvList([
|
|
1380
|
+
{ label: 'Branch', value: pushResult.branch },
|
|
1381
|
+
{ label: 'Commit', value: pushResult.commitSha },
|
|
1382
|
+
{ label: 'Upstream', value: pushResult.upstream ?? '<set on push>' },
|
|
1383
|
+
{ label: 'Retry', value: pushResult.retriedCommit ? 'yes' : 'no' },
|
|
1384
|
+
]), { decorate: runtime.decorate }),
|
|
1385
|
+
section('Message', [
|
|
1386
|
+
pushResult.message,
|
|
1387
|
+
pushResult.messageSource === 'fallback'
|
|
1388
|
+
? `source: deterministic fallback (${pushResult.fallbackReason ?? 'Claude did not return a usable message'})`
|
|
1389
|
+
: `source: ${pushResult.messageSource}`,
|
|
1390
|
+
].join('\n'), { decorate: runtime.decorate }),
|
|
1391
|
+
nextActions(result.nextActions, { decorate: runtime.decorate }),
|
|
1392
|
+
], { decorate: runtime.decorate }))
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
return result
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function renderFeedbackResult(result: FeedbackRunResult, decorate: boolean): string {
|
|
1399
|
+
const selected = result.selectedIds?.length ? result.selectedIds.join(', ') : '<none>'
|
|
1400
|
+
return resultCard('Feedback workflow', [
|
|
1401
|
+
section('Summary', kvList([
|
|
1402
|
+
{ label: 'Repository', value: `${result.organizationName}/${result.repository}` },
|
|
1403
|
+
{ label: 'Commit', value: result.commitHash },
|
|
1404
|
+
{ label: 'Mode', value: describeFeedbackSelectionMode(result.selectionMode) },
|
|
1405
|
+
{ label: 'Feedbacks', value: String(result.summary.totalFeedbacks) },
|
|
1406
|
+
{ label: 'Actionable', value: String(result.summary.actionableCount) },
|
|
1407
|
+
{ label: 'Informational', value: String(result.summary.informationalCount) },
|
|
1408
|
+
{ label: 'Review state', value: describeFeedbackEmptyState(result.emptyState) },
|
|
1409
|
+
{ label: 'Selected', value: selected },
|
|
1410
|
+
{ label: 'Claude launched', value: result.launchedClaude ? 'yes' : 'no' },
|
|
1411
|
+
]), { decorate }),
|
|
1412
|
+
result.diffStat
|
|
1413
|
+
? section('Local diff summary', result.diffStat, { decorate })
|
|
1414
|
+
: undefined,
|
|
1415
|
+
nextActions(result.nextActions, { decorate }),
|
|
1416
|
+
], { decorate })
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function validateFeedbackOptions(options: FeedbackCommandFlags & GlobalCliOptions): FeedbackCommandFlags & GlobalCliOptions {
|
|
1420
|
+
if (options.apply && options.apply !== 'all' && options.apply !== 'select') {
|
|
1421
|
+
throw createUserFacingError({
|
|
1422
|
+
code: 'feedback_apply_invalid',
|
|
1423
|
+
summary: '`--apply` must be either `all` or `select`.',
|
|
1424
|
+
fixes: ['Use `tela feedback --apply all` or `tela feedback --apply select`.'],
|
|
1425
|
+
})
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
if (options.json && options.apply === 'select') {
|
|
1429
|
+
throw createUserFacingError({
|
|
1430
|
+
code: 'feedback_json_select_conflict',
|
|
1431
|
+
summary: '`--json` cannot be combined with `--apply select` because selection is interactive.',
|
|
1432
|
+
fixes: ['Use `tela feedback --list --json`, or run the interactive command without `--json`.'],
|
|
1433
|
+
})
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
return {
|
|
1437
|
+
...options,
|
|
1438
|
+
list: options.json && !options.list && !options.apply
|
|
1439
|
+
? true
|
|
1440
|
+
: options.list,
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
async function runFeedbackServiceFlow(
|
|
1445
|
+
repoRoot: string,
|
|
1446
|
+
deps: CliDeps,
|
|
1447
|
+
options: FeedbackCommandFlags & GlobalCliOptions,
|
|
1448
|
+
): Promise<FeedbackRunResult> {
|
|
1449
|
+
const config = await ensureConfigForCommand(deps, options)
|
|
1450
|
+
const loadFeedbackDataFn = deps.loadFeedbackData ?? loadFeedbackData
|
|
1451
|
+
const launchFeedbackClaudeFn = deps.launchFeedbackClaude ?? launchFeedbackClaude
|
|
1452
|
+
const commandExistsFn = deps.commandExists ?? commandExists
|
|
1453
|
+
const getDiffStatAgainstHeadFn = deps.getDiffStatAgainstHead ?? getDiffStatAgainstHead
|
|
1454
|
+
const selectionMode = getFeedbackSelectionMode(options)
|
|
1455
|
+
const data = await loadFeedbackDataFn({
|
|
1456
|
+
cwd: repoRoot,
|
|
1457
|
+
config,
|
|
1458
|
+
})
|
|
1459
|
+
const review = buildFeedbackReviewModel(data)
|
|
1460
|
+
|
|
1461
|
+
if (selectionMode === 'list' || review.emptyState !== 'none') {
|
|
1462
|
+
return buildFeedbackRunResult({
|
|
1463
|
+
data,
|
|
1464
|
+
selectionMode,
|
|
1465
|
+
launchedClaude: false,
|
|
1466
|
+
})
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
if (!await commandExistsFn('claude')) {
|
|
1470
|
+
throw createUserFacingError({
|
|
1471
|
+
code: 'feedback_claude_missing',
|
|
1472
|
+
summary: 'Claude CLI is not available on PATH.',
|
|
1473
|
+
fixes: ['Install the Claude CLI before using `tela feedback --apply ...`.'],
|
|
1474
|
+
})
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const selectedFeedbacks = data.actionableEntries
|
|
1478
|
+
const prompt = buildClaudeFeedbackPrompt({
|
|
1479
|
+
repoRoot: data.repoRoot,
|
|
1480
|
+
organizationName: data.organizationName,
|
|
1481
|
+
repository: data.repository,
|
|
1482
|
+
commitHash: data.commitHash,
|
|
1483
|
+
selectedFeedbacks,
|
|
1484
|
+
})
|
|
1485
|
+
|
|
1486
|
+
await launchFeedbackClaudeFn({
|
|
1487
|
+
repoRoot: data.repoRoot,
|
|
1488
|
+
prompt,
|
|
1489
|
+
})
|
|
1490
|
+
|
|
1491
|
+
const diffStat = await getDiffStatAgainstHeadFn(repoRoot)
|
|
1492
|
+
|
|
1493
|
+
return buildFeedbackRunResult({
|
|
1494
|
+
data,
|
|
1495
|
+
selectionMode,
|
|
1496
|
+
launchedClaude: true,
|
|
1497
|
+
selectedFeedbacks,
|
|
1498
|
+
diffStat,
|
|
1499
|
+
})
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
async function runFeedbackInk(
|
|
1503
|
+
repoRoot: string,
|
|
1504
|
+
deps: CliDeps,
|
|
1505
|
+
options: FeedbackCommandFlags & GlobalCliOptions,
|
|
1506
|
+
): Promise<FeedbackRunResult> {
|
|
1507
|
+
const barkSession = createBarkSession(deps, options, 'Feedback')
|
|
1508
|
+
await barkSession.playOnce()
|
|
1509
|
+
const config = await ensureConfigForCommand(deps, options, barkSession)
|
|
1510
|
+
const loadFeedbackDataFn = deps.loadFeedbackData ?? loadFeedbackData
|
|
1511
|
+
const launchFeedbackClaudeFn = deps.launchFeedbackClaude ?? launchFeedbackClaude
|
|
1512
|
+
const commandExistsFn = deps.commandExists ?? commandExists
|
|
1513
|
+
const getDiffStatAgainstHeadFn = deps.getDiffStatAgainstHead ?? getDiffStatAgainstHead
|
|
1514
|
+
const selectionMode = getFeedbackSelectionMode(options)
|
|
1515
|
+
const data = await loadFeedbackDataFn({
|
|
1516
|
+
cwd: repoRoot,
|
|
1517
|
+
config,
|
|
1518
|
+
})
|
|
1519
|
+
|
|
1520
|
+
const selection = await renderCommandScreen<{
|
|
1521
|
+
selectedFeedbacks: FeedbackData['entries']
|
|
1522
|
+
prompt?: string
|
|
1523
|
+
launchedClaude: boolean
|
|
1524
|
+
selectionMode: ReturnType<typeof getFeedbackSelectionMode>
|
|
1525
|
+
}>((controller) => React.createElement(FeedbackSelectionScreen, {
|
|
1526
|
+
controller,
|
|
1527
|
+
data,
|
|
1528
|
+
mode: selectionMode,
|
|
1529
|
+
}))
|
|
1530
|
+
|
|
1531
|
+
if (!selection.prompt || selection.selectedFeedbacks.length === 0) {
|
|
1532
|
+
return buildFeedbackRunResult({
|
|
1533
|
+
data,
|
|
1534
|
+
selectionMode,
|
|
1535
|
+
launchedClaude: false,
|
|
1536
|
+
selectedFeedbacks: selection.selectedFeedbacks,
|
|
1537
|
+
})
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
if (!await commandExistsFn('claude')) {
|
|
1541
|
+
throw createUserFacingError({
|
|
1542
|
+
code: 'feedback_claude_missing',
|
|
1543
|
+
summary: 'Claude CLI is not available on PATH.',
|
|
1544
|
+
fixes: ['Install the Claude CLI before using `tela feedback --apply ...`.'],
|
|
1545
|
+
})
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
await renderCommandScreen<void>((controller) => React.createElement(FeedbackClaudeHandoffScreen, {
|
|
1549
|
+
controller,
|
|
1550
|
+
repoRoot: data.repoRoot,
|
|
1551
|
+
commitHash: data.commitHash,
|
|
1552
|
+
selectedFeedbacks: selection.selectedFeedbacks,
|
|
1553
|
+
}))
|
|
1554
|
+
|
|
1555
|
+
await launchFeedbackClaudeFn({
|
|
1556
|
+
repoRoot: data.repoRoot,
|
|
1557
|
+
prompt: selection.prompt,
|
|
1558
|
+
})
|
|
1559
|
+
|
|
1560
|
+
const diffStat = await getDiffStatAgainstHeadFn(repoRoot)
|
|
1561
|
+
|
|
1562
|
+
await renderCommandScreen<void>((controller) => React.createElement(FeedbackOutcomeScreen, {
|
|
1563
|
+
controller,
|
|
1564
|
+
commitHash: data.commitHash,
|
|
1565
|
+
selectionMode,
|
|
1566
|
+
diffStat: diffStat || 'No local diff detected after Claude exited.',
|
|
1567
|
+
selectedFeedbacks: selection.selectedFeedbacks,
|
|
1568
|
+
}))
|
|
1569
|
+
|
|
1570
|
+
return buildFeedbackRunResult({
|
|
1571
|
+
data,
|
|
1572
|
+
selectionMode,
|
|
1573
|
+
launchedClaude: true,
|
|
1574
|
+
selectedFeedbacks: selection.selectedFeedbacks,
|
|
1575
|
+
diffStat,
|
|
1576
|
+
})
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
export async function runFeedback(
|
|
1580
|
+
deps: CliDeps = {},
|
|
1581
|
+
options: FeedbackCommandFlags & GlobalCliOptions = {},
|
|
1582
|
+
): Promise<FeedbackRunResult> {
|
|
1583
|
+
const normalizedOptions = validateFeedbackOptions(options)
|
|
1584
|
+
const runtime = createRuntime(deps, options)
|
|
1585
|
+
const findRepo = deps.findRepositoryRoot ?? findRepositoryRoot
|
|
1586
|
+
const feedbackCommand = deps.runFeedbackCommand ?? runFeedbackCommand
|
|
1587
|
+
const repoRoot = await findRepo(process.cwd())
|
|
1588
|
+
|
|
1589
|
+
if (!repoRoot) {
|
|
1590
|
+
throw createUserFacingError({
|
|
1591
|
+
code: 'feedback_outside_repo',
|
|
1592
|
+
summary: 'Feedback must be run from inside a git repository.',
|
|
1593
|
+
fixes: ['Change into a repository, or run `tela sync <repo>` first.'],
|
|
1594
|
+
})
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
const interactive = isInteractiveInkSession(normalizedOptions)
|
|
1598
|
+
if (!interactive) {
|
|
1599
|
+
emitBootstrap(runtime, 'Tela feedback', 'Review commit feedback and optionally hand selected fixes to Claude.')
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
const result = interactive
|
|
1603
|
+
? await runFeedbackInk(repoRoot, deps, normalizedOptions)
|
|
1604
|
+
: deps.prompts
|
|
1605
|
+
? await feedbackCommand({
|
|
1606
|
+
cwd: repoRoot,
|
|
1607
|
+
prompts: deps.prompts,
|
|
1608
|
+
list: normalizedOptions.list,
|
|
1609
|
+
apply: normalizedOptions.apply,
|
|
1610
|
+
})
|
|
1611
|
+
: await runFeedbackServiceFlow(repoRoot, deps, normalizedOptions)
|
|
1612
|
+
|
|
1613
|
+
if (normalizedOptions.json) {
|
|
1614
|
+
runtime.emitJson(result)
|
|
1615
|
+
}
|
|
1616
|
+
else {
|
|
1617
|
+
runtime.emit(renderFeedbackResult(result, runtime.decorate))
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
return result
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
export async function runAgent(
|
|
1624
|
+
deps: CliDeps = {},
|
|
1625
|
+
options: RunCommandOptions = {},
|
|
1626
|
+
): Promise<RunCommandResult> {
|
|
1627
|
+
const runtime = createRuntime(deps, options)
|
|
1628
|
+
const runAgentCommandFn = deps.runLocalAgentCommand ?? runLocalAgentCommand
|
|
1629
|
+
const prepareRunFn = deps.prepareLocalAgentRun ?? prepareLocalAgentRun
|
|
1630
|
+
const surface = resolveRunSurface(options)
|
|
1631
|
+
|
|
1632
|
+
let result: RunCommandResult
|
|
1633
|
+
|
|
1634
|
+
if (surface === 'viewer') {
|
|
1635
|
+
const prepared = await prepareRunFn({
|
|
1636
|
+
cwd: process.cwd(),
|
|
1637
|
+
commandExists: deps.commandExists,
|
|
1638
|
+
})
|
|
1639
|
+
const barkSession = createBarkSession(deps, options, 'Run')
|
|
1640
|
+
await barkSession.playOnce()
|
|
1641
|
+
|
|
1642
|
+
if (prepared.config.isMultiturn) {
|
|
1643
|
+
await renderCommandScreen<void>((controller) => React.createElement(RunChatHandoffScreen, {
|
|
1644
|
+
controller,
|
|
1645
|
+
snapshot: createInitialRunSnapshot(prepared, 'chat'),
|
|
1646
|
+
}), {
|
|
1647
|
+
exitOnCtrlC: false,
|
|
1648
|
+
})
|
|
1649
|
+
|
|
1650
|
+
result = await runAgentCommandFn({
|
|
1651
|
+
cwd: process.cwd(),
|
|
1652
|
+
contract: prepared,
|
|
1653
|
+
surface: 'viewer',
|
|
1654
|
+
commandExists: deps.commandExists,
|
|
1655
|
+
runInteractiveCommand: deps.runInteractiveCommand,
|
|
1656
|
+
runStreamingCommand: deps.runStreamingCommand,
|
|
1657
|
+
writeStdout: deps.writeStdout,
|
|
1658
|
+
writeStderr: deps.writeStderr,
|
|
1659
|
+
stdinIsTTY: process.stdin.isTTY,
|
|
1660
|
+
stdoutIsTTY: process.stdout.isTTY,
|
|
1661
|
+
verbose: options.verbose,
|
|
1662
|
+
})
|
|
1663
|
+
}
|
|
1664
|
+
else {
|
|
1665
|
+
result = await renderCommandScreen<RunCommandResult>((controller) => React.createElement(RunScreen, {
|
|
1666
|
+
controller,
|
|
1667
|
+
initialSnapshot: createInitialRunSnapshot(prepared, 'one-shot'),
|
|
1668
|
+
execute: async ({ onSnapshot, onControl }) => {
|
|
1669
|
+
return await runAgentCommandFn({
|
|
1670
|
+
cwd: process.cwd(),
|
|
1671
|
+
contract: prepared,
|
|
1672
|
+
surface: 'viewer',
|
|
1673
|
+
commandExists: deps.commandExists,
|
|
1674
|
+
runInteractiveCommand: deps.runInteractiveCommand,
|
|
1675
|
+
runStreamingCommand: deps.runStreamingCommand,
|
|
1676
|
+
writeStdout: deps.writeStdout,
|
|
1677
|
+
writeStderr: deps.writeStderr,
|
|
1678
|
+
onSnapshot,
|
|
1679
|
+
onControl,
|
|
1680
|
+
stdinIsTTY: process.stdin.isTTY,
|
|
1681
|
+
stdoutIsTTY: process.stdout.isTTY,
|
|
1682
|
+
verbose: options.verbose,
|
|
1683
|
+
})
|
|
1684
|
+
},
|
|
1685
|
+
}), {
|
|
1686
|
+
exitOnCtrlC: false,
|
|
1687
|
+
})
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
emitBootstrap(runtime, 'Tela run', 'Run the local agent repository with Claude.')
|
|
1692
|
+
|
|
1693
|
+
result = await runAgentCommandFn({
|
|
1694
|
+
cwd: process.cwd(),
|
|
1695
|
+
surface,
|
|
1696
|
+
commandExists: deps.commandExists,
|
|
1697
|
+
runInteractiveCommand: deps.runInteractiveCommand,
|
|
1698
|
+
runStreamingCommand: deps.runStreamingCommand,
|
|
1699
|
+
writeStdout: deps.writeStdout,
|
|
1700
|
+
writeStderr: deps.writeStderr,
|
|
1701
|
+
stdinIsTTY: process.stdin.isTTY,
|
|
1702
|
+
stdoutIsTTY: process.stdout.isTTY,
|
|
1703
|
+
verbose: options.verbose,
|
|
1704
|
+
})
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (options.json) {
|
|
1708
|
+
runtime.emitJson(result)
|
|
1709
|
+
}
|
|
1710
|
+
else {
|
|
1711
|
+
runtime.emit(renderRunExecutionRecap(result, { decorate: runtime.decorate }))
|
|
1712
|
+
runtime.emit(renderRunArtifactCard(result, { decorate: runtime.decorate }))
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
return result
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
export async function runStatus(
|
|
1719
|
+
deps: CliDeps = {},
|
|
1720
|
+
options: StatusCommandOptions = {},
|
|
1721
|
+
): Promise<Awaited<ReturnType<typeof getStatusReport>>> {
|
|
1722
|
+
const runtime = createRuntime(deps, options)
|
|
1723
|
+
const getReport = deps.getStatusReport ?? getStatusReport
|
|
1724
|
+
emitBootstrap(runtime, 'Tela status', 'Inspect config, tool availability, repository state, and agent reachability.')
|
|
1725
|
+
const report = await getReport(process.cwd(), deps.fetchFn ?? fetch)
|
|
1726
|
+
|
|
1727
|
+
if (options.json) {
|
|
1728
|
+
runtime.emitJson(report)
|
|
1729
|
+
}
|
|
1730
|
+
else {
|
|
1731
|
+
runtime.emit(renderStatusReport(report, { decorate: runtime.decorate }))
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
return report
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
export async function runDoctor(
|
|
1738
|
+
deps: CliDeps = {},
|
|
1739
|
+
options: DoctorCommandOptions = {},
|
|
1740
|
+
): Promise<Awaited<ReturnType<typeof getDoctorReport>>> {
|
|
1741
|
+
const runtime = createRuntime(deps, options)
|
|
1742
|
+
const getReport = deps.getDoctorReport ?? getDoctorReport
|
|
1743
|
+
emitBootstrap(runtime, 'Tela doctor', 'Run a full environment diagnosis with explicit remediation steps.')
|
|
1744
|
+
const report = await getReport(process.cwd(), deps.fetchFn ?? fetch)
|
|
1745
|
+
|
|
1746
|
+
if (options.json) {
|
|
1747
|
+
runtime.emitJson(report)
|
|
1748
|
+
}
|
|
1749
|
+
else {
|
|
1750
|
+
runtime.emit(renderDoctorReport(report, { decorate: runtime.decorate }))
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
if (!report.ok) {
|
|
1754
|
+
process.exitCode = 1
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
return report
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function addStandardReadOptions(command: Command): Command {
|
|
1761
|
+
return command
|
|
1762
|
+
.option('--json', 'emit machine-readable JSON')
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
export function createProgram(deps: CliDeps = {}): Command {
|
|
1766
|
+
const program = new Command()
|
|
1767
|
+
.name('tela')
|
|
1768
|
+
.description('Tela CLI workspace helper')
|
|
1769
|
+
.showHelpAfterError()
|
|
1770
|
+
.option('--silent-bark', 'disable bark audio during interactive Ink startup')
|
|
1771
|
+
.option('--verbose', 'include raw evidence on failures')
|
|
1772
|
+
|
|
1773
|
+
installCustomHelp(program)
|
|
1774
|
+
|
|
1775
|
+
program
|
|
1776
|
+
.command('setup')
|
|
1777
|
+
.description('Configure Tela CLI settings and choose the Agent API target')
|
|
1778
|
+
.action(async () => {
|
|
1779
|
+
const globalOptions = program.opts<GlobalCliOptions>()
|
|
1780
|
+
await runSetup(deps, globalOptions)
|
|
1781
|
+
})
|
|
1782
|
+
|
|
1783
|
+
addStandardReadOptions(
|
|
1784
|
+
program
|
|
1785
|
+
.command('sync')
|
|
1786
|
+
.argument('[repo]', 'repository name')
|
|
1787
|
+
.option('--current', 'sync the current checkout')
|
|
1788
|
+
.option('--all', 'sync every repository under the configured clone root')
|
|
1789
|
+
.option('--dry-run', 'show what would happen without changing anything')
|
|
1790
|
+
.description('Clone or update a workspace repository, or sync the current checkout'),
|
|
1791
|
+
).action(async (repository: string | undefined, commandOptions: SyncCommandOptions) => {
|
|
1792
|
+
const globalOptions = program.opts<GlobalCliOptions>()
|
|
1793
|
+
await runSync(repository, deps, {
|
|
1794
|
+
...globalOptions,
|
|
1795
|
+
...commandOptions,
|
|
1796
|
+
})
|
|
1797
|
+
})
|
|
1798
|
+
|
|
1799
|
+
addStandardReadOptions(
|
|
1800
|
+
program
|
|
1801
|
+
.command('run')
|
|
1802
|
+
.option('--plain', 'force scrollback-first text output instead of the live viewer')
|
|
1803
|
+
.description('Run the local agent repository with Claude'),
|
|
1804
|
+
).action(async (commandOptions: RunCommandOptions) => {
|
|
1805
|
+
const globalOptions = program.opts<GlobalCliOptions>()
|
|
1806
|
+
await runAgent(deps, {
|
|
1807
|
+
...globalOptions,
|
|
1808
|
+
...commandOptions,
|
|
1809
|
+
})
|
|
1810
|
+
})
|
|
1811
|
+
|
|
1812
|
+
addStandardReadOptions(
|
|
1813
|
+
program
|
|
1814
|
+
.command('push')
|
|
1815
|
+
.option('--message <text>', 'use an explicit commit message')
|
|
1816
|
+
.option('--claude', 'generate the commit message with Claude')
|
|
1817
|
+
.option('--manual', 'enter the commit message manually')
|
|
1818
|
+
.option('--dry-run', 'show the push plan without committing or pushing')
|
|
1819
|
+
.description('Stage, commit, and push the current repository'),
|
|
1820
|
+
).action(async (commandOptions: PushCommandOptions) => {
|
|
1821
|
+
const globalOptions = program.opts<GlobalCliOptions>()
|
|
1822
|
+
await runPush(deps, {
|
|
1823
|
+
...globalOptions,
|
|
1824
|
+
...commandOptions,
|
|
1825
|
+
})
|
|
1826
|
+
})
|
|
1827
|
+
|
|
1828
|
+
addStandardReadOptions(
|
|
1829
|
+
program
|
|
1830
|
+
.command('feedback')
|
|
1831
|
+
.option('--list', 'list feedback for the current commit without launching Claude')
|
|
1832
|
+
.option('--apply <mode>', 'launch Claude for `all` or `select` feedback suggestions')
|
|
1833
|
+
.description('List commit feedback and address selected suggestions with Claude'),
|
|
1834
|
+
).action(async (commandOptions: FeedbackCommandFlags & GlobalCliOptions) => {
|
|
1835
|
+
const globalOptions = program.opts<GlobalCliOptions>()
|
|
1836
|
+
await runFeedback(deps, {
|
|
1837
|
+
...globalOptions,
|
|
1838
|
+
...commandOptions,
|
|
1839
|
+
})
|
|
1840
|
+
})
|
|
1841
|
+
|
|
1842
|
+
addStandardReadOptions(
|
|
1843
|
+
program
|
|
1844
|
+
.command('status')
|
|
1845
|
+
.description('Show Tela config, repo state, tool availability, and Agent API origin reachability'),
|
|
1846
|
+
).action(async (commandOptions: StatusCommandOptions) => {
|
|
1847
|
+
const globalOptions = program.opts<GlobalCliOptions>()
|
|
1848
|
+
await runStatus(deps, {
|
|
1849
|
+
...globalOptions,
|
|
1850
|
+
...commandOptions,
|
|
1851
|
+
})
|
|
1852
|
+
})
|
|
1853
|
+
|
|
1854
|
+
addStandardReadOptions(
|
|
1855
|
+
program
|
|
1856
|
+
.command('doctor')
|
|
1857
|
+
.alias('diag')
|
|
1858
|
+
.description('Diagnose config, tools, filesystem permissions, repo health, and Agent API origin reachability'),
|
|
1859
|
+
).action(async (commandOptions: DoctorCommandOptions) => {
|
|
1860
|
+
const globalOptions = program.opts<GlobalCliOptions>()
|
|
1861
|
+
await runDoctor(deps, {
|
|
1862
|
+
...globalOptions,
|
|
1863
|
+
...commandOptions,
|
|
1864
|
+
})
|
|
1865
|
+
})
|
|
1866
|
+
|
|
1867
|
+
return program
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
export async function main(argv = process.argv): Promise<void> {
|
|
1871
|
+
await createProgram().parseAsync(argv)
|
|
1872
|
+
}
|