saycoder 0.1.2 → 0.1.4

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,7 +5,7 @@ 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`.
8
+ 2. If missing, asks whether to install it with `npm install -g opencode-ai --foreground-scripts --verbose --loglevel verbose`.
9
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.
@@ -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. If OpenCode installation or verification fails, setup stops and prints the manual install command.
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,41 @@ 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, { stdio: "inherit" });
123
- return result.status === 0;
141
+ const result = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", args, {
142
+ encoding: "utf8",
143
+ stdio: ["ignore", "pipe", "pipe"],
144
+ });
145
+
146
+ if (result.stdout) output.write(result.stdout);
147
+ if (result.stderr) process.stderr.write(result.stderr);
148
+
149
+ return {
150
+ ok: result.status === 0,
151
+ status: result.status,
152
+ signal: result.signal,
153
+ error: result.error,
154
+ output: `${result.stdout || ""}\n${result.stderr || ""}`,
155
+ };
124
156
  }
125
157
 
126
158
  function npmInstallOpenCodeCommand(registry) {
127
- const command = ["npm", "install", "-g", "opencode-ai", "--foreground-scripts"];
159
+ const command = ["npm", "install", "-g", "opencode-ai", "--foreground-scripts", "--verbose", "--loglevel", "verbose"];
128
160
  if (registry) command.push("--registry", registry);
129
- return command.join(" ");
161
+ return command.map(shellQuote).join(" ");
130
162
  }
131
163
 
132
164
  function pathRefreshHint() {
@@ -136,14 +168,18 @@ function pathRefreshHint() {
136
168
  }
137
169
 
138
170
  function verifyOpenCode() {
139
- const result = spawnSync("opencode", ["--version"], {
140
- encoding: "utf8",
141
- shell: process.platform === "win32",
142
- stdio: ["ignore", "pipe", "pipe"],
143
- });
171
+ const commands = process.platform === "win32" ? ["opencode.cmd", "opencode.exe", "opencode"] : ["opencode"];
144
172
 
145
- if (result.status !== 0) return null;
146
- return (result.stdout || result.stderr || "").trim();
173
+ for (const command of commands) {
174
+ const result = spawnSync(command, ["--version"], {
175
+ encoding: "utf8",
176
+ stdio: ["ignore", "pipe", "pipe"],
177
+ });
178
+
179
+ if (result.status === 0) return (result.stdout || result.stderr || "").trim();
180
+ }
181
+
182
+ return null;
147
183
  }
148
184
 
149
185
  function step(message) {
@@ -169,10 +205,11 @@ async function measureMirror(mirror) {
169
205
  const timeout = setTimeout(() => controller.abort(), 5000);
170
206
 
171
207
  try {
172
- const response = await fetch(`${mirror.url.replace(/\/+$/, "")}/opencode-ai`, {
173
- method: "HEAD",
174
- signal: controller.signal,
175
- });
208
+ const url = `${mirror.url.replace(/\/+$/, "")}/opencode-ai`;
209
+ let response = await fetch(url, { method: "HEAD", signal: controller.signal });
210
+ if (response.status === 405 || response.status === 403) {
211
+ response = await fetch(url, { method: "GET", signal: controller.signal });
212
+ }
176
213
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
177
214
  return { ...mirror, latency: Date.now() - started, ok: true };
178
215
  } catch {
@@ -489,8 +526,8 @@ async function setup(flags) {
489
526
 
490
527
  flags["npm-registry"] = await chooseNpmRegistry(flags);
491
528
 
492
- const installed = installOpenCode(flags["npm-registry"]);
493
- if (installed) {
529
+ const installResult = installOpenCode(flags["npm-registry"]);
530
+ if (installResult.ok) {
494
531
  success("OpenCode installed.");
495
532
  const version = verifyOpenCode();
496
533
  if (version) success(`OpenCode verified: ${version}`);
@@ -500,8 +537,14 @@ async function setup(flags) {
500
537
  }
501
538
  } else {
502
539
  warn("OpenCode installation failed. SayCoder setup cannot continue until OpenCode is installed.");
540
+ warn(`npm exit code: ${installResult.status ?? "unknown"}${installResult.signal ? `, signal: ${installResult.signal}` : ""}`);
541
+ if (installResult.error) warn(`npm spawn error: ${installResult.error.message}`);
542
+ const debugLogs = findNpmDebugLogs(installResult.output);
543
+ if (debugLogs.length > 0) warn(`npm debug log: ${debugLogs[0]}`);
544
+ else warn("npm did not print a debug log path. Check the latest npm debug log under your npm cache/_logs directory.");
503
545
  warn(`Manual install command: ${npmInstallOpenCodeCommand(flags["npm-registry"])}`);
504
- throw new Error("Please install OpenCode manually, reopen PowerShell if needed, then rerun `npx saycoder`.");
546
+ 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.");
547
+ throw new Error("OpenCode installation failed; config was not written. Fix npm/OpenCode install, then rerun `npx saycoder`.");
505
548
  }
506
549
  } else if (!flags["skip-install-check"]) {
507
550
  success("OpenCode is installed.");
@@ -537,7 +580,7 @@ async function setup(flags) {
537
580
  const skillPaths = installSkills(configDir, Boolean(flags["dry-run"]));
538
581
 
539
582
  if (flags["dry-run"]) {
540
- console.log(JSON.stringify(merged, null, 2));
583
+ console.log(JSON.stringify(redactSecrets(merged), null, 2));
541
584
  console.log(`\nDry run only. Skills would be installed to:\n${skillPaths.map((item) => `- ${item}`).join("\n")}`);
542
585
  return;
543
586
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saycoder",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "SayCoder setup helper for OpenCode models, MCP servers, and skills.",
5
5
  "type": "module",
6
6
  "bin": {