|
|
|
@@ -23,6 +23,8 @@ interface WebtermConfig {
|
|
|
|
|
defaults: { targetId: string };
|
|
|
|
|
sessionStartModes: SessionStartModes;
|
|
|
|
|
autoResumePrompt: AutoResumePrompt;
|
|
|
|
|
browserTerminalHistory: BrowserTerminalHistory;
|
|
|
|
|
browserOutputPolicy: BrowserOutputPolicy;
|
|
|
|
|
targets: WebtermTarget[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -39,6 +41,17 @@ interface AutoResumePrompt {
|
|
|
|
|
readiness: { minDelayMs: number; minOutputBytes: number; quietPeriodMs: number; timeoutMs: number };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface BrowserTerminalHistory {
|
|
|
|
|
desktopScrollbackLines: number;
|
|
|
|
|
mobileScrollbackLines: number;
|
|
|
|
|
mobileMaxWidthPx: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface BrowserOutputPolicy {
|
|
|
|
|
maxFramesPerSecond: number;
|
|
|
|
|
maxFrameBytes: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface WebtermTarget {
|
|
|
|
|
id: string;
|
|
|
|
|
enabled: boolean;
|
|
|
|
@@ -59,6 +72,8 @@ interface WebtermTarget {
|
|
|
|
|
autoStartSessions: Array<{ title: string; cwd: string; command: string; cols: number; rows: number; resumePrompt: boolean; fallbackShell: boolean }>;
|
|
|
|
|
sessionStartModes: SessionStartModes;
|
|
|
|
|
autoResumePrompt: AutoResumePrompt;
|
|
|
|
|
browserTerminalHistory: BrowserTerminalHistory;
|
|
|
|
|
browserOutputPolicy: BrowserOutputPolicy;
|
|
|
|
|
environment: Record<string, string>;
|
|
|
|
|
};
|
|
|
|
|
publicExposure: {
|
|
|
|
@@ -256,17 +271,35 @@ function readWebtermConfig(): WebtermConfig {
|
|
|
|
|
const defaults = recordField(yaml, "defaults", "webterm");
|
|
|
|
|
const sessionStartModes = parseSessionStartModes(recordField(yaml, "sessionStartModes", "webterm"), "webterm.sessionStartModes");
|
|
|
|
|
const autoResumePrompt = parseAutoResumePrompt(recordField(yaml, "autoResumePrompt", "webterm"), "webterm.autoResumePrompt");
|
|
|
|
|
const browserTerminalHistory = parseBrowserTerminalHistory(recordField(yaml, "browserTerminalHistory", "webterm"), "webterm.browserTerminalHistory");
|
|
|
|
|
const browserOutputPolicy = parseBrowserOutputPolicy(recordField(yaml, "browserOutputPolicy", "webterm"), "webterm.browserOutputPolicy");
|
|
|
|
|
const targetsRaw = yaml.targets;
|
|
|
|
|
if (!Array.isArray(targetsRaw)) throw new Error("webterm.targets must be an array");
|
|
|
|
|
return {
|
|
|
|
|
defaults: { targetId: stringField(defaults, "targetId", "webterm.defaults") },
|
|
|
|
|
sessionStartModes,
|
|
|
|
|
autoResumePrompt,
|
|
|
|
|
targets: targetsRaw.map((item, index) => parseTarget(asRecord(item, `webterm.targets[${index}]`), `webterm.targets[${index}]`, sessionStartModes, autoResumePrompt)),
|
|
|
|
|
browserTerminalHistory,
|
|
|
|
|
browserOutputPolicy,
|
|
|
|
|
targets: targetsRaw.map((item, index) => parseTarget(
|
|
|
|
|
asRecord(item, `webterm.targets[${index}]`),
|
|
|
|
|
`webterm.targets[${index}]`,
|
|
|
|
|
sessionStartModes,
|
|
|
|
|
autoResumePrompt,
|
|
|
|
|
browserTerminalHistory,
|
|
|
|
|
browserOutputPolicy,
|
|
|
|
|
)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseTarget(record: Record<string, unknown>, path: string, sessionStartModes: SessionStartModes, autoResumePrompt: AutoResumePrompt): WebtermTarget {
|
|
|
|
|
function parseTarget(
|
|
|
|
|
record: Record<string, unknown>,
|
|
|
|
|
path: string,
|
|
|
|
|
sessionStartModes: SessionStartModes,
|
|
|
|
|
autoResumePrompt: AutoResumePrompt,
|
|
|
|
|
browserTerminalHistory: BrowserTerminalHistory,
|
|
|
|
|
browserOutputPolicy: BrowserOutputPolicy,
|
|
|
|
|
): WebtermTarget {
|
|
|
|
|
const runtime = recordField(record, "runtime", path);
|
|
|
|
|
const exposure = recordField(record, "publicExposure", path);
|
|
|
|
|
const dns = recordField(exposure, "dns", `${path}.publicExposure`);
|
|
|
|
@@ -295,6 +328,8 @@ function parseTarget(record: Record<string, unknown>, path: string, sessionStart
|
|
|
|
|
autoStartSessions,
|
|
|
|
|
sessionStartModes,
|
|
|
|
|
autoResumePrompt,
|
|
|
|
|
browserTerminalHistory,
|
|
|
|
|
browserOutputPolicy,
|
|
|
|
|
environment,
|
|
|
|
|
},
|
|
|
|
|
publicExposure: {
|
|
|
|
@@ -339,6 +374,8 @@ function renderCompose(target: WebtermTarget): string {
|
|
|
|
|
SESSION_START_MODES_JSON: JSON.stringify(target.runtime.sessionStartModes.options),
|
|
|
|
|
SESSION_START_DEFAULT_MODE: target.runtime.sessionStartModes.defaultMode,
|
|
|
|
|
AUTO_RESUME_PROMPT_JSON: JSON.stringify(target.runtime.autoResumePrompt),
|
|
|
|
|
BROWSER_TERMINAL_HISTORY_JSON: JSON.stringify(target.runtime.browserTerminalHistory),
|
|
|
|
|
BROWSER_OUTPUT_POLICY_JSON: JSON.stringify(target.runtime.browserOutputPolicy),
|
|
|
|
|
};
|
|
|
|
|
const envLines = Object.entries(environment)
|
|
|
|
|
.map(([key, value]) => ` ${key}: ${quoteYaml(value)}`)
|
|
|
|
@@ -442,6 +479,8 @@ function targetSummary(target: WebtermTarget): Record<string, unknown> {
|
|
|
|
|
sendEnter: target.runtime.autoResumePrompt.sendEnter,
|
|
|
|
|
enterDelayMs: target.runtime.autoResumePrompt.enterDelayMs,
|
|
|
|
|
},
|
|
|
|
|
browserTerminalHistory: target.runtime.browserTerminalHistory,
|
|
|
|
|
browserOutputPolicy: target.runtime.browserOutputPolicy,
|
|
|
|
|
sourceMounts: target.runtime.sourceMounts.map((mount) => `${mount.source}->${mount.target}${mount.readOnly ? ":ro" : ""}`),
|
|
|
|
|
hostPort: target.runtime.hostPort,
|
|
|
|
|
containerPort: target.runtime.containerPort,
|
|
|
|
@@ -601,6 +640,27 @@ function parseAutoResumePrompt(record: Record<string, unknown>, path: string): A
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseBrowserTerminalHistory(record: Record<string, unknown>, path: string): BrowserTerminalHistory {
|
|
|
|
|
return {
|
|
|
|
|
desktopScrollbackLines: positiveIntegerField(record, "desktopScrollbackLines", path),
|
|
|
|
|
mobileScrollbackLines: positiveIntegerField(record, "mobileScrollbackLines", path),
|
|
|
|
|
mobileMaxWidthPx: positiveIntegerField(record, "mobileMaxWidthPx", path),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseBrowserOutputPolicy(record: Record<string, unknown>, path: string): BrowserOutputPolicy {
|
|
|
|
|
return {
|
|
|
|
|
maxFramesPerSecond: positiveIntegerField(record, "maxFramesPerSecond", path),
|
|
|
|
|
maxFrameBytes: positiveIntegerField(record, "maxFrameBytes", path),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function positiveIntegerField(record: Record<string, unknown>, key: string, path: string): number {
|
|
|
|
|
const value = integerField(record, key, path);
|
|
|
|
|
if (value <= 0) throw new Error(`${path}.${key} must be positive`);
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseLiteral<T extends string>(value: string, expected: T, path: string): T {
|
|
|
|
|
if (value !== expected) throw new Error(`${path} must be ${expected}`);
|
|
|
|
|
return expected;
|
|
|
|
|