mirror of
https://github.com/different-ai/openwork
synced 2026-05-10 17:22:05 +02:00
* refactor(repo): move OpenWork apps into apps and ee layout Rebase the monorepo layout migration onto the latest dev changes so the moved app, desktop, share, and cloud surfaces keep working from their new paths. Carry the latest deeplink, token persistence, build, Vercel, and docs updates forward to avoid stale references and broken deploy tooling. * chore(repo): drop generated desktop artifacts Ignore the moved Tauri target and sidecar paths so local cargo checks do not pollute the branch. Remove the accidentally committed outputs from the repo while keeping the layout migration intact. * fix(release): drop built server cli artifact Stop tracking the locally built apps/server/cli binary so generated server outputs do not leak into commits. Also update the release workflow to check the published scoped package name for @openwork/server before deciding whether npm publish is needed. * fix(workspace): add stable CLI bin wrappers Point the server and router package bins at committed wrapper scripts so workspace installs can create shims before dist outputs exist. Keep the wrappers compatible with built binaries and source checkouts to avoid Vercel install warnings without changing runtime behavior.
332 lines
11 KiB
TypeScript
332 lines
11 KiB
TypeScript
import { homedir } from "node:os";
|
|
import { dirname, resolve } from "node:path";
|
|
import type { ApprovalMode, ApprovalConfig, ServerConfig, WorkspaceConfig, LogFormat } from "./types.js";
|
|
import { buildWorkspaceInfos } from "./workspaces.js";
|
|
import { parseList, readJsonFile, shortId } from "./utils.js";
|
|
|
|
interface CliArgs {
|
|
configPath?: string;
|
|
host?: string;
|
|
port?: number;
|
|
token?: string;
|
|
hostToken?: string;
|
|
approvalMode?: ApprovalMode;
|
|
approvalTimeoutMs?: number;
|
|
opencodeBaseUrl?: string;
|
|
opencodeDirectory?: string;
|
|
opencodeUsername?: string;
|
|
opencodePassword?: string;
|
|
workspaces: string[];
|
|
corsOrigins?: string[];
|
|
readOnly?: boolean;
|
|
verbose?: boolean;
|
|
logFormat?: LogFormat;
|
|
logRequests?: boolean;
|
|
version?: boolean;
|
|
help?: boolean;
|
|
}
|
|
|
|
interface FileConfig {
|
|
host?: string;
|
|
port?: number;
|
|
token?: string;
|
|
hostToken?: string;
|
|
approval?: Partial<ApprovalConfig>;
|
|
workspaces?: WorkspaceConfig[];
|
|
corsOrigins?: string[];
|
|
authorizedRoots?: string[];
|
|
readOnly?: boolean;
|
|
opencodeUsername?: string;
|
|
opencodePassword?: string;
|
|
logFormat?: LogFormat;
|
|
logRequests?: boolean;
|
|
}
|
|
|
|
const DEFAULT_PORT = 8787;
|
|
const DEFAULT_HOST = "127.0.0.1";
|
|
const DEFAULT_TIMEOUT_MS = 30000;
|
|
const DEFAULT_LOG_FORMAT: LogFormat = "pretty";
|
|
const DEFAULT_LOG_REQUESTS = true;
|
|
|
|
function normalizeLogFormat(value: string | undefined): LogFormat | undefined {
|
|
if (!value) return undefined;
|
|
const normalized = value.trim().toLowerCase();
|
|
if (normalized === "json") return "json";
|
|
if (normalized === "pretty" || normalized === "text" || normalized === "human") return "pretty";
|
|
return undefined;
|
|
}
|
|
|
|
function parseBoolean(value: string | undefined): boolean | undefined {
|
|
if (!value) return undefined;
|
|
const normalized = value.trim().toLowerCase();
|
|
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
|
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
|
return undefined;
|
|
}
|
|
|
|
export function parseCliArgs(argv: string[]): CliArgs {
|
|
const args: CliArgs = { workspaces: [] };
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const value = argv[index];
|
|
if (!value) continue;
|
|
if (value === "--help" || value === "-h") {
|
|
args.help = true;
|
|
continue;
|
|
}
|
|
if (value === "--version") {
|
|
args.version = true;
|
|
continue;
|
|
}
|
|
if (value === "--verbose") {
|
|
args.verbose = true;
|
|
continue;
|
|
}
|
|
if (value === "--log-format") {
|
|
args.logFormat = argv[index + 1] as LogFormat | undefined;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--log-requests") {
|
|
args.logRequests = true;
|
|
continue;
|
|
}
|
|
if (value === "--no-log-requests") {
|
|
args.logRequests = false;
|
|
continue;
|
|
}
|
|
if (value === "--config") {
|
|
args.configPath = argv[index + 1];
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--host") {
|
|
args.host = argv[index + 1];
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--port") {
|
|
const port = Number(argv[index + 1]);
|
|
if (!Number.isNaN(port)) args.port = port;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--token") {
|
|
args.token = argv[index + 1];
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--host-token") {
|
|
args.hostToken = argv[index + 1];
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--approval") {
|
|
const mode = argv[index + 1] as ApprovalMode | undefined;
|
|
if (mode === "manual" || mode === "auto") args.approvalMode = mode;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--approval-timeout") {
|
|
const timeout = Number(argv[index + 1]);
|
|
if (!Number.isNaN(timeout)) args.approvalTimeoutMs = timeout;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--opencode-base-url") {
|
|
args.opencodeBaseUrl = argv[index + 1];
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--opencode-directory") {
|
|
args.opencodeDirectory = argv[index + 1];
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--opencode-username") {
|
|
args.opencodeUsername = argv[index + 1];
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--opencode-password") {
|
|
args.opencodePassword = argv[index + 1];
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--workspace") {
|
|
const path = argv[index + 1];
|
|
if (path) args.workspaces.push(path);
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--cors") {
|
|
args.corsOrigins = parseList(argv[index + 1]);
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (value === "--read-only") {
|
|
args.readOnly = true;
|
|
continue;
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
export function printHelp(): void {
|
|
const message = [
|
|
"openwork-server",
|
|
"",
|
|
"Options:",
|
|
" --config <path> Path to server.json",
|
|
" --host <host> Hostname (default 127.0.0.1)",
|
|
" --port <port> Port (default 8787)",
|
|
" --token <token> Client bearer token",
|
|
" --host-token <token> Host approval token",
|
|
" --approval <mode> manual | auto",
|
|
" --approval-timeout <ms> Approval timeout",
|
|
" --opencode-base-url <url> OpenCode base URL to share",
|
|
" --opencode-directory <path> OpenCode workspace directory to share",
|
|
" --opencode-username <user> OpenCode server username",
|
|
" --opencode-password <pass> OpenCode server password",
|
|
" --workspace <path> Workspace root (repeatable)",
|
|
" --cors <origins> Comma-separated origins or *",
|
|
" --read-only Disable writes",
|
|
" --log-format <format> Log output format: pretty | json",
|
|
" --log-requests Log incoming requests (default: true)",
|
|
" --no-log-requests Disable request logging",
|
|
" --verbose Print resolved config",
|
|
" --version Show version",
|
|
].join("\n");
|
|
console.log(message);
|
|
}
|
|
|
|
async function loadFileConfig(configPath: string): Promise<FileConfig> {
|
|
const parsed = await readJsonFile<FileConfig>(configPath);
|
|
return parsed ?? {};
|
|
}
|
|
|
|
export async function resolveServerConfig(cli: CliArgs): Promise<ServerConfig> {
|
|
const envConfigPath = process.env.OPENWORK_SERVER_CONFIG;
|
|
const configPath = cli.configPath ?? envConfigPath ?? resolve(homedir(), ".config", "openwork", "server.json");
|
|
const fileConfig = await loadFileConfig(configPath);
|
|
const configDir = dirname(configPath);
|
|
|
|
const envWorkspaces = parseList(process.env.OPENWORK_WORKSPACES);
|
|
let workspaceConfigs: WorkspaceConfig[] =
|
|
cli.workspaces.length > 0
|
|
? cli.workspaces.map((path) => ({ path }))
|
|
: envWorkspaces.length > 0
|
|
? envWorkspaces.map((path) => ({ path }))
|
|
: fileConfig.workspaces ?? [];
|
|
|
|
const envOpencodeBaseUrl = process.env.OPENWORK_OPENCODE_BASE_URL;
|
|
const envOpencodeDirectory = process.env.OPENWORK_OPENCODE_DIRECTORY;
|
|
const envOpencodeUsername = process.env.OPENWORK_OPENCODE_USERNAME;
|
|
const envOpencodePassword = process.env.OPENWORK_OPENCODE_PASSWORD;
|
|
const opencodeBaseUrl = cli.opencodeBaseUrl ?? envOpencodeBaseUrl;
|
|
const opencodeDirectory = cli.opencodeDirectory ?? envOpencodeDirectory;
|
|
const opencodeUsername = cli.opencodeUsername ?? envOpencodeUsername ?? fileConfig.opencodeUsername;
|
|
const opencodePassword = cli.opencodePassword ?? envOpencodePassword ?? fileConfig.opencodePassword;
|
|
|
|
if (workspaceConfigs.length > 0 && (opencodeBaseUrl || opencodeDirectory || opencodeUsername || opencodePassword)) {
|
|
const allowDirectoryOverride = workspaceConfigs.length === 1 && opencodeDirectory;
|
|
workspaceConfigs = workspaceConfigs.map((workspace, index) => {
|
|
const nextDirectory =
|
|
workspace.directory ?? (allowDirectoryOverride && index === 0 ? opencodeDirectory : undefined);
|
|
return {
|
|
...workspace,
|
|
baseUrl: workspace.baseUrl ?? opencodeBaseUrl,
|
|
directory: nextDirectory,
|
|
opencodeUsername: workspace.opencodeUsername ?? opencodeUsername,
|
|
opencodePassword: workspace.opencodePassword ?? opencodePassword,
|
|
};
|
|
});
|
|
}
|
|
|
|
const workspaces = buildWorkspaceInfos(workspaceConfigs, configDir);
|
|
|
|
const tokenFromEnv = process.env.OPENWORK_TOKEN;
|
|
const hostTokenFromEnv = process.env.OPENWORK_HOST_TOKEN;
|
|
|
|
const token = cli.token ?? tokenFromEnv ?? fileConfig.token ?? shortId();
|
|
const hostToken = cli.hostToken ?? hostTokenFromEnv ?? fileConfig.hostToken ?? shortId();
|
|
|
|
const tokenSource: ServerConfig["tokenSource"] = cli.token
|
|
? "cli"
|
|
: tokenFromEnv
|
|
? "env"
|
|
: fileConfig.token
|
|
? "file"
|
|
: "generated";
|
|
|
|
const hostTokenSource: ServerConfig["hostTokenSource"] = cli.hostToken
|
|
? "cli"
|
|
: hostTokenFromEnv
|
|
? "env"
|
|
: fileConfig.hostToken
|
|
? "file"
|
|
: "generated";
|
|
|
|
const approvalMode =
|
|
cli.approvalMode ??
|
|
(process.env.OPENWORK_APPROVAL_MODE as ApprovalMode | undefined) ??
|
|
fileConfig.approval?.mode ??
|
|
"manual";
|
|
|
|
const approvalTimeoutMs =
|
|
cli.approvalTimeoutMs ??
|
|
(process.env.OPENWORK_APPROVAL_TIMEOUT_MS ? Number(process.env.OPENWORK_APPROVAL_TIMEOUT_MS) : undefined) ??
|
|
fileConfig.approval?.timeoutMs ??
|
|
DEFAULT_TIMEOUT_MS;
|
|
|
|
const approval: ApprovalConfig = {
|
|
mode: approvalMode === "auto" ? "auto" : "manual",
|
|
timeoutMs: Number.isNaN(approvalTimeoutMs) ? DEFAULT_TIMEOUT_MS : approvalTimeoutMs,
|
|
};
|
|
|
|
const envCorsOrigins = process.env.OPENWORK_CORS_ORIGINS;
|
|
const parsedEnvCors = envCorsOrigins ? parseList(envCorsOrigins) : null;
|
|
const corsOrigins = cli.corsOrigins ?? parsedEnvCors ?? fileConfig.corsOrigins ?? ["*"];
|
|
|
|
const envReadOnly = process.env.OPENWORK_READONLY;
|
|
const parsedReadOnly = envReadOnly
|
|
? ["true", "1", "yes"].includes(envReadOnly.toLowerCase())
|
|
: undefined;
|
|
const readOnly = cli.readOnly ?? parsedReadOnly ?? fileConfig.readOnly ?? false;
|
|
|
|
const envLogFormat = process.env.OPENWORK_LOG_FORMAT;
|
|
const logFormat =
|
|
cli.logFormat ??
|
|
normalizeLogFormat(envLogFormat) ??
|
|
normalizeLogFormat(fileConfig.logFormat) ??
|
|
DEFAULT_LOG_FORMAT;
|
|
|
|
const envLogRequests = parseBoolean(process.env.OPENWORK_LOG_REQUESTS);
|
|
const logRequests = cli.logRequests ?? envLogRequests ?? fileConfig.logRequests ?? DEFAULT_LOG_REQUESTS;
|
|
|
|
const authorizedRoots =
|
|
fileConfig.authorizedRoots?.length
|
|
? fileConfig.authorizedRoots.map((root) => resolve(configDir, root))
|
|
: workspaces.map((workspace) => workspace.path);
|
|
|
|
const host = cli.host ?? process.env.OPENWORK_HOST ?? fileConfig.host ?? DEFAULT_HOST;
|
|
const port = cli.port ?? (process.env.OPENWORK_PORT ? Number(process.env.OPENWORK_PORT) : undefined) ?? fileConfig.port ?? DEFAULT_PORT;
|
|
|
|
return {
|
|
host,
|
|
port: Number.isNaN(port) ? DEFAULT_PORT : port,
|
|
token,
|
|
hostToken,
|
|
configPath,
|
|
approval,
|
|
corsOrigins,
|
|
workspaces,
|
|
authorizedRoots,
|
|
readOnly,
|
|
startedAt: Date.now(),
|
|
tokenSource,
|
|
hostTokenSource,
|
|
logFormat,
|
|
logRequests,
|
|
};
|
|
}
|