Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 48 additions & 47 deletions packages/server/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ import boxen from "boxen";
import chalk from "chalk";
import { Command } from "commander";
import { version } from "../package.json";
import { getOrGenerateAuthSecret } from "./config";
import {
CLI_OPTION_DEFINITIONS,
getOrGenerateAuthSecret,
type ResolvedCliOptions,
} from "./config";
import {
buildOptionDescription,
optionKeys,
type RawCliOptions,
resolveOptions,
} from "./config/cli-parser";
import * as logger from "./logger";
import { ensurePostgres } from "./postgres";
import { startServer } from "./server";
Expand All @@ -14,67 +24,55 @@ const program = new Command();

program
.name("blink-server")
.description("Self-hosted Blink server")
.version(version)
.option("-p, --port <port>", "Port to run the server on", "3005")
.option(
"-d, --dev [host]",
"Proxy frontend requests to Next.js dev server (default: localhost:3000)"
)
.option(
"--wildcard-access-url <host>",
'Wildcard access URL for subdomain routing (e.g. "*.blink.example.com")'
)
.action(async (options) => {
try {
await runServer(options);
} catch (error) {
console.error(error, error instanceof Error ? error.stack : undefined);
logger.error(
error instanceof Error ? error.message : "An unknown error occurred"
);
process.exit(1);
}
});
.description(`Blink Server v${version}`)
.version(version);

for (const key of optionKeys) {
const spec = CLI_OPTION_DEFINITIONS[key];
const option = program.createOption(spec.flags, buildOptionDescription(spec));
if ("hidden" in spec && spec.hidden) {
option.hideHelp();
}
program.addOption(option);
}

async function runServer(options: {
port: string;
dev?: boolean | string;
wildcardAccessUrl?: string;
}) {
const port = parseInt(options.port, 10);
if (Number.isNaN(port) || port < 1 || port > 65535) {
throw new Error(`Invalid port: ${options.port}`);
program.action(async (options) => {
try {
await runServer(resolveOptions(options as RawCliOptions));
} catch (error) {
logger.error(
error instanceof Error ? error.message : "An unknown error occurred"
);
process.exit(1);
}
});

async function runServer(options: ResolvedCliOptions) {
console.log(chalk.bold("blink■"), version, chalk.gray("agents as a service"));

// Check and setup environment variables
let postgresUrl = process.env.POSTGRES_URL || process.env.DATABASE_URL;
// Resolve configuration.
let postgresUrl = options.postgresUrl;

if (!postgresUrl) {
postgresUrl = await ensurePostgres();
}

const authSecret = getOrGenerateAuthSecret();
const authSecret = options.authSecret ?? getOrGenerateAuthSecret();

const baseUrl = process.env.BASE_URL || `http://localhost:${port}`;
const baseUrlHost = options.host === "0.0.0.0" ? "localhost" : options.host;
const baseUrl = `http://${baseUrlHost}:${options.port}`;

const devProxy = options.dev
? options.dev === true
? "localhost:3000"
: options.dev
: undefined;
const devProxy = options.dev;

// Determine access URL - use BLINK_ACCESS_URL if set, otherwise create devhook
// Determine access URL - use configured access URL if set, otherwise create devhook.
let accessUrl: string;
let tunnelCleanup: (() => void) | undefined;
const tunnelServerUrl =
process.env.TUNNEL_SERVER_URL ?? "https://try.blink.host";
if (process.env.BLINK_ACCESS_URL) {
accessUrl = process.env.BLINK_ACCESS_URL;
const tunnelServerUrl = options.tunnelServerUrl;
const accessUrlOverride = options.accessUrl;
if (accessUrlOverride) {
accessUrl = accessUrlOverride;
} else {
const tunnel = await startTunnelProxy(tunnelServerUrl, port);
const tunnel = await startTunnelProxy(tunnelServerUrl, options.port);
accessUrl = tunnel.accessUrl;
tunnelCleanup = tunnel[Symbol.dispose];
}
Expand All @@ -94,13 +92,16 @@ async function runServer(options: {

// Start the server
const _srv = await startServer({
port,
host: options.host,
port: options.port,
postgresUrl,
authSecret,
baseUrl,
devProxy,
accessUrl,
wildcardAccessUrl: options.wildcardAccessUrl,
agentImage: options.agentImage,
devhookDisableAuth: options.devhookDisableAuth,
});

const box = boxen(
Expand Down
32 changes: 0 additions & 32 deletions packages/server/src/config.ts

This file was deleted.

78 changes: 78 additions & 0 deletions packages/server/src/config/cli-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
CLI_OPTION_DEFINITIONS,
type CliOptionKey,
getCliEnvValue,
parseCliOptionValue,
type ResolvedCliOptions,
} from "./config";

type RawCliOptionValue = string | boolean | undefined;
export type RawCliOptions = Partial<Record<CliOptionKey, RawCliOptionValue>>;

const toStringValue = (value: RawCliOptionValue): string | undefined => {
if (typeof value === "boolean") {
return value ? "true" : "false";
}
return value;
};

const hasShortFlag = (flags: string): boolean => {
const firstFlag = flags.split(",")[0]?.trim() ?? "";
return firstFlag.startsWith("-") && !firstFlag.startsWith("--");
};

export const optionKeys = (
Object.keys(CLI_OPTION_DEFINITIONS) as CliOptionKey[]
).sort((left, right) => {
const leftHasShort = hasShortFlag(CLI_OPTION_DEFINITIONS[left].flags);
const rightHasShort = hasShortFlag(CLI_OPTION_DEFINITIONS[right].flags);
if (leftHasShort !== rightHasShort) {
return leftHasShort ? -1 : 1;
}
return left.localeCompare(right);
});

export const buildOptionDescription = (spec: {
description: string;
env: string;
defaultValue?: unknown;
}): string => {
const parts = [spec.description];
if ("defaultValue" in spec && spec.defaultValue !== undefined) {
parts.push(`(default: ${spec.defaultValue})`);
}
parts.push(`[env: ${spec.env}]`);
return parts.join(" ");
};

export const resolveOptions = (
rawOptions: RawCliOptions
): ResolvedCliOptions => {
const resolved = {} as ResolvedCliOptions;
for (const key of optionKeys) {
const cliValue = rawOptions[key];
if (cliValue !== undefined) {
(resolved as Record<string, unknown>)[key] = parseCliOptionValue(
key,
toStringValue(cliValue),
"cli"
);
continue;
}
const envValue = getCliEnvValue(key);
if (envValue !== undefined) {
(resolved as Record<string, unknown>)[key] = envValue;
continue;
}
const spec = CLI_OPTION_DEFINITIONS[key];
const defaultValue = "defaultValue" in spec ? spec.defaultValue : undefined;
if (defaultValue !== undefined) {
(resolved as Record<string, unknown>)[key] = parseCliOptionValue(
key,
defaultValue,
"default"
);
}
}
return resolved;
};
Loading