From 78a4f42bc0faa44ae7733fdce214551bc266ae3c Mon Sep 17 00:00:00 2001 From: "nhat.nguyenhong" Date: Thu, 24 Sep 2026 14:07:32 +0000 Subject: [PATCH] fix: sync-models will now prompt for API key when there is none, and it won't terminate when there is no opencode.json --- README.md | 5 +++++ scripts/sync-models.mjs | 47 ++++++++++++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3894a37..1b0fa76 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,11 @@ without writing. Each row shows the model id and name plus a second line with it (`ctx 1M (1000000)`); the catalog carries no pricing fields, so no cost is shown. Needs an interactive terminal. +Before anything else the script requires a CommandCode API key (from +`provider.commandcode.options.apiKey` or `COMMANDCODE_API_KEY`). If neither is present — including +when `opencode.json` does not exist yet — it exits and points you at the CommandCode Console +() to generate one. It never writes without a key. + Then restart opencode and confirm: ```powershell diff --git a/scripts/sync-models.mjs b/scripts/sync-models.mjs index 313e563..0b56e72 100644 --- a/scripts/sync-models.mjs +++ b/scripts/sync-models.mjs @@ -7,15 +7,16 @@ // the result into the global opencode config. Deselecting a model removes it from the config. // // Run: node scripts/sync-models.mjs (build first: npm run build) -import { readFileSync, writeFileSync, renameSync } from "node:fs"; +import { mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import readline from "node:readline"; import { createCommandCode } from "../dist/index.js"; import { redact } from "../dist/redact.js"; import { DEFAULT_BASE_URL, DEFAULT_CC_VERSION, DEFAULT_MAX_TOKENS, MODELS_PATH } from "../dist/constants.js"; const CONFIG_PATH = join(homedir(), ".config", "opencode", "opencode.json"); +const CONSOLE_URL = "https://commandcode.ai/studio/provider"; const CONCURRENCY = 6; // 64x64 solid red PNG, used only to provoke a vision-accepting vs vision-rejecting response. @@ -36,8 +37,15 @@ const NO_IMAGE = const VISION_HEURISTIC = /claude|gpt-5|gemini|grok|qwen.*vl|vision|(?:^|[^a-z])vl(?:[^a-z]|$)|omni|multimodal/i; function readConfig(path) { - const text = readFileSync(path, "utf8"); - return { text, data: JSON.parse(text) }; + try { + const text = readFileSync(path, "utf8"); + return { text, data: JSON.parse(text), exists: true }; + } catch (error) { + // A fresh install has no opencode.json yet; treat it as empty so the console + // guidance / env-key path can still run. Bad JSON or unreadable files still throw. + if (error.code === "ENOENT") return { text: "", data: {}, exists: false }; + throw error; + } } function detectIndent(text) { @@ -360,23 +368,36 @@ function pickModels(catalog, existingIds) { } async function main() { - if (!process.stdin.isTTY || !process.stdout.isTTY) { - console.error("Interactive picker needs a TTY; nothing was written."); - process.exit(1); - } - let config; let text; + let exists; try { - ({ text, data: config } = readConfig(CONFIG_PATH)); + ({ text, data: config, exists } = readConfig(CONFIG_PATH)); } catch (error) { console.error(`Cannot read ${CONFIG_PATH}: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } + // Mandatory first step: without a CommandCode key there is nothing to fetch or + // probe, so point a fresh user at the Console before doing anything else. const key = resolveKey(config, process.env); if (!key) { - console.error("No API key: set COMMANDCODE_API_KEY or provider.commandcode.options.apiKey"); + const where = exists + ? `No CommandCode API key in ${CONFIG_PATH}.` + : `No opencode.json found at ${CONFIG_PATH}.`; + console.error( + `${where}\n` + + `Generate an API key in the CommandCode Console: ${CONSOLE_URL}\n` + + (exists + ? `Add it as provider.commandcode.options.apiKey, or export COMMANDCODE_API_KEY.` + : `Create that file with provider.commandcode.options.apiKey, or export COMMANDCODE_API_KEY.`) + + `\nNothing was written.`, + ); + process.exit(1); + } + + if (!process.stdin.isTTY || !process.stdout.isTTY) { + console.error("Interactive picker needs a TTY; nothing was written."); process.exit(1); } @@ -447,7 +468,9 @@ async function main() { config.provider.commandcode.models = merged; const indent = detectIndent(text); - const trailing = text.endsWith("\n") ? "\n" : ""; + const trailing = text === "" || text.endsWith("\n") ? "\n" : ""; + // The config directory may not exist yet on a fresh install; create it before the atomic write. + mkdirSync(dirname(CONFIG_PATH), { recursive: true }); writeAtomic(CONFIG_PATH, `${JSON.stringify(config, null, indent)}${trailing}`); console.log(