@meistrari/tela-cli 0.1.1 → 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/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
- AGENT_API_BROWSER_HELP,
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
- redactApiKey,
80
+ redactSecret,
81
+ resolveEnvironmentConfigForPreset,
78
82
  saveConfig,
79
83
  validateAgentApiUrl,
80
- validateApiKey,
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 isInteractiveInkSession(options: GlobalCliOptions = {}): boolean {
194
- return !options.json && Boolean(process.stdout.isTTY && process.stdin.isTTY)
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 Ink startup'],
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: process.stdin.isTTY,
326
- stdoutIsTTY: process.stdout.isTTY,
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: 'API key', value: redactApiKey(inspection.config.apiKey) },
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(prompts: PromptModule, cloneRoot: string): { created: boolean, warning?: string } {
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
- const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
411
- const config = {
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
- apiKey: input.apiKey.trim() || inspection.config?.apiKey || '',
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 environment.',
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 renderCommandScreen<SetupScreenResult>((controller) => React.createElement(SetupScreen, {
553
+ return await renderScreen<SetupScreenResult>((controller) => React.createElement(SetupScreen, {
468
554
  controller,
469
555
  currentConfigSummary: summarizeConfigInspection(inspection).split('\n'),
470
556
  defaults: {
471
- agentApiUrl: inspection.config?.agentApiUrl ?? DEFAULT_AGENT_API_URL,
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
- apiKey: inspection.config?.apiKey ?? '',
565
+ forgejoOrganization: inspection.config?.forgejoOrganization ?? null,
566
+ forgejoUserRegistered: inspection.config?.forgejoUserRegistered ?? false,
474
567
  },
475
- hasExistingApiKey: Boolean(inspection.config?.apiKey),
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
- execute: async (input: {
490
- agentApiUrl: string
491
- cloneRoot: string
492
- apiKey: string
493
- createCloneRoot: boolean
494
- }) => await executeSetupFlow(input, deps),
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('Agent API', AGENT_API_BROWSER_HELP, { decorate: runtime.decorate }),
664
+ section('Authentication', DEVICE_FLOW_BROWSER_HELP, { decorate: runtime.decorate }),
552
665
  section('What setup updates', bulletList([
553
- 'Agent API target and origin URL',
666
+ 'Tela environment and service endpoints',
554
667
  'Clone root directory',
555
- 'API key',
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(prompts, config.cloneRoot)
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 environment.',
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: 'API key', value: redactApiKey(config.apiKey) },
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('Agent API', AGENT_API_BROWSER_HELP, { decorate: runtime.decorate }),
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 barkSession = createBarkSession(deps, options, 'Sync')
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, options, barkSession)
990
+ : await ensureConfigForCommand(deps)
856
991
  const availableRepositories = config ? listRepositories(config.cloneRoot) : undefined
857
992
 
858
- return await renderCommandScreen<SyncCommandResult>((controller) => React.createElement(SyncScreen, {
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({ prompts })
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 barkSession = createBarkSession(deps, options, 'Push')
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 renderCommandScreen<PushCommandResult>((controller) => React.createElement(PushScreen, {
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, options)
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 barkSession = createBarkSession(deps, options, 'Feedback')
1508
- await barkSession.playOnce()
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 renderCommandScreen<{
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 renderCommandScreen<void>((controller) => React.createElement(FeedbackClaudeHandoffScreen, {
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 renderCommandScreen<void>((controller) => React.createElement(FeedbackOutcomeScreen, {
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 renderCommandScreen<void>((controller) => React.createElement(RunChatHandoffScreen, {
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: process.stdin.isTTY,
1660
- stdoutIsTTY: process.stdout.isTTY,
1791
+ stdinIsTTY: getStdinIsTTY(deps),
1792
+ stdoutIsTTY: getStdoutIsTTY(deps),
1661
1793
  verbose: options.verbose,
1662
1794
  })
1663
1795
  }
1664
1796
  else {
1665
- result = await renderCommandScreen<RunCommandResult>((controller) => React.createElement(RunScreen, {
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: process.stdin.isTTY,
1681
- stdoutIsTTY: process.stdout.isTTY,
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: process.stdin.isTTY,
1702
- stdoutIsTTY: process.stdout.isTTY,
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 Ink startup')
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 choose the Agent API target')
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 origin reachability'),
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 origin reachability'),
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, {