@hncode/hncode 0.2.0 → 0.3.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.js ADDED
@@ -0,0 +1,107 @@
1
+ // Auto-update support for hncode.
2
+ //
3
+ // hncode is installed as a global npm package (@hncode/hncode). This module
4
+ // checks npm for a newer version and, when one exists, installs it in the
5
+ // background without ever blocking the TUI. The running process is unaffected —
6
+ // the new files land on disk and take effect on next launch, which is the
7
+ // "seamless / no-sense" behaviour we want.
8
+ //
9
+ // Everything here is fire-and-forget: check() and update() return Promises that
10
+ // resolve to a status string, never throw to the caller. The TUI calls them from
11
+ // timers / startup without awaiting.
12
+
13
+ import { spawn, execSync } from 'node:child_process';
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const PKG_NAME = '@hncode/hncode';
19
+ // How often the auto-check runs once we are past the startup check.
20
+ export const AUTO_UPDATE_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
21
+
22
+ // The version installed locally, read from this package's own package.json.
23
+ export function localVersion() {
24
+ try {
25
+ const here = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
26
+ const pkg = JSON.parse(fs.readFileSync(here, 'utf8'));
27
+ return pkg.version || '';
28
+ } catch {
29
+ return '';
30
+ }
31
+ }
32
+
33
+ // Latest version published to npm, or '' if the registry is unreachable /
34
+ // not installed / not a package.
35
+ export function remoteVersion() {
36
+ try {
37
+ const out = execSync(`npm view ${PKG_NAME} version --silent`, {
38
+ stdio: ['ignore', 'pipe', 'ignore'],
39
+ timeout: 15000,
40
+ }).toString().trim();
41
+ return out || '';
42
+ } catch {
43
+ return '';
44
+ }
45
+ }
46
+
47
+ // Parse a semver-ish "x.y.z" (ignore prerelease tags) into comparable numbers.
48
+ function parseSemver(v) {
49
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(String(v).trim());
50
+ if (!m) return null;
51
+ return { major: +m[1], minor: +m[2], patch: +m[3] };
52
+ }
53
+
54
+ // True when `remote` is strictly newer than `local`.
55
+ export function isNewer(remote, local) {
56
+ const r = parseSemver(remote), l = parseSemver(local);
57
+ if (!r || !l) return false;
58
+ if (r.major !== l.major) return r.major > l.major;
59
+ if (r.minor !== l.minor) return r.minor > l.minor;
60
+ return r.patch > l.patch;
61
+ }
62
+
63
+ // Spin up the actual install in the BACKGROUND. Detached + unref'd so it keeps
64
+ // running after this process exits; stdout/err are swallowed (seamless).
65
+ // Returns: { ok: true } or { ok: false, error }.
66
+ export function updateBackground() {
67
+ return new Promise((resolve) => {
68
+ try {
69
+ // `npm install -g` needs to run detached; no need to wait here.
70
+ const child = spawn('npm', ['install', '-g', `${PKG_NAME}@latest`], {
71
+ detached: true,
72
+ stdio: 'ignore',
73
+ shell: process.platform === 'win32', // npm is npm.cmd on Windows
74
+ });
75
+ child.unref();
76
+ child.on('error', (e) => resolve({ ok: false, error: e.message }));
77
+ // We consider it "started" — the actual completion is unknown and that is
78
+ // fine; the next process will be the new version.
79
+ resolve({ ok: true });
80
+ } catch (e) {
81
+ resolve({ ok: false, error: (e && e.message) || String(e) });
82
+ }
83
+ });
84
+ }
85
+
86
+ // Full auto-check: read remote, if newer start a background update.
87
+ // Returns a status object. The TUI's auto-check IGNORES it (fully silent); the
88
+ // /update command reports it.
89
+ export async function checkAndUpdate() {
90
+ const local = localVersion();
91
+ if (!local) return { status: 'none', message: "can't read local version" };
92
+ const remote = remoteVersion();
93
+ if (!remote) return { status: 'none', message: 'npm registry unreachable' };
94
+
95
+ if (isNewer(remote, local)) {
96
+ const r = await updateBackground();
97
+ return {
98
+ status: r.ok ? 'updated' : 'error',
99
+ message: r.ok
100
+ ? `New version ${remote} — installing in background (restart to use it)`
101
+ : `Update failed: ${r.error}`,
102
+ current: local,
103
+ latest: remote,
104
+ };
105
+ }
106
+ return { status: 'uptodate', message: `hncode is up to date (${local})`, current: local, latest: remote };
107
+ }