nanoai-cli 1.0.7 → 1.0.9

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
@@ -1,54 +1,113 @@
1
- # nanoagent
1
+ # nanoai-cli
2
2
 
3
- The NanoAgent CLI (`nanoai`) terminal UI, ACP server, and automation-friendly agent.
3
+ `nanoai-cli` installs the NanoAgent CLI as the `nanoai` command.
4
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.
5
+ NanoAgent is a local AI coding agent for terminal workflows, ACP-compatible editors, and automation. This npm package is a thin installer: it downloads the matching self-contained NanoAgent release for your platform, verifies it against the published `SHA256SUMS`, and launches it without requiring a .NET toolchain.
6
+
7
+ ## Why use this package
8
+
9
+ - Install NanoAgent with `npm`, `pnpm`, or `bun`.
10
+ - Download the correct native binary for the current platform automatically.
11
+ - Verify release archives before extraction with published SHA-256 checksums.
12
+ - Recover automatically on first run if `postinstall` was skipped or the binary is missing.
9
13
 
10
14
  ## Install
11
15
 
12
16
  ```bash
13
- npm install -g nanoagent
17
+ npm install -g nanoai-cli
14
18
  # or
15
- bun add -g nanoagent
19
+ pnpm add -g nanoai-cli
16
20
  # or
17
- pnpm add -g nanoagent
21
+ bun add -g nanoai-cli
18
22
  ```
19
23
 
20
- Then run:
24
+ Start NanoAgent:
21
25
 
22
26
  ```bash
23
27
  nanoai
24
28
  ```
25
29
 
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.
30
+ If you want a quick non-interactive smoke test after install:
31
+
32
+ ```bash
33
+ nanoai --version
34
+ ```
35
+
36
+ ## How installation works
37
+
38
+ On install, the package tries to:
39
+
40
+ 1. Resolve the correct release asset for the current OS and CPU architecture.
41
+ 2. Download the matching `NanoAgent.CLI-<rid>.zip` archive from GitHub Releases.
42
+ 3. Download `SHA256SUMS` from the same release.
43
+ 4. Verify the archive checksum before extraction.
44
+ 5. Extract the NanoAgent binary into the package's `vendor/` directory.
45
+
46
+ If the download is skipped or fails during `postinstall`, installation still succeeds. The launcher downloads the binary automatically the first time you run `nanoai`.
47
+
48
+ ## bun note
49
+
50
+ `bun add` skips `postinstall` scripts by default, so the binary is usually downloaded on first launch instead of during installation.
51
+
52
+ To fetch it eagerly after installing with bun, run:
53
+
54
+ ```bash
55
+ bunx nanoai-cli --version
56
+ ```
29
57
 
30
58
  ## Supported platforms
31
59
 
32
- | OS | Architecture |
33
- | ------- | ------------ |
34
- | Windows | x64 |
35
- | macOS | x64, arm64 |
36
- | Linux | x64, arm64 |
60
+ | OS | Architectures |
61
+ | --- | --- |
62
+ | Windows | x64 |
63
+ | macOS | x64, arm64 |
64
+ | Linux | x64, arm64 |
65
+
66
+ ## Updates
67
+
68
+ By default, the package downloads the release tag that matches the npm package version, using `v<package-version>`.
69
+
70
+ At runtime, the launcher can also check GitHub for a newer NanoAgent release. When running interactively, it prompts before replacing the installed binary with the latest release and then continues launch.
71
+
72
+ Skip the runtime update prompt with either:
73
+
74
+ ```bash
75
+ nanoai --no-update-check
76
+ ```
77
+
78
+ or:
79
+
80
+ ```bash
81
+ NANOAGENT_SKIP_UPDATE_CHECK=1 nanoai
82
+ ```
37
83
 
38
84
  ## Environment variables
39
85
 
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). |
86
+ | Variable | Purpose |
87
+ | --- | --- |
88
+ | `NANOAGENT_SKIP_DOWNLOAD` | Set to `1` to skip the install-time download. The binary will still be fetched on first run. |
89
+ | `NANOAGENT_SKIP_UPDATE_CHECK` | Set to `1` to disable the runtime check for newer GitHub releases. |
90
+ | `NANOAGENT_CLI_TAG` | Override the release tag to download, such as `v1.2.3`. |
91
+ | `NANOAGENT_CLI_VERSION` | Override the version used to derive the default release tag. |
92
+ | `NANOAGENT_CLI_BASE_URL` | Override the release asset base URL for mirrors, testing, or private distribution. |
93
+
94
+ ## Manual reinstall
45
95
 
46
- ## Reinstalling the binary
96
+ If you need to force a fresh binary download for a local install, run:
47
97
 
48
98
  ```bash
49
- node node_modules/nanoagent/scripts/download.js
99
+ node ./node_modules/nanoai-cli/scripts/download.js
50
100
  ```
51
101
 
102
+ If the package was installed globally, reinstalling the package is usually the simplest way to refresh the bundled launcher files.
103
+
104
+ ## Learn more
105
+
106
+ - Product overview: [NanoAgent README](https://github.com/rizwan3d/NanoAgent#readme)
107
+ - Full documentation: [docs/documentation.md](https://github.com/rizwan3d/NanoAgent/blob/master/docs/documentation.md)
108
+ - Releases: [GitHub Releases](https://github.com/rizwan3d/NanoAgent/releases)
109
+ - Issues: [GitHub Issues](https://github.com/rizwan3d/NanoAgent/issues)
110
+
52
111
  ## License
53
112
 
54
113
  Apache-2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nanoai-cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Terminal UI, ACP server, and automation-friendly CLI for NanoAgent.",
5
5
  "keywords": [
6
6
  "nanoagent",
@@ -47,6 +47,7 @@
47
47
  "arm64"
48
48
  ],
49
49
  "dependencies": {
50
+ "@inquirer/select": "^4.4.0",
50
51
  "adm-zip": "^0.5.16"
51
52
  }
52
53
  }
@@ -77,7 +77,7 @@ function extractExecutable(zipBuffer, destinationPath) {
77
77
 
78
78
  // Ensures the platform binary is present in vendor/. Returns the absolute path.
79
79
  async function ensureBinary(options = {}) {
80
- const { force = false, log = () => {} } = options;
80
+ const { force = false, log = () => {}, tag } = options;
81
81
 
82
82
  const binaryPath = platform.installedBinaryPath();
83
83
  if (!force && fs.existsSync(binaryPath)) {
@@ -86,11 +86,14 @@ async function ensureBinary(options = {}) {
86
86
 
87
87
  const rid = platform.resolveRid();
88
88
  const asset = platform.assetName(rid);
89
- const base = platform.baseDownloadUrl();
89
+ const resolvedTag = tag && tag.trim()
90
+ ? tag.trim()
91
+ : platform.resolveTag();
92
+ const base = platform.baseDownloadUrl(resolvedTag);
90
93
  const assetUrl = `${base}/${asset}`;
91
94
  const checksumsUrl = `${base}/${platform.CHECKSUMS_NAME}`;
92
95
 
93
- log(`Downloading ${asset} (${platform.resolveTag()})...`);
96
+ log(`Downloading ${asset} (${resolvedTag})...`);
94
97
  const archiveBuffer = await fetchBuffer(assetUrl);
95
98
 
96
99
  log(`Verifying ${platform.CHECKSUMS_NAME}...`);
@@ -62,12 +62,15 @@ function resolveTag() {
62
62
  return `v${resolveVersion()}`;
63
63
  }
64
64
 
65
- function baseDownloadUrl() {
65
+ function baseDownloadUrl(tagOverride) {
66
66
  const override = process.env.NANOAGENT_CLI_BASE_URL;
67
67
  if (override && override.trim()) {
68
68
  return override.trim().replace(/\/+$/, "");
69
69
  }
70
- return `https://github.com/${OWNER}/${REPO}/releases/download/${resolveTag()}`;
70
+ const tag = tagOverride && tagOverride.trim()
71
+ ? tagOverride.trim()
72
+ : resolveTag();
73
+ return `https://github.com/${OWNER}/${REPO}/releases/download/${tag}`;
71
74
  }
72
75
 
73
76
  function assetName(rid) {
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const { spawnSync } = require("child_process");
5
+
6
+ const platform = require("./platform");
7
+ const { ensureBinary } = require("./download");
8
+
9
+ const LatestReleaseApiUrl = `https://api.github.com/repos/${platform.OWNER}/${platform.REPO}/releases/latest`;
10
+ const VersionPattern = /\b(?:NanoAgent\s+CLI\s+)?v?(\d+(?:\.\d+){1,3}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\b/i;
11
+
12
+ function normalizeVersionText(value) {
13
+ if (!value || !value.trim()) {
14
+ return "0.0.0";
15
+ }
16
+
17
+ let normalized = value.trim();
18
+ const metadataIndex = normalized.indexOf("+");
19
+ if (metadataIndex >= 0) {
20
+ normalized = normalized.slice(0, metadataIndex);
21
+ }
22
+
23
+ if (normalized.startsWith("v") || normalized.startsWith("V")) {
24
+ normalized = normalized.slice(1);
25
+ }
26
+
27
+ return normalized;
28
+ }
29
+
30
+ function parseComparableVersion(value) {
31
+ const normalized = normalizeVersionText(value);
32
+ const prereleaseIndex = normalized.indexOf("-");
33
+ const releasePart = prereleaseIndex >= 0
34
+ ? normalized.slice(0, prereleaseIndex)
35
+ : normalized;
36
+ const segments = releasePart
37
+ .split(".")
38
+ .map((segment) => Number.parseInt(segment, 10));
39
+
40
+ if (segments.length === 0 || segments.some((segment) => !Number.isFinite(segment))) {
41
+ return null;
42
+ }
43
+
44
+ while (segments.length < 4) {
45
+ segments.push(0);
46
+ }
47
+
48
+ return {
49
+ normalized,
50
+ segments,
51
+ hasPrerelease: prereleaseIndex >= 0,
52
+ };
53
+ }
54
+
55
+ function compareVersions(left, right) {
56
+ const parsedLeft = parseComparableVersion(left);
57
+ const parsedRight = parseComparableVersion(right);
58
+
59
+ if (!parsedLeft || !parsedRight) {
60
+ const normalizedLeft = normalizeVersionText(left);
61
+ const normalizedRight = normalizeVersionText(right);
62
+ return normalizedLeft.localeCompare(normalizedRight, undefined, { numeric: true, sensitivity: "base" });
63
+ }
64
+
65
+ for (let index = 0; index < Math.max(parsedLeft.segments.length, parsedRight.segments.length); index += 1) {
66
+ const leftSegment = parsedLeft.segments[index] ?? 0;
67
+ const rightSegment = parsedRight.segments[index] ?? 0;
68
+ if (leftSegment !== rightSegment) {
69
+ return leftSegment - rightSegment;
70
+ }
71
+ }
72
+
73
+ if (parsedLeft.hasPrerelease !== parsedRight.hasPrerelease) {
74
+ return parsedLeft.hasPrerelease ? -1 : 1;
75
+ }
76
+
77
+ return parsedLeft.normalized.localeCompare(
78
+ parsedRight.normalized,
79
+ undefined,
80
+ { numeric: true, sensitivity: "base" }
81
+ );
82
+ }
83
+
84
+ async function fetchLatestRelease(options = {}) {
85
+ const { timeoutMs = 4000 } = options;
86
+ if (typeof fetch !== "function") {
87
+ return null;
88
+ }
89
+
90
+ const response = await fetch(LatestReleaseApiUrl, {
91
+ headers: {
92
+ "Accept": "application/vnd.github+json",
93
+ "User-Agent": `${platform.APP_NAME}-npm-launcher`,
94
+ },
95
+ redirect: "follow",
96
+ signal: typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function"
97
+ ? AbortSignal.timeout(timeoutMs)
98
+ : undefined,
99
+ });
100
+
101
+ if (!response.ok) {
102
+ throw new Error(`GitHub returned HTTP ${response.status} ${response.statusText}.`);
103
+ }
104
+
105
+ const payload = await response.json();
106
+ const tag = typeof payload?.tag_name === "string" ? payload.tag_name.trim() : "";
107
+ if (!tag) {
108
+ throw new Error("GitHub did not return a release tag.");
109
+ }
110
+
111
+ const releaseUrl = typeof payload?.html_url === "string" && payload.html_url.trim()
112
+ ? payload.html_url.trim()
113
+ : `https://github.com/${platform.OWNER}/${platform.REPO}/releases/latest`;
114
+
115
+ return {
116
+ tag,
117
+ version: normalizeVersionText(tag),
118
+ releaseUrl,
119
+ };
120
+ }
121
+
122
+ function readInstalledBinaryVersion(binaryPath) {
123
+ if (!binaryPath || !fs.existsSync(binaryPath)) {
124
+ return null;
125
+ }
126
+
127
+ try {
128
+ const result = spawnSync(binaryPath, ["--version"], {
129
+ encoding: "utf8",
130
+ stdio: ["ignore", "pipe", "pipe"],
131
+ timeout: 10000,
132
+ windowsHide: true,
133
+ });
134
+
135
+ const combined = `${result.stdout || ""}\n${result.stderr || ""}`;
136
+ const match = combined.match(VersionPattern);
137
+ return match?.[1] ? normalizeVersionText(match[1]) : null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ function shouldSkipRuntimeUpdateCheck() {
144
+ if (process.env.NANOAGENT_SKIP_UPDATE_CHECK === "1") {
145
+ return true;
146
+ }
147
+
148
+ if (process.env.NANOAGENT_CLI_TAG || process.env.NANOAGENT_CLI_BASE_URL || process.env.NANOAGENT_CLI_VERSION) {
149
+ return true;
150
+ }
151
+
152
+ return process.argv.slice(2).some((arg) => arg === "--no-update-check");
153
+ }
154
+
155
+ function canPromptForUpdate() {
156
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
157
+ }
158
+
159
+ async function promptForUpdate(currentVersion, latestVersion) {
160
+ const { default: select } = await import("@inquirer/select");
161
+
162
+ return await select({
163
+ message: `NanoAgent ${latestVersion} is available. Update before launch?`,
164
+ choices: [
165
+ {
166
+ name: `Yes, update from ${currentVersion} to ${latestVersion}`,
167
+ value: true,
168
+ description: "Downloads the latest NanoAgent CLI binary, then starts nanoai.",
169
+ },
170
+ {
171
+ name: `No, continue with ${currentVersion}`,
172
+ value: false,
173
+ description: "Skip this update check and launch the currently installed binary.",
174
+ },
175
+ ],
176
+ default: false,
177
+ loop: false,
178
+ });
179
+ }
180
+
181
+ async function maybeUpdateBinary(binaryPath, options = {}) {
182
+ const { log = () => {} } = options;
183
+
184
+ if (shouldSkipRuntimeUpdateCheck()) {
185
+ return binaryPath;
186
+ }
187
+
188
+ let latestRelease;
189
+ try {
190
+ latestRelease = await fetchLatestRelease();
191
+ } catch {
192
+ return binaryPath;
193
+ }
194
+
195
+ if (!latestRelease) {
196
+ return binaryPath;
197
+ }
198
+
199
+ const currentVersion = readInstalledBinaryVersion(binaryPath) || normalizeVersionText(platform.resolveVersion());
200
+ if (compareVersions(latestRelease.version, currentVersion) <= 0) {
201
+ return binaryPath;
202
+ }
203
+
204
+ if (!canPromptForUpdate()) {
205
+ return binaryPath;
206
+ }
207
+
208
+ let shouldUpdate;
209
+ try {
210
+ shouldUpdate = await promptForUpdate(currentVersion, latestRelease.version);
211
+ } catch {
212
+ return binaryPath;
213
+ }
214
+
215
+ if (!shouldUpdate) {
216
+ return binaryPath;
217
+ }
218
+
219
+ log(`Updating NanoAgent CLI to ${latestRelease.tag}...`);
220
+
221
+ try {
222
+ return await ensureBinary({
223
+ force: true,
224
+ tag: latestRelease.tag,
225
+ log,
226
+ });
227
+ } catch (error) {
228
+ const message = error && error.message
229
+ ? error.message
230
+ : String(error);
231
+ log(`Update failed: ${message}`);
232
+ log("Starting the currently installed NanoAgent CLI instead.");
233
+ return binaryPath;
234
+ }
235
+ }
236
+
237
+ module.exports = {
238
+ compareVersions,
239
+ fetchLatestRelease,
240
+ maybeUpdateBinary,
241
+ normalizeVersionText,
242
+ readInstalledBinaryVersion,
243
+ shouldSkipRuntimeUpdateCheck,
244
+ };
package/bin/nanoai.js DELETED
@@ -1,62 +0,0 @@
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();