@meistrari/tela-cli 0.7.0 → 0.7.4

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