@gigzen/populace 0.1.0 → 1.0.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/src/update.mjs ADDED
@@ -0,0 +1,141 @@
1
+ // Is there a newer Populace than the one running?
2
+ //
3
+ // A version check phones home, and a testing tool that quietly makes network
4
+ // calls you did not ask for has no business asking you to trust it with your
5
+ // staging credentials. So this one is:
6
+ //
7
+ // · explicit `populace update` asks; a run mentions it at most once
8
+ // a day, after the report, never before
9
+ // · off with one flag POPULACE_NO_UPDATE_CHECK=1, and the CI environment
10
+ // variable turns it off on its own
11
+ // · silent on failure no registry, no network, a firewall — nothing is said,
12
+ // because a version check is never worth an error message
13
+ // · anonymous a plain GET to the public registry. No identifiers, no
14
+ // telemetry, nothing about your app or your runs
15
+ //
16
+ // Cached in the system temp directory for a day so twenty runs make one request.
17
+
18
+ import fs from "node:fs";
19
+ import os from "node:os";
20
+ import path from "node:path";
21
+ import { PACKAGE_NAME, VERSION } from "./version.mjs";
22
+
23
+ const REGISTRY = "https://registry.npmjs.org";
24
+ const CACHE = path.join(os.tmpdir(), "populace-update-check.json");
25
+ const DAY = 24 * 60 * 60 * 1000;
26
+
27
+ /** Off in CI, and off whenever anyone says so. */
28
+ export function checksDisabled() {
29
+ return Boolean(process.env.POPULACE_NO_UPDATE_CHECK || process.env.CI);
30
+ }
31
+
32
+ /** -1 a is older, 0 same, 1 a is newer. Plain semver; pre-release tags ignored. */
33
+ export function compare(a, b) {
34
+ const parts = (v) => String(v).split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
35
+ const [x, y] = [parts(a), parts(b)];
36
+ for (let i = 0; i < 3; i++) {
37
+ if ((x[i] || 0) > (y[i] || 0)) return 1;
38
+ if ((x[i] || 0) < (y[i] || 0)) return -1;
39
+ }
40
+ return 0;
41
+ }
42
+
43
+ function readCache() {
44
+ try {
45
+ const c = JSON.parse(fs.readFileSync(CACHE, "utf8"));
46
+ return Date.now() - c.at < DAY ? c : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function writeCache(latest) {
53
+ try {
54
+ fs.writeFileSync(CACHE, JSON.stringify({ at: Date.now(), latest }));
55
+ } catch {
56
+ // A read-only temp directory is not a reason to fail anything.
57
+ }
58
+ }
59
+
60
+ /**
61
+ * The newest published version, or null.
62
+ *
63
+ * Never throws and never waits long: this runs after a report a person is
64
+ * already reading, and a version check that delays it has cost more than it
65
+ * is worth.
66
+ */
67
+ export async function latestVersion({ timeoutMs = 3000, useCache = true } = {}) {
68
+ if (checksDisabled()) return null;
69
+ if (useCache) {
70
+ const cached = readCache();
71
+ if (cached) return cached.latest;
72
+ }
73
+
74
+ const controller = new AbortController();
75
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
76
+ try {
77
+ // No Accept header. The abbreviated-packument type
78
+ // (application/vnd.npm.install-v1+json) is valid on the full packument and
79
+ // returns 406 on /latest — which this swallowed as "could not reach the
80
+ // registry", hiding a bug behind a reassuring message.
81
+ const res = await fetch(`${REGISTRY}/${PACKAGE_NAME}/latest`, { signal: controller.signal });
82
+ if (!res.ok) return null;
83
+ const { version } = await res.json();
84
+ if (!version) return null;
85
+ writeCache(version);
86
+ return version;
87
+ } catch {
88
+ return null;
89
+ } finally {
90
+ clearTimeout(timer);
91
+ }
92
+ }
93
+
94
+ /** One line for the end of a report, or null when there is nothing to say. */
95
+ export async function updateNotice(options) {
96
+ const latest = await latestVersion(options);
97
+ if (!latest || compare(latest, VERSION) !== 1) return null;
98
+ return ` A newer Populace is out: ${VERSION} → ${latest} npm i -g ${PACKAGE_NAME}`;
99
+ }
100
+
101
+ /** `populace update` — the explicit check, which always says something. */
102
+ export async function updateCommand() {
103
+ if (checksDisabled()) {
104
+ console.log(`
105
+ Update checks are switched off${process.env.CI ? " (CI is set)" : " (POPULACE_NO_UPDATE_CHECK is set)"}.
106
+ Running ${PACKAGE_NAME} ${VERSION}.
107
+ `);
108
+ return;
109
+ }
110
+
111
+ console.log(`\n Running ${PACKAGE_NAME} ${VERSION}. Asking the npm registry…`);
112
+ const latest = await latestVersion({ timeoutMs: 10000, useCache: false });
113
+
114
+ if (!latest) {
115
+ console.log(`
116
+ Could not reach the registry. That is all this means — nothing is wrong with
117
+ your install, and Populace never needs the network to run.
118
+ `);
119
+ return;
120
+ }
121
+
122
+ const d = compare(latest, VERSION);
123
+ if (d === 1) {
124
+ console.log(`
125
+ ${VERSION} → ${latest} is available.
126
+
127
+ npm i -g ${PACKAGE_NAME}
128
+ npx ${PACKAGE_NAME}@latest run
129
+
130
+ Turn these checks off with POPULACE_NO_UPDATE_CHECK=1.
131
+ `);
132
+ } else if (d === 0) {
133
+ console.log(`\n Up to date.\n`);
134
+ } else {
135
+ // Running ahead of the registry: a local build, or a publish still pending.
136
+ console.log(`
137
+ You are running ${VERSION}; the registry has ${latest}. That means this is a
138
+ local or unpublished build, not that anything is wrong.
139
+ `);
140
+ }
141
+ }