@latentforce/shift 1.0.0 → 1.0.2
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/README.md +108 -64
- package/build/cli/commands/init.js +136 -0
- package/build/cli/commands/start.js +81 -0
- package/build/cli/commands/status.js +46 -0
- package/build/cli/commands/stop.js +18 -0
- package/build/daemon/daemon-manager.js +136 -0
- package/build/daemon/daemon.js +119 -0
- package/build/daemon/tools-executor.js +383 -0
- package/build/daemon/websocket-client.js +334 -0
- package/build/index.js +39 -126
- package/build/mcp-server.js +124 -0
- package/build/utils/api-client.js +50 -0
- package/build/utils/config.js +165 -0
- package/build/utils/prompts.js +50 -0
- package/build/utils/tree-scanner.js +148 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,103 +1,108 @@
|
|
|
1
|
-
# shift
|
|
1
|
+
# @latentforce/shift
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A CLI and MCP server for AI-powered code intelligence. It provides:
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- **`SHIFT_PROJECT_ID`** (optional if you pass per call): The default Shift Lite project UUID. If set, all tools use this project unless overridden by the `project_id` parameter in a tool call.
|
|
8
|
-
- Example: `export SHIFT_PROJECT_ID=9af16a19-d073-4134-a0cd-272b0baf912e`
|
|
9
|
-
- **`SHIFT_BACKEND_URL`** (optional): Webapp backend base URL. Default: `http://127.0.0.1:9000`.
|
|
10
|
-
|
|
11
|
-
## Project ID: env vs per-call
|
|
12
|
-
|
|
13
|
-
- **Default:** Set `SHIFT_PROJECT_ID` in the MCP config (or env). All tools use that project.
|
|
14
|
-
- **Override per call:** Each tool (`blast_radius`, `dependencies`, `file_summary`) accepts an optional **`project_id`** parameter. If you pass it, that call uses that project; otherwise the tool uses `SHIFT_PROJECT_ID`. So you can use one MCP for multiple projects by passing `project_id` in the call (e.g. "blast radius for project abc-123").
|
|
5
|
+
1. **CLI commands** - Index and manage your project
|
|
6
|
+
2. **MCP server** - Expose tools (blast_radius, dependencies, file_summary) to Claude Desktop/Code
|
|
15
7
|
|
|
16
8
|
## Installation
|
|
17
9
|
|
|
18
|
-
### Option 1: Install globally (simple, persistent)
|
|
19
|
-
|
|
20
10
|
```bash
|
|
21
11
|
npm install -g @latentforce/shift
|
|
22
12
|
```
|
|
23
13
|
|
|
24
|
-
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Step 1: Start and configure your project
|
|
17
|
+
|
|
25
18
|
```bash
|
|
26
|
-
|
|
19
|
+
cd /path/to/your/project
|
|
20
|
+
shift start
|
|
27
21
|
```
|
|
28
22
|
|
|
29
|
-
|
|
23
|
+
This will:
|
|
24
|
+
- Prompt for your Shift API key (saved to `~/.shift/config.json`)
|
|
25
|
+
- Let you select or enter your project ID
|
|
26
|
+
- Start a background daemon that connects to the backend
|
|
27
|
+
|
|
28
|
+
### Step 2: Index your project
|
|
29
|
+
|
|
30
30
|
```bash
|
|
31
|
-
|
|
31
|
+
shift init
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
This scans your project files and sends the structure to the backend for indexing.
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
### Step 3: Configure MCP for Claude
|
|
36
37
|
|
|
37
|
-
Claude
|
|
38
|
+
Now Claude can use the MCP tools. See [Claude Desktop setup](#claude-desktop-setup) or [Claude Code setup](#claude-code-setup) below.
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
You can use this package without installing it globally by using npx, or install it globally for faster access.
|
|
40
|
+
## CLI Commands
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
| Command | Description |
|
|
43
|
+
|---------|-------------|
|
|
44
|
+
| `shift start` | Start the daemon and configure the project |
|
|
45
|
+
| `shift init` | Scan and index project files to backend |
|
|
46
|
+
| `shift stop` | Stop the running daemon |
|
|
47
|
+
| `shift status` | Show current status (API key, project, daemon, connection) |
|
|
48
|
+
| `shift` or `shift mcp` | Start MCP server on stdio (used by Claude) |
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
- Replace SHIFT_BACKEND_URL and SHIFT_PROJECT_ID with correct values.
|
|
48
|
-
- SHIFT_PROJECT_ID may be ignored as every tool accepts an optional **`project_id`** parameter.
|
|
49
|
-
For Windows Users
|
|
50
|
-
```bash
|
|
51
|
-
claude mcp add --scope user --transport stdio --env SHIFT_BACKEND_URL=BACKEND_URL --env SHIFT_PROJECT_ID=9af16a19-d073-4134-a0cd-272b0baf912e shift -- cmd /c npx -y @latentforce/shift
|
|
52
|
-
```
|
|
50
|
+
## MCP Tools
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
claude mcp add --scope user --transport stdio --env SHIFT_BACKEND_URL=BACKEND_URL --env SHIFT_PROJECT_ID=9af16a19-d073-4134-a0cd-272b0baf912e shift -- npx -y @latentforce/shift
|
|
57
|
-
```
|
|
52
|
+
The MCP server exposes these tools to Claude:
|
|
58
53
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
| Tool | Description |
|
|
55
|
+
|------|-------------|
|
|
56
|
+
| `blast_radius` | Analyze what would be affected if a file is modified |
|
|
57
|
+
| `dependencies` | Get all dependencies for a file (direct and transitive) |
|
|
58
|
+
| `file_summary` | Generate a summary of a file with optional parent context |
|
|
63
59
|
|
|
64
|
-
|
|
65
|
-
Start a Claude Code session:
|
|
66
|
-
```bash
|
|
67
|
-
claude
|
|
68
|
-
```
|
|
60
|
+
## Environment Variables
|
|
69
61
|
|
|
70
|
-
|
|
62
|
+
| Variable | Description | Default |
|
|
63
|
+
|----------|-------------|---------|
|
|
64
|
+
| `SHIFT_PROJECT_ID` | Default project UUID (can be overridden per tool call) | - |
|
|
65
|
+
| `SHIFT_BACKEND_URL` | Backend API URL | `http://127.0.0.1:9000` |
|
|
66
|
+
| `SHIFT_API_URL` | API URL for CLI | `http://localhost:9000` |
|
|
67
|
+
| `SHIFT_ORCH_URL` | Orchestrator URL | `http://localhost:9999` |
|
|
68
|
+
| `SHIFT_WS_URL` | WebSocket URL | `ws://localhost:9999` |
|
|
71
69
|
|
|
72
|
-
Claude
|
|
70
|
+
## Claude Code Setup
|
|
73
71
|
|
|
74
|
-
###
|
|
72
|
+
### Add MCP Server
|
|
75
73
|
|
|
76
|
-
**macOS (Claude Desktop app):**
|
|
77
74
|
```bash
|
|
78
|
-
|
|
75
|
+
claude mcp add-json shift '{"type":"stdio","command":"shift","args":[],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID_HERE","SHIFT_BACKEND_URL":"https://dev-shift-lite.latentforce.ai"}}'
|
|
79
76
|
```
|
|
80
|
-
*(In Claude Desktop: Claude menu → Settings… → Developer → Edit Config)*
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
~/.config/claude-desktop/claude_desktop_config.json
|
|
85
|
-
```
|
|
78
|
+
### Verify
|
|
86
79
|
|
|
87
|
-
**Windows:**
|
|
88
80
|
```bash
|
|
89
|
-
|
|
81
|
+
claude mcp list
|
|
90
82
|
```
|
|
91
83
|
|
|
92
|
-
|
|
84
|
+
## Claude Desktop Setup
|
|
85
|
+
|
|
86
|
+
### Config File Location
|
|
87
|
+
|
|
88
|
+
| OS | Path |
|
|
89
|
+
|----|------|
|
|
90
|
+
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
91
|
+
| Linux | `~/.config/claude-desktop/claude_desktop_config.json` |
|
|
92
|
+
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
93
|
+
|
|
94
|
+
### Configuration
|
|
95
|
+
|
|
96
|
+
Add to your config file:
|
|
97
|
+
|
|
93
98
|
```json
|
|
94
99
|
{
|
|
95
100
|
"mcpServers": {
|
|
96
101
|
"shift": {
|
|
97
102
|
"command": "shift",
|
|
98
103
|
"env": {
|
|
99
|
-
"SHIFT_BACKEND_URL": "
|
|
100
|
-
"SHIFT_PROJECT_ID": "
|
|
104
|
+
"SHIFT_BACKEND_URL": "https://dev-shift-lite.latentforce.ai",
|
|
105
|
+
"SHIFT_PROJECT_ID": "YOUR_PROJECT_ID_HERE"
|
|
101
106
|
}
|
|
102
107
|
}
|
|
103
108
|
}
|
|
@@ -106,16 +111,55 @@ Add this (replace `YOUR_PROJECT_UUID` with your Shift Lite project ID):
|
|
|
106
111
|
|
|
107
112
|
Restart Claude Desktop after saving.
|
|
108
113
|
|
|
114
|
+
## Project ID
|
|
115
|
+
|
|
116
|
+
You can set the project ID in two ways:
|
|
117
|
+
|
|
118
|
+
1. **Environment variable**: Set `SHIFT_PROJECT_ID` in MCP config
|
|
119
|
+
2. **Per-call override**: Pass `project_id` parameter in each tool call
|
|
120
|
+
|
|
121
|
+
This allows one MCP server to work with multiple projects.
|
|
122
|
+
|
|
109
123
|
## Development
|
|
110
124
|
|
|
111
|
-
Clone and build:
|
|
112
125
|
```bash
|
|
126
|
+
# Clone and install
|
|
113
127
|
npm install
|
|
128
|
+
|
|
129
|
+
# Build
|
|
114
130
|
npm run build
|
|
115
|
-
```
|
|
116
131
|
|
|
117
|
-
Run locally
|
|
118
|
-
```bash
|
|
132
|
+
# Run locally
|
|
119
133
|
node build/index.js
|
|
134
|
+
|
|
135
|
+
# Test CLI commands
|
|
136
|
+
node build/index.js start
|
|
137
|
+
node build/index.js init
|
|
138
|
+
node build/index.js status
|
|
139
|
+
node build/index.js stop
|
|
120
140
|
```
|
|
121
141
|
|
|
142
|
+
## How It All Works Together
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
146
|
+
│ User's Machine │
|
|
147
|
+
│ │
|
|
148
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
|
|
149
|
+
│ │ shift start │────▶│ Daemon │────▶│ Backend (WS) │ │
|
|
150
|
+
│ │ shift init │ │ (background)│ │ │ │
|
|
151
|
+
│ │ shift stop │ └─────────────┘ │ │ │
|
|
152
|
+
│ │ shift status│ │ │ │
|
|
153
|
+
│ └─────────────┘ │ Shift Lite │ │
|
|
154
|
+
│ │ Backend │ │
|
|
155
|
+
│ ┌─────────────┐ ┌─────────────┐ │ │ │
|
|
156
|
+
│ │ Claude │────▶│ shift (MCP) │────▶│ (REST API) │ │
|
|
157
|
+
│ │ Desktop/Code│ │ Server │ │ │ │
|
|
158
|
+
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
|
|
159
|
+
│ │
|
|
160
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
161
|
+
|
|
162
|
+
1. `shift start` - Starts daemon, connects to backend via WebSocket
|
|
163
|
+
2. `shift init` - Indexes project files to backend
|
|
164
|
+
3. Claude calls MCP tools - MCP server queries backend REST API
|
|
165
|
+
```
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { getApiKey, setApiKey, readProjectConfig, writeProjectConfig } from '../../utils/config.js';
|
|
4
|
+
import { promptApiKey } from '../../utils/prompts.js';
|
|
5
|
+
import { getDaemonStatus } from '../../daemon/daemon-manager.js';
|
|
6
|
+
import { getProjectTree, extractAllFilePaths, categorizeFiles } from '../../utils/tree-scanner.js';
|
|
7
|
+
import { sendInitScan } from '../../utils/api-client.js';
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
export async function initCommand() {
|
|
10
|
+
const projectRoot = process.cwd();
|
|
11
|
+
console.log('╔═══════════════════════════════════════════════╗');
|
|
12
|
+
console.log('║ Initializing Shift Project ║');
|
|
13
|
+
console.log('╚═══════════════════════════════════════════════╝\n');
|
|
14
|
+
// Step 1: Check API key
|
|
15
|
+
console.log('[Init] Step 1/5: Checking API key...');
|
|
16
|
+
let apiKey = getApiKey();
|
|
17
|
+
if (!apiKey) {
|
|
18
|
+
apiKey = await promptApiKey();
|
|
19
|
+
setApiKey(apiKey);
|
|
20
|
+
console.log('[Init] ✓ API key saved\n');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.log('[Init] ✓ API key found\n');
|
|
24
|
+
}
|
|
25
|
+
// Step 2: Check project config
|
|
26
|
+
console.log('[Init] Step 2/5: Checking project configuration...');
|
|
27
|
+
const projectConfig = readProjectConfig(projectRoot);
|
|
28
|
+
if (!projectConfig) {
|
|
29
|
+
console.error('\n❌ No project configured for this directory.');
|
|
30
|
+
console.log('Run "shift start" first to configure the project.\n');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
console.log(`[Init] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
|
|
34
|
+
// Step 3: Check daemon status (warn if not running)
|
|
35
|
+
console.log('[Init] Step 3/5: Checking daemon status...');
|
|
36
|
+
const status = getDaemonStatus(projectRoot);
|
|
37
|
+
if (!status.running) {
|
|
38
|
+
console.log('[Init] ⚠️ Warning: Daemon is not running. Run "shift start" to start it.\n');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log(`[Init] ✓ Daemon running (PID: ${status.pid}, Connected: ${status.connected})\n`);
|
|
42
|
+
}
|
|
43
|
+
// Step 4: Scan project structure (matching extension's Step 6)
|
|
44
|
+
console.log('[Init] Step 4/5: Scanning project structure...');
|
|
45
|
+
// Get project tree (matching extension's getProjectTree)
|
|
46
|
+
const treeData = getProjectTree(projectRoot, {
|
|
47
|
+
depth: 0, // Unlimited depth
|
|
48
|
+
exclude_patterns: [
|
|
49
|
+
'.git',
|
|
50
|
+
'node_modules',
|
|
51
|
+
'__pycache__',
|
|
52
|
+
'.vscode',
|
|
53
|
+
'dist',
|
|
54
|
+
'build',
|
|
55
|
+
'.shift',
|
|
56
|
+
'.next',
|
|
57
|
+
'coverage',
|
|
58
|
+
'venv',
|
|
59
|
+
'env',
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
console.log(`[Init] Files: ${treeData.file_count}`);
|
|
63
|
+
console.log(`[Init] Directories: ${treeData.dir_count}`);
|
|
64
|
+
console.log(`[Init] Total size: ${treeData.total_size_mb} MB\n`);
|
|
65
|
+
// Get git info (matching extension)
|
|
66
|
+
let gitInfo = {
|
|
67
|
+
current_branch: '',
|
|
68
|
+
original_branch: 'shift_original',
|
|
69
|
+
migrate_branch: 'shift_migrated',
|
|
70
|
+
has_uncommitted_changes: false,
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
const { stdout: currentBranch } = await execAsync('git branch --show-current', { cwd: projectRoot });
|
|
74
|
+
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: projectRoot });
|
|
75
|
+
gitInfo.current_branch = currentBranch.trim();
|
|
76
|
+
gitInfo.has_uncommitted_changes = statusOutput.trim().length > 0;
|
|
77
|
+
console.log(`[Init] ✓ Git info: branch=${gitInfo.current_branch}, uncommitted=${gitInfo.has_uncommitted_changes}\n`);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
console.log('[Init] ⚠️ Not a git repository or git not available\n');
|
|
81
|
+
}
|
|
82
|
+
// Extract file paths and categorize (matching extension)
|
|
83
|
+
const allFiles = extractAllFilePaths(treeData.tree);
|
|
84
|
+
const categorized = categorizeFiles(treeData.tree);
|
|
85
|
+
console.log(`[Init] Source files: ${categorized.source_files.length}`);
|
|
86
|
+
console.log(`[Init] Config files: ${categorized.config_files.length}`);
|
|
87
|
+
console.log(`[Init] Asset files: ${categorized.asset_files.length}\n`);
|
|
88
|
+
// Step 5: Send scan to backend (matching extension's Step 9)
|
|
89
|
+
console.log('[Init] Step 5/5: Sending scan to backend...');
|
|
90
|
+
const payload = {
|
|
91
|
+
project_id: projectConfig.project_id,
|
|
92
|
+
project_tree: treeData,
|
|
93
|
+
git_info: gitInfo,
|
|
94
|
+
file_manifest: {
|
|
95
|
+
all_files: allFiles,
|
|
96
|
+
categorized: categorized,
|
|
97
|
+
},
|
|
98
|
+
metadata: {
|
|
99
|
+
extension_version: '1.0.2-cli', // CLI version
|
|
100
|
+
scan_timestamp: new Date().toISOString(),
|
|
101
|
+
project_name: projectConfig.project_name,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
const response = await sendInitScan(apiKey, projectConfig.project_id, payload);
|
|
106
|
+
console.log('[Init] ✓ Backend initialization completed');
|
|
107
|
+
console.log(`[Init] Files read: ${response.files_read}`);
|
|
108
|
+
console.log(`[Init] Files failed: ${response.files_failed}`);
|
|
109
|
+
// Update local config with agent info (matching extension)
|
|
110
|
+
if (response.agents_created?.theme_planner) {
|
|
111
|
+
const agentInfo = {
|
|
112
|
+
agent_id: response.agents_created.theme_planner.agent_id,
|
|
113
|
+
agent_name: response.agents_created.theme_planner.agent_name,
|
|
114
|
+
agent_type: 'theme_planner',
|
|
115
|
+
created_at: new Date().toISOString(),
|
|
116
|
+
};
|
|
117
|
+
projectConfig.agents = projectConfig.agents || [];
|
|
118
|
+
projectConfig.agents.push(agentInfo);
|
|
119
|
+
writeProjectConfig(projectConfig, projectRoot);
|
|
120
|
+
console.log(`[Init] ✓ Agent info saved: ${agentInfo.agent_id}`);
|
|
121
|
+
}
|
|
122
|
+
console.log('\n╔═══════════════════════════════════════════════╗');
|
|
123
|
+
console.log('║ ✓ Project Initialization Complete ║');
|
|
124
|
+
console.log('╚═══════════════════════════════════════════════╝');
|
|
125
|
+
if (response.next_steps) {
|
|
126
|
+
console.log('\nNext steps:');
|
|
127
|
+
response.next_steps.forEach((step) => console.log(` - ${step}`));
|
|
128
|
+
}
|
|
129
|
+
console.log('\nFile indexing has been triggered. This may take a few moments.');
|
|
130
|
+
console.log('Use "shift status" to check the daemon connection.\n');
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.error(`\n❌ Failed to initialize project: ${error.message}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { getApiKey, setApiKey, setProject, readProjectConfig } from '../../utils/config.js';
|
|
2
|
+
import { promptApiKey, promptProjectId, promptSelectProject } from '../../utils/prompts.js';
|
|
3
|
+
import { startDaemon, getDaemonStatus } from '../../daemon/daemon-manager.js';
|
|
4
|
+
import { fetchProjects } from '../../utils/api-client.js';
|
|
5
|
+
export async function startCommand() {
|
|
6
|
+
const projectRoot = process.cwd();
|
|
7
|
+
console.log('╔════════════════════════════════════════════╗');
|
|
8
|
+
console.log('║ Starting Shift ║');
|
|
9
|
+
console.log('╚════════════════════════════════════════════╝\n');
|
|
10
|
+
// Step 1: Ensure API key exists
|
|
11
|
+
console.log('[Start] Step 1/4: Checking API key...');
|
|
12
|
+
let apiKey = getApiKey();
|
|
13
|
+
if (!apiKey) {
|
|
14
|
+
apiKey = await promptApiKey();
|
|
15
|
+
setApiKey(apiKey);
|
|
16
|
+
console.log('[Start] ✓ API key saved to ~/.shift/config.json\n');
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.log('[Start] ✓ API key found\n');
|
|
20
|
+
}
|
|
21
|
+
// Step 2: Check/create project config
|
|
22
|
+
console.log('[Start] Step 2/4: Checking project configuration...');
|
|
23
|
+
let projectConfig = readProjectConfig(projectRoot);
|
|
24
|
+
if (!projectConfig) {
|
|
25
|
+
// Fetch available projects
|
|
26
|
+
console.log('[Start] Fetching your projects...');
|
|
27
|
+
try {
|
|
28
|
+
const projects = await fetchProjects(apiKey);
|
|
29
|
+
if (!projects || projects.length === 0) {
|
|
30
|
+
console.error('\n❌ No projects found. Create one in the Shift dashboard first.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
console.log(`[Start] Found ${projects.length} project(s)\n`);
|
|
34
|
+
// Let user select a project
|
|
35
|
+
const selected = await promptSelectProject(projects);
|
|
36
|
+
// Save project config (matching extension's .shift/config.json structure)
|
|
37
|
+
setProject(selected.project_id, selected.project_name, projectRoot);
|
|
38
|
+
console.log(`[Start] ✓ Project "${selected.project_name}" saved to .shift/config.json\n`);
|
|
39
|
+
projectConfig = readProjectConfig(projectRoot);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// If fetching fails, fall back to manual entry
|
|
43
|
+
console.error(`\n⚠️ Could not fetch projects: ${error.message}`);
|
|
44
|
+
console.log('Falling back to manual project ID entry...\n');
|
|
45
|
+
const projectId = await promptProjectId();
|
|
46
|
+
setProject(projectId, 'Unknown Project', projectRoot);
|
|
47
|
+
console.log(`[Start] ✓ Project ID saved to .shift/config.json\n`);
|
|
48
|
+
projectConfig = readProjectConfig(projectRoot);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(`[Start] ✓ Project found: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
|
|
53
|
+
}
|
|
54
|
+
if (!projectConfig) {
|
|
55
|
+
console.error('❌ Failed to configure project');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
// Step 3: Check if daemon is already running
|
|
59
|
+
console.log('[Start] Step 3/4: Checking daemon status...');
|
|
60
|
+
const status = getDaemonStatus(projectRoot);
|
|
61
|
+
if (status.running) {
|
|
62
|
+
console.log(`[Start] ✓ Daemon is already running (PID: ${status.pid})`);
|
|
63
|
+
console.log(`[Start] WebSocket: ${status.connected ? 'Connected' : 'Connecting...'}\n`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log('[Start] Daemon not running\n');
|
|
67
|
+
// Step 4: Start daemon
|
|
68
|
+
console.log('[Start] Step 4/4: Starting daemon...');
|
|
69
|
+
const result = await startDaemon(projectRoot, projectConfig.project_id, apiKey);
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
console.error(`\n❌ Failed to start daemon: ${result.error}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
console.log(`[Start] ✓ Daemon started (PID: ${result.pid})`);
|
|
75
|
+
console.log('\n╔════════════════════════════════════════════╗');
|
|
76
|
+
console.log('║ Shift is now running! ║');
|
|
77
|
+
console.log('╚════════════════════════════════════════════╝');
|
|
78
|
+
console.log('\nUse "shift status" to check connection status.');
|
|
79
|
+
console.log('Use "shift init" to scan and index the project.');
|
|
80
|
+
console.log('Use "shift stop" to stop the daemon.\n');
|
|
81
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { getApiKey, readProjectConfig } 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
|
+
console.log('API Key: ✓ Configured');
|
|
16
|
+
// Check project config
|
|
17
|
+
const projectConfig = readProjectConfig(projectRoot);
|
|
18
|
+
if (!projectConfig) {
|
|
19
|
+
console.log('Project: ❌ Not configured');
|
|
20
|
+
console.log('\nRun "shift start" to configure this project.\n');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log(`Project: ${projectConfig.project_name}`);
|
|
24
|
+
console.log(`Project ID: ${projectConfig.project_id}`);
|
|
25
|
+
// Check agents
|
|
26
|
+
if (projectConfig.agents && projectConfig.agents.length > 0) {
|
|
27
|
+
console.log(`Agents: ${projectConfig.agents.length} configured`);
|
|
28
|
+
projectConfig.agents.forEach((agent) => {
|
|
29
|
+
console.log(` - ${agent.agent_name} (${agent.agent_type})`);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// Check daemon status
|
|
33
|
+
const status = getDaemonStatus(projectRoot);
|
|
34
|
+
if (!status.running) {
|
|
35
|
+
console.log('Daemon: ❌ Not running');
|
|
36
|
+
console.log('\nRun "shift start" to start the daemon.\n');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.log(`Daemon: ✓ Running (PID: ${status.pid})`);
|
|
40
|
+
console.log(`WebSocket: ${status.connected ? '✓ Connected' : '⚠️ Disconnected'}`);
|
|
41
|
+
if (status.startedAt) {
|
|
42
|
+
const startedAt = new Date(status.startedAt);
|
|
43
|
+
console.log(`Started: ${startedAt.toLocaleString()}`);
|
|
44
|
+
}
|
|
45
|
+
console.log('');
|
|
46
|
+
}
|
|
@@ -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
|
+
}
|