Add hooks and CLAUDE.md resource types with install/uninstall scripts
Introduces two new resource types (hooks, claude-md) with full CRUD, visual hook config editor, section-delimited CLAUDE.md installs, uninstall endpoints, and shell injection hardening in sync scripts.
This commit is contained in:
@@ -7,6 +7,26 @@ export function isPowerShell(request: Request): boolean {
|
||||
return /PowerShell/i.test(ua);
|
||||
}
|
||||
|
||||
// --- Shell safety helpers ---
|
||||
|
||||
/** Escape a string for safe use inside double-quoted bash echo */
|
||||
function escBash(s: string): string {
|
||||
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
|
||||
}
|
||||
|
||||
/** Escape a string for safe use inside double-quoted PowerShell Write-Host */
|
||||
function escPS(s: string): string {
|
||||
return s.replace(/`/g, '``').replace(/"/g, '`"').replace(/\$/g, '`$');
|
||||
}
|
||||
|
||||
/** Validate that a value is safe to use as a path segment in generated scripts (slug or relativePath) */
|
||||
function assertSafePath(s: string): string {
|
||||
if (/[^a-zA-Z0-9_./-]/.test(s) || s.includes('..') || s.startsWith('/')) {
|
||||
throw new Error(`Unsafe path segment rejected: ${s}`);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// --- Push scripts (type-aware) ---
|
||||
|
||||
export async function buildPushScript(baseUrl: string, skillsDir: string): Promise<string> {
|
||||
@@ -277,32 +297,37 @@ export async function buildSyncScriptForType(baseUrl: string, targetDir: string,
|
||||
lines.push('');
|
||||
|
||||
for (const r of resources) {
|
||||
const safeSlug = assertSafePath(r.slug);
|
||||
const safeName = escBash(r.name);
|
||||
|
||||
if (r.format === 'folder') {
|
||||
// Fetch full resource to get file list
|
||||
const full = await getResource(type, r.slug);
|
||||
if (!full) continue;
|
||||
|
||||
lines.push(`mkdir -p "$TARGET_DIR/${r.slug}"`);
|
||||
lines.push(`curl -fsSL "${baseUrl}/${type}/${r.slug}" -o "$TARGET_DIR/${r.slug}/${config.mainFileName}"`);
|
||||
lines.push(`mkdir -p "$TARGET_DIR/${safeSlug}"`);
|
||||
lines.push(`curl -fsSL "${baseUrl}/${type}/${safeSlug}" -o "$TARGET_DIR/${safeSlug}/${config.mainFileName}"`);
|
||||
|
||||
for (const f of full.files) {
|
||||
const dir = f.relativePath.split('/').slice(0, -1).join('/');
|
||||
const safePath = assertSafePath(f.relativePath);
|
||||
const dir = safePath.split('/').slice(0, -1).join('/');
|
||||
if (dir) {
|
||||
lines.push(`mkdir -p "$TARGET_DIR/${r.slug}/${dir}"`);
|
||||
lines.push(`mkdir -p "$TARGET_DIR/${safeSlug}/${dir}"`);
|
||||
}
|
||||
lines.push(`curl -fsSL "${baseUrl}/api/resources/${type}/${r.slug}/files/${f.relativePath}" -o "$TARGET_DIR/${r.slug}/${f.relativePath}"`);
|
||||
lines.push(`curl -fsSL "${baseUrl}/api/resources/${type}/${safeSlug}/files/${safePath}" -o "$TARGET_DIR/${safeSlug}/${safePath}"`);
|
||||
}
|
||||
|
||||
const scriptFiles = full.files.filter(f => f.relativePath.startsWith('scripts/'));
|
||||
for (const f of scriptFiles) {
|
||||
lines.push(`chmod +x "$TARGET_DIR/${r.slug}/${f.relativePath}"`);
|
||||
const safePath = assertSafePath(f.relativePath);
|
||||
lines.push(`chmod +x "$TARGET_DIR/${safeSlug}/${safePath}"`);
|
||||
}
|
||||
|
||||
lines.push(`echo " ✓ ${r.name} (folder, ${full.files.length + 1} files)"`);
|
||||
lines.push(`echo " ✓ ${safeName} (folder, ${full.files.length + 1} files)"`);
|
||||
} else {
|
||||
const resourceUrl = `${baseUrl}/${type}/${r.slug}`;
|
||||
lines.push(`curl -fsSL "${resourceUrl}" -o "$TARGET_DIR/${r.slug}.md"`);
|
||||
lines.push(`echo " ✓ ${r.name}"`);
|
||||
const resourceUrl = `${baseUrl}/${type}/${safeSlug}`;
|
||||
lines.push(`curl -fsSL "${resourceUrl}" -o "$TARGET_DIR/${safeSlug}.md"`);
|
||||
lines.push(`echo " ✓ ${safeName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,27 +364,31 @@ export async function buildSyncScriptPSForType(baseUrl: string, targetDir: strin
|
||||
lines.push('');
|
||||
|
||||
for (const r of resources) {
|
||||
const safeSlug = assertSafePath(r.slug);
|
||||
const safeName = escPS(r.name);
|
||||
|
||||
if (r.format === 'folder') {
|
||||
const full = await getResource(type, r.slug);
|
||||
if (!full) continue;
|
||||
|
||||
lines.push(`New-Item -ItemType Directory -Force -Path (Join-Path $TargetDir "${r.slug}") | Out-Null`);
|
||||
lines.push(`Invoke-WebRequest -Uri "${baseUrl}/${type}/${r.slug}" -OutFile (Join-Path $TargetDir "${r.slug}\\${config.mainFileName}")`);
|
||||
lines.push(`New-Item -ItemType Directory -Force -Path (Join-Path $TargetDir "${safeSlug}") | Out-Null`);
|
||||
lines.push(`Invoke-WebRequest -Uri "${baseUrl}/${type}/${safeSlug}" -OutFile (Join-Path $TargetDir "${safeSlug}\\${config.mainFileName}")`);
|
||||
|
||||
for (const f of full.files) {
|
||||
const dir = f.relativePath.split('/').slice(0, -1).join('\\');
|
||||
const safePath = assertSafePath(f.relativePath);
|
||||
const dir = safePath.split('/').slice(0, -1).join('\\');
|
||||
if (dir) {
|
||||
lines.push(`New-Item -ItemType Directory -Force -Path (Join-Path $TargetDir "${r.slug}\\${dir}") | Out-Null`);
|
||||
lines.push(`New-Item -ItemType Directory -Force -Path (Join-Path $TargetDir "${safeSlug}\\${dir}") | Out-Null`);
|
||||
}
|
||||
const winPath = f.relativePath.replace(/\//g, '\\');
|
||||
lines.push(`Invoke-WebRequest -Uri "${baseUrl}/api/resources/${type}/${r.slug}/files/${f.relativePath}" -OutFile (Join-Path $TargetDir "${r.slug}\\${winPath}")`);
|
||||
const winPath = safePath.replace(/\//g, '\\');
|
||||
lines.push(`Invoke-WebRequest -Uri "${baseUrl}/api/resources/${type}/${safeSlug}/files/${safePath}" -OutFile (Join-Path $TargetDir "${safeSlug}\\${winPath}")`);
|
||||
}
|
||||
|
||||
lines.push(`Write-Host " ✓ ${r.name} (folder, ${full.files.length + 1} files)"`);
|
||||
lines.push(`Write-Host " ✓ ${safeName} (folder, ${full.files.length + 1} files)"`);
|
||||
} else {
|
||||
const resourceUrl = `${baseUrl}/${type}/${r.slug}`;
|
||||
lines.push(`Invoke-WebRequest -Uri "${resourceUrl}" -OutFile (Join-Path $TargetDir "${r.slug}.md")`);
|
||||
lines.push(`Write-Host " ✓ ${r.name}"`);
|
||||
const resourceUrl = `${baseUrl}/${type}/${safeSlug}`;
|
||||
lines.push(`Invoke-WebRequest -Uri "${resourceUrl}" -OutFile (Join-Path $TargetDir "${safeSlug}.md")`);
|
||||
lines.push(`Write-Host " ✓ ${safeName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user