@cmdctrl/cursor-cli 0.1.1 → 0.2.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.
- 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 +21 -2
- 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 +24 -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
package/src/commands/register.ts
CHANGED
|
@@ -1,199 +1,57 @@
|
|
|
1
1
|
import * as os from 'os';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
writeConfig,
|
|
7
|
-
writeCredentials,
|
|
8
|
-
readConfig,
|
|
9
|
-
isRegistered,
|
|
10
|
-
CmdCtrlConfig,
|
|
11
|
-
Credentials
|
|
12
|
-
} from '../config/config';
|
|
2
|
+
import { ConfigManager, registerDevice } from '@cmdctrl/daemon-sdk';
|
|
3
|
+
|
|
4
|
+
const configManager = new ConfigManager('cursor-cli');
|
|
13
5
|
|
|
14
6
|
interface RegisterOptions {
|
|
15
7
|
server: string;
|
|
16
8
|
name?: string;
|
|
17
9
|
}
|
|
18
10
|
|
|
19
|
-
interface DeviceCodeResponse {
|
|
20
|
-
deviceCode: string;
|
|
21
|
-
userCode: string;
|
|
22
|
-
verificationUrl: string;
|
|
23
|
-
expiresIn: number;
|
|
24
|
-
interval: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface TokenResponse {
|
|
28
|
-
accessToken: string;
|
|
29
|
-
refreshToken: string;
|
|
30
|
-
expiresIn: number;
|
|
31
|
-
deviceId: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Make an HTTP(S) request
|
|
36
|
-
*/
|
|
37
|
-
function request(
|
|
38
|
-
url: string,
|
|
39
|
-
method: string,
|
|
40
|
-
body?: object
|
|
41
|
-
): Promise<{ status: number; data: unknown }> {
|
|
42
|
-
return new Promise((resolve, reject) => {
|
|
43
|
-
const parsed = new URL(url);
|
|
44
|
-
const client = parsed.protocol === 'https:' ? https : http;
|
|
45
|
-
|
|
46
|
-
const options = {
|
|
47
|
-
hostname: parsed.hostname,
|
|
48
|
-
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
49
|
-
path: parsed.pathname + parsed.search,
|
|
50
|
-
method,
|
|
51
|
-
headers: {
|
|
52
|
-
'Content-Type': 'application/json',
|
|
53
|
-
Accept: 'application/json'
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const req = client.request(options, (res) => {
|
|
58
|
-
let data = '';
|
|
59
|
-
res.on('data', (chunk) => (data += chunk));
|
|
60
|
-
res.on('end', () => {
|
|
61
|
-
try {
|
|
62
|
-
const parsed = data ? JSON.parse(data) : {};
|
|
63
|
-
resolve({ status: res.statusCode || 0, data: parsed });
|
|
64
|
-
} catch {
|
|
65
|
-
resolve({ status: res.statusCode || 0, data: {} });
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
req.on('error', reject);
|
|
71
|
-
|
|
72
|
-
if (body) {
|
|
73
|
-
req.write(JSON.stringify(body));
|
|
74
|
-
}
|
|
75
|
-
req.end();
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Poll for token completion (after user verifies in browser)
|
|
81
|
-
*/
|
|
82
|
-
async function pollForToken(
|
|
83
|
-
serverUrl: string,
|
|
84
|
-
deviceCode: string,
|
|
85
|
-
interval: number,
|
|
86
|
-
expiresIn: number
|
|
87
|
-
): Promise<TokenResponse | null> {
|
|
88
|
-
const startTime = Date.now();
|
|
89
|
-
const expiresAt = startTime + expiresIn * 1000;
|
|
90
|
-
|
|
91
|
-
while (Date.now() < expiresAt) {
|
|
92
|
-
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const response = await request(`${serverUrl}/api/devices/token`, 'POST', {
|
|
96
|
-
deviceCode
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
if (response.status === 200) {
|
|
100
|
-
return response.data as TokenResponse;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 400 with "authorization_pending" means keep polling
|
|
104
|
-
const data = response.data as { error?: string };
|
|
105
|
-
if (response.status === 400 && data.error === 'authorization_pending') {
|
|
106
|
-
process.stdout.write('.');
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Other errors should stop polling
|
|
111
|
-
if (response.status >= 400) {
|
|
112
|
-
console.error('\nError polling for token:', data);
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
} catch (err) {
|
|
116
|
-
console.error('\nError polling for token:', err);
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
console.error('\nDevice code expired. Please try again.');
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Register command - implements GitHub CLI style device auth flow
|
|
127
|
-
*/
|
|
128
11
|
export async function register(options: RegisterOptions): Promise<void> {
|
|
129
|
-
const serverUrl = options.server.replace(/\/$/, '');
|
|
12
|
+
const serverUrl = options.server.replace(/\/$/, '');
|
|
130
13
|
const deviceName = options.name || `${os.hostname()}-cursor`;
|
|
131
14
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const config = readConfig();
|
|
15
|
+
if (configManager.isRegistered()) {
|
|
16
|
+
const config = configManager.readConfig();
|
|
135
17
|
console.log(`Already registered as "${config?.deviceName}" (${config?.deviceId})`);
|
|
136
18
|
console.log(`Server: ${config?.serverUrl}`);
|
|
137
|
-
console.log(`\nTo re-register, run:
|
|
19
|
+
console.log(`\nTo re-register, run: cmdctrl-cursor-cli unregister`);
|
|
138
20
|
return;
|
|
139
21
|
}
|
|
140
22
|
|
|
141
23
|
console.log(`Registering Cursor CLI device "${deviceName}" with ${serverUrl}...\n`);
|
|
142
24
|
|
|
143
|
-
|
|
144
|
-
let codeResponse: DeviceCodeResponse;
|
|
145
|
-
try {
|
|
146
|
-
const response = await request(`${serverUrl}/api/devices/code`, 'POST', {
|
|
147
|
-
deviceName,
|
|
148
|
-
hostname: os.hostname(),
|
|
149
|
-
agentType: 'cursor_cli'
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
if (response.status !== 200) {
|
|
153
|
-
console.error('Failed to get device code:', response.data);
|
|
154
|
-
process.exit(1);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
codeResponse = response.data as DeviceCodeResponse;
|
|
158
|
-
} catch (err) {
|
|
159
|
-
console.error('Failed to connect to server:', err);
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Step 2: Display instructions to user
|
|
164
|
-
console.log('To complete registration, open this URL in your browser:\n');
|
|
165
|
-
console.log(` ${codeResponse.verificationUrl}\n`);
|
|
166
|
-
console.log('Waiting for verification...');
|
|
167
|
-
|
|
168
|
-
// Step 3: Poll for completion
|
|
169
|
-
const tokenResponse = await pollForToken(
|
|
25
|
+
const result = await registerDevice(
|
|
170
26
|
serverUrl,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
27
|
+
deviceName,
|
|
28
|
+
os.hostname(),
|
|
29
|
+
'cursor_cli',
|
|
30
|
+
(url, _userCode) => {
|
|
31
|
+
console.log('To complete registration, open this URL in your browser:\n');
|
|
32
|
+
console.log(` ${url}\n`);
|
|
33
|
+
console.log('Waiting for verification...');
|
|
34
|
+
}
|
|
174
35
|
);
|
|
175
36
|
|
|
176
|
-
if (!
|
|
37
|
+
if (!result) {
|
|
38
|
+
console.error('\nDevice code expired. Please try again.');
|
|
177
39
|
process.exit(1);
|
|
178
40
|
}
|
|
179
41
|
|
|
180
|
-
|
|
181
|
-
const config: CmdCtrlConfig = {
|
|
42
|
+
configManager.writeConfig({
|
|
182
43
|
serverUrl,
|
|
183
|
-
deviceId:
|
|
184
|
-
deviceName
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const credentials: Credentials = {
|
|
188
|
-
accessToken: tokenResponse.accessToken,
|
|
189
|
-
refreshToken: tokenResponse.refreshToken,
|
|
190
|
-
expiresAt: Date.now() + tokenResponse.expiresIn * 1000
|
|
191
|
-
};
|
|
44
|
+
deviceId: result.deviceId,
|
|
45
|
+
deviceName,
|
|
46
|
+
});
|
|
192
47
|
|
|
193
|
-
|
|
194
|
-
|
|
48
|
+
configManager.writeCredentials({
|
|
49
|
+
refreshToken: result.refreshToken,
|
|
50
|
+
accessToken: result.accessToken,
|
|
51
|
+
expiresAt: result.expiresIn ? Date.now() + result.expiresIn * 1000 : undefined,
|
|
52
|
+
});
|
|
195
53
|
|
|
196
54
|
console.log('\n\nRegistration complete!');
|
|
197
|
-
console.log(`Device ID: ${
|
|
55
|
+
console.log(`Device ID: ${result.deviceId}`);
|
|
198
56
|
console.log(`\nRun 'cmdctrl-cursor-cli start' to connect to the server.`);
|
|
199
57
|
}
|
package/src/commands/start.ts
CHANGED
|
@@ -1,62 +1,153 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { DaemonClient } from '../client/websocket';
|
|
9
|
-
|
|
10
|
-
interface StartOptions {
|
|
11
|
-
foreground?: boolean;
|
|
12
|
-
}
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { DaemonClient, ConfigManager } from '@cmdctrl/daemon-sdk';
|
|
4
|
+
import { CursorAdapter } from '../adapter/cursor-cli';
|
|
5
|
+
import { MessageStore } from '../message-store';
|
|
6
|
+
|
|
7
|
+
const configManager = new ConfigManager('cursor-cli');
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export async function start(options: StartOptions): Promise<void> {
|
|
18
|
-
// Check if registered
|
|
19
|
-
if (!isRegistered()) {
|
|
20
|
-
console.error('Device not registered. Run "cmdctrl-cursor-cli-daemon register" first.');
|
|
9
|
+
export async function start(): Promise<void> {
|
|
10
|
+
if (!configManager.isRegistered()) {
|
|
11
|
+
console.error('Device not registered. Run "cmdctrl-cursor-cli register" first.');
|
|
21
12
|
process.exit(1);
|
|
22
13
|
}
|
|
23
14
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
console.error('Daemon is already running. Use "cmdctrl-cursor-cli-daemon stop" to stop it.');
|
|
15
|
+
if (configManager.isDaemonRunning()) {
|
|
16
|
+
console.error('Daemon is already running. Run "cmdctrl-cursor-cli stop" first.');
|
|
27
17
|
process.exit(1);
|
|
28
18
|
}
|
|
29
19
|
|
|
30
|
-
const config = readConfig()!;
|
|
31
|
-
const credentials = readCredentials()!;
|
|
20
|
+
const config = configManager.readConfig()!;
|
|
21
|
+
const credentials = configManager.readCredentials()!;
|
|
22
|
+
|
|
23
|
+
let daemonVersion = 'unknown';
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
26
|
+
daemonVersion = pkg.version;
|
|
27
|
+
} catch {
|
|
28
|
+
try {
|
|
29
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
30
|
+
daemonVersion = pkg.version;
|
|
31
|
+
} catch { /* use default */ }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log('Cursor CLI Daemon');
|
|
35
|
+
console.log(` Server: ${config.serverUrl}`);
|
|
36
|
+
console.log(` Device: ${config.deviceName} (${config.deviceId})`);
|
|
37
|
+
console.log(` Version: ${daemonVersion}`);
|
|
38
|
+
console.log('');
|
|
39
|
+
|
|
40
|
+
configManager.writePidFile(process.pid);
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
const messageStore = new MessageStore();
|
|
43
|
+
const pendingInstructions = new Map<string, string>();
|
|
44
|
+
const taskSessionMap = new Map<string, string>();
|
|
45
|
+
|
|
46
|
+
// Event callback wired into the DaemonClient below
|
|
47
|
+
let sendEvent: (taskId: string, eventType: string, data: Record<string, unknown>) => void;
|
|
48
|
+
|
|
49
|
+
const adapter = new CursorAdapter((taskId, eventType, data) => {
|
|
50
|
+
const sessionId = data.session_id as string | undefined;
|
|
51
|
+
|
|
52
|
+
// Store initial user message when session starts
|
|
53
|
+
if (eventType === 'SESSION_STARTED' && sessionId) {
|
|
54
|
+
const instruction = pendingInstructions.get(taskId);
|
|
55
|
+
if (instruction) {
|
|
56
|
+
messageStore.storeMessage(sessionId, 'USER', instruction);
|
|
57
|
+
pendingInstructions.delete(taskId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Store agent response on completion
|
|
62
|
+
if (eventType === 'TASK_COMPLETE' && data.result) {
|
|
63
|
+
const sid = (data.session_id as string) || taskSessionMap.get(taskId);
|
|
64
|
+
if (sid) {
|
|
65
|
+
messageStore.storeMessage(sid, 'AGENT', data.result as string);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
35
68
|
|
|
36
|
-
|
|
37
|
-
|
|
69
|
+
sendEvent(taskId, eventType, data);
|
|
70
|
+
});
|
|
38
71
|
|
|
39
|
-
|
|
40
|
-
|
|
72
|
+
const client = new DaemonClient({
|
|
73
|
+
serverUrl: config.serverUrl,
|
|
74
|
+
deviceId: config.deviceId,
|
|
75
|
+
agentType: 'cursor_cli',
|
|
76
|
+
token: credentials.refreshToken,
|
|
77
|
+
version: daemonVersion,
|
|
78
|
+
});
|
|
41
79
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
80
|
+
// Wire up sendEvent to the client's internal method
|
|
81
|
+
sendEvent = (taskId, eventType, data) => {
|
|
82
|
+
// Use the client to send events — the SDK handles this via task handles,
|
|
83
|
+
// but since the adapter uses a callback pattern, we send raw events
|
|
84
|
+
(client as any).send({
|
|
85
|
+
type: 'event',
|
|
86
|
+
task_id: taskId,
|
|
87
|
+
event_type: eventType,
|
|
88
|
+
...data,
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
client.onTaskStart(async (task) => {
|
|
93
|
+
pendingInstructions.set(task.taskId, task.instruction);
|
|
94
|
+
try {
|
|
95
|
+
await adapter.startTask(task.taskId, task.instruction, task.projectPath);
|
|
96
|
+
} catch (err: unknown) {
|
|
97
|
+
task.error(err instanceof Error ? err.message : 'Unknown error');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
client.onTaskResume(async (task) => {
|
|
102
|
+
messageStore.storeMessage(task.sessionId, 'USER', task.message);
|
|
103
|
+
taskSessionMap.set(task.taskId, task.sessionId);
|
|
104
|
+
try {
|
|
105
|
+
await adapter.resumeTask(task.taskId, task.sessionId, task.message, task.projectPath);
|
|
106
|
+
} catch (err: unknown) {
|
|
107
|
+
task.error(err instanceof Error ? err.message : 'Unknown error');
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
client.onTaskCancel(async (taskId) => {
|
|
112
|
+
await adapter.cancelTask(taskId);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
client.onGetMessages((req) => {
|
|
116
|
+
const result = messageStore.getMessages(
|
|
117
|
+
req.sessionId,
|
|
118
|
+
req.limit,
|
|
119
|
+
req.beforeUuid,
|
|
120
|
+
req.afterUuid
|
|
121
|
+
);
|
|
122
|
+
return {
|
|
123
|
+
messages: result.messages,
|
|
124
|
+
hasMore: result.hasMore,
|
|
125
|
+
oldestUuid: result.oldestUuid,
|
|
126
|
+
newestUuid: result.newestUuid,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
client.onVersionStatus((msg) => {
|
|
131
|
+
if (msg.status === 'update_available') {
|
|
132
|
+
console.warn(`Update available: v${msg.latest_version} (you have v${msg.your_version})`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const shutdown = async () => {
|
|
137
|
+
console.log('\nShutting down...');
|
|
138
|
+
await adapter.stopAll();
|
|
45
139
|
await client.disconnect();
|
|
140
|
+
configManager.deletePidFile();
|
|
46
141
|
process.exit(0);
|
|
47
142
|
};
|
|
48
143
|
|
|
49
|
-
process.on('SIGINT',
|
|
50
|
-
process.on('SIGTERM',
|
|
144
|
+
process.on('SIGINT', shutdown);
|
|
145
|
+
process.on('SIGTERM', shutdown);
|
|
51
146
|
|
|
52
147
|
try {
|
|
53
148
|
await client.connect();
|
|
54
|
-
console.log('Cursor CLI daemon
|
|
55
|
-
|
|
56
|
-
// In foreground mode, just keep running
|
|
57
|
-
if (options.foreground) {
|
|
58
|
-
console.log('Running in foreground. Press Ctrl+C to stop.');
|
|
59
|
-
}
|
|
149
|
+
console.log('Cursor CLI daemon running. Press Ctrl+C to stop.\n');
|
|
150
|
+
await new Promise(() => {});
|
|
60
151
|
} catch (err) {
|
|
61
152
|
console.error('Failed to connect:', err);
|
|
62
153
|
process.exit(1);
|
package/src/commands/status.ts
CHANGED
|
@@ -1,46 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
readConfig,
|
|
3
|
-
readCredentials,
|
|
4
|
-
isRegistered,
|
|
5
|
-
isDaemonRunning,
|
|
6
|
-
readPidFile
|
|
7
|
-
} from '../config/config';
|
|
1
|
+
import { ConfigManager } from '@cmdctrl/daemon-sdk';
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (!isRegistered()) {
|
|
3
|
+
const configManager = new ConfigManager('cursor-cli');
|
|
4
|
+
|
|
5
|
+
export function status(): void {
|
|
6
|
+
if (!configManager.isRegistered()) {
|
|
14
7
|
console.log('Status: Not registered');
|
|
15
|
-
console.log('\nRun "cmdctrl-cursor-cli
|
|
8
|
+
console.log('\nRun "cmdctrl-cursor-cli register" to register this device.');
|
|
16
9
|
return;
|
|
17
10
|
}
|
|
18
11
|
|
|
19
|
-
const config = readConfig()!;
|
|
20
|
-
const credentials = readCredentials();
|
|
21
|
-
const running = isDaemonRunning();
|
|
22
|
-
const pid = readPidFile();
|
|
12
|
+
const config = configManager.readConfig()!;
|
|
13
|
+
const credentials = configManager.readCredentials();
|
|
23
14
|
|
|
24
15
|
console.log('Cursor CLI Daemon Status');
|
|
25
16
|
console.log('========================');
|
|
26
17
|
console.log(`Device Name: ${config.deviceName}`);
|
|
27
18
|
console.log(`Device ID: ${config.deviceId}`);
|
|
28
19
|
console.log(`Server: ${config.serverUrl}`);
|
|
29
|
-
console.log();
|
|
30
|
-
console.log(`Daemon: ${running ? `Running (PID ${pid})` : 'Stopped'}`);
|
|
31
20
|
|
|
32
|
-
if (credentials) {
|
|
21
|
+
if (credentials?.expiresAt) {
|
|
33
22
|
const expiresIn = credentials.expiresAt - Date.now();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
console.log(
|
|
23
|
+
if (expiresIn > 0) {
|
|
24
|
+
const hours = Math.floor(expiresIn / 3600000);
|
|
25
|
+
const minutes = Math.floor((expiresIn % 3600000) / 60000);
|
|
26
|
+
console.log(`Token: Valid (expires in ${hours}h ${minutes}m)`);
|
|
27
|
+
} else {
|
|
28
|
+
console.log('Token: Expired');
|
|
40
29
|
}
|
|
30
|
+
} else {
|
|
31
|
+
console.log('Token: Present');
|
|
41
32
|
}
|
|
42
33
|
|
|
43
|
-
if (
|
|
44
|
-
|
|
34
|
+
if (configManager.isDaemonRunning()) {
|
|
35
|
+
const pid = configManager.readPidFile();
|
|
36
|
+
console.log(`Daemon: Running (PID ${pid})`);
|
|
37
|
+
} else {
|
|
38
|
+
console.log('Daemon: Not running');
|
|
39
|
+
console.log('\nRun "cmdctrl-cursor-cli start" to start the daemon.');
|
|
45
40
|
}
|
|
46
41
|
}
|
package/src/commands/stop.ts
CHANGED
|
@@ -1,47 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isDaemonRunning,
|
|
3
|
-
readPidFile,
|
|
4
|
-
deletePidFile
|
|
5
|
-
} from '../config/config';
|
|
1
|
+
import { ConfigManager } from '@cmdctrl/daemon-sdk';
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (!isDaemonRunning()) {
|
|
3
|
+
const configManager = new ConfigManager('cursor-cli');
|
|
4
|
+
|
|
5
|
+
export function stop(): void {
|
|
6
|
+
if (!configManager.isDaemonRunning()) {
|
|
12
7
|
console.log('Daemon is not running.');
|
|
13
8
|
return;
|
|
14
9
|
}
|
|
15
10
|
|
|
16
|
-
const pid = readPidFile();
|
|
17
|
-
if (
|
|
18
|
-
console.log('
|
|
11
|
+
const pid = configManager.readPidFile();
|
|
12
|
+
if (pid === null) {
|
|
13
|
+
console.log('No PID file found.');
|
|
19
14
|
return;
|
|
20
15
|
}
|
|
21
16
|
|
|
22
|
-
console.log(`Stopping Cursor CLI daemon (PID ${pid})...`);
|
|
23
|
-
|
|
24
17
|
try {
|
|
25
|
-
// Send SIGTERM to gracefully shutdown
|
|
26
18
|
process.kill(pid, 'SIGTERM');
|
|
19
|
+
console.log(`Sent SIGTERM to daemon (PID ${pid})`);
|
|
27
20
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
deletePidFile();
|
|
42
|
-
console.log('Daemon stopped.');
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
console.log('Daemon still running, sending SIGKILL...');
|
|
25
|
+
process.kill(pid, 'SIGKILL');
|
|
26
|
+
} catch {
|
|
27
|
+
// Process is gone
|
|
28
|
+
}
|
|
29
|
+
configManager.deletePidFile();
|
|
30
|
+
console.log('Daemon stopped.');
|
|
31
|
+
}, 2000);
|
|
43
32
|
} catch (err) {
|
|
44
33
|
console.error('Failed to stop daemon:', err);
|
|
45
|
-
deletePidFile();
|
|
34
|
+
configManager.deletePidFile();
|
|
46
35
|
}
|
|
47
36
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ConfigManager } from '@cmdctrl/daemon-sdk';
|
|
2
|
+
|
|
3
|
+
const configManager = new ConfigManager('cursor-cli');
|
|
4
|
+
|
|
5
|
+
export async function unregister(): Promise<void> {
|
|
6
|
+
const config = configManager.readConfig();
|
|
7
|
+
|
|
8
|
+
if (!config) {
|
|
9
|
+
console.log('Not registered.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (configManager.isDaemonRunning()) {
|
|
14
|
+
console.error('Error: Daemon is currently running.');
|
|
15
|
+
console.error('Please stop the daemon first with: cmdctrl-cursor-cli stop');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(`Unregistering device "${config.deviceName}" (${config.deviceId})...`);
|
|
20
|
+
|
|
21
|
+
const credentials = configManager.readCredentials();
|
|
22
|
+
if (credentials) {
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(`${config.serverUrl}/api/devices/${config.deviceId}`, {
|
|
25
|
+
method: 'DELETE',
|
|
26
|
+
headers: { 'Authorization': `Bearer ${credentials.refreshToken}` },
|
|
27
|
+
});
|
|
28
|
+
if (response.ok || response.status === 204) {
|
|
29
|
+
console.log('Device removed from server.');
|
|
30
|
+
} else if (response.status === 404) {
|
|
31
|
+
console.log('Device was already removed from server.');
|
|
32
|
+
} else {
|
|
33
|
+
console.warn(`Warning: Failed to remove device from server (HTTP ${response.status}).`);
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
console.warn('Warning: Could not reach server to remove device.');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
configManager.clearRegistration();
|
|
41
|
+
console.log('Local registration data cleared.');
|
|
42
|
+
console.log('You can now register again with: cmdctrl-cursor-cli register -s <server-url>');
|
|
43
|
+
}
|
package/src/commands/update.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
1
|
+
import { execSync, spawn } from 'child_process';
|
|
2
2
|
import { readFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
+
import { ConfigManager } from '@cmdctrl/daemon-sdk';
|
|
5
|
+
import { stop } from './stop';
|
|
6
|
+
|
|
7
|
+
const configManager = new ConfigManager('cursor-cli');
|
|
4
8
|
|
|
5
9
|
// Get the current version from package.json
|
|
6
10
|
function getCurrentVersion(): string {
|
|
@@ -32,6 +36,7 @@ async function getLatestVersion(packageName: string): Promise<string | null> {
|
|
|
32
36
|
export async function update(): Promise<void> {
|
|
33
37
|
const packageName = '@cmdctrl/cursor-cli';
|
|
34
38
|
const currentVersion = getCurrentVersion();
|
|
39
|
+
const wasRunning = configManager.isDaemonRunning();
|
|
35
40
|
|
|
36
41
|
console.log(`Current version: ${currentVersion}`);
|
|
37
42
|
console.log(`Checking for updates...`);
|
|
@@ -48,6 +53,14 @@ export async function update(): Promise<void> {
|
|
|
48
53
|
return;
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
// Stop daemon before updating so the old process doesn't hold stale code
|
|
57
|
+
if (wasRunning) {
|
|
58
|
+
console.log('Stopping daemon before update...');
|
|
59
|
+
stop();
|
|
60
|
+
// Wait for stop to complete (uses setTimeout internally)
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
console.log(`Updating ${packageName}: v${currentVersion} → v${latestVersion}`);
|
|
52
65
|
|
|
53
66
|
try {
|
|
@@ -68,6 +81,14 @@ export async function update(): Promise<void> {
|
|
|
68
81
|
console.log(`\nUpdate installed. Run 'cmdctrl-cursor-cli --version' to verify.`);
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
// Restart daemon if it was running before update
|
|
85
|
+
if (wasRunning) {
|
|
86
|
+
console.log('Restarting daemon...');
|
|
87
|
+
const child = spawn('cmdctrl-cursor-cli', ['start'], {
|
|
88
|
+
detached: true,
|
|
89
|
+
stdio: 'ignore',
|
|
90
|
+
});
|
|
91
|
+
child.unref();
|
|
92
|
+
console.log('Daemon restarted.');
|
|
93
|
+
}
|
|
73
94
|
}
|