@@ -10,140 +10,147 @@ import { homedir } from "os";
1010const CHECKPOINT_BASE = join ( homedir ( ) , ".coahcode" , "checkpoints" ) ;
1111
1212export interface Checkpoint {
13- readonly id : string ;
14- readonly timestamp : number ;
15- readonly description : string ;
16- readonly workspacePath : string ;
17- readonly hash : string ;
13+ readonly id : string ;
14+ readonly timestamp : number ;
15+ readonly description : string ;
16+ readonly workspacePath : string ;
17+ readonly hash : string ;
1818}
1919
2020async function runGit ( args : string [ ] , cwd : string ) : Promise < string > {
21- return new Promise ( ( resolve , reject ) => {
22- const proc = spawn ( "git" , args , { cwd, stdio : [ "ignore" , "pipe" , "pipe" ] } ) ;
23- let stdout = "" ;
24- let stderr = "" ;
25- proc . stdout . on ( "data" , ( d ) => { stdout += d . toString ( ) ; } ) ;
26- proc . stderr . on ( "data" , ( d ) => { stderr += d . toString ( ) ; } ) ;
27- proc . on ( "close" , ( code ) => {
28- if ( code === 0 ) resolve ( stdout . trim ( ) ) ;
29- else reject ( new Error ( `git ${ args . join ( " " ) } failed: ${ stderr } ` ) ) ;
30- } ) ;
31- } ) ;
21+ return new Promise ( ( resolve , reject ) => {
22+ const proc = spawn ( "git" , args , { cwd, stdio : [ "ignore" , "pipe" , "pipe" ] } ) ;
23+ let stdout = "" ;
24+ let stderr = "" ;
25+ proc . stdout . on ( "data" , ( d ) => {
26+ stdout += d . toString ( ) ;
27+ } ) ;
28+ proc . stderr . on ( "data" , ( d ) => {
29+ stderr += d . toString ( ) ;
30+ } ) ;
31+ proc . on ( "close" , ( code ) => {
32+ if ( code === 0 ) resolve ( stdout . trim ( ) ) ;
33+ else reject ( new Error ( `git ${ args . join ( " " ) } failed: ${ stderr } ` ) ) ;
34+ } ) ;
35+ } ) ;
3236}
3337
3438export class CheckpointManager {
35- private readonly checkpointDir : string ;
36- private initialized = false ;
37-
38- constructor ( private readonly workspacePath : string ) {
39- // Hash workspace path for unique checkpoint dir
40- const pathHash = Buffer . from ( workspacePath ) . toString ( "base64url" ) . slice ( 0 , 20 ) ;
41- this . checkpointDir = join ( CHECKPOINT_BASE , pathHash ) ;
42- }
43-
44- private async ensureInit ( ) : Promise < void > {
45- if ( this . initialized ) return ;
46-
47- await fs . mkdir ( this . checkpointDir , { recursive : true } ) ;
48-
49- // Check if already a git repo
50- try {
51- await runGit ( [ "rev-parse" , "--git-dir" ] , this . checkpointDir ) ;
52- } catch {
53- // Initialize shadow git repo
54- await runGit ( [ "init" ] , this . checkpointDir ) ;
55- await runGit ( [ "config" , "user.name" , "CoahCode Checkpoints" ] , this . checkpointDir ) ;
56- await runGit ( [ "config" , "user.email" , "checkpoints@coahcode.local" ] , this . checkpointDir ) ;
57- }
58-
59- this . initialized = true ;
60- }
61-
62- async createCheckpoint ( description : string , files : readonly string [ ] ) : Promise < Checkpoint > {
63- await this . ensureInit ( ) ;
64-
65- // Copy files to checkpoint dir
66- for ( const file of files ) {
67- try {
68- const content = await fs . readFile ( file , "utf-8" ) ;
69- const relative = file . startsWith ( this . workspacePath )
70- ? file . slice ( this . workspacePath . length + 1 )
71- : file ;
72-
73- const destPath = join ( this . checkpointDir , relative ) ;
74- const destDir = destPath . substring ( 0 , destPath . lastIndexOf ( "/" ) ) ;
75- await fs . mkdir ( destDir , { recursive : true } ) ;
76- await fs . writeFile ( destPath , content , "utf-8" ) ;
77- } catch {
78- // File might not exist yet (new file creation)
79- }
80- }
81-
82- // Commit
83- try {
84- await runGit ( [ "add" , "-A" ] , this . checkpointDir ) ;
85- await runGit ( [ "commit" , "-m" , description , "--allow-empty" ] , this . checkpointDir ) ;
86- } catch {
87- // Nothing to commit
88- }
89-
90- const hash = await runGit ( [ "rev-parse" , "HEAD" ] , this . checkpointDir ) . catch ( ( ) => "unknown" ) ;
91-
92- return {
93- id : `cp_${ Date . now ( ) } ` ,
94- timestamp : Date . now ( ) ,
95- description,
96- workspacePath : this . workspacePath ,
97- hash,
98- } ;
99- }
100-
101- async listCheckpoints ( limit = 20 ) : Promise < readonly Checkpoint [ ] > {
102- await this . ensureInit ( ) ;
103-
104- try {
105- const log = await runGit (
106- [ "log" , `--max-count=${ limit } ` , "--format=%H|%at|%s" ] ,
107- this . checkpointDir ,
108- ) ;
109-
110- return log . split ( "\n" ) . filter ( Boolean ) . map ( ( line ) => {
111- const [ hash = "" , timestamp = "0" , ...descParts ] = line . split ( "|" ) ;
112- return {
113- id : `cp_${ timestamp } ` ,
114- timestamp : parseInt ( timestamp , 10 ) * 1000 ,
115- description : descParts . join ( "|" ) ,
116- workspacePath : this . workspacePath ,
117- hash,
118- } ;
119- } ) ;
120- } catch {
121- return [ ] ;
122- }
123- }
124-
125- async rollback ( hash : string ) : Promise < { restoredFiles : readonly string [ ] } > {
126- await this . ensureInit ( ) ;
127-
128- // Get list of files at that commit
129- const files = await runGit ( [ "ls-tree" , "-r" , "--name-only" , hash ] , this . checkpointDir ) ;
130- const fileList = files . split ( "\n" ) . filter ( Boolean ) ;
131-
132- const restoredFiles : string [ ] = [ ] ;
133-
134- for ( const relative of fileList ) {
135- try {
136- const content = await runGit ( [ "show" , `${ hash } :${ relative } ` ] , this . checkpointDir ) ;
137- const destPath = join ( this . workspacePath , relative ) ;
138- const destDir = destPath . substring ( 0 , destPath . lastIndexOf ( "/" ) ) ;
139- await fs . mkdir ( destDir , { recursive : true } ) ;
140- await fs . writeFile ( destPath , content , "utf-8" ) ;
141- restoredFiles . push ( destPath ) ;
142- } catch {
143- // File might have issues
144- }
145- }
146-
147- return { restoredFiles } ;
148- }
39+ private readonly checkpointDir : string ;
40+ private initialized = false ;
41+
42+ constructor ( private readonly workspacePath : string ) {
43+ // Hash workspace path for unique checkpoint dir
44+ const pathHash = Buffer . from ( workspacePath ) . toString ( "base64url" ) . slice ( 0 , 20 ) ;
45+ this . checkpointDir = join ( CHECKPOINT_BASE , pathHash ) ;
46+ }
47+
48+ private async ensureInit ( ) : Promise < void > {
49+ if ( this . initialized ) return ;
50+
51+ await fs . mkdir ( this . checkpointDir , { recursive : true } ) ;
52+
53+ // Check if already a git repo
54+ try {
55+ await runGit ( [ "rev-parse" , "--git-dir" ] , this . checkpointDir ) ;
56+ } catch {
57+ // Initialize shadow git repo
58+ await runGit ( [ "init" ] , this . checkpointDir ) ;
59+ await runGit ( [ "config" , "user.name" , "CoahCode Checkpoints" ] , this . checkpointDir ) ;
60+ await runGit ( [ "config" , "user.email" , "checkpoints@coahcode.local" ] , this . checkpointDir ) ;
61+ }
62+
63+ this . initialized = true ;
64+ }
65+
66+ async createCheckpoint ( description : string , files : readonly string [ ] ) : Promise < Checkpoint > {
67+ await this . ensureInit ( ) ;
68+
69+ // Copy files to checkpoint dir
70+ for ( const file of files ) {
71+ try {
72+ const content = await fs . readFile ( file , "utf-8" ) ;
73+ const relative = file . startsWith ( this . workspacePath )
74+ ? file . slice ( this . workspacePath . length + 1 )
75+ : file ;
76+
77+ const destPath = join ( this . checkpointDir , relative ) ;
78+ const destDir = destPath . substring ( 0 , destPath . lastIndexOf ( "/" ) ) ;
79+ await fs . mkdir ( destDir , { recursive : true } ) ;
80+ await fs . writeFile ( destPath , content , "utf-8" ) ;
81+ } catch {
82+ // File might not exist yet (new file creation)
83+ }
84+ }
85+
86+ // Commit
87+ try {
88+ await runGit ( [ "add" , "-A" ] , this . checkpointDir ) ;
89+ await runGit ( [ "commit" , "-m" , description , "--allow-empty" ] , this . checkpointDir ) ;
90+ } catch {
91+ // Nothing to commit
92+ }
93+
94+ const hash = await runGit ( [ "rev-parse" , "HEAD" ] , this . checkpointDir ) . catch ( ( ) => "unknown" ) ;
95+
96+ return {
97+ id : `cp_${ Date . now ( ) } ` ,
98+ timestamp : Date . now ( ) ,
99+ description,
100+ workspacePath : this . workspacePath ,
101+ hash,
102+ } ;
103+ }
104+
105+ async listCheckpoints ( limit = 20 ) : Promise < readonly Checkpoint [ ] > {
106+ await this . ensureInit ( ) ;
107+
108+ try {
109+ const log = await runGit (
110+ [ "log" , `--max-count=${ limit } ` , "--format=%H|%at|%s" ] ,
111+ this . checkpointDir ,
112+ ) ;
113+
114+ return log
115+ . split ( "\n" )
116+ . filter ( Boolean )
117+ . map ( ( line ) => {
118+ const [ hash = "" , timestamp = "0" , ...descParts ] = line . split ( "|" ) ;
119+ return {
120+ id : `cp_${ timestamp } ` ,
121+ timestamp : parseInt ( timestamp , 10 ) * 1000 ,
122+ description : descParts . join ( "|" ) ,
123+ workspacePath : this . workspacePath ,
124+ hash,
125+ } ;
126+ } ) ;
127+ } catch {
128+ return [ ] ;
129+ }
130+ }
131+
132+ async rollback ( hash : string ) : Promise < { restoredFiles : readonly string [ ] } > {
133+ await this . ensureInit ( ) ;
134+
135+ // Get list of files at that commit
136+ const files = await runGit ( [ "ls-tree" , "-r" , "--name-only" , hash ] , this . checkpointDir ) ;
137+ const fileList = files . split ( "\n" ) . filter ( Boolean ) ;
138+
139+ const restoredFiles : string [ ] = [ ] ;
140+
141+ for ( const relative of fileList ) {
142+ try {
143+ const content = await runGit ( [ "show" , `${ hash } :${ relative } ` ] , this . checkpointDir ) ;
144+ const destPath = join ( this . workspacePath , relative ) ;
145+ const destDir = destPath . substring ( 0 , destPath . lastIndexOf ( "/" ) ) ;
146+ await fs . mkdir ( destDir , { recursive : true } ) ;
147+ await fs . writeFile ( destPath , content , "utf-8" ) ;
148+ restoredFiles . push ( destPath ) ;
149+ } catch {
150+ // File might have issues
151+ }
152+ }
153+
154+ return { restoredFiles } ;
155+ }
149156}
0 commit comments