@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.
- package/README.md +165 -58
- package/build/cli/commands/config.js +136 -0
- package/build/cli/commands/init.js +156 -0
- package/build/cli/commands/start.js +100 -0
- package/build/cli/commands/status.js +51 -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 +46 -126
- package/build/mcp-server.js +124 -0
- package/build/utils/api-client.js +66 -0
- package/build/utils/config.js +242 -0
- package/build/utils/prompts.js +69 -0
- package/build/utils/tree-scanner.js +148 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,103 +1,171 @@
|
|
|
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
|
-
|
|
5
|
+
1. **CLI commands** - Index and manage your project
|
|
6
|
+
2. **MCP server** - Expose tools (blast_radius, dependencies, file_summary) to Claude Desktop/Code
|
|
6
7
|
|
|
7
|
-
|
|
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`.
|
|
8
|
+
## Installation
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g @latentforce/shift
|
|
12
|
+
```
|
|
12
13
|
|
|
13
|
-
|
|
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").
|
|
14
|
+
## Quick Start
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
### Step 1: Configure backend URLs (if using hosted service)
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
If you're connecting to a hosted Shift backend (not localhost), configure the URLs first:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
|
|
21
|
+
shift config set api-url https://api.latentforce.ai
|
|
22
|
+
shift config set orch-url https://orch.latentforce.ai
|
|
23
|
+
shift config set ws-url wss://ws.latentforce.ai
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
### Step 2: Start and configure your project
|
|
27
|
+
|
|
25
28
|
```bash
|
|
26
|
-
|
|
29
|
+
cd /path/to/your/project
|
|
30
|
+
shift start
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
This will:
|
|
34
|
+
- Prompt for your Shift API key or let you continue as guest
|
|
35
|
+
- Let you select or enter your project ID
|
|
36
|
+
- Start a background daemon that connects to the backend
|
|
37
|
+
|
|
38
|
+
### Step 3: Index your project
|
|
39
|
+
|
|
30
40
|
```bash
|
|
31
|
-
|
|
41
|
+
shift init
|
|
32
42
|
```
|
|
33
43
|
|
|
44
|
+
This scans your project files and sends the structure to the backend for indexing.
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
### Step 4: Configure MCP for Claude
|
|
36
47
|
|
|
37
|
-
Claude
|
|
48
|
+
Now Claude can use the MCP tools. See [Claude Desktop setup](#claude-desktop-setup) or [Claude Code setup](#claude-code-setup) below.
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
You can use this package without installing it globally by using npx, or install it globally for faster access.
|
|
50
|
+
## CLI Commands
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
| Command | Description |
|
|
53
|
+
|---------|-------------|
|
|
54
|
+
| `shift start` | Start the daemon and configure the project |
|
|
55
|
+
| `shift init` | Scan and index project files to backend |
|
|
56
|
+
| `shift stop` | Stop the running daemon |
|
|
57
|
+
| `shift status` | Show current status (API key, project, daemon, connection) |
|
|
58
|
+
| `shift config` | Show/set configuration (URLs, API key) |
|
|
59
|
+
| `shift` or `shift mcp` | Start MCP server on stdio (used by Claude) |
|
|
45
60
|
|
|
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
|
-
```
|
|
61
|
+
## Authentication
|
|
53
62
|
|
|
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
|
-
```
|
|
63
|
+
When running `shift start` or `shift init`, you'll be prompted to choose:
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
1. **API Key** - Enter your Shift API key for full access
|
|
66
|
+
2. **Guest Mode** - Continue without an API key (limited features)
|
|
67
|
+
|
|
68
|
+
Both options work with all CLI commands. Your choice is saved to `~/.shift/config.json`.
|
|
69
|
+
|
|
70
|
+
## MCP Tools
|
|
71
|
+
|
|
72
|
+
The MCP server exposes these tools to Claude:
|
|
73
|
+
|
|
74
|
+
| Tool | Description |
|
|
75
|
+
|------|-------------|
|
|
76
|
+
| `blast_radius` | Analyze what would be affected if a file is modified |
|
|
77
|
+
| `dependencies` | Get all dependencies for a file (direct and transitive) |
|
|
78
|
+
| `file_summary` | Generate a summary of a file with optional parent context |
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
### Using the Config Command (Recommended for Global Install)
|
|
83
|
+
|
|
84
|
+
The easiest way to configure Shift after installing globally with `npm i -g @latentforce/shift`:
|
|
63
85
|
|
|
64
|
-
### Use It
|
|
65
|
-
Start a Claude Code session:
|
|
66
86
|
```bash
|
|
67
|
-
|
|
87
|
+
# Show current configuration
|
|
88
|
+
shift config
|
|
89
|
+
|
|
90
|
+
# Set backend URLs
|
|
91
|
+
shift config set api-url https://api.latentforce.ai
|
|
92
|
+
shift config set orch-url https://orch.latentforce.ai
|
|
93
|
+
shift config set ws-url wss://ws.latentforce.ai
|
|
94
|
+
|
|
95
|
+
# Set API key directly (alternative to interactive prompt)
|
|
96
|
+
shift config set api-key your-api-key-here
|
|
97
|
+
|
|
98
|
+
# Clear configuration
|
|
99
|
+
shift config clear urls # Reset URLs to defaults
|
|
100
|
+
shift config clear # Clear all config (prompts for confirmation)
|
|
68
101
|
```
|
|
69
102
|
|
|
70
|
-
|
|
103
|
+
Configuration is saved to `~/.shift/config.json`.
|
|
104
|
+
|
|
105
|
+
### Using Environment Variables
|
|
106
|
+
|
|
107
|
+
You can also set environment variables (they take priority over config file):
|
|
71
108
|
|
|
72
|
-
|
|
109
|
+
| Variable | Description | Default |
|
|
110
|
+
|----------|-------------|---------|
|
|
111
|
+
| `SHIFT_PROJECT_ID` | Default project UUID (MCP only) | - |
|
|
112
|
+
| `SHIFT_BACKEND_URL` | Backend API URL (MCP only) | `http://127.0.0.1:9000` |
|
|
73
113
|
|
|
74
|
-
|
|
114
|
+
Example using environment variables:
|
|
75
115
|
|
|
76
|
-
**macOS (Claude Desktop app):**
|
|
77
116
|
```bash
|
|
78
|
-
|
|
117
|
+
# Set for current session
|
|
118
|
+
export SHIFT_API_URL=https://api.latentforce.ai
|
|
119
|
+
export SHIFT_ORCH_URL=https://orch.latentforce.ai
|
|
120
|
+
export SHIFT_WS_URL=wss://ws.latentforce.ai
|
|
121
|
+
|
|
122
|
+
# Or inline when running commands
|
|
123
|
+
SHIFT_API_URL=https://api.latentforce.ai shift start
|
|
79
124
|
```
|
|
80
|
-
*(In Claude Desktop: Claude menu → Settings… → Developer → Edit Config)*
|
|
81
125
|
|
|
82
|
-
|
|
126
|
+
### Priority Order
|
|
127
|
+
|
|
128
|
+
URLs are resolved in this order:
|
|
129
|
+
1. Environment variables (highest priority)
|
|
130
|
+
2. Config file (`~/.shift/config.json`)
|
|
131
|
+
3. Defaults (localhost)
|
|
132
|
+
|
|
133
|
+
## Claude Code Setup
|
|
134
|
+
|
|
135
|
+
### Add MCP Server
|
|
136
|
+
|
|
83
137
|
```bash
|
|
84
|
-
|
|
138
|
+
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"}}'
|
|
85
139
|
```
|
|
86
140
|
|
|
87
|
-
|
|
141
|
+
### Verify
|
|
142
|
+
|
|
88
143
|
```bash
|
|
89
|
-
|
|
144
|
+
claude mcp list
|
|
90
145
|
```
|
|
91
146
|
|
|
92
|
-
|
|
147
|
+
## Claude Desktop Setup
|
|
148
|
+
|
|
149
|
+
### Config File Location
|
|
150
|
+
|
|
151
|
+
| OS | Path |
|
|
152
|
+
|----|------|
|
|
153
|
+
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
154
|
+
| Linux | `~/.config/claude-desktop/claude_desktop_config.json` |
|
|
155
|
+
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
156
|
+
|
|
157
|
+
### Configuration
|
|
158
|
+
|
|
159
|
+
Add to your config file:
|
|
160
|
+
|
|
93
161
|
```json
|
|
94
162
|
{
|
|
95
163
|
"mcpServers": {
|
|
96
164
|
"shift": {
|
|
97
165
|
"command": "shift",
|
|
98
166
|
"env": {
|
|
99
|
-
"SHIFT_BACKEND_URL": "
|
|
100
|
-
"SHIFT_PROJECT_ID": "
|
|
167
|
+
"SHIFT_BACKEND_URL": "https://dev-shift-lite.latentforce.ai",
|
|
168
|
+
"SHIFT_PROJECT_ID": "YOUR_PROJECT_ID_HERE"
|
|
101
169
|
}
|
|
102
170
|
}
|
|
103
171
|
}
|
|
@@ -106,16 +174,55 @@ Add this (replace `YOUR_PROJECT_UUID` with your Shift Lite project ID):
|
|
|
106
174
|
|
|
107
175
|
Restart Claude Desktop after saving.
|
|
108
176
|
|
|
177
|
+
## Project ID
|
|
178
|
+
|
|
179
|
+
You can set the project ID in two ways:
|
|
180
|
+
|
|
181
|
+
1. **Environment variable**: Set `SHIFT_PROJECT_ID` in MCP config
|
|
182
|
+
2. **Per-call override**: Pass `project_id` parameter in each tool call
|
|
183
|
+
|
|
184
|
+
This allows one MCP server to work with multiple projects.
|
|
185
|
+
|
|
109
186
|
## Development
|
|
110
187
|
|
|
111
|
-
Clone and build:
|
|
112
188
|
```bash
|
|
189
|
+
# Clone and install
|
|
113
190
|
npm install
|
|
191
|
+
|
|
192
|
+
# Build
|
|
114
193
|
npm run build
|
|
115
|
-
```
|
|
116
194
|
|
|
117
|
-
Run locally
|
|
118
|
-
```bash
|
|
195
|
+
# Run locally
|
|
119
196
|
node build/index.js
|
|
197
|
+
|
|
198
|
+
# Test CLI commands
|
|
199
|
+
node build/index.js start
|
|
200
|
+
node build/index.js init
|
|
201
|
+
node build/index.js status
|
|
202
|
+
node build/index.js stop
|
|
120
203
|
```
|
|
121
204
|
|
|
205
|
+
## How It All Works Together
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
209
|
+
│ User's Machine │
|
|
210
|
+
│ │
|
|
211
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
|
|
212
|
+
│ │ shift start │────▶│ Daemon │────▶│ Backend (WS) │ │
|
|
213
|
+
│ │ shift init │ │ (background)│ │ │ │
|
|
214
|
+
│ │ shift stop │ └─────────────┘ │ │ │
|
|
215
|
+
│ │ shift status│ │ │ │
|
|
216
|
+
│ └─────────────┘ │ Shift Lite │ │
|
|
217
|
+
│ │ Backend │ │
|
|
218
|
+
│ ┌─────────────┐ ┌─────────────┐ │ │ │
|
|
219
|
+
│ │ Claude │────▶│ shift (MCP) │────▶│ (REST API) │ │
|
|
220
|
+
│ │ Desktop/Code│ │ Server │ │ │ │
|
|
221
|
+
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
|
|
222
|
+
│ │
|
|
223
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
224
|
+
|
|
225
|
+
1. `shift start` - Starts daemon, connects to backend via WebSocket
|
|
226
|
+
2. `shift init` - Indexes project files to backend
|
|
227
|
+
3. Claude calls MCP tools - MCP server queries backend REST API
|
|
228
|
+
```
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { readGlobalConfig, setApiKey, setApiUrl, setOrchUrl, setWsUrl, clearUrls, clearApiKey, getConfiguredUrls, isGuestKey, } from '../../utils/config.js';
|
|
2
|
+
import { prompt } from '../../utils/prompts.js';
|
|
3
|
+
export async function configCommand(action, key, value) {
|
|
4
|
+
const parsedAction = (action || 'show');
|
|
5
|
+
switch (parsedAction) {
|
|
6
|
+
case 'show':
|
|
7
|
+
showConfig();
|
|
8
|
+
break;
|
|
9
|
+
case 'set':
|
|
10
|
+
await setConfigValue(key, value);
|
|
11
|
+
break;
|
|
12
|
+
case 'clear':
|
|
13
|
+
await clearConfig(key);
|
|
14
|
+
break;
|
|
15
|
+
default:
|
|
16
|
+
console.log('Usage: shift config [show|set|clear] [key] [value]');
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log('Commands:');
|
|
19
|
+
console.log(' show Show current configuration');
|
|
20
|
+
console.log(' set <key> <value> Set a configuration value');
|
|
21
|
+
console.log(' clear [key] Clear configuration (all or specific key)');
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log('Keys:');
|
|
24
|
+
console.log(' api-key Your Shift API key');
|
|
25
|
+
console.log(' api-url SHIFT_API_URL (default: http://localhost:9000)');
|
|
26
|
+
console.log(' orch-url SHIFT_ORCH_URL (default: http://localhost:9999)');
|
|
27
|
+
console.log(' ws-url SHIFT_WS_URL (default: ws://localhost:9999)');
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log('Examples:');
|
|
30
|
+
console.log(' shift config');
|
|
31
|
+
console.log(' shift config set api-url https://api.latentforce.ai');
|
|
32
|
+
console.log(' shift config set orch-url https://orch.latentforce.ai');
|
|
33
|
+
console.log(' shift config set ws-url wss://ws.latentforce.ai');
|
|
34
|
+
console.log(' shift config clear urls');
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function showConfig() {
|
|
39
|
+
console.log('╔════════════════════════════════════════════╗');
|
|
40
|
+
console.log('║ Shift Configuration ║');
|
|
41
|
+
console.log('╚════════════════════════════════════════════╝\n');
|
|
42
|
+
const globalConfig = readGlobalConfig();
|
|
43
|
+
const urls = getConfiguredUrls();
|
|
44
|
+
// API Key
|
|
45
|
+
console.log('API Key:');
|
|
46
|
+
if (globalConfig?.api_key) {
|
|
47
|
+
const keyType = isGuestKey() ? '(guest)' : '(paid)';
|
|
48
|
+
const maskedKey = globalConfig.api_key.substring(0, 8) + '...' + globalConfig.api_key.slice(-4);
|
|
49
|
+
console.log(` ${maskedKey} ${keyType}`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(' Not configured');
|
|
53
|
+
}
|
|
54
|
+
console.log('');
|
|
55
|
+
// URLs
|
|
56
|
+
console.log('URLs:');
|
|
57
|
+
console.log(` API URL: ${urls.api_url}`);
|
|
58
|
+
console.log(` Orch URL: ${urls.orch_url}`);
|
|
59
|
+
console.log(` WS URL: ${urls.ws_url}`);
|
|
60
|
+
console.log('');
|
|
61
|
+
// Source info
|
|
62
|
+
console.log('Config file: ~/.shift/config.json');
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log('Tip: URLs can also be set via environment variables:');
|
|
65
|
+
console.log(' SHIFT_API_URL, SHIFT_ORCH_URL, SHIFT_WS_URL');
|
|
66
|
+
}
|
|
67
|
+
async function setConfigValue(key, value) {
|
|
68
|
+
if (!key) {
|
|
69
|
+
console.error('Error: Key is required. Run "shift config" to see available keys.');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
// If no value provided, prompt for it
|
|
73
|
+
if (!value) {
|
|
74
|
+
value = await prompt(`Enter value for ${key}: `);
|
|
75
|
+
if (!value) {
|
|
76
|
+
console.error('Error: Value is required.');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
switch (key) {
|
|
81
|
+
case 'api-key':
|
|
82
|
+
setApiKey(value);
|
|
83
|
+
console.log('✓ API key saved');
|
|
84
|
+
break;
|
|
85
|
+
case 'api-url':
|
|
86
|
+
setApiUrl(value);
|
|
87
|
+
console.log(`✓ API URL set to: ${value}`);
|
|
88
|
+
break;
|
|
89
|
+
case 'orch-url':
|
|
90
|
+
setOrchUrl(value);
|
|
91
|
+
console.log(`✓ Orch URL set to: ${value}`);
|
|
92
|
+
break;
|
|
93
|
+
case 'ws-url':
|
|
94
|
+
setWsUrl(value);
|
|
95
|
+
console.log(`✓ WS URL set to: ${value}`);
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
console.error(`Unknown key: ${key}`);
|
|
99
|
+
console.log('Available keys: api-key, api-url, orch-url, ws-url');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
console.log('\nNote: Restart any running daemon for URL changes to take effect.');
|
|
103
|
+
}
|
|
104
|
+
async function clearConfig(key) {
|
|
105
|
+
if (!key) {
|
|
106
|
+
// Clear everything
|
|
107
|
+
const answer = await prompt('Clear all configuration? (y/N): ');
|
|
108
|
+
if (answer.toLowerCase() !== 'y') {
|
|
109
|
+
console.log('Cancelled.');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
clearApiKey();
|
|
113
|
+
clearUrls();
|
|
114
|
+
console.log('✓ All configuration cleared');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
switch (key) {
|
|
118
|
+
case 'api-key':
|
|
119
|
+
clearApiKey();
|
|
120
|
+
console.log('✓ API key cleared');
|
|
121
|
+
break;
|
|
122
|
+
case 'urls':
|
|
123
|
+
clearUrls();
|
|
124
|
+
console.log('✓ URLs cleared (will use defaults)');
|
|
125
|
+
break;
|
|
126
|
+
case 'api-url':
|
|
127
|
+
case 'orch-url':
|
|
128
|
+
case 'ws-url':
|
|
129
|
+
console.log('Use "shift config clear urls" to clear all URLs');
|
|
130
|
+
break;
|
|
131
|
+
default:
|
|
132
|
+
console.error(`Unknown key: ${key}`);
|
|
133
|
+
console.log('Available keys: api-key, urls');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { getApiKey, setApiKey, setGuestKey, isGuestKey, readProjectConfig, writeProjectConfig } from '../../utils/config.js';
|
|
4
|
+
import { promptApiKey, promptKeyChoice } from '../../utils/prompts.js';
|
|
5
|
+
import { requestGuestKey } from '../../utils/api-client.js';
|
|
6
|
+
import { getDaemonStatus } from '../../daemon/daemon-manager.js';
|
|
7
|
+
import { getProjectTree, extractAllFilePaths, categorizeFiles } from '../../utils/tree-scanner.js';
|
|
8
|
+
import { sendInitScan } from '../../utils/api-client.js';
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
export async function initCommand() {
|
|
11
|
+
const projectRoot = process.cwd();
|
|
12
|
+
console.log('╔═══════════════════════════════════════════════╗');
|
|
13
|
+
console.log('║ Initializing Shift Project ║');
|
|
14
|
+
console.log('╚═══════════════════════════════════════════════╝\n');
|
|
15
|
+
// Step 1: Check API key (with guest flow support)
|
|
16
|
+
console.log('[Init] Step 1/5: Checking API key...');
|
|
17
|
+
let apiKey = getApiKey();
|
|
18
|
+
if (!apiKey) {
|
|
19
|
+
const choice = await promptKeyChoice();
|
|
20
|
+
if (choice === 'paid') {
|
|
21
|
+
apiKey = await promptApiKey();
|
|
22
|
+
setApiKey(apiKey);
|
|
23
|
+
console.log('[Init] ✓ API key saved to ~/.shift/config.json\n');
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.log('\n[Init] Requesting guest session...');
|
|
27
|
+
try {
|
|
28
|
+
const guestResponse = await requestGuestKey();
|
|
29
|
+
setGuestKey(guestResponse.api_key);
|
|
30
|
+
apiKey = guestResponse.api_key;
|
|
31
|
+
console.log('[Init] ✓ Guest session started\n');
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error(`\n❌ Failed to get guest key: ${error.message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (isGuestKey()) {
|
|
40
|
+
console.log('[Init] ✓ Guest key found\n');
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
console.log('[Init] ✓ API key found\n');
|
|
44
|
+
}
|
|
45
|
+
// Step 2: Check project config
|
|
46
|
+
console.log('[Init] Step 2/5: Checking project configuration...');
|
|
47
|
+
const projectConfig = readProjectConfig(projectRoot);
|
|
48
|
+
if (!projectConfig) {
|
|
49
|
+
console.error('\n❌ No project configured for this directory.');
|
|
50
|
+
console.log('Run "shift start" first to configure the project.\n');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
console.log(`[Init] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
|
|
54
|
+
// Step 3: Check daemon status (warn if not running)
|
|
55
|
+
console.log('[Init] Step 3/5: Checking daemon status...');
|
|
56
|
+
const status = getDaemonStatus(projectRoot);
|
|
57
|
+
if (!status.running) {
|
|
58
|
+
console.log('[Init] ⚠️ Warning: Daemon is not running. Run "shift start" to start it.\n');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log(`[Init] ✓ Daemon running (PID: ${status.pid}, Connected: ${status.connected})\n`);
|
|
62
|
+
}
|
|
63
|
+
// Step 4: Scan project structure (matching extension's Step 6)
|
|
64
|
+
console.log('[Init] Step 4/5: Scanning project structure...');
|
|
65
|
+
// Get project tree (matching extension's getProjectTree)
|
|
66
|
+
const treeData = getProjectTree(projectRoot, {
|
|
67
|
+
depth: 0, // Unlimited depth
|
|
68
|
+
exclude_patterns: [
|
|
69
|
+
'.git',
|
|
70
|
+
'node_modules',
|
|
71
|
+
'__pycache__',
|
|
72
|
+
'.vscode',
|
|
73
|
+
'dist',
|
|
74
|
+
'build',
|
|
75
|
+
'.shift',
|
|
76
|
+
'.next',
|
|
77
|
+
'coverage',
|
|
78
|
+
'venv',
|
|
79
|
+
'env',
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
console.log(`[Init] Files: ${treeData.file_count}`);
|
|
83
|
+
console.log(`[Init] Directories: ${treeData.dir_count}`);
|
|
84
|
+
console.log(`[Init] Total size: ${treeData.total_size_mb} MB\n`);
|
|
85
|
+
// Get git info (matching extension)
|
|
86
|
+
let gitInfo = {
|
|
87
|
+
current_branch: '',
|
|
88
|
+
original_branch: 'shift_original',
|
|
89
|
+
migrate_branch: 'shift_migrated',
|
|
90
|
+
has_uncommitted_changes: false,
|
|
91
|
+
};
|
|
92
|
+
try {
|
|
93
|
+
const { stdout: currentBranch } = await execAsync('git branch --show-current', { cwd: projectRoot });
|
|
94
|
+
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: projectRoot });
|
|
95
|
+
gitInfo.current_branch = currentBranch.trim();
|
|
96
|
+
gitInfo.has_uncommitted_changes = statusOutput.trim().length > 0;
|
|
97
|
+
console.log(`[Init] ✓ Git info: branch=${gitInfo.current_branch}, uncommitted=${gitInfo.has_uncommitted_changes}\n`);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
console.log('[Init] ⚠️ Not a git repository or git not available\n');
|
|
101
|
+
}
|
|
102
|
+
// Extract file paths and categorize (matching extension)
|
|
103
|
+
const allFiles = extractAllFilePaths(treeData.tree);
|
|
104
|
+
const categorized = categorizeFiles(treeData.tree);
|
|
105
|
+
console.log(`[Init] Source files: ${categorized.source_files.length}`);
|
|
106
|
+
console.log(`[Init] Config files: ${categorized.config_files.length}`);
|
|
107
|
+
console.log(`[Init] Asset files: ${categorized.asset_files.length}\n`);
|
|
108
|
+
// Step 5: Send scan to backend (matching extension's Step 9)
|
|
109
|
+
console.log('[Init] Step 5/5: Sending scan to backend...');
|
|
110
|
+
const payload = {
|
|
111
|
+
project_id: projectConfig.project_id,
|
|
112
|
+
project_tree: treeData,
|
|
113
|
+
git_info: gitInfo,
|
|
114
|
+
file_manifest: {
|
|
115
|
+
all_files: allFiles,
|
|
116
|
+
categorized: categorized,
|
|
117
|
+
},
|
|
118
|
+
metadata: {
|
|
119
|
+
extension_version: '1.0.2-cli', // CLI version
|
|
120
|
+
scan_timestamp: new Date().toISOString(),
|
|
121
|
+
project_name: projectConfig.project_name,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
try {
|
|
125
|
+
const response = await sendInitScan(apiKey, projectConfig.project_id, payload);
|
|
126
|
+
console.log('[Init] ✓ Backend initialization completed');
|
|
127
|
+
console.log(`[Init] Files read: ${response.files_read}`);
|
|
128
|
+
console.log(`[Init] Files failed: ${response.files_failed}`);
|
|
129
|
+
// Update local config with agent info (matching extension)
|
|
130
|
+
if (response.agents_created?.theme_planner) {
|
|
131
|
+
const agentInfo = {
|
|
132
|
+
agent_id: response.agents_created.theme_planner.agent_id,
|
|
133
|
+
agent_name: response.agents_created.theme_planner.agent_name,
|
|
134
|
+
agent_type: 'theme_planner',
|
|
135
|
+
created_at: new Date().toISOString(),
|
|
136
|
+
};
|
|
137
|
+
projectConfig.agents = projectConfig.agents || [];
|
|
138
|
+
projectConfig.agents.push(agentInfo);
|
|
139
|
+
writeProjectConfig(projectConfig, projectRoot);
|
|
140
|
+
console.log(`[Init] ✓ Agent info saved: ${agentInfo.agent_id}`);
|
|
141
|
+
}
|
|
142
|
+
console.log('\n╔═══════════════════════════════════════════════╗');
|
|
143
|
+
console.log('║ ✓ Project Initialization Complete ║');
|
|
144
|
+
console.log('╚═══════════════════════════════════════════════╝');
|
|
145
|
+
if (response.next_steps) {
|
|
146
|
+
console.log('\nNext steps:');
|
|
147
|
+
response.next_steps.forEach((step) => console.log(` - ${step}`));
|
|
148
|
+
}
|
|
149
|
+
console.log('\nFile indexing has been triggered. This may take a few moments.');
|
|
150
|
+
console.log('Use "shift status" to check the daemon connection.\n');
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
console.error(`\n❌ Failed to initialize project: ${error.message}`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { getApiKey, setApiKey, setGuestKey, isGuestKey, setProject, readProjectConfig } from '../../utils/config.js';
|
|
2
|
+
import { promptApiKey, promptProjectId, promptSelectProject, promptKeyChoice } from '../../utils/prompts.js';
|
|
3
|
+
import { startDaemon, getDaemonStatus } from '../../daemon/daemon-manager.js';
|
|
4
|
+
import { fetchProjects, requestGuestKey } 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
|
+
const choice = await promptKeyChoice();
|
|
15
|
+
if (choice === 'paid') {
|
|
16
|
+
apiKey = await promptApiKey();
|
|
17
|
+
setApiKey(apiKey);
|
|
18
|
+
console.log('[Start] ✓ API key saved to ~/.shift/config.json\n');
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
console.log('\n[Start] Requesting guest session...');
|
|
22
|
+
try {
|
|
23
|
+
const guestResponse = await requestGuestKey();
|
|
24
|
+
setGuestKey(guestResponse.api_key);
|
|
25
|
+
apiKey = guestResponse.api_key;
|
|
26
|
+
console.log('[Start] ✓ Guest session started\n');
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error(`\n❌ Failed to get guest key: ${error.message}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (isGuestKey()) {
|
|
35
|
+
console.log('[Start] ✓ Guest key found\n');
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.log('[Start] ✓ API key found\n');
|
|
39
|
+
}
|
|
40
|
+
// Step 2: Check/create project config
|
|
41
|
+
console.log('[Start] Step 2/4: Checking project configuration...');
|
|
42
|
+
let projectConfig = readProjectConfig(projectRoot);
|
|
43
|
+
if (!projectConfig) {
|
|
44
|
+
// Fetch available projects
|
|
45
|
+
console.log('[Start] Fetching your projects...');
|
|
46
|
+
try {
|
|
47
|
+
const projects = await fetchProjects(apiKey);
|
|
48
|
+
if (!projects || projects.length === 0) {
|
|
49
|
+
console.error('\n❌ No projects found. Create one in the Shift dashboard first.');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
console.log(`[Start] Found ${projects.length} project(s)\n`);
|
|
53
|
+
// Let user select a project
|
|
54
|
+
const selected = await promptSelectProject(projects);
|
|
55
|
+
// Save project config (matching extension's .shift/config.json structure)
|
|
56
|
+
setProject(selected.project_id, selected.project_name, projectRoot);
|
|
57
|
+
console.log(`[Start] ✓ Project "${selected.project_name}" saved to .shift/config.json\n`);
|
|
58
|
+
projectConfig = readProjectConfig(projectRoot);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// If fetching fails, fall back to manual entry
|
|
62
|
+
console.error(`\n⚠️ Could not fetch projects: ${error.message}`);
|
|
63
|
+
console.log('Falling back to manual project ID entry...\n');
|
|
64
|
+
const projectId = await promptProjectId();
|
|
65
|
+
setProject(projectId, 'Unknown Project', projectRoot);
|
|
66
|
+
console.log(`[Start] ✓ Project ID saved to .shift/config.json\n`);
|
|
67
|
+
projectConfig = readProjectConfig(projectRoot);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.log(`[Start] ✓ Project found: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
|
|
72
|
+
}
|
|
73
|
+
if (!projectConfig) {
|
|
74
|
+
console.error('❌ Failed to configure project');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
// Step 3: Check if daemon is already running
|
|
78
|
+
console.log('[Start] Step 3/4: Checking daemon status...');
|
|
79
|
+
const status = getDaemonStatus(projectRoot);
|
|
80
|
+
if (status.running) {
|
|
81
|
+
console.log(`[Start] ✓ Daemon is already running (PID: ${status.pid})`);
|
|
82
|
+
console.log(`[Start] WebSocket: ${status.connected ? 'Connected' : 'Connecting...'}\n`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log('[Start] Daemon not running\n');
|
|
86
|
+
// Step 4: Start daemon
|
|
87
|
+
console.log('[Start] Step 4/4: Starting daemon...');
|
|
88
|
+
const result = await startDaemon(projectRoot, projectConfig.project_id, apiKey);
|
|
89
|
+
if (!result.success) {
|
|
90
|
+
console.error(`\n❌ Failed to start daemon: ${result.error}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
console.log(`[Start] ✓ Daemon started (PID: ${result.pid})`);
|
|
94
|
+
console.log('\n╔════════════════════════════════════════════╗');
|
|
95
|
+
console.log('║ Shift is now running! ║');
|
|
96
|
+
console.log('╚════════════════════════════════════════════╝');
|
|
97
|
+
console.log('\nUse "shift status" to check connection status.');
|
|
98
|
+
console.log('Use "shift init" to scan and index the project.');
|
|
99
|
+
console.log('Use "shift stop" to stop the daemon.\n');
|
|
100
|
+
}
|