@meistrari/tela-cli 0.4.0 → 0.5.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
@@ -12,9 +12,9 @@ import { RunChatHandoffScreen, RunScreen } from './ink/screens/run-screen.tsx'
12
12
  import { SetupScreen, type SetupScreenResult } from './ink/screens/setup/setup-screen.tsx'
13
13
  import { SyncScreen, type SyncProgressItem } from './ink/screens/sync-screen.tsx'
14
14
  import { renderCommandScreen } from './ink/runtime.tsx'
15
- import { listOrganizations } from './agent-api.ts'
15
+ import { listAvailableRepositories, listOrganizations } from './agent-api.ts'
16
16
  import { authenticateWithDeviceFlow, type SetupAuthEvent } from './auth.ts'
17
- import { createUserFacingError } from './errors.ts'
17
+ import { createUserFacingError, toUserFacingError } from './errors.ts'
18
18
  import {
19
19
  getCommandMetadata,
20
20
  listCommandMetadata,
@@ -82,6 +82,7 @@ import {
82
82
  } from './config.ts'
83
83
  import { findRepositoryRoot, getDiffStatAgainstHead } from './git.ts'
84
84
  import {
85
+ createGitSshKey,
85
86
  ensureForgejoUserAccess,
86
87
  listDiscoveredSshPublicKeys,
87
88
  type SelectedSshPublicKey,
@@ -89,6 +90,7 @@ import {
89
90
  import { commandExists, runInteractiveCommand, runStreamingCommand } from './process.ts'
90
91
  import { CommandRuntime } from './runtime.ts'
91
92
  import type {
93
+ AvailableRepositorySummary,
92
94
  BatchSyncFailure,
93
95
  SyncResult,
94
96
  TelaCliAuthState,
@@ -124,6 +126,7 @@ interface CliDeps {
124
126
  syncAllRepositories?: typeof syncAllRepositories
125
127
  syncCurrentRepository?: typeof syncCurrentRepository
126
128
  listCloneRootRepositories?: typeof listCloneRootRepositories
129
+ listAvailableRepositories?: typeof listAvailableRepositories
127
130
  previewPush?: typeof previewPush
128
131
  pushRepository?: typeof pushRepository
129
132
  runLocalAgentCommand?: typeof runLocalAgentCommand
@@ -265,11 +268,13 @@ function renderRootHelp(program: Command): string {
265
268
  return joinBlocks([
266
269
  bootstrapPane('Tela CLI', [
267
270
  'One-shot workflows for setting up Tela, syncing repositories, running local agents, preparing pushes, and applying commit feedback.',
271
+ 'Run `tela` with no subcommand to list workspace repositories available to sync.',
268
272
  '',
269
273
  'First successful path:',
270
274
  stepList([
271
275
  'tela setup',
272
276
  'tela status',
277
+ 'tela',
273
278
  'tela sync <repo> or tela sync --current',
274
279
  'tela run',
275
280
  'tela push',
@@ -376,6 +381,7 @@ function summarizeConfigInspection(inspection: ConfigInspection): string {
376
381
  { label: 'Workspace', value: inspection.config.auth?.workspace.title ?? '<signed out>' },
377
382
  { label: 'Organization', value: inspection.config.forgejoOrganization?.name ?? '<not selected>' },
378
383
  { label: 'Forgejo user', value: inspection.config.forgejoUserRegistered ? 'registered' : 'pending' },
384
+ { label: 'Git SSH key', value: inspection.config.gitSshKey ? `${inspection.config.gitSshKey.title} (${inspection.config.gitSshKey.privateKeyPath})` : '<missing>' },
379
385
  ])
380
386
  }
381
387
 
@@ -406,6 +412,7 @@ async function executeSetupFlow(
406
412
  input: SetupConfigInput & {
407
413
  createCloneRoot: boolean
408
414
  sshKey: SelectedSshPublicKey
415
+ sshPrivateKeyPath: string
409
416
  auth: TelaCliAuthState
410
417
  organization: TelaCliForgejoOrganization
411
418
  },
@@ -427,6 +434,7 @@ async function executeSetupFlow(
427
434
  auth: input.auth,
428
435
  forgejoOrganization: input.organization,
429
436
  forgejoUserRegistered: false,
437
+ gitSshKey: createGitSshKey(input.sshKey, input.sshPrivateKeyPath),
430
438
  }
431
439
  const forgejoProvision = await ensureForgejoUserAccess({
432
440
  config: baseConfig,
@@ -567,6 +575,7 @@ async function runSetupInk(
567
575
  auth: input.auth,
568
576
  forgejoOrganization: null,
569
577
  forgejoUserRegistered: false,
578
+ gitSshKey: inspection.config?.gitSshKey ?? null,
570
579
  }
571
580
  const organizations = await listOrganizations(config, deps.fetchFn ?? fetch)
572
581
  if (organizations.length === 0) {
@@ -705,6 +714,7 @@ export async function runSetup(
705
714
  { label: 'Workspace', value: config.auth?.workspace.title ?? '<missing>' },
706
715
  { label: 'Organization', value: config.forgejoOrganization?.name ?? '<missing>' },
707
716
  { label: 'Forgejo user', value: config.forgejoUserRegistered ? 'registered' : 'pending' },
717
+ { label: 'Git SSH key', value: config.gitSshKey ? `${config.gitSshKey.title} (${config.gitSshKey.privateKeyPath})` : '<missing>' },
708
718
  { label: 'Session', value: config.auth ? `${redactSecret(config.auth.session.accessToken)} (expires ${new Date(config.auth.session.expiresAt).toISOString()})` : '<missing>' },
709
719
  ]), { decorate: runtime.decorate }),
710
720
  section('Authentication', DEVICE_FLOW_BROWSER_HELP, { decorate: runtime.decorate }),
@@ -714,6 +724,7 @@ export async function runSetup(
714
724
  { label: 'auth', status: 'authenticated', detail: `${config.auth?.user.email ?? '<missing>'} @ ${config.auth?.workspace.title ?? '<missing>'}` },
715
725
  { label: 'organization', status: config.forgejoOrganization ? 'selected' : 'missing', detail: config.forgejoOrganization?.name ?? 'not selected' },
716
726
  { label: 'forgejo-user', status: config.forgejoUserRegistered ? 'registered' : 'pending', detail: config.forgejoUserRegistered ? 'SSH-key provisioning completed.' : 'SSH-key provisioning is pending.' },
727
+ { label: 'git-ssh-key', status: config.gitSshKey ? 'configured' : 'missing', detail: config.gitSshKey?.privateKeyPath ?? 'rerun setup to choose an SSH key' },
717
728
  { label: 'clone-root', status: cloneRootCreated ? 'created' : (existsSync(config.cloneRoot) ? 'ready' : 'missing'), detail: config.cloneRoot },
718
729
  { label: 'git', status: gitAvailable ? 'available' : 'missing', detail: gitAvailable ? 'ready' : 'install git' },
719
730
  { label: 'claude', status: claudeAvailable ? 'available' : 'missing', detail: claudeAvailable ? 'ready' : 'install Claude CLI' },
@@ -755,6 +766,26 @@ function syncCategoryFixes(category: SyncFailureCategory): string[] {
755
766
  }
756
767
  }
757
768
 
769
+ function buildSyncFailureError(error: unknown): ReturnType<typeof createUserFacingError> {
770
+ const category = categorizeSyncError(error)
771
+ const userError = toUserFacingError(error)
772
+ const stderrDetails = userError.evidence
773
+ .find(line => line.startsWith('stderr: '))
774
+ ?.replace(/^stderr: /, '')
775
+ const stdoutDetails = userError.evidence
776
+ .find(line => line.startsWith('stdout: '))
777
+ ?.replace(/^stdout: /, '')
778
+
779
+ return createUserFacingError({
780
+ code: `sync_${category}`,
781
+ summary: error instanceof Error ? error.message : String(error),
782
+ details: stderrDetails || stdoutDetails || userError.details,
783
+ fixes: syncCategoryFixes(category),
784
+ evidence: userError.evidence,
785
+ cause: error,
786
+ })
787
+ }
788
+
758
789
  function renderSyncPlan(input: {
759
790
  mode: SyncCommandResult['mode']
760
791
  target: string
@@ -1018,13 +1049,7 @@ async function runSyncInk(
1018
1049
  }
1019
1050
  }
1020
1051
  catch (error) {
1021
- const category = categorizeSyncError(error)
1022
- throw createUserFacingError({
1023
- code: `sync_${category}`,
1024
- summary: error instanceof Error ? error.message : String(error),
1025
- fixes: syncCategoryFixes(category),
1026
- cause: error,
1027
- })
1052
+ throw buildSyncFailureError(error)
1028
1053
  }
1029
1054
  },
1030
1055
  }))
@@ -1196,13 +1221,7 @@ export async function runSync(
1196
1221
  return result
1197
1222
  }
1198
1223
  catch (error) {
1199
- const category = categorizeSyncError(error)
1200
- throw createUserFacingError({
1201
- code: `sync_${category}`,
1202
- summary: error instanceof Error ? error.message : String(error),
1203
- fixes: syncCategoryFixes(category),
1204
- cause: error,
1205
- })
1224
+ throw buildSyncFailureError(error)
1206
1225
  }
1207
1226
  }
1208
1227
 
@@ -1850,8 +1869,10 @@ export async function runDoctor(
1850
1869
  }
1851
1870
 
1852
1871
  export interface ListRepositoriesResult {
1853
- cloneRoot: string
1854
- repositories: string[]
1872
+ success: boolean
1873
+ repositories: AvailableRepositorySummary[]
1874
+ message?: string
1875
+ nextActions?: string[]
1855
1876
  }
1856
1877
 
1857
1878
  export async function runListRepositories(
@@ -1860,35 +1881,76 @@ export async function runListRepositories(
1860
1881
  ): Promise<ListRepositoriesResult> {
1861
1882
  const runtime = createRuntime(deps, options)
1862
1883
  const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
1863
- const cloneRoot = inspection.config?.cloneRoot ?? getDefaultCloneRoot()
1864
- const listRepos = deps.listCloneRootRepositories ?? listCloneRootRepositories
1865
- const repositories = listRepos(cloneRoot)
1866
- const result: ListRepositoriesResult = { cloneRoot, repositories }
1884
+ const decorate = runtime.decorate
1885
+ const setupRequiredNextActions = [
1886
+ 'Run `tela setup` to configure Tela CLI and sign in.',
1887
+ 'Run `tela --help` to see all available commands.',
1888
+ ]
1889
+ const setupRequiredMessage = 'Setup and sign-in are required before Tela can list repositories available to sync.'
1890
+
1891
+ if (!inspection.valid || !inspection.config?.auth) {
1892
+ const result: ListRepositoriesResult = {
1893
+ success: false,
1894
+ repositories: [],
1895
+ message: setupRequiredMessage,
1896
+ nextActions: setupRequiredNextActions,
1897
+ }
1898
+
1899
+ if (options.json) {
1900
+ runtime.emitJson(result)
1901
+ return result
1902
+ }
1903
+
1904
+ emitBootstrap(runtime, 'Tela CLI', 'Workspace repositories available to sync.')
1905
+ runtime.emit(joinBlocks([
1906
+ section('Repositories', [
1907
+ setupRequiredMessage,
1908
+ '',
1909
+ 'These repositories will appear here after setup is complete.',
1910
+ ], { decorate }),
1911
+ nextActions(setupRequiredNextActions, { decorate }),
1912
+ ]))
1913
+
1914
+ return result
1915
+ }
1916
+
1917
+ const listRepos = deps.listAvailableRepositories ?? listAvailableRepositories
1918
+ const repositories = await listRepos(inspection.config, deps.fetchFn ?? fetch)
1919
+ const result: ListRepositoriesResult = {
1920
+ success: true,
1921
+ repositories,
1922
+ }
1867
1923
 
1868
1924
  if (options.json) {
1869
1925
  runtime.emitJson(result)
1870
1926
  return result
1871
1927
  }
1872
1928
 
1873
- emitBootstrap(runtime, 'Tela CLI', 'Workspace repositories under the configured clone root.')
1874
- const decorate = runtime.decorate
1929
+ emitBootstrap(runtime, 'Tela CLI', 'Workspace repositories available to sync.')
1875
1930
  const body = repositories.length > 0
1876
- ? section('Repositories', bulletList(repositories), { decorate })
1931
+ ? section(
1932
+ 'Repositories',
1933
+ table(
1934
+ ['Organization', 'Repository', 'Sync'],
1935
+ repositories.map(repository => [
1936
+ repository.organization.name,
1937
+ repository.agentRepo.repository,
1938
+ repository.syncStatus,
1939
+ ]),
1940
+ { decorate },
1941
+ ),
1942
+ { decorate },
1943
+ )
1877
1944
  : section('Repositories', [
1878
- `No repositories found under ${cloneRoot}.`,
1945
+ 'No workspace repositories are currently registered for this workspace.',
1879
1946
  '',
1880
- 'Next steps:',
1881
- bulletList([
1882
- 'Run `tela setup` to configure the clone root.',
1883
- 'Run `tela sync <repo>` to clone a workspace repository.',
1884
- ]),
1947
+ 'There is nothing to sync yet.',
1885
1948
  ], { decorate })
1886
1949
 
1887
1950
  runtime.emit(joinBlocks([
1888
1951
  body,
1889
- section('Clone root', cloneRoot, { decorate }),
1890
1952
  nextActions([
1891
- 'Run `tela sync <repo>` to clone or refresh a repository.',
1953
+ 'Run `tela sync <repo>` to clone or refresh an available repository.',
1892
1954
  'Run `tela --help` to see all available commands.',
1893
1955
  ], { decorate }),
1894
1956
  ]))
package/src/types.ts CHANGED
@@ -22,6 +22,12 @@ export interface TelaCliForgejoOrganization {
22
22
  name: string
23
23
  }
24
24
 
25
+ export interface TelaCliGitSshKey {
26
+ title: string
27
+ publicKeyPath: string
28
+ privateKeyPath: string
29
+ }
30
+
25
31
  export interface TelaCliAuthState {
26
32
  user: TelaCliUser
27
33
  workspace: TelaCliWorkspace
@@ -40,6 +46,7 @@ export interface TelaCliConfig {
40
46
  auth: TelaCliAuthState | null
41
47
  forgejoOrganization: TelaCliForgejoOrganization | null
42
48
  forgejoUserRegistered: boolean
49
+ gitSshKey: TelaCliGitSshKey | null
43
50
  }
44
51
 
45
52
  export interface OrganizationSummary {
@@ -75,6 +82,21 @@ export interface RepositoryLookupResponse {
75
82
  error?: string
76
83
  }
77
84
 
85
+ export interface RepositoryListItemResponse {
86
+ organization: OrganizationSummary
87
+ agentRepo: AgentRepoSummary
88
+ }
89
+
90
+ export interface RepositoryListResponse {
91
+ success: boolean
92
+ repositories?: RepositoryListItemResponse[]
93
+ error?: string
94
+ }
95
+
96
+ export interface AvailableRepositorySummary extends RepositoryListItemResponse {
97
+ syncStatus: 'available'
98
+ }
99
+
78
100
  export interface OrganizationListResponse {
79
101
  success: boolean
80
102
  organizations?: TelaCliForgejoOrganization[]