@meistrari/tela-cli 0.1.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 +95 -0
- package/cli.ts +25 -0
- package/package.json +30 -0
- package/src/agent-api.ts +124 -0
- package/src/auth.ts +16 -0
- package/src/commands/doctor.ts +35 -0
- package/src/commands/feedback.ts +701 -0
- package/src/commands/metadata.ts +208 -0
- package/src/commands/push.ts +208 -0
- package/src/commands/run.ts +1601 -0
- package/src/commands/status.ts +87 -0
- package/src/commands/sync.ts +245 -0
- package/src/commit-message.ts +69 -0
- package/src/config.ts +384 -0
- package/src/diagnostics.ts +578 -0
- package/src/errors.ts +122 -0
- package/src/git.ts +250 -0
- package/src/ink/bark.tsx +288 -0
- package/src/ink/components.tsx +175 -0
- package/src/ink/runtime.tsx +89 -0
- package/src/ink/screens/feedback-screen.tsx +394 -0
- package/src/ink/screens/push-screen.tsx +231 -0
- package/src/ink/screens/run-screen.tsx +291 -0
- package/src/ink/screens/setup-screen.tsx +352 -0
- package/src/ink/screens/sync-screen.tsx +282 -0
- package/src/process.ts +251 -0
- package/src/program.ts +1872 -0
- package/src/runtime.ts +147 -0
- package/src/types.ts +80 -0
- package/src/ui.ts +297 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { isAbsolute, join } from 'node:path'
|
|
4
|
+
import type { TelaCliConfig } from './types.ts'
|
|
5
|
+
|
|
6
|
+
const CONFIG_FILENAME = 'tela-cli.json'
|
|
7
|
+
export const AGENT_API_PRESET_URLS = {
|
|
8
|
+
production: 'https://agent-api.tela.com',
|
|
9
|
+
staging: 'https://agent-api-staging.tela.com',
|
|
10
|
+
} as const
|
|
11
|
+
export type AgentApiPreset = keyof typeof AGENT_API_PRESET_URLS | 'custom'
|
|
12
|
+
export const DEFAULT_AGENT_API_URL = AGENT_API_PRESET_URLS.production
|
|
13
|
+
export const AGENT_API_BROWSER_HELP = 'API-only endpoint; browser navigation is not expected.'
|
|
14
|
+
const AGENT_API_PRESET_ALIASES: Record<Exclude<AgentApiPreset, 'custom'>, string[]> = {
|
|
15
|
+
production: [AGENT_API_PRESET_URLS.production],
|
|
16
|
+
staging: [
|
|
17
|
+
AGENT_API_PRESET_URLS.staging,
|
|
18
|
+
'https://agent-api-staging.tela.com',
|
|
19
|
+
],
|
|
20
|
+
}
|
|
21
|
+
const DEFAULT_CLONE_ROOT_SEGMENTS = ['Code', 'Tela']
|
|
22
|
+
|
|
23
|
+
export interface TextPromptOptions {
|
|
24
|
+
message: string
|
|
25
|
+
defaultValue?: string
|
|
26
|
+
placeholder?: string
|
|
27
|
+
validate?: (value: string) => string | undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PasswordPromptOptions extends TextPromptOptions {
|
|
31
|
+
mask?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Option<T> {
|
|
35
|
+
value: T
|
|
36
|
+
label: string
|
|
37
|
+
hint?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SelectOptions<T> {
|
|
41
|
+
message: string
|
|
42
|
+
options: Array<Option<T>>
|
|
43
|
+
initialValue?: T
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ConfigInspection {
|
|
47
|
+
path: string
|
|
48
|
+
exists: boolean
|
|
49
|
+
valid: boolean
|
|
50
|
+
config?: TelaCliConfig
|
|
51
|
+
error?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PromptModule {
|
|
55
|
+
intro(message: string): void
|
|
56
|
+
outro(message: string): void
|
|
57
|
+
cancel(message: string): void
|
|
58
|
+
isCancel(value: unknown): boolean
|
|
59
|
+
note?(message: string, title?: string): void
|
|
60
|
+
select<T>(options: SelectOptions<T>): Promise<T | symbol>
|
|
61
|
+
text(options: TextPromptOptions): Promise<string | symbol>
|
|
62
|
+
password(options: PasswordPromptOptions): Promise<string | symbol>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function promptValue<T>(value: T | symbol): T {
|
|
66
|
+
return value as T
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ensureAbsoluteUrl(value: string, label: string): string {
|
|
70
|
+
let url: URL
|
|
71
|
+
try {
|
|
72
|
+
url = new URL(value)
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new Error(`${label} must be a valid URL`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return url.toString().replace(/\/$/, '')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const AGENT_API_PRESET_OPTIONS: Array<Option<AgentApiPreset>> = [
|
|
82
|
+
{
|
|
83
|
+
value: 'production',
|
|
84
|
+
label: 'Production',
|
|
85
|
+
hint: AGENT_API_PRESET_URLS.production,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
value: 'staging',
|
|
89
|
+
label: 'Staging',
|
|
90
|
+
hint: AGENT_API_PRESET_URLS.staging,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
value: 'custom',
|
|
94
|
+
label: 'Custom',
|
|
95
|
+
hint: 'Enter a custom API origin URL',
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
export function inferAgentApiPreset(agentApiUrl: string | null | undefined): AgentApiPreset {
|
|
100
|
+
if (!agentApiUrl) {
|
|
101
|
+
return 'production'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let normalized = agentApiUrl.trim()
|
|
105
|
+
try {
|
|
106
|
+
normalized = ensureAbsoluteUrl(normalized, 'Agent API origin URL')
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return 'custom'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const preset of ['production', 'staging'] as const) {
|
|
113
|
+
if (AGENT_API_PRESET_ALIASES[preset].includes(normalized)) {
|
|
114
|
+
return preset
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return 'custom'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function resolveAgentApiUrlForPreset(preset: AgentApiPreset, customUrl = ''): string {
|
|
122
|
+
switch (preset) {
|
|
123
|
+
case 'production':
|
|
124
|
+
return AGENT_API_PRESET_URLS.production
|
|
125
|
+
case 'staging':
|
|
126
|
+
return AGENT_API_PRESET_URLS.staging
|
|
127
|
+
case 'custom':
|
|
128
|
+
return ensureAbsoluteUrl(customUrl, 'Agent API origin URL')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function ensureNonEmpty(value: string, label: string): string {
|
|
133
|
+
const normalized = value.trim()
|
|
134
|
+
if (!normalized) {
|
|
135
|
+
throw new Error(`${label} is required`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return normalized
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getHomeDir(): string {
|
|
142
|
+
return process.env.HOME || homedir()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getTelaDir(): string {
|
|
146
|
+
return join(getHomeDir(), '.tela')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function getDefaultCloneRoot(): string {
|
|
150
|
+
return join(getHomeDir(), ...DEFAULT_CLONE_ROOT_SEGMENTS)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function resolveCloneRoot(value: string): string {
|
|
154
|
+
const expanded = expandHome(ensureNonEmpty(value, 'Clone root'))
|
|
155
|
+
return isAbsolute(expanded) ? expanded : join(process.cwd(), expanded)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function validateAgentApiUrl(value: string): string | undefined {
|
|
159
|
+
try {
|
|
160
|
+
ensureAbsoluteUrl(value, 'Agent API origin URL')
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return error instanceof Error ? error.message : 'Invalid URL'
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function validateCloneRoot(value: string): string | undefined {
|
|
169
|
+
try {
|
|
170
|
+
resolveCloneRoot(value)
|
|
171
|
+
return undefined
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
return error instanceof Error ? error.message : 'Invalid directory'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function validateApiKey(value: string, existingApiKey?: string): string | undefined {
|
|
179
|
+
const nextValue = value.trim() || existingApiKey || ''
|
|
180
|
+
return nextValue ? undefined : 'API key is required'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeConfig(config: TelaCliConfig): TelaCliConfig {
|
|
184
|
+
return {
|
|
185
|
+
agentApiUrl: ensureAbsoluteUrl(config.agentApiUrl, 'Agent API origin URL'),
|
|
186
|
+
cloneRoot: resolveCloneRoot(config.cloneRoot),
|
|
187
|
+
apiKey: ensureNonEmpty(config.apiKey, 'API key'),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function parseJsonConfig(input: string): TelaCliConfig {
|
|
192
|
+
const parsed = JSON.parse(input) as Partial<Record<keyof TelaCliConfig, unknown>>
|
|
193
|
+
|
|
194
|
+
return normalizeConfig({
|
|
195
|
+
agentApiUrl: String(parsed.agentApiUrl ?? ''),
|
|
196
|
+
cloneRoot: String(parsed.cloneRoot ?? ''),
|
|
197
|
+
apiKey: String(parsed.apiKey ?? ''),
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function tryLoadConfig(): TelaCliConfig | null {
|
|
202
|
+
try {
|
|
203
|
+
return loadConfig()
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function cancelSetup(prompts: PromptModule): never {
|
|
211
|
+
prompts.cancel('Setup cancelled.')
|
|
212
|
+
throw new Error('Setup cancelled')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function getConfigPath(): string {
|
|
216
|
+
return join(getTelaDir(), CONFIG_FILENAME)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function expandHome(input: string): string {
|
|
220
|
+
if (input === '~') {
|
|
221
|
+
return getHomeDir()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (input.startsWith('~/')) {
|
|
225
|
+
return join(getHomeDir(), input.slice(2))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return input
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function redactApiKey(apiKey: string): string {
|
|
232
|
+
const trimmed = apiKey.trim()
|
|
233
|
+
if (!trimmed) {
|
|
234
|
+
return '<missing>'
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (trimmed.length <= 8) {
|
|
238
|
+
return `${trimmed.slice(0, 2)}***${trimmed.slice(-2)}`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return `${trimmed.slice(0, 4)}***${trimmed.slice(-4)}`
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function loadConfig(): TelaCliConfig | null {
|
|
245
|
+
const configPath = getConfigPath()
|
|
246
|
+
if (!existsSync(configPath)) {
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return parseJsonConfig(readFileSync(configPath, 'utf-8'))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function inspectSavedConfig(): ConfigInspection {
|
|
254
|
+
const path = getConfigPath()
|
|
255
|
+
|
|
256
|
+
if (!existsSync(path)) {
|
|
257
|
+
return {
|
|
258
|
+
path,
|
|
259
|
+
exists: false,
|
|
260
|
+
valid: false,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const config = loadConfig()
|
|
266
|
+
return {
|
|
267
|
+
path,
|
|
268
|
+
exists: true,
|
|
269
|
+
valid: Boolean(config),
|
|
270
|
+
config: config ?? undefined,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return {
|
|
275
|
+
path,
|
|
276
|
+
exists: true,
|
|
277
|
+
valid: false,
|
|
278
|
+
error: error instanceof Error ? error.message : String(error),
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function saveConfig(config: TelaCliConfig): void {
|
|
284
|
+
const telaDir = getTelaDir()
|
|
285
|
+
mkdirSync(telaDir, { recursive: true, mode: 0o700 })
|
|
286
|
+
|
|
287
|
+
const normalized = normalizeConfig(config)
|
|
288
|
+
const configPath = getConfigPath()
|
|
289
|
+
writeFileSync(configPath, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 })
|
|
290
|
+
chmodSync(configPath, 0o600)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function setupConfigInteractive(options: {
|
|
294
|
+
prompts?: PromptModule
|
|
295
|
+
existingConfig?: TelaCliConfig | null
|
|
296
|
+
skipLifecycle?: boolean
|
|
297
|
+
} = {}): Promise<TelaCliConfig> {
|
|
298
|
+
const prompts = options.prompts
|
|
299
|
+
if (!prompts) {
|
|
300
|
+
throw new Error('Interactive prompts are not available without a prompt implementation.')
|
|
301
|
+
}
|
|
302
|
+
const existingConfig = options.existingConfig ?? tryLoadConfig()
|
|
303
|
+
|
|
304
|
+
if (!options.skipLifecycle) {
|
|
305
|
+
prompts.intro('Tela CLI setup')
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
prompts.note?.(AGENT_API_BROWSER_HELP, 'Agent API')
|
|
309
|
+
const initialAgentApiPreset = inferAgentApiPreset(existingConfig?.agentApiUrl ?? DEFAULT_AGENT_API_URL)
|
|
310
|
+
const agentApiPresetResult = await prompts.select<AgentApiPreset>({
|
|
311
|
+
message: 'Agent API target',
|
|
312
|
+
initialValue: initialAgentApiPreset,
|
|
313
|
+
options: AGENT_API_PRESET_OPTIONS,
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
if (prompts.isCancel(agentApiPresetResult)) {
|
|
317
|
+
cancelSetup(prompts)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const agentApiPreset = promptValue(agentApiPresetResult)
|
|
321
|
+
let agentApiUrl = agentApiPreset === 'custom'
|
|
322
|
+
? (initialAgentApiPreset === 'custom' ? (existingConfig?.agentApiUrl ?? '') : '')
|
|
323
|
+
: resolveAgentApiUrlForPreset(agentApiPreset)
|
|
324
|
+
if (agentApiPreset === 'custom') {
|
|
325
|
+
prompts.note?.(AGENT_API_BROWSER_HELP, 'Agent API origin')
|
|
326
|
+
const customAgentApiUrlResult = await prompts.text({
|
|
327
|
+
message: 'Custom Agent API origin URL',
|
|
328
|
+
defaultValue: initialAgentApiPreset === 'custom' ? existingConfig?.agentApiUrl : undefined,
|
|
329
|
+
validate: validateAgentApiUrl,
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
if (prompts.isCancel(customAgentApiUrlResult)) {
|
|
333
|
+
cancelSetup(prompts)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
agentApiUrl = resolveAgentApiUrlForPreset('custom', promptValue(customAgentApiUrlResult))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const cloneRootResult = await prompts.text({
|
|
340
|
+
message: 'Clone root directory',
|
|
341
|
+
defaultValue: existingConfig?.cloneRoot ?? getDefaultCloneRoot(),
|
|
342
|
+
validate: validateCloneRoot,
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
if (prompts.isCancel(cloneRootResult)) {
|
|
346
|
+
cancelSetup(prompts)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const apiKeyResult = await prompts.password({
|
|
350
|
+
message: existingConfig?.apiKey ? 'API key' : 'API key',
|
|
351
|
+
placeholder: existingConfig?.apiKey ? 'Leave blank to keep the current API key' : undefined,
|
|
352
|
+
mask: '*',
|
|
353
|
+
validate: value => validateApiKey(value, existingConfig?.apiKey),
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
if (prompts.isCancel(apiKeyResult)) {
|
|
357
|
+
cancelSetup(prompts)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const config = normalizeConfig({
|
|
361
|
+
agentApiUrl,
|
|
362
|
+
cloneRoot: promptValue(cloneRootResult),
|
|
363
|
+
apiKey: promptValue(apiKeyResult).trim() || existingConfig?.apiKey || '',
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
saveConfig(config)
|
|
367
|
+
if (!options.skipLifecycle) {
|
|
368
|
+
prompts.outro('Tela CLI configured')
|
|
369
|
+
}
|
|
370
|
+
return config
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function ensureConfigInteractive(
|
|
374
|
+
options: {
|
|
375
|
+
prompts?: PromptModule
|
|
376
|
+
} = {},
|
|
377
|
+
): Promise<TelaCliConfig> {
|
|
378
|
+
const existing = tryLoadConfig()
|
|
379
|
+
if (existing) {
|
|
380
|
+
return existing
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return await setupConfigInteractive(options)
|
|
384
|
+
}
|