@laitszkin/apollo-toolkit 3.2.1 → 3.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/AGENTS.md CHANGED
@@ -4,8 +4,9 @@
4
4
 
5
5
  - This repository is a skill catalog: each top-level skill lives in its own directory and is installable when that directory contains `SKILL.md`.
6
6
  - Typical skill layout is lightweight and consistent: `SKILL.md`, `README.md`, `LICENSE`, plus optional `agents/`, `references/`, and `scripts/`.
7
- - The npm package exposes an `apollo-toolkit` CLI that stages a managed copy under `~/.apollo-toolkit` and copies each skill folder into selected target directories.
8
- - `scripts/install_skills.sh` and `scripts/install_skills.ps1` remain available for local/curl installs and mirror the managed-home copy behavior.
7
+ - The npm package exposes an `apollo-toolkit` CLI that stages a managed copy under `~/.apollo-toolkit` and copies or symlinks each skill folder into selected target directories.
8
+ - The installer writes a `.apollo-toolkit-manifest.json` per target directory to track installed skills, historical skill names, and install mode for future uninstall and deduplication.
9
+ - `scripts/install_skills.sh` and `scripts/install_skills.ps1` remain available for local/curl installs and mirror the managed-home install behavior with symlink/copy choice and uninstall support.
9
10
 
10
11
  ## Core Business Flow
11
12
 
@@ -20,7 +21,9 @@ This repository enables users to install and run a curated set of reusable agent
20
21
  - Users can research a topic deeply and produce evidence-based deliverables.
21
22
  - Users can research the latest completed market week and produce a PDF watchlist of tradeable instruments for the coming week.
22
23
  - Users can turn a marked weekly finance PDF into a concise evidence-based financial event report.
23
- - Users can install Apollo Toolkit through npm or npx and interactively choose one or more target skill directories to populate with copied skills.
24
+ - Users can install Apollo Toolkit through npm or npx and interactively choose one or more target skill directories to populate with copied or symlinked skills, with the option to include codex-exclusive skills in non-codex targets.
25
+ - Users can uninstall all Apollo Toolkit-installed skills from all targets or specific targets via `apltk uninstall`.
26
+ - Users can choose between symlink mode (auto-update via git pull) and copy mode (stable snapshot) with `--symlink` / `--copy` flags.
24
27
  - Users can run bundled helper tools through `apltk tools` and direct `apltk <tool>` commands for selected packaged skill scripts.
25
28
  - Users can design and implement new features through a spec-first workflow.
26
29
  - Users can generate shared feature planning artifacts for approval-gated workflows, including parallel multi-spec batches coordinated through one batch-level `coordination.md`.
@@ -72,7 +75,11 @@ This repository enables users to install and run a curated set of reusable agent
72
75
  - `python3 scripts/validate_skill_frontmatter.py` - 驗證所有頂層技能 `SKILL.md` 的 frontmatter。
73
76
  - `python3 scripts/validate_openai_agent_config.py` - 驗證所有技能 `agents/openai.yaml` 設定。
74
77
  - `./scripts/install_skills.sh codex` - 用本地安裝腳本把技能安裝到 Codex 目錄。
75
- - `./scripts/install_skills.sh all` - 用本地安裝腳本同步安裝到所有支援目標。
78
+ - `./scripts/install_skills.sh codex --symlink` - 以 symlink 模式安裝(推薦)。
79
+ - `./scripts/install_skills.sh all --copy` - 以複製模式安裝到所有支援目標。
80
+ - `./scripts/install_skills.sh uninstall` - 從所有目標移除已安裝的技能。
81
+ - `./scripts/install_skills.sh uninstall codex` - 只從 codex 目標移除。
82
+ - `node bin/apollo-toolkit.js uninstall` - 透過 CLI 移除所有已安裝技能。
76
83
 
77
84
  ## Core Project Purpose
78
85
 
package/CHANGELOG.md CHANGED
@@ -4,8 +4,26 @@ All notable changes to this repository are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### Added
8
+ - (None yet)
9
+
10
+ ## [v3.3.0] - 2026-04-26
11
+
12
+ ### Added
13
+ - Add `apltk uninstall` command to remove all installed skills from all targets (or specific targets) via manifest-based cleanup.
14
+ - Add symlink install mode (`--symlink`) so skills auto-update when `git pull` runs in `~/.apollo-toolkit`, removing the need to re-run the installer after patch updates.
15
+ - Add `--copy` flag to explicitly select copy mode when symlink is not desired.
16
+ - Add interactive prompt during install that explains symlink pros/cons and lets the user choose between symlink and copy mode.
17
+ - Add interactive prompt to optionally install codex-exclusive skills into non-codex targets during global install.
18
+ - Add `.apollo-toolkit-manifest.json` per target directory to track installed skills, historical skill names, and install mode for future uninstall and deduplication.
19
+ - Add `listAllKnownSkillNames()` to combine current and historically-appeared skill names with automatic deduplication.
20
+ - Add `uninstall` subcommand to `scripts/install_skills.sh` and `scripts/install_skills.ps1`.
21
+ - Add `--symlink` / `--copy` flags to both shell and PowerShell install scripts.
22
+
23
+ ## [v3.2.2] - 2026-04-25
24
+
7
25
  ### Changed
8
- - None yet.
26
+ - Tighten `implement-specs-with-worktree` so targeted Rust verification must avoid multi-filter `cargo test` invocations and rerun any zero-test selector before treating the worktree spec as validated.
9
27
 
10
28
  ## [v3.2.1] - 2026-04-24
11
29
 
package/README.md CHANGED
@@ -65,9 +65,26 @@ The interactive installer:
65
65
  - shows a branded `Apollo Toolkit` terminal welcome screen with a short staged reveal
66
66
  - installs a managed copy into `~/.apollo-toolkit`
67
67
  - lets you multi-select `codex`, `openclaw`, `trae`, `agents`, `claude-code`, or `all`
68
- - copies `~/.apollo-toolkit/<skill>` into each selected target
68
+ - asks whether to install skills as **symlinks** (recommended) or **file copies**
69
+ - lets you choose whether to include codex-exclusive skills in non-codex targets
70
+ - copies or symlinks `~/.apollo-toolkit/<skill>` into each selected target
69
71
  - removes stale previously installed skill directories that existed in the previous installed version but no longer exist in the current package skill list
70
72
  - replaces legacy symlink-based installs created by older Apollo Toolkit installers with real copied directories
73
+ - writes a manifest (`.apollo-toolkit-manifest.json`) per target for future uninstall and skill tracking
74
+
75
+ ### Symlink vs Copy
76
+
77
+ | Mode | Pro | Con |
78
+ | --- | --- | --- |
79
+ | **Symlink** (recommended) | Auto-updates when you `git pull` in `~/.apollo-toolkit`; no need to re-run installer after patch updates | Changes pushed to the repo automatically reflect in your skills — you may receive updates you did not intend to accept |
80
+ | **Copy** | Stable snapshot; won't change until you re-run the installer | Must manually re-run `apltk` after each toolkit update to get the latest skills |
81
+
82
+ ### Uninstall
83
+
84
+ ```bash
85
+ apltk uninstall # Remove all installed skills from all targets
86
+ apltk uninstall codex # Remove only from codex
87
+ ```
71
88
 
72
89
  ### Global install
73
90
 
@@ -98,6 +115,13 @@ npx @laitszkin/apollo-toolkit codex openclaw
98
115
  npx @laitszkin/apollo-toolkit all
99
116
  ```
100
117
 
118
+ Add `--symlink` (recommended) or `--copy` to skip the interactive prompt:
119
+
120
+ ```bash
121
+ npx @laitszkin/apollo-toolkit codex --symlink
122
+ npx @laitszkin/apollo-toolkit all --copy
123
+ ```
124
+
101
125
  ### Optional overrides
102
126
 
103
127
  ```bash
@@ -121,24 +145,27 @@ Installers still live in `scripts/` for local repository usage and curl / iwr in
121
145
  ```bash
122
146
  ./scripts/install_skills.sh
123
147
  ./scripts/install_skills.sh codex
124
- ./scripts/install_skills.sh openclaw
125
- ./scripts/install_skills.sh trae
126
- ./scripts/install_skills.sh agents
127
- ./scripts/install_skills.sh all
148
+ ./scripts/install_skills.sh codex --symlink
149
+ ./scripts/install_skills.sh all --copy
150
+ ./scripts/install_skills.sh uninstall
151
+ ./scripts/install_skills.sh uninstall codex trae
128
152
  ```
129
153
 
130
154
  ```powershell
131
155
  ./scripts/install_skills.ps1
132
156
  ./scripts/install_skills.ps1 codex
133
- ./scripts/install_skills.ps1 agents
134
- ./scripts/install_skills.ps1 all
157
+ ./scripts/install_skills.ps1 agents --symlink
158
+ ./scripts/install_skills.ps1 all --copy
159
+ ./scripts/install_skills.ps1 uninstall
160
+ ./scripts/install_skills.ps1 uninstall codex trae
135
161
  ```
136
162
 
137
163
  ### Curl / iwr one-liners
138
164
 
139
165
  ```bash
140
166
  curl -fsSL https://raw.githubusercontent.com/LaiTszKin/apollo-toolkit/main/scripts/install_skills.sh | bash
141
- curl -fsSL https://raw.githubusercontent.com/LaiTszKin/apollo-toolkit/main/scripts/install_skills.sh | bash -s -- codex
167
+ curl -fsSL https://raw.githubusercontent.com/LaiTszKin/apollo-toolkit/main/scripts/install_skills.sh | bash -s -- codex --symlink
168
+ curl -fsSL https://raw.githubusercontent.com/LaiTszKin/apollo-toolkit/main/scripts/install_skills.sh | bash -s -- uninstall
142
169
  ```
143
170
 
144
171
  ```powershell
@@ -25,7 +25,7 @@ description: >-
25
25
 
26
26
  - Evidence: Read and understand the complete specs set before starting implementation, identify the authoritative parent branch that the worktree should inherit from, verify whether the requested scope is already implemented on that parent branch or current main working tree, and when the requested plan path is missing from the current worktree verify where the authoritative copy actually lives before substituting any nearby spec.
27
27
  - Execution: Create or use an isolated worktree for implementation only when the requested spec still needs work, sync the exact approved plan set into that worktree when it is missing there, create the worktree branch from the same parent branch as the worktree base, use the spec-set name as the canonical branch/worktree name, prefer direct `git` ref checks over brittle shell inference when deciding whether a branch or worktree already exists, and commit to a local branch when done.
28
- - Quality: Complete all planned tasks, run relevant tests, backfill the spec documents with actual completion status, avoid dragging unrelated sibling specs into the worktree just because they share a batch directory, revert unrelated formatter-only noise outside the spec-owned scope before committing, and if branch/worktree creation reports ambiguous state re-check the actual git refs and worktree list before retrying.
28
+ - Quality: Complete all planned tasks, run relevant tests, backfill the spec documents with actual completion status, avoid dragging unrelated sibling specs into the worktree just because they share a batch directory, revert unrelated formatter-only noise outside the spec-owned scope before committing, if branch/worktree creation reports ambiguous state re-check the actual git refs and worktree list before retrying, and when using targeted Rust `cargo test` selectors remember Cargo accepts only one positional test filter so each distinct selector needs its own confirmed command.
29
29
  - Output: Keep the worktree branch clean with only the intended implementation commits.
30
30
 
31
31
  ## Goal
@@ -109,6 +109,8 @@ Use branch naming from `references/branch-naming.md`.
109
109
  - E2E tests for key user-visible paths
110
110
  - Adversarial tests for abuse paths
111
111
  - Run relevant tests and fix failures.
112
+ - When using targeted Rust `cargo test` commands, pass at most one positional test filter per invocation; if multiple selectors are needed, run separate commands or a broader confirmed selector.
113
+ - Treat any targeted test command that executes zero tests as non-verification and rerun with a selector that proves the intended coverage actually ran.
112
114
  - Do not skip testing even for seemingly small changes.
113
115
 
114
116
  ### 5) Backfill completion status
package/lib/cli.js CHANGED
@@ -6,9 +6,12 @@ const {
6
6
  TARGET_DEFINITIONS,
7
7
  VALID_MODES,
8
8
  installLinks,
9
+ listAllKnownSkillNames,
10
+ listCodexSkillNames,
9
11
  normalizeModes,
10
12
  resolveToolkitHome,
11
13
  syncToolkitHome,
14
+ uninstallSkills,
12
15
  getTargetRoots,
13
16
  } = require('./installer');
14
17
  const { formatToolList, getToolCommand, runTool } = require('./tool-runner');
@@ -41,7 +44,7 @@ function color(text, code, enabled) {
41
44
  return text;
42
45
  }
43
46
 
44
- return `\u001b[${code}m${text}\u001b[0m`;
47
+ return `[${code}m${text}`;
45
48
  }
46
49
 
47
50
  function sleep(ms) {
@@ -134,6 +137,7 @@ function buildHelpText({ version, colorEnabled }) {
134
137
  'Usage:',
135
138
  ` apltk [install] [${buildModeUsagePattern()}]...`,
136
139
  ` apollo-toolkit [install] [${buildModeUsagePattern()}]...`,
140
+ ' apltk uninstall',
137
141
  ' apltk tools',
138
142
  ' apltk <tool> [...args]',
139
143
  ' apltk tools <tool> [...args]',
@@ -143,6 +147,7 @@ function buildHelpText({ version, colorEnabled }) {
143
147
  'Examples:',
144
148
  ' apltk',
145
149
  ' apltk codex openclaw',
150
+ ' apltk uninstall',
146
151
  ' npx @laitszkin/apollo-toolkit',
147
152
  ' npx @laitszkin/apollo-toolkit codex openclaw',
148
153
  ' npm i -g @laitszkin/apollo-toolkit',
@@ -194,8 +199,24 @@ function parseArguments(argv) {
194
199
  toolkitHome: null,
195
200
  toolName: null,
196
201
  toolArgs: [],
202
+ linkMode: null, // 'copy' | 'symlink' | null (prompt)
197
203
  };
198
204
 
205
+ if (args[0] === 'uninstall') {
206
+ result.command = 'uninstall';
207
+ args.shift();
208
+ // remaining args could be specific modes
209
+ while (args.length > 0) {
210
+ const arg = args.shift();
211
+ if (arg === '--help' || arg === '-h') {
212
+ result.showHelp = true;
213
+ } else {
214
+ result.modes.push(arg);
215
+ }
216
+ }
217
+ return result;
218
+ }
219
+
199
220
  if (args[0] === 'tools' || args[0] === 'tool') {
200
221
  args.shift();
201
222
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
@@ -234,6 +255,16 @@ function parseArguments(argv) {
234
255
  continue;
235
256
  }
236
257
 
258
+ if (arg === '--symlink') {
259
+ result.linkMode = 'symlink';
260
+ continue;
261
+ }
262
+
263
+ if (arg === '--copy') {
264
+ result.linkMode = 'copy';
265
+ continue;
266
+ }
267
+
237
268
  if (arg === 'install') {
238
269
  continue;
239
270
  }
@@ -246,7 +277,7 @@ function parseArguments(argv) {
246
277
 
247
278
  function clearScreen(output) {
248
279
  if (output.isTTY) {
249
- output.write('\u001b[2J\u001b[H');
280
+ output.write('');
250
281
  }
251
282
  }
252
283
 
@@ -315,20 +346,20 @@ async function promptForModes({ stdin, stdout, version, env }) {
315
346
 
316
347
  const onData = (chunk) => {
317
348
  const value = chunk.toString('utf8');
318
- if (value === '\u0003') {
349
+ if (value === '') {
319
350
  cleanup();
320
351
  reject(new Error('Installation cancelled.'));
321
352
  return;
322
353
  }
323
354
 
324
- if (value === '\u001b[A' || value === 'k') {
355
+ if (value === '' || value === 'k') {
325
356
  cursor = (cursor - 1 + TARGET_OPTIONS.length) % TARGET_OPTIONS.length;
326
357
  message = '';
327
358
  renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
328
359
  return;
329
360
  }
330
361
 
331
- if (value === '\u001b[B' || value === 'j') {
362
+ if (value === '' || value === 'j') {
332
363
  cursor = (cursor + 1) % TARGET_OPTIONS.length;
333
364
  message = '';
334
365
  renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
@@ -349,7 +380,7 @@ async function promptForModes({ stdin, stdout, version, env }) {
349
380
  return;
350
381
  }
351
382
 
352
- if (value.toLowerCase() === 'q' || value === '\u001b') {
383
+ if (value.toLowerCase() === 'q' || value === '') {
353
384
  cleanup();
354
385
  reject(new Error('Installation cancelled.'));
355
386
  return;
@@ -374,11 +405,77 @@ async function promptForModes({ stdin, stdout, version, env }) {
374
405
  });
375
406
  }
376
407
 
377
- async function confirmInstall({ stdin, stdout, version, toolkitHome, modes, env }) {
408
+ async function promptYesNo({ stdin, stdout, env, question, defaultYes = true }) {
409
+ if (!stdin.isTTY || !stdout.isTTY) {
410
+ return defaultYes;
411
+ }
412
+
413
+ const rl = createInterface({ input: stdin, output: stdout });
414
+ try {
415
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
416
+ const answer = await rl.question(`${question} ${hint} `);
417
+ const trimmed = answer.trim().toLowerCase();
418
+ if (trimmed === '') {
419
+ return defaultYes;
420
+ }
421
+ return trimmed === 'y' || trimmed === 'yes';
422
+ } finally {
423
+ rl.close();
424
+ }
425
+ }
426
+
427
+ function buildSymlinkInfo({ colorEnabled }) {
428
+ return [
429
+ '',
430
+ color('Symlink mode:', '1', colorEnabled),
431
+ ` ${color('Pro:', '1;32', colorEnabled)} Skills auto-update when you ${color('git pull', '1;33', colorEnabled)} in ~/.apollo-toolkit`,
432
+ ` ${color('Pro:', '1;32', colorEnabled)} No need to re-run installer after patch updates`,
433
+ ` ${color('Con:', '1;31', colorEnabled)} Changes pushed to the repo automatically reflect in your skills -`,
434
+ ` you may receive updates you did not intend to accept`,
435
+ '',
436
+ ].join('\n');
437
+ }
438
+
439
+ async function promptSymlinkChoice({ stdin, stdout, env, colorEnabled }) {
440
+ stdout.write(buildSymlinkInfo({ colorEnabled }));
441
+ return promptYesNo({
442
+ stdin,
443
+ stdout,
444
+ env,
445
+ question: 'Install skills as symlinks (recommended)?',
446
+ defaultYes: true,
447
+ });
448
+ }
449
+
450
+ // Ask user whether to include codex-exclusive skills in non-codex targets.
451
+ async function promptIncludeExclusiveSkills({ stdin, stdout, env, colorEnabled, codexSkillNames, nonCodexModes }) {
452
+ if (codexSkillNames.length === 0 || nonCodexModes.length === 0) {
453
+ return false;
454
+ }
455
+
456
+ stdout.write([
457
+ '',
458
+ color('Exclusive skills detected:', '1;33', colorEnabled),
459
+ ` The following skills are exclusive to codex: ${codexSkillNames.join(', ')}`,
460
+ ` Your selected non-codex targets: ${nonCodexModes.join(', ')}`,
461
+ '',
462
+ ].join('\n'));
463
+
464
+ return promptYesNo({
465
+ stdin,
466
+ stdout,
467
+ env,
468
+ question: `Install codex-exclusive skills to ${nonCodexModes.join(', ')} as well?`,
469
+ defaultYes: false,
470
+ });
471
+ }
472
+
473
+ async function confirmInstall({ stdin, stdout, version, toolkitHome, modes, linkMode, env }) {
378
474
  const colorEnabled = supportsColor(stdout, env);
379
475
  stdout.write(`${buildBanner({ version, colorEnabled })}\n\n`);
380
476
  stdout.write(`Apollo Toolkit home: ${toolkitHome}\n`);
381
- stdout.write(`Targets: ${modes.join(', ')}\n\n`);
477
+ stdout.write(`Targets: ${modes.join(', ')}\n`);
478
+ stdout.write(`Install mode: ${linkMode === 'symlink' ? 'symlink (auto-update via git pull)' : 'copy (manual reinstall for updates)'}\n\n`);
382
479
 
383
480
  const targets = await getTargetRoots(modes, env).catch((error) => {
384
481
  throw error;
@@ -408,6 +505,7 @@ function printSummary({ stdout, version, toolkitHome, modes, installResult, env
408
505
  stdout.write('\n');
409
506
  stdout.write(`Apollo Toolkit home: ${toolkitHome}\n`);
410
507
  stdout.write(`Installed skills: ${installResult.skillNames.length}\n`);
508
+ stdout.write(`Install mode: ${installResult.linkMode === 'symlink' ? 'symlink' : 'copy'}\n`);
411
509
  stdout.write(`Targets: ${modes.join(', ')}\n\n`);
412
510
 
413
511
  for (const target of installResult.targets) {
@@ -415,6 +513,22 @@ function printSummary({ stdout, version, toolkitHome, modes, installResult, env
415
513
  }
416
514
  }
417
515
 
516
+ function printUninstallSummary({ stdout, uninstallResult, env }) {
517
+ const colorEnabled = supportsColor(stdout, env);
518
+
519
+ if (uninstallResult.length === 0) {
520
+ stdout.write(color('No Apollo Toolkit installations found.\n', '1;33', colorEnabled));
521
+ return;
522
+ }
523
+
524
+ stdout.write(color('Uninstall complete.', '1;32', colorEnabled));
525
+ stdout.write('\n\n');
526
+ for (const result of uninstallResult) {
527
+ stdout.write(`${color(result.target, '1', colorEnabled)} (${result.root})\n`);
528
+ stdout.write(` Removed: ${result.removedSkills.join(', ')}\n`);
529
+ }
530
+ }
531
+
418
532
  async function run(argv, context = {}) {
419
533
  const sourceRoot = context.sourceRoot || path.resolve(__dirname, '..');
420
534
  const stdout = context.stdout || process.stdout;
@@ -425,14 +539,17 @@ async function run(argv, context = {}) {
425
539
 
426
540
  try {
427
541
  const parsed = parseArguments(argv);
542
+
428
543
  if (parsed.showHelp) {
429
544
  stdout.write(`${buildHelpText({ version: packageJson.version, colorEnabled: supportsColor(stdout, env) })}\n`);
430
545
  return 0;
431
546
  }
547
+
432
548
  if (parsed.showToolsHelp) {
433
549
  stdout.write(`${buildToolsHelp({ version: packageJson.version, colorEnabled: supportsColor(stdout, env) })}\n`);
434
550
  return 0;
435
551
  }
552
+
436
553
  if (parsed.command === 'tool') {
437
554
  return (context.runTool || runTool)(parsed.toolName, parsed.toolArgs, {
438
555
  sourceRoot,
@@ -443,6 +560,39 @@ async function run(argv, context = {}) {
443
560
  });
444
561
  }
445
562
 
563
+ // --- Uninstall flow ---
564
+ if (parsed.command === 'uninstall') {
565
+ const toolkitHome = parsed.toolkitHome || resolveToolkitHome(env);
566
+ const modes = parsed.modes.length > 0 ? normalizeModes(parsed.modes) : null;
567
+
568
+ // Show what will be removed
569
+ const allKnown = await listAllKnownSkillNames({ toolkitHome, modes: modes || [], env });
570
+ stdout.write(color(`Apollo Toolkit home: ${toolkitHome}\n`, '2', supportsColor(stdout, env)));
571
+
572
+ const confirmed = await promptYesNo({
573
+ stdin,
574
+ stdout,
575
+ env,
576
+ question: `This will remove all Apollo Toolkit-installed skills${modes ? ` from: ${modes.join(', ')}` : ' from all targets'}. Continue?`,
577
+ defaultYes: false,
578
+ });
579
+
580
+ if (!confirmed) {
581
+ stdout.write('Uninstall cancelled.\n');
582
+ return 1;
583
+ }
584
+
585
+ const uninstallResult = await uninstallSkills({ env, modes });
586
+ printUninstallSummary({ stdout, uninstallResult, env });
587
+
588
+ if (allKnown.length > 0) {
589
+ stdout.write(`\nPreviously known skills (may still exist elsewhere): ${allKnown.join(', ')}\n`);
590
+ }
591
+
592
+ return 0;
593
+ }
594
+
595
+ // --- Install flow ---
446
596
  const updateResult = await checkForPackageUpdate({
447
597
  packageName: packageJson.name,
448
598
  currentVersion: packageJson.version,
@@ -463,12 +613,39 @@ async function run(argv, context = {}) {
463
613
  ? normalizeModes(parsed.modes)
464
614
  : normalizeModes(await promptForModes({ stdin, stdout, version: packageJson.version, env }));
465
615
 
616
+ const colorEnabled = supportsColor(stdout, env);
617
+
618
+ // Determine link mode
619
+ let linkMode = parsed.linkMode;
620
+ if (!linkMode) {
621
+ linkMode = (await promptSymlinkChoice({ stdin, stdout, env, colorEnabled })) ? 'symlink' : 'copy';
622
+ }
623
+
624
+ // Determine whether to include exclusive (codex) skills in non-codex targets
625
+ const nonCodexModes = modes.filter((m) => m !== 'codex');
626
+ const codexSkillNames = await listCodexSkillNames(toolkitHome).catch(() => []);
627
+ const includeExclusiveSkills = await promptIncludeExclusiveSkills({
628
+ stdin,
629
+ stdout,
630
+ env,
631
+ colorEnabled,
632
+ codexSkillNames,
633
+ nonCodexModes,
634
+ });
635
+
636
+ // syncToolkitHome needs to include the codex container when exclusive skills
637
+ // are requested, so the source files are available for symlink/copy.
638
+ const effectiveModes = includeExclusiveSkills
639
+ ? [...new Set([...modes, 'codex'])]
640
+ : modes;
641
+
466
642
  const confirmed = await confirmInstall({
467
643
  stdin,
468
644
  stdout,
469
645
  version: packageJson.version,
470
646
  toolkitHome,
471
647
  modes,
648
+ linkMode,
472
649
  env,
473
650
  });
474
651
 
@@ -481,13 +658,15 @@ async function run(argv, context = {}) {
481
658
  sourceRoot,
482
659
  toolkitHome,
483
660
  version: packageJson.version,
484
- modes,
661
+ modes: effectiveModes,
485
662
  });
486
663
 
487
664
  const installResult = await installLinks({
488
665
  toolkitHome,
489
666
  modes,
490
667
  previousSkillNames: syncResult.previousSkillNames,
668
+ linkMode,
669
+ includeExclusiveSkills,
491
670
  env: {
492
671
  ...env,
493
672
  APOLLO_TOOLKIT_HOME: toolkitHome,
@@ -509,6 +688,8 @@ module.exports = {
509
688
  buildToolsHelp,
510
689
  parseArguments,
511
690
  promptForModes,
691
+ promptSymlinkChoice,
692
+ promptIncludeExclusiveSkills,
512
693
  readPackageJson,
513
694
  run,
514
695
  };