@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.
Files changed (59) hide show
  1. package/README.md +56 -106
  2. package/dist/commands/auth.d.ts +2 -0
  3. package/dist/commands/context.d.ts +2 -0
  4. package/dist/commands/doctor-auth.d.ts +2 -0
  5. package/dist/commands/env.d.ts +2 -0
  6. package/dist/credentials.d.ts +2 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +7027 -0
  9. package/dist/output.d.ts +13 -0
  10. package/dist/program.d.ts +2 -0
  11. package/package.json +17 -24
  12. package/scripts/install-skills.cjs +127 -0
  13. package/skills/tela-cli/SKILL.md +20 -0
  14. package/skills/tela-cli/references/agent.md +111 -0
  15. package/cli.ts +0 -25
  16. package/postinstall.sh +0 -19
  17. package/src/agent-api.ts +0 -300
  18. package/src/assets.d.ts +0 -4
  19. package/src/auth.ts +0 -432
  20. package/src/claude.ts +0 -101
  21. package/src/commands/doctor.ts +0 -35
  22. package/src/commands/feedback.ts +0 -701
  23. package/src/commands/metadata.ts +0 -242
  24. package/src/commands/push.ts +0 -208
  25. package/src/commands/run.ts +0 -1699
  26. package/src/commands/status.ts +0 -109
  27. package/src/commands/sync.ts +0 -245
  28. package/src/commit-message.ts +0 -69
  29. package/src/config.ts +0 -799
  30. package/src/diagnostics.ts +0 -813
  31. package/src/errors.ts +0 -122
  32. package/src/forgejo.ts +0 -233
  33. package/src/git.ts +0 -457
  34. package/src/ink/assets/160092__jorickhoofd__dog-bark-1.mp3 +0 -0
  35. package/src/ink/bark.tsx +0 -241
  36. package/src/ink/components.tsx +0 -178
  37. package/src/ink/runtime.tsx +0 -90
  38. package/src/ink/screens/feedback-screen.tsx +0 -395
  39. package/src/ink/screens/push-screen.tsx +0 -232
  40. package/src/ink/screens/run-screen.tsx +0 -292
  41. package/src/ink/screens/setup/breadcrumb.tsx +0 -24
  42. package/src/ink/screens/setup/setup-screen.tsx +0 -612
  43. package/src/ink/screens/setup/step-frame.tsx +0 -31
  44. package/src/ink/screens/setup/steps/done-step.tsx +0 -29
  45. package/src/ink/screens/setup/steps/environment-step.tsx +0 -23
  46. package/src/ink/screens/setup/steps/organization-step.tsx +0 -35
  47. package/src/ink/screens/setup/steps/review-step.tsx +0 -78
  48. package/src/ink/screens/setup/steps/running-step.tsx +0 -37
  49. package/src/ink/screens/setup/steps/ssh-key-step.tsx +0 -48
  50. package/src/ink/screens/setup/steps/text-field-step.tsx +0 -18
  51. package/src/ink/screens/setup/steps.ts +0 -156
  52. package/src/ink/screens/setup/types.ts +0 -65
  53. package/src/ink/screens/sync-screen.tsx +0 -283
  54. package/src/process.ts +0 -251
  55. package/src/program.ts +0 -2105
  56. package/src/runtime.ts +0 -147
  57. package/src/types.ts +0 -165
  58. package/src/ui.ts +0 -295
  59. package/src/version.ts +0 -3
@@ -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
- }