@meistrari/tela-cli 0.1.1 → 0.3.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.
@@ -0,0 +1,574 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
+ import { useInput } from 'ink'
3
+ import { ENVIRONMENT_PRESET_OPTIONS, resolveEnvironmentConfigForPreset } from '../../../config.ts'
4
+ import {
5
+ loadSshPublicKey,
6
+ validateSshPublicKeyPath,
7
+ } from '../../../forgejo.ts'
8
+ import type { TelaCliAuthState, TelaCliForgejoOrganization } from '../../../types.ts'
9
+ import { StepFrame } from './step-frame.tsx'
10
+ import type { BreadcrumbEntry } from './breadcrumb.tsx'
11
+ import {
12
+ getVisibleEditSteps,
13
+ summarizeStep,
14
+ type SetupStepDescriptor,
15
+ type SetupStepId,
16
+ } from './steps.ts'
17
+ import { DoneStep } from './steps/done-step.tsx'
18
+ import { EnvironmentStep } from './steps/environment-step.tsx'
19
+ import { OrganizationStep } from './steps/organization-step.tsx'
20
+ import { ReviewStep } from './steps/review-step.tsx'
21
+ import { RunningStep } from './steps/running-step.tsx'
22
+ import { SshKeyStep, buildSshKeyOptions } from './steps/ssh-key-step.tsx'
23
+ import { TextFieldStep } from './steps/text-field-step.tsx'
24
+ import {
25
+ CUSTOM_SSH_KEY_OPTION,
26
+ type SetupFormValues,
27
+ type SetupScreenProps,
28
+ type SetupScreenResult,
29
+ } from './types.ts'
30
+
31
+ export type { SetupScreenProps, SetupScreenResult } from './types.ts'
32
+
33
+ type SetupPhase =
34
+ | 'editing'
35
+ | 'review'
36
+ | 'authenticating'
37
+ | 'selectingOrganization'
38
+ | 'provisioning'
39
+ | 'done'
40
+
41
+ type TextStepId = Exclude<SetupStepId, 'environment' | 'sshKey'>
42
+
43
+ function pushProgressLines(lines: string[], message: string): string[] {
44
+ const nextLines = message
45
+ .split('\n')
46
+ .map(line => line.trim())
47
+ .filter(Boolean)
48
+
49
+ if (nextLines.length === 0) {
50
+ return lines
51
+ }
52
+
53
+ return [...lines, ...nextLines].slice(-12)
54
+ }
55
+
56
+ export function SetupScreen(props: SetupScreenProps): React.ReactElement {
57
+ const [phase, setPhase] = useState<SetupPhase>('editing')
58
+ const [stepIndex, setStepIndex] = useState(0)
59
+ const [values, setValues] = useState<SetupFormValues>({
60
+ environment: props.defaults.environment,
61
+ agentApiUrl: props.defaults.agentApiUrl,
62
+ authApiUrl: props.defaults.authApiUrl,
63
+ authDashboardUrl: props.defaults.authDashboardUrl,
64
+ telaApiUrl: props.defaults.telaApiUrl,
65
+ requesterApplicationId: props.defaults.requesterApplicationId,
66
+ targetApplicationId: props.defaults.targetApplicationId,
67
+ cloneRoot: props.defaults.cloneRoot,
68
+ sshKeySelection: props.sshPublicKeys[0]?.path ?? CUSTOM_SSH_KEY_OPTION,
69
+ sshKeyPath: '',
70
+ })
71
+ const [errors, setErrors] = useState<Partial<Record<SetupStepId, string>>>({})
72
+ const [createCloneRoot, setCreateCloneRoot] = useState(true)
73
+ const [progressLines, setProgressLines] = useState<string[]>([])
74
+ const [verificationUrl, setVerificationUrl] = useState<string | null>(null)
75
+ const [userCode, setUserCode] = useState<string | null>(null)
76
+ const [runningStage, setRunningStage] = useState<'auth' | 'provision'>('auth')
77
+ const [authenticated, setAuthenticated] = useState<TelaCliAuthState | null>(null)
78
+ const [organizationOptions, setOrganizationOptions] = useState<TelaCliForgejoOrganization[]>([])
79
+ const [organizationIndex, setOrganizationIndex] = useState(0)
80
+ const [preferredOrganizationId, setPreferredOrganizationId] = useState<string | undefined>(undefined)
81
+ const [selectedOrganization, setSelectedOrganization] = useState<TelaCliForgejoOrganization | null>(null)
82
+ const [result, setResult] = useState<SetupScreenResult | null>(null)
83
+ const authStartedRef = useRef(false)
84
+ const provisionStartedRef = useRef(false)
85
+
86
+ const visibleSteps = useMemo(() => getVisibleEditSteps(values), [values])
87
+ const safeStepIndex = Math.min(stepIndex, Math.max(visibleSteps.length - 1, 0))
88
+ const currentStep: SetupStepDescriptor | undefined = visibleSteps[safeStepIndex]
89
+
90
+ const resolvedConfig = useMemo(() => {
91
+ try {
92
+ return resolveEnvironmentConfigForPreset(values.environment, values)
93
+ }
94
+ catch {
95
+ return {
96
+ environment: values.environment,
97
+ agentApiUrl: values.agentApiUrl,
98
+ authApiUrl: values.authApiUrl,
99
+ authDashboardUrl: values.authDashboardUrl,
100
+ telaApiUrl: values.telaApiUrl,
101
+ requesterApplicationId: values.requesterApplicationId,
102
+ targetApplicationId: values.targetApplicationId,
103
+ }
104
+ }
105
+ }, [values])
106
+
107
+ const sshKeyOptions = useMemo(() => buildSshKeyOptions(props.sshPublicKeys), [props.sshPublicKeys])
108
+ const selectedSshKey = useMemo(() => {
109
+ if (values.sshKeySelection === CUSTOM_SSH_KEY_OPTION) {
110
+ try {
111
+ return values.sshKeyPath.trim() ? loadSshPublicKey(values.sshKeyPath) : undefined
112
+ }
113
+ catch {
114
+ return undefined
115
+ }
116
+ }
117
+
118
+ return props.sshPublicKeys.find(key => key.path === values.sshKeySelection)
119
+ }, [props.sshPublicKeys, values.sshKeyPath, values.sshKeySelection])
120
+ const cloneRootState = useMemo(() => props.inspectCloneRoot(values.cloneRoot), [props, values.cloneRoot])
121
+
122
+ const breadcrumb = useMemo<BreadcrumbEntry[]>(() => {
123
+ const entries: BreadcrumbEntry[] = []
124
+ const answered = visibleSteps.slice(0, safeStepIndex)
125
+ for (const step of answered) {
126
+ entries.push({
127
+ label: step.label,
128
+ value: summarizeStep(step, values, selectedSshKey?.title),
129
+ })
130
+ }
131
+
132
+ return entries
133
+ }, [safeStepIndex, selectedSshKey, values, visibleSteps])
134
+
135
+ const reviewResolved = Boolean(authenticated && selectedOrganization)
136
+
137
+ function resetResolvedSetup(): void {
138
+ setAuthenticated(null)
139
+ setOrganizationOptions([])
140
+ setOrganizationIndex(0)
141
+ setPreferredOrganizationId(undefined)
142
+ setSelectedOrganization(null)
143
+ setProgressLines([])
144
+ setVerificationUrl(null)
145
+ setUserCode(null)
146
+ setRunningStage('auth')
147
+ }
148
+
149
+ useEffect(() => {
150
+ if (phase !== 'authenticating') {
151
+ authStartedRef.current = false
152
+ }
153
+
154
+ if (phase !== 'provisioning') {
155
+ provisionStartedRef.current = false
156
+ }
157
+ }, [phase])
158
+
159
+ useEffect(() => {
160
+ if (phase !== 'authenticating' || authStartedRef.current) {
161
+ return
162
+ }
163
+
164
+ authStartedRef.current = true
165
+ resetResolvedSetup()
166
+ setRunningStage('auth')
167
+ setProgressLines(['Starting Tela sign-in...'])
168
+
169
+ void props.authenticate({
170
+ ...resolvedConfig,
171
+ cloneRoot: values.cloneRoot,
172
+ }, (event) => {
173
+ if (event.type === 'user_code') {
174
+ setVerificationUrl(event.verificationUrl ?? null)
175
+ setUserCode(event.userCode ?? null)
176
+ }
177
+ setProgressLines(current => pushProgressLines(current, event.message))
178
+ })
179
+ .then(async (auth) => {
180
+ setAuthenticated(auth)
181
+ setVerificationUrl(null)
182
+ setUserCode(null)
183
+ setProgressLines(current => pushProgressLines(
184
+ current,
185
+ `Signed in as ${auth.user.email} in ${auth.workspace.title}.`,
186
+ ))
187
+ setProgressLines(current => pushProgressLines(current, 'Loading workspace organizations...'))
188
+
189
+ const organizationResolution = await props.resolveOrganizations({
190
+ ...resolvedConfig,
191
+ cloneRoot: values.cloneRoot,
192
+ auth,
193
+ })
194
+
195
+ setOrganizationOptions(organizationResolution.organizations)
196
+ setPreferredOrganizationId(organizationResolution.preferredOrganization?.id)
197
+
198
+ if (organizationResolution.organizations.length === 1) {
199
+ const organization = organizationResolution.organizations[0]!
200
+ setSelectedOrganization(organization)
201
+ setProgressLines(current => pushProgressLines(
202
+ current,
203
+ `Using workspace organization ${organization.name}.`,
204
+ ))
205
+ setPhase('review')
206
+ return
207
+ }
208
+
209
+ const preferredIndex = organizationResolution.organizations.findIndex(
210
+ organization => organization.id === organizationResolution.preferredOrganization?.id,
211
+ )
212
+ setOrganizationIndex(Math.max(preferredIndex, 0))
213
+ setPhase('selectingOrganization')
214
+ })
215
+ .catch((error) => {
216
+ props.controller.reject(error)
217
+ })
218
+ }, [phase, props, resolvedConfig, values.cloneRoot])
219
+
220
+ useEffect(() => {
221
+ if (phase !== 'provisioning' || provisionStartedRef.current) {
222
+ return
223
+ }
224
+
225
+ provisionStartedRef.current = true
226
+ setRunningStage('provision')
227
+ setVerificationUrl(null)
228
+ setUserCode(null)
229
+
230
+ if (!selectedSshKey) {
231
+ props.controller.reject(new Error('SSH public key is required.'))
232
+ return
233
+ }
234
+
235
+ if (!authenticated || !selectedOrganization) {
236
+ props.controller.reject(new Error('Authenticated organization selection is required.'))
237
+ return
238
+ }
239
+
240
+ setProgressLines(current => pushProgressLines(
241
+ current,
242
+ `Provisioning Forgejo access for ${selectedOrganization.name}.`,
243
+ ))
244
+
245
+ void props.execute({
246
+ ...resolvedConfig,
247
+ cloneRoot: values.cloneRoot,
248
+ createCloneRoot,
249
+ sshKey: selectedSshKey,
250
+ auth: authenticated,
251
+ organization: selectedOrganization,
252
+ }, (event) => {
253
+ setProgressLines(current => pushProgressLines(current, event.message))
254
+ })
255
+ .then((nextResult) => {
256
+ setResult(nextResult)
257
+ setPhase('done')
258
+ })
259
+ .catch((error) => {
260
+ props.controller.reject(error)
261
+ })
262
+ }, [
263
+ authenticated,
264
+ createCloneRoot,
265
+ phase,
266
+ props,
267
+ resolvedConfig,
268
+ selectedOrganization,
269
+ selectedSshKey,
270
+ values.cloneRoot,
271
+ ])
272
+
273
+ useEffect(() => {
274
+ if (phase === 'done' && result) {
275
+ props.controller.resolve(result, 80)
276
+ }
277
+ }, [phase, props, result])
278
+
279
+ useInput((input, key) => {
280
+ if (phase === 'authenticating' || phase === 'provisioning' || phase === 'done') {
281
+ return
282
+ }
283
+
284
+ if (phase === 'selectingOrganization') {
285
+ if (key.escape || key.leftArrow) {
286
+ resetResolvedSetup()
287
+ setPhase('review')
288
+ return
289
+ }
290
+
291
+ if (key.upArrow || key.downArrow) {
292
+ const direction = key.upArrow ? -1 : 1
293
+ setOrganizationIndex(current => {
294
+ if (organizationOptions.length === 0) {
295
+ return current
296
+ }
297
+
298
+ return (organizationOptions.length + current + direction) % organizationOptions.length
299
+ })
300
+ return
301
+ }
302
+
303
+ if (key.return) {
304
+ const organization = organizationOptions[organizationIndex]
305
+ if (!organization) {
306
+ props.controller.reject(new Error('Organization selection is required.'))
307
+ return
308
+ }
309
+
310
+ setSelectedOrganization(organization)
311
+ setProgressLines(current => pushProgressLines(
312
+ current,
313
+ `Selected workspace organization ${organization.name}.`,
314
+ ))
315
+ setPhase('review')
316
+ }
317
+
318
+ return
319
+ }
320
+
321
+ if (phase === 'review') {
322
+ if (key.escape || key.leftArrow) {
323
+ if (reviewResolved) {
324
+ if (organizationOptions.length > 1) {
325
+ setSelectedOrganization(null)
326
+ setPhase('selectingOrganization')
327
+ return
328
+ }
329
+
330
+ resetResolvedSetup()
331
+ return
332
+ }
333
+
334
+ setPhase('editing')
335
+ setStepIndex(Math.max(visibleSteps.length - 1, 0))
336
+ return
337
+ }
338
+
339
+ if (!cloneRootState.exists && (input === ' ' || key.rightArrow)) {
340
+ setCreateCloneRoot(current => !current)
341
+ return
342
+ }
343
+
344
+ if (key.return) {
345
+ setPhase(reviewResolved ? 'provisioning' : 'authenticating')
346
+ }
347
+
348
+ return
349
+ }
350
+
351
+ if (!currentStep) {
352
+ return
353
+ }
354
+
355
+ if (key.escape || key.leftArrow) {
356
+ if (safeStepIndex === 0) {
357
+ props.controller.reject(new Error('Setup cancelled.'))
358
+ return
359
+ }
360
+
361
+ setErrors(current => ({ ...current, [currentStep.id]: undefined }))
362
+ setStepIndex(current => Math.max(current - 1, 0))
363
+ return
364
+ }
365
+
366
+ if (currentStep.kind === 'select-environment' && (key.upArrow || key.downArrow)) {
367
+ const direction = key.upArrow ? -1 : 1
368
+ setValues((current) => {
369
+ const currentIdx = ENVIRONMENT_PRESET_OPTIONS.findIndex(option => option.value === current.environment)
370
+ const nextIdx = (ENVIRONMENT_PRESET_OPTIONS.length + currentIdx + direction) % ENVIRONMENT_PRESET_OPTIONS.length
371
+ return {
372
+ ...current,
373
+ environment: ENVIRONMENT_PRESET_OPTIONS[nextIdx]?.value ?? current.environment,
374
+ }
375
+ })
376
+ return
377
+ }
378
+
379
+ if (currentStep.kind === 'select-ssh-key' && (key.upArrow || key.downArrow)) {
380
+ const direction = key.upArrow ? -1 : 1
381
+ setValues((current) => {
382
+ const currentIdx = sshKeyOptions.findIndex(option => option.key === current.sshKeySelection)
383
+ const nextIdx = (sshKeyOptions.length + currentIdx + direction) % sshKeyOptions.length
384
+ return {
385
+ ...current,
386
+ sshKeySelection: sshKeyOptions[nextIdx]?.key ?? current.sshKeySelection,
387
+ }
388
+ })
389
+ return
390
+ }
391
+
392
+ if (currentStep.kind === 'text' && (key.backspace || key.delete)) {
393
+ const fieldKey = currentStep.id as TextStepId
394
+ setValues(current => ({
395
+ ...current,
396
+ [fieldKey]: current[fieldKey].slice(0, -1),
397
+ }))
398
+ setErrors(current => ({ ...current, [currentStep.id]: undefined }))
399
+ return
400
+ }
401
+
402
+ if (key.return) {
403
+ const validationError = validateStep(currentStep, values, props, selectedSshKey)
404
+ if (validationError) {
405
+ setErrors(current => ({ ...current, [currentStep.id]: validationError }))
406
+ return
407
+ }
408
+
409
+ const nextIndex = safeStepIndex + 1
410
+ const nextVisible = getVisibleEditSteps(values)
411
+ if (nextIndex >= nextVisible.length) {
412
+ setPhase('review')
413
+ return
414
+ }
415
+
416
+ setStepIndex(nextIndex)
417
+ return
418
+ }
419
+
420
+ if (currentStep.kind === 'text' && input) {
421
+ const fieldKey = currentStep.id as TextStepId
422
+ setValues(current => ({
423
+ ...current,
424
+ [fieldKey]: `${current[fieldKey]}${input}`,
425
+ }))
426
+ setErrors(current => ({ ...current, [currentStep.id]: undefined }))
427
+ }
428
+ })
429
+
430
+ if (phase === 'authenticating' || phase === 'provisioning') {
431
+ return (
432
+ <RunningStep
433
+ stage={runningStage}
434
+ organizationName={selectedOrganization?.name}
435
+ verificationUrl={verificationUrl}
436
+ userCode={userCode}
437
+ progressLines={progressLines}
438
+ />
439
+ )
440
+ }
441
+
442
+ if (phase === 'done' && result) {
443
+ return <DoneStep result={result} />
444
+ }
445
+
446
+ if (phase === 'selectingOrganization' && authenticated) {
447
+ return (
448
+ <OrganizationStep
449
+ auth={authenticated}
450
+ organizations={organizationOptions}
451
+ currentIndex={organizationIndex}
452
+ preferredOrganizationId={preferredOrganizationId}
453
+ />
454
+ )
455
+ }
456
+
457
+ if (phase === 'review') {
458
+ return (
459
+ <ReviewStep
460
+ values={values}
461
+ resolvedConfig={resolvedConfig}
462
+ auth={authenticated}
463
+ selectedOrganization={selectedOrganization}
464
+ selectedSshKey={selectedSshKey}
465
+ cloneRootState={cloneRootState}
466
+ createCloneRoot={createCloneRoot}
467
+ />
468
+ )
469
+ }
470
+
471
+ if (!currentStep) {
472
+ return (
473
+ <StepFrame
474
+ stepNumber={1}
475
+ totalSteps={1}
476
+ stepLabel="Setup"
477
+ question="Preparing setup…"
478
+ breadcrumb={[]}
479
+ footer="Esc cancel"
480
+ >
481
+ <></>
482
+ </StepFrame>
483
+ )
484
+ }
485
+
486
+ const footer = buildFooter(currentStep, safeStepIndex === 0)
487
+ const error = errors[currentStep.id]
488
+
489
+ return (
490
+ <StepFrame
491
+ stepNumber={safeStepIndex + 1}
492
+ totalSteps={visibleSteps.length}
493
+ stepLabel={currentStep.label}
494
+ question={currentStep.question}
495
+ breadcrumb={breadcrumb}
496
+ footer={footer}
497
+ >
498
+ {renderStepBody(currentStep, values, error, sshKeyOptions, props.sshPublicKeys.length > 0)}
499
+ </StepFrame>
500
+ )
501
+ }
502
+
503
+ function renderStepBody(
504
+ step: SetupStepDescriptor,
505
+ values: SetupFormValues,
506
+ error: string | undefined,
507
+ sshKeyOptions: ReturnType<typeof buildSshKeyOptions>,
508
+ hasDiscoveredKeys: boolean,
509
+ ): React.ReactElement {
510
+ if (step.kind === 'select-environment') {
511
+ return <EnvironmentStep values={values} />
512
+ }
513
+
514
+ if (step.kind === 'select-ssh-key') {
515
+ return (
516
+ <SshKeyStep
517
+ options={sshKeyOptions}
518
+ selection={values.sshKeySelection}
519
+ hasDiscoveredKeys={hasDiscoveredKeys}
520
+ />
521
+ )
522
+ }
523
+
524
+ const fieldKey = step.id as TextStepId
525
+ return (
526
+ <TextFieldStep
527
+ value={values[fieldKey]}
528
+ placeholder={step.placeholder}
529
+ error={error}
530
+ />
531
+ )
532
+ }
533
+
534
+ function buildFooter(step: SetupStepDescriptor, isFirstStep: boolean): string {
535
+ const back = isFirstStep ? 'Esc cancel' : 'Esc back'
536
+ switch (step.kind) {
537
+ case 'select-environment':
538
+ return `↑/↓ choose · Enter continue · ${back}`
539
+ case 'select-ssh-key':
540
+ return `↑/↓ choose · Enter continue · ${back}`
541
+ case 'text':
542
+ return `Type to edit · Enter continue · ${back}`
543
+ }
544
+ }
545
+
546
+ function validateStep(
547
+ step: SetupStepDescriptor,
548
+ values: SetupFormValues,
549
+ props: SetupScreenProps,
550
+ selectedSshKey: ReturnType<typeof loadSshPublicKey> | undefined,
551
+ ): string | undefined {
552
+ switch (step.id) {
553
+ case 'environment':
554
+ return undefined
555
+ case 'agentApiUrl':
556
+ return props.validateAgentApiUrl(values.agentApiUrl)
557
+ case 'authApiUrl':
558
+ return props.validateAuthApiUrl(values.authApiUrl)
559
+ case 'authDashboardUrl':
560
+ return props.validateAuthDashboardUrl(values.authDashboardUrl)
561
+ case 'telaApiUrl':
562
+ return props.validateTelaApiUrl(values.telaApiUrl)
563
+ case 'requesterApplicationId':
564
+ return props.validateApplicationId(values.requesterApplicationId, 'Requester application ID')
565
+ case 'targetApplicationId':
566
+ return props.validateApplicationId(values.targetApplicationId, 'Target application ID')
567
+ case 'cloneRoot':
568
+ return props.validateCloneRoot(values.cloneRoot)
569
+ case 'sshKey':
570
+ return selectedSshKey ? undefined : 'SSH public key is required'
571
+ case 'sshKeyPath':
572
+ return validateSshPublicKeyPath(values.sshKeyPath)
573
+ }
574
+ }
@@ -0,0 +1,30 @@
1
+ import React, { type ReactNode } from 'react'
2
+ import { AppFrame, Section } from '../../components.tsx'
3
+ import { Breadcrumb, type BreadcrumbEntry } from './breadcrumb.tsx'
4
+
5
+ export interface StepFrameProps {
6
+ stepNumber: number
7
+ totalSteps: number
8
+ stepLabel: string
9
+ question: string
10
+ breadcrumb: BreadcrumbEntry[]
11
+ footer?: string
12
+ status?: string
13
+ children: ReactNode
14
+ }
15
+
16
+ export function StepFrame(props: StepFrameProps): React.ReactElement {
17
+ return (
18
+ <AppFrame
19
+ title="Setup"
20
+ subtitle={`Step ${props.stepNumber} of ${props.totalSteps} — ${props.stepLabel}`}
21
+ footer={props.footer}
22
+ status={props.status}
23
+ >
24
+ <Breadcrumb entries={props.breadcrumb} />
25
+ <Section title={props.question}>
26
+ {props.children}
27
+ </Section>
28
+ </AppFrame>
29
+ )
30
+ }
@@ -0,0 +1,28 @@
1
+ import React from 'react'
2
+ import { Text } from 'ink'
3
+ import { AppFrame, Section } from '../../../components.tsx'
4
+ import type { SetupScreenResult } from '../types.ts'
5
+
6
+ export function DoneStep(props: { result: SetupScreenResult }): React.ReactElement {
7
+ return (
8
+ <AppFrame
9
+ title="Setup"
10
+ subtitle="Setup complete."
11
+ >
12
+ <Section title="Authenticated">
13
+ <Text>User: {props.result.config.auth?.user.email ?? '<missing>'}</Text>
14
+ <Text>Workspace: {props.result.config.auth?.workspace.title ?? '<missing>'}</Text>
15
+ <Text>Organization: {props.result.config.forgejoOrganization?.name ?? '<missing>'}</Text>
16
+ <Text>Clone root: {props.result.config.cloneRoot}</Text>
17
+ <Text>Forgejo user: {props.result.config.forgejoUserRegistered ? 'registered' : 'pending'}</Text>
18
+ </Section>
19
+ <Section title="Checks">
20
+ {props.result.checks.map(check => (
21
+ <Text key={check.label}>
22
+ {check.label}: {check.status} — {check.detail}
23
+ </Text>
24
+ ))}
25
+ </Section>
26
+ </AppFrame>
27
+ )
28
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react'
2
+ import { ENVIRONMENT_PRESET_OPTIONS } from '../../../../config.ts'
3
+ import { SelectList } from '../../../components.tsx'
4
+ import type { SetupFormValues } from '../types.ts'
5
+
6
+ export function EnvironmentStep(props: { values: SetupFormValues }): React.ReactElement {
7
+ const currentIndex = Math.max(
8
+ ENVIRONMENT_PRESET_OPTIONS.findIndex(option => option.value === props.values.environment),
9
+ 0,
10
+ )
11
+
12
+ return (
13
+ <SelectList
14
+ items={ENVIRONMENT_PRESET_OPTIONS.map(option => ({
15
+ key: option.value,
16
+ label: option.label,
17
+ hint: option.hint,
18
+ }))}
19
+ currentIndex={currentIndex}
20
+ />
21
+ )
22
+ }
@@ -0,0 +1,34 @@
1
+ import React from 'react'
2
+ import { Text } from 'ink'
3
+ import { AppFrame, Section, SelectList } from '../../../components.tsx'
4
+ import type { TelaCliAuthState, TelaCliForgejoOrganization } from '../../../../types.ts'
5
+
6
+ export function OrganizationStep(props: {
7
+ auth: TelaCliAuthState
8
+ organizations: TelaCliForgejoOrganization[]
9
+ currentIndex: number
10
+ preferredOrganizationId?: string
11
+ }): React.ReactElement {
12
+ return (
13
+ <AppFrame
14
+ title="Setup"
15
+ subtitle="Choose the workspace organization for Forgejo access."
16
+ footer="↑/↓ choose · Enter continue · Esc back"
17
+ >
18
+ <Section title="Authenticated">
19
+ <Text>User: {props.auth.user.email}</Text>
20
+ <Text>Workspace: {props.auth.workspace.title}</Text>
21
+ </Section>
22
+ <Section title="Forgejo organization">
23
+ <SelectList
24
+ items={props.organizations.map(organization => ({
25
+ key: organization.id,
26
+ label: organization.name,
27
+ hint: organization.id === props.preferredOrganizationId ? 'saved from previous setup' : undefined,
28
+ }))}
29
+ currentIndex={props.currentIndex}
30
+ />
31
+ </Section>
32
+ </AppFrame>
33
+ )
34
+ }