@meistrari/tela-cli 0.3.0 → 0.4.1
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/package.json +1 -1
- package/src/commands/status.ts +3 -0
- package/src/config.ts +40 -0
- package/src/diagnostics.ts +70 -0
- package/src/forgejo.ts +44 -0
- package/src/git.ts +221 -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 +11 -14
- package/src/types.ts +7 -0
package/package.json
CHANGED
package/src/commands/status.ts
CHANGED
|
@@ -31,6 +31,9 @@ export function renderStatusReport(report: StatusReport, options: RenderOptions
|
|
|
31
31
|
{ label: 'Workspace', value: report.config.workspace ?? '<signed out>' },
|
|
32
32
|
{ label: 'Organization', value: report.config.forgejoOrganization ?? '<not selected>' },
|
|
33
33
|
{ label: 'Forgejo user', value: report.config.forgejoUserRegistered == null ? '<unknown>' : (report.config.forgejoUserRegistered ? 'registered' : 'pending') },
|
|
34
|
+
{ label: 'Git SSH key', value: report.config.gitSshKeyTitle ?? '<missing>' },
|
|
35
|
+
{ label: 'Git SSH public key', value: report.config.gitSshPublicKeyPath ?? '<missing>' },
|
|
36
|
+
{ label: 'Git SSH private key', value: report.config.gitSshPrivateKeyPath ?? '<missing>' },
|
|
34
37
|
{ label: 'Access token', value: report.config.accessToken ?? '<missing>' },
|
|
35
38
|
{ label: 'Session expiry', value: report.config.sessionExpiresAt ? new Date(report.config.sessionExpiresAt).toISOString() : '<missing>' },
|
|
36
39
|
{ label: 'Refresh token', value: report.config.hasRefreshToken ? 'present' : 'missing' },
|
package/src/config.ts
CHANGED
|
@@ -5,9 +5,13 @@ import { listOrganizations, provisionForgejoUser } from './agent-api.ts'
|
|
|
5
5
|
import type { FetchLike } from './auth.ts'
|
|
6
6
|
import { createUserFacingError } from './errors.ts'
|
|
7
7
|
import {
|
|
8
|
+
createGitSshKey,
|
|
8
9
|
ensureForgejoUserAccess,
|
|
10
|
+
inferSshPrivateKeyPath,
|
|
9
11
|
listDiscoveredSshPublicKeys,
|
|
10
12
|
loadSshPublicKey,
|
|
13
|
+
resolveSshPrivateKeyPath,
|
|
14
|
+
validateSshPrivateKeyPath,
|
|
11
15
|
validateSshPublicKeyPath,
|
|
12
16
|
type SelectedSshPublicKey,
|
|
13
17
|
} from './forgejo.ts'
|
|
@@ -15,6 +19,7 @@ import type {
|
|
|
15
19
|
TelaCliAuthState,
|
|
16
20
|
TelaCliConfig,
|
|
17
21
|
TelaCliForgejoOrganization,
|
|
22
|
+
TelaCliGitSshKey,
|
|
18
23
|
TelaEnvironment,
|
|
19
24
|
} from './types.ts'
|
|
20
25
|
|
|
@@ -213,6 +218,18 @@ function normalizeForgejoOrganization(
|
|
|
213
218
|
}
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
function normalizeGitSshKey(gitSshKey: TelaCliConfig['gitSshKey']): TelaCliGitSshKey | null {
|
|
222
|
+
if (!gitSshKey) {
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
title: ensureNonEmpty(String(gitSshKey.title ?? ''), 'Git SSH key title'),
|
|
228
|
+
publicKeyPath: expandHome(ensureNonEmpty(String(gitSshKey.publicKeyPath ?? ''), 'Git SSH public key path')),
|
|
229
|
+
privateKeyPath: expandHome(ensureNonEmpty(String(gitSshKey.privateKeyPath ?? ''), 'Git SSH private key path')),
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
216
233
|
function normalizeConfig(config: TelaCliConfig): TelaCliConfig {
|
|
217
234
|
const environment = ensureEnvironment(config.environment)
|
|
218
235
|
const auth = normalizeAuth(config.auth)
|
|
@@ -229,6 +246,7 @@ function normalizeConfig(config: TelaCliConfig): TelaCliConfig {
|
|
|
229
246
|
auth,
|
|
230
247
|
forgejoOrganization: auth ? normalizeForgejoOrganization(config.forgejoOrganization) : null,
|
|
231
248
|
forgejoUserRegistered: Boolean(config.forgejoUserRegistered),
|
|
249
|
+
gitSshKey: normalizeGitSshKey(config.gitSshKey),
|
|
232
250
|
}
|
|
233
251
|
}
|
|
234
252
|
|
|
@@ -257,6 +275,7 @@ function parseJsonConfig(input: string): TelaCliConfig {
|
|
|
257
275
|
auth,
|
|
258
276
|
forgejoOrganization: parsed.forgejoOrganization as TelaCliConfig['forgejoOrganization'] ?? null,
|
|
259
277
|
forgejoUserRegistered: Boolean(parsed.forgejoUserRegistered ?? false),
|
|
278
|
+
gitSshKey: parsed.gitSshKey as TelaCliConfig['gitSshKey'] ?? null,
|
|
260
279
|
})
|
|
261
280
|
}
|
|
262
281
|
|
|
@@ -317,6 +336,25 @@ async function promptForSshPublicKey(prompts: PromptModule): Promise<SelectedSsh
|
|
|
317
336
|
return loadSshPublicKey(promptValue(customPath))
|
|
318
337
|
}
|
|
319
338
|
|
|
339
|
+
async function promptForSshPrivateKeyPath(
|
|
340
|
+
prompts: PromptModule,
|
|
341
|
+
sshKey: SelectedSshPublicKey,
|
|
342
|
+
): Promise<string> {
|
|
343
|
+
const privateKeyPath = await prompts.text({
|
|
344
|
+
message: 'SSH private key path',
|
|
345
|
+
defaultValue: inferSshPrivateKeyPath(sshKey.path),
|
|
346
|
+
placeholder: sshKey.path.endsWith('.pub')
|
|
347
|
+
? sshKey.path.slice(0, -4)
|
|
348
|
+
: '~/.ssh/id_ed25519',
|
|
349
|
+
validate: validateSshPrivateKeyPath,
|
|
350
|
+
})
|
|
351
|
+
if (prompts.isCancel(privateKeyPath)) {
|
|
352
|
+
cancelSetup(prompts)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return resolveSshPrivateKeyPath(promptValue(privateKeyPath))
|
|
356
|
+
}
|
|
357
|
+
|
|
320
358
|
function getHomeDir(): string {
|
|
321
359
|
return process.env.HOME || homedir()
|
|
322
360
|
}
|
|
@@ -700,6 +738,7 @@ export async function setupConfigInteractive(options: {
|
|
|
700
738
|
}
|
|
701
739
|
|
|
702
740
|
const sshKey = await promptForSshPublicKey(prompts)
|
|
741
|
+
const privateKeyPath = await promptForSshPrivateKeyPath(prompts, sshKey)
|
|
703
742
|
|
|
704
743
|
const resolvedSetup = {
|
|
705
744
|
...resolveEnvironmentConfigForPreset(environment, customValues),
|
|
@@ -718,6 +757,7 @@ export async function setupConfigInteractive(options: {
|
|
|
718
757
|
auth,
|
|
719
758
|
forgejoOrganization: null,
|
|
720
759
|
forgejoUserRegistered: false,
|
|
760
|
+
gitSshKey: createGitSshKey(sshKey, privateKeyPath),
|
|
721
761
|
})
|
|
722
762
|
const organization = await resolveForgejoOrganizationInteractive({
|
|
723
763
|
prompts,
|
package/src/diagnostics.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { accessSync, constants, existsSync, statSync } from 'node:fs'
|
|
|
2
2
|
import { dirname } from 'node:path'
|
|
3
3
|
import { buildAgentApiHeaders } from './auth.ts'
|
|
4
4
|
import { inspectSavedConfig, redactSecret } from './config.ts'
|
|
5
|
+
import { resolveSshPrivateKeyPath } from './forgejo.ts'
|
|
5
6
|
import {
|
|
6
7
|
findRepositoryRoot,
|
|
7
8
|
getCurrentBranch,
|
|
@@ -57,6 +58,9 @@ export interface StatusReport {
|
|
|
57
58
|
workspace?: string
|
|
58
59
|
forgejoOrganization?: string
|
|
59
60
|
forgejoUserRegistered?: boolean
|
|
61
|
+
gitSshKeyTitle?: string
|
|
62
|
+
gitSshPublicKeyPath?: string
|
|
63
|
+
gitSshPrivateKeyPath?: string
|
|
60
64
|
accessToken?: string
|
|
61
65
|
sessionExpiresAt?: number
|
|
62
66
|
hasRefreshToken?: boolean
|
|
@@ -264,6 +268,7 @@ function buildStatusNextActions(
|
|
|
264
268
|
hasAuth: boolean,
|
|
265
269
|
hasForgejoOrganization: boolean,
|
|
266
270
|
hasForgejoAccess: boolean,
|
|
271
|
+
hasGitSshKey: boolean,
|
|
267
272
|
): string[] {
|
|
268
273
|
const actions: string[] = []
|
|
269
274
|
|
|
@@ -279,6 +284,9 @@ function buildStatusNextActions(
|
|
|
279
284
|
else if (!hasForgejoAccess) {
|
|
280
285
|
actions.push('Run `tela setup` again to provision Forgejo access with your SSH key.')
|
|
281
286
|
}
|
|
287
|
+
else if (!hasGitSshKey) {
|
|
288
|
+
actions.push('Run `tela setup` again to choose the SSH key Tela should force on Git operations.')
|
|
289
|
+
}
|
|
282
290
|
|
|
283
291
|
if (!repo.inRepository) {
|
|
284
292
|
actions.push('Run `tela sync <repo>` or change into a cloned repository before mutating commands.')
|
|
@@ -329,6 +337,17 @@ export async function createStatusReport(
|
|
|
329
337
|
if (!configInspection.config.forgejoUserRegistered) {
|
|
330
338
|
warnings.push('Forgejo user access is not provisioned yet.')
|
|
331
339
|
}
|
|
340
|
+
if (!configInspection.config.gitSshKey) {
|
|
341
|
+
warnings.push('Git SSH key selection is missing.')
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
try {
|
|
345
|
+
resolveSshPrivateKeyPath(configInspection.config.gitSshKey.privateKeyPath)
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
warnings.push(`Configured Git SSH private key is unavailable: ${error instanceof Error ? error.message : String(error)}`)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
332
351
|
}
|
|
333
352
|
|
|
334
353
|
if (!gitAvailable) {
|
|
@@ -362,6 +381,7 @@ export async function createStatusReport(
|
|
|
362
381
|
Boolean(configInspection.config?.auth),
|
|
363
382
|
Boolean(configInspection.config?.forgejoOrganization),
|
|
364
383
|
Boolean(configInspection.config?.forgejoUserRegistered),
|
|
384
|
+
Boolean(configInspection.config?.gitSshKey),
|
|
365
385
|
)
|
|
366
386
|
|
|
367
387
|
return {
|
|
@@ -379,6 +399,9 @@ export async function createStatusReport(
|
|
|
379
399
|
workspace: configInspection.config?.auth?.workspace.title,
|
|
380
400
|
forgejoOrganization: configInspection.config?.forgejoOrganization?.name,
|
|
381
401
|
forgejoUserRegistered: configInspection.config?.forgejoUserRegistered,
|
|
402
|
+
gitSshKeyTitle: configInspection.config?.gitSshKey?.title,
|
|
403
|
+
gitSshPublicKeyPath: configInspection.config?.gitSshKey?.publicKeyPath,
|
|
404
|
+
gitSshPrivateKeyPath: configInspection.config?.gitSshKey?.privateKeyPath,
|
|
382
405
|
accessToken: configInspection.config?.auth ? redactSecret(configInspection.config.auth.session.accessToken) : undefined,
|
|
383
406
|
sessionExpiresAt: configInspection.config?.auth?.session.expiresAt,
|
|
384
407
|
hasRefreshToken: Boolean(configInspection.config?.auth?.session.refreshToken),
|
|
@@ -474,6 +497,47 @@ function buildCloneRootCheck(cloneRoot: string | undefined): DoctorCheck {
|
|
|
474
497
|
}
|
|
475
498
|
}
|
|
476
499
|
|
|
500
|
+
function buildGitSshKeyCheck(gitSshKey: {
|
|
501
|
+
title?: string
|
|
502
|
+
publicKeyPath?: string
|
|
503
|
+
privateKeyPath?: string
|
|
504
|
+
}): DoctorCheck {
|
|
505
|
+
if (!gitSshKey.privateKeyPath) {
|
|
506
|
+
return {
|
|
507
|
+
id: 'git-ssh-key',
|
|
508
|
+
group: 'access',
|
|
509
|
+
status: 'warn',
|
|
510
|
+
summary: 'No Git SSH key is configured.',
|
|
511
|
+
fixes: ['Run `tela setup` again and choose the SSH key to force on Git operations.'],
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
const privateKeyPath = resolveSshPrivateKeyPath(gitSshKey.privateKeyPath)
|
|
517
|
+
return {
|
|
518
|
+
id: 'git-ssh-key',
|
|
519
|
+
group: 'access',
|
|
520
|
+
status: 'pass',
|
|
521
|
+
summary: `Git SSH key is configured: ${gitSshKey.title ?? '<unnamed>'}.`,
|
|
522
|
+
fixes: ['No action required.'],
|
|
523
|
+
evidence: [
|
|
524
|
+
gitSshKey.publicKeyPath ? `public key: ${gitSshKey.publicKeyPath}` : '',
|
|
525
|
+
`private key: ${privateKeyPath}`,
|
|
526
|
+
].filter(Boolean),
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
return {
|
|
531
|
+
id: 'git-ssh-key',
|
|
532
|
+
group: 'access',
|
|
533
|
+
status: 'fail',
|
|
534
|
+
summary: 'Configured Git SSH private key is unavailable.',
|
|
535
|
+
fixes: ['Restore the private key file or run `tela setup` again and choose another SSH key.'],
|
|
536
|
+
evidence: [error instanceof Error ? error.message : String(error)],
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
477
541
|
function buildRepoChecks(repo: RepoSnapshot): DoctorCheck[] {
|
|
478
542
|
if (!repo.inRepository) {
|
|
479
543
|
return [
|
|
@@ -688,6 +752,12 @@ export async function createDoctorReport(
|
|
|
688
752
|
? ['No action required.']
|
|
689
753
|
: ['Run `tela setup` again and complete SSH-key provisioning for Forgejo access.'],
|
|
690
754
|
})
|
|
755
|
+
|
|
756
|
+
checks.push(buildGitSshKeyCheck({
|
|
757
|
+
title: configInspection.config.gitSshKey?.title,
|
|
758
|
+
publicKeyPath: configInspection.config.gitSshKey?.publicKeyPath,
|
|
759
|
+
privateKeyPath: configInspection.config.gitSshKey?.privateKeyPath,
|
|
760
|
+
}))
|
|
691
761
|
}
|
|
692
762
|
|
|
693
763
|
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)) {
|
package/src/git.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { basename } from 'node:path'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { inspectSavedConfig } from './config.ts'
|
|
3
|
+
import { createUserFacingError } from './errors.ts'
|
|
4
|
+
import { resolveSshPrivateKeyPath } from './forgejo.ts'
|
|
5
|
+
import {
|
|
6
|
+
CommandError,
|
|
7
|
+
runCommand,
|
|
8
|
+
type CommandResult,
|
|
9
|
+
type RunCommandOptions,
|
|
10
|
+
} from './process.ts'
|
|
11
|
+
import type { TelaCliGitSshKey } from './types.ts'
|
|
4
12
|
|
|
5
13
|
export interface GitStatusEntry {
|
|
6
14
|
index: string
|
|
@@ -15,8 +23,167 @@ export interface GitStatusCounts {
|
|
|
15
23
|
total: number
|
|
16
24
|
}
|
|
17
25
|
|
|
26
|
+
interface GitRunOptions extends Omit<RunCommandOptions, 'env'> {
|
|
27
|
+
requireConfiguredSsh?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function quoteForShell(value: string): string {
|
|
31
|
+
return `'${value.replaceAll('\'', `'"'"'`)}'`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildGitSshCommand(privateKeyPath: string): string {
|
|
35
|
+
return `ssh -i ${quoteForShell(privateKeyPath)} -o IdentitiesOnly=yes`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isSshRemoteUrl(remoteUrl: string): boolean {
|
|
39
|
+
const trimmed = remoteUrl.trim()
|
|
40
|
+
return /^ssh:\/\//i.test(trimmed) || /^[^\s@]+@[^\s:]+:.+$/.test(trimmed)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveConfiguredGitSshKey(requireConfiguredSsh: boolean): TelaCliGitSshKey | null {
|
|
44
|
+
const inspection = inspectSavedConfig()
|
|
45
|
+
if (!inspection.exists) {
|
|
46
|
+
if (requireConfiguredSsh) {
|
|
47
|
+
throw createUserFacingError({
|
|
48
|
+
code: 'git_ssh_key_config_missing',
|
|
49
|
+
summary: 'Tela CLI is not configured with a Git SSH key.',
|
|
50
|
+
fixes: [
|
|
51
|
+
'Run `tela setup` and choose the SSH key to force on Git operations.',
|
|
52
|
+
],
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!inspection.valid || !inspection.config) {
|
|
60
|
+
if (requireConfiguredSsh) {
|
|
61
|
+
throw createUserFacingError({
|
|
62
|
+
code: 'git_ssh_key_config_invalid',
|
|
63
|
+
summary: 'Tela CLI config is invalid, so the Git SSH key could not be loaded.',
|
|
64
|
+
details: inspection.error,
|
|
65
|
+
fixes: [
|
|
66
|
+
'Run `tela setup` to rewrite the config and choose the SSH key again.',
|
|
67
|
+
],
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!inspection.config.gitSshKey) {
|
|
75
|
+
if (requireConfiguredSsh) {
|
|
76
|
+
throw createUserFacingError({
|
|
77
|
+
code: 'git_ssh_key_missing',
|
|
78
|
+
summary: 'No Git SSH key is configured.',
|
|
79
|
+
fixes: [
|
|
80
|
+
'Run `tela setup` again and choose the SSH key to use for Git operations.',
|
|
81
|
+
],
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return {
|
|
90
|
+
...inspection.config.gitSshKey,
|
|
91
|
+
privateKeyPath: resolveSshPrivateKeyPath(inspection.config.gitSshKey.privateKeyPath),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (requireConfiguredSsh) {
|
|
96
|
+
throw createUserFacingError({
|
|
97
|
+
code: 'git_ssh_key_invalid',
|
|
98
|
+
summary: 'The configured Git SSH private key is unavailable.',
|
|
99
|
+
details: error instanceof Error ? error.message : String(error),
|
|
100
|
+
fixes: [
|
|
101
|
+
'Restore the configured private key file, or run `tela setup` again and choose another SSH key.',
|
|
102
|
+
],
|
|
103
|
+
cause: error,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildGitEnv(requireConfiguredSsh: boolean): Record<string, string> | undefined {
|
|
112
|
+
const gitSshKey = resolveConfiguredGitSshKey(requireConfiguredSsh)
|
|
113
|
+
if (!gitSshKey) {
|
|
114
|
+
return undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
GIT_SSH_COMMAND: buildGitSshCommand(gitSshKey.privateKeyPath),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function runGitCommand(
|
|
123
|
+
args: string[],
|
|
124
|
+
options: GitRunOptions = {},
|
|
125
|
+
): Promise<CommandResult> {
|
|
126
|
+
const env = buildGitEnv(Boolean(options.requireConfiguredSsh))
|
|
127
|
+
|
|
128
|
+
return await runCommand(
|
|
129
|
+
['git', ...args],
|
|
130
|
+
{
|
|
131
|
+
...options,
|
|
132
|
+
env: env ? env : undefined,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function getRemoteUrlInternal(cwd: string, remote: string): Promise<string | null> {
|
|
138
|
+
const result = await runGitCommand(['remote', 'get-url', remote], { cwd, allowFailure: true })
|
|
139
|
+
if (result.exitCode !== 0) {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result.stdout.trim() || null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function remoteRequiresConfiguredSsh(cwd: string, remote: string): Promise<boolean> {
|
|
147
|
+
const remoteUrl = await getRemoteUrlInternal(cwd, remote)
|
|
148
|
+
return Boolean(remoteUrl && isSshRemoteUrl(remoteUrl))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function anyRemoteRequiresConfiguredSsh(cwd: string): Promise<boolean> {
|
|
152
|
+
const result = await runGitCommand(['remote'], { cwd, allowFailure: true })
|
|
153
|
+
if (result.exitCode !== 0) {
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const remotes = result.stdout
|
|
158
|
+
.split(/\r?\n/)
|
|
159
|
+
.map(line => line.trim())
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
|
|
162
|
+
for (const remote of remotes) {
|
|
163
|
+
if (await remoteRequiresConfiguredSsh(cwd, remote)) {
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function upstreamRequiresConfiguredSsh(cwd: string): Promise<boolean> {
|
|
172
|
+
const upstream = await getUpstreamBranch(cwd)
|
|
173
|
+
if (!upstream) {
|
|
174
|
+
return false
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const separator = upstream.indexOf('/')
|
|
178
|
+
if (separator <= 0) {
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return await remoteRequiresConfiguredSsh(cwd, upstream.slice(0, separator))
|
|
183
|
+
}
|
|
184
|
+
|
|
18
185
|
export async function findRepositoryRoot(cwd: string): Promise<string | null> {
|
|
19
|
-
const result = await
|
|
186
|
+
const result = await runGitCommand(['rev-parse', '--show-toplevel'], { cwd, allowFailure: true })
|
|
20
187
|
if (result.exitCode !== 0) {
|
|
21
188
|
return null
|
|
22
189
|
}
|
|
@@ -35,7 +202,7 @@ export async function getRepositoryRoot(cwd: string): Promise<string> {
|
|
|
35
202
|
|
|
36
203
|
export async function assertGitRepository(cwd: string): Promise<void> {
|
|
37
204
|
try {
|
|
38
|
-
await
|
|
205
|
+
await runGitCommand(['rev-parse', '--is-inside-work-tree'], { cwd })
|
|
39
206
|
}
|
|
40
207
|
catch (error) {
|
|
41
208
|
if (error instanceof CommandError) {
|
|
@@ -46,24 +213,32 @@ export async function assertGitRepository(cwd: string): Promise<void> {
|
|
|
46
213
|
}
|
|
47
214
|
|
|
48
215
|
export async function cloneRepository(remoteUrl: string, targetDir: string): Promise<void> {
|
|
49
|
-
await
|
|
216
|
+
await runGitCommand(['clone', remoteUrl, targetDir], {
|
|
217
|
+
requireConfiguredSsh: isSshRemoteUrl(remoteUrl),
|
|
218
|
+
})
|
|
50
219
|
}
|
|
51
220
|
|
|
52
221
|
export async function getOriginUrl(cwd: string): Promise<string> {
|
|
53
|
-
const result = await
|
|
222
|
+
const result = await runGitCommand(['remote', 'get-url', 'origin'], { cwd })
|
|
54
223
|
return result.stdout.trim()
|
|
55
224
|
}
|
|
56
225
|
|
|
57
226
|
export async function fetchOrigin(cwd: string): Promise<void> {
|
|
58
|
-
await
|
|
227
|
+
await runGitCommand(['fetch', 'origin', '--prune'], {
|
|
228
|
+
cwd,
|
|
229
|
+
requireConfiguredSsh: await remoteRequiresConfiguredSsh(cwd, 'origin'),
|
|
230
|
+
})
|
|
59
231
|
}
|
|
60
232
|
|
|
61
233
|
export async function fetchAllRemotes(cwd: string): Promise<void> {
|
|
62
|
-
await
|
|
234
|
+
await runGitCommand(['fetch', '--all', '--prune'], {
|
|
235
|
+
cwd,
|
|
236
|
+
requireConfiguredSsh: await anyRemoteRequiresConfiguredSsh(cwd),
|
|
237
|
+
})
|
|
63
238
|
}
|
|
64
239
|
|
|
65
240
|
export async function getWorktreeStatus(cwd: string): Promise<string> {
|
|
66
|
-
const result = await
|
|
241
|
+
const result = await runGitCommand(['status', '--short'], { cwd })
|
|
67
242
|
return result.stdout.trim()
|
|
68
243
|
}
|
|
69
244
|
|
|
@@ -72,12 +247,12 @@ export async function isWorktreeDirty(cwd: string): Promise<boolean> {
|
|
|
72
247
|
}
|
|
73
248
|
|
|
74
249
|
export async function stageAll(cwd: string): Promise<void> {
|
|
75
|
-
await
|
|
250
|
+
await runGitCommand(['add', '-A'], { cwd })
|
|
76
251
|
}
|
|
77
252
|
|
|
78
253
|
export async function getCurrentBranch(cwd: string): Promise<string> {
|
|
79
254
|
try {
|
|
80
|
-
const result = await
|
|
255
|
+
const result = await runGitCommand(['symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd })
|
|
81
256
|
return result.stdout.trim()
|
|
82
257
|
}
|
|
83
258
|
catch (error) {
|
|
@@ -89,8 +264,8 @@ export async function getCurrentBranch(cwd: string): Promise<string> {
|
|
|
89
264
|
}
|
|
90
265
|
|
|
91
266
|
export async function getUpstreamBranch(cwd: string): Promise<string | null> {
|
|
92
|
-
const result = await
|
|
93
|
-
['
|
|
267
|
+
const result = await runGitCommand(
|
|
268
|
+
['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
|
|
94
269
|
{ cwd, allowFailure: true },
|
|
95
270
|
)
|
|
96
271
|
|
|
@@ -102,15 +277,21 @@ export async function getUpstreamBranch(cwd: string): Promise<string | null> {
|
|
|
102
277
|
}
|
|
103
278
|
|
|
104
279
|
export async function pullCurrentBranch(cwd: string): Promise<void> {
|
|
105
|
-
await
|
|
280
|
+
await runGitCommand(['pull', '--ff-only'], {
|
|
281
|
+
cwd,
|
|
282
|
+
requireConfiguredSsh: await upstreamRequiresConfiguredSsh(cwd),
|
|
283
|
+
})
|
|
106
284
|
}
|
|
107
285
|
|
|
108
286
|
export async function pullDefaultBranch(cwd: string, defaultBranch: string): Promise<void> {
|
|
109
|
-
await
|
|
287
|
+
await runGitCommand(['pull', '--ff-only', 'origin', defaultBranch], {
|
|
288
|
+
cwd,
|
|
289
|
+
requireConfiguredSsh: await remoteRequiresConfiguredSsh(cwd, 'origin'),
|
|
290
|
+
})
|
|
110
291
|
}
|
|
111
292
|
|
|
112
293
|
export async function getShortStatus(cwd: string): Promise<string> {
|
|
113
|
-
const result = await
|
|
294
|
+
const result = await runGitCommand(['status', '--short'], { cwd })
|
|
114
295
|
return result.stdout
|
|
115
296
|
}
|
|
116
297
|
|
|
@@ -156,22 +337,22 @@ export async function getStatusCounts(cwd: string): Promise<GitStatusCounts> {
|
|
|
156
337
|
}
|
|
157
338
|
|
|
158
339
|
export async function getStagedDiff(cwd: string): Promise<string> {
|
|
159
|
-
const result = await
|
|
340
|
+
const result = await runGitCommand(['diff', '--cached'], { cwd })
|
|
160
341
|
return result.stdout
|
|
161
342
|
}
|
|
162
343
|
|
|
163
344
|
export async function getDiffAgainstHead(cwd: string): Promise<string> {
|
|
164
|
-
const result = await
|
|
345
|
+
const result = await runGitCommand(['diff', 'HEAD', '--'], { cwd })
|
|
165
346
|
return result.stdout
|
|
166
347
|
}
|
|
167
348
|
|
|
168
349
|
export async function getRecentLog(cwd: string, count = 10): Promise<string> {
|
|
169
|
-
const result = await
|
|
350
|
+
const result = await runGitCommand(['log', '--oneline', `-${count}`], { cwd, allowFailure: true })
|
|
170
351
|
return result.stdout
|
|
171
352
|
}
|
|
172
353
|
|
|
173
354
|
export async function getStagedFiles(cwd: string): Promise<string[]> {
|
|
174
|
-
const result = await
|
|
355
|
+
const result = await runGitCommand(['diff', '--cached', '--name-only', '--diff-filter=ACMRTUXB'], { cwd })
|
|
175
356
|
return result.stdout
|
|
176
357
|
.split(/\r?\n/)
|
|
177
358
|
.map(line => line.trim())
|
|
@@ -179,7 +360,7 @@ export async function getStagedFiles(cwd: string): Promise<string[]> {
|
|
|
179
360
|
}
|
|
180
361
|
|
|
181
362
|
export async function getChangedFilesAgainstHead(cwd: string): Promise<string[]> {
|
|
182
|
-
const result = await
|
|
363
|
+
const result = await runGitCommand(['diff', '--name-only', 'HEAD', '--diff-filter=ACMRTUXB'], { cwd })
|
|
183
364
|
return result.stdout
|
|
184
365
|
.split(/\r?\n/)
|
|
185
366
|
.map(line => line.trim())
|
|
@@ -187,7 +368,7 @@ export async function getChangedFilesAgainstHead(cwd: string): Promise<string[]>
|
|
|
187
368
|
}
|
|
188
369
|
|
|
189
370
|
export async function getDiffStatAgainstHead(cwd: string): Promise<string> {
|
|
190
|
-
const result = await
|
|
371
|
+
const result = await runGitCommand(['diff', '--stat', 'HEAD'], { cwd, allowFailure: true })
|
|
191
372
|
return result.stdout.trim()
|
|
192
373
|
}
|
|
193
374
|
|
|
@@ -203,48 +384,43 @@ export function buildFallbackCommitMessage(files: string[]): string {
|
|
|
203
384
|
return `Update ${basename(files[0]!) || files[0]} and ${files.length - 1} other files`
|
|
204
385
|
}
|
|
205
386
|
|
|
206
|
-
function buildCommitDelimiter(message: string): string {
|
|
207
|
-
let delimiter = `__TELA_COMMIT_${randomUUID().replace(/-/g, '_')}__`
|
|
208
|
-
while (message.includes(delimiter)) {
|
|
209
|
-
delimiter = `__TELA_COMMIT_${randomUUID().replace(/-/g, '_')}__`
|
|
210
|
-
}
|
|
211
|
-
return delimiter
|
|
212
|
-
}
|
|
213
|
-
|
|
214
387
|
export async function commitWithMessage(cwd: string, message: string): Promise<void> {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
'__TELA_COMMIT__',
|
|
220
|
-
].join('\n').replaceAll('__TELA_COMMIT__', delimiter)
|
|
221
|
-
|
|
222
|
-
await runShell(script, { cwd })
|
|
388
|
+
await runGitCommand(['commit', '-F', '-'], {
|
|
389
|
+
cwd,
|
|
390
|
+
input: message,
|
|
391
|
+
})
|
|
223
392
|
}
|
|
224
393
|
|
|
225
394
|
export async function pushBranch(cwd: string, branch: string, setUpstream: boolean): Promise<void> {
|
|
226
395
|
const args = setUpstream
|
|
227
|
-
? ['
|
|
228
|
-
: ['
|
|
229
|
-
|
|
396
|
+
? ['push', '--set-upstream', 'origin', branch]
|
|
397
|
+
: ['push']
|
|
398
|
+
const requireConfiguredSsh = setUpstream
|
|
399
|
+
? await remoteRequiresConfiguredSsh(cwd, 'origin')
|
|
400
|
+
: await upstreamRequiresConfiguredSsh(cwd)
|
|
401
|
+
|
|
402
|
+
await runGitCommand(args, {
|
|
403
|
+
cwd,
|
|
404
|
+
requireConfiguredSsh,
|
|
405
|
+
})
|
|
230
406
|
}
|
|
231
407
|
|
|
232
408
|
export async function getHeadShortSha(cwd: string): Promise<string> {
|
|
233
|
-
const result = await
|
|
409
|
+
const result = await runGitCommand(['rev-parse', '--short', 'HEAD'], { cwd })
|
|
234
410
|
return result.stdout.trim()
|
|
235
411
|
}
|
|
236
412
|
|
|
237
413
|
export async function getHeadCommitHash(cwd: string): Promise<string> {
|
|
238
|
-
const result = await
|
|
414
|
+
const result = await runGitCommand(['rev-parse', 'HEAD'], { cwd })
|
|
239
415
|
return result.stdout.trim()
|
|
240
416
|
}
|
|
241
417
|
|
|
242
418
|
export async function getHeadCommitMessage(cwd: string): Promise<string> {
|
|
243
|
-
const result = await
|
|
419
|
+
const result = await runGitCommand(['log', '-1', '--pretty=%B'], { cwd })
|
|
244
420
|
return result.stdout.trim()
|
|
245
421
|
}
|
|
246
422
|
|
|
247
423
|
export async function getHeadShortMessage(cwd: string): Promise<string> {
|
|
248
|
-
const result = await
|
|
424
|
+
const result = await runGitCommand(['log', '-1', '--pretty=%s'], { cwd })
|
|
249
425
|
return result.stdout.trim()
|
|
250
426
|
}
|
|
@@ -2,7 +2,9 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
|
2
2
|
import { useInput } from 'ink'
|
|
3
3
|
import { ENVIRONMENT_PRESET_OPTIONS, resolveEnvironmentConfigForPreset } from '../../../config.ts'
|
|
4
4
|
import {
|
|
5
|
+
inferSshPrivateKeyPath,
|
|
5
6
|
loadSshPublicKey,
|
|
7
|
+
validateSshPrivateKeyPath,
|
|
6
8
|
validateSshPublicKeyPath,
|
|
7
9
|
} from '../../../forgejo.ts'
|
|
8
10
|
import type { TelaCliAuthState, TelaCliForgejoOrganization } from '../../../types.ts'
|
|
@@ -67,6 +69,9 @@ export function SetupScreen(props: SetupScreenProps): React.ReactElement {
|
|
|
67
69
|
cloneRoot: props.defaults.cloneRoot,
|
|
68
70
|
sshKeySelection: props.sshPublicKeys[0]?.path ?? CUSTOM_SSH_KEY_OPTION,
|
|
69
71
|
sshKeyPath: '',
|
|
72
|
+
sshPrivateKeyPath: props.sshPublicKeys[0]
|
|
73
|
+
? inferSshPrivateKeyPath(props.sshPublicKeys[0].path)
|
|
74
|
+
: '',
|
|
70
75
|
})
|
|
71
76
|
const [errors, setErrors] = useState<Partial<Record<SetupStepId, string>>>({})
|
|
72
77
|
const [createCloneRoot, setCreateCloneRoot] = useState(true)
|
|
@@ -82,6 +87,7 @@ export function SetupScreen(props: SetupScreenProps): React.ReactElement {
|
|
|
82
87
|
const [result, setResult] = useState<SetupScreenResult | null>(null)
|
|
83
88
|
const authStartedRef = useRef(false)
|
|
84
89
|
const provisionStartedRef = useRef(false)
|
|
90
|
+
const previousSuggestedPrivateKeyPathRef = useRef(values.sshPrivateKeyPath)
|
|
85
91
|
|
|
86
92
|
const visibleSteps = useMemo(() => getVisibleEditSteps(values), [values])
|
|
87
93
|
const safeStepIndex = Math.min(stepIndex, Math.max(visibleSteps.length - 1, 0))
|
|
@@ -117,6 +123,10 @@ export function SetupScreen(props: SetupScreenProps): React.ReactElement {
|
|
|
117
123
|
|
|
118
124
|
return props.sshPublicKeys.find(key => key.path === values.sshKeySelection)
|
|
119
125
|
}, [props.sshPublicKeys, values.sshKeyPath, values.sshKeySelection])
|
|
126
|
+
const suggestedPrivateKeyPath = useMemo(
|
|
127
|
+
() => selectedSshKey ? inferSshPrivateKeyPath(selectedSshKey.path) : '',
|
|
128
|
+
[selectedSshKey],
|
|
129
|
+
)
|
|
120
130
|
const cloneRootState = useMemo(() => props.inspectCloneRoot(values.cloneRoot), [props, values.cloneRoot])
|
|
121
131
|
|
|
122
132
|
const breadcrumb = useMemo<BreadcrumbEntry[]>(() => {
|
|
@@ -247,6 +257,7 @@ export function SetupScreen(props: SetupScreenProps): React.ReactElement {
|
|
|
247
257
|
cloneRoot: values.cloneRoot,
|
|
248
258
|
createCloneRoot,
|
|
249
259
|
sshKey: selectedSshKey,
|
|
260
|
+
sshPrivateKeyPath: values.sshPrivateKeyPath,
|
|
250
261
|
auth: authenticated,
|
|
251
262
|
organization: selectedOrganization,
|
|
252
263
|
}, (event) => {
|
|
@@ -267,6 +278,7 @@ export function SetupScreen(props: SetupScreenProps): React.ReactElement {
|
|
|
267
278
|
resolvedConfig,
|
|
268
279
|
selectedOrganization,
|
|
269
280
|
selectedSshKey,
|
|
281
|
+
values.sshPrivateKeyPath,
|
|
270
282
|
values.cloneRoot,
|
|
271
283
|
])
|
|
272
284
|
|
|
@@ -276,6 +288,29 @@ export function SetupScreen(props: SetupScreenProps): React.ReactElement {
|
|
|
276
288
|
}
|
|
277
289
|
}, [phase, props, result])
|
|
278
290
|
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
setValues((current) => {
|
|
293
|
+
const previousSuggested = previousSuggestedPrivateKeyPathRef.current
|
|
294
|
+
previousSuggestedPrivateKeyPathRef.current = suggestedPrivateKeyPath
|
|
295
|
+
|
|
296
|
+
if (
|
|
297
|
+
current.sshPrivateKeyPath
|
|
298
|
+
&& current.sshPrivateKeyPath !== previousSuggested
|
|
299
|
+
) {
|
|
300
|
+
return current
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (current.sshPrivateKeyPath === suggestedPrivateKeyPath) {
|
|
304
|
+
return current
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
...current,
|
|
309
|
+
sshPrivateKeyPath: suggestedPrivateKeyPath,
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
}, [suggestedPrivateKeyPath])
|
|
313
|
+
|
|
279
314
|
useInput((input, key) => {
|
|
280
315
|
if (phase === 'authenticating' || phase === 'provisioning' || phase === 'done') {
|
|
281
316
|
return
|
|
@@ -570,5 +605,7 @@ function validateStep(
|
|
|
570
605
|
return selectedSshKey ? undefined : 'SSH public key is required'
|
|
571
606
|
case 'sshKeyPath':
|
|
572
607
|
return validateSshPublicKeyPath(values.sshKeyPath)
|
|
608
|
+
case 'sshPrivateKeyPath':
|
|
609
|
+
return validateSshPrivateKeyPath(values.sshPrivateKeyPath)
|
|
573
610
|
}
|
|
574
611
|
}
|
|
@@ -12,6 +12,7 @@ export type SetupStepId =
|
|
|
12
12
|
| 'cloneRoot'
|
|
13
13
|
| 'sshKey'
|
|
14
14
|
| 'sshKeyPath'
|
|
15
|
+
| 'sshPrivateKeyPath'
|
|
15
16
|
|
|
16
17
|
export type SetupStepKind = 'select-environment' | 'select-ssh-key' | 'text'
|
|
17
18
|
|
|
@@ -103,6 +104,14 @@ export const EDIT_STEPS: readonly SetupStepDescriptor[] = [
|
|
|
103
104
|
placeholder: '~/.ssh/id_ed25519.pub',
|
|
104
105
|
visible: values => values.sshKeySelection === CUSTOM_SSH_KEY_OPTION,
|
|
105
106
|
},
|
|
107
|
+
{
|
|
108
|
+
id: 'sshPrivateKeyPath',
|
|
109
|
+
label: 'Private Key Path',
|
|
110
|
+
question: 'Path to your SSH private key',
|
|
111
|
+
kind: 'text',
|
|
112
|
+
placeholder: '~/.ssh/id_ed25519',
|
|
113
|
+
visible: () => true,
|
|
114
|
+
},
|
|
106
115
|
]
|
|
107
116
|
|
|
108
117
|
export function getVisibleEditSteps(values: SetupFormValues): SetupStepDescriptor[] {
|
|
@@ -141,5 +150,7 @@ export function summarizeStep(
|
|
|
141
150
|
: (sshKeyTitle ?? values.sshKeySelection)
|
|
142
151
|
case 'sshKeyPath':
|
|
143
152
|
return values.sshKeyPath
|
|
153
|
+
case 'sshPrivateKeyPath':
|
|
154
|
+
return values.sshPrivateKeyPath
|
|
144
155
|
}
|
|
145
156
|
}
|
|
@@ -13,6 +13,7 @@ export const CUSTOM_SSH_KEY_OPTION = '__custom__'
|
|
|
13
13
|
export type SetupFormValues = SetupConfigInput & {
|
|
14
14
|
sshKeySelection: string
|
|
15
15
|
sshKeyPath: string
|
|
16
|
+
sshPrivateKeyPath: string
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export interface SetupScreenResult {
|
|
@@ -55,6 +56,7 @@ export interface SetupScreenProps {
|
|
|
55
56
|
input: SetupConfigInput & {
|
|
56
57
|
createCloneRoot: boolean
|
|
57
58
|
sshKey: SelectedSshPublicKey
|
|
59
|
+
sshPrivateKeyPath: string
|
|
58
60
|
auth: TelaCliAuthState
|
|
59
61
|
organization: TelaCliForgejoOrganization
|
|
60
62
|
},
|
package/src/program.ts
CHANGED
|
@@ -14,11 +14,6 @@ import { SyncScreen, type SyncProgressItem } from './ink/screens/sync-screen.tsx
|
|
|
14
14
|
import { renderCommandScreen } from './ink/runtime.tsx'
|
|
15
15
|
import { listOrganizations } from './agent-api.ts'
|
|
16
16
|
import { authenticateWithDeviceFlow, type SetupAuthEvent } from './auth.ts'
|
|
17
|
-
import {
|
|
18
|
-
checkAgentApiReachability,
|
|
19
|
-
getAgentApiOriginCheckStatus,
|
|
20
|
-
getAgentApiOriginWarning,
|
|
21
|
-
} from './diagnostics.ts'
|
|
22
17
|
import { createUserFacingError } from './errors.ts'
|
|
23
18
|
import {
|
|
24
19
|
getCommandMetadata,
|
|
@@ -87,6 +82,7 @@ import {
|
|
|
87
82
|
} from './config.ts'
|
|
88
83
|
import { findRepositoryRoot, getDiffStatAgainstHead } from './git.ts'
|
|
89
84
|
import {
|
|
85
|
+
createGitSshKey,
|
|
90
86
|
ensureForgejoUserAccess,
|
|
91
87
|
listDiscoveredSshPublicKeys,
|
|
92
88
|
type SelectedSshPublicKey,
|
|
@@ -381,6 +377,7 @@ function summarizeConfigInspection(inspection: ConfigInspection): string {
|
|
|
381
377
|
{ label: 'Workspace', value: inspection.config.auth?.workspace.title ?? '<signed out>' },
|
|
382
378
|
{ label: 'Organization', value: inspection.config.forgejoOrganization?.name ?? '<not selected>' },
|
|
383
379
|
{ label: 'Forgejo user', value: inspection.config.forgejoUserRegistered ? 'registered' : 'pending' },
|
|
380
|
+
{ label: 'Git SSH key', value: inspection.config.gitSshKey ? `${inspection.config.gitSshKey.title} (${inspection.config.gitSshKey.privateKeyPath})` : '<missing>' },
|
|
384
381
|
])
|
|
385
382
|
}
|
|
386
383
|
|
|
@@ -411,6 +408,7 @@ async function executeSetupFlow(
|
|
|
411
408
|
input: SetupConfigInput & {
|
|
412
409
|
createCloneRoot: boolean
|
|
413
410
|
sshKey: SelectedSshPublicKey
|
|
411
|
+
sshPrivateKeyPath: string
|
|
414
412
|
auth: TelaCliAuthState
|
|
415
413
|
organization: TelaCliForgejoOrganization
|
|
416
414
|
},
|
|
@@ -432,6 +430,7 @@ async function executeSetupFlow(
|
|
|
432
430
|
auth: input.auth,
|
|
433
431
|
forgejoOrganization: input.organization,
|
|
434
432
|
forgejoUserRegistered: false,
|
|
433
|
+
gitSshKey: createGitSshKey(input.sshKey, input.sshPrivateKeyPath),
|
|
435
434
|
}
|
|
436
435
|
const forgejoProvision = await ensureForgejoUserAccess({
|
|
437
436
|
config: baseConfig,
|
|
@@ -453,17 +452,15 @@ async function executeSetupFlow(
|
|
|
453
452
|
mkdirSync(config.cloneRoot, { recursive: true, mode: 0o755 })
|
|
454
453
|
}
|
|
455
454
|
|
|
456
|
-
const [gitAvailable, claudeAvailable
|
|
455
|
+
const [gitAvailable, claudeAvailable] = await Promise.all([
|
|
457
456
|
commandExistsFn('git'),
|
|
458
457
|
commandExistsFn('claude'),
|
|
459
|
-
checkAgentApiReachability(config.agentApiUrl, fetchFn),
|
|
460
458
|
])
|
|
461
459
|
|
|
462
460
|
const warnings = [
|
|
463
461
|
!existsSync(config.cloneRoot) ? `Clone root does not exist yet: ${config.cloneRoot}` : undefined,
|
|
464
462
|
gitAvailable ? undefined : '`git` is not available on PATH.',
|
|
465
463
|
claudeAvailable ? undefined : '`claude` is not available on PATH.',
|
|
466
|
-
getAgentApiOriginWarning(reachability),
|
|
467
464
|
].filter((warning): warning is string => Boolean(warning))
|
|
468
465
|
const checks = [
|
|
469
466
|
{ label: 'auth', status: 'authenticated', detail: `${input.auth.user.email} @ ${input.auth.workspace.title}` },
|
|
@@ -472,7 +469,7 @@ async function executeSetupFlow(
|
|
|
472
469
|
{ label: 'clone-root', status: existsSync(config.cloneRoot) ? 'ready' : 'missing', detail: config.cloneRoot },
|
|
473
470
|
{ label: 'git', status: gitAvailable ? 'available' : 'missing', detail: gitAvailable ? 'ready' : 'install git' },
|
|
474
471
|
{ label: 'claude', status: claudeAvailable ? 'available' : 'missing', detail: claudeAvailable ? 'ready' : 'install Claude CLI' },
|
|
475
|
-
{ label: 'agent-api-origin', status:
|
|
472
|
+
{ label: 'agent-api-origin', status: 'verified', detail: config.agentApiUrl },
|
|
476
473
|
]
|
|
477
474
|
|
|
478
475
|
return {
|
|
@@ -574,6 +571,7 @@ async function runSetupInk(
|
|
|
574
571
|
auth: input.auth,
|
|
575
572
|
forgejoOrganization: null,
|
|
576
573
|
forgejoUserRegistered: false,
|
|
574
|
+
gitSshKey: inspection.config?.gitSshKey ?? null,
|
|
577
575
|
}
|
|
578
576
|
const organizations = await listOrganizations(config, deps.fetchFn ?? fetch)
|
|
579
577
|
if (organizations.length === 0) {
|
|
@@ -623,7 +621,6 @@ export async function runSetup(
|
|
|
623
621
|
const setup = deps.setupConfigInteractive ?? setupConfigInteractive
|
|
624
622
|
const inspect = deps.inspectSavedConfig ?? inspectSavedConfig
|
|
625
623
|
const commandExistsFn = deps.commandExists ?? commandExists
|
|
626
|
-
const fetchFn = deps.fetchFn ?? fetch
|
|
627
624
|
const authenticate = deps.authenticateWithDeviceFlow ?? authenticateWithDeviceFlow
|
|
628
625
|
const inspection = inspect()
|
|
629
626
|
|
|
@@ -681,17 +678,15 @@ export async function runSetup(
|
|
|
681
678
|
}
|
|
682
679
|
}
|
|
683
680
|
|
|
684
|
-
const [gitAvailable, claudeAvailable
|
|
681
|
+
const [gitAvailable, claudeAvailable] = await Promise.all([
|
|
685
682
|
commandExistsFn('git'),
|
|
686
683
|
commandExistsFn('claude'),
|
|
687
|
-
checkAgentApiReachability(config.agentApiUrl, fetchFn),
|
|
688
684
|
])
|
|
689
685
|
|
|
690
686
|
const warnings = [
|
|
691
687
|
cloneRootWarning,
|
|
692
688
|
gitAvailable ? undefined : '`git` is not available on PATH.',
|
|
693
689
|
claudeAvailable ? undefined : '`claude` is not available on PATH.',
|
|
694
|
-
getAgentApiOriginWarning(reachability),
|
|
695
690
|
].filter((warning): warning is string => Boolean(warning))
|
|
696
691
|
|
|
697
692
|
const nextSetupActions = [
|
|
@@ -715,6 +710,7 @@ export async function runSetup(
|
|
|
715
710
|
{ label: 'Workspace', value: config.auth?.workspace.title ?? '<missing>' },
|
|
716
711
|
{ label: 'Organization', value: config.forgejoOrganization?.name ?? '<missing>' },
|
|
717
712
|
{ label: 'Forgejo user', value: config.forgejoUserRegistered ? 'registered' : 'pending' },
|
|
713
|
+
{ label: 'Git SSH key', value: config.gitSshKey ? `${config.gitSshKey.title} (${config.gitSshKey.privateKeyPath})` : '<missing>' },
|
|
718
714
|
{ label: 'Session', value: config.auth ? `${redactSecret(config.auth.session.accessToken)} (expires ${new Date(config.auth.session.expiresAt).toISOString()})` : '<missing>' },
|
|
719
715
|
]), { decorate: runtime.decorate }),
|
|
720
716
|
section('Authentication', DEVICE_FLOW_BROWSER_HELP, { decorate: runtime.decorate }),
|
|
@@ -724,10 +720,11 @@ export async function runSetup(
|
|
|
724
720
|
{ label: 'auth', status: 'authenticated', detail: `${config.auth?.user.email ?? '<missing>'} @ ${config.auth?.workspace.title ?? '<missing>'}` },
|
|
725
721
|
{ label: 'organization', status: config.forgejoOrganization ? 'selected' : 'missing', detail: config.forgejoOrganization?.name ?? 'not selected' },
|
|
726
722
|
{ label: 'forgejo-user', status: config.forgejoUserRegistered ? 'registered' : 'pending', detail: config.forgejoUserRegistered ? 'SSH-key provisioning completed.' : 'SSH-key provisioning is pending.' },
|
|
723
|
+
{ label: 'git-ssh-key', status: config.gitSshKey ? 'configured' : 'missing', detail: config.gitSshKey?.privateKeyPath ?? 'rerun setup to choose an SSH key' },
|
|
727
724
|
{ label: 'clone-root', status: cloneRootCreated ? 'created' : (existsSync(config.cloneRoot) ? 'ready' : 'missing'), detail: config.cloneRoot },
|
|
728
725
|
{ label: 'git', status: gitAvailable ? 'available' : 'missing', detail: gitAvailable ? 'ready' : 'install git' },
|
|
729
726
|
{ label: 'claude', status: claudeAvailable ? 'available' : 'missing', detail: claudeAvailable ? 'ready' : 'install Claude CLI' },
|
|
730
|
-
{ label: 'agent-api-origin', status:
|
|
727
|
+
{ label: 'agent-api-origin', status: 'verified', detail: config.agentApiUrl },
|
|
731
728
|
]),
|
|
732
729
|
{ decorate: runtime.decorate },
|
|
733
730
|
), { decorate: runtime.decorate }),
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,12 @@ export interface TelaCliForgejoOrganization {
|
|
|
22
22
|
name: string
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface TelaCliGitSshKey {
|
|
26
|
+
title: string
|
|
27
|
+
publicKeyPath: string
|
|
28
|
+
privateKeyPath: string
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
export interface TelaCliAuthState {
|
|
26
32
|
user: TelaCliUser
|
|
27
33
|
workspace: TelaCliWorkspace
|
|
@@ -40,6 +46,7 @@ export interface TelaCliConfig {
|
|
|
40
46
|
auth: TelaCliAuthState | null
|
|
41
47
|
forgejoOrganization: TelaCliForgejoOrganization | null
|
|
42
48
|
forgejoUserRegistered: boolean
|
|
49
|
+
gitSshKey: TelaCliGitSshKey | null
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
export interface OrganizationSummary {
|