@meistrari/tela-cli 0.1.1 → 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/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
- apiKey: string
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
- }