@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
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env python3
2
+ """Collect auto-close and reference-only issue refs for a release range."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ import subprocess
10
+ import sys
11
+ from dataclasses import asdict, dataclass, field
12
+ from typing import Callable, Sequence
13
+
14
+ CommandRunner = Callable[[Sequence[str]], str]
15
+
16
+ CLOSING_KEYWORD_RE = re.compile(
17
+ r"\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b",
18
+ re.IGNORECASE,
19
+ )
20
+ ISSUE_REF_RE = re.compile(r"#(\d+)")
21
+ SQUASH_REF_RE = re.compile(r"\(#(\d+)\)$")
22
+ MERGE_PR_RE = re.compile(r"^Merge pull request #(\d+)\b")
23
+ HEADING_RE = re.compile(r"^##\s+(?P<title>.+?)\s*$")
24
+
25
+
26
+ @dataclass
27
+ class BodyIssueRefs:
28
+ auto_close_issues: list[int] = field(default_factory=list)
29
+ reference_only_issues: list[int] = field(default_factory=list)
30
+ warnings: list[str] = field(default_factory=list)
31
+
32
+
33
+ @dataclass
34
+ class CommitRef:
35
+ number: int
36
+ kind: str
37
+ source: str
38
+ auto_close_issues: list[int] = field(default_factory=list)
39
+ reference_only_issues: list[int] = field(default_factory=list)
40
+ warnings: list[str] = field(default_factory=list)
41
+
42
+
43
+ @dataclass
44
+ class ReleaseIssueRefs:
45
+ repo: str
46
+ range: str
47
+ refs: list[CommitRef] = field(default_factory=list)
48
+ auto_close_issues: list[int] = field(default_factory=list)
49
+ reference_only_issues: list[int] = field(default_factory=list)
50
+ warnings: list[str] = field(default_factory=list)
51
+
52
+
53
+ def run_command(args: Sequence[str]) -> str:
54
+ result = subprocess.run(args, check=True, capture_output=True, text=True)
55
+ return result.stdout
56
+
57
+
58
+ def unique_sorted(numbers: Sequence[int | str]) -> list[int]:
59
+ return sorted({int(value) for value in numbers})
60
+
61
+
62
+ def dedupe_preserve_order(items: Sequence[str]) -> list[str]:
63
+ seen: set[str] = set()
64
+ ordered: list[str] = []
65
+ for item in items:
66
+ if item not in seen:
67
+ seen.add(item)
68
+ ordered.append(item)
69
+ return ordered
70
+
71
+
72
+ def format_issue_refs(numbers: Sequence[int]) -> str:
73
+ return ", ".join(f"#{number}" for number in unique_sorted(numbers))
74
+
75
+
76
+ def extract_section(body: str, section_title: str) -> str:
77
+ lines = body.splitlines()
78
+ in_section = False
79
+ collected: list[str] = []
80
+
81
+ for line in lines:
82
+ heading = HEADING_RE.match(line.strip())
83
+ if heading:
84
+ title = heading.group("title").strip()
85
+ if in_section:
86
+ break
87
+ in_section = title == section_title
88
+ continue
89
+
90
+ if in_section:
91
+ collected.append(line)
92
+
93
+ return "\n".join(collected)
94
+
95
+
96
+ def extract_issue_numbers(text: str) -> list[int]:
97
+ return unique_sorted(match.group(1) for match in ISSUE_REF_RE.finditer(text or ""))
98
+
99
+
100
+ def parse_pr_body_refs(body: str, pr_number: int | None = None) -> BodyIssueRefs:
101
+ closing_section = extract_section(body, "Closing Issues")
102
+ related_section = extract_section(body, "Related Issues / Links")
103
+
104
+ auto_close = set(extract_issue_numbers(closing_section))
105
+ auto_close.update(int(match.group(1)) for match in CLOSING_KEYWORD_RE.finditer(body or ""))
106
+
107
+ reference_only = set(extract_issue_numbers(related_section)) - auto_close
108
+ warnings: list[str] = []
109
+
110
+ if reference_only:
111
+ prefix = f"PR #{pr_number}" if pr_number is not None else "PR body"
112
+ warnings.append(
113
+ f"{prefix} references {format_issue_refs(sorted(reference_only))} only in "
114
+ "`Related Issues / Links`; they will not auto-close on release."
115
+ )
116
+
117
+ return BodyIssueRefs(
118
+ auto_close_issues=sorted(auto_close),
119
+ reference_only_issues=sorted(reference_only),
120
+ warnings=warnings,
121
+ )
122
+
123
+
124
+ def extract_release_commit_refs(
125
+ no_merge_subjects: str,
126
+ merge_subjects: str,
127
+ ) -> list[tuple[int, str]]:
128
+ refs: dict[int, str] = {}
129
+
130
+ for subject in no_merge_subjects.splitlines():
131
+ match = SQUASH_REF_RE.search(subject.strip())
132
+ if match:
133
+ refs.setdefault(int(match.group(1)), "squash")
134
+
135
+ for subject in merge_subjects.splitlines():
136
+ match = MERGE_PR_RE.search(subject.strip())
137
+ if match:
138
+ refs.setdefault(int(match.group(1)), "merge")
139
+
140
+ return [(number, refs[number]) for number in sorted(refs)]
141
+
142
+
143
+ def resolve_repo_slug(runner: CommandRunner) -> str:
144
+ return runner(
145
+ ["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]
146
+ ).strip()
147
+
148
+
149
+ def fetch_issue_labels(number: int, repo_slug: str, runner: CommandRunner) -> list[str]:
150
+ """Return label names for a GitHub issue."""
151
+ payload = json.loads(runner(["gh", "api", f"repos/{repo_slug}/issues/{number}"]) or "{}")
152
+ return [label["name"] for label in payload.get("labels", [])]
153
+
154
+
155
+ SPEC_LABEL = "gwt-spec"
156
+
157
+
158
+ def classify_release_ref(
159
+ number: int,
160
+ source: str,
161
+ repo_slug: str,
162
+ runner: CommandRunner,
163
+ ) -> CommitRef:
164
+ issue_payload = json.loads(runner(["gh", "api", f"repos/{repo_slug}/issues/{number}"]) or "{}")
165
+ if issue_payload.get("pull_request"):
166
+ pr_payload = json.loads(
167
+ runner(["gh", "pr", "view", str(number), "--repo", repo_slug, "--json", "body"]) or "{}"
168
+ )
169
+ pr_refs = parse_pr_body_refs(pr_payload.get("body") or "", pr_number=number)
170
+ return CommitRef(
171
+ number=number,
172
+ kind="pr",
173
+ source=source,
174
+ auto_close_issues=pr_refs.auto_close_issues,
175
+ reference_only_issues=pr_refs.reference_only_issues,
176
+ warnings=pr_refs.warnings,
177
+ )
178
+
179
+ return CommitRef(
180
+ number=number,
181
+ kind="issue",
182
+ source=source,
183
+ auto_close_issues=[number],
184
+ )
185
+
186
+
187
+ def collect_release_issue_refs(
188
+ range_expr: str,
189
+ repo_slug: str | None = None,
190
+ runner: CommandRunner = run_command,
191
+ ) -> ReleaseIssueRefs:
192
+ repo = repo_slug or resolve_repo_slug(runner)
193
+ no_merge_subjects = runner(["git", "log", "--pretty=%s", "--no-merges", range_expr])
194
+ merge_subjects = runner(["git", "log", "--merges", "--pretty=%s", range_expr])
195
+
196
+ refs: list[CommitRef] = []
197
+ auto_close: set[int] = set()
198
+ reference_only: set[int] = set()
199
+ warnings: list[str] = []
200
+
201
+ for number, source in extract_release_commit_refs(no_merge_subjects, merge_subjects):
202
+ ref = classify_release_ref(number, source, repo, runner)
203
+ refs.append(ref)
204
+ auto_close.update(ref.auto_close_issues)
205
+ reference_only.update(ref.reference_only_issues)
206
+ warnings.extend(ref.warnings)
207
+
208
+ # Post-filter: move gwt-spec issues from auto-close to reference-only
209
+ spec_protected: list[int] = []
210
+ for issue_number in sorted(auto_close):
211
+ labels = fetch_issue_labels(issue_number, repo, runner)
212
+ if SPEC_LABEL in labels:
213
+ spec_protected.append(issue_number)
214
+
215
+ if spec_protected:
216
+ auto_close.difference_update(spec_protected)
217
+ reference_only.update(spec_protected)
218
+ warnings.append(
219
+ f"gwt-spec issues moved to reference-only: "
220
+ f"{format_issue_refs(spec_protected)}. "
221
+ "gwt-spec issues are never auto-closed by releases."
222
+ )
223
+
224
+ reference_only.difference_update(auto_close)
225
+ if reference_only:
226
+ warnings.insert(
227
+ 0,
228
+ "Reference-only issues detected: "
229
+ f"{format_issue_refs(sorted(reference_only))}. "
230
+ "Add them to `Closing Issues` if they should auto-close.",
231
+ )
232
+
233
+ return ReleaseIssueRefs(
234
+ repo=repo,
235
+ range=range_expr,
236
+ refs=refs,
237
+ auto_close_issues=sorted(auto_close),
238
+ reference_only_issues=sorted(reference_only),
239
+ warnings=dedupe_preserve_order(warnings),
240
+ )
241
+
242
+
243
+ def render_text(report: ReleaseIssueRefs) -> str:
244
+ lines = [
245
+ f"Repo: {report.repo}",
246
+ f"Range: {report.range}",
247
+ "",
248
+ "Auto-close issues:",
249
+ ]
250
+
251
+ if report.auto_close_issues:
252
+ lines.extend(f"- Closes #{number}" for number in report.auto_close_issues)
253
+ else:
254
+ lines.append("- None")
255
+
256
+ lines.extend(["", "Reference-only issues:"])
257
+ if report.reference_only_issues:
258
+ lines.extend(f"- #{number}" for number in report.reference_only_issues)
259
+ else:
260
+ lines.append("- None")
261
+
262
+ if report.warnings:
263
+ lines.extend(["", "Warnings:"])
264
+ lines.extend(f"- {warning}" for warning in report.warnings)
265
+
266
+ return "\n".join(lines)
267
+
268
+
269
+ def build_parser() -> argparse.ArgumentParser:
270
+ parser = argparse.ArgumentParser(
271
+ description="Collect release auto-close and reference-only issue refs."
272
+ )
273
+ parser.add_argument(
274
+ "--range",
275
+ dest="range_expr",
276
+ required=True,
277
+ help="Git revision range to inspect, for example v1.2.3..HEAD or HEAD.",
278
+ )
279
+ parser.add_argument(
280
+ "--repo",
281
+ dest="repo_slug",
282
+ default=None,
283
+ help="GitHub repo slug (owner/name). Defaults to `gh repo view`.",
284
+ )
285
+ parser.add_argument(
286
+ "--format",
287
+ choices=("text", "json"),
288
+ default="text",
289
+ help="Output format.",
290
+ )
291
+ return parser
292
+
293
+
294
+ def main() -> int:
295
+ parser = build_parser()
296
+ args = parser.parse_args()
297
+
298
+ try:
299
+ report = collect_release_issue_refs(args.range_expr, repo_slug=args.repo_slug)
300
+ except subprocess.CalledProcessError as error:
301
+ stderr = (error.stderr or "").strip()
302
+ if stderr:
303
+ print(stderr, file=sys.stderr)
304
+ else:
305
+ print(str(error), file=sys.stderr)
306
+ return error.returncode or 1
307
+
308
+ if args.format == "json":
309
+ print(json.dumps(asdict(report), ensure_ascii=False, indent=2))
310
+ else:
311
+ print(render_text(report))
312
+
313
+ return 0
314
+
315
+
316
+ if __name__ == "__main__":
317
+ raise SystemExit(main())
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+
7
+ echo "Running commit-time backend tests..."
8
+
9
+ (
10
+ cd "$ROOT_DIR"
11
+ cargo test -p gwt-tui commands::branches::tests:: -- --nocapture
12
+ cargo test -p gwt-tui commands::project::tests:: -- --nocapture
13
+ )
14
+
15
+ echo "Commit-time backend tests passed."
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ GUI_DIR="$ROOT_DIR/gwt-gui"
7
+
8
+ SUITES=(
9
+ "e2e/agent-canvas-browser.spec.ts"
10
+ "e2e/agent-terminal.spec.ts"
11
+ "e2e/branch-worktree.spec.ts"
12
+ "e2e/cleanup-migration.spec.ts"
13
+ "e2e/dialogs-common.spec.ts"
14
+ "e2e/issue-cache-sync.spec.ts"
15
+ "e2e/open-project-smoke.spec.ts"
16
+ "e2e/pr-management.spec.ts"
17
+ "e2e/project-management.spec.ts"
18
+ "e2e/responsive-performance.spec.ts"
19
+ "e2e/settings-config.spec.ts"
20
+ "e2e/status-bar.spec.ts"
21
+ "e2e/top-level-tools.spec.ts"
22
+ "e2e/voice-input-settings.spec.ts"
23
+ "e2e/web-preview-fallback.spec.ts"
24
+ "e2e/windows-shell-selection.spec.ts"
25
+ )
26
+
27
+ run_playwright_coverage() {
28
+ local mode="$1"
29
+
30
+ echo "Running commit-time E2E coverage in ${mode} mode..."
31
+ (
32
+ cd "$GUI_DIR"
33
+ rm -rf .nyc_output e2e/.nyc_output coverage-e2e
34
+ mkdir -p .nyc_output
35
+ if [ "$mode" = "headed" ]; then
36
+ E2E_COVERAGE=1 pnpm exec playwright test "${SUITES[@]}" --project=chromium --headed --workers=1
37
+ else
38
+ E2E_COVERAGE=1 pnpm exec playwright test "${SUITES[@]}" --project=chromium
39
+ fi
40
+ if ! python3 - <<'EOF'
41
+ from pathlib import Path
42
+ raise SystemExit(0 if any(Path(".nyc_output").glob("*.json")) else 1)
43
+ EOF
44
+ then
45
+ echo "E2E coverage run did not produce any .nyc_output JSON files." >&2
46
+ exit 1
47
+ fi
48
+ pnpm exec nyc report --nycrc-path .nycrc.e2e.json
49
+ node ../scripts/check-e2e-coverage-threshold.mjs
50
+ )
51
+ }
52
+
53
+ headed_possible=1
54
+
55
+ if [ -n "${CI:-}" ]; then
56
+ headed_possible=0
57
+ fi
58
+
59
+ if [ "$(uname -s)" = "Linux" ] && [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then
60
+ headed_possible=0
61
+ fi
62
+
63
+ if [ "$headed_possible" -eq 1 ]; then
64
+ run_playwright_coverage headed
65
+ exit 0
66
+ fi
67
+
68
+ echo "Headed Playwright coverage unavailable in this environment; using headless mode."
69
+ run_playwright_coverage headless
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ GUI_DIR="$ROOT_DIR/gwt-gui"
7
+
8
+ # Keep commit-time E2E focused on the current shell/UX-critical flows.
9
+ SUITES=(
10
+ "e2e/agent-canvas-browser.spec.ts"
11
+ "e2e/agent-terminal.spec.ts"
12
+ "e2e/branch-worktree.spec.ts"
13
+ "e2e/cleanup-migration.spec.ts"
14
+ "e2e/dialogs-common.spec.ts"
15
+ "e2e/issue-cache-sync.spec.ts"
16
+ "e2e/open-project-smoke.spec.ts"
17
+ "e2e/pr-management.spec.ts"
18
+ "e2e/project-management.spec.ts"
19
+ "e2e/responsive-performance.spec.ts"
20
+ "e2e/settings-config.spec.ts"
21
+ "e2e/status-bar.spec.ts"
22
+ "e2e/top-level-tools.spec.ts"
23
+ "e2e/voice-input-settings.spec.ts"
24
+ "e2e/web-preview-fallback.spec.ts"
25
+ "e2e/windows-shell-selection.spec.ts"
26
+ )
27
+
28
+ run_playwright() {
29
+ local mode="$1"
30
+ shift
31
+
32
+ echo "Running commit-time E2E in ${mode} mode..."
33
+ (
34
+ cd "$GUI_DIR"
35
+ if [ "$mode" = "headed" ]; then
36
+ pnpm exec playwright test "${SUITES[@]}" --project=chromium --headed --workers=1
37
+ else
38
+ pnpm exec playwright test "${SUITES[@]}" --project=chromium
39
+ fi
40
+ )
41
+ }
42
+
43
+ headed_possible=1
44
+
45
+ if [ -n "${CI:-}" ]; then
46
+ headed_possible=0
47
+ fi
48
+
49
+ # Linux/X11/Wayland environments without a display cannot run headed.
50
+ if [ "$(uname -s)" = "Linux" ] && [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then
51
+ headed_possible=0
52
+ fi
53
+
54
+ if [ "$headed_possible" -eq 1 ]; then
55
+ run_playwright headed
56
+ exit 0
57
+ fi
58
+
59
+ echo "Headed Playwright unavailable in this environment; using headless mode."
60
+ run_playwright headless
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ cargo test -p gwt-core -p gwt-tauri --all-features &
5
+ pid_rust=$!
6
+
7
+ (cd gwt-gui && pnpm test) &
8
+ pid_vitest=$!
9
+
10
+ fail=0
11
+ wait $pid_rust || fail=1
12
+ wait $pid_vitest || fail=1
13
+ exit $fail