@hncode/hncode 0.3.3 → 0.3.5

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 CHANGED
@@ -10,26 +10,48 @@
10
10
  // resolve to a status string, never throw to the caller. The TUI calls them from
11
11
  // timers / startup without awaiting.
12
12
 
13
- import { spawn, execSync } from 'node:child_process';
13
+ import { spawn } from 'node:child_process';
14
14
  import { localVersion } from './version.js';
15
15
 
16
16
  const PKG_NAME = '@hncode/hncode';
17
+ // Spawn npm portably. On Windows npm is a `.cmd` shim, and Node refuses to spawn
18
+ // a `.cmd` directly (EINVAL) — it must go through a shell. Using
19
+ // `cmd.exe /c npm ...` keeps that behaviour while avoiding `shell: true`, which
20
+ // Node warns about (with a shell, args are concatenated rather than escaped).
21
+ function spawnNpm(args, opts) {
22
+ if (process.platform === 'win32') return spawn('cmd.exe', ['/c', 'npm', ...args], opts);
23
+ return spawn('npm', args, opts);
24
+ }
17
25
  // How often the auto-check runs once we are past the startup check.
18
26
  export const AUTO_UPDATE_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
19
27
  export { localVersion };
20
28
 
21
29
  // Latest version published to npm, or '' if the registry is unreachable /
22
30
  // not installed / not a package.
23
- export function remoteVersion() {
24
- try {
25
- const out = execSync(`npm view ${PKG_NAME} version --silent`, {
26
- stdio: ['ignore', 'pipe', 'ignore'],
27
- timeout: 15000,
28
- }).toString().trim();
29
- return out || '';
30
- } catch {
31
- return '';
32
- }
31
+ //
32
+ // ASYNC on purpose: `execSync` blocks the whole event loop, so a cold `npm view`
33
+ // (measured at ~15s on a slow registry, vs ~2s warm) froze the TUI — no key
34
+ // handling, no repaint — for the entire duration. `spawn` runs the same command
35
+ // off the main thread, and the timeout kills it rather than waiting it out.
36
+ export function remoteVersion(timeoutMs = 10000) {
37
+ return new Promise((resolve) => {
38
+ let child;
39
+ try {
40
+ child = spawnNpm(['view', PKG_NAME, 'version', '--silent'], {
41
+ stdio: ['ignore', 'pipe', 'ignore'],
42
+ });
43
+ } catch {
44
+ resolve('');
45
+ return;
46
+ }
47
+ let out = '';
48
+ let done = false;
49
+ const finish = (v) => { if (!done) { done = true; clearTimeout(timer); resolve(v); } };
50
+ const timer = setTimeout(() => { try { child.kill(); } catch {} finish(''); }, timeoutMs);
51
+ if (child.stdout) child.stdout.on('data', (d) => { out += d.toString(); });
52
+ child.on('error', () => finish(''));
53
+ child.on('close', () => finish((out || '').trim()));
54
+ });
33
55
  }
34
56
 
35
57
  // Parse a semver-ish "x.y.z" (ignore prerelease tags) into comparable numbers.
@@ -55,10 +77,9 @@ export function updateBackground() {
55
77
  return new Promise((resolve) => {
56
78
  try {
57
79
  // `npm install -g` needs to run detached; no need to wait here.
58
- const child = spawn('npm', ['install', '-g', `${PKG_NAME}@latest`], {
80
+ const child = spawnNpm(['install', '-g', `${PKG_NAME}@latest`], {
59
81
  detached: true,
60
82
  stdio: 'ignore',
61
- shell: process.platform === 'win32', // npm is npm.cmd on Windows
62
83
  });
63
84
  child.unref();
64
85
  child.on('error', (e) => resolve({ ok: false, error: e.message }));
@@ -77,7 +98,7 @@ export function updateBackground() {
77
98
  export async function checkAndUpdate() {
78
99
  const local = localVersion();
79
100
  if (!local) return { status: 'none', message: "can't read local version" };
80
- const remote = remoteVersion();
101
+ const remote = await remoteVersion();
81
102
  if (!remote) return { status: 'none', message: 'npm registry unreachable' };
82
103
 
83
104
  if (isNewer(remote, local)) {