@meistrari/tela-cli 0.1.1 → 0.3.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
@@ -2,11 +2,6 @@ import { existsSync, mkdirSync, statSync } from 'node:fs'
2
2
  import { basename, join } from 'node:path'
3
3
  import React from 'react'
4
4
  import { Command } from 'commander'
5
- import {
6
- createInkBarkSession,
7
- shouldShowBarkIntro,
8
- type BarkIntroSession,
9
- } from './ink/bark.tsx'
10
5
  import {
11
6
  FeedbackClaudeHandoffScreen,
12
7
  FeedbackOutcomeScreen,
@@ -14,9 +9,11 @@ import {
14
9
  } from './ink/screens/feedback-screen.tsx'
15
10
  import { PushScreen } from './ink/screens/push-screen.tsx'
16
11
  import { RunChatHandoffScreen, RunScreen } from './ink/screens/run-screen.tsx'
17
- import { SetupScreen, type SetupScreenResult } from './ink/screens/setup-screen.tsx'
12
+ import { SetupScreen, type SetupScreenResult } from './ink/screens/setup/setup-screen.tsx'
18
13
  import { SyncScreen, type SyncProgressItem } from './ink/screens/sync-screen.tsx'
19
14
  import { renderCommandScreen } from './ink/runtime.tsx'
15
+ import { listOrganizations } from './agent-api.ts'
16
+ import { authenticateWithDeviceFlow, type SetupAuthEvent } from './auth.ts'
20
17
  import {
21
18
  checkAgentApiReachability,
22
19
  getAgentApiOriginCheckStatus,
@@ -69,26 +66,39 @@ import {
69
66
  import { getDoctorReport, renderDoctorReport } from './commands/doctor.ts'
70
67
  import { getStatusReport, renderStatusReport } from './commands/status.ts'
71
68
  import {
72
- AGENT_API_BROWSER_HELP,
73
- DEFAULT_AGENT_API_URL,
69
+ DEVICE_FLOW_BROWSER_HELP,
74
70
  ensureConfigInteractive,
71
+ ENVIRONMENT_PRESET_OPTIONS,
72
+ getPreferredForgejoOrganization,
75
73
  getDefaultCloneRoot,
76
74
  inspectSavedConfig,
77
- redactApiKey,
75
+ redactSecret,
76
+ resolveEnvironmentConfigForPreset,
78
77
  saveConfig,
79
78
  validateAgentApiUrl,
80
- validateApiKey,
79
+ validateApplicationId,
80
+ validateAuthApiUrl,
81
+ validateAuthDashboardUrl,
81
82
  validateCloneRoot,
83
+ validateTelaApiUrl,
82
84
  setupConfigInteractive,
83
85
  type ConfigInspection,
86
+ type SetupConfigInput,
84
87
  } from './config.ts'
85
88
  import { findRepositoryRoot, getDiffStatAgainstHead } from './git.ts'
89
+ import {
90
+ ensureForgejoUserAccess,
91
+ listDiscoveredSshPublicKeys,
92
+ type SelectedSshPublicKey,
93
+ } from './forgejo.ts'
86
94
  import { commandExists, runInteractiveCommand, runStreamingCommand } from './process.ts'
87
95
  import { CommandRuntime } from './runtime.ts'
88
96
  import type {
89
97
  BatchSyncFailure,
90
98
  SyncResult,
99
+ TelaCliAuthState,
91
100
  TelaCliConfig,
101
+ TelaCliForgejoOrganization,
92
102
  } from './types.ts'
93
103
  import {
94
104
  bulletList,
@@ -109,6 +119,7 @@ function promptValue<T>(value: T | symbol): T {
109
119
  }
110
120
 
111
121
  interface CliDeps {
122
+ authenticateWithDeviceFlow?: typeof authenticateWithDeviceFlow
112
123
  prompts?: PromptModule
113
124
  ensureConfigInteractive?: typeof ensureConfigInteractive
114
125
  setupConfigInteractive?: typeof setupConfigInteractive
@@ -128,19 +139,20 @@ interface CliDeps {
128
139
  getDiffStatAgainstHead?: typeof getDiffStatAgainstHead
129
140
  getStatusReport?: typeof getStatusReport
130
141
  getDoctorReport?: typeof getDoctorReport
131
- createInkBarkSession?: typeof createInkBarkSession
142
+ renderCommandScreen?: typeof renderCommandScreen
132
143
  commandExists?: typeof commandExists
133
144
  runInteractiveCommand?: typeof runInteractiveCommand
134
145
  runStreamingCommand?: typeof runStreamingCommand
135
146
  fetchFn?: typeof fetch
136
147
  writeStdout?: (message: string) => void
137
148
  writeStderr?: (message: string) => void
149
+ stdinIsTTY?: boolean
150
+ stdoutIsTTY?: boolean
138
151
  }
139
152
 
140
153
  interface GlobalCliOptions {
141
154
  json?: boolean
142
155
  verbose?: boolean
143
- silentBark?: boolean
144
156
  }
145
157
 
146
158
  interface SyncCommandOptions extends GlobalCliOptions {
@@ -190,16 +202,24 @@ function renderOptionsForStdout(): { decorate: boolean } {
190
202
  return { decorate: Boolean(process.stdout.isTTY) }
191
203
  }
192
204
 
193
- function isInteractiveInkSession(options: GlobalCliOptions = {}): boolean {
194
- return !options.json && Boolean(process.stdout.isTTY && process.stdin.isTTY)
205
+ function getStdinIsTTY(deps: CliDeps): boolean {
206
+ return deps.stdinIsTTY ?? Boolean(process.stdin.isTTY)
195
207
  }
196
208
 
197
- function resolveRunSurface(options: RunCommandOptions = {}): 'plain' | 'viewer' | 'json' {
209
+ function getStdoutIsTTY(deps: CliDeps): boolean {
210
+ return deps.stdoutIsTTY ?? Boolean(process.stdout.isTTY)
211
+ }
212
+
213
+ function isInteractiveInkSession(deps: CliDeps, options: GlobalCliOptions = {}): boolean {
214
+ return !options.json && getStdoutIsTTY(deps) && getStdinIsTTY(deps)
215
+ }
216
+
217
+ function resolveRunSurface(deps: CliDeps, options: RunCommandOptions = {}): 'plain' | 'viewer' | 'json' {
198
218
  if (options.json) {
199
219
  return 'json'
200
220
  }
201
221
 
202
- if (options.plain || !isInteractiveInkSession(options)) {
222
+ if (options.plain || !isInteractiveInkSession(deps, options)) {
203
223
  return 'plain'
204
224
  }
205
225
 
@@ -230,6 +250,10 @@ function createRuntime(deps: CliDeps, options: GlobalCliOptions = {}): CommandRu
230
250
  })
231
251
  }
232
252
 
253
+ function getRenderCommandScreen(deps: CliDeps): typeof renderCommandScreen {
254
+ return deps.renderCommandScreen ?? renderCommandScreen
255
+ }
256
+
233
257
  function renderExamples(command: CommandMetadata): string {
234
258
  return command.examples
235
259
  .map(example => `${example.command.padEnd(36, ' ')} ${example.description}`)
@@ -261,7 +285,6 @@ function renderRootHelp(program: Command): string {
261
285
  section('Global flags', table(
262
286
  ['Flag', 'Description'],
263
287
  [
264
- ['--silent-bark', 'Disable bark audio during interactive Ink startup'],
265
288
  ['--verbose', 'Include raw evidence for failures where available'],
266
289
  ['-h, --help', 'Show command help'],
267
290
  ],
@@ -312,25 +335,6 @@ function installCustomHelp(program: Command): void {
312
335
  })
313
336
  }
314
337
 
315
- function createBarkSession(
316
- deps: CliDeps,
317
- options: GlobalCliOptions,
318
- commandTitle: string,
319
- ): BarkIntroSession {
320
- const createSession = deps.createInkBarkSession ?? createInkBarkSession
321
- return createSession({
322
- commandTitle,
323
- enabled: shouldShowBarkIntro({
324
- json: options.json,
325
- stdinIsTTY: process.stdin.isTTY,
326
- stdoutIsTTY: process.stdout.isTTY,
327
- }),
328
- silentBark: options.silentBark,
329
- env: process.env,
330
- platform: process.platform,
331
- })
332
- }
333
-
334
338
  function emitBootstrap(runtime: CommandRuntime, title: string, summary: string): void {
335
339
  runtime.emit(bootstrapPane(title, summary, { decorate: runtime.decorate }))
336
340
  }
@@ -353,6 +357,7 @@ function summarizeConfigInspection(inspection: ConfigInspection): string {
353
357
  return kvList([
354
358
  { label: 'Config path', value: inspection.path },
355
359
  { label: 'Status', value: 'missing' },
360
+ { label: 'Environment', value: 'production' },
356
361
  { label: 'Clone root', value: getDefaultCloneRoot() },
357
362
  ])
358
363
  }
@@ -367,9 +372,15 @@ function summarizeConfigInspection(inspection: ConfigInspection): string {
367
372
 
368
373
  return kvList([
369
374
  { label: 'Config path', value: inspection.path },
375
+ { label: 'Environment', value: inspection.config.environment },
370
376
  { label: 'Agent API origin', value: inspection.config.agentApiUrl },
377
+ { label: 'Auth API URL', value: inspection.config.authApiUrl },
378
+ { label: 'Tela API URL', value: inspection.config.telaApiUrl },
371
379
  { label: 'Clone root', value: inspection.config.cloneRoot },
372
- { label: 'API key', value: redactApiKey(inspection.config.apiKey) },
380
+ { label: 'User', value: inspection.config.auth?.user.email ?? '<signed out>' },
381
+ { label: 'Workspace', value: inspection.config.auth?.workspace.title ?? '<signed out>' },
382
+ { label: 'Organization', value: inspection.config.forgejoOrganization?.name ?? '<not selected>' },
383
+ { label: 'Forgejo user', value: inspection.config.forgejoUserRegistered ? 'registered' : 'pending' },
373
384
  ])
374
385
  }
375
386
 
@@ -377,7 +388,7 @@ function buildSetupChecksRows(checks: Array<{ label: string, status: string, det
377
388
  return checks.map(check => [check.label, check.status, check.detail])
378
389
  }
379
390
 
380
- function ensureCloneRootReady(prompts: PromptModule, cloneRoot: string): { created: boolean, warning?: string } {
391
+ function ensureCloneRootReady(cloneRoot: string): { created: boolean, warning?: string } {
381
392
  if (!existsSync(cloneRoot)) {
382
393
  return { created: false }
383
394
  }
@@ -397,30 +408,51 @@ function ensureCloneRootReady(prompts: PromptModule, cloneRoot: string): { creat
397
408
  }
398
409
 
399
410
  async function executeSetupFlow(
400
- input: {
401
- agentApiUrl: string
402
- cloneRoot: string
403
- apiKey: string
411
+ input: SetupConfigInput & {
404
412
  createCloneRoot: boolean
413
+ sshKey: SelectedSshPublicKey
414
+ auth: TelaCliAuthState
415
+ organization: TelaCliForgejoOrganization
405
416
  },
406
417
  deps: CliDeps = {},
418
+ onEvent?: (event: Pick<SetupAuthEvent, 'message' | 'title'>) => void,
407
419
  ): Promise<SetupScreenResult> {
408
420
  const commandExistsFn = deps.commandExists ?? commandExists
409
421
  const fetchFn = deps.fetchFn ?? fetch
410
- const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
411
- const config = {
422
+
423
+ const baseConfig: TelaCliConfig = {
424
+ environment: input.environment,
412
425
  agentApiUrl: input.agentApiUrl,
426
+ authApiUrl: input.authApiUrl,
427
+ authDashboardUrl: input.authDashboardUrl,
428
+ telaApiUrl: input.telaApiUrl,
413
429
  cloneRoot: input.cloneRoot,
414
- apiKey: input.apiKey.trim() || inspection.config?.apiKey || '',
415
- }
430
+ requesterApplicationId: input.requesterApplicationId,
431
+ targetApplicationId: input.targetApplicationId,
432
+ auth: input.auth,
433
+ forgejoOrganization: input.organization,
434
+ forgejoUserRegistered: false,
435
+ }
436
+ const forgejoProvision = await ensureForgejoUserAccess({
437
+ config: baseConfig,
438
+ organization: input.organization,
439
+ sshKey: input.sshKey,
440
+ fetchFn,
441
+ onProgress: (message, title) => {
442
+ onEvent?.({
443
+ title,
444
+ message,
445
+ })
446
+ },
447
+ saveConfig,
448
+ })
449
+ const config = forgejoProvision.config
416
450
 
417
451
  const cloneRootExists = existsSync(config.cloneRoot)
418
452
  if (!cloneRootExists && input.createCloneRoot) {
419
453
  mkdirSync(config.cloneRoot, { recursive: true, mode: 0o755 })
420
454
  }
421
455
 
422
- saveConfig(config)
423
-
424
456
  const [gitAvailable, claudeAvailable, reachability] = await Promise.all([
425
457
  commandExistsFn('git'),
426
458
  commandExistsFn('claude'),
@@ -434,6 +466,9 @@ async function executeSetupFlow(
434
466
  getAgentApiOriginWarning(reachability),
435
467
  ].filter((warning): warning is string => Boolean(warning))
436
468
  const checks = [
469
+ { label: 'auth', status: 'authenticated', detail: `${input.auth.user.email} @ ${input.auth.workspace.title}` },
470
+ { label: 'organization', status: 'selected', detail: input.organization.name },
471
+ { label: 'forgejo-user', status: forgejoProvision.response.userCreated ? 'created' : 'ensured', detail: forgejoProvision.detail },
437
472
  { label: 'clone-root', status: existsSync(config.cloneRoot) ? 'ready' : 'missing', detail: config.cloneRoot },
438
473
  { label: 'git', status: gitAvailable ? 'available' : 'missing', detail: gitAvailable ? 'ready' : 'install git' },
439
474
  { label: 'claude', status: claudeAvailable ? 'available' : 'missing', detail: claudeAvailable ? 'ready' : 'install Claude CLI' },
@@ -445,7 +480,7 @@ async function executeSetupFlow(
445
480
  config,
446
481
  warnings,
447
482
  nextActions: [
448
- 'Run `tela status` to review the saved environment.',
483
+ 'Run `tela status` to review the saved workspace session.',
449
484
  existsSync(config.cloneRoot)
450
485
  ? 'Run `tela sync <repo>` or `tela sync --current` when you need a checkout.'
451
486
  : 'Create the clone root or rerun setup before the first repository sync.',
@@ -457,25 +492,56 @@ async function executeSetupFlow(
457
492
  }
458
493
  }
459
494
 
495
+ function createMissingSetupOrganizationsError(workspaceTitle: string): ReturnType<typeof createUserFacingError> {
496
+ return createUserFacingError({
497
+ code: 'workspace_organization_required',
498
+ summary: 'No Forgejo organizations are registered for this workspace.',
499
+ details: `Tela authenticated successfully for ${workspaceTitle}, but the workspace does not have any registered organizations yet.`,
500
+ fixes: [
501
+ 'Ask the Tela team to register at least one organization for this workspace, then rerun `tela setup`.',
502
+ ],
503
+ })
504
+ }
505
+
460
506
  async function runSetupInk(
461
507
  deps: CliDeps = {},
462
- options: GlobalCliOptions = {},
463
- barkSession = createBarkSession(deps, options, 'Setup'),
464
508
  ): Promise<SetupScreenResult> {
509
+ const renderScreen = getRenderCommandScreen(deps)
465
510
  const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
466
- await barkSession.playOnce()
467
- return await renderCommandScreen<SetupScreenResult>((controller) => React.createElement(SetupScreen, {
511
+ const defaultEnvironment = inspection.config?.environment ?? 'production'
512
+ const defaultEnvironmentConfig = defaultEnvironment === 'custom'
513
+ ? {
514
+ environment: 'custom' as const,
515
+ agentApiUrl: inspection.config?.agentApiUrl ?? '',
516
+ authApiUrl: inspection.config?.authApiUrl ?? '',
517
+ authDashboardUrl: inspection.config?.authDashboardUrl ?? '',
518
+ telaApiUrl: inspection.config?.telaApiUrl ?? '',
519
+ requesterApplicationId: inspection.config?.requesterApplicationId ?? '',
520
+ targetApplicationId: inspection.config?.targetApplicationId ?? '',
521
+ }
522
+ : resolveEnvironmentConfigForPreset(defaultEnvironment)
523
+ return await renderScreen<SetupScreenResult>((controller) => React.createElement(SetupScreen, {
468
524
  controller,
469
525
  currentConfigSummary: summarizeConfigInspection(inspection).split('\n'),
470
526
  defaults: {
471
- agentApiUrl: inspection.config?.agentApiUrl ?? DEFAULT_AGENT_API_URL,
527
+ environment: defaultEnvironment,
528
+ agentApiUrl: inspection.config?.agentApiUrl ?? defaultEnvironmentConfig.agentApiUrl,
529
+ authApiUrl: inspection.config?.authApiUrl ?? defaultEnvironmentConfig.authApiUrl,
530
+ authDashboardUrl: inspection.config?.authDashboardUrl ?? defaultEnvironmentConfig.authDashboardUrl,
531
+ telaApiUrl: inspection.config?.telaApiUrl ?? defaultEnvironmentConfig.telaApiUrl,
532
+ requesterApplicationId: inspection.config?.requesterApplicationId ?? defaultEnvironmentConfig.requesterApplicationId,
533
+ targetApplicationId: inspection.config?.targetApplicationId ?? defaultEnvironmentConfig.targetApplicationId,
472
534
  cloneRoot: inspection.config?.cloneRoot ?? getDefaultCloneRoot(),
473
- apiKey: inspection.config?.apiKey ?? '',
535
+ forgejoOrganization: inspection.config?.forgejoOrganization ?? null,
536
+ forgejoUserRegistered: inspection.config?.forgejoUserRegistered ?? false,
474
537
  },
475
- hasExistingApiKey: Boolean(inspection.config?.apiKey),
538
+ sshPublicKeys: listDiscoveredSshPublicKeys(),
476
539
  validateAgentApiUrl,
540
+ validateAuthApiUrl,
541
+ validateAuthDashboardUrl,
542
+ validateTelaApiUrl,
543
+ validateApplicationId,
477
544
  validateCloneRoot,
478
- validateApiKey,
479
545
  inspectCloneRoot: (path: string) => {
480
546
  if (!existsSync(path)) {
481
547
  return { exists: false, isDirectory: false }
@@ -486,42 +552,58 @@ async function runSetupInk(
486
552
  isDirectory: statSync(path).isDirectory(),
487
553
  }
488
554
  },
489
- execute: async (input: {
490
- agentApiUrl: string
491
- cloneRoot: string
492
- apiKey: string
493
- createCloneRoot: boolean
494
- }) => await executeSetupFlow(input, deps),
555
+ authenticate: async (input, onEvent) => await (deps.authenticateWithDeviceFlow ?? authenticateWithDeviceFlow)({
556
+ environment: input.environment,
557
+ authApiUrl: input.authApiUrl,
558
+ authDashboardUrl: input.authDashboardUrl,
559
+ requesterApplicationId: input.requesterApplicationId,
560
+ targetApplicationId: input.targetApplicationId,
561
+ }, {
562
+ onEvent,
563
+ }),
564
+ resolveOrganizations: async (input) => {
565
+ const config: TelaCliConfig = {
566
+ environment: input.environment,
567
+ agentApiUrl: input.agentApiUrl,
568
+ authApiUrl: input.authApiUrl,
569
+ authDashboardUrl: input.authDashboardUrl,
570
+ telaApiUrl: input.telaApiUrl,
571
+ cloneRoot: input.cloneRoot,
572
+ requesterApplicationId: input.requesterApplicationId,
573
+ targetApplicationId: input.targetApplicationId,
574
+ auth: input.auth,
575
+ forgejoOrganization: null,
576
+ forgejoUserRegistered: false,
577
+ }
578
+ const organizations = await listOrganizations(config, deps.fetchFn ?? fetch)
579
+ if (organizations.length === 0) {
580
+ throw createMissingSetupOrganizationsError(input.auth.workspace.title)
581
+ }
582
+
583
+ return {
584
+ organizations,
585
+ preferredOrganization: getPreferredForgejoOrganization(
586
+ inspection.config ?? null,
587
+ input.auth,
588
+ organizations,
589
+ ),
590
+ }
591
+ },
592
+ execute: async (input, onEvent) => await executeSetupFlow(input, deps, onEvent),
495
593
  }))
496
594
  }
497
595
 
498
596
  async function ensureConfigForCommand(
499
597
  deps: CliDeps,
500
- options: GlobalCliOptions,
501
- barkSession?: BarkIntroSession,
502
598
  ): Promise<TelaCliConfig> {
503
599
  const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
504
- if (inspection.valid && inspection.config) {
600
+ if (inspection.valid && inspection.config?.auth) {
505
601
  return inspection.config
506
602
  }
507
603
 
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
604
  throw createUserFacingError({
523
605
  code: 'config_required',
524
- summary: 'Tela CLI is not configured.',
606
+ summary: inspection.valid ? 'Tela CLI is signed out.' : 'Tela CLI is not configured.',
525
607
  fixes: [
526
608
  'Run `tela setup` in an interactive terminal first.',
527
609
  ],
@@ -532,8 +614,8 @@ export async function runSetup(
532
614
  deps: CliDeps = {},
533
615
  options: GlobalCliOptions = {},
534
616
  ): Promise<SetupCommandResult> {
535
- if (isInteractiveInkSession(options)) {
536
- return await runSetupInk(deps, options)
617
+ if (isInteractiveInkSession(deps, options)) {
618
+ return await runSetupInk(deps)
537
619
  }
538
620
 
539
621
  const prompts = getPromptModule(deps)
@@ -542,17 +624,20 @@ export async function runSetup(
542
624
  const inspect = deps.inspectSavedConfig ?? inspectSavedConfig
543
625
  const commandExistsFn = deps.commandExists ?? commandExists
544
626
  const fetchFn = deps.fetchFn ?? fetch
627
+ const authenticate = deps.authenticateWithDeviceFlow ?? authenticateWithDeviceFlow
545
628
  const inspection = inspect()
546
629
 
547
630
  prompts.intro('Tela CLI setup')
548
631
  emitBootstrap(runtime, 'Tela setup', 'Bootstrap the local Tela environment before the first sync or push.')
549
632
  runtime.emit(resultCard('Setup preflight', [
550
633
  section('Current config', summarizeConfigInspection(inspection), { decorate: runtime.decorate }),
551
- section('Agent API', AGENT_API_BROWSER_HELP, { decorate: runtime.decorate }),
634
+ section('Authentication', DEVICE_FLOW_BROWSER_HELP, { decorate: runtime.decorate }),
552
635
  section('What setup updates', bulletList([
553
- 'Agent API target and origin URL',
636
+ 'Tela environment and service endpoints',
554
637
  'Clone root directory',
555
- 'API key',
638
+ 'Workspace-backed Tela session',
639
+ 'Forgejo organization selection for the workspace',
640
+ 'Forgejo user access with your SSH public key',
556
641
  ]), { decorate: runtime.decorate }),
557
642
  ], { decorate: runtime.decorate }))
558
643
 
@@ -560,11 +645,22 @@ export async function runSetup(
560
645
  prompts,
561
646
  existingConfig: inspection.config ?? null,
562
647
  skipLifecycle: true,
648
+ authenticate: async (input, notify) => {
649
+ return await authenticate({
650
+ environment: input.environment,
651
+ authApiUrl: input.authApiUrl,
652
+ authDashboardUrl: input.authDashboardUrl,
653
+ requesterApplicationId: input.requesterApplicationId,
654
+ targetApplicationId: input.targetApplicationId,
655
+ }, {
656
+ onEvent: event => notify(event.message, event.title),
657
+ })
658
+ },
563
659
  })
564
660
 
565
661
  let cloneRootCreated = false
566
662
  let cloneRootWarning: string | undefined
567
- const cloneRootStatus = ensureCloneRootReady(prompts, config.cloneRoot)
663
+ const cloneRootStatus = ensureCloneRootReady(config.cloneRoot)
568
664
  cloneRootWarning = cloneRootStatus.warning
569
665
 
570
666
  if (!existsSync(config.cloneRoot)) {
@@ -599,7 +695,7 @@ export async function runSetup(
599
695
  ].filter((warning): warning is string => Boolean(warning))
600
696
 
601
697
  const nextSetupActions = [
602
- 'Run `tela status` to review the saved environment.',
698
+ 'Run `tela status` to review the saved workspace session.',
603
699
  cloneRootCreated
604
700
  ? 'Run `tela sync <repo>` to clone a repository into the new clone root.'
605
701
  : 'Run `tela sync <repo>` or `tela sync --current` when you need a checkout.',
@@ -610,14 +706,24 @@ export async function runSetup(
610
706
 
611
707
  runtime.emit(resultCard('Setup complete', [
612
708
  section('Saved config', kvList([
709
+ { label: 'Environment', value: config.environment },
613
710
  { label: 'Agent API origin', value: config.agentApiUrl },
711
+ { label: 'Auth API URL', value: config.authApiUrl },
712
+ { label: 'Tela API URL', value: config.telaApiUrl },
614
713
  { label: 'Clone root', value: config.cloneRoot },
615
- { label: 'API key', value: redactApiKey(config.apiKey) },
714
+ { label: 'User', value: config.auth?.user.email ?? '<missing>' },
715
+ { label: 'Workspace', value: config.auth?.workspace.title ?? '<missing>' },
716
+ { label: 'Organization', value: config.forgejoOrganization?.name ?? '<missing>' },
717
+ { label: 'Forgejo user', value: config.forgejoUserRegistered ? 'registered' : 'pending' },
718
+ { label: 'Session', value: config.auth ? `${redactSecret(config.auth.session.accessToken)} (expires ${new Date(config.auth.session.expiresAt).toISOString()})` : '<missing>' },
616
719
  ]), { decorate: runtime.decorate }),
617
- section('Agent API', AGENT_API_BROWSER_HELP, { decorate: runtime.decorate }),
720
+ section('Authentication', DEVICE_FLOW_BROWSER_HELP, { decorate: runtime.decorate }),
618
721
  section('Checks', table(
619
722
  ['Check', 'Status', 'Detail'],
620
723
  buildSetupChecksRows([
724
+ { label: 'auth', status: 'authenticated', detail: `${config.auth?.user.email ?? '<missing>'} @ ${config.auth?.workspace.title ?? '<missing>'}` },
725
+ { label: 'organization', status: config.forgejoOrganization ? 'selected' : 'missing', detail: config.forgejoOrganization?.name ?? 'not selected' },
726
+ { label: 'forgejo-user', status: config.forgejoUserRegistered ? 'registered' : 'pending', detail: config.forgejoUserRegistered ? 'SSH-key provisioning completed.' : 'SSH-key provisioning is pending.' },
621
727
  { label: 'clone-root', status: cloneRootCreated ? 'created' : (existsSync(config.cloneRoot) ? 'ready' : 'missing'), detail: config.cloneRoot },
622
728
  { label: 'git', status: gitAvailable ? 'available' : 'missing', detail: gitAvailable ? 'ready' : 'install git' },
623
729
  { label: 'claude', status: claudeAvailable ? 'available' : 'missing', detail: claudeAvailable ? 'ready' : 'install Claude CLI' },
@@ -841,8 +947,7 @@ async function runSyncInk(
841
947
  deps: CliDeps,
842
948
  options: SyncCommandOptions,
843
949
  ): Promise<SyncCommandResult> {
844
- const barkSession = createBarkSession(deps, options, 'Sync')
845
- await barkSession.playOnce()
950
+ const renderScreen = getRenderCommandScreen(deps)
846
951
  const findRepo = deps.findRepositoryRoot ?? findRepositoryRoot
847
952
  const listRepositories = deps.listCloneRootRepositories ?? listCloneRootRepositories
848
953
  const syncAll = deps.syncAllRepositories ?? syncAllRepositories
@@ -852,10 +957,10 @@ async function runSyncInk(
852
957
  const mode = resolveSyncMode(repository, options, repoRoot)
853
958
  const config = mode === 'current'
854
959
  ? undefined
855
- : await ensureConfigForCommand(deps, options, barkSession)
960
+ : await ensureConfigForCommand(deps)
856
961
  const availableRepositories = config ? listRepositories(config.cloneRoot) : undefined
857
962
 
858
- return await renderCommandScreen<SyncCommandResult>((controller) => React.createElement(SyncScreen, {
963
+ return await renderScreen<SyncCommandResult>((controller) => React.createElement(SyncScreen, {
859
964
  controller,
860
965
  mode,
861
966
  dryRun: options.dryRun,
@@ -942,7 +1047,7 @@ export async function runSync(
942
1047
  ): Promise<SyncCommandResult> {
943
1048
  validateSyncOptions(repository, options)
944
1049
 
945
- if (isInteractiveInkSession(options)) {
1050
+ if (isInteractiveInkSession(deps, options)) {
946
1051
  return await runSyncInk(repository, deps, options)
947
1052
  }
948
1053
 
@@ -959,7 +1064,7 @@ export async function runSync(
959
1064
 
960
1065
  const repoRoot = await findRepo(process.cwd())
961
1066
  const config = options.all || !repoRoot || Boolean(repository)
962
- ? await ensureConfig({ prompts })
1067
+ ? await ensureConfig()
963
1068
  : undefined
964
1069
 
965
1070
  if (options.all) {
@@ -1170,8 +1275,7 @@ async function runPushInk(
1170
1275
  deps: CliDeps,
1171
1276
  options: PushCommandOptions,
1172
1277
  ): Promise<PushCommandResult> {
1173
- const barkSession = createBarkSession(deps, options, 'Push')
1174
- await barkSession.playOnce()
1278
+ const renderScreen = getRenderCommandScreen(deps)
1175
1279
  const findRepo = deps.findRepositoryRoot ?? findRepositoryRoot
1176
1280
  const preview = deps.previewPush ?? previewPush
1177
1281
  const push = deps.pushRepository ?? pushRepository
@@ -1190,7 +1294,7 @@ async function runPushInk(
1190
1294
  : undefined
1191
1295
  const initialManualMessage = options.message?.trim() || undefined
1192
1296
 
1193
- return await renderCommandScreen<PushCommandResult>((controller) => React.createElement(PushScreen, {
1297
+ return await renderScreen<PushCommandResult>((controller) => React.createElement(PushScreen, {
1194
1298
  controller,
1195
1299
  initialMode,
1196
1300
  initialManualMessage,
@@ -1241,7 +1345,7 @@ export async function runPush(
1241
1345
  deps: CliDeps = {},
1242
1346
  options: PushCommandOptions = {},
1243
1347
  ): Promise<PushCommandResult> {
1244
- if (isInteractiveInkSession(options)) {
1348
+ if (isInteractiveInkSession(deps, options)) {
1245
1349
  return await runPushInk(deps, options)
1246
1350
  }
1247
1351
 
@@ -1446,7 +1550,7 @@ async function runFeedbackServiceFlow(
1446
1550
  deps: CliDeps,
1447
1551
  options: FeedbackCommandFlags & GlobalCliOptions,
1448
1552
  ): Promise<FeedbackRunResult> {
1449
- const config = await ensureConfigForCommand(deps, options)
1553
+ const config = await ensureConfigForCommand(deps)
1450
1554
  const loadFeedbackDataFn = deps.loadFeedbackData ?? loadFeedbackData
1451
1555
  const launchFeedbackClaudeFn = deps.launchFeedbackClaude ?? launchFeedbackClaude
1452
1556
  const commandExistsFn = deps.commandExists ?? commandExists
@@ -1504,9 +1608,8 @@ async function runFeedbackInk(
1504
1608
  deps: CliDeps,
1505
1609
  options: FeedbackCommandFlags & GlobalCliOptions,
1506
1610
  ): Promise<FeedbackRunResult> {
1507
- const barkSession = createBarkSession(deps, options, 'Feedback')
1508
- await barkSession.playOnce()
1509
- const config = await ensureConfigForCommand(deps, options, barkSession)
1611
+ const renderScreen = getRenderCommandScreen(deps)
1612
+ const config = await ensureConfigForCommand(deps)
1510
1613
  const loadFeedbackDataFn = deps.loadFeedbackData ?? loadFeedbackData
1511
1614
  const launchFeedbackClaudeFn = deps.launchFeedbackClaude ?? launchFeedbackClaude
1512
1615
  const commandExistsFn = deps.commandExists ?? commandExists
@@ -1517,7 +1620,7 @@ async function runFeedbackInk(
1517
1620
  config,
1518
1621
  })
1519
1622
 
1520
- const selection = await renderCommandScreen<{
1623
+ const selection = await renderScreen<{
1521
1624
  selectedFeedbacks: FeedbackData['entries']
1522
1625
  prompt?: string
1523
1626
  launchedClaude: boolean
@@ -1545,7 +1648,7 @@ async function runFeedbackInk(
1545
1648
  })
1546
1649
  }
1547
1650
 
1548
- await renderCommandScreen<void>((controller) => React.createElement(FeedbackClaudeHandoffScreen, {
1651
+ await renderScreen<void>((controller) => React.createElement(FeedbackClaudeHandoffScreen, {
1549
1652
  controller,
1550
1653
  repoRoot: data.repoRoot,
1551
1654
  commitHash: data.commitHash,
@@ -1559,7 +1662,7 @@ async function runFeedbackInk(
1559
1662
 
1560
1663
  const diffStat = await getDiffStatAgainstHeadFn(repoRoot)
1561
1664
 
1562
- await renderCommandScreen<void>((controller) => React.createElement(FeedbackOutcomeScreen, {
1665
+ await renderScreen<void>((controller) => React.createElement(FeedbackOutcomeScreen, {
1563
1666
  controller,
1564
1667
  commitHash: data.commitHash,
1565
1668
  selectionMode,
@@ -1594,7 +1697,7 @@ export async function runFeedback(
1594
1697
  })
1595
1698
  }
1596
1699
 
1597
- const interactive = isInteractiveInkSession(normalizedOptions)
1700
+ const interactive = isInteractiveInkSession(deps, normalizedOptions)
1598
1701
  if (!interactive) {
1599
1702
  emitBootstrap(runtime, 'Tela feedback', 'Review commit feedback and optionally hand selected fixes to Claude.')
1600
1703
  }
@@ -1625,9 +1728,10 @@ export async function runAgent(
1625
1728
  options: RunCommandOptions = {},
1626
1729
  ): Promise<RunCommandResult> {
1627
1730
  const runtime = createRuntime(deps, options)
1731
+ const renderScreen = getRenderCommandScreen(deps)
1628
1732
  const runAgentCommandFn = deps.runLocalAgentCommand ?? runLocalAgentCommand
1629
1733
  const prepareRunFn = deps.prepareLocalAgentRun ?? prepareLocalAgentRun
1630
- const surface = resolveRunSurface(options)
1734
+ const surface = resolveRunSurface(deps, options)
1631
1735
 
1632
1736
  let result: RunCommandResult
1633
1737
 
@@ -1636,11 +1740,9 @@ export async function runAgent(
1636
1740
  cwd: process.cwd(),
1637
1741
  commandExists: deps.commandExists,
1638
1742
  })
1639
- const barkSession = createBarkSession(deps, options, 'Run')
1640
- await barkSession.playOnce()
1641
1743
 
1642
1744
  if (prepared.config.isMultiturn) {
1643
- await renderCommandScreen<void>((controller) => React.createElement(RunChatHandoffScreen, {
1745
+ await renderScreen<void>((controller) => React.createElement(RunChatHandoffScreen, {
1644
1746
  controller,
1645
1747
  snapshot: createInitialRunSnapshot(prepared, 'chat'),
1646
1748
  }), {
@@ -1656,13 +1758,13 @@ export async function runAgent(
1656
1758
  runStreamingCommand: deps.runStreamingCommand,
1657
1759
  writeStdout: deps.writeStdout,
1658
1760
  writeStderr: deps.writeStderr,
1659
- stdinIsTTY: process.stdin.isTTY,
1660
- stdoutIsTTY: process.stdout.isTTY,
1761
+ stdinIsTTY: getStdinIsTTY(deps),
1762
+ stdoutIsTTY: getStdoutIsTTY(deps),
1661
1763
  verbose: options.verbose,
1662
1764
  })
1663
1765
  }
1664
1766
  else {
1665
- result = await renderCommandScreen<RunCommandResult>((controller) => React.createElement(RunScreen, {
1767
+ result = await renderScreen<RunCommandResult>((controller) => React.createElement(RunScreen, {
1666
1768
  controller,
1667
1769
  initialSnapshot: createInitialRunSnapshot(prepared, 'one-shot'),
1668
1770
  execute: async ({ onSnapshot, onControl }) => {
@@ -1677,8 +1779,8 @@ export async function runAgent(
1677
1779
  writeStderr: deps.writeStderr,
1678
1780
  onSnapshot,
1679
1781
  onControl,
1680
- stdinIsTTY: process.stdin.isTTY,
1681
- stdoutIsTTY: process.stdout.isTTY,
1782
+ stdinIsTTY: getStdinIsTTY(deps),
1783
+ stdoutIsTTY: getStdoutIsTTY(deps),
1682
1784
  verbose: options.verbose,
1683
1785
  })
1684
1786
  },
@@ -1698,8 +1800,8 @@ export async function runAgent(
1698
1800
  runStreamingCommand: deps.runStreamingCommand,
1699
1801
  writeStdout: deps.writeStdout,
1700
1802
  writeStderr: deps.writeStderr,
1701
- stdinIsTTY: process.stdin.isTTY,
1702
- stdoutIsTTY: process.stdout.isTTY,
1803
+ stdinIsTTY: getStdinIsTTY(deps),
1804
+ stdoutIsTTY: getStdoutIsTTY(deps),
1703
1805
  verbose: options.verbose,
1704
1806
  })
1705
1807
  }
@@ -1757,6 +1859,53 @@ export async function runDoctor(
1757
1859
  return report
1758
1860
  }
1759
1861
 
1862
+ export interface ListRepositoriesResult {
1863
+ cloneRoot: string
1864
+ repositories: string[]
1865
+ }
1866
+
1867
+ export async function runListRepositories(
1868
+ deps: CliDeps = {},
1869
+ options: GlobalCliOptions = {},
1870
+ ): Promise<ListRepositoriesResult> {
1871
+ const runtime = createRuntime(deps, options)
1872
+ const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
1873
+ const cloneRoot = inspection.config?.cloneRoot ?? getDefaultCloneRoot()
1874
+ const listRepos = deps.listCloneRootRepositories ?? listCloneRootRepositories
1875
+ const repositories = listRepos(cloneRoot)
1876
+ const result: ListRepositoriesResult = { cloneRoot, repositories }
1877
+
1878
+ if (options.json) {
1879
+ runtime.emitJson(result)
1880
+ return result
1881
+ }
1882
+
1883
+ emitBootstrap(runtime, 'Tela CLI', 'Workspace repositories under the configured clone root.')
1884
+ const decorate = runtime.decorate
1885
+ const body = repositories.length > 0
1886
+ ? section('Repositories', bulletList(repositories), { decorate })
1887
+ : section('Repositories', [
1888
+ `No repositories found under ${cloneRoot}.`,
1889
+ '',
1890
+ 'Next steps:',
1891
+ bulletList([
1892
+ 'Run `tela setup` to configure the clone root.',
1893
+ 'Run `tela sync <repo>` to clone a workspace repository.',
1894
+ ]),
1895
+ ], { decorate })
1896
+
1897
+ runtime.emit(joinBlocks([
1898
+ body,
1899
+ section('Clone root', cloneRoot, { decorate }),
1900
+ nextActions([
1901
+ 'Run `tela sync <repo>` to clone or refresh a repository.',
1902
+ 'Run `tela --help` to see all available commands.',
1903
+ ], { decorate }),
1904
+ ]))
1905
+
1906
+ return result
1907
+ }
1908
+
1760
1909
  function addStandardReadOptions(command: Command): Command {
1761
1910
  return command
1762
1911
  .option('--json', 'emit machine-readable JSON')
@@ -1765,16 +1914,23 @@ function addStandardReadOptions(command: Command): Command {
1765
1914
  export function createProgram(deps: CliDeps = {}): Command {
1766
1915
  const program = new Command()
1767
1916
  .name('tela')
1768
- .description('Tela CLI workspace helper')
1917
+ .description('Tela CLI workspace and auth helper')
1769
1918
  .showHelpAfterError()
1770
- .option('--silent-bark', 'disable bark audio during interactive Ink startup')
1771
1919
  .option('--verbose', 'include raw evidence on failures')
1772
1920
 
1773
1921
  installCustomHelp(program)
1774
1922
 
1923
+ addStandardReadOptions(program).action(async (commandOptions: GlobalCliOptions) => {
1924
+ const globalOptions = program.opts<GlobalCliOptions>()
1925
+ await runListRepositories(deps, {
1926
+ ...globalOptions,
1927
+ ...commandOptions,
1928
+ })
1929
+ })
1930
+
1775
1931
  program
1776
1932
  .command('setup')
1777
- .description('Configure Tela CLI settings and choose the Agent API target')
1933
+ .description('Configure Tela CLI settings and complete device-flow authentication')
1778
1934
  .action(async () => {
1779
1935
  const globalOptions = program.opts<GlobalCliOptions>()
1780
1936
  await runSetup(deps, globalOptions)
@@ -1842,7 +1998,7 @@ export function createProgram(deps: CliDeps = {}): Command {
1842
1998
  addStandardReadOptions(
1843
1999
  program
1844
2000
  .command('status')
1845
- .description('Show Tela config, repo state, tool availability, and Agent API origin reachability'),
2001
+ .description('Show Tela config, session state, repo state, tool availability, and Agent API reachability'),
1846
2002
  ).action(async (commandOptions: StatusCommandOptions) => {
1847
2003
  const globalOptions = program.opts<GlobalCliOptions>()
1848
2004
  await runStatus(deps, {
@@ -1855,7 +2011,7 @@ export function createProgram(deps: CliDeps = {}): Command {
1855
2011
  program
1856
2012
  .command('doctor')
1857
2013
  .alias('diag')
1858
- .description('Diagnose config, tools, filesystem permissions, repo health, and Agent API origin reachability'),
2014
+ .description('Diagnose config, auth session, tools, filesystem permissions, repo health, and Agent API reachability'),
1859
2015
  ).action(async (commandOptions: DoctorCommandOptions) => {
1860
2016
  const globalOptions = program.opts<GlobalCliOptions>()
1861
2017
  await runDoctor(deps, {