Skip to content
Open
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ npx @grinev/opencode-telegram-bot@latest

> Quick start is for npm usage. You do not need to clone this repository. If you run this command from the source directory (repository root), it may fail with `opencode-telegram: not found`. To run from sources, use the [Development](#development) section.

On first launch, an interactive wizard will guide you through the configuration — it asks for interface language first, then your bot token, user ID, OpenCode API URL, and optional OpenCode server credentials (username/password). After that, you're ready to go. Open your bot in Telegram and start sending tasks.
If required configuration is not supplied through process environment variables or an `.env` file, an interactive wizard will guide you through setup. It asks for interface language first, then your bot token, user ID, OpenCode API URL, and optional OpenCode server credentials (username/password). After that, you're ready to go. Open your bot in Telegram and start sending tasks.

#### Alternative: Global Install

Expand Down Expand Up @@ -199,7 +199,7 @@ For this to work, the console OpenCode instance must be started on the same port

### Environment Variables

When installed via npm, the configuration wizard handles the initial setup. The `.env` file is stored in your platform's app data directory:
Configuration can be provided through process environment variables or an `.env` file. Process environment values take precedence. When installed via npm, the configuration wizard handles any missing required values and stores the generated `.env` file in your platform's app data directory:

- **macOS:** `~/Library/Application Support/opencode-telegram-bot/.env`
- **Windows:** `%APPDATA%\opencode-telegram-bot\.env`
Expand Down
16 changes: 11 additions & 5 deletions src/runtime/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ function isValidHttpUrl(value: string): boolean {
}
}

export function validateRuntimeEnvValues(values: Record<string, string>): EnvValidationResult {
export function validateRuntimeEnvValues(
values: Record<string, string | undefined>,
): EnvValidationResult {
if (!values.TELEGRAM_BOT_TOKEN || values.TELEGRAM_BOT_TOKEN.trim().length === 0) {
return { isValid: false, reason: "Missing TELEGRAM_BOT_TOKEN" };
}
Expand Down Expand Up @@ -562,13 +564,17 @@ function ensureInteractiveTty(): void {

async function validateExistingEnv(envFilePath: string): Promise<EnvValidationResult> {
const content = await readEnvFileIfExists(envFilePath);
const effectiveValues: Record<string, string | undefined> =
content === null ? {} : dotenv.parse(content);

if (content === null) {
return { isValid: false, reason: "Missing .env" };
for (const key of WIZARD_ENV_KEYS) {
const processValue = process.env[key];
if (processValue !== undefined) {
effectiveValues[key] = processValue;
}
}

const parsed = dotenv.parse(content);
return validateRuntimeEnvValues(parsed);
return validateRuntimeEnvValues(effectiveValues);
}

async function runWizardAndPersist(runtimePaths: RuntimePaths): Promise<void> {
Expand Down
96 changes: 94 additions & 2 deletions tests/runtime/bootstrap.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import fs from "node:fs";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { buildEnvFileContent, validateRuntimeEnvValues } from "../../src/runtime/bootstrap.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
buildEnvFileContent,
ensureRuntimeConfigForStart,
validateRuntimeEnvValues,
} from "../../src/runtime/bootstrap.js";
import { setRuntimeMode } from "../../src/runtime/mode.js";

const ENV_EXAMPLE_CONTENT = fs.readFileSync(path.resolve(process.cwd(), ".env.example"), "utf-8");

Expand Down Expand Up @@ -185,3 +192,88 @@ describe("runtime/bootstrap", () => {
expect(updated.trimEnd().endsWith("ANOTHER_CUSTOM=1")).toBe(true);
});
});

describe("runtime/bootstrap installed configuration", () => {
let tempHome: string;
let stdinTtyDescriptor: PropertyDescriptor | undefined;
let stdoutTtyDescriptor: PropertyDescriptor | undefined;

beforeEach(async () => {
tempHome = await mkdtemp(path.join(os.tmpdir(), "opencode-telegram-bootstrap-"));
stdinTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
stdoutTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: false });
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: false });
vi.stubEnv("OPENCODE_TELEGRAM_HOME", tempHome);
vi.stubEnv("TELEGRAM_BOT_TOKEN", "123456:process-token");
vi.stubEnv("TELEGRAM_ALLOWED_USER_ID", "123456789");
vi.stubEnv("OPENCODE_MODEL_PROVIDER", "process-provider");
vi.stubEnv("OPENCODE_MODEL_ID", "process-model");
vi.stubEnv("OPENCODE_API_URL", "");
vi.stubEnv("BOT_LOCALE", "en");
setRuntimeMode("installed");
});

afterEach(async () => {
if (stdinTtyDescriptor) {
Object.defineProperty(process.stdin, "isTTY", stdinTtyDescriptor);
} else {
Reflect.deleteProperty(process.stdin, "isTTY");
}
if (stdoutTtyDescriptor) {
Object.defineProperty(process.stdout, "isTTY", stdoutTtyDescriptor);
} else {
Reflect.deleteProperty(process.stdout, "isTTY");
}
vi.unstubAllEnvs();
delete process.env.OPENCODE_TELEGRAM_RUNTIME_MODE;
await rm(tempHome, { recursive: true, force: true });
});

it("accepts required values from process.env without an .env file", async () => {
await ensureRuntimeConfigForStart();

expect(fs.existsSync(path.join(tempHome, ".env"))).toBe(false);
await expect(readFile(path.join(tempHome, "settings.json"), "utf-8")).resolves.toBe("{}\n");
});

it("merges .env values with process.env taking precedence", async () => {
delete process.env.OPENCODE_MODEL_PROVIDER;
delete process.env.OPENCODE_MODEL_ID;
await writeFile(
path.join(tempHome, ".env"),
[
"TELEGRAM_BOT_TOKEN=",
"TELEGRAM_ALLOWED_USER_ID=invalid",
"OPENCODE_MODEL_PROVIDER=file-provider",
"OPENCODE_MODEL_ID=file-model",
"",
].join("\n"),
"utf-8",
);

await ensureRuntimeConfigForStart();

await expect(readFile(path.join(tempHome, "settings.json"), "utf-8")).resolves.toBe("{}\n");
});

it("rejects an invalid process.env value even when .env is valid", async () => {
vi.stubEnv("TELEGRAM_ALLOWED_USER_ID", "invalid");
await writeFile(
path.join(tempHome, ".env"),
[
"TELEGRAM_BOT_TOKEN=123456:file-token",
"TELEGRAM_ALLOWED_USER_ID=42",
"OPENCODE_MODEL_PROVIDER=file-provider",
"OPENCODE_MODEL_ID=file-model",
"",
].join("\n"),
"utf-8",
);

await expect(ensureRuntimeConfigForStart()).rejects.toThrow(
"Interactive wizard requires a TTY terminal",
);
expect(fs.existsSync(path.join(tempHome, "settings.json"))).toBe(false);
});
});