@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/git.ts DELETED
@@ -1,457 +0,0 @@
1
- import { basename } from 'node:path'
2
- import { inspectSavedConfig } from './config.ts'
3
- import { createUserFacingError } from './errors.ts'
4
- import { resolveSshPrivateKeyPath } from './forgejo.ts'
5
- import {
6
- CommandError,
7
- runCommand,
8
- type CommandResult,
9
- type RunCommandOptions,
10
- } from './process.ts'
11
- import type { TelaCliGitSshKey } from './types.ts'
12
-
13
- export interface GitStatusEntry {
14
- index: string
15
- worktree: string
16
- path: string
17
- }
18
-
19
- export interface GitStatusCounts {
20
- staged: number
21
- unstaged: number
22
- untracked: number
23
- total: number
24
- }
25
-
26
- type GitSshHostKeyPolicy = 'default' | 'accept-new'
27
-
28
- interface GitRunOptions extends Omit<RunCommandOptions, 'env'> {
29
- requireConfiguredSsh?: boolean
30
- sshHostKeyPolicy?: GitSshHostKeyPolicy
31
- }
32
-
33
- function quoteForShell(value: string): string {
34
- return `'${value.replaceAll('\'', `'"'"'`)}'`
35
- }
36
-
37
- export function buildGitSshCommand(
38
- privateKeyPath: string,
39
- options: {
40
- hostKeyPolicy?: GitSshHostKeyPolicy
41
- } = {},
42
- ): string {
43
- const args = [
44
- `-i ${quoteForShell(privateKeyPath)}`,
45
- '-o IdentitiesOnly=yes',
46
- ]
47
-
48
- if (options.hostKeyPolicy === 'accept-new') {
49
- args.push('-o StrictHostKeyChecking=accept-new')
50
- }
51
-
52
- return `ssh ${args.join(' ')}`
53
- }
54
-
55
- export function isSshRemoteUrl(remoteUrl: string): boolean {
56
- const trimmed = remoteUrl.trim()
57
- return /^ssh:\/\//i.test(trimmed) || /^[^\s@]+@[^\s:]+:.+$/.test(trimmed)
58
- }
59
-
60
- function resolveConfiguredGitSshKey(requireConfiguredSsh: boolean): TelaCliGitSshKey | null {
61
- const inspection = inspectSavedConfig()
62
- if (!inspection.exists) {
63
- if (requireConfiguredSsh) {
64
- throw createUserFacingError({
65
- code: 'git_ssh_key_config_missing',
66
- summary: 'Tela CLI is not configured with a Git SSH key.',
67
- fixes: [
68
- 'Run `tela setup` and choose the SSH key to force on Git operations.',
69
- ],
70
- })
71
- }
72
-
73
- return null
74
- }
75
-
76
- if (!inspection.valid || !inspection.config) {
77
- if (requireConfiguredSsh) {
78
- throw createUserFacingError({
79
- code: 'git_ssh_key_config_invalid',
80
- summary: 'Tela CLI config is invalid, so the Git SSH key could not be loaded.',
81
- details: inspection.error,
82
- fixes: [
83
- 'Run `tela setup` to rewrite the config and choose the SSH key again.',
84
- ],
85
- })
86
- }
87
-
88
- return null
89
- }
90
-
91
- if (!inspection.config.gitSshKey) {
92
- if (requireConfiguredSsh) {
93
- throw createUserFacingError({
94
- code: 'git_ssh_key_missing',
95
- summary: 'No Git SSH key is configured.',
96
- fixes: [
97
- 'Run `tela setup` again and choose the SSH key to use for Git operations.',
98
- ],
99
- })
100
- }
101
-
102
- return null
103
- }
104
-
105
- try {
106
- return {
107
- ...inspection.config.gitSshKey,
108
- privateKeyPath: resolveSshPrivateKeyPath(inspection.config.gitSshKey.privateKeyPath),
109
- }
110
- }
111
- catch (error) {
112
- if (requireConfiguredSsh) {
113
- throw createUserFacingError({
114
- code: 'git_ssh_key_invalid',
115
- summary: 'The configured Git SSH private key is unavailable.',
116
- details: error instanceof Error ? error.message : String(error),
117
- fixes: [
118
- 'Restore the configured private key file, or run `tela setup` again and choose another SSH key.',
119
- ],
120
- cause: error,
121
- })
122
- }
123
-
124
- return null
125
- }
126
- }
127
-
128
- function buildGitEnv(
129
- requireConfiguredSsh: boolean,
130
- sshHostKeyPolicy: GitSshHostKeyPolicy = 'default',
131
- ): Record<string, string> | undefined {
132
- const gitSshKey = resolveConfiguredGitSshKey(requireConfiguredSsh)
133
- if (!gitSshKey) {
134
- return undefined
135
- }
136
-
137
- return {
138
- GIT_SSH_COMMAND: buildGitSshCommand(gitSshKey.privateKeyPath, {
139
- hostKeyPolicy: sshHostKeyPolicy,
140
- }),
141
- }
142
- }
143
-
144
- async function runGitCommand(
145
- args: string[],
146
- options: GitRunOptions = {},
147
- ): Promise<CommandResult> {
148
- const env = buildGitEnv(
149
- Boolean(options.requireConfiguredSsh),
150
- options.sshHostKeyPolicy,
151
- )
152
-
153
- return await runCommand(
154
- ['git', ...args],
155
- {
156
- ...options,
157
- env: env ? env : undefined,
158
- },
159
- )
160
- }
161
-
162
- async function getRemoteUrlInternal(cwd: string, remote: string): Promise<string | null> {
163
- const result = await runGitCommand(['remote', 'get-url', remote], { cwd, allowFailure: true })
164
- if (result.exitCode !== 0) {
165
- return null
166
- }
167
-
168
- return result.stdout.trim() || null
169
- }
170
-
171
- async function remoteRequiresConfiguredSsh(cwd: string, remote: string): Promise<boolean> {
172
- const remoteUrl = await getRemoteUrlInternal(cwd, remote)
173
- return Boolean(remoteUrl && isSshRemoteUrl(remoteUrl))
174
- }
175
-
176
- async function anyRemoteRequiresConfiguredSsh(cwd: string): Promise<boolean> {
177
- const result = await runGitCommand(['remote'], { cwd, allowFailure: true })
178
- if (result.exitCode !== 0) {
179
- return false
180
- }
181
-
182
- const remotes = result.stdout
183
- .split(/\r?\n/)
184
- .map(line => line.trim())
185
- .filter(Boolean)
186
-
187
- for (const remote of remotes) {
188
- if (await remoteRequiresConfiguredSsh(cwd, remote)) {
189
- return true
190
- }
191
- }
192
-
193
- return false
194
- }
195
-
196
- async function upstreamRequiresConfiguredSsh(cwd: string): Promise<boolean> {
197
- const upstream = await getUpstreamBranch(cwd)
198
- if (!upstream) {
199
- return false
200
- }
201
-
202
- const separator = upstream.indexOf('/')
203
- if (separator <= 0) {
204
- return false
205
- }
206
-
207
- return await remoteRequiresConfiguredSsh(cwd, upstream.slice(0, separator))
208
- }
209
-
210
- export async function findRepositoryRoot(cwd: string): Promise<string | null> {
211
- const result = await runGitCommand(['rev-parse', '--show-toplevel'], { cwd, allowFailure: true })
212
- if (result.exitCode !== 0) {
213
- return null
214
- }
215
-
216
- return result.stdout.trim() || null
217
- }
218
-
219
- export async function getRepositoryRoot(cwd: string): Promise<string> {
220
- const repoRoot = await findRepositoryRoot(cwd)
221
- if (!repoRoot) {
222
- throw new Error(`Not a git repository: ${cwd}`)
223
- }
224
-
225
- return repoRoot
226
- }
227
-
228
- export async function assertGitRepository(cwd: string): Promise<void> {
229
- try {
230
- await runGitCommand(['rev-parse', '--is-inside-work-tree'], { cwd })
231
- }
232
- catch (error) {
233
- if (error instanceof CommandError) {
234
- throw new Error(`Not a git repository: ${cwd}`)
235
- }
236
- throw error
237
- }
238
- }
239
-
240
- export async function cloneRepository(remoteUrl: string, targetDir: string): Promise<void> {
241
- await runGitCommand(['clone', remoteUrl, targetDir], {
242
- requireConfiguredSsh: isSshRemoteUrl(remoteUrl),
243
- // Sync accepts first-seen SSH host keys automatically, but changed host keys still fail.
244
- sshHostKeyPolicy: 'accept-new',
245
- })
246
- }
247
-
248
- export async function getOriginUrl(cwd: string): Promise<string> {
249
- const result = await runGitCommand(['remote', 'get-url', 'origin'], { cwd })
250
- return result.stdout.trim()
251
- }
252
-
253
- export async function fetchOrigin(cwd: string): Promise<void> {
254
- await runGitCommand(['fetch', 'origin', '--prune'], {
255
- cwd,
256
- requireConfiguredSsh: await remoteRequiresConfiguredSsh(cwd, 'origin'),
257
- sshHostKeyPolicy: 'accept-new',
258
- })
259
- }
260
-
261
- export async function fetchAllRemotes(cwd: string): Promise<void> {
262
- await runGitCommand(['fetch', '--all', '--prune'], {
263
- cwd,
264
- requireConfiguredSsh: await anyRemoteRequiresConfiguredSsh(cwd),
265
- sshHostKeyPolicy: 'accept-new',
266
- })
267
- }
268
-
269
- export async function getWorktreeStatus(cwd: string): Promise<string> {
270
- const result = await runGitCommand(['status', '--short'], { cwd })
271
- return result.stdout.trim()
272
- }
273
-
274
- export async function isWorktreeDirty(cwd: string): Promise<boolean> {
275
- return Boolean(await getWorktreeStatus(cwd))
276
- }
277
-
278
- export async function stageAll(cwd: string): Promise<void> {
279
- await runGitCommand(['add', '-A'], { cwd })
280
- }
281
-
282
- export async function getCurrentBranch(cwd: string): Promise<string> {
283
- try {
284
- const result = await runGitCommand(['symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd })
285
- return result.stdout.trim()
286
- }
287
- catch (error) {
288
- if (error instanceof CommandError) {
289
- throw new Error('Detached HEAD; check out a branch before syncing or pushing.')
290
- }
291
- throw error
292
- }
293
- }
294
-
295
- export async function getUpstreamBranch(cwd: string): Promise<string | null> {
296
- const result = await runGitCommand(
297
- ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
298
- { cwd, allowFailure: true },
299
- )
300
-
301
- if (result.exitCode !== 0) {
302
- return null
303
- }
304
-
305
- return result.stdout.trim() || null
306
- }
307
-
308
- export async function pullCurrentBranch(cwd: string): Promise<void> {
309
- await runGitCommand(['pull', '--ff-only'], {
310
- cwd,
311
- requireConfiguredSsh: await upstreamRequiresConfiguredSsh(cwd),
312
- sshHostKeyPolicy: 'accept-new',
313
- })
314
- }
315
-
316
- export async function pullDefaultBranch(cwd: string, defaultBranch: string): Promise<void> {
317
- await runGitCommand(['pull', '--ff-only', 'origin', defaultBranch], {
318
- cwd,
319
- requireConfiguredSsh: await remoteRequiresConfiguredSsh(cwd, 'origin'),
320
- sshHostKeyPolicy: 'accept-new',
321
- })
322
- }
323
-
324
- export async function getShortStatus(cwd: string): Promise<string> {
325
- const result = await runGitCommand(['status', '--short'], { cwd })
326
- return result.stdout
327
- }
328
-
329
- export function parseShortStatus(output: string): GitStatusEntry[] {
330
- return output
331
- .split(/\r?\n/)
332
- .map(line => line.trimEnd())
333
- .filter(Boolean)
334
- .map((line) => {
335
- const normalized = line.padEnd(3, ' ')
336
- return {
337
- index: normalized[0] ?? ' ',
338
- worktree: normalized[1] ?? ' ',
339
- path: normalized.slice(3).trim(),
340
- }
341
- })
342
- }
343
-
344
- export async function getStatusCounts(cwd: string): Promise<GitStatusCounts> {
345
- const entries = parseShortStatus(await getShortStatus(cwd))
346
- const counts: GitStatusCounts = {
347
- staged: 0,
348
- unstaged: 0,
349
- untracked: 0,
350
- total: entries.length,
351
- }
352
-
353
- for (const entry of entries) {
354
- if (entry.index !== ' ' && entry.index !== '?') {
355
- counts.staged += 1
356
- }
357
-
358
- if (entry.worktree !== ' ' && entry.worktree !== '?') {
359
- counts.unstaged += 1
360
- }
361
-
362
- if (entry.index === '?' || entry.worktree === '?') {
363
- counts.untracked += 1
364
- }
365
- }
366
-
367
- return counts
368
- }
369
-
370
- export async function getStagedDiff(cwd: string): Promise<string> {
371
- const result = await runGitCommand(['diff', '--cached'], { cwd })
372
- return result.stdout
373
- }
374
-
375
- export async function getDiffAgainstHead(cwd: string): Promise<string> {
376
- const result = await runGitCommand(['diff', 'HEAD', '--'], { cwd })
377
- return result.stdout
378
- }
379
-
380
- export async function getRecentLog(cwd: string, count = 10): Promise<string> {
381
- const result = await runGitCommand(['log', '--oneline', `-${count}`], { cwd, allowFailure: true })
382
- return result.stdout
383
- }
384
-
385
- export async function getStagedFiles(cwd: string): Promise<string[]> {
386
- const result = await runGitCommand(['diff', '--cached', '--name-only', '--diff-filter=ACMRTUXB'], { cwd })
387
- return result.stdout
388
- .split(/\r?\n/)
389
- .map(line => line.trim())
390
- .filter(Boolean)
391
- }
392
-
393
- export async function getChangedFilesAgainstHead(cwd: string): Promise<string[]> {
394
- const result = await runGitCommand(['diff', '--name-only', 'HEAD', '--diff-filter=ACMRTUXB'], { cwd })
395
- return result.stdout
396
- .split(/\r?\n/)
397
- .map(line => line.trim())
398
- .filter(Boolean)
399
- }
400
-
401
- export async function getDiffStatAgainstHead(cwd: string): Promise<string> {
402
- const result = await runGitCommand(['diff', '--stat', 'HEAD'], { cwd, allowFailure: true })
403
- return result.stdout.trim()
404
- }
405
-
406
- export function buildFallbackCommitMessage(files: string[]): string {
407
- if (files.length === 0) {
408
- return 'Update repository files'
409
- }
410
-
411
- if (files.length === 1) {
412
- return `Update ${basename(files[0]!) || files[0]}`
413
- }
414
-
415
- return `Update ${basename(files[0]!) || files[0]} and ${files.length - 1} other files`
416
- }
417
-
418
- export async function commitWithMessage(cwd: string, message: string): Promise<void> {
419
- await runGitCommand(['commit', '-F', '-'], {
420
- cwd,
421
- input: message,
422
- })
423
- }
424
-
425
- export async function pushBranch(cwd: string, branch: string, setUpstream: boolean): Promise<void> {
426
- const args = setUpstream
427
- ? ['push', '--set-upstream', 'origin', branch]
428
- : ['push']
429
- const requireConfiguredSsh = setUpstream
430
- ? await remoteRequiresConfiguredSsh(cwd, 'origin')
431
- : await upstreamRequiresConfiguredSsh(cwd)
432
-
433
- await runGitCommand(args, {
434
- cwd,
435
- requireConfiguredSsh,
436
- })
437
- }
438
-
439
- export async function getHeadShortSha(cwd: string): Promise<string> {
440
- const result = await runGitCommand(['rev-parse', '--short', 'HEAD'], { cwd })
441
- return result.stdout.trim()
442
- }
443
-
444
- export async function getHeadCommitHash(cwd: string): Promise<string> {
445
- const result = await runGitCommand(['rev-parse', 'HEAD'], { cwd })
446
- return result.stdout.trim()
447
- }
448
-
449
- export async function getHeadCommitMessage(cwd: string): Promise<string> {
450
- const result = await runGitCommand(['log', '-1', '--pretty=%B'], { cwd })
451
- return result.stdout.trim()
452
- }
453
-
454
- export async function getHeadShortMessage(cwd: string): Promise<string> {
455
- const result = await runGitCommand(['log', '-1', '--pretty=%s'], { cwd })
456
- return result.stdout.trim()
457
- }
package/src/ink/bark.tsx DELETED
@@ -1,241 +0,0 @@
1
- /** @jsxImportSource react */
2
- import { existsSync, mkdtempSync, rmSync } from 'node:fs'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
- import React, { useEffect, useMemo, useState } from 'react'
6
- import { Text } from 'ink'
7
- import barkAudioAssetPath from './assets/160092__jorickhoofd__dog-bark-1.mp3' with { type: 'file' }
8
- import { getBootstrapPixelRows } from '../ui.ts'
9
- import { AppFrame } from './components.tsx'
10
- import {
11
- renderCommandScreen,
12
- type CommandScreenController,
13
- } from './runtime.tsx'
14
-
15
- export const BARK_FRAME_DURATION_MS = 500
16
- export const BARK_FRAME_COUNT = 2
17
- export const BARK_ANIMATION_DURATION_MS = BARK_FRAME_DURATION_MS * BARK_FRAME_COUNT
18
-
19
- export const BARKING_PIXEL_ROWS = [
20
- '3222222223344553222222233222',
21
- '2223332224557888853233333222',
22
- '2233433336688889a98534433222',
23
- '2333433368877688aa9853433233',
24
- '3333333499754477a89754333234',
25
- '3433333685333455579755433344',
26
- '3333324654136764238545643344',
27
- '3333316443063136716542653334',
28
- '3334214434671124964531554333',
29
- '3334123235864246995431345443',
30
- '3343112157743224786521335543',
31
- '3452112245524023687520333543',
32
- '3452111244424006276630323244',
33
- '3342111354221000135420222344',
34
- '3343143444411000036421223544',
35
- '3444244511411223136322434544',
36
- '4444444400221526155313245554',
37
- '4443343421133546253553135555',
38
- '44433344111218b9362991014555',
39
- '3444333214113111627b41101355',
40
- '333332111652233339c911111134',
41
- '23342111169722236cb411111113',
42
- '3333111117a51248ccb211111113',
43
- '3333111129931117cc5111111113',
44
- '333211112a9a2278971111111112',
45
- '2231111119b912ab721111111102',
46
- '2231111116b51299321111101101',
47
- '2221111112721176111111101001',
48
- ] as const
49
-
50
-
51
- const BARK_FRAME_ROWS = [
52
- [...getBootstrapPixelRows()],
53
- [...BARKING_PIXEL_ROWS],
54
- ] as const
55
-
56
- export interface BarkAudioWriteResult {
57
- directory: string
58
- filePath: string
59
- }
60
-
61
- export interface BarkAudioPlaybackResult {
62
- attempted: boolean
63
- }
64
-
65
- export interface BarkAudioPlaybackOptions {
66
- silentBark?: boolean
67
- env?: Record<string, string | undefined>
68
- platform?: string
69
- afplayPath?: string
70
- audioPlayerExists?: (path: string) => boolean
71
- writeAudioFile?: () => BarkAudioWriteResult | Promise<BarkAudioWriteResult>
72
- spawnAudio?: (command: string[]) => { exited?: Promise<unknown> } | void
73
- }
74
-
75
- export interface BarkIntroSessionOptions {
76
- commandTitle: string
77
- enabled: boolean
78
- silentBark?: boolean
79
- env?: Record<string, string | undefined>
80
- platform?: string
81
- afplayPath?: string
82
- renderIntro?: (
83
- build: (controller: CommandScreenController<void>) => React.ReactElement,
84
- ) => Promise<void>
85
- playAudio?: (options: BarkAudioPlaybackOptions) => Promise<BarkAudioPlaybackResult>
86
- }
87
-
88
- export interface BarkIntroSession {
89
- playOnce(): Promise<void>
90
- }
91
-
92
- export function getBarkFrameRows(): string[][] {
93
- return BARK_FRAME_ROWS.map(frame => [...frame])
94
- }
95
-
96
- export function getBarkAnimationFrameIndex(elapsedMs: number): number {
97
- const clamped = Math.max(0, Math.min(elapsedMs, BARK_ANIMATION_DURATION_MS - 1))
98
- return Math.floor(clamped / BARK_FRAME_DURATION_MS)
99
- }
100
-
101
- export function shouldShowBarkIntro(options: {
102
- json?: boolean
103
- stdinIsTTY?: boolean
104
- stdoutIsTTY?: boolean
105
- }): boolean {
106
- return !options.json && Boolean(options.stdinIsTTY && options.stdoutIsTTY)
107
- }
108
-
109
- export function shouldPlayBarkAudio(options: {
110
- silentBark?: boolean
111
- env?: Record<string, string | undefined>
112
- platform?: string
113
- afplayPath?: string
114
- audioPlayerExists?: (path: string) => boolean
115
- }): boolean {
116
- if (options.silentBark || options.env?.TELA_SILENT_BARK === '1') {
117
- return false
118
- }
119
-
120
- if ((options.platform ?? process.platform) !== 'darwin') {
121
- return false
122
- }
123
-
124
- const afplayPath = options.afplayPath ?? '/usr/bin/afplay'
125
- const audioPlayerExists = options.audioPlayerExists ?? existsSync
126
- return audioPlayerExists(afplayPath)
127
- }
128
-
129
- export async function writeBarkAudioFile(baseDirectory = tmpdir()): Promise<BarkAudioWriteResult> {
130
- const directory = mkdtempSync(join(baseDirectory, 'tela-bark-'))
131
- const filePath = join(directory, 'bark.mp3')
132
- await Bun.write(filePath, Bun.file(barkAudioAssetPath))
133
- return { directory, filePath }
134
- }
135
-
136
- function spawnAudioCommand(command: string[]): { exited?: Promise<unknown> } {
137
- const proc = Bun.spawn(command, {
138
- stdin: 'ignore',
139
- stdout: 'ignore',
140
- stderr: 'ignore',
141
- })
142
-
143
- return { exited: proc.exited }
144
- }
145
-
146
- export async function playBarkAudio(options: BarkAudioPlaybackOptions = {}): Promise<BarkAudioPlaybackResult> {
147
- if (!shouldPlayBarkAudio(options)) {
148
- return { attempted: false }
149
- }
150
-
151
- const writeAudioFile = options.writeAudioFile ?? writeBarkAudioFile
152
- const spawnAudio = options.spawnAudio ?? spawnAudioCommand
153
- const afplayPath = options.afplayPath ?? '/usr/bin/afplay'
154
- let audioFile: BarkAudioWriteResult | undefined
155
-
156
- try {
157
- audioFile = await writeAudioFile()
158
- const writtenAudioFile = audioFile
159
- const result = spawnAudio([afplayPath, writtenAudioFile.filePath])
160
- void Promise.resolve(result?.exited)
161
- .catch(() => {})
162
- .finally(() => {
163
- rmSync(writtenAudioFile.directory, { recursive: true, force: true })
164
- })
165
- return { attempted: true }
166
- }
167
- catch {
168
- if (audioFile) {
169
- rmSync(audioFile.directory, { recursive: true, force: true })
170
- }
171
- return { attempted: false }
172
- }
173
- }
174
-
175
- export function createInkBarkSession(options: BarkIntroSessionOptions): BarkIntroSession {
176
- let played = false
177
-
178
- return {
179
- async playOnce(): Promise<void> {
180
- if (played || !options.enabled) {
181
- return
182
- }
183
-
184
- played = true
185
-
186
- const playAudio = options.playAudio ?? playBarkAudio
187
- void playAudio({
188
- silentBark: options.silentBark,
189
- env: options.env,
190
- platform: options.platform,
191
- afplayPath: options.afplayPath,
192
- }).catch(() => {})
193
-
194
- const renderIntro = options.renderIntro ?? (async (build) => {
195
- await renderCommandScreen<void>(build, { clearOnExit: true })
196
- })
197
-
198
- await renderIntro((controller) => React.createElement(BarkIntroScreen, {
199
- controller,
200
- title: options.commandTitle,
201
- }))
202
- },
203
- }
204
- }
205
-
206
- export function BarkIntroScreen(props: {
207
- controller: CommandScreenController<void>
208
- title: string
209
- }): React.ReactElement {
210
- const frames = useMemo(() => getBarkFrameRows(), [])
211
- const [frameIndex, setFrameIndex] = useState(0)
212
-
213
- useEffect(() => {
214
- const interval = setInterval(() => {
215
- setFrameIndex((current) => {
216
- if (current >= frames.length - 1) {
217
- clearInterval(interval)
218
- props.controller.resolve(undefined, 20)
219
- return current
220
- }
221
-
222
- return current + 1
223
- })
224
- }, BARK_FRAME_DURATION_MS)
225
-
226
- return () => {
227
- clearInterval(interval)
228
- }
229
- }, [frames.length, props.controller])
230
-
231
- return (
232
- <AppFrame
233
- title={props.title}
234
- subtitle="Starting interactive command"
235
- status="Bark intro"
236
- mascotRows={frames[frameIndex]}
237
- >
238
- <Text dimColor>Preparing the workflow…</Text>
239
- </AppFrame>
240
- )
241
- }