@itcase/config 1.6.80 → 1.6.81
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 +4 -2
- package/package.json +15 -15
- package/smart-commit/cli.js +322 -42
- package/smart-commit/pacman.js +140 -0
- package/smart-commit/upyachka.js +229 -0
package/README.md
CHANGED
|
@@ -90,8 +90,10 @@ npm install --save-dev commitizen @commitlint/cz-commitlint
|
|
|
90
90
|
`smart-commit` — Cursor Agent (`ask`) планирует коммиты по `SCOPES`, печатает сообщения, спрашивает `[y/N]`. Перед коммитом ждёт и снимает устаревший `.git/index.lock`, если его никто не держит. Lockfile (`package-lock.json` и аналоги) всегда кладётся в тот же коммит, что и соседний `package.json`; при одном только bump deps агент не вызывается.
|
|
91
91
|
|
|
92
92
|
```text
|
|
93
|
-
[
|
|
93
|
+
[random pixel animation]
|
|
94
94
|
Drafting commit messages… (12s)
|
|
95
|
+
Agent started (Composer)
|
|
96
|
+
Reading src/foo.ts
|
|
95
97
|
|
|
96
98
|
Commits:
|
|
97
99
|
• feat(other): add smart-commit cli
|
|
@@ -146,7 +148,7 @@ CLI ставится в `~/.local/bin`. Логин в Cursor IDE этот CLI н
|
|
|
146
148
|
|
|
147
149
|
Обновление CLI: `agent update`.
|
|
148
150
|
|
|
149
|
-
Пока Agent думает, в терминале крутится pixel-анимация (случайно nyan cat
|
|
151
|
+
Пока Agent думает, в терминале крутится pixel-анимация (случайно nyan cat, dancing banana, упячка или Pac-Man) + статус `(Ns)`, а под ними живой лог агента (`log-update`). Пока процесс агента жив, CLI его не убивает по таймауту. Принудительно: `--cat` / `--banana` / `--upyachka` / `--pacman`. Для полного лога: `npm run smart-commit -- --verbose --dry-run`.
|
|
150
152
|
|
|
151
153
|
> **Важно:** в npm есть одноимённый пакет `smart-commit`. Используйте `npm run smart-commit` из проекта с `@itcase/config` — bin берётся из `node_modules`. `npx smart-commit` без локальной установки может подтянуть чужой пакет; CLI перед запуском проверяет, что выполняется именно `@itcase/config`.
|
|
152
154
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itcase/config",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.81",
|
|
4
4
|
"author": "ITCase",
|
|
5
5
|
"description": "ITCase Config",
|
|
6
6
|
"engines": {
|
|
@@ -63,23 +63,23 @@
|
|
|
63
63
|
"@svgr/plugin-jsx": "^8.1.0",
|
|
64
64
|
"@svgr/plugin-svgo": "^8.1.0",
|
|
65
65
|
"@svgr/webpack": "^8.1.0",
|
|
66
|
-
"autoprefixer": "^10.5.
|
|
66
|
+
"autoprefixer": "^10.5.5",
|
|
67
67
|
"chokidar-cli": "^3.0.0",
|
|
68
68
|
"commitizen": "^4.3.2",
|
|
69
69
|
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
70
|
-
"cssnano": "^9.0.
|
|
71
|
-
"cssnano-preset-default": "^9.0.
|
|
72
|
-
"inquirer": "^14.2.
|
|
70
|
+
"cssnano": "^9.0.4",
|
|
71
|
+
"cssnano-preset-default": "^9.0.4",
|
|
72
|
+
"inquirer": "^14.2.2",
|
|
73
73
|
"log-update": "^8.0.0",
|
|
74
|
-
"postcss": "^8.5.
|
|
74
|
+
"postcss": "^8.5.28",
|
|
75
75
|
"postcss-aspect-ratio-polyfill": "^2.0.0",
|
|
76
76
|
"postcss-clamp": "^4.1.0",
|
|
77
|
-
"postcss-cli": "^
|
|
77
|
+
"postcss-cli": "^12.0.0",
|
|
78
78
|
"postcss-color-hsla-fallback": "^1.0.0",
|
|
79
79
|
"postcss-combine-duplicated-selectors": "^10.0.3",
|
|
80
80
|
"postcss-convert-unit": "^0.1.3",
|
|
81
81
|
"postcss-dark-theme-class": "^2.0.0",
|
|
82
|
-
"postcss-discard-duplicates": "^9.0.
|
|
82
|
+
"postcss-discard-duplicates": "^9.0.2",
|
|
83
83
|
"postcss-each": "^1.1.0",
|
|
84
84
|
"postcss-easings": "^4.0.0",
|
|
85
85
|
"postcss-extend-rule": "^4.0.0",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"postcss-nested-ancestors": "^3.0.0",
|
|
96
96
|
"postcss-normalize": "^13.0.1",
|
|
97
97
|
"postcss-prepend-imports": "^1.0.1",
|
|
98
|
-
"postcss-preset-env": "^11.5.
|
|
98
|
+
"postcss-preset-env": "^11.5.2",
|
|
99
99
|
"postcss-pxtorem": "^6.1.0",
|
|
100
100
|
"postcss-sort-media-queries": "^6.7.1",
|
|
101
101
|
"postcss-unit": "^0.0.3",
|
|
@@ -109,17 +109,17 @@
|
|
|
109
109
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
110
110
|
"rollup-preserve-directives": "^1.1.3",
|
|
111
111
|
"semantic-release": "^25.0.9",
|
|
112
|
-
"semantic-release-lerna": "^3.6.
|
|
112
|
+
"semantic-release-lerna": "^3.6.4"
|
|
113
113
|
},
|
|
114
114
|
"devDependencies": {
|
|
115
|
-
"@itcase/lint": "^1.1.
|
|
115
|
+
"@itcase/lint": "^1.1.123",
|
|
116
116
|
"eslint": "9.39.2",
|
|
117
117
|
"husky": "^9.1.7",
|
|
118
|
-
"inquirer": "^14.2.
|
|
119
|
-
"lint-staged": "^17.
|
|
118
|
+
"inquirer": "^14.2.2",
|
|
119
|
+
"lint-staged": "^17.5.0",
|
|
120
120
|
"prettier": "^3.9.6",
|
|
121
|
-
"storybook": "^10.
|
|
122
|
-
"stylelint": "^17.
|
|
121
|
+
"storybook": "^10.6.0",
|
|
122
|
+
"stylelint": "^17.15.0",
|
|
123
123
|
"typescript": "^6.0.3"
|
|
124
124
|
}
|
|
125
125
|
}
|
package/smart-commit/cli.js
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
} from '../commitlint/commitlintCommon.js'
|
|
18
18
|
import { getBananaFrame } from './dancing-banana.js'
|
|
19
19
|
import { getNyanFrame } from './nyan-cat.js'
|
|
20
|
+
import { getPacmanFrame } from './pacman.js'
|
|
21
|
+
import { getUpyachkaFrame } from './upyachka.js'
|
|
20
22
|
|
|
21
23
|
const PACKAGE_NAME = '@itcase/config'
|
|
22
24
|
const CLI_RELATIVE = 'smart-commit/cli.js'
|
|
@@ -30,10 +32,14 @@ const LOCKFILE_NAMES = new Set([
|
|
|
30
32
|
'bun.lock',
|
|
31
33
|
'bun.lockb',
|
|
32
34
|
])
|
|
33
|
-
const AGENT_TIMEOUT_MS = 120_000
|
|
34
35
|
const AGENT_STEP_MS = 2_500
|
|
36
|
+
const AGENT_STILL_WORKING_S = 120
|
|
37
|
+
const AGENT_LOG_ROWS = 8
|
|
38
|
+
const AGENT_LOG_MAX = 160
|
|
35
39
|
const NYAN_FRAME_MS = 90
|
|
36
40
|
const BANANA_FRAME_MS = 100
|
|
41
|
+
const PACMAN_FRAME_MS = 120
|
|
42
|
+
const UPYACHKA_FRAME_MS = 100
|
|
37
43
|
const INDEX_LOCK_RETRIES = 8
|
|
38
44
|
const INDEX_LOCK_WAIT_MS = 250
|
|
39
45
|
const STALE_LOCK_MS = 1500
|
|
@@ -43,13 +49,14 @@ const AGENT_WAIT_STEPS = [
|
|
|
43
49
|
'Waiting for response',
|
|
44
50
|
]
|
|
45
51
|
const HELP = `smart-commit [--dry-run] [--yes|-y] [--type t] [--model id] [--verbose]
|
|
46
|
-
[--cat|--nyan] [--banana]
|
|
52
|
+
[--cat|--nyan] [--banana] [--upyachka] [--pacman]
|
|
47
53
|
|
|
48
54
|
Cursor Agent → commit messages by SCOPES → [y/N] → git commit
|
|
49
55
|
Run via: npm exec smart-commit (from project with @itcase/config)
|
|
50
56
|
Needs: agent login (or CURSOR_API_KEY) + commitlint.config.* with SCOPES
|
|
51
57
|
|
|
52
|
-
Progress sprite is random (nyan cat
|
|
58
|
+
Progress sprite is random (nyan cat, banana, upyachka, or Pac-Man).
|
|
59
|
+
Force with --cat, --banana, --upyachka, or --pacman. Agent log is below.
|
|
53
60
|
`
|
|
54
61
|
const AGENT_MISSING = `Cursor Agent CLI not found — smart-commit cannot run.
|
|
55
62
|
|
|
@@ -213,6 +220,8 @@ function parseArgs(argv) {
|
|
|
213
220
|
else if (arg === '--staged') option.staged = true
|
|
214
221
|
else if (arg === '--cat' || arg === '--nyan') option.sprite = 'cat'
|
|
215
222
|
else if (arg === '--banana') option.sprite = 'banana'
|
|
223
|
+
else if (arg === '--upyachka') option.sprite = 'upyachka'
|
|
224
|
+
else if (arg === '--pacman') option.sprite = 'pacman'
|
|
216
225
|
else if (arg === '--type') option.type = argv[++i] || ''
|
|
217
226
|
else if (arg === '--model') option.model = argv[++i] || ''
|
|
218
227
|
else if (arg.startsWith('--type=')) option.type = arg.slice(7)
|
|
@@ -223,8 +232,9 @@ function parseArgs(argv) {
|
|
|
223
232
|
}
|
|
224
233
|
|
|
225
234
|
function pickSprite(forced) {
|
|
226
|
-
|
|
227
|
-
|
|
235
|
+
const sprites = ['cat', 'banana', 'upyachka', 'pacman']
|
|
236
|
+
if (sprites.includes(forced)) return forced
|
|
237
|
+
return sprites[Math.floor(Math.random() * sprites.length)]
|
|
228
238
|
}
|
|
229
239
|
|
|
230
240
|
function assertItcaseConfigPackage(cwd) {
|
|
@@ -460,49 +470,109 @@ async function withAgentProgress(context, callback) {
|
|
|
460
470
|
let phase = 'early'
|
|
461
471
|
let receivedChars = 0
|
|
462
472
|
let frameIndex = 0
|
|
473
|
+
const logLines = []
|
|
474
|
+
const streamByKey = { assistant: -1, thinking: -1 }
|
|
463
475
|
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
476
|
+
const elapsedSeconds = () => Math.floor((Date.now() - startedAt) / 1000)
|
|
477
|
+
|
|
478
|
+
const onAgentActivity = (activity = {}) => {
|
|
479
|
+
if (activity.charCount > 0) {
|
|
480
|
+
receivedChars = activity.charCount
|
|
467
481
|
phase = 'receiving'
|
|
468
482
|
}
|
|
483
|
+
if (!activity.log) return
|
|
484
|
+
if (activity.stream) {
|
|
485
|
+
const key = activity.streamKey || 'assistant'
|
|
486
|
+
const index = streamByKey[key]
|
|
487
|
+
if (index >= 0 && index < logLines.length) logLines[index] = activity.log
|
|
488
|
+
else {
|
|
489
|
+
logLines.push(activity.log)
|
|
490
|
+
streamByKey[key] = logLines.length - 1
|
|
491
|
+
}
|
|
492
|
+
} else if (logLines[logLines.length - 1] !== activity.log) {
|
|
493
|
+
logLines.push(activity.log)
|
|
494
|
+
streamByKey.assistant = -1
|
|
495
|
+
}
|
|
496
|
+
if (phase !== 'receiving') phase = 'working'
|
|
469
497
|
}
|
|
470
498
|
|
|
471
499
|
const currentLabel = () => {
|
|
500
|
+
const elapsed = elapsedSeconds()
|
|
472
501
|
if (phase === 'receiving') {
|
|
473
502
|
return `Receiving response (${receivedChars} chars)`
|
|
474
503
|
}
|
|
475
|
-
if (
|
|
504
|
+
if (elapsed >= AGENT_STILL_WORKING_S) return 'Agent still working'
|
|
505
|
+
if (phase === 'waiting' || phase === 'working') {
|
|
476
506
|
return AGENT_WAIT_STEPS[waitStepIndex % AGENT_WAIT_STEPS.length]
|
|
477
507
|
}
|
|
478
508
|
return earlySteps[Math.min(earlyStepIndex, earlySteps.length - 1)]
|
|
479
509
|
}
|
|
480
510
|
|
|
481
511
|
const formatText = () => {
|
|
482
|
-
const
|
|
483
|
-
|
|
512
|
+
const text = `${currentLabel()}… (${elapsedSeconds()}s)`
|
|
513
|
+
const width = process.stderr.columns || process.stdout.columns || 80
|
|
514
|
+
return clipLog(text, width)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const reportActivity = (activity) => {
|
|
518
|
+
onAgentActivity(activity)
|
|
519
|
+
if (
|
|
520
|
+
(!process.stderr.isTTY || context.verbose) &&
|
|
521
|
+
activity.log &&
|
|
522
|
+
!activity.stream
|
|
523
|
+
) {
|
|
524
|
+
process.stderr.write(`${activity.log}\n`)
|
|
525
|
+
}
|
|
484
526
|
}
|
|
485
527
|
|
|
486
528
|
if (!process.stderr.isTTY || context.verbose) {
|
|
487
529
|
process.stderr.write(`${formatText()}\n`)
|
|
488
|
-
|
|
530
|
+
const waitTimer = setInterval(() => {
|
|
531
|
+
if (phase === 'receiving') return
|
|
532
|
+
process.stderr.write(`Thinking… (${elapsedSeconds()}s)\n`)
|
|
533
|
+
}, AGENT_STEP_MS)
|
|
534
|
+
try {
|
|
535
|
+
return await callback({ onAgentActivity: reportActivity })
|
|
536
|
+
} finally {
|
|
537
|
+
clearInterval(waitTimer)
|
|
538
|
+
}
|
|
489
539
|
}
|
|
490
540
|
|
|
491
541
|
const columns = process.stderr.columns || process.stdout.columns || 80
|
|
492
542
|
const rows = process.stderr.rows || process.stdout.rows || 24
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
543
|
+
const logRows = Math.min(AGENT_LOG_ROWS, Math.max(3, rows - 10))
|
|
544
|
+
const spriteRows = Math.max(10, rows - logRows)
|
|
545
|
+
const sprite = {
|
|
546
|
+
banana: {
|
|
547
|
+
render: (index) => getBananaFrame(index, columns, spriteRows),
|
|
548
|
+
frameMs: BANANA_FRAME_MS,
|
|
549
|
+
},
|
|
550
|
+
cat: {
|
|
551
|
+
render: (index) => getNyanFrame(index, columns),
|
|
552
|
+
frameMs: NYAN_FRAME_MS,
|
|
553
|
+
},
|
|
554
|
+
pacman: {
|
|
555
|
+
render: (index) => getPacmanFrame(index, columns, spriteRows),
|
|
556
|
+
frameMs: PACMAN_FRAME_MS,
|
|
557
|
+
},
|
|
558
|
+
upyachka: {
|
|
559
|
+
render: (index) => getUpyachkaFrame(index, columns, spriteRows),
|
|
560
|
+
frameMs: UPYACHKA_FRAME_MS,
|
|
561
|
+
},
|
|
562
|
+
}[context.sprite]
|
|
498
563
|
process.stderr.write('\x1b[?7l')
|
|
499
564
|
|
|
500
565
|
const render = () => {
|
|
501
|
-
|
|
566
|
+
const waiting = phase !== 'receiving'
|
|
567
|
+
const extra = waiting ? `Thinking… ${elapsedSeconds()}s` : ''
|
|
568
|
+
const logBlock = formatLogBlock(logLines, columns, logRows, extra)
|
|
569
|
+
logUpdateStderr(
|
|
570
|
+
`${sprite.render(frameIndex)}\n${formatText()}${logBlock ? `\n${logBlock}` : ''}`,
|
|
571
|
+
)
|
|
502
572
|
frameIndex++
|
|
503
573
|
}
|
|
504
574
|
|
|
505
|
-
const frameTimer = setInterval(render, frameMs)
|
|
575
|
+
const frameTimer = setInterval(render, sprite.frameMs)
|
|
506
576
|
const stepTimer = setInterval(() => {
|
|
507
577
|
if (phase === 'early') {
|
|
508
578
|
if (earlyStepIndex < earlySteps.length - 1) {
|
|
@@ -510,14 +580,14 @@ async function withAgentProgress(context, callback) {
|
|
|
510
580
|
} else {
|
|
511
581
|
phase = 'waiting'
|
|
512
582
|
}
|
|
513
|
-
} else if (phase
|
|
583
|
+
} else if (phase !== 'receiving') {
|
|
514
584
|
waitStepIndex++
|
|
515
585
|
}
|
|
516
586
|
}, AGENT_STEP_MS)
|
|
517
587
|
|
|
518
588
|
try {
|
|
519
589
|
render()
|
|
520
|
-
return await callback({ onAgentActivity })
|
|
590
|
+
return await callback({ onAgentActivity: reportActivity })
|
|
521
591
|
} finally {
|
|
522
592
|
clearInterval(frameTimer)
|
|
523
593
|
clearInterval(stepTimer)
|
|
@@ -526,6 +596,16 @@ async function withAgentProgress(context, callback) {
|
|
|
526
596
|
}
|
|
527
597
|
}
|
|
528
598
|
|
|
599
|
+
function formatLogBlock(lines, columns, logRows, extra = '') {
|
|
600
|
+
const all = extra ? [...lines, extra] : lines
|
|
601
|
+
if (!all.length) return ''
|
|
602
|
+
const width = Math.max(20, columns || 80)
|
|
603
|
+
return all
|
|
604
|
+
.slice(-logRows)
|
|
605
|
+
.map((line) => clipLog(line, width))
|
|
606
|
+
.join('\n')
|
|
607
|
+
}
|
|
608
|
+
|
|
529
609
|
function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
|
|
530
610
|
const cmd = resolveAgent()
|
|
531
611
|
const args = [
|
|
@@ -533,7 +613,8 @@ function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
|
|
|
533
613
|
'--mode',
|
|
534
614
|
'ask',
|
|
535
615
|
'--output-format',
|
|
536
|
-
'
|
|
616
|
+
'stream-json',
|
|
617
|
+
'--stream-partial-output',
|
|
537
618
|
'--trust',
|
|
538
619
|
'--workspace',
|
|
539
620
|
cwd,
|
|
@@ -544,6 +625,9 @@ function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
|
|
|
544
625
|
args.push(prompt)
|
|
545
626
|
|
|
546
627
|
if (verbose) process.stderr.write(`Using: ${cmd}\n`)
|
|
628
|
+
if (onAgentActivity) {
|
|
629
|
+
onAgentActivity({ log: `Sending prompt (${prompt.length} chars)` })
|
|
630
|
+
}
|
|
547
631
|
|
|
548
632
|
return new Promise((resolve, reject) => {
|
|
549
633
|
const child = spawn(cmd, args, {
|
|
@@ -551,44 +635,240 @@ function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
|
|
|
551
635
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
552
636
|
})
|
|
553
637
|
|
|
554
|
-
|
|
638
|
+
const stream = {
|
|
639
|
+
buffer: '',
|
|
640
|
+
lastAssistant: '',
|
|
641
|
+
raw: '',
|
|
642
|
+
result: '',
|
|
643
|
+
thinking: '',
|
|
644
|
+
}
|
|
555
645
|
let stderr = ''
|
|
646
|
+
let settled = false
|
|
647
|
+
|
|
648
|
+
const stopChild = (signal = 'SIGTERM') => {
|
|
649
|
+
if (child.exitCode !== null || child.killed) return
|
|
650
|
+
child.kill(signal)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const finish = (error, value) => {
|
|
654
|
+
if (settled) return
|
|
655
|
+
settled = true
|
|
656
|
+
process.removeListener('SIGINT', onSignal)
|
|
657
|
+
process.removeListener('SIGTERM', onSignal)
|
|
658
|
+
if (error) reject(error)
|
|
659
|
+
else resolve(value)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const onSignal = () => {
|
|
663
|
+
stopChild('SIGTERM')
|
|
664
|
+
finish(new Error('Interrupted'))
|
|
665
|
+
}
|
|
666
|
+
process.once('SIGINT', onSignal)
|
|
667
|
+
process.once('SIGTERM', onSignal)
|
|
556
668
|
|
|
557
669
|
child.stdout.on('data', (chunk) => {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
670
|
+
const text = String(chunk)
|
|
671
|
+
stream.raw += text
|
|
672
|
+
const activities = consumeAgentStdout(stream, text)
|
|
673
|
+
for (const activity of activities) {
|
|
674
|
+
if (onAgentActivity) onAgentActivity(activity)
|
|
675
|
+
}
|
|
561
676
|
})
|
|
562
677
|
child.stderr.on('data', (chunk) => {
|
|
563
|
-
|
|
564
|
-
|
|
678
|
+
const text = String(chunk)
|
|
679
|
+
stderr += text
|
|
680
|
+
const line = lastNonEmptyLine(text)
|
|
681
|
+
if (line && onAgentActivity) onAgentActivity({ log: line })
|
|
682
|
+
if (verbose) process.stderr.write(text)
|
|
565
683
|
})
|
|
566
684
|
|
|
567
|
-
const timeout = setTimeout(() => {
|
|
568
|
-
child.kill('SIGTERM')
|
|
569
|
-
reject(
|
|
570
|
-
new Error(
|
|
571
|
-
`Cursor Agent timed out after ${AGENT_TIMEOUT_MS / 1000}s. Check agent login / network.`,
|
|
572
|
-
),
|
|
573
|
-
)
|
|
574
|
-
}, AGENT_TIMEOUT_MS)
|
|
575
|
-
|
|
576
685
|
child.on('error', (error) => {
|
|
577
|
-
|
|
578
|
-
reject(error)
|
|
686
|
+
finish(error)
|
|
579
687
|
})
|
|
580
688
|
|
|
581
689
|
child.on('close', (code) => {
|
|
582
|
-
|
|
690
|
+
consumeAgentStdout(stream, '\n')
|
|
691
|
+
const raw = (
|
|
692
|
+
stream.result ||
|
|
693
|
+
stream.lastAssistant ||
|
|
694
|
+
stream.buffer ||
|
|
695
|
+
stream.raw
|
|
696
|
+
).trim()
|
|
583
697
|
if (code) {
|
|
584
|
-
|
|
698
|
+
finish(new Error(formatAgentError(stderr, raw)))
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
if (!raw) {
|
|
702
|
+
finish(new Error(formatAgentError(stderr, '')))
|
|
585
703
|
return
|
|
586
704
|
}
|
|
587
|
-
|
|
705
|
+
finish(null, raw)
|
|
588
706
|
})
|
|
589
707
|
})
|
|
590
708
|
}
|
|
591
709
|
|
|
710
|
+
function consumeAgentStdout(stream, chunk) {
|
|
711
|
+
stream.buffer += chunk
|
|
712
|
+
const lines = stream.buffer.split('\n')
|
|
713
|
+
stream.buffer = lines.pop() || ''
|
|
714
|
+
const activities = []
|
|
715
|
+
for (const line of lines) {
|
|
716
|
+
const activity = applyAgentLine(stream, line)
|
|
717
|
+
if (activity) activities.push(activity)
|
|
718
|
+
}
|
|
719
|
+
return activities
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function applyAgentLine(stream, line) {
|
|
723
|
+
const trimmed = line.trim()
|
|
724
|
+
if (!trimmed) return null
|
|
725
|
+
|
|
726
|
+
let event
|
|
727
|
+
try {
|
|
728
|
+
event = JSON.parse(trimmed)
|
|
729
|
+
} catch {
|
|
730
|
+
if (!stream.result) stream.result = trimmed
|
|
731
|
+
return {
|
|
732
|
+
charCount: trimmed.length,
|
|
733
|
+
log: trimmed,
|
|
734
|
+
stream: true,
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (!event || typeof event !== 'object') return null
|
|
739
|
+
if (event.type === 'user') return null
|
|
740
|
+
|
|
741
|
+
if (event.type === 'thinking') {
|
|
742
|
+
const text = extractThinkingText(event)
|
|
743
|
+
if (event.subtype === 'completed') {
|
|
744
|
+
return stream.thinking
|
|
745
|
+
? {
|
|
746
|
+
log: `Thinking: ${stream.thinking}`,
|
|
747
|
+
stream: true,
|
|
748
|
+
streamKey: 'thinking',
|
|
749
|
+
}
|
|
750
|
+
: { log: 'Finished thinking' }
|
|
751
|
+
}
|
|
752
|
+
if (text) {
|
|
753
|
+
stream.thinking += text
|
|
754
|
+
return {
|
|
755
|
+
log: `Thinking: ${stream.thinking}`,
|
|
756
|
+
stream: true,
|
|
757
|
+
streamKey: 'thinking',
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return { log: 'Thinking' }
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (event.type === 'result' && typeof event.result === 'string') {
|
|
764
|
+
stream.result = event.result
|
|
765
|
+
return { charCount: stream.result.length, log: 'Agent finished' }
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (event.type === 'assistant') {
|
|
769
|
+
if (event.model_call_id != null) return null
|
|
770
|
+
const thinking = extractContentText(event.message?.content, 'thinking')
|
|
771
|
+
if (thinking) {
|
|
772
|
+
stream.thinking += thinking
|
|
773
|
+
return {
|
|
774
|
+
log: `Thinking: ${stream.thinking}`,
|
|
775
|
+
stream: true,
|
|
776
|
+
streamKey: 'thinking',
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const text = extractAssistantText(event)
|
|
780
|
+
if (!text) return null
|
|
781
|
+
if (typeof event.timestamp_ms === 'number') stream.lastAssistant += text
|
|
782
|
+
else stream.lastAssistant = text
|
|
783
|
+
return {
|
|
784
|
+
charCount: stream.lastAssistant.length,
|
|
785
|
+
log: stream.lastAssistant,
|
|
786
|
+
stream: true,
|
|
787
|
+
streamKey: 'assistant',
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const log = describeAgentEvent(event)
|
|
792
|
+
if (log) return { log }
|
|
793
|
+
if (event.type) {
|
|
794
|
+
return {
|
|
795
|
+
log: event.subtype
|
|
796
|
+
? `${event.type}: ${event.subtype}`
|
|
797
|
+
: String(event.type),
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return null
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function extractAssistantText(event) {
|
|
804
|
+
return extractContentText(event.message?.content, 'text')
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function extractThinkingText(event) {
|
|
808
|
+
if (typeof event.text === 'string' && event.text) return event.text
|
|
809
|
+
if (typeof event.delta === 'string' && event.delta) return event.delta
|
|
810
|
+
return (
|
|
811
|
+
extractContentText(event.message?.content, 'thinking') ||
|
|
812
|
+
extractContentText(event.content, 'thinking') ||
|
|
813
|
+
extractContentText(event.message?.content, 'text')
|
|
814
|
+
)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function extractContentText(content, type) {
|
|
818
|
+
if (!Array.isArray(content)) return ''
|
|
819
|
+
return content
|
|
820
|
+
.filter((part) => part?.type === type && typeof part.text === 'string')
|
|
821
|
+
.map((part) => part.text)
|
|
822
|
+
.join('')
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function describeAgentEvent(event) {
|
|
826
|
+
if (event.type === 'system' && event.subtype === 'init') {
|
|
827
|
+
return event.model ? `Agent started (${event.model})` : 'Agent started'
|
|
828
|
+
}
|
|
829
|
+
if (event.type === 'tool_call') {
|
|
830
|
+
return describeToolCall(event.tool_call, event.subtype === 'completed')
|
|
831
|
+
}
|
|
832
|
+
return ''
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function describeToolCall(toolCall, done) {
|
|
836
|
+
if (!toolCall || typeof toolCall !== 'object') {
|
|
837
|
+
return done ? 'Tool done' : 'Tool call'
|
|
838
|
+
}
|
|
839
|
+
const [key, payload] = Object.entries(toolCall)[0] || []
|
|
840
|
+
if (key === 'function' && payload?.name) {
|
|
841
|
+
return done ? `Done ${payload.name}` : payload.name
|
|
842
|
+
}
|
|
843
|
+
const args =
|
|
844
|
+
payload?.args && typeof payload.args === 'object' ? payload.args : {}
|
|
845
|
+
const name = (key || 'tool').replace(/ToolCall$/, '')
|
|
846
|
+
const detail =
|
|
847
|
+
args.path ||
|
|
848
|
+
args.file_path ||
|
|
849
|
+
args.command ||
|
|
850
|
+
args.query ||
|
|
851
|
+
args.pattern ||
|
|
852
|
+
args.glob ||
|
|
853
|
+
''
|
|
854
|
+
const label = detail ? `${name} ${detail}` : name
|
|
855
|
+
return done ? `Done ${label}` : `${label}`
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function lastNonEmptyLine(text) {
|
|
859
|
+
const lines = String(text)
|
|
860
|
+
.split(/\r?\n/)
|
|
861
|
+
.map((line) => line.trim())
|
|
862
|
+
.filter(Boolean)
|
|
863
|
+
return lines.at(-1) || ''
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function clipLog(text, max = AGENT_LOG_MAX) {
|
|
867
|
+
const oneLine = String(text).replace(/\s+/g, ' ').trim()
|
|
868
|
+
if (oneLine.length <= max) return oneLine
|
|
869
|
+
return `…${oneLine.slice(-(max - 1))}`
|
|
870
|
+
}
|
|
871
|
+
|
|
592
872
|
function formatAgentError(stderr, stdout) {
|
|
593
873
|
const text = (stderr || stdout || AGENT_MISSING).trim()
|
|
594
874
|
if (/authentication required/i.test(text)) {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// High-resolution terminal sprites reconstructed from the supplied reference.
|
|
2
|
+
// Two vertical source pixels share one terminal cell via half-block glyphs.
|
|
3
|
+
|
|
4
|
+
const RESET = '\x1b[0m'
|
|
5
|
+
const FRAME_WIDTH = 80
|
|
6
|
+
const FRAME_HEIGHT = 16
|
|
7
|
+
const FRAME_COUNT = 82
|
|
8
|
+
const SPRITE_WIDTH = 14
|
|
9
|
+
const SPRITE_HEIGHT = 16
|
|
10
|
+
const COLORS = { B: 33, C: 51, O: 208, P: 218, R: 196, W: 231, Y: 226 }
|
|
11
|
+
|
|
12
|
+
const GHOST = [
|
|
13
|
+
'................',
|
|
14
|
+
'......XXXX......',
|
|
15
|
+
'....XXXXXXXX....',
|
|
16
|
+
'...XXXXXXXXXX...',
|
|
17
|
+
'..XXXXXXXXXXXX..',
|
|
18
|
+
'..XWWXXXXWWWXXX.',
|
|
19
|
+
'..WWWWXXWWWWXX..',
|
|
20
|
+
'..BBWWXXBBWWXX..',
|
|
21
|
+
'.XBBWWXXBBWWXXX.',
|
|
22
|
+
'.XXWWXXXXWWXXXX.',
|
|
23
|
+
'.XXXXXXXXXXXXXX.',
|
|
24
|
+
'.XXXXXXXXXXXXXX.',
|
|
25
|
+
'.XXXXXXXXXXXXXX.',
|
|
26
|
+
'.XXXXXXXXXXXXXX.',
|
|
27
|
+
'.XXXXXXXXXXXXXX.',
|
|
28
|
+
'.XX.XXX..XXX.XX.',
|
|
29
|
+
'.X...XX..XX...X.',
|
|
30
|
+
'................',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const PACMAN_CLOSED = [
|
|
34
|
+
'................',
|
|
35
|
+
'.....YYYYYY.....',
|
|
36
|
+
'...YYYYYYYYYY...',
|
|
37
|
+
'..YYYYYYYYYYYY..',
|
|
38
|
+
'.YYYYYYYYYYYYYY.',
|
|
39
|
+
'YYYYYYYYYYYYYYYY',
|
|
40
|
+
'YYYYYYYYYYYYYYYY',
|
|
41
|
+
'YYYYYYYYYYYYYYYY',
|
|
42
|
+
'YYYYYYYYYYYYYYYY',
|
|
43
|
+
'YYYYYYYYYYYYYYYY',
|
|
44
|
+
'YYYYYYYYYYYYYYYY',
|
|
45
|
+
'YYYYYYYYYYYYYYYY',
|
|
46
|
+
'YYYYYYYYYYYYYYYY',
|
|
47
|
+
'.YYYYYYYYYYYYYY.',
|
|
48
|
+
'..YYYYYYYYYYYY..',
|
|
49
|
+
'...YYYYYYYYYY...',
|
|
50
|
+
'.....YYYYYY.....',
|
|
51
|
+
'................',
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const PACMAN_OPEN = [
|
|
55
|
+
'................',
|
|
56
|
+
'.....YYYYYY.....',
|
|
57
|
+
'...YYYYYYYYYY...',
|
|
58
|
+
'..YYYYYYYYYYYY..',
|
|
59
|
+
'.YYYYYYYYYYYYYY.',
|
|
60
|
+
'YYYYYYYYYYYYYYYY',
|
|
61
|
+
'..YYYYYYYYYYYYYY',
|
|
62
|
+
'....YYYYYYYYYYYY',
|
|
63
|
+
'......YYYYYYYYYY',
|
|
64
|
+
'........YYYYYYYY',
|
|
65
|
+
'......YYYYYYYYYY',
|
|
66
|
+
'....YYYYYYYYYYYY',
|
|
67
|
+
'..YYYYYYYYYYYYYY',
|
|
68
|
+
'.YYYYYYYYYYYYYY.',
|
|
69
|
+
'..YYYYYYYYYYYY..',
|
|
70
|
+
'...YYYYYYYYYY...',
|
|
71
|
+
'.....YYYYYY.....',
|
|
72
|
+
'................',
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
function drawSprite(canvas, sprite, left, color) {
|
|
76
|
+
const columns = pickIndexes(sprite[0].length, SPRITE_WIDTH)
|
|
77
|
+
const rows = pickIndexes(sprite.length, SPRITE_HEIGHT)
|
|
78
|
+
for (let y = 0; y < rows.length; y++) {
|
|
79
|
+
for (let x = 0; x < columns.length; x++) {
|
|
80
|
+
const cell = sprite[rows[y]][columns[x]]
|
|
81
|
+
const targetX = left + x
|
|
82
|
+
if (cell === '.' || targetX < 0 || targetX >= FRAME_WIDTH) continue
|
|
83
|
+
canvas[y][targetX] = cell === 'X' ? color : cell
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildFrame(index) {
|
|
89
|
+
const canvas = Array.from({ length: FRAME_HEIGHT }, () =>
|
|
90
|
+
Array(FRAME_WIDTH).fill('.'),
|
|
91
|
+
)
|
|
92
|
+
const pacmanLeft = FRAME_WIDTH - 7 - index * 2
|
|
93
|
+
const pacman = Math.floor(index / 2) % 2 ? PACMAN_CLOSED : PACMAN_OPEN
|
|
94
|
+
drawSprite(canvas, pacman, pacmanLeft, 'Y')
|
|
95
|
+
|
|
96
|
+
for (const [ghostIndex, color] of ['R', 'C', 'P', 'O'].entries()) {
|
|
97
|
+
drawSprite(canvas, GHOST, pacmanLeft + 20 + ghostIndex * 18, color)
|
|
98
|
+
}
|
|
99
|
+
return canvas
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function pickIndexes(srcSize, maxSize) {
|
|
103
|
+
if (srcSize <= maxSize) {
|
|
104
|
+
return Array.from({ length: srcSize }, (_, index) => index)
|
|
105
|
+
}
|
|
106
|
+
return Array.from({ length: maxSize }, (_, index) =>
|
|
107
|
+
Math.round((index * (srcSize - 1)) / (maxSize - 1)),
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function paint(top, bottom) {
|
|
112
|
+
const topColor = COLORS[top]
|
|
113
|
+
const bottomColor = COLORS[bottom]
|
|
114
|
+
if (topColor === undefined && bottomColor === undefined) return ' '
|
|
115
|
+
if (topColor === bottomColor) return `\x1b[48;5;${topColor}m \x1b[0m`
|
|
116
|
+
if (topColor === undefined) return `\x1b[38;5;${bottomColor}m▄\x1b[0m`
|
|
117
|
+
if (bottomColor === undefined) return `\x1b[38;5;${topColor}m▀\x1b[0m`
|
|
118
|
+
return `\x1b[38;5;${topColor};48;5;${bottomColor}m▀\x1b[0m`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getPacmanFrame(index, terminalColumns = 80, terminalRows = 24) {
|
|
122
|
+
const frame = buildFrame(index % FRAME_COUNT)
|
|
123
|
+
const maxColumns = Math.max(16, (terminalColumns || 80) - 1)
|
|
124
|
+
const maxLogicalRows = Math.max(4, ((terminalRows || 24) - 2) * 2)
|
|
125
|
+
const columns = pickIndexes(FRAME_WIDTH, maxColumns)
|
|
126
|
+
const rowCount = Math.min(FRAME_HEIGHT, maxLogicalRows)
|
|
127
|
+
const rows = pickIndexes(FRAME_HEIGHT, rowCount - (rowCount % 2))
|
|
128
|
+
const lines = []
|
|
129
|
+
|
|
130
|
+
for (let index = 0; index < rows.length; index += 2) {
|
|
131
|
+
const top = frame[rows[index]]
|
|
132
|
+
const bottom = frame[rows[index + 1]]
|
|
133
|
+
lines.push(columns.map((x) => paint(top[x], bottom[x])).join('') + RESET)
|
|
134
|
+
}
|
|
135
|
+
return lines.join('\n')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getPacmanFrameCount() {
|
|
139
|
+
return FRAME_COUNT
|
|
140
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Five frames extracted from the classic upyachka GIF.
|
|
2
|
+
// Default-color block pixels stay visible on light and dark terminals.
|
|
3
|
+
|
|
4
|
+
const FRAME_WIDTH = 29
|
|
5
|
+
const FRAME_HEIGHT = 36
|
|
6
|
+
|
|
7
|
+
const FRAMES = [
|
|
8
|
+
[
|
|
9
|
+
'..................#..........',
|
|
10
|
+
'................#####........',
|
|
11
|
+
'...............#######.......',
|
|
12
|
+
'............#..##....##......',
|
|
13
|
+
'............#####....##......',
|
|
14
|
+
'...........######....##......',
|
|
15
|
+
'...........##.####.###.......',
|
|
16
|
+
'...........##...#####........',
|
|
17
|
+
'...........##....###.........',
|
|
18
|
+
'...........##....###.........',
|
|
19
|
+
'...........##...#####........',
|
|
20
|
+
'...........#....##.##........',
|
|
21
|
+
'.............#####..#........',
|
|
22
|
+
'..............########.......',
|
|
23
|
+
'...............##.####.......',
|
|
24
|
+
'..............##.............',
|
|
25
|
+
'..............##.............',
|
|
26
|
+
'.............######..........',
|
|
27
|
+
'.............##########......',
|
|
28
|
+
'.............##....#####.....',
|
|
29
|
+
'.............##.......##.....',
|
|
30
|
+
'.............##.......##.....',
|
|
31
|
+
'.............##......##......',
|
|
32
|
+
'.............##......##......',
|
|
33
|
+
'.............##......##......',
|
|
34
|
+
'.............##......##......',
|
|
35
|
+
'.............##.....##.......',
|
|
36
|
+
'.............##.....##.......',
|
|
37
|
+
'.............##.....##.......',
|
|
38
|
+
'.............##..............',
|
|
39
|
+
'.............##..............',
|
|
40
|
+
'.............##..............',
|
|
41
|
+
'.............##..............',
|
|
42
|
+
'.............##..............',
|
|
43
|
+
'.............##..............',
|
|
44
|
+
'..............#..............',
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
'...............##............',
|
|
48
|
+
'.............######..........',
|
|
49
|
+
'............###..###.........',
|
|
50
|
+
'............##....##.##......',
|
|
51
|
+
'............##....#####......',
|
|
52
|
+
'............##....######.....',
|
|
53
|
+
'.............#######..##.....',
|
|
54
|
+
'..............#####...##.....',
|
|
55
|
+
'...............###....##.....',
|
|
56
|
+
'...............###....##.....',
|
|
57
|
+
'..............##.##...##.....',
|
|
58
|
+
'..............##.##....#.....',
|
|
59
|
+
'.............##.#####........',
|
|
60
|
+
'.............########........',
|
|
61
|
+
'.............####.##.........',
|
|
62
|
+
'...................##........',
|
|
63
|
+
'...................##........',
|
|
64
|
+
'...............#######.......',
|
|
65
|
+
'............##########.......',
|
|
66
|
+
'...........#####....##.......',
|
|
67
|
+
'...........##.......##.......',
|
|
68
|
+
'............##......##.......',
|
|
69
|
+
'............##......##.......',
|
|
70
|
+
'............##......##.......',
|
|
71
|
+
'............##......##.......',
|
|
72
|
+
'............##......##.......',
|
|
73
|
+
'.............##.....##.......',
|
|
74
|
+
'.............##.....##.......',
|
|
75
|
+
'.............##.....##.......',
|
|
76
|
+
'....................##.......',
|
|
77
|
+
'....................##.......',
|
|
78
|
+
'....................##.......',
|
|
79
|
+
'....................##.......',
|
|
80
|
+
'....................##.......',
|
|
81
|
+
'....................##.......',
|
|
82
|
+
'....................#........',
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
'.............................',
|
|
86
|
+
'.............................',
|
|
87
|
+
'.............................',
|
|
88
|
+
'.............................',
|
|
89
|
+
'.............................',
|
|
90
|
+
'.............................',
|
|
91
|
+
'..............#..............',
|
|
92
|
+
'............#####............',
|
|
93
|
+
'...........###.###...........',
|
|
94
|
+
'...........##...##...........',
|
|
95
|
+
'..........##.....#...........',
|
|
96
|
+
'...........##...##...........',
|
|
97
|
+
'.......###############.......',
|
|
98
|
+
'.......###############.......',
|
|
99
|
+
'.......##....###....##.......',
|
|
100
|
+
'........##....#.....##.......',
|
|
101
|
+
'........##....##....#........',
|
|
102
|
+
'.........##...##...##........',
|
|
103
|
+
'.........##...##...##........',
|
|
104
|
+
'..........#...##...##........',
|
|
105
|
+
'..............##.............',
|
|
106
|
+
'..............##.............',
|
|
107
|
+
'..............##.............',
|
|
108
|
+
'..............##.............',
|
|
109
|
+
'..........###########........',
|
|
110
|
+
'......#######...########.....',
|
|
111
|
+
'.....#####..........####.....',
|
|
112
|
+
'.....##...............##.....',
|
|
113
|
+
'......##..............##.....',
|
|
114
|
+
'......##.............##......',
|
|
115
|
+
'......##.............##......',
|
|
116
|
+
'......##.............##......',
|
|
117
|
+
'.......##...........##.......',
|
|
118
|
+
'.......##...........##.......',
|
|
119
|
+
'.......##...........##.......',
|
|
120
|
+
'.......##....................',
|
|
121
|
+
],
|
|
122
|
+
[
|
|
123
|
+
'.............................',
|
|
124
|
+
'............#####............',
|
|
125
|
+
'...........#######...........',
|
|
126
|
+
'###.......###...##...........',
|
|
127
|
+
'.####.....##....##.......####',
|
|
128
|
+
'...####...##....##....######.',
|
|
129
|
+
'.....########..##########....',
|
|
130
|
+
'.......###############.......',
|
|
131
|
+
'............#####............',
|
|
132
|
+
'.............##..............',
|
|
133
|
+
'.............##..............',
|
|
134
|
+
'..............#..............',
|
|
135
|
+
'..............#..............',
|
|
136
|
+
'..............#..............',
|
|
137
|
+
'..............#..............',
|
|
138
|
+
'..............##.............',
|
|
139
|
+
'..............##.............',
|
|
140
|
+
'..............##.............',
|
|
141
|
+
'..............##.............',
|
|
142
|
+
'.............###.............',
|
|
143
|
+
'............#####............',
|
|
144
|
+
'............##.###...........',
|
|
145
|
+
'...........##...##...........',
|
|
146
|
+
'...........##....##..........',
|
|
147
|
+
'..........##.....##..........',
|
|
148
|
+
'.........##.......##.........',
|
|
149
|
+
'.........##.......##.........',
|
|
150
|
+
'.........##........##........',
|
|
151
|
+
'........###........##........',
|
|
152
|
+
'........##.........##........',
|
|
153
|
+
'........##.........##........',
|
|
154
|
+
'........##.........##........',
|
|
155
|
+
'.......##...........##.......',
|
|
156
|
+
'.......##...........##.......',
|
|
157
|
+
'.......##...........##.......',
|
|
158
|
+
'.............................',
|
|
159
|
+
],
|
|
160
|
+
[
|
|
161
|
+
'.............................',
|
|
162
|
+
'.............................',
|
|
163
|
+
'.............................',
|
|
164
|
+
'.............................',
|
|
165
|
+
'.............................',
|
|
166
|
+
'.............................',
|
|
167
|
+
'.............................',
|
|
168
|
+
'.............................',
|
|
169
|
+
'.............................',
|
|
170
|
+
'.............................',
|
|
171
|
+
'.............................',
|
|
172
|
+
'............###..............',
|
|
173
|
+
'...........######............',
|
|
174
|
+
'..........###..###...........',
|
|
175
|
+
'..........##....##...........',
|
|
176
|
+
'..........##....##...........',
|
|
177
|
+
'..........##...###...........',
|
|
178
|
+
'...........######............',
|
|
179
|
+
'............####.............',
|
|
180
|
+
'...........#####.............',
|
|
181
|
+
'..........#######............',
|
|
182
|
+
'.........#########...........',
|
|
183
|
+
'........##########...........',
|
|
184
|
+
'........###..######..........',
|
|
185
|
+
'.............##..##..........',
|
|
186
|
+
'.....###.....##......###.....',
|
|
187
|
+
'.....######..##...#####......',
|
|
188
|
+
'......#################......',
|
|
189
|
+
'......##...#######...##......',
|
|
190
|
+
'......##.....##.....##.......',
|
|
191
|
+
'.......##...........##.......',
|
|
192
|
+
'.......##..........##........',
|
|
193
|
+
'.......###.........##........',
|
|
194
|
+
'........##.........##........',
|
|
195
|
+
'........##.........#.........',
|
|
196
|
+
'.............................',
|
|
197
|
+
],
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
function pickIndexes(srcSize, maxSize) {
|
|
201
|
+
if (srcSize <= maxSize) {
|
|
202
|
+
return Array.from({ length: srcSize }, (_, index) => index)
|
|
203
|
+
}
|
|
204
|
+
return Array.from({ length: maxSize }, (_, index) =>
|
|
205
|
+
Math.round((index * (srcSize - 1)) / (maxSize - 1)),
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function getUpyachkaFrame(
|
|
210
|
+
index,
|
|
211
|
+
terminalColumns = 80,
|
|
212
|
+
terminalRows = 24,
|
|
213
|
+
) {
|
|
214
|
+
const maxColumns = Math.max(8, Math.floor((terminalColumns - 1) / 2))
|
|
215
|
+
const maxRows = Math.max(8, (terminalRows || 24) - 2)
|
|
216
|
+
const columns = pickIndexes(FRAME_WIDTH, maxColumns)
|
|
217
|
+
const rows = pickIndexes(FRAME_HEIGHT, maxRows)
|
|
218
|
+
const frame = FRAMES[index % FRAMES.length]
|
|
219
|
+
|
|
220
|
+
return rows
|
|
221
|
+
.map((y) =>
|
|
222
|
+
columns.map((x) => (frame[y][x] === '#' ? '██' : ' ')).join(''),
|
|
223
|
+
)
|
|
224
|
+
.join('\n')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function getUpyachkaFrameCount() {
|
|
228
|
+
return FRAMES.length
|
|
229
|
+
}
|