@portel/photon 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/README.md +326 -1177
  2. package/dist/auto-ui/beam.d.ts +14 -0
  3. package/dist/auto-ui/beam.d.ts.map +1 -0
  4. package/dist/auto-ui/beam.js +3057 -0
  5. package/dist/auto-ui/beam.js.map +1 -0
  6. package/dist/auto-ui/bridge/index.d.ts +37 -0
  7. package/dist/auto-ui/bridge/index.d.ts.map +1 -0
  8. package/dist/auto-ui/bridge/index.js +555 -0
  9. package/dist/auto-ui/bridge/index.js.map +1 -0
  10. package/dist/auto-ui/bridge/openai-shim.d.ts +20 -0
  11. package/dist/auto-ui/bridge/openai-shim.d.ts.map +1 -0
  12. package/dist/auto-ui/bridge/openai-shim.js +231 -0
  13. package/dist/auto-ui/bridge/openai-shim.js.map +1 -0
  14. package/dist/auto-ui/bridge/photon-app.d.ts +162 -0
  15. package/dist/auto-ui/bridge/photon-app.d.ts.map +1 -0
  16. package/dist/auto-ui/bridge/photon-app.js +460 -0
  17. package/dist/auto-ui/bridge/photon-app.js.map +1 -0
  18. package/dist/auto-ui/bridge/types.d.ts +128 -0
  19. package/dist/auto-ui/bridge/types.d.ts.map +1 -0
  20. package/dist/auto-ui/bridge/types.js +7 -0
  21. package/dist/auto-ui/bridge/types.js.map +1 -0
  22. package/dist/auto-ui/components/card.d.ts +13 -0
  23. package/dist/auto-ui/components/card.d.ts.map +1 -0
  24. package/dist/auto-ui/components/card.js +64 -0
  25. package/dist/auto-ui/components/card.js.map +1 -0
  26. package/dist/auto-ui/components/form.d.ts +15 -0
  27. package/dist/auto-ui/components/form.d.ts.map +1 -0
  28. package/dist/auto-ui/components/form.js +72 -0
  29. package/dist/auto-ui/components/form.js.map +1 -0
  30. package/dist/auto-ui/components/list.d.ts +13 -0
  31. package/dist/auto-ui/components/list.d.ts.map +1 -0
  32. package/dist/auto-ui/components/list.js +58 -0
  33. package/dist/auto-ui/components/list.js.map +1 -0
  34. package/dist/auto-ui/components/progress.d.ts +18 -0
  35. package/dist/auto-ui/components/progress.d.ts.map +1 -0
  36. package/dist/auto-ui/components/progress.js +125 -0
  37. package/dist/auto-ui/components/progress.js.map +1 -0
  38. package/dist/auto-ui/components/table.d.ts +13 -0
  39. package/dist/auto-ui/components/table.d.ts.map +1 -0
  40. package/dist/auto-ui/components/table.js +82 -0
  41. package/dist/auto-ui/components/table.js.map +1 -0
  42. package/dist/auto-ui/components/tree.d.ts +13 -0
  43. package/dist/auto-ui/components/tree.d.ts.map +1 -0
  44. package/dist/auto-ui/components/tree.js +61 -0
  45. package/dist/auto-ui/components/tree.js.map +1 -0
  46. package/dist/auto-ui/daemon-tools.d.ts +45 -0
  47. package/dist/auto-ui/daemon-tools.d.ts.map +1 -0
  48. package/dist/auto-ui/daemon-tools.js +580 -0
  49. package/dist/auto-ui/daemon-tools.js.map +1 -0
  50. package/dist/auto-ui/design-system/index.d.ts +21 -0
  51. package/dist/auto-ui/design-system/index.d.ts.map +1 -0
  52. package/dist/auto-ui/design-system/index.js +27 -0
  53. package/dist/auto-ui/design-system/index.js.map +1 -0
  54. package/dist/auto-ui/design-system/tokens.d.ts +9 -0
  55. package/dist/auto-ui/design-system/tokens.d.ts.map +1 -0
  56. package/dist/auto-ui/design-system/tokens.js +27 -0
  57. package/dist/auto-ui/design-system/tokens.js.map +1 -0
  58. package/dist/auto-ui/design-system/transaction-ui.d.ts +70 -0
  59. package/dist/auto-ui/design-system/transaction-ui.d.ts.map +1 -0
  60. package/dist/auto-ui/design-system/transaction-ui.js +982 -0
  61. package/dist/auto-ui/design-system/transaction-ui.js.map +1 -0
  62. package/dist/auto-ui/frontend/index.html +84 -0
  63. package/dist/auto-ui/index.d.ts +23 -0
  64. package/dist/auto-ui/index.d.ts.map +1 -0
  65. package/dist/auto-ui/index.js +28 -0
  66. package/dist/auto-ui/index.js.map +1 -0
  67. package/dist/auto-ui/openapi-generator.d.ts +71 -0
  68. package/dist/auto-ui/openapi-generator.d.ts.map +1 -0
  69. package/dist/auto-ui/openapi-generator.js +223 -0
  70. package/dist/auto-ui/openapi-generator.js.map +1 -0
  71. package/dist/auto-ui/photon-bridge.d.ts +159 -0
  72. package/dist/auto-ui/photon-bridge.d.ts.map +1 -0
  73. package/dist/auto-ui/photon-bridge.js +262 -0
  74. package/dist/auto-ui/photon-bridge.js.map +1 -0
  75. package/dist/auto-ui/photon-host.d.ts +113 -0
  76. package/dist/auto-ui/photon-host.d.ts.map +1 -0
  77. package/dist/auto-ui/photon-host.js +284 -0
  78. package/dist/auto-ui/photon-host.js.map +1 -0
  79. package/dist/auto-ui/platform-compat.d.ts +71 -0
  80. package/dist/auto-ui/platform-compat.d.ts.map +1 -0
  81. package/dist/auto-ui/platform-compat.js +628 -0
  82. package/dist/auto-ui/platform-compat.js.map +1 -0
  83. package/dist/auto-ui/playground-html.d.ts +15 -0
  84. package/dist/auto-ui/playground-html.d.ts.map +1 -0
  85. package/dist/auto-ui/playground-html.js +1113 -0
  86. package/dist/auto-ui/playground-html.js.map +1 -0
  87. package/dist/auto-ui/playground-server.d.ts +7 -0
  88. package/dist/auto-ui/playground-server.d.ts.map +1 -0
  89. package/dist/auto-ui/playground-server.js +840 -0
  90. package/dist/auto-ui/playground-server.js.map +1 -0
  91. package/dist/auto-ui/registry.d.ts +13 -0
  92. package/dist/auto-ui/registry.d.ts.map +1 -0
  93. package/dist/auto-ui/registry.js +62 -0
  94. package/dist/auto-ui/registry.js.map +1 -0
  95. package/dist/auto-ui/renderer.d.ts +14 -0
  96. package/dist/auto-ui/renderer.d.ts.map +1 -0
  97. package/dist/auto-ui/renderer.js +88 -0
  98. package/dist/auto-ui/renderer.js.map +1 -0
  99. package/dist/auto-ui/rendering/components.d.ts +29 -0
  100. package/dist/auto-ui/rendering/components.d.ts.map +1 -0
  101. package/dist/auto-ui/rendering/components.js +773 -0
  102. package/dist/auto-ui/rendering/components.js.map +1 -0
  103. package/dist/auto-ui/rendering/field-analyzer.d.ts +48 -0
  104. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -0
  105. package/dist/auto-ui/rendering/field-analyzer.js +270 -0
  106. package/dist/auto-ui/rendering/field-analyzer.js.map +1 -0
  107. package/dist/auto-ui/rendering/field-renderers.d.ts +64 -0
  108. package/dist/auto-ui/rendering/field-renderers.d.ts.map +1 -0
  109. package/dist/auto-ui/rendering/field-renderers.js +317 -0
  110. package/dist/auto-ui/rendering/field-renderers.js.map +1 -0
  111. package/dist/auto-ui/rendering/index.d.ts +28 -0
  112. package/dist/auto-ui/rendering/index.d.ts.map +1 -0
  113. package/dist/auto-ui/rendering/index.js +60 -0
  114. package/dist/auto-ui/rendering/index.js.map +1 -0
  115. package/dist/auto-ui/rendering/layout-selector.d.ts +48 -0
  116. package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -0
  117. package/dist/auto-ui/rendering/layout-selector.js +352 -0
  118. package/dist/auto-ui/rendering/layout-selector.js.map +1 -0
  119. package/dist/auto-ui/rendering/template-engine.d.ts +41 -0
  120. package/dist/auto-ui/rendering/template-engine.d.ts.map +1 -0
  121. package/dist/auto-ui/rendering/template-engine.js +238 -0
  122. package/dist/auto-ui/rendering/template-engine.js.map +1 -0
  123. package/dist/auto-ui/streamable-http-transport.d.ts +103 -0
  124. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -0
  125. package/dist/auto-ui/streamable-http-transport.js +1875 -0
  126. package/dist/auto-ui/streamable-http-transport.js.map +1 -0
  127. package/dist/auto-ui/types.d.ts +384 -0
  128. package/dist/auto-ui/types.d.ts.map +1 -0
  129. package/dist/auto-ui/types.js +92 -0
  130. package/dist/auto-ui/types.js.map +1 -0
  131. package/dist/beam.bundle.js +63137 -0
  132. package/dist/beam.bundle.js.map +7 -0
  133. package/dist/claude-code-plugin.d.ts.map +1 -1
  134. package/dist/claude-code-plugin.js +30 -30
  135. package/dist/claude-code-plugin.js.map +1 -1
  136. package/dist/cli/commands/info.d.ts +11 -0
  137. package/dist/cli/commands/info.d.ts.map +1 -0
  138. package/dist/cli/commands/info.js +313 -0
  139. package/dist/cli/commands/info.js.map +1 -0
  140. package/dist/cli/commands/marketplace.d.ts +11 -0
  141. package/dist/cli/commands/marketplace.d.ts.map +1 -0
  142. package/dist/cli/commands/marketplace.js +198 -0
  143. package/dist/cli/commands/marketplace.js.map +1 -0
  144. package/dist/cli/commands/package-app.d.ts +9 -0
  145. package/dist/cli/commands/package-app.d.ts.map +1 -0
  146. package/dist/cli/commands/package-app.js +191 -0
  147. package/dist/cli/commands/package-app.js.map +1 -0
  148. package/dist/cli/commands/package.d.ts +11 -0
  149. package/dist/cli/commands/package.d.ts.map +1 -0
  150. package/dist/cli/commands/package.js +573 -0
  151. package/dist/cli/commands/package.js.map +1 -0
  152. package/dist/cli-alias.d.ts.map +1 -1
  153. package/dist/cli-alias.js +30 -28
  154. package/dist/cli-alias.js.map +1 -1
  155. package/dist/cli-formatter.d.ts +8 -24
  156. package/dist/cli-formatter.d.ts.map +1 -1
  157. package/dist/cli-formatter.js +8 -325
  158. package/dist/cli-formatter.js.map +1 -1
  159. package/dist/cli.d.ts +15 -1
  160. package/dist/cli.d.ts.map +1 -1
  161. package/dist/cli.js +1166 -1131
  162. package/dist/cli.js.map +1 -1
  163. package/dist/daemon/client.d.ts +84 -3
  164. package/dist/daemon/client.d.ts.map +1 -1
  165. package/dist/daemon/client.js +561 -11
  166. package/dist/daemon/client.js.map +1 -1
  167. package/dist/daemon/manager.d.ts +51 -12
  168. package/dist/daemon/manager.d.ts.map +1 -1
  169. package/dist/daemon/manager.js +122 -61
  170. package/dist/daemon/manager.js.map +1 -1
  171. package/dist/daemon/protocol.d.ts +62 -6
  172. package/dist/daemon/protocol.d.ts.map +1 -1
  173. package/dist/daemon/protocol.js +76 -1
  174. package/dist/daemon/protocol.js.map +1 -1
  175. package/dist/daemon/server.d.ts +6 -6
  176. package/dist/daemon/server.js +743 -133
  177. package/dist/daemon/server.js.map +1 -1
  178. package/dist/daemon/session-manager.d.ts +8 -1
  179. package/dist/daemon/session-manager.d.ts.map +1 -1
  180. package/dist/daemon/session-manager.js +32 -9
  181. package/dist/daemon/session-manager.js.map +1 -1
  182. package/dist/deploy/cloudflare.d.ts +12 -0
  183. package/dist/deploy/cloudflare.d.ts.map +1 -0
  184. package/dist/deploy/cloudflare.js +216 -0
  185. package/dist/deploy/cloudflare.js.map +1 -0
  186. package/dist/index.d.ts +1 -0
  187. package/dist/index.d.ts.map +1 -1
  188. package/dist/index.js +3 -0
  189. package/dist/index.js.map +1 -1
  190. package/dist/loader.d.ts +191 -21
  191. package/dist/loader.d.ts.map +1 -1
  192. package/dist/loader.js +1186 -319
  193. package/dist/loader.js.map +1 -1
  194. package/dist/markdown-utils.d.ts +8 -0
  195. package/dist/markdown-utils.d.ts.map +1 -0
  196. package/dist/markdown-utils.js +63 -0
  197. package/dist/markdown-utils.js.map +1 -0
  198. package/dist/marketplace-manager.d.ts +10 -0
  199. package/dist/marketplace-manager.d.ts.map +1 -1
  200. package/dist/marketplace-manager.js +112 -28
  201. package/dist/marketplace-manager.js.map +1 -1
  202. package/dist/mcp-client.d.ts +9 -0
  203. package/dist/mcp-client.d.ts.map +1 -0
  204. package/dist/mcp-client.js +11 -0
  205. package/dist/mcp-client.js.map +1 -0
  206. package/dist/mcp-elicitation.d.ts +32 -0
  207. package/dist/mcp-elicitation.d.ts.map +1 -0
  208. package/dist/mcp-elicitation.js +26 -0
  209. package/dist/mcp-elicitation.js.map +1 -0
  210. package/dist/path-resolver.d.ts +9 -12
  211. package/dist/path-resolver.d.ts.map +1 -1
  212. package/dist/path-resolver.js +13 -43
  213. package/dist/path-resolver.js.map +1 -1
  214. package/dist/photon-cli-runner.d.ts.map +1 -1
  215. package/dist/photon-cli-runner.js +204 -77
  216. package/dist/photon-cli-runner.js.map +1 -1
  217. package/dist/photon-doc-extractor.d.ts +89 -0
  218. package/dist/photon-doc-extractor.d.ts.map +1 -1
  219. package/dist/photon-doc-extractor.js +560 -32
  220. package/dist/photon-doc-extractor.js.map +1 -1
  221. package/dist/photons/maker.photon.d.ts +182 -0
  222. package/dist/photons/maker.photon.d.ts.map +1 -0
  223. package/dist/photons/maker.photon.js +504 -0
  224. package/dist/photons/maker.photon.js.map +1 -0
  225. package/dist/photons/maker.photon.ts +626 -0
  226. package/dist/photons/marketplace.photon.d.ts +110 -0
  227. package/dist/photons/marketplace.photon.d.ts.map +1 -0
  228. package/dist/photons/marketplace.photon.js +260 -0
  229. package/dist/photons/marketplace.photon.js.map +1 -0
  230. package/dist/photons/marketplace.photon.ts +378 -0
  231. package/dist/photons/tunnel.photon.d.ts +80 -0
  232. package/dist/photons/tunnel.photon.d.ts.map +1 -0
  233. package/dist/photons/tunnel.photon.js +269 -0
  234. package/dist/photons/tunnel.photon.js.map +1 -0
  235. package/dist/photons/tunnel.photon.ts +345 -0
  236. package/dist/security-scanner.d.ts.map +1 -1
  237. package/dist/security-scanner.js +18 -15
  238. package/dist/security-scanner.js.map +1 -1
  239. package/dist/serv/auth/jwt.d.ts +89 -0
  240. package/dist/serv/auth/jwt.d.ts.map +1 -0
  241. package/dist/serv/auth/jwt.js +239 -0
  242. package/dist/serv/auth/jwt.js.map +1 -0
  243. package/dist/serv/auth/oauth.d.ts +117 -0
  244. package/dist/serv/auth/oauth.d.ts.map +1 -0
  245. package/dist/serv/auth/oauth.js +395 -0
  246. package/dist/serv/auth/oauth.js.map +1 -0
  247. package/dist/serv/auth/well-known.d.ts +60 -0
  248. package/dist/serv/auth/well-known.d.ts.map +1 -0
  249. package/dist/serv/auth/well-known.js +154 -0
  250. package/dist/serv/auth/well-known.js.map +1 -0
  251. package/dist/serv/db/d1-client.d.ts +65 -0
  252. package/dist/serv/db/d1-client.d.ts.map +1 -0
  253. package/dist/serv/db/d1-client.js +137 -0
  254. package/dist/serv/db/d1-client.js.map +1 -0
  255. package/dist/serv/db/d1-stores.d.ts +62 -0
  256. package/dist/serv/db/d1-stores.d.ts.map +1 -0
  257. package/dist/serv/db/d1-stores.js +307 -0
  258. package/dist/serv/db/d1-stores.js.map +1 -0
  259. package/dist/serv/index.d.ts +114 -0
  260. package/dist/serv/index.d.ts.map +1 -0
  261. package/dist/serv/index.js +172 -0
  262. package/dist/serv/index.js.map +1 -0
  263. package/dist/serv/local.d.ts +118 -0
  264. package/dist/serv/local.d.ts.map +1 -0
  265. package/dist/serv/local.js +392 -0
  266. package/dist/serv/local.js.map +1 -0
  267. package/dist/serv/middleware/auth.d.ts +66 -0
  268. package/dist/serv/middleware/auth.d.ts.map +1 -0
  269. package/dist/serv/middleware/auth.js +178 -0
  270. package/dist/serv/middleware/auth.js.map +1 -0
  271. package/dist/serv/middleware/tenant.d.ts +94 -0
  272. package/dist/serv/middleware/tenant.d.ts.map +1 -0
  273. package/dist/serv/middleware/tenant.js +152 -0
  274. package/dist/serv/middleware/tenant.js.map +1 -0
  275. package/dist/serv/runtime/executor.d.ts +76 -0
  276. package/dist/serv/runtime/executor.d.ts.map +1 -0
  277. package/dist/serv/runtime/executor.js +105 -0
  278. package/dist/serv/runtime/executor.js.map +1 -0
  279. package/dist/serv/runtime/index.d.ts +8 -0
  280. package/dist/serv/runtime/index.d.ts.map +1 -0
  281. package/dist/serv/runtime/index.js +10 -0
  282. package/dist/serv/runtime/index.js.map +1 -0
  283. package/dist/serv/runtime/oauth-context.d.ts +121 -0
  284. package/dist/serv/runtime/oauth-context.d.ts.map +1 -0
  285. package/dist/serv/runtime/oauth-context.js +153 -0
  286. package/dist/serv/runtime/oauth-context.js.map +1 -0
  287. package/dist/serv/session/kv-store.d.ts +54 -0
  288. package/dist/serv/session/kv-store.d.ts.map +1 -0
  289. package/dist/serv/session/kv-store.js +149 -0
  290. package/dist/serv/session/kv-store.js.map +1 -0
  291. package/dist/serv/session/store.d.ts +113 -0
  292. package/dist/serv/session/store.d.ts.map +1 -0
  293. package/dist/serv/session/store.js +284 -0
  294. package/dist/serv/session/store.js.map +1 -0
  295. package/dist/serv/types/index.d.ts +147 -0
  296. package/dist/serv/types/index.d.ts.map +1 -0
  297. package/dist/serv/types/index.js +8 -0
  298. package/dist/serv/types/index.js.map +1 -0
  299. package/dist/serv/vault/token-vault.d.ts +102 -0
  300. package/dist/serv/vault/token-vault.d.ts.map +1 -0
  301. package/dist/serv/vault/token-vault.js +177 -0
  302. package/dist/serv/vault/token-vault.js.map +1 -0
  303. package/dist/server.d.ts +184 -0
  304. package/dist/server.d.ts.map +1 -1
  305. package/dist/server.js +1995 -86
  306. package/dist/server.js.map +1 -1
  307. package/dist/shared/cli-sections.d.ts +6 -0
  308. package/dist/shared/cli-sections.d.ts.map +1 -0
  309. package/dist/shared/cli-sections.js +16 -0
  310. package/dist/shared/cli-sections.js.map +1 -0
  311. package/dist/shared/cli-utils.d.ts +81 -0
  312. package/dist/shared/cli-utils.d.ts.map +1 -0
  313. package/dist/shared/cli-utils.js +174 -0
  314. package/dist/shared/cli-utils.js.map +1 -0
  315. package/dist/shared/config-docs.d.ts +6 -0
  316. package/dist/shared/config-docs.d.ts.map +1 -0
  317. package/dist/shared/config-docs.js +6 -0
  318. package/dist/shared/config-docs.js.map +1 -0
  319. package/dist/shared/error-handler.d.ts +128 -0
  320. package/dist/shared/error-handler.d.ts.map +1 -0
  321. package/dist/shared/error-handler.js +342 -0
  322. package/dist/shared/error-handler.js.map +1 -0
  323. package/dist/shared/logger.d.ts +42 -0
  324. package/dist/shared/logger.d.ts.map +1 -0
  325. package/dist/shared/logger.js +123 -0
  326. package/dist/shared/logger.js.map +1 -0
  327. package/dist/shared/performance.d.ts +65 -0
  328. package/dist/shared/performance.d.ts.map +1 -0
  329. package/dist/shared/performance.js +136 -0
  330. package/dist/shared/performance.js.map +1 -0
  331. package/dist/shared/task-runner.d.ts +2 -0
  332. package/dist/shared/task-runner.d.ts.map +1 -0
  333. package/dist/shared/task-runner.js +16 -0
  334. package/dist/shared/task-runner.js.map +1 -0
  335. package/dist/shared/validation.d.ts +6 -0
  336. package/dist/shared/validation.d.ts.map +1 -0
  337. package/dist/shared/validation.js +6 -0
  338. package/dist/shared/validation.js.map +1 -0
  339. package/dist/shared-utils.d.ts +63 -0
  340. package/dist/shared-utils.d.ts.map +1 -0
  341. package/dist/shared-utils.js +123 -0
  342. package/dist/shared-utils.js.map +1 -0
  343. package/dist/template-manager.d.ts +23 -2
  344. package/dist/template-manager.d.ts.map +1 -1
  345. package/dist/template-manager.js +176 -87
  346. package/dist/template-manager.js.map +1 -1
  347. package/dist/test-client.d.ts.map +1 -1
  348. package/dist/test-client.js +10 -8
  349. package/dist/test-client.js.map +1 -1
  350. package/dist/test-runner.d.ts +52 -0
  351. package/dist/test-runner.d.ts.map +1 -0
  352. package/dist/test-runner.js +785 -0
  353. package/dist/test-runner.js.map +1 -0
  354. package/dist/testing.d.ts +103 -0
  355. package/dist/testing.d.ts.map +1 -0
  356. package/dist/testing.js +163 -0
  357. package/dist/testing.js.map +1 -0
  358. package/dist/version-checker.d.ts.map +1 -1
  359. package/dist/version-checker.js +2 -2
  360. package/dist/version-checker.js.map +1 -1
  361. package/dist/version.d.ts +10 -0
  362. package/dist/version.d.ts.map +1 -0
  363. package/dist/version.js +21 -0
  364. package/dist/version.js.map +1 -0
  365. package/dist/watcher.d.ts +6 -3
  366. package/dist/watcher.d.ts.map +1 -1
  367. package/dist/watcher.js +49 -10
  368. package/dist/watcher.js.map +1 -1
  369. package/package.json +57 -7
  370. package/templates/cloudflare/worker.ts.template +381 -0
  371. package/templates/cloudflare/wrangler.toml.template +9 -0
  372. package/dist/base.d.ts +0 -58
  373. package/dist/base.d.ts.map +0 -1
  374. package/dist/base.js +0 -92
  375. package/dist/base.js.map +0 -1
  376. package/dist/dependency-manager.d.ts +0 -49
  377. package/dist/dependency-manager.d.ts.map +0 -1
  378. package/dist/dependency-manager.js +0 -165
  379. package/dist/dependency-manager.js.map +0 -1
  380. package/dist/registry-manager.d.ts +0 -76
  381. package/dist/registry-manager.d.ts.map +0 -1
  382. package/dist/registry-manager.js +0 -220
  383. package/dist/registry-manager.js.map +0 -1
  384. package/dist/schema-extractor.d.ts +0 -110
  385. package/dist/schema-extractor.d.ts.map +0 -1
  386. package/dist/schema-extractor.js +0 -727
  387. package/dist/schema-extractor.js.map +0 -1
  388. package/dist/test-marketplace-sources.d.ts +0 -5
  389. package/dist/test-marketplace-sources.d.ts.map +0 -1
  390. package/dist/test-marketplace-sources.js +0 -53
  391. package/dist/test-marketplace-sources.js.map +0 -1
  392. package/dist/types.d.ts +0 -109
  393. package/dist/types.d.ts.map +0 -1
  394. package/dist/types.js +0 -12
  395. 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
- }
634
+ return path.join(home, '.config/Claude/claude_desktop_config.json');
357
635
  }
358
- catch {
359
- // which/where command failed, photon not in PATH
360
- }
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,288 +1002,305 @@ 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
+ // Use actual bound port from BEAM_PORT env var (set by startBeam after binding)
1093
+ if (options.open) {
1094
+ const actualPort = process.env.BEAM_PORT || port;
1095
+ const url = `http://localhost:${actualPort}`;
1096
+ const { exec } = await import('child_process');
1097
+ const openCmd = process.platform === 'darwin'
1098
+ ? 'open'
1099
+ : process.platform === 'win32'
1100
+ ? 'start'
1101
+ : 'xdg-open';
1102
+ exec(`${openCmd} ${url}`, (err) => {
1103
+ if (err)
1104
+ logger.debug(`Could not auto-open browser: ${err.message}`);
1105
+ });
1106
+ }
1107
+ // Handle shutdown signals
1108
+ const shutdown = async () => {
1109
+ console.error('\nShutting down Photon Beam...');
1110
+ // Gracefully close external MCP clients to prevent ugly tracebacks
1111
+ try {
1112
+ const { stopBeam } = await import('./auto-ui/beam.js');
1113
+ await stopBeam();
1114
+ }
1115
+ catch {
1116
+ // Ignore cleanup errors
1117
+ }
1118
+ process.exit(0);
1119
+ };
1120
+ process.on('SIGINT', shutdown);
1121
+ process.on('SIGTERM', shutdown);
1122
+ }
1123
+ catch (error) {
1124
+ logger.error(`Error: ${getErrorMessage(error)}`);
1125
+ process.exit(1);
1126
+ }
1127
+ });
1128
+ // Serve command: multi-tenant MCP hosting (formerly serv)
1129
+ program
1130
+ .command('serve', { hidden: true })
1131
+ .option('-p, --port <number>', 'Port to run on', '4000')
1132
+ .option('-d, --debug', 'Enable debug logging')
1133
+ .description('Start local multi-tenant MCP hosting for development')
1134
+ .action(async (options) => {
1135
+ try {
1136
+ const port = parseInt(options.port, 10);
1137
+ const availablePort = await findAvailablePort(port);
1138
+ if (availablePort !== port) {
1139
+ console.error(`⚠️ Port ${port} is in use, using ${availablePort} instead\n`);
1140
+ }
1141
+ // Import and start LocalServ
1142
+ const { createLocalServ, getTestToken } = await import('./serv/local.js');
1143
+ const { serv, tenant, user } = createLocalServ({
1144
+ port: availablePort,
1145
+ baseUrl: `http://localhost:${availablePort}`,
1146
+ debug: options.debug,
1147
+ });
1148
+ // Get a test token
1149
+ const token = await getTestToken(serv, tenant, user);
1150
+ console.error(`
1151
+ ⚡ Photon Serve (Multi-tenant Development)
1152
+
1153
+ URL: http://localhost:${availablePort}
1154
+ Tenant: ${tenant.slug} (${tenant.name})
1155
+ User: ${user.email}
1156
+
1157
+ Test Token:
1158
+ ${token}
1159
+
1160
+ MCP Endpoint:
1161
+ http://localhost:${availablePort}/tenant/${tenant.slug}/mcp
1162
+
1163
+ Well-Known:
1164
+ http://localhost:${availablePort}/.well-known/oauth-protected-resource
1165
+
1166
+ Press Ctrl+C to stop
1167
+ `);
1168
+ // Simple HTTP server
1169
+ const http = await import('http');
1170
+ const server = http.createServer(async (req, res) => {
1171
+ const url = req.url || '/';
1172
+ const method = req.method || 'GET';
1173
+ const headers = {};
1174
+ for (const [key, value] of Object.entries(req.headers)) {
1175
+ if (typeof value === 'string')
1176
+ headers[key] = value;
1177
+ }
1178
+ // Read body if present
1179
+ let body = '';
1180
+ if (method === 'POST') {
1181
+ body = await new Promise((resolve) => {
1182
+ let data = '';
1183
+ req.on('data', (chunk) => (data += chunk));
1184
+ req.on('end', () => resolve(data));
1185
+ });
1186
+ }
1187
+ const result = await serv.handleRequest(method, url, headers, body);
1188
+ res.writeHead(result.status, result.headers);
1189
+ res.end(result.body);
1190
+ });
1191
+ server.listen(availablePort);
1192
+ // Handle shutdown
1193
+ const shutdown = async () => {
1194
+ console.error('\nShutting down Photon Serve...');
1195
+ await serv.shutdown();
1196
+ server.close();
1197
+ process.exit(0);
1198
+ };
1199
+ process.on('SIGINT', shutdown);
1200
+ process.on('SIGTERM', shutdown);
1201
+ }
1202
+ catch (error) {
1203
+ logger.error(`Error: ${getErrorMessage(error)}`);
1204
+ process.exit(1);
1205
+ }
1206
+ });
1207
+ // Host command: manage hosting and deployment (preview, deploy)
1208
+ const host = program
1209
+ .command('host', { hidden: true })
1210
+ .description('Manage cloud hosting and deployment');
1211
+ host
1212
+ .command('preview')
1213
+ .argument('<target>', 'Deployment target: cloudflare (or cf)')
1214
+ .argument('<name>', 'Photon name (without .photon.ts extension)')
1215
+ .option('--output <dir>', 'Output directory for generated project')
1216
+ .description('Run Photon locally in a simulated deployment environment')
1217
+ .action(async (target, name, options) => {
1218
+ try {
1219
+ // Get working directory from global options
1220
+ const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1221
+ // Resolve file path from name
1222
+ const photonPath = await resolvePhotonPath(name, workingDir);
1223
+ if (!photonPath) {
1224
+ logger.error(`Photon not found: ${name}`);
1225
+ console.error(`Searched in: ${workingDir}`);
1226
+ console.error(`Tip: Use 'photon info' to see available photons`);
1227
+ process.exit(1);
1228
+ }
1229
+ const normalizedTarget = target.toLowerCase();
1230
+ if (normalizedTarget === 'cloudflare' || normalizedTarget === 'cf') {
1231
+ const { devCloudflare } = await import('./deploy/cloudflare.js');
1232
+ await devCloudflare({
1233
+ photonPath,
1234
+ outputDir: options.output,
1235
+ });
1236
+ }
1237
+ else {
1238
+ logger.error(`Unknown target: ${target}`);
1239
+ console.error('Supported targets: cloudflare (cf)');
1240
+ process.exit(1);
1241
+ }
1242
+ }
1243
+ catch (error) {
1244
+ logger.error(`Error: ${getErrorMessage(error)}`);
1245
+ process.exit(1);
1246
+ }
1247
+ });
1248
+ host
1249
+ .command('deploy')
1250
+ .argument('<target>', 'Deployment target: cloudflare (or cf)')
1251
+ .argument('<name>', 'Photon name (without .photon.ts extension)')
1252
+ .option('--dev', 'Enable Beam UI in deployment')
1253
+ .option('--dry-run', 'Generate project without deploying')
1254
+ .option('--output <dir>', 'Output directory for generated project')
1255
+ .description('Deploy a Photon to cloud platforms')
1256
+ .action(async (target, name, options) => {
1257
+ try {
1258
+ // Get working directory from global options
1259
+ const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1260
+ // Resolve file path from name
1261
+ const photonPath = await resolvePhotonPath(name, workingDir);
1262
+ if (!photonPath) {
1263
+ logger.error(`Photon not found: ${name}`);
1264
+ console.error(`Searched in: ${workingDir}`);
1265
+ console.error(`Tip: Use 'photon info' to see available photons`);
1266
+ process.exit(1);
1267
+ }
1268
+ const normalizedTarget = target.toLowerCase();
1269
+ if (normalizedTarget === 'cloudflare' || normalizedTarget === 'cf') {
1270
+ const { deployToCloudflare } = await import('./deploy/cloudflare.js');
1271
+ await deployToCloudflare({
1272
+ photonPath,
1273
+ devMode: options.dev,
1274
+ dryRun: options.dryRun,
1275
+ outputDir: options.output,
1276
+ });
1277
+ }
1278
+ else {
1279
+ logger.error(`Unknown deployment target: ${target}`);
1280
+ console.error('Supported targets: cloudflare (cf)');
1281
+ process.exit(1);
1282
+ }
1283
+ }
1284
+ catch (error) {
1285
+ logger.error(`Deployment failed: ${getErrorMessage(error)}`);
1286
+ process.exit(1);
1287
+ }
1288
+ });
1289
+ // Search command: search for MCPs across marketplaces
1290
+ program
1291
+ .command('search', { hidden: true })
1292
+ .argument('<query>', 'MCP name or keyword to search for')
1293
+ .description('Search for MCP in all enabled marketplaces')
1294
+ .action(async (query) => {
1295
+ try {
1296
+ const { MarketplaceManager } = await import('./marketplace-manager.js');
1297
+ const { formatOutput, printInfo, printError } = await import('./cli-formatter.js');
1298
+ const manager = new MarketplaceManager();
1299
+ await manager.initialize();
1300
+ // Auto-update stale caches
1301
+ const updated = await manager.autoUpdateStaleCaches();
1302
+ if (updated) {
1303
+ printInfo('Refreshed marketplace data...\n');
915
1304
  }
916
1305
  printInfo(`Searching for '${query}' in marketplaces...`);
917
1306
  const results = await manager.search(query);
@@ -926,9 +1315,10 @@ program
926
1315
  for (const entry of entries) {
927
1316
  tableData.push({
928
1317
  name: mcpName,
929
- version: entry.metadata?.version || '-',
1318
+ version: entry.metadata?.version || PHOTON_VERSION,
930
1319
  description: entry.metadata?.description
931
- ? entry.metadata.description.substring(0, 50) + (entry.metadata.description.length > 50 ? '...' : '')
1320
+ ? entry.metadata.description.substring(0, 50) +
1321
+ (entry.metadata.description.length > 50 ? '...' : '')
932
1322
  : '-',
933
1323
  marketplace: entry.marketplace.name,
934
1324
  });
@@ -940,7 +1330,7 @@ program
940
1330
  }
941
1331
  catch (error) {
942
1332
  const { printError } = await import('./cli-formatter.js');
943
- printError(error.message);
1333
+ printError(getErrorMessage(error));
944
1334
  process.exit(1);
945
1335
  }
946
1336
  });
@@ -964,7 +1354,7 @@ maker
964
1354
  // Check if file already exists
965
1355
  try {
966
1356
  await fs.access(filePath);
967
- console.error(`❌ File already exists: ${filePath}`);
1357
+ logger.error(`File already exists: ${filePath}`);
968
1358
  process.exit(1);
969
1359
  }
970
1360
  catch {
@@ -984,18 +1374,16 @@ maker
984
1374
  // Convert kebab-case to PascalCase for class name
985
1375
  const className = name
986
1376
  .split(/[-_]/)
987
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
1377
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
988
1378
  .join('');
989
- const content = template
990
- .replace(/TemplateName/g, className)
991
- .replace(/template-name/g, name);
1379
+ const content = template.replace(/TemplateName/g, className).replace(/template-name/g, name);
992
1380
  // Write file
993
1381
  await fs.writeFile(filePath, content, 'utf-8');
994
1382
  console.error(`✅ Created ${fileName} in ${workingDir}`);
995
1383
  console.error(`Run with: photon mcp ${name} --dev`);
996
1384
  }
997
1385
  catch (error) {
998
- console.error(`❌ Error: ${error.message}`);
1386
+ logger.error(`Error: ${getErrorMessage(error)}`);
999
1387
  process.exit(1);
1000
1388
  }
1001
1389
  });
@@ -1011,10 +1399,11 @@ maker
1011
1399
  // Resolve file path from name in working directory
1012
1400
  const filePath = await resolvePhotonPath(name, workingDir);
1013
1401
  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);
1402
+ exitWithError(`Photon not found: ${name}`, {
1403
+ exitCode: ExitCode.NOT_FOUND,
1404
+ searchedIn: workingDir,
1405
+ suggestion: "Use 'photon info' to see available photons",
1406
+ });
1018
1407
  }
1019
1408
  console.error(`Validating ${path.basename(filePath)}...\n`);
1020
1409
  // Import loader and try to load
@@ -1030,7 +1419,7 @@ maker
1030
1419
  process.exit(0);
1031
1420
  }
1032
1421
  catch (error) {
1033
- console.error(`❌ Validation failed: ${error.message}`);
1422
+ logger.error(`Validation failed: ${getErrorMessage(error)}`);
1034
1423
  process.exit(1);
1035
1424
  }
1036
1425
  });
@@ -1057,8 +1446,8 @@ maker
1057
1446
  }
1058
1447
  }
1059
1448
  catch (error) {
1060
- console.error(`❌ Error: ${error.message}`);
1061
- if (process.env.DEBUG) {
1449
+ logger.error(`Error: ${getErrorMessage(error)}`);
1450
+ if (process.env.DEBUG && error instanceof Error) {
1062
1451
  console.error(error.stack);
1063
1452
  }
1064
1453
  process.exit(1);
@@ -1077,848 +1466,342 @@ maker
1077
1466
  await performMarketplaceInit(dirPath, options);
1078
1467
  }
1079
1468
  catch (error) {
1080
- console.error(`❌ Error: ${error.message}`);
1081
- if (process.env.DEBUG) {
1469
+ logger.error(`Error: ${getErrorMessage(error)}`);
1470
+ if (process.env.DEBUG && error instanceof Error) {
1082
1471
  console.error(error.stack);
1083
1472
  }
1084
1473
  process.exit(1);
1085
1474
  }
1086
1475
  });
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) => {
1476
+ // maker diagram: generate Mermaid diagram for a Photon
1477
+ maker
1478
+ .command('diagram <photon>')
1479
+ .option('--dir <path>', 'Directory containing photon (defaults to current directory)')
1480
+ .description('Generate Mermaid diagram for a Photon')
1481
+ .action(async (photonName, options) => {
1129
1482
  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`);
1483
+ const { PhotonDocExtractor } = await import('./photon-doc-extractor.js');
1484
+ // Resolve photon path
1485
+ const dirPath = options.dir || '.';
1486
+ let photonPath = photonName;
1487
+ // If not a path, look in the directory
1488
+ if (!photonName.includes('/') && !photonName.includes('\\')) {
1489
+ if (!photonName.endsWith('.photon.ts')) {
1490
+ photonName = `${photonName}.photon.ts`;
1143
1491
  }
1492
+ photonPath = path.resolve(dirPath, photonName);
1144
1493
  }
1145
1494
  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}`);
1168
- }
1169
- else {
1170
- console.error(`❌ Marketplace '${name}' not found`);
1171
- process.exit(1);
1172
- }
1173
- }
1174
- 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}`);
1495
+ photonPath = path.resolve(photonName);
1214
1496
  }
1215
- else {
1216
- console.error(`❌ Marketplace '${name}' not found`);
1497
+ if (!existsSync(photonPath)) {
1498
+ logger.error(`Photon not found: ${photonPath}`);
1217
1499
  process.exit(1);
1218
1500
  }
1501
+ const extractor = new PhotonDocExtractor(photonPath);
1502
+ const diagram = await extractor.generateDiagram();
1503
+ // Output just the diagram (can be piped or copied)
1504
+ console.log(diagram);
1219
1505
  }
1220
1506
  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}`);
1507
+ logger.error(`Error: ${getErrorMessage(error)}`);
1508
+ if (process.env.DEBUG && error instanceof Error) {
1509
+ console.error(error.stack);
1400
1510
  }
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
1511
  process.exit(1);
1408
1512
  }
1409
1513
  });
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) => {
1514
+ // maker diagrams: generate Mermaid diagrams for all Photons in a directory
1515
+ maker
1516
+ .command('diagrams')
1517
+ .option('--dir <path>', 'Directory to scan (defaults to current directory)')
1518
+ .description('Generate Mermaid diagrams for all Photons in a directory')
1519
+ .action(async (options) => {
1418
1520
  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`);
1521
+ const { PhotonDocExtractor } = await import('./photon-doc-extractor.js');
1522
+ const dirPath = path.resolve(options.dir || '.');
1523
+ const files = await fs.readdir(dirPath);
1524
+ const photonFiles = files.filter((f) => f.endsWith('.photon.ts'));
1525
+ if (photonFiles.length === 0) {
1526
+ console.error('No .photon.ts files found');
1427
1527
  process.exit(1);
1428
1528
  }
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...');
1529
+ console.error(`📦 Found ${photonFiles.length} photons\n`);
1530
+ for (const file of photonFiles) {
1531
+ const photonPath = path.join(dirPath, file);
1532
+ const name = file.replace('.photon.ts', '');
1632
1533
  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
- }
1534
+ const extractor = new PhotonDocExtractor(photonPath);
1535
+ const diagram = await extractor.generateDiagram();
1536
+ console.log(`## ${name}\n`);
1537
+ console.log('```mermaid');
1538
+ console.log(diagram);
1539
+ console.log('```\n');
1651
1540
  }
1652
- catch (error) {
1653
- if (error.code === 'ENOENT') {
1654
- printInfo(`No cache directory found`);
1655
- }
1656
- else {
1657
- throw error;
1658
- }
1541
+ catch (err) {
1542
+ console.error(`⚠️ Failed to generate diagram for ${name}: ${err.message}`);
1659
1543
  }
1660
1544
  }
1661
1545
  }
1662
1546
  catch (error) {
1663
- const { printError } = await import('./cli-formatter.js');
1664
- printError(error.message);
1547
+ logger.error(`Error: ${getErrorMessage(error)}`);
1548
+ if (process.env.DEBUG && error instanceof Error) {
1549
+ console.error(error.stack);
1550
+ }
1665
1551
  process.exit(1);
1666
1552
  }
1667
1553
  });
1554
+ // Register marketplace commands (list, add, remove, enable, disable)
1555
+ registerMarketplaceCommands(program);
1556
+ // Register info command
1557
+ registerInfoCommand(program, DEFAULT_WORKING_DIR);
1558
+ // Register package management commands
1559
+ registerPackageCommands(program, DEFAULT_WORKING_DIR);
1560
+ // Register package-app command (cross-platform PWA launchers)
1561
+ registerPackageAppCommand(program, DEFAULT_WORKING_DIR);
1668
1562
  // Doctor command: diagnose photon environment
1669
1563
  program
1670
- .command('doctor', { hidden: true })
1564
+ .command('doctor')
1671
1565
  .argument('[name]', 'Photon name to diagnose (checks environment if omitted)')
1672
- .description('Run diagnostics on photon environment and installations')
1566
+ .description('Run diagnostics on photon environment, ports, and configuration')
1567
+ .option('--port <number>', 'Port to check for availability', '3000')
1673
1568
  .action(async (name, options, command) => {
1674
1569
  try {
1675
- const { formatOutput, printInfo, printSuccess, printWarning, STATUS } = await import('./cli-formatter.js');
1570
+ const { formatOutput, printHeader, printInfo, printSuccess, printWarning, STATUS } = await import('./cli-formatter.js');
1676
1571
  const workingDir = command.parent?.opts().dir || DEFAULT_WORKING_DIR;
1677
- let issuesFound = 0;
1678
- printInfo('Running Photon diagnostics...\n');
1679
- // Build diagnostic data
1680
1572
  const diagnostics = {};
1681
- // Check Node version
1573
+ const suggestions = [];
1574
+ let issuesFound = 0;
1575
+ printHeader('Photon Doctor');
1576
+ printInfo('Running environment checks...\n');
1577
+ // Node runtime
1682
1578
  const nodeVersion = process.version;
1683
1579
  const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
1684
1580
  diagnostics['Node.js'] = {
1685
1581
  version: nodeVersion,
1686
1582
  status: majorVersion >= 18 ? STATUS.OK : STATUS.ERROR,
1687
- note: majorVersion >= 18 ? 'supported' : 'requires Node.js 18+',
1688
1583
  };
1689
- if (majorVersion < 18)
1584
+ if (majorVersion < 18) {
1690
1585
  issuesFound++;
1691
- // Check npm/npx
1586
+ suggestions.push('Upgrade to Node.js 18+ (https://nodejs.org).');
1587
+ }
1588
+ // npm availability
1692
1589
  try {
1693
1590
  const { execSync } = await import('child_process');
1694
1591
  const npmVersion = execSync('npm --version', { encoding: 'utf-8' }).trim();
1695
- diagnostics['Package Manager'] = {
1696
- npm: npmVersion,
1697
- status: STATUS.OK,
1698
- };
1592
+ diagnostics['Package manager'] = { npm: npmVersion, status: STATUS.OK };
1699
1593
  }
1700
1594
  catch {
1701
- diagnostics['Package Manager'] = {
1702
- npm: 'not found',
1703
- status: STATUS.ERROR,
1704
- };
1595
+ diagnostics['Package manager'] = { npm: 'not found', status: STATUS.ERROR };
1705
1596
  issuesFound++;
1597
+ suggestions.push('Install npm / npx so Photon can install dependencies.');
1706
1598
  }
1707
- // Check working directory
1599
+ // Working directory health
1708
1600
  try {
1709
1601
  await fs.access(workingDir);
1710
1602
  const stats = await fs.stat(workingDir);
1711
1603
  if (stats.isDirectory()) {
1712
1604
  const mcps = await listPhotonMCPs(workingDir);
1713
- diagnostics['Working Directory'] = {
1605
+ diagnostics['Working directory'] = {
1714
1606
  path: workingDir,
1715
1607
  status: STATUS.OK,
1716
1608
  photons: mcps.length,
1717
1609
  };
1718
1610
  }
1719
1611
  else {
1720
- diagnostics['Working Directory'] = {
1612
+ diagnostics['Working directory'] = {
1721
1613
  path: workingDir,
1722
1614
  status: STATUS.ERROR,
1723
- note: 'not a directory',
1615
+ note: 'Not a directory',
1724
1616
  };
1725
1617
  issuesFound++;
1618
+ suggestions.push(`Fix working directory: rm ${workingDir} && mkdir -p ${workingDir}`);
1726
1619
  }
1727
1620
  }
1728
1621
  catch {
1729
- diagnostics['Working Directory'] = {
1622
+ diagnostics['Working directory'] = {
1730
1623
  path: workingDir,
1731
- status: STATUS.UNKNOWN,
1732
- note: 'will be created on first use',
1624
+ status: STATUS.WARN,
1625
+ note: 'Will be created on first use',
1733
1626
  };
1734
1627
  }
1735
- // Check cache directory
1628
+ // Cache directory insight
1736
1629
  const cacheDir = path.join(os.homedir(), '.cache', 'photon-mcp', 'compiled');
1737
1630
  try {
1738
1631
  await fs.access(cacheDir);
1739
1632
  const files = await fs.readdir(cacheDir);
1740
- diagnostics['Cache Directory'] = {
1633
+ diagnostics['Cache directory'] = {
1741
1634
  path: cacheDir,
1742
1635
  status: STATUS.OK,
1743
1636
  cachedFiles: files.length,
1744
1637
  };
1745
1638
  }
1746
1639
  catch {
1747
- diagnostics['Cache Directory'] = {
1640
+ diagnostics['Cache directory'] = {
1748
1641
  path: cacheDir,
1749
1642
  status: STATUS.UNKNOWN,
1750
- note: 'will be created on first use',
1643
+ note: 'Created on demand',
1751
1644
  };
1752
1645
  }
1753
- // Check marketplaces and conflicts
1754
- let manager;
1646
+ // Port availability
1647
+ const port = parseInt(options.port, 10);
1648
+ const available = await isPortAvailable(port);
1649
+ diagnostics['Ports'] = {
1650
+ port,
1651
+ status: available ? STATUS.OK : STATUS.ERROR,
1652
+ note: available ? 'Available' : 'In use by another process',
1653
+ };
1654
+ if (!available) {
1655
+ issuesFound++;
1656
+ suggestions.push(`Port ${port} is busy. Run Photon with '--port ${port + 1}' or stop the conflicting service.`);
1657
+ }
1658
+ // Marketplace configuration
1755
1659
  try {
1756
1660
  const { MarketplaceManager } = await import('./marketplace-manager.js');
1757
- manager = new MarketplaceManager();
1661
+ const manager = new MarketplaceManager();
1758
1662
  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
- }
1663
+ const enabled = manager.getAll().filter((m) => m.enabled);
1664
+ if (enabled.length === 0) {
1665
+ diagnostics['Marketplaces'] = {
1666
+ status: STATUS.WARN,
1667
+ note: 'No marketplaces configured. Add one with: photon marketplace add portel-dev/photons',
1668
+ };
1669
+ suggestions.push('Add at least one marketplace so you can install community photons.');
1780
1670
  }
1781
1671
  else {
1672
+ const conflicts = await manager.detectAllConflicts();
1782
1673
  diagnostics['Marketplaces'] = {
1783
- enabled: 0,
1784
- status: STATUS.UNKNOWN,
1785
- note: 'Add one with: photon marketplace add portel-dev/photons',
1674
+ status: conflicts.size > 0 ? STATUS.WARN : STATUS.OK,
1675
+ enabled: enabled.map((m) => m.name),
1676
+ conflicts: conflicts.size,
1786
1677
  };
1678
+ if (conflicts.size > 0) {
1679
+ issuesFound++;
1680
+ suggestions.push('Resolve duplicate photons with: photon marketplace resolve');
1681
+ }
1787
1682
  }
1788
1683
  }
1789
1684
  catch (error) {
1790
- diagnostics['Marketplaces'] = {
1791
- status: STATUS.ERROR,
1792
- error: error.message,
1793
- };
1685
+ diagnostics['Marketplaces'] = { status: STATUS.ERROR, error: getErrorMessage(error) };
1686
+ suggestions.push('Marketplace config failed to load. Run photon marketplace list to debug.');
1794
1687
  issuesFound++;
1795
1688
  }
1796
- // Check specific photon if provided
1689
+ // Photon-specific checks
1797
1690
  if (name) {
1798
- const photonDiag = {};
1691
+ const photonSection = {};
1799
1692
  const filePath = await resolvePhotonPath(name, workingDir);
1800
1693
  if (!filePath) {
1801
- photonDiag['location'] = 'not found';
1802
- photonDiag['status'] = STATUS.ERROR;
1694
+ photonSection.status = STATUS.ERROR;
1695
+ photonSection.note = `Not installed in ${workingDir}`;
1696
+ suggestions.push(`Install ${name} with: photon add ${name}`);
1803
1697
  issuesFound++;
1804
1698
  }
1805
1699
  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++;
1700
+ photonSection.status = STATUS.OK;
1701
+ photonSection.path = filePath;
1702
+ const params = await extractConstructorParams(filePath);
1703
+ if (params.length > 0) {
1704
+ photonSection.environment = params.map((param) => {
1705
+ const envVar = toEnvVarName(name, param.name);
1706
+ const value = process.env[envVar];
1707
+ const ok = Boolean(value) || param.isOptional || param.hasDefault;
1708
+ if (!ok) {
1709
+ issuesFound++;
1710
+ suggestions.push(`Set ${envVar} for ${name} (e.g. export ${envVar}=value).`);
1711
+ }
1712
+ return {
1713
+ name: envVar,
1714
+ status: ok ? STATUS.OK : STATUS.ERROR,
1715
+ value: value
1716
+ ? 'configured'
1717
+ : param.hasDefault
1718
+ ? `default: ${formatDefaultValue(param.defaultValue)}`
1719
+ : 'missing',
1720
+ };
1721
+ });
1820
1722
  }
1821
- // Check cache
1822
1723
  const cachedFile = path.join(cacheDir, `${name}.js`);
1823
1724
  try {
1824
1725
  await fs.access(cachedFile);
1825
- photonDiag['cache'] = STATUS.OK;
1726
+ photonSection.cache = { status: STATUS.OK, note: 'Warm' };
1826
1727
  }
1827
1728
  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}`;
1729
+ photonSection.cache = {
1730
+ status: STATUS.WARN,
1731
+ note: 'Not compiled yet (first run will compile)',
1732
+ };
1863
1733
  }
1864
1734
  }
1865
- diagnostics[`Photon: ${name}`] = photonDiag;
1735
+ diagnostics[`Photon: ${name}`] = photonSection;
1866
1736
  }
1867
1737
  formatOutput(diagnostics, 'tree');
1868
- // Summary
1869
- console.log('');
1870
- if (issuesFound === 0) {
1871
- printSuccess('No issues found! Photon environment is healthy.');
1738
+ if (suggestions.length > 0) {
1739
+ printHeader('Suggested fixes');
1740
+ suggestions.forEach((tip, idx) => console.log(` ${idx + 1}. ${tip}`));
1741
+ }
1742
+ if (issuesFound === 0 && suggestions.length === 0) {
1743
+ printSuccess('\nAll checks passed!');
1872
1744
  }
1873
1745
  else {
1874
- printWarning(`Found ${issuesFound} issue(s). Please address the items marked with '${STATUS.ERROR}' above.`);
1875
- process.exit(1);
1746
+ printWarning(`\nDetected ${issuesFound || suggestions.length} potential issue(s).`);
1876
1747
  }
1877
1748
  }
1878
1749
  catch (error) {
1879
1750
  const { printError } = await import('./cli-formatter.js');
1880
- printError(error.message);
1751
+ printError(getErrorMessage(error));
1881
1752
  process.exit(1);
1882
1753
  }
1883
1754
  });
1884
1755
  // CLI command: directly invoke photon methods
1756
+ // Also serves as escape hatch for photons with reserved names (e.g., photon cli list get)
1885
1757
  program
1886
- .command('cli <photon> [method] [args...]', { hidden: true })
1887
- .description('Run photon methods directly from the command line')
1758
+ .command('cli <photon> [method] [args...]')
1759
+ .description('Run photon methods from command line (escape hatch for reserved names)')
1888
1760
  .allowUnknownOption()
1889
1761
  .helpOption(false) // Disable default help so we can handle it ourselves
1890
1762
  .action(async (photon, method, args) => {
1891
1763
  // Handle help flag
1892
1764
  if (photon === '--help' || photon === '-h') {
1893
1765
  console.log(`USAGE:
1894
- photon cli <photon-name> [method] [args...]
1766
+ photon <photon-name> [method] [args...]
1767
+ photon cli <photon-name> [method] [args...] (explicit form)
1895
1768
 
1896
1769
  DESCRIPTION:
1897
1770
  Run photon methods directly from the command line. Photons provide
1898
1771
  a CLI interface automatically based on their exported methods.
1899
1772
 
1773
+ The 'cli' command is optional - you can run photons directly:
1774
+ photon lg-remote volume +5 (implicit)
1775
+ photon cli lg-remote volume +5 (explicit)
1776
+
1777
+ Use 'photon cli' explicitly when your photon name conflicts with
1778
+ a reserved command (serve, beam, list, init, etc.)
1779
+
1900
1780
  EXAMPLES:
1901
1781
  # List all methods for a photon
1902
- photon cli lg-remote
1782
+ photon lg-remote
1903
1783
 
1904
1784
  # Call a method with no parameters
1905
- photon cli lg-remote status
1785
+ photon lg-remote status
1906
1786
 
1907
1787
  # 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
1788
+ photon lg-remote volume 50
1789
+ photon lg-remote volume +5
1790
+ photon spotify play
1911
1791
 
1912
1792
  # Get method-specific help
1913
- photon cli lg-remote volume --help
1793
+ photon lg-remote volume --help
1914
1794
 
1915
1795
  # Output raw JSON instead of formatted text
1916
- photon cli lg-remote status --json
1796
+ photon lg-remote status --json
1797
+
1798
+ # Escape hatch for reserved-name photons
1799
+ photon cli list get (photon named "list", method "get")
1800
+ photon cli serve status (photon named "serve", method "status")
1917
1801
 
1918
1802
  SEE ALSO:
1919
- photon info List all installed photons
1803
+ photon list List all installed photons
1920
1804
  photon add <name> Install a photon from marketplace
1921
- photon alias Create CLI shortcuts for photons
1922
1805
  `);
1923
1806
  return;
1924
1807
  }
@@ -1957,13 +1840,124 @@ program
1957
1840
  const { listAliases } = await import('./cli-alias.js');
1958
1841
  await listAliases();
1959
1842
  });
1843
+ // Test command: run tests for photons
1844
+ program
1845
+ .command('test')
1846
+ .argument('[photon]', 'Photon to test (tests all if omitted)')
1847
+ .argument('[test]', 'Specific test to run')
1848
+ .option('--json', 'Output results as JSON')
1849
+ .option('--mode <mode>', 'Test mode: direct (unit), cli (integration via CLI), mcp (integration via MCP), all', 'direct')
1850
+ .description('Run test methods in photons')
1851
+ .action(async (photon, test, options) => {
1852
+ try {
1853
+ const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1854
+ const { runTests } = await import('./test-runner.js');
1855
+ // Validate mode
1856
+ const validModes = ['direct', 'cli', 'mcp', 'all'];
1857
+ if (!validModes.includes(options.mode)) {
1858
+ logger.error(`Invalid mode: ${options.mode}. Valid modes: ${validModes.join(', ')}`);
1859
+ process.exit(1);
1860
+ }
1861
+ const summary = await runTests(workingDir, photon, test, {
1862
+ json: options.json,
1863
+ mode: options.mode,
1864
+ });
1865
+ // Exit with error code if any tests failed
1866
+ if (summary.failed > 0) {
1867
+ process.exit(1);
1868
+ }
1869
+ }
1870
+ catch (error) {
1871
+ logger.error(`Error: ${getErrorMessage(error)}`);
1872
+ process.exit(1);
1873
+ }
1874
+ });
1875
+ // Reserved commands that should NOT be treated as photon names
1876
+ // Reserved commands that should NOT be treated as photon names
1877
+ // If first arg is not in this list, it's assumed to be a photon name (implicit CLI mode)
1878
+ const RESERVED_COMMANDS = [
1879
+ // Core commands
1880
+ 'serve',
1881
+ 'sse',
1882
+ 'beam',
1883
+ 'list',
1884
+ 'ls',
1885
+ 'info',
1886
+ 'test',
1887
+ // Photon management
1888
+ 'new',
1889
+ 'init',
1890
+ 'validate',
1891
+ 'sync',
1892
+ 'add',
1893
+ 'remove',
1894
+ 'rm',
1895
+ // Maintenance
1896
+ 'upgrade',
1897
+ 'up',
1898
+ 'update',
1899
+ 'doctor',
1900
+ 'clear-cache',
1901
+ 'clean',
1902
+ // Aliases
1903
+ 'cli',
1904
+ 'alias',
1905
+ 'unalias',
1906
+ 'aliases',
1907
+ // Marketplace
1908
+ 'marketplace',
1909
+ // Packaging
1910
+ 'package',
1911
+ // Hidden/advanced
1912
+ 'mcp',
1913
+ 'search',
1914
+ 'maker',
1915
+ 'host',
1916
+ 'diagram',
1917
+ 'diagrams',
1918
+ 'enable',
1919
+ 'disable',
1920
+ // Help/version (handled by commander)
1921
+ 'help',
1922
+ '--help',
1923
+ '-h',
1924
+ 'version',
1925
+ '--version',
1926
+ '-V',
1927
+ ];
1960
1928
  // All known commands for "did you mean" suggestions
1961
1929
  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',
1930
+ 'serve',
1931
+ 'sse',
1932
+ 'beam',
1933
+ 'list',
1934
+ 'ls',
1935
+ 'info',
1936
+ 'test',
1937
+ 'new',
1938
+ 'init',
1939
+ 'validate',
1940
+ 'sync',
1941
+ 'add',
1942
+ 'remove',
1943
+ 'rm',
1944
+ 'upgrade',
1945
+ 'up',
1946
+ 'update',
1947
+ 'clear-cache',
1948
+ 'clean',
1949
+ 'doctor',
1950
+ 'cli',
1951
+ 'alias',
1952
+ 'unalias',
1953
+ 'aliases',
1954
+ 'mcp',
1955
+ 'search',
1956
+ 'marketplace',
1957
+ 'maker',
1958
+ 'host',
1959
+ 'diagram',
1960
+ 'diagrams',
1967
1961
  ];
1968
1962
  const knownSubcommands = {
1969
1963
  marketplace: ['list', 'add', 'remove', 'enable', 'disable'],
@@ -2019,7 +2013,7 @@ program.on('command:*', async (operands) => {
2019
2013
  printError(`Unknown command: ${unknownCommand}`);
2020
2014
  // Check if it's a subcommand typo for a known parent
2021
2015
  const args = process.argv.slice(2);
2022
- const parentIndex = args.findIndex(arg => knownSubcommands[arg]);
2016
+ const parentIndex = args.findIndex((arg) => knownSubcommands[arg]);
2023
2017
  if (parentIndex !== -1 && parentIndex < args.indexOf(unknownCommand)) {
2024
2018
  const parent = args[parentIndex];
2025
2019
  const suggestion = findClosestCommand(unknownCommand, knownSubcommands[parent]);
@@ -2038,7 +2032,48 @@ program.on('command:*', async (operands) => {
2038
2032
  printInfo(`Run 'photon --help' for usage`);
2039
2033
  process.exit(1);
2040
2034
  });
2041
- program.parse();
2035
+ // ══════════════════════════════════════════════════════════════════════════════
2036
+ // IMPLICIT CLI MODE
2037
+ // ══════════════════════════════════════════════════════════════════════════════
2038
+ // If the first argument is not a reserved command, treat it as a photon name
2039
+ // This enables: `photon lg-remote volume +5` instead of `photon cli lg-remote volume +5`
2040
+ function preprocessArgs() {
2041
+ const args = process.argv.slice(2);
2042
+ // No args - default to beam with auto-open browser
2043
+ if (args.length === 0) {
2044
+ return [...process.argv, 'beam', '--open'];
2045
+ }
2046
+ // Find the first non-flag argument (skip values of flags that take a parameter)
2047
+ const flagsWithValues = ['--dir', '--log-level'];
2048
+ const firstArgIndex = args.findIndex((arg, i) => {
2049
+ if (arg.startsWith('-'))
2050
+ return false;
2051
+ // Skip values of preceding flags (e.g., "." in "--dir .")
2052
+ if (i > 0 && flagsWithValues.includes(args[i - 1]))
2053
+ return false;
2054
+ return true;
2055
+ });
2056
+ if (firstArgIndex === -1) {
2057
+ // No subcommand — only flags present (e.g., --dir=. --log-level debug)
2058
+ // photon --help / -h / --version / -V → show program help/version
2059
+ if (args.some((a) => a === '--help' || a === '-h' || a === '--version' || a === '-V')) {
2060
+ return process.argv;
2061
+ }
2062
+ // Otherwise default to beam (e.g., photon --dir=. → photon --dir=. beam --open)
2063
+ return [...process.argv, 'beam', '--open'];
2064
+ }
2065
+ const firstArg = args[firstArgIndex];
2066
+ // If first arg is a reserved command, let commander handle normally
2067
+ if (RESERVED_COMMANDS.includes(firstArg)) {
2068
+ return process.argv;
2069
+ }
2070
+ // First arg looks like a photon name - inject 'cli' command
2071
+ // photon lg-remote volume +5 → photon cli lg-remote volume +5
2072
+ const newArgs = [...process.argv];
2073
+ newArgs.splice(2 + firstArgIndex, 0, 'cli');
2074
+ return newArgs;
2075
+ }
2076
+ program.parse(preprocessArgs());
2042
2077
  /**
2043
2078
  * Inline template fallback
2044
2079
  */