bimba-cli 0.7.29 → 0.7.31

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/serve.js +108 -95
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.7.29",
3
+ "version": "0.7.31",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/HeapVoid/bimba.git"
package/serve.js CHANGED
@@ -3,7 +3,6 @@ import * as compiler from 'imba/compiler'
3
3
  import { mkdirSync, watch, existsSync, statSync, writeFileSync, realpathSync } from 'fs'
4
4
  import path from 'path'
5
5
  import { theme } from './utils.js'
6
- import { printerr } from './plugin.js'
7
6
 
8
7
  // ─── HMR Client (injected into browser) ──────────────────────────────────────
9
8
 
@@ -628,32 +627,63 @@ export function serve(entrypoint, flags) {
628
627
  const srcDir = path.dirname(entrypoint)
629
628
  const sockets = new Set()
630
629
 
631
- // ── Status line (prints current compile result, fades out on success) ──────
630
+ // ── Live status block (shows only the current compile state) ───────────────
632
631
 
633
- let _fadeTimers = []
634
- let _fadeId = 0
632
+ let _statusRows = 0
633
+ let _statusTimer = null
635
634
  let _statusFile = null
636
- let _statusSaved = false
637
- let _statusKind = null
638
635
  const _isTTY = process.stdout.isTTY
639
636
 
640
- function cancelFade() {
641
- _fadeTimers.forEach(t => clearTimeout(t))
642
- _fadeTimers = []
637
+ function stripAnsi(text) {
638
+ return String(text).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '')
639
+ }
640
+
641
+ function renderedRows(lines) {
642
+ const columns = Math.max(1, process.stdout.columns || 80)
643
+ return lines.reduce((total, line) => {
644
+ const length = stripAnsi(line).length
645
+ return total + Math.max(1, Math.ceil(length / columns))
646
+ }, 0)
643
647
  }
644
648
 
645
649
  function clearStatus(file) {
646
- if (file && _statusFile && _statusFile !== file) return
647
- cancelFade()
648
- if (_statusSaved) {
649
- process.stdout.write('\x1b[u\x1b[J')
650
- _statusSaved = false
650
+ if (file && _statusFile && _statusFile !== file) return false
651
+ if (_statusTimer) {
652
+ clearTimeout(_statusTimer)
653
+ _statusTimer = null
651
654
  }
655
+ if (_isTTY && _statusRows) {
656
+ process.stdout.write(`\x1b[${_statusRows}A\r\x1b[J`)
657
+ }
658
+ _statusRows = 0
652
659
  _statusFile = null
653
- _statusKind = null
660
+ return true
661
+ }
662
+
663
+ function formatErrorLines(errors) {
664
+ const lines = []
665
+ for (const err of errors || []) {
666
+ const message = errorMessage(err)
667
+ const line = errorLine(err)
668
+ lines.push(` ${theme.error(' ' + message + ' ')}${line != null ? theme.margin(` line ${line + 1} `) : ''}`)
669
+ const snippet = errorSnippet(err)
670
+ if (snippet && snippet !== message) {
671
+ lines.push(...String(snippet).split('\n').slice(0, 6).map(item => ` ${theme.code(item)}`))
672
+ }
673
+ lines.push('')
674
+ }
675
+ return lines
654
676
  }
655
677
 
656
- function printStatus(file, state, errors) {
678
+ function statusLines(file, state, errors) {
679
+ const now = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
680
+ const status = state === 'ok' ? theme.success(' ok ') : theme.failure(' fail ')
681
+ const lines = [` ${theme.folder(now)} ${theme.filename(file)} ${status}`]
682
+ if (errors?.length) lines.push('', ...formatErrorLines(errors))
683
+ return lines
684
+ }
685
+
686
+ function printStatus(file, state, errors, options = {}) {
657
687
  // non-TTY (pipes, Claude Code bash, CI): plain newline-terminated output,
658
688
  // no ANSI cursor tricks, no fade-out — so logs stay readable.
659
689
  if (!_isTTY) {
@@ -670,74 +700,20 @@ export function serve(entrypoint, flags) {
670
700
  return
671
701
  }
672
702
 
673
- cancelFade()
674
- if (_statusSaved) {
675
- process.stdout.write('\x1b[u\x1b[J')
676
- _statusSaved = false
677
- }
678
- const now = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
679
- const status = state === 'ok' ? theme.success(' ok ') : theme.failure(' fail ')
680
-
681
- process.stdout.write('\x1b[s')
682
- _statusSaved = true
703
+ clearStatus()
683
704
  _statusFile = file
684
- _statusKind = state
705
+ const lines = statusLines(file, state, errors)
706
+ process.stdout.write(lines.join('\n') + '\n')
707
+ _statusRows = renderedRows(lines)
685
708
 
686
- if (errors?.length) {
687
- process.stdout.write(` ${theme.folder(now)} ${theme.filename(file)} ${status}\n`)
688
- for (const err of errors) {
689
- try { printerr(err) } catch(_) { process.stdout.write(' ' + err.message + '\n') }
690
- }
691
- } else {
692
- const myId = ++_fadeId
693
- const plainLine = ` ${now} ${file} ok `
694
- const total = plainLine.length
695
- process.stdout.write(` ${theme.folder(now)} ${theme.filename(file)} ${status}`)
696
- for (let i = 1; i <= total; i++) {
697
- _fadeTimers.push(setTimeout(() => {
698
- if (_fadeId !== myId) return
699
- process.stdout.write('\x1b[1D \x1b[1D')
700
- if (i === total) {
701
- _statusSaved = false
702
- _statusFile = null
703
- _statusKind = null
704
- }
705
- }, 5000 + i * 22))
706
- }
707
- }
709
+ if (options.clearAfter) _statusTimer = setTimeout(() => clearStatus(file), options.clearAfter)
708
710
  }
709
711
 
710
712
  // ── File watcher ───────────────────────────────────────────────────────────
711
713
 
712
714
  const _activeErrors = new Map()
713
-
714
- function renderErrorPanel() {
715
- if (!_isTTY) return false
716
-
717
- cancelFade()
718
- if (_statusSaved) {
719
- process.stdout.write('\x1b[u\x1b[J')
720
- _statusSaved = false
721
- }
722
- _statusFile = null
723
- _statusKind = null
724
-
725
- if (!_activeErrors.size) return false
726
-
727
- process.stdout.write('\x1b[s')
728
- _statusSaved = true
729
- _statusKind = 'errors'
730
-
731
- for (const item of _activeErrors.values()) {
732
- const file = item.file
733
- process.stdout.write(` ${theme.folder(item.time)} ${theme.filename(file)} ${theme.failure(' fail ')}\n`)
734
- for (const err of item.errors) {
735
- try { printerr(err) } catch(_) { process.stdout.write(' ' + err.message + '\n') }
736
- }
737
- }
738
-
739
- return true
740
- }
715
+ const _terminalErrors = new Map()
716
+ const TERMINAL_DUPLICATE_MS = 5000
741
717
 
742
718
  function broadcast(payload) {
743
719
  const msg = JSON.stringify(payload)
@@ -774,6 +750,12 @@ export function serve(entrypoint, flags) {
774
750
  return Array.from(variants).filter(Boolean)
775
751
  }
776
752
 
753
+ function terminalErrorKey(file) {
754
+ const variants = fileVariants(file)
755
+ const rooted = variants.find(variant => srcRel && variant.startsWith(srcRel + '/'))
756
+ return `path:${rooted || variants[0] || normalizeFile(file)}`
757
+ }
758
+
777
759
  function fileCandidates(file) {
778
760
  const candidates = []
779
761
  for (const variant of fileVariants(file)) {
@@ -862,9 +844,34 @@ export function serve(entrypoint, flags) {
862
844
  .join('\n---\n')
863
845
  }
864
846
 
847
+ function terminalErrorSignature(errors) {
848
+ return serializeErrors(errors)
849
+ .map(error => error.message)
850
+ .join('\n---\n')
851
+ }
852
+
853
+ function renderActiveErrors() {
854
+ if (!_isTTY) return false
855
+ if (!_activeErrors.size) {
856
+ clearStatus()
857
+ return false
858
+ }
859
+
860
+ clearStatus()
861
+ const lines = []
862
+ for (const item of _activeErrors.values()) {
863
+ if (lines.length) lines.push('')
864
+ lines.push(...statusLines(item.file, 'fail', item.errors))
865
+ }
866
+ process.stdout.write(lines.join('\n') + '\n')
867
+ _statusFile = null
868
+ _statusRows = renderedRows(lines)
869
+ return true
870
+ }
871
+
865
872
  function showTrackedError(item) {
866
873
  const file = item.file
867
- if (_isTTY) renderErrorPanel()
874
+ if (_isTTY) renderActiveErrors()
868
875
  else printStatus(file, 'fail', item.errors)
869
876
  broadcast({ type: 'error', file, time: item.time, errors: item.payload })
870
877
  }
@@ -872,23 +879,32 @@ export function serve(entrypoint, flags) {
872
879
  function reportError(file, errors) {
873
880
  const display = normalizeFile(file)
874
881
  const key = errorKey(display)
882
+ const terminalKey = terminalErrorKey(display)
875
883
  const list = Array.isArray(errors) ? errors : [errors]
876
884
  const signature = errorSignature(list)
885
+ const printSignature = terminalErrorSignature(list)
877
886
  const previous = takeError(display)
887
+ const now = Date.now()
888
+ const recent = _terminalErrors.get(terminalKey)
878
889
  const duplicate = previous?.signature === signature
890
+ || previous?.printSignature === printSignature
891
+ || (recent?.signature === printSignature && now - recent.time < TERMINAL_DUPLICATE_MS)
879
892
 
880
893
  const item = {
881
894
  file: display,
882
895
  signature,
896
+ printSignature,
883
897
  errors: list,
884
898
  payload: serializeErrors(list),
885
899
  time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
886
900
  }
887
901
  _activeErrors.set(key, item)
902
+ _terminalErrors.set(terminalKey, { signature: printSignature, time: now })
888
903
 
889
- // The terminal is append-only in many real shells. Repeated reports of the
890
- // same active error update the browser overlay, but must not print again.
904
+ // Repeated reports of the same active error update the live status and
905
+ // browser overlay, but do not create another terminal entry.
891
906
  if (duplicate) {
907
+ if (_isTTY) renderActiveErrors()
892
908
  broadcast({ type: 'error', file: display, time: item.time, errors: item.payload })
893
909
  return
894
910
  }
@@ -908,27 +924,22 @@ export function serve(entrypoint, flags) {
908
924
 
909
925
  function clearError(file) {
910
926
  const key = file ? normalizeFile(file) : null
911
- const hadPanel = _statusKind === 'errors'
912
927
  const wasStatusFile = key && _statusFile && sameFile(_statusFile, key)
913
928
  const hadError = key ? !!takeError(key) : _activeErrors.size > 0
914
929
 
915
- if (!key) _activeErrors.clear()
930
+ if (!key) {
931
+ _activeErrors.clear()
932
+ _terminalErrors.clear()
933
+ } else if (hadError) {
934
+ _terminalErrors.delete(terminalErrorKey(key))
935
+ }
916
936
 
917
937
  let showedNext = false
918
- if (_isTTY && (hadError || wasStatusFile || hadPanel)) {
919
- showedNext = renderErrorPanel()
920
- } else if (!key || hadError || wasStatusFile) {
921
- clearStatus(key)
922
- }
938
+ if (_isTTY && _activeErrors.size) showedNext = renderActiveErrors()
939
+ else if (!key || hadError || wasStatusFile) clearStatus(key)
923
940
 
924
941
  broadcast({ type: 'clear-error', file: key })
925
942
 
926
- if (!_isTTY && wasStatusFile && _activeErrors.size) {
927
- const nextItem = Array.from(_activeErrors.values()).at(-1)
928
- showTrackedError(nextItem)
929
- showedNext = true
930
- }
931
-
932
943
  return { cleared: hadError || wasStatusFile, file: key, showedNext }
933
944
  }
934
945
 
@@ -937,7 +948,9 @@ export function serve(entrypoint, flags) {
937
948
  const result = clearError(key)
938
949
  const active = _activeErrors.size
939
950
  const shouldPrint = result?.cleared && !result.showedNext && !active
940
- if (shouldPrint) printStatus(key, 'ok')
951
+ if (shouldPrint) {
952
+ printStatus(key, 'ok', null, { clearAfter: 3500 })
953
+ }
941
954
  return { cleared: !!result?.cleared, printed: !!shouldPrint, showedNext: !!result?.showedNext, active }
942
955
  }
943
956
 
@@ -1011,7 +1024,7 @@ export function serve(entrypoint, flags) {
1011
1024
  // No change at all — skip
1012
1025
  if (out.changeType === 'none' || out.changeType === 'cached') return
1013
1026
 
1014
- if (!success.printed && !success.showedNext && !success.active) printStatus(rel, 'ok')
1027
+ if (!success.printed && !success.showedNext && !success.active) printStatus(rel, 'ok', null, { clearAfter: 3500 })
1015
1028
  broadcast({ type: 'update', file: rel, slots: out.slots || 'shifted' })
1016
1029
  } catch(e) {
1017
1030
  if (!isCurrentChange(file, version)) return