@claudemini/shit-cli 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/disable.js CHANGED
@@ -7,17 +7,17 @@
7
7
 
8
8
  import { existsSync, readFileSync, writeFileSync, rmSync } from 'fs';
9
9
  import { join } from 'path';
10
-
11
- function findProjectRoot() {
12
- let dir = process.cwd();
13
- while (dir !== '/') {
14
- if (existsSync(join(dir, '.git'))) {
15
- return dir;
16
- }
17
- dir = join(dir, '..');
18
- }
19
- throw new Error('Not in a git repository');
20
- }
10
+ import { getProjectRoot } from './config.js';
11
+
12
+ const CLAUDE_HOOK_TYPES = [
13
+ 'SessionStart',
14
+ 'SessionEnd',
15
+ 'UserPromptSubmit',
16
+ 'PreToolUse',
17
+ 'PostToolUse',
18
+ 'Stop',
19
+ 'Notification',
20
+ ];
21
21
 
22
22
  function removeClaudeHooks(projectRoot) {
23
23
  const settingsFile = join(projectRoot, '.claude', 'settings.json');
@@ -28,13 +28,49 @@ function removeClaudeHooks(projectRoot) {
28
28
 
29
29
  try {
30
30
  const settings = JSON.parse(readFileSync(settingsFile, 'utf-8'));
31
+ let removed = false;
31
32
 
32
33
  if (settings.hooks) {
33
- // Remove shit-cli hooks
34
- delete settings.hooks.session_start;
35
- delete settings.hooks.session_end;
36
- delete settings.hooks.tool_use;
37
- delete settings.hooks.edit_applied;
34
+ for (const hookType of CLAUDE_HOOK_TYPES) {
35
+ const rawEntries = settings.hooks[hookType];
36
+ if (Array.isArray(rawEntries)) {
37
+ const nextEntries = rawEntries
38
+ .map(entry => {
39
+ if (!Array.isArray(entry?.hooks)) {
40
+ return entry;
41
+ }
42
+ const nextHooks = entry.hooks.filter(hook =>
43
+ !(typeof hook?.command === 'string' && hook.command.includes('shit log'))
44
+ );
45
+ if (nextHooks.length !== entry.hooks.length) {
46
+ removed = true;
47
+ }
48
+ if (nextHooks.length === 0) {
49
+ return null;
50
+ }
51
+ return { ...entry, hooks: nextHooks };
52
+ })
53
+ .filter(Boolean);
54
+
55
+ if (nextEntries.length > 0) {
56
+ settings.hooks[hookType] = nextEntries;
57
+ } else {
58
+ delete settings.hooks[hookType];
59
+ }
60
+ } else if (typeof rawEntries === 'string' && rawEntries.includes('shit log')) {
61
+ delete settings.hooks[hookType];
62
+ removed = true;
63
+ }
64
+ }
65
+
66
+ // Cleanup legacy wrong names written by old versions.
67
+ const legacyKeys = ['session_start', 'session_end', 'tool_use', 'edit_applied'];
68
+ for (const key of legacyKeys) {
69
+ if (Object.prototype.hasOwnProperty.call(settings.hooks, key)) {
70
+ delete settings.hooks[key];
71
+ removed = true;
72
+ }
73
+ }
38
74
 
39
75
  // If hooks object is empty, remove it
40
76
  if (Object.keys(settings.hooks).length === 0) {
@@ -43,7 +79,7 @@ function removeClaudeHooks(projectRoot) {
43
79
  }
44
80
 
45
81
  writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
46
- return true;
82
+ return removed;
47
83
  } catch {
48
84
  return false;
49
85
  }
@@ -74,7 +110,7 @@ function removeFromGitignore(projectRoot) {
74
110
 
75
111
  export default async function disable(args) {
76
112
  try {
77
- const projectRoot = findProjectRoot();
113
+ const projectRoot = getProjectRoot();
78
114
  const cleanData = args.includes('--clean') || args.includes('--purge');
79
115
 
80
116
  console.log('🔧 Disabling shit-cli in repository...');
package/lib/doctor.js CHANGED
@@ -5,20 +5,10 @@
5
5
  * Similar to 'entire doctor' - repairs corrupted state and cleans up
6
6
  */
7
7
 
8
- import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync } from 'fs';
8
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import { execSync } from 'child_process';
11
-
12
- function findProjectRoot() {
13
- let dir = process.cwd();
14
- while (dir !== '/') {
15
- if (existsSync(join(dir, '.git'))) {
16
- return dir;
17
- }
18
- dir = join(dir, '..');
19
- }
20
- throw new Error('Not in a git repository');
21
- }
11
+ import { getProjectRoot, SESSION_ID_REGEX } from './config.js';
22
12
 
23
13
  function git(cmd, cwd) {
24
14
  return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 10000 }).trim();
@@ -33,7 +23,6 @@ function checkShitLogsStructure(projectRoot) {
33
23
  type: 'missing_directory',
34
24
  message: '.shit-logs directory not found',
35
25
  fix: () => {
36
- const { mkdirSync } = require('fs');
37
26
  mkdirSync(shitLogsDir, { recursive: true });
38
27
  return 'Created .shit-logs directory';
39
28
  }
@@ -67,7 +56,6 @@ function checkShitLogsStructure(projectRoot) {
67
56
  message: 'index.json is corrupted',
68
57
  fix: () => {
69
58
  const backup = `${indexFile}.backup.${Date.now()}`;
70
- const { copyFileSync } = require('fs');
71
59
  copyFileSync(indexFile, backup);
72
60
 
73
61
  const initialIndex = {
@@ -98,7 +86,7 @@ function checkSessionDirectories(projectRoot) {
98
86
  const entries = readdirSync(shitLogsDir);
99
87
  const sessionDirs = entries.filter(name => {
100
88
  const fullPath = join(shitLogsDir, name);
101
- return statSync(fullPath).isDirectory() && name.match(/^\d{4}-\d{2}-\d{2}-[a-f0-9-]+$/);
89
+ return statSync(fullPath).isDirectory() && SESSION_ID_REGEX.test(name);
102
90
  });
103
91
 
104
92
  for (const sessionDir of sessionDirs) {
@@ -132,7 +120,6 @@ function checkSessionDirectories(projectRoot) {
132
120
  sessionDir,
133
121
  fix: () => {
134
122
  const backup = `${stateFile}.backup.${Date.now()}`;
135
- const { copyFileSync } = require('fs');
136
123
  copyFileSync(stateFile, backup);
137
124
 
138
125
  const defaultState = {
@@ -166,7 +153,7 @@ function checkOrphanedShadowBranches(projectRoot) {
166
153
  const sessionDirs = existsSync(shitLogsDir) ?
167
154
  readdirSync(shitLogsDir).filter(name => {
168
155
  const fullPath = join(shitLogsDir, name);
169
- return statSync(fullPath).isDirectory() && name.match(/^\d{4}-\d{2}-\d{2}-[a-f0-9-]+$/);
156
+ return statSync(fullPath).isDirectory() && SESSION_ID_REGEX.test(name);
170
157
  }) : [];
171
158
 
172
159
  for (const branch of branches) {
@@ -208,11 +195,12 @@ function checkStuckSessions(projectRoot) {
208
195
  const entries = readdirSync(shitLogsDir);
209
196
  const sessionDirs = entries.filter(name => {
210
197
  const fullPath = join(shitLogsDir, name);
211
- return statSync(fullPath).isDirectory() && name.match(/^\d{4}-\d{2}-\d{2}-[a-f0-9-]+$/);
198
+ return statSync(fullPath).isDirectory() && SESSION_ID_REGEX.test(name);
212
199
  });
213
200
 
214
201
  const now = new Date();
215
202
  const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
203
+ const endedHookTypes = new Set(['session-end', 'SessionEnd', 'stop', 'session_end', 'end']);
216
204
 
217
205
  for (const sessionDir of sessionDirs) {
218
206
  const sessionPath = join(shitLogsDir, sessionDir);
@@ -221,13 +209,18 @@ function checkStuckSessions(projectRoot) {
221
209
  if (existsSync(stateFile)) {
222
210
  try {
223
211
  const state = JSON.parse(readFileSync(stateFile, 'utf-8'));
224
- const startTime = new Date(state.start_time);
225
-
226
- // Check for sessions older than 24 hours without end_time
227
- if (!state.end_time && startTime < oneDayAgo) {
212
+ const lastActivity = new Date(state.last_time || state.start_time);
213
+ const hasValidLastActivity = !Number.isNaN(lastActivity.getTime());
214
+ const endedByHook = endedHookTypes.has(state.last_hook_type);
215
+ const hasShadowBranch = typeof state.shadow_branch === 'string' && state.shadow_branch.length > 0;
216
+ const hasCheckpoint = Array.isArray(state.checkpoints) && state.checkpoints.length > 0;
217
+ const consideredEnded = Boolean(state.end_time) || endedByHook || hasShadowBranch || hasCheckpoint;
218
+
219
+ // Stuck means inactive for >24h and not explicitly/implicitly ended.
220
+ if (!consideredEnded && hasValidLastActivity && lastActivity < oneDayAgo) {
228
221
  issues.push({
229
222
  type: 'stuck_session',
230
- message: `Stuck session: ${sessionDir} (started ${startTime.toLocaleString()})`,
223
+ message: `Stuck session: ${sessionDir} (last activity ${lastActivity.toLocaleString()})`,
231
224
  sessionDir,
232
225
  fix: () => {
233
226
  state.end_time = new Date().toISOString();
@@ -248,7 +241,7 @@ function checkStuckSessions(projectRoot) {
248
241
 
249
242
  export default async function doctor(args) {
250
243
  try {
251
- const projectRoot = findProjectRoot();
244
+ const projectRoot = getProjectRoot();
252
245
  const autoFix = args.includes('--fix') || args.includes('--auto-fix');
253
246
  const verbose = args.includes('--verbose') || args.includes('-v');
254
247
 
package/lib/enable.js CHANGED
@@ -9,17 +9,7 @@
9
9
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
10
10
  import { join } from 'path';
11
11
  import { execSync } from 'child_process';
12
-
13
- function findProjectRoot() {
14
- let dir = process.cwd();
15
- while (dir !== '/') {
16
- if (existsSync(join(dir, '.git'))) {
17
- return dir;
18
- }
19
- dir = join(dir, '..');
20
- }
21
- throw new Error('Not in a git repository');
22
- }
12
+ import { getProjectRoot } from './config.js';
23
13
 
24
14
  // Agent-specific hook configurations
25
15
  const AGENT_HOOKS = {
@@ -95,9 +85,30 @@ function setupAgentHooks(projectRoot, agentType) {
95
85
  }
96
86
 
97
87
  // Add shit-cli hooks
98
- if (!settings.hooks) settings.hooks = {};
88
+ if (!settings.hooks) {
89
+ settings.hooks = {};
90
+ }
99
91
 
100
92
  for (const [hookName, command] of Object.entries(config.hooks)) {
93
+ if (agentType === 'claude-code') {
94
+ const current = settings.hooks[hookName];
95
+ const existing = Array.isArray(current)
96
+ ? current
97
+ : (typeof current === 'string'
98
+ ? [{ matcher: '', hooks: [{ type: 'command', command: current }] }]
99
+ : []);
100
+ const alreadyExists = existing.some(entry =>
101
+ Array.isArray(entry?.hooks) &&
102
+ entry.hooks.some(hook => typeof hook?.command === 'string' && hook.command.includes('shit log'))
103
+ );
104
+
105
+ if (!alreadyExists) {
106
+ existing.push({ matcher: '', hooks: [{ type: 'command', command }] });
107
+ }
108
+ settings.hooks[hookName] = existing;
109
+ continue;
110
+ }
111
+
101
112
  settings.hooks[hookName] = command;
102
113
  }
103
114
 
@@ -164,7 +175,7 @@ function initializeShitLogs(projectRoot) {
164
175
 
165
176
  export default async function enable(args) {
166
177
  try {
167
- const projectRoot = findProjectRoot();
178
+ const projectRoot = getProjectRoot();
168
179
 
169
180
  // Parse arguments
170
181
  const agents = [];
package/lib/explain.js CHANGED
@@ -5,19 +5,25 @@
5
5
  * Provides human-readable explanation of what happened in a session
6
6
  */
7
7
 
8
- import { existsSync, readFileSync } from 'fs';
8
+ import { existsSync, readFileSync, readdirSync } from 'fs';
9
9
  import { join } from 'path';
10
- import { execSync } from 'child_process';
10
+ import { execFileSync } from 'child_process';
11
+ import { getProjectRoot, SESSION_ID_REGEX } from './config.js';
11
12
 
12
- function findProjectRoot() {
13
- let dir = process.cwd();
14
- while (dir !== '/') {
15
- if (existsSync(join(dir, '.git'))) {
16
- return dir;
13
+ function git(args, cwd, ignoreError = false) {
14
+ try {
15
+ return execFileSync('git', args, {
16
+ cwd,
17
+ encoding: 'utf-8',
18
+ timeout: 10000,
19
+ stdio: ['pipe', 'pipe', 'pipe'],
20
+ }).trim();
21
+ } catch (error) {
22
+ if (ignoreError) {
23
+ return null;
17
24
  }
18
- dir = join(dir, '..');
25
+ throw error;
19
26
  }
20
- throw new Error('Not in a git repository');
21
27
  }
22
28
 
23
29
  function getSessionData(sessionId, projectRoot) {
@@ -41,7 +47,6 @@ function getSessionData(sessionId, projectRoot) {
41
47
  }
42
48
 
43
49
  function explainFromSummary(summaryText) {
44
- const lines = summaryText.split('\n');
45
50
  const explanation = [];
46
51
 
47
52
  // Extract key information
@@ -113,23 +118,20 @@ function explainFromSummary(summaryText) {
113
118
  function explainFromCommit(commitSha, projectRoot) {
114
119
  try {
115
120
  // Try to find associated checkpoint
116
- const checkpoints = execSync('git branch --list "shit/checkpoints/v1/*"', {
117
- cwd: projectRoot,
118
- encoding: 'utf-8'
119
- }).split('\n').filter(Boolean);
121
+ const checkpoints = (git(['branch', '--list', 'shit/checkpoints/v1/*'], projectRoot, true) || '')
122
+ .split('\n')
123
+ .map(line => line.trim().replace(/^\*?\s*/, ''))
124
+ .filter(Boolean);
120
125
 
121
126
  for (const branch of checkpoints) {
122
127
  try {
123
- const log = execSync(`git log ${branch} --oneline -1`, {
124
- cwd: projectRoot,
125
- encoding: 'utf-8'
126
- }).trim();
128
+ const log = git(['log', branch, '--oneline', '-1'], projectRoot, true);
129
+ if (!log) {
130
+ continue;
131
+ }
127
132
 
128
133
  if (log.includes(commitSha.slice(0, 12))) {
129
- const message = execSync(`git log ${branch} --format=%B -1`, {
130
- cwd: projectRoot,
131
- encoding: 'utf-8'
132
- }).trim();
134
+ const message = git(['log', branch, '--format=%B', '-1'], projectRoot, true) || '';
133
135
 
134
136
  return `📸 This commit has an associated checkpoint:\n\nBranch: ${branch}\n\n${message}`;
135
137
  }
@@ -146,13 +148,13 @@ function explainFromCommit(commitSha, projectRoot) {
146
148
 
147
149
  export default async function explain(args) {
148
150
  try {
149
- const projectRoot = findProjectRoot();
151
+ const projectRoot = getProjectRoot();
150
152
  const target = args[0];
151
153
 
152
154
  if (!target) {
153
155
  console.log('Usage: shit explain <session-id | commit-sha>');
154
156
  console.log('\nExamples:');
155
- console.log(' shit explain 2026-02-28-abc12345 # Explain a session');
157
+ console.log(' shit explain b5613b31-c732-4546-9be5-f8ae36f2327f # Explain a session');
156
158
  console.log(' shit explain abc1234 # Explain a commit');
157
159
  process.exit(1);
158
160
  }
@@ -170,23 +172,26 @@ export default async function explain(args) {
170
172
  }
171
173
 
172
174
  // Try as commit
173
- try {
174
- execSync('git rev-parse --verify ' + target + '^{commit}', {
175
- cwd: projectRoot,
176
- stdio: 'ignore'
177
- });
178
-
179
- console.log('📸 Commit Explanation\n');
180
- console.log(explainFromCommit(target, projectRoot));
181
- return;
182
- } catch {
183
- // Not a commit
175
+ const isCommitLike = /^[a-f0-9]{4,40}$/i.test(target);
176
+ if (isCommitLike) {
177
+ const resolvedCommit = git(['rev-parse', '--verify', `${target}^{commit}`], projectRoot, true);
178
+ if (resolvedCommit) {
179
+ console.log('📸 Commit Explanation\n');
180
+ console.log(explainFromCommit(resolvedCommit, projectRoot));
181
+ return;
182
+ }
184
183
  }
185
184
 
185
+ // Not a commit
186
+
186
187
  // Try partial match
187
- const sessions = execSync('ls .shit-logs', { cwd: projectRoot, encoding: 'utf-8' })
188
- .split('\n')
189
- .filter(s => s.includes(target));
188
+ const logDir = join(projectRoot, '.shit-logs');
189
+ const sessions = existsSync(logDir)
190
+ ? readdirSync(logDir, { withFileTypes: true })
191
+ .filter(entry => entry.isDirectory() && SESSION_ID_REGEX.test(entry.name))
192
+ .map(entry => entry.name)
193
+ .filter(name => name.includes(target))
194
+ : [];
190
195
 
191
196
  if (sessions.length > 0) {
192
197
  const matchedSession = sessions[0];
package/lib/reset.js CHANGED
@@ -5,20 +5,10 @@
5
5
  * Similar to 'entire reset' - removes shadow branch and session state
6
6
  */
7
7
 
8
- import { existsSync, readFileSync, rmSync } from 'fs';
8
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import { execSync } from 'child_process';
11
-
12
- function findProjectRoot() {
13
- let dir = process.cwd();
14
- while (dir !== '/') {
15
- if (existsSync(join(dir, '.git'))) {
16
- return dir;
17
- }
18
- dir = join(dir, '..');
19
- }
20
- throw new Error('Not in a git repository');
21
- }
11
+ import { getProjectRoot, SESSION_ID_REGEX } from './config.js';
22
12
 
23
13
  function git(cmd, cwd) {
24
14
  return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 10000 }).trim();
@@ -53,9 +43,11 @@ function findActiveSession(projectRoot) {
53
43
  return null;
54
44
  }
55
45
 
56
- const { readdirSync, statSync } = require('fs');
57
46
  const sessions = readdirSync(shitLogsDir)
58
- .filter(name => name.match(/^\d{4}-\d{2}-\d{2}-[a-f0-9-]+$/))
47
+ .filter(name => {
48
+ const fullPath = join(shitLogsDir, name);
49
+ return SESSION_ID_REGEX.test(name) && statSync(fullPath).isDirectory();
50
+ })
59
51
  .map(name => ({
60
52
  name,
61
53
  path: join(shitLogsDir, name),
@@ -68,7 +60,7 @@ function findActiveSession(projectRoot) {
68
60
 
69
61
  export default async function reset(args) {
70
62
  try {
71
- const projectRoot = findProjectRoot();
63
+ const projectRoot = getProjectRoot();
72
64
  const force = args.includes('--force') || args.includes('-f');
73
65
 
74
66
  // Get current commit
@@ -112,7 +104,7 @@ export default async function reset(args) {
112
104
  if (state.checkpoints && state.checkpoints.some(cp => cp.linked_commit === commitSha)) {
113
105
  // Remove checkpoint references
114
106
  state.checkpoints = state.checkpoints.filter(cp => cp.linked_commit !== commitSha);
115
- require('fs').writeFileSync(stateFile, JSON.stringify(state, null, 2));
107
+ writeFileSync(stateFile, JSON.stringify(state, null, 2));
116
108
  console.log('✅ Updated session state');
117
109
  }
118
110
  } catch {
package/lib/resume.js CHANGED
@@ -8,17 +8,8 @@
8
8
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import { execSync } from 'child_process';
11
-
12
- function findProjectRoot() {
13
- let dir = process.cwd();
14
- while (dir !== '/') {
15
- if (existsSync(join(dir, '.git'))) {
16
- return dir;
17
- }
18
- dir = join(dir, '..');
19
- }
20
- throw new Error('Not in a git repository');
21
- }
11
+ import { randomUUID } from 'crypto';
12
+ import { getProjectRoot } from './config.js';
22
13
 
23
14
  function git(cmd, cwd) {
24
15
  return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 10000 }).trim();
@@ -26,19 +17,37 @@ function git(cmd, cwd) {
26
17
 
27
18
  function findCheckpoint(projectRoot, checkpointId) {
28
19
  try {
29
- const branches = git('branch --list "shit/*"', projectRoot)
20
+ const branches = git('branch --list "shit/checkpoints/v1/*" "shit/*"', projectRoot)
30
21
  .split('\n')
31
22
  .map(b => b.trim().replace(/^\*?\s*/, ''))
32
23
  .filter(Boolean);
33
24
 
34
25
  for (const branch of branches) {
35
- const match = branch.match(/^shit\/([a-f0-9]+)-([a-f0-9]+)$/);
36
- if (match) {
37
- const [, baseCommit, sessionShort] = match;
38
- if (sessionShort.startsWith(checkpointId) || baseCommit.startsWith(checkpointId)) {
39
- const log = git(`log ${branch} --oneline -1`, projectRoot);
40
- const timestamp = git(`log ${branch} --format=%ci -1`, projectRoot);
26
+ const timestamp = git(`log ${branch} --format=%ci -1`, projectRoot);
27
+ const log = git(`log ${branch} --oneline -1`, projectRoot);
28
+
29
+ const checkpointMatch = branch.match(/^shit\/checkpoints\/v1\/(\d{4}-\d{2}-\d{2})-([a-f0-9]+)$/);
30
+ if (checkpointMatch) {
31
+ const branchKey = `${checkpointMatch[1]}-${checkpointMatch[2]}`;
32
+ const message = git(`log ${branch} --format=%B -1`, projectRoot);
33
+ const linkedMatch = message.match(/@ ([a-f0-9]+)/);
34
+ const baseCommit = linkedMatch ? linkedMatch[1] : null;
35
+
36
+ if (branchKey.startsWith(checkpointId) || checkpointMatch[2].startsWith(checkpointId) || (baseCommit && baseCommit.startsWith(checkpointId))) {
37
+ return {
38
+ branch,
39
+ baseCommit,
40
+ sessionShort: checkpointMatch[2],
41
+ timestamp,
42
+ message: log.split(' ').slice(1).join(' ')
43
+ };
44
+ }
45
+ }
41
46
 
47
+ const shadowMatch = branch.match(/^shit\/([a-f0-9]+)-([a-f0-9]+)$/);
48
+ if (shadowMatch) {
49
+ const [, baseCommit, sessionShort] = shadowMatch;
50
+ if (sessionShort.startsWith(checkpointId) || baseCommit.startsWith(checkpointId)) {
42
51
  return {
43
52
  branch,
44
53
  baseCommit,
@@ -116,11 +125,7 @@ function restoreSessionData(projectRoot, sessionData, newSessionId) {
116
125
  }
117
126
 
118
127
  function generateSessionId() {
119
- const now = new Date();
120
- const date = now.toISOString().split('T')[0]; // YYYY-MM-DD
121
- const uuid = Math.random().toString(36).substring(2, 15) +
122
- Math.random().toString(36).substring(2, 15);
123
- return `${date}-${uuid}`;
128
+ return randomUUID();
124
129
  }
125
130
 
126
131
  function updateSessionState(sessionDir, checkpoint) {
@@ -149,7 +154,7 @@ function updateSessionState(sessionDir, checkpoint) {
149
154
 
150
155
  export default async function resume(args) {
151
156
  try {
152
- const projectRoot = findProjectRoot();
157
+ const projectRoot = getProjectRoot();
153
158
  const checkpointId = args.find(arg => !arg.startsWith('-'));
154
159
 
155
160
  if (!checkpointId) {
@@ -164,19 +169,19 @@ export default async function resume(args) {
164
169
  const checkpoint = findCheckpoint(projectRoot, checkpointId);
165
170
  if (!checkpoint) {
166
171
  console.error(`❌ Checkpoint not found: ${checkpointId}`);
167
- console.error(' Use "shit shadow" to list available checkpoints');
172
+ console.error(' Use "shit checkpoints" to list available checkpoints');
168
173
  process.exit(1);
169
174
  }
170
175
 
171
176
  console.log(`✅ Found checkpoint: ${checkpoint.sessionShort}`);
172
177
  console.log(` Branch: ${checkpoint.branch}`);
173
- console.log(` Base commit: ${checkpoint.baseCommit}`);
178
+ console.log(` Base commit: ${checkpoint.baseCommit || 'unknown'}`);
174
179
  console.log(` Created: ${new Date(checkpoint.timestamp).toLocaleString()}`);
175
180
  console.log();
176
181
 
177
182
  // Check if we're already at the right commit
178
183
  const currentCommit = git('rev-parse HEAD', projectRoot);
179
- if (!currentCommit.startsWith(checkpoint.baseCommit)) {
184
+ if (checkpoint.baseCommit && !currentCommit.startsWith(checkpoint.baseCommit)) {
180
185
  console.log(`🔄 Checking out base commit: ${checkpoint.baseCommit}`);
181
186
  git(`checkout ${checkpoint.baseCommit}`, projectRoot);
182
187
  }