@portel/photon 1.9.0 → 1.10.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 (294) hide show
  1. package/README.md +163 -210
  2. package/dist/async/dedup-map.d.ts +40 -0
  3. package/dist/async/dedup-map.d.ts.map +1 -0
  4. package/dist/async/dedup-map.js +80 -0
  5. package/dist/async/dedup-map.js.map +1 -0
  6. package/dist/async/index.d.ts +11 -0
  7. package/dist/async/index.d.ts.map +1 -0
  8. package/dist/async/index.js +11 -0
  9. package/dist/async/index.js.map +1 -0
  10. package/dist/async/loading-gate.d.ts +27 -0
  11. package/dist/async/loading-gate.d.ts.map +1 -0
  12. package/dist/async/loading-gate.js +48 -0
  13. package/dist/async/loading-gate.js.map +1 -0
  14. package/dist/async/with-timeout.d.ts +6 -0
  15. package/dist/async/with-timeout.d.ts.map +1 -0
  16. package/dist/async/with-timeout.js +17 -0
  17. package/dist/async/with-timeout.js.map +1 -0
  18. package/dist/auto-ui/beam/class-metadata.d.ts +52 -0
  19. package/dist/auto-ui/beam/class-metadata.d.ts.map +1 -0
  20. package/dist/auto-ui/beam/class-metadata.js +133 -0
  21. package/dist/auto-ui/beam/class-metadata.js.map +1 -0
  22. package/dist/auto-ui/beam/config.d.ts +13 -0
  23. package/dist/auto-ui/beam/config.d.ts.map +1 -0
  24. package/dist/auto-ui/beam/config.js +52 -0
  25. package/dist/auto-ui/beam/config.js.map +1 -0
  26. package/dist/auto-ui/beam/external-mcp.d.ts +37 -0
  27. package/dist/auto-ui/beam/external-mcp.d.ts.map +1 -0
  28. package/dist/auto-ui/beam/external-mcp.js +311 -0
  29. package/dist/auto-ui/beam/external-mcp.js.map +1 -0
  30. package/dist/auto-ui/beam/photon-management.d.ts +51 -0
  31. package/dist/auto-ui/beam/photon-management.d.ts.map +1 -0
  32. package/dist/auto-ui/beam/photon-management.js +310 -0
  33. package/dist/auto-ui/beam/photon-management.js.map +1 -0
  34. package/dist/auto-ui/beam/routes/api-browse.d.ts +17 -0
  35. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -0
  36. package/dist/auto-ui/beam/routes/api-browse.js +531 -0
  37. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -0
  38. package/dist/auto-ui/beam/routes/api-config.d.ts +9 -0
  39. package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -0
  40. package/dist/auto-ui/beam/routes/api-config.js +494 -0
  41. package/dist/auto-ui/beam/routes/api-config.js.map +1 -0
  42. package/dist/auto-ui/beam/routes/api-marketplace.d.ts +8 -0
  43. package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -0
  44. package/dist/auto-ui/beam/routes/api-marketplace.js +490 -0
  45. package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -0
  46. package/dist/auto-ui/beam/startup.d.ts +41 -0
  47. package/dist/auto-ui/beam/startup.d.ts.map +1 -0
  48. package/dist/auto-ui/beam/startup.js +98 -0
  49. package/dist/auto-ui/beam/startup.js.map +1 -0
  50. package/dist/auto-ui/beam/subscription.d.ts +35 -0
  51. package/dist/auto-ui/beam/subscription.d.ts.map +1 -0
  52. package/dist/auto-ui/beam/subscription.js +151 -0
  53. package/dist/auto-ui/beam/subscription.js.map +1 -0
  54. package/dist/auto-ui/beam/types.d.ts +103 -0
  55. package/dist/auto-ui/beam/types.d.ts.map +1 -0
  56. package/dist/auto-ui/beam/types.js +8 -0
  57. package/dist/auto-ui/beam/types.js.map +1 -0
  58. package/dist/auto-ui/beam.d.ts +2 -0
  59. package/dist/auto-ui/beam.d.ts.map +1 -1
  60. package/dist/auto-ui/beam.js +729 -2596
  61. package/dist/auto-ui/beam.js.map +1 -1
  62. package/dist/auto-ui/bridge/index.d.ts.map +1 -1
  63. package/dist/auto-ui/bridge/index.js +10 -2
  64. package/dist/auto-ui/bridge/index.js.map +1 -1
  65. package/dist/auto-ui/components/card.d.ts.map +1 -1
  66. package/dist/auto-ui/components/card.js +3 -1
  67. package/dist/auto-ui/components/card.js.map +1 -1
  68. package/dist/auto-ui/components/progress.d.ts.map +1 -1
  69. package/dist/auto-ui/components/progress.js.map +1 -1
  70. package/dist/auto-ui/daemon-tools.d.ts +1 -1
  71. package/dist/auto-ui/daemon-tools.d.ts.map +1 -1
  72. package/dist/auto-ui/daemon-tools.js +4 -3
  73. package/dist/auto-ui/daemon-tools.js.map +1 -1
  74. package/dist/auto-ui/photon-bridge.d.ts +6 -2
  75. package/dist/auto-ui/photon-bridge.d.ts.map +1 -1
  76. package/dist/auto-ui/photon-bridge.js +20 -8
  77. package/dist/auto-ui/photon-bridge.js.map +1 -1
  78. package/dist/auto-ui/platform-compat.d.ts.map +1 -1
  79. package/dist/auto-ui/platform-compat.js +4 -0
  80. package/dist/auto-ui/platform-compat.js.map +1 -1
  81. package/dist/auto-ui/streamable-http-transport.d.ts +4 -2
  82. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  83. package/dist/auto-ui/streamable-http-transport.js +120 -30
  84. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  85. package/dist/auto-ui/types.d.ts +4 -2
  86. package/dist/auto-ui/types.d.ts.map +1 -1
  87. package/dist/auto-ui/types.js.map +1 -1
  88. package/dist/beam.bundle.js +8225 -3999
  89. package/dist/beam.bundle.js.map +4 -4
  90. package/dist/cli/commands/alias.d.ts +14 -0
  91. package/dist/cli/commands/alias.d.ts.map +1 -0
  92. package/dist/cli/commands/alias.js +41 -0
  93. package/dist/cli/commands/alias.js.map +1 -0
  94. package/dist/cli/commands/audit.d.ts +9 -0
  95. package/dist/cli/commands/audit.d.ts.map +1 -0
  96. package/dist/cli/commands/audit.js +377 -0
  97. package/dist/cli/commands/audit.js.map +1 -0
  98. package/dist/cli/commands/beam.d.ts +20 -0
  99. package/dist/cli/commands/beam.d.ts.map +1 -0
  100. package/dist/cli/commands/beam.js +256 -0
  101. package/dist/cli/commands/beam.js.map +1 -0
  102. package/dist/cli/commands/config.d.ts +14 -0
  103. package/dist/cli/commands/config.d.ts.map +1 -0
  104. package/dist/cli/commands/config.js +165 -0
  105. package/dist/cli/commands/config.js.map +1 -0
  106. package/dist/cli/commands/daemon.d.ts +11 -0
  107. package/dist/cli/commands/daemon.d.ts.map +1 -0
  108. package/dist/cli/commands/daemon.js +108 -0
  109. package/dist/cli/commands/daemon.js.map +1 -0
  110. package/dist/cli/commands/doctor.d.ts +14 -0
  111. package/dist/cli/commands/doctor.d.ts.map +1 -0
  112. package/dist/cli/commands/doctor.js +257 -0
  113. package/dist/cli/commands/doctor.js.map +1 -0
  114. package/dist/cli/commands/host.d.ts +11 -0
  115. package/dist/cli/commands/host.d.ts.map +1 -0
  116. package/dist/cli/commands/host.js +96 -0
  117. package/dist/cli/commands/host.js.map +1 -0
  118. package/dist/cli/commands/info.d.ts +1 -1
  119. package/dist/cli/commands/info.d.ts.map +1 -1
  120. package/dist/cli/commands/info.js +16 -15
  121. package/dist/cli/commands/info.js.map +1 -1
  122. package/dist/cli/commands/init.d.ts +20 -0
  123. package/dist/cli/commands/init.d.ts.map +1 -0
  124. package/dist/cli/commands/init.js +774 -0
  125. package/dist/cli/commands/init.js.map +1 -0
  126. package/dist/cli/commands/maker.d.ts +12 -0
  127. package/dist/cli/commands/maker.d.ts.map +1 -0
  128. package/dist/cli/commands/maker.js +605 -0
  129. package/dist/cli/commands/maker.js.map +1 -0
  130. package/dist/cli/commands/mcp.d.ts +27 -0
  131. package/dist/cli/commands/mcp.d.ts.map +1 -0
  132. package/dist/cli/commands/mcp.js +390 -0
  133. package/dist/cli/commands/mcp.js.map +1 -0
  134. package/dist/cli/commands/package-app.d.ts +1 -1
  135. package/dist/cli/commands/package-app.d.ts.map +1 -1
  136. package/dist/cli/commands/package-app.js +5 -4
  137. package/dist/cli/commands/package-app.js.map +1 -1
  138. package/dist/cli/commands/package.d.ts +1 -1
  139. package/dist/cli/commands/package.d.ts.map +1 -1
  140. package/dist/cli/commands/package.js +134 -32
  141. package/dist/cli/commands/package.js.map +1 -1
  142. package/dist/cli/commands/run.d.ts +34 -0
  143. package/dist/cli/commands/run.d.ts.map +1 -0
  144. package/dist/cli/commands/run.js +334 -0
  145. package/dist/cli/commands/run.js.map +1 -0
  146. package/dist/cli/commands/search.d.ts +11 -0
  147. package/dist/cli/commands/search.d.ts.map +1 -0
  148. package/dist/cli/commands/search.js +60 -0
  149. package/dist/cli/commands/search.js.map +1 -0
  150. package/dist/cli/commands/serve.d.ts +11 -0
  151. package/dist/cli/commands/serve.d.ts.map +1 -0
  152. package/dist/cli/commands/serve.js +138 -0
  153. package/dist/cli/commands/serve.js.map +1 -0
  154. package/dist/cli/commands/test.d.ts +14 -0
  155. package/dist/cli/commands/test.d.ts.map +1 -0
  156. package/dist/cli/commands/test.js +51 -0
  157. package/dist/cli/commands/test.js.map +1 -0
  158. package/dist/cli/commands/update.d.ts +11 -0
  159. package/dist/cli/commands/update.d.ts.map +1 -0
  160. package/dist/cli/commands/update.js +72 -0
  161. package/dist/cli/commands/update.js.map +1 -0
  162. package/dist/cli/index.d.ts +14 -0
  163. package/dist/cli/index.d.ts.map +1 -0
  164. package/dist/cli/index.js +139 -0
  165. package/dist/cli/index.js.map +1 -0
  166. package/dist/cli-alias.js +2 -2
  167. package/dist/cli-alias.js.map +1 -1
  168. package/dist/cli.d.ts +3 -16
  169. package/dist/cli.d.ts.map +1 -1
  170. package/dist/cli.js +4 -2725
  171. package/dist/cli.js.map +1 -1
  172. package/dist/context-store.d.ts +13 -12
  173. package/dist/context-store.d.ts.map +1 -1
  174. package/dist/context-store.js +47 -23
  175. package/dist/context-store.js.map +1 -1
  176. package/dist/context.d.ts +35 -0
  177. package/dist/context.d.ts.map +1 -0
  178. package/dist/context.js +38 -0
  179. package/dist/context.js.map +1 -0
  180. package/dist/daemon/client.d.ts +25 -13
  181. package/dist/daemon/client.d.ts.map +1 -1
  182. package/dist/daemon/client.js +183 -135
  183. package/dist/daemon/client.js.map +1 -1
  184. package/dist/daemon/manager.d.ts +58 -26
  185. package/dist/daemon/manager.d.ts.map +1 -1
  186. package/dist/daemon/manager.js +348 -157
  187. package/dist/daemon/manager.js.map +1 -1
  188. package/dist/daemon/protocol.d.ts +9 -3
  189. package/dist/daemon/protocol.d.ts.map +1 -1
  190. package/dist/daemon/protocol.js +2 -0
  191. package/dist/daemon/protocol.js.map +1 -1
  192. package/dist/daemon/server.js +850 -200
  193. package/dist/daemon/server.js.map +1 -1
  194. package/dist/daemon/session-manager.d.ts +16 -2
  195. package/dist/daemon/session-manager.d.ts.map +1 -1
  196. package/dist/daemon/session-manager.js +65 -7
  197. package/dist/daemon/session-manager.js.map +1 -1
  198. package/dist/daemon/state-machine.d.ts +22 -0
  199. package/dist/daemon/state-machine.d.ts.map +1 -0
  200. package/dist/daemon/state-machine.js +48 -0
  201. package/dist/daemon/state-machine.js.map +1 -0
  202. package/dist/deploy/cloudflare.d.ts.map +1 -1
  203. package/dist/deploy/cloudflare.js +5 -5
  204. package/dist/deploy/cloudflare.js.map +1 -1
  205. package/dist/loader.d.ts +65 -7
  206. package/dist/loader.d.ts.map +1 -1
  207. package/dist/loader.js +587 -63
  208. package/dist/loader.js.map +1 -1
  209. package/dist/marketplace-manager.d.ts +84 -12
  210. package/dist/marketplace-manager.d.ts.map +1 -1
  211. package/dist/marketplace-manager.js +470 -26
  212. package/dist/marketplace-manager.js.map +1 -1
  213. package/dist/path-resolver.d.ts +3 -1
  214. package/dist/path-resolver.d.ts.map +1 -1
  215. package/dist/path-resolver.js +4 -3
  216. package/dist/path-resolver.js.map +1 -1
  217. package/dist/photon-cli-runner.d.ts +1 -1
  218. package/dist/photon-cli-runner.d.ts.map +1 -1
  219. package/dist/photon-cli-runner.js +34 -44
  220. package/dist/photon-cli-runner.js.map +1 -1
  221. package/dist/photon-doc-extractor.d.ts +1 -0
  222. package/dist/photon-doc-extractor.d.ts.map +1 -1
  223. package/dist/photon-doc-extractor.js +33 -12
  224. package/dist/photon-doc-extractor.js.map +1 -1
  225. package/dist/photons/maker.photon.d.ts.map +1 -1
  226. package/dist/photons/maker.photon.js +4 -4
  227. package/dist/photons/maker.photon.js.map +1 -1
  228. package/dist/photons/maker.photon.ts +4 -3
  229. package/dist/photons/marketplace.photon.d.ts.map +1 -1
  230. package/dist/photons/marketplace.photon.js +10 -27
  231. package/dist/photons/marketplace.photon.js.map +1 -1
  232. package/dist/photons/marketplace.photon.ts +14 -33
  233. package/dist/photons/tunnel.photon.d.ts.map +1 -1
  234. package/dist/photons/tunnel.photon.js +4 -8
  235. package/dist/photons/tunnel.photon.js.map +1 -1
  236. package/dist/photons/tunnel.photon.ts +4 -7
  237. package/dist/serv/session/kv-store.d.ts +1 -1
  238. package/dist/serv/session/kv-store.d.ts.map +1 -1
  239. package/dist/serv/session/store.d.ts.map +1 -1
  240. package/dist/serv/session/store.js +16 -14
  241. package/dist/serv/session/store.js.map +1 -1
  242. package/dist/serv/vault/token-vault.js +1 -1
  243. package/dist/serv/vault/token-vault.js.map +1 -1
  244. package/dist/server.d.ts +34 -12
  245. package/dist/server.d.ts.map +1 -1
  246. package/dist/server.js +364 -313
  247. package/dist/server.js.map +1 -1
  248. package/dist/shared/audit.d.ts +30 -0
  249. package/dist/shared/audit.d.ts.map +1 -0
  250. package/dist/shared/audit.js +89 -0
  251. package/dist/shared/audit.js.map +1 -0
  252. package/dist/shared/cli-sections.d.ts +0 -4
  253. package/dist/shared/cli-sections.d.ts.map +1 -1
  254. package/dist/shared/cli-sections.js +0 -6
  255. package/dist/shared/cli-sections.js.map +1 -1
  256. package/dist/shared/cli-utils.d.ts +2 -56
  257. package/dist/shared/cli-utils.d.ts.map +1 -1
  258. package/dist/shared/cli-utils.js +1 -87
  259. package/dist/shared/cli-utils.js.map +1 -1
  260. package/dist/shared/error-handler.d.ts +6 -72
  261. package/dist/shared/error-handler.d.ts.map +1 -1
  262. package/dist/shared/error-handler.js +22 -213
  263. package/dist/shared/error-handler.js.map +1 -1
  264. package/dist/shared/security.d.ts +0 -9
  265. package/dist/shared/security.d.ts.map +1 -1
  266. package/dist/shared/security.js +0 -30
  267. package/dist/shared/security.js.map +1 -1
  268. package/dist/shared-utils.d.ts +0 -26
  269. package/dist/shared-utils.d.ts.map +1 -1
  270. package/dist/shared-utils.js +0 -44
  271. package/dist/shared-utils.js.map +1 -1
  272. package/dist/shell-completions.d.ts +1 -1
  273. package/dist/shell-completions.d.ts.map +1 -1
  274. package/dist/shell-completions.js +5 -5
  275. package/dist/shell-completions.js.map +1 -1
  276. package/dist/template-manager.d.ts.map +1 -1
  277. package/dist/template-manager.js +14 -1
  278. package/dist/template-manager.js.map +1 -1
  279. package/dist/test-runner.d.ts +0 -12
  280. package/dist/test-runner.d.ts.map +1 -1
  281. package/dist/test-runner.js +4 -39
  282. package/dist/test-runner.js.map +1 -1
  283. package/dist/testing.d.ts +1 -1
  284. package/dist/testing.d.ts.map +1 -1
  285. package/dist/testing.js +2 -2
  286. package/dist/testing.js.map +1 -1
  287. package/dist/version-checker.d.ts +4 -4
  288. package/dist/version-checker.d.ts.map +1 -1
  289. package/dist/version-checker.js +33 -4
  290. package/dist/version-checker.js.map +1 -1
  291. package/dist/watcher.d.ts.map +1 -1
  292. package/dist/watcher.js +14 -12
  293. package/dist/watcher.js.map +1 -1
  294. package/package.json +24 -17
package/dist/cli.js CHANGED
@@ -2,2730 +2,9 @@
2
2
  /**
3
3
  * Photon MCP CLI
4
4
  *
5
- * Command-line interface for running .photon.ts files as MCP servers
5
+ * Thin entry point delegates to cli/index.ts which registers
6
+ * all command modules and handles argv preprocessing.
6
7
  */
7
- import { Command } from 'commander';
8
- import * as path from 'path';
9
- import * as fs from 'fs/promises';
10
- import { existsSync } from 'fs';
11
- import * as os from 'os';
12
- import * as net from 'net';
13
- import { PhotonServer } from './server.js';
14
- import { FileWatcher } from './watcher.js';
15
- import { resolvePhotonPath, listPhotonMCPs, ensureWorkingDir, DEFAULT_WORKING_DIR, } from './path-resolver.js';
16
- import { SchemaExtractor } from '@portel/photon-core';
17
- import { createRequire } from 'module';
18
- import { fileURLToPath } from 'url';
19
- const require = createRequire(import.meta.url);
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, isNodeError, } from './shared/error-handler.js';
28
- import { validateOrThrow, inRange, isPositive, isInteger } from './shared/validation.js';
29
- import { createReadline, promptText, 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
- import { validateAssetPath, isPathWithin } from './shared/security.js';
35
- // ══════════════════════════════════════════════════════════════════════════════
36
- // BUNDLED PHOTONS
37
- // ══════════════════════════════════════════════════════════════════════════════
38
- /** Bundled photon names that ship with the runtime */
39
- // BUNDLED_PHOTONS and getBundledPhotonPath are imported from shared-utils.js
40
- /**
41
- * Parse extended photon name format
42
- *
43
- * Supports:
44
- * - "rss-feed" → { name: "rss-feed" }
45
- * - "alice/custom-photons:rss-feed" → { name: "rss-feed", marketplaceSource: "alice/custom-photons" }
46
- *
47
- * Rule: colon splits only when left side contains `/` (a marketplace source)
48
- * and right side is a simple name (no `/`).
49
- */
50
- export function parsePhotonSpec(spec) {
51
- const colonIndex = spec.indexOf(':');
52
- if (colonIndex > 0) {
53
- const left = spec.slice(0, colonIndex);
54
- const right = spec.slice(colonIndex + 1);
55
- // Left must contain `/` (marketplace source) and right must be a simple name
56
- if (left.includes('/') && right && !right.includes('/')) {
57
- return { name: right, marketplaceSource: left };
58
- }
59
- }
60
- return { name: spec };
61
- }
62
- /**
63
- * Resolve photon path - checks bundled first, then user directory
64
- */
65
- async function resolvePhotonPathWithBundled(name, workingDir) {
66
- // Check bundled photons first
67
- const bundledPath = getBundledPhotonPath(name, __dirname);
68
- if (bundledPath) {
69
- return bundledPath;
70
- }
71
- // Fall back to user photons
72
- return resolvePhotonPath(name, workingDir);
73
- }
74
- // ══════════════════════════════════════════════════════════════════════════════
75
- // PORT UTILITIES
76
- // ══════════════════════════════════════════════════════════════════════════════
77
- /**
78
- * Check if a port is available
79
- */
80
- function isPortAvailable(port) {
81
- return new Promise((resolve) => {
82
- const server = net.createServer();
83
- server.once('error', () => resolve(false));
84
- server.once('listening', () => {
85
- server.close();
86
- resolve(true);
87
- });
88
- // Listen on all interfaces (same as http.createServer default)
89
- server.listen(port);
90
- });
91
- }
92
- function getLogOptionsFromCommand(command) {
93
- const root = command?.parent?.opts?.() ?? program.opts();
94
- try {
95
- const level = normalizeLogLevel(root.logLevel);
96
- return {
97
- level,
98
- json: Boolean(root.jsonLogs),
99
- };
100
- }
101
- catch (error) {
102
- handleError(error, { exitOnError: true });
103
- throw error; // TypeScript doesn't know handleError exits
104
- }
105
- }
106
- /**
107
- * Find an available port starting from the given port
108
- */
109
- async function findAvailablePort(startPort, maxAttempts = 10) {
110
- // Validate port range
111
- validateOrThrow(startPort, [
112
- inRange('start port', 1, 65535),
113
- isInteger('start port'),
114
- isPositive('start port'),
115
- ]);
116
- for (let i = 0; i < maxAttempts; i++) {
117
- const port = startPort + i;
118
- if (port > 65535) {
119
- throw new Error(`Port ${port} exceeds maximum port number (65535)`);
120
- }
121
- if (await isPortAvailable(port)) {
122
- return port;
123
- }
124
- }
125
- throw new Error(`No available port found between ${startPort} and ${startPort + maxAttempts - 1}`);
126
- }
127
- function cliHeading(title) {
128
- console.log('');
129
- printHeader(title);
130
- }
131
- function cliListItem(text) {
132
- printInfo(` ${text}`);
133
- }
134
- function cliSpacer() {
135
- console.log('');
136
- }
137
- function cliHint(message) {
138
- printWarning(message);
139
- }
140
- // ══════════════════════════════════════════════════════════════════════════════
141
- // ELICITATION HANDLERS
142
- // ══════════════════════════════════════════════════════════════════════════════
143
- /**
144
- * Handle form-based elicitation (MCP-aligned)
145
- * Renders a multi-field form in CLI using readline
146
- */
147
- async function handleFormElicitation(ask) {
148
- cliHeading(`📝 ${ask.message}`);
149
- cliHint('Press Enter to accept defaults. Fields marked * are required.');
150
- cliSpacer();
151
- const rl = createReadline();
152
- const question = (prompt) => {
153
- return new Promise((resolve) => {
154
- rl.question(prompt, (answer) => resolve(answer));
155
- });
156
- };
157
- const result = {};
158
- const required = ask.schema.required || [];
159
- for (const [key, prop] of Object.entries(ask.schema.properties)) {
160
- const title = prop.title || key;
161
- const isRequired = required.includes(key);
162
- const reqMark = isRequired ? '*' : '';
163
- const defaultVal = prop.default !== undefined ? ` [${prop.default}]` : '';
164
- let value;
165
- // Handle different property types
166
- if (prop.type === 'boolean') {
167
- const answer = await question(`${title}${reqMark} (y/n)${defaultVal}: `);
168
- if (answer === '' && prop.default !== undefined) {
169
- value = prop.default;
170
- }
171
- else {
172
- value = answer.toLowerCase().startsWith('y');
173
- }
174
- }
175
- else if (prop.enum || prop.oneOf) {
176
- // Single select
177
- const options = prop.oneOf
178
- ? prop.oneOf.map((o) => ({ value: o.const, label: o.title }))
179
- : prop.enum.map((e) => ({ value: e, label: e }));
180
- printInfo(`${title}${reqMark}:`);
181
- options.forEach((opt, i) => {
182
- const isDefault = opt.value === prop.default ? ' (default)' : '';
183
- cliListItem(`${i + 1}. ${opt.label}${isDefault}`);
184
- });
185
- const answer = await question(`Choose (1-${options.length})${defaultVal}: `);
186
- const idx = parseInt(answer) - 1;
187
- if (idx >= 0 && idx < options.length) {
188
- value = options[idx].value;
189
- }
190
- else if (answer === '' && prop.default !== undefined) {
191
- value = prop.default;
192
- }
193
- else {
194
- value = options[0].value;
195
- }
196
- }
197
- else if (prop.type === 'array') {
198
- // Multi-select
199
- const items = prop.items?.anyOf || prop.items?.enum?.map((e) => ({ const: e, title: e }));
200
- if (items) {
201
- printInfo(`${title}${reqMark} (comma-separated numbers):`);
202
- items.forEach((item, i) => {
203
- const label = item.title || item.const || item;
204
- cliListItem(`${i + 1}. ${label}`);
205
- });
206
- const answer = await question('Choose: ');
207
- const indices = answer.split(',').map((s) => parseInt(s.trim()) - 1);
208
- value = indices
209
- .filter((idx) => idx >= 0 && idx < items.length)
210
- .map((idx) => items[idx].const || items[idx]);
211
- if (value.length === 0 && prop.default) {
212
- value = prop.default;
213
- }
214
- }
215
- else {
216
- value = prop.default || [];
217
- }
218
- }
219
- else if (prop.type === 'number' || prop.type === 'integer') {
220
- const answer = await question(`${title}${reqMark}${defaultVal}: `);
221
- if (answer === '' && prop.default !== undefined) {
222
- value = prop.default;
223
- }
224
- else {
225
- value = prop.type === 'integer' ? parseInt(answer) : parseFloat(answer);
226
- if (isNaN(value))
227
- value = prop.default ?? 0;
228
- }
229
- }
230
- else {
231
- // String or default
232
- const format = prop.format ? ` (${prop.format})` : '';
233
- const answer = await question(`${title}${reqMark}${format}${defaultVal}: `);
234
- value = answer || prop.default || '';
235
- }
236
- result[key] = value;
237
- cliSpacer();
238
- }
239
- rl.close();
240
- cliSpacer();
241
- return { action: 'accept', content: result };
242
- }
243
- /**
244
- * Handle URL-based elicitation (OAuth flows)
245
- * Opens URL in browser and waits for user confirmation
246
- */
247
- async function handleUrlElicitation(ask) {
248
- cliHeading(`🔗 ${ask.message}`);
249
- printInfo(`URL: ${ask.url}`);
250
- cliHint('Opening your default browser...');
251
- cliSpacer();
252
- // Open URL in default browser
253
- const platform = process.platform;
254
- const openCommand = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open';
255
- try {
256
- // Security: validate URL and use execFile to prevent shell injection
257
- new URL(ask.url); // throws on invalid URL
258
- const { execFile } = await import('child_process');
259
- execFile(openCommand, [ask.url]);
260
- }
261
- catch (error) {
262
- const msg = isNodeError(error, 'ENOENT')
263
- ? `Could not find '${openCommand}' to open URLs`
264
- : getErrorMessage(error);
265
- cliHint(`Could not open browser: ${msg}. Please open the URL manually.`);
266
- }
267
- const shouldContinue = await promptWait('Press Enter when done', true);
268
- return { action: shouldContinue ? 'accept' : 'cancel' };
269
- }
270
- /**
271
- * Handle select elicitation with options
272
- */
273
- async function handleSelectElicitation(ask) {
274
- cliHeading(ask.message);
275
- const options = ask.options.map((opt) => typeof opt === 'string' ? { value: opt, label: opt } : opt);
276
- options.forEach((opt, i) => {
277
- const isDefault = ask.default === opt.value || (Array.isArray(ask.default) && ask.default.includes(opt.value));
278
- const defaultMark = isDefault ? ' ✓' : '';
279
- const desc = opt.description ? ` - ${opt.description}` : '';
280
- cliListItem(`${i + 1}. ${opt.label}${desc}${defaultMark}`);
281
- });
282
- cliSpacer();
283
- const rl = createReadline();
284
- const prompt = ask.multi
285
- ? `Choose (comma-separated, 1-${options.length}): `
286
- : `Choose (1-${options.length}): `;
287
- return new Promise((resolve) => {
288
- rl.question(prompt, (answer) => {
289
- rl.close();
290
- if (ask.multi) {
291
- if (answer.trim() === '') {
292
- resolve(Array.isArray(ask.default) ? ask.default : []);
293
- }
294
- else {
295
- const indices = answer.split(',').map((s) => parseInt(s.trim()) - 1);
296
- const values = indices
297
- .filter((idx) => idx >= 0 && idx < options.length)
298
- .map((idx) => options[idx].value);
299
- resolve(values);
300
- }
301
- }
302
- else {
303
- const idx = parseInt(answer) - 1;
304
- if (idx >= 0 && idx < options.length) {
305
- resolve(options[idx].value);
306
- }
307
- else if (answer.trim() === '' && ask.default) {
308
- resolve(ask.default);
309
- }
310
- else {
311
- resolve(options[0].value);
312
- }
313
- }
314
- });
315
- });
316
- }
317
- /**
318
- * Extract constructor parameters from a Photon MCP file
319
- */
320
- async function extractConstructorParams(filePath) {
321
- try {
322
- const source = await fs.readFile(filePath, 'utf-8');
323
- const extractor = new SchemaExtractor();
324
- return extractor.extractConstructorParams(source);
325
- }
326
- catch (error) {
327
- printError(`Failed to extract constructor params: ${getErrorMessage(error)}`);
328
- return [];
329
- }
330
- }
331
- /**
332
- * Ensure .gitignore includes marketplace template directory
333
- */
334
- async function ensureGitignore(workingDir) {
335
- const gitignorePath = path.join(workingDir, '.gitignore');
336
- const templatesPattern = '.marketplace/_templates/';
337
- try {
338
- let gitignoreContent = '';
339
- if (existsSync(gitignorePath)) {
340
- gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
341
- }
342
- // Check if pattern already exists
343
- if (gitignoreContent.includes(templatesPattern)) {
344
- return; // Already configured
345
- }
346
- // Add templates pattern to .gitignore
347
- const newContent = gitignoreContent.endsWith('\n')
348
- ? gitignoreContent + templatesPattern + '\n'
349
- : gitignoreContent + '\n' + templatesPattern + '\n';
350
- await fs.writeFile(gitignorePath, newContent, 'utf-8');
351
- console.error(' ✓ Added .marketplace/_templates/ to .gitignore');
352
- }
353
- catch (error) {
354
- // Non-fatal - just warn
355
- console.error(` ⚠ Could not update .gitignore: ${getErrorMessage(error)}`);
356
- }
357
- }
358
- /**
359
- * Perform marketplace sync - generates documentation files
360
- */
361
- async function performMarketplaceSync(dirPath, options) {
362
- const resolvedPath = path.resolve(dirPath);
363
- const isDefaultDir = resolvedPath === DEFAULT_WORKING_DIR;
364
- if (!existsSync(resolvedPath)) {
365
- exitWithError(`Directory not found: ${resolvedPath}`, {
366
- exitCode: ExitCode.NOT_FOUND,
367
- suggestion: 'Check the path and ensure the directory exists',
368
- });
369
- }
370
- // Scan for .photon.ts files
371
- console.error('📦 Scanning for .photon.ts files...');
372
- const files = await fs.readdir(resolvedPath);
373
- let photonFiles = files.filter((f) => f.endsWith('.photon.ts'));
374
- // Filter out installed photons if requested (for ~/.photon)
375
- if (options.filterInstalled && isDefaultDir) {
376
- const { readLocalMetadata } = await import('./marketplace-manager.js');
377
- const metadata = await readLocalMetadata();
378
- // Metadata keys may include .photon.ts extension
379
- const installedNames = new Set(Object.keys(metadata.photons || {}).map((k) => k.replace(/\.photon\.ts$/, '')));
380
- const originalCount = photonFiles.length;
381
- photonFiles = photonFiles.filter((f) => {
382
- const name = f.replace(/\.photon\.ts$/, '');
383
- return !installedNames.has(name);
384
- });
385
- if (originalCount !== photonFiles.length) {
386
- console.error(` Filtered out ${originalCount - photonFiles.length} installed photons`);
387
- }
388
- }
389
- if (photonFiles.length === 0) {
390
- exitWithError(`No .photon.ts files found`, {
391
- exitCode: ExitCode.NOT_FOUND,
392
- searchedIn: resolvedPath,
393
- suggestion: "Create a .photon.ts file or use 'photon maker new' to generate one",
394
- });
395
- }
396
- console.error(` Found ${photonFiles.length} photons\n`);
397
- // Initialize template manager
398
- const { TemplateManager } = await import('./template-manager.js');
399
- const templateMgr = new TemplateManager(resolvedPath);
400
- console.error('📝 Ensuring templates...');
401
- await templateMgr.ensureTemplates();
402
- // Ensure .gitignore excludes templates
403
- await ensureGitignore(resolvedPath);
404
- console.error('');
405
- // Extract metadata from each Photon
406
- console.error('📄 Extracting documentation...');
407
- const { calculateFileHash } = await import('./marketplace-manager.js');
408
- const { PhotonDocExtractor } = await import('./photon-doc-extractor.js');
409
- const photons = [];
410
- for (const file of photonFiles.sort()) {
411
- const filePath = path.join(resolvedPath, file);
412
- // Extract full metadata
413
- const extractor = new PhotonDocExtractor(filePath);
414
- const metadata = await extractor.extractFullMetadata();
415
- // Calculate hash
416
- const hash = await calculateFileHash(filePath);
417
- console.error(` ✓ ${metadata.name} (${metadata.tools?.length || 0} tools)`);
418
- // Build manifest entry
419
- photons.push({
420
- name: metadata.name,
421
- version: metadata.version,
422
- description: metadata.description,
423
- author: metadata.author || options.owner || 'Unknown',
424
- license: metadata.license || 'MIT',
425
- repository: metadata.repository,
426
- homepage: metadata.homepage,
427
- icon: metadata.icon || null,
428
- source: `../${file}`,
429
- hash,
430
- tools: metadata.tools?.map((t) => t.name),
431
- assets: metadata.assets,
432
- photonType: metadata.photonType,
433
- features: metadata.features,
434
- });
435
- // Generate individual photon documentation
436
- const photonMarkdown = await templateMgr.renderTemplate('photon.md', metadata);
437
- const docPath = path.join(resolvedPath, `${metadata.name}.md`);
438
- await fs.writeFile(docPath, photonMarkdown, 'utf-8');
439
- }
440
- // Create manifest
441
- console.error('\n📋 Updating manifest...');
442
- const baseName = path.basename(resolvedPath);
443
- const marketplaceDir = path.join(resolvedPath, '.marketplace');
444
- await fs.mkdir(marketplaceDir, { recursive: true });
445
- const manifestPath = path.join(marketplaceDir, 'photons.json');
446
- // Read existing manifest to preserve owner if not explicitly provided
447
- let existingOwner;
448
- if (existsSync(manifestPath) && !options.owner) {
449
- try {
450
- const existingManifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
451
- existingOwner = existingManifest.owner;
452
- }
453
- catch {
454
- // Ignore parse errors
455
- }
456
- }
457
- const manifest = {
458
- name: options.name || baseName,
459
- version: PHOTON_VERSION,
460
- description: options.description || undefined,
461
- owner: options.owner
462
- ? {
463
- name: options.owner,
464
- }
465
- : existingOwner,
466
- photons,
467
- };
468
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
469
- console.error(' ✓ .marketplace/photons.json');
470
- // Sync README with generated content
471
- console.error('\n📖 Syncing README.md...');
472
- const { ReadmeSyncer } = await import('./readme-syncer.js');
473
- const readmePath = path.join(resolvedPath, 'README.md');
474
- const syncer = new ReadmeSyncer(readmePath);
475
- // Render README section from template
476
- const readmeContent = await templateMgr.renderTemplate('readme.md', {
477
- marketplaceName: manifest.name,
478
- marketplaceDescription: manifest.description || '',
479
- photons: photons.map((p) => ({
480
- name: p.name,
481
- description: p.description,
482
- version: p.version,
483
- license: p.license,
484
- tools: p.tools || [],
485
- photonType: p.photonType || 'api',
486
- features: p.features || [],
487
- })),
488
- });
489
- const isUpdate = await syncer.sync(readmeContent);
490
- if (isUpdate) {
491
- console.error(' ✓ README.md synced (user content preserved)');
492
- }
493
- else {
494
- console.error(' ✓ README.md created');
495
- }
496
- console.error('\n✅ Marketplace synced successfully!');
497
- console.error(`\n Marketplace: ${manifest.name}`);
498
- console.error(` Photons: ${photons.length}`);
499
- console.error(` Documentation: ${photons.length} markdown files generated`);
500
- console.error(`\n Generated files:`);
501
- console.error(` • .marketplace/photons.json (manifest)`);
502
- console.error(` • *.md (${photons.length} documentation files at root)`);
503
- console.error(` • README.md (auto-generated table)`);
504
- }
505
- /**
506
- * Initialize a marketplace with git hooks
507
- */
508
- async function performMarketplaceInit(dirPath, options) {
509
- const absolutePath = path.resolve(dirPath);
510
- // Check if directory exists
511
- if (!existsSync(absolutePath)) {
512
- await fs.mkdir(absolutePath, { recursive: true });
513
- console.error(`📁 Created directory: ${absolutePath}`);
514
- }
515
- // Check if it's a git repository
516
- const gitDir = path.join(absolutePath, '.git');
517
- if (!existsSync(gitDir)) {
518
- exitWithError('Not a git repository', {
519
- exitCode: ExitCode.CONFIG_ERROR,
520
- searchedIn: absolutePath,
521
- suggestion: 'Initialize with: git init',
522
- });
523
- }
524
- // Create .githooks directory
525
- const hooksDir = path.join(absolutePath, '.githooks');
526
- await fs.mkdir(hooksDir, { recursive: true });
527
- // Create pre-commit hook
528
- const preCommitHook = `#!/bin/bash
529
- # Pre-commit hook: Auto-sync marketplace manifest before commit
530
- # This ensures .marketplace/photons.json and .claude-plugin/ are always up-to-date
531
-
532
- # Check if any .photon.ts files or marketplace files are being committed
533
- if git diff --cached --name-only | grep -qE '\\.photon\\.ts$|\\.marketplace/|\\.claude-plugin/'; then
534
- echo "🔄 Syncing marketplace manifest..."
535
-
536
- # Run photon maker sync with --claude-code to generate plugin files
537
- if photon maker sync --dir . --claude-code; then
538
- # Stage the generated files
539
- git add .marketplace/photons.json README.md *.md .claude-plugin/ 2>/dev/null
540
- echo "✅ Marketplace and Claude Code plugin synced and staged"
541
- else
542
- echo "❌ Failed to sync marketplace"
543
- exit 1
544
- fi
545
- fi
546
-
547
- exit 0
548
- `;
549
- const preCommitPath = path.join(hooksDir, 'pre-commit');
550
- await fs.writeFile(preCommitPath, preCommitHook, { mode: 0o755 });
551
- console.error('✅ Created .githooks/pre-commit');
552
- // Create setup script
553
- const setupScript = `#!/bin/bash
554
- # Setup script to install git hooks for this marketplace
555
-
556
- REPO_ROOT="$(git rev-parse --show-toplevel)"
557
- HOOKS_DIR="$REPO_ROOT/.git/hooks"
558
- SOURCE_HOOKS="$REPO_ROOT/.githooks"
559
-
560
- echo "🔧 Installing git hooks for Photon marketplace..."
561
-
562
- # Copy pre-commit hook
563
- if [ -f "$SOURCE_HOOKS/pre-commit" ]; then
564
- cp "$SOURCE_HOOKS/pre-commit" "$HOOKS_DIR/pre-commit"
565
- chmod +x "$HOOKS_DIR/pre-commit"
566
- echo "✅ Installed pre-commit hook (auto-syncs marketplace manifest)"
567
- else
568
- echo "❌ pre-commit hook not found"
569
- exit 1
570
- fi
571
-
572
- echo ""
573
- echo "✅ Git hooks installed successfully!"
574
- echo ""
575
- echo "The pre-commit hook will automatically run 'photon maker sync'"
576
- echo "whenever you commit changes to .photon.ts files."
577
- `;
578
- const setupPath = path.join(hooksDir, 'setup.sh');
579
- await fs.writeFile(setupPath, setupScript, { mode: 0o755 });
580
- console.error('✅ Created .githooks/setup.sh');
581
- // Install hooks to .git/hooks
582
- const gitHooksDir = path.join(absolutePath, '.git', 'hooks');
583
- const gitPreCommitPath = path.join(gitHooksDir, 'pre-commit');
584
- await fs.writeFile(gitPreCommitPath, preCommitHook, { mode: 0o755 });
585
- console.error('✅ Installed hooks to .git/hooks');
586
- // Create .marketplace directory
587
- const marketplaceDir = path.join(absolutePath, '.marketplace');
588
- await fs.mkdir(marketplaceDir, { recursive: true });
589
- console.error('✅ Created .marketplace directory');
590
- // Run initial sync (don't filter installed for marketplace repos)
591
- console.error('\n🔄 Running initial marketplace sync...\n');
592
- await performMarketplaceSync(absolutePath, options);
593
- console.error('\n✅ Marketplace initialized successfully!');
594
- console.error('\nNext steps:');
595
- console.error('1. Add your .photon.ts files to this directory');
596
- console.error('2. Commit your changes (hooks will auto-sync)');
597
- console.error('3. Push to GitHub to share your marketplace');
598
- console.error('\nContributors can setup hooks with:');
599
- console.error(' bash .githooks/setup.sh');
600
- }
601
- /**
602
- * Format default value for display in config
603
- */
604
- function formatDefaultValue(value) {
605
- if (typeof value === 'string') {
606
- // Check if it's a function call expression
607
- if (value.includes('homedir()')) {
608
- // Replace homedir() with actual home directory
609
- // Handle both path.join() and join()
610
- return value.replace(/(?:path\.)?join\(homedir\(\),\s*['"]([^'"]+)['"]\)/g, (_, folderName) => {
611
- return path.join(os.homedir(), folderName);
612
- });
613
- }
614
- if (value.includes('process.cwd()')) {
615
- return process.cwd();
616
- }
617
- return value;
618
- }
619
- if (typeof value === 'number' || typeof value === 'boolean') {
620
- return String(value);
621
- }
622
- // For other complex expressions
623
- return String(value);
624
- }
625
- /**
626
- * Get OS-specific MCP client config path
627
- */
628
- function getConfigPath() {
629
- const platform = process.platform;
630
- const home = os.homedir();
631
- if (platform === 'darwin') {
632
- return path.join(home, 'Library/Application Support/Claude/claude_desktop_config.json');
633
- }
634
- else if (platform === 'win32') {
635
- // On Windows, use APPDATA if available, otherwise fall back to home/AppData/Roaming
636
- const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
637
- return path.join(appData, 'Claude', 'claude_desktop_config.json');
638
- }
639
- else {
640
- // Linux/other
641
- return path.join(home, '.config/Claude/claude_desktop_config.json');
642
- }
643
- }
644
- /**
645
- * Validate configuration for an MCP
646
- */
647
- async function validateConfiguration(filePath, mcpName) {
648
- cliHeading(`🔍 Validating configuration for: ${mcpName}`);
649
- cliSpacer();
650
- const params = await extractConstructorParams(filePath);
651
- if (params.length === 0) {
652
- printSuccess('No configuration required for this MCP.');
653
- return;
654
- }
655
- let hasErrors = false;
656
- const results = [];
657
- for (const param of params) {
658
- const envVarName = toEnvVarName(mcpName, param.name);
659
- const envValue = process.env[envVarName];
660
- const isRequired = !param.isOptional && !param.hasDefault;
661
- if (isRequired && !envValue) {
662
- hasErrors = true;
663
- results.push({
664
- name: param.name,
665
- envVar: envVarName,
666
- status: '❌ Missing (required)',
667
- });
668
- }
669
- else if (envValue) {
670
- results.push({
671
- name: param.name,
672
- envVar: envVarName,
673
- status: '✅ Set',
674
- value: envValue.length > 20 ? `${envValue.substring(0, 17)}...` : envValue,
675
- });
676
- }
677
- else {
678
- results.push({
679
- name: param.name,
680
- envVar: envVarName,
681
- status: '⚪ Optional',
682
- value: param.hasDefault ? `default: ${formatDefaultValue(param.defaultValue)}` : undefined,
683
- });
684
- }
685
- }
686
- printHeader('Configuration status');
687
- results.forEach((r) => {
688
- printInfo(` ${r.status} ${r.envVar}`);
689
- if (r.value) {
690
- printInfo(` Value: ${r.value}`);
691
- }
692
- });
693
- cliSpacer();
694
- if (hasErrors) {
695
- exitWithError('Validation failed: Missing required environment variables', {
696
- exitCode: ExitCode.CONFIG_ERROR,
697
- suggestion: `Run 'photon mcp ${mcpName} --config' to see the configuration template`,
698
- });
699
- }
700
- else {
701
- printSuccess('Configuration valid!');
702
- cliHint(`Run: photon mcp ${mcpName}`);
703
- }
704
- }
705
- /**
706
- * Show configuration template for an MCP
707
- */
708
- async function showConfigTemplate(filePath, mcpName, workingDir = DEFAULT_WORKING_DIR) {
709
- cliHeading(`📋 Configuration template for: ${mcpName}`);
710
- cliSpacer();
711
- const params = await extractConstructorParams(filePath);
712
- if (params.length === 0) {
713
- printSuccess('No configuration required for this MCP.');
714
- return;
715
- }
716
- printHeader('Environment variables');
717
- params.forEach((param) => {
718
- const envVarName = toEnvVarName(mcpName, param.name);
719
- const isRequired = !param.isOptional && !param.hasDefault;
720
- const status = isRequired ? '[REQUIRED]' : '[OPTIONAL]';
721
- printInfo(` ${envVarName} ${status}`);
722
- printInfo(` Type: ${param.type}`);
723
- if (param.hasDefault) {
724
- printInfo(` Default: ${formatDefaultValue(param.defaultValue)}`);
725
- }
726
- cliSpacer();
727
- });
728
- printHeader('Claude Desktop configuration');
729
- const envExample = {};
730
- params.forEach((param) => {
731
- const envVarName = toEnvVarName(mcpName, param.name);
732
- if (!param.isOptional && !param.hasDefault) {
733
- envExample[envVarName] = `<your-${param.name}>`;
734
- }
735
- });
736
- const needsWorkingDir = workingDir !== DEFAULT_WORKING_DIR;
737
- const config = {
738
- mcpServers: {
739
- [mcpName]: {
740
- command: 'npx',
741
- args: needsWorkingDir
742
- ? ['@portel/photon', 'mcp', mcpName, '--dir', workingDir]
743
- : ['@portel/photon', 'mcp', mcpName],
744
- env: envExample,
745
- },
746
- },
747
- };
748
- console.log(JSON.stringify(config, null, 2));
749
- cliSpacer();
750
- cliHint(`Add this to: ${getConfigPath()}`);
751
- cliHint(`Validate with: photon mcp ${mcpName} --validate`);
752
- }
753
- const version = PHOTON_VERSION;
754
- const program = new Command();
755
- program
756
- .name('photon')
757
- .description('Universal runtime for single-file TypeScript programs')
758
- .version(version)
759
- .option('--dir <path>', 'Photon directory (default: ~/.photon)', DEFAULT_WORKING_DIR)
760
- .option('--log-level <level>', 'Set log verbosity (error|warn|info|debug)', 'info')
761
- .option('--json-logs', 'Emit newline-delimited JSON logs for runtime output')
762
- .configureHelp({
763
- sortSubcommands: false,
764
- sortOptions: false,
765
- // Hide Commander's auto-generated "Commands:" section since we show
766
- // a custom categorized section in addHelpText('after', ...)
767
- visibleCommands: () => [],
768
- })
769
- .addHelpText('after', `
770
- Runtime Commands:
771
- mcp <name> Run a photon as MCP server (for AI assistants)
772
- cli <photon> [method] Run photon methods from command line
773
- sse <name> Run Photon as HTTP server with SSE transport
774
- beam Launch Photon Beam (interactive control panel)
775
- serve Start local multi-tenant MCP hosting for development
776
-
777
- Configuration:
778
- use <photon> [instance] Switch to a named instance of a stateful photon
779
- instances <photon> List all instances of a stateful photon
780
- set <photon> [values] Configure environment for a photon
781
-
782
- Hosting:
783
- host <command> Manage cloud hosting (preview, deploy)
784
-
785
- Package Management:
786
- add <name> Install a photon from marketplace
787
- remove <name> Remove an installed photon
788
- upgrade [name] Upgrade photon(s) to latest version
789
- search <query> Search marketplaces for photons
790
- info [name] Show installed photons and details
791
-
792
- Maintenance:
793
- update Refresh marketplace indexes & check CLI version
794
- doctor [name] Diagnose environment and installations
795
-
796
- Development:
797
- maker new <name> Create a new photon from template
798
- maker validate <name> Validate photon syntax and schemas
799
- maker sync Generate marketplace manifest
800
- maker init Initialize marketplace with git hooks
801
-
802
- Advanced:
803
- marketplace Manage marketplace sources
804
- alias <photon> Create CLI shortcuts for photons
805
-
806
- Run 'photon <command> --help' for detailed usage.
807
- `);
808
- // Update command: refresh marketplace indexes and check for CLI updates
809
- program
810
- .command('update', { hidden: true })
811
- .description('Update marketplace indexes and check for CLI updates')
812
- .action(async () => {
813
- try {
814
- const { printInfo, printSuccess, printWarning, printHeader } = await import('./cli-formatter.js');
815
- const { MarketplaceManager } = await import('./marketplace-manager.js');
816
- const manager = new MarketplaceManager();
817
- await manager.initialize();
818
- const results = await runTask('Refreshing marketplace indexes', async () => {
819
- return manager.updateAllCaches();
820
- });
821
- console.log('');
822
- const entries = Array.from(results.entries());
823
- let successCount = 0;
824
- for (const [marketplaceName, success] of entries) {
825
- if (success) {
826
- printSuccess(marketplaceName);
827
- successCount++;
828
- }
829
- else {
830
- printWarning(`${marketplaceName} (no manifest)`);
831
- }
832
- }
833
- printInfo(`\nUpdated ${successCount}/${entries.length} marketplaces`);
834
- let latestVersion = null;
835
- try {
836
- latestVersion = await runTask('Checking for Photon CLI updates', async () => {
837
- const { execSync } = await import('child_process');
838
- return execSync('npm view @portel/photon version', {
839
- encoding: 'utf-8',
840
- timeout: 10000,
841
- }).trim();
842
- });
843
- }
844
- catch {
845
- printWarning('\nCould not check for CLI updates');
846
- }
847
- if (latestVersion) {
848
- console.log('');
849
- if (latestVersion !== version) {
850
- printHeader('Update available');
851
- printWarning(`Current: ${version}`);
852
- printInfo(`Latest: ${latestVersion}`);
853
- printInfo(`Update with: npm install -g @portel/photon`);
854
- }
855
- else {
856
- printSuccess(`Photon CLI is up to date (${version})`);
857
- }
858
- }
859
- }
860
- catch (error) {
861
- const { printError } = await import('./cli-formatter.js');
862
- printError(getErrorMessage(error));
863
- process.exit(1);
864
- }
865
- });
866
- // MCP Runtime: run a .photon.ts file as MCP server
867
- program
868
- .command('mcp', { hidden: true })
869
- .argument('<name>', 'MCP name (without .photon.ts extension)')
870
- .description('Run a Photon as MCP server')
871
- .option('--dev', 'Enable development mode with hot reload')
872
- .option('--validate', 'Validate configuration without running server')
873
- .option('--config', 'Show configuration template and exit')
874
- .option('--transport <type>', 'Transport type: stdio (default) or sse', 'stdio')
875
- .option('--port <number>', 'Port for SSE transport (default: 3000)', '3000')
876
- .action(async (rawName, options, command) => {
877
- try {
878
- // Parse extended name format (e.g., "alice/repo:rss-feed")
879
- const { name, marketplaceSource } = parsePhotonSpec(rawName);
880
- // Get working directory from global options
881
- const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
882
- const logOptions = getLogOptionsFromCommand(command);
883
- // Resolve file path - check bundled photons first, then user directory
884
- let filePath = await resolvePhotonPathWithBundled(name, workingDir);
885
- // Auto-install from marketplace if not found locally
886
- let unresolvedPhoton;
887
- if (!filePath) {
888
- const { MarketplaceManager, calculateHash } = await import('./marketplace-manager.js');
889
- const manager = new MarketplaceManager();
890
- await manager.initialize();
891
- // If marketplace source given, add it (persistent, idempotent)
892
- if (marketplaceSource) {
893
- const { marketplace: addedMp, added } = await manager.add(marketplaceSource);
894
- if (added) {
895
- console.error(`Added marketplace: ${addedMp.name}`);
896
- await manager.updateMarketplaceCache(addedMp.name);
897
- }
898
- }
899
- // Check for conflicts (multiple sources)
900
- const conflict = await manager.checkConflict(name);
901
- if (conflict.sources.length === 0) {
902
- // Not found anywhere
903
- exitWithError(`MCP not found: ${name}`, {
904
- exitCode: ExitCode.NOT_FOUND,
905
- searchedIn: workingDir,
906
- suggestion: DEFAULT_BUNDLED_PHOTONS.includes(name)
907
- ? `'${name}' is a bundled photon but could not be found`
908
- : marketplaceSource
909
- ? `Photon '${name}' not found in ${marketplaceSource}`
910
- : "Use 'photon search <name>' to find it or 'photon marketplace add <source>' to add a marketplace",
911
- });
912
- }
913
- else if (conflict.sources.length === 1 || !conflict.hasConflict) {
914
- // Single source — auto-download
915
- const source = conflict.sources[0];
916
- console.error(`Installing ${name} from ${source.marketplace.name}...`);
917
- const result = await manager.fetchMCP(name);
918
- if (!result) {
919
- exitWithError(`Failed to download: ${name}`, {
920
- exitCode: ExitCode.ERROR,
921
- suggestion: 'Check your internet connection and marketplace configuration',
922
- });
923
- }
924
- // Ensure working directory exists and save
925
- await ensureWorkingDir(workingDir);
926
- const targetPath = path.join(workingDir, `${name}.photon.ts`);
927
- await fs.writeFile(targetPath, result.content, 'utf-8');
928
- // Save metadata
929
- if (source.metadata) {
930
- const contentHash = calculateHash(result.content);
931
- await manager.savePhotonMetadata(`${name}.photon.ts`, source.marketplace, source.metadata, contentHash);
932
- // Download assets
933
- if (source.metadata.assets && source.metadata.assets.length > 0) {
934
- const assets = await manager.fetchAssets(source.marketplace, source.metadata.assets);
935
- for (const [assetPath, content] of assets) {
936
- // Security: validate asset path to prevent traversal
937
- const safePath = validateAssetPath(assetPath);
938
- const assetTarget = path.join(workingDir, safePath);
939
- if (!isPathWithin(assetTarget, workingDir)) {
940
- console.error(`Skipping unsafe asset path: ${assetPath}`);
941
- continue;
942
- }
943
- const assetDir = path.dirname(assetTarget);
944
- await fs.mkdir(assetDir, { recursive: true });
945
- await fs.writeFile(assetTarget, content, 'utf-8');
946
- }
947
- }
948
- }
949
- console.error(`Installed ${name}`);
950
- filePath = targetPath;
951
- }
952
- else {
953
- // Multiple sources — defer to server for elicitation
954
- unresolvedPhoton = {
955
- name,
956
- workingDir,
957
- sources: conflict.sources,
958
- recommendation: conflict.recommendation,
959
- };
960
- }
961
- }
962
- // Handle --validate flag (requires resolved filePath)
963
- if (options.validate) {
964
- if (!filePath) {
965
- exitWithError(`Cannot validate: ${name} has multiple sources. Install it first with 'photon add ${name}'.`, {
966
- exitCode: ExitCode.CONFIG_ERROR,
967
- });
968
- }
969
- await validateConfiguration(filePath, name);
970
- return;
971
- }
972
- // Handle --config flag
973
- if (options.config) {
974
- if (!filePath) {
975
- exitWithError(`Cannot show config: ${name} has multiple sources. Install it first with 'photon add ${name}'.`, {
976
- exitCode: ExitCode.CONFIG_ERROR,
977
- });
978
- }
979
- await showConfigTemplate(filePath, name, workingDir);
980
- return;
981
- }
982
- // Validate transport option
983
- const transport = options.transport;
984
- if (transport !== 'stdio' && transport !== 'sse') {
985
- exitWithError(`Invalid transport: ${options.transport}`, {
986
- exitCode: ExitCode.INVALID_ARGUMENT,
987
- suggestion: 'Valid options: stdio, sse',
988
- });
989
- }
990
- // Set PHOTON_NAME for daemon broker pub/sub to work
991
- // This ensures channel messages go to the correct daemon socket
992
- process.env.PHOTON_NAME = name;
993
- // Start MCP server
994
- const server = new PhotonServer({
995
- filePath: filePath || '', // empty when unresolved — server handles it
996
- devMode: options.dev,
997
- transport,
998
- port: parseInt(options.port, 10),
999
- logOptions: { ...logOptions, scope: transport },
1000
- unresolvedPhoton,
1001
- });
1002
- // Handle shutdown signals
1003
- const shutdown = async () => {
1004
- console.error('\nShutting down...');
1005
- await server.stop();
1006
- process.exit(0);
1007
- };
1008
- process.on('SIGINT', shutdown);
1009
- process.on('SIGTERM', shutdown);
1010
- // Start the server
1011
- await server.start();
1012
- // Start file watcher in dev mode (only if resolved)
1013
- if (options.dev && filePath) {
1014
- const watcher = new FileWatcher(server, filePath, server.createScopedLogger('watcher'));
1015
- watcher.start();
1016
- // Clean up watcher on shutdown
1017
- process.on('SIGINT', async () => {
1018
- await watcher.stop();
1019
- });
1020
- process.on('SIGTERM', async () => {
1021
- await watcher.stop();
1022
- });
1023
- }
1024
- }
1025
- catch (error) {
1026
- logger.error(`Error: ${getErrorMessage(error)}`);
1027
- process.exit(1);
1028
- }
1029
- });
1030
- // SSE command: quick SSE server with auto port detection (formerly serve)
1031
- program
1032
- .command('sse', { hidden: true })
1033
- .argument('<name>', 'Photon name (without .photon.ts extension)')
1034
- .option('-p, --port <number>', 'Port to start from (auto-finds available)', '3000')
1035
- .option('--dev', 'Enable development mode with hot reload')
1036
- .description('Run Photon as HTTP server with SSE transport (auto port detection)')
1037
- .action(async (name, options, command) => {
1038
- try {
1039
- // Get working directory from global options
1040
- const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1041
- const logOptions = getLogOptionsFromCommand(command);
1042
- // Resolve file path from name
1043
- const filePath = await resolvePhotonPath(name, workingDir);
1044
- if (!filePath) {
1045
- exitWithError(`Photon not found: ${name}`, {
1046
- exitCode: ExitCode.NOT_FOUND,
1047
- searchedIn: workingDir,
1048
- suggestion: "Use 'photon info' to see available photons",
1049
- });
1050
- }
1051
- // Find available port
1052
- const startPort = parseInt(options.port, 10);
1053
- const port = await findAvailablePort(startPort);
1054
- if (port !== startPort) {
1055
- console.error(`⚠️ Port ${startPort} is in use, using ${port} instead\n`);
1056
- }
1057
- // Start SSE server
1058
- const server = new PhotonServer({
1059
- filePath,
1060
- devMode: options.dev,
1061
- transport: 'sse',
1062
- port,
1063
- logOptions: { ...logOptions, scope: 'sse' },
1064
- });
1065
- // Handle shutdown signals
1066
- const shutdown = async () => {
1067
- console.error('\nShutting down...');
1068
- await server.stop();
1069
- process.exit(0);
1070
- };
1071
- process.on('SIGINT', shutdown);
1072
- process.on('SIGTERM', shutdown);
1073
- // Start the server
1074
- await server.start();
1075
- // Start file watcher in dev mode
1076
- if (options.dev) {
1077
- const watcher = new FileWatcher(server, filePath, server.createScopedLogger('watcher'));
1078
- watcher.start();
1079
- process.on('SIGINT', async () => {
1080
- await watcher.stop();
1081
- });
1082
- process.on('SIGTERM', async () => {
1083
- await watcher.stop();
1084
- });
1085
- }
1086
- }
1087
- catch (error) {
1088
- logger.error(`Error: ${getErrorMessage(error)}`);
1089
- process.exit(1);
1090
- }
1091
- });
1092
- // Beam command: interactive UI for all photons
1093
- program
1094
- .command('beam', { hidden: true })
1095
- .option('-p, --port <number>', 'Port to start from (auto-finds available)', '3000')
1096
- .option('-o, --open', 'Auto-open browser after starting')
1097
- .option('--no-open', 'Do not auto-open browser')
1098
- .description('Launch Photon Beam - interactive control panel for all your photons')
1099
- .action(async (options, command) => {
1100
- try {
1101
- // Get working directory from global options
1102
- const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1103
- // Find available port
1104
- const startPort = parseInt(options.port, 10);
1105
- const port = await findAvailablePort(startPort);
1106
- if (port !== startPort) {
1107
- console.error(`⚠️ Port ${startPort} is in use, using ${port} instead\n`);
1108
- }
1109
- // Import and start Beam server
1110
- const { startBeam } = await import('./auto-ui/beam.js');
1111
- await startBeam(workingDir, port);
1112
- // Auto-open browser if requested
1113
- // Use actual bound port from BEAM_PORT env var (set by startBeam after binding)
1114
- if (options.open) {
1115
- const actualPort = process.env.BEAM_PORT || port;
1116
- const url = `http://localhost:${actualPort}`;
1117
- const { exec } = await import('child_process');
1118
- const openCmd = process.platform === 'darwin'
1119
- ? 'open'
1120
- : process.platform === 'win32'
1121
- ? 'start'
1122
- : 'xdg-open';
1123
- exec(`${openCmd} ${url}`, (err) => {
1124
- if (err)
1125
- logger.debug(`Could not auto-open browser: ${err.message}`);
1126
- });
1127
- }
1128
- // Handle shutdown signals (guard against duplicate Ctrl+C)
1129
- let shuttingDown = false;
1130
- const shutdown = async () => {
1131
- if (shuttingDown)
1132
- return;
1133
- shuttingDown = true;
1134
- console.error('\nShutting down Photon Beam...');
1135
- // Gracefully close external MCP clients to prevent ugly tracebacks
1136
- try {
1137
- const { stopBeam } = await import('./auto-ui/beam.js');
1138
- await stopBeam();
1139
- }
1140
- catch {
1141
- // Ignore cleanup errors
1142
- }
1143
- process.exit(0);
1144
- };
1145
- process.on('SIGINT', shutdown);
1146
- process.on('SIGTERM', shutdown);
1147
- }
1148
- catch (error) {
1149
- logger.error(`Error: ${getErrorMessage(error)}`);
1150
- process.exit(1);
1151
- }
1152
- });
1153
- // Serve command: multi-tenant MCP hosting (formerly serv)
1154
- program
1155
- .command('serve', { hidden: true })
1156
- .option('-p, --port <number>', 'Port to run on', '4000')
1157
- .option('-d, --debug', 'Enable debug logging')
1158
- .description('Start local multi-tenant MCP hosting for development')
1159
- .action(async (options) => {
1160
- try {
1161
- const port = parseInt(options.port, 10);
1162
- const availablePort = await findAvailablePort(port);
1163
- if (availablePort !== port) {
1164
- console.error(`⚠️ Port ${port} is in use, using ${availablePort} instead\n`);
1165
- }
1166
- // Import and start LocalServ
1167
- const { createLocalServ, getTestToken } = await import('./serv/local.js');
1168
- const { serv, tenant, user } = createLocalServ({
1169
- port: availablePort,
1170
- baseUrl: `http://localhost:${availablePort}`,
1171
- debug: options.debug,
1172
- });
1173
- // Get a test token
1174
- const token = await getTestToken(serv, tenant, user);
1175
- console.error(`
1176
- ⚡ Photon Serve (Multi-tenant Development)
1177
-
1178
- URL: http://localhost:${availablePort}
1179
- Tenant: ${tenant.slug} (${tenant.name})
1180
- User: ${user.email}
1181
-
1182
- Test Token:
1183
- ${token}
1184
-
1185
- MCP Endpoint:
1186
- http://localhost:${availablePort}/tenant/${tenant.slug}/mcp
1187
-
1188
- Well-Known:
1189
- http://localhost:${availablePort}/.well-known/oauth-protected-resource
1190
-
1191
- Press Ctrl+C to stop
1192
- `);
1193
- // Simple HTTP server
1194
- const http = await import('http');
1195
- const server = http.createServer(async (req, res) => {
1196
- const url = req.url || '/';
1197
- const method = req.method || 'GET';
1198
- const headers = {};
1199
- for (const [key, value] of Object.entries(req.headers)) {
1200
- if (typeof value === 'string')
1201
- headers[key] = value;
1202
- }
1203
- // Read body if present
1204
- let body = '';
1205
- if (method === 'POST') {
1206
- body = await new Promise((resolve) => {
1207
- let data = '';
1208
- req.on('data', (chunk) => (data += chunk));
1209
- req.on('end', () => resolve(data));
1210
- });
1211
- }
1212
- const result = await serv.handleRequest(method, url, headers, body);
1213
- res.writeHead(result.status, result.headers);
1214
- res.end(result.body);
1215
- });
1216
- server.listen(availablePort);
1217
- // Handle shutdown
1218
- const shutdown = async () => {
1219
- console.error('\nShutting down Photon Serve...');
1220
- await serv.shutdown();
1221
- server.close();
1222
- process.exit(0);
1223
- };
1224
- process.on('SIGINT', shutdown);
1225
- process.on('SIGTERM', shutdown);
1226
- }
1227
- catch (error) {
1228
- logger.error(`Error: ${getErrorMessage(error)}`);
1229
- process.exit(1);
1230
- }
1231
- });
1232
- // Host command: manage hosting and deployment (preview, deploy)
1233
- const host = program
1234
- .command('host', { hidden: true })
1235
- .description('Manage cloud hosting and deployment');
1236
- host
1237
- .command('preview')
1238
- .argument('<target>', 'Deployment target: cloudflare (or cf)')
1239
- .argument('<name>', 'Photon name (without .photon.ts extension)')
1240
- .option('--output <dir>', 'Output directory for generated project')
1241
- .description('Run Photon locally in a simulated deployment environment')
1242
- .action(async (target, name, options) => {
1243
- try {
1244
- // Get working directory from global options
1245
- const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1246
- // Resolve file path from name
1247
- const photonPath = await resolvePhotonPath(name, workingDir);
1248
- if (!photonPath) {
1249
- logger.error(`Photon not found: ${name}`);
1250
- console.error(`Searched in: ${workingDir}`);
1251
- console.error(`Tip: Use 'photon info' to see available photons`);
1252
- process.exit(1);
1253
- }
1254
- const normalizedTarget = target.toLowerCase();
1255
- if (normalizedTarget === 'cloudflare' || normalizedTarget === 'cf') {
1256
- const { devCloudflare } = await import('./deploy/cloudflare.js');
1257
- await devCloudflare({
1258
- photonPath,
1259
- outputDir: options.output,
1260
- });
1261
- }
1262
- else {
1263
- logger.error(`Unknown target: ${target}`);
1264
- console.error('Supported targets: cloudflare (cf)');
1265
- process.exit(1);
1266
- }
1267
- }
1268
- catch (error) {
1269
- logger.error(`Error: ${getErrorMessage(error)}`);
1270
- process.exit(1);
1271
- }
1272
- });
1273
- host
1274
- .command('deploy')
1275
- .argument('<target>', 'Deployment target: cloudflare (or cf)')
1276
- .argument('<name>', 'Photon name (without .photon.ts extension)')
1277
- .option('--dev', 'Enable Beam UI in deployment')
1278
- .option('--dry-run', 'Generate project without deploying')
1279
- .option('--output <dir>', 'Output directory for generated project')
1280
- .description('Deploy a Photon to cloud platforms')
1281
- .action(async (target, name, options) => {
1282
- try {
1283
- // Get working directory from global options
1284
- const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1285
- // Resolve file path from name
1286
- const photonPath = await resolvePhotonPath(name, workingDir);
1287
- if (!photonPath) {
1288
- logger.error(`Photon not found: ${name}`);
1289
- console.error(`Searched in: ${workingDir}`);
1290
- console.error(`Tip: Use 'photon info' to see available photons`);
1291
- process.exit(1);
1292
- }
1293
- const normalizedTarget = target.toLowerCase();
1294
- if (normalizedTarget === 'cloudflare' || normalizedTarget === 'cf') {
1295
- const { deployToCloudflare } = await import('./deploy/cloudflare.js');
1296
- await deployToCloudflare({
1297
- photonPath,
1298
- devMode: options.dev,
1299
- dryRun: options.dryRun,
1300
- outputDir: options.output,
1301
- });
1302
- }
1303
- else {
1304
- logger.error(`Unknown deployment target: ${target}`);
1305
- console.error('Supported targets: cloudflare (cf)');
1306
- process.exit(1);
1307
- }
1308
- }
1309
- catch (error) {
1310
- logger.error(`Deployment failed: ${getErrorMessage(error)}`);
1311
- process.exit(1);
1312
- }
1313
- });
1314
- // Search command: search for MCPs across marketplaces
1315
- program
1316
- .command('search', { hidden: true })
1317
- .argument('<query>', 'MCP name or keyword to search for')
1318
- .description('Search for MCP in all enabled marketplaces')
1319
- .action(async (query) => {
1320
- try {
1321
- const { MarketplaceManager } = await import('./marketplace-manager.js');
1322
- const { formatOutput, printInfo, printError } = await import('./cli-formatter.js');
1323
- const manager = new MarketplaceManager();
1324
- await manager.initialize();
1325
- // Auto-update stale caches
1326
- const updated = await manager.autoUpdateStaleCaches();
1327
- if (updated) {
1328
- printInfo('Refreshed marketplace data...\n');
1329
- }
1330
- printInfo(`Searching for '${query}' in marketplaces...`);
1331
- const results = await manager.search(query);
1332
- if (results.size === 0) {
1333
- printError(`No results found for '${query}'`);
1334
- printInfo(`Tip: Run 'photon marketplace update' to manually refresh marketplace data`);
1335
- return;
1336
- }
1337
- // Build table data from search results
1338
- const tableData = [];
1339
- for (const [mcpName, entries] of results) {
1340
- for (const entry of entries) {
1341
- tableData.push({
1342
- name: mcpName,
1343
- version: entry.metadata?.version || PHOTON_VERSION,
1344
- description: entry.metadata?.description
1345
- ? entry.metadata.description.substring(0, 50) +
1346
- (entry.metadata.description.length > 50 ? '...' : '')
1347
- : '-',
1348
- marketplace: entry.marketplace.name,
1349
- });
1350
- }
1351
- }
1352
- console.log('');
1353
- formatOutput(tableData, 'table');
1354
- printInfo(`\nInstall with: photon add <name>`);
1355
- }
1356
- catch (error) {
1357
- const { printError } = await import('./cli-formatter.js');
1358
- printError(getErrorMessage(error));
1359
- process.exit(1);
1360
- }
1361
- });
1362
- // Maker command: commands for photon creators/publishers
1363
- const maker = program
1364
- .command('maker', { hidden: true })
1365
- .description('Commands for creating photons and marketplaces');
1366
- // maker new: create a new photon from template
1367
- maker
1368
- .command('new')
1369
- .argument('<name>', 'Name for the new photon')
1370
- .description('Create a new photon from template')
1371
- .action(async (name, options, command) => {
1372
- try {
1373
- // Get working directory from global options
1374
- const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1375
- // Ensure working directory exists
1376
- await ensureWorkingDir(workingDir);
1377
- const fileName = `${name}.photon.ts`;
1378
- const filePath = path.join(workingDir, fileName);
1379
- // Check if file already exists
1380
- try {
1381
- await fs.access(filePath);
1382
- exitWithError(`File already exists: ${filePath}`, {
1383
- suggestion: `Choose a different name or delete the existing file`,
1384
- });
1385
- }
1386
- catch (err) {
1387
- if (!isNodeError(err, 'ENOENT')) {
1388
- exitWithError(`Cannot access ${filePath}: ${getErrorMessage(err)}`);
1389
- }
1390
- // ENOENT = file doesn't exist — good, proceed
1391
- }
1392
- // Read template
1393
- const templatePath = path.join(__dirname, '..', 'templates', 'photon.template.ts');
1394
- let template;
1395
- try {
1396
- template = await fs.readFile(templatePath, 'utf-8');
1397
- }
1398
- catch (err) {
1399
- logger.debug(`Template not found at ${templatePath}, using inline template`);
1400
- template = getInlineTemplate();
1401
- }
1402
- // Replace placeholders
1403
- // Convert kebab-case to PascalCase for class name
1404
- const className = name
1405
- .split(/[-_]/)
1406
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
1407
- .join('');
1408
- const content = template.replace(/TemplateName/g, className).replace(/template-name/g, name);
1409
- // Write file
1410
- await fs.writeFile(filePath, content, 'utf-8');
1411
- console.error(`✅ Created ${fileName} in ${workingDir}`);
1412
- console.error(`Run with: photon mcp ${name} --dev`);
1413
- }
1414
- catch (error) {
1415
- logger.error(`Error: ${getErrorMessage(error)}`);
1416
- process.exit(1);
1417
- }
1418
- });
1419
- // maker validate: validate photon syntax and schemas
1420
- maker
1421
- .command('validate')
1422
- .argument('<name>', 'Photon name (without .photon.ts extension)')
1423
- .description('Validate photon syntax and schemas')
1424
- .action(async (name, options, command) => {
1425
- try {
1426
- // Get working directory from global options
1427
- const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
1428
- // Resolve file path from name in working directory
1429
- const filePath = await resolvePhotonPath(name, workingDir);
1430
- if (!filePath) {
1431
- exitWithError(`Photon not found: ${name}`, {
1432
- exitCode: ExitCode.NOT_FOUND,
1433
- searchedIn: workingDir,
1434
- suggestion: "Use 'photon info' to see available photons",
1435
- });
1436
- }
1437
- console.error(`Validating ${path.basename(filePath)}...\n`);
1438
- // Import loader and try to load
1439
- const { PhotonLoader } = await import('./loader.js');
1440
- const loader = new PhotonLoader(false); // quiet mode for inspection
1441
- const mcp = await loader.loadFile(filePath);
1442
- console.error(`✅ Valid Photon`);
1443
- console.error(`Name: ${mcp.name}`);
1444
- console.error(`Tools: ${mcp.tools.length}`);
1445
- for (const tool of mcp.tools) {
1446
- console.error(` - ${tool.name}: ${tool.description}`);
1447
- }
1448
- process.exit(0);
1449
- }
1450
- catch (error) {
1451
- logger.error(`Validation failed: ${getErrorMessage(error)}`);
1452
- process.exit(1);
1453
- }
1454
- });
1455
- // maker sync: generate marketplace manifest
1456
- maker
1457
- .command('sync')
1458
- .option('--dir <path>', 'Directory to sync (defaults to current directory)')
1459
- .option('--name <name>', 'Marketplace name')
1460
- .option('--description <desc>', 'Marketplace description')
1461
- .option('--owner <owner>', 'Owner name')
1462
- .option('--claude-code', 'Generate Claude Code plugin files')
1463
- .description('Generate marketplace manifest and documentation from your photons')
1464
- .action(async (options) => {
1465
- try {
1466
- const dirPath = options.dir || '.';
1467
- const resolvedPath = path.resolve(dirPath);
1468
- // Only filter installed photons when syncing ~/.photon
1469
- const filterInstalled = resolvedPath === DEFAULT_WORKING_DIR;
1470
- await performMarketplaceSync(dirPath, { ...options, filterInstalled });
1471
- // Generate Claude Code plugin if requested
1472
- if (options.claudeCode) {
1473
- const { generateClaudeCodePlugin } = await import('./claude-code-plugin.js');
1474
- await generateClaudeCodePlugin(dirPath, options);
1475
- }
1476
- }
1477
- catch (error) {
1478
- logger.error(`Error: ${getErrorMessage(error)}`);
1479
- if (process.env.DEBUG && error instanceof Error) {
1480
- console.error(error.stack);
1481
- }
1482
- process.exit(1);
1483
- }
1484
- });
1485
- maker
1486
- .command('init')
1487
- .option('--dir <path>', 'Directory to initialize (defaults to current directory)')
1488
- .option('--name <name>', 'Marketplace name')
1489
- .option('--description <desc>', 'Marketplace description')
1490
- .option('--owner <owner>', 'Owner name')
1491
- .description('Initialize a directory as a Photon marketplace with git hooks')
1492
- .action(async (options) => {
1493
- try {
1494
- const dirPath = options.dir || '.';
1495
- await performMarketplaceInit(dirPath, options);
1496
- }
1497
- catch (error) {
1498
- logger.error(`Error: ${getErrorMessage(error)}`);
1499
- if (process.env.DEBUG && error instanceof Error) {
1500
- console.error(error.stack);
1501
- }
1502
- process.exit(1);
1503
- }
1504
- });
1505
- // maker diagram: generate Mermaid diagram for a Photon
1506
- maker
1507
- .command('diagram <photon>')
1508
- .option('--dir <path>', 'Directory containing photon (defaults to current directory)')
1509
- .description('Generate Mermaid diagram for a Photon')
1510
- .action(async (photonName, options) => {
1511
- try {
1512
- const { PhotonDocExtractor } = await import('./photon-doc-extractor.js');
1513
- // Resolve photon path
1514
- const dirPath = options.dir || '.';
1515
- let photonPath = photonName;
1516
- // If not a path, look in the directory
1517
- if (!photonName.includes('/') && !photonName.includes('\\')) {
1518
- if (!photonName.endsWith('.photon.ts')) {
1519
- photonName = `${photonName}.photon.ts`;
1520
- }
1521
- photonPath = path.resolve(dirPath, photonName);
1522
- }
1523
- else {
1524
- photonPath = path.resolve(photonName);
1525
- }
1526
- if (!existsSync(photonPath)) {
1527
- logger.error(`Photon not found: ${photonPath}`);
1528
- process.exit(1);
1529
- }
1530
- const extractor = new PhotonDocExtractor(photonPath);
1531
- const diagram = await extractor.generateDiagram();
1532
- // Output just the diagram (can be piped or copied)
1533
- console.log(diagram);
1534
- }
1535
- catch (error) {
1536
- logger.error(`Error: ${getErrorMessage(error)}`);
1537
- if (process.env.DEBUG && error instanceof Error) {
1538
- console.error(error.stack);
1539
- }
1540
- process.exit(1);
1541
- }
1542
- });
1543
- // maker diagrams: generate Mermaid diagrams for all Photons in a directory
1544
- maker
1545
- .command('diagrams')
1546
- .option('--dir <path>', 'Directory to scan (defaults to current directory)')
1547
- .description('Generate Mermaid diagrams for all Photons in a directory')
1548
- .action(async (options) => {
1549
- try {
1550
- const { PhotonDocExtractor } = await import('./photon-doc-extractor.js');
1551
- const dirPath = path.resolve(options.dir || '.');
1552
- const files = await fs.readdir(dirPath);
1553
- const photonFiles = files.filter((f) => f.endsWith('.photon.ts'));
1554
- if (photonFiles.length === 0) {
1555
- console.error('No .photon.ts files found');
1556
- process.exit(1);
1557
- }
1558
- console.error(`📦 Found ${photonFiles.length} photons\n`);
1559
- for (const file of photonFiles) {
1560
- const photonPath = path.join(dirPath, file);
1561
- const name = file.replace('.photon.ts', '');
1562
- try {
1563
- const extractor = new PhotonDocExtractor(photonPath);
1564
- const diagram = await extractor.generateDiagram();
1565
- console.log(`## ${name}\n`);
1566
- console.log('```mermaid');
1567
- console.log(diagram);
1568
- console.log('```\n');
1569
- }
1570
- catch (err) {
1571
- console.error(`⚠️ Failed to generate diagram for ${name}: ${err.message}`);
1572
- }
1573
- }
1574
- }
1575
- catch (error) {
1576
- logger.error(`Error: ${getErrorMessage(error)}`);
1577
- if (process.env.DEBUG && error instanceof Error) {
1578
- console.error(error.stack);
1579
- }
1580
- process.exit(1);
1581
- }
1582
- });
1583
- // Register marketplace commands (list, add, remove, enable, disable)
1584
- registerMarketplaceCommands(program);
1585
- // Register info command
1586
- registerInfoCommand(program, DEFAULT_WORKING_DIR);
1587
- // Register package management commands
1588
- registerPackageCommands(program, DEFAULT_WORKING_DIR);
1589
- // Register package-app command (cross-platform PWA launchers)
1590
- registerPackageAppCommand(program, DEFAULT_WORKING_DIR);
1591
- // Doctor command: diagnose photon environment
1592
- program
1593
- .command('doctor')
1594
- .argument('[name]', 'Photon name to diagnose (checks environment if omitted)')
1595
- .description('Run diagnostics on photon environment, ports, and configuration')
1596
- .option('--port <number>', 'Port to check for availability', '3000')
1597
- .action(async (name, options, command) => {
1598
- try {
1599
- const { formatOutput, printHeader, printInfo, printSuccess, printWarning, STATUS } = await import('./cli-formatter.js');
1600
- const workingDir = command.parent?.opts().dir || DEFAULT_WORKING_DIR;
1601
- const diagnostics = {};
1602
- const suggestions = [];
1603
- let issuesFound = 0;
1604
- printHeader('Photon Doctor');
1605
- printInfo('Running environment checks...\n');
1606
- // Node runtime
1607
- const nodeVersion = process.version;
1608
- const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
1609
- diagnostics['Node.js'] = {
1610
- version: nodeVersion,
1611
- status: majorVersion >= 18 ? STATUS.OK : STATUS.ERROR,
1612
- };
1613
- if (majorVersion < 18) {
1614
- issuesFound++;
1615
- suggestions.push('Upgrade to Node.js 18+ (https://nodejs.org).');
1616
- }
1617
- // npm availability
1618
- try {
1619
- const { execSync } = await import('child_process');
1620
- const npmVersion = execSync('npm --version', { encoding: 'utf-8' }).trim();
1621
- diagnostics['Package manager'] = { npm: npmVersion, status: STATUS.OK };
1622
- }
1623
- catch {
1624
- diagnostics['Package manager'] = { npm: 'not found', status: STATUS.ERROR };
1625
- issuesFound++;
1626
- suggestions.push('Install npm / npx so Photon can install dependencies.');
1627
- }
1628
- // Working directory health
1629
- try {
1630
- await fs.access(workingDir);
1631
- const stats = await fs.stat(workingDir);
1632
- if (stats.isDirectory()) {
1633
- const mcps = await listPhotonMCPs(workingDir);
1634
- diagnostics['Working directory'] = {
1635
- path: workingDir,
1636
- status: STATUS.OK,
1637
- photons: mcps.length,
1638
- };
1639
- }
1640
- else {
1641
- diagnostics['Working directory'] = {
1642
- path: workingDir,
1643
- status: STATUS.ERROR,
1644
- note: 'Not a directory',
1645
- };
1646
- issuesFound++;
1647
- suggestions.push(`Fix working directory: rm ${workingDir} && mkdir -p ${workingDir}`);
1648
- }
1649
- }
1650
- catch {
1651
- diagnostics['Working directory'] = {
1652
- path: workingDir,
1653
- status: STATUS.WARN,
1654
- note: 'Will be created on first use',
1655
- };
1656
- }
1657
- // Cache directory insight
1658
- const cacheDir = path.join(os.homedir(), '.cache', 'photon-mcp', 'compiled');
1659
- try {
1660
- await fs.access(cacheDir);
1661
- const files = await fs.readdir(cacheDir);
1662
- diagnostics['Cache directory'] = {
1663
- path: cacheDir,
1664
- status: STATUS.OK,
1665
- cachedFiles: files.length,
1666
- };
1667
- }
1668
- catch {
1669
- diagnostics['Cache directory'] = {
1670
- path: cacheDir,
1671
- status: STATUS.UNKNOWN,
1672
- note: 'Created on demand',
1673
- };
1674
- }
1675
- // Port availability
1676
- const port = parseInt(options.port, 10);
1677
- const available = await isPortAvailable(port);
1678
- diagnostics['Ports'] = {
1679
- port,
1680
- status: available ? STATUS.OK : STATUS.ERROR,
1681
- note: available ? 'Available' : 'In use by another process',
1682
- };
1683
- if (!available) {
1684
- issuesFound++;
1685
- suggestions.push(`Port ${port} is busy. Run Photon with '--port ${port + 1}' or stop the conflicting service.`);
1686
- }
1687
- // Marketplace configuration
1688
- try {
1689
- const { MarketplaceManager } = await import('./marketplace-manager.js');
1690
- const manager = new MarketplaceManager();
1691
- await manager.initialize();
1692
- const enabled = manager.getAll().filter((m) => m.enabled);
1693
- if (enabled.length === 0) {
1694
- diagnostics['Marketplaces'] = {
1695
- status: STATUS.WARN,
1696
- note: 'No marketplaces configured. Add one with: photon marketplace add portel-dev/photons',
1697
- };
1698
- suggestions.push('Add at least one marketplace so you can install community photons.');
1699
- }
1700
- else {
1701
- const conflicts = await manager.detectAllConflicts();
1702
- diagnostics['Marketplaces'] = {
1703
- status: conflicts.size > 0 ? STATUS.WARN : STATUS.OK,
1704
- enabled: enabled.map((m) => m.name),
1705
- conflicts: conflicts.size,
1706
- };
1707
- if (conflicts.size > 0) {
1708
- issuesFound++;
1709
- suggestions.push('Resolve duplicate photons with: photon marketplace resolve');
1710
- }
1711
- }
1712
- }
1713
- catch (error) {
1714
- diagnostics['Marketplaces'] = { status: STATUS.ERROR, error: getErrorMessage(error) };
1715
- suggestions.push('Marketplace config failed to load. Run photon marketplace list to debug.');
1716
- issuesFound++;
1717
- }
1718
- // Photon-specific checks
1719
- if (name) {
1720
- const photonSection = {};
1721
- const filePath = await resolvePhotonPath(name, workingDir);
1722
- if (!filePath) {
1723
- photonSection.status = STATUS.ERROR;
1724
- photonSection.note = `Not installed in ${workingDir}`;
1725
- suggestions.push(`Install ${name} with: photon add ${name}`);
1726
- issuesFound++;
1727
- }
1728
- else {
1729
- photonSection.status = STATUS.OK;
1730
- photonSection.path = filePath;
1731
- const params = await extractConstructorParams(filePath);
1732
- if (params.length > 0) {
1733
- photonSection.environment = params.map((param) => {
1734
- const envVar = toEnvVarName(name, param.name);
1735
- const value = process.env[envVar];
1736
- const ok = Boolean(value) || param.isOptional || param.hasDefault;
1737
- if (!ok) {
1738
- issuesFound++;
1739
- suggestions.push(`Set ${envVar} for ${name} (e.g. export ${envVar}=value).`);
1740
- }
1741
- return {
1742
- name: envVar,
1743
- status: ok ? STATUS.OK : STATUS.ERROR,
1744
- value: value
1745
- ? 'configured'
1746
- : param.hasDefault
1747
- ? `default: ${formatDefaultValue(param.defaultValue)}`
1748
- : 'missing',
1749
- };
1750
- });
1751
- }
1752
- const cachedFile = path.join(cacheDir, `${name}.js`);
1753
- try {
1754
- await fs.access(cachedFile);
1755
- photonSection.cache = { status: STATUS.OK, note: 'Warm' };
1756
- }
1757
- catch {
1758
- photonSection.cache = {
1759
- status: STATUS.WARN,
1760
- note: 'Not compiled yet (first run will compile)',
1761
- };
1762
- }
1763
- }
1764
- diagnostics[`Photon: ${name}`] = photonSection;
1765
- }
1766
- formatOutput(diagnostics, 'tree');
1767
- if (suggestions.length > 0) {
1768
- printHeader('Suggested fixes');
1769
- suggestions.forEach((tip, idx) => console.log(` ${idx + 1}. ${tip}`));
1770
- }
1771
- if (issuesFound === 0 && suggestions.length === 0) {
1772
- printSuccess('\nAll checks passed!');
1773
- }
1774
- else {
1775
- printWarning(`\nDetected ${issuesFound || suggestions.length} potential issue(s).`);
1776
- }
1777
- }
1778
- catch (error) {
1779
- const { printError } = await import('./cli-formatter.js');
1780
- printError(getErrorMessage(error));
1781
- process.exit(1);
1782
- }
1783
- });
1784
- // CLI command: directly invoke photon methods
1785
- // Also serves as escape hatch for photons with reserved names (e.g., photon cli list get)
1786
- program
1787
- .command('cli <photon> [method] [args...]')
1788
- .description('Run photon methods from command line (escape hatch for reserved names)')
1789
- .allowUnknownOption()
1790
- .helpOption(false) // Disable default help so we can handle it ourselves
1791
- .action(async (photon, method, args) => {
1792
- // Handle help flag
1793
- if (photon === '--help' || photon === '-h') {
1794
- console.log(`USAGE:
1795
- photon <photon-name> [method] [args...]
1796
- photon cli <photon-name> [method] [args...] (explicit form)
1797
-
1798
- DESCRIPTION:
1799
- Run photon methods directly from the command line. Photons provide
1800
- a CLI interface automatically based on their exported methods.
1801
-
1802
- The 'cli' command is optional - you can run photons directly:
1803
- photon lg-remote volume +5 (implicit)
1804
- photon cli lg-remote volume +5 (explicit)
1805
-
1806
- Use 'photon cli' explicitly when your photon name conflicts with
1807
- a reserved command (serve, beam, list, init, etc.)
1808
-
1809
- EXAMPLES:
1810
- # List all methods for a photon
1811
- photon lg-remote
1812
-
1813
- # Call a method with no parameters
1814
- photon lg-remote status
1815
-
1816
- # Call a method with parameters
1817
- photon lg-remote volume 50
1818
- photon lg-remote volume +5
1819
- photon spotify play
1820
-
1821
- # Get method-specific help
1822
- photon lg-remote volume --help
1823
-
1824
- # Output raw JSON instead of formatted text
1825
- photon lg-remote status --json
1826
-
1827
- # Escape hatch for reserved-name photons
1828
- photon cli list get (photon named "list", method "get")
1829
- photon cli serve status (photon named "serve", method "status")
1830
-
1831
- SEE ALSO:
1832
- photon list List all installed photons
1833
- photon add <name> Install a photon from marketplace
1834
- `);
1835
- return;
1836
- }
1837
- const { listMethods, runMethod } = await import('./photon-cli-runner.js');
1838
- if (!method) {
1839
- // List all methods
1840
- await listMethods(photon);
1841
- }
1842
- else {
1843
- // Run specific method
1844
- await runMethod(photon, method, args);
1845
- }
1846
- });
1847
- // Use command: switch to a named instance of a stateful photon
1848
- program
1849
- .command('use')
1850
- .argument('<photon>', 'Photon name')
1851
- .argument('[instance]', 'Instance name (omit for default)')
1852
- .description('Switch to a named instance of a stateful photon')
1853
- .action(async (photonName, instance) => {
1854
- try {
1855
- const { CLISessionStore } = await import('./context-store.js');
1856
- // Write to CLI session store only — each client manages its own instance
1857
- new CLISessionStore().setCurrentInstance(photonName, instance || '');
1858
- const label = instance || 'default';
1859
- printSuccess(`${photonName} → instance: ${label}`);
1860
- // Refresh completions cache (picks up new instance)
1861
- try {
1862
- const { generateCompletionCache } = await import('./shell-completions.js');
1863
- await generateCompletionCache();
1864
- }
1865
- catch {
1866
- // Best-effort: don't break the use command if cache refresh fails
1867
- }
1868
- }
1869
- catch (error) {
1870
- printError(getErrorMessage(error));
1871
- process.exit(1);
1872
- }
1873
- });
1874
- // Instances command: list all instances of a stateful photon
1875
- program
1876
- .command('instances')
1877
- .argument('<photon>', 'Photon name')
1878
- .description('List all instances of a stateful photon')
1879
- .action(async (photonName) => {
1880
- try {
1881
- const { InstanceStore } = await import('./context-store.js');
1882
- const store = new InstanceStore();
1883
- const instances = store.listInstances(photonName);
1884
- const current = store.getCurrentInstance(photonName) || 'default';
1885
- if (instances.length === 0) {
1886
- printInfo(`No instances found for ${photonName}.`);
1887
- return;
1888
- }
1889
- cliHeading(`${photonName} — Instances`);
1890
- cliSpacer();
1891
- for (const name of instances) {
1892
- const marker = name === current ? ' ← current' : '';
1893
- console.log(` ${name}${marker}`);
1894
- }
1895
- cliSpacer();
1896
- }
1897
- catch (error) {
1898
- printError(getErrorMessage(error));
1899
- process.exit(1);
1900
- }
1901
- });
1902
- // Shell command group: shell integration utilities
1903
- const shell = program.command('shell').description('Shell integration utilities');
1904
- shell
1905
- .command('init')
1906
- .option('--hook', 'Output the shell hook script (used internally by eval/Invoke-Expression)')
1907
- .description('Set up shell integration for direct photon commands and tab completion')
1908
- .action(async (options) => {
1909
- // Detect shell type
1910
- const userShell = process.env.SHELL || '';
1911
- const isPowerShell = !!process.env.PSModulePath;
1912
- const isZsh = !isPowerShell && userShell.includes('zsh');
1913
- const isBash = !isPowerShell && userShell.includes('bash');
1914
- let shellType = 'unsupported';
1915
- if (isZsh)
1916
- shellType = 'zsh';
1917
- else if (isBash)
1918
- shellType = 'bash';
1919
- else if (isPowerShell || process.platform === 'win32')
1920
- shellType = 'powershell';
1921
- // Unsupported shell — show supported list and exit
1922
- if (shellType === 'unsupported') {
1923
- const detected = userShell ? path.basename(userShell) : 'unknown';
1924
- printError(`Unsupported shell: ${detected}`);
1925
- console.log('');
1926
- console.log(' Supported shells:');
1927
- console.log(' zsh ~/.zshrc (macOS default)');
1928
- console.log(' bash ~/.bashrc (Linux default)');
1929
- console.log(' PowerShell $PROFILE (Windows default, cross-platform)');
1930
- console.log('');
1931
- console.log(' To use a specific shell, set $SHELL and retry:');
1932
- console.log(' SHELL=/bin/zsh photon shell init');
1933
- process.exit(1);
1934
- }
1935
- // RC file and eval/invoke line per shell
1936
- let rcFile;
1937
- let evalLine;
1938
- const marker = '# photon shell integration';
1939
- if (shellType === 'powershell') {
1940
- // PowerShell profile path: cross-platform
1941
- rcFile =
1942
- process.platform === 'win32'
1943
- ? path.join(os.homedir(), 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1')
1944
- : path.join(os.homedir(), '.config', 'powershell', 'Microsoft.PowerShell_profile.ps1');
1945
- evalLine = 'Invoke-Expression (& photon shell init --hook)';
1946
- }
1947
- else {
1948
- rcFile = isZsh ? path.join(os.homedir(), '.zshrc') : path.join(os.homedir(), '.bashrc');
1949
- evalLine = 'eval "$(photon shell init --hook)"';
1950
- }
1951
- // --hook flag: output the hook script
1952
- if (options.hook) {
1953
- const photonDir = path.join(os.homedir(), '.photon');
1954
- let photonNames = [];
1955
- try {
1956
- const entries = await fs.readdir(photonDir);
1957
- photonNames = entries
1958
- .filter((e) => /\.photon\.(ts|js)$/.test(e))
1959
- .map((e) => e.replace(/\.photon\.(ts|js)$/, ''));
1960
- }
1961
- catch {
1962
- // ~/.photon/ doesn't exist yet
1963
- }
1964
- if (shellType === 'zsh') {
1965
- const functions = photonNames
1966
- .map((name) => `${name}() { photon cli ${name} "$@"; }`)
1967
- .join('\n');
1968
- console.log(`${marker}
1969
-
1970
- # Shell functions for installed photons (direct invocation)
1971
- ${functions}
1972
-
1973
- # Fallback for newly installed photons (before shell restart)
1974
- command_not_found_handler() {
1975
- if [ -f "$HOME/.photon/\$1.photon.ts" ] || [ -f "$HOME/.photon/\$1.photon.js" ]; then
1976
- photon cli "$@"
1977
- return $?
1978
- fi
1979
- echo "zsh: command not found: \$1" >&2
1980
- return 127
1981
- }
1982
-
1983
- # Tab completion for photon methods, params, and instances
1984
- _photon_cache="$HOME/.photon/cache/completions.cache"
1985
-
1986
- _photon_complete_direct() {
1987
- local cmd="\$words[1]"
1988
- local curcontext="\$curcontext" state line
1989
- _arguments -C "1: :->method" "*::arg:->params"
1990
- case "\$state" in
1991
- method)
1992
- if [[ -f "\$_photon_cache" ]]; then
1993
- local -a methods
1994
- methods=("\${(@f)$(grep "^method:\${cmd}:" "\$_photon_cache" | while IFS=: read -r _ _ name desc; do echo "\${name}:\${desc}"; done)}")
1995
- _describe 'method' methods
1996
- fi
1997
- ;;
1998
- params)
1999
- if [[ -f "\$_photon_cache" ]]; then
2000
- local method="\$line[1]"
2001
- local -a params
2002
- params=("\${(@f)$(grep "^param:\${cmd}:\${method}:" "\$_photon_cache" | while IFS=: read -r _ _ _ name type req; do echo "--\${name}[\${type}]"; done)}")
2003
- _describe 'parameter' params
2004
- fi
2005
- ;;
2006
- esac
2007
- }
2008
-
2009
- # Register completion for each photon function (guard for non-interactive shells)
2010
- if (( $+functions[compdef] )); then
2011
- ${photonNames.map((name) => ` compdef _photon_complete_direct ${name}`).join('\n')}
2012
- fi
2013
-
2014
- # Completion for the photon command itself
2015
- _photon() {
2016
- local curcontext="\$curcontext" state line
2017
- _arguments -C \\
2018
- "1: :->cmds" \\
2019
- "*::arg:->args"
2020
- case "\$state" in
2021
- cmds)
2022
- local -a builtins
2023
- builtins=(
2024
- 'cli:Run a photon method'
2025
- 'use:Switch to a named instance'
2026
- 'instances:List instances of a photon'
2027
- 'set:Configure environment for a photon'
2028
- 'beam:Start the interactive UI'
2029
- 'serve:Start MCP stdio server'
2030
- 'list:List installed photons'
2031
- 'add:Install a photon'
2032
- 'remove:Uninstall a photon'
2033
- 'search:Search for photons'
2034
- 'info:Show photon details'
2035
- 'shell:Shell integration'
2036
- 'test:Run photon tests'
2037
- 'doctor:Check system health'
2038
- )
2039
- _describe 'command' builtins
2040
- ;;
2041
- args)
2042
- case \$line[1] in
2043
- cli)
2044
- local curcontext="\$curcontext" state line
2045
- _arguments -C "1: :->photon_name" "*::arg:->method_args"
2046
- case "\$state" in
2047
- photon_name)
2048
- if [[ -f "\$_photon_cache" ]]; then
2049
- local -a photons
2050
- photons=("\${(@f)$(grep "^photon:" "\$_photon_cache" | while IFS=: read -r _ name desc; do echo "\${name}:\${desc}"; done)}")
2051
- _describe 'photon' photons
2052
- fi
2053
- ;;
2054
- method_args)
2055
- words[1]="\$line[1]"
2056
- _photon_complete_direct
2057
- ;;
2058
- esac
2059
- ;;
2060
- use|instances|set|info|serve)
2061
- if [[ -f "\$_photon_cache" ]]; then
2062
- local curcontext="\$curcontext" state line
2063
- _arguments -C "1: :->photon_name" "*::arg:->instance"
2064
- case "\$state" in
2065
- photon_name)
2066
- local -a photons
2067
- photons=("\${(@f)$(grep "^photon:" "\$_photon_cache" | while IFS=: read -r _ name desc; do echo "\${name}:\${desc}"; done)}")
2068
- _describe 'photon' photons
2069
- ;;
2070
- instance)
2071
- if [[ "\$line[-2]" == "use" ]]; then
2072
- local -a instances
2073
- instances=("\${(@f)$(grep "^instance:\${line[1]}:" "\$_photon_cache" | cut -d: -f3)}")
2074
- [[ \${#instances} -gt 0 ]] && _describe 'instance' instances
2075
- fi
2076
- ;;
2077
- esac
2078
- fi
2079
- ;;
2080
- shell)
2081
- local -a subcmds
2082
- subcmds=('init:Set up shell integration' 'completions:Manage completion cache')
2083
- _describe 'subcommand' subcmds
2084
- ;;
2085
- esac
2086
- ;;
2087
- esac
2088
- }
2089
-
2090
- if (( $+functions[compdef] )); then
2091
- compdef _photon photon
2092
- fi`);
2093
- }
2094
- else if (shellType === 'bash') {
2095
- const functions = photonNames
2096
- .map((name) => `${name}() { photon cli ${name} "$@"; }`)
2097
- .join('\n');
2098
- console.log(`${marker}
2099
-
2100
- # Shell functions for installed photons (direct invocation)
2101
- ${functions}
2102
-
2103
- # Fallback for newly installed photons (before shell restart)
2104
- command_not_found_handle() {
2105
- if [ -f "$HOME/.photon/\$1.photon.ts" ] || [ -f "$HOME/.photon/\$1.photon.js" ]; then
2106
- photon cli "$@"
2107
- return $?
2108
- fi
2109
- echo "bash: \$1: command not found" >&2
2110
- return 127
2111
- }
2112
-
2113
- # Tab completion for photon methods, params, and instances
2114
- _photon_cache="$HOME/.photon/cache/completions.cache"
2115
-
2116
- _photon_complete_direct() {
2117
- local cur="\${COMP_WORDS[COMP_CWORD]}"
2118
- local cmd="\${COMP_WORDS[0]}"
2119
- COMPREPLY=()
2120
-
2121
- if [[ ! -f "\$_photon_cache" ]]; then return; fi
2122
-
2123
- if [[ \$COMP_CWORD -eq 1 ]]; then
2124
- local methods
2125
- methods="$(grep "^method:\${cmd}:" "\$_photon_cache" | cut -d: -f3)"
2126
- COMPREPLY=($(compgen -W "\$methods" -- "\$cur"))
2127
- elif [[ \$COMP_CWORD -eq 2 ]]; then
2128
- local method="\${COMP_WORDS[1]}"
2129
- local params
2130
- params="$(grep "^param:\${cmd}:\${method}:" "\$_photon_cache" | cut -d: -f4 | sed 's/^/--/')"
2131
- COMPREPLY=($(compgen -W "\$params" -- "\$cur"))
2132
- fi
2133
- }
2134
-
2135
- _photon_complete() {
2136
- local cur="\${COMP_WORDS[COMP_CWORD]}"
2137
- local prev="\${COMP_WORDS[COMP_CWORD-1]}"
2138
- COMPREPLY=()
2139
-
2140
- if [[ ! -f "\$_photon_cache" ]]; then return; fi
2141
-
2142
- if [[ \$COMP_CWORD -eq 1 ]]; then
2143
- COMPREPLY=($(compgen -W "cli use instances set beam serve list add remove search info shell test doctor" -- "\$cur"))
2144
- elif [[ \$COMP_CWORD -eq 2 ]]; then
2145
- case "\${COMP_WORDS[1]}" in
2146
- cli|use|instances|set|info|serve)
2147
- local photons
2148
- photons="$(grep "^photon:" "\$_photon_cache" | cut -d: -f2)"
2149
- COMPREPLY=($(compgen -W "\$photons" -- "\$cur"))
2150
- ;;
2151
- shell)
2152
- COMPREPLY=($(compgen -W "init completions" -- "\$cur"))
2153
- ;;
2154
- esac
2155
- elif [[ \$COMP_CWORD -eq 3 ]]; then
2156
- case "\${COMP_WORDS[1]}" in
2157
- cli)
2158
- local methods
2159
- methods="$(grep "^method:\${COMP_WORDS[2]}:" "\$_photon_cache" | cut -d: -f3)"
2160
- COMPREPLY=($(compgen -W "\$methods" -- "\$cur"))
2161
- ;;
2162
- use)
2163
- local instances
2164
- instances="$(grep "^instance:\${COMP_WORDS[2]}:" "\$_photon_cache" | cut -d: -f3)"
2165
- COMPREPLY=($(compgen -W "\$instances" -- "\$cur"))
2166
- ;;
2167
- esac
2168
- elif [[ \$COMP_CWORD -ge 4 && "\${COMP_WORDS[1]}" == "cli" ]]; then
2169
- local params
2170
- params="$(grep "^param:\${COMP_WORDS[2]}:\${COMP_WORDS[3]}:" "\$_photon_cache" | cut -d: -f4 | sed 's/^/--/')"
2171
- COMPREPLY=($(compgen -W "\$params" -- "\$cur"))
2172
- fi
2173
- }
2174
-
2175
- # Register completions
2176
- ${photonNames.map((name) => `complete -F _photon_complete_direct ${name}`).join('\n')}
2177
- complete -F _photon_complete photon`);
2178
- }
2179
- else if (shellType === 'powershell') {
2180
- // PowerShell functions and completion
2181
- const functions = photonNames
2182
- .map((name) => `function ${name} { photon cli ${name} @Args }`)
2183
- .join('\n');
2184
- const functionNames = photonNames.map((n) => `'${n}'`).join(', ');
2185
- console.log(`${marker}
2186
-
2187
- # Functions for installed photons (direct invocation)
2188
- ${functions}
2189
-
2190
- # Fallback for newly installed photons (CommandNotFoundAction, PowerShell 7.4+)
2191
- if ($PSVersionTable.PSVersion.Major -ge 7 -and $PSVersionTable.PSVersion.Minor -ge 4) {
2192
- $ExecutionContext.InvokeCommand.CommandNotFoundAction = {
2193
- param($Name, $EventArgs)
2194
- $photonFile = Join-Path $HOME ".photon" "$Name.photon.ts"
2195
- $photonFileJs = Join-Path $HOME ".photon" "$Name.photon.js"
2196
- if ((Test-Path $photonFile) -or (Test-Path $photonFileJs)) {
2197
- $EventArgs.CommandScriptBlock = { photon cli $Name @Args }.GetNewClosure()
2198
- $EventArgs.StopSearch = $true
2199
- }
2200
- }
2201
- }
2202
-
2203
- # Tab completion for photon methods, params, and instances
2204
- $_photonCache = Join-Path $HOME ".photon" "cache" "completions.cache"
2205
-
2206
- # Completion for direct photon commands
2207
- ${photonNames
2208
- .map((name) => `Register-ArgumentCompleter -CommandName ${name} -ScriptBlock {
2209
- param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
2210
- if (-not (Test-Path $_photonCache)) { return }
2211
- $pos = $commandAst.CommandElements.Count
2212
- if ($pos -le 1) {
2213
- # Complete method names
2214
- Get-Content $_photonCache | Where-Object { $_ -match "^method:\${commandName}:" } | ForEach-Object {
2215
- $parts = $_ -split ':', 4
2216
- [System.Management.Automation.CompletionResult]::new($parts[2], $parts[2], 'ParameterValue', ($parts[3] ?? $parts[2]))
2217
- } | Where-Object { $_.CompletionText -like "$wordToComplete*" }
2218
- } elseif ($pos -le 2) {
2219
- # Complete parameter names
2220
- $method = $commandAst.CommandElements[1].Value
2221
- Get-Content $_photonCache | Where-Object { $_ -match "^param:\${commandName}:\${method}:" } | ForEach-Object {
2222
- $parts = $_ -split ':', 6
2223
- $paramName = "--$($parts[3])"
2224
- [System.Management.Automation.CompletionResult]::new($paramName, $paramName, 'ParameterName', "$($parts[4]) parameter")
2225
- } | Where-Object { $_.CompletionText -like "$wordToComplete*" }
2226
- }
2227
- }`)
2228
- .join('\n')}
2229
-
2230
- # Completion for the photon command itself
2231
- Register-ArgumentCompleter -CommandName photon -ScriptBlock {
2232
- param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
2233
- $pos = $commandAst.CommandElements.Count
2234
- if ($pos -le 1) {
2235
- @('cli','use','instances','set','beam','serve','list','add','remove','search','info','shell','test','doctor') |
2236
- Where-Object { $_ -like "$wordToComplete*" } |
2237
- ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
2238
- } elseif ($pos -le 2) {
2239
- $sub = $commandAst.CommandElements[1].Value
2240
- switch ($sub) {
2241
- { $_ -in 'cli','use','instances','set','info','serve' } {
2242
- if (Test-Path $_photonCache) {
2243
- Get-Content $_photonCache | Where-Object { $_ -match "^photon:" } | ForEach-Object {
2244
- $parts = $_ -split ':', 3
2245
- [System.Management.Automation.CompletionResult]::new($parts[1], $parts[1], 'ParameterValue', ($parts[2] ?? $parts[1]))
2246
- } | Where-Object { $_.CompletionText -like "$wordToComplete*" }
2247
- }
2248
- }
2249
- 'shell' {
2250
- @('init','completions') | Where-Object { $_ -like "$wordToComplete*" } |
2251
- ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
2252
- }
2253
- }
2254
- } elseif ($pos -le 3) {
2255
- $sub = $commandAst.CommandElements[1].Value
2256
- $photonName = $commandAst.CommandElements[2].Value
2257
- if ($sub -eq 'cli' -and (Test-Path $_photonCache)) {
2258
- Get-Content $_photonCache | Where-Object { $_ -match "^method:\${photonName}:" } | ForEach-Object {
2259
- $parts = $_ -split ':', 4
2260
- [System.Management.Automation.CompletionResult]::new($parts[2], $parts[2], 'ParameterValue', ($parts[3] ?? $parts[2]))
2261
- } | Where-Object { $_.CompletionText -like "$wordToComplete*" }
2262
- } elseif ($sub -eq 'use' -and (Test-Path $_photonCache)) {
2263
- Get-Content $_photonCache | Where-Object { $_ -match "^instance:\${photonName}:" } | ForEach-Object {
2264
- $parts = $_ -split ':', 3
2265
- [System.Management.Automation.CompletionResult]::new($parts[2], $parts[2], 'ParameterValue', $parts[2])
2266
- } | Where-Object { $_.CompletionText -like "$wordToComplete*" }
2267
- }
2268
- }
2269
- }`);
2270
- }
2271
- // Silently generate cache on first hook (non-blocking)
2272
- const { CACHE_FILE } = await import('./shell-completions.js');
2273
- try {
2274
- await fs.access(CACHE_FILE);
2275
- }
2276
- catch {
2277
- // Cache doesn't exist yet — generate it
2278
- const { generateCompletionCache } = await import('./shell-completions.js');
2279
- await generateCompletionCache();
2280
- }
2281
- return;
2282
- }
2283
- // Interactive mode → install into rc file
2284
- try {
2285
- // Ensure profile directory exists (PowerShell profile dir may not)
2286
- const rcDir = path.dirname(rcFile);
2287
- await fs.mkdir(rcDir, { recursive: true });
2288
- let rcContent = '';
2289
- try {
2290
- rcContent = await fs.readFile(rcFile, 'utf-8');
2291
- }
2292
- catch {
2293
- // rc file doesn't exist, we'll create it
2294
- }
2295
- if (rcContent.includes(marker) || rcContent.includes(evalLine)) {
2296
- printInfo(`Shell integration already installed in ${rcFile}`);
2297
- if (shellType === 'powershell') {
2298
- console.log(` Restart PowerShell or run: . $PROFILE`);
2299
- }
2300
- else {
2301
- console.log(` Restart your shell or run: source ${rcFile}`);
2302
- }
2303
- return;
2304
- }
2305
- const block = `\n${marker}\n${evalLine}\n`;
2306
- await fs.appendFile(rcFile, block);
2307
- // Generate completions cache
2308
- const { generateCompletionCache } = await import('./shell-completions.js');
2309
- await generateCompletionCache();
2310
- printSuccess(`Installed shell integration into ${rcFile}`);
2311
- if (shellType === 'powershell') {
2312
- console.log(` Restart PowerShell or run: . $PROFILE`);
2313
- }
2314
- else {
2315
- console.log(` Restart your shell or run: source ${rcFile}`);
2316
- }
2317
- console.log('');
2318
- console.log(` Then type any photon name directly:`);
2319
- console.log(` list get → photon cli list get`);
2320
- console.log(` list add "Milk" → photon cli list add "Milk"`);
2321
- console.log('');
2322
- console.log(' Tab completion is enabled for:');
2323
- console.log(' Photon names, methods, parameters, and instances.');
2324
- }
2325
- catch (error) {
2326
- printError(`Failed to update ${rcFile}: ${getErrorMessage(error)}`);
2327
- console.log(` Add this line manually to your shell profile:`);
2328
- console.log(` ${evalLine}`);
2329
- process.exit(1);
2330
- }
2331
- });
2332
- shell
2333
- .command('completions')
2334
- .option('--generate', 'Regenerate the completions cache')
2335
- .description('Manage shell completion cache')
2336
- .action(async (options) => {
2337
- const { generateCompletionCache, CACHE_FILE } = await import('./shell-completions.js');
2338
- if (options.generate) {
2339
- await generateCompletionCache();
2340
- printSuccess(`Completions cache updated: ${CACHE_FILE}`);
2341
- return;
2342
- }
2343
- // Default: show cache status
2344
- try {
2345
- const stat = await fs.stat(CACHE_FILE);
2346
- const age = Date.now() - stat.mtimeMs;
2347
- const ageStr = age < 60_000
2348
- ? 'just now'
2349
- : age < 3_600_000
2350
- ? `${Math.floor(age / 60_000)}m ago`
2351
- : `${Math.floor(age / 3_600_000)}h ago`;
2352
- printInfo(`Cache: ${CACHE_FILE}`);
2353
- console.log(` Last updated: ${ageStr}`);
2354
- console.log(` Run \`photon shell completions --generate\` to refresh`);
2355
- }
2356
- catch {
2357
- printInfo('No completions cache found.');
2358
- console.log(' Run `photon shell completions --generate` to create one.');
2359
- }
2360
- });
2361
- // Set command: configure environment for photons (primitive params without defaults)
2362
- program
2363
- .command('set')
2364
- .argument('<photon>', 'Photon name')
2365
- .argument('[args...]', 'Environment values (name=value pairs)')
2366
- .description('Configure environment for a photon (params without defaults)')
2367
- .action(async (photonName, args) => {
2368
- try {
2369
- const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
2370
- // Resolve photon path
2371
- const filePath = await resolvePhotonPathWithBundled(photonName, workingDir);
2372
- if (!filePath) {
2373
- printError(`Photon not found: ${photonName}`);
2374
- process.exit(1);
2375
- }
2376
- // Extract constructor params and filter env params
2377
- const allParams = await extractConstructorParams(filePath);
2378
- const { getEnvParams, EnvStore } = await import('./context-store.js');
2379
- const envParams = getEnvParams(allParams);
2380
- if (envParams.length === 0) {
2381
- printInfo(`${photonName} has no environment parameters.`);
2382
- return;
2383
- }
2384
- const store = new EnvStore();
2385
- // Parse name=value pairs from args
2386
- const values = {};
2387
- const paramNames = new Set(envParams.map((p) => p.name));
2388
- for (const arg of args) {
2389
- const eqIdx = arg.indexOf('=');
2390
- if (eqIdx > 0) {
2391
- const key = arg.slice(0, eqIdx);
2392
- const val = arg.slice(eqIdx + 1);
2393
- if (paramNames.has(key)) {
2394
- values[key] = val;
2395
- }
2396
- }
2397
- else if (envParams.length === 1) {
2398
- // Single env param: positional value
2399
- values[envParams[0].name] = arg;
2400
- }
2401
- }
2402
- // Find params that still need values
2403
- const remaining = envParams.filter((p) => !(p.name in values));
2404
- if (remaining.length > 0) {
2405
- // Interactive mode for remaining params
2406
- cliHeading(`${photonName} — Environment`);
2407
- cliSpacer();
2408
- const masked = store.getMasked(photonName);
2409
- for (const param of remaining) {
2410
- const currentDisplay = masked[param.name] ? `Current: ${masked[param.name]}` : 'Not set';
2411
- const answer = await promptText(` ${param.name} (required)\n ${currentDisplay}\n > `);
2412
- if (answer.trim() !== '') {
2413
- values[param.name] = answer.trim();
2414
- }
2415
- }
2416
- }
2417
- if (Object.keys(values).length > 0) {
2418
- store.write(photonName, values);
2419
- const summary = Object.keys(values).join(', ');
2420
- printSuccess(`Environment saved: ${summary}`);
2421
- }
2422
- else {
2423
- printInfo('No changes.');
2424
- }
2425
- }
2426
- catch (error) {
2427
- printError(getErrorMessage(error));
2428
- process.exit(1);
2429
- }
2430
- });
2431
- // Alias commands: create CLI shortcuts for photons
2432
- program
2433
- .command('alias', { hidden: true })
2434
- .argument('<photon>', 'Photon to create alias for')
2435
- .argument('[alias-name]', 'Custom alias name (defaults to photon name)')
2436
- .description('Create a CLI alias for a photon')
2437
- .action(async (photon, aliasName) => {
2438
- const { createAlias } = await import('./cli-alias.js');
2439
- await createAlias(photon, aliasName);
2440
- });
2441
- program
2442
- .command('unalias', { hidden: true })
2443
- .argument('<alias-name>', 'Alias to remove')
2444
- .description('Remove a CLI alias')
2445
- .action(async (aliasName) => {
2446
- const { removeAlias } = await import('./cli-alias.js');
2447
- await removeAlias(aliasName);
2448
- });
2449
- program
2450
- .command('aliases', { hidden: true })
2451
- .description('List all CLI aliases')
2452
- .action(async () => {
2453
- const { listAliases } = await import('./cli-alias.js');
2454
- await listAliases();
2455
- });
2456
- // Test command: run tests for photons
2457
- program
2458
- .command('test')
2459
- .argument('[photon]', 'Photon to test (tests all if omitted)')
2460
- .argument('[test]', 'Specific test to run')
2461
- .option('--json', 'Output results as JSON')
2462
- .option('--mode <mode>', 'Test mode: direct (unit), cli (integration via CLI), mcp (integration via MCP), all', 'direct')
2463
- .description('Run test methods in photons')
2464
- .action(async (photon, test, options) => {
2465
- try {
2466
- const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
2467
- const { runTests } = await import('./test-runner.js');
2468
- // Validate mode
2469
- const validModes = ['direct', 'cli', 'mcp', 'all'];
2470
- if (!validModes.includes(options.mode)) {
2471
- logger.error(`Invalid mode: ${options.mode}. Valid modes: ${validModes.join(', ')}`);
2472
- process.exit(1);
2473
- }
2474
- const summary = await runTests(workingDir, photon, test, {
2475
- json: options.json,
2476
- mode: options.mode,
2477
- });
2478
- // Exit with error code if any tests failed
2479
- if (summary.failed > 0) {
2480
- process.exit(1);
2481
- }
2482
- }
2483
- catch (error) {
2484
- logger.error(`Error: ${getErrorMessage(error)}`);
2485
- process.exit(1);
2486
- }
2487
- });
2488
- // Reserved commands that should NOT be treated as photon names
2489
- // Reserved commands that should NOT be treated as photon names
2490
- // If first arg is not in this list, it's assumed to be a photon name (implicit CLI mode)
2491
- const RESERVED_COMMANDS = [
2492
- // Core commands
2493
- 'serve',
2494
- 'sse',
2495
- 'beam',
2496
- 'list',
2497
- 'ls',
2498
- 'info',
2499
- 'test',
2500
- // Photon management
2501
- 'new',
2502
- 'init',
2503
- 'validate',
2504
- 'sync',
2505
- 'add',
2506
- 'remove',
2507
- 'rm',
2508
- // Maintenance
2509
- 'upgrade',
2510
- 'up',
2511
- 'update',
2512
- 'doctor',
2513
- 'clear-cache',
2514
- 'clean',
2515
- // Instance/env
2516
- 'use',
2517
- 'instances',
2518
- 'set',
2519
- // Aliases
2520
- 'cli',
2521
- 'alias',
2522
- 'unalias',
2523
- 'aliases',
2524
- // Marketplace
2525
- 'marketplace',
2526
- // Packaging
2527
- 'package',
2528
- // Hidden/advanced
2529
- 'mcp',
2530
- 'search',
2531
- 'maker',
2532
- 'host',
2533
- 'shell',
2534
- 'diagram',
2535
- 'diagrams',
2536
- 'enable',
2537
- 'disable',
2538
- // Help/version (handled by commander)
2539
- 'help',
2540
- '--help',
2541
- '-h',
2542
- 'version',
2543
- '--version',
2544
- '-V',
2545
- ];
2546
- // All known commands for "did you mean" suggestions
2547
- const knownCommands = [
2548
- 'serve',
2549
- 'sse',
2550
- 'beam',
2551
- 'list',
2552
- 'ls',
2553
- 'info',
2554
- 'test',
2555
- 'new',
2556
- 'init',
2557
- 'validate',
2558
- 'sync',
2559
- 'add',
2560
- 'remove',
2561
- 'rm',
2562
- 'upgrade',
2563
- 'up',
2564
- 'update',
2565
- 'clear-cache',
2566
- 'clean',
2567
- 'doctor',
2568
- 'use',
2569
- 'instances',
2570
- 'set',
2571
- 'cli',
2572
- 'alias',
2573
- 'unalias',
2574
- 'aliases',
2575
- 'mcp',
2576
- 'search',
2577
- 'marketplace',
2578
- 'maker',
2579
- 'host',
2580
- 'shell',
2581
- 'diagram',
2582
- 'diagrams',
2583
- ];
2584
- const knownSubcommands = {
2585
- marketplace: ['list', 'add', 'remove', 'enable', 'disable'],
2586
- maker: ['new', 'validate', 'sync', 'init'],
2587
- shell: ['init', 'completions'],
2588
- };
2589
- /**
2590
- * Calculate Levenshtein distance between two strings
2591
- */
2592
- function levenshteinDistance(a, b) {
2593
- const matrix = [];
2594
- for (let i = 0; i <= b.length; i++) {
2595
- matrix[i] = [i];
2596
- }
2597
- for (let j = 0; j <= a.length; j++) {
2598
- matrix[0][j] = j;
2599
- }
2600
- for (let i = 1; i <= b.length; i++) {
2601
- for (let j = 1; j <= a.length; j++) {
2602
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
2603
- matrix[i][j] = matrix[i - 1][j - 1];
2604
- }
2605
- else {
2606
- matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
2607
- matrix[i][j - 1] + 1, // insertion
2608
- matrix[i - 1][j] + 1 // deletion
2609
- );
2610
- }
2611
- }
2612
- }
2613
- return matrix[b.length][a.length];
2614
- }
2615
- /**
2616
- * Find closest matching command
2617
- */
2618
- function findClosestCommand(input, commands) {
2619
- let closest = null;
2620
- let minDistance = Infinity;
2621
- for (const cmd of commands) {
2622
- const distance = levenshteinDistance(input.toLowerCase(), cmd.toLowerCase());
2623
- // Only suggest if distance is small enough (max 3 edits for short commands, proportional for longer)
2624
- const maxDistance = Math.max(2, Math.floor(cmd.length / 2));
2625
- if (distance < minDistance && distance <= maxDistance) {
2626
- minDistance = distance;
2627
- closest = cmd;
2628
- }
2629
- }
2630
- return closest;
2631
- }
2632
- // Handle unknown commands with "did you mean" suggestions
2633
- program.on('command:*', async (operands) => {
2634
- const { printError, printInfo } = await import('./cli-formatter.js');
2635
- const unknownCommand = operands[0];
2636
- printError(`Unknown command: ${unknownCommand}`);
2637
- // Check if it's a subcommand typo for a known parent
2638
- const args = process.argv.slice(2);
2639
- const parentIndex = args.findIndex((arg) => knownSubcommands[arg]);
2640
- if (parentIndex !== -1 && parentIndex < args.indexOf(unknownCommand)) {
2641
- const parent = args[parentIndex];
2642
- const suggestion = findClosestCommand(unknownCommand, knownSubcommands[parent]);
2643
- if (suggestion) {
2644
- printInfo(`Did you mean: photon ${parent} ${suggestion}`);
2645
- }
2646
- }
2647
- else {
2648
- // Check for top-level command typo
2649
- const suggestion = findClosestCommand(unknownCommand, knownCommands);
2650
- if (suggestion) {
2651
- printInfo(`Did you mean: photon ${suggestion}`);
2652
- }
2653
- }
2654
- console.log('');
2655
- printInfo(`Run 'photon --help' for usage`);
2656
- process.exit(1);
2657
- });
2658
- // ══════════════════════════════════════════════════════════════════════════════
2659
- // IMPLICIT CLI MODE
2660
- // ══════════════════════════════════════════════════════════════════════════════
2661
- // If the first argument is not a reserved command, treat it as a photon name
2662
- // This enables: `photon lg-remote volume +5` instead of `photon cli lg-remote volume +5`
2663
- function preprocessArgs() {
2664
- const args = process.argv.slice(2);
2665
- // No args - launch Beam (the primary interface)
2666
- // Use `photon -h` or `photon --help` for help
2667
- if (args.length === 0) {
2668
- return [...process.argv, 'beam'];
2669
- }
2670
- // Find the first non-flag argument (skip values of flags that take a parameter)
2671
- const flagsWithValues = ['--dir', '--log-level'];
2672
- const firstArgIndex = args.findIndex((arg, i) => {
2673
- if (arg.startsWith('-'))
2674
- return false;
2675
- // Skip values of preceding flags (e.g., "." in "--dir .")
2676
- if (i > 0 && flagsWithValues.includes(args[i - 1]))
2677
- return false;
2678
- return true;
2679
- });
2680
- if (firstArgIndex === -1) {
2681
- // No subcommand — only flags present (e.g., --dir=. --log-level debug)
2682
- // photon --help / -h / --version / -V → show program help/version
2683
- if (args.some((a) => a === '--help' || a === '-h' || a === '--version' || a === '-V')) {
2684
- return process.argv;
2685
- }
2686
- // Otherwise launch Beam (e.g., photon --dir=.)
2687
- return [...process.argv, 'beam'];
2688
- }
2689
- const firstArg = args[firstArgIndex];
2690
- // If first arg is a reserved command, let commander handle normally
2691
- if (RESERVED_COMMANDS.includes(firstArg)) {
2692
- return process.argv;
2693
- }
2694
- // First arg looks like a photon name - inject 'cli' command
2695
- // photon lg-remote volume +5 → photon cli lg-remote volume +5
2696
- const newArgs = [...process.argv];
2697
- newArgs.splice(2 + firstArgIndex, 0, 'cli');
2698
- return newArgs;
2699
- }
2700
- program.parse(preprocessArgs());
2701
- /**
2702
- * Inline template fallback
2703
- */
2704
- function getInlineTemplate() {
2705
- return `/**
2706
- * TemplateName Photon MCP
2707
- *
2708
- * Single-file MCP server using Photon
2709
- */
2710
-
2711
- export default class TemplateName {
2712
- /**
2713
- * Example tool
2714
- * @param message Message to echo
2715
- */
2716
- async echo(params: { message: string }) {
2717
- return \`Echo: \${params.message}\`;
2718
- }
2719
-
2720
- /**
2721
- * Add two numbers
2722
- * @param a First number
2723
- * @param b Second number
2724
- */
2725
- async add(params: { a: number; b: number }) {
2726
- return params.a + params.b;
2727
- }
2728
- }
2729
- `;
2730
- }
8
+ import { main } from './cli/index.js';
9
+ void main();
2731
10
  //# sourceMappingURL=cli.js.map