@meistrari/tela-cli 0.4.1 → 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/README.md +23 -6
- package/package.json +1 -1
- package/src/agent-api.ts +76 -0
- package/src/claude.ts +101 -0
- package/src/commands/metadata.ts +23 -6
- package/src/commands/run.ts +116 -18
- package/src/commands/status.ts +11 -1
- package/src/diagnostics.ts +52 -9
- package/src/git.ts +36 -5
- package/src/program.ts +88 -33
- package/src/types.ts +15 -0
package/README.md
CHANGED
|
@@ -14,6 +14,18 @@ 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
|
+
|
|
17
29
|
## Commands
|
|
18
30
|
|
|
19
31
|
### `setup`
|
|
@@ -29,7 +41,7 @@ Examples:
|
|
|
29
41
|
|
|
30
42
|
Clone or update a Tela repository, or refresh the current checkout
|
|
31
43
|
|
|
32
|
-
When to use: Use this before coding to get a clean local checkout, or to refresh every repository under the configured clone root.
|
|
44
|
+
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
45
|
|
|
34
46
|
Examples:
|
|
35
47
|
- `tela sync agent-api` — Clone or update a specific repository under the clone root
|
|
@@ -38,14 +50,14 @@ Examples:
|
|
|
38
50
|
|
|
39
51
|
### `run`
|
|
40
52
|
|
|
41
|
-
Run the local agent repository with Claude
|
|
53
|
+
Run the local agent repository with project-scoped Claude
|
|
42
54
|
|
|
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.
|
|
55
|
+
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
56
|
|
|
45
57
|
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
|
|
58
|
+
- `tela run` — Run the current agent repository with project-scoped Claude and the live viewer
|
|
59
|
+
- `tela run --plain` — Force project-scoped execution with scrollback-first text output
|
|
60
|
+
- `tela run --json` — Emit the final project-scoped execution and artifact envelope as JSON
|
|
49
61
|
|
|
50
62
|
### `push`
|
|
51
63
|
|
|
@@ -96,6 +108,11 @@ Examples:
|
|
|
96
108
|
- Human output is structured for terminal use with grouped summaries and next actions.
|
|
97
109
|
- `--json` is available on read-heavy flows and on `tela run` for the final execution envelope.
|
|
98
110
|
|
|
111
|
+
## `tela run` scope
|
|
112
|
+
|
|
113
|
+
- `tela run` loads project-scoped Claude settings by default and excludes user/global Claude config.
|
|
114
|
+
- 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.
|
|
115
|
+
|
|
99
116
|
## First run
|
|
100
117
|
|
|
101
118
|
1. `tela setup`
|
package/package.json
CHANGED
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
|
+
}
|
package/src/commands/metadata.ts
CHANGED
|
@@ -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,18 @@ 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
|
+
'',
|
|
179
191
|
'## Commands',
|
|
180
192
|
'',
|
|
181
193
|
]
|
|
@@ -205,6 +217,11 @@ export function renderReadmeMarkdown(): string {
|
|
|
205
217
|
lines.push('- Human output is structured for terminal use with grouped summaries and next actions.')
|
|
206
218
|
lines.push('- `--json` is available on read-heavy flows and on `tela run` for the final execution envelope.')
|
|
207
219
|
lines.push('')
|
|
220
|
+
lines.push('## `tela run` scope')
|
|
221
|
+
lines.push('')
|
|
222
|
+
lines.push('- `tela run` loads project-scoped Claude settings by default and excludes user/global Claude config.')
|
|
223
|
+
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.')
|
|
224
|
+
lines.push('')
|
|
208
225
|
lines.push('## First run')
|
|
209
226
|
lines.push('')
|
|
210
227
|
lines.push('1. `tela setup`')
|
package/src/commands/run.ts
CHANGED
|
@@ -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 (!
|
|
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
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
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)
|
package/src/commands/status.ts
CHANGED
|
@@ -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
|
-
[
|
|
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
|
package/src/diagnostics.ts
CHANGED
|
@@ -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,
|
|
327
|
+
const [configInspection, gitAvailable, claudeInspection, repo] = await Promise.all([
|
|
315
328
|
Promise.resolve(inspectSavedConfig()),
|
|
316
329
|
commandExists('git'),
|
|
317
|
-
|
|
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 (!
|
|
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:
|
|
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,
|
|
665
|
+
const [configInspection, gitAvailable, claudeInspection, repo] = await Promise.all([
|
|
636
666
|
Promise.resolve(inspectSavedConfig()),
|
|
637
667
|
commandExists('git'),
|
|
638
|
-
|
|
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:
|
|
679
|
-
|
|
680
|
-
|
|
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(
|
|
35
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
@@ -125,6 +126,7 @@ interface CliDeps {
|
|
|
125
126
|
syncAllRepositories?: typeof syncAllRepositories
|
|
126
127
|
syncCurrentRepository?: typeof syncCurrentRepository
|
|
127
128
|
listCloneRootRepositories?: typeof listCloneRootRepositories
|
|
129
|
+
listAvailableRepositories?: typeof listAvailableRepositories
|
|
128
130
|
previewPush?: typeof previewPush
|
|
129
131
|
pushRepository?: typeof pushRepository
|
|
130
132
|
runLocalAgentCommand?: typeof runLocalAgentCommand
|
|
@@ -266,11 +268,13 @@ function renderRootHelp(program: Command): string {
|
|
|
266
268
|
return joinBlocks([
|
|
267
269
|
bootstrapPane('Tela CLI', [
|
|
268
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.',
|
|
269
272
|
'',
|
|
270
273
|
'First successful path:',
|
|
271
274
|
stepList([
|
|
272
275
|
'tela setup',
|
|
273
276
|
'tela status',
|
|
277
|
+
'tela',
|
|
274
278
|
'tela sync <repo> or tela sync --current',
|
|
275
279
|
'tela run',
|
|
276
280
|
'tela push',
|
|
@@ -762,6 +766,26 @@ function syncCategoryFixes(category: SyncFailureCategory): string[] {
|
|
|
762
766
|
}
|
|
763
767
|
}
|
|
764
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
|
+
|
|
765
789
|
function renderSyncPlan(input: {
|
|
766
790
|
mode: SyncCommandResult['mode']
|
|
767
791
|
target: string
|
|
@@ -1025,13 +1049,7 @@ async function runSyncInk(
|
|
|
1025
1049
|
}
|
|
1026
1050
|
}
|
|
1027
1051
|
catch (error) {
|
|
1028
|
-
|
|
1029
|
-
throw createUserFacingError({
|
|
1030
|
-
code: `sync_${category}`,
|
|
1031
|
-
summary: error instanceof Error ? error.message : String(error),
|
|
1032
|
-
fixes: syncCategoryFixes(category),
|
|
1033
|
-
cause: error,
|
|
1034
|
-
})
|
|
1052
|
+
throw buildSyncFailureError(error)
|
|
1035
1053
|
}
|
|
1036
1054
|
},
|
|
1037
1055
|
}))
|
|
@@ -1203,13 +1221,7 @@ export async function runSync(
|
|
|
1203
1221
|
return result
|
|
1204
1222
|
}
|
|
1205
1223
|
catch (error) {
|
|
1206
|
-
|
|
1207
|
-
throw createUserFacingError({
|
|
1208
|
-
code: `sync_${category}`,
|
|
1209
|
-
summary: error instanceof Error ? error.message : String(error),
|
|
1210
|
-
fixes: syncCategoryFixes(category),
|
|
1211
|
-
cause: error,
|
|
1212
|
-
})
|
|
1224
|
+
throw buildSyncFailureError(error)
|
|
1213
1225
|
}
|
|
1214
1226
|
}
|
|
1215
1227
|
|
|
@@ -1857,8 +1869,10 @@ export async function runDoctor(
|
|
|
1857
1869
|
}
|
|
1858
1870
|
|
|
1859
1871
|
export interface ListRepositoriesResult {
|
|
1860
|
-
|
|
1861
|
-
repositories:
|
|
1872
|
+
success: boolean
|
|
1873
|
+
repositories: AvailableRepositorySummary[]
|
|
1874
|
+
message?: string
|
|
1875
|
+
nextActions?: string[]
|
|
1862
1876
|
}
|
|
1863
1877
|
|
|
1864
1878
|
export async function runListRepositories(
|
|
@@ -1867,35 +1881,76 @@ export async function runListRepositories(
|
|
|
1867
1881
|
): Promise<ListRepositoriesResult> {
|
|
1868
1882
|
const runtime = createRuntime(deps, options)
|
|
1869
1883
|
const inspection = (deps.inspectSavedConfig ?? inspectSavedConfig)()
|
|
1870
|
-
const
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
|
|
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
|
+
}
|
|
1874
1923
|
|
|
1875
1924
|
if (options.json) {
|
|
1876
1925
|
runtime.emitJson(result)
|
|
1877
1926
|
return result
|
|
1878
1927
|
}
|
|
1879
1928
|
|
|
1880
|
-
emitBootstrap(runtime, 'Tela CLI', 'Workspace repositories
|
|
1881
|
-
const decorate = runtime.decorate
|
|
1929
|
+
emitBootstrap(runtime, 'Tela CLI', 'Workspace repositories available to sync.')
|
|
1882
1930
|
const body = repositories.length > 0
|
|
1883
|
-
? section(
|
|
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
|
+
)
|
|
1884
1944
|
: section('Repositories', [
|
|
1885
|
-
|
|
1945
|
+
'No workspace repositories are currently registered for this workspace.',
|
|
1886
1946
|
'',
|
|
1887
|
-
'
|
|
1888
|
-
bulletList([
|
|
1889
|
-
'Run `tela setup` to configure the clone root.',
|
|
1890
|
-
'Run `tela sync <repo>` to clone a workspace repository.',
|
|
1891
|
-
]),
|
|
1947
|
+
'There is nothing to sync yet.',
|
|
1892
1948
|
], { decorate })
|
|
1893
1949
|
|
|
1894
1950
|
runtime.emit(joinBlocks([
|
|
1895
1951
|
body,
|
|
1896
|
-
section('Clone root', cloneRoot, { decorate }),
|
|
1897
1952
|
nextActions([
|
|
1898
|
-
'Run `tela sync <repo>` to clone or refresh
|
|
1953
|
+
'Run `tela sync <repo>` to clone or refresh an available repository.',
|
|
1899
1954
|
'Run `tela --help` to see all available commands.',
|
|
1900
1955
|
], { decorate }),
|
|
1901
1956
|
]))
|
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[]
|