@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/src/runtime.ts ADDED
@@ -0,0 +1,147 @@
1
+ import { createUserFacingError, toUserFacingError, type UserFacingError } from './errors.ts'
2
+
3
+ export interface RuntimePromptModule {
4
+ note?: (message: string, title?: string) => void
5
+ intro?: (message: string) => void
6
+ outro?: (message: string) => void
7
+ cancel?: (message: string) => void
8
+ log?: {
9
+ success?: (message: string) => void
10
+ error?: (message: string) => void
11
+ }
12
+ }
13
+
14
+ export interface RuntimeOptions {
15
+ json?: boolean
16
+ verbose?: boolean
17
+ decorate?: boolean
18
+ prompts?: RuntimePromptModule
19
+ writeStdout?: (message: string) => void
20
+ writeStderr?: (message: string) => void
21
+ }
22
+
23
+ export interface StepRecord {
24
+ label: string
25
+ status: 'success' | 'failed'
26
+ detail?: string
27
+ }
28
+
29
+ export class CommandRuntime {
30
+ public readonly json: boolean
31
+ public readonly verbose: boolean
32
+ public readonly decorate: boolean
33
+ public readonly prompts?: RuntimePromptModule
34
+ private readonly writeStdout?: (message: string) => void
35
+ private readonly writeStderr?: (message: string) => void
36
+ private readonly steps: StepRecord[] = []
37
+
38
+ constructor(options: RuntimeOptions = {}) {
39
+ this.json = options.json ?? false
40
+ this.verbose = options.verbose ?? false
41
+ this.decorate = options.decorate ?? (!this.json && Boolean(process.stdout.isTTY))
42
+ this.prompts = options.prompts
43
+ this.writeStdout = options.writeStdout
44
+ this.writeStderr = options.writeStderr
45
+ }
46
+
47
+ emit(message: string): void {
48
+ if (this.json) {
49
+ return
50
+ }
51
+
52
+ if (this.writeStdout) {
53
+ this.writeStdout(message)
54
+ return
55
+ }
56
+
57
+ if (this.prompts?.note) {
58
+ this.prompts.note(message)
59
+ return
60
+ }
61
+
62
+ console.log(message)
63
+ }
64
+
65
+ emitJson(payload: unknown): void {
66
+ const message = `${JSON.stringify(payload, null, 2)}\n`
67
+
68
+ if (this.writeStdout) {
69
+ this.writeStdout(message)
70
+ return
71
+ }
72
+
73
+ process.stdout.write(message)
74
+ }
75
+
76
+ emitError(message: string): void {
77
+ if (this.writeStderr) {
78
+ this.writeStderr(message)
79
+ return
80
+ }
81
+
82
+ if (this.prompts?.note) {
83
+ this.prompts.note(message)
84
+ return
85
+ }
86
+
87
+ console.error(message)
88
+ }
89
+
90
+ emitSuccess(message: string): void {
91
+ if (this.json) {
92
+ return
93
+ }
94
+
95
+ if (this.prompts?.log?.success) {
96
+ this.prompts.log.success(message)
97
+ return
98
+ }
99
+
100
+ this.emit(message)
101
+ }
102
+
103
+ async runStep<T>(
104
+ label: string,
105
+ fn: () => Promise<T>,
106
+ detail?: (value: T) => string | undefined,
107
+ ): Promise<T> {
108
+ try {
109
+ const value = await fn()
110
+ this.steps.push({
111
+ label,
112
+ status: 'success',
113
+ detail: detail?.(value),
114
+ })
115
+ return value
116
+ }
117
+ catch (error) {
118
+ const userError = toUserFacingError(error)
119
+ this.steps.push({
120
+ label,
121
+ status: 'failed',
122
+ detail: userError.summary,
123
+ })
124
+ throw error
125
+ }
126
+ }
127
+
128
+ getStepRecords(): StepRecord[] {
129
+ return [...this.steps]
130
+ }
131
+
132
+ appendEvidence(error: UserFacingError, evidence: string[]): UserFacingError {
133
+ if (evidence.length === 0) {
134
+ return error
135
+ }
136
+
137
+ return createUserFacingError({
138
+ code: error.code,
139
+ summary: error.summary,
140
+ details: error.details,
141
+ fixes: error.fixes,
142
+ evidence: [...error.evidence, ...evidence],
143
+ exitCode: error.exitCode,
144
+ cause: error.cause,
145
+ })
146
+ }
147
+ }
package/src/types.ts ADDED
@@ -0,0 +1,80 @@
1
+ export interface TelaCliConfig {
2
+ agentApiUrl: string
3
+ cloneRoot: string
4
+ apiKey: string
5
+ }
6
+
7
+ export interface OrganizationSummary {
8
+ id: string
9
+ workspaceId: string
10
+ name: string
11
+ }
12
+
13
+ export interface AgentRepoSummary {
14
+ id: string
15
+ workspaceId: string
16
+ organizationId: string
17
+ repository: string
18
+ }
19
+
20
+ export interface ForgejoRepositorySummary {
21
+ id: number
22
+ name: string
23
+ fullName: string
24
+ ownerLogin: string
25
+ htmlUrl: string
26
+ cloneUrl: string
27
+ sshUrl: string
28
+ defaultBranch: string | null
29
+ private: boolean
30
+ }
31
+
32
+ export interface RepositoryLookupResponse {
33
+ success: boolean
34
+ organization?: OrganizationSummary
35
+ agentRepo?: AgentRepoSummary
36
+ forgejoRepository?: ForgejoRepositorySummary
37
+ error?: string
38
+ }
39
+
40
+ export type AgentFeedbackStatus = 'success' | 'failed'
41
+
42
+ export interface AgentFeedbackItem {
43
+ status: AgentFeedbackStatus
44
+ reason: string
45
+ suggestion: string
46
+ }
47
+
48
+ export interface AgentFeedbackListResponse {
49
+ commitHash: string
50
+ feedbacks: AgentFeedbackItem[]
51
+ }
52
+
53
+ export interface SyncResult {
54
+ targetDir: string
55
+ branch: string
56
+ head: string
57
+ cloned: boolean
58
+ }
59
+
60
+ export interface BatchSyncFailure {
61
+ targetDir: string
62
+ error: string
63
+ category?: string
64
+ }
65
+
66
+ export interface BatchSyncResult {
67
+ successes: SyncResult[]
68
+ failures: BatchSyncFailure[]
69
+ }
70
+
71
+ export interface PushResult {
72
+ repoRoot: string
73
+ branch: string
74
+ message: string
75
+ retriedCommit: boolean
76
+ commitSha: string
77
+ upstream: string | null
78
+ messageSource: 'manual' | 'claude' | 'fallback'
79
+ fallbackReason?: string
80
+ }
package/src/ui.ts ADDED
@@ -0,0 +1,297 @@
1
+ export interface RenderOptions {
2
+ decorate?: boolean
3
+ }
4
+
5
+ const BOOTSTRAP_PIXEL_ROWS = [
6
+ '2222222222233443222222233222',
7
+ '2223332223556777743233333222',
8
+ '2233433335788789986434433222',
9
+ '2333433347888889988743433233',
10
+ '3333333368777778987554333234',
11
+ '3433332488755567887415433344',
12
+ '3333321476543456776512643344',
13
+ '3333311355433445666421553334',
14
+ '3334313453222554774211454333',
15
+ '3334213323223534664212334443',
16
+ '3343111125223327113202333443',
17
+ '3453121136444346345302333343',
18
+ '3453131146568422244401222354',
19
+ '3342142354345742244202222444',
20
+ '3344343554444764553213324545',
21
+ '3445444514535655543213555544',
22
+ '4444443410135566532131455554',
23
+ '4443343321123454312640255555',
24
+ '444333431111123214a910035555',
25
+ '34443331132111017bc401102465',
26
+ '3333320006742139cd9111110135',
27
+ '23342010169733addc5111111014',
28
+ '3333111017a3013cda2111111113',
29
+ '3333111029831217d51111111103',
30
+ '333200102a9a218a711111111102',
31
+ '2231011019c911bc411111110102',
32
+ '2231001105c411aa111111100101',
33
+ '2221001112821175111110000000',
34
+ ] as const
35
+
36
+ const TELA_ASCII_HEADER = [
37
+ '███████████╗ ████████╗███████╗██╗ █████╗ ',
38
+ '██ ██║ ╚══██╔══╝██╔════╝██║ ██╔══██╗',
39
+ '██ ██║ ██║ █████╗ ██║ ███████║',
40
+ '██ ██║ ██║ ██╔══╝ ██║ ██╔══██║',
41
+ '██ ██║ ██║ ███████╗███████╗██║ ██║',
42
+ '╚██████████╝ ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝',
43
+ ] as const
44
+
45
+ const ASCII_PIXEL_RAMP = ' .,:-=+*#%@'
46
+
47
+ function isDecorated(options: RenderOptions = {}): boolean {
48
+ return options.decorate ?? true
49
+ }
50
+
51
+ function applyAnsi(text: string, code: number, options: RenderOptions = {}): string {
52
+ if (!isDecorated(options)) {
53
+ return text
54
+ }
55
+
56
+ return `\u001B[${code}m${text}\u001B[0m`
57
+ }
58
+
59
+ function bold(text: string, options: RenderOptions = {}): string {
60
+ return applyAnsi(text, 1, options)
61
+ }
62
+
63
+ function color(text: string, code: number, options: RenderOptions = {}): string {
64
+ return applyAnsi(text, code, options)
65
+ }
66
+
67
+ function linesOf(input: string | string[]): string[] {
68
+ return Array.isArray(input)
69
+ ? input.flatMap(part => String(part).split('\n'))
70
+ : String(input).split('\n')
71
+ }
72
+
73
+ function stripAnsi(input: string): string {
74
+ return input.replace(/\u001B\[[0-9;]*m/g, '')
75
+ }
76
+
77
+ function visibleWidth(input: string): number {
78
+ return stripAnsi(input).length
79
+ }
80
+
81
+ function padRight(input: string, width: number): string {
82
+ const padding = Math.max(0, width - visibleWidth(input))
83
+ return `${input}${' '.repeat(padding)}`
84
+ }
85
+
86
+ export function heading(text: string, options: RenderOptions = {}): string {
87
+ return bold(text, options)
88
+ }
89
+
90
+ export function pane(
91
+ title: string,
92
+ content: string | string[],
93
+ options: RenderOptions = {},
94
+ ): string {
95
+ const decorate = isDecorated(options)
96
+ const inner = linesOf(content)
97
+ const header = decorate
98
+ ? color(`┌─ ${title}`, 36, options)
99
+ : `[${title}]`
100
+ const body = inner.map(line => decorate ? `│ ${line}` : line)
101
+
102
+ return [header, ...body].join('\n')
103
+ }
104
+
105
+ function renderPixelArtLine(row: string, options: RenderOptions = {}): string {
106
+ if (!isDecorated(options)) {
107
+ return renderBootstrapAsciiLine(row)
108
+ }
109
+
110
+ return row
111
+ .split('')
112
+ .map((cell) => {
113
+ const level = Number.parseInt(cell, 16)
114
+ const colorIndex = 232 + Math.round((level / 15) * 23)
115
+ return `\u001B[48;5;${colorIndex}m \u001B[0m`
116
+ })
117
+ .join('')
118
+ }
119
+
120
+ export function renderBootstrapAsciiLine(row: string): string {
121
+ return row
122
+ .split('')
123
+ .map((cell) => {
124
+ const level = Number.parseInt(cell, 16)
125
+ const index = Math.round((level / 15) * (ASCII_PIXEL_RAMP.length - 1))
126
+ return ASCII_PIXEL_RAMP[index] ?? ' '
127
+ })
128
+ .join('')
129
+ }
130
+
131
+ export function getBootstrapAsciiArt(): string[] {
132
+ return BOOTSTRAP_PIXEL_ROWS.map(renderBootstrapAsciiLine)
133
+ }
134
+
135
+ export function getBootstrapPixelRows(): readonly string[] {
136
+ return BOOTSTRAP_PIXEL_ROWS
137
+ }
138
+
139
+ export function getTelaAsciiHeader(): string[] {
140
+ return [...TELA_ASCII_HEADER]
141
+ }
142
+
143
+ function renderAsciiHeaderLine(line: string, options: RenderOptions = {}): string {
144
+ return isDecorated(options) ? color(line, 97, options) : line
145
+ }
146
+
147
+ export function bootstrapPane(
148
+ title: string,
149
+ content: string | string[],
150
+ options: RenderOptions = {},
151
+ ): string {
152
+ return pane(
153
+ title,
154
+ [
155
+ '',
156
+ ...TELA_ASCII_HEADER.map(line => renderAsciiHeaderLine(line, options)),
157
+ '',
158
+ ...BOOTSTRAP_PIXEL_ROWS.map(row => renderPixelArtLine(row, options)),
159
+ '',
160
+ ...linesOf(content),
161
+ ],
162
+ options,
163
+ )
164
+ }
165
+
166
+ export function section(
167
+ title: string,
168
+ content: string | string[],
169
+ options: RenderOptions = {},
170
+ ): string {
171
+ const lines = linesOf(content)
172
+ return [
173
+ heading(title, options),
174
+ ...lines.map(line => line ? ` ${line}` : ''),
175
+ ].join('\n')
176
+ }
177
+
178
+ export function kvList(
179
+ entries: Array<{ label: string, value: string }>,
180
+ options: RenderOptions = {},
181
+ ): string {
182
+ if (entries.length === 0) {
183
+ return ''
184
+ }
185
+
186
+ const labelWidth = entries.reduce((max, entry) => Math.max(max, entry.label.length), 0)
187
+ return entries
188
+ .map(entry => `${padRight(`${entry.label}:`, labelWidth + 1)} ${entry.value}`)
189
+ .join('\n')
190
+ }
191
+
192
+ export function stepList(steps: string[]): string {
193
+ return steps
194
+ .map((step, index) => `${index + 1}. ${step}`)
195
+ .join('\n')
196
+ }
197
+
198
+ export function bulletList(items: string[], prefix = '- '): string {
199
+ return items.map(item => `${prefix}${item}`).join('\n')
200
+ }
201
+
202
+ export function callout(
203
+ kind: 'info' | 'warn' | 'error' | 'success',
204
+ title: string,
205
+ content: string | string[],
206
+ options: RenderOptions = {},
207
+ ): string {
208
+ const prefixMap = {
209
+ info: { symbol: 'i', color: 36 },
210
+ warn: { symbol: '!', color: 33 },
211
+ error: { symbol: 'x', color: 31 },
212
+ success: { symbol: '✓', color: 32 },
213
+ } as const
214
+
215
+ const prefix = prefixMap[kind]
216
+ const decorate = isDecorated(options)
217
+ const marker = decorate
218
+ ? color(prefix.symbol, prefix.color, options)
219
+ : prefix.symbol
220
+
221
+ return pane(
222
+ `${marker} ${title}`,
223
+ content,
224
+ options,
225
+ )
226
+ }
227
+
228
+ export function resultCard(
229
+ title: string,
230
+ sections: Array<string | undefined | null>,
231
+ options: RenderOptions = {},
232
+ ): string {
233
+ return pane(
234
+ title,
235
+ sections.filter(Boolean).join('\n\n'),
236
+ options,
237
+ )
238
+ }
239
+
240
+ export function errorCard(input: {
241
+ summary: string
242
+ details?: string
243
+ fixes?: string[]
244
+ evidence?: string[]
245
+ }, options: RenderOptions = {}): string {
246
+ const parts: string[] = []
247
+
248
+ if (input.details) {
249
+ parts.push(input.details)
250
+ }
251
+
252
+ if (input.fixes?.length) {
253
+ parts.push(section('How to fix it', stepList(input.fixes), options))
254
+ }
255
+
256
+ if (input.evidence?.length) {
257
+ parts.push(section('Evidence', bulletList(input.evidence), options))
258
+ }
259
+
260
+ return callout('error', input.summary, parts, options)
261
+ }
262
+
263
+ export function nextActions(actions: string[], options: RenderOptions = {}): string {
264
+ return section('Next actions', stepList(actions), options)
265
+ }
266
+
267
+ export function table(
268
+ headers: string[],
269
+ rows: string[][],
270
+ options: RenderOptions = {},
271
+ ): string {
272
+ const allRows = [headers, ...rows]
273
+ const widths = headers.map((_, columnIndex) => allRows.reduce((max, row) => {
274
+ return Math.max(max, visibleWidth(row[columnIndex] ?? ''))
275
+ }, 0))
276
+
277
+ const divider = widths.map(width => '-'.repeat(width)).join(' ')
278
+ const renderedRows = allRows.map((row, rowIndex) => {
279
+ const rendered = row
280
+ .map((cell, columnIndex) => padRight(cell ?? '', widths[columnIndex] ?? 0))
281
+ .join(' ')
282
+
283
+ if (rowIndex === 0 && isDecorated(options)) {
284
+ return bold(rendered, options)
285
+ }
286
+
287
+ return rendered
288
+ })
289
+
290
+ return [renderedRows[0] ?? '', divider, ...renderedRows.slice(1)].join('\n')
291
+ }
292
+
293
+ export function joinBlocks(blocks: Array<string | undefined | null>): string {
294
+ return blocks
295
+ .filter((block): block is string => Boolean(block && block.trim()))
296
+ .join('\n\n')
297
+ }