@meistrari/tela-cli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +14 -1
- package/src/config.ts +40 -0
- package/src/diagnostics.ts +122 -9
- package/src/forgejo.ts +44 -0
- package/src/git.ts +252 -45
- package/src/ink/screens/setup/setup-screen.tsx +37 -0
- package/src/ink/screens/setup/steps/review-step.tsx +1 -0
- package/src/ink/screens/setup/steps.ts +11 -0
- package/src/ink/screens/setup/types.ts +2 -0
- package/src/program.ts +95 -33
- package/src/types.ts +22 -0
package/src/diagnostics.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
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'
|
|
10
|
+
import { resolveSshPrivateKeyPath } from './forgejo.ts'
|
|
5
11
|
import {
|
|
6
12
|
findRepositoryRoot,
|
|
7
13
|
getCurrentBranch,
|
|
@@ -57,6 +63,9 @@ export interface StatusReport {
|
|
|
57
63
|
workspace?: string
|
|
58
64
|
forgejoOrganization?: string
|
|
59
65
|
forgejoUserRegistered?: boolean
|
|
66
|
+
gitSshKeyTitle?: string
|
|
67
|
+
gitSshPublicKeyPath?: string
|
|
68
|
+
gitSshPrivateKeyPath?: string
|
|
60
69
|
accessToken?: string
|
|
61
70
|
sessionExpiresAt?: number
|
|
62
71
|
hasRefreshToken?: boolean
|
|
@@ -66,6 +75,9 @@ export interface StatusReport {
|
|
|
66
75
|
tools: {
|
|
67
76
|
git: boolean
|
|
68
77
|
claude: boolean
|
|
78
|
+
claudeVersion?: string
|
|
79
|
+
claudeProjectScopedRunSupported?: boolean | null
|
|
80
|
+
claudeDetail?: string
|
|
69
81
|
agentApiReachable: boolean | null
|
|
70
82
|
agentApiCategory?: AgentApiReachabilityCategory
|
|
71
83
|
agentApiStatus?: number
|
|
@@ -264,6 +276,8 @@ function buildStatusNextActions(
|
|
|
264
276
|
hasAuth: boolean,
|
|
265
277
|
hasForgejoOrganization: boolean,
|
|
266
278
|
hasForgejoAccess: boolean,
|
|
279
|
+
hasGitSshKey: boolean,
|
|
280
|
+
claudeFix?: string,
|
|
267
281
|
): string[] {
|
|
268
282
|
const actions: string[] = []
|
|
269
283
|
|
|
@@ -279,6 +293,9 @@ function buildStatusNextActions(
|
|
|
279
293
|
else if (!hasForgejoAccess) {
|
|
280
294
|
actions.push('Run `tela setup` again to provision Forgejo access with your SSH key.')
|
|
281
295
|
}
|
|
296
|
+
else if (!hasGitSshKey) {
|
|
297
|
+
actions.push('Run `tela setup` again to choose the SSH key Tela should force on Git operations.')
|
|
298
|
+
}
|
|
282
299
|
|
|
283
300
|
if (!repo.inRepository) {
|
|
284
301
|
actions.push('Run `tela sync <repo>` or change into a cloned repository before mutating commands.')
|
|
@@ -292,6 +309,10 @@ function buildStatusNextActions(
|
|
|
292
309
|
actions.push('Review local changes before syncing, or use `tela push` to stage and commit them.')
|
|
293
310
|
}
|
|
294
311
|
|
|
312
|
+
if (claudeFix) {
|
|
313
|
+
actions.push(claudeFix)
|
|
314
|
+
}
|
|
315
|
+
|
|
295
316
|
if (warnings.length === 0) {
|
|
296
317
|
actions.push('Run `tela sync --current`, `tela run`, or `tela push` when you are ready.')
|
|
297
318
|
}
|
|
@@ -303,15 +324,16 @@ export async function createStatusReport(
|
|
|
303
324
|
cwd = process.cwd(),
|
|
304
325
|
fetchFn: typeof fetch = fetch,
|
|
305
326
|
): Promise<StatusReport> {
|
|
306
|
-
const [configInspection, gitAvailable,
|
|
327
|
+
const [configInspection, gitAvailable, claudeInspection, repo] = await Promise.all([
|
|
307
328
|
Promise.resolve(inspectSavedConfig()),
|
|
308
329
|
commandExists('git'),
|
|
309
|
-
|
|
330
|
+
inspectClaudeCli(),
|
|
310
331
|
inspectCurrentRepository(cwd),
|
|
311
332
|
])
|
|
312
333
|
|
|
313
334
|
const warnings: string[] = []
|
|
314
335
|
let reachability: AgentApiReachability | undefined
|
|
336
|
+
let claudeFix: string | undefined
|
|
315
337
|
|
|
316
338
|
if (!configInspection.exists) {
|
|
317
339
|
warnings.push('Tela CLI is not configured yet.')
|
|
@@ -329,14 +351,35 @@ export async function createStatusReport(
|
|
|
329
351
|
if (!configInspection.config.forgejoUserRegistered) {
|
|
330
352
|
warnings.push('Forgejo user access is not provisioned yet.')
|
|
331
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
|
+
}
|
|
332
365
|
}
|
|
333
366
|
|
|
334
367
|
if (!gitAvailable) {
|
|
335
368
|
warnings.push('`git` is not available on PATH.')
|
|
336
369
|
}
|
|
337
370
|
|
|
338
|
-
if (!
|
|
371
|
+
if (!claudeInspection.available) {
|
|
339
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\`.`
|
|
340
383
|
}
|
|
341
384
|
|
|
342
385
|
if (repo.detached) {
|
|
@@ -362,6 +405,8 @@ export async function createStatusReport(
|
|
|
362
405
|
Boolean(configInspection.config?.auth),
|
|
363
406
|
Boolean(configInspection.config?.forgejoOrganization),
|
|
364
407
|
Boolean(configInspection.config?.forgejoUserRegistered),
|
|
408
|
+
Boolean(configInspection.config?.gitSshKey),
|
|
409
|
+
claudeFix,
|
|
365
410
|
)
|
|
366
411
|
|
|
367
412
|
return {
|
|
@@ -379,6 +424,9 @@ export async function createStatusReport(
|
|
|
379
424
|
workspace: configInspection.config?.auth?.workspace.title,
|
|
380
425
|
forgejoOrganization: configInspection.config?.forgejoOrganization?.name,
|
|
381
426
|
forgejoUserRegistered: configInspection.config?.forgejoUserRegistered,
|
|
427
|
+
gitSshKeyTitle: configInspection.config?.gitSshKey?.title,
|
|
428
|
+
gitSshPublicKeyPath: configInspection.config?.gitSshKey?.publicKeyPath,
|
|
429
|
+
gitSshPrivateKeyPath: configInspection.config?.gitSshKey?.privateKeyPath,
|
|
382
430
|
accessToken: configInspection.config?.auth ? redactSecret(configInspection.config.auth.session.accessToken) : undefined,
|
|
383
431
|
sessionExpiresAt: configInspection.config?.auth?.session.expiresAt,
|
|
384
432
|
hasRefreshToken: Boolean(configInspection.config?.auth?.session.refreshToken),
|
|
@@ -387,7 +435,12 @@ export async function createStatusReport(
|
|
|
387
435
|
repo,
|
|
388
436
|
tools: {
|
|
389
437
|
git: gitAvailable,
|
|
390
|
-
claude:
|
|
438
|
+
claude: claudeInspection.available,
|
|
439
|
+
claudeVersion: claudeInspection.version,
|
|
440
|
+
claudeProjectScopedRunSupported: claudeInspection.available
|
|
441
|
+
? claudeInspection.supportsProjectScopedRun
|
|
442
|
+
: null,
|
|
443
|
+
claudeDetail: claudeInspection.detail,
|
|
391
444
|
agentApiReachable: reachability?.reachable ?? null,
|
|
392
445
|
agentApiCategory: reachability?.category,
|
|
393
446
|
agentApiStatus: reachability?.statusCode,
|
|
@@ -474,6 +527,47 @@ function buildCloneRootCheck(cloneRoot: string | undefined): DoctorCheck {
|
|
|
474
527
|
}
|
|
475
528
|
}
|
|
476
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
|
+
|
|
477
571
|
function buildRepoChecks(repo: RepoSnapshot): DoctorCheck[] {
|
|
478
572
|
if (!repo.inRepository) {
|
|
479
573
|
return [
|
|
@@ -568,10 +662,10 @@ export async function createDoctorReport(
|
|
|
568
662
|
cwd = process.cwd(),
|
|
569
663
|
fetchFn: typeof fetch = fetch,
|
|
570
664
|
): Promise<DoctorReport> {
|
|
571
|
-
const [configInspection, gitAvailable,
|
|
665
|
+
const [configInspection, gitAvailable, claudeInspection, repo] = await Promise.all([
|
|
572
666
|
Promise.resolve(inspectSavedConfig()),
|
|
573
667
|
commandExists('git'),
|
|
574
|
-
|
|
668
|
+
inspectClaudeCli(),
|
|
575
669
|
inspectCurrentRepository(cwd),
|
|
576
670
|
])
|
|
577
671
|
|
|
@@ -611,9 +705,22 @@ export async function createDoctorReport(
|
|
|
611
705
|
{
|
|
612
706
|
id: 'tool-claude',
|
|
613
707
|
group: 'tools',
|
|
614
|
-
status:
|
|
615
|
-
|
|
616
|
-
|
|
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,
|
|
617
724
|
},
|
|
618
725
|
...buildRepoChecks(repo),
|
|
619
726
|
]
|
|
@@ -688,6 +795,12 @@ export async function createDoctorReport(
|
|
|
688
795
|
? ['No action required.']
|
|
689
796
|
: ['Run `tela setup` again and complete SSH-key provisioning for Forgejo access.'],
|
|
690
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
|
+
}))
|
|
691
804
|
}
|
|
692
805
|
|
|
693
806
|
const nextActions = buildNextActions(checks)
|
package/src/forgejo.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ForgejoUserCreateResponse,
|
|
9
9
|
ForgejoUserSshKeyInput,
|
|
10
10
|
TelaCliConfig,
|
|
11
|
+
TelaCliGitSshKey,
|
|
11
12
|
TelaCliForgejoOrganization,
|
|
12
13
|
} from './types.ts'
|
|
13
14
|
|
|
@@ -95,6 +96,49 @@ export function validateSshPublicKeyPath(value: string): string | undefined {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
export function inferSshPrivateKeyPath(publicKeyPath: string): string {
|
|
100
|
+
const expandedPath = expandHome(publicKeyPath.trim())
|
|
101
|
+
return expandedPath.endsWith('.pub')
|
|
102
|
+
? expandedPath.slice(0, -4)
|
|
103
|
+
: expandedPath
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function resolveSshPrivateKeyPath(filePath: string): string {
|
|
107
|
+
const expandedPath = expandHome(filePath.trim())
|
|
108
|
+
if (!expandedPath) {
|
|
109
|
+
throw new Error('SSH private key path is required')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!existsSync(expandedPath)) {
|
|
113
|
+
throw new Error(`SSH private key file does not exist: ${expandedPath}`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const stats = statSync(expandedPath)
|
|
117
|
+
if (!stats.isFile()) {
|
|
118
|
+
throw new Error(`SSH private key path is not a file: ${expandedPath}`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return expandedPath
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function validateSshPrivateKeyPath(value: string): string | undefined {
|
|
125
|
+
try {
|
|
126
|
+
resolveSshPrivateKeyPath(value)
|
|
127
|
+
return undefined
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return error instanceof Error ? error.message : 'SSH private key path is invalid'
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createGitSshKey(sshKey: SelectedSshPublicKey, privateKeyPath: string): TelaCliGitSshKey {
|
|
135
|
+
return {
|
|
136
|
+
title: sshKey.title,
|
|
137
|
+
publicKeyPath: sshKey.path,
|
|
138
|
+
privateKeyPath: resolveSshPrivateKeyPath(privateKeyPath),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
98
142
|
export function listDiscoveredSshPublicKeys(): SelectedSshPublicKey[] {
|
|
99
143
|
const sshDirectory = getSshDirectory()
|
|
100
144
|
if (!existsSync(sshDirectory)) {
|