@opennous/cli 0.12.2 → 0.12.3
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/CHANGELOG.md +11 -0
- package/package.json +2 -1
- package/src/certHint.js +46 -0
- package/src/index.js +23 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @opennous/cli changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.3
|
|
4
|
+
|
|
5
|
+
### Fixed: a plain message when the server's certificate isn't trusted
|
|
6
|
+
|
|
7
|
+
Against a self-hosted install with Caddy's local certificate, the CLI used to crash with
|
|
8
|
+
a Node stack trace (`UNABLE_TO_GET_ISSUER_CERT_LOCALLY`). It now says to point
|
|
9
|
+
`NODE_EXTRA_CA_CERTS` at the root certificate and links the self-host guide (GET-1011).
|
|
10
|
+
|
|
11
|
+
On Windows, where `api.localhost` doesn't resolve until the hosts file names it, it says which
|
|
12
|
+
line to add (GET-1015).
|
|
13
|
+
|
|
3
14
|
## 0.10.1
|
|
4
15
|
|
|
5
16
|
### Changed — new default vault folder structure
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennous/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"description": "Nous CLI — set up Nous, connect it to your coding agent, and read the Context API from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"dev": "node src/index.js",
|
|
16
|
+
"test": "node ../../scripts/run-tests.mjs",
|
|
16
17
|
"start": "node src/index.js"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
package/src/certHint.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// A self-hosted Nous on a laptop serves https://api.localhost with Caddy's own local CA, which
|
|
2
|
+
// Node does not trust. fetch then rejects with a bare "fetch failed" whose cause carries one of
|
|
3
|
+
// these codes. Name the fix in one line instead of printing a stack trace (GET-1011).
|
|
4
|
+
const UNTRUSTED_CERT_CODES = new Set([
|
|
5
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
6
|
+
"UNABLE_TO_GET_ISSUER_CERT",
|
|
7
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
8
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
9
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export const CERT_DOCS_URL =
|
|
13
|
+
"https://github.com/NousC/opennous/blob/main/docs/self-host-bundled.md#trying-it-on-a-laptop";
|
|
14
|
+
|
|
15
|
+
/** The cert code on an error or anywhere down its `cause` chain, else null. */
|
|
16
|
+
export function untrustedCertCode(err) {
|
|
17
|
+
for (let e = err, depth = 0; e && depth < 5; e = e.cause, depth++) {
|
|
18
|
+
if (typeof e.code === "string" && UNTRUSTED_CERT_CODES.has(e.code)) return e.code;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The *.localhost name on a "not found" error down the cause chain, else null. */
|
|
24
|
+
function unresolvedLocalhost(err) {
|
|
25
|
+
for (let e = err, depth = 0; e && depth < 5; e = e.cause, depth++) {
|
|
26
|
+
if (e.code === "ENOTFOUND" && /\.localhost$/i.test(e.hostname || "")) return e.hostname;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One line naming the fix for a laptop self-host the CLI can't reach (untrusted certificate, or a
|
|
32
|
+
* *.localhost name Windows doesn't resolve), or null when the error is something else. */
|
|
33
|
+
export function certHint(err) {
|
|
34
|
+
// Windows resolves only "localhost" itself; the subdomains need the hosts file (GET-1015).
|
|
35
|
+
const host = unresolvedLocalhost(err);
|
|
36
|
+
if (host) {
|
|
37
|
+
return `Couldn't connect: ${host} doesn't resolve on this machine (Windows only resolves "localhost" itself). ` +
|
|
38
|
+
`Add "127.0.0.1 api.localhost mcp.localhost" to your hosts file and run it again. ` +
|
|
39
|
+
`How: ${CERT_DOCS_URL}`;
|
|
40
|
+
}
|
|
41
|
+
const code = untrustedCertCode(err);
|
|
42
|
+
if (!code) return null;
|
|
43
|
+
return `Couldn't connect: this server's TLS certificate isn't trusted by Node (${code}). ` +
|
|
44
|
+
`Set NODE_EXTRA_CA_CERTS to your server's CA certificate (for a local self-host, Caddy's root.crt) and run it again. ` +
|
|
45
|
+
`How: ${CERT_DOCS_URL}`;
|
|
46
|
+
}
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,16 @@ import { homedir, hostname } from "os";
|
|
|
12
12
|
import { join, resolve, dirname, relative, sep } from "path";
|
|
13
13
|
import { createInterface } from "readline";
|
|
14
14
|
import { spawn, spawnSync } from "child_process";
|
|
15
|
+
import { certHint } from "./certHint.js";
|
|
16
|
+
|
|
17
|
+
// An untrusted certificate (a local self-host behind Caddy's own CA) surfaces as a rejected
|
|
18
|
+
// fetch somewhere in an async action. Print the one-line fix instead of a stack trace; anything
|
|
19
|
+
// else still fails the way it always did.
|
|
20
|
+
process.on("unhandledRejection", (err) => {
|
|
21
|
+
const hint = certHint(err);
|
|
22
|
+
if (hint) { console.error(hint); process.exit(1); }
|
|
23
|
+
throw err;
|
|
24
|
+
});
|
|
15
25
|
|
|
16
26
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
17
27
|
|
|
@@ -32,7 +42,7 @@ async function fetchWhoami(apiUrl, key) {
|
|
|
32
42
|
return await r.json();
|
|
33
43
|
} catch { return null; }
|
|
34
44
|
}
|
|
35
|
-
// "
|
|
45
|
+
// "Theo Bramwell · Fernpoint · admin · AE"
|
|
36
46
|
function identityLine(who) {
|
|
37
47
|
const y = who?.you || {};
|
|
38
48
|
return [
|
|
@@ -114,11 +124,17 @@ function apiClient() {
|
|
|
114
124
|
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
115
125
|
}
|
|
116
126
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
let res;
|
|
128
|
+
try {
|
|
129
|
+
res = await fetch(url.toString(), {
|
|
130
|
+
method,
|
|
131
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
132
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
133
|
+
});
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error(certHint(e) || `Couldn't reach ${apiUrl}: ${e.message}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
122
138
|
if (!res.ok) {
|
|
123
139
|
const err = await res.json().catch(() => ({ message: res.statusText }));
|
|
124
140
|
console.error(`Error: ${err.error || err.message}`);
|
|
@@ -245,7 +261,7 @@ async function browserLogin(url, opts = {}) {
|
|
|
245
261
|
}
|
|
246
262
|
start = await r.json();
|
|
247
263
|
} catch (e) {
|
|
248
|
-
console.error(`Couldn't start: ${e.message}`);
|
|
264
|
+
console.error(certHint(e) || `Couldn't start: ${e.message}`);
|
|
249
265
|
process.exit(1);
|
|
250
266
|
}
|
|
251
267
|
|