Files
SuperCharged-Claude-Code-Up…/skills/plugins/claude-code-safety-net/src/core/config.ts
admin b723e2bd7d 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>
2026-01-23 18:05:17 +00:00

223 lines
6.0 KiB
TypeScript

import { existsSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, resolve } from 'node:path';
import {
COMMAND_PATTERN,
type Config,
MAX_REASON_LENGTH,
NAME_PATTERN,
type ValidationResult,
} from '../types.ts';
const DEFAULT_CONFIG: Config = {
version: 1,
rules: [],
};
export interface LoadConfigOptions {
/** Override user config directory (for testing) */
userConfigDir?: string;
}
export function loadConfig(cwd?: string, options?: LoadConfigOptions): Config {
const safeCwd = typeof cwd === 'string' ? cwd : process.cwd();
const userConfigDir = options?.userConfigDir ?? join(homedir(), '.cc-safety-net');
const userConfigPath = join(userConfigDir, 'config.json');
const projectConfigPath = join(safeCwd, '.safety-net.json');
const userConfig = loadSingleConfig(userConfigPath);
const projectConfig = loadSingleConfig(projectConfigPath);
return mergeConfigs(userConfig, projectConfig);
}
function loadSingleConfig(path: string): Config | null {
if (!existsSync(path)) {
return null;
}
try {
const content = readFileSync(path, 'utf-8');
if (!content.trim()) {
return null;
}
const parsed = JSON.parse(content) as unknown;
const result = validateConfig(parsed);
if (result.errors.length > 0) {
return null;
}
// Ensure rules array exists (may be undefined if not in input)
const cfg = parsed as Record<string, unknown>;
return {
version: cfg.version as number,
rules: (cfg.rules as Config['rules']) ?? [],
};
} catch {
return null;
}
}
function mergeConfigs(userConfig: Config | null, projectConfig: Config | null): Config {
if (!userConfig && !projectConfig) {
return DEFAULT_CONFIG;
}
if (!userConfig) {
return projectConfig ?? DEFAULT_CONFIG;
}
if (!projectConfig) {
return userConfig;
}
const projectRuleNames = new Set(projectConfig.rules.map((r) => r.name.toLowerCase()));
const mergedRules = [
...userConfig.rules.filter((r) => !projectRuleNames.has(r.name.toLowerCase())),
...projectConfig.rules,
];
return {
version: 1,
rules: mergedRules,
};
}
/** @internal Exported for testing */
export function validateConfig(config: unknown): ValidationResult {
const errors: string[] = [];
const ruleNames = new Set<string>();
if (!config || typeof config !== 'object') {
errors.push('Config must be an object');
return { errors, ruleNames };
}
const cfg = config as Record<string, unknown>;
if (cfg.version !== 1) {
errors.push('version must be 1');
}
if (cfg.rules !== undefined) {
if (!Array.isArray(cfg.rules)) {
errors.push('rules must be an array');
} else {
for (let i = 0; i < cfg.rules.length; i++) {
const rule = cfg.rules[i] as unknown;
const ruleErrors = validateRule(rule, i, ruleNames);
errors.push(...ruleErrors);
}
}
}
return { errors, ruleNames };
}
function validateRule(rule: unknown, index: number, ruleNames: Set<string>): string[] {
const errors: string[] = [];
const prefix = `rules[${index}]`;
if (!rule || typeof rule !== 'object') {
errors.push(`${prefix}: must be an object`);
return errors;
}
const r = rule as Record<string, unknown>;
if (typeof r.name !== 'string') {
errors.push(`${prefix}.name: required string`);
} else {
if (!NAME_PATTERN.test(r.name)) {
errors.push(
`${prefix}.name: must match pattern (letters, numbers, hyphens, underscores; max 64 chars)`,
);
}
const lowerName = r.name.toLowerCase();
if (ruleNames.has(lowerName)) {
errors.push(`${prefix}.name: duplicate rule name "${r.name}"`);
} else {
ruleNames.add(lowerName);
}
}
if (typeof r.command !== 'string') {
errors.push(`${prefix}.command: required string`);
} else if (!COMMAND_PATTERN.test(r.command)) {
errors.push(`${prefix}.command: must match pattern (letters, numbers, hyphens, underscores)`);
}
if (r.subcommand !== undefined) {
if (typeof r.subcommand !== 'string') {
errors.push(`${prefix}.subcommand: must be a string if provided`);
} else if (!COMMAND_PATTERN.test(r.subcommand)) {
errors.push(
`${prefix}.subcommand: must match pattern (letters, numbers, hyphens, underscores)`,
);
}
}
if (!Array.isArray(r.block_args)) {
errors.push(`${prefix}.block_args: required array`);
} else {
if (r.block_args.length === 0) {
errors.push(`${prefix}.block_args: must have at least one element`);
}
for (let i = 0; i < r.block_args.length; i++) {
const arg = r.block_args[i];
if (typeof arg !== 'string') {
errors.push(`${prefix}.block_args[${i}]: must be a string`);
} else if (arg === '') {
errors.push(`${prefix}.block_args[${i}]: must not be empty`);
}
}
}
if (typeof r.reason !== 'string') {
errors.push(`${prefix}.reason: required string`);
} else if (r.reason === '') {
errors.push(`${prefix}.reason: must not be empty`);
} else if (r.reason.length > MAX_REASON_LENGTH) {
errors.push(`${prefix}.reason: must be at most ${MAX_REASON_LENGTH} characters`);
}
return errors;
}
export function validateConfigFile(path: string): ValidationResult {
const errors: string[] = [];
const ruleNames = new Set<string>();
if (!existsSync(path)) {
errors.push(`File not found: ${path}`);
return { errors, ruleNames };
}
try {
const content = readFileSync(path, 'utf-8');
if (!content.trim()) {
errors.push('Config file is empty');
return { errors, ruleNames };
}
const parsed = JSON.parse(content) as unknown;
return validateConfig(parsed);
} catch (e) {
errors.push(`Invalid JSON: ${e instanceof Error ? e.message : String(e)}`);
return { errors, ruleNames };
}
}
export function getUserConfigPath(): string {
return join(homedir(), '.cc-safety-net', 'config.json');
}
export function getProjectConfigPath(cwd?: string): string {
return resolve(cwd ?? process.cwd(), '.safety-net.json');
}
export type { ValidationResult };