Files
everything-claude-code/skills/ck/commands/init.mjs
Sreedhara GS 17f6f95090 fix(ck): address Greptile + CodeRabbit review bugs
- Fix read-after-write in session-start.mjs: read prevSession BEFORE
  overwriting current-session.json so unsaved-session detection fires
- Fix shell injection in resume.mjs: replace execSync shell string with
  fs.existsSync for directory existence check
- Fix shell injection in shared.mjs gitSummary: replace nested \$(git ...)
  subshell with a separate runGit() call to get rev count
- Fix displayName never shown: render functions now use ctx.displayName
  ?? ctx.name so user-supplied names show instead of the slug
- Fix renderListTable: uses context.displayName ?? entry.name
- Fix init.mjs: use path.basename() instead of cwd.split('/').pop()
- Fix save.mjs confirmation: show original name, not contextDir slug

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 16:44:11 +09:00

144 lines
6.0 KiB
JavaScript

#!/usr/bin/env node
/**
* ck — Context Keeper v2
* init.mjs — auto-detect project info and output JSON for Claude to confirm
*
* Usage: node init.mjs
* stdout: JSON with auto-detected project info
* exit 0: success exit 1: error
*/
import { readFileSync, existsSync } from 'fs';
import { resolve, basename } from 'path';
import { readProjects } from './shared.mjs';
const cwd = process.env.PWD || process.cwd();
const projects = readProjects();
const output = {
path: cwd,
name: null,
description: null,
stack: [],
goal: null,
constraints: [],
repo: null,
alreadyRegistered: !!projects[cwd],
};
function readFile(filename) {
const p = resolve(cwd, filename);
if (!existsSync(p)) return null;
try { return readFileSync(p, 'utf8'); } catch { return null; }
}
function extractSection(md, heading) {
const re = new RegExp(`## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`);
const m = md.match(re);
return m ? m[1].trim() : null;
}
// ── package.json ──────────────────────────────────────────────────────────────
const pkg = readFile('package.json');
if (pkg) {
try {
const parsed = JSON.parse(pkg);
if (parsed.name && !output.name) output.name = parsed.name;
if (parsed.description && !output.description) output.description = parsed.description;
// Detect stack from dependencies
const deps = Object.keys({ ...(parsed.dependencies || {}), ...(parsed.devDependencies || {}) });
const stackMap = {
next: 'Next.js', react: 'React', vue: 'Vue', svelte: 'Svelte', astro: 'Astro',
express: 'Express', fastify: 'Fastify', hono: 'Hono', nestjs: 'NestJS',
typescript: 'TypeScript', prisma: 'Prisma', drizzle: 'Drizzle',
'@neondatabase/serverless': 'Neon', '@upstash/redis': 'Upstash Redis',
'@clerk/nextjs': 'Clerk', stripe: 'Stripe', tailwindcss: 'Tailwind CSS',
};
for (const [dep, label] of Object.entries(stackMap)) {
if (deps.includes(dep) && !output.stack.includes(label)) {
output.stack.push(label);
}
}
if (deps.includes('typescript') || existsSync(resolve(cwd, 'tsconfig.json'))) {
if (!output.stack.includes('TypeScript')) output.stack.push('TypeScript');
}
} catch { /* malformed package.json */ }
}
// ── go.mod ────────────────────────────────────────────────────────────────────
const goMod = readFile('go.mod');
if (goMod) {
if (!output.stack.includes('Go')) output.stack.push('Go');
const modName = goMod.match(/^module\s+(\S+)/m)?.[1];
if (modName && !output.name) output.name = modName.split('/').pop();
}
// ── Cargo.toml ────────────────────────────────────────────────────────────────
const cargo = readFile('Cargo.toml');
if (cargo) {
if (!output.stack.includes('Rust')) output.stack.push('Rust');
const crateName = cargo.match(/^name\s*=\s*"(.+?)"/m)?.[1];
if (crateName && !output.name) output.name = crateName;
}
// ── pyproject.toml ────────────────────────────────────────────────────────────
const pyproject = readFile('pyproject.toml');
if (pyproject) {
if (!output.stack.includes('Python')) output.stack.push('Python');
const pyName = pyproject.match(/^name\s*=\s*"(.+?)"/m)?.[1];
if (pyName && !output.name) output.name = pyName;
}
// ── .git/config (repo URL) ────────────────────────────────────────────────────
const gitConfig = readFile('.git/config');
if (gitConfig) {
const repoMatch = gitConfig.match(/url\s*=\s*(.+)/);
if (repoMatch) output.repo = repoMatch[1].trim();
}
// ── CLAUDE.md ─────────────────────────────────────────────────────────────────
const claudeMd = readFile('CLAUDE.md');
if (claudeMd) {
const goal = extractSection(claudeMd, 'Current Goal');
if (goal && !output.goal) output.goal = goal.split('\n')[0].trim();
const doNot = extractSection(claudeMd, 'Do Not Do');
if (doNot) {
const bullets = doNot.split('\n')
.filter(l => /^[-*]\s+/.test(l))
.map(l => l.replace(/^[-*]\s+/, '').trim());
output.constraints = bullets;
}
const stack = extractSection(claudeMd, 'Tech Stack');
if (stack && output.stack.length === 0) {
output.stack = stack.split(/[,\n]/).map(s => s.replace(/^[-*]\s+/, '').trim()).filter(Boolean);
}
// Description from first section or "What This Is"
const whatItIs = extractSection(claudeMd, 'What This Is') || extractSection(claudeMd, 'About');
if (whatItIs && !output.description) output.description = whatItIs.split('\n')[0].trim();
}
// ── README.md (description fallback) ─────────────────────────────────────────
const readme = readFile('README.md');
if (readme && !output.description) {
// First non-header, non-badge, non-empty paragraph
const lines = readme.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('!') && !trimmed.startsWith('>') && !trimmed.startsWith('[') && trimmed !== '---' && trimmed !== '___') {
output.description = trimmed.slice(0, 120);
break;
}
}
}
// ── Name fallback: directory name ─────────────────────────────────────────────
if (!output.name) {
output.name = basename(cwd).toLowerCase().replace(/\s+/g, '-');
}
console.log(JSON.stringify(output, null, 2));