saycoder 0.1.0 → 0.1.1

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.
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,91 @@ 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 selectMirror(mirrors) {
204
+ const available = mirrors.filter((mirror) => mirror.ok);
205
+ if (available.length === 0) throw new Error("No npm mirror is reachable. Please retry later or use the default npm registry.");
206
+ if (!input.isTTY || !output.isTTY) return available[0];
207
+
208
+ let selected = 0;
209
+ emitKeypressEvents(input);
210
+ input.setRawMode(true);
211
+ renderMirrorChoices(available, selected);
212
+
213
+ return await new Promise((resolve) => {
214
+ const onKeypress = (_text, key) => {
215
+ if (key.name === "up") selected = (selected - 1 + available.length) % available.length;
216
+ else if (key.name === "down") selected = (selected + 1) % available.length;
217
+ else if (key.name === "return") {
218
+ input.setRawMode(false);
219
+ input.off("keypress", onKeypress);
220
+ console.log(`\nSelected: ${available[selected].name} (${available[selected].url})`);
221
+ resolve(available[selected]);
222
+ return;
223
+ } else if (key.ctrl && key.name === "c") {
224
+ input.setRawMode(false);
225
+ process.exit(130);
226
+ }
227
+ renderMirrorChoices(available, selected);
228
+ };
229
+
230
+ input.on("keypress", onKeypress);
231
+ });
232
+ }
233
+
234
+ async function chooseNpmRegistry(flags) {
235
+ if (flags["npm-registry"]) return flags["npm-registry"];
236
+ if (!input.isTTY) return undefined;
237
+
238
+ const useMirror = await confirm("是否需要国内镜像(国内优化)?", false);
239
+ if (!useMirror) return undefined;
240
+
241
+ const mirrors = await measureMirrors();
242
+ const mirror = await selectMirror(mirrors);
243
+ return mirror.url;
244
+ }
245
+
128
246
  async function prompt(question) {
129
247
  if (!input.isTTY) return "";
130
248
  const rl = createInterface({ input, output });
@@ -348,13 +466,22 @@ async function setup(flags) {
348
466
  return;
349
467
  }
350
468
 
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";
469
+ flags["npm-registry"] = await chooseNpmRegistry(flags);
470
+
471
+ const installed = installOpenCode(flags["npm-registry"]);
472
+ if (installed) {
473
+ success("OpenCode installed.");
474
+ const version = verifyOpenCode();
475
+ if (version) success(`OpenCode verified: ${version}`);
476
+ else {
477
+ warn(pathRefreshHint());
478
+ throw new Error("OpenCode was installed but could not be verified in the current shell.");
479
+ }
480
+ } else {
481
+ warn("OpenCode installation failed. SayCoder setup cannot continue until OpenCode is installed.");
482
+ warn(`Manual install command: ${npmInstallOpenCodeCommand(flags["npm-registry"])}`);
483
+ throw new Error("Please install OpenCode manually, reopen PowerShell if needed, then rerun `npx saycoder`.");
354
484
  }
355
-
356
- installOpenCode(flags["npm-registry"]);
357
- success("OpenCode installed.");
358
485
  } else if (!flags["skip-install-check"]) {
359
486
  success("OpenCode is installed.");
360
487
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saycoder",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "SayCoder setup helper for OpenCode models, MCP servers, and skills.",
5
5
  "type": "module",
6
6
  "bin": {