@portel/photon 1.4.1 → 1.5.1

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 (379) hide show
  1. package/README.md +287 -1160
  2. package/dist/auto-ui/beam.d.ts +9 -0
  3. package/dist/auto-ui/beam.d.ts.map +1 -0
  4. package/dist/auto-ui/beam.js +2381 -0
  5. package/dist/auto-ui/beam.js.map +1 -0
  6. package/dist/auto-ui/components/card.d.ts +13 -0
  7. package/dist/auto-ui/components/card.d.ts.map +1 -0
  8. package/dist/auto-ui/components/card.js +64 -0
  9. package/dist/auto-ui/components/card.js.map +1 -0
  10. package/dist/auto-ui/components/form.d.ts +15 -0
  11. package/dist/auto-ui/components/form.d.ts.map +1 -0
  12. package/dist/auto-ui/components/form.js +72 -0
  13. package/dist/auto-ui/components/form.js.map +1 -0
  14. package/dist/auto-ui/components/list.d.ts +13 -0
  15. package/dist/auto-ui/components/list.d.ts.map +1 -0
  16. package/dist/auto-ui/components/list.js +58 -0
  17. package/dist/auto-ui/components/list.js.map +1 -0
  18. package/dist/auto-ui/components/progress.d.ts +18 -0
  19. package/dist/auto-ui/components/progress.d.ts.map +1 -0
  20. package/dist/auto-ui/components/progress.js +125 -0
  21. package/dist/auto-ui/components/progress.js.map +1 -0
  22. package/dist/auto-ui/components/table.d.ts +13 -0
  23. package/dist/auto-ui/components/table.d.ts.map +1 -0
  24. package/dist/auto-ui/components/table.js +82 -0
  25. package/dist/auto-ui/components/table.js.map +1 -0
  26. package/dist/auto-ui/components/tree.d.ts +13 -0
  27. package/dist/auto-ui/components/tree.d.ts.map +1 -0
  28. package/dist/auto-ui/components/tree.js +61 -0
  29. package/dist/auto-ui/components/tree.js.map +1 -0
  30. package/dist/auto-ui/daemon-tools.d.ts +45 -0
  31. package/dist/auto-ui/daemon-tools.d.ts.map +1 -0
  32. package/dist/auto-ui/daemon-tools.js +580 -0
  33. package/dist/auto-ui/daemon-tools.js.map +1 -0
  34. package/dist/auto-ui/design-system/index.d.ts +21 -0
  35. package/dist/auto-ui/design-system/index.d.ts.map +1 -0
  36. package/dist/auto-ui/design-system/index.js +27 -0
  37. package/dist/auto-ui/design-system/index.js.map +1 -0
  38. package/dist/auto-ui/design-system/tokens.d.ts +9 -0
  39. package/dist/auto-ui/design-system/tokens.d.ts.map +1 -0
  40. package/dist/auto-ui/design-system/tokens.js +27 -0
  41. package/dist/auto-ui/design-system/tokens.js.map +1 -0
  42. package/dist/auto-ui/design-system/transaction-ui.d.ts +70 -0
  43. package/dist/auto-ui/design-system/transaction-ui.d.ts.map +1 -0
  44. package/dist/auto-ui/design-system/transaction-ui.js +982 -0
  45. package/dist/auto-ui/design-system/transaction-ui.js.map +1 -0
  46. package/dist/auto-ui/frontend/index.html +84 -0
  47. package/dist/auto-ui/index.d.ts +21 -0
  48. package/dist/auto-ui/index.d.ts.map +1 -0
  49. package/dist/auto-ui/index.js +25 -0
  50. package/dist/auto-ui/index.js.map +1 -0
  51. package/dist/auto-ui/openapi-generator.d.ts +71 -0
  52. package/dist/auto-ui/openapi-generator.d.ts.map +1 -0
  53. package/dist/auto-ui/openapi-generator.js +223 -0
  54. package/dist/auto-ui/openapi-generator.js.map +1 -0
  55. package/dist/auto-ui/photon-bridge.d.ts +159 -0
  56. package/dist/auto-ui/photon-bridge.d.ts.map +1 -0
  57. package/dist/auto-ui/photon-bridge.js +262 -0
  58. package/dist/auto-ui/photon-bridge.js.map +1 -0
  59. package/dist/auto-ui/photon-host.d.ts +113 -0
  60. package/dist/auto-ui/photon-host.d.ts.map +1 -0
  61. package/dist/auto-ui/photon-host.js +284 -0
  62. package/dist/auto-ui/photon-host.js.map +1 -0
  63. package/dist/auto-ui/platform-compat.d.ts +71 -0
  64. package/dist/auto-ui/platform-compat.d.ts.map +1 -0
  65. package/dist/auto-ui/platform-compat.js +574 -0
  66. package/dist/auto-ui/platform-compat.js.map +1 -0
  67. package/dist/auto-ui/playground-html.d.ts +15 -0
  68. package/dist/auto-ui/playground-html.d.ts.map +1 -0
  69. package/dist/auto-ui/playground-html.js +1113 -0
  70. package/dist/auto-ui/playground-html.js.map +1 -0
  71. package/dist/auto-ui/playground-server.d.ts +7 -0
  72. package/dist/auto-ui/playground-server.d.ts.map +1 -0
  73. package/dist/auto-ui/playground-server.js +840 -0
  74. package/dist/auto-ui/playground-server.js.map +1 -0
  75. package/dist/auto-ui/registry.d.ts +13 -0
  76. package/dist/auto-ui/registry.d.ts.map +1 -0
  77. package/dist/auto-ui/registry.js +62 -0
  78. package/dist/auto-ui/registry.js.map +1 -0
  79. package/dist/auto-ui/renderer.d.ts +14 -0
  80. package/dist/auto-ui/renderer.d.ts.map +1 -0
  81. package/dist/auto-ui/renderer.js +88 -0
  82. package/dist/auto-ui/renderer.js.map +1 -0
  83. package/dist/auto-ui/rendering/components.d.ts +29 -0
  84. package/dist/auto-ui/rendering/components.d.ts.map +1 -0
  85. package/dist/auto-ui/rendering/components.js +773 -0
  86. package/dist/auto-ui/rendering/components.js.map +1 -0
  87. package/dist/auto-ui/rendering/field-analyzer.d.ts +48 -0
  88. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -0
  89. package/dist/auto-ui/rendering/field-analyzer.js +270 -0
  90. package/dist/auto-ui/rendering/field-analyzer.js.map +1 -0
  91. package/dist/auto-ui/rendering/field-renderers.d.ts +64 -0
  92. package/dist/auto-ui/rendering/field-renderers.d.ts.map +1 -0
  93. package/dist/auto-ui/rendering/field-renderers.js +317 -0
  94. package/dist/auto-ui/rendering/field-renderers.js.map +1 -0
  95. package/dist/auto-ui/rendering/index.d.ts +28 -0
  96. package/dist/auto-ui/rendering/index.d.ts.map +1 -0
  97. package/dist/auto-ui/rendering/index.js +60 -0
  98. package/dist/auto-ui/rendering/index.js.map +1 -0
  99. package/dist/auto-ui/rendering/layout-selector.d.ts +48 -0
  100. package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -0
  101. package/dist/auto-ui/rendering/layout-selector.js +352 -0
  102. package/dist/auto-ui/rendering/layout-selector.js.map +1 -0
  103. package/dist/auto-ui/rendering/template-engine.d.ts +41 -0
  104. package/dist/auto-ui/rendering/template-engine.d.ts.map +1 -0
  105. package/dist/auto-ui/rendering/template-engine.js +238 -0
  106. package/dist/auto-ui/rendering/template-engine.js.map +1 -0
  107. package/dist/auto-ui/streamable-http-transport.d.ts +79 -0
  108. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -0
  109. package/dist/auto-ui/streamable-http-transport.js +1314 -0
  110. package/dist/auto-ui/streamable-http-transport.js.map +1 -0
  111. package/dist/auto-ui/types.d.ts +310 -0
  112. package/dist/auto-ui/types.d.ts.map +1 -0
  113. package/dist/auto-ui/types.js +71 -0
  114. package/dist/auto-ui/types.js.map +1 -0
  115. package/dist/beam.bundle.js +13506 -0
  116. package/dist/beam.bundle.js.map +7 -0
  117. package/dist/claude-code-plugin.d.ts.map +1 -1
  118. package/dist/claude-code-plugin.js +30 -30
  119. package/dist/claude-code-plugin.js.map +1 -1
  120. package/dist/cli/commands/info.d.ts +11 -0
  121. package/dist/cli/commands/info.d.ts.map +1 -0
  122. package/dist/cli/commands/info.js +313 -0
  123. package/dist/cli/commands/info.js.map +1 -0
  124. package/dist/cli/commands/marketplace.d.ts +11 -0
  125. package/dist/cli/commands/marketplace.d.ts.map +1 -0
  126. package/dist/cli/commands/marketplace.js +198 -0
  127. package/dist/cli/commands/marketplace.js.map +1 -0
  128. package/dist/cli/commands/package-app.d.ts +9 -0
  129. package/dist/cli/commands/package-app.d.ts.map +1 -0
  130. package/dist/cli/commands/package-app.js +191 -0
  131. package/dist/cli/commands/package-app.js.map +1 -0
  132. package/dist/cli/commands/package.d.ts +11 -0
  133. package/dist/cli/commands/package.d.ts.map +1 -0
  134. package/dist/cli/commands/package.js +573 -0
  135. package/dist/cli/commands/package.js.map +1 -0
  136. package/dist/cli-alias.d.ts.map +1 -1
  137. package/dist/cli-alias.js +30 -28
  138. package/dist/cli-alias.js.map +1 -1
  139. package/dist/cli-formatter.d.ts +8 -24
  140. package/dist/cli-formatter.d.ts.map +1 -1
  141. package/dist/cli-formatter.js +8 -325
  142. package/dist/cli-formatter.js.map +1 -1
  143. package/dist/cli.d.ts +15 -1
  144. package/dist/cli.d.ts.map +1 -1
  145. package/dist/cli.js +1157 -1132
  146. package/dist/cli.js.map +1 -1
  147. package/dist/daemon/client.d.ts +79 -0
  148. package/dist/daemon/client.d.ts.map +1 -1
  149. package/dist/daemon/client.js +532 -8
  150. package/dist/daemon/client.js.map +1 -1
  151. package/dist/daemon/manager.d.ts +46 -12
  152. package/dist/daemon/manager.d.ts.map +1 -1
  153. package/dist/daemon/manager.js +102 -61
  154. package/dist/daemon/manager.js.map +1 -1
  155. package/dist/daemon/protocol.d.ts +62 -6
  156. package/dist/daemon/protocol.d.ts.map +1 -1
  157. package/dist/daemon/protocol.js +76 -1
  158. package/dist/daemon/protocol.js.map +1 -1
  159. package/dist/daemon/server.d.ts +6 -6
  160. package/dist/daemon/server.js +743 -133
  161. package/dist/daemon/server.js.map +1 -1
  162. package/dist/daemon/session-manager.d.ts +8 -1
  163. package/dist/daemon/session-manager.d.ts.map +1 -1
  164. package/dist/daemon/session-manager.js +32 -9
  165. package/dist/daemon/session-manager.js.map +1 -1
  166. package/dist/deploy/cloudflare.d.ts +12 -0
  167. package/dist/deploy/cloudflare.d.ts.map +1 -0
  168. package/dist/deploy/cloudflare.js +216 -0
  169. package/dist/deploy/cloudflare.js.map +1 -0
  170. package/dist/index.d.ts +1 -0
  171. package/dist/index.d.ts.map +1 -1
  172. package/dist/index.js +3 -0
  173. package/dist/index.js.map +1 -1
  174. package/dist/loader.d.ts +168 -21
  175. package/dist/loader.d.ts.map +1 -1
  176. package/dist/loader.js +1120 -318
  177. package/dist/loader.js.map +1 -1
  178. package/dist/markdown-utils.d.ts +8 -0
  179. package/dist/markdown-utils.d.ts.map +1 -0
  180. package/dist/markdown-utils.js +63 -0
  181. package/dist/markdown-utils.js.map +1 -0
  182. package/dist/marketplace-manager.d.ts +10 -0
  183. package/dist/marketplace-manager.d.ts.map +1 -1
  184. package/dist/marketplace-manager.js +112 -28
  185. package/dist/marketplace-manager.js.map +1 -1
  186. package/dist/mcp-client.d.ts +9 -0
  187. package/dist/mcp-client.d.ts.map +1 -0
  188. package/dist/mcp-client.js +11 -0
  189. package/dist/mcp-client.js.map +1 -0
  190. package/dist/mcp-elicitation.d.ts +32 -0
  191. package/dist/mcp-elicitation.d.ts.map +1 -0
  192. package/dist/mcp-elicitation.js +26 -0
  193. package/dist/mcp-elicitation.js.map +1 -0
  194. package/dist/path-resolver.d.ts +9 -12
  195. package/dist/path-resolver.d.ts.map +1 -1
  196. package/dist/path-resolver.js +13 -43
  197. package/dist/path-resolver.js.map +1 -1
  198. package/dist/photon-cli-runner.d.ts.map +1 -1
  199. package/dist/photon-cli-runner.js +202 -77
  200. package/dist/photon-cli-runner.js.map +1 -1
  201. package/dist/photon-doc-extractor.d.ts +88 -0
  202. package/dist/photon-doc-extractor.d.ts.map +1 -1
  203. package/dist/photon-doc-extractor.js +536 -27
  204. package/dist/photon-doc-extractor.js.map +1 -1
  205. package/dist/photons/maker.photon.d.ts +182 -0
  206. package/dist/photons/maker.photon.d.ts.map +1 -0
  207. package/dist/photons/maker.photon.js +504 -0
  208. package/dist/photons/maker.photon.js.map +1 -0
  209. package/dist/photons/maker.photon.ts +626 -0
  210. package/dist/photons/marketplace.photon.d.ts +110 -0
  211. package/dist/photons/marketplace.photon.d.ts.map +1 -0
  212. package/dist/photons/marketplace.photon.js +260 -0
  213. package/dist/photons/marketplace.photon.js.map +1 -0
  214. package/dist/photons/marketplace.photon.ts +378 -0
  215. package/dist/photons/tunnel.photon.d.ts +80 -0
  216. package/dist/photons/tunnel.photon.d.ts.map +1 -0
  217. package/dist/photons/tunnel.photon.js +269 -0
  218. package/dist/photons/tunnel.photon.js.map +1 -0
  219. package/dist/photons/tunnel.photon.ts +345 -0
  220. package/dist/security-scanner.d.ts.map +1 -1
  221. package/dist/security-scanner.js +18 -15
  222. package/dist/security-scanner.js.map +1 -1
  223. package/dist/serv/auth/jwt.d.ts +89 -0
  224. package/dist/serv/auth/jwt.d.ts.map +1 -0
  225. package/dist/serv/auth/jwt.js +239 -0
  226. package/dist/serv/auth/jwt.js.map +1 -0
  227. package/dist/serv/auth/oauth.d.ts +117 -0
  228. package/dist/serv/auth/oauth.d.ts.map +1 -0
  229. package/dist/serv/auth/oauth.js +395 -0
  230. package/dist/serv/auth/oauth.js.map +1 -0
  231. package/dist/serv/auth/well-known.d.ts +60 -0
  232. package/dist/serv/auth/well-known.d.ts.map +1 -0
  233. package/dist/serv/auth/well-known.js +154 -0
  234. package/dist/serv/auth/well-known.js.map +1 -0
  235. package/dist/serv/db/d1-client.d.ts +65 -0
  236. package/dist/serv/db/d1-client.d.ts.map +1 -0
  237. package/dist/serv/db/d1-client.js +137 -0
  238. package/dist/serv/db/d1-client.js.map +1 -0
  239. package/dist/serv/db/d1-stores.d.ts +62 -0
  240. package/dist/serv/db/d1-stores.d.ts.map +1 -0
  241. package/dist/serv/db/d1-stores.js +307 -0
  242. package/dist/serv/db/d1-stores.js.map +1 -0
  243. package/dist/serv/index.d.ts +114 -0
  244. package/dist/serv/index.d.ts.map +1 -0
  245. package/dist/serv/index.js +172 -0
  246. package/dist/serv/index.js.map +1 -0
  247. package/dist/serv/local.d.ts +118 -0
  248. package/dist/serv/local.d.ts.map +1 -0
  249. package/dist/serv/local.js +392 -0
  250. package/dist/serv/local.js.map +1 -0
  251. package/dist/serv/middleware/auth.d.ts +66 -0
  252. package/dist/serv/middleware/auth.d.ts.map +1 -0
  253. package/dist/serv/middleware/auth.js +178 -0
  254. package/dist/serv/middleware/auth.js.map +1 -0
  255. package/dist/serv/middleware/tenant.d.ts +94 -0
  256. package/dist/serv/middleware/tenant.d.ts.map +1 -0
  257. package/dist/serv/middleware/tenant.js +152 -0
  258. package/dist/serv/middleware/tenant.js.map +1 -0
  259. package/dist/serv/runtime/executor.d.ts +76 -0
  260. package/dist/serv/runtime/executor.d.ts.map +1 -0
  261. package/dist/serv/runtime/executor.js +105 -0
  262. package/dist/serv/runtime/executor.js.map +1 -0
  263. package/dist/serv/runtime/index.d.ts +8 -0
  264. package/dist/serv/runtime/index.d.ts.map +1 -0
  265. package/dist/serv/runtime/index.js +10 -0
  266. package/dist/serv/runtime/index.js.map +1 -0
  267. package/dist/serv/runtime/oauth-context.d.ts +121 -0
  268. package/dist/serv/runtime/oauth-context.d.ts.map +1 -0
  269. package/dist/serv/runtime/oauth-context.js +153 -0
  270. package/dist/serv/runtime/oauth-context.js.map +1 -0
  271. package/dist/serv/session/kv-store.d.ts +54 -0
  272. package/dist/serv/session/kv-store.d.ts.map +1 -0
  273. package/dist/serv/session/kv-store.js +149 -0
  274. package/dist/serv/session/kv-store.js.map +1 -0
  275. package/dist/serv/session/store.d.ts +113 -0
  276. package/dist/serv/session/store.d.ts.map +1 -0
  277. package/dist/serv/session/store.js +284 -0
  278. package/dist/serv/session/store.js.map +1 -0
  279. package/dist/serv/types/index.d.ts +147 -0
  280. package/dist/serv/types/index.d.ts.map +1 -0
  281. package/dist/serv/types/index.js +8 -0
  282. package/dist/serv/types/index.js.map +1 -0
  283. package/dist/serv/vault/token-vault.d.ts +102 -0
  284. package/dist/serv/vault/token-vault.d.ts.map +1 -0
  285. package/dist/serv/vault/token-vault.js +177 -0
  286. package/dist/serv/vault/token-vault.js.map +1 -0
  287. package/dist/server.d.ts +173 -0
  288. package/dist/server.d.ts.map +1 -1
  289. package/dist/server.js +1622 -86
  290. package/dist/server.js.map +1 -1
  291. package/dist/shared/cli-sections.d.ts +6 -0
  292. package/dist/shared/cli-sections.d.ts.map +1 -0
  293. package/dist/shared/cli-sections.js +16 -0
  294. package/dist/shared/cli-sections.js.map +1 -0
  295. package/dist/shared/cli-utils.d.ts +81 -0
  296. package/dist/shared/cli-utils.d.ts.map +1 -0
  297. package/dist/shared/cli-utils.js +174 -0
  298. package/dist/shared/cli-utils.js.map +1 -0
  299. package/dist/shared/config-docs.d.ts +6 -0
  300. package/dist/shared/config-docs.d.ts.map +1 -0
  301. package/dist/shared/config-docs.js +6 -0
  302. package/dist/shared/config-docs.js.map +1 -0
  303. package/dist/shared/error-handler.d.ts +128 -0
  304. package/dist/shared/error-handler.d.ts.map +1 -0
  305. package/dist/shared/error-handler.js +342 -0
  306. package/dist/shared/error-handler.js.map +1 -0
  307. package/dist/shared/logger.d.ts +42 -0
  308. package/dist/shared/logger.d.ts.map +1 -0
  309. package/dist/shared/logger.js +123 -0
  310. package/dist/shared/logger.js.map +1 -0
  311. package/dist/shared/performance.d.ts +65 -0
  312. package/dist/shared/performance.d.ts.map +1 -0
  313. package/dist/shared/performance.js +136 -0
  314. package/dist/shared/performance.js.map +1 -0
  315. package/dist/shared/task-runner.d.ts +2 -0
  316. package/dist/shared/task-runner.d.ts.map +1 -0
  317. package/dist/shared/task-runner.js +16 -0
  318. package/dist/shared/task-runner.js.map +1 -0
  319. package/dist/shared/validation.d.ts +6 -0
  320. package/dist/shared/validation.d.ts.map +1 -0
  321. package/dist/shared/validation.js +6 -0
  322. package/dist/shared/validation.js.map +1 -0
  323. package/dist/shared-utils.d.ts +63 -0
  324. package/dist/shared-utils.d.ts.map +1 -0
  325. package/dist/shared-utils.js +123 -0
  326. package/dist/shared-utils.js.map +1 -0
  327. package/dist/template-manager.d.ts +23 -2
  328. package/dist/template-manager.d.ts.map +1 -1
  329. package/dist/template-manager.js +175 -86
  330. package/dist/template-manager.js.map +1 -1
  331. package/dist/test-client.d.ts.map +1 -1
  332. package/dist/test-client.js +10 -8
  333. package/dist/test-client.js.map +1 -1
  334. package/dist/test-runner.d.ts +52 -0
  335. package/dist/test-runner.d.ts.map +1 -0
  336. package/dist/test-runner.js +785 -0
  337. package/dist/test-runner.js.map +1 -0
  338. package/dist/testing.d.ts +103 -0
  339. package/dist/testing.d.ts.map +1 -0
  340. package/dist/testing.js +163 -0
  341. package/dist/testing.js.map +1 -0
  342. package/dist/version-checker.d.ts.map +1 -1
  343. package/dist/version-checker.js +2 -2
  344. package/dist/version-checker.js.map +1 -1
  345. package/dist/version.d.ts +2 -0
  346. package/dist/version.d.ts.map +1 -0
  347. package/dist/version.js +5 -0
  348. package/dist/version.js.map +1 -0
  349. package/dist/watcher.d.ts +6 -3
  350. package/dist/watcher.d.ts.map +1 -1
  351. package/dist/watcher.js +49 -10
  352. package/dist/watcher.js.map +1 -1
  353. package/package.json +47 -7
  354. package/templates/cloudflare/worker.ts.template +381 -0
  355. package/templates/cloudflare/wrangler.toml.template +9 -0
  356. package/dist/base.d.ts +0 -58
  357. package/dist/base.d.ts.map +0 -1
  358. package/dist/base.js +0 -92
  359. package/dist/base.js.map +0 -1
  360. package/dist/dependency-manager.d.ts +0 -49
  361. package/dist/dependency-manager.d.ts.map +0 -1
  362. package/dist/dependency-manager.js +0 -165
  363. package/dist/dependency-manager.js.map +0 -1
  364. package/dist/registry-manager.d.ts +0 -76
  365. package/dist/registry-manager.d.ts.map +0 -1
  366. package/dist/registry-manager.js +0 -220
  367. package/dist/registry-manager.js.map +0 -1
  368. package/dist/schema-extractor.d.ts +0 -110
  369. package/dist/schema-extractor.d.ts.map +0 -1
  370. package/dist/schema-extractor.js +0 -727
  371. package/dist/schema-extractor.js.map +0 -1
  372. package/dist/test-marketplace-sources.d.ts +0 -5
  373. package/dist/test-marketplace-sources.d.ts.map +0 -1
  374. package/dist/test-marketplace-sources.js +0 -53
  375. package/dist/test-marketplace-sources.js.map +0 -1
  376. package/dist/types.d.ts +0 -109
  377. package/dist/types.d.ts.map +0 -1
  378. package/dist/types.js +0 -12
  379. package/dist/types.js.map +0 -1
package/dist/cli.js CHANGED
@@ -9,15 +9,305 @@ import * as path from 'path';
9
9
  import * as fs from 'fs/promises';
10
10
  import { existsSync } from 'fs';
11
11
  import * as os from 'os';
12
- import * as readline from 'readline';
12
+ import * as net from 'net';
13
13
  import { PhotonServer } from './server.js';
14
14
  import { FileWatcher } from './watcher.js';
15
- import { resolvePhotonPath, listPhotonMCPs, ensureWorkingDir, DEFAULT_WORKING_DIR } from './path-resolver.js';
15
+ import { resolvePhotonPath, listPhotonMCPs, ensureWorkingDir, DEFAULT_WORKING_DIR, } from './path-resolver.js';
16
16
  import { SchemaExtractor } from '@portel/photon-core';
17
17
  import { createRequire } from 'module';
18
18
  import { fileURLToPath } from 'url';
19
19
  const require = createRequire(import.meta.url);
20
20
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ import { getBundledPhotonPath, DEFAULT_BUNDLED_PHOTONS } from './shared-utils.js';
22
+ import { PHOTON_VERSION } from './version.js';
23
+ import { toEnvVarName } from './shared/config-docs.js';
24
+ import { runTask } from './shared/task-runner.js';
25
+ import { normalizeLogLevel, logger } from './shared/logger.js';
26
+ import { printHeader, printInfo, printWarning, printError, printSuccess } from './cli-formatter.js';
27
+ import { handleError, getErrorMessage, ExitCode, exitWithError, } from './shared/error-handler.js';
28
+ import { validateOrThrow, inRange, isPositive, isInteger } from './shared/validation.js';
29
+ import { createReadline, promptWait } from './shared/cli-utils.js';
30
+ import { registerMarketplaceCommands } from './cli/commands/marketplace.js';
31
+ import { registerInfoCommand } from './cli/commands/info.js';
32
+ import { registerPackageCommands } from './cli/commands/package.js';
33
+ import { registerPackageAppCommand } from './cli/commands/package-app.js';
34
+ // ══════════════════════════════════════════════════════════════════════════════
35
+ // BUNDLED PHOTONS
36
+ // ══════════════════════════════════════════════════════════════════════════════
37
+ /** Bundled photon names that ship with the runtime */
38
+ // BUNDLED_PHOTONS and getBundledPhotonPath are imported from shared-utils.js
39
+ /**
40
+ * Parse extended photon name format
41
+ *
42
+ * Supports:
43
+ * - "rss-feed" → { name: "rss-feed" }
44
+ * - "alice/custom-photons:rss-feed" → { name: "rss-feed", marketplaceSource: "alice/custom-photons" }
45
+ *
46
+ * Rule: colon splits only when left side contains `/` (a marketplace source)
47
+ * and right side is a simple name (no `/`).
48
+ */
49
+ export function parsePhotonSpec(spec) {
50
+ const colonIndex = spec.indexOf(':');
51
+ if (colonIndex > 0) {
52
+ const left = spec.slice(0, colonIndex);
53
+ const right = spec.slice(colonIndex + 1);
54
+ // Left must contain `/` (marketplace source) and right must be a simple name
55
+ if (left.includes('/') && right && !right.includes('/')) {
56
+ return { name: right, marketplaceSource: left };
57
+ }
58
+ }
59
+ return { name: spec };
60
+ }
61
+ /**
62
+ * Resolve photon path - checks bundled first, then user directory
63
+ */
64
+ async function resolvePhotonPathWithBundled(name, workingDir) {
65
+ // Check bundled photons first
66
+ const bundledPath = getBundledPhotonPath(name, __dirname);
67
+ if (bundledPath) {
68
+ return bundledPath;
69
+ }
70
+ // Fall back to user photons
71
+ return resolvePhotonPath(name, workingDir);
72
+ }
73
+ // ══════════════════════════════════════════════════════════════════════════════
74
+ // PORT UTILITIES
75
+ // ══════════════════════════════════════════════════════════════════════════════
76
+ /**
77
+ * Check if a port is available
78
+ */
79
+ function isPortAvailable(port) {
80
+ return new Promise((resolve) => {
81
+ const server = net.createServer();
82
+ server.once('error', () => resolve(false));
83
+ server.once('listening', () => {
84
+ server.close();
85
+ resolve(true);
86
+ });
87
+ // Listen on all interfaces (same as http.createServer default)
88
+ server.listen(port);
89
+ });
90
+ }
91
+ function getLogOptionsFromCommand(command) {
92
+ const root = command?.parent?.opts?.() ?? program.opts();
93
+ try {
94
+ const level = normalizeLogLevel(root.logLevel);
95
+ return {
96
+ level,
97
+ json: Boolean(root.jsonLogs),
98
+ };
99
+ }
100
+ catch (error) {
101
+ handleError(error, { exitOnError: true });
102
+ throw error; // TypeScript doesn't know handleError exits
103
+ }
104
+ }
105
+ /**
106
+ * Find an available port starting from the given port
107
+ */
108
+ async function findAvailablePort(startPort, maxAttempts = 10) {
109
+ // Validate port range
110
+ validateOrThrow(startPort, [
111
+ inRange('start port', 1, 65535),
112
+ isInteger('start port'),
113
+ isPositive('start port'),
114
+ ]);
115
+ for (let i = 0; i < maxAttempts; i++) {
116
+ const port = startPort + i;
117
+ if (port > 65535) {
118
+ throw new Error(`Port ${port} exceeds maximum port number (65535)`);
119
+ }
120
+ if (await isPortAvailable(port)) {
121
+ return port;
122
+ }
123
+ }
124
+ throw new Error(`No available port found between ${startPort} and ${startPort + maxAttempts - 1}`);
125
+ }
126
+ function cliHeading(title) {
127
+ console.log('');
128
+ printHeader(title);
129
+ }
130
+ function cliListItem(text) {
131
+ printInfo(` ${text}`);
132
+ }
133
+ function cliSpacer() {
134
+ console.log('');
135
+ }
136
+ function cliHint(message) {
137
+ printWarning(message);
138
+ }
139
+ // ══════════════════════════════════════════════════════════════════════════════
140
+ // ELICITATION HANDLERS
141
+ // ══════════════════════════════════════════════════════════════════════════════
142
+ /**
143
+ * Handle form-based elicitation (MCP-aligned)
144
+ * Renders a multi-field form in CLI using readline
145
+ */
146
+ async function handleFormElicitation(ask) {
147
+ cliHeading(`📝 ${ask.message}`);
148
+ cliHint('Press Enter to accept defaults. Fields marked * are required.');
149
+ cliSpacer();
150
+ const rl = createReadline();
151
+ const question = (prompt) => {
152
+ return new Promise((resolve) => {
153
+ rl.question(prompt, (answer) => resolve(answer));
154
+ });
155
+ };
156
+ const result = {};
157
+ const required = ask.schema.required || [];
158
+ for (const [key, prop] of Object.entries(ask.schema.properties)) {
159
+ const title = prop.title || key;
160
+ const isRequired = required.includes(key);
161
+ const reqMark = isRequired ? '*' : '';
162
+ const defaultVal = prop.default !== undefined ? ` [${prop.default}]` : '';
163
+ let value;
164
+ // Handle different property types
165
+ if (prop.type === 'boolean') {
166
+ const answer = await question(`${title}${reqMark} (y/n)${defaultVal}: `);
167
+ if (answer === '' && prop.default !== undefined) {
168
+ value = prop.default;
169
+ }
170
+ else {
171
+ value = answer.toLowerCase().startsWith('y');
172
+ }
173
+ }
174
+ else if (prop.enum || prop.oneOf) {
175
+ // Single select
176
+ const options = prop.oneOf
177
+ ? prop.oneOf.map((o) => ({ value: o.const, label: o.title }))
178
+ : prop.enum.map((e) => ({ value: e, label: e }));
179
+ printInfo(`${title}${reqMark}:`);
180
+ options.forEach((opt, i) => {
181
+ const isDefault = opt.value === prop.default ? ' (default)' : '';
182
+ cliListItem(`${i + 1}. ${opt.label}${isDefault}`);
183
+ });
184
+ const answer = await question(`Choose (1-${options.length})${defaultVal}: `);
185
+ const idx = parseInt(answer) - 1;
186
+ if (idx >= 0 && idx < options.length) {
187
+ value = options[idx].value;
188
+ }
189
+ else if (answer === '' && prop.default !== undefined) {
190
+ value = prop.default;
191
+ }
192
+ else {
193
+ value = options[0].value;
194
+ }
195
+ }
196
+ else if (prop.type === 'array') {
197
+ // Multi-select
198
+ const items = prop.items?.anyOf || prop.items?.enum?.map((e) => ({ const: e, title: e }));
199
+ if (items) {
200
+ printInfo(`${title}${reqMark} (comma-separated numbers):`);
201
+ items.forEach((item, i) => {
202
+ const label = item.title || item.const || item;
203
+ cliListItem(`${i + 1}. ${label}`);
204
+ });
205
+ const answer = await question('Choose: ');
206
+ const indices = answer.split(',').map((s) => parseInt(s.trim()) - 1);
207
+ value = indices
208
+ .filter((idx) => idx >= 0 && idx < items.length)
209
+ .map((idx) => items[idx].const || items[idx]);
210
+ if (value.length === 0 && prop.default) {
211
+ value = prop.default;
212
+ }
213
+ }
214
+ else {
215
+ value = prop.default || [];
216
+ }
217
+ }
218
+ else if (prop.type === 'number' || prop.type === 'integer') {
219
+ const answer = await question(`${title}${reqMark}${defaultVal}: `);
220
+ if (answer === '' && prop.default !== undefined) {
221
+ value = prop.default;
222
+ }
223
+ else {
224
+ value = prop.type === 'integer' ? parseInt(answer) : parseFloat(answer);
225
+ if (isNaN(value))
226
+ value = prop.default ?? 0;
227
+ }
228
+ }
229
+ else {
230
+ // String or default
231
+ const format = prop.format ? ` (${prop.format})` : '';
232
+ const answer = await question(`${title}${reqMark}${format}${defaultVal}: `);
233
+ value = answer || prop.default || '';
234
+ }
235
+ result[key] = value;
236
+ cliSpacer();
237
+ }
238
+ rl.close();
239
+ cliSpacer();
240
+ return { action: 'accept', content: result };
241
+ }
242
+ /**
243
+ * Handle URL-based elicitation (OAuth flows)
244
+ * Opens URL in browser and waits for user confirmation
245
+ */
246
+ async function handleUrlElicitation(ask) {
247
+ cliHeading(`🔗 ${ask.message}`);
248
+ printInfo(`URL: ${ask.url}`);
249
+ cliHint('Opening your default browser...');
250
+ cliSpacer();
251
+ // Open URL in default browser
252
+ const platform = process.platform;
253
+ const openCommand = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open';
254
+ try {
255
+ const { exec } = await import('child_process');
256
+ exec(`${openCommand} "${ask.url}"`);
257
+ }
258
+ catch (error) {
259
+ cliHint('Please open the URL manually in your browser.');
260
+ }
261
+ const shouldContinue = await promptWait('Press Enter when done', true);
262
+ return { action: shouldContinue ? 'accept' : 'cancel' };
263
+ }
264
+ /**
265
+ * Handle select elicitation with options
266
+ */
267
+ async function handleSelectElicitation(ask) {
268
+ cliHeading(ask.message);
269
+ const options = ask.options.map((opt) => typeof opt === 'string' ? { value: opt, label: opt } : opt);
270
+ options.forEach((opt, i) => {
271
+ const isDefault = ask.default === opt.value || (Array.isArray(ask.default) && ask.default.includes(opt.value));
272
+ const defaultMark = isDefault ? ' ✓' : '';
273
+ const desc = opt.description ? ` - ${opt.description}` : '';
274
+ cliListItem(`${i + 1}. ${opt.label}${desc}${defaultMark}`);
275
+ });
276
+ cliSpacer();
277
+ const rl = createReadline();
278
+ const prompt = ask.multi
279
+ ? `Choose (comma-separated, 1-${options.length}): `
280
+ : `Choose (1-${options.length}): `;
281
+ return new Promise((resolve) => {
282
+ rl.question(prompt, (answer) => {
283
+ rl.close();
284
+ if (ask.multi) {
285
+ if (answer.trim() === '') {
286
+ resolve(Array.isArray(ask.default) ? ask.default : []);
287
+ }
288
+ else {
289
+ const indices = answer.split(',').map((s) => parseInt(s.trim()) - 1);
290
+ const values = indices
291
+ .filter((idx) => idx >= 0 && idx < options.length)
292
+ .map((idx) => options[idx].value);
293
+ resolve(values);
294
+ }
295
+ }
296
+ else {
297
+ const idx = parseInt(answer) - 1;
298
+ if (idx >= 0 && idx < options.length) {
299
+ resolve(options[idx].value);
300
+ }
301
+ else if (answer.trim() === '' && ask.default) {
302
+ resolve(ask.default);
303
+ }
304
+ else {
305
+ resolve(options[0].value);
306
+ }
307
+ }
308
+ });
309
+ });
310
+ }
21
311
  /**
22
312
  * Extract constructor parameters from a Photon MCP file
23
313
  */
@@ -28,21 +318,10 @@ async function extractConstructorParams(filePath) {
28
318
  return extractor.extractConstructorParams(source);
29
319
  }
30
320
  catch (error) {
31
- console.error(`Failed to extract constructor params: ${error.message}`);
321
+ printError(`Failed to extract constructor params: ${getErrorMessage(error)}`);
32
322
  return [];
33
323
  }
34
324
  }
35
- /**
36
- * Convert MCP name and parameter name to environment variable name
37
- */
38
- function toEnvVarName(mcpName, paramName) {
39
- const mcpPrefix = mcpName.toUpperCase().replace(/-/g, '_');
40
- const paramSuffix = paramName
41
- .replace(/([A-Z])/g, '_$1')
42
- .toUpperCase()
43
- .replace(/^_/, '');
44
- return `${mcpPrefix}_${paramSuffix}`;
45
- }
46
325
  /**
47
326
  * Ensure .gitignore includes marketplace template directory
48
327
  */
@@ -67,7 +346,7 @@ async function ensureGitignore(workingDir) {
67
346
  }
68
347
  catch (error) {
69
348
  // Non-fatal - just warn
70
- console.error(` ⚠ Could not update .gitignore: ${error.message}`);
349
+ console.error(` ⚠ Could not update .gitignore: ${getErrorMessage(error)}`);
71
350
  }
72
351
  }
73
352
  /**
@@ -77,21 +356,23 @@ async function performMarketplaceSync(dirPath, options) {
77
356
  const resolvedPath = path.resolve(dirPath);
78
357
  const isDefaultDir = resolvedPath === DEFAULT_WORKING_DIR;
79
358
  if (!existsSync(resolvedPath)) {
80
- console.error(`❌ Directory not found: ${resolvedPath}`);
81
- process.exit(1);
359
+ exitWithError(`Directory not found: ${resolvedPath}`, {
360
+ exitCode: ExitCode.NOT_FOUND,
361
+ suggestion: 'Check the path and ensure the directory exists',
362
+ });
82
363
  }
83
364
  // Scan for .photon.ts files
84
365
  console.error('📦 Scanning for .photon.ts files...');
85
366
  const files = await fs.readdir(resolvedPath);
86
- let photonFiles = files.filter(f => f.endsWith('.photon.ts'));
367
+ let photonFiles = files.filter((f) => f.endsWith('.photon.ts'));
87
368
  // Filter out installed photons if requested (for ~/.photon)
88
369
  if (options.filterInstalled && isDefaultDir) {
89
370
  const { readLocalMetadata } = await import('./marketplace-manager.js');
90
371
  const metadata = await readLocalMetadata();
91
372
  // Metadata keys may include .photon.ts extension
92
- const installedNames = new Set(Object.keys(metadata.photons || {}).map(k => k.replace(/\.photon\.ts$/, '')));
373
+ const installedNames = new Set(Object.keys(metadata.photons || {}).map((k) => k.replace(/\.photon\.ts$/, '')));
93
374
  const originalCount = photonFiles.length;
94
- photonFiles = photonFiles.filter(f => {
375
+ photonFiles = photonFiles.filter((f) => {
95
376
  const name = f.replace(/\.photon\.ts$/, '');
96
377
  return !installedNames.has(name);
97
378
  });
@@ -100,8 +381,11 @@ async function performMarketplaceSync(dirPath, options) {
100
381
  }
101
382
  }
102
383
  if (photonFiles.length === 0) {
103
- console.error(`❌ No .photon.ts files found in ${resolvedPath}`);
104
- process.exit(1);
384
+ exitWithError(`No .photon.ts files found`, {
385
+ exitCode: ExitCode.NOT_FOUND,
386
+ searchedIn: resolvedPath,
387
+ suggestion: "Create a .photon.ts file or use 'photon maker new' to generate one",
388
+ });
105
389
  }
106
390
  console.error(` Found ${photonFiles.length} photons\n`);
107
391
  // Initialize template manager
@@ -136,7 +420,10 @@ async function performMarketplaceSync(dirPath, options) {
136
420
  homepage: metadata.homepage,
137
421
  source: `../${file}`,
138
422
  hash,
139
- tools: metadata.tools?.map(t => t.name),
423
+ tools: metadata.tools?.map((t) => t.name),
424
+ assets: metadata.assets,
425
+ photonType: metadata.photonType,
426
+ features: metadata.features,
140
427
  });
141
428
  // Generate individual photon documentation
142
429
  const photonMarkdown = await templateMgr.renderTemplate('photon.md', metadata);
@@ -146,18 +433,31 @@ async function performMarketplaceSync(dirPath, options) {
146
433
  // Create manifest
147
434
  console.error('\n📋 Updating manifest...');
148
435
  const baseName = path.basename(resolvedPath);
436
+ const marketplaceDir = path.join(resolvedPath, '.marketplace');
437
+ await fs.mkdir(marketplaceDir, { recursive: true });
438
+ const manifestPath = path.join(marketplaceDir, 'photons.json');
439
+ // Read existing manifest to preserve owner if not explicitly provided
440
+ let existingOwner;
441
+ if (existsSync(manifestPath) && !options.owner) {
442
+ try {
443
+ const existingManifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
444
+ existingOwner = existingManifest.owner;
445
+ }
446
+ catch {
447
+ // Ignore parse errors
448
+ }
449
+ }
149
450
  const manifest = {
150
451
  name: options.name || baseName,
151
- version: '1.0.0',
452
+ version: PHOTON_VERSION,
152
453
  description: options.description || undefined,
153
- owner: options.owner ? {
154
- name: options.owner,
155
- } : undefined,
454
+ owner: options.owner
455
+ ? {
456
+ name: options.owner,
457
+ }
458
+ : existingOwner,
156
459
  photons,
157
460
  };
158
- const marketplaceDir = path.join(resolvedPath, '.marketplace');
159
- await fs.mkdir(marketplaceDir, { recursive: true });
160
- const manifestPath = path.join(marketplaceDir, 'photons.json');
161
461
  await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
162
462
  console.error(' ✓ .marketplace/photons.json');
163
463
  // Sync README with generated content
@@ -169,12 +469,14 @@ async function performMarketplaceSync(dirPath, options) {
169
469
  const readmeContent = await templateMgr.renderTemplate('readme.md', {
170
470
  marketplaceName: manifest.name,
171
471
  marketplaceDescription: manifest.description || '',
172
- photons: photons.map(p => ({
472
+ photons: photons.map((p) => ({
173
473
  name: p.name,
174
474
  description: p.description,
175
475
  version: p.version,
176
476
  license: p.license,
177
477
  tools: p.tools || [],
478
+ photonType: p.photonType || 'api',
479
+ features: p.features || [],
178
480
  })),
179
481
  });
180
482
  const isUpdate = await syncer.sync(readmeContent);
@@ -206,8 +508,11 @@ async function performMarketplaceInit(dirPath, options) {
206
508
  // Check if it's a git repository
207
509
  const gitDir = path.join(absolutePath, '.git');
208
510
  if (!existsSync(gitDir)) {
209
- console.error('⚠️ Not a git repository. Initialize with: git init');
210
- process.exit(1);
511
+ exitWithError('Not a git repository', {
512
+ exitCode: ExitCode.CONFIG_ERROR,
513
+ searchedIn: absolutePath,
514
+ suggestion: 'Initialize with: git init',
515
+ });
211
516
  }
212
517
  // Create .githooks directory
213
518
  const hooksDir = path.join(absolutePath, '.githooks');
@@ -315,59 +620,29 @@ function formatDefaultValue(value) {
315
620
  */
316
621
  function getConfigPath() {
317
622
  const platform = process.platform;
623
+ const home = os.homedir();
318
624
  if (platform === 'darwin') {
319
- return path.join(os.homedir(), 'Library/Application Support/Claude/claude_desktop_config.json');
625
+ return path.join(home, 'Library/Application Support/Claude/claude_desktop_config.json');
320
626
  }
321
627
  else if (platform === 'win32') {
322
- return path.join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json');
628
+ // On Windows, use APPDATA if available, otherwise fall back to home/AppData/Roaming
629
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
630
+ return path.join(appData, 'Claude', 'claude_desktop_config.json');
323
631
  }
324
632
  else {
325
633
  // Linux/other
326
- return path.join(os.homedir(), '.config/Claude/claude_desktop_config.json');
327
- }
328
- }
329
- /**
330
- * Check if photon is installed globally
331
- * @returns "photon" if available globally (cross-platform), null otherwise
332
- */
333
- async function getGlobalPhotonPath() {
334
- const { execFile } = await import('child_process');
335
- const { promisify } = await import('util');
336
- const execFileAsync = promisify(execFile);
337
- try {
338
- const command = process.platform === 'win32' ? 'where' : 'which';
339
- const { stdout } = await execFileAsync(command, ['photon']);
340
- const photonPath = stdout.trim().split('\n')[0]; // Take first match
341
- if (photonPath) {
342
- // Verify it's actually the @portel/photon package by checking version
343
- try {
344
- const { stdout: versionOutput } = await execFileAsync(photonPath, ['--version']);
345
- // If it outputs a version, it's likely our photon
346
- if (versionOutput.trim()) {
347
- // Return "photon" instead of full path for cross-platform compatibility
348
- // The shell will resolve it from PATH
349
- return 'photon';
350
- }
351
- }
352
- catch {
353
- // Version check failed, might not be our photon
354
- return null;
355
- }
356
- }
357
- }
358
- catch {
359
- // which/where command failed, photon not in PATH
634
+ return path.join(home, '.config/Claude/claude_desktop_config.json');
360
635
  }
361
- return null;
362
636
  }
363
637
  /**
364
638
  * Validate configuration for an MCP
365
639
  */
366
640
  async function validateConfiguration(filePath, mcpName) {
367
- console.log(`🔍 Validating configuration for: ${mcpName}\n`);
641
+ cliHeading(`🔍 Validating configuration for: ${mcpName}`);
642
+ cliSpacer();
368
643
  const params = await extractConstructorParams(filePath);
369
644
  if (params.length === 0) {
370
- console.log('No configuration required');
645
+ printSuccess('No configuration required for this MCP.');
371
646
  return;
372
647
  }
373
648
  let hasErrors = false;
@@ -381,15 +656,15 @@ async function validateConfiguration(filePath, mcpName) {
381
656
  results.push({
382
657
  name: param.name,
383
658
  envVar: envVarName,
384
- status: '❌ MISSING (required)',
659
+ status: '❌ Missing (required)',
385
660
  });
386
661
  }
387
662
  else if (envValue) {
388
663
  results.push({
389
664
  name: param.name,
390
665
  envVar: envVarName,
391
- status: '✅ SET',
392
- value: envValue.length > 20 ? envValue.substring(0, 17) + '...' : envValue,
666
+ status: '✅ Set',
667
+ value: envValue.length > 20 ? `${envValue.substring(0, 17)}...` : envValue,
393
668
  });
394
669
  }
395
670
  else {
@@ -401,55 +676,51 @@ async function validateConfiguration(filePath, mcpName) {
401
676
  });
402
677
  }
403
678
  }
404
- // Print results
405
- console.log('Configuration Status:\n');
406
- results.forEach(r => {
407
- console.log(` ${r.status} ${r.envVar}`);
679
+ printHeader('Configuration status');
680
+ results.forEach((r) => {
681
+ printInfo(` ${r.status} ${r.envVar}`);
408
682
  if (r.value) {
409
- console.log(` Value: ${r.value}`);
683
+ printInfo(` Value: ${r.value}`);
410
684
  }
411
- console.log();
412
685
  });
686
+ cliSpacer();
413
687
  if (hasErrors) {
414
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
415
- console.log('❌ Validation failed: Missing required environment variables');
416
- console.log('\nRun: photon mcp ' + mcpName + ' --config');
417
- console.log(' To see configuration template');
418
- process.exit(1);
688
+ exitWithError('Validation failed: Missing required environment variables', {
689
+ exitCode: ExitCode.CONFIG_ERROR,
690
+ suggestion: `Run 'photon mcp ${mcpName} --config' to see the configuration template`,
691
+ });
419
692
  }
420
693
  else {
421
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
422
- console.log('✅ Configuration valid!');
423
- console.log('\nYou can now run: photon mcp ' + mcpName);
694
+ printSuccess('Configuration valid!');
695
+ cliHint(`Run: photon mcp ${mcpName}`);
424
696
  }
425
697
  }
426
698
  /**
427
699
  * Show configuration template for an MCP
428
700
  */
429
701
  async function showConfigTemplate(filePath, mcpName, workingDir = DEFAULT_WORKING_DIR) {
430
- console.log(`📋 Configuration template for: ${mcpName}\n`);
702
+ cliHeading(`📋 Configuration template for: ${mcpName}`);
703
+ cliSpacer();
431
704
  const params = await extractConstructorParams(filePath);
432
705
  if (params.length === 0) {
433
- console.log('No configuration required for this MCP');
706
+ printSuccess('No configuration required for this MCP.');
434
707
  return;
435
708
  }
436
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
437
- console.log('Environment Variables:\n');
438
- params.forEach(param => {
709
+ printHeader('Environment variables');
710
+ params.forEach((param) => {
439
711
  const envVarName = toEnvVarName(mcpName, param.name);
440
712
  const isRequired = !param.isOptional && !param.hasDefault;
441
713
  const status = isRequired ? '[REQUIRED]' : '[OPTIONAL]';
442
- console.log(` ${envVarName} ${status}`);
443
- console.log(` Type: ${param.type}`);
714
+ printInfo(` ${envVarName} ${status}`);
715
+ printInfo(` Type: ${param.type}`);
444
716
  if (param.hasDefault) {
445
- console.log(` Default: ${formatDefaultValue(param.defaultValue)}`);
717
+ printInfo(` Default: ${formatDefaultValue(param.defaultValue)}`);
446
718
  }
447
- console.log();
719
+ cliSpacer();
448
720
  });
449
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
450
- console.log('Claude Desktop Configuration:\n');
721
+ printHeader('Claude Desktop configuration');
451
722
  const envExample = {};
452
- params.forEach(param => {
723
+ params.forEach((param) => {
453
724
  const envVarName = toEnvVarName(mcpName, param.name);
454
725
  if (!param.isOptional && !param.hasDefault) {
455
726
  envExample[envVarName] = `<your-${param.name}>`;
@@ -468,25 +739,19 @@ async function showConfigTemplate(filePath, mcpName, workingDir = DEFAULT_WORKIN
468
739
  },
469
740
  };
470
741
  console.log(JSON.stringify(config, null, 2));
471
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
472
- console.log(`\nAdd this to: ${getConfigPath()}`);
473
- console.log('\nValidate: photon mcp ' + mcpName + ' --validate');
474
- }
475
- // Get version from package.json
476
- let version = '1.0.0';
477
- try {
478
- const packageJson = require('../package.json');
479
- version = packageJson.version;
480
- }
481
- catch {
482
- // Fallback version
742
+ cliSpacer();
743
+ cliHint(`Add this to: ${getConfigPath()}`);
744
+ cliHint(`Validate with: photon mcp ${mcpName} --validate`);
483
745
  }
746
+ const version = PHOTON_VERSION;
484
747
  const program = new Command();
485
748
  program
486
749
  .name('photon')
487
750
  .description('Universal runtime for single-file TypeScript programs')
488
751
  .version(version)
489
752
  .option('--dir <path>', 'Photon directory (default: ~/.photon)', DEFAULT_WORKING_DIR)
753
+ .option('--log-level <level>', 'Set log verbosity (error|warn|info|debug)', 'info')
754
+ .option('--json-logs', 'Emit newline-delimited JSON logs for runtime output')
490
755
  .configureHelp({
491
756
  sortSubcommands: false,
492
757
  sortOptions: false,
@@ -495,6 +760,12 @@ program
495
760
  Runtime Commands:
496
761
  mcp <name> Run a photon as MCP server (for AI assistants)
497
762
  cli <photon> [method] Run photon methods from command line
763
+ sse <name> Run Photon as HTTP server with SSE transport
764
+ beam Launch Photon Beam (interactive control panel)
765
+ serve Start local multi-tenant MCP hosting for development
766
+
767
+ Hosting:
768
+ host <command> Manage cloud hosting (preview, deploy)
498
769
 
499
770
  Package Management:
500
771
  add <name> Install a photon from marketplace
@@ -525,49 +796,55 @@ program
525
796
  .description('Update marketplace indexes and check for CLI updates')
526
797
  .action(async () => {
527
798
  try {
528
- const { printInfo, printSuccess, printWarning } = await import('./cli-formatter.js');
529
- // Update all marketplace caches
530
- printInfo('Refreshing marketplace indexes...\n');
799
+ const { printInfo, printSuccess, printWarning, printHeader } = await import('./cli-formatter.js');
531
800
  const { MarketplaceManager } = await import('./marketplace-manager.js');
532
801
  const manager = new MarketplaceManager();
533
802
  await manager.initialize();
534
- const results = await manager.updateAllCaches();
535
- for (const [marketplaceName, success] of results) {
803
+ const results = await runTask('Refreshing marketplace indexes', async () => {
804
+ return manager.updateAllCaches();
805
+ });
806
+ console.log('');
807
+ const entries = Array.from(results.entries());
808
+ let successCount = 0;
809
+ for (const [marketplaceName, success] of entries) {
536
810
  if (success) {
537
- printSuccess(`${marketplaceName}`);
811
+ printSuccess(marketplaceName);
812
+ successCount++;
538
813
  }
539
814
  else {
540
815
  printWarning(`${marketplaceName} (no manifest)`);
541
816
  }
542
817
  }
543
- const successCount = Array.from(results.values()).filter(Boolean).length;
544
- console.log('');
545
- printInfo(`Updated ${successCount}/${results.size} marketplaces`);
546
- // Check for CLI updates
547
- console.log('');
548
- printInfo('Checking for Photon CLI updates...');
818
+ printInfo(`\nUpdated ${successCount}/${entries.length} marketplaces`);
819
+ let latestVersion = null;
549
820
  try {
550
- const { execSync } = await import('child_process');
551
- const latestVersion = execSync('npm view @portel/photon version', {
552
- encoding: 'utf-8',
553
- timeout: 10000,
554
- }).trim();
555
- if (latestVersion && latestVersion !== version) {
556
- console.log('');
557
- printWarning(`New Photon version available: ${version} → ${latestVersion}`);
821
+ latestVersion = await runTask('Checking for Photon CLI updates', async () => {
822
+ const { execSync } = await import('child_process');
823
+ return execSync('npm view @portel/photon version', {
824
+ encoding: 'utf-8',
825
+ timeout: 10000,
826
+ }).trim();
827
+ });
828
+ }
829
+ catch {
830
+ printWarning('\nCould not check for CLI updates');
831
+ }
832
+ if (latestVersion) {
833
+ console.log('');
834
+ if (latestVersion !== version) {
835
+ printHeader('Update available');
836
+ printWarning(`Current: ${version}`);
837
+ printInfo(`Latest: ${latestVersion}`);
558
838
  printInfo(`Update with: npm install -g @portel/photon`);
559
839
  }
560
840
  else {
561
841
  printSuccess(`Photon CLI is up to date (${version})`);
562
842
  }
563
843
  }
564
- catch {
565
- printWarning('Could not check for CLI updates');
566
- }
567
844
  }
568
845
  catch (error) {
569
846
  const { printError } = await import('./cli-formatter.js');
570
- printError(error.message);
847
+ printError(getErrorMessage(error));
571
848
  process.exit(1);
572
849
  }
573
850
  });
@@ -579,32 +856,127 @@ program
579
856
  .option('--dev', 'Enable development mode with hot reload')
580
857
  .option('--validate', 'Validate configuration without running server')
581
858
  .option('--config', 'Show configuration template and exit')
582
- .action(async (name, options, command) => {
859
+ .option('--transport <type>', 'Transport type: stdio (default) or sse', 'stdio')
860
+ .option('--port <number>', 'Port for SSE transport (default: 3000)', '3000')
861
+ .action(async (rawName, options, command) => {
583
862
  try {
863
+ // Parse extended name format (e.g., "alice/repo:rss-feed")
864
+ const { name, marketplaceSource } = parsePhotonSpec(rawName);
584
865
  // Get working directory from global options
585
866
  const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
586
- // Resolve file path from name in working directory
587
- const filePath = await resolvePhotonPath(name, workingDir);
867
+ const logOptions = getLogOptionsFromCommand(command);
868
+ // Resolve file path - check bundled photons first, then user directory
869
+ let filePath = await resolvePhotonPathWithBundled(name, workingDir);
870
+ // Auto-install from marketplace if not found locally
871
+ let unresolvedPhoton;
588
872
  if (!filePath) {
589
- console.error(`❌ MCP not found: ${name}`);
590
- console.error(`Searched in: ${workingDir}`);
591
- console.error(`Tip: Use 'photon info' to see available MCPs`);
592
- process.exit(1);
873
+ const { MarketplaceManager, calculateHash } = await import('./marketplace-manager.js');
874
+ const manager = new MarketplaceManager();
875
+ await manager.initialize();
876
+ // If marketplace source given, add it (persistent, idempotent)
877
+ if (marketplaceSource) {
878
+ const { marketplace: addedMp, added } = await manager.add(marketplaceSource);
879
+ if (added) {
880
+ console.error(`Added marketplace: ${addedMp.name}`);
881
+ await manager.updateMarketplaceCache(addedMp.name);
882
+ }
883
+ }
884
+ // Check for conflicts (multiple sources)
885
+ const conflict = await manager.checkConflict(name);
886
+ if (conflict.sources.length === 0) {
887
+ // Not found anywhere
888
+ exitWithError(`MCP not found: ${name}`, {
889
+ exitCode: ExitCode.NOT_FOUND,
890
+ searchedIn: workingDir,
891
+ suggestion: DEFAULT_BUNDLED_PHOTONS.includes(name)
892
+ ? `'${name}' is a bundled photon but could not be found`
893
+ : marketplaceSource
894
+ ? `Photon '${name}' not found in ${marketplaceSource}`
895
+ : "Use 'photon search <name>' to find it or 'photon marketplace add <source>' to add a marketplace",
896
+ });
897
+ }
898
+ else if (conflict.sources.length === 1 || !conflict.hasConflict) {
899
+ // Single source — auto-download
900
+ const source = conflict.sources[0];
901
+ console.error(`Installing ${name} from ${source.marketplace.name}...`);
902
+ const result = await manager.fetchMCP(name);
903
+ if (!result) {
904
+ exitWithError(`Failed to download: ${name}`, {
905
+ exitCode: ExitCode.ERROR,
906
+ suggestion: 'Check your internet connection and marketplace configuration',
907
+ });
908
+ }
909
+ // Ensure working directory exists and save
910
+ await ensureWorkingDir(workingDir);
911
+ const targetPath = path.join(workingDir, `${name}.photon.ts`);
912
+ await fs.writeFile(targetPath, result.content, 'utf-8');
913
+ // Save metadata
914
+ if (source.metadata) {
915
+ const contentHash = calculateHash(result.content);
916
+ await manager.savePhotonMetadata(`${name}.photon.ts`, source.marketplace, source.metadata, contentHash);
917
+ // Download assets
918
+ if (source.metadata.assets && source.metadata.assets.length > 0) {
919
+ const assets = await manager.fetchAssets(source.marketplace, source.metadata.assets);
920
+ for (const [assetPath, content] of assets) {
921
+ const assetTarget = path.join(workingDir, assetPath);
922
+ const assetDir = path.dirname(assetTarget);
923
+ await fs.mkdir(assetDir, { recursive: true });
924
+ await fs.writeFile(assetTarget, content, 'utf-8');
925
+ }
926
+ }
927
+ }
928
+ console.error(`Installed ${name}`);
929
+ filePath = targetPath;
930
+ }
931
+ else {
932
+ // Multiple sources — defer to server for elicitation
933
+ unresolvedPhoton = {
934
+ name,
935
+ workingDir,
936
+ sources: conflict.sources,
937
+ recommendation: conflict.recommendation,
938
+ };
939
+ }
593
940
  }
594
- // Handle --validate flag
941
+ // Handle --validate flag (requires resolved filePath)
595
942
  if (options.validate) {
943
+ if (!filePath) {
944
+ exitWithError(`Cannot validate: ${name} has multiple sources. Install it first with 'photon add ${name}'.`, {
945
+ exitCode: ExitCode.CONFIG_ERROR,
946
+ });
947
+ }
596
948
  await validateConfiguration(filePath, name);
597
949
  return;
598
950
  }
599
951
  // Handle --config flag
600
952
  if (options.config) {
953
+ if (!filePath) {
954
+ exitWithError(`Cannot show config: ${name} has multiple sources. Install it first with 'photon add ${name}'.`, {
955
+ exitCode: ExitCode.CONFIG_ERROR,
956
+ });
957
+ }
601
958
  await showConfigTemplate(filePath, name, workingDir);
602
959
  return;
603
960
  }
961
+ // Validate transport option
962
+ const transport = options.transport;
963
+ if (transport !== 'stdio' && transport !== 'sse') {
964
+ exitWithError(`Invalid transport: ${options.transport}`, {
965
+ exitCode: ExitCode.INVALID_ARGUMENT,
966
+ suggestion: 'Valid options: stdio, sse',
967
+ });
968
+ }
969
+ // Set PHOTON_NAME for daemon broker pub/sub to work
970
+ // This ensures channel messages go to the correct daemon socket
971
+ process.env.PHOTON_NAME = name;
604
972
  // Start MCP server
605
973
  const server = new PhotonServer({
606
- filePath,
974
+ filePath: filePath || '', // empty when unresolved — server handles it
607
975
  devMode: options.dev,
976
+ transport,
977
+ port: parseInt(options.port, 10),
978
+ logOptions: { ...logOptions, scope: transport },
979
+ unresolvedPhoton,
608
980
  });
609
981
  // Handle shutdown signals
610
982
  const shutdown = async () => {
@@ -616,9 +988,9 @@ program
616
988
  process.on('SIGTERM', shutdown);
617
989
  // Start the server
618
990
  await server.start();
619
- // Start file watcher in dev mode
620
- if (options.dev) {
621
- const watcher = new FileWatcher(server, filePath);
991
+ // Start file watcher in dev mode (only if resolved)
992
+ if (options.dev && filePath) {
993
+ const watcher = new FileWatcher(server, filePath, server.createScopedLogger('watcher'));
622
994
  watcher.start();
623
995
  // Clean up watcher on shutdown
624
996
  process.on('SIGINT', async () => {
@@ -630,290 +1002,297 @@ program
630
1002
  }
631
1003
  }
632
1004
  catch (error) {
633
- console.error(`❌ Error: ${error.message}`);
1005
+ logger.error(`Error: ${getErrorMessage(error)}`);
634
1006
  process.exit(1);
635
1007
  }
636
1008
  });
637
- // Info command: show installed and available Photons
1009
+ // SSE command: quick SSE server with auto port detection (formerly serve)
638
1010
  program
639
- .command('info', { hidden: true })
640
- .argument('[name]', 'Photon name to show details for (shows all if omitted)')
641
- .option('--mcp', 'Output as MCP server configuration')
642
- .alias('list')
643
- .alias('ls')
644
- .description('Show installed and available Photons')
1011
+ .command('sse', { hidden: true })
1012
+ .argument('<name>', 'Photon name (without .photon.ts extension)')
1013
+ .option('-p, --port <number>', 'Port to start from (auto-finds available)', '3000')
1014
+ .option('--dev', 'Enable development mode with hot reload')
1015
+ .description('Run Photon as HTTP server with SSE transport (auto port detection)')
645
1016
  .action(async (name, options, command) => {
646
1017
  try {
647
- const { formatOutput, printInfo, printError, STATUS } = await import('./cli-formatter.js');
648
- // Get working directory from global/parent options
649
- const parentOpts = command.parent?.opts() || {};
650
- const workingDir = parentOpts.dir || DEFAULT_WORKING_DIR;
651
- const asMcp = options.mcp || false;
652
- const mcps = await listPhotonMCPs(workingDir);
653
- // Initialize marketplace manager for all operations
654
- const { MarketplaceManager } = await import('./marketplace-manager.js');
655
- const manager = new MarketplaceManager();
656
- await manager.initialize();
657
- // Show single Photon details
658
- if (name) {
659
- const filePath = await resolvePhotonPath(name, workingDir);
660
- const isInstalled = !!filePath;
661
- if (asMcp) {
662
- // MCP config only works for installed photons
663
- if (!isInstalled) {
664
- printError(`'${name}' is not installed`);
665
- printInfo(`Install with: photon add ${name}`);
666
- process.exit(1);
667
- }
668
- // Show as MCP config for single Photon
669
- const constructorParams = await extractConstructorParams(filePath);
670
- const env = {};
671
- for (const param of constructorParams) {
672
- const envVarName = toEnvVarName(name, param.name);
673
- const defaultDisplay = param.defaultValue !== undefined
674
- ? formatDefaultValue(param.defaultValue)
675
- : `<your-${param.name}>`;
676
- env[envVarName] = defaultDisplay;
677
- }
678
- // Check for global photon installation
679
- const globalPhotonPath = await getGlobalPhotonPath();
680
- const needsWorkingDir = workingDir !== DEFAULT_WORKING_DIR;
681
- const config = globalPhotonPath
682
- ? {
683
- command: globalPhotonPath,
684
- args: needsWorkingDir
685
- ? ['mcp', name, '--dir', workingDir]
686
- : ['mcp', name],
687
- ...(Object.keys(env).length > 0 && { env }),
688
- }
689
- : {
690
- command: 'npx',
691
- args: needsWorkingDir
692
- ? ['@portel/photon', 'mcp', name, '--dir', workingDir]
693
- : ['@portel/photon', 'mcp', name],
694
- ...(Object.keys(env).length > 0 && { env }),
695
- };
696
- // Get OS-specific config path
697
- const configPath = getConfigPath();
698
- console.log(`# Photon MCP Server Configuration: ${name}`);
699
- console.log(`# Add to mcpServers in: ${configPath}\n`);
700
- console.log(JSON.stringify({ [name]: config }, null, 2));
701
- }
702
- else {
703
- // Show info for specific photon - both local and marketplace
704
- // Show local installation if present
705
- if (isInstalled) {
706
- const { PhotonDocExtractor } = await import('./photon-doc-extractor.js');
707
- const extractor = new PhotonDocExtractor(filePath);
708
- const photonMetadata = await extractor.extractFullMetadata();
709
- const fileName = `${name}.photon.ts`;
710
- const metadata = await manager.getPhotonInstallMetadata(fileName);
711
- const isModified = metadata ? await manager.isPhotonModified(filePath, fileName) : false;
712
- printInfo(`Installed in ${workingDir}:\n`);
713
- // Build info as tree structure
714
- const infoData = {
715
- name: name,
716
- version: photonMetadata.version || '-',
717
- location: filePath,
718
- };
719
- if (photonMetadata.description) {
720
- infoData.description = photonMetadata.description;
721
- }
722
- if (metadata) {
723
- infoData.installed = new Date(metadata.installedAt).toLocaleDateString();
724
- infoData.source = metadata.marketplace;
725
- if (isModified) {
726
- infoData.status = 'modified locally';
727
- }
728
- }
729
- const toolCount = photonMetadata.tools?.length || 0;
730
- if (toolCount > 0) {
731
- infoData.tools = toolCount;
732
- }
733
- formatOutput(infoData, 'tree');
734
- console.log('');
735
- // Show appropriate run command
736
- if (metadata && !isModified) {
737
- printInfo(`Run with: photon mcp ${name}`);
738
- printInfo(`To customize: Copy to a new name and run with --dev for hot reload`);
739
- }
740
- else if (metadata && isModified) {
741
- printInfo(`Run with: photon mcp ${name} --dev`);
742
- printInfo(`Note: Modified from marketplace - consider renaming to avoid upgrade conflicts`);
743
- }
744
- else {
745
- printInfo(`Run with: photon mcp ${name} --dev`);
746
- }
747
- console.log('');
748
- }
749
- // Show marketplace availability in tree format
750
- const searchResults = await manager.search(name);
751
- if (searchResults.size > 0) {
752
- printInfo('Available in marketplaces:\n');
753
- // Get all sources for this specific photon
754
- const sources = searchResults.get(name);
755
- if (sources && sources.length > 0) {
756
- // Get local installation info to mark which one is installed
757
- const fileName = `${name}.photon.ts`;
758
- const installMetadata = await manager.getPhotonInstallMetadata(fileName);
759
- // Build marketplace data as tree
760
- const marketplaceData = {};
761
- for (const source of sources) {
762
- const isCurrentlyInstalled = installMetadata?.marketplace === source.marketplace.name;
763
- const version = source.metadata?.version || 'unknown';
764
- marketplaceData[source.marketplace.name] = {
765
- version,
766
- source: source.marketplace.repo,
767
- status: isCurrentlyInstalled ? 'installed' : '-',
768
- };
769
- }
770
- formatOutput(marketplaceData, 'tree');
771
- }
772
- }
773
- else if (!isInstalled) {
774
- printError(`'${name}' not found locally or in any marketplace`);
775
- printInfo(`Tip: Use 'photon search <query>' to find similar MCPs`);
776
- process.exit(1);
777
- }
778
- }
779
- return;
780
- }
781
- // Show all Photons
782
- if (asMcp) {
783
- // MCP config mode for all Photons
784
- const allConfigs = {};
785
- // Check for global photon installation once
786
- const globalPhotonPath = await getGlobalPhotonPath();
787
- const needsWorkingDir = workingDir !== DEFAULT_WORKING_DIR;
788
- for (const mcpName of mcps) {
789
- const filePath = await resolvePhotonPath(mcpName, workingDir);
790
- if (!filePath)
791
- continue;
792
- const constructorParams = await extractConstructorParams(filePath);
793
- const env = {};
794
- for (const param of constructorParams) {
795
- const envVarName = toEnvVarName(mcpName, param.name);
796
- const defaultDisplay = param.defaultValue !== undefined
797
- ? formatDefaultValue(param.defaultValue)
798
- : `<your-${param.name}>`;
799
- env[envVarName] = defaultDisplay;
800
- }
801
- allConfigs[mcpName] = globalPhotonPath
802
- ? {
803
- command: globalPhotonPath,
804
- args: needsWorkingDir
805
- ? ['mcp', mcpName, '--dir', workingDir]
806
- : ['mcp', mcpName],
807
- ...(Object.keys(env).length > 0 && { env }),
808
- }
809
- : {
810
- command: 'npx',
811
- args: needsWorkingDir
812
- ? ['@portel/photon', 'mcp', mcpName, '--dir', workingDir]
813
- : ['@portel/photon', 'mcp', mcpName],
814
- ...(Object.keys(env).length > 0 && { env }),
815
- };
816
- }
817
- // Get OS-specific config path
818
- const configPath = getConfigPath();
819
- console.log(`# Photon MCP Server Configuration (${mcps.length} servers)`);
820
- console.log(`# Add to mcpServers in: ${configPath}\n`);
821
- console.log(JSON.stringify({ mcpServers: allConfigs }, null, 2));
822
- return;
1018
+ // Get working directory from global options
1019
+ const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1020
+ const logOptions = getLogOptionsFromCommand(command);
1021
+ // Resolve file path from name
1022
+ const filePath = await resolvePhotonPath(name, workingDir);
1023
+ if (!filePath) {
1024
+ exitWithError(`Photon not found: ${name}`, {
1025
+ exitCode: ExitCode.NOT_FOUND,
1026
+ searchedIn: workingDir,
1027
+ suggestion: "Use 'photon info' to see available photons",
1028
+ });
823
1029
  }
824
- // Normal list mode - show both installed and available
825
- // Show installed photons as table
826
- if (mcps.length > 0) {
827
- printInfo(`Installed in ${workingDir} (${mcps.length}):\n`);
828
- const tableData = [];
829
- for (const mcpName of mcps) {
830
- const fileName = `${mcpName}.photon.ts`;
831
- const filePath = path.join(workingDir, fileName);
832
- // Get installation metadata
833
- const metadata = await manager.getPhotonInstallMetadata(fileName);
834
- if (metadata) {
835
- // Has metadata - show version and status
836
- const isModified = await manager.isPhotonModified(filePath, fileName);
837
- tableData.push({
838
- name: mcpName,
839
- version: metadata.version,
840
- source: metadata.marketplace,
841
- status: isModified ? 'modified' : STATUS.OK,
842
- });
843
- }
844
- else {
845
- // No metadata - local or pre-metadata Photon
846
- tableData.push({
847
- name: mcpName,
848
- version: '-',
849
- source: 'local',
850
- status: STATUS.OK,
851
- });
852
- }
853
- }
854
- formatOutput(tableData, 'table');
855
- console.log('');
1030
+ // Find available port
1031
+ const startPort = parseInt(options.port, 10);
1032
+ const port = await findAvailablePort(startPort);
1033
+ if (port !== startPort) {
1034
+ console.error(`⚠️ Port ${startPort} is in use, using ${port} instead\n`);
856
1035
  }
857
- else {
858
- printInfo(`No photons installed in ${workingDir}`);
859
- printInfo(`Install with: photon add <name>\n`);
860
- }
861
- // Show marketplace availability in tree format
862
- printInfo('Available in marketplaces:\n');
863
- const marketplaces = manager.getAll().filter(m => m.enabled);
864
- const counts = await manager.getMarketplaceCounts();
865
- // Build marketplace tree
866
- const marketplaceTree = {};
867
- for (const marketplace of marketplaces) {
868
- const count = counts.get(marketplace.name) || 0;
869
- if (count > 0) {
870
- // Get a few sample photons from this marketplace
871
- const manifest = await manager['getCachedManifest'](marketplace.name);
872
- if (manifest && manifest.photons) {
873
- const samples = manifest.photons.slice(0, 3);
874
- const photonList = {};
875
- samples.forEach((photon) => {
876
- const installedMark = mcps.includes(photon.name) ? ' (installed)' : '';
877
- photonList[photon.name] = `v${photon.version}${installedMark}`;
878
- });
879
- if (count > 3) {
880
- photonList['...'] = `${count - 3} more`;
881
- }
882
- marketplaceTree[`${marketplace.name} (${marketplace.repo})`] = photonList;
883
- }
884
- }
885
- else {
886
- marketplaceTree[marketplace.name] = 'no manifest';
887
- }
1036
+ // Start SSE server
1037
+ const server = new PhotonServer({
1038
+ filePath,
1039
+ devMode: options.dev,
1040
+ transport: 'sse',
1041
+ port,
1042
+ logOptions: { ...logOptions, scope: 'sse' },
1043
+ });
1044
+ // Handle shutdown signals
1045
+ const shutdown = async () => {
1046
+ console.error('\nShutting down...');
1047
+ await server.stop();
1048
+ process.exit(0);
1049
+ };
1050
+ process.on('SIGINT', shutdown);
1051
+ process.on('SIGTERM', shutdown);
1052
+ // Start the server
1053
+ await server.start();
1054
+ // Start file watcher in dev mode
1055
+ if (options.dev) {
1056
+ const watcher = new FileWatcher(server, filePath, server.createScopedLogger('watcher'));
1057
+ watcher.start();
1058
+ process.on('SIGINT', async () => {
1059
+ await watcher.stop();
1060
+ });
1061
+ process.on('SIGTERM', async () => {
1062
+ await watcher.stop();
1063
+ });
888
1064
  }
889
- formatOutput(marketplaceTree, 'tree');
890
- console.log('');
891
- printInfo(`Details: photon info <name>`);
892
- printInfo(`MCP config: photon info <name> --mcp`);
893
1065
  }
894
1066
  catch (error) {
895
- const { printError } = await import('./cli-formatter.js');
896
- printError(error.message);
1067
+ logger.error(`Error: ${getErrorMessage(error)}`);
897
1068
  process.exit(1);
898
1069
  }
899
1070
  });
900
- // Search command: search for MCPs across marketplaces
1071
+ // Beam command: interactive UI for all photons
901
1072
  program
902
- .command('search', { hidden: true })
903
- .argument('<query>', 'MCP name or keyword to search for')
904
- .description('Search for MCP in all enabled marketplaces')
905
- .action(async (query) => {
1073
+ .command('beam', { hidden: true })
1074
+ .option('-p, --port <number>', 'Port to start from (auto-finds available)', '3000')
1075
+ .option('-o, --open', 'Auto-open browser after starting')
1076
+ .option('--no-open', 'Do not auto-open browser')
1077
+ .description('Launch Photon Beam - interactive control panel for all your photons')
1078
+ .action(async (options, command) => {
906
1079
  try {
907
- const { MarketplaceManager } = await import('./marketplace-manager.js');
908
- const { formatOutput, printInfo, printError } = await import('./cli-formatter.js');
909
- const manager = new MarketplaceManager();
910
- await manager.initialize();
911
- // Auto-update stale caches
912
- const updated = await manager.autoUpdateStaleCaches();
913
- if (updated) {
914
- printInfo('Refreshed marketplace data...\n');
1080
+ // Get working directory from global options
1081
+ const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1082
+ // Find available port
1083
+ const startPort = parseInt(options.port, 10);
1084
+ const port = await findAvailablePort(startPort);
1085
+ if (port !== startPort) {
1086
+ console.error(`⚠️ Port ${startPort} is in use, using ${port} instead\n`);
1087
+ }
1088
+ // Import and start Beam server
1089
+ const { startBeam } = await import('./auto-ui/beam.js');
1090
+ await startBeam(workingDir, port);
1091
+ // Auto-open browser if requested
1092
+ if (options.open) {
1093
+ const url = `http://localhost:${port}`;
1094
+ const { exec } = await import('child_process');
1095
+ const openCmd = process.platform === 'darwin'
1096
+ ? 'open'
1097
+ : process.platform === 'win32'
1098
+ ? 'start'
1099
+ : 'xdg-open';
1100
+ exec(`${openCmd} ${url}`, (err) => {
1101
+ if (err)
1102
+ logger.debug(`Could not auto-open browser: ${err.message}`);
1103
+ });
915
1104
  }
916
- printInfo(`Searching for '${query}' in marketplaces...`);
1105
+ // Handle shutdown signals
1106
+ const shutdown = () => {
1107
+ console.error('\nShutting down Photon Beam...');
1108
+ process.exit(0);
1109
+ };
1110
+ process.on('SIGINT', shutdown);
1111
+ process.on('SIGTERM', shutdown);
1112
+ }
1113
+ catch (error) {
1114
+ logger.error(`Error: ${getErrorMessage(error)}`);
1115
+ process.exit(1);
1116
+ }
1117
+ });
1118
+ // Serve command: multi-tenant MCP hosting (formerly serv)
1119
+ program
1120
+ .command('serve', { hidden: true })
1121
+ .option('-p, --port <number>', 'Port to run on', '4000')
1122
+ .option('-d, --debug', 'Enable debug logging')
1123
+ .description('Start local multi-tenant MCP hosting for development')
1124
+ .action(async (options) => {
1125
+ try {
1126
+ const port = parseInt(options.port, 10);
1127
+ const availablePort = await findAvailablePort(port);
1128
+ if (availablePort !== port) {
1129
+ console.error(`⚠️ Port ${port} is in use, using ${availablePort} instead\n`);
1130
+ }
1131
+ // Import and start LocalServ
1132
+ const { createLocalServ, getTestToken } = await import('./serv/local.js');
1133
+ const { serv, tenant, user } = createLocalServ({
1134
+ port: availablePort,
1135
+ baseUrl: `http://localhost:${availablePort}`,
1136
+ debug: options.debug,
1137
+ });
1138
+ // Get a test token
1139
+ const token = await getTestToken(serv, tenant, user);
1140
+ console.error(`
1141
+ ⚡ Photon Serve (Multi-tenant Development)
1142
+
1143
+ URL: http://localhost:${availablePort}
1144
+ Tenant: ${tenant.slug} (${tenant.name})
1145
+ User: ${user.email}
1146
+
1147
+ Test Token:
1148
+ ${token}
1149
+
1150
+ MCP Endpoint:
1151
+ http://localhost:${availablePort}/tenant/${tenant.slug}/mcp
1152
+
1153
+ Well-Known:
1154
+ http://localhost:${availablePort}/.well-known/oauth-protected-resource
1155
+
1156
+ Press Ctrl+C to stop
1157
+ `);
1158
+ // Simple HTTP server
1159
+ const http = await import('http');
1160
+ const server = http.createServer(async (req, res) => {
1161
+ const url = req.url || '/';
1162
+ const method = req.method || 'GET';
1163
+ const headers = {};
1164
+ for (const [key, value] of Object.entries(req.headers)) {
1165
+ if (typeof value === 'string')
1166
+ headers[key] = value;
1167
+ }
1168
+ // Read body if present
1169
+ let body = '';
1170
+ if (method === 'POST') {
1171
+ body = await new Promise((resolve) => {
1172
+ let data = '';
1173
+ req.on('data', (chunk) => (data += chunk));
1174
+ req.on('end', () => resolve(data));
1175
+ });
1176
+ }
1177
+ const result = await serv.handleRequest(method, url, headers, body);
1178
+ res.writeHead(result.status, result.headers);
1179
+ res.end(result.body);
1180
+ });
1181
+ server.listen(availablePort);
1182
+ // Handle shutdown
1183
+ const shutdown = async () => {
1184
+ console.error('\nShutting down Photon Serve...');
1185
+ await serv.shutdown();
1186
+ server.close();
1187
+ process.exit(0);
1188
+ };
1189
+ process.on('SIGINT', shutdown);
1190
+ process.on('SIGTERM', shutdown);
1191
+ }
1192
+ catch (error) {
1193
+ logger.error(`Error: ${getErrorMessage(error)}`);
1194
+ process.exit(1);
1195
+ }
1196
+ });
1197
+ // Host command: manage hosting and deployment (preview, deploy)
1198
+ const host = program
1199
+ .command('host', { hidden: true })
1200
+ .description('Manage cloud hosting and deployment');
1201
+ host
1202
+ .command('preview')
1203
+ .argument('<target>', 'Deployment target: cloudflare (or cf)')
1204
+ .argument('<name>', 'Photon name (without .photon.ts extension)')
1205
+ .option('--output <dir>', 'Output directory for generated project')
1206
+ .description('Run Photon locally in a simulated deployment environment')
1207
+ .action(async (target, name, options) => {
1208
+ try {
1209
+ // Get working directory from global options
1210
+ const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1211
+ // Resolve file path from name
1212
+ const photonPath = await resolvePhotonPath(name, workingDir);
1213
+ if (!photonPath) {
1214
+ logger.error(`Photon not found: ${name}`);
1215
+ console.error(`Searched in: ${workingDir}`);
1216
+ console.error(`Tip: Use 'photon info' to see available photons`);
1217
+ process.exit(1);
1218
+ }
1219
+ const normalizedTarget = target.toLowerCase();
1220
+ if (normalizedTarget === 'cloudflare' || normalizedTarget === 'cf') {
1221
+ const { devCloudflare } = await import('./deploy/cloudflare.js');
1222
+ await devCloudflare({
1223
+ photonPath,
1224
+ outputDir: options.output,
1225
+ });
1226
+ }
1227
+ else {
1228
+ logger.error(`Unknown target: ${target}`);
1229
+ console.error('Supported targets: cloudflare (cf)');
1230
+ process.exit(1);
1231
+ }
1232
+ }
1233
+ catch (error) {
1234
+ logger.error(`Error: ${getErrorMessage(error)}`);
1235
+ process.exit(1);
1236
+ }
1237
+ });
1238
+ host
1239
+ .command('deploy')
1240
+ .argument('<target>', 'Deployment target: cloudflare (or cf)')
1241
+ .argument('<name>', 'Photon name (without .photon.ts extension)')
1242
+ .option('--dev', 'Enable Beam UI in deployment')
1243
+ .option('--dry-run', 'Generate project without deploying')
1244
+ .option('--output <dir>', 'Output directory for generated project')
1245
+ .description('Deploy a Photon to cloud platforms')
1246
+ .action(async (target, name, options) => {
1247
+ try {
1248
+ // Get working directory from global options
1249
+ const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1250
+ // Resolve file path from name
1251
+ const photonPath = await resolvePhotonPath(name, workingDir);
1252
+ if (!photonPath) {
1253
+ logger.error(`Photon not found: ${name}`);
1254
+ console.error(`Searched in: ${workingDir}`);
1255
+ console.error(`Tip: Use 'photon info' to see available photons`);
1256
+ process.exit(1);
1257
+ }
1258
+ const normalizedTarget = target.toLowerCase();
1259
+ if (normalizedTarget === 'cloudflare' || normalizedTarget === 'cf') {
1260
+ const { deployToCloudflare } = await import('./deploy/cloudflare.js');
1261
+ await deployToCloudflare({
1262
+ photonPath,
1263
+ devMode: options.dev,
1264
+ dryRun: options.dryRun,
1265
+ outputDir: options.output,
1266
+ });
1267
+ }
1268
+ else {
1269
+ logger.error(`Unknown deployment target: ${target}`);
1270
+ console.error('Supported targets: cloudflare (cf)');
1271
+ process.exit(1);
1272
+ }
1273
+ }
1274
+ catch (error) {
1275
+ logger.error(`Deployment failed: ${getErrorMessage(error)}`);
1276
+ process.exit(1);
1277
+ }
1278
+ });
1279
+ // Search command: search for MCPs across marketplaces
1280
+ program
1281
+ .command('search', { hidden: true })
1282
+ .argument('<query>', 'MCP name or keyword to search for')
1283
+ .description('Search for MCP in all enabled marketplaces')
1284
+ .action(async (query) => {
1285
+ try {
1286
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
1287
+ const { formatOutput, printInfo, printError } = await import('./cli-formatter.js');
1288
+ const manager = new MarketplaceManager();
1289
+ await manager.initialize();
1290
+ // Auto-update stale caches
1291
+ const updated = await manager.autoUpdateStaleCaches();
1292
+ if (updated) {
1293
+ printInfo('Refreshed marketplace data...\n');
1294
+ }
1295
+ printInfo(`Searching for '${query}' in marketplaces...`);
917
1296
  const results = await manager.search(query);
918
1297
  if (results.size === 0) {
919
1298
  printError(`No results found for '${query}'`);
@@ -926,9 +1305,10 @@ program
926
1305
  for (const entry of entries) {
927
1306
  tableData.push({
928
1307
  name: mcpName,
929
- version: entry.metadata?.version || '-',
1308
+ version: entry.metadata?.version || PHOTON_VERSION,
930
1309
  description: entry.metadata?.description
931
- ? entry.metadata.description.substring(0, 50) + (entry.metadata.description.length > 50 ? '...' : '')
1310
+ ? entry.metadata.description.substring(0, 50) +
1311
+ (entry.metadata.description.length > 50 ? '...' : '')
932
1312
  : '-',
933
1313
  marketplace: entry.marketplace.name,
934
1314
  });
@@ -940,7 +1320,7 @@ program
940
1320
  }
941
1321
  catch (error) {
942
1322
  const { printError } = await import('./cli-formatter.js');
943
- printError(error.message);
1323
+ printError(getErrorMessage(error));
944
1324
  process.exit(1);
945
1325
  }
946
1326
  });
@@ -964,7 +1344,7 @@ maker
964
1344
  // Check if file already exists
965
1345
  try {
966
1346
  await fs.access(filePath);
967
- console.error(`❌ File already exists: ${filePath}`);
1347
+ logger.error(`File already exists: ${filePath}`);
968
1348
  process.exit(1);
969
1349
  }
970
1350
  catch {
@@ -984,18 +1364,16 @@ maker
984
1364
  // Convert kebab-case to PascalCase for class name
985
1365
  const className = name
986
1366
  .split(/[-_]/)
987
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
1367
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
988
1368
  .join('');
989
- const content = template
990
- .replace(/TemplateName/g, className)
991
- .replace(/template-name/g, name);
1369
+ const content = template.replace(/TemplateName/g, className).replace(/template-name/g, name);
992
1370
  // Write file
993
1371
  await fs.writeFile(filePath, content, 'utf-8');
994
1372
  console.error(`✅ Created ${fileName} in ${workingDir}`);
995
1373
  console.error(`Run with: photon mcp ${name} --dev`);
996
1374
  }
997
1375
  catch (error) {
998
- console.error(`❌ Error: ${error.message}`);
1376
+ logger.error(`Error: ${getErrorMessage(error)}`);
999
1377
  process.exit(1);
1000
1378
  }
1001
1379
  });
@@ -1011,10 +1389,11 @@ maker
1011
1389
  // Resolve file path from name in working directory
1012
1390
  const filePath = await resolvePhotonPath(name, workingDir);
1013
1391
  if (!filePath) {
1014
- console.error(`❌ Photon not found: ${name}`);
1015
- console.error(`Searched in: ${workingDir}`);
1016
- console.error(`Tip: Use 'photon info' to see available photons`);
1017
- process.exit(1);
1392
+ exitWithError(`Photon not found: ${name}`, {
1393
+ exitCode: ExitCode.NOT_FOUND,
1394
+ searchedIn: workingDir,
1395
+ suggestion: "Use 'photon info' to see available photons",
1396
+ });
1018
1397
  }
1019
1398
  console.error(`Validating ${path.basename(filePath)}...\n`);
1020
1399
  // Import loader and try to load
@@ -1030,7 +1409,7 @@ maker
1030
1409
  process.exit(0);
1031
1410
  }
1032
1411
  catch (error) {
1033
- console.error(`❌ Validation failed: ${error.message}`);
1412
+ logger.error(`Validation failed: ${getErrorMessage(error)}`);
1034
1413
  process.exit(1);
1035
1414
  }
1036
1415
  });
@@ -1057,8 +1436,8 @@ maker
1057
1436
  }
1058
1437
  }
1059
1438
  catch (error) {
1060
- console.error(`❌ Error: ${error.message}`);
1061
- if (process.env.DEBUG) {
1439
+ logger.error(`Error: ${getErrorMessage(error)}`);
1440
+ if (process.env.DEBUG && error instanceof Error) {
1062
1441
  console.error(error.stack);
1063
1442
  }
1064
1443
  process.exit(1);
@@ -1077,848 +1456,342 @@ maker
1077
1456
  await performMarketplaceInit(dirPath, options);
1078
1457
  }
1079
1458
  catch (error) {
1080
- console.error(`❌ Error: ${error.message}`);
1081
- if (process.env.DEBUG) {
1459
+ logger.error(`Error: ${getErrorMessage(error)}`);
1460
+ if (process.env.DEBUG && error instanceof Error) {
1082
1461
  console.error(error.stack);
1083
1462
  }
1084
1463
  process.exit(1);
1085
1464
  }
1086
1465
  });
1087
- // Marketplace command: manage MCP marketplaces
1088
- const marketplace = program
1089
- .command('marketplace', { hidden: true })
1090
- .description('Manage MCP marketplaces');
1091
- marketplace
1092
- .command('list')
1093
- .description('List all configured marketplaces')
1094
- .action(async () => {
1095
- try {
1096
- const { MarketplaceManager } = await import('./marketplace-manager.js');
1097
- const { formatOutput, printInfo, STATUS } = await import('./cli-formatter.js');
1098
- const manager = new MarketplaceManager();
1099
- await manager.initialize();
1100
- const marketplaces = manager.getAll();
1101
- if (marketplaces.length === 0) {
1102
- printInfo('No marketplaces configured');
1103
- printInfo('Add one with: photon marketplace add portel-dev/photons');
1104
- return;
1105
- }
1106
- // Get MCP counts
1107
- const counts = await manager.getMarketplaceCounts();
1108
- // Build table data
1109
- const tableData = marketplaces.map(m => ({
1110
- name: m.name,
1111
- source: m.source || m.repo || '-',
1112
- photons: counts.get(m.name) || 0,
1113
- status: m.enabled ? STATUS.OK : STATUS.OFF,
1114
- }));
1115
- printInfo(`Configured marketplaces (${marketplaces.length}):\n`);
1116
- formatOutput(tableData, 'table');
1117
- }
1118
- catch (error) {
1119
- const { printError } = await import('./cli-formatter.js');
1120
- printError(error.message);
1121
- process.exit(1);
1122
- }
1123
- });
1124
- marketplace
1125
- .command('add')
1126
- .argument('<repo>', 'GitHub repository (username/repo or github.com URL)')
1127
- .description('Add a new MCP marketplace from GitHub')
1128
- .action(async (repo) => {
1466
+ // maker diagram: generate Mermaid diagram for a Photon
1467
+ maker
1468
+ .command('diagram <photon>')
1469
+ .option('--dir <path>', 'Directory containing photon (defaults to current directory)')
1470
+ .description('Generate Mermaid diagram for a Photon')
1471
+ .action(async (photonName, options) => {
1129
1472
  try {
1130
- const { MarketplaceManager } = await import('./marketplace-manager.js');
1131
- const manager = new MarketplaceManager();
1132
- await manager.initialize();
1133
- const { marketplace: result, added } = await manager.add(repo);
1134
- if (added) {
1135
- console.error(`✅ Added marketplace: ${result.name}`);
1136
- console.error(`Source: ${repo}`);
1137
- console.error(`URL: ${result.url}`);
1138
- // Auto-fetch marketplace.json
1139
- console.error(`Fetching marketplace metadata...`);
1140
- const success = await manager.updateMarketplaceCache(result.name);
1141
- if (success) {
1142
- console.error(`✅ Marketplace ready to use`);
1473
+ const { PhotonDocExtractor } = await import('./photon-doc-extractor.js');
1474
+ // Resolve photon path
1475
+ const dirPath = options.dir || '.';
1476
+ let photonPath = photonName;
1477
+ // If not a path, look in the directory
1478
+ if (!photonName.includes('/') && !photonName.includes('\\')) {
1479
+ if (!photonName.endsWith('.photon.ts')) {
1480
+ photonName = `${photonName}.photon.ts`;
1143
1481
  }
1482
+ photonPath = path.resolve(dirPath, photonName);
1144
1483
  }
1145
1484
  else {
1146
- console.error(`ℹ️ Marketplace already exists: ${result.name}`);
1147
- console.error(`Source: ${result.source}`);
1148
- console.error(`Skipping duplicate addition`);
1149
- }
1150
- }
1151
- catch (error) {
1152
- console.error(`❌ Error: ${error.message}`);
1153
- process.exit(1);
1154
- }
1155
- });
1156
- marketplace
1157
- .command('remove')
1158
- .argument('<name>', 'Marketplace name')
1159
- .description('Remove a marketplace')
1160
- .action(async (name) => {
1161
- try {
1162
- const { MarketplaceManager } = await import('./marketplace-manager.js');
1163
- const manager = new MarketplaceManager();
1164
- await manager.initialize();
1165
- const removed = await manager.remove(name);
1166
- if (removed) {
1167
- console.error(`✅ Removed marketplace: ${name}`);
1485
+ photonPath = path.resolve(photonName);
1168
1486
  }
1169
- else {
1170
- console.error(`❌ Marketplace '${name}' not found`);
1487
+ if (!existsSync(photonPath)) {
1488
+ logger.error(`Photon not found: ${photonPath}`);
1171
1489
  process.exit(1);
1172
1490
  }
1491
+ const extractor = new PhotonDocExtractor(photonPath);
1492
+ const diagram = await extractor.generateDiagram();
1493
+ // Output just the diagram (can be piped or copied)
1494
+ console.log(diagram);
1173
1495
  }
1174
1496
  catch (error) {
1175
- console.error(`❌ Error: ${error.message}`);
1176
- process.exit(1);
1177
- }
1178
- });
1179
- marketplace
1180
- .command('enable')
1181
- .argument('<name>', 'Marketplace name')
1182
- .description('Enable a marketplace')
1183
- .action(async (name) => {
1184
- try {
1185
- const { MarketplaceManager } = await import('./marketplace-manager.js');
1186
- const manager = new MarketplaceManager();
1187
- await manager.initialize();
1188
- const success = await manager.setEnabled(name, true);
1189
- if (success) {
1190
- console.error(`✅ Enabled marketplace: ${name}`);
1191
- }
1192
- else {
1193
- console.error(`❌ Marketplace '${name}' not found`);
1194
- process.exit(1);
1195
- }
1196
- }
1197
- catch (error) {
1198
- console.error(`❌ Error: ${error.message}`);
1199
- process.exit(1);
1200
- }
1201
- });
1202
- marketplace
1203
- .command('disable')
1204
- .argument('<name>', 'Marketplace name')
1205
- .description('Disable a marketplace')
1206
- .action(async (name) => {
1207
- try {
1208
- const { MarketplaceManager } = await import('./marketplace-manager.js');
1209
- const manager = new MarketplaceManager();
1210
- await manager.initialize();
1211
- const success = await manager.setEnabled(name, false);
1212
- if (success) {
1213
- console.error(`✅ Disabled marketplace: ${name}`);
1214
- }
1215
- else {
1216
- console.error(`❌ Marketplace '${name}' not found`);
1217
- process.exit(1);
1218
- }
1219
- }
1220
- catch (error) {
1221
- console.error(`❌ Error: ${error.message}`);
1222
- process.exit(1);
1223
- }
1224
- });
1225
- // Add command: add MCP from marketplace
1226
- program
1227
- .command('add', { hidden: true })
1228
- .argument('<name>', 'MCP name to add')
1229
- .option('--marketplace <name>', 'Specific marketplace to use')
1230
- .option('-y, --yes', 'Automatically select first suggestion without prompting')
1231
- .description('Add an MCP from a marketplace')
1232
- .action(async (name, options, command) => {
1233
- try {
1234
- // Get working directory from global options
1235
- const workingDir = command.parent?.opts().dir || DEFAULT_WORKING_DIR;
1236
- await ensureWorkingDir(workingDir);
1237
- const { MarketplaceManager } = await import('./marketplace-manager.js');
1238
- const manager = new MarketplaceManager();
1239
- await manager.initialize();
1240
- // Check for conflicts
1241
- let conflict = await manager.checkConflict(name, options.marketplace);
1242
- if (!conflict.sources || conflict.sources.length === 0) {
1243
- console.error(`❌ MCP '${name}' not found in any enabled marketplace\n`);
1244
- // Search for similar names
1245
- const searchResults = await manager.search(name);
1246
- if (searchResults.size > 0) {
1247
- console.error(`Did you mean one of these?\n`);
1248
- // Convert search results to array for selection
1249
- const suggestions = [];
1250
- let count = 0;
1251
- for (const [mcpName, sources] of searchResults) {
1252
- if (count >= 5)
1253
- break; // Limit to 5 suggestions
1254
- const source = sources[0]; // Use first marketplace
1255
- const version = source.metadata?.version || 'unknown';
1256
- const description = source.metadata?.description || 'No description';
1257
- suggestions.push({ name: mcpName, version, description });
1258
- console.error(` [${count + 1}] ${mcpName} (v${version})`);
1259
- console.error(` ${description}`);
1260
- count++;
1261
- }
1262
- // Interactive selection or auto-select with -y
1263
- let selectedIndex;
1264
- if (options.yes) {
1265
- // Auto-select first suggestion
1266
- selectedIndex = 0;
1267
- }
1268
- else {
1269
- // Interactive selection
1270
- selectedIndex = await new Promise((resolve) => {
1271
- const rl = readline.createInterface({
1272
- input: process.stdin,
1273
- output: process.stderr,
1274
- });
1275
- const askQuestion = () => {
1276
- rl.question(`\nWhich one? [1-${suggestions.length}] (or press Enter to cancel): `, (answer) => {
1277
- const trimmed = answer.trim();
1278
- // Empty input = cancel
1279
- if (trimmed === '') {
1280
- rl.close();
1281
- resolve(null);
1282
- return;
1283
- }
1284
- const choice = parseInt(trimmed, 10);
1285
- // Validate input
1286
- if (isNaN(choice) || choice < 1 || choice > suggestions.length) {
1287
- console.error(`Invalid choice. Please enter a number between 1 and ${suggestions.length}.`);
1288
- askQuestion();
1289
- }
1290
- else {
1291
- rl.close();
1292
- resolve(choice - 1);
1293
- }
1294
- });
1295
- };
1296
- askQuestion();
1297
- });
1298
- if (selectedIndex === null) {
1299
- console.error('\nCancelled.');
1300
- process.exit(0);
1301
- }
1302
- }
1303
- // Update name to the selected MCP
1304
- name = suggestions[selectedIndex].name;
1305
- console.error(`\n✓ Selected: ${name}`);
1306
- // Re-check for conflicts with the new name
1307
- conflict = await manager.checkConflict(name, options.marketplace);
1308
- if (!conflict.sources || conflict.sources.length === 0) {
1309
- console.error(`❌ MCP '${name}' is no longer available`);
1310
- process.exit(1);
1311
- }
1312
- }
1313
- else {
1314
- console.error(`Run 'photon info' to see all available MCPs`);
1315
- process.exit(1);
1316
- }
1317
- }
1318
- // Check if already exists locally
1319
- const filePath = path.join(workingDir, `${name}.photon.ts`);
1320
- const fileName = `${name}.photon.ts`;
1321
- if (existsSync(filePath)) {
1322
- console.error(`⚠️ MCP '${name}' already exists`);
1323
- console.error(`Use 'photon upgrade ${name}' to update it`);
1324
- process.exit(1);
1325
- }
1326
- // Handle conflicts
1327
- let selectedMarketplace;
1328
- let selectedMetadata;
1329
- if (conflict.hasConflict) {
1330
- console.error(`⚠️ MCP '${name}' found in multiple marketplaces:\n`);
1331
- conflict.sources.forEach((source, index) => {
1332
- const marker = source.marketplace.name === conflict.recommendation ? '→' : ' ';
1333
- const version = source.metadata?.version || 'unknown';
1334
- console.error(` ${marker} [${index + 1}] ${source.marketplace.name} (v${version})`);
1335
- console.error(` ${source.marketplace.repo || source.marketplace.url}`);
1336
- });
1337
- if (conflict.recommendation) {
1338
- console.error(`\n💡 Recommended: ${conflict.recommendation} (newest version)`);
1339
- }
1340
- // Get default choice (recommended or first)
1341
- const recommendedIndex = conflict.sources.findIndex(s => s.marketplace.name === conflict.recommendation);
1342
- const defaultChoice = recommendedIndex !== -1 ? recommendedIndex + 1 : 1;
1343
- // Interactive selection
1344
- const selectedIndex = await new Promise((resolve) => {
1345
- const rl = readline.createInterface({
1346
- input: process.stdin,
1347
- output: process.stderr,
1348
- });
1349
- const askQuestion = () => {
1350
- rl.question(`\nWhich marketplace? [1-${conflict.sources.length}] (default: ${defaultChoice}): `, (answer) => {
1351
- const trimmed = answer.trim();
1352
- // Empty input = use default
1353
- if (trimmed === '') {
1354
- rl.close();
1355
- resolve(defaultChoice - 1);
1356
- return;
1357
- }
1358
- const choice = parseInt(trimmed, 10);
1359
- // Validate input
1360
- if (isNaN(choice) || choice < 1 || choice > conflict.sources.length) {
1361
- console.error(`Invalid choice. Please enter a number between 1 and ${conflict.sources.length}.`);
1362
- askQuestion();
1363
- }
1364
- else {
1365
- rl.close();
1366
- resolve(choice - 1);
1367
- }
1368
- });
1369
- };
1370
- askQuestion();
1371
- });
1372
- const selectedSource = conflict.sources[selectedIndex];
1373
- selectedMarketplace = selectedSource.marketplace;
1374
- selectedMetadata = selectedSource.metadata;
1375
- console.error(`\n✓ Using: ${selectedMarketplace.name}`);
1376
- }
1377
- else {
1378
- selectedMarketplace = conflict.sources[0].marketplace;
1379
- selectedMetadata = conflict.sources[0].metadata;
1380
- console.error(`Adding ${name} from ${selectedMarketplace.name}...`);
1381
- }
1382
- // Fetch content from selected marketplace
1383
- const result = await manager.fetchMCP(name);
1384
- if (!result) {
1385
- console.error(`❌ Failed to fetch MCP content`);
1386
- process.exit(1);
1387
- }
1388
- const content = result.content;
1389
- // Write file
1390
- await fs.writeFile(filePath, content, 'utf-8');
1391
- // Save installation metadata if we have it
1392
- if (selectedMetadata) {
1393
- const { calculateHash } = await import('./marketplace-manager.js');
1394
- const contentHash = calculateHash(content);
1395
- await manager.savePhotonMetadata(fileName, selectedMarketplace, selectedMetadata, contentHash);
1396
- }
1397
- console.error(`✅ Added ${name} from ${selectedMarketplace.name}`);
1398
- if (selectedMetadata?.version) {
1399
- console.error(`Version: ${selectedMetadata.version}`);
1497
+ logger.error(`Error: ${getErrorMessage(error)}`);
1498
+ if (process.env.DEBUG && error instanceof Error) {
1499
+ console.error(error.stack);
1400
1500
  }
1401
- console.error(`Location: ${filePath}`);
1402
- console.error(`\nRun with: photon mcp ${name}`);
1403
- console.error(`\nTo customize: Copy to a new name and run with --dev for hot reload`);
1404
- }
1405
- catch (error) {
1406
- console.error(`❌ Error: ${error.message}`);
1407
1501
  process.exit(1);
1408
1502
  }
1409
1503
  });
1410
- // Remove command: remove an installed photon
1411
- program
1412
- .command('remove', { hidden: true })
1413
- .argument('<name>', 'MCP name to remove')
1414
- .alias('rm')
1415
- .option('--keep-cache', 'Keep compiled cache for this photon')
1416
- .description('Remove an installed photon')
1417
- .action(async (name, options, command) => {
1504
+ // maker diagrams: generate Mermaid diagrams for all Photons in a directory
1505
+ maker
1506
+ .command('diagrams')
1507
+ .option('--dir <path>', 'Directory to scan (defaults to current directory)')
1508
+ .description('Generate Mermaid diagrams for all Photons in a directory')
1509
+ .action(async (options) => {
1418
1510
  try {
1419
- const { printInfo, printSuccess, printError } = await import('./cli-formatter.js');
1420
- const workingDir = command.parent?.opts().dir || DEFAULT_WORKING_DIR;
1421
- // Find the photon file
1422
- const filePath = await resolvePhotonPath(name, workingDir);
1423
- if (!filePath) {
1424
- printError(`Photon not found: ${name}`);
1425
- printInfo(`Searched in: ${workingDir}`);
1426
- printInfo(`Tip: Use 'photon info' to see installed photons`);
1511
+ const { PhotonDocExtractor } = await import('./photon-doc-extractor.js');
1512
+ const dirPath = path.resolve(options.dir || '.');
1513
+ const files = await fs.readdir(dirPath);
1514
+ const photonFiles = files.filter((f) => f.endsWith('.photon.ts'));
1515
+ if (photonFiles.length === 0) {
1516
+ console.error('No .photon.ts files found');
1427
1517
  process.exit(1);
1428
1518
  }
1429
- printInfo(`Removing ${name}...`);
1430
- // Remove the .photon.ts file
1431
- await fs.unlink(filePath);
1432
- printSuccess(`Removed ${name}.photon.ts`);
1433
- // Clear compiled cache unless --keep-cache
1434
- if (!options.keepCache) {
1435
- const cacheDir = path.join(os.homedir(), '.cache', 'photon-mcp', 'compiled');
1436
- const cachedFiles = [
1437
- path.join(cacheDir, `${name}.js`),
1438
- path.join(cacheDir, `${name}.js.map`),
1439
- ];
1440
- for (const cachedFile of cachedFiles) {
1441
- try {
1442
- await fs.unlink(cachedFile);
1443
- }
1444
- catch {
1445
- // Ignore if cache doesn't exist
1446
- }
1447
- }
1448
- printSuccess(`Cleared cache`);
1449
- }
1450
- console.log('');
1451
- printSuccess(`Successfully removed ${name}`);
1452
- printInfo(`To reinstall: photon add ${name}`);
1453
- }
1454
- catch (error) {
1455
- const { printError } = await import('./cli-formatter.js');
1456
- printError(error.message);
1457
- process.exit(1);
1458
- }
1459
- });
1460
- // Upgrade command: update MCPs from marketplace
1461
- program
1462
- .command('upgrade', { hidden: true })
1463
- .argument('[name]', 'MCP name to upgrade (upgrades all if omitted)')
1464
- .option('--check', 'Check for updates without upgrading')
1465
- .alias('up')
1466
- .description('Upgrade MCP(s) from marketplaces')
1467
- .action(async (name, options, command) => {
1468
- try {
1469
- const { formatOutput, printInfo, printSuccess, printWarning, printError, STATUS } = await import('./cli-formatter.js');
1470
- // Get working directory from global options
1471
- const workingDir = command.parent?.opts().dir || DEFAULT_WORKING_DIR;
1472
- const { VersionChecker } = await import('./version-checker.js');
1473
- const checker = new VersionChecker();
1474
- await checker.initialize();
1475
- if (name) {
1476
- // Upgrade single MCP
1477
- const filePath = await resolvePhotonPath(name, workingDir);
1478
- if (!filePath) {
1479
- printError(`MCP not found: ${name}`);
1480
- printInfo(`Searched in: ${workingDir}`);
1481
- process.exit(1);
1482
- }
1483
- printInfo(`Checking ${name} for updates...`);
1484
- const versionInfo = await checker.checkForUpdate(name, filePath);
1485
- if (!versionInfo.local) {
1486
- printWarning('Could not determine local version');
1487
- return;
1488
- }
1489
- if (!versionInfo.remote) {
1490
- printWarning('Not found in any marketplace. This might be a local-only MCP.');
1491
- return;
1492
- }
1493
- if (options.check) {
1494
- const tableData = [{
1495
- name,
1496
- local: versionInfo.local,
1497
- remote: versionInfo.remote,
1498
- status: versionInfo.needsUpdate ? STATUS.UPDATE : STATUS.OK,
1499
- }];
1500
- formatOutput(tableData, 'table');
1501
- return;
1502
- }
1503
- if (!versionInfo.needsUpdate) {
1504
- printSuccess(`Already up to date (${versionInfo.local})`);
1505
- return;
1506
- }
1507
- printInfo(`Upgrading ${name}: ${versionInfo.local} → ${versionInfo.remote}`);
1508
- const success = await checker.updateMCP(name, filePath);
1509
- if (success) {
1510
- printSuccess(`Successfully upgraded ${name} to ${versionInfo.remote}`);
1511
- }
1512
- else {
1513
- printError(`Failed to upgrade ${name}`);
1514
- process.exit(1);
1515
- }
1516
- }
1517
- else {
1518
- // Check/upgrade all MCPs
1519
- printInfo(`Checking all MCPs in ${workingDir}...\n`);
1520
- const updates = await checker.checkAllUpdates(workingDir);
1521
- if (updates.size === 0) {
1522
- printInfo('No MCPs found');
1523
- return;
1524
- }
1525
- const needsUpdate = [];
1526
- // Build table data
1527
- const tableData = [];
1528
- for (const [mcpName, info] of updates) {
1529
- if (info.needsUpdate) {
1530
- needsUpdate.push(mcpName);
1531
- tableData.push({
1532
- name: mcpName,
1533
- local: info.local || '-',
1534
- remote: info.remote || '-',
1535
- status: STATUS.UPDATE,
1536
- });
1537
- }
1538
- else if (info.local && info.remote) {
1539
- tableData.push({
1540
- name: mcpName,
1541
- local: info.local,
1542
- remote: info.remote,
1543
- status: STATUS.OK,
1544
- });
1545
- }
1546
- else {
1547
- tableData.push({
1548
- name: mcpName,
1549
- local: info.local || '-',
1550
- remote: info.remote || 'local only',
1551
- status: STATUS.UNKNOWN,
1552
- });
1553
- }
1554
- }
1555
- formatOutput(tableData, 'table');
1556
- if (needsUpdate.length === 0) {
1557
- console.log('');
1558
- printSuccess('All MCPs are up to date!');
1559
- return;
1560
- }
1561
- if (options.check) {
1562
- console.log('');
1563
- printInfo(`${needsUpdate.length} MCP(s) have updates available`);
1564
- printInfo(`Run 'photon upgrade' to upgrade all`);
1565
- return;
1566
- }
1567
- // Upgrade all that need updates
1568
- console.log('');
1569
- printInfo(`Upgrading ${needsUpdate.length} MCP(s)...`);
1570
- for (const mcpName of needsUpdate) {
1571
- const filePath = path.join(workingDir, `${mcpName}.photon.ts`);
1572
- const success = await checker.updateMCP(mcpName, filePath);
1573
- if (success) {
1574
- printSuccess(`Upgraded ${mcpName}`);
1575
- }
1576
- else {
1577
- printError(`Failed to upgrade ${mcpName}`);
1578
- }
1579
- }
1580
- }
1581
- }
1582
- catch (error) {
1583
- const { printError } = await import('./cli-formatter.js');
1584
- printError(error.message);
1585
- process.exit(1);
1586
- }
1587
- });
1588
- // Clear-cache command: clear compiled photon cache
1589
- program
1590
- .command('clear-cache', { hidden: true })
1591
- .argument('[name]', 'MCP name to clear cache for (clears all if omitted)')
1592
- .alias('clean')
1593
- .description('Clear compiled photon cache')
1594
- .action(async (name, options, command) => {
1595
- try {
1596
- const { printInfo, printSuccess, printError } = await import('./cli-formatter.js');
1597
- const cacheDir = path.join(os.homedir(), '.cache', 'photon-mcp', 'compiled');
1598
- if (name) {
1599
- // Clear cache for specific photon
1600
- const workingDir = command.parent?.opts().dir || DEFAULT_WORKING_DIR;
1601
- const filePath = await resolvePhotonPath(name, workingDir);
1602
- if (!filePath) {
1603
- printError(`Photon not found: ${name}`);
1604
- printInfo(`Tip: Use 'photon info' to see installed photons`);
1605
- process.exit(1);
1606
- }
1607
- printInfo(`Clearing cache for ${name}...`);
1608
- const cachedFiles = [
1609
- path.join(cacheDir, `${name}.js`),
1610
- path.join(cacheDir, `${name}.js.map`),
1611
- ];
1612
- let cleared = false;
1613
- for (const cachedFile of cachedFiles) {
1614
- try {
1615
- await fs.unlink(cachedFile);
1616
- cleared = true;
1617
- }
1618
- catch {
1619
- // Ignore if file doesn't exist
1620
- }
1621
- }
1622
- if (cleared) {
1623
- printSuccess(`Cleared cache for ${name}`);
1624
- }
1625
- else {
1626
- printInfo(`No cache found for ${name}`);
1627
- }
1628
- }
1629
- else {
1630
- // Clear all cache
1631
- printInfo('Clearing all compiled photon cache...');
1519
+ console.error(`📦 Found ${photonFiles.length} photons\n`);
1520
+ for (const file of photonFiles) {
1521
+ const photonPath = path.join(dirPath, file);
1522
+ const name = file.replace('.photon.ts', '');
1632
1523
  try {
1633
- const files = await fs.readdir(cacheDir);
1634
- let count = 0;
1635
- for (const file of files) {
1636
- const filePath = path.join(cacheDir, file);
1637
- try {
1638
- await fs.unlink(filePath);
1639
- count++;
1640
- }
1641
- catch {
1642
- // Ignore errors
1643
- }
1644
- }
1645
- if (count > 0) {
1646
- printSuccess(`Cleared ${count} cached file(s)`);
1647
- }
1648
- else {
1649
- printInfo(`Cache is already empty`);
1650
- }
1524
+ const extractor = new PhotonDocExtractor(photonPath);
1525
+ const diagram = await extractor.generateDiagram();
1526
+ console.log(`## ${name}\n`);
1527
+ console.log('```mermaid');
1528
+ console.log(diagram);
1529
+ console.log('```\n');
1651
1530
  }
1652
- catch (error) {
1653
- if (error.code === 'ENOENT') {
1654
- printInfo(`No cache directory found`);
1655
- }
1656
- else {
1657
- throw error;
1658
- }
1531
+ catch (err) {
1532
+ console.error(`⚠️ Failed to generate diagram for ${name}: ${err.message}`);
1659
1533
  }
1660
1534
  }
1661
1535
  }
1662
1536
  catch (error) {
1663
- const { printError } = await import('./cli-formatter.js');
1664
- printError(error.message);
1537
+ logger.error(`Error: ${getErrorMessage(error)}`);
1538
+ if (process.env.DEBUG && error instanceof Error) {
1539
+ console.error(error.stack);
1540
+ }
1665
1541
  process.exit(1);
1666
1542
  }
1667
1543
  });
1544
+ // Register marketplace commands (list, add, remove, enable, disable)
1545
+ registerMarketplaceCommands(program);
1546
+ // Register info command
1547
+ registerInfoCommand(program, DEFAULT_WORKING_DIR);
1548
+ // Register package management commands
1549
+ registerPackageCommands(program, DEFAULT_WORKING_DIR);
1550
+ // Register package-app command (cross-platform PWA launchers)
1551
+ registerPackageAppCommand(program, DEFAULT_WORKING_DIR);
1668
1552
  // Doctor command: diagnose photon environment
1669
1553
  program
1670
- .command('doctor', { hidden: true })
1554
+ .command('doctor')
1671
1555
  .argument('[name]', 'Photon name to diagnose (checks environment if omitted)')
1672
- .description('Run diagnostics on photon environment and installations')
1556
+ .description('Run diagnostics on photon environment, ports, and configuration')
1557
+ .option('--port <number>', 'Port to check for availability', '3000')
1673
1558
  .action(async (name, options, command) => {
1674
1559
  try {
1675
- const { formatOutput, printInfo, printSuccess, printWarning, STATUS } = await import('./cli-formatter.js');
1560
+ const { formatOutput, printHeader, printInfo, printSuccess, printWarning, STATUS } = await import('./cli-formatter.js');
1676
1561
  const workingDir = command.parent?.opts().dir || DEFAULT_WORKING_DIR;
1677
- let issuesFound = 0;
1678
- printInfo('Running Photon diagnostics...\n');
1679
- // Build diagnostic data
1680
1562
  const diagnostics = {};
1681
- // Check Node version
1563
+ const suggestions = [];
1564
+ let issuesFound = 0;
1565
+ printHeader('Photon Doctor');
1566
+ printInfo('Running environment checks...\n');
1567
+ // Node runtime
1682
1568
  const nodeVersion = process.version;
1683
1569
  const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
1684
1570
  diagnostics['Node.js'] = {
1685
1571
  version: nodeVersion,
1686
1572
  status: majorVersion >= 18 ? STATUS.OK : STATUS.ERROR,
1687
- note: majorVersion >= 18 ? 'supported' : 'requires Node.js 18+',
1688
1573
  };
1689
- if (majorVersion < 18)
1574
+ if (majorVersion < 18) {
1690
1575
  issuesFound++;
1691
- // Check npm/npx
1576
+ suggestions.push('Upgrade to Node.js 18+ (https://nodejs.org).');
1577
+ }
1578
+ // npm availability
1692
1579
  try {
1693
1580
  const { execSync } = await import('child_process');
1694
1581
  const npmVersion = execSync('npm --version', { encoding: 'utf-8' }).trim();
1695
- diagnostics['Package Manager'] = {
1696
- npm: npmVersion,
1697
- status: STATUS.OK,
1698
- };
1582
+ diagnostics['Package manager'] = { npm: npmVersion, status: STATUS.OK };
1699
1583
  }
1700
1584
  catch {
1701
- diagnostics['Package Manager'] = {
1702
- npm: 'not found',
1703
- status: STATUS.ERROR,
1704
- };
1585
+ diagnostics['Package manager'] = { npm: 'not found', status: STATUS.ERROR };
1705
1586
  issuesFound++;
1587
+ suggestions.push('Install npm / npx so Photon can install dependencies.');
1706
1588
  }
1707
- // Check working directory
1589
+ // Working directory health
1708
1590
  try {
1709
1591
  await fs.access(workingDir);
1710
1592
  const stats = await fs.stat(workingDir);
1711
1593
  if (stats.isDirectory()) {
1712
1594
  const mcps = await listPhotonMCPs(workingDir);
1713
- diagnostics['Working Directory'] = {
1595
+ diagnostics['Working directory'] = {
1714
1596
  path: workingDir,
1715
1597
  status: STATUS.OK,
1716
1598
  photons: mcps.length,
1717
1599
  };
1718
1600
  }
1719
1601
  else {
1720
- diagnostics['Working Directory'] = {
1602
+ diagnostics['Working directory'] = {
1721
1603
  path: workingDir,
1722
1604
  status: STATUS.ERROR,
1723
- note: 'not a directory',
1605
+ note: 'Not a directory',
1724
1606
  };
1725
1607
  issuesFound++;
1608
+ suggestions.push(`Fix working directory: rm ${workingDir} && mkdir -p ${workingDir}`);
1726
1609
  }
1727
1610
  }
1728
1611
  catch {
1729
- diagnostics['Working Directory'] = {
1612
+ diagnostics['Working directory'] = {
1730
1613
  path: workingDir,
1731
- status: STATUS.UNKNOWN,
1732
- note: 'will be created on first use',
1614
+ status: STATUS.WARN,
1615
+ note: 'Will be created on first use',
1733
1616
  };
1734
1617
  }
1735
- // Check cache directory
1618
+ // Cache directory insight
1736
1619
  const cacheDir = path.join(os.homedir(), '.cache', 'photon-mcp', 'compiled');
1737
1620
  try {
1738
1621
  await fs.access(cacheDir);
1739
1622
  const files = await fs.readdir(cacheDir);
1740
- diagnostics['Cache Directory'] = {
1623
+ diagnostics['Cache directory'] = {
1741
1624
  path: cacheDir,
1742
1625
  status: STATUS.OK,
1743
1626
  cachedFiles: files.length,
1744
1627
  };
1745
1628
  }
1746
1629
  catch {
1747
- diagnostics['Cache Directory'] = {
1630
+ diagnostics['Cache directory'] = {
1748
1631
  path: cacheDir,
1749
1632
  status: STATUS.UNKNOWN,
1750
- note: 'will be created on first use',
1633
+ note: 'Created on demand',
1751
1634
  };
1752
1635
  }
1753
- // Check marketplaces and conflicts
1754
- let manager;
1636
+ // Port availability
1637
+ const port = parseInt(options.port, 10);
1638
+ const available = await isPortAvailable(port);
1639
+ diagnostics['Ports'] = {
1640
+ port,
1641
+ status: available ? STATUS.OK : STATUS.ERROR,
1642
+ note: available ? 'Available' : 'In use by another process',
1643
+ };
1644
+ if (!available) {
1645
+ issuesFound++;
1646
+ suggestions.push(`Port ${port} is busy. Run Photon with '--port ${port + 1}' or stop the conflicting service.`);
1647
+ }
1648
+ // Marketplace configuration
1755
1649
  try {
1756
1650
  const { MarketplaceManager } = await import('./marketplace-manager.js');
1757
- manager = new MarketplaceManager();
1651
+ const manager = new MarketplaceManager();
1758
1652
  await manager.initialize();
1759
- const marketplaces = manager.getAll();
1760
- const enabled = marketplaces.filter((m) => m.enabled);
1761
- if (enabled.length > 0) {
1762
- // Check for conflicts
1763
- const conflicts = await manager.detectAllConflicts();
1764
- if (conflicts.size > 0) {
1765
- diagnostics['Marketplaces'] = {
1766
- enabled: enabled.length,
1767
- status: STATUS.WARN,
1768
- sources: enabled.map((m) => m.name),
1769
- conflicts: `${conflicts.size} photon(s) in multiple marketplaces`,
1770
- };
1771
- issuesFound++;
1772
- }
1773
- else {
1774
- diagnostics['Marketplaces'] = {
1775
- enabled: enabled.length,
1776
- status: STATUS.OK,
1777
- sources: enabled.map((m) => m.name),
1778
- };
1779
- }
1653
+ const enabled = manager.getAll().filter((m) => m.enabled);
1654
+ if (enabled.length === 0) {
1655
+ diagnostics['Marketplaces'] = {
1656
+ status: STATUS.WARN,
1657
+ note: 'No marketplaces configured. Add one with: photon marketplace add portel-dev/photons',
1658
+ };
1659
+ suggestions.push('Add at least one marketplace so you can install community photons.');
1780
1660
  }
1781
1661
  else {
1662
+ const conflicts = await manager.detectAllConflicts();
1782
1663
  diagnostics['Marketplaces'] = {
1783
- enabled: 0,
1784
- status: STATUS.UNKNOWN,
1785
- note: 'Add one with: photon marketplace add portel-dev/photons',
1664
+ status: conflicts.size > 0 ? STATUS.WARN : STATUS.OK,
1665
+ enabled: enabled.map((m) => m.name),
1666
+ conflicts: conflicts.size,
1786
1667
  };
1668
+ if (conflicts.size > 0) {
1669
+ issuesFound++;
1670
+ suggestions.push('Resolve duplicate photons with: photon marketplace resolve');
1671
+ }
1787
1672
  }
1788
1673
  }
1789
1674
  catch (error) {
1790
- diagnostics['Marketplaces'] = {
1791
- status: STATUS.ERROR,
1792
- error: error.message,
1793
- };
1675
+ diagnostics['Marketplaces'] = { status: STATUS.ERROR, error: getErrorMessage(error) };
1676
+ suggestions.push('Marketplace config failed to load. Run photon marketplace list to debug.');
1794
1677
  issuesFound++;
1795
1678
  }
1796
- // Check specific photon if provided
1679
+ // Photon-specific checks
1797
1680
  if (name) {
1798
- const photonDiag = {};
1681
+ const photonSection = {};
1799
1682
  const filePath = await resolvePhotonPath(name, workingDir);
1800
1683
  if (!filePath) {
1801
- photonDiag['location'] = 'not found';
1802
- photonDiag['status'] = STATUS.ERROR;
1684
+ photonSection.status = STATUS.ERROR;
1685
+ photonSection.note = `Not installed in ${workingDir}`;
1686
+ suggestions.push(`Install ${name} with: photon add ${name}`);
1803
1687
  issuesFound++;
1804
1688
  }
1805
1689
  else {
1806
- photonDiag['location'] = filePath;
1807
- photonDiag['status'] = STATUS.OK;
1808
- // Check if it compiles
1809
- try {
1810
- const source = await fs.readFile(filePath, 'utf-8');
1811
- const extractor = new SchemaExtractor();
1812
- const params = extractor.extractConstructorParams(source);
1813
- photonDiag['syntax'] = STATUS.OK;
1814
- photonDiag['constructorParams'] = params.length;
1815
- }
1816
- catch (error) {
1817
- photonDiag['syntax'] = STATUS.ERROR;
1818
- photonDiag['syntaxError'] = error.message;
1819
- issuesFound++;
1690
+ photonSection.status = STATUS.OK;
1691
+ photonSection.path = filePath;
1692
+ const params = await extractConstructorParams(filePath);
1693
+ if (params.length > 0) {
1694
+ photonSection.environment = params.map((param) => {
1695
+ const envVar = toEnvVarName(name, param.name);
1696
+ const value = process.env[envVar];
1697
+ const ok = Boolean(value) || param.isOptional || param.hasDefault;
1698
+ if (!ok) {
1699
+ issuesFound++;
1700
+ suggestions.push(`Set ${envVar} for ${name} (e.g. export ${envVar}=value).`);
1701
+ }
1702
+ return {
1703
+ name: envVar,
1704
+ status: ok ? STATUS.OK : STATUS.ERROR,
1705
+ value: value
1706
+ ? 'configured'
1707
+ : param.hasDefault
1708
+ ? `default: ${formatDefaultValue(param.defaultValue)}`
1709
+ : 'missing',
1710
+ };
1711
+ });
1820
1712
  }
1821
- // Check cache
1822
1713
  const cachedFile = path.join(cacheDir, `${name}.js`);
1823
1714
  try {
1824
1715
  await fs.access(cachedFile);
1825
- photonDiag['cache'] = STATUS.OK;
1716
+ photonSection.cache = { status: STATUS.OK, note: 'Warm' };
1826
1717
  }
1827
1718
  catch {
1828
- photonDiag['cache'] = 'not cached yet';
1829
- }
1830
- // Check dependencies and security
1831
- try {
1832
- const { DependencyManager } = await import('@portel/photon-core');
1833
- const depManager = new DependencyManager();
1834
- const dependencies = await depManager.extractDependencies(filePath);
1835
- if (dependencies.length > 0) {
1836
- photonDiag['dependencies'] = dependencies.map(dep => `${dep.name}@${dep.version}`);
1837
- // Security audit
1838
- try {
1839
- const { SecurityScanner } = await import('./security-scanner.js');
1840
- const scanner = new SecurityScanner();
1841
- const depStrings = dependencies.map(d => `${d.name}@${d.version}`);
1842
- const result = await scanner.auditMCP(name, depStrings);
1843
- if (result.totalVulnerabilities > 0) {
1844
- photonDiag['security'] = STATUS.ERROR;
1845
- photonDiag['vulnerabilities'] = `${result.totalVulnerabilities} (${result.criticalCount} critical, ${result.highCount} high)`;
1846
- issuesFound++;
1847
- }
1848
- else {
1849
- photonDiag['security'] = STATUS.OK;
1850
- }
1851
- }
1852
- catch {
1853
- photonDiag['security'] = 'could not check';
1854
- }
1855
- }
1856
- else {
1857
- photonDiag['dependencies'] = 'none';
1858
- photonDiag['security'] = STATUS.OK;
1859
- }
1860
- }
1861
- catch (error) {
1862
- photonDiag['dependencies'] = `error: ${error.message}`;
1719
+ photonSection.cache = {
1720
+ status: STATUS.WARN,
1721
+ note: 'Not compiled yet (first run will compile)',
1722
+ };
1863
1723
  }
1864
1724
  }
1865
- diagnostics[`Photon: ${name}`] = photonDiag;
1725
+ diagnostics[`Photon: ${name}`] = photonSection;
1866
1726
  }
1867
1727
  formatOutput(diagnostics, 'tree');
1868
- // Summary
1869
- console.log('');
1870
- if (issuesFound === 0) {
1871
- printSuccess('No issues found! Photon environment is healthy.');
1728
+ if (suggestions.length > 0) {
1729
+ printHeader('Suggested fixes');
1730
+ suggestions.forEach((tip, idx) => console.log(` ${idx + 1}. ${tip}`));
1731
+ }
1732
+ if (issuesFound === 0 && suggestions.length === 0) {
1733
+ printSuccess('\nAll checks passed!');
1872
1734
  }
1873
1735
  else {
1874
- printWarning(`Found ${issuesFound} issue(s). Please address the items marked with '${STATUS.ERROR}' above.`);
1875
- process.exit(1);
1736
+ printWarning(`\nDetected ${issuesFound || suggestions.length} potential issue(s).`);
1876
1737
  }
1877
1738
  }
1878
1739
  catch (error) {
1879
1740
  const { printError } = await import('./cli-formatter.js');
1880
- printError(error.message);
1741
+ printError(getErrorMessage(error));
1881
1742
  process.exit(1);
1882
1743
  }
1883
1744
  });
1884
1745
  // CLI command: directly invoke photon methods
1746
+ // Also serves as escape hatch for photons with reserved names (e.g., photon cli list get)
1885
1747
  program
1886
- .command('cli <photon> [method] [args...]', { hidden: true })
1887
- .description('Run photon methods directly from the command line')
1748
+ .command('cli <photon> [method] [args...]')
1749
+ .description('Run photon methods from command line (escape hatch for reserved names)')
1888
1750
  .allowUnknownOption()
1889
1751
  .helpOption(false) // Disable default help so we can handle it ourselves
1890
1752
  .action(async (photon, method, args) => {
1891
1753
  // Handle help flag
1892
1754
  if (photon === '--help' || photon === '-h') {
1893
1755
  console.log(`USAGE:
1894
- photon cli <photon-name> [method] [args...]
1756
+ photon <photon-name> [method] [args...]
1757
+ photon cli <photon-name> [method] [args...] (explicit form)
1895
1758
 
1896
1759
  DESCRIPTION:
1897
1760
  Run photon methods directly from the command line. Photons provide
1898
1761
  a CLI interface automatically based on their exported methods.
1899
1762
 
1763
+ The 'cli' command is optional - you can run photons directly:
1764
+ photon lg-remote volume +5 (implicit)
1765
+ photon cli lg-remote volume +5 (explicit)
1766
+
1767
+ Use 'photon cli' explicitly when your photon name conflicts with
1768
+ a reserved command (serve, beam, list, init, etc.)
1769
+
1900
1770
  EXAMPLES:
1901
1771
  # List all methods for a photon
1902
- photon cli lg-remote
1772
+ photon lg-remote
1903
1773
 
1904
1774
  # Call a method with no parameters
1905
- photon cli lg-remote status
1775
+ photon lg-remote status
1906
1776
 
1907
1777
  # Call a method with parameters
1908
- photon cli lg-remote volume 50
1909
- photon cli lg-remote volume +5
1910
- photon cli lg-remote volume --level=-3
1778
+ photon lg-remote volume 50
1779
+ photon lg-remote volume +5
1780
+ photon spotify play
1911
1781
 
1912
1782
  # Get method-specific help
1913
- photon cli lg-remote volume --help
1783
+ photon lg-remote volume --help
1914
1784
 
1915
1785
  # Output raw JSON instead of formatted text
1916
- photon cli lg-remote status --json
1786
+ photon lg-remote status --json
1787
+
1788
+ # Escape hatch for reserved-name photons
1789
+ photon cli list get (photon named "list", method "get")
1790
+ photon cli serve status (photon named "serve", method "status")
1917
1791
 
1918
1792
  SEE ALSO:
1919
- photon info List all installed photons
1793
+ photon list List all installed photons
1920
1794
  photon add <name> Install a photon from marketplace
1921
- photon alias Create CLI shortcuts for photons
1922
1795
  `);
1923
1796
  return;
1924
1797
  }
@@ -1957,13 +1830,124 @@ program
1957
1830
  const { listAliases } = await import('./cli-alias.js');
1958
1831
  await listAliases();
1959
1832
  });
1833
+ // Test command: run tests for photons
1834
+ program
1835
+ .command('test')
1836
+ .argument('[photon]', 'Photon to test (tests all if omitted)')
1837
+ .argument('[test]', 'Specific test to run')
1838
+ .option('--json', 'Output results as JSON')
1839
+ .option('--mode <mode>', 'Test mode: direct (unit), cli (integration via CLI), mcp (integration via MCP), all', 'direct')
1840
+ .description('Run test methods in photons')
1841
+ .action(async (photon, test, options) => {
1842
+ try {
1843
+ const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1844
+ const { runTests } = await import('./test-runner.js');
1845
+ // Validate mode
1846
+ const validModes = ['direct', 'cli', 'mcp', 'all'];
1847
+ if (!validModes.includes(options.mode)) {
1848
+ logger.error(`Invalid mode: ${options.mode}. Valid modes: ${validModes.join(', ')}`);
1849
+ process.exit(1);
1850
+ }
1851
+ const summary = await runTests(workingDir, photon, test, {
1852
+ json: options.json,
1853
+ mode: options.mode,
1854
+ });
1855
+ // Exit with error code if any tests failed
1856
+ if (summary.failed > 0) {
1857
+ process.exit(1);
1858
+ }
1859
+ }
1860
+ catch (error) {
1861
+ logger.error(`Error: ${getErrorMessage(error)}`);
1862
+ process.exit(1);
1863
+ }
1864
+ });
1865
+ // Reserved commands that should NOT be treated as photon names
1866
+ // Reserved commands that should NOT be treated as photon names
1867
+ // If first arg is not in this list, it's assumed to be a photon name (implicit CLI mode)
1868
+ const RESERVED_COMMANDS = [
1869
+ // Core commands
1870
+ 'serve',
1871
+ 'sse',
1872
+ 'beam',
1873
+ 'list',
1874
+ 'ls',
1875
+ 'info',
1876
+ 'test',
1877
+ // Photon management
1878
+ 'new',
1879
+ 'init',
1880
+ 'validate',
1881
+ 'sync',
1882
+ 'add',
1883
+ 'remove',
1884
+ 'rm',
1885
+ // Maintenance
1886
+ 'upgrade',
1887
+ 'up',
1888
+ 'update',
1889
+ 'doctor',
1890
+ 'clear-cache',
1891
+ 'clean',
1892
+ // Aliases
1893
+ 'cli',
1894
+ 'alias',
1895
+ 'unalias',
1896
+ 'aliases',
1897
+ // Marketplace
1898
+ 'marketplace',
1899
+ // Packaging
1900
+ 'package',
1901
+ // Hidden/advanced
1902
+ 'mcp',
1903
+ 'search',
1904
+ 'maker',
1905
+ 'host',
1906
+ 'diagram',
1907
+ 'diagrams',
1908
+ 'enable',
1909
+ 'disable',
1910
+ // Help/version (handled by commander)
1911
+ 'help',
1912
+ '--help',
1913
+ '-h',
1914
+ 'version',
1915
+ '--version',
1916
+ '-V',
1917
+ ];
1960
1918
  // All known commands for "did you mean" suggestions
1961
1919
  const knownCommands = [
1962
- 'mcp', 'info', 'list', 'ls', 'search',
1963
- 'add', 'remove', 'rm', 'upgrade', 'up', 'update',
1964
- 'clear-cache', 'clean', 'doctor',
1965
- 'cli', 'alias', 'unalias', 'aliases',
1966
- 'marketplace', 'maker',
1920
+ 'serve',
1921
+ 'sse',
1922
+ 'beam',
1923
+ 'list',
1924
+ 'ls',
1925
+ 'info',
1926
+ 'test',
1927
+ 'new',
1928
+ 'init',
1929
+ 'validate',
1930
+ 'sync',
1931
+ 'add',
1932
+ 'remove',
1933
+ 'rm',
1934
+ 'upgrade',
1935
+ 'up',
1936
+ 'update',
1937
+ 'clear-cache',
1938
+ 'clean',
1939
+ 'doctor',
1940
+ 'cli',
1941
+ 'alias',
1942
+ 'unalias',
1943
+ 'aliases',
1944
+ 'mcp',
1945
+ 'search',
1946
+ 'marketplace',
1947
+ 'maker',
1948
+ 'host',
1949
+ 'diagram',
1950
+ 'diagrams',
1967
1951
  ];
1968
1952
  const knownSubcommands = {
1969
1953
  marketplace: ['list', 'add', 'remove', 'enable', 'disable'],
@@ -2019,7 +2003,7 @@ program.on('command:*', async (operands) => {
2019
2003
  printError(`Unknown command: ${unknownCommand}`);
2020
2004
  // Check if it's a subcommand typo for a known parent
2021
2005
  const args = process.argv.slice(2);
2022
- const parentIndex = args.findIndex(arg => knownSubcommands[arg]);
2006
+ const parentIndex = args.findIndex((arg) => knownSubcommands[arg]);
2023
2007
  if (parentIndex !== -1 && parentIndex < args.indexOf(unknownCommand)) {
2024
2008
  const parent = args[parentIndex];
2025
2009
  const suggestion = findClosestCommand(unknownCommand, knownSubcommands[parent]);
@@ -2038,7 +2022,48 @@ program.on('command:*', async (operands) => {
2038
2022
  printInfo(`Run 'photon --help' for usage`);
2039
2023
  process.exit(1);
2040
2024
  });
2041
- program.parse();
2025
+ // ══════════════════════════════════════════════════════════════════════════════
2026
+ // IMPLICIT CLI MODE
2027
+ // ══════════════════════════════════════════════════════════════════════════════
2028
+ // If the first argument is not a reserved command, treat it as a photon name
2029
+ // This enables: `photon lg-remote volume +5` instead of `photon cli lg-remote volume +5`
2030
+ function preprocessArgs() {
2031
+ const args = process.argv.slice(2);
2032
+ // No args - default to beam with auto-open browser
2033
+ if (args.length === 0) {
2034
+ return [...process.argv, 'beam', '--open'];
2035
+ }
2036
+ // Find the first non-flag argument (skip values of flags that take a parameter)
2037
+ const flagsWithValues = ['--dir', '--log-level'];
2038
+ const firstArgIndex = args.findIndex((arg, i) => {
2039
+ if (arg.startsWith('-'))
2040
+ return false;
2041
+ // Skip values of preceding flags (e.g., "." in "--dir .")
2042
+ if (i > 0 && flagsWithValues.includes(args[i - 1]))
2043
+ return false;
2044
+ return true;
2045
+ });
2046
+ if (firstArgIndex === -1) {
2047
+ // No subcommand — only flags present (e.g., --dir=. --log-level debug)
2048
+ // photon --help / -h / --version / -V → show program help/version
2049
+ if (args.some((a) => a === '--help' || a === '-h' || a === '--version' || a === '-V')) {
2050
+ return process.argv;
2051
+ }
2052
+ // Otherwise default to beam (e.g., photon --dir=. → photon --dir=. beam --open)
2053
+ return [...process.argv, 'beam', '--open'];
2054
+ }
2055
+ const firstArg = args[firstArgIndex];
2056
+ // If first arg is a reserved command, let commander handle normally
2057
+ if (RESERVED_COMMANDS.includes(firstArg)) {
2058
+ return process.argv;
2059
+ }
2060
+ // First arg looks like a photon name - inject 'cli' command
2061
+ // photon lg-remote volume +5 → photon cli lg-remote volume +5
2062
+ const newArgs = [...process.argv];
2063
+ newArgs.splice(2 + firstArgIndex, 0, 'cli');
2064
+ return newArgs;
2065
+ }
2066
+ program.parse(preprocessArgs());
2042
2067
  /**
2043
2068
  * Inline template fallback
2044
2069
  */