opscale-setup 1.0.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/.github/workflows/publish.yml +33 -0
- package/README.md +111 -0
- package/bin/index.js +8 -0
- package/package.json +46 -0
- package/src/index.js +21 -0
- package/src/installer.js +75 -0
- package/src/logger.js +20 -0
- package/src/templates/CLAUDE.md.js +82 -0
- package/src/templates/hooks/auto-commit-msg.js +39 -0
- package/src/templates/hooks/block-dangerous.js +40 -0
- package/src/templates/hooks/branch-guard.js +48 -0
- package/src/templates/hooks/build-validator.js +49 -0
- package/src/templates/hooks/cost-tracker.js +63 -0
- package/src/templates/hooks/dep-audit.js +66 -0
- package/src/templates/hooks/deploy-check.js +65 -0
- package/src/templates/hooks/lint-check.js +51 -0
- package/src/templates/hooks/secret-guard.js +46 -0
- package/src/templates/hooks/test-runner.js +67 -0
- package/src/templates/settings.js +128 -0
- package/src/templates/skills/code-review.md +61 -0
- package/src/templates/skills/debugging.md +61 -0
- package/src/templates/skills/deployment.md +89 -0
- package/src/templates/skills/pr-description.md +61 -0
- package/src/templates/skills/test-generation.md +56 -0
- package/src/wizard.js +62 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write # required for npm provenance
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: '20'
|
|
21
|
+
registry-url: 'https://registry.npmjs.org'
|
|
22
|
+
cache: 'npm'
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: npm ci
|
|
26
|
+
|
|
27
|
+
- name: Run tests
|
|
28
|
+
run: npm test --if-present
|
|
29
|
+
|
|
30
|
+
- name: Publish with provenance
|
|
31
|
+
run: npm publish --provenance --access public
|
|
32
|
+
env:
|
|
33
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# opscale-setup
|
|
2
|
+
|
|
3
|
+
> Deploy a production-ready Claude Code environment in one command.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx opscale-setup
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
No global install required. Answers 3 questions, then writes everything to your
|
|
10
|
+
project in under 10 seconds.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What it installs
|
|
15
|
+
|
|
16
|
+
| File | Description |
|
|
17
|
+
|------|-------------|
|
|
18
|
+
| `CLAUDE.md` | Best-practice prompting guide, tailored to your stack |
|
|
19
|
+
| `.claude/settings.json` | Optimized Claude Code config with hooks wired up |
|
|
20
|
+
| `.claude/hooks/` (10 hooks) | Production-grade guardrails and automation |
|
|
21
|
+
| `.claude/skills/` (5 skills) | High-value slash commands for daily dev work |
|
|
22
|
+
|
|
23
|
+
### Hooks
|
|
24
|
+
|
|
25
|
+
| Hook | Trigger | What it does |
|
|
26
|
+
|------|---------|--------------|
|
|
27
|
+
| `block-dangerous.js` | PreToolUse Bash | Blocks `rm -rf /`, `DROP DATABASE`, fork bombs, `curl \| bash` |
|
|
28
|
+
| `secret-guard.js` | PreToolUse Bash + Write | Detects AWS keys, GitHub tokens, Anthropic keys, private key blocks |
|
|
29
|
+
| `branch-guard.js` | PreToolUse Bash | Warns on direct push to main/master; blocks force-push |
|
|
30
|
+
| `dep-audit.js` | PreToolUse Bash | Intercepts npm/pip/cargo installs, flags typosquats + critical CVEs |
|
|
31
|
+
| `deploy-check.js` | PreToolUse Bash | Enforces clean working tree + passing tests before any deploy |
|
|
32
|
+
| `auto-commit-msg.js` | PostToolUse Bash | Validates Conventional Commits format after every commit |
|
|
33
|
+
| `cost-tracker.js` | Stop | Logs session token usage + estimated USD cost to `.claude/cost-log.json` |
|
|
34
|
+
| `test-runner.js` | PostToolUse Write | Auto-discovers and runs related tests when source files are written |
|
|
35
|
+
| `lint-check.js` | PostToolUse Write | Runs ESLint/Biome/Ruff/gofmt on written files |
|
|
36
|
+
| `build-validator.js` | PostToolUse Write | Runs `tsc --noEmit` / `cargo check` / `go build` on written files |
|
|
37
|
+
|
|
38
|
+
### Skills
|
|
39
|
+
|
|
40
|
+
| Skill | Command | What it does |
|
|
41
|
+
|-------|---------|--------------|
|
|
42
|
+
| code-review | `/code-review` | Security + correctness review of the current diff |
|
|
43
|
+
| test-generation | `/test-generation <file>` | Generates a full, runnable test suite |
|
|
44
|
+
| pr-description | `/pr-description` | Writes PR title + body from git log + diff |
|
|
45
|
+
| debugging | `/debugging "<symptom>"` | Systematic 5-step debug protocol |
|
|
46
|
+
| deployment | `/deployment` | Pre-deploy checklist + rollback procedures |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Setup wizard
|
|
51
|
+
|
|
52
|
+
The wizard asks exactly 3 questions:
|
|
53
|
+
|
|
54
|
+
1. **Project type** — Node.js, Python, Go, Rust, Java, Ruby, Full-stack, Generic
|
|
55
|
+
2. **Team size** — Solo, Small (2–10), Medium (11–50), Large (50+)
|
|
56
|
+
3. **Deploy target** — AWS, GCP, Azure, Vercel, Kubernetes, Docker, VPS, Generic
|
|
57
|
+
|
|
58
|
+
Answers are embedded in `CLAUDE.md` and `settings.json` to tailor the generated
|
|
59
|
+
files to your environment.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Node.js 18+
|
|
66
|
+
- Claude Code CLI (`npm install -g @anthropic-ai/claude-code`)
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Publishing your own fork
|
|
71
|
+
|
|
72
|
+
1. Fork this repo and update `package.json` with your npm username.
|
|
73
|
+
2. Add `NPM_TOKEN` to your GitHub repository secrets.
|
|
74
|
+
3. Tag a release:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git tag v1.0.0
|
|
78
|
+
git push origin v1.0.0
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
GitHub Actions will run tests and publish to npm automatically.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Customizing hooks
|
|
86
|
+
|
|
87
|
+
Hooks are plain Node.js scripts in `.claude/hooks/`. They receive tool input as
|
|
88
|
+
JSON on stdin and exit non-zero to abort the tool call.
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
// .claude/hooks/my-hook.js
|
|
92
|
+
import { readFileSync } from 'fs';
|
|
93
|
+
|
|
94
|
+
const input = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
|
|
95
|
+
const command = input?.tool_input?.command ?? '';
|
|
96
|
+
|
|
97
|
+
if (command.includes('something-forbidden')) {
|
|
98
|
+
console.error('[my-hook] blocked');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
process.exit(0);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Wire it up in `.claude/settings.json` under `hooks.PreToolUse` or `hooks.PostToolUse`.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
package/bin/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opscale-setup",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Deploy a production-ready Claude Code environment in one command",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"opscale-setup": "bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/index.js",
|
|
12
|
+
"test": "node --test src/__tests__/*.test.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"claude",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"ai",
|
|
18
|
+
"developer-tools",
|
|
19
|
+
"cli",
|
|
20
|
+
"setup",
|
|
21
|
+
"hooks",
|
|
22
|
+
"anthropic"
|
|
23
|
+
],
|
|
24
|
+
"author": "opscale-setup contributors",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"fs-extra": "^11.2.0",
|
|
29
|
+
"inquirer": "^9.2.12",
|
|
30
|
+
"ora": "^8.0.1"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/jordan23wagner-ops/opscale-setup.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/YOUR_USERNAME/opscale-setup/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/YOUR_USERNAME/opscale-setup#readme",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { runWizard } from './wizard.js';
|
|
3
|
+
import { install } from './installer.js';
|
|
4
|
+
import { log, banner } from './logger.js';
|
|
5
|
+
|
|
6
|
+
export async function main() {
|
|
7
|
+
banner();
|
|
8
|
+
|
|
9
|
+
const config = await runWizard();
|
|
10
|
+
|
|
11
|
+
log.info('Installing Claude Code environment...');
|
|
12
|
+
await install(config);
|
|
13
|
+
|
|
14
|
+
console.log('\n' + chalk.green.bold(' Setup complete!') + '\n');
|
|
15
|
+
console.log(chalk.dim(' Files created:'));
|
|
16
|
+
console.log(chalk.dim(' CLAUDE.md ') + chalk.white('Best-practice prompting guide'));
|
|
17
|
+
console.log(chalk.dim(' .claude/settings.json ') + chalk.white('Optimized Claude Code config'));
|
|
18
|
+
console.log(chalk.dim(' .claude/hooks/ ') + chalk.white('10 production-grade hooks'));
|
|
19
|
+
console.log(chalk.dim(' .claude/skills/ ') + chalk.white('5 high-value skill definitions'));
|
|
20
|
+
console.log('\n' + chalk.cyan(' Run') + ' claude ' + chalk.cyan('to start your session.\n'));
|
|
21
|
+
}
|
package/src/installer.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { log } from './logger.js';
|
|
6
|
+
import { renderClaudeMd } from './templates/CLAUDE.md.js';
|
|
7
|
+
import { renderSettings } from './templates/settings.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const TEMPLATES_DIR = path.join(__dirname, 'templates');
|
|
11
|
+
const HOOKS_DIR = path.join(TEMPLATES_DIR, 'hooks');
|
|
12
|
+
const SKILLS_DIR = path.join(TEMPLATES_DIR, 'skills');
|
|
13
|
+
|
|
14
|
+
export async function install(config) {
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
17
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
18
|
+
const skillsDir = path.join(claudeDir, 'skills');
|
|
19
|
+
|
|
20
|
+
const spinner = ora({ text: 'Creating directories...', prefixText: ' ' }).start();
|
|
21
|
+
|
|
22
|
+
await fs.ensureDir(hooksDir);
|
|
23
|
+
await fs.ensureDir(skillsDir);
|
|
24
|
+
spinner.succeed('Directories ready');
|
|
25
|
+
|
|
26
|
+
// CLAUDE.md
|
|
27
|
+
spinner.start('Writing CLAUDE.md...');
|
|
28
|
+
await fs.writeFile(path.join(cwd, 'CLAUDE.md'), renderClaudeMd(config));
|
|
29
|
+
spinner.succeed('CLAUDE.md written');
|
|
30
|
+
|
|
31
|
+
// settings.json
|
|
32
|
+
spinner.start('Writing .claude/settings.json...');
|
|
33
|
+
await fs.writeJSON(path.join(claudeDir, 'settings.json'), renderSettings(config), { spaces: 2 });
|
|
34
|
+
spinner.succeed('.claude/settings.json written');
|
|
35
|
+
|
|
36
|
+
// Hooks
|
|
37
|
+
spinner.start('Installing hooks...');
|
|
38
|
+
const hookFiles = await fs.readdir(HOOKS_DIR);
|
|
39
|
+
for (const file of hookFiles) {
|
|
40
|
+
const src = path.join(HOOKS_DIR, file);
|
|
41
|
+
const dest = path.join(hooksDir, file);
|
|
42
|
+
await fs.copy(src, dest, { overwrite: false });
|
|
43
|
+
// Mark shell scripts executable on Unix
|
|
44
|
+
if (file.endsWith('.sh')) {
|
|
45
|
+
await fs.chmod(dest, 0o755).catch(() => {});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
spinner.succeed(`${hookFiles.length} hooks installed`);
|
|
49
|
+
|
|
50
|
+
// Skills
|
|
51
|
+
spinner.start('Installing skills...');
|
|
52
|
+
const skillFiles = await fs.readdir(SKILLS_DIR);
|
|
53
|
+
for (const file of skillFiles) {
|
|
54
|
+
const src = path.join(SKILLS_DIR, file);
|
|
55
|
+
const dest = path.join(skillsDir, file);
|
|
56
|
+
await fs.copy(src, dest, { overwrite: false });
|
|
57
|
+
}
|
|
58
|
+
spinner.succeed(`${skillFiles.length} skills installed`);
|
|
59
|
+
|
|
60
|
+
// .gitignore safety — ensure .claude/hooks is gitignored if desired
|
|
61
|
+
await patchGitignore(cwd);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function patchGitignore(cwd) {
|
|
65
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
66
|
+
const marker = '# opscale-setup';
|
|
67
|
+
const block = `\n${marker}\n.claude/settings.local.json\n`;
|
|
68
|
+
|
|
69
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
70
|
+
const content = await fs.readFile(gitignorePath, 'utf8');
|
|
71
|
+
if (!content.includes(marker)) {
|
|
72
|
+
await fs.appendFile(gitignorePath, block);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export function banner() {
|
|
4
|
+
console.log('\n' + chalk.bold.cyan(' ██████╗ ██████╗ ███████╗ ██████╗ █████╗ ██╗ ███████╗'));
|
|
5
|
+
console.log(chalk.bold.cyan(' ██╔═══██╗██╔══██╗██╔════╝██╔════╝██╔══██╗██║ ██╔════╝'));
|
|
6
|
+
console.log(chalk.bold.cyan(' ██║ ██║██████╔╝███████╗██║ ███████║██║ █████╗ '));
|
|
7
|
+
console.log(chalk.bold.cyan(' ██║ ██║██╔═══╝ ╚════██║██║ ██╔══██║██║ ██╔══╝ '));
|
|
8
|
+
console.log(chalk.bold.cyan(' ╚██████╔╝██║ ███████║╚██████╗██║ ██║███████╗███████╗'));
|
|
9
|
+
console.log(chalk.bold.cyan(' ╚═════╝ ╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚══════╝'));
|
|
10
|
+
console.log('\n' + chalk.bold(' opscale-setup') + chalk.dim(' — Production Claude Code in one command'));
|
|
11
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────────────\n'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const log = {
|
|
15
|
+
info: (msg) => console.log(chalk.blue(' ℹ ') + msg),
|
|
16
|
+
success: (msg) => console.log(chalk.green(' ✓ ') + msg),
|
|
17
|
+
warn: (msg) => console.log(chalk.yellow(' ⚠ ') + msg),
|
|
18
|
+
error: (msg) => console.log(chalk.red(' ✗ ') + msg),
|
|
19
|
+
step: (msg) => console.log(chalk.cyan(' → ') + msg),
|
|
20
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export function renderClaudeMd({ projectType, teamSize, deployTarget }) {
|
|
2
|
+
const projectLabel = {
|
|
3
|
+
node: 'Node.js / TypeScript', python: 'Python', go: 'Go', rust: 'Rust',
|
|
4
|
+
java: 'Java / Kotlin', ruby: 'Ruby on Rails', fullstack: 'Full-stack monorepo',
|
|
5
|
+
generic: 'Generic',
|
|
6
|
+
}[projectType] ?? projectType;
|
|
7
|
+
|
|
8
|
+
return `# CLAUDE.md
|
|
9
|
+
|
|
10
|
+
## Project Context
|
|
11
|
+
|
|
12
|
+
- **Stack**: ${projectLabel}
|
|
13
|
+
- **Team size**: ${teamSize}
|
|
14
|
+
- **Deploy target**: ${deployTarget}
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## How to Work with This Codebase
|
|
19
|
+
|
|
20
|
+
### Before Starting Any Task
|
|
21
|
+
|
|
22
|
+
1. Read existing tests to understand intended behavior before changing code.
|
|
23
|
+
2. Check \`git log --oneline -20\` to understand recent changes and momentum.
|
|
24
|
+
3. Grep for the symbol or pattern you're about to create — don't duplicate.
|
|
25
|
+
4. If the task is ambiguous, ask one clarifying question before writing code.
|
|
26
|
+
|
|
27
|
+
### Coding Standards
|
|
28
|
+
|
|
29
|
+
- Match the style of the surrounding code exactly (indentation, naming, quotes).
|
|
30
|
+
- No commented-out code. Delete dead code; git history preserves it.
|
|
31
|
+
- No TODO comments unless the task explicitly requires deferral.
|
|
32
|
+
- Prefer editing existing files over creating new abstractions.
|
|
33
|
+
- Three similar lines is better than a premature abstraction.
|
|
34
|
+
|
|
35
|
+
### What NOT to Do
|
|
36
|
+
|
|
37
|
+
- Do not add error handling for scenarios that cannot happen in this codebase.
|
|
38
|
+
- Do not write multi-paragraph docstrings for self-explanatory functions.
|
|
39
|
+
- Do not add feature flags or backwards-compat shims unless asked.
|
|
40
|
+
- Do not create planning documents, analysis files, or READMEs unless asked.
|
|
41
|
+
- Do not run \`git add -A\` — stage specific files by name only.
|
|
42
|
+
- Do not amend published commits.
|
|
43
|
+
|
|
44
|
+
### Security Rules
|
|
45
|
+
|
|
46
|
+
- Never log secrets, tokens, or PII.
|
|
47
|
+
- Never commit \`.env\` files or credential files.
|
|
48
|
+
- All user input must be validated at the boundary; trust internal types.
|
|
49
|
+
- SQL queries must use parameterized statements — no string interpolation.
|
|
50
|
+
- No \`eval()\`, \`exec()\`, or dynamic code execution on untrusted input.
|
|
51
|
+
|
|
52
|
+
### Git Workflow
|
|
53
|
+
|
|
54
|
+
- Commit message format: \`<type>(<scope>): <what changed, present tense>\`
|
|
55
|
+
- Types: \`feat\`, \`fix\`, \`refactor\`, \`test\`, \`docs\`, \`chore\`, \`perf\`
|
|
56
|
+
- One logical change per commit. Keep PRs small and focused.
|
|
57
|
+
- Branch naming: \`<type>/<short-slug>\` e.g. \`feat/user-auth\`
|
|
58
|
+
|
|
59
|
+
### Testing
|
|
60
|
+
|
|
61
|
+
- Write tests for every new public function.
|
|
62
|
+
- Tests should verify behavior, not implementation.
|
|
63
|
+
- Do not mock the database in integration tests.
|
|
64
|
+
- A failing test must be fixed before merging — never skip.
|
|
65
|
+
|
|
66
|
+
### Deploy Checklist (${deployTarget})
|
|
67
|
+
|
|
68
|
+
- All tests pass locally before pushing.
|
|
69
|
+
- Environment variables documented in \`.env.example\`.
|
|
70
|
+
- Database migrations are backwards-compatible.
|
|
71
|
+
- Rollback plan documented for schema changes.
|
|
72
|
+
- Secrets rotation procedure known to team.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Asking for Help
|
|
77
|
+
|
|
78
|
+
If you are unsure, say so. A wrong confident answer is worse than an honest
|
|
79
|
+
"I don't know — here's what I'd check." Prefer small, verifiable steps over
|
|
80
|
+
large speculative changes.
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse / Bash — after a git commit, validates the commit message
|
|
4
|
+
* against Conventional Commits format and prints a warning if it doesn't
|
|
5
|
+
* match. Never blocks — this is advisory only.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
|
|
10
|
+
const CONVENTIONAL = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?: .{1,100}$/;
|
|
11
|
+
|
|
12
|
+
let input;
|
|
13
|
+
try {
|
|
14
|
+
input = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
|
|
15
|
+
} catch {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const command = input?.tool_input?.command ?? '';
|
|
20
|
+
if (!/git\s+commit/.test(command)) process.exit(0);
|
|
21
|
+
|
|
22
|
+
// Read the last commit message
|
|
23
|
+
let msg = '';
|
|
24
|
+
try {
|
|
25
|
+
msg = execSync('git log -1 --pretty=%s', { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
26
|
+
} catch {
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!CONVENTIONAL.test(msg)) {
|
|
31
|
+
console.error(
|
|
32
|
+
`[auto-commit-msg] WARNING: commit message doesn't follow Conventional Commits:\n` +
|
|
33
|
+
` "${msg}"\n` +
|
|
34
|
+
` Expected: <type>(<scope>): <description>\n` +
|
|
35
|
+
` Types: feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
process.exit(0);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse / Bash — blocks shell commands that are almost never safe to run
|
|
4
|
+
* autonomously. Reads the tool input JSON from stdin and exits non-zero to abort.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
|
|
8
|
+
const BLOCKED = [
|
|
9
|
+
/rm\s+-rf\s+[/~]/, // rm -rf / or ~/
|
|
10
|
+
/rm\s+-rf\s+\*/, // rm -rf *
|
|
11
|
+
/DROP\s+DATABASE/i,
|
|
12
|
+
/DROP\s+TABLE/i,
|
|
13
|
+
/TRUNCATE\s+TABLE/i,
|
|
14
|
+
/chmod\s+777/,
|
|
15
|
+
/curl[^|]+\|\s*bash/, // curl ... | bash
|
|
16
|
+
/wget[^|]+\|\s*bash/, // wget ... | bash
|
|
17
|
+
/git\s+push\s+--force(?!\s*--no)/, // force push (not --force-with-lease)
|
|
18
|
+
/git\s+reset\s+--hard\s+HEAD~[2-9]/, // hard reset >1 commit
|
|
19
|
+
/:(){:|:&};:/, // fork bomb
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
let input;
|
|
23
|
+
try {
|
|
24
|
+
input = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
|
|
25
|
+
} catch {
|
|
26
|
+
process.exit(0); // no stdin — allow
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const command = input?.tool_input?.command ?? '';
|
|
30
|
+
|
|
31
|
+
for (const pattern of BLOCKED) {
|
|
32
|
+
if (pattern.test(command)) {
|
|
33
|
+
console.error(
|
|
34
|
+
`[block-dangerous] BLOCKED: command matched safety pattern: ${pattern}\n Command: ${command}`
|
|
35
|
+
);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.exit(0);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse / Bash — warns when Claude is about to push or commit directly
|
|
4
|
+
* to protected branches (main, master, production, release/*).
|
|
5
|
+
* Exits non-zero to block; exits 0 to allow.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
|
|
10
|
+
const PROTECTED = /^(main|master|production|release\/.+)$/;
|
|
11
|
+
|
|
12
|
+
let input;
|
|
13
|
+
try {
|
|
14
|
+
input = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
|
|
15
|
+
} catch {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const command = input?.tool_input?.command ?? '';
|
|
20
|
+
|
|
21
|
+
// Only care about git push / git commit on protected branches
|
|
22
|
+
if (!/git\s+(push|commit)/.test(command)) process.exit(0);
|
|
23
|
+
|
|
24
|
+
let branch = '';
|
|
25
|
+
try {
|
|
26
|
+
branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
27
|
+
.toString().trim();
|
|
28
|
+
} catch {
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (PROTECTED.test(branch)) {
|
|
33
|
+
// Force-push to protected branch is always blocked
|
|
34
|
+
if (/--force/.test(command) || /-f\b/.test(command)) {
|
|
35
|
+
console.error(
|
|
36
|
+
`[branch-guard] BLOCKED: force push to protected branch "${branch}" is not allowed.`
|
|
37
|
+
);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Warn but allow regular pushes — print to stderr so Claude sees it
|
|
42
|
+
console.error(
|
|
43
|
+
`[branch-guard] WARNING: you are about to push directly to "${branch}".\n` +
|
|
44
|
+
` Consider using a feature branch and opening a pull request instead.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
process.exit(0);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse / Write — after Claude writes a file, runs a fast type-check or
|
|
4
|
+
* build to catch compile errors immediately. Advisory only — never blocks.
|
|
5
|
+
*
|
|
6
|
+
* Detects: TypeScript (tsc --noEmit), Python (py_compile), Go (go build), Rust (cargo check).
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, existsSync } from 'fs';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
let input;
|
|
13
|
+
try {
|
|
14
|
+
input = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
|
|
15
|
+
} catch {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const filePath = input?.tool_input?.file_path ?? '';
|
|
20
|
+
if (!filePath || !existsSync(filePath)) process.exit(0);
|
|
21
|
+
|
|
22
|
+
const ext = path.extname(filePath);
|
|
23
|
+
|
|
24
|
+
function run(cmd, label) {
|
|
25
|
+
try {
|
|
26
|
+
execSync(cmd, { stdio: 'pipe', timeout: 60_000 });
|
|
27
|
+
console.error(`[build-validator] ${label} passed`);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error(
|
|
30
|
+
`[build-validator] ${label} failed:\n` +
|
|
31
|
+
(err.stdout?.toString() ?? '') +
|
|
32
|
+
(err.stderr?.toString() ?? '')
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (['.ts', '.tsx'].includes(ext)) {
|
|
38
|
+
if (existsSync('tsconfig.json')) {
|
|
39
|
+
run('npx tsc --noEmit --skipLibCheck', 'TypeScript');
|
|
40
|
+
}
|
|
41
|
+
} else if (ext === '.py') {
|
|
42
|
+
run(`python -m py_compile ${filePath}`, 'Python syntax');
|
|
43
|
+
} else if (ext === '.go') {
|
|
44
|
+
run('go build ./...', 'Go build');
|
|
45
|
+
} else if (ext === '.rs') {
|
|
46
|
+
run('cargo check --quiet', 'Rust check');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
process.exit(0);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stop hook — reads session usage from Claude Code's stop event and appends
|
|
4
|
+
* a cost record to .claude/cost-log.json so teams can track AI spend over time.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
// Claude Code pricing (USD per million tokens) as of mid-2025
|
|
10
|
+
// Update these when Anthropic changes pricing.
|
|
11
|
+
const PRICING = {
|
|
12
|
+
'claude-opus-4': { input: 15.00, output: 75.00, cacheRead: 1.50, cacheWrite: 18.75 },
|
|
13
|
+
'claude-sonnet-4': { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },
|
|
14
|
+
'claude-haiku-4': { input: 0.80, output: 4.00, cacheRead: 0.08, cacheWrite: 1.00 },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function cost(usage, model) {
|
|
18
|
+
const rates = PRICING[model] ?? PRICING['claude-sonnet-4'];
|
|
19
|
+
const M = 1_000_000;
|
|
20
|
+
return (
|
|
21
|
+
(usage.input_tokens ?? 0) / M * rates.input +
|
|
22
|
+
(usage.output_tokens ?? 0) / M * rates.output +
|
|
23
|
+
(usage.cache_read_tokens ?? 0) / M * rates.cacheRead +
|
|
24
|
+
(usage.cache_creation_tokens ?? 0) / M * rates.cacheWrite
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let event;
|
|
29
|
+
try {
|
|
30
|
+
event = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
|
|
31
|
+
} catch {
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const usage = event?.usage ?? event?.session_stats ?? {};
|
|
36
|
+
const model = event?.model ?? 'claude-sonnet-4';
|
|
37
|
+
const usd = cost(usage, model);
|
|
38
|
+
|
|
39
|
+
const logPath = path.join(process.cwd(), '.claude', 'cost-log.json');
|
|
40
|
+
let log = [];
|
|
41
|
+
if (existsSync(logPath)) {
|
|
42
|
+
try { log = JSON.parse(readFileSync(logPath, 'utf8')); } catch { log = []; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
log.push({
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
model,
|
|
48
|
+
usage,
|
|
49
|
+
estimated_usd: +usd.toFixed(6),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Keep last 500 sessions to cap file size
|
|
53
|
+
if (log.length > 500) log = log.slice(-500);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
writeFileSync(logPath, JSON.stringify(log, null, 2));
|
|
57
|
+
const total = log.reduce((sum, r) => sum + (r.estimated_usd ?? 0), 0);
|
|
58
|
+
console.error(
|
|
59
|
+
`[cost-tracker] Session cost: ~$${usd.toFixed(4)} | All-time logged: ~$${total.toFixed(2)}`
|
|
60
|
+
);
|
|
61
|
+
} catch { /* read-only fs — skip silently */ }
|
|
62
|
+
|
|
63
|
+
process.exit(0);
|