SuperCharge Claude Code v1.0.0 - Complete Customization Package
Features: - 30+ Custom Skills (cognitive, development, UI/UX, autonomous agents) - RalphLoop autonomous agent integration - Multi-AI consultation (Qwen) - Agent management system with sync capabilities - Custom hooks for session management - MCP servers integration - Plugin marketplace setup - Comprehensive installation script Components: - Skills: always-use-superpowers, ralph, brainstorming, ui-ux-pro-max, etc. - Agents: 100+ agents across engineering, marketing, product, etc. - Hooks: session-start-superpowers, qwen-consult, ralph-auto-trigger - Commands: /brainstorm, /write-plan, /execute-plan - MCP Servers: zai-mcp-server, web-search-prime, web-reader, zread - Binaries: ralphloop wrapper Installation: ./supercharge.sh
This commit is contained in:
68
plugins/claude-code-safety-net/src/bin/cc-safety-net.ts
Normal file
68
plugins/claude-code-safety-net/src/bin/cc-safety-net.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
import { runClaudeCodeHook } from './claude-code.ts';
|
||||
import { CUSTOM_RULES_DOC } from './custom-rules-doc.ts';
|
||||
import { runGeminiCLIHook } from './gemini-cli.ts';
|
||||
import { printHelp, printVersion } from './help.ts';
|
||||
import { printStatusline } from './statusline.ts';
|
||||
import { verifyConfig } from './verify-config.ts';
|
||||
|
||||
function printCustomRulesDoc(): void {
|
||||
console.log(CUSTOM_RULES_DOC);
|
||||
}
|
||||
|
||||
type HookMode = 'claude-code' | 'gemini-cli' | 'statusline';
|
||||
|
||||
function handleCliFlags(): HookMode | null {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (args.includes('--version') || args.includes('-V')) {
|
||||
printVersion();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (args.includes('--verify-config') || args.includes('-vc')) {
|
||||
process.exit(verifyConfig());
|
||||
}
|
||||
|
||||
if (args.includes('--custom-rules-doc')) {
|
||||
printCustomRulesDoc();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (args.includes('--statusline')) {
|
||||
return 'statusline';
|
||||
}
|
||||
|
||||
if (args.includes('--claude-code') || args.includes('-cc')) {
|
||||
return 'claude-code';
|
||||
}
|
||||
|
||||
if (args.includes('--gemini-cli') || args.includes('-gc')) {
|
||||
return 'gemini-cli';
|
||||
}
|
||||
|
||||
console.error(`Unknown option: ${args[0]}`);
|
||||
console.error("Run 'cc-safety-net --help' for usage.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const mode = handleCliFlags();
|
||||
if (mode === 'claude-code') {
|
||||
await runClaudeCodeHook();
|
||||
} else if (mode === 'gemini-cli') {
|
||||
await runGeminiCLIHook();
|
||||
} else if (mode === 'statusline') {
|
||||
await printStatusline();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error('Safety Net error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
81
plugins/claude-code-safety-net/src/bin/claude-code.ts
Normal file
81
plugins/claude-code-safety-net/src/bin/claude-code.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { analyzeCommand, loadConfig } from '../core/analyze.ts';
|
||||
import { redactSecrets, writeAuditLog } from '../core/audit.ts';
|
||||
import { envTruthy } from '../core/env.ts';
|
||||
import { formatBlockedMessage } from '../core/format.ts';
|
||||
import type { HookInput, HookOutput } from '../types.ts';
|
||||
|
||||
function outputDeny(reason: string, command?: string, segment?: string): void {
|
||||
const message = formatBlockedMessage({
|
||||
reason,
|
||||
command,
|
||||
segment,
|
||||
redact: redactSecrets,
|
||||
});
|
||||
|
||||
const output: HookOutput = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PreToolUse',
|
||||
permissionDecision: 'deny',
|
||||
permissionDecisionReason: message,
|
||||
},
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(output));
|
||||
}
|
||||
|
||||
export async function runClaudeCodeHook(): Promise<void> {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(chunk as Buffer);
|
||||
}
|
||||
|
||||
const inputText = Buffer.concat(chunks).toString('utf-8').trim();
|
||||
|
||||
if (!inputText) {
|
||||
return;
|
||||
}
|
||||
|
||||
let input: HookInput;
|
||||
try {
|
||||
input = JSON.parse(inputText) as HookInput;
|
||||
} catch {
|
||||
if (envTruthy('SAFETY_NET_STRICT')) {
|
||||
outputDeny('Failed to parse hook input JSON (strict mode)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.tool_name !== 'Bash') {
|
||||
return;
|
||||
}
|
||||
|
||||
const command = input.tool_input?.command;
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cwd = input.cwd ?? process.cwd();
|
||||
const strict = envTruthy('SAFETY_NET_STRICT');
|
||||
const paranoidAll = envTruthy('SAFETY_NET_PARANOID');
|
||||
const paranoidRm = paranoidAll || envTruthy('SAFETY_NET_PARANOID_RM');
|
||||
const paranoidInterpreters = paranoidAll || envTruthy('SAFETY_NET_PARANOID_INTERPRETERS');
|
||||
|
||||
const config = loadConfig(cwd);
|
||||
|
||||
const result = analyzeCommand(command, {
|
||||
cwd,
|
||||
config,
|
||||
strict,
|
||||
paranoidRm,
|
||||
paranoidInterpreters,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
const sessionId = input.session_id;
|
||||
if (sessionId) {
|
||||
writeAuditLog(sessionId, command, result.segment, result.reason, cwd);
|
||||
}
|
||||
outputDeny(result.reason, command, result.segment);
|
||||
}
|
||||
}
|
||||
116
plugins/claude-code-safety-net/src/bin/custom-rules-doc.ts
Normal file
116
plugins/claude-code-safety-net/src/bin/custom-rules-doc.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
export const CUSTOM_RULES_DOC = `# Custom Rules Reference
|
||||
|
||||
Agent reference for generating \`.safety-net.json\` config files.
|
||||
|
||||
## Config Locations
|
||||
|
||||
| Scope | Path | Priority |
|
||||
|-------|------|----------|
|
||||
| User | \`~/.cc-safety-net/config.json\` | Lower |
|
||||
| Project | \`.safety-net.json\` (cwd) | Higher (overrides user) |
|
||||
|
||||
Duplicate rule names (case-insensitive) → project wins.
|
||||
|
||||
## Schema
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json",
|
||||
"version": 1,
|
||||
"rules": [...]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
- \`$schema\`: Optional. Enables IDE autocomplete and inline validation.
|
||||
- \`version\`: Required. Must be \`1\`.
|
||||
- \`rules\`: Optional. Defaults to \`[]\`.
|
||||
|
||||
**Always include \`$schema\`** when generating config files for IDE support.
|
||||
|
||||
## Rule Fields
|
||||
|
||||
| Field | Required | Constraints |
|
||||
|-------|----------|-------------|
|
||||
| \`name\` | Yes | \`^[a-zA-Z][a-zA-Z0-9_-]{0,63}$\` — unique (case-insensitive) |
|
||||
| \`command\` | Yes | \`^[a-zA-Z][a-zA-Z0-9_-]*$\` — basename only, not path |
|
||||
| \`subcommand\` | No | Same pattern as command. Omit to match any. |
|
||||
| \`block_args\` | Yes | Non-empty array of non-empty strings |
|
||||
| \`reason\` | Yes | Non-empty string, max 256 chars |
|
||||
|
||||
## Guidelines:
|
||||
|
||||
- \`name\`: kebab-case, descriptive (e.g., \`block-git-add-all\`)
|
||||
- \`command\`: binary name only, lowercase
|
||||
- \`subcommand\`: omit if rule applies to any subcommand
|
||||
- \`block_args\`: include all variants (e.g., both \`-g\` and \`--global\`)
|
||||
- \`reason\`: explain why blocked AND suggest alternative
|
||||
|
||||
## Matching Behavior
|
||||
|
||||
- **Command**: Normalized to basename (\`/usr/bin/git\` → \`git\`)
|
||||
- **Subcommand**: First non-option argument after command
|
||||
- **Arguments**: Matched literally. Command blocked if **any** \`block_args\` item present.
|
||||
- **Short options**: Expanded (\`-Ap\` matches \`-A\`)
|
||||
- **Long options**: Exact match (\`--all-files\` does NOT match \`--all\`)
|
||||
- **Execution order**: Built-in rules first, then custom rules (additive only)
|
||||
|
||||
## Examples
|
||||
|
||||
### Block \`git add -A\`
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json",
|
||||
"version": 1,
|
||||
"rules": [
|
||||
{
|
||||
"name": "block-git-add-all",
|
||||
"command": "git",
|
||||
"subcommand": "add",
|
||||
"block_args": ["-A", "--all", "."],
|
||||
"reason": "Use 'git add <specific-files>' instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Block global npm install
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json",
|
||||
"version": 1,
|
||||
"rules": [
|
||||
{
|
||||
"name": "block-npm-global",
|
||||
"command": "npm",
|
||||
"subcommand": "install",
|
||||
"block_args": ["-g", "--global"],
|
||||
"reason": "Use npx or local install."
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Block docker system prune
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json",
|
||||
"version": 1,
|
||||
"rules": [
|
||||
{
|
||||
"name": "block-docker-prune",
|
||||
"command": "docker",
|
||||
"subcommand": "system",
|
||||
"block_args": ["prune"],
|
||||
"reason": "Use targeted cleanup instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Error Handling
|
||||
|
||||
Invalid config → silent fallback to built-in rules only. No custom rules applied.
|
||||
`;
|
||||
84
plugins/claude-code-safety-net/src/bin/gemini-cli.ts
Normal file
84
plugins/claude-code-safety-net/src/bin/gemini-cli.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { analyzeCommand, loadConfig } from '../core/analyze.ts';
|
||||
import { redactSecrets, writeAuditLog } from '../core/audit.ts';
|
||||
import { envTruthy } from '../core/env.ts';
|
||||
import { formatBlockedMessage } from '../core/format.ts';
|
||||
import type { GeminiHookInput, GeminiHookOutput } from '../types.ts';
|
||||
|
||||
function outputGeminiDeny(reason: string, command?: string, segment?: string): void {
|
||||
const message = formatBlockedMessage({
|
||||
reason,
|
||||
command,
|
||||
segment,
|
||||
redact: redactSecrets,
|
||||
});
|
||||
|
||||
// Gemini CLI expects exit code 0 with JSON for policy blocks; exit 2 is for hook errors.
|
||||
const output: GeminiHookOutput = {
|
||||
decision: 'deny',
|
||||
reason: message,
|
||||
systemMessage: message,
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(output));
|
||||
}
|
||||
|
||||
export async function runGeminiCLIHook(): Promise<void> {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(chunk as Buffer);
|
||||
}
|
||||
|
||||
const inputText = Buffer.concat(chunks).toString('utf-8').trim();
|
||||
|
||||
if (!inputText) {
|
||||
return;
|
||||
}
|
||||
|
||||
let input: GeminiHookInput;
|
||||
try {
|
||||
input = JSON.parse(inputText) as GeminiHookInput;
|
||||
} catch {
|
||||
if (envTruthy('SAFETY_NET_STRICT')) {
|
||||
outputGeminiDeny('Failed to parse hook input JSON (strict mode)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.hook_event_name !== 'BeforeTool') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.tool_name !== 'run_shell_command') {
|
||||
return;
|
||||
}
|
||||
|
||||
const command = input.tool_input?.command;
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cwd = input.cwd ?? process.cwd();
|
||||
const strict = envTruthy('SAFETY_NET_STRICT');
|
||||
const paranoidAll = envTruthy('SAFETY_NET_PARANOID');
|
||||
const paranoidRm = paranoidAll || envTruthy('SAFETY_NET_PARANOID_RM');
|
||||
const paranoidInterpreters = paranoidAll || envTruthy('SAFETY_NET_PARANOID_INTERPRETERS');
|
||||
|
||||
const config = loadConfig(cwd);
|
||||
|
||||
const result = analyzeCommand(command, {
|
||||
cwd,
|
||||
config,
|
||||
strict,
|
||||
paranoidRm,
|
||||
paranoidInterpreters,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
const sessionId = input.session_id;
|
||||
if (sessionId) {
|
||||
writeAuditLog(sessionId, command, result.segment, result.reason, cwd);
|
||||
}
|
||||
outputGeminiDeny(result.reason, command, result.segment);
|
||||
}
|
||||
}
|
||||
32
plugins/claude-code-safety-net/src/bin/help.ts
Normal file
32
plugins/claude-code-safety-net/src/bin/help.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
declare const __PKG_VERSION__: string | undefined;
|
||||
|
||||
const version = typeof __PKG_VERSION__ !== 'undefined' ? __PKG_VERSION__ : 'dev';
|
||||
|
||||
export function printHelp(): void {
|
||||
console.log(`cc-safety-net v${version}
|
||||
|
||||
Blocks destructive git and filesystem commands before execution.
|
||||
|
||||
USAGE:
|
||||
cc-safety-net -cc, --claude-code Run as Claude Code PreToolUse hook (reads JSON from stdin)
|
||||
cc-safety-net -gc, --gemini-cli Run as Gemini CLI BeforeTool hook (reads JSON from stdin)
|
||||
cc-safety-net -vc, --verify-config Validate config files
|
||||
cc-safety-net --custom-rules-doc Print custom rules documentation
|
||||
cc-safety-net --statusline Print status line with mode indicators
|
||||
cc-safety-net -h, --help Show this help
|
||||
cc-safety-net -V, --version Show version
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
SAFETY_NET_STRICT=1 Fail-closed on unparseable commands
|
||||
SAFETY_NET_PARANOID=1 Enable all paranoid checks
|
||||
SAFETY_NET_PARANOID_RM=1 Block non-temp rm -rf within cwd
|
||||
SAFETY_NET_PARANOID_INTERPRETERS=1 Block interpreter one-liners
|
||||
|
||||
CONFIG FILES:
|
||||
~/.cc-safety-net/config.json User-scope config
|
||||
.safety-net.json Project-scope config`);
|
||||
}
|
||||
|
||||
export function printVersion(): void {
|
||||
console.log(version);
|
||||
}
|
||||
117
plugins/claude-code-safety-net/src/bin/statusline.ts
Normal file
117
plugins/claude-code-safety-net/src/bin/statusline.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { envTruthy } from '../core/env.ts';
|
||||
|
||||
/**
|
||||
* Read piped stdin content asynchronously.
|
||||
* Returns null if stdin is a TTY (no piped input) or empty.
|
||||
*/
|
||||
async function readStdinAsync(): Promise<string | null> {
|
||||
if (process.stdin.isTTY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let data = '';
|
||||
process.stdin.setEncoding('utf-8');
|
||||
process.stdin.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
const trimmed = data.trim();
|
||||
resolve(trimmed || null);
|
||||
});
|
||||
process.stdin.on('error', () => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getSettingsPath(): string {
|
||||
// Allow override for testing
|
||||
if (process.env.CLAUDE_SETTINGS_PATH) {
|
||||
return process.env.CLAUDE_SETTINGS_PATH;
|
||||
}
|
||||
return join(homedir(), '.claude', 'settings.json');
|
||||
}
|
||||
|
||||
interface ClaudeSettings {
|
||||
enabledPlugins?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
function isPluginEnabled(): boolean {
|
||||
const settingsPath = getSettingsPath();
|
||||
|
||||
if (!existsSync(settingsPath)) {
|
||||
// Default to disabled if settings file doesn't exist
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(settingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content) as ClaudeSettings;
|
||||
|
||||
// If enabledPlugins doesn't exist or plugin not listed, default to disabled
|
||||
if (!settings.enabledPlugins) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pluginKey = 'safety-net@cc-marketplace';
|
||||
// If not explicitly set, default to disabled
|
||||
if (!(pluginKey in settings.enabledPlugins)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return settings.enabledPlugins[pluginKey] === true;
|
||||
} catch {
|
||||
// On any error (invalid JSON, etc.), default to disabled
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function printStatusline(): Promise<void> {
|
||||
const enabled = isPluginEnabled();
|
||||
|
||||
// Build our status string
|
||||
let status: string;
|
||||
|
||||
if (!enabled) {
|
||||
status = '🛡️ Safety Net ❌';
|
||||
} else {
|
||||
const strict = envTruthy('SAFETY_NET_STRICT');
|
||||
const paranoidAll = envTruthy('SAFETY_NET_PARANOID');
|
||||
const paranoidRm = paranoidAll || envTruthy('SAFETY_NET_PARANOID_RM');
|
||||
const paranoidInterpreters = paranoidAll || envTruthy('SAFETY_NET_PARANOID_INTERPRETERS');
|
||||
|
||||
let modeEmojis = '';
|
||||
|
||||
// Strict mode: 🔒
|
||||
if (strict) {
|
||||
modeEmojis += '🔒';
|
||||
}
|
||||
|
||||
// Paranoid modes: 👁️ if PARANOID or (PARANOID_RM + PARANOID_INTERPRETERS)
|
||||
// Otherwise individual emojis: 🗑️ for RM, 🐚 for interpreters
|
||||
if (paranoidAll || (paranoidRm && paranoidInterpreters)) {
|
||||
modeEmojis += '👁️';
|
||||
} else if (paranoidRm) {
|
||||
modeEmojis += '🗑️';
|
||||
} else if (paranoidInterpreters) {
|
||||
modeEmojis += '🐚';
|
||||
}
|
||||
|
||||
// If no mode flags, show ✅
|
||||
const statusEmoji = modeEmojis || '✅';
|
||||
status = `🛡️ Safety Net ${statusEmoji}`;
|
||||
}
|
||||
|
||||
// Check for piped stdin input and prepend with separator
|
||||
// Skip JSON input (Claude Code pipes status JSON that shouldn't be echoed)
|
||||
const stdinInput = await readStdinAsync();
|
||||
if (stdinInput && !stdinInput.startsWith('{')) {
|
||||
console.log(`${stdinInput} | ${status}`);
|
||||
} else {
|
||||
console.log(status);
|
||||
}
|
||||
}
|
||||
132
plugins/claude-code-safety-net/src/bin/verify-config.ts
Normal file
132
plugins/claude-code-safety-net/src/bin/verify-config.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Verify user and project scope config files for safety-net.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import {
|
||||
getProjectConfigPath,
|
||||
getUserConfigPath,
|
||||
type ValidationResult,
|
||||
validateConfigFile,
|
||||
} from '../core/config.ts';
|
||||
|
||||
export interface VerifyConfigOptions {
|
||||
userConfigPath?: string;
|
||||
projectConfigPath?: string;
|
||||
}
|
||||
|
||||
const HEADER = 'Safety Net Config';
|
||||
const SEPARATOR = '═'.repeat(HEADER.length);
|
||||
const SCHEMA_URL =
|
||||
'https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json';
|
||||
|
||||
function printHeader(): void {
|
||||
console.log(HEADER);
|
||||
console.log(SEPARATOR);
|
||||
}
|
||||
|
||||
function printValidConfig(scope: string, path: string, result: ValidationResult): void {
|
||||
console.log(`\n✓ ${scope} config: ${path}`);
|
||||
if (result.ruleNames.size > 0) {
|
||||
console.log(' Rules:');
|
||||
let i = 1;
|
||||
for (const name of result.ruleNames) {
|
||||
console.log(` ${i}. ${name}`);
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
console.log(' Rules: (none)');
|
||||
}
|
||||
}
|
||||
|
||||
function printInvalidConfig(scope: string, path: string, errors: string[]): void {
|
||||
console.error(`\n✗ ${scope} config: ${path}`);
|
||||
console.error(' Errors:');
|
||||
let errorNum = 1;
|
||||
for (const error of errors) {
|
||||
for (const part of error.split('; ')) {
|
||||
console.error(` ${errorNum}. ${part}`);
|
||||
errorNum++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addSchemaIfMissing(path: string): boolean {
|
||||
try {
|
||||
const content = readFileSync(path, 'utf-8');
|
||||
const parsed = JSON.parse(content) as Record<string, unknown>;
|
||||
|
||||
if (parsed.$schema) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const updated = { $schema: SCHEMA_URL, ...parsed };
|
||||
writeFileSync(path, JSON.stringify(updated, null, 2), 'utf-8');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify config files and print results.
|
||||
* @returns Exit code (0 = success, 1 = errors found)
|
||||
*/
|
||||
export function verifyConfig(options: VerifyConfigOptions = {}): number {
|
||||
const userConfig = options.userConfigPath ?? getUserConfigPath();
|
||||
const projectConfig = options.projectConfigPath ?? getProjectConfigPath();
|
||||
|
||||
let hasErrors = false;
|
||||
const configsChecked: Array<{
|
||||
scope: string;
|
||||
path: string;
|
||||
result: ValidationResult;
|
||||
}> = [];
|
||||
|
||||
printHeader();
|
||||
|
||||
if (existsSync(userConfig)) {
|
||||
const result = validateConfigFile(userConfig);
|
||||
configsChecked.push({ scope: 'User', path: userConfig, result });
|
||||
if (result.errors.length > 0) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync(projectConfig)) {
|
||||
const result = validateConfigFile(projectConfig);
|
||||
configsChecked.push({
|
||||
scope: 'Project',
|
||||
path: resolve(projectConfig),
|
||||
result,
|
||||
});
|
||||
if (result.errors.length > 0) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (configsChecked.length === 0) {
|
||||
console.log('\nNo config files found. Using built-in rules only.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const { scope, path, result } of configsChecked) {
|
||||
if (result.errors.length > 0) {
|
||||
printInvalidConfig(scope, path, result.errors);
|
||||
} else {
|
||||
if (addSchemaIfMissing(path)) {
|
||||
console.log(`\nAdded $schema to ${scope.toLowerCase()} config.`);
|
||||
}
|
||||
printValidConfig(scope, path, result);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasErrors) {
|
||||
console.error('\nConfig validation failed.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
console.log('\nAll configs valid.');
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user