hirify-cli 0.6.0 → 0.6.2
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 +2 -1
- package/bin/lib/cli.js +2 -2
- package/bin/lib/commands.js +10 -2
- package/bin/lib/main.js +14 -18
- package/bin/lib/setup.js +37 -25
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ npx -y hirify-cli@latest init
|
|
|
14
14
|
|
|
15
15
|
One command installs the CLI globally, installs the skill for the agents found on this computer
|
|
16
16
|
(Claude Code, Codex, Cursor, OpenCode, Gemini CLI, Antigravity) and signs you in through the browser.
|
|
17
|
-
Run it yourself or let your agent run it.
|
|
17
|
+
Run it yourself or let your agent run it. It installs the newest release, whichever version
|
|
18
|
+
started it; a pinned version (`HIRIFY_VERSION_PIN`, `HIRIFY_NO_AUTO_UPDATE=1`) is kept.
|
|
18
19
|
|
|
19
20
|
The same steps one by one:
|
|
20
21
|
|
package/bin/lib/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { CliError, redact, safeText } from './errors.js'
|
|
2
2
|
const ARGUMENTS = { auth: '[<key>]', update: '[<version>]', 'vacancy search': '[<query>]', 'vacancy read': '<slug>', 'vacancy reveal': '<slug>', 'vacancy apply': '<slug>', 'vacancy hide': '<slug>...', 'company hide': '[<name>...]', 'feed show': '<id>', 'feed create': '<name>', 'feed deliver': '<id>', 'webhook create': '<name> <url>', 'feedback send': '<bug|feature> <title>', 'api call': '<capability>', 'capabilities show': '<capability>' }
|
|
3
3
|
const GLOBAL = ['json', 'fields', 'error-format', 'debug', 'timeout', 'help']
|
|
4
|
-
export const BOOLEAN_FLAGS = new Set(['json', 'debug', 'help', 'force', 'no-browser', '
|
|
4
|
+
export const BOOLEAN_FLAGS = new Set(['json', 'debug', 'help', 'force', 'no-browser', 'stdin', 'telegram', 'no-telegram', 'no-webhook', 'check', 'rollback', 'undo'])
|
|
5
5
|
export const COMMANDS = {
|
|
6
6
|
intro: { args: [0, 0], description: 'what this can do, and in what order' },
|
|
7
7
|
version: { args: [0, 0], description: 'installed version' },
|
|
8
|
-
init: { args: [0, 0], flags: ['no-
|
|
8
|
+
init: { args: [0, 0], flags: ['no-browser', 'callback-port'], description: 'install the CLI and the skill, then sign in' },
|
|
9
9
|
skill: { args: [0, 0], description: 'install the working rules for your agent' },
|
|
10
10
|
login: { args: [0, 0], flags: ['force', 'no-browser', 'callback-port'], description: 'sign in through your browser' },
|
|
11
11
|
logout: { args: [0, 0], description: 'sign out on this computer' },
|
package/bin/lib/commands.js
CHANGED
|
@@ -204,6 +204,14 @@ function detailRows(pairs) {
|
|
|
204
204
|
.map(([label, text]) => `${(label + ':').padEnd(10)}${text}`)
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* The word that names a vacancy, offered under both names a manifest may use for the path
|
|
209
|
+
* slot. Hirify moved vacancies.read and vacancies.reveal from `{slug}` to `{vacancy_id}` on
|
|
210
|
+
* 2026-09-30 and reads a non-numeric value in that slot as the slug, so one set of params
|
|
211
|
+
* serves the old manifest, the new one, a slug and a numeric id alike.
|
|
212
|
+
*/
|
|
213
|
+
const vacancyRef = (word) => ({ slug: word, vacancy_id: word })
|
|
214
|
+
|
|
207
215
|
/**
|
|
208
216
|
* One vacancy in full, with the text a person reads on the site. This is the command an
|
|
209
217
|
* agent shortlists with, so it is deliberately the cheap one: it counts against the day's
|
|
@@ -218,7 +226,7 @@ async function cmdVacancyRead(args, words) {
|
|
|
218
226
|
'\n Slugs come from hirify vacancy search or hirify feed show.')
|
|
219
227
|
}
|
|
220
228
|
|
|
221
|
-
const res = await callCapability('vacancies.read', { params:
|
|
229
|
+
const res = await callCapability('vacancies.read', { params: vacancyRef(slug), allow: [200, 404] })
|
|
222
230
|
if (res.status === 404) die('there is no vacancy with that slug.')
|
|
223
231
|
|
|
224
232
|
const d = res.body?.data ?? {}
|
|
@@ -276,7 +284,7 @@ function contactLine(c) {
|
|
|
276
284
|
async function cmdVacancyReveal(args, words) {
|
|
277
285
|
const [slug] = words
|
|
278
286
|
if (!slug) die('a vacancy slug is required: hirify vacancy reveal <slug>')
|
|
279
|
-
const body = await callCapability('vacancies.reveal', { params:
|
|
287
|
+
const body = await callCapability('vacancies.reveal', { params: vacancyRef(slug) })
|
|
280
288
|
const d = body?.data ?? {}
|
|
281
289
|
out(body, () => {
|
|
282
290
|
console.log(`company: ${d.company ?? '-'}`)
|
package/bin/lib/main.js
CHANGED
|
@@ -73,30 +73,26 @@ export async function main(argv = process.argv.slice(2), { baseRoot = ownRoot, b
|
|
|
73
73
|
const { installGlobally, installSkill, describeSkill } = await import('./setup.js')
|
|
74
74
|
const result = {}
|
|
75
75
|
let skillRoot = ownRoot
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (root) skillRoot = root
|
|
82
|
-
}
|
|
76
|
+
output.progress('Installing the Hirify CLI...')
|
|
77
|
+
const { root, ...cli } = await installGlobally({ version: pkg.version, latest: updater.check, signal, env: config.env })
|
|
78
|
+
result.cli = cli
|
|
79
|
+
// The skill comes from the CLI that is installed, so the two match.
|
|
80
|
+
if (cli.installed && cli.version !== pkg.version) skillRoot = root
|
|
83
81
|
try { result.skill = installSkill({ packageRoot: skillRoot, env: config.env }) }
|
|
84
82
|
catch (error) { if (!(error instanceof CliError)) throw error; result.skill = { error: error.code, message: error.message, agents: [] } }
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
catch (error) { if (!(error instanceof CliError) || signal.aborted) throw error; result.sign_in = { signed_in: false, error: error.code, message: error.message } }
|
|
91
|
-
}
|
|
83
|
+
try {
|
|
84
|
+
const { source } = auth.status()
|
|
85
|
+
// A configured key is a finished sign-in: no browser, no waiting.
|
|
86
|
+
result.sign_in = ['environment', 'key'].includes(source) ? { signed_in: true, already_signed_in: true, source } : await auth.login({ noBrowser: Boolean(command.values['no-browser']), port: Number(command.values['callback-port'] || 0), consentMs: Math.min(command.timeout, 300000) })
|
|
87
|
+
} catch (error) { if (!(error instanceof CliError) || signal.aborted) throw error; result.sign_in = { signed_in: false, error: error.code, message: error.message } }
|
|
92
88
|
if (command.json) output.json(result)
|
|
93
89
|
else {
|
|
94
|
-
|
|
90
|
+
output.console.log(result.cli.installed ? `Hirify CLI ${result.cli.version} is installed. The command is: hirify` : `The CLI could not be installed globally (${result.cli.reason}). Every command still works as: npx -y hirify-cli <command>`)
|
|
95
91
|
if (result.skill.error) output.console.log(result.skill.message); else for (const line of describeSkill(result.skill)) output.console.log(line)
|
|
96
|
-
|
|
97
|
-
if (result.sign_in
|
|
92
|
+
output.console.log(result.sign_in.signed_in ? (result.sign_in.already_signed_in ? 'Already signed in.' : 'Signed in. Access saved on this computer.') : result.sign_in.message)
|
|
93
|
+
if (result.sign_in.signed_in) output.console.log('Next: ask your agent to look at your Hirify profile and saved searches.')
|
|
98
94
|
}
|
|
99
|
-
if (result.cli
|
|
95
|
+
if (!result.cli.installed || result.skill.error || result.skill.agents.some(item => !item.installed) || !result.sign_in.signed_in) exitCode = 1
|
|
100
96
|
} else if (command.name === 'auth') {
|
|
101
97
|
secrets.add(command.words[0])
|
|
102
98
|
await store.commit({ kind: 'key', access_token: command.words[0] })
|
package/bin/lib/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, cpSync, rmSync, symlinkSync,
|
|
1
|
+
import { existsSync, mkdirSync, cpSync, rmSync, symlinkSync, realpathSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
|
-
import { join, relative } from 'node:path'
|
|
3
|
+
import { dirname, join, relative } from 'node:path'
|
|
4
4
|
import semver from 'semver'
|
|
5
5
|
import { CliError } from './errors.js'
|
|
6
6
|
import { PACKAGE_NAME, runProcess } from './update.js'
|
|
@@ -26,20 +26,24 @@ export function agentDirectories({ env = process.env, home = homedir() } = {}) {
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const entry = path => { try { return lstatSync(path) } catch (error) { if (error.code === 'ENOENT') return null; throw error } }
|
|
30
|
-
const real = path => { try { return realpathSync(path) } catch { return path } }
|
|
31
29
|
// Removing a link removes the link, never the directory it points to.
|
|
32
|
-
const remove = path =>
|
|
33
|
-
const readJSON = file => { try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return null } }
|
|
30
|
+
const remove = path => rmSync(path, { recursive: true, force: true })
|
|
34
31
|
// OS messages may carry private paths, so only the code is reported.
|
|
35
32
|
const reason = error => ['EACCES', 'EPERM', 'EROFS', 'ENOSPC', 'EDQUOT', 'ENOTDIR', 'ENOENT', 'EEXIST', 'ELOOP'].includes(error?.code) ? error.code : 'unknown'
|
|
36
33
|
|
|
34
|
+
// A junction needs no elevated rights on Windows; elsewhere the link stays relative.
|
|
35
|
+
const linkSkill = (path, target, platform) => (platform === 'win32'
|
|
36
|
+
? symlinkSync(realpathSync(path), target, 'junction')
|
|
37
|
+
: symlinkSync(relative(dirname(target), realpathSync(path)), target, 'dir'))
|
|
38
|
+
|
|
37
39
|
/**
|
|
38
40
|
* Install the skill carried by this package: one shared copy under `~/.agents/skills`, linked into
|
|
39
41
|
* every agent found here. The skill comes from the package of the CLI it is for, so the two match.
|
|
40
|
-
* An agent that cannot be written to is reported and does not stop the others.
|
|
42
|
+
* An agent that cannot be written to is reported and does not stop the others. An agent that
|
|
43
|
+
* cannot take a link gets a copy, and the result says so: a copy does not follow the shared one
|
|
44
|
+
* when the CLI updates, so the person knows to run `hirify skill` again.
|
|
41
45
|
*/
|
|
42
|
-
export function installSkill({ packageRoot, env = process.env, home = homedir(), platform = process.platform } = {}) {
|
|
46
|
+
export function installSkill({ packageRoot, env = process.env, home = homedir(), platform = process.platform, link = linkSkill } = {}) {
|
|
43
47
|
const source = join(packageRoot, 'skills', SKILL_NAME)
|
|
44
48
|
if (!existsSync(join(source, 'SKILL.md'))) throw new CliError('skill_missing', 'This installation does not carry the skill. Install it with: npx skills add hirifyme/hirify-cli')
|
|
45
49
|
const shared = join(home, '.agents', 'skills')
|
|
@@ -55,41 +59,47 @@ export function installSkill({ packageRoot, env = process.env, home = homedir(),
|
|
|
55
59
|
try {
|
|
56
60
|
mkdirSync(agent.skills, { recursive: true })
|
|
57
61
|
// An agent whose skills directory already is the shared one needs no link of its own.
|
|
58
|
-
const parent =
|
|
59
|
-
if (parent ===
|
|
62
|
+
const parent = realpathSync(agent.skills)
|
|
63
|
+
if (parent === realpathSync(shared)) { agents.push({ agent: agent.name, installed: true }); continue }
|
|
60
64
|
const target = join(parent, SKILL_NAME)
|
|
61
65
|
remove(target)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
else symlinkSync(relative(parent, real(path)), target, 'dir')
|
|
66
|
-
} catch { cpSync(source, target, { recursive: true }) }
|
|
67
|
-
agents.push({ agent: agent.name, installed: true })
|
|
66
|
+
let copied = false
|
|
67
|
+
try { link(path, target, platform) } catch { cpSync(source, target, { recursive: true }); copied = true }
|
|
68
|
+
agents.push(copied ? { agent: agent.name, installed: true, copied: true } : { agent: agent.name, installed: true })
|
|
68
69
|
} catch (error) { agents.push({ agent: agent.name, installed: false, reason: reason(error) }) }
|
|
69
70
|
}
|
|
70
71
|
return { skill: SKILL_NAME, path, agents }
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
/**
|
|
74
|
-
* Put the
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
75
|
+
* Put the newest release of the CLI on PATH, whichever package started the command, with the
|
|
76
|
+
* person's own npm and its settings: the environment goes to npm as it is, because what npx adds
|
|
77
|
+
* to it is the same configuration, already resolved. A pinned version and switched-off updates
|
|
78
|
+
* keep the running release, as they do for automatic updates. A failure is a result, not an
|
|
79
|
+
* error: without a global installation every command still runs as `npx -y hirify-cli <command>`.
|
|
80
|
+
* An installation that is already at that release or past it is kept. `root` is where the
|
|
81
|
+
* installed package is, and with it the skill that matches it.
|
|
79
82
|
*/
|
|
80
|
-
export async function installGlobally({ version, signal, env = process.env, run = runProcess } = {}) {
|
|
83
|
+
export async function installGlobally({ version, latest, signal, env = process.env, run = runProcess } = {}) {
|
|
81
84
|
const options = { signal, env }
|
|
85
|
+
if (!env.HIRIFY_VERSION_PIN && env.HIRIFY_NO_AUTO_UPDATE !== '1') {
|
|
86
|
+
try { version = (await latest()).version }
|
|
87
|
+
catch (error) { if (signal?.aborted || !(error instanceof CliError)) throw error; return { installed: false, reason: error.code } }
|
|
88
|
+
}
|
|
82
89
|
let root
|
|
83
90
|
try { root = await run('npm', ['root', '--global'], { ...options, timeout: 10000 }) }
|
|
84
91
|
catch (error) { if (signal?.aborted) throw error; return { installed: false, reason: 'npm_unavailable' } }
|
|
85
92
|
if (root.code !== 0 || !root.stdout.trim()) return { installed: false, reason: 'npm_unavailable' }
|
|
86
93
|
const location = join(root.stdout.trim(), PACKAGE_NAME)
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
let current
|
|
95
|
+
// Only a package that is not there counts as absent; one that cannot be read is reported.
|
|
96
|
+
try { current = JSON.parse(readFileSync(join(location, 'package.json'), 'utf8')) }
|
|
97
|
+
catch (error) { if (error.code !== 'ENOENT') return { installed: false, reason: 'global_package_unreadable' } }
|
|
98
|
+
if (current?.name === PACKAGE_NAME && semver.valid(current.version) && semver.gte(current.version, version)) return { installed: true, version: current.version, changed: false, root: location }
|
|
89
99
|
let result
|
|
90
100
|
try { result = await run('npm', ['install', '--global', '--ignore-scripts', '--no-audit', '--no-fund', `${PACKAGE_NAME}@${version}`], { ...options, timeout: 180000 }) }
|
|
91
101
|
catch (error) { if (signal?.aborted) throw error; return { installed: false, reason: 'install_failed' } }
|
|
92
|
-
return result.code === 0 ? { installed: true, version, changed: true } : { installed: false, reason: 'install_failed' }
|
|
102
|
+
return result.code === 0 ? { installed: true, version, changed: true, root: location } : { installed: false, reason: 'install_failed' }
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
/** Lines a person reads after `hirify skill` or `hirify init`. */
|
|
@@ -98,5 +108,7 @@ export function describeSkill(result) {
|
|
|
98
108
|
const failed = result.agents.filter(item => !item.installed).map(item => `${item.agent} (${item.reason})`)
|
|
99
109
|
const lines = [done.length ? `The skill is installed for ${done.join(', ')}.` : `The skill is saved in ${result.path}. No supported agent was found on this computer.`]
|
|
100
110
|
if (failed.length) lines.push(`It could not be installed for ${failed.join(', ')}. Check that the agent's skills directory is a folder you can write to, then run hirify skill.`)
|
|
111
|
+
const copies = result.agents.filter(item => item.copied).map(item => item.agent)
|
|
112
|
+
if (copies.length) lines.push(`For ${copies.join(', ')} the skill is a copy, not a link: after the CLI updates, run hirify skill to refresh it.`)
|
|
101
113
|
return lines
|
|
102
114
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hirify-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "hirify-cli",
|
|
9
|
-
"version": "0.6.
|
|
9
|
+
"version": "0.6.2",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"cross-spawn": "7.0.6",
|
package/package.json
CHANGED