@theonlykaks/kaks 0.0.1

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,679 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import process from 'node:process';
5
+
6
+ import axios from 'axios';
7
+ import chalk from 'chalk';
8
+ import { execa } from 'execa';
9
+ import inquirer from 'inquirer';
10
+ import ora from 'ora';
11
+
12
+ export const CONFIG_DIR = path.join(os.homedir(), '.kaks');
13
+ export const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
14
+ export const LOCAL_CONFIG_NAME = '.kaks.json';
15
+
16
+ const AI_PROVIDER_DEFAULTS = {
17
+ gemini: { model: 'gemini-2.0-flash', envKey: 'GEMINI_API_KEY' },
18
+ openai: { model: 'gpt-4o-mini', envKey: 'OPENAI_API_KEY' },
19
+ ollama: { model: 'llama3.1', envKey: 'LLAMA_API_KEY' },
20
+ };
21
+
22
+ export class CliError extends Error {
23
+ constructor(message, options = {}) {
24
+ super(message);
25
+ this.name = 'CliError';
26
+ this.exitCode = options.exitCode ?? 1;
27
+ this.cause = options.cause;
28
+ }
29
+ }
30
+
31
+ export class AiConfigError extends CliError {
32
+ constructor(provider) {
33
+ const envKey = AI_PROVIDER_DEFAULTS[provider]?.envKey;
34
+ const suffix = envKey ? ` Set ${envKey} or run "kaks init".` : ' Run "kaks init" to configure AI.';
35
+ super(`API key not found for ${provider}.${suffix}`);
36
+ this.name = 'AiConfigError';
37
+ }
38
+ }
39
+
40
+ export function getDefaultConfig() {
41
+ return {
42
+ ai: {
43
+ provider: 'gemini',
44
+ model: AI_PROVIDER_DEFAULTS.gemini.model,
45
+ temperature: 0.7,
46
+ maxTokens: 2048,
47
+ },
48
+ projects: {},
49
+ defaults: {
50
+ editor: 'code',
51
+ browser: 'default',
52
+ shell: process.platform === 'win32' ? 'powershell' : path.basename(process.env.SHELL ?? 'bash'),
53
+ },
54
+ };
55
+ }
56
+
57
+ export async function pathExists(filePath) {
58
+ try {
59
+ await fs.access(filePath);
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ export async function readJson(filePath) {
67
+ try {
68
+ const raw = await fs.readFile(filePath, 'utf8');
69
+ return JSON.parse(raw);
70
+ } catch (error) {
71
+ if (error.code === 'ENOENT') {
72
+ return null;
73
+ }
74
+
75
+ if (error instanceof SyntaxError) {
76
+ throw new CliError(`Invalid JSON in ${filePath}: ${error.message}`, { cause: error });
77
+ }
78
+
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ export async function writeJson(filePath, value) {
84
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
85
+ await fs.writeFile(`${filePath}.tmp`, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
86
+ await fs.rename(`${filePath}.tmp`, filePath);
87
+
88
+ if (process.platform !== 'win32') {
89
+ await fs.chmod(filePath, 0o600).catch(() => undefined);
90
+ }
91
+ }
92
+
93
+ export async function loadGlobalConfig() {
94
+ const stored = await readJson(CONFIG_PATH);
95
+ return mergeConfig(getDefaultConfig(), stored ?? {});
96
+ }
97
+
98
+ export async function saveGlobalConfig(config) {
99
+ await writeJson(CONFIG_PATH, mergeConfig(getDefaultConfig(), config));
100
+ }
101
+
102
+ export async function loadLocalConfig(cwd = process.cwd()) {
103
+ return readJson(path.join(cwd, LOCAL_CONFIG_NAME));
104
+ }
105
+
106
+ export function mergeConfig(base, override) {
107
+ if (!isPlainObject(base) || !isPlainObject(override)) {
108
+ return override;
109
+ }
110
+
111
+ const output = { ...base };
112
+ for (const [key, value] of Object.entries(override)) {
113
+ output[key] = isPlainObject(value) && isPlainObject(base[key])
114
+ ? mergeConfig(base[key], value)
115
+ : value;
116
+ }
117
+ return output;
118
+ }
119
+
120
+ function isPlainObject(value) {
121
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
122
+ }
123
+
124
+ export function getByPath(object, keyPath) {
125
+ return keyPath.split('.').reduce((current, key) => current?.[key], object);
126
+ }
127
+
128
+ export function setByPath(object, keyPath, value) {
129
+ const keys = keyPath.split('.');
130
+ let cursor = object;
131
+
132
+ for (const key of keys.slice(0, -1)) {
133
+ if (!isPlainObject(cursor[key])) {
134
+ cursor[key] = {};
135
+ }
136
+ cursor = cursor[key];
137
+ }
138
+
139
+ cursor[keys.at(-1)] = value;
140
+ }
141
+
142
+ export function deleteByPath(object, keyPath) {
143
+ const keys = keyPath.split('.');
144
+ const finalKey = keys.pop();
145
+ const parent = keys.reduce((current, key) => current?.[key], object);
146
+
147
+ if (parent && Object.hasOwn(parent, finalKey)) {
148
+ delete parent[finalKey];
149
+ return true;
150
+ }
151
+
152
+ return false;
153
+ }
154
+
155
+ export function parseConfigValue(value) {
156
+ if (value === 'true') return true;
157
+ if (value === 'false') return false;
158
+ if (value === 'null') return null;
159
+
160
+ if (/^-?\d+(\.\d+)?$/.test(value)) {
161
+ return Number(value);
162
+ }
163
+
164
+ if (/^[{[]/.test(value)) {
165
+ try {
166
+ return JSON.parse(value);
167
+ } catch {
168
+ return value;
169
+ }
170
+ }
171
+
172
+ return value;
173
+ }
174
+
175
+ export function validateConfigValue(key, value) {
176
+ if (key === 'ai.provider' && !Object.hasOwn(AI_PROVIDER_DEFAULTS, value)) {
177
+ throw new CliError('Unsupported provider. Use one of: gemini, openai, ollama.');
178
+ }
179
+ }
180
+
181
+ export function resolveUserPath(inputPath, basePath = process.cwd()) {
182
+ if (!inputPath) {
183
+ return basePath;
184
+ }
185
+
186
+ const expanded = inputPath.startsWith('~')
187
+ ? path.join(os.homedir(), inputPath.slice(1))
188
+ : inputPath;
189
+
190
+ return path.resolve(basePath, expanded);
191
+ }
192
+
193
+ export async function resolveProject(config, requestedName) {
194
+ const projects = config.projects ?? {};
195
+ const names = Object.keys(projects);
196
+
197
+ if (!names.length) {
198
+ throw new CliError('No projects configured. Add one with: kaks config add-project <name> --path <path>');
199
+ }
200
+
201
+ const name = requestedName ?? await promptForProjectName(names);
202
+
203
+ if (!Object.hasOwn(projects, name)) {
204
+ const suggestion = getClosestMatch(name, names);
205
+ const hint = suggestion ? ` Did you mean "${suggestion}"?` : '';
206
+ throw new CliError(`Unknown project "${name}".${hint}`);
207
+ }
208
+
209
+ const project = normalizeProject(name, projects[name], config.defaults ?? {});
210
+ return { name, project };
211
+ }
212
+
213
+ async function promptForProjectName(projectNames) {
214
+ const { name } = await inquirer.prompt([
215
+ {
216
+ type: 'list',
217
+ name: 'name',
218
+ message: 'Choose a project',
219
+ choices: projectNames,
220
+ },
221
+ ]);
222
+ return name;
223
+ }
224
+
225
+ function normalizeProject(name, project, defaults) {
226
+ const projectPath = resolveUserPath(project.path ?? process.cwd());
227
+
228
+ return {
229
+ ...project,
230
+ name,
231
+ path: projectPath,
232
+ editor: project.editor ?? defaults.editor ?? 'code',
233
+ browserCommand: project.browserCommand ?? project.browserName ?? defaults.browser ?? 'default',
234
+ };
235
+ }
236
+
237
+ export function getProjectOpenUrls(project) {
238
+ if (Array.isArray(project.openUrls)) {
239
+ return project.openUrls;
240
+ }
241
+
242
+ if (typeof project.browser === 'string' && looksLikeUrl(project.browser)) {
243
+ return [project.browser];
244
+ }
245
+
246
+ if (typeof project.url === 'string') {
247
+ return [project.url];
248
+ }
249
+
250
+ return [];
251
+ }
252
+
253
+ export function getProjectServices(project) {
254
+ const services = [];
255
+
256
+ if (Array.isArray(project.services)) {
257
+ services.push(...project.services);
258
+ }
259
+
260
+ for (const name of ['frontend', 'backend']) {
261
+ const service = project[name];
262
+ if (service?.startCmd || service?.cmd) {
263
+ services.push({ name, ...service, cmd: service.cmd ?? service.startCmd });
264
+ }
265
+ }
266
+
267
+ if (project.startCmd || project.cmd) {
268
+ services.push({
269
+ name: project.name ?? 'app',
270
+ cmd: project.cmd ?? project.startCmd,
271
+ cwd: project.path,
272
+ });
273
+ }
274
+
275
+ return services.map((service) => ({
276
+ name: service.name ?? 'service',
277
+ cmd: service.cmd ?? service.startCmd,
278
+ cwd: resolveUserPath(service.cwd ?? service.path ?? project.path, project.path),
279
+ port: service.port,
280
+ })).filter((service) => Boolean(service.cmd));
281
+ }
282
+
283
+ export function normalizeUrl(input) {
284
+ const trimmed = String(input ?? '').trim();
285
+ if (!trimmed) {
286
+ throw new CliError('Missing URL. Try: kaks go example.com');
287
+ }
288
+
289
+ if (/^https?:\/\//i.test(trimmed)) {
290
+ return trimmed;
291
+ }
292
+
293
+ if (/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/i.test(trimmed)) {
294
+ return `http://${trimmed}`;
295
+ }
296
+
297
+ return `https://${trimmed}`;
298
+ }
299
+
300
+ export function assertValidUrl(url) {
301
+ try {
302
+ const parsed = new URL(url);
303
+ if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname.includes('.')
304
+ && parsed.hostname !== 'localhost'
305
+ && !/^\d{1,3}(\.\d{1,3}){3}$/.test(parsed.hostname)) {
306
+ throw new Error('Invalid host');
307
+ }
308
+ } catch (error) {
309
+ throw new CliError("Doesn't look like a valid URL. Try: kaks go example.com", { cause: error });
310
+ }
311
+ }
312
+
313
+ function looksLikeUrl(value) {
314
+ return /^https?:\/\//i.test(value) || /^[\w.-]+\.[a-z]{2,}/i.test(value) || /^localhost:/i.test(value);
315
+ }
316
+
317
+ export async function openTarget(target, browser = 'default') {
318
+ const { command, args } = getOpenCommand(target, browser);
319
+ const child = execa(command, args, { detached: true, stdio: 'ignore' });
320
+ child.unref?.();
321
+ await child;
322
+ }
323
+
324
+ function getOpenCommand(target, browser) {
325
+ const selectedBrowser = String(browser ?? 'default').toLowerCase();
326
+ const defaultBrowser = !selectedBrowser || selectedBrowser === 'default';
327
+
328
+ if (process.platform === 'win32') {
329
+ if (defaultBrowser) {
330
+ return { command: 'cmd', args: ['/c', 'start', '', target] };
331
+ }
332
+ return { command: 'cmd', args: ['/c', 'start', '', windowsBrowserCommand(selectedBrowser), target] };
333
+ }
334
+
335
+ if (process.platform === 'darwin') {
336
+ if (defaultBrowser) {
337
+ return { command: 'open', args: [target] };
338
+ }
339
+ return { command: 'open', args: ['-a', macBrowserName(selectedBrowser), target] };
340
+ }
341
+
342
+ if (defaultBrowser) {
343
+ return { command: 'xdg-open', args: [target] };
344
+ }
345
+
346
+ return { command: linuxBrowserCommand(selectedBrowser), args: [target] };
347
+ }
348
+
349
+ function windowsBrowserCommand(browser) {
350
+ return {
351
+ chrome: 'chrome',
352
+ firefox: 'firefox',
353
+ edge: 'msedge',
354
+ }[browser] ?? browser;
355
+ }
356
+
357
+ function macBrowserName(browser) {
358
+ return {
359
+ chrome: 'Google Chrome',
360
+ firefox: 'Firefox',
361
+ edge: 'Microsoft Edge',
362
+ }[browser] ?? browser;
363
+ }
364
+
365
+ function linuxBrowserCommand(browser) {
366
+ return {
367
+ chrome: 'google-chrome',
368
+ firefox: 'firefox',
369
+ edge: 'microsoft-edge',
370
+ }[browser] ?? browser;
371
+ }
372
+
373
+ export async function launchEditor(editor, targetPath) {
374
+ try {
375
+ const child = execa(editor, [targetPath], { detached: true, stdio: 'ignore' });
376
+ child.unref?.();
377
+ await child;
378
+ } catch (error) {
379
+ throw new CliError(`Editor not found: ${editor}. Set your editor with: kaks config set defaults.editor <path>`, {
380
+ cause: error,
381
+ });
382
+ }
383
+ }
384
+
385
+ export async function launchExplorer(targetPath) {
386
+ const commands = {
387
+ win32: { command: 'explorer', args: [targetPath] },
388
+ darwin: { command: 'open', args: [targetPath] },
389
+ linux: { command: 'xdg-open', args: [targetPath] },
390
+ };
391
+ const launcher = commands[process.platform] ?? commands.linux;
392
+
393
+ try {
394
+ const child = execa(launcher.command, launcher.args, { detached: true, stdio: 'ignore' });
395
+ child.unref?.();
396
+ await child;
397
+ } catch (error) {
398
+ throw new CliError(`Could not open file explorer for ${targetPath}.`, { cause: error });
399
+ }
400
+ }
401
+
402
+ export async function copyToClipboard(text) {
403
+ const candidates = process.platform === 'win32'
404
+ ? [{ command: 'clip', args: [] }]
405
+ : process.platform === 'darwin'
406
+ ? [{ command: 'pbcopy', args: [] }]
407
+ : [
408
+ { command: 'wl-copy', args: [] },
409
+ { command: 'xclip', args: ['-selection', 'clipboard'] },
410
+ { command: 'xsel', args: ['--clipboard', '--input'] },
411
+ ];
412
+
413
+ for (const candidate of candidates) {
414
+ try {
415
+ await execa(candidate.command, candidate.args, { input: text });
416
+ return;
417
+ } catch {
418
+ // Try the next platform clipboard command.
419
+ }
420
+ }
421
+
422
+ throw new CliError('Clipboard command not found on this system.');
423
+ }
424
+
425
+ export async function readStdin() {
426
+ if (process.stdin.isTTY) {
427
+ return '';
428
+ }
429
+
430
+ let input = '';
431
+ process.stdin.setEncoding('utf8');
432
+ for await (const chunk of process.stdin) {
433
+ input += chunk;
434
+ }
435
+ return input;
436
+ }
437
+
438
+ export async function readTextFileWithLimits(filePath, options = {}) {
439
+ const warnSize = options.warnSize ?? 100 * 1024;
440
+ const maxSize = options.maxSize ?? 500 * 1024;
441
+ const absolutePath = resolveUserPath(filePath);
442
+
443
+ let stat;
444
+ try {
445
+ stat = await fs.stat(absolutePath);
446
+ } catch (error) {
447
+ if (error.code === 'ENOENT') {
448
+ const suggestion = await suggestSimilarFile(absolutePath);
449
+ const hint = suggestion ? ` Did you mean "${suggestion}"?` : '';
450
+ throw new CliError(`File not found: ${filePath}.${hint}`, { cause: error });
451
+ }
452
+ throw error;
453
+ }
454
+
455
+ if (!stat.isFile()) {
456
+ throw new CliError(`Not a file: ${filePath}`);
457
+ }
458
+
459
+ const bytesToRead = Math.min(stat.size, maxSize);
460
+ const handle = await fs.open(absolutePath, 'r');
461
+ try {
462
+ const buffer = Buffer.alloc(bytesToRead);
463
+ const { bytesRead } = await handle.read(buffer, 0, bytesToRead, 0);
464
+ const contentBuffer = buffer.subarray(0, bytesRead);
465
+
466
+ if (contentBuffer.includes(0)) {
467
+ throw new CliError(`Binary file rejected: ${filePath}`);
468
+ }
469
+
470
+ return {
471
+ absolutePath,
472
+ displayPath: path.relative(process.cwd(), absolutePath) || absolutePath,
473
+ content: contentBuffer.toString('utf8'),
474
+ size: stat.size,
475
+ warned: stat.size > warnSize,
476
+ truncated: stat.size > maxSize,
477
+ };
478
+ } finally {
479
+ await handle.close();
480
+ }
481
+ }
482
+
483
+ async function suggestSimilarFile(absolutePath) {
484
+ const directory = path.dirname(absolutePath);
485
+ const basename = path.basename(absolutePath).toLowerCase();
486
+
487
+ try {
488
+ const entries = await fs.readdir(directory);
489
+ return entries.find((entry) => entry.toLowerCase().includes(basename.slice(0, 4))) ?? null;
490
+ } catch {
491
+ return null;
492
+ }
493
+ }
494
+
495
+ export function detectLanguage(filePath) {
496
+ const basename = path.basename(filePath).toLowerCase();
497
+ const extension = path.extname(filePath).slice(1).toLowerCase();
498
+
499
+ if (basename === 'dockerfile') return 'dockerfile';
500
+ if (['yml', 'yaml'].includes(extension)) return 'yaml';
501
+ if (extension === 'js' || extension === 'mjs' || extension === 'cjs') return 'javascript';
502
+ if (extension === 'ts' || extension === 'tsx') return 'typescript';
503
+ if (extension === 'json') return 'json';
504
+ if (extension === 'md') return 'markdown';
505
+ if (extension === 'py') return 'python';
506
+ if (extension === 'log') return 'log';
507
+ return extension || 'text';
508
+ }
509
+
510
+ export function tailLines(text, count) {
511
+ const lines = text.split(/\r?\n/);
512
+ return lines.slice(Math.max(0, lines.length - count)).join('\n');
513
+ }
514
+
515
+ export function parsePositiveInteger(value, fallback = undefined) {
516
+ const parsed = Number.parseInt(value, 10);
517
+ if (!Number.isInteger(parsed) || parsed <= 0) {
518
+ throw new CliError(`Expected a positive integer, received: ${value}`);
519
+ }
520
+ return parsed ?? fallback;
521
+ }
522
+
523
+ export function hasAiCredentials(config, provider = config.ai?.provider ?? 'gemini') {
524
+ const normalizedProvider = provider.toLowerCase();
525
+ if (normalizedProvider === 'ollama') {
526
+ return true;
527
+ }
528
+
529
+ const envKey = AI_PROVIDER_DEFAULTS[normalizedProvider]?.envKey;
530
+ return Boolean((envKey && process.env[envKey]) || config.ai?.apiKey);
531
+ }
532
+
533
+ export async function completeWithAi({ systemPrompt, userPrompt, config, model }) {
534
+ const provider = String(config.ai?.provider ?? 'gemini').toLowerCase();
535
+ const aiDefaults = AI_PROVIDER_DEFAULTS[provider];
536
+
537
+ if (!aiDefaults) {
538
+ throw new CliError(`Unsupported AI provider: ${provider}`);
539
+ }
540
+
541
+ const selectedModel = model ?? config.ai?.model ?? aiDefaults.model;
542
+ const temperature = Number(config.ai?.temperature ?? 0.7);
543
+ const maxTokens = Number(config.ai?.maxTokens ?? 2048);
544
+
545
+ try {
546
+ if (provider === 'openai') {
547
+ return await completeWithOpenAi({ systemPrompt, userPrompt, model: selectedModel, temperature, maxTokens, config });
548
+ }
549
+
550
+ if (provider === 'ollama') {
551
+ return await completeWithOllama({ systemPrompt, userPrompt, model: selectedModel, temperature, maxTokens });
552
+ }
553
+
554
+ return await completeWithGemini({ systemPrompt, userPrompt, model: selectedModel, temperature, maxTokens, config });
555
+ } catch (error) {
556
+ if (error instanceof CliError) {
557
+ throw error;
558
+ }
559
+ throw normalizeAiError(error, provider);
560
+ }
561
+ }
562
+
563
+ async function completeWithOpenAi({ systemPrompt, userPrompt, model, temperature, maxTokens, config }) {
564
+ const apiKey = process.env.OPENAI_API_KEY ?? config.ai?.apiKey;
565
+ if (!apiKey) {
566
+ throw new AiConfigError('openai');
567
+ }
568
+
569
+ const { data } = await axios.post('https://api.openai.com/v1/chat/completions', {
570
+ model,
571
+ messages: [
572
+ { role: 'system', content: systemPrompt },
573
+ { role: 'user', content: userPrompt },
574
+ ],
575
+ temperature,
576
+ max_tokens: maxTokens,
577
+ }, {
578
+ headers: { Authorization: `Bearer ${apiKey}` },
579
+ timeout: 60_000,
580
+ });
581
+
582
+ return data.choices?.[0]?.message?.content?.trim() ?? '';
583
+ }
584
+
585
+ async function completeWithGemini({ systemPrompt, userPrompt, model, temperature, maxTokens, config }) {
586
+ const apiKey = process.env.GEMINI_API_KEY ?? config.ai?.apiKey;
587
+ if (!apiKey) {
588
+ throw new AiConfigError('gemini');
589
+ }
590
+
591
+ const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`;
592
+ const { data } = await axios.post(`${endpoint}?key=${apiKey}`, {
593
+ systemInstruction: {
594
+ parts: [{ text: systemPrompt }],
595
+ },
596
+ contents: [
597
+ {
598
+ role: 'user',
599
+ parts: [{ text: userPrompt }],
600
+ },
601
+ ],
602
+ generationConfig: {
603
+ temperature,
604
+ maxOutputTokens: maxTokens,
605
+ },
606
+ }, {
607
+ timeout: 60_000,
608
+ });
609
+
610
+ return data.candidates?.[0]?.content?.parts?.map((part) => part.text ?? '').join('').trim() ?? '';
611
+ }
612
+
613
+ async function completeWithOllama({ systemPrompt, userPrompt, model, temperature, maxTokens }) {
614
+ const { data } = await axios.post('http://localhost:11434/api/generate', {
615
+ model,
616
+ prompt: `${systemPrompt}\n\n${userPrompt}`,
617
+ stream: false,
618
+ options: {
619
+ temperature,
620
+ num_predict: maxTokens,
621
+ },
622
+ }, {
623
+ timeout: 120_000,
624
+ });
625
+
626
+ return data.response?.trim() ?? '';
627
+ }
628
+
629
+ function normalizeAiError(error, provider) {
630
+ if (error.response?.status === 401 || error.response?.status === 403) {
631
+ return new CliError(`Authentication failed for ${provider}. Check your API key.`, { cause: error });
632
+ }
633
+
634
+ if (error.response?.status === 429) {
635
+ const retryAfter = error.response.headers?.['retry-after'];
636
+ const suffix = retryAfter ? ` Try again after ${retryAfter} seconds.` : ' Try again later.';
637
+ return new CliError(`Rate limited by ${provider}.${suffix}`, { cause: error });
638
+ }
639
+
640
+ if (error.code === 'ECONNABORTED') {
641
+ return new CliError(`Request to ${provider} timed out.`, { cause: error });
642
+ }
643
+
644
+ if (!error.response) {
645
+ return new CliError(`Could not reach ${provider}. Check your internet connection or provider service.`, { cause: error });
646
+ }
647
+
648
+ return new CliError(`AI request failed: ${error.response.status} ${error.response.statusText ?? ''}`.trim(), {
649
+ cause: error,
650
+ });
651
+ }
652
+
653
+ export async function runWithSpinner(message, task) {
654
+ const spinner = ora(message).start();
655
+ try {
656
+ const result = await task();
657
+ spinner.stop();
658
+ return result;
659
+ } catch (error) {
660
+ spinner.stop();
661
+ throw error;
662
+ }
663
+ }
664
+
665
+ export function handleCommandError(error) {
666
+ const message = error instanceof CliError
667
+ ? error.message
668
+ : `Something went wrong. Error: ${error.message}`;
669
+
670
+ const color = error instanceof AiConfigError ? chalk.yellow : chalk.red;
671
+ console.error(color(message));
672
+
673
+ if (!(error instanceof CliError) && error.stack && process.env.kaks_DEBUG) {
674
+ console.error(chalk.dim(error.stack));
675
+ }
676
+
677
+ process.exitCode = error.exitCode ?? 1;
678
+ }
679
+