@meistrari/tela-cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -6
- package/package.json +5 -1
- package/postinstall.sh +19 -0
- package/src/agent-api.ts +102 -2
- package/src/assets.d.ts +4 -0
- package/src/auth.ts +423 -7
- package/src/commands/feedback.ts +1 -1
- package/src/commands/metadata.ts +18 -6
- package/src/commands/status.ts +10 -1
- package/src/config.ts +522 -147
- package/src/diagnostics.ts +127 -5
- package/src/forgejo.ts +189 -0
- package/src/ink/assets/160092__jorickhoofd__dog-bark-1.mp3 +0 -0
- package/src/ink/bark.tsx +14 -62
- package/src/ink/screens/setup/breadcrumb.tsx +23 -0
- package/src/ink/screens/setup/setup-screen.tsx +574 -0
- package/src/ink/screens/setup/step-frame.tsx +30 -0
- package/src/ink/screens/setup/steps/done-step.tsx +28 -0
- package/src/ink/screens/setup/steps/environment-step.tsx +22 -0
- package/src/ink/screens/setup/steps/organization-step.tsx +34 -0
- package/src/ink/screens/setup/steps/review-step.tsx +76 -0
- package/src/ink/screens/setup/steps/running-step.tsx +36 -0
- package/src/ink/screens/setup/steps/ssh-key-step.tsx +47 -0
- package/src/ink/screens/setup/steps/text-field-step.tsx +17 -0
- package/src/ink/screens/setup/steps.ts +145 -0
- package/src/ink/screens/setup/types.ts +63 -0
- package/src/program.ts +228 -96
- package/src/types.ts +64 -1
- package/src/ink/screens/setup-screen.tsx +0 -352
package/src/program.ts
CHANGED
|
@@ -14,9 +14,11 @@ import {
|
|
|
14
14
|
} from './ink/screens/feedback-screen.tsx'
|
|
15
15
|
import { PushScreen } from './ink/screens/push-screen.tsx'
|
|
16
16
|
import { RunChatHandoffScreen, RunScreen } from './ink/screens/run-screen.tsx'
|
|
17
|
-
import { SetupScreen, type SetupScreenResult } from './ink/screens/setup-screen.tsx'
|
|
17
|
+
import { SetupScreen, type SetupScreenResult } from './ink/screens/setup/setup-screen.tsx'
|
|
18
18
|
import { SyncScreen, type SyncProgressItem } from './ink/screens/sync-screen.tsx'
|
|
19
19
|
import { renderCommandScreen } from './ink/runtime.tsx'
|
|
20
|
+
import { listOrganizations } from './agent-api.ts'
|
|
21
|
+
import { authenticateWithDeviceFlow, type SetupAuthEvent } from './auth.ts'
|
|
20
22
|
import {
|
|
21
23
|
checkAgentApiReachability,
|
|
22
24
|
getAgentApiOriginCheckStatus,
|
|
@@ -69,26 +71,39 @@ import {
|
|
|
69
71
|
import { getDoctorReport, renderDoctorReport } from './commands/doctor.ts'
|
|
70
72
|
import { getStatusReport, renderStatusReport } from './commands/status.ts'
|
|
71
73
|
import {
|
|
72
|
-
|
|
73
|
-
DEFAULT_AGENT_API_URL,
|
|
74
|
+
DEVICE_FLOW_BROWSER_HELP,
|
|
74
75
|
ensureConfigInteractive,
|
|
76
|
+
ENVIRONMENT_PRESET_OPTIONS,
|
|
77
|
+
getPreferredForgejoOrganization,
|
|
75
78
|
getDefaultCloneRoot,
|
|
76
79
|
inspectSavedConfig,
|
|
77
|
-
|
|
80
|
+
redactSecret,
|
|
81
|
+
resolveEnvironmentConfigForPreset,
|
|
78
82
|
saveConfig,
|
|
79
83
|
validateAgentApiUrl,
|
|
80
|
-
|
|
84
|
+
validateApplicationId,
|
|
85
|
+
validateAuthApiUrl,
|
|
86
|
+
validateAuthDashboardUrl,
|
|
81
87
|
validateCloneRoot,
|
|
88
|
+
validateTelaApiUrl,
|
|
82
89
|
setupConfigInteractive,
|
|
83
90
|
type ConfigInspection,
|
|
91
|
+
type SetupConfigInput,
|
|
84
92
|
} from './config.ts'
|
|
85
93
|
import { findRepositoryRoot, getDiffStatAgainstHead } from './git.ts'
|
|
94
|
+
import {
|
|
95
|
+
ensureForgejoUserAccess,
|
|
96
|
+
listDiscoveredSshPublicKeys,
|
|
97
|
+
type SelectedSshPublicKey,
|
|
98
|
+
} from './forgejo.ts'
|
|
86
99
|
import { commandExists, runInteractiveCommand, runStreamingCommand } from './process.ts'
|
|
87
100
|
import { CommandRuntime } from './runtime.ts'
|
|
88
101
|
import type {
|
|
89
102
|
BatchSyncFailure,
|
|
90
103
|
SyncResult,
|
|
104
|
+
TelaCliAuthState,
|
|
91
105
|
TelaCliConfig,
|
|
106
|
+
TelaCliForgejoOrganization,
|
|
92
107
|
} from './types.ts'
|
|
93
108
|
import {
|
|
94
109
|
bulletList,
|
|
@@ -109,6 +124,7 @@ function promptValue<T>(value: T | symbol): T {
|
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
interface CliDeps {
|
|
127
|
+
authenticateWithDeviceFlow?: typeof authenticateWithDeviceFlow
|
|
112
128
|
prompts?: PromptModule
|
|
113
129
|
ensureConfigInteractive?: typeof ensureConfigInteractive
|
|
114
130
|
setupConfigInteractive?: typeof setupConfigInteractive
|
|
@@ -129,12 +145,15 @@ interface CliDeps {
|
|
|
129
145
|
getStatusReport?: typeof getStatusReport
|
|
130
146
|
getDoctorReport?: typeof getDoctorReport
|
|
131
147
|
createInkBarkSession?: typeof createInkBarkSession
|
|
148
|
+
renderCommandScreen?: typeof renderCommandScreen
|
|
132
149
|
commandExists?: typeof commandExists
|
|
133
150
|
runInteractiveCommand?: typeof runInteractiveCommand
|
|
134
151
|
runStreamingCommand?: typeof runStreamingCommand
|
|
135
152
|
fetchFn?: typeof fetch
|
|
136
153
|
writeStdout?: (message: string) => void
|
|
137
154
|
writeStderr?: (message: string) => void
|
|
155
|
+
stdinIsTTY?: boolean
|
|
156
|
+
stdoutIsTTY?: boolean
|
|
138
157
|
}
|
|
139
158
|
|
|
140
159
|
interface GlobalCliOptions {
|
|
@@ -190,16 +209,24 @@ function renderOptionsForStdout(): { decorate: boolean } {
|
|
|
190
209
|
return { decorate: Boolean(process.stdout.isTTY) }
|
|
191
210
|
}
|
|
192
211
|
|
|
193
|
-
function
|
|
194
|
-
return
|
|
212
|
+
function getStdinIsTTY(deps: CliDeps): boolean {
|
|
213
|
+
return deps.stdinIsTTY ?? Boolean(process.stdin.isTTY)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getStdoutIsTTY(deps: CliDeps): boolean {
|
|
217
|
+
return deps.stdoutIsTTY ?? Boolean(process.stdout.isTTY)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isInteractiveInkSession(deps: CliDeps, options: GlobalCliOptions = {}): boolean {
|
|
221
|
+
return !options.json && getStdoutIsTTY(deps) && getStdinIsTTY(deps)
|
|
195
222
|
}
|
|
196
223
|
|
|
197
|
-
function resolveRunSurface(options: RunCommandOptions = {}): 'plain' | 'viewer' | 'json' {
|
|
224
|
+
function resolveRunSurface(deps: CliDeps, options: RunCommandOptions = {}): 'plain' | 'viewer' | 'json' {
|
|
198
225
|
if (options.json) {
|
|
199
226
|
return 'json'
|
|
200
227
|
}
|
|
201
228
|
|
|
202
|
-
if (options.plain || !isInteractiveInkSession(options)) {
|
|
229
|
+
if (options.plain || !isInteractiveInkSession(deps, options)) {
|
|
203
230
|
return 'plain'
|
|
204
231
|
}
|
|
205
232
|
|
|
@@ -230,6 +257,10 @@ function createRuntime(deps: CliDeps, options: GlobalCliOptions = {}): CommandRu
|
|
|
230
257
|
})
|
|
231
258
|
}
|
|
232
259
|
|
|
260
|
+
function getRenderCommandScreen(deps: CliDeps): typeof renderCommandScreen {
|
|
261
|
+
return deps.renderCommandScreen ?? renderCommandScreen
|
|
262
|
+
}
|
|
263
|
+
|
|
233
264
|
function renderExamples(command: CommandMetadata): string {
|
|
234
265
|
return command.examples
|
|
235
266
|
.map(example => `${example.command.padEnd(36, ' ')} ${example.description}`)
|
|
@@ -261,7 +292,7 @@ function renderRootHelp(program: Command): string {
|
|
|
261
292
|
section('Global flags', table(
|
|
262
293
|
['Flag', 'Description'],
|
|
263
294
|
[
|
|
264
|
-
['--silent-bark', 'Disable bark audio during interactive
|
|
295
|
+
['--silent-bark', 'Disable bark audio during the interactive `tela setup` intro'],
|
|
265
296
|
['--verbose', 'Include raw evidence for failures where available'],
|
|
266
297
|
['-h, --help', 'Show command help'],
|
|
267
298
|
],
|
|
@@ -322,8 +353,8 @@ function createBarkSession(
|
|
|
322
353
|
commandTitle,
|
|
323
354
|
enabled: shouldShowBarkIntro({
|
|
324
355
|
json: options.json,
|
|
325
|
-
stdinIsTTY:
|
|
326
|
-
stdoutIsTTY:
|
|
356
|
+
stdinIsTTY: getStdinIsTTY(deps),
|
|
357
|
+
stdoutIsTTY: getStdoutIsTTY(deps),
|
|
327
358
|
}),
|
|
328
359
|
silentBark: options.silentBark,
|
|
329
360
|
env: process.env,
|
|
@@ -353,6 +384,7 @@ function summarizeConfigInspection(inspection: ConfigInspection): string {
|
|
|
353
384
|
return kvList([
|
|
354
385
|
{ label: 'Config path', value: inspection.path },
|
|
355
386
|
{ label: 'Status', value: 'missing' },
|
|
387
|
+
{ label: 'Environment', value: 'production' },
|
|
356
388
|
{ label: 'Clone root', value: getDefaultCloneRoot() },
|
|
357
389
|
])
|
|
358
390
|
}
|
|
@@ -367,9 +399,15 @@ function summarizeConfigInspection(inspection: ConfigInspection): string {
|
|
|
367
399
|
|
|
368
400
|
return kvList([
|
|
369
401
|
{ label: 'Config path', value: inspection.path },
|
|
402
|
+
{ label: 'Environment', value: inspection.config.environment },
|
|
370
403
|
{ label: 'Agent API origin', value: inspection.config.agentApiUrl },
|
|
404
|
+
{ label: 'Auth API URL', value: inspection.config.authApiUrl },
|
|
405
|
+
{ label: 'Tela API URL', value: inspection.config.telaApiUrl },
|
|
371
406
|
{ label: 'Clone root', value: inspection.config.cloneRoot },
|
|
372
|
-
{ label: '
|
|
407
|
+
{ label: 'User', value: inspection.config.auth?.user.email ?? '<signed out>' },
|
|
408
|
+
{ label: 'Workspace', value: inspection.config.auth?.workspace.title ?? '<signed out>' },
|
|
409
|
+
{ label: 'Organization', value: inspection.config.forgejoOrganization?.name ?? '<not selected>' },
|
|
410
|
+
{ label: 'Forgejo user', value: inspection.config.forgejoUserRegistered ? 'registered' : 'pending' },
|
|
373
411
|
])
|
|
374
412
|
}
|
|
375
413
|
|
|
@@ -377,7 +415,7 @@ function buildSetupChecksRows(checks: Array<{ label: string, status: string, det
|
|
|
377
415
|
return checks.map(check => [check.label, check.status, check.detail])
|
|
378
416
|
}
|
|
379
417
|
|
|
380
|
-
function ensureCloneRootReady(
|
|
418
|
+
function ensureCloneRootReady(cloneRoot: string): { created: boolean, warning?: string } {
|
|
381
419
|
if (!existsSync(cloneRoot)) {
|
|
382
420
|
return { created: false }
|
|
383
421
|
}
|
|
@@ -397,30 +435,51 @@ function ensureCloneRootReady(prompts: PromptModule, cloneRoot: string): { creat
|
|
|
397
435
|
}
|
|
398
436
|
|
|
399
437
|
async function executeSetupFlow(
|
|
400
|
-
input: {
|
|
401
|
-
agentApiUrl: string
|
|
402
|
-
cloneRoot: string
|
|
403
|
-
apiKey: string
|
|
438
|
+
input: SetupConfigInput & {
|
|
404
439
|
createCloneRoot: boolean
|
|
440
|
+
sshKey: SelectedSshPublicKey
|
|
441
|
+
auth: TelaCliAuthState
|
|
442
|
+
organization: TelaCliForgejoOrganization
|
|
405
443
|
},
|
|
406
444
|
deps: CliDeps = {},
|
|
445
|
+
onEvent?: (event: Pick<SetupAuthEvent, 'message' | 'title'>) => void,
|
|
407
446
|
): Promise<SetupScreenResult> {
|
|
408
447
|
const commandExistsFn = deps.commandExists ?? commandExists
|
|
409
448
|
const fetchFn = deps.fetchFn ?? fetch
|
|
410
|
-
|
|
411
|
-
const
|
|
449
|
+
|
|
450
|
+
const baseConfig: TelaCliConfig = {
|
|
451
|
+
environment: input.environment,
|
|
412
452
|
agentApiUrl: input.agentApiUrl,
|
|
453
|
+
authApiUrl: input.authApiUrl,
|
|
454
|
+
authDashboardUrl: input.authDashboardUrl,
|
|
455
|
+
telaApiUrl: input.telaApiUrl,
|
|
413
456
|
cloneRoot: input.cloneRoot,
|
|
414
|
-
|
|
415
|
-
|
|
457
|
+
requesterApplicationId: input.requesterApplicationId,
|
|
458
|
+
targetApplicationId: input.targetApplicationId,
|
|
459
|
+
auth: input.auth,
|
|
460
|
+
forgejoOrganization: input.organization,
|
|
461
|
+
forgejoUserRegistered: false,
|
|
462
|
+
}
|
|
463
|
+
const forgejoProvision = await ensureForgejoUserAccess({
|
|
464
|
+
config: baseConfig,
|
|
465
|
+
organization: input.organization,
|
|
466
|
+
sshKey: input.sshKey,
|
|
467
|
+
fetchFn,
|
|
468
|
+
onProgress: (message, title) => {
|
|
469
|
+
onEvent?.({
|
|
470
|
+
title,
|
|
471
|
+
message,
|
|
472
|
+
})
|
|
473
|
+
},
|
|
474
|
+
saveConfig,
|
|
475
|
+
})
|
|
476
|
+
const config = forgejoProvision.config
|
|
416
477
|
|
|
417
478
|
const cloneRootExists = existsSync(config.cloneRoot)
|
|
418
479
|
if (!cloneRootExists && input.createCloneRoot) {
|
|
419
480
|
mkdirSync(config.cloneRoot, { recursive: true, mode: 0o755 })
|
|
420
481
|
}
|
|
421
482
|
|
|
422
|
-
saveConfig(config)
|
|
423
|
-
|
|
424
483
|
const [gitAvailable, claudeAvailable, reachability] = await Promise.all([
|
|
425
484
|
commandExistsFn('git'),
|
|
426
485
|
commandExistsFn('claude'),
|
|
@@ -434,6 +493,9 @@ async function executeSetupFlow(
|
|
|
434
493
|
getAgentApiOriginWarning(reachability),
|
|
435
494
|
].filter((warning): warning is string => Boolean(warning))
|
|
436
495
|
const checks = [
|
|
496
|
+
{ label: 'auth', status: 'authenticated', detail: `${input.auth.user.email} @ ${input.auth.workspace.title}` },
|
|
497
|
+
{ label: 'organization', status: 'selected', detail: input.organization.name },
|
|
498
|
+
{ label: 'forgejo-user', status: forgejoProvision.response.userCreated ? 'created' : 'ensured', detail: forgejoProvision.detail },
|
|
437
499
|
{ label: 'clone-root', status: existsSync(config.cloneRoot) ? 'ready' : 'missing', detail: config.cloneRoot },
|
|
438
500
|
{ label: 'git', status: gitAvailable ? 'available' : 'missing', detail: gitAvailable ? 'ready' : 'install git' },
|
|
439
501
|
{ label: 'claude', status: claudeAvailable ? 'available' : 'missing', detail: claudeAvailable ? 'ready' : 'install Claude CLI' },
|
|
@@ -445,7 +507,7 @@ async function executeSetupFlow(
|
|
|
445
507
|
config,
|
|
446
508
|
warnings,
|
|
447
509
|
nextActions: [
|
|
448
|
-
'Run `tela status` to review the saved
|
|
510
|
+
'Run `tela status` to review the saved workspace session.',
|
|
449
511
|
existsSync(config.cloneRoot)
|
|
450
512
|
? 'Run `tela sync <repo>` or `tela sync --current` when you need a checkout.'
|
|
451
513
|
: 'Create the clone root or rerun setup before the first repository sync.',
|
|
@@ -457,25 +519,59 @@ async function executeSetupFlow(
|
|
|
457
519
|
}
|
|
458
520
|
}
|
|
459
521
|
|
|
522
|
+
function createMissingSetupOrganizationsError(workspaceTitle: string): ReturnType<typeof createUserFacingError> {
|
|
523
|
+
return createUserFacingError({
|
|
524
|
+
code: 'workspace_organization_required',
|
|
525
|
+
summary: 'No Forgejo organizations are registered for this workspace.',
|
|
526
|
+
details: `Tela authenticated successfully for ${workspaceTitle}, but the workspace does not have any registered organizations yet.`,
|
|
527
|
+
fixes: [
|
|
528
|
+
'Ask the Tela team to register at least one organization for this workspace, then rerun `tela setup`.',
|
|
529
|
+
],
|
|
530
|
+
})
|
|
531
|
+
}
|
|
532
|
+
|
|
460
533
|
async function runSetupInk(
|
|
461
534
|
deps: CliDeps = {},
|
|
462
535
|
options: GlobalCliOptions = {},
|
|
463
536
|
barkSession = createBarkSession(deps, options, 'Setup'),
|
|
464
537
|
): Promise<SetupScreenResult> {
|
|
538
|
+
const renderScreen = getRenderCommandScreen(deps)
|
|
465
539
|
const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
|
|
540
|
+
const defaultEnvironment = inspection.config?.environment ?? 'production'
|
|
541
|
+
const defaultEnvironmentConfig = defaultEnvironment === 'custom'
|
|
542
|
+
? {
|
|
543
|
+
environment: 'custom' as const,
|
|
544
|
+
agentApiUrl: inspection.config?.agentApiUrl ?? '',
|
|
545
|
+
authApiUrl: inspection.config?.authApiUrl ?? '',
|
|
546
|
+
authDashboardUrl: inspection.config?.authDashboardUrl ?? '',
|
|
547
|
+
telaApiUrl: inspection.config?.telaApiUrl ?? '',
|
|
548
|
+
requesterApplicationId: inspection.config?.requesterApplicationId ?? '',
|
|
549
|
+
targetApplicationId: inspection.config?.targetApplicationId ?? '',
|
|
550
|
+
}
|
|
551
|
+
: resolveEnvironmentConfigForPreset(defaultEnvironment)
|
|
466
552
|
await barkSession.playOnce()
|
|
467
|
-
return await
|
|
553
|
+
return await renderScreen<SetupScreenResult>((controller) => React.createElement(SetupScreen, {
|
|
468
554
|
controller,
|
|
469
555
|
currentConfigSummary: summarizeConfigInspection(inspection).split('\n'),
|
|
470
556
|
defaults: {
|
|
471
|
-
|
|
557
|
+
environment: defaultEnvironment,
|
|
558
|
+
agentApiUrl: inspection.config?.agentApiUrl ?? defaultEnvironmentConfig.agentApiUrl,
|
|
559
|
+
authApiUrl: inspection.config?.authApiUrl ?? defaultEnvironmentConfig.authApiUrl,
|
|
560
|
+
authDashboardUrl: inspection.config?.authDashboardUrl ?? defaultEnvironmentConfig.authDashboardUrl,
|
|
561
|
+
telaApiUrl: inspection.config?.telaApiUrl ?? defaultEnvironmentConfig.telaApiUrl,
|
|
562
|
+
requesterApplicationId: inspection.config?.requesterApplicationId ?? defaultEnvironmentConfig.requesterApplicationId,
|
|
563
|
+
targetApplicationId: inspection.config?.targetApplicationId ?? defaultEnvironmentConfig.targetApplicationId,
|
|
472
564
|
cloneRoot: inspection.config?.cloneRoot ?? getDefaultCloneRoot(),
|
|
473
|
-
|
|
565
|
+
forgejoOrganization: inspection.config?.forgejoOrganization ?? null,
|
|
566
|
+
forgejoUserRegistered: inspection.config?.forgejoUserRegistered ?? false,
|
|
474
567
|
},
|
|
475
|
-
|
|
568
|
+
sshPublicKeys: listDiscoveredSshPublicKeys(),
|
|
476
569
|
validateAgentApiUrl,
|
|
570
|
+
validateAuthApiUrl,
|
|
571
|
+
validateAuthDashboardUrl,
|
|
572
|
+
validateTelaApiUrl,
|
|
573
|
+
validateApplicationId,
|
|
477
574
|
validateCloneRoot,
|
|
478
|
-
validateApiKey,
|
|
479
575
|
inspectCloneRoot: (path: string) => {
|
|
480
576
|
if (!existsSync(path)) {
|
|
481
577
|
return { exists: false, isDirectory: false }
|
|
@@ -486,42 +582,58 @@ async function runSetupInk(
|
|
|
486
582
|
isDirectory: statSync(path).isDirectory(),
|
|
487
583
|
}
|
|
488
584
|
},
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
585
|
+
authenticate: async (input, onEvent) => await (deps.authenticateWithDeviceFlow ?? authenticateWithDeviceFlow)({
|
|
586
|
+
environment: input.environment,
|
|
587
|
+
authApiUrl: input.authApiUrl,
|
|
588
|
+
authDashboardUrl: input.authDashboardUrl,
|
|
589
|
+
requesterApplicationId: input.requesterApplicationId,
|
|
590
|
+
targetApplicationId: input.targetApplicationId,
|
|
591
|
+
}, {
|
|
592
|
+
onEvent,
|
|
593
|
+
}),
|
|
594
|
+
resolveOrganizations: async (input) => {
|
|
595
|
+
const config: TelaCliConfig = {
|
|
596
|
+
environment: input.environment,
|
|
597
|
+
agentApiUrl: input.agentApiUrl,
|
|
598
|
+
authApiUrl: input.authApiUrl,
|
|
599
|
+
authDashboardUrl: input.authDashboardUrl,
|
|
600
|
+
telaApiUrl: input.telaApiUrl,
|
|
601
|
+
cloneRoot: input.cloneRoot,
|
|
602
|
+
requesterApplicationId: input.requesterApplicationId,
|
|
603
|
+
targetApplicationId: input.targetApplicationId,
|
|
604
|
+
auth: input.auth,
|
|
605
|
+
forgejoOrganization: null,
|
|
606
|
+
forgejoUserRegistered: false,
|
|
607
|
+
}
|
|
608
|
+
const organizations = await listOrganizations(config, deps.fetchFn ?? fetch)
|
|
609
|
+
if (organizations.length === 0) {
|
|
610
|
+
throw createMissingSetupOrganizationsError(input.auth.workspace.title)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
organizations,
|
|
615
|
+
preferredOrganization: getPreferredForgejoOrganization(
|
|
616
|
+
inspection.config ?? null,
|
|
617
|
+
input.auth,
|
|
618
|
+
organizations,
|
|
619
|
+
),
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
execute: async (input, onEvent) => await executeSetupFlow(input, deps, onEvent),
|
|
495
623
|
}))
|
|
496
624
|
}
|
|
497
625
|
|
|
498
626
|
async function ensureConfigForCommand(
|
|
499
627
|
deps: CliDeps,
|
|
500
|
-
options: GlobalCliOptions,
|
|
501
|
-
barkSession?: BarkIntroSession,
|
|
502
628
|
): Promise<TelaCliConfig> {
|
|
503
629
|
const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
|
|
504
|
-
if (inspection.valid && inspection.config) {
|
|
630
|
+
if (inspection.valid && inspection.config?.auth) {
|
|
505
631
|
return inspection.config
|
|
506
632
|
}
|
|
507
633
|
|
|
508
|
-
if (isInteractiveInkSession(options)) {
|
|
509
|
-
const setupResult = await runSetupInk(
|
|
510
|
-
deps,
|
|
511
|
-
options,
|
|
512
|
-
barkSession ?? createBarkSession(deps, options, 'Setup'),
|
|
513
|
-
)
|
|
514
|
-
return setupResult.config
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (deps.prompts) {
|
|
518
|
-
const ensureConfig = deps.ensureConfigInteractive ?? ensureConfigInteractive
|
|
519
|
-
return await ensureConfig({ prompts: deps.prompts })
|
|
520
|
-
}
|
|
521
|
-
|
|
522
634
|
throw createUserFacingError({
|
|
523
635
|
code: 'config_required',
|
|
524
|
-
summary: 'Tela CLI is not configured.',
|
|
636
|
+
summary: inspection.valid ? 'Tela CLI is signed out.' : 'Tela CLI is not configured.',
|
|
525
637
|
fixes: [
|
|
526
638
|
'Run `tela setup` in an interactive terminal first.',
|
|
527
639
|
],
|
|
@@ -532,7 +644,7 @@ export async function runSetup(
|
|
|
532
644
|
deps: CliDeps = {},
|
|
533
645
|
options: GlobalCliOptions = {},
|
|
534
646
|
): Promise<SetupCommandResult> {
|
|
535
|
-
if (isInteractiveInkSession(options)) {
|
|
647
|
+
if (isInteractiveInkSession(deps, options)) {
|
|
536
648
|
return await runSetupInk(deps, options)
|
|
537
649
|
}
|
|
538
650
|
|
|
@@ -542,17 +654,20 @@ export async function runSetup(
|
|
|
542
654
|
const inspect = deps.inspectSavedConfig ?? inspectSavedConfig
|
|
543
655
|
const commandExistsFn = deps.commandExists ?? commandExists
|
|
544
656
|
const fetchFn = deps.fetchFn ?? fetch
|
|
657
|
+
const authenticate = deps.authenticateWithDeviceFlow ?? authenticateWithDeviceFlow
|
|
545
658
|
const inspection = inspect()
|
|
546
659
|
|
|
547
660
|
prompts.intro('Tela CLI setup')
|
|
548
661
|
emitBootstrap(runtime, 'Tela setup', 'Bootstrap the local Tela environment before the first sync or push.')
|
|
549
662
|
runtime.emit(resultCard('Setup preflight', [
|
|
550
663
|
section('Current config', summarizeConfigInspection(inspection), { decorate: runtime.decorate }),
|
|
551
|
-
section('
|
|
664
|
+
section('Authentication', DEVICE_FLOW_BROWSER_HELP, { decorate: runtime.decorate }),
|
|
552
665
|
section('What setup updates', bulletList([
|
|
553
|
-
'
|
|
666
|
+
'Tela environment and service endpoints',
|
|
554
667
|
'Clone root directory',
|
|
555
|
-
'
|
|
668
|
+
'Workspace-backed Tela session',
|
|
669
|
+
'Forgejo organization selection for the workspace',
|
|
670
|
+
'Forgejo user access with your SSH public key',
|
|
556
671
|
]), { decorate: runtime.decorate }),
|
|
557
672
|
], { decorate: runtime.decorate }))
|
|
558
673
|
|
|
@@ -560,11 +675,22 @@ export async function runSetup(
|
|
|
560
675
|
prompts,
|
|
561
676
|
existingConfig: inspection.config ?? null,
|
|
562
677
|
skipLifecycle: true,
|
|
678
|
+
authenticate: async (input, notify) => {
|
|
679
|
+
return await authenticate({
|
|
680
|
+
environment: input.environment,
|
|
681
|
+
authApiUrl: input.authApiUrl,
|
|
682
|
+
authDashboardUrl: input.authDashboardUrl,
|
|
683
|
+
requesterApplicationId: input.requesterApplicationId,
|
|
684
|
+
targetApplicationId: input.targetApplicationId,
|
|
685
|
+
}, {
|
|
686
|
+
onEvent: event => notify(event.message, event.title),
|
|
687
|
+
})
|
|
688
|
+
},
|
|
563
689
|
})
|
|
564
690
|
|
|
565
691
|
let cloneRootCreated = false
|
|
566
692
|
let cloneRootWarning: string | undefined
|
|
567
|
-
const cloneRootStatus = ensureCloneRootReady(
|
|
693
|
+
const cloneRootStatus = ensureCloneRootReady(config.cloneRoot)
|
|
568
694
|
cloneRootWarning = cloneRootStatus.warning
|
|
569
695
|
|
|
570
696
|
if (!existsSync(config.cloneRoot)) {
|
|
@@ -599,7 +725,7 @@ export async function runSetup(
|
|
|
599
725
|
].filter((warning): warning is string => Boolean(warning))
|
|
600
726
|
|
|
601
727
|
const nextSetupActions = [
|
|
602
|
-
'Run `tela status` to review the saved
|
|
728
|
+
'Run `tela status` to review the saved workspace session.',
|
|
603
729
|
cloneRootCreated
|
|
604
730
|
? 'Run `tela sync <repo>` to clone a repository into the new clone root.'
|
|
605
731
|
: 'Run `tela sync <repo>` or `tela sync --current` when you need a checkout.',
|
|
@@ -610,14 +736,24 @@ export async function runSetup(
|
|
|
610
736
|
|
|
611
737
|
runtime.emit(resultCard('Setup complete', [
|
|
612
738
|
section('Saved config', kvList([
|
|
739
|
+
{ label: 'Environment', value: config.environment },
|
|
613
740
|
{ label: 'Agent API origin', value: config.agentApiUrl },
|
|
741
|
+
{ label: 'Auth API URL', value: config.authApiUrl },
|
|
742
|
+
{ label: 'Tela API URL', value: config.telaApiUrl },
|
|
614
743
|
{ label: 'Clone root', value: config.cloneRoot },
|
|
615
|
-
{ label: '
|
|
744
|
+
{ label: 'User', value: config.auth?.user.email ?? '<missing>' },
|
|
745
|
+
{ label: 'Workspace', value: config.auth?.workspace.title ?? '<missing>' },
|
|
746
|
+
{ label: 'Organization', value: config.forgejoOrganization?.name ?? '<missing>' },
|
|
747
|
+
{ label: 'Forgejo user', value: config.forgejoUserRegistered ? 'registered' : 'pending' },
|
|
748
|
+
{ label: 'Session', value: config.auth ? `${redactSecret(config.auth.session.accessToken)} (expires ${new Date(config.auth.session.expiresAt).toISOString()})` : '<missing>' },
|
|
616
749
|
]), { decorate: runtime.decorate }),
|
|
617
|
-
section('
|
|
750
|
+
section('Authentication', DEVICE_FLOW_BROWSER_HELP, { decorate: runtime.decorate }),
|
|
618
751
|
section('Checks', table(
|
|
619
752
|
['Check', 'Status', 'Detail'],
|
|
620
753
|
buildSetupChecksRows([
|
|
754
|
+
{ label: 'auth', status: 'authenticated', detail: `${config.auth?.user.email ?? '<missing>'} @ ${config.auth?.workspace.title ?? '<missing>'}` },
|
|
755
|
+
{ label: 'organization', status: config.forgejoOrganization ? 'selected' : 'missing', detail: config.forgejoOrganization?.name ?? 'not selected' },
|
|
756
|
+
{ label: 'forgejo-user', status: config.forgejoUserRegistered ? 'registered' : 'pending', detail: config.forgejoUserRegistered ? 'SSH-key provisioning completed.' : 'SSH-key provisioning is pending.' },
|
|
621
757
|
{ label: 'clone-root', status: cloneRootCreated ? 'created' : (existsSync(config.cloneRoot) ? 'ready' : 'missing'), detail: config.cloneRoot },
|
|
622
758
|
{ label: 'git', status: gitAvailable ? 'available' : 'missing', detail: gitAvailable ? 'ready' : 'install git' },
|
|
623
759
|
{ label: 'claude', status: claudeAvailable ? 'available' : 'missing', detail: claudeAvailable ? 'ready' : 'install Claude CLI' },
|
|
@@ -841,8 +977,7 @@ async function runSyncInk(
|
|
|
841
977
|
deps: CliDeps,
|
|
842
978
|
options: SyncCommandOptions,
|
|
843
979
|
): Promise<SyncCommandResult> {
|
|
844
|
-
const
|
|
845
|
-
await barkSession.playOnce()
|
|
980
|
+
const renderScreen = getRenderCommandScreen(deps)
|
|
846
981
|
const findRepo = deps.findRepositoryRoot ?? findRepositoryRoot
|
|
847
982
|
const listRepositories = deps.listCloneRootRepositories ?? listCloneRootRepositories
|
|
848
983
|
const syncAll = deps.syncAllRepositories ?? syncAllRepositories
|
|
@@ -852,10 +987,10 @@ async function runSyncInk(
|
|
|
852
987
|
const mode = resolveSyncMode(repository, options, repoRoot)
|
|
853
988
|
const config = mode === 'current'
|
|
854
989
|
? undefined
|
|
855
|
-
: await ensureConfigForCommand(deps
|
|
990
|
+
: await ensureConfigForCommand(deps)
|
|
856
991
|
const availableRepositories = config ? listRepositories(config.cloneRoot) : undefined
|
|
857
992
|
|
|
858
|
-
return await
|
|
993
|
+
return await renderScreen<SyncCommandResult>((controller) => React.createElement(SyncScreen, {
|
|
859
994
|
controller,
|
|
860
995
|
mode,
|
|
861
996
|
dryRun: options.dryRun,
|
|
@@ -942,7 +1077,7 @@ export async function runSync(
|
|
|
942
1077
|
): Promise<SyncCommandResult> {
|
|
943
1078
|
validateSyncOptions(repository, options)
|
|
944
1079
|
|
|
945
|
-
if (isInteractiveInkSession(options)) {
|
|
1080
|
+
if (isInteractiveInkSession(deps, options)) {
|
|
946
1081
|
return await runSyncInk(repository, deps, options)
|
|
947
1082
|
}
|
|
948
1083
|
|
|
@@ -959,7 +1094,7 @@ export async function runSync(
|
|
|
959
1094
|
|
|
960
1095
|
const repoRoot = await findRepo(process.cwd())
|
|
961
1096
|
const config = options.all || !repoRoot || Boolean(repository)
|
|
962
|
-
? await ensureConfig(
|
|
1097
|
+
? await ensureConfig()
|
|
963
1098
|
: undefined
|
|
964
1099
|
|
|
965
1100
|
if (options.all) {
|
|
@@ -1170,8 +1305,7 @@ async function runPushInk(
|
|
|
1170
1305
|
deps: CliDeps,
|
|
1171
1306
|
options: PushCommandOptions,
|
|
1172
1307
|
): Promise<PushCommandResult> {
|
|
1173
|
-
const
|
|
1174
|
-
await barkSession.playOnce()
|
|
1308
|
+
const renderScreen = getRenderCommandScreen(deps)
|
|
1175
1309
|
const findRepo = deps.findRepositoryRoot ?? findRepositoryRoot
|
|
1176
1310
|
const preview = deps.previewPush ?? previewPush
|
|
1177
1311
|
const push = deps.pushRepository ?? pushRepository
|
|
@@ -1190,7 +1324,7 @@ async function runPushInk(
|
|
|
1190
1324
|
: undefined
|
|
1191
1325
|
const initialManualMessage = options.message?.trim() || undefined
|
|
1192
1326
|
|
|
1193
|
-
return await
|
|
1327
|
+
return await renderScreen<PushCommandResult>((controller) => React.createElement(PushScreen, {
|
|
1194
1328
|
controller,
|
|
1195
1329
|
initialMode,
|
|
1196
1330
|
initialManualMessage,
|
|
@@ -1241,7 +1375,7 @@ export async function runPush(
|
|
|
1241
1375
|
deps: CliDeps = {},
|
|
1242
1376
|
options: PushCommandOptions = {},
|
|
1243
1377
|
): Promise<PushCommandResult> {
|
|
1244
|
-
if (isInteractiveInkSession(options)) {
|
|
1378
|
+
if (isInteractiveInkSession(deps, options)) {
|
|
1245
1379
|
return await runPushInk(deps, options)
|
|
1246
1380
|
}
|
|
1247
1381
|
|
|
@@ -1446,7 +1580,7 @@ async function runFeedbackServiceFlow(
|
|
|
1446
1580
|
deps: CliDeps,
|
|
1447
1581
|
options: FeedbackCommandFlags & GlobalCliOptions,
|
|
1448
1582
|
): Promise<FeedbackRunResult> {
|
|
1449
|
-
const config = await ensureConfigForCommand(deps
|
|
1583
|
+
const config = await ensureConfigForCommand(deps)
|
|
1450
1584
|
const loadFeedbackDataFn = deps.loadFeedbackData ?? loadFeedbackData
|
|
1451
1585
|
const launchFeedbackClaudeFn = deps.launchFeedbackClaude ?? launchFeedbackClaude
|
|
1452
1586
|
const commandExistsFn = deps.commandExists ?? commandExists
|
|
@@ -1504,9 +1638,8 @@ async function runFeedbackInk(
|
|
|
1504
1638
|
deps: CliDeps,
|
|
1505
1639
|
options: FeedbackCommandFlags & GlobalCliOptions,
|
|
1506
1640
|
): Promise<FeedbackRunResult> {
|
|
1507
|
-
const
|
|
1508
|
-
await
|
|
1509
|
-
const config = await ensureConfigForCommand(deps, options, barkSession)
|
|
1641
|
+
const renderScreen = getRenderCommandScreen(deps)
|
|
1642
|
+
const config = await ensureConfigForCommand(deps)
|
|
1510
1643
|
const loadFeedbackDataFn = deps.loadFeedbackData ?? loadFeedbackData
|
|
1511
1644
|
const launchFeedbackClaudeFn = deps.launchFeedbackClaude ?? launchFeedbackClaude
|
|
1512
1645
|
const commandExistsFn = deps.commandExists ?? commandExists
|
|
@@ -1517,7 +1650,7 @@ async function runFeedbackInk(
|
|
|
1517
1650
|
config,
|
|
1518
1651
|
})
|
|
1519
1652
|
|
|
1520
|
-
const selection = await
|
|
1653
|
+
const selection = await renderScreen<{
|
|
1521
1654
|
selectedFeedbacks: FeedbackData['entries']
|
|
1522
1655
|
prompt?: string
|
|
1523
1656
|
launchedClaude: boolean
|
|
@@ -1545,7 +1678,7 @@ async function runFeedbackInk(
|
|
|
1545
1678
|
})
|
|
1546
1679
|
}
|
|
1547
1680
|
|
|
1548
|
-
await
|
|
1681
|
+
await renderScreen<void>((controller) => React.createElement(FeedbackClaudeHandoffScreen, {
|
|
1549
1682
|
controller,
|
|
1550
1683
|
repoRoot: data.repoRoot,
|
|
1551
1684
|
commitHash: data.commitHash,
|
|
@@ -1559,7 +1692,7 @@ async function runFeedbackInk(
|
|
|
1559
1692
|
|
|
1560
1693
|
const diffStat = await getDiffStatAgainstHeadFn(repoRoot)
|
|
1561
1694
|
|
|
1562
|
-
await
|
|
1695
|
+
await renderScreen<void>((controller) => React.createElement(FeedbackOutcomeScreen, {
|
|
1563
1696
|
controller,
|
|
1564
1697
|
commitHash: data.commitHash,
|
|
1565
1698
|
selectionMode,
|
|
@@ -1594,7 +1727,7 @@ export async function runFeedback(
|
|
|
1594
1727
|
})
|
|
1595
1728
|
}
|
|
1596
1729
|
|
|
1597
|
-
const interactive = isInteractiveInkSession(normalizedOptions)
|
|
1730
|
+
const interactive = isInteractiveInkSession(deps, normalizedOptions)
|
|
1598
1731
|
if (!interactive) {
|
|
1599
1732
|
emitBootstrap(runtime, 'Tela feedback', 'Review commit feedback and optionally hand selected fixes to Claude.')
|
|
1600
1733
|
}
|
|
@@ -1625,9 +1758,10 @@ export async function runAgent(
|
|
|
1625
1758
|
options: RunCommandOptions = {},
|
|
1626
1759
|
): Promise<RunCommandResult> {
|
|
1627
1760
|
const runtime = createRuntime(deps, options)
|
|
1761
|
+
const renderScreen = getRenderCommandScreen(deps)
|
|
1628
1762
|
const runAgentCommandFn = deps.runLocalAgentCommand ?? runLocalAgentCommand
|
|
1629
1763
|
const prepareRunFn = deps.prepareLocalAgentRun ?? prepareLocalAgentRun
|
|
1630
|
-
const surface = resolveRunSurface(options)
|
|
1764
|
+
const surface = resolveRunSurface(deps, options)
|
|
1631
1765
|
|
|
1632
1766
|
let result: RunCommandResult
|
|
1633
1767
|
|
|
@@ -1636,11 +1770,9 @@ export async function runAgent(
|
|
|
1636
1770
|
cwd: process.cwd(),
|
|
1637
1771
|
commandExists: deps.commandExists,
|
|
1638
1772
|
})
|
|
1639
|
-
const barkSession = createBarkSession(deps, options, 'Run')
|
|
1640
|
-
await barkSession.playOnce()
|
|
1641
1773
|
|
|
1642
1774
|
if (prepared.config.isMultiturn) {
|
|
1643
|
-
await
|
|
1775
|
+
await renderScreen<void>((controller) => React.createElement(RunChatHandoffScreen, {
|
|
1644
1776
|
controller,
|
|
1645
1777
|
snapshot: createInitialRunSnapshot(prepared, 'chat'),
|
|
1646
1778
|
}), {
|
|
@@ -1656,13 +1788,13 @@ export async function runAgent(
|
|
|
1656
1788
|
runStreamingCommand: deps.runStreamingCommand,
|
|
1657
1789
|
writeStdout: deps.writeStdout,
|
|
1658
1790
|
writeStderr: deps.writeStderr,
|
|
1659
|
-
stdinIsTTY:
|
|
1660
|
-
stdoutIsTTY:
|
|
1791
|
+
stdinIsTTY: getStdinIsTTY(deps),
|
|
1792
|
+
stdoutIsTTY: getStdoutIsTTY(deps),
|
|
1661
1793
|
verbose: options.verbose,
|
|
1662
1794
|
})
|
|
1663
1795
|
}
|
|
1664
1796
|
else {
|
|
1665
|
-
result = await
|
|
1797
|
+
result = await renderScreen<RunCommandResult>((controller) => React.createElement(RunScreen, {
|
|
1666
1798
|
controller,
|
|
1667
1799
|
initialSnapshot: createInitialRunSnapshot(prepared, 'one-shot'),
|
|
1668
1800
|
execute: async ({ onSnapshot, onControl }) => {
|
|
@@ -1677,8 +1809,8 @@ export async function runAgent(
|
|
|
1677
1809
|
writeStderr: deps.writeStderr,
|
|
1678
1810
|
onSnapshot,
|
|
1679
1811
|
onControl,
|
|
1680
|
-
stdinIsTTY:
|
|
1681
|
-
stdoutIsTTY:
|
|
1812
|
+
stdinIsTTY: getStdinIsTTY(deps),
|
|
1813
|
+
stdoutIsTTY: getStdoutIsTTY(deps),
|
|
1682
1814
|
verbose: options.verbose,
|
|
1683
1815
|
})
|
|
1684
1816
|
},
|
|
@@ -1698,8 +1830,8 @@ export async function runAgent(
|
|
|
1698
1830
|
runStreamingCommand: deps.runStreamingCommand,
|
|
1699
1831
|
writeStdout: deps.writeStdout,
|
|
1700
1832
|
writeStderr: deps.writeStderr,
|
|
1701
|
-
stdinIsTTY:
|
|
1702
|
-
stdoutIsTTY:
|
|
1833
|
+
stdinIsTTY: getStdinIsTTY(deps),
|
|
1834
|
+
stdoutIsTTY: getStdoutIsTTY(deps),
|
|
1703
1835
|
verbose: options.verbose,
|
|
1704
1836
|
})
|
|
1705
1837
|
}
|
|
@@ -1765,16 +1897,16 @@ function addStandardReadOptions(command: Command): Command {
|
|
|
1765
1897
|
export function createProgram(deps: CliDeps = {}): Command {
|
|
1766
1898
|
const program = new Command()
|
|
1767
1899
|
.name('tela')
|
|
1768
|
-
.description('Tela CLI workspace helper')
|
|
1900
|
+
.description('Tela CLI workspace and auth helper')
|
|
1769
1901
|
.showHelpAfterError()
|
|
1770
|
-
.option('--silent-bark', 'disable bark audio during interactive
|
|
1902
|
+
.option('--silent-bark', 'disable bark audio during the interactive tela setup intro')
|
|
1771
1903
|
.option('--verbose', 'include raw evidence on failures')
|
|
1772
1904
|
|
|
1773
1905
|
installCustomHelp(program)
|
|
1774
1906
|
|
|
1775
1907
|
program
|
|
1776
1908
|
.command('setup')
|
|
1777
|
-
.description('Configure Tela CLI settings and
|
|
1909
|
+
.description('Configure Tela CLI settings and complete device-flow authentication')
|
|
1778
1910
|
.action(async () => {
|
|
1779
1911
|
const globalOptions = program.opts<GlobalCliOptions>()
|
|
1780
1912
|
await runSetup(deps, globalOptions)
|
|
@@ -1842,7 +1974,7 @@ export function createProgram(deps: CliDeps = {}): Command {
|
|
|
1842
1974
|
addStandardReadOptions(
|
|
1843
1975
|
program
|
|
1844
1976
|
.command('status')
|
|
1845
|
-
.description('Show Tela config, repo state, tool availability, and Agent API
|
|
1977
|
+
.description('Show Tela config, session state, repo state, tool availability, and Agent API reachability'),
|
|
1846
1978
|
).action(async (commandOptions: StatusCommandOptions) => {
|
|
1847
1979
|
const globalOptions = program.opts<GlobalCliOptions>()
|
|
1848
1980
|
await runStatus(deps, {
|
|
@@ -1855,7 +1987,7 @@ export function createProgram(deps: CliDeps = {}): Command {
|
|
|
1855
1987
|
program
|
|
1856
1988
|
.command('doctor')
|
|
1857
1989
|
.alias('diag')
|
|
1858
|
-
.description('Diagnose config, tools, filesystem permissions, repo health, and Agent API
|
|
1990
|
+
.description('Diagnose config, auth session, tools, filesystem permissions, repo health, and Agent API reachability'),
|
|
1859
1991
|
).action(async (commandOptions: DoctorCommandOptions) => {
|
|
1860
1992
|
const globalOptions = program.opts<GlobalCliOptions>()
|
|
1861
1993
|
await runDoctor(deps, {
|