@nasebanal/cli 0.5.0 → 0.6.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.
- package/README.md +14 -0
- package/dist/config.js +1 -1
- package/dist/index.js +4 -0
- package/dist/update.js +127 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,6 +117,20 @@ needed for a registry install.
|
|
|
117
117
|
> `api-specs-*` packages), it captures this package too. Install with
|
|
118
118
|
> `npm install -g @nasebanal/cli --@nasebanal:registry=https://registry.npmjs.org/`.
|
|
119
119
|
|
|
120
|
+
### Upgrading
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
nb upgrade # install the latest published version
|
|
124
|
+
nb upgrade --check # only report whether a newer version exists
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
When a newer version is out, interactive runs print a one-line notice on stderr
|
|
128
|
+
(`Update available: 0.5.0 -> 0.6.0`). The registry is checked at most once a day
|
|
129
|
+
and the result is cached in `~/.config/nasebanal/update-check.json`. Nothing is
|
|
130
|
+
printed when stderr is not a terminal or `CI` is set; `NB_NO_UPDATE_NOTIFIER=1`
|
|
131
|
+
turns the notice off. A dev link to a source checkout is never overwritten:
|
|
132
|
+
`nb upgrade` tells you to `git pull && npm run build` instead.
|
|
133
|
+
|
|
120
134
|
### Versioning and releases
|
|
121
135
|
|
|
122
136
|
Versions follow SemVer (0.x until 1.0: breaking changes bump the minor). Each
|
package/dist/config.js
CHANGED
|
@@ -12,7 +12,7 @@ import { homedir } from "node:os";
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { mkdirSync, readFileSync, writeFileSync, chmodSync, existsSync } from "node:fs";
|
|
14
14
|
import { refreshTokens } from "./oauth.js";
|
|
15
|
-
function configDir() {
|
|
15
|
+
export function configDir() {
|
|
16
16
|
const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
17
17
|
return join(base, "nasebanal");
|
|
18
18
|
}
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { buildAuthCommand } from "./auth.js";
|
|
|
14
14
|
import { specExists } from "./spec/loader.js";
|
|
15
15
|
import { printError } from "./output.js";
|
|
16
16
|
import { buildReportUploadCommand } from "./reports/upload.js";
|
|
17
|
+
import { buildUpgradeCommand, startUpdateNotifier } from "./update.js";
|
|
17
18
|
// Single source of truth for the version: package.json (dist/ and src/ both sit
|
|
18
19
|
// next to it). A hard-coded string here drifts from the release tag.
|
|
19
20
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
@@ -26,6 +27,7 @@ program
|
|
|
26
27
|
.option("--token <token>", "override the stored PAT for this invocation")
|
|
27
28
|
.showHelpAfterError();
|
|
28
29
|
program.addCommand(buildAuthCommand());
|
|
30
|
+
program.addCommand(buildUpgradeCommand(version));
|
|
29
31
|
for (const api of API_CATALOG) {
|
|
30
32
|
if (!specExists(api.spec)) {
|
|
31
33
|
// Spec not synced yet — register a stub that explains how to fix it rather
|
|
@@ -50,8 +52,10 @@ for (const api of API_CATALOG) {
|
|
|
50
52
|
program.addCommand(group);
|
|
51
53
|
}
|
|
52
54
|
async function main() {
|
|
55
|
+
const notifier = startUpdateNotifier(version, process.argv);
|
|
53
56
|
try {
|
|
54
57
|
await program.parseAsync(process.argv);
|
|
58
|
+
await notifier.refresh();
|
|
55
59
|
}
|
|
56
60
|
catch (err) {
|
|
57
61
|
printError(err);
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-update: a passive "new version available" notice and `nb upgrade`.
|
|
3
|
+
*
|
|
4
|
+
* Follows the usual CLI convention (npm, gh, gcloud): the notice is a single
|
|
5
|
+
* line on stderr, shown at most once a day from a cached registry lookup, and
|
|
6
|
+
* silent for non-interactive use (pipes, CI) so scripted output never changes.
|
|
7
|
+
* `NB_NO_UPDATE_NOTIFIER=1` turns it off.
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { Command } from "commander";
|
|
13
|
+
import { configDir } from "./config.js";
|
|
14
|
+
const PACKAGE = "@nasebanal/cli";
|
|
15
|
+
const REGISTRY = "https://registry.npmjs.org";
|
|
16
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
const FETCH_TIMEOUT_MS = 1500;
|
|
18
|
+
/** 1 if a > b, -1 if a < b, 0 if equal (plain `x.y.z`; pre-release tags are ignored). */
|
|
19
|
+
export function compareVersions(a, b) {
|
|
20
|
+
const pa = a.split("-")[0].split(".").map(Number);
|
|
21
|
+
const pb = b.split("-")[0].split(".").map(Number);
|
|
22
|
+
for (let i = 0; i < 3; i++) {
|
|
23
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
24
|
+
if (d !== 0)
|
|
25
|
+
return d > 0 ? 1 : -1;
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
function cachePath() {
|
|
30
|
+
return join(configDir(), "update-check.json");
|
|
31
|
+
}
|
|
32
|
+
function readCache() {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(readFileSync(cachePath(), "utf8"));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function writeCache(cache) {
|
|
41
|
+
try {
|
|
42
|
+
mkdirSync(configDir(), { recursive: true });
|
|
43
|
+
writeFileSync(cachePath(), JSON.stringify(cache) + "\n");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* best effort */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Latest published version. Goes to the public registry directly, so a `~/.npmrc` scope mapping cannot hide it. */
|
|
50
|
+
export async function fetchLatest(timeoutMs = FETCH_TIMEOUT_MS) {
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`${REGISTRY}/${PACKAGE.replace("/", "%2F")}/latest`, {
|
|
53
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok)
|
|
56
|
+
return undefined;
|
|
57
|
+
return (await res.json()).version;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function notifierEnabled() {
|
|
64
|
+
return !process.env.NB_NO_UPDATE_NOTIFIER && !process.env.CI && !!process.stderr.isTTY;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Arrange for the update notice to print when the process exits. Reads the
|
|
68
|
+
* cache synchronously; when it is older than a day, refreshes it afterwards
|
|
69
|
+
* (bounded by a short timeout) so the notice appears from the next run on.
|
|
70
|
+
*/
|
|
71
|
+
export function startUpdateNotifier(current, argv) {
|
|
72
|
+
const noop = { refresh: async () => { } };
|
|
73
|
+
if (!notifierEnabled() || argv.slice(2).some((a) => a === "upgrade" || a === "--version" || a === "-V"))
|
|
74
|
+
return noop;
|
|
75
|
+
const cache = readCache();
|
|
76
|
+
if (cache?.latest && compareVersions(cache.latest, current) > 0) {
|
|
77
|
+
const latest = cache.latest;
|
|
78
|
+
process.on("exit", () => {
|
|
79
|
+
process.stderr.write(`\nUpdate available: ${current} -> ${latest}. Run \`nb upgrade\` to install it.\n`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const stale = !cache || Date.now() - cache.checkedAt > CHECK_INTERVAL_MS;
|
|
83
|
+
return {
|
|
84
|
+
refresh: async () => {
|
|
85
|
+
if (!stale)
|
|
86
|
+
return;
|
|
87
|
+
const latest = await fetchLatest();
|
|
88
|
+
// Record the attempt even on failure so an offline machine is not retried on every command.
|
|
89
|
+
writeCache({ checkedAt: Date.now(), latest: latest ?? cache?.latest });
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** True when `nb` is a dev link to a checkout rather than an npm-installed copy. */
|
|
94
|
+
function isLinkedCheckout() {
|
|
95
|
+
try {
|
|
96
|
+
return !realpathSync(process.argv[1]).includes("node_modules");
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function buildUpgradeCommand(current) {
|
|
103
|
+
return new Command("upgrade")
|
|
104
|
+
.description("Upgrade nb to the latest published version")
|
|
105
|
+
.option("--check", "only report whether a newer version exists")
|
|
106
|
+
.action(async (opts) => {
|
|
107
|
+
const latest = await fetchLatest(5000);
|
|
108
|
+
if (!latest)
|
|
109
|
+
throw new Error("Could not reach the npm registry to look up the latest version.");
|
|
110
|
+
writeCache({ checkedAt: Date.now(), latest });
|
|
111
|
+
if (compareVersions(latest, current) <= 0) {
|
|
112
|
+
process.stdout.write(`nb ${current} is up to date.\n`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
process.stdout.write(`Update available: ${current} -> ${latest}\n`);
|
|
116
|
+
if (opts.check)
|
|
117
|
+
return;
|
|
118
|
+
if (isLinkedCheckout()) {
|
|
119
|
+
process.stdout.write("This nb is linked to a source checkout: `git pull && npm run build` there instead.\n");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const r = spawnSync("npm", ["install", "-g", `${PACKAGE}@${latest}`, `--@nasebanal:registry=${REGISTRY}/`], { stdio: "inherit" });
|
|
123
|
+
if (r.status !== 0) {
|
|
124
|
+
throw new Error(`npm install failed. Run it yourself: npm install -g ${PACKAGE}@latest --@nasebanal:registry=${REGISTRY}/`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|