td-ai-tools 1.2.0 → 1.2.2

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 (34) hide show
  1. package/package.json +1 -1
  2. package/skills/README.md +3 -0
  3. package/skills/client-overview/SKILL.md +93 -0
  4. package/skills/client-overview/agents/openai.yaml +4 -0
  5. package/skills/client-overview/scripts/branch_client_overview_context.py +317 -0
  6. package/skills/debugging-ios-webkit/SKILL.md +60 -0
  7. package/skills/debugging-ios-webkit/references/device.md +62 -0
  8. package/skills/debugging-ios-webkit/references/playwright.md +60 -0
  9. package/skills/debugging-ios-webkit/references/simulator.md +76 -0
  10. package/skills/debugging-ios-webkit/scripts/device_eval.py +62 -0
  11. package/skills/debugging-ios-webkit/scripts/device_experiment.py +99 -0
  12. package/skills/debugging-ios-webkit/scripts/device_snapshot.py +62 -0
  13. package/skills/record-changes/SKILL.md +3 -3
  14. package/skills/shopify-lint/SKILL.md +50 -0
  15. package/skills/shopify-lint/agents/openai.yaml +4 -0
  16. package/skills/shopify-lint/scripts/setup.sh +60 -0
  17. package/skills/shopify-lint/scripts/shopify_lint.py +259 -0
  18. package/skills/shopify-lint/tests/test_shopify_lint.py +137 -0
  19. package/skills/shopify-lint/theme-check-theory/.theme-check.example.yml +14 -0
  20. package/skills/shopify-lint/theme-check-theory/README.md +123 -0
  21. package/skills/shopify-lint/theme-check-theory/configs/recommended.yml +8 -0
  22. package/skills/shopify-lint/theme-check-theory/package-lock.json +1947 -0
  23. package/skills/shopify-lint/theme-check-theory/package.json +44 -0
  24. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.test.ts +198 -0
  25. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.ts +143 -0
  26. package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.test.ts +137 -0
  27. package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.ts +64 -0
  28. package/skills/shopify-lint/theme-check-theory/src/index.test.ts +20 -0
  29. package/skills/shopify-lint/theme-check-theory/src/index.ts +11 -0
  30. package/skills/shopify-lint/theme-check-theory/src/test-utils.ts +31 -0
  31. package/skills/shopify-lint/theme-check-theory/src/utils/ast.ts +126 -0
  32. package/skills/shopify-lint/theme-check-theory/tsconfig.build.json +10 -0
  33. package/skills/shopify-lint/theme-check-theory/tsconfig.json +15 -0
  34. package/skills/shopify-lint/theme-check-theory/vitest.config.ts +11 -0
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env python3
2
+ """Run Shopify Theme Check and report offenses only for branch-modified files."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+
15
+ SEVERITY_RANK = {"error": 0, "warning": 1, "info": 2}
16
+
17
+
18
+ class ShopifyLintError(RuntimeError):
19
+ """Raised when Git or Shopify CLI cannot produce filterable results."""
20
+
21
+
22
+ def run_command(command: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
23
+ return subprocess.run(
24
+ command,
25
+ cwd=cwd,
26
+ capture_output=True,
27
+ text=True,
28
+ check=False,
29
+ )
30
+
31
+
32
+ def git_output(repo_root: Path, *arguments: str) -> str:
33
+ result = run_command(["git", *arguments], repo_root)
34
+ if result.returncode != 0:
35
+ detail = result.stderr.strip() or result.stdout.strip()
36
+ raise ShopifyLintError(f"Git command failed: {detail}")
37
+ return result.stdout
38
+
39
+
40
+ def find_repo_root(theme_path: Path) -> Path:
41
+ result = run_command(["git", "rev-parse", "--show-toplevel"], theme_path)
42
+ if result.returncode != 0:
43
+ raise ShopifyLintError("Theme path must be inside a Git repository.")
44
+ return Path(result.stdout.strip()).resolve()
45
+
46
+
47
+ def discover_base_ref(repo_root: Path) -> str:
48
+ configured = os.environ.get("SHOPIFY_LINT_BASE_REF")
49
+ if configured:
50
+ return configured
51
+
52
+ remote_head = run_command(
53
+ ["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
54
+ repo_root,
55
+ )
56
+ candidates = [remote_head.stdout.strip(), "origin/main", "main", "origin/master", "master"]
57
+ for candidate in candidates:
58
+ if not candidate:
59
+ continue
60
+ result = run_command(
61
+ ["git", "rev-parse", "--verify", "--quiet", f"{candidate}^{{commit}}"],
62
+ repo_root,
63
+ )
64
+ if result.returncode == 0:
65
+ return candidate
66
+
67
+ raise ShopifyLintError(
68
+ "Could not determine the base branch. Pass --base-ref or set "
69
+ "SHOPIFY_LINT_BASE_REF."
70
+ )
71
+
72
+
73
+ def parse_nul_paths(output: str) -> set[str]:
74
+ return {path for path in output.split("\0") if path}
75
+
76
+
77
+ def collect_changed_files(repo_root: Path, base_ref: str) -> set[str]:
78
+ merge_base = git_output(repo_root, "merge-base", "HEAD", base_ref).strip()
79
+ committed = git_output(
80
+ repo_root,
81
+ "diff",
82
+ "--name-only",
83
+ "--diff-filter=ACMR",
84
+ "-z",
85
+ f"{merge_base}..HEAD",
86
+ )
87
+ working_tree = git_output(
88
+ repo_root,
89
+ "diff",
90
+ "--name-only",
91
+ "--diff-filter=ACMR",
92
+ "-z",
93
+ "HEAD",
94
+ )
95
+ untracked = git_output(
96
+ repo_root,
97
+ "ls-files",
98
+ "--others",
99
+ "--exclude-standard",
100
+ "-z",
101
+ )
102
+ return parse_nul_paths(committed) | parse_nul_paths(working_tree) | parse_nul_paths(
103
+ untracked
104
+ )
105
+
106
+
107
+ def run_theme_check(
108
+ theme_path: Path, fail_level: str, config: Path | None
109
+ ) -> list[dict[str, Any]]:
110
+ command = [
111
+ "shopify",
112
+ "theme",
113
+ "check",
114
+ "--output",
115
+ "json",
116
+ "--path",
117
+ str(theme_path),
118
+ "--fail-level",
119
+ fail_level,
120
+ ]
121
+ if config:
122
+ command.extend(["--config", str(config)])
123
+
124
+ try:
125
+ result = subprocess.run(
126
+ command,
127
+ capture_output=True,
128
+ text=True,
129
+ check=False,
130
+ )
131
+ except FileNotFoundError as error:
132
+ raise ShopifyLintError("Shopify CLI is not installed or is not on PATH.") from error
133
+
134
+ try:
135
+ reports = json.loads(result.stdout)
136
+ except json.JSONDecodeError as error:
137
+ detail = result.stderr.strip() or result.stdout.strip() or "no output"
138
+ raise ShopifyLintError(f"Shopify Theme Check failed: {detail}") from error
139
+
140
+ if not isinstance(reports, list):
141
+ raise ShopifyLintError("Shopify Theme Check returned an unexpected JSON shape.")
142
+ return reports
143
+
144
+
145
+ def repo_relative_path(path: str, repo_root: Path) -> str | None:
146
+ candidate = Path(path)
147
+ if not candidate.is_absolute():
148
+ candidate = repo_root / candidate
149
+ try:
150
+ return candidate.resolve().relative_to(repo_root.resolve()).as_posix()
151
+ except ValueError:
152
+ return None
153
+
154
+
155
+ def filter_reports(
156
+ reports: list[dict[str, Any]], changed_files: set[str], repo_root: Path
157
+ ) -> list[dict[str, Any]]:
158
+ return [
159
+ report
160
+ for report in reports
161
+ if repo_relative_path(str(report.get("path", "")), repo_root) in changed_files
162
+ ]
163
+
164
+
165
+ def normalized_reports(
166
+ reports: list[dict[str, Any]], repo_root: Path
167
+ ) -> list[dict[str, Any]]:
168
+ normalized = []
169
+ for report in reports:
170
+ item = dict(report)
171
+ item["path"] = repo_relative_path(str(report.get("path", "")), repo_root)
172
+ normalized.append(item)
173
+ return normalized
174
+
175
+
176
+ def render_json(reports: list[dict[str, Any]], repo_root: Path) -> str:
177
+ return json.dumps(normalized_reports(reports, repo_root), indent=2)
178
+
179
+
180
+ def render_text(reports: list[dict[str, Any]], repo_root: Path) -> str:
181
+ if not reports:
182
+ return "No Theme Check offenses found in files modified on this branch."
183
+
184
+ lines = []
185
+ offense_count = 0
186
+ for report in normalized_reports(reports, repo_root):
187
+ lines.append(str(report["path"]))
188
+ for offense in report.get("offenses", []):
189
+ offense_count += 1
190
+ severity = str(offense.get("severity", "unknown")).upper()
191
+ check = offense.get("check", "UnknownCheck")
192
+ row = offense.get("start_row", 0)
193
+ column = offense.get("start_column", 0)
194
+ message = offense.get("message", "")
195
+ lines.append(f" {severity} {check} {row}:{column} {message}".rstrip())
196
+ lines.append("")
197
+
198
+ file_label = "file" if len(reports) == 1 else "files"
199
+ offense_label = "offense" if offense_count == 1 else "offenses"
200
+ lines.append(
201
+ f"Theme Check found {offense_count} {offense_label} in "
202
+ f"{len(reports)} modified {file_label}."
203
+ )
204
+ return "\n".join(lines)
205
+
206
+
207
+ def exit_code_for(reports: list[dict[str, Any]], fail_level: str) -> int:
208
+ threshold = SEVERITY_RANK[fail_level]
209
+ for report in reports:
210
+ for offense in report.get("offenses", []):
211
+ severity = str(offense.get("severity", "info")).lower()
212
+ if SEVERITY_RANK.get(severity, 0) <= threshold:
213
+ return 1
214
+ return 0
215
+
216
+
217
+ def build_parser() -> argparse.ArgumentParser:
218
+ parser = argparse.ArgumentParser(
219
+ description="Run Shopify Theme Check for files modified on the current branch."
220
+ )
221
+ parser.add_argument("--path", type=Path, default=Path.cwd(), help="Theme root path.")
222
+ parser.add_argument("--base-ref", help="Base branch or commit used for branch changes.")
223
+ parser.add_argument(
224
+ "--fail-level",
225
+ choices=tuple(SEVERITY_RANK),
226
+ default="error",
227
+ help="Minimum filtered severity that returns exit code 1.",
228
+ )
229
+ parser.add_argument("--config", type=Path, help="Theme Check config path.")
230
+ parser.add_argument(
231
+ "--format", choices=("text", "json"), default="text", help="Filtered output format."
232
+ )
233
+ return parser
234
+
235
+
236
+ def main() -> int:
237
+ arguments = build_parser().parse_args()
238
+ theme_path = arguments.path.resolve()
239
+ try:
240
+ repo_root = find_repo_root(theme_path)
241
+ base_ref = arguments.base_ref or discover_base_ref(repo_root)
242
+ changed_files = collect_changed_files(repo_root, base_ref)
243
+ reports = run_theme_check(theme_path, arguments.fail_level, arguments.config)
244
+ filtered = filter_reports(reports, changed_files, repo_root)
245
+ except ShopifyLintError as error:
246
+ print(f"shopify-lint: {error}", file=sys.stderr)
247
+ return 2
248
+
249
+ output = (
250
+ render_json(filtered, repo_root)
251
+ if arguments.format == "json"
252
+ else render_text(filtered, repo_root)
253
+ )
254
+ print(output)
255
+ return exit_code_for(filtered, arguments.fail_level)
256
+
257
+
258
+ if __name__ == "__main__":
259
+ raise SystemExit(main())
@@ -0,0 +1,137 @@
1
+ """Tests for the branch-scoped Shopify Theme Check runner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ import unittest
10
+ from pathlib import Path
11
+ from unittest.mock import patch
12
+
13
+
14
+ SKILL_ROOT = Path(__file__).resolve().parents[1]
15
+ sys.path.insert(0, str(SKILL_ROOT / "scripts"))
16
+
17
+ import shopify_lint # noqa: E402
18
+
19
+
20
+ class ChangedFilesTests(unittest.TestCase):
21
+ def run_git(self, root: Path, *args: str) -> None:
22
+ subprocess.run(["git", *args], cwd=root, check=True, capture_output=True, text=True)
23
+
24
+ def test_collects_committed_staged_unstaged_and_untracked_files(self) -> None:
25
+ with tempfile.TemporaryDirectory() as directory:
26
+ root = Path(directory)
27
+ self.run_git(root, "init", "-q", "-b", "main")
28
+ self.run_git(root, "config", "user.email", "test@example.com")
29
+ self.run_git(root, "config", "user.name", "Test User")
30
+
31
+ for name in ("committed.liquid", "staged.liquid", "unstaged.liquid"):
32
+ (root / name).write_text("initial\n")
33
+ self.run_git(root, "add", ".")
34
+ self.run_git(root, "commit", "-qm", "initial")
35
+ self.run_git(root, "switch", "-qc", "feature")
36
+
37
+ (root / "committed.liquid").write_text("branch change\n")
38
+ self.run_git(root, "add", "committed.liquid")
39
+ self.run_git(root, "commit", "-qm", "branch change")
40
+ (root / "staged.liquid").write_text("staged change\n")
41
+ self.run_git(root, "add", "staged.liquid")
42
+ (root / "unstaged.liquid").write_text("unstaged change\n")
43
+ (root / "untracked.liquid").write_text("untracked\n")
44
+
45
+ changed = shopify_lint.collect_changed_files(root, "main")
46
+
47
+ self.assertEqual(
48
+ changed,
49
+ {
50
+ "committed.liquid",
51
+ "staged.liquid",
52
+ "unstaged.liquid",
53
+ "untracked.liquid",
54
+ },
55
+ )
56
+
57
+
58
+ class ThemeCheckTests(unittest.TestCase):
59
+ def test_runs_shopify_cli_with_json_output(self) -> None:
60
+ completed = subprocess.CompletedProcess([], 1, stdout="[]", stderr="")
61
+
62
+ with patch.object(shopify_lint.subprocess, "run", return_value=completed) as run:
63
+ result = shopify_lint.run_theme_check(Path("/theme"), "warning", None)
64
+
65
+ self.assertEqual(result, [])
66
+ run.assert_called_once_with(
67
+ [
68
+ "shopify",
69
+ "theme",
70
+ "check",
71
+ "--output",
72
+ "json",
73
+ "--path",
74
+ "/theme",
75
+ "--fail-level",
76
+ "warning",
77
+ ],
78
+ capture_output=True,
79
+ text=True,
80
+ check=False,
81
+ )
82
+
83
+ def test_filters_reports_to_changed_repo_relative_paths(self) -> None:
84
+ root = Path("/repo")
85
+ reports = [
86
+ {
87
+ "path": "/repo/sections/td-changed.liquid",
88
+ "offenses": [{"check": "ChangedCheck", "severity": "error"}],
89
+ },
90
+ {
91
+ "path": "/repo/sections/unchanged.liquid",
92
+ "offenses": [{"check": "IgnoredCheck", "severity": "error"}],
93
+ },
94
+ ]
95
+
96
+ filtered = shopify_lint.filter_reports(
97
+ reports, {"sections/td-changed.liquid"}, root
98
+ )
99
+
100
+ self.assertEqual(filtered, [reports[0]])
101
+
102
+ def test_json_output_contains_only_changed_file_reports(self) -> None:
103
+ reports = [
104
+ {
105
+ "path": "/repo/sections/td-changed.liquid",
106
+ "offenses": [{"check": "LiquidHTMLSyntaxError", "severity": "error"}],
107
+ }
108
+ ]
109
+
110
+ rendered = shopify_lint.render_json(reports, Path("/repo"))
111
+
112
+ self.assertEqual(
113
+ json.loads(rendered),
114
+ [
115
+ {
116
+ "path": "sections/td-changed.liquid",
117
+ "offenses": [
118
+ {"check": "LiquidHTMLSyntaxError", "severity": "error"}
119
+ ],
120
+ }
121
+ ],
122
+ )
123
+
124
+ def test_fail_level_is_recomputed_from_filtered_offenses(self) -> None:
125
+ warning_report = [
126
+ {
127
+ "path": "changed.liquid",
128
+ "offenses": [{"check": "RemoteAsset", "severity": "warning"}],
129
+ }
130
+ ]
131
+
132
+ self.assertEqual(shopify_lint.exit_code_for(warning_report, "error"), 0)
133
+ self.assertEqual(shopify_lint.exit_code_for(warning_report, "warning"), 1)
134
+
135
+
136
+ if __name__ == "__main__":
137
+ unittest.main()
@@ -0,0 +1,14 @@
1
+ extends:
2
+ - theme-check:recommended
3
+
4
+ require:
5
+ - ./node_modules/theme-check-theory
6
+
7
+ UnusedSectionSettings:
8
+ enabled: true
9
+ severity: warning
10
+
11
+ UnguardedTextSetting:
12
+ enabled: true
13
+ severity: warning
14
+
@@ -0,0 +1,123 @@
1
+ # theme-check-theory
2
+
3
+ Theory Digital's custom [Shopify Theme Check](https://shopify.dev/docs/storefronts/themes/tools/theme-check)
4
+ rules — house standards enforced on top of `theme-check:recommended`.
5
+
6
+ Built for the TypeScript Theme Check engine (`@shopify/theme-check-common` v3)
7
+ integrated into Shopify CLI. It is not compatible with the archived Ruby
8
+ `Shopify/theme-check` project.
9
+
10
+ ## Checks
11
+
12
+ | Code | Severity | What it catches |
13
+ | --- | --- | --- |
14
+ | `UnusedSectionSettings` | warning | A setting declared in `{% schema %}` that is never referenced in the file. |
15
+ | `UnguardedTextSetting` | warning | A text-like setting output with `{{ }}` and no presence guard (`{% if %}`/`{% unless %}`) or `\| default`. |
16
+
17
+ ### UnusedSectionSettings
18
+
19
+ This single-file check collects every `id` under `settings` and
20
+ `blocks[].settings` in the schema, then collects static
21
+ `section.settings.<id>` and `block.settings.<id>` references in the Liquid. It
22
+ reports every declared setting that has no matching reference.
23
+
24
+ If a file uses dynamic access such as `section.settings[key]`, or passes an
25
+ entire settings object elsewhere, the check cannot prove which setting is
26
+ unused. It therefore stays silent for the whole file to avoid false positives.
27
+
28
+ ### UnguardedTextSetting
29
+
30
+ Text-like settings render as an empty string when a merchant leaves them blank,
31
+ so outputting them unguarded is a recurring bug source. A setting is considered
32
+ handled if, anywhere in the file, it is either tested in an `if`, `unless`,
33
+ `elsif`, or `case` conditional, or output with a `default` filter.
34
+
35
+ Version 1 is deliberately coarse and per-file. It favors near-zero false
36
+ positives over catching a setting guarded in one branch and output raw in
37
+ another.
38
+
39
+ The setting types considered text-like are configurable with
40
+ `textualSettingTypes`. The defaults are `text`, `textarea`, `richtext`,
41
+ `inline_richtext`, `html`, `liquid`, and `url`.
42
+
43
+ ```yaml
44
+ UnguardedTextSetting:
45
+ enabled: true
46
+ severity: warning
47
+ textualSettingTypes:
48
+ - text
49
+ - textarea
50
+ - image_picker
51
+ ```
52
+
53
+ ## Usage in a project
54
+
55
+ Install the module as a development dependency:
56
+
57
+ ```bash
58
+ npm install --save-dev theme-check-theory
59
+ ```
60
+
61
+ Reference its CommonJS entry point from the project's `.theme-check.yml`:
62
+
63
+ ```yaml
64
+ extends:
65
+ - theme-check:recommended
66
+
67
+ require:
68
+ - ./node_modules/theme-check-theory
69
+
70
+ UnusedSectionSettings:
71
+ enabled: true
72
+ severity: warning
73
+
74
+ UnguardedTextSetting:
75
+ enabled: true
76
+ severity: warning
77
+ ```
78
+
79
+ See [`.theme-check.example.yml`](.theme-check.example.yml) for a copyable
80
+ configuration. The custom-check-only settings are also available in
81
+ [`configs/recommended.yml`](configs/recommended.yml).
82
+
83
+ Run Theme Check through Shopify CLI:
84
+
85
+ ```bash
86
+ shopify theme check
87
+ ```
88
+
89
+ ## CI
90
+
91
+ Start new checks at `warning` or `info` so they surface without blocking.
92
+ Review the findings across existing themes, then promote them to `error` once
93
+ the themes are clean.
94
+
95
+ ```yaml
96
+ # .github/workflows/theme-check.yml
97
+ - run: npm ci
98
+ - run: npx shopify theme check --fail-level error
99
+ ```
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ npm install
105
+ npm test
106
+ npm run build
107
+ ```
108
+
109
+ Tests use `runLiquidCheck` and `check` from
110
+ `@shopify/theme-check-common/dist/test/test-helper`, so Vitest exercises the
111
+ real Theme Check parser and traversal rather than a mocked AST.
112
+
113
+ ## Adding a check
114
+
115
+ 1. Create `src/checks/<name>.ts` exporting a `LiquidCheckDefinition`.
116
+ 2. Add it to the `checks` array in `src/index.ts`.
117
+ 3. Add `src/checks/<name>.test.ts` with true-positive and false-positive cases.
118
+ 4. Add it to `configs/recommended.yml`.
119
+
120
+ Shared schema parsing and setting-reference helpers live in `src/utils/ast.ts`.
121
+ Their node shapes are verified against `@shopify/liquid-html-parser` 2.9.x;
122
+ run the tests when upgrading that dependency.
123
+
@@ -0,0 +1,8 @@
1
+ UnusedSectionSettings:
2
+ enabled: true
3
+ severity: warning
4
+
5
+ UnguardedTextSetting:
6
+ enabled: true
7
+ severity: warning
8
+