@meistrari/tela-cli 0.7.0 → 0.7.4
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 +56 -106
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/doctor-auth.d.ts +2 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/credentials.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7027 -0
- package/dist/output.d.ts +13 -0
- package/dist/program.d.ts +2 -0
- package/package.json +17 -24
- package/scripts/install-skills.cjs +127 -0
- package/skills/tela-cli/SKILL.md +20 -0
- package/skills/tela-cli/references/agent.md +111 -0
- package/cli.ts +0 -25
- package/postinstall.sh +0 -19
- package/src/agent-api.ts +0 -300
- package/src/assets.d.ts +0 -4
- package/src/auth.ts +0 -432
- package/src/claude.ts +0 -101
- package/src/commands/doctor.ts +0 -35
- package/src/commands/feedback.ts +0 -701
- package/src/commands/metadata.ts +0 -242
- package/src/commands/push.ts +0 -208
- package/src/commands/run.ts +0 -1699
- package/src/commands/status.ts +0 -109
- package/src/commands/sync.ts +0 -245
- package/src/commit-message.ts +0 -69
- package/src/config.ts +0 -799
- package/src/diagnostics.ts +0 -813
- package/src/errors.ts +0 -122
- package/src/forgejo.ts +0 -233
- package/src/git.ts +0 -457
- package/src/ink/assets/160092__jorickhoofd__dog-bark-1.mp3 +0 -0
- package/src/ink/bark.tsx +0 -241
- package/src/ink/components.tsx +0 -178
- package/src/ink/runtime.tsx +0 -90
- package/src/ink/screens/feedback-screen.tsx +0 -395
- package/src/ink/screens/push-screen.tsx +0 -232
- package/src/ink/screens/run-screen.tsx +0 -292
- package/src/ink/screens/setup/breadcrumb.tsx +0 -24
- package/src/ink/screens/setup/setup-screen.tsx +0 -612
- package/src/ink/screens/setup/step-frame.tsx +0 -31
- package/src/ink/screens/setup/steps/done-step.tsx +0 -29
- package/src/ink/screens/setup/steps/environment-step.tsx +0 -23
- package/src/ink/screens/setup/steps/organization-step.tsx +0 -35
- package/src/ink/screens/setup/steps/review-step.tsx +0 -78
- package/src/ink/screens/setup/steps/running-step.tsx +0 -37
- package/src/ink/screens/setup/steps/ssh-key-step.tsx +0 -48
- package/src/ink/screens/setup/steps/text-field-step.tsx +0 -18
- package/src/ink/screens/setup/steps.ts +0 -156
- package/src/ink/screens/setup/types.ts +0 -65
- package/src/ink/screens/sync-screen.tsx +0 -283
- package/src/process.ts +0 -251
- package/src/program.ts +0 -2105
- package/src/runtime.ts +0 -147
- package/src/types.ts +0 -165
- package/src/ui.ts +0 -295
- package/src/version.ts +0 -3
package/src/diagnostics.ts
DELETED
|
@@ -1,813 +0,0 @@
|
|
|
1
|
-
import { accessSync, constants, existsSync, statSync } from 'node:fs'
|
|
2
|
-
import { dirname } from 'node:path'
|
|
3
|
-
import { buildAgentApiHeaders } from './auth.ts'
|
|
4
|
-
import {
|
|
5
|
-
formatClaudeCliVersion,
|
|
6
|
-
inspectClaudeCli,
|
|
7
|
-
MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN,
|
|
8
|
-
} from './claude.ts'
|
|
9
|
-
import { inspectSavedConfig, redactSecret } from './config.ts'
|
|
10
|
-
import { resolveSshPrivateKeyPath } from './forgejo.ts'
|
|
11
|
-
import {
|
|
12
|
-
findRepositoryRoot,
|
|
13
|
-
getCurrentBranch,
|
|
14
|
-
getHeadShortSha,
|
|
15
|
-
getOriginUrl,
|
|
16
|
-
getShortStatus,
|
|
17
|
-
getStatusCounts,
|
|
18
|
-
getUpstreamBranch,
|
|
19
|
-
} from './git.ts'
|
|
20
|
-
import { commandExists } from './process.ts'
|
|
21
|
-
|
|
22
|
-
export type DiagnosticStatus = 'pass' | 'warn' | 'fail'
|
|
23
|
-
export type AgentApiReachabilityCategory = 'invalid_url' | 'tls_error' | 'network_error' | 'http_response'
|
|
24
|
-
|
|
25
|
-
export interface AgentApiReachability {
|
|
26
|
-
reachable: boolean
|
|
27
|
-
category: AgentApiReachabilityCategory
|
|
28
|
-
statusCode?: number
|
|
29
|
-
detail: string
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface RepoSnapshot {
|
|
33
|
-
cwd: string
|
|
34
|
-
inRepository: boolean
|
|
35
|
-
repoRoot?: string
|
|
36
|
-
branch?: string
|
|
37
|
-
upstream?: string | null
|
|
38
|
-
detached: boolean
|
|
39
|
-
dirty: boolean
|
|
40
|
-
originUrl?: string | null
|
|
41
|
-
shortStatus?: string
|
|
42
|
-
head?: string
|
|
43
|
-
statusCounts?: {
|
|
44
|
-
staged: number
|
|
45
|
-
unstaged: number
|
|
46
|
-
untracked: number
|
|
47
|
-
total: number
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface StatusReport {
|
|
52
|
-
ok: boolean
|
|
53
|
-
config: {
|
|
54
|
-
present: boolean
|
|
55
|
-
valid: boolean
|
|
56
|
-
path: string
|
|
57
|
-
environment?: string
|
|
58
|
-
agentApiUrl?: string
|
|
59
|
-
authApiUrl?: string
|
|
60
|
-
telaApiUrl?: string
|
|
61
|
-
cloneRoot?: string
|
|
62
|
-
user?: string
|
|
63
|
-
workspace?: string
|
|
64
|
-
forgejoOrganization?: string
|
|
65
|
-
forgejoUserRegistered?: boolean
|
|
66
|
-
gitSshKeyTitle?: string
|
|
67
|
-
gitSshPublicKeyPath?: string
|
|
68
|
-
gitSshPrivateKeyPath?: string
|
|
69
|
-
accessToken?: string
|
|
70
|
-
sessionExpiresAt?: number
|
|
71
|
-
hasRefreshToken?: boolean
|
|
72
|
-
error?: string
|
|
73
|
-
}
|
|
74
|
-
repo: RepoSnapshot
|
|
75
|
-
tools: {
|
|
76
|
-
git: boolean
|
|
77
|
-
claude: boolean
|
|
78
|
-
claudeVersion?: string
|
|
79
|
-
claudeProjectScopedRunSupported?: boolean | null
|
|
80
|
-
claudeDetail?: string
|
|
81
|
-
agentApiReachable: boolean | null
|
|
82
|
-
agentApiCategory?: AgentApiReachabilityCategory
|
|
83
|
-
agentApiStatus?: number
|
|
84
|
-
agentApiDetail?: string
|
|
85
|
-
}
|
|
86
|
-
warnings: string[]
|
|
87
|
-
nextActions: string[]
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function formatSessionState(expiresAt: number | undefined, hasRefreshToken: boolean | undefined): string | undefined {
|
|
91
|
-
if (!expiresAt) {
|
|
92
|
-
return undefined
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const relative = expiresAt <= Date.now() ? 'expired' : 'active'
|
|
96
|
-
return `${relative}${hasRefreshToken ? ' · refresh token saved' : ''}`
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface DoctorCheck {
|
|
100
|
-
id: string
|
|
101
|
-
group: string
|
|
102
|
-
status: DiagnosticStatus
|
|
103
|
-
summary: string
|
|
104
|
-
fixes: string[]
|
|
105
|
-
evidence?: string[]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface DoctorReport {
|
|
109
|
-
ok: boolean
|
|
110
|
-
checks: DoctorCheck[]
|
|
111
|
-
nextActions: string[]
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function isDetachedHeadError(error: unknown): boolean {
|
|
115
|
-
return error instanceof Error && error.message.includes('Detached HEAD')
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function classifyAgentApiReachabilityError(message: string): AgentApiReachabilityCategory {
|
|
119
|
-
return /tls|ssl|certificate|hostname|subject name/i.test(message)
|
|
120
|
-
? 'tls_error'
|
|
121
|
-
: 'network_error'
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export async function checkAgentApiReachability(url: string, fetchFn: typeof fetch = fetch): Promise<AgentApiReachability> {
|
|
125
|
-
try {
|
|
126
|
-
new URL(url)
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
return {
|
|
130
|
-
reachable: false,
|
|
131
|
-
category: 'invalid_url',
|
|
132
|
-
detail: 'Agent API origin URL must be a valid URL',
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const response = await fetchFn(url, {
|
|
138
|
-
method: 'GET',
|
|
139
|
-
signal: AbortSignal.timeout(5_000),
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
const detail = response.statusText
|
|
143
|
-
? `HTTP ${response.status} ${response.statusText}`
|
|
144
|
-
: `HTTP ${response.status}`
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
reachable: true,
|
|
148
|
-
category: 'http_response',
|
|
149
|
-
statusCode: response.status,
|
|
150
|
-
detail,
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
const detail = error instanceof Error ? error.message : String(error)
|
|
155
|
-
return {
|
|
156
|
-
reachable: false,
|
|
157
|
-
category: classifyAgentApiReachabilityError(detail),
|
|
158
|
-
detail,
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function getAgentApiOriginCheckStatus(reachability: AgentApiReachability): string {
|
|
164
|
-
switch (reachability.category) {
|
|
165
|
-
case 'http_response':
|
|
166
|
-
return 'responded'
|
|
167
|
-
case 'invalid_url':
|
|
168
|
-
return 'invalid-url'
|
|
169
|
-
case 'tls_error':
|
|
170
|
-
return 'tls-error'
|
|
171
|
-
case 'network_error':
|
|
172
|
-
return 'network-error'
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function getAgentApiOriginWarning(reachability: AgentApiReachability): string | undefined {
|
|
177
|
-
switch (reachability.category) {
|
|
178
|
-
case 'http_response':
|
|
179
|
-
return undefined
|
|
180
|
-
case 'invalid_url':
|
|
181
|
-
return `Agent API origin URL is invalid: ${reachability.detail}`
|
|
182
|
-
case 'tls_error':
|
|
183
|
-
return `Agent API origin TLS check failed: ${reachability.detail}`
|
|
184
|
-
case 'network_error':
|
|
185
|
-
return `Agent API origin is unreachable: ${reachability.detail}`
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function getAgentApiOriginFixes(reachability: AgentApiReachability): string[] {
|
|
190
|
-
switch (reachability.category) {
|
|
191
|
-
case 'http_response':
|
|
192
|
-
return ['No action required.']
|
|
193
|
-
case 'invalid_url':
|
|
194
|
-
return ['Run `tela setup` and choose production, staging, or a valid custom Agent API origin URL.']
|
|
195
|
-
case 'tls_error':
|
|
196
|
-
return ['Check the configured hostname and TLS certificate, or choose the correct Agent API target in `tela setup`.']
|
|
197
|
-
case 'network_error':
|
|
198
|
-
return ['Check your network connection, VPN, or the configured Agent API origin in `tela setup`.']
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export function getAgentApiOriginSummary(url: string, reachability: AgentApiReachability): string {
|
|
203
|
-
switch (reachability.category) {
|
|
204
|
-
case 'http_response':
|
|
205
|
-
return `Agent API origin responded at ${url} (${reachability.detail})`
|
|
206
|
-
case 'invalid_url':
|
|
207
|
-
return `Agent API origin URL is invalid: ${url}`
|
|
208
|
-
case 'tls_error':
|
|
209
|
-
return `Agent API origin TLS check failed at ${url}`
|
|
210
|
-
case 'network_error':
|
|
211
|
-
return `Agent API origin is unreachable at ${url}`
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export async function inspectCurrentRepository(cwd = process.cwd()): Promise<RepoSnapshot> {
|
|
216
|
-
const repoRoot = await findRepositoryRoot(cwd)
|
|
217
|
-
if (!repoRoot) {
|
|
218
|
-
return {
|
|
219
|
-
cwd,
|
|
220
|
-
inRepository: false,
|
|
221
|
-
detached: false,
|
|
222
|
-
dirty: false,
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const shortStatus = await getShortStatus(repoRoot)
|
|
227
|
-
const dirty = Boolean(shortStatus.trim())
|
|
228
|
-
const statusCounts = await getStatusCounts(repoRoot)
|
|
229
|
-
const head = await getHeadShortSha(repoRoot)
|
|
230
|
-
|
|
231
|
-
let branch: string | undefined
|
|
232
|
-
let detached = false
|
|
233
|
-
try {
|
|
234
|
-
branch = await getCurrentBranch(repoRoot)
|
|
235
|
-
}
|
|
236
|
-
catch (error) {
|
|
237
|
-
if (!isDetachedHeadError(error)) {
|
|
238
|
-
throw error
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
detached = true
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
let upstream: string | null | undefined
|
|
245
|
-
if (!detached) {
|
|
246
|
-
upstream = await getUpstreamBranch(repoRoot)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
let originUrl: string | null = null
|
|
250
|
-
try {
|
|
251
|
-
originUrl = await getOriginUrl(repoRoot)
|
|
252
|
-
}
|
|
253
|
-
catch {
|
|
254
|
-
originUrl = null
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
cwd,
|
|
259
|
-
inRepository: true,
|
|
260
|
-
repoRoot,
|
|
261
|
-
branch,
|
|
262
|
-
upstream,
|
|
263
|
-
detached,
|
|
264
|
-
dirty,
|
|
265
|
-
originUrl,
|
|
266
|
-
shortStatus: shortStatus.trim() || undefined,
|
|
267
|
-
head,
|
|
268
|
-
statusCounts,
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function buildStatusNextActions(
|
|
273
|
-
warnings: string[],
|
|
274
|
-
repo: RepoSnapshot,
|
|
275
|
-
hasConfig: boolean,
|
|
276
|
-
hasAuth: boolean,
|
|
277
|
-
hasForgejoOrganization: boolean,
|
|
278
|
-
hasForgejoAccess: boolean,
|
|
279
|
-
hasGitSshKey: boolean,
|
|
280
|
-
claudeFix?: string,
|
|
281
|
-
): string[] {
|
|
282
|
-
const actions: string[] = []
|
|
283
|
-
|
|
284
|
-
if (!hasConfig) {
|
|
285
|
-
actions.push('Run `tela setup` to configure the CLI.')
|
|
286
|
-
}
|
|
287
|
-
else if (!hasAuth) {
|
|
288
|
-
actions.push('Run `tela setup` to sign in again.')
|
|
289
|
-
}
|
|
290
|
-
else if (!hasForgejoOrganization) {
|
|
291
|
-
actions.push('Run `tela setup` again to choose a workspace organization for Forgejo access.')
|
|
292
|
-
}
|
|
293
|
-
else if (!hasForgejoAccess) {
|
|
294
|
-
actions.push('Run `tela setup` again to provision Forgejo access with your SSH key.')
|
|
295
|
-
}
|
|
296
|
-
else if (!hasGitSshKey) {
|
|
297
|
-
actions.push('Run `tela setup` again to choose the SSH key Tela should force on Git operations.')
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (!repo.inRepository) {
|
|
301
|
-
actions.push('Run `tela sync <repo>` or change into a cloned repository before mutating commands.')
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (repo.detached) {
|
|
305
|
-
actions.push('Check out a branch before syncing or pushing.')
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (repo.dirty) {
|
|
309
|
-
actions.push('Review local changes before syncing, or use `tela push` to stage and commit them.')
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (claudeFix) {
|
|
313
|
-
actions.push(claudeFix)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (warnings.length === 0) {
|
|
317
|
-
actions.push('Run `tela sync --current`, `tela run`, or `tela push` when you are ready.')
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return Array.from(new Set(actions))
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export async function createStatusReport(
|
|
324
|
-
cwd = process.cwd(),
|
|
325
|
-
fetchFn: typeof fetch = fetch,
|
|
326
|
-
): Promise<StatusReport> {
|
|
327
|
-
const [configInspection, gitAvailable, claudeInspection, repo] = await Promise.all([
|
|
328
|
-
Promise.resolve(inspectSavedConfig()),
|
|
329
|
-
commandExists('git'),
|
|
330
|
-
inspectClaudeCli(),
|
|
331
|
-
inspectCurrentRepository(cwd),
|
|
332
|
-
])
|
|
333
|
-
|
|
334
|
-
const warnings: string[] = []
|
|
335
|
-
let reachability: AgentApiReachability | undefined
|
|
336
|
-
let claudeFix: string | undefined
|
|
337
|
-
|
|
338
|
-
if (!configInspection.exists) {
|
|
339
|
-
warnings.push('Tela CLI is not configured yet.')
|
|
340
|
-
}
|
|
341
|
-
else if (!configInspection.valid) {
|
|
342
|
-
warnings.push(`Tela CLI config is invalid: ${configInspection.error ?? 'unknown error'}`)
|
|
343
|
-
}
|
|
344
|
-
else if (!configInspection.config?.auth) {
|
|
345
|
-
warnings.push('Tela CLI is signed out.')
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
if (!configInspection.config.forgejoOrganization) {
|
|
349
|
-
warnings.push('Forgejo organization selection is missing.')
|
|
350
|
-
}
|
|
351
|
-
if (!configInspection.config.forgejoUserRegistered) {
|
|
352
|
-
warnings.push('Forgejo user access is not provisioned yet.')
|
|
353
|
-
}
|
|
354
|
-
if (!configInspection.config.gitSshKey) {
|
|
355
|
-
warnings.push('Git SSH key selection is missing.')
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
try {
|
|
359
|
-
resolveSshPrivateKeyPath(configInspection.config.gitSshKey.privateKeyPath)
|
|
360
|
-
}
|
|
361
|
-
catch (error) {
|
|
362
|
-
warnings.push(`Configured Git SSH private key is unavailable: ${error instanceof Error ? error.message : String(error)}`)
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (!gitAvailable) {
|
|
368
|
-
warnings.push('`git` is not available on PATH.')
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (!claudeInspection.available) {
|
|
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\`.`
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (repo.detached) {
|
|
386
|
-
warnings.push('The current repository is on a detached HEAD.')
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (repo.dirty) {
|
|
390
|
-
warnings.push('The current repository has local changes.')
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (configInspection.valid && configInspection.config) {
|
|
394
|
-
reachability = await checkAgentApiReachability(configInspection.config.agentApiUrl, fetchFn)
|
|
395
|
-
const warning = getAgentApiOriginWarning(reachability)
|
|
396
|
-
if (warning) {
|
|
397
|
-
warnings.push(warning)
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const nextActions = buildStatusNextActions(
|
|
402
|
-
warnings,
|
|
403
|
-
repo,
|
|
404
|
-
Boolean(configInspection.valid),
|
|
405
|
-
Boolean(configInspection.config?.auth),
|
|
406
|
-
Boolean(configInspection.config?.forgejoOrganization),
|
|
407
|
-
Boolean(configInspection.config?.forgejoUserRegistered),
|
|
408
|
-
Boolean(configInspection.config?.gitSshKey),
|
|
409
|
-
claudeFix,
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
return {
|
|
413
|
-
ok: warnings.length === 0,
|
|
414
|
-
config: {
|
|
415
|
-
present: configInspection.exists,
|
|
416
|
-
valid: configInspection.valid,
|
|
417
|
-
path: configInspection.path,
|
|
418
|
-
environment: configInspection.config?.environment,
|
|
419
|
-
agentApiUrl: configInspection.config?.agentApiUrl,
|
|
420
|
-
authApiUrl: configInspection.config?.authApiUrl,
|
|
421
|
-
telaApiUrl: configInspection.config?.telaApiUrl,
|
|
422
|
-
cloneRoot: configInspection.config?.cloneRoot,
|
|
423
|
-
user: configInspection.config?.auth?.user.email,
|
|
424
|
-
workspace: configInspection.config?.auth?.workspace.title,
|
|
425
|
-
forgejoOrganization: configInspection.config?.forgejoOrganization?.name,
|
|
426
|
-
forgejoUserRegistered: configInspection.config?.forgejoUserRegistered,
|
|
427
|
-
gitSshKeyTitle: configInspection.config?.gitSshKey?.title,
|
|
428
|
-
gitSshPublicKeyPath: configInspection.config?.gitSshKey?.publicKeyPath,
|
|
429
|
-
gitSshPrivateKeyPath: configInspection.config?.gitSshKey?.privateKeyPath,
|
|
430
|
-
accessToken: configInspection.config?.auth ? redactSecret(configInspection.config.auth.session.accessToken) : undefined,
|
|
431
|
-
sessionExpiresAt: configInspection.config?.auth?.session.expiresAt,
|
|
432
|
-
hasRefreshToken: Boolean(configInspection.config?.auth?.session.refreshToken),
|
|
433
|
-
error: configInspection.error,
|
|
434
|
-
},
|
|
435
|
-
repo,
|
|
436
|
-
tools: {
|
|
437
|
-
git: gitAvailable,
|
|
438
|
-
claude: claudeInspection.available,
|
|
439
|
-
claudeVersion: claudeInspection.version,
|
|
440
|
-
claudeProjectScopedRunSupported: claudeInspection.available
|
|
441
|
-
? claudeInspection.supportsProjectScopedRun
|
|
442
|
-
: null,
|
|
443
|
-
claudeDetail: claudeInspection.detail,
|
|
444
|
-
agentApiReachable: reachability?.reachable ?? null,
|
|
445
|
-
agentApiCategory: reachability?.category,
|
|
446
|
-
agentApiStatus: reachability?.statusCode,
|
|
447
|
-
agentApiDetail: reachability?.detail,
|
|
448
|
-
},
|
|
449
|
-
warnings,
|
|
450
|
-
nextActions,
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function buildCloneRootCheck(cloneRoot: string | undefined): DoctorCheck {
|
|
455
|
-
if (!cloneRoot) {
|
|
456
|
-
return {
|
|
457
|
-
id: 'clone-root',
|
|
458
|
-
group: 'filesystem',
|
|
459
|
-
status: 'warn',
|
|
460
|
-
summary: 'Clone root is unavailable because config is missing or invalid.',
|
|
461
|
-
fixes: ['Run `tela setup` to configure a clone root.'],
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (existsSync(cloneRoot)) {
|
|
466
|
-
const stats = statSync(cloneRoot)
|
|
467
|
-
if (!stats.isDirectory()) {
|
|
468
|
-
return {
|
|
469
|
-
id: 'clone-root',
|
|
470
|
-
group: 'filesystem',
|
|
471
|
-
status: 'fail',
|
|
472
|
-
summary: `Clone root exists but is not a directory: ${cloneRoot}`,
|
|
473
|
-
fixes: [
|
|
474
|
-
`Move or remove \`${cloneRoot}\`, then rerun \`tela setup\`.`,
|
|
475
|
-
],
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
try {
|
|
480
|
-
accessSync(cloneRoot, constants.R_OK | constants.W_OK | constants.X_OK)
|
|
481
|
-
return {
|
|
482
|
-
id: 'clone-root',
|
|
483
|
-
group: 'filesystem',
|
|
484
|
-
status: 'pass',
|
|
485
|
-
summary: `Clone root is ready: ${cloneRoot}`,
|
|
486
|
-
fixes: ['No action required.'],
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
catch (error) {
|
|
490
|
-
return {
|
|
491
|
-
id: 'clone-root',
|
|
492
|
-
group: 'filesystem',
|
|
493
|
-
status: 'fail',
|
|
494
|
-
summary: `Clone root is not writable: ${cloneRoot}`,
|
|
495
|
-
fixes: [
|
|
496
|
-
`Fix permissions for \`${cloneRoot}\` or choose another clone root in \`tela setup\`.`,
|
|
497
|
-
],
|
|
498
|
-
evidence: [error instanceof Error ? error.message : String(error)],
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const parentDir = dirname(cloneRoot)
|
|
504
|
-
try {
|
|
505
|
-
accessSync(parentDir, constants.W_OK | constants.X_OK)
|
|
506
|
-
return {
|
|
507
|
-
id: 'clone-root',
|
|
508
|
-
group: 'filesystem',
|
|
509
|
-
status: 'warn',
|
|
510
|
-
summary: `Clone root does not exist yet: ${cloneRoot}`,
|
|
511
|
-
fixes: [
|
|
512
|
-
`Create it with \`mkdir -p ${cloneRoot}\` or rerun \`tela setup\`.`,
|
|
513
|
-
],
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
catch (error) {
|
|
517
|
-
return {
|
|
518
|
-
id: 'clone-root',
|
|
519
|
-
group: 'filesystem',
|
|
520
|
-
status: 'fail',
|
|
521
|
-
summary: `Clone root parent is not writable: ${parentDir}`,
|
|
522
|
-
fixes: [
|
|
523
|
-
`Fix permissions for \`${parentDir}\` or choose another clone root in \`tela setup\`.`,
|
|
524
|
-
],
|
|
525
|
-
evidence: [error instanceof Error ? error.message : String(error)],
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function buildGitSshKeyCheck(gitSshKey: {
|
|
531
|
-
title?: string
|
|
532
|
-
publicKeyPath?: string
|
|
533
|
-
privateKeyPath?: string
|
|
534
|
-
}): DoctorCheck {
|
|
535
|
-
if (!gitSshKey.privateKeyPath) {
|
|
536
|
-
return {
|
|
537
|
-
id: 'git-ssh-key',
|
|
538
|
-
group: 'access',
|
|
539
|
-
status: 'warn',
|
|
540
|
-
summary: 'No Git SSH key is configured.',
|
|
541
|
-
fixes: ['Run `tela setup` again and choose the SSH key to force on Git operations.'],
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
try {
|
|
546
|
-
const privateKeyPath = resolveSshPrivateKeyPath(gitSshKey.privateKeyPath)
|
|
547
|
-
return {
|
|
548
|
-
id: 'git-ssh-key',
|
|
549
|
-
group: 'access',
|
|
550
|
-
status: 'pass',
|
|
551
|
-
summary: `Git SSH key is configured: ${gitSshKey.title ?? '<unnamed>'}.`,
|
|
552
|
-
fixes: ['No action required.'],
|
|
553
|
-
evidence: [
|
|
554
|
-
gitSshKey.publicKeyPath ? `public key: ${gitSshKey.publicKeyPath}` : '',
|
|
555
|
-
`private key: ${privateKeyPath}`,
|
|
556
|
-
].filter(Boolean),
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
catch (error) {
|
|
560
|
-
return {
|
|
561
|
-
id: 'git-ssh-key',
|
|
562
|
-
group: 'access',
|
|
563
|
-
status: 'fail',
|
|
564
|
-
summary: 'Configured Git SSH private key is unavailable.',
|
|
565
|
-
fixes: ['Restore the private key file or run `tela setup` again and choose another SSH key.'],
|
|
566
|
-
evidence: [error instanceof Error ? error.message : String(error)],
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function buildRepoChecks(repo: RepoSnapshot): DoctorCheck[] {
|
|
572
|
-
if (!repo.inRepository) {
|
|
573
|
-
return [
|
|
574
|
-
{
|
|
575
|
-
id: 'repo-context',
|
|
576
|
-
group: 'repository',
|
|
577
|
-
status: 'warn',
|
|
578
|
-
summary: 'Current directory is not inside a git repository.',
|
|
579
|
-
fixes: [
|
|
580
|
-
'Change into a Tela repository or run `tela sync <repo>` first.',
|
|
581
|
-
],
|
|
582
|
-
},
|
|
583
|
-
]
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const checks: DoctorCheck[] = [
|
|
587
|
-
{
|
|
588
|
-
id: 'repo-context',
|
|
589
|
-
group: 'repository',
|
|
590
|
-
status: 'pass',
|
|
591
|
-
summary: `Inside git repository: ${repo.repoRoot}`,
|
|
592
|
-
fixes: ['No action required.'],
|
|
593
|
-
},
|
|
594
|
-
]
|
|
595
|
-
|
|
596
|
-
checks.push({
|
|
597
|
-
id: 'repo-head',
|
|
598
|
-
group: 'repository',
|
|
599
|
-
status: repo.detached ? 'fail' : 'pass',
|
|
600
|
-
summary: repo.detached
|
|
601
|
-
? 'Repository is on a detached HEAD.'
|
|
602
|
-
: `Current branch is ${repo.branch}.`,
|
|
603
|
-
fixes: repo.detached
|
|
604
|
-
? ['Check out a branch before syncing or pushing.']
|
|
605
|
-
: ['No action required.'],
|
|
606
|
-
})
|
|
607
|
-
|
|
608
|
-
checks.push({
|
|
609
|
-
id: 'repo-worktree',
|
|
610
|
-
group: 'repository',
|
|
611
|
-
status: repo.dirty ? 'warn' : 'pass',
|
|
612
|
-
summary: repo.dirty
|
|
613
|
-
? 'Repository has local changes.'
|
|
614
|
-
: 'Repository worktree is clean.',
|
|
615
|
-
fixes: repo.dirty
|
|
616
|
-
? ['Review the local diff before syncing, or use `tela push` to stage and commit changes.']
|
|
617
|
-
: ['No action required.'],
|
|
618
|
-
evidence: repo.shortStatus ? [repo.shortStatus] : undefined,
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
checks.push({
|
|
622
|
-
id: 'repo-upstream',
|
|
623
|
-
group: 'repository',
|
|
624
|
-
status: repo.detached ? 'warn' : (repo.upstream ? 'pass' : 'warn'),
|
|
625
|
-
summary: repo.detached
|
|
626
|
-
? 'Upstream check skipped because HEAD is detached.'
|
|
627
|
-
: (repo.upstream
|
|
628
|
-
? `Upstream branch is ${repo.upstream}.`
|
|
629
|
-
: 'Current branch does not track an upstream.'),
|
|
630
|
-
fixes: repo.detached
|
|
631
|
-
? ['Check out a branch before configuring tracking.']
|
|
632
|
-
: (repo.upstream
|
|
633
|
-
? ['No action required.']
|
|
634
|
-
: ['Push the branch once or run `git branch --set-upstream-to origin/<branch>` after creating the remote branch.']),
|
|
635
|
-
})
|
|
636
|
-
|
|
637
|
-
checks.push({
|
|
638
|
-
id: 'repo-origin',
|
|
639
|
-
group: 'repository',
|
|
640
|
-
status: repo.originUrl ? 'pass' : 'warn',
|
|
641
|
-
summary: repo.originUrl
|
|
642
|
-
? `Origin remote: ${repo.originUrl}`
|
|
643
|
-
: 'Origin remote is missing.',
|
|
644
|
-
fixes: repo.originUrl
|
|
645
|
-
? ['No action required.']
|
|
646
|
-
: ['Add an origin remote or run `tela sync <repo>` to clone a fresh checkout.'],
|
|
647
|
-
})
|
|
648
|
-
|
|
649
|
-
return checks
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function buildNextActions(checks: DoctorCheck[]): string[] {
|
|
653
|
-
const failing = checks.filter(check => check.status !== 'pass')
|
|
654
|
-
if (failing.length === 0) {
|
|
655
|
-
return ['Environment looks healthy. Run `tela status`, `tela sync`, `tela run`, or `tela push` as needed.']
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return Array.from(new Set(failing.flatMap(check => check.fixes))).slice(0, 5)
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
export async function createDoctorReport(
|
|
662
|
-
cwd = process.cwd(),
|
|
663
|
-
fetchFn: typeof fetch = fetch,
|
|
664
|
-
): Promise<DoctorReport> {
|
|
665
|
-
const [configInspection, gitAvailable, claudeInspection, repo] = await Promise.all([
|
|
666
|
-
Promise.resolve(inspectSavedConfig()),
|
|
667
|
-
commandExists('git'),
|
|
668
|
-
inspectClaudeCli(),
|
|
669
|
-
inspectCurrentRepository(cwd),
|
|
670
|
-
])
|
|
671
|
-
|
|
672
|
-
const checks: DoctorCheck[] = [
|
|
673
|
-
configInspection.exists
|
|
674
|
-
? (configInspection.valid
|
|
675
|
-
? {
|
|
676
|
-
id: 'config',
|
|
677
|
-
group: 'config',
|
|
678
|
-
status: 'pass',
|
|
679
|
-
summary: `Config loaded from ${configInspection.path}`,
|
|
680
|
-
fixes: ['No action required.'],
|
|
681
|
-
}
|
|
682
|
-
: {
|
|
683
|
-
id: 'config',
|
|
684
|
-
group: 'config',
|
|
685
|
-
status: 'fail',
|
|
686
|
-
summary: `Config file is invalid: ${configInspection.path}`,
|
|
687
|
-
fixes: ['Run `tela setup` to rewrite the config file.'],
|
|
688
|
-
evidence: configInspection.error ? [configInspection.error] : undefined,
|
|
689
|
-
})
|
|
690
|
-
: {
|
|
691
|
-
id: 'config',
|
|
692
|
-
group: 'config',
|
|
693
|
-
status: 'fail',
|
|
694
|
-
summary: 'Tela CLI config file is missing.',
|
|
695
|
-
fixes: ['Run `tela setup` to create the config file.'],
|
|
696
|
-
},
|
|
697
|
-
buildCloneRootCheck(configInspection.config?.cloneRoot),
|
|
698
|
-
{
|
|
699
|
-
id: 'tool-git',
|
|
700
|
-
group: 'tools',
|
|
701
|
-
status: gitAvailable ? 'pass' : 'fail',
|
|
702
|
-
summary: gitAvailable ? '`git` is available.' : '`git` is missing from PATH.',
|
|
703
|
-
fixes: gitAvailable ? ['No action required.'] : ['Install `git` and make sure it is available on PATH.'],
|
|
704
|
-
},
|
|
705
|
-
{
|
|
706
|
-
id: 'tool-claude',
|
|
707
|
-
group: 'tools',
|
|
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,
|
|
724
|
-
},
|
|
725
|
-
...buildRepoChecks(repo),
|
|
726
|
-
]
|
|
727
|
-
|
|
728
|
-
if (configInspection.valid && configInspection.config) {
|
|
729
|
-
const reachability = await checkAgentApiReachability(configInspection.config.agentApiUrl, fetchFn)
|
|
730
|
-
checks.push({
|
|
731
|
-
id: 'agent-api-origin',
|
|
732
|
-
group: 'network',
|
|
733
|
-
status: reachability.reachable ? 'pass' : 'warn',
|
|
734
|
-
summary: getAgentApiOriginSummary(configInspection.config.agentApiUrl, reachability),
|
|
735
|
-
fixes: getAgentApiOriginFixes(reachability),
|
|
736
|
-
evidence: reachability.reachable ? undefined : [reachability.detail],
|
|
737
|
-
})
|
|
738
|
-
|
|
739
|
-
if (configInspection.config.auth) {
|
|
740
|
-
try {
|
|
741
|
-
const headers = await buildAgentApiHeaders(configInspection.config, { fetchFn })
|
|
742
|
-
checks.push({
|
|
743
|
-
id: 'auth-session',
|
|
744
|
-
group: 'auth',
|
|
745
|
-
status: 'pass',
|
|
746
|
-
summary: `Saved session is usable for ${configInspection.config.auth.user.email} in ${configInspection.config.auth.workspace.title}.`,
|
|
747
|
-
fixes: ['No action required.'],
|
|
748
|
-
evidence: [
|
|
749
|
-
`data-token header: ${redactSecret(headers['x-data-token'])}`,
|
|
750
|
-
],
|
|
751
|
-
})
|
|
752
|
-
}
|
|
753
|
-
catch (error) {
|
|
754
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
755
|
-
checks.push({
|
|
756
|
-
id: 'auth-session',
|
|
757
|
-
group: 'auth',
|
|
758
|
-
status: 'fail',
|
|
759
|
-
summary: 'Saved Tela session could not be refreshed or exchanged for a data token.',
|
|
760
|
-
fixes: ['Run `tela setup` in an interactive terminal to sign in again.'],
|
|
761
|
-
evidence: [message],
|
|
762
|
-
})
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
else {
|
|
766
|
-
checks.push({
|
|
767
|
-
id: 'auth-session',
|
|
768
|
-
group: 'auth',
|
|
769
|
-
status: 'fail',
|
|
770
|
-
summary: 'No saved Tela session is available.',
|
|
771
|
-
fixes: ['Run `tela setup` in an interactive terminal to sign in.'],
|
|
772
|
-
})
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
checks.push({
|
|
776
|
-
id: 'forgejo-organization',
|
|
777
|
-
group: 'access',
|
|
778
|
-
status: configInspection.config.forgejoOrganization ? 'pass' : 'warn',
|
|
779
|
-
summary: configInspection.config.forgejoOrganization
|
|
780
|
-
? `Forgejo organization is selected: ${configInspection.config.forgejoOrganization.name}.`
|
|
781
|
-
: 'No Forgejo organization is selected.',
|
|
782
|
-
fixes: configInspection.config.forgejoOrganization
|
|
783
|
-
? ['No action required.']
|
|
784
|
-
: ['Run `tela setup` again and choose a workspace organization for Forgejo access.'],
|
|
785
|
-
})
|
|
786
|
-
|
|
787
|
-
checks.push({
|
|
788
|
-
id: 'forgejo-user',
|
|
789
|
-
group: 'access',
|
|
790
|
-
status: configInspection.config.forgejoUserRegistered ? 'pass' : 'warn',
|
|
791
|
-
summary: configInspection.config.forgejoUserRegistered
|
|
792
|
-
? 'Forgejo user access is provisioned.'
|
|
793
|
-
: 'Forgejo user access has not been provisioned yet.',
|
|
794
|
-
fixes: configInspection.config.forgejoUserRegistered
|
|
795
|
-
? ['No action required.']
|
|
796
|
-
: ['Run `tela setup` again and complete SSH-key provisioning for Forgejo access.'],
|
|
797
|
-
})
|
|
798
|
-
|
|
799
|
-
checks.push(buildGitSshKeyCheck({
|
|
800
|
-
title: configInspection.config.gitSshKey?.title,
|
|
801
|
-
publicKeyPath: configInspection.config.gitSshKey?.publicKeyPath,
|
|
802
|
-
privateKeyPath: configInspection.config.gitSshKey?.privateKeyPath,
|
|
803
|
-
}))
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
const nextActions = buildNextActions(checks)
|
|
807
|
-
|
|
808
|
-
return {
|
|
809
|
-
ok: checks.every(check => check.status === 'pass'),
|
|
810
|
-
checks,
|
|
811
|
-
nextActions,
|
|
812
|
-
}
|
|
813
|
-
}
|