jfl 0.1.0 → 0.2.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 +443 -145
- package/clawdbot-plugin/clawdbot.plugin.json +20 -0
- package/clawdbot-plugin/index.js +555 -0
- package/clawdbot-plugin/index.ts +582 -0
- package/clawdbot-skill/SKILL.md +33 -336
- package/clawdbot-skill/index.ts +491 -321
- package/clawdbot-skill/skill.json +4 -13
- package/dist/commands/clawdbot.d.ts +11 -0
- package/dist/commands/clawdbot.d.ts.map +1 -0
- package/dist/commands/clawdbot.js +215 -0
- package/dist/commands/clawdbot.js.map +1 -0
- package/dist/commands/context-hub.d.ts +5 -0
- package/dist/commands/context-hub.d.ts.map +1 -1
- package/dist/commands/context-hub.js +394 -28
- package/dist/commands/context-hub.js.map +1 -1
- package/dist/commands/gtm-process-update.d.ts +10 -0
- package/dist/commands/gtm-process-update.d.ts.map +1 -0
- package/dist/commands/gtm-process-update.js +101 -0
- package/dist/commands/gtm-process-update.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +278 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +32 -33
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/memory.d.ts +38 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +229 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/commands/migrate-services.d.ts +8 -0
- package/dist/commands/migrate-services.d.ts.map +1 -0
- package/dist/commands/migrate-services.js +182 -0
- package/dist/commands/migrate-services.js.map +1 -0
- package/dist/commands/onboard.d.ts +24 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +663 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/commands/openclaw.d.ts +56 -0
- package/dist/commands/openclaw.d.ts.map +1 -0
- package/dist/commands/openclaw.js +700 -0
- package/dist/commands/openclaw.js.map +1 -0
- package/dist/commands/orchestrate.d.ts +14 -0
- package/dist/commands/orchestrate.d.ts.map +1 -0
- package/dist/commands/orchestrate.js +270 -0
- package/dist/commands/orchestrate.js.map +1 -0
- package/dist/commands/profile.d.ts +46 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +498 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/commands/repair.d.ts.map +1 -1
- package/dist/commands/repair.js +37 -0
- package/dist/commands/repair.js.map +1 -1
- package/dist/commands/service-agent.d.ts +16 -0
- package/dist/commands/service-agent.d.ts.map +1 -0
- package/dist/commands/service-agent.js +375 -0
- package/dist/commands/service-agent.js.map +1 -0
- package/dist/commands/service-manager.d.ts +12 -0
- package/dist/commands/service-manager.d.ts.map +1 -0
- package/dist/commands/service-manager.js +967 -0
- package/dist/commands/service-manager.js.map +1 -0
- package/dist/commands/service-validate.d.ts +12 -0
- package/dist/commands/service-validate.d.ts.map +1 -0
- package/dist/commands/service-validate.js +611 -0
- package/dist/commands/service-validate.js.map +1 -0
- package/dist/commands/services-create.d.ts +15 -0
- package/dist/commands/services-create.d.ts.map +1 -0
- package/dist/commands/services-create.js +1452 -0
- package/dist/commands/services-create.js.map +1 -0
- package/dist/commands/services-scan.d.ts +13 -0
- package/dist/commands/services-scan.d.ts.map +1 -0
- package/dist/commands/services-scan.js +251 -0
- package/dist/commands/services-scan.js.map +1 -0
- package/dist/commands/services-sync-agents.d.ts +23 -0
- package/dist/commands/services-sync-agents.d.ts.map +1 -0
- package/dist/commands/services-sync-agents.js +207 -0
- package/dist/commands/services-sync-agents.js.map +1 -0
- package/dist/commands/services.d.ts +19 -0
- package/dist/commands/services.d.ts.map +1 -0
- package/dist/commands/services.js +742 -0
- package/dist/commands/services.js.map +1 -0
- package/dist/commands/session.d.ts +5 -1
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +68 -586
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +17 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +75 -21
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/validate-settings.d.ts +37 -0
- package/dist/commands/validate-settings.d.ts.map +1 -0
- package/dist/commands/validate-settings.js +197 -0
- package/dist/commands/validate-settings.js.map +1 -0
- package/dist/commands/voice.d.ts +0 -1
- package/dist/commands/voice.d.ts.map +1 -1
- package/dist/commands/voice.js +16 -15
- package/dist/commands/voice.js.map +1 -1
- package/dist/index.js +395 -141
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-generator.d.ts +26 -0
- package/dist/lib/agent-generator.d.ts.map +1 -0
- package/dist/lib/agent-generator.js +331 -0
- package/dist/lib/agent-generator.js.map +1 -0
- package/dist/lib/memory-db.d.ts +102 -0
- package/dist/lib/memory-db.d.ts.map +1 -0
- package/dist/lib/memory-db.js +313 -0
- package/dist/lib/memory-db.js.map +1 -0
- package/dist/lib/memory-indexer.d.ts +47 -0
- package/dist/lib/memory-indexer.d.ts.map +1 -0
- package/dist/lib/memory-indexer.js +215 -0
- package/dist/lib/memory-indexer.js.map +1 -0
- package/dist/lib/memory-search.d.ts +41 -0
- package/dist/lib/memory-search.d.ts.map +1 -0
- package/dist/lib/memory-search.js +246 -0
- package/dist/lib/memory-search.js.map +1 -0
- package/dist/lib/openclaw-registry.d.ts +48 -0
- package/dist/lib/openclaw-registry.d.ts.map +1 -0
- package/dist/lib/openclaw-registry.js +181 -0
- package/dist/lib/openclaw-registry.js.map +1 -0
- package/dist/lib/openclaw-sdk.d.ts +107 -0
- package/dist/lib/openclaw-sdk.d.ts.map +1 -0
- package/dist/lib/openclaw-sdk.js +208 -0
- package/dist/lib/openclaw-sdk.js.map +1 -0
- package/dist/lib/peer-agent-generator.d.ts +44 -0
- package/dist/lib/peer-agent-generator.d.ts.map +1 -0
- package/dist/lib/peer-agent-generator.js +286 -0
- package/dist/lib/peer-agent-generator.js.map +1 -0
- package/dist/lib/service-dependencies.d.ts +44 -0
- package/dist/lib/service-dependencies.d.ts.map +1 -0
- package/dist/lib/service-dependencies.js +314 -0
- package/dist/lib/service-dependencies.js.map +1 -0
- package/dist/lib/service-detector.d.ts +61 -0
- package/dist/lib/service-detector.d.ts.map +1 -0
- package/dist/lib/service-detector.js +521 -0
- package/dist/lib/service-detector.js.map +1 -0
- package/dist/lib/service-gtm.d.ts +157 -0
- package/dist/lib/service-gtm.d.ts.map +1 -0
- package/dist/lib/service-gtm.js +786 -0
- package/dist/lib/service-gtm.js.map +1 -0
- package/dist/lib/service-mcp-base.d.ts +103 -0
- package/dist/lib/service-mcp-base.d.ts.map +1 -0
- package/dist/lib/service-mcp-base.js +263 -0
- package/dist/lib/service-mcp-base.js.map +1 -0
- package/dist/lib/service-utils.d.ts +103 -0
- package/dist/lib/service-utils.d.ts.map +1 -0
- package/dist/lib/service-utils.js +368 -0
- package/dist/lib/service-utils.js.map +1 -0
- package/dist/lib/skill-generator.d.ts +21 -0
- package/dist/lib/skill-generator.d.ts.map +1 -0
- package/dist/lib/skill-generator.js +253 -0
- package/dist/lib/skill-generator.js.map +1 -0
- package/dist/lib/stratus-client.d.ts +100 -0
- package/dist/lib/stratus-client.d.ts.map +1 -0
- package/dist/lib/stratus-client.js +255 -0
- package/dist/lib/stratus-client.js.map +1 -0
- package/dist/mcp/context-hub-mcp.js +135 -53
- package/dist/mcp/context-hub-mcp.js.map +1 -1
- package/dist/mcp/service-mcp-server.d.ts +12 -0
- package/dist/mcp/service-mcp-server.d.ts.map +1 -0
- package/dist/mcp/service-mcp-server.js +434 -0
- package/dist/mcp/service-mcp-server.js.map +1 -0
- package/dist/mcp/service-peer-mcp.d.ts +36 -0
- package/dist/mcp/service-peer-mcp.d.ts.map +1 -0
- package/dist/mcp/service-peer-mcp.js +220 -0
- package/dist/mcp/service-peer-mcp.js.map +1 -0
- package/dist/mcp/service-registry-mcp.d.ts +13 -0
- package/dist/mcp/service-registry-mcp.d.ts.map +1 -0
- package/dist/mcp/service-registry-mcp.js +330 -0
- package/dist/mcp/service-registry-mcp.js.map +1 -0
- package/dist/ui/banner.js +1 -1
- package/dist/ui/banner.js.map +1 -1
- package/dist/ui/context-hub-logs.d.ts +10 -0
- package/dist/ui/context-hub-logs.d.ts.map +1 -0
- package/dist/ui/context-hub-logs.js +175 -0
- package/dist/ui/context-hub-logs.js.map +1 -0
- package/dist/ui/service-dashboard.d.ts +11 -0
- package/dist/ui/service-dashboard.d.ts.map +1 -0
- package/dist/ui/service-dashboard.js +357 -0
- package/dist/ui/service-dashboard.js.map +1 -0
- package/dist/ui/services-manager.d.ts +11 -0
- package/dist/ui/services-manager.d.ts.map +1 -0
- package/dist/ui/services-manager.js +507 -0
- package/dist/ui/services-manager.js.map +1 -0
- package/dist/utils/auth-guard.d.ts.map +1 -1
- package/dist/utils/auth-guard.js +8 -9
- package/dist/utils/auth-guard.js.map +1 -1
- package/dist/utils/claude-md-generator.d.ts +10 -0
- package/dist/utils/claude-md-generator.d.ts.map +1 -0
- package/dist/utils/claude-md-generator.js +215 -0
- package/dist/utils/claude-md-generator.js.map +1 -0
- package/dist/utils/ensure-context-hub.d.ts +20 -0
- package/dist/utils/ensure-context-hub.d.ts.map +1 -0
- package/dist/utils/ensure-context-hub.js +65 -0
- package/dist/utils/ensure-context-hub.js.map +1 -0
- package/dist/utils/ensure-project.d.ts.map +1 -1
- package/dist/utils/ensure-project.js +3 -4
- package/dist/utils/ensure-project.js.map +1 -1
- package/dist/utils/jfl-config.d.ts +19 -0
- package/dist/utils/jfl-config.d.ts.map +1 -0
- package/dist/utils/jfl-config.js +112 -0
- package/dist/utils/jfl-config.js.map +1 -0
- package/dist/utils/jfl-migration.d.ts +29 -0
- package/dist/utils/jfl-migration.d.ts.map +1 -0
- package/dist/utils/jfl-migration.js +142 -0
- package/dist/utils/jfl-migration.js.map +1 -0
- package/dist/utils/jfl-paths.d.ts +55 -0
- package/dist/utils/jfl-paths.d.ts.map +1 -0
- package/dist/utils/jfl-paths.js +120 -0
- package/dist/utils/jfl-paths.js.map +1 -0
- package/dist/utils/settings-validator.d.ts +73 -0
- package/dist/utils/settings-validator.d.ts.map +1 -0
- package/dist/utils/settings-validator.js +222 -0
- package/dist/utils/settings-validator.js.map +1 -0
- package/package.json +19 -3
- package/scripts/commit-gtm.sh +56 -0
- package/scripts/commit-product.sh +68 -0
- package/scripts/context-query.sh +45 -0
- package/scripts/session/auto-commit.sh +297 -0
- package/scripts/session/jfl-doctor.sh +707 -0
- package/scripts/session/session-cleanup.sh +268 -0
- package/scripts/session/session-end.sh +198 -0
- package/scripts/session/session-init.sh +350 -0
- package/scripts/session/session-init.sh.backup +292 -0
- package/scripts/session/session-sync.sh +167 -0
- package/scripts/session/test-context-preservation.sh +160 -0
- package/scripts/session/test-critical-infrastructure.sh +293 -0
- package/scripts/session/test-experience-level.sh +336 -0
- package/scripts/session/test-session-cleanup.sh +268 -0
- package/scripts/session/test-session-sync.sh +320 -0
- package/scripts/voice-start.sh +36 -8
- package/scripts/where-am-i.sh +78 -0
- package/template/.claude/service-settings.json +32 -0
- package/template/.claude/settings.json +14 -1
- package/template/.claude/skills/end/SKILL.md +1780 -0
- package/template/.jfl/config.json +2 -1
- package/template/CLAUDE.md +1039 -134
- package/template/CLAUDE.md.bak +1187 -0
- package/template/scripts/commit-gtm.sh +56 -0
- package/template/scripts/commit-product.sh +68 -0
- package/template/scripts/migrate-to-branch-sessions.sh +201 -0
- package/template/scripts/session/auto-commit.sh +58 -6
- package/template/scripts/session/jfl-doctor.sh +137 -17
- package/template/scripts/session/session-cleanup.sh +268 -0
- package/template/scripts/session/session-end.sh +4 -0
- package/template/scripts/session/session-init.sh +253 -66
- package/template/scripts/session/test-critical-infrastructure.sh +293 -0
- package/template/scripts/session/test-experience-level.sh +336 -0
- package/template/scripts/session/test-session-cleanup.sh +268 -0
- package/template/scripts/session/test-session-sync.sh +320 -0
- package/template/scripts/where-am-i.sh +78 -0
- package/template/templates/service-agent/.claude/settings.json +32 -0
- package/template/templates/service-agent/CLAUDE.md +334 -0
- package/template/templates/service-agent/knowledge/ARCHITECTURE.md +115 -0
- package/template/templates/service-agent/knowledge/DEPLOYMENT.md +199 -0
- package/template/templates/service-agent/knowledge/RUNBOOK.md +412 -0
- package/template/templates/service-agent/knowledge/SERVICE_SPEC.md +77 -0
- package/dist/commands/session-mgmt.d.ts +0 -33
- package/dist/commands/session-mgmt.d.ts.map +0 -1
- package/dist/commands/session-mgmt.js +0 -404
- package/dist/commands/session-mgmt.js.map +0 -1
- package/template/scripts/session/auto-merge.sh +0 -325
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Manager API Server
|
|
3
|
+
*
|
|
4
|
+
* HTTP API for managing services across all JFL projects.
|
|
5
|
+
* Provides unified service status and control interface.
|
|
6
|
+
*
|
|
7
|
+
* @purpose Service Manager API daemon for TUI and external integration
|
|
8
|
+
*/
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import ora from "ora";
|
|
11
|
+
import { execSync, spawn } from "child_process";
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import * as http from "http";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
import { promisify } from "util";
|
|
17
|
+
import { exec } from "child_process";
|
|
18
|
+
import { JFL_PATHS, JFL_FILES } from "../utils/jfl-paths.js";
|
|
19
|
+
const execAsync = promisify(exec);
|
|
20
|
+
const DEFAULT_PORT = 3402;
|
|
21
|
+
const PID_FILE = ".jfl/service-manager.pid";
|
|
22
|
+
const LOG_FILE = ".jfl/logs/service-manager.log";
|
|
23
|
+
const CONFIG_FILE = path.join(JFL_PATHS.config, "service-manager.json");
|
|
24
|
+
const GLOBAL_SERVICES_FILE = path.join(JFL_PATHS.data, "services.json");
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Config Management
|
|
27
|
+
// ============================================================================
|
|
28
|
+
function loadConfig() {
|
|
29
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
30
|
+
const defaultConfig = { port: DEFAULT_PORT };
|
|
31
|
+
saveConfig(defaultConfig);
|
|
32
|
+
return defaultConfig;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const content = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
36
|
+
return JSON.parse(content);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { port: DEFAULT_PORT };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function saveConfig(config) {
|
|
43
|
+
const dir = path.dirname(CONFIG_FILE);
|
|
44
|
+
if (!fs.existsSync(dir)) {
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
48
|
+
}
|
|
49
|
+
function getPort() {
|
|
50
|
+
if (process.env.JFL_SERVICE_MANAGER_PORT) {
|
|
51
|
+
return parseInt(process.env.JFL_SERVICE_MANAGER_PORT, 10);
|
|
52
|
+
}
|
|
53
|
+
return loadConfig().port;
|
|
54
|
+
}
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Service Detection
|
|
57
|
+
// ============================================================================
|
|
58
|
+
function loadGlobalServices() {
|
|
59
|
+
if (!fs.existsSync(GLOBAL_SERVICES_FILE)) {
|
|
60
|
+
return { version: "1.0", services: {} };
|
|
61
|
+
}
|
|
62
|
+
const content = fs.readFileSync(GLOBAL_SERVICES_FILE, "utf-8");
|
|
63
|
+
return JSON.parse(content);
|
|
64
|
+
}
|
|
65
|
+
function loadProjectServices(projectRoot) {
|
|
66
|
+
const projectServicesFile = path.join(projectRoot, ".jfl", "services.json");
|
|
67
|
+
if (!fs.existsSync(projectServicesFile)) {
|
|
68
|
+
return { version: "1.0", services: {} };
|
|
69
|
+
}
|
|
70
|
+
const content = fs.readFileSync(projectServicesFile, "utf-8");
|
|
71
|
+
return JSON.parse(content);
|
|
72
|
+
}
|
|
73
|
+
function substituteVariables(str, vars) {
|
|
74
|
+
let result = str;
|
|
75
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
76
|
+
result = result.replace(new RegExp(`\\$\\{${key}\\}`, "g"), value);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
async function isServiceRunning(service) {
|
|
81
|
+
try {
|
|
82
|
+
const vars = {
|
|
83
|
+
PORT: service.port?.toString() || "",
|
|
84
|
+
HOME: homedir(),
|
|
85
|
+
};
|
|
86
|
+
const detectionCmd = substituteVariables(service.detection_command, vars);
|
|
87
|
+
const { stdout } = await execAsync(detectionCmd);
|
|
88
|
+
const pidStr = stdout.trim();
|
|
89
|
+
if (pidStr) {
|
|
90
|
+
const pid = parseInt(pidStr.split('\n')[0], 10);
|
|
91
|
+
if (!isNaN(pid)) {
|
|
92
|
+
return { running: true, pid };
|
|
93
|
+
}
|
|
94
|
+
return { running: true };
|
|
95
|
+
}
|
|
96
|
+
return { running: false };
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return { running: false };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function getServiceStartTime(service) {
|
|
103
|
+
if (!service.pid_file)
|
|
104
|
+
return null;
|
|
105
|
+
const vars = {
|
|
106
|
+
HOME: homedir(),
|
|
107
|
+
};
|
|
108
|
+
const pidFile = substituteVariables(service.pid_file, vars);
|
|
109
|
+
if (!fs.existsSync(pidFile))
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
const stats = fs.statSync(pidFile);
|
|
113
|
+
return stats.mtime;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function getServiceStatus(name, service) {
|
|
120
|
+
const running = await isServiceRunning(service);
|
|
121
|
+
const startTime = getServiceStartTime(service);
|
|
122
|
+
let uptime;
|
|
123
|
+
let started_at;
|
|
124
|
+
if (running.running && startTime) {
|
|
125
|
+
const now = new Date();
|
|
126
|
+
const diffMs = now.getTime() - startTime.getTime();
|
|
127
|
+
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
128
|
+
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
129
|
+
uptime = hours > 0 ? `${hours}h${minutes}m` : `${minutes}m`;
|
|
130
|
+
started_at = startTime.toISOString();
|
|
131
|
+
}
|
|
132
|
+
const vars = {
|
|
133
|
+
HOME: homedir(),
|
|
134
|
+
PORT: service.port?.toString() || "",
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
name,
|
|
138
|
+
status: running.running ? "running" : "stopped",
|
|
139
|
+
pid: running.pid,
|
|
140
|
+
port: service.port,
|
|
141
|
+
uptime,
|
|
142
|
+
started_at,
|
|
143
|
+
description: service.description,
|
|
144
|
+
log_path: service.log_file ? substituteVariables(service.log_file, vars) : undefined,
|
|
145
|
+
health_url: service.health_url ? substituteVariables(service.health_url, vars) : undefined
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function getAllServiceStatuses() {
|
|
149
|
+
const globalServices = loadGlobalServices();
|
|
150
|
+
const projectRoot = process.cwd();
|
|
151
|
+
const projectServices = loadProjectServices(projectRoot);
|
|
152
|
+
const statuses = [];
|
|
153
|
+
// Get global services
|
|
154
|
+
for (const [name, service] of Object.entries(globalServices.services)) {
|
|
155
|
+
const status = await getServiceStatus(name, service);
|
|
156
|
+
statuses.push(status);
|
|
157
|
+
}
|
|
158
|
+
// Get project services
|
|
159
|
+
for (const [name, service] of Object.entries(projectServices.services)) {
|
|
160
|
+
const status = await getServiceStatus(name, service);
|
|
161
|
+
statuses.push(status);
|
|
162
|
+
}
|
|
163
|
+
return statuses;
|
|
164
|
+
}
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Service Control
|
|
167
|
+
// ============================================================================
|
|
168
|
+
async function startServiceByName(serviceName) {
|
|
169
|
+
const globalServices = loadGlobalServices();
|
|
170
|
+
const projectRoot = process.cwd();
|
|
171
|
+
const projectServices = loadProjectServices(projectRoot);
|
|
172
|
+
let service = globalServices.services[serviceName] || projectServices.services[serviceName];
|
|
173
|
+
if (!service) {
|
|
174
|
+
return { success: false, message: `Service not found: ${serviceName}` };
|
|
175
|
+
}
|
|
176
|
+
// Check if already running
|
|
177
|
+
const running = await isServiceRunning(service);
|
|
178
|
+
if (running.running) {
|
|
179
|
+
return { success: true, message: `Service already running (PID: ${running.pid})` };
|
|
180
|
+
}
|
|
181
|
+
const vars = {
|
|
182
|
+
PORT: service.port?.toString() || "",
|
|
183
|
+
HOME: homedir(),
|
|
184
|
+
WORKSPACE: projectRoot,
|
|
185
|
+
};
|
|
186
|
+
const startCmd = substituteVariables(service.start_command, vars);
|
|
187
|
+
try {
|
|
188
|
+
await execAsync(startCmd, { cwd: projectRoot });
|
|
189
|
+
return { success: true, message: `Started ${serviceName}` };
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
return { success: false, message: `Failed to start: ${error.message}` };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function stopServiceByName(serviceName) {
|
|
196
|
+
const globalServices = loadGlobalServices();
|
|
197
|
+
const projectRoot = process.cwd();
|
|
198
|
+
const projectServices = loadProjectServices(projectRoot);
|
|
199
|
+
let service = globalServices.services[serviceName] || projectServices.services[serviceName];
|
|
200
|
+
if (!service) {
|
|
201
|
+
return { success: false, message: `Service not found: ${serviceName}` };
|
|
202
|
+
}
|
|
203
|
+
const vars = {
|
|
204
|
+
PORT: service.port?.toString() || "",
|
|
205
|
+
HOME: homedir(),
|
|
206
|
+
};
|
|
207
|
+
const stopCmd = substituteVariables(service.stop_command, vars);
|
|
208
|
+
try {
|
|
209
|
+
await execAsync(stopCmd);
|
|
210
|
+
return { success: true, message: `Stopped ${serviceName}` };
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
return { success: false, message: `Failed to stop: ${error.message}` };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function restartServiceByName(serviceName) {
|
|
217
|
+
await stopServiceByName(serviceName);
|
|
218
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
219
|
+
return startServiceByName(serviceName);
|
|
220
|
+
}
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// Service Tool Execution (Auto-Generated MCP)
|
|
223
|
+
// ============================================================================
|
|
224
|
+
async function executeServiceTool(serviceName, service, toolName, args) {
|
|
225
|
+
// Check for custom .jfl-mcp.js file first
|
|
226
|
+
const servicePath = service.path;
|
|
227
|
+
if (servicePath) {
|
|
228
|
+
const customMcpPath = path.join(servicePath, '.jfl-mcp.js');
|
|
229
|
+
if (fs.existsSync(customMcpPath)) {
|
|
230
|
+
// Use custom MCP server
|
|
231
|
+
return await executeCustomMCP(customMcpPath, toolName, args);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Fall back to auto-generated tools from services.json
|
|
235
|
+
return await executeAutoGeneratedTool(serviceName, service, toolName, args);
|
|
236
|
+
}
|
|
237
|
+
async function executeCustomMCP(mcpPath, toolName, args) {
|
|
238
|
+
// Spawn the custom MCP server and send JSON-RPC request
|
|
239
|
+
const { spawn } = await import("child_process");
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
const child = spawn("node", [mcpPath]);
|
|
242
|
+
let stdout = "";
|
|
243
|
+
let stderr = "";
|
|
244
|
+
child.stdout.on("data", (data) => {
|
|
245
|
+
stdout += data.toString();
|
|
246
|
+
});
|
|
247
|
+
child.stderr.on("data", (data) => {
|
|
248
|
+
stderr += data.toString();
|
|
249
|
+
});
|
|
250
|
+
child.on("close", (code) => {
|
|
251
|
+
if (code !== 0) {
|
|
252
|
+
reject(new Error(`MCP server exited with code ${code}: ${stderr}`));
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
try {
|
|
256
|
+
// Parse JSON-RPC response
|
|
257
|
+
const lines = stdout.split('\n').filter(l => l.trim());
|
|
258
|
+
const lastLine = lines[lines.length - 1];
|
|
259
|
+
const response = JSON.parse(lastLine);
|
|
260
|
+
if (response.result?.content?.[0]?.text) {
|
|
261
|
+
resolve(response.result.content[0].text);
|
|
262
|
+
}
|
|
263
|
+
else if (response.error) {
|
|
264
|
+
reject(new Error(response.error.message));
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
resolve(JSON.stringify(response.result));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
reject(new Error(`Failed to parse MCP response: ${error}`));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
// Send JSON-RPC requests
|
|
276
|
+
const requests = [
|
|
277
|
+
JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize" }),
|
|
278
|
+
JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: toolName, arguments: args } })
|
|
279
|
+
];
|
|
280
|
+
requests.forEach(req => child.stdin.write(req + '\n'));
|
|
281
|
+
child.stdin.end();
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async function executeAutoGeneratedTool(serviceName, service, toolName, args) {
|
|
285
|
+
// Handle standard tools
|
|
286
|
+
switch (toolName) {
|
|
287
|
+
case "status":
|
|
288
|
+
return await handleStatus(serviceName, service);
|
|
289
|
+
case "logs":
|
|
290
|
+
return await handleLogs(serviceName, service, args?.lines || 50);
|
|
291
|
+
case "health":
|
|
292
|
+
return await handleHealth(serviceName, service);
|
|
293
|
+
case "start":
|
|
294
|
+
const startResult = await startServiceByName(serviceName);
|
|
295
|
+
return startResult.message;
|
|
296
|
+
case "stop":
|
|
297
|
+
const stopResult = await stopServiceByName(serviceName);
|
|
298
|
+
return stopResult.message;
|
|
299
|
+
case "restart":
|
|
300
|
+
const restartResult = await restartServiceByName(serviceName);
|
|
301
|
+
return restartResult.message;
|
|
302
|
+
}
|
|
303
|
+
// Handle custom tools from services.json
|
|
304
|
+
if (service.mcp?.tools) {
|
|
305
|
+
const customTool = service.mcp.tools.find((t) => t.name === toolName);
|
|
306
|
+
if (customTool) {
|
|
307
|
+
return await executeCustomTool(serviceName, service, customTool, args);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
311
|
+
}
|
|
312
|
+
async function handleStatus(serviceName, service) {
|
|
313
|
+
const status = await getServiceStatus(serviceName, service);
|
|
314
|
+
const lines = [
|
|
315
|
+
`Service: ${status.name}`,
|
|
316
|
+
`Status: ${status.status}`,
|
|
317
|
+
`Description: ${status.description}`,
|
|
318
|
+
];
|
|
319
|
+
if (status.port)
|
|
320
|
+
lines.push(`Port: ${status.port}`);
|
|
321
|
+
if (status.pid)
|
|
322
|
+
lines.push(`PID: ${status.pid}`);
|
|
323
|
+
if (status.uptime)
|
|
324
|
+
lines.push(`Uptime: ${status.uptime}`);
|
|
325
|
+
if (status.started_at)
|
|
326
|
+
lines.push(`Started: ${status.started_at}`);
|
|
327
|
+
if (status.log_path)
|
|
328
|
+
lines.push(`Logs: ${status.log_path}`);
|
|
329
|
+
if (status.health_url)
|
|
330
|
+
lines.push(`Health URL: ${status.health_url}`);
|
|
331
|
+
return lines.join('\n');
|
|
332
|
+
}
|
|
333
|
+
async function handleLogs(serviceName, service, lines) {
|
|
334
|
+
const vars = {
|
|
335
|
+
PORT: service.port?.toString() || "",
|
|
336
|
+
HOME: homedir(),
|
|
337
|
+
};
|
|
338
|
+
const logFile = service.log_file ? substituteVariables(service.log_file, vars) : undefined;
|
|
339
|
+
if (!logFile || !fs.existsSync(logFile)) {
|
|
340
|
+
return "No logs available";
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const { stdout } = await execAsync(`tail -n ${lines} "${logFile}"`);
|
|
344
|
+
return stdout || "No logs";
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
return `Error reading logs: ${error.message}`;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function handleHealth(serviceName, service) {
|
|
351
|
+
if (!service.health_url) {
|
|
352
|
+
return `Health check not configured for ${serviceName}`;
|
|
353
|
+
}
|
|
354
|
+
const vars = {
|
|
355
|
+
PORT: service.port?.toString() || "",
|
|
356
|
+
};
|
|
357
|
+
const healthUrl = substituteVariables(service.health_url, vars);
|
|
358
|
+
try {
|
|
359
|
+
const response = await fetch(healthUrl, {
|
|
360
|
+
method: "GET",
|
|
361
|
+
signal: AbortSignal.timeout(5000),
|
|
362
|
+
});
|
|
363
|
+
if (response.ok) {
|
|
364
|
+
const data = await response.text();
|
|
365
|
+
return `Health check passed for ${serviceName}\n${data}`;
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
return `Health check failed for ${serviceName}: HTTP ${response.status}`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
return `Health check failed for ${serviceName}: ${error.message}`;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function executeCustomTool(serviceName, service, tool, args) {
|
|
376
|
+
const vars = {
|
|
377
|
+
SERVICE_NAME: serviceName,
|
|
378
|
+
PORT: service.port?.toString() || "",
|
|
379
|
+
HOME: homedir(),
|
|
380
|
+
SERVICE_PATH: service.path || process.cwd(),
|
|
381
|
+
};
|
|
382
|
+
// Substitute variables in command
|
|
383
|
+
let command = tool.command;
|
|
384
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
385
|
+
command = command.replace(new RegExp(`\\$\\{${key}\\}`, "g"), value);
|
|
386
|
+
}
|
|
387
|
+
// Substitute any arguments passed to the tool
|
|
388
|
+
if (args) {
|
|
389
|
+
for (const [key, value] of Object.entries(args)) {
|
|
390
|
+
command = command.replace(new RegExp(`\\$\\{${key}\\}`, "g"), String(value));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const timeout = tool.timeout || 120000; // 2 minutes default
|
|
394
|
+
try {
|
|
395
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
396
|
+
timeout,
|
|
397
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
398
|
+
});
|
|
399
|
+
let result = "";
|
|
400
|
+
if (stdout)
|
|
401
|
+
result += stdout;
|
|
402
|
+
if (stderr)
|
|
403
|
+
result += `\nStderr:\n${stderr}`;
|
|
404
|
+
return result || "Command completed successfully (no output)";
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
throw new Error(`Command failed: ${error.message}\nCommand: ${command}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function removeServiceByName(serviceName) {
|
|
411
|
+
const globalServices = loadGlobalServices();
|
|
412
|
+
const projectRoot = process.cwd();
|
|
413
|
+
const projectServices = loadProjectServices(projectRoot);
|
|
414
|
+
// Check global services
|
|
415
|
+
if (globalServices.services[serviceName]) {
|
|
416
|
+
delete globalServices.services[serviceName];
|
|
417
|
+
fs.writeFileSync(GLOBAL_SERVICES_FILE, JSON.stringify(globalServices, null, 2));
|
|
418
|
+
return { success: true, message: `Removed ${serviceName} from global services` };
|
|
419
|
+
}
|
|
420
|
+
// Check project services
|
|
421
|
+
const projectServicesFile = path.join(projectRoot, ".jfl", "services.json");
|
|
422
|
+
if (projectServices.services[serviceName]) {
|
|
423
|
+
delete projectServices.services[serviceName];
|
|
424
|
+
fs.writeFileSync(projectServicesFile, JSON.stringify(projectServices, null, 2));
|
|
425
|
+
return { success: true, message: `Removed ${serviceName} from project services` };
|
|
426
|
+
}
|
|
427
|
+
return { success: false, message: `Service not found: ${serviceName}` };
|
|
428
|
+
}
|
|
429
|
+
// ============================================================================
|
|
430
|
+
// HTTP Server
|
|
431
|
+
// ============================================================================
|
|
432
|
+
function createServer(port) {
|
|
433
|
+
const server = http.createServer(async (req, res) => {
|
|
434
|
+
// CORS
|
|
435
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
436
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
437
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
438
|
+
if (req.method === "OPTIONS") {
|
|
439
|
+
res.writeHead(200);
|
|
440
|
+
res.end();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
444
|
+
// Health check
|
|
445
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
446
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
447
|
+
res.end(JSON.stringify({ status: "ok", port }));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
// List all services
|
|
451
|
+
if (url.pathname === "/services" && req.method === "GET") {
|
|
452
|
+
try {
|
|
453
|
+
const services = await getAllServiceStatuses();
|
|
454
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
455
|
+
res.end(JSON.stringify({ services }));
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
459
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
// Start service
|
|
464
|
+
const startMatch = url.pathname.match(/^\/services\/([^/]+)\/start$/);
|
|
465
|
+
if (startMatch && req.method === "POST") {
|
|
466
|
+
const serviceName = startMatch[1];
|
|
467
|
+
const result = await startServiceByName(serviceName);
|
|
468
|
+
res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
|
|
469
|
+
res.end(JSON.stringify(result));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
// Stop service
|
|
473
|
+
const stopMatch = url.pathname.match(/^\/services\/([^/]+)\/stop$/);
|
|
474
|
+
if (stopMatch && req.method === "POST") {
|
|
475
|
+
const serviceName = stopMatch[1];
|
|
476
|
+
const result = await stopServiceByName(serviceName);
|
|
477
|
+
res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
|
|
478
|
+
res.end(JSON.stringify(result));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// Restart service
|
|
482
|
+
const restartMatch = url.pathname.match(/^\/services\/([^/]+)\/restart$/);
|
|
483
|
+
if (restartMatch && req.method === "POST") {
|
|
484
|
+
const serviceName = restartMatch[1];
|
|
485
|
+
const result = await restartServiceByName(serviceName);
|
|
486
|
+
res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
|
|
487
|
+
res.end(JSON.stringify(result));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
// Get service logs
|
|
491
|
+
const logsMatch = url.pathname.match(/^\/services\/([^/]+)\/logs$/);
|
|
492
|
+
if (logsMatch && req.method === "GET") {
|
|
493
|
+
const serviceName = logsMatch[1];
|
|
494
|
+
const lines = parseInt(url.searchParams.get("lines") || "50", 10);
|
|
495
|
+
try {
|
|
496
|
+
const globalServices = loadGlobalServices();
|
|
497
|
+
const projectRoot = process.cwd();
|
|
498
|
+
const projectServices = loadProjectServices(projectRoot);
|
|
499
|
+
const service = globalServices.services[serviceName] || projectServices.services[serviceName];
|
|
500
|
+
if (!service) {
|
|
501
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
502
|
+
res.end(JSON.stringify({ error: `Service not found: ${serviceName}` }));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const vars = {
|
|
506
|
+
PORT: service.port?.toString() || "",
|
|
507
|
+
HOME: homedir(),
|
|
508
|
+
};
|
|
509
|
+
const logFile = service.log_file ? substituteVariables(service.log_file, vars) : undefined;
|
|
510
|
+
if (!logFile || !fs.existsSync(logFile)) {
|
|
511
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
512
|
+
res.end(JSON.stringify({ logs: "No logs available" }));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
// Read last N lines using tail
|
|
516
|
+
try {
|
|
517
|
+
const { stdout } = await execAsync(`tail -n ${lines} "${logFile}"`);
|
|
518
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
519
|
+
res.end(JSON.stringify({ logs: stdout }));
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
523
|
+
res.end(JSON.stringify({ logs: `Error reading logs: ${error.message}` }));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
528
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
// Remove service
|
|
533
|
+
const removeMatch = url.pathname.match(/^\/services\/([^/]+)$/);
|
|
534
|
+
if (removeMatch && req.method === "DELETE") {
|
|
535
|
+
const serviceName = removeMatch[1];
|
|
536
|
+
const result = await removeServiceByName(serviceName);
|
|
537
|
+
res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" });
|
|
538
|
+
res.end(JSON.stringify(result));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
// ========================================================================
|
|
542
|
+
// Service Registry API (for service mesh)
|
|
543
|
+
// ========================================================================
|
|
544
|
+
// List all services in registry
|
|
545
|
+
if (url.pathname === "/registry" && req.method === "GET") {
|
|
546
|
+
try {
|
|
547
|
+
const services = await getAllServiceStatuses();
|
|
548
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
549
|
+
res.end(JSON.stringify({ services }));
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
553
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
554
|
+
}
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
// Get specific service info
|
|
558
|
+
const registryGetMatch = url.pathname.match(/^\/registry\/([^/]+)$/);
|
|
559
|
+
if (registryGetMatch && req.method === "GET") {
|
|
560
|
+
const serviceName = registryGetMatch[1];
|
|
561
|
+
try {
|
|
562
|
+
const services = await getAllServiceStatuses();
|
|
563
|
+
const service = services.find(s => s.name === serviceName);
|
|
564
|
+
if (!service) {
|
|
565
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
566
|
+
res.end(JSON.stringify({ error: `Service not found: ${serviceName}` }));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
570
|
+
res.end(JSON.stringify({ service }));
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
574
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// Call a service's tool (proxy to service MCP server)
|
|
579
|
+
const registryCallMatch = url.pathname.match(/^\/registry\/([^/]+)\/call$/);
|
|
580
|
+
if (registryCallMatch && req.method === "POST") {
|
|
581
|
+
const serviceName = registryCallMatch[1];
|
|
582
|
+
let body = "";
|
|
583
|
+
req.on("data", chunk => { body += chunk.toString(); });
|
|
584
|
+
req.on("end", async () => {
|
|
585
|
+
try {
|
|
586
|
+
const { tool, args } = JSON.parse(body);
|
|
587
|
+
// Get service info
|
|
588
|
+
const globalServices = loadGlobalServices();
|
|
589
|
+
const projectRoot = process.cwd();
|
|
590
|
+
const projectServices = loadProjectServices(projectRoot);
|
|
591
|
+
const service = globalServices.services[serviceName] || projectServices.services[serviceName];
|
|
592
|
+
if (!service) {
|
|
593
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
594
|
+
res.end(JSON.stringify({ error: `Service not found: ${serviceName}` }));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
// Execute the tool
|
|
598
|
+
const result = await executeServiceTool(serviceName, service, tool, args);
|
|
599
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
600
|
+
res.end(JSON.stringify({ result }));
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
604
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
// Register a service (for service-local MCP servers)
|
|
610
|
+
if (url.pathname === "/registry/register" && req.method === "POST") {
|
|
611
|
+
let body = "";
|
|
612
|
+
req.on("data", chunk => { body += chunk.toString(); });
|
|
613
|
+
req.on("end", async () => {
|
|
614
|
+
try {
|
|
615
|
+
const { name, description, version, path: servicePath, mcpServerPath } = JSON.parse(body);
|
|
616
|
+
// Add to global services (or project services if in a GTM project)
|
|
617
|
+
const globalServices = loadGlobalServices();
|
|
618
|
+
// Check if service already exists
|
|
619
|
+
if (globalServices.services[name]) {
|
|
620
|
+
// Update existing
|
|
621
|
+
globalServices.services[name].description = description;
|
|
622
|
+
if (mcpServerPath) {
|
|
623
|
+
globalServices.services[name].mcp = {
|
|
624
|
+
enabled: true,
|
|
625
|
+
serverPath: mcpServerPath
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
// Create new (minimal config - can be enhanced later)
|
|
631
|
+
globalServices.services[name] = {
|
|
632
|
+
name,
|
|
633
|
+
type: "process",
|
|
634
|
+
description,
|
|
635
|
+
start_command: `cd ${servicePath} && npm start`,
|
|
636
|
+
stop_command: "echo 'Manual stop required'",
|
|
637
|
+
detection_command: "echo ''",
|
|
638
|
+
mcp: {
|
|
639
|
+
enabled: true,
|
|
640
|
+
serverPath: mcpServerPath
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
fs.writeFileSync(GLOBAL_SERVICES_FILE, JSON.stringify(globalServices, null, 2));
|
|
645
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
646
|
+
res.end(JSON.stringify({ success: true, message: `Registered ${name}` }));
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
650
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
// 404
|
|
656
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
657
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
658
|
+
});
|
|
659
|
+
return server;
|
|
660
|
+
}
|
|
661
|
+
// ============================================================================
|
|
662
|
+
// Daemon Management
|
|
663
|
+
// ============================================================================
|
|
664
|
+
function getPidFile() {
|
|
665
|
+
const pidDir = JFL_FILES.servicesPids;
|
|
666
|
+
if (!fs.existsSync(pidDir)) {
|
|
667
|
+
fs.mkdirSync(pidDir, { recursive: true });
|
|
668
|
+
}
|
|
669
|
+
return path.join(pidDir, "service-manager.pid");
|
|
670
|
+
}
|
|
671
|
+
function getLogFile() {
|
|
672
|
+
const logDir = JFL_FILES.servicesLogs;
|
|
673
|
+
if (!fs.existsSync(logDir)) {
|
|
674
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
675
|
+
}
|
|
676
|
+
return path.join(logDir, "service-manager.log");
|
|
677
|
+
}
|
|
678
|
+
function isRunning() {
|
|
679
|
+
const pidFile = getPidFile();
|
|
680
|
+
if (!fs.existsSync(pidFile)) {
|
|
681
|
+
return { running: false };
|
|
682
|
+
}
|
|
683
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
684
|
+
try {
|
|
685
|
+
process.kill(pid, 0); // Check if process exists
|
|
686
|
+
return { running: true, pid };
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
// Process doesn't exist, clean up stale PID file
|
|
690
|
+
fs.unlinkSync(pidFile);
|
|
691
|
+
return { running: false };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async function isPortInUse(port) {
|
|
695
|
+
return new Promise((resolve) => {
|
|
696
|
+
const server = http.createServer();
|
|
697
|
+
server.once('error', (err) => {
|
|
698
|
+
if (err.code === 'EADDRINUSE') {
|
|
699
|
+
resolve(true);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
resolve(false);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
server.once('listening', () => {
|
|
706
|
+
server.close();
|
|
707
|
+
resolve(false);
|
|
708
|
+
});
|
|
709
|
+
server.listen(port);
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
async function startDaemon(port) {
|
|
713
|
+
const status = isRunning();
|
|
714
|
+
if (status.running) {
|
|
715
|
+
return { success: true, message: `Service Manager already running (PID: ${status.pid})` };
|
|
716
|
+
}
|
|
717
|
+
// Check if port is in use
|
|
718
|
+
const portInUse = await isPortInUse(port);
|
|
719
|
+
if (portInUse) {
|
|
720
|
+
return { success: false, message: `Port ${port} is already in use` };
|
|
721
|
+
}
|
|
722
|
+
const logFile = getLogFile();
|
|
723
|
+
const pidFile = getPidFile();
|
|
724
|
+
// Find jfl command
|
|
725
|
+
let jflCmd = "jfl";
|
|
726
|
+
try {
|
|
727
|
+
execSync("which jfl", { encoding: "utf-8" }).trim();
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
jflCmd = process.argv[1];
|
|
731
|
+
}
|
|
732
|
+
// Start as detached process
|
|
733
|
+
const child = spawn(jflCmd, ["service-manager", "serve", "--port", String(port)], {
|
|
734
|
+
detached: true,
|
|
735
|
+
stdio: ["ignore", fs.openSync(logFile, "a"), fs.openSync(logFile, "a")],
|
|
736
|
+
env: { ...process.env, NODE_ENV: "production" }
|
|
737
|
+
});
|
|
738
|
+
child.unref();
|
|
739
|
+
// Wait for startup
|
|
740
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
741
|
+
// Write PID file
|
|
742
|
+
if (child.pid) {
|
|
743
|
+
try {
|
|
744
|
+
process.kill(child.pid, 0);
|
|
745
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
746
|
+
return { success: true, message: `Started (PID: ${child.pid})` };
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
return { success: false, message: "Process started but immediately exited" };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return { success: false, message: "Failed to spawn daemon process" };
|
|
753
|
+
}
|
|
754
|
+
async function stopDaemon() {
|
|
755
|
+
const status = isRunning();
|
|
756
|
+
if (!status.running || !status.pid) {
|
|
757
|
+
return { success: true, message: "Service Manager is not running" };
|
|
758
|
+
}
|
|
759
|
+
const pidFile = getPidFile();
|
|
760
|
+
try {
|
|
761
|
+
// Send SIGTERM first
|
|
762
|
+
process.kill(status.pid, "SIGTERM");
|
|
763
|
+
// Wait up to 3 seconds for graceful shutdown
|
|
764
|
+
let attempts = 0;
|
|
765
|
+
while (attempts < 6) {
|
|
766
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
767
|
+
try {
|
|
768
|
+
process.kill(status.pid, 0);
|
|
769
|
+
attempts++;
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
// If still running, force kill
|
|
776
|
+
try {
|
|
777
|
+
process.kill(status.pid, 0);
|
|
778
|
+
process.kill(status.pid, "SIGKILL");
|
|
779
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
// Process is gone
|
|
783
|
+
}
|
|
784
|
+
// Clean up PID file
|
|
785
|
+
if (fs.existsSync(pidFile)) {
|
|
786
|
+
fs.unlinkSync(pidFile);
|
|
787
|
+
}
|
|
788
|
+
return { success: true, message: "Service Manager stopped" };
|
|
789
|
+
}
|
|
790
|
+
catch (err) {
|
|
791
|
+
return { success: false, message: `Failed to stop: ${err}` };
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// ============================================================================
|
|
795
|
+
// CLI Command
|
|
796
|
+
// ============================================================================
|
|
797
|
+
export async function serviceManagerCommand(action, options = {}) {
|
|
798
|
+
const port = options.port || getPort();
|
|
799
|
+
switch (action) {
|
|
800
|
+
case "start": {
|
|
801
|
+
const spinner = ora("Starting Service Manager...").start();
|
|
802
|
+
const result = await startDaemon(port);
|
|
803
|
+
if (result.success) {
|
|
804
|
+
if (result.message.includes("already running")) {
|
|
805
|
+
spinner.info(result.message);
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
spinner.succeed(`Service Manager started on port ${port}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
spinner.fail(result.message);
|
|
813
|
+
}
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
case "stop": {
|
|
817
|
+
const spinner = ora("Stopping Service Manager...").start();
|
|
818
|
+
const result = await stopDaemon();
|
|
819
|
+
if (result.success) {
|
|
820
|
+
spinner.succeed(result.message);
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
spinner.fail(result.message);
|
|
824
|
+
}
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
case "restart": {
|
|
828
|
+
await serviceManagerCommand("stop", options);
|
|
829
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
830
|
+
await serviceManagerCommand("start", options);
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
case "status": {
|
|
834
|
+
const status = isRunning();
|
|
835
|
+
if (status.running) {
|
|
836
|
+
console.log(chalk.green(`\n Service Manager is running`));
|
|
837
|
+
console.log(chalk.gray(` PID: ${status.pid}`));
|
|
838
|
+
console.log(chalk.gray(` Port: ${port}`));
|
|
839
|
+
console.log();
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
console.log(chalk.yellow("\n Service Manager is not running"));
|
|
843
|
+
console.log(chalk.gray(" Run: jfl service-manager start\n"));
|
|
844
|
+
}
|
|
845
|
+
break;
|
|
846
|
+
}
|
|
847
|
+
case "ensure": {
|
|
848
|
+
const status = isRunning();
|
|
849
|
+
if (status.running) {
|
|
850
|
+
// Already running, verify it's healthy
|
|
851
|
+
try {
|
|
852
|
+
const response = await fetch(`http://localhost:${port}/health`, {
|
|
853
|
+
signal: AbortSignal.timeout(2000)
|
|
854
|
+
});
|
|
855
|
+
if (response.ok) {
|
|
856
|
+
// Healthy and running, nothing to do
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
catch {
|
|
861
|
+
// Process exists but not responding, fall through to restart
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
// Check if port is blocked by orphaned process
|
|
865
|
+
const portInUse = await isPortInUse(port);
|
|
866
|
+
if (portInUse) {
|
|
867
|
+
// Port is in use - check if it's actually Service Manager
|
|
868
|
+
try {
|
|
869
|
+
const response = await fetch(`http://localhost:${port}/health`, {
|
|
870
|
+
signal: AbortSignal.timeout(2000)
|
|
871
|
+
});
|
|
872
|
+
if (response.ok) {
|
|
873
|
+
// It's a healthy Service Manager but PID file is missing/wrong
|
|
874
|
+
// Don't kill it - just return
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
// Process on port is not responding to health check
|
|
880
|
+
}
|
|
881
|
+
// Port in use but not responding - try to clean up
|
|
882
|
+
try {
|
|
883
|
+
const lsofOutput = execSync(`lsof -ti :${port}`, { encoding: 'utf-8' }).trim();
|
|
884
|
+
if (lsofOutput) {
|
|
885
|
+
const orphanedPid = parseInt(lsofOutput.split('\n')[0], 10);
|
|
886
|
+
// Only kill if it's different from our tracked PID
|
|
887
|
+
if (!status.pid || orphanedPid !== status.pid) {
|
|
888
|
+
process.kill(orphanedPid, 'SIGTERM');
|
|
889
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
// lsof failed or process already gone
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// Start silently
|
|
898
|
+
await startDaemon(port);
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
case "serve": {
|
|
902
|
+
// Run server in foreground (used by daemon)
|
|
903
|
+
const server = createServer(port);
|
|
904
|
+
let isListening = false;
|
|
905
|
+
process.on("uncaughtException", (err) => {
|
|
906
|
+
console.error(`Uncaught exception: ${err.message}`);
|
|
907
|
+
console.error(err.stack);
|
|
908
|
+
});
|
|
909
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
910
|
+
console.error(`Unhandled rejection at ${promise}: ${reason}`);
|
|
911
|
+
});
|
|
912
|
+
server.on("error", (err) => {
|
|
913
|
+
console.error(`Server error: ${err.message}`);
|
|
914
|
+
if (err.code === "EADDRINUSE") {
|
|
915
|
+
console.error(`Port ${port} is already in use. Exiting.`);
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
server.listen(port, () => {
|
|
920
|
+
isListening = true;
|
|
921
|
+
console.log(`Service Manager listening on port ${port}`);
|
|
922
|
+
console.log(`PID: ${process.pid}`);
|
|
923
|
+
console.log(`Ready to serve requests`);
|
|
924
|
+
});
|
|
925
|
+
const shutdown = () => {
|
|
926
|
+
if (!isListening) {
|
|
927
|
+
process.exit(0);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
console.log("Shutting down...");
|
|
931
|
+
server.close(() => {
|
|
932
|
+
console.log("Server closed");
|
|
933
|
+
process.exit(0);
|
|
934
|
+
});
|
|
935
|
+
setTimeout(() => {
|
|
936
|
+
console.log("Force exit after timeout");
|
|
937
|
+
process.exit(1);
|
|
938
|
+
}, 5000);
|
|
939
|
+
};
|
|
940
|
+
process.on("SIGTERM", shutdown);
|
|
941
|
+
process.on("SIGINT", shutdown);
|
|
942
|
+
const heartbeat = setInterval(() => {
|
|
943
|
+
if (isListening && Date.now() % 300000 < 60000) {
|
|
944
|
+
console.log(`Heartbeat: Still running (PID: ${process.pid})`);
|
|
945
|
+
}
|
|
946
|
+
}, 60000);
|
|
947
|
+
process.on("exit", () => {
|
|
948
|
+
clearInterval(heartbeat);
|
|
949
|
+
});
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
default: {
|
|
953
|
+
console.log(chalk.bold("\n Service Manager - API for JFL services\n"));
|
|
954
|
+
console.log(chalk.gray(" Commands:"));
|
|
955
|
+
console.log(" jfl service-manager start Start the daemon");
|
|
956
|
+
console.log(" jfl service-manager stop Stop the daemon");
|
|
957
|
+
console.log(" jfl service-manager restart Restart the daemon");
|
|
958
|
+
console.log(" jfl service-manager status Check if running");
|
|
959
|
+
console.log(" jfl service-manager ensure Start if not running (for hooks)");
|
|
960
|
+
console.log();
|
|
961
|
+
console.log(chalk.gray(" Options:"));
|
|
962
|
+
console.log(" --port <port> Port to run on (default: 3402)");
|
|
963
|
+
console.log();
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
//# sourceMappingURL=service-manager.js.map
|