gent-cli 7.0.0 → 9.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/package.json +2 -2
- package/src/commands/ai.js +82 -0
- package/src/commands/ask.js +121 -0
- package/src/commands/branch.js +4 -0
- package/src/commands/changelog.js +121 -0
- package/src/commands/clone.js +36 -131
- package/src/commands/config.js +147 -0
- package/src/commands/docs.js +141 -0
- package/src/commands/doctor.js +169 -0
- package/src/commands/log.js +1 -2
- package/src/commands/members.js +134 -0
- package/src/commands/password.js +134 -0
- package/src/commands/pull.js +37 -95
- package/src/commands/push.js +27 -5
- package/src/commands/review.js +157 -0
- package/src/commands/search.js +77 -0
- package/src/commands/setup.js +201 -0
- package/src/commands/share.js +63 -0
- package/src/commands/show.js +3 -2
- package/src/commands/tag.js +13 -4
- package/src/commands/template.js +135 -0
- package/src/commands/web.js +72 -0
- package/src/index.js +166 -11
- package/src/services/auth-service.js +3 -4
- package/src/utils/ai-service.js +100 -37
- package/src/utils/api-client.js +33 -10
- package/src/utils/auth-storage.js +15 -3
- package/src/utils/constants.js +11 -2
- package/src/utils/env-loader.js +61 -0
- package/src/utils/user-config.js +225 -0
|
@@ -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;
|
package/src/commands/show.js
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* Retrieves commit from commits.json, reads blob content from object store,
|
|
17
17
|
* computes diff against parent commit's tree, and displays unified diff.
|
|
18
18
|
*
|
|
19
|
-
* BACKEND
|
|
20
|
-
*
|
|
19
|
+
* BACKEND: none — fully local. Reads commits.json + the local object store.
|
|
20
|
+
* (Remote commit_detail returns { sha, tree_sha, parent_shas[], author_name,
|
|
21
|
+
* author_email, committed_at, message } — tree_sha is a bare string, no embedded tree.)
|
|
21
22
|
*
|
|
22
23
|
* ============================================================================
|
|
23
24
|
*/
|
package/src/commands/tag.js
CHANGED
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* Annotated tag = includes tagger info, message, timestamp.
|
|
19
19
|
*
|
|
20
20
|
* BACKEND EXPECTATIONS:
|
|
21
|
-
* POST
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* POST /api/repos/:owner_id/:repo_name/tags/create/ { name, commit_sha, message, annotated, tagger_name, tagger_email }
|
|
22
|
+
* DELETE /api/repos/:owner_id/:repo_name/tags/:name/
|
|
23
|
+
* (tag list is local-only — the CLI never GETs /tags/)
|
|
24
24
|
*
|
|
25
25
|
* ============================================================================
|
|
26
26
|
*/
|
|
@@ -181,8 +181,15 @@ async function syncTagCreate(name, tagObj, taggerName, taggerEmail, gentPath) {
|
|
|
181
181
|
await apiClient.post(url, payload);
|
|
182
182
|
console.log(chalk.gray(` ↑ Synced to remote`));
|
|
183
183
|
} catch (error) {
|
|
184
|
+
// Duplicates now upsert (200) on the backend, so a 400 is a real failure
|
|
185
|
+
// — most often the tagged commit hasn't been pushed yet.
|
|
184
186
|
if (error.response?.status === 400) {
|
|
185
|
-
|
|
187
|
+
const data = error.response.data;
|
|
188
|
+
const msg = (data && data.error) || (typeof data === 'object' ? JSON.stringify(data) : data) || 'Bad request';
|
|
189
|
+
console.log(chalk.yellow(` ⚠ Remote sync failed: ${msg}`));
|
|
190
|
+
console.log(chalk.gray(` (has the tagged commit been pushed to the remote?)`));
|
|
191
|
+
} else if (error.response?.status === 403) {
|
|
192
|
+
console.log(chalk.yellow(` ⚠ Remote sync failed: ${error.response.data?.error || 'no write access to repository'}`));
|
|
186
193
|
}
|
|
187
194
|
}
|
|
188
195
|
}
|
|
@@ -208,6 +215,8 @@ async function syncTagDelete(name, gentPath) {
|
|
|
208
215
|
} catch (error) {
|
|
209
216
|
if (error.response?.status === 404) {
|
|
210
217
|
// Tag didn't exist remotely
|
|
218
|
+
} else if (error.response?.status === 403) {
|
|
219
|
+
console.log(chalk.yellow(` ⚠ Remote delete failed: ${error.response.data?.error || 'no write access to repository'}`));
|
|
211
220
|
}
|
|
212
221
|
}
|
|
213
222
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Command - Quick-start from a baked-in starter.
|
|
3
|
+
*
|
|
4
|
+
* gent template list → show available templates
|
|
5
|
+
* gent template use <name> [directory] → scaffold a starter
|
|
6
|
+
*
|
|
7
|
+
* Templates are tiny inline definitions — no network needed. Keep them small
|
|
8
|
+
* so the CLI bundle stays light.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
const { pathExists, ensureDir } = require('../utils/fileSystem');
|
|
15
|
+
const initCommand = require('./init');
|
|
16
|
+
|
|
17
|
+
const TEMPLATES = {
|
|
18
|
+
node: {
|
|
19
|
+
description: 'Minimal Node.js project (package.json + index.js)',
|
|
20
|
+
files: {
|
|
21
|
+
'package.json': JSON.stringify({
|
|
22
|
+
name: '__NAME__',
|
|
23
|
+
version: '0.1.0',
|
|
24
|
+
main: 'index.js',
|
|
25
|
+
scripts: { start: 'node index.js' },
|
|
26
|
+
}, null, 2) + '\n',
|
|
27
|
+
'index.js': "console.log('hello from __NAME__');\n",
|
|
28
|
+
'.gentignore': 'node_modules/\n.env\n',
|
|
29
|
+
'README.md': '# __NAME__\n\nA Node.js project scaffolded with `gent template use node`.\n',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
python: {
|
|
33
|
+
description: 'Minimal Python project (main.py + requirements.txt)',
|
|
34
|
+
files: {
|
|
35
|
+
'main.py': "def main():\n print('hello from __NAME__')\n\nif __name__ == '__main__':\n main()\n",
|
|
36
|
+
'requirements.txt': '',
|
|
37
|
+
'.gentignore': '__pycache__/\n.venv/\n*.pyc\n.env\n',
|
|
38
|
+
'README.md': '# __NAME__\n\nA Python project scaffolded with `gent template use python`.\n',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
react: {
|
|
42
|
+
description: 'Vite + React skeleton (vite-style index.html + src/main.jsx)',
|
|
43
|
+
files: {
|
|
44
|
+
'package.json': JSON.stringify({
|
|
45
|
+
name: '__NAME__',
|
|
46
|
+
version: '0.1.0',
|
|
47
|
+
type: 'module',
|
|
48
|
+
scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
|
|
49
|
+
dependencies: { react: '^18.3.1', 'react-dom': '^18.3.1' },
|
|
50
|
+
devDependencies: { vite: '^5.4.0', '@vitejs/plugin-react': '^4.3.1' },
|
|
51
|
+
}, null, 2) + '\n',
|
|
52
|
+
'index.html': '<!doctype html>\n<html><head><title>__NAME__</title></head>\n<body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>\n',
|
|
53
|
+
'src/main.jsx': "import React from 'react';\nimport { createRoot } from 'react-dom/client';\ncreateRoot(document.getElementById('root')).render(<h1>__NAME__</h1>);\n",
|
|
54
|
+
'.gentignore': 'node_modules/\ndist/\n.env\n',
|
|
55
|
+
'README.md': '# __NAME__\n\nRun `npm install && npm run dev` to start.\n',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
'django-api': {
|
|
59
|
+
description: 'Minimal Django project shell (manage.py + settings stub)',
|
|
60
|
+
files: {
|
|
61
|
+
'manage.py': "#!/usr/bin/env python\nimport os, sys\nif __name__ == '__main__':\n os.environ.setdefault('DJANGO_SETTINGS_MODULE', '__NAME__.settings')\n from django.core.management import execute_from_command_line\n execute_from_command_line(sys.argv)\n",
|
|
62
|
+
'requirements.txt': 'Django>=5.0\n',
|
|
63
|
+
'.gentignore': '__pycache__/\n*.pyc\n.venv/\ndb.sqlite3\n.env\n',
|
|
64
|
+
'README.md': '# __NAME__\n\nRun `python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt`.\n',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
async function template(subcommand, args = [], options = {}) {
|
|
70
|
+
const sub = (subcommand || 'list').toLowerCase();
|
|
71
|
+
switch (sub) {
|
|
72
|
+
case 'list': return list();
|
|
73
|
+
case 'use': return use(args[0], args[1], options);
|
|
74
|
+
default:
|
|
75
|
+
console.error(chalk.red(`Unknown subcommand '${sub}'`));
|
|
76
|
+
console.log(chalk.gray('Usage: gent template <list|use>'));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function list() {
|
|
82
|
+
console.log(chalk.bold.cyan('\nAvailable templates\n'));
|
|
83
|
+
for (const [name, t] of Object.entries(TEMPLATES)) {
|
|
84
|
+
console.log(` ${chalk.cyan(name.padEnd(14))} ${chalk.gray(t.description)}`);
|
|
85
|
+
}
|
|
86
|
+
console.log(chalk.gray('\n Usage: gent template use <name> [directory]\n'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function use(name, directory) {
|
|
90
|
+
if (!name) {
|
|
91
|
+
console.error(chalk.red('Usage: gent template use <name> [directory]'));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const tpl = TEMPLATES[name];
|
|
95
|
+
if (!tpl) {
|
|
96
|
+
console.error(chalk.red(`Unknown template '${name}'`));
|
|
97
|
+
console.log(chalk.gray(`Available: ${Object.keys(TEMPLATES).join(', ')}`));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const targetDir = directory || name;
|
|
102
|
+
const targetPath = path.resolve(process.cwd(), targetDir);
|
|
103
|
+
const projectName = path.basename(targetPath).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
104
|
+
|
|
105
|
+
if (await pathExists(targetPath)) {
|
|
106
|
+
const entries = await fs.readdir(targetPath);
|
|
107
|
+
if (entries.length > 0) {
|
|
108
|
+
console.error(chalk.red(`Directory '${targetDir}' is not empty.`));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
await ensureDir(targetPath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const [rel, content] of Object.entries(tpl.files)) {
|
|
116
|
+
const filePath = path.join(targetPath, rel);
|
|
117
|
+
await ensureDir(path.dirname(filePath));
|
|
118
|
+
const body = content.replace(/__NAME__/g, projectName);
|
|
119
|
+
await fs.writeFile(filePath, body, 'utf-8');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(chalk.green(`✓ Scaffolded '${name}' into ${targetDir}/`));
|
|
123
|
+
|
|
124
|
+
// Auto-init a gent repo so the next steps work
|
|
125
|
+
const originalCwd = process.cwd();
|
|
126
|
+
try {
|
|
127
|
+
process.chdir(targetPath);
|
|
128
|
+
await initCommand({});
|
|
129
|
+
} finally {
|
|
130
|
+
process.chdir(originalCwd);
|
|
131
|
+
}
|
|
132
|
+
console.log(chalk.gray(' Next: cd ' + targetDir + ' && gent add -A && gent commit -m "init"'));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = template;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Command - Open the current repo (or a specific commit/branch) in browser.
|
|
3
|
+
*
|
|
4
|
+
* gent web → open repo page
|
|
5
|
+
* gent web --branch <name> → open a specific branch
|
|
6
|
+
* gent web --commit <hash> → open a specific commit
|
|
7
|
+
* gent web --print → don't launch, just print the URL
|
|
8
|
+
*
|
|
9
|
+
* Builds the URL from the configured api.base_url and the remote's owner_id/repo_name.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { exec } = require('child_process');
|
|
14
|
+
const chalk = require('chalk');
|
|
15
|
+
const { getGentPath, readJSON } = require('../utils/fileSystem');
|
|
16
|
+
const { CONFIG_FILE, parseRemoteUrl } = require('../utils/constants');
|
|
17
|
+
const userConfig = require('../utils/user-config');
|
|
18
|
+
|
|
19
|
+
async function web(options = {}) {
|
|
20
|
+
try {
|
|
21
|
+
const gentPath = await getGentPath();
|
|
22
|
+
const config = await readJSON(path.join(gentPath, CONFIG_FILE));
|
|
23
|
+
const remote = (config.remotes || {}).origin;
|
|
24
|
+
if (!remote) {
|
|
25
|
+
console.error(chalk.red('No origin remote configured.'));
|
|
26
|
+
console.log(chalk.yellow('Run `gent remote add origin <url>` or `gent init --remote`.'));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const info = parseRemoteUrl(remote.url);
|
|
30
|
+
if (!info) {
|
|
31
|
+
console.error(chalk.red(`Origin URL '${remote.url}' isn't a recognized gent URL.`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { value: baseUrl } = await userConfig.getResolved('api.base_url');
|
|
36
|
+
// Strip /api suffix if present so we get the web host
|
|
37
|
+
const webHost = baseUrl.replace(/\/api\/?$/, '').replace(/\/$/, '');
|
|
38
|
+
|
|
39
|
+
let url = `${webHost}/${info.owner_id}/${info.repo_name}`;
|
|
40
|
+
if (options.branch) url += `/tree/${encodeURIComponent(options.branch)}`;
|
|
41
|
+
if (options.commit) url += `/commit/${encodeURIComponent(options.commit)}`;
|
|
42
|
+
|
|
43
|
+
if (options.print) {
|
|
44
|
+
console.log(url);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(chalk.gray(`Opening ${url}`));
|
|
49
|
+
openInBrowser(url);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code === 'ENOENT' && error.message.includes('.gent')) {
|
|
52
|
+
console.error(chalk.red('Error: Not a gent repository'));
|
|
53
|
+
} else {
|
|
54
|
+
console.error(chalk.red('Error:'), error.message);
|
|
55
|
+
}
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function openInBrowser(url) {
|
|
61
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
62
|
+
: process.platform === 'win32' ? 'start ""'
|
|
63
|
+
: 'xdg-open';
|
|
64
|
+
exec(`${cmd} "${url}"`, (err) => {
|
|
65
|
+
if (err) {
|
|
66
|
+
console.error(chalk.yellow('Could not auto-open. URL:'));
|
|
67
|
+
console.log(url);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = web;
|