@theholocron/cli 2.0.0-alpha.74 → 2.0.0-alpha.75

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.
Files changed (2) hide show
  1. package/dist/cli.mjs +115 -0
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -16,6 +16,7 @@ import { execFile, execFileSync, spawnSync } from "node:child_process";
16
16
  import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
17
17
  import { pathToFileURL } from "node:url";
18
18
  import { promisify } from "node:util";
19
+ import { homedir } from "node:os";
19
20
  //#region src/capabilities/index.ts
20
21
  const CARDINALITY = {
21
22
  source: "single",
@@ -4113,6 +4114,118 @@ function parseTokenArgs(tokens) {
4113
4114
  return result;
4114
4115
  }
4115
4116
  //#endregion
4117
+ //#region src/update-notifier.ts
4118
+ const PACKAGE_NAME = "@theholocron/cli";
4119
+ const CACHE_TTL_MS = 1440 * 60 * 1e3;
4120
+ const FETCH_TIMEOUT_MS = 3e3;
4121
+ function getCacheDir() {
4122
+ return process.env["HOLOCRON_CACHE_DIR"] ?? join(homedir(), ".cache", "holocron");
4123
+ }
4124
+ function getCachePath() {
4125
+ return join(getCacheDir(), "update-check.json");
4126
+ }
4127
+ function readCache() {
4128
+ try {
4129
+ return JSON.parse(readFileSync(getCachePath(), "utf8"));
4130
+ } catch {
4131
+ return null;
4132
+ }
4133
+ }
4134
+ function writeCache(entry) {
4135
+ try {
4136
+ mkdirSync(getCacheDir(), { recursive: true });
4137
+ writeFileSync(getCachePath(), JSON.stringify(entry));
4138
+ } catch {}
4139
+ }
4140
+ async function fetchLatestVersion(channel) {
4141
+ try {
4142
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
4143
+ if (!res.ok) return null;
4144
+ const data = await res.json();
4145
+ return data["dist-tags"][channel] ?? data["dist-tags"]["latest"] ?? null;
4146
+ } catch {
4147
+ return null;
4148
+ }
4149
+ }
4150
+ function getChannel(version) {
4151
+ return /^[^-]+-([a-zA-Z]+)/.exec(version)?.[1] ?? "latest";
4152
+ }
4153
+ function isUpdateAvailable(current, latest) {
4154
+ const normalize = (v) => v.replace(/^v/, "");
4155
+ const c = normalize(current);
4156
+ const l = normalize(latest);
4157
+ if (c === l) return false;
4158
+ const splitPre = (v) => {
4159
+ const idx = v.indexOf("-");
4160
+ return idx === -1 ? [v, ""] : [v.slice(0, idx), v.slice(idx + 1)];
4161
+ };
4162
+ const [cRelease, cPre] = splitPre(c);
4163
+ const [lRelease, lPre] = splitPre(l);
4164
+ const parseRelease = (r) => r.split(".").map(Number);
4165
+ const cParts = parseRelease(cRelease);
4166
+ const lParts = parseRelease(lRelease);
4167
+ for (let i = 0; i < Math.max(cParts.length, lParts.length); i++) {
4168
+ const cv = cParts[i] ?? 0;
4169
+ const lv = lParts[i] ?? 0;
4170
+ if (lv > cv) return true;
4171
+ if (lv < cv) return false;
4172
+ }
4173
+ if (!lPre && cPre) return true;
4174
+ if (lPre && !cPre) return false;
4175
+ const cPreParts = cPre.split(".");
4176
+ const lPreParts = lPre.split(".");
4177
+ for (let i = 0; i < Math.max(cPreParts.length, lPreParts.length); i++) {
4178
+ const cv = cPreParts[i] ?? "";
4179
+ const lv = lPreParts[i] ?? "";
4180
+ const cvNum = Number(cv);
4181
+ const lvNum = Number(lv);
4182
+ if (!isNaN(cvNum) && !isNaN(lvNum)) {
4183
+ if (lvNum > cvNum) return true;
4184
+ if (lvNum < cvNum) return false;
4185
+ } else {
4186
+ if (lv > cv) return true;
4187
+ if (lv < cv) return false;
4188
+ }
4189
+ }
4190
+ return false;
4191
+ }
4192
+ function formatNotice(current, latest) {
4193
+ const installCmd = `npm install -g ${PACKAGE_NAME}`;
4194
+ const raw1 = `Update available: ${current} → ${latest}`;
4195
+ const raw2 = `Run ${installCmd} to update`;
4196
+ const width = Math.max(raw1.length, raw2.length) + 4;
4197
+ const bar = chalk.yellow("─".repeat(width));
4198
+ const border = chalk.yellow("│");
4199
+ const pad = (raw, styled) => `${border} ${styled}${" ".repeat(width - 2 - raw.length)} ${border}`;
4200
+ return [
4201
+ "",
4202
+ chalk.yellow(`╭${bar}╮`),
4203
+ pad(raw1, `Update available: ${chalk.dim(current)} → ${chalk.green(latest)}`),
4204
+ pad(raw2, `Run ${chalk.cyan(installCmd)} to update`),
4205
+ chalk.yellow(`╰${bar}╯`),
4206
+ ""
4207
+ ].join("\n");
4208
+ }
4209
+ async function checkForUpdates(currentVersion) {
4210
+ if (process.env["CI"] || process.env["NO_UPDATE_NOTIFIER"]) return null;
4211
+ const channel = getChannel(currentVersion);
4212
+ const cache = readCache();
4213
+ const now = Date.now();
4214
+ let latestVersion = null;
4215
+ if (cache && now - cache.checkedAt < CACHE_TTL_MS) latestVersion = cache.latestVersion;
4216
+ else {
4217
+ latestVersion = await fetchLatestVersion(channel);
4218
+ if (latestVersion) writeCache({
4219
+ latestVersion,
4220
+ checkedAt: now
4221
+ });
4222
+ }
4223
+ if (!latestVersion || !isUpdateAvailable(currentVersion, latestVersion)) return null;
4224
+ return () => {
4225
+ process.stderr.write(formatNotice(currentVersion, latestVersion) + "\n");
4226
+ };
4227
+ }
4228
+ //#endregion
4116
4229
  //#region src/cli.ts
4117
4230
  const { version: CLI_VERSION } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
4118
4231
  /** Parses --token values and returns the context spread, or null on parse error (exits with code 1). */
@@ -4129,6 +4242,7 @@ function tokenContext(rawTokens) {
4129
4242
  throw err;
4130
4243
  }
4131
4244
  }
4245
+ const updateCheckPromise = checkForUpdates(CLI_VERSION);
4132
4246
  await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [options]").option("dry-run", {
4133
4247
  type: "boolean",
4134
4248
  default: false,
@@ -4491,6 +4605,7 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
4491
4605
  }).command("list", "List every provider with a stored bootstrap token", () => {}, async () => {
4492
4606
  await runAuthList();
4493
4607
  }).demandCommand(1, "Run `holocron auth --help` to see available auth subcommands."), () => {}).demandCommand(1, "Run `holocron --help` to see available commands.").strict().help().parse();
4608
+ (await updateCheckPromise)?.();
4494
4609
  /**
4495
4610
  * Parse `--scope` strings: `repo` | `env=NAME` | `org=NAME`.
4496
4611
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/cli",
3
- "version": "2.0.0-alpha.74",
3
+ "version": "2.0.0-alpha.75",
4
4
  "description": "The Holocron CLI — a pluggable, capability-based orchestrator for spinning up and operating software projects.",
5
5
  "homepage": "https://github.com/theholocron/holocron/tree/main/packages/cli#readme",
6
6
  "bugs": "https://github.com/theholocron/holocron/issues",