@latentforce/shift 1.0.1 → 1.0.3

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.
@@ -0,0 +1,51 @@
1
+ import { getApiKey, readProjectConfig, isGuestKey } from '../../utils/config.js';
2
+ import { getDaemonStatus } from '../../daemon/daemon-manager.js';
3
+ export async function statusCommand() {
4
+ const projectRoot = process.cwd();
5
+ console.log('\n╔════════════════════════════════════════════╗');
6
+ console.log('║ Shift Status ║');
7
+ console.log('╚════════════════════════════════════════════╝\n');
8
+ // Check API key
9
+ const apiKey = getApiKey();
10
+ if (!apiKey) {
11
+ console.log('API Key: ❌ Not configured');
12
+ console.log('\nRun "shift start" to configure your API key.\n');
13
+ return;
14
+ }
15
+ if (isGuestKey()) {
16
+ console.log('API Key: ✓ Guest key');
17
+ }
18
+ else {
19
+ console.log('API Key: ✓ Configured');
20
+ }
21
+ // Check project config
22
+ const projectConfig = readProjectConfig(projectRoot);
23
+ if (!projectConfig) {
24
+ console.log('Project: ❌ Not configured');
25
+ console.log('\nRun "shift start" to configure this project.\n');
26
+ return;
27
+ }
28
+ console.log(`Project: ${projectConfig.project_name}`);
29
+ console.log(`Project ID: ${projectConfig.project_id}`);
30
+ // Check agents
31
+ if (projectConfig.agents && projectConfig.agents.length > 0) {
32
+ console.log(`Agents: ${projectConfig.agents.length} configured`);
33
+ projectConfig.agents.forEach((agent) => {
34
+ console.log(` - ${agent.agent_name} (${agent.agent_type})`);
35
+ });
36
+ }
37
+ // Check daemon status
38
+ const status = getDaemonStatus(projectRoot);
39
+ if (!status.running) {
40
+ console.log('Daemon: ❌ Not running');
41
+ console.log('\nRun "shift start" to start the daemon.\n');
42
+ return;
43
+ }
44
+ console.log(`Daemon: ✓ Running (PID: ${status.pid})`);
45
+ console.log(`WebSocket: ${status.connected ? '✓ Connected' : '⚠️ Disconnected'}`);
46
+ if (status.startedAt) {
47
+ const startedAt = new Date(status.startedAt);
48
+ console.log(`Started: ${startedAt.toLocaleString()}`);
49
+ }
50
+ console.log('');
51
+ }
@@ -0,0 +1,18 @@
1
+ import { getDaemonStatus, stopDaemon } from '../../daemon/daemon-manager.js';
2
+ export async function stopCommand() {
3
+ const projectRoot = process.cwd();
4
+ console.log('\nStopping Shift daemon...\n');
5
+ // Check if daemon is running
6
+ const status = getDaemonStatus(projectRoot);
7
+ if (!status.running) {
8
+ console.log('Daemon is not running.');
9
+ return;
10
+ }
11
+ console.log(`Stopping daemon (PID: ${status.pid})...`);
12
+ const result = await stopDaemon(projectRoot);
13
+ if (!result.success) {
14
+ console.error(`Failed to stop daemon: ${result.error}`);
15
+ process.exit(1);
16
+ }
17
+ console.log('Daemon stopped successfully.\n');
18
+ }
@@ -0,0 +1,136 @@
1
+ import { spawn } from 'child_process';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ import { readDaemonPid, writeDaemonPid, removeDaemonPid, removeDaemonStatus, readDaemonStatus, isProcessRunning, } from '../utils/config.js';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ function getDaemonScriptPath() {
9
+ // The daemon.js will be in the same directory as daemon-manager.js after compilation
10
+ return path.join(__dirname, 'daemon.js');
11
+ }
12
+ export async function startDaemon(projectRoot, projectId, apiKey) {
13
+ // Check if daemon is already running
14
+ const existingPid = readDaemonPid(projectRoot);
15
+ if (existingPid && isProcessRunning(existingPid)) {
16
+ return {
17
+ success: false,
18
+ error: `Daemon already running with PID ${existingPid}`,
19
+ pid: existingPid,
20
+ };
21
+ }
22
+ // Clean up stale files if process is not running
23
+ if (existingPid) {
24
+ removeDaemonPid(projectRoot);
25
+ removeDaemonStatus(projectRoot);
26
+ }
27
+ const daemonScript = getDaemonScriptPath();
28
+ if (!fs.existsSync(daemonScript)) {
29
+ return {
30
+ success: false,
31
+ error: `Daemon script not found at ${daemonScript}`,
32
+ };
33
+ }
34
+ try {
35
+ // Spawn detached daemon process
36
+ const child = spawn(process.execPath, [daemonScript, projectRoot, projectId, apiKey], {
37
+ detached: true,
38
+ stdio: 'ignore',
39
+ cwd: projectRoot,
40
+ });
41
+ if (!child.pid) {
42
+ return {
43
+ success: false,
44
+ error: 'Failed to spawn daemon process',
45
+ };
46
+ }
47
+ // Detach from parent
48
+ child.unref();
49
+ // Write PID file
50
+ writeDaemonPid(child.pid, projectRoot);
51
+ return {
52
+ success: true,
53
+ pid: child.pid,
54
+ };
55
+ }
56
+ catch (error) {
57
+ return {
58
+ success: false,
59
+ error: `Failed to start daemon: ${error.message}`,
60
+ };
61
+ }
62
+ }
63
+ export async function stopDaemon(projectRoot) {
64
+ const pid = readDaemonPid(projectRoot);
65
+ if (!pid) {
66
+ return {
67
+ success: false,
68
+ error: 'No daemon PID file found',
69
+ };
70
+ }
71
+ if (!isProcessRunning(pid)) {
72
+ // Clean up stale files
73
+ removeDaemonPid(projectRoot);
74
+ removeDaemonStatus(projectRoot);
75
+ return {
76
+ success: true,
77
+ };
78
+ }
79
+ try {
80
+ // Send SIGTERM for graceful shutdown
81
+ process.kill(pid, 'SIGTERM');
82
+ // Wait for process to exit (with timeout)
83
+ const maxWait = 5000;
84
+ const checkInterval = 100;
85
+ let waited = 0;
86
+ while (waited < maxWait && isProcessRunning(pid)) {
87
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
88
+ waited += checkInterval;
89
+ }
90
+ // If still running, force kill
91
+ if (isProcessRunning(pid)) {
92
+ try {
93
+ process.kill(pid, 'SIGKILL');
94
+ }
95
+ catch {
96
+ // Process might have exited between check and kill
97
+ }
98
+ }
99
+ // Clean up files
100
+ removeDaemonPid(projectRoot);
101
+ removeDaemonStatus(projectRoot);
102
+ return {
103
+ success: true,
104
+ };
105
+ }
106
+ catch (error) {
107
+ // Clean up files even on error
108
+ removeDaemonPid(projectRoot);
109
+ removeDaemonStatus(projectRoot);
110
+ return {
111
+ success: false,
112
+ error: `Failed to stop daemon: ${error.message}`,
113
+ };
114
+ }
115
+ }
116
+ export function getDaemonStatus(projectRoot) {
117
+ const pid = readDaemonPid(projectRoot);
118
+ const status = readDaemonStatus(projectRoot);
119
+ if (!pid) {
120
+ return { running: false };
121
+ }
122
+ const running = isProcessRunning(pid);
123
+ if (!running) {
124
+ // Clean up stale files
125
+ removeDaemonPid(projectRoot);
126
+ removeDaemonStatus(projectRoot);
127
+ return { running: false };
128
+ }
129
+ return {
130
+ running: true,
131
+ pid,
132
+ connected: status?.connected ?? false,
133
+ projectId: status?.project_id,
134
+ startedAt: status?.started_at,
135
+ };
136
+ }
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Daemon process entry point.
4
+ * This script runs as a detached background process and manages the WebSocket connection.
5
+ * Matching extension's behavior.
6
+ *
7
+ * Usage: node daemon.js <projectRoot> <projectId> <apiKey>
8
+ */
9
+ import { WebSocketClient } from './websocket-client.js';
10
+ import { writeDaemonStatus, removeDaemonPid, removeDaemonStatus } from '../utils/config.js';
11
+ const args = process.argv.slice(2);
12
+ if (args.length < 3) {
13
+ console.error('Usage: daemon <projectRoot> <projectId> <apiKey>');
14
+ process.exit(1);
15
+ }
16
+ const [projectRoot, projectId, apiKey] = args;
17
+ let wsClient = null;
18
+ let isShuttingDown = false;
19
+ let startedAt = new Date().toISOString();
20
+ function updateStatus(connected) {
21
+ const status = {
22
+ pid: process.pid,
23
+ connected,
24
+ project_id: projectId,
25
+ started_at: startedAt,
26
+ };
27
+ writeDaemonStatus(status, projectRoot);
28
+ }
29
+ function cleanup() {
30
+ if (isShuttingDown)
31
+ return;
32
+ isShuttingDown = true;
33
+ console.log('[Daemon] Shutting down...');
34
+ if (wsClient) {
35
+ wsClient.disconnect();
36
+ wsClient = null;
37
+ }
38
+ removeDaemonPid(projectRoot);
39
+ removeDaemonStatus(projectRoot);
40
+ process.exit(0);
41
+ }
42
+ // Handle graceful shutdown
43
+ process.on('SIGTERM', cleanup);
44
+ process.on('SIGINT', cleanup);
45
+ process.on('SIGHUP', cleanup);
46
+ // Handle uncaught errors
47
+ process.on('uncaughtException', (error) => {
48
+ console.error('[Daemon] Uncaught exception:', error);
49
+ cleanup();
50
+ });
51
+ process.on('unhandledRejection', (reason) => {
52
+ console.error('[Daemon] Unhandled rejection:', reason);
53
+ cleanup();
54
+ });
55
+ async function main() {
56
+ console.log('╔════════════════════════════════════════════╗');
57
+ console.log('║ Shift Daemon Starting ║');
58
+ console.log('╚════════════════════════════════════════════╝');
59
+ console.log(`[Daemon] Project ID: ${projectId}`);
60
+ console.log(`[Daemon] Project root: ${projectRoot}`);
61
+ console.log(`[Daemon] PID: ${process.pid}`);
62
+ // Initial status - not connected yet
63
+ updateStatus(false);
64
+ wsClient = new WebSocketClient({
65
+ projectId,
66
+ apiKey,
67
+ workspaceRoot: projectRoot,
68
+ });
69
+ // Set up event listeners - matching extension's setupWebSocketListeners
70
+ wsClient.on('connecting', () => {
71
+ console.log('[Daemon] WebSocket connecting...');
72
+ });
73
+ wsClient.on('connected', (projectInfo) => {
74
+ console.log('[Daemon] ✓ Connected to project:', projectInfo?.project_name);
75
+ updateStatus(true);
76
+ });
77
+ wsClient.on('disconnected', (code, reason) => {
78
+ console.log(`[Daemon] Disconnected (code: ${code}, reason: ${reason})`);
79
+ updateStatus(false);
80
+ });
81
+ wsClient.on('auth_failed', (message) => {
82
+ console.error(`[Daemon] ❌ Authentication failed: ${message}`);
83
+ updateStatus(false);
84
+ // Don't exit immediately, let the reconnect logic handle it
85
+ });
86
+ wsClient.on('error', (error) => {
87
+ console.error('[Daemon] WebSocket error:', error.message);
88
+ updateStatus(false);
89
+ });
90
+ wsClient.on('reconnecting', (attempt) => {
91
+ console.log(`[Daemon] Reconnecting... (attempt ${attempt})`);
92
+ });
93
+ wsClient.on('max_reconnects_reached', () => {
94
+ console.error('[Daemon] ❌ Max reconnection attempts reached. Stopping daemon.');
95
+ cleanup();
96
+ });
97
+ wsClient.on('message', (message) => {
98
+ console.log(`[Daemon] Received message: ${message.type}`);
99
+ });
100
+ wsClient.on('tool_executed', ({ tool, result, requestId }) => {
101
+ console.log(`[Daemon] ✓ Tool executed: ${tool} (${requestId})`);
102
+ });
103
+ wsClient.on('tool_error', ({ tool, error, requestId }) => {
104
+ console.error(`[Daemon] ✗ Tool failed: ${tool} (${requestId}):`, error.message);
105
+ });
106
+ // Connect to WebSocket
107
+ try {
108
+ await wsClient.connect();
109
+ console.log('[Daemon] ✓ Initial connection successful');
110
+ }
111
+ catch (error) {
112
+ console.error('[Daemon] Initial connection failed:', error.message);
113
+ // Don't exit - the WebSocket client will handle reconnection
114
+ }
115
+ }
116
+ main().catch((error) => {
117
+ console.error('[Daemon] Fatal error:', error);
118
+ cleanup();
119
+ });