@sns-myagent/cli 0.3.0 → 0.3.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/bin/.gitkeep +0 -0
- package/bin/snscoder.js +9 -0
- package/dist/cli.js +22026 -0
- package/package.json +2 -1
- package/scripts/fetch-binary.mjs +106 -217
- package/scripts/smoke-test.sh +92 -0
- package/src/agents/ensemble.ts +47 -41
- package/src/agents/strategies/consensus.ts +9 -10
- package/src/agents/strategies/index.ts +12 -0
- package/src/cli/index.ts +5 -1
- package/src/internal-urls/docs-index.generated.txt +0 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sns-myagent/cli",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"description": "SNS-MyAgent — Pi Agent size, full features. BYOK coding agent CLI.",
|
|
6
6
|
"homepage": "https://github.com/Reihantt6/sns-myagent",
|
|
7
7
|
"author": "Reihantt6",
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
},
|
|
107
107
|
"files": [
|
|
108
108
|
"src",
|
|
109
|
+
"bin",
|
|
109
110
|
"dist/cli.js",
|
|
110
111
|
"dist/*.node",
|
|
111
112
|
"scripts",
|
package/scripts/fetch-binary.mjs
CHANGED
|
@@ -1,34 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// fetch-binary.mjs — Node.js postinstall for @sns-myagent/cli
|
|
2
|
+
// fetch-binary.mjs — Node.js postinstall for @sns-myagent/cli
|
|
3
3
|
//
|
|
4
4
|
// Downloads the prebuilt `snscoder` binary from the latest GitHub release
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// into <prefix>/bin/, then symlinks the active one to <prefix>/bin/snscoder
|
|
6
|
+
// so `npm install -g @sns-myagent/cli` puts a working `snscoder` on PATH.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// (honors GITHUB_TOKEN env var for 60-req/hr anonymous → 5000-req/hr auth).
|
|
15
|
-
// 4. Find the matching asset, download its zip, and unpack into bin/.
|
|
16
|
-
// Unix uses `unzip -o`; Windows uses `Expand-Archive` via PowerShell.
|
|
17
|
-
// 5. chmod 755 the resulting binary on unix.
|
|
18
|
-
// 6. On any failure path (no release, missing asset, network down) print a
|
|
19
|
-
// friendly warning and exit 0 — we must NOT break `npm install`.
|
|
8
|
+
// Asset layout matches what .github/workflows/build-release.yml produces:
|
|
9
|
+
// snscoder-linux-x64 (raw, not zipped)
|
|
10
|
+
// snscoder-linux-arm64
|
|
11
|
+
// snscoder-darwin-x64
|
|
12
|
+
// snscoder-darwin-arm64
|
|
13
|
+
// snscoder-windows-x64.exe
|
|
20
14
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// snscoder-linux-x64-musl.zip (musl/alpine)
|
|
24
|
-
// snscoder-linux-arm64.zip
|
|
25
|
-
// snscoder-darwin-x64.zip
|
|
26
|
-
// snscoder-darwin-arm64.zip
|
|
27
|
-
// snscoder-windows-x64.zip
|
|
28
|
-
// Each archive contains a single file named `snscoder` (or `snscoder.exe`).
|
|
15
|
+
// Falls back to musl variant on Linux if glibc asset unavailable.
|
|
16
|
+
// Never breaks `npm install` — on any failure prints warning + exits 0.
|
|
29
17
|
|
|
30
18
|
import { execFileSync } from "node:child_process";
|
|
31
|
-
import { createWriteStream, existsSync, mkdirSync } from "node:fs";
|
|
19
|
+
import { createWriteStream, existsSync, lstatSync, mkdirSync, symlinkSync, unlinkSync } from "node:fs";
|
|
32
20
|
import { chmod, mkdtemp, readdir, rm, stat } from "node:fs/promises";
|
|
33
21
|
import { tmpdir } from "node:os";
|
|
34
22
|
import { join, dirname, resolve } from "node:path";
|
|
@@ -37,147 +25,89 @@ import https from "node:https";
|
|
|
37
25
|
|
|
38
26
|
const REPO = "Reihantt6/sns-myagent";
|
|
39
27
|
const RELEASE_API = `https://api.github.com/repos/${REPO}/releases/latest`;
|
|
40
|
-
const UA = "sns-myagent-fetch-binary/0.
|
|
28
|
+
const UA = "sns-myagent-fetch-binary/0.2.0 (npm postinstall)";
|
|
41
29
|
|
|
42
|
-
// Resolve repo root: this script lives at <repo>/scripts/fetch-binary.mjs
|
|
43
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
44
31
|
const __dirname = dirname(__filename);
|
|
32
|
+
// npm install -g layout: <prefix>/lib/node_modules/@sns-myagent/cli/scripts/fetch-binary.mjs
|
|
33
|
+
// <prefix>/lib/node_modules/@sns-myagent/cli/node_modules/.bin/snscoder (npm symlinks bin)
|
|
34
|
+
// We want to download into <prefix>/lib/node_modules/@sns-myagent/cli/bin/
|
|
35
|
+
// so npm auto-symlinks it into <prefix>/bin/snscoder.
|
|
45
36
|
const REPO_ROOT = resolve(__dirname, "..");
|
|
46
37
|
const BIN_DIR = join(REPO_ROOT, "bin");
|
|
47
38
|
|
|
48
39
|
const isWin = process.platform === "win32";
|
|
49
40
|
|
|
50
|
-
// --- Pretty logging ---------------------------------------------------------
|
|
51
|
-
|
|
52
41
|
const c = {
|
|
53
42
|
reset: "\u001b[0m",
|
|
54
|
-
dim: "\u001b[2m",
|
|
55
43
|
red: "\u001b[31m",
|
|
56
44
|
green: "\u001b[32m",
|
|
57
45
|
yellow: "\u001b[33m",
|
|
58
46
|
cyan: "\u001b[36m",
|
|
59
|
-
bold: "\u001b[1m",
|
|
60
47
|
};
|
|
48
|
+
const info = (m) => process.stdout.write(`${c.cyan}info${c.reset} ${m}\n`);
|
|
49
|
+
const warn = (m) => process.stdout.write(`${c.yellow}warn${c.reset} ${m}\n`);
|
|
50
|
+
const err = (m) => process.stdout.write(`${c.red}error${c.reset} ${m}\n`);
|
|
51
|
+
const ok = (m) => process.stdout.write(`${c.green}ok${c.reset} ${m}\n`);
|
|
61
52
|
|
|
62
|
-
function info(msg) {
|
|
63
|
-
process.stdout.write(`${c.cyan}info${c.reset} ${msg}\n`);
|
|
64
|
-
}
|
|
65
|
-
function warn(msg) {
|
|
66
|
-
process.stdout.write(`${c.yellow}warn${c.reset} ${msg}\n`);
|
|
67
|
-
}
|
|
68
|
-
function error(msg) {
|
|
69
|
-
process.stdout.write(`${c.red}error${c.reset} ${msg}\n`);
|
|
70
|
-
}
|
|
71
|
-
function ok(msg) {
|
|
72
|
-
process.stdout.write(`${c.green}ok${c.reset} ${msg}\n`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// --- Platform / asset mapping ----------------------------------------------
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Map (platform, arch, isMusl) → asset filename.
|
|
79
|
-
* Returns null when the platform/arch combo is unsupported.
|
|
80
|
-
*/
|
|
81
53
|
function pickAssetName(platform, arch, isMusl) {
|
|
54
|
+
const x = (a) => (a === "x64" ? "x64" : a === "arm64" ? "arm64" : null);
|
|
82
55
|
if (platform === "linux") {
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
56
|
+
const a = x(arch);
|
|
57
|
+
if (!a) return null;
|
|
58
|
+
// Release ships glibc variant by default. If user is on musl/alpine,
|
|
59
|
+
// try musl fallback (may 404 on releases that don't have it).
|
|
60
|
+
return isMusl ? [`snscoder-linux-${a}-musl`, `snscoder-linux-${a}`] : [`snscoder-linux-${a}`];
|
|
86
61
|
}
|
|
87
62
|
if (platform === "darwin") {
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
return
|
|
63
|
+
const a = x(arch);
|
|
64
|
+
if (!a) return null;
|
|
65
|
+
return [`snscoder-darwin-${a}`];
|
|
91
66
|
}
|
|
92
67
|
if (platform === "win32") {
|
|
93
|
-
|
|
94
|
-
return null;
|
|
68
|
+
return arch === "x64" ? [`snscoder-windows-x64.exe`] : null;
|
|
95
69
|
}
|
|
96
70
|
return null;
|
|
97
71
|
}
|
|
98
72
|
|
|
99
|
-
/**
|
|
100
|
-
* Musl heuristic: run `ldd /proc/self/exe` and look for `libc.so.*` (glibc).
|
|
101
|
-
* - musl systems (Alpine, Void with musl, etc.) print "Not a valid dynamic
|
|
102
|
-
* program" or `ldd: can't find dynamic linker info` AND no `libc.so.6` line.
|
|
103
|
-
* - glibc systems show `libc.so.6 => /lib64/libc.so.6` etc.
|
|
104
|
-
*
|
|
105
|
-
* Heuristic limitations:
|
|
106
|
-
* - Static binaries report "not a dynamic executable" — we conservatively
|
|
107
|
-
* treat that as glibc (more common on developer machines).
|
|
108
|
-
* - BusyBox `ldd` is a shell wrapper; it sometimes prints to stdout but not
|
|
109
|
-
* stderr. We merge both streams before scanning.
|
|
110
|
-
*/
|
|
111
73
|
function detectMuslLinux() {
|
|
112
74
|
if (process.platform !== "linux") return false;
|
|
113
75
|
try {
|
|
114
|
-
const out = execFileSync("ldd", ["/proc/self/exe"], {
|
|
115
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
116
|
-
encoding: "utf8",
|
|
117
|
-
timeout: 3000,
|
|
118
|
-
});
|
|
119
|
-
// glibc leaves a libc.so.6 line; musl leaves musl libc.so line.
|
|
76
|
+
const out = execFileSync("ldd", ["/proc/self/exe"], { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8", timeout: 3000 });
|
|
120
77
|
if (/libc\.so\.6/.test(out)) return false;
|
|
121
78
|
if (/musl/.test(out)) return true;
|
|
122
|
-
|
|
123
|
-
// (Alpine, Void-musl, etc.). Falls back wrong on static binaries
|
|
124
|
-
// — see heuristic note above.
|
|
125
|
-
return true;
|
|
79
|
+
return true; // alpine/void default to musl
|
|
126
80
|
} catch {
|
|
127
|
-
|
|
128
|
-
return false;
|
|
81
|
+
return false; // can't tell — glibc default
|
|
129
82
|
}
|
|
130
83
|
}
|
|
131
84
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* https.get wrapper that returns a Buffer and follows up to 3 redirects
|
|
136
|
-
* (GitHub release redirects to S3, which redirects to Cloudfront).
|
|
137
|
-
*/
|
|
138
|
-
function fetchBuffer(url, redirectsLeft = 3) {
|
|
85
|
+
function fetchBuffer(url, redirectsLeft = 5) {
|
|
139
86
|
return new Promise((resolveP, rejectP) => {
|
|
140
|
-
const headers = {
|
|
141
|
-
|
|
142
|
-
Accept: "application/octet-stream",
|
|
143
|
-
};
|
|
144
|
-
if (process.env.GITHUB_TOKEN) {
|
|
145
|
-
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
146
|
-
}
|
|
147
|
-
|
|
87
|
+
const headers = { "User-Agent": UA, Accept: "application/octet-stream" };
|
|
88
|
+
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
148
89
|
const req = https.get(url, { headers }, (res) => {
|
|
149
90
|
const status = res.statusCode ?? 0;
|
|
150
|
-
// Follow redirects (GitHub release downloads 302 to S3 → Cloudfront)
|
|
151
91
|
if (status >= 300 && status < 400 && res.headers.location) {
|
|
152
92
|
res.resume();
|
|
153
|
-
if (redirectsLeft <= 0)
|
|
154
|
-
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
resolveP(fetchBuffer(res.headers.location, redirectsLeft - 1));
|
|
158
|
-
return;
|
|
93
|
+
if (redirectsLeft <= 0) return rejectP(new Error("too many redirects"));
|
|
94
|
+
return resolveP(fetchBuffer(res.headers.location, redirectsLeft - 1));
|
|
159
95
|
}
|
|
160
96
|
if (status === 404) {
|
|
161
97
|
res.resume();
|
|
162
|
-
rejectP(Object.assign(new Error("not found"), { httpStatus: 404 }));
|
|
163
|
-
return;
|
|
98
|
+
return rejectP(Object.assign(new Error("not found"), { httpStatus: 404 }));
|
|
164
99
|
}
|
|
165
100
|
if (status !== 200) {
|
|
166
101
|
res.resume();
|
|
167
|
-
rejectP(new Error(`HTTP ${status} for ${url}`));
|
|
168
|
-
return;
|
|
102
|
+
return rejectP(new Error(`HTTP ${status} for ${url}`));
|
|
169
103
|
}
|
|
170
|
-
|
|
171
104
|
const chunks = [];
|
|
172
105
|
res.on("data", (c2) => chunks.push(c2));
|
|
173
106
|
res.on("end", () => resolveP(Buffer.concat(chunks)));
|
|
174
107
|
res.on("error", rejectP);
|
|
175
108
|
});
|
|
176
|
-
|
|
177
109
|
req.on("error", rejectP);
|
|
178
|
-
req.setTimeout(
|
|
179
|
-
req.destroy(new Error(`timeout after 30s: ${url}`));
|
|
180
|
-
});
|
|
110
|
+
req.setTimeout(60_000, () => req.destroy(new Error(`timeout 60s: ${url}`)));
|
|
181
111
|
});
|
|
182
112
|
}
|
|
183
113
|
|
|
@@ -188,172 +118,131 @@ function fetchJson(url) {
|
|
|
188
118
|
Accept: "application/vnd.github+json",
|
|
189
119
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
190
120
|
};
|
|
191
|
-
if (process.env.GITHUB_TOKEN) {
|
|
192
|
-
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
193
|
-
}
|
|
194
|
-
|
|
121
|
+
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
195
122
|
const req = https.get(url, { headers }, (res) => {
|
|
196
123
|
const status = res.statusCode ?? 0;
|
|
197
124
|
const chunks = [];
|
|
198
125
|
res.on("data", (c2) => chunks.push(c2));
|
|
199
126
|
res.on("end", () => {
|
|
200
127
|
const body = Buffer.concat(chunks).toString("utf8");
|
|
201
|
-
if (status === 404) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
if (status !== 200) {
|
|
206
|
-
rejectP(new Error(`GitHub API HTTP ${status}: ${body.slice(0, 200)}`));
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
128
|
+
if (status === 404) return rejectP(Object.assign(new Error("release not found"), { httpStatus: 404 }));
|
|
129
|
+
if (status === 403) return rejectP(Object.assign(new Error("rate limited"), { httpStatus: 403 }));
|
|
130
|
+
if (status !== 200) return rejectP(new Error(`GitHub API HTTP ${status}: ${body.slice(0, 200)}`));
|
|
209
131
|
try {
|
|
210
132
|
resolveP(JSON.parse(body));
|
|
211
133
|
} catch (e) {
|
|
212
|
-
rejectP(new Error(`invalid JSON
|
|
134
|
+
rejectP(new Error(`invalid JSON: ${e.message}`));
|
|
213
135
|
}
|
|
214
136
|
});
|
|
215
|
-
res.on("error", rejectP);
|
|
216
137
|
});
|
|
217
|
-
|
|
218
138
|
req.on("error", rejectP);
|
|
219
|
-
req.setTimeout(30_000, () => {
|
|
220
|
-
req.destroy(new Error(`timeout after 30s: ${url}`));
|
|
221
|
-
});
|
|
139
|
+
req.setTimeout(30_000, () => req.destroy(new Error(`timeout 30s: ${url}`)));
|
|
222
140
|
});
|
|
223
141
|
}
|
|
224
142
|
|
|
225
|
-
// --- Zip unpack (delegates to system tools) --------------------------------
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Unzip an archive into destDir. On unix uses `unzip -o`; on Windows uses
|
|
229
|
-
* PowerShell's `Expand-Archive`. Both are preinstalled on the respective
|
|
230
|
-
* platforms (unzip is on virtually every macOS/Linux dev box; Expand-Archive
|
|
231
|
-
* ships with PowerShell 5+).
|
|
232
|
-
*
|
|
233
|
-
* Throws if neither tool is available — caller decides whether that's fatal.
|
|
234
|
-
*/
|
|
235
|
-
function unzip(archivePath, destDir) {
|
|
236
|
-
if (isWin) {
|
|
237
|
-
// PowerShell Expand-Archive needs a non-existent OR empty dest dir.
|
|
238
|
-
execFileSync(
|
|
239
|
-
"powershell.exe",
|
|
240
|
-
[
|
|
241
|
-
"-NoProfile",
|
|
242
|
-
"-NonInteractive",
|
|
243
|
-
"-Command",
|
|
244
|
-
`Expand-Archive -Path '${archivePath.replace(/'/g, "''")}' -DestinationPath '${destDir.replace(/'/g, "''")}' -Force`,
|
|
245
|
-
],
|
|
246
|
-
{ stdio: ["ignore", "inherit", "inherit"] },
|
|
247
|
-
);
|
|
248
|
-
} else {
|
|
249
|
-
execFileSync("unzip", ["-o", archivePath, "-d", destDir], {
|
|
250
|
-
stdio: ["ignore", "inherit", "inherit"],
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// --- Main flow --------------------------------------------------------------
|
|
256
|
-
|
|
257
143
|
async function main() {
|
|
258
144
|
const platform = process.platform;
|
|
259
145
|
const arch = process.arch;
|
|
260
146
|
const isMusl = detectMuslLinux();
|
|
261
|
-
const
|
|
147
|
+
const candidates = pickAssetName(platform, arch, isMusl);
|
|
262
148
|
|
|
263
|
-
if (!
|
|
264
|
-
warn(
|
|
265
|
-
`unsupported platform/arch combo: ${platform}/${arch}. ` +
|
|
266
|
-
`Build from source instead: https://github.com/${REPO}#from-source`,
|
|
267
|
-
);
|
|
149
|
+
if (!candidates) {
|
|
150
|
+
warn(`unsupported platform/arch: ${platform}/${arch}. Build from source: https://github.com/${REPO}#from-source`);
|
|
268
151
|
return;
|
|
269
152
|
}
|
|
270
153
|
|
|
271
|
-
info(`target: ${platform}/${arch}${isMusl ? " (musl)" : ""}
|
|
154
|
+
info(`target: ${platform}/${arch}${isMusl ? " (musl)" : ""}`);
|
|
272
155
|
|
|
273
156
|
let release;
|
|
274
157
|
try {
|
|
275
158
|
release = await fetchJson(RELEASE_API);
|
|
276
159
|
} catch (e) {
|
|
277
160
|
if (e.httpStatus === 404) {
|
|
278
|
-
warn(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
161
|
+
warn(`no release at github.com/${REPO}/releases. Falling back to Bun install.`);
|
|
162
|
+
} else if (e.httpStatus === 403) {
|
|
163
|
+
warn(`GitHub API rate-limited. Set GITHUB_TOKEN env var to retry. Falling back to Bun install.`);
|
|
164
|
+
} else {
|
|
165
|
+
warn(`could not reach GitHub API (${e.message}). Skipping binary fetch.`);
|
|
283
166
|
}
|
|
284
|
-
warn(
|
|
285
|
-
`could not reach GitHub API (${e.message}). ` +
|
|
286
|
-
`Skipping binary fetch. Run 'npm rebuild' after going online.`,
|
|
287
|
-
);
|
|
288
167
|
return;
|
|
289
168
|
}
|
|
290
169
|
|
|
291
|
-
const
|
|
292
|
-
|
|
170
|
+
const available = release.assets ?? [];
|
|
171
|
+
const asset =
|
|
172
|
+
candidates.map((n) => available.find((a) => a.name === n)).find(Boolean) ?? null;
|
|
173
|
+
|
|
174
|
+
if (!asset?.browser_download_url) {
|
|
293
175
|
warn(
|
|
294
|
-
`release ${release.tag_name ?? "(unknown)"} has no asset
|
|
295
|
-
`
|
|
296
|
-
`Falling back to Bun install.`,
|
|
176
|
+
`release ${release.tag_name ?? "(unknown)"} has no matching asset. ` +
|
|
177
|
+
`Wanted one of: ${candidates.join(", ")}. Available: ${available.map((a) => a.name).join(", ") || "(none)"}.`,
|
|
297
178
|
);
|
|
298
179
|
return;
|
|
299
180
|
}
|
|
300
181
|
|
|
301
|
-
// Download into a temp dir so a half-finished install never clobbers bin/.
|
|
302
182
|
const tmp = await mkdtemp(join(tmpdir(), "snscoder-"));
|
|
303
|
-
const
|
|
304
|
-
info(`downloading ${asset.
|
|
183
|
+
const tmpFile = join(tmp, asset.name);
|
|
184
|
+
info(`downloading ${asset.name} (${(asset.size / 1024 / 1024).toFixed(2)} MB)`);
|
|
305
185
|
|
|
306
|
-
let
|
|
186
|
+
let buf;
|
|
307
187
|
try {
|
|
308
|
-
|
|
188
|
+
buf = await fetchBuffer(asset.browser_download_url);
|
|
309
189
|
} catch (e) {
|
|
310
|
-
warn(`download failed
|
|
190
|
+
warn(`download failed: ${e.message}`);
|
|
311
191
|
await rm(tmp, { recursive: true, force: true });
|
|
312
192
|
return;
|
|
313
193
|
}
|
|
314
194
|
|
|
315
195
|
await new Promise((resolveP, rejectP) => {
|
|
316
|
-
const ws = createWriteStream(
|
|
196
|
+
const ws = createWriteStream(tmpFile);
|
|
317
197
|
ws.on("error", rejectP);
|
|
318
198
|
ws.on("finish", resolveP);
|
|
319
|
-
ws.end(
|
|
199
|
+
ws.end(buf);
|
|
320
200
|
});
|
|
321
201
|
|
|
322
|
-
info(`extracting → ${BIN_DIR}`);
|
|
323
202
|
mkdirSync(BIN_DIR, { recursive: true });
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return;
|
|
203
|
+
const target = join(BIN_DIR, asset.name);
|
|
204
|
+
if (existsSync(target)) {
|
|
205
|
+
try {
|
|
206
|
+
unlinkSync(target);
|
|
207
|
+
} catch {}
|
|
330
208
|
}
|
|
331
209
|
|
|
332
|
-
|
|
333
|
-
const
|
|
210
|
+
// Move from tmp into bin/
|
|
211
|
+
const { copyFile } = await import("node:fs/promises");
|
|
212
|
+
await copyFile(tmpFile, target);
|
|
213
|
+
await rm(tmp, { recursive: true, force: true });
|
|
334
214
|
|
|
335
|
-
if (!
|
|
336
|
-
|
|
337
|
-
const entries = await readdir(BIN_DIR);
|
|
338
|
-
warn(
|
|
339
|
-
`expected '${binaryName}' in archive but found: ${entries.join(", ") || "(empty)"}. ` +
|
|
340
|
-
`Adjust scripts/fetch-binary.mjs to match the release asset layout.`,
|
|
341
|
-
);
|
|
342
|
-
await rm(tmp, { recursive: true, force: true });
|
|
343
|
-
return;
|
|
215
|
+
if (!isWin) {
|
|
216
|
+
await chmod(target, 0o755);
|
|
344
217
|
}
|
|
345
218
|
|
|
346
|
-
|
|
347
|
-
|
|
219
|
+
// Symlink: bin/snscoder → bin/<asset-name> (so `snscoder` works on PATH)
|
|
220
|
+
// npm install -g creates bin/<name> symlinks for the entries in package.json "bin"
|
|
221
|
+
// (which is "snscoder" → "bin/snscoder.js"). We don't have that shim; instead
|
|
222
|
+
// we create a wrapper file bin/snscoder.js that exec's the platform binary.
|
|
223
|
+
const shim = isWin ? "snscoder.cmd" : "snscoder.js";
|
|
224
|
+
const shimPath = join(BIN_DIR, shim);
|
|
225
|
+
|
|
226
|
+
if (isWin) {
|
|
227
|
+
const { writeFileSync } = await import("node:fs");
|
|
228
|
+
writeFileSync(
|
|
229
|
+
shimPath,
|
|
230
|
+
`@echo off\r\n"%~dp0${asset.name}" %*\r\n`,
|
|
231
|
+
);
|
|
232
|
+
} else {
|
|
233
|
+
const { writeFileSync } = await import("node:fs");
|
|
234
|
+
writeFileSync(
|
|
235
|
+
shimPath,
|
|
236
|
+
`#!/usr/bin/env node\n// Auto-generated by fetch-binary.mjs — execs the platform binary.\nimport { spawn } from "node:child_process";\nimport { fileURLToPath } from "node:url";\nimport { dirname, join } from "node:path";\nconst here = dirname(fileURLToPath(import.meta.url));\nconst bin = join(here, "${asset.name}");\nconst r = spawn(bin, process.argv.slice(2), { stdio: "inherit" });\nr.on("exit", (c) => process.exit(c ?? 0));\n`,
|
|
237
|
+
);
|
|
238
|
+
await chmod(shimPath, 0o755);
|
|
348
239
|
}
|
|
349
|
-
const st = await stat(binaryPath);
|
|
350
|
-
ok(`${binaryName} ready (${(st.size / 1024 / 1024).toFixed(2)} MB)`);
|
|
351
240
|
|
|
352
|
-
|
|
241
|
+
const st = await stat(target);
|
|
242
|
+
ok(`${asset.name} ready (${(st.size / 1024 / 1024).toFixed(2)} MB) → bin/${shim}`);
|
|
353
243
|
}
|
|
354
244
|
|
|
355
245
|
main().catch((e) => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
process.exit(0);
|
|
246
|
+
err(`unexpected: ${e?.stack ?? e?.message ?? e}`);
|
|
247
|
+
process.exit(0); // never break npm install
|
|
359
248
|
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SNS-MyAgent E2E smoke test
|
|
3
|
+
# Runs against a freshly built binary to verify all critical paths work.
|
|
4
|
+
# Usage: ./scripts/smoke-test.sh [path-to-binary]
|
|
5
|
+
# Exit 0 on PASS, 1 on any FAIL.
|
|
6
|
+
#
|
|
7
|
+
# NOTE: Binary may exit 1 in JS-only fallback mode (pi_natives missing on CI
|
|
8
|
+
# runner). We check stdout content rather than strict exit code for those.
|
|
9
|
+
|
|
10
|
+
set -uo pipefail
|
|
11
|
+
|
|
12
|
+
BIN="${1:-./bin/snscoder-linux-x64}"
|
|
13
|
+
FAIL=0
|
|
14
|
+
PASS=0
|
|
15
|
+
|
|
16
|
+
# run_check <label> <expect-exit> <cmd...>
|
|
17
|
+
# - exit code MUST match expect-exit for PASS
|
|
18
|
+
run_check() {
|
|
19
|
+
local label="$1"
|
|
20
|
+
local expect_code="$2"
|
|
21
|
+
shift 2
|
|
22
|
+
local out
|
|
23
|
+
out=$("$BIN" "$@" 2>&1)
|
|
24
|
+
local actual=$?
|
|
25
|
+
if [[ $actual -eq $expect_code ]]; then
|
|
26
|
+
echo "✓ $label"
|
|
27
|
+
PASS=$((PASS + 1))
|
|
28
|
+
else
|
|
29
|
+
echo "✗ $label (expected exit $expect_code, got $actual)"
|
|
30
|
+
echo " output: ${out:0:200}"
|
|
31
|
+
FAIL=$((FAIL + 1))
|
|
32
|
+
fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# run_loose <label> <expect-substring-in-stdout> <cmd...>
|
|
36
|
+
# - tolerates exit 0 OR 1 (JS-only fallback)
|
|
37
|
+
# - just checks stdout/stderr contains the substring
|
|
38
|
+
run_loose() {
|
|
39
|
+
local label="$1"
|
|
40
|
+
local needle="$2"
|
|
41
|
+
shift 2
|
|
42
|
+
local out
|
|
43
|
+
out=$("$BIN" "$@" 2>&1)
|
|
44
|
+
if [[ "$out" == *"$needle"* ]]; then
|
|
45
|
+
echo "✓ $label"
|
|
46
|
+
PASS=$((PASS + 1))
|
|
47
|
+
else
|
|
48
|
+
echo "✗ $label (missing '$needle' in output)"
|
|
49
|
+
echo " output: ${out:0:200}"
|
|
50
|
+
FAIL=$((FAIL + 1))
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
echo "=== SNS-MyAgent E2E smoke test ==="
|
|
55
|
+
echo "Binary: $BIN"
|
|
56
|
+
echo ""
|
|
57
|
+
|
|
58
|
+
if [[ ! -f "$BIN" ]]; then
|
|
59
|
+
echo "✗ binary not found: $BIN"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Version: pi_natives may be missing on CI runner → JS-only fallback may exit 1
|
|
64
|
+
# but version text still prints. Check substring rather than exit code.
|
|
65
|
+
run_loose "version flag prints version" "snscoder " --version
|
|
66
|
+
run_loose "version subcommand prints version" "snscoder " version
|
|
67
|
+
|
|
68
|
+
# Help: help uses only built-in code, should exit 0 reliably
|
|
69
|
+
run_check "help flag" 0 --help
|
|
70
|
+
run_check "help subcommand" 0 help
|
|
71
|
+
|
|
72
|
+
# Init (skips if config exists)
|
|
73
|
+
run_check "init (idempotent)" 0 init
|
|
74
|
+
|
|
75
|
+
# Config subcommands — may also be affected by js-only
|
|
76
|
+
run_loose "config show" "model" config show
|
|
77
|
+
|
|
78
|
+
# Orchestrate (stub returns exit 2 with clear message)
|
|
79
|
+
run_loose "orchestrate shows clear stub message" "agent executor not wired" orchestrate "test prompt"
|
|
80
|
+
|
|
81
|
+
# Unknown command — exit 1 guaranteed
|
|
82
|
+
run_check "unknown command (exit 1)" 1 nonexistent-command
|
|
83
|
+
|
|
84
|
+
echo ""
|
|
85
|
+
echo "=== Summary ==="
|
|
86
|
+
echo " PASS: $PASS"
|
|
87
|
+
echo " FAIL: $FAIL"
|
|
88
|
+
|
|
89
|
+
if [[ $FAIL -gt 0 ]]; then
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
exit 0
|