@ateriss_/aiv-cli 0.1.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/README.md +954 -0
- package/dist/index.js +318 -0
- package/package.json +41 -0
- package/src/agents/architecture.ts +32 -0
- package/src/agents/base.ts +125 -0
- package/src/agents/business.ts +32 -0
- package/src/agents/index.ts +4 -0
- package/src/agents/security.ts +33 -0
- package/src/cache/index.ts +47 -0
- package/src/cli/banner.ts +32 -0
- package/src/cli/commands/agents.ts +49 -0
- package/src/cli/commands/config.ts +426 -0
- package/src/cli/commands/context.ts +131 -0
- package/src/cli/commands/init.ts +117 -0
- package/src/cli/commands/prs.ts +118 -0
- package/src/cli/commands/review.ts +171 -0
- package/src/cli/renderer.ts +102 -0
- package/src/cli/selector.ts +78 -0
- package/src/config/index.ts +241 -0
- package/src/context/builder.ts +199 -0
- package/src/context/generator.ts +138 -0
- package/src/context/manager.ts +83 -0
- package/src/context/tree.ts +90 -0
- package/src/git/github.ts +112 -0
- package/src/git/utils.ts +51 -0
- package/src/i18n/en.ts +203 -0
- package/src/i18n/es.ts +203 -0
- package/src/i18n/index.ts +57 -0
- package/src/index.ts +29 -0
- package/src/orchestrator/index.ts +110 -0
- package/src/providers/base.ts +16 -0
- package/src/providers/claude.ts +36 -0
- package/src/providers/factory.ts +84 -0
- package/src/providers/fallback.ts +47 -0
- package/src/providers/gemini.ts +58 -0
- package/src/providers/mock.ts +27 -0
- package/src/providers/openai.ts +41 -0
- package/src/types.ts +175 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +12 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { table } from 'table';
|
|
5
|
+
import {
|
|
6
|
+
isInitialized, loadRepoConfig, saveRepoConfig,
|
|
7
|
+
loadGlobalConfig, saveGlobalConfig,
|
|
8
|
+
configPath, rulesPath, globalConfigPath,
|
|
9
|
+
addAccount, removeAccount, setDefaultAccount, setRepoAccount, listAccounts,
|
|
10
|
+
addCustomProvider, removeCustomProvider, listCustomProviders,
|
|
11
|
+
} from '../../config';
|
|
12
|
+
import { t, setLang, isSupported, SUPPORTED_LANGS } from '../../i18n';
|
|
13
|
+
import { GithubAccount } from '../../types';
|
|
14
|
+
|
|
15
|
+
export function configCommand(): Command {
|
|
16
|
+
const cmd = new Command('config').alias('cf').description('View or update aiv configuration');
|
|
17
|
+
|
|
18
|
+
// ── show ──────────────────────────────────────────────────────────────────────
|
|
19
|
+
cmd
|
|
20
|
+
.command('show')
|
|
21
|
+
.description('Show global and repo config')
|
|
22
|
+
.action(() => {
|
|
23
|
+
const tr = t();
|
|
24
|
+
console.log('\n' + chalk.bold(tr.configGlobalTitle) + '\n');
|
|
25
|
+
const gPath = globalConfigPath();
|
|
26
|
+
console.log(fs.existsSync(gPath)
|
|
27
|
+
? fs.readFileSync(gPath, 'utf8')
|
|
28
|
+
: chalk.dim(tr.configNotCreated));
|
|
29
|
+
|
|
30
|
+
if (isInitialized()) {
|
|
31
|
+
console.log(chalk.bold(tr.configRepoConfigTitle) + '\n');
|
|
32
|
+
console.log(fs.readFileSync(configPath(), 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ── set-provider ──────────────────────────────────────────────────────────────
|
|
37
|
+
cmd
|
|
38
|
+
.command('set-provider <provider>')
|
|
39
|
+
.description('Set default AI provider (claude | openai | gemini | mock | <custom-name>)')
|
|
40
|
+
.action((provider: string) => {
|
|
41
|
+
const BUILTIN = new Set(['claude', 'openai', 'gemini', 'mock']);
|
|
42
|
+
const config = loadGlobalConfig();
|
|
43
|
+
const isKnown = BUILTIN.has(provider) || provider in (config.custom_providers ?? {});
|
|
44
|
+
if (!isKnown) {
|
|
45
|
+
console.log(chalk.red(t().invalidProvider));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
config.providers.default = provider;
|
|
49
|
+
saveGlobalConfig(config);
|
|
50
|
+
console.log(chalk.green(t().configProviderSet(provider)));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ── add-provider ──────────────────────────────────────────────────────────────
|
|
54
|
+
cmd
|
|
55
|
+
.command('add-provider <name>')
|
|
56
|
+
.description('Register an AI provider. Built-in: gemini. Custom: any OpenAI-compatible endpoint.')
|
|
57
|
+
.option('--base-url <url>', 'Base URL for OpenAI-compatible providers (required for custom)')
|
|
58
|
+
.option('--api-key-env <var>', 'Env var holding the API key')
|
|
59
|
+
.option('--model <model>', 'Default model for this provider')
|
|
60
|
+
.option('--force', 'Overwrite if provider already exists')
|
|
61
|
+
.action((name: string, opts) => {
|
|
62
|
+
const BUILTIN_NAMES = new Set(['claude', 'openai', 'gemini', 'mock']);
|
|
63
|
+
if (BUILTIN_NAMES.has(name)) {
|
|
64
|
+
applyBuiltinProviderUpdate(name, opts);
|
|
65
|
+
} else {
|
|
66
|
+
applyCustomProviderAdd(name, opts);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ── remove-provider ───────────────────────────────────────────────────────────
|
|
71
|
+
cmd
|
|
72
|
+
.command('remove-provider <name>')
|
|
73
|
+
.description('Remove a custom provider from global config')
|
|
74
|
+
.action((name: string) => {
|
|
75
|
+
try {
|
|
76
|
+
removeCustomProvider(name);
|
|
77
|
+
console.log(chalk.green(t().customProviderRemoved(name)));
|
|
78
|
+
} catch {
|
|
79
|
+
console.log(chalk.red(t().customProviderNotFound(name)));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ── list-providers ────────────────────────────────────────────────────────────
|
|
84
|
+
cmd
|
|
85
|
+
.command('list-providers')
|
|
86
|
+
.description('List all configured providers (built-in and custom)')
|
|
87
|
+
.action(() => {
|
|
88
|
+
const tr = t();
|
|
89
|
+
const config = loadGlobalConfig();
|
|
90
|
+
const customs = listCustomProviders();
|
|
91
|
+
|
|
92
|
+
// Built-ins
|
|
93
|
+
console.log(chalk.bold('\n Built-in Providers\n'));
|
|
94
|
+
for (const name of ['claude', 'openai', 'gemini'] as const) {
|
|
95
|
+
const s = config[name];
|
|
96
|
+
const mark = config.providers.default === name ? chalk.green(' ✔ default') : '';
|
|
97
|
+
if (s) {
|
|
98
|
+
console.log(` ${chalk.cyan(name.padEnd(8))} model=${chalk.dim(s.model)} key_env=${chalk.dim(s.api_key_env)}${mark}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Custom
|
|
103
|
+
console.log(chalk.bold(tr.customProviderTitle));
|
|
104
|
+
if (customs.length === 0) {
|
|
105
|
+
console.log(chalk.dim(tr.customProviderNone));
|
|
106
|
+
} else {
|
|
107
|
+
const rows = customs.map(({ name, cfg, hasToken }) => [
|
|
108
|
+
config.providers.default === name ? chalk.green(name) : name,
|
|
109
|
+
chalk.dim(cfg.base_url),
|
|
110
|
+
chalk.dim(cfg.model),
|
|
111
|
+
chalk.dim(cfg.api_key_env),
|
|
112
|
+
hasToken ? chalk.green('found') : chalk.red('missing'),
|
|
113
|
+
]);
|
|
114
|
+
const header = [
|
|
115
|
+
chalk.bold(tr.customProviderColName),
|
|
116
|
+
chalk.bold(tr.customProviderColUrl),
|
|
117
|
+
chalk.bold(tr.customProviderColModel),
|
|
118
|
+
chalk.bold(tr.customProviderColEnvVar),
|
|
119
|
+
chalk.bold(tr.customProviderColToken),
|
|
120
|
+
];
|
|
121
|
+
console.log(table([header, ...rows], {
|
|
122
|
+
border: {
|
|
123
|
+
topBody: '─', topJoin: '┬', topLeft: '╭', topRight: '╮',
|
|
124
|
+
bottomBody: '─', bottomJoin: '┴', bottomLeft: '╰', bottomRight: '╯',
|
|
125
|
+
bodyLeft: '│', bodyRight: '│', bodyJoin: '│',
|
|
126
|
+
joinBody: '─', joinLeft: '├', joinRight: '┤', joinJoin: '┼',
|
|
127
|
+
},
|
|
128
|
+
columnDefault: { paddingLeft: 1, paddingRight: 1 },
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
console.log();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── set-repo ──────────────────────────────────────────────────────────────────
|
|
135
|
+
cmd
|
|
136
|
+
.command('set-repo <owner> <repo>')
|
|
137
|
+
.description('Set GitHub owner and repo in repo config')
|
|
138
|
+
.action((owner: string, repo: string) => {
|
|
139
|
+
if (!isInitialized()) { console.log(chalk.red(t().notInitialized)); return; }
|
|
140
|
+
const config = loadRepoConfig();
|
|
141
|
+
config.github ??= {};
|
|
142
|
+
config.github.owner = owner;
|
|
143
|
+
config.github.repo = repo;
|
|
144
|
+
saveRepoConfig(config);
|
|
145
|
+
console.log(chalk.green(t().configRepoSet(owner, repo)));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ── set-lang ──────────────────────────────────────────────────────────────────
|
|
149
|
+
cmd
|
|
150
|
+
.command(`set-lang <lang>`)
|
|
151
|
+
.description(`Set display language (${SUPPORTED_LANGS.join(' | ')})`)
|
|
152
|
+
.action((lang: string) => {
|
|
153
|
+
if (!isSupported(lang)) {
|
|
154
|
+
console.log(chalk.red(t().configInvalidLang));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const config = loadGlobalConfig();
|
|
158
|
+
config.lang = lang;
|
|
159
|
+
saveGlobalConfig(config);
|
|
160
|
+
setLang(lang);
|
|
161
|
+
console.log(chalk.green(t().configLangSet(lang)));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ── rules ─────────────────────────────────────────────────────────────────────
|
|
165
|
+
cmd
|
|
166
|
+
.command('rules')
|
|
167
|
+
.description('Show repo rules.yml')
|
|
168
|
+
.action(() => {
|
|
169
|
+
if (!isInitialized()) { console.log(chalk.red(t().notInitialized)); return; }
|
|
170
|
+
const tr = t();
|
|
171
|
+
console.log('\n' + chalk.bold(tr.configRulesTitle) + '\n');
|
|
172
|
+
const rPath = rulesPath();
|
|
173
|
+
console.log(fs.existsSync(rPath) ? fs.readFileSync(rPath, 'utf8') : chalk.yellow(tr.configNoRules));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── accounts ──────────────────────────────────────────────────────────────────
|
|
177
|
+
cmd
|
|
178
|
+
.command('accounts')
|
|
179
|
+
.description('List GitHub accounts in global config')
|
|
180
|
+
.action(() => {
|
|
181
|
+
const tr = t();
|
|
182
|
+
const accounts = listAccounts();
|
|
183
|
+
|
|
184
|
+
console.log(chalk.bold(tr.accountsTitle));
|
|
185
|
+
|
|
186
|
+
if (accounts.length === 0) {
|
|
187
|
+
console.log(chalk.yellow(tr.accountsNone));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const rows = accounts.map(({ name, account, isDefault, hasToken }) => [
|
|
192
|
+
isDefault ? chalk.green(name) : name,
|
|
193
|
+
isDefault ? chalk.green(tr.accountsDefaultMark) : '',
|
|
194
|
+
account.username ? chalk.dim(account.username) : chalk.dim('—'),
|
|
195
|
+
chalk.dim(account.token_env),
|
|
196
|
+
hasToken ? chalk.green(tr.accountsTokenFound) : chalk.red(tr.accountsTokenMissing),
|
|
197
|
+
chalk.dim(account.description ?? ''),
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
const header = [
|
|
201
|
+
chalk.bold(tr.accountsColName),
|
|
202
|
+
chalk.bold(tr.accountsColDefault),
|
|
203
|
+
chalk.bold(tr.accountsColUser),
|
|
204
|
+
chalk.bold(tr.accountsColEnvVar),
|
|
205
|
+
chalk.bold(tr.accountsColToken),
|
|
206
|
+
chalk.bold(tr.accountsColDesc),
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
console.log(table([header, ...rows], {
|
|
210
|
+
border: {
|
|
211
|
+
topBody: '─', topJoin: '┬', topLeft: '╭', topRight: '╮',
|
|
212
|
+
bottomBody: '─', bottomJoin: '┴', bottomLeft: '╰', bottomRight: '╯',
|
|
213
|
+
bodyLeft: '│', bodyRight: '│', bodyJoin: '│',
|
|
214
|
+
joinBody: '─', joinLeft: '├', joinRight: '┤', joinJoin: '┼',
|
|
215
|
+
},
|
|
216
|
+
columnDefault: { paddingLeft: 1, paddingRight: 1 },
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
console.log(chalk.dim(tr.accountsGlobalHint));
|
|
220
|
+
if (isInitialized()) {
|
|
221
|
+
const repoAccount = loadRepoConfig().github?.account;
|
|
222
|
+
if (repoAccount) {
|
|
223
|
+
console.log(chalk.dim(tr.configRepoAccountHint(repoAccount)));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
console.log(chalk.dim(tr.accountsRepoHint));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ── add-account ───────────────────────────────────────────────────────────────
|
|
230
|
+
cmd
|
|
231
|
+
.command('add-account <name>')
|
|
232
|
+
.description('Add a GitHub account to global config')
|
|
233
|
+
.requiredOption('--token-env <envVar>', 'Environment variable holding the GitHub token')
|
|
234
|
+
.option('--username <username>', 'GitHub username (optional, for display)')
|
|
235
|
+
.option('--description <desc>', 'Short description (optional)')
|
|
236
|
+
.option('--force', 'Overwrite if account already exists')
|
|
237
|
+
.action((name: string, opts) => {
|
|
238
|
+
const existing = listAccounts().find(a => a.name === name);
|
|
239
|
+
if (existing && !opts.force) {
|
|
240
|
+
console.log(chalk.yellow(t().accountsAlreadyExists(name)));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const account: GithubAccount = {
|
|
244
|
+
token_env: opts.tokenEnv,
|
|
245
|
+
username: opts.username,
|
|
246
|
+
description: opts.description,
|
|
247
|
+
};
|
|
248
|
+
addAccount(name, account);
|
|
249
|
+
console.log(chalk.green(t().accountsAdded(name)));
|
|
250
|
+
console.log(chalk.dim(t().configTokenEnvHint(opts.tokenEnv)));
|
|
251
|
+
if (opts.username) console.log(chalk.dim(t().configUsernameHint(opts.username)));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ── remove-account ────────────────────────────────────────────────────────────
|
|
255
|
+
cmd
|
|
256
|
+
.command('remove-account <name>')
|
|
257
|
+
.description('Remove a GitHub account from global config')
|
|
258
|
+
.action((name: string) => {
|
|
259
|
+
try {
|
|
260
|
+
removeAccount(name);
|
|
261
|
+
console.log(chalk.green(t().accountsRemoved(name)));
|
|
262
|
+
} catch {
|
|
263
|
+
console.log(chalk.red(t().accountsNotFound(name)));
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ── default-account ───────────────────────────────────────────────────────────
|
|
268
|
+
cmd
|
|
269
|
+
.command('default-account <name>')
|
|
270
|
+
.description('Set the global default GitHub account')
|
|
271
|
+
.action((name: string) => {
|
|
272
|
+
try {
|
|
273
|
+
setDefaultAccount(name);
|
|
274
|
+
console.log(chalk.green(t().accountsDefaultSet(name)));
|
|
275
|
+
} catch {
|
|
276
|
+
console.log(chalk.red(t().accountsNotFound(name)));
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ── use-account ───────────────────────────────────────────────────────────────
|
|
281
|
+
cmd
|
|
282
|
+
.command('use-account <name>')
|
|
283
|
+
.description('Use a specific GitHub account for this repo (overrides global default)')
|
|
284
|
+
.action((name: string) => {
|
|
285
|
+
if (!isInitialized()) { console.log(chalk.red(t().notInitialized)); return; }
|
|
286
|
+
try {
|
|
287
|
+
setRepoAccount(name);
|
|
288
|
+
console.log(chalk.green(t().accountsRepoSet(name)));
|
|
289
|
+
console.log(chalk.dim(t().configSavedToRepo));
|
|
290
|
+
} catch (e: any) {
|
|
291
|
+
console.log(chalk.red((e as Error).message));
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ── set-agent-provider ────────────────────────────────────────────────────────
|
|
296
|
+
cmd
|
|
297
|
+
.command('set-agent-provider <agent> [spec]')
|
|
298
|
+
.description('Assign a provider+model to a specific agent (e.g. business claude/claude-haiku-4-5)')
|
|
299
|
+
.option('--clear', 'Remove the override for this agent')
|
|
300
|
+
.action((agent: string, spec: string | undefined, opts) => {
|
|
301
|
+
const VALID_AGENTS = new Set(['business', 'architecture', 'security', 'context']);
|
|
302
|
+
if (!VALID_AGENTS.has(agent)) {
|
|
303
|
+
console.log(chalk.red(t().configInvalidAgentName(agent)));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const config = loadGlobalConfig();
|
|
308
|
+
config.providers.agents ??= {};
|
|
309
|
+
|
|
310
|
+
if (opts.clear) {
|
|
311
|
+
delete config.providers.agents[agent];
|
|
312
|
+
saveGlobalConfig(config);
|
|
313
|
+
console.log(chalk.green(t().configAgentProviderSet(agent, '(default)')));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!spec) {
|
|
318
|
+
console.log(chalk.red(t().configInvalidProviderSpec('')));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const BUILTIN_PROVIDERS = new Set(['claude', 'openai', 'gemini', 'mock']);
|
|
323
|
+
const providerName = spec.split('/')[0];
|
|
324
|
+
const knownCustom = Object.keys(loadGlobalConfig().custom_providers ?? {});
|
|
325
|
+
if (!BUILTIN_PROVIDERS.has(providerName) && !knownCustom.includes(providerName)) {
|
|
326
|
+
console.log(chalk.red(t().configInvalidProviderSpec(spec)));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
config.providers.agents[agent] = spec;
|
|
331
|
+
saveGlobalConfig(config);
|
|
332
|
+
console.log(chalk.green(t().configAgentProviderSet(agent, spec)));
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// ── set-fallback ─────────────────────────────────────────────────────────────
|
|
336
|
+
cmd
|
|
337
|
+
.command('set-fallback [providers...]')
|
|
338
|
+
.description('Set ordered fallback provider chain (e.g. openai claude). Pass no args to clear.')
|
|
339
|
+
.action((providers: string[]) => {
|
|
340
|
+
const cfg = loadGlobalConfig();
|
|
341
|
+
const VALID = new Set(['claude', 'openai', 'gemini', 'mock', ...Object.keys(cfg.custom_providers ?? {})]);
|
|
342
|
+
const invalid = providers.filter(p => !VALID.has(p));
|
|
343
|
+
if (invalid.length > 0) {
|
|
344
|
+
console.log(chalk.red(t().configInvalidProviderSpec(invalid.join(', '))));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const config = cfg;
|
|
349
|
+
config.providers.fallback = providers;
|
|
350
|
+
saveGlobalConfig(config);
|
|
351
|
+
|
|
352
|
+
if (providers.length === 0) {
|
|
353
|
+
console.log(chalk.green(t().configFallbackCleared));
|
|
354
|
+
} else {
|
|
355
|
+
console.log(chalk.green(t().configFallbackSet(providers.join(' → '))));
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ── show-agent-providers ──────────────────────────────────────────────────────
|
|
360
|
+
cmd
|
|
361
|
+
.command('show-agents')
|
|
362
|
+
.description('Show per-agent provider assignments and fallback chain')
|
|
363
|
+
.action(() => {
|
|
364
|
+
const config = loadGlobalConfig();
|
|
365
|
+
const tr = t();
|
|
366
|
+
const agents = config.providers.agents ?? {};
|
|
367
|
+
const fallback = config.providers.fallback ?? [];
|
|
368
|
+
|
|
369
|
+
console.log(chalk.bold(tr.configAgentProviderShow));
|
|
370
|
+
|
|
371
|
+
if (Object.keys(agents).length === 0) {
|
|
372
|
+
console.log(chalk.dim(tr.configAgentProviderNone));
|
|
373
|
+
} else {
|
|
374
|
+
for (const [agent, spec] of Object.entries(agents)) {
|
|
375
|
+
console.log(` ${chalk.cyan(agent.padEnd(14))} ${spec}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
console.log();
|
|
380
|
+
console.log(` ${'default'.padEnd(14)} ${chalk.dim(config.providers.default)}`);
|
|
381
|
+
|
|
382
|
+
if (fallback.length > 0) {
|
|
383
|
+
console.log(` ${'fallback'.padEnd(14)} ${chalk.dim(fallback.join(' → '))}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
console.log();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
cmd.action(() => cmd.help());
|
|
390
|
+
|
|
391
|
+
return cmd;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ─── Helpers for add-provider ─────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
function applyBuiltinProviderUpdate(name: string, opts: { model?: string; apiKeyEnv?: string }): void {
|
|
397
|
+
if (!opts.model && !opts.apiKeyEnv) {
|
|
398
|
+
console.log(chalk.yellow(' Provide --model, --api-key-env, or both to update a built-in provider.'));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const config = loadGlobalConfig();
|
|
402
|
+
const section = (config as any)[name] ?? {};
|
|
403
|
+
if (opts.model) section.model = opts.model;
|
|
404
|
+
if (opts.apiKeyEnv) section.api_key_env = opts.apiKeyEnv;
|
|
405
|
+
(config as any)[name] = section;
|
|
406
|
+
saveGlobalConfig(config);
|
|
407
|
+
console.log(chalk.green(t().builtinProviderSet(name, section.model, section.api_key_env)));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function applyCustomProviderAdd(name: string, opts: { baseUrl?: string; apiKeyEnv?: string; model?: string; force?: boolean }): void {
|
|
411
|
+
if (!opts.baseUrl) {
|
|
412
|
+
console.log(chalk.red(t().customProviderBaseUrlRequired));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const config = loadGlobalConfig();
|
|
416
|
+
if (config.custom_providers?.[name] && !opts.force) {
|
|
417
|
+
console.log(chalk.yellow(t().customProviderAlreadyExists(name)));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
addCustomProvider(name, {
|
|
421
|
+
base_url: opts.baseUrl,
|
|
422
|
+
api_key_env: opts.apiKeyEnv ?? `${name.toUpperCase()}_API_KEY`,
|
|
423
|
+
model: opts.model ?? 'unknown',
|
|
424
|
+
});
|
|
425
|
+
console.log(chalk.green(t().customProviderAdded(name)));
|
|
426
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { isInitialized, contextPath, rulesPath, resolveConfig } from '../../config';
|
|
6
|
+
import { refreshContextFiles } from '../../context/manager';
|
|
7
|
+
import { generateContextAndRules } from '../../context/generator';
|
|
8
|
+
import { createProviderFor } from '../../providers/factory';
|
|
9
|
+
import { t } from '../../i18n';
|
|
10
|
+
|
|
11
|
+
export function contextCommand(): Command {
|
|
12
|
+
const cmd = new Command('context').alias('ctx').description('Manage local project context');
|
|
13
|
+
|
|
14
|
+
// ── refresh ──────────────────────────────────────────────────────────────────
|
|
15
|
+
cmd
|
|
16
|
+
.command('refresh')
|
|
17
|
+
.description('Rebuild project context and tree from current codebase')
|
|
18
|
+
.action(async () => {
|
|
19
|
+
if (!isInitialized()) {
|
|
20
|
+
console.log(chalk.red(t().notInitialized));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(chalk.bold(t().contextRefreshTitle));
|
|
25
|
+
|
|
26
|
+
const treeSpinner = ora(t().contextScanningTree).start();
|
|
27
|
+
const ctxSpinner = ora(t().contextRebuildingCtx);
|
|
28
|
+
|
|
29
|
+
const { treeOk, contextOk } = await refreshContextFiles(process.cwd());
|
|
30
|
+
|
|
31
|
+
if (treeOk) {
|
|
32
|
+
treeSpinner.succeed(chalk.green(t().contextTreeUpdated));
|
|
33
|
+
} else {
|
|
34
|
+
treeSpinner.fail(t().contextTreeFailed('scan failed'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ctxSpinner.start();
|
|
38
|
+
if (contextOk) {
|
|
39
|
+
ctxSpinner.succeed(chalk.green(t().contextCtxUpdated));
|
|
40
|
+
} else {
|
|
41
|
+
ctxSpinner.fail(t().contextCtxFailed('analysis failed'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(chalk.dim(t().contextEditHint(chalk.cyan('.aiv/context.md'))));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ── show ──────────────────────────────────────────────────────────────────────
|
|
48
|
+
cmd
|
|
49
|
+
.command('show')
|
|
50
|
+
.description('Show current context.md contents')
|
|
51
|
+
.action(() => {
|
|
52
|
+
if (!isInitialized()) {
|
|
53
|
+
console.log(chalk.red(t().notInitialized));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const file = contextPath();
|
|
57
|
+
if (!fs.existsSync(file)) {
|
|
58
|
+
console.log(chalk.yellow(t().contextNoFile));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.log('\n' + fs.readFileSync(file, 'utf8'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ── generate ─────────────────────────────────────────────────────────────────
|
|
65
|
+
cmd
|
|
66
|
+
.command('generate')
|
|
67
|
+
.description('Use AI to generate context.md and rules.yml from the current codebase')
|
|
68
|
+
.option('--context-only', 'Generate only context.md')
|
|
69
|
+
.option('--rules-only', 'Generate only rules.yml')
|
|
70
|
+
.option('--force', 'Overwrite existing files without asking')
|
|
71
|
+
.action(async (opts) => {
|
|
72
|
+
if (!isInitialized()) {
|
|
73
|
+
console.log(chalk.red(t().notInitialized));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const config = resolveConfig();
|
|
78
|
+
let provider: ReturnType<typeof createProviderFor>;
|
|
79
|
+
try {
|
|
80
|
+
provider = createProviderFor(config, 'context');
|
|
81
|
+
} catch (e: any) {
|
|
82
|
+
console.log(chalk.red(t().contextGenerateProviderError(e.message)));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(chalk.bold(t().contextGenerateTitle));
|
|
87
|
+
|
|
88
|
+
const spinner = ora(t().contextGenerating).start();
|
|
89
|
+
let generated: { context: string; rules: string };
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
generated = await generateContextAndRules(process.cwd(), provider);
|
|
93
|
+
spinner.succeed(chalk.green(t().contextGenerateDone));
|
|
94
|
+
} catch (e: any) {
|
|
95
|
+
spinner.fail(chalk.red(t().contextGenerateFailed(e.message)));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const writeContext = !opts.rulesOnly;
|
|
100
|
+
const writeRules = !opts.contextOnly;
|
|
101
|
+
|
|
102
|
+
if (writeContext) {
|
|
103
|
+
await writeWithConfirm(contextPath(), generated.context, '.aiv/context.md', opts.force);
|
|
104
|
+
}
|
|
105
|
+
if (writeRules) {
|
|
106
|
+
await writeWithConfirm(rulesPath(), generated.rules, '.aiv/rules.yml', opts.force);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(chalk.dim(t().contextEditHint(chalk.cyan('.aiv/context.md'))));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return cmd;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function writeWithConfirm(filePath: string, content: string, label: string, force: boolean): Promise<void> {
|
|
116
|
+
if (fs.existsSync(filePath) && !force) {
|
|
117
|
+
const { default: inquirer } = await import('inquirer') as any;
|
|
118
|
+
const { ok } = await inquirer.prompt([{
|
|
119
|
+
type: 'confirm',
|
|
120
|
+
name: 'ok',
|
|
121
|
+
message: t().contextGenerateConfirmOverwrite(label),
|
|
122
|
+
default: true,
|
|
123
|
+
}]);
|
|
124
|
+
if (!ok) {
|
|
125
|
+
console.log(chalk.dim(t().contextGenerateSkipped(label)));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
130
|
+
console.log(chalk.green(t().contextGenerateWritten(label)));
|
|
131
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import {
|
|
7
|
+
getAivDir, saveGlobalConfig, saveRepoConfig, saveRules,
|
|
8
|
+
contextPath, treePath, isGlobalSetup,
|
|
9
|
+
DEFAULT_GLOBAL_CONFIG, DEFAULT_REPO_CONFIG, DEFAULT_RULES, isInitialized,
|
|
10
|
+
} from '../../config';
|
|
11
|
+
import { buildContext } from '../../context/builder';
|
|
12
|
+
import { buildTree } from '../../context/tree';
|
|
13
|
+
import { appendGitignore } from '../../git/utils';
|
|
14
|
+
import { t } from '../../i18n';
|
|
15
|
+
|
|
16
|
+
export function initCommand(): Command {
|
|
17
|
+
return new Command('init')
|
|
18
|
+
.alias('i')
|
|
19
|
+
.description('Initialize aiv in the current repository')
|
|
20
|
+
.option('--force', 'Reinitialize even if already initialized')
|
|
21
|
+
.action(async (opts) => {
|
|
22
|
+
const cwd = process.cwd();
|
|
23
|
+
|
|
24
|
+
if (isInitialized(cwd) && !opts.force) {
|
|
25
|
+
console.log(chalk.yellow(t().initAlreadyDone));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(chalk.bold(t().initTitle));
|
|
30
|
+
|
|
31
|
+
// 1. Global config (~/.aiv/config.yml) — created once, not on every init
|
|
32
|
+
if (isGlobalSetup()) {
|
|
33
|
+
console.log(chalk.dim(` ↳ ${t().initGlobalConfigExists}`));
|
|
34
|
+
} else {
|
|
35
|
+
const gSpinner = ora(t().initWritingGlobalConfig).start();
|
|
36
|
+
saveGlobalConfig(DEFAULT_GLOBAL_CONFIG);
|
|
37
|
+
gSpinner.succeed(chalk.green(t().initGlobalConfigCreated));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Repo .aiv/ folder
|
|
41
|
+
const aivDir = getAivDir(cwd);
|
|
42
|
+
if (!fs.existsSync(aivDir)) fs.mkdirSync(aivDir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
const configSpinner = ora(t().initWritingConfig).start();
|
|
45
|
+
saveRepoConfig(DEFAULT_REPO_CONFIG, cwd);
|
|
46
|
+
configSpinner.succeed(chalk.green(t().initConfigCreated));
|
|
47
|
+
|
|
48
|
+
// 3. rules.yml
|
|
49
|
+
saveRules(DEFAULT_RULES, cwd);
|
|
50
|
+
console.log(chalk.green(` ✔ ${t().initRulesCreated}`));
|
|
51
|
+
|
|
52
|
+
// 4. tree.json
|
|
53
|
+
const treeSpinner = ora(t().initScanningTree).start();
|
|
54
|
+
try {
|
|
55
|
+
const tree = await buildTree(cwd);
|
|
56
|
+
fs.writeFileSync(treePath(cwd), JSON.stringify(tree, null, 2), 'utf8');
|
|
57
|
+
treeSpinner.succeed(chalk.green(t().initTreeCreated));
|
|
58
|
+
} catch {
|
|
59
|
+
treeSpinner.warn(t().initTreeSkipped);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 5. context.md
|
|
63
|
+
const ctxSpinner = ora(t().initBuildingContext).start();
|
|
64
|
+
try {
|
|
65
|
+
const context = await buildContext(cwd);
|
|
66
|
+
fs.writeFileSync(contextPath(cwd), context, 'utf8');
|
|
67
|
+
ctxSpinner.succeed(chalk.green(t().initContextCreated));
|
|
68
|
+
} catch {
|
|
69
|
+
ctxSpinner.warn(t().initContextSkipped);
|
|
70
|
+
fs.writeFileSync(contextPath(cwd), getDefaultContext(cwd), 'utf8');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 6. .gitignore
|
|
74
|
+
appendGitignore(cwd);
|
|
75
|
+
console.log(chalk.green(` ✔ ${t().initGitignoreUpdated}`));
|
|
76
|
+
|
|
77
|
+
const apiKeyEnv = DEFAULT_GLOBAL_CONFIG.claude?.api_key_env ?? 'CLAUDE_API_KEY';
|
|
78
|
+
const tokenEnv = DEFAULT_GLOBAL_CONFIG.github.accounts['default']?.token_env ?? 'GITHUB_TOKEN';
|
|
79
|
+
|
|
80
|
+
console.log(chalk.bold.green(t().initSuccessTitle));
|
|
81
|
+
console.log(` ${chalk.cyan('1.')} ${t().initStep1(apiKeyEnv)}`);
|
|
82
|
+
console.log(` ${chalk.cyan('2.')} ${t().initStep2(tokenEnv)}`);
|
|
83
|
+
console.log(` ${chalk.cyan('3.')} ${t().initStep3}`);
|
|
84
|
+
console.log(` ${chalk.cyan('4.')} ${t().initStep4}\n`);
|
|
85
|
+
console.log(` ${t().initEditContext(chalk.cyan('.aiv/context.md'))}`);
|
|
86
|
+
console.log(` ${t().initEditRules(chalk.cyan('.aiv/rules.yml'))}\n`);
|
|
87
|
+
console.log(chalk.dim(t().initGlobalHint));
|
|
88
|
+
console.log(chalk.dim(t().initAddAccountHint + '\n'));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getDefaultContext(cwd: string): string {
|
|
93
|
+
const name = path.basename(cwd);
|
|
94
|
+
return `# Project Context: ${name}
|
|
95
|
+
|
|
96
|
+
## Architecture
|
|
97
|
+
(Describe your system architecture here)
|
|
98
|
+
|
|
99
|
+
## Modules
|
|
100
|
+
(List main modules and their responsibilities)
|
|
101
|
+
|
|
102
|
+
## Technologies
|
|
103
|
+
(List key technologies and frameworks)
|
|
104
|
+
|
|
105
|
+
## Critical Dependencies
|
|
106
|
+
(List dependencies that are critical to the system)
|
|
107
|
+
|
|
108
|
+
## Sensitive Zones
|
|
109
|
+
(List sensitive areas: auth, payments, data access, etc.)
|
|
110
|
+
|
|
111
|
+
## Business Rules
|
|
112
|
+
(Describe key business rules the AI should be aware of)
|
|
113
|
+
|
|
114
|
+
## System Summary
|
|
115
|
+
(High-level summary of what this system does)
|
|
116
|
+
`;
|
|
117
|
+
}
|