@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
|
@@ -9,9 +9,10 @@ import * as crypto from 'crypto';
|
|
|
9
9
|
import { createLogger } from './shared/logger.js';
|
|
10
10
|
import { getErrorMessage } from './shared/error-handler.js';
|
|
11
11
|
import { verifyContentHash, validateAssetPath, isPathWithin } from './shared/security.js';
|
|
12
|
+
import { getDefaultContext } from './context.js';
|
|
12
13
|
// Timeout for marketplace fetch requests
|
|
13
14
|
const FETCH_TIMEOUT_MS = 10 * 1000;
|
|
14
|
-
const CONFIG_DIR =
|
|
15
|
+
const CONFIG_DIR = getDefaultContext().baseDir;
|
|
15
16
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'marketplaces.json');
|
|
16
17
|
const CACHE_DIR = path.join(CONFIG_DIR, '.cache', 'marketplaces');
|
|
17
18
|
const METADATA_FILE = path.join(CONFIG_DIR, '.metadata.json');
|
|
@@ -49,6 +50,28 @@ export function calculateHash(content) {
|
|
|
49
50
|
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
50
51
|
return `sha256:${hash}`;
|
|
51
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Calculate combined hash of a photon source file + its declared assets.
|
|
55
|
+
* This ensures asset-only changes (e.g. board.html update) are detected.
|
|
56
|
+
*/
|
|
57
|
+
export async function calculatePhotonHash(sourceFilePath, assets, baseDir) {
|
|
58
|
+
const hasher = crypto.createHash('sha256');
|
|
59
|
+
// Always include the source file
|
|
60
|
+
hasher.update(await fs.readFile(sourceFilePath, 'utf-8'));
|
|
61
|
+
// Include each asset file in sorted order for determinism
|
|
62
|
+
if (assets && assets.length > 0 && baseDir) {
|
|
63
|
+
for (const asset of [...assets].sort()) {
|
|
64
|
+
const assetPath = path.join(baseDir, asset);
|
|
65
|
+
try {
|
|
66
|
+
hasher.update(await fs.readFile(assetPath));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Asset missing — skip (will be caught by validation)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return `sha256:${hasher.digest('hex')}`;
|
|
74
|
+
}
|
|
52
75
|
/**
|
|
53
76
|
* Read local installation metadata
|
|
54
77
|
*/
|
|
@@ -65,24 +88,26 @@ export async function readLocalMetadata() {
|
|
|
65
88
|
}
|
|
66
89
|
return { photons: {} };
|
|
67
90
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Write local installation metadata
|
|
70
|
-
*/
|
|
71
|
-
export async function writeLocalMetadata(metadata) {
|
|
72
|
-
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
73
|
-
await fs.writeFile(METADATA_FILE, JSON.stringify(metadata, null, 2), 'utf-8');
|
|
74
|
-
}
|
|
75
91
|
export class MarketplaceManager {
|
|
76
92
|
config = { marketplaces: [] };
|
|
77
93
|
logger;
|
|
78
|
-
|
|
94
|
+
configDir;
|
|
95
|
+
configFile;
|
|
96
|
+
cacheDir;
|
|
97
|
+
metadataFile;
|
|
98
|
+
constructor(logger, baseDir) {
|
|
79
99
|
this.logger = logger ?? createLogger({ component: 'marketplace-manager', minimal: true });
|
|
100
|
+
const dir = baseDir || CONFIG_DIR;
|
|
101
|
+
this.configDir = dir;
|
|
102
|
+
this.configFile = path.join(dir, 'marketplaces.json');
|
|
103
|
+
this.cacheDir = path.join(dir, '.cache', 'marketplaces');
|
|
104
|
+
this.metadataFile = path.join(dir, '.metadata.json');
|
|
80
105
|
}
|
|
81
106
|
async initialize() {
|
|
82
|
-
await fs.mkdir(
|
|
83
|
-
await fs.mkdir(
|
|
84
|
-
if (existsSync(
|
|
85
|
-
const data = await fs.readFile(
|
|
107
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
108
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
109
|
+
if (existsSync(this.configFile)) {
|
|
110
|
+
const data = await fs.readFile(this.configFile, 'utf-8');
|
|
86
111
|
try {
|
|
87
112
|
this.config = JSON.parse(data);
|
|
88
113
|
}
|
|
@@ -103,7 +128,7 @@ export class MarketplaceManager {
|
|
|
103
128
|
}
|
|
104
129
|
}
|
|
105
130
|
async save() {
|
|
106
|
-
await fs.writeFile(
|
|
131
|
+
await fs.writeFile(this.configFile, JSON.stringify(this.config, null, 2), 'utf-8');
|
|
107
132
|
}
|
|
108
133
|
/**
|
|
109
134
|
* Get all marketplaces
|
|
@@ -325,7 +350,7 @@ export class MarketplaceManager {
|
|
|
325
350
|
* Get cache file path for marketplace
|
|
326
351
|
*/
|
|
327
352
|
getCacheFile(marketplaceName) {
|
|
328
|
-
return path.join(
|
|
353
|
+
return path.join(this.cacheDir, `${marketplaceName}.json`);
|
|
329
354
|
}
|
|
330
355
|
/**
|
|
331
356
|
* Fetch photons.json manifest from various sources
|
|
@@ -563,11 +588,30 @@ export class MarketplaceManager {
|
|
|
563
588
|
}
|
|
564
589
|
const metadata = manifest?.photons.find((p) => p.name === mcpName);
|
|
565
590
|
// Security: verify content hash if metadata provides one
|
|
566
|
-
|
|
567
|
-
|
|
591
|
+
// Prefer contentHash (source-only) for download verification; fall back to hash
|
|
592
|
+
let verifyHash = metadata?.contentHash || metadata?.hash;
|
|
593
|
+
if (verifyHash && marketplace.sourceType !== 'local') {
|
|
594
|
+
const expectedHash = verifyHash.replace(/^sha256:/, '');
|
|
568
595
|
if (!verifyContentHash(content, expectedHash)) {
|
|
569
|
-
|
|
570
|
-
|
|
596
|
+
// Hash mismatch may mean local cache is stale — refresh and retry once
|
|
597
|
+
this.logger.info(`Content hash mismatch for ${mcpName} — refreshing manifest cache`);
|
|
598
|
+
const updated = await this.updateMarketplaceCache(marketplace.name);
|
|
599
|
+
if (updated) {
|
|
600
|
+
const freshManifest = await this.getCachedManifest(marketplace.name);
|
|
601
|
+
const freshMeta = freshManifest?.photons.find((p) => p.name === mcpName);
|
|
602
|
+
verifyHash = freshMeta?.contentHash || freshMeta?.hash;
|
|
603
|
+
if (verifyHash) {
|
|
604
|
+
const freshHash = verifyHash.replace(/^sha256:/, '');
|
|
605
|
+
if (!verifyContentHash(content, freshHash)) {
|
|
606
|
+
this.logger.warn(`Content hash mismatch for ${mcpName} after cache refresh — skipping`);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
this.logger.warn(`Content hash mismatch for ${mcpName} — skipping`);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
571
615
|
}
|
|
572
616
|
}
|
|
573
617
|
return { content, marketplace, metadata };
|
|
@@ -783,29 +827,92 @@ export class MarketplaceManager {
|
|
|
783
827
|
}
|
|
784
828
|
return [];
|
|
785
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* Install a photon and its assets to the working directory.
|
|
832
|
+
*
|
|
833
|
+
* This is the canonical installation path — used by both CLI and Beam UI.
|
|
834
|
+
* Handles writing the .photon.ts file, saving metadata, and downloading
|
|
835
|
+
* all declared asset files (UI templates, etc.) with path-traversal protection.
|
|
836
|
+
*
|
|
837
|
+
* @param result - The result from fetchMCP()
|
|
838
|
+
* @param name - Photon name (used for filename)
|
|
839
|
+
* @param workingDir - Directory to install into (e.g., ~/.photon)
|
|
840
|
+
* @returns Installed file path and list of asset paths written
|
|
841
|
+
*/
|
|
842
|
+
async installPhoton(result, name, workingDir) {
|
|
843
|
+
// Inject @forkedFrom tag if not already present
|
|
844
|
+
let content = result.content;
|
|
845
|
+
if (!content.includes('@forkedFrom')) {
|
|
846
|
+
const origin = `${result.marketplace.repo}#${name}`;
|
|
847
|
+
// Insert before the first closing */ of the file-level docblock
|
|
848
|
+
content = content.replace(/(\s*\*\/)/, `\n * @forkedFrom ${origin}$1`);
|
|
849
|
+
}
|
|
850
|
+
// Write the .photon.ts file
|
|
851
|
+
await fs.mkdir(workingDir, { recursive: true });
|
|
852
|
+
const photonPath = path.join(workingDir, `${name}.photon.ts`);
|
|
853
|
+
await fs.writeFile(photonPath, content, 'utf-8');
|
|
854
|
+
const assetsInstalled = [];
|
|
855
|
+
if (result.metadata) {
|
|
856
|
+
// Download and save all declared assets first (before hashing)
|
|
857
|
+
if (result.metadata.assets && result.metadata.assets.length > 0) {
|
|
858
|
+
const assets = await this.fetchAssets(result.marketplace, result.metadata.assets);
|
|
859
|
+
for (const [assetPath, content] of assets) {
|
|
860
|
+
const safePath = validateAssetPath(assetPath);
|
|
861
|
+
const assetTarget = path.join(workingDir, safePath);
|
|
862
|
+
if (!isPathWithin(assetTarget, workingDir))
|
|
863
|
+
continue;
|
|
864
|
+
await fs.mkdir(path.dirname(assetTarget), { recursive: true });
|
|
865
|
+
await fs.writeFile(assetTarget, content, 'utf-8');
|
|
866
|
+
assetsInstalled.push(assetPath);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// Save install metadata — use combined hash (source+assets) for update detection
|
|
870
|
+
// The manifest's `hash` field is the combined hash; fall back to content-only hash
|
|
871
|
+
const combinedHash = result.metadata.hash || calculateHash(result.content);
|
|
872
|
+
await this.savePhotonMetadata(`${name}.photon.ts`, result.marketplace, result.metadata, combinedHash);
|
|
873
|
+
}
|
|
874
|
+
return { photonPath, assetsInstalled };
|
|
875
|
+
}
|
|
786
876
|
/**
|
|
787
877
|
* Save installation metadata for a Photon
|
|
788
878
|
*/
|
|
789
|
-
async savePhotonMetadata(fileName, marketplace, metadata,
|
|
790
|
-
const localMetadata = await
|
|
879
|
+
async savePhotonMetadata(fileName, marketplace, metadata, combinedHash) {
|
|
880
|
+
const localMetadata = await this.readMetadata();
|
|
791
881
|
localMetadata.photons[fileName] = {
|
|
792
882
|
marketplace: marketplace.name,
|
|
793
883
|
marketplaceRepo: marketplace.repo,
|
|
794
884
|
version: metadata.version,
|
|
795
|
-
originalHash:
|
|
885
|
+
originalHash: combinedHash,
|
|
796
886
|
installedAt: new Date().toISOString(),
|
|
797
887
|
};
|
|
798
|
-
await
|
|
888
|
+
await this.writeMetadata(localMetadata);
|
|
799
889
|
}
|
|
800
890
|
/**
|
|
801
891
|
* Get local installation metadata for a Photon
|
|
802
892
|
*/
|
|
803
893
|
async getPhotonInstallMetadata(fileName) {
|
|
804
|
-
const localMetadata = await
|
|
894
|
+
const localMetadata = await this.readMetadata();
|
|
805
895
|
return localMetadata.photons[fileName] || null;
|
|
806
896
|
}
|
|
897
|
+
async readMetadata() {
|
|
898
|
+
try {
|
|
899
|
+
if (existsSync(this.metadataFile)) {
|
|
900
|
+
const data = await fs.readFile(this.metadataFile, 'utf-8');
|
|
901
|
+
return JSON.parse(data);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
catch {
|
|
905
|
+
/* corrupted — start fresh */
|
|
906
|
+
}
|
|
907
|
+
return { photons: {} };
|
|
908
|
+
}
|
|
909
|
+
async writeMetadata(metadata) {
|
|
910
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
911
|
+
await fs.writeFile(this.metadataFile, JSON.stringify(metadata, null, 2), 'utf-8');
|
|
912
|
+
}
|
|
807
913
|
/**
|
|
808
914
|
* Check if a Photon file has been modified since installation
|
|
915
|
+
* Compares combined hash (source + assets) against stored originalHash
|
|
809
916
|
*/
|
|
810
917
|
async isPhotonModified(filePath, fileName) {
|
|
811
918
|
const metadata = await this.getPhotonInstallMetadata(fileName);
|
|
@@ -813,8 +920,31 @@ export class MarketplaceManager {
|
|
|
813
920
|
return false; // No metadata, can't determine
|
|
814
921
|
}
|
|
815
922
|
try {
|
|
816
|
-
|
|
817
|
-
|
|
923
|
+
// Strip @forkedFrom from source (injected during install, not in manifest)
|
|
924
|
+
const rawContent = await fs.readFile(filePath, 'utf-8');
|
|
925
|
+
const strippedContent = rawContent.replace(/\n\s*\*\s*@forkedFrom\s+[^\n]+/, '');
|
|
926
|
+
// Write stripped content to temp for hashing, then compute combined hash
|
|
927
|
+
// For efficiency, hash inline: source (stripped) + assets
|
|
928
|
+
const workingDir = path.dirname(filePath);
|
|
929
|
+
const photonName = fileName.replace(/\.photon\.ts$/, '');
|
|
930
|
+
// Look up assets from manifest
|
|
931
|
+
const photonMeta = await this.getPhotonMetadata(photonName);
|
|
932
|
+
const assets = photonMeta?.metadata.assets;
|
|
933
|
+
const hasher = crypto.createHash('sha256');
|
|
934
|
+
hasher.update(strippedContent);
|
|
935
|
+
if (assets && assets.length > 0) {
|
|
936
|
+
for (const asset of [...assets].sort()) {
|
|
937
|
+
const assetPath = path.join(workingDir, asset);
|
|
938
|
+
try {
|
|
939
|
+
hasher.update(await fs.readFile(assetPath));
|
|
940
|
+
}
|
|
941
|
+
catch {
|
|
942
|
+
// Asset missing — skip
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const hash = `sha256:${hasher.digest('hex')}`;
|
|
947
|
+
return hash !== metadata.originalHash;
|
|
818
948
|
}
|
|
819
949
|
catch {
|
|
820
950
|
return false; // file unreadable → not modified
|
|
@@ -947,6 +1077,320 @@ export class MarketplaceManager {
|
|
|
947
1077
|
recommendation,
|
|
948
1078
|
};
|
|
949
1079
|
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Get marketplace sources suitable as fork targets
|
|
1082
|
+
*/
|
|
1083
|
+
async getForkTargets() {
|
|
1084
|
+
const all = this.getAll();
|
|
1085
|
+
return all
|
|
1086
|
+
.filter((m) => m.sourceType === 'github' && m.repo)
|
|
1087
|
+
.map((m) => ({ name: m.name, repo: m.repo, sourceType: m.sourceType }));
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Fork a photon — remove marketplace tracking, optionally push to a target repo.
|
|
1091
|
+
* Shared logic used by both CLI and Beam.
|
|
1092
|
+
*/
|
|
1093
|
+
async forkPhoton(name, workingDir, options) {
|
|
1094
|
+
const fileName = `${name}.photon.ts`;
|
|
1095
|
+
const filePath = path.join(workingDir, fileName);
|
|
1096
|
+
// Check file exists
|
|
1097
|
+
if (!existsSync(filePath)) {
|
|
1098
|
+
return { success: false, message: `Photon not found: ${name}` };
|
|
1099
|
+
}
|
|
1100
|
+
// Read install metadata
|
|
1101
|
+
const localMetadata = await readLocalMetadata();
|
|
1102
|
+
const installMeta = localMetadata.photons[fileName];
|
|
1103
|
+
if (!installMeta) {
|
|
1104
|
+
return {
|
|
1105
|
+
success: true,
|
|
1106
|
+
message: `${name} is already a local photon (no marketplace tracking)`,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
// Check @forkedFrom tag
|
|
1110
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
1111
|
+
const hasForkedFrom = content.includes('@forkedFrom');
|
|
1112
|
+
// Handle target repo push if specified
|
|
1113
|
+
if (options?.targetRepo || options?.createRepo) {
|
|
1114
|
+
const { execSync } = await import('child_process');
|
|
1115
|
+
// Check gh CLI
|
|
1116
|
+
try {
|
|
1117
|
+
execSync('gh --version', { stdio: 'pipe' });
|
|
1118
|
+
}
|
|
1119
|
+
catch {
|
|
1120
|
+
return {
|
|
1121
|
+
success: false,
|
|
1122
|
+
message: 'GitHub CLI (gh) is required but not installed',
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
if (options.createRepo) {
|
|
1126
|
+
// Create new repo and push
|
|
1127
|
+
try {
|
|
1128
|
+
execSync(`gh repo create ${options.createRepo} --public --confirm`, { stdio: 'pipe' });
|
|
1129
|
+
}
|
|
1130
|
+
catch {
|
|
1131
|
+
// Repo may already exist
|
|
1132
|
+
}
|
|
1133
|
+
const targetRepo = options.createRepo;
|
|
1134
|
+
const tmpDir = path.join(os.tmpdir(), `photon-fork-${Date.now()}`);
|
|
1135
|
+
try {
|
|
1136
|
+
execSync(`gh repo clone ${targetRepo} "${tmpDir}" -- --depth=1`, {
|
|
1137
|
+
stdio: 'pipe',
|
|
1138
|
+
});
|
|
1139
|
+
await fs.copyFile(filePath, path.join(tmpDir, fileName));
|
|
1140
|
+
// Copy assets
|
|
1141
|
+
const photonMeta = await this.getPhotonMetadata(name);
|
|
1142
|
+
if (photonMeta?.metadata.assets) {
|
|
1143
|
+
for (const asset of photonMeta.metadata.assets) {
|
|
1144
|
+
const srcAsset = path.join(workingDir, asset);
|
|
1145
|
+
if (existsSync(srcAsset)) {
|
|
1146
|
+
const dstAsset = path.join(tmpDir, asset);
|
|
1147
|
+
await fs.mkdir(path.dirname(dstAsset), { recursive: true });
|
|
1148
|
+
await fs.copyFile(srcAsset, dstAsset);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
execSync(`cd "${tmpDir}" && git add -A && git commit -m "fork: ${name} photon" && git push origin`, { stdio: 'pipe' });
|
|
1153
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
1154
|
+
}
|
|
1155
|
+
catch (e) {
|
|
1156
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
1157
|
+
return {
|
|
1158
|
+
success: false,
|
|
1159
|
+
message: `Failed to push to new repo: ${e.message}`,
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
else if (options.targetRepo) {
|
|
1164
|
+
// Push to existing repo
|
|
1165
|
+
const tmpDir = path.join(os.tmpdir(), `photon-fork-${Date.now()}`);
|
|
1166
|
+
try {
|
|
1167
|
+
execSync(`gh repo clone ${options.targetRepo} "${tmpDir}" -- --depth=1`, {
|
|
1168
|
+
stdio: 'pipe',
|
|
1169
|
+
});
|
|
1170
|
+
await fs.copyFile(filePath, path.join(tmpDir, fileName));
|
|
1171
|
+
// Copy assets
|
|
1172
|
+
const photonMeta = await this.getPhotonMetadata(name);
|
|
1173
|
+
if (photonMeta?.metadata.assets) {
|
|
1174
|
+
for (const asset of photonMeta.metadata.assets) {
|
|
1175
|
+
const srcAsset = path.join(workingDir, asset);
|
|
1176
|
+
if (existsSync(srcAsset)) {
|
|
1177
|
+
const dstAsset = path.join(tmpDir, asset);
|
|
1178
|
+
await fs.mkdir(path.dirname(dstAsset), { recursive: true });
|
|
1179
|
+
await fs.copyFile(srcAsset, dstAsset);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
execSync(`cd "${tmpDir}" && git add -A && git commit -m "fork: ${name} photon" && git push origin`, { stdio: 'pipe' });
|
|
1184
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
1185
|
+
}
|
|
1186
|
+
catch (e) {
|
|
1187
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
1188
|
+
return {
|
|
1189
|
+
success: false,
|
|
1190
|
+
message: `Failed to push to ${options.targetRepo}: ${e.message}`,
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
// Remove marketplace tracking
|
|
1196
|
+
delete localMetadata.photons[fileName];
|
|
1197
|
+
await this.writeMetadata(localMetadata);
|
|
1198
|
+
const parts = [];
|
|
1199
|
+
parts.push(`${name} is now your own`);
|
|
1200
|
+
if (hasForkedFrom) {
|
|
1201
|
+
parts.push('Origin preserved as @forkedFrom tag');
|
|
1202
|
+
}
|
|
1203
|
+
parts.push('Marketplace update tracking removed');
|
|
1204
|
+
return { success: true, message: parts.join('. ') };
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Contribute a photon back upstream via PR.
|
|
1208
|
+
* Shared logic used by both CLI and Beam.
|
|
1209
|
+
*/
|
|
1210
|
+
async contributePhoton(name, workingDir, options) {
|
|
1211
|
+
const { execSync } = await import('child_process');
|
|
1212
|
+
const fileName = `${name}.photon.ts`;
|
|
1213
|
+
const filePath = path.join(workingDir, fileName);
|
|
1214
|
+
// Check file exists
|
|
1215
|
+
if (!existsSync(filePath)) {
|
|
1216
|
+
return { success: false, message: `Photon not found: ${name}` };
|
|
1217
|
+
}
|
|
1218
|
+
// Check gh CLI
|
|
1219
|
+
try {
|
|
1220
|
+
execSync('gh --version', { stdio: 'pipe' });
|
|
1221
|
+
}
|
|
1222
|
+
catch {
|
|
1223
|
+
return {
|
|
1224
|
+
success: false,
|
|
1225
|
+
message: 'GitHub CLI (gh) is required. Install: https://cli.github.com/',
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
// Check gh auth
|
|
1229
|
+
try {
|
|
1230
|
+
execSync('gh auth status', { stdio: 'pipe' });
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
return {
|
|
1234
|
+
success: false,
|
|
1235
|
+
message: 'GitHub CLI is not authenticated. Run: gh auth login',
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
// Read install metadata
|
|
1239
|
+
const localMetadata = await readLocalMetadata();
|
|
1240
|
+
const installMeta = localMetadata.photons[fileName];
|
|
1241
|
+
if (!installMeta) {
|
|
1242
|
+
return {
|
|
1243
|
+
success: false,
|
|
1244
|
+
message: `${name} was not installed from a marketplace`,
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
if (!installMeta.marketplaceRepo) {
|
|
1248
|
+
return {
|
|
1249
|
+
success: false,
|
|
1250
|
+
message: `No upstream repository found for ${name}`,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
// Check if actually modified
|
|
1254
|
+
const modified = await this.isPhotonModified(filePath, fileName);
|
|
1255
|
+
if (!modified) {
|
|
1256
|
+
return {
|
|
1257
|
+
success: true,
|
|
1258
|
+
message: `${name} has not been modified — nothing to contribute`,
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const repo = installMeta.marketplaceRepo;
|
|
1262
|
+
const branchName = options?.branch || `contribute/${name}-${Date.now()}`;
|
|
1263
|
+
if (options?.dryRun) {
|
|
1264
|
+
return {
|
|
1265
|
+
success: true,
|
|
1266
|
+
message: [
|
|
1267
|
+
`[dry-run] Would:`,
|
|
1268
|
+
` 1. Fork ${repo}`,
|
|
1269
|
+
` 2. Clone fork to temp directory`,
|
|
1270
|
+
` 3. Copy modified ${fileName} (and assets)`,
|
|
1271
|
+
` 4. Create branch ${branchName}`,
|
|
1272
|
+
` 5. Commit and push`,
|
|
1273
|
+
` 6. Create PR to ${repo}`,
|
|
1274
|
+
].join('\n'),
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
// Fork the repo
|
|
1278
|
+
try {
|
|
1279
|
+
execSync(`gh repo fork ${repo} --clone=false`, { stdio: 'pipe' });
|
|
1280
|
+
}
|
|
1281
|
+
catch {
|
|
1282
|
+
// Fork may already exist
|
|
1283
|
+
}
|
|
1284
|
+
// Get fork name
|
|
1285
|
+
const forkJson = execSync('gh api user', { encoding: 'utf-8' });
|
|
1286
|
+
const ghUser = JSON.parse(forkJson).login;
|
|
1287
|
+
const repoName = repo.split('/')[1];
|
|
1288
|
+
const forkRepo = `${ghUser}/${repoName}`;
|
|
1289
|
+
// Clone to temp dir
|
|
1290
|
+
const tmpDir = path.join(os.tmpdir(), `photon-contribute-${Date.now()}`);
|
|
1291
|
+
try {
|
|
1292
|
+
execSync(`gh repo clone ${forkRepo} "${tmpDir}" -- --depth=1`, {
|
|
1293
|
+
stdio: 'pipe',
|
|
1294
|
+
});
|
|
1295
|
+
// Copy modified photon file
|
|
1296
|
+
await fs.copyFile(filePath, path.join(tmpDir, fileName));
|
|
1297
|
+
// Copy assets
|
|
1298
|
+
const photonMeta = await this.getPhotonMetadata(name);
|
|
1299
|
+
if (photonMeta?.metadata.assets) {
|
|
1300
|
+
for (const asset of photonMeta.metadata.assets) {
|
|
1301
|
+
const srcAsset = path.join(workingDir, asset);
|
|
1302
|
+
if (existsSync(srcAsset)) {
|
|
1303
|
+
const dstAsset = path.join(tmpDir, asset);
|
|
1304
|
+
await fs.mkdir(path.dirname(dstAsset), { recursive: true });
|
|
1305
|
+
await fs.copyFile(srcAsset, dstAsset);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
// Create branch, commit, push
|
|
1310
|
+
execSync(`cd "${tmpDir}" && git checkout -b "${branchName}" && git add -A && git commit -m "improve: update ${name} photon" && git push origin "${branchName}"`, { stdio: 'pipe' });
|
|
1311
|
+
// Create PR
|
|
1312
|
+
const prOutput = execSync(`cd "${tmpDir}" && gh pr create --repo "${repo}" --title "Improve ${name} photon" --body "Contributed improvements to ${name} photon via Photon marketplace."`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1313
|
+
// Cleanup temp dir
|
|
1314
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
1315
|
+
const prUrl = prOutput.trim();
|
|
1316
|
+
return {
|
|
1317
|
+
success: true,
|
|
1318
|
+
prUrl,
|
|
1319
|
+
message: `Pull request created: ${prUrl}`,
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
catch (e) {
|
|
1323
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
1324
|
+
return {
|
|
1325
|
+
success: false,
|
|
1326
|
+
message: `Contribute failed: ${e.message}`,
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Resolve and install a photon from a GitHub shorthand reference.
|
|
1332
|
+
* Format: owner/repo (photon name = repo name)
|
|
1333
|
+
* or owner/repo/photon-name
|
|
1334
|
+
*
|
|
1335
|
+
* - Adds the marketplace to config (idempotent)
|
|
1336
|
+
* - Fetches and installs the photon to workingDir
|
|
1337
|
+
* - If already installed, skips fetch
|
|
1338
|
+
* - Returns the resolved photon name
|
|
1339
|
+
*/
|
|
1340
|
+
async fetchAndInstallFromRef(ref, workingDir) {
|
|
1341
|
+
const parts = ref.split('/');
|
|
1342
|
+
let owner, repo, photonName;
|
|
1343
|
+
if (parts.length === 2) {
|
|
1344
|
+
[owner, repo] = parts;
|
|
1345
|
+
photonName = repo;
|
|
1346
|
+
}
|
|
1347
|
+
else if (parts.length === 3) {
|
|
1348
|
+
[owner, repo, photonName] = parts;
|
|
1349
|
+
}
|
|
1350
|
+
else {
|
|
1351
|
+
throw new Error(`Invalid photon reference: ${ref}. Expected owner/repo or owner/repo/photon-name`);
|
|
1352
|
+
}
|
|
1353
|
+
const photonFile = path.join(workingDir, `${photonName}.photon.ts`);
|
|
1354
|
+
// Already installed — skip fetch
|
|
1355
|
+
if (existsSync(photonFile)) {
|
|
1356
|
+
return { photonName, alreadyInstalled: true };
|
|
1357
|
+
}
|
|
1358
|
+
// Add marketplace (idempotent — skips if already added)
|
|
1359
|
+
const repoShorthand = `${owner}/${repo}`;
|
|
1360
|
+
const { marketplace: marketplaceInfo } = await this.add(repoShorthand);
|
|
1361
|
+
// Fetch photon content directly from GitHub raw
|
|
1362
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/${photonName}.photon.ts`;
|
|
1363
|
+
let content;
|
|
1364
|
+
try {
|
|
1365
|
+
const response = await fetch(rawUrl, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
1366
|
+
if (!response.ok) {
|
|
1367
|
+
throw new Error(`HTTP ${response.status}`);
|
|
1368
|
+
}
|
|
1369
|
+
content = await response.text();
|
|
1370
|
+
}
|
|
1371
|
+
catch (err) {
|
|
1372
|
+
throw new Error(`Could not fetch photon '${photonName}' from ${repoShorthand}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1373
|
+
}
|
|
1374
|
+
// Try to get manifest metadata (best-effort)
|
|
1375
|
+
const marketplace = this.get(marketplaceInfo.name);
|
|
1376
|
+
if (!marketplace)
|
|
1377
|
+
throw new Error(`Marketplace not found after add: ${marketplaceInfo.name}`);
|
|
1378
|
+
await this.updateMarketplaceCache(marketplace.name).catch(() => {
|
|
1379
|
+
/* non-fatal */
|
|
1380
|
+
});
|
|
1381
|
+
const manifest = await this.getCachedManifest(marketplace.name);
|
|
1382
|
+
const metadata = manifest?.photons.find((p) => p.name === photonName);
|
|
1383
|
+
// When no manifest exists, use a minimal synthetic metadata so that
|
|
1384
|
+
// .metadata.json is always written (enables update detection via hash comparison)
|
|
1385
|
+
const effectiveMetadata = metadata ?? {
|
|
1386
|
+
name: photonName,
|
|
1387
|
+
version: 'unknown',
|
|
1388
|
+
description: '',
|
|
1389
|
+
source: rawUrl,
|
|
1390
|
+
};
|
|
1391
|
+
await this.installPhoton({ content, marketplace, metadata: effectiveMetadata }, photonName, workingDir);
|
|
1392
|
+
return { photonName, alreadyInstalled: false };
|
|
1393
|
+
}
|
|
950
1394
|
/**
|
|
951
1395
|
* Compare two semver versions
|
|
952
1396
|
* Returns: positive if v1 > v2, negative if v1 < v2, 0 if equal
|