@nforma.ai/nforma 0.2.1

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 (215) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +1024 -0
  3. package/agents/qgsd-codebase-mapper.md +764 -0
  4. package/agents/qgsd-debugger.md +1201 -0
  5. package/agents/qgsd-executor.md +472 -0
  6. package/agents/qgsd-integration-checker.md +443 -0
  7. package/agents/qgsd-phase-researcher.md +502 -0
  8. package/agents/qgsd-plan-checker.md +643 -0
  9. package/agents/qgsd-planner.md +1182 -0
  10. package/agents/qgsd-project-researcher.md +621 -0
  11. package/agents/qgsd-quorum-orchestrator.md +628 -0
  12. package/agents/qgsd-quorum-slot-worker.md +41 -0
  13. package/agents/qgsd-quorum-synthesizer.md +133 -0
  14. package/agents/qgsd-quorum-test-worker.md +37 -0
  15. package/agents/qgsd-quorum-worker.md +161 -0
  16. package/agents/qgsd-research-synthesizer.md +239 -0
  17. package/agents/qgsd-roadmapper.md +660 -0
  18. package/agents/qgsd-verifier.md +628 -0
  19. package/bin/accept-debug-invariant.cjs +165 -0
  20. package/bin/account-manager.cjs +719 -0
  21. package/bin/aggregate-requirements.cjs +466 -0
  22. package/bin/analyze-assumptions.cjs +757 -0
  23. package/bin/analyze-state-space.cjs +921 -0
  24. package/bin/attribute-trace-divergence.cjs +150 -0
  25. package/bin/auth-drivers/gh-cli.cjs +93 -0
  26. package/bin/auth-drivers/index.cjs +46 -0
  27. package/bin/auth-drivers/pool.cjs +67 -0
  28. package/bin/auth-drivers/simple.cjs +95 -0
  29. package/bin/autoClosePtoF.cjs +110 -0
  30. package/bin/blessed-terminal.cjs +350 -0
  31. package/bin/build-phase-index.cjs +472 -0
  32. package/bin/call-quorum-slot.cjs +541 -0
  33. package/bin/ccr-secure-config.cjs +99 -0
  34. package/bin/ccr-secure-start.cjs +83 -0
  35. package/bin/check-bundled-sdks.cjs +177 -0
  36. package/bin/check-coverage-guard.cjs +112 -0
  37. package/bin/check-liveness-fairness.cjs +95 -0
  38. package/bin/check-mcp-health.cjs +123 -0
  39. package/bin/check-provider-health.cjs +395 -0
  40. package/bin/check-results-exit.cjs +24 -0
  41. package/bin/check-spec-sync.cjs +360 -0
  42. package/bin/check-trace-redaction.cjs +271 -0
  43. package/bin/check-trace-schema-drift.cjs +99 -0
  44. package/bin/compareDrift.cjs +21 -0
  45. package/bin/conformance-schema.cjs +12 -0
  46. package/bin/count-scenarios.cjs +420 -0
  47. package/bin/debt-dedup.cjs +144 -0
  48. package/bin/debt-ledger.cjs +61 -0
  49. package/bin/debt-retention.cjs +76 -0
  50. package/bin/debt-state-machine.cjs +80 -0
  51. package/bin/detect-coverage-gaps.cjs +204 -0
  52. package/bin/detect-project-intent.cjs +362 -0
  53. package/bin/export-prism-constants.cjs +164 -0
  54. package/bin/extract-annotations.cjs +633 -0
  55. package/bin/extractFormalExpected.cjs +104 -0
  56. package/bin/fingerprint-drift.cjs +24 -0
  57. package/bin/fingerprint-issue.cjs +46 -0
  58. package/bin/formal-core.cjs +519 -0
  59. package/bin/formal-ref-linker.cjs +141 -0
  60. package/bin/formal-test-sync.cjs +788 -0
  61. package/bin/generate-formal-specs.cjs +588 -0
  62. package/bin/generate-petri-net.cjs +397 -0
  63. package/bin/generate-phase-spec.cjs +249 -0
  64. package/bin/generate-proposed-changes.cjs +194 -0
  65. package/bin/generate-tla-cfg.cjs +122 -0
  66. package/bin/generate-traceability-matrix.cjs +701 -0
  67. package/bin/generate-triage-bundle.cjs +300 -0
  68. package/bin/gh-account-rotate.cjs +34 -0
  69. package/bin/initialize-model-registry.cjs +105 -0
  70. package/bin/install-formal-tools.cjs +382 -0
  71. package/bin/install.js +2424 -0
  72. package/bin/isNumericThreshold.cjs +34 -0
  73. package/bin/issue-classifier.cjs +151 -0
  74. package/bin/levenshtein.cjs +74 -0
  75. package/bin/lint-formal-models.cjs +580 -0
  76. package/bin/load-baseline-requirements.cjs +275 -0
  77. package/bin/manage-agents-core.cjs +815 -0
  78. package/bin/migrate-formal-dir.cjs +172 -0
  79. package/bin/migrate-planning.cjs +206 -0
  80. package/bin/migrate-to-slots.cjs +255 -0
  81. package/bin/nForma.cjs +2726 -0
  82. package/bin/observe-config.cjs +353 -0
  83. package/bin/observe-debt-writer.cjs +140 -0
  84. package/bin/observe-handler-grafana.cjs +128 -0
  85. package/bin/observe-handler-internal.cjs +301 -0
  86. package/bin/observe-handler-logstash.cjs +153 -0
  87. package/bin/observe-handler-prometheus.cjs +185 -0
  88. package/bin/observe-handlers.cjs +436 -0
  89. package/bin/observe-registry.cjs +131 -0
  90. package/bin/observe-render.cjs +168 -0
  91. package/bin/planning-paths.cjs +167 -0
  92. package/bin/polyrepo.cjs +560 -0
  93. package/bin/prism-priority.cjs +153 -0
  94. package/bin/probe-quorum-slots.cjs +167 -0
  95. package/bin/promote-model.cjs +225 -0
  96. package/bin/propose-debug-invariants.cjs +165 -0
  97. package/bin/providers.json +392 -0
  98. package/bin/pty-proxy.py +129 -0
  99. package/bin/qgsd-solve.cjs +2477 -0
  100. package/bin/quorum-consensus-gate.cjs +238 -0
  101. package/bin/quorum-formal-context.cjs +183 -0
  102. package/bin/quorum-slot-dispatch.cjs +934 -0
  103. package/bin/read-policy.cjs +60 -0
  104. package/bin/requirement-map.cjs +63 -0
  105. package/bin/requirements-core.cjs +247 -0
  106. package/bin/resolve-cli.cjs +101 -0
  107. package/bin/review-mcp-logs.cjs +294 -0
  108. package/bin/run-account-manager-tlc.cjs +188 -0
  109. package/bin/run-account-pool-alloy.cjs +158 -0
  110. package/bin/run-alloy.cjs +153 -0
  111. package/bin/run-audit-alloy.cjs +187 -0
  112. package/bin/run-breaker-tlc.cjs +181 -0
  113. package/bin/run-formal-check.cjs +395 -0
  114. package/bin/run-formal-verify.cjs +701 -0
  115. package/bin/run-installer-alloy.cjs +188 -0
  116. package/bin/run-oauth-rotation-prism.cjs +132 -0
  117. package/bin/run-oscillation-tlc.cjs +202 -0
  118. package/bin/run-phase-tlc.cjs +228 -0
  119. package/bin/run-prism.cjs +446 -0
  120. package/bin/run-protocol-tlc.cjs +201 -0
  121. package/bin/run-quorum-composition-alloy.cjs +155 -0
  122. package/bin/run-sensitivity-sweep.cjs +231 -0
  123. package/bin/run-stop-hook-tlc.cjs +188 -0
  124. package/bin/run-tlc.cjs +467 -0
  125. package/bin/run-transcript-alloy.cjs +173 -0
  126. package/bin/run-uppaal.cjs +264 -0
  127. package/bin/secrets.cjs +134 -0
  128. package/bin/sensitivity-report.cjs +219 -0
  129. package/bin/sensitivity-sweep-feedback.cjs +194 -0
  130. package/bin/set-secret.cjs +29 -0
  131. package/bin/setup-telemetry-cron.sh +36 -0
  132. package/bin/sweepPtoF.cjs +63 -0
  133. package/bin/sync-baseline-requirements.cjs +290 -0
  134. package/bin/task-envelope.cjs +360 -0
  135. package/bin/telemetry-collector.cjs +229 -0
  136. package/bin/unified-mcp-server.mjs +735 -0
  137. package/bin/update-agents.cjs +369 -0
  138. package/bin/update-scoreboard.cjs +1134 -0
  139. package/bin/validate-debt-entry.cjs +207 -0
  140. package/bin/validate-invariant.cjs +419 -0
  141. package/bin/validate-memory.cjs +389 -0
  142. package/bin/validate-requirements-haiku.cjs +435 -0
  143. package/bin/validate-traces.cjs +438 -0
  144. package/bin/verify-formal-results.cjs +124 -0
  145. package/bin/verify-quorum-health.cjs +273 -0
  146. package/bin/write-check-result.cjs +106 -0
  147. package/bin/xstate-to-tla.cjs +483 -0
  148. package/bin/xstate-trace-walker.cjs +205 -0
  149. package/commands/qgsd/add-phase.md +43 -0
  150. package/commands/qgsd/add-requirement.md +24 -0
  151. package/commands/qgsd/add-todo.md +47 -0
  152. package/commands/qgsd/audit-milestone.md +37 -0
  153. package/commands/qgsd/check-todos.md +45 -0
  154. package/commands/qgsd/cleanup.md +18 -0
  155. package/commands/qgsd/close-formal-gaps.md +33 -0
  156. package/commands/qgsd/complete-milestone.md +136 -0
  157. package/commands/qgsd/debug.md +166 -0
  158. package/commands/qgsd/discuss-phase.md +83 -0
  159. package/commands/qgsd/execute-phase.md +117 -0
  160. package/commands/qgsd/fix-tests.md +27 -0
  161. package/commands/qgsd/formal-test-sync.md +32 -0
  162. package/commands/qgsd/health.md +22 -0
  163. package/commands/qgsd/help.md +22 -0
  164. package/commands/qgsd/insert-phase.md +32 -0
  165. package/commands/qgsd/join-discord.md +18 -0
  166. package/commands/qgsd/list-phase-assumptions.md +46 -0
  167. package/commands/qgsd/map-codebase.md +71 -0
  168. package/commands/qgsd/map-requirements.md +20 -0
  169. package/commands/qgsd/mcp-restart.md +176 -0
  170. package/commands/qgsd/mcp-set-model.md +134 -0
  171. package/commands/qgsd/mcp-setup.md +1371 -0
  172. package/commands/qgsd/mcp-status.md +274 -0
  173. package/commands/qgsd/mcp-update.md +238 -0
  174. package/commands/qgsd/new-milestone.md +44 -0
  175. package/commands/qgsd/new-project.md +42 -0
  176. package/commands/qgsd/observe.md +260 -0
  177. package/commands/qgsd/pause-work.md +38 -0
  178. package/commands/qgsd/plan-milestone-gaps.md +34 -0
  179. package/commands/qgsd/plan-phase.md +44 -0
  180. package/commands/qgsd/polyrepo.md +50 -0
  181. package/commands/qgsd/progress.md +24 -0
  182. package/commands/qgsd/queue.md +54 -0
  183. package/commands/qgsd/quick.md +133 -0
  184. package/commands/qgsd/quorum-test.md +275 -0
  185. package/commands/qgsd/quorum.md +707 -0
  186. package/commands/qgsd/reapply-patches.md +110 -0
  187. package/commands/qgsd/remove-phase.md +31 -0
  188. package/commands/qgsd/research-phase.md +189 -0
  189. package/commands/qgsd/resume-work.md +40 -0
  190. package/commands/qgsd/set-profile.md +34 -0
  191. package/commands/qgsd/settings.md +39 -0
  192. package/commands/qgsd/solve.md +565 -0
  193. package/commands/qgsd/sync-baselines.md +119 -0
  194. package/commands/qgsd/triage.md +233 -0
  195. package/commands/qgsd/update.md +37 -0
  196. package/commands/qgsd/verify-work.md +38 -0
  197. package/hooks/dist/config-loader.js +297 -0
  198. package/hooks/dist/conformance-schema.cjs +12 -0
  199. package/hooks/dist/gsd-context-monitor.js +64 -0
  200. package/hooks/dist/qgsd-check-update.js +62 -0
  201. package/hooks/dist/qgsd-circuit-breaker.js +682 -0
  202. package/hooks/dist/qgsd-precompact.js +156 -0
  203. package/hooks/dist/qgsd-prompt.js +653 -0
  204. package/hooks/dist/qgsd-session-start.js +122 -0
  205. package/hooks/dist/qgsd-slot-correlator.js +58 -0
  206. package/hooks/dist/qgsd-spec-regen.js +86 -0
  207. package/hooks/dist/qgsd-statusline.js +91 -0
  208. package/hooks/dist/qgsd-stop.js +553 -0
  209. package/hooks/dist/qgsd-token-collector.js +133 -0
  210. package/hooks/dist/unified-mcp-server.mjs +669 -0
  211. package/package.json +95 -0
  212. package/scripts/build-hooks.js +46 -0
  213. package/scripts/postinstall.js +48 -0
  214. package/scripts/secret-audit.sh +45 -0
  215. package/templates/qgsd.json +49 -0
@@ -0,0 +1,350 @@
1
+ 'use strict';
2
+ /**
3
+ * blessed-terminal.cjs
4
+ *
5
+ * Drop-in replacement for blessed-xterm's XTerm widget.
6
+ * Uses @xterm/headless (pure JS, no native addons) + child_process.spawn
7
+ * instead of node-pty (native C++ addon).
8
+ *
9
+ * API surface used by nforma.cjs:
10
+ * new BlessedTerminal({ shell, args, cwd, cursorType, scrollback,
11
+ * ignoreKeys, top, left, right, bottom,
12
+ * border, style, label, tags })
13
+ * term.show() / term.hide() / term.focus() — inherited from blessed.Box
14
+ * term.terminate() — kill child process
15
+ * term.on('exit', (code, signal) => ...) — child process exited
16
+ */
17
+
18
+ const blessed = require('blessed');
19
+ const { spawn } = require('child_process');
20
+ const { Terminal } = require('@xterm/headless');
21
+
22
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * Convert an xterm.js CellData into a blessed sattr integer.
26
+ * blessed sattr format: (flags << 18) | (fg << 9) | bg
27
+ * flags bits: 0=bold, 1=underline, 2=blink, 3=inverse
28
+ * fg/bg: 0-255 palette, 256=default bg, 257=default fg
29
+ */
30
+ function cellToSattr(cell) {
31
+ // Flags
32
+ let flags = 0;
33
+ if (cell.isBold()) flags |= 1;
34
+ if (cell.isUnderline()) flags |= 2;
35
+ if (cell.isBlink()) flags |= 4;
36
+ if (cell.isInverse()) flags |= 8;
37
+
38
+ // Foreground color
39
+ let fg;
40
+ if (cell.isFgDefault()) {
41
+ fg = 257; // blessed default fg sentinel
42
+ } else if (cell.isFgPalette()) {
43
+ fg = cell.getFgColor() & 0xff;
44
+ } else if (cell.isFgRGB()) {
45
+ // RGB — map to nearest 256-color index
46
+ const raw = cell.getFgColor(); // 0xRRGGBB packed as int
47
+ fg = rgbToAnsi256((raw >> 16) & 0xff, (raw >> 8) & 0xff, raw & 0xff);
48
+ } else {
49
+ fg = 257;
50
+ }
51
+
52
+ // Background color
53
+ let bg;
54
+ if (cell.isBgDefault()) {
55
+ bg = 256; // blessed default bg sentinel
56
+ } else if (cell.isBgPalette()) {
57
+ bg = cell.getBgColor() & 0xff;
58
+ } else if (cell.isBgRGB()) {
59
+ const raw = cell.getBgColor();
60
+ bg = rgbToAnsi256((raw >> 16) & 0xff, (raw >> 8) & 0xff, raw & 0xff);
61
+ } else {
62
+ bg = 256;
63
+ }
64
+
65
+ return (flags << 18) | (fg << 9) | bg;
66
+ }
67
+
68
+ /**
69
+ * Map RGB values to nearest xterm-256 palette index.
70
+ * Uses the standard 6x6x6 cube (indices 16-231) and grayscale ramp (232-255).
71
+ */
72
+ function rgbToAnsi256(r, g, b) {
73
+ // Check grayscale ramp first (232-255): 24 steps from rgb(8,8,8) to rgb(238,238,238)
74
+ if (r === g && g === b) {
75
+ if (r < 8) return 16; // black
76
+ if (r > 248) return 231; // white
77
+ return Math.round((r - 8) / 247 * 24) + 232;
78
+ }
79
+ // 6x6x6 cube (indices 16-231)
80
+ const ri = Math.round(r / 255 * 5);
81
+ const gi = Math.round(g / 255 * 5);
82
+ const bi = Math.round(b / 255 * 5);
83
+ return 16 + (36 * ri) + (6 * gi) + bi;
84
+ }
85
+
86
+ // ─── BlessedTerminal ─────────────────────────────────────────────────────────
87
+
88
+ class BlessedTerminal extends blessed.Box {
89
+ constructor(options) {
90
+ // Separate terminal-specific options from blessed Box options
91
+ const {
92
+ shell = process.env.SHELL || '/bin/sh',
93
+ args = [],
94
+ cwd = process.cwd(),
95
+ cursorType, // ignored — blessed has no cursor API
96
+ scrollback = 1000,
97
+ ignoreKeys = [],
98
+ ...boxOptions
99
+ } = options || {};
100
+
101
+ // Defaults for blessed Box
102
+ boxOptions.scrollable = false;
103
+ super(boxOptions);
104
+
105
+ this._shellCmd = shell;
106
+ this._shellArgs = args;
107
+ this._shellCwd = cwd;
108
+ this._scrollback = scrollback;
109
+ this._ignoreKeys = ignoreKeys;
110
+
111
+ // Deferred: we initialize _term and _child lazily on first render or
112
+ // when the widget is attached to a screen (whichever comes first).
113
+ this._term = null;
114
+ this._child = null;
115
+ this._ready = false;
116
+ this._destroyed = false;
117
+
118
+ // Input routing state (mirrors blessed-xterm skipInputDataOnce pattern)
119
+ this._skipInputDataOnce = false;
120
+ this._skipInputDataAlways = false;
121
+
122
+ // Listeners we need to remove on destroy
123
+ this._inputDataListener = null;
124
+ this._keypressListener = null;
125
+ this._resizeListener = null;
126
+
127
+ // Start as soon as we're attached to a screen
128
+ this.on('attach', () => this._init());
129
+ this.on('destroy', () => this._cleanup());
130
+ }
131
+
132
+ // ─── Initialise xterm + child process ───────────────────────────────────
133
+
134
+ _init() {
135
+ if (this._ready || this._destroyed) return;
136
+ this._ready = true;
137
+
138
+ const cols = Math.max(this.width - (this.ileft + this.iright), 1);
139
+ const rows = Math.max(this.height - (this.itop + this.ibottom), 1);
140
+
141
+ // @xterm/headless terminal — parses VT100/ANSI and maintains buffer
142
+ this._term = new Terminal({
143
+ cols,
144
+ rows,
145
+ scrollback: this._scrollback,
146
+ allowProposedApi: true,
147
+ });
148
+
149
+ // child_process.spawn — pure JS, no native addons
150
+ const env = Object.assign({}, process.env, {
151
+ TERM: 'xterm-256color',
152
+ COLORTERM: 'truecolor',
153
+ FORCE_COLOR: '3',
154
+ COLUMNS: String(cols),
155
+ LINES: String(rows),
156
+ });
157
+
158
+ this._child = spawn(this._shellCmd, this._shellArgs, {
159
+ cwd: this._shellCwd,
160
+ env,
161
+ stdio: 'pipe',
162
+ });
163
+
164
+ // Pipe child stdout/stderr → xterm parser
165
+ this._child.stdout.on('data', (data) => {
166
+ if (!this._destroyed) this._term.write(data.toString());
167
+ if (this.screen) this.screen.render();
168
+ });
169
+ this._child.stderr.on('data', (data) => {
170
+ if (!this._destroyed) this._term.write(data.toString());
171
+ if (this.screen) this.screen.render();
172
+ });
173
+
174
+ // Child exit → emit 'exit' on widget
175
+ this._child.on('exit', (code, signal) => {
176
+ if (!this._destroyed) this.emit('exit', code, signal);
177
+ });
178
+
179
+ // Handle spawn errors gracefully
180
+ this._child.on('error', (err) => {
181
+ if (!this._destroyed) {
182
+ this._term.write(`\r\nFailed to start: ${err.message}\r\n`);
183
+ if (this.screen) this.screen.render();
184
+ }
185
+ });
186
+
187
+ // Wire up keyboard input routing
188
+ this._wireInput();
189
+
190
+ // Resize handler
191
+ this._resizeListener = () => this._handleResize();
192
+ this.screen.on('resize', this._resizeListener);
193
+ }
194
+
195
+ // ─── Input routing ──────────────────────────────────────────────────────
196
+
197
+ _wireInput() {
198
+ if (!this.screen) return;
199
+
200
+ // Raw input data → child stdin (when this widget is focused)
201
+ this._inputDataListener = (data) => {
202
+ if (this.screen.focused !== this) return;
203
+ if (this._skipInputDataAlways) return;
204
+ if (this._skipInputDataOnce) { this._skipInputDataOnce = false; return; }
205
+ if (this._child && this._child.stdin && !this._child.stdin.destroyed) {
206
+ try { this._child.stdin.write(data); } catch (_) {}
207
+ }
208
+ };
209
+ this.screen.program.input.on('data', this._inputDataListener);
210
+
211
+ // Keypress → implement ignoreKeys (skip next raw data event for ignored keys)
212
+ this._keypressListener = (ch, key) => {
213
+ if (this.screen.focused !== this) return;
214
+ if (!key) return;
215
+ if (this._ignoreKeys.indexOf(key.full) >= 0) {
216
+ this._skipInputDataOnce = true;
217
+ }
218
+ };
219
+ this.screen.on('keypress', this._keypressListener);
220
+ }
221
+
222
+ // ─── Resize handler ─────────────────────────────────────────────────────
223
+
224
+ _handleResize() {
225
+ if (!this._term || this._destroyed) return;
226
+ const cols = Math.max(this.width - (this.ileft + this.iright), 1);
227
+ const rows = Math.max(this.height - (this.itop + this.ibottom), 1);
228
+ try { this._term.resize(cols, rows); } catch (_) {}
229
+ // Note: with piped stdio there's no SIGWINCH to send, so the child
230
+ // process may not reflow. This is acceptable for Claude CLI output.
231
+ }
232
+
233
+ // ─── Render override — bridge xterm buffer → blessed screen lines ────────
234
+
235
+ render() {
236
+ const ret = this._render();
237
+ if (!ret) return;
238
+
239
+ if (!this._term || this._destroyed) return ret;
240
+
241
+ const xi = ret.xi + this.ileft;
242
+ const xl = ret.xl - this.iright;
243
+ const yi = ret.yi + this.itop;
244
+ const yl = ret.yl - this.ibottom;
245
+
246
+ const buf = this._term.buffer.active;
247
+ const cols = this._term.cols;
248
+ const rows = this._term.rows;
249
+
250
+ for (let y = Math.max(yi, 0); y < yl; y++) {
251
+ const screenY = y;
252
+ const termY = buf.viewportY + (y - yi);
253
+
254
+ if (termY < 0 || termY >= rows + buf.viewportY) continue;
255
+
256
+ const sline = this.screen.lines[screenY];
257
+ if (!sline) continue;
258
+
259
+ const tline = buf.getLine(termY);
260
+ if (!tline) {
261
+ // Blank this screen line
262
+ for (let x = Math.max(xi, 0); x < xl; x++) {
263
+ if (!sline[x]) continue;
264
+ sline[x][0] = 0x20200; // default attrs (fg=257, bg=256 in blessed)
265
+ sline[x][1] = ' ';
266
+ }
267
+ sline.dirty = true;
268
+ continue;
269
+ }
270
+
271
+ let dirty = false;
272
+ for (let x = Math.max(xi, 0); x < xl; x++) {
273
+ const screenX = x;
274
+ const termX = x - xi;
275
+
276
+ if (termX >= cols) {
277
+ if (sline[screenX]) { sline[screenX][0] = 0x20200; sline[screenX][1] = ' '; dirty = true; }
278
+ continue;
279
+ }
280
+
281
+ if (!sline[screenX]) continue;
282
+
283
+ const cell = tline.getCell(termX);
284
+ if (!cell) {
285
+ sline[screenX][0] = 0x20200;
286
+ sline[screenX][1] = ' ';
287
+ dirty = true;
288
+ continue;
289
+ }
290
+
291
+ const ch = cell.getChars() || ' ';
292
+ const sattr = cellToSattr(cell);
293
+
294
+ if (sline[screenX][0] !== sattr || sline[screenX][1] !== ch) {
295
+ sline[screenX][0] = sattr;
296
+ sline[screenX][1] = ch;
297
+ dirty = true;
298
+ }
299
+ }
300
+ if (dirty) sline.dirty = true;
301
+ }
302
+
303
+ return ret;
304
+ }
305
+
306
+ // ─── Public API ─────────────────────────────────────────────────────────
307
+
308
+ /**
309
+ * terminate() — kill child process gracefully, then force-kill after 2s.
310
+ */
311
+ terminate() {
312
+ if (!this._child) return;
313
+ const child = this._child;
314
+ try { child.kill('SIGTERM'); } catch (_) {}
315
+ const timer = setTimeout(() => {
316
+ try { child.kill('SIGKILL'); } catch (_) {}
317
+ }, 2000);
318
+ if (timer.unref) timer.unref(); // don't keep process alive
319
+ }
320
+
321
+ // ─── Cleanup ────────────────────────────────────────────────────────────
322
+
323
+ _cleanup() {
324
+ if (this._destroyed) return;
325
+ this._destroyed = true;
326
+
327
+ // Remove listeners
328
+ if (this._inputDataListener && this.screen && this.screen.program && this.screen.program.input) {
329
+ try { this.screen.program.input.removeListener('data', this._inputDataListener); } catch (_) {}
330
+ }
331
+ if (this._keypressListener && this.screen) {
332
+ try { this.screen.removeListener('keypress', this._keypressListener); } catch (_) {}
333
+ }
334
+ if (this._resizeListener && this.screen) {
335
+ try { this.screen.removeListener('resize', this._resizeListener); } catch (_) {}
336
+ }
337
+
338
+ // Kill child process
339
+ try { this.terminate(); } catch (_) {}
340
+ this._child = null;
341
+
342
+ // Dispose xterm terminal
343
+ if (this._term) {
344
+ try { this._term.dispose(); } catch (_) {}
345
+ this._term = null;
346
+ }
347
+ }
348
+ }
349
+
350
+ module.exports = BlessedTerminal;