@akiojin/gwt 6.30.3 → 9.0.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 (98) hide show
  1. package/.cargo/config.toml +2 -0
  2. package/.claude-plugin/marketplace.json +18 -0
  3. package/.coderabbit.yaml +8 -0
  4. package/.codex/skills/gwt-fix-issue/scripts/inspect_issue.py +833 -0
  5. package/.dockerignore +63 -0
  6. package/.gitattributes +27 -0
  7. package/.husky/commit-msg +2 -0
  8. package/.husky/pre-commit +9 -0
  9. package/.husky/pre-push +12 -0
  10. package/.markdownlint.json +18 -0
  11. package/.markdownlintignore +2 -0
  12. package/Dockerfile +58 -0
  13. package/README.ja.md +161 -484
  14. package/README.md +164 -444
  15. package/cliff.toml +56 -0
  16. package/clippy.toml +2 -0
  17. package/cmake/ci-disable-native.cmake +16 -0
  18. package/codecov.yml +16 -0
  19. package/commitlint.config.cjs +107 -0
  20. package/deny.toml +35 -0
  21. package/docker-compose.yml +59 -0
  22. package/messages/errors.toml +52 -0
  23. package/package.json +12 -22
  24. package/rustfmt.toml +8 -0
  25. package/scripts/check-e2e-coverage-threshold.mjs +238 -0
  26. package/scripts/entrypoint.sh +36 -25
  27. package/scripts/install-linux-deps.sh +46 -0
  28. package/scripts/postinstall.js +79 -227
  29. package/scripts/release_issue_refs.py +317 -0
  30. package/scripts/run-local-backend-tests-on-commit.sh +15 -0
  31. package/scripts/run-local-e2e-coverage-on-commit.sh +69 -0
  32. package/scripts/run-local-e2e-on-commit.sh +60 -0
  33. package/scripts/test-all.sh +13 -0
  34. package/scripts/test_release_issue_refs.py +257 -0
  35. package/scripts/validate-skill-frontmatter.sh +108 -0
  36. package/scripts/verify-ci-node-toolchain.sh +76 -0
  37. package/scripts/verify-husky-hooks.sh +6 -0
  38. package/scripts/voice-eval.sh +48 -0
  39. package/tests/voice_eval/README.md +53 -0
  40. package/tests/voice_eval/manifest.template.json +55 -0
  41. package/tests/voice_eval/samples/.gitkeep +1 -0
  42. package/tests/voice_eval/script-ja.txt +10 -0
  43. package/vendor/ratatui-core/src/backend/test.rs +1077 -0
  44. package/vendor/ratatui-core/src/backend.rs +405 -0
  45. package/vendor/ratatui-core/src/buffer/assert.rs +71 -0
  46. package/vendor/ratatui-core/src/buffer/buffer.rs +1388 -0
  47. package/vendor/ratatui-core/src/buffer/cell.rs +377 -0
  48. package/vendor/ratatui-core/src/buffer.rs +9 -0
  49. package/vendor/ratatui-core/src/layout/alignment.rs +89 -0
  50. package/vendor/ratatui-core/src/layout/constraint.rs +526 -0
  51. package/vendor/ratatui-core/src/layout/direction.rs +63 -0
  52. package/vendor/ratatui-core/src/layout/flex.rs +212 -0
  53. package/vendor/ratatui-core/src/layout/layout.rs +2838 -0
  54. package/vendor/ratatui-core/src/layout/margin.rs +79 -0
  55. package/vendor/ratatui-core/src/layout/offset.rs +66 -0
  56. package/vendor/ratatui-core/src/layout/position.rs +253 -0
  57. package/vendor/ratatui-core/src/layout/rect/iter.rs +356 -0
  58. package/vendor/ratatui-core/src/layout/rect/ops.rs +136 -0
  59. package/vendor/ratatui-core/src/layout/rect.rs +1114 -0
  60. package/vendor/ratatui-core/src/layout/size.rs +147 -0
  61. package/vendor/ratatui-core/src/layout.rs +333 -0
  62. package/vendor/ratatui-core/src/lib.rs +82 -0
  63. package/vendor/ratatui-core/src/style/anstyle.rs +348 -0
  64. package/vendor/ratatui-core/src/style/color.rs +788 -0
  65. package/vendor/ratatui-core/src/style/palette/material.rs +608 -0
  66. package/vendor/ratatui-core/src/style/palette/tailwind.rs +653 -0
  67. package/vendor/ratatui-core/src/style/palette.rs +6 -0
  68. package/vendor/ratatui-core/src/style/palette_conversion.rs +82 -0
  69. package/vendor/ratatui-core/src/style/stylize.rs +668 -0
  70. package/vendor/ratatui-core/src/style.rs +1069 -0
  71. package/vendor/ratatui-core/src/symbols/bar.rs +51 -0
  72. package/vendor/ratatui-core/src/symbols/block.rs +51 -0
  73. package/vendor/ratatui-core/src/symbols/border.rs +709 -0
  74. package/vendor/ratatui-core/src/symbols/braille.rs +21 -0
  75. package/vendor/ratatui-core/src/symbols/half_block.rs +3 -0
  76. package/vendor/ratatui-core/src/symbols/line.rs +259 -0
  77. package/vendor/ratatui-core/src/symbols/marker.rs +82 -0
  78. package/vendor/ratatui-core/src/symbols/merge.rs +748 -0
  79. package/vendor/ratatui-core/src/symbols/pixel.rs +30 -0
  80. package/vendor/ratatui-core/src/symbols/scrollbar.rs +46 -0
  81. package/vendor/ratatui-core/src/symbols/shade.rs +5 -0
  82. package/vendor/ratatui-core/src/symbols.rs +15 -0
  83. package/vendor/ratatui-core/src/terminal/frame.rs +192 -0
  84. package/vendor/ratatui-core/src/terminal/terminal.rs +926 -0
  85. package/vendor/ratatui-core/src/terminal/viewport.rs +58 -0
  86. package/vendor/ratatui-core/src/terminal.rs +40 -0
  87. package/vendor/ratatui-core/src/text/grapheme.rs +84 -0
  88. package/vendor/ratatui-core/src/text/line.rs +1678 -0
  89. package/vendor/ratatui-core/src/text/masked.rs +149 -0
  90. package/vendor/ratatui-core/src/text/span.rs +904 -0
  91. package/vendor/ratatui-core/src/text/text.rs +1434 -0
  92. package/vendor/ratatui-core/src/text.rs +64 -0
  93. package/vendor/ratatui-core/src/widgets/stateful_widget.rs +193 -0
  94. package/vendor/ratatui-core/src/widgets/widget.rs +174 -0
  95. package/vendor/ratatui-core/src/widgets.rs +9 -0
  96. package/bin/gwt.js +0 -131
  97. package/scripts/postinstall.test.js +0 -71
  98. package/scripts/release-download.js +0 -66
package/cliff.toml ADDED
@@ -0,0 +1,56 @@
1
+ # git-cliff configuration
2
+ # https://git-cliff.org/docs/configuration
3
+
4
+ [changelog]
5
+ header = """
6
+ # Changelog
7
+
8
+ All notable changes to this project will be documented in this file.
9
+ """
10
+ body = """
11
+ {% if version %}\
12
+ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
13
+ {% else %}\
14
+ ## [Unreleased]
15
+ {% endif %}\
16
+ {% for group, commits in commits | group_by(attribute="group") %}
17
+ ### {{ group | striptags | trim | upper_first }}
18
+ {% for commit in commits %}
19
+ - {% if commit.scope %}**{{ commit.scope }}:** {% endif %}\
20
+ {{ commit.message | upper_first }}\
21
+ {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif %}\
22
+ {% endfor %}
23
+ {% endfor %}\n
24
+ """
25
+ footer = ""
26
+ trim = true
27
+
28
+ [git]
29
+ conventional_commits = true
30
+ filter_unconventional = true
31
+ split_commits = false
32
+ commit_parsers = [
33
+ { message = "^feat", group = "Features" },
34
+ { message = "^fix", group = "Bug Fixes" },
35
+ { message = "^doc", group = "Documentation" },
36
+ { message = "^perf", group = "Performance" },
37
+ { message = "^refactor", group = "Refactor" },
38
+ { message = "^style", group = "Styling" },
39
+ { message = "^test", group = "Testing" },
40
+ { message = "^chore\\(release\\)", skip = true },
41
+ { message = "^chore\\(deps\\)", skip = true },
42
+ { message = "^chore", group = "Miscellaneous Tasks" },
43
+ { body = ".*security", group = "Security" },
44
+ ]
45
+ protect_breaking_commits = false
46
+ filter_commits = false
47
+ topo_order = false
48
+ sort_commits = "oldest"
49
+ tag_pattern = "v[0-9].*"
50
+
51
+ [bump]
52
+ # chore/docsのみでもpatchバージョンを上げる
53
+ features_always_bump_minor = true
54
+ breaking_always_bump_major = true
55
+ # デフォルトでpatch bump(feat/fix/breakingがない場合)
56
+ initial_tag = "v0.0.0"
package/clippy.toml ADDED
@@ -0,0 +1,2 @@
1
+ # Clippy configuration
2
+ cognitive-complexity-threshold = 25
@@ -0,0 +1,16 @@
1
+ # CI toolchain override for macOS GitHub Actions runners.
2
+ #
3
+ # whisper-rs-sys passes CMAKE_* env vars to cmake, so we use
4
+ # CMAKE_TOOLCHAIN_FILE pointing here. Toolchain-file cache variables
5
+ # are applied before project() and take precedence over defaults.
6
+ #
7
+ # 1. Disable GGML_NATIVE to avoid ARM i8mm intrinsic errors
8
+ # (Xcode 16.4 Apple Clang + -mcpu=native).
9
+ set(GGML_NATIVE OFF CACHE BOOL "Disable native CPU optimizations for CI" FORCE)
10
+ #
11
+ # 2. Set deployment target to macOS 11.0+.
12
+ # ggml uses std::filesystem (requires 10.15+); ARM Macs need 11.0+.
13
+ # cmake-rs (via cc crate) may inject -mmacosx-version-min=10.13 into
14
+ # CMAKE_C_FLAGS, but cmake appends its own flag AFTER those, and
15
+ # clang uses the last -mmacosx-version-min it sees.
16
+ set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "macOS 11.0+ for ARM and std::filesystem" FORCE)
package/codecov.yml ADDED
@@ -0,0 +1,16 @@
1
+ coverage:
2
+ status:
3
+ project:
4
+ default:
5
+ target: 90%
6
+ threshold: 2%
7
+ patch:
8
+ default:
9
+ target: 90%
10
+ flags:
11
+ rust:
12
+ paths: [crates/]
13
+ carryforward: true
14
+ frontend:
15
+ paths: [gwt-gui/src/]
16
+ carryforward: true
@@ -0,0 +1,107 @@
1
+ const resolveLocal = (specifier) => {
2
+ try {
3
+ return require.resolve(specifier, { paths: [__dirname] });
4
+ } catch {
5
+ return null;
6
+ }
7
+ };
8
+
9
+ const hasConventionalConfig = Boolean(
10
+ resolveLocal("@commitlint/config-conventional"),
11
+ );
12
+ const hasConventionalParser = Boolean(
13
+ resolveLocal("conventional-changelog-conventionalcommits"),
14
+ );
15
+
16
+ const severity = {
17
+ off: 0,
18
+ warn: 1,
19
+ error: 2,
20
+ };
21
+
22
+ const conventionalTypeEnum = [
23
+ "build",
24
+ "chore",
25
+ "ci",
26
+ "docs",
27
+ "feat",
28
+ "fix",
29
+ "perf",
30
+ "refactor",
31
+ "revert",
32
+ "style",
33
+ "test",
34
+ ];
35
+
36
+ const customTypeEnum = [
37
+ "feat",
38
+ "fix",
39
+ "docs",
40
+ "style",
41
+ "refactor",
42
+ "perf",
43
+ "test",
44
+ "build",
45
+ "ci",
46
+ "chore",
47
+ "revert",
48
+ ];
49
+
50
+ const conventionalRules = {
51
+ "body-leading-blank": [severity.warn, "always"],
52
+ "body-max-line-length": [severity.error, "always", 100],
53
+ "footer-leading-blank": [severity.warn, "always"],
54
+ "footer-max-line-length": [severity.error, "always", 100],
55
+ "header-max-length": [severity.error, "always", 100],
56
+ "header-trim": [severity.error, "always"],
57
+ "subject-case": [
58
+ severity.error,
59
+ "never",
60
+ ["sentence-case", "start-case", "pascal-case", "upper-case"],
61
+ ],
62
+ "subject-empty": [severity.error, "never"],
63
+ "subject-full-stop": [severity.error, "never", "."],
64
+ "type-case": [severity.error, "always", "lower-case"],
65
+ "type-empty": [severity.error, "never"],
66
+ "type-enum": [severity.error, "always", conventionalTypeEnum],
67
+ };
68
+
69
+ const customRules = {
70
+ "subject-empty": [2, "never"],
71
+ "subject-max-length": [2, "always", 100],
72
+ "header-max-length": [2, "always", 100],
73
+ "subject-case": [0],
74
+ "body-max-line-length": [2, "always", 100],
75
+ "type-enum": [2, "always", customTypeEnum],
76
+ };
77
+
78
+ const rules = hasConventionalConfig
79
+ ? customRules
80
+ : { ...conventionalRules, ...customRules };
81
+
82
+ module.exports = {
83
+ ...(hasConventionalConfig
84
+ ? { extends: ["@commitlint/config-conventional"] }
85
+ : hasConventionalParser
86
+ ? { parserPreset: "conventional-changelog-conventionalcommits" }
87
+ : {}),
88
+ rules,
89
+ // Ignore commits that don't follow Conventional Commits format
90
+ ignores: [
91
+ (commit) => {
92
+ const firstLine = commit.split("\n")[0].trim();
93
+ // Merge commits (git-generated)
94
+ if (/^merge(\s|:)/i.test(firstLine)) return true;
95
+ // Branch-name-style commits (historical)
96
+ if (/^(bugfix|feature|hotfix|release)\//.test(firstLine)) return true;
97
+ // Historical commits without conventional prefix (Fix/Stabilize pattern)
98
+ if (
99
+ /^(Fix|Stabilize|Update|Add|Remove|Refactor|Clean|Format|Resolve)\s/.test(
100
+ firstLine,
101
+ )
102
+ )
103
+ return true;
104
+ return false;
105
+ },
106
+ ],
107
+ };
package/deny.toml ADDED
@@ -0,0 +1,35 @@
1
+ # cargo-deny configuration
2
+ # https://embarkstudios.github.io/cargo-deny/
3
+
4
+ [advisories]
5
+ db-path = "~/.cargo/advisory-db"
6
+ db-urls = ["https://github.com/rustsec/advisory-db"]
7
+ vulnerability = "deny"
8
+ unmaintained = "warn"
9
+ yanked = "warn"
10
+ notice = "warn"
11
+
12
+ [licenses]
13
+ unlicensed = "deny"
14
+ allow = [
15
+ "MIT",
16
+ "Apache-2.0",
17
+ "Apache-2.0 WITH LLVM-exception",
18
+ "BSD-2-Clause",
19
+ "BSD-3-Clause",
20
+ "ISC",
21
+ "Zlib",
22
+ "CC0-1.0",
23
+ "Unicode-DFS-2016",
24
+ ]
25
+ copyleft = "deny"
26
+
27
+ [bans]
28
+ multiple-versions = "warn"
29
+ wildcards = "deny"
30
+ highlight = "all"
31
+
32
+ [sources]
33
+ unknown-registry = "deny"
34
+ unknown-git = "deny"
35
+ allow-registry = ["https://github.com/rust-lang/crates.io-index"]
@@ -0,0 +1,59 @@
1
+ services:
2
+ gwt:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ volumes:
7
+ - .:/gwt
8
+ - bash-history:/root/.bash_history_dir
9
+ - claude-config:/root/.claude
10
+ - codex-config:/root/.codex
11
+ - ${HOME:?HOME must be set}/.claude:/root/.claude-host:ro
12
+ - ${HOME:?HOME must be set}/.codex:/root/.codex-host:ro
13
+ ports:
14
+ - ${PORT:-3000}:3000
15
+ extra_hosts:
16
+ - "host.docker.internal:host-gateway"
17
+ environment:
18
+ - NPM_USER
19
+ - NPM_EMAIL
20
+ - NPM_PASS
21
+ - GIT_USER_EMAIL
22
+ - GIT_USER_NAME
23
+ - GITHUB_TOKEN
24
+ - GITHUB_PERSONAL_ACCESS_TOKEN
25
+ - GITHUB_USERNAME
26
+ - HISTFILE=/root/.bash_history_dir/.bash_history
27
+ - HISTSIZE=100000
28
+ - HISTFILESIZE=100000
29
+ - MAX_MCP_OUTPUT_TOKENS
30
+ - ENABLE_BACKGROUND_TASKS=1
31
+ - FORCE_AUTO_BACKGROUND_TASKS=1
32
+ - PORT=3000
33
+ stdin_open: true
34
+ tty: true
35
+ working_dir: /gwt
36
+
37
+ playwright-novnc:
38
+ image: ghcr.io/xtr-dev/mcp-playwright-novnc:latest
39
+ platform: ${PLAYWRIGHT_NOVNC_PLATFORM:-linux/amd64}
40
+ ports:
41
+ - ${PLAYWRIGHT_NOVNC_PORT:-6080}:6080
42
+ - ${PLAYWRIGHT_NOVNC_VNC_PORT:-3080}:3080
43
+ volumes:
44
+ - .:/gwt
45
+ environment:
46
+ - SCREEN_WIDTH=1920
47
+ - SCREEN_HEIGHT=1080
48
+ - SCREEN_DEPTH=24
49
+ - MCP_BROWSER=chromium
50
+ - PLAYWRIGHT_BASE_URL=http://gwt:3000
51
+ working_dir: /gwt
52
+ shm_size: 2gb
53
+ depends_on:
54
+ - gwt
55
+
56
+ volumes:
57
+ bash-history: {}
58
+ claude-config: {}
59
+ codex-config: {}
@@ -0,0 +1,52 @@
1
+ [git]
2
+ E1001 = "Repository not found"
3
+ E1002 = "Not a git repository"
4
+ E1003 = "Branch not found"
5
+ E1004 = "Branch already exists"
6
+ E1005 = "Remote not found"
7
+ E1006 = "Fetch failed"
8
+ E1007 = "Fast-forward pull failed"
9
+ E1008 = "Uncommitted changes detected"
10
+ E1009 = "Unpushed commits detected"
11
+ E1010 = "Branch diverged from remote"
12
+ E1011 = "Git command failed"
13
+ E1012 = "Git executable not found"
14
+ E1013 = "Git operation failed"
15
+ E1014 = "Branch create failed"
16
+ E1015 = "Branch delete failed"
17
+
18
+ [worktree]
19
+ E2001 = "Worktree not found"
20
+ E2002 = "Worktree already exists"
21
+ E2003 = "Failed to create worktree"
22
+ E2004 = "Failed to remove worktree"
23
+ E2005 = "Protected branch cannot be deleted"
24
+ E2006 = "Worktree path invalid"
25
+ E2007 = "Orphaned worktree detected"
26
+ E2008 = "Worktree locked by another process"
27
+ E2009 = "Path exists but is not a stale worktree"
28
+
29
+ [config]
30
+ E3001 = "Configuration file not found"
31
+ E3002 = "Configuration parse error"
32
+ E3003 = "Configuration write error"
33
+ E3004 = "Invalid configuration value"
34
+ E3005 = "Profile not found"
35
+ E3006 = "Session not found"
36
+ E3007 = "Configuration migration failed"
37
+
38
+ [agent]
39
+ E4001 = "Agent not found"
40
+ E4002 = "Agent launch failed"
41
+ E4003 = "Agent configuration invalid"
42
+ E4004 = "Agent process terminated unexpectedly"
43
+
44
+ [web]
45
+ E5001 = "Server bind failed"
46
+ E5002 = "WebSocket connection failed"
47
+ E5003 = "API request failed"
48
+ E5004 = "PTY spawn failed"
49
+
50
+ [general]
51
+ E9001 = "IO error"
52
+ E9002 = "Internal error"
package/package.json CHANGED
@@ -1,14 +1,19 @@
1
1
  {
2
2
  "name": "@akiojin/gwt",
3
- "version": "6.30.3",
4
- "description": "Interactive Git worktree manager with Coding Agent selection (Claude Code / Codex CLI / Gemini CLI)",
3
+ "version": "9.0.1",
4
+ "description": "TUI for Git worktree management and coding agent launch",
5
+ "type": "module",
5
6
  "bin": {
6
- "gwt": "bin/gwt.js"
7
+ "gwt": "bin/gwt-tui"
7
8
  },
8
- "type": "module",
9
9
  "scripts": {
10
10
  "postinstall": "node scripts/postinstall.js",
11
+ "dev": "cargo run -p gwt-tui",
12
+ "build": "cargo build --release -p gwt-tui",
13
+ "test": "cargo test -p gwt-core -p gwt-tui --all-features",
14
+ "test:all": "bash scripts/test-all.sh",
11
15
  "prepare": "test -n \"$CI\" || bunx husky install",
16
+ "lint:skills": "bash scripts/validate-skill-frontmatter.sh",
12
17
  "lint:husky": "bash scripts/verify-husky-hooks.sh"
13
18
  },
14
19
  "keywords": [
@@ -30,27 +35,12 @@
30
35
  "type": "git",
31
36
  "url": "https://github.com/akiojin/gwt.git"
32
37
  },
33
- "publishConfig": {
34
- "access": "public"
35
- },
36
- "files": [
37
- "bin/",
38
- "scripts/"
39
- ],
40
38
  "engines": {
41
39
  "node": ">=18"
42
40
  },
43
- "os": [
44
- "darwin",
45
- "linux",
46
- "win32"
47
- ],
48
- "cpu": [
49
- "x64",
50
- "arm64"
51
- ],
41
+ "packageManager": "pnpm@10.29.2",
52
42
  "devDependencies": {
53
- "@commitlint/cli": "^20.4.1",
54
- "@commitlint/config-conventional": "^20.4.1"
43
+ "@commitlint/cli": "^20.5.0",
44
+ "@commitlint/config-conventional": "^20.5.0"
55
45
  }
56
46
  }
package/rustfmt.toml ADDED
@@ -0,0 +1,8 @@
1
+ # Rustfmt configuration
2
+ edition = "2021"
3
+ max_width = 100
4
+ tab_spaces = 4
5
+ use_small_heuristics = "Default"
6
+ imports_granularity = "Crate"
7
+ group_imports = "StdExternalCrate"
8
+ reorder_imports = true
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
8
+ const repoRoot = path.resolve(scriptDir, "..");
9
+ const guiDir = path.join(repoRoot, "gwt-gui");
10
+ const nycrcPath = path.join(guiDir, ".nycrc.e2e.json");
11
+ const nycTempDir = path.join(guiDir, ".nyc_output");
12
+
13
+ function loadJson(filePath) {
14
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
15
+ }
16
+
17
+ function clone(value) {
18
+ return JSON.parse(JSON.stringify(value));
19
+ }
20
+
21
+ function mergeCounts(existing, incoming) {
22
+ for (const [id, count] of Object.entries(incoming)) {
23
+ existing[id] = (existing[id] ?? 0) + Number(count ?? 0);
24
+ }
25
+ }
26
+
27
+ function mergeBranchCounts(existing, incoming) {
28
+ for (const [id, counts] of Object.entries(incoming)) {
29
+ const nextCounts = Array.isArray(counts) ? counts : [];
30
+ const mergedCounts = Array.isArray(existing[id]) ? existing[id] : [];
31
+ existing[id] = nextCounts.map(
32
+ (count, index) => Number(count ?? 0) + Number(mergedCounts[index] ?? 0),
33
+ );
34
+ }
35
+ }
36
+
37
+ function mergeCoverageMaps(jsonFiles) {
38
+ const merged = new Map();
39
+
40
+ for (const jsonFile of jsonFiles) {
41
+ const coverageMap = loadJson(path.join(nycTempDir, jsonFile));
42
+ for (const [sourcePath, fileCoverage] of Object.entries(coverageMap)) {
43
+ if (!merged.has(sourcePath)) {
44
+ merged.set(sourcePath, clone(fileCoverage));
45
+ continue;
46
+ }
47
+
48
+ const target = merged.get(sourcePath);
49
+ mergeCounts(target.s, fileCoverage.s ?? {});
50
+ mergeCounts(target.f, fileCoverage.f ?? {});
51
+ mergeBranchCounts(target.b, fileCoverage.b ?? {});
52
+ }
53
+ }
54
+
55
+ return merged;
56
+ }
57
+
58
+ function countCovered(counter) {
59
+ return Object.values(counter).filter((value) => Number(value) > 0).length;
60
+ }
61
+
62
+ function getSourceLineCount(filePath) {
63
+ try {
64
+ return fs.readFileSync(filePath, "utf8").split("\n").length;
65
+ } catch {
66
+ return Number.POSITIVE_INFINITY;
67
+ }
68
+ }
69
+
70
+ function isThinWrapperFile(filePath) {
71
+ return getSourceLineCount(filePath) <= 10;
72
+ }
73
+
74
+ function summarizeLines(statementMap, statementCounts) {
75
+ const lineHits = new Map();
76
+ for (const [id, location] of Object.entries(statementMap ?? {})) {
77
+ const count = Number(statementCounts?.[id] ?? 0);
78
+ const startLine = Number(location?.start?.line ?? 0);
79
+ const endLine = Number(location?.end?.line ?? startLine);
80
+ for (let line = startLine; line <= endLine; line += 1) {
81
+ if (!lineHits.has(line)) {
82
+ lineHits.set(line, false);
83
+ }
84
+ if (count > 0) {
85
+ lineHits.set(line, true);
86
+ }
87
+ }
88
+ }
89
+
90
+ return {
91
+ covered: [...lineHits.values()].filter(Boolean).length,
92
+ total: lineHits.size,
93
+ };
94
+ }
95
+
96
+ function summarizeFile(fileCoverage, filePath) {
97
+ const statements = {
98
+ covered: countCovered(fileCoverage.s ?? {}),
99
+ total: Object.keys(fileCoverage.s ?? {}).length,
100
+ };
101
+
102
+ const thinWrapper = isThinWrapperFile(filePath);
103
+
104
+ let functionIds = Object.keys(fileCoverage.f ?? {});
105
+ if (thinWrapper) {
106
+ functionIds = functionIds.filter((id) => {
107
+ const name = fileCoverage.fnMap?.[id]?.name ?? "";
108
+ return !String(name).startsWith("(anonymous_");
109
+ });
110
+ }
111
+ const functions = {
112
+ covered: functionIds.filter(
113
+ (id) => Number(fileCoverage.f?.[id] ?? 0) > 0,
114
+ ).length,
115
+ total: functionIds.length,
116
+ };
117
+
118
+ const branchCounts = thinWrapper ? [] : Object.values(fileCoverage.b ?? {});
119
+ const branches = {
120
+ covered: branchCounts.flat().filter((value) => Number(value) > 0).length,
121
+ total: branchCounts.reduce(
122
+ (sum, counts) => sum + (Array.isArray(counts) ? counts.length : 0),
123
+ 0,
124
+ ),
125
+ };
126
+
127
+ const lines = summarizeLines(fileCoverage.statementMap, fileCoverage.s);
128
+
129
+ return { statements, functions, branches, lines };
130
+ }
131
+
132
+ function addMetric(target, source) {
133
+ target.covered += source.covered;
134
+ target.total += source.total;
135
+ }
136
+
137
+ function formatPercent(metric) {
138
+ if (metric.total === 0) return "100.00";
139
+ return ((metric.covered / metric.total) * 100).toFixed(2);
140
+ }
141
+
142
+ function fail(message) {
143
+ console.error(message);
144
+ process.exit(1);
145
+ }
146
+
147
+ if (!fs.existsSync(nycrcPath)) {
148
+ fail(`Missing E2E nyc config: ${nycrcPath}`);
149
+ }
150
+
151
+ if (!fs.existsSync(nycTempDir)) {
152
+ fail(`Missing E2E coverage temp dir: ${nycTempDir}`);
153
+ }
154
+
155
+ const nycrc = loadJson(nycrcPath);
156
+ const jsonFiles = fs
157
+ .readdirSync(nycTempDir)
158
+ .filter((file) => file.endsWith(".json"))
159
+ .sort();
160
+
161
+ if (jsonFiles.length === 0) {
162
+ fail(`No E2E coverage JSON files found in ${nycTempDir}`);
163
+ }
164
+
165
+ const mergedCoverage = mergeCoverageMaps(jsonFiles);
166
+ const targetFiles = (nycrc.targetFiles ?? nycrc.include ?? []).map((relativePath) =>
167
+ path.resolve(guiDir, relativePath),
168
+ );
169
+
170
+ const aggregate = {
171
+ statements: { covered: 0, total: 0 },
172
+ functions: { covered: 0, total: 0 },
173
+ branches: { covered: 0, total: 0 },
174
+ lines: { covered: 0, total: 0 },
175
+ };
176
+
177
+ console.log("E2E coverage targets:");
178
+ for (const filePath of targetFiles) {
179
+ const fileCoverage = mergedCoverage.get(filePath);
180
+ const summary = fileCoverage
181
+ ? summarizeFile(fileCoverage, filePath)
182
+ : {
183
+ statements: { covered: 0, total: 0 },
184
+ functions: { covered: 0, total: 0 },
185
+ branches: { covered: 0, total: 0 },
186
+ lines: { covered: 0, total: 0 },
187
+ };
188
+
189
+ addMetric(aggregate.statements, summary.statements);
190
+ addMetric(aggregate.functions, summary.functions);
191
+ addMetric(aggregate.branches, summary.branches);
192
+ addMetric(aggregate.lines, summary.lines);
193
+
194
+ console.log(`- ${path.relative(guiDir, filePath)}`);
195
+ console.log(
196
+ ` statements ${formatPercent(summary.statements)}% (${summary.statements.covered}/${summary.statements.total})`,
197
+ );
198
+ console.log(
199
+ ` functions ${formatPercent(summary.functions)}% (${summary.functions.covered}/${summary.functions.total})`,
200
+ );
201
+ console.log(
202
+ ` branches ${formatPercent(summary.branches)}% (${summary.branches.covered}/${summary.branches.total})`,
203
+ );
204
+ console.log(
205
+ ` lines ${formatPercent(summary.lines)}% (${summary.lines.covered}/${summary.lines.total})`,
206
+ );
207
+ }
208
+
209
+ console.log("\nAggregate E2E coverage over target shell files:");
210
+ for (const metricName of ["statements", "functions", "branches", "lines"]) {
211
+ const metric = aggregate[metricName];
212
+ console.log(
213
+ `- ${metricName}: ${formatPercent(metric)}% (${metric.covered}/${metric.total})`,
214
+ );
215
+ }
216
+
217
+ const thresholds = {
218
+ statements: Number(nycrc.statements ?? 0),
219
+ functions: Number(nycrc.functions ?? 0),
220
+ branches: Number(nycrc.branches ?? 0),
221
+ lines: Number(nycrc.lines ?? 0),
222
+ };
223
+
224
+ const failures = [];
225
+ for (const [metricName, threshold] of Object.entries(thresholds)) {
226
+ const metric = aggregate[metricName];
227
+ const percent =
228
+ metric.total === 0 ? 100 : (metric.covered / metric.total) * 100;
229
+ if (percent < threshold) {
230
+ failures.push(
231
+ `${metricName} ${percent.toFixed(2)}% < required ${threshold.toFixed(2)}%`,
232
+ );
233
+ }
234
+ }
235
+
236
+ if (failures.length > 0) {
237
+ fail(`E2E coverage threshold failed:\n- ${failures.join("\n- ")}`);
238
+ }