saycoder 0.1.1 → 0.1.3
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 +17 -3
- package/bin/saycoder.js +80 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@ 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 --foreground-scripts`.
|
|
9
|
-
3. Optionally enables China mirror optimization, measures available mirrors, and lets the user choose with arrow keys.
|
|
8
|
+
2. If missing, asks whether to install it with `npm install -g opencode-ai --foreground-scripts --verbose --loglevel verbose`.
|
|
9
|
+
3. Optionally enables China mirror optimization, measures available mirrors, and lets the user choose with arrow keys or number input on Windows.
|
|
10
10
|
4. Installs OpenCode and verifies `opencode --version` before continuing.
|
|
11
11
|
5. Prompts the user for a SayCoder API key.
|
|
12
12
|
6. Validates the key with `GET https://jstapi.xinbai.icu/v1/models`.
|
|
@@ -25,13 +25,25 @@ SayCoder's base URL is only used for the OpenAI-compatible model channel, not fo
|
|
|
25
25
|
|
|
26
26
|
## China mirror
|
|
27
27
|
|
|
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.
|
|
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, or number input on Windows/non-TTY terminals. Mirror probes use the package metadata endpoint and fall back from `HEAD` to `GET` for registries that reject `HEAD`.
|
|
29
|
+
|
|
30
|
+
If OpenCode installation or verification fails, setup stops before writing OpenCode config and prints the npm exit code, any npm spawn error, a detected npm debug log path when npm reports one, and the manual install command.
|
|
29
31
|
You can also force it with:
|
|
30
32
|
|
|
31
33
|
```bash
|
|
32
34
|
node bin/saycoder.js setup --npm-registry https://registry.npmmirror.com
|
|
33
35
|
```
|
|
34
36
|
|
|
37
|
+
## Windows PowerShell troubleshooting
|
|
38
|
+
|
|
39
|
+
If automatic `opencode-ai` installation fails on Windows, rerun the printed command in a fresh PowerShell window as the same user. For example:
|
|
40
|
+
|
|
41
|
+
```powershell
|
|
42
|
+
npm install -g opencode-ai --foreground-scripts --verbose --loglevel verbose --registry https://registry.npmmirror.com
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then check the npm debug log shown by `saycoder` or the newest log under your npm cache `_logs` directory. After a successful manual install, close and reopen PowerShell so the global npm bin path is refreshed, verify with `opencode --version`, then run `npx saycoder` again.
|
|
46
|
+
|
|
35
47
|
## Installed skills
|
|
36
48
|
|
|
37
49
|
- `saycoder-lite-superpowers`: clarify user intent, split logical goals, implement production code, and report completed/not completed work.
|
|
@@ -44,6 +56,8 @@ node bin/saycoder.js setup --npm-registry https://registry.npmmirror.com
|
|
|
44
56
|
node bin/saycoder.js setup --dry-run --skip-install-check --skip-validate --api-key test-key
|
|
45
57
|
```
|
|
46
58
|
|
|
59
|
+
Dry-run output redacts API keys and other token-like fields.
|
|
60
|
+
|
|
47
61
|
## Intended usage
|
|
48
62
|
|
|
49
63
|
```bash
|
package/bin/saycoder.js
CHANGED
|
@@ -103,6 +103,15 @@ function sortObject(value) {
|
|
|
103
103
|
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, sortObject(nested)]));
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
function redactSecrets(value) {
|
|
107
|
+
if (Array.isArray(value)) return value.map(redactSecrets);
|
|
108
|
+
if (!value || typeof value !== "object") return value;
|
|
109
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => {
|
|
110
|
+
if (/api[-_]?key|authorization|token|secret/i.test(key)) return [key, "<redacted>"];
|
|
111
|
+
return [key, redactSecrets(nested)];
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
106
115
|
function unique(values) {
|
|
107
116
|
return [...new Set(values.filter(Boolean))];
|
|
108
117
|
}
|
|
@@ -115,18 +124,42 @@ function commandExists(command) {
|
|
|
115
124
|
return result.status === 0;
|
|
116
125
|
}
|
|
117
126
|
|
|
127
|
+
function shellQuote(value) {
|
|
128
|
+
if (/^[A-Za-z0-9_/:=@.%+-]+$/.test(value)) return value;
|
|
129
|
+
return JSON.stringify(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function findNpmDebugLogs(outputText) {
|
|
133
|
+
const matches = outputText.match(/(?:[A-Z]:)?[^\r\n]*npm[^\r\n]*(?:debug|eresolve-report)[^\r\n]*\.log/gi) || [];
|
|
134
|
+
return unique(matches.map((item) => item.trim().replace(/^['"]|['"]$/g, "")));
|
|
135
|
+
}
|
|
136
|
+
|
|
118
137
|
function installOpenCode(registry) {
|
|
119
|
-
const args = ["install", "-g", "opencode-ai", "--foreground-scripts"];
|
|
138
|
+
const args = ["install", "-g", "opencode-ai", "--foreground-scripts", "--verbose", "--loglevel", "verbose"];
|
|
120
139
|
if (registry) args.push("--registry", registry);
|
|
121
140
|
step(`Installing OpenCode with npm${registry ? ` using ${registry}` : ""}...`);
|
|
122
|
-
const result = spawnSync("npm", args, {
|
|
123
|
-
|
|
141
|
+
const result = spawnSync("npm", args, {
|
|
142
|
+
encoding: "utf8",
|
|
143
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
144
|
+
shell: process.platform === "win32",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (result.stdout) output.write(result.stdout);
|
|
148
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
ok: result.status === 0,
|
|
152
|
+
status: result.status,
|
|
153
|
+
signal: result.signal,
|
|
154
|
+
error: result.error,
|
|
155
|
+
output: `${result.stdout || ""}\n${result.stderr || ""}`,
|
|
156
|
+
};
|
|
124
157
|
}
|
|
125
158
|
|
|
126
159
|
function npmInstallOpenCodeCommand(registry) {
|
|
127
|
-
const command = ["npm", "install", "-g", "opencode-ai", "--foreground-scripts"];
|
|
160
|
+
const command = ["npm", "install", "-g", "opencode-ai", "--foreground-scripts", "--verbose", "--loglevel", "verbose"];
|
|
128
161
|
if (registry) command.push("--registry", registry);
|
|
129
|
-
return command.join(" ");
|
|
162
|
+
return command.map(shellQuote).join(" ");
|
|
130
163
|
}
|
|
131
164
|
|
|
132
165
|
function pathRefreshHint() {
|
|
@@ -169,10 +202,11 @@ async function measureMirror(mirror) {
|
|
|
169
202
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
170
203
|
|
|
171
204
|
try {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
205
|
+
const url = `${mirror.url.replace(/\/+$/, "")}/opencode-ai`;
|
|
206
|
+
let response = await fetch(url, { method: "HEAD", signal: controller.signal });
|
|
207
|
+
if (response.status === 405 || response.status === 403) {
|
|
208
|
+
response = await fetch(url, { method: "GET", signal: controller.signal });
|
|
209
|
+
}
|
|
176
210
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
177
211
|
return { ...mirror, latency: Date.now() - started, ok: true };
|
|
178
212
|
} catch {
|
|
@@ -200,13 +234,32 @@ function renderMirrorChoices(mirrors, selected) {
|
|
|
200
234
|
});
|
|
201
235
|
}
|
|
202
236
|
|
|
237
|
+
async function selectMirrorByNumber(mirrors) {
|
|
238
|
+
console.log(color.blue("选择 npm 镜像源"));
|
|
239
|
+
console.log(color.dim("输入序号并回车。\n"));
|
|
240
|
+
|
|
241
|
+
mirrors.forEach((mirror, index) => {
|
|
242
|
+
const latency = mirror.ok ? color.green(formatLatency(mirror.latency)) : color.red("不可用");
|
|
243
|
+
console.log(`${index + 1}. ${mirror.name} ${latency} ${color.dim(mirror.url)}`);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
while (true) {
|
|
247
|
+
const answer = await prompt("请选择镜像序号: ");
|
|
248
|
+
const selected = Number.parseInt(answer, 10) - 1;
|
|
249
|
+
if (mirrors[selected]?.ok) return mirrors[selected];
|
|
250
|
+
warn("请输入可用镜像的序号。");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
203
254
|
async function selectMirror(mirrors) {
|
|
204
255
|
const available = mirrors.filter((mirror) => mirror.ok);
|
|
205
256
|
if (available.length === 0) throw new Error("No npm mirror is reachable. Please retry later or use the default npm registry.");
|
|
206
257
|
if (!input.isTTY || !output.isTTY) return available[0];
|
|
258
|
+
if (process.platform === "win32" || typeof input.setRawMode !== "function") return await selectMirrorByNumber(available);
|
|
207
259
|
|
|
208
260
|
let selected = 0;
|
|
209
261
|
emitKeypressEvents(input);
|
|
262
|
+
input.resume();
|
|
210
263
|
input.setRawMode(true);
|
|
211
264
|
renderMirrorChoices(available, selected);
|
|
212
265
|
|
|
@@ -216,12 +269,14 @@ async function selectMirror(mirrors) {
|
|
|
216
269
|
else if (key.name === "down") selected = (selected + 1) % available.length;
|
|
217
270
|
else if (key.name === "return") {
|
|
218
271
|
input.setRawMode(false);
|
|
272
|
+
input.pause();
|
|
219
273
|
input.off("keypress", onKeypress);
|
|
220
274
|
console.log(`\nSelected: ${available[selected].name} (${available[selected].url})`);
|
|
221
275
|
resolve(available[selected]);
|
|
222
276
|
return;
|
|
223
277
|
} else if (key.ctrl && key.name === "c") {
|
|
224
278
|
input.setRawMode(false);
|
|
279
|
+
input.pause();
|
|
225
280
|
process.exit(130);
|
|
226
281
|
}
|
|
227
282
|
renderMirrorChoices(available, selected);
|
|
@@ -468,8 +523,8 @@ async function setup(flags) {
|
|
|
468
523
|
|
|
469
524
|
flags["npm-registry"] = await chooseNpmRegistry(flags);
|
|
470
525
|
|
|
471
|
-
const
|
|
472
|
-
if (
|
|
526
|
+
const installResult = installOpenCode(flags["npm-registry"]);
|
|
527
|
+
if (installResult.ok) {
|
|
473
528
|
success("OpenCode installed.");
|
|
474
529
|
const version = verifyOpenCode();
|
|
475
530
|
if (version) success(`OpenCode verified: ${version}`);
|
|
@@ -479,8 +534,14 @@ async function setup(flags) {
|
|
|
479
534
|
}
|
|
480
535
|
} else {
|
|
481
536
|
warn("OpenCode installation failed. SayCoder setup cannot continue until OpenCode is installed.");
|
|
537
|
+
warn(`npm exit code: ${installResult.status ?? "unknown"}${installResult.signal ? `, signal: ${installResult.signal}` : ""}`);
|
|
538
|
+
if (installResult.error) warn(`npm spawn error: ${installResult.error.message}`);
|
|
539
|
+
const debugLogs = findNpmDebugLogs(installResult.output);
|
|
540
|
+
if (debugLogs.length > 0) warn(`npm debug log: ${debugLogs[0]}`);
|
|
541
|
+
else warn("npm did not print a debug log path. Check the latest npm debug log under your npm cache/_logs directory.");
|
|
482
542
|
warn(`Manual install command: ${npmInstallOpenCodeCommand(flags["npm-registry"])}`);
|
|
483
|
-
|
|
543
|
+
if (process.platform === "win32") warn("On Windows, run the manual command in a fresh PowerShell as the same user; if it succeeds, close and reopen PowerShell so PATH refreshes.");
|
|
544
|
+
throw new Error("OpenCode installation failed; config was not written. Fix npm/OpenCode install, then rerun `npx saycoder`.");
|
|
484
545
|
}
|
|
485
546
|
} else if (!flags["skip-install-check"]) {
|
|
486
547
|
success("OpenCode is installed.");
|
|
@@ -516,7 +577,7 @@ async function setup(flags) {
|
|
|
516
577
|
const skillPaths = installSkills(configDir, Boolean(flags["dry-run"]));
|
|
517
578
|
|
|
518
579
|
if (flags["dry-run"]) {
|
|
519
|
-
console.log(JSON.stringify(merged, null, 2));
|
|
580
|
+
console.log(JSON.stringify(redactSecrets(merged), null, 2));
|
|
520
581
|
console.log(`\nDry run only. Skills would be installed to:\n${skillPaths.map((item) => `- ${item}`).join("\n")}`);
|
|
521
582
|
return;
|
|
522
583
|
}
|
|
@@ -529,12 +590,13 @@ async function setup(flags) {
|
|
|
529
590
|
console.log(color.green("\n模型选择中意模型,即可开始编程了!\n"));
|
|
530
591
|
}
|
|
531
592
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
try {
|
|
593
|
+
async function main() {
|
|
594
|
+
const flags = parseArgs(process.argv.slice(2));
|
|
535
595
|
if (flags.command === "setup") await setup(flags);
|
|
536
596
|
else usage();
|
|
537
|
-
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
main().catch((error) => {
|
|
538
600
|
console.error(color.red(`saycoder: ${error.message}`));
|
|
539
601
|
process.exitCode = 1;
|
|
540
|
-
}
|
|
602
|
+
});
|