@sumant.pathak/devjar 1.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.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/bin/devjar.js +55 -0
- package/package.json +35 -0
- package/src/config.js +103 -0
- package/src/init.js +388 -0
- package/src/prompt.js +135 -0
- package/src/providers/anthropic.js +27 -0
- package/src/providers/gemini.js +37 -0
- package/src/providers/index.js +54 -0
- package/src/providers/ollama.js +40 -0
- package/src/providers/openai.js +37 -0
- package/src/stats.js +173 -0
- package/src/update.js +235 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Provider: OpenAI — gpt-4o-mini (cheap, fast)
|
|
2
|
+
// Requires: API key from https://platform.openai.com
|
|
3
|
+
|
|
4
|
+
export const name = 'openai';
|
|
5
|
+
export const defaultModel = 'gpt-4o-mini';
|
|
6
|
+
|
|
7
|
+
export async function normalize(userPrompt, systemPrompt, config = {}) {
|
|
8
|
+
const apiKey = config.apiKey || process.env.OPENAI_API_KEY;
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
'OpenAI API key required.\n' +
|
|
12
|
+
' Get key: https://platform.openai.com/api-keys\n' +
|
|
13
|
+
' Set it: devjar config --provider openai --key sk-...'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
model: config.model || defaultModel,
|
|
22
|
+
max_tokens: 500,
|
|
23
|
+
messages: [
|
|
24
|
+
{ role: 'system', content: systemPrompt },
|
|
25
|
+
{ role: 'user', content: userPrompt },
|
|
26
|
+
],
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const err = await res.json().catch(() => ({}));
|
|
32
|
+
throw new Error(`OpenAI error ${res.status}: ${err?.error?.message || res.statusText}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
return data.choices?.[0]?.message?.content?.trim() || '';
|
|
37
|
+
}
|
package/src/stats.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// devjar stats — token usage tracker
|
|
2
|
+
// Reads ~/.devjar/history.json, shows savings estimates
|
|
3
|
+
// --json flag for raw output
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
const HISTORY_FILE = path.join(os.homedir(), '.devjar', 'history.json');
|
|
11
|
+
const AVG_STARC_TOKENS = 200; // avg tokens in a structured STAR-C output
|
|
12
|
+
|
|
13
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
function loadHistory() {
|
|
16
|
+
try {
|
|
17
|
+
if (!fs.existsSync(HISTORY_FILE)) return [];
|
|
18
|
+
return JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
|
|
19
|
+
} catch { return []; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function estimateTokensSaved(entry) {
|
|
23
|
+
const rawChars = (entry.original || '').length;
|
|
24
|
+
// Formula: (original_chars - 200) × 0.25
|
|
25
|
+
// Negative = vague prompt was already short, minimal saving
|
|
26
|
+
return Math.max(0, Math.round((rawChars - 200) * 0.25));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toDateStr(iso) {
|
|
30
|
+
return iso ? iso.slice(0, 10) : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function todayStr() {
|
|
34
|
+
return new Date().toISOString().slice(0, 10);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function calcStreak(history) {
|
|
38
|
+
if (!history.length) return 0;
|
|
39
|
+
const days = [...new Set(history.map(e => toDateStr(e.timestamp)))].sort().reverse();
|
|
40
|
+
let streak = 0;
|
|
41
|
+
let expected = todayStr();
|
|
42
|
+
for (const day of days) {
|
|
43
|
+
if (day === expected) {
|
|
44
|
+
streak++;
|
|
45
|
+
const d = new Date(expected);
|
|
46
|
+
d.setDate(d.getDate() - 1);
|
|
47
|
+
expected = d.toISOString().slice(0, 10);
|
|
48
|
+
} else break;
|
|
49
|
+
}
|
|
50
|
+
return streak;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function avg(arr) {
|
|
54
|
+
if (!arr.length) return 0;
|
|
55
|
+
return Math.round(arr.reduce((s, v) => s + v, 0) / arr.length);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function bar(value, max, width = 20) {
|
|
59
|
+
const filled = max > 0 ? Math.round((value / max) * width) : 0;
|
|
60
|
+
return chalk.cyan('█'.repeat(filled)) + chalk.gray('░'.repeat(width - filled));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Main Export ────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export async function stats(options = {}) {
|
|
66
|
+
if (options.reset) {
|
|
67
|
+
try {
|
|
68
|
+
fs.writeFileSync(HISTORY_FILE, '[]', 'utf8');
|
|
69
|
+
console.log(chalk.green('✓ History reset.'));
|
|
70
|
+
} catch (e) {
|
|
71
|
+
console.log(chalk.red('✗ Could not reset: ' + e.message));
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const history = loadHistory();
|
|
77
|
+
|
|
78
|
+
if (!history.length) {
|
|
79
|
+
console.log(chalk.yellow('No history yet.') + chalk.gray(' Run `devjar prompt` to start tracking.'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Compute today ──
|
|
84
|
+
const today = history.filter(e => toDateStr(e.timestamp) === todayStr());
|
|
85
|
+
const todayTokens = today.reduce((s, e) => s + estimateTokensSaved(e), 0);
|
|
86
|
+
const todayOrigLens = today.map(e => (e.original || '').length);
|
|
87
|
+
const todayStructLens = today.map(e => (e.structured || '').length);
|
|
88
|
+
|
|
89
|
+
// ── Compute all-time ──
|
|
90
|
+
const totalTokens = history.reduce((s, e) => s + estimateTokensSaved(e), 0);
|
|
91
|
+
const streak = calcStreak(history);
|
|
92
|
+
|
|
93
|
+
// Most active project
|
|
94
|
+
const projectCounts = {};
|
|
95
|
+
for (const e of history) {
|
|
96
|
+
projectCounts[e.project || 'unknown'] = (projectCounts[e.project || 'unknown'] || 0) + 1;
|
|
97
|
+
}
|
|
98
|
+
const topProject = Object.entries(projectCounts).sort((a, b) => b[1] - a[1])[0];
|
|
99
|
+
|
|
100
|
+
// Per-project breakdown (top 5)
|
|
101
|
+
const projectList = Object.entries(projectCounts)
|
|
102
|
+
.sort((a, b) => b[1] - a[1])
|
|
103
|
+
.slice(0, 5);
|
|
104
|
+
|
|
105
|
+
const maxCount = projectList[0]?.[1] || 1;
|
|
106
|
+
|
|
107
|
+
if (options.json) {
|
|
108
|
+
console.log(JSON.stringify({
|
|
109
|
+
today: {
|
|
110
|
+
prompts: today.length,
|
|
111
|
+
avgOriginalLen: avg(todayOrigLens),
|
|
112
|
+
avgStructuredLen: avg(todayStructLens),
|
|
113
|
+
estimatedTokensSaved: todayTokens,
|
|
114
|
+
},
|
|
115
|
+
allTime: {
|
|
116
|
+
totalPrompts: history.length,
|
|
117
|
+
totalEstimatedTokensSaved: totalTokens,
|
|
118
|
+
mostActiveProject: topProject?.[0],
|
|
119
|
+
streakDays: streak,
|
|
120
|
+
projects: Object.fromEntries(projectList),
|
|
121
|
+
},
|
|
122
|
+
}, null, 2));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Render ──
|
|
127
|
+
console.log(chalk.bold.blue('⬡ devjar') + chalk.gray(' stats'));
|
|
128
|
+
console.log(chalk.gray('─'.repeat(52)));
|
|
129
|
+
|
|
130
|
+
// Today
|
|
131
|
+
console.log(chalk.bold.white('Today') + chalk.gray(` (${todayStr()})`));
|
|
132
|
+
if (today.length === 0) {
|
|
133
|
+
console.log(chalk.gray(' No prompts yet today.'));
|
|
134
|
+
} else {
|
|
135
|
+
console.log(
|
|
136
|
+
chalk.gray(' Prompts normalized: ') + chalk.cyan(today.length)
|
|
137
|
+
);
|
|
138
|
+
console.log(
|
|
139
|
+
chalk.gray(' Avg original length: ') + chalk.white(avg(todayOrigLens) + ' chars')
|
|
140
|
+
);
|
|
141
|
+
console.log(
|
|
142
|
+
chalk.gray(' Avg structured length:') + chalk.white(avg(todayStructLens) + ' chars')
|
|
143
|
+
);
|
|
144
|
+
console.log(
|
|
145
|
+
chalk.gray(' Estimated tokens saved:') + chalk.green(` ~${todayTokens}`)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log();
|
|
150
|
+
console.log(chalk.bold.white('All Time'));
|
|
151
|
+
console.log(chalk.gray(' Total prompts: ') + chalk.cyan(history.length));
|
|
152
|
+
console.log(chalk.gray(' Total tokens saved: ') + chalk.green(`~${totalTokens.toLocaleString()}`));
|
|
153
|
+
console.log(chalk.gray(' Most active project: ') + chalk.white(topProject?.[0] || '—') + chalk.gray(` (${topProject?.[1] || 0} prompts)`));
|
|
154
|
+
console.log(
|
|
155
|
+
chalk.gray(' Streak: ') +
|
|
156
|
+
(streak > 0 ? chalk.yellow('🔥 ' + streak + ' day' + (streak > 1 ? 's' : '')) : chalk.gray('0 days'))
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (projectList.length > 1) {
|
|
160
|
+
console.log();
|
|
161
|
+
console.log(chalk.bold.white('Projects'));
|
|
162
|
+
for (const [name, count] of projectList) {
|
|
163
|
+
console.log(
|
|
164
|
+
` ${bar(count, maxCount, 16)} ` +
|
|
165
|
+
chalk.white(name.padEnd(20)) +
|
|
166
|
+
chalk.gray(` ${count} prompt${count > 1 ? 's' : ''}`)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(chalk.gray('─'.repeat(52)));
|
|
172
|
+
console.log(chalk.gray(` History: ${HISTORY_FILE}`));
|
|
173
|
+
}
|
package/src/update.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// devjar update — regenerate or live-watch CLAUDE.md
|
|
2
|
+
// One-time: full 3-phase re-scan (same as init but overwrites)
|
|
3
|
+
// --watch: chokidar file watcher, Phase 1 on every change,
|
|
4
|
+
// Phase 2 (Haiku) every 10 changes
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import chokidar from 'chokidar';
|
|
10
|
+
import {
|
|
11
|
+
init,
|
|
12
|
+
walkDir,
|
|
13
|
+
buildFileTree,
|
|
14
|
+
readJsonSafe,
|
|
15
|
+
detectStack,
|
|
16
|
+
pickKeyFiles,
|
|
17
|
+
extractPatterns,
|
|
18
|
+
} from './init.js';
|
|
19
|
+
|
|
20
|
+
const IGNORE_WATCH = [
|
|
21
|
+
'node_modules', '.git', 'dist', '.next', 'out', 'build',
|
|
22
|
+
'CLAUDE.md', '*.lock', '.env*',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const IGNORE_DIRS_WATCH = new Set([
|
|
26
|
+
'node_modules', '.git', 'dist', '.next', 'out', 'build', '.cache', 'coverage', 'public',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function findClaudeMd(baseDir) {
|
|
32
|
+
return path.join(baseDir, 'CLAUDE.md');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function countExistingFiles(claudeMdPath) {
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(claudeMdPath, 'utf8');
|
|
38
|
+
return (content.match(/📄/g) || []).length;
|
|
39
|
+
} catch { return 0; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function patchFileTreeSection(claudeMdPath, newTree) {
|
|
43
|
+
try {
|
|
44
|
+
let content = fs.readFileSync(claudeMdPath, 'utf8');
|
|
45
|
+
// Replace between ## Project Map and the next ##
|
|
46
|
+
content = content.replace(
|
|
47
|
+
/(## Project Map\n)([\s\S]*?)(\n## )/,
|
|
48
|
+
`$1${newTree}\n$3`
|
|
49
|
+
);
|
|
50
|
+
fs.writeFileSync(claudeMdPath, content, 'utf8');
|
|
51
|
+
return true;
|
|
52
|
+
} catch { return false; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function patchRulesSection(claudeMdPath, rules) {
|
|
56
|
+
try {
|
|
57
|
+
let content = fs.readFileSync(claudeMdPath, 'utf8');
|
|
58
|
+
const rulesText = rules.map((r, i) => `${i + 1}. ${r}`).join('\n');
|
|
59
|
+
content = content.replace(
|
|
60
|
+
/(## Unbreakable Rules\n)([\s\S]*?)(\n---)/,
|
|
61
|
+
`$1${rulesText}\n$3`
|
|
62
|
+
);
|
|
63
|
+
fs.writeFileSync(claudeMdPath, content, 'utf8');
|
|
64
|
+
return true;
|
|
65
|
+
} catch { return false; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function timestamp() {
|
|
69
|
+
return chalk.gray(`[${new Date().toLocaleTimeString()}]`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Phase 1 only: update file tree ────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function runPhase1(baseDir) {
|
|
75
|
+
const BINARY_EXTS = new Set([
|
|
76
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.webp',
|
|
77
|
+
'.woff', '.woff2', '.ttf', '.eot', '.mp4', '.mp3', '.pdf',
|
|
78
|
+
'.zip', '.exe', '.dll', '.bin', '.map',
|
|
79
|
+
]);
|
|
80
|
+
const SKIP_FILES = new Set([
|
|
81
|
+
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb',
|
|
82
|
+
'.env', '.env.local', '.env.production', '.DS_Store',
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
function walk(dir, depth = 0) {
|
|
86
|
+
const entries = [];
|
|
87
|
+
if (depth > 2) return entries;
|
|
88
|
+
let items;
|
|
89
|
+
try { items = fs.readdirSync(dir, { withFileTypes: true }); } catch { return entries; }
|
|
90
|
+
for (const item of items) {
|
|
91
|
+
if (item.name.startsWith('.') && item.name !== '.env.example') continue;
|
|
92
|
+
if (IGNORE_DIRS_WATCH.has(item.name)) continue;
|
|
93
|
+
const fullPath = path.join(dir, item.name);
|
|
94
|
+
const relPath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
95
|
+
if (item.isDirectory()) {
|
|
96
|
+
entries.push({ type: 'dir', path: relPath, name: item.name });
|
|
97
|
+
entries.push(...walk(fullPath, depth + 1));
|
|
98
|
+
} else if (item.isFile()) {
|
|
99
|
+
const ext = path.extname(item.name).toLowerCase();
|
|
100
|
+
if (BINARY_EXTS.has(ext) || SKIP_FILES.has(item.name)) continue;
|
|
101
|
+
const size = fs.statSync(fullPath).size;
|
|
102
|
+
entries.push({ type: 'file', path: relPath, name: item.name, size });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return entries;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const entries = walk(baseDir);
|
|
109
|
+
return { entries, tree: buildFileTree(entries), fileCount: entries.filter(e => e.type === 'file').length };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Watch mode ─────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
async function runWatch(baseDir, mapFile, options) {
|
|
115
|
+
const projectName = readJsonSafe(path.join(baseDir, 'package.json'))?.name || path.basename(baseDir);
|
|
116
|
+
|
|
117
|
+
console.log(chalk.bold.blue('⬡ devjar') + chalk.gray(` watching ${chalk.white(projectName)}...`));
|
|
118
|
+
console.log(chalk.gray(` CLAUDE.md: ${mapFile}`));
|
|
119
|
+
console.log(chalk.gray(' Ctrl+C to stop\n'));
|
|
120
|
+
|
|
121
|
+
let changeCount = 0;
|
|
122
|
+
let haikusFired = 0;
|
|
123
|
+
let debounceTimer = null;
|
|
124
|
+
|
|
125
|
+
const watcher = chokidar.watch(baseDir, {
|
|
126
|
+
ignored: [
|
|
127
|
+
/(^|[/\\])\../, // dotfiles
|
|
128
|
+
/node_modules/,
|
|
129
|
+
/\.git/,
|
|
130
|
+
/dist/,
|
|
131
|
+
/\.next/,
|
|
132
|
+
/CLAUDE\.md/,
|
|
133
|
+
/.*\.lock$/,
|
|
134
|
+
],
|
|
135
|
+
persistent: true,
|
|
136
|
+
ignoreInitial: true,
|
|
137
|
+
depth: 3,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
async function handleChange(eventType, filePath) {
|
|
141
|
+
const relPath = path.relative(baseDir, filePath).replace(/\\/g, '/');
|
|
142
|
+
changeCount++;
|
|
143
|
+
|
|
144
|
+
const icon = eventType === 'add' ? chalk.green('+') :
|
|
145
|
+
eventType === 'unlink' ? chalk.red('-') :
|
|
146
|
+
chalk.yellow('~');
|
|
147
|
+
|
|
148
|
+
process.stdout.write(
|
|
149
|
+
`${timestamp()} ${icon} ${chalk.white(relPath)}\n`
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Debounce Phase 1 updates
|
|
153
|
+
clearTimeout(debounceTimer);
|
|
154
|
+
debounceTimer = setTimeout(async () => {
|
|
155
|
+
const { tree, fileCount } = runPhase1(baseDir);
|
|
156
|
+
const ok = patchFileTreeSection(mapFile, tree);
|
|
157
|
+
if (ok) {
|
|
158
|
+
process.stdout.write(
|
|
159
|
+
`${timestamp()} ${chalk.green('↑')} CLAUDE.md updated — ` +
|
|
160
|
+
chalk.white(`${fileCount} files`) +
|
|
161
|
+
(eventType === 'add' ? chalk.gray(` — added ${relPath}`) : '') +
|
|
162
|
+
'\n'
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Every 10 changes: trigger full Haiku re-analysis
|
|
167
|
+
if (changeCount % 10 === 0 && process.env.ANTHROPIC_API_KEY) {
|
|
168
|
+
haikusFired++;
|
|
169
|
+
process.stdout.write(
|
|
170
|
+
`${timestamp()} ${chalk.cyan('⟳')} ${chalk.yellow('10 changes reached — re-analyzing rules via Haiku...')}\n`
|
|
171
|
+
);
|
|
172
|
+
try {
|
|
173
|
+
const { entries } = runPhase1(baseDir);
|
|
174
|
+
const { contents } = pickKeyFiles(baseDir, entries);
|
|
175
|
+
const patterns = await extractPatterns(contents);
|
|
176
|
+
if (patterns?.rules) {
|
|
177
|
+
patchRulesSection(mapFile, patterns.rules);
|
|
178
|
+
process.stdout.write(
|
|
179
|
+
`${timestamp()} ${chalk.green('✓')} Rules updated — ` +
|
|
180
|
+
chalk.white(`${patterns.rules.length} rules extracted\n`)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
} catch (e) {
|
|
184
|
+
process.stdout.write(chalk.yellow(`${timestamp()} ⚠ Haiku re-analysis failed: ${e.message}\n`));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}, parseInt(options.debounce || '500', 10));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
watcher.on('add', (p) => handleChange('add', p));
|
|
191
|
+
watcher.on('change', (p) => handleChange('change', p));
|
|
192
|
+
watcher.on('unlink', (p) => handleChange('unlink', p));
|
|
193
|
+
watcher.on('error', (e) => console.error(chalk.red('Watcher error:'), e));
|
|
194
|
+
|
|
195
|
+
// Keep process alive
|
|
196
|
+
process.on('SIGINT', () => {
|
|
197
|
+
console.log(chalk.gray(`\n\n Stopped after ${changeCount} changes, ${haikusFired} Haiku calls.`));
|
|
198
|
+
watcher.close();
|
|
199
|
+
process.exit(0);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Main Export ────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
export async function update(dir = '.', options = {}) {
|
|
206
|
+
const baseDir = path.resolve(dir);
|
|
207
|
+
const mapFile = findClaudeMd(baseDir);
|
|
208
|
+
|
|
209
|
+
if (!fs.existsSync(mapFile)) {
|
|
210
|
+
console.log(chalk.red('✗ No CLAUDE.md found.') + chalk.gray(' Run ') + chalk.cyan('devjar init') + chalk.gray(' first.'));
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Watch mode ──
|
|
215
|
+
if (options.watch) {
|
|
216
|
+
return runWatch(baseDir, mapFile, options);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── One-time regenerate ──
|
|
220
|
+
console.log(chalk.bold.blue('⬡ devjar') + chalk.gray(' update'));
|
|
221
|
+
console.log(chalk.gray('─'.repeat(52)));
|
|
222
|
+
|
|
223
|
+
const prevFileCount = countExistingFiles(mapFile);
|
|
224
|
+
|
|
225
|
+
// Delegate to init (overwrites CLAUDE.md)
|
|
226
|
+
await init(dir, { output: mapFile, depth: options.depth || '2' });
|
|
227
|
+
|
|
228
|
+
const { fileCount: newFileCount } = runPhase1(baseDir);
|
|
229
|
+
const diff = newFileCount - prevFileCount;
|
|
230
|
+
const diffStr = diff > 0 ? chalk.green(`+${diff} new files`) :
|
|
231
|
+
diff < 0 ? chalk.red(`${diff} files removed`) :
|
|
232
|
+
chalk.gray('no file changes');
|
|
233
|
+
|
|
234
|
+
console.log(chalk.gray(` ${diffStr}`));
|
|
235
|
+
}
|