hirify-cli 0.4.2 → 0.4.3

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
@@ -5,6 +5,9 @@ account already has, search across the board, a vacancy in full, and the contact
5
5
 
6
6
  Node 18 or newer. No dependencies.
7
7
 
8
+ A global installation checks for a new release when it starts and updates itself before running
9
+ the command. Set `HIRIFY_NO_AUTO_UPDATE=1` only when the installed version must stay fixed.
10
+
8
11
  Install it globally with `npm install -g hirify-cli`, or run it without installing through `npx`
9
12
  as shown below. In either case, the command is `hirify` after a global install.
10
13
 
package/bin/hirify.js CHANGED
@@ -15,7 +15,7 @@ import { homedir } from 'node:os'
15
15
  import { join, dirname } from 'node:path'
16
16
  import { fileURLToPath } from 'node:url'
17
17
  import { createServer } from 'node:http'
18
- import { spawn } from 'node:child_process'
18
+ import { execFileSync, spawn, spawnSync } from 'node:child_process'
19
19
  import { randomBytes, createHash } from 'node:crypto'
20
20
 
21
21
  const API = process.env.HIRIFY_API || 'https://api.hirify.me'
@@ -61,10 +61,14 @@ const VERSION = (() => {
61
61
  }
62
62
  })()
63
63
  const USER_AGENT = `hirify-cli/${VERSION}`
64
+ const PACKAGE_NAME = 'hirify-cli'
65
+ const PACKAGE_LATEST = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`
66
+ const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm'
64
67
 
65
68
  const HELP = `hirify - job search for AI agents
66
69
 
67
70
  hirify intro what this can do, and in what order
71
+ hirify version installed version
68
72
 
69
73
  hirify login sign in through your browser [--force]
70
74
  hirify logout sign out on this computer
@@ -114,6 +118,88 @@ const EXIT_MANIFEST_UNSUPPORTED = 2
114
118
 
115
119
  const die = (msg, code = EXIT_ERROR) => { console.error(`hirify: ${msg}`); process.exit(code) }
116
120
 
121
+ /** Compare the public numeric part of two npm versions without adding a dependency. */
122
+ function newerVersion(candidate, current) {
123
+ const parts = (value) => {
124
+ const match = String(value).match(/^(\d+)\.(\d+)\.(\d+)(?:-|$)/)
125
+ return match ? match.slice(1).map(Number) : null
126
+ }
127
+ const next = parts(candidate)
128
+ const installed = parts(current)
129
+ if (!next || !installed) return false
130
+ for (let i = 0; i < 3; i++) {
131
+ if (next[i] !== installed[i]) return next[i] > installed[i]
132
+ }
133
+ return false
134
+ }
135
+
136
+ /**
137
+ * A package launched from npx already follows the requested npm version. Only a global
138
+ * installation is ours to update: local project dependencies and the npx cache must not be
139
+ * changed behind their owner. `npm root -g` is asked only when this file is under node_modules,
140
+ * so a checkout and the test suite take no extra process or network trip.
141
+ */
142
+ function isGlobalInstall() {
143
+ const script = fileURLToPath(import.meta.url)
144
+ if (!script.includes(`${join('node_modules', PACKAGE_NAME)}`)) return false
145
+ try {
146
+ const root = execFileSync(NPM, ['root', '-g'], {
147
+ encoding: 'utf8',
148
+ stdio: ['ignore', 'pipe', 'ignore'],
149
+ timeout: 2000,
150
+ }).trim()
151
+ return script.startsWith(join(root, PACKAGE_NAME) + '/') || script.startsWith(join(root, PACKAGE_NAME) + '\\')
152
+ } catch {
153
+ return false
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Update a global installation before running the requested command. A failed registry check is
159
+ * silent because job search must keep working offline. A failed install leaves one exact recovery
160
+ * command and then continues with the installed version.
161
+ */
162
+ async function autoUpdate() {
163
+ if (process.env.HIRIFY_NO_AUTO_UPDATE === '1' || !isGlobalInstall()) return false
164
+
165
+ const controller = new AbortController()
166
+ const timeout = setTimeout(() => controller.abort(), 2000)
167
+ let latest
168
+ try {
169
+ const response = await fetch(PACKAGE_LATEST, {
170
+ headers: { Accept: 'application/json', 'User-Agent': USER_AGENT },
171
+ signal: controller.signal,
172
+ })
173
+ if (!response.ok) return false
174
+ latest = (await response.json())?.version
175
+ } catch {
176
+ return false
177
+ } finally {
178
+ clearTimeout(timeout)
179
+ }
180
+
181
+ if (!newerVersion(latest, VERSION)) return false
182
+
183
+ console.error(`Updating Hirify CLI from ${VERSION} to ${latest}...`)
184
+ const update = spawnSync(NPM, ['install', '-g', `${PACKAGE_NAME}@latest`], {
185
+ encoding: 'utf8',
186
+ stdio: ['ignore', 'pipe', 'pipe'],
187
+ timeout: 120000,
188
+ })
189
+
190
+ if (update.status !== 0) {
191
+ console.error(`Hirify CLI ${latest} is available. Automatic update could not complete.`)
192
+ console.error(`Run: npm install -g ${PACKAGE_NAME}@latest`)
193
+ return false
194
+ }
195
+
196
+ console.error(`Hirify CLI updated to ${latest}.`)
197
+ const rerun = spawnSync(process.execPath, [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
198
+ stdio: 'inherit',
199
+ })
200
+ process.exit(rerun.status ?? EXIT_ERROR)
201
+ }
202
+
117
203
  // Options that stand on their own. Everything else takes the next word as its value, and
118
204
  // that rule is what lets `search` carry a filter the CLI has never heard of: the list of
119
205
  // filters lives on the server, and a CLI that keeps its own copy is a CLI that lags.
@@ -830,6 +916,10 @@ function cmdLogout() {
830
916
  : 'Nobody is signed in here.')
831
917
  }
832
918
 
919
+ function cmdVersion() {
920
+ console.log(VERSION)
921
+ }
922
+
833
923
  // ── commands ───────────────────────────────────────────────────────────────
834
924
  async function cmdAccountShow() {
835
925
  const body = await callCapability('account.status')
@@ -1648,7 +1738,7 @@ function cmdSkill() {
1648
1738
  // Commands that are not something you do to a thing. Signing in is not an operation on a
1649
1739
  // vacancy or a feed, and `intro` is the first thing anyone runs, so it stays one word.
1650
1740
  const PLAIN = {
1651
- intro: cmdIntro, skill: cmdSkill,
1741
+ intro: cmdIntro, skill: cmdSkill, version: cmdVersion,
1652
1742
  login: cmdLogin, logout: cmdLogout, auth: cmdAuth,
1653
1743
  }
1654
1744
 
@@ -1667,6 +1757,8 @@ const NOUNS = {
1667
1757
 
1668
1758
  const [noun, ...args] = process.argv.slice(2)
1669
1759
 
1760
+ await autoUpdate()
1761
+
1670
1762
  if (!noun || noun === '--help' || noun === '-h' || noun === 'help') { console.log(HELP); process.exit(EXIT_OK) }
1671
1763
 
1672
1764
  // `Object.hasOwn`, not a plain lookup: every object inherits `toString` and `constructor`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hirify-cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Hirify CLI for AI agents: browser sign-in, job feeds, search, reading a vacancy in full, contacts to apply to, and applying on Hirify.",
5
5
  "bin": {
6
6
  "hirify": "bin/hirify.js"