@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,130 @@
1
+ import process from 'node:process';
2
+
3
+ import chalk from 'chalk';
4
+ import { execaCommand } from 'execa';
5
+
6
+ import {
7
+ CliError,
8
+ getProjectServices,
9
+ handleCommandError,
10
+ loadGlobalConfig,
11
+ resolveProject,
12
+ } from './shared.js';
13
+
14
+ const SERVICE_COLORS = [chalk.cyan, chalk.green, chalk.magenta, chalk.yellow, chalk.blue];
15
+
16
+ export function registerStartCommand(program) {
17
+ program
18
+ .command('start [projectName]')
19
+ .description('Start configured project services concurrently')
20
+ .option('--only <service>', 'Start only one configured service')
21
+ .option('--detach', 'Start services in the background')
22
+ .addHelpText('after', `
23
+
24
+ Examples:
25
+ $ kaks start myapp
26
+ $ kaks start myapp --only frontend
27
+ $ kaks start myapp --detach
28
+ `)
29
+ .action(async (projectName, options) => {
30
+ try {
31
+ await startProject(projectName, options);
32
+ } catch (error) {
33
+ handleCommandError(error);
34
+ }
35
+ });
36
+ }
37
+
38
+ export async function startProject(projectName, options = {}) {
39
+ const config = await loadGlobalConfig();
40
+ const { name, project } = await resolveProject(config, projectName);
41
+ let services = getProjectServices(project);
42
+
43
+ if (options.only) {
44
+ services = services.filter((service) => service.name === options.only);
45
+ if (!services.length) {
46
+ throw new CliError(`Service not found: ${options.only}`);
47
+ }
48
+ }
49
+
50
+ if (!services.length) {
51
+ throw new CliError(`No start commands configured for "${name}". Add services with "kaks config add-project" or edit ${project.path}.`);
52
+ }
53
+
54
+ console.log(`Starting project: ${name}\n`);
55
+ for (const service of services) {
56
+ const port = service.port ? ` (${service.port})` : '';
57
+ console.log(`${service.name}${port} -> ${service.cmd}`);
58
+ }
59
+ console.log('');
60
+
61
+ if (options.detach) {
62
+ await startDetached(services);
63
+ console.log('Services started in the background.');
64
+ return;
65
+ }
66
+
67
+ await startAttached(services);
68
+ }
69
+
70
+ async function startDetached(services) {
71
+ for (const service of services) {
72
+ const child = execaCommand(service.cmd, {
73
+ cwd: service.cwd,
74
+ detached: true,
75
+ reject: false,
76
+ shell: true,
77
+ stdio: 'ignore',
78
+ });
79
+ child.unref?.();
80
+ }
81
+ }
82
+
83
+ async function startAttached(services) {
84
+ const children = services.map((service, index) => {
85
+ const color = SERVICE_COLORS[index % SERVICE_COLORS.length];
86
+ const child = execaCommand(service.cmd, {
87
+ cwd: service.cwd,
88
+ shell: true,
89
+ all: true,
90
+ reject: false,
91
+ env: process.env,
92
+ });
93
+
94
+ child.all?.on('data', (chunk) => {
95
+ for (const line of chunk.toString().split(/\r?\n/).filter(Boolean)) {
96
+ console.log(`${color(`[${service.name}]`)} ${line}`);
97
+ }
98
+ });
99
+
100
+ return { service, child };
101
+ });
102
+
103
+ let shuttingDown = false;
104
+ const shutdown = () => {
105
+ if (shuttingDown) {
106
+ return;
107
+ }
108
+ shuttingDown = true;
109
+ console.log('\nStopping services...');
110
+ for (const { child } of children) {
111
+ child.kill('SIGTERM', { forceKillAfterDelay: 5000 });
112
+ }
113
+ };
114
+
115
+ process.once('SIGINT', shutdown);
116
+ process.once('SIGTERM', shutdown);
117
+
118
+ const results = await Promise.all(children.map(({ child }) => child));
119
+ process.removeListener('SIGINT', shutdown);
120
+ process.removeListener('SIGTERM', shutdown);
121
+
122
+ const failed = results
123
+ .map((result, index) => ({ result, service: children[index].service }))
124
+ .filter(({ result }) => result.exitCode && result.exitCode !== 0);
125
+
126
+ if (failed.length) {
127
+ const names = failed.map(({ service }) => service.name).join(', ');
128
+ throw new CliError(`Service exited with an error: ${names}`);
129
+ }
130
+ }
@@ -0,0 +1,231 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+
5
+ import {
6
+ CliError,
7
+ completeWithAi,
8
+ handleCommandError,
9
+ hasAiCredentials,
10
+ loadGlobalConfig,
11
+ parsePositiveInteger,
12
+ readStdin,
13
+ resolveUserPath,
14
+ runWithSpinner,
15
+ tailLines,
16
+ } from './shared.js';
17
+
18
+ const ONE_MEGABYTE = 1024 * 1024;
19
+ const LARGE_FILE_TAIL_LINES = 500;
20
+
21
+ const SYSTEM_PROMPT = [
22
+ 'You are a log analysis expert.',
23
+ 'Summarize errors, warnings, key events, likely root causes, and concrete next steps.',
24
+ 'Be structured and concise.',
25
+ ].join(' ');
26
+
27
+ export function registerSummarizeCommand(program) {
28
+ program
29
+ .command('summarize <logfile>')
30
+ .description('Summarize a log file or stdin')
31
+ .option('--tail <n>', 'Only read the last N lines', parsePositiveInteger)
32
+ .option('--errors-only', 'Analyze only error, exception, fatal, and warning lines')
33
+ .option('--json', 'Print a local JSON summary')
34
+ .option('--model <model>', 'Override the configured model')
35
+ .addHelpText('after', `
36
+
37
+ Examples:
38
+ $ kaks summarize logs/server-error.log
39
+ $ kaks summarize app.log --tail 200
40
+ $ Get-Content app.log | kaks summarize -
41
+ `)
42
+ .action(async (logfile, options) => {
43
+ try {
44
+ await summarize(logfile, options);
45
+ } catch (error) {
46
+ handleCommandError(error);
47
+ }
48
+ });
49
+ }
50
+
51
+ export async function summarize(logfile, options = {}) {
52
+ const input = await readLogInput(logfile, options.tail);
53
+ let content = input.content;
54
+
55
+ if (options.errorsOnly) {
56
+ content = content
57
+ .split(/\r?\n/)
58
+ .filter((line) => /(error|exception|fatal|fail|warn|econn|enoent|timeout|unhandled)/i.test(line))
59
+ .join('\n');
60
+ }
61
+
62
+ if (!content.trim()) {
63
+ throw new CliError(options.errorsOnly ? 'No errors or warnings found.' : 'Nothing to summarize.');
64
+ }
65
+
66
+ const localSummary = buildLocalSummary(content, input.label);
67
+
68
+ if (options.json) {
69
+ console.log(JSON.stringify(localSummary, null, 2));
70
+ return;
71
+ }
72
+
73
+ if (input.autoTailed) {
74
+ console.warn(`Large file detected. Analyzing the last ${LARGE_FILE_TAIL_LINES} lines.`);
75
+ }
76
+
77
+ const config = await loadGlobalConfig();
78
+
79
+ if (!hasAiCredentials(config)) {
80
+ printLocalSummary(localSummary);
81
+ console.warn('\nAI insight skipped because no provider credentials are configured. Run "kaks init" to set them up.');
82
+ return;
83
+ }
84
+
85
+ const prompt = [
86
+ `Log source: ${input.label}`,
87
+ `Line count: ${localSummary.lines}`,
88
+ `Errors: ${localSummary.errors}`,
89
+ `Warnings: ${localSummary.warnings}`,
90
+ '',
91
+ 'Log content:',
92
+ '```log',
93
+ content,
94
+ '```',
95
+ ].join('\n');
96
+
97
+ const summary = await runWithSpinner(`Analyzing ${localSummary.lines} lines...`, () => completeWithAi({
98
+ systemPrompt: SYSTEM_PROMPT,
99
+ userPrompt: prompt,
100
+ config,
101
+ model: options.model,
102
+ }));
103
+
104
+ console.log(`\nLog Summary: ${input.label}\n`);
105
+ console.log(`${summary}\n`);
106
+ }
107
+
108
+ async function readLogInput(logfile, requestedTail) {
109
+ if (logfile === '-') {
110
+ const content = await readStdin();
111
+ return {
112
+ label: 'stdin',
113
+ content: requestedTail ? tailLines(content, requestedTail) : content,
114
+ autoTailed: false,
115
+ };
116
+ }
117
+
118
+ const absolutePath = resolveUserPath(logfile);
119
+ let stat;
120
+ try {
121
+ stat = await fs.stat(absolutePath);
122
+ } catch (error) {
123
+ if (error.code === 'ENOENT') {
124
+ throw new CliError(`File not found: ${logfile}`, { cause: error });
125
+ }
126
+ throw error;
127
+ }
128
+
129
+ if (!stat.isFile()) {
130
+ throw new CliError(`Not a file: ${logfile}`);
131
+ }
132
+
133
+ let content;
134
+ let autoTailed = false;
135
+
136
+ if (stat.size > ONE_MEGABYTE && !requestedTail) {
137
+ content = await readLastBytes(absolutePath, Math.min(stat.size, 512 * 1024));
138
+ content = tailLines(content, LARGE_FILE_TAIL_LINES);
139
+ autoTailed = true;
140
+ } else {
141
+ content = await fs.readFile(absolutePath, 'utf8');
142
+ }
143
+
144
+ if (requestedTail) {
145
+ content = tailLines(content, requestedTail);
146
+ }
147
+
148
+ return {
149
+ label: path.relative(process.cwd(), absolutePath) || absolutePath,
150
+ content,
151
+ autoTailed,
152
+ };
153
+ }
154
+
155
+ async function readLastBytes(filePath, bytesToRead) {
156
+ const stat = await fs.stat(filePath);
157
+ const start = Math.max(0, stat.size - bytesToRead);
158
+ const handle = await fs.open(filePath, 'r');
159
+ try {
160
+ const buffer = Buffer.alloc(bytesToRead);
161
+ const { bytesRead } = await handle.read(buffer, 0, bytesToRead, start);
162
+ return buffer.subarray(0, bytesRead).toString('utf8');
163
+ } finally {
164
+ await handle.close();
165
+ }
166
+ }
167
+
168
+ function buildLocalSummary(content, source) {
169
+ const lines = content.split(/\r?\n/).filter(Boolean);
170
+ const errorLines = lines.filter((line) => /(error|exception|fatal|fail|econn|enoent|timeout|unhandled)/i.test(line));
171
+ const warningLines = lines.filter((line) => /\b(warn|warning)\b/i.test(line));
172
+ const infoLines = Math.max(0, lines.length - errorLines.length - warningLines.length);
173
+
174
+ return {
175
+ source,
176
+ lines: lines.length,
177
+ errors: errorLines.length,
178
+ warnings: warningLines.length,
179
+ info: infoLines,
180
+ criticalIssues: summarizeIssues(errorLines),
181
+ };
182
+ }
183
+
184
+ function summarizeIssues(errorLines) {
185
+ const counts = new Map();
186
+
187
+ for (const line of errorLines) {
188
+ const key = classifyIssue(line);
189
+ counts.set(key, (counts.get(key) ?? 0) + 1);
190
+ }
191
+
192
+ return [...counts.entries()]
193
+ .sort((a, b) => b[1] - a[1])
194
+ .slice(0, 5)
195
+ .map(([message, count]) => ({ message, count }));
196
+ }
197
+
198
+ function classifyIssue(line) {
199
+ const patterns = [
200
+ 'ECONNREFUSED',
201
+ 'EADDRINUSE',
202
+ 'ENOENT',
203
+ 'ENOMEM',
204
+ 'JWT_SECRET',
205
+ 'TypeError',
206
+ 'ReferenceError',
207
+ 'SyntaxError',
208
+ 'timeout',
209
+ ];
210
+
211
+ return patterns.find((pattern) => new RegExp(pattern, 'i').test(line))
212
+ ?? line.replace(/\d{2,4}[-:/]\d{1,2}[-:/]\d{1,2}[\sT]?\d{0,2}:?\d{0,2}:?\d{0,2}/g, '').trim().slice(0, 120)
213
+ ?? 'Unclassified error';
214
+ }
215
+
216
+ function printLocalSummary(summary) {
217
+ console.log(`\nLog Summary: ${summary.source}\n`);
218
+ console.log(`Errors: ${summary.errors}`);
219
+ console.log(`Warnings: ${summary.warnings}`);
220
+ console.log(`Info: ${summary.info}`);
221
+
222
+ if (!summary.criticalIssues.length) {
223
+ console.log('\nLog looks clean.');
224
+ return;
225
+ }
226
+
227
+ console.log('\nCritical Issues:');
228
+ summary.criticalIssues.forEach((issue, index) => {
229
+ console.log(`${index + 1}. ${issue.message} (x${issue.count})`);
230
+ });
231
+ }