Wire CLI flags through parseCliArgs → runWebCliBranch → launchWebMode so users can bind to a custom host/port and whitelist CORS origins for LAN/Tailscale access. - Add webHost, webPort, webAllowedOrigins to CliFlags - Parse --host, --port (validated 1-65535), --allowed-origins (csv) - Forward into launchWebMode options - Set GSD_WEB_ALLOWED_ORIGINS in subprocess env when provided - Add allowedOrigins to WebModeLaunchOptions Usage: gsd --web --host 0.0.0.0 --port 8080 --allowed-origins http://192.168.1.10:8080 Closes #1847
This commit is contained in:
parent
8bed02c077
commit
3f8d7921ca
4 changed files with 227 additions and 0 deletions
|
|
@ -18,6 +18,12 @@ export interface CliFlags {
|
|||
web?: boolean
|
||||
/** Optional project path for web mode: `gsd --web <path>` or `gsd web start <path>` */
|
||||
webPath?: string
|
||||
/** Custom host to bind web server to: `--host 0.0.0.0` */
|
||||
webHost?: string
|
||||
/** Custom port for web server: `--port 8080` */
|
||||
webPort?: number
|
||||
/** Additional allowed origins for CORS: `--allowed-origins http://192.168.1.10:8080` */
|
||||
webAllowedOrigins?: string[]
|
||||
help?: boolean
|
||||
version?: boolean
|
||||
}
|
||||
|
|
@ -54,6 +60,17 @@ export function parseCliArgs(argv: string[]): CliFlags {
|
|||
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
||||
flags.webPath = args[++i]
|
||||
}
|
||||
} else if (arg === '--host' && i + 1 < args.length) {
|
||||
flags.webHost = args[++i]
|
||||
} else if (arg === '--port' && i + 1 < args.length) {
|
||||
const portStr = args[++i]
|
||||
const port = parseInt(portStr, 10)
|
||||
if (Number.isFinite(port) && port > 0 && port < 65536) {
|
||||
flags.webPort = port
|
||||
}
|
||||
} else if (arg === '--allowed-origins' && i + 1 < args.length) {
|
||||
const origins = args[++i].split(',').map(o => o.trim()).filter(Boolean)
|
||||
flags.webAllowedOrigins = (flags.webAllowedOrigins ?? []).concat(origins)
|
||||
} else if (arg === '--model' && i + 1 < args.length) {
|
||||
flags.model = args[++i]
|
||||
} else if (arg === '--extension' && i + 1 < args.length) {
|
||||
|
|
@ -266,6 +283,9 @@ export async function runWebCliBranch(
|
|||
cwd: currentCwd,
|
||||
projectSessionsDir,
|
||||
agentDir,
|
||||
host: flags.webHost,
|
||||
port: flags.webPort,
|
||||
allowedOrigins: flags.webAllowedOrigins,
|
||||
})
|
||||
|
||||
if (!status.ok) {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,9 @@ test('cli.ts branches to web mode before interactive startup and preserves cwd-s
|
|||
cwd,
|
||||
projectSessionsDir: cliWeb.getProjectSessionsDir(cwd),
|
||||
agentDir: join(process.env.HOME || '', '.gsd', 'agent'),
|
||||
host: undefined,
|
||||
port: undefined,
|
||||
allowedOrigins: undefined,
|
||||
})
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true })
|
||||
|
|
|
|||
201
src/tests/web-mode-network-flags.test.ts
Normal file
201
src/tests/web-mode-network-flags.test.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
|
||||
const cliWeb = await import('../cli-web-branch.ts')
|
||||
const webMode = await import('../web-mode.ts')
|
||||
|
||||
// ─── CLI flag parsing ────────────────────────────────────────────────
|
||||
|
||||
test('parseCliArgs captures --host flag', () => {
|
||||
const flags = cliWeb.parseCliArgs(['node', 'dist/loader.js', '--web', '--host', '0.0.0.0'])
|
||||
assert.equal(flags.web, true)
|
||||
assert.equal(flags.webHost, '0.0.0.0')
|
||||
})
|
||||
|
||||
test('parseCliArgs captures --port flag', () => {
|
||||
const flags = cliWeb.parseCliArgs(['node', 'dist/loader.js', '--web', '--port', '8080'])
|
||||
assert.equal(flags.web, true)
|
||||
assert.equal(flags.webPort, 8080)
|
||||
})
|
||||
|
||||
test('parseCliArgs ignores invalid port values', () => {
|
||||
const flags = cliWeb.parseCliArgs(['node', 'dist/loader.js', '--web', '--port', 'abc'])
|
||||
assert.equal(flags.webPort, undefined)
|
||||
})
|
||||
|
||||
test('parseCliArgs ignores out-of-range port', () => {
|
||||
const flags = cliWeb.parseCliArgs(['node', 'dist/loader.js', '--web', '--port', '99999'])
|
||||
assert.equal(flags.webPort, undefined)
|
||||
})
|
||||
|
||||
test('parseCliArgs captures --allowed-origins flag', () => {
|
||||
const flags = cliWeb.parseCliArgs(['node', 'dist/loader.js', '--web', '--allowed-origins', 'http://192.168.1.10:3000'])
|
||||
assert.deepEqual(flags.webAllowedOrigins, ['http://192.168.1.10:3000'])
|
||||
})
|
||||
|
||||
test('parseCliArgs splits comma-separated allowed origins', () => {
|
||||
const flags = cliWeb.parseCliArgs([
|
||||
'node', 'dist/loader.js', '--web',
|
||||
'--allowed-origins', 'http://192.168.1.10:3000,http://tailscale-host:3000',
|
||||
])
|
||||
assert.deepEqual(flags.webAllowedOrigins, ['http://192.168.1.10:3000', 'http://tailscale-host:3000'])
|
||||
})
|
||||
|
||||
test('parseCliArgs captures all web network flags together', () => {
|
||||
const flags = cliWeb.parseCliArgs([
|
||||
'node', 'dist/loader.js', '--web',
|
||||
'--host', '0.0.0.0',
|
||||
'--port', '4000',
|
||||
'--allowed-origins', 'http://my-tailscale:4000',
|
||||
])
|
||||
assert.equal(flags.webHost, '0.0.0.0')
|
||||
assert.equal(flags.webPort, 4000)
|
||||
assert.deepEqual(flags.webAllowedOrigins, ['http://my-tailscale:4000'])
|
||||
})
|
||||
|
||||
test('parseCliArgs does not set network flags when not provided', () => {
|
||||
const flags = cliWeb.parseCliArgs(['node', 'dist/loader.js', '--web'])
|
||||
assert.equal(flags.webHost, undefined)
|
||||
assert.equal(flags.webPort, undefined)
|
||||
assert.equal(flags.webAllowedOrigins, undefined)
|
||||
})
|
||||
|
||||
// ─── launchWebMode env forwarding ────────────────────────────────────
|
||||
|
||||
test('launchWebMode forwards custom host, port, and allowed origins to subprocess env', async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'gsd-web-net-'))
|
||||
const standaloneRoot = join(tmp, 'dist', 'web', 'standalone')
|
||||
const serverPath = join(standaloneRoot, 'server.js')
|
||||
mkdirSync(standaloneRoot, { recursive: true })
|
||||
writeFileSync(serverPath, 'console.log("stub")\n')
|
||||
|
||||
let spawnEnv: Record<string, string> | undefined
|
||||
|
||||
try {
|
||||
const status = await webMode.launchWebMode(
|
||||
{
|
||||
cwd: '/tmp/project',
|
||||
projectSessionsDir: '/tmp/.gsd/sessions',
|
||||
agentDir: '/tmp/.gsd/agent',
|
||||
packageRoot: tmp,
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
allowedOrigins: ['http://192.168.1.10:8080', 'http://tailscale-host:8080'],
|
||||
},
|
||||
{
|
||||
initResources: () => {},
|
||||
spawn: (_command, _args, options) => {
|
||||
spawnEnv = (options as { env: Record<string, string> }).env
|
||||
return { pid: 99999, once: () => undefined, unref: () => {} } as any
|
||||
},
|
||||
waitForBootReady: async () => undefined,
|
||||
openBrowser: () => {},
|
||||
stderr: { write: () => true },
|
||||
},
|
||||
)
|
||||
|
||||
assert.equal(status.ok, true)
|
||||
if (!status.ok) throw new Error('expected success')
|
||||
assert.equal(status.host, '0.0.0.0')
|
||||
assert.equal(status.port, 8080)
|
||||
assert.equal(status.url, 'http://0.0.0.0:8080')
|
||||
|
||||
assert.ok(spawnEnv)
|
||||
assert.equal(spawnEnv!.HOSTNAME, '0.0.0.0')
|
||||
assert.equal(spawnEnv!.PORT, '8080')
|
||||
assert.equal(spawnEnv!.GSD_WEB_HOST, '0.0.0.0')
|
||||
assert.equal(spawnEnv!.GSD_WEB_PORT, '8080')
|
||||
assert.equal(spawnEnv!.GSD_WEB_ALLOWED_ORIGINS, 'http://192.168.1.10:8080,http://tailscale-host:8080')
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('launchWebMode omits GSD_WEB_ALLOWED_ORIGINS when none provided', async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'gsd-web-no-origins-'))
|
||||
const standaloneRoot = join(tmp, 'dist', 'web', 'standalone')
|
||||
const serverPath = join(standaloneRoot, 'server.js')
|
||||
mkdirSync(standaloneRoot, { recursive: true })
|
||||
writeFileSync(serverPath, 'console.log("stub")\n')
|
||||
|
||||
let spawnEnv: Record<string, string> | undefined
|
||||
|
||||
try {
|
||||
await webMode.launchWebMode(
|
||||
{
|
||||
cwd: '/tmp/project',
|
||||
projectSessionsDir: '/tmp/.gsd/sessions',
|
||||
agentDir: '/tmp/.gsd/agent',
|
||||
packageRoot: tmp,
|
||||
},
|
||||
{
|
||||
initResources: () => {},
|
||||
resolvePort: async () => 45000,
|
||||
env: { CLEAN_ENV: '1' },
|
||||
spawn: (_command, _args, options) => {
|
||||
spawnEnv = (options as { env: Record<string, string> }).env
|
||||
return { pid: 99999, once: () => undefined, unref: () => {} } as any
|
||||
},
|
||||
waitForBootReady: async () => undefined,
|
||||
openBrowser: () => {},
|
||||
stderr: { write: () => true },
|
||||
},
|
||||
)
|
||||
|
||||
assert.ok(spawnEnv)
|
||||
assert.equal(spawnEnv!.GSD_WEB_ALLOWED_ORIGINS, undefined)
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
// ─── runWebCliBranch end-to-end forwarding ───────────────────────────
|
||||
|
||||
test('runWebCliBranch forwards --host, --port, --allowed-origins to launchWebMode', async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'gsd-web-branch-flags-'))
|
||||
const projectDir = join(tmp, 'project')
|
||||
mkdirSync(projectDir, { recursive: true })
|
||||
|
||||
let receivedOptions: Record<string, unknown> | undefined
|
||||
|
||||
try {
|
||||
const flags = cliWeb.parseCliArgs([
|
||||
'node', 'dist/loader.js', '--web', projectDir,
|
||||
'--host', '0.0.0.0',
|
||||
'--port', '9000',
|
||||
'--allowed-origins', 'http://my-host:9000',
|
||||
])
|
||||
|
||||
const result = await cliWeb.runWebCliBranch(flags, {
|
||||
runWebMode: async (options) => {
|
||||
receivedOptions = options as unknown as Record<string, unknown>
|
||||
return {
|
||||
mode: 'web' as const,
|
||||
ok: true as const,
|
||||
cwd: options.cwd,
|
||||
projectSessionsDir: options.projectSessionsDir,
|
||||
host: '0.0.0.0',
|
||||
port: 9000,
|
||||
url: 'http://0.0.0.0:9000',
|
||||
hostKind: 'source-dev' as const,
|
||||
hostPath: '/tmp/fake-web/package.json',
|
||||
hostRoot: '/tmp/fake-web',
|
||||
}
|
||||
},
|
||||
stderr: { write: () => true },
|
||||
})
|
||||
|
||||
assert.equal(result.handled, true)
|
||||
if (!result.handled) throw new Error('expected handled')
|
||||
assert.equal(result.exitCode, 0)
|
||||
assert.ok(receivedOptions)
|
||||
assert.equal(receivedOptions!.host, '0.0.0.0')
|
||||
assert.equal(receivedOptions!.port, 9000)
|
||||
assert.deepEqual(receivedOptions!.allowedOrigins, ['http://my-host:9000'])
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
|
@ -36,6 +36,8 @@ export interface WebModeLaunchOptions {
|
|||
packageRoot?: string
|
||||
host?: string
|
||||
port?: number
|
||||
/** Additional allowed origins for CORS (forwarded as GSD_WEB_ALLOWED_ORIGINS). */
|
||||
allowedOrigins?: string[]
|
||||
}
|
||||
|
||||
export interface ResolvedWebHostBootstrap {
|
||||
|
|
@ -539,6 +541,7 @@ export async function launchWebMode(
|
|||
GSD_WEB_PACKAGE_ROOT: resolution.packageRoot,
|
||||
GSD_WEB_HOST_KIND: resolution.kind,
|
||||
...(resolution.kind === 'source-dev' ? { NEXT_PUBLIC_GSD_DEV: '1' } : {}),
|
||||
...(options.allowedOrigins?.length ? { GSD_WEB_ALLOWED_ORIGINS: options.allowedOrigins.join(',') } : {}),
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue