@portel/photon 1.7.0 → 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 (94) hide show
  1. package/README.md +23 -24
  2. package/dist/auto-ui/beam.d.ts.map +1 -1
  3. package/dist/auto-ui/beam.js +117 -42
  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 +1 -1
  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/rendering/components.d.ts.map +1 -1
  11. package/dist/auto-ui/rendering/components.js +568 -0
  12. package/dist/auto-ui/rendering/components.js.map +1 -1
  13. package/dist/auto-ui/rendering/field-analyzer.d.ts +56 -0
  14. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -1
  15. package/dist/auto-ui/rendering/field-analyzer.js +177 -0
  16. package/dist/auto-ui/rendering/field-analyzer.js.map +1 -1
  17. package/dist/auto-ui/rendering/layout-selector.d.ts +14 -2
  18. package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -1
  19. package/dist/auto-ui/rendering/layout-selector.js +125 -1
  20. package/dist/auto-ui/rendering/layout-selector.js.map +1 -1
  21. package/dist/auto-ui/streamable-http-transport.d.ts +1 -1
  22. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  23. package/dist/auto-ui/streamable-http-transport.js +353 -19
  24. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  25. package/dist/auto-ui/types.d.ts +7 -1
  26. package/dist/auto-ui/types.d.ts.map +1 -1
  27. package/dist/auto-ui/types.js.map +1 -1
  28. package/dist/beam.bundle.js +22441 -4216
  29. package/dist/beam.bundle.js.map +4 -4
  30. package/dist/cli/commands/info.d.ts.map +1 -1
  31. package/dist/cli/commands/info.js +37 -0
  32. package/dist/cli/commands/info.js.map +1 -1
  33. package/dist/cli/commands/package.d.ts.map +1 -1
  34. package/dist/cli/commands/package.js +16 -0
  35. package/dist/cli/commands/package.js.map +1 -1
  36. package/dist/cli.d.ts.map +1 -1
  37. package/dist/cli.js +628 -14
  38. package/dist/cli.js.map +1 -1
  39. package/dist/context-store.d.ts +79 -0
  40. package/dist/context-store.d.ts.map +1 -0
  41. package/dist/context-store.js +210 -0
  42. package/dist/context-store.js.map +1 -0
  43. package/dist/daemon/client.d.ts +13 -4
  44. package/dist/daemon/client.d.ts.map +1 -1
  45. package/dist/daemon/client.js +138 -77
  46. package/dist/daemon/client.js.map +1 -1
  47. package/dist/daemon/manager.d.ts +0 -25
  48. package/dist/daemon/manager.d.ts.map +1 -1
  49. package/dist/daemon/manager.js +10 -38
  50. package/dist/daemon/manager.js.map +1 -1
  51. package/dist/daemon/protocol.d.ts +7 -2
  52. package/dist/daemon/protocol.d.ts.map +1 -1
  53. package/dist/daemon/protocol.js.map +1 -1
  54. package/dist/daemon/server.js +257 -35
  55. package/dist/daemon/server.js.map +1 -1
  56. package/dist/daemon/session-manager.d.ts +24 -4
  57. package/dist/daemon/session-manager.d.ts.map +1 -1
  58. package/dist/daemon/session-manager.js +62 -12
  59. package/dist/daemon/session-manager.js.map +1 -1
  60. package/dist/index.d.ts +0 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +0 -3
  63. package/dist/index.js.map +1 -1
  64. package/dist/loader.d.ts +3 -20
  65. package/dist/loader.d.ts.map +1 -1
  66. package/dist/loader.js +53 -75
  67. package/dist/loader.js.map +1 -1
  68. package/dist/photon-cli-runner.d.ts.map +1 -1
  69. package/dist/photon-cli-runner.js +258 -218
  70. package/dist/photon-cli-runner.js.map +1 -1
  71. package/dist/photon-doc-extractor.d.ts +2 -0
  72. package/dist/photon-doc-extractor.d.ts.map +1 -1
  73. package/dist/photon-doc-extractor.js +42 -6
  74. package/dist/photon-doc-extractor.js.map +1 -1
  75. package/dist/photons/maker.photon.d.ts.map +1 -1
  76. package/dist/photons/maker.photon.js +3 -1
  77. package/dist/photons/maker.photon.js.map +1 -1
  78. package/dist/photons/maker.photon.ts +3 -1
  79. package/dist/serv/index.d.ts.map +1 -1
  80. package/dist/serv/index.js.map +1 -1
  81. package/dist/server.d.ts +32 -15
  82. package/dist/server.d.ts.map +1 -1
  83. package/dist/server.js +468 -469
  84. package/dist/server.js.map +1 -1
  85. package/dist/shared/security.d.ts.map +1 -1
  86. package/dist/shared/security.js +4 -8
  87. package/dist/shared/security.js.map +1 -1
  88. package/dist/shell-completions.d.ts +21 -0
  89. package/dist/shell-completions.d.ts.map +1 -0
  90. package/dist/shell-completions.js +102 -0
  91. package/dist/shell-completions.js.map +1 -0
  92. package/dist/template-manager.d.ts.map +1 -1
  93. package/dist/template-manager.js.map +1 -1
  94. package/package.json +11 -7
package/dist/cli.js CHANGED
@@ -24,9 +24,9 @@ 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';
@@ -259,7 +259,10 @@ async function handleUrlElicitation(ask) {
259
259
  execFile(openCommand, [ask.url]);
260
260
  }
261
261
  catch (error) {
262
- 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.`);
263
266
  }
264
267
  const shouldContinue = await promptWait('Press Enter when done', true);
265
268
  return { action: shouldContinue ? 'accept' : 'cancel' };
@@ -758,6 +761,9 @@ program
758
761
  .configureHelp({
759
762
  sortSubcommands: false,
760
763
  sortOptions: false,
764
+ // Hide Commander's auto-generated "Commands:" section since we show
765
+ // a custom categorized section in addHelpText('after', ...)
766
+ visibleCommands: () => [],
761
767
  })
762
768
  .addHelpText('after', `
763
769
  Runtime Commands:
@@ -767,6 +773,11 @@ Runtime Commands:
767
773
  beam Launch Photon Beam (interactive control panel)
768
774
  serve Start local multi-tenant MCP hosting for development
769
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
+
770
781
  Hosting:
771
782
  host <command> Manage cloud hosting (preview, deploy)
772
783
 
@@ -1113,8 +1124,12 @@ program
1113
1124
  logger.debug(`Could not auto-open browser: ${err.message}`);
1114
1125
  });
1115
1126
  }
1116
- // Handle shutdown signals
1127
+ // Handle shutdown signals (guard against duplicate Ctrl+C)
1128
+ let shuttingDown = false;
1117
1129
  const shutdown = async () => {
1130
+ if (shuttingDown)
1131
+ return;
1132
+ shuttingDown = true;
1118
1133
  console.error('\nShutting down Photon Beam...');
1119
1134
  // Gracefully close external MCP clients to prevent ugly tracebacks
1120
1135
  try {
@@ -1363,11 +1378,15 @@ maker
1363
1378
  // Check if file already exists
1364
1379
  try {
1365
1380
  await fs.access(filePath);
1366
- logger.error(`File already exists: ${filePath}`);
1367
- process.exit(1);
1381
+ exitWithError(`File already exists: ${filePath}`, {
1382
+ suggestion: `Choose a different name or delete the existing file`,
1383
+ });
1368
1384
  }
1369
- catch {
1370
- // 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
1371
1390
  }
1372
1391
  // Read template
1373
1392
  const templatePath = path.join(__dirname, '..', 'templates', 'photon.template.ts');
@@ -1375,8 +1394,8 @@ maker
1375
1394
  try {
1376
1395
  template = await fs.readFile(templatePath, 'utf-8');
1377
1396
  }
1378
- catch {
1379
- // Fallback inline template if file not found
1397
+ catch (err) {
1398
+ logger.debug(`Template not found at ${templatePath}, using inline template`);
1380
1399
  template = getInlineTemplate();
1381
1400
  }
1382
1401
  // Replace placeholders
@@ -1824,6 +1843,590 @@ SEE ALSO:
1824
1843
  await runMethod(photon, method, args);
1825
1844
  }
1826
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
+ });
1827
2430
  // Alias commands: create CLI shortcuts for photons
1828
2431
  program
1829
2432
  .command('alias', { hidden: true })
@@ -1908,6 +2511,10 @@ const RESERVED_COMMANDS = [
1908
2511
  'doctor',
1909
2512
  'clear-cache',
1910
2513
  'clean',
2514
+ // Instance/env
2515
+ 'use',
2516
+ 'instances',
2517
+ 'set',
1911
2518
  // Aliases
1912
2519
  'cli',
1913
2520
  'alias',
@@ -1922,6 +2529,7 @@ const RESERVED_COMMANDS = [
1922
2529
  'search',
1923
2530
  'maker',
1924
2531
  'host',
2532
+ 'shell',
1925
2533
  'diagram',
1926
2534
  'diagrams',
1927
2535
  'enable',
@@ -1956,6 +2564,9 @@ const knownCommands = [
1956
2564
  'clear-cache',
1957
2565
  'clean',
1958
2566
  'doctor',
2567
+ 'use',
2568
+ 'instances',
2569
+ 'set',
1959
2570
  'cli',
1960
2571
  'alias',
1961
2572
  'unalias',
@@ -1965,12 +2576,14 @@ const knownCommands = [
1965
2576
  'marketplace',
1966
2577
  'maker',
1967
2578
  'host',
2579
+ 'shell',
1968
2580
  'diagram',
1969
2581
  'diagrams',
1970
2582
  ];
1971
2583
  const knownSubcommands = {
1972
2584
  marketplace: ['list', 'add', 'remove', 'enable', 'disable'],
1973
2585
  maker: ['new', 'validate', 'sync', 'init'],
2586
+ shell: ['init', 'completions'],
1974
2587
  };
1975
2588
  /**
1976
2589
  * Calculate Levenshtein distance between two strings
@@ -2048,9 +2661,10 @@ program.on('command:*', async (operands) => {
2048
2661
  // This enables: `photon lg-remote volume +5` instead of `photon cli lg-remote volume +5`
2049
2662
  function preprocessArgs() {
2050
2663
  const args = process.argv.slice(2);
2051
- // 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
2052
2666
  if (args.length === 0) {
2053
- return [...process.argv, 'beam', '--open'];
2667
+ return [...process.argv, 'beam'];
2054
2668
  }
2055
2669
  // Find the first non-flag argument (skip values of flags that take a parameter)
2056
2670
  const flagsWithValues = ['--dir', '--log-level'];
@@ -2068,8 +2682,8 @@ function preprocessArgs() {
2068
2682
  if (args.some((a) => a === '--help' || a === '-h' || a === '--version' || a === '-V')) {
2069
2683
  return process.argv;
2070
2684
  }
2071
- // Otherwise default to beam (e.g., photon --dir=. → photon --dir=. beam --open)
2072
- return [...process.argv, 'beam', '--open'];
2685
+ // Otherwise launch Beam (e.g., photon --dir=.)
2686
+ return [...process.argv, 'beam'];
2073
2687
  }
2074
2688
  const firstArg = args[firstArgIndex];
2075
2689
  // If first arg is a reserved command, let commander handle normally