@latentforce/shift 1.0.8 → 1.0.10
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 +198 -26
- package/build/cli/commands/init.js +90 -58
- package/build/cli/commands/start.js +36 -68
- package/build/cli/commands/status.js +17 -7
- package/build/cli/commands/update-drg.js +188 -0
- package/build/daemon/tools-executor.js +19 -86
- package/build/index.js +144 -15
- package/build/mcp-server.js +105 -8
- package/build/utils/api-client.js +70 -35
- package/build/utils/auth-resolver.js +184 -0
- package/build/utils/prompts.js +45 -0
- package/build/utils/shiftignore.js +108 -0
- package/build/utils/tree-scanner.js +11 -2
- package/package.json +2 -1
|
@@ -10,7 +10,7 @@ export async function statusCommand() {
|
|
|
10
10
|
const apiKey = getApiKey();
|
|
11
11
|
if (!apiKey) {
|
|
12
12
|
console.log('API Key: ❌ Not configured');
|
|
13
|
-
console.log('\nRun "shift start" to configure your API key.\n');
|
|
13
|
+
console.log('\nRun "shift-cli start" to configure your API key.\n');
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
if (isGuestKey()) {
|
|
@@ -23,7 +23,7 @@ export async function statusCommand() {
|
|
|
23
23
|
const projectConfig = readProjectConfig(projectRoot);
|
|
24
24
|
if (!projectConfig) {
|
|
25
25
|
console.log('Project: ❌ Not configured');
|
|
26
|
-
console.log('\nRun "shift start" to configure this project.\n');
|
|
26
|
+
console.log('\nRun "shift-cli start" to configure this project.\n');
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
console.log(`Project: ${projectConfig.project_name}`);
|
|
@@ -35,25 +35,35 @@ export async function statusCommand() {
|
|
|
35
35
|
console.log(` - ${agent.agent_name} (${agent.agent_type})`);
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
-
// Check project
|
|
38
|
+
// Check project status from backend
|
|
39
39
|
try {
|
|
40
40
|
const projectStatus = await fetchProjectStatus(apiKey, projectConfig.project_id);
|
|
41
|
+
// Init scan status
|
|
42
|
+
const scanStatus = projectStatus.init_scan_status ?? 'not_started';
|
|
43
|
+
const scanLabel = scanStatus === 'completed' ? '✓ Complete'
|
|
44
|
+
: scanStatus === 'in_progress' ? '⏳ In progress'
|
|
45
|
+
: scanStatus === 'failed' ? '❌ Failed'
|
|
46
|
+
: '❌ Not started';
|
|
47
|
+
console.log(`Init Scan: ${scanLabel}`);
|
|
48
|
+
// Indexed
|
|
41
49
|
if (projectStatus.indexed) {
|
|
42
|
-
console.log(`Indexed: ✓
|
|
50
|
+
console.log(`Indexed: ✓ ${projectStatus.file_count} files`);
|
|
43
51
|
}
|
|
44
52
|
else {
|
|
45
53
|
console.log('Indexed: ❌ Not indexed');
|
|
46
|
-
|
|
54
|
+
if (scanStatus === 'not_started') {
|
|
55
|
+
console.log(' Run "shift-cli init" to scan the project.');
|
|
56
|
+
}
|
|
47
57
|
}
|
|
48
58
|
}
|
|
49
59
|
catch {
|
|
50
|
-
console.log('
|
|
60
|
+
console.log('Status: ⚠️ Unable to check (server unavailable)');
|
|
51
61
|
}
|
|
52
62
|
// Check daemon status
|
|
53
63
|
const status = getDaemonStatus(projectRoot);
|
|
54
64
|
if (!status.running) {
|
|
55
65
|
console.log('Daemon: ❌ Not running');
|
|
56
|
-
console.log('\nRun "shift start" to start the daemon.\n');
|
|
66
|
+
console.log('\nRun "shift-cli start" to start the daemon.\n');
|
|
57
67
|
return;
|
|
58
68
|
}
|
|
59
69
|
console.log(`Daemon: ✓ Running (PID: ${status.pid})`);
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { getApiKey, readProjectConfig } from '../../utils/config.js';
|
|
6
|
+
import { getProjectTree, extractAllFilePaths } from '../../utils/tree-scanner.js';
|
|
7
|
+
import { loadShiftIgnore, filterPaths } from '../../utils/shiftignore.js';
|
|
8
|
+
import { sendUpdateDrg } from '../../utils/api-client.js';
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
async function detectGitChanges(projectRoot, extensions) {
|
|
11
|
+
const changes = { added: [], modified: [], deleted: [] };
|
|
12
|
+
try {
|
|
13
|
+
const { stdout } = await execAsync('git status --porcelain', { cwd: projectRoot });
|
|
14
|
+
for (const line of stdout.split('\n')) {
|
|
15
|
+
if (!line.trim())
|
|
16
|
+
continue;
|
|
17
|
+
const statusCode = line.substring(0, 2);
|
|
18
|
+
let filePath = line.substring(3).trim();
|
|
19
|
+
// Handle renamed files (R status shows "old -> new")
|
|
20
|
+
if (filePath.includes(' -> ')) {
|
|
21
|
+
const parts = filePath.split(' -> ');
|
|
22
|
+
// Mark old path as deleted, new path as added
|
|
23
|
+
const oldPath = parts[0].trim();
|
|
24
|
+
const newPath = parts[1].trim();
|
|
25
|
+
const oldExt = path.extname(oldPath).toLowerCase();
|
|
26
|
+
const newExt = path.extname(newPath).toLowerCase();
|
|
27
|
+
if (extensions.has(oldExt))
|
|
28
|
+
changes.deleted.push(oldPath);
|
|
29
|
+
if (extensions.has(newExt))
|
|
30
|
+
changes.added.push(newPath);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Filter by extensions
|
|
34
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
35
|
+
if (!extensions.has(ext))
|
|
36
|
+
continue;
|
|
37
|
+
// Categorize based on git status codes
|
|
38
|
+
if (statusCode === '??' || statusCode.trimEnd() === 'A') {
|
|
39
|
+
changes.added.push(filePath);
|
|
40
|
+
}
|
|
41
|
+
else if (statusCode.includes('D')) {
|
|
42
|
+
changes.deleted.push(filePath);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// M, MM, AM, etc. — treat as modified
|
|
46
|
+
changes.modified.push(filePath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Not a git repo or git not available
|
|
52
|
+
}
|
|
53
|
+
return changes;
|
|
54
|
+
}
|
|
55
|
+
const JS_TS_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
|
|
56
|
+
export async function updateDrgCommand(options = {}) {
|
|
57
|
+
const projectRoot = process.cwd();
|
|
58
|
+
const mode = options.mode || 'incremental';
|
|
59
|
+
console.log('╔═══════════════════════════════════════════════╗');
|
|
60
|
+
console.log('║ Updating Dependency Graph (DRG) ║');
|
|
61
|
+
console.log('╚═══════════════════════════════════════════════╝\n');
|
|
62
|
+
// Step 1: Check API key
|
|
63
|
+
console.log('[DRG] Step 1/4: Checking API key...');
|
|
64
|
+
const apiKey = getApiKey();
|
|
65
|
+
if (!apiKey) {
|
|
66
|
+
console.error('\n❌ No API key found. Run "shift-cli init" first to configure your API key.\n');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
console.log('[DRG] ✓ API key found\n');
|
|
70
|
+
// Step 2: Read project config
|
|
71
|
+
console.log('[DRG] Step 2/4: Reading project configuration...');
|
|
72
|
+
const projectConfig = readProjectConfig(projectRoot);
|
|
73
|
+
if (!projectConfig) {
|
|
74
|
+
console.error('\n❌ No project configured for this directory.');
|
|
75
|
+
console.log('Run "shift-cli start" first to configure the project.\n');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
console.log(`[DRG] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
|
|
79
|
+
// Step 3: Scan project for JS/TS files
|
|
80
|
+
console.log('[DRG] Step 3/4: Scanning project for JS/TS files...');
|
|
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
|
+
}
|
|
88
|
+
const treeData = getProjectTree(projectRoot, {
|
|
89
|
+
depth: 0,
|
|
90
|
+
exclude_patterns: [
|
|
91
|
+
'.git',
|
|
92
|
+
'node_modules',
|
|
93
|
+
'__pycache__',
|
|
94
|
+
'.vscode',
|
|
95
|
+
'dist',
|
|
96
|
+
'build',
|
|
97
|
+
'.shift',
|
|
98
|
+
'.next',
|
|
99
|
+
'coverage',
|
|
100
|
+
'venv',
|
|
101
|
+
'env',
|
|
102
|
+
],
|
|
103
|
+
shiftIgnore,
|
|
104
|
+
});
|
|
105
|
+
const allFiles = extractAllFilePaths(treeData.tree);
|
|
106
|
+
const jstsFiles = allFiles.filter((filePath) => {
|
|
107
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
108
|
+
return JS_TS_EXTENSIONS.has(ext);
|
|
109
|
+
});
|
|
110
|
+
console.log(`[DRG] Total files scanned: ${allFiles.length}`);
|
|
111
|
+
console.log(`[DRG] JS/TS files found: ${jstsFiles.length}\n`);
|
|
112
|
+
if (jstsFiles.length === 0) {
|
|
113
|
+
console.log('⚠️ No JS/TS files found in project. Nothing to update.\n');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Read file contents
|
|
117
|
+
const files = [];
|
|
118
|
+
let readErrors = 0;
|
|
119
|
+
for (const filePath of jstsFiles) {
|
|
120
|
+
const absolutePath = path.join(projectRoot, filePath);
|
|
121
|
+
try {
|
|
122
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
123
|
+
files.push({ file_path: filePath, content });
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
readErrors++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (readErrors > 0) {
|
|
130
|
+
console.log(`[DRG] ⚠️ Could not read ${readErrors} file(s)\n`);
|
|
131
|
+
}
|
|
132
|
+
// Always use git to compute added/modified/deleted (single source of truth)
|
|
133
|
+
console.log('[DRG] Detecting git changes...');
|
|
134
|
+
const gitChanges = await detectGitChanges(projectRoot, JS_TS_EXTENSIONS);
|
|
135
|
+
// Filter git changes through .shiftignore
|
|
136
|
+
gitChanges.added = filterPaths(shiftIgnore, gitChanges.added);
|
|
137
|
+
gitChanges.modified = filterPaths(shiftIgnore, gitChanges.modified);
|
|
138
|
+
gitChanges.deleted = filterPaths(shiftIgnore, gitChanges.deleted);
|
|
139
|
+
console.log(`[DRG] Git changes — added: ${gitChanges.added.length}, modified: ${gitChanges.modified.length}, deleted: ${gitChanges.deleted.length}`);
|
|
140
|
+
// Which files to send: incremental = only changed; baseline = all (full re-analysis)
|
|
141
|
+
const changedPaths = new Set([...gitChanges.added, ...gitChanges.modified]);
|
|
142
|
+
const filesToSend = mode === 'baseline'
|
|
143
|
+
? files
|
|
144
|
+
: files.filter((f) => changedPaths.has(f.file_path));
|
|
145
|
+
if (mode === 'baseline') {
|
|
146
|
+
console.log(`[DRG] Baseline: sending all ${filesToSend.length} JS/TS files.\n`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.log(`[DRG] Incremental: sending ${filesToSend.length} changed file(s).\n`);
|
|
150
|
+
}
|
|
151
|
+
if (filesToSend.length === 0 && gitChanges.deleted.length === 0) {
|
|
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)`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
console.log('\nUse "shift-cli status" to check progress.\n');
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
console.error(`\n❌ Failed to update DRG: ${error.message}`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -6,6 +6,7 @@ import * as fs from 'fs/promises';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { exec } from 'child_process';
|
|
8
8
|
import { promisify } from 'util';
|
|
9
|
+
import { getProjectTree as getProjectTreeSync } from '../utils/tree-scanner.js';
|
|
9
10
|
const execAsync = promisify(exec);
|
|
10
11
|
export class ToolsExecutor {
|
|
11
12
|
wsClient;
|
|
@@ -285,99 +286,31 @@ export class ToolsExecutor {
|
|
|
285
286
|
};
|
|
286
287
|
}
|
|
287
288
|
/**
|
|
288
|
-
* Get project tree
|
|
289
|
+
* Get project tree — delegates to shared tree-scanner utility
|
|
289
290
|
*/
|
|
290
291
|
async getProjectTree(params) {
|
|
291
|
-
const { depth = 0, exclude_patterns =
|
|
292
|
-
'.git',
|
|
293
|
-
'node_modules',
|
|
294
|
-
'__pycache__',
|
|
295
|
-
'.vscode',
|
|
296
|
-
'dist',
|
|
297
|
-
'build',
|
|
298
|
-
'.shift',
|
|
299
|
-
'.next',
|
|
300
|
-
'.cache',
|
|
301
|
-
'coverage',
|
|
302
|
-
'.pytest_cache',
|
|
303
|
-
'venv',
|
|
304
|
-
'env',
|
|
305
|
-
'.env'
|
|
306
|
-
] } = params;
|
|
292
|
+
const { depth = 0, exclude_patterns } = params;
|
|
307
293
|
console.log(`[ToolsExecutor] Getting project tree from workspace root: ${this.workspaceRoot}`);
|
|
308
294
|
console.log(`[ToolsExecutor] Depth limit: ${depth === 0 ? 'unlimited' : depth}`);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const scanDirectory = async (dirPath, currentDepth = 0, relativePath = '') => {
|
|
314
|
-
if (currentDepth >= max_depth) {
|
|
315
|
-
return [];
|
|
316
|
-
}
|
|
317
|
-
const items = [];
|
|
318
|
-
try {
|
|
319
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
320
|
-
for (const entry of entries) {
|
|
321
|
-
if (exclude_patterns.some((pattern) => entry.name.includes(pattern))) {
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
const itemPath = path.join(dirPath, entry.name);
|
|
325
|
-
const itemRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
326
|
-
if (entry.isDirectory()) {
|
|
327
|
-
dir_count++;
|
|
328
|
-
const children = await scanDirectory(itemPath, currentDepth + 1, itemRelativePath);
|
|
329
|
-
items.push({
|
|
330
|
-
name: entry.name,
|
|
331
|
-
type: 'directory',
|
|
332
|
-
path: itemRelativePath,
|
|
333
|
-
depth: currentDepth,
|
|
334
|
-
children: children
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
else if (entry.isFile()) {
|
|
338
|
-
file_count++;
|
|
339
|
-
try {
|
|
340
|
-
const stats = await fs.stat(itemPath);
|
|
341
|
-
total_size += stats.size;
|
|
342
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
343
|
-
items.push({
|
|
344
|
-
name: entry.name,
|
|
345
|
-
type: 'file',
|
|
346
|
-
path: itemRelativePath,
|
|
347
|
-
depth: currentDepth,
|
|
348
|
-
size: stats.size,
|
|
349
|
-
extension: ext,
|
|
350
|
-
modified: stats.mtime
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
console.warn(`[ToolsExecutor] Cannot read: ${itemPath}`);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
catch (err) {
|
|
360
|
-
console.error(`[ToolsExecutor] Error scanning ${dirPath}:`, err.message);
|
|
361
|
-
}
|
|
362
|
-
return items;
|
|
363
|
-
};
|
|
364
|
-
const tree = await scanDirectory(this.workspaceRoot, 0, '');
|
|
365
|
-
const total_size_mb = (total_size / (1024 * 1024)).toFixed(2);
|
|
295
|
+
const result = getProjectTreeSync(this.workspaceRoot, {
|
|
296
|
+
depth,
|
|
297
|
+
...(exclude_patterns && { exclude_patterns }),
|
|
298
|
+
});
|
|
366
299
|
console.log(`[ToolsExecutor] ✓ Scanned project tree:`);
|
|
367
|
-
console.log(`[ToolsExecutor] Files: ${file_count}`);
|
|
368
|
-
console.log(`[ToolsExecutor] Directories: ${dir_count}`);
|
|
369
|
-
console.log(`[ToolsExecutor] Total size: ${total_size_mb} MB`);
|
|
300
|
+
console.log(`[ToolsExecutor] Files: ${result.file_count}`);
|
|
301
|
+
console.log(`[ToolsExecutor] Directories: ${result.dir_count}`);
|
|
302
|
+
console.log(`[ToolsExecutor] Total size: ${result.total_size_mb} MB`);
|
|
370
303
|
return {
|
|
371
304
|
status: 'success',
|
|
372
|
-
tree: tree,
|
|
373
|
-
file_count: file_count,
|
|
374
|
-
dir_count: dir_count,
|
|
375
|
-
total_size_bytes:
|
|
376
|
-
total_size_mb: total_size_mb,
|
|
377
|
-
scanned_from:
|
|
378
|
-
depth_limit:
|
|
379
|
-
actual_max_depth:
|
|
380
|
-
excluded_patterns:
|
|
305
|
+
tree: result.tree,
|
|
306
|
+
file_count: result.file_count,
|
|
307
|
+
dir_count: result.dir_count,
|
|
308
|
+
total_size_bytes: result.total_size_bytes,
|
|
309
|
+
total_size_mb: result.total_size_mb,
|
|
310
|
+
scanned_from: result.scanned_from,
|
|
311
|
+
depth_limit: result.depth_limit,
|
|
312
|
+
actual_max_depth: result.actual_max_depth,
|
|
313
|
+
excluded_patterns: result.excluded_patterns,
|
|
381
314
|
};
|
|
382
315
|
}
|
|
383
316
|
}
|
package/build/index.js
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const { version } = require('../package.json');
|
|
3
6
|
const program = new Command();
|
|
4
7
|
program
|
|
5
8
|
.name('shift-cli')
|
|
6
|
-
.description('Shift CLI - AI-powered code intelligence'
|
|
7
|
-
|
|
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\n' +
|
|
20
|
+
' Ignore rules: .shiftignore (auto-created on first run)')
|
|
21
|
+
.version(version);
|
|
8
22
|
// MCP server mode (default when run via MCP host)
|
|
9
23
|
program
|
|
10
24
|
.command('mcp', { isDefault: true, hidden: true })
|
|
11
25
|
.description('Start MCP server on stdio')
|
|
12
26
|
.action(async () => {
|
|
13
|
-
// Check if 'mcp' was explicitly passed as argument
|
|
14
27
|
const mcpExplicitlyRequested = process.argv.includes('mcp');
|
|
15
|
-
// If running interactively (TTY) and mcp wasn't explicitly requested, show help
|
|
16
28
|
if (process.stdin.isTTY && !mcpExplicitlyRequested) {
|
|
17
29
|
program.outputHelp();
|
|
18
30
|
return;
|
|
@@ -20,41 +32,158 @@ program
|
|
|
20
32
|
const { startMcpServer } = await import('./mcp-server.js');
|
|
21
33
|
await startMcpServer();
|
|
22
34
|
});
|
|
23
|
-
//
|
|
24
|
-
program
|
|
35
|
+
// --- start ---
|
|
36
|
+
const startCmd = program
|
|
25
37
|
.command('start')
|
|
26
38
|
.description('Start the Shift daemon for this project')
|
|
27
|
-
.
|
|
39
|
+
.option('--guest', 'Use guest authentication (auto-creates a temporary project)')
|
|
40
|
+
.option('--api-key <key>', 'Provide your Shift API key directly instead of interactive prompt')
|
|
41
|
+
.option('--project-name <name>', 'Create a new project or match an existing one by name')
|
|
42
|
+
.option('--project-id <id>', 'Link to an existing project by its UUID')
|
|
43
|
+
.option('--template <id>', 'Use a specific migration template when creating the project')
|
|
44
|
+
.action(async (options) => {
|
|
28
45
|
const { startCommand } = await import('./cli/commands/start.js');
|
|
29
|
-
await startCommand(
|
|
46
|
+
await startCommand({
|
|
47
|
+
guest: options.guest,
|
|
48
|
+
apiKey: options.apiKey,
|
|
49
|
+
projectName: options.projectName,
|
|
50
|
+
projectId: options.projectId,
|
|
51
|
+
template: options.template,
|
|
52
|
+
});
|
|
30
53
|
});
|
|
31
|
-
|
|
54
|
+
startCmd.addHelpText('after', `
|
|
55
|
+
Details:
|
|
56
|
+
Resolves authentication, configures the project, and launches a
|
|
57
|
+
background daemon that maintains a WebSocket connection to the
|
|
58
|
+
Shift backend. Creates a default .shiftignore if one doesn't exist.
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
shift-cli start Interactive setup
|
|
62
|
+
shift-cli start --guest Quick start without API key
|
|
63
|
+
shift-cli start --api-key <key> --project-name "My App"
|
|
64
|
+
`);
|
|
65
|
+
// --- init ---
|
|
66
|
+
const initCmd = program
|
|
32
67
|
.command('init')
|
|
33
68
|
.description('Initialize and scan the project for file indexing')
|
|
34
|
-
.option('-f, --force', 'Force re-indexing even if project is already indexed')
|
|
69
|
+
.option('-f, --force', 'Force re-indexing even if the project is already indexed')
|
|
70
|
+
.option('--guest', 'Use guest authentication (auto-creates a temporary project)')
|
|
71
|
+
.option('--api-key <key>', 'Provide your Shift API key directly instead of interactive prompt')
|
|
72
|
+
.option('--project-name <name>', 'Create a new project or match an existing one by name')
|
|
73
|
+
.option('--project-id <id>', 'Link to an existing project by its UUID')
|
|
74
|
+
.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)')
|
|
35
76
|
.action(async (options) => {
|
|
36
77
|
const { initCommand } = await import('./cli/commands/init.js');
|
|
37
|
-
await initCommand({
|
|
78
|
+
await initCommand({
|
|
79
|
+
force: options.force ?? false,
|
|
80
|
+
guest: options.guest,
|
|
81
|
+
apiKey: options.apiKey,
|
|
82
|
+
projectName: options.projectName,
|
|
83
|
+
projectId: options.projectId,
|
|
84
|
+
template: options.template,
|
|
85
|
+
noIgnore: options.noIgnore,
|
|
86
|
+
});
|
|
38
87
|
});
|
|
39
|
-
|
|
88
|
+
initCmd.addHelpText('after', `
|
|
89
|
+
Details:
|
|
90
|
+
Performs a full project scan — collects the file tree, categorizes files
|
|
91
|
+
(source, config, assets), gathers git info, and sends everything to the
|
|
92
|
+
Shift backend for indexing. Automatically starts the daemon if not running.
|
|
93
|
+
Creates a default .shiftignore if one doesn't exist.
|
|
94
|
+
|
|
95
|
+
If the project is already indexed, you will be prompted to re-index.
|
|
96
|
+
Use --force to skip the prompt.
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
shift-cli init Interactive initialization
|
|
100
|
+
shift-cli init --force Force re-index without prompt
|
|
101
|
+
shift-cli init --no-ignore Index all files, ignore .shiftignore
|
|
102
|
+
shift-cli init --guest Quick init with guest auth
|
|
103
|
+
`);
|
|
104
|
+
// --- stop ---
|
|
105
|
+
const stopCmd = program
|
|
40
106
|
.command('stop')
|
|
41
|
-
.description('Stop the Shift daemon')
|
|
107
|
+
.description('Stop the Shift daemon for this project')
|
|
42
108
|
.action(async () => {
|
|
43
109
|
const { stopCommand } = await import('./cli/commands/stop.js');
|
|
44
110
|
await stopCommand();
|
|
45
111
|
});
|
|
46
|
-
|
|
112
|
+
stopCmd.addHelpText('after', `
|
|
113
|
+
Details:
|
|
114
|
+
Terminates the background daemon process. The daemon can be
|
|
115
|
+
restarted later with "shift-cli start".
|
|
116
|
+
`);
|
|
117
|
+
// --- status ---
|
|
118
|
+
const statusCmd = program
|
|
47
119
|
.command('status')
|
|
48
120
|
.description('Show the current Shift status')
|
|
49
121
|
.action(async () => {
|
|
50
122
|
const { statusCommand } = await import('./cli/commands/status.js');
|
|
51
123
|
await statusCommand();
|
|
52
124
|
});
|
|
53
|
-
|
|
125
|
+
statusCmd.addHelpText('after', `
|
|
126
|
+
Details:
|
|
127
|
+
Displays a comprehensive overview of:
|
|
128
|
+
- API key configuration (guest or authenticated)
|
|
129
|
+
- Project details (name, ID)
|
|
130
|
+
- Registered agents
|
|
131
|
+
- Backend indexing status and file count
|
|
132
|
+
- Daemon process status (PID, WebSocket connection, uptime)
|
|
133
|
+
`);
|
|
134
|
+
// --- update-drg ---
|
|
135
|
+
const updateDrgCmd = program
|
|
136
|
+
.command('update-drg')
|
|
137
|
+
.description('Update the dependency relationship graph (DRG)')
|
|
138
|
+
.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
|
+
.action(async (options) => {
|
|
141
|
+
const { updateDrgCommand } = await import('./cli/commands/update-drg.js');
|
|
142
|
+
await updateDrgCommand({ mode: options.mode, noIgnore: options.noIgnore });
|
|
143
|
+
});
|
|
144
|
+
updateDrgCmd.addHelpText('after', `
|
|
145
|
+
Details:
|
|
146
|
+
Scans JavaScript/TypeScript files (.js, .jsx, .ts, .tsx, .mjs, .cjs),
|
|
147
|
+
detects git changes, and sends file contents to the backend for
|
|
148
|
+
dependency analysis. Respects .shiftignore rules.
|
|
149
|
+
|
|
150
|
+
Modes:
|
|
151
|
+
incremental Send only git-changed files (default, faster)
|
|
152
|
+
baseline Send all JS/TS files (full re-analysis)
|
|
153
|
+
|
|
154
|
+
Examples:
|
|
155
|
+
shift-cli update-drg Incremental update
|
|
156
|
+
shift-cli update-drg -m baseline Full re-analysis
|
|
157
|
+
shift-cli update-drg --no-ignore Include ignored files
|
|
158
|
+
`);
|
|
159
|
+
// --- config ---
|
|
160
|
+
const configCmd = program
|
|
54
161
|
.command('config [action] [key] [value]')
|
|
55
162
|
.description('Manage Shift configuration (URLs, API key)')
|
|
56
163
|
.action(async (action, key, value) => {
|
|
57
164
|
const { configCommand } = await import('./cli/commands/config.js');
|
|
58
165
|
await configCommand(action, key, value);
|
|
59
166
|
});
|
|
167
|
+
configCmd.addHelpText('after', `
|
|
168
|
+
Actions:
|
|
169
|
+
show Display current configuration (default)
|
|
170
|
+
set <key> <val> Set a configuration value
|
|
171
|
+
clear [key] Clear a specific key or all configuration
|
|
172
|
+
|
|
173
|
+
Configurable keys:
|
|
174
|
+
api-key Your Shift API key
|
|
175
|
+
api-url Backend API URL (default: http://localhost:9000)
|
|
176
|
+
orch-url Orchestrator URL (default: http://localhost:9999)
|
|
177
|
+
ws-url WebSocket URL (default: ws://localhost:9999)
|
|
178
|
+
|
|
179
|
+
URLs can also be set via environment variables:
|
|
180
|
+
SHIFT_API_URL, SHIFT_ORCH_URL, SHIFT_WS_URL
|
|
181
|
+
|
|
182
|
+
Examples:
|
|
183
|
+
shift-cli config Show config
|
|
184
|
+
shift-cli config set api-key sk-abc123 Set API key
|
|
185
|
+
shift-cli config set api-url https://api.shift.ai Set API URL
|
|
186
|
+
shift-cli config clear api-key Clear API key
|
|
187
|
+
shift-cli config clear Clear all config
|
|
188
|
+
`);
|
|
60
189
|
program.parse();
|