@itcase/config 1.6.79 → 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 +342 -44
- 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
|
|
|
@@ -142,14 +149,25 @@ async function main(argv) {
|
|
|
142
149
|
|
|
143
150
|
ensureGitIndexReady(cwd)
|
|
144
151
|
|
|
152
|
+
const total = activePlans.length
|
|
153
|
+
process.stdout.write(`Creating ${total} commit(s)…\n`)
|
|
154
|
+
|
|
145
155
|
let created = 0
|
|
146
|
-
for (
|
|
156
|
+
for (let index = 0; index < total; index++) {
|
|
157
|
+
const plan = activePlans[index]
|
|
158
|
+
const step = `[${index + 1}/${total}]`
|
|
147
159
|
const planFiles = plan.files.filter((file) => fileHasChanges(cwd, file))
|
|
148
160
|
if (!planFiles.length) {
|
|
149
|
-
process.stdout.write(
|
|
161
|
+
process.stdout.write(`${step} skip empty: ${plan.message}\n`)
|
|
150
162
|
continue
|
|
151
163
|
}
|
|
152
164
|
|
|
165
|
+
const startedAt = Date.now()
|
|
166
|
+
const fileLabel =
|
|
167
|
+
planFiles.length === 1 ? '1 file' : `${planFiles.length} files`
|
|
168
|
+
process.stdout.write(`${step} ${plan.message}\n`)
|
|
169
|
+
process.stdout.write(` staging ${fileLabel}…\n`)
|
|
170
|
+
|
|
153
171
|
// Drop leftover index entries so only this plan's files are committed.
|
|
154
172
|
git(cwd, ['reset', '-q', 'HEAD'])
|
|
155
173
|
stageFiles(cwd, planFiles)
|
|
@@ -162,6 +180,7 @@ async function main(argv) {
|
|
|
162
180
|
)
|
|
163
181
|
}
|
|
164
182
|
|
|
183
|
+
process.stdout.write(' git commit (husky)…\n')
|
|
165
184
|
try {
|
|
166
185
|
git(cwd, ['commit', '-m', plan.message], { inherit: true })
|
|
167
186
|
} catch (error) {
|
|
@@ -170,6 +189,7 @@ async function main(argv) {
|
|
|
170
189
|
})
|
|
171
190
|
}
|
|
172
191
|
created++
|
|
192
|
+
process.stdout.write(` done (${formatElapsed(startedAt)})\n`)
|
|
173
193
|
}
|
|
174
194
|
process.stdout.write(`Created ${created} commit(s).\n`)
|
|
175
195
|
|
|
@@ -200,6 +220,8 @@ function parseArgs(argv) {
|
|
|
200
220
|
else if (arg === '--staged') option.staged = true
|
|
201
221
|
else if (arg === '--cat' || arg === '--nyan') option.sprite = 'cat'
|
|
202
222
|
else if (arg === '--banana') option.sprite = 'banana'
|
|
223
|
+
else if (arg === '--upyachka') option.sprite = 'upyachka'
|
|
224
|
+
else if (arg === '--pacman') option.sprite = 'pacman'
|
|
203
225
|
else if (arg === '--type') option.type = argv[++i] || ''
|
|
204
226
|
else if (arg === '--model') option.model = argv[++i] || ''
|
|
205
227
|
else if (arg.startsWith('--type=')) option.type = arg.slice(7)
|
|
@@ -210,8 +232,9 @@ function parseArgs(argv) {
|
|
|
210
232
|
}
|
|
211
233
|
|
|
212
234
|
function pickSprite(forced) {
|
|
213
|
-
|
|
214
|
-
|
|
235
|
+
const sprites = ['cat', 'banana', 'upyachka', 'pacman']
|
|
236
|
+
if (sprites.includes(forced)) return forced
|
|
237
|
+
return sprites[Math.floor(Math.random() * sprites.length)]
|
|
215
238
|
}
|
|
216
239
|
|
|
217
240
|
function assertItcaseConfigPackage(cwd) {
|
|
@@ -447,49 +470,109 @@ async function withAgentProgress(context, callback) {
|
|
|
447
470
|
let phase = 'early'
|
|
448
471
|
let receivedChars = 0
|
|
449
472
|
let frameIndex = 0
|
|
473
|
+
const logLines = []
|
|
474
|
+
const streamByKey = { assistant: -1, thinking: -1 }
|
|
475
|
+
|
|
476
|
+
const elapsedSeconds = () => Math.floor((Date.now() - startedAt) / 1000)
|
|
450
477
|
|
|
451
|
-
const onAgentActivity = (
|
|
452
|
-
if (charCount > 0) {
|
|
453
|
-
receivedChars = charCount
|
|
478
|
+
const onAgentActivity = (activity = {}) => {
|
|
479
|
+
if (activity.charCount > 0) {
|
|
480
|
+
receivedChars = activity.charCount
|
|
454
481
|
phase = 'receiving'
|
|
455
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'
|
|
456
497
|
}
|
|
457
498
|
|
|
458
499
|
const currentLabel = () => {
|
|
500
|
+
const elapsed = elapsedSeconds()
|
|
459
501
|
if (phase === 'receiving') {
|
|
460
502
|
return `Receiving response (${receivedChars} chars)`
|
|
461
503
|
}
|
|
462
|
-
if (
|
|
504
|
+
if (elapsed >= AGENT_STILL_WORKING_S) return 'Agent still working'
|
|
505
|
+
if (phase === 'waiting' || phase === 'working') {
|
|
463
506
|
return AGENT_WAIT_STEPS[waitStepIndex % AGENT_WAIT_STEPS.length]
|
|
464
507
|
}
|
|
465
508
|
return earlySteps[Math.min(earlyStepIndex, earlySteps.length - 1)]
|
|
466
509
|
}
|
|
467
510
|
|
|
468
511
|
const formatText = () => {
|
|
469
|
-
const
|
|
470
|
-
|
|
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
|
+
}
|
|
471
526
|
}
|
|
472
527
|
|
|
473
528
|
if (!process.stderr.isTTY || context.verbose) {
|
|
474
529
|
process.stderr.write(`${formatText()}\n`)
|
|
475
|
-
|
|
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
|
+
}
|
|
476
539
|
}
|
|
477
540
|
|
|
478
541
|
const columns = process.stderr.columns || process.stdout.columns || 80
|
|
479
542
|
const rows = process.stderr.rows || process.stdout.rows || 24
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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]
|
|
485
563
|
process.stderr.write('\x1b[?7l')
|
|
486
564
|
|
|
487
565
|
const render = () => {
|
|
488
|
-
|
|
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
|
+
)
|
|
489
572
|
frameIndex++
|
|
490
573
|
}
|
|
491
574
|
|
|
492
|
-
const frameTimer = setInterval(render, frameMs)
|
|
575
|
+
const frameTimer = setInterval(render, sprite.frameMs)
|
|
493
576
|
const stepTimer = setInterval(() => {
|
|
494
577
|
if (phase === 'early') {
|
|
495
578
|
if (earlyStepIndex < earlySteps.length - 1) {
|
|
@@ -497,14 +580,14 @@ async function withAgentProgress(context, callback) {
|
|
|
497
580
|
} else {
|
|
498
581
|
phase = 'waiting'
|
|
499
582
|
}
|
|
500
|
-
} else if (phase
|
|
583
|
+
} else if (phase !== 'receiving') {
|
|
501
584
|
waitStepIndex++
|
|
502
585
|
}
|
|
503
586
|
}, AGENT_STEP_MS)
|
|
504
587
|
|
|
505
588
|
try {
|
|
506
589
|
render()
|
|
507
|
-
return await callback({ onAgentActivity })
|
|
590
|
+
return await callback({ onAgentActivity: reportActivity })
|
|
508
591
|
} finally {
|
|
509
592
|
clearInterval(frameTimer)
|
|
510
593
|
clearInterval(stepTimer)
|
|
@@ -513,6 +596,16 @@ async function withAgentProgress(context, callback) {
|
|
|
513
596
|
}
|
|
514
597
|
}
|
|
515
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
|
+
|
|
516
609
|
function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
|
|
517
610
|
const cmd = resolveAgent()
|
|
518
611
|
const args = [
|
|
@@ -520,7 +613,8 @@ function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
|
|
|
520
613
|
'--mode',
|
|
521
614
|
'ask',
|
|
522
615
|
'--output-format',
|
|
523
|
-
'
|
|
616
|
+
'stream-json',
|
|
617
|
+
'--stream-partial-output',
|
|
524
618
|
'--trust',
|
|
525
619
|
'--workspace',
|
|
526
620
|
cwd,
|
|
@@ -531,6 +625,9 @@ function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
|
|
|
531
625
|
args.push(prompt)
|
|
532
626
|
|
|
533
627
|
if (verbose) process.stderr.write(`Using: ${cmd}\n`)
|
|
628
|
+
if (onAgentActivity) {
|
|
629
|
+
onAgentActivity({ log: `Sending prompt (${prompt.length} chars)` })
|
|
630
|
+
}
|
|
534
631
|
|
|
535
632
|
return new Promise((resolve, reject) => {
|
|
536
633
|
const child = spawn(cmd, args, {
|
|
@@ -538,44 +635,240 @@ function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
|
|
|
538
635
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
539
636
|
})
|
|
540
637
|
|
|
541
|
-
|
|
638
|
+
const stream = {
|
|
639
|
+
buffer: '',
|
|
640
|
+
lastAssistant: '',
|
|
641
|
+
raw: '',
|
|
642
|
+
result: '',
|
|
643
|
+
thinking: '',
|
|
644
|
+
}
|
|
542
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)
|
|
543
668
|
|
|
544
669
|
child.stdout.on('data', (chunk) => {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+
}
|
|
548
676
|
})
|
|
549
677
|
child.stderr.on('data', (chunk) => {
|
|
550
|
-
|
|
551
|
-
|
|
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)
|
|
552
683
|
})
|
|
553
684
|
|
|
554
|
-
const timeout = setTimeout(() => {
|
|
555
|
-
child.kill('SIGTERM')
|
|
556
|
-
reject(
|
|
557
|
-
new Error(
|
|
558
|
-
`Cursor Agent timed out after ${AGENT_TIMEOUT_MS / 1000}s. Check agent login / network.`,
|
|
559
|
-
),
|
|
560
|
-
)
|
|
561
|
-
}, AGENT_TIMEOUT_MS)
|
|
562
|
-
|
|
563
685
|
child.on('error', (error) => {
|
|
564
|
-
|
|
565
|
-
reject(error)
|
|
686
|
+
finish(error)
|
|
566
687
|
})
|
|
567
688
|
|
|
568
689
|
child.on('close', (code) => {
|
|
569
|
-
|
|
690
|
+
consumeAgentStdout(stream, '\n')
|
|
691
|
+
const raw = (
|
|
692
|
+
stream.result ||
|
|
693
|
+
stream.lastAssistant ||
|
|
694
|
+
stream.buffer ||
|
|
695
|
+
stream.raw
|
|
696
|
+
).trim()
|
|
570
697
|
if (code) {
|
|
571
|
-
|
|
698
|
+
finish(new Error(formatAgentError(stderr, raw)))
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
if (!raw) {
|
|
702
|
+
finish(new Error(formatAgentError(stderr, '')))
|
|
572
703
|
return
|
|
573
704
|
}
|
|
574
|
-
|
|
705
|
+
finish(null, raw)
|
|
575
706
|
})
|
|
576
707
|
})
|
|
577
708
|
}
|
|
578
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
|
+
|
|
579
872
|
function formatAgentError(stderr, stdout) {
|
|
580
873
|
const text = (stderr || stdout || AGENT_MISSING).trim()
|
|
581
874
|
if (/authentication required/i.test(text)) {
|
|
@@ -887,6 +1180,11 @@ function sleepSync(ms) {
|
|
|
887
1180
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
|
|
888
1181
|
}
|
|
889
1182
|
|
|
1183
|
+
function formatElapsed(startedAt) {
|
|
1184
|
+
const elapsed = Math.max(0, Math.floor((Date.now() - startedAt) / 1000))
|
|
1185
|
+
return `${elapsed}s`
|
|
1186
|
+
}
|
|
1187
|
+
|
|
890
1188
|
function indexLockPath(cwd) {
|
|
891
1189
|
const result = spawnSync('git', ['rev-parse', '--git-path', 'index.lock'], {
|
|
892
1190
|
cwd: cwd,
|
|
@@ -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
|
+
}
|