feat(web): add Cloudflare Quick Tunnel support for remote access (#85)
* feat(web): add Cloudflare Quick Tunnel support for remote access Implement --try-cf-tunnel flag that creates a temporary public URL routing to the locally running server via cloudflared. Features: - Auto-detect and spawn cloudflared subprocess - Extract and display *.trycloudflare.com URL - Proper cleanup on server shutdown - Dynamic port handling (tunnel routes to actual listening port) - Isolated HOME dir to avoid config conflicts - Installation help and limitations warning * docs: add Cloudflare tunnel feature to README - Document --try-cf-tunnel flag in CLI examples - Add Cloudflare Quick Tunnel to Web/PWA features * docs: add cloudflared as prerequisite for --try-cf-tunnel * feat(web): auto-generate password for Cloudflare tunnel When using --try-cf-tunnel without --ui-password, automatically generate a secure random password to protect the publicly exposed service. Features: - 16-character random password (easy to read, no ambiguous characters) - Password is displayed after tunnel is established - Warning to save the password (not shown again) - Works in both foreground and daemon modes - Uses same password for subsequent restarts (stored in instance file) * fix(web): improve Cloudflare tunnel UX and fix spawn ENAMETOOLONG - Generate and display password before tunnel starts (green colored) - Display tunnel URL in cyan via onTunnelReady callback - Fix ENAMETOOLONG by using env:undefined instead of copying process.env - Pass tryCfTunnel flag from CLI to main function - Remove duplicate tunnel URL output - Update warning message to reflect password protection - Change start script to use CLI wrapper for proper initialization --------- Co-authored-by: aptdnfapt <197602950+aptdnfapt@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
aptdnfapt
parent
fa2223a0e7
commit
6c30245134
+56
-15
@@ -11,10 +11,20 @@ const __dirname = path.dirname(__filename);
|
||||
const DEFAULT_PORT = 3000;
|
||||
const PACKAGE_JSON = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
||||
|
||||
function generateRandomPassword(length = 16) {
|
||||
const charset = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
|
||||
let password = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * charset.length);
|
||||
password += charset[randomIndex];
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const envPassword = process.env.OPENCHAMBER_UI_PASSWORD || undefined;
|
||||
const options = { port: DEFAULT_PORT, daemon: false, uiPassword: envPassword };
|
||||
const options = { port: DEFAULT_PORT, daemon: false, uiPassword: envPassword, tryCfTunnel: false };
|
||||
let command = 'serve';
|
||||
|
||||
const consumeValue = (currentIndex, inlineValue) => {
|
||||
@@ -57,6 +67,9 @@ function parseArgs() {
|
||||
case 'd':
|
||||
options.daemon = true;
|
||||
break;
|
||||
case 'try-cf-tunnel':
|
||||
options.tryCfTunnel = true;
|
||||
break;
|
||||
case 'ui-password': {
|
||||
const { value, nextIndex } = consumeValue(i, inlineValue);
|
||||
i = nextIndex;
|
||||
@@ -97,11 +110,12 @@ COMMANDS:
|
||||
update Check for and install updates
|
||||
|
||||
OPTIONS:
|
||||
-p, --port Web server port (default: ${DEFAULT_PORT})
|
||||
--ui-password Protect browser UI with single password
|
||||
-d, --daemon Run in background (serve command)
|
||||
-h, --help Show help
|
||||
-v, --version Show version
|
||||
-p, --port Web server port (default: ${DEFAULT_PORT})
|
||||
--ui-password Protect browser UI with single password
|
||||
--try-cf-tunnel Create a Cloudflare Quick Tunnel for remote access
|
||||
-d, --daemon Run in background (serve command)
|
||||
-h, --help Show help
|
||||
-v, --version Show version
|
||||
|
||||
ENVIRONMENT:
|
||||
OPENCHAMBER_UI_PASSWORD Alternative to --ui-password flag
|
||||
@@ -110,6 +124,7 @@ EXAMPLES:
|
||||
openchamber # Start on default port 3000
|
||||
openchamber --port 8080 # Start on port 8080
|
||||
openchamber serve --daemon # Start in background
|
||||
openchamber --try-cf-tunnel # Start with Cloudflare Quick Tunnel
|
||||
openchamber stop # Stop all running instances
|
||||
openchamber stop --port 3000 # Stop specific instance
|
||||
openchamber status # Check status
|
||||
@@ -353,9 +368,20 @@ const commands = {
|
||||
|
||||
const serverPath = path.join(__dirname, '..', 'server', 'index.js');
|
||||
|
||||
let effectiveUiPassword = options.uiPassword;
|
||||
let showAutoGeneratedPassword = false;
|
||||
|
||||
if (options.tryCfTunnel && typeof effectiveUiPassword !== 'string') {
|
||||
effectiveUiPassword = generateRandomPassword(16);
|
||||
showAutoGeneratedPassword = true;
|
||||
}
|
||||
|
||||
const serverArgs = [serverPath, '--port', options.port.toString()];
|
||||
if (typeof options.uiPassword === 'string') {
|
||||
serverArgs.push('--ui-password', options.uiPassword);
|
||||
if (typeof effectiveUiPassword === 'string') {
|
||||
serverArgs.push('--ui-password', effectiveUiPassword);
|
||||
}
|
||||
if (options.tryCfTunnel) {
|
||||
serverArgs.push('--try-cf-tunnel');
|
||||
}
|
||||
|
||||
if (options.daemon) {
|
||||
@@ -367,7 +393,8 @@ const commands = {
|
||||
...process.env,
|
||||
OPENCHAMBER_PORT: options.port.toString(),
|
||||
OPENCODE_BINARY: opencodeBinary,
|
||||
...(typeof options.uiPassword === 'string' ? { OPENCHAMBER_UI_PASSWORD: options.uiPassword } : {})
|
||||
...(typeof effectiveUiPassword === 'string' ? { OPENCHAMBER_UI_PASSWORD: effectiveUiPassword } : {}),
|
||||
OPENCHAMBER_TRY_CF_TUNNEL: options.tryCfTunnel ? 'true' : 'false',
|
||||
}
|
||||
});
|
||||
|
||||
@@ -376,10 +403,14 @@ const commands = {
|
||||
setTimeout(() => {
|
||||
if (isProcessRunning(child.pid)) {
|
||||
writePidFile(pidFilePath, child.pid);
|
||||
writeInstanceOptions(instanceFilePath, options);
|
||||
writeInstanceOptions(instanceFilePath, { ...options, uiPassword: effectiveUiPassword });
|
||||
console.log(`OpenChamber started in daemon mode on port ${options.port}`);
|
||||
console.log(`PID: ${child.pid}`);
|
||||
console.log(`Visit: http://localhost:${options.port}`);
|
||||
if (showAutoGeneratedPassword) {
|
||||
console.log(`\n🔐 Auto-generated password: \x1b[92m${effectiveUiPassword}\x1b[0m`);
|
||||
console.log('⚠️ Save this password - it won\'t be shown again!\n');
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to start server in daemon mode');
|
||||
process.exit(1);
|
||||
@@ -389,16 +420,26 @@ const commands = {
|
||||
} else {
|
||||
|
||||
process.env.OPENCODE_BINARY = opencodeBinary;
|
||||
if (typeof options.uiPassword === 'string') {
|
||||
process.env.OPENCHAMBER_UI_PASSWORD = options.uiPassword;
|
||||
if (typeof effectiveUiPassword === 'string') {
|
||||
process.env.OPENCHAMBER_UI_PASSWORD = effectiveUiPassword;
|
||||
}
|
||||
writeInstanceOptions(instanceFilePath, options);
|
||||
if (showAutoGeneratedPassword) {
|
||||
console.log(`\n🔐 Auto-generated password: \x1b[92m${effectiveUiPassword}\x1b[0m`);
|
||||
console.log('⚠️ Save this password - it won\'t be shown again!\n');
|
||||
}
|
||||
|
||||
writeInstanceOptions(instanceFilePath, { ...options, uiPassword: effectiveUiPassword });
|
||||
|
||||
const { startWebUiServer } = await import(serverPath);
|
||||
await startWebUiServer({
|
||||
const server = await startWebUiServer({
|
||||
port: options.port,
|
||||
attachSignals: true,
|
||||
exitOnShutdown: true,
|
||||
uiPassword: typeof options.uiPassword === 'string' ? options.uiPassword : null
|
||||
uiPassword: typeof effectiveUiPassword === 'string' ? effectiveUiPassword : null,
|
||||
tryCfTunnel: options.tryCfTunnel,
|
||||
onTunnelReady: (url) => {
|
||||
console.log(`\n🌐 Tunnel URL: \x1b[36m${url}\x1b[0m\n`);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user