@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/src/git.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import { basename } from 'node:path'
2
- import { randomUUID } from 'node:crypto'
3
- import { CommandError, runCommand, runShell } from './process.ts'
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,192 @@ export interface GitStatusCounts {
15
23
  total: number
16
24
  }
17
25
 
26
+ type GitSshHostKeyPolicy = 'default' | 'accept-new'
27
+
28
+ interface GitRunOptions extends Omit<RunCommandOptions, 'env'> {
29
+ requireConfiguredSsh?: boolean
30
+ sshHostKeyPolicy?: GitSshHostKeyPolicy
31
+ }
32
+
33
+ function quoteForShell(value: string): string {
34
+ return `'${value.replaceAll('\'', `'"'"'`)}'`
35
+ }
36
+
37
+ export function buildGitSshCommand(
38
+ privateKeyPath: string,
39
+ options: {
40
+ hostKeyPolicy?: GitSshHostKeyPolicy
41
+ } = {},
42
+ ): string {
43
+ const args = [
44
+ `-i ${quoteForShell(privateKeyPath)}`,
45
+ '-o IdentitiesOnly=yes',
46
+ ]
47
+
48
+ if (options.hostKeyPolicy === 'accept-new') {
49
+ args.push('-o StrictHostKeyChecking=accept-new')
50
+ }
51
+
52
+ return `ssh ${args.join(' ')}`
53
+ }
54
+
55
+ export function isSshRemoteUrl(remoteUrl: string): boolean {
56
+ const trimmed = remoteUrl.trim()
57
+ return /^ssh:\/\//i.test(trimmed) || /^[^\s@]+@[^\s:]+:.+$/.test(trimmed)
58
+ }
59
+
60
+ function resolveConfiguredGitSshKey(requireConfiguredSsh: boolean): TelaCliGitSshKey | null {
61
+ const inspection = inspectSavedConfig()
62
+ if (!inspection.exists) {
63
+ if (requireConfiguredSsh) {
64
+ throw createUserFacingError({
65
+ code: 'git_ssh_key_config_missing',
66
+ summary: 'Tela CLI is not configured with a Git SSH key.',
67
+ fixes: [
68
+ 'Run `tela setup` and choose the SSH key to force on Git operations.',
69
+ ],
70
+ })
71
+ }
72
+
73
+ return null
74
+ }
75
+
76
+ if (!inspection.valid || !inspection.config) {
77
+ if (requireConfiguredSsh) {
78
+ throw createUserFacingError({
79
+ code: 'git_ssh_key_config_invalid',
80
+ summary: 'Tela CLI config is invalid, so the Git SSH key could not be loaded.',
81
+ details: inspection.error,
82
+ fixes: [
83
+ 'Run `tela setup` to rewrite the config and choose the SSH key again.',
84
+ ],
85
+ })
86
+ }
87
+
88
+ return null
89
+ }
90
+
91
+ if (!inspection.config.gitSshKey) {
92
+ if (requireConfiguredSsh) {
93
+ throw createUserFacingError({
94
+ code: 'git_ssh_key_missing',
95
+ summary: 'No Git SSH key is configured.',
96
+ fixes: [
97
+ 'Run `tela setup` again and choose the SSH key to use for Git operations.',
98
+ ],
99
+ })
100
+ }
101
+
102
+ return null
103
+ }
104
+
105
+ try {
106
+ return {
107
+ ...inspection.config.gitSshKey,
108
+ privateKeyPath: resolveSshPrivateKeyPath(inspection.config.gitSshKey.privateKeyPath),
109
+ }
110
+ }
111
+ catch (error) {
112
+ if (requireConfiguredSsh) {
113
+ throw createUserFacingError({
114
+ code: 'git_ssh_key_invalid',
115
+ summary: 'The configured Git SSH private key is unavailable.',
116
+ details: error instanceof Error ? error.message : String(error),
117
+ fixes: [
118
+ 'Restore the configured private key file, or run `tela setup` again and choose another SSH key.',
119
+ ],
120
+ cause: error,
121
+ })
122
+ }
123
+
124
+ return null
125
+ }
126
+ }
127
+
128
+ function buildGitEnv(
129
+ requireConfiguredSsh: boolean,
130
+ sshHostKeyPolicy: GitSshHostKeyPolicy = 'default',
131
+ ): Record<string, string> | undefined {
132
+ const gitSshKey = resolveConfiguredGitSshKey(requireConfiguredSsh)
133
+ if (!gitSshKey) {
134
+ return undefined
135
+ }
136
+
137
+ return {
138
+ GIT_SSH_COMMAND: buildGitSshCommand(gitSshKey.privateKeyPath, {
139
+ hostKeyPolicy: sshHostKeyPolicy,
140
+ }),
141
+ }
142
+ }
143
+
144
+ async function runGitCommand(
145
+ args: string[],
146
+ options: GitRunOptions = {},
147
+ ): Promise<CommandResult> {
148
+ const env = buildGitEnv(
149
+ Boolean(options.requireConfiguredSsh),
150
+ options.sshHostKeyPolicy,
151
+ )
152
+
153
+ return await runCommand(
154
+ ['git', ...args],
155
+ {
156
+ ...options,
157
+ env: env ? env : undefined,
158
+ },
159
+ )
160
+ }
161
+
162
+ async function getRemoteUrlInternal(cwd: string, remote: string): Promise<string | null> {
163
+ const result = await runGitCommand(['remote', 'get-url', remote], { cwd, allowFailure: true })
164
+ if (result.exitCode !== 0) {
165
+ return null
166
+ }
167
+
168
+ return result.stdout.trim() || null
169
+ }
170
+
171
+ async function remoteRequiresConfiguredSsh(cwd: string, remote: string): Promise<boolean> {
172
+ const remoteUrl = await getRemoteUrlInternal(cwd, remote)
173
+ return Boolean(remoteUrl && isSshRemoteUrl(remoteUrl))
174
+ }
175
+
176
+ async function anyRemoteRequiresConfiguredSsh(cwd: string): Promise<boolean> {
177
+ const result = await runGitCommand(['remote'], { cwd, allowFailure: true })
178
+ if (result.exitCode !== 0) {
179
+ return false
180
+ }
181
+
182
+ const remotes = result.stdout
183
+ .split(/\r?\n/)
184
+ .map(line => line.trim())
185
+ .filter(Boolean)
186
+
187
+ for (const remote of remotes) {
188
+ if (await remoteRequiresConfiguredSsh(cwd, remote)) {
189
+ return true
190
+ }
191
+ }
192
+
193
+ return false
194
+ }
195
+
196
+ async function upstreamRequiresConfiguredSsh(cwd: string): Promise<boolean> {
197
+ const upstream = await getUpstreamBranch(cwd)
198
+ if (!upstream) {
199
+ return false
200
+ }
201
+
202
+ const separator = upstream.indexOf('/')
203
+ if (separator <= 0) {
204
+ return false
205
+ }
206
+
207
+ return await remoteRequiresConfiguredSsh(cwd, upstream.slice(0, separator))
208
+ }
209
+
18
210
  export async function findRepositoryRoot(cwd: string): Promise<string | null> {
19
- const result = await runCommand(['git', 'rev-parse', '--show-toplevel'], { cwd, allowFailure: true })
211
+ const result = await runGitCommand(['rev-parse', '--show-toplevel'], { cwd, allowFailure: true })
20
212
  if (result.exitCode !== 0) {
21
213
  return null
22
214
  }
@@ -35,7 +227,7 @@ export async function getRepositoryRoot(cwd: string): Promise<string> {
35
227
 
36
228
  export async function assertGitRepository(cwd: string): Promise<void> {
37
229
  try {
38
- await runCommand(['git', 'rev-parse', '--is-inside-work-tree'], { cwd })
230
+ await runGitCommand(['rev-parse', '--is-inside-work-tree'], { cwd })
39
231
  }
40
232
  catch (error) {
41
233
  if (error instanceof CommandError) {
@@ -46,24 +238,36 @@ export async function assertGitRepository(cwd: string): Promise<void> {
46
238
  }
47
239
 
48
240
  export async function cloneRepository(remoteUrl: string, targetDir: string): Promise<void> {
49
- await runCommand(['git', 'clone', remoteUrl, targetDir])
241
+ await runGitCommand(['clone', remoteUrl, targetDir], {
242
+ requireConfiguredSsh: isSshRemoteUrl(remoteUrl),
243
+ // Sync accepts first-seen SSH host keys automatically, but changed host keys still fail.
244
+ sshHostKeyPolicy: 'accept-new',
245
+ })
50
246
  }
51
247
 
52
248
  export async function getOriginUrl(cwd: string): Promise<string> {
53
- const result = await runCommand(['git', 'remote', 'get-url', 'origin'], { cwd })
249
+ const result = await runGitCommand(['remote', 'get-url', 'origin'], { cwd })
54
250
  return result.stdout.trim()
55
251
  }
56
252
 
57
253
  export async function fetchOrigin(cwd: string): Promise<void> {
58
- await runCommand(['git', 'fetch', 'origin', '--prune'], { cwd })
254
+ await runGitCommand(['fetch', 'origin', '--prune'], {
255
+ cwd,
256
+ requireConfiguredSsh: await remoteRequiresConfiguredSsh(cwd, 'origin'),
257
+ sshHostKeyPolicy: 'accept-new',
258
+ })
59
259
  }
60
260
 
61
261
  export async function fetchAllRemotes(cwd: string): Promise<void> {
62
- await runCommand(['git', 'fetch', '--all', '--prune'], { cwd })
262
+ await runGitCommand(['fetch', '--all', '--prune'], {
263
+ cwd,
264
+ requireConfiguredSsh: await anyRemoteRequiresConfiguredSsh(cwd),
265
+ sshHostKeyPolicy: 'accept-new',
266
+ })
63
267
  }
64
268
 
65
269
  export async function getWorktreeStatus(cwd: string): Promise<string> {
66
- const result = await runCommand(['git', 'status', '--short'], { cwd })
270
+ const result = await runGitCommand(['status', '--short'], { cwd })
67
271
  return result.stdout.trim()
68
272
  }
69
273
 
@@ -72,12 +276,12 @@ export async function isWorktreeDirty(cwd: string): Promise<boolean> {
72
276
  }
73
277
 
74
278
  export async function stageAll(cwd: string): Promise<void> {
75
- await runCommand(['git', 'add', '-A'], { cwd })
279
+ await runGitCommand(['add', '-A'], { cwd })
76
280
  }
77
281
 
78
282
  export async function getCurrentBranch(cwd: string): Promise<string> {
79
283
  try {
80
- const result = await runCommand(['git', 'symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd })
284
+ const result = await runGitCommand(['symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd })
81
285
  return result.stdout.trim()
82
286
  }
83
287
  catch (error) {
@@ -89,8 +293,8 @@ export async function getCurrentBranch(cwd: string): Promise<string> {
89
293
  }
90
294
 
91
295
  export async function getUpstreamBranch(cwd: string): Promise<string | null> {
92
- const result = await runCommand(
93
- ['git', 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
296
+ const result = await runGitCommand(
297
+ ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
94
298
  { cwd, allowFailure: true },
95
299
  )
96
300
 
@@ -102,15 +306,23 @@ export async function getUpstreamBranch(cwd: string): Promise<string | null> {
102
306
  }
103
307
 
104
308
  export async function pullCurrentBranch(cwd: string): Promise<void> {
105
- await runCommand(['git', 'pull', '--ff-only'], { cwd })
309
+ await runGitCommand(['pull', '--ff-only'], {
310
+ cwd,
311
+ requireConfiguredSsh: await upstreamRequiresConfiguredSsh(cwd),
312
+ sshHostKeyPolicy: 'accept-new',
313
+ })
106
314
  }
107
315
 
108
316
  export async function pullDefaultBranch(cwd: string, defaultBranch: string): Promise<void> {
109
- await runCommand(['git', 'pull', '--ff-only', 'origin', defaultBranch], { cwd })
317
+ await runGitCommand(['pull', '--ff-only', 'origin', defaultBranch], {
318
+ cwd,
319
+ requireConfiguredSsh: await remoteRequiresConfiguredSsh(cwd, 'origin'),
320
+ sshHostKeyPolicy: 'accept-new',
321
+ })
110
322
  }
111
323
 
112
324
  export async function getShortStatus(cwd: string): Promise<string> {
113
- const result = await runCommand(['git', 'status', '--short'], { cwd })
325
+ const result = await runGitCommand(['status', '--short'], { cwd })
114
326
  return result.stdout
115
327
  }
116
328
 
@@ -156,22 +368,22 @@ export async function getStatusCounts(cwd: string): Promise<GitStatusCounts> {
156
368
  }
157
369
 
158
370
  export async function getStagedDiff(cwd: string): Promise<string> {
159
- const result = await runCommand(['git', 'diff', '--cached'], { cwd })
371
+ const result = await runGitCommand(['diff', '--cached'], { cwd })
160
372
  return result.stdout
161
373
  }
162
374
 
163
375
  export async function getDiffAgainstHead(cwd: string): Promise<string> {
164
- const result = await runCommand(['git', 'diff', 'HEAD', '--'], { cwd })
376
+ const result = await runGitCommand(['diff', 'HEAD', '--'], { cwd })
165
377
  return result.stdout
166
378
  }
167
379
 
168
380
  export async function getRecentLog(cwd: string, count = 10): Promise<string> {
169
- const result = await runCommand(['git', 'log', '--oneline', `-${count}`], { cwd, allowFailure: true })
381
+ const result = await runGitCommand(['log', '--oneline', `-${count}`], { cwd, allowFailure: true })
170
382
  return result.stdout
171
383
  }
172
384
 
173
385
  export async function getStagedFiles(cwd: string): Promise<string[]> {
174
- const result = await runCommand(['git', 'diff', '--cached', '--name-only', '--diff-filter=ACMRTUXB'], { cwd })
386
+ const result = await runGitCommand(['diff', '--cached', '--name-only', '--diff-filter=ACMRTUXB'], { cwd })
175
387
  return result.stdout
176
388
  .split(/\r?\n/)
177
389
  .map(line => line.trim())
@@ -179,7 +391,7 @@ export async function getStagedFiles(cwd: string): Promise<string[]> {
179
391
  }
180
392
 
181
393
  export async function getChangedFilesAgainstHead(cwd: string): Promise<string[]> {
182
- const result = await runCommand(['git', 'diff', '--name-only', 'HEAD', '--diff-filter=ACMRTUXB'], { cwd })
394
+ const result = await runGitCommand(['diff', '--name-only', 'HEAD', '--diff-filter=ACMRTUXB'], { cwd })
183
395
  return result.stdout
184
396
  .split(/\r?\n/)
185
397
  .map(line => line.trim())
@@ -187,7 +399,7 @@ export async function getChangedFilesAgainstHead(cwd: string): Promise<string[]>
187
399
  }
188
400
 
189
401
  export async function getDiffStatAgainstHead(cwd: string): Promise<string> {
190
- const result = await runCommand(['git', 'diff', '--stat', 'HEAD'], { cwd, allowFailure: true })
402
+ const result = await runGitCommand(['diff', '--stat', 'HEAD'], { cwd, allowFailure: true })
191
403
  return result.stdout.trim()
192
404
  }
193
405
 
@@ -203,48 +415,43 @@ export function buildFallbackCommitMessage(files: string[]): string {
203
415
  return `Update ${basename(files[0]!) || files[0]} and ${files.length - 1} other files`
204
416
  }
205
417
 
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
418
  export async function commitWithMessage(cwd: string, message: string): Promise<void> {
215
- const delimiter = buildCommitDelimiter(message)
216
- const script = [
217
- 'git commit -F - <<\'__TELA_COMMIT__\'',
218
- message,
219
- '__TELA_COMMIT__',
220
- ].join('\n').replaceAll('__TELA_COMMIT__', delimiter)
221
-
222
- await runShell(script, { cwd })
419
+ await runGitCommand(['commit', '-F', '-'], {
420
+ cwd,
421
+ input: message,
422
+ })
223
423
  }
224
424
 
225
425
  export async function pushBranch(cwd: string, branch: string, setUpstream: boolean): Promise<void> {
226
426
  const args = setUpstream
227
- ? ['git', 'push', '--set-upstream', 'origin', branch]
228
- : ['git', 'push']
229
- await runCommand(args, { cwd })
427
+ ? ['push', '--set-upstream', 'origin', branch]
428
+ : ['push']
429
+ const requireConfiguredSsh = setUpstream
430
+ ? await remoteRequiresConfiguredSsh(cwd, 'origin')
431
+ : await upstreamRequiresConfiguredSsh(cwd)
432
+
433
+ await runGitCommand(args, {
434
+ cwd,
435
+ requireConfiguredSsh,
436
+ })
230
437
  }
231
438
 
232
439
  export async function getHeadShortSha(cwd: string): Promise<string> {
233
- const result = await runCommand(['git', 'rev-parse', '--short', 'HEAD'], { cwd })
440
+ const result = await runGitCommand(['rev-parse', '--short', 'HEAD'], { cwd })
234
441
  return result.stdout.trim()
235
442
  }
236
443
 
237
444
  export async function getHeadCommitHash(cwd: string): Promise<string> {
238
- const result = await runCommand(['git', 'rev-parse', 'HEAD'], { cwd })
445
+ const result = await runGitCommand(['rev-parse', 'HEAD'], { cwd })
239
446
  return result.stdout.trim()
240
447
  }
241
448
 
242
449
  export async function getHeadCommitMessage(cwd: string): Promise<string> {
243
- const result = await runCommand(['git', 'log', '-1', '--pretty=%B'], { cwd })
450
+ const result = await runGitCommand(['log', '-1', '--pretty=%B'], { cwd })
244
451
  return result.stdout.trim()
245
452
  }
246
453
 
247
454
  export async function getHeadShortMessage(cwd: string): Promise<string> {
248
- const result = await runCommand(['git', 'log', '-1', '--pretty=%s'], { cwd })
455
+ const result = await runGitCommand(['log', '-1', '--pretty=%s'], { cwd })
249
456
  return result.stdout.trim()
250
457
  }
@@ -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
  }
@@ -57,6 +57,7 @@ export function ReviewStep(props: {
57
57
  ? `${props.selectedSshKey.title} (${props.selectedSshKey.path})`
58
58
  : '<missing>'}
59
59
  </Text>
60
+ <Text>SSH private key: {props.values.sshPrivateKeyPath || '<missing>'}</Text>
60
61
  <Text>
61
62
  Clone root state:
62
63
  {' '}
@@ -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
  },