@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.
- 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 -241
- package/src/ink/components.tsx +0 -178
- package/src/ink/runtime.tsx +0 -90
- package/src/ink/screens/feedback-screen.tsx +0 -395
- package/src/ink/screens/push-screen.tsx +0 -232
- package/src/ink/screens/run-screen.tsx +0 -292
- package/src/ink/screens/setup/breadcrumb.tsx +0 -24
- package/src/ink/screens/setup/setup-screen.tsx +0 -612
- package/src/ink/screens/setup/step-frame.tsx +0 -31
- package/src/ink/screens/setup/steps/done-step.tsx +0 -29
- package/src/ink/screens/setup/steps/environment-step.tsx +0 -23
- package/src/ink/screens/setup/steps/organization-step.tsx +0 -35
- package/src/ink/screens/setup/steps/review-step.tsx +0 -78
- package/src/ink/screens/setup/steps/running-step.tsx +0 -37
- package/src/ink/screens/setup/steps/ssh-key-step.tsx +0 -48
- package/src/ink/screens/setup/steps/text-field-step.tsx +0 -18
- 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 -283
- 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/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
|
-
}
|
|
Binary file
|
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
|
-
}
|