@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.
Files changed (51) hide show
  1. package/dist/adapter/cursor-cli.d.ts +23 -19
  2. package/dist/adapter/cursor-cli.d.ts.map +1 -1
  3. package/dist/adapter/cursor-cli.js +156 -126
  4. package/dist/adapter/cursor-cli.js.map +1 -1
  5. package/dist/adapter/events.d.ts +36 -20
  6. package/dist/adapter/events.d.ts.map +1 -1
  7. package/dist/adapter/events.js +40 -35
  8. package/dist/adapter/events.js.map +1 -1
  9. package/dist/commands/register.d.ts +0 -3
  10. package/dist/commands/register.d.ts.map +1 -1
  11. package/dist/commands/register.js +23 -122
  12. package/dist/commands/register.js.map +1 -1
  13. package/dist/commands/start.d.ts +1 -8
  14. package/dist/commands/start.d.ts.map +1 -1
  15. package/dist/commands/start.js +117 -30
  16. package/dist/commands/start.js.map +1 -1
  17. package/dist/commands/status.d.ts +1 -4
  18. package/dist/commands/status.d.ts.map +1 -1
  19. package/dist/commands/status.js +25 -22
  20. package/dist/commands/status.js.map +1 -1
  21. package/dist/commands/stop.d.ts +1 -4
  22. package/dist/commands/stop.d.ts.map +1 -1
  23. package/dist/commands/stop.js +21 -26
  24. package/dist/commands/stop.js.map +1 -1
  25. package/dist/commands/unregister.d.ts +2 -0
  26. package/dist/commands/unregister.d.ts.map +1 -0
  27. package/dist/commands/unregister.js +43 -0
  28. package/dist/commands/unregister.js.map +1 -0
  29. package/dist/commands/update.d.ts.map +1 -1
  30. package/dist/commands/update.js +21 -2
  31. package/dist/commands/update.js.map +1 -1
  32. package/dist/index.js +8 -4
  33. package/dist/index.js.map +1 -1
  34. package/dist/message-store.d.ts +18 -0
  35. package/dist/message-store.d.ts.map +1 -0
  36. package/dist/message-store.js +49 -0
  37. package/dist/message-store.js.map +1 -0
  38. package/package.json +2 -2
  39. package/src/adapter/cursor-cli.ts +165 -147
  40. package/src/adapter/events.ts +65 -51
  41. package/src/commands/register.ts +28 -170
  42. package/src/commands/start.ts +132 -41
  43. package/src/commands/status.ts +23 -28
  44. package/src/commands/stop.ts +21 -32
  45. package/src/commands/unregister.ts +43 -0
  46. package/src/commands/update.ts +24 -3
  47. package/src/index.ts +9 -4
  48. package/src/message-store.ts +61 -0
  49. package/src/client/messages.ts +0 -75
  50. package/src/client/websocket.ts +0 -308
  51. package/src/config/config.ts +0 -146
@@ -1,199 +1,57 @@
1
1
  import * as os from 'os';
2
- import * as http from 'http';
3
- import * as https from 'https';
4
- import { URL } from 'url';
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(/\/$/, ''); // Remove trailing slash
12
+ const serverUrl = options.server.replace(/\/$/, '');
130
13
  const deviceName = options.name || `${os.hostname()}-cursor`;
131
14
 
132
- // Check if already registered
133
- if (isRegistered()) {
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: ${process.argv[1]} unregister`);
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
- // Step 1: Request device code
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
- codeResponse.deviceCode,
172
- codeResponse.interval,
173
- codeResponse.expiresIn
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 (!tokenResponse) {
37
+ if (!result) {
38
+ console.error('\nDevice code expired. Please try again.');
177
39
  process.exit(1);
178
40
  }
179
41
 
180
- // Step 4: Save config and credentials
181
- const config: CmdCtrlConfig = {
42
+ configManager.writeConfig({
182
43
  serverUrl,
183
- deviceId: tokenResponse.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
- writeConfig(config);
194
- writeCredentials(credentials);
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: ${tokenResponse.deviceId}`);
55
+ console.log(`Device ID: ${result.deviceId}`);
198
56
  console.log(`\nRun 'cmdctrl-cursor-cli start' to connect to the server.`);
199
57
  }
@@ -1,62 +1,153 @@
1
- import {
2
- readConfig,
3
- readCredentials,
4
- isRegistered,
5
- writePidFile,
6
- isDaemonRunning
7
- } from '../config/config';
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
- * Start the daemon
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
- // Check if already running
25
- if (isDaemonRunning()) {
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
- console.log(`Starting Cursor CLI daemon for device "${config.deviceName}"...`);
34
- console.log(`Server: ${config.serverUrl}`);
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
- // Write PID file
37
- writePidFile(process.pid);
69
+ sendEvent(taskId, eventType, data);
70
+ });
38
71
 
39
- // Create and connect client
40
- const client = new DaemonClient(config, credentials);
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
- // Handle shutdown signals
43
- const shutdown = async (signal: string) => {
44
- console.log(`\nReceived ${signal}, shutting down...`);
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', () => shutdown('SIGINT'));
50
- process.on('SIGTERM', () => shutdown('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 connected and ready for tasks.');
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);
@@ -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
- * Show daemon status
11
- */
12
- export async function status(): Promise<void> {
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-daemon register" to register this device.');
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
- const expired = expiresIn <= 0;
35
- console.log(`Token: ${expired ? 'Expired' : 'Valid'}`);
36
- if (!expired) {
37
- const hours = Math.floor(expiresIn / 1000 / 60 / 60);
38
- const minutes = Math.floor((expiresIn / 1000 / 60) % 60);
39
- console.log(`Expires in: ${hours}h ${minutes}m`);
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 (!running) {
44
- console.log('\nRun "cmdctrl-cursor-cli-daemon start" to start the daemon.');
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
  }
@@ -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
- * Stop the daemon
9
- */
10
- export async function stop(): Promise<void> {
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 (!pid) {
18
- console.log('Could not find daemon PID.');
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
- // Wait a bit for graceful shutdown
29
- await new Promise(resolve => setTimeout(resolve, 1000));
30
-
31
- // Check if still running
32
- try {
33
- process.kill(pid, 0);
34
- // Still running, send SIGKILL
35
- console.log('Daemon not responding, force killing...');
36
- process.kill(pid, 'SIGKILL');
37
- } catch {
38
- // Process is gone
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
+ }
@@ -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
- console.log('\nIf the daemon is running, restart it:');
72
- console.log(' cmdctrl-cursor-cli stop && cmdctrl-cursor-cli start');
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
  }