@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 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rbbtsn0w/adg",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Agent Directory Group (ADG) toolkit — two domains: plugins and skills.",
5
5
  "type": "module",
6
6
  "license": "MIT",