@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.
- package/README.md +163 -210
- package/dist/async/dedup-map.d.ts +40 -0
- package/dist/async/dedup-map.d.ts.map +1 -0
- package/dist/async/dedup-map.js +80 -0
- package/dist/async/dedup-map.js.map +1 -0
- package/dist/async/index.d.ts +11 -0
- package/dist/async/index.d.ts.map +1 -0
- package/dist/async/index.js +11 -0
- package/dist/async/index.js.map +1 -0
- package/dist/async/loading-gate.d.ts +27 -0
- package/dist/async/loading-gate.d.ts.map +1 -0
- package/dist/async/loading-gate.js +48 -0
- package/dist/async/loading-gate.js.map +1 -0
- package/dist/async/with-timeout.d.ts +6 -0
- package/dist/async/with-timeout.d.ts.map +1 -0
- package/dist/async/with-timeout.js +17 -0
- package/dist/async/with-timeout.js.map +1 -0
- package/dist/auto-ui/beam/class-metadata.d.ts +52 -0
- package/dist/auto-ui/beam/class-metadata.d.ts.map +1 -0
- package/dist/auto-ui/beam/class-metadata.js +133 -0
- package/dist/auto-ui/beam/class-metadata.js.map +1 -0
- package/dist/auto-ui/beam/config.d.ts +13 -0
- package/dist/auto-ui/beam/config.d.ts.map +1 -0
- package/dist/auto-ui/beam/config.js +52 -0
- package/dist/auto-ui/beam/config.js.map +1 -0
- package/dist/auto-ui/beam/external-mcp.d.ts +37 -0
- package/dist/auto-ui/beam/external-mcp.d.ts.map +1 -0
- package/dist/auto-ui/beam/external-mcp.js +311 -0
- package/dist/auto-ui/beam/external-mcp.js.map +1 -0
- package/dist/auto-ui/beam/photon-management.d.ts +51 -0
- package/dist/auto-ui/beam/photon-management.d.ts.map +1 -0
- package/dist/auto-ui/beam/photon-management.js +310 -0
- package/dist/auto-ui/beam/photon-management.js.map +1 -0
- package/dist/auto-ui/beam/routes/api-browse.d.ts +17 -0
- package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -0
- package/dist/auto-ui/beam/routes/api-browse.js +531 -0
- package/dist/auto-ui/beam/routes/api-browse.js.map +1 -0
- package/dist/auto-ui/beam/routes/api-config.d.ts +9 -0
- package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -0
- package/dist/auto-ui/beam/routes/api-config.js +494 -0
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -0
- package/dist/auto-ui/beam/routes/api-marketplace.d.ts +8 -0
- package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -0
- package/dist/auto-ui/beam/routes/api-marketplace.js +490 -0
- package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -0
- package/dist/auto-ui/beam/startup.d.ts +41 -0
- package/dist/auto-ui/beam/startup.d.ts.map +1 -0
- package/dist/auto-ui/beam/startup.js +98 -0
- package/dist/auto-ui/beam/startup.js.map +1 -0
- package/dist/auto-ui/beam/subscription.d.ts +35 -0
- package/dist/auto-ui/beam/subscription.d.ts.map +1 -0
- package/dist/auto-ui/beam/subscription.js +151 -0
- package/dist/auto-ui/beam/subscription.js.map +1 -0
- package/dist/auto-ui/beam/types.d.ts +103 -0
- package/dist/auto-ui/beam/types.d.ts.map +1 -0
- package/dist/auto-ui/beam/types.js +8 -0
- package/dist/auto-ui/beam/types.js.map +1 -0
- package/dist/auto-ui/beam.d.ts +2 -0
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +729 -2596
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.d.ts.map +1 -1
- package/dist/auto-ui/bridge/index.js +10 -2
- package/dist/auto-ui/bridge/index.js.map +1 -1
- package/dist/auto-ui/components/card.d.ts.map +1 -1
- package/dist/auto-ui/components/card.js +3 -1
- package/dist/auto-ui/components/card.js.map +1 -1
- package/dist/auto-ui/components/progress.d.ts.map +1 -1
- package/dist/auto-ui/components/progress.js.map +1 -1
- package/dist/auto-ui/daemon-tools.d.ts +1 -1
- package/dist/auto-ui/daemon-tools.d.ts.map +1 -1
- package/dist/auto-ui/daemon-tools.js +4 -3
- package/dist/auto-ui/daemon-tools.js.map +1 -1
- package/dist/auto-ui/photon-bridge.d.ts +6 -2
- package/dist/auto-ui/photon-bridge.d.ts.map +1 -1
- package/dist/auto-ui/photon-bridge.js +20 -8
- package/dist/auto-ui/photon-bridge.js.map +1 -1
- package/dist/auto-ui/platform-compat.d.ts.map +1 -1
- package/dist/auto-ui/platform-compat.js +4 -0
- package/dist/auto-ui/platform-compat.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +4 -2
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +120 -30
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +4 -2
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +8225 -3999
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/alias.d.ts +14 -0
- package/dist/cli/commands/alias.d.ts.map +1 -0
- package/dist/cli/commands/alias.js +41 -0
- package/dist/cli/commands/alias.js.map +1 -0
- package/dist/cli/commands/audit.d.ts +9 -0
- package/dist/cli/commands/audit.d.ts.map +1 -0
- package/dist/cli/commands/audit.js +377 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/beam.d.ts +20 -0
- package/dist/cli/commands/beam.d.ts.map +1 -0
- package/dist/cli/commands/beam.js +256 -0
- package/dist/cli/commands/beam.js.map +1 -0
- package/dist/cli/commands/config.d.ts +14 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +165 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/daemon.d.ts +11 -0
- package/dist/cli/commands/daemon.d.ts.map +1 -0
- package/dist/cli/commands/daemon.js +108 -0
- package/dist/cli/commands/daemon.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +257 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/host.d.ts +11 -0
- package/dist/cli/commands/host.d.ts.map +1 -0
- package/dist/cli/commands/host.js +96 -0
- package/dist/cli/commands/host.js.map +1 -0
- package/dist/cli/commands/info.d.ts +1 -1
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +16 -15
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +774 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/maker.d.ts +12 -0
- package/dist/cli/commands/maker.d.ts.map +1 -0
- package/dist/cli/commands/maker.js +605 -0
- package/dist/cli/commands/maker.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +27 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +390 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/package-app.d.ts +1 -1
- package/dist/cli/commands/package-app.d.ts.map +1 -1
- package/dist/cli/commands/package-app.js +5 -4
- package/dist/cli/commands/package-app.js.map +1 -1
- package/dist/cli/commands/package.d.ts +1 -1
- package/dist/cli/commands/package.d.ts.map +1 -1
- package/dist/cli/commands/package.js +134 -32
- package/dist/cli/commands/package.js.map +1 -1
- package/dist/cli/commands/run.d.ts +34 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +334 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/search.d.ts +11 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +60 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +11 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +138 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/test.d.ts +14 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/test.js +51 -0
- package/dist/cli/commands/test.js.map +1 -0
- package/dist/cli/commands/update.d.ts +11 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +72 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +139 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli-alias.js +2 -2
- package/dist/cli-alias.js.map +1 -1
- package/dist/cli.d.ts +3 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -2725
- package/dist/cli.js.map +1 -1
- package/dist/context-store.d.ts +13 -12
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +47 -23
- package/dist/context-store.js.map +1 -1
- package/dist/context.d.ts +35 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +38 -0
- package/dist/context.js.map +1 -0
- package/dist/daemon/client.d.ts +25 -13
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +183 -135
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +58 -26
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +348 -157
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/protocol.d.ts +9 -3
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js +2 -0
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/server.js +850 -200
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +16 -2
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +65 -7
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/daemon/state-machine.d.ts +22 -0
- package/dist/daemon/state-machine.d.ts.map +1 -0
- package/dist/daemon/state-machine.js +48 -0
- package/dist/daemon/state-machine.js.map +1 -0
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +5 -5
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/loader.d.ts +65 -7
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +587 -63
- package/dist/loader.js.map +1 -1
- package/dist/marketplace-manager.d.ts +84 -12
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +470 -26
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/path-resolver.d.ts +3 -1
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +4 -3
- package/dist/path-resolver.js.map +1 -1
- package/dist/photon-cli-runner.d.ts +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +34 -44
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +1 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +33 -12
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/maker.photon.d.ts.map +1 -1
- package/dist/photons/maker.photon.js +4 -4
- package/dist/photons/maker.photon.js.map +1 -1
- package/dist/photons/maker.photon.ts +4 -3
- package/dist/photons/marketplace.photon.d.ts.map +1 -1
- package/dist/photons/marketplace.photon.js +10 -27
- package/dist/photons/marketplace.photon.js.map +1 -1
- package/dist/photons/marketplace.photon.ts +14 -33
- package/dist/photons/tunnel.photon.d.ts.map +1 -1
- package/dist/photons/tunnel.photon.js +4 -8
- package/dist/photons/tunnel.photon.js.map +1 -1
- package/dist/photons/tunnel.photon.ts +4 -7
- package/dist/serv/session/kv-store.d.ts +1 -1
- package/dist/serv/session/kv-store.d.ts.map +1 -1
- package/dist/serv/session/store.d.ts.map +1 -1
- package/dist/serv/session/store.js +16 -14
- package/dist/serv/session/store.js.map +1 -1
- package/dist/serv/vault/token-vault.js +1 -1
- package/dist/serv/vault/token-vault.js.map +1 -1
- package/dist/server.d.ts +34 -12
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +364 -313
- package/dist/server.js.map +1 -1
- package/dist/shared/audit.d.ts +30 -0
- package/dist/shared/audit.d.ts.map +1 -0
- package/dist/shared/audit.js +89 -0
- package/dist/shared/audit.js.map +1 -0
- package/dist/shared/cli-sections.d.ts +0 -4
- package/dist/shared/cli-sections.d.ts.map +1 -1
- package/dist/shared/cli-sections.js +0 -6
- package/dist/shared/cli-sections.js.map +1 -1
- package/dist/shared/cli-utils.d.ts +2 -56
- package/dist/shared/cli-utils.d.ts.map +1 -1
- package/dist/shared/cli-utils.js +1 -87
- package/dist/shared/cli-utils.js.map +1 -1
- package/dist/shared/error-handler.d.ts +6 -72
- package/dist/shared/error-handler.d.ts.map +1 -1
- package/dist/shared/error-handler.js +22 -213
- package/dist/shared/error-handler.js.map +1 -1
- package/dist/shared/security.d.ts +0 -9
- package/dist/shared/security.d.ts.map +1 -1
- package/dist/shared/security.js +0 -30
- package/dist/shared/security.js.map +1 -1
- package/dist/shared-utils.d.ts +0 -26
- package/dist/shared-utils.d.ts.map +1 -1
- package/dist/shared-utils.js +0 -44
- package/dist/shared-utils.js.map +1 -1
- package/dist/shell-completions.d.ts +1 -1
- package/dist/shell-completions.d.ts.map +1 -1
- package/dist/shell-completions.js +5 -5
- package/dist/shell-completions.js.map +1 -1
- package/dist/template-manager.d.ts.map +1 -1
- package/dist/template-manager.js +14 -1
- package/dist/template-manager.js.map +1 -1
- package/dist/test-runner.d.ts +0 -12
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +4 -39
- package/dist/test-runner.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/dist/version-checker.d.ts +4 -4
- package/dist/version-checker.d.ts.map +1 -1
- package/dist/version-checker.js +33 -4
- package/dist/version-checker.js.map +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +14 -12
- package/dist/watcher.js.map +1 -1
- package/package.json +24 -17
package/dist/cli.js
CHANGED
|
@@ -2,2730 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Photon MCP CLI
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Thin entry point — delegates to cli/index.ts which registers
|
|
6
|
+
* all command modules and handles argv preprocessing.
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
-
|
|
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
|