@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/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
+ }