@meistrari/tela-skills 1.3.4 → 1.4.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 +3 -0
- package/cli.ts +315 -61
- package/list-workspaces.ts +43 -0
- package/localhost.ts +44 -0
- package/package.json +7 -3
- package/skill/TELA_LOCALHOST_SKILL.md +106 -0
- package/skill/TELA_SKILL.md +5 -5
- package/skill/common.ts +52 -2
- package/skill/index.ts +5 -5
- package/skill/localhost-utils.test.ts +107 -0
- package/skill/localhost-utils.ts +175 -0
- package/skill/workflow-builders.ts +1 -1
- package/skill/{workflow-nodes-v1-v2.md → workflow-nodes-v2-v3.md} +20 -20
- package/skill/{workflow-references-v1-v2.md → workflow-references-v2-v3.md} +6 -6
- package/skill/workflow-run-builders.ts +2 -2
- package/skill/workflow-spec-builders.ts +4 -4
- package/skill/workflow-v2-create-update.md +12 -23
- package/skill/workflow-v3-create-update.md +65 -0
- package/skill/workflow.md +16 -16
- package/skill/workflows.ts +37 -37
- package/skill/workflow-v1-create-update.md +0 -54
package/README.md
CHANGED
|
@@ -60,6 +60,9 @@ bun run cli.ts
|
|
|
60
60
|
|
|
61
61
|
# Verify skill loads correctly
|
|
62
62
|
bun --preload skill/preload.ts -e "console.log(Object.keys(tela))"
|
|
63
|
+
|
|
64
|
+
# Localhost (API :4000, App :3000)
|
|
65
|
+
bun localhost.ts "console.log(await tela.listProjects())"
|
|
63
66
|
```
|
|
64
67
|
|
|
65
68
|
### File layout after installation
|
package/cli.ts
CHANGED
|
@@ -5,60 +5,49 @@ import { homedir } from 'node:os'
|
|
|
5
5
|
import { join, dirname } from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import open from 'open'
|
|
8
|
+
import {
|
|
9
|
+
discoverLocalhostServices,
|
|
10
|
+
parseApiGatewayKeys,
|
|
11
|
+
readAuthApiSecret,
|
|
12
|
+
signLocalhostJwt,
|
|
13
|
+
fetchWorkspaces,
|
|
14
|
+
saveLocalhostSession,
|
|
15
|
+
writeLocalhostEnv,
|
|
16
|
+
readSavedReposPath,
|
|
17
|
+
saveReposPath,
|
|
18
|
+
} from './skill/localhost-utils.ts'
|
|
8
19
|
|
|
9
20
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
21
|
const SKILLS_DIR = join(homedir(), '.claude', 'skills', 'tela')
|
|
11
22
|
const OPEN_ISSUE_SKILLS_DIR = join(homedir(), '.claude', 'skills', 'tela-open-issue')
|
|
23
|
+
const LOCALHOST_SKILLS_DIR = join(homedir(), '.claude', 'skills', 'tela-localhost')
|
|
12
24
|
const SESSION_DIR = join(homedir(), '.tela')
|
|
13
25
|
const BACKOFFICE_URL = 'https://backoffice.tela.com'
|
|
14
26
|
const STAGING_BACKOFFICE_URL = 'https://staging-backoffice.tela.com'
|
|
15
27
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const env = environment as 'production' | 'staging'
|
|
33
|
-
const sessionPath = join(SESSION_DIR, `session.${env}`)
|
|
34
|
-
const sessionLabel = `~/.tela/session.${env}`
|
|
35
|
-
const backofficeUrl = env === 'staging' ? STAGING_BACKOFFICE_URL : BACKOFFICE_URL
|
|
36
|
-
|
|
37
|
-
const skillsExist = existsSync(SKILLS_DIR)
|
|
38
|
-
|
|
39
|
-
if (skillsExist) {
|
|
40
|
-
const update = await p.confirm({
|
|
41
|
-
message: 'Tela skills already installed. Update to latest version?',
|
|
42
|
-
initialValue: true,
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
if (p.isCancel(update) || !update) {
|
|
46
|
-
p.cancel('Installation cancelled.')
|
|
47
|
-
process.exit(0)
|
|
28
|
+
// --- CLI Flag Parsing ---
|
|
29
|
+
function parseFlags(argv: string[]): Record<string, string | true> {
|
|
30
|
+
const flags: Record<string, string | true> = {}
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const arg = argv[i]!
|
|
33
|
+
if (arg.startsWith('--')) {
|
|
34
|
+
const key = arg.slice(2)
|
|
35
|
+
const next = argv[i + 1]
|
|
36
|
+
if (next && !next.startsWith('--')) {
|
|
37
|
+
flags[key] = next
|
|
38
|
+
i++
|
|
39
|
+
} else {
|
|
40
|
+
flags[key] = true
|
|
41
|
+
}
|
|
48
42
|
}
|
|
49
43
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
message: 'Install Tela skills to ~/.claude/skills/tela/?',
|
|
53
|
-
initialValue: true,
|
|
54
|
-
})
|
|
44
|
+
return flags
|
|
45
|
+
}
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
process.exit(0)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
47
|
+
const flags = parseFlags(process.argv.slice(2))
|
|
48
|
+
const YES = 'yes' in flags || 'y' in flags
|
|
61
49
|
|
|
50
|
+
async function installSkills() {
|
|
62
51
|
const spinner = p.spinner()
|
|
63
52
|
spinner.start('Installing skill files...')
|
|
64
53
|
|
|
@@ -68,17 +57,23 @@ async function main() {
|
|
|
68
57
|
const sourceDir = join(__dirname, 'skill')
|
|
69
58
|
const openIssueSkillFile = 'TELA_OPEN_ISSUE_SKILL.md'
|
|
70
59
|
const telaSkillFile = 'TELA_SKILL.md'
|
|
71
|
-
const
|
|
60
|
+
const localhostSkillFile = 'TELA_LOCALHOST_SKILL.md'
|
|
61
|
+
const files = readdirSync(sourceDir).filter(f =>
|
|
62
|
+
f !== openIssueSkillFile && f !== telaSkillFile && f !== localhostSkillFile
|
|
63
|
+
)
|
|
72
64
|
|
|
73
65
|
for (const file of files) {
|
|
74
66
|
cpSync(join(sourceDir, file), join(SKILLS_DIR, file))
|
|
75
67
|
}
|
|
76
68
|
cpSync(join(sourceDir, telaSkillFile), join(SKILLS_DIR, 'SKILL.md'))
|
|
77
69
|
|
|
78
|
-
// Install open-issue skill to its own directory
|
|
79
70
|
mkdirSync(OPEN_ISSUE_SKILLS_DIR, { recursive: true })
|
|
80
71
|
cpSync(join(sourceDir, openIssueSkillFile), join(OPEN_ISSUE_SKILLS_DIR, 'SKILL.md'))
|
|
81
72
|
|
|
73
|
+
// Install localhost skill to its own directory
|
|
74
|
+
mkdirSync(LOCALHOST_SKILLS_DIR, { recursive: true })
|
|
75
|
+
cpSync(join(sourceDir, localhostSkillFile), join(LOCALHOST_SKILLS_DIR, 'SKILL.md'))
|
|
76
|
+
|
|
82
77
|
const agentsSourceDir = join(__dirname, 'agents')
|
|
83
78
|
const agentsDestDir = join(homedir(), '.claude', 'agents')
|
|
84
79
|
if (existsSync(agentsSourceDir)) {
|
|
@@ -107,38 +102,296 @@ async function main() {
|
|
|
107
102
|
}
|
|
108
103
|
|
|
109
104
|
depSpinner.stop(`Installed ${dependencies.length} dependencies`)
|
|
110
|
-
}
|
|
111
|
-
catch (err: unknown) {
|
|
105
|
+
} catch (err: unknown) {
|
|
112
106
|
spinner.stop('Installation failed')
|
|
113
107
|
p.log.error(err instanceof Error ? err.message : String(err))
|
|
114
108
|
process.exit(1)
|
|
115
109
|
}
|
|
110
|
+
}
|
|
116
111
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
async function handleLocalhostSetup() {
|
|
113
|
+
// 1. Discover Docker services
|
|
114
|
+
const discoverSpinner = p.spinner()
|
|
115
|
+
discoverSpinner.start('Discovering local services via Docker...')
|
|
116
|
+
|
|
117
|
+
const services = discoverLocalhostServices()
|
|
118
|
+
|
|
119
|
+
if (!services.authApiPort) {
|
|
120
|
+
discoverSpinner.stop('auth-api container not found')
|
|
121
|
+
p.log.error('Could not find auth-api Docker container. Make sure local services are running.')
|
|
122
|
+
process.exit(1)
|
|
123
|
+
}
|
|
124
|
+
if (!services.telaApiPort) {
|
|
125
|
+
discoverSpinner.stop('tela-api container not found')
|
|
126
|
+
p.log.error('Could not find tela-api Docker container. Make sure local services are running.')
|
|
127
|
+
process.exit(1)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
discoverSpinner.stop(`Found services — auth-api :${services.authApiPort}, tela-api :${services.telaApiPort}${services.telaAppPort ? `, tela-app :${services.telaAppPort}` : ''}`)
|
|
131
|
+
|
|
132
|
+
// 2. Ask for meistrari repos path
|
|
133
|
+
const savedPath = readSavedReposPath()
|
|
134
|
+
const reposPathFlag = typeof flags['repos-path'] === 'string' ? flags['repos-path'] : null
|
|
135
|
+
|
|
136
|
+
let reposPath: string
|
|
137
|
+
if (reposPathFlag) {
|
|
138
|
+
reposPath = reposPathFlag
|
|
139
|
+
p.log.info(`Using repos path: ${reposPath}`)
|
|
140
|
+
} else if (YES && savedPath) {
|
|
141
|
+
reposPath = savedPath
|
|
142
|
+
p.log.info(`Using saved repos path: ${reposPath}`)
|
|
143
|
+
} else {
|
|
144
|
+
const result = await p.text({
|
|
145
|
+
message: 'Where is the tela monorepo cloned?',
|
|
146
|
+
placeholder: '/path/to/tela',
|
|
147
|
+
...(savedPath ? { defaultValue: savedPath } : {}),
|
|
148
|
+
validate(value): string | undefined {
|
|
149
|
+
if (!value.trim()) return 'Path is required'
|
|
150
|
+
return undefined
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
if (p.isCancel(result)) {
|
|
155
|
+
p.cancel('Setup cancelled.')
|
|
156
|
+
process.exit(0)
|
|
157
|
+
}
|
|
158
|
+
reposPath = result
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const resolvedPath = reposPath.replace(/^~/, homedir())
|
|
162
|
+
saveReposPath(resolvedPath)
|
|
163
|
+
|
|
164
|
+
// 3. Read RSA keys and auth secret
|
|
165
|
+
const keysSpinner = p.spinner()
|
|
166
|
+
keysSpinner.start('Reading RSA keys from api-gateway...')
|
|
167
|
+
|
|
168
|
+
let keys: ReturnType<typeof parseApiGatewayKeys>
|
|
169
|
+
let authApiSecret: string
|
|
170
|
+
try {
|
|
171
|
+
keys = parseApiGatewayKeys(resolvedPath)
|
|
172
|
+
authApiSecret = readAuthApiSecret(resolvedPath)
|
|
173
|
+
keysSpinner.stop('RSA keys loaded')
|
|
174
|
+
} catch (err) {
|
|
175
|
+
keysSpinner.stop('Failed to read keys')
|
|
176
|
+
p.log.error(err instanceof Error ? err.message : String(err))
|
|
177
|
+
process.exit(1)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 4. Fetch workspaces
|
|
181
|
+
const authApiUrl = `http://localhost:${services.authApiPort}`
|
|
182
|
+
const wsSpinner = p.spinner()
|
|
183
|
+
wsSpinner.start('Fetching workspaces from auth-api...')
|
|
184
|
+
|
|
185
|
+
let workspaces: Awaited<ReturnType<typeof fetchWorkspaces>>
|
|
186
|
+
try {
|
|
187
|
+
workspaces = await fetchWorkspaces(authApiUrl, authApiSecret)
|
|
188
|
+
wsSpinner.stop(`Found ${workspaces.length} workspace(s)`)
|
|
189
|
+
} catch (err) {
|
|
190
|
+
wsSpinner.stop('Failed to fetch workspaces')
|
|
191
|
+
p.log.error(err instanceof Error ? err.message : String(err))
|
|
192
|
+
process.exit(1)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (workspaces.length === 0) {
|
|
196
|
+
p.log.warning('No workspaces found. Create one using auth-api CLI scripts first.')
|
|
197
|
+
process.exit(1)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 5. Select workspace
|
|
201
|
+
const workspaceFlag = typeof flags['workspace'] === 'string' ? flags['workspace'] : null
|
|
202
|
+
|
|
203
|
+
let workspace: { id: string; title: string; slug: string }
|
|
204
|
+
if (workspaceFlag) {
|
|
205
|
+
const match = workspaces.find(ws => ws.id === workspaceFlag || ws.slug === workspaceFlag)
|
|
206
|
+
if (!match) {
|
|
207
|
+
p.log.error(`Workspace "${workspaceFlag}" not found. Available: ${workspaces.map(ws => `${ws.slug} (${ws.id})`).join(', ')}`)
|
|
208
|
+
process.exit(1)
|
|
209
|
+
}
|
|
210
|
+
workspace = match
|
|
211
|
+
p.log.info(`Using workspace: ${workspace.title}`)
|
|
212
|
+
} else if (YES && workspaces.length === 1) {
|
|
213
|
+
workspace = workspaces[0]!
|
|
214
|
+
p.log.info(`Auto-selecting only workspace: ${workspace.title}`)
|
|
215
|
+
} else {
|
|
216
|
+
const selectedWorkspace = await p.select({
|
|
217
|
+
message: 'Which workspace do you want to connect to?',
|
|
218
|
+
options: workspaces.map(ws => ({
|
|
219
|
+
value: ws,
|
|
220
|
+
label: ws.title,
|
|
221
|
+
hint: ws.slug,
|
|
222
|
+
})),
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
if (p.isCancel(selectedWorkspace)) {
|
|
226
|
+
p.cancel('Setup cancelled.')
|
|
227
|
+
process.exit(0)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
workspace = selectedWorkspace as { id: string; title: string; slug: string }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 6. Generate JWT and save
|
|
234
|
+
const tokenSpinner = p.spinner()
|
|
235
|
+
tokenSpinner.start('Generating authentication token...')
|
|
236
|
+
|
|
237
|
+
const token = signLocalhostJwt({
|
|
238
|
+
workspace: { id: workspace.id, title: workspace.title },
|
|
239
|
+
privateKey: keys.privateKey,
|
|
240
|
+
kid: keys.kid,
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
saveLocalhostSession(token)
|
|
244
|
+
writeLocalhostEnv(services.telaApiPort, services.telaAppPort)
|
|
245
|
+
|
|
246
|
+
tokenSpinner.stop('Token generated and saved')
|
|
247
|
+
|
|
248
|
+
// 7. Install skills (reuse existing logic)
|
|
249
|
+
if (YES) {
|
|
250
|
+
await installSkills()
|
|
251
|
+
} else {
|
|
252
|
+
const skillsExist = existsSync(SKILLS_DIR)
|
|
253
|
+
|
|
254
|
+
if (skillsExist) {
|
|
255
|
+
const update = await p.confirm({
|
|
256
|
+
message: 'Tela skills already installed. Update to latest version?',
|
|
257
|
+
initialValue: true,
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
if (!p.isCancel(update) && update) {
|
|
261
|
+
await installSkills()
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
const install = await p.confirm({
|
|
265
|
+
message: 'Install Tela skills to ~/.claude/skills/tela/?',
|
|
266
|
+
initialValue: true,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
if (!p.isCancel(install) && install) {
|
|
270
|
+
await installSkills()
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
writeFileSync(join(SESSION_DIR, 'environment'), 'localhost', { mode: 0o600 })
|
|
276
|
+
|
|
277
|
+
p.log.success(`Connected to workspace "${workspace.title}" on localhost`)
|
|
278
|
+
|
|
279
|
+
p.note(
|
|
280
|
+
`Use in Claude Code with bun --preload:
|
|
281
|
+
|
|
282
|
+
bun --preload ~/.claude/skills/tela/preload.ts -e "
|
|
283
|
+
console.log(await tela.listProjects())
|
|
284
|
+
"`,
|
|
285
|
+
'Usage',
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
p.outro('Localhost setup complete!')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function main() {
|
|
292
|
+
p.intro('Tela Skills for Claude Code')
|
|
293
|
+
|
|
294
|
+
const envFlag = typeof flags['env'] === 'string' ? flags['env'] : null
|
|
295
|
+
const validEnvs = ['production', 'staging', 'localhost'] as const
|
|
296
|
+
|
|
297
|
+
let environment: string
|
|
298
|
+
if (envFlag) {
|
|
299
|
+
if (!validEnvs.includes(envFlag as any)) {
|
|
300
|
+
p.log.error(`Invalid environment: "${envFlag}". Valid: ${validEnvs.join(', ')}`)
|
|
301
|
+
process.exit(1)
|
|
302
|
+
}
|
|
303
|
+
environment = envFlag
|
|
304
|
+
p.log.info(`Environment: ${environment}`)
|
|
305
|
+
} else {
|
|
306
|
+
const selected = await p.select({
|
|
307
|
+
message: 'Which environment do you want to connect to?',
|
|
308
|
+
options: [
|
|
309
|
+
{ value: 'production', label: 'Production' },
|
|
310
|
+
{ value: 'staging', label: 'Staging' },
|
|
311
|
+
{ value: 'localhost', label: 'Localhost (internal)' },
|
|
312
|
+
],
|
|
121
313
|
})
|
|
122
314
|
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
315
|
+
if (p.isCancel(selected)) {
|
|
316
|
+
p.cancel('Setup cancelled.')
|
|
317
|
+
process.exit(0)
|
|
318
|
+
}
|
|
319
|
+
environment = selected as string
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (environment === 'localhost') {
|
|
323
|
+
await handleLocalhostSetup()
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const env = environment as 'production' | 'staging'
|
|
328
|
+
const sessionPath = join(SESSION_DIR, `session.${env}`)
|
|
329
|
+
const sessionLabel = `~/.tela/session.${env}`
|
|
330
|
+
const backofficeUrl = env === 'staging' ? STAGING_BACKOFFICE_URL : BACKOFFICE_URL
|
|
331
|
+
|
|
332
|
+
if (YES) {
|
|
333
|
+
await installSkills()
|
|
334
|
+
} else {
|
|
335
|
+
const skillsExist = existsSync(SKILLS_DIR)
|
|
336
|
+
|
|
337
|
+
if (skillsExist) {
|
|
338
|
+
const update = await p.confirm({
|
|
339
|
+
message: 'Tela skills already installed. Update to latest version?',
|
|
340
|
+
initialValue: true,
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
if (p.isCancel(update) || !update) {
|
|
344
|
+
p.cancel('Installation cancelled.')
|
|
345
|
+
process.exit(0)
|
|
346
|
+
}
|
|
129
347
|
}
|
|
130
348
|
else {
|
|
349
|
+
const install = await p.confirm({
|
|
350
|
+
message: 'Install Tela skills to ~/.claude/skills/tela/?',
|
|
351
|
+
initialValue: true,
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
if (p.isCancel(install) || !install) {
|
|
355
|
+
p.cancel('Installation cancelled.')
|
|
356
|
+
process.exit(0)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
await installSkills()
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (existsSync(sessionPath)) {
|
|
364
|
+
if (YES) {
|
|
131
365
|
p.log.info(`Keeping existing session at ${sessionLabel}`)
|
|
366
|
+
} else {
|
|
367
|
+
const reconnect = await p.confirm({
|
|
368
|
+
message: 'Session already configured. Clear and reconnect?',
|
|
369
|
+
initialValue: false,
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
if (!p.isCancel(reconnect) && reconnect) {
|
|
373
|
+
const { unlinkSync } = await import('node:fs')
|
|
374
|
+
unlinkSync(sessionPath)
|
|
375
|
+
const envPath = join(SESSION_DIR, 'environment')
|
|
376
|
+
if (existsSync(envPath)) unlinkSync(envPath)
|
|
377
|
+
p.log.info('Session cleared. Proceeding to authentication...')
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
p.log.info(`Keeping existing session at ${sessionLabel}`)
|
|
381
|
+
}
|
|
132
382
|
}
|
|
133
383
|
}
|
|
134
384
|
|
|
135
385
|
if (!existsSync(sessionPath)) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
386
|
+
const shouldAuth = YES || await (async () => {
|
|
387
|
+
const setupKey = await p.confirm({
|
|
388
|
+
message: 'Set up Tela authentication now?',
|
|
389
|
+
initialValue: true,
|
|
390
|
+
})
|
|
391
|
+
return !p.isCancel(setupKey) && setupKey
|
|
392
|
+
})()
|
|
140
393
|
|
|
141
|
-
if (
|
|
394
|
+
if (shouldAuth) {
|
|
142
395
|
let resolveToken: (token: string) => void
|
|
143
396
|
let rejectToken: (error: Error) => void
|
|
144
397
|
const tokenPromise = new Promise<string>((resolve, reject) => {
|
|
@@ -147,6 +400,7 @@ async function main() {
|
|
|
147
400
|
})
|
|
148
401
|
|
|
149
402
|
const server = Bun.serve({
|
|
403
|
+
hostname: '127.0.0.1',
|
|
150
404
|
port: 0,
|
|
151
405
|
fetch(req) {
|
|
152
406
|
const url = new URL(req.url)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* List available Tela workspaces from the local auth-api.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun list-workspaces.ts
|
|
7
|
+
* bunx @meistrari/tela-skills/list-workspaces
|
|
8
|
+
*
|
|
9
|
+
* Requires:
|
|
10
|
+
* - Docker running with auth-api container
|
|
11
|
+
* - Tela monorepo path saved at ~/.tela/tela-repo-path (via CLI setup)
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
discoverLocalhostServices,
|
|
15
|
+
readSavedReposPath,
|
|
16
|
+
readAuthApiSecret,
|
|
17
|
+
fetchWorkspaces,
|
|
18
|
+
} from './skill/localhost-utils.ts'
|
|
19
|
+
|
|
20
|
+
const reposPath = readSavedReposPath()
|
|
21
|
+
if (!reposPath) {
|
|
22
|
+
console.error('No tela repo path saved. Run CLI setup first: bunx @meistrari/tela-skills → select Localhost')
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const services = discoverLocalhostServices()
|
|
27
|
+
if (!services.authApiPort) {
|
|
28
|
+
console.error('auth-api Docker container not found. Are local services running?')
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const secret = readAuthApiSecret(reposPath)
|
|
33
|
+
const workspaces = await fetchWorkspaces(`http://localhost:${services.authApiPort}`, secret)
|
|
34
|
+
|
|
35
|
+
if (workspaces.length === 0) {
|
|
36
|
+
console.log('No workspaces found.')
|
|
37
|
+
process.exit(0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log('Available workspaces:')
|
|
41
|
+
for (const ws of workspaces) {
|
|
42
|
+
console.log(` - ${ws.title} (${ws.id}) [${ws.slug}]`)
|
|
43
|
+
}
|
package/localhost.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Run tela-skills against localhost (discovers ports via Docker).
|
|
4
|
+
* Usage: bun run localhost -- "console.log(await tela.listProjects())"
|
|
5
|
+
* or: bun localhost.ts "console.log(await tela.listProjects())"
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from 'node:fs'
|
|
8
|
+
import { dirname, join } from 'node:path'
|
|
9
|
+
import { homedir } from 'node:os'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
import { discoverLocalhostServices, writeLocalhostEnv } from './skill/localhost-utils.ts'
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
const TELA_PRELOAD = join(__dirname, 'skill', 'preload.ts')
|
|
15
|
+
const SESSION_PATH = join(homedir(), '.tela', 'session.local')
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2)
|
|
18
|
+
const code = args.join(' ').trim()
|
|
19
|
+
if (!code) {
|
|
20
|
+
console.error('Usage: bun localhost.ts "console.log(await tela.listProjects())"')
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check session.local exists
|
|
25
|
+
if (!existsSync(SESSION_PATH)) {
|
|
26
|
+
console.error('Error: No localhost session found at ~/.tela/session.local')
|
|
27
|
+
console.error('Run CLI setup first: bunx @meistrari/tela-skills → select Localhost')
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Discover ports from Docker
|
|
32
|
+
const services = discoverLocalhostServices()
|
|
33
|
+
if (!services.telaApiPort) {
|
|
34
|
+
console.error('Error: tela-api Docker container not found. Are local services running?')
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Write .env with discovered ports
|
|
39
|
+
writeLocalhostEnv(services.telaApiPort, services.telaAppPort)
|
|
40
|
+
|
|
41
|
+
const proc = Bun.spawn(['bun', '--preload', TELA_PRELOAD, '-e', code], {
|
|
42
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
43
|
+
})
|
|
44
|
+
process.exit(await proc.exited)
|
package/package.json
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meistrari/tela-skills",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Tela API skills for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"tela-skills": "./cli.ts"
|
|
7
|
+
"tela-skills": "./cli.ts",
|
|
8
|
+
"tela-list-workspaces": "./list-workspaces.ts"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
11
|
+
"prepare": "ln -sf ../../scripts/pre-commit .git/hooks/pre-commit",
|
|
10
12
|
"postinstall": "mise-en-place postinstall",
|
|
11
13
|
"build": "echo \"No build step required, script placeholder for npm publish action compatibility\""
|
|
12
14
|
},
|
|
13
15
|
"files": [
|
|
14
16
|
"cli.ts",
|
|
17
|
+
"localhost.ts",
|
|
18
|
+
"list-workspaces.ts",
|
|
15
19
|
"skill/**/*"
|
|
16
20
|
],
|
|
17
21
|
"keywords": [
|
|
@@ -29,7 +33,7 @@
|
|
|
29
33
|
"open": "^11.0.0"
|
|
30
34
|
},
|
|
31
35
|
"devDependencies": {
|
|
32
|
-
"@meistrari/mise-en-place": "^2.10.
|
|
36
|
+
"@meistrari/mise-en-place": "^2.10.6",
|
|
33
37
|
"bun-types": "^1.3.4"
|
|
34
38
|
}
|
|
35
39
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tela-localhost
|
|
3
|
+
description: Manage localhost workspaces for Tela. Switch workspaces, check status, and regenerate tokens for local development.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tela Localhost Workspace Manager
|
|
7
|
+
|
|
8
|
+
Manage your local Tela development environment. Switch between workspaces, check connection status, and regenerate authentication tokens.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
- Docker running with tela services (auth-api, tela-api)
|
|
13
|
+
- Localhost environment configured via `bunx @meistrari/tela-skills` (select "Localhost")
|
|
14
|
+
- Tela monorepo path saved at `~/.tela/tela-repo-path`
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
### List Workspaces
|
|
19
|
+
|
|
20
|
+
Show all available workspaces in the local environment.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bunx @meistrari/tela-skills/list-workspaces
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or if running from the repo directly:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bun list-workspaces.ts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Switch Workspace
|
|
33
|
+
|
|
34
|
+
Switch to a different workspace by generating a new JWT token.
|
|
35
|
+
|
|
36
|
+
Replace `WORKSPACE_ID` and `WORKSPACE_TITLE` with the target workspace values from "List Workspaces".
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bun --preload ~/.claude/skills/tela/preload.ts -e "
|
|
40
|
+
const { join } = await import('node:path')
|
|
41
|
+
const { homedir } = await import('node:os')
|
|
42
|
+
const lu = await import(join(homedir(), '.claude', 'skills', 'tela', 'localhost-utils.ts'))
|
|
43
|
+
const reposPath = lu.readSavedReposPath()
|
|
44
|
+
if (!reposPath) { console.error('No tela repo path saved. Run CLI setup first.'); process.exit(1) }
|
|
45
|
+
|
|
46
|
+
const keys = lu.parseApiGatewayKeys(reposPath)
|
|
47
|
+
const token = lu.signLocalhostJwt({
|
|
48
|
+
workspace: { id: 'WORKSPACE_ID', title: 'WORKSPACE_TITLE' },
|
|
49
|
+
privateKey: keys.privateKey,
|
|
50
|
+
kid: keys.kid,
|
|
51
|
+
})
|
|
52
|
+
lu.saveLocalhostSession(token)
|
|
53
|
+
console.log('Switched to workspace: WORKSPACE_TITLE')
|
|
54
|
+
console.log('New token saved to ~/.tela/session.local')
|
|
55
|
+
"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Check Status
|
|
59
|
+
|
|
60
|
+
Verify localhost connection is working and show current workspace.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bun --preload ~/.claude/skills/tela/preload.ts -e "
|
|
64
|
+
const { readFileSync, existsSync } = await import('node:fs')
|
|
65
|
+
const { join } = await import('node:path')
|
|
66
|
+
const { homedir } = await import('node:os')
|
|
67
|
+
|
|
68
|
+
const envPath = join(homedir(), '.tela', '.env')
|
|
69
|
+
const sessionPath = join(homedir(), '.tela', 'session.local')
|
|
70
|
+
|
|
71
|
+
if (!existsSync(sessionPath)) {
|
|
72
|
+
console.log('Status: NOT CONFIGURED')
|
|
73
|
+
console.log('Run CLI setup: bunx @meistrari/tela-skills → select Localhost')
|
|
74
|
+
process.exit(0)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Decode JWT payload
|
|
78
|
+
const token = readFileSync(sessionPath, 'utf-8').trim()
|
|
79
|
+
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString())
|
|
80
|
+
console.log('Status: CONFIGURED')
|
|
81
|
+
console.log('Workspace:', payload.workspace?.title, '(' + payload.workspace?.id + ')')
|
|
82
|
+
console.log('Token expires:', new Date(payload.exp * 1000).toISOString())
|
|
83
|
+
|
|
84
|
+
// Check env
|
|
85
|
+
if (existsSync(envPath)) {
|
|
86
|
+
const env = readFileSync(envPath, 'utf-8')
|
|
87
|
+
console.log('API URL:', env.match(/TELA_API_URL=(.*)/)?.[1] || 'not set')
|
|
88
|
+
console.log('App URL:', env.match(/TELA_APP_URL=(.*)/)?.[1] || 'not set')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Try a test call
|
|
92
|
+
try {
|
|
93
|
+
const projects = await tela.listProjects()
|
|
94
|
+
console.log('Connection: OK (' + projects.length + ' projects)')
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.log('Connection: FAILED -', e.message)
|
|
97
|
+
}
|
|
98
|
+
"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Troubleshooting
|
|
102
|
+
|
|
103
|
+
- **"auth-api not running"**: Start Docker services in the tela monorepo (`make dev`)
|
|
104
|
+
- **"No tela repo path saved"**: Run `bunx @meistrari/tela-skills` and select Localhost to configure
|
|
105
|
+
- **"Connection: FAILED"**: Check that tela-api Docker container is running (`docker ps | grep tela-api`)
|
|
106
|
+
- **Token expired**: Tokens are auto-regenerated when expired. If auto-regeneration fails, use "Switch Workspace" to regenerate manually
|