@getopencrawl/cli 0.4.12 → 0.4.13

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/README.md CHANGED
@@ -55,18 +55,23 @@ launcher version; there are no install scripts.
55
55
  Before ordinary commands, the launcher checks for updates at most once per day.
56
56
  It awaits the update in the invoking process; hooks never check for updates.
57
57
  It checks the npm `next` release channel, installs into a private user-owned cache with
58
- npm integrity verification and install scripts disabled, then atomically selects
59
- the complete release. The command uses the selected binary. No sudo or global-directory
60
- writes are needed. Offline/update failures preserve the working copy and retry
61
- after 15 minutes. Concurrent invocations share an update lock; updates never
62
- downgrade. An unused installation updates when it is next invoked, not on a timer.
58
+ npm integrity verification and install scripts disabled, then atomically publishes
59
+ the complete release. Each invocation selects the highest complete cached version
60
+ newer than the npm-installed copy. Concurrent installers keep separate temporary
61
+ stages and reuse a complete same-version winner; a delayed older update cannot
62
+ lower the selected version. No sudo or global-directory writes are needed.
63
+ Offline/update failures preserve the working copy and retry after 15 minutes.
64
+ An unused installation updates when it is next invoked, not on a timer.
63
65
 
64
66
  `OPENCRAWL_NO_UPDATE=1` disables automatic updates for reproducible agent runs.
65
67
  `opencrawl update [--json]` explicitly updates from that same channel. Cached releases live under
66
68
  `$XDG_CACHE_HOME/opencrawl` (default `~/.cache/opencrawl`); previous releases remain
67
69
  available for recovery. Install a newer npm version directly to override an older
68
- cached version. Set the opt-out and remove `current.json` to return to the npm
69
- installed version. Standalone binary installation/self-update is not offered in
70
+ cached version. `current.json` remains a compatibility hint for older installed
71
+ launchers; removing it does not disable cached releases in current launchers.
72
+ To return to the npm-installed version, stop running CLI processes, set the
73
+ opt-out, and move the cache's `releases` directory outside the cache. Keep it for
74
+ recovery. Standalone binary installation/self-update is not offered in
70
75
  this release; distribute the npm command.
71
76
 
72
77
  ## Development and release
package/bin/opencrawl.cjs CHANGED
@@ -46,14 +46,28 @@ function binary(root) {
46
46
  if (!fs.existsSync(file)) throw new Error('The OpenCrawl platform binary is missing. Reinstall @getopencrawl/cli.');
47
47
  return file;
48
48
  }
49
+ function releaseRoot(version) {
50
+ return path.join(cacheRoot(), 'releases', version, 'node_modules', '@getopencrawl', 'cli');
51
+ }
52
+ function validRelease(root, version) {
53
+ try {
54
+ const pkg = read(path.join(root, 'package.json'));
55
+ return pkg?.name === PACKAGE && pkg.version === version &&
56
+ fs.statSync(path.join(root, 'bin/opencrawl.cjs')).isFile() && fs.statSync(binary(root)).isFile();
57
+ } catch { return false; }
58
+ }
49
59
  function activeRoot(root = ROOT) {
50
- const pkg = read(path.join(root, 'package.json'));
51
- const current = read(path.join(cacheRoot(), 'current.json'));
52
- if (stable(current?.version) && stable(pkg?.version) && compare(current.version, pkg.version) > 0) {
53
- const cached = path.join(cacheRoot(), 'releases', current.version, 'node_modules', '@getopencrawl', 'cli');
54
- if (read(path.join(cached, 'package.json'))?.version === current.version) {
55
- try { binary(cached); return cached; } catch {} // a damaged cache must not break the installed copy
56
- }
60
+ const installed = read(path.join(root, 'package.json'))?.version;
61
+ if (!stable(installed)) return root;
62
+ let versions;
63
+ try { versions = fs.readdirSync(path.join(cacheRoot(), 'releases')).filter(stable).sort((a, b) => compare(b, a)); }
64
+ catch { return root; }
65
+ // Complete release directories are immutable publications. The mutable legacy
66
+ // pointer can be stale or overwritten by an older updater; it grants no priority.
67
+ for (const version of versions) {
68
+ if (compare(version, installed) <= 0) break;
69
+ const cached = releaseRoot(version);
70
+ if (validRelease(cached, version)) return cached;
57
71
  }
58
72
  return root;
59
73
  }
@@ -66,49 +80,37 @@ function npmCli(nodeExecutable = process.execPath) {
66
80
  }
67
81
  throw new Error('npm is required for updates. Install Node.js with npm and retry.');
68
82
  }
69
- function acquireUpdateLock() {
70
- const lock = path.join(cacheRoot(), 'update.lock');
71
- fs.mkdirSync(cacheRoot(), {recursive: true, mode: 0o700});
72
- try { const fd = fs.openSync(lock, 'wx', 0o600); fs.writeFileSync(fd, String(process.pid)); fs.closeSync(fd); }
73
- catch (error) {
74
- if (error.code !== 'EEXIST') throw error;
75
- const pid = Number(fs.readFileSync(lock, 'utf8'));
76
- if (pid > 0) {
77
- try { process.kill(pid, 0); return null; }
78
- catch (check) { if (check.code !== 'ESRCH') return null; }
79
- } else if (Date.now() - fs.statSync(lock).mtimeMs < 5 * 60_000) return null;
80
- try { fs.unlinkSync(lock); } catch {}
81
- return acquireUpdateLock();
82
- }
83
- return () => fs.unlinkSync(lock);
84
- }
85
83
  async function update({force = false, root = ROOT, fetcher = fetch, install = installRelease} = {}) {
86
- const unlock = acquireUpdateLock();
87
- if (!unlock) return {updated: false, busy: true};
88
- try {
89
- const stateFile = path.join(cacheRoot(), 'update.json');
90
- const state = read(stateFile);
91
- const current = read(path.join(activeRoot(root), 'package.json')).version;
92
- if (!force && state?.checkedAt > Date.now() - DAY) return {updated: false, version: current};
93
- const response = await fetcher(`${REGISTRY}/@getopencrawl%2fcli/${CHANNEL}`, {redirect: 'error', signal: AbortSignal.timeout(10_000)});
94
- if (!response.ok) throw new Error('The npm registry could not check for updates.');
95
- const latest = await response.json();
96
- if (latest.name !== PACKAGE || !stable(latest.version)) throw new Error('The registry returned invalid release metadata.');
97
- if (compare(latest.version, current) <= 0) { write(stateFile, {checkedAt: Date.now()}); return {updated: false, version: current}; }
98
- await install(latest.version);
99
- write(path.join(cacheRoot(), 'current.json'), {version: latest.version});
100
- write(stateFile, {checkedAt: Date.now()});
101
- return {updated: true, version: latest.version};
102
- } finally { unlock(); }
84
+ const stateFile = path.join(cacheRoot(), 'update.json');
85
+ const state = read(stateFile);
86
+ const current = read(path.join(activeRoot(root), 'package.json')).version;
87
+ if (!force && state?.checkedAt > Date.now() - DAY) return {updated: false, version: current};
88
+ const response = await fetcher(`${REGISTRY}/@getopencrawl%2fcli/${CHANNEL}`, {redirect: 'error', signal: AbortSignal.timeout(10_000)});
89
+ if (!response.ok) throw new Error('The npm registry could not check for updates.');
90
+ const latest = await response.json();
91
+ if (latest.name !== PACKAGE || !stable(latest.version)) throw new Error('The registry returned invalid release metadata.');
92
+ if (compare(latest.version, current) <= 0) { write(stateFile, {checkedAt: Date.now()}); return {updated: false, version: current}; }
93
+ await install(latest.version);
94
+ if (!validRelease(releaseRoot(latest.version), latest.version)) throw new Error('The installed update is incomplete.');
95
+ const selected = read(path.join(activeRoot(root), 'package.json')).version;
96
+ // Keep old npm-installed bootstraps able to delegate to the cached launcher.
97
+ // New launchers select the maximum complete release, even if this hint races.
98
+ write(path.join(cacheRoot(), 'current.json'), {version: selected});
99
+ write(stateFile, {checkedAt: Date.now()});
100
+ return {updated: compare(selected, current) > 0, version: selected};
103
101
  }
104
102
  async function installRelease(version, {registry = REGISTRY} = {}) {
103
+ if (!stable(version)) throw new Error('Invalid release version.');
105
104
  const origin = new URL(registry);
106
105
  if (registry !== REGISTRY && !(origin.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(origin.hostname))) throw new Error('Invalid release registry.');
107
106
  const releases = path.join(cacheRoot(), 'releases');
108
107
  fs.mkdirSync(releases, {recursive: true, mode: 0o700});
109
108
  const destination = path.join(releases, version);
110
109
  // Reuse a fully staged version if pointer publication was interrupted.
111
- if (fs.existsSync(destination)) { binary(path.join(destination, 'node_modules/@getopencrawl/cli')); return; }
110
+ if (fs.existsSync(destination)) {
111
+ if (!validRelease(releaseRoot(version), version)) throw new Error('The cached update is incomplete.');
112
+ return;
113
+ }
112
114
  const stage = fs.mkdtempSync(path.join(releases, '.stage-'));
113
115
  try {
114
116
  fs.writeFileSync(path.join(stage, 'package.json'), '{"private":true}\n');
@@ -119,8 +121,13 @@ async function installRelease(version, {registry = REGISTRY} = {}) {
119
121
  const installed = path.join(stage, 'node_modules/@getopencrawl/cli');
120
122
  const pkg = read(path.join(installed, 'package.json'));
121
123
  if (pkg?.name !== PACKAGE || pkg.version !== version) throw new Error('The installed release does not match the requested version.');
122
- binary(installed);
123
- fs.renameSync(stage, destination);
124
+ if (!validRelease(installed, version)) throw new Error('The installed update is incomplete.');
125
+ try { fs.renameSync(stage, destination); }
126
+ catch (error) {
127
+ // Another installer may have published this exact immutable release while
128
+ // npm was running. Validate its complete result and discard only our stage.
129
+ if (!validRelease(releaseRoot(version), version)) throw error;
130
+ }
124
131
  } finally { fs.rmSync(stage, {recursive: true, force: true}); }
125
132
  }
126
133
  async function automaticUpdate() {
@@ -145,7 +152,7 @@ async function main(args = process.argv.slice(2)) {
145
152
  catch {} // The native parser produces the public, versioned usage error.
146
153
  if (parsed?.positionals.length === 1 && parsed.positionals[0] === 'update' && !parsed.values.help && !parsed.values.version) {
147
154
  const data = await update({force: true});
148
- process.stdout.write(args.includes('--json') ? JSON.stringify({schemaVersion: 1, data}) + '\n' : data.busy ? 'An update is already running.\n' : data.updated ? `Installed opencrawl ${data.version}. Your next invocation will use it.\n` : `opencrawl ${data.version} is current.\n`);
155
+ process.stdout.write(args.includes('--json') ? JSON.stringify({schemaVersion: 1, data}) + '\n' : data.updated ? `Installed opencrawl ${data.version}. Your next invocation will use it.\n` : `opencrawl ${data.version} is current.\n`);
149
156
  return 0;
150
157
  }
151
158
  if (!(args[0] === 'collect' && ['hook','worker'].includes(args[1]))) await automaticUpdate();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getopencrawl/cli",
3
- "version": "0.4.12",
3
+ "version": "0.4.13",
4
4
  "description": "OpenCrawl CLI: sign in now, query the index as features become available",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -24,10 +24,10 @@
24
24
  "registry": "https://registry.npmjs.org"
25
25
  },
26
26
  "optionalDependencies": {
27
- "@getopencrawl/cli-darwin-arm64": "0.4.12",
28
- "@getopencrawl/cli-darwin-x64": "0.4.12",
29
- "@getopencrawl/cli-linux-arm64": "0.4.12",
30
- "@getopencrawl/cli-linux-x64": "0.4.12",
31
- "@getopencrawl/cli-win32-x64": "0.4.12"
27
+ "@getopencrawl/cli-darwin-arm64": "0.4.13",
28
+ "@getopencrawl/cli-darwin-x64": "0.4.13",
29
+ "@getopencrawl/cli-linux-arm64": "0.4.13",
30
+ "@getopencrawl/cli-linux-x64": "0.4.13",
31
+ "@getopencrawl/cli-win32-x64": "0.4.13"
32
32
  }
33
33
  }