@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.
@@ -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 indexing status (work done)
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: ✓ Complete (${projectStatus.file_count} files)`);
50
+ console.log(`Indexed: ✓ ${projectStatus.file_count} files`);
43
51
  }
44
52
  else {
45
53
  console.log('Indexed: ❌ Not indexed');
46
- console.log(' Run "shift-cli init" to scan the project.');
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('Indexed: ⚠️ Unable to check (server unavailable)');
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
- let file_count = 0;
310
- let dir_count = 0;
311
- let total_size = 0;
312
- const max_depth = depth === 0 ? Infinity : depth;
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: total_size,
376
- total_size_mb: total_size_mb,
377
- scanned_from: this.workspaceRoot,
378
- depth_limit: depth,
379
- actual_max_depth: depth === 0 ? 'unlimited' : depth,
380
- excluded_patterns: exclude_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
- .version('1.0.6');
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
- // CLI commands
24
- program
35
+ // --- start ---
36
+ const startCmd = program
25
37
  .command('start')
26
38
  .description('Start the Shift daemon for this project')
27
- .action(async () => {
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
- program
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({ force: options.force ?? false });
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
- program
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
- program
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
- program
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();