@portel/photon 1.18.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  2. package/dist/auto-ui/beam/routes/api-browse.js +16 -4
  3. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  4. package/dist/auto-ui/beam/routes/api-config.js +4 -4
  5. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  6. package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
  7. package/dist/auto-ui/beam/routes/api-marketplace.js +14 -1
  8. package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
  9. package/dist/auto-ui/beam.d.ts.map +1 -1
  10. package/dist/auto-ui/beam.js +196 -77
  11. package/dist/auto-ui/beam.js.map +1 -1
  12. package/dist/auto-ui/bridge/index.d.ts.map +1 -1
  13. package/dist/auto-ui/bridge/index.js +17 -0
  14. package/dist/auto-ui/bridge/index.js.map +1 -1
  15. package/dist/auto-ui/streamable-http-transport.d.ts +1 -0
  16. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  17. package/dist/auto-ui/streamable-http-transport.js +64 -16
  18. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  19. package/dist/auto-ui/types.d.ts +12 -0
  20. package/dist/auto-ui/types.d.ts.map +1 -1
  21. package/dist/auto-ui/types.js.map +1 -1
  22. package/dist/beam-form.bundle.js +49 -6
  23. package/dist/beam-form.bundle.js.map +2 -2
  24. package/dist/beam.bundle.js +2090 -512
  25. package/dist/beam.bundle.js.map +4 -4
  26. package/dist/capability-negotiator.d.ts +67 -0
  27. package/dist/capability-negotiator.d.ts.map +1 -0
  28. package/dist/capability-negotiator.js +104 -0
  29. package/dist/capability-negotiator.js.map +1 -0
  30. package/dist/channel-manager.d.ts +122 -0
  31. package/dist/channel-manager.d.ts.map +1 -0
  32. package/dist/channel-manager.js +266 -0
  33. package/dist/channel-manager.js.map +1 -0
  34. package/dist/claude-code-plugin.js +1 -1
  35. package/dist/cli/commands/beam.d.ts.map +1 -1
  36. package/dist/cli/commands/beam.js +8 -2
  37. package/dist/cli/commands/beam.js.map +1 -1
  38. package/dist/cli/commands/changelog.d.ts +9 -0
  39. package/dist/cli/commands/changelog.d.ts.map +1 -0
  40. package/dist/cli/commands/changelog.js +133 -0
  41. package/dist/cli/commands/changelog.js.map +1 -0
  42. package/dist/cli/commands/maker.d.ts.map +1 -1
  43. package/dist/cli/commands/maker.js +23 -2
  44. package/dist/cli/commands/maker.js.map +1 -1
  45. package/dist/cli/commands/mcp.d.ts.map +1 -1
  46. package/dist/cli/commands/mcp.js +53 -0
  47. package/dist/cli/commands/mcp.js.map +1 -1
  48. package/dist/cli/commands/package.d.ts.map +1 -1
  49. package/dist/cli/commands/package.js +43 -9
  50. package/dist/cli/commands/package.js.map +1 -1
  51. package/dist/cli/commands/run.d.ts.map +1 -1
  52. package/dist/cli/commands/run.js +1 -0
  53. package/dist/cli/commands/run.js.map +1 -1
  54. package/dist/cli/commands/update.d.ts +3 -2
  55. package/dist/cli/commands/update.d.ts.map +1 -1
  56. package/dist/cli/commands/update.js +50 -43
  57. package/dist/cli/commands/update.js.map +1 -1
  58. package/dist/cli/index.d.ts.map +1 -1
  59. package/dist/cli/index.js +16 -2
  60. package/dist/cli/index.js.map +1 -1
  61. package/dist/cli-alias.js +1 -1
  62. package/dist/cli-alias.js.map +1 -1
  63. package/dist/context-store.d.ts +23 -33
  64. package/dist/context-store.d.ts.map +1 -1
  65. package/dist/context-store.js +147 -97
  66. package/dist/context-store.js.map +1 -1
  67. package/dist/context.d.ts +15 -10
  68. package/dist/context.d.ts.map +1 -1
  69. package/dist/context.js +37 -13
  70. package/dist/context.js.map +1 -1
  71. package/dist/daemon/client.d.ts.map +1 -1
  72. package/dist/daemon/client.js +12 -0
  73. package/dist/daemon/client.js.map +1 -1
  74. package/dist/daemon/server.js +34 -51
  75. package/dist/daemon/server.js.map +1 -1
  76. package/dist/daemon/worker-manager.d.ts.map +1 -1
  77. package/dist/daemon/worker-manager.js +21 -7
  78. package/dist/daemon/worker-manager.js.map +1 -1
  79. package/dist/data-migration.d.ts +27 -0
  80. package/dist/data-migration.d.ts.map +1 -0
  81. package/dist/data-migration.js +307 -0
  82. package/dist/data-migration.js.map +1 -0
  83. package/dist/editor-support/docblock-tag-catalog.d.ts.map +1 -1
  84. package/dist/editor-support/docblock-tag-catalog.js +6 -0
  85. package/dist/editor-support/docblock-tag-catalog.js.map +1 -1
  86. package/dist/loader.d.ts +13 -0
  87. package/dist/loader.d.ts.map +1 -1
  88. package/dist/loader.js +169 -22
  89. package/dist/loader.js.map +1 -1
  90. package/dist/marketplace-manager.d.ts +6 -0
  91. package/dist/marketplace-manager.d.ts.map +1 -1
  92. package/dist/marketplace-manager.js +185 -62
  93. package/dist/marketplace-manager.js.map +1 -1
  94. package/dist/namespace-migration.d.ts +1 -0
  95. package/dist/namespace-migration.d.ts.map +1 -1
  96. package/dist/namespace-migration.js +86 -0
  97. package/dist/namespace-migration.js.map +1 -1
  98. package/dist/photon-cli-runner.d.ts.map +1 -1
  99. package/dist/photon-cli-runner.js +47 -21
  100. package/dist/photon-cli-runner.js.map +1 -1
  101. package/dist/photon-doc-extractor.d.ts +1 -0
  102. package/dist/photon-doc-extractor.d.ts.map +1 -1
  103. package/dist/photon-doc-extractor.js +6 -0
  104. package/dist/photon-doc-extractor.js.map +1 -1
  105. package/dist/readme-syncer.d.ts.map +1 -1
  106. package/dist/readme-syncer.js +6 -1
  107. package/dist/readme-syncer.js.map +1 -1
  108. package/dist/resource-server.d.ts +105 -0
  109. package/dist/resource-server.d.ts.map +1 -0
  110. package/dist/resource-server.js +723 -0
  111. package/dist/resource-server.js.map +1 -0
  112. package/dist/serv/auth/jwt.d.ts +2 -0
  113. package/dist/serv/auth/jwt.d.ts.map +1 -1
  114. package/dist/serv/auth/jwt.js +11 -5
  115. package/dist/serv/auth/jwt.js.map +1 -1
  116. package/dist/serv/vault/token-vault.d.ts +2 -0
  117. package/dist/serv/vault/token-vault.d.ts.map +1 -1
  118. package/dist/serv/vault/token-vault.js +6 -0
  119. package/dist/serv/vault/token-vault.js.map +1 -1
  120. package/dist/server.d.ts +30 -119
  121. package/dist/server.d.ts.map +1 -1
  122. package/dist/server.js +252 -1122
  123. package/dist/server.js.map +1 -1
  124. package/dist/shared/audit.d.ts.map +1 -1
  125. package/dist/shared/audit.js +11 -4
  126. package/dist/shared/audit.js.map +1 -1
  127. package/dist/shared/security.d.ts +10 -0
  128. package/dist/shared/security.d.ts.map +1 -1
  129. package/dist/shared/security.js +27 -0
  130. package/dist/shared/security.js.map +1 -1
  131. package/dist/task-executor.d.ts +69 -0
  132. package/dist/task-executor.d.ts.map +1 -0
  133. package/dist/task-executor.js +182 -0
  134. package/dist/task-executor.js.map +1 -0
  135. package/dist/tasks/store.d.ts.map +1 -1
  136. package/dist/tasks/store.js +6 -2
  137. package/dist/tasks/store.js.map +1 -1
  138. package/dist/types/photon-instance.d.ts +50 -0
  139. package/dist/types/photon-instance.d.ts.map +1 -0
  140. package/dist/types/photon-instance.js +9 -0
  141. package/dist/types/photon-instance.js.map +1 -0
  142. package/dist/types/server-types.d.ts +61 -0
  143. package/dist/types/server-types.d.ts.map +1 -0
  144. package/dist/types/server-types.js +8 -0
  145. package/dist/types/server-types.js.map +1 -0
  146. package/dist/version-notify.d.ts +27 -0
  147. package/dist/version-notify.d.ts.map +1 -0
  148. package/dist/version-notify.js +142 -0
  149. package/dist/version-notify.js.map +1 -0
  150. package/package.json +3 -3
  151. package/dist/auto-ui/bridge/openai-shim.d.ts +0 -20
  152. package/dist/auto-ui/bridge/openai-shim.d.ts.map +0 -1
  153. package/dist/auto-ui/bridge/openai-shim.js +0 -231
  154. package/dist/auto-ui/bridge/openai-shim.js.map +0 -1
  155. package/dist/auto-ui/bridge/photon-app.d.ts +0 -162
  156. package/dist/auto-ui/bridge/photon-app.d.ts.map +0 -1
  157. package/dist/auto-ui/bridge/photon-app.js +0 -460
  158. package/dist/auto-ui/bridge/photon-app.js.map +0 -1
  159. package/dist/auto-ui/daemon-tools.d.ts +0 -45
  160. package/dist/auto-ui/daemon-tools.d.ts.map +0 -1
  161. package/dist/auto-ui/daemon-tools.js +0 -581
  162. package/dist/auto-ui/daemon-tools.js.map +0 -1
  163. package/dist/auto-ui/design-system/index.d.ts +0 -21
  164. package/dist/auto-ui/design-system/index.d.ts.map +0 -1
  165. package/dist/auto-ui/design-system/index.js +0 -27
  166. package/dist/auto-ui/design-system/index.js.map +0 -1
  167. package/dist/auto-ui/design-system/transaction-ui.d.ts +0 -70
  168. package/dist/auto-ui/design-system/transaction-ui.d.ts.map +0 -1
  169. package/dist/auto-ui/design-system/transaction-ui.js +0 -982
  170. package/dist/auto-ui/design-system/transaction-ui.js.map +0 -1
  171. package/dist/auto-ui/playground-server.d.ts +0 -7
  172. package/dist/auto-ui/playground-server.d.ts.map +0 -1
  173. package/dist/auto-ui/playground-server.js +0 -840
  174. package/dist/auto-ui/playground-server.js.map +0 -1
  175. package/dist/auto-ui/rendering/components.d.ts +0 -29
  176. package/dist/auto-ui/rendering/components.d.ts.map +0 -1
  177. package/dist/auto-ui/rendering/components.js +0 -1341
  178. package/dist/auto-ui/rendering/components.js.map +0 -1
  179. package/dist/auto-ui/rendering/field-analyzer.d.ts +0 -104
  180. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +0 -1
  181. package/dist/auto-ui/rendering/field-analyzer.js +0 -447
  182. package/dist/auto-ui/rendering/field-analyzer.js.map +0 -1
  183. package/dist/auto-ui/rendering/field-renderers.d.ts +0 -64
  184. package/dist/auto-ui/rendering/field-renderers.d.ts.map +0 -1
  185. package/dist/auto-ui/rendering/field-renderers.js +0 -317
  186. package/dist/auto-ui/rendering/field-renderers.js.map +0 -1
  187. package/dist/auto-ui/rendering/index.d.ts +0 -28
  188. package/dist/auto-ui/rendering/index.d.ts.map +0 -1
  189. package/dist/auto-ui/rendering/index.js +0 -60
  190. package/dist/auto-ui/rendering/index.js.map +0 -1
  191. package/dist/auto-ui/rendering/layout-selector.d.ts +0 -60
  192. package/dist/auto-ui/rendering/layout-selector.d.ts.map +0 -1
  193. package/dist/auto-ui/rendering/layout-selector.js +0 -476
  194. package/dist/auto-ui/rendering/layout-selector.js.map +0 -1
  195. package/dist/markdown-utils.d.ts +0 -8
  196. package/dist/markdown-utils.d.ts.map +0 -1
  197. package/dist/markdown-utils.js +0 -64
  198. package/dist/markdown-utils.js.map +0 -1
  199. package/dist/mcp-client.d.ts +0 -9
  200. package/dist/mcp-client.d.ts.map +0 -1
  201. package/dist/mcp-client.js +0 -11
  202. package/dist/mcp-client.js.map +0 -1
  203. package/dist/mcp-elicitation.d.ts +0 -32
  204. package/dist/mcp-elicitation.d.ts.map +0 -1
  205. package/dist/mcp-elicitation.js +0 -26
  206. package/dist/mcp-elicitation.js.map +0 -1
  207. package/dist/photons/builder-compass.photon.d.ts +0 -167
  208. package/dist/photons/builder-compass.photon.d.ts.map +0 -1
  209. package/dist/photons/builder-compass.photon.js +0 -816
  210. package/dist/photons/builder-compass.photon.js.map +0 -1
  211. package/dist/photons/builder-compass.photon.ts +0 -1129
  212. package/dist/photons/docs/ui/docs.html +0 -441
  213. package/dist/photons/docs.photon.d.ts +0 -237
  214. package/dist/photons/docs.photon.d.ts.map +0 -1
  215. package/dist/photons/docs.photon.js +0 -483
  216. package/dist/photons/docs.photon.js.map +0 -1
  217. package/dist/photons/docs.photon.ts +0 -536
  218. package/dist/photons/slides.photon.d.ts +0 -212
  219. package/dist/photons/slides.photon.d.ts.map +0 -1
  220. package/dist/photons/slides.photon.js +0 -355
  221. package/dist/photons/slides.photon.js.map +0 -1
  222. package/dist/photons/slides.photon.ts +0 -370
  223. package/dist/photons/spreadsheet/ui/spreadsheet.html +0 -779
  224. package/dist/photons/spreadsheet.photon.d.ts +0 -554
  225. package/dist/photons/spreadsheet.photon.d.ts.map +0 -1
  226. package/dist/photons/spreadsheet.photon.js +0 -1050
  227. package/dist/photons/spreadsheet.photon.js.map +0 -1
  228. package/dist/photons/spreadsheet.photon.ts +0 -1239
  229. package/dist/photons/ui/builder-compass.html +0 -1199
  230. package/dist/photons/ui/builder-compass.photon.html +0 -380
  231. package/dist/security-scanner.d.ts +0 -52
  232. package/dist/security-scanner.d.ts.map +0 -1
  233. package/dist/security-scanner.js +0 -181
  234. package/dist/security-scanner.js.map +0 -1
  235. package/dist/shared/performance.d.ts +0 -65
  236. package/dist/shared/performance.d.ts.map +0 -1
  237. package/dist/shared/performance.js +0 -136
  238. package/dist/shared/performance.js.map +0 -1
@@ -11,6 +11,7 @@ import { createLogger } from './shared/logger.js';
11
11
  import { getErrorMessage } from './shared/error-handler.js';
12
12
  import { verifyContentHash, validateAssetPath, isPathWithin } from './shared/security.js';
13
13
  import { getDefaultContext } from './context.js';
14
+ import { getMetadataPath, resolvePath } from '@portel/photon-core';
14
15
  import { SchemaExtractor } from '@portel/photon-core';
15
16
  // Timeout for marketplace fetch requests
16
17
  const FETCH_TIMEOUT_MS = 10 * 1000;
@@ -32,8 +33,12 @@ function getGitHubToken() {
32
33
  _ghToken = process.env.GITHUB_TOKEN || null;
33
34
  if (!_ghToken) {
34
35
  try {
35
- const { execSync } = require('child_process');
36
- _ghToken = execSync('gh auth token 2>/dev/null', { encoding: 'utf-8' }).trim() || null;
36
+ const { execFileSync } = require('child_process');
37
+ _ghToken =
38
+ execFileSync('gh', ['auth', 'token'], {
39
+ encoding: 'utf-8',
40
+ stdio: ['pipe', 'pipe', 'ignore'],
41
+ }).trim() || null;
37
42
  }
38
43
  catch {
39
44
  _ghToken = null;
@@ -56,7 +61,7 @@ function ghFetch(url, options = {}) {
56
61
  return fetch(url, { ...options, headers });
57
62
  }
58
63
  const CONFIG_DIR = getDefaultContext().baseDir;
59
- const METADATA_FILE = path.join(CONFIG_DIR, '.metadata.json');
64
+ const METADATA_FILE = getMetadataPath();
60
65
  // Cache is considered stale after 24 hours
61
66
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
62
67
  // Built-in marketplaces that ship with the runtime
@@ -126,6 +131,11 @@ export async function readLocalMetadata() {
126
131
  }
127
132
  return { photons: {} };
128
133
  }
134
+ function validateSafeName(name, label) {
135
+ if (/[;&|$`(){}\\\<>!#~\n\r]/.test(name)) {
136
+ throw new Error(`Invalid ${label}: contains unsafe characters`);
137
+ }
138
+ }
129
139
  export class MarketplaceManager {
130
140
  config = { marketplaces: [] };
131
141
  logger;
@@ -138,8 +148,8 @@ export class MarketplaceManager {
138
148
  const dir = baseDir || CONFIG_DIR;
139
149
  this.configDir = dir;
140
150
  this.configFile = path.join(dir, 'marketplaces.json');
141
- this.cacheDir = path.join(dir, '.cache', 'marketplaces');
142
- this.metadataFile = path.join(dir, '.metadata.json');
151
+ this.cacheDir = path.join(getDefaultContext().cacheDir, 'marketplaces');
152
+ this.metadataFile = getMetadataPath(dir);
143
153
  }
144
154
  async initialize() {
145
155
  await fs.mkdir(this.configDir, { recursive: true });
@@ -731,7 +741,7 @@ export class MarketplaceManager {
731
741
  * Safe to call on every startup — only fetches when assets are actually missing.
732
742
  */
733
743
  async repairMissingAssets(workingDir) {
734
- const localMetadata = await readLocalMetadata();
744
+ const localMetadata = await this.readMetadata();
735
745
  let repaired = 0;
736
746
  for (const [fileName, installInfo] of Object.entries(localMetadata.photons)) {
737
747
  const photonName = fileName.replace(/\.photon\.ts$/, '');
@@ -965,6 +975,11 @@ export class MarketplaceManager {
965
975
  }
966
976
  }
967
977
  }
978
+ // Install @photon transitive dependencies from the same marketplace
979
+ const transitiveDeps = await this.installTransitiveDeps(content, result.marketplace, workingDir);
980
+ if (transitiveDeps.length > 0) {
981
+ assetsInstalled.push(...transitiveDeps.map((d) => `@photon ${d}`));
982
+ }
968
983
  return { photonPath, assetsInstalled };
969
984
  }
970
985
  /**
@@ -1004,10 +1019,21 @@ export class MarketplaceManager {
1004
1019
  const deps = extractor.extractPhotonDependencies(content);
1005
1020
  const installed = [];
1006
1021
  for (const dep of deps) {
1022
+ // Resolve dependency name from source
1023
+ let depName;
1007
1024
  if (dep.sourceType === 'local' &&
1008
1025
  (dep.source.startsWith('./') || dep.source.startsWith('../'))) {
1009
- const depFileName = path.basename(dep.source);
1010
- const depName = depFileName.replace(/\.photon\.(ts|js)$/, '');
1026
+ // Relative path: ./chat.photon.ts → chat
1027
+ depName = path.basename(dep.source).replace(/\.photon\.(ts|js)$/, '');
1028
+ }
1029
+ else if (dep.sourceType === 'marketplace' || !dep.source.startsWith('/')) {
1030
+ // Marketplace name: whatsapp, agent-router, courier
1031
+ depName = dep.source;
1032
+ }
1033
+ else {
1034
+ continue; // Absolute paths — skip
1035
+ }
1036
+ {
1011
1037
  if (visited.has(depName))
1012
1038
  continue;
1013
1039
  visited.add(depName);
@@ -1016,6 +1042,9 @@ export class MarketplaceManager {
1016
1042
  const installDir = namespace ? path.join(workingDir, namespace) : workingDir;
1017
1043
  if (existsSync(path.join(installDir, `${depName}.photon.ts`)))
1018
1044
  continue;
1045
+ // Also check flat in workingDir
1046
+ if (existsSync(path.join(workingDir, `${depName}.photon.ts`)))
1047
+ continue;
1019
1048
  // Fetch from the same marketplace
1020
1049
  this.logger.info(`Fetching @photon dependency: ${depName}`);
1021
1050
  const depResult = await this.fetchMCPFromMarketplace(depName, marketplace);
@@ -1123,7 +1152,7 @@ export class MarketplaceManager {
1123
1152
  return { photons: {} };
1124
1153
  }
1125
1154
  async writeMetadata(metadata) {
1126
- await fs.mkdir(this.configDir, { recursive: true });
1155
+ await fs.mkdir(path.dirname(this.metadataFile), { recursive: true });
1127
1156
  await writeJSON(this.metadataFile, metadata);
1128
1157
  }
1129
1158
  /**
@@ -1307,30 +1336,82 @@ export class MarketplaceManager {
1307
1336
  * Shared logic used by both CLI and Beam.
1308
1337
  */
1309
1338
  async forkPhoton(name, workingDir, options) {
1310
- const fileName = `${name}.photon.ts`;
1311
- const filePath = path.join(workingDir, fileName);
1312
- // Check file exists
1313
- if (!existsSync(filePath)) {
1339
+ validateSafeName(name, 'photon name');
1340
+ if (options?.targetRepo)
1341
+ validateSafeName(options.targetRepo, 'target repo');
1342
+ if (options?.createRepo)
1343
+ validateSafeName(options.createRepo, 'repo name');
1344
+ if (options?.newName)
1345
+ validateSafeName(options.newName, 'new photon name');
1346
+ const sourcePath = await resolvePath(name, workingDir);
1347
+ if (!sourcePath) {
1314
1348
  return { success: false, message: `Photon not found: ${name}` };
1315
1349
  }
1316
- // Read install metadata
1317
- const localMetadata = await readLocalMetadata();
1318
- const installMeta = localMetadata.photons[fileName];
1319
- if (!installMeta) {
1350
+ const sourceName = path.basename(sourcePath).replace(/\.photon\.(ts|js)$/, '');
1351
+ const normalizedSourceKey = this.toMetadataKey(sourcePath, workingDir);
1352
+ const requestedNewName = options?.newName?.trim();
1353
+ const localMetadata = await this.readMetadata();
1354
+ const installMeta = localMetadata.photons[normalizedSourceKey];
1355
+ const isLocalSource = !installMeta;
1356
+ const targetName = requestedNewName || sourceName;
1357
+ const suggestedName = `${sourceName}-copy`;
1358
+ if (isLocalSource && !requestedNewName) {
1320
1359
  return {
1321
- success: true,
1322
- message: `${name} is already a local photon (no marketplace tracking)`,
1360
+ success: false,
1361
+ message: `${sourceName} is already local. Choose a new local name to fork it.`,
1362
+ requiresName: true,
1363
+ suggestedName,
1323
1364
  };
1324
1365
  }
1366
+ if (isLocalSource && requestedNewName === sourceName) {
1367
+ return {
1368
+ success: false,
1369
+ message: 'Forking a local photon requires a different new name.',
1370
+ requiresName: true,
1371
+ suggestedName,
1372
+ };
1373
+ }
1374
+ const targetPath = path.join(workingDir, `${targetName}.photon.ts`);
1375
+ const sourceRealPath = await fs.realpath(sourcePath).catch(() => sourcePath);
1376
+ const targetExists = existsSync(targetPath);
1377
+ const targetRealPath = targetExists
1378
+ ? await fs.realpath(targetPath).catch(() => targetPath)
1379
+ : null;
1380
+ if (targetExists && targetRealPath !== sourceRealPath) {
1381
+ return {
1382
+ success: false,
1383
+ message: `A local photon named ${targetName} already exists. Choose a different local name.`,
1384
+ requiresName: true,
1385
+ suggestedName,
1386
+ };
1387
+ }
1388
+ const fileName = `${targetName}.photon.ts`;
1389
+ const filePath = targetPath;
1325
1390
  // Check @forkedFrom tag
1326
- const content = await readText(filePath);
1391
+ const content = await readText(sourcePath);
1327
1392
  const hasForkedFrom = content.includes('@forkedFrom');
1393
+ const sourceAssetDir = path.join(path.dirname(sourcePath), sourceName);
1394
+ const targetAssetDir = path.join(workingDir, targetName);
1395
+ if (targetRealPath !== sourceRealPath && !isLocalSource) {
1396
+ await fs.mkdir(workingDir, { recursive: true });
1397
+ await fs.rename(sourcePath, targetPath);
1398
+ await this.movePhotonAssetDir(sourceAssetDir, targetAssetDir);
1399
+ }
1400
+ else if (targetRealPath !== sourceRealPath) {
1401
+ await fs.copyFile(sourcePath, targetPath);
1402
+ await this.copyPhotonAssetDir(sourceAssetDir, targetAssetDir);
1403
+ }
1404
+ else if (path.dirname(sourcePath) !== workingDir || targetName !== sourceName) {
1405
+ await fs.mkdir(workingDir, { recursive: true });
1406
+ await fs.rename(sourcePath, targetPath);
1407
+ await this.movePhotonAssetDir(sourceAssetDir, targetAssetDir);
1408
+ }
1328
1409
  // Handle target repo push if specified
1329
1410
  if (options?.targetRepo || options?.createRepo) {
1330
- const { execSync } = await import('child_process');
1411
+ const { execFileSync } = await import('child_process');
1331
1412
  // Check gh CLI
1332
1413
  try {
1333
- execSync('gh --version', { stdio: 'pipe' });
1414
+ execFileSync('gh', ['--version'], { stdio: 'pipe' });
1334
1415
  }
1335
1416
  catch {
1336
1417
  return {
@@ -1341,7 +1422,9 @@ export class MarketplaceManager {
1341
1422
  if (options.createRepo) {
1342
1423
  // Create new repo and push
1343
1424
  try {
1344
- execSync(`gh repo create ${options.createRepo} --public --confirm`, { stdio: 'pipe' });
1425
+ execFileSync('gh', ['repo', 'create', options.createRepo, '--public', '--confirm'], {
1426
+ stdio: 'pipe',
1427
+ });
1345
1428
  }
1346
1429
  catch {
1347
1430
  // Repo may already exist
@@ -1349,23 +1432,17 @@ export class MarketplaceManager {
1349
1432
  const targetRepo = options.createRepo;
1350
1433
  const tmpDir = path.join(os.tmpdir(), `photon-fork-${Date.now()}`);
1351
1434
  try {
1352
- execSync(`gh repo clone ${targetRepo} "${tmpDir}" -- --depth=1`, {
1435
+ execFileSync('gh', ['repo', 'clone', targetRepo, tmpDir, '--', '--depth=1'], {
1353
1436
  stdio: 'pipe',
1354
1437
  });
1355
1438
  await fs.copyFile(filePath, path.join(tmpDir, fileName));
1356
- // Copy assets
1357
- const photonMeta = await this.getPhotonMetadata(name);
1358
- if (photonMeta?.metadata.assets) {
1359
- for (const asset of photonMeta.metadata.assets) {
1360
- const srcAsset = path.join(workingDir, asset);
1361
- if (existsSync(srcAsset)) {
1362
- const dstAsset = path.join(tmpDir, asset);
1363
- await fs.mkdir(path.dirname(dstAsset), { recursive: true });
1364
- await fs.copyFile(srcAsset, dstAsset);
1365
- }
1366
- }
1367
- }
1368
- execSync(`cd "${tmpDir}" && git add -A && git commit -m "fork: ${name} photon" && git push origin`, { stdio: 'pipe' });
1439
+ await this.copyPhotonAssetDir(targetAssetDir, path.join(tmpDir, targetName));
1440
+ execFileSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' });
1441
+ execFileSync('git', ['commit', '-m', `fork: ${name} photon`], {
1442
+ cwd: tmpDir,
1443
+ stdio: 'pipe',
1444
+ });
1445
+ execFileSync('git', ['push', 'origin'], { cwd: tmpDir, stdio: 'pipe' });
1369
1446
  await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
1370
1447
  }
1371
1448
  catch (e) {
@@ -1380,23 +1457,17 @@ export class MarketplaceManager {
1380
1457
  // Push to existing repo
1381
1458
  const tmpDir = path.join(os.tmpdir(), `photon-fork-${Date.now()}`);
1382
1459
  try {
1383
- execSync(`gh repo clone ${options.targetRepo} "${tmpDir}" -- --depth=1`, {
1460
+ execFileSync('gh', ['repo', 'clone', options.targetRepo, tmpDir, '--', '--depth=1'], {
1384
1461
  stdio: 'pipe',
1385
1462
  });
1386
1463
  await fs.copyFile(filePath, path.join(tmpDir, fileName));
1387
- // Copy assets
1388
- const photonMeta = await this.getPhotonMetadata(name);
1389
- if (photonMeta?.metadata.assets) {
1390
- for (const asset of photonMeta.metadata.assets) {
1391
- const srcAsset = path.join(workingDir, asset);
1392
- if (existsSync(srcAsset)) {
1393
- const dstAsset = path.join(tmpDir, asset);
1394
- await fs.mkdir(path.dirname(dstAsset), { recursive: true });
1395
- await fs.copyFile(srcAsset, dstAsset);
1396
- }
1397
- }
1398
- }
1399
- execSync(`cd "${tmpDir}" && git add -A && git commit -m "fork: ${name} photon" && git push origin`, { stdio: 'pipe' });
1464
+ await this.copyPhotonAssetDir(targetAssetDir, path.join(tmpDir, targetName));
1465
+ execFileSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' });
1466
+ execFileSync('git', ['commit', '-m', `fork: ${name} photon`], {
1467
+ cwd: tmpDir,
1468
+ stdio: 'pipe',
1469
+ });
1470
+ execFileSync('git', ['push', 'origin'], { cwd: tmpDir, stdio: 'pipe' });
1400
1471
  await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
1401
1472
  }
1402
1473
  catch (e) {
@@ -1409,22 +1480,59 @@ export class MarketplaceManager {
1409
1480
  }
1410
1481
  }
1411
1482
  // Remove marketplace tracking
1412
- delete localMetadata.photons[fileName];
1483
+ if (installMeta) {
1484
+ delete localMetadata.photons[normalizedSourceKey];
1485
+ }
1413
1486
  await this.writeMetadata(localMetadata);
1414
1487
  const parts = [];
1415
- parts.push(`${name} is now your own`);
1488
+ if (targetName !== sourceName) {
1489
+ parts.push(`Created local fork ${targetName} from ${sourceName}`);
1490
+ }
1491
+ else {
1492
+ parts.push(`${sourceName} is now your own`);
1493
+ }
1416
1494
  if (hasForkedFrom) {
1417
1495
  parts.push('Origin preserved as @forkedFrom tag');
1418
1496
  }
1419
- parts.push('Marketplace update tracking removed');
1497
+ if (installMeta) {
1498
+ parts.push('Marketplace update tracking removed');
1499
+ }
1420
1500
  return { success: true, message: parts.join('. ') };
1421
1501
  }
1502
+ toMetadataKey(filePath, workingDir) {
1503
+ return path.relative(workingDir, filePath).split(path.sep).join('/');
1504
+ }
1505
+ async copyPhotonAssetDir(sourceDir, targetDir) {
1506
+ const stat = await fs.lstat(sourceDir).catch(() => null);
1507
+ if (!stat)
1508
+ return;
1509
+ if (stat.isSymbolicLink()) {
1510
+ const linkTarget = await fs.readlink(sourceDir);
1511
+ await fs.symlink(linkTarget, targetDir).catch(() => { });
1512
+ return;
1513
+ }
1514
+ if (!stat.isDirectory())
1515
+ return;
1516
+ await fs.cp(sourceDir, targetDir, { recursive: true, force: true });
1517
+ }
1518
+ async movePhotonAssetDir(sourceDir, targetDir) {
1519
+ const stat = await fs.lstat(sourceDir).catch(() => null);
1520
+ if (!stat)
1521
+ return;
1522
+ if (existsSync(targetDir))
1523
+ return;
1524
+ await fs.mkdir(path.dirname(targetDir), { recursive: true });
1525
+ await fs.rename(sourceDir, targetDir);
1526
+ }
1422
1527
  /**
1423
1528
  * Contribute a photon back upstream via PR.
1424
1529
  * Shared logic used by both CLI and Beam.
1425
1530
  */
1426
1531
  async contributePhoton(name, workingDir, options) {
1427
- const { execSync } = await import('child_process');
1532
+ validateSafeName(name, 'photon name');
1533
+ if (options?.branch)
1534
+ validateSafeName(options.branch, 'branch name');
1535
+ const { execFileSync } = await import('child_process');
1428
1536
  const fileName = `${name}.photon.ts`;
1429
1537
  const filePath = path.join(workingDir, fileName);
1430
1538
  // Check file exists
@@ -1433,7 +1541,7 @@ export class MarketplaceManager {
1433
1541
  }
1434
1542
  // Check gh CLI
1435
1543
  try {
1436
- execSync('gh --version', { stdio: 'pipe' });
1544
+ execFileSync('gh', ['--version'], { stdio: 'pipe' });
1437
1545
  }
1438
1546
  catch {
1439
1547
  return {
@@ -1443,7 +1551,7 @@ export class MarketplaceManager {
1443
1551
  }
1444
1552
  // Check gh auth
1445
1553
  try {
1446
- execSync('gh auth status', { stdio: 'pipe' });
1554
+ execFileSync('gh', ['auth', 'status'], { stdio: 'pipe' });
1447
1555
  }
1448
1556
  catch {
1449
1557
  return {
@@ -1492,20 +1600,20 @@ export class MarketplaceManager {
1492
1600
  }
1493
1601
  // Fork the repo
1494
1602
  try {
1495
- execSync(`gh repo fork ${repo} --clone=false`, { stdio: 'pipe' });
1603
+ execFileSync('gh', ['repo', 'fork', repo, '--clone=false'], { stdio: 'pipe' });
1496
1604
  }
1497
1605
  catch {
1498
1606
  // Fork may already exist
1499
1607
  }
1500
1608
  // Get fork name
1501
- const forkJson = execSync('gh api user', { encoding: 'utf-8' });
1609
+ const forkJson = execFileSync('gh', ['api', 'user'], { encoding: 'utf-8' });
1502
1610
  const ghUser = JSON.parse(forkJson).login;
1503
1611
  const repoName = repo.split('/')[1];
1504
1612
  const forkRepo = `${ghUser}/${repoName}`;
1505
1613
  // Clone to temp dir
1506
1614
  const tmpDir = path.join(os.tmpdir(), `photon-contribute-${Date.now()}`);
1507
1615
  try {
1508
- execSync(`gh repo clone ${forkRepo} "${tmpDir}" -- --depth=1`, {
1616
+ execFileSync('gh', ['repo', 'clone', forkRepo, tmpDir, '--', '--depth=1'], {
1509
1617
  stdio: 'pipe',
1510
1618
  });
1511
1619
  // Copy modified photon file
@@ -1523,9 +1631,24 @@ export class MarketplaceManager {
1523
1631
  }
1524
1632
  }
1525
1633
  // Create branch, commit, push
1526
- execSync(`cd "${tmpDir}" && git checkout -b "${branchName}" && git add -A && git commit -m "improve: update ${name} photon" && git push origin "${branchName}"`, { stdio: 'pipe' });
1634
+ execFileSync('git', ['checkout', '-b', branchName], { cwd: tmpDir, stdio: 'pipe' });
1635
+ execFileSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' });
1636
+ execFileSync('git', ['commit', '-m', `improve: update ${name} photon`], {
1637
+ cwd: tmpDir,
1638
+ stdio: 'pipe',
1639
+ });
1640
+ execFileSync('git', ['push', 'origin', branchName], { cwd: tmpDir, stdio: 'pipe' });
1527
1641
  // Create PR
1528
- 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'] });
1642
+ const prOutput = execFileSync('gh', [
1643
+ 'pr',
1644
+ 'create',
1645
+ '--repo',
1646
+ repo,
1647
+ '--title',
1648
+ `Improve ${name} photon`,
1649
+ '--body',
1650
+ `Contributed improvements to ${name} photon via Photon marketplace.`,
1651
+ ], { cwd: tmpDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
1529
1652
  // Cleanup temp dir
1530
1653
  await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
1531
1654
  const prUrl = prOutput.trim();