@meistrari/tela-cli 0.7.0 → 0.7.5

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.
Files changed (59) hide show
  1. package/README.md +56 -106
  2. package/dist/commands/auth.d.ts +2 -0
  3. package/dist/commands/context.d.ts +2 -0
  4. package/dist/commands/doctor-auth.d.ts +2 -0
  5. package/dist/commands/env.d.ts +2 -0
  6. package/dist/credentials.d.ts +2 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +7027 -0
  9. package/dist/output.d.ts +13 -0
  10. package/dist/program.d.ts +2 -0
  11. package/package.json +17 -24
  12. package/scripts/install-skills.cjs +127 -0
  13. package/skills/tela-cli/SKILL.md +20 -0
  14. package/skills/tela-cli/references/agent.md +111 -0
  15. package/cli.ts +0 -25
  16. package/postinstall.sh +0 -19
  17. package/src/agent-api.ts +0 -300
  18. package/src/assets.d.ts +0 -4
  19. package/src/auth.ts +0 -432
  20. package/src/claude.ts +0 -101
  21. package/src/commands/doctor.ts +0 -35
  22. package/src/commands/feedback.ts +0 -701
  23. package/src/commands/metadata.ts +0 -242
  24. package/src/commands/push.ts +0 -208
  25. package/src/commands/run.ts +0 -1699
  26. package/src/commands/status.ts +0 -109
  27. package/src/commands/sync.ts +0 -245
  28. package/src/commit-message.ts +0 -69
  29. package/src/config.ts +0 -799
  30. package/src/diagnostics.ts +0 -813
  31. package/src/errors.ts +0 -122
  32. package/src/forgejo.ts +0 -233
  33. package/src/git.ts +0 -457
  34. package/src/ink/assets/160092__jorickhoofd__dog-bark-1.mp3 +0 -0
  35. package/src/ink/bark.tsx +0 -241
  36. package/src/ink/components.tsx +0 -178
  37. package/src/ink/runtime.tsx +0 -90
  38. package/src/ink/screens/feedback-screen.tsx +0 -395
  39. package/src/ink/screens/push-screen.tsx +0 -232
  40. package/src/ink/screens/run-screen.tsx +0 -292
  41. package/src/ink/screens/setup/breadcrumb.tsx +0 -24
  42. package/src/ink/screens/setup/setup-screen.tsx +0 -612
  43. package/src/ink/screens/setup/step-frame.tsx +0 -31
  44. package/src/ink/screens/setup/steps/done-step.tsx +0 -29
  45. package/src/ink/screens/setup/steps/environment-step.tsx +0 -23
  46. package/src/ink/screens/setup/steps/organization-step.tsx +0 -35
  47. package/src/ink/screens/setup/steps/review-step.tsx +0 -78
  48. package/src/ink/screens/setup/steps/running-step.tsx +0 -37
  49. package/src/ink/screens/setup/steps/ssh-key-step.tsx +0 -48
  50. package/src/ink/screens/setup/steps/text-field-step.tsx +0 -18
  51. package/src/ink/screens/setup/steps.ts +0 -156
  52. package/src/ink/screens/setup/types.ts +0 -65
  53. package/src/ink/screens/sync-screen.tsx +0 -283
  54. package/src/process.ts +0 -251
  55. package/src/program.ts +0 -2105
  56. package/src/runtime.ts +0 -147
  57. package/src/types.ts +0 -165
  58. package/src/ui.ts +0 -295
  59. package/src/version.ts +0 -3
package/src/config.ts DELETED
@@ -1,799 +0,0 @@
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 { listOrganizations, provisionForgejoUser } from './agent-api.ts'
5
- import type { FetchLike } from './auth.ts'
6
- import { createUserFacingError } from './errors.ts'
7
- import {
8
- createGitSshKey,
9
- ensureForgejoUserAccess,
10
- inferSshPrivateKeyPath,
11
- listDiscoveredSshPublicKeys,
12
- loadSshPublicKey,
13
- resolveSshPrivateKeyPath,
14
- validateSshPrivateKeyPath,
15
- validateSshPublicKeyPath,
16
- type SelectedSshPublicKey,
17
- } from './forgejo.ts'
18
- import type {
19
- TelaCliAuthState,
20
- TelaCliConfig,
21
- TelaCliForgejoOrganization,
22
- TelaCliGitSshKey,
23
- TelaEnvironment,
24
- } from './types.ts'
25
-
26
- const CONFIG_FILENAME = 'tela-cli.json'
27
- const DEFAULT_CLONE_ROOT_SEGMENTS = ['Code', 'Tela']
28
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
29
-
30
- const PLACEHOLDER_APPLICATION_IDS = new Set([
31
- '00000000-0000-4000-8000-000000000001',
32
- '00000000-0000-4000-8000-000000000002',
33
- '00000000-0000-4000-8000-000000000003',
34
- '00000000-0000-4000-8000-000000000004',
35
- ])
36
-
37
- export const DEFAULT_SETUP_AUTH_TIMEOUT_MS = 15 * 60 * 1000
38
- export const DEVICE_FLOW_BROWSER_HELP = 'Tela sign-in uses a browser-based device flow and continues polling until you approve the request.'
39
- const CUSTOM_SSH_KEY_OPTION = '__custom__'
40
-
41
- export interface SetupEnvironmentConfig {
42
- environment: TelaEnvironment
43
- agentApiUrl: string
44
- authApiUrl: string
45
- authDashboardUrl: string
46
- telaApiUrl: string
47
- requesterApplicationId: string
48
- targetApplicationId: string
49
- }
50
-
51
- export const ENVIRONMENT_PRESETS: Record<Exclude<TelaEnvironment, 'custom'>, Omit<SetupEnvironmentConfig, 'environment'>> = {
52
- production: {
53
- agentApiUrl: 'https://agent-api-production-81c9.up.railway.app',
54
- authApiUrl: 'https://auth.tela.com',
55
- authDashboardUrl: 'https://accounts.tela.com',
56
- telaApiUrl: 'https://api.tela.com',
57
- requesterApplicationId: '860d5506-cab2-4b8b-b08b-fb4222b65bb2',
58
- targetApplicationId: 'fe9fa028-4d34-407c-b27a-2ecc4779ae69',
59
- },
60
- staging: {
61
- agentApiUrl: 'https://agent-api.telastaging.com',
62
- authApiUrl: 'https://auth-api.telastaging.com',
63
- authDashboardUrl: 'https://accounts.telastaging.com',
64
- telaApiUrl: 'https://api.telastaging.com',
65
- requesterApplicationId: 'c5cda347-835e-497c-bdeb-a464f5a1beb0',
66
- targetApplicationId: '1c021ec1-b7d1-4d72-8733-ac69d930b2df',
67
- },
68
- }
69
-
70
- export interface TextPromptOptions {
71
- message: string
72
- defaultValue?: string
73
- placeholder?: string
74
- validate?: (value: string) => string | undefined
75
- }
76
-
77
- export interface Option<T> {
78
- value: T
79
- label: string
80
- hint?: string
81
- }
82
-
83
- export interface SelectOptions<T> {
84
- message: string
85
- options: Array<Option<T>>
86
- initialValue?: T
87
- }
88
-
89
- export interface ConfigInspection {
90
- path: string
91
- exists: boolean
92
- valid: boolean
93
- config?: TelaCliConfig
94
- error?: string
95
- }
96
-
97
- export interface PromptModule {
98
- intro(message: string): void
99
- outro(message: string): void
100
- cancel(message: string): void
101
- isCancel(value: unknown): boolean
102
- note?(message: string, title?: string): void
103
- select<T>(options: SelectOptions<T>): Promise<T | symbol>
104
- text(options: TextPromptOptions): Promise<string | symbol>
105
- }
106
-
107
- export interface SetupConfigInput extends SetupEnvironmentConfig {
108
- cloneRoot: string
109
- }
110
-
111
- export type SetupAuthenticateFn = (
112
- input: SetupConfigInput,
113
- notify: (message: string, title?: string) => void,
114
- ) => Promise<TelaCliAuthState>
115
-
116
- export const ENVIRONMENT_PRESET_OPTIONS: Array<Option<TelaEnvironment>> = [
117
- {
118
- value: 'production',
119
- label: 'Production',
120
- hint: ENVIRONMENT_PRESETS.production.agentApiUrl,
121
- },
122
- {
123
- value: 'staging',
124
- label: 'Staging',
125
- hint: ENVIRONMENT_PRESETS.staging.agentApiUrl,
126
- },
127
- {
128
- value: 'custom',
129
- label: 'Custom',
130
- hint: 'Enter custom Tela service URLs and app IDs',
131
- },
132
- ]
133
-
134
- function promptValue<T>(value: T | symbol): T {
135
- return value as T
136
- }
137
-
138
- function ensureNonEmpty(value: string, label: string): string {
139
- const normalized = value.trim()
140
- if (!normalized) {
141
- throw new Error(`${label} is required`)
142
- }
143
-
144
- return normalized
145
- }
146
-
147
- function ensureEnvironment(value: string): TelaEnvironment {
148
- if (value === 'production' || value === 'staging' || value === 'custom') {
149
- return value
150
- }
151
-
152
- throw new Error('Environment must be production, staging, or custom')
153
- }
154
-
155
- function ensureAbsoluteUrl(value: string, label: string): string {
156
- let url: URL
157
- try {
158
- url = new URL(value)
159
- }
160
- catch {
161
- throw new Error(`${label} must be a valid URL`)
162
- }
163
-
164
- return url.toString().replace(/\/$/, '')
165
- }
166
-
167
- function ensureUuid(value: string, label: string): string {
168
- const normalized = ensureNonEmpty(value, label)
169
- if (!UUID_PATTERN.test(normalized)) {
170
- throw new Error(`${label} must be a UUID`)
171
- }
172
-
173
- return normalized
174
- }
175
-
176
- function normalizeAuth(auth: TelaCliConfig['auth']): TelaCliConfig['auth'] {
177
- if (!auth) {
178
- return null
179
- }
180
-
181
- const user = auth.user ?? {} as TelaCliAuthState['user']
182
- const workspace = auth.workspace ?? {} as TelaCliAuthState['workspace']
183
- const session = auth.session ?? {} as TelaCliAuthState['session']
184
-
185
- const expiresAt = Number(session.expiresAt)
186
- if (!Number.isFinite(expiresAt) || expiresAt <= 0) {
187
- throw new Error('Auth session expiration must be a valid timestamp')
188
- }
189
-
190
- return {
191
- user: {
192
- id: ensureNonEmpty(String(user.id ?? ''), 'Authenticated user ID'),
193
- email: ensureNonEmpty(String(user.email ?? ''), 'Authenticated user email'),
194
- name: ensureNonEmpty(String(user.name ?? user.email ?? ''), 'Authenticated user name'),
195
- },
196
- workspace: {
197
- id: ensureNonEmpty(String(workspace.id ?? ''), 'Authenticated workspace ID'),
198
- title: ensureNonEmpty(String(workspace.title ?? ''), 'Authenticated workspace title'),
199
- },
200
- session: {
201
- accessToken: ensureNonEmpty(String(session.accessToken ?? ''), 'Auth access token'),
202
- refreshToken: ensureNonEmpty(String(session.refreshToken ?? ''), 'Auth refresh token'),
203
- expiresAt,
204
- },
205
- }
206
- }
207
-
208
- function normalizeForgejoOrganization(
209
- organization: TelaCliConfig['forgejoOrganization'],
210
- ): TelaCliForgejoOrganization | null {
211
- if (!organization) {
212
- return null
213
- }
214
-
215
- return {
216
- id: ensureNonEmpty(String(organization.id ?? ''), 'Forgejo organization ID'),
217
- name: ensureNonEmpty(String(organization.name ?? ''), 'Forgejo organization name'),
218
- }
219
- }
220
-
221
- function normalizeGitSshKey(gitSshKey: TelaCliConfig['gitSshKey']): TelaCliGitSshKey | null {
222
- if (!gitSshKey) {
223
- return null
224
- }
225
-
226
- return {
227
- title: ensureNonEmpty(String(gitSshKey.title ?? ''), 'Git SSH key title'),
228
- publicKeyPath: expandHome(ensureNonEmpty(String(gitSshKey.publicKeyPath ?? ''), 'Git SSH public key path')),
229
- privateKeyPath: expandHome(ensureNonEmpty(String(gitSshKey.privateKeyPath ?? ''), 'Git SSH private key path')),
230
- }
231
- }
232
-
233
- function normalizeConfig(config: TelaCliConfig): TelaCliConfig {
234
- const environment = ensureEnvironment(config.environment)
235
- const auth = normalizeAuth(config.auth)
236
-
237
- return {
238
- environment,
239
- agentApiUrl: ensureAbsoluteUrl(config.agentApiUrl, 'Agent API origin URL'),
240
- authApiUrl: ensureAbsoluteUrl(config.authApiUrl, 'Auth API URL'),
241
- authDashboardUrl: ensureAbsoluteUrl(config.authDashboardUrl, 'Auth dashboard URL'),
242
- telaApiUrl: ensureAbsoluteUrl(config.telaApiUrl, 'Tela API URL'),
243
- cloneRoot: resolveCloneRoot(config.cloneRoot),
244
- requesterApplicationId: ensureUuid(config.requesterApplicationId, 'Requester application ID'),
245
- targetApplicationId: ensureUuid(config.targetApplicationId, 'Target application ID'),
246
- auth,
247
- forgejoOrganization: auth ? normalizeForgejoOrganization(config.forgejoOrganization) : null,
248
- forgejoUserRegistered: Boolean(config.forgejoUserRegistered),
249
- gitSshKey: normalizeGitSshKey(config.gitSshKey),
250
- }
251
- }
252
-
253
- function parseJsonConfig(input: string): TelaCliConfig {
254
- const parsed = JSON.parse(input) as Partial<Record<keyof TelaCliConfig, unknown>> & {
255
- auth?: Partial<Record<'user' | 'workspace' | 'session', unknown>> | null
256
- }
257
-
258
- const auth = parsed.auth == null
259
- ? null
260
- : {
261
- user: parsed.auth.user as TelaCliAuthState['user'],
262
- workspace: parsed.auth.workspace as TelaCliAuthState['workspace'],
263
- session: parsed.auth.session as TelaCliAuthState['session'],
264
- }
265
-
266
- return normalizeConfig({
267
- environment: String(parsed.environment ?? '') as TelaEnvironment,
268
- agentApiUrl: String(parsed.agentApiUrl ?? ''),
269
- authApiUrl: String(parsed.authApiUrl ?? ''),
270
- authDashboardUrl: String(parsed.authDashboardUrl ?? ''),
271
- telaApiUrl: String(parsed.telaApiUrl ?? ''),
272
- cloneRoot: String(parsed.cloneRoot ?? ''),
273
- requesterApplicationId: String(parsed.requesterApplicationId ?? ''),
274
- targetApplicationId: String(parsed.targetApplicationId ?? ''),
275
- auth,
276
- forgejoOrganization: parsed.forgejoOrganization as TelaCliConfig['forgejoOrganization'] ?? null,
277
- forgejoUserRegistered: Boolean(parsed.forgejoUserRegistered ?? false),
278
- gitSshKey: parsed.gitSshKey as TelaCliConfig['gitSshKey'] ?? null,
279
- })
280
- }
281
-
282
- function tryLoadConfig(): TelaCliConfig | null {
283
- try {
284
- return loadConfig()
285
- }
286
- catch {
287
- return null
288
- }
289
- }
290
-
291
- function cancelSetup(prompts: PromptModule): never {
292
- prompts.cancel('Setup cancelled.')
293
- throw new Error('Setup cancelled')
294
- }
295
-
296
- async function promptForSshPublicKey(prompts: PromptModule): Promise<SelectedSshPublicKey> {
297
- const discoveredKeys = listDiscoveredSshPublicKeys()
298
- if (discoveredKeys.length === 0) {
299
- prompts.note?.('No SSH public keys were found under ~/.ssh. Enter a custom public key path to continue.', 'SSH key')
300
- }
301
-
302
- const selection = await prompts.select<string>({
303
- message: 'SSH public key to send',
304
- initialValue: discoveredKeys[0]?.path ?? CUSTOM_SSH_KEY_OPTION,
305
- options: [
306
- ...discoveredKeys.map(key => ({
307
- value: key.path,
308
- label: key.title,
309
- hint: key.path,
310
- })),
311
- {
312
- value: CUSTOM_SSH_KEY_OPTION,
313
- label: 'Custom path…',
314
- hint: 'Load another public key file',
315
- },
316
- ],
317
- })
318
- if (prompts.isCancel(selection)) {
319
- cancelSetup(prompts)
320
- }
321
-
322
- const selectedValue = promptValue(selection)
323
- if (selectedValue !== CUSTOM_SSH_KEY_OPTION) {
324
- return discoveredKeys.find(key => key.path === selectedValue) ?? loadSshPublicKey(selectedValue)
325
- }
326
-
327
- const customPath = await prompts.text({
328
- message: 'SSH public key path',
329
- placeholder: '~/.ssh/id_ed25519.pub',
330
- validate: validateSshPublicKeyPath,
331
- })
332
- if (prompts.isCancel(customPath)) {
333
- cancelSetup(prompts)
334
- }
335
-
336
- return loadSshPublicKey(promptValue(customPath))
337
- }
338
-
339
- async function promptForSshPrivateKeyPath(
340
- prompts: PromptModule,
341
- sshKey: SelectedSshPublicKey,
342
- ): Promise<string> {
343
- const privateKeyPath = await prompts.text({
344
- message: 'SSH private key path',
345
- defaultValue: inferSshPrivateKeyPath(sshKey.path),
346
- placeholder: sshKey.path.endsWith('.pub')
347
- ? sshKey.path.slice(0, -4)
348
- : '~/.ssh/id_ed25519',
349
- validate: validateSshPrivateKeyPath,
350
- })
351
- if (prompts.isCancel(privateKeyPath)) {
352
- cancelSetup(prompts)
353
- }
354
-
355
- return resolveSshPrivateKeyPath(promptValue(privateKeyPath))
356
- }
357
-
358
- function getHomeDir(): string {
359
- return process.env.HOME || homedir()
360
- }
361
-
362
- function getTelaDir(): string {
363
- return join(getHomeDir(), '.tela')
364
- }
365
-
366
- export function getConfigPath(): string {
367
- return join(getTelaDir(), CONFIG_FILENAME)
368
- }
369
-
370
- export function expandHome(input: string): string {
371
- if (input === '~') {
372
- return getHomeDir()
373
- }
374
-
375
- if (input.startsWith('~/')) {
376
- return join(getHomeDir(), input.slice(2))
377
- }
378
-
379
- return input
380
- }
381
-
382
- export function getDefaultCloneRoot(): string {
383
- return join(getHomeDir(), ...DEFAULT_CLONE_ROOT_SEGMENTS)
384
- }
385
-
386
- export function resolveCloneRoot(value: string): string {
387
- const expanded = expandHome(ensureNonEmpty(value, 'Clone root'))
388
- return isAbsolute(expanded) ? expanded : join(process.cwd(), expanded)
389
- }
390
-
391
- export function validateCloneRoot(value: string): string | undefined {
392
- try {
393
- resolveCloneRoot(value)
394
- return undefined
395
- }
396
- catch (error) {
397
- return error instanceof Error ? error.message : 'Invalid directory'
398
- }
399
- }
400
-
401
- export function validateAgentApiUrl(value: string): string | undefined {
402
- try {
403
- ensureAbsoluteUrl(value, 'Agent API origin URL')
404
- return undefined
405
- }
406
- catch (error) {
407
- return error instanceof Error ? error.message : 'Invalid URL'
408
- }
409
- }
410
-
411
- export function validateAuthApiUrl(value: string): string | undefined {
412
- try {
413
- ensureAbsoluteUrl(value, 'Auth API URL')
414
- return undefined
415
- }
416
- catch (error) {
417
- return error instanceof Error ? error.message : 'Invalid URL'
418
- }
419
- }
420
-
421
- export function validateAuthDashboardUrl(value: string): string | undefined {
422
- try {
423
- ensureAbsoluteUrl(value, 'Auth dashboard URL')
424
- return undefined
425
- }
426
- catch (error) {
427
- return error instanceof Error ? error.message : 'Invalid URL'
428
- }
429
- }
430
-
431
- export function validateTelaApiUrl(value: string): string | undefined {
432
- try {
433
- ensureAbsoluteUrl(value, 'Tela API URL')
434
- return undefined
435
- }
436
- catch (error) {
437
- return error instanceof Error ? error.message : 'Invalid URL'
438
- }
439
- }
440
-
441
- export function validateApplicationId(value: string, label = 'Application ID'): string | undefined {
442
- try {
443
- ensureUuid(value, label)
444
- return undefined
445
- }
446
- catch (error) {
447
- return error instanceof Error ? error.message : 'Invalid application ID'
448
- }
449
- }
450
-
451
- export function redactSecret(secret: string): string {
452
- const trimmed = secret.trim()
453
- if (!trimmed) {
454
- return '<missing>'
455
- }
456
-
457
- if (trimmed.length <= 8) {
458
- return `${trimmed.slice(0, 2)}***${trimmed.slice(-2)}`
459
- }
460
-
461
- return `${trimmed.slice(0, 4)}***${trimmed.slice(-4)}`
462
- }
463
-
464
- export function resolveEnvironmentConfigForPreset(
465
- environment: TelaEnvironment,
466
- custom: Partial<Omit<SetupEnvironmentConfig, 'environment'>> = {},
467
- ): SetupEnvironmentConfig {
468
- if (environment === 'custom') {
469
- return {
470
- environment,
471
- agentApiUrl: ensureAbsoluteUrl(String(custom.agentApiUrl ?? ''), 'Agent API origin URL'),
472
- authApiUrl: ensureAbsoluteUrl(String(custom.authApiUrl ?? ''), 'Auth API URL'),
473
- authDashboardUrl: ensureAbsoluteUrl(String(custom.authDashboardUrl ?? ''), 'Auth dashboard URL'),
474
- telaApiUrl: ensureAbsoluteUrl(String(custom.telaApiUrl ?? ''), 'Tela API URL'),
475
- requesterApplicationId: ensureUuid(String(custom.requesterApplicationId ?? ''), 'Requester application ID'),
476
- targetApplicationId: ensureUuid(String(custom.targetApplicationId ?? ''), 'Target application ID'),
477
- }
478
- }
479
-
480
- const preset = ENVIRONMENT_PRESETS[environment]
481
- return {
482
- environment,
483
- ...preset,
484
- }
485
- }
486
-
487
- export function isPlaceholderApplicationId(value: string): boolean {
488
- return PLACEHOLDER_APPLICATION_IDS.has(value.trim())
489
- }
490
-
491
- export function hasSavedAuth(config: TelaCliConfig | null | undefined): boolean {
492
- return Boolean(config?.auth?.session.accessToken && config.auth?.session.refreshToken)
493
- }
494
-
495
- export function loadConfig(): TelaCliConfig | null {
496
- const configPath = getConfigPath()
497
- if (!existsSync(configPath)) {
498
- return null
499
- }
500
-
501
- return parseJsonConfig(readFileSync(configPath, 'utf-8'))
502
- }
503
-
504
- export function inspectSavedConfig(): ConfigInspection {
505
- const path = getConfigPath()
506
-
507
- if (!existsSync(path)) {
508
- return {
509
- path,
510
- exists: false,
511
- valid: false,
512
- }
513
- }
514
-
515
- try {
516
- const config = loadConfig()
517
- return {
518
- path,
519
- exists: true,
520
- valid: Boolean(config),
521
- config: config ?? undefined,
522
- }
523
- }
524
- catch (error) {
525
- return {
526
- path,
527
- exists: true,
528
- valid: false,
529
- error: error instanceof Error ? error.message : String(error),
530
- }
531
- }
532
- }
533
-
534
- export function saveConfig(config: TelaCliConfig): void {
535
- const telaDir = getTelaDir()
536
- mkdirSync(telaDir, { recursive: true, mode: 0o700 })
537
-
538
- const normalized = normalizeConfig(config)
539
- const configPath = getConfigPath()
540
- writeFileSync(configPath, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 })
541
- chmodSync(configPath, 0o600)
542
- }
543
-
544
- export function clearSavedAuth(config: TelaCliConfig): TelaCliConfig {
545
- const cleared = normalizeConfig({
546
- ...config,
547
- auth: null,
548
- forgejoOrganization: null,
549
- forgejoUserRegistered: false,
550
- })
551
- saveConfig(cleared)
552
- return cleared
553
- }
554
-
555
- function createMissingForgejoOrganizationsError(workspaceTitle: string): ReturnType<typeof createUserFacingError> {
556
- return createUserFacingError({
557
- code: 'workspace_organization_required',
558
- summary: 'No Forgejo organizations are registered for this workspace.',
559
- details: `Tela authenticated successfully for ${workspaceTitle}, but the workspace does not have any registered organizations yet.`,
560
- fixes: [
561
- 'Ask the Tela team to register at least one organization for this workspace, then rerun `tela setup`.',
562
- ],
563
- })
564
- }
565
-
566
- export function getPreferredForgejoOrganization(
567
- existingConfig: TelaCliConfig | null | undefined,
568
- auth: TelaCliAuthState,
569
- organizations: TelaCliForgejoOrganization[],
570
- ): TelaCliForgejoOrganization | null {
571
- if (!existingConfig?.forgejoOrganization) {
572
- return null
573
- }
574
-
575
- if (existingConfig.auth?.workspace.id !== auth.workspace.id) {
576
- return null
577
- }
578
-
579
- return organizations.find(organization => organization.id === existingConfig.forgejoOrganization?.id) ?? null
580
- }
581
-
582
- async function resolveForgejoOrganizationInteractive(options: {
583
- prompts: PromptModule
584
- existingConfig: TelaCliConfig | null
585
- auth: TelaCliAuthState
586
- config: TelaCliConfig
587
- fetchFn?: FetchLike
588
- }): Promise<TelaCliForgejoOrganization> {
589
- const organizations = await listOrganizations(options.config, options.fetchFn)
590
- if (organizations.length === 0) {
591
- throw createMissingForgejoOrganizationsError(options.auth.workspace.title)
592
- }
593
-
594
- const preferredOrganization = getPreferredForgejoOrganization(
595
- options.existingConfig,
596
- options.auth,
597
- organizations,
598
- )
599
-
600
- if (organizations.length === 1) {
601
- const organization = organizations[0]!
602
- options.prompts.note?.(`Using workspace organization ${organization.name}.`, 'Organization')
603
- return organization
604
- }
605
-
606
- const selection = await options.prompts.select<string>({
607
- message: 'Forgejo organization',
608
- initialValue: preferredOrganization?.id ?? organizations[0]?.id,
609
- options: organizations.map(organization => ({
610
- value: organization.id,
611
- label: organization.name,
612
- hint: organization.id === preferredOrganization?.id ? 'saved from previous setup' : undefined,
613
- })),
614
- })
615
- if (options.prompts.isCancel(selection)) {
616
- cancelSetup(options.prompts)
617
- }
618
-
619
- const selectedId = promptValue(selection)
620
- const selectedOrganization = organizations.find(organization => organization.id === selectedId)
621
- if (!selectedOrganization) {
622
- throw new Error(`Unknown organization selection: ${selectedId}`)
623
- }
624
-
625
- options.prompts.note?.(`Selected workspace organization ${selectedOrganization.name}.`, 'Organization')
626
- return selectedOrganization
627
- }
628
-
629
- export async function setupConfigInteractive(options: {
630
- prompts?: PromptModule
631
- existingConfig?: TelaCliConfig | null
632
- skipLifecycle?: boolean
633
- authenticate: SetupAuthenticateFn
634
- fetchFn?: FetchLike
635
- provisionForgejoUser?: typeof provisionForgejoUser
636
- }): Promise<TelaCliConfig> {
637
- const prompts = options.prompts
638
- if (!prompts) {
639
- throw new Error('Interactive prompts are not available without a prompt implementation.')
640
- }
641
-
642
- const existingConfig = options.existingConfig ?? tryLoadConfig()
643
- if (!options.skipLifecycle) {
644
- prompts.intro('Tela CLI setup')
645
- }
646
-
647
- prompts.note?.(DEVICE_FLOW_BROWSER_HELP, 'Authentication')
648
-
649
- const environmentResult = await prompts.select<TelaEnvironment>({
650
- message: 'Tela environment',
651
- initialValue: existingConfig?.environment ?? 'production',
652
- options: ENVIRONMENT_PRESET_OPTIONS,
653
- })
654
- if (prompts.isCancel(environmentResult)) {
655
- cancelSetup(prompts)
656
- }
657
-
658
- const environment = promptValue(environmentResult)
659
- const customDefaults = environment === 'custom' && existingConfig?.environment === 'custom'
660
- ? existingConfig
661
- : null
662
-
663
- const customValues: Partial<Omit<SetupEnvironmentConfig, 'environment'>> = {}
664
- if (environment === 'custom') {
665
- const promptsByField: Array<{
666
- key: keyof Omit<SetupEnvironmentConfig, 'environment'>
667
- message: string
668
- placeholder: string
669
- defaultValue?: string
670
- validate: (value: string) => string | undefined
671
- }> = [
672
- {
673
- key: 'agentApiUrl',
674
- message: 'Custom Agent API origin URL',
675
- placeholder: 'https://agent-api.example.com',
676
- defaultValue: customDefaults?.agentApiUrl,
677
- validate: validateAgentApiUrl,
678
- },
679
- {
680
- key: 'authApiUrl',
681
- message: 'Custom Auth API URL',
682
- placeholder: 'https://auth.example.com',
683
- defaultValue: customDefaults?.authApiUrl,
684
- validate: validateAuthApiUrl,
685
- },
686
- {
687
- key: 'authDashboardUrl',
688
- message: 'Custom Auth dashboard URL',
689
- placeholder: 'https://accounts.example.com',
690
- defaultValue: customDefaults?.authDashboardUrl,
691
- validate: validateAuthDashboardUrl,
692
- },
693
- {
694
- key: 'telaApiUrl',
695
- message: 'Custom Tela API URL',
696
- placeholder: 'https://api.example.com',
697
- defaultValue: customDefaults?.telaApiUrl,
698
- validate: validateTelaApiUrl,
699
- },
700
- {
701
- key: 'requesterApplicationId',
702
- message: 'Requester application ID',
703
- placeholder: '00000000-0000-0000-0000-000000000000',
704
- defaultValue: customDefaults?.requesterApplicationId,
705
- validate: value => validateApplicationId(value, 'Requester application ID'),
706
- },
707
- {
708
- key: 'targetApplicationId',
709
- message: 'Target application ID',
710
- placeholder: '00000000-0000-0000-0000-000000000000',
711
- defaultValue: customDefaults?.targetApplicationId,
712
- validate: value => validateApplicationId(value, 'Target application ID'),
713
- },
714
- ]
715
-
716
- for (const field of promptsByField) {
717
- const response = await prompts.text({
718
- message: field.message,
719
- placeholder: field.placeholder,
720
- defaultValue: field.defaultValue,
721
- validate: field.validate,
722
- })
723
- if (prompts.isCancel(response)) {
724
- cancelSetup(prompts)
725
- }
726
-
727
- customValues[field.key] = promptValue(response)
728
- }
729
- }
730
-
731
- const cloneRootResult = await prompts.text({
732
- message: 'Clone root directory',
733
- defaultValue: existingConfig?.cloneRoot ?? getDefaultCloneRoot(),
734
- validate: validateCloneRoot,
735
- })
736
- if (prompts.isCancel(cloneRootResult)) {
737
- cancelSetup(prompts)
738
- }
739
-
740
- const sshKey = await promptForSshPublicKey(prompts)
741
- const privateKeyPath = await promptForSshPrivateKeyPath(prompts, sshKey)
742
-
743
- const resolvedSetup = {
744
- ...resolveEnvironmentConfigForPreset(environment, customValues),
745
- cloneRoot: resolveCloneRoot(promptValue(cloneRootResult)),
746
- }
747
-
748
- const auth = await options.authenticate(
749
- resolvedSetup,
750
- (message, title) => {
751
- prompts.note?.(message, title)
752
- },
753
- )
754
-
755
- const config = normalizeConfig({
756
- ...resolvedSetup,
757
- auth,
758
- forgejoOrganization: null,
759
- forgejoUserRegistered: false,
760
- gitSshKey: createGitSshKey(sshKey, privateKeyPath),
761
- })
762
- const organization = await resolveForgejoOrganizationInteractive({
763
- prompts,
764
- existingConfig,
765
- auth,
766
- config,
767
- fetchFn: options.fetchFn,
768
- })
769
- const provisioned = await ensureForgejoUserAccess({
770
- config,
771
- organization,
772
- sshKey,
773
- fetchFn: options.fetchFn,
774
- onProgress: (message, title) => {
775
- prompts.note?.(message, title)
776
- },
777
- provisionFn: options.provisionForgejoUser,
778
- saveConfig,
779
- })
780
-
781
- if (!options.skipLifecycle) {
782
- prompts.outro('Tela CLI configured')
783
- }
784
-
785
- return provisioned.config
786
- }
787
-
788
- export async function ensureConfigInteractive(): Promise<TelaCliConfig> {
789
- const existing = tryLoadConfig()
790
- if (existing?.auth) {
791
- return existing
792
- }
793
-
794
- throw createUserFacingError({
795
- code: 'config_required',
796
- summary: 'Tela CLI is not authenticated.',
797
- fixes: ['Run `tela setup` in an interactive terminal first.'],
798
- })
799
- }