@latentforce/shift 1.0.9 → 1.0.11
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 -22
- package/build/cli/commands/add.js +209 -0
- package/build/cli/commands/init.js +46 -2
- package/build/cli/commands/start.js +3 -1
- package/build/cli/commands/update-drg.js +91 -72
- package/build/index.js +133 -24
- package/build/utils/api-client.js +21 -12
- package/build/utils/config.js +19 -1
- package/build/utils/shiftignore.js +108 -0
- package/build/utils/tree-scanner.js +18 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
AI-powered code intelligence CLI and MCP server for Claude.
|
|
4
4
|
|
|
5
|
+
Shift indexes your codebase, builds a dependency relationship graph (DRG), and provides AI-powered insights via MCP tools — enabling Claude to understand your project's structure, dependencies, and blast radius of changes.
|
|
6
|
+
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
10
|
npm install -g @latentforce/shift
|
|
9
11
|
```
|
|
10
12
|
|
|
13
|
+
Requires Node.js >= 18.
|
|
14
|
+
|
|
11
15
|
## Quick Start
|
|
12
16
|
|
|
13
17
|
### Guest mode (zero-config)
|
|
@@ -34,19 +38,27 @@ shift-cli start # Configure API key and project interactively
|
|
|
34
38
|
shift-cli init # Index project files
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
## MCP Integration
|
|
42
|
+
|
|
43
|
+
After initializing your project, use `shift-cli add` to configure Shift's MCP server in your AI coding tool:
|
|
38
44
|
|
|
39
45
|
```bash
|
|
40
|
-
|
|
46
|
+
shift-cli add <tool>
|
|
41
47
|
```
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
### Supported tools
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
| Tool | Command | Config method |
|
|
52
|
+
|------|---------|---------------|
|
|
53
|
+
| Claude Code | `shift-cli add claude-code` | Runs `claude mcp add-json` |
|
|
54
|
+
| Opencode | `shift-cli add opencode` | Writes `opencode.json` |
|
|
55
|
+
| Codex | `shift-cli add codex` | Runs `codex mcp add` |
|
|
56
|
+
| GitHub Copilot | `shift-cli add copilot` | Writes `.vscode/mcp.json` |
|
|
57
|
+
| Factory Droid | `shift-cli add droid` | Runs `droid mcp add` |
|
|
58
|
+
|
|
59
|
+
For CLI-based tools, if the CLI is not installed, the command will print manual setup instructions.
|
|
48
60
|
|
|
49
|
-
###
|
|
61
|
+
### Claude Desktop (manual)
|
|
50
62
|
|
|
51
63
|
Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Windows, `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
52
64
|
|
|
@@ -68,52 +80,179 @@ Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Window
|
|
|
68
80
|
|
|
69
81
|
| Command | Description |
|
|
70
82
|
|---------|-------------|
|
|
71
|
-
| `shift-cli start` | Start daemon and configure project |
|
|
83
|
+
| `shift-cli start` | Start the daemon and configure the project |
|
|
72
84
|
| `shift-cli init` | Scan and index project files |
|
|
73
85
|
| `shift-cli update-drg` | Update the dependency relationship graph |
|
|
86
|
+
| `shift-cli add <tool>` | Add Shift MCP server to an AI coding tool |
|
|
74
87
|
| `shift-cli stop` | Stop the daemon |
|
|
75
88
|
| `shift-cli status` | Show current status |
|
|
76
89
|
| `shift-cli config` | Manage configuration |
|
|
77
90
|
|
|
78
|
-
###
|
|
91
|
+
### `shift-cli init`
|
|
92
|
+
|
|
93
|
+
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.
|
|
79
94
|
|
|
80
|
-
|
|
95
|
+
If the project is already indexed, you will be prompted to re-index. Use `--force` to skip the prompt.
|
|
81
96
|
|
|
82
97
|
| Flag | Description |
|
|
83
98
|
|------|-------------|
|
|
84
|
-
| `--guest` | Use guest authentication (auto-creates project) |
|
|
99
|
+
| `--guest` | Use guest authentication (auto-creates a temporary project) |
|
|
85
100
|
| `--api-key <key>` | Provide API key directly |
|
|
86
101
|
| `--project-name <name>` | Create new project or match existing by name |
|
|
87
102
|
| `--project-id <id>` | Use existing project UUID |
|
|
88
103
|
| `--template <id>` | Migration template ID for project creation |
|
|
89
104
|
| `-f, --force` | Force re-indexing even if already indexed |
|
|
90
105
|
|
|
91
|
-
|
|
106
|
+
```bash
|
|
107
|
+
shift-cli init # Interactive initialization
|
|
108
|
+
shift-cli init --force # Force re-index without prompt
|
|
109
|
+
shift-cli init --guest # Quick init with guest auth
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### `shift-cli start`
|
|
113
|
+
|
|
114
|
+
Resolves authentication, configures the project, and launches a background daemon that maintains a WebSocket connection to the Shift backend.
|
|
92
115
|
|
|
93
116
|
| Flag | Description |
|
|
94
117
|
|------|-------------|
|
|
95
|
-
| `--guest` | Use guest authentication (auto-creates project) |
|
|
118
|
+
| `--guest` | Use guest authentication (auto-creates a temporary project) |
|
|
96
119
|
| `--api-key <key>` | Provide API key directly |
|
|
97
120
|
| `--project-name <name>` | Create new project or match existing by name |
|
|
98
121
|
| `--project-id <id>` | Use existing project UUID |
|
|
99
122
|
| `--template <id>` | Migration template ID for project creation |
|
|
100
123
|
|
|
101
|
-
|
|
124
|
+
```bash
|
|
125
|
+
shift-cli start # Interactive setup
|
|
126
|
+
shift-cli start --guest # Quick start without API key
|
|
127
|
+
shift-cli start --api-key <key> --project-name "My App"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `shift-cli update-drg`
|
|
131
|
+
|
|
132
|
+
Scans JavaScript/TypeScript files (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`), detects git changes, and sends file contents to the backend for dependency analysis.
|
|
102
133
|
|
|
103
134
|
| Flag | Description |
|
|
104
135
|
|------|-------------|
|
|
105
136
|
| `-m, --mode <mode>` | `baseline` (all files) or `incremental` (git-changed only, default) |
|
|
106
137
|
|
|
107
|
-
|
|
138
|
+
**Modes:**
|
|
139
|
+
- **incremental** (default) — sends only git-changed files for faster updates
|
|
140
|
+
- **baseline** — sends all JS/TS files for a full re-analysis
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
shift-cli update-drg # Incremental update (default)
|
|
144
|
+
shift-cli update-drg -m baseline # Full re-analysis
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `shift-cli add <tool>`
|
|
108
148
|
|
|
109
|
-
|
|
149
|
+
Configures Shift's MCP server in the specified AI coding tool. Reads `project_id` from `.shift/config.json` (run `shift-cli init` first).
|
|
110
150
|
|
|
111
151
|
```bash
|
|
112
|
-
shift-cli
|
|
113
|
-
shift-cli
|
|
152
|
+
shift-cli add claude-code # Configure via Claude Code CLI
|
|
153
|
+
shift-cli add opencode # Write to opencode.json
|
|
154
|
+
shift-cli add codex # Configure via Codex CLI
|
|
155
|
+
shift-cli add copilot # Write to .vscode/mcp.json
|
|
156
|
+
shift-cli add droid # Configure via Droid CLI
|
|
114
157
|
```
|
|
115
158
|
|
|
116
|
-
|
|
159
|
+
### `shift-cli config`
|
|
160
|
+
|
|
161
|
+
Manage Shift configuration (URLs, API key).
|
|
162
|
+
|
|
163
|
+
| Action | Description |
|
|
164
|
+
|--------|-------------|
|
|
165
|
+
| `show` | Display current configuration (default) |
|
|
166
|
+
| `set <key> <value>` | Set a configuration value |
|
|
167
|
+
| `clear [key]` | Clear a specific key or all configuration |
|
|
168
|
+
|
|
169
|
+
**Configurable keys:** `api-key`, `api-url`, `orch-url`, `ws-url`
|
|
170
|
+
|
|
171
|
+
URLs can also be set via environment variables: `SHIFT_API_URL`, `SHIFT_ORCH_URL`, `SHIFT_WS_URL`.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
shift-cli config # Show config
|
|
175
|
+
shift-cli config set api-key sk-abc123 # Set API key
|
|
176
|
+
shift-cli config set api-url https://api.shift.ai # Set API URL
|
|
177
|
+
shift-cli config clear api-key # Clear API key
|
|
178
|
+
shift-cli config clear # Clear all config
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## `.shiftignore`
|
|
182
|
+
|
|
183
|
+
Shift uses a `.shiftignore` file to control which files and directories are excluded from indexing and dependency analysis. It follows the same syntax as `.gitignore`.
|
|
184
|
+
|
|
185
|
+
A default `.shiftignore` is **automatically created** the first time you run `shift-cli init` or `shift-cli start`. You can edit it to customize which files are excluded.
|
|
186
|
+
|
|
187
|
+
### Syntax
|
|
188
|
+
|
|
189
|
+
- Blank lines are ignored
|
|
190
|
+
- Lines starting with `#` are comments
|
|
191
|
+
- Standard glob patterns (`*`, `**`, `?`)
|
|
192
|
+
- Trailing `/` matches directories only
|
|
193
|
+
- Leading `/` anchors to the project root
|
|
194
|
+
- `!` negates a pattern (re-includes a previously ignored path)
|
|
195
|
+
|
|
196
|
+
### Default `.shiftignore`
|
|
197
|
+
|
|
198
|
+
The auto-generated file excludes common non-source directories and files:
|
|
199
|
+
|
|
200
|
+
```gitignore
|
|
201
|
+
# Dependencies
|
|
202
|
+
node_modules/
|
|
203
|
+
vendor/
|
|
204
|
+
bower_components/
|
|
205
|
+
|
|
206
|
+
# Build output
|
|
207
|
+
dist/
|
|
208
|
+
build/
|
|
209
|
+
out/
|
|
210
|
+
.next/
|
|
211
|
+
|
|
212
|
+
# Test & coverage
|
|
213
|
+
coverage/
|
|
214
|
+
.nyc_output/
|
|
215
|
+
|
|
216
|
+
# Environment & secrets
|
|
217
|
+
.env
|
|
218
|
+
.env.*
|
|
219
|
+
*.pem
|
|
220
|
+
*.key
|
|
221
|
+
|
|
222
|
+
# Logs
|
|
223
|
+
*.log
|
|
224
|
+
logs/
|
|
225
|
+
|
|
226
|
+
# OS files
|
|
227
|
+
.DS_Store
|
|
228
|
+
Thumbs.db
|
|
229
|
+
|
|
230
|
+
# IDE
|
|
231
|
+
.vscode/
|
|
232
|
+
.idea/
|
|
233
|
+
*.swp
|
|
234
|
+
*.swo
|
|
235
|
+
|
|
236
|
+
# Python
|
|
237
|
+
__pycache__/
|
|
238
|
+
*.pyc
|
|
239
|
+
venv/
|
|
240
|
+
.venv/
|
|
241
|
+
|
|
242
|
+
# Misc
|
|
243
|
+
*.min.js
|
|
244
|
+
*.min.css
|
|
245
|
+
*.map
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Bypassing `.shiftignore`
|
|
249
|
+
|
|
250
|
+
Use the `--no-ignore` flag to skip `.shiftignore` rules for a single run:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
shift-cli init --no-ignore # Index all files
|
|
254
|
+
shift-cli update-drg --no-ignore # Include ignored files in DRG update
|
|
255
|
+
```
|
|
117
256
|
|
|
118
257
|
## MCP Tools
|
|
119
258
|
|
|
@@ -125,6 +264,10 @@ Scans `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs` files.
|
|
|
125
264
|
|
|
126
265
|
Each tool accepts an optional `project_id` parameter. If not provided, it falls back to the `SHIFT_PROJECT_ID` environment variable.
|
|
127
266
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
267
|
+
## Configuration Files
|
|
268
|
+
|
|
269
|
+
| File | Location | Description |
|
|
270
|
+
|------|----------|-------------|
|
|
271
|
+
| Global config | `~/.shift/config.json` | API key and server URLs |
|
|
272
|
+
| Project config | `.shift/config.json` | Project ID, name, and agent info |
|
|
273
|
+
| Ignore rules | `.shiftignore` | Files/directories to exclude (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,11 +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';
|
|
11
|
+
import { getProjectTree, extractAllFilePaths, categorizeFiles, countProjectLOC } from '../../utils/tree-scanner.js';
|
|
10
12
|
const require = createRequire(import.meta.url);
|
|
11
13
|
const { version } = require('../../../package.json');
|
|
12
14
|
const execAsync = promisify(exec);
|
|
@@ -63,6 +65,8 @@ export async function initCommand(options = {}) {
|
|
|
63
65
|
console.error('\n❌ Failed to configure project.');
|
|
64
66
|
process.exit(1);
|
|
65
67
|
}
|
|
68
|
+
// Ensure .shift/scan_target.json exists (template for user customization)
|
|
69
|
+
ensureScanTargetFile(projectRoot);
|
|
66
70
|
// Check if project is already indexed (skip check if --force flag is used)
|
|
67
71
|
if (!options.force) {
|
|
68
72
|
try {
|
|
@@ -160,11 +164,49 @@ export async function initCommand(options = {}) {
|
|
|
160
164
|
// Extract file paths and categorize (matching extension)
|
|
161
165
|
const allFiles = extractAllFilePaths(treeData.tree);
|
|
162
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}`);
|
|
163
170
|
console.log(`[Init] Source files: ${categorized.source_files.length}`);
|
|
164
171
|
console.log(`[Init] Config files: ${categorized.config_files.length}`);
|
|
165
172
|
console.log(`[Init] Asset files: ${categorized.asset_files.length}\n`);
|
|
166
173
|
// Step 5: Send scan to backend (matching extension's Step 9)
|
|
167
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
|
+
}
|
|
168
210
|
const payload = {
|
|
169
211
|
project_id: project.projectId,
|
|
170
212
|
project_tree: treeData,
|
|
@@ -178,6 +220,8 @@ export async function initCommand(options = {}) {
|
|
|
178
220
|
scan_timestamp: new Date().toISOString(),
|
|
179
221
|
project_name: project.projectName,
|
|
180
222
|
},
|
|
223
|
+
scan_targets: scanTargets,
|
|
224
|
+
total_loc: totalLOC,
|
|
181
225
|
};
|
|
182
226
|
try {
|
|
183
227
|
const response = await sendInitScan(apiKey, project.projectId, payload);
|
|
@@ -1,4 +1,4 @@
|
|
|
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
4
|
export async function startCommand(options = {}) {
|
|
@@ -37,6 +37,8 @@ export async function startCommand(options = {}) {
|
|
|
37
37
|
console.error('❌ Failed to configure project');
|
|
38
38
|
process.exit(1);
|
|
39
39
|
}
|
|
40
|
+
// Ensure .shift/scan_target.json exists (template for user customization)
|
|
41
|
+
ensureScanTargetFile(projectRoot);
|
|
40
42
|
// Step 3: Check if daemon is already running
|
|
41
43
|
console.log('[Start] Step 3/4: Checking daemon status...');
|
|
42
44
|
const status = getDaemonStatus(projectRoot);
|
|
@@ -9,7 +9,7 @@ const execAsync = promisify(exec);
|
|
|
9
9
|
async function detectGitChanges(projectRoot, extensions) {
|
|
10
10
|
const changes = { added: [], modified: [], deleted: [] };
|
|
11
11
|
try {
|
|
12
|
-
const { stdout } = await execAsync('git status --porcelain', { cwd: projectRoot });
|
|
12
|
+
const { stdout } = await execAsync('git status --porcelain -u', { cwd: projectRoot });
|
|
13
13
|
for (const line of stdout.split('\n')) {
|
|
14
14
|
if (!line.trim())
|
|
15
15
|
continue;
|
|
@@ -51,7 +51,24 @@ async function detectGitChanges(projectRoot, extensions) {
|
|
|
51
51
|
}
|
|
52
52
|
return changes;
|
|
53
53
|
}
|
|
54
|
-
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
|
+
};
|
|
55
72
|
export async function updateDrgCommand(options = {}) {
|
|
56
73
|
const projectRoot = process.cwd();
|
|
57
74
|
const mode = options.mode || 'incremental';
|
|
@@ -75,8 +92,8 @@ export async function updateDrgCommand(options = {}) {
|
|
|
75
92
|
process.exit(1);
|
|
76
93
|
}
|
|
77
94
|
console.log(`[DRG] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
|
|
78
|
-
// Step 3: Scan project for
|
|
79
|
-
console.log('[DRG] Step 3/4: Scanning project for
|
|
95
|
+
// Step 3: Scan project for all supported language files
|
|
96
|
+
console.log('[DRG] Step 3/4: Scanning project for supported languages...');
|
|
80
97
|
const treeData = getProjectTree(projectRoot, {
|
|
81
98
|
depth: 0,
|
|
82
99
|
exclude_patterns: [
|
|
@@ -94,82 +111,84 @@ export async function updateDrgCommand(options = {}) {
|
|
|
94
111
|
],
|
|
95
112
|
});
|
|
96
113
|
const allFiles = extractAllFilePaths(treeData.tree);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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);
|
|
101
121
|
console.log(`[DRG] Total files scanned: ${allFiles.length}`);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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');
|
|
105
128
|
return;
|
|
106
129
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
}
|
|
115
147
|
}
|
|
116
|
-
|
|
117
|
-
readErrors
|
|
148
|
+
if (readErrors > 0) {
|
|
149
|
+
console.log(`[DRG] [${label}] ⚠️ Could not read ${readErrors} file(s)\n`);
|
|
118
150
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
console.log(`[DRG] Step 4/4: Sending ${filesToSend.length} files to backend (mode: ${mode})...`);
|
|
144
|
-
const payload = {
|
|
145
|
-
project_id: projectConfig.project_id,
|
|
146
|
-
language: 'js_ts',
|
|
147
|
-
mode: mode,
|
|
148
|
-
changes: gitChanges,
|
|
149
|
-
files: filesToSend,
|
|
150
|
-
};
|
|
151
|
-
try {
|
|
152
|
-
const response = await sendUpdateDrg(apiKey, payload);
|
|
153
|
-
console.log('\n╔═══════════════════════════════════════════════╗');
|
|
154
|
-
console.log('║ ✓ DRG Update Complete ║');
|
|
155
|
-
console.log('╚═══════════════════════════════════════════════╝\n');
|
|
156
|
-
console.log(` Success: ${response.success}`);
|
|
157
|
-
console.log(` Message: ${response.message}`);
|
|
158
|
-
if (response.stats) {
|
|
159
|
-
console.log(` Files sent: ${response.stats.total_files_provided}`);
|
|
160
|
-
console.log(` Unique: ${response.stats.unique_paths}`);
|
|
161
|
-
console.log(` Added: ${response.stats.added}`);
|
|
162
|
-
console.log(` Modified: ${response.stats.modified}`);
|
|
163
|
-
console.log(` Deleted: ${response.stats.deleted}`);
|
|
164
|
-
const affected = response.stats.affected_file_paths;
|
|
165
|
-
if (affected?.length) {
|
|
166
|
-
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}`);
|
|
167
175
|
}
|
|
168
176
|
}
|
|
169
|
-
|
|
177
|
+
catch (error) {
|
|
178
|
+
lastError = error;
|
|
179
|
+
console.error(`[DRG] [${label}] ❌ ${error.message}`);
|
|
180
|
+
}
|
|
181
|
+
console.log('');
|
|
170
182
|
}
|
|
171
|
-
|
|
172
|
-
|
|
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}`);
|
|
173
189
|
process.exit(1);
|
|
174
190
|
}
|
|
191
|
+
if (lastError) {
|
|
192
|
+
console.log(`⚠️ Some languages failed (see above).`);
|
|
193
|
+
}
|
|
175
194
|
}
|
package/build/index.js
CHANGED
|
@@ -6,16 +6,24 @@ const { version } = require('../package.json');
|
|
|
6
6
|
const program = new Command();
|
|
7
7
|
program
|
|
8
8
|
.name('shift-cli')
|
|
9
|
-
.description('Shift CLI - AI-powered code intelligence'
|
|
9
|
+
.description('Shift CLI - AI-powered code intelligence and dependency analysis.\n\n' +
|
|
10
|
+
'Shift indexes your codebase, builds a dependency relationship graph (DRG),\n' +
|
|
11
|
+
'and provides AI-powered insights via MCP tools.\n\n' +
|
|
12
|
+
'Quick start:\n' +
|
|
13
|
+
' shift-cli start Start the daemon and configure project\n' +
|
|
14
|
+
' shift-cli init Scan and index the project\n' +
|
|
15
|
+
' shift-cli update-drg Build/update the dependency graph\n' +
|
|
16
|
+
' shift-cli status Check current status\n\n' +
|
|
17
|
+
'Configuration:\n' +
|
|
18
|
+
' Global config: ~/.shift/config.json\n' +
|
|
19
|
+
' Project config: .shift/config.json')
|
|
10
20
|
.version(version);
|
|
11
21
|
// MCP server mode (default when run via MCP host)
|
|
12
22
|
program
|
|
13
23
|
.command('mcp', { isDefault: true, hidden: true })
|
|
14
24
|
.description('Start MCP server on stdio')
|
|
15
25
|
.action(async () => {
|
|
16
|
-
// Check if 'mcp' was explicitly passed as argument
|
|
17
26
|
const mcpExplicitlyRequested = process.argv.includes('mcp');
|
|
18
|
-
// If running interactively (TTY) and mcp wasn't explicitly requested, show help
|
|
19
27
|
if (process.stdin.isTTY && !mcpExplicitlyRequested) {
|
|
20
28
|
program.outputHelp();
|
|
21
29
|
return;
|
|
@@ -23,15 +31,15 @@ program
|
|
|
23
31
|
const { startMcpServer } = await import('./mcp-server.js');
|
|
24
32
|
await startMcpServer();
|
|
25
33
|
});
|
|
26
|
-
//
|
|
27
|
-
program
|
|
34
|
+
// --- start ---
|
|
35
|
+
const startCmd = program
|
|
28
36
|
.command('start')
|
|
29
37
|
.description('Start the Shift daemon for this project')
|
|
30
|
-
.option('--guest', 'Use guest authentication (auto-creates project)')
|
|
31
|
-
.option('--api-key <key>', 'Provide API key directly')
|
|
32
|
-
.option('--project-name <name>', 'Create new project or match existing by name')
|
|
33
|
-
.option('--project-id <id>', '
|
|
34
|
-
.option('--template <id>', '
|
|
38
|
+
.option('--guest', 'Use guest authentication (auto-creates a temporary project)')
|
|
39
|
+
.option('--api-key <key>', 'Provide your Shift API key directly instead of interactive prompt')
|
|
40
|
+
.option('--project-name <name>', 'Create a new project or match an existing one by name')
|
|
41
|
+
.option('--project-id <id>', 'Link to an existing project by its UUID')
|
|
42
|
+
.option('--template <id>', 'Use a specific migration template when creating the project')
|
|
35
43
|
.action(async (options) => {
|
|
36
44
|
const { startCommand } = await import('./cli/commands/start.js');
|
|
37
45
|
await startCommand({
|
|
@@ -42,15 +50,27 @@ program
|
|
|
42
50
|
template: options.template,
|
|
43
51
|
});
|
|
44
52
|
});
|
|
45
|
-
|
|
53
|
+
startCmd.addHelpText('after', `
|
|
54
|
+
Details:
|
|
55
|
+
Resolves authentication, configures the project, and launches a
|
|
56
|
+
background daemon that maintains a WebSocket connection to the
|
|
57
|
+
Shift backend.
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
shift-cli start Interactive setup
|
|
61
|
+
shift-cli start --guest Quick start without API key
|
|
62
|
+
shift-cli start --api-key <key> --project-name "My App"
|
|
63
|
+
`);
|
|
64
|
+
// --- init ---
|
|
65
|
+
const initCmd = program
|
|
46
66
|
.command('init')
|
|
47
67
|
.description('Initialize and scan the project for file indexing')
|
|
48
|
-
.option('-f, --force', 'Force re-indexing even if project is already indexed')
|
|
49
|
-
.option('--guest', 'Use guest authentication (auto-creates project)')
|
|
50
|
-
.option('--api-key <key>', 'Provide API key directly')
|
|
51
|
-
.option('--project-name <name>', 'Create new project or match existing by name')
|
|
52
|
-
.option('--project-id <id>', '
|
|
53
|
-
.option('--template <id>', '
|
|
68
|
+
.option('-f, --force', 'Force re-indexing even if the project is already indexed')
|
|
69
|
+
.option('--guest', 'Use guest authentication (auto-creates a temporary project)')
|
|
70
|
+
.option('--api-key <key>', 'Provide your Shift API key directly instead of interactive prompt')
|
|
71
|
+
.option('--project-name <name>', 'Create a new project or match an existing one by name')
|
|
72
|
+
.option('--project-id <id>', 'Link to an existing project by its UUID')
|
|
73
|
+
.option('--template <id>', 'Use a specific migration template when creating the project')
|
|
54
74
|
.action(async (options) => {
|
|
55
75
|
const { initCommand } = await import('./cli/commands/init.js');
|
|
56
76
|
await initCommand({
|
|
@@ -62,33 +82,122 @@ program
|
|
|
62
82
|
template: options.template,
|
|
63
83
|
});
|
|
64
84
|
});
|
|
65
|
-
|
|
85
|
+
initCmd.addHelpText('after', `
|
|
86
|
+
Details:
|
|
87
|
+
Performs a full project scan — collects the file tree, categorizes files
|
|
88
|
+
(source, config, assets), gathers git info, and sends everything to the
|
|
89
|
+
Shift backend for indexing. Automatically starts the daemon if not running.
|
|
90
|
+
|
|
91
|
+
If the project is already indexed, you will be prompted to re-index.
|
|
92
|
+
Use --force to skip the prompt.
|
|
93
|
+
|
|
94
|
+
Examples:
|
|
95
|
+
shift-cli init Interactive initialization
|
|
96
|
+
shift-cli init --force Force re-index without prompt
|
|
97
|
+
shift-cli init --guest Quick init with guest auth
|
|
98
|
+
`);
|
|
99
|
+
// --- stop ---
|
|
100
|
+
const stopCmd = program
|
|
66
101
|
.command('stop')
|
|
67
|
-
.description('Stop the Shift daemon')
|
|
102
|
+
.description('Stop the Shift daemon for this project')
|
|
68
103
|
.action(async () => {
|
|
69
104
|
const { stopCommand } = await import('./cli/commands/stop.js');
|
|
70
105
|
await stopCommand();
|
|
71
106
|
});
|
|
72
|
-
|
|
107
|
+
stopCmd.addHelpText('after', `
|
|
108
|
+
Details:
|
|
109
|
+
Terminates the background daemon process. The daemon can be
|
|
110
|
+
restarted later with "shift-cli start".
|
|
111
|
+
`);
|
|
112
|
+
// --- status ---
|
|
113
|
+
const statusCmd = program
|
|
73
114
|
.command('status')
|
|
74
115
|
.description('Show the current Shift status')
|
|
75
116
|
.action(async () => {
|
|
76
117
|
const { statusCommand } = await import('./cli/commands/status.js');
|
|
77
118
|
await statusCommand();
|
|
78
119
|
});
|
|
79
|
-
|
|
120
|
+
statusCmd.addHelpText('after', `
|
|
121
|
+
Details:
|
|
122
|
+
Displays a comprehensive overview of:
|
|
123
|
+
- API key configuration (guest or authenticated)
|
|
124
|
+
- Project details (name, ID)
|
|
125
|
+
- Registered agents
|
|
126
|
+
- Backend indexing status and file count
|
|
127
|
+
- Daemon process status (PID, WebSocket connection, uptime)
|
|
128
|
+
`);
|
|
129
|
+
// --- update-drg ---
|
|
130
|
+
const updateDrgCmd = program
|
|
80
131
|
.command('update-drg')
|
|
81
|
-
.description('Update the dependency relationship graph (DRG)
|
|
82
|
-
.option('-m, --mode <mode>', 'Update mode: baseline (all files) or incremental (git-changed only)', 'incremental')
|
|
132
|
+
.description('Update the dependency relationship graph (DRG)')
|
|
133
|
+
.option('-m, --mode <mode>', 'Update mode: "baseline" (all files) or "incremental" (git-changed only)', 'incremental')
|
|
83
134
|
.action(async (options) => {
|
|
84
135
|
const { updateDrgCommand } = await import('./cli/commands/update-drg.js');
|
|
85
136
|
await updateDrgCommand({ mode: options.mode });
|
|
86
137
|
});
|
|
87
|
-
|
|
138
|
+
updateDrgCmd.addHelpText('after', `
|
|
139
|
+
Details:
|
|
140
|
+
Scans JavaScript/TypeScript files (.js, .jsx, .ts, .tsx, .mjs, .cjs),
|
|
141
|
+
detects git changes, and sends file contents to the backend for
|
|
142
|
+
dependency analysis.
|
|
143
|
+
|
|
144
|
+
Modes:
|
|
145
|
+
incremental Send only git-changed files (default, faster)
|
|
146
|
+
baseline Send all JS/TS files (full re-analysis)
|
|
147
|
+
|
|
148
|
+
Examples:
|
|
149
|
+
shift-cli update-drg Incremental update
|
|
150
|
+
shift-cli update-drg -m baseline Full re-analysis
|
|
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
|
|
172
|
+
`);
|
|
173
|
+
// --- config ---
|
|
174
|
+
const configCmd = program
|
|
88
175
|
.command('config [action] [key] [value]')
|
|
89
176
|
.description('Manage Shift configuration (URLs, API key)')
|
|
90
177
|
.action(async (action, key, value) => {
|
|
91
178
|
const { configCommand } = await import('./cli/commands/config.js');
|
|
92
179
|
await configCommand(action, key, value);
|
|
93
180
|
});
|
|
181
|
+
configCmd.addHelpText('after', `
|
|
182
|
+
Actions:
|
|
183
|
+
show Display current configuration (default)
|
|
184
|
+
set <key> <val> Set a configuration value
|
|
185
|
+
clear [key] Clear a specific key or all configuration
|
|
186
|
+
|
|
187
|
+
Configurable keys:
|
|
188
|
+
api-key Your Shift API key
|
|
189
|
+
api-url Backend API URL (default: http://localhost:9000)
|
|
190
|
+
orch-url Orchestrator URL (default: http://localhost:9999)
|
|
191
|
+
ws-url WebSocket URL (default: ws://localhost:9999)
|
|
192
|
+
|
|
193
|
+
URLs can also be set via environment variables:
|
|
194
|
+
SHIFT_API_URL, SHIFT_ORCH_URL, SHIFT_WS_URL
|
|
195
|
+
|
|
196
|
+
Examples:
|
|
197
|
+
shift-cli config Show config
|
|
198
|
+
shift-cli config set api-key sk-abc123 Set API key
|
|
199
|
+
shift-cli config set api-url https://api.shift.ai Set API URL
|
|
200
|
+
shift-cli config clear api-key Clear API key
|
|
201
|
+
shift-cli config clear Clear all config
|
|
202
|
+
`);
|
|
94
203
|
program.parse();
|
|
@@ -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);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import ignore from 'ignore';
|
|
4
|
+
const SHIFTIGNORE_FILE = '.shiftignore';
|
|
5
|
+
/**
|
|
6
|
+
* Load and parse a .shiftignore file from the project root.
|
|
7
|
+
* Returns an `ignore` instance that can test paths, or null if no .shiftignore exists.
|
|
8
|
+
*
|
|
9
|
+
* .shiftignore uses the same syntax as .gitignore:
|
|
10
|
+
* - Blank lines are ignored
|
|
11
|
+
* - Lines starting with # are comments
|
|
12
|
+
* - Standard glob patterns (*, **, ?)
|
|
13
|
+
* - Trailing / matches directories only
|
|
14
|
+
* - Leading / anchors to root
|
|
15
|
+
* - ! negates a pattern
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_SHIFTIGNORE = `# .shiftignore — files and directories to exclude from Shift indexing
|
|
18
|
+
# Uses the same syntax as .gitignore
|
|
19
|
+
|
|
20
|
+
# Dependencies
|
|
21
|
+
node_modules/
|
|
22
|
+
vendor/
|
|
23
|
+
bower_components/
|
|
24
|
+
|
|
25
|
+
# Build output
|
|
26
|
+
dist/
|
|
27
|
+
build/
|
|
28
|
+
out/
|
|
29
|
+
.next/
|
|
30
|
+
|
|
31
|
+
# Test & coverage
|
|
32
|
+
coverage/
|
|
33
|
+
.nyc_output/
|
|
34
|
+
|
|
35
|
+
# Environment & secrets
|
|
36
|
+
.env
|
|
37
|
+
.env.*
|
|
38
|
+
*.pem
|
|
39
|
+
*.key
|
|
40
|
+
|
|
41
|
+
# Logs
|
|
42
|
+
*.log
|
|
43
|
+
logs/
|
|
44
|
+
|
|
45
|
+
# OS files
|
|
46
|
+
.DS_Store
|
|
47
|
+
Thumbs.db
|
|
48
|
+
|
|
49
|
+
# IDE
|
|
50
|
+
.vscode/
|
|
51
|
+
.idea/
|
|
52
|
+
*.swp
|
|
53
|
+
*.swo
|
|
54
|
+
|
|
55
|
+
# Python
|
|
56
|
+
__pycache__/
|
|
57
|
+
*.pyc
|
|
58
|
+
venv/
|
|
59
|
+
.venv/
|
|
60
|
+
|
|
61
|
+
# Misc
|
|
62
|
+
*.min.js
|
|
63
|
+
*.min.css
|
|
64
|
+
*.map
|
|
65
|
+
`;
|
|
66
|
+
/**
|
|
67
|
+
* Create a default .shiftignore file in the project root if one doesn't exist.
|
|
68
|
+
* Returns true if a new file was created, false if it already exists.
|
|
69
|
+
*/
|
|
70
|
+
export function scaffoldShiftIgnore(projectRoot) {
|
|
71
|
+
const ignorePath = path.join(projectRoot, SHIFTIGNORE_FILE);
|
|
72
|
+
if (fs.existsSync(ignorePath)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
fs.writeFileSync(ignorePath, DEFAULT_SHIFTIGNORE, 'utf-8');
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
console.warn(`[ShiftIgnore] Warning: Could not create ${SHIFTIGNORE_FILE}: ${err.message}`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function loadShiftIgnore(projectRoot) {
|
|
85
|
+
const ignorePath = path.join(projectRoot, SHIFTIGNORE_FILE);
|
|
86
|
+
if (!fs.existsSync(ignorePath)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const content = fs.readFileSync(ignorePath, 'utf-8');
|
|
91
|
+
const ig = ignore();
|
|
92
|
+
ig.add(content);
|
|
93
|
+
return ig;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
console.warn(`[ShiftIgnore] Warning: Could not read ${SHIFTIGNORE_FILE}: ${err.message}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Filter an array of file paths through the shiftIgnore rules.
|
|
102
|
+
* Returns only paths that are NOT ignored.
|
|
103
|
+
*/
|
|
104
|
+
export function filterPaths(ig, paths) {
|
|
105
|
+
if (!ig)
|
|
106
|
+
return paths;
|
|
107
|
+
return paths.filter(p => !ig.ignores(p.replace(/\\/g, '/')));
|
|
108
|
+
}
|
|
@@ -34,7 +34,7 @@ export function getProjectTree(workspaceRoot, options = {}) {
|
|
|
34
34
|
try {
|
|
35
35
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
36
36
|
for (const entry of entries) {
|
|
37
|
-
// Check if should be excluded
|
|
37
|
+
// Check if should be excluded by built-in patterns
|
|
38
38
|
if (exclude_patterns.some(pattern => entry.name.includes(pattern))) {
|
|
39
39
|
continue;
|
|
40
40
|
}
|
|
@@ -114,6 +114,23 @@ export function extractAllFilePaths(tree, basePath = '') {
|
|
|
114
114
|
* Categorize files by type
|
|
115
115
|
* Matching extension's categorizeFiles function
|
|
116
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
|
+
}
|
|
117
134
|
export function categorizeFiles(tree, basePath = '') {
|
|
118
135
|
const categories = {
|
|
119
136
|
source_files: [],
|