@gitset-dev/cli 1.2.1 → 2.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.
Files changed (41) hide show
  1. package/{LICENSE.md → LICENSE} +1 -1
  2. package/README.md +80 -159
  3. package/index.js +230 -0
  4. package/lib/ai/capabilities.js +81 -0
  5. package/lib/ai/errors.js +115 -0
  6. package/lib/ai/index.js +98 -0
  7. package/lib/ai/providers/anthropic.js +62 -0
  8. package/lib/ai/providers/gemini.js +77 -0
  9. package/lib/ai/providers/mock.js +41 -0
  10. package/lib/ai/providers/openai-compatible.js +72 -0
  11. package/lib/ai/retry.js +31 -0
  12. package/lib/cli-commit.js +143 -0
  13. package/lib/cli-config.js +207 -0
  14. package/lib/cli-issue.js +144 -0
  15. package/lib/cli-pr.js +168 -0
  16. package/lib/cli-readme.js +153 -0
  17. package/lib/commit-local.js +67 -0
  18. package/lib/config.js +134 -0
  19. package/lib/generate-local.js +59 -0
  20. package/lib/labels-ai.js +41 -0
  21. package/lib/prompts/defaults.js +191 -0
  22. package/lib/prompts/index.js +79 -0
  23. package/lib/setup-wizard.js +115 -0
  24. package/npm-shrinkwrap.json +566 -0
  25. package/package.json +35 -35
  26. package/src/commands/dependabot-resolver.js +314 -0
  27. package/src/commands/gitignore.js +110 -0
  28. package/src/commands/init.js +38 -0
  29. package/src/commands/labelspack.js +94 -0
  30. package/src/commands/license.js +208 -0
  31. package/src/commands/release.js +180 -0
  32. package/src/commands/repo.js +176 -0
  33. package/src/commands/status.js +56 -0
  34. package/src/commands/template.js +92 -0
  35. package/src/commands/tree.js +77 -0
  36. package/src/utils/dependabot-analyzer.js +101 -0
  37. package/src/utils/labels.js +102 -0
  38. package/src/utils/theme.js +70 -0
  39. package/src/utils/ui.js +173 -0
  40. package/.github/workflows/npm_publish.yml +0 -38
  41. package/src/index.js +0 -505
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ const CODES = Object.freeze({
4
+ AUTH: 'auth',
5
+ RATE_LIMIT: 'rate_limit',
6
+ CONTEXT_LENGTH: 'context_length',
7
+ TIMEOUT: 'timeout',
8
+ NETWORK: 'network',
9
+ BAD_REQUEST: 'bad_request',
10
+ PROVIDER_ERROR: 'provider_error',
11
+ UNSUPPORTED: 'unsupported',
12
+ });
13
+
14
+ const RETRYABLE = new Set([
15
+ CODES.RATE_LIMIT,
16
+ CODES.NETWORK,
17
+ CODES.TIMEOUT,
18
+ CODES.PROVIDER_ERROR,
19
+ ]);
20
+
21
+ class AIError extends Error {
22
+
23
+ constructor(code, message, opts = {}) {
24
+ super(message);
25
+ this.name = 'AIError';
26
+ this.code = code;
27
+ this.provider = opts.provider || null;
28
+ this.status = opts.status ?? null;
29
+ this.retryAfterMs = opts.retryAfterMs ?? null;
30
+
31
+ this.retryable = opts.retryable ?? RETRYABLE.has(code);
32
+ if (opts.cause !== undefined) this.cause = opts.cause;
33
+ }
34
+
35
+ toJSON() {
36
+ return {
37
+ error: true,
38
+ code: this.code,
39
+ message: this.message,
40
+ provider: this.provider,
41
+ retryable: this.retryable,
42
+ };
43
+ }
44
+ }
45
+
46
+ function normalizeError(err, provider) {
47
+ if (err instanceof AIError) return err;
48
+
49
+ const status = err?.status ?? err?.statusCode ?? err?.response?.status ?? null;
50
+ const sysCode = err?.code || err?.cause?.code || null;
51
+ const msg = String(err?.message || err || 'Unknown AI provider error');
52
+
53
+ if (err?.name === 'AbortError' || sysCode === 'ABORT_ERR') {
54
+ return new AIError(CODES.TIMEOUT, 'AI request timed out or was aborted.', { provider, cause: err });
55
+ }
56
+ if (['ENOTFOUND', 'ECONNRESET', 'ECONNREFUSED', 'EAI_AGAIN', 'UND_ERR_SOCKET'].includes(sysCode)) {
57
+ return new AIError(CODES.NETWORK, `Could not reach the ${provider} API.`, { provider, cause: err });
58
+ }
59
+ if (status === 401 || status === 403) {
60
+ const detail = upstreamDetail(err);
61
+ const base = `The ${provider} API key was rejected (HTTP ${status}). Check the key and model access.`;
62
+ return new AIError(CODES.AUTH, detail ? `${base} ${detail}` : base, { provider, status, cause: err });
63
+ }
64
+ if (status === 429) {
65
+ const retryAfterMs = parseRetryAfter(err);
66
+ const detail = upstreamDetail(err);
67
+
68
+ const hardLimit = /credit|billing|prepay|plan and billing|exceeded your current quota|quota.*(exhausted|depleted)|insufficient|payment/i.test(detail);
69
+ const message = detail
70
+ ? `${provider}: ${detail}`
71
+ : `Rate limited by ${provider}. This is your provider account's limit.`;
72
+ return new AIError(CODES.RATE_LIMIT, message, { provider, status, retryAfterMs, retryable: !hardLimit, cause: err });
73
+ }
74
+ if (status === 400 || status === 422) {
75
+ if (/context length|too long|maximum.*tokens|max_tokens|token limit/i.test(msg)) {
76
+ return new AIError(CODES.CONTEXT_LENGTH, 'The input is too large for this model\'s context window.', { provider, status, cause: err });
77
+ }
78
+ return new AIError(CODES.BAD_REQUEST, `${provider} rejected the request: ${msg}`, { provider, status, cause: err });
79
+ }
80
+ if (status === 408 || status === 504) {
81
+ return new AIError(CODES.TIMEOUT, `${provider} timed out.`, { provider, status, cause: err });
82
+ }
83
+ if (status != null && status >= 500) {
84
+
85
+ const detail = upstreamDetail(err);
86
+ const base = `${provider} is unavailable (HTTP ${status}).`;
87
+ return new AIError(CODES.PROVIDER_ERROR, detail ? `${base} ${detail}` : base, { provider, status, cause: err });
88
+ }
89
+
90
+ const detail = upstreamDetail(err);
91
+ return new AIError(CODES.PROVIDER_ERROR, `${provider} error: ${detail || msg}`, { provider, status, cause: err });
92
+ }
93
+
94
+ function upstreamDetail(err) {
95
+ const raw = String(err?.message || '').trim();
96
+ if (!raw) return '';
97
+ const brace = raw.indexOf('{');
98
+ if (brace !== -1) {
99
+ try {
100
+ const j = JSON.parse(raw.slice(brace));
101
+ const m = j?.error?.message || j?.message;
102
+ if (m) return String(m).trim().slice(0, 300);
103
+ } catch { }
104
+ }
105
+ return raw.replace(/^got status:\s*\d*\s*/i, '').slice(0, 300);
106
+ }
107
+
108
+ function parseRetryAfter(err) {
109
+ const h = err?.headers?.get?.('retry-after') ?? err?.response?.headers?.['retry-after'];
110
+ if (!h) return null;
111
+ const secs = Number(h);
112
+ return Number.isFinite(secs) ? secs * 1000 : null;
113
+ }
114
+
115
+ module.exports = { AIError, CODES, normalizeError };
@@ -0,0 +1,98 @@
1
+ // VENDORED from gitset-core-v2/lib/ai — do not edit here. Run `pnpm sync:ai`.
2
+ 'use strict';
3
+
4
+ const { AIError, CODES, normalizeError } = require('./errors');
5
+ const { withRetry } = require('./retry');
6
+ const { PROVIDERS, SUPPORTED, FORBIDDEN_MODELS, isForbiddenModel, estimateTokens } = require('./capabilities');
7
+
8
+ const ADAPTERS = {
9
+ anthropic: () => require('./providers/anthropic'),
10
+ openai: () => require('./providers/openai-compatible'),
11
+ openrouter: () => require('./providers/openai-compatible'),
12
+ deepseek: () => require('./providers/openai-compatible'),
13
+ gemini: () => require('./providers/gemini'),
14
+ mock: () => require('./providers/mock'),
15
+ };
16
+
17
+ function createProvider(cfg = {}) {
18
+ const provider = String(cfg.provider || '').toLowerCase();
19
+ const meta = PROVIDERS[provider];
20
+ if (!meta) {
21
+ throw new AIError(CODES.UNSUPPORTED, `Unknown provider "${cfg.provider}". Supported: ${SUPPORTED.join(', ')}.`, { provider });
22
+ }
23
+ if (provider !== 'mock' && !cfg.apiKey) {
24
+ throw new AIError(CODES.AUTH, `No API key supplied for ${meta.label}. Gitset is BYOAI — add your own key.`, { provider });
25
+ }
26
+
27
+ const model = cfg.model || meta.defaultModel;
28
+ if (isForbiddenModel(model)) {
29
+ throw new AIError(
30
+ CODES.UNSUPPORTED,
31
+ `Model "${model}" can't be used with Gitset. Pick another model for ${meta.label}.`,
32
+ { provider },
33
+ );
34
+ }
35
+ const baseURL = cfg.baseURL || meta.baseURL;
36
+ const timeoutMs = cfg.timeoutMs ?? 60_000;
37
+ const retry = cfg.retry || {};
38
+
39
+ const adapter = ADAPTERS[provider]().create({ apiKey: cfg.apiKey, model, baseURL, timeoutMs });
40
+
41
+ function preflight(request) {
42
+ if (!request || !Array.isArray(request.messages) || request.messages.length === 0) {
43
+ throw new AIError(CODES.BAD_REQUEST, 'request.messages must be a non-empty array.', { provider });
44
+ }
45
+ if (isForbiddenModel(request.model)) {
46
+ throw new AIError(
47
+ CODES.UNSUPPORTED,
48
+ `Model "${request.model}" can't be used with Gitset. Pick another model for ${meta.label}.`,
49
+ { provider },
50
+ );
51
+ }
52
+ const approx = estimateTokens(
53
+ (request.system || '') + request.messages.map((m) => m.content || '').join('\n'),
54
+ ) + (request.maxTokens || 0);
55
+ if (approx > meta.contextFloor * 1.5) {
56
+ throw new AIError(
57
+ CODES.CONTEXT_LENGTH,
58
+ `Input (~${approx} tokens) likely exceeds ${meta.label}'s context window.`,
59
+ { provider },
60
+ );
61
+ }
62
+ }
63
+
64
+ return {
65
+ provider,
66
+ model,
67
+ capabilities: {
68
+ streaming: meta.streaming,
69
+ jsonMode: meta.jsonMode,
70
+ maxContext: meta.contextFloor,
71
+ label: meta.label,
72
+ },
73
+
74
+ async generate(request) {
75
+ preflight(request);
76
+ return withRetry(() => adapter.generate(request), { provider, signal: request.signal, ...retry });
77
+ },
78
+
79
+ async *stream(request) {
80
+ preflight(request);
81
+ if (!meta.streaming) {
82
+ const r = await this.generate(request);
83
+ yield { done: true, text: r.text, usage: r.usage, finishReason: r.finishReason };
84
+ return;
85
+ }
86
+ try {
87
+ yield* adapter.stream(request);
88
+ } catch (err) {
89
+ throw normalizeError(err, provider);
90
+ }
91
+ },
92
+ };
93
+ }
94
+
95
+ module.exports = {
96
+ createProvider, AIError, CODES, PROVIDERS, SUPPORTED,
97
+ FORBIDDEN_MODELS, isForbiddenModel, estimateTokens,
98
+ };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ function create({ apiKey, model, timeoutMs = 60_000 }) {
4
+ let client;
5
+ function getClient() {
6
+ if (client) return client;
7
+ const Anthropic = require('@anthropic-ai/sdk');
8
+ client = new Anthropic({ apiKey, timeout: timeoutMs, maxRetries: 0 });
9
+ return client;
10
+ }
11
+
12
+ function buildParams(req) {
13
+ return {
14
+ model: req.model || model,
15
+ max_tokens: req.maxTokens ?? 4096,
16
+ temperature: req.temperature ?? 0.7,
17
+ ...(req.system ? { system: req.system } : {}),
18
+ messages: (req.messages || []).map((m) => ({
19
+ role: m.role === 'assistant' ? 'assistant' : 'user',
20
+ content: m.content,
21
+ })),
22
+ };
23
+ }
24
+
25
+ return {
26
+ name: 'anthropic',
27
+
28
+ async generate(req) {
29
+ const msg = await getClient().messages.create(buildParams(req), { signal: req.signal });
30
+ const text = (msg.content || [])
31
+ .filter((b) => b.type === 'text')
32
+ .map((b) => b.text)
33
+ .join('');
34
+ return {
35
+ text,
36
+ usage: { inputTokens: msg.usage?.input_tokens ?? 0, outputTokens: msg.usage?.output_tokens ?? 0 },
37
+ model: msg.model,
38
+ finishReason: msg.stop_reason || 'stop',
39
+ raw: msg,
40
+ };
41
+ },
42
+
43
+ async *stream(req) {
44
+ const s = getClient().messages.stream(buildParams(req), { signal: req.signal });
45
+ for await (const event of s) {
46
+ if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
47
+ yield { delta: event.delta.text };
48
+ }
49
+ }
50
+ const final = await s.finalMessage();
51
+ const text = (final.content || []).filter((b) => b.type === 'text').map((b) => b.text).join('');
52
+ yield {
53
+ done: true,
54
+ text,
55
+ usage: { inputTokens: final.usage?.input_tokens ?? 0, outputTokens: final.usage?.output_tokens ?? 0 },
56
+ finishReason: final.stop_reason || 'stop',
57
+ };
58
+ },
59
+ };
60
+ }
61
+
62
+ module.exports = { create };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ function create({ apiKey, model, timeoutMs = 60_000 }) {
4
+ let ai;
5
+ function getClient() {
6
+ if (ai) return ai;
7
+ const { GoogleGenAI } = require('@google/genai');
8
+ ai = new GoogleGenAI({ apiKey });
9
+ return ai;
10
+ }
11
+
12
+ function toContents(req) {
13
+ return (req.messages || []).map((m) => ({
14
+ role: m.role === 'assistant' ? 'model' : 'user',
15
+ parts: [{ text: m.content }],
16
+ }));
17
+ }
18
+
19
+ function buildConfig(req) {
20
+ const cfg = {
21
+ temperature: req.temperature ?? 0.7,
22
+ ...(req.maxTokens ? { maxOutputTokens: req.maxTokens } : {}),
23
+ ...(req.system ? { systemInstruction: req.system } : {}),
24
+ };
25
+ const ac = req.signal ? { abortSignal: req.signal } : {};
26
+ return { config: cfg, http: { ...ac, timeout: timeoutMs } };
27
+ }
28
+
29
+ return {
30
+ name: 'gemini',
31
+
32
+ async generate(req) {
33
+ const { config, http } = buildConfig(req);
34
+ const resp = await getClient().models.generateContent({
35
+ model: req.model || model,
36
+ contents: toContents(req),
37
+ config,
38
+ httpOptions: http,
39
+ });
40
+ const u = resp.usageMetadata || {};
41
+ return {
42
+ text: resp.text || '',
43
+ usage: { inputTokens: u.promptTokenCount ?? 0, outputTokens: u.candidatesTokenCount ?? 0 },
44
+ model: req.model || model,
45
+ finishReason: resp.candidates?.[0]?.finishReason || 'stop',
46
+ raw: resp,
47
+ };
48
+ },
49
+
50
+ async *stream(req) {
51
+ const { config, http } = buildConfig(req);
52
+ const s = await getClient().models.generateContentStream({
53
+ model: req.model || model,
54
+ contents: toContents(req),
55
+ config,
56
+ httpOptions: http,
57
+ });
58
+ let text = '';
59
+ let usage = { inputTokens: 0, outputTokens: 0 };
60
+ let finishReason = 'stop';
61
+ for await (const chunk of s) {
62
+ if (chunk.text) { text += chunk.text; yield { delta: chunk.text }; }
63
+ if (chunk.usageMetadata) {
64
+ usage = {
65
+ inputTokens: chunk.usageMetadata.promptTokenCount ?? 0,
66
+ outputTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,
67
+ };
68
+ }
69
+ const fr = chunk.candidates?.[0]?.finishReason;
70
+ if (fr) finishReason = fr;
71
+ }
72
+ yield { done: true, text, usage, finishReason };
73
+ },
74
+ };
75
+ }
76
+
77
+ module.exports = { create };
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const { estimateTokens } = require('../capabilities');
4
+
5
+ function create({ model = 'mock-1' } = {}) {
6
+ function answer(req) {
7
+ const last = [...(req.messages || [])].reverse().find((m) => m.role !== 'assistant');
8
+ const seed = (last?.content || '').trim();
9
+ return `[mock:${model}] ${seed ? seed.slice(0, 280) : 'no input'}`;
10
+ }
11
+
12
+ return {
13
+ name: 'mock',
14
+
15
+ async generate(req) {
16
+ if (req.signal?.aborted) throw new Error('aborted');
17
+ const text = answer(req);
18
+ const inputTokens = estimateTokens((req.system || '') + JSON.stringify(req.messages || []));
19
+ return {
20
+ text,
21
+ usage: { inputTokens, outputTokens: estimateTokens(text) },
22
+ model,
23
+ finishReason: 'stop',
24
+ raw: { mock: true },
25
+ };
26
+ },
27
+
28
+ async *stream(req) {
29
+ const text = answer(req);
30
+ for (const word of text.split(' ')) yield { delta: word + ' ' };
31
+ yield {
32
+ done: true,
33
+ text,
34
+ usage: { inputTokens: estimateTokens(JSON.stringify(req.messages || [])), outputTokens: estimateTokens(text) },
35
+ finishReason: 'stop',
36
+ };
37
+ },
38
+ };
39
+ }
40
+
41
+ module.exports = { create };
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ function create({ apiKey, model, baseURL, timeoutMs = 60_000 }) {
4
+ let client;
5
+ function getClient() {
6
+ if (client) return client;
7
+ const OpenAI = require('openai');
8
+ client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}), timeout: timeoutMs, maxRetries: 0 });
9
+ return client;
10
+ }
11
+
12
+ function buildMessages(req) {
13
+ const msgs = [];
14
+ if (req.system) msgs.push({ role: 'system', content: req.system });
15
+ for (const m of req.messages || []) {
16
+ msgs.push({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content });
17
+ }
18
+ return msgs;
19
+ }
20
+
21
+ function baseParams(req) {
22
+ return {
23
+ model: req.model || model,
24
+ messages: buildMessages(req),
25
+ temperature: req.temperature ?? 0.7,
26
+ ...(req.maxTokens ? { max_tokens: req.maxTokens } : {}),
27
+ };
28
+ }
29
+
30
+ return {
31
+ name: 'openai-compatible',
32
+
33
+ async generate(req) {
34
+ const r = await getClient().chat.completions.create(
35
+ baseParams(req),
36
+ { signal: req.signal },
37
+ );
38
+ const choice = r.choices?.[0];
39
+ return {
40
+ text: choice?.message?.content || '',
41
+ usage: {
42
+ inputTokens: r.usage?.prompt_tokens ?? 0,
43
+ outputTokens: r.usage?.completion_tokens ?? 0,
44
+ },
45
+ model: r.model,
46
+ finishReason: choice?.finish_reason || 'stop',
47
+ raw: r,
48
+ };
49
+ },
50
+
51
+ async *stream(req) {
52
+ const s = await getClient().chat.completions.create(
53
+ { ...baseParams(req), stream: true, stream_options: { include_usage: true } },
54
+ { signal: req.signal },
55
+ );
56
+ let text = '';
57
+ let usage = { inputTokens: 0, outputTokens: 0 };
58
+ let finishReason = 'stop';
59
+ for await (const part of s) {
60
+ const d = part.choices?.[0]?.delta?.content;
61
+ if (d) { text += d; yield { delta: d }; }
62
+ if (part.choices?.[0]?.finish_reason) finishReason = part.choices[0].finish_reason;
63
+ if (part.usage) {
64
+ usage = { inputTokens: part.usage.prompt_tokens ?? 0, outputTokens: part.usage.completion_tokens ?? 0 };
65
+ }
66
+ }
67
+ yield { done: true, text, usage, finishReason };
68
+ },
69
+ };
70
+ }
71
+
72
+ module.exports = { create };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const { normalizeError } = require('./errors');
4
+
5
+ async function withRetry(fn, opts = {}) {
6
+ const { provider, retries = 3, baseDelayMs = 500, maxDelayMs = 8000, signal } = opts;
7
+ let attempt = 0;
8
+ for (;;) {
9
+ try {
10
+ return await fn();
11
+ } catch (raw) {
12
+ const err = normalizeError(raw, provider);
13
+ if (!err.retryable || attempt >= retries) throw err;
14
+ const backoff = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
15
+ const jitter = Math.random() * backoff * 0.25;
16
+ const delay = err.retryAfterMs != null ? err.retryAfterMs : backoff + jitter;
17
+ attempt += 1;
18
+ await sleep(delay, signal);
19
+ }
20
+ }
21
+ }
22
+
23
+ function sleep(ms, signal) {
24
+ return new Promise((resolve, reject) => {
25
+ if (signal?.aborted) return reject(new Error('aborted'));
26
+ const t = setTimeout(resolve, ms);
27
+ signal?.addEventListener('abort', () => { clearTimeout(t); reject(new Error('aborted')); }, { once: true });
28
+ });
29
+ }
30
+
31
+ module.exports = { withRetry };
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `gitset commit` — local, BYOAI commit-message generation with iterative
5
+ * refinement. No backend, key never leaves the machine.
6
+ *
7
+ * Flags:
8
+ * --provider <name> override default provider for this run
9
+ * --model <name> override model
10
+ * --style <name> commit style (default: conventional)
11
+ * --all, -a `git add -A` before generating
12
+ * --yes, -y non-interactive: accept first message and commit
13
+ * --json machine output ({message,provider,model}); implies -y-less (no commit)
14
+ * --print print the message only; do not commit
15
+ *
16
+ * Exit codes: 0 ok · 1 usage/git error · 2 AI provider error.
17
+ */
18
+ const { execFileSync, spawnSync } = require('child_process');
19
+ const readline = require('readline');
20
+ const config = require('./config');
21
+ const { getStagedChanges, generateCommitMessage, AIError } = require('./commit-local');
22
+ const theme = require('../src/utils/theme');
23
+
24
+ const out = (s = '') => console.log(s);
25
+ const errln = (s) => console.error(`${theme.error('✗')} ${s}`);
26
+
27
+ function getFlag(argv, name) {
28
+ const i = argv.indexOf(name);
29
+ return i !== -1 && i + 1 < argv.length && !argv[i + 1].startsWith('-') ? argv[i + 1] : null;
30
+ }
31
+ const hasFlag = (argv, ...names) => names.some((n) => argv.includes(n));
32
+
33
+ function ask(question) {
34
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
35
+ return new Promise((resolve) => rl.question(question, (a) => { rl.close(); resolve(a.trim()); }));
36
+ }
37
+
38
+ async function runCommitCommand(argv) {
39
+ const json = hasFlag(argv, '--json');
40
+ const printOnly = hasFlag(argv, '--print');
41
+ const nonInteractive = hasFlag(argv, '--yes', '-y') || json || printOnly || !process.stdin.isTTY;
42
+
43
+ let providerCfg;
44
+ try {
45
+ providerCfg = await config.ensureConfigured(getFlag(argv, '--provider'), { interactive: !nonInteractive });
46
+ } catch (e) {
47
+ errln(e.message);
48
+ return 1;
49
+ }
50
+
51
+ if (hasFlag(argv, '--all', '-a')) {
52
+ try {
53
+ execFileSync('git', ['add', '-A'], { stdio: 'ignore' });
54
+ } catch {
55
+ errln('`git add -A` failed — is this a git repository?');
56
+ return 1;
57
+ }
58
+ }
59
+
60
+ let changes;
61
+ try {
62
+ changes = getStagedChanges();
63
+ } catch (e) {
64
+ errln(e.message);
65
+ return 1;
66
+ }
67
+ if (!changes.diff.trim()) {
68
+ errln('Nothing staged. Stage changes with `git add` (or pass --all).');
69
+ return 1;
70
+ }
71
+
72
+ const style = getFlag(argv, '--style') || 'conventional';
73
+ const model = getFlag(argv, '--model');
74
+ const baseCfg = model ? { ...providerCfg, model } : providerCfg;
75
+
76
+ async function gen(previous, instruction) {
77
+ try {
78
+ if (!json && !printOnly) process.stderr.write(theme.dim(`… generating via ${providerCfg.provider} (${baseCfg.model || 'default model'})\n`));
79
+ return await generateCommitMessage({
80
+ providerCfg: baseCfg, diff: changes.diff, stats: changes.stat, style, previous, instruction,
81
+ });
82
+ } catch (e) {
83
+ if (e instanceof AIError) { errln(`AI provider error (${e.code}): ${e.message}`); throw { exit: 2 }; }
84
+ if (e.code === 'NO_STAGED_CHANGES') { errln(e.message); throw { exit: 1 }; }
85
+ errln(e.message || 'Generation failed'); throw { exit: 2 };
86
+ }
87
+ }
88
+
89
+ let result;
90
+ try {
91
+ result = await gen();
92
+ } catch (e) {
93
+ return e.exit || 2;
94
+ }
95
+
96
+ if (json) {
97
+ out(JSON.stringify({ message: result.message, provider: result.provider, model: result.model }));
98
+ return 0;
99
+ }
100
+ if (printOnly) { out(result.message); return 0; }
101
+
102
+ function doCommit(message) {
103
+ const r = spawnSync('git', ['commit', '-m', message], { stdio: 'inherit' });
104
+ return r.status === 0 ? 0 : 1;
105
+ }
106
+
107
+ if (nonInteractive) {
108
+ out(result.message);
109
+ return doCommit(result.message);
110
+ }
111
+
112
+ for (;;) {
113
+ out();
114
+ out(theme.bold('Proposed commit message:'));
115
+ out(theme.accent(result.message.split('\n').map((l) => ` ${l}`).join('\n')));
116
+ out();
117
+ out(theme.dim('tip: edit ~/.gitset/templates/commit.md (or run `gitset template edit commit`) to change the style'));
118
+ const choice = (await ask(
119
+ `${theme.key('a', 'ccept')} ${theme.key('r', 'efine')} ${theme.key('g', 'regenerate')} ${theme.key('e', 'dit')} ${theme.key('q', 'uit')} > `,
120
+ )).toLowerCase();
121
+
122
+ if (choice === 'a' || choice === '') return doCommit(result.message);
123
+ if (choice === 'q') { out('Aborted. Nothing committed.'); return 0; }
124
+ if (choice === 'e') {
125
+ const edited = await ask('New message: ');
126
+ if (edited) return doCommit(edited);
127
+ continue;
128
+ }
129
+ if (choice === 'g') {
130
+ try { result = await gen(); } catch (e) { return e.exit || 2; }
131
+ continue;
132
+ }
133
+ if (choice === 'r') {
134
+ const instruction = await ask('Refinement instruction: ');
135
+ if (!instruction) continue;
136
+ try { result = await gen(result.message, instruction); } catch (e) { return e.exit || 2; }
137
+ continue;
138
+ }
139
+ out(theme.warn('Unrecognized option.'));
140
+ }
141
+ }
142
+
143
+ module.exports = { runCommitCommand };