@latentforce/shift 1.0.8 → 1.0.9

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.
@@ -30,49 +30,84 @@ export async function requestGuestKey() {
30
30
  * Matching extension's fetchProjects function in api-client.js
31
31
  */
32
32
  export async function fetchProjects(apiKey) {
33
- try {
34
- const response = await fetch(`${API_BASE_URL}/api/vscode-projects`, {
35
- method: 'GET',
36
- headers: {
37
- 'Authorization': `Bearer ${apiKey}`,
38
- },
39
- });
40
- if (!response.ok) {
41
- const text = await response.text();
42
- throw new Error(text || `HTTP ${response.status}`);
43
- }
44
- const data = await response.json();
45
- return data.projects || [];
46
- }
47
- catch (error) {
48
- console.error('Failed to fetch projects:', error);
49
- throw new Error(error.message || 'Failed to fetch projects');
33
+ const response = await fetch(`${API_BASE_URL}/api/v1/mcp/projects`, {
34
+ method: 'GET',
35
+ headers: {
36
+ 'Authorization': `Bearer ${apiKey}`,
37
+ },
38
+ });
39
+ if (!response.ok) {
40
+ const text = await response.text();
41
+ throw new Error(text || `HTTP ${response.status}`);
50
42
  }
43
+ const data = await response.json();
44
+ return data.projects || [];
51
45
  }
52
46
  /**
53
47
  * Send init scan to backend
54
48
  * Matching extension's init-scan API call
55
49
  */
56
50
  export async function sendInitScan(apiKey, projectId, payload) {
57
- try {
58
- const response = await fetch(`${API_BASE_URL_ORCH}/api/projects/${projectId}/init-scan`, {
59
- method: 'POST',
60
- headers: {
61
- 'Authorization': `Bearer ${apiKey}`,
62
- 'Content-Type': 'application/json',
63
- },
64
- body: JSON.stringify(payload),
65
- });
66
- if (!response.ok) {
67
- const text = await response.text();
68
- throw new Error(text || `HTTP ${response.status}`);
69
- }
70
- return await response.json();
51
+ const response = await fetch(`${API_BASE_URL_ORCH}/api/projects/${projectId}/init-scan`, {
52
+ method: 'POST',
53
+ headers: {
54
+ 'Authorization': `Bearer ${apiKey}`,
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ body: JSON.stringify(payload),
58
+ });
59
+ if (!response.ok) {
60
+ const text = await response.text();
61
+ throw new Error(text || `HTTP ${response.status}`);
71
62
  }
72
- catch (error) {
73
- console.error('Failed to send init scan:', error);
74
- throw error;
63
+ return await response.json();
64
+ }
65
+ /**
66
+ * Send update-drg request to backend
67
+ * Calls POST /api/v1/mcp/update-drg
68
+ */
69
+ export async function sendUpdateDrg(apiKey, payload) {
70
+ const response = await fetch(`${API_BASE_URL}/api/v1/mcp/update-drg`, {
71
+ method: 'POST',
72
+ headers: {
73
+ 'Authorization': `Bearer ${apiKey}`,
74
+ 'Content-Type': 'application/json',
75
+ },
76
+ body: JSON.stringify(payload),
77
+ });
78
+ if (!response.ok) {
79
+ const text = await response.text();
80
+ throw new Error(text || `HTTP ${response.status}`);
81
+ }
82
+ return await response.json();
83
+ }
84
+ export async function fetchMigrationTemplates(apiKey) {
85
+ const response = await fetch(`${API_BASE_URL}/api/v1/mcp/migration-templates`, {
86
+ method: 'GET',
87
+ headers: { 'Authorization': `Bearer ${apiKey}` },
88
+ });
89
+ if (!response.ok) {
90
+ const text = await response.text();
91
+ throw new Error(text || `HTTP ${response.status}`);
92
+ }
93
+ const data = await response.json();
94
+ return data.templates || [];
95
+ }
96
+ export async function createProject(apiKey, params) {
97
+ const response = await fetch(`${API_BASE_URL}/api/v1/mcp/projects/create`, {
98
+ method: 'POST',
99
+ headers: {
100
+ 'Authorization': `Bearer ${apiKey}`,
101
+ 'Content-Type': 'application/json',
102
+ },
103
+ body: JSON.stringify(params),
104
+ });
105
+ if (!response.ok) {
106
+ const text = await response.text();
107
+ throw new Error(text || `HTTP ${response.status}`);
75
108
  }
109
+ const data = await response.json();
110
+ return data.project || data;
76
111
  }
77
112
  /**
78
113
  * Fetch project indexing/work status
@@ -82,7 +117,7 @@ export async function fetchProjectStatus(apiKey, projectId) {
82
117
  const controller = new AbortController();
83
118
  const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
84
119
  try {
85
- const response = await fetch(`${API_BASE_URL}/api/projects/${projectId}/status`, {
120
+ const response = await fetch(`${API_BASE_URL}/api/v1/mcp/projects/${projectId}/status`, {
86
121
  method: 'GET',
87
122
  headers: {
88
123
  'Authorization': `Bearer ${apiKey}`,
@@ -0,0 +1,184 @@
1
+ import * as path from 'path';
2
+ import { getApiKey, setApiKey, setGuestKey, isGuestKey, setProject, readProjectConfig, } from './config.js';
3
+ import { requestGuestKey, fetchProjects, fetchMigrationTemplates, createProject, } from './api-client.js';
4
+ import { promptApiKey, promptKeyChoice, promptSelectProject, promptProjectName, promptCreateOrSelect, promptSelectTemplate, } from './prompts.js';
5
+ /**
6
+ * Resolve API key from flags, config, or interactive prompts.
7
+ * Guest flow returns both apiKey and projectId (from guest-key endpoint).
8
+ */
9
+ export async function resolveApiKey(opts = {}) {
10
+ const label = opts.commandLabel || 'Shift';
11
+ const interactive = opts.interactive ?? true;
12
+ // If both --guest and --api-key, api-key wins
13
+ if (opts.guest && opts.apiKey) {
14
+ console.log(`[${label}] ⚠️ Both --guest and --api-key provided; using --api-key.`);
15
+ }
16
+ // 1. Explicit --api-key flag
17
+ if (opts.apiKey) {
18
+ setApiKey(opts.apiKey);
19
+ console.log(`[${label}] ✓ API key saved\n`);
20
+ return { apiKey: opts.apiKey };
21
+ }
22
+ // 2. Existing key in config
23
+ const existingKey = getApiKey();
24
+ if (existingKey) {
25
+ if (isGuestKey()) {
26
+ console.log(`[${label}] ✓ Guest key found\n`);
27
+ }
28
+ else {
29
+ console.log(`[${label}] ✓ API key found\n`);
30
+ }
31
+ return { apiKey: existingKey };
32
+ }
33
+ // 3. --guest flag
34
+ if (opts.guest) {
35
+ return await doGuestFlow(label);
36
+ }
37
+ // 4. Interactive fallback
38
+ if (interactive) {
39
+ const choice = await promptKeyChoice();
40
+ if (choice === 'paid') {
41
+ const apiKey = await promptApiKey();
42
+ setApiKey(apiKey);
43
+ console.log(`[${label}] ✓ API key saved\n`);
44
+ return { apiKey };
45
+ }
46
+ else {
47
+ return await doGuestFlow(label);
48
+ }
49
+ }
50
+ // 5. Non-interactive with no key
51
+ console.error(`\n❌ No API key found. Provide --api-key <key> or --guest.`);
52
+ process.exit(1);
53
+ }
54
+ async function doGuestFlow(label) {
55
+ console.log(`\n[${label}] Requesting guest session...`);
56
+ try {
57
+ const resp = await requestGuestKey();
58
+ setGuestKey(resp.api_key);
59
+ console.log(`[${label}] ✓ Guest session started\n`);
60
+ const result = { apiKey: resp.api_key };
61
+ // Guest endpoint may return a project_id
62
+ if (resp.project_id) {
63
+ result.projectId = resp.project_id;
64
+ }
65
+ return result;
66
+ }
67
+ catch (error) {
68
+ console.error(`\n❌ Failed to get guest key: ${error.message}`);
69
+ process.exit(1);
70
+ }
71
+ }
72
+ /**
73
+ * Resolve project from flags, local config, or interactive prompts.
74
+ * Can create new projects when --project-name is given.
75
+ */
76
+ export async function resolveProject(opts) {
77
+ const label = opts.commandLabel || 'Shift';
78
+ const interactive = opts.interactive ?? true;
79
+ const projectRoot = opts.projectRoot || process.cwd();
80
+ // 1. Check existing local config
81
+ const existingConfig = readProjectConfig(projectRoot);
82
+ if (existingConfig) {
83
+ console.log(`[${label}] ✓ Project: ${existingConfig.project_name} (${existingConfig.project_id})\n`);
84
+ return { projectId: existingConfig.project_id, projectName: existingConfig.project_name };
85
+ }
86
+ // 2. Explicit --project-id
87
+ if (opts.projectId) {
88
+ const name = opts.projectName || 'CLI Project';
89
+ setProject(opts.projectId, name, projectRoot);
90
+ console.log(`[${label}] ✓ Project "${name}" saved (${opts.projectId})\n`);
91
+ return { projectId: opts.projectId, projectName: name };
92
+ }
93
+ // 3. --project-name: try to match existing, else create new
94
+ if (opts.projectName) {
95
+ return await resolveByName(opts, projectRoot, label);
96
+ }
97
+ // 4. Interactive fallback
98
+ if (interactive) {
99
+ return await interactiveProjectSetup(opts.apiKey, opts.template, projectRoot, label);
100
+ }
101
+ // 5. Non-interactive with no project info
102
+ console.error(`\n❌ No project configured. Provide --project-id or --project-name.`);
103
+ process.exit(1);
104
+ }
105
+ async function resolveByName(opts, projectRoot, label) {
106
+ const { apiKey, projectName, template } = opts;
107
+ const interactive = opts.interactive ?? true;
108
+ // Try to match existing project by name
109
+ console.log(`[${label}] Looking for project "${projectName}"...`);
110
+ try {
111
+ const projects = await fetchProjects(apiKey);
112
+ const match = projects.find((p) => p.project_name.toLowerCase() === projectName.toLowerCase());
113
+ if (match) {
114
+ setProject(match.project_id, match.project_name, projectRoot);
115
+ console.log(`[${label}] ✓ Found existing project "${match.project_name}" (${match.project_id})\n`);
116
+ return { projectId: match.project_id, projectName: match.project_name };
117
+ }
118
+ }
119
+ catch {
120
+ // Can't fetch projects — proceed to create
121
+ }
122
+ // No match — create new project
123
+ console.log(`[${label}] No existing project named "${projectName}". Creating new project...`);
124
+ return await createNewProject(apiKey, projectName, template, interactive, projectRoot, label);
125
+ }
126
+ async function createNewProject(apiKey, name, templateFlag, interactive, projectRoot, label) {
127
+ let templateId = templateFlag;
128
+ if (!templateId) {
129
+ // Fetch templates and let user select
130
+ try {
131
+ const templates = await fetchMigrationTemplates(apiKey);
132
+ if (templates.length > 0) {
133
+ if (interactive) {
134
+ templateId = await promptSelectTemplate(templates);
135
+ }
136
+ else {
137
+ // Non-interactive: use first template
138
+ templateId = templates[0].template_id;
139
+ console.log(`[${label}] Using default template: ${templates[0].name}`);
140
+ }
141
+ }
142
+ }
143
+ catch {
144
+ console.log(`[${label}] ⚠️ Could not fetch templates, proceeding without one.`);
145
+ }
146
+ }
147
+ try {
148
+ const project = await createProject(apiKey, {
149
+ project_name: name,
150
+ migration_template_id: templateId,
151
+ });
152
+ setProject(project.project_id, project.project_name, projectRoot);
153
+ console.log(`[${label}] ✓ Created project "${project.project_name}" (${project.project_id})\n`);
154
+ return { projectId: project.project_id, projectName: project.project_name };
155
+ }
156
+ catch (error) {
157
+ console.error(`\n❌ Failed to create project: ${error.message}`);
158
+ process.exit(1);
159
+ }
160
+ }
161
+ async function interactiveProjectSetup(apiKey, templateFlag, projectRoot, label) {
162
+ // Always ask user what they want to do first
163
+ const action = await promptCreateOrSelect();
164
+ if (action === 'select') {
165
+ let projects = [];
166
+ try {
167
+ projects = await fetchProjects(apiKey);
168
+ }
169
+ catch {
170
+ // fetch failed
171
+ }
172
+ if (projects.length > 0) {
173
+ const selected = await promptSelectProject(projects);
174
+ setProject(selected.project_id, selected.project_name, projectRoot);
175
+ console.log(`[${label}] ✓ Project "${selected.project_name}" saved\n`);
176
+ return { projectId: selected.project_id, projectName: selected.project_name };
177
+ }
178
+ console.log(`[${label}] No existing projects found. Let's create one.\n`);
179
+ }
180
+ // Create new project — suggest directory name as default
181
+ const defaultName = path.basename(projectRoot);
182
+ const name = await promptProjectName(defaultName);
183
+ return await createNewProject(apiKey, name, templateFlag, true, projectRoot, label);
184
+ }
@@ -67,3 +67,48 @@ export async function promptKeyChoice() {
67
67
  process.exit(1);
68
68
  }
69
69
  }
70
+ export async function promptCreateOrSelect() {
71
+ console.log('\nProject setup:');
72
+ console.log('');
73
+ console.log(' 1. Select an existing project');
74
+ console.log(' 2. Create a new project');
75
+ console.log('');
76
+ const answer = await prompt('Select an option (1 or 2): ');
77
+ const selection = parseInt(answer, 10);
78
+ if (selection === 1) {
79
+ return 'select';
80
+ }
81
+ else if (selection === 2) {
82
+ return 'create';
83
+ }
84
+ else {
85
+ console.error('Invalid selection.');
86
+ process.exit(1);
87
+ }
88
+ }
89
+ export async function promptProjectName(defaultName) {
90
+ const hint = defaultName ? ` (${defaultName})` : '';
91
+ const name = await prompt(`Enter project name${hint}: `);
92
+ const result = name || defaultName || '';
93
+ if (!result) {
94
+ console.error('Error: Project name is required.');
95
+ process.exit(1);
96
+ }
97
+ return result;
98
+ }
99
+ export async function promptSelectTemplate(templates) {
100
+ console.log('\nAvailable migration templates:');
101
+ console.log('-----------------------------');
102
+ templates.forEach((tmpl, index) => {
103
+ const desc = tmpl.description ? ` - ${tmpl.description}` : '';
104
+ console.log(` ${index + 1}. ${tmpl.name}${desc}`);
105
+ });
106
+ console.log('');
107
+ const answer = await prompt(`Select a template (1-${templates.length}): `);
108
+ const selection = parseInt(answer, 10);
109
+ if (isNaN(selection) || selection < 1 || selection > templates.length) {
110
+ console.error('Invalid selection.');
111
+ process.exit(1);
112
+ }
113
+ return templates[selection - 1].template_id;
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latentforce/shift",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Shift CLI - AI-powered code intelligence with MCP support",
5
5
  "type": "module",
6
6
  "main": "./build/index.js",