@meistrari/tela-cli 0.6.0 → 0.7.4
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 +56 -106
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/doctor-auth.d.ts +2 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/credentials.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7027 -0
- package/dist/output.d.ts +13 -0
- package/dist/program.d.ts +2 -0
- package/package.json +17 -24
- package/scripts/install-skills.cjs +127 -0
- package/skills/tela-cli/SKILL.md +20 -0
- package/skills/tela-cli/references/agent.md +111 -0
- package/cli.ts +0 -25
- package/postinstall.sh +0 -19
- package/src/agent-api.ts +0 -300
- package/src/assets.d.ts +0 -4
- package/src/auth.ts +0 -432
- package/src/claude.ts +0 -101
- package/src/commands/doctor.ts +0 -35
- package/src/commands/feedback.ts +0 -701
- package/src/commands/metadata.ts +0 -242
- package/src/commands/push.ts +0 -208
- package/src/commands/run.ts +0 -1699
- package/src/commands/status.ts +0 -109
- package/src/commands/sync.ts +0 -245
- package/src/commit-message.ts +0 -69
- package/src/config.ts +0 -799
- package/src/diagnostics.ts +0 -813
- package/src/errors.ts +0 -122
- package/src/forgejo.ts +0 -233
- package/src/git.ts +0 -457
- package/src/ink/assets/160092__jorickhoofd__dog-bark-1.mp3 +0 -0
- package/src/ink/bark.tsx +0 -240
- package/src/ink/components.tsx +0 -177
- package/src/ink/runtime.tsx +0 -89
- package/src/ink/screens/feedback-screen.tsx +0 -394
- package/src/ink/screens/push-screen.tsx +0 -231
- package/src/ink/screens/run-screen.tsx +0 -291
- package/src/ink/screens/setup/breadcrumb.tsx +0 -23
- package/src/ink/screens/setup/setup-screen.tsx +0 -611
- package/src/ink/screens/setup/step-frame.tsx +0 -30
- package/src/ink/screens/setup/steps/done-step.tsx +0 -28
- package/src/ink/screens/setup/steps/environment-step.tsx +0 -22
- package/src/ink/screens/setup/steps/organization-step.tsx +0 -34
- package/src/ink/screens/setup/steps/review-step.tsx +0 -77
- package/src/ink/screens/setup/steps/running-step.tsx +0 -36
- package/src/ink/screens/setup/steps/ssh-key-step.tsx +0 -47
- package/src/ink/screens/setup/steps/text-field-step.tsx +0 -17
- package/src/ink/screens/setup/steps.ts +0 -156
- package/src/ink/screens/setup/types.ts +0 -65
- package/src/ink/screens/sync-screen.tsx +0 -282
- package/src/process.ts +0 -251
- package/src/program.ts +0 -2105
- package/src/runtime.ts +0 -147
- package/src/types.ts +0 -165
- package/src/ui.ts +0 -295
- 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
|
-
}
|