@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/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
+ }