Reorganize: Move all skills to skills/ folder
- Created skills/ directory - Moved 272 skills to skills/ subfolder - Kept agents/ at root level - Kept installation scripts and docs at root level Repository structure: - skills/ - All 272 skills from skills.sh - agents/ - Agent definitions - *.sh, *.ps1 - Installation scripts - README.md, etc. - Documentation Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bun
|
||||
import * as z from 'zod';
|
||||
|
||||
const SCHEMA_OUTPUT_PATH = 'assets/cc-safety-net.schema.json';
|
||||
|
||||
const CustomRuleSchema = z
|
||||
.strictObject({
|
||||
name: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/)
|
||||
.describe('Unique identifier for the rule (case-insensitive for duplicate detection)'),
|
||||
command: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/)
|
||||
.describe(
|
||||
"Base command to match (e.g., 'git', 'npm', 'docker'). Paths are normalized to basename.",
|
||||
),
|
||||
subcommand: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/)
|
||||
.optional()
|
||||
.describe(
|
||||
"Optional subcommand to match (e.g., 'add', 'install'). If omitted, matches any subcommand.",
|
||||
),
|
||||
block_args: z
|
||||
.array(z.string().min(1))
|
||||
.min(1)
|
||||
.describe(
|
||||
'Arguments that trigger the block. Command is blocked if ANY of these are present.',
|
||||
),
|
||||
reason: z.string().min(1).max(256).describe('Message shown when the command is blocked'),
|
||||
})
|
||||
.describe('A custom rule that blocks specific command patterns');
|
||||
|
||||
const ConfigSchema = z.strictObject({
|
||||
$schema: z.string().optional().describe('JSON Schema reference for IDE support'),
|
||||
version: z.literal(1).describe('Schema version (must be 1)'),
|
||||
rules: z.array(CustomRuleSchema).default([]).describe('Custom blocking rules'),
|
||||
});
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log('Generating JSON Schema...');
|
||||
|
||||
const jsonSchema = z.toJSONSchema(ConfigSchema, {
|
||||
io: 'input',
|
||||
target: 'draft-7',
|
||||
});
|
||||
|
||||
const finalSchema = {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
$id: 'https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json',
|
||||
title: 'Safety Net Configuration',
|
||||
description: 'Configuration file for cc-safety-net plugin custom rules',
|
||||
...jsonSchema,
|
||||
};
|
||||
|
||||
await Bun.write(SCHEMA_OUTPUT_PATH, `${JSON.stringify(finalSchema, null, 2)}\n`);
|
||||
|
||||
console.log(`✓ JSON Schema generated: ${SCHEMA_OUTPUT_PATH}`);
|
||||
}
|
||||
|
||||
main();
|
||||
46
skills/plugins/claude-code-safety-net/scripts/build.ts
Normal file
46
skills/plugins/claude-code-safety-net/scripts/build.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Build script that injects __PKG_VERSION__ at compile time
|
||||
* to avoid embedding the full package.json in the bundle.
|
||||
*/
|
||||
|
||||
import pkg from '../package.json';
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: ['src/index.ts', 'src/bin/cc-safety-net.ts'],
|
||||
outdir: 'dist',
|
||||
target: 'node',
|
||||
define: {
|
||||
__PKG_VERSION__: JSON.stringify(pkg.version),
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Build failed:');
|
||||
for (const log of result.logs) {
|
||||
console.error(log);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const indexOutput = result.outputs.find((o) => o.path.endsWith('index.js'));
|
||||
const binOutput = result.outputs.find((o) => o.path.endsWith('cc-safety-net.js'));
|
||||
if (indexOutput) {
|
||||
console.log(` dist/index.js ${(indexOutput.size / 1024).toFixed(2)} KB`);
|
||||
}
|
||||
if (binOutput) {
|
||||
console.log(` dist/bin/cc-safety-net.js ${(binOutput.size / 1024).toFixed(2)} KB`);
|
||||
}
|
||||
|
||||
// Run build:types and build:schema
|
||||
const typesResult = Bun.spawnSync(['bun', 'run', 'build:types']);
|
||||
if (typesResult.exitCode !== 0) {
|
||||
console.error('build:types failed');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const schemaResult = Bun.spawnSync(['bun', 'run', 'build:schema']);
|
||||
if (schemaResult.exitCode !== 0) {
|
||||
console.error('build:schema failed');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from 'bun';
|
||||
|
||||
export type CommandRunner = (
|
||||
strings: TemplateStringsArray,
|
||||
...values: readonly string[]
|
||||
) => { text: () => Promise<string> };
|
||||
|
||||
const DEFAULT_RUNNER: CommandRunner = $;
|
||||
|
||||
export const EXCLUDED_AUTHORS = ['actions-user', 'github-actions[bot]', 'kenryu42'];
|
||||
|
||||
/** Regex to match included commit types (with optional scope) */
|
||||
export const INCLUDED_COMMIT_PATTERN = /^(feat|fix)(\([^)]+\))?:/i;
|
||||
|
||||
export const REPO = process.env.GITHUB_REPOSITORY ?? 'kenryu42/claude-code-safety-net';
|
||||
|
||||
/** Paths that indicate Claude Code plugin changes */
|
||||
const CLAUDE_CODE_PATHS = ['commands/', 'hooks/', '.claude-plugin/'];
|
||||
|
||||
/** Paths that indicate OpenCode plugin changes */
|
||||
const OPENCODE_PATHS = ['.opencode/'];
|
||||
|
||||
/**
|
||||
* Get the files changed in a commit.
|
||||
*/
|
||||
async function getChangedFiles(
|
||||
hash: string,
|
||||
runner: CommandRunner = DEFAULT_RUNNER,
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const output = await runner`git diff-tree --no-commit-id --name-only -r ${hash}`.text();
|
||||
return output.split('\n').filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file path belongs to Claude Code plugin.
|
||||
*/
|
||||
function isClaudeCodeFile(path: string): boolean {
|
||||
return CLAUDE_CODE_PATHS.some((prefix) => path.startsWith(prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file path belongs to OpenCode plugin.
|
||||
*/
|
||||
function isOpenCodeFile(path: string): boolean {
|
||||
return OPENCODE_PATHS.some((prefix) => path.startsWith(prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a commit based on its changed files.
|
||||
* Priority: core > claude-code > opencode (higher priority wins ties).
|
||||
*/
|
||||
function classifyCommit(files: string[]): 'core' | 'claude-code' | 'opencode' {
|
||||
if (files.length === 0) return 'core';
|
||||
|
||||
const hasCore = files.some((file) => !isClaudeCodeFile(file) && !isOpenCodeFile(file));
|
||||
if (hasCore) return 'core';
|
||||
|
||||
const hasClaudeCode = files.some((file) => isClaudeCodeFile(file));
|
||||
if (hasClaudeCode) return 'claude-code';
|
||||
|
||||
return 'opencode';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a commit message should be included in the changelog.
|
||||
* @param message - The commit message (can include hash prefix like "abc1234 feat: message")
|
||||
*/
|
||||
export function isIncludedCommit(message: string): boolean {
|
||||
// Remove optional hash prefix (e.g., "abc1234 " from git log output)
|
||||
const messageWithoutHash = message.replace(/^\w+\s+/, '');
|
||||
|
||||
return INCLUDED_COMMIT_PATTERN.test(messageWithoutHash);
|
||||
}
|
||||
|
||||
export async function getLatestReleasedTag(
|
||||
runner: CommandRunner = DEFAULT_RUNNER,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const tag =
|
||||
await runner`gh release list --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // empty'`.text();
|
||||
return tag.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface CategorizedChangelog {
|
||||
core: string[];
|
||||
claudeCode: string[];
|
||||
openCode: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format changelog and contributors into release notes.
|
||||
*/
|
||||
export function formatReleaseNotes(
|
||||
changelog: CategorizedChangelog,
|
||||
contributors: string[],
|
||||
): string[] {
|
||||
const notes: string[] = [];
|
||||
|
||||
// Core section
|
||||
notes.push('## Core');
|
||||
if (changelog.core.length > 0) {
|
||||
notes.push(...changelog.core);
|
||||
} else {
|
||||
notes.push('No changes in this release');
|
||||
}
|
||||
|
||||
// Claude Code section
|
||||
notes.push('');
|
||||
notes.push('## Claude Code');
|
||||
if (changelog.claudeCode.length > 0) {
|
||||
notes.push(...changelog.claudeCode);
|
||||
} else {
|
||||
notes.push('No changes in this release');
|
||||
}
|
||||
|
||||
// OpenCode section
|
||||
notes.push('');
|
||||
notes.push('## OpenCode');
|
||||
if (changelog.openCode.length > 0) {
|
||||
notes.push(...changelog.openCode);
|
||||
} else {
|
||||
notes.push('No changes in this release');
|
||||
}
|
||||
|
||||
// Contributors section
|
||||
if (contributors.length > 0) {
|
||||
notes.push(...contributors);
|
||||
}
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
export async function generateChangelog(
|
||||
previousTag: string,
|
||||
runner: CommandRunner = DEFAULT_RUNNER,
|
||||
): Promise<CategorizedChangelog> {
|
||||
const result: CategorizedChangelog = {
|
||||
core: [],
|
||||
claudeCode: [],
|
||||
openCode: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const log = await runner`git log ${previousTag}..HEAD --oneline --format="%h %s"`.text();
|
||||
const commits = log.split('\n').filter((line) => line && isIncludedCommit(line));
|
||||
|
||||
for (const commit of commits) {
|
||||
const hash = commit.split(' ')[0];
|
||||
if (!hash) continue;
|
||||
|
||||
const files = await getChangedFiles(hash, runner);
|
||||
const category = classifyCommit(files);
|
||||
|
||||
if (category === 'core') {
|
||||
result.core.push(`- ${commit}`);
|
||||
} else if (category === 'claude-code') {
|
||||
result.claudeCode.push(`- ${commit}`);
|
||||
} else {
|
||||
result.openCode.push(`- ${commit}`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No commits found
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getContributors(
|
||||
previousTag: string,
|
||||
runner: CommandRunner = DEFAULT_RUNNER,
|
||||
): Promise<string[]> {
|
||||
return getContributorsForRepo(previousTag, REPO, runner);
|
||||
}
|
||||
|
||||
export async function getContributorsForRepo(
|
||||
previousTag: string,
|
||||
repo: string,
|
||||
runner: CommandRunner = DEFAULT_RUNNER,
|
||||
): Promise<string[]> {
|
||||
const notes: string[] = [];
|
||||
|
||||
try {
|
||||
const compare =
|
||||
await runner`gh api "/repos/${repo}/compare/${previousTag}...HEAD" --jq '.commits[] | {login: .author.login, message: .commit.message}'`.text();
|
||||
const contributors = new Map<string, string[]>();
|
||||
|
||||
for (const line of compare.split('\n').filter(Boolean)) {
|
||||
const { login, message } = JSON.parse(line) as {
|
||||
login: string | null;
|
||||
message: string;
|
||||
};
|
||||
const title = message.split('\n')[0] ?? '';
|
||||
if (!isIncludedCommit(title)) continue;
|
||||
|
||||
if (login && !EXCLUDED_AUTHORS.includes(login)) {
|
||||
if (!contributors.has(login)) contributors.set(login, []);
|
||||
contributors.get(login)?.push(title);
|
||||
}
|
||||
}
|
||||
|
||||
if (contributors.size > 0) {
|
||||
notes.push('');
|
||||
notes.push(
|
||||
`**Thank you to ${contributors.size} community contributor${contributors.size > 1 ? 's' : ''}:**`,
|
||||
);
|
||||
for (const [username, userCommits] of contributors) {
|
||||
notes.push(`- @${username}:`);
|
||||
for (const commit of userCommits) {
|
||||
notes.push(` - ${commit}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Failed to fetch contributors
|
||||
}
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
export type RunChangelogOptions = {
|
||||
runner?: CommandRunner;
|
||||
log?: (message: string) => void;
|
||||
};
|
||||
|
||||
export async function runChangelog(options: RunChangelogOptions = {}): Promise<void> {
|
||||
const runner = options.runner ?? DEFAULT_RUNNER;
|
||||
const log = options.log ?? console.log;
|
||||
const previousTag = await getLatestReleasedTag(runner);
|
||||
|
||||
if (!previousTag) {
|
||||
log('Initial release');
|
||||
return;
|
||||
}
|
||||
|
||||
const changelog = await generateChangelog(previousTag, runner);
|
||||
const contributors = await getContributorsForRepo(previousTag, REPO, runner);
|
||||
const notes = formatReleaseNotes(changelog, contributors);
|
||||
|
||||
log(notes.join('\n'));
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
runChangelog();
|
||||
}
|
||||
164
skills/plugins/claude-code-safety-net/scripts/publish.ts
Normal file
164
skills/plugins/claude-code-safety-net/scripts/publish.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from 'bun';
|
||||
import { formatReleaseNotes, generateChangelog, getContributors } from './generate-changelog';
|
||||
|
||||
const PACKAGE_NAME = 'cc-safety-net';
|
||||
|
||||
const bump = process.env.BUMP as 'major' | 'minor' | 'patch' | undefined;
|
||||
const versionOverride = process.env.VERSION;
|
||||
const dryRun = process.argv.includes('--dry-run');
|
||||
|
||||
console.log(`=== ${dryRun ? '[DRY-RUN] ' : ''}Publishing cc-safety-net ===\n`);
|
||||
|
||||
async function fetchPreviousVersion(): Promise<string> {
|
||||
try {
|
||||
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
|
||||
if (!res.ok) throw new Error(`Failed to fetch: ${res.statusText}`);
|
||||
const data = (await res.json()) as { version: string };
|
||||
console.log(`Previous version: ${data.version}`);
|
||||
return data.version;
|
||||
} catch {
|
||||
console.log('No previous version found, starting from 0.0.0');
|
||||
return '0.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
function bumpVersion(version: string, type: 'major' | 'minor' | 'patch'): string {
|
||||
const parts = version.split('.').map((part) => Number(part));
|
||||
const major = parts[0] ?? 0;
|
||||
const minor = parts[1] ?? 0;
|
||||
const patch = parts[2] ?? 0;
|
||||
switch (type) {
|
||||
case 'major':
|
||||
return `${major + 1}.0.0`;
|
||||
case 'minor':
|
||||
return `${major}.${minor + 1}.0`;
|
||||
case 'patch':
|
||||
return `${major}.${minor}.${patch + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePackageVersion(newVersion: string): Promise<void> {
|
||||
const pkgPath = new URL('../package.json', import.meta.url).pathname;
|
||||
if (dryRun) {
|
||||
console.log(`Would update: ${pkgPath}`);
|
||||
return;
|
||||
}
|
||||
let pkg = await Bun.file(pkgPath).text();
|
||||
pkg = pkg.replace(/"version": "[^"]+"/, `"version": "${newVersion}"`);
|
||||
await Bun.write(pkgPath, pkg);
|
||||
console.log(`Updated: ${pkgPath}`);
|
||||
}
|
||||
|
||||
async function updatePluginVersion(newVersion: string): Promise<void> {
|
||||
const pluginPath = new URL('../.claude-plugin/plugin.json', import.meta.url).pathname;
|
||||
if (dryRun) {
|
||||
console.log(`Would update: ${pluginPath}`);
|
||||
return;
|
||||
}
|
||||
let plugin = await Bun.file(pluginPath).text();
|
||||
plugin = plugin.replace(/"version": "[^"]+"/, `"version": "${newVersion}"`);
|
||||
await Bun.write(pluginPath, plugin);
|
||||
console.log(`Updated: ${pluginPath}`);
|
||||
}
|
||||
|
||||
async function buildAndPublish(): Promise<void> {
|
||||
// Build AFTER version files are updated so correct version is injected into bundle
|
||||
console.log('\nBuilding...');
|
||||
const buildResult = Bun.spawnSync(['bun', 'run', 'build']);
|
||||
if (buildResult.exitCode !== 0) {
|
||||
console.error('Build failed');
|
||||
console.error(buildResult.stderr.toString());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log('Would publish to npm');
|
||||
return;
|
||||
}
|
||||
console.log('Publishing to npm...');
|
||||
if (process.env.CI) {
|
||||
await $`npm publish --access public --provenance --ignore-scripts`;
|
||||
} else {
|
||||
await $`npm publish --access public --ignore-scripts`;
|
||||
}
|
||||
}
|
||||
|
||||
async function gitTagAndRelease(newVersion: string, notes: string[]): Promise<void> {
|
||||
if (dryRun) {
|
||||
console.log('\nWould commit, tag, push, and create GitHub release (CI only)');
|
||||
return;
|
||||
}
|
||||
if (!process.env.CI) return;
|
||||
|
||||
console.log('\nCommitting and tagging...');
|
||||
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`;
|
||||
await $`git config user.name "github-actions[bot]"`;
|
||||
await $`git add package.json .claude-plugin/plugin.json assets/cc-safety-net.schema.json`;
|
||||
|
||||
const hasStagedChanges = await $`git diff --cached --quiet`.nothrow();
|
||||
if (hasStagedChanges.exitCode !== 0) {
|
||||
await $`git commit -m "release: v${newVersion}"`;
|
||||
} else {
|
||||
console.log('No changes to commit (version already updated)');
|
||||
}
|
||||
|
||||
const tagExists = await $`git rev-parse v${newVersion}`.nothrow();
|
||||
if (tagExists.exitCode !== 0) {
|
||||
await $`git tag v${newVersion}`;
|
||||
} else {
|
||||
console.log(`Tag v${newVersion} already exists`);
|
||||
}
|
||||
|
||||
await $`git push origin HEAD --tags`;
|
||||
|
||||
console.log('\nCreating GitHub release...');
|
||||
const releaseNotes = notes.length > 0 ? notes.join('\n') : 'No notable changes';
|
||||
const releaseExists = await $`gh release view v${newVersion}`.nothrow();
|
||||
if (releaseExists.exitCode !== 0) {
|
||||
await $`gh release create v${newVersion} --title "v${newVersion}" --notes ${releaseNotes}`;
|
||||
} else {
|
||||
console.log(`Release v${newVersion} already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkVersionExists(version: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/${version}`);
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const previous = await fetchPreviousVersion();
|
||||
const newVersion =
|
||||
versionOverride || (bump ? bumpVersion(previous, bump) : bumpVersion(previous, 'patch'));
|
||||
console.log(`New version: ${newVersion}\n`);
|
||||
|
||||
if (await checkVersionExists(newVersion)) {
|
||||
console.log(`Version ${newVersion} already exists on npm. Skipping publish.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await updatePackageVersion(newVersion);
|
||||
await updatePluginVersion(newVersion);
|
||||
const changelog = await generateChangelog(`v${previous}`);
|
||||
const contributors = await getContributors(`v${previous}`);
|
||||
const notes = formatReleaseNotes(changelog, contributors);
|
||||
|
||||
await buildAndPublish();
|
||||
await gitTagAndRelease(newVersion, notes);
|
||||
|
||||
if (dryRun) {
|
||||
console.log('\n--- Release Notes ---');
|
||||
console.log(notes.length > 0 ? notes.join('\n') : 'No notable changes');
|
||||
console.log(`\n=== [DRY-RUN] Would publish ${PACKAGE_NAME}@${newVersion} ===`);
|
||||
} else {
|
||||
console.log(`\n=== Successfully published ${PACKAGE_NAME}@${newVersion} ===`);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user