fix(setup): address code review issues (#221)

- Fix resolve variable shadowing in promptSecret (rename to fulfill)
- Fix readline leak on invalid choice (close before returning err)
- Remove Anthropic/Gemini from presets (not OpenAI-compatible)
- Fix GLM URL: api.z.ai → open.bigmodel.cn
- Restore terminal raw mode before process.exit on Ctrl+C
- Add debug logging to fetchAvailableModels failures
- Add comment explaining DashScope-specific model filter patterns
- Move PresetProvider and CmdSetupSuccess types to types.ts per convention
This commit is contained in:
2026-05-13 09:43:57 +08:00
parent 0c02cb7574
commit 47f2b1a128
6 changed files with 41 additions and 31 deletions
@@ -1,11 +1,15 @@
import { existsSync } from "node:fs";
import { stdin as input, stdout as output } from "node:process";
import { createInterface } from "node:readline/promises";
import { resolve } from "node:path";
import { resolve as resolvePath } from "node:path";
import { err, ok, type Result } from "@uncaged/workflow-protocol";
import { createLogger } from "@uncaged/workflow-util";
import { printCliError, printCliLine, printCliWarn } from "../../cli-output.js";
const setupDispatchLog = createLogger({ sink: { kind: "stderr" } });
import { loadPresetProviders } from "./preset-providers.js";
import { cmdSetup, printSetupSummary } from "./setup.js";
import type { SetupCliArgs } from "./types.js";
@@ -153,7 +157,7 @@ async function promptLine(
/** Read a line with terminal echo disabled (for secrets). */
async function promptSecret(label: string): Promise<string> {
process.stdout.write(label);
return new Promise((resolve) => {
return new Promise((fulfill) => {
let buf = "";
const rawWasSet = process.stdin.isRaw;
if (process.stdin.isTTY) {
@@ -171,7 +175,7 @@ async function promptSecret(label: string): Promise<string> {
process.stdin.pause();
process.stdin.removeListener("data", onData);
process.stdout.write("\n");
resolve(buf.trim());
fulfill(buf.trim());
return;
}
if (c === "\u007F" || c === "\b") {
@@ -182,6 +186,9 @@ async function promptSecret(label: string): Promise<string> {
continue;
}
if (c === "\u0003") {
if (process.stdin.isTTY) {
process.stdin.setRawMode(rawWasSet);
}
process.exit(130);
}
buf += c;
@@ -205,19 +212,23 @@ async function fetchAvailableModels(
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) {
setupDispatchLog("R5KH7WM3", `GET ${url} returned ${res.status}`);
return [];
}
const body = (await res.json()) as OpenAiModelsResponse;
if (!Array.isArray(body.data)) {
return [];
}
// Filter out non-chat models. Some patterns are DashScope-specific (sambert, cosyvoice,
// wordart, wanx, wan2, paraformer) but harmless for other providers.
const NON_CHAT_RE =
/speech|embed|image|video|audio|ocr|rerank|tts|asr|paraformer|sambert|cosyvoice|wordart|wanx|wan2|flux|stable-diffusion|z-image|s2s|livetranslate|realtime|gui-/i;
return body.data
.map((m) => m.id)
.filter((id) => !NON_CHAT_RE.test(id))
.sort();
} catch {
} catch (e) {
setupDispatchLog("V8NQ4JT6", `fetch models failed: ${e instanceof Error ? e.message : String(e)}`);
return [];
}
}
@@ -242,6 +253,7 @@ async function collectInteractiveSetup(): Promise<Result<SetupCliArgs, string>>
const choice = await promptLine(rl, `Choose [1-${presets.length + 1}]: `);
const choiceNum = Number.parseInt(choice, 10);
if (Number.isNaN(choiceNum) || choiceNum < 1 || choiceNum > presets.length + 1) {
rl.close();
return err(`invalid choice: ${choice}`);
}
@@ -331,7 +343,7 @@ async function collectInteractiveSetup(): Promise<Result<SetupCliArgs, string>>
}
const candidate = wsPath === "" ? "./workflows" : wsPath;
// Validate path before passing to cmdSetup.
const resolved = resolve(process.cwd(), candidate);
const resolved = resolvePath(process.cwd(), candidate);
if (existsSync(resolved)) {
printCliWarn(`directory already exists: ${resolved}`);
printCliLine("Please enter a different path, or type 'skip' to skip.");
@@ -1,4 +1,4 @@
export { dispatchSetup } from "./dispatch.js";
export { loadPresetProviders, type PresetProvider } from "./preset-providers.js";
export { type CmdSetupSuccess, cmdSetup, printSetupSummary } from "./setup.js";
export type { SetupCliArgs } from "./types.js";
export { loadPresetProviders } from "./preset-providers.js";
export { cmdSetup, printSetupSummary } from "./setup.js";
export type { CmdSetupSuccess, PresetProvider, SetupCliArgs } from "./types.js";
@@ -3,11 +3,9 @@ import { join } from "node:path";
import { parse as parseYaml } from "yaml";
export type PresetProvider = {
name: string;
label: string;
baseUrl: string;
};
import type { PresetProvider } from "./types.js";
type RawPresetEntry = {
name: unknown;
@@ -8,14 +8,6 @@
label: OpenAI
baseUrl: https://api.openai.com/v1
- name: anthropic
label: Anthropic
baseUrl: https://api.anthropic.com/v1
- name: gemini
label: Google Gemini
baseUrl: https://generativelanguage.googleapis.com/v1beta
- name: xai
label: xAI
baseUrl: https://api.x.ai/v1
@@ -52,7 +44,7 @@
- name: glm
label: GLM (Zhipu AI)
baseUrl: https://api.z.ai/api/paas/v4
baseUrl: https://open.bigmodel.cn/api/paas/v4
- name: stepfun
label: StepFun
@@ -9,18 +9,11 @@ import { createLogger } from "@uncaged/workflow-util";
import { printCliLine } from "../../cli-output.js";
import { cmdInitWorkspace } from "../init/index.js";
import type { SetupCliArgs } from "./types.js";
import type { CmdSetupSuccess, SetupCliArgs } from "./types.js";
const setupLog = createLogger({ sink: { kind: "stderr" } });
export type CmdSetupSuccess = {
registryPath: string;
provider: string;
defaultModel: string;
maxDepth: number;
supervisorInterval: number;
initWorkspaceRootPath: string | null;
};
function mergeWorkflowConfig(
prev: WorkflowConfig | null,
@@ -6,3 +6,18 @@ export type SetupCliArgs = {
defaultModel: string;
initWorkspaceName: string | null;
};
export type PresetProvider = {
name: string;
label: string;
baseUrl: string;
};
export type CmdSetupSuccess = {
registryPath: string;
provider: string;
defaultModel: string;
maxDepth: number;
supervisorInterval: number;
initWorkspaceRootPath: string | null;
};