@meistrari/tela-cli 0.4.1 → 0.6.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 CHANGED
@@ -14,6 +14,24 @@ This installs the package globally and adds a `tela` alias to your `~/.zshrc`. A
14
14
  source ~/.zshrc
15
15
  ```
16
16
 
17
+ ## Default command
18
+
19
+ ### `tela`
20
+
21
+ List workspace repositories that are available to sync locally.
22
+
23
+ When to use: Run this with no subcommand when you want to see which workspace repositories are available before using `tela sync <repo>`.
24
+
25
+ Examples:
26
+ - `tela` — Render the available repositories in a human-readable table
27
+ - `tela --json` — Emit the available repositories as JSON
28
+
29
+ ## Version
30
+
31
+ Examples:
32
+ - `tela --version` — Print the current Tela CLI version
33
+ - `tela -v` — Print the current Tela CLI version
34
+
17
35
  ## Commands
18
36
 
19
37
  ### `setup`
@@ -29,7 +47,7 @@ Examples:
29
47
 
30
48
  Clone or update a Tela repository, or refresh the current checkout
31
49
 
32
- When to use: Use this before coding to get a clean local checkout, or to refresh every repository under the configured clone root.
50
+ When to use: Use this before coding to get a clean local checkout, or to refresh every repository under the configured clone root. SSH-backed sync automatically accepts first-seen host keys during clone and update flows.
33
51
 
34
52
  Examples:
35
53
  - `tela sync agent-api` — Clone or update a specific repository under the clone root
@@ -38,14 +56,14 @@ Examples:
38
56
 
39
57
  ### `run`
40
58
 
41
- Run the local agent repository with Claude
59
+ Run the local agent repository with project-scoped Claude
42
60
 
43
- When to use: Use this from inside an agent repository when you want Claude to follow the local contract files, show its progress live, and produce the expected result artifact.
61
+ When to use: Use this from inside an agent repository when you want Claude to follow the repo-local contract files, ignore user/global Claude config by default, show its progress live, and produce the expected result artifact.
44
62
 
45
63
  Examples:
46
- - `tela run` — Run the current agent repository with the live viewer
47
- - `tela run --plain` — Force scrollback-first text output
48
- - `tela run --json` — Emit the final execution and artifact envelope as JSON
64
+ - `tela run` — Run the current agent repository with project-scoped Claude and the live viewer
65
+ - `tela run --plain` — Force project-scoped execution with scrollback-first text output
66
+ - `tela run --json` — Emit the final project-scoped execution and artifact envelope as JSON
49
67
 
50
68
  ### `push`
51
69
 
@@ -96,6 +114,11 @@ Examples:
96
114
  - Human output is structured for terminal use with grouped summaries and next actions.
97
115
  - `--json` is available on read-heavy flows and on `tela run` for the final execution envelope.
98
116
 
117
+ ## `tela run` scope
118
+
119
+ - `tela run` loads project-scoped Claude settings by default and excludes user/global Claude config.
120
+ - Remaining external inputs are explicit: Claude's built-in system prompt, managed enterprise settings, auth/provider credentials, minimal shell runtime environment, and the default model when `agent.config.json` does not set one.
121
+
99
122
  ## First run
100
123
 
101
124
  1. `tela setup`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-cli",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tela": "cli.ts"
package/src/agent-api.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import { buildAgentApiHeaders, type FetchLike } from './auth.ts'
2
2
  import type {
3
+ AgentRepoSummary,
4
+ AvailableRepositorySummary,
3
5
  AgentFeedbackItem,
4
6
  AgentFeedbackListResponse,
5
7
  ForgejoUserCreateResponse,
6
8
  ForgejoUserSshKeyInput,
7
9
  OrganizationListResponse,
10
+ OrganizationSummary,
11
+ RepositoryListItemResponse,
12
+ RepositoryListResponse,
8
13
  RepositoryLookupResponse,
9
14
  TelaCliConfig,
10
15
  TelaCliForgejoOrganization,
@@ -45,6 +50,39 @@ function isForgejoOrganization(value: unknown): value is TelaCliForgejoOrganizat
45
50
  return typeof candidate.id === 'string' && typeof candidate.name === 'string'
46
51
  }
47
52
 
53
+ function isOrganizationSummary(value: unknown): value is OrganizationSummary {
54
+ if (!value || typeof value !== 'object') {
55
+ return false
56
+ }
57
+
58
+ const candidate = value as Partial<OrganizationSummary>
59
+ return typeof candidate.id === 'string'
60
+ && typeof candidate.workspaceId === 'string'
61
+ && typeof candidate.name === 'string'
62
+ }
63
+
64
+ function isAgentRepoSummary(value: unknown): value is AgentRepoSummary {
65
+ if (!value || typeof value !== 'object') {
66
+ return false
67
+ }
68
+
69
+ const candidate = value as Partial<AgentRepoSummary>
70
+ return typeof candidate.id === 'string'
71
+ && typeof candidate.workspaceId === 'string'
72
+ && typeof candidate.organizationId === 'string'
73
+ && typeof candidate.repository === 'string'
74
+ }
75
+
76
+ function isRepositoryListItemResponse(value: unknown): value is RepositoryListItemResponse {
77
+ if (!value || typeof value !== 'object') {
78
+ return false
79
+ }
80
+
81
+ const candidate = value as Partial<RepositoryListItemResponse>
82
+ return isOrganizationSummary(candidate.organization)
83
+ && isAgentRepoSummary(candidate.agentRepo)
84
+ }
85
+
48
86
  export class AgentApiError extends Error {
49
87
  constructor(
50
88
  message: string,
@@ -173,6 +211,44 @@ export async function listOrganizations(
173
211
  return payload.organizations
174
212
  }
175
213
 
214
+ export async function listAvailableRepositories(
215
+ config: TelaCliConfig,
216
+ fetchFn: FetchLike = fetch,
217
+ ): Promise<AvailableRepositorySummary[]> {
218
+ const headers = await buildAgentApiHeaders(config, { fetchFn })
219
+ const response = await fetchFn(
220
+ joinUrl(config.agentApiUrl, '/v3/agents/repositories'),
221
+ {
222
+ method: 'GET',
223
+ headers,
224
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
225
+ },
226
+ )
227
+
228
+ const responseText = await response.text()
229
+ const payload = parseJsonBody(responseText) as RepositoryListResponse | null
230
+
231
+ if (!response.ok) {
232
+ throw new AgentApiError(
233
+ payload?.error ?? `Agent API repository listing failed with status ${response.status}`,
234
+ response.status,
235
+ responseText,
236
+ )
237
+ }
238
+
239
+ if (!payload
240
+ || payload.success !== true
241
+ || !Array.isArray(payload.repositories)
242
+ || !payload.repositories.every(isRepositoryListItemResponse)) {
243
+ throw new AgentApiError('Agent API repository listing returned an invalid payload', response.status, responseText)
244
+ }
245
+
246
+ return payload.repositories.map(repository => ({
247
+ ...repository,
248
+ syncStatus: 'available' as const,
249
+ }))
250
+ }
251
+
176
252
  export async function provisionForgejoUser(
177
253
  input: {
178
254
  organizationName: string
package/src/claude.ts ADDED
@@ -0,0 +1,101 @@
1
+ import { commandExists, runCommand } from './process.ts'
2
+
3
+ export interface ClaudeCliVersion {
4
+ major: number
5
+ minor: number
6
+ patch: number
7
+ }
8
+
9
+ export interface ClaudeCliInspection {
10
+ available: boolean
11
+ version?: string
12
+ parsedVersion?: ClaudeCliVersion
13
+ supportsProjectScopedRun: boolean
14
+ problem?: 'missing' | 'unparseable' | 'unsupported'
15
+ detail?: string
16
+ }
17
+
18
+ export const MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN: ClaudeCliVersion = {
19
+ major: 2,
20
+ minor: 1,
21
+ patch: 108,
22
+ }
23
+
24
+ export function formatClaudeCliVersion(version: ClaudeCliVersion): string {
25
+ return `${version.major}.${version.minor}.${version.patch}`
26
+ }
27
+
28
+ export function compareClaudeCliVersions(left: ClaudeCliVersion, right: ClaudeCliVersion): number {
29
+ if (left.major !== right.major) {
30
+ return left.major - right.major
31
+ }
32
+
33
+ if (left.minor !== right.minor) {
34
+ return left.minor - right.minor
35
+ }
36
+
37
+ return left.patch - right.patch
38
+ }
39
+
40
+ export function parseClaudeCliVersion(raw: string): ClaudeCliVersion | null {
41
+ const match = raw.match(/\b(\d+)\.(\d+)\.(\d+)\b/)
42
+ if (!match) {
43
+ return null
44
+ }
45
+
46
+ return {
47
+ major: Number(match[1]),
48
+ minor: Number(match[2]),
49
+ patch: Number(match[3]),
50
+ }
51
+ }
52
+
53
+ export async function inspectClaudeCli(options: {
54
+ commandExistsFn?: typeof commandExists
55
+ runCommandFn?: typeof runCommand
56
+ } = {}): Promise<ClaudeCliInspection> {
57
+ const commandExistsFn = options.commandExistsFn ?? commandExists
58
+ const runCommandFn = options.runCommandFn ?? runCommand
59
+
60
+ if (!await commandExistsFn('claude')) {
61
+ return {
62
+ available: false,
63
+ supportsProjectScopedRun: false,
64
+ problem: 'missing',
65
+ detail: '`claude` is not available on PATH.',
66
+ }
67
+ }
68
+
69
+ const result = await runCommandFn(['claude', '--version'], { allowFailure: true })
70
+ const output = `${result.stdout}\n${result.stderr}`.trim()
71
+ const parsedVersion = parseClaudeCliVersion(output)
72
+
73
+ if (!parsedVersion) {
74
+ return {
75
+ available: true,
76
+ supportsProjectScopedRun: false,
77
+ problem: 'unparseable',
78
+ detail: output || 'Claude CLI version output was empty.',
79
+ }
80
+ }
81
+
82
+ const version = formatClaudeCliVersion(parsedVersion)
83
+ if (compareClaudeCliVersions(parsedVersion, MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN) < 0) {
84
+ return {
85
+ available: true,
86
+ version,
87
+ parsedVersion,
88
+ supportsProjectScopedRun: false,
89
+ problem: 'unsupported',
90
+ detail: `Claude CLI ${version} is older than the required ${formatClaudeCliVersion(MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN)}.`,
91
+ }
92
+ }
93
+
94
+ return {
95
+ available: true,
96
+ version,
97
+ parsedVersion,
98
+ supportsProjectScopedRun: true,
99
+ detail: `Claude CLI ${version} supports project-scoped runs.`,
100
+ }
101
+ }
@@ -32,7 +32,7 @@ const COMMANDS: CommandMetadata[] = [
32
32
  name: 'sync',
33
33
  summary: 'Clone or update a Tela repository, or refresh the current checkout',
34
34
  argumentHint: '[repo]',
35
- whenToUse: 'Use this before coding to get a clean local checkout, or to refresh every repository under the configured clone root.',
35
+ whenToUse: 'Use this before coding to get a clean local checkout, or to refresh every repository under the configured clone root. SSH-backed sync automatically accepts first-seen host keys during clone and update flows.',
36
36
  examples: [
37
37
  {
38
38
  command: 'tela sync agent-api',
@@ -52,20 +52,20 @@ const COMMANDS: CommandMetadata[] = [
52
52
  },
53
53
  {
54
54
  name: 'run',
55
- summary: 'Run the local agent repository with Claude',
56
- whenToUse: 'Use this from inside an agent repository when you want Claude to follow the local contract files, show its progress live, and produce the expected result artifact.',
55
+ summary: 'Run the local agent repository with project-scoped Claude',
56
+ whenToUse: 'Use this from inside an agent repository when you want Claude to follow the repo-local contract files, ignore user/global Claude config by default, show its progress live, and produce the expected result artifact.',
57
57
  examples: [
58
58
  {
59
59
  command: 'tela run',
60
- description: 'Run the current agent repository with the live viewer',
60
+ description: 'Run the current agent repository with project-scoped Claude and the live viewer',
61
61
  },
62
62
  {
63
63
  command: 'tela run --plain',
64
- description: 'Force scrollback-first text output',
64
+ description: 'Force project-scoped execution with scrollback-first text output',
65
65
  },
66
66
  {
67
67
  command: 'tela run --json',
68
- description: 'Emit the final execution and artifact envelope as JSON',
68
+ description: 'Emit the final project-scoped execution and artifact envelope as JSON',
69
69
  },
70
70
  ],
71
71
  supportsJson: true,
@@ -176,6 +176,24 @@ export function renderReadmeMarkdown(): string {
176
176
  'source ~/.zshrc',
177
177
  '```',
178
178
  '',
179
+ '## Default command',
180
+ '',
181
+ '### `tela`',
182
+ '',
183
+ 'List workspace repositories that are available to sync locally.',
184
+ '',
185
+ 'When to use: Run this with no subcommand when you want to see which workspace repositories are available before using `tela sync <repo>`.',
186
+ '',
187
+ 'Examples:',
188
+ '- `tela` — Render the available repositories in a human-readable table',
189
+ '- `tela --json` — Emit the available repositories as JSON',
190
+ '',
191
+ '## Version',
192
+ '',
193
+ 'Examples:',
194
+ '- `tela --version` — Print the current Tela CLI version',
195
+ '- `tela -v` — Print the current Tela CLI version',
196
+ '',
179
197
  '## Commands',
180
198
  '',
181
199
  ]
@@ -205,6 +223,11 @@ export function renderReadmeMarkdown(): string {
205
223
  lines.push('- Human output is structured for terminal use with grouped summaries and next actions.')
206
224
  lines.push('- `--json` is available on read-heavy flows and on `tela run` for the final execution envelope.')
207
225
  lines.push('')
226
+ lines.push('## `tela run` scope')
227
+ lines.push('')
228
+ lines.push('- `tela run` loads project-scoped Claude settings by default and excludes user/global Claude config.')
229
+ lines.push('- Remaining external inputs are explicit: Claude\'s built-in system prompt, managed enterprise settings, auth/provider credentials, minimal shell runtime environment, and the default model when `agent.config.json` does not set one.')
230
+ lines.push('')
208
231
  lines.push('## First run')
209
232
  lines.push('')
210
233
  lines.push('1. `tela setup`')
@@ -1,9 +1,16 @@
1
+ import { existsSync } from 'node:fs'
1
2
  import { readFile } from 'node:fs/promises'
2
3
  import { basename, join } from 'node:path'
4
+ import {
5
+ formatClaudeCliVersion,
6
+ inspectClaudeCli,
7
+ MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN,
8
+ } from '../claude.ts'
3
9
  import { getRepositoryRoot } from '../git.ts'
4
10
  import { createUserFacingError } from '../errors.ts'
5
11
  import {
6
12
  commandExists,
13
+ runCommand,
7
14
  runInteractiveCommand,
8
15
  runStreamingCommand,
9
16
  spawnStreamingCommand,
@@ -37,6 +44,7 @@ export interface PreparedLocalAgentRun {
37
44
  config: AgentRunConfig
38
45
  resultKind: 'json' | 'markdown'
39
46
  resultPath: string
47
+ mcpConfigPath?: string
40
48
  }
41
49
 
42
50
  export interface RunStepSummary {
@@ -110,6 +118,8 @@ export interface RunLocalAgentCommandOptions {
110
118
  contract?: PreparedLocalAgentRun
111
119
  surface?: RunOutputSurface
112
120
  commandExists?: typeof commandExists
121
+ inspectClaudeCli?: typeof inspectClaudeCli
122
+ runCommand?: typeof runCommand
113
123
  runInteractiveCommand?: typeof runInteractiveCommand
114
124
  runStreamingCommand?: typeof runStreamingCommand
115
125
  spawnStreamingCommand?: typeof spawnStreamingCommand
@@ -1130,6 +1140,88 @@ function parseAgentConfig(path: string, content: string): AgentRunConfig {
1130
1140
  }
1131
1141
  }
1132
1142
 
1143
+ const CLAUDE_RUN_ENV_DENYLIST = new Set([
1144
+ 'CLAUDE_CONFIG_DIR',
1145
+ 'CLAUDE_ENV_FILE',
1146
+ 'ANTHROPIC_MODEL',
1147
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION',
1148
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
1149
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
1150
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES',
1151
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
1152
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL_NAME',
1153
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION',
1154
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
1155
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
1156
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
1157
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
1158
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
1159
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
1160
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
1161
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
1162
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
1163
+ 'CLAUDE_CODE_SUBAGENT_MODEL',
1164
+ ])
1165
+
1166
+ function buildClaudeRunEnvironment(
1167
+ baseEnv: NodeJS.ProcessEnv = process.env,
1168
+ ): Record<string, string | undefined> {
1169
+ return Object.fromEntries(
1170
+ Object.entries(baseEnv).filter(([key]) => !CLAUDE_RUN_ENV_DENYLIST.has(key)),
1171
+ )
1172
+ }
1173
+
1174
+ function buildUnsupportedClaudeVersionError(detail: string | undefined): ReturnType<typeof createUserFacingError> {
1175
+ const requiredVersion = formatClaudeCliVersion(MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN)
1176
+
1177
+ return createUserFacingError({
1178
+ code: 'run_claude_version_unsupported',
1179
+ summary: `\`tela run\` requires Claude CLI ${requiredVersion} or newer for project-scoped runs.`,
1180
+ details: detail,
1181
+ fixes: [
1182
+ 'Update Claude CLI before using `tela run`.',
1183
+ 'Run `claude --version` to confirm the installed version.',
1184
+ ],
1185
+ })
1186
+ }
1187
+
1188
+ function buildClaudeBaseCommand(prepared: PreparedLocalAgentRun): string[] {
1189
+ const command = [
1190
+ 'claude',
1191
+ '--setting-sources',
1192
+ 'project',
1193
+ ]
1194
+
1195
+ if (prepared.mcpConfigPath) {
1196
+ command.push('--strict-mcp-config', '--mcp-config', prepared.mcpConfigPath)
1197
+ }
1198
+
1199
+ if (prepared.config.model) {
1200
+ command.push('--model', prepared.config.model)
1201
+ }
1202
+
1203
+ return command
1204
+ }
1205
+
1206
+ function buildClaudeCommand(
1207
+ prepared: PreparedLocalAgentRun,
1208
+ mode: RunMode,
1209
+ ): string[] {
1210
+ const command = buildClaudeBaseCommand(prepared)
1211
+
1212
+ if (mode === 'one-shot') {
1213
+ command.push(
1214
+ '-p',
1215
+ '--output-format',
1216
+ 'stream-json',
1217
+ '--verbose',
1218
+ )
1219
+ }
1220
+
1221
+ command.push(INITIAL_PROMPT)
1222
+ return command
1223
+ }
1224
+
1133
1225
  async function resolveRepositoryRoot(cwd: string): Promise<string> {
1134
1226
  try {
1135
1227
  return await getRepositoryRoot(cwd)
@@ -1176,12 +1268,19 @@ async function validateRequiredFiles(repoRoot: string): Promise<void> {
1176
1268
  export async function prepareLocalAgentRun(options: {
1177
1269
  cwd?: string
1178
1270
  commandExists?: typeof commandExists
1271
+ inspectClaudeCli?: typeof inspectClaudeCli
1272
+ runCommand?: typeof runCommand
1179
1273
  } = {}): Promise<PreparedLocalAgentRun> {
1180
1274
  const cwd = options.cwd ?? process.cwd()
1181
- const commandExistsFn = options.commandExists ?? commandExists
1182
1275
  const repoRoot = await resolveRepositoryRoot(cwd)
1276
+ const inspectClaudeCliFn = options.inspectClaudeCli ?? inspectClaudeCli
1277
+
1278
+ const claudeInspection = await inspectClaudeCliFn({
1279
+ commandExistsFn: options.commandExists,
1280
+ runCommandFn: options.runCommand,
1281
+ })
1183
1282
 
1184
- if (!await commandExistsFn('claude')) {
1283
+ if (!claudeInspection.available) {
1185
1284
  throw createUserFacingError({
1186
1285
  code: 'run_claude_missing',
1187
1286
  summary: 'Claude CLI is not available on PATH.',
@@ -1189,6 +1288,10 @@ export async function prepareLocalAgentRun(options: {
1189
1288
  })
1190
1289
  }
1191
1290
 
1291
+ if (!claudeInspection.supportsProjectScopedRun) {
1292
+ throw buildUnsupportedClaudeVersionError(claudeInspection.detail)
1293
+ }
1294
+
1192
1295
  await validateRequiredFiles(repoRoot)
1193
1296
 
1194
1297
  const [outputFormatContent, agentConfigContent] = await Promise.all([
@@ -1209,6 +1312,9 @@ export async function prepareLocalAgentRun(options: {
1209
1312
  config,
1210
1313
  resultKind,
1211
1314
  resultPath: join(repoRoot, 'output', resultKind === 'json' ? 'result.json' : 'result.md'),
1315
+ mcpConfigPath: existsSync(join(repoRoot, '.mcp.json'))
1316
+ ? join(repoRoot, '.mcp.json')
1317
+ : undefined,
1212
1318
  }
1213
1319
  }
1214
1320
 
@@ -1375,13 +1481,10 @@ async function runInteractiveAgent(
1375
1481
  writeLine(options.writeStdout, 'Launching Claude in interactive chat mode. Tela will resume after Claude exits.')
1376
1482
  }
1377
1483
 
1378
- const command = [
1379
- 'claude',
1380
- ...(prepared.config.model ? ['--model', prepared.config.model] : []),
1381
- INITIAL_PROMPT,
1382
- ]
1383
-
1384
- await runInteractiveCommandFn(command, { cwd: prepared.repoRoot })
1484
+ await runInteractiveCommandFn(buildClaudeCommand(prepared, 'chat'), {
1485
+ cwd: prepared.repoRoot,
1486
+ env: buildClaudeRunEnvironment(),
1487
+ })
1385
1488
  }
1386
1489
 
1387
1490
  async function runOneShotAgent(
@@ -1408,18 +1511,11 @@ async function runOneShotAgent(
1408
1511
  }
1409
1512
 
1410
1513
  let cancelled = false
1411
- const command = [
1412
- 'claude',
1413
- '-p',
1414
- '--output-format',
1415
- 'stream-json',
1416
- '--verbose',
1417
- ...(prepared.config.model ? ['--model', prepared.config.model] : []),
1418
- INITIAL_PROMPT,
1419
- ]
1514
+ const command = buildClaudeCommand(prepared, 'one-shot')
1420
1515
 
1421
1516
  const handle = spawnStreamingCommandFn(command, {
1422
1517
  cwd: prepared.repoRoot,
1518
+ env: buildClaudeRunEnvironment(),
1423
1519
  allowFailure: true,
1424
1520
  onStdoutLine: async (line) => {
1425
1521
  const trimmed = line.trim()
@@ -1509,6 +1605,8 @@ export async function runLocalAgentCommand(
1509
1605
  const prepared = options.contract ?? await prepareLocalAgentRun({
1510
1606
  cwd: options.cwd ?? process.cwd(),
1511
1607
  commandExists: options.commandExists,
1608
+ inspectClaudeCli: options.inspectClaudeCli,
1609
+ runCommand: options.runCommand,
1512
1610
  })
1513
1611
  const mode: RunMode = prepared.config.isMultiturn ? 'chat' : 'one-shot'
1514
1612
  const tracker = new RunStateTracker(prepared, mode)
@@ -52,7 +52,17 @@ export function renderStatusReport(report: StatusReport, options: RenderOptions
52
52
 
53
53
  const toolRows = [
54
54
  ['git', report.tools.git ? 'available' : 'missing', report.tools.git ? 'ready' : 'install git'],
55
- ['claude', report.tools.claude ? 'available' : 'missing', report.tools.claude ? 'ready' : 'required for `tela run`, `tela feedback`, and Claude commit messages'],
55
+ [
56
+ 'claude',
57
+ !report.tools.claude
58
+ ? 'missing'
59
+ : (report.tools.claudeProjectScopedRunSupported === false ? 'unsupported' : 'available'),
60
+ !report.tools.claude
61
+ ? 'required for `tela run`, `tela feedback`, and Claude commit messages'
62
+ : (report.tools.claudeProjectScopedRunSupported === false
63
+ ? (report.tools.claudeDetail ?? `version ${report.tools.claudeVersion ?? '<unknown>'}`)
64
+ : (report.tools.claudeVersion ? `version ${report.tools.claudeVersion}` : 'ready')),
65
+ ],
56
66
  [
57
67
  'agent-api-origin',
58
68
  report.tools.agentApiReachable === null
@@ -1,6 +1,11 @@
1
1
  import { accessSync, constants, existsSync, statSync } from 'node:fs'
2
2
  import { dirname } from 'node:path'
3
3
  import { buildAgentApiHeaders } from './auth.ts'
4
+ import {
5
+ formatClaudeCliVersion,
6
+ inspectClaudeCli,
7
+ MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN,
8
+ } from './claude.ts'
4
9
  import { inspectSavedConfig, redactSecret } from './config.ts'
5
10
  import { resolveSshPrivateKeyPath } from './forgejo.ts'
6
11
  import {
@@ -70,6 +75,9 @@ export interface StatusReport {
70
75
  tools: {
71
76
  git: boolean
72
77
  claude: boolean
78
+ claudeVersion?: string
79
+ claudeProjectScopedRunSupported?: boolean | null
80
+ claudeDetail?: string
73
81
  agentApiReachable: boolean | null
74
82
  agentApiCategory?: AgentApiReachabilityCategory
75
83
  agentApiStatus?: number
@@ -269,6 +277,7 @@ function buildStatusNextActions(
269
277
  hasForgejoOrganization: boolean,
270
278
  hasForgejoAccess: boolean,
271
279
  hasGitSshKey: boolean,
280
+ claudeFix?: string,
272
281
  ): string[] {
273
282
  const actions: string[] = []
274
283
 
@@ -300,6 +309,10 @@ function buildStatusNextActions(
300
309
  actions.push('Review local changes before syncing, or use `tela push` to stage and commit them.')
301
310
  }
302
311
 
312
+ if (claudeFix) {
313
+ actions.push(claudeFix)
314
+ }
315
+
303
316
  if (warnings.length === 0) {
304
317
  actions.push('Run `tela sync --current`, `tela run`, or `tela push` when you are ready.')
305
318
  }
@@ -311,15 +324,16 @@ export async function createStatusReport(
311
324
  cwd = process.cwd(),
312
325
  fetchFn: typeof fetch = fetch,
313
326
  ): Promise<StatusReport> {
314
- const [configInspection, gitAvailable, claudeAvailable, repo] = await Promise.all([
327
+ const [configInspection, gitAvailable, claudeInspection, repo] = await Promise.all([
315
328
  Promise.resolve(inspectSavedConfig()),
316
329
  commandExists('git'),
317
- commandExists('claude'),
330
+ inspectClaudeCli(),
318
331
  inspectCurrentRepository(cwd),
319
332
  ])
320
333
 
321
334
  const warnings: string[] = []
322
335
  let reachability: AgentApiReachability | undefined
336
+ let claudeFix: string | undefined
323
337
 
324
338
  if (!configInspection.exists) {
325
339
  warnings.push('Tela CLI is not configured yet.')
@@ -354,8 +368,18 @@ export async function createStatusReport(
354
368
  warnings.push('`git` is not available on PATH.')
355
369
  }
356
370
 
357
- if (!claudeAvailable) {
371
+ if (!claudeInspection.available) {
358
372
  warnings.push('`claude` is not available on PATH.')
373
+ claudeFix = 'Install the Claude CLI before using `tela run`, `tela push --claude`, or `tela feedback`.'
374
+ }
375
+ else if (!claudeInspection.supportsProjectScopedRun) {
376
+ const requiredVersion = formatClaudeCliVersion(MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN)
377
+ warnings.push(
378
+ claudeInspection.problem === 'unparseable'
379
+ ? `Claude CLI version could not be determined for project-scoped runs: ${claudeInspection.detail ?? 'unknown output'}`
380
+ : `Claude CLI must be updated to ${requiredVersion} or newer for project-scoped \`tela run\`.`,
381
+ )
382
+ claudeFix = `Update Claude CLI to version ${requiredVersion} or newer before using \`tela run\`.`
359
383
  }
360
384
 
361
385
  if (repo.detached) {
@@ -382,6 +406,7 @@ export async function createStatusReport(
382
406
  Boolean(configInspection.config?.forgejoOrganization),
383
407
  Boolean(configInspection.config?.forgejoUserRegistered),
384
408
  Boolean(configInspection.config?.gitSshKey),
409
+ claudeFix,
385
410
  )
386
411
 
387
412
  return {
@@ -410,7 +435,12 @@ export async function createStatusReport(
410
435
  repo,
411
436
  tools: {
412
437
  git: gitAvailable,
413
- claude: claudeAvailable,
438
+ claude: claudeInspection.available,
439
+ claudeVersion: claudeInspection.version,
440
+ claudeProjectScopedRunSupported: claudeInspection.available
441
+ ? claudeInspection.supportsProjectScopedRun
442
+ : null,
443
+ claudeDetail: claudeInspection.detail,
414
444
  agentApiReachable: reachability?.reachable ?? null,
415
445
  agentApiCategory: reachability?.category,
416
446
  agentApiStatus: reachability?.statusCode,
@@ -632,10 +662,10 @@ export async function createDoctorReport(
632
662
  cwd = process.cwd(),
633
663
  fetchFn: typeof fetch = fetch,
634
664
  ): Promise<DoctorReport> {
635
- const [configInspection, gitAvailable, claudeAvailable, repo] = await Promise.all([
665
+ const [configInspection, gitAvailable, claudeInspection, repo] = await Promise.all([
636
666
  Promise.resolve(inspectSavedConfig()),
637
667
  commandExists('git'),
638
- commandExists('claude'),
668
+ inspectClaudeCli(),
639
669
  inspectCurrentRepository(cwd),
640
670
  ])
641
671
 
@@ -675,9 +705,22 @@ export async function createDoctorReport(
675
705
  {
676
706
  id: 'tool-claude',
677
707
  group: 'tools',
678
- status: claudeAvailable ? 'pass' : 'warn',
679
- summary: claudeAvailable ? '`claude` is available.' : '`claude` is not available on PATH.',
680
- fixes: claudeAvailable ? ['No action required.'] : ['Install the Claude CLI before using `tela run`, `tela push --claude`, or `tela feedback`.'],
708
+ status: !claudeInspection.available
709
+ ? 'warn'
710
+ : (claudeInspection.supportsProjectScopedRun ? 'pass' : 'fail'),
711
+ summary: !claudeInspection.available
712
+ ? '`claude` is not available on PATH.'
713
+ : (claudeInspection.supportsProjectScopedRun
714
+ ? `Claude CLI is available (${claudeInspection.version ?? 'unknown version'}) and supports project-scoped runs.`
715
+ : (claudeInspection.problem === 'unparseable'
716
+ ? 'Claude CLI version could not be determined for project-scoped runs.'
717
+ : `Claude CLI ${claudeInspection.version ?? '<unknown>'} is too old for project-scoped runs.`)),
718
+ fixes: !claudeInspection.available
719
+ ? ['Install the Claude CLI before using `tela run`, `tela push --claude`, or `tela feedback`.']
720
+ : (claudeInspection.supportsProjectScopedRun
721
+ ? ['No action required.']
722
+ : [`Update Claude CLI to version ${formatClaudeCliVersion(MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN)} or newer before using \`tela run\`.`]),
723
+ evidence: claudeInspection.available && claudeInspection.detail ? [claudeInspection.detail] : undefined,
681
724
  },
682
725
  ...buildRepoChecks(repo),
683
726
  ]
package/src/git.ts CHANGED
@@ -23,16 +23,33 @@ export interface GitStatusCounts {
23
23
  total: number
24
24
  }
25
25
 
26
+ type GitSshHostKeyPolicy = 'default' | 'accept-new'
27
+
26
28
  interface GitRunOptions extends Omit<RunCommandOptions, 'env'> {
27
29
  requireConfiguredSsh?: boolean
30
+ sshHostKeyPolicy?: GitSshHostKeyPolicy
28
31
  }
29
32
 
30
33
  function quoteForShell(value: string): string {
31
34
  return `'${value.replaceAll('\'', `'"'"'`)}'`
32
35
  }
33
36
 
34
- export function buildGitSshCommand(privateKeyPath: string): string {
35
- return `ssh -i ${quoteForShell(privateKeyPath)} -o IdentitiesOnly=yes`
37
+ export function buildGitSshCommand(
38
+ privateKeyPath: string,
39
+ options: {
40
+ hostKeyPolicy?: GitSshHostKeyPolicy
41
+ } = {},
42
+ ): string {
43
+ const args = [
44
+ `-i ${quoteForShell(privateKeyPath)}`,
45
+ '-o IdentitiesOnly=yes',
46
+ ]
47
+
48
+ if (options.hostKeyPolicy === 'accept-new') {
49
+ args.push('-o StrictHostKeyChecking=accept-new')
50
+ }
51
+
52
+ return `ssh ${args.join(' ')}`
36
53
  }
37
54
 
38
55
  export function isSshRemoteUrl(remoteUrl: string): boolean {
@@ -108,14 +125,19 @@ function resolveConfiguredGitSshKey(requireConfiguredSsh: boolean): TelaCliGitSs
108
125
  }
109
126
  }
110
127
 
111
- function buildGitEnv(requireConfiguredSsh: boolean): Record<string, string> | undefined {
128
+ function buildGitEnv(
129
+ requireConfiguredSsh: boolean,
130
+ sshHostKeyPolicy: GitSshHostKeyPolicy = 'default',
131
+ ): Record<string, string> | undefined {
112
132
  const gitSshKey = resolveConfiguredGitSshKey(requireConfiguredSsh)
113
133
  if (!gitSshKey) {
114
134
  return undefined
115
135
  }
116
136
 
117
137
  return {
118
- GIT_SSH_COMMAND: buildGitSshCommand(gitSshKey.privateKeyPath),
138
+ GIT_SSH_COMMAND: buildGitSshCommand(gitSshKey.privateKeyPath, {
139
+ hostKeyPolicy: sshHostKeyPolicy,
140
+ }),
119
141
  }
120
142
  }
121
143
 
@@ -123,7 +145,10 @@ async function runGitCommand(
123
145
  args: string[],
124
146
  options: GitRunOptions = {},
125
147
  ): Promise<CommandResult> {
126
- const env = buildGitEnv(Boolean(options.requireConfiguredSsh))
148
+ const env = buildGitEnv(
149
+ Boolean(options.requireConfiguredSsh),
150
+ options.sshHostKeyPolicy,
151
+ )
127
152
 
128
153
  return await runCommand(
129
154
  ['git', ...args],
@@ -215,6 +240,8 @@ export async function assertGitRepository(cwd: string): Promise<void> {
215
240
  export async function cloneRepository(remoteUrl: string, targetDir: string): Promise<void> {
216
241
  await runGitCommand(['clone', remoteUrl, targetDir], {
217
242
  requireConfiguredSsh: isSshRemoteUrl(remoteUrl),
243
+ // Sync accepts first-seen SSH host keys automatically, but changed host keys still fail.
244
+ sshHostKeyPolicy: 'accept-new',
218
245
  })
219
246
  }
220
247
 
@@ -227,6 +254,7 @@ export async function fetchOrigin(cwd: string): Promise<void> {
227
254
  await runGitCommand(['fetch', 'origin', '--prune'], {
228
255
  cwd,
229
256
  requireConfiguredSsh: await remoteRequiresConfiguredSsh(cwd, 'origin'),
257
+ sshHostKeyPolicy: 'accept-new',
230
258
  })
231
259
  }
232
260
 
@@ -234,6 +262,7 @@ export async function fetchAllRemotes(cwd: string): Promise<void> {
234
262
  await runGitCommand(['fetch', '--all', '--prune'], {
235
263
  cwd,
236
264
  requireConfiguredSsh: await anyRemoteRequiresConfiguredSsh(cwd),
265
+ sshHostKeyPolicy: 'accept-new',
237
266
  })
238
267
  }
239
268
 
@@ -280,6 +309,7 @@ export async function pullCurrentBranch(cwd: string): Promise<void> {
280
309
  await runGitCommand(['pull', '--ff-only'], {
281
310
  cwd,
282
311
  requireConfiguredSsh: await upstreamRequiresConfiguredSsh(cwd),
312
+ sshHostKeyPolicy: 'accept-new',
283
313
  })
284
314
  }
285
315
 
@@ -287,6 +317,7 @@ export async function pullDefaultBranch(cwd: string, defaultBranch: string): Pro
287
317
  await runGitCommand(['pull', '--ff-only', 'origin', defaultBranch], {
288
318
  cwd,
289
319
  requireConfiguredSsh: await remoteRequiresConfiguredSsh(cwd, 'origin'),
320
+ sshHostKeyPolicy: 'accept-new',
290
321
  })
291
322
  }
292
323
 
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,
@@ -90,6 +90,7 @@ import {
90
90
  import { commandExists, runInteractiveCommand, runStreamingCommand } from './process.ts'
91
91
  import { CommandRuntime } from './runtime.ts'
92
92
  import type {
93
+ AvailableRepositorySummary,
93
94
  BatchSyncFailure,
94
95
  SyncResult,
95
96
  TelaCliAuthState,
@@ -107,6 +108,7 @@ import {
107
108
  stepList,
108
109
  table,
109
110
  } from './ui.ts'
111
+ import { CLI_VERSION } from './version.ts'
110
112
 
111
113
  type PromptModule = FeedbackPromptModule
112
114
 
@@ -125,6 +127,7 @@ interface CliDeps {
125
127
  syncAllRepositories?: typeof syncAllRepositories
126
128
  syncCurrentRepository?: typeof syncCurrentRepository
127
129
  listCloneRootRepositories?: typeof listCloneRootRepositories
130
+ listAvailableRepositories?: typeof listAvailableRepositories
128
131
  previewPush?: typeof previewPush
129
132
  pushRepository?: typeof pushRepository
130
133
  runLocalAgentCommand?: typeof runLocalAgentCommand
@@ -266,11 +269,13 @@ function renderRootHelp(program: Command): string {
266
269
  return joinBlocks([
267
270
  bootstrapPane('Tela CLI', [
268
271
  'One-shot workflows for setting up Tela, syncing repositories, running local agents, preparing pushes, and applying commit feedback.',
272
+ 'Run `tela` with no subcommand to list workspace repositories available to sync.',
269
273
  '',
270
274
  'First successful path:',
271
275
  stepList([
272
276
  'tela setup',
273
277
  'tela status',
278
+ 'tela',
274
279
  'tela sync <repo> or tela sync --current',
275
280
  'tela run',
276
281
  'tela push',
@@ -282,6 +287,7 @@ function renderRootHelp(program: Command): string {
282
287
  ['Flag', 'Description'],
283
288
  [
284
289
  ['--verbose', 'Include raw evidence for failures where available'],
290
+ ['-v, --version', 'Show current Tela CLI version'],
285
291
  ['-h, --help', 'Show command help'],
286
292
  ],
287
293
  renderOptions,
@@ -331,6 +337,27 @@ function installCustomHelp(program: Command): void {
331
337
  })
332
338
  }
333
339
 
340
+ function installCommanderOutput(program: Command, deps: CliDeps): void {
341
+ program.configureOutput({
342
+ writeOut: (message) => {
343
+ if (deps.writeStdout) {
344
+ deps.writeStdout(message)
345
+ return
346
+ }
347
+
348
+ process.stdout.write(message)
349
+ },
350
+ writeErr: (message) => {
351
+ if (deps.writeStderr) {
352
+ deps.writeStderr(message)
353
+ return
354
+ }
355
+
356
+ process.stderr.write(message)
357
+ },
358
+ })
359
+ }
360
+
334
361
  function emitBootstrap(runtime: CommandRuntime, title: string, summary: string): void {
335
362
  runtime.emit(bootstrapPane(title, summary, { decorate: runtime.decorate }))
336
363
  }
@@ -762,6 +789,26 @@ function syncCategoryFixes(category: SyncFailureCategory): string[] {
762
789
  }
763
790
  }
764
791
 
792
+ function buildSyncFailureError(error: unknown): ReturnType<typeof createUserFacingError> {
793
+ const category = categorizeSyncError(error)
794
+ const userError = toUserFacingError(error)
795
+ const stderrDetails = userError.evidence
796
+ .find(line => line.startsWith('stderr: '))
797
+ ?.replace(/^stderr: /, '')
798
+ const stdoutDetails = userError.evidence
799
+ .find(line => line.startsWith('stdout: '))
800
+ ?.replace(/^stdout: /, '')
801
+
802
+ return createUserFacingError({
803
+ code: `sync_${category}`,
804
+ summary: error instanceof Error ? error.message : String(error),
805
+ details: stderrDetails || stdoutDetails || userError.details,
806
+ fixes: syncCategoryFixes(category),
807
+ evidence: userError.evidence,
808
+ cause: error,
809
+ })
810
+ }
811
+
765
812
  function renderSyncPlan(input: {
766
813
  mode: SyncCommandResult['mode']
767
814
  target: string
@@ -1025,13 +1072,7 @@ async function runSyncInk(
1025
1072
  }
1026
1073
  }
1027
1074
  catch (error) {
1028
- const category = categorizeSyncError(error)
1029
- throw createUserFacingError({
1030
- code: `sync_${category}`,
1031
- summary: error instanceof Error ? error.message : String(error),
1032
- fixes: syncCategoryFixes(category),
1033
- cause: error,
1034
- })
1075
+ throw buildSyncFailureError(error)
1035
1076
  }
1036
1077
  },
1037
1078
  }))
@@ -1203,13 +1244,7 @@ export async function runSync(
1203
1244
  return result
1204
1245
  }
1205
1246
  catch (error) {
1206
- const category = categorizeSyncError(error)
1207
- throw createUserFacingError({
1208
- code: `sync_${category}`,
1209
- summary: error instanceof Error ? error.message : String(error),
1210
- fixes: syncCategoryFixes(category),
1211
- cause: error,
1212
- })
1247
+ throw buildSyncFailureError(error)
1213
1248
  }
1214
1249
  }
1215
1250
 
@@ -1857,8 +1892,10 @@ export async function runDoctor(
1857
1892
  }
1858
1893
 
1859
1894
  export interface ListRepositoriesResult {
1860
- cloneRoot: string
1861
- repositories: string[]
1895
+ success: boolean
1896
+ repositories: AvailableRepositorySummary[]
1897
+ message?: string
1898
+ nextActions?: string[]
1862
1899
  }
1863
1900
 
1864
1901
  export async function runListRepositories(
@@ -1867,35 +1904,76 @@ export async function runListRepositories(
1867
1904
  ): Promise<ListRepositoriesResult> {
1868
1905
  const runtime = createRuntime(deps, options)
1869
1906
  const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
1870
- const cloneRoot = inspection.config?.cloneRoot ?? getDefaultCloneRoot()
1871
- const listRepos = deps.listCloneRootRepositories ?? listCloneRootRepositories
1872
- const repositories = listRepos(cloneRoot)
1873
- const result: ListRepositoriesResult = { cloneRoot, repositories }
1907
+ const decorate = runtime.decorate
1908
+ const setupRequiredNextActions = [
1909
+ 'Run `tela setup` to configure Tela CLI and sign in.',
1910
+ 'Run `tela --help` to see all available commands.',
1911
+ ]
1912
+ const setupRequiredMessage = 'Setup and sign-in are required before Tela can list repositories available to sync.'
1913
+
1914
+ if (!inspection.valid || !inspection.config?.auth) {
1915
+ const result: ListRepositoriesResult = {
1916
+ success: false,
1917
+ repositories: [],
1918
+ message: setupRequiredMessage,
1919
+ nextActions: setupRequiredNextActions,
1920
+ }
1921
+
1922
+ if (options.json) {
1923
+ runtime.emitJson(result)
1924
+ return result
1925
+ }
1926
+
1927
+ emitBootstrap(runtime, 'Tela CLI', 'Workspace repositories available to sync.')
1928
+ runtime.emit(joinBlocks([
1929
+ section('Repositories', [
1930
+ setupRequiredMessage,
1931
+ '',
1932
+ 'These repositories will appear here after setup is complete.',
1933
+ ], { decorate }),
1934
+ nextActions(setupRequiredNextActions, { decorate }),
1935
+ ]))
1936
+
1937
+ return result
1938
+ }
1939
+
1940
+ const listRepos = deps.listAvailableRepositories ?? listAvailableRepositories
1941
+ const repositories = await listRepos(inspection.config, deps.fetchFn ?? fetch)
1942
+ const result: ListRepositoriesResult = {
1943
+ success: true,
1944
+ repositories,
1945
+ }
1874
1946
 
1875
1947
  if (options.json) {
1876
1948
  runtime.emitJson(result)
1877
1949
  return result
1878
1950
  }
1879
1951
 
1880
- emitBootstrap(runtime, 'Tela CLI', 'Workspace repositories under the configured clone root.')
1881
- const decorate = runtime.decorate
1952
+ emitBootstrap(runtime, 'Tela CLI', 'Workspace repositories available to sync.')
1882
1953
  const body = repositories.length > 0
1883
- ? section('Repositories', bulletList(repositories), { decorate })
1954
+ ? section(
1955
+ 'Repositories',
1956
+ table(
1957
+ ['Organization', 'Repository', 'Sync'],
1958
+ repositories.map(repository => [
1959
+ repository.organization.name,
1960
+ repository.agentRepo.repository,
1961
+ repository.syncStatus,
1962
+ ]),
1963
+ { decorate },
1964
+ ),
1965
+ { decorate },
1966
+ )
1884
1967
  : section('Repositories', [
1885
- `No repositories found under ${cloneRoot}.`,
1968
+ 'No workspace repositories are currently registered for this workspace.',
1886
1969
  '',
1887
- 'Next steps:',
1888
- bulletList([
1889
- 'Run `tela setup` to configure the clone root.',
1890
- 'Run `tela sync <repo>` to clone a workspace repository.',
1891
- ]),
1970
+ 'There is nothing to sync yet.',
1892
1971
  ], { decorate })
1893
1972
 
1894
1973
  runtime.emit(joinBlocks([
1895
1974
  body,
1896
- section('Clone root', cloneRoot, { decorate }),
1897
1975
  nextActions([
1898
- 'Run `tela sync <repo>` to clone or refresh a repository.',
1976
+ 'Run `tela sync <repo>` to clone or refresh an available repository.',
1899
1977
  'Run `tela --help` to see all available commands.',
1900
1978
  ], { decorate }),
1901
1979
  ]))
@@ -1913,8 +1991,10 @@ export function createProgram(deps: CliDeps = {}): Command {
1913
1991
  .name('tela')
1914
1992
  .description('Tela CLI workspace and auth helper')
1915
1993
  .showHelpAfterError()
1994
+ .version(CLI_VERSION, '-v, --version', 'show current Tela CLI version')
1916
1995
  .option('--verbose', 'include raw evidence on failures')
1917
1996
 
1997
+ installCommanderOutput(program, deps)
1918
1998
  installCustomHelp(program)
1919
1999
 
1920
2000
  addStandardReadOptions(program).action(async (commandOptions: GlobalCliOptions) => {
package/src/types.ts CHANGED
@@ -82,6 +82,21 @@ export interface RepositoryLookupResponse {
82
82
  error?: string
83
83
  }
84
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
+
85
100
  export interface OrganizationListResponse {
86
101
  success: boolean
87
102
  organizations?: TelaCliForgejoOrganization[]
package/src/version.ts ADDED
@@ -0,0 +1,3 @@
1
+ import packageJson from '../package.json' with { type: 'json' }
2
+
3
+ export const CLI_VERSION = packageJson.version