@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.
@@ -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
+ }