@rbbtsn0w/adg 0.1.1 → 0.2.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/dist/bin/adg.js +33 -1
- package/dist/src/update-check.js +118 -0
- package/package.json +1 -1
package/dist/bin/adg.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { realpathSync } from "node:fs";
|
|
4
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { checkForUpdate, formatUpdateNotice } from "../src/update-check.js";
|
|
7
8
|
import { ADAPTER_TARGETS } from "../src/adapters/index.js";
|
|
8
9
|
import { initScaffold } from "../src/commands/init.js";
|
|
9
10
|
import { adaptPlugin } from "../src/commands/adapt.js";
|
|
@@ -659,12 +660,43 @@ function runSkills(verb, rest) {
|
|
|
659
660
|
const r = spawnSync(process.execPath, skillsChildArgv(entry, args), { stdio: "inherit" });
|
|
660
661
|
process.exit(r.status ?? 1);
|
|
661
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* Read the package version from package.json.
|
|
665
|
+
*
|
|
666
|
+
* Works in both source mode (`bin/adg.ts` → package.json is 1 level up) and
|
|
667
|
+
* compiled mode (`dist/bin/adg.js` → package.json is 2 levels up).
|
|
668
|
+
*/
|
|
669
|
+
export function getVersion() {
|
|
670
|
+
const self = fileURLToPath(import.meta.url);
|
|
671
|
+
// Source: bin/adg.ts → up 1 level reaches the repo root.
|
|
672
|
+
// Compiled: dist/bin/adg.js → up 2 levels reaches the repo root.
|
|
673
|
+
const up = self.endsWith(".ts") ? ".." : join("..", "..");
|
|
674
|
+
const pkg = JSON.parse(readFileSync(join(dirname(self), up, "package.json"), "utf8"));
|
|
675
|
+
return pkg.version;
|
|
676
|
+
}
|
|
662
677
|
async function main(argv) {
|
|
663
678
|
const [domain, verb, ...rest] = argv;
|
|
679
|
+
// --version / -v at the root level: print version and exit.
|
|
680
|
+
// Note: `-v` is also the short flag for `--verbose` in subcommands, but only
|
|
681
|
+
// when it appears *after* a domain (e.g. `adg plugins list -v`). Checking
|
|
682
|
+
// argv[0] here means we only intercept `adg -v` / `adg --version`, never
|
|
683
|
+
// a subcommand's own flags.
|
|
684
|
+
if (domain === "--version" || domain === "-v") {
|
|
685
|
+
console.log(getVersion());
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
664
688
|
if (!domain || domain === "help" || domain === "--help" || domain === "-h") {
|
|
665
689
|
console.log(TOP_USAGE);
|
|
666
690
|
return;
|
|
667
691
|
}
|
|
692
|
+
// Check for an available update (reads local cache; schedules a background
|
|
693
|
+
// network refresh when the cache is stale — the refresh uses an unreffed
|
|
694
|
+
// socket so it cannot delay process exit).
|
|
695
|
+
const currentVersion = getVersion();
|
|
696
|
+
const latestVersion = checkForUpdate(currentVersion);
|
|
697
|
+
if (latestVersion) {
|
|
698
|
+
process.stderr.write(formatUpdateNotice(currentVersion, latestVersion));
|
|
699
|
+
}
|
|
668
700
|
switch (domain) {
|
|
669
701
|
case "plugins":
|
|
670
702
|
case "plugin": // tolerated alias
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-blocking update check for the ADG CLI.
|
|
3
|
+
*
|
|
4
|
+
* On every invocation we read a local cache file to decide whether to show an
|
|
5
|
+
* "update available" notice. When the cache is stale (older than 24 h) we
|
|
6
|
+
* schedule a background HTTP request — using an unreffed socket so it cannot
|
|
7
|
+
* block the process from exiting — that refreshes the cache for the *next* run.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import https from "node:https";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { compare, parseVersion } from "./semver.js";
|
|
14
|
+
const PACKAGE_NAME = "@rbbtsn0w/adg";
|
|
15
|
+
// URL-encode the slash in the scoped package name for the npm registry API.
|
|
16
|
+
const REGISTRY_URL = `https://registry.npmjs.org/@rbbtsn0w%2Fadg/latest`;
|
|
17
|
+
const CACHE_FILENAME = "update-check.json";
|
|
18
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
19
|
+
/** Resolve the directory that holds the update-check cache file. */
|
|
20
|
+
export function updateCacheDir(env = process.env) {
|
|
21
|
+
const stateHome = env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
22
|
+
return join(stateHome, "adg");
|
|
23
|
+
}
|
|
24
|
+
function cachePath(env = process.env) {
|
|
25
|
+
return join(updateCacheDir(env), CACHE_FILENAME);
|
|
26
|
+
}
|
|
27
|
+
/** Read the on-disk cache, returning null on any error. */
|
|
28
|
+
export function readUpdateCache(env = process.env) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = readFileSync(cachePath(env), "utf8");
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Write the cache, creating the directory if needed. Silently ignores errors. */
|
|
38
|
+
export function writeUpdateCache(cache, env = process.env) {
|
|
39
|
+
try {
|
|
40
|
+
const dir = updateCacheDir(env);
|
|
41
|
+
if (!existsSync(dir))
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
writeFileSync(cachePath(env), JSON.stringify(cache), "utf8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Ignore write errors (read-only FS, permissions, etc.)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Fire-and-forget background fetch of the latest version from the npm registry.
|
|
51
|
+
* The socket is unreffed so Node can exit naturally without waiting for the
|
|
52
|
+
* request to complete — the cache will be refreshed on the *next* run.
|
|
53
|
+
*/
|
|
54
|
+
export function scheduleUpdateCacheRefresh(currentVersion, env = process.env) {
|
|
55
|
+
try {
|
|
56
|
+
const req = https.get(REGISTRY_URL, { headers: { "User-Agent": `adg/${currentVersion}`, Accept: "application/json" } }, (res) => {
|
|
57
|
+
let body = "";
|
|
58
|
+
res.on("data", (chunk) => { body += String(chunk); });
|
|
59
|
+
res.on("end", () => {
|
|
60
|
+
try {
|
|
61
|
+
const data = JSON.parse(body);
|
|
62
|
+
if (typeof data.version === "string") {
|
|
63
|
+
writeUpdateCache({ latestVersion: data.version, checkedAt: new Date().toISOString() }, env);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Ignore parse errors
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
res.resume(); // drain the response so the socket is released
|
|
71
|
+
});
|
|
72
|
+
// Destroy the request after 5 s to avoid a long-running socket that
|
|
73
|
+
// could delay the next run's check (not the current run — the socket is
|
|
74
|
+
// unreffed so the process exits freely).
|
|
75
|
+
req.setTimeout(5000, () => req.destroy());
|
|
76
|
+
req.on("error", () => { }); // Ignore network errors
|
|
77
|
+
// Unref as soon as the underlying socket is assigned so the request does
|
|
78
|
+
// not keep the event loop alive after the command finishes.
|
|
79
|
+
req.on("socket", (socket) => socket.unref());
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// If https.get itself throws, ignore it silently.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check whether an update is available and print a notice if so.
|
|
87
|
+
*
|
|
88
|
+
* Reads the local cache synchronously (fast, no network) and, when the cache
|
|
89
|
+
* is stale, schedules a background refresh for the next invocation.
|
|
90
|
+
*
|
|
91
|
+
* @param currentVersion The version string from package.json (e.g. "0.1.1").
|
|
92
|
+
* @returns The newer version string if an update is available, otherwise undefined.
|
|
93
|
+
*/
|
|
94
|
+
export function checkForUpdate(currentVersion, env = process.env, refresh = scheduleUpdateCacheRefresh) {
|
|
95
|
+
const cache = readUpdateCache(env);
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const checkedAt = cache ? new Date(cache.checkedAt).getTime() : 0;
|
|
98
|
+
const checkedAtMs = Number.isFinite(checkedAt) ? checkedAt : 0;
|
|
99
|
+
const isStale = !cache || now - checkedAtMs > CACHE_TTL_MS;
|
|
100
|
+
if (isStale) {
|
|
101
|
+
refresh(currentVersion, env);
|
|
102
|
+
}
|
|
103
|
+
if (!cache)
|
|
104
|
+
return undefined;
|
|
105
|
+
try {
|
|
106
|
+
const current = parseVersion(currentVersion);
|
|
107
|
+
const latest = parseVersion(cache.latestVersion);
|
|
108
|
+
return compare(latest, current) > 0 ? cache.latestVersion : undefined;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Format an update notice for display on stderr. */
|
|
115
|
+
export function formatUpdateNotice(currentVersion, latestVersion) {
|
|
116
|
+
return (`\n Update available: ${currentVersion} → ${latestVersion}\n` +
|
|
117
|
+
` Run: npm install -g ${PACKAGE_NAME}@latest\n`);
|
|
118
|
+
}
|