nanoai-cli 1.0.7
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 +54 -0
- package/bin/nanoai.js +62 -0
- package/package.json +52 -0
- package/scripts/download.js +132 -0
- package/scripts/platform.js +100 -0
- package/scripts/postinstall.js +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# nanoagent
|
|
2
|
+
|
|
3
|
+
The NanoAgent CLI (`nanoai`) — terminal UI, ACP server, and automation-friendly agent.
|
|
4
|
+
|
|
5
|
+
This package is a thin installer: on install (or on first run) it downloads the
|
|
6
|
+
matching self-contained NanoAgent CLI binary from the project's
|
|
7
|
+
[GitHub Releases](https://github.com/rizwan3d/NanoAgent/releases) and verifies it
|
|
8
|
+
against the published `SHA256SUMS`. No build toolchain is required.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g nanoagent
|
|
14
|
+
# or
|
|
15
|
+
bun add -g nanoagent
|
|
16
|
+
# or
|
|
17
|
+
pnpm add -g nanoagent
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then run:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
nanoai
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> **bun note:** bun skips `postinstall` scripts by default, so the binary is
|
|
27
|
+
> downloaded automatically the first time you run `nanoai`. To download eagerly,
|
|
28
|
+
> run `bunx nanoagent --version` once after installing.
|
|
29
|
+
|
|
30
|
+
## Supported platforms
|
|
31
|
+
|
|
32
|
+
| OS | Architecture |
|
|
33
|
+
| ------- | ------------ |
|
|
34
|
+
| Windows | x64 |
|
|
35
|
+
| macOS | x64, arm64 |
|
|
36
|
+
| Linux | x64, arm64 |
|
|
37
|
+
|
|
38
|
+
## Environment variables
|
|
39
|
+
|
|
40
|
+
| Variable | Purpose |
|
|
41
|
+
| ------------------------- | -------------------------------------------------------------- |
|
|
42
|
+
| `NANOAGENT_SKIP_DOWNLOAD` | Set to `1` to skip the postinstall download (fetched on run). |
|
|
43
|
+
| `NANOAGENT_CLI_TAG` | Override the release tag to download (default `v<version>`). |
|
|
44
|
+
| `NANOAGENT_CLI_BASE_URL` | Override the release asset base URL (mirrors, testing). |
|
|
45
|
+
|
|
46
|
+
## Reinstalling the binary
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node node_modules/nanoagent/scripts/download.js
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
Apache-2.0
|
package/bin/nanoai.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Launcher for the NanoAgent CLI. Spawns the vendored native binary, passing
|
|
5
|
+
// through all arguments, stdio, and the exit code. If the binary is missing
|
|
6
|
+
// (e.g. postinstall was skipped by bun, or a prior download failed), it is
|
|
7
|
+
// downloaded on demand before the first launch.
|
|
8
|
+
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const { spawn } = require("child_process");
|
|
11
|
+
|
|
12
|
+
const platform = require("../scripts/platform");
|
|
13
|
+
|
|
14
|
+
function launch(binaryPath) {
|
|
15
|
+
const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit" });
|
|
16
|
+
|
|
17
|
+
child.on("error", (err) => {
|
|
18
|
+
console.error(`[nanoagent] Failed to start NanoAgent CLI: ${err.message}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
child.on("exit", (code, signal) => {
|
|
23
|
+
if (signal) {
|
|
24
|
+
// Re-raise the terminating signal so shells see the real cause.
|
|
25
|
+
process.kill(process.pid, signal);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
process.exit(code === null ? 1 : code);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Forward termination signals to the child so Ctrl+C behaves normally.
|
|
32
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
33
|
+
process.on(sig, () => {
|
|
34
|
+
if (!child.killed) {
|
|
35
|
+
try {
|
|
36
|
+
child.kill(sig);
|
|
37
|
+
} catch {
|
|
38
|
+
/* child already gone */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function main() {
|
|
46
|
+
const binaryPath = platform.installedBinaryPath();
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(binaryPath)) {
|
|
49
|
+
const { ensureBinary } = require("../scripts/download");
|
|
50
|
+
try {
|
|
51
|
+
await ensureBinary({ log: (m) => console.error(`[nanoagent] ${m}`) });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(`[nanoagent] Unable to download the NanoAgent CLI binary: ${err.message}`);
|
|
54
|
+
console.error("[nanoagent] Check your network connection and try running `nanoai` again.");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
launch(binaryPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nanoai-cli",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "Terminal UI, ACP server, and automation-friendly CLI for NanoAgent.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nanoagent",
|
|
7
|
+
"nanoai",
|
|
8
|
+
"ai",
|
|
9
|
+
"agent",
|
|
10
|
+
"cli",
|
|
11
|
+
"acp",
|
|
12
|
+
"coding-agent"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/rizwan3d/NanoAgent#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/rizwan3d/NanoAgent/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/rizwan3d/NanoAgent.git",
|
|
21
|
+
"directory": "packaging/npm"
|
|
22
|
+
},
|
|
23
|
+
"license": "Apache-2.0",
|
|
24
|
+
"author": "NanoAgent",
|
|
25
|
+
"type": "commonjs",
|
|
26
|
+
"bin": {
|
|
27
|
+
"nanoai": "bin/nanoai.js"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"postinstall": "node scripts/postinstall.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin/",
|
|
34
|
+
"scripts/",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"os": [
|
|
41
|
+
"win32",
|
|
42
|
+
"darwin",
|
|
43
|
+
"linux"
|
|
44
|
+
],
|
|
45
|
+
"cpu": [
|
|
46
|
+
"x64",
|
|
47
|
+
"arm64"
|
|
48
|
+
],
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"adm-zip": "^0.5.16"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Downloads, verifies, and extracts the NanoAgent CLI binary from the matching
|
|
4
|
+
// GitHub release. Shared by the postinstall step and the runtime launcher so the
|
|
5
|
+
// CLI self-heals on first run even when a package manager skips lifecycle
|
|
6
|
+
// scripts (notably `bun install`, which does not run postinstall by default).
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const crypto = require("crypto");
|
|
11
|
+
const AdmZip = require("adm-zip");
|
|
12
|
+
|
|
13
|
+
const platform = require("./platform");
|
|
14
|
+
|
|
15
|
+
async function fetchBuffer(url) {
|
|
16
|
+
if (typeof fetch !== "function") {
|
|
17
|
+
throw new Error(
|
|
18
|
+
"Global fetch is unavailable. NanoAgent's npm package requires Node.js 18 or newer."
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const response = await fetch(url, {
|
|
23
|
+
redirect: "follow",
|
|
24
|
+
headers: { "User-Agent": `${platform.APP_NAME}-npm-installer` },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`Request to ${url} failed with HTTP ${response.status} ${response.statusText}.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Buffer.from(await response.arrayBuffer());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseExpectedChecksum(checksumsText, assetName) {
|
|
35
|
+
for (const rawLine of checksumsText.split(/\r?\n/)) {
|
|
36
|
+
const line = rawLine.trim();
|
|
37
|
+
if (!line) continue;
|
|
38
|
+
|
|
39
|
+
const match = line.match(/^([0-9a-fA-F]{64})[\s*]+(.+)$/);
|
|
40
|
+
if (!match) continue;
|
|
41
|
+
|
|
42
|
+
let file = match[2].trim();
|
|
43
|
+
file = file.replace(/^\*/, "").replace(/^\.\//, "");
|
|
44
|
+
|
|
45
|
+
if (file === assetName) {
|
|
46
|
+
return match[1].toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sha256(buffer) {
|
|
53
|
+
return crypto.createHash("sha256").update(buffer).digest("hex").toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractExecutable(zipBuffer, destinationPath) {
|
|
57
|
+
const zip = new AdmZip(zipBuffer);
|
|
58
|
+
const wanted = platform.executableFileName();
|
|
59
|
+
|
|
60
|
+
const entry = zip.getEntries().find((candidate) => {
|
|
61
|
+
if (candidate.isDirectory) return false;
|
|
62
|
+
const base = candidate.entryName.split("/").pop();
|
|
63
|
+
return base === wanted;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!entry) {
|
|
67
|
+
throw new Error(`Release archive did not contain the expected executable '${wanted}'.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const data = entry.getData();
|
|
71
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
72
|
+
fs.writeFileSync(destinationPath, data);
|
|
73
|
+
if (process.platform !== "win32") {
|
|
74
|
+
fs.chmodSync(destinationPath, 0o755);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Ensures the platform binary is present in vendor/. Returns the absolute path.
|
|
79
|
+
async function ensureBinary(options = {}) {
|
|
80
|
+
const { force = false, log = () => {} } = options;
|
|
81
|
+
|
|
82
|
+
const binaryPath = platform.installedBinaryPath();
|
|
83
|
+
if (!force && fs.existsSync(binaryPath)) {
|
|
84
|
+
return binaryPath;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const rid = platform.resolveRid();
|
|
88
|
+
const asset = platform.assetName(rid);
|
|
89
|
+
const base = platform.baseDownloadUrl();
|
|
90
|
+
const assetUrl = `${base}/${asset}`;
|
|
91
|
+
const checksumsUrl = `${base}/${platform.CHECKSUMS_NAME}`;
|
|
92
|
+
|
|
93
|
+
log(`Downloading ${asset} (${platform.resolveTag()})...`);
|
|
94
|
+
const archiveBuffer = await fetchBuffer(assetUrl);
|
|
95
|
+
|
|
96
|
+
log(`Verifying ${platform.CHECKSUMS_NAME}...`);
|
|
97
|
+
const checksumsText = (await fetchBuffer(checksumsUrl)).toString("utf8");
|
|
98
|
+
const expected = parseExpectedChecksum(checksumsText, asset);
|
|
99
|
+
if (!expected) {
|
|
100
|
+
throw new Error(`${platform.CHECKSUMS_NAME} does not contain a checksum for ${asset}.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const actual = sha256(archiveBuffer);
|
|
104
|
+
if (actual !== expected) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`SHA256 verification failed for ${asset}. Expected ${expected}, got ${actual}.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
log("Extracting NanoAgent CLI...");
|
|
111
|
+
// Extract to a temp file first, then rename so concurrent runs never observe
|
|
112
|
+
// a partially written executable.
|
|
113
|
+
const tempPath = path.join(
|
|
114
|
+
platform.vendorDir(),
|
|
115
|
+
`.${platform.executableFileName()}.${process.pid}.tmp`
|
|
116
|
+
);
|
|
117
|
+
extractExecutable(archiveBuffer, tempPath);
|
|
118
|
+
fs.renameSync(tempPath, binaryPath);
|
|
119
|
+
|
|
120
|
+
log(`Installed NanoAgent CLI to ${binaryPath}.`);
|
|
121
|
+
return binaryPath;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { ensureBinary, parseExpectedChecksum };
|
|
125
|
+
|
|
126
|
+
// Allow `node scripts/download.js` for manual/forced reinstall.
|
|
127
|
+
if (require.main === module) {
|
|
128
|
+
ensureBinary({ force: true, log: (m) => console.error(`[nanoagent] ${m}`) }).catch((err) => {
|
|
129
|
+
console.error(`[nanoagent] ${err.message}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Shared platform/asset resolution used by both the postinstall step and the
|
|
4
|
+
// runtime launcher. Keep this dependency-free so it loads in any environment.
|
|
5
|
+
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
const OWNER = "rizwan3d";
|
|
10
|
+
const REPO = "NanoAgent";
|
|
11
|
+
const APP_NAME = "NanoAgent.CLI";
|
|
12
|
+
// Executable name inside the release archive (matches the AOT-published output).
|
|
13
|
+
const EXECUTABLE_NAME = "NanoAgent.CLI";
|
|
14
|
+
const CHECKSUMS_NAME = "SHA256SUMS";
|
|
15
|
+
|
|
16
|
+
// Maps Node's process.platform/process.arch onto the .NET runtime identifiers
|
|
17
|
+
// used for the published release assets.
|
|
18
|
+
function resolveRid() {
|
|
19
|
+
const platform = process.platform;
|
|
20
|
+
const arch = process.arch;
|
|
21
|
+
|
|
22
|
+
if (platform === "win32") {
|
|
23
|
+
if (arch === "x64") return "win-x64";
|
|
24
|
+
throw new Error(`Unsupported Windows architecture '${arch}'. NanoAgent ships win-x64 only.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (platform === "darwin") {
|
|
28
|
+
if (arch === "x64") return "osx-x64";
|
|
29
|
+
if (arch === "arm64") return "osx-arm64";
|
|
30
|
+
throw new Error(`Unsupported macOS architecture '${arch}'.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (platform === "linux") {
|
|
34
|
+
if (arch === "x64") return "linux-x64";
|
|
35
|
+
if (arch === "arm64") return "linux-arm64";
|
|
36
|
+
throw new Error(`Unsupported Linux architecture '${arch}'.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new Error(`Unsupported operating system '${platform}'.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function executableFileName() {
|
|
43
|
+
return process.platform === "win32" ? `${EXECUTABLE_NAME}.exe` : EXECUTABLE_NAME;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Version baked into package.json by the release workflow; the matching release
|
|
47
|
+
// is tagged "v<version>".
|
|
48
|
+
function resolveVersion() {
|
|
49
|
+
const override = process.env.NANOAGENT_CLI_VERSION;
|
|
50
|
+
if (override && override.trim()) {
|
|
51
|
+
return override.trim().replace(/^v/i, "");
|
|
52
|
+
}
|
|
53
|
+
const pkg = require(path.join(__dirname, "..", "package.json"));
|
|
54
|
+
return pkg.version;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveTag() {
|
|
58
|
+
const override = process.env.NANOAGENT_CLI_TAG;
|
|
59
|
+
if (override && override.trim()) {
|
|
60
|
+
return override.trim();
|
|
61
|
+
}
|
|
62
|
+
return `v${resolveVersion()}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function baseDownloadUrl() {
|
|
66
|
+
const override = process.env.NANOAGENT_CLI_BASE_URL;
|
|
67
|
+
if (override && override.trim()) {
|
|
68
|
+
return override.trim().replace(/\/+$/, "");
|
|
69
|
+
}
|
|
70
|
+
return `https://github.com/${OWNER}/${REPO}/releases/download/${resolveTag()}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assetName(rid) {
|
|
74
|
+
return `${APP_NAME}-${rid}.zip`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function vendorDir() {
|
|
78
|
+
return path.join(__dirname, "..", "vendor");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function installedBinaryPath() {
|
|
82
|
+
return path.join(vendorDir(), executableFileName());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
OWNER,
|
|
87
|
+
REPO,
|
|
88
|
+
APP_NAME,
|
|
89
|
+
EXECUTABLE_NAME,
|
|
90
|
+
CHECKSUMS_NAME,
|
|
91
|
+
resolveRid,
|
|
92
|
+
executableFileName,
|
|
93
|
+
resolveVersion,
|
|
94
|
+
resolveTag,
|
|
95
|
+
baseDownloadUrl,
|
|
96
|
+
assetName,
|
|
97
|
+
vendorDir,
|
|
98
|
+
installedBinaryPath,
|
|
99
|
+
homedir: os.homedir,
|
|
100
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Runs after `npm install`/`pnpm install`. Best-effort: if the download fails
|
|
4
|
+
// (offline, proxy, rate limit) we do NOT fail the install. The binary is fetched
|
|
5
|
+
// lazily on first `nanoai` invocation instead. This also keeps `bun install`
|
|
6
|
+
// working, since bun skips postinstall scripts by default.
|
|
7
|
+
|
|
8
|
+
// Skip in CI/build contexts that set npm_config_global=false dev installs of the
|
|
9
|
+
// monorepo, or when the user explicitly opts out.
|
|
10
|
+
if (process.env.NANOAGENT_SKIP_DOWNLOAD === "1") {
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { ensureBinary } = require("./download");
|
|
15
|
+
|
|
16
|
+
ensureBinary({ log: (m) => console.error(`[nanoagent] ${m}`) })
|
|
17
|
+
.then(() => {
|
|
18
|
+
console.error("[nanoagent] Ready. Run `nanoai` to start.");
|
|
19
|
+
})
|
|
20
|
+
.catch((err) => {
|
|
21
|
+
console.error(`[nanoagent] Could not pre-download the CLI binary: ${err.message}`);
|
|
22
|
+
console.error("[nanoagent] It will be downloaded automatically the first time you run `nanoai`.");
|
|
23
|
+
// Intentionally exit 0 so install succeeds.
|
|
24
|
+
});
|