@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/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,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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
93
|
-
['
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
? ['
|
|
228
|
-
: ['
|
|
229
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
|
@@ -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
|
},
|