jobhunt-kit 0.3.0 → 0.4.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.
@@ -8,7 +8,7 @@
8
8
  "name": "jobhunt-kit",
9
9
  "source": "./plugins/jobhunt-kit",
10
10
  "description": "Candidate intake, resume review, Hirify search and local tracking",
11
- "version": "0.3.0"
11
+ "version": "0.4.0"
12
12
  }
13
13
  ]
14
14
  }
package/README.md CHANGED
@@ -7,7 +7,7 @@ npx jobhunt-kit install
7
7
  Поиск работы вместе с AI-агентом: профиль кандидата, проверка резюме, подбор вакансий
8
8
  через Hirify, сопроводительные письма и история откликов.
9
9
 
10
- Нужны **Node.js 24+ и Codex или Claude Code**. Установщик предложит выбрать
10
+ Нужны **Node.js 24+ и агент с поддержкой навыков**. Установщик предложит выбрать
11
11
  агентов и область установки: рабочая папка или глобально для пользователя.
12
12
  При установке в рабочую папку он создаст `my-jobhunt`, установит зависимости
13
13
  и подключит навык к выбранным агентам. Другой путь: `install ./my-folder`.
@@ -84,21 +84,51 @@ npx jobhunt-kit report
84
84
  Без интерактивных вопросов:
85
85
 
86
86
  ```sh
87
- npx jobhunt-kit install --agents codex,claude
88
- npx jobhunt-kit install --agents codex
89
- npx jobhunt-kit install --agents claude --scope global
87
+ npx jobhunt-kit install --agents codex,cursor,gemini
88
+ npx jobhunt-kit install --agents all
89
+ npx jobhunt-kit install --providers opencode,pi --scope global
90
+ npx jobhunt-kit agents
90
91
  ```
91
92
 
92
- В режиме `project` навыки устанавливаются в созданную рабочую папку:
93
- Codex`.agents/skills/jobhunt-kit`, Claude Code — `.claude/skills/jobhunt-kit`.
94
- В режиме `global` используются те же каталоги в домашней папке пользователя;
95
- рабочая папка и профиль создаются позже при обращении к агенту.
93
+ В режиме `project` навык устанавливается в созданную рабочую папку,
94
+ в режиме `global` в домашнюю папку пользователя.
95
+
96
+ | Агент | Значение `--agents` | Рабочая папка | Глобально (от `~`) |
97
+ |---|---|---|---|
98
+ | Codex | `codex` | `.agents/skills/` | `.agents/skills/` |
99
+ | Claude Code | `claude` | `.claude/skills/` | `.claude/skills/` |
100
+ | Cursor | `cursor` | `.cursor/skills/` | `.cursor/skills/` |
101
+ | Gemini CLI | `gemini` | `.gemini/skills/` | `.gemini/skills/` |
102
+ | GitHub Copilot | `github` | `.github/skills/` | `.copilot/skills/` |
103
+ | OpenCode | `opencode` | `.opencode/skills/` | `.config/opencode/skills/` |
104
+ | Antigravity | `antigravity` | `.agent/skills/` | `.gemini/config/skills/` |
105
+ | Pi | `pi` | `.pi/skills/` | `.pi/agent/skills/` |
106
+ | Grok Build | `grok` | `.grok/skills/` | `.grok/skills/` |
107
+ | Hermes Agent | `hermes` | `.hermes/skills/` | `.hermes/skills/` |
108
+ | DeepSeek Harness | `dsh` | `.dsh/skills/` | `.dsh/skills/` |
109
+ | Kiro | `kiro` | `.kiro/skills/` | `.kiro/skills/` |
110
+ | Qoder | `qoder` | `.qoder/skills/` | `.qoder/skills/` |
111
+ | Trae | `trae` | `.trae/skills/` | `.trae/skills/` |
112
+ | Trae CN | `trae-cn` | `.trae-cn/skills/` | `.trae-cn/skills/` |
113
+ | Rovo Dev | `rovo-dev` | `.rovodev/skills/` | `.rovodev/skills/` |
114
+ | Mistral Vibe | `vibe` | `.vibe/skills/` | `.vibe/skills/` |
115
+ | Veto | `veto` | — | `.veto/skills/` |
116
+
117
+ В каждом каталоге создаётся отдельная папка `jobhunt-kit` с навыком и его ресурсами.
118
+ `--agents all` выбирает всех агентов для указанной области; Veto доступен только
119
+ глобально. `--providers` — синоним `--agents`. Можно использовать имена
120
+ `copilot`, `claude-code`, `deepseek` и `grok-build`.
121
+
122
+ OpenCode учитывает `OPENCODE_CONFIG_DIR` и `XDG_CONFIG_HOME`; Hermes и DeepSeek —
123
+ `HERMES_HOME` и `DSH_HOME`. Глобальные пути должны находиться в домашней папке.
124
+ При установке Hermes в проект выполните `hermes skills trust` в рабочей папке.
125
+ В остальных агентах при необходимости включите поддержку навыков и доверие к проекту.
96
126
 
97
127
  После установки откройте новую сессию и попросите использовать **jobhunt-kit**.
98
128
  В Claude Code можно вызвать `/jobhunt-kit`. Отдельные команды marketplace
99
129
  и `--plugin-dir` для этого способа не нужны.
100
130
 
101
- `--yes` выбирает обнаруженных агентов (или обоих, если ничего не обнаружено)
131
+ `--yes` выбирает обнаруженных агентов (или Codex и Claude Code, если ничего не обнаружено)
102
132
  и область `project`. Обнаружение использует каталоги агентов, а не запускает их.
103
133
  Для скриптов задавайте `--agents` и `--scope` явно.
104
134
 
package/bin/agents.mjs ADDED
@@ -0,0 +1,66 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+
4
+ // Directory conventions checked against Impeccable's providers.rs and README.
5
+ export const AGENTS = [
6
+ {id:'codex', name:'Codex', dir:'.agents', detect:'.codex', aliases:['agents']},
7
+ {id:'claude', name:'Claude Code', dir:'.claude', aliases:['claude-code']},
8
+ {id:'cursor', name:'Cursor', dir:'.cursor'},
9
+ {id:'gemini', name:'Gemini CLI', dir:'.gemini'},
10
+ {id:'github', name:'GitHub Copilot', dir:'.github', global:'.copilot/skills', detect:'.copilot', aliases:['copilot']},
11
+ {id:'opencode', name:'OpenCode', dir:'.opencode', global:'.config/opencode/skills'},
12
+ {id:'antigravity', name:'Antigravity', dir:'.agent', global:'.gemini/config/skills', aliases:['agent']},
13
+ {id:'pi', name:'Pi', dir:'.pi', global:'.pi/agent/skills'},
14
+ {id:'grok', name:'Grok Build', dir:'.grok', aliases:['grok-build','xai']},
15
+ {id:'hermes', name:'Hermes Agent', dir:'.hermes'},
16
+ {id:'dsh', name:'DeepSeek Harness', dir:'.dsh', aliases:['deepseek','deepseek-harness']},
17
+ {id:'kiro', name:'Kiro', dir:'.kiro'},
18
+ {id:'qoder', name:'Qoder', dir:'.qoder'},
19
+ {id:'trae', name:'Trae', dir:'.trae'},
20
+ {id:'trae-cn', name:'Trae CN', dir:'.trae-cn'},
21
+ {id:'rovo-dev', name:'Rovo Dev', dir:'.rovodev', aliases:['rovodev']},
22
+ {id:'vibe', name:'Mistral Vibe', dir:'.vibe'},
23
+ {id:'veto', name:'Veto', dir:'.veto', globalOnly:true}
24
+ ];
25
+
26
+ export function parseAgents(value) {
27
+ const selected = value.split(',').map(x => x.trim().toLowerCase().replace(/^\./,''));
28
+ if (selected.length === 1 && selected[0] === 'all') return AGENTS.map(a=>a.id);
29
+ return [...new Set(selected.map(id => {
30
+ const found=AGENTS.find(a=>a.id===id || a.aliases?.includes(id));
31
+ if (!found) throw new Error(`Choose agents: ${AGENTS.map(a=>a.id).join(', ')} (or all)`);
32
+ return found.id;
33
+ }))];
34
+ }
35
+
36
+ export function skillDirectory(id, {scope, home, cwd, env = process.env}) {
37
+ const agent=AGENTS.find(a=>a.id===id);
38
+ if (!agent) throw new Error(`Unknown agent: ${id}`);
39
+ if (scope==='project') {
40
+ if (agent.globalOnly) throw new Error(`${agent.name} supports --scope global only`);
41
+ return join(cwd,agent.dir,'skills','jobhunt-kit');
42
+ }
43
+ let folder=join(home, agent.global || join(agent.dir,'skills'));
44
+ if (id==='opencode') {
45
+ if (env.OPENCODE_CONFIG_DIR) folder=join(resolve(cwd,env.OPENCODE_CONFIG_DIR),'skills');
46
+ else if (env.XDG_CONFIG_HOME) folder=join(resolve(cwd,env.XDG_CONFIG_HOME),'opencode','skills');
47
+ }
48
+ const override=id==='hermes' ? env.HERMES_HOME : id==='dsh' ? env.DSH_HOME : null;
49
+ if (override) folder=join(resolve(cwd,override),'skills');
50
+ const delta=relative(resolve(home),resolve(folder));
51
+ if (isAbsolute(delta) || delta==='..' || delta.startsWith(`..${sep}`)) {
52
+ throw new Error(`${agent.name}: global configuration path must be inside the home directory: ${folder}`);
53
+ }
54
+ return join(folder,'jobhunt-kit');
55
+ }
56
+
57
+ export function detectAgents({home,cwd,env=process.env}) {
58
+ return AGENTS.filter(a=>{
59
+ const project=a.id==='github' ? join(cwd,'.github/skills') : join(cwd,a.dir);
60
+ let global;
61
+ try { global=dirname(dirname(skillDirectory(a.id,{scope:'global',home,cwd,env}))); }
62
+ catch { global=null; }
63
+ return (!a.globalOnly && existsSync(project)) || global && existsSync(global)
64
+ || a.detect && existsSync(join(home,a.detect));
65
+ }).map(a=>a.id);
66
+ }
@@ -1,40 +1,38 @@
1
1
  import { parseArgs } from 'node:util';
2
2
  import { createInterface } from 'node:readline/promises';
3
- import { existsSync } from 'node:fs';
4
3
  import { homedir } from 'node:os';
5
- import { join } from 'node:path';
4
+ import { AGENTS, detectAgents, parseAgents } from './agents.mjs';
5
+ export { parseAgents } from './agents.mjs';
6
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 } = {}) {
7
+ export async function installOptions(args, { input = process.stdin, output = process.stdout, home = homedir(), cwd = process.cwd(), env = process.env, ask } = {}) {
16
8
  const {values, positionals} = parseArgs({args, allowPositionals: true, options: {
17
- agents: {type:'string'}, scope: {type:'string'}, yes: {type:'boolean', short:'y'}
9
+ agents: {type:'string'}, providers: {type:'string'}, scope: {type:'string'}, yes: {type:'boolean', short:'y'}
18
10
  }});
19
11
  if (positionals.length > 1) throw new Error('Expected at most one destination folder');
20
- let agents = values.agents ? parseAgents(values.agents) : null;
12
+ if (values.agents && values.providers) throw new Error('Use either --agents or --providers');
13
+ const selection = values.agents ?? values.providers;
14
+ let agents = selection !== undefined ? parseAgents(selection) : null;
15
+ let allSelected = selection?.trim().toLowerCase()==='all';
21
16
  let scope = values.scope;
22
17
  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')));
18
+ const detected = detectAgents({home,cwd,env});
24
19
  const defaults = detected.length ? detected : ['codex','claude'];
25
20
  let terminal;
26
21
  try {
27
- if (!agents && !values.yes || !scope && !values.yes && !values.agents) {
22
+ if (!agents && !values.yes || !scope && !values.yes && selection === undefined) {
28
23
  if (!ask && (!input.isTTY || !output.isTTY)) throw new Error('Non-interactive install: pass --agents codex,claude --scope project, or --yes');
29
24
  if (!ask) {
30
25
  terminal = createInterface({input, output});
31
26
  ask = question => terminal.question(question);
32
27
  terminal.on('SIGINT', () => terminal.close());
33
28
  }
34
- output.write(`\nJobhunt Kit\nDetected: ${detected.join(', ') || 'none'}\n`);
29
+ output.write(`\nJobhunt Kit\n${AGENTS.map(a=>`${a.id}: ${a.name}${a.globalOnly?' (global only)':''}`).join('\n')}\nDetected: ${detected.join(', ') || 'none'}\n`);
35
30
  while (!agents) {
36
- const answer = await ask(`Agents — codex, claude, or codex,claude [${defaults.join(',')}]: `);
37
- try { agents = parseAgents(answer.trim() || defaults.join(',')); }
31
+ const answer = await ask(`Agents — comma-separated names or all [${defaults.join(',')}]: `);
32
+ try {
33
+ const chosen = answer.trim() || defaults.join(',');
34
+ agents = parseAgents(chosen); allSelected=chosen.toLowerCase()==='all';
35
+ }
38
36
  catch (e) { output.write(`${e.message}\n`); }
39
37
  }
40
38
  while (!scope) {
@@ -46,6 +44,11 @@ export async function installOptions(args, { input = process.stdin, output = pro
46
44
  } finally { terminal?.close(); }
47
45
  scope ||= 'project';
48
46
  agents ||= defaults;
47
+ if (scope==='project') {
48
+ const globalOnly = agents.filter(id=>AGENTS.find(a=>a.id===id).globalOnly);
49
+ if (allSelected) agents=agents.filter(id=>!globalOnly.includes(id));
50
+ else if (globalOnly.length) throw new Error(`${globalOnly.join(', ')} requires --scope global`);
51
+ }
49
52
  if (scope === 'global' && positionals.length) throw new Error('Global installation does not take a workspace folder');
50
53
  return {destination: positionals[0] || './my-jobhunt', agents, scope};
51
54
  }
@@ -5,6 +5,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import { spawnSync } from 'node:child_process';
6
6
  import { homedir } from 'node:os';
7
7
  import { installOptions } from './install-options.mjs';
8
+ import { AGENTS, skillDirectory } from './agents.mjs';
8
9
 
9
10
  const SOURCE = resolve(dirname(fileURLToPath(import.meta.url)), '..');
10
11
  const roots = ['bin', 'installer-assets', 'plugins/jobhunt-kit', 'scripts', 'tests',
@@ -26,9 +27,9 @@ function noSymlinks(path) {
26
27
  current = parent;
27
28
  }
28
29
  }
29
- export function install(destination, { source = SOURCE, installDependencies = true, agents = [], scope = 'project', home = homedir() } = {}) {
30
+ export function install(destination, { source = SOURCE, installDependencies = true, agents = [], scope = 'project', home = homedir(), env = process.env } = {}) {
30
31
  if (Number(process.versions.node.split('.')[0]) < 24) throw new Error('Node.js 24 or newer is required');
31
- if (!['project', 'global'].includes(scope) || agents.some(a => !['codex', 'claude'].includes(a))) throw new Error('Invalid installation scope or agents');
32
+ if (!['project', 'global'].includes(scope) || agents.some(a => !AGENTS.some(p=>p.id===a))) throw new Error('Invalid installation scope or agents');
32
33
  if (scope === 'global' && !agents.length) throw new Error('Global installation requires at least one agent');
33
34
  const target = resolve(scope === 'global' ? home : destination);
34
35
  source = resolve(source);
@@ -43,7 +44,7 @@ export function install(destination, { source = SOURCE, installDependencies = tr
43
44
  }
44
45
  const destinations = [];
45
46
  for (const agent of [...new Set(agents)]) {
46
- const native = join(agent === 'codex' ? '.agents' : '.claude', 'skills', 'jobhunt-kit');
47
+ const native = relative(target,skillDirectory(agent,{scope,home:resolve(home),cwd:scope==='project'?target:process.cwd(),env}));
47
48
  destinations.push({agent, path: join(target, native)});
48
49
  files.set(join(native, 'SKILL.md'), join(source, 'installer-assets/native-SKILL.md'));
49
50
  const plugin = join(source, 'plugins/jobhunt-kit');
@@ -87,8 +88,13 @@ export function install(destination, { source = SOURCE, installDependencies = tr
87
88
  return { directory: target, copied: pending.length, dependencies_installed: installDependencies, scope, agents: destinations };
88
89
  }
89
90
  export async function main(args) {
91
+ if (args[0] === 'agents') {
92
+ if (args.length!==1) throw new Error('Usage: jobhunt-kit agents');
93
+ console.log(AGENTS.map(a=>`${a.id.padEnd(14)} ${a.name}${a.globalOnly?' (global only)':''}`).join('\n'));
94
+ return;
95
+ }
90
96
  if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
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.');
97
+ console.log('Usage: jobhunt-kit <command> [arguments] [--workspace dir | --data dir]\n\ninstall [directory] Choose agents and install (default: ./my-jobhunt)\n --agents codex,cursor,... Select agents (or all); --providers is an alias\n agents List supported agents\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.');
92
98
  return;
93
99
  }
94
100
  if (!['init', 'install'].includes(args[0])) {
@@ -107,6 +113,7 @@ export async function main(args) {
107
113
  const result = install(options.destination, options);
108
114
  console.log(`\nJobhunt Kit ready: ${result.directory}\nCopied files: ${result.copied}`);
109
115
  for (const agent of result.agents) console.log(`${agent.agent}: ${agent.path}`);
116
+ if (result.scope==='project' && result.agents.some(a=>a.agent==='hermes')) console.log('Hermes: run hermes skills trust from this workspace before using project skills.');
110
117
  console.log(result.scope === 'global' ? 'Open your working folder in a new agent session.' : 'Open this folder in a new agent session.');
111
118
  console.log('Ask: use jobhunt-kit to initialize my job search profile.\nNo profile, schedule, search or application was created.');
112
119
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "jobhunt-kit-plugin",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "jobhunt-kit-plugin",
9
- "version": "0.3.0",
9
+ "version": "0.4.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.0",
3
+ "version": "0.4.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "jobhunt-kit",
9
- "version": "0.3.0",
9
+ "version": "0.4.0",
10
10
  "dependencies": {
11
11
  "hirify-cli": "0.4.5",
12
12
  "mammoth": "1.12.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobhunt-kit",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "bin": {
5
5
  "jobhunt-kit": "bin/jobhunt-kit.mjs"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobhunt-kit",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Reusable candidate intake, resume review, Hirify search and local application tracking",
5
5
  "author": {
6
6
  "name": "Jobhunt Kit contributors"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobhunt-kit",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Reusable candidate intake, resume review, Hirify search and local application tracking",
5
5
  "author": {
6
6
  "name": "Jobhunt Kit contributors"
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "jobhunt-kit-plugin",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "jobhunt-kit-plugin",
9
- "version": "0.3.0",
9
+ "version": "0.4.0",
10
10
  "dependencies": {
11
11
  "hirify-cli": "0.4.5",
12
12
  "mammoth": "1.12.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobhunt-kit-plugin",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,6 +1,8 @@
1
1
  # Команды рабочих сценариев
2
2
 
3
- Установка: `npx jobhunt-kit install [папка]` с выбором Codex/Claude Code и области.
3
+ Установка: `npx jobhunt-kit install [папка]` с выбором агентов и области.
4
+ Список: `npx jobhunt-kit agents`. Все для выбранной области: `--agents all`.
5
+ `--providers` поддерживается как синоним `--agents`.
4
6
  Без вопросов: `npx jobhunt-kit install --agents codex,claude --scope project`.
5
7
  Глобально: `npx jobhunt-kit install --agents claude --scope global` (без пути).
6
8
  В глобальном режиме устанавливаются только навыки; зависимости и данные создаёт
@@ -0,0 +1,12 @@
1
+ # Установка в агенты
2
+
3
+ Пути сверены с [каталогом провайдеров Impeccable](https://github.com/pbakaus/impeccable/blob/main/crates/skills/src/providers.rs)
4
+ и [его README](https://github.com/pbakaus/impeccable#installation), 2026-09-08.
5
+ Для Veto README описывает только глобальный каталог.
6
+ Для Copilot глобальный путь `.copilot/skills` соответствует
7
+ [документации GitHub](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills).
8
+
9
+ Установщик копирует переносимый SKILL.md и ресурсы. Он не меняет настройки
10
+ доверия, не устанавливает сами приложения и не запускает их. Автоматические тесты
11
+ проверяют все пути, комплектность файлов и конфликты; загрузка навыка в каждом
12
+ из приложений отдельно не проверена.
@@ -6,6 +6,7 @@ import { tmpdir } from 'node:os';
6
6
  import { join, resolve, sep } from 'node:path';
7
7
  import { install } from '../bin/jobhunt-kit.mjs';
8
8
  import { installOptions } from '../bin/install-options.mjs';
9
+ import { AGENTS, parseAgents, skillDirectory, detectAgents } from '../bin/agents.mjs';
9
10
 
10
11
  function target(t) {
11
12
  const path = mkdtempSync(join(tmpdir(), 'job-search-install-'));
@@ -114,3 +115,40 @@ test('global skill can initialize separate data and generate a schedule with per
114
115
  assert.equal(result.scheduled,false);
115
116
  assert.ok(readFileSync(result.path,'utf8').includes(join(home,'.agents/skills/jobhunt-kit/bundle')));
116
117
  });
118
+ test('all supported harnesses receive complete bundles at their project and global paths', t => {
119
+ const project=target(t), home=target(t);
120
+ const projectAgents=AGENTS.filter(a=>!a.globalOnly).map(a=>a.id);
121
+ install(project,{agents:projectAgents,installDependencies:false,env:{}});
122
+ install('unused',{agents:AGENTS.map(a=>a.id),scope:'global',home,env:{}});
123
+ const expected={github:'.copilot/skills',opencode:'.config/opencode/skills',antigravity:'.gemini/config/skills',pi:'.pi/agent/skills'};
124
+ for(const a of AGENTS){
125
+ const global=join(home,expected[a.id] || `${a.dir}/skills`,'jobhunt-kit');
126
+ assert.ok(existsSync(join(global,'SKILL.md')),a.id);
127
+ assert.ok(existsSync(join(global,'bundle/scripts/setup.mjs')),a.id);
128
+ if(!a.globalOnly) assert.ok(existsSync(join(project,a.dir,'skills/jobhunt-kit/SKILL.md')),a.id);
129
+ }
130
+ assert.equal(existsSync(join(project,'.veto')),false);
131
+ });
132
+ test('provider aliases and all work in flags and interactive selection', async () => {
133
+ assert.deepEqual(parseAgents('copilot,github,.claude,claude-code,deepseek'),['github','claude','dsh']);
134
+ const quiet={input:{isTTY:false},output:{isTTY:false}};
135
+ const project=await installOptions(['--providers','all'],quiet);
136
+ assert.equal(project.agents.length,17); assert.equal(project.agents.includes('veto'),false);
137
+ const global=await installOptions(['--agents','all','--scope','global'],quiet);
138
+ assert.equal(global.agents.length,18);
139
+ await assert.rejects(installOptions(['--agents','veto'],quiet),/global/);
140
+ await assert.rejects(installOptions(['--agents','codex','--providers','cursor'],quiet),/either/);
141
+ const answers=['all','project'];
142
+ assert.equal((await installOptions([],{output:{write(){}},ask:async()=>answers.shift()})).agents.length,17);
143
+ });
144
+ test('custom global configuration directories are detected and cannot escape home', t => {
145
+ const home=target(t),cwd=target(t);
146
+ const env={OPENCODE_CONFIG_DIR:join(home,'custom/open'),HERMES_HOME:join(home,'custom/hermes'),DSH_HOME:join(home,'custom/dsh')};
147
+ for(const p of Object.values(env)) mkdirSync(p,{recursive:true});
148
+ for(const id of ['opencode','hermes','dsh']) assert.ok(detectAgents({home,cwd,env}).includes(id));
149
+ assert.equal(skillDirectory('opencode',{home,cwd,env,scope:'global'}),join(env.OPENCODE_CONFIG_DIR,'skills/jobhunt-kit'));
150
+ assert.equal(skillDirectory('opencode',{home,cwd,env:{XDG_CONFIG_HOME:join(home,'xdg')},scope:'global'}),join(home,'xdg/opencode/skills/jobhunt-kit'));
151
+ assert.throws(()=>skillDirectory('hermes',{home,cwd,env:{HERMES_HOME:home+'-sibling'},scope:'global'}),/inside the home/);
152
+ assert.throws(()=>install('unused',{home,scope:'global',agents:['codex','opencode'],env:{OPENCODE_CONFIG_DIR:cwd}}),/inside the home/);
153
+ assert.equal(existsSync(join(home,'.agents')),false);
154
+ });