@meistrari/tela-cli 0.6.0 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -106
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/doctor-auth.d.ts +2 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/credentials.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7027 -0
- package/dist/output.d.ts +13 -0
- package/dist/program.d.ts +2 -0
- package/package.json +17 -24
- package/scripts/install-skills.cjs +127 -0
- package/skills/tela-cli/SKILL.md +20 -0
- package/skills/tela-cli/references/agent.md +111 -0
- package/cli.ts +0 -25
- package/postinstall.sh +0 -19
- package/src/agent-api.ts +0 -300
- package/src/assets.d.ts +0 -4
- package/src/auth.ts +0 -432
- package/src/claude.ts +0 -101
- package/src/commands/doctor.ts +0 -35
- package/src/commands/feedback.ts +0 -701
- package/src/commands/metadata.ts +0 -242
- package/src/commands/push.ts +0 -208
- package/src/commands/run.ts +0 -1699
- package/src/commands/status.ts +0 -109
- package/src/commands/sync.ts +0 -245
- package/src/commit-message.ts +0 -69
- package/src/config.ts +0 -799
- package/src/diagnostics.ts +0 -813
- package/src/errors.ts +0 -122
- package/src/forgejo.ts +0 -233
- package/src/git.ts +0 -457
- package/src/ink/assets/160092__jorickhoofd__dog-bark-1.mp3 +0 -0
- package/src/ink/bark.tsx +0 -240
- package/src/ink/components.tsx +0 -177
- package/src/ink/runtime.tsx +0 -89
- package/src/ink/screens/feedback-screen.tsx +0 -394
- package/src/ink/screens/push-screen.tsx +0 -231
- package/src/ink/screens/run-screen.tsx +0 -291
- package/src/ink/screens/setup/breadcrumb.tsx +0 -23
- package/src/ink/screens/setup/setup-screen.tsx +0 -611
- package/src/ink/screens/setup/step-frame.tsx +0 -30
- package/src/ink/screens/setup/steps/done-step.tsx +0 -28
- package/src/ink/screens/setup/steps/environment-step.tsx +0 -22
- package/src/ink/screens/setup/steps/organization-step.tsx +0 -34
- package/src/ink/screens/setup/steps/review-step.tsx +0 -77
- package/src/ink/screens/setup/steps/running-step.tsx +0 -36
- package/src/ink/screens/setup/steps/ssh-key-step.tsx +0 -47
- package/src/ink/screens/setup/steps/text-field-step.tsx +0 -17
- package/src/ink/screens/setup/steps.ts +0 -156
- package/src/ink/screens/setup/types.ts +0 -65
- package/src/ink/screens/sync-screen.tsx +0 -282
- package/src/process.ts +0 -251
- package/src/program.ts +0 -2105
- package/src/runtime.ts +0 -147
- package/src/types.ts +0 -165
- package/src/ui.ts +0 -295
- package/src/version.ts +0 -3
package/src/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
|
-
}
|