pasika 0.1.6 → 0.3.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.
Files changed (91) hide show
  1. package/README.md +99 -78
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.js +132 -0
  4. package/dist/enforcement/coverage.d.ts +51 -0
  5. package/dist/enforcement/coverage.js +210 -0
  6. package/dist/enforcement/docs-check.d.ts +17 -0
  7. package/dist/enforcement/docs-check.js +159 -0
  8. package/dist/enforcement/normalize.d.ts +11 -0
  9. package/dist/enforcement/normalize.js +21 -0
  10. package/dist/enforcement/parse-docs.d.ts +58 -0
  11. package/dist/enforcement/parse-docs.js +94 -0
  12. package/dist/enforcement/types.d.ts +57 -0
  13. package/dist/enforcement/types.js +59 -0
  14. package/dist/eslint/pasika/index.d.ts +13 -1
  15. package/dist/eslint/pasika/index.js +24 -11
  16. package/dist/eslint/pasika/project/ccf.d.ts +48 -0
  17. package/dist/eslint/pasika/project/ccf.js +119 -0
  18. package/dist/eslint/pasika/project/index.d.ts +21 -0
  19. package/dist/eslint/pasika/project/index.js +139 -0
  20. package/dist/eslint/pasika/project/parse-module.d.ts +27 -0
  21. package/dist/eslint/pasika/project/parse-module.js +128 -0
  22. package/dist/eslint/pasika/rules/component-placement.d.ts +11 -0
  23. package/dist/eslint/pasika/rules/component-placement.js +75 -0
  24. package/dist/eslint/pasika/rules/enforce-barrel-exports.d.ts +9 -0
  25. package/dist/eslint/pasika/rules/enforce-barrel-exports.js +78 -0
  26. package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +9 -0
  27. package/dist/eslint/pasika/rules/enforce-cn-merge.js +84 -0
  28. package/dist/eslint/pasika/rules/enforce-cva-variant-props.d.ts +9 -0
  29. package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +81 -0
  30. package/dist/eslint/pasika/rules/filename-case.js +10 -0
  31. package/dist/eslint/pasika/rules/import-boundaries.d.ts +2 -0
  32. package/dist/eslint/pasika/rules/{organization-imports.js → import-boundaries.js} +62 -19
  33. package/dist/eslint/pasika/rules/no-arbitrary-tailwind.d.ts +9 -0
  34. package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +97 -0
  35. package/dist/eslint/pasika/rules/no-mixed-concerns.d.ts +9 -0
  36. package/dist/eslint/pasika/rules/no-mixed-concerns.js +84 -0
  37. package/dist/eslint/pasika/rules/support-file-placement.d.ts +15 -0
  38. package/dist/eslint/pasika/rules/support-file-placement.js +70 -0
  39. package/enforcement/registry.json +1139 -0
  40. package/package.json +21 -15
  41. package/claude/hooks/.vulyk +0 -3
  42. package/claude/hooks/AGENTS.md +0 -3
  43. package/claude/hooks/CLAUDE.md +0 -1
  44. package/claude/hooks/claude-hooks.md +0 -30
  45. package/claude/hooks/notification.sh +0 -38
  46. package/claude/hooks/protect-files.sh +0 -21
  47. package/claude/hooks/status-line/index.js +0 -57
  48. package/claude/scripts/render-settings.ts +0 -223
  49. package/claude/settings.base.json +0 -38
  50. package/dist/claude/scripts/render-settings.js +0 -145
  51. package/dist/eslint/pasika/rules/organization-imports.d.ts +0 -2
  52. package/dist/eslint.config.js +0 -7
  53. package/dist/scripts/pasika.js +0 -59
  54. package/docs/agent-conventions.md +0 -27
  55. package/docs/claude/hooks.md +0 -30
  56. package/docs/code-organization-guide/code-organization-guide.md +0 -69
  57. package/docs/code-organization-guide/references/application-architecture-reference.md +0 -149
  58. package/docs/code-organization-guide/rules/component-placement-rule.md +0 -119
  59. package/docs/code-organization-guide/rules/configuration-rule.md +0 -42
  60. package/docs/code-organization-guide/rules/constants-rule.md +0 -74
  61. package/docs/code-organization-guide/rules/exports-and-imports-rule.md +0 -84
  62. package/docs/code-organization-guide/rules/folder-nesting-rule.md +0 -82
  63. package/docs/code-organization-guide/rules/hook-extraction-rule.md +0 -141
  64. package/docs/code-organization-guide/rules/interactive-component-rule.md +0 -97
  65. package/docs/code-organization-guide/rules/jsx-hygiene-rule.md +0 -67
  66. package/docs/code-organization-guide/rules/locales-rule.md +0 -53
  67. package/docs/code-organization-guide/rules/nameable-visual-concept-rule.md +0 -66
  68. package/docs/code-organization-guide/rules/no-mixed-concerns-rule.md +0 -63
  69. package/docs/code-organization-guide/rules/repeated-structure-rule.md +0 -93
  70. package/docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md +0 -112
  71. package/docs/code-organization-guide/rules/sole-state-owner-rule.md +0 -101
  72. package/docs/code-organization-guide/rules/types-and-schemas-rule.md +0 -139
  73. package/docs/code-organization-guide/rules/utilities-rule.md +0 -86
  74. package/docs/documentation-guide/_templates/grouped-reference.md +0 -11
  75. package/docs/documentation-guide/_templates/guide.md +0 -19
  76. package/docs/documentation-guide/_templates/rule.md +0 -21
  77. package/docs/documentation-guide/_templates/single-lookup-reference.md +0 -5
  78. package/docs/documentation-guide/documentation-guide.md +0 -13
  79. package/docs/documentation-guide/references/documentation-types-reference.md +0 -9
  80. package/docs/documentation-guide/rules/guide-creation-rule.md +0 -113
  81. package/docs/documentation-guide/rules/reference-creation-rule.md +0 -132
  82. package/docs/documentation-guide/rules/rule-creation-rule.md +0 -81
  83. package/docs/documentation-guide/rules/template-usage-rule.md +0 -49
  84. package/docs/shadcn-theme.md +0 -121
  85. package/docs/styling-guide/rules/arbitrary-value-rule.md +0 -31
  86. package/docs/styling-guide/rules/class-composition-rule.md +0 -52
  87. package/docs/styling-guide/rules/component-ui-state-rule.md +0 -53
  88. package/docs/styling-guide/rules/component-variant-rule.md +0 -125
  89. package/docs/styling-guide/rules/global-stylesheet-rule.md +0 -67
  90. package/docs/styling-guide/rules/theme-and-utility-definition-rule.md +0 -86
  91. package/docs/styling-guide/styling-guide.md +0 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pasika",
3
- "version": "0.1.6",
3
+ "version": "0.3.0",
4
4
  "description": "Reusable agent setup package",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,35 +14,41 @@
14
14
  "import": "./dist/eslint/pasika/index.js"
15
15
  }
16
16
  },
17
- "bin": {
18
- "pasika": "dist/scripts/pasika.js"
19
- },
20
17
  "files": [
21
- "claude",
22
- "docs",
23
18
  "dist",
24
- "AGENTS.md",
25
- "CLAUDE.md",
26
- "README.md"
19
+ "enforcement/registry.json"
27
20
  ],
28
21
  "scripts": {
29
- "build": "rm -rf dist && tsc -p tsconfig.json && tsc -p tsconfig.eslint.json",
30
- "check": "npm run lint && npm run typecheck",
31
- "docs:generate": "rm -f AGENTS.md CLAUDE.md && npx vulyk docs",
22
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
32
23
  "fix": "eslint . --fix",
33
24
  "lint": "eslint .",
34
25
  "prepack": "npm run build",
35
- "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.eslint.json --noEmit"
26
+ "typecheck": "tsc --noEmit",
27
+ "coverage": "tsx cli/index.ts coverage",
28
+ "docs": "tsx cli/index.ts docs",
29
+ "test": "node --import tsx --test 'eslint/**/*.test.ts' 'enforcement/**/*.test.ts'"
36
30
  },
37
31
  "devDependencies": {
32
+ "@types/estree": "^1.0.9",
38
33
  "@types/node": "^24.12.2",
34
+ "@typescript-eslint/parser": "^8.68.0",
39
35
  "eslint": "^9.39.4",
40
36
  "prettier": "^3.8.1",
37
+ "tsx": "^4.23.12",
41
38
  "typescript": "^5.9.2",
42
- "vulyk": "github:Bredansky/vulyk#829cac4a244add961304070474db3e978db01d2a",
43
- "zirka": "^0.0.28"
39
+ "zirka": "^0.0.38"
44
40
  },
45
41
  "engines": {
46
42
  "node": ">=22"
43
+ },
44
+ "dependencies": {
45
+ "commander": "^12.1.0",
46
+ "zod": "^4.4.3"
47
+ },
48
+ "bin": {
49
+ "pasika": "dist/cli/index.js"
50
+ },
51
+ "peerDependencies": {
52
+ "typescript": ">=5.0.0"
47
53
  }
48
54
  }
@@ -1,3 +0,0 @@
1
- 🍯 AGENTS.md
2
- 🍯 CLAUDE.md
3
- 🍯 claude-hooks.md
@@ -1,3 +0,0 @@
1
- # Claude Hooks
2
- Shared Claude hooks shipped by pasika.
3
- Full documentation: docs/claude/hooks.md
@@ -1 +0,0 @@
1
- @AGENTS.md
@@ -1,30 +0,0 @@
1
- # Claude Hooks
2
-
3
- `pasika` ships the reusable shared Claude hooks.
4
-
5
- ## `status-line/index.js`
6
-
7
- Displays a two-line status view with:
8
-
9
- - model name
10
- - current folder
11
- - git branch when available
12
- - used context percentage as a 10-segment bar
13
- - session cost
14
- - session duration
15
-
16
- ## `notification.sh`
17
-
18
- Sends a desktop notification when Claude needs attention.
19
-
20
- - macOS: `terminal-notifier` with click-to-focus VS Code behavior, falls back to `osascript`
21
- - Linux: `notify-send`
22
- - Windows: PowerShell message box
23
-
24
- ## `protect-files.sh`
25
-
26
- Blocks edits to:
27
-
28
- - `.env` files
29
- - `package-lock.json`
30
- - files inside `.git/`
@@ -1,38 +0,0 @@
1
- #!/bin/bash
2
- # notification.sh - Cross-platform notification
3
-
4
- OS="$(uname -s)"
5
-
6
- case "$OS" in
7
- Darwin*)
8
- if command -v terminal-notifier &> /dev/null; then
9
- PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
10
- TEMP_SCRIPT=$(mktemp)
11
-
12
- cat > "$TEMP_SCRIPT" <<SCRIPTEND
13
- #!/bin/bash
14
- /usr/local/bin/code -r "$PROJECT_DIR"
15
- sleep 0.5
16
- osascript -e 'tell application "Visual Studio Code" to activate'
17
- SCRIPTEND
18
-
19
- chmod +x "$TEMP_SCRIPT"
20
-
21
- terminal-notifier \
22
- -message "Claude Code needs your attention - Click to focus terminal" \
23
- -title "Claude Code" \
24
- -sound Ping \
25
- -execute "$TEMP_SCRIPT"
26
-
27
- (sleep 60; rm -f "$TEMP_SCRIPT") &
28
- else
29
- osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'
30
- fi
31
- ;;
32
- Linux*)
33
- notify-send 'Claude Code' 'Claude Code needs your attention'
34
- ;;
35
- MINGW*|MSYS*|CYGWIN*)
36
- powershell.exe -Command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')"
37
- ;;
38
- esac
@@ -1,21 +0,0 @@
1
- #!/bin/bash
2
- # protect-files.sh - Block edits to sensitive files
3
-
4
- INPUT=$(cat)
5
- FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
6
-
7
- PROTECTED_PATTERNS=(
8
- "/\.env$|^\.env$"
9
- "package-lock\.json$"
10
- "/\.git/|^\.git/"
11
- )
12
-
13
- for pattern in "${PROTECTED_PATTERNS[@]}"; do
14
- if echo "$FILE_PATH" | grep -qE "$pattern"; then
15
- echo "Blocked: Cannot edit protected file '$FILE_PATH'" >&2
16
- echo "Reason: This is a sensitive/generated file that should not be manually edited" >&2
17
- exit 2
18
- fi
19
- done
20
-
21
- exit 0
@@ -1,57 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { execSync } from "node:child_process";
4
- import path from "node:path";
5
-
6
- let input = "";
7
-
8
- process.stdin.on("data", (chunk) => {
9
- input += chunk;
10
- });
11
-
12
- process.stdin.on("end", () => {
13
- try {
14
- const data = JSON.parse(input || "{}");
15
- const model = data.model?.display_name || data.model?.name || "Claude";
16
- const dir = path.basename(data.workspace?.current_dir || process.cwd());
17
- const cost = Number(data.cost?.total_cost_usd || 0);
18
- const pct = Math.floor(Number(data.context_window?.used_percentage || 0));
19
- const durationMs = Number(data.cost?.total_duration_ms || 0);
20
-
21
- const CYAN = "\x1b[36m";
22
- const GREEN = "\x1b[32m";
23
- const YELLOW = "\x1b[33m";
24
- const RED = "\x1b[31m";
25
- const RESET = "\x1b[0m";
26
-
27
- let barColor = GREEN;
28
- if (pct >= 90) {
29
- barColor = RED;
30
- } else if (pct >= 70) {
31
- barColor = YELLOW;
32
- }
33
-
34
- const filled = Math.max(0, Math.min(10, Math.floor(pct / 10)));
35
- const bar = "█".repeat(filled) + "░".repeat(10 - filled);
36
- const mins = Math.floor(durationMs / 60000);
37
- const secs = Math.floor((durationMs % 60000) / 1000);
38
-
39
- let branch = "";
40
- try {
41
- branch = execSync("git branch --show-current", {
42
- encoding: "utf8",
43
- stdio: ["pipe", "pipe", "ignore"],
44
- }).trim();
45
- branch = branch ? ` | 🌿 ${branch}` : "";
46
- } catch {
47
- branch = "";
48
- }
49
-
50
- process.stdout.write(`${CYAN}[${model}]${RESET} 📁 ${dir}${branch}\n`);
51
- process.stdout.write(
52
- `${barColor}${bar}${RESET} ${pct}% | ${YELLOW}$${cost.toFixed(2)}${RESET} | ⏱️ ${mins}m ${secs}s\n`,
53
- );
54
- } catch {
55
- process.stdout.write("Claude\n");
56
- }
57
- });
@@ -1,223 +0,0 @@
1
- import { access, copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { constants } from "node:fs";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
-
9
- interface ClaudeSettings {
10
- $schema?: string;
11
- attribution?: Record<string, unknown>;
12
- permissions?: {
13
- ask?: string[];
14
- deny?: string[];
15
- };
16
- hooks: {
17
- Notification?: HookMatcherEntry[];
18
- PreToolUse?: HookMatcherEntry[];
19
- [key: string]: unknown;
20
- };
21
- statusLine: {
22
- type?: string;
23
- command: string;
24
- };
25
- [key: string]: unknown;
26
- }
27
-
28
- interface CommandHook {
29
- type?: string;
30
- command: string;
31
- [key: string]: unknown;
32
- }
33
-
34
- interface HookMatcherEntry {
35
- matcher?: string;
36
- hooks: CommandHook[];
37
- [key: string]: unknown;
38
- }
39
-
40
- const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
41
-
42
- const fileExists = async (filePath: string): Promise<boolean> => {
43
- try {
44
- await access(filePath, constants.F_OK);
45
- return true;
46
- } catch {
47
- return false;
48
- }
49
- };
50
-
51
- const isClaudeSettings = (value: unknown): value is ClaudeSettings => {
52
- if (!isRecord(value)) {
53
- return false;
54
- }
55
-
56
- const hooks = value.hooks;
57
- const statusLine = value.statusLine;
58
-
59
- if (!isRecord(hooks) || !isRecord(statusLine) || typeof statusLine.command !== "string") {
60
- return false;
61
- }
62
-
63
- const notification = hooks.Notification;
64
- if (notification !== undefined && !Array.isArray(notification)) {
65
- return false;
66
- }
67
-
68
- const preToolUse = hooks.PreToolUse;
69
- return preToolUse === undefined || Array.isArray(preToolUse);
70
- };
71
-
72
- const readClaudeSettings = async (templatePath: string): Promise<ClaudeSettings> => {
73
- const templateRaw = await readFile(templatePath, "utf8");
74
- const parsed: unknown = JSON.parse(templateRaw);
75
-
76
- if (!isClaudeSettings(parsed)) {
77
- throw new Error("Claude settings template has an invalid shape.");
78
- }
79
-
80
- return parsed;
81
- };
82
-
83
- const mergeStringArrays = (left: string[] = [], right: string[] = []): string[] =>
84
- Array.from(new Set([...left, ...right]));
85
-
86
- const upsertHookMatcherEntry = (
87
- entries: HookMatcherEntry[] | undefined,
88
- matcher: string,
89
- command: string,
90
- ): HookMatcherEntry[] => {
91
- const nextEntries = [...(entries ?? [])];
92
- const existingIndex = nextEntries.findIndex((entry) => (entry.matcher ?? "") === matcher);
93
- const hook: CommandHook = { type: "command", command };
94
-
95
- if (existingIndex === -1) {
96
- nextEntries.push({ matcher, hooks: [hook] });
97
- return nextEntries;
98
- }
99
-
100
- const existingEntry = nextEntries[existingIndex];
101
- nextEntries[existingIndex] = { ...existingEntry, hooks: [hook] };
102
- return nextEntries;
103
- };
104
-
105
- const mergeClaudeSettings = (
106
- existingSettings: ClaudeSettings,
107
- baseSettings: ClaudeSettings,
108
- packagePath: string,
109
- ): ClaudeSettings => {
110
- const notificationCommand = `${packagePath}/hooks/notification.sh`;
111
- const protectFilesCommand = `${packagePath}/hooks/protect-files.sh`;
112
- const statusLineCommand = `node ${packagePath}/hooks/status-line/index.js`;
113
-
114
- return {
115
- ...existingSettings,
116
- $schema: baseSettings.$schema ?? existingSettings.$schema,
117
- attribution: { ...(baseSettings.attribution ?? {}), ...(existingSettings.attribution ?? {}) },
118
- permissions: {
119
- ...(existingSettings.permissions ?? {}),
120
- ...(baseSettings.permissions ?? {}),
121
- ask: mergeStringArrays(baseSettings.permissions?.ask, existingSettings.permissions?.ask),
122
- deny: mergeStringArrays(baseSettings.permissions?.deny, existingSettings.permissions?.deny),
123
- },
124
- hooks: {
125
- ...existingSettings.hooks,
126
- Notification: upsertHookMatcherEntry(existingSettings.hooks.Notification, "", notificationCommand),
127
- PreToolUse: upsertHookMatcherEntry(existingSettings.hooks.PreToolUse, "Edit|Write", protectFilesCommand),
128
- },
129
- statusLine: {
130
- ...existingSettings.statusLine,
131
- ...baseSettings.statusLine,
132
- type: "command",
133
- command: statusLineCommand,
134
- },
135
- };
136
- };
137
-
138
- export const renderClaude = async (targetDir: string, force: boolean, packageRoot: string): Promise<string[]> => {
139
- const templatePath = path.join(packageRoot, "claude/settings.base.json");
140
- const baseSettings = await readClaudeSettings(templatePath);
141
- const packagePath = "./node_modules/pasika/claude";
142
- const notificationEntries = upsertHookMatcherEntry(
143
- baseSettings.hooks.Notification,
144
- "",
145
- `${packagePath}/hooks/notification.sh`,
146
- );
147
- const preToolUseEntries = upsertHookMatcherEntry(
148
- baseSettings.hooks.PreToolUse,
149
- "Edit|Write",
150
- `${packagePath}/hooks/protect-files.sh`,
151
- );
152
- const settings: ClaudeSettings = {
153
- ...baseSettings,
154
- hooks: {
155
- ...baseSettings.hooks,
156
- Notification: notificationEntries,
157
- PreToolUse: preToolUseEntries,
158
- },
159
- statusLine: {
160
- ...baseSettings.statusLine,
161
- type: "command",
162
- command: `node ${packagePath}/hooks/status-line/index.js`,
163
- },
164
- };
165
-
166
- const outputDir = path.join(targetDir, ".claude");
167
- const outputPath = path.join(outputDir, "settings.json");
168
- const hooksOutputDir = path.join(outputDir, "hooks");
169
-
170
- await mkdir(outputDir, { recursive: true });
171
- await mkdir(hooksOutputDir, { recursive: true });
172
-
173
- const finalSettings =
174
- !force && (await fileExists(outputPath))
175
- ? mergeClaudeSettings(await readClaudeSettings(outputPath), settings, packagePath)
176
- : settings;
177
-
178
- await writeFile(outputPath, `${JSON.stringify(finalSettings, null, 2)}\n`, "utf8");
179
-
180
- const outputs = [outputPath];
181
- const hookDocFiles = ["AGENTS.md", "CLAUDE.md"] as const;
182
-
183
- for (const fileName of hookDocFiles) {
184
- const sourcePath = path.join(packageRoot, "claude/hooks", fileName);
185
- if (!(await fileExists(sourcePath))) {
186
- continue;
187
- }
188
-
189
- const destinationPath = path.join(hooksOutputDir, fileName);
190
- await copyFile(sourcePath, destinationPath);
191
- outputs.push(destinationPath);
192
- }
193
-
194
- return outputs;
195
- };
196
-
197
- const isDirectRun = process.argv[1] ? path.resolve(process.argv[1]) === __filename : false;
198
-
199
- if (isDirectRun) {
200
- const args = process.argv.slice(2);
201
- const getArgValue = (flag: string): string | undefined => {
202
- const index = args.indexOf(flag);
203
- if (index === -1) {
204
- return undefined;
205
- }
206
-
207
- return args[index + 1];
208
- };
209
-
210
- const targetDir = path.resolve(process.cwd(), getArgValue("--target-dir") ?? ".");
211
- const force = args.includes("--force");
212
- const packageRoot = path.resolve(__dirname, "../..");
213
-
214
- try {
215
- const outputs = await renderClaude(targetDir, force, packageRoot);
216
- for (const output of outputs) {
217
- process.stdout.write(`Wrote ${output}\n`);
218
- }
219
- } catch (error) {
220
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
221
- process.exit(1);
222
- }
223
- }
@@ -1,38 +0,0 @@
1
- {
2
- "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
- "attribution": {
4
- "commit": "",
5
- "pr": ""
6
- },
7
- "permissions": {
8
- "ask": ["Bash(git commit:*)", "Bash(git push:*)"]
9
- },
10
- "hooks": {
11
- "Notification": [
12
- {
13
- "matcher": "",
14
- "hooks": [
15
- {
16
- "type": "command",
17
- "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notification.sh"
18
- }
19
- ]
20
- }
21
- ],
22
- "PreToolUse": [
23
- {
24
- "matcher": "Edit|Write",
25
- "hooks": [
26
- {
27
- "type": "command",
28
- "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/protect-files.sh"
29
- }
30
- ]
31
- }
32
- ]
33
- },
34
- "statusLine": {
35
- "type": "command",
36
- "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/status-line/index.js"
37
- }
38
- }
@@ -1,145 +0,0 @@
1
- import { access, copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { constants } from "node:fs";
4
- import { fileURLToPath } from "node:url";
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = path.dirname(__filename);
7
- const isRecord = (value) => typeof value === "object" && value !== null;
8
- const fileExists = async (filePath) => {
9
- try {
10
- await access(filePath, constants.F_OK);
11
- return true;
12
- }
13
- catch {
14
- return false;
15
- }
16
- };
17
- const isClaudeSettings = (value) => {
18
- if (!isRecord(value)) {
19
- return false;
20
- }
21
- const hooks = value.hooks;
22
- const statusLine = value.statusLine;
23
- if (!isRecord(hooks) || !isRecord(statusLine) || typeof statusLine.command !== "string") {
24
- return false;
25
- }
26
- const notification = hooks.Notification;
27
- if (notification !== undefined && !Array.isArray(notification)) {
28
- return false;
29
- }
30
- const preToolUse = hooks.PreToolUse;
31
- return preToolUse === undefined || Array.isArray(preToolUse);
32
- };
33
- const readClaudeSettings = async (templatePath) => {
34
- const templateRaw = await readFile(templatePath, "utf8");
35
- const parsed = JSON.parse(templateRaw);
36
- if (!isClaudeSettings(parsed)) {
37
- throw new Error("Claude settings template has an invalid shape.");
38
- }
39
- return parsed;
40
- };
41
- const mergeStringArrays = (left = [], right = []) => Array.from(new Set([...left, ...right]));
42
- const upsertHookMatcherEntry = (entries, matcher, command) => {
43
- const nextEntries = [...(entries ?? [])];
44
- const existingIndex = nextEntries.findIndex((entry) => (entry.matcher ?? "") === matcher);
45
- const hook = { type: "command", command };
46
- if (existingIndex === -1) {
47
- nextEntries.push({ matcher, hooks: [hook] });
48
- return nextEntries;
49
- }
50
- const existingEntry = nextEntries[existingIndex];
51
- nextEntries[existingIndex] = { ...existingEntry, hooks: [hook] };
52
- return nextEntries;
53
- };
54
- const mergeClaudeSettings = (existingSettings, baseSettings, packagePath) => {
55
- const notificationCommand = `${packagePath}/hooks/notification.sh`;
56
- const protectFilesCommand = `${packagePath}/hooks/protect-files.sh`;
57
- const statusLineCommand = `node ${packagePath}/hooks/status-line/index.js`;
58
- return {
59
- ...existingSettings,
60
- $schema: baseSettings.$schema ?? existingSettings.$schema,
61
- attribution: { ...(baseSettings.attribution ?? {}), ...(existingSettings.attribution ?? {}) },
62
- permissions: {
63
- ...(existingSettings.permissions ?? {}),
64
- ...(baseSettings.permissions ?? {}),
65
- ask: mergeStringArrays(baseSettings.permissions?.ask, existingSettings.permissions?.ask),
66
- deny: mergeStringArrays(baseSettings.permissions?.deny, existingSettings.permissions?.deny),
67
- },
68
- hooks: {
69
- ...existingSettings.hooks,
70
- Notification: upsertHookMatcherEntry(existingSettings.hooks.Notification, "", notificationCommand),
71
- PreToolUse: upsertHookMatcherEntry(existingSettings.hooks.PreToolUse, "Edit|Write", protectFilesCommand),
72
- },
73
- statusLine: {
74
- ...existingSettings.statusLine,
75
- ...baseSettings.statusLine,
76
- type: "command",
77
- command: statusLineCommand,
78
- },
79
- };
80
- };
81
- export const renderClaude = async (targetDir, force, packageRoot) => {
82
- const templatePath = path.join(packageRoot, "claude/settings.base.json");
83
- const baseSettings = await readClaudeSettings(templatePath);
84
- const packagePath = "./node_modules/pasika/claude";
85
- const notificationEntries = upsertHookMatcherEntry(baseSettings.hooks.Notification, "", `${packagePath}/hooks/notification.sh`);
86
- const preToolUseEntries = upsertHookMatcherEntry(baseSettings.hooks.PreToolUse, "Edit|Write", `${packagePath}/hooks/protect-files.sh`);
87
- const settings = {
88
- ...baseSettings,
89
- hooks: {
90
- ...baseSettings.hooks,
91
- Notification: notificationEntries,
92
- PreToolUse: preToolUseEntries,
93
- },
94
- statusLine: {
95
- ...baseSettings.statusLine,
96
- type: "command",
97
- command: `node ${packagePath}/hooks/status-line/index.js`,
98
- },
99
- };
100
- const outputDir = path.join(targetDir, ".claude");
101
- const outputPath = path.join(outputDir, "settings.json");
102
- const hooksOutputDir = path.join(outputDir, "hooks");
103
- await mkdir(outputDir, { recursive: true });
104
- await mkdir(hooksOutputDir, { recursive: true });
105
- const finalSettings = !force && (await fileExists(outputPath))
106
- ? mergeClaudeSettings(await readClaudeSettings(outputPath), settings, packagePath)
107
- : settings;
108
- await writeFile(outputPath, `${JSON.stringify(finalSettings, null, 2)}\n`, "utf8");
109
- const outputs = [outputPath];
110
- const hookDocFiles = ["AGENTS.md", "CLAUDE.md"];
111
- for (const fileName of hookDocFiles) {
112
- const sourcePath = path.join(packageRoot, "claude/hooks", fileName);
113
- if (!(await fileExists(sourcePath))) {
114
- continue;
115
- }
116
- const destinationPath = path.join(hooksOutputDir, fileName);
117
- await copyFile(sourcePath, destinationPath);
118
- outputs.push(destinationPath);
119
- }
120
- return outputs;
121
- };
122
- const isDirectRun = process.argv[1] ? path.resolve(process.argv[1]) === __filename : false;
123
- if (isDirectRun) {
124
- const args = process.argv.slice(2);
125
- const getArgValue = (flag) => {
126
- const index = args.indexOf(flag);
127
- if (index === -1) {
128
- return undefined;
129
- }
130
- return args[index + 1];
131
- };
132
- const targetDir = path.resolve(process.cwd(), getArgValue("--target-dir") ?? ".");
133
- const force = args.includes("--force");
134
- const packageRoot = path.resolve(__dirname, "../..");
135
- try {
136
- const outputs = await renderClaude(targetDir, force, packageRoot);
137
- for (const output of outputs) {
138
- process.stdout.write(`Wrote ${output}\n`);
139
- }
140
- }
141
- catch (error) {
142
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
143
- process.exit(1);
144
- }
145
- }
@@ -1,2 +0,0 @@
1
- import type { Rule } from "eslint";
2
- export declare const organizationImportsRule: Rule.RuleModule;
@@ -1,7 +0,0 @@
1
- import { RuleSeverity, styleguide } from "zirka";
2
- const { eslintConfig } = styleguide({
3
- node: RuleSeverity.Error,
4
- typescript: RuleSeverity.Error,
5
- ignores: ["dist/**", "node_modules/**", "prettier.config.mjs"],
6
- });
7
- export default eslintConfig;