@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 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nasebanal/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "gcloud-style command-line interface for NASEBANAL APIs, generated from nb-api-specs OpenAPI contracts.",
5
5
  "license": "MIT",
6
6
  "type": "module",