@forgeailab/smith 0.0.0

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.
Files changed (3) hide show
  1. package/README.md +27 -0
  2. package/bin/cli.js +361 -0
  3. package/package.json +35 -0
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # Smith npm bootstrapper
2
+
3
+ Run Smith from npm without installing a platform binary first:
4
+
5
+ ```bash
6
+ npx @forgeailab/smith
7
+ ```
8
+
9
+ The package downloads the matching Smith GitHub release archive for macOS,
10
+ glibc Linux, or musl Linux, verifies it against `SHA256SUMS` when available,
11
+ caches it under `~/.smith/npx`, and starts the `smith` binary.
12
+
13
+ Useful commands:
14
+
15
+ ```bash
16
+ npx @forgeailab/smith -p "explain this repo" # one headless turn
17
+ npx @forgeailab/smith setup # guided provider/model setup
18
+ npx @forgeailab/smith --help
19
+ ```
20
+
21
+ By default, the wrapper downloads the GitHub release tag that matches the npm
22
+ package version. For testing a different release:
23
+
24
+ ```bash
25
+ npx @forgeailab/smith --release latest
26
+ SMITH_NPX_TAG=v0.1.0 npx @forgeailab/smith
27
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const childProcess = require("node:child_process");
5
+ const crypto = require("node:crypto");
6
+ const fs = require("node:fs");
7
+ const https = require("node:https");
8
+ const os = require("node:os");
9
+ const path = require("node:path");
10
+ const process = require("node:process");
11
+
12
+ const PACKAGE = require("../package.json");
13
+
14
+ const REPO = "ForgeAILab/smith";
15
+ const CACHE_ROOT = path.join(os.homedir(), ".smith", "npx");
16
+ const USER_AGENT = `${PACKAGE.name}/${PACKAGE.version}`;
17
+
18
+ function usage() {
19
+ const version = PACKAGE.version;
20
+ console.log(`Smith npm bootstrapper ${version}
21
+
22
+ Usage:
23
+ npx ${PACKAGE.name} [smith-options]
24
+
25
+ Examples:
26
+ npx ${PACKAGE.name} # interactive TUI
27
+ npx ${PACKAGE.name} -p "explain this repo" # one headless turn
28
+ npx ${PACKAGE.name} setup # guided provider/model setup
29
+
30
+ Options handled by the bootstrapper:
31
+ --release <tag|latest> Download a specific GitHub release tag
32
+ --version Show the npm bootstrapper version
33
+ --help Show this help
34
+
35
+ All other options are passed through to the smith binary.`);
36
+ }
37
+
38
+ function isHelp(args) {
39
+ return args.includes("--help") || args.includes("-h");
40
+ }
41
+
42
+ function isVersion(args) {
43
+ return args.includes("--version") || args.includes("-V");
44
+ }
45
+
46
+ function parseArgs(argv) {
47
+ const args = [...argv];
48
+ let release =
49
+ process.env.SMITH_NPX_TAG ||
50
+ (PACKAGE.version === "0.0.0" ? "latest" : `v${PACKAGE.version}`);
51
+ const passthrough = [];
52
+
53
+ for (let i = 0; i < args.length; i += 1) {
54
+ const arg = args[i];
55
+ if (arg === "--release") {
56
+ const value = args[i + 1];
57
+ if (!value) {
58
+ throw new Error("--release requires a tag value");
59
+ }
60
+ release = value;
61
+ i += 1;
62
+ continue;
63
+ }
64
+ if (arg.startsWith("--release=")) {
65
+ release = arg.slice("--release=".length);
66
+ continue;
67
+ }
68
+ passthrough.push(arg);
69
+ }
70
+
71
+ return { passthrough, release };
72
+ }
73
+
74
+ function platformInfo(platform = process.platform, archInput = process.arch) {
75
+ if (platform !== "darwin" && platform !== "linux") {
76
+ throw new Error(
77
+ `Unsupported platform ${platform}. Smith release archives currently support macOS and Linux.`
78
+ );
79
+ }
80
+
81
+ let osName = platform === "darwin" ? "macos" : "linux";
82
+ let arch = archInput;
83
+ if (arch === "x64") arch = "x86_64";
84
+ if (arch === "arm64") arch = "aarch64";
85
+
86
+ if (arch !== "x86_64" && arch !== "aarch64") {
87
+ throw new Error(`Unsupported architecture ${archInput}`);
88
+ }
89
+
90
+ if (platform === "linux" && linuxLibc() === "musl") {
91
+ osName = "linux-musl";
92
+ }
93
+
94
+ const artifact = `smith-${arch}-${osName}`;
95
+ return { artifact, archiveName: `${artifact}.tar.gz` };
96
+ }
97
+
98
+ function linuxLibc() {
99
+ const override = process.env.SMITH_NPX_LIBC;
100
+ if (override === "gnu" || override === "musl") {
101
+ return override;
102
+ }
103
+
104
+ const report = process.report?.getReport?.();
105
+ if (report?.header?.glibcVersionRuntime) {
106
+ return "gnu";
107
+ }
108
+
109
+ let output = "";
110
+ try {
111
+ output = childProcess.execFileSync("ldd", ["--version"], {
112
+ encoding: "utf8",
113
+ stdio: ["ignore", "pipe", "pipe"],
114
+ });
115
+ } catch (error) {
116
+ output = `${error.stdout || ""}\n${error.stderr || ""}`;
117
+ }
118
+
119
+ return /musl/i.test(output) ? "musl" : "gnu";
120
+ }
121
+
122
+ function request(url, redirects = 0) {
123
+ return new Promise((resolve, reject) => {
124
+ const req = https.get(
125
+ url,
126
+ {
127
+ headers: {
128
+ "user-agent": USER_AGENT,
129
+ accept: "application/vnd.github+json, application/octet-stream, */*",
130
+ },
131
+ },
132
+ (res) => {
133
+ const location = res.headers.location;
134
+ if (
135
+ location &&
136
+ [301, 302, 303, 307, 308].includes(res.statusCode || 0)
137
+ ) {
138
+ res.resume();
139
+ if (redirects > 5) {
140
+ reject(new Error(`Too many redirects for ${url}`));
141
+ return;
142
+ }
143
+ request(new URL(location, url).toString(), redirects + 1)
144
+ .then(resolve)
145
+ .catch(reject);
146
+ return;
147
+ }
148
+
149
+ if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300) {
150
+ res.resume();
151
+ reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
152
+ return;
153
+ }
154
+
155
+ resolve(res);
156
+ }
157
+ );
158
+ req.on("error", reject);
159
+ });
160
+ }
161
+
162
+ async function fetchText(url) {
163
+ const res = await request(url);
164
+ return new Promise((resolve, reject) => {
165
+ let body = "";
166
+ res.setEncoding("utf8");
167
+ res.on("data", (chunk) => {
168
+ body += chunk;
169
+ });
170
+ res.on("end", () => resolve(body));
171
+ res.on("error", reject);
172
+ });
173
+ }
174
+
175
+ async function resolveReleaseTag(release) {
176
+ if (release !== "latest") {
177
+ return release;
178
+ }
179
+
180
+ const body = await fetchText(`https://api.github.com/repos/${REPO}/releases/latest`);
181
+ const json = JSON.parse(body);
182
+ if (!json.tag_name) {
183
+ throw new Error("GitHub latest release response did not include tag_name");
184
+ }
185
+ return json.tag_name;
186
+ }
187
+
188
+ function parseChecksum(sums, archiveName) {
189
+ for (const line of sums.split(/\r?\n/)) {
190
+ const match = line.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
191
+ if (match && match[2].trim() === archiveName) {
192
+ return match[1].toLowerCase();
193
+ }
194
+ }
195
+ return null;
196
+ }
197
+
198
+ function downloadFile(url, dest, expectedSha256) {
199
+ const temp = `${dest}.tmp-${process.pid}`;
200
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
201
+
202
+ return new Promise((resolve, reject) => {
203
+ request(url)
204
+ .then((res) => {
205
+ const total = Number(res.headers["content-length"] || 0);
206
+ let downloaded = 0;
207
+ const hash = crypto.createHash("sha256");
208
+ const file = fs.createWriteStream(temp);
209
+
210
+ const cleanup = (error) => {
211
+ file.destroy();
212
+ try {
213
+ fs.unlinkSync(temp);
214
+ } catch {}
215
+ reject(error);
216
+ };
217
+
218
+ res.on("data", (chunk) => {
219
+ downloaded += chunk.length;
220
+ hash.update(chunk);
221
+ if (total > 0) {
222
+ const pct = Math.round((downloaded / total) * 100);
223
+ process.stderr.write(`\rDownloading Smith release: ${pct}%`);
224
+ }
225
+ });
226
+
227
+ res.on("error", cleanup);
228
+ file.on("error", cleanup);
229
+ file.on("finish", () => {
230
+ const actual = hash.digest("hex");
231
+ if (expectedSha256 && actual !== expectedSha256) {
232
+ cleanup(
233
+ new Error(
234
+ `Checksum mismatch for ${path.basename(dest)}: expected ${expectedSha256}, got ${actual}`
235
+ )
236
+ );
237
+ return;
238
+ }
239
+ fs.renameSync(temp, dest);
240
+ if (total > 0) {
241
+ process.stderr.write("\n");
242
+ }
243
+ resolve();
244
+ });
245
+
246
+ res.pipe(file);
247
+ })
248
+ .catch((error) => {
249
+ try {
250
+ fs.unlinkSync(temp);
251
+ } catch {}
252
+ reject(error);
253
+ });
254
+ });
255
+ }
256
+
257
+ function runTar(archive, dest) {
258
+ fs.mkdirSync(dest, { recursive: true });
259
+ childProcess.execFileSync("tar", ["-xzf", archive, "-C", dest], {
260
+ stdio: "pipe",
261
+ });
262
+ }
263
+
264
+ async function ensureRelease(release) {
265
+ const { artifact, archiveName } = platformInfo();
266
+ const tag = await resolveReleaseTag(release);
267
+ const installDir = path.join(CACHE_ROOT, "releases", tag, artifact);
268
+ const readyFile = path.join(installDir, ".ready");
269
+ const binaryPath = path.join(installDir, "smith");
270
+
271
+ if (fs.existsSync(readyFile) && fs.existsSync(binaryPath)) {
272
+ return { binaryPath, installDir, tag };
273
+ }
274
+
275
+ const archive = path.join(CACHE_ROOT, "archives", tag, archiveName);
276
+ const releaseBase = `https://github.com/${REPO}/releases/download/${tag}`;
277
+
278
+ let expectedSha256 = null;
279
+ try {
280
+ const sums = await fetchText(`${releaseBase}/SHA256SUMS`);
281
+ expectedSha256 = parseChecksum(sums, archiveName);
282
+ } catch {}
283
+
284
+ if (!fs.existsSync(archive)) {
285
+ console.error(`Fetching Smith ${tag} for ${artifact}...`);
286
+ await downloadFile(`${releaseBase}/${archiveName}`, archive, expectedSha256);
287
+ } else if (expectedSha256) {
288
+ const hash = crypto.createHash("sha256");
289
+ hash.update(fs.readFileSync(archive));
290
+ const actual = hash.digest("hex");
291
+ if (actual !== expectedSha256) {
292
+ fs.unlinkSync(archive);
293
+ console.error(`Cached Smith archive checksum changed; refetching ${archiveName}...`);
294
+ await downloadFile(`${releaseBase}/${archiveName}`, archive, expectedSha256);
295
+ }
296
+ }
297
+
298
+ const tempDir = `${installDir}.tmp-${process.pid}`;
299
+ fs.rmSync(tempDir, { recursive: true, force: true });
300
+ fs.mkdirSync(tempDir, { recursive: true });
301
+ runTar(archive, tempDir);
302
+
303
+ if (!fs.existsSync(path.join(tempDir, "smith"))) {
304
+ throw new Error("Release archive did not include smith");
305
+ }
306
+ fs.chmodSync(path.join(tempDir, "smith"), 0o755);
307
+
308
+ fs.rmSync(installDir, { recursive: true, force: true });
309
+ fs.renameSync(tempDir, installDir);
310
+ fs.writeFileSync(readyFile, `${new Date().toISOString()}\n`);
311
+
312
+ return { binaryPath, installDir, tag };
313
+ }
314
+
315
+ function runBinary(binary, args, env) {
316
+ const child = childProcess.spawn(binary, args, {
317
+ env,
318
+ stdio: "inherit",
319
+ });
320
+
321
+ child.on("exit", (code, signal) => {
322
+ if (signal) {
323
+ process.kill(process.pid, signal);
324
+ return;
325
+ }
326
+ process.exit(code || 0);
327
+ });
328
+ child.on("error", (error) => {
329
+ console.error(`Failed to start smith: ${error.message}`);
330
+ process.exit(1);
331
+ });
332
+
333
+ process.on("SIGINT", () => child.kill("SIGINT"));
334
+ process.on("SIGTERM", () => child.kill("SIGTERM"));
335
+ }
336
+
337
+ async function main() {
338
+ const rawArgs = process.argv.slice(2);
339
+ if (isHelp(rawArgs)) {
340
+ usage();
341
+ return;
342
+ }
343
+ if (isVersion(rawArgs)) {
344
+ console.log(PACKAGE.version);
345
+ return;
346
+ }
347
+
348
+ const options = parseArgs(rawArgs);
349
+ const release = await ensureRelease(options.release);
350
+ runBinary(release.binaryPath, options.passthrough, { ...process.env });
351
+ }
352
+
353
+ if (require.main === module) {
354
+ main().catch((error) => {
355
+ console.error(`smith npm bootstrap failed: ${error.message}`);
356
+ if (process.env.SMITH_NPX_DEBUG && error.stack) {
357
+ console.error(error.stack);
358
+ }
359
+ process.exit(1);
360
+ });
361
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@forgeailab/smith",
3
+ "version": "0.0.0",
4
+ "description": "NPX bootstrapper for the Smith terminal-first coding agent",
5
+ "license": "(MIT OR Apache-2.0)",
6
+ "homepage": "https://github.com/ForgeAILab/smith#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ForgeAILab/smith.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/ForgeAILab/smith/issues"
13
+ },
14
+ "bin": {
15
+ "smith": "bin/cli.js"
16
+ },
17
+ "files": [
18
+ "bin",
19
+ "README.md"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "keywords": [
28
+ "ai",
29
+ "agent",
30
+ "cli",
31
+ "coding-agent",
32
+ "smith",
33
+ "tui"
34
+ ]
35
+ }