td-ai-tools 1.2.1 → 1.2.3

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 (40) hide show
  1. package/README.md +2 -0
  2. package/lib/installer.js +27 -17
  3. package/lib/project-hooks.js +171 -0
  4. package/package.json +2 -2
  5. package/skills/README.md +1 -2
  6. package/skills/shopify-lint/SKILL.md +53 -0
  7. package/skills/shopify-lint/agents/openai.yaml +4 -0
  8. package/skills/shopify-lint/hooks/claude-stop.sh +49 -0
  9. package/skills/shopify-lint/hooks/codex-stop.sh +49 -0
  10. package/skills/shopify-lint/hooks/manifest.json +34 -0
  11. package/skills/shopify-lint/scripts/__pycache__/shopify_lint.cpython-312.pyc +0 -0
  12. package/skills/shopify-lint/scripts/setup.sh +62 -0
  13. package/skills/shopify-lint/scripts/shopify_lint.py +259 -0
  14. package/skills/shopify-lint/tests/__pycache__/test_shopify_lint.cpython-312.pyc +0 -0
  15. package/skills/shopify-lint/tests/test_shopify_lint.py +137 -0
  16. package/skills/shopify-lint/theme-check-theory/.theme-check.example.yml +14 -0
  17. package/skills/shopify-lint/theme-check-theory/README.md +123 -0
  18. package/skills/shopify-lint/theme-check-theory/configs/recommended.yml +8 -0
  19. package/skills/shopify-lint/theme-check-theory/package-lock.json +1947 -0
  20. package/skills/shopify-lint/theme-check-theory/package.json +44 -0
  21. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.test.ts +198 -0
  22. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.ts +143 -0
  23. package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.test.ts +137 -0
  24. package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.ts +64 -0
  25. package/skills/shopify-lint/theme-check-theory/src/index.test.ts +20 -0
  26. package/skills/shopify-lint/theme-check-theory/src/index.ts +11 -0
  27. package/skills/shopify-lint/theme-check-theory/src/test-utils.ts +31 -0
  28. package/skills/shopify-lint/theme-check-theory/src/utils/ast.ts +126 -0
  29. package/skills/shopify-lint/theme-check-theory/tsconfig.build.json +10 -0
  30. package/skills/shopify-lint/theme-check-theory/tsconfig.json +15 -0
  31. package/skills/shopify-lint/theme-check-theory/vitest.config.ts +11 -0
  32. package/skills/car-ticket-generator/SKILL.md +0 -131
  33. package/skills/car-ticket-generator/agents/openai.yaml +0 -4
  34. package/skills/everhour-basecamp-estimates/.env.example +0 -2
  35. package/skills/everhour-basecamp-estimates/SKILL.md +0 -75
  36. package/skills/everhour-basecamp-estimates/agents/openai.yaml +0 -4
  37. package/skills/everhour-basecamp-estimates/scripts/update_estimates.py +0 -654
  38. package/skills/everhour-basecamp-estimates/tests/test_update_estimates.py +0 -286
  39. package/skills/playwright-cli/PRIMER.md +0 -122
  40. package/skills/visual-regression/scripts/__pycache__/visual_regression.cpython-312.pyc +0 -0
package/README.md CHANGED
@@ -57,6 +57,8 @@ This keeps the installed assets available to both Claude-style and `.agents`-sty
57
57
 
58
58
  Some skills provide a recognized setup command (`setup.sh`, `scripts/setup.sh`, or `package.json` with `scripts.setup`). Non-interactive installs and updates run setup only when you pass `--setup`; interactive installs/updates ask for confirmation only when the selected skills include a recognized setup command. Accepted setup runs once inside each installed copy: `.claude/skills/<name>/` and `.agents/skills/<name>/`.
59
59
 
60
+ Skills can also declare reusable project-hook setup in `hooks/manifest.json`. The shared installer in `lib/project-hooks.js` copies listed hook assets and merges each registration into its Claude or Codex JSON settings file, preserving unrelated configuration and updating an existing hook with the same `type` and `command` instead of duplicating it. A hook manifest counts as a recognized setup even when the skill has no setup script, so future skills only need to provide their assets and declarative registrations.
61
+
60
62
  `install` now errors when the target item already exists. Use `update` to replace an existing installed skill or agent pack.
61
63
 
62
64
  `delete` removes installed items from both `.claude/` and `.agents/` target directories, and works on any installed skill or agent pack regardless of whether it is in the catalogue.
package/lib/installer.js CHANGED
@@ -9,6 +9,7 @@ import path from 'node:path';
9
9
  import { spawnSync } from 'node:child_process';
10
10
  import { copyDir } from './fs-utils.js';
11
11
  import { bundledAgentsIn } from './catalog.js';
12
+ import { hasProjectHooks, installProjectHooks } from './project-hooks.js';
12
13
 
13
14
  /** @typedef {import('./catalog.js').Ctx} Ctx */
14
15
  /** @typedef {import('./catalog.js').Target} Target */
@@ -66,7 +67,8 @@ export function findSkillSetup(skillDir) {
66
67
  * @returns {boolean}
67
68
  */
68
69
  export function skillHasSetup(ctx, name) {
69
- return Boolean(findSkillSetup(path.join(ctx.skillsDir, name)));
70
+ const skillDir = path.join(ctx.skillsDir, name);
71
+ return Boolean(findSkillSetup(skillDir)) || hasProjectHooks(skillDir);
70
72
  }
71
73
 
72
74
  /**
@@ -75,28 +77,36 @@ export function skillHasSetup(ctx, name) {
75
77
  * @param {string} name - Skill name.
76
78
  * @param {Target} target - The destination layout.
77
79
  * @param {string} dest - Installed skill directory.
80
+ * @param {string} projectRoot - Root of the project receiving the skill.
78
81
  * @param {ReportFn} report
79
82
  * @returns {boolean} Whether setup succeeded or no setup existed.
80
83
  */
81
- function runSkillSetup(name, target, dest, report) {
84
+ function runSkillSetup(name, target, dest, projectRoot, report) {
82
85
  const setup = findSkillSetup(dest);
83
- if (!setup) return true;
84
-
85
- report('step', `setup: ${name} running ${setup.command} ${setup.args.join(' ')} in ${target.root}/skills/${name}/`);
86
- const result = spawnSync(setup.command, setup.args, {
87
- cwd: dest,
88
- stdio: 'inherit',
89
- shell: false,
90
- });
91
- if (result.error) {
92
- report('error', `setup: ${name} failed in ${target.root}/skills/${name}/: ${result.error.message}`);
93
- return false;
86
+ if (setup) {
87
+ report('step', `setup: ${name} running ${setup.command} ${setup.args.join(' ')} in ${target.root}/skills/${name}/`);
88
+ const result = spawnSync(setup.command, setup.args, {
89
+ cwd: dest,
90
+ stdio: 'inherit',
91
+ shell: false,
92
+ });
93
+ if (result.error) {
94
+ report('error', `setup: ${name} failed in ${target.root}/skills/${name}/: ${result.error.message}`);
95
+ return false;
96
+ }
97
+ if (result.status !== 0) {
98
+ report('error', `setup: ${name} failed in ${target.root}/skills/${name}/ with exit code ${result.status}`);
99
+ return false;
100
+ }
101
+ report('success', `setup: ${name} completed in ${target.root}/skills/${name}/`);
94
102
  }
95
- if (result.status !== 0) {
96
- report('error', `setup: ${name} failed in ${target.root}/skills/${name}/ with exit code ${result.status}`);
103
+
104
+ try {
105
+ installProjectHooks(projectRoot, dest, { report });
106
+ } catch (error) {
107
+ report('error', `setup: ${name} hook installation failed: ${error.message}`);
97
108
  return false;
98
109
  }
99
- report('success', `setup: ${name} completed in ${target.root}/skills/${name}/`);
100
110
  return true;
101
111
  }
102
112
 
@@ -150,7 +160,7 @@ export function installSkill(ctx, name, { replaceExisting = false, runSetup = fa
150
160
  copyDir(src, dest);
151
161
  report('success', `skill: ${name} ${replaceExisting ? 'updated' : 'installed'} → ${target.root}/skills/${name}/`);
152
162
  registerBundledAgents(ctx, target, src, report);
153
- if (runSetup && !runSkillSetup(name, target, dest, report)) ok = false;
163
+ if (runSetup && !runSkillSetup(name, target, dest, ctx.targetRoot, report)) ok = false;
154
164
  }
155
165
  return ok;
156
166
  }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @file Declarative, non-destructive project hook installation for skills.
3
+ *
4
+ * Skills opt in by shipping `hooks/manifest.json`. Hook assets are copied from
5
+ * that directory and registrations are merged into runtime JSON settings.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ const MANIFEST_PATH = path.join('hooks', 'manifest.json');
11
+
12
+ /** @type {(kind: 'success'|'warn'|'info', message: string) => void} */
13
+ const noop = () => {};
14
+
15
+ /**
16
+ * Whether a skill directory contains a project hook manifest.
17
+ *
18
+ * @param {string} skillDir
19
+ * @returns {boolean}
20
+ */
21
+ export function hasProjectHooks(skillDir) {
22
+ return fs.existsSync(path.join(skillDir, MANIFEST_PATH));
23
+ }
24
+
25
+ /**
26
+ * Resolve a manifest path while preventing traversal outside its allowed root.
27
+ *
28
+ * @param {string} root
29
+ * @param {string} relativePath
30
+ * @param {string} label
31
+ * @returns {string}
32
+ */
33
+ function resolveWithin(root, relativePath, label) {
34
+ if (typeof relativePath !== 'string' || !relativePath || path.isAbsolute(relativePath)) {
35
+ throw new Error(`${label} must be a non-empty relative path`);
36
+ }
37
+ const resolvedRoot = path.resolve(root);
38
+ const resolved = path.resolve(resolvedRoot, relativePath);
39
+ const relation = path.relative(resolvedRoot, resolved);
40
+ if (relation === '..' || relation.startsWith(`..${path.sep}`) || path.isAbsolute(relation)) {
41
+ throw new Error(`${label} resolves outside the project root: ${relativePath}`);
42
+ }
43
+ return resolved;
44
+ }
45
+
46
+ /**
47
+ * Merge one runtime hook into a settings object.
48
+ *
49
+ * Command hooks are identified by `type` + `command`. Reinstalling updates the
50
+ * managed hook in place while preserving unrelated settings, event groups,
51
+ * group fields, hook fields, and sibling hooks.
52
+ *
53
+ * @param {Record<string, any>} settings
54
+ * @param {string} event
55
+ * @param {Record<string, any>} hook
56
+ * @param {Record<string, any>} [group]
57
+ * @returns {void}
58
+ */
59
+ export function mergeHookRegistration(settings, event, hook, group = {}) {
60
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
61
+ throw new Error('hook settings must be a JSON object');
62
+ }
63
+ if (typeof event !== 'string' || !event) {
64
+ throw new Error('hook event must be a non-empty string');
65
+ }
66
+ if (!hook || typeof hook !== 'object' || Array.isArray(hook)
67
+ || typeof hook.type !== 'string' || typeof hook.command !== 'string') {
68
+ throw new Error('hook must define string type and command fields');
69
+ }
70
+ if (!group || typeof group !== 'object' || Array.isArray(group)) {
71
+ throw new Error('hook group must be a JSON object');
72
+ }
73
+
74
+ if (settings.hooks === undefined) settings.hooks = {};
75
+ if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) {
76
+ throw new Error('settings.hooks must be a JSON object');
77
+ }
78
+ if (settings.hooks[event] === undefined) settings.hooks[event] = [];
79
+ if (!Array.isArray(settings.hooks[event])) {
80
+ throw new Error(`settings.hooks.${event} must be an array`);
81
+ }
82
+
83
+ for (const existingGroup of settings.hooks[event]) {
84
+ if (!existingGroup || !Array.isArray(existingGroup.hooks)) continue;
85
+ const existingHook = existingGroup.hooks.find(candidate => (
86
+ candidate?.type === hook.type && candidate?.command === hook.command
87
+ ));
88
+ if (!existingHook) continue;
89
+ Object.assign(existingGroup, group);
90
+ Object.assign(existingHook, hook);
91
+ return;
92
+ }
93
+
94
+ settings.hooks[event].push({ ...group, hooks: [{ ...hook }] });
95
+ }
96
+
97
+ /**
98
+ * Read JSON settings, backing up malformed input before starting a clean file.
99
+ *
100
+ * @param {string} settingsPath
101
+ * @param {(kind: 'success'|'warn'|'info', message: string) => void} report
102
+ * @returns {Record<string, any>}
103
+ */
104
+ function readSettings(settingsPath, report) {
105
+ if (!fs.existsSync(settingsPath)) return {};
106
+ try {
107
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
108
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
109
+ throw new Error('root value is not an object');
110
+ }
111
+ return settings;
112
+ } catch (error) {
113
+ const backupPath = `${settingsPath}.bak`;
114
+ fs.copyFileSync(settingsPath, backupPath);
115
+ report('warn', `hooks: ${settingsPath} was invalid JSON; backed up to ${backupPath}`);
116
+ return {};
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Install the hook assets and registrations declared by a skill.
122
+ *
123
+ * Manifest shape:
124
+ * `{ files: [{ source, destination }], registrations: [{ settings, event,
125
+ * hook, group? }] }`. File sources are relative to `hooks/`; destinations and
126
+ * settings paths are relative to the installing project.
127
+ *
128
+ * @param {string} projectRoot
129
+ * @param {string} skillDir
130
+ * @param {object} [options]
131
+ * @param {(kind: 'success'|'warn'|'info', message: string) => void} [options.report]
132
+ * @returns {boolean} Whether a manifest was found and installed.
133
+ */
134
+ export function installProjectHooks(projectRoot, skillDir, { report = noop } = {}) {
135
+ const manifestPath = path.join(skillDir, MANIFEST_PATH);
136
+ if (!fs.existsSync(manifestPath)) return false;
137
+
138
+ const hooksDir = path.dirname(manifestPath);
139
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
140
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
141
+ throw new Error(`hook manifest must be a JSON object: ${manifestPath}`);
142
+ }
143
+
144
+ for (const file of manifest.files || []) {
145
+ const source = resolveWithin(hooksDir, file.source, 'hook source');
146
+ const destination = resolveWithin(projectRoot, file.destination, 'hook destination');
147
+ if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
148
+ throw new Error(`hook source does not exist: ${source}`);
149
+ }
150
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
151
+ fs.copyFileSync(source, destination);
152
+ fs.chmodSync(destination, fs.statSync(source).mode);
153
+ report('success', `hooks: installed ${file.destination}`);
154
+ }
155
+
156
+ for (const registration of manifest.registrations || []) {
157
+ const settingsPath = resolveWithin(projectRoot, registration.settings, 'hook settings path');
158
+ const settings = readSettings(settingsPath, report);
159
+ mergeHookRegistration(
160
+ settings,
161
+ registration.event,
162
+ registration.hook,
163
+ registration.group || {},
164
+ );
165
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
166
+ fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
167
+ report('success', `hooks: merged ${registration.event} into ${registration.settings}`);
168
+ }
169
+
170
+ return true;
171
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "td-ai-tools",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Install agent skills and packs into your project",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "test": "node --test",
7
+ "test": "node --test test/*.test.js",
8
8
  "readmes": "node scripts/sync-readmes.js",
9
9
  "readmes:check": "node scripts/sync-readmes.js --check",
10
10
  "smoke:install": "./scripts/smoke-install.sh"
package/skills/README.md CHANGED
@@ -6,10 +6,8 @@
6
6
  - `basecamp`: Interact with Basecamp via the Basecamp CLI.
7
7
  - `browser-validation`: Before completing a task validate frontend or template changes in a real browser with the Playwright-CLI…
8
8
  - `cache-reset`: Clear and warm Laravel and Statamic caches (including Statamic Glide image caches) after content or template…
9
- - `car-ticket-generator`: Generate a ticket for the codex-auto-runner queue
10
9
  - `client-overview`: Generate a client-facing markdown report that summarizes all changes on the current branch against the…
11
10
  - `debugging-ios-webkit`: Debugs iOS Safari/Chrome-iOS rendering bugs — stale paints, viewport/browser-chrome clipping, mobile-only CSS…
12
- - `everhour-basecamp-estimates`: Bulk update Everhour task estimates from a Basecamp todo or todolist URL, then append bracketed hours to the…
13
11
  - `forge-cli`: Manage Laravel Forge servers, sites, and provisioned resources from the terminal with the Laravel Forge CLI,…
14
12
  - `horizon-component-migration`: Bundle Shopify Horizon components into a migration package for a different theme, including recursive…
15
13
  - `playwright-cli`: Automates browser interactions for web testing, screenshots, and data extraction.
@@ -18,6 +16,7 @@
18
16
  - `pull-request-statamic`: Generates GitHub pull request descriptions for Statamic and Laravel development by analyzing git diffs and…
19
17
  - `record-changes`: Update `docs/changes.md` by summarizing the current branch against the primary development branch.
20
18
  - `shopify-cli`: Shopify CLI workflows for theme development.
19
+ - `shopify-lint`: Run Shopify CLI Theme Check with Theory Digital's bundled custom checks while reporting and failing only on…
21
20
  - `stylesheet-migration`: Migrate Shopify Liquid `{% stylesheet %}` blocks into theme CSS assets using bundled Python scripts.
22
21
  - `td-js-vanilla-rules`: Theory Digital vanilla JavaScript standards for Shopify theme work.
23
22
  - `td-review`: Run parallel code review agents on a PR (including TD theme compliance) and produce a synthesized findings…
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: shopify-lint
3
+ version: 1.1.0
4
+ description: Run Shopify CLI Theme Check with Theory Digital's bundled custom checks while reporting and failing only on offenses in files modified on the current Git branch. Use when Codex needs to lint a Shopify theme, validate branch-scoped Liquid or theme changes, enforce Theory theme rules, or avoid surfacing pre-existing Theme Check offenses from untouched files.
5
+ ---
6
+
7
+ # Shopify Lint
8
+
9
+ ## Setup
10
+
11
+ The bundled `theme-check-theory` custom-check package ships as TypeScript source only; its `node_modules` and compiled `dist/` are not committed. Run setup once before the first run (and after upgrading the skill):
12
+
13
+ ```bash
14
+ bash .agents/skills/shopify-lint/scripts/setup.sh
15
+ ```
16
+
17
+ Installing with `td-ai-tools install --setup shopify-lint` runs this automatically. The script:
18
+
19
+ 1. Runs `npm install && npm run build` inside `theme-check-theory/`, producing `dist/index.js` — the CommonJS entry point the root `.theme-check.yml` requires.
20
+ 2. Writes a `.theme-check.yml` at the project root wiring in the bundled checks. If one already exists it is left untouched; ensure its `require:` list includes `./.agents/skills/shopify-lint/theme-check-theory`.
21
+ 3. Through the installer's shared project-hook setup, installs matching Claude and Codex Stop hooks and merges their registrations into `.claude/settings.json` and `.codex/hooks.json` without replacing unrelated settings or hooks. Re-running setup updates the Shopify Lint hook in place instead of duplicating it.
22
+
23
+ The Stop hooks run this skill's branch-scoped lint before handoff, return modified-file offenses to the agent, and allow handoff after three unsuccessful fix rounds. They require `jq`, `python3`, and Shopify CLI to be available when the hook runs. Directly running `scripts/setup.sh` performs the package build and Theme Check configuration; use `td-ai-tools install --setup` or `td-ai-tools update --setup` to apply the shared hook manifest as well.
24
+
25
+ Keep these dependencies inside `theme-check-theory/node_modules`; do not install Node dependencies at the theme root.
26
+
27
+ ## Usage
28
+
29
+ Run the bundled Python script from the Shopify theme root:
30
+
31
+ ```bash
32
+ python3 .agents/skills/shopify-lint/scripts/shopify_lint.py --path .
33
+ ```
34
+
35
+ The script runs `shopify theme check --output json`, identifies files changed since the current branch's merge-base, adds staged, unstaged, and untracked files, and emits only reports whose paths are in that set. The root `.theme-check.yml` directly requires the bundled `theme-check-theory` package from this skill directory.
36
+
37
+ ## Workflow
38
+
39
+ 1. Confirm `shopify` and `python3` are available.
40
+ 2. Run the bundled script instead of calling `shopify theme check` directly.
41
+ 3. Treat exit code `0` as no failing offenses in modified files, `1` as filtered offenses at or above the fail level, and `2` as a Git, CLI, or JSON-processing error.
42
+ 4. Fix reported issues and rerun until the command passes. Do not fix offenses in untouched files unless the user expands the scope.
43
+ 5. Keep custom-check dependencies inside `theme-check-theory/node_modules`. Do not install Node dependencies at the theme root.
44
+
45
+ The default base is `origin/HEAD`, then `origin/main`, `main`, `origin/master`, or `master`. Override it when needed:
46
+
47
+ ```bash
48
+ python3 .agents/skills/shopify-lint/scripts/shopify_lint.py --path . --base-ref origin/develop
49
+ ```
50
+
51
+ Set `SHOPIFY_LINT_BASE_REF` for the same override in automation. Use `--format json` for machine-readable filtered output and `--fail-level warning` or `--fail-level info` for stricter runs. When passing `--config <path>`, preserve the bundled package's `require` entry or the Theory checks will not load.
52
+
53
+ Do not use Theme Check auto-correction through this workflow because it can modify untouched files before filtering.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Shopify Lint"
3
+ short_description: "Lint modified theme files with Theory checks"
4
+ default_prompt: "Use $shopify-lint to run Theme Check for files modified on my current branch."
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # Stop hook: run Theory's branch-scoped Shopify Theme Check before the agent
3
+ # hands off. Blocks the stop (feeding offenses back to the agent) when lint
4
+ # finds offenses in files modified on the current branch.
5
+ set -u
6
+
7
+ input=$(cat)
8
+ session_id=$(printf '%s' "$input" | jq -r '.session_id // "unknown"')
9
+
10
+ root="${CLAUDE_PROJECT_DIR:-$PWD}"
11
+ cd "$root" || exit 0
12
+
13
+ # Only lint when this checkout is a Shopify theme with the skill installed.
14
+ lint_script=".agents/skills/shopify-lint/scripts/shopify_lint.py"
15
+ [ -f "$lint_script" ] || exit 0
16
+
17
+ # Cap consecutive blocks so an offense the agent can't fix doesn't loop forever.
18
+ counter_file="${TMPDIR:-/tmp}/claude-shopify-lint-stop-${session_id}.count"
19
+
20
+ output=$(python3 "$lint_script" --path . 2>&1)
21
+ status=$?
22
+
23
+ if [ "$status" -eq 0 ]; then
24
+ rm -f "$counter_file"
25
+ exit 0
26
+ fi
27
+
28
+ if [ "$status" -eq 1 ]; then
29
+ count=0
30
+ [ -f "$counter_file" ] && count=$(cat "$counter_file" 2>/dev/null || echo 0)
31
+ count=$((count + 1))
32
+ printf '%s' "$count" > "$counter_file"
33
+
34
+ if [ "$count" -gt 3 ]; then
35
+ rm -f "$counter_file"
36
+ jq -n --arg msg "Shopify lint still failing after 3 fix rounds — allowing handoff. Remaining offenses:\n$output" \
37
+ '{systemMessage: $msg}'
38
+ exit 0
39
+ fi
40
+
41
+ jq -n --arg reason "Shopify Theme Check found offenses in files modified on this branch. Fix these before finishing (leave offenses in unmodified files alone — they are pre-existing), then stop again and the check will re-run:
42
+
43
+ $output" '{decision: "block", reason: $reason}'
44
+ exit 0
45
+ fi
46
+
47
+ # Exit 2 = Git/CLI/JSON tooling error. Don't block handoff on a broken toolchain.
48
+ jq -n --arg msg "Shopify lint hook could not run (exit $status): $output" '{systemMessage: $msg}'
49
+ exit 0
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # Stop hook: run Theory's branch-scoped Shopify Theme Check before the agent
3
+ # hands off. Continues the turn with offenses from files modified on the
4
+ # current branch so Codex can fix them before trying to stop again.
5
+ set -u
6
+
7
+ input=$(cat)
8
+ session_id=$(printf '%s' "$input" | jq -r '.session_id // "unknown"' | tr -cd '[:alnum:]_-')
9
+
10
+ root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
11
+ cd "$root" || exit 0
12
+
13
+ # Only lint when this checkout is a Shopify theme with the skill installed.
14
+ lint_script=".agents/skills/shopify-lint/scripts/shopify_lint.py"
15
+ [ -f "$lint_script" ] || exit 0
16
+
17
+ # Cap consecutive continuations so an offense the agent can't fix doesn't loop forever.
18
+ counter_file="${TMPDIR:-/tmp}/codex-shopify-lint-stop-${session_id:-unknown}.count"
19
+
20
+ output=$(python3 "$lint_script" --path . 2>&1)
21
+ status=$?
22
+
23
+ if [ "$status" -eq 0 ]; then
24
+ rm -f "$counter_file"
25
+ exit 0
26
+ fi
27
+
28
+ if [ "$status" -eq 1 ]; then
29
+ count=0
30
+ [ -f "$counter_file" ] && count=$(cat "$counter_file" 2>/dev/null || echo 0)
31
+ count=$((count + 1))
32
+ printf '%s' "$count" > "$counter_file"
33
+
34
+ if [ "$count" -gt 3 ]; then
35
+ rm -f "$counter_file"
36
+ jq -n --arg msg "Shopify lint still failing after 3 fix rounds — allowing handoff. Remaining offenses:\n$output" \
37
+ '{systemMessage: $msg}'
38
+ exit 0
39
+ fi
40
+
41
+ jq -n --arg reason "Shopify Theme Check found offenses in files modified on this branch. Fix these before finishing (leave offenses in unmodified files alone — they are pre-existing), then stop again and the check will re-run:
42
+
43
+ $output" '{continue: false, stopReason: $reason}'
44
+ exit 0
45
+ fi
46
+
47
+ # Exit 2 = Git/CLI/JSON tooling error. Don't continue the turn on a broken toolchain.
48
+ jq -n --arg msg "Shopify lint hook could not run (exit $status): $output" '{systemMessage: $msg}'
49
+ exit 0
@@ -0,0 +1,34 @@
1
+ {
2
+ "files": [
3
+ {
4
+ "source": "claude-stop.sh",
5
+ "destination": ".claude/hooks/shopify-lint-stop.sh"
6
+ },
7
+ {
8
+ "source": "codex-stop.sh",
9
+ "destination": ".codex/hooks/td-shopify-lint-stop.sh"
10
+ }
11
+ ],
12
+ "registrations": [
13
+ {
14
+ "settings": ".claude/settings.json",
15
+ "event": "Stop",
16
+ "hook": {
17
+ "type": "command",
18
+ "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/shopify-lint-stop.sh\"",
19
+ "timeout": 300,
20
+ "statusMessage": "Running Shopify Theme Check on branch changes..."
21
+ }
22
+ },
23
+ {
24
+ "settings": ".codex/hooks.json",
25
+ "event": "Stop",
26
+ "hook": {
27
+ "type": "command",
28
+ "command": "bash \"$(git rev-parse --show-toplevel)/.codex/hooks/td-shopify-lint-stop.sh\"",
29
+ "timeout": 300,
30
+ "statusMessage": "Running Shopify Theme Check on branch changes..."
31
+ }
32
+ }
33
+ ]
34
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # shopify-lint setup
5
+ #
6
+ # Run automatically by the installer (`--setup`) from the installed skill
7
+ # directory, or manually with `bash scripts/setup.sh`. It:
8
+ # 1. Builds the bundled `theme-check-theory` custom-check package
9
+ # (its node_modules and dist/ are intentionally not committed).
10
+ # 2. Writes a `.theme-check.yml` at the project root that wires the bundled
11
+ # checks into Shopify CLI Theme Check.
12
+ # 3. When run by td-ai-tools, its shared setup stage installs the hook assets
13
+ # declared in hooks/manifest.json and merges their Stop registrations.
14
+
15
+ SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
16
+ PKG_DIR="$SKILL_DIR/theme-check-theory"
17
+
18
+ # 1. Build the bundled custom-check package.
19
+ echo "shopify-lint setup: building theme-check-theory in $PKG_DIR"
20
+ cd "$PKG_DIR"
21
+ npm install
22
+ npm run build
23
+
24
+ # 2. Generate the project-root .theme-check.yml.
25
+ #
26
+ # The installed layout is <project>/<.agents|.claude>/skills/shopify-lint, so
27
+ # the project root is three levels above the skill directory. Guard against
28
+ # running outside that layout (e.g. from the catalog repo) to avoid writing the
29
+ # config into an unexpected directory.
30
+ TARGET_DIR="$(cd "$SKILL_DIR/../.." && pwd)"
31
+ TARGET_BASE="$(basename "$TARGET_DIR")"
32
+ if [[ "$TARGET_BASE" != ".agents" && "$TARGET_BASE" != ".claude" ]]; then
33
+ echo "shopify-lint setup: unrecognized install layout ($TARGET_DIR); skipping .theme-check.yml generation." >&2
34
+ exit 0
35
+ fi
36
+
37
+ PROJECT_ROOT="$(dirname "$TARGET_DIR")"
38
+ THEME_CHECK_FILE="$PROJECT_ROOT/.theme-check.yml"
39
+
40
+ if [[ -f "$THEME_CHECK_FILE" ]]; then
41
+ echo "shopify-lint setup: $THEME_CHECK_FILE already exists; leaving it unchanged."
42
+ echo " Ensure its 'require:' list includes ./.agents/skills/shopify-lint/theme-check-theory"
43
+ exit 0
44
+ fi
45
+
46
+ cat > "$THEME_CHECK_FILE" <<'YAML'
47
+ extends:
48
+ - theme-check:recommended
49
+
50
+ require:
51
+ - ./.agents/skills/shopify-lint/theme-check-theory
52
+
53
+ UnusedSectionSettings:
54
+ enabled: true
55
+ severity: warning
56
+
57
+ UnguardedTextSetting:
58
+ enabled: true
59
+ severity: warning
60
+ YAML
61
+
62
+ echo "shopify-lint setup: wrote $THEME_CHECK_FILE"