@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/README.md +139 -116
- package/bin/shit.js +17 -6
- package/lib/checkpoint.js +39 -32
- package/lib/checkpoints.js +3 -15
- package/lib/commit.js +7 -15
- package/lib/config.js +3 -1
- package/lib/disable.js +54 -18
- package/lib/doctor.js +17 -24
- package/lib/enable.js +24 -13
- package/lib/explain.js +43 -38
- package/lib/reset.js +8 -16
- package/lib/resume.js +32 -27
- package/lib/review.js +728 -0
- package/lib/rewind.js +63 -38
- package/lib/session.js +6 -0
- package/lib/status.js +44 -19
- package/lib/summarize.js +2 -13
- package/package.json +21 -5
- package/.claude/settings.json +0 -81
- package/.claude/settings.local.json +0 -20
- package/COMPARISON.md +0 -92
- package/DESIGN_PHILOSOPHY.md +0 -138
- package/QUICKSTART.md +0 -109
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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() &&
|
|
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() &&
|
|
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() &&
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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} (
|
|
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 =
|
|
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)
|
|
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 =
|
|
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 {
|
|
10
|
+
import { execFileSync } from 'child_process';
|
|
11
|
+
import { getProjectRoot, SESSION_ID_REGEX } from './config.js';
|
|
11
12
|
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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 =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
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,
|
|
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 =>
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
}
|