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.
Files changed (262) hide show
  1. package/README.md +443 -145
  2. package/clawdbot-plugin/clawdbot.plugin.json +20 -0
  3. package/clawdbot-plugin/index.js +555 -0
  4. package/clawdbot-plugin/index.ts +582 -0
  5. package/clawdbot-skill/SKILL.md +33 -336
  6. package/clawdbot-skill/index.ts +491 -321
  7. package/clawdbot-skill/skill.json +4 -13
  8. package/dist/commands/clawdbot.d.ts +11 -0
  9. package/dist/commands/clawdbot.d.ts.map +1 -0
  10. package/dist/commands/clawdbot.js +215 -0
  11. package/dist/commands/clawdbot.js.map +1 -0
  12. package/dist/commands/context-hub.d.ts +5 -0
  13. package/dist/commands/context-hub.d.ts.map +1 -1
  14. package/dist/commands/context-hub.js +394 -28
  15. package/dist/commands/context-hub.js.map +1 -1
  16. package/dist/commands/gtm-process-update.d.ts +10 -0
  17. package/dist/commands/gtm-process-update.d.ts.map +1 -0
  18. package/dist/commands/gtm-process-update.js +101 -0
  19. package/dist/commands/gtm-process-update.js.map +1 -0
  20. package/dist/commands/init.d.ts.map +1 -1
  21. package/dist/commands/init.js +278 -4
  22. package/dist/commands/init.js.map +1 -1
  23. package/dist/commands/login.d.ts.map +1 -1
  24. package/dist/commands/login.js +32 -33
  25. package/dist/commands/login.js.map +1 -1
  26. package/dist/commands/memory.d.ts +38 -0
  27. package/dist/commands/memory.d.ts.map +1 -0
  28. package/dist/commands/memory.js +229 -0
  29. package/dist/commands/memory.js.map +1 -0
  30. package/dist/commands/migrate-services.d.ts +8 -0
  31. package/dist/commands/migrate-services.d.ts.map +1 -0
  32. package/dist/commands/migrate-services.js +182 -0
  33. package/dist/commands/migrate-services.js.map +1 -0
  34. package/dist/commands/onboard.d.ts +24 -0
  35. package/dist/commands/onboard.d.ts.map +1 -0
  36. package/dist/commands/onboard.js +663 -0
  37. package/dist/commands/onboard.js.map +1 -0
  38. package/dist/commands/openclaw.d.ts +56 -0
  39. package/dist/commands/openclaw.d.ts.map +1 -0
  40. package/dist/commands/openclaw.js +700 -0
  41. package/dist/commands/openclaw.js.map +1 -0
  42. package/dist/commands/orchestrate.d.ts +14 -0
  43. package/dist/commands/orchestrate.d.ts.map +1 -0
  44. package/dist/commands/orchestrate.js +270 -0
  45. package/dist/commands/orchestrate.js.map +1 -0
  46. package/dist/commands/profile.d.ts +46 -0
  47. package/dist/commands/profile.d.ts.map +1 -0
  48. package/dist/commands/profile.js +498 -0
  49. package/dist/commands/profile.js.map +1 -0
  50. package/dist/commands/repair.d.ts.map +1 -1
  51. package/dist/commands/repair.js +37 -0
  52. package/dist/commands/repair.js.map +1 -1
  53. package/dist/commands/service-agent.d.ts +16 -0
  54. package/dist/commands/service-agent.d.ts.map +1 -0
  55. package/dist/commands/service-agent.js +375 -0
  56. package/dist/commands/service-agent.js.map +1 -0
  57. package/dist/commands/service-manager.d.ts +12 -0
  58. package/dist/commands/service-manager.d.ts.map +1 -0
  59. package/dist/commands/service-manager.js +967 -0
  60. package/dist/commands/service-manager.js.map +1 -0
  61. package/dist/commands/service-validate.d.ts +12 -0
  62. package/dist/commands/service-validate.d.ts.map +1 -0
  63. package/dist/commands/service-validate.js +611 -0
  64. package/dist/commands/service-validate.js.map +1 -0
  65. package/dist/commands/services-create.d.ts +15 -0
  66. package/dist/commands/services-create.d.ts.map +1 -0
  67. package/dist/commands/services-create.js +1452 -0
  68. package/dist/commands/services-create.js.map +1 -0
  69. package/dist/commands/services-scan.d.ts +13 -0
  70. package/dist/commands/services-scan.d.ts.map +1 -0
  71. package/dist/commands/services-scan.js +251 -0
  72. package/dist/commands/services-scan.js.map +1 -0
  73. package/dist/commands/services-sync-agents.d.ts +23 -0
  74. package/dist/commands/services-sync-agents.d.ts.map +1 -0
  75. package/dist/commands/services-sync-agents.js +207 -0
  76. package/dist/commands/services-sync-agents.js.map +1 -0
  77. package/dist/commands/services.d.ts +19 -0
  78. package/dist/commands/services.d.ts.map +1 -0
  79. package/dist/commands/services.js +742 -0
  80. package/dist/commands/services.js.map +1 -0
  81. package/dist/commands/session.d.ts +5 -1
  82. package/dist/commands/session.d.ts.map +1 -1
  83. package/dist/commands/session.js +68 -586
  84. package/dist/commands/session.js.map +1 -1
  85. package/dist/commands/status.d.ts.map +1 -1
  86. package/dist/commands/status.js +17 -0
  87. package/dist/commands/status.js.map +1 -1
  88. package/dist/commands/update.d.ts.map +1 -1
  89. package/dist/commands/update.js +75 -21
  90. package/dist/commands/update.js.map +1 -1
  91. package/dist/commands/validate-settings.d.ts +37 -0
  92. package/dist/commands/validate-settings.d.ts.map +1 -0
  93. package/dist/commands/validate-settings.js +197 -0
  94. package/dist/commands/validate-settings.js.map +1 -0
  95. package/dist/commands/voice.d.ts +0 -1
  96. package/dist/commands/voice.d.ts.map +1 -1
  97. package/dist/commands/voice.js +16 -15
  98. package/dist/commands/voice.js.map +1 -1
  99. package/dist/index.js +395 -141
  100. package/dist/index.js.map +1 -1
  101. package/dist/lib/agent-generator.d.ts +26 -0
  102. package/dist/lib/agent-generator.d.ts.map +1 -0
  103. package/dist/lib/agent-generator.js +331 -0
  104. package/dist/lib/agent-generator.js.map +1 -0
  105. package/dist/lib/memory-db.d.ts +102 -0
  106. package/dist/lib/memory-db.d.ts.map +1 -0
  107. package/dist/lib/memory-db.js +313 -0
  108. package/dist/lib/memory-db.js.map +1 -0
  109. package/dist/lib/memory-indexer.d.ts +47 -0
  110. package/dist/lib/memory-indexer.d.ts.map +1 -0
  111. package/dist/lib/memory-indexer.js +215 -0
  112. package/dist/lib/memory-indexer.js.map +1 -0
  113. package/dist/lib/memory-search.d.ts +41 -0
  114. package/dist/lib/memory-search.d.ts.map +1 -0
  115. package/dist/lib/memory-search.js +246 -0
  116. package/dist/lib/memory-search.js.map +1 -0
  117. package/dist/lib/openclaw-registry.d.ts +48 -0
  118. package/dist/lib/openclaw-registry.d.ts.map +1 -0
  119. package/dist/lib/openclaw-registry.js +181 -0
  120. package/dist/lib/openclaw-registry.js.map +1 -0
  121. package/dist/lib/openclaw-sdk.d.ts +107 -0
  122. package/dist/lib/openclaw-sdk.d.ts.map +1 -0
  123. package/dist/lib/openclaw-sdk.js +208 -0
  124. package/dist/lib/openclaw-sdk.js.map +1 -0
  125. package/dist/lib/peer-agent-generator.d.ts +44 -0
  126. package/dist/lib/peer-agent-generator.d.ts.map +1 -0
  127. package/dist/lib/peer-agent-generator.js +286 -0
  128. package/dist/lib/peer-agent-generator.js.map +1 -0
  129. package/dist/lib/service-dependencies.d.ts +44 -0
  130. package/dist/lib/service-dependencies.d.ts.map +1 -0
  131. package/dist/lib/service-dependencies.js +314 -0
  132. package/dist/lib/service-dependencies.js.map +1 -0
  133. package/dist/lib/service-detector.d.ts +61 -0
  134. package/dist/lib/service-detector.d.ts.map +1 -0
  135. package/dist/lib/service-detector.js +521 -0
  136. package/dist/lib/service-detector.js.map +1 -0
  137. package/dist/lib/service-gtm.d.ts +157 -0
  138. package/dist/lib/service-gtm.d.ts.map +1 -0
  139. package/dist/lib/service-gtm.js +786 -0
  140. package/dist/lib/service-gtm.js.map +1 -0
  141. package/dist/lib/service-mcp-base.d.ts +103 -0
  142. package/dist/lib/service-mcp-base.d.ts.map +1 -0
  143. package/dist/lib/service-mcp-base.js +263 -0
  144. package/dist/lib/service-mcp-base.js.map +1 -0
  145. package/dist/lib/service-utils.d.ts +103 -0
  146. package/dist/lib/service-utils.d.ts.map +1 -0
  147. package/dist/lib/service-utils.js +368 -0
  148. package/dist/lib/service-utils.js.map +1 -0
  149. package/dist/lib/skill-generator.d.ts +21 -0
  150. package/dist/lib/skill-generator.d.ts.map +1 -0
  151. package/dist/lib/skill-generator.js +253 -0
  152. package/dist/lib/skill-generator.js.map +1 -0
  153. package/dist/lib/stratus-client.d.ts +100 -0
  154. package/dist/lib/stratus-client.d.ts.map +1 -0
  155. package/dist/lib/stratus-client.js +255 -0
  156. package/dist/lib/stratus-client.js.map +1 -0
  157. package/dist/mcp/context-hub-mcp.js +135 -53
  158. package/dist/mcp/context-hub-mcp.js.map +1 -1
  159. package/dist/mcp/service-mcp-server.d.ts +12 -0
  160. package/dist/mcp/service-mcp-server.d.ts.map +1 -0
  161. package/dist/mcp/service-mcp-server.js +434 -0
  162. package/dist/mcp/service-mcp-server.js.map +1 -0
  163. package/dist/mcp/service-peer-mcp.d.ts +36 -0
  164. package/dist/mcp/service-peer-mcp.d.ts.map +1 -0
  165. package/dist/mcp/service-peer-mcp.js +220 -0
  166. package/dist/mcp/service-peer-mcp.js.map +1 -0
  167. package/dist/mcp/service-registry-mcp.d.ts +13 -0
  168. package/dist/mcp/service-registry-mcp.d.ts.map +1 -0
  169. package/dist/mcp/service-registry-mcp.js +330 -0
  170. package/dist/mcp/service-registry-mcp.js.map +1 -0
  171. package/dist/ui/banner.js +1 -1
  172. package/dist/ui/banner.js.map +1 -1
  173. package/dist/ui/context-hub-logs.d.ts +10 -0
  174. package/dist/ui/context-hub-logs.d.ts.map +1 -0
  175. package/dist/ui/context-hub-logs.js +175 -0
  176. package/dist/ui/context-hub-logs.js.map +1 -0
  177. package/dist/ui/service-dashboard.d.ts +11 -0
  178. package/dist/ui/service-dashboard.d.ts.map +1 -0
  179. package/dist/ui/service-dashboard.js +357 -0
  180. package/dist/ui/service-dashboard.js.map +1 -0
  181. package/dist/ui/services-manager.d.ts +11 -0
  182. package/dist/ui/services-manager.d.ts.map +1 -0
  183. package/dist/ui/services-manager.js +507 -0
  184. package/dist/ui/services-manager.js.map +1 -0
  185. package/dist/utils/auth-guard.d.ts.map +1 -1
  186. package/dist/utils/auth-guard.js +8 -9
  187. package/dist/utils/auth-guard.js.map +1 -1
  188. package/dist/utils/claude-md-generator.d.ts +10 -0
  189. package/dist/utils/claude-md-generator.d.ts.map +1 -0
  190. package/dist/utils/claude-md-generator.js +215 -0
  191. package/dist/utils/claude-md-generator.js.map +1 -0
  192. package/dist/utils/ensure-context-hub.d.ts +20 -0
  193. package/dist/utils/ensure-context-hub.d.ts.map +1 -0
  194. package/dist/utils/ensure-context-hub.js +65 -0
  195. package/dist/utils/ensure-context-hub.js.map +1 -0
  196. package/dist/utils/ensure-project.d.ts.map +1 -1
  197. package/dist/utils/ensure-project.js +3 -4
  198. package/dist/utils/ensure-project.js.map +1 -1
  199. package/dist/utils/jfl-config.d.ts +19 -0
  200. package/dist/utils/jfl-config.d.ts.map +1 -0
  201. package/dist/utils/jfl-config.js +112 -0
  202. package/dist/utils/jfl-config.js.map +1 -0
  203. package/dist/utils/jfl-migration.d.ts +29 -0
  204. package/dist/utils/jfl-migration.d.ts.map +1 -0
  205. package/dist/utils/jfl-migration.js +142 -0
  206. package/dist/utils/jfl-migration.js.map +1 -0
  207. package/dist/utils/jfl-paths.d.ts +55 -0
  208. package/dist/utils/jfl-paths.d.ts.map +1 -0
  209. package/dist/utils/jfl-paths.js +120 -0
  210. package/dist/utils/jfl-paths.js.map +1 -0
  211. package/dist/utils/settings-validator.d.ts +73 -0
  212. package/dist/utils/settings-validator.d.ts.map +1 -0
  213. package/dist/utils/settings-validator.js +222 -0
  214. package/dist/utils/settings-validator.js.map +1 -0
  215. package/package.json +19 -3
  216. package/scripts/commit-gtm.sh +56 -0
  217. package/scripts/commit-product.sh +68 -0
  218. package/scripts/context-query.sh +45 -0
  219. package/scripts/session/auto-commit.sh +297 -0
  220. package/scripts/session/jfl-doctor.sh +707 -0
  221. package/scripts/session/session-cleanup.sh +268 -0
  222. package/scripts/session/session-end.sh +198 -0
  223. package/scripts/session/session-init.sh +350 -0
  224. package/scripts/session/session-init.sh.backup +292 -0
  225. package/scripts/session/session-sync.sh +167 -0
  226. package/scripts/session/test-context-preservation.sh +160 -0
  227. package/scripts/session/test-critical-infrastructure.sh +293 -0
  228. package/scripts/session/test-experience-level.sh +336 -0
  229. package/scripts/session/test-session-cleanup.sh +268 -0
  230. package/scripts/session/test-session-sync.sh +320 -0
  231. package/scripts/voice-start.sh +36 -8
  232. package/scripts/where-am-i.sh +78 -0
  233. package/template/.claude/service-settings.json +32 -0
  234. package/template/.claude/settings.json +14 -1
  235. package/template/.claude/skills/end/SKILL.md +1780 -0
  236. package/template/.jfl/config.json +2 -1
  237. package/template/CLAUDE.md +1039 -134
  238. package/template/CLAUDE.md.bak +1187 -0
  239. package/template/scripts/commit-gtm.sh +56 -0
  240. package/template/scripts/commit-product.sh +68 -0
  241. package/template/scripts/migrate-to-branch-sessions.sh +201 -0
  242. package/template/scripts/session/auto-commit.sh +58 -6
  243. package/template/scripts/session/jfl-doctor.sh +137 -17
  244. package/template/scripts/session/session-cleanup.sh +268 -0
  245. package/template/scripts/session/session-end.sh +4 -0
  246. package/template/scripts/session/session-init.sh +253 -66
  247. package/template/scripts/session/test-critical-infrastructure.sh +293 -0
  248. package/template/scripts/session/test-experience-level.sh +336 -0
  249. package/template/scripts/session/test-session-cleanup.sh +268 -0
  250. package/template/scripts/session/test-session-sync.sh +320 -0
  251. package/template/scripts/where-am-i.sh +78 -0
  252. package/template/templates/service-agent/.claude/settings.json +32 -0
  253. package/template/templates/service-agent/CLAUDE.md +334 -0
  254. package/template/templates/service-agent/knowledge/ARCHITECTURE.md +115 -0
  255. package/template/templates/service-agent/knowledge/DEPLOYMENT.md +199 -0
  256. package/template/templates/service-agent/knowledge/RUNBOOK.md +412 -0
  257. package/template/templates/service-agent/knowledge/SERVICE_SPEC.md +77 -0
  258. package/dist/commands/session-mgmt.d.ts +0 -33
  259. package/dist/commands/session-mgmt.d.ts.map +0 -1
  260. package/dist/commands/session-mgmt.js +0 -404
  261. package/dist/commands/session-mgmt.js.map +0 -1
  262. 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