hirify-cli 0.4.1 → 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 +5 -1
- package/bin/hirify.js +121 -5
- package/package.json +1 -1
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
|
|
|
@@ -15,7 +18,8 @@ npx hirify-cli login
|
|
|
15
18
|
```
|
|
16
19
|
|
|
17
20
|
Your browser opens, you confirm access on hirify.me, and the terminal continues on its own.
|
|
18
|
-
Nothing to copy back.
|
|
21
|
+
Nothing to copy back. Running `hirify login` again keeps the current sign-in; use
|
|
22
|
+
`hirify login --force` only when you need to switch accounts or approve changed permissions.
|
|
19
23
|
|
|
20
24
|
```bash
|
|
21
25
|
npx hirify-cli intro
|
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,12 +61,16 @@ 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
|
-
hirify login sign in through your browser
|
|
73
|
+
hirify login sign in through your browser [--force]
|
|
70
74
|
hirify logout sign out on this computer
|
|
71
75
|
hirify account show your plan and allowances
|
|
72
76
|
|
|
@@ -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.
|
|
@@ -592,7 +678,14 @@ async function startCallbackServer() {
|
|
|
592
678
|
})
|
|
593
679
|
})
|
|
594
680
|
|
|
595
|
-
return {
|
|
681
|
+
return {
|
|
682
|
+
port: server.address().port,
|
|
683
|
+
received,
|
|
684
|
+
close: () => {
|
|
685
|
+
server.close()
|
|
686
|
+
server.closeIdleConnections?.()
|
|
687
|
+
},
|
|
688
|
+
}
|
|
596
689
|
}
|
|
597
690
|
|
|
598
691
|
/**
|
|
@@ -648,6 +741,19 @@ function openBrowser(url) {
|
|
|
648
741
|
|
|
649
742
|
async function cmdLogin(args) {
|
|
650
743
|
const noBrowser = args.includes('--no-browser')
|
|
744
|
+
const force = args.includes('--force')
|
|
745
|
+
const current = readSession()
|
|
746
|
+
|
|
747
|
+
if (!force && current?.kind === 'oauth') {
|
|
748
|
+
if (current.expires_at && current.expires_at - 60 <= now() && current.refresh_token) {
|
|
749
|
+
await refreshSession(current)
|
|
750
|
+
}
|
|
751
|
+
console.log('Already signed in. Use `hirify login --force` to sign in again.')
|
|
752
|
+
await cmdAccountShow()
|
|
753
|
+
console.log('\nRules for your agent: npx skills add hirifyme/hirify-cli')
|
|
754
|
+
return
|
|
755
|
+
}
|
|
756
|
+
|
|
651
757
|
const endpoints = await discover()
|
|
652
758
|
|
|
653
759
|
let listener
|
|
@@ -701,8 +807,12 @@ async function cmdLogin(args) {
|
|
|
701
807
|
if (!opened) console.log(`\n${authUrl}\n`)
|
|
702
808
|
else console.log(`If it did not open, use this link:\n${authUrl}`)
|
|
703
809
|
|
|
704
|
-
|
|
810
|
+
let timeoutId
|
|
811
|
+
const timeout = new Promise((resolve) => {
|
|
812
|
+
timeoutId = setTimeout(() => resolve({ timedOut: true }), LOGIN_TIMEOUT_MS)
|
|
813
|
+
})
|
|
705
814
|
const answer = await Promise.race([listener.received, timeout])
|
|
815
|
+
clearTimeout(timeoutId)
|
|
706
816
|
listener.close()
|
|
707
817
|
|
|
708
818
|
if (answer.timedOut) {
|
|
@@ -806,6 +916,10 @@ function cmdLogout() {
|
|
|
806
916
|
: 'Nobody is signed in here.')
|
|
807
917
|
}
|
|
808
918
|
|
|
919
|
+
function cmdVersion() {
|
|
920
|
+
console.log(VERSION)
|
|
921
|
+
}
|
|
922
|
+
|
|
809
923
|
// ── commands ───────────────────────────────────────────────────────────────
|
|
810
924
|
async function cmdAccountShow() {
|
|
811
925
|
const body = await callCapability('account.status')
|
|
@@ -1624,7 +1738,7 @@ function cmdSkill() {
|
|
|
1624
1738
|
// Commands that are not something you do to a thing. Signing in is not an operation on a
|
|
1625
1739
|
// vacancy or a feed, and `intro` is the first thing anyone runs, so it stays one word.
|
|
1626
1740
|
const PLAIN = {
|
|
1627
|
-
intro: cmdIntro, skill: cmdSkill,
|
|
1741
|
+
intro: cmdIntro, skill: cmdSkill, version: cmdVersion,
|
|
1628
1742
|
login: cmdLogin, logout: cmdLogout, auth: cmdAuth,
|
|
1629
1743
|
}
|
|
1630
1744
|
|
|
@@ -1643,6 +1757,8 @@ const NOUNS = {
|
|
|
1643
1757
|
|
|
1644
1758
|
const [noun, ...args] = process.argv.slice(2)
|
|
1645
1759
|
|
|
1760
|
+
await autoUpdate()
|
|
1761
|
+
|
|
1646
1762
|
if (!noun || noun === '--help' || noun === '-h' || noun === 'help') { console.log(HELP); process.exit(EXIT_OK) }
|
|
1647
1763
|
|
|
1648
1764
|
// `Object.hasOwn`, not a plain lookup: every object inherits `toString` and `constructor`,
|
package/package.json
CHANGED