zames_pro 2.75.5 → 2.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +10 -4
  3. package/dist/agent-loop.js +61 -1
  4. package/dist/browser.js +40 -8
  5. package/dist/commands.js +0 -1
  6. package/dist/config.js +8 -3
  7. package/dist/context.js +99 -0
  8. package/dist/deepseek-ui.js +1 -3
  9. package/dist/i18n/common.js +13 -16
  10. package/dist/i18n/config.js +2 -2
  11. package/dist/i18n/help.js +48 -6
  12. package/dist/i18n/spinner.js +7 -1
  13. package/dist/index.js +10 -50
  14. package/dist/net-capture.js +43 -11
  15. package/dist/provider-handoff.js +0 -4
  16. package/dist/qwen-ui.js +0 -8
  17. package/dist/run-task.js +24 -1
  18. package/dist/shell.js +0 -6
  19. package/dist/system-prompt.js +52 -2
  20. package/dist/tools.js +33 -0
  21. package/dist/tui/AskQuestion.js +108 -0
  22. package/dist/tui/FullScreenApp.js +2 -1
  23. package/dist/tui/InputLine.js +0 -3
  24. package/dist/tui/StatusBar.js +0 -9
  25. package/dist/tui/TuiEditor.js +4 -43
  26. package/dist/tui/edit.js +13 -22
  27. package/dist/tui/keys.js +12 -14
  28. package/dist/tui/run-ask.js +22 -0
  29. package/dist/tui/store.js +0 -2
  30. package/dist/tui/widgets.js +3 -9
  31. package/package.json +2 -2
  32. package/scripts/build-readme.mts +0 -101
  33. package/scripts/changelog-draft.mts +0 -93
  34. package/scripts/clean-dist.mts +0 -17
  35. package/scripts/coverage-gate.mts +0 -112
  36. package/scripts/ctd-live-driver.mts +0 -51
  37. package/scripts/ctd-live.tsx +0 -33
  38. package/scripts/escd-live-driver.mts +0 -37
  39. package/scripts/eslint-ts6.mts +0 -58
  40. package/scripts/inline-status-driver.mts +0 -31
  41. package/scripts/inline-status.tsx +0 -33
  42. package/scripts/mouse-churn.mts +0 -34
  43. package/scripts/qwen-models-smoke.mts +0 -117
  44. package/scripts/qwen-smoke.mts +0 -221
  45. package/scripts/release.mts +0 -199
  46. package/scripts/render-demo.mts +0 -154
  47. package/scripts/render-how-it-works.mts +0 -229
  48. package/scripts/ru-shot.mts +0 -20
  49. package/scripts/sched-smoke.mts +0 -150
  50. package/scripts/self-smoke.mts +0 -309
  51. package/scripts/tui-comp-shot.mts +0 -63
  52. package/scripts/tui-components.tsx +0 -62
  53. package/scripts/tui-config-driver.mts +0 -82
  54. package/scripts/tui-config-probe.tsx +0 -74
  55. package/scripts/tui-edit-driver.mts +0 -100
  56. package/scripts/tui-edit-probe.tsx +0 -38
  57. package/scripts/tui-editor-probe.tsx +0 -24
  58. package/scripts/tui-input-driver.mts +0 -54
  59. package/scripts/tui-input-shot.tsx +0 -59
  60. package/scripts/tui-log-driver.mts +0 -35
  61. package/scripts/tui-log-shot.tsx +0 -48
  62. package/scripts/tui-longpaste-driver.mts +0 -56
  63. package/scripts/tui-longpaste.tsx +0 -35
  64. package/scripts/tui-manyselect-shot.mts +0 -34
  65. package/scripts/tui-manyselect.tsx +0 -17
  66. package/scripts/tui-mouse-churn-probe.tsx +0 -28
  67. package/scripts/tui-paste-driver.mts +0 -88
  68. package/scripts/tui-paste-kinds.tsx +0 -76
  69. package/scripts/tui-pastetest.mts +0 -38
  70. package/scripts/tui-probe.tsx +0 -62
  71. package/scripts/tui-repro.tsx +0 -64
  72. package/scripts/tui-resize.mts +0 -70
  73. package/scripts/tui-scroll-driver.mts +0 -53
  74. package/scripts/tui-scroll-probe.tsx +0 -46
  75. package/scripts/tui-shot.mts +0 -106
  76. package/scripts/tui-status-driver.mts +0 -24
  77. package/scripts/tui-status-shot.tsx +0 -27
  78. package/scripts/tui-submit.tsx +0 -57
  79. package/scripts/tui-wheel-driver.mts +0 -61
  80. package/scripts/tui-wheel-probe.tsx +0 -48
  81. package/scripts/tui-widgets-driver.mts +0 -20
  82. package/scripts/tui-widgets.tsx +0 -44
package/dist/tui/keys.js CHANGED
@@ -35,8 +35,8 @@ export function parseCsiU(raw) {
35
35
  if (cp === 13 && (ctrl || alt))
36
36
  return { type: 'newline' };
37
37
  if (cp === 3) {
38
- // Ctrl/Alt+Delete deletes the word on the LEFT (see dispatchKey).
39
- return ctrl || alt ? { type: 'deleteWordLeft' } : { type: 'delete' };
38
+ // Ctrl/Alt+Delete deletes the word on the RIGHT (standard editor binding).
39
+ return ctrl || alt ? { type: 'deleteWordRight' } : { type: 'delete' };
40
40
  }
41
41
  if (cp === 127) {
42
42
  return ctrl || alt ? { type: 'deleteWordLeft' } : { type: 'backspace' };
@@ -85,11 +85,11 @@ export function dispatchKey(input, key, ctx, isVisualEdge) {
85
85
  return { type: 'wordLeft' };
86
86
  if (key.rightArrow)
87
87
  return { type: 'wordRight' };
88
- // Ctrl+Delete deletes the word BEFORE the cursor (the operator's binding:
89
- // "Ctrl+Del must delete the word before the cursor"). Ctrl+Backspace does
90
- // the same. Plain Delete still deletes one char to the RIGHT.
88
+ // Ctrl+Delete deletes the word AFTER the cursor (the standard editor
89
+ // binding, same as the legacy LineEditor). Ctrl+Backspace deletes the word
90
+ // BEFORE it. Plain Delete still deletes one char to the right.
91
91
  if (key.delete)
92
- return { type: 'deleteWordLeft' };
92
+ return { type: 'deleteWordRight' };
93
93
  if (key.backspace)
94
94
  return { type: 'deleteWordLeft' };
95
95
  if (key.home)
@@ -127,8 +127,6 @@ export function dispatchKey(input, key, ctx, isVisualEdge) {
127
127
  return { type: 'suggestPrev' };
128
128
  case 'v':
129
129
  return { type: 'clipboard' };
130
- case 'y':
131
- return { type: 'copyCode' };
132
130
  case 'j':
133
131
  return { type: 'newline' };
134
132
  default:
@@ -145,16 +143,16 @@ export function dispatchKey(input, key, ctx, isVisualEdge) {
145
143
  if (input === 'f')
146
144
  return { type: 'wordRight' };
147
145
  // VSCode sends Ctrl+Delete as `ESC d` (which Ink reports as Alt+d).
148
- // The operator expects it to delete the word BEFORE the cursor.
146
+ // Standard editor binding: delete the word AFTER the cursor.
149
147
  if (input === 'd')
150
- return { type: 'deleteWordLeft' };
148
+ return { type: 'deleteWordRight' };
151
149
  if (key.backspace)
152
150
  return { type: 'deleteWordLeft' };
153
- // Alt/Ctrl+Delete also deletes the word BEFORE the cursor (the operator's
154
- // binding, consistent with ESC d): some terminals encode Ctrl+Delete as
155
- // meta+Delete rather than ESC d.
151
+ // Alt/Ctrl+Delete also deletes the word AFTER the cursor (consistent with
152
+ // ESC d): some terminals encode Ctrl+Delete as meta+Delete rather than
153
+ // ESC d.
156
154
  if (key.delete)
157
- return { type: 'deleteWordLeft' };
155
+ return { type: 'deleteWordRight' };
158
156
  return null;
159
157
  }
160
158
  // ---- LF as a bare newline (Ctrl+J) ----
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink';
3
+ import { AskQuestion } from './AskQuestion.js';
4
+ /**
5
+ * Show the modal question and resolve with the chosen option, the typed text,
6
+ * or null when dismissed / stdin is not a TTY.
7
+ */
8
+ export async function askQuestionInk(opts) {
9
+ if (!(process.stdin.isTTY && process.stdout.isTTY))
10
+ return null;
11
+ return await new Promise((resolve) => {
12
+ let answered = false;
13
+ const app = render(_jsx(AskQuestion, { ...opts, onResult: (v) => {
14
+ answered = true;
15
+ resolve(v);
16
+ } }), { exitOnCtrlC: false });
17
+ app.waitUntilExit().then(() => {
18
+ if (!answered)
19
+ resolve(null);
20
+ });
21
+ });
22
+ }
package/dist/tui/store.js CHANGED
@@ -44,8 +44,6 @@ function initialState() {
44
44
  thinkingEnabled: false,
45
45
  searchEnabled: false,
46
46
  statusLine: '',
47
- codeBlocks: [],
48
- codeFocus: -1,
49
47
  columns: process.stdout.columns || 80,
50
48
  };
51
49
  }
@@ -42,19 +42,13 @@ export function HintBar({ hints, }) {
42
42
  return (_jsx(Text, { children: hints.map(([k, l], i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { color: UI.dim, children: ' · ' }) : null, _jsx(Text, { color: UI.accent, children: k }), _jsx(Text, { color: UI.dim, children: ' ' + l })] }, i))) }));
43
43
  }
44
44
  /**
45
- * A CODE BLOCK with a syntax-highlighted body and a copy affordance.
45
+ * A CODE BLOCK with a syntax-highlighted body and an optional title label.
46
46
  *
47
47
  * The agent's answers are printed as pre-rendered ANSI (markdown.ts already
48
48
  * highlights with cli-highlight), so the live log does NOT use this component —
49
49
  * it exists for menus/panels that show a snippet inline (and for a future
50
- * structured log). Copying is a KEY, not a button: a terminal has no pointer.
51
- * Pass `onCopy` and show `hint` so the operator learns the binding.
50
+ * structured log).
52
51
  */
53
- export function CodeBlock({ code, lang, highlighted, width, hint, focused, onCopy, }) {
54
- // React cannot call `onCopy` from a keydown here (Ink has no per-element
55
- // focus ring by default); the caller wires the key and passes `focused` to
56
- // show which block it will copy. `onCopy` is kept so a future focus manager
57
- // can invoke it without changing the call sites.
58
- void onCopy;
52
+ export function CodeBlock({ code, lang, highlighted, width, hint, focused, }) {
59
53
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: focused ? UI.accent : UI.border, paddingX: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: UI.dim, children: lang || 'code' }), hint ? _jsx(Text, { color: UI.accent, children: hint }) : null] }), (highlighted || code).split(String.fromCharCode(10)).map((line, i) => (_jsx(Text, { children: line || ' ' }, i)))] }));
60
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.75.5",
3
+ "version": "2.76.0",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
- "scripts",
11
+ "scripts/postinstall.mjs",
12
12
  "README.md",
13
13
  "CHANGELOG.md",
14
14
  "LICENSE",
@@ -1,101 +0,0 @@
1
- // Assemble README.md from the two language sources.
2
- //
3
- // Why: npm renders exactly ONE readme per package (README.md in the root) and
4
- // has no language tabs. npm renders the readme with GitHub Flavored Markdown
5
- // (via GitHub's API), so a <details> block DOES work there -- and on GitHub
6
- // too. We keep the English text on top and fold the Russian translation into
7
- // a collapsible block at the bottom. The language switcher is an in-page
8
- // `#readme-ru` anchor, so one README.md serves both languages -- there is
9
- // no separate README.ru.md anymore (it was invisible on npm and just
10
- // duplicated the folded block).
11
- //
12
- // The two sources (docs/readme.en.md, docs/readme.ru.md) are the single point
13
- // of edit for each language; this script wires the switcher and the collapsible
14
- // cross-language block so neither language has to be maintained by hand. The
15
- // generated file is committed; test/readme-built.test.ts re-runs buildReadmes()
16
- // and fails if it drifts from the sources.
17
- //
18
- // Usage: npom run build:readme
19
-
20
- import fs from 'node:fs'
21
- import path from 'node:path'
22
- import { fileURLToPath } from 'node:url'
23
-
24
- const NL = String.fromCharCode(10)
25
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
26
- export const SWITCHER = '<!-- SWITCHER -->'
27
-
28
- // In-page anchor the switcher links to. GitHub strips `id` on some elements but
29
- // keeps `name` on an <a>, so both are set.
30
- export const RU_ANCHOR = '<a id="readme-ru" name="readme-ru"></a>'
31
-
32
- const EN_SWITCH = [
33
- '<p align="center">',
34
- ' <strong>English</strong> | <a href="#readme-ru">Русский</a>',
35
- '</p>',
36
- ].join(NL)
37
-
38
- export function readSource(
39
- rootDir: string,
40
- name: string,
41
- ): { head: string; body: string } {
42
- const file = path.join(rootDir, 'docs', name)
43
- const text = fs.readFileSync(file, 'utf-8')
44
- const i = text.indexOf(SWITCHER)
45
- if (i < 0) {
46
- throw new Error(
47
- file + ' has no ' + SWITCHER + ' marker (was it edited by hand?)',
48
- )
49
- }
50
- return {
51
- // Everything above the marker: logo, title, badges.
52
- head: text.slice(0, i).replace(/\n+$/, ''),
53
- // Everything below: the actual documentation.
54
- body: text
55
- .slice(i + SWITCHER.length)
56
- .replace(/^\n+/, '')
57
- .replace(/\n+$/, ''),
58
- }
59
- }
60
-
61
- function details(summary: string, body: string): string {
62
- return [
63
- '<details>',
64
- '<summary>' + summary + '</summary>',
65
- '',
66
- body,
67
- '',
68
- '</details>',
69
- ].join(NL)
70
- }
71
-
72
- // Returns the generated README.md WITHOUT touching the disk, so a test can
73
- // compare it against the committed file. Only README.md is generated: the
74
- // Russian translation lives inside it as a <details> block, and the switcher
75
- // is an in-page `#readme-ru` anchor.
76
- export function buildReadmes(rootDir: string): { readmeEn: string } {
77
- const en = readSource(rootDir, 'readme.en.md')
78
- const ru = readSource(rootDir, 'readme.ru.md')
79
- const readmeEn = [
80
- en.head,
81
- '',
82
- EN_SWITCH,
83
- '',
84
- en.body,
85
- '',
86
- RU_ANCHOR,
87
- details('🇷🇺 Читать по-русски (Russian)', ru.body),
88
- '',
89
- ].join(NL)
90
- return { readmeEn }
91
- }
92
-
93
- // Run only when invoked directly (not when imported by a test).
94
- const invoked =
95
- process.argv[1] &&
96
- path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
97
- if (invoked) {
98
- const { readmeEn } = buildReadmes(root)
99
- fs.writeFileSync(path.join(root, 'README.md'), readmeEn)
100
- console.log('build-readme: wrote README.md')
101
- }
@@ -1,93 +0,0 @@
1
- import { spawnSync } from 'node:child_process'
2
- import fs from 'node:fs'
3
- import path from 'node:path'
4
- import { fileURLToPath } from 'node:url'
5
- import {
6
- parseConventionalCommit,
7
- groupCommits,
8
- renderChangelogDraft,
9
- syncUnreleased,
10
- } from '../src/changelog.js'
11
-
12
- // Draft a CHANGELOG section from conventional commits since a ref (BACKLOG
13
- // D2). PRINTS to stdout and never touches CHANGELOG.md -- the operator (or
14
- // the agent) pastes the result in by hand, so a bad commit subject can never
15
- // corrupt the changelog.
16
- //
17
- // Usage:
18
- // npm run changelog:draft # since the latest tag, or an explicit ref
19
- // npm run changelog:draft -- v2.63.0
20
- // npm run changelog:draft -- --write # update the Unreleased section in place
21
- // npm run changelog:draft -- --check # exit 1 if Unreleased is out of date
22
-
23
- function git(args: string[]): string {
24
- const r = spawnSync('git', args, { encoding: 'utf-8' })
25
- if (r.status !== 0) return ''
26
- return (r.stdout || '').toString()
27
- }
28
-
29
- const argv = process.argv.slice(2)
30
- const write = argv.includes('--write')
31
- const check = argv.includes('--check')
32
- const argRef = argv.find((a) => !a.startsWith('-'))
33
-
34
- // Resolve the start ref: an explicit arg, otherwise the latest tag,
35
- // otherwise the root commit.
36
- const latestTag = git(['describe', '--tags', '--abbrev=0']).trim()
37
- const ref =
38
- argRef ||
39
- latestTag ||
40
- git(['revlist', 'HEAD']).trim().split('\n').pop() ||
41
- 'HEAD'
42
-
43
- const subjects = git([
44
- 'log',
45
- '--no-merges',
46
- '--pretty=format:%s',
47
- `${ref}..HEAD`,
48
- ])
49
- .split('\n')
50
- .map((s) => s.trim())
51
- .filter(Boolean)
52
-
53
- const parsed = subjects
54
- .map(parseConventionalCommit)
55
- .filter((c): c is NonNullable<typeof c> => c !== null)
56
- const body = renderChangelogDraft(groupCommits(parsed))
57
-
58
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
59
- const changelogPath = path.join(root, 'CHANGELOG.md')
60
-
61
- // D2: sync the `## [Unreleased]` section in CHANGELOG.md from the conventional
62
- // commits since the last tag. `--write` edits the file; `--check` fails when
63
- // the committed section is out of date (for CI); neither just prints the body.
64
- if (write || check) {
65
- const onDisk = fs.readFileSync(changelogPath, 'utf-8')
66
- const nextText = syncUnreleased(onDisk, body)
67
- if (write) {
68
- if (nextText !== onDisk) {
69
- fs.writeFileSync(changelogPath, nextText)
70
- console.log('changelog: updated the Unreleased section')
71
- } else {
72
- console.log('changelog: Unreleased already up to date')
73
- }
74
- process.exit(0)
75
- }
76
- if (nextText !== onDisk) {
77
- console.error(
78
- 'changelog: the Unreleased section is out of date — run `npm run changelog:sync`',
79
- )
80
- process.exit(1)
81
- }
82
- console.log('changelog: Unreleased is up to date')
83
- process.exit(0)
84
- }
85
-
86
- if (!body) {
87
- console.error('no conventional commits since ' + ref)
88
- process.exit(0)
89
- }
90
-
91
- console.log('## [Unreleased]')
92
- console.log('')
93
- console.log(body)
@@ -1,17 +0,0 @@
1
- import { rmSync, existsSync } from 'node:fs'
2
- import path from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
-
5
- // tsc emits into dist/ without clearing it, so a source file DELETED from
6
- // src/ leaves an orphan .js in the published tree (this really happened:
7
- // dist/confirm.js survived after src/confirm.ts was removed). Wipe dist/ first
8
- // so the build output mirrors src/ exactly.
9
- //
10
- // Cross-platform: use node's fs.rm instead of a shell `rm -rf` (Windows has no
11
- // rm) and never touch anything outside this repo.
12
- const here = path.dirname(fileURLToPath(import.meta.url))
13
- const dist = path.resolve(here, '..', 'dist')
14
- if (existsSync(dist)) {
15
- rmSync(dist, { recursive: true, force: true })
16
- console.log('clean-dist: removed', dist)
17
- }
@@ -1,112 +0,0 @@
1
- import { spawnSync } from 'node:child_process'
2
-
3
- // Soft coverage FLOOR for the PURE logic modules — the ones a regression would
4
- // silently break without any UI symptom (command formatting, schedule parsing,
5
- // the raw SSE/model-answer parsing). The floors are deliberately a few points
6
- // BELOW the current values so ordinary churn does not fail the build; they
7
- // only catch a real drop (a module losing its tests).
8
- //
9
- // Node's test runner has no built-in coverage threshold, so we parse the
10
- // per-file line % from the coverage report it prints and compare here. This is
11
- // informational in the sense that the FLOORS are conservative, but a drop
12
- // below one is a hard failure.
13
- const FLOORS = {
14
- 'commands.ts': 85,
15
- 'scheduler.ts': 90,
16
- 'net-capture.ts': 90,
17
- 'xml-toolcall.ts': 90,
18
- 'diff.ts': 90,
19
- 'theme.ts': 95,
20
- // Added later: core logic or already well-tested, so a silent regression
21
- // that deletes their tests should be caught too. Floors sit a few points
22
- // below the current values.
23
- 'extraTools.ts': 85,
24
- 'fsutil.ts': 90,
25
- 'sessions.ts': 90,
26
- 'context.ts': 85,
27
- 'markdown.ts': 90,
28
- 'tools.ts': 90,
29
- 'system-prompt.ts': 90,
30
- 'undo.ts': 85,
31
- 'checkpoint.ts': 80,
32
- 'permissions.ts': 85,
33
- // changelog.ts is mostly comments + type/interface declarations (lines
34
- // 1-30) that carry no runtime statements, so its LINE % caps well below the
35
- // other modules even with every executable branch tested. The floor reflects
36
- // that ceiling (a real drop below it still fails).
37
- 'changelog.ts': 78,
38
- 'attach-refs.ts': 85,
39
- // C3: the pure /config subcommand dispatcher — silent routing regressions
40
- // (a wrong subcommand branch) are invisible without tests.
41
- 'config-command.ts': 85,
42
- // The Ink TUI's PURE halves: the editor state transitions (tui/edit.ts) and
43
- // the key dispatcher (tui/keys.ts). A silent regression there breaks the
44
- // operator's editing without any UI test catching it. NOTE: the gate keys on
45
- // the BASENAME (its regex has no slash), so these are 'edit.ts' / 'keys.ts' /
46
- // 'store.ts' — unique in the tree today.
47
- 'edit.ts': 85,
48
- 'keys.ts': 85,
49
- 'store.ts': 85,
50
- 'banner.ts': 85,
51
- 'tool-summary.ts': 85,
52
- // N12: web.ts holds the SSRF guard and the DuckDuckGo/HTML parsers — a
53
- // regression there is silent. Floors sit below the current values.
54
- 'web.ts': 40,
55
- 'spinner.ts': 55,
56
- }
57
-
58
- const run = spawnSync(
59
- 'npx',
60
- ['tsx', '--test', '--experimental-test-coverage', 'test/*.test.ts'],
61
- {
62
- encoding: 'utf-8',
63
- shell: process.platform === 'win32',
64
- maxBuffer: 1024 * 1024 * 64,
65
- },
66
- )
67
-
68
- const out = (run.stdout || '') + (run.stderr || '')
69
-
70
- if (run.status !== 0) {
71
- console.error('coverage-gate: test run failed (exit ' + run.status + ')')
72
- process.exit(1)
73
- }
74
-
75
- const linePct = new Map()
76
- for (const line of out.split('\n')) {
77
- // Per-file rows look like:
78
- // ℹ commands.ts | 94.14 | 78.70 | 93.88 | 40-56 ...
79
- const m = line.match(/^\s*\S?\s*([A-Za-z0-9._-]+\.ts)\s+\|\s+([\d.]+)\s+\|/)
80
- if (m) linePct.set(m[1], Number(m[2]))
81
- }
82
-
83
- const failures = []
84
- for (const [file, floor] of Object.entries(FLOORS)) {
85
- const pct = linePct.get(file)
86
- if (pct === undefined) {
87
- failures.push(file + ': no coverage row found')
88
- continue
89
- }
90
- if (pct < floor) {
91
- failures.push(file + ': ' + pct + '% < floor ' + floor + '%')
92
- } else {
93
- console.log('coverage-gate: ' + file + ' ' + pct + '% >= ' + floor + '%')
94
- }
95
- }
96
-
97
- // Print the lowest-covered modules, so a dead or untested module is visible
98
- // in the CI log even when it has no floor yet. Informational only.
99
- const sorted = [...linePct.entries()].sort((a, b) => a[1] - b[1]).slice(0, 8)
100
- if (sorted.length) {
101
- console.log('coverage-gate: lowest covered modules (informational):')
102
- for (const [name, pct] of sorted) {
103
- console.log(' ' + name + ' ' + pct + '%')
104
- }
105
- }
106
-
107
- if (failures.length) {
108
- console.error('coverage-gate: FAILED')
109
- for (const f of failures) console.error(' ' + f)
110
- process.exit(1)
111
- }
112
- console.log('coverage-gate: OK')
@@ -1,51 +0,0 @@
1
- // Drive the live-like probe: type a seed, then send Ctrl+Delete in EVERY
2
- // encoding a terminal might use, and print the raw bytes + the buffer.
3
- import { spawn } from 'node:child_process'
4
- import fs from 'node:fs'
5
- import path from 'node:path'
6
-
7
- const ESC = String.fromCharCode(27)
8
- const LOG = '/tmp/ctd-live.log'
9
- try {
10
- fs.unlinkSync(LOG)
11
- } catch {
12
- /* ignore */
13
- }
14
-
15
- const child = spawn(
16
- 'script',
17
- ['-qec', 'npx tsx scripts/ctd-live.tsx', '/dev/null'],
18
- {
19
- cwd: path.resolve('.'),
20
- env: { ...process.env, COLUMNS: '80', LINES: '24', PROBE_CTD_LOG: LOG },
21
- },
22
- )
23
- child.stdout.on('data', () => {})
24
- child.stderr.on('data', () => {})
25
-
26
- const seq = process.argv[2] || 'xterm'
27
- const enc: Record<string, string> = {
28
- xterm: ESC + '[3;5~',
29
- kitty: ESC + '[3;5u',
30
- vt: ESC + '[3;5:1~',
31
- }
32
- const bytes = enc[seq] ?? seq
33
-
34
- setTimeout(() => {
35
- child.stdin.write('foo bar baz') // seed
36
- setTimeout(() => {
37
- child.stdin.write(bytes) // the Ctrl+Delete encoding
38
- setTimeout(() => {
39
- const log = fs.readFileSync(LOG, 'utf-8')
40
- process.stdout.write('=== ' + seq + ' ===' + String.fromCharCode(10))
41
- process.stdout.write(
42
- log
43
- .split(String.fromCharCode(10))
44
- .filter((l) => l.startsWith('RAW') || l.startsWith('BUF'))
45
- .join(String.fromCharCode(10)) + String.fromCharCode(10),
46
- )
47
- child.kill('SIGTERM')
48
- }, 500)
49
- }, 400)
50
- }, 5000)
51
- child.on('close', () => process.exit(0))
@@ -1,33 +0,0 @@
1
- // Minimal LIVE-like probe: the REAL TuiEditor exactly as index.ts builds it
2
- // (default options), plus a raw-stdin TAP that logs every byte the terminal
3
- // sends, so we can see EXACTLY what Ctrl+Delete produces on this terminal.
4
- import fs from 'node:fs'
5
- import { TuiEditor } from '../src/tui/TuiEditor.js'
6
-
7
- const LOG = process.env.PROBE_CTD_LOG || '/tmp/ctd-live.log'
8
- fs.writeFileSync(LOG, '')
9
- const w = (s: string): void => {
10
- fs.appendFileSync(LOG, s + String.fromCharCode(10))
11
- }
12
-
13
- const ed = new TuiEditor({
14
- prompt: '❯ › ',
15
- commands: [],
16
- locale: 'ru',
17
- alternateScreen: true,
18
- })
19
- ed.onSubmit = () => {}
20
- ed.onAttach = () => null
21
- ed.onClipboard = () => Promise.resolve(null)
22
- ed.start()
23
-
24
- // Tap raw stdin: hex-dump every chunk so we see the exact bytes.
25
- process.stdin.on('data', (d: Buffer) => {
26
- w('RAW ' + d.toString('hex') + ' ' + JSON.stringify(d.toString('utf-8')))
27
- })
28
- // Log the buffer after every change.
29
- ed.store.subscribe(() => {
30
- const s = ed.store.getSnapshot().edit
31
- w('BUF ' + JSON.stringify(s.buf) + ' cur=' + s.cursor)
32
- })
33
- setInterval(() => {}, 1 << 30)
@@ -1,37 +0,0 @@
1
- // Send `ESC d` (what VSCode sends for Ctrl+Delete) to the REAL TuiEditor and
2
- // print the resulting buffer.
3
- import { spawn } from 'node:child_process'
4
- import fs from 'node:fs'
5
- import path from 'node:path'
6
- const ESC = String.fromCharCode(27)
7
- const LOG = '/tmp/ctd-live.log'
8
- try {
9
- fs.unlinkSync(LOG)
10
- } catch {}
11
- const child = spawn(
12
- 'script',
13
- ['-qec', 'npx tsx scripts/ctd-live.tsx', '/dev/null'],
14
- {
15
- cwd: path.resolve('.'),
16
- env: { ...process.env, COLUMNS: '80', LINES: '24', PROBE_CTD_LOG: LOG },
17
- },
18
- )
19
- child.stdout.on('data', () => {})
20
- child.stderr.on('data', () => {})
21
- setTimeout(() => {
22
- child.stdin.write('foo bar baz')
23
- setTimeout(() => {
24
- child.stdin.write(ESC + 'd')
25
- setTimeout(() => {
26
- const log = fs.readFileSync(LOG, 'utf-8')
27
- process.stdout.write(
28
- log
29
- .split(String.fromCharCode(10))
30
- .filter((l) => l.startsWith('RAW') || l.startsWith('BUF'))
31
- .join(String.fromCharCode(10)) + String.fromCharCode(10),
32
- )
33
- child.kill('SIGTERM')
34
- }, 500)
35
- }, 400)
36
- }, 5000)
37
- child.on('close', () => process.exit(0))
@@ -1,58 +0,0 @@
1
- // Run ESLint so that typescript-eslint uses the TypeScript 6 API.
2
- //
3
- // The project is built with TypeScript 7, but typescript-eslint (8.x) does
4
- // not support the TS 7 API yet and refuses to start. Its own recommendation is
5
- // to run "side by side with TypeScript 6": load the TS 6 compiler just for the
6
- // linter. We do that by aliasing the `typescript` module to the `typescript-6`
7
- // package for the duration of the ESLint process, without touching the
8
- // compiler the project actually builds with.
9
- import { createRequire } from 'module'
10
- import Module from 'module'
11
- import path from 'path'
12
- import { fileURLToPath } from 'url'
13
-
14
- // Module._resolveFilename is a private Node field (not in @types/node), so it
15
- // is reached through a small structural type instead of `any`.
16
- type ModuleWithResolve = typeof Module & {
17
- _resolveFilename: (request: string, ...rest: unknown[]) => string
18
- }
19
-
20
- const require = createRequire(import.meta.url)
21
- const here = path.dirname(fileURLToPath(import.meta.url))
22
- const ts6 = path.join(here, '..', 'node_modules', 'typescript-6')
23
-
24
- try {
25
- require.resolve(path.join(ts6, 'lib', 'typescript.js'))
26
- } catch {
27
- console.error(
28
- 'eslint-ts6: the typescript-6 package is missing. Run: npm i -D typescript-6@npm:typescript@6',
29
- )
30
- process.exit(1)
31
- }
32
-
33
- // Patch Module._resolveFilename so any require('typescript') resolves to the
34
- // TS 6 package instead of the TS 7 the project depends on.
35
- const M = Module as ModuleWithResolve
36
- const origResolve = M._resolveFilename
37
- M._resolveFilename = (request: string, ...rest: unknown[]) => {
38
- if (request === 'typescript') {
39
- try {
40
- return require.resolve(path.join(ts6, 'lib', 'typescript.js'))
41
- } catch {
42
- return origResolve(request, ...rest)
43
- }
44
- }
45
- return origResolve(request, ...rest)
46
- }
47
-
48
- const { ESLint } = await import('eslint')
49
- const eslint = new ESLint({ fix: process.argv.includes('--fix') })
50
- const results = await eslint.lintFiles(['.'])
51
- if (process.argv.includes('--fix')) {
52
- await ESLint.outputFixes(results)
53
- }
54
- const formatter = await eslint.loadFormatter('stylish')
55
- const out = await formatter.format(results)
56
- if (out) process.stdout.write(out)
57
- const errors = results.reduce((n, r) => n + r.errorCount, 0)
58
- process.exit(errors > 0 ? 1 : 0)
@@ -1,31 +0,0 @@
1
- import { spawn } from 'node:child_process'
2
- import path from 'node:path'
3
- import { Term } from '../test/vt100.mts'
4
- const NL = String.fromCharCode(10)
5
- const term = new Term(12, 60)
6
- const child = spawn(
7
- 'script',
8
- ['-qec', 'npx tsx scripts/inline-status.tsx', '/dev/null'],
9
- {
10
- cwd: path.resolve('.'),
11
- env: { ...process.env, COLUMNS: '60', LINES: '12' },
12
- },
13
- )
14
- child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
15
- child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
16
- setTimeout(() => {
17
- const g = term.grid
18
- if (g[term.r]) g[term.r][term.c] = String.fromCharCode(9673)
19
- process.stdout.write(
20
- g
21
- .map((r) => r.join('').replace(/\s+$/, ''))
22
- .map((l, n) => String(n).padStart(2) + '| ' + l)
23
- .join(NL) +
24
- NL +
25
- 'CURSOR row=' +
26
- term.r +
27
- NL,
28
- )
29
- child.kill('SIGTERM')
30
- }, 5000)
31
- child.on('close', () => process.exit(0))