@portel/photon 1.19.0 → 1.20.1

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 (146) 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.d.ts.map +1 -1
  5. package/dist/auto-ui/beam/routes/api-config.js +165 -24
  6. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  7. package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
  8. package/dist/auto-ui/beam/routes/api-marketplace.js +14 -1
  9. package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
  10. package/dist/auto-ui/beam.d.ts.map +1 -1
  11. package/dist/auto-ui/beam.js +187 -77
  12. package/dist/auto-ui/beam.js.map +1 -1
  13. package/dist/auto-ui/bridge/index.d.ts.map +1 -1
  14. package/dist/auto-ui/bridge/index.js +17 -0
  15. package/dist/auto-ui/bridge/index.js.map +1 -1
  16. package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
  17. package/dist/auto-ui/bridge/renderers.js +12 -4
  18. package/dist/auto-ui/bridge/renderers.js.map +1 -1
  19. package/dist/auto-ui/streamable-http-transport.d.ts +1 -0
  20. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  21. package/dist/auto-ui/streamable-http-transport.js +179 -44
  22. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  23. package/dist/auto-ui/types.d.ts +12 -0
  24. package/dist/auto-ui/types.d.ts.map +1 -1
  25. package/dist/auto-ui/types.js.map +1 -1
  26. package/dist/beam-form.bundle.js +63 -185
  27. package/dist/beam-form.bundle.js.map +4 -4
  28. package/dist/beam.bundle.js +2115 -761
  29. package/dist/beam.bundle.js.map +4 -4
  30. package/dist/capability-negotiator.d.ts +67 -0
  31. package/dist/capability-negotiator.d.ts.map +1 -0
  32. package/dist/capability-negotiator.js +104 -0
  33. package/dist/capability-negotiator.js.map +1 -0
  34. package/dist/channel-manager.d.ts +122 -0
  35. package/dist/channel-manager.d.ts.map +1 -0
  36. package/dist/channel-manager.js +266 -0
  37. package/dist/channel-manager.js.map +1 -0
  38. package/dist/cli/commands/beam.d.ts.map +1 -1
  39. package/dist/cli/commands/beam.js +47 -30
  40. package/dist/cli/commands/beam.js.map +1 -1
  41. package/dist/cli/commands/build.d.ts.map +1 -1
  42. package/dist/cli/commands/build.js +27 -2
  43. package/dist/cli/commands/build.js.map +1 -1
  44. package/dist/cli/commands/daemon.d.ts.map +1 -1
  45. package/dist/cli/commands/daemon.js +12 -6
  46. package/dist/cli/commands/daemon.js.map +1 -1
  47. package/dist/cli/commands/mcp.d.ts.map +1 -1
  48. package/dist/cli/commands/mcp.js +18 -6
  49. package/dist/cli/commands/mcp.js.map +1 -1
  50. package/dist/cli/commands/package.d.ts.map +1 -1
  51. package/dist/cli/commands/package.js +25 -7
  52. package/dist/cli/commands/package.js.map +1 -1
  53. package/dist/cli/commands/serve.d.ts.map +1 -1
  54. package/dist/cli/commands/serve.js +14 -2
  55. package/dist/cli/commands/serve.js.map +1 -1
  56. package/dist/cli-alias.d.ts.map +1 -1
  57. package/dist/cli-alias.js +2 -3
  58. package/dist/cli-alias.js.map +1 -1
  59. package/dist/context-store.d.ts +4 -4
  60. package/dist/context-store.d.ts.map +1 -1
  61. package/dist/context-store.js +18 -15
  62. package/dist/context-store.js.map +1 -1
  63. package/dist/context.d.ts +25 -2
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +69 -4
  66. package/dist/context.js.map +1 -1
  67. package/dist/daemon/client.d.ts.map +1 -1
  68. package/dist/daemon/client.js +16 -1
  69. package/dist/daemon/client.js.map +1 -1
  70. package/dist/daemon/manager.d.ts +2 -0
  71. package/dist/daemon/manager.d.ts.map +1 -1
  72. package/dist/daemon/manager.js +40 -8
  73. package/dist/daemon/manager.js.map +1 -1
  74. package/dist/daemon/server.js +89 -64
  75. package/dist/daemon/server.js.map +1 -1
  76. package/dist/daemon/worker-host.js +7 -0
  77. package/dist/daemon/worker-host.js.map +1 -1
  78. package/dist/daemon/worker-manager.d.ts.map +1 -1
  79. package/dist/daemon/worker-manager.js +79 -17
  80. package/dist/daemon/worker-manager.js.map +1 -1
  81. package/dist/daemon/worker-protocol.d.ts +3 -0
  82. package/dist/daemon/worker-protocol.d.ts.map +1 -1
  83. package/dist/deploy/cloudflare.d.ts.map +1 -1
  84. package/dist/deploy/cloudflare.js +2 -4
  85. package/dist/deploy/cloudflare.js.map +1 -1
  86. package/dist/loader.d.ts +11 -1
  87. package/dist/loader.d.ts.map +1 -1
  88. package/dist/loader.js +129 -13
  89. package/dist/loader.js.map +1 -1
  90. package/dist/marketplace-manager.d.ts +7 -1
  91. package/dist/marketplace-manager.d.ts.map +1 -1
  92. package/dist/marketplace-manager.js +165 -61
  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 +40 -21
  100. package/dist/photon-cli-runner.js.map +1 -1
  101. package/dist/photon-doc-extractor.d.ts.map +1 -1
  102. package/dist/photon-doc-extractor.js +59 -15
  103. package/dist/photon-doc-extractor.js.map +1 -1
  104. package/dist/resource-server.d.ts +105 -0
  105. package/dist/resource-server.d.ts.map +1 -0
  106. package/dist/resource-server.js +723 -0
  107. package/dist/resource-server.js.map +1 -0
  108. package/dist/serv/auth/jwt.d.ts +2 -0
  109. package/dist/serv/auth/jwt.d.ts.map +1 -1
  110. package/dist/serv/auth/jwt.js +11 -5
  111. package/dist/serv/auth/jwt.js.map +1 -1
  112. package/dist/serv/vault/token-vault.d.ts +2 -0
  113. package/dist/serv/vault/token-vault.d.ts.map +1 -1
  114. package/dist/serv/vault/token-vault.js +6 -0
  115. package/dist/serv/vault/token-vault.js.map +1 -1
  116. package/dist/server.d.ts +20 -149
  117. package/dist/server.d.ts.map +1 -1
  118. package/dist/server.js +246 -1233
  119. package/dist/server.js.map +1 -1
  120. package/dist/shared/audit.d.ts.map +1 -1
  121. package/dist/shared/audit.js +7 -0
  122. package/dist/shared/audit.js.map +1 -1
  123. package/dist/shared/security.d.ts +10 -0
  124. package/dist/shared/security.d.ts.map +1 -1
  125. package/dist/shared/security.js +27 -0
  126. package/dist/shared/security.js.map +1 -1
  127. package/dist/shared-utils.d.ts +4 -0
  128. package/dist/shared-utils.d.ts.map +1 -1
  129. package/dist/shared-utils.js +22 -0
  130. package/dist/shared-utils.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/template-manager.d.ts.map +1 -1
  136. package/dist/template-manager.js +56 -234
  137. package/dist/template-manager.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/package.json +3 -3
@@ -5,13 +5,14 @@ import * as fs from 'fs/promises';
5
5
  import * as path from 'path';
6
6
  import * as os from 'os';
7
7
  import { existsSync } from 'fs';
8
+ import { execFileSync } from 'child_process';
8
9
  import { readText, readJSON, writeText, writeJSON } from './shared/io.js';
9
10
  import * as crypto from 'crypto';
10
11
  import { createLogger } from './shared/logger.js';
11
12
  import { getErrorMessage } from './shared/error-handler.js';
12
13
  import { verifyContentHash, validateAssetPath, isPathWithin } from './shared/security.js';
13
14
  import { getDefaultContext } from './context.js';
14
- import { getMetadataPath } from '@portel/photon-core';
15
+ import { getMetadataPath, resolvePath } from '@portel/photon-core';
15
16
  import { SchemaExtractor } from '@portel/photon-core';
16
17
  // Timeout for marketplace fetch requests
17
18
  const FETCH_TIMEOUT_MS = 10 * 1000;
@@ -33,8 +34,11 @@ function getGitHubToken() {
33
34
  _ghToken = process.env.GITHUB_TOKEN || null;
34
35
  if (!_ghToken) {
35
36
  try {
36
- const { execSync } = require('child_process');
37
- _ghToken = execSync('gh auth token 2>/dev/null', { encoding: 'utf-8' }).trim() || null;
37
+ _ghToken =
38
+ execFileSync('gh', ['auth', 'token'], {
39
+ encoding: 'utf-8',
40
+ stdio: ['pipe', 'pipe', 'ignore'],
41
+ }).trim() || null;
38
42
  }
39
43
  catch {
40
44
  _ghToken = null;
@@ -115,10 +119,11 @@ export async function calculatePhotonHash(sourceFilePath, assets, baseDir) {
115
119
  /**
116
120
  * Read local installation metadata
117
121
  */
118
- export async function readLocalMetadata() {
122
+ export async function readLocalMetadata(baseDir) {
123
+ const metaFile = baseDir ? getMetadataPath(baseDir) : METADATA_FILE;
119
124
  try {
120
- if (existsSync(METADATA_FILE)) {
121
- return await readJSON(METADATA_FILE);
125
+ if (existsSync(metaFile)) {
126
+ return await readJSON(metaFile);
122
127
  }
123
128
  }
124
129
  catch (error) {
@@ -127,6 +132,11 @@ export async function readLocalMetadata() {
127
132
  }
128
133
  return { photons: {} };
129
134
  }
135
+ function validateSafeName(name, label) {
136
+ if (/[;&|$`(){}\\\<>!#~\n\r]/.test(name)) {
137
+ throw new Error(`Invalid ${label}: contains unsafe characters`);
138
+ }
139
+ }
130
140
  export class MarketplaceManager {
131
141
  config = { marketplaces: [] };
132
142
  logger;
@@ -732,7 +742,7 @@ export class MarketplaceManager {
732
742
  * Safe to call on every startup — only fetches when assets are actually missing.
733
743
  */
734
744
  async repairMissingAssets(workingDir) {
735
- const localMetadata = await readLocalMetadata();
745
+ const localMetadata = await this.readMetadata();
736
746
  let repaired = 0;
737
747
  for (const [fileName, installInfo] of Object.entries(localMetadata.photons)) {
738
748
  const photonName = fileName.replace(/\.photon\.ts$/, '');
@@ -1143,7 +1153,7 @@ export class MarketplaceManager {
1143
1153
  return { photons: {} };
1144
1154
  }
1145
1155
  async writeMetadata(metadata) {
1146
- await fs.mkdir(this.configDir, { recursive: true });
1156
+ await fs.mkdir(path.dirname(this.metadataFile), { recursive: true });
1147
1157
  await writeJSON(this.metadataFile, metadata);
1148
1158
  }
1149
1159
  /**
@@ -1327,30 +1337,82 @@ export class MarketplaceManager {
1327
1337
  * Shared logic used by both CLI and Beam.
1328
1338
  */
1329
1339
  async forkPhoton(name, workingDir, options) {
1330
- const fileName = `${name}.photon.ts`;
1331
- const filePath = path.join(workingDir, fileName);
1332
- // Check file exists
1333
- if (!existsSync(filePath)) {
1340
+ validateSafeName(name, 'photon name');
1341
+ if (options?.targetRepo)
1342
+ validateSafeName(options.targetRepo, 'target repo');
1343
+ if (options?.createRepo)
1344
+ validateSafeName(options.createRepo, 'repo name');
1345
+ if (options?.newName)
1346
+ validateSafeName(options.newName, 'new photon name');
1347
+ const sourcePath = await resolvePath(name, workingDir);
1348
+ if (!sourcePath) {
1334
1349
  return { success: false, message: `Photon not found: ${name}` };
1335
1350
  }
1336
- // Read install metadata
1337
- const localMetadata = await readLocalMetadata();
1338
- const installMeta = localMetadata.photons[fileName];
1339
- if (!installMeta) {
1351
+ const sourceName = path.basename(sourcePath).replace(/\.photon\.(ts|js)$/, '');
1352
+ const normalizedSourceKey = this.toMetadataKey(sourcePath, workingDir);
1353
+ const requestedNewName = options?.newName?.trim();
1354
+ const localMetadata = await this.readMetadata();
1355
+ const installMeta = localMetadata.photons[normalizedSourceKey];
1356
+ const isLocalSource = !installMeta;
1357
+ const targetName = requestedNewName || sourceName;
1358
+ const suggestedName = `${sourceName}-copy`;
1359
+ if (isLocalSource && !requestedNewName) {
1340
1360
  return {
1341
- success: true,
1342
- message: `${name} is already a local photon (no marketplace tracking)`,
1361
+ success: false,
1362
+ message: `${sourceName} is already local. Choose a new local name to fork it.`,
1363
+ requiresName: true,
1364
+ suggestedName,
1343
1365
  };
1344
1366
  }
1367
+ if (isLocalSource && requestedNewName === sourceName) {
1368
+ return {
1369
+ success: false,
1370
+ message: 'Forking a local photon requires a different new name.',
1371
+ requiresName: true,
1372
+ suggestedName,
1373
+ };
1374
+ }
1375
+ const targetPath = path.join(workingDir, `${targetName}.photon.ts`);
1376
+ const sourceRealPath = await fs.realpath(sourcePath).catch(() => sourcePath);
1377
+ const targetExists = existsSync(targetPath);
1378
+ const targetRealPath = targetExists
1379
+ ? await fs.realpath(targetPath).catch(() => targetPath)
1380
+ : null;
1381
+ if (targetExists && targetRealPath !== sourceRealPath) {
1382
+ return {
1383
+ success: false,
1384
+ message: `A local photon named ${targetName} already exists. Choose a different local name.`,
1385
+ requiresName: true,
1386
+ suggestedName,
1387
+ };
1388
+ }
1389
+ const fileName = `${targetName}.photon.ts`;
1390
+ const filePath = targetPath;
1345
1391
  // Check @forkedFrom tag
1346
- const content = await readText(filePath);
1392
+ const content = await readText(sourcePath);
1347
1393
  const hasForkedFrom = content.includes('@forkedFrom');
1394
+ const sourceAssetDir = path.join(path.dirname(sourcePath), sourceName);
1395
+ const targetAssetDir = path.join(workingDir, targetName);
1396
+ if (targetRealPath !== sourceRealPath && !isLocalSource) {
1397
+ await fs.mkdir(workingDir, { recursive: true });
1398
+ await fs.rename(sourcePath, targetPath);
1399
+ await this.movePhotonAssetDir(sourceAssetDir, targetAssetDir);
1400
+ }
1401
+ else if (targetRealPath !== sourceRealPath) {
1402
+ await fs.copyFile(sourcePath, targetPath);
1403
+ await this.copyPhotonAssetDir(sourceAssetDir, targetAssetDir);
1404
+ }
1405
+ else if (path.dirname(sourcePath) !== workingDir || targetName !== sourceName) {
1406
+ await fs.mkdir(workingDir, { recursive: true });
1407
+ await fs.rename(sourcePath, targetPath);
1408
+ await this.movePhotonAssetDir(sourceAssetDir, targetAssetDir);
1409
+ }
1348
1410
  // Handle target repo push if specified
1349
1411
  if (options?.targetRepo || options?.createRepo) {
1350
- const { execSync } = await import('child_process');
1412
+ const { execFileSync } = await import('child_process');
1351
1413
  // Check gh CLI
1352
1414
  try {
1353
- execSync('gh --version', { stdio: 'pipe' });
1415
+ execFileSync('gh', ['--version'], { stdio: 'pipe' });
1354
1416
  }
1355
1417
  catch {
1356
1418
  return {
@@ -1361,7 +1423,9 @@ export class MarketplaceManager {
1361
1423
  if (options.createRepo) {
1362
1424
  // Create new repo and push
1363
1425
  try {
1364
- execSync(`gh repo create ${options.createRepo} --public --confirm`, { stdio: 'pipe' });
1426
+ execFileSync('gh', ['repo', 'create', options.createRepo, '--public', '--confirm'], {
1427
+ stdio: 'pipe',
1428
+ });
1365
1429
  }
1366
1430
  catch {
1367
1431
  // Repo may already exist
@@ -1369,23 +1433,17 @@ export class MarketplaceManager {
1369
1433
  const targetRepo = options.createRepo;
1370
1434
  const tmpDir = path.join(os.tmpdir(), `photon-fork-${Date.now()}`);
1371
1435
  try {
1372
- execSync(`gh repo clone ${targetRepo} "${tmpDir}" -- --depth=1`, {
1436
+ execFileSync('gh', ['repo', 'clone', targetRepo, tmpDir, '--', '--depth=1'], {
1373
1437
  stdio: 'pipe',
1374
1438
  });
1375
1439
  await fs.copyFile(filePath, path.join(tmpDir, fileName));
1376
- // Copy assets
1377
- const photonMeta = await this.getPhotonMetadata(name);
1378
- if (photonMeta?.metadata.assets) {
1379
- for (const asset of photonMeta.metadata.assets) {
1380
- const srcAsset = path.join(workingDir, asset);
1381
- if (existsSync(srcAsset)) {
1382
- const dstAsset = path.join(tmpDir, asset);
1383
- await fs.mkdir(path.dirname(dstAsset), { recursive: true });
1384
- await fs.copyFile(srcAsset, dstAsset);
1385
- }
1386
- }
1387
- }
1388
- execSync(`cd "${tmpDir}" && git add -A && git commit -m "fork: ${name} photon" && git push origin`, { stdio: 'pipe' });
1440
+ await this.copyPhotonAssetDir(targetAssetDir, path.join(tmpDir, targetName));
1441
+ execFileSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' });
1442
+ execFileSync('git', ['commit', '-m', `fork: ${name} photon`], {
1443
+ cwd: tmpDir,
1444
+ stdio: 'pipe',
1445
+ });
1446
+ execFileSync('git', ['push', 'origin'], { cwd: tmpDir, stdio: 'pipe' });
1389
1447
  await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
1390
1448
  }
1391
1449
  catch (e) {
@@ -1400,23 +1458,17 @@ export class MarketplaceManager {
1400
1458
  // Push to existing repo
1401
1459
  const tmpDir = path.join(os.tmpdir(), `photon-fork-${Date.now()}`);
1402
1460
  try {
1403
- execSync(`gh repo clone ${options.targetRepo} "${tmpDir}" -- --depth=1`, {
1461
+ execFileSync('gh', ['repo', 'clone', options.targetRepo, tmpDir, '--', '--depth=1'], {
1404
1462
  stdio: 'pipe',
1405
1463
  });
1406
1464
  await fs.copyFile(filePath, path.join(tmpDir, fileName));
1407
- // Copy assets
1408
- const photonMeta = await this.getPhotonMetadata(name);
1409
- if (photonMeta?.metadata.assets) {
1410
- for (const asset of photonMeta.metadata.assets) {
1411
- const srcAsset = path.join(workingDir, asset);
1412
- if (existsSync(srcAsset)) {
1413
- const dstAsset = path.join(tmpDir, asset);
1414
- await fs.mkdir(path.dirname(dstAsset), { recursive: true });
1415
- await fs.copyFile(srcAsset, dstAsset);
1416
- }
1417
- }
1418
- }
1419
- execSync(`cd "${tmpDir}" && git add -A && git commit -m "fork: ${name} photon" && git push origin`, { stdio: 'pipe' });
1465
+ await this.copyPhotonAssetDir(targetAssetDir, path.join(tmpDir, targetName));
1466
+ execFileSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' });
1467
+ execFileSync('git', ['commit', '-m', `fork: ${name} photon`], {
1468
+ cwd: tmpDir,
1469
+ stdio: 'pipe',
1470
+ });
1471
+ execFileSync('git', ['push', 'origin'], { cwd: tmpDir, stdio: 'pipe' });
1420
1472
  await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
1421
1473
  }
1422
1474
  catch (e) {
@@ -1429,22 +1481,59 @@ export class MarketplaceManager {
1429
1481
  }
1430
1482
  }
1431
1483
  // Remove marketplace tracking
1432
- delete localMetadata.photons[fileName];
1484
+ if (installMeta) {
1485
+ delete localMetadata.photons[normalizedSourceKey];
1486
+ }
1433
1487
  await this.writeMetadata(localMetadata);
1434
1488
  const parts = [];
1435
- parts.push(`${name} is now your own`);
1489
+ if (targetName !== sourceName) {
1490
+ parts.push(`Created local fork ${targetName} from ${sourceName}`);
1491
+ }
1492
+ else {
1493
+ parts.push(`${sourceName} is now your own`);
1494
+ }
1436
1495
  if (hasForkedFrom) {
1437
1496
  parts.push('Origin preserved as @forkedFrom tag');
1438
1497
  }
1439
- parts.push('Marketplace update tracking removed');
1498
+ if (installMeta) {
1499
+ parts.push('Marketplace update tracking removed');
1500
+ }
1440
1501
  return { success: true, message: parts.join('. ') };
1441
1502
  }
1503
+ toMetadataKey(filePath, workingDir) {
1504
+ return path.relative(workingDir, filePath).split(path.sep).join('/');
1505
+ }
1506
+ async copyPhotonAssetDir(sourceDir, targetDir) {
1507
+ const stat = await fs.lstat(sourceDir).catch(() => null);
1508
+ if (!stat)
1509
+ return;
1510
+ if (stat.isSymbolicLink()) {
1511
+ const linkTarget = await fs.readlink(sourceDir);
1512
+ await fs.symlink(linkTarget, targetDir).catch(() => { });
1513
+ return;
1514
+ }
1515
+ if (!stat.isDirectory())
1516
+ return;
1517
+ await fs.cp(sourceDir, targetDir, { recursive: true, force: true });
1518
+ }
1519
+ async movePhotonAssetDir(sourceDir, targetDir) {
1520
+ const stat = await fs.lstat(sourceDir).catch(() => null);
1521
+ if (!stat)
1522
+ return;
1523
+ if (existsSync(targetDir))
1524
+ return;
1525
+ await fs.mkdir(path.dirname(targetDir), { recursive: true });
1526
+ await fs.rename(sourceDir, targetDir);
1527
+ }
1442
1528
  /**
1443
1529
  * Contribute a photon back upstream via PR.
1444
1530
  * Shared logic used by both CLI and Beam.
1445
1531
  */
1446
1532
  async contributePhoton(name, workingDir, options) {
1447
- const { execSync } = await import('child_process');
1533
+ validateSafeName(name, 'photon name');
1534
+ if (options?.branch)
1535
+ validateSafeName(options.branch, 'branch name');
1536
+ const { execFileSync } = await import('child_process');
1448
1537
  const fileName = `${name}.photon.ts`;
1449
1538
  const filePath = path.join(workingDir, fileName);
1450
1539
  // Check file exists
@@ -1453,7 +1542,7 @@ export class MarketplaceManager {
1453
1542
  }
1454
1543
  // Check gh CLI
1455
1544
  try {
1456
- execSync('gh --version', { stdio: 'pipe' });
1545
+ execFileSync('gh', ['--version'], { stdio: 'pipe' });
1457
1546
  }
1458
1547
  catch {
1459
1548
  return {
@@ -1463,7 +1552,7 @@ export class MarketplaceManager {
1463
1552
  }
1464
1553
  // Check gh auth
1465
1554
  try {
1466
- execSync('gh auth status', { stdio: 'pipe' });
1555
+ execFileSync('gh', ['auth', 'status'], { stdio: 'pipe' });
1467
1556
  }
1468
1557
  catch {
1469
1558
  return {
@@ -1512,20 +1601,20 @@ export class MarketplaceManager {
1512
1601
  }
1513
1602
  // Fork the repo
1514
1603
  try {
1515
- execSync(`gh repo fork ${repo} --clone=false`, { stdio: 'pipe' });
1604
+ execFileSync('gh', ['repo', 'fork', repo, '--clone=false'], { stdio: 'pipe' });
1516
1605
  }
1517
1606
  catch {
1518
1607
  // Fork may already exist
1519
1608
  }
1520
1609
  // Get fork name
1521
- const forkJson = execSync('gh api user', { encoding: 'utf-8' });
1610
+ const forkJson = execFileSync('gh', ['api', 'user'], { encoding: 'utf-8' });
1522
1611
  const ghUser = JSON.parse(forkJson).login;
1523
1612
  const repoName = repo.split('/')[1];
1524
1613
  const forkRepo = `${ghUser}/${repoName}`;
1525
1614
  // Clone to temp dir
1526
1615
  const tmpDir = path.join(os.tmpdir(), `photon-contribute-${Date.now()}`);
1527
1616
  try {
1528
- execSync(`gh repo clone ${forkRepo} "${tmpDir}" -- --depth=1`, {
1617
+ execFileSync('gh', ['repo', 'clone', forkRepo, tmpDir, '--', '--depth=1'], {
1529
1618
  stdio: 'pipe',
1530
1619
  });
1531
1620
  // Copy modified photon file
@@ -1543,9 +1632,24 @@ export class MarketplaceManager {
1543
1632
  }
1544
1633
  }
1545
1634
  // Create branch, commit, push
1546
- execSync(`cd "${tmpDir}" && git checkout -b "${branchName}" && git add -A && git commit -m "improve: update ${name} photon" && git push origin "${branchName}"`, { stdio: 'pipe' });
1635
+ execFileSync('git', ['checkout', '-b', branchName], { cwd: tmpDir, stdio: 'pipe' });
1636
+ execFileSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' });
1637
+ execFileSync('git', ['commit', '-m', `improve: update ${name} photon`], {
1638
+ cwd: tmpDir,
1639
+ stdio: 'pipe',
1640
+ });
1641
+ execFileSync('git', ['push', 'origin', branchName], { cwd: tmpDir, stdio: 'pipe' });
1547
1642
  // Create PR
1548
- 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'] });
1643
+ const prOutput = execFileSync('gh', [
1644
+ 'pr',
1645
+ 'create',
1646
+ '--repo',
1647
+ repo,
1648
+ '--title',
1649
+ `Improve ${name} photon`,
1650
+ '--body',
1651
+ `Contributed improvements to ${name} photon via Photon marketplace.`,
1652
+ ], { cwd: tmpDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
1549
1653
  // Cleanup temp dir
1550
1654
  await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
1551
1655
  const prUrl = prOutput.trim();