@cmdctrl/cursor-cli 0.1.1 → 0.2.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/dist/adapter/cursor-cli.d.ts +23 -19
- package/dist/adapter/cursor-cli.d.ts.map +1 -1
- package/dist/adapter/cursor-cli.js +156 -126
- package/dist/adapter/cursor-cli.js.map +1 -1
- package/dist/adapter/events.d.ts +36 -20
- package/dist/adapter/events.d.ts.map +1 -1
- package/dist/adapter/events.js +40 -35
- package/dist/adapter/events.js.map +1 -1
- package/dist/commands/register.d.ts +0 -3
- package/dist/commands/register.d.ts.map +1 -1
- package/dist/commands/register.js +23 -122
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/start.d.ts +1 -8
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +117 -30
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.d.ts +1 -4
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +25 -22
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/stop.d.ts +1 -4
- package/dist/commands/stop.d.ts.map +1 -1
- package/dist/commands/stop.js +21 -26
- package/dist/commands/stop.js.map +1 -1
- package/dist/commands/unregister.d.ts +2 -0
- package/dist/commands/unregister.d.ts.map +1 -0
- package/dist/commands/unregister.js +43 -0
- package/dist/commands/unregister.js.map +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +0 -3
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/message-store.d.ts +18 -0
- package/dist/message-store.d.ts.map +1 -0
- package/dist/message-store.js +49 -0
- package/dist/message-store.js.map +1 -0
- package/package.json +2 -2
- package/src/adapter/cursor-cli.ts +165 -147
- package/src/adapter/events.ts +65 -51
- package/src/commands/register.ts +28 -170
- package/src/commands/start.ts +132 -41
- package/src/commands/status.ts +23 -28
- package/src/commands/stop.ts +21 -32
- package/src/commands/unregister.ts +43 -0
- package/src/commands/update.ts +0 -3
- package/src/index.ts +9 -4
- package/src/message-store.ts +61 -0
- package/src/client/messages.ts +0 -75
- package/src/client/websocket.ts +0 -308
- package/src/config/config.ts +0 -146
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory message store for Cursor CLI sessions.
|
|
3
|
+
*
|
|
4
|
+
* Cursor CLI manages its own sessions internally, but for CmdCtrl's
|
|
5
|
+
* get_messages protocol we need to track messages ourselves.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from 'crypto';
|
|
9
|
+
import type { MessageEntry } from '@cmdctrl/daemon-sdk';
|
|
10
|
+
|
|
11
|
+
export class MessageStore {
|
|
12
|
+
private sessions: Map<string, MessageEntry[]> = new Map();
|
|
13
|
+
|
|
14
|
+
storeMessage(sessionId: string, role: 'USER' | 'AGENT' | 'SYSTEM', content: string): string {
|
|
15
|
+
const uuid = randomUUID();
|
|
16
|
+
if (!this.sessions.has(sessionId)) {
|
|
17
|
+
this.sessions.set(sessionId, []);
|
|
18
|
+
}
|
|
19
|
+
this.sessions.get(sessionId)!.push({
|
|
20
|
+
uuid,
|
|
21
|
+
role,
|
|
22
|
+
content,
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
});
|
|
25
|
+
return uuid;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getMessages(
|
|
29
|
+
sessionId: string,
|
|
30
|
+
limit: number,
|
|
31
|
+
beforeUuid?: string,
|
|
32
|
+
afterUuid?: string
|
|
33
|
+
): {
|
|
34
|
+
messages: MessageEntry[];
|
|
35
|
+
hasMore: boolean;
|
|
36
|
+
oldestUuid?: string;
|
|
37
|
+
newestUuid?: string;
|
|
38
|
+
} {
|
|
39
|
+
let messages = this.sessions.get(sessionId) || [];
|
|
40
|
+
|
|
41
|
+
if (beforeUuid) {
|
|
42
|
+
const idx = messages.findIndex(m => m.uuid === beforeUuid);
|
|
43
|
+
if (idx > 0) messages = messages.slice(0, idx);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (afterUuid) {
|
|
47
|
+
const idx = messages.findIndex(m => m.uuid === afterUuid);
|
|
48
|
+
if (idx >= 0) messages = messages.slice(idx + 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const hasMore = messages.length > limit;
|
|
52
|
+
const limited = messages.slice(-limit);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
messages: limited,
|
|
56
|
+
hasMore,
|
|
57
|
+
oldestUuid: limited[0]?.uuid,
|
|
58
|
+
newestUuid: limited[limited.length - 1]?.uuid,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/client/messages.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Message types for daemon <-> server communication
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
// Server -> Daemon messages
|
|
6
|
-
|
|
7
|
-
export interface PingMessage {
|
|
8
|
-
type: 'ping';
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface TaskStartMessage {
|
|
12
|
-
type: 'task_start';
|
|
13
|
-
task_id: string;
|
|
14
|
-
instruction: string;
|
|
15
|
-
project_path?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface TaskResumeMessage {
|
|
19
|
-
type: 'task_resume';
|
|
20
|
-
task_id: string;
|
|
21
|
-
session_id: string;
|
|
22
|
-
message: string;
|
|
23
|
-
project_path?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface TaskCancelMessage {
|
|
27
|
-
type: 'task_cancel';
|
|
28
|
-
task_id: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface VersionStatusMessage {
|
|
32
|
-
type: 'version_status';
|
|
33
|
-
status: 'current' | 'update_available' | 'update_required';
|
|
34
|
-
your_version: string;
|
|
35
|
-
min_version?: string;
|
|
36
|
-
recommended_version?: string;
|
|
37
|
-
latest_version?: string;
|
|
38
|
-
changelog_url?: string;
|
|
39
|
-
message?: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export type ServerMessage =
|
|
43
|
-
| PingMessage
|
|
44
|
-
| TaskStartMessage
|
|
45
|
-
| TaskResumeMessage
|
|
46
|
-
| TaskCancelMessage
|
|
47
|
-
| VersionStatusMessage;
|
|
48
|
-
|
|
49
|
-
// Daemon -> Server messages
|
|
50
|
-
|
|
51
|
-
export interface PongMessage {
|
|
52
|
-
type: 'pong';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface StatusMessage {
|
|
56
|
-
type: 'status';
|
|
57
|
-
running_tasks: string[];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface EventMessage {
|
|
61
|
-
type: 'event';
|
|
62
|
-
task_id: string;
|
|
63
|
-
event_type: string;
|
|
64
|
-
[key: string]: unknown;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export type DaemonMessage = PongMessage | StatusMessage | EventMessage;
|
|
68
|
-
|
|
69
|
-
// Event types sent from daemon to server
|
|
70
|
-
export type EventType =
|
|
71
|
-
| 'WAIT_FOR_USER'
|
|
72
|
-
| 'TASK_COMPLETE'
|
|
73
|
-
| 'OUTPUT'
|
|
74
|
-
| 'PROGRESS'
|
|
75
|
-
| 'ERROR';
|
package/src/client/websocket.ts
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
import WebSocket from 'ws';
|
|
2
|
-
import { URL } from 'url';
|
|
3
|
-
import { CmdCtrlConfig, Credentials } from '../config/config';
|
|
4
|
-
import { CursorAdapter } from '../adapter/cursor-cli';
|
|
5
|
-
import {
|
|
6
|
-
ServerMessage,
|
|
7
|
-
DaemonMessage,
|
|
8
|
-
TaskStartMessage,
|
|
9
|
-
TaskResumeMessage,
|
|
10
|
-
TaskCancelMessage,
|
|
11
|
-
VersionStatusMessage,
|
|
12
|
-
} from './messages';
|
|
13
|
-
import { readFileSync } from 'fs';
|
|
14
|
-
import { join } from 'path';
|
|
15
|
-
|
|
16
|
-
const MAX_RECONNECT_DELAY = 30000; // 30 seconds
|
|
17
|
-
const INITIAL_RECONNECT_DELAY = 1000; // 1 second
|
|
18
|
-
const PING_INTERVAL = 30000; // 30 seconds
|
|
19
|
-
|
|
20
|
-
export class DaemonClient {
|
|
21
|
-
private ws: WebSocket | null = null;
|
|
22
|
-
private config: CmdCtrlConfig;
|
|
23
|
-
private credentials: Credentials;
|
|
24
|
-
private reconnectDelay = INITIAL_RECONNECT_DELAY;
|
|
25
|
-
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
26
|
-
private pingTimer: NodeJS.Timeout | null = null;
|
|
27
|
-
private shouldReconnect = true;
|
|
28
|
-
private adapter: CursorAdapter;
|
|
29
|
-
|
|
30
|
-
constructor(config: CmdCtrlConfig, credentials: Credentials) {
|
|
31
|
-
this.config = config;
|
|
32
|
-
this.credentials = credentials;
|
|
33
|
-
this.adapter = new CursorAdapter(this.sendEvent.bind(this));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Connect to the CmdCtrl server via WebSocket
|
|
38
|
-
*/
|
|
39
|
-
async connect(): Promise<void> {
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
const serverUrl = new URL(this.config.serverUrl);
|
|
42
|
-
const wsProtocol = serverUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
43
|
-
const wsUrl = `${wsProtocol}//${serverUrl.host}/ws/daemon`;
|
|
44
|
-
|
|
45
|
-
console.log(`Connecting to ${wsUrl}...`);
|
|
46
|
-
|
|
47
|
-
// Read version from package.json
|
|
48
|
-
let daemonVersion = 'unknown';
|
|
49
|
-
try {
|
|
50
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
51
|
-
daemonVersion = pkg.version;
|
|
52
|
-
} catch {
|
|
53
|
-
try {
|
|
54
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
55
|
-
daemonVersion = pkg.version;
|
|
56
|
-
} catch { /* use default */ }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
this.ws = new WebSocket(wsUrl, {
|
|
60
|
-
headers: {
|
|
61
|
-
Authorization: `Bearer ${this.credentials.accessToken}`,
|
|
62
|
-
'X-Device-ID': this.config.deviceId,
|
|
63
|
-
'X-Agent-Type': 'cursor_cli',
|
|
64
|
-
'X-Daemon-Version': daemonVersion,
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
this.ws.on('open', () => {
|
|
69
|
-
console.log('WebSocket connected');
|
|
70
|
-
this.reconnectDelay = INITIAL_RECONNECT_DELAY;
|
|
71
|
-
this.startPingInterval();
|
|
72
|
-
this.sendStatus();
|
|
73
|
-
resolve();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
this.ws.on('message', (data) => {
|
|
77
|
-
this.handleMessage(data.toString());
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
this.ws.on('close', (code, reason) => {
|
|
81
|
-
console.log(`WebSocket closed: ${code} ${reason}`);
|
|
82
|
-
this.stopPingInterval();
|
|
83
|
-
this.scheduleReconnect();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
this.ws.on('error', (err) => {
|
|
87
|
-
console.error('WebSocket error:', err.message);
|
|
88
|
-
if (this.ws?.readyState === WebSocket.CONNECTING) {
|
|
89
|
-
reject(err);
|
|
90
|
-
}
|
|
91
|
-
// For established connections, the 'close' event usually follows 'error',
|
|
92
|
-
// but if the connection was killed abruptly, we may not get a clean close.
|
|
93
|
-
// Force close the socket to ensure 'close' event fires and triggers reconnect.
|
|
94
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
95
|
-
this.ws.terminate();
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Disconnect from server
|
|
103
|
-
*/
|
|
104
|
-
async disconnect(): Promise<void> {
|
|
105
|
-
this.shouldReconnect = false;
|
|
106
|
-
|
|
107
|
-
if (this.reconnectTimer) {
|
|
108
|
-
clearTimeout(this.reconnectTimer);
|
|
109
|
-
this.reconnectTimer = null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
this.stopPingInterval();
|
|
113
|
-
|
|
114
|
-
// Stop all running tasks
|
|
115
|
-
await this.adapter.stopAll();
|
|
116
|
-
|
|
117
|
-
if (this.ws) {
|
|
118
|
-
this.ws.close(1000, 'Daemon shutting down');
|
|
119
|
-
this.ws = null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Send a message to the server
|
|
125
|
-
*/
|
|
126
|
-
private send(message: DaemonMessage): void {
|
|
127
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
128
|
-
this.ws.send(JSON.stringify(message));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Send an event for a task
|
|
134
|
-
*/
|
|
135
|
-
private sendEvent(
|
|
136
|
-
taskId: string,
|
|
137
|
-
eventType: string,
|
|
138
|
-
data: Record<string, unknown>
|
|
139
|
-
): void {
|
|
140
|
-
this.send({
|
|
141
|
-
type: 'event',
|
|
142
|
-
task_id: taskId,
|
|
143
|
-
event_type: eventType,
|
|
144
|
-
...data
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Send current status to server
|
|
150
|
-
*/
|
|
151
|
-
private sendStatus(): void {
|
|
152
|
-
this.send({
|
|
153
|
-
type: 'status',
|
|
154
|
-
running_tasks: this.adapter.getRunningTasks()
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Handle incoming message from server
|
|
160
|
-
*/
|
|
161
|
-
private handleMessage(raw: string): void {
|
|
162
|
-
let msg: ServerMessage;
|
|
163
|
-
try {
|
|
164
|
-
msg = JSON.parse(raw) as ServerMessage;
|
|
165
|
-
} catch {
|
|
166
|
-
console.error('Failed to parse message:', raw);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
switch (msg.type) {
|
|
171
|
-
case 'ping':
|
|
172
|
-
this.send({ type: 'pong' });
|
|
173
|
-
break;
|
|
174
|
-
|
|
175
|
-
case 'task_start':
|
|
176
|
-
this.handleTaskStart(msg as TaskStartMessage);
|
|
177
|
-
break;
|
|
178
|
-
|
|
179
|
-
case 'task_resume':
|
|
180
|
-
this.handleTaskResume(msg as TaskResumeMessage);
|
|
181
|
-
break;
|
|
182
|
-
|
|
183
|
-
case 'task_cancel':
|
|
184
|
-
this.handleTaskCancel(msg as TaskCancelMessage);
|
|
185
|
-
break;
|
|
186
|
-
|
|
187
|
-
case 'version_status':
|
|
188
|
-
this.handleVersionStatus(msg as VersionStatusMessage);
|
|
189
|
-
break;
|
|
190
|
-
|
|
191
|
-
default:
|
|
192
|
-
console.warn('Unknown message type:', (msg as { type: string }).type);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Handle task_start message
|
|
198
|
-
*/
|
|
199
|
-
private async handleTaskStart(msg: TaskStartMessage): Promise<void> {
|
|
200
|
-
console.log(`Starting task ${msg.task_id}: ${msg.instruction.substring(0, 50)}...`);
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
await this.adapter.startTask(msg.task_id, msg.instruction, msg.project_path);
|
|
204
|
-
} catch (err) {
|
|
205
|
-
console.error(`Failed to start task ${msg.task_id}:`, err);
|
|
206
|
-
this.sendEvent(msg.task_id, 'ERROR', {
|
|
207
|
-
error: (err as Error).message
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Handle task_resume message
|
|
214
|
-
*/
|
|
215
|
-
private async handleTaskResume(msg: TaskResumeMessage): Promise<void> {
|
|
216
|
-
console.log(`Resuming task ${msg.task_id} with session ${msg.session_id}`);
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
await this.adapter.resumeTask(msg.task_id, msg.session_id, msg.message, msg.project_path);
|
|
220
|
-
} catch (err) {
|
|
221
|
-
console.error(`Failed to resume task ${msg.task_id}:`, err);
|
|
222
|
-
this.sendEvent(msg.task_id, 'ERROR', {
|
|
223
|
-
error: (err as Error).message
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Handle task_cancel message
|
|
230
|
-
*/
|
|
231
|
-
private async handleTaskCancel(msg: TaskCancelMessage): Promise<void> {
|
|
232
|
-
console.log(`Cancelling task ${msg.task_id}`);
|
|
233
|
-
await this.adapter.cancelTask(msg.task_id);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Handle version_status message from server
|
|
238
|
-
*/
|
|
239
|
-
private handleVersionStatus(msg: VersionStatusMessage): void {
|
|
240
|
-
if (msg.status === 'update_required') {
|
|
241
|
-
console.error(`\n✖ Daemon version ${msg.your_version} is no longer supported (minimum: ${msg.min_version})`);
|
|
242
|
-
console.error(` Run: cmdctrl-cursor-cli update`);
|
|
243
|
-
if (msg.changelog_url) console.error(` Changelog: ${msg.changelog_url}`);
|
|
244
|
-
if (msg.message) console.error(` ${msg.message}`);
|
|
245
|
-
console.error('');
|
|
246
|
-
this.shouldReconnect = false;
|
|
247
|
-
process.exit(1);
|
|
248
|
-
} else if (msg.status === 'update_available') {
|
|
249
|
-
console.warn(`\n⚠ Update available: v${msg.latest_version} (you have v${msg.your_version})`);
|
|
250
|
-
console.warn(` Run: cmdctrl-cursor-cli update`);
|
|
251
|
-
if (msg.changelog_url) console.warn(` Changelog: ${msg.changelog_url}`);
|
|
252
|
-
console.warn('');
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Schedule reconnection with exponential backoff
|
|
258
|
-
*/
|
|
259
|
-
private scheduleReconnect(): void {
|
|
260
|
-
if (!this.shouldReconnect) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
console.log(`Reconnecting in ${this.reconnectDelay / 1000}s...`);
|
|
265
|
-
|
|
266
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
267
|
-
try {
|
|
268
|
-
await this.connect();
|
|
269
|
-
} catch {
|
|
270
|
-
// Increase delay with exponential backoff
|
|
271
|
-
this.reconnectDelay = Math.min(
|
|
272
|
-
this.reconnectDelay * 2,
|
|
273
|
-
MAX_RECONNECT_DELAY
|
|
274
|
-
);
|
|
275
|
-
this.scheduleReconnect();
|
|
276
|
-
}
|
|
277
|
-
}, this.reconnectDelay);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Start ping interval to keep connection alive
|
|
282
|
-
*/
|
|
283
|
-
private startPingInterval(): void {
|
|
284
|
-
this.pingTimer = setInterval(() => {
|
|
285
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
286
|
-
this.ws.ping();
|
|
287
|
-
}
|
|
288
|
-
}, PING_INTERVAL);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Stop ping interval
|
|
293
|
-
*/
|
|
294
|
-
private stopPingInterval(): void {
|
|
295
|
-
if (this.pingTimer) {
|
|
296
|
-
clearInterval(this.pingTimer);
|
|
297
|
-
this.pingTimer = null;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Refresh access token using refresh token
|
|
303
|
-
*/
|
|
304
|
-
async refreshToken(): Promise<boolean> {
|
|
305
|
-
console.log('Token refresh not yet implemented');
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
}
|
package/src/config/config.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as os from 'os';
|
|
4
|
-
|
|
5
|
-
export interface CmdCtrlConfig {
|
|
6
|
-
serverUrl: string;
|
|
7
|
-
deviceId: string;
|
|
8
|
-
deviceName: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface Credentials {
|
|
12
|
-
accessToken: string;
|
|
13
|
-
refreshToken: string;
|
|
14
|
-
expiresAt: number; // Unix timestamp
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const CONFIG_DIR = path.join(os.homedir(), '.cmdctrl-cursor-cli');
|
|
18
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
19
|
-
const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials');
|
|
20
|
-
const PID_FILE = path.join(CONFIG_DIR, 'daemon.pid');
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Ensure the config directory exists with proper permissions
|
|
24
|
-
*/
|
|
25
|
-
export function ensureConfigDir(): void {
|
|
26
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
27
|
-
fs.mkdirSync(CONFIG_DIR, { mode: 0o700 });
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Read the config file
|
|
33
|
-
*/
|
|
34
|
-
export function readConfig(): CmdCtrlConfig | null {
|
|
35
|
-
try {
|
|
36
|
-
if (!fs.existsSync(CONFIG_FILE)) {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
40
|
-
return JSON.parse(content) as CmdCtrlConfig;
|
|
41
|
-
} catch {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Write the config file
|
|
48
|
-
*/
|
|
49
|
-
export function writeConfig(config: CmdCtrlConfig): void {
|
|
50
|
-
ensureConfigDir();
|
|
51
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Read credentials (access/refresh tokens)
|
|
56
|
-
*/
|
|
57
|
-
export function readCredentials(): Credentials | null {
|
|
58
|
-
try {
|
|
59
|
-
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
const content = fs.readFileSync(CREDENTIALS_FILE, 'utf-8');
|
|
63
|
-
return JSON.parse(content) as Credentials;
|
|
64
|
-
} catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Write credentials with restrictive permissions (600)
|
|
71
|
-
*/
|
|
72
|
-
export function writeCredentials(creds: Credentials): void {
|
|
73
|
-
ensureConfigDir();
|
|
74
|
-
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Delete credentials (for logout/revoke)
|
|
79
|
-
*/
|
|
80
|
-
export function deleteCredentials(): void {
|
|
81
|
-
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
82
|
-
fs.unlinkSync(CREDENTIALS_FILE);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Check if device is registered
|
|
88
|
-
*/
|
|
89
|
-
export function isRegistered(): boolean {
|
|
90
|
-
const config = readConfig();
|
|
91
|
-
const creds = readCredentials();
|
|
92
|
-
return config !== null && creds !== null && config.deviceId !== '';
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Write daemon PID file
|
|
97
|
-
*/
|
|
98
|
-
export function writePidFile(pid: number): void {
|
|
99
|
-
ensureConfigDir();
|
|
100
|
-
fs.writeFileSync(PID_FILE, pid.toString(), { mode: 0o600 });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Read daemon PID
|
|
105
|
-
*/
|
|
106
|
-
export function readPidFile(): number | null {
|
|
107
|
-
try {
|
|
108
|
-
if (!fs.existsSync(PID_FILE)) {
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
const content = fs.readFileSync(PID_FILE, 'utf-8');
|
|
112
|
-
return parseInt(content, 10);
|
|
113
|
-
} catch {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Delete PID file
|
|
120
|
-
*/
|
|
121
|
-
export function deletePidFile(): void {
|
|
122
|
-
if (fs.existsSync(PID_FILE)) {
|
|
123
|
-
fs.unlinkSync(PID_FILE);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Check if daemon is running
|
|
129
|
-
*/
|
|
130
|
-
export function isDaemonRunning(): boolean {
|
|
131
|
-
const pid = readPidFile();
|
|
132
|
-
if (pid === null) {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
// Signal 0 doesn't kill, just checks if process exists
|
|
137
|
-
process.kill(pid, 0);
|
|
138
|
-
return true;
|
|
139
|
-
} catch {
|
|
140
|
-
// Process doesn't exist, clean up stale PID file
|
|
141
|
-
deletePidFile();
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export { CONFIG_DIR, CONFIG_FILE, CREDENTIALS_FILE, PID_FILE };
|