gent-cli 7.0.0 → 8.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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Doctor Command - Health check for the gent CLI.
3
+ *
4
+ * gent doctor → run all checks
5
+ * gent doctor --ai → also ping Anthropic with a 1-token request
6
+ *
7
+ * Each row prints PASS / WARN / FAIL plus a hint on how to fix the issue.
8
+ */
9
+
10
+ const path = require('path');
11
+ const fs = require('fs').promises;
12
+ const chalk = require('chalk');
13
+ const axios = require('axios');
14
+ const packageJson = require('../../package.json');
15
+ const { GENT_DIR } = require('../utils/constants');
16
+ const userConfig = require('../utils/user-config');
17
+ const apiClient = require('../utils/api-client');
18
+ const authStorage = require('../utils/auth-storage');
19
+ const ai = require('../utils/ai-service');
20
+
21
+ const MIN_NODE_MAJOR = 18;
22
+
23
+ async function doctor(options = {}) {
24
+ console.log(chalk.bold.cyan('\nGent CLI health check\n'));
25
+
26
+ const checks = [];
27
+
28
+ checks.push(await checkNodeVersion());
29
+ checks.push(await checkCliVersion());
30
+ checks.push(await checkRepo());
31
+ checks.push(await checkAuth());
32
+ checks.push(await checkApi());
33
+ checks.push(await checkAiKey(!!options.ai));
34
+
35
+ for (const c of checks) {
36
+ printCheck(c);
37
+ }
38
+
39
+ const failed = checks.filter(c => c.status === 'fail').length;
40
+ const warned = checks.filter(c => c.status === 'warn').length;
41
+ const passed = checks.filter(c => c.status === 'pass').length;
42
+
43
+ console.log();
44
+ console.log(
45
+ chalk.green(` ${passed} pass`) + ' ' +
46
+ chalk.yellow(`${warned} warn`) + ' ' +
47
+ chalk.red(`${failed} fail`)
48
+ );
49
+
50
+ if (failed > 0) process.exit(1);
51
+ }
52
+
53
+ function printCheck(c) {
54
+ const badge = c.status === 'pass' ? chalk.green('✓ PASS')
55
+ : c.status === 'warn' ? chalk.yellow('! WARN')
56
+ : chalk.red('✗ FAIL');
57
+ console.log(` ${badge} ${chalk.bold(c.name)} ${chalk.gray(`— ${c.detail}`)}`);
58
+ if (c.hint) console.log(chalk.gray(` hint: ${c.hint}`));
59
+ }
60
+
61
+ async function checkNodeVersion() {
62
+ const v = process.versions.node;
63
+ const major = parseInt(v.split('.')[0], 10);
64
+ if (major >= MIN_NODE_MAJOR) {
65
+ return { name: 'Node version', status: 'pass', detail: `v${v}` };
66
+ }
67
+ return {
68
+ name: 'Node version',
69
+ status: 'fail',
70
+ detail: `v${v} (need ≥${MIN_NODE_MAJOR})`,
71
+ hint: `Install Node ${MIN_NODE_MAJOR}+ — e.g. via nvm.`,
72
+ };
73
+ }
74
+
75
+ async function checkCliVersion() {
76
+ return {
77
+ name: 'Gent CLI',
78
+ status: 'pass',
79
+ detail: `v${packageJson.version}`,
80
+ };
81
+ }
82
+
83
+ async function checkRepo() {
84
+ const gentPath = path.join(process.cwd(), GENT_DIR);
85
+ try {
86
+ const stat = await fs.stat(gentPath);
87
+ if (!stat.isDirectory()) throw new Error('not a directory');
88
+ return { name: 'Repository (.gent)', status: 'pass', detail: gentPath };
89
+ } catch {
90
+ return {
91
+ name: 'Repository (.gent)',
92
+ status: 'warn',
93
+ detail: 'not a gent repo (this directory)',
94
+ hint: 'Run `gent init` to start a repo here, or cd into one.',
95
+ };
96
+ }
97
+ }
98
+
99
+ async function checkAuth() {
100
+ const isAuth = await authStorage.isAuthenticated();
101
+ if (!isAuth) {
102
+ return {
103
+ name: 'Authentication',
104
+ status: 'warn',
105
+ detail: 'not logged in',
106
+ hint: 'Run `gent login` or `gent register`.',
107
+ };
108
+ }
109
+ const user = await authStorage.getUser();
110
+ const who = user ? `${user.email}` : 'unknown user';
111
+ return { name: 'Authentication', status: 'pass', detail: who };
112
+ }
113
+
114
+ async function checkApi() {
115
+ const { value: baseUrl, source } = await userConfig.getResolved('api.base_url');
116
+ try {
117
+ await axios.get(baseUrl, { timeout: 8000, validateStatus: () => true });
118
+ return {
119
+ name: 'Backend reachable',
120
+ status: 'pass',
121
+ detail: `${baseUrl} [${source}]`,
122
+ };
123
+ } catch (err) {
124
+ return {
125
+ name: 'Backend reachable',
126
+ status: 'fail',
127
+ detail: `${baseUrl} → ${err.code || err.message}`,
128
+ hint: 'If running a local server: `gent config set api.base_url http://localhost:8000`.',
129
+ };
130
+ }
131
+ }
132
+
133
+ async function checkAiKey(probe) {
134
+ const { value: key, source } = await ai.resolveKey();
135
+ if (!key) {
136
+ return {
137
+ name: 'AI key',
138
+ status: 'warn',
139
+ detail: 'not configured (AI features will be skipped, not failed)',
140
+ hint: 'Run `gent config set ai.api_key <key>` or set ANTHROPIC_API_KEY.',
141
+ };
142
+ }
143
+
144
+ if (!probe) {
145
+ return {
146
+ name: 'AI key',
147
+ status: 'pass',
148
+ detail: `present [${source}], model: ${await ai.resolveModel()} (use --ai to live-test)`,
149
+ };
150
+ }
151
+
152
+ try {
153
+ await ai.complete({ prompt: 'ping', maxTokens: 4 });
154
+ return {
155
+ name: 'AI key',
156
+ status: 'pass',
157
+ detail: `verified — model ${await ai.resolveModel()} responded`,
158
+ };
159
+ } catch (err) {
160
+ return {
161
+ name: 'AI key',
162
+ status: 'fail',
163
+ detail: err.message,
164
+ hint: 'Re-check the key (`gent config set ai.api_key`) or model (`gent config set ai.model`).',
165
+ };
166
+ }
167
+ }
168
+
169
+ module.exports = doctor;
@@ -30,7 +30,7 @@ const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
30
30
  const { COMMITS_FILE, CONFIG_FILE, API_ENDPOINTS, buildRepoUrl, parseRemoteUrl } = require('../utils/constants');
31
31
  const apiClient = require('../utils/api-client');
32
32
  const authStorage = require('../utils/auth-storage');
33
- const { storeBlob, objectExists, readBlobAsString, decodeRemoteBlobContent } = require('../utils/hash-engine');
33
+ const { storeBlob, objectExists, readBlob, readBlobAsString, decodeRemoteBlobContent } = require('../utils/hash-engine');
34
34
  const { findMergeBase, mergeTreeEntries } = require('../utils/merge-engine');
35
35
  const { generateCommitHash } = require('../utils/helpers');
36
36
 
@@ -311,10 +311,11 @@ async function checkoutTree(gentPath, cwd, previousTree, nextTree) {
311
311
  const relPath = entry.name || entry.path;
312
312
  if (!relPath || !entry.hash) continue;
313
313
 
314
- const content = await readBlobAsString(gentPath, entry.hash);
314
+ // Write the raw Buffer so binary blobs round-trip byte-exact.
315
+ const buf = await readBlob(gentPath, entry.hash);
315
316
  const fullPath = path.join(cwd, relPath);
316
317
  await fs.mkdir(path.dirname(fullPath), { recursive: true });
317
- await fs.writeFile(fullPath, content, 'utf-8');
318
+ await fs.writeFile(fullPath, buf);
318
319
  }
319
320
  }
320
321
 
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Review Command - AI code review on staged or HEAD changes.
3
+ *
4
+ * gent review → review staged changes (or HEAD if no staging)
5
+ * gent review --staged → force staged
6
+ * gent review --head → force HEAD commit diff
7
+ * gent review <ref> → review diff for that commit
8
+ *
9
+ * Output: prioritized bug/risk list followed by smaller polish suggestions.
10
+ * Without an AI key, prints the raw diff so the command still has value.
11
+ */
12
+
13
+ const path = require('path');
14
+ const chalk = require('chalk');
15
+ const ora = require('ora');
16
+ const { getGentPath, readJSON } = require('../utils/fileSystem');
17
+ const { COMMITS_FILE, STAGING_FILE } = require('../utils/constants');
18
+ const { readBlobAsString, treeToMap } = require('../utils/hash-engine');
19
+ const { formatUnifiedDiff } = require('../utils/diff-engine');
20
+ const ai = require('../utils/ai-service');
21
+
22
+ const MAX_DIFF_CHARS = 16000;
23
+
24
+ async function review(refArg, options = {}) {
25
+ try {
26
+ const gentPath = await getGentPath();
27
+ const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
28
+ const commits = repository.commits || [];
29
+ const commitMap = new Map(commits.map(c => [c.hash, c]));
30
+
31
+ let title;
32
+ let diffText;
33
+
34
+ const explicitStaged = options.staged === true;
35
+ const explicitHead = options.head === true;
36
+ let useStaged = explicitStaged;
37
+
38
+ if (!explicitStaged && !explicitHead && !refArg) {
39
+ // Default: staged if anything is staged, else HEAD
40
+ const staging = await readJSON(path.join(gentPath, STAGING_FILE)).catch(() => ({}));
41
+ const entries = staging.entries || [];
42
+ useStaged = entries.length > 0;
43
+ }
44
+
45
+ if (useStaged) {
46
+ const result = await stagedDiff(gentPath, repository, commitMap);
47
+ if (!result) {
48
+ console.log(chalk.yellow('Nothing staged to review.'));
49
+ return;
50
+ }
51
+ title = 'Staged changes';
52
+ diffText = result;
53
+ } else {
54
+ const ref = refArg || repository.branches[repository.currentBranch];
55
+ const commit = ref ? (commitMap.get(ref) || commits.find(c => c.hash.startsWith(ref))) : null;
56
+ if (!commit) {
57
+ console.log(chalk.yellow(ref ? `Commit '${ref}' not found` : 'No commits yet'));
58
+ return;
59
+ }
60
+ const parent = commit.parent ? commitMap.get(commit.parent) : null;
61
+ title = `Commit ${commit.hash.slice(0, 7)} — ${commit.message.split('\n')[0]}`;
62
+ diffText = await diffTrees(gentPath, treeEntriesOf(parent), treeEntriesOf(commit));
63
+ }
64
+
65
+ if (!diffText) {
66
+ console.log(chalk.gray('No textual changes to review.'));
67
+ return;
68
+ }
69
+
70
+ const trimmed = diffText.length > MAX_DIFF_CHARS
71
+ ? diffText.slice(0, MAX_DIFF_CHARS) + '\n... (diff truncated)'
72
+ : diffText;
73
+
74
+ console.log(chalk.bold.cyan(`\n${title}\n`));
75
+
76
+ if (!ai.isEnabled()) {
77
+ console.log(trimmed);
78
+ console.log(chalk.gray(`\n${ai.disabledHint()}`));
79
+ return;
80
+ }
81
+
82
+ const spinner = ora(`Reviewing with ${ai.getModel()}...`).start();
83
+ try {
84
+ const out = await ai.complete({
85
+ system:
86
+ 'You are a senior code reviewer. Given a unified diff, list concrete ' +
87
+ 'issues you would block on, then smaller suggestions. Format:\n' +
88
+ '🔴 Bugs / risks\n - file:line — short description\n' +
89
+ '🟡 Suggestions\n - file — short description\n' +
90
+ '🟢 Looks good\n - one-line positive note\n' +
91
+ 'Be specific. If nothing is wrong, say so plainly.',
92
+ prompt: `Review this diff:\n\n${trimmed}`,
93
+ maxTokens: 1500,
94
+ thinking: true,
95
+ });
96
+ spinner.stop();
97
+ console.log(out + '\n');
98
+ } catch (err) {
99
+ spinner.fail(chalk.yellow('AI review failed — showing the raw diff instead'));
100
+ console.log(chalk.gray(`(${err.message})\n`));
101
+ console.log(trimmed);
102
+ }
103
+ } catch (error) {
104
+ if (error.code === 'ENOENT' && error.message.includes('.gent')) {
105
+ console.error(chalk.red('Error: Not a gent repository'));
106
+ } else {
107
+ console.error(chalk.red('Error:'), error.message);
108
+ }
109
+ process.exit(1);
110
+ }
111
+ }
112
+
113
+ function treeEntriesOf(commit) {
114
+ if (!commit) return [];
115
+ if (Array.isArray(commit.tree)) return commit.tree;
116
+ return (commit.files || []).map(f => ({ name: f.path || f.name, hash: f.hash }));
117
+ }
118
+
119
+ async function diffTrees(gentPath, oldEntries, newEntries) {
120
+ const oldMap = treeToMap(oldEntries);
121
+ const newMap = treeToMap(newEntries);
122
+ const files = new Set([...oldMap.keys(), ...newMap.keys()]);
123
+ const parts = [];
124
+ for (const file of files) {
125
+ const oh = oldMap.get(file);
126
+ const nh = newMap.get(file);
127
+ if (oh === nh) continue;
128
+ let oldText = '', newText = '';
129
+ try { if (oh) oldText = await readBlobAsString(gentPath, oh); } catch { /* binary */ }
130
+ try { if (nh) newText = await readBlobAsString(gentPath, nh); } catch { /* binary */ }
131
+ const d = formatUnifiedDiff(file, oldText, newText);
132
+ if (d) parts.push(d);
133
+ }
134
+ return parts.join('\n\n');
135
+ }
136
+
137
+ async function stagedDiff(gentPath, repository, commitMap) {
138
+ const staging = await readJSON(path.join(gentPath, STAGING_FILE)).catch(() => ({}));
139
+ const entries = staging.entries || [];
140
+ if (entries.length === 0) return null;
141
+
142
+ const headHash = repository.branches[repository.currentBranch];
143
+ const head = headHash ? commitMap.get(headHash) : null;
144
+ const headTree = treeEntriesOf(head);
145
+ const overlay = new Map(headTree.map(e => [e.name, e.hash]));
146
+ for (const e of entries) {
147
+ if (e.status === 'deleted') overlay.delete(e.path);
148
+ else overlay.set(e.path, e.hash);
149
+ }
150
+ return diffTrees(
151
+ gentPath,
152
+ headTree,
153
+ [...overlay].map(([name, hash]) => ({ name, hash }))
154
+ );
155
+ }
156
+
157
+ module.exports = review;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Search Command - Fuzzy-search your repositories on the gent backend.
3
+ *
4
+ * gent search <query>
5
+ * gent search --mine → only repos you own
6
+ * gent search --json → machine-readable output
7
+ *
8
+ * The current backend's /api/repos/ endpoint returns the user's repos; we
9
+ * filter client-side. If the backend grows a search endpoint, switch the URL.
10
+ */
11
+
12
+ const chalk = require('chalk');
13
+ const ora = require('ora');
14
+ const { API_ENDPOINTS } = require('../utils/constants');
15
+ const apiClient = require('../utils/api-client');
16
+ const authStorage = require('../utils/auth-storage');
17
+
18
+ async function search(query, options = {}) {
19
+ try {
20
+ if (!query && !options.mine) {
21
+ console.error(chalk.red('Usage: gent search <query>'));
22
+ process.exit(1);
23
+ }
24
+
25
+ const isAuth = await authStorage.isAuthenticated();
26
+ if (!isAuth) {
27
+ console.error(chalk.red('Not authenticated.'));
28
+ console.log(chalk.yellow('Run `gent login` first.'));
29
+ process.exit(1);
30
+ }
31
+
32
+ const spinner = ora('Searching...').start();
33
+ const data = await apiClient.get(API_ENDPOINTS.REPOS);
34
+ const repos = Array.isArray(data) ? data : (data.results || []);
35
+ spinner.stop();
36
+
37
+ const me = await authStorage.getUser();
38
+ const myId = me?.id;
39
+
40
+ const q = (query || '').toLowerCase();
41
+ const filtered = repos.filter(r => {
42
+ if (options.mine && myId && r.owner_id !== myId) return false;
43
+ if (!q) return true;
44
+ const haystack = [r.name, r.description, r.owner_name, r.owner_email]
45
+ .filter(Boolean).join(' ').toLowerCase();
46
+ return haystack.includes(q);
47
+ });
48
+
49
+ if (options.json) {
50
+ console.log(JSON.stringify(filtered, null, 2));
51
+ return;
52
+ }
53
+
54
+ if (filtered.length === 0) {
55
+ console.log(chalk.gray('No matches.'));
56
+ return;
57
+ }
58
+
59
+ console.log(chalk.bold.cyan(`\nFound ${filtered.length} repo(s):\n`));
60
+ for (const r of filtered) {
61
+ const visibility = r.is_private ? chalk.red('private') : chalk.green('public');
62
+ const desc = r.description ? chalk.gray(` — ${r.description}`) : '';
63
+ console.log(` ${chalk.white.bold(r.name)} [${visibility}]${desc}`);
64
+ console.log(` ${chalk.gray(`/api/repos/${r.owner_id}/${r.name}`)}`);
65
+ }
66
+ console.log();
67
+ } catch (error) {
68
+ if (error.response?.status === 401) {
69
+ console.error(chalk.red('Authentication failed — run `gent login`.'));
70
+ } else {
71
+ console.error(chalk.red('Error:'), error.message);
72
+ }
73
+ process.exit(1);
74
+ }
75
+ }
76
+
77
+ module.exports = search;
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Setup Command - First-run interactive wizard.
3
+ *
4
+ * gent setup
5
+ *
6
+ * Walks the user through: backend URL → login/register → AI key → identity.
7
+ * Each step is skippable; nothing is required.
8
+ */
9
+
10
+ const chalk = require('chalk');
11
+ const inquirer = require('inquirer');
12
+ const boxen = require('boxen');
13
+ const ora = require('ora');
14
+ const axios = require('axios');
15
+ const userConfig = require('../utils/user-config');
16
+ const authStorage = require('../utils/auth-storage');
17
+ const authService = require('../services/auth-service');
18
+ const ai = require('../utils/ai-service');
19
+
20
+ async function setup() {
21
+ console.log(boxen(
22
+ chalk.bold.cyan('Welcome to Gent\n') +
23
+ chalk.white('Let\'s get you set up. Every step is optional — press Enter to skip.'),
24
+ { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'cyan' }
25
+ ));
26
+
27
+ await stepBackend();
28
+ await stepAuth();
29
+ await stepAiKey();
30
+ await stepIdentity();
31
+
32
+ console.log(chalk.green('\n✓ Setup complete!'));
33
+ console.log(chalk.gray(' Run `gent doctor` any time to verify your setup.\n'));
34
+ }
35
+
36
+ async function stepBackend() {
37
+ console.log(chalk.bold('\n1. Backend server'));
38
+ const current = await userConfig.getResolved('api.base_url');
39
+ console.log(chalk.gray(` Current: ${current.value} [${current.source}]`));
40
+
41
+ const { change } = await inquirer.prompt([{
42
+ type: 'confirm',
43
+ name: 'change',
44
+ message: 'Change backend URL?',
45
+ default: false,
46
+ }]);
47
+ if (!change) return;
48
+
49
+ const { url } = await inquirer.prompt([{
50
+ type: 'input',
51
+ name: 'url',
52
+ message: 'Backend URL:',
53
+ default: current.value,
54
+ validate: (v) => /^https?:\/\//.test(v) || 'Must start with http:// or https://',
55
+ }]);
56
+
57
+ const spinner = ora('Probing backend...').start();
58
+ try {
59
+ await axios.get(url, { timeout: 8000, validateStatus: () => true });
60
+ spinner.succeed(chalk.green('Backend reachable'));
61
+ } catch (err) {
62
+ spinner.warn(chalk.yellow(`Could not reach ${url} (${err.code || err.message}) — saving anyway`));
63
+ }
64
+ await userConfig.set('api.base_url', url);
65
+ }
66
+
67
+ async function stepAuth() {
68
+ console.log(chalk.bold('\n2. Account'));
69
+ const isAuth = await authStorage.isAuthenticated();
70
+ if (isAuth) {
71
+ const user = await authStorage.getUser();
72
+ console.log(chalk.gray(` Already logged in as ${user?.email || 'unknown'} — skipping.`));
73
+ return;
74
+ }
75
+
76
+ const { action } = await inquirer.prompt([{
77
+ type: 'list',
78
+ name: 'action',
79
+ message: 'What would you like to do?',
80
+ choices: [
81
+ { name: 'Log in to an existing account', value: 'login' },
82
+ { name: 'Create a new account', value: 'register' },
83
+ { name: 'Skip for now', value: 'skip' },
84
+ ],
85
+ default: 'login',
86
+ }]);
87
+ if (action === 'skip') return;
88
+
89
+ const credentials = await inquirer.prompt([
90
+ {
91
+ type: 'input',
92
+ name: 'email',
93
+ message: 'Email:',
94
+ validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || 'Invalid email',
95
+ },
96
+ {
97
+ type: 'password',
98
+ name: 'password',
99
+ message: 'Password:',
100
+ mask: '*',
101
+ },
102
+ ]);
103
+
104
+ const spinner = ora(action === 'login' ? 'Logging in...' : 'Creating account...').start();
105
+ try {
106
+ if (action === 'login') {
107
+ await authService.login(credentials.email, credentials.password);
108
+ } else {
109
+ const more = await inquirer.prompt([
110
+ { type: 'password', name: 'passwordConfirm', message: 'Confirm password:', mask: '*' },
111
+ { type: 'input', name: 'firstName', message: 'First name:', default: '' },
112
+ { type: 'input', name: 'lastName', message: 'Last name:', default: '' },
113
+ ]);
114
+ await authService.register(
115
+ credentials.email,
116
+ credentials.password,
117
+ more.passwordConfirm,
118
+ more.firstName,
119
+ more.lastName
120
+ );
121
+ }
122
+ spinner.succeed(chalk.green('Signed in'));
123
+ } catch (err) {
124
+ spinner.fail(chalk.red(err.message));
125
+ }
126
+ }
127
+
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
+ async function stepIdentity() {
177
+ console.log(chalk.bold('\n4. Default identity'));
178
+ const currentName = await userConfig.getResolved('user.name');
179
+ const currentEmail = await userConfig.getResolved('user.email');
180
+
181
+ if (currentName.value && currentEmail.value) {
182
+ console.log(chalk.gray(` ${currentName.value} <${currentEmail.value}> — skipping.`));
183
+ return;
184
+ }
185
+
186
+ // Prefill from logged-in user if available
187
+ const u = await authStorage.getUser();
188
+ const defaultName = currentName.value ||
189
+ (u ? [u.first_name, u.last_name].filter(Boolean).join(' ') : '');
190
+ const defaultEmail = currentEmail.value || (u ? u.email : '');
191
+
192
+ const answers = await inquirer.prompt([
193
+ { type: 'input', name: 'name', message: 'Default name on commits:', default: defaultName },
194
+ { type: 'input', name: 'email', message: 'Default email on commits:', default: defaultEmail },
195
+ ]);
196
+
197
+ if (answers.name) await userConfig.set('user.name', answers.name);
198
+ if (answers.email) await userConfig.set('user.email', answers.email);
199
+ }
200
+
201
+ module.exports = setup;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Share Command - Print a shareable link to current branch/commit.
3
+ *
4
+ * gent share → link to current HEAD on current branch
5
+ * gent share --branch <name> → link to a branch's tip
6
+ * gent share --commit <hash> → link to a specific commit
7
+ *
8
+ * Like `gent web --print` but always commit-scoped by default — handy for
9
+ * Slack/PR descriptions.
10
+ */
11
+
12
+ const path = require('path');
13
+ const chalk = require('chalk');
14
+ const { getGentPath, readJSON } = require('../utils/fileSystem');
15
+ const { CONFIG_FILE, COMMITS_FILE, parseRemoteUrl } = require('../utils/constants');
16
+ const userConfig = require('../utils/user-config');
17
+
18
+ async function share(options = {}) {
19
+ try {
20
+ const gentPath = await getGentPath();
21
+ const config = await readJSON(path.join(gentPath, CONFIG_FILE));
22
+ const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
23
+
24
+ const remote = (config.remotes || {}).origin;
25
+ if (!remote) {
26
+ console.error(chalk.red('No origin remote configured.'));
27
+ console.log(chalk.yellow('Set one with `gent remote add origin <url>`.'));
28
+ process.exit(1);
29
+ }
30
+ const info = parseRemoteUrl(remote.url);
31
+ if (!info) {
32
+ console.error(chalk.red('Origin URL is not in a recognized gent format.'));
33
+ process.exit(1);
34
+ }
35
+
36
+ const { value: baseUrl } = await userConfig.getResolved('api.base_url');
37
+ const webHost = baseUrl.replace(/\/api\/?$/, '').replace(/\/$/, '');
38
+ const repoBase = `${webHost}/${info.owner_id}/${info.repo_name}`;
39
+
40
+ if (options.commit) {
41
+ console.log(`${repoBase}/commit/${options.commit}`);
42
+ return;
43
+ }
44
+ const branch = options.branch || repository.currentBranch;
45
+ const tip = repository.branches[branch];
46
+ if (tip) {
47
+ console.log(`${repoBase}/commit/${tip}`);
48
+ console.log(chalk.gray(`(${branch} @ ${tip.slice(0, 7)})`));
49
+ } else {
50
+ console.log(`${repoBase}/tree/${encodeURIComponent(branch)}`);
51
+ console.log(chalk.gray(`(${branch} has no commits yet)`));
52
+ }
53
+ } catch (error) {
54
+ if (error.code === 'ENOENT' && error.message.includes('.gent')) {
55
+ console.error(chalk.red('Error: Not a gent repository'));
56
+ } else {
57
+ console.error(chalk.red('Error:'), error.message);
58
+ }
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ module.exports = share;