@meistrari/tela-cli 0.1.0 → 0.2.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 +18 -6
- package/package.json +5 -1
- package/postinstall.sh +19 -0
- package/src/agent-api.ts +102 -2
- package/src/assets.d.ts +4 -0
- package/src/auth.ts +423 -7
- package/src/commands/feedback.ts +1 -1
- package/src/commands/metadata.ts +18 -6
- package/src/commands/status.ts +10 -1
- package/src/config.ts +522 -147
- package/src/diagnostics.ts +127 -5
- package/src/forgejo.ts +189 -0
- package/src/ink/assets/160092__jorickhoofd__dog-bark-1.mp3 +0 -0
- package/src/ink/bark.tsx +14 -62
- package/src/ink/screens/setup/breadcrumb.tsx +23 -0
- package/src/ink/screens/setup/setup-screen.tsx +574 -0
- package/src/ink/screens/setup/step-frame.tsx +30 -0
- package/src/ink/screens/setup/steps/done-step.tsx +28 -0
- package/src/ink/screens/setup/steps/environment-step.tsx +22 -0
- package/src/ink/screens/setup/steps/organization-step.tsx +34 -0
- package/src/ink/screens/setup/steps/review-step.tsx +76 -0
- package/src/ink/screens/setup/steps/running-step.tsx +36 -0
- package/src/ink/screens/setup/steps/ssh-key-step.tsx +47 -0
- package/src/ink/screens/setup/steps/text-field-step.tsx +17 -0
- package/src/ink/screens/setup/steps.ts +145 -0
- package/src/ink/screens/setup/types.ts +63 -0
- package/src/program.ts +228 -96
- package/src/types.ts +64 -1
- package/src/ink/screens/setup-screen.tsx +0 -352
package/src/types.ts
CHANGED
|
@@ -1,7 +1,45 @@
|
|
|
1
|
+
export type TelaEnvironment = 'production' | 'staging' | 'custom'
|
|
2
|
+
|
|
3
|
+
export interface TelaCliUser {
|
|
4
|
+
id: string
|
|
5
|
+
email: string
|
|
6
|
+
name: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TelaCliWorkspace {
|
|
10
|
+
id: string
|
|
11
|
+
title: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TelaCliSession {
|
|
15
|
+
accessToken: string
|
|
16
|
+
refreshToken: string
|
|
17
|
+
expiresAt: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TelaCliForgejoOrganization {
|
|
21
|
+
id: string
|
|
22
|
+
name: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TelaCliAuthState {
|
|
26
|
+
user: TelaCliUser
|
|
27
|
+
workspace: TelaCliWorkspace
|
|
28
|
+
session: TelaCliSession
|
|
29
|
+
}
|
|
30
|
+
|
|
1
31
|
export interface TelaCliConfig {
|
|
32
|
+
environment: TelaEnvironment
|
|
2
33
|
agentApiUrl: string
|
|
34
|
+
authApiUrl: string
|
|
35
|
+
authDashboardUrl: string
|
|
36
|
+
telaApiUrl: string
|
|
3
37
|
cloneRoot: string
|
|
4
|
-
|
|
38
|
+
requesterApplicationId: string
|
|
39
|
+
targetApplicationId: string
|
|
40
|
+
auth: TelaCliAuthState | null
|
|
41
|
+
forgejoOrganization: TelaCliForgejoOrganization | null
|
|
42
|
+
forgejoUserRegistered: boolean
|
|
5
43
|
}
|
|
6
44
|
|
|
7
45
|
export interface OrganizationSummary {
|
|
@@ -37,6 +75,31 @@ export interface RepositoryLookupResponse {
|
|
|
37
75
|
error?: string
|
|
38
76
|
}
|
|
39
77
|
|
|
78
|
+
export interface OrganizationListResponse {
|
|
79
|
+
success: boolean
|
|
80
|
+
organizations?: TelaCliForgejoOrganization[]
|
|
81
|
+
error?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ForgejoUserSshKeyInput {
|
|
85
|
+
title: string
|
|
86
|
+
key: string
|
|
87
|
+
readOnly?: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ForgejoUserCreateResponse {
|
|
91
|
+
success: boolean
|
|
92
|
+
username?: string
|
|
93
|
+
email?: string
|
|
94
|
+
organizationName?: string
|
|
95
|
+
teamName?: string
|
|
96
|
+
teamId?: number
|
|
97
|
+
userCreated?: boolean
|
|
98
|
+
teamCreated?: boolean
|
|
99
|
+
sshKeyCreated?: boolean
|
|
100
|
+
error?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
40
103
|
export type AgentFeedbackStatus = 'success' | 'failed'
|
|
41
104
|
|
|
42
105
|
export interface AgentFeedbackItem {
|
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { Box, Text, useInput } from 'ink'
|
|
3
|
-
import type { TelaCliConfig } from '../../types.ts'
|
|
4
|
-
import {
|
|
5
|
-
AGENT_API_BROWSER_HELP,
|
|
6
|
-
AGENT_API_PRESET_OPTIONS,
|
|
7
|
-
inferAgentApiPreset,
|
|
8
|
-
resolveAgentApiUrlForPreset,
|
|
9
|
-
} from '../../config.ts'
|
|
10
|
-
import { AppFrame, Callout, KeyHints, Section, SelectList, SpinnerText, TextEntry } from '../components.tsx'
|
|
11
|
-
import type { CommandScreenController } from '../runtime.tsx'
|
|
12
|
-
|
|
13
|
-
export interface SetupScreenResult {
|
|
14
|
-
ok: true
|
|
15
|
-
config: TelaCliConfig
|
|
16
|
-
warnings: string[]
|
|
17
|
-
nextActions: string[]
|
|
18
|
-
checks: Array<{ label: string, status: string, detail: string }>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface SetupScreenProps {
|
|
22
|
-
controller: CommandScreenController<SetupScreenResult>
|
|
23
|
-
currentConfigSummary: string[]
|
|
24
|
-
defaults: {
|
|
25
|
-
agentApiUrl: string
|
|
26
|
-
cloneRoot: string
|
|
27
|
-
apiKey: string
|
|
28
|
-
}
|
|
29
|
-
hasExistingApiKey: boolean
|
|
30
|
-
validateAgentApiUrl: (value: string) => string | undefined
|
|
31
|
-
validateCloneRoot: (value: string) => string | undefined
|
|
32
|
-
validateApiKey: (value: string, existingApiKey?: string) => string | undefined
|
|
33
|
-
inspectCloneRoot: (path: string) => {
|
|
34
|
-
exists: boolean
|
|
35
|
-
isDirectory: boolean
|
|
36
|
-
}
|
|
37
|
-
execute: (input: {
|
|
38
|
-
agentApiUrl: string
|
|
39
|
-
cloneRoot: string
|
|
40
|
-
apiKey: string
|
|
41
|
-
createCloneRoot: boolean
|
|
42
|
-
}) => Promise<SetupScreenResult>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
type SetupField = 'agentApiPreset' | 'agentApiUrl' | 'cloneRoot' | 'apiKey'
|
|
46
|
-
type SetupPhase = 'editing' | 'review' | 'running' | 'done'
|
|
47
|
-
|
|
48
|
-
function fieldLabel(field: SetupField): string {
|
|
49
|
-
switch (field) {
|
|
50
|
-
case 'agentApiPreset':
|
|
51
|
-
return 'Agent API target'
|
|
52
|
-
case 'agentApiUrl':
|
|
53
|
-
return 'Custom Agent API origin URL'
|
|
54
|
-
case 'cloneRoot':
|
|
55
|
-
return 'Clone root directory'
|
|
56
|
-
case 'apiKey':
|
|
57
|
-
return 'API key'
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function SetupScreen(props: SetupScreenProps): React.ReactElement {
|
|
62
|
-
const initialAgentApiPreset = useMemo(() => inferAgentApiPreset(props.defaults.agentApiUrl), [props.defaults.agentApiUrl])
|
|
63
|
-
const [phase, setPhase] = useState<SetupPhase>('editing')
|
|
64
|
-
const [fieldIndex, setFieldIndex] = useState(0)
|
|
65
|
-
const [values, setValues] = useState({
|
|
66
|
-
agentApiPreset: initialAgentApiPreset,
|
|
67
|
-
agentApiUrl: initialAgentApiPreset === 'custom' ? props.defaults.agentApiUrl : '',
|
|
68
|
-
cloneRoot: props.defaults.cloneRoot,
|
|
69
|
-
apiKey: '',
|
|
70
|
-
})
|
|
71
|
-
const [errors, setErrors] = useState<Partial<Record<SetupField, string>>>({})
|
|
72
|
-
const [createCloneRoot, setCreateCloneRoot] = useState(true)
|
|
73
|
-
const [result, setResult] = useState<SetupScreenResult | null>(null)
|
|
74
|
-
const startedRef = useRef(false)
|
|
75
|
-
|
|
76
|
-
const fieldOrder = useMemo(() => {
|
|
77
|
-
const order: SetupField[] = ['agentApiPreset']
|
|
78
|
-
if (values.agentApiPreset === 'custom') {
|
|
79
|
-
order.push('agentApiUrl')
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
order.push('cloneRoot', 'apiKey')
|
|
83
|
-
return order
|
|
84
|
-
}, [values.agentApiPreset])
|
|
85
|
-
const currentField = fieldOrder[fieldIndex]!
|
|
86
|
-
const cloneRootState = useMemo(() => props.inspectCloneRoot(values.cloneRoot), [props, values.cloneRoot])
|
|
87
|
-
const selectedPresetIndex = useMemo(
|
|
88
|
-
() => AGENT_API_PRESET_OPTIONS.findIndex(option => option.value === values.agentApiPreset),
|
|
89
|
-
[values.agentApiPreset],
|
|
90
|
-
)
|
|
91
|
-
const resolvedAgentApiUrl = useMemo(() => {
|
|
92
|
-
if (values.agentApiPreset === 'custom') {
|
|
93
|
-
return props.validateAgentApiUrl(values.agentApiUrl)
|
|
94
|
-
? values.agentApiUrl || '<invalid custom origin>'
|
|
95
|
-
: values.agentApiUrl
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
return resolveAgentApiUrlForPreset(values.agentApiPreset, values.agentApiUrl)
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
return values.agentApiUrl || '<invalid custom origin>'
|
|
103
|
-
}
|
|
104
|
-
}, [props, values.agentApiPreset, values.agentApiUrl])
|
|
105
|
-
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
if (phase !== 'running' || startedRef.current) {
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
startedRef.current = true
|
|
112
|
-
void props.execute({
|
|
113
|
-
agentApiUrl: resolvedAgentApiUrl,
|
|
114
|
-
cloneRoot: values.cloneRoot,
|
|
115
|
-
apiKey: values.apiKey,
|
|
116
|
-
createCloneRoot,
|
|
117
|
-
})
|
|
118
|
-
.then((nextResult) => {
|
|
119
|
-
setResult(nextResult)
|
|
120
|
-
setPhase('done')
|
|
121
|
-
})
|
|
122
|
-
.catch((error) => {
|
|
123
|
-
props.controller.reject(error)
|
|
124
|
-
})
|
|
125
|
-
}, [createCloneRoot, phase, props, values])
|
|
126
|
-
|
|
127
|
-
useEffect(() => {
|
|
128
|
-
if (phase === 'done' && result) {
|
|
129
|
-
props.controller.resolve(result, 40)
|
|
130
|
-
}
|
|
131
|
-
}, [phase, props, result])
|
|
132
|
-
|
|
133
|
-
useInput((input, key) => {
|
|
134
|
-
if (phase === 'editing') {
|
|
135
|
-
if (key.escape) {
|
|
136
|
-
props.controller.reject(new Error('Setup cancelled.'))
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (currentField === 'agentApiPreset' && (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow)) {
|
|
141
|
-
const direction = (key.upArrow || key.leftArrow) ? -1 : 1
|
|
142
|
-
setValues(current => {
|
|
143
|
-
const currentIndex = AGENT_API_PRESET_OPTIONS.findIndex(option => option.value === current.agentApiPreset)
|
|
144
|
-
const nextIndex = (AGENT_API_PRESET_OPTIONS.length + currentIndex + direction) % AGENT_API_PRESET_OPTIONS.length
|
|
145
|
-
const nextPreset = AGENT_API_PRESET_OPTIONS[nextIndex]?.value ?? current.agentApiPreset
|
|
146
|
-
return {
|
|
147
|
-
...current,
|
|
148
|
-
agentApiPreset: nextPreset,
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if ((key.backspace || key.delete) && currentField !== 'agentApiPreset') {
|
|
155
|
-
setValues(current => ({
|
|
156
|
-
...current,
|
|
157
|
-
[currentField]: current[currentField].slice(0, -1),
|
|
158
|
-
}))
|
|
159
|
-
setErrors(current => ({
|
|
160
|
-
...current,
|
|
161
|
-
[currentField]: undefined,
|
|
162
|
-
}))
|
|
163
|
-
return
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (key.return) {
|
|
167
|
-
const error = currentField === 'agentApiPreset'
|
|
168
|
-
? undefined
|
|
169
|
-
: currentField === 'agentApiUrl'
|
|
170
|
-
? props.validateAgentApiUrl(values.agentApiUrl)
|
|
171
|
-
: currentField === 'cloneRoot'
|
|
172
|
-
? props.validateCloneRoot(values.cloneRoot)
|
|
173
|
-
: props.validateApiKey(values.apiKey, props.hasExistingApiKey ? props.defaults.apiKey : undefined)
|
|
174
|
-
|
|
175
|
-
if (error) {
|
|
176
|
-
setErrors(current => ({
|
|
177
|
-
...current,
|
|
178
|
-
[currentField]: error,
|
|
179
|
-
}))
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (fieldIndex === fieldOrder.length - 1) {
|
|
184
|
-
setPhase('review')
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
setFieldIndex(current => current + 1)
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (key.tab) {
|
|
193
|
-
setFieldIndex(current => (current + 1) % fieldOrder.length)
|
|
194
|
-
return
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (currentField === 'agentApiPreset') {
|
|
198
|
-
return
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (input) {
|
|
202
|
-
setValues(current => ({
|
|
203
|
-
...current,
|
|
204
|
-
[currentField]: `${current[currentField]}${input}`,
|
|
205
|
-
}))
|
|
206
|
-
setErrors(current => ({
|
|
207
|
-
...current,
|
|
208
|
-
[currentField]: undefined,
|
|
209
|
-
}))
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (phase === 'review') {
|
|
216
|
-
if (key.escape) {
|
|
217
|
-
setPhase('editing')
|
|
218
|
-
return
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (!cloneRootState.exists && (input === ' ' || key.leftArrow || key.rightArrow)) {
|
|
222
|
-
setCreateCloneRoot(current => !current)
|
|
223
|
-
return
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (key.return) {
|
|
227
|
-
setPhase('running')
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
return (
|
|
233
|
-
<AppFrame
|
|
234
|
-
title="Setup"
|
|
235
|
-
subtitle="Configure the local Tela environment with an Ink-driven wizard."
|
|
236
|
-
footer={
|
|
237
|
-
phase === 'editing'
|
|
238
|
-
? '↑/↓ choose target · Type to edit fields · Enter next · Tab switch field · Esc cancel'
|
|
239
|
-
: phase === 'review'
|
|
240
|
-
? 'Enter save and run checks · Space toggle clone-root creation · Esc back'
|
|
241
|
-
: undefined
|
|
242
|
-
}
|
|
243
|
-
status={phase === 'running' ? 'Saving config and running checks' : undefined}
|
|
244
|
-
>
|
|
245
|
-
{phase === 'editing' ? (
|
|
246
|
-
<>
|
|
247
|
-
<Section title="Current config">
|
|
248
|
-
{props.currentConfigSummary.map(line => (
|
|
249
|
-
<Text key={line}>{line}</Text>
|
|
250
|
-
))}
|
|
251
|
-
</Section>
|
|
252
|
-
<Section title="Agent API target">
|
|
253
|
-
<SelectList
|
|
254
|
-
items={AGENT_API_PRESET_OPTIONS.map(option => ({
|
|
255
|
-
key: option.value,
|
|
256
|
-
label: option.label,
|
|
257
|
-
hint: option.hint,
|
|
258
|
-
}))}
|
|
259
|
-
currentIndex={selectedPresetIndex}
|
|
260
|
-
/>
|
|
261
|
-
</Section>
|
|
262
|
-
<Callout tone="info">{AGENT_API_BROWSER_HELP}</Callout>
|
|
263
|
-
<Section title="Fields">
|
|
264
|
-
<Text>Resolved Agent API origin: {resolvedAgentApiUrl}</Text>
|
|
265
|
-
{values.agentApiPreset === 'custom' ? (
|
|
266
|
-
<Box marginTop={1}>
|
|
267
|
-
<Text color={currentField === 'agentApiUrl' ? 'greenBright' : undefined}>
|
|
268
|
-
{currentField === 'agentApiUrl' ? '› ' : ' '}
|
|
269
|
-
Custom Agent API origin URL
|
|
270
|
-
</Text>
|
|
271
|
-
<TextEntry
|
|
272
|
-
label=""
|
|
273
|
-
value={values.agentApiUrl}
|
|
274
|
-
placeholder="https://agent-api.example.com"
|
|
275
|
-
error={currentField === 'agentApiUrl' ? errors.agentApiUrl : undefined}
|
|
276
|
-
/>
|
|
277
|
-
</Box>
|
|
278
|
-
) : null}
|
|
279
|
-
<TextEntry
|
|
280
|
-
label={fieldLabel('cloneRoot')}
|
|
281
|
-
value={values.cloneRoot}
|
|
282
|
-
placeholder="/path/to/Code/Tela"
|
|
283
|
-
error={currentField === 'cloneRoot' ? errors.cloneRoot : undefined}
|
|
284
|
-
/>
|
|
285
|
-
<TextEntry
|
|
286
|
-
label={fieldLabel('apiKey')}
|
|
287
|
-
value={values.apiKey}
|
|
288
|
-
placeholder={props.hasExistingApiKey ? 'Leave blank to keep the current API key' : 'Enter API key'}
|
|
289
|
-
masked
|
|
290
|
-
error={currentField === 'apiKey' ? errors.apiKey : undefined}
|
|
291
|
-
/>
|
|
292
|
-
</Section>
|
|
293
|
-
<KeyHints items={[
|
|
294
|
-
`Active field: ${fieldLabel(currentField)}`,
|
|
295
|
-
`Target: ${AGENT_API_PRESET_OPTIONS[selectedPresetIndex]?.label ?? 'Custom'}`,
|
|
296
|
-
]} />
|
|
297
|
-
</>
|
|
298
|
-
) : null}
|
|
299
|
-
|
|
300
|
-
{phase === 'review' ? (
|
|
301
|
-
<>
|
|
302
|
-
<Section title="Review">
|
|
303
|
-
<Text>Agent API target: {AGENT_API_PRESET_OPTIONS[selectedPresetIndex]?.label ?? 'Custom'}</Text>
|
|
304
|
-
<Text>Agent API origin: {resolvedAgentApiUrl}</Text>
|
|
305
|
-
<Text>Clone root: {values.cloneRoot}</Text>
|
|
306
|
-
<Text>API key: {values.apiKey ? '*'.repeat(values.apiKey.length) : (props.hasExistingApiKey ? '<keep existing>' : '<missing>')}</Text>
|
|
307
|
-
<Text>Clone root state: {cloneRootState.exists ? (cloneRootState.isDirectory ? 'existing directory' : 'existing non-directory path') : 'missing'}</Text>
|
|
308
|
-
{!cloneRootState.exists ? (
|
|
309
|
-
<Text>Create clone root if missing: {createCloneRoot ? 'yes' : 'no'}</Text>
|
|
310
|
-
) : null}
|
|
311
|
-
</Section>
|
|
312
|
-
<Callout tone="info">{AGENT_API_BROWSER_HELP}</Callout>
|
|
313
|
-
{!cloneRootState.exists ? (
|
|
314
|
-
<Callout tone="info">
|
|
315
|
-
Toggle clone-root creation before saving if you want setup to leave the path untouched.
|
|
316
|
-
</Callout>
|
|
317
|
-
) : null}
|
|
318
|
-
</>
|
|
319
|
-
) : null}
|
|
320
|
-
|
|
321
|
-
{phase === 'running' ? (
|
|
322
|
-
<Section title="Running">
|
|
323
|
-
<SpinnerText label="Persisting configuration and checking local dependencies" />
|
|
324
|
-
</Section>
|
|
325
|
-
) : null}
|
|
326
|
-
|
|
327
|
-
{phase === 'done' && result ? (
|
|
328
|
-
<>
|
|
329
|
-
<Section title="Saved config">
|
|
330
|
-
<Text>Agent API origin: {result.config.agentApiUrl}</Text>
|
|
331
|
-
<Text>Clone root: {result.config.cloneRoot}</Text>
|
|
332
|
-
</Section>
|
|
333
|
-
<Callout tone="info">{AGENT_API_BROWSER_HELP}</Callout>
|
|
334
|
-
<Section title="Checks">
|
|
335
|
-
{result.checks.map(check => (
|
|
336
|
-
<Text key={check.label}>{`${check.label}: ${check.status} — ${check.detail}`}</Text>
|
|
337
|
-
))}
|
|
338
|
-
</Section>
|
|
339
|
-
{result.warnings.length > 0 ? (
|
|
340
|
-
<Section title="Warnings">
|
|
341
|
-
{result.warnings.map(warning => (
|
|
342
|
-
<Text key={warning} color="yellow">{warning}</Text>
|
|
343
|
-
))}
|
|
344
|
-
</Section>
|
|
345
|
-
) : (
|
|
346
|
-
<Callout tone="success">Setup checks completed successfully.</Callout>
|
|
347
|
-
)}
|
|
348
|
-
</>
|
|
349
|
-
) : null}
|
|
350
|
-
</AppFrame>
|
|
351
|
-
)
|
|
352
|
-
}
|