@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,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
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
// Global config location: ~/.shift/config.json
|
|
5
|
+
const GLOBAL_SHIFT_DIR = path.join(os.homedir(), '.shift');
|
|
6
|
+
const GLOBAL_CONFIG_FILE = path.join(GLOBAL_SHIFT_DIR, 'config.json');
|
|
7
|
+
// Local config location: .shift/ in current directory
|
|
8
|
+
const LOCAL_SHIFT_DIR = '.shift';
|
|
9
|
+
const LOCAL_CONFIG_FILE = 'config.json'; // Changed from project.json to match extension
|
|
10
|
+
const DAEMON_PID_FILE = 'daemon.pid';
|
|
11
|
+
const DAEMON_STATUS_FILE = 'daemon.status.json';
|
|
12
|
+
// Default URLs
|
|
13
|
+
const DEFAULT_API_URL = 'https://dev-shift-lite.latentforce.ai';
|
|
14
|
+
const DEFAULT_ORCH_URL = 'https://agent-orch.latentforce.ai';
|
|
15
|
+
const DEFAULT_WS_URL = 'wss://agent-orch.latentforce.ai';
|
|
16
|
+
// Helper to get URL from: 1) env var, 2) global config, 3) default
|
|
17
|
+
function getConfiguredUrl(envVar, configKey, defaultUrl) {
|
|
18
|
+
// Priority: Environment variable > Global config > Default
|
|
19
|
+
if (process.env[envVar]) {
|
|
20
|
+
return process.env[envVar];
|
|
21
|
+
}
|
|
22
|
+
const config = readGlobalConfigInternal();
|
|
23
|
+
if (config?.[configKey]) {
|
|
24
|
+
return config[configKey];
|
|
25
|
+
}
|
|
26
|
+
return defaultUrl;
|
|
27
|
+
}
|
|
28
|
+
// Internal read to avoid circular dependency
|
|
29
|
+
function readGlobalConfigInternal() {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
32
|
+
const content = fs.readFileSync(GLOBAL_CONFIG_FILE, 'utf-8');
|
|
33
|
+
return JSON.parse(content);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Invalid config, treat as not existing
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
// API URLs - Priority: env var > global config > default
|
|
42
|
+
export const API_BASE_URL = getConfiguredUrl('SHIFT_API_URL', 'api_url', DEFAULT_API_URL);
|
|
43
|
+
export const API_BASE_URL_ORCH = getConfiguredUrl('SHIFT_ORCH_URL', 'orch_url', DEFAULT_ORCH_URL);
|
|
44
|
+
export const WS_URL = getConfiguredUrl('SHIFT_WS_URL', 'ws_url', DEFAULT_WS_URL);
|
|
45
|
+
// --- Global Config Functions ---
|
|
46
|
+
export function ensureGlobalShiftDir() {
|
|
47
|
+
if (!fs.existsSync(GLOBAL_SHIFT_DIR)) {
|
|
48
|
+
fs.mkdirSync(GLOBAL_SHIFT_DIR, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function readGlobalConfig() {
|
|
52
|
+
try {
|
|
53
|
+
if (fs.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
54
|
+
const content = fs.readFileSync(GLOBAL_CONFIG_FILE, 'utf-8');
|
|
55
|
+
return JSON.parse(content);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Invalid config, treat as not existing
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
export function writeGlobalConfig(config) {
|
|
64
|
+
ensureGlobalShiftDir();
|
|
65
|
+
fs.writeFileSync(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
66
|
+
}
|
|
67
|
+
export function getApiKey() {
|
|
68
|
+
const config = readGlobalConfig();
|
|
69
|
+
return config?.api_key || null;
|
|
70
|
+
}
|
|
71
|
+
export function setApiKey(apiKey) {
|
|
72
|
+
const config = readGlobalConfig() || { api_key: '' };
|
|
73
|
+
config.api_key = apiKey;
|
|
74
|
+
delete config.key_type;
|
|
75
|
+
writeGlobalConfig(config);
|
|
76
|
+
}
|
|
77
|
+
export function setGuestKey(apiKey) {
|
|
78
|
+
const config = {
|
|
79
|
+
api_key: apiKey,
|
|
80
|
+
key_type: 'guest',
|
|
81
|
+
};
|
|
82
|
+
writeGlobalConfig(config);
|
|
83
|
+
}
|
|
84
|
+
export function isGuestKey() {
|
|
85
|
+
const config = readGlobalConfig();
|
|
86
|
+
return config?.key_type === 'guest';
|
|
87
|
+
}
|
|
88
|
+
export function clearApiKey() {
|
|
89
|
+
const config = readGlobalConfig() || { api_key: '' };
|
|
90
|
+
config.api_key = '';
|
|
91
|
+
delete config.key_type;
|
|
92
|
+
writeGlobalConfig(config);
|
|
93
|
+
}
|
|
94
|
+
// --- URL Configuration Functions ---
|
|
95
|
+
export function setApiUrl(url) {
|
|
96
|
+
const config = readGlobalConfig() || { api_key: '' };
|
|
97
|
+
config.api_url = url;
|
|
98
|
+
writeGlobalConfig(config);
|
|
99
|
+
}
|
|
100
|
+
export function setOrchUrl(url) {
|
|
101
|
+
const config = readGlobalConfig() || { api_key: '' };
|
|
102
|
+
config.orch_url = url;
|
|
103
|
+
writeGlobalConfig(config);
|
|
104
|
+
}
|
|
105
|
+
export function setWsUrl(url) {
|
|
106
|
+
const config = readGlobalConfig() || { api_key: '' };
|
|
107
|
+
config.ws_url = url;
|
|
108
|
+
writeGlobalConfig(config);
|
|
109
|
+
}
|
|
110
|
+
export function clearUrls() {
|
|
111
|
+
const config = readGlobalConfig() || { api_key: '' };
|
|
112
|
+
delete config.api_url;
|
|
113
|
+
delete config.orch_url;
|
|
114
|
+
delete config.ws_url;
|
|
115
|
+
writeGlobalConfig(config);
|
|
116
|
+
}
|
|
117
|
+
export function getConfiguredUrls() {
|
|
118
|
+
return {
|
|
119
|
+
api_url: API_BASE_URL,
|
|
120
|
+
orch_url: API_BASE_URL_ORCH,
|
|
121
|
+
ws_url: WS_URL,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// --- Local Config Functions ---
|
|
125
|
+
export function getLocalShiftDir(projectRoot) {
|
|
126
|
+
const root = projectRoot || process.cwd();
|
|
127
|
+
return path.join(root, LOCAL_SHIFT_DIR);
|
|
128
|
+
}
|
|
129
|
+
export function ensureLocalShiftDir(projectRoot) {
|
|
130
|
+
const dir = getLocalShiftDir(projectRoot);
|
|
131
|
+
if (!fs.existsSync(dir)) {
|
|
132
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const SCAN_TARGET_TEMPLATE = [
|
|
136
|
+
{
|
|
137
|
+
language: null,
|
|
138
|
+
path: '',
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
/**
|
|
142
|
+
* Create .shift/scan_target.json with a template if it doesn't exist.
|
|
143
|
+
* Users can edit this file to manually specify scan targets.
|
|
144
|
+
*/
|
|
145
|
+
export function ensureScanTargetFile(projectRoot) {
|
|
146
|
+
const filePath = path.join(getLocalShiftDir(projectRoot), 'scan_target.json');
|
|
147
|
+
if (!fs.existsSync(filePath)) {
|
|
148
|
+
fs.writeFileSync(filePath, JSON.stringify(SCAN_TARGET_TEMPLATE, null, 2));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export function readProjectConfig(projectRoot) {
|
|
152
|
+
try {
|
|
153
|
+
const filePath = path.join(getLocalShiftDir(projectRoot), LOCAL_CONFIG_FILE);
|
|
154
|
+
if (fs.existsSync(filePath)) {
|
|
155
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
156
|
+
return JSON.parse(content);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Invalid config, treat as not existing
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
export function writeProjectConfig(config, projectRoot) {
|
|
165
|
+
ensureLocalShiftDir(projectRoot);
|
|
166
|
+
const filePath = path.join(getLocalShiftDir(projectRoot), LOCAL_CONFIG_FILE);
|
|
167
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
|
|
168
|
+
// Also create .gitignore in .shift folder (matching extension)
|
|
169
|
+
const gitignorePath = path.join(getLocalShiftDir(projectRoot), '.gitignore');
|
|
170
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
171
|
+
fs.writeFileSync(gitignorePath, '*\n!.gitignore\n!config.json\n!scan_target.json\n');
|
|
172
|
+
}
|
|
173
|
+
// Create scan_target.json template if it doesn't exist
|
|
174
|
+
ensureScanTargetFile(projectRoot);
|
|
175
|
+
}
|
|
176
|
+
export function getProjectId(projectRoot) {
|
|
177
|
+
const config = readProjectConfig(projectRoot);
|
|
178
|
+
return config?.project_id || null;
|
|
179
|
+
}
|
|
180
|
+
export function getProjectName(projectRoot) {
|
|
181
|
+
const config = readProjectConfig(projectRoot);
|
|
182
|
+
return config?.project_name || null;
|
|
183
|
+
}
|
|
184
|
+
export function setProject(projectId, projectName, projectRoot) {
|
|
185
|
+
const existing = readProjectConfig(projectRoot);
|
|
186
|
+
const config = {
|
|
187
|
+
project_id: projectId,
|
|
188
|
+
project_name: projectName,
|
|
189
|
+
agents: existing?.agents || [],
|
|
190
|
+
created_at: existing?.created_at || new Date().toISOString(),
|
|
191
|
+
};
|
|
192
|
+
writeProjectConfig(config, projectRoot);
|
|
193
|
+
}
|
|
194
|
+
// --- Daemon Status Functions ---
|
|
195
|
+
export function getDaemonPidPath(projectRoot) {
|
|
196
|
+
return path.join(getLocalShiftDir(projectRoot), DAEMON_PID_FILE);
|
|
197
|
+
}
|
|
198
|
+
export function getDaemonStatusPath(projectRoot) {
|
|
199
|
+
return path.join(getLocalShiftDir(projectRoot), DAEMON_STATUS_FILE);
|
|
200
|
+
}
|
|
201
|
+
export function readDaemonPid(projectRoot) {
|
|
202
|
+
try {
|
|
203
|
+
const filePath = getDaemonPidPath(projectRoot);
|
|
204
|
+
if (fs.existsSync(filePath)) {
|
|
205
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
206
|
+
const pid = parseInt(content, 10);
|
|
207
|
+
return isNaN(pid) ? null : pid;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Error reading PID file
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
export function writeDaemonPid(pid, projectRoot) {
|
|
216
|
+
ensureLocalShiftDir(projectRoot);
|
|
217
|
+
const filePath = getDaemonPidPath(projectRoot);
|
|
218
|
+
fs.writeFileSync(filePath, String(pid));
|
|
219
|
+
}
|
|
220
|
+
export function removeDaemonPid(projectRoot) {
|
|
221
|
+
const filePath = getDaemonPidPath(projectRoot);
|
|
222
|
+
if (fs.existsSync(filePath)) {
|
|
223
|
+
fs.unlinkSync(filePath);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
export function readDaemonStatus(projectRoot) {
|
|
227
|
+
try {
|
|
228
|
+
const filePath = getDaemonStatusPath(projectRoot);
|
|
229
|
+
if (fs.existsSync(filePath)) {
|
|
230
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
231
|
+
return JSON.parse(content);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Invalid status file
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
export function writeDaemonStatus(status, projectRoot) {
|
|
240
|
+
ensureLocalShiftDir(projectRoot);
|
|
241
|
+
const filePath = getDaemonStatusPath(projectRoot);
|
|
242
|
+
fs.writeFileSync(filePath, JSON.stringify(status, null, 2));
|
|
243
|
+
}
|
|
244
|
+
export function removeDaemonStatus(projectRoot) {
|
|
245
|
+
const filePath = getDaemonStatusPath(projectRoot);
|
|
246
|
+
if (fs.existsSync(filePath)) {
|
|
247
|
+
fs.unlinkSync(filePath);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// --- Process Check ---
|
|
251
|
+
export function isProcessRunning(pid) {
|
|
252
|
+
try {
|
|
253
|
+
// Sending signal 0 checks if process exists without killing it
|
|
254
|
+
process.kill(pid, 0);
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
/**
|
|
4
|
+
* Get the primary MAC address (first non-internal interface)
|
|
5
|
+
*/
|
|
6
|
+
function getPrimaryMacAddress() {
|
|
7
|
+
const interfaces = os.networkInterfaces();
|
|
8
|
+
for (const [name, addrs] of Object.entries(interfaces)) {
|
|
9
|
+
if (!addrs)
|
|
10
|
+
continue;
|
|
11
|
+
for (const addr of addrs) {
|
|
12
|
+
// Skip internal/loopback and addresses without MAC
|
|
13
|
+
if (addr.internal || !addr.mac || addr.mac === '00:00:00:00:00:00') {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
return addr.mac;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return 'unknown';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generate a unique, deterministic machine fingerprint
|
|
23
|
+
* Uses MAC address + platform for stability
|
|
24
|
+
* - MAC is globally unique per network interface
|
|
25
|
+
* - Platform helps distinguish dual-boot scenarios
|
|
26
|
+
*/
|
|
27
|
+
export function generateMachineId() {
|
|
28
|
+
const mac = getPrimaryMacAddress();
|
|
29
|
+
const platform = os.platform();
|
|
30
|
+
// Combine MAC + platform
|
|
31
|
+
const fingerprintData = `${mac}|${platform}`;
|
|
32
|
+
// Hash for privacy and fixed length
|
|
33
|
+
return crypto.createHash('sha256').update(fingerprintData).digest('hex');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Generate a shorter machine ID (first 16 chars of full hash)
|
|
37
|
+
*/
|
|
38
|
+
export function generateShortMachineId() {
|
|
39
|
+
return generateMachineId().substring(0, 16);
|
|
40
|
+
}
|
|
41
|
+
export function getMachineFingerprint() {
|
|
42
|
+
return {
|
|
43
|
+
machine_id: generateMachineId(),
|
|
44
|
+
platform: os.platform(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
function createReadlineInterface() {
|
|
3
|
+
return readline.createInterface({
|
|
4
|
+
input: process.stdin,
|
|
5
|
+
output: process.stdout,
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export function prompt(question) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = createReadlineInterface();
|
|
11
|
+
rl.question(question, (answer) => {
|
|
12
|
+
rl.close();
|
|
13
|
+
resolve(answer.trim());
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export async function promptApiKey() {
|
|
18
|
+
console.log('\nNo Shift API key found.');
|
|
19
|
+
const apiKey = await prompt('Please paste your Shift API key: ');
|
|
20
|
+
if (!apiKey) {
|
|
21
|
+
console.error('Error: API key is required.');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
return apiKey;
|
|
25
|
+
}
|
|
26
|
+
export async function promptProjectId() {
|
|
27
|
+
console.log('\nNo project configured for this directory.');
|
|
28
|
+
const projectId = await prompt('Please enter your Shift project ID: ');
|
|
29
|
+
if (!projectId) {
|
|
30
|
+
console.error('Error: Project ID is required.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
return projectId;
|
|
34
|
+
}
|
|
35
|
+
export async function promptSelectProject(projects) {
|
|
36
|
+
console.log('\nAvailable projects:');
|
|
37
|
+
console.log('-------------------');
|
|
38
|
+
projects.forEach((project, index) => {
|
|
39
|
+
const description = project.description ? ` - ${project.description}` : '';
|
|
40
|
+
console.log(` ${index + 1}. ${project.project_name}${description}`);
|
|
41
|
+
});
|
|
42
|
+
console.log('');
|
|
43
|
+
const answer = await prompt(`Select a project (1-${projects.length}): `);
|
|
44
|
+
const selection = parseInt(answer, 10);
|
|
45
|
+
if (isNaN(selection) || selection < 1 || selection > projects.length) {
|
|
46
|
+
console.error('Invalid selection.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return projects[selection - 1];
|
|
50
|
+
}
|
|
51
|
+
export async function promptKeyChoice() {
|
|
52
|
+
console.log('\nNo Shift API key found.');
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(' 1. I have an API key');
|
|
55
|
+
console.log(' 2. Continue as guest');
|
|
56
|
+
console.log('');
|
|
57
|
+
const answer = await prompt('Select an option (1 or 2): ');
|
|
58
|
+
const selection = parseInt(answer, 10);
|
|
59
|
+
if (selection === 1) {
|
|
60
|
+
return 'paid';
|
|
61
|
+
}
|
|
62
|
+
else if (selection === 2) {
|
|
63
|
+
return 'guest';
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.error('Invalid selection.');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
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
|
+
}
|