@portel/photon 1.6.1 → 1.8.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 (113) hide show
  1. package/README.md +111 -160
  2. package/dist/auto-ui/beam.d.ts.map +1 -1
  3. package/dist/auto-ui/beam.js +218 -106
  4. package/dist/auto-ui/beam.js.map +1 -1
  5. package/dist/auto-ui/design-system/tokens.d.ts +1 -1
  6. package/dist/auto-ui/design-system/tokens.d.ts.map +1 -1
  7. package/dist/auto-ui/design-system/tokens.js +2 -2
  8. package/dist/auto-ui/design-system/tokens.js.map +1 -1
  9. package/dist/auto-ui/frontend/index.html +1 -1
  10. package/dist/auto-ui/platform-compat.d.ts.map +1 -1
  11. package/dist/auto-ui/platform-compat.js +12 -2
  12. package/dist/auto-ui/platform-compat.js.map +1 -1
  13. package/dist/auto-ui/playground-html.js +5 -5
  14. package/dist/auto-ui/rendering/components.d.ts.map +1 -1
  15. package/dist/auto-ui/rendering/components.js +568 -0
  16. package/dist/auto-ui/rendering/components.js.map +1 -1
  17. package/dist/auto-ui/rendering/field-analyzer.d.ts +56 -0
  18. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -1
  19. package/dist/auto-ui/rendering/field-analyzer.js +177 -0
  20. package/dist/auto-ui/rendering/field-analyzer.js.map +1 -1
  21. package/dist/auto-ui/rendering/layout-selector.d.ts +14 -2
  22. package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -1
  23. package/dist/auto-ui/rendering/layout-selector.js +125 -1
  24. package/dist/auto-ui/rendering/layout-selector.js.map +1 -1
  25. package/dist/auto-ui/streamable-http-transport.d.ts +1 -1
  26. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  27. package/dist/auto-ui/streamable-http-transport.js +370 -26
  28. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  29. package/dist/auto-ui/types.d.ts +7 -1
  30. package/dist/auto-ui/types.d.ts.map +1 -1
  31. package/dist/auto-ui/types.js.map +1 -1
  32. package/dist/beam.bundle.js +21932 -3307
  33. package/dist/beam.bundle.js.map +4 -4
  34. package/dist/cli/commands/info.d.ts.map +1 -1
  35. package/dist/cli/commands/info.js +37 -0
  36. package/dist/cli/commands/info.js.map +1 -1
  37. package/dist/cli/commands/package.d.ts.map +1 -1
  38. package/dist/cli/commands/package.js +16 -0
  39. package/dist/cli/commands/package.js.map +1 -1
  40. package/dist/cli.d.ts.map +1 -1
  41. package/dist/cli.js +640 -17
  42. package/dist/cli.js.map +1 -1
  43. package/dist/context-store.d.ts +79 -0
  44. package/dist/context-store.d.ts.map +1 -0
  45. package/dist/context-store.js +210 -0
  46. package/dist/context-store.js.map +1 -0
  47. package/dist/daemon/client.d.ts +13 -4
  48. package/dist/daemon/client.d.ts.map +1 -1
  49. package/dist/daemon/client.js +138 -77
  50. package/dist/daemon/client.js.map +1 -1
  51. package/dist/daemon/manager.d.ts +0 -25
  52. package/dist/daemon/manager.d.ts.map +1 -1
  53. package/dist/daemon/manager.js +10 -38
  54. package/dist/daemon/manager.js.map +1 -1
  55. package/dist/daemon/protocol.d.ts +7 -2
  56. package/dist/daemon/protocol.d.ts.map +1 -1
  57. package/dist/daemon/protocol.js.map +1 -1
  58. package/dist/daemon/server.js +317 -83
  59. package/dist/daemon/server.js.map +1 -1
  60. package/dist/daemon/session-manager.d.ts +24 -4
  61. package/dist/daemon/session-manager.d.ts.map +1 -1
  62. package/dist/daemon/session-manager.js +62 -12
  63. package/dist/daemon/session-manager.js.map +1 -1
  64. package/dist/index.d.ts +0 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +0 -3
  67. package/dist/index.js.map +1 -1
  68. package/dist/loader.d.ts +3 -20
  69. package/dist/loader.d.ts.map +1 -1
  70. package/dist/loader.js +87 -77
  71. package/dist/loader.js.map +1 -1
  72. package/dist/markdown-utils.d.ts.map +1 -1
  73. package/dist/markdown-utils.js +2 -1
  74. package/dist/markdown-utils.js.map +1 -1
  75. package/dist/marketplace-manager.d.ts.map +1 -1
  76. package/dist/marketplace-manager.js +20 -3
  77. package/dist/marketplace-manager.js.map +1 -1
  78. package/dist/photon-cli-runner.d.ts.map +1 -1
  79. package/dist/photon-cli-runner.js +258 -218
  80. package/dist/photon-cli-runner.js.map +1 -1
  81. package/dist/photon-doc-extractor.d.ts +2 -0
  82. package/dist/photon-doc-extractor.d.ts.map +1 -1
  83. package/dist/photon-doc-extractor.js +45 -7
  84. package/dist/photon-doc-extractor.js.map +1 -1
  85. package/dist/photons/maker.photon.d.ts.map +1 -1
  86. package/dist/photons/maker.photon.js +22 -4
  87. package/dist/photons/maker.photon.js.map +1 -1
  88. package/dist/photons/maker.photon.ts +47 -11
  89. package/dist/security-scanner.d.ts.map +1 -1
  90. package/dist/security-scanner.js +8 -2
  91. package/dist/security-scanner.js.map +1 -1
  92. package/dist/serv/index.d.ts +1 -1
  93. package/dist/serv/index.d.ts.map +1 -1
  94. package/dist/serv/index.js +6 -4
  95. package/dist/serv/index.js.map +1 -1
  96. package/dist/server.d.ts +32 -15
  97. package/dist/server.d.ts.map +1 -1
  98. package/dist/server.js +525 -483
  99. package/dist/server.js.map +1 -1
  100. package/dist/shared/security.d.ts +79 -0
  101. package/dist/shared/security.d.ts.map +1 -0
  102. package/dist/shared/security.js +251 -0
  103. package/dist/shared/security.js.map +1 -0
  104. package/dist/shell-completions.d.ts +21 -0
  105. package/dist/shell-completions.d.ts.map +1 -0
  106. package/dist/shell-completions.js +102 -0
  107. package/dist/shell-completions.js.map +1 -0
  108. package/dist/template-manager.d.ts.map +1 -1
  109. package/dist/template-manager.js +10 -3
  110. package/dist/template-manager.js.map +1 -1
  111. package/dist/version.d.ts.map +1 -1
  112. package/dist/version.js.map +1 -1
  113. package/package.json +12 -7
package/dist/cli.js CHANGED
@@ -24,13 +24,14 @@ import { toEnvVarName } from './shared/config-docs.js';
24
24
  import { runTask } from './shared/task-runner.js';
25
25
  import { normalizeLogLevel, logger } from './shared/logger.js';
26
26
  import { printHeader, printInfo, printWarning, printError, printSuccess } from './cli-formatter.js';
27
- import { handleError, getErrorMessage, ExitCode, exitWithError, } from './shared/error-handler.js';
27
+ import { handleError, getErrorMessage, ExitCode, exitWithError, isNodeError, } from './shared/error-handler.js';
28
28
  import { validateOrThrow, inRange, isPositive, isInteger } from './shared/validation.js';
29
- import { createReadline, promptWait } from './shared/cli-utils.js';
29
+ import { createReadline, promptText, promptWait } from './shared/cli-utils.js';
30
30
  import { registerMarketplaceCommands } from './cli/commands/marketplace.js';
31
31
  import { registerInfoCommand } from './cli/commands/info.js';
32
32
  import { registerPackageCommands } from './cli/commands/package.js';
33
33
  import { registerPackageAppCommand } from './cli/commands/package-app.js';
34
+ import { validateAssetPath, isPathWithin } from './shared/security.js';
34
35
  // ══════════════════════════════════════════════════════════════════════════════
35
36
  // BUNDLED PHOTONS
36
37
  // ══════════════════════════════════════════════════════════════════════════════
@@ -252,11 +253,16 @@ async function handleUrlElicitation(ask) {
252
253
  const platform = process.platform;
253
254
  const openCommand = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open';
254
255
  try {
255
- const { exec } = await import('child_process');
256
- exec(`${openCommand} "${ask.url}"`);
256
+ // Security: validate URL and use execFile to prevent shell injection
257
+ new URL(ask.url); // throws on invalid URL
258
+ const { execFile } = await import('child_process');
259
+ execFile(openCommand, [ask.url]);
257
260
  }
258
261
  catch (error) {
259
- cliHint('Please open the URL manually in your browser.');
262
+ const msg = isNodeError(error, 'ENOENT')
263
+ ? `Could not find '${openCommand}' to open URLs`
264
+ : getErrorMessage(error);
265
+ cliHint(`Could not open browser: ${msg}. Please open the URL manually.`);
260
266
  }
261
267
  const shouldContinue = await promptWait('Press Enter when done', true);
262
268
  return { action: shouldContinue ? 'accept' : 'cancel' };
@@ -755,6 +761,9 @@ program
755
761
  .configureHelp({
756
762
  sortSubcommands: false,
757
763
  sortOptions: false,
764
+ // Hide Commander's auto-generated "Commands:" section since we show
765
+ // a custom categorized section in addHelpText('after', ...)
766
+ visibleCommands: () => [],
758
767
  })
759
768
  .addHelpText('after', `
760
769
  Runtime Commands:
@@ -764,6 +773,11 @@ Runtime Commands:
764
773
  beam Launch Photon Beam (interactive control panel)
765
774
  serve Start local multi-tenant MCP hosting for development
766
775
 
776
+ Configuration:
777
+ use <photon> [instance] Switch to a named instance of a stateful photon
778
+ instances <photon> List all instances of a stateful photon
779
+ set <photon> [values] Configure environment for a photon
780
+
767
781
  Hosting:
768
782
  host <command> Manage cloud hosting (preview, deploy)
769
783
 
@@ -918,7 +932,13 @@ program
918
932
  if (source.metadata.assets && source.metadata.assets.length > 0) {
919
933
  const assets = await manager.fetchAssets(source.marketplace, source.metadata.assets);
920
934
  for (const [assetPath, content] of assets) {
921
- const assetTarget = path.join(workingDir, assetPath);
935
+ // Security: validate asset path to prevent traversal
936
+ const safePath = validateAssetPath(assetPath);
937
+ const assetTarget = path.join(workingDir, safePath);
938
+ if (!isPathWithin(assetTarget, workingDir)) {
939
+ console.error(`Skipping unsafe asset path: ${assetPath}`);
940
+ continue;
941
+ }
922
942
  const assetDir = path.dirname(assetTarget);
923
943
  await fs.mkdir(assetDir, { recursive: true });
924
944
  await fs.writeFile(assetTarget, content, 'utf-8');
@@ -1104,8 +1124,12 @@ program
1104
1124
  logger.debug(`Could not auto-open browser: ${err.message}`);
1105
1125
  });
1106
1126
  }
1107
- // Handle shutdown signals
1127
+ // Handle shutdown signals (guard against duplicate Ctrl+C)
1128
+ let shuttingDown = false;
1108
1129
  const shutdown = async () => {
1130
+ if (shuttingDown)
1131
+ return;
1132
+ shuttingDown = true;
1109
1133
  console.error('\nShutting down Photon Beam...');
1110
1134
  // Gracefully close external MCP clients to prevent ugly tracebacks
1111
1135
  try {
@@ -1354,11 +1378,15 @@ maker
1354
1378
  // Check if file already exists
1355
1379
  try {
1356
1380
  await fs.access(filePath);
1357
- logger.error(`File already exists: ${filePath}`);
1358
- process.exit(1);
1381
+ exitWithError(`File already exists: ${filePath}`, {
1382
+ suggestion: `Choose a different name or delete the existing file`,
1383
+ });
1359
1384
  }
1360
- catch {
1361
- // File doesn't exist, good
1385
+ catch (err) {
1386
+ if (!isNodeError(err, 'ENOENT')) {
1387
+ exitWithError(`Cannot access ${filePath}: ${getErrorMessage(err)}`);
1388
+ }
1389
+ // ENOENT = file doesn't exist — good, proceed
1362
1390
  }
1363
1391
  // Read template
1364
1392
  const templatePath = path.join(__dirname, '..', 'templates', 'photon.template.ts');
@@ -1366,8 +1394,8 @@ maker
1366
1394
  try {
1367
1395
  template = await fs.readFile(templatePath, 'utf-8');
1368
1396
  }
1369
- catch {
1370
- // Fallback inline template if file not found
1397
+ catch (err) {
1398
+ logger.debug(`Template not found at ${templatePath}, using inline template`);
1371
1399
  template = getInlineTemplate();
1372
1400
  }
1373
1401
  // Replace placeholders
@@ -1815,6 +1843,590 @@ SEE ALSO:
1815
1843
  await runMethod(photon, method, args);
1816
1844
  }
1817
1845
  });
1846
+ // Use command: switch to a named instance of a stateful photon
1847
+ program
1848
+ .command('use')
1849
+ .argument('<photon>', 'Photon name')
1850
+ .argument('[instance]', 'Instance name (omit for default)')
1851
+ .description('Switch to a named instance of a stateful photon')
1852
+ .action(async (photonName, instance) => {
1853
+ try {
1854
+ const { CLISessionStore } = await import('./context-store.js');
1855
+ // Write to CLI session store only — each client manages its own instance
1856
+ new CLISessionStore().setCurrentInstance(photonName, instance || '');
1857
+ const label = instance || 'default';
1858
+ printSuccess(`${photonName} → instance: ${label}`);
1859
+ // Refresh completions cache (picks up new instance)
1860
+ try {
1861
+ const { generateCompletionCache } = await import('./shell-completions.js');
1862
+ await generateCompletionCache();
1863
+ }
1864
+ catch {
1865
+ // Best-effort: don't break the use command if cache refresh fails
1866
+ }
1867
+ }
1868
+ catch (error) {
1869
+ printError(getErrorMessage(error));
1870
+ process.exit(1);
1871
+ }
1872
+ });
1873
+ // Instances command: list all instances of a stateful photon
1874
+ program
1875
+ .command('instances')
1876
+ .argument('<photon>', 'Photon name')
1877
+ .description('List all instances of a stateful photon')
1878
+ .action(async (photonName) => {
1879
+ try {
1880
+ const { InstanceStore } = await import('./context-store.js');
1881
+ const store = new InstanceStore();
1882
+ const instances = store.listInstances(photonName);
1883
+ const current = store.getCurrentInstance(photonName) || 'default';
1884
+ if (instances.length === 0) {
1885
+ printInfo(`No instances found for ${photonName}.`);
1886
+ return;
1887
+ }
1888
+ cliHeading(`${photonName} — Instances`);
1889
+ cliSpacer();
1890
+ for (const name of instances) {
1891
+ const marker = name === current ? ' ← current' : '';
1892
+ console.log(` ${name}${marker}`);
1893
+ }
1894
+ cliSpacer();
1895
+ }
1896
+ catch (error) {
1897
+ printError(getErrorMessage(error));
1898
+ process.exit(1);
1899
+ }
1900
+ });
1901
+ // Shell command group: shell integration utilities
1902
+ const shell = program.command('shell').description('Shell integration utilities');
1903
+ shell
1904
+ .command('init')
1905
+ .option('--hook', 'Output the shell hook script (used internally by eval/Invoke-Expression)')
1906
+ .description('Set up shell integration for direct photon commands and tab completion')
1907
+ .action(async (options) => {
1908
+ // Detect shell type
1909
+ const userShell = process.env.SHELL || '';
1910
+ const isPowerShell = !!process.env.PSModulePath;
1911
+ const isZsh = !isPowerShell && userShell.includes('zsh');
1912
+ const isBash = !isPowerShell && userShell.includes('bash');
1913
+ let shellType = 'unsupported';
1914
+ if (isZsh)
1915
+ shellType = 'zsh';
1916
+ else if (isBash)
1917
+ shellType = 'bash';
1918
+ else if (isPowerShell || process.platform === 'win32')
1919
+ shellType = 'powershell';
1920
+ // Unsupported shell — show supported list and exit
1921
+ if (shellType === 'unsupported') {
1922
+ const detected = userShell ? path.basename(userShell) : 'unknown';
1923
+ printError(`Unsupported shell: ${detected}`);
1924
+ console.log('');
1925
+ console.log(' Supported shells:');
1926
+ console.log(' zsh ~/.zshrc (macOS default)');
1927
+ console.log(' bash ~/.bashrc (Linux default)');
1928
+ console.log(' PowerShell $PROFILE (Windows default, cross-platform)');
1929
+ console.log('');
1930
+ console.log(' To use a specific shell, set $SHELL and retry:');
1931
+ console.log(' SHELL=/bin/zsh photon shell init');
1932
+ process.exit(1);
1933
+ }
1934
+ // RC file and eval/invoke line per shell
1935
+ let rcFile;
1936
+ let evalLine;
1937
+ const marker = '# photon shell integration';
1938
+ if (shellType === 'powershell') {
1939
+ // PowerShell profile path: cross-platform
1940
+ rcFile =
1941
+ process.platform === 'win32'
1942
+ ? path.join(os.homedir(), 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1')
1943
+ : path.join(os.homedir(), '.config', 'powershell', 'Microsoft.PowerShell_profile.ps1');
1944
+ evalLine = 'Invoke-Expression (& photon shell init --hook)';
1945
+ }
1946
+ else {
1947
+ rcFile = isZsh ? path.join(os.homedir(), '.zshrc') : path.join(os.homedir(), '.bashrc');
1948
+ evalLine = 'eval "$(photon shell init --hook)"';
1949
+ }
1950
+ // --hook flag: output the hook script
1951
+ if (options.hook) {
1952
+ const photonDir = path.join(os.homedir(), '.photon');
1953
+ let photonNames = [];
1954
+ try {
1955
+ const entries = await fs.readdir(photonDir);
1956
+ photonNames = entries
1957
+ .filter((e) => /\.photon\.(ts|js)$/.test(e))
1958
+ .map((e) => e.replace(/\.photon\.(ts|js)$/, ''));
1959
+ }
1960
+ catch {
1961
+ // ~/.photon/ doesn't exist yet
1962
+ }
1963
+ if (shellType === 'zsh') {
1964
+ const functions = photonNames
1965
+ .map((name) => `${name}() { photon cli ${name} "$@"; }`)
1966
+ .join('\n');
1967
+ console.log(`${marker}
1968
+
1969
+ # Shell functions for installed photons (direct invocation)
1970
+ ${functions}
1971
+
1972
+ # Fallback for newly installed photons (before shell restart)
1973
+ command_not_found_handler() {
1974
+ if [ -f "$HOME/.photon/\$1.photon.ts" ] || [ -f "$HOME/.photon/\$1.photon.js" ]; then
1975
+ photon cli "$@"
1976
+ return $?
1977
+ fi
1978
+ echo "zsh: command not found: \$1" >&2
1979
+ return 127
1980
+ }
1981
+
1982
+ # Tab completion for photon methods, params, and instances
1983
+ _photon_cache="$HOME/.photon/cache/completions.cache"
1984
+
1985
+ _photon_complete_direct() {
1986
+ local cmd="\$words[1]"
1987
+ local curcontext="\$curcontext" state line
1988
+ _arguments -C "1: :->method" "*::arg:->params"
1989
+ case "\$state" in
1990
+ method)
1991
+ if [[ -f "\$_photon_cache" ]]; then
1992
+ local -a methods
1993
+ methods=("\${(@f)$(grep "^method:\${cmd}:" "\$_photon_cache" | while IFS=: read -r _ _ name desc; do echo "\${name}:\${desc}"; done)}")
1994
+ _describe 'method' methods
1995
+ fi
1996
+ ;;
1997
+ params)
1998
+ if [[ -f "\$_photon_cache" ]]; then
1999
+ local method="\$line[1]"
2000
+ local -a params
2001
+ params=("\${(@f)$(grep "^param:\${cmd}:\${method}:" "\$_photon_cache" | while IFS=: read -r _ _ _ name type req; do echo "--\${name}[\${type}]"; done)}")
2002
+ _describe 'parameter' params
2003
+ fi
2004
+ ;;
2005
+ esac
2006
+ }
2007
+
2008
+ # Register completion for each photon function (guard for non-interactive shells)
2009
+ if (( $+functions[compdef] )); then
2010
+ ${photonNames.map((name) => ` compdef _photon_complete_direct ${name}`).join('\n')}
2011
+ fi
2012
+
2013
+ # Completion for the photon command itself
2014
+ _photon() {
2015
+ local curcontext="\$curcontext" state line
2016
+ _arguments -C \\
2017
+ "1: :->cmds" \\
2018
+ "*::arg:->args"
2019
+ case "\$state" in
2020
+ cmds)
2021
+ local -a builtins
2022
+ builtins=(
2023
+ 'cli:Run a photon method'
2024
+ 'use:Switch to a named instance'
2025
+ 'instances:List instances of a photon'
2026
+ 'set:Configure environment for a photon'
2027
+ 'beam:Start the interactive UI'
2028
+ 'serve:Start MCP stdio server'
2029
+ 'list:List installed photons'
2030
+ 'add:Install a photon'
2031
+ 'remove:Uninstall a photon'
2032
+ 'search:Search for photons'
2033
+ 'info:Show photon details'
2034
+ 'shell:Shell integration'
2035
+ 'test:Run photon tests'
2036
+ 'doctor:Check system health'
2037
+ )
2038
+ _describe 'command' builtins
2039
+ ;;
2040
+ args)
2041
+ case \$line[1] in
2042
+ cli)
2043
+ local curcontext="\$curcontext" state line
2044
+ _arguments -C "1: :->photon_name" "*::arg:->method_args"
2045
+ case "\$state" in
2046
+ photon_name)
2047
+ if [[ -f "\$_photon_cache" ]]; then
2048
+ local -a photons
2049
+ photons=("\${(@f)$(grep "^photon:" "\$_photon_cache" | while IFS=: read -r _ name desc; do echo "\${name}:\${desc}"; done)}")
2050
+ _describe 'photon' photons
2051
+ fi
2052
+ ;;
2053
+ method_args)
2054
+ words[1]="\$line[1]"
2055
+ _photon_complete_direct
2056
+ ;;
2057
+ esac
2058
+ ;;
2059
+ use|instances|set|info|serve)
2060
+ if [[ -f "\$_photon_cache" ]]; then
2061
+ local curcontext="\$curcontext" state line
2062
+ _arguments -C "1: :->photon_name" "*::arg:->instance"
2063
+ case "\$state" in
2064
+ photon_name)
2065
+ local -a photons
2066
+ photons=("\${(@f)$(grep "^photon:" "\$_photon_cache" | while IFS=: read -r _ name desc; do echo "\${name}:\${desc}"; done)}")
2067
+ _describe 'photon' photons
2068
+ ;;
2069
+ instance)
2070
+ if [[ "\$line[-2]" == "use" ]]; then
2071
+ local -a instances
2072
+ instances=("\${(@f)$(grep "^instance:\${line[1]}:" "\$_photon_cache" | cut -d: -f3)}")
2073
+ [[ \${#instances} -gt 0 ]] && _describe 'instance' instances
2074
+ fi
2075
+ ;;
2076
+ esac
2077
+ fi
2078
+ ;;
2079
+ shell)
2080
+ local -a subcmds
2081
+ subcmds=('init:Set up shell integration' 'completions:Manage completion cache')
2082
+ _describe 'subcommand' subcmds
2083
+ ;;
2084
+ esac
2085
+ ;;
2086
+ esac
2087
+ }
2088
+
2089
+ if (( $+functions[compdef] )); then
2090
+ compdef _photon photon
2091
+ fi`);
2092
+ }
2093
+ else if (shellType === 'bash') {
2094
+ const functions = photonNames
2095
+ .map((name) => `${name}() { photon cli ${name} "$@"; }`)
2096
+ .join('\n');
2097
+ console.log(`${marker}
2098
+
2099
+ # Shell functions for installed photons (direct invocation)
2100
+ ${functions}
2101
+
2102
+ # Fallback for newly installed photons (before shell restart)
2103
+ command_not_found_handle() {
2104
+ if [ -f "$HOME/.photon/\$1.photon.ts" ] || [ -f "$HOME/.photon/\$1.photon.js" ]; then
2105
+ photon cli "$@"
2106
+ return $?
2107
+ fi
2108
+ echo "bash: \$1: command not found" >&2
2109
+ return 127
2110
+ }
2111
+
2112
+ # Tab completion for photon methods, params, and instances
2113
+ _photon_cache="$HOME/.photon/cache/completions.cache"
2114
+
2115
+ _photon_complete_direct() {
2116
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
2117
+ local cmd="\${COMP_WORDS[0]}"
2118
+ COMPREPLY=()
2119
+
2120
+ if [[ ! -f "\$_photon_cache" ]]; then return; fi
2121
+
2122
+ if [[ \$COMP_CWORD -eq 1 ]]; then
2123
+ local methods
2124
+ methods="$(grep "^method:\${cmd}:" "\$_photon_cache" | cut -d: -f3)"
2125
+ COMPREPLY=($(compgen -W "\$methods" -- "\$cur"))
2126
+ elif [[ \$COMP_CWORD -eq 2 ]]; then
2127
+ local method="\${COMP_WORDS[1]}"
2128
+ local params
2129
+ params="$(grep "^param:\${cmd}:\${method}:" "\$_photon_cache" | cut -d: -f4 | sed 's/^/--/')"
2130
+ COMPREPLY=($(compgen -W "\$params" -- "\$cur"))
2131
+ fi
2132
+ }
2133
+
2134
+ _photon_complete() {
2135
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
2136
+ local prev="\${COMP_WORDS[COMP_CWORD-1]}"
2137
+ COMPREPLY=()
2138
+
2139
+ if [[ ! -f "\$_photon_cache" ]]; then return; fi
2140
+
2141
+ if [[ \$COMP_CWORD -eq 1 ]]; then
2142
+ COMPREPLY=($(compgen -W "cli use instances set beam serve list add remove search info shell test doctor" -- "\$cur"))
2143
+ elif [[ \$COMP_CWORD -eq 2 ]]; then
2144
+ case "\${COMP_WORDS[1]}" in
2145
+ cli|use|instances|set|info|serve)
2146
+ local photons
2147
+ photons="$(grep "^photon:" "\$_photon_cache" | cut -d: -f2)"
2148
+ COMPREPLY=($(compgen -W "\$photons" -- "\$cur"))
2149
+ ;;
2150
+ shell)
2151
+ COMPREPLY=($(compgen -W "init completions" -- "\$cur"))
2152
+ ;;
2153
+ esac
2154
+ elif [[ \$COMP_CWORD -eq 3 ]]; then
2155
+ case "\${COMP_WORDS[1]}" in
2156
+ cli)
2157
+ local methods
2158
+ methods="$(grep "^method:\${COMP_WORDS[2]}:" "\$_photon_cache" | cut -d: -f3)"
2159
+ COMPREPLY=($(compgen -W "\$methods" -- "\$cur"))
2160
+ ;;
2161
+ use)
2162
+ local instances
2163
+ instances="$(grep "^instance:\${COMP_WORDS[2]}:" "\$_photon_cache" | cut -d: -f3)"
2164
+ COMPREPLY=($(compgen -W "\$instances" -- "\$cur"))
2165
+ ;;
2166
+ esac
2167
+ elif [[ \$COMP_CWORD -ge 4 && "\${COMP_WORDS[1]}" == "cli" ]]; then
2168
+ local params
2169
+ params="$(grep "^param:\${COMP_WORDS[2]}:\${COMP_WORDS[3]}:" "\$_photon_cache" | cut -d: -f4 | sed 's/^/--/')"
2170
+ COMPREPLY=($(compgen -W "\$params" -- "\$cur"))
2171
+ fi
2172
+ }
2173
+
2174
+ # Register completions
2175
+ ${photonNames.map((name) => `complete -F _photon_complete_direct ${name}`).join('\n')}
2176
+ complete -F _photon_complete photon`);
2177
+ }
2178
+ else if (shellType === 'powershell') {
2179
+ // PowerShell functions and completion
2180
+ const functions = photonNames
2181
+ .map((name) => `function ${name} { photon cli ${name} @Args }`)
2182
+ .join('\n');
2183
+ const functionNames = photonNames.map((n) => `'${n}'`).join(', ');
2184
+ console.log(`${marker}
2185
+
2186
+ # Functions for installed photons (direct invocation)
2187
+ ${functions}
2188
+
2189
+ # Fallback for newly installed photons (CommandNotFoundAction, PowerShell 7.4+)
2190
+ if ($PSVersionTable.PSVersion.Major -ge 7 -and $PSVersionTable.PSVersion.Minor -ge 4) {
2191
+ $ExecutionContext.InvokeCommand.CommandNotFoundAction = {
2192
+ param($Name, $EventArgs)
2193
+ $photonFile = Join-Path $HOME ".photon" "$Name.photon.ts"
2194
+ $photonFileJs = Join-Path $HOME ".photon" "$Name.photon.js"
2195
+ if ((Test-Path $photonFile) -or (Test-Path $photonFileJs)) {
2196
+ $EventArgs.CommandScriptBlock = { photon cli $Name @Args }.GetNewClosure()
2197
+ $EventArgs.StopSearch = $true
2198
+ }
2199
+ }
2200
+ }
2201
+
2202
+ # Tab completion for photon methods, params, and instances
2203
+ $_photonCache = Join-Path $HOME ".photon" "cache" "completions.cache"
2204
+
2205
+ # Completion for direct photon commands
2206
+ ${photonNames
2207
+ .map((name) => `Register-ArgumentCompleter -CommandName ${name} -ScriptBlock {
2208
+ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
2209
+ if (-not (Test-Path $_photonCache)) { return }
2210
+ $pos = $commandAst.CommandElements.Count
2211
+ if ($pos -le 1) {
2212
+ # Complete method names
2213
+ Get-Content $_photonCache | Where-Object { $_ -match "^method:\${commandName}:" } | ForEach-Object {
2214
+ $parts = $_ -split ':', 4
2215
+ [System.Management.Automation.CompletionResult]::new($parts[2], $parts[2], 'ParameterValue', ($parts[3] ?? $parts[2]))
2216
+ } | Where-Object { $_.CompletionText -like "$wordToComplete*" }
2217
+ } elseif ($pos -le 2) {
2218
+ # Complete parameter names
2219
+ $method = $commandAst.CommandElements[1].Value
2220
+ Get-Content $_photonCache | Where-Object { $_ -match "^param:\${commandName}:\${method}:" } | ForEach-Object {
2221
+ $parts = $_ -split ':', 6
2222
+ $paramName = "--$($parts[3])"
2223
+ [System.Management.Automation.CompletionResult]::new($paramName, $paramName, 'ParameterName', "$($parts[4]) parameter")
2224
+ } | Where-Object { $_.CompletionText -like "$wordToComplete*" }
2225
+ }
2226
+ }`)
2227
+ .join('\n')}
2228
+
2229
+ # Completion for the photon command itself
2230
+ Register-ArgumentCompleter -CommandName photon -ScriptBlock {
2231
+ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
2232
+ $pos = $commandAst.CommandElements.Count
2233
+ if ($pos -le 1) {
2234
+ @('cli','use','instances','set','beam','serve','list','add','remove','search','info','shell','test','doctor') |
2235
+ Where-Object { $_ -like "$wordToComplete*" } |
2236
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
2237
+ } elseif ($pos -le 2) {
2238
+ $sub = $commandAst.CommandElements[1].Value
2239
+ switch ($sub) {
2240
+ { $_ -in 'cli','use','instances','set','info','serve' } {
2241
+ if (Test-Path $_photonCache) {
2242
+ Get-Content $_photonCache | Where-Object { $_ -match "^photon:" } | ForEach-Object {
2243
+ $parts = $_ -split ':', 3
2244
+ [System.Management.Automation.CompletionResult]::new($parts[1], $parts[1], 'ParameterValue', ($parts[2] ?? $parts[1]))
2245
+ } | Where-Object { $_.CompletionText -like "$wordToComplete*" }
2246
+ }
2247
+ }
2248
+ 'shell' {
2249
+ @('init','completions') | Where-Object { $_ -like "$wordToComplete*" } |
2250
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
2251
+ }
2252
+ }
2253
+ } elseif ($pos -le 3) {
2254
+ $sub = $commandAst.CommandElements[1].Value
2255
+ $photonName = $commandAst.CommandElements[2].Value
2256
+ if ($sub -eq 'cli' -and (Test-Path $_photonCache)) {
2257
+ Get-Content $_photonCache | Where-Object { $_ -match "^method:\${photonName}:" } | ForEach-Object {
2258
+ $parts = $_ -split ':', 4
2259
+ [System.Management.Automation.CompletionResult]::new($parts[2], $parts[2], 'ParameterValue', ($parts[3] ?? $parts[2]))
2260
+ } | Where-Object { $_.CompletionText -like "$wordToComplete*" }
2261
+ } elseif ($sub -eq 'use' -and (Test-Path $_photonCache)) {
2262
+ Get-Content $_photonCache | Where-Object { $_ -match "^instance:\${photonName}:" } | ForEach-Object {
2263
+ $parts = $_ -split ':', 3
2264
+ [System.Management.Automation.CompletionResult]::new($parts[2], $parts[2], 'ParameterValue', $parts[2])
2265
+ } | Where-Object { $_.CompletionText -like "$wordToComplete*" }
2266
+ }
2267
+ }
2268
+ }`);
2269
+ }
2270
+ // Silently generate cache on first hook (non-blocking)
2271
+ const { CACHE_FILE } = await import('./shell-completions.js');
2272
+ try {
2273
+ await fs.access(CACHE_FILE);
2274
+ }
2275
+ catch {
2276
+ // Cache doesn't exist yet — generate it
2277
+ const { generateCompletionCache } = await import('./shell-completions.js');
2278
+ await generateCompletionCache();
2279
+ }
2280
+ return;
2281
+ }
2282
+ // Interactive mode → install into rc file
2283
+ try {
2284
+ // Ensure profile directory exists (PowerShell profile dir may not)
2285
+ const rcDir = path.dirname(rcFile);
2286
+ await fs.mkdir(rcDir, { recursive: true });
2287
+ let rcContent = '';
2288
+ try {
2289
+ rcContent = await fs.readFile(rcFile, 'utf-8');
2290
+ }
2291
+ catch {
2292
+ // rc file doesn't exist, we'll create it
2293
+ }
2294
+ if (rcContent.includes(marker) || rcContent.includes(evalLine)) {
2295
+ printInfo(`Shell integration already installed in ${rcFile}`);
2296
+ if (shellType === 'powershell') {
2297
+ console.log(` Restart PowerShell or run: . $PROFILE`);
2298
+ }
2299
+ else {
2300
+ console.log(` Restart your shell or run: source ${rcFile}`);
2301
+ }
2302
+ return;
2303
+ }
2304
+ const block = `\n${marker}\n${evalLine}\n`;
2305
+ await fs.appendFile(rcFile, block);
2306
+ // Generate completions cache
2307
+ const { generateCompletionCache } = await import('./shell-completions.js');
2308
+ await generateCompletionCache();
2309
+ printSuccess(`Installed shell integration into ${rcFile}`);
2310
+ if (shellType === 'powershell') {
2311
+ console.log(` Restart PowerShell or run: . $PROFILE`);
2312
+ }
2313
+ else {
2314
+ console.log(` Restart your shell or run: source ${rcFile}`);
2315
+ }
2316
+ console.log('');
2317
+ console.log(` Then type any photon name directly:`);
2318
+ console.log(` list get → photon cli list get`);
2319
+ console.log(` list add "Milk" → photon cli list add "Milk"`);
2320
+ console.log('');
2321
+ console.log(' Tab completion is enabled for:');
2322
+ console.log(' Photon names, methods, parameters, and instances.');
2323
+ }
2324
+ catch (error) {
2325
+ printError(`Failed to update ${rcFile}: ${getErrorMessage(error)}`);
2326
+ console.log(` Add this line manually to your shell profile:`);
2327
+ console.log(` ${evalLine}`);
2328
+ process.exit(1);
2329
+ }
2330
+ });
2331
+ shell
2332
+ .command('completions')
2333
+ .option('--generate', 'Regenerate the completions cache')
2334
+ .description('Manage shell completion cache')
2335
+ .action(async (options) => {
2336
+ const { generateCompletionCache, CACHE_FILE } = await import('./shell-completions.js');
2337
+ if (options.generate) {
2338
+ await generateCompletionCache();
2339
+ printSuccess(`Completions cache updated: ${CACHE_FILE}`);
2340
+ return;
2341
+ }
2342
+ // Default: show cache status
2343
+ try {
2344
+ const stat = await fs.stat(CACHE_FILE);
2345
+ const age = Date.now() - stat.mtimeMs;
2346
+ const ageStr = age < 60_000
2347
+ ? 'just now'
2348
+ : age < 3_600_000
2349
+ ? `${Math.floor(age / 60_000)}m ago`
2350
+ : `${Math.floor(age / 3_600_000)}h ago`;
2351
+ printInfo(`Cache: ${CACHE_FILE}`);
2352
+ console.log(` Last updated: ${ageStr}`);
2353
+ console.log(` Run \`photon shell completions --generate\` to refresh`);
2354
+ }
2355
+ catch {
2356
+ printInfo('No completions cache found.');
2357
+ console.log(' Run `photon shell completions --generate` to create one.');
2358
+ }
2359
+ });
2360
+ // Set command: configure environment for photons (primitive params without defaults)
2361
+ program
2362
+ .command('set')
2363
+ .argument('<photon>', 'Photon name')
2364
+ .argument('[args...]', 'Environment values (name=value pairs)')
2365
+ .description('Configure environment for a photon (params without defaults)')
2366
+ .action(async (photonName, args) => {
2367
+ try {
2368
+ const workingDir = program.opts().dir || DEFAULT_WORKING_DIR;
2369
+ // Resolve photon path
2370
+ const filePath = await resolvePhotonPathWithBundled(photonName, workingDir);
2371
+ if (!filePath) {
2372
+ printError(`Photon not found: ${photonName}`);
2373
+ process.exit(1);
2374
+ }
2375
+ // Extract constructor params and filter env params
2376
+ const allParams = await extractConstructorParams(filePath);
2377
+ const { getEnvParams, EnvStore } = await import('./context-store.js');
2378
+ const envParams = getEnvParams(allParams);
2379
+ if (envParams.length === 0) {
2380
+ printInfo(`${photonName} has no environment parameters.`);
2381
+ return;
2382
+ }
2383
+ const store = new EnvStore();
2384
+ // Parse name=value pairs from args
2385
+ const values = {};
2386
+ const paramNames = new Set(envParams.map((p) => p.name));
2387
+ for (const arg of args) {
2388
+ const eqIdx = arg.indexOf('=');
2389
+ if (eqIdx > 0) {
2390
+ const key = arg.slice(0, eqIdx);
2391
+ const val = arg.slice(eqIdx + 1);
2392
+ if (paramNames.has(key)) {
2393
+ values[key] = val;
2394
+ }
2395
+ }
2396
+ else if (envParams.length === 1) {
2397
+ // Single env param: positional value
2398
+ values[envParams[0].name] = arg;
2399
+ }
2400
+ }
2401
+ // Find params that still need values
2402
+ const remaining = envParams.filter((p) => !(p.name in values));
2403
+ if (remaining.length > 0) {
2404
+ // Interactive mode for remaining params
2405
+ cliHeading(`${photonName} — Environment`);
2406
+ cliSpacer();
2407
+ const masked = store.getMasked(photonName);
2408
+ for (const param of remaining) {
2409
+ const currentDisplay = masked[param.name] ? `Current: ${masked[param.name]}` : 'Not set';
2410
+ const answer = await promptText(` ${param.name} (required)\n ${currentDisplay}\n > `);
2411
+ if (answer.trim() !== '') {
2412
+ values[param.name] = answer.trim();
2413
+ }
2414
+ }
2415
+ }
2416
+ if (Object.keys(values).length > 0) {
2417
+ store.write(photonName, values);
2418
+ const summary = Object.keys(values).join(', ');
2419
+ printSuccess(`Environment saved: ${summary}`);
2420
+ }
2421
+ else {
2422
+ printInfo('No changes.');
2423
+ }
2424
+ }
2425
+ catch (error) {
2426
+ printError(getErrorMessage(error));
2427
+ process.exit(1);
2428
+ }
2429
+ });
1818
2430
  // Alias commands: create CLI shortcuts for photons
1819
2431
  program
1820
2432
  .command('alias', { hidden: true })
@@ -1899,6 +2511,10 @@ const RESERVED_COMMANDS = [
1899
2511
  'doctor',
1900
2512
  'clear-cache',
1901
2513
  'clean',
2514
+ // Instance/env
2515
+ 'use',
2516
+ 'instances',
2517
+ 'set',
1902
2518
  // Aliases
1903
2519
  'cli',
1904
2520
  'alias',
@@ -1913,6 +2529,7 @@ const RESERVED_COMMANDS = [
1913
2529
  'search',
1914
2530
  'maker',
1915
2531
  'host',
2532
+ 'shell',
1916
2533
  'diagram',
1917
2534
  'diagrams',
1918
2535
  'enable',
@@ -1947,6 +2564,9 @@ const knownCommands = [
1947
2564
  'clear-cache',
1948
2565
  'clean',
1949
2566
  'doctor',
2567
+ 'use',
2568
+ 'instances',
2569
+ 'set',
1950
2570
  'cli',
1951
2571
  'alias',
1952
2572
  'unalias',
@@ -1956,12 +2576,14 @@ const knownCommands = [
1956
2576
  'marketplace',
1957
2577
  'maker',
1958
2578
  'host',
2579
+ 'shell',
1959
2580
  'diagram',
1960
2581
  'diagrams',
1961
2582
  ];
1962
2583
  const knownSubcommands = {
1963
2584
  marketplace: ['list', 'add', 'remove', 'enable', 'disable'],
1964
2585
  maker: ['new', 'validate', 'sync', 'init'],
2586
+ shell: ['init', 'completions'],
1965
2587
  };
1966
2588
  /**
1967
2589
  * Calculate Levenshtein distance between two strings
@@ -2039,9 +2661,10 @@ program.on('command:*', async (operands) => {
2039
2661
  // This enables: `photon lg-remote volume +5` instead of `photon cli lg-remote volume +5`
2040
2662
  function preprocessArgs() {
2041
2663
  const args = process.argv.slice(2);
2042
- // No args - default to beam with auto-open browser
2664
+ // No args - launch Beam (the primary interface)
2665
+ // Use `photon -h` or `photon --help` for help
2043
2666
  if (args.length === 0) {
2044
- return [...process.argv, 'beam', '--open'];
2667
+ return [...process.argv, 'beam'];
2045
2668
  }
2046
2669
  // Find the first non-flag argument (skip values of flags that take a parameter)
2047
2670
  const flagsWithValues = ['--dir', '--log-level'];
@@ -2059,8 +2682,8 @@ function preprocessArgs() {
2059
2682
  if (args.some((a) => a === '--help' || a === '-h' || a === '--version' || a === '-V')) {
2060
2683
  return process.argv;
2061
2684
  }
2062
- // Otherwise default to beam (e.g., photon --dir=. → photon --dir=. beam --open)
2063
- return [...process.argv, 'beam', '--open'];
2685
+ // Otherwise launch Beam (e.g., photon --dir=.)
2686
+ return [...process.argv, 'beam'];
2064
2687
  }
2065
2688
  const firstArg = args[firstArgIndex];
2066
2689
  // If first arg is a reserved command, let commander handle normally