gent-cli 6.0.0 → 7.0.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/QUICKSTART.md +116 -85
- package/README.md +62 -3
- package/package.json +5 -3
- package/src/commands/branch.js +3 -0
- package/src/commands/checkout.js +5 -0
- package/src/commands/commit.js +65 -2
- package/src/commands/explain.js +145 -0
- package/src/commands/log.js +51 -1
- package/src/commands/merge.js +4 -0
- package/src/commands/remote.js +15 -1
- package/src/commands/reset.js +10 -0
- package/src/commands/resolve.js +280 -0
- package/src/commands/summary.js +176 -0
- package/src/commands/undo.js +115 -0
- package/src/index.js +39 -3
- package/src/utils/ai-service.js +156 -0
- package/src/utils/diff-engine.js +68 -2
- package/src/utils/journal.js +215 -0
- package/src/utils/merge-engine.js +340 -140
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Operation Journal - Safety net for history-changing commands
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Record every history-changing operation (commit, merge, reset, checkout,
|
|
8
|
+
* branch delete, pull) so it can be reversed with a single `gent undo` and
|
|
9
|
+
* re-applied with `gent redo`. Friendlier and more discoverable than
|
|
10
|
+
* `git reflog`: a human-readable list and one-command undo.
|
|
11
|
+
*
|
|
12
|
+
* WHAT IS RECORDED:
|
|
13
|
+
* Before an operation mutates `commits.json`, `recordOp()` snapshots the
|
|
14
|
+
* parts that change — the `branches` map and `currentBranch`. That snapshot
|
|
15
|
+
* plus a label/description/timestamp becomes one journal entry.
|
|
16
|
+
*
|
|
17
|
+
* UNDO SEMANTICS (intentionally non-destructive):
|
|
18
|
+
* - `gent undo` restores branch pointers + current branch to their state
|
|
19
|
+
* before the last operation.
|
|
20
|
+
* - Working files are NEVER deleted. For operations that discard file
|
|
21
|
+
* content (reset --hard, fast-forward merge, pull) the entry is flagged
|
|
22
|
+
* `restoreTree`, and undo also rewrites those files from the object store.
|
|
23
|
+
* For commit / checkout / branch-delete, undo leaves the working tree
|
|
24
|
+
* as-is — a just-committed change simply becomes uncommitted again.
|
|
25
|
+
* - `gent redo` re-applies the last undone operation. Any new history-
|
|
26
|
+
* changing operation clears the redo stack.
|
|
27
|
+
*
|
|
28
|
+
* STORAGE:
|
|
29
|
+
* .gent/journal.json → { entries: [...], redo: [...] }
|
|
30
|
+
* History is capped to the most recent MAX_ENTRIES operations.
|
|
31
|
+
*
|
|
32
|
+
* ============================================================================
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const fs = require('fs').promises;
|
|
36
|
+
const path = require('path');
|
|
37
|
+
const { readJSON, writeJSON, pathExists } = require('./fileSystem');
|
|
38
|
+
const { COMMITS_FILE, STAGING_FILE } = require('./constants');
|
|
39
|
+
const { readBlobAsString } = require('./hash-engine');
|
|
40
|
+
|
|
41
|
+
const JOURNAL_FILE = 'journal.json';
|
|
42
|
+
const MAX_ENTRIES = 100;
|
|
43
|
+
|
|
44
|
+
// ─── Persistence ────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
async function readJournal(gentPath) {
|
|
47
|
+
const p = path.join(gentPath, JOURNAL_FILE);
|
|
48
|
+
if (!await pathExists(p)) return { entries: [], redo: [] };
|
|
49
|
+
try {
|
|
50
|
+
const j = await readJSON(p);
|
|
51
|
+
return { entries: j.entries || [], redo: j.redo || [] };
|
|
52
|
+
} catch {
|
|
53
|
+
return { entries: [], redo: [] };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function writeJournal(gentPath, journal) {
|
|
58
|
+
await writeJSON(path.join(gentPath, JOURNAL_FILE), journal);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Helpers ────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function snapshotState(repository) {
|
|
64
|
+
return {
|
|
65
|
+
branches: { ...(repository.branches || {}) },
|
|
66
|
+
currentBranch: repository.currentBranch || 'main'
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeId() {
|
|
71
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Resolve a commit's tree entries (supports legacy `files` shape). */
|
|
75
|
+
function treeOf(commits, hash) {
|
|
76
|
+
if (!hash) return [];
|
|
77
|
+
const c = (commits || []).find(x => x.hash === hash);
|
|
78
|
+
if (!c) return [];
|
|
79
|
+
if (Array.isArray(c.tree)) return c.tree;
|
|
80
|
+
return (c.files || []).map(f => ({ mode: '100644', name: f.path || f.name, hash: f.hash, type: 'blob' }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Overwrite/create working files from a tree (never deletes). */
|
|
84
|
+
async function restoreFiles(gentPath, cwd, tree) {
|
|
85
|
+
for (const e of tree) {
|
|
86
|
+
try {
|
|
87
|
+
const content = await readBlobAsString(gentPath, e.hash);
|
|
88
|
+
const full = path.join(cwd, e.name);
|
|
89
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
90
|
+
await fs.writeFile(full, content, 'utf-8');
|
|
91
|
+
} catch {
|
|
92
|
+
// Blob may be missing for legacy commits — best effort.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function clearStaging(gentPath) {
|
|
98
|
+
await writeJSON(path.join(gentPath, STAGING_FILE), { entries: [], files: [], mergeState: null });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Public API ─────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Record the pre-operation state. Call this BEFORE the command writes
|
|
105
|
+
* commits.json. Journaling must never break the underlying command, so all
|
|
106
|
+
* failures are swallowed.
|
|
107
|
+
* @param {String} gentPath
|
|
108
|
+
* @param {String} op - short op label (commit|merge|reset|checkout|branch-delete|pull)
|
|
109
|
+
* @param {String} description - human-readable detail
|
|
110
|
+
* @param {Object} [meta] - e.g. { restoreTree: true }
|
|
111
|
+
*/
|
|
112
|
+
async function recordOp(gentPath, op, description, meta = {}) {
|
|
113
|
+
try {
|
|
114
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
115
|
+
const journal = await readJournal(gentPath);
|
|
116
|
+
journal.entries.push({
|
|
117
|
+
id: makeId(),
|
|
118
|
+
op,
|
|
119
|
+
description: description || '',
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
meta,
|
|
122
|
+
state: snapshotState(repository)
|
|
123
|
+
});
|
|
124
|
+
if (journal.entries.length > MAX_ENTRIES) {
|
|
125
|
+
journal.entries = journal.entries.slice(-MAX_ENTRIES);
|
|
126
|
+
}
|
|
127
|
+
journal.redo = []; // a fresh action invalidates the redo stack
|
|
128
|
+
await writeJournal(gentPath, journal);
|
|
129
|
+
} catch {
|
|
130
|
+
// Never let journaling failures surface to the user.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Return journal entries, most-recent first. */
|
|
135
|
+
async function listEntries(gentPath) {
|
|
136
|
+
const journal = await readJournal(gentPath);
|
|
137
|
+
return [...journal.entries].reverse();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Restore commits.json pointers + (optionally) working tree to `targetState`.
|
|
142
|
+
* Shared by undo and redo.
|
|
143
|
+
*/
|
|
144
|
+
async function applyState(gentPath, cwd, repository, commits, targetState, restoreTree) {
|
|
145
|
+
repository.branches = { ...targetState.branches };
|
|
146
|
+
repository.currentBranch = targetState.currentBranch;
|
|
147
|
+
await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
|
|
148
|
+
|
|
149
|
+
if (restoreTree) {
|
|
150
|
+
const head = targetState.branches[targetState.currentBranch];
|
|
151
|
+
await restoreFiles(gentPath, cwd, treeOf(commits, head));
|
|
152
|
+
}
|
|
153
|
+
await clearStaging(gentPath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Reverse the last recorded operation.
|
|
158
|
+
* @returns {Promise<{ok: Boolean, reason?: String, entry?: Object, head?: String, branch?: String}>}
|
|
159
|
+
*/
|
|
160
|
+
async function applyUndo(gentPath, cwd) {
|
|
161
|
+
const journal = await readJournal(gentPath);
|
|
162
|
+
if (journal.entries.length === 0) return { ok: false, reason: 'nothing-to-undo' };
|
|
163
|
+
|
|
164
|
+
const entry = journal.entries.pop();
|
|
165
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
166
|
+
const commits = repository.commits || [];
|
|
167
|
+
|
|
168
|
+
// Remember the post-operation state so `redo` can re-apply it.
|
|
169
|
+
const postState = snapshotState(repository);
|
|
170
|
+
journal.redo.push({ ...entry, state: postState });
|
|
171
|
+
|
|
172
|
+
await applyState(gentPath, cwd, repository, commits, entry.state, !!(entry.meta && entry.meta.restoreTree));
|
|
173
|
+
await writeJournal(gentPath, journal);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
entry,
|
|
178
|
+
branch: entry.state.currentBranch,
|
|
179
|
+
head: entry.state.branches[entry.state.currentBranch] || null
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Re-apply the last undone operation.
|
|
185
|
+
* @returns {Promise<{ok: Boolean, reason?: String, entry?: Object, head?: String, branch?: String}>}
|
|
186
|
+
*/
|
|
187
|
+
async function applyRedo(gentPath, cwd) {
|
|
188
|
+
const journal = await readJournal(gentPath);
|
|
189
|
+
if (journal.redo.length === 0) return { ok: false, reason: 'nothing-to-redo' };
|
|
190
|
+
|
|
191
|
+
const entry = journal.redo.pop();
|
|
192
|
+
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
193
|
+
const commits = repository.commits || [];
|
|
194
|
+
|
|
195
|
+
const preState = snapshotState(repository);
|
|
196
|
+
journal.entries.push({ ...entry, state: preState });
|
|
197
|
+
|
|
198
|
+
await applyState(gentPath, cwd, repository, commits, entry.state, !!(entry.meta && entry.meta.restoreTree));
|
|
199
|
+
await writeJournal(gentPath, journal);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
entry,
|
|
204
|
+
branch: entry.state.currentBranch,
|
|
205
|
+
head: entry.state.branches[entry.state.currentBranch] || null
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
recordOp,
|
|
211
|
+
listEntries,
|
|
212
|
+
applyUndo,
|
|
213
|
+
applyRedo,
|
|
214
|
+
readJournal
|
|
215
|
+
};
|