@schalkneethling/toolkit 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -7
- package/commands/rpm-advance.md +13 -0
- package/commands/rpm-checkpoint.md +13 -0
- package/commands/rpm-feedback.md +15 -0
- package/commands/rpm-handoff.md +14 -0
- package/commands/rpm-review.md +13 -0
- package/commands/rpm-start.md +13 -0
- package/dist/index.mjs +90 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/skills/css-coder/SKILL.md +95 -0
- package/skills/css-coder/references/patterns.md +224 -0
- package/skills/css-tokens/README.md +152 -0
- package/skills/css-tokens/SKILL.md +125 -0
- package/skills/css-tokens/references/tokens.css +162 -0
- package/skills/frontend-security/SKILL.md +134 -0
- package/skills/frontend-security/references/csp-configuration.md +191 -0
- package/skills/frontend-security/references/csrf-protection.md +327 -0
- package/skills/frontend-security/references/dom-security.md +229 -0
- package/skills/frontend-security/references/file-upload-security.md +310 -0
- package/skills/frontend-security/references/framework-patterns.md +307 -0
- package/skills/frontend-security/references/input-validation.md +232 -0
- package/skills/frontend-security/references/jwt-security.md +300 -0
- package/skills/frontend-security/references/nodejs-npm-security.md +261 -0
- package/skills/frontend-security/references/xss-prevention.md +163 -0
- package/skills/frontend-testing/SKILL.md +357 -0
- package/skills/frontend-testing/references/accessibility-testing.md +368 -0
- package/skills/frontend-testing/references/aria-snapshots.md +517 -0
- package/skills/frontend-testing/references/locator-strategies.md +295 -0
- package/skills/frontend-testing/references/visual-regression.md +466 -0
- package/skills/refined-plan-mode/SKILL.md +84 -0
package/README.md
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
# claude-toolkit
|
|
2
2
|
|
|
3
|
-
CLI for managing [Claude Code](https://claude.com/claude-code) hooks and
|
|
3
|
+
CLI for managing [Claude Code](https://claude.com/claude-code) hooks, skills, and commands across projects. Hooks are copied into a project's `.claude/` directory; skills are copied into `.claude-toolkit/skills/` and symlinked into wherever Claude Code expects to find them; commands are copied into `.claude/commands/`.
|
|
4
4
|
|
|
5
5
|
## Repo layout
|
|
6
6
|
|
|
7
7
|
```plaintext
|
|
8
8
|
.
|
|
9
|
+
├── commands/ # Claude Code custom slash commands (*.md)
|
|
10
|
+
│ ├── rpm-start.md
|
|
11
|
+
│ ├── rpm-advance.md
|
|
12
|
+
│ └── ...
|
|
9
13
|
├── hooks/
|
|
14
|
+
│ ├── auto-approve-safe-commands/
|
|
15
|
+
│ │ ├── hook.mjs # the hook script itself
|
|
16
|
+
│ │ └── settings-fragment.json # deep-merged into .claude/settings.json on install
|
|
10
17
|
│ └── block-dangerous-commands/
|
|
11
|
-
│ ├── hook.
|
|
12
|
-
│ └── settings-fragment.json
|
|
13
|
-
├── skills/ #
|
|
18
|
+
│ ├── hook.mjs
|
|
19
|
+
│ └── settings-fragment.json
|
|
20
|
+
├── skills/ # skill directories dropped here
|
|
14
21
|
├── src/
|
|
15
22
|
│ └── index.ts # the toolkit CLI
|
|
16
23
|
├── package.json
|
|
@@ -41,6 +48,14 @@ Copies `skills/<name>/` into `<project>/.claude-toolkit/skills/<name>/` and crea
|
|
|
41
48
|
toolkit add skill css-shared-first --link .claude/skills --link docs/skills
|
|
42
49
|
```
|
|
43
50
|
|
|
51
|
+
### `toolkit add command <name>`
|
|
52
|
+
|
|
53
|
+
Copies `commands/<name>.md` into `<project>/.claude/commands/<name>.md`. Records the source hash in `.claude/toolkit-manifest.json`.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
toolkit add command rpm-start
|
|
57
|
+
```
|
|
58
|
+
|
|
44
59
|
### `toolkit update [--force]`
|
|
45
60
|
|
|
46
61
|
For every entry in `.claude/toolkit-manifest.json`, compares the current source hash to the installed hash:
|
|
@@ -49,13 +64,14 @@ For every entry in `.claude/toolkit-manifest.json`, compares the current source
|
|
|
49
64
|
- If the installed file was modified locally (its hash differs from the one recorded in the manifest), warns and skips unless `--force` is passed.
|
|
50
65
|
- Silent if everything is current.
|
|
51
66
|
|
|
52
|
-
### `toolkit list hook` / `toolkit list skill`
|
|
67
|
+
### `toolkit list hook` / `toolkit list skill` / `toolkit list command`
|
|
53
68
|
|
|
54
|
-
Lists available hooks or
|
|
69
|
+
Lists available hooks, skills, or commands shipped by this repo, with the current source hash.
|
|
55
70
|
|
|
56
71
|
## Versioning
|
|
57
72
|
|
|
58
|
-
- Each
|
|
73
|
+
- Each command is hashed over its `.md` file only.
|
|
74
|
+
- Each hook is hashed over `hook.mjs` only (not the README or `settings-fragment.json`).
|
|
59
75
|
- Each skill is hashed over every file in the skill directory (sorted by path).
|
|
60
76
|
- SHA-256, truncated to the first 7 hex characters.
|
|
61
77
|
|
|
@@ -65,6 +81,12 @@ The CLI writes `<project>/.claude/toolkit-manifest.json`:
|
|
|
65
81
|
|
|
66
82
|
```json
|
|
67
83
|
{
|
|
84
|
+
"commands": {
|
|
85
|
+
"rpm-start": {
|
|
86
|
+
"hash": "b8e2a1f",
|
|
87
|
+
"installedAt": "2026-04-18"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
68
90
|
"hooks": {
|
|
69
91
|
"block-dangerous-commands": {
|
|
70
92
|
"hash": "a3f9c2d",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# /rpm:advance
|
|
2
|
+
|
|
3
|
+
Use the Refined Plan Mode skill to continue the loop from the current state.
|
|
4
|
+
|
|
5
|
+
Steps:
|
|
6
|
+
|
|
7
|
+
1. Inspect `.plan-review/.current-version`, `.plan-review/approved-plan.md`, available plan files, and available feedback files.
|
|
8
|
+
2. If an approved plan exists, execute that plan.
|
|
9
|
+
3. If feedback exists for the current plan version, incorporate it into the next plan version.
|
|
10
|
+
4. If there is a current plan but no feedback or approval, remind the user that the plan is awaiting review.
|
|
11
|
+
5. If no plan exists, start with `/rpm:start` behavior.
|
|
12
|
+
|
|
13
|
+
Keep the response focused on the next state transition.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# /rpm:checkpoint
|
|
2
|
+
|
|
3
|
+
Use the Refined Plan Mode skill to summarize the current review-loop state.
|
|
4
|
+
|
|
5
|
+
Report:
|
|
6
|
+
|
|
7
|
+
- Current plan version from `.plan-review/.current-version`, if present.
|
|
8
|
+
- Latest plan file path.
|
|
9
|
+
- Whether feedback exists for the current version.
|
|
10
|
+
- Whether `.plan-review/approved-plan.md` exists.
|
|
11
|
+
- The recommended next action.
|
|
12
|
+
|
|
13
|
+
Do not modify files unless the user also asks you to advance or revise the plan.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# /rpm:feedback
|
|
2
|
+
|
|
3
|
+
Use the Refined Plan Mode skill to incorporate submitted feedback into the next plan version.
|
|
4
|
+
|
|
5
|
+
Steps:
|
|
6
|
+
|
|
7
|
+
1. Read `.plan-review/.current-version` to find the current version.
|
|
8
|
+
2. Read `.plan-review/feedback/plan-vN-feedback.json` for that version.
|
|
9
|
+
3. Read `.plan-review/plans/plan-vN.md`.
|
|
10
|
+
4. Address every feedback item in a revised plan, adding a "Feedback Addressed" section that maps comments to changes made.
|
|
11
|
+
5. Write the revision to `.plan-review/plans/plan-vN+1.md`.
|
|
12
|
+
6. Update `.plan-review/.current-version` to the new version.
|
|
13
|
+
7. Reply with a short note naming the feedback file read and the new plan file written.
|
|
14
|
+
|
|
15
|
+
If the feedback file is missing, report the exact path expected and stop.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# /rpm:handoff
|
|
2
|
+
|
|
3
|
+
Use the Refined Plan Mode skill to prepare a compact continuation summary for another agent or a future session.
|
|
4
|
+
|
|
5
|
+
Include:
|
|
6
|
+
|
|
7
|
+
- Goal.
|
|
8
|
+
- Current plan version and file path.
|
|
9
|
+
- Feedback status.
|
|
10
|
+
- Approval status.
|
|
11
|
+
- Important assumptions or unresolved decisions.
|
|
12
|
+
- Recommended next command or action.
|
|
13
|
+
|
|
14
|
+
Prefer reading the current plan and feedback files directly instead of relying on chat history.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# /rpm:review
|
|
2
|
+
|
|
3
|
+
Use the Refined Plan Mode skill to audit the latest plan before the user reviews it.
|
|
4
|
+
|
|
5
|
+
Steps:
|
|
6
|
+
|
|
7
|
+
1. Read the current plan version.
|
|
8
|
+
2. Review the plan for missing context, vague steps, untested assumptions, risky sequencing, and weak validation.
|
|
9
|
+
3. If improvements are needed, write a revised next version and update `.plan-review/.current-version`.
|
|
10
|
+
4. If the plan is already review-ready, leave files unchanged.
|
|
11
|
+
5. Reply with either the new plan version written or a short explanation that the current plan is ready for review.
|
|
12
|
+
|
|
13
|
+
This command reviews plan quality. It does not implement the plan.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# /rpm:start
|
|
2
|
+
|
|
3
|
+
Use the Refined Plan Mode skill to start a plan review loop for the user's current task.
|
|
4
|
+
|
|
5
|
+
Steps:
|
|
6
|
+
|
|
7
|
+
1. Inspect the repository enough to understand the task and relevant constraints.
|
|
8
|
+
2. Ask only blocking clarification questions. If reasonable assumptions are available, state them in the plan instead of stopping.
|
|
9
|
+
3. Create `.plan-review/plans/plan-v1.md` with the complete plan.
|
|
10
|
+
4. Create or update `.plan-review/.current-version` with `v1`.
|
|
11
|
+
5. Reply with a concise summary and tell the user the plan is ready for review in Refined Plan Mode.
|
|
12
|
+
|
|
13
|
+
Do not implement the plan yet unless the user explicitly asks you to proceed without review.
|
package/dist/index.mjs
CHANGED
|
@@ -2,23 +2,26 @@
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
//#region src/index.ts
|
|
9
9
|
/**
|
|
10
|
-
* toolkit — personal CLI for managing Claude Code hooks and
|
|
10
|
+
* toolkit — personal CLI for managing Claude Code hooks, skills, and commands.
|
|
11
11
|
*
|
|
12
12
|
* Commands:
|
|
13
13
|
* toolkit add hook <name>
|
|
14
14
|
* toolkit add skill <name> [--link <target>...]
|
|
15
|
+
* toolkit add command <name>
|
|
15
16
|
* toolkit update [--force]
|
|
16
17
|
* toolkit list hook
|
|
17
18
|
* toolkit list skill
|
|
19
|
+
* toolkit list command
|
|
18
20
|
*/
|
|
19
21
|
const TOOLKIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
22
|
const HOOKS_SRC = join(TOOLKIT_ROOT, "hooks");
|
|
21
23
|
const SKILLS_SRC = join(TOOLKIT_ROOT, "skills");
|
|
24
|
+
const COMMANDS_SRC = join(TOOLKIT_ROOT, "commands");
|
|
22
25
|
const PROJECT_ROOT = process.cwd();
|
|
23
26
|
const CLAUDE_DIR = join(PROJECT_ROOT, ".claude");
|
|
24
27
|
const TOOLKIT_DIR = join(PROJECT_ROOT, ".claude-toolkit");
|
|
@@ -31,17 +34,20 @@ function shortHash(content) {
|
|
|
31
34
|
}
|
|
32
35
|
function readManifest() {
|
|
33
36
|
if (!existsSync(MANIFEST_PATH)) return {
|
|
37
|
+
commands: {},
|
|
34
38
|
hooks: {},
|
|
35
39
|
skills: {}
|
|
36
40
|
};
|
|
37
41
|
try {
|
|
38
42
|
const parsed = JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
|
|
39
43
|
return {
|
|
44
|
+
commands: parsed.commands ?? {},
|
|
40
45
|
hooks: parsed.hooks ?? {},
|
|
41
46
|
skills: parsed.skills ?? {}
|
|
42
47
|
};
|
|
43
48
|
} catch {
|
|
44
49
|
return {
|
|
50
|
+
commands: {},
|
|
45
51
|
hooks: {},
|
|
46
52
|
skills: {}
|
|
47
53
|
};
|
|
@@ -63,6 +69,9 @@ function deepMerge(target, source) {
|
|
|
63
69
|
}
|
|
64
70
|
return source;
|
|
65
71
|
}
|
|
72
|
+
function hashCommandSource(name) {
|
|
73
|
+
return shortHash(readFileSync(join(COMMANDS_SRC, `${name}.md`)));
|
|
74
|
+
}
|
|
66
75
|
function hashHookSource(name) {
|
|
67
76
|
return shortHash(readFileSync(join(HOOKS_SRC, name, "hook.mjs")));
|
|
68
77
|
}
|
|
@@ -110,7 +119,39 @@ function diffLines(oldStr, newStr) {
|
|
|
110
119
|
}
|
|
111
120
|
return out.join("\n");
|
|
112
121
|
}
|
|
122
|
+
function sanitizeName(name, kind) {
|
|
123
|
+
name = basename(name);
|
|
124
|
+
if (!name) {
|
|
125
|
+
console.error(`Invalid ${kind} name`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
return name;
|
|
129
|
+
}
|
|
130
|
+
function addCommand(name) {
|
|
131
|
+
name = sanitizeName(name, "command");
|
|
132
|
+
const src = join(COMMANDS_SRC, `${name}.md`);
|
|
133
|
+
if (!existsSync(src)) {
|
|
134
|
+
console.error(`Command not found: ${name}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const commandsDir = join(CLAUDE_DIR, "commands");
|
|
138
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
139
|
+
const dest = resolve(commandsDir, `${name}.md`);
|
|
140
|
+
if (!dest.startsWith(commandsDir + sep)) {
|
|
141
|
+
console.error("Invalid command name");
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
writeFileSync(dest, readFileSync(src));
|
|
145
|
+
const manifest = readManifest();
|
|
146
|
+
manifest.commands[name] = {
|
|
147
|
+
hash: hashCommandSource(name),
|
|
148
|
+
installedAt: today()
|
|
149
|
+
};
|
|
150
|
+
writeManifest(manifest);
|
|
151
|
+
console.log(`Installed command: ${name} → ${relative(PROJECT_ROOT, dest)}`);
|
|
152
|
+
}
|
|
113
153
|
function addHook(name) {
|
|
154
|
+
name = sanitizeName(name, "hook");
|
|
114
155
|
const srcDir = join(HOOKS_SRC, name);
|
|
115
156
|
if (!existsSync(srcDir)) {
|
|
116
157
|
console.error(`Hook not found: ${name}`);
|
|
@@ -120,7 +161,11 @@ function addHook(name) {
|
|
|
120
161
|
const fragmentPath = join(srcDir, "settings-fragment.json");
|
|
121
162
|
const hooksDir = join(CLAUDE_DIR, "hooks");
|
|
122
163
|
mkdirSync(hooksDir, { recursive: true });
|
|
123
|
-
const destHook =
|
|
164
|
+
const destHook = resolve(hooksDir, `${name}.mjs`);
|
|
165
|
+
if (!destHook.startsWith(hooksDir + sep)) {
|
|
166
|
+
console.error("Invalid hook name");
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
124
169
|
writeFileSync(destHook, readFileSync(hookSrc));
|
|
125
170
|
if (existsSync(fragmentPath)) {
|
|
126
171
|
const fragment = JSON.parse(readFileSync(fragmentPath, "utf8"));
|
|
@@ -137,12 +182,17 @@ function addHook(name) {
|
|
|
137
182
|
console.log(`Installed hook: ${name} → ${relative(PROJECT_ROOT, destHook)}`);
|
|
138
183
|
}
|
|
139
184
|
function addSkill(name, links) {
|
|
185
|
+
name = sanitizeName(name, "skill");
|
|
140
186
|
const srcDir = join(SKILLS_SRC, name);
|
|
141
187
|
if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {
|
|
142
188
|
console.error(`Skill not found: ${name}`);
|
|
143
189
|
process.exit(1);
|
|
144
190
|
}
|
|
145
|
-
const destDir =
|
|
191
|
+
const destDir = resolve(TOOLKIT_DIR, "skills", name);
|
|
192
|
+
if (!destDir.startsWith(join(TOOLKIT_DIR, "skills") + sep)) {
|
|
193
|
+
console.error("Invalid skill name");
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
146
196
|
mkdirSync(dirname(destDir), { recursive: true });
|
|
147
197
|
cpSync(srcDir, destDir, { recursive: true });
|
|
148
198
|
const resolvedLinks = links.length > 0 ? links : [join(".claude", "skills")];
|
|
@@ -219,9 +269,36 @@ async function update(force) {
|
|
|
219
269
|
linkedTo: entry.linkedTo
|
|
220
270
|
};
|
|
221
271
|
}
|
|
272
|
+
for (const [name, entry] of Object.entries(manifest.commands)) {
|
|
273
|
+
const src = join(COMMANDS_SRC, `${name}.md`);
|
|
274
|
+
if (!existsSync(src)) continue;
|
|
275
|
+
const sourceHash = hashCommandSource(name);
|
|
276
|
+
if (sourceHash === entry.hash) continue;
|
|
277
|
+
changed = true;
|
|
278
|
+
console.log(`\n~ command: ${name} (${entry.hash} → ${sourceHash})`);
|
|
279
|
+
if (!(force || await confirm(`Update command "${name}"?`))) continue;
|
|
280
|
+
writeFileSync(join(CLAUDE_DIR, "commands", `${name}.md`), readFileSync(src));
|
|
281
|
+
manifest.commands[name] = {
|
|
282
|
+
hash: sourceHash,
|
|
283
|
+
installedAt: today()
|
|
284
|
+
};
|
|
285
|
+
}
|
|
222
286
|
if (changed) writeManifest(manifest);
|
|
223
287
|
}
|
|
224
288
|
function list(kind) {
|
|
289
|
+
if (kind === "command") {
|
|
290
|
+
if (!existsSync(COMMANDS_SRC)) {
|
|
291
|
+
console.log("(no commands available)");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const files = readdirSync(COMMANDS_SRC).filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
|
|
295
|
+
if (files.length === 0) {
|
|
296
|
+
console.log("(no commands available)");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
for (const name of files) console.log(`${name} ${hashCommandSource(name)}`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
225
302
|
const dir = kind === "hook" ? HOOKS_SRC : SKILLS_SRC;
|
|
226
303
|
if (!existsSync(dir)) {
|
|
227
304
|
console.log(`(no ${kind}s available)`);
|
|
@@ -241,9 +318,11 @@ function usage() {
|
|
|
241
318
|
console.error(`Usage:
|
|
242
319
|
toolkit add hook <name>
|
|
243
320
|
toolkit add skill <name> [--link <target>]...
|
|
321
|
+
toolkit add command <name>
|
|
244
322
|
toolkit update [--force]
|
|
245
323
|
toolkit list hook
|
|
246
|
-
toolkit list skill
|
|
324
|
+
toolkit list skill
|
|
325
|
+
toolkit list command`);
|
|
247
326
|
process.exit(1);
|
|
248
327
|
}
|
|
249
328
|
async function main() {
|
|
@@ -272,11 +351,16 @@ async function main() {
|
|
|
272
351
|
addSkill(name, links ? links : []);
|
|
273
352
|
return;
|
|
274
353
|
}
|
|
354
|
+
if (command === "add" && resource === "command") {
|
|
355
|
+
if (!name) usage();
|
|
356
|
+
addCommand(name);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
275
359
|
if (command === "update") {
|
|
276
360
|
await update(force);
|
|
277
361
|
return;
|
|
278
362
|
}
|
|
279
|
-
if (command === "list" && (resource === "hook" || resource === "skill")) {
|
|
363
|
+
if (command === "list" && (resource === "hook" || resource === "skill" || resource === "command")) {
|
|
280
364
|
list(resource);
|
|
281
365
|
return;
|
|
282
366
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * toolkit — personal CLI for managing Claude Code hooks and skills.\n *\n * Commands:\n * toolkit add hook <name>\n * toolkit add skill <name> [--link <target>...]\n * toolkit update [--force]\n * toolkit list hook\n * toolkit list skill\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n cpSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n statSync,\n symlinkSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { createInterface } from \"node:readline/promises\";\nimport { dirname, join, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\nconst TOOLKIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst HOOKS_SRC = join(TOOLKIT_ROOT, \"hooks\");\nconst SKILLS_SRC = join(TOOLKIT_ROOT, \"skills\");\n\nconst PROJECT_ROOT = process.cwd();\nconst CLAUDE_DIR = join(PROJECT_ROOT, \".claude\");\nconst TOOLKIT_DIR = join(PROJECT_ROOT, \".claude-toolkit\");\nconst MANIFEST_PATH = join(CLAUDE_DIR, \"toolkit-manifest.json\");\n\ntype HookEntry = { hash: string; installedAt: string };\ntype SkillEntry = { hash: string; installedAt: string; linkedTo: string[] };\ntype Manifest = {\n hooks: Record<string, HookEntry>;\n skills: Record<string, SkillEntry>;\n};\n\n// ---------- helpers ----------\n\nfunction today(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction shortHash(content: string | Buffer): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 7);\n}\n\nfunction readManifest(): Manifest {\n if (!existsSync(MANIFEST_PATH)) {\n return { hooks: {}, skills: {} };\n }\n\n try {\n const parsed = JSON.parse(\n readFileSync(MANIFEST_PATH, \"utf8\"),\n ) as Partial<Manifest>;\n return { hooks: parsed.hooks ?? {}, skills: parsed.skills ?? {} };\n } catch {\n return { hooks: {}, skills: {} };\n }\n}\n\nfunction writeManifest(m: Manifest): void {\n mkdirSync(CLAUDE_DIR, { recursive: true });\n writeFileSync(MANIFEST_PATH, JSON.stringify(m, null, 2) + \"\\n\");\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction deepMerge<T>(target: T, source: T): T {\n if (Array.isArray(target) && Array.isArray(source)) {\n return [...target, ...source] as T;\n }\n if (isPlainObject(target) && isPlainObject(source)) {\n const out: Record<string, unknown> = { ...target };\n for (const [k, v] of Object.entries(source)) {\n out[k] = k in out ? deepMerge(out[k], v) : v;\n }\n return out as T;\n }\n return source;\n}\n\nfunction hashHookSource(name: string): string {\n const p = join(HOOKS_SRC, name, \"hook.mjs\");\n return shortHash(readFileSync(p));\n}\n\nfunction hashSkillSource(name: string): string {\n const dir = join(SKILLS_SRC, name);\n const files = collectFiles(dir).sort();\n const h = createHash(\"sha256\");\n for (const f of files) {\n h.update(relative(dir, f));\n h.update(\"\\0\");\n h.update(readFileSync(f));\n h.update(\"\\0\");\n }\n return h.digest(\"hex\").slice(0, 7);\n}\n\nfunction collectFiles(dir: string): string[] {\n const out: string[] = [];\n if (!existsSync(dir)) {\n return out;\n }\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === \".gitkeep\") {\n continue;\n }\n\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n out.push(...collectFiles(full));\n } else if (entry.isFile()) {\n out.push(full);\n }\n }\n return out;\n}\n\nasync function confirm(question: string): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();\n rl.close();\n return answer === \"y\" || answer === \"yes\";\n}\n\nfunction diffLines(oldStr: string, newStr: string): string {\n const a = oldStr.split(\"\\n\");\n const b = newStr.split(\"\\n\");\n const out: string[] = [];\n const max = Math.max(a.length, b.length);\n for (let i = 0; i < max; i++) {\n if (a[i] === b[i]) {\n continue;\n }\n\n if (a[i] !== undefined) {\n out.push(`- ${a[i]}`);\n }\n\n if (b[i] !== undefined) {\n out.push(`+ ${b[i]}`);\n }\n }\n return out.join(\"\\n\");\n}\n\n// ---------- commands ----------\n\nfunction addHook(name: string): void {\n const srcDir = join(HOOKS_SRC, name);\n if (!existsSync(srcDir)) {\n console.error(`Hook not found: ${name}`);\n process.exit(1);\n }\n\n const hookSrc = join(srcDir, \"hook.mjs\");\n const fragmentPath = join(srcDir, \"settings-fragment.json\");\n\n const hooksDir = join(CLAUDE_DIR, \"hooks\");\n mkdirSync(hooksDir, { recursive: true });\n const destHook = join(hooksDir, `${name}.mjs`);\n writeFileSync(destHook, readFileSync(hookSrc));\n\n if (existsSync(fragmentPath)) {\n const fragment = JSON.parse(readFileSync(fragmentPath, \"utf8\"));\n const settingsPath = join(CLAUDE_DIR, \"settings.json\");\n const current = existsSync(settingsPath)\n ? JSON.parse(readFileSync(settingsPath, \"utf8\"))\n : {};\n const merged = deepMerge(current, fragment);\n writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + \"\\n\");\n }\n\n const manifest = readManifest();\n manifest.hooks[name] = { hash: hashHookSource(name), installedAt: today() };\n writeManifest(manifest);\n\n console.log(`Installed hook: ${name} → ${relative(PROJECT_ROOT, destHook)}`);\n}\n\nfunction addSkill(name: string, links: string[]): void {\n const srcDir = join(SKILLS_SRC, name);\n if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {\n console.error(`Skill not found: ${name}`);\n process.exit(1);\n }\n\n const destDir = join(TOOLKIT_DIR, \"skills\", name);\n mkdirSync(dirname(destDir), { recursive: true });\n cpSync(srcDir, destDir, { recursive: true });\n\n const resolvedLinks = links.length > 0 ? links : [join(\".claude\", \"skills\")];\n for (const link of resolvedLinks) {\n const linkDir = resolve(PROJECT_ROOT, link);\n mkdirSync(linkDir, { recursive: true });\n\n const linkPath = join(linkDir, name);\n if (existsSync(linkPath) || lstatExists(linkPath)) {\n unlinkSync(linkPath);\n }\n\n const relTarget = relative(linkDir, destDir);\n symlinkSync(relTarget, linkPath, \"dir\");\n }\n\n const manifest = readManifest();\n manifest.skills[name] = {\n hash: hashSkillSource(name),\n installedAt: today(),\n linkedTo: resolvedLinks,\n };\n writeManifest(manifest);\n\n console.log(`Installed skill: ${name} → ${relative(PROJECT_ROOT, destDir)}`);\n for (const l of resolvedLinks) {\n console.log(` linked: ${join(l, name)}`);\n }\n}\n\nfunction lstatExists(p: string): boolean {\n try {\n lstatSync(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function update(force: boolean): Promise<void> {\n const manifest = readManifest();\n let changed = false;\n\n for (const [name, entry] of Object.entries(manifest.hooks)) {\n const srcDir = join(HOOKS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashHookSource(name);\n const installedPath = join(CLAUDE_DIR, \"hooks\", `${name}.mjs`);\n const installedHash = existsSync(installedPath)\n ? shortHash(readFileSync(installedPath))\n : null;\n\n const sourceChanged = sourceHash !== entry.hash;\n const locallyModified =\n installedHash !== null && installedHash !== entry.hash;\n\n if (!sourceChanged && !locallyModified) {\n continue;\n }\n\n changed = true;\n\n if (locallyModified && !force) {\n console.warn(\n `! hook \"${name}\" was modified locally (installed=${installedHash}, manifest=${entry.hash}). Use --force to overwrite.`,\n );\n continue;\n }\n\n if (sourceChanged) {\n const oldSrc = existsSync(installedPath)\n ? readFileSync(installedPath, \"utf8\")\n : \"\";\n const newSrc = readFileSync(join(srcDir, \"hook.mjs\"), \"utf8\");\n console.log(`\\n~ hook: ${name} (${entry.hash} → ${sourceHash})`);\n console.log(diffLines(oldSrc, newSrc));\n const ok = force || (await confirm(`Update hook \"${name}\"?`));\n\n if (!ok) {\n continue;\n }\n\n writeFileSync(installedPath, newSrc);\n manifest.hooks[name] = { hash: sourceHash, installedAt: today() };\n }\n }\n\n for (const [name, entry] of Object.entries(manifest.skills)) {\n const srcDir = join(SKILLS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashSkillSource(name);\n if (sourceHash === entry.hash) {\n continue;\n }\n\n changed = true;\n console.log(`\\n~ skill: ${name} (${entry.hash} → ${sourceHash})`);\n const ok = force || (await confirm(`Update skill \"${name}\"?`));\n if (!ok) {\n continue;\n }\n\n const destDir = join(TOOLKIT_DIR, \"skills\", name);\n cpSync(srcDir, destDir, { recursive: true, force: true });\n manifest.skills[name] = {\n hash: sourceHash,\n installedAt: today(),\n linkedTo: entry.linkedTo,\n };\n }\n\n if (changed) {\n writeManifest(manifest);\n }\n}\n\nfunction list(kind: \"hook\" | \"skill\"): void {\n const dir = kind === \"hook\" ? HOOKS_SRC : SKILLS_SRC;\n if (!existsSync(dir)) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n const entries = readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory() || (kind === \"skill\" && e.isSymbolicLink()))\n .map((e) => e.name);\n\n if (entries.length === 0) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n\n for (const name of entries) {\n const hash = kind === \"hook\" ? hashHookSource(name) : hashSkillSource(name);\n console.log(`${name} ${hash}`);\n }\n}\n\n// ---------- argv ----------\n\nfunction usage(): never {\n console.error(\n `Usage:\n toolkit add hook <name>\n toolkit add skill <name> [--link <target>]...\n toolkit update [--force]\n toolkit list hook\n toolkit list skill`,\n );\n process.exit(1);\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n options: {\n force: {\n default: false,\n type: \"boolean\",\n },\n links: {\n multiple: true,\n type: \"string\",\n },\n },\n allowPositionals: true,\n });\n\n const { force, links } = values;\n const [command, resource, name] = positionals;\n\n if (command === \"add\" && resource === \"hook\") {\n if (!name) {\n usage();\n }\n\n addHook(name);\n return;\n }\n\n if (command === \"add\" && resource === \"skill\") {\n if (!name) {\n usage();\n }\n\n addSkill(name, links ? links : []);\n return;\n }\n\n if (command === \"update\") {\n await update(force);\n return;\n }\n\n if (command === \"list\" && (resource === \"hook\" || resource === \"skill\")) {\n list(resource);\n return;\n }\n\n usage();\n}\n\nmain().catch((err) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;AA+BA,MAAM,eAAe,QAAQ,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,KAAK;AAC3E,MAAM,YAAY,KAAK,cAAc,QAAQ;AAC7C,MAAM,aAAa,KAAK,cAAc,SAAS;AAE/C,MAAM,eAAe,QAAQ,KAAK;AAClC,MAAM,aAAa,KAAK,cAAc,UAAU;AAChD,MAAM,cAAc,KAAK,cAAc,kBAAkB;AACzD,MAAM,gBAAgB,KAAK,YAAY,wBAAwB;AAW/D,SAAS,QAAgB;AACvB,yBAAO,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,GAAG,GAAG;;AAG9C,SAAS,UAAU,SAAkC;AACnD,QAAO,WAAW,SAAS,CAAC,OAAO,QAAQ,CAAC,OAAO,MAAM,CAAC,MAAM,GAAG,EAAE;;AAGvE,SAAS,eAAyB;AAChC,KAAI,CAAC,WAAW,cAAc,CAC5B,QAAO;EAAE,OAAO,EAAE;EAAE,QAAQ,EAAE;EAAE;AAGlC,KAAI;EACF,MAAM,SAAS,KAAK,MAClB,aAAa,eAAe,OAAO,CACpC;AACD,SAAO;GAAE,OAAO,OAAO,SAAS,EAAE;GAAE,QAAQ,OAAO,UAAU,EAAE;GAAE;SAC3D;AACN,SAAO;GAAE,OAAO,EAAE;GAAE,QAAQ,EAAE;GAAE;;;AAIpC,SAAS,cAAc,GAAmB;AACxC,WAAU,YAAY,EAAE,WAAW,MAAM,CAAC;AAC1C,eAAc,eAAe,KAAK,UAAU,GAAG,MAAM,EAAE,GAAG,KAAK;;AAGjE,SAAS,cAAc,GAA0C;AAC/D,QAAO,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,MAAM,QAAQ,EAAE;;AAGjE,SAAS,UAAa,QAAW,QAAc;AAC7C,KAAI,MAAM,QAAQ,OAAO,IAAI,MAAM,QAAQ,OAAO,CAChD,QAAO,CAAC,GAAG,QAAQ,GAAG,OAAO;AAE/B,KAAI,cAAc,OAAO,IAAI,cAAc,OAAO,EAAE;EAClD,MAAM,MAA+B,EAAE,GAAG,QAAQ;AAClD,OAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,OAAO,CACzC,KAAI,KAAK,KAAK,MAAM,UAAU,IAAI,IAAI,EAAE,GAAG;AAE7C,SAAO;;AAET,QAAO;;AAGT,SAAS,eAAe,MAAsB;AAE5C,QAAO,UAAU,aADP,KAAK,WAAW,MAAM,WAAW,CACX,CAAC;;AAGnC,SAAS,gBAAgB,MAAsB;CAC7C,MAAM,MAAM,KAAK,YAAY,KAAK;CAClC,MAAM,QAAQ,aAAa,IAAI,CAAC,MAAM;CACtC,MAAM,IAAI,WAAW,SAAS;AAC9B,MAAK,MAAM,KAAK,OAAO;AACrB,IAAE,OAAO,SAAS,KAAK,EAAE,CAAC;AAC1B,IAAE,OAAO,KAAK;AACd,IAAE,OAAO,aAAa,EAAE,CAAC;AACzB,IAAE,OAAO,KAAK;;AAEhB,QAAO,EAAE,OAAO,MAAM,CAAC,MAAM,GAAG,EAAE;;AAGpC,SAAS,aAAa,KAAuB;CAC3C,MAAM,MAAgB,EAAE;AACxB,KAAI,CAAC,WAAW,IAAI,CAClB,QAAO;AAGT,MAAK,MAAM,SAAS,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC,EAAE;AAC7D,MAAI,MAAM,SAAS,WACjB;EAGF,MAAM,OAAO,KAAK,KAAK,MAAM,KAAK;AAClC,MAAI,MAAM,aAAa,CACrB,KAAI,KAAK,GAAG,aAAa,KAAK,CAAC;WACtB,MAAM,QAAQ,CACvB,KAAI,KAAK,KAAK;;AAGlB,QAAO;;AAGT,eAAe,QAAQ,UAAoC;CACzD,MAAM,KAAK,gBAAgB;EAAE,OAAO,QAAQ;EAAO,QAAQ,QAAQ;EAAQ,CAAC;CAC5E,MAAM,UAAU,MAAM,GAAG,SAAS,GAAG,SAAS,SAAS,EAAE,MAAM,CAAC,aAAa;AAC7E,IAAG,OAAO;AACV,QAAO,WAAW,OAAO,WAAW;;AAGtC,SAAS,UAAU,QAAgB,QAAwB;CACzD,MAAM,IAAI,OAAO,MAAM,KAAK;CAC5B,MAAM,IAAI,OAAO,MAAM,KAAK;CAC5B,MAAM,MAAgB,EAAE;CACxB,MAAM,MAAM,KAAK,IAAI,EAAE,QAAQ,EAAE,OAAO;AACxC,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,MAAI,EAAE,OAAO,EAAE,GACb;AAGF,MAAI,EAAE,OAAO,KAAA,EACX,KAAI,KAAK,KAAK,EAAE,KAAK;AAGvB,MAAI,EAAE,OAAO,KAAA,EACX,KAAI,KAAK,KAAK,EAAE,KAAK;;AAGzB,QAAO,IAAI,KAAK,KAAK;;AAKvB,SAAS,QAAQ,MAAoB;CACnC,MAAM,SAAS,KAAK,WAAW,KAAK;AACpC,KAAI,CAAC,WAAW,OAAO,EAAE;AACvB,UAAQ,MAAM,mBAAmB,OAAO;AACxC,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,KAAK,QAAQ,WAAW;CACxC,MAAM,eAAe,KAAK,QAAQ,yBAAyB;CAE3D,MAAM,WAAW,KAAK,YAAY,QAAQ;AAC1C,WAAU,UAAU,EAAE,WAAW,MAAM,CAAC;CACxC,MAAM,WAAW,KAAK,UAAU,GAAG,KAAK,MAAM;AAC9C,eAAc,UAAU,aAAa,QAAQ,CAAC;AAE9C,KAAI,WAAW,aAAa,EAAE;EAC5B,MAAM,WAAW,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;EAC/D,MAAM,eAAe,KAAK,YAAY,gBAAgB;EAItD,MAAM,SAAS,UAHC,WAAW,aAAa,GACpC,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC,GAC9C,EAAE,EAC4B,SAAS;AAC3C,gBAAc,cAAc,KAAK,UAAU,QAAQ,MAAM,EAAE,GAAG,KAAK;;CAGrE,MAAM,WAAW,cAAc;AAC/B,UAAS,MAAM,QAAQ;EAAE,MAAM,eAAe,KAAK;EAAE,aAAa,OAAO;EAAE;AAC3E,eAAc,SAAS;AAEvB,SAAQ,IAAI,mBAAmB,KAAK,KAAK,SAAS,cAAc,SAAS,GAAG;;AAG9E,SAAS,SAAS,MAAc,OAAuB;CACrD,MAAM,SAAS,KAAK,YAAY,KAAK;AACrC,KAAI,CAAC,WAAW,OAAO,IAAI,CAAC,SAAS,OAAO,CAAC,aAAa,EAAE;AAC1D,UAAQ,MAAM,oBAAoB,OAAO;AACzC,UAAQ,KAAK,EAAE;;CAGjB,MAAM,UAAU,KAAK,aAAa,UAAU,KAAK;AACjD,WAAU,QAAQ,QAAQ,EAAE,EAAE,WAAW,MAAM,CAAC;AAChD,QAAO,QAAQ,SAAS,EAAE,WAAW,MAAM,CAAC;CAE5C,MAAM,gBAAgB,MAAM,SAAS,IAAI,QAAQ,CAAC,KAAK,WAAW,SAAS,CAAC;AAC5E,MAAK,MAAM,QAAQ,eAAe;EAChC,MAAM,UAAU,QAAQ,cAAc,KAAK;AAC3C,YAAU,SAAS,EAAE,WAAW,MAAM,CAAC;EAEvC,MAAM,WAAW,KAAK,SAAS,KAAK;AACpC,MAAI,WAAW,SAAS,IAAI,YAAY,SAAS,CAC/C,YAAW,SAAS;AAItB,cADkB,SAAS,SAAS,QAAQ,EACrB,UAAU,MAAM;;CAGzC,MAAM,WAAW,cAAc;AAC/B,UAAS,OAAO,QAAQ;EACtB,MAAM,gBAAgB,KAAK;EAC3B,aAAa,OAAO;EACpB,UAAU;EACX;AACD,eAAc,SAAS;AAEvB,SAAQ,IAAI,oBAAoB,KAAK,KAAK,SAAS,cAAc,QAAQ,GAAG;AAC5E,MAAK,MAAM,KAAK,cACd,SAAQ,IAAI,aAAa,KAAK,GAAG,KAAK,GAAG;;AAI7C,SAAS,YAAY,GAAoB;AACvC,KAAI;AACF,YAAU,EAAE;AACZ,SAAO;SACD;AACN,SAAO;;;AAIX,eAAe,OAAO,OAA+B;CACnD,MAAM,WAAW,cAAc;CAC/B,IAAI,UAAU;AAEd,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,SAAS,MAAM,EAAE;EAC1D,MAAM,SAAS,KAAK,WAAW,KAAK;AACpC,MAAI,CAAC,WAAW,OAAO,CACrB;EAGF,MAAM,aAAa,eAAe,KAAK;EACvC,MAAM,gBAAgB,KAAK,YAAY,SAAS,GAAG,KAAK,MAAM;EAC9D,MAAM,gBAAgB,WAAW,cAAc,GAC3C,UAAU,aAAa,cAAc,CAAC,GACtC;EAEJ,MAAM,gBAAgB,eAAe,MAAM;EAC3C,MAAM,kBACJ,kBAAkB,QAAQ,kBAAkB,MAAM;AAEpD,MAAI,CAAC,iBAAiB,CAAC,gBACrB;AAGF,YAAU;AAEV,MAAI,mBAAmB,CAAC,OAAO;AAC7B,WAAQ,KACN,WAAW,KAAK,oCAAoC,cAAc,aAAa,MAAM,KAAK,8BAC3F;AACD;;AAGF,MAAI,eAAe;GACjB,MAAM,SAAS,WAAW,cAAc,GACpC,aAAa,eAAe,OAAO,GACnC;GACJ,MAAM,SAAS,aAAa,KAAK,QAAQ,WAAW,EAAE,OAAO;AAC7D,WAAQ,IAAI,aAAa,KAAK,IAAI,MAAM,KAAK,KAAK,WAAW,GAAG;AAChE,WAAQ,IAAI,UAAU,QAAQ,OAAO,CAAC;AAGtC,OAAI,EAFO,SAAU,MAAM,QAAQ,gBAAgB,KAAK,IAAI,EAG1D;AAGF,iBAAc,eAAe,OAAO;AACpC,YAAS,MAAM,QAAQ;IAAE,MAAM;IAAY,aAAa,OAAO;IAAE;;;AAIrE,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,SAAS,OAAO,EAAE;EAC3D,MAAM,SAAS,KAAK,YAAY,KAAK;AACrC,MAAI,CAAC,WAAW,OAAO,CACrB;EAGF,MAAM,aAAa,gBAAgB,KAAK;AACxC,MAAI,eAAe,MAAM,KACvB;AAGF,YAAU;AACV,UAAQ,IAAI,cAAc,KAAK,IAAI,MAAM,KAAK,KAAK,WAAW,GAAG;AAEjE,MAAI,EADO,SAAU,MAAM,QAAQ,iBAAiB,KAAK,IAAI,EAE3D;AAIF,SAAO,QADS,KAAK,aAAa,UAAU,KAAK,EACzB;GAAE,WAAW;GAAM,OAAO;GAAM,CAAC;AACzD,WAAS,OAAO,QAAQ;GACtB,MAAM;GACN,aAAa,OAAO;GACpB,UAAU,MAAM;GACjB;;AAGH,KAAI,QACF,eAAc,SAAS;;AAI3B,SAAS,KAAK,MAA8B;CAC1C,MAAM,MAAM,SAAS,SAAS,YAAY;AAC1C,KAAI,CAAC,WAAW,IAAI,EAAE;AACpB,UAAQ,IAAI,OAAO,KAAK,cAAc;AACtC;;CAEF,MAAM,UAAU,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC,CACtD,QAAQ,MAAM,EAAE,aAAa,IAAK,SAAS,WAAW,EAAE,gBAAgB,CAAE,CAC1E,KAAK,MAAM,EAAE,KAAK;AAErB,KAAI,QAAQ,WAAW,GAAG;AACxB,UAAQ,IAAI,OAAO,KAAK,cAAc;AACtC;;AAGF,MAAK,MAAM,QAAQ,SAAS;EAC1B,MAAM,OAAO,SAAS,SAAS,eAAe,KAAK,GAAG,gBAAgB,KAAK;AAC3E,UAAQ,IAAI,GAAG,KAAK,IAAI,OAAO;;;AAMnC,SAAS,QAAe;AACtB,SAAQ,MACN;;;;;sBAMD;AACD,SAAQ,KAAK,EAAE;;AAGjB,eAAe,OAAsB;CACnC,MAAM,EAAE,QAAQ,gBAAgB,UAAU;EACxC,SAAS;GACP,OAAO;IACL,SAAS;IACT,MAAM;IACP;GACD,OAAO;IACL,UAAU;IACV,MAAM;IACP;GACF;EACD,kBAAkB;EACnB,CAAC;CAEF,MAAM,EAAE,OAAO,UAAU;CACzB,MAAM,CAAC,SAAS,UAAU,QAAQ;AAElC,KAAI,YAAY,SAAS,aAAa,QAAQ;AAC5C,MAAI,CAAC,KACH,QAAO;AAGT,UAAQ,KAAK;AACb;;AAGF,KAAI,YAAY,SAAS,aAAa,SAAS;AAC7C,MAAI,CAAC,KACH,QAAO;AAGT,WAAS,MAAM,QAAQ,QAAQ,EAAE,CAAC;AAClC;;AAGF,KAAI,YAAY,UAAU;AACxB,QAAM,OAAO,MAAM;AACnB;;AAGF,KAAI,YAAY,WAAW,aAAa,UAAU,aAAa,UAAU;AACvE,OAAK,SAAS;AACd;;AAGF,QAAO;;AAGT,MAAM,CAAC,OAAO,QAAQ;AACpB,SAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC;AAC/D,SAAQ,KAAK,EAAE;EACf"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * toolkit — personal CLI for managing Claude Code hooks, skills, and commands.\n *\n * Commands:\n * toolkit add hook <name>\n * toolkit add skill <name> [--link <target>...]\n * toolkit add command <name>\n * toolkit update [--force]\n * toolkit list hook\n * toolkit list skill\n * toolkit list command\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n cpSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n statSync,\n symlinkSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { createInterface } from \"node:readline/promises\";\nimport { basename, dirname, join, relative, resolve, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\nconst TOOLKIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst HOOKS_SRC = join(TOOLKIT_ROOT, \"hooks\");\nconst SKILLS_SRC = join(TOOLKIT_ROOT, \"skills\");\nconst COMMANDS_SRC = join(TOOLKIT_ROOT, \"commands\");\n\nconst PROJECT_ROOT = process.cwd();\nconst CLAUDE_DIR = join(PROJECT_ROOT, \".claude\");\nconst TOOLKIT_DIR = join(PROJECT_ROOT, \".claude-toolkit\");\nconst MANIFEST_PATH = join(CLAUDE_DIR, \"toolkit-manifest.json\");\n\ntype HookEntry = { hash: string; installedAt: string };\ntype SkillEntry = { hash: string; installedAt: string; linkedTo: string[] };\ntype CommandEntry = { hash: string; installedAt: string };\ntype Manifest = {\n commands: Record<string, CommandEntry>;\n hooks: Record<string, HookEntry>;\n skills: Record<string, SkillEntry>;\n};\n\n// ---------- helpers ----------\n\nfunction today(): string {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction shortHash(content: string | Buffer): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 7);\n}\n\nfunction readManifest(): Manifest {\n if (!existsSync(MANIFEST_PATH)) {\n return { commands: {}, hooks: {}, skills: {} };\n }\n\n try {\n const parsed = JSON.parse(\n readFileSync(MANIFEST_PATH, \"utf8\"),\n ) as Partial<Manifest>;\n return {\n commands: parsed.commands ?? {},\n hooks: parsed.hooks ?? {},\n skills: parsed.skills ?? {},\n };\n } catch {\n return { commands: {}, hooks: {}, skills: {} };\n }\n}\n\nfunction writeManifest(m: Manifest): void {\n mkdirSync(CLAUDE_DIR, { recursive: true });\n writeFileSync(MANIFEST_PATH, JSON.stringify(m, null, 2) + \"\\n\");\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction deepMerge<T>(target: T, source: T): T {\n if (Array.isArray(target) && Array.isArray(source)) {\n return [...target, ...source] as T;\n }\n if (isPlainObject(target) && isPlainObject(source)) {\n const out: Record<string, unknown> = { ...target };\n for (const [k, v] of Object.entries(source)) {\n out[k] = k in out ? deepMerge(out[k], v) : v;\n }\n return out as T;\n }\n return source;\n}\n\nfunction hashCommandSource(name: string): string {\n const p = join(COMMANDS_SRC, `${name}.md`);\n return shortHash(readFileSync(p));\n}\n\nfunction hashHookSource(name: string): string {\n const p = join(HOOKS_SRC, name, \"hook.mjs\");\n return shortHash(readFileSync(p));\n}\n\nfunction hashSkillSource(name: string): string {\n const dir = join(SKILLS_SRC, name);\n const files = collectFiles(dir).sort();\n const h = createHash(\"sha256\");\n for (const f of files) {\n h.update(relative(dir, f));\n h.update(\"\\0\");\n h.update(readFileSync(f));\n h.update(\"\\0\");\n }\n return h.digest(\"hex\").slice(0, 7);\n}\n\nfunction collectFiles(dir: string): string[] {\n const out: string[] = [];\n if (!existsSync(dir)) {\n return out;\n }\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === \".gitkeep\") {\n continue;\n }\n\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n out.push(...collectFiles(full));\n } else if (entry.isFile()) {\n out.push(full);\n }\n }\n return out;\n}\n\nasync function confirm(question: string): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();\n rl.close();\n return answer === \"y\" || answer === \"yes\";\n}\n\nfunction diffLines(oldStr: string, newStr: string): string {\n const a = oldStr.split(\"\\n\");\n const b = newStr.split(\"\\n\");\n const out: string[] = [];\n const max = Math.max(a.length, b.length);\n for (let i = 0; i < max; i++) {\n if (a[i] === b[i]) {\n continue;\n }\n\n if (a[i] !== undefined) {\n out.push(`- ${a[i]}`);\n }\n\n if (b[i] !== undefined) {\n out.push(`+ ${b[i]}`);\n }\n }\n return out.join(\"\\n\");\n}\n\n// ---------- commands ----------\n\nfunction sanitizeName(name: string, kind: string): string {\n name = basename(name);\n if (!name) {\n console.error(`Invalid ${kind} name`);\n process.exit(1);\n }\n return name;\n}\n\nfunction addCommand(name: string): void {\n name = sanitizeName(name, \"command\");\n\n const src = join(COMMANDS_SRC, `${name}.md`);\n if (!existsSync(src)) {\n console.error(`Command not found: ${name}`);\n process.exit(1);\n }\n\n const commandsDir = join(CLAUDE_DIR, \"commands\");\n mkdirSync(commandsDir, { recursive: true });\n const dest = resolve(commandsDir, `${name}.md`);\n if (!dest.startsWith(commandsDir + sep)) {\n console.error(\"Invalid command name\");\n process.exit(1);\n }\n writeFileSync(dest, readFileSync(src));\n\n const manifest = readManifest();\n manifest.commands[name] = {\n hash: hashCommandSource(name),\n installedAt: today(),\n };\n writeManifest(manifest);\n\n console.log(`Installed command: ${name} → ${relative(PROJECT_ROOT, dest)}`);\n}\n\nfunction addHook(name: string): void {\n name = sanitizeName(name, \"hook\");\n\n const srcDir = join(HOOKS_SRC, name);\n if (!existsSync(srcDir)) {\n console.error(`Hook not found: ${name}`);\n process.exit(1);\n }\n\n const hookSrc = join(srcDir, \"hook.mjs\");\n const fragmentPath = join(srcDir, \"settings-fragment.json\");\n\n const hooksDir = join(CLAUDE_DIR, \"hooks\");\n mkdirSync(hooksDir, { recursive: true });\n const destHook = resolve(hooksDir, `${name}.mjs`);\n if (!destHook.startsWith(hooksDir + sep)) {\n console.error(\"Invalid hook name\");\n process.exit(1);\n }\n writeFileSync(destHook, readFileSync(hookSrc));\n\n if (existsSync(fragmentPath)) {\n const fragment = JSON.parse(readFileSync(fragmentPath, \"utf8\"));\n const settingsPath = join(CLAUDE_DIR, \"settings.json\");\n const current = existsSync(settingsPath)\n ? JSON.parse(readFileSync(settingsPath, \"utf8\"))\n : {};\n const merged = deepMerge(current, fragment);\n writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + \"\\n\");\n }\n\n const manifest = readManifest();\n manifest.hooks[name] = { hash: hashHookSource(name), installedAt: today() };\n writeManifest(manifest);\n\n console.log(`Installed hook: ${name} → ${relative(PROJECT_ROOT, destHook)}`);\n}\n\nfunction addSkill(name: string, links: string[]): void {\n name = sanitizeName(name, \"skill\");\n\n const srcDir = join(SKILLS_SRC, name);\n if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {\n console.error(`Skill not found: ${name}`);\n process.exit(1);\n }\n\n const destDir = resolve(TOOLKIT_DIR, \"skills\", name);\n if (!destDir.startsWith(join(TOOLKIT_DIR, \"skills\") + sep)) {\n console.error(\"Invalid skill name\");\n process.exit(1);\n }\n mkdirSync(dirname(destDir), { recursive: true });\n cpSync(srcDir, destDir, { recursive: true });\n\n const resolvedLinks = links.length > 0 ? links : [join(\".claude\", \"skills\")];\n for (const link of resolvedLinks) {\n const linkDir = resolve(PROJECT_ROOT, link);\n mkdirSync(linkDir, { recursive: true });\n\n const linkPath = join(linkDir, name);\n if (existsSync(linkPath) || lstatExists(linkPath)) {\n unlinkSync(linkPath);\n }\n\n const relTarget = relative(linkDir, destDir);\n symlinkSync(relTarget, linkPath, \"dir\");\n }\n\n const manifest = readManifest();\n manifest.skills[name] = {\n hash: hashSkillSource(name),\n installedAt: today(),\n linkedTo: resolvedLinks,\n };\n writeManifest(manifest);\n\n console.log(`Installed skill: ${name} → ${relative(PROJECT_ROOT, destDir)}`);\n for (const l of resolvedLinks) {\n console.log(` linked: ${join(l, name)}`);\n }\n}\n\nfunction lstatExists(p: string): boolean {\n try {\n lstatSync(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function update(force: boolean): Promise<void> {\n const manifest = readManifest();\n let changed = false;\n\n for (const [name, entry] of Object.entries(manifest.hooks)) {\n const srcDir = join(HOOKS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashHookSource(name);\n const installedPath = join(CLAUDE_DIR, \"hooks\", `${name}.mjs`);\n const installedHash = existsSync(installedPath)\n ? shortHash(readFileSync(installedPath))\n : null;\n\n const sourceChanged = sourceHash !== entry.hash;\n const locallyModified =\n installedHash !== null && installedHash !== entry.hash;\n\n if (!sourceChanged && !locallyModified) {\n continue;\n }\n\n changed = true;\n\n if (locallyModified && !force) {\n console.warn(\n `! hook \"${name}\" was modified locally (installed=${installedHash}, manifest=${entry.hash}). Use --force to overwrite.`,\n );\n continue;\n }\n\n if (sourceChanged) {\n const oldSrc = existsSync(installedPath)\n ? readFileSync(installedPath, \"utf8\")\n : \"\";\n const newSrc = readFileSync(join(srcDir, \"hook.mjs\"), \"utf8\");\n console.log(`\\n~ hook: ${name} (${entry.hash} → ${sourceHash})`);\n console.log(diffLines(oldSrc, newSrc));\n const ok = force || (await confirm(`Update hook \"${name}\"?`));\n\n if (!ok) {\n continue;\n }\n\n writeFileSync(installedPath, newSrc);\n manifest.hooks[name] = { hash: sourceHash, installedAt: today() };\n }\n }\n\n for (const [name, entry] of Object.entries(manifest.skills)) {\n const srcDir = join(SKILLS_SRC, name);\n if (!existsSync(srcDir)) {\n continue;\n }\n\n const sourceHash = hashSkillSource(name);\n if (sourceHash === entry.hash) {\n continue;\n }\n\n changed = true;\n console.log(`\\n~ skill: ${name} (${entry.hash} → ${sourceHash})`);\n const ok = force || (await confirm(`Update skill \"${name}\"?`));\n if (!ok) {\n continue;\n }\n\n const destDir = join(TOOLKIT_DIR, \"skills\", name);\n cpSync(srcDir, destDir, { recursive: true, force: true });\n manifest.skills[name] = {\n hash: sourceHash,\n installedAt: today(),\n linkedTo: entry.linkedTo,\n };\n }\n\n for (const [name, entry] of Object.entries(manifest.commands)) {\n const src = join(COMMANDS_SRC, `${name}.md`);\n if (!existsSync(src)) {\n continue;\n }\n\n const sourceHash = hashCommandSource(name);\n if (sourceHash === entry.hash) {\n continue;\n }\n\n changed = true;\n console.log(`\\n~ command: ${name} (${entry.hash} → ${sourceHash})`);\n const ok = force || (await confirm(`Update command \"${name}\"?`));\n if (!ok) {\n continue;\n }\n\n const dest = join(CLAUDE_DIR, \"commands\", `${name}.md`);\n writeFileSync(dest, readFileSync(src));\n manifest.commands[name] = { hash: sourceHash, installedAt: today() };\n }\n\n if (changed) {\n writeManifest(manifest);\n }\n}\n\nfunction list(kind: \"hook\" | \"skill\" | \"command\"): void {\n if (kind === \"command\") {\n if (!existsSync(COMMANDS_SRC)) {\n console.log(\"(no commands available)\");\n return;\n }\n const files = readdirSync(COMMANDS_SRC)\n .filter((f) => f.endsWith(\".md\"))\n .map((f) => f.replace(/\\.md$/, \"\"));\n if (files.length === 0) {\n console.log(\"(no commands available)\");\n return;\n }\n for (const name of files) {\n console.log(`${name} ${hashCommandSource(name)}`);\n }\n return;\n }\n\n const dir = kind === \"hook\" ? HOOKS_SRC : SKILLS_SRC;\n if (!existsSync(dir)) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n const entries = readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory() || (kind === \"skill\" && e.isSymbolicLink()))\n .map((e) => e.name);\n\n if (entries.length === 0) {\n console.log(`(no ${kind}s available)`);\n return;\n }\n\n for (const name of entries) {\n const hash = kind === \"hook\" ? hashHookSource(name) : hashSkillSource(name);\n console.log(`${name} ${hash}`);\n }\n}\n\n// ---------- argv ----------\n\nfunction usage(): never {\n console.error(\n `Usage:\n toolkit add hook <name>\n toolkit add skill <name> [--link <target>]...\n toolkit add command <name>\n toolkit update [--force]\n toolkit list hook\n toolkit list skill\n toolkit list command`,\n );\n process.exit(1);\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n options: {\n force: {\n default: false,\n type: \"boolean\",\n },\n links: {\n multiple: true,\n type: \"string\",\n },\n },\n allowPositionals: true,\n });\n\n const { force, links } = values;\n const [command, resource, name] = positionals;\n\n if (command === \"add\" && resource === \"hook\") {\n if (!name) {\n usage();\n }\n\n addHook(name);\n return;\n }\n\n if (command === \"add\" && resource === \"skill\") {\n if (!name) {\n usage();\n }\n\n addSkill(name, links ? links : []);\n return;\n }\n\n if (command === \"add\" && resource === \"command\") {\n if (!name) {\n usage();\n }\n\n addCommand(name);\n return;\n }\n\n if (command === \"update\") {\n await update(force);\n return;\n }\n\n if (\n command === \"list\" &&\n (resource === \"hook\" || resource === \"skill\" || resource === \"command\")\n ) {\n list(resource as \"hook\" | \"skill\" | \"command\");\n return;\n }\n\n usage();\n}\n\nmain().catch((err) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schalkneethling/toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI for managing Claude Code hooks and skills across projects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
7
7
|
"toolkit": "./dist/index.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
+
"commands",
|
|
10
11
|
"dist",
|
|
11
12
|
"hooks",
|
|
12
13
|
"!**/*.ts",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: css-coder
|
|
3
|
+
description: CSS authoring guidance emphasizing web standards, accessibility, and performance. Use when writing, reviewing, or refactoring CSS. Provides patterns, snippets, and conventions that prioritize native CSS over frameworks, semantic structure, and maintainable code. Refer to references/patterns.md for specific patterns and snippets.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CSS Coder
|
|
7
|
+
|
|
8
|
+
Guidance for writing CSS that prioritizes web standards, accessibility, performance, and maintainability.
|
|
9
|
+
|
|
10
|
+
## Core Principles
|
|
11
|
+
|
|
12
|
+
1. **Web standards first** — Use native CSS features before reaching for libraries or frameworks. No Tailwind, no CSS-in-JS unless explicitly requested.
|
|
13
|
+
2. **Accessibility as a requirement** — Ensure styles support, never hinder, assistive technologies. Respect user preferences (motion, color scheme, contrast).
|
|
14
|
+
3. **Performance matters** — Minimize repaints, avoid layout thrashing, use efficient selectors.
|
|
15
|
+
4. **Readable over clever** — Future maintainers (including the author) should understand the code at a glance.
|
|
16
|
+
5. **Explicit over implicit** — Avoid magic numbers and unexplained values. Use custom properties for shared values.
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
1. **Check references first** — Before writing CSS, consult `references/patterns.md` for established patterns and snippets.
|
|
21
|
+
2. **Validate against specs** — When uncertain, reference MDN Web Docs or CSS specifications.
|
|
22
|
+
3. **Suggest alternatives** — Offer ideas beyond the skill's patterns when appropriate, but always aligned with the core principles above.
|
|
23
|
+
|
|
24
|
+
## Writing Guidelines
|
|
25
|
+
|
|
26
|
+
### Selectors
|
|
27
|
+
|
|
28
|
+
- Prefer class selectors over element or ID selectors for styling.
|
|
29
|
+
- Keep specificity low and predictable.
|
|
30
|
+
- Avoid deep nesting (aim for 2-3 levels maximum).
|
|
31
|
+
|
|
32
|
+
### Custom Properties
|
|
33
|
+
|
|
34
|
+
- Use `--` prefixed custom properties for colors, spacing, typography, and other repeated values.
|
|
35
|
+
- Define at `:root` or appropriate scope.
|
|
36
|
+
- Name descriptively: `--color-primary`, `--spacing-md`, `--font-size-body`.
|
|
37
|
+
|
|
38
|
+
### Logical Properties
|
|
39
|
+
|
|
40
|
+
- Always use logical properties (`margin-inline`, `padding-block`, `inset-inline-start`, `block-size`, etc.) instead of physical properties (`margin-left`, `padding-top`, `left`, `height`, etc.).
|
|
41
|
+
- Logical properties support internationalization and different writing modes automatically.
|
|
42
|
+
- Only fall back to physical properties where logical equivalents do not yet exist.
|
|
43
|
+
|
|
44
|
+
### Units
|
|
45
|
+
|
|
46
|
+
- Use `rem` for typography and spacing (respects user font-size preferences).
|
|
47
|
+
- Use `em` for component-relative sizing when appropriate.
|
|
48
|
+
- Use viewport units (`vw`, `vh`, `dvh`) thoughtfully, with fallbacks where needed.
|
|
49
|
+
- Avoid `px` for font sizes; acceptable for borders, shadows, and fine details.
|
|
50
|
+
|
|
51
|
+
### Colors
|
|
52
|
+
|
|
53
|
+
- Use modern space-separated syntax for all color functions (`rgb()`, `hsl()`, `oklch()`).
|
|
54
|
+
- Recommend `oklch()` for vibrant or wide-gamut colors.
|
|
55
|
+
- Use relative color syntax to derive hover states or transparent variants from existing variables.
|
|
56
|
+
- See `references/patterns.md` for syntax examples.
|
|
57
|
+
|
|
58
|
+
### Layout
|
|
59
|
+
|
|
60
|
+
- Use CSS Grid for two-dimensional layouts, or when vertical flow is needed without extra declarations.
|
|
61
|
+
- Use Flexbox for one-dimensional alignment, noting it defaults to `row` direction.
|
|
62
|
+
- Avoid floats for layout (legacy use only).
|
|
63
|
+
|
|
64
|
+
### Media Queries
|
|
65
|
+
|
|
66
|
+
- Use modern range syntax: `@media (width < 48rem)`, `@media (width >= 48rem)`.
|
|
67
|
+
- Prefer **Shared First** over mobile-first: define shared styles outside queries, scope viewport-specific styles with bounded queries.
|
|
68
|
+
- Keep breakpoints to a minimum — add more only when there's a clear need.
|
|
69
|
+
- See `references/patterns.md` for detailed examples.
|
|
70
|
+
|
|
71
|
+
### Accessibility
|
|
72
|
+
|
|
73
|
+
- Never use `display: none` or `visibility: hidden` to hide content that should remain accessible to screen readers. Use appropriate techniques from the references.
|
|
74
|
+
- Respect `prefers-reduced-motion`, `prefers-color-scheme`, and `prefers-contrast`.
|
|
75
|
+
- Ensure sufficient color contrast (WCAG AA minimum, AAA preferred).
|
|
76
|
+
- Maintain visible focus indicators — never remove `:focus` styles without replacement.
|
|
77
|
+
|
|
78
|
+
### Performance
|
|
79
|
+
|
|
80
|
+
- Avoid expensive properties in animations (prefer `transform` and `opacity`).
|
|
81
|
+
- Use `will-change` sparingly and only when needed.
|
|
82
|
+
- Minimize use of `*` selectors.
|
|
83
|
+
- Prefer `@layer` for managing cascade when working with larger codebases.
|
|
84
|
+
|
|
85
|
+
## References
|
|
86
|
+
|
|
87
|
+
Consult `references/patterns.md` for:
|
|
88
|
+
|
|
89
|
+
- Visually-hidden utility
|
|
90
|
+
- User preference queries (motion, color scheme, contrast)
|
|
91
|
+
- Modern color syntax and relative colors
|
|
92
|
+
- Shared First responsive patterns
|
|
93
|
+
- Any project-specific conventions
|
|
94
|
+
|
|
95
|
+
This file will grow as patterns are added. If a needed pattern doesn't exist, suggest one aligned with the core principles.
|