@latentforce/shift 1.0.10 → 1.0.12
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 +182 -123
- package/build/cli/commands/add.js +209 -0
- package/build/cli/commands/init.js +46 -16
- package/build/cli/commands/start.js +3 -6
- package/build/cli/commands/update-drg.js +91 -85
- package/build/index.js +25 -11
- package/build/utils/api-client.js +21 -12
- package/build/utils/config.js +19 -1
- package/build/utils/tree-scanner.js +18 -10
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -10,65 +10,45 @@ Shift indexes your codebase, builds a dependency relationship graph (DRG), and p
|
|
|
10
10
|
npm install -g @latentforce/shift
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## Getting an API Key
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
1. Go to **https://dev-shift-lite.latentforce.ai/auth**
|
|
16
|
+
2. **Sign up** for an account
|
|
17
|
+
3. **Sign in** to your dashboard
|
|
18
|
+
4. Copy your **API key** from the dashboard
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
From the dashboard you can also **create a project** and **select a migration template** — or you can do both directly from the CLI (see below).
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
cd /path/to/your/project
|
|
21
|
-
shift-cli init --guest # Auto-creates guest key + project, scans, and indexes
|
|
22
|
-
```
|
|
22
|
+
## Quick Start
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
All commands **must be run** from your project's root directory:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
|
|
28
|
-
shift-cli init --api-key YOUR_KEY --project-name "My App"
|
|
29
|
-
|
|
30
|
-
# Fully non-interactive (CI/CD friendly)
|
|
31
|
-
shift-cli init --api-key YOUR_KEY --project-name "My App" --template TEMPLATE_ID
|
|
27
|
+
cd /path/to/your/project
|
|
32
28
|
```
|
|
33
29
|
|
|
34
|
-
### Interactive (
|
|
30
|
+
### Option A: Interactive (recommended)
|
|
35
31
|
|
|
36
32
|
```bash
|
|
37
|
-
shift-cli start # Configure API key and project
|
|
38
|
-
shift-cli init #
|
|
33
|
+
shift-cli start # Step 1: Configure API key and project, launch daemon
|
|
34
|
+
shift-cli init # Step 2: Scan and index project files
|
|
35
|
+
shift-cli status # Step 3: Verify everything is connected
|
|
39
36
|
```
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
claude mcp add-json shift '{"type":"stdio","command":"shift-cli","args":["mcp"],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID"}}'
|
|
47
|
-
```
|
|
38
|
+
When you run `shift-cli start` interactively, it will:
|
|
39
|
+
1. **Ask for your API key** — paste the key from your dashboard (or choose guest mode)
|
|
40
|
+
2. **Ask to create or select a project** — you can either:
|
|
41
|
+
- **Select an existing project** from your account (if you created one on the dashboard)
|
|
42
|
+
- **Create a new project** from the CLI — it will prompt for a name (defaults to your directory name) and let you select a migration template
|
|
48
43
|
|
|
49
|
-
|
|
44
|
+
### Option B: Non-interactive (CI/CD friendly)
|
|
50
45
|
|
|
51
46
|
```bash
|
|
52
|
-
|
|
47
|
+
shift-cli init --api-key YOUR_KEY --project-name "My App"
|
|
53
48
|
```
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Windows, `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
50
|
+
If a project named "My App" already exists in your account, it will be reused. Otherwise a new one is created with the specified template.
|
|
58
51
|
|
|
59
|
-
```json
|
|
60
|
-
{
|
|
61
|
-
"mcpServers": {
|
|
62
|
-
"shift": {
|
|
63
|
-
"command": "shift-cli",
|
|
64
|
-
"args": ["mcp"],
|
|
65
|
-
"env": {
|
|
66
|
-
"SHIFT_PROJECT_ID": "YOUR_PROJECT_ID"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
52
|
|
|
73
53
|
## CLI Commands
|
|
74
54
|
|
|
@@ -76,37 +56,44 @@ Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Window
|
|
|
76
56
|
|---------|-------------|
|
|
77
57
|
| `shift-cli start` | Start the daemon and configure the project |
|
|
78
58
|
| `shift-cli init` | Scan and index project files |
|
|
59
|
+
| `shift-cli status` | Show current status |
|
|
79
60
|
| `shift-cli update-drg` | Update the dependency relationship graph |
|
|
80
61
|
| `shift-cli stop` | Stop the daemon |
|
|
81
|
-
| `shift-cli
|
|
62
|
+
| `shift-cli add <tool>` | Add Shift MCP server to an AI coding tool |
|
|
82
63
|
| `shift-cli config` | Manage configuration |
|
|
83
64
|
|
|
84
|
-
### `shift-cli
|
|
65
|
+
### `shift-cli start`
|
|
85
66
|
|
|
86
|
-
|
|
67
|
+
Resolves authentication, configures the project, and launches a background daemon that maintains a WebSocket connection to the Shift backend.
|
|
87
68
|
|
|
88
|
-
|
|
69
|
+
**When run interactively (no flags):**
|
|
70
|
+
1. Prompts you to enter your API key or choose guest mode
|
|
71
|
+
2. Prompts you to **select an existing project** or **create a new one**
|
|
72
|
+
- If creating: asks for a project name and lets you pick a migration template
|
|
73
|
+
3. Saves config to `.shift/config.json`
|
|
74
|
+
4. Launches the background daemon
|
|
75
|
+
|
|
76
|
+
If you already created a project on the dashboard (https://dev-shift-lite.latentforce.ai), you can select it from the list. Otherwise, create one directly from the CLI.
|
|
89
77
|
|
|
90
78
|
| Flag | Description |
|
|
91
79
|
|------|-------------|
|
|
92
80
|
| `--guest` | Use guest authentication (auto-creates a temporary project) |
|
|
93
|
-
| `--api-key <key>` | Provide API key directly |
|
|
94
|
-
| `--project-name <name>` | Create new project or match existing by name |
|
|
95
|
-
| `--project-id <id>` | Use existing project UUID |
|
|
81
|
+
| `--api-key <key>` | Provide API key directly (skips the key prompt) |
|
|
82
|
+
| `--project-name <name>` | Create new project or match existing by name (skips project prompt) |
|
|
83
|
+
| `--project-id <id>` | Use existing project UUID (skips project prompt) |
|
|
96
84
|
| `--template <id>` | Migration template ID for project creation |
|
|
97
|
-
| `-f, --force` | Force re-indexing even if already indexed |
|
|
98
|
-
| `--no-ignore` | Skip `.shiftignore` rules for this run (send all files) |
|
|
99
85
|
|
|
100
86
|
```bash
|
|
101
|
-
shift-cli
|
|
102
|
-
shift-cli
|
|
103
|
-
shift-cli
|
|
104
|
-
shift-cli init --guest # Quick init with guest auth
|
|
87
|
+
shift-cli start # Interactive setup (prompts for key + project)
|
|
88
|
+
shift-cli start --guest # Quick start without API key
|
|
89
|
+
shift-cli start --api-key <key> --project-name "My App" # Non-interactive
|
|
105
90
|
```
|
|
106
91
|
|
|
107
|
-
### `shift-cli
|
|
92
|
+
### `shift-cli init`
|
|
93
|
+
|
|
94
|
+
Performs a full project scan — collects the file tree, categorizes files (source, config, assets), gathers git info, and sends everything to the Shift backend for indexing. Automatically starts the daemon if not running.
|
|
108
95
|
|
|
109
|
-
|
|
96
|
+
If the project is already indexed, you will be prompted to re-index. Use `--force` to skip the prompt.
|
|
110
97
|
|
|
111
98
|
| Flag | Description |
|
|
112
99
|
|------|-------------|
|
|
@@ -115,30 +102,53 @@ Resolves authentication, configures the project, and launches a background daemo
|
|
|
115
102
|
| `--project-name <name>` | Create new project or match existing by name |
|
|
116
103
|
| `--project-id <id>` | Use existing project UUID |
|
|
117
104
|
| `--template <id>` | Migration template ID for project creation |
|
|
105
|
+
| `-f, --force` | Force re-indexing even if already indexed |
|
|
118
106
|
|
|
119
107
|
```bash
|
|
120
|
-
shift-cli
|
|
121
|
-
shift-cli
|
|
122
|
-
|
|
108
|
+
shift-cli init # Interactive initialization
|
|
109
|
+
shift-cli init --force # Force re-index without prompt
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### `shift-cli status`
|
|
113
|
+
|
|
114
|
+
Displays comprehensive information about the current Shift setup — API key status, project details, backend indexing state, and daemon health.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
shift-cli status
|
|
123
118
|
```
|
|
124
119
|
|
|
125
120
|
### `shift-cli update-drg`
|
|
126
121
|
|
|
127
|
-
Scans
|
|
122
|
+
Scans source files across all supported languages, detects git changes, and sends file contents to the backend for dependency analysis.
|
|
123
|
+
|
|
124
|
+
**Supported languages and extensions:**
|
|
125
|
+
|
|
126
|
+
| Language | Extensions |
|
|
127
|
+
|----------|------------|
|
|
128
|
+
| JavaScript / TypeScript | `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs` |
|
|
129
|
+
| Python | `.py` |
|
|
130
|
+
| C# | `.cs` |
|
|
131
|
+
| C/C++ | `.cpp`, `.cc`, `.cxx`, `.h`, `.hpp`, `.c` |
|
|
128
132
|
|
|
129
133
|
| Flag | Description |
|
|
130
134
|
|------|-------------|
|
|
131
135
|
| `-m, --mode <mode>` | `baseline` (all files) or `incremental` (git-changed only, default) |
|
|
132
|
-
| `--no-ignore` | Skip `.shiftignore` rules for this run (send all files) |
|
|
133
136
|
|
|
134
137
|
**Modes:**
|
|
135
138
|
- **incremental** (default) — sends only git-changed files for faster updates
|
|
136
|
-
- **baseline** — sends all
|
|
139
|
+
- **baseline** — sends all source files for a full re-analysis
|
|
137
140
|
|
|
138
141
|
```bash
|
|
139
142
|
shift-cli update-drg # Incremental update (default)
|
|
140
143
|
shift-cli update-drg -m baseline # Full re-analysis
|
|
141
|
-
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### `shift-cli stop`
|
|
147
|
+
|
|
148
|
+
Stops the background daemon process.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
shift-cli stop
|
|
142
152
|
```
|
|
143
153
|
|
|
144
154
|
### `shift-cli config`
|
|
@@ -151,92 +161,141 @@ Manage Shift configuration (URLs, API key).
|
|
|
151
161
|
| `set <key> <value>` | Set a configuration value |
|
|
152
162
|
| `clear [key]` | Clear a specific key or all configuration |
|
|
153
163
|
|
|
154
|
-
**Configurable
|
|
155
|
-
|
|
156
|
-
URLs can also be set via environment variables: `SHIFT_API_URL`, `SHIFT_ORCH_URL`, `SHIFT_WS_URL`.
|
|
164
|
+
**Configurable key:** `api-key`
|
|
157
165
|
|
|
158
166
|
```bash
|
|
159
167
|
shift-cli config # Show config
|
|
160
168
|
shift-cli config set api-key sk-abc123 # Set API key
|
|
161
|
-
shift-cli config set api-url https://api.shift.ai # Set API URL
|
|
162
169
|
shift-cli config clear api-key # Clear API key
|
|
163
170
|
shift-cli config clear # Clear all config
|
|
164
171
|
```
|
|
172
|
+
## MCP Integration
|
|
173
|
+
|
|
174
|
+
After initializing your project, use `shift-cli add` to configure Shift's MCP server in your AI coding tool:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
shift-cli add <tool>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Supported tools
|
|
181
|
+
|
|
182
|
+
| Tool | Command | Config method |
|
|
183
|
+
|------|---------|---------------|
|
|
184
|
+
| Claude Code | `shift-cli add claude-code` | Runs `claude mcp add-json` |
|
|
185
|
+
| Opencode | `shift-cli add opencode` | Writes `opencode.json` |
|
|
186
|
+
| Codex | `shift-cli add codex` | Runs `codex mcp add` |
|
|
187
|
+
| GitHub Copilot | `shift-cli add copilot` | Writes `.vscode/mcp.json` |
|
|
188
|
+
| Factory Droid | `shift-cli add droid` | Runs `droid mcp add` |
|
|
189
|
+
|
|
190
|
+
For CLI-based tools, if the CLI is not installed, the command will print manual setup instructions.
|
|
191
|
+
|
|
192
|
+
### Claude Desktop (manual)
|
|
165
193
|
|
|
166
|
-
|
|
194
|
+
Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Windows, `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"mcpServers": {
|
|
199
|
+
"shift": {
|
|
200
|
+
"command": "shift-cli",
|
|
201
|
+
"args": ["mcp"],
|
|
202
|
+
"env": {
|
|
203
|
+
"SHIFT_PROJECT_ID": "YOUR_PROJECT_ID"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
## `.shift/scan_target.json`
|
|
167
210
|
|
|
168
|
-
Shift uses a
|
|
211
|
+
Shift uses a `scan_target.json` file inside the `.shift/` directory to let you specify which parts of your codebase to scan and in which language. This is especially useful for monorepos or projects with multiple languages.
|
|
169
212
|
|
|
170
|
-
A default
|
|
213
|
+
A default template is **automatically created** the first time you run `shift-cli init` or `shift-cli start`. By default, it is unconfigured and the server will auto-detect scan targets. Edit the file to manually specify targets.
|
|
171
214
|
|
|
172
|
-
###
|
|
215
|
+
### Valid Languages
|
|
173
216
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
- Trailing `/` matches directories only
|
|
178
|
-
- Leading `/` anchors to the project root
|
|
179
|
-
- `!` negates a pattern (re-includes a previously ignored path)
|
|
217
|
+
```
|
|
218
|
+
javascript, typescript, python, cpp, csharp
|
|
219
|
+
```
|
|
180
220
|
|
|
181
|
-
###
|
|
221
|
+
### Format
|
|
182
222
|
|
|
183
|
-
The
|
|
223
|
+
The file is a JSON array of objects, each with:
|
|
184
224
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
bower_components/
|
|
225
|
+
| Field | Type | Description |
|
|
226
|
+
|-------|------|-------------|
|
|
227
|
+
| `language` | `string \| null` | One of the valid languages above, or `null` for auto-detect |
|
|
228
|
+
| `path` | `string` | Relative path from project root (empty string `""` for root) |
|
|
190
229
|
|
|
191
|
-
|
|
192
|
-
dist/
|
|
193
|
-
build/
|
|
194
|
-
out/
|
|
195
|
-
.next/
|
|
230
|
+
### Default Template (auto-generated)
|
|
196
231
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
232
|
+
```json
|
|
233
|
+
[
|
|
234
|
+
{
|
|
235
|
+
"language": null,
|
|
236
|
+
"path": ""
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
```
|
|
200
240
|
|
|
201
|
-
|
|
202
|
-
.env
|
|
203
|
-
.env.*
|
|
204
|
-
*.pem
|
|
205
|
-
*.key
|
|
241
|
+
When left as-is (single entry with `language: null` and `path: ""`), the server auto-detects scan targets.
|
|
206
242
|
|
|
207
|
-
|
|
208
|
-
*.log
|
|
209
|
-
logs/
|
|
243
|
+
### Examples
|
|
210
244
|
|
|
211
|
-
|
|
212
|
-
.DS_Store
|
|
213
|
-
Thumbs.db
|
|
245
|
+
**Single-language project (TypeScript at root):**
|
|
214
246
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
247
|
+
```json
|
|
248
|
+
[
|
|
249
|
+
{
|
|
250
|
+
"language": "typescript",
|
|
251
|
+
"path": ""
|
|
252
|
+
}
|
|
253
|
+
]
|
|
254
|
+
```
|
|
220
255
|
|
|
221
|
-
|
|
222
|
-
__pycache__/
|
|
223
|
-
*.pyc
|
|
224
|
-
venv/
|
|
225
|
-
.venv/
|
|
256
|
+
**Monorepo with multiple languages:**
|
|
226
257
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
258
|
+
```json
|
|
259
|
+
[
|
|
260
|
+
{
|
|
261
|
+
"language": "typescript",
|
|
262
|
+
"path": "frontend"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"language": "python",
|
|
266
|
+
"path": "backend"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"language": "csharp",
|
|
270
|
+
"path": "services/auth"
|
|
271
|
+
}
|
|
272
|
+
]
|
|
231
273
|
```
|
|
232
274
|
|
|
233
|
-
|
|
275
|
+
**C++ project in a subdirectory:**
|
|
234
276
|
|
|
235
|
-
|
|
277
|
+
```json
|
|
278
|
+
[
|
|
279
|
+
{
|
|
280
|
+
"language": "cpp",
|
|
281
|
+
"path": "engine/src"
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
```
|
|
236
285
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
286
|
+
**Mixed JavaScript and Python at root:**
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
[
|
|
290
|
+
{
|
|
291
|
+
"language": "javascript",
|
|
292
|
+
"path": ""
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"language": "python",
|
|
296
|
+
"path": ""
|
|
297
|
+
}
|
|
298
|
+
]
|
|
240
299
|
```
|
|
241
300
|
|
|
242
301
|
## MCP Tools
|
|
@@ -255,4 +314,4 @@ Each tool accepts an optional `project_id` parameter. If not provided, it falls
|
|
|
255
314
|
|------|----------|-------------|
|
|
256
315
|
| Global config | `~/.shift/config.json` | API key and server URLs |
|
|
257
316
|
| Project config | `.shift/config.json` | Project ID, name, and agent info |
|
|
258
|
-
|
|
|
317
|
+
| Scan targets | `.shift/scan_target.json` | Language and path targets for scanning (auto-created) |
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { getProjectId } from '../../utils/config.js';
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
const SUPPORTED_TOOLS = ['claude-code', 'opencode', 'codex', 'copilot', 'droid'];
|
|
8
|
+
function isNotFound(e) {
|
|
9
|
+
return e.code === 'ENOENT' || /ENOENT|not found|not recognized/.test(e.message);
|
|
10
|
+
}
|
|
11
|
+
function getStderr(e) {
|
|
12
|
+
return (e.stderr || e.message || '').trim();
|
|
13
|
+
}
|
|
14
|
+
function shellEscape(arg) {
|
|
15
|
+
if (/^[a-zA-Z0-9._\-=/]+$/.test(arg))
|
|
16
|
+
return arg;
|
|
17
|
+
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
18
|
+
}
|
|
19
|
+
function buildCommand(bin, args) {
|
|
20
|
+
return [bin, ...args.map(shellEscape)].join(' ');
|
|
21
|
+
}
|
|
22
|
+
function getMcpConfig(projectId) {
|
|
23
|
+
return {
|
|
24
|
+
command: 'shift-cli',
|
|
25
|
+
args: ['mcp'],
|
|
26
|
+
env: { SHIFT_PROJECT_ID: projectId },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function addClaudeCode(projectId) {
|
|
30
|
+
const jsonPayload = JSON.stringify({
|
|
31
|
+
type: 'stdio',
|
|
32
|
+
command: 'shift-cli',
|
|
33
|
+
args: ['mcp'],
|
|
34
|
+
env: { SHIFT_PROJECT_ID: projectId },
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
await execAsync(buildCommand('claude', ['mcp', 'add-json', 'shift', jsonPayload]));
|
|
38
|
+
console.log(' Added Shift MCP server to Claude Code.');
|
|
39
|
+
console.log('\n Verify with: claude mcp list');
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
const err = e;
|
|
43
|
+
const stderr = getStderr(err);
|
|
44
|
+
if (/already exists/.test(stderr)) {
|
|
45
|
+
console.log(' Shift MCP server is already configured in Claude Code.');
|
|
46
|
+
console.log(' To update, remove it first: claude mcp remove shift');
|
|
47
|
+
}
|
|
48
|
+
else if (isNotFound(err)) {
|
|
49
|
+
console.log(' "claude" CLI not found. Add manually:\n');
|
|
50
|
+
console.log(` claude mcp add-json shift '${jsonPayload}'`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log(` Failed: ${stderr}`);
|
|
54
|
+
console.log('\n Try adding manually:\n');
|
|
55
|
+
console.log(` claude mcp add-json shift '${jsonPayload}'`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function addCodex(projectId) {
|
|
60
|
+
const config = getMcpConfig(projectId);
|
|
61
|
+
const args = ['mcp', 'add', 'shift'];
|
|
62
|
+
for (const [k, v] of Object.entries(config.env)) {
|
|
63
|
+
args.push('--env', `${k}=${v}`);
|
|
64
|
+
}
|
|
65
|
+
args.push('--', config.command, ...config.args);
|
|
66
|
+
const manualCmd = `codex ${args.join(' ')}`;
|
|
67
|
+
try {
|
|
68
|
+
await execAsync(buildCommand('codex', args));
|
|
69
|
+
console.log(' Added Shift MCP server to Codex.');
|
|
70
|
+
console.log('\n Verify with: codex mcp list');
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
const err = e;
|
|
74
|
+
const stderr = getStderr(err);
|
|
75
|
+
if (isNotFound(err)) {
|
|
76
|
+
console.log(' "codex" CLI not found. Make sure Codex CLI is installed and on your PATH.\n');
|
|
77
|
+
console.log(' Once installed, add manually:\n');
|
|
78
|
+
}
|
|
79
|
+
else if (/already exists/.test(stderr)) {
|
|
80
|
+
console.log(' Shift MCP server is already configured in Codex.');
|
|
81
|
+
console.log(' To update, remove it first: codex mcp remove shift');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.log(` Failed: ${stderr}`);
|
|
86
|
+
console.log('\n Try adding manually:\n');
|
|
87
|
+
}
|
|
88
|
+
console.log(` ${manualCmd}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function addFactoryDroid(projectId) {
|
|
92
|
+
const config = getMcpConfig(projectId);
|
|
93
|
+
const args = ['mcp', 'add', 'shift', config.command];
|
|
94
|
+
for (const [k, v] of Object.entries(config.env)) {
|
|
95
|
+
args.push('--env', `${k}=${v}`);
|
|
96
|
+
}
|
|
97
|
+
const manualCmd = `droid ${args.join(' ')}`;
|
|
98
|
+
try {
|
|
99
|
+
await execAsync(buildCommand('droid', args));
|
|
100
|
+
console.log(' Added Shift MCP server to Factory Droid.');
|
|
101
|
+
console.log('\n Verify: type /mcp within droid to see configured servers');
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
const err = e;
|
|
105
|
+
console.log(` Failed: ${getStderr(err)}`);
|
|
106
|
+
console.log('\n Try adding manually:\n');
|
|
107
|
+
console.log(` ${manualCmd}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function mergeJsonConfig(filePath, serverKey, serverValue) {
|
|
111
|
+
let config = {};
|
|
112
|
+
if (fs.existsSync(filePath)) {
|
|
113
|
+
try {
|
|
114
|
+
config = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
console.log(` Warning: Could not parse existing ${filePath}, creating new file.`);
|
|
118
|
+
config = {};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Navigate/create nested keys
|
|
122
|
+
let obj = config;
|
|
123
|
+
for (let i = 0; i < serverKey.length - 1; i++) {
|
|
124
|
+
const key = serverKey[i];
|
|
125
|
+
if (typeof obj[key] !== 'object' || obj[key] === null) {
|
|
126
|
+
obj[key] = {};
|
|
127
|
+
}
|
|
128
|
+
obj = obj[key];
|
|
129
|
+
}
|
|
130
|
+
const finalKey = serverKey[serverKey.length - 1];
|
|
131
|
+
obj[finalKey] = serverValue;
|
|
132
|
+
// Ensure parent directory exists
|
|
133
|
+
const dir = path.dirname(filePath);
|
|
134
|
+
if (!fs.existsSync(dir)) {
|
|
135
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
|
|
138
|
+
}
|
|
139
|
+
function addOpencode(projectId, projectRoot) {
|
|
140
|
+
const filePath = path.join(projectRoot, 'opencode.json');
|
|
141
|
+
let existing = {};
|
|
142
|
+
if (fs.existsSync(filePath)) {
|
|
143
|
+
try {
|
|
144
|
+
existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
145
|
+
}
|
|
146
|
+
catch { /* will be recreated */ }
|
|
147
|
+
}
|
|
148
|
+
if (!existing['$schema']) {
|
|
149
|
+
existing['$schema'] = 'https://opencode.ai/config.json';
|
|
150
|
+
const dir = path.dirname(filePath);
|
|
151
|
+
if (!fs.existsSync(dir))
|
|
152
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
153
|
+
fs.writeFileSync(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
154
|
+
}
|
|
155
|
+
mergeJsonConfig(filePath, ['mcp', 'shift'], {
|
|
156
|
+
type: 'local',
|
|
157
|
+
command: ["shift-cli", "mcp"],
|
|
158
|
+
enabled: true,
|
|
159
|
+
environment: { "SHIFT_PROJECT_ID": projectId },
|
|
160
|
+
});
|
|
161
|
+
console.log(` Updated ${filePath}`);
|
|
162
|
+
console.log('\n Verify: check "mcp.shift" in opencode.json');
|
|
163
|
+
}
|
|
164
|
+
function addCopilot(projectId, projectRoot) {
|
|
165
|
+
const config = getMcpConfig(projectId);
|
|
166
|
+
const filePath = path.join(projectRoot, '.vscode', 'mcp.json');
|
|
167
|
+
mergeJsonConfig(filePath, ['servers', 'shift'], {
|
|
168
|
+
command: config.command,
|
|
169
|
+
args: config.args,
|
|
170
|
+
env: config.env,
|
|
171
|
+
});
|
|
172
|
+
console.log(` Updated ${filePath}`);
|
|
173
|
+
console.log('\n Verify: check "servers.shift" in .vscode/mcp.json');
|
|
174
|
+
}
|
|
175
|
+
export async function addCommand(tool) {
|
|
176
|
+
if (!SUPPORTED_TOOLS.includes(tool)) {
|
|
177
|
+
console.error(`\n Unknown tool: "${tool}"\n`);
|
|
178
|
+
console.error(' Supported tools:');
|
|
179
|
+
for (const t of SUPPORTED_TOOLS) {
|
|
180
|
+
console.error(` - ${t}`);
|
|
181
|
+
}
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
const projectRoot = process.cwd();
|
|
185
|
+
const projectId = getProjectId(projectRoot);
|
|
186
|
+
if (!projectId) {
|
|
187
|
+
console.error('\n No project configured. Run "shift-cli init" first to set up your project.\n');
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
console.log(`\n Configuring Shift MCP for ${tool}...\n`);
|
|
191
|
+
switch (tool) {
|
|
192
|
+
case 'claude-code':
|
|
193
|
+
await addClaudeCode(projectId);
|
|
194
|
+
break;
|
|
195
|
+
case 'opencode':
|
|
196
|
+
addOpencode(projectId, projectRoot);
|
|
197
|
+
break;
|
|
198
|
+
case 'codex':
|
|
199
|
+
await addCodex(projectId);
|
|
200
|
+
break;
|
|
201
|
+
case 'copilot':
|
|
202
|
+
addCopilot(projectId, projectRoot);
|
|
203
|
+
break;
|
|
204
|
+
case 'droid':
|
|
205
|
+
await addFactoryDroid(projectId);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
console.log('');
|
|
209
|
+
}
|
|
@@ -2,12 +2,13 @@ import { exec } from 'child_process';
|
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { createInterface } from 'readline';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
|
-
import {
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { readProjectConfig, writeProjectConfig, ensureScanTargetFile } from '../../utils/config.js';
|
|
6
8
|
import { fetchProjectStatus, sendInitScan } from '../../utils/api-client.js';
|
|
7
9
|
import { resolveApiKey, resolveProject } from '../../utils/auth-resolver.js';
|
|
8
10
|
import { getDaemonStatus, startDaemon } from '../../daemon/daemon-manager.js';
|
|
9
|
-
import { getProjectTree, extractAllFilePaths, categorizeFiles } from '../../utils/tree-scanner.js';
|
|
10
|
-
import { loadShiftIgnore, scaffoldShiftIgnore } from '../../utils/shiftignore.js';
|
|
11
|
+
import { getProjectTree, extractAllFilePaths, categorizeFiles, countProjectLOC } from '../../utils/tree-scanner.js';
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
13
|
const { version } = require('../../../package.json');
|
|
13
14
|
const execAsync = promisify(exec);
|
|
@@ -64,6 +65,8 @@ export async function initCommand(options = {}) {
|
|
|
64
65
|
console.error('\n❌ Failed to configure project.');
|
|
65
66
|
process.exit(1);
|
|
66
67
|
}
|
|
68
|
+
// Ensure .shift/scan_target.json exists (template for user customization)
|
|
69
|
+
ensureScanTargetFile(projectRoot);
|
|
67
70
|
// Check if project is already indexed (skip check if --force flag is used)
|
|
68
71
|
if (!options.force) {
|
|
69
72
|
try {
|
|
@@ -121,18 +124,6 @@ export async function initCommand(options = {}) {
|
|
|
121
124
|
}
|
|
122
125
|
// Step 4: Scan project structure (matching extension's Step 6)
|
|
123
126
|
console.log('[Init] Step 4/5: Scanning project structure...');
|
|
124
|
-
// Scaffold .shiftignore if it doesn't exist
|
|
125
|
-
if (scaffoldShiftIgnore(projectRoot)) {
|
|
126
|
-
console.log('[Init] ✓ Created default .shiftignore (edit it to customize ignored files)\n');
|
|
127
|
-
}
|
|
128
|
-
// Load .shiftignore (skip if --no-ignore)
|
|
129
|
-
const shiftIgnore = options.noIgnore ? null : loadShiftIgnore(projectRoot);
|
|
130
|
-
if (options.noIgnore) {
|
|
131
|
-
console.log('[Init] ⚠️ --no-ignore flag set — skipping .shiftignore rules\n');
|
|
132
|
-
}
|
|
133
|
-
else if (shiftIgnore) {
|
|
134
|
-
console.log('[Init] ✓ Applying .shiftignore rules\n');
|
|
135
|
-
}
|
|
136
127
|
// Get project tree (matching extension's getProjectTree)
|
|
137
128
|
const treeData = getProjectTree(projectRoot, {
|
|
138
129
|
depth: 0, // Unlimited depth
|
|
@@ -149,7 +140,6 @@ export async function initCommand(options = {}) {
|
|
|
149
140
|
'venv',
|
|
150
141
|
'env',
|
|
151
142
|
],
|
|
152
|
-
shiftIgnore,
|
|
153
143
|
});
|
|
154
144
|
console.log(`[Init] Files: ${treeData.file_count}`);
|
|
155
145
|
console.log(`[Init] Directories: ${treeData.dir_count}`);
|
|
@@ -174,11 +164,49 @@ export async function initCommand(options = {}) {
|
|
|
174
164
|
// Extract file paths and categorize (matching extension)
|
|
175
165
|
const allFiles = extractAllFilePaths(treeData.tree);
|
|
176
166
|
const categorized = categorizeFiles(treeData.tree);
|
|
167
|
+
// Count total lines of code
|
|
168
|
+
const totalLOC = countProjectLOC(projectRoot, allFiles);
|
|
169
|
+
console.log(`[Init] Total LOC: ${totalLOC}`);
|
|
177
170
|
console.log(`[Init] Source files: ${categorized.source_files.length}`);
|
|
178
171
|
console.log(`[Init] Config files: ${categorized.config_files.length}`);
|
|
179
172
|
console.log(`[Init] Asset files: ${categorized.asset_files.length}\n`);
|
|
180
173
|
// Step 5: Send scan to backend (matching extension's Step 9)
|
|
181
174
|
console.log('[Init] Step 5/5: Sending scan to backend...');
|
|
175
|
+
// Read scan targets from .shift/scan_target.json if it exists
|
|
176
|
+
let scanTargets = null;
|
|
177
|
+
const scanTargetPath = path.join(projectRoot, '.shift', 'scan_target.json');
|
|
178
|
+
if (existsSync(scanTargetPath)) {
|
|
179
|
+
try {
|
|
180
|
+
const raw = readFileSync(scanTargetPath, 'utf-8');
|
|
181
|
+
const parsed = JSON.parse(raw);
|
|
182
|
+
// Accept both array format and single object format
|
|
183
|
+
const targets = Array.isArray(parsed) ? parsed : [parsed];
|
|
184
|
+
const mapped = targets.map((t) => ({
|
|
185
|
+
language: t.language ?? t.lang ?? null,
|
|
186
|
+
path: t.path ?? '',
|
|
187
|
+
}));
|
|
188
|
+
// Treat default template (single entry with language=null, path="") as unconfigured
|
|
189
|
+
const isDefault = mapped.length === 1 && mapped[0].language === null && mapped[0].path === '';
|
|
190
|
+
if (isDefault) {
|
|
191
|
+
console.log('[Init] .shift/scan_target.json is default template — server will auto-detect scan targets');
|
|
192
|
+
console.log('[Init] Edit the file to manually specify scan targets');
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
scanTargets = mapped;
|
|
196
|
+
console.log(`[Init] ✓ Loaded ${scanTargets.length} scan target(s) from .shift/scan_target.json`);
|
|
197
|
+
for (const t of scanTargets) {
|
|
198
|
+
console.log(`[Init] → language=${t.language ?? 'auto'}, path="${t.path || '(root)'}"`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
console.log(`[Init] ⚠️ Failed to read .shift/scan_target.json: ${err.message}`);
|
|
204
|
+
console.log('[Init] Proceeding without scan targets (server will auto-detect)');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.log('[Init] No .shift/scan_target.json found — server will auto-detect scan targets');
|
|
209
|
+
}
|
|
182
210
|
const payload = {
|
|
183
211
|
project_id: project.projectId,
|
|
184
212
|
project_tree: treeData,
|
|
@@ -192,6 +220,8 @@ export async function initCommand(options = {}) {
|
|
|
192
220
|
scan_timestamp: new Date().toISOString(),
|
|
193
221
|
project_name: project.projectName,
|
|
194
222
|
},
|
|
223
|
+
scan_targets: scanTargets,
|
|
224
|
+
total_loc: totalLOC,
|
|
195
225
|
};
|
|
196
226
|
try {
|
|
197
227
|
const response = await sendInitScan(apiKey, project.projectId, payload);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { readProjectConfig, setProject } from '../../utils/config.js';
|
|
1
|
+
import { readProjectConfig, setProject, ensureScanTargetFile } from '../../utils/config.js';
|
|
2
2
|
import { startDaemon, getDaemonStatus } from '../../daemon/daemon-manager.js';
|
|
3
3
|
import { resolveApiKey, resolveProject } from '../../utils/auth-resolver.js';
|
|
4
|
-
import { scaffoldShiftIgnore } from '../../utils/shiftignore.js';
|
|
5
4
|
export async function startCommand(options = {}) {
|
|
6
5
|
const projectRoot = process.cwd();
|
|
7
6
|
const isAuthInteractive = !options.guest && !options.apiKey;
|
|
@@ -38,10 +37,8 @@ export async function startCommand(options = {}) {
|
|
|
38
37
|
console.error('❌ Failed to configure project');
|
|
39
38
|
process.exit(1);
|
|
40
39
|
}
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
console.log('[Start] ✓ Created default .shiftignore (edit it to customize ignored files)\n');
|
|
44
|
-
}
|
|
40
|
+
// Ensure .shift/scan_target.json exists (template for user customization)
|
|
41
|
+
ensureScanTargetFile(projectRoot);
|
|
45
42
|
// Step 3: Check if daemon is already running
|
|
46
43
|
console.log('[Start] Step 3/4: Checking daemon status...');
|
|
47
44
|
const status = getDaemonStatus(projectRoot);
|
|
@@ -4,13 +4,12 @@ import { exec } from 'child_process';
|
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
import { getApiKey, readProjectConfig } from '../../utils/config.js';
|
|
6
6
|
import { getProjectTree, extractAllFilePaths } from '../../utils/tree-scanner.js';
|
|
7
|
-
import { loadShiftIgnore, filterPaths } from '../../utils/shiftignore.js';
|
|
8
7
|
import { sendUpdateDrg } from '../../utils/api-client.js';
|
|
9
8
|
const execAsync = promisify(exec);
|
|
10
9
|
async function detectGitChanges(projectRoot, extensions) {
|
|
11
10
|
const changes = { added: [], modified: [], deleted: [] };
|
|
12
11
|
try {
|
|
13
|
-
const { stdout } = await execAsync('git status --porcelain', { cwd: projectRoot });
|
|
12
|
+
const { stdout } = await execAsync('git status --porcelain -u', { cwd: projectRoot });
|
|
14
13
|
for (const line of stdout.split('\n')) {
|
|
15
14
|
if (!line.trim())
|
|
16
15
|
continue;
|
|
@@ -52,7 +51,24 @@ async function detectGitChanges(projectRoot, extensions) {
|
|
|
52
51
|
}
|
|
53
52
|
return changes;
|
|
54
53
|
}
|
|
55
|
-
const
|
|
54
|
+
const LANGUAGE_CONFIG = {
|
|
55
|
+
js_ts: {
|
|
56
|
+
extensions: new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']),
|
|
57
|
+
label: 'JS/TS',
|
|
58
|
+
},
|
|
59
|
+
python: {
|
|
60
|
+
extensions: new Set(['.py']),
|
|
61
|
+
label: 'Python',
|
|
62
|
+
},
|
|
63
|
+
csharp: {
|
|
64
|
+
extensions: new Set(['.cs']),
|
|
65
|
+
label: 'C#',
|
|
66
|
+
},
|
|
67
|
+
cpp: {
|
|
68
|
+
extensions: new Set(['.cpp', '.cc', '.cxx', '.h', '.hpp', '.c']),
|
|
69
|
+
label: 'C/C++',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
56
72
|
export async function updateDrgCommand(options = {}) {
|
|
57
73
|
const projectRoot = process.cwd();
|
|
58
74
|
const mode = options.mode || 'incremental';
|
|
@@ -76,15 +92,8 @@ export async function updateDrgCommand(options = {}) {
|
|
|
76
92
|
process.exit(1);
|
|
77
93
|
}
|
|
78
94
|
console.log(`[DRG] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
|
|
79
|
-
// Step 3: Scan project for
|
|
80
|
-
console.log('[DRG] Step 3/4: Scanning project for
|
|
81
|
-
const shiftIgnore = options.noIgnore ? null : loadShiftIgnore(projectRoot);
|
|
82
|
-
if (options.noIgnore) {
|
|
83
|
-
console.log('[DRG] ⚠️ --no-ignore flag set — skipping .shiftignore rules\n');
|
|
84
|
-
}
|
|
85
|
-
else if (shiftIgnore) {
|
|
86
|
-
console.log('[DRG] ✓ Found .shiftignore — applying custom ignore rules\n');
|
|
87
|
-
}
|
|
95
|
+
// Step 3: Scan project for all supported language files
|
|
96
|
+
console.log('[DRG] Step 3/4: Scanning project for supported languages...');
|
|
88
97
|
const treeData = getProjectTree(projectRoot, {
|
|
89
98
|
depth: 0,
|
|
90
99
|
exclude_patterns: [
|
|
@@ -100,89 +109,86 @@ export async function updateDrgCommand(options = {}) {
|
|
|
100
109
|
'venv',
|
|
101
110
|
'env',
|
|
102
111
|
],
|
|
103
|
-
shiftIgnore,
|
|
104
112
|
});
|
|
105
113
|
const allFiles = extractAllFilePaths(treeData.tree);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
// Group files by language (each file counted in first matching language)
|
|
115
|
+
const filesByLanguage = {};
|
|
116
|
+
for (const lang of Object.keys(LANGUAGE_CONFIG)) {
|
|
117
|
+
const extSet = LANGUAGE_CONFIG[lang].extensions;
|
|
118
|
+
filesByLanguage[lang] = allFiles.filter((filePath) => extSet.has(path.extname(filePath).toLowerCase()));
|
|
119
|
+
}
|
|
120
|
+
const languagesWithFiles = Object.entries(filesByLanguage).filter(([_, files]) => files.length > 0);
|
|
110
121
|
console.log(`[DRG] Total files scanned: ${allFiles.length}`);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
for (const [lang, files] of languagesWithFiles) {
|
|
123
|
+
console.log(`[DRG] ${LANGUAGE_CONFIG[lang].label} files: ${files.length}`);
|
|
124
|
+
}
|
|
125
|
+
console.log('');
|
|
126
|
+
if (languagesWithFiles.length === 0) {
|
|
127
|
+
console.log('⚠️ No supported language files found (JS/TS, Python, C#, C++). Nothing to update.\n');
|
|
114
128
|
return;
|
|
115
129
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
let anySent = false;
|
|
131
|
+
let lastError = null;
|
|
132
|
+
for (const [language, langFiles] of languagesWithFiles) {
|
|
133
|
+
const extSet = LANGUAGE_CONFIG[language].extensions;
|
|
134
|
+
const label = LANGUAGE_CONFIG[language].label;
|
|
135
|
+
// Read file contents for this language
|
|
136
|
+
const files = [];
|
|
137
|
+
let readErrors = 0;
|
|
138
|
+
for (const filePath of langFiles) {
|
|
139
|
+
const absolutePath = path.join(projectRoot, filePath);
|
|
140
|
+
try {
|
|
141
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
142
|
+
files.push({ file_path: filePath, content });
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
readErrors++;
|
|
146
|
+
}
|
|
124
147
|
}
|
|
125
|
-
|
|
126
|
-
readErrors
|
|
148
|
+
if (readErrors > 0) {
|
|
149
|
+
console.log(`[DRG] [${label}] ⚠️ Could not read ${readErrors} file(s)\n`);
|
|
127
150
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
console.log('\n✓ No JS/TS changes detected. DRG is up to date.\n');
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
// Step 4: Send to backend
|
|
156
|
-
console.log(`[DRG] Step 4/4: Sending ${filesToSend.length} files to backend (mode: ${mode})...`);
|
|
157
|
-
const payload = {
|
|
158
|
-
project_id: projectConfig.project_id,
|
|
159
|
-
language: 'js_ts',
|
|
160
|
-
mode: mode,
|
|
161
|
-
changes: gitChanges,
|
|
162
|
-
files: filesToSend,
|
|
163
|
-
};
|
|
164
|
-
try {
|
|
165
|
-
const response = await sendUpdateDrg(apiKey, payload);
|
|
166
|
-
console.log('\n╔═══════════════════════════════════════════════╗');
|
|
167
|
-
console.log('║ ✓ DRG Update Complete ║');
|
|
168
|
-
console.log('╚═══════════════════════════════════════════════╝\n');
|
|
169
|
-
console.log(` Success: ${response.success}`);
|
|
170
|
-
console.log(` Message: ${response.message}`);
|
|
171
|
-
if (response.stats) {
|
|
172
|
-
console.log(` Files sent: ${response.stats.total_files_provided}`);
|
|
173
|
-
console.log(` Unique: ${response.stats.unique_paths}`);
|
|
174
|
-
console.log(` Added: ${response.stats.added}`);
|
|
175
|
-
console.log(` Modified: ${response.stats.modified}`);
|
|
176
|
-
console.log(` Deleted: ${response.stats.deleted}`);
|
|
177
|
-
const affected = response.stats.affected_file_paths;
|
|
178
|
-
if (affected?.length) {
|
|
179
|
-
console.log(` Affected by deletions (edges updated): ${affected.length} file(s)`);
|
|
151
|
+
const gitChanges = await detectGitChanges(projectRoot, extSet);
|
|
152
|
+
const changedPaths = new Set([...gitChanges.added, ...gitChanges.modified]);
|
|
153
|
+
const filesToSend = mode === 'baseline'
|
|
154
|
+
? files
|
|
155
|
+
: files.filter((f) => changedPaths.has(f.file_path));
|
|
156
|
+
if (filesToSend.length === 0 && gitChanges.deleted.length === 0) {
|
|
157
|
+
console.log(`[DRG] [${label}] No changes. Skipping.\n`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
console.log(`[DRG] [${label}] Git changes — added: ${gitChanges.added.length}, modified: ${gitChanges.modified.length}, deleted: ${gitChanges.deleted.length}`);
|
|
161
|
+
console.log(`[DRG] [${label}] Sending ${filesToSend.length} file(s) (mode: ${mode})...`);
|
|
162
|
+
const payload = {
|
|
163
|
+
project_id: projectConfig.project_id,
|
|
164
|
+
language,
|
|
165
|
+
mode,
|
|
166
|
+
changes: gitChanges,
|
|
167
|
+
files: filesToSend,
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
const response = await sendUpdateDrg(apiKey, payload);
|
|
171
|
+
anySent = true;
|
|
172
|
+
console.log(`[DRG] [${label}] ✓ ${response.message}`);
|
|
173
|
+
if (response.stats) {
|
|
174
|
+
console.log(`[DRG] [${label}] Files sent: ${response.stats.total_files_provided}, Added: ${response.stats.added}, Modified: ${response.stats.modified}, Deleted: ${response.stats.deleted}`);
|
|
180
175
|
}
|
|
181
176
|
}
|
|
182
|
-
|
|
177
|
+
catch (error) {
|
|
178
|
+
lastError = error;
|
|
179
|
+
console.error(`[DRG] [${label}] ❌ ${error.message}`);
|
|
180
|
+
}
|
|
181
|
+
console.log('');
|
|
183
182
|
}
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
// Step 4: Summary
|
|
184
|
+
console.log('╔═══════════════════════════════════════════════╗');
|
|
185
|
+
console.log('║ ✓ DRG Update Complete ║');
|
|
186
|
+
console.log('╚═══════════════════════════════════════════════╝\n');
|
|
187
|
+
if (lastError && !anySent) {
|
|
188
|
+
console.error(`❌ All updates failed. Last error: ${lastError.message}`);
|
|
186
189
|
process.exit(1);
|
|
187
190
|
}
|
|
191
|
+
if (lastError) {
|
|
192
|
+
console.log(`⚠️ Some languages failed (see above).`);
|
|
193
|
+
}
|
|
188
194
|
}
|
package/build/index.js
CHANGED
|
@@ -16,8 +16,7 @@ program
|
|
|
16
16
|
' shift-cli status Check current status\n\n' +
|
|
17
17
|
'Configuration:\n' +
|
|
18
18
|
' Global config: ~/.shift/config.json\n' +
|
|
19
|
-
' Project config: .shift/config.json
|
|
20
|
-
' Ignore rules: .shiftignore (auto-created on first run)')
|
|
19
|
+
' Project config: .shift/config.json')
|
|
21
20
|
.version(version);
|
|
22
21
|
// MCP server mode (default when run via MCP host)
|
|
23
22
|
program
|
|
@@ -55,7 +54,7 @@ startCmd.addHelpText('after', `
|
|
|
55
54
|
Details:
|
|
56
55
|
Resolves authentication, configures the project, and launches a
|
|
57
56
|
background daemon that maintains a WebSocket connection to the
|
|
58
|
-
Shift backend.
|
|
57
|
+
Shift backend.
|
|
59
58
|
|
|
60
59
|
Examples:
|
|
61
60
|
shift-cli start Interactive setup
|
|
@@ -72,7 +71,6 @@ const initCmd = program
|
|
|
72
71
|
.option('--project-name <name>', 'Create a new project or match an existing one by name')
|
|
73
72
|
.option('--project-id <id>', 'Link to an existing project by its UUID')
|
|
74
73
|
.option('--template <id>', 'Use a specific migration template when creating the project')
|
|
75
|
-
.option('--no-ignore', 'Skip .shiftignore rules for this run (send all files)')
|
|
76
74
|
.action(async (options) => {
|
|
77
75
|
const { initCommand } = await import('./cli/commands/init.js');
|
|
78
76
|
await initCommand({
|
|
@@ -82,7 +80,6 @@ const initCmd = program
|
|
|
82
80
|
projectName: options.projectName,
|
|
83
81
|
projectId: options.projectId,
|
|
84
82
|
template: options.template,
|
|
85
|
-
noIgnore: options.noIgnore,
|
|
86
83
|
});
|
|
87
84
|
});
|
|
88
85
|
initCmd.addHelpText('after', `
|
|
@@ -90,7 +87,6 @@ Details:
|
|
|
90
87
|
Performs a full project scan — collects the file tree, categorizes files
|
|
91
88
|
(source, config, assets), gathers git info, and sends everything to the
|
|
92
89
|
Shift backend for indexing. Automatically starts the daemon if not running.
|
|
93
|
-
Creates a default .shiftignore if one doesn't exist.
|
|
94
90
|
|
|
95
91
|
If the project is already indexed, you will be prompted to re-index.
|
|
96
92
|
Use --force to skip the prompt.
|
|
@@ -98,7 +94,6 @@ Details:
|
|
|
98
94
|
Examples:
|
|
99
95
|
shift-cli init Interactive initialization
|
|
100
96
|
shift-cli init --force Force re-index without prompt
|
|
101
|
-
shift-cli init --no-ignore Index all files, ignore .shiftignore
|
|
102
97
|
shift-cli init --guest Quick init with guest auth
|
|
103
98
|
`);
|
|
104
99
|
// --- stop ---
|
|
@@ -136,16 +131,15 @@ const updateDrgCmd = program
|
|
|
136
131
|
.command('update-drg')
|
|
137
132
|
.description('Update the dependency relationship graph (DRG)')
|
|
138
133
|
.option('-m, --mode <mode>', 'Update mode: "baseline" (all files) or "incremental" (git-changed only)', 'incremental')
|
|
139
|
-
.option('--no-ignore', 'Skip .shiftignore rules for this run (send all files)')
|
|
140
134
|
.action(async (options) => {
|
|
141
135
|
const { updateDrgCommand } = await import('./cli/commands/update-drg.js');
|
|
142
|
-
await updateDrgCommand({ mode: options.mode
|
|
136
|
+
await updateDrgCommand({ mode: options.mode });
|
|
143
137
|
});
|
|
144
138
|
updateDrgCmd.addHelpText('after', `
|
|
145
139
|
Details:
|
|
146
140
|
Scans JavaScript/TypeScript files (.js, .jsx, .ts, .tsx, .mjs, .cjs),
|
|
147
141
|
detects git changes, and sends file contents to the backend for
|
|
148
|
-
dependency analysis.
|
|
142
|
+
dependency analysis.
|
|
149
143
|
|
|
150
144
|
Modes:
|
|
151
145
|
incremental Send only git-changed files (default, faster)
|
|
@@ -154,7 +148,27 @@ Modes:
|
|
|
154
148
|
Examples:
|
|
155
149
|
shift-cli update-drg Incremental update
|
|
156
150
|
shift-cli update-drg -m baseline Full re-analysis
|
|
157
|
-
|
|
151
|
+
`);
|
|
152
|
+
// --- add mcp servers---
|
|
153
|
+
const addCmd = program
|
|
154
|
+
.command('add <tool>')
|
|
155
|
+
.description('Add Shift MCP server to an AI coding tool')
|
|
156
|
+
.action(async (tool) => {
|
|
157
|
+
const { addCommand } = await import('./cli/commands/add.js');
|
|
158
|
+
await addCommand(tool);
|
|
159
|
+
});
|
|
160
|
+
addCmd.addHelpText('after', `
|
|
161
|
+
Supported tools:
|
|
162
|
+
claude-code Configure via Claude Code CLI
|
|
163
|
+
opencode Write to opencode.json
|
|
164
|
+
codex Configure via Codex CLI
|
|
165
|
+
copilot Write to .vscode/mcp.json
|
|
166
|
+
droid Configure via Factory-Droid CLI
|
|
167
|
+
|
|
168
|
+
Examples:
|
|
169
|
+
shift-cli add claude-code
|
|
170
|
+
shift-cli add copilot
|
|
171
|
+
shift-cli add opencode
|
|
158
172
|
`);
|
|
159
173
|
// --- config ---
|
|
160
174
|
const configCmd = program
|
|
@@ -48,19 +48,28 @@ export async function fetchProjects(apiKey) {
|
|
|
48
48
|
* Matching extension's init-scan API call
|
|
49
49
|
*/
|
|
50
50
|
export async function sendInitScan(apiKey, projectId, payload) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
try {
|
|
52
|
+
// Ensure proper URL construction with leading slash
|
|
53
|
+
const path = `/api/projects/${projectId}/init-scan`;
|
|
54
|
+
const url = new URL(path, API_BASE_URL_ORCH).toString();
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(payload),
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const text = await response.text();
|
|
65
|
+
throw new Error(text || `HTTP ${response.status}`);
|
|
66
|
+
}
|
|
67
|
+
return await response.json();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
// Re-throw for caller to handle; keep stack/context
|
|
71
|
+
throw error;
|
|
62
72
|
}
|
|
63
|
-
return await response.json();
|
|
64
73
|
}
|
|
65
74
|
/**
|
|
66
75
|
* Send update-drg request to backend
|
package/build/utils/config.js
CHANGED
|
@@ -132,6 +132,22 @@ export function ensureLocalShiftDir(projectRoot) {
|
|
|
132
132
|
fs.mkdirSync(dir, { recursive: true });
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
+
const SCAN_TARGET_TEMPLATE = [
|
|
136
|
+
{
|
|
137
|
+
language: null,
|
|
138
|
+
path: '',
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
/**
|
|
142
|
+
* Create .shift/scan_target.json with a template if it doesn't exist.
|
|
143
|
+
* Users can edit this file to manually specify scan targets.
|
|
144
|
+
*/
|
|
145
|
+
export function ensureScanTargetFile(projectRoot) {
|
|
146
|
+
const filePath = path.join(getLocalShiftDir(projectRoot), 'scan_target.json');
|
|
147
|
+
if (!fs.existsSync(filePath)) {
|
|
148
|
+
fs.writeFileSync(filePath, JSON.stringify(SCAN_TARGET_TEMPLATE, null, 2));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
135
151
|
export function readProjectConfig(projectRoot) {
|
|
136
152
|
try {
|
|
137
153
|
const filePath = path.join(getLocalShiftDir(projectRoot), LOCAL_CONFIG_FILE);
|
|
@@ -152,8 +168,10 @@ export function writeProjectConfig(config, projectRoot) {
|
|
|
152
168
|
// Also create .gitignore in .shift folder (matching extension)
|
|
153
169
|
const gitignorePath = path.join(getLocalShiftDir(projectRoot), '.gitignore');
|
|
154
170
|
if (!fs.existsSync(gitignorePath)) {
|
|
155
|
-
fs.writeFileSync(gitignorePath, '*\n!.gitignore\n!config.json\n');
|
|
171
|
+
fs.writeFileSync(gitignorePath, '*\n!.gitignore\n!config.json\n!scan_target.json\n');
|
|
156
172
|
}
|
|
173
|
+
// Create scan_target.json template if it doesn't exist
|
|
174
|
+
ensureScanTargetFile(projectRoot);
|
|
157
175
|
}
|
|
158
176
|
export function getProjectId(projectRoot) {
|
|
159
177
|
const config = readProjectConfig(projectRoot);
|
|
@@ -21,7 +21,7 @@ const DEFAULT_EXCLUDE_PATTERNS = [
|
|
|
21
21
|
* Get project tree - matching extension's getProjectTree function
|
|
22
22
|
*/
|
|
23
23
|
export function getProjectTree(workspaceRoot, options = {}) {
|
|
24
|
-
const { depth = 0, exclude_patterns = DEFAULT_EXCLUDE_PATTERNS,
|
|
24
|
+
const { depth = 0, exclude_patterns = DEFAULT_EXCLUDE_PATTERNS, } = options;
|
|
25
25
|
let file_count = 0;
|
|
26
26
|
let dir_count = 0;
|
|
27
27
|
let total_size = 0;
|
|
@@ -40,15 +40,6 @@ export function getProjectTree(workspaceRoot, options = {}) {
|
|
|
40
40
|
}
|
|
41
41
|
const itemPath = path.join(dirPath, entry.name);
|
|
42
42
|
const itemRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
43
|
-
// Check if should be excluded by .shiftignore
|
|
44
|
-
if (shiftIgnore) {
|
|
45
|
-
// Use forward slashes for ignore matching; add trailing / for directories
|
|
46
|
-
const testPath = itemRelativePath.replace(/\\/g, '/');
|
|
47
|
-
const isDir = entry.isDirectory();
|
|
48
|
-
if (shiftIgnore.ignores(isDir ? testPath + '/' : testPath)) {
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
43
|
if (entry.isDirectory()) {
|
|
53
44
|
dir_count++;
|
|
54
45
|
const children = scanDirectory(itemPath, currentDepth + 1, itemRelativePath);
|
|
@@ -123,6 +114,23 @@ export function extractAllFilePaths(tree, basePath = '') {
|
|
|
123
114
|
* Categorize files by type
|
|
124
115
|
* Matching extension's categorizeFiles function
|
|
125
116
|
*/
|
|
117
|
+
/**
|
|
118
|
+
* Count total lines of code across all project files
|
|
119
|
+
*/
|
|
120
|
+
export function countProjectLOC(rootPath, filePaths) {
|
|
121
|
+
let totalLOC = 0;
|
|
122
|
+
for (const filePath of filePaths) {
|
|
123
|
+
try {
|
|
124
|
+
const fullPath = path.join(rootPath, filePath);
|
|
125
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
126
|
+
totalLOC += content.split('\n').length;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Skip binary/unreadable files
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return totalLOC;
|
|
133
|
+
}
|
|
126
134
|
export function categorizeFiles(tree, basePath = '') {
|
|
127
135
|
const categories = {
|
|
128
136
|
source_files: [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@latentforce/shift",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Shift CLI - AI-powered code intelligence with MCP support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./build/index.js",
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
40
40
|
"commander": "^12.0.0",
|
|
41
|
-
"ignore": "^7.0.5",
|
|
42
41
|
"ws": "^8.14.0",
|
|
43
42
|
"zod": "^3.25.76"
|
|
44
43
|
},
|