gent-cli 23.0.0 → 25.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 +6 -5
- package/README.md +7 -6
- package/package.json +3 -3
- package/src/commands/ai.js +15 -30
- package/src/commands/ask.js +4 -1
- package/src/commands/canonical.js +116 -7
- package/src/commands/changelog.js +2 -0
- package/src/commands/chat.js +80 -0
- package/src/commands/commit.js +1 -0
- package/src/commands/config.js +1 -16
- package/src/commands/docs.js +2 -0
- package/src/commands/doctor.js +12 -12
- package/src/commands/explain.js +3 -3
- package/src/commands/merge.js +25 -3
- package/src/commands/repos.js +114 -9
- package/src/commands/resolve.js +68 -23
- package/src/commands/review.js +4 -3
- package/src/commands/setup.js +2 -52
- package/src/commands/summary.js +5 -1
- package/src/index.js +22 -11
- package/src/utils/ai-service.js +86 -163
- package/src/utils/user-config.js +3 -33
package/src/commands/merge.js
CHANGED
|
@@ -15,6 +15,8 @@ const { findMergeBase, mergeTreeEntries, autoMerge } = require('../utils/merge-e
|
|
|
15
15
|
const { storeTree, readBlob, hashBlob } = require('../utils/hash-engine');
|
|
16
16
|
const pet = require('./pet');
|
|
17
17
|
const journal = require('../utils/journal');
|
|
18
|
+
const ai = require('../utils/ai-service');
|
|
19
|
+
const reviewCommand = require('./review');
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Merge a branch into the current branch
|
|
@@ -25,6 +27,11 @@ async function merge(sourceBranch, options) {
|
|
|
25
27
|
const spinner = ora(`Merging '${sourceBranch}'...`).start();
|
|
26
28
|
|
|
27
29
|
try {
|
|
30
|
+
if (options.ai) {
|
|
31
|
+
await ai.prime();
|
|
32
|
+
if (!ai.isEnabled()) throw new Error(ai.disabledHint());
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
const gentPath = await getGentPath();
|
|
29
36
|
const cwd = path.dirname(gentPath);
|
|
30
37
|
const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
|
|
@@ -95,6 +102,10 @@ async function merge(sourceBranch, options) {
|
|
|
95
102
|
|
|
96
103
|
spinner.succeed(chalk.green(`Fast-forward merge: ${currentBranch} → ${theirsHash.substring(0, 7)}`));
|
|
97
104
|
await pet.celebrate('merge');
|
|
105
|
+
if (options.ai) {
|
|
106
|
+
console.log(chalk.bold.cyan('\nAI review of the completed merge'));
|
|
107
|
+
await reviewCommand(theirsHash, { head: true });
|
|
108
|
+
}
|
|
98
109
|
return;
|
|
99
110
|
}
|
|
100
111
|
|
|
@@ -134,8 +145,10 @@ async function merge(sourceBranch, options) {
|
|
|
134
145
|
}
|
|
135
146
|
}
|
|
136
147
|
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
if (!options.ai) {
|
|
149
|
+
console.log(chalk.yellow(`\nConflict markers: <<<<<<< HEAD / ======= / >>>>>>> ${sourceBranch}`));
|
|
150
|
+
console.log(chalk.cyan('Resolve conflicts, then run "gent resolve"'));
|
|
151
|
+
}
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
// Store merged tree
|
|
@@ -199,6 +212,10 @@ async function merge(sourceBranch, options) {
|
|
|
199
212
|
console.log(chalk.gray(` Ours: ${oursHash.substring(0, 7)} Theirs: ${theirsHash.substring(0, 7)}`));
|
|
200
213
|
console.log(chalk.green(` ${autoResolved} file(s) merged automatically`));
|
|
201
214
|
await pet.celebrate('merge');
|
|
215
|
+
if (options.ai) {
|
|
216
|
+
console.log(chalk.bold.cyan('\nAI review of the completed merge'));
|
|
217
|
+
await reviewCommand(mergeCommit.hash, { head: true });
|
|
218
|
+
}
|
|
202
219
|
} else {
|
|
203
220
|
// Stage the merge state for manual resolution
|
|
204
221
|
const staging = await readJSON(path.join(gentPath, STAGING_FILE));
|
|
@@ -212,7 +229,12 @@ async function merge(sourceBranch, options) {
|
|
|
212
229
|
conflicts: mergeResult.conflicts
|
|
213
230
|
};
|
|
214
231
|
await writeJSON(path.join(gentPath, STAGING_FILE), staging);
|
|
215
|
-
|
|
232
|
+
if (options.ai) {
|
|
233
|
+
process.exitCode = 0;
|
|
234
|
+
await require('./resolve')({ ai: true });
|
|
235
|
+
} else {
|
|
236
|
+
process.exitCode = 1;
|
|
237
|
+
}
|
|
216
238
|
}
|
|
217
239
|
|
|
218
240
|
} catch (error) {
|
package/src/commands/repos.js
CHANGED
|
@@ -5,25 +5,50 @@
|
|
|
5
5
|
const chalk = require('chalk');
|
|
6
6
|
const ora = require('ora');
|
|
7
7
|
const path = require('path');
|
|
8
|
-
const
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
const { API_ENDPOINTS, GENT_DIR, CONFIG_FILE } = require('../utils/constants');
|
|
9
10
|
const apiClient = require('../utils/api-client');
|
|
10
11
|
const authStorage = require('../utils/auth-storage');
|
|
12
|
+
const { pathExists, readJSON, writeJSON } = require('../utils/fileSystem');
|
|
13
|
+
const interactive = require('../utils/interactive');
|
|
14
|
+
const initCommand = require('./init');
|
|
15
|
+
const repository = require('../utils/repository');
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* List or create remote repositories
|
|
14
19
|
* @param {Object} options
|
|
15
20
|
*/
|
|
16
|
-
async function repos(options) {
|
|
21
|
+
async function repos(extraNames, options) {
|
|
17
22
|
try {
|
|
23
|
+
options = options || {};
|
|
24
|
+
extraNames = extraNames || [];
|
|
25
|
+
let local = null;
|
|
26
|
+
|
|
27
|
+
if (options.create) {
|
|
28
|
+
const validation = validateRepositoryName(options.create, extraNames);
|
|
29
|
+
if (!validation.valid) {
|
|
30
|
+
console.error(chalk.red(validation.message));
|
|
31
|
+
console.log(chalk.yellow(`Use "gent repos --create ${validation.suggestion}" instead.`));
|
|
32
|
+
console.log(chalk.gray('No repository was created.'));
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
local = await prepareLocalRepository(options);
|
|
37
|
+
if (!local) return;
|
|
38
|
+
} else if (extraNames.length) {
|
|
39
|
+
throw new Error('unexpected repository name; use --create <name>');
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
const isAuth = await authStorage.isAuthenticated();
|
|
19
43
|
if (!isAuth) {
|
|
20
44
|
console.error(chalk.red('Not authenticated'));
|
|
21
45
|
console.log(chalk.yellow('Run "gent login" first'));
|
|
46
|
+
process.exitCode = 1;
|
|
22
47
|
return;
|
|
23
48
|
}
|
|
24
49
|
|
|
25
50
|
if (options.create) {
|
|
26
|
-
await createRepo(options);
|
|
51
|
+
await createRepo(options, local);
|
|
27
52
|
return;
|
|
28
53
|
}
|
|
29
54
|
|
|
@@ -73,7 +98,7 @@ async function listRepos() {
|
|
|
73
98
|
/**
|
|
74
99
|
* Create a new remote repository
|
|
75
100
|
*/
|
|
76
|
-
async function createRepo(options) {
|
|
101
|
+
async function createRepo(options, local) {
|
|
77
102
|
const name = options.create;
|
|
78
103
|
if (typeof name !== 'string' || !name) {
|
|
79
104
|
console.error(chalk.red('Usage: gent repos --create <name>'));
|
|
@@ -91,13 +116,93 @@ async function createRepo(options) {
|
|
|
91
116
|
if (options.defaultBranch) {
|
|
92
117
|
payload.default_branch = options.defaultBranch;
|
|
93
118
|
}
|
|
119
|
+
if (local.kind === 'canonical') payload.object_format = 'sha256';
|
|
94
120
|
|
|
95
|
-
|
|
96
|
-
|
|
121
|
+
let data;
|
|
122
|
+
try {
|
|
123
|
+
data = await apiClient.post(API_ENDPOINTS.REPOS_CREATE, payload);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
spinner.stop();
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
const remoteRepo = data.repository || data;
|
|
129
|
+
const remoteUrl = await linkOrigin(local, remoteRepo);
|
|
130
|
+
|
|
131
|
+
spinner.succeed(chalk.green(`Created repository '${remoteRepo.name}'`));
|
|
132
|
+
console.log(chalk.gray(` URL: /api/repos/${remoteRepo.owner_id}/${remoteRepo.name}`));
|
|
133
|
+
console.log(chalk.green(` Linked local repository to '${remoteUrl}' as origin`));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validateRepositoryName(name, extraNames = []) {
|
|
137
|
+
const words = [name, ...extraNames].filter(value => typeof value === 'string' && value.trim());
|
|
138
|
+
const combined = words.join(' ').trim();
|
|
139
|
+
const suggestion = combined
|
|
140
|
+
.toLowerCase()
|
|
141
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
142
|
+
.replace(/-+/g, '-')
|
|
143
|
+
.replace(/^[-.]+|[-.]+$/g, '') || 'repository-name';
|
|
144
|
+
const valid = words.length === 1 && /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(name);
|
|
145
|
+
return {
|
|
146
|
+
valid,
|
|
147
|
+
suggestion,
|
|
148
|
+
message: `Invalid repository name '${combined || String(name || '')}'. Repository names cannot contain spaces or unsupported characters.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function prepareLocalRepository(options) {
|
|
153
|
+
const gentPath = path.join(process.cwd(), GENT_DIR);
|
|
154
|
+
if (!await pathExists(gentPath)) {
|
|
155
|
+
if (!options.yes) {
|
|
156
|
+
if (!interactive.isInteractive()) {
|
|
157
|
+
console.error(chalk.red('No local Gent repository exists in this folder.'));
|
|
158
|
+
console.log(chalk.yellow('Run "gent init" first, or retry with --yes to initialize and link it automatically.'));
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const { initialize } = await inquirer.prompt([{
|
|
163
|
+
type: 'confirm',
|
|
164
|
+
name: 'initialize',
|
|
165
|
+
message: 'No local Gent repository exists in this folder. Create it before creating and linking the remote?',
|
|
166
|
+
default: true,
|
|
167
|
+
}]);
|
|
168
|
+
if (!initialize) {
|
|
169
|
+
console.log(chalk.yellow('Cancelled. No local or remote repository was created.'));
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
await initCommand({ yes: true });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const legacyConfig = path.join(gentPath, CONFIG_FILE);
|
|
177
|
+
if (await pathExists(legacyConfig)) {
|
|
178
|
+
const config = await readJSON(legacyConfig);
|
|
179
|
+
if (config.remotes?.origin) throw new Error("remote 'origin' already exists; no repository was created");
|
|
180
|
+
return { kind: 'legacy', config, configPath: legacyConfig };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const canonical = await repository.open(process.cwd());
|
|
184
|
+
if (canonical.config.get('remote.origin.url')) throw new Error("remote 'origin' already exists; no repository was created");
|
|
185
|
+
return { kind: 'canonical', repo: canonical };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function linkOrigin(local, remoteRepo) {
|
|
189
|
+
if (local.kind === 'legacy') {
|
|
190
|
+
const url = `/api/repos/${remoteRepo.owner_id}/${remoteRepo.name}`;
|
|
191
|
+
local.config.remotes = local.config.remotes || {};
|
|
192
|
+
local.config.remotes.origin = { url };
|
|
193
|
+
await writeJSON(local.configPath, local.config);
|
|
194
|
+
return url;
|
|
195
|
+
}
|
|
97
196
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
197
|
+
const base = (await apiClient.resolveBaseUrl()).replace(/\/$/, '');
|
|
198
|
+
const owner = remoteRepo.owner_username || remoteRepo.owner_id;
|
|
199
|
+
const url = `${base}/${encodeURIComponent(String(owner))}/${encodeURIComponent(remoteRepo.name)}.git`;
|
|
200
|
+
local.repo.localConfig.set('remote.origin.url', url);
|
|
201
|
+
local.repo.localConfig.set('remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*');
|
|
202
|
+
await local.repo.localConfig.save();
|
|
203
|
+
return url;
|
|
101
204
|
}
|
|
102
205
|
|
|
103
206
|
module.exports = repos;
|
|
207
|
+
module.exports.validateRepositoryName = validateRepositoryName;
|
|
208
|
+
module.exports.prepareLocalRepository = prepareLocalRepository;
|
package/src/commands/resolve.js
CHANGED
|
@@ -31,9 +31,17 @@ const { generateCommitHash } = require('../utils/helpers');
|
|
|
31
31
|
const authStorage = require('../utils/auth-storage');
|
|
32
32
|
const journal = require('../utils/journal');
|
|
33
33
|
const ai = require('../utils/ai-service');
|
|
34
|
+
const reviewCommand = require('./review');
|
|
34
35
|
|
|
35
|
-
async function resolve() {
|
|
36
|
+
async function resolve(options = {}) {
|
|
36
37
|
try {
|
|
38
|
+
await ai.prime();
|
|
39
|
+
if (options.ai && !ai.isEnabled()) {
|
|
40
|
+
console.error(chalk.red(ai.disabledHint()));
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
const gentPath = await getGentPath();
|
|
38
46
|
const cwd = process.cwd();
|
|
39
47
|
|
|
@@ -57,6 +65,19 @@ async function resolve() {
|
|
|
57
65
|
return;
|
|
58
66
|
}
|
|
59
67
|
|
|
68
|
+
if (!options.ai && ai.isEnabled() && process.stdin.isTTY && process.stdout.isTTY) {
|
|
69
|
+
const { mode } = await inquirer.prompt([{
|
|
70
|
+
type: 'list',
|
|
71
|
+
name: 'mode',
|
|
72
|
+
message: 'How should Gent resolve this merge?',
|
|
73
|
+
choices: [
|
|
74
|
+
{ name: 'Merge with AI (fast) — resolve, commit, then review', value: 'ai' },
|
|
75
|
+
{ name: 'Resolve manually — choose each conflict', value: 'manual' },
|
|
76
|
+
],
|
|
77
|
+
}]);
|
|
78
|
+
options.ai = mode === 'ai';
|
|
79
|
+
}
|
|
80
|
+
|
|
60
81
|
console.log(chalk.bold.cyan(`\nResolving merge of '${mergeState.sourceBranch}' — ${markerFiles.length} file(s)\n`));
|
|
61
82
|
|
|
62
83
|
// Working copy of merged tree entries (we patch hashes as files resolve).
|
|
@@ -92,7 +113,7 @@ async function resolve() {
|
|
|
92
113
|
continue;
|
|
93
114
|
}
|
|
94
115
|
idx++;
|
|
95
|
-
const resolvedLines = await resolveHunk(seg, file, idx, conflictCount);
|
|
116
|
+
const resolvedLines = await resolveHunk(seg, file, idx, conflictCount, options);
|
|
96
117
|
if (resolvedLines === null) { aborted = true; break; }
|
|
97
118
|
out.push(...resolvedLines);
|
|
98
119
|
}
|
|
@@ -119,23 +140,28 @@ async function resolve() {
|
|
|
119
140
|
|
|
120
141
|
if (unresolvedFiles > 0) {
|
|
121
142
|
console.log(chalk.yellow(`\n${unresolvedFiles} file(s) still have conflicts. Re-run "gent resolve" when ready.`));
|
|
143
|
+
process.exitCode = 1;
|
|
122
144
|
return;
|
|
123
145
|
}
|
|
124
146
|
|
|
125
147
|
// All conflicts resolved — offer to finalize the merge commit.
|
|
126
|
-
const
|
|
148
|
+
const finalize = options.ai || (await inquirer.prompt([{
|
|
127
149
|
type: 'confirm',
|
|
128
150
|
name: 'finalize',
|
|
129
151
|
message: 'All conflicts resolved. Create the merge commit now?',
|
|
130
152
|
default: true
|
|
131
|
-
}]);
|
|
153
|
+
}])).finalize;
|
|
132
154
|
|
|
133
155
|
if (!finalize) {
|
|
134
156
|
console.log(chalk.cyan('Resolved files staged. Run "gent commit" when ready.'));
|
|
135
157
|
return;
|
|
136
158
|
}
|
|
137
159
|
|
|
138
|
-
await finalizeMerge(gentPath, staging, mergeState, entriesByName);
|
|
160
|
+
const mergeCommit = await finalizeMerge(gentPath, staging, mergeState, entriesByName);
|
|
161
|
+
if (options.ai) {
|
|
162
|
+
console.log(chalk.bold.cyan('\nAI review of the completed merge'));
|
|
163
|
+
await reviewCommand(mergeCommit.hash, { head: true });
|
|
164
|
+
}
|
|
139
165
|
} catch (error) {
|
|
140
166
|
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
141
167
|
console.error(chalk.red('Error: Not a gent repository'));
|
|
@@ -151,7 +177,7 @@ async function resolve() {
|
|
|
151
177
|
* Prompt for one conflict hunk. Returns the chosen lines, or null to abort
|
|
152
178
|
* (leave the rest of the file as-is with markers).
|
|
153
179
|
*/
|
|
154
|
-
async function resolveHunk(seg, file, idx, total) {
|
|
180
|
+
async function resolveHunk(seg, file, idx, total, options = {}) {
|
|
155
181
|
console.log(chalk.gray(` Conflict ${idx}/${total}:`));
|
|
156
182
|
console.log(chalk.green(' <<< ours'));
|
|
157
183
|
seg.ours.forEach(l => console.log(chalk.green(` ${l}`)));
|
|
@@ -169,6 +195,12 @@ async function resolveHunk(seg, file, idx, total) {
|
|
|
169
195
|
}
|
|
170
196
|
choices.push({ name: 'Skip the rest of this file', value: 'skip' });
|
|
171
197
|
|
|
198
|
+
if (options.ai) {
|
|
199
|
+
const suggestion = await askAiForHunk(seg, file, true);
|
|
200
|
+
if (suggestion !== null) return suggestion;
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
172
204
|
const { choice } = await inquirer.prompt([{
|
|
173
205
|
type: 'list',
|
|
174
206
|
name: 'choice',
|
|
@@ -191,28 +223,40 @@ async function resolveHunk(seg, file, idx, total) {
|
|
|
191
223
|
return text.replace(/\n$/, '').split('\n');
|
|
192
224
|
}
|
|
193
225
|
case 'ai': {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
theirs: seg.theirs.join('\n'),
|
|
198
|
-
fileName: file
|
|
199
|
-
});
|
|
200
|
-
console.log(chalk.cyan(' AI suggestion:'));
|
|
201
|
-
suggestion.split('\n').forEach(l => console.log(chalk.cyan(` ${l}`)));
|
|
202
|
-
const { accept } = await inquirer.prompt([{
|
|
203
|
-
type: 'confirm', name: 'accept', message: 'Use this suggestion?', default: true
|
|
204
|
-
}]);
|
|
205
|
-
if (accept) return suggestion.split('\n');
|
|
206
|
-
return resolveHunk(seg, file, idx, total); // re-ask
|
|
207
|
-
} catch (err) {
|
|
208
|
-
console.log(chalk.yellow(` AI failed (${err.message}); choose another option.`));
|
|
209
|
-
return resolveHunk(seg, file, idx, total);
|
|
210
|
-
}
|
|
226
|
+
const suggestion = await askAiForHunk(seg, file);
|
|
227
|
+
if (suggestion !== null) return suggestion;
|
|
228
|
+
return resolveHunk(seg, file, idx, total, { ai: false });
|
|
211
229
|
}
|
|
212
230
|
default: return seg.ours;
|
|
213
231
|
}
|
|
214
232
|
}
|
|
215
233
|
|
|
234
|
+
async function askAiForHunk(seg, file, autoAccept = false) {
|
|
235
|
+
try {
|
|
236
|
+
const suggestion = await ai.resolveConflictHunk({
|
|
237
|
+
ours: seg.ours.join('\n'),
|
|
238
|
+
theirs: seg.theirs.join('\n'),
|
|
239
|
+
fileName: file
|
|
240
|
+
});
|
|
241
|
+
if (autoAccept) {
|
|
242
|
+
console.log(chalk.green(` ✓ AI resolved ${file}`));
|
|
243
|
+
return suggestion.split('\n');
|
|
244
|
+
}
|
|
245
|
+
console.log(chalk.cyan(' AI suggestion (review before accepting):'));
|
|
246
|
+
suggestion.split('\n').forEach(line => console.log(chalk.cyan(` ${line}`)));
|
|
247
|
+
const { accept } = await inquirer.prompt([{
|
|
248
|
+
type: 'confirm',
|
|
249
|
+
name: 'accept',
|
|
250
|
+
message: 'Use this AI suggestion?',
|
|
251
|
+
default: false,
|
|
252
|
+
}]);
|
|
253
|
+
return accept ? suggestion.split('\n') : null;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.log(chalk.yellow(` AI failed (${error.message}); no file was changed.`));
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
216
260
|
/** Store the resolved file as a blob, patch the tree entry, and stage it. */
|
|
217
261
|
async function stageResolved(gentPath, staging, entriesByName, file, content) {
|
|
218
262
|
const hash = await storeBlob(gentPath, content);
|
|
@@ -276,6 +320,7 @@ async function finalizeMerge(gentPath, staging, mergeState, entriesByName) {
|
|
|
276
320
|
await writeJSON(path.join(gentPath, STAGING_FILE), staging);
|
|
277
321
|
|
|
278
322
|
console.log(chalk.green(`\n✓ Merge committed — ${mergeCommit.hash.substring(0, 7)}`));
|
|
323
|
+
return mergeCommit;
|
|
279
324
|
}
|
|
280
325
|
|
|
281
326
|
module.exports = resolve;
|
package/src/commands/review.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* gent review <ref> → review diff for that commit
|
|
8
8
|
*
|
|
9
9
|
* Output: prioritized bug/risk list followed by smaller polish suggestions.
|
|
10
|
-
* Without
|
|
10
|
+
* Without local AI, prints the raw diff so the command still has value.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const path = require('path');
|
|
@@ -73,6 +73,7 @@ async function review(refArg, options = {}) {
|
|
|
73
73
|
|
|
74
74
|
console.log(chalk.bold.cyan(`\n${title}\n`));
|
|
75
75
|
|
|
76
|
+
await ai.prime();
|
|
76
77
|
if (!ai.isEnabled()) {
|
|
77
78
|
console.log(trimmed);
|
|
78
79
|
console.log(chalk.gray(`\n${ai.disabledHint()}`));
|
|
@@ -82,6 +83,7 @@ async function review(refArg, options = {}) {
|
|
|
82
83
|
const spinner = ora(`Reviewing with ${ai.getModel()}...`).start();
|
|
83
84
|
try {
|
|
84
85
|
const out = await ai.complete({
|
|
86
|
+
profile: 'review',
|
|
85
87
|
system:
|
|
86
88
|
'You are a senior code reviewer. Given a unified diff, list concrete ' +
|
|
87
89
|
'issues you would block on, then smaller suggestions. Format:\n' +
|
|
@@ -90,8 +92,7 @@ async function review(refArg, options = {}) {
|
|
|
90
92
|
'🟢 Looks good\n - one-line positive note\n' +
|
|
91
93
|
'Be specific. If nothing is wrong, say so plainly.',
|
|
92
94
|
prompt: `Review this diff:\n\n${trimmed}`,
|
|
93
|
-
maxTokens:
|
|
94
|
-
thinking: true,
|
|
95
|
+
maxTokens: 800,
|
|
95
96
|
});
|
|
96
97
|
spinner.stop();
|
|
97
98
|
console.log(out + '\n');
|
package/src/commands/setup.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* gent setup
|
|
5
5
|
*
|
|
6
|
-
* Walks the user through: backend URL → login/register →
|
|
6
|
+
* Walks the user through: backend URL → login/register → identity.
|
|
7
7
|
* Each step is skippable; nothing is required.
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -15,7 +15,6 @@ const axios = require('axios');
|
|
|
15
15
|
const userConfig = require('../utils/user-config');
|
|
16
16
|
const authStorage = require('../utils/auth-storage');
|
|
17
17
|
const authService = require('../services/auth-service');
|
|
18
|
-
const ai = require('../utils/ai-service');
|
|
19
18
|
|
|
20
19
|
async function setup() {
|
|
21
20
|
console.log(boxen(
|
|
@@ -26,7 +25,6 @@ async function setup() {
|
|
|
26
25
|
|
|
27
26
|
await stepBackend();
|
|
28
27
|
await stepAuth();
|
|
29
|
-
await stepAiKey();
|
|
30
28
|
await stepIdentity();
|
|
31
29
|
|
|
32
30
|
console.log(chalk.green('\n✓ Setup complete!'));
|
|
@@ -125,56 +123,8 @@ async function stepAuth() {
|
|
|
125
123
|
}
|
|
126
124
|
}
|
|
127
125
|
|
|
128
|
-
async function stepAiKey() {
|
|
129
|
-
console.log(chalk.bold('\n3. AI features (optional)'));
|
|
130
|
-
const existing = await ai.resolveKey();
|
|
131
|
-
if (existing.value) {
|
|
132
|
-
console.log(chalk.gray(` Key already configured [${existing.source}] — skipping.`));
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
console.log(chalk.gray(' Gent uses Anthropic Claude for commit-message suggestions,'));
|
|
137
|
-
console.log(chalk.gray(' diff explanations, AI conflict resolution, code review, and more.'));
|
|
138
|
-
console.log(chalk.gray(' Get a key at: https://console.anthropic.com/settings/keys'));
|
|
139
|
-
|
|
140
|
-
const { provide } = await inquirer.prompt([{
|
|
141
|
-
type: 'confirm',
|
|
142
|
-
name: 'provide',
|
|
143
|
-
message: 'Add an Anthropic API key now?',
|
|
144
|
-
default: true,
|
|
145
|
-
}]);
|
|
146
|
-
if (!provide) return;
|
|
147
|
-
|
|
148
|
-
const { key } = await inquirer.prompt([{
|
|
149
|
-
type: 'password',
|
|
150
|
-
name: 'key',
|
|
151
|
-
message: 'Anthropic API key:',
|
|
152
|
-
mask: '*',
|
|
153
|
-
validate: (v) => v.length > 0 || 'Cannot be empty',
|
|
154
|
-
}]);
|
|
155
|
-
|
|
156
|
-
await userConfig.set('ai.api_key', key);
|
|
157
|
-
|
|
158
|
-
const { testNow } = await inquirer.prompt([{
|
|
159
|
-
type: 'confirm',
|
|
160
|
-
name: 'testNow',
|
|
161
|
-
message: 'Test the key now (1 small request)?',
|
|
162
|
-
default: true,
|
|
163
|
-
}]);
|
|
164
|
-
if (testNow) {
|
|
165
|
-
const spinner = ora('Asking Claude to say hi...').start();
|
|
166
|
-
try {
|
|
167
|
-
await ai.complete({ prompt: 'Reply with the single word: ok', maxTokens: 4 });
|
|
168
|
-
spinner.succeed(chalk.green('AI key works'));
|
|
169
|
-
} catch (err) {
|
|
170
|
-
spinner.fail(chalk.red(err.message));
|
|
171
|
-
console.log(chalk.yellow(' You can fix this with `gent config set ai.api_key <key>`.'));
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
126
|
async function stepIdentity() {
|
|
177
|
-
console.log(chalk.bold('\
|
|
127
|
+
console.log(chalk.bold('\n3. Default identity'));
|
|
178
128
|
const currentName = await userConfig.getResolved('user.name');
|
|
179
129
|
const currentEmail = await userConfig.getResolved('user.email');
|
|
180
130
|
|
package/src/commands/summary.js
CHANGED
|
@@ -150,12 +150,16 @@ async function summary(options = {}) {
|
|
|
150
150
|
}));
|
|
151
151
|
|
|
152
152
|
if (options.ai) {
|
|
153
|
+
await ai.prime();
|
|
153
154
|
if (!ai.isEnabled()) {
|
|
154
155
|
console.log(chalk.gray(ai.disabledHint()));
|
|
155
156
|
} else {
|
|
156
157
|
try {
|
|
157
158
|
const facts = lines.join('\n').replace(/\[[0-9;]*m/g, ''); // strip colors
|
|
158
|
-
const narrative = await ai.explainChanges(
|
|
159
|
+
const narrative = await ai.explainChanges(
|
|
160
|
+
`Repository stats:\n${facts}\n\nGive a 2-3 sentence health assessment.`,
|
|
161
|
+
'summary',
|
|
162
|
+
);
|
|
159
163
|
console.log(chalk.cyan(narrative));
|
|
160
164
|
} catch (err) {
|
|
161
165
|
console.log(chalk.yellow(`AI summary failed: ${err.message}`));
|
package/src/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* History: commit, log, show, tag, explain
|
|
14
14
|
* Branching: branch, checkout, merge, resolve, stash
|
|
15
15
|
* Safety: undo, redo
|
|
16
|
-
* Insight: summary, ask, review, docs, changelog
|
|
16
|
+
* Insight: summary, ask, chat, review, docs, changelog
|
|
17
17
|
* Remote: remote, repos, members, push, pull, search, web, share
|
|
18
18
|
* Auth: register, login, logout, whoami, password
|
|
19
19
|
* AI: ai (status|test|models)
|
|
@@ -72,6 +72,7 @@ const doctorCommand = require('./commands/doctor');
|
|
|
72
72
|
const setupCommand = require('./commands/setup');
|
|
73
73
|
const aiCommand = require('./commands/ai');
|
|
74
74
|
const askCommand = route('ask', require('./commands/ask'));
|
|
75
|
+
const chatCommand = require('./commands/chat');
|
|
75
76
|
const reviewCommand = route('review', require('./commands/review'));
|
|
76
77
|
const docsCommand = route('docs', require('./commands/docs'));
|
|
77
78
|
const changelogCommand = route('changelog', require('./commands/changelog'));
|
|
@@ -188,7 +189,7 @@ program
|
|
|
188
189
|
.description('Record changes to the repository')
|
|
189
190
|
.option('-m, --message <message>', 'Commit message')
|
|
190
191
|
.option('-a, --all', 'Automatically stage all modified files')
|
|
191
|
-
.option('--ai', 'Suggest a commit message with
|
|
192
|
+
.option('--ai', 'Suggest a commit message with local Gent AI')
|
|
192
193
|
.action(commitCommand);
|
|
193
194
|
|
|
194
195
|
program
|
|
@@ -222,7 +223,7 @@ program
|
|
|
222
223
|
program
|
|
223
224
|
.command('summary')
|
|
224
225
|
.description('Show a repository health & statistics dashboard')
|
|
225
|
-
.option('--ai', 'Add an AI-written health narrative
|
|
226
|
+
.option('--ai', 'Add an AI-written health narrative')
|
|
226
227
|
.action(summaryCommand);
|
|
227
228
|
|
|
228
229
|
// ─── Branching & Merging ────────────────────────────────
|
|
@@ -258,6 +259,7 @@ program
|
|
|
258
259
|
.command('merge [branch]')
|
|
259
260
|
.description('Merge a branch into the current branch (3-way smart merge)')
|
|
260
261
|
.option('-m, --message <message>', 'Merge commit message')
|
|
262
|
+
.option('--ai', 'Resolve conflicts with AI, commit, then review the merge')
|
|
261
263
|
.option('--continue', 'Finish a resolved canonical merge')
|
|
262
264
|
.option('--abort', 'Abort a canonical merge')
|
|
263
265
|
.action(async (branch, options) => {
|
|
@@ -284,6 +286,7 @@ program
|
|
|
284
286
|
program
|
|
285
287
|
.command('resolve')
|
|
286
288
|
.description('Interactively resolve merge conflicts left by "gent merge"')
|
|
289
|
+
.option('--ai', 'Resolve every text conflict with AI, commit, then review')
|
|
287
290
|
.action(resolveCommand);
|
|
288
291
|
|
|
289
292
|
program
|
|
@@ -316,12 +319,13 @@ program
|
|
|
316
319
|
.action(remoteCommand);
|
|
317
320
|
|
|
318
321
|
program
|
|
319
|
-
.command('repos')
|
|
322
|
+
.command('repos [names...]')
|
|
320
323
|
.description('List or create remote repositories')
|
|
321
324
|
.option('--create <name>', 'Create a new remote repository')
|
|
322
325
|
.option('--description <text>', 'Repository description (with --create)')
|
|
323
326
|
.option('--private', 'Make repository private (with --create)')
|
|
324
327
|
.option('--default-branch <name>', 'Default branch name (with --create)')
|
|
328
|
+
.option('-y, --yes', 'Initialize the current folder without prompting')
|
|
325
329
|
.action(reposCommand);
|
|
326
330
|
|
|
327
331
|
program
|
|
@@ -350,18 +354,18 @@ program
|
|
|
350
354
|
|
|
351
355
|
program
|
|
352
356
|
.command('setup')
|
|
353
|
-
.description('Interactive first-run wizard (backend URL, login,
|
|
357
|
+
.description('Interactive first-run wizard (backend URL, login, identity)')
|
|
354
358
|
.action(setupCommand);
|
|
355
359
|
|
|
356
360
|
program
|
|
357
361
|
.command('config [subcommand] [args...]')
|
|
358
|
-
.description('Manage CLI settings (list|get|set|unset|path)
|
|
362
|
+
.description('Manage CLI settings (list|get|set|unset|path)')
|
|
359
363
|
.action(configCommand);
|
|
360
364
|
|
|
361
365
|
program
|
|
362
366
|
.command('doctor')
|
|
363
|
-
.description('Run a health check across node, repo, auth, backend, and AI
|
|
364
|
-
.option('--ai', 'Also live-test
|
|
367
|
+
.description('Run a health check across node, repo, auth, backend, and Gent AI')
|
|
368
|
+
.option('--ai', 'Also live-test Gent AI with a tiny request')
|
|
365
369
|
.action(doctorCommand);
|
|
366
370
|
|
|
367
371
|
program
|
|
@@ -373,7 +377,7 @@ program
|
|
|
373
377
|
|
|
374
378
|
program
|
|
375
379
|
.command('ask [question]')
|
|
376
|
-
.description('Ask
|
|
380
|
+
.description('Ask local Gent AI a question about this repo')
|
|
377
381
|
.action(async (question, options) => {
|
|
378
382
|
if (!question && interactive.isInteractive()) {
|
|
379
383
|
question = await interactive.promptAsk();
|
|
@@ -388,6 +392,11 @@ program
|
|
|
388
392
|
.option('--head', 'Force review of HEAD commit')
|
|
389
393
|
.action(reviewCommand);
|
|
390
394
|
|
|
395
|
+
program
|
|
396
|
+
.command('chat [message]')
|
|
397
|
+
.description('Chat with Gent AI about the current repository')
|
|
398
|
+
.action(chatCommand);
|
|
399
|
+
|
|
391
400
|
program
|
|
392
401
|
.command('docs')
|
|
393
402
|
.description('Generate a README.md draft for this repo using AI')
|
|
@@ -550,7 +559,7 @@ function showQuickstart() {
|
|
|
550
559
|
console.log(chalk.gray('A Git-like VCS with cloud sync + AI superpowers.\n'));
|
|
551
560
|
console.log(chalk.bold('First time? Try:'));
|
|
552
561
|
console.log(` ${chalk.cyan('gent auto')} ${chalk.gray('guided init → commit → push (interactive)')}`);
|
|
553
|
-
console.log(` ${chalk.cyan('gent setup')} ${chalk.gray('configure login +
|
|
562
|
+
console.log(` ${chalk.cyan('gent setup')} ${chalk.gray('configure login + identity + remote')}`);
|
|
554
563
|
console.log(` ${chalk.cyan('gent doctor')} ${chalk.gray('check everything is wired up')}`);
|
|
555
564
|
console.log(` ${chalk.cyan('gent template list')} ${chalk.gray('scaffold a starter project')}`);
|
|
556
565
|
console.log();
|
|
@@ -558,9 +567,11 @@ function showQuickstart() {
|
|
|
558
567
|
console.log(` ${chalk.cyan('gent init && gent add -A && gent commit -m "init"')}`);
|
|
559
568
|
console.log(` ${chalk.cyan('gent push')} / ${chalk.cyan('gent pull')} / ${chalk.cyan('gent merge <branch>')}`);
|
|
560
569
|
console.log();
|
|
561
|
-
console.log(chalk.bold('AI features
|
|
570
|
+
console.log(chalk.bold('Local AI features:'));
|
|
571
|
+
console.log(` ${chalk.cyan('gent chat')} ${chalk.gray('interactive repository chat')}`);
|
|
562
572
|
console.log(` ${chalk.cyan('gent ask "what does this repo do?"')}`);
|
|
563
573
|
console.log(` ${chalk.cyan('gent review')} ${chalk.gray('review staged changes')}`);
|
|
574
|
+
console.log(` ${chalk.cyan('gent merge dev --ai')} ${chalk.gray('resolve, commit, then review')}`);
|
|
564
575
|
console.log(` ${chalk.cyan('gent docs --write')} ${chalk.gray('generate README.md')}`);
|
|
565
576
|
console.log(` ${chalk.cyan('gent changelog')} ${chalk.gray('grouped release notes')}`);
|
|
566
577
|
console.log();
|