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>
This commit is contained in:
Sreedhara GS
2026-03-27 16:44:11 +09:00
parent 1e226ba556
commit 17f6f95090
5 changed files with 23 additions and 24 deletions

View File

@@ -9,7 +9,7 @@
*/ */
import { readFileSync, existsSync } from 'fs'; import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path'; import { resolve, basename } from 'path';
import { readProjects } from './shared.mjs'; import { readProjects } from './shared.mjs';
const cwd = process.env.PWD || process.cwd(); const cwd = process.env.PWD || process.cwd();
@@ -137,7 +137,7 @@ if (readme && !output.description) {
// ── Name fallback: directory name ───────────────────────────────────────────── // ── Name fallback: directory name ─────────────────────────────────────────────
if (!output.name) { if (!output.name) {
output.name = cwd.split('/').pop().toLowerCase().replace(/\s+/g, '-'); output.name = basename(cwd).toLowerCase().replace(/\s+/g, '-');
} }
console.log(JSON.stringify(output, null, 2)); console.log(JSON.stringify(output, null, 2));

View File

@@ -8,8 +8,8 @@
* exit 0: success exit 1: not found * exit 0: success exit 1: not found
*/ */
import { existsSync } from 'fs';
import { resolveContext, renderBriefingBox } from './shared.mjs'; import { resolveContext, renderBriefingBox } from './shared.mjs';
import { execSync } from 'child_process';
const arg = process.argv[2]; const arg = process.argv[2];
const cwd = process.env.PWD || process.cwd(); const cwd = process.env.PWD || process.cwd();
@@ -25,16 +25,11 @@ const { context, projectPath } = resolved;
// Attempt to cd to the project path // Attempt to cd to the project path
if (projectPath && projectPath !== cwd) { if (projectPath && projectPath !== cwd) {
try { if (existsSync(projectPath)) {
const exists = execSync(`test -d "${projectPath}" && echo yes || echo no`, { console.log(`→ cd ${projectPath}`);
stdio: 'pipe', encoding: 'utf8', timeout: 2000, } else {
}).trim(); console.log(`⚠ Path not found: ${projectPath}`);
if (exists === 'yes') { }
console.log(`→ cd ${projectPath}`);
} else {
console.log(`⚠ Path not found: ${projectPath}`);
}
} catch { /* non-fatal */ }
} }
console.log(''); console.log('');

View File

@@ -85,7 +85,7 @@ if (isInit) {
}; };
writeProjects(projects); writeProjects(projects);
console.log(`✓ Project '${contextDir}' registered.`); console.log(`✓ Project '${name}' registered.`);
console.log(` Use /ck:save to save session state and /ck:resume to reload it next time.`); console.log(` Use /ck:save to save session state and /ck:resume to reload it next time.`);
process.exit(0); process.exit(0);
} }

View File

@@ -165,9 +165,10 @@ export function gitSummary(projectPath, sinceDate) {
const commits = log.split('\n').filter(Boolean).length; const commits = log.split('\n').filter(Boolean).length;
if (commits === 0) return null; if (commits === 0) return null;
// Count unique files changed across those commits // Count unique files changed: use a separate runGit call to avoid nested shell substitution
const diff = runGit(`diff --shortstat HEAD@{$(git -C "${projectPath}" rev-list --count HEAD --since="${sinceDate}")}..HEAD 2>/dev/null`, projectPath) const countStr = runGit(`rev-list --count HEAD --since="${sinceDate}"`, projectPath);
|| runGit(`diff --shortstat HEAD~${Math.min(commits, 50)}..HEAD`, projectPath); const revCount = countStr ? parseInt(countStr, 10) : commits;
const diff = runGit(`diff --shortstat HEAD~${Math.min(revCount, 50)}..HEAD`, projectPath);
if (diff) { if (diff) {
const filesMatch = diff.match(/(\d+) file/); const filesMatch = diff.match(/(\d+) file/);
@@ -196,7 +197,7 @@ export function renderContextMd(ctx) {
const latest = ctx.sessions?.[ctx.sessions.length - 1] || null; const latest = ctx.sessions?.[ctx.sessions.length - 1] || null;
const lines = [ const lines = [
`<!-- Generated by ck v2 — edit context.json instead -->`, `<!-- Generated by ck v2 — edit context.json instead -->`,
`# Project: ${ctx.name}`, `# Project: ${ctx.displayName ?? ctx.name}`,
`> Path: ${ctx.path}`, `> Path: ${ctx.path}`,
]; ];
if (ctx.repo) lines.push(`> Repo: ${ctx.repo}`); if (ctx.repo) lines.push(`> Repo: ${ctx.repo}`);
@@ -282,7 +283,7 @@ export function renderBriefingBox(ctx, meta = {}) {
const lines = [ const lines = [
`${'─'.repeat(W)}`, `${'─'.repeat(W)}`,
`│ RESUMING: ${pad(ctx.name, W - 12)}`, `│ RESUMING: ${pad(ctx.displayName ?? ctx.name, W - 12)}`,
`│ Last session: ${pad(`${when} | Sessions: ${sessions}`, W - 16)}`, `│ Last session: ${pad(`${when} | Sessions: ${sessions}`, W - 16)}`,
]; ];
if (shortSessId) lines.push(`│ Session ID: ${pad(shortSessId, W - 14)}`); if (shortSessId) lines.push(`│ Session ID: ${pad(shortSessId, W - 14)}`);
@@ -318,7 +319,7 @@ export function renderInfoBlock(ctx) {
const latest = ctx.sessions?.[ctx.sessions.length - 1] || {}; const latest = ctx.sessions?.[ctx.sessions.length - 1] || {};
const sep = '─'.repeat(44); const sep = '─'.repeat(44);
const lines = [ const lines = [
`ck: ${ctx.name}`, `ck: ${ctx.displayName ?? ctx.name}`,
sep, sep,
]; ];
lines.push(`PATH ${ctx.path}`); lines.push(`PATH ${ctx.path}`);
@@ -352,7 +353,7 @@ export function renderListTable(entries, cwd, todayStr) {
const statusLabel = icon === '●' ? '● Active' : icon === '◐' ? '◐ Warm' : '○ Stale'; const statusLabel = icon === '●' ? '● Active' : icon === '◐' ? '◐ Warm' : '○ Stale';
const sessId = latest.id ? latest.id.slice(0, 8) : '—'; const sessId = latest.id ? latest.id.slice(0, 8) : '—';
const summary = (latest.summary || '—').slice(0, 34); const summary = (latest.summary || '—').slice(0, 34);
const displayName = (e.name + (isHere ? ' <-' : '')).slice(0, 18); const displayName = ((e.context?.displayName ?? e.name) + (isHere ? ' <-' : '')).slice(0, 18);
return { return {
num: String(i + 1), num: String(i + 1),
name: displayName, name: displayName,

View File

@@ -86,6 +86,9 @@ function main() {
const projects = readJson(PROJECTS_FILE) || {}; const projects = readJson(PROJECTS_FILE) || {};
const entry = projects[cwd]; const entry = projects[cwd];
// Read previous session BEFORE overwriting current-session.json
const prevSession = readJson(CURRENT_SESSION);
// Write current-session.json // Write current-session.json
try { try {
writeFileSync(CURRENT_SESSION, JSON.stringify({ writeFileSync(CURRENT_SESSION, JSON.stringify({
@@ -108,17 +111,17 @@ function main() {
const latest = context.sessions?.[context.sessions.length - 1] || {}; const latest = context.sessions?.[context.sessions.length - 1] || {};
const sessionDate = latest.date || context.createdAt; const sessionDate = latest.date || context.createdAt;
const sessionCount = context.sessions?.length || 0; const sessionCount = context.sessions?.length || 0;
const displayName = context.displayName ?? context.name;
// ── Compact summary block (~100 tokens) ────────────────────────────── // ── Compact summary block (~100 tokens) ──────────────────────────────
const summaryLines = [ const summaryLines = [
`ck: ${context.name} | ${daysAgo(sessionDate)} | ${sessionCount} session${sessionCount !== 1 ? 's' : ''}`, `ck: ${displayName} | ${daysAgo(sessionDate)} | ${sessionCount} session${sessionCount !== 1 ? 's' : ''}`,
`Goal: ${context.goal || '—'}`, `Goal: ${context.goal || '—'}`,
latest.leftOff ? `Left off: ${latest.leftOff.split('\n')[0]}` : null, latest.leftOff ? `Left off: ${latest.leftOff.split('\n')[0]}` : null,
latest.nextSteps?.length ? `Next: ${latest.nextSteps.slice(0, 2).join(' · ')}` : null, latest.nextSteps?.length ? `Next: ${latest.nextSteps.slice(0, 2).join(' · ')}` : null,
].filter(Boolean); ].filter(Boolean);
// ── Unsaved session detection ───────────────────────────────────────── // ── Unsaved session detection ─────────────────────────────────────────
const prevSession = readJson(CURRENT_SESSION);
if (prevSession?.sessionId && prevSession.sessionId !== sessionId) { if (prevSession?.sessionId && prevSession.sessionId !== sessionId) {
// Check if previous session ID exists in sessions array // Check if previous session ID exists in sessions array
const alreadySaved = context.sessions?.some(s => s.id === prevSession.sessionId); const alreadySaved = context.sessions?.some(s => s.id === prevSession.sessionId);
@@ -141,7 +144,7 @@ function main() {
parts.push([ parts.push([
`---`, `---`,
`## ck: ${context.name}`, `## ck: ${displayName}`,
``, ``,
summaryLines.join('\n'), summaryLines.join('\n'),
].join('\n')); ].join('\n'));