@openchamber/web 1.11.6 → 1.11.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/bin/cli.js +443 -2
- package/dist/assets/{MarkdownRendererImpl-COdbjw73.js → MarkdownRendererImpl-DaF15QNC.js} +3 -3
- package/dist/assets/{MultiRunWindow-BKSHxjMq.js → MultiRunWindow-Cl7wS_CB.js} +1 -1
- package/dist/assets/{OnboardingScreen-Chjg337p.js → OnboardingScreen-DTv6YJI1.js} +2 -2
- package/dist/assets/{SettingsWindow-C0lRRW8M.js → SettingsWindow-_c3TTL2z.js} +1 -1
- package/dist/assets/{TerminalView-Bvil3j1u.js → TerminalView-CuXkDROt.js} +3 -3
- package/dist/assets/es-CYoUf2D-.js +15 -0
- package/dist/assets/{index-B9LvUHdG.js → index-3WXrN3AX.js} +1 -1
- package/dist/assets/index-BREIbhcb.css +1 -0
- package/dist/assets/ko-2tM0fIna.js +15 -0
- package/dist/assets/main-BF3kWAJ9.js +239 -0
- package/dist/assets/{main-Blhx9Fp5.js → main-o8ZERrmU.js} +2 -2
- package/dist/assets/miniChat-BZQjpK23.js +2 -0
- package/dist/assets/{modelPrefsAutoSave-DRJSYigo.js → modelPrefsAutoSave-wwnbqBk7.js} +110 -108
- package/dist/assets/pl-Dq8uAotM.js +15 -0
- package/dist/assets/pt-BR-nh9s9DFT.js +15 -0
- package/dist/assets/{renderElectronMiniChatApp-BxZRI73j.js → renderElectronMiniChatApp-C-Ezew9P.js} +2 -2
- package/dist/assets/uk-BZtz0wUV.js +15 -0
- package/dist/assets/{vendor-.bun-Bum-iBXX.js → vendor-.bun-CV3tusA8.js} +1 -1
- package/dist/assets/zh-CN-j_nYMchE.js +15 -0
- package/dist/assets/zh-TW-B11UpkDJ.js +15 -0
- package/dist/index.html +11 -28
- package/dist/mini-chat.html +4 -4
- package/package.json +1 -1
- package/server/lib/fs/routes.js +5 -0
- package/server/lib/fs/routes.test.js +61 -1
- package/server/lib/git/DOCUMENTATION.md +1 -0
- package/server/lib/git/routes.js +82 -1
- package/server/lib/git/service.js +338 -19
- package/server/lib/git/service.test.js +414 -8
- package/server/lib/opencode/env-runtime.js +52 -4
- package/server/lib/opencode/env-runtime.test.js +82 -6
- package/server/lib/opencode/openchamber-routes.js +9 -7
- package/server/lib/opencode/settings-helpers.js +3 -0
- package/server/lib/opencode/settings-runtime.js +39 -1
- package/server/lib/opencode/settings-runtime.test.js +39 -0
- package/server/lib/skills-catalog/source.js +1 -1
- package/dist/assets/es-BZIAUghG.js +0 -15
- package/dist/assets/index-UcCH2KN9.css +0 -1
- package/dist/assets/ko-DU9l-zox.js +0 -15
- package/dist/assets/main-d2-dY4er.js +0 -232
- package/dist/assets/miniChat-CJ7-rZFl.js +0 -2
- package/dist/assets/pl-CdqzokG-.js +0 -15
- package/dist/assets/pt-BR-Bknbr_Y3.js +0 -15
- package/dist/assets/uk-Be4E8ZNO.js +0 -15
- package/dist/assets/zh-CN-qpPiaZMg.js +0 -15
package/README.md
CHANGED
|
@@ -24,6 +24,10 @@ Or install manually: `bun add -g @openchamber/web` (or npm, pnpm, yarn).
|
|
|
24
24
|
openchamber # Start on port 3000
|
|
25
25
|
openchamber --port 8080 # Custom port
|
|
26
26
|
openchamber --ui-password secret # Password-protect UI
|
|
27
|
+
openchamber startup enable # Start at login as a native service
|
|
28
|
+
OPENCHAMBER_UI_PASSWORD=secret openchamber startup enable # Save service password env
|
|
29
|
+
openchamber startup status # Show startup service status
|
|
30
|
+
openchamber startup disable # Remove startup service
|
|
27
31
|
openchamber tunnel help # Tunnel lifecycle commands
|
|
28
32
|
openchamber tunnel providers # Show provider capabilities
|
|
29
33
|
openchamber tunnel profile add --provider cloudflare --mode managed-remote --name prod-main --hostname app.example.com --token <token>
|
|
@@ -39,6 +43,8 @@ openchamber stop # Stop server
|
|
|
39
43
|
openchamber update # Update to latest version
|
|
40
44
|
```
|
|
41
45
|
|
|
46
|
+
`startup enable` snapshots your current environment into the native service so startup behaves like you launched `openchamber` from the same shell. This preserves provider tokens, PATH, SSH agent settings, and other CLI auth/config env vars. Use `--no-env-snapshot` for a minimal service env.
|
|
47
|
+
|
|
42
48
|
### Tunnel behavior notes
|
|
43
49
|
|
|
44
50
|
- One active tunnel per running OpenChamber instance (port).
|
package/bin/cli.js
CHANGED
|
@@ -32,6 +32,7 @@ const DEFAULT_TAIL_LINES = 200;
|
|
|
32
32
|
const DAEMON_READY_TIMEOUT_MS = 30000;
|
|
33
33
|
const LOG_ROTATE_MAX_BYTES = 10 * 1024 * 1024;
|
|
34
34
|
const LOG_ROTATE_KEEP = 5;
|
|
35
|
+
const STARTUP_SERVICE_ID = 'dev.openchamber.web';
|
|
35
36
|
const TUNNEL_PROFILES_VERSION = 1;
|
|
36
37
|
const TUNNEL_PROFILES_FILE_NAME = 'tunnel-profiles.json';
|
|
37
38
|
const LEGACY_CLOUDFLARE_MANAGED_REMOTE_FILE_NAME = 'cloudflare-managed-remote-tunnels.json';
|
|
@@ -601,6 +602,7 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
601
602
|
quiet: false,
|
|
602
603
|
explicitPort: false,
|
|
603
604
|
explicitUiPassword: false,
|
|
605
|
+
envSnapshot: true,
|
|
604
606
|
foreground: false,
|
|
605
607
|
};
|
|
606
608
|
|
|
@@ -753,6 +755,9 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
753
755
|
case 'no-follow':
|
|
754
756
|
options.follow = false;
|
|
755
757
|
break;
|
|
758
|
+
case 'no-env-snapshot':
|
|
759
|
+
options.envSnapshot = false;
|
|
760
|
+
break;
|
|
756
761
|
case 'lines': {
|
|
757
762
|
const { value, nextIndex } = consumeValue(i, inlineValue);
|
|
758
763
|
i = nextIndex;
|
|
@@ -833,11 +838,13 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
833
838
|
const command = positional[0] || 'serve';
|
|
834
839
|
const subcommand = command === 'tunnel' ? (positional[1] || 'help') : null;
|
|
835
840
|
const tunnelAction = command === 'tunnel' ? (positional[2] || null) : null;
|
|
841
|
+
const startupAction = command === 'startup' ? (positional[1] || 'status') : null;
|
|
836
842
|
|
|
837
843
|
return {
|
|
838
844
|
command,
|
|
839
845
|
subcommand,
|
|
840
846
|
tunnelAction,
|
|
847
|
+
startupAction,
|
|
841
848
|
options,
|
|
842
849
|
removedFlagErrors,
|
|
843
850
|
helpRequested,
|
|
@@ -858,6 +865,7 @@ COMMANDS:
|
|
|
858
865
|
restart Stop and start the server
|
|
859
866
|
status Show server status
|
|
860
867
|
tunnel Tunnel lifecycle commands
|
|
868
|
+
startup Manage launch at system startup
|
|
861
869
|
logs Tail OpenChamber logs
|
|
862
870
|
update Check for and install updates
|
|
863
871
|
|
|
@@ -883,11 +891,39 @@ EXAMPLES:
|
|
|
883
891
|
openchamber # Start in daemon mode on default port 3000 (or free port)
|
|
884
892
|
openchamber --port 8080 # Start on port 8080 (daemon)
|
|
885
893
|
openchamber serve --foreground # Start in foreground (for systemd Type=simple)
|
|
894
|
+
openchamber startup enable # Start OpenChamber at user login
|
|
886
895
|
openchamber tunnel help # Show tunnel lifecycle help
|
|
887
896
|
openchamber logs # Follow logs for latest running instance
|
|
888
897
|
`);
|
|
889
898
|
}
|
|
890
899
|
|
|
900
|
+
function showStartupHelp() {
|
|
901
|
+
console.log(`
|
|
902
|
+
OpenChamber Startup Commands
|
|
903
|
+
|
|
904
|
+
USAGE:
|
|
905
|
+
openchamber startup <SUBCOMMAND> [OPTIONS]
|
|
906
|
+
|
|
907
|
+
SUBCOMMANDS:
|
|
908
|
+
status Show startup integration status
|
|
909
|
+
enable Install and start native user startup integration
|
|
910
|
+
disable Stop and remove native user startup integration
|
|
911
|
+
|
|
912
|
+
OPTIONS:
|
|
913
|
+
-p, --port Web server port used by startup service
|
|
914
|
+
--host Bind address used by startup service
|
|
915
|
+
--ui-password Protect browser UI with single password
|
|
916
|
+
--no-env-snapshot Do not save current environment for startup service
|
|
917
|
+
--json Output machine-readable JSON
|
|
918
|
+
-q, --quiet Suppress non-essential output
|
|
919
|
+
|
|
920
|
+
EXAMPLES:
|
|
921
|
+
openchamber startup enable
|
|
922
|
+
openchamber startup enable --port 3000
|
|
923
|
+
openchamber startup status --json
|
|
924
|
+
`);
|
|
925
|
+
}
|
|
926
|
+
|
|
891
927
|
function showTunnelHelp() {
|
|
892
928
|
console.log(`
|
|
893
929
|
Tunnel Lifecycle Commands
|
|
@@ -1702,6 +1738,352 @@ function getRunDir() {
|
|
|
1702
1738
|
return dir;
|
|
1703
1739
|
}
|
|
1704
1740
|
|
|
1741
|
+
function getStartupServicePaths() {
|
|
1742
|
+
if (process.platform === 'darwin') {
|
|
1743
|
+
return {
|
|
1744
|
+
platform: 'macos',
|
|
1745
|
+
servicePath: path.join(os.homedir(), 'Library', 'LaunchAgents', `${STARTUP_SERVICE_ID}.plist`),
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
if (process.platform === 'linux') {
|
|
1749
|
+
return {
|
|
1750
|
+
platform: 'linux',
|
|
1751
|
+
servicePath: path.join(os.homedir(), '.config', 'systemd', 'user', 'openchamber.service'),
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
if (process.platform === 'win32') {
|
|
1755
|
+
return { platform: 'windows', servicePath: STARTUP_SERVICE_ID };
|
|
1756
|
+
}
|
|
1757
|
+
return { platform: process.platform, servicePath: null };
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function escapeXml(value) {
|
|
1761
|
+
return String(value)
|
|
1762
|
+
.replace(/&/g, '&')
|
|
1763
|
+
.replace(/</g, '<')
|
|
1764
|
+
.replace(/>/g, '>')
|
|
1765
|
+
.replace(/"/g, '"')
|
|
1766
|
+
.replace(/'/g, ''');
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function systemdEscapeArg(value) {
|
|
1770
|
+
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function startupShellQuote(value) {
|
|
1774
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function systemdUnitPath(value) {
|
|
1778
|
+
return String(value).replace(/\\/g, '\\\\').replace(/ /g, '\\x20');
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function powershellQuote(value) {
|
|
1782
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
function startupEnvFileQuote(value) {
|
|
1786
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function systemdEnvFileQuote(value) {
|
|
1790
|
+
return `"${String(value)
|
|
1791
|
+
.replace(/\\/g, '\\\\')
|
|
1792
|
+
.replace(/"/g, '\\"')
|
|
1793
|
+
.replace(/`/g, '\\`')
|
|
1794
|
+
.replace(/\$/g, '\\$')}"`;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function getStartupEnvFilePath() {
|
|
1798
|
+
return path.join(getDataDir(), 'startup.env');
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function getMacosStartupWrapperPath() {
|
|
1802
|
+
return path.join(getDataDir(), 'bin', 'OpenChamber');
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function collectStartupEnv(options = {}) {
|
|
1806
|
+
const env = options.envSnapshot === false ? {} : Object.fromEntries(
|
|
1807
|
+
Object.entries(process.env)
|
|
1808
|
+
.filter(([key, value]) => shouldPersistStartupEnv(key, value))
|
|
1809
|
+
.map(([key, value]) => [key, String(value)])
|
|
1810
|
+
);
|
|
1811
|
+
|
|
1812
|
+
if (options.envSnapshot !== false) {
|
|
1813
|
+
const opencodeBinary = process.env.OPENCODE_BINARY || searchPathFor('opencode');
|
|
1814
|
+
if (typeof opencodeBinary === 'string' && opencodeBinary.trim().length > 0) {
|
|
1815
|
+
env.OPENCODE_BINARY = opencodeBinary.trim();
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
const uiPassword = hasUiPasswordConfigured(options.uiPassword) ? options.uiPassword : undefined;
|
|
1819
|
+
if (uiPassword) {
|
|
1820
|
+
env.OPENCHAMBER_UI_PASSWORD = uiPassword;
|
|
1821
|
+
}
|
|
1822
|
+
if (typeof process.env.OPENCHAMBER_DATA_DIR === 'string' && process.env.OPENCHAMBER_DATA_DIR.trim().length > 0) {
|
|
1823
|
+
env.OPENCHAMBER_DATA_DIR = path.resolve(process.env.OPENCHAMBER_DATA_DIR.trim());
|
|
1824
|
+
}
|
|
1825
|
+
return env;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function shouldPersistStartupEnv(key, value) {
|
|
1829
|
+
if (typeof key !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return false;
|
|
1830
|
+
if (typeof value !== 'string') return false;
|
|
1831
|
+
if (/[\r\n]/.test(value)) return false;
|
|
1832
|
+
|
|
1833
|
+
// These are shell/session implementation details, not app configuration.
|
|
1834
|
+
const volatileKeys = new Set([
|
|
1835
|
+
'_',
|
|
1836
|
+
'BASH_ENV',
|
|
1837
|
+
'COLUMNS',
|
|
1838
|
+
'CONDA_DEFAULT_ENV',
|
|
1839
|
+
'CONDA_PREFIX',
|
|
1840
|
+
'CONDA_PROMPT_MODIFIER',
|
|
1841
|
+
'CONDA_SHLVL',
|
|
1842
|
+
'ENV',
|
|
1843
|
+
'HISTFILE',
|
|
1844
|
+
'HISTFILESIZE',
|
|
1845
|
+
'HISTSIZE',
|
|
1846
|
+
'LINES',
|
|
1847
|
+
'OLDPWD',
|
|
1848
|
+
'PROMPT',
|
|
1849
|
+
'PROMPT_COMMAND',
|
|
1850
|
+
'PS1',
|
|
1851
|
+
'PS2',
|
|
1852
|
+
'PS3',
|
|
1853
|
+
'PS4',
|
|
1854
|
+
'PWD',
|
|
1855
|
+
'PYENV_VERSION',
|
|
1856
|
+
'SHLVL',
|
|
1857
|
+
'TERM',
|
|
1858
|
+
'TERM_PROGRAM',
|
|
1859
|
+
'TERM_PROGRAM_VERSION',
|
|
1860
|
+
'TTY',
|
|
1861
|
+
'VIRTUAL_ENV',
|
|
1862
|
+
'VIRTUAL_ENV_PROMPT',
|
|
1863
|
+
]);
|
|
1864
|
+
return !volatileKeys.has(key);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
function writeStartupEnvFile(options = {}, fileOptions = {}) {
|
|
1868
|
+
const envFilePath = getStartupEnvFilePath();
|
|
1869
|
+
const lines = [];
|
|
1870
|
+
const env = collectStartupEnv(options);
|
|
1871
|
+
const quoteValue = typeof fileOptions.quoteValue === 'function' ? fileOptions.quoteValue : startupEnvFileQuote;
|
|
1872
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1873
|
+
lines.push(`${key}=${quoteValue(value)}`);
|
|
1874
|
+
}
|
|
1875
|
+
fs.mkdirSync(path.dirname(envFilePath), { recursive: true, mode: 0o700 });
|
|
1876
|
+
fs.writeFileSync(envFilePath, lines.length > 0 ? `${lines.join('\n')}\n` : '', { mode: 0o600 });
|
|
1877
|
+
return envFilePath;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
function removeStartupEnvFile() {
|
|
1881
|
+
try { fs.unlinkSync(getStartupEnvFilePath()); } catch {}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function resolveCliEntrypoint() {
|
|
1885
|
+
const entry = typeof process.argv[1] === 'string' && process.argv[1].trim().length > 0
|
|
1886
|
+
? process.argv[1]
|
|
1887
|
+
: path.join(__dirname, 'cli.js');
|
|
1888
|
+
try {
|
|
1889
|
+
return fs.realpathSync(entry);
|
|
1890
|
+
} catch {
|
|
1891
|
+
return path.resolve(entry);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
function buildStartupArgs(options = {}) {
|
|
1896
|
+
const args = [resolveCliEntrypoint(), 'serve', '--foreground', '--port', String(options.port || DEFAULT_PORT)];
|
|
1897
|
+
if (typeof options.host === 'string' && options.host.length > 0) {
|
|
1898
|
+
args.push('--host', options.host);
|
|
1899
|
+
}
|
|
1900
|
+
return args;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function writeMacosStartupWrapper(options = {}) {
|
|
1904
|
+
const wrapperPath = getMacosStartupWrapperPath();
|
|
1905
|
+
const args = buildStartupArgs(options).map(startupShellQuote).join(' ');
|
|
1906
|
+
const content = `#!/bin/sh
|
|
1907
|
+
exec ${startupShellQuote(process.execPath)} ${args}
|
|
1908
|
+
`;
|
|
1909
|
+
fs.mkdirSync(path.dirname(wrapperPath), { recursive: true, mode: 0o700 });
|
|
1910
|
+
fs.writeFileSync(wrapperPath, content, { mode: 0o700 });
|
|
1911
|
+
return wrapperPath;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function buildMacosLaunchAgent(options = {}) {
|
|
1915
|
+
const wrapperPath = writeMacosStartupWrapper(options);
|
|
1916
|
+
const args = [wrapperPath];
|
|
1917
|
+
const env = collectStartupEnv(options);
|
|
1918
|
+
const logDir = path.join(os.homedir(), 'Library', 'Logs', 'OpenChamber');
|
|
1919
|
+
const argXml = args.map((arg) => ` <string>${escapeXml(arg)}</string>`).join('\n');
|
|
1920
|
+
const envXml = Object.entries(env).length > 0
|
|
1921
|
+
? ` <key>EnvironmentVariables</key>\n <dict>\n${Object.entries(env).map(([key, value]) => ` <key>${escapeXml(key)}</key>\n <string>${escapeXml(value)}</string>`).join('\n')}\n </dict>\n`
|
|
1922
|
+
: '';
|
|
1923
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1924
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1925
|
+
<plist version="1.0">
|
|
1926
|
+
<dict>
|
|
1927
|
+
<key>Label</key>
|
|
1928
|
+
<string>${STARTUP_SERVICE_ID}</string>
|
|
1929
|
+
<key>ProgramArguments</key>
|
|
1930
|
+
<array>
|
|
1931
|
+
${argXml}
|
|
1932
|
+
</array>
|
|
1933
|
+
${envXml} <key>ProcessType</key>
|
|
1934
|
+
<string>Background</string>
|
|
1935
|
+
<key>RunAtLoad</key>
|
|
1936
|
+
<true/>
|
|
1937
|
+
<key>KeepAlive</key>
|
|
1938
|
+
<true/>
|
|
1939
|
+
<key>WorkingDirectory</key>
|
|
1940
|
+
<string>${escapeXml(os.homedir())}</string>
|
|
1941
|
+
<key>StandardOutPath</key>
|
|
1942
|
+
<string>${escapeXml(path.join(logDir, 'startup.log'))}</string>
|
|
1943
|
+
<key>StandardErrorPath</key>
|
|
1944
|
+
<string>${escapeXml(path.join(logDir, 'startup.err.log'))}</string>
|
|
1945
|
+
</dict>
|
|
1946
|
+
</plist>
|
|
1947
|
+
`;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function buildSystemdUserService(options = {}) {
|
|
1951
|
+
const args = buildStartupArgs(options).map((arg) => `"${systemdEscapeArg(arg)}"`).join(' ');
|
|
1952
|
+
const envFilePath = getStartupEnvFilePath();
|
|
1953
|
+
return `[Unit]
|
|
1954
|
+
Description=OpenChamber web server
|
|
1955
|
+
After=network-online.target
|
|
1956
|
+
|
|
1957
|
+
[Service]
|
|
1958
|
+
Type=simple
|
|
1959
|
+
EnvironmentFile=-${systemdEscapeArg(envFilePath)}
|
|
1960
|
+
ExecStart="${systemdEscapeArg(process.execPath)}" ${args}
|
|
1961
|
+
WorkingDirectory=${systemdUnitPath(os.homedir())}
|
|
1962
|
+
Restart=always
|
|
1963
|
+
RestartSec=5
|
|
1964
|
+
|
|
1965
|
+
[Install]
|
|
1966
|
+
WantedBy=default.target
|
|
1967
|
+
`;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function runStartupCommand(command, args, options = {}) {
|
|
1971
|
+
const result = spawnSync(command, args, {
|
|
1972
|
+
encoding: 'utf8',
|
|
1973
|
+
stdio: options.stdio || 'pipe',
|
|
1974
|
+
windowsHide: true,
|
|
1975
|
+
});
|
|
1976
|
+
if (result.error) {
|
|
1977
|
+
throw result.error;
|
|
1978
|
+
}
|
|
1979
|
+
if (result.status !== 0 && options.allowFailure !== true) {
|
|
1980
|
+
const detail = (result.stderr || result.stdout || '').trim();
|
|
1981
|
+
throw new Error(`${command} ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`);
|
|
1982
|
+
}
|
|
1983
|
+
return result;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
function getStartupStatus() {
|
|
1987
|
+
const paths = getStartupServicePaths();
|
|
1988
|
+
if (!paths.servicePath) {
|
|
1989
|
+
return { supported: false, platform: paths.platform, enabled: false, servicePath: null };
|
|
1990
|
+
}
|
|
1991
|
+
if (paths.platform === 'windows') {
|
|
1992
|
+
const result = runStartupCommand('schtasks.exe', ['/Query', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
|
|
1993
|
+
return { supported: true, platform: paths.platform, enabled: result.status === 0, active: null, servicePath: paths.servicePath };
|
|
1994
|
+
}
|
|
1995
|
+
if (paths.platform === 'linux') {
|
|
1996
|
+
const enabledResult = runStartupCommand('systemctl', ['--user', 'is-enabled', 'openchamber.service'], { allowFailure: true });
|
|
1997
|
+
const activeResult = runStartupCommand('systemctl', ['--user', 'is-active', 'openchamber.service'], { allowFailure: true });
|
|
1998
|
+
const activeState = (activeResult.stdout || '').trim() || 'inactive';
|
|
1999
|
+
return {
|
|
2000
|
+
supported: true,
|
|
2001
|
+
platform: paths.platform,
|
|
2002
|
+
enabled: enabledResult.status === 0 || fs.existsSync(paths.servicePath),
|
|
2003
|
+
active: activeState === 'active',
|
|
2004
|
+
activeState,
|
|
2005
|
+
servicePath: paths.servicePath,
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
return {
|
|
2009
|
+
supported: true,
|
|
2010
|
+
platform: paths.platform,
|
|
2011
|
+
enabled: fs.existsSync(paths.servicePath),
|
|
2012
|
+
active: null,
|
|
2013
|
+
servicePath: paths.servicePath,
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
function enableStartupService(options = {}) {
|
|
2018
|
+
const paths = getStartupServicePaths();
|
|
2019
|
+
if (!paths.servicePath) {
|
|
2020
|
+
throw new TunnelCliError(`Startup integration is not supported on ${paths.platform}.`, EXIT_CODE.USAGE_ERROR);
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
if (paths.platform === 'macos') {
|
|
2024
|
+
removeStartupEnvFile();
|
|
2025
|
+
fs.mkdirSync(path.dirname(paths.servicePath), { recursive: true, mode: 0o700 });
|
|
2026
|
+
fs.mkdirSync(path.join(os.homedir(), 'Library', 'Logs', 'OpenChamber'), { recursive: true, mode: 0o700 });
|
|
2027
|
+
fs.writeFileSync(paths.servicePath, buildMacosLaunchAgent(options), { mode: 0o600 });
|
|
2028
|
+
runStartupCommand('/bin/launchctl', ['bootout', `gui/${process.getuid()}`, paths.servicePath], { allowFailure: true });
|
|
2029
|
+
runStartupCommand('/bin/launchctl', ['bootstrap', `gui/${process.getuid()}`, paths.servicePath]);
|
|
2030
|
+
runStartupCommand('/bin/launchctl', ['kickstart', '-k', `gui/${process.getuid()}/${STARTUP_SERVICE_ID}`], { allowFailure: true });
|
|
2031
|
+
return getStartupStatus();
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
if (paths.platform === 'linux') {
|
|
2035
|
+
writeStartupEnvFile(options, { quoteValue: systemdEnvFileQuote });
|
|
2036
|
+
fs.mkdirSync(path.dirname(paths.servicePath), { recursive: true, mode: 0o700 });
|
|
2037
|
+
fs.writeFileSync(paths.servicePath, buildSystemdUserService(options), { mode: 0o600 });
|
|
2038
|
+
runStartupCommand('systemctl', ['--user', 'daemon-reload']);
|
|
2039
|
+
runStartupCommand('systemctl', ['--user', 'enable', '--now', 'openchamber.service']);
|
|
2040
|
+
return getStartupStatus();
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
const envFilePath = writeStartupEnvFile(options);
|
|
2044
|
+
const startupArgs = buildStartupArgs(options).map(powershellQuote).join(', ');
|
|
2045
|
+
const powerShellCommand = [
|
|
2046
|
+
`$envFile=${powershellQuote(envFilePath)}`,
|
|
2047
|
+
`if (Test-Path $envFile) { Get-Content $envFile | ForEach-Object { if ($_ -match '^([^=]+)=(.*)$') { $v=$matches[2]; if ($v.StartsWith("'") -and $v.EndsWith("'")) { $v=$v.Substring(1,$v.Length-2).Replace("'\\''","'") }; [Environment]::SetEnvironmentVariable($matches[1], $v, 'Process') } } }`,
|
|
2048
|
+
`& ${powershellQuote(process.execPath)} ${startupArgs}`,
|
|
2049
|
+
].join('; ');
|
|
2050
|
+
const taskArgs = `powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "${powerShellCommand.replace(/"/g, '\\"')}"`;
|
|
2051
|
+
runStartupCommand('schtasks.exe', [
|
|
2052
|
+
'/Create',
|
|
2053
|
+
'/TN', STARTUP_SERVICE_ID,
|
|
2054
|
+
'/SC', 'ONLOGON',
|
|
2055
|
+
'/RL', 'LIMITED',
|
|
2056
|
+
'/F',
|
|
2057
|
+
'/TR', taskArgs,
|
|
2058
|
+
]);
|
|
2059
|
+
runStartupCommand('schtasks.exe', ['/Run', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
|
|
2060
|
+
return getStartupStatus();
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function disableStartupService() {
|
|
2064
|
+
const paths = getStartupServicePaths();
|
|
2065
|
+
if (!paths.servicePath) {
|
|
2066
|
+
throw new TunnelCliError(`Startup integration is not supported on ${paths.platform}.`, EXIT_CODE.USAGE_ERROR);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
if (paths.platform === 'macos') {
|
|
2070
|
+
runStartupCommand('/bin/launchctl', ['bootout', `gui/${process.getuid()}`, paths.servicePath], { allowFailure: true });
|
|
2071
|
+
try { fs.unlinkSync(paths.servicePath); } catch {}
|
|
2072
|
+
return getStartupStatus();
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
if (paths.platform === 'linux') {
|
|
2076
|
+
runStartupCommand('systemctl', ['--user', 'disable', '--now', 'openchamber.service'], { allowFailure: true });
|
|
2077
|
+
try { fs.unlinkSync(paths.servicePath); } catch {}
|
|
2078
|
+
runStartupCommand('systemctl', ['--user', 'daemon-reload'], { allowFailure: true });
|
|
2079
|
+
return getStartupStatus();
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
runStartupCommand('schtasks.exe', ['/End', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
|
|
2083
|
+
runStartupCommand('schtasks.exe', ['/Delete', '/TN', STARTUP_SERVICE_ID, '/F'], { allowFailure: true });
|
|
2084
|
+
return getStartupStatus();
|
|
2085
|
+
}
|
|
2086
|
+
|
|
1705
2087
|
async function getPidFilePath(port) {
|
|
1706
2088
|
return path.join(getRunDir(), `openchamber-${port}.pid`);
|
|
1707
2089
|
}
|
|
@@ -4719,6 +5101,58 @@ const commands = {
|
|
|
4719
5101
|
});
|
|
4720
5102
|
},
|
|
4721
5103
|
|
|
5104
|
+
async startup(options, action = 'status') {
|
|
5105
|
+
const normalized = typeof action === 'string' ? action.trim().toLowerCase() : 'status';
|
|
5106
|
+
if (!['status', 'enable', 'disable'].includes(normalized)) {
|
|
5107
|
+
throw new TunnelCliError(
|
|
5108
|
+
`Unknown startup subcommand '${action}'. Use 'openchamber startup --help'.`,
|
|
5109
|
+
EXIT_CODE.USAGE_ERROR
|
|
5110
|
+
);
|
|
5111
|
+
}
|
|
5112
|
+
|
|
5113
|
+
let status;
|
|
5114
|
+
if (normalized === 'enable') {
|
|
5115
|
+
status = enableStartupService(options);
|
|
5116
|
+
} else if (normalized === 'disable') {
|
|
5117
|
+
status = disableStartupService();
|
|
5118
|
+
} else {
|
|
5119
|
+
status = getStartupStatus();
|
|
5120
|
+
}
|
|
5121
|
+
|
|
5122
|
+
const result = { action: normalized, ...status };
|
|
5123
|
+
if (!result.supported) {
|
|
5124
|
+
throw new TunnelCliError(
|
|
5125
|
+
`Startup integration is not supported on ${result.platform}.`,
|
|
5126
|
+
EXIT_CODE.USAGE_ERROR
|
|
5127
|
+
);
|
|
5128
|
+
}
|
|
5129
|
+
if (normalized === 'enable' && result.activeState === 'failed') {
|
|
5130
|
+
throw new TunnelCliError(
|
|
5131
|
+
'Startup service was installed but failed to start. Run `journalctl --user -u openchamber.service -n 80 --no-pager` for details.',
|
|
5132
|
+
EXIT_CODE.GENERAL_ERROR
|
|
5133
|
+
);
|
|
5134
|
+
}
|
|
5135
|
+
if (isJsonMode(options)) {
|
|
5136
|
+
printJson(result);
|
|
5137
|
+
return;
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
if (isQuietMode(options)) {
|
|
5141
|
+
process.stdout.write(`startup ${result.enabled ? 'enabled' : 'disabled'} platform:${result.platform} supported:${result.supported ? 'yes' : 'no'}${result.servicePath ? ` path:${result.servicePath}` : ''}\n`);
|
|
5142
|
+
return;
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
clackIntro('OpenChamber Startup');
|
|
5146
|
+
logStatus(result.enabled ? 'success' : 'info', `startup ${result.enabled ? 'enabled' : 'disabled'}`, result.servicePath || undefined);
|
|
5147
|
+
if (typeof result.activeState === 'string') {
|
|
5148
|
+
logStatus(result.active ? 'success' : result.activeState === 'failed' ? 'error' : 'warning', `service ${result.activeState}`);
|
|
5149
|
+
}
|
|
5150
|
+
if (normalized === 'enable') {
|
|
5151
|
+
logStatus('info', 'service command', 'openchamber serve --foreground');
|
|
5152
|
+
}
|
|
5153
|
+
clackOutro(normalized === 'status' ? 'status complete' : `${normalized} complete`);
|
|
5154
|
+
},
|
|
5155
|
+
|
|
4722
5156
|
async update(options = {}) {
|
|
4723
5157
|
const showOutput = shouldRenderHumanOutput(options);
|
|
4724
5158
|
const updateSpin = createSpinner(options);
|
|
@@ -4843,7 +5277,7 @@ const commands = {
|
|
|
4843
5277
|
|
|
4844
5278
|
async function main() {
|
|
4845
5279
|
const parsed = parseArgs();
|
|
4846
|
-
const { command, subcommand, tunnelAction, options, removedFlagErrors, helpRequested, versionRequested } = parsed;
|
|
5280
|
+
const { command, subcommand, tunnelAction, startupAction, options, removedFlagErrors, helpRequested, versionRequested } = parsed;
|
|
4847
5281
|
activeCommandOptions = options;
|
|
4848
5282
|
|
|
4849
5283
|
if (versionRequested) {
|
|
@@ -4875,6 +5309,8 @@ async function main() {
|
|
|
4875
5309
|
if (helpRequested) {
|
|
4876
5310
|
if (command === 'tunnel') {
|
|
4877
5311
|
showTunnelHelp();
|
|
5312
|
+
} else if (command === 'startup') {
|
|
5313
|
+
showStartupHelp();
|
|
4878
5314
|
} else {
|
|
4879
5315
|
showHelp();
|
|
4880
5316
|
}
|
|
@@ -4886,8 +5322,13 @@ async function main() {
|
|
|
4886
5322
|
return;
|
|
4887
5323
|
}
|
|
4888
5324
|
|
|
5325
|
+
if (command === 'startup') {
|
|
5326
|
+
await commands.startup(options, startupAction);
|
|
5327
|
+
return;
|
|
5328
|
+
}
|
|
5329
|
+
|
|
4889
5330
|
if (!commands[command]) {
|
|
4890
|
-
const knownCommands = ['serve', 'stop', 'restart', 'status', 'tunnel', 'logs', 'update'];
|
|
5331
|
+
const knownCommands = ['serve', 'stop', 'restart', 'status', 'tunnel', 'startup', 'logs', 'update'];
|
|
4891
5332
|
const suggestion = findClosestMatch(command, knownCommands);
|
|
4892
5333
|
const hint = suggestion ? ` Did you mean '${suggestion}'?` : '';
|
|
4893
5334
|
if (isJsonMode(options)) {
|