pasika 0.1.6 → 0.2.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 +40 -55
- package/dist/eslint/pasika/index.d.ts +18 -0
- package/dist/eslint/pasika/index.js +18 -3
- package/dist/eslint/pasika/rules/enforce-barrel-exports.d.ts +9 -0
- package/dist/eslint/pasika/rules/enforce-barrel-exports.js +78 -0
- package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +9 -0
- package/dist/eslint/pasika/rules/enforce-cn-merge.js +84 -0
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.d.ts +9 -0
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +76 -0
- package/dist/eslint/pasika/rules/filename-case.js +10 -0
- package/dist/eslint/pasika/rules/import-boundaries.d.ts +2 -0
- package/dist/eslint/pasika/rules/{organization-imports.js → import-boundaries.js} +11 -1
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.d.ts +9 -0
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +107 -0
- package/dist/eslint/pasika/rules/no-mixed-concerns.d.ts +9 -0
- package/dist/eslint/pasika/rules/no-mixed-concerns.js +84 -0
- package/package.json +5 -16
- package/claude/hooks/.vulyk +0 -3
- package/claude/hooks/AGENTS.md +0 -3
- package/claude/hooks/CLAUDE.md +0 -1
- package/claude/hooks/claude-hooks.md +0 -30
- package/claude/hooks/notification.sh +0 -38
- package/claude/hooks/protect-files.sh +0 -21
- package/claude/hooks/status-line/index.js +0 -57
- package/claude/scripts/render-settings.ts +0 -223
- package/claude/settings.base.json +0 -38
- package/dist/claude/scripts/render-settings.js +0 -145
- package/dist/eslint/pasika/rules/organization-imports.d.ts +0 -2
- package/dist/eslint.config.js +0 -7
- package/dist/scripts/pasika.js +0 -59
- package/docs/agent-conventions.md +0 -27
- package/docs/claude/hooks.md +0 -30
- package/docs/code-organization-guide/code-organization-guide.md +0 -69
- package/docs/code-organization-guide/references/application-architecture-reference.md +0 -149
- package/docs/code-organization-guide/rules/component-placement-rule.md +0 -119
- package/docs/code-organization-guide/rules/configuration-rule.md +0 -42
- package/docs/code-organization-guide/rules/constants-rule.md +0 -74
- package/docs/code-organization-guide/rules/exports-and-imports-rule.md +0 -84
- package/docs/code-organization-guide/rules/folder-nesting-rule.md +0 -82
- package/docs/code-organization-guide/rules/hook-extraction-rule.md +0 -141
- package/docs/code-organization-guide/rules/interactive-component-rule.md +0 -97
- package/docs/code-organization-guide/rules/jsx-hygiene-rule.md +0 -67
- package/docs/code-organization-guide/rules/locales-rule.md +0 -53
- package/docs/code-organization-guide/rules/nameable-visual-concept-rule.md +0 -66
- package/docs/code-organization-guide/rules/no-mixed-concerns-rule.md +0 -63
- package/docs/code-organization-guide/rules/repeated-structure-rule.md +0 -93
- package/docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md +0 -112
- package/docs/code-organization-guide/rules/sole-state-owner-rule.md +0 -101
- package/docs/code-organization-guide/rules/types-and-schemas-rule.md +0 -139
- package/docs/code-organization-guide/rules/utilities-rule.md +0 -86
- package/docs/documentation-guide/_templates/grouped-reference.md +0 -11
- package/docs/documentation-guide/_templates/guide.md +0 -19
- package/docs/documentation-guide/_templates/rule.md +0 -21
- package/docs/documentation-guide/_templates/single-lookup-reference.md +0 -5
- package/docs/documentation-guide/documentation-guide.md +0 -13
- package/docs/documentation-guide/references/documentation-types-reference.md +0 -9
- package/docs/documentation-guide/rules/guide-creation-rule.md +0 -113
- package/docs/documentation-guide/rules/reference-creation-rule.md +0 -132
- package/docs/documentation-guide/rules/rule-creation-rule.md +0 -81
- package/docs/documentation-guide/rules/template-usage-rule.md +0 -49
- package/docs/shadcn-theme.md +0 -121
- package/docs/styling-guide/rules/arbitrary-value-rule.md +0 -31
- package/docs/styling-guide/rules/class-composition-rule.md +0 -52
- package/docs/styling-guide/rules/component-ui-state-rule.md +0 -53
- package/docs/styling-guide/rules/component-variant-rule.md +0 -125
- package/docs/styling-guide/rules/global-stylesheet-rule.md +0 -67
- package/docs/styling-guide/rules/theme-and-utility-definition-rule.md +0 -86
- package/docs/styling-guide/styling-guide.md +0 -14
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: pasika/no-mixed-concerns
|
|
3
|
+
*
|
|
4
|
+
* Enforces the "No Mixed Concerns Rule" — one React component per .tsx file.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/code-organization-guide/rules/no-mixed-concerns-rule.md
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Return the name of an export node so we can report it clearly.
|
|
10
|
+
* Handles:
|
|
11
|
+
* export function Foo() { }
|
|
12
|
+
* export const Foo = () => { }
|
|
13
|
+
* export const Foo = function () { }
|
|
14
|
+
* export default function Foo() { }
|
|
15
|
+
* export default () => { }
|
|
16
|
+
*/
|
|
17
|
+
// biome-ignore lint/suspicious/noExplicitAny: AST node type from ESLint with dynamic property access
|
|
18
|
+
function getExportName(declaration) {
|
|
19
|
+
if (declaration.type === "FunctionDeclaration" && declaration.id) {
|
|
20
|
+
return declaration.id.name;
|
|
21
|
+
}
|
|
22
|
+
if (declaration.type === "VariableDeclaration") {
|
|
23
|
+
const declarator = declaration.declarations[0];
|
|
24
|
+
if (declarator?.id.type === "Identifier" &&
|
|
25
|
+
(declarator.init?.type === "ArrowFunctionExpression" || declarator.init?.type === "FunctionExpression")) {
|
|
26
|
+
return declarator.id.name;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (declaration.type === "ArrowFunctionExpression") {
|
|
30
|
+
return "default";
|
|
31
|
+
}
|
|
32
|
+
if (declaration.type === "FunctionExpression" && declaration.id) {
|
|
33
|
+
return declaration.id.name;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export const noMixedConcernsRule = {
|
|
38
|
+
meta: {
|
|
39
|
+
schema: [],
|
|
40
|
+
type: "problem",
|
|
41
|
+
docs: {
|
|
42
|
+
description: "Enforce one exported React component per .tsx file.",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
create(context) {
|
|
46
|
+
if (!context.filename.endsWith(".tsx"))
|
|
47
|
+
return {};
|
|
48
|
+
let exportedComponentCount = 0;
|
|
49
|
+
const extraExports = [];
|
|
50
|
+
function registerExport(name) {
|
|
51
|
+
exportedComponentCount++;
|
|
52
|
+
if (exportedComponentCount > 1) {
|
|
53
|
+
extraExports.push({ name });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
ExportNamedDeclaration(node) {
|
|
58
|
+
if (!node.declaration)
|
|
59
|
+
return;
|
|
60
|
+
const name = getExportName(node.declaration);
|
|
61
|
+
if (name)
|
|
62
|
+
registerExport(name);
|
|
63
|
+
},
|
|
64
|
+
ExportDefaultDeclaration(node) {
|
|
65
|
+
const name = getExportName(node.declaration);
|
|
66
|
+
if (name)
|
|
67
|
+
registerExport(name);
|
|
68
|
+
},
|
|
69
|
+
"Program:exit"() {
|
|
70
|
+
if (exportedComponentCount > 1) {
|
|
71
|
+
for (const extra of extraExports) {
|
|
72
|
+
context.report({
|
|
73
|
+
loc: { line: 1, column: 0 },
|
|
74
|
+
message: `File exports multiple components. "${extra.name}" is an extra component export. ` +
|
|
75
|
+
"Move it to its own file. Each .tsx file MUST contain exactly one component. " +
|
|
76
|
+
"See docs/code-organization-guide/rules/no-mixed-concerns-rule.md",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any -- re-enable after AST node access block */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pasika",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Reusable agent setup package",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,33 +14,22 @@
|
|
|
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
|
-
"
|
|
22
|
-
"docs",
|
|
23
|
-
"dist",
|
|
24
|
-
"AGENTS.md",
|
|
25
|
-
"CLAUDE.md",
|
|
26
|
-
"README.md"
|
|
18
|
+
"dist/eslint"
|
|
27
19
|
],
|
|
28
20
|
"scripts": {
|
|
29
|
-
"build": "rm -rf dist && tsc -p tsconfig.
|
|
30
|
-
"check": "npm run lint && npm run typecheck",
|
|
31
|
-
"docs:generate": "rm -f AGENTS.md CLAUDE.md && npx vulyk docs",
|
|
21
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
32
22
|
"fix": "eslint . --fix",
|
|
33
23
|
"lint": "eslint .",
|
|
34
24
|
"prepack": "npm run build",
|
|
35
|
-
"typecheck": "tsc
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
36
26
|
},
|
|
37
27
|
"devDependencies": {
|
|
38
28
|
"@types/node": "^24.12.2",
|
|
39
29
|
"eslint": "^9.39.4",
|
|
40
30
|
"prettier": "^3.8.1",
|
|
41
31
|
"typescript": "^5.9.2",
|
|
42
|
-
"
|
|
43
|
-
"zirka": "^0.0.28"
|
|
32
|
+
"zirka": "^0.0.35"
|
|
44
33
|
},
|
|
45
34
|
"engines": {
|
|
46
35
|
"node": ">=22"
|
package/claude/hooks/.vulyk
DELETED
package/claude/hooks/AGENTS.md
DELETED
package/claude/hooks/CLAUDE.md
DELETED
|
@@ -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
|
-
}
|