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,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env Loader - Load .env files into process.env before commands run.
|
|
3
|
+
*
|
|
4
|
+
* Precedence (lower wins — does NOT clobber existing env):
|
|
5
|
+
* 1. process.env (real shell vars) ← highest, untouched
|
|
6
|
+
* 2. <cwd>/.env ← project-local
|
|
7
|
+
* 3. ~/.gent/.env ← user-global
|
|
8
|
+
*
|
|
9
|
+
* No new dependency: a tiny KEY=VALUE parser (supports quoted values + comments).
|
|
10
|
+
* Silently no-ops if files are missing or malformed.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const { GENT_DIR } = require('./constants');
|
|
17
|
+
|
|
18
|
+
function parse(content) {
|
|
19
|
+
const out = {};
|
|
20
|
+
const lines = content.split(/\r?\n/);
|
|
21
|
+
for (const raw of lines) {
|
|
22
|
+
const line = raw.trim();
|
|
23
|
+
if (!line || line.startsWith('#')) continue;
|
|
24
|
+
const eq = line.indexOf('=');
|
|
25
|
+
if (eq < 1) continue;
|
|
26
|
+
const key = line.slice(0, eq).trim();
|
|
27
|
+
let value = line.slice(eq + 1).trim();
|
|
28
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
29
|
+
|
|
30
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
31
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
32
|
+
value = value.slice(1, -1);
|
|
33
|
+
} else {
|
|
34
|
+
const hash = value.indexOf(' #');
|
|
35
|
+
if (hash !== -1) value = value.slice(0, hash).trim();
|
|
36
|
+
}
|
|
37
|
+
out[key] = value;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadOne(filePath) {
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
45
|
+
const parsed = parse(content);
|
|
46
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
47
|
+
if (process.env[k] === undefined) {
|
|
48
|
+
process.env[k] = v;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Missing or unreadable — fine.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function load() {
|
|
57
|
+
loadOne(path.join(process.cwd(), '.env'));
|
|
58
|
+
loadOne(path.join(os.homedir(), GENT_DIR, '.env'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { load, parse };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* User Config - Global per-user CLI settings (~/.gent/config.json)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Persist CLI-wide settings that should NOT live in a project's .gent/ dir:
|
|
8
|
+
* - AI key, AI model
|
|
9
|
+
* - API base URL (so users can point at a local backend without code edits)
|
|
10
|
+
* - Default identity (name/email) used when project .gent/config.json lacks one
|
|
11
|
+
*
|
|
12
|
+
* RESOLUTION ORDER (used by getResolved):
|
|
13
|
+
* env var > ~/.gent/config.json > built-in default
|
|
14
|
+
*
|
|
15
|
+
* STORAGE:
|
|
16
|
+
* Plain JSON at ~/.gent/config.json. The AI key field is lightly obfuscated
|
|
17
|
+
* (AES via crypto-js, same scheme as auth-storage) so it isn't readable at a
|
|
18
|
+
* glance — not a real secret store, but better than plaintext on disk.
|
|
19
|
+
*
|
|
20
|
+
* KEYS (dot-notation):
|
|
21
|
+
* ai.api_key Anthropic API key
|
|
22
|
+
* ai.model Model id (e.g. claude-opus-4-7, claude-haiku-4-5)
|
|
23
|
+
* api.base_url Backend base URL (e.g. http://localhost:8000)
|
|
24
|
+
* user.name Default author name
|
|
25
|
+
* user.email Default author email
|
|
26
|
+
*
|
|
27
|
+
* ============================================================================
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('fs').promises;
|
|
31
|
+
const path = require('path');
|
|
32
|
+
const os = require('os');
|
|
33
|
+
const CryptoJS = require('crypto-js');
|
|
34
|
+
const { GENT_DIR } = require('./constants');
|
|
35
|
+
|
|
36
|
+
const CONFIG_FILE_NAME = 'cli-config.json';
|
|
37
|
+
const SECRET_KEYS = new Set(['ai.api_key']);
|
|
38
|
+
const OBFUSCATION_KEY = 'gent-cli-config-v1';
|
|
39
|
+
const OBFUSCATION_PREFIX = 'enc:v1:';
|
|
40
|
+
|
|
41
|
+
const ALLOWED_KEYS = new Set([
|
|
42
|
+
'ai.api_key',
|
|
43
|
+
'ai.model',
|
|
44
|
+
'api.base_url',
|
|
45
|
+
'user.name',
|
|
46
|
+
'user.email',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const DEFAULTS = {
|
|
50
|
+
'ai.model': 'claude-opus-4-7',
|
|
51
|
+
'api.base_url': 'https://gent-api.onrender.com',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const ENV_OVERRIDES = {
|
|
55
|
+
'ai.api_key': 'ANTHROPIC_API_KEY',
|
|
56
|
+
'ai.model': 'GENT_AI_MODEL',
|
|
57
|
+
'api.base_url': 'GENT_API_URL',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function getConfigPath() {
|
|
61
|
+
return path.join(os.homedir(), GENT_DIR, CONFIG_FILE_NAME);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function obfuscate(plaintext) {
|
|
65
|
+
if (typeof plaintext !== 'string' || plaintext.length === 0) return plaintext;
|
|
66
|
+
return OBFUSCATION_PREFIX + CryptoJS.AES.encrypt(plaintext, OBFUSCATION_KEY).toString();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function deobfuscate(value) {
|
|
70
|
+
if (typeof value !== 'string' || !value.startsWith(OBFUSCATION_PREFIX)) return value;
|
|
71
|
+
try {
|
|
72
|
+
const bytes = CryptoJS.AES.decrypt(value.slice(OBFUSCATION_PREFIX.length), OBFUSCATION_KEY);
|
|
73
|
+
return bytes.toString(CryptoJS.enc.Utf8);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setDeep(obj, dottedKey, value) {
|
|
80
|
+
const parts = dottedKey.split('.');
|
|
81
|
+
let cur = obj;
|
|
82
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
83
|
+
if (typeof cur[parts[i]] !== 'object' || cur[parts[i]] === null) {
|
|
84
|
+
cur[parts[i]] = {};
|
|
85
|
+
}
|
|
86
|
+
cur = cur[parts[i]];
|
|
87
|
+
}
|
|
88
|
+
cur[parts[parts.length - 1]] = value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getDeep(obj, dottedKey) {
|
|
92
|
+
const parts = dottedKey.split('.');
|
|
93
|
+
let cur = obj;
|
|
94
|
+
for (const p of parts) {
|
|
95
|
+
if (!cur || typeof cur !== 'object') return undefined;
|
|
96
|
+
cur = cur[p];
|
|
97
|
+
}
|
|
98
|
+
return cur;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function unsetDeep(obj, dottedKey) {
|
|
102
|
+
const parts = dottedKey.split('.');
|
|
103
|
+
let cur = obj;
|
|
104
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
105
|
+
if (!cur || typeof cur[parts[i]] !== 'object') return;
|
|
106
|
+
cur = cur[parts[i]];
|
|
107
|
+
}
|
|
108
|
+
delete cur[parts[parts.length - 1]];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function readRaw() {
|
|
112
|
+
try {
|
|
113
|
+
const raw = await fs.readFile(getConfigPath(), 'utf-8');
|
|
114
|
+
return JSON.parse(raw);
|
|
115
|
+
} catch {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function writeRaw(data) {
|
|
121
|
+
const dir = path.dirname(getConfigPath());
|
|
122
|
+
await fs.mkdir(dir, { recursive: true });
|
|
123
|
+
await fs.writeFile(getConfigPath(), JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isAllowedKey(key) {
|
|
127
|
+
return ALLOWED_KEYS.has(key);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function listAllowedKeys() {
|
|
131
|
+
return Array.from(ALLOWED_KEYS);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get raw stored value (decoded if secret). Does NOT consult env or defaults.
|
|
136
|
+
*/
|
|
137
|
+
async function get(key) {
|
|
138
|
+
if (!isAllowedKey(key)) throw new Error(`Unknown config key: ${key}`);
|
|
139
|
+
const data = await readRaw();
|
|
140
|
+
const raw = getDeep(data, key);
|
|
141
|
+
if (raw === undefined || raw === null) return undefined;
|
|
142
|
+
if (SECRET_KEYS.has(key)) return deobfuscate(raw);
|
|
143
|
+
return raw;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolve a config value using: env > stored > default.
|
|
148
|
+
* Returns { value, source } where source is 'env' | 'config' | 'default' | 'unset'.
|
|
149
|
+
*/
|
|
150
|
+
async function getResolved(key) {
|
|
151
|
+
if (!isAllowedKey(key)) throw new Error(`Unknown config key: ${key}`);
|
|
152
|
+
const envName = ENV_OVERRIDES[key];
|
|
153
|
+
if (envName && process.env[envName]) {
|
|
154
|
+
return { value: process.env[envName], source: 'env', envName };
|
|
155
|
+
}
|
|
156
|
+
const stored = await get(key);
|
|
157
|
+
if (stored !== undefined && stored !== null && stored !== '') {
|
|
158
|
+
return { value: stored, source: 'config' };
|
|
159
|
+
}
|
|
160
|
+
if (DEFAULTS[key] !== undefined) {
|
|
161
|
+
return { value: DEFAULTS[key], source: 'default' };
|
|
162
|
+
}
|
|
163
|
+
return { value: undefined, source: 'unset' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function set(key, value) {
|
|
167
|
+
if (!isAllowedKey(key)) {
|
|
168
|
+
throw new Error(`Unknown config key '${key}'. Allowed: ${listAllowedKeys().join(', ')}`);
|
|
169
|
+
}
|
|
170
|
+
if (typeof value !== 'string') value = String(value);
|
|
171
|
+
const data = await readRaw();
|
|
172
|
+
const toStore = SECRET_KEYS.has(key) ? obfuscate(value) : value;
|
|
173
|
+
setDeep(data, key, toStore);
|
|
174
|
+
await writeRaw(data);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function unset(key) {
|
|
178
|
+
if (!isAllowedKey(key)) throw new Error(`Unknown config key: ${key}`);
|
|
179
|
+
const data = await readRaw();
|
|
180
|
+
unsetDeep(data, key);
|
|
181
|
+
await writeRaw(data);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Return all stored values (secrets masked) plus their resolved value/source.
|
|
186
|
+
*/
|
|
187
|
+
async function listAll() {
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const key of listAllowedKeys()) {
|
|
190
|
+
const resolved = await getResolved(key);
|
|
191
|
+
const isSecret = SECRET_KEYS.has(key);
|
|
192
|
+
const display = isSecret && resolved.value
|
|
193
|
+
? maskSecret(resolved.value)
|
|
194
|
+
: resolved.value;
|
|
195
|
+
out.push({
|
|
196
|
+
key,
|
|
197
|
+
value: display,
|
|
198
|
+
rawValue: resolved.value,
|
|
199
|
+
source: resolved.source,
|
|
200
|
+
envName: resolved.envName,
|
|
201
|
+
isSecret,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function maskSecret(s) {
|
|
208
|
+
if (!s || typeof s !== 'string') return s;
|
|
209
|
+
if (s.length <= 12) return '****';
|
|
210
|
+
return s.slice(0, 8) + '...' + s.slice(-4);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
get,
|
|
215
|
+
set,
|
|
216
|
+
unset,
|
|
217
|
+
getResolved,
|
|
218
|
+
listAll,
|
|
219
|
+
isAllowedKey,
|
|
220
|
+
listAllowedKeys,
|
|
221
|
+
getConfigPath,
|
|
222
|
+
maskSecret,
|
|
223
|
+
ENV_OVERRIDES,
|
|
224
|
+
DEFAULTS,
|
|
225
|
+
};
|