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 +9 -7
- package/bin/saycoder.js +135 -8
- 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.
|
|
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,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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
}
|