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 +9 -7
- package/bin/saycoder.js +162 -13
- package/package.json +1 -1
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.
|
|
10
|
-
4.
|
|
11
|
-
5.
|
|
12
|
-
6.
|
|
13
|
-
7.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
main().catch((error) => {
|
|
411
560
|
console.error(color.red(`saycoder: ${error.message}`));
|
|
412
561
|
process.exitCode = 1;
|
|
413
|
-
}
|
|
562
|
+
});
|