saycoder 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,12 +5,14 @@ A small CLI that configures OpenCode to use SayCoder as the model layer, plus pr
5
5
  ## Setup flow
6
6
 
7
7
  1. Checks whether `opencode` is installed.
8
- 2. If missing, asks whether to install it with `npm install -g opencode-ai`.
9
- 3. Prompts the user for a SayCoder API key.
10
- 4. Validates the key with `GET https://jstapi.xinbai.icu/v1/models`.
11
- 5. Uses the returned model list to populate OpenCode provider models.
12
- 6. Writes OpenCode config and installs lightweight SayCoder skills.
13
- 7. Tells the user to choose a model and start coding.
8
+ 2. If missing, asks whether to install it with `npm install -g opencode-ai --foreground-scripts`.
9
+ 3. Optionally enables China mirror optimization, measures available mirrors, and lets the user choose with arrow keys or number input on Windows.
10
+ 4. Installs OpenCode and verifies `opencode --version` before continuing.
11
+ 5. Prompts the user for a SayCoder API key.
12
+ 6. Validates the key with `GET https://jstapi.xinbai.icu/v1/models`.
13
+ 7. Uses the returned model list to populate OpenCode provider models.
14
+ 8. Writes OpenCode config and installs lightweight SayCoder skills.
15
+ 9. Tells the user to choose a model and start coding.
14
16
 
15
17
  ## MCP defaults
16
18
 
@@ -23,7 +25,7 @@ SayCoder's base URL is only used for the OpenAI-compatible model channel, not fo
23
25
 
24
26
  ## China mirror
25
27
 
26
- When OpenCode is missing, setup asks whether to install through the China npm mirror.
28
+ When OpenCode is missing, setup asks whether to install through China mirror optimization. If enabled, it measures common npm mirrors and lets you choose one with arrow keys. If OpenCode installation or verification fails, setup stops and prints the manual install command.
27
29
  You can also force it with:
28
30
 
29
31
  ```bash
package/bin/saycoder.js CHANGED
@@ -7,6 +7,7 @@ import { homedir } from "node:os";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { stdin as input, stdout as output } from "node:process";
9
9
  import { spawnSync } from "node:child_process";
10
+ import { emitKeypressEvents } from "node:readline";
10
11
 
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
13
  const packageRoot = resolve(__dirname, "..");
@@ -30,6 +31,15 @@ const color = {
30
31
  yellow: (value) => paint(33, value),
31
32
  };
32
33
 
34
+ const NPM_MIRRORS = [
35
+ { name: "阿里云 npmmirror", url: "https://registry.npmmirror.com" },
36
+ { name: "腾讯云", url: "https://mirrors.cloud.tencent.com/npm/" },
37
+ { name: "华为云", url: "https://repo.huaweicloud.com/repository/npm/" },
38
+ { name: "中科大 USTC", url: "http://mirrors.ustc.edu.cn/npm/" },
39
+ { name: "清华 TUNA", url: "https://mirrors.tuna.tsinghua.edu.cn/npm/" },
40
+ { name: "网易", url: "https://mirrors.163.com/npm/" },
41
+ ];
42
+
33
43
  function parseArgs(argv) {
34
44
  const [rawCommand = "setup", ...rest] = argv;
35
45
  const command = rawCommand.startsWith("--") ? "setup" : rawCommand;
@@ -106,11 +116,34 @@ function commandExists(command) {
106
116
  }
107
117
 
108
118
  function installOpenCode(registry) {
109
- const args = ["install", "-g", "opencode-ai"];
119
+ const args = ["install", "-g", "opencode-ai", "--foreground-scripts"];
110
120
  if (registry) args.push("--registry", registry);
111
121
  step(`Installing OpenCode with npm${registry ? ` using ${registry}` : ""}...`);
112
122
  const result = spawnSync("npm", args, { stdio: "inherit" });
113
- if (result.status !== 0) throw new Error("OpenCode installation failed. Please install it manually and retry.");
123
+ return result.status === 0;
124
+ }
125
+
126
+ function npmInstallOpenCodeCommand(registry) {
127
+ const command = ["npm", "install", "-g", "opencode-ai", "--foreground-scripts"];
128
+ if (registry) command.push("--registry", registry);
129
+ return command.join(" ");
130
+ }
131
+
132
+ function pathRefreshHint() {
133
+ return process.platform === "win32"
134
+ ? "If installation succeeded but opencode is still not found, close and reopen PowerShell."
135
+ : "If installation succeeded but opencode is still not found, reopen your shell or check npm global bin path.";
136
+ }
137
+
138
+ function verifyOpenCode() {
139
+ const result = spawnSync("opencode", ["--version"], {
140
+ encoding: "utf8",
141
+ shell: process.platform === "win32",
142
+ stdio: ["ignore", "pipe", "pipe"],
143
+ });
144
+
145
+ if (result.status !== 0) return null;
146
+ return (result.stdout || result.stderr || "").trim();
114
147
  }
115
148
 
116
149
  function step(message) {
@@ -125,6 +158,112 @@ function warn(message) {
125
158
  console.log(`${color.yellow("!")} ${message}`);
126
159
  }
127
160
 
161
+ function formatLatency(ms) {
162
+ if (!Number.isFinite(ms)) return "不可用";
163
+ return `${Math.round(ms)}ms`;
164
+ }
165
+
166
+ async function measureMirror(mirror) {
167
+ const started = Date.now();
168
+ const controller = new AbortController();
169
+ const timeout = setTimeout(() => controller.abort(), 5000);
170
+
171
+ try {
172
+ const response = await fetch(`${mirror.url.replace(/\/+$/, "")}/opencode-ai`, {
173
+ method: "HEAD",
174
+ signal: controller.signal,
175
+ });
176
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
177
+ return { ...mirror, latency: Date.now() - started, ok: true };
178
+ } catch {
179
+ return { ...mirror, latency: Infinity, ok: false };
180
+ } finally {
181
+ clearTimeout(timeout);
182
+ }
183
+ }
184
+
185
+ async function measureMirrors() {
186
+ step("正在测速 npm 镜像...");
187
+ const results = await Promise.all(NPM_MIRRORS.map(measureMirror));
188
+ return results.sort((left, right) => left.latency - right.latency);
189
+ }
190
+
191
+ function renderMirrorChoices(mirrors, selected) {
192
+ output.write("\x1b[2J\x1b[0f");
193
+ console.log(color.blue("选择 npm 镜像源"));
194
+ console.log(color.dim("上下键选择,回车确认。\n"));
195
+
196
+ mirrors.forEach((mirror, index) => {
197
+ const pointer = index === selected ? color.green("›") : " ";
198
+ const latency = mirror.ok ? color.green(formatLatency(mirror.latency)) : color.red("不可用");
199
+ console.log(`${pointer} ${mirror.name.padEnd(16)} ${latency.padEnd(useColor ? 14 : 8)} ${color.dim(mirror.url)}`);
200
+ });
201
+ }
202
+
203
+ async function selectMirrorByNumber(mirrors) {
204
+ console.log(color.blue("选择 npm 镜像源"));
205
+ console.log(color.dim("输入序号并回车。\n"));
206
+
207
+ mirrors.forEach((mirror, index) => {
208
+ const latency = mirror.ok ? color.green(formatLatency(mirror.latency)) : color.red("不可用");
209
+ console.log(`${index + 1}. ${mirror.name} ${latency} ${color.dim(mirror.url)}`);
210
+ });
211
+
212
+ while (true) {
213
+ const answer = await prompt("请选择镜像序号: ");
214
+ const selected = Number.parseInt(answer, 10) - 1;
215
+ if (mirrors[selected]?.ok) return mirrors[selected];
216
+ warn("请输入可用镜像的序号。");
217
+ }
218
+ }
219
+
220
+ async function selectMirror(mirrors) {
221
+ const available = mirrors.filter((mirror) => mirror.ok);
222
+ if (available.length === 0) throw new Error("No npm mirror is reachable. Please retry later or use the default npm registry.");
223
+ if (!input.isTTY || !output.isTTY) return available[0];
224
+ if (process.platform === "win32" || typeof input.setRawMode !== "function") return await selectMirrorByNumber(available);
225
+
226
+ let selected = 0;
227
+ emitKeypressEvents(input);
228
+ input.resume();
229
+ input.setRawMode(true);
230
+ renderMirrorChoices(available, selected);
231
+
232
+ return await new Promise((resolve) => {
233
+ const onKeypress = (_text, key) => {
234
+ if (key.name === "up") selected = (selected - 1 + available.length) % available.length;
235
+ else if (key.name === "down") selected = (selected + 1) % available.length;
236
+ else if (key.name === "return") {
237
+ input.setRawMode(false);
238
+ input.pause();
239
+ input.off("keypress", onKeypress);
240
+ console.log(`\nSelected: ${available[selected].name} (${available[selected].url})`);
241
+ resolve(available[selected]);
242
+ return;
243
+ } else if (key.ctrl && key.name === "c") {
244
+ input.setRawMode(false);
245
+ input.pause();
246
+ process.exit(130);
247
+ }
248
+ renderMirrorChoices(available, selected);
249
+ };
250
+
251
+ input.on("keypress", onKeypress);
252
+ });
253
+ }
254
+
255
+ async function chooseNpmRegistry(flags) {
256
+ if (flags["npm-registry"]) return flags["npm-registry"];
257
+ if (!input.isTTY) return undefined;
258
+
259
+ const useMirror = await confirm("是否需要国内镜像(国内优化)?", false);
260
+ if (!useMirror) return undefined;
261
+
262
+ const mirrors = await measureMirrors();
263
+ const mirror = await selectMirror(mirrors);
264
+ return mirror.url;
265
+ }
266
+
128
267
  async function prompt(question) {
129
268
  if (!input.isTTY) return "";
130
269
  const rl = createInterface({ input, output });
@@ -348,13 +487,22 @@ async function setup(flags) {
348
487
  return;
349
488
  }
350
489
 
351
- if (!flags["npm-registry"] && input.isTTY) {
352
- const useMirror = await confirm("Use China npm mirror for faster download?", false);
353
- if (useMirror) flags["npm-registry"] = "https://registry.npmmirror.com";
490
+ flags["npm-registry"] = await chooseNpmRegistry(flags);
491
+
492
+ const installed = installOpenCode(flags["npm-registry"]);
493
+ if (installed) {
494
+ success("OpenCode installed.");
495
+ const version = verifyOpenCode();
496
+ if (version) success(`OpenCode verified: ${version}`);
497
+ else {
498
+ warn(pathRefreshHint());
499
+ throw new Error("OpenCode was installed but could not be verified in the current shell.");
500
+ }
501
+ } else {
502
+ warn("OpenCode installation failed. SayCoder setup cannot continue until OpenCode is installed.");
503
+ warn(`Manual install command: ${npmInstallOpenCodeCommand(flags["npm-registry"])}`);
504
+ throw new Error("Please install OpenCode manually, reopen PowerShell if needed, then rerun `npx saycoder`.");
354
505
  }
355
-
356
- installOpenCode(flags["npm-registry"]);
357
- success("OpenCode installed.");
358
506
  } else if (!flags["skip-install-check"]) {
359
507
  success("OpenCode is installed.");
360
508
  }
@@ -402,12 +550,13 @@ async function setup(flags) {
402
550
  console.log(color.green("\n模型选择中意模型,即可开始编程了!\n"));
403
551
  }
404
552
 
405
- const flags = parseArgs(process.argv.slice(2));
406
-
407
- try {
553
+ async function main() {
554
+ const flags = parseArgs(process.argv.slice(2));
408
555
  if (flags.command === "setup") await setup(flags);
409
556
  else usage();
410
- } catch (error) {
557
+ }
558
+
559
+ main().catch((error) => {
411
560
  console.error(color.red(`saycoder: ${error.message}`));
412
561
  process.exitCode = 1;
413
- }
562
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saycoder",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "SayCoder setup helper for OpenCode models, MCP servers, and skills.",
5
5
  "type": "module",
6
6
  "bin": {