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.
@@ -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.2.0"
11
+ "version": "0.3.0"
12
12
  }
13
13
  ]
14
14
  }
package/README.md CHANGED
@@ -7,9 +7,10 @@ npx jobhunt-kit install
7
7
  Поиск работы вместе с AI-агентом: профиль кандидата, проверка резюме, подбор вакансий
8
8
  через Hirify, сопроводительные письма и история откликов.
9
9
 
10
- Нужны **Node.js 24+, Git и Codex или Claude Code**. Установщик создаст папку
11
- `my-jobhunt` и установит зависимости. Чтобы выбрать другую папку, добавьте путь
12
- в конце команды: `install ./my-folder`.
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
- codex plugin marketplace add .
90
- codex plugin add jobhunt-kit@personal
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
- После установки начните новую сессию. `personal` имя каталога плагинов,
94
- включённого в шаблон.
95
-
96
- **Claude Code** для текущей сессии:
92
+ В режиме `project` навыки устанавливаются в созданную рабочую папку:
93
+ Codex `.agents/skills/jobhunt-kit`, Claude Code — `.claude/skills/jobhunt-kit`.
94
+ В режиме `global` используются те же каталоги в домашней папке пользователя;
95
+ рабочая папка и профиль создаются позже при обращении к агенту.
97
96
 
98
- ```sh
99
- claude --plugin-dir ./plugins/jobhunt-kit
100
- ```
97
+ После установки откройте новую сессию и попросите использовать **jobhunt-kit**.
98
+ В Claude Code можно вызвать `/jobhunt-kit`. Отдельные команды marketplace
99
+ и `--plugin-dir` для этого способа не нужны.
101
100
 
102
- Вызов навыка: `/jobhunt-kit:job-profile`. Для постоянной установки используйте
103
- `/plugin marketplace add .`, затем `/plugin install jobhunt-kit@jobhunt-kit-marketplace`.
101
+ `--yes` выбирает обнаруженных агентов (или обоих, если ничего не обнаружено)
102
+ и область `project`. Обнаружение использует каталоги агентов, а не запускает их.
103
+ Для скриптов задавайте `--agents` и `--scope` явно.
104
104
 
105
- При отдельной установке плагина агент подготовит локальные зависимости во время
106
- инициализации профиля. Вход в Hirify выполняется отдельно.
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
+ }
@@ -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
- const target = resolve(destination);
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
- for (const root of roots) for (const path of collect(join(source, root))) files.set(relative(source, path), path);
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] Install template (default: ./my-jobhunt)\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.');
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
- if (args.length > 2 || (args[0] === 'init' && !args[1]) || args[1]?.startsWith('-')) {
87
- throw new Error('Usage: jobhunt-kit install [directory] (default: ./my-jobhunt), or jobhunt-kit init <directory>');
88
- }
89
- const result = install(args[1] || './my-jobhunt');
90
- console.log(`\nJobhunt Kit ready: ${result.directory}\nCopied files: ${result.copied}\nOpen this folder in Codex or Claude Code and ask: use job-profile to initialize my profile.\nWhen needed, sign in yourself from that folder: npm run hirify -- login\nNo profile, schedule, search or application was created.`);
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.2.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.2.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.2.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.2.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,6 +1,6 @@
1
1
  {
2
2
  "name": "jobhunt-kit",
3
- "version": "0.2.0",
3
+ "version": "0.3.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.2.0",
3
+ "version": "0.3.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.2.0",
3
+ "version": "0.3.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.2.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.2.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,6 @@
1
1
  {
2
2
  "name": "jobhunt-kit-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -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
- const plugin = join(workspace, 'plugins', 'jobhunt-kit');
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;
@@ -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
+ });