sasacode 0.9.8 → 0.10.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 CHANGED
@@ -7,6 +7,6 @@ npm install -g sasacode
7
7
  sasacode
8
8
  ```
9
9
 
10
- This package is a launcher. On its first run it downloads the single binary of the same version for your machine (macOS arm64 / x64, Linux x64 / arm64, glibc or musl) from [GitHub Releases](https://github.com/sasanokusa/sasacode/releases), checks it against the release's `SHA256SUMS`, and keeps it in `~/.sasacode/npm-bin`. Later runs start it directly. Update with `npm install -g sasacode@latest`. Windows is supported through WSL.
10
+ This package is a launcher. On its first run it downloads the single binary of the same version for your machine (macOS arm64 / x64, Linux x64 / arm64, glibc or musl, Windows x64 / arm64) from [GitHub Releases](https://github.com/sasanokusa/sasacode/releases), checks it against the release's `SHA256SUMS`, and keeps it in `~/.sasacode/npm-bin`. Later runs start it directly. Update with `npm install -g sasacode@latest`. On Windows, the `bash` tool needs [Git for Windows](https://git-scm.com/download/win) (Git Bash).
11
11
 
12
- Without npm: `curl -fsSL https://sasanokusa.com/sasacode/install.sh | sh`.
12
+ Without npm: `curl -fsSL https://sasanokusa.com/sasacode/install.sh | sh` (macOS, Linux), or in PowerShell `irm https://sasanokusa.com/sasacode/install.ps1 | iex` (Windows).
package/bin/sasacode.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // are the ones scripts/install.sh uses.
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { createHash } from "node:crypto";
8
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { constants, homedir } from "node:os";
10
10
  import { dirname, join } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
@@ -19,11 +19,16 @@ function die(message) {
19
19
  process.exit(1);
20
20
  }
21
21
 
22
+ const windows = process.platform === "win32";
23
+
22
24
  function target() {
23
- const os = { darwin: "darwin", linux: "linux" }[process.platform];
24
- if (!os) die(process.platform === "win32" ? "Windows is supported through WSL: install sasacode inside a WSL shell" : `unsupported OS: ${process.platform}`);
25
+ const os = { darwin: "darwin", linux: "linux", win32: "windows" }[process.platform];
26
+ if (!os) die(`unsupported OS: ${process.platform}`);
25
27
  let arch = { x64: "x64", arm64: "arm64" }[process.arch];
26
28
  if (!arch) die(`unsupported CPU: ${process.arch}`);
29
+ // Windows has no cheap CPU check (PowerShell is slow, and can hang without a full environment):
30
+ // the x64 build is tried first and the baseline one taken when it does not run (no AVX2).
31
+ if (os === "windows") return `windows-${arch}`;
27
32
  // An x64 Node under Rosetta on Apple Silicon should still get the native build.
28
33
  if (os === "darwin" && arch === "x64" && spawnSync("sysctl", ["-n", "sysctl.proc_translated"], { encoding: "utf8" }).stdout?.trim() === "1") arch = "arm64";
29
34
  if (os === "linux") {
@@ -34,6 +39,14 @@ function target() {
34
39
  return `${os}-${arch}`;
35
40
  }
36
41
 
42
+ function readdirSafe(path) {
43
+ try {
44
+ return readdirSync(path);
45
+ } catch {
46
+ return [];
47
+ }
48
+ }
49
+
37
50
  function readSafe(path) {
38
51
  try {
39
52
  return readFileSync(path, "utf8");
@@ -48,31 +61,49 @@ async function download(url) {
48
61
  return Buffer.from(await res.arrayBuffer());
49
62
  }
50
63
 
51
- async function install(name, dest) {
64
+ /** Download, check and keep the binary `name` at `dest`. With `mayNotRun`, false when it does not run here. */
65
+ async function install(name, dest, mayNotRun = false) {
52
66
  const url = `${base}/download/v${version}`;
67
+ // Windows releases are a .zip holding an .exe; the rest a .tar.gz.
68
+ const file = windows ? `${name}.zip` : `${name}.tar.gz`;
69
+ const binary = windows ? `${name}.exe` : name;
53
70
  process.stderr.write(`sasacode (npm): downloading sasacode v${version} (${name.replace(/^sasacode-/, "")}) from ${url}\n`);
54
- const [archive, sums] = await Promise.all([download(`${url}/${name}.tar.gz`), download(`${url}/SHA256SUMS`)]);
55
- const expected = sums.toString("utf8").split("\n").find((l) => l.endsWith(` ${name}.tar.gz`))?.split(/\s+/)[0];
56
- if (!expected) die(`${name}.tar.gz is not listed in SHA256SUMS`);
57
- if (createHash("sha256").update(archive).digest("hex") !== expected) die(`checksum mismatch for ${name}.tar.gz`);
71
+ const [archive, sums] = await Promise.all([download(`${url}/${file}`), download(`${url}/SHA256SUMS`)]);
72
+ const expected = sums.toString("utf8").split(/\r?\n/).find((l) => l.endsWith(` ${file}`))?.split(/\s+/)[0];
73
+ if (!expected) die(`${file} is not listed in SHA256SUMS`);
74
+ if (createHash("sha256").update(archive).digest("hex") !== expected) die(`checksum mismatch for ${file}`);
58
75
  const tmp = mkdtempSync(join(dirname(dest), ".download-"));
59
76
  try {
60
- writeFileSync(join(tmp, `${name}.tar.gz`), archive);
61
- if (spawnSync("tar", ["-xzf", `${name}.tar.gz`], { cwd: tmp }).status !== 0) die(`could not unpack ${name}.tar.gz`);
62
- chmodSync(join(tmp, name), 0o755);
63
- if (!spawnSync(join(tmp, name), ["--version"], { encoding: "utf8" }).stdout?.trim()) die("the downloaded binary does not run on this system");
64
- renameSync(join(tmp, name), dest); // atomic: a second run at the same time finds a whole file or none
77
+ writeFileSync(join(tmp, file), archive);
78
+ // Windows' own tar (bsdtar) reads .zip; a GNU tar earlier on the PATH (Git's) would not.
79
+ const tar = windows ? join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe") : "tar";
80
+ if (spawnSync(tar, [windows ? "-xf" : "-xzf", file], { cwd: tmp, windowsHide: true }).status !== 0) die(`could not unpack ${file}`);
81
+ chmodSync(join(tmp, binary), 0o755);
82
+ if (!spawnSync(join(tmp, binary), ["--version"], { encoding: "utf8", windowsHide: true }).stdout?.trim()) {
83
+ if (mayNotRun) return false;
84
+ die("the downloaded binary does not run on this system");
85
+ }
86
+ renameSync(join(tmp, binary), dest); // atomic: a second run at the same time finds a whole file or none
87
+ return true;
65
88
  } finally {
66
89
  rmSync(tmp, { recursive: true, force: true });
67
90
  }
68
91
  }
69
92
 
70
- const name = `sasacode-${target()}`;
71
93
  const dir = join(process.env.SASACODE_HOME ?? join(homedir(), ".sasacode"), "npm-bin", `v${version}`);
72
- const bin = join(dir, name);
73
- if (!existsSync(bin)) {
74
- mkdirSync(dir, { recursive: true });
75
- await install(name, bin);
94
+ // On Windows the build kept for this version is whichever one ran here (x64 or its baseline).
95
+ const kept = windows ? readdirSafe(dir).find((f) => /^sasacode-windows-[\w-]+\.exe$/.test(f)) : undefined;
96
+ let bin = kept && join(dir, kept);
97
+ if (!bin) {
98
+ const name = `sasacode-${target()}`;
99
+ bin = join(dir, windows ? `${name}.exe` : name);
100
+ if (!existsSync(bin)) {
101
+ mkdirSync(dir, { recursive: true });
102
+ if (!(await install(name, bin, name === "sasacode-windows-x64"))) {
103
+ bin = join(dir, "sasacode-windows-x64-baseline.exe"); // a CPU without AVX2
104
+ await install("sasacode-windows-x64-baseline", bin);
105
+ }
106
+ }
76
107
  }
77
108
  // `sasacode update` would replace this cached binary; with npm, npm does the updating.
78
109
  const run = spawnSync(bin, process.argv.slice(2), { stdio: "inherit", env: { ...process.env, SASACODE_INSTALLED_BY: "npm" } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sasacode",
3
- "version": "0.9.8",
3
+ "version": "0.10.1",
4
4
  "description": "A small, pluggable coding agent for the terminal. This package fetches the single binary for your machine on first run.",
5
5
  "keywords": ["coding-agent", "cli", "llm", "agent", "terminal"],
6
6
  "type": "module",