@meistrari/tela-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/cli.ts +25 -0
- package/package.json +30 -0
- package/src/agent-api.ts +124 -0
- package/src/auth.ts +16 -0
- package/src/commands/doctor.ts +35 -0
- package/src/commands/feedback.ts +701 -0
- package/src/commands/metadata.ts +208 -0
- package/src/commands/push.ts +208 -0
- package/src/commands/run.ts +1601 -0
- package/src/commands/status.ts +87 -0
- package/src/commands/sync.ts +245 -0
- package/src/commit-message.ts +69 -0
- package/src/config.ts +384 -0
- package/src/diagnostics.ts +578 -0
- package/src/errors.ts +122 -0
- package/src/git.ts +250 -0
- package/src/ink/bark.tsx +288 -0
- package/src/ink/components.tsx +175 -0
- package/src/ink/runtime.tsx +89 -0
- package/src/ink/screens/feedback-screen.tsx +394 -0
- package/src/ink/screens/push-screen.tsx +231 -0
- package/src/ink/screens/run-screen.tsx +291 -0
- package/src/ink/screens/setup-screen.tsx +352 -0
- package/src/ink/screens/sync-screen.tsx +282 -0
- package/src/process.ts +251 -0
- package/src/program.ts +1872 -0
- package/src/runtime.ts +147 -0
- package/src/types.ts +80 -0
- package/src/ui.ts +297 -0
package/src/git.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { basename } from 'node:path'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { CommandError, runCommand, runShell } from './process.ts'
|
|
4
|
+
|
|
5
|
+
export interface GitStatusEntry {
|
|
6
|
+
index: string
|
|
7
|
+
worktree: string
|
|
8
|
+
path: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GitStatusCounts {
|
|
12
|
+
staged: number
|
|
13
|
+
unstaged: number
|
|
14
|
+
untracked: number
|
|
15
|
+
total: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function findRepositoryRoot(cwd: string): Promise<string | null> {
|
|
19
|
+
const result = await runCommand(['git', 'rev-parse', '--show-toplevel'], { cwd, allowFailure: true })
|
|
20
|
+
if (result.exitCode !== 0) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return result.stdout.trim() || null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getRepositoryRoot(cwd: string): Promise<string> {
|
|
28
|
+
const repoRoot = await findRepositoryRoot(cwd)
|
|
29
|
+
if (!repoRoot) {
|
|
30
|
+
throw new Error(`Not a git repository: ${cwd}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return repoRoot
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function assertGitRepository(cwd: string): Promise<void> {
|
|
37
|
+
try {
|
|
38
|
+
await runCommand(['git', 'rev-parse', '--is-inside-work-tree'], { cwd })
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof CommandError) {
|
|
42
|
+
throw new Error(`Not a git repository: ${cwd}`)
|
|
43
|
+
}
|
|
44
|
+
throw error
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function cloneRepository(remoteUrl: string, targetDir: string): Promise<void> {
|
|
49
|
+
await runCommand(['git', 'clone', remoteUrl, targetDir])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function getOriginUrl(cwd: string): Promise<string> {
|
|
53
|
+
const result = await runCommand(['git', 'remote', 'get-url', 'origin'], { cwd })
|
|
54
|
+
return result.stdout.trim()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function fetchOrigin(cwd: string): Promise<void> {
|
|
58
|
+
await runCommand(['git', 'fetch', 'origin', '--prune'], { cwd })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function fetchAllRemotes(cwd: string): Promise<void> {
|
|
62
|
+
await runCommand(['git', 'fetch', '--all', '--prune'], { cwd })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function getWorktreeStatus(cwd: string): Promise<string> {
|
|
66
|
+
const result = await runCommand(['git', 'status', '--short'], { cwd })
|
|
67
|
+
return result.stdout.trim()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function isWorktreeDirty(cwd: string): Promise<boolean> {
|
|
71
|
+
return Boolean(await getWorktreeStatus(cwd))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function stageAll(cwd: string): Promise<void> {
|
|
75
|
+
await runCommand(['git', 'add', '-A'], { cwd })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function getCurrentBranch(cwd: string): Promise<string> {
|
|
79
|
+
try {
|
|
80
|
+
const result = await runCommand(['git', 'symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd })
|
|
81
|
+
return result.stdout.trim()
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof CommandError) {
|
|
85
|
+
throw new Error('Detached HEAD; check out a branch before syncing or pushing.')
|
|
86
|
+
}
|
|
87
|
+
throw error
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function getUpstreamBranch(cwd: string): Promise<string | null> {
|
|
92
|
+
const result = await runCommand(
|
|
93
|
+
['git', 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
|
|
94
|
+
{ cwd, allowFailure: true },
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if (result.exitCode !== 0) {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result.stdout.trim() || null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function pullCurrentBranch(cwd: string): Promise<void> {
|
|
105
|
+
await runCommand(['git', 'pull', '--ff-only'], { cwd })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function pullDefaultBranch(cwd: string, defaultBranch: string): Promise<void> {
|
|
109
|
+
await runCommand(['git', 'pull', '--ff-only', 'origin', defaultBranch], { cwd })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function getShortStatus(cwd: string): Promise<string> {
|
|
113
|
+
const result = await runCommand(['git', 'status', '--short'], { cwd })
|
|
114
|
+
return result.stdout
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function parseShortStatus(output: string): GitStatusEntry[] {
|
|
118
|
+
return output
|
|
119
|
+
.split(/\r?\n/)
|
|
120
|
+
.map(line => line.trimEnd())
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.map((line) => {
|
|
123
|
+
const normalized = line.padEnd(3, ' ')
|
|
124
|
+
return {
|
|
125
|
+
index: normalized[0] ?? ' ',
|
|
126
|
+
worktree: normalized[1] ?? ' ',
|
|
127
|
+
path: normalized.slice(3).trim(),
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function getStatusCounts(cwd: string): Promise<GitStatusCounts> {
|
|
133
|
+
const entries = parseShortStatus(await getShortStatus(cwd))
|
|
134
|
+
const counts: GitStatusCounts = {
|
|
135
|
+
staged: 0,
|
|
136
|
+
unstaged: 0,
|
|
137
|
+
untracked: 0,
|
|
138
|
+
total: entries.length,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
if (entry.index !== ' ' && entry.index !== '?') {
|
|
143
|
+
counts.staged += 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (entry.worktree !== ' ' && entry.worktree !== '?') {
|
|
147
|
+
counts.unstaged += 1
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (entry.index === '?' || entry.worktree === '?') {
|
|
151
|
+
counts.untracked += 1
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return counts
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function getStagedDiff(cwd: string): Promise<string> {
|
|
159
|
+
const result = await runCommand(['git', 'diff', '--cached'], { cwd })
|
|
160
|
+
return result.stdout
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function getDiffAgainstHead(cwd: string): Promise<string> {
|
|
164
|
+
const result = await runCommand(['git', 'diff', 'HEAD', '--'], { cwd })
|
|
165
|
+
return result.stdout
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function getRecentLog(cwd: string, count = 10): Promise<string> {
|
|
169
|
+
const result = await runCommand(['git', 'log', '--oneline', `-${count}`], { cwd, allowFailure: true })
|
|
170
|
+
return result.stdout
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function getStagedFiles(cwd: string): Promise<string[]> {
|
|
174
|
+
const result = await runCommand(['git', 'diff', '--cached', '--name-only', '--diff-filter=ACMRTUXB'], { cwd })
|
|
175
|
+
return result.stdout
|
|
176
|
+
.split(/\r?\n/)
|
|
177
|
+
.map(line => line.trim())
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function getChangedFilesAgainstHead(cwd: string): Promise<string[]> {
|
|
182
|
+
const result = await runCommand(['git', 'diff', '--name-only', 'HEAD', '--diff-filter=ACMRTUXB'], { cwd })
|
|
183
|
+
return result.stdout
|
|
184
|
+
.split(/\r?\n/)
|
|
185
|
+
.map(line => line.trim())
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function getDiffStatAgainstHead(cwd: string): Promise<string> {
|
|
190
|
+
const result = await runCommand(['git', 'diff', '--stat', 'HEAD'], { cwd, allowFailure: true })
|
|
191
|
+
return result.stdout.trim()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function buildFallbackCommitMessage(files: string[]): string {
|
|
195
|
+
if (files.length === 0) {
|
|
196
|
+
return 'Update repository files'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (files.length === 1) {
|
|
200
|
+
return `Update ${basename(files[0]!) || files[0]}`
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return `Update ${basename(files[0]!) || files[0]} and ${files.length - 1} other files`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildCommitDelimiter(message: string): string {
|
|
207
|
+
let delimiter = `__TELA_COMMIT_${randomUUID().replace(/-/g, '_')}__`
|
|
208
|
+
while (message.includes(delimiter)) {
|
|
209
|
+
delimiter = `__TELA_COMMIT_${randomUUID().replace(/-/g, '_')}__`
|
|
210
|
+
}
|
|
211
|
+
return delimiter
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function commitWithMessage(cwd: string, message: string): Promise<void> {
|
|
215
|
+
const delimiter = buildCommitDelimiter(message)
|
|
216
|
+
const script = [
|
|
217
|
+
'git commit -F - <<\'__TELA_COMMIT__\'',
|
|
218
|
+
message,
|
|
219
|
+
'__TELA_COMMIT__',
|
|
220
|
+
].join('\n').replaceAll('__TELA_COMMIT__', delimiter)
|
|
221
|
+
|
|
222
|
+
await runShell(script, { cwd })
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function pushBranch(cwd: string, branch: string, setUpstream: boolean): Promise<void> {
|
|
226
|
+
const args = setUpstream
|
|
227
|
+
? ['git', 'push', '--set-upstream', 'origin', branch]
|
|
228
|
+
: ['git', 'push']
|
|
229
|
+
await runCommand(args, { cwd })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function getHeadShortSha(cwd: string): Promise<string> {
|
|
233
|
+
const result = await runCommand(['git', 'rev-parse', '--short', 'HEAD'], { cwd })
|
|
234
|
+
return result.stdout.trim()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function getHeadCommitHash(cwd: string): Promise<string> {
|
|
238
|
+
const result = await runCommand(['git', 'rev-parse', 'HEAD'], { cwd })
|
|
239
|
+
return result.stdout.trim()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function getHeadCommitMessage(cwd: string): Promise<string> {
|
|
243
|
+
const result = await runCommand(['git', 'log', '-1', '--pretty=%B'], { cwd })
|
|
244
|
+
return result.stdout.trim()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function getHeadShortMessage(cwd: string): Promise<string> {
|
|
248
|
+
const result = await runCommand(['git', 'log', '-1', '--pretty=%s'], { cwd })
|
|
249
|
+
return result.stdout.trim()
|
|
250
|
+
}
|
package/src/ink/bark.tsx
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import React, { useEffect, useMemo, useState } from 'react'
|
|
5
|
+
import { Text } from 'ink'
|
|
6
|
+
import { getBootstrapPixelRows } from '../ui.ts'
|
|
7
|
+
import { AppFrame } from './components.tsx'
|
|
8
|
+
import {
|
|
9
|
+
renderCommandScreen,
|
|
10
|
+
type CommandScreenController,
|
|
11
|
+
} from './runtime.tsx'
|
|
12
|
+
|
|
13
|
+
export const BARK_FRAME_DURATION_MS = 500
|
|
14
|
+
export const BARK_FRAME_COUNT = 2
|
|
15
|
+
export const BARK_ANIMATION_DURATION_MS = BARK_FRAME_DURATION_MS * BARK_FRAME_COUNT
|
|
16
|
+
|
|
17
|
+
export const BARKING_PIXEL_ROWS = [
|
|
18
|
+
'3222222223344553222222233222',
|
|
19
|
+
'2223332224557888853233333222',
|
|
20
|
+
'2233433336688889a98534433222',
|
|
21
|
+
'2333433368877688aa9853433233',
|
|
22
|
+
'3333333499754477a89754333234',
|
|
23
|
+
'3433333685333455579755433344',
|
|
24
|
+
'3333324654136764238545643344',
|
|
25
|
+
'3333316443063136716542653334',
|
|
26
|
+
'3334214434671124964531554333',
|
|
27
|
+
'3334123235864246995431345443',
|
|
28
|
+
'3343112157743224786521335543',
|
|
29
|
+
'3452112245524023687520333543',
|
|
30
|
+
'3452111244424006276630323244',
|
|
31
|
+
'3342111354221000135420222344',
|
|
32
|
+
'3343143444411000036421223544',
|
|
33
|
+
'3444244511411223136322434544',
|
|
34
|
+
'4444444400221526155313245554',
|
|
35
|
+
'4443343421133546253553135555',
|
|
36
|
+
'44433344111218b9362991014555',
|
|
37
|
+
'3444333214113111627b41101355',
|
|
38
|
+
'333332111652233339c911111134',
|
|
39
|
+
'23342111169722236cb411111113',
|
|
40
|
+
'3333111117a51248ccb211111113',
|
|
41
|
+
'3333111129931117cc5111111113',
|
|
42
|
+
'333211112a9a2278971111111112',
|
|
43
|
+
'2231111119b912ab721111111102',
|
|
44
|
+
'2231111116b51299321111101101',
|
|
45
|
+
'2221111112721176111111101001',
|
|
46
|
+
] as const
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
const BARK_FRAME_ROWS = [
|
|
50
|
+
[...getBootstrapPixelRows()],
|
|
51
|
+
[...BARKING_PIXEL_ROWS],
|
|
52
|
+
] as const
|
|
53
|
+
|
|
54
|
+
function pseudoNoise(seed: number): number {
|
|
55
|
+
const raw = Math.sin((seed + 1) * 12.9898) * 43758.5453
|
|
56
|
+
return (raw - Math.floor(raw)) * 2 - 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface BarkAudioWriteResult {
|
|
60
|
+
directory: string
|
|
61
|
+
filePath: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BarkAudioPlaybackResult {
|
|
65
|
+
attempted: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BarkAudioPlaybackOptions {
|
|
69
|
+
silentBark?: boolean
|
|
70
|
+
env?: Record<string, string | undefined>
|
|
71
|
+
platform?: string
|
|
72
|
+
afplayPath?: string
|
|
73
|
+
audioPlayerExists?: (path: string) => boolean
|
|
74
|
+
writeAudioFile?: () => BarkAudioWriteResult
|
|
75
|
+
spawnAudio?: (command: string[]) => { exited?: Promise<unknown> } | void
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface BarkIntroSessionOptions {
|
|
79
|
+
commandTitle: string
|
|
80
|
+
enabled: boolean
|
|
81
|
+
silentBark?: boolean
|
|
82
|
+
env?: Record<string, string | undefined>
|
|
83
|
+
platform?: string
|
|
84
|
+
afplayPath?: string
|
|
85
|
+
renderIntro?: (
|
|
86
|
+
build: (controller: CommandScreenController<void>) => React.ReactElement,
|
|
87
|
+
) => Promise<void>
|
|
88
|
+
playAudio?: (options: BarkAudioPlaybackOptions) => Promise<BarkAudioPlaybackResult>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface BarkIntroSession {
|
|
92
|
+
playOnce(): Promise<void>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getBarkFrameRows(): string[][] {
|
|
96
|
+
return BARK_FRAME_ROWS.map(frame => [...frame])
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getBarkAnimationFrameIndex(elapsedMs: number): number {
|
|
100
|
+
const clamped = Math.max(0, Math.min(elapsedMs, BARK_ANIMATION_DURATION_MS - 1))
|
|
101
|
+
return Math.floor(clamped / BARK_FRAME_DURATION_MS)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function shouldShowBarkIntro(options: {
|
|
105
|
+
json?: boolean
|
|
106
|
+
stdinIsTTY?: boolean
|
|
107
|
+
stdoutIsTTY?: boolean
|
|
108
|
+
}): boolean {
|
|
109
|
+
return !options.json && Boolean(options.stdinIsTTY && options.stdoutIsTTY)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function shouldPlayBarkAudio(options: {
|
|
113
|
+
silentBark?: boolean
|
|
114
|
+
env?: Record<string, string | undefined>
|
|
115
|
+
platform?: string
|
|
116
|
+
afplayPath?: string
|
|
117
|
+
audioPlayerExists?: (path: string) => boolean
|
|
118
|
+
}): boolean {
|
|
119
|
+
if (options.silentBark || options.env?.TELA_SILENT_BARK === '1') {
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if ((options.platform ?? process.platform) !== 'darwin') {
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const afplayPath = options.afplayPath ?? '/usr/bin/afplay'
|
|
128
|
+
const audioPlayerExists = options.audioPlayerExists ?? existsSync
|
|
129
|
+
return audioPlayerExists(afplayPath)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function buildBarkWavBuffer(): Uint8Array {
|
|
133
|
+
const sampleRate = 22_050
|
|
134
|
+
const durationSeconds = 0.18
|
|
135
|
+
const sampleCount = Math.floor(sampleRate * durationSeconds)
|
|
136
|
+
const pcm = new Int16Array(sampleCount)
|
|
137
|
+
|
|
138
|
+
for (let index = 0; index < sampleCount; index += 1) {
|
|
139
|
+
const t = index / sampleRate
|
|
140
|
+
const attack = Math.min(1, t * 40)
|
|
141
|
+
const decay = Math.exp(-15 * t)
|
|
142
|
+
const envelope = attack * decay
|
|
143
|
+
const pitch = 760 - (420 * t)
|
|
144
|
+
const body = Math.sin((2 * Math.PI * pitch) * t) * 0.58
|
|
145
|
+
const growl = Math.sin((2 * Math.PI * (pitch * 0.47)) * t + 0.8) * 0.22
|
|
146
|
+
const snap = pseudoNoise(index) * (t < 0.035 ? 0.28 : 0.08)
|
|
147
|
+
const sample = Math.max(-1, Math.min(1, (body + growl + snap) * envelope))
|
|
148
|
+
pcm[index] = Math.round(sample * 0x7fff)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const dataSize = pcm.byteLength
|
|
152
|
+
const buffer = new ArrayBuffer(44 + dataSize)
|
|
153
|
+
const view = new DataView(buffer)
|
|
154
|
+
const bytes = new Uint8Array(buffer)
|
|
155
|
+
|
|
156
|
+
const writeString = (offset: number, value: string) => {
|
|
157
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
158
|
+
view.setUint8(offset + index, value.charCodeAt(index))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
writeString(0, 'RIFF')
|
|
163
|
+
view.setUint32(4, 36 + dataSize, true)
|
|
164
|
+
writeString(8, 'WAVE')
|
|
165
|
+
writeString(12, 'fmt ')
|
|
166
|
+
view.setUint32(16, 16, true)
|
|
167
|
+
view.setUint16(20, 1, true)
|
|
168
|
+
view.setUint16(22, 1, true)
|
|
169
|
+
view.setUint32(24, sampleRate, true)
|
|
170
|
+
view.setUint32(28, sampleRate * 2, true)
|
|
171
|
+
view.setUint16(32, 2, true)
|
|
172
|
+
view.setUint16(34, 16, true)
|
|
173
|
+
writeString(36, 'data')
|
|
174
|
+
view.setUint32(40, dataSize, true)
|
|
175
|
+
bytes.set(new Uint8Array(pcm.buffer), 44)
|
|
176
|
+
|
|
177
|
+
return bytes
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function writeBarkAudioFile(baseDirectory = tmpdir()): BarkAudioWriteResult {
|
|
181
|
+
const directory = mkdtempSync(join(baseDirectory, 'tela-bark-'))
|
|
182
|
+
const filePath = join(directory, 'bark.wav')
|
|
183
|
+
writeFileSync(filePath, buildBarkWavBuffer())
|
|
184
|
+
return { directory, filePath }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function spawnAudioCommand(command: string[]): { exited?: Promise<unknown> } {
|
|
188
|
+
const proc = Bun.spawn(command, {
|
|
189
|
+
stdin: 'ignore',
|
|
190
|
+
stdout: 'ignore',
|
|
191
|
+
stderr: 'ignore',
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return { exited: proc.exited }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function playBarkAudio(options: BarkAudioPlaybackOptions = {}): Promise<BarkAudioPlaybackResult> {
|
|
198
|
+
if (!shouldPlayBarkAudio(options)) {
|
|
199
|
+
return { attempted: false }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const writeAudioFile = options.writeAudioFile ?? writeBarkAudioFile
|
|
203
|
+
const spawnAudio = options.spawnAudio ?? spawnAudioCommand
|
|
204
|
+
const afplayPath = options.afplayPath ?? '/usr/bin/afplay'
|
|
205
|
+
const audioFile = writeAudioFile()
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const result = spawnAudio([afplayPath, audioFile.filePath])
|
|
209
|
+
void Promise.resolve(result?.exited)
|
|
210
|
+
.catch(() => {})
|
|
211
|
+
.finally(() => {
|
|
212
|
+
rmSync(audioFile.directory, { recursive: true, force: true })
|
|
213
|
+
})
|
|
214
|
+
return { attempted: true }
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
rmSync(audioFile.directory, { recursive: true, force: true })
|
|
218
|
+
return { attempted: false }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function createInkBarkSession(options: BarkIntroSessionOptions): BarkIntroSession {
|
|
223
|
+
let played = false
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
async playOnce(): Promise<void> {
|
|
227
|
+
if (played || !options.enabled) {
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
played = true
|
|
232
|
+
|
|
233
|
+
const playAudio = options.playAudio ?? playBarkAudio
|
|
234
|
+
void playAudio({
|
|
235
|
+
silentBark: options.silentBark,
|
|
236
|
+
env: options.env,
|
|
237
|
+
platform: options.platform,
|
|
238
|
+
afplayPath: options.afplayPath,
|
|
239
|
+
}).catch(() => {})
|
|
240
|
+
|
|
241
|
+
const renderIntro = options.renderIntro ?? (async (build) => {
|
|
242
|
+
await renderCommandScreen<void>(build, { clearOnExit: true })
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
await renderIntro((controller) => React.createElement(BarkIntroScreen, {
|
|
246
|
+
controller,
|
|
247
|
+
title: options.commandTitle,
|
|
248
|
+
}))
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function BarkIntroScreen(props: {
|
|
254
|
+
controller: CommandScreenController<void>
|
|
255
|
+
title: string
|
|
256
|
+
}): React.ReactElement {
|
|
257
|
+
const frames = useMemo(() => getBarkFrameRows(), [])
|
|
258
|
+
const [frameIndex, setFrameIndex] = useState(0)
|
|
259
|
+
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
const interval = setInterval(() => {
|
|
262
|
+
setFrameIndex((current) => {
|
|
263
|
+
if (current >= frames.length - 1) {
|
|
264
|
+
clearInterval(interval)
|
|
265
|
+
props.controller.resolve(undefined, 20)
|
|
266
|
+
return current
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return current + 1
|
|
270
|
+
})
|
|
271
|
+
}, BARK_FRAME_DURATION_MS)
|
|
272
|
+
|
|
273
|
+
return () => {
|
|
274
|
+
clearInterval(interval)
|
|
275
|
+
}
|
|
276
|
+
}, [frames.length, props.controller])
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<AppFrame
|
|
280
|
+
title={props.title}
|
|
281
|
+
subtitle="Starting interactive command"
|
|
282
|
+
status="Bark intro"
|
|
283
|
+
mascotRows={frames[frameIndex]}
|
|
284
|
+
>
|
|
285
|
+
<Text dimColor>Preparing the workflow…</Text>
|
|
286
|
+
</AppFrame>
|
|
287
|
+
)
|
|
288
|
+
}
|