@latentforce/shift 1.0.10 → 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 +29 -14
- 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
|
@@ -40,19 +40,25 @@ shift-cli init # Index project files
|
|
|
40
40
|
|
|
41
41
|
## MCP Integration
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
After initializing your project, use `shift-cli add` to configure Shift's MCP server in your AI coding tool:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
46
|
+
shift-cli add <tool>
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
### Supported tools
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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` |
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
For CLI-based tools, if the CLI is not installed, the command will print manual setup instructions.
|
|
60
|
+
|
|
61
|
+
### Claude Desktop (manual)
|
|
56
62
|
|
|
57
63
|
Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Windows, `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
58
64
|
|
|
@@ -77,13 +83,14 @@ Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Window
|
|
|
77
83
|
| `shift-cli start` | Start the daemon and configure the project |
|
|
78
84
|
| `shift-cli init` | Scan and index project files |
|
|
79
85
|
| `shift-cli update-drg` | Update the dependency relationship graph |
|
|
86
|
+
| `shift-cli add <tool>` | Add Shift MCP server to an AI coding tool |
|
|
80
87
|
| `shift-cli stop` | Stop the daemon |
|
|
81
88
|
| `shift-cli status` | Show current status |
|
|
82
89
|
| `shift-cli config` | Manage configuration |
|
|
83
90
|
|
|
84
91
|
### `shift-cli init`
|
|
85
92
|
|
|
86
|
-
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
|
|
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.
|
|
87
94
|
|
|
88
95
|
If the project is already indexed, you will be prompted to re-index. Use `--force` to skip the prompt.
|
|
89
96
|
|
|
@@ -95,18 +102,16 @@ If the project is already indexed, you will be prompted to re-index. Use `--forc
|
|
|
95
102
|
| `--project-id <id>` | Use existing project UUID |
|
|
96
103
|
| `--template <id>` | Migration template ID for project creation |
|
|
97
104
|
| `-f, --force` | Force re-indexing even if already indexed |
|
|
98
|
-
| `--no-ignore` | Skip `.shiftignore` rules for this run (send all files) |
|
|
99
105
|
|
|
100
106
|
```bash
|
|
101
107
|
shift-cli init # Interactive initialization
|
|
102
108
|
shift-cli init --force # Force re-index without prompt
|
|
103
|
-
shift-cli init --no-ignore # Index all files, skip .shiftignore
|
|
104
109
|
shift-cli init --guest # Quick init with guest auth
|
|
105
110
|
```
|
|
106
111
|
|
|
107
112
|
### `shift-cli start`
|
|
108
113
|
|
|
109
|
-
Resolves authentication, configures the project, and launches a background daemon that maintains a WebSocket connection to the Shift backend.
|
|
114
|
+
Resolves authentication, configures the project, and launches a background daemon that maintains a WebSocket connection to the Shift backend.
|
|
110
115
|
|
|
111
116
|
| Flag | Description |
|
|
112
117
|
|------|-------------|
|
|
@@ -124,12 +129,11 @@ shift-cli start --api-key <key> --project-name "My App"
|
|
|
124
129
|
|
|
125
130
|
### `shift-cli update-drg`
|
|
126
131
|
|
|
127
|
-
Scans JavaScript/TypeScript files (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`), detects git changes, and sends file contents to the backend for dependency analysis.
|
|
132
|
+
Scans JavaScript/TypeScript files (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`), detects git changes, and sends file contents to the backend for dependency analysis.
|
|
128
133
|
|
|
129
134
|
| Flag | Description |
|
|
130
135
|
|------|-------------|
|
|
131
136
|
| `-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
137
|
|
|
134
138
|
**Modes:**
|
|
135
139
|
- **incremental** (default) — sends only git-changed files for faster updates
|
|
@@ -138,7 +142,18 @@ Scans JavaScript/TypeScript files (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`)
|
|
|
138
142
|
```bash
|
|
139
143
|
shift-cli update-drg # Incremental update (default)
|
|
140
144
|
shift-cli update-drg -m baseline # Full re-analysis
|
|
141
|
-
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `shift-cli add <tool>`
|
|
148
|
+
|
|
149
|
+
Configures Shift's MCP server in the specified AI coding tool. Reads `project_id` from `.shift/config.json` (run `shift-cli init` first).
|
|
150
|
+
|
|
151
|
+
```bash
|
|
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
|
|
142
157
|
```
|
|
143
158
|
|
|
144
159
|
### `shift-cli config`
|
|
@@ -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.11",
|
|
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
|
},
|