stakeout-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/LICENSE +131 -0
- package/README.md +152 -0
- package/dist/commands/chat.d.ts +5 -0
- package/dist/commands/chat.js +162 -0
- package/dist/commands/clear.d.ts +7 -0
- package/dist/commands/clear.js +89 -0
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.js +64 -0
- package/dist/commands/dashboard.d.ts +5 -0
- package/dist/commands/dashboard.js +9 -0
- package/dist/commands/digest.d.ts +6 -0
- package/dist/commands/digest.js +113 -0
- package/dist/commands/export.d.ts +8 -0
- package/dist/commands/export.js +118 -0
- package/dist/commands/hook.d.ts +6 -0
- package/dist/commands/hook.js +57 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +70 -0
- package/dist/commands/log.d.ts +9 -0
- package/dist/commands/log.js +103 -0
- package/dist/commands/note.d.ts +9 -0
- package/dist/commands/note.js +48 -0
- package/dist/commands/record.d.ts +6 -0
- package/dist/commands/record.js +106 -0
- package/dist/commands/repo.d.ts +7 -0
- package/dist/commands/repo.js +60 -0
- package/dist/commands/search.d.ts +5 -0
- package/dist/commands/search.js +69 -0
- package/dist/commands/stats.d.ts +1 -0
- package/dist/commands/stats.js +99 -0
- package/dist/commands/tag.d.ts +8 -0
- package/dist/commands/tag.js +61 -0
- package/dist/commands/tui.d.ts +1 -0
- package/dist/commands/tui.js +5 -0
- package/dist/commands/watch.d.ts +6 -0
- package/dist/commands/watch.js +101 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +195 -0
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/config.js +51 -0
- package/dist/lib/database.d.ts +31 -0
- package/dist/lib/database.js +222 -0
- package/dist/lib/diff.d.ts +3 -0
- package/dist/lib/diff.js +118 -0
- package/dist/lib/summarizer.d.ts +2 -0
- package/dist/lib/summarizer.js +90 -0
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +125 -0
- package/dist/types/index.d.ts +38 -0
- package/dist/types/index.js +1 -0
- package/dist/web/public/app.js +387 -0
- package/dist/web/public/index.html +131 -0
- package/dist/web/public/styles.css +571 -0
- package/dist/web/server.d.ts +1 -0
- package/dist/web/server.js +402 -0
- package/package.json +69 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getEntries } from '../lib/database.js';
|
|
3
|
+
import { loadConfig } from '../lib/config.js';
|
|
4
|
+
import { Ollama } from 'ollama';
|
|
5
|
+
import OpenAI from 'openai';
|
|
6
|
+
export async function digest(options) {
|
|
7
|
+
const since = options.since ? parseSince(options.since) : getWeekAgo();
|
|
8
|
+
console.log(chalk.dim('🔍 Gathering intel...'));
|
|
9
|
+
const entries = getEntries({
|
|
10
|
+
limit: 100,
|
|
11
|
+
since,
|
|
12
|
+
path: options.path
|
|
13
|
+
});
|
|
14
|
+
if (entries.length === 0) {
|
|
15
|
+
console.log(chalk.yellow('No entries found in this time period.'));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
console.log(chalk.dim(`📊 Found ${entries.length} entries`));
|
|
19
|
+
console.log(chalk.dim('🤖 Generating digest...\n'));
|
|
20
|
+
// Build a summary of all entries
|
|
21
|
+
const entriesSummary = entries.map(e => {
|
|
22
|
+
const date = new Date(e.timestamp).toLocaleDateString();
|
|
23
|
+
const dirs = e.directories.length > 0 ? ` (${e.directories.join(', ')})` : '';
|
|
24
|
+
return `- [${date}]${dirs}: ${e.summary}`;
|
|
25
|
+
}).join('\n');
|
|
26
|
+
const prompt = `You are analyzing a development activity log. Create a high-level digest that:
|
|
27
|
+
|
|
28
|
+
1. Identifies the main themes/areas of work
|
|
29
|
+
2. Highlights significant changes or milestones
|
|
30
|
+
3. Notes any patterns or concerns
|
|
31
|
+
4. Provides a brief "executive summary" (2-3 sentences)
|
|
32
|
+
|
|
33
|
+
Keep the digest concise but insightful. Focus on the big picture.
|
|
34
|
+
|
|
35
|
+
Activity log:
|
|
36
|
+
${entriesSummary}
|
|
37
|
+
|
|
38
|
+
Generate a digest with sections: Executive Summary, Key Areas, Notable Changes, Patterns/Observations`;
|
|
39
|
+
try {
|
|
40
|
+
const summary = await generateDigest(prompt);
|
|
41
|
+
console.log(chalk.bold.green('📋 STAKEOUT DIGEST'));
|
|
42
|
+
console.log(chalk.dim(`Period: ${new Date(since).toLocaleDateString()} - ${new Date().toLocaleDateString()}`));
|
|
43
|
+
console.log(chalk.dim(`Entries analyzed: ${entries.length}`));
|
|
44
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(summary);
|
|
47
|
+
console.log('');
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error(chalk.red('Error generating digest: ' + error.message));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function generateDigest(prompt) {
|
|
55
|
+
const config = loadConfig();
|
|
56
|
+
if (config.llm_provider === 'ollama') {
|
|
57
|
+
const ollama = new Ollama({ host: config.ollama_host });
|
|
58
|
+
const response = await ollama.chat({
|
|
59
|
+
model: config.ollama_model,
|
|
60
|
+
messages: [{ role: 'user', content: prompt }],
|
|
61
|
+
options: { temperature: 0.4 }
|
|
62
|
+
});
|
|
63
|
+
return response.message.content.trim();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if (!config.openai_api_key) {
|
|
67
|
+
throw new Error('OpenAI API key not configured');
|
|
68
|
+
}
|
|
69
|
+
const openai = new OpenAI({ apiKey: config.openai_api_key });
|
|
70
|
+
const response = await openai.chat.completions.create({
|
|
71
|
+
model: config.openai_model,
|
|
72
|
+
messages: [{ role: 'user', content: prompt }],
|
|
73
|
+
temperature: 0.4,
|
|
74
|
+
max_tokens: 1000
|
|
75
|
+
});
|
|
76
|
+
return response.choices[0]?.message?.content?.trim() ?? 'Unable to generate digest';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function parseSince(since) {
|
|
80
|
+
const now = new Date();
|
|
81
|
+
const match = since.match(/^(\d+)\s*(day|days|week|weeks|month|months)$/i);
|
|
82
|
+
if (match) {
|
|
83
|
+
const amount = parseInt(match[1], 10);
|
|
84
|
+
const unit = match[2].toLowerCase();
|
|
85
|
+
switch (unit) {
|
|
86
|
+
case 'day':
|
|
87
|
+
case 'days':
|
|
88
|
+
now.setDate(now.getDate() - amount);
|
|
89
|
+
break;
|
|
90
|
+
case 'week':
|
|
91
|
+
case 'weeks':
|
|
92
|
+
now.setDate(now.getDate() - (amount * 7));
|
|
93
|
+
break;
|
|
94
|
+
case 'month':
|
|
95
|
+
case 'months':
|
|
96
|
+
now.setMonth(now.getMonth() - amount);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
return now.toISOString();
|
|
100
|
+
}
|
|
101
|
+
// Try to parse as date
|
|
102
|
+
const parsed = new Date(since);
|
|
103
|
+
if (!isNaN(parsed.getTime())) {
|
|
104
|
+
return parsed.toISOString();
|
|
105
|
+
}
|
|
106
|
+
// Default to 1 week
|
|
107
|
+
return getWeekAgo();
|
|
108
|
+
}
|
|
109
|
+
function getWeekAgo() {
|
|
110
|
+
const now = new Date();
|
|
111
|
+
now.setDate(now.getDate() - 7);
|
|
112
|
+
return now.toISOString();
|
|
113
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { writeFileSync } from 'fs';
|
|
3
|
+
import { getEntries } from '../lib/database.js';
|
|
4
|
+
export async function exportCmd(options) {
|
|
5
|
+
const format = options.format || 'markdown';
|
|
6
|
+
const since = options.since ? parseSince(options.since) : undefined;
|
|
7
|
+
const entries = getEntries({
|
|
8
|
+
limit: 1000,
|
|
9
|
+
since,
|
|
10
|
+
path: options.path
|
|
11
|
+
});
|
|
12
|
+
if (entries.length === 0) {
|
|
13
|
+
console.log(chalk.yellow('No entries to export.'));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
let content;
|
|
17
|
+
let extension;
|
|
18
|
+
switch (format.toLowerCase()) {
|
|
19
|
+
case 'json':
|
|
20
|
+
content = JSON.stringify(entries, null, 2);
|
|
21
|
+
extension = 'json';
|
|
22
|
+
break;
|
|
23
|
+
case 'csv':
|
|
24
|
+
content = exportToCsv(entries);
|
|
25
|
+
extension = 'csv';
|
|
26
|
+
break;
|
|
27
|
+
case 'markdown':
|
|
28
|
+
case 'md':
|
|
29
|
+
default:
|
|
30
|
+
content = exportToMarkdown(entries);
|
|
31
|
+
extension = 'md';
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
const outputPath = options.output || `stakeout-export-${Date.now()}.${extension}`;
|
|
35
|
+
writeFileSync(outputPath, content);
|
|
36
|
+
console.log(chalk.green(`✓ Exported ${entries.length} entries to ${outputPath}`));
|
|
37
|
+
}
|
|
38
|
+
function exportToMarkdown(entries) {
|
|
39
|
+
const lines = [
|
|
40
|
+
'# STAKEOUT Export',
|
|
41
|
+
'',
|
|
42
|
+
`Generated: ${new Date().toISOString()}`,
|
|
43
|
+
`Entries: ${entries.length}`,
|
|
44
|
+
'',
|
|
45
|
+
'---',
|
|
46
|
+
''
|
|
47
|
+
];
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const date = new Date(entry.timestamp).toLocaleString();
|
|
50
|
+
lines.push(`## Entry #${entry.id}`);
|
|
51
|
+
lines.push('');
|
|
52
|
+
lines.push(`**Date:** ${date}`);
|
|
53
|
+
if (entry.commit_hash) {
|
|
54
|
+
lines.push(`**Commit:** \`${entry.commit_hash.slice(0, 7)}\``);
|
|
55
|
+
}
|
|
56
|
+
if (entry.commit_message) {
|
|
57
|
+
lines.push(`**Message:** ${entry.commit_message}`);
|
|
58
|
+
}
|
|
59
|
+
lines.push('');
|
|
60
|
+
lines.push('### Summary');
|
|
61
|
+
lines.push('');
|
|
62
|
+
lines.push(entry.summary);
|
|
63
|
+
lines.push('');
|
|
64
|
+
if (entry.directories.length > 0) {
|
|
65
|
+
lines.push('### Directories');
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push(entry.directories.map((d) => `- \`${d}\``).join('\n'));
|
|
68
|
+
lines.push('');
|
|
69
|
+
}
|
|
70
|
+
if (entry.files_changed.length > 0) {
|
|
71
|
+
lines.push('### Files Changed');
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push(entry.files_changed.slice(0, 20).map((f) => `- \`${f}\``).join('\n'));
|
|
74
|
+
if (entry.files_changed.length > 20) {
|
|
75
|
+
lines.push(`- ... and ${entry.files_changed.length - 20} more`);
|
|
76
|
+
}
|
|
77
|
+
lines.push('');
|
|
78
|
+
}
|
|
79
|
+
lines.push('---');
|
|
80
|
+
lines.push('');
|
|
81
|
+
}
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
function exportToCsv(entries) {
|
|
85
|
+
const headers = ['id', 'timestamp', 'commit_hash', 'summary', 'directories', 'file_count'];
|
|
86
|
+
const rows = entries.map(e => [
|
|
87
|
+
e.id,
|
|
88
|
+
e.timestamp,
|
|
89
|
+
e.commit_hash || '',
|
|
90
|
+
`"${e.summary.replace(/"/g, '""')}"`,
|
|
91
|
+
`"${e.directories.join(', ')}"`,
|
|
92
|
+
e.files_changed.length
|
|
93
|
+
]);
|
|
94
|
+
return [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
|
|
95
|
+
}
|
|
96
|
+
function parseSince(since) {
|
|
97
|
+
const now = new Date();
|
|
98
|
+
const match = since.match(/^(\d+)\s*(day|days|week|weeks|month|months)$/i);
|
|
99
|
+
if (match) {
|
|
100
|
+
const amount = parseInt(match[1], 10);
|
|
101
|
+
const unit = match[2].toLowerCase();
|
|
102
|
+
switch (unit) {
|
|
103
|
+
case 'day':
|
|
104
|
+
case 'days':
|
|
105
|
+
now.setDate(now.getDate() - amount);
|
|
106
|
+
break;
|
|
107
|
+
case 'week':
|
|
108
|
+
case 'weeks':
|
|
109
|
+
now.setDate(now.getDate() - (amount * 7));
|
|
110
|
+
break;
|
|
111
|
+
case 'month':
|
|
112
|
+
case 'months':
|
|
113
|
+
now.setMonth(now.getMonth() - amount);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return now.toISOString();
|
|
118
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { simpleGit } from 'simple-git';
|
|
5
|
+
const HOOK_SCRIPT = `#!/bin/sh
|
|
6
|
+
# STAKEOUT post-commit hook
|
|
7
|
+
# Records changes after each commit
|
|
8
|
+
|
|
9
|
+
stakeout record --last-commit 2>/dev/null || true
|
|
10
|
+
`;
|
|
11
|
+
export async function hook(options) {
|
|
12
|
+
const repoPath = options.path || process.cwd();
|
|
13
|
+
const git = simpleGit(repoPath);
|
|
14
|
+
const isRepo = await git.checkIsRepo();
|
|
15
|
+
if (!isRepo) {
|
|
16
|
+
console.error(chalk.red('Not a git repository: ' + repoPath));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const gitDir = join(repoPath, '.git');
|
|
20
|
+
const hooksDir = join(gitDir, 'hooks');
|
|
21
|
+
const hookPath = join(hooksDir, 'post-commit');
|
|
22
|
+
if (options.remove) {
|
|
23
|
+
if (existsSync(hookPath)) {
|
|
24
|
+
const { unlinkSync } = await import('fs');
|
|
25
|
+
unlinkSync(hookPath);
|
|
26
|
+
console.log(chalk.green('✓ Removed post-commit hook'));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.log(chalk.yellow('No hook installed'));
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Create hooks directory if it doesn't exist
|
|
34
|
+
if (!existsSync(hooksDir)) {
|
|
35
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
// Check if hook already exists
|
|
38
|
+
if (existsSync(hookPath)) {
|
|
39
|
+
console.log(chalk.yellow('Hook already exists at: ' + hookPath));
|
|
40
|
+
console.log(chalk.dim('Use --remove to uninstall first'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Write the hook
|
|
44
|
+
writeFileSync(hookPath, HOOK_SCRIPT, { mode: 0o755 });
|
|
45
|
+
// Make sure it's executable (for Unix systems)
|
|
46
|
+
try {
|
|
47
|
+
chmodSync(hookPath, 0o755);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Windows doesn't need chmod
|
|
51
|
+
}
|
|
52
|
+
console.log(chalk.green('✓ Installed post-commit hook'));
|
|
53
|
+
console.log(chalk.dim(' Location: ' + hookPath));
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(chalk.cyan('Now every commit will automatically record a summary!'));
|
|
56
|
+
console.log(chalk.dim('To remove: stakeout hook --remove'));
|
|
57
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { simpleGit } from 'simple-git';
|
|
5
|
+
import { loadConfig, saveConfig } from '../lib/config.js';
|
|
6
|
+
import { hook } from './hook.js';
|
|
7
|
+
export async function init(options) {
|
|
8
|
+
const repoPath = options.path || process.cwd();
|
|
9
|
+
console.log(chalk.bold.green('\n🔍 STAKEOUT Setup\n'));
|
|
10
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
11
|
+
// Check if it's a git repo
|
|
12
|
+
const git = simpleGit(repoPath);
|
|
13
|
+
const isRepo = await git.checkIsRepo();
|
|
14
|
+
if (!isRepo) {
|
|
15
|
+
console.log(chalk.red('✗ Not a git repository'));
|
|
16
|
+
console.log(chalk.dim(` Path: ${repoPath}`));
|
|
17
|
+
console.log(chalk.dim(' Initialize with: git init'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log(chalk.green('✓ Git repository detected'));
|
|
21
|
+
console.log(chalk.dim(` Path: ${repoPath}`));
|
|
22
|
+
// Get repo info
|
|
23
|
+
const log = await git.log({ maxCount: 1 }).catch(() => null);
|
|
24
|
+
if (log?.latest) {
|
|
25
|
+
console.log(chalk.dim(` Latest commit: ${log.latest.hash.slice(0, 7)} - ${log.latest.message.slice(0, 40)}`));
|
|
26
|
+
}
|
|
27
|
+
// Update config with this path
|
|
28
|
+
const config = loadConfig();
|
|
29
|
+
config.watched_path = repoPath;
|
|
30
|
+
saveConfig(config);
|
|
31
|
+
console.log(chalk.green('✓ Set as watched path'));
|
|
32
|
+
// Check Ollama
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(chalk.dim('Checking LLM provider...'));
|
|
35
|
+
try {
|
|
36
|
+
const { Ollama } = await import('ollama');
|
|
37
|
+
const ollama = new Ollama({ host: config.ollama_host });
|
|
38
|
+
await ollama.list();
|
|
39
|
+
console.log(chalk.green(`✓ Ollama connected (${config.ollama_model})`));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
console.log(chalk.yellow('⚠ Ollama not available'));
|
|
43
|
+
console.log(chalk.dim(' Start with: ollama serve'));
|
|
44
|
+
console.log(chalk.dim(' Or use: stakeout config --provider openai'));
|
|
45
|
+
}
|
|
46
|
+
// Install hook if requested
|
|
47
|
+
if (options.hook !== false) {
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(chalk.dim('Installing git hook...'));
|
|
50
|
+
const hooksDir = join(repoPath, '.git', 'hooks');
|
|
51
|
+
const hookPath = join(hooksDir, 'post-commit');
|
|
52
|
+
if (existsSync(hookPath)) {
|
|
53
|
+
console.log(chalk.yellow('⚠ Hook already exists'));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
await hook({ path: repoPath });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Summary
|
|
60
|
+
console.log('');
|
|
61
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
62
|
+
console.log(chalk.bold.green('\n✓ STAKEOUT is ready!\n'));
|
|
63
|
+
console.log('Quick commands:');
|
|
64
|
+
console.log(chalk.cyan(' stakeout record') + chalk.dim(' # Record current changes'));
|
|
65
|
+
console.log(chalk.cyan(' stakeout record -c') + chalk.dim(' # Record last commit'));
|
|
66
|
+
console.log(chalk.cyan(' stakeout dash') + chalk.dim(' # Open web dashboard'));
|
|
67
|
+
console.log(chalk.cyan(' stakeout ui') + chalk.dim(' # Open terminal UI'));
|
|
68
|
+
console.log(chalk.cyan(' stakeout watch') + chalk.dim(' # Auto-record mode'));
|
|
69
|
+
console.log('');
|
|
70
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getEntries } from '../lib/database.js';
|
|
3
|
+
export async function log(options) {
|
|
4
|
+
const limit = options.limit ? parseInt(options.limit, 10) : 10;
|
|
5
|
+
const since = options.since ? parseSince(options.since) : undefined;
|
|
6
|
+
const entries = getEntries({
|
|
7
|
+
limit,
|
|
8
|
+
since,
|
|
9
|
+
path: options.path,
|
|
10
|
+
favorites: options.favorites,
|
|
11
|
+
tag: options.tag
|
|
12
|
+
});
|
|
13
|
+
if (entries.length === 0) {
|
|
14
|
+
console.log(chalk.yellow('No entries found.'));
|
|
15
|
+
console.log(chalk.dim('Record changes with: stakeout record'));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let title = 'STAKEOUT LOG';
|
|
19
|
+
if (options.favorites)
|
|
20
|
+
title = 'FAVORITES';
|
|
21
|
+
if (options.tag)
|
|
22
|
+
title = `TAG: ${options.tag}`;
|
|
23
|
+
console.log(chalk.bold(`\n📋 ${title} (${entries.length} entries)\n`));
|
|
24
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const date = new Date(entry.timestamp);
|
|
27
|
+
const timeAgo = getTimeAgo(date);
|
|
28
|
+
console.log('');
|
|
29
|
+
// Build status indicators
|
|
30
|
+
const indicators = [];
|
|
31
|
+
if (entry.favorite)
|
|
32
|
+
indicators.push(chalk.yellow('★'));
|
|
33
|
+
if (entry.is_breaking)
|
|
34
|
+
indicators.push(chalk.red('⚠️'));
|
|
35
|
+
console.log(chalk.cyan(`#${entry.id}`) +
|
|
36
|
+
(indicators.length > 0 ? ' ' + indicators.join(' ') : '') +
|
|
37
|
+
chalk.dim(` · ${timeAgo}`) +
|
|
38
|
+
(entry.commit_hash ? chalk.dim(` · ${entry.commit_hash.slice(0, 7)}`) : ''));
|
|
39
|
+
console.log(chalk.white(entry.summary));
|
|
40
|
+
if (entry.tags && entry.tags.length > 0) {
|
|
41
|
+
console.log(chalk.magenta(` 🏷️ ${entry.tags.join(', ')}`));
|
|
42
|
+
}
|
|
43
|
+
if (entry.notes) {
|
|
44
|
+
console.log(chalk.dim(` 📝 ${entry.notes}`));
|
|
45
|
+
}
|
|
46
|
+
if (entry.directories.length > 0) {
|
|
47
|
+
console.log(chalk.dim(` 📁 ${entry.directories.join(', ')}`));
|
|
48
|
+
}
|
|
49
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
50
|
+
}
|
|
51
|
+
console.log('');
|
|
52
|
+
}
|
|
53
|
+
function parseSince(since) {
|
|
54
|
+
const now = new Date();
|
|
55
|
+
const match = since.match(/^(\d+)\s*(day|days|week|weeks|hour|hours|month|months)$/i);
|
|
56
|
+
if (!match) {
|
|
57
|
+
// Try to parse as date
|
|
58
|
+
const parsed = new Date(since);
|
|
59
|
+
if (!isNaN(parsed.getTime())) {
|
|
60
|
+
return parsed.toISOString();
|
|
61
|
+
}
|
|
62
|
+
console.error(chalk.red(`Invalid --since format: ${since}`));
|
|
63
|
+
console.error(chalk.dim('Examples: "3 days", "1 week", "2 hours"'));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const amount = parseInt(match[1], 10);
|
|
67
|
+
const unit = match[2].toLowerCase();
|
|
68
|
+
switch (unit) {
|
|
69
|
+
case 'hour':
|
|
70
|
+
case 'hours':
|
|
71
|
+
now.setHours(now.getHours() - amount);
|
|
72
|
+
break;
|
|
73
|
+
case 'day':
|
|
74
|
+
case 'days':
|
|
75
|
+
now.setDate(now.getDate() - amount);
|
|
76
|
+
break;
|
|
77
|
+
case 'week':
|
|
78
|
+
case 'weeks':
|
|
79
|
+
now.setDate(now.getDate() - (amount * 7));
|
|
80
|
+
break;
|
|
81
|
+
case 'month':
|
|
82
|
+
case 'months':
|
|
83
|
+
now.setMonth(now.getMonth() - amount);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
return now.toISOString();
|
|
87
|
+
}
|
|
88
|
+
function getTimeAgo(date) {
|
|
89
|
+
const now = new Date();
|
|
90
|
+
const diffMs = now.getTime() - date.getTime();
|
|
91
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
92
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
93
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
94
|
+
if (diffMins < 1)
|
|
95
|
+
return 'just now';
|
|
96
|
+
if (diffMins < 60)
|
|
97
|
+
return `${diffMins}m ago`;
|
|
98
|
+
if (diffHours < 24)
|
|
99
|
+
return `${diffHours}h ago`;
|
|
100
|
+
if (diffDays < 7)
|
|
101
|
+
return `${diffDays}d ago`;
|
|
102
|
+
return date.toLocaleDateString();
|
|
103
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getEntry, updateEntry } from '../lib/database.js';
|
|
3
|
+
export async function note(entryId, options) {
|
|
4
|
+
const id = parseInt(entryId, 10);
|
|
5
|
+
if (isNaN(id)) {
|
|
6
|
+
console.error(chalk.red('Invalid entry ID'));
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const entry = getEntry(id);
|
|
10
|
+
if (!entry) {
|
|
11
|
+
console.error(chalk.red(`Entry #${id} not found`));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
// If no options, show current state
|
|
15
|
+
if (!options.note && !options.favorite && !options.unfavorite && !options.breaking && !options.notBreaking) {
|
|
16
|
+
console.log(chalk.bold(`\nEntry #${id}\n`));
|
|
17
|
+
console.log(chalk.dim('Summary: ') + entry.summary);
|
|
18
|
+
console.log(chalk.dim('Favorite: ') + (entry.favorite ? chalk.yellow('★ Yes') : 'No'));
|
|
19
|
+
console.log(chalk.dim('Breaking: ') + (entry.is_breaking ? chalk.red('⚠️ Yes') : 'No'));
|
|
20
|
+
console.log(chalk.dim('Notes: ') + (entry.notes || chalk.dim('(none)')));
|
|
21
|
+
console.log('');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const updates = {};
|
|
25
|
+
if (options.note !== undefined) {
|
|
26
|
+
updates.notes = options.note;
|
|
27
|
+
console.log(chalk.green(`✓ Updated note for entry #${id}`));
|
|
28
|
+
}
|
|
29
|
+
if (options.favorite) {
|
|
30
|
+
updates.favorite = true;
|
|
31
|
+
console.log(chalk.green(`✓ Marked entry #${id} as favorite ★`));
|
|
32
|
+
}
|
|
33
|
+
if (options.unfavorite) {
|
|
34
|
+
updates.favorite = false;
|
|
35
|
+
console.log(chalk.green(`✓ Removed favorite from entry #${id}`));
|
|
36
|
+
}
|
|
37
|
+
if (options.breaking) {
|
|
38
|
+
updates.is_breaking = true;
|
|
39
|
+
console.log(chalk.green(`✓ Marked entry #${id} as breaking change ⚠️`));
|
|
40
|
+
}
|
|
41
|
+
if (options.notBreaking) {
|
|
42
|
+
updates.is_breaking = false;
|
|
43
|
+
console.log(chalk.green(`✓ Unmarked entry #${id} as breaking change`));
|
|
44
|
+
}
|
|
45
|
+
if (Object.keys(updates).length > 0) {
|
|
46
|
+
updateEntry(id, updates);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { collectDiff, collectLastCommit } from '../lib/diff.js';
|
|
3
|
+
import { summarize } from '../lib/summarizer.js';
|
|
4
|
+
import { insertEntry, entryExists, updateRepoLastRecorded } from '../lib/database.js';
|
|
5
|
+
import { loadConfig } from '../lib/config.js';
|
|
6
|
+
// Default patterns that indicate potentially breaking changes
|
|
7
|
+
const DEFAULT_BREAKING_PATTERNS = [
|
|
8
|
+
/^src\/api\//i,
|
|
9
|
+
/^api\//i,
|
|
10
|
+
/schema/i,
|
|
11
|
+
/migration/i,
|
|
12
|
+
/\.proto$/i,
|
|
13
|
+
/breaking/i,
|
|
14
|
+
/major/i,
|
|
15
|
+
/deprecated/i,
|
|
16
|
+
/removed?/i,
|
|
17
|
+
/delete/i,
|
|
18
|
+
];
|
|
19
|
+
export async function record(options) {
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
const repoPath = options.path || config.watched_path || process.cwd();
|
|
22
|
+
console.log(chalk.dim('🔍 Collecting changes...'));
|
|
23
|
+
const diff = options.lastCommit
|
|
24
|
+
? await collectLastCommit(repoPath)
|
|
25
|
+
: await collectDiff(repoPath);
|
|
26
|
+
if (!diff) {
|
|
27
|
+
console.log(chalk.yellow('No changes detected.'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Check for duplicates
|
|
31
|
+
if (entryExists(diff.diff_hash)) {
|
|
32
|
+
console.log(chalk.yellow('These changes have already been recorded.'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.log(chalk.dim(`📁 ${diff.files.length} files changed`));
|
|
36
|
+
console.log(chalk.dim('🤖 Generating summary...'));
|
|
37
|
+
try {
|
|
38
|
+
const summary = await summarize(diff);
|
|
39
|
+
// Detect breaking changes
|
|
40
|
+
const isBreaking = detectBreakingChange(diff.files, diff.diff_text, summary);
|
|
41
|
+
const entry = {
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
files_changed: diff.files,
|
|
44
|
+
directories: diff.directories,
|
|
45
|
+
summary,
|
|
46
|
+
diff_hash: diff.diff_hash,
|
|
47
|
+
commit_hash: diff.commit_hash,
|
|
48
|
+
commit_message: diff.commit_message,
|
|
49
|
+
repo_path: repoPath,
|
|
50
|
+
is_breaking: isBreaking,
|
|
51
|
+
tags: [],
|
|
52
|
+
favorite: false,
|
|
53
|
+
notes: ''
|
|
54
|
+
};
|
|
55
|
+
const id = insertEntry(entry);
|
|
56
|
+
// Update repo last recorded time
|
|
57
|
+
updateRepoLastRecorded(repoPath);
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(chalk.green('✓ Recorded entry #' + id));
|
|
60
|
+
if (isBreaking) {
|
|
61
|
+
console.log(chalk.red.bold('⚠️ POTENTIAL BREAKING CHANGE DETECTED'));
|
|
62
|
+
}
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log(chalk.bold('Summary:'));
|
|
65
|
+
console.log(chalk.white(summary));
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(chalk.dim(`Files: ${diff.files.slice(0, 5).join(', ')}${diff.files.length > 5 ? ` (+${diff.files.length - 5} more)` : ''}`));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error(chalk.red('Error: ' + error.message));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function detectBreakingChange(files, diffText, summary) {
|
|
75
|
+
// Check files against breaking patterns
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
for (const pattern of DEFAULT_BREAKING_PATTERNS) {
|
|
78
|
+
if (pattern.test(file)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Check summary for breaking keywords
|
|
84
|
+
const breakingKeywords = [
|
|
85
|
+
'breaking change',
|
|
86
|
+
'breaking:',
|
|
87
|
+
'removed',
|
|
88
|
+
'deleted',
|
|
89
|
+
'deprecated',
|
|
90
|
+
'incompatible',
|
|
91
|
+
'migration required',
|
|
92
|
+
'schema change',
|
|
93
|
+
'api change'
|
|
94
|
+
];
|
|
95
|
+
const lowerSummary = summary.toLowerCase();
|
|
96
|
+
for (const keyword of breakingKeywords) {
|
|
97
|
+
if (lowerSummary.includes(keyword)) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Check diff for removal of exports or public APIs
|
|
102
|
+
if (diffText.includes('-export ') || diffText.includes('-public ')) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|