@latentforce/latentgraph 1.0.0
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 +320 -0
- package/build/cli/commands/add.js +350 -0
- package/build/cli/commands/config.js +142 -0
- package/build/cli/commands/init.js +285 -0
- package/build/cli/commands/start.js +65 -0
- package/build/cli/commands/status.js +76 -0
- package/build/cli/commands/stop.js +18 -0
- package/build/cli/commands/update-drg.js +194 -0
- package/build/daemon/daemon-manager.js +136 -0
- package/build/daemon/daemon.js +119 -0
- package/build/daemon/tools-executor.js +462 -0
- package/build/daemon/websocket-client.js +334 -0
- package/build/index.js +205 -0
- package/build/mcp-server.js +484 -0
- package/build/utils/api-client.js +147 -0
- package/build/utils/auth-resolver.js +184 -0
- package/build/utils/config.js +260 -0
- package/build/utils/machine-id.js +46 -0
- package/build/utils/prompts.js +114 -0
- package/build/utils/shiftignore.js +108 -0
- package/build/utils/tree-scanner.js +165 -0
- package/package.json +49 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readGlobalConfig, setApiKey, setApiUrl, setOrchUrl, setWsUrl, clearUrls, clearApiKey, getConfiguredUrls, isGuestKey, } from '../../utils/config.js';
|
|
2
|
+
import { prompt } from '../../utils/prompts.js';
|
|
3
|
+
export async function configCommand(action, key, value) {
|
|
4
|
+
const parsedAction = (action || 'show');
|
|
5
|
+
switch (parsedAction) {
|
|
6
|
+
case 'show':
|
|
7
|
+
showConfig();
|
|
8
|
+
break;
|
|
9
|
+
case 'set':
|
|
10
|
+
await setConfigValue(key, value);
|
|
11
|
+
break;
|
|
12
|
+
case 'clear':
|
|
13
|
+
await clearConfig(key);
|
|
14
|
+
break;
|
|
15
|
+
default:
|
|
16
|
+
console.log('Usage: lgraph config [show|set|clear] [key] [value]');
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log('Commands:');
|
|
19
|
+
console.log(' show Show current configuration');
|
|
20
|
+
console.log(' set <key> <value> Set a configuration value');
|
|
21
|
+
console.log(' clear [key] Clear configuration (all or specific key)');
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log('Keys:');
|
|
24
|
+
console.log(' api-key Your Shift API key');
|
|
25
|
+
console.log(' api-url SHIFT_API_URL');
|
|
26
|
+
console.log(' orch-url SHIFT_ORCH_URL');
|
|
27
|
+
console.log(' ws-url SHIFT_WS_URL');
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log('Examples:');
|
|
30
|
+
console.log(' lgraph config');
|
|
31
|
+
console.log(' lgraph config set api-url http://localhost:9000');
|
|
32
|
+
console.log(' lgraph config set orch-url http://localhost:9999');
|
|
33
|
+
console.log(' lgraph config set ws-url ws://localhost:9999');
|
|
34
|
+
console.log(' lgraph config clear urls');
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function showConfig() {
|
|
39
|
+
console.log('╔════════════════════════════════════════════╗');
|
|
40
|
+
console.log('║ Shift Configuration ║');
|
|
41
|
+
console.log('╚════════════════════════════════════════════╝\n');
|
|
42
|
+
const globalConfig = readGlobalConfig();
|
|
43
|
+
const urls = getConfiguredUrls();
|
|
44
|
+
// API Key
|
|
45
|
+
console.log('API Key:');
|
|
46
|
+
if (globalConfig?.api_key) {
|
|
47
|
+
const keyType = isGuestKey() ? '(guest)' : '(paid)';
|
|
48
|
+
const maskedKey = globalConfig.api_key.substring(0, 8) + '...' + globalConfig.api_key.slice(-4);
|
|
49
|
+
console.log(` ${maskedKey} ${keyType}`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(' Not configured');
|
|
53
|
+
}
|
|
54
|
+
console.log('');
|
|
55
|
+
// URLs
|
|
56
|
+
console.log('URLs:');
|
|
57
|
+
console.log(` API URL: ${urls.api_url}`);
|
|
58
|
+
console.log(` Orch URL: ${urls.orch_url}`);
|
|
59
|
+
console.log(` WS URL: ${urls.ws_url}`);
|
|
60
|
+
console.log('');
|
|
61
|
+
// Source info
|
|
62
|
+
console.log('Config file: ~/.shift/config.json');
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log('Tip: URLs can also be set via environment variables:');
|
|
65
|
+
console.log(' SHIFT_API_URL, SHIFT_ORCH_URL, SHIFT_WS_URL');
|
|
66
|
+
}
|
|
67
|
+
async function setConfigValue(key, value) {
|
|
68
|
+
if (!key) {
|
|
69
|
+
console.error('Error: Key is required. Run "lgraph config" to see available keys.');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const validKeys = ['api-key', 'api-url', 'orch-url', 'ws-url'];
|
|
73
|
+
if (!validKeys.includes(key)) {
|
|
74
|
+
console.error(`Unknown key: ${key}`);
|
|
75
|
+
console.log(`Available keys: ${validKeys.join(', ')}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
// If no value provided, prompt for it
|
|
79
|
+
if (!value) {
|
|
80
|
+
value = await prompt(`Enter value for ${key}: `);
|
|
81
|
+
if (!value) {
|
|
82
|
+
console.error('Error: Value is required.');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
switch (key) {
|
|
87
|
+
case 'api-key':
|
|
88
|
+
setApiKey(value);
|
|
89
|
+
console.log('✓ API key saved');
|
|
90
|
+
break;
|
|
91
|
+
case 'api-url':
|
|
92
|
+
setApiUrl(value);
|
|
93
|
+
console.log(`✓ API URL set to: ${value}`);
|
|
94
|
+
break;
|
|
95
|
+
case 'orch-url':
|
|
96
|
+
setOrchUrl(value);
|
|
97
|
+
console.log(`✓ Orch URL set to: ${value}`);
|
|
98
|
+
break;
|
|
99
|
+
case 'ws-url':
|
|
100
|
+
setWsUrl(value);
|
|
101
|
+
console.log(`✓ WS URL set to: ${value}`);
|
|
102
|
+
break;
|
|
103
|
+
default:
|
|
104
|
+
console.error(`Unknown key: ${key}`);
|
|
105
|
+
console.log('Available keys: api-key, api-url, orch-url, ws-url');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
console.log('\nNote: Restart any running daemon for URL changes to take effect.');
|
|
109
|
+
}
|
|
110
|
+
async function clearConfig(key) {
|
|
111
|
+
if (!key) {
|
|
112
|
+
// Clear everything
|
|
113
|
+
const answer = await prompt('Clear all configuration? (y/N): ');
|
|
114
|
+
if (answer.toLowerCase() !== 'y') {
|
|
115
|
+
console.log('Cancelled.');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
clearApiKey();
|
|
119
|
+
clearUrls();
|
|
120
|
+
console.log('✓ All configuration cleared');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
switch (key) {
|
|
124
|
+
case 'api-key':
|
|
125
|
+
clearApiKey();
|
|
126
|
+
console.log('✓ API key cleared');
|
|
127
|
+
break;
|
|
128
|
+
case 'urls':
|
|
129
|
+
clearUrls();
|
|
130
|
+
console.log('✓ URLs cleared (will use defaults)');
|
|
131
|
+
break;
|
|
132
|
+
case 'api-url':
|
|
133
|
+
case 'orch-url':
|
|
134
|
+
case 'ws-url':
|
|
135
|
+
console.log('Use "lgraph config clear urls" to clear all URLs');
|
|
136
|
+
break;
|
|
137
|
+
default:
|
|
138
|
+
console.error(`Unknown key: ${key}`);
|
|
139
|
+
console.log('Available keys: api-key, urls');
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { createInterface } from 'readline';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { readProjectConfig, writeProjectConfig, ensureScanTargetFile } from '../../utils/config.js';
|
|
8
|
+
import { fetchProjectStatus, sendInitScan } from '../../utils/api-client.js';
|
|
9
|
+
import { resolveApiKey, resolveProject } from '../../utils/auth-resolver.js';
|
|
10
|
+
import { getDaemonStatus, startDaemon } from '../../daemon/daemon-manager.js';
|
|
11
|
+
import { getProjectTree, extractAllFilePaths, categorizeFiles, countProjectLOC } from '../../utils/tree-scanner.js';
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const { version } = require('../../../package.json');
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
/**
|
|
16
|
+
* Prompt user to confirm re-indexing an already indexed project
|
|
17
|
+
*/
|
|
18
|
+
async function promptForReindex() {
|
|
19
|
+
const rl = createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout,
|
|
22
|
+
});
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
rl.question('Do you want to re-index the project? (y/N): ', (answer) => {
|
|
25
|
+
rl.close();
|
|
26
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export async function initCommand(options = {}) {
|
|
31
|
+
const projectRoot = process.cwd();
|
|
32
|
+
const isAuthInteractive = !options.guest && !options.apiKey;
|
|
33
|
+
const isProjectInteractive = !!(process.stdin.isTTY);
|
|
34
|
+
console.log('╔═══════════════════════════════════════════════╗');
|
|
35
|
+
console.log('║ Initializing Shift Project ║');
|
|
36
|
+
console.log('╚═══════════════════════════════════════════════╝\n');
|
|
37
|
+
// Step 1: Resolve API key
|
|
38
|
+
console.log('[Init] Step 1/5: Checking API key...');
|
|
39
|
+
const authResult = await resolveApiKey({
|
|
40
|
+
guest: options.guest,
|
|
41
|
+
apiKey: options.apiKey,
|
|
42
|
+
interactive: isAuthInteractive,
|
|
43
|
+
commandLabel: 'Init',
|
|
44
|
+
});
|
|
45
|
+
const apiKey = authResult.apiKey;
|
|
46
|
+
// Step 2: Resolve project
|
|
47
|
+
console.log('[Init] Step 2/5: Checking project configuration...');
|
|
48
|
+
// If guest flow returned a project_id, save it before resolving
|
|
49
|
+
if (authResult.projectId && !readProjectConfig(projectRoot)) {
|
|
50
|
+
const { setProject } = await import('../../utils/config.js');
|
|
51
|
+
setProject(authResult.projectId, options.projectName || 'Guest Project', projectRoot);
|
|
52
|
+
}
|
|
53
|
+
const project = await resolveProject({
|
|
54
|
+
apiKey,
|
|
55
|
+
projectId: options.projectId,
|
|
56
|
+
projectName: options.projectName,
|
|
57
|
+
template: options.template,
|
|
58
|
+
interactive: isProjectInteractive,
|
|
59
|
+
commandLabel: 'Init',
|
|
60
|
+
projectRoot,
|
|
61
|
+
});
|
|
62
|
+
// Re-read project config (may have been written by resolveProject)
|
|
63
|
+
const projectConfig = readProjectConfig(projectRoot);
|
|
64
|
+
if (!projectConfig) {
|
|
65
|
+
console.error('\n❌ Failed to configure project.');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
// Ensure .shift/scan_target.json exists (template for user customization)
|
|
69
|
+
ensureScanTargetFile(projectRoot);
|
|
70
|
+
// Check if project is already indexed (skip check if --force flag is used)
|
|
71
|
+
if (!options.force) {
|
|
72
|
+
try {
|
|
73
|
+
const projectStatus = await fetchProjectStatus(apiKey, project.projectId);
|
|
74
|
+
if (projectStatus.indexed) {
|
|
75
|
+
console.log(`[Init] ✓ Project already indexed (${projectStatus.file_count} files)\n`);
|
|
76
|
+
if (isProjectInteractive) {
|
|
77
|
+
const shouldReindex = await promptForReindex();
|
|
78
|
+
if (!shouldReindex) {
|
|
79
|
+
console.log('\n╔═══════════════════════════════════════════════╗');
|
|
80
|
+
console.log('║ Project Already Initialized ║');
|
|
81
|
+
console.log('╚═══════════════════════════════════════════════╝');
|
|
82
|
+
console.log('\nUse "lgraph status" to check the current status.');
|
|
83
|
+
console.log('Use "lgraph init --force" to force re-indexing.\n');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
console.log('\n[Init] Proceeding with re-indexing...\n');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log('[Init] Already indexed. Use --force to re-index.\n');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// If we can't check status, continue with init (server might be unavailable)
|
|
96
|
+
console.log('[Init] ⚠️ Could not check indexing status, proceeding with initialization...\n');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.log('[Init] Force flag detected, skipping indexing status check...\n');
|
|
101
|
+
}
|
|
102
|
+
// Step 3: Check daemon status — start it automatically if not running
|
|
103
|
+
console.log('[Init] Step 3/5: Checking daemon status...');
|
|
104
|
+
const daemonStatus = getDaemonStatus(projectRoot);
|
|
105
|
+
if (!daemonStatus.running) {
|
|
106
|
+
console.log('[Init] Daemon not running. Starting it...');
|
|
107
|
+
try {
|
|
108
|
+
const daemonResult = await startDaemon(projectRoot, project.projectId, apiKey);
|
|
109
|
+
if (daemonResult.success) {
|
|
110
|
+
console.log(`[Init] ✓ Daemon started (PID: ${daemonResult.pid})\n`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log(`[Init] ⚠️ Could not start daemon: ${daemonResult.error}`);
|
|
114
|
+
console.log('[Init] Continuing without daemon — run "lgraph start" later.\n');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.log(`[Init] ⚠️ Could not start daemon: ${err.message}`);
|
|
119
|
+
console.log('[Init] Continuing without daemon — run "lgraph start" later.\n');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.log(`[Init] ✓ Daemon running (PID: ${daemonStatus.pid}, Connected: ${daemonStatus.connected})\n`);
|
|
124
|
+
}
|
|
125
|
+
// Step 4: Scan project structure (matching extension's Step 6)
|
|
126
|
+
console.log('[Init] Step 4/5: Scanning project structure...');
|
|
127
|
+
// Get project tree (matching extension's getProjectTree)
|
|
128
|
+
const treeData = getProjectTree(projectRoot, {
|
|
129
|
+
depth: 0, // Unlimited depth
|
|
130
|
+
exclude_patterns: [
|
|
131
|
+
'.git',
|
|
132
|
+
'node_modules',
|
|
133
|
+
'__pycache__',
|
|
134
|
+
'.vscode',
|
|
135
|
+
'dist',
|
|
136
|
+
'build',
|
|
137
|
+
'.shift',
|
|
138
|
+
'.next',
|
|
139
|
+
'coverage',
|
|
140
|
+
'venv',
|
|
141
|
+
'env',
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
console.log(`[Init] Files: ${treeData.file_count}`);
|
|
145
|
+
console.log(`[Init] Directories: ${treeData.dir_count}`);
|
|
146
|
+
console.log(`[Init] Total size: ${treeData.total_size_mb} MB\n`);
|
|
147
|
+
// Get git info (matching extension)
|
|
148
|
+
let gitInfo = {
|
|
149
|
+
current_branch: '',
|
|
150
|
+
original_branch: 'shift_original',
|
|
151
|
+
migrate_branch: 'shift_migrated',
|
|
152
|
+
has_uncommitted_changes: false,
|
|
153
|
+
};
|
|
154
|
+
try {
|
|
155
|
+
const { stdout: currentBranch } = await execAsync('git branch --show-current', { cwd: projectRoot });
|
|
156
|
+
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: projectRoot });
|
|
157
|
+
gitInfo.current_branch = currentBranch.trim();
|
|
158
|
+
gitInfo.has_uncommitted_changes = statusOutput.trim().length > 0;
|
|
159
|
+
console.log(`[Init] ✓ Git info: branch=${gitInfo.current_branch}, uncommitted=${gitInfo.has_uncommitted_changes}\n`);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
console.log('[Init] ⚠️ Not a git repository or git not available\n');
|
|
163
|
+
}
|
|
164
|
+
// Extract file paths and categorize (matching extension)
|
|
165
|
+
const allFiles = extractAllFilePaths(treeData.tree);
|
|
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}`);
|
|
170
|
+
console.log(`[Init] Source files: ${categorized.source_files.length}`);
|
|
171
|
+
console.log(`[Init] Config files: ${categorized.config_files.length}`);
|
|
172
|
+
console.log(`[Init] Asset files: ${categorized.asset_files.length}\n`);
|
|
173
|
+
// Step 5: Send scan to backend (matching extension's Step 9)
|
|
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
|
+
}
|
|
210
|
+
// Read source file contents so orchestrator can run pipeline inline (e.g. LOCAL mode) and index via init-scan
|
|
211
|
+
const MAX_FILES_TO_SEND = 800;
|
|
212
|
+
const MAX_FILE_SIZE_CHARS = 150000;
|
|
213
|
+
const filesToRead = categorized.source_files.slice(0, MAX_FILES_TO_SEND);
|
|
214
|
+
const filesWithContent = [];
|
|
215
|
+
let filesSkipped = 0;
|
|
216
|
+
for (const filePath of filesToRead) {
|
|
217
|
+
try {
|
|
218
|
+
const absolutePath = path.join(projectRoot, filePath);
|
|
219
|
+
const content = readFileSync(absolutePath, 'utf-8');
|
|
220
|
+
const truncated = content.length > MAX_FILE_SIZE_CHARS
|
|
221
|
+
? content.slice(0, MAX_FILE_SIZE_CHARS) + '\n/* ... truncated */'
|
|
222
|
+
: content;
|
|
223
|
+
filesWithContent.push({ file_path: filePath, content: truncated });
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
filesSkipped++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (filesWithContent.length > 0) {
|
|
230
|
+
console.log(`[Init] Read ${filesWithContent.length} source file(s) for inline indexing${filesSkipped > 0 ? ` (${filesSkipped} skipped)` : ''}`);
|
|
231
|
+
}
|
|
232
|
+
const payload = {
|
|
233
|
+
project_id: project.projectId,
|
|
234
|
+
project_tree: treeData,
|
|
235
|
+
git_info: gitInfo,
|
|
236
|
+
file_manifest: {
|
|
237
|
+
all_files: allFiles,
|
|
238
|
+
categorized: categorized,
|
|
239
|
+
},
|
|
240
|
+
metadata: {
|
|
241
|
+
extension_version: `${version}-cli`,
|
|
242
|
+
scan_timestamp: new Date().toISOString(),
|
|
243
|
+
project_name: project.projectName,
|
|
244
|
+
},
|
|
245
|
+
scan_targets: scanTargets,
|
|
246
|
+
total_loc: totalLOC,
|
|
247
|
+
files: filesWithContent.length > 0 ? filesWithContent : undefined,
|
|
248
|
+
};
|
|
249
|
+
try {
|
|
250
|
+
const response = await sendInitScan(apiKey, project.projectId, payload);
|
|
251
|
+
console.log('[Init] ✓ Backend initialization completed');
|
|
252
|
+
const count = response.source_files_count ?? response.files_read;
|
|
253
|
+
if (count != null) {
|
|
254
|
+
console.log(`[Init] Source files queued: ${count}`);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.log(`[Init] (backend did not return file count)`);
|
|
258
|
+
}
|
|
259
|
+
// Update local config with agent info (matching extension)
|
|
260
|
+
if (response.agents_created?.theme_planner) {
|
|
261
|
+
const agentInfo = {
|
|
262
|
+
agent_id: response.agents_created.theme_planner.agent_id,
|
|
263
|
+
agent_name: response.agents_created.theme_planner.agent_name,
|
|
264
|
+
agent_type: 'theme_planner',
|
|
265
|
+
created_at: new Date().toISOString(),
|
|
266
|
+
};
|
|
267
|
+
projectConfig.agents = projectConfig.agents || [];
|
|
268
|
+
projectConfig.agents.push(agentInfo);
|
|
269
|
+
writeProjectConfig(projectConfig, projectRoot);
|
|
270
|
+
console.log(`[Init] ✓ Agent info saved: ${agentInfo.agent_id}`);
|
|
271
|
+
}
|
|
272
|
+
console.log('\n╔═══════════════════════════════════════════════╗');
|
|
273
|
+
console.log('║ ✓ Project Initialization Complete ║');
|
|
274
|
+
console.log('╚═══════════════════════════════════════════════╝');
|
|
275
|
+
console.log('\nIndexing is running in the background. Use "lgraph status" to check progress.');
|
|
276
|
+
if (response.next_steps) {
|
|
277
|
+
console.log('\nNext steps:');
|
|
278
|
+
response.next_steps.forEach((step) => console.log(` - ${step}`));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
console.error(`\n❌ Failed to initialize project: ${error.message}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readProjectConfig, setProject, ensureScanTargetFile } from '../../utils/config.js';
|
|
2
|
+
import { startDaemon, getDaemonStatus } from '../../daemon/daemon-manager.js';
|
|
3
|
+
import { resolveApiKey, resolveProject } from '../../utils/auth-resolver.js';
|
|
4
|
+
export async function startCommand(options = {}) {
|
|
5
|
+
const projectRoot = process.cwd();
|
|
6
|
+
const isAuthInteractive = !options.guest && !options.apiKey;
|
|
7
|
+
const isProjectInteractive = !!(process.stdin.isTTY);
|
|
8
|
+
console.log('╔════════════════════════════════════════════╗');
|
|
9
|
+
console.log('║ Starting Shift ║');
|
|
10
|
+
console.log('╚════════════════════════════════════════════╝\n');
|
|
11
|
+
// Step 1: Resolve API key
|
|
12
|
+
console.log('[Start] Step 1/4: Checking API key...');
|
|
13
|
+
const authResult = await resolveApiKey({
|
|
14
|
+
guest: options.guest,
|
|
15
|
+
apiKey: options.apiKey,
|
|
16
|
+
interactive: isAuthInteractive,
|
|
17
|
+
commandLabel: 'Start',
|
|
18
|
+
});
|
|
19
|
+
const apiKey = authResult.apiKey;
|
|
20
|
+
// Step 2: Resolve project
|
|
21
|
+
console.log('[Start] Step 2/4: Checking project configuration...');
|
|
22
|
+
// If guest flow returned a project_id, save it before resolving
|
|
23
|
+
if (authResult.projectId && !readProjectConfig(projectRoot)) {
|
|
24
|
+
setProject(authResult.projectId, options.projectName || 'Guest Project', projectRoot);
|
|
25
|
+
}
|
|
26
|
+
const project = await resolveProject({
|
|
27
|
+
apiKey,
|
|
28
|
+
projectId: options.projectId,
|
|
29
|
+
projectName: options.projectName,
|
|
30
|
+
template: options.template,
|
|
31
|
+
interactive: isProjectInteractive,
|
|
32
|
+
commandLabel: 'Start',
|
|
33
|
+
projectRoot,
|
|
34
|
+
});
|
|
35
|
+
const projectConfig = readProjectConfig(projectRoot);
|
|
36
|
+
if (!projectConfig) {
|
|
37
|
+
console.error('❌ Failed to configure project');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
// Ensure .shift/scan_target.json exists (template for user customization)
|
|
41
|
+
ensureScanTargetFile(projectRoot);
|
|
42
|
+
// Step 3: Check if daemon is already running
|
|
43
|
+
console.log('[Start] Step 3/4: Checking daemon status...');
|
|
44
|
+
const status = getDaemonStatus(projectRoot);
|
|
45
|
+
if (status.running) {
|
|
46
|
+
console.log(`[Start] ✓ Daemon is already running (PID: ${status.pid})`);
|
|
47
|
+
console.log(`[Start] WebSocket: ${status.connected ? 'Connected' : 'Connecting...'}\n`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log('[Start] Daemon not running\n');
|
|
51
|
+
// Step 4: Start daemon
|
|
52
|
+
console.log('[Start] Step 4/4: Starting daemon...');
|
|
53
|
+
const result = await startDaemon(projectRoot, project.projectId, apiKey);
|
|
54
|
+
if (!result.success) {
|
|
55
|
+
console.error(`\n❌ Failed to start daemon: ${result.error}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
console.log(`[Start] ✓ Daemon started (PID: ${result.pid})`);
|
|
59
|
+
console.log('\n╔════════════════════════════════════════════╗');
|
|
60
|
+
console.log('║ Shift is now running! ║');
|
|
61
|
+
console.log('╚════════════════════════════════════════════╝');
|
|
62
|
+
console.log('\nUse "lgraph status" to check connection status.');
|
|
63
|
+
console.log('Use "lgraph init" to scan and index the project.');
|
|
64
|
+
console.log('Use "lgraph stop" to stop the daemon.\n');
|
|
65
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getApiKey, readProjectConfig, isGuestKey } from '../../utils/config.js';
|
|
2
|
+
import { getDaemonStatus } from '../../daemon/daemon-manager.js';
|
|
3
|
+
import { fetchProjectStatus } from '../../utils/api-client.js';
|
|
4
|
+
export async function statusCommand() {
|
|
5
|
+
const projectRoot = process.cwd();
|
|
6
|
+
console.log('\n╔════════════════════════════════════════════╗');
|
|
7
|
+
console.log('║ Shift Status ║');
|
|
8
|
+
console.log('╚════════════════════════════════════════════╝\n');
|
|
9
|
+
// Check API key
|
|
10
|
+
const apiKey = getApiKey();
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
console.log('API Key: ❌ Not configured');
|
|
13
|
+
console.log('\nRun "lgraph start" to configure your API key.\n');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (isGuestKey()) {
|
|
17
|
+
console.log('API Key: ✓ Guest key');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log('API Key: ✓ Configured');
|
|
21
|
+
}
|
|
22
|
+
// Check project config
|
|
23
|
+
const projectConfig = readProjectConfig(projectRoot);
|
|
24
|
+
if (!projectConfig) {
|
|
25
|
+
console.log('Project: ❌ Not configured');
|
|
26
|
+
console.log('\nRun "lgraph start" to configure this project.\n');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(`Project: ${projectConfig.project_name}`);
|
|
30
|
+
console.log(`Project ID: ${projectConfig.project_id}`);
|
|
31
|
+
// Check agents
|
|
32
|
+
if (projectConfig.agents && projectConfig.agents.length > 0) {
|
|
33
|
+
console.log(`Agents: ${projectConfig.agents.length} configured`);
|
|
34
|
+
projectConfig.agents.forEach((agent) => {
|
|
35
|
+
console.log(` - ${agent.agent_name} (${agent.agent_type})`);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// Check project status from backend
|
|
39
|
+
try {
|
|
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
|
|
49
|
+
if (projectStatus.indexed) {
|
|
50
|
+
console.log(`Indexed: ✓ ${projectStatus.file_count} files`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log('Indexed: ❌ Not indexed');
|
|
54
|
+
if (scanStatus === 'not_started') {
|
|
55
|
+
console.log(' Run "lgraph init" to scan the project.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
console.log('Status: ⚠️ Unable to check (server unavailable)');
|
|
61
|
+
}
|
|
62
|
+
// Check daemon status
|
|
63
|
+
const status = getDaemonStatus(projectRoot);
|
|
64
|
+
if (!status.running) {
|
|
65
|
+
console.log('Daemon: ❌ Not running');
|
|
66
|
+
console.log('\nRun "lgraph start" to start the daemon.\n');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
console.log(`Daemon: ✓ Running (PID: ${status.pid})`);
|
|
70
|
+
console.log(`WebSocket: ${status.connected ? '✓ Connected' : '⚠️ Disconnected'}`);
|
|
71
|
+
if (status.startedAt) {
|
|
72
|
+
const startedAt = new Date(status.startedAt);
|
|
73
|
+
console.log(`Started: ${startedAt.toLocaleString()}`);
|
|
74
|
+
}
|
|
75
|
+
console.log('');
|
|
76
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getDaemonStatus, stopDaemon } from '../../daemon/daemon-manager.js';
|
|
2
|
+
export async function stopCommand() {
|
|
3
|
+
const projectRoot = process.cwd();
|
|
4
|
+
console.log('\nStopping Shift daemon...\n');
|
|
5
|
+
// Check if daemon is running
|
|
6
|
+
const status = getDaemonStatus(projectRoot);
|
|
7
|
+
if (!status.running) {
|
|
8
|
+
console.log('Daemon is not running.');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
console.log(`Stopping daemon (PID: ${status.pid})...`);
|
|
12
|
+
const result = await stopDaemon(projectRoot);
|
|
13
|
+
if (!result.success) {
|
|
14
|
+
console.error(`Failed to stop daemon: ${result.error}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
console.log('Daemon stopped successfully.\n');
|
|
18
|
+
}
|