codesession-cli 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 +200 -0
- package/dist/db.d.ts +15 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +229 -0
- package/dist/db.js.map +1 -0
- package/dist/formatters.d.ts +9 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +121 -0
- package/dist/formatters.js.map +1 -0
- package/dist/git.d.ts +7 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +47 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +168 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/watcher.d.ts +3 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +48 -0
- package/dist/watcher.js.map +1 -0
- package/package.json +46 -0
- package/src/db.ts +239 -0
- package/src/formatters.ts +152 -0
- package/src/git.ts +44 -0
- package/src/index.ts +206 -0
- package/src/types.ts +49 -0
- package/src/watcher.ts +52 -0
- package/test.ts +4 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import { Session, SessionStats, FileChange, Commit } from './types';
|
|
4
|
+
import { formatDistanceToNow, format } from 'date-fns';
|
|
5
|
+
|
|
6
|
+
export function formatDuration(seconds: number): string {
|
|
7
|
+
const hours = Math.floor(seconds / 3600);
|
|
8
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
9
|
+
|
|
10
|
+
if (hours > 0) {
|
|
11
|
+
return `${hours}h ${minutes}m`;
|
|
12
|
+
}
|
|
13
|
+
return `${minutes}m`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatCost(cost: number): string {
|
|
17
|
+
return `$${cost.toFixed(2)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function displaySession(session: Session): void {
|
|
21
|
+
console.log(chalk.bold.cyan(`\nSession: ${session.name}\n`));
|
|
22
|
+
|
|
23
|
+
const table = new Table({
|
|
24
|
+
head: [chalk.cyan.bold('Metric'), chalk.cyan.bold('Value')],
|
|
25
|
+
style: { head: [], border: [] },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
table.push(
|
|
29
|
+
['Status', session.status === 'active' ? chalk.green('Active') : chalk.gray('Completed')],
|
|
30
|
+
['Started', format(new Date(session.startTime), 'MMM dd, yyyy HH:mm')],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (session.endTime) {
|
|
34
|
+
table.push(['Ended', format(new Date(session.endTime), 'MMM dd, yyyy HH:mm')]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (session.duration) {
|
|
38
|
+
table.push(['Duration', chalk.white(formatDuration(session.duration))]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
table.push(
|
|
42
|
+
['Files Changed', chalk.white(session.filesChanged.toString())],
|
|
43
|
+
['Commits', chalk.white(session.commits.toString())],
|
|
44
|
+
['AI Tokens', chalk.white(session.aiTokens.toLocaleString())],
|
|
45
|
+
['AI Cost', chalk.yellow(formatCost(session.aiCost))],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (session.notes) {
|
|
49
|
+
table.push(['Notes', chalk.gray(session.notes)]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(table.toString());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function displaySessions(sessions: Session[]): void {
|
|
56
|
+
if (sessions.length === 0) {
|
|
57
|
+
console.log(chalk.yellow('\nNo sessions found.\n'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const table = new Table({
|
|
62
|
+
head: [
|
|
63
|
+
chalk.cyan.bold('ID'),
|
|
64
|
+
chalk.cyan.bold('Name'),
|
|
65
|
+
chalk.cyan.bold('Started'),
|
|
66
|
+
chalk.cyan.bold('Duration'),
|
|
67
|
+
chalk.cyan.bold('Files'),
|
|
68
|
+
chalk.cyan.bold('Commits'),
|
|
69
|
+
chalk.cyan.bold('AI Cost'),
|
|
70
|
+
],
|
|
71
|
+
style: { head: [], border: [] },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
for (const session of sessions) {
|
|
75
|
+
table.push([
|
|
76
|
+
chalk.gray(`#${session.id}`),
|
|
77
|
+
session.status === 'active' ? chalk.green(session.name) : chalk.white(session.name),
|
|
78
|
+
formatDistanceToNow(new Date(session.startTime), { addSuffix: true }),
|
|
79
|
+
session.duration ? formatDuration(session.duration) : chalk.gray('ongoing'),
|
|
80
|
+
session.filesChanged.toString(),
|
|
81
|
+
session.commits.toString(),
|
|
82
|
+
chalk.yellow(formatCost(session.aiCost)),
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log('\n' + table.toString() + '\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function displayStats(stats: SessionStats): void {
|
|
90
|
+
console.log(chalk.bold.cyan('\nOverall Stats\n'));
|
|
91
|
+
|
|
92
|
+
const table = new Table({
|
|
93
|
+
head: [chalk.cyan.bold('Metric'), chalk.cyan.bold('Value')],
|
|
94
|
+
style: { head: [], border: [] },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
table.push(
|
|
98
|
+
['Total Sessions', chalk.white(stats.totalSessions.toLocaleString())],
|
|
99
|
+
['Total Time', chalk.white(formatDuration(stats.totalTime))],
|
|
100
|
+
['Average Session', chalk.white(formatDuration(stats.avgSessionTime))],
|
|
101
|
+
['Files Changed', chalk.white(stats.totalFiles.toLocaleString())],
|
|
102
|
+
['Commits', chalk.white(stats.totalCommits.toLocaleString())],
|
|
103
|
+
['Total AI Cost', chalk.yellow(formatCost(stats.totalAICost))],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
console.log(table.toString() + '\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function displayFileChanges(changes: FileChange[]): void {
|
|
110
|
+
if (changes.length === 0) return;
|
|
111
|
+
|
|
112
|
+
console.log(chalk.bold.cyan('\nFile Changes\n'));
|
|
113
|
+
|
|
114
|
+
const table = new Table({
|
|
115
|
+
head: [chalk.cyan.bold('Type'), chalk.cyan.bold('File'), chalk.cyan.bold('Time')],
|
|
116
|
+
style: { head: [], border: [] },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
for (const change of changes) {
|
|
120
|
+
const typeColor = change.changeType === 'created' ? chalk.green :
|
|
121
|
+
change.changeType === 'modified' ? chalk.yellow : chalk.red;
|
|
122
|
+
|
|
123
|
+
table.push([
|
|
124
|
+
typeColor(change.changeType),
|
|
125
|
+
change.filePath,
|
|
126
|
+
formatDistanceToNow(new Date(change.timestamp), { addSuffix: true }),
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(table.toString() + '\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function displayCommits(commits: Commit[]): void {
|
|
134
|
+
if (commits.length === 0) return;
|
|
135
|
+
|
|
136
|
+
console.log(chalk.bold.cyan('\nCommits\n'));
|
|
137
|
+
|
|
138
|
+
const table = new Table({
|
|
139
|
+
head: [chalk.cyan.bold('Hash'), chalk.cyan.bold('Message'), chalk.cyan.bold('Time')],
|
|
140
|
+
style: { head: [], border: [] },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
for (const commit of commits) {
|
|
144
|
+
table.push([
|
|
145
|
+
chalk.gray(commit.hash),
|
|
146
|
+
commit.message,
|
|
147
|
+
formatDistanceToNow(new Date(commit.timestamp), { addSuffix: true }),
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(table.toString() + '\n');
|
|
152
|
+
}
|
package/src/git.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { simpleGit, SimpleGit } from 'simple-git';
|
|
2
|
+
import { addCommit } from './db';
|
|
3
|
+
|
|
4
|
+
let git: SimpleGit;
|
|
5
|
+
let lastCommitHash: string | null = null;
|
|
6
|
+
|
|
7
|
+
export function initGit(cwd: string): void {
|
|
8
|
+
git = simpleGit(cwd);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function checkForNewCommits(sessionId: number): Promise<void> {
|
|
12
|
+
if (!git) return;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const log = await git.log({ maxCount: 1 });
|
|
16
|
+
if (log.latest && log.latest.hash !== lastCommitHash) {
|
|
17
|
+
lastCommitHash = log.latest.hash;
|
|
18
|
+
|
|
19
|
+
addCommit({
|
|
20
|
+
sessionId,
|
|
21
|
+
hash: log.latest.hash.substring(0, 7),
|
|
22
|
+
message: log.latest.message,
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
// Not a git repo or no commits yet
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getGitInfo(): Promise<{ branch: string; hasChanges: boolean } | null> {
|
|
32
|
+
if (!git) return null;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
36
|
+
const status = await git.status();
|
|
37
|
+
return {
|
|
38
|
+
branch: branch.trim(),
|
|
39
|
+
hasChanges: !status.isClean(),
|
|
40
|
+
};
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import {
|
|
6
|
+
createSession,
|
|
7
|
+
getActiveSession,
|
|
8
|
+
endSession,
|
|
9
|
+
getSession,
|
|
10
|
+
getSessions,
|
|
11
|
+
getStats,
|
|
12
|
+
getFileChanges,
|
|
13
|
+
getCommits,
|
|
14
|
+
addAIUsage
|
|
15
|
+
} from './db';
|
|
16
|
+
import { initGit, checkForNewCommits, getGitInfo } from './git';
|
|
17
|
+
import { startWatcher, stopWatcher } from './watcher';
|
|
18
|
+
import {
|
|
19
|
+
displaySession,
|
|
20
|
+
displaySessions,
|
|
21
|
+
displayStats,
|
|
22
|
+
displayFileChanges,
|
|
23
|
+
displayCommits
|
|
24
|
+
} from './formatters';
|
|
25
|
+
|
|
26
|
+
const program = new Command();
|
|
27
|
+
|
|
28
|
+
program
|
|
29
|
+
.name('devsession')
|
|
30
|
+
.description('Track your AI coding sessions: time, files, commits, AI costs')
|
|
31
|
+
.version('1.0.0');
|
|
32
|
+
|
|
33
|
+
// Start command
|
|
34
|
+
program
|
|
35
|
+
.command('start')
|
|
36
|
+
.description('Start a new coding session')
|
|
37
|
+
.argument('<name>', 'Session name')
|
|
38
|
+
.action(async (name: string) => {
|
|
39
|
+
const active = getActiveSession();
|
|
40
|
+
if (active) {
|
|
41
|
+
console.log(chalk.yellow(`\nSession "${active.name}" is already active.`));
|
|
42
|
+
console.log(chalk.gray('End it with: ds end\n'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cwd = process.cwd();
|
|
47
|
+
const sessionId = createSession({
|
|
48
|
+
name,
|
|
49
|
+
startTime: new Date().toISOString(),
|
|
50
|
+
workingDirectory: cwd,
|
|
51
|
+
filesChanged: 0,
|
|
52
|
+
commits: 0,
|
|
53
|
+
aiCost: 0,
|
|
54
|
+
aiTokens: 0,
|
|
55
|
+
status: 'active',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Initialize git tracking
|
|
59
|
+
initGit(cwd);
|
|
60
|
+
|
|
61
|
+
// Start file watcher
|
|
62
|
+
startWatcher(sessionId, cwd);
|
|
63
|
+
|
|
64
|
+
// Check for commits every 10 seconds
|
|
65
|
+
const gitInterval = setInterval(async () => {
|
|
66
|
+
await checkForNewCommits(sessionId);
|
|
67
|
+
}, 10000);
|
|
68
|
+
|
|
69
|
+
// Store interval ID
|
|
70
|
+
(global as any).gitInterval = gitInterval;
|
|
71
|
+
|
|
72
|
+
const gitInfo = await getGitInfo();
|
|
73
|
+
|
|
74
|
+
console.log(chalk.green(`\n✓ Session started: ${name}`));
|
|
75
|
+
if (gitInfo) {
|
|
76
|
+
console.log(chalk.gray(` Branch: ${gitInfo.branch}`));
|
|
77
|
+
}
|
|
78
|
+
console.log(chalk.gray(` Directory: ${cwd}`));
|
|
79
|
+
console.log(chalk.gray('\n Tracking: files, commits, AI usage'));
|
|
80
|
+
console.log(chalk.gray(' End with: ds end\n'));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// End command
|
|
84
|
+
program
|
|
85
|
+
.command('end')
|
|
86
|
+
.description('End the active session')
|
|
87
|
+
.option('-n, --notes <notes>', 'Session notes')
|
|
88
|
+
.action((options) => {
|
|
89
|
+
const session = getActiveSession();
|
|
90
|
+
if (!session) {
|
|
91
|
+
console.log(chalk.yellow('\nNo active session.\n'));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Stop tracking
|
|
96
|
+
stopWatcher();
|
|
97
|
+
if ((global as any).gitInterval) {
|
|
98
|
+
clearInterval((global as any).gitInterval);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
endSession(session.id!, new Date().toISOString(), options.notes);
|
|
102
|
+
|
|
103
|
+
const updated = getSession(session.id!);
|
|
104
|
+
if (updated) {
|
|
105
|
+
console.log(chalk.green('\n✓ Session ended\n'));
|
|
106
|
+
displaySession(updated);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Show command
|
|
111
|
+
program
|
|
112
|
+
.command('show')
|
|
113
|
+
.description('Show session details')
|
|
114
|
+
.argument('[id]', 'Session ID (defaults to last session)')
|
|
115
|
+
.option('--files', 'Show file changes')
|
|
116
|
+
.option('--commits', 'Show commits')
|
|
117
|
+
.action((id: string | undefined, options) => {
|
|
118
|
+
let session;
|
|
119
|
+
|
|
120
|
+
if (id) {
|
|
121
|
+
session = getSession(parseInt(id));
|
|
122
|
+
} else {
|
|
123
|
+
const sessions = getSessions(1);
|
|
124
|
+
session = sessions[0];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!session) {
|
|
128
|
+
console.log(chalk.yellow('\nSession not found.\n'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
displaySession(session);
|
|
133
|
+
|
|
134
|
+
if (options.files) {
|
|
135
|
+
const files = getFileChanges(session.id!);
|
|
136
|
+
displayFileChanges(files);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (options.commits) {
|
|
140
|
+
const commits = getCommits(session.id!);
|
|
141
|
+
displayCommits(commits);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// List command
|
|
146
|
+
program
|
|
147
|
+
.command('list')
|
|
148
|
+
.alias('ls')
|
|
149
|
+
.description('List recent sessions')
|
|
150
|
+
.option('-l, --limit <number>', 'Number of sessions to show', parseInt, 10)
|
|
151
|
+
.action((options) => {
|
|
152
|
+
const sessions = getSessions(options.limit);
|
|
153
|
+
displaySessions(sessions);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Stats command
|
|
157
|
+
program
|
|
158
|
+
.command('stats')
|
|
159
|
+
.description('Show overall statistics')
|
|
160
|
+
.action(() => {
|
|
161
|
+
const stats = getStats();
|
|
162
|
+
displayStats(stats);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Log AI usage command
|
|
166
|
+
program
|
|
167
|
+
.command('log-ai')
|
|
168
|
+
.description('Log AI usage for active session')
|
|
169
|
+
.requiredOption('-p, --provider <provider>', 'AI provider')
|
|
170
|
+
.requiredOption('-m, --model <model>', 'Model name')
|
|
171
|
+
.requiredOption('-t, --tokens <tokens>', 'Total tokens', parseInt)
|
|
172
|
+
.requiredOption('-c, --cost <cost>', 'Cost in dollars', parseFloat)
|
|
173
|
+
.action((options) => {
|
|
174
|
+
const session = getActiveSession();
|
|
175
|
+
if (!session) {
|
|
176
|
+
console.log(chalk.yellow('\nNo active session. Start one with: ds start <name>\n'));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
addAIUsage({
|
|
181
|
+
sessionId: session.id!,
|
|
182
|
+
provider: options.provider,
|
|
183
|
+
model: options.model,
|
|
184
|
+
tokens: options.tokens,
|
|
185
|
+
cost: options.cost,
|
|
186
|
+
timestamp: new Date().toISOString(),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
console.log(chalk.green(`\n✓ Logged AI usage: ${options.tokens.toLocaleString()} tokens, $${options.cost.toFixed(2)}\n`));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Status command
|
|
193
|
+
program
|
|
194
|
+
.command('status')
|
|
195
|
+
.description('Show active session status')
|
|
196
|
+
.action(() => {
|
|
197
|
+
const session = getActiveSession();
|
|
198
|
+
if (!session) {
|
|
199
|
+
console.log(chalk.yellow('\nNo active session.\n'));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
displaySession(session);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
program.parse();
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface Session {
|
|
2
|
+
id?: number;
|
|
3
|
+
name: string;
|
|
4
|
+
startTime: string;
|
|
5
|
+
endTime?: string;
|
|
6
|
+
duration?: number; // in seconds
|
|
7
|
+
workingDirectory: string;
|
|
8
|
+
filesChanged: number;
|
|
9
|
+
commits: number;
|
|
10
|
+
aiCost: number;
|
|
11
|
+
aiTokens: number;
|
|
12
|
+
notes?: string;
|
|
13
|
+
status: 'active' | 'completed';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FileChange {
|
|
17
|
+
id?: number;
|
|
18
|
+
sessionId: number;
|
|
19
|
+
filePath: string;
|
|
20
|
+
changeType: 'created' | 'modified' | 'deleted';
|
|
21
|
+
timestamp: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Commit {
|
|
25
|
+
id?: number;
|
|
26
|
+
sessionId: number;
|
|
27
|
+
hash: string;
|
|
28
|
+
message: string;
|
|
29
|
+
timestamp: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AIUsage {
|
|
33
|
+
id?: number;
|
|
34
|
+
sessionId: number;
|
|
35
|
+
provider: string;
|
|
36
|
+
model: string;
|
|
37
|
+
tokens: number;
|
|
38
|
+
cost: number;
|
|
39
|
+
timestamp: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SessionStats {
|
|
43
|
+
totalSessions: number;
|
|
44
|
+
totalTime: number;
|
|
45
|
+
totalFiles: number;
|
|
46
|
+
totalCommits: number;
|
|
47
|
+
totalAICost: number;
|
|
48
|
+
avgSessionTime: number;
|
|
49
|
+
}
|
package/src/watcher.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import { relative } from 'path';
|
|
3
|
+
import { addFileChange } from './db';
|
|
4
|
+
|
|
5
|
+
let watcher: chokidar.FSWatcher | null = null;
|
|
6
|
+
const changedFiles = new Set<string>();
|
|
7
|
+
|
|
8
|
+
export function startWatcher(sessionId: number, cwd: string): void {
|
|
9
|
+
if (watcher) return;
|
|
10
|
+
|
|
11
|
+
watcher = chokidar.watch(cwd, {
|
|
12
|
+
ignored: /(^|[\/\\])\..|(node_modules|dist|build|\.git)/,
|
|
13
|
+
persistent: true,
|
|
14
|
+
ignoreInitial: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
watcher
|
|
18
|
+
.on('add', (path) => handleChange(sessionId, path, cwd, 'created'))
|
|
19
|
+
.on('change', (path) => handleChange(sessionId, path, cwd, 'modified'))
|
|
20
|
+
.on('unlink', (path) => handleChange(sessionId, path, cwd, 'deleted'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function stopWatcher(): void {
|
|
24
|
+
if (watcher) {
|
|
25
|
+
watcher.close();
|
|
26
|
+
watcher = null;
|
|
27
|
+
changedFiles.clear();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handleChange(
|
|
32
|
+
sessionId: number,
|
|
33
|
+
path: string,
|
|
34
|
+
cwd: string,
|
|
35
|
+
changeType: 'created' | 'modified' | 'deleted'
|
|
36
|
+
): void {
|
|
37
|
+
const relativePath = relative(cwd, path);
|
|
38
|
+
|
|
39
|
+
// Deduplicate rapid changes to same file
|
|
40
|
+
const key = `${relativePath}-${changeType}`;
|
|
41
|
+
if (changedFiles.has(key)) return;
|
|
42
|
+
|
|
43
|
+
changedFiles.add(key);
|
|
44
|
+
setTimeout(() => changedFiles.delete(key), 1000);
|
|
45
|
+
|
|
46
|
+
addFileChange({
|
|
47
|
+
sessionId,
|
|
48
|
+
filePath: relativePath,
|
|
49
|
+
changeType,
|
|
50
|
+
timestamp: new Date().toISOString(),
|
|
51
|
+
});
|
|
52
|
+
}
|
package/test.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"moduleResolution": "node"
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|