jobhunt-kit 0.2.0 → 0.3.0
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +22 -21
- package/bin/install-options.mjs +51 -0
- package/bin/jobhunt-kit.mjs +31 -10
- package/installer-assets/native-SKILL.md +24 -0
- package/installer-assets/runtime-lock.json +2 -2
- package/installer-assets/workspace-lock.json +2 -2
- package/package.json +1 -1
- package/plugins/jobhunt-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/jobhunt-kit/.codex-plugin/plugin.json +1 -1
- package/plugins/jobhunt-kit/package-lock.json +2 -2
- package/plugins/jobhunt-kit/package.json +1 -1
- package/plugins/jobhunt-kit/references/cli.md +5 -1
- package/plugins/jobhunt-kit/scripts/commands.mjs +5 -2
- package/tests/installer.test.mjs +64 -0
package/README.md
CHANGED
|
@@ -7,9 +7,10 @@ npx jobhunt-kit install
|
|
|
7
7
|
Поиск работы вместе с AI-агентом: профиль кандидата, проверка резюме, подбор вакансий
|
|
8
8
|
через Hirify, сопроводительные письма и история откликов.
|
|
9
9
|
|
|
10
|
-
Нужны **Node.js 24
|
|
11
|
-
|
|
12
|
-
в
|
|
10
|
+
Нужны **Node.js 24+ и Codex или Claude Code**. Установщик предложит выбрать
|
|
11
|
+
агентов и область установки: рабочая папка или глобально для пользователя.
|
|
12
|
+
При установке в рабочую папку он создаст `my-jobhunt`, установит зависимости
|
|
13
|
+
и подключит навык к выбранным агентам. Другой путь: `install ./my-folder`.
|
|
13
14
|
|
|
14
15
|
## Начало работы
|
|
15
16
|
|
|
@@ -78,32 +79,32 @@ npx jobhunt-kit report
|
|
|
78
79
|
Папка `local/` исключена из Git. Храните личные файлы в ней; для резервной копии
|
|
79
80
|
сохраняйте всю папку при остановленных процессах. Данные на диске не зашифрованы.
|
|
80
81
|
|
|
81
|
-
##
|
|
82
|
+
## Выбор агентов
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
При необходимости их можно подключить отдельно как плагин.
|
|
85
|
-
|
|
86
|
-
**Codex** — из корня установленной папки:
|
|
84
|
+
Без интерактивных вопросов:
|
|
87
85
|
|
|
88
86
|
```sh
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
npx jobhunt-kit install --agents codex,claude
|
|
88
|
+
npx jobhunt-kit install --agents codex
|
|
89
|
+
npx jobhunt-kit install --agents claude --scope global
|
|
91
90
|
```
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
В режиме `project` навыки устанавливаются в созданную рабочую папку:
|
|
93
|
+
Codex — `.agents/skills/jobhunt-kit`, Claude Code — `.claude/skills/jobhunt-kit`.
|
|
94
|
+
В режиме `global` используются те же каталоги в домашней папке пользователя;
|
|
95
|
+
рабочая папка и профиль создаются позже при обращении к агенту.
|
|
97
96
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
После установки откройте новую сессию и попросите использовать **jobhunt-kit**.
|
|
98
|
+
В Claude Code можно вызвать `/jobhunt-kit`. Отдельные команды marketplace
|
|
99
|
+
и `--plugin-dir` для этого способа не нужны.
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
`--yes` выбирает обнаруженных агентов (или обоих, если ничего не обнаружено)
|
|
102
|
+
и область `project`. Обнаружение использует каталоги агентов, а не запускает их.
|
|
103
|
+
Для скриптов задавайте `--agents` и `--scope` явно.
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
Установщик сохраняет существующие файлы: при различиях останавливается до копирования.
|
|
106
|
+
Для установки новой версии используйте новую папку. `init <папка>` по-прежнему
|
|
107
|
+
создаёт только шаблон без подключения навыков.
|
|
107
108
|
|
|
108
109
|
## Разработка
|
|
109
110
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export function parseAgents(value) {
|
|
8
|
+
const agents = [...new Set(value.split(',').map(x => x.trim().toLowerCase()))];
|
|
9
|
+
if (!agents.length || agents.some(x => !['codex', 'claude'].includes(x))) {
|
|
10
|
+
throw new Error('Choose agents: codex, claude or codex,claude');
|
|
11
|
+
}
|
|
12
|
+
return agents;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function installOptions(args, { input = process.stdin, output = process.stdout, home = homedir(), cwd = process.cwd(), ask } = {}) {
|
|
16
|
+
const {values, positionals} = parseArgs({args, allowPositionals: true, options: {
|
|
17
|
+
agents: {type:'string'}, scope: {type:'string'}, yes: {type:'boolean', short:'y'}
|
|
18
|
+
}});
|
|
19
|
+
if (positionals.length > 1) throw new Error('Expected at most one destination folder');
|
|
20
|
+
let agents = values.agents ? parseAgents(values.agents) : null;
|
|
21
|
+
let scope = values.scope;
|
|
22
|
+
if (scope && !['project','global'].includes(scope)) throw new Error('Scope must be project or global');
|
|
23
|
+
const detected = ['codex','claude'].filter(a => existsSync(join(home, `.${a}`)) || existsSync(join(cwd, a === 'codex' ? '.agents' : '.claude')));
|
|
24
|
+
const defaults = detected.length ? detected : ['codex','claude'];
|
|
25
|
+
let terminal;
|
|
26
|
+
try {
|
|
27
|
+
if (!agents && !values.yes || !scope && !values.yes && !values.agents) {
|
|
28
|
+
if (!ask && (!input.isTTY || !output.isTTY)) throw new Error('Non-interactive install: pass --agents codex,claude --scope project, or --yes');
|
|
29
|
+
if (!ask) {
|
|
30
|
+
terminal = createInterface({input, output});
|
|
31
|
+
ask = question => terminal.question(question);
|
|
32
|
+
terminal.on('SIGINT', () => terminal.close());
|
|
33
|
+
}
|
|
34
|
+
output.write(`\nJobhunt Kit\nDetected: ${detected.join(', ') || 'none'}\n`);
|
|
35
|
+
while (!agents) {
|
|
36
|
+
const answer = await ask(`Agents — codex, claude, or codex,claude [${defaults.join(',')}]: `);
|
|
37
|
+
try { agents = parseAgents(answer.trim() || defaults.join(',')); }
|
|
38
|
+
catch (e) { output.write(`${e.message}\n`); }
|
|
39
|
+
}
|
|
40
|
+
while (!scope) {
|
|
41
|
+
const answer = (await ask('Scope — project or global [project]: ')).trim() || 'project';
|
|
42
|
+
if (['project','global'].includes(answer)) scope = answer;
|
|
43
|
+
else output.write('Choose project or global\n');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} finally { terminal?.close(); }
|
|
47
|
+
scope ||= 'project';
|
|
48
|
+
agents ||= defaults;
|
|
49
|
+
if (scope === 'global' && positionals.length) throw new Error('Global installation does not take a workspace folder');
|
|
50
|
+
return {destination: positionals[0] || './my-jobhunt', agents, scope};
|
|
51
|
+
}
|
package/bin/jobhunt-kit.mjs
CHANGED
|
@@ -3,6 +3,8 @@ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSy
|
|
|
3
3
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { installOptions } from './install-options.mjs';
|
|
6
8
|
|
|
7
9
|
const SOURCE = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
10
|
const roots = ['bin', 'installer-assets', 'plugins/jobhunt-kit', 'scripts', 'tests',
|
|
@@ -24,17 +26,30 @@ function noSymlinks(path) {
|
|
|
24
26
|
current = parent;
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
|
-
export function install(destination, { source = SOURCE, installDependencies = true } = {}) {
|
|
29
|
+
export function install(destination, { source = SOURCE, installDependencies = true, agents = [], scope = 'project', home = homedir() } = {}) {
|
|
28
30
|
if (Number(process.versions.node.split('.')[0]) < 24) throw new Error('Node.js 24 or newer is required');
|
|
29
|
-
|
|
31
|
+
if (!['project', 'global'].includes(scope) || agents.some(a => !['codex', 'claude'].includes(a))) throw new Error('Invalid installation scope or agents');
|
|
32
|
+
if (scope === 'global' && !agents.length) throw new Error('Global installation requires at least one agent');
|
|
33
|
+
const target = resolve(scope === 'global' ? home : destination);
|
|
30
34
|
source = resolve(source);
|
|
31
35
|
if (target === source || !relative(source, target).startsWith('..')) throw new Error('Choose a destination outside the installer package');
|
|
32
36
|
const files = new Map();
|
|
33
|
-
|
|
37
|
+
if (scope === 'project') {
|
|
38
|
+
for (const root of roots) for (const path of collect(join(source, root))) files.set(relative(source, path), path);
|
|
34
39
|
// npm omits package-lock.json and may omit .gitignore; ship them under explicit asset names.
|
|
35
40
|
files.set('.gitignore', join(source, 'installer-assets/gitignore.txt'));
|
|
36
41
|
files.set('package-lock.json', join(source, 'installer-assets/workspace-lock.json'));
|
|
37
42
|
files.set('plugins/jobhunt-kit/package-lock.json', join(source, 'installer-assets/runtime-lock.json'));
|
|
43
|
+
}
|
|
44
|
+
const destinations = [];
|
|
45
|
+
for (const agent of [...new Set(agents)]) {
|
|
46
|
+
const native = join(agent === 'codex' ? '.agents' : '.claude', 'skills', 'jobhunt-kit');
|
|
47
|
+
destinations.push({agent, path: join(target, native)});
|
|
48
|
+
files.set(join(native, 'SKILL.md'), join(source, 'installer-assets/native-SKILL.md'));
|
|
49
|
+
const plugin = join(source, 'plugins/jobhunt-kit');
|
|
50
|
+
for (const path of collect(plugin)) files.set(join(native, 'bundle', relative(plugin, path)), path);
|
|
51
|
+
files.set(join(native, 'bundle/package-lock.json'), join(source, 'installer-assets/runtime-lock.json'));
|
|
52
|
+
}
|
|
38
53
|
const pending = [];
|
|
39
54
|
for (const [name, input] of files) {
|
|
40
55
|
const output = join(target, name);
|
|
@@ -54,6 +69,7 @@ export function install(destination, { source = SOURCE, installDependencies = tr
|
|
|
54
69
|
mkdirSync(dirname(output), { recursive: true });
|
|
55
70
|
copyFileSync(input, output);
|
|
56
71
|
}
|
|
72
|
+
if (scope === 'global') return {directory: target, copied: pending.length, dependencies_installed: false, scope, agents: destinations};
|
|
57
73
|
if (installDependencies) {
|
|
58
74
|
const options = { cwd: target, stdio: 'inherit', windowsHide: true };
|
|
59
75
|
const result = process.platform === 'win32'
|
|
@@ -68,11 +84,11 @@ export function install(destination, { source = SOURCE, installDependencies = tr
|
|
|
68
84
|
mkdirSync(binDir, { recursive: true });
|
|
69
85
|
writeFileSync(join(binDir, 'jobhunt-kit.cmd'), '@echo off\r\nnode "%~dp0..\\..\\bin\\jobhunt-kit.mjs" %*\r\n');
|
|
70
86
|
writeFileSync(join(binDir, 'jobhunt-kit'), '#!/bin/sh\nexec node "$(dirname "$0")/../../bin/jobhunt-kit.mjs" "$@"\n', { mode: 0o755 });
|
|
71
|
-
return { directory: target, copied: pending.length, dependencies_installed: installDependencies };
|
|
87
|
+
return { directory: target, copied: pending.length, dependencies_installed: installDependencies, scope, agents: destinations };
|
|
72
88
|
}
|
|
73
89
|
export async function main(args) {
|
|
74
90
|
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
75
|
-
console.log('Usage: jobhunt-kit <command> [arguments] [--workspace dir | --data dir]\n\ninstall [directory]
|
|
91
|
+
console.log('Usage: jobhunt-kit <command> [arguments] [--workspace dir | --data dir]\n\ninstall [directory] Choose agents and install (default: ./my-jobhunt)\n --agents codex,claude Select agents without prompts\n --scope project|global Workspace skills or user-wide skills\n --yes, -y Use detected agents and project scope\ninit <directory> Alias with an explicit destination\nprofile init|show|check Initialize/view/validate local profile\nprofile save --input file Save profile and clear confirmation\nprofile confirm --note text Record explicit candidate confirmation\nresume [file] Import file, fingerprint and mechanical checks\nresume check|reviewed Inspect file / record agent review (--input file)\nsearch [plan] Prepare context for agent; no live search\nsearch start|event|record|finish Persist search operations (--input file)\napply preview|export|begin slug Review/export/reserve application\napply prepare|approve|send|finish|resolve --input file\ntrack list|show slug|status slug --input file\nhistory | runs | report Inspect history or generate Markdown report\npolicy show|set --input file Inspect/set application policy\nschedule --input file Generate scheduler prompt; does not schedule\ndoctor Check local setup without network\n\nNode.js 24+ required. Default data: ./local/jobhunt-kit. Only apply send performs a live application call.');
|
|
76
92
|
return;
|
|
77
93
|
}
|
|
78
94
|
if (!['init', 'install'].includes(args[0])) {
|
|
@@ -83,11 +99,16 @@ export async function main(args) {
|
|
|
83
99
|
if (result?.attempt_id && Object.hasOwn(result, 'exit_code')) process.exitCode = result.exit_code ?? 1;
|
|
84
100
|
return;
|
|
85
101
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
let options;
|
|
103
|
+
if (args[0] === 'init') {
|
|
104
|
+
if (args.length !== 2 || args[1].startsWith('-')) throw new Error('Usage: jobhunt-kit init <directory>');
|
|
105
|
+
options = {destination: args[1]};
|
|
106
|
+
} else options = await installOptions(args.slice(1));
|
|
107
|
+
const result = install(options.destination, options);
|
|
108
|
+
console.log(`\nJobhunt Kit ready: ${result.directory}\nCopied files: ${result.copied}`);
|
|
109
|
+
for (const agent of result.agents) console.log(`${agent.agent}: ${agent.path}`);
|
|
110
|
+
console.log(result.scope === 'global' ? 'Open your working folder in a new agent session.' : 'Open this folder in a new agent session.');
|
|
111
|
+
console.log('Ask: use jobhunt-kit to initialize my job search profile.\nNo profile, schedule, search or application was created.');
|
|
91
112
|
}
|
|
92
113
|
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
93
114
|
try { await main(process.argv.slice(2)); }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jobhunt-kit
|
|
3
|
+
description: Set up a candidate profile, review a resume, find suitable jobs through Hirify, prepare applications and track job-search progress. Use when the user wants help with their job search.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Jobhunt Kit
|
|
7
|
+
|
|
8
|
+
Read the relevant workflow before acting:
|
|
9
|
+
|
|
10
|
+
- Profile and onboarding: [job-profile](bundle/skills/job-profile/SKILL.md).
|
|
11
|
+
- Resume review: [job-resume](bundle/skills/job-resume/SKILL.md).
|
|
12
|
+
- Vacancy search: [job-search](bundle/skills/job-search/SKILL.md).
|
|
13
|
+
- Application and cover letter: [job-apply](bundle/skills/job-apply/SKILL.md).
|
|
14
|
+
- History, status and scheduling: [job-track](bundle/skills/job-track/SKILL.md).
|
|
15
|
+
|
|
16
|
+
The package root P is the `bundle` folder next to this file. Resolve it from this
|
|
17
|
+
file's absolute location, never from the terminal working directory. Candidate
|
|
18
|
+
data D belongs in the user's working folder under `local/jobhunt-kit`, never here.
|
|
19
|
+
For a global installation, establish the user's working folder before creating data.
|
|
20
|
+
|
|
21
|
+
On the user's request to initialize a profile, run
|
|
22
|
+
`node <P>/scripts/setup.mjs --data <D> --install-cli` to prepare local dependencies.
|
|
23
|
+
Then use [CLI commands](bundle/references/cli.md) via `node <P>/scripts/cli.mjs`.
|
|
24
|
+
Installation does not authorize applications, account changes or scheduled tasks.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jobhunt-kit-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "jobhunt-kit-plugin",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.3.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"hirify-cli": "0.4.5",
|
|
12
12
|
"mammoth": "1.12.2",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jobhunt-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "jobhunt-kit",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.3.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"hirify-cli": "0.4.5",
|
|
12
12
|
"mammoth": "1.12.2",
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jobhunt-kit-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "jobhunt-kit-plugin",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.3.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"hirify-cli": "0.4.5",
|
|
12
12
|
"mammoth": "1.12.2",
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Команды рабочих сценариев
|
|
2
2
|
|
|
3
|
-
Установка: `npx jobhunt-kit install [папка]
|
|
3
|
+
Установка: `npx jobhunt-kit install [папка]` с выбором Codex/Claude Code и области.
|
|
4
|
+
Без вопросов: `npx jobhunt-kit install --agents codex,claude --scope project`.
|
|
5
|
+
Глобально: `npx jobhunt-kit install --agents claude --scope global` (без пути).
|
|
6
|
+
В глобальном режиме устанавливаются только навыки; зависимости и данные создаёт
|
|
7
|
+
агент в рабочей папке при инициализации профиля.
|
|
4
8
|
Без пути создаётся `./my-jobhunt`. Старый `init <папка>` также поддерживается.
|
|
5
9
|
Из установленной папки: `npx jobhunt-kit <команда>`.
|
|
6
10
|
Из другой папки: `npx jobhunt-kit <команда> --workspace <папка>`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import { resolve, join, dirname } from 'node:path';
|
|
3
|
+
import { resolve, join, dirname, basename } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { Tracker, initData } from './tracker.mjs';
|
|
@@ -58,7 +58,10 @@ function scheduleTemplate(dir, workspace, input) {
|
|
|
58
58
|
if (!['search', 'prepare', 'auto'].includes(input.mode)) throw new Error('Schedule mode must be search, prepare or auto');
|
|
59
59
|
required(input.frequency, 'frequency'); required(input.timezone, 'timezone');
|
|
60
60
|
new Intl.DateTimeFormat('en', { timeZone: input.timezone });
|
|
61
|
-
|
|
61
|
+
let plugin = join(workspace, 'plugins', 'jobhunt-kit');
|
|
62
|
+
const native = basename(ROOT) === 'bundle' && basename(dirname(ROOT)) === 'jobhunt-kit'
|
|
63
|
+
&& basename(dirname(dirname(ROOT))) === 'skills' && existsSync(join(dirname(ROOT), 'SKILL.md'));
|
|
64
|
+
if (!existsSync(join(plugin, 'skills', 'job-search', 'SKILL.md')) && native) plugin = ROOT;
|
|
62
65
|
if (!existsSync(join(plugin, 'skills', 'job-search', 'SKILL.md'))) throw new Error('Schedule needs an installed workspace; pass --workspace to avoid storing an npm cache path');
|
|
63
66
|
const template = readFileSync(join(ROOT, 'templates', 'scheduled-search.md'), 'utf8');
|
|
64
67
|
let prompt = template;
|
package/tests/installer.test.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join, resolve, sep } from 'node:path';
|
|
7
7
|
import { install } from '../bin/jobhunt-kit.mjs';
|
|
8
|
+
import { installOptions } from '../bin/install-options.mjs';
|
|
8
9
|
|
|
9
10
|
function target(t) {
|
|
10
11
|
const path = mkdtempSync(join(tmpdir(), 'job-search-install-'));
|
|
@@ -50,3 +51,66 @@ test('CLI executes through the package link used by npm exec', t => {
|
|
|
50
51
|
unlinkSync(path);
|
|
51
52
|
}
|
|
52
53
|
});
|
|
54
|
+
test('selected agent receives a complete portable skill; other agent is untouched', t => {
|
|
55
|
+
const path = target(t);
|
|
56
|
+
install(path, {installDependencies:false, agents:['claude']});
|
|
57
|
+
assert.ok(existsSync(join(path,'.claude/skills/jobhunt-kit/SKILL.md')));
|
|
58
|
+
assert.ok(existsSync(join(path,'.claude/skills/jobhunt-kit/bundle/references/workflow.md')));
|
|
59
|
+
assert.ok(existsSync(join(path,'.claude/skills/jobhunt-kit/bundle/package-lock.json')));
|
|
60
|
+
assert.equal(existsSync(join(path,'.agents/skills/jobhunt-kit')),false);
|
|
61
|
+
});
|
|
62
|
+
test('global installation targets only selected skill directories and is idempotent', t => {
|
|
63
|
+
const home = target(t);
|
|
64
|
+
const opts={home,scope:'global',agents:['codex','claude']};
|
|
65
|
+
const first=install('unused',opts);
|
|
66
|
+
assert.equal(first.agents.length,2);
|
|
67
|
+
assert.equal(first.dependencies_installed,false);
|
|
68
|
+
assert.equal(existsSync(join(home,'package.json')),false);
|
|
69
|
+
assert.equal(existsSync(join(home,'local')),false);
|
|
70
|
+
assert.equal(install('unused',opts).copied,0);
|
|
71
|
+
});
|
|
72
|
+
test('native skill conflict aborts the entire install before workspace files are copied', t => {
|
|
73
|
+
const path=target(t);
|
|
74
|
+
const folder=join(path,'.claude/skills/jobhunt-kit'); mkdirSync(folder,{recursive:true});
|
|
75
|
+
writeFileSync(join(folder,'SKILL.md'),'User instructions');
|
|
76
|
+
assert.throws(()=>install(path,{installDependencies:false,agents:['codex','claude']}),/Existing file differs/);
|
|
77
|
+
assert.equal(existsSync(join(path,'package.json')),false);
|
|
78
|
+
assert.equal(existsSync(join(path,'.agents')),false);
|
|
79
|
+
assert.equal(readFileSync(join(folder,'SKILL.md'),'utf8'),'User instructions');
|
|
80
|
+
});
|
|
81
|
+
test('native installation rejects redirected skill directories', t => {
|
|
82
|
+
const path=target(t); mkdirSync(path,{recursive:true});
|
|
83
|
+
const elsewhere=join(path,'elsewhere'); mkdirSync(elsewhere);
|
|
84
|
+
const link=join(path,'.claude'); symlinkSync(elsewhere,link,process.platform==='win32'?'junction':'dir');
|
|
85
|
+
try { assert.throws(()=>install(path,{installDependencies:false,agents:['claude']}),/symlink/); }
|
|
86
|
+
finally { unlinkSync(link); }
|
|
87
|
+
assert.equal(existsSync(join(elsewhere,'skills')),false);
|
|
88
|
+
});
|
|
89
|
+
test('interactive installer retries invalid choices and selects agents and scope', async t => {
|
|
90
|
+
const answers=['invalid','claude,codex','global'];
|
|
91
|
+
const options=await installOptions([],{home:target(t),cwd:target(t),output:{write(){}},ask:async()=>answers.shift()});
|
|
92
|
+
assert.deepEqual(options.agents,['claude','codex']); assert.equal(options.scope,'global');
|
|
93
|
+
});
|
|
94
|
+
test('noninteractive installer requires explicit selection and validates before writing', async () => {
|
|
95
|
+
const quiet={input:{isTTY:false},output:{isTTY:false}};
|
|
96
|
+
await assert.rejects(installOptions([],quiet),/Non-interactive/);
|
|
97
|
+
await assert.rejects(installOptions(['--agents','other'],quiet),/Choose agents/);
|
|
98
|
+
await assert.rejects(installOptions(['some-folder','--agents','claude','--scope','global'],quiet),/does not take/);
|
|
99
|
+
assert.deepEqual((await installOptions(['--agents','codex,claude'],quiet)).agents,['codex','claude']);
|
|
100
|
+
});
|
|
101
|
+
test('global skill can initialize separate data and generate a schedule with persistent paths', t => {
|
|
102
|
+
const home=target(t);
|
|
103
|
+
install('unused',{home,scope:'global',agents:['codex']});
|
|
104
|
+
const workspace=join(home,'work'); mkdirSync(workspace);
|
|
105
|
+
const cli=join(home,'.agents/skills/jobhunt-kit/bundle/scripts/cli.mjs');
|
|
106
|
+
const run=args=>{
|
|
107
|
+
const r=spawnSync(process.execPath,[cli,...args,'--workspace',workspace],{encoding:'utf8'});
|
|
108
|
+
assert.equal(r.status,0,r.stderr);return JSON.parse(r.stdout);
|
|
109
|
+
};
|
|
110
|
+
run(['profile','init']);
|
|
111
|
+
const input=join(workspace,'schedule.json');
|
|
112
|
+
writeFileSync(input,JSON.stringify({frequency:'Weekly',timezone:'UTC',mode:'search'}));
|
|
113
|
+
const result=run(['schedule','--input',input]);
|
|
114
|
+
assert.equal(result.scheduled,false);
|
|
115
|
+
assert.ok(readFileSync(result.path,'utf8').includes(join(home,'.agents/skills/jobhunt-kit/bundle')));
|
|
116
|
+
});
|