@laitszkin/apollo-toolkit 3.2.2 → 3.3.1

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 Apollo Toolkit-installed skills through an interactive target selector or specific non-interactive 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,12 @@ 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 互動選擇要移除的 agent target 技能。
83
+ - `node bin/apollo-toolkit.js uninstall codex --yes` - 以非互動方式移除指定 target 的已安裝技能。
76
84
 
77
85
  ## Core Project Purpose
78
86
 
package/CHANGELOG.md CHANGED
@@ -4,8 +4,32 @@ All notable changes to this repository are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- ### Changed
8
- - None yet.
7
+ ### Added
8
+ - (None yet)
9
+
10
+ ## [v3.3.1] - 2026-04-26
11
+
12
+ ### Added
13
+ - Add an interactive `apltk uninstall` target selector so users can choose which agent skill directories to remove.
14
+ - Add `apltk uninstall --yes` for non-interactive uninstall confirmation.
15
+
16
+ ### Fixed
17
+ - Fix default `apltk uninstall` cleanup so a missing OpenClaw workspace no longer prevents uninstalling Codex, Trae, Agents, or Claude Code targets.
18
+ - Remove manifest-tracked historical skills during CLI uninstall so renamed or removed skills do not remain behind.
19
+ - Ignore unsafe manifest skill names during install and uninstall cleanup so removals remain scoped to direct child skill directories.
20
+
21
+ ## [v3.3.0] - 2026-04-26
22
+
23
+ ### Added
24
+ - Add `apltk uninstall` command to remove all installed skills from all targets (or specific targets) via manifest-based cleanup.
25
+ - 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.
26
+ - Add `--copy` flag to explicitly select copy mode when symlink is not desired.
27
+ - Add interactive prompt during install that explains symlink pros/cons and lets the user choose between symlink and copy mode.
28
+ - Add interactive prompt to optionally install codex-exclusive skills into non-codex targets during global install.
29
+ - Add `.apollo-toolkit-manifest.json` per target directory to track installed skills, historical skill names, and install mode for future uninstall and deduplication.
30
+ - Add `listAllKnownSkillNames()` to combine current and historically-appeared skill names with automatic deduplication.
31
+ - Add `uninstall` subcommand to `scripts/install_skills.sh` and `scripts/install_skills.ps1`.
32
+ - Add `--symlink` / `--copy` flags to both shell and PowerShell install scripts.
9
33
 
10
34
  ## [v3.2.2] - 2026-04-25
11
35
 
package/README.md CHANGED
@@ -65,9 +65,30 @@ 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 # Choose which agent targets to uninstall
86
+ apltk uninstall codex # Remove only from codex
87
+ apltk uninstall codex agents --yes # Non-interactive cleanup for selected targets
88
+ ```
89
+
90
+ The uninstall flow removes the manifest-tracked current and historical skill
91
+ directories for the selected targets, then removes each target manifest.
71
92
 
72
93
  ### Global install
73
94
 
@@ -98,6 +119,13 @@ npx @laitszkin/apollo-toolkit codex openclaw
98
119
  npx @laitszkin/apollo-toolkit all
99
120
  ```
100
121
 
122
+ Add `--symlink` (recommended) or `--copy` to skip the interactive prompt:
123
+
124
+ ```bash
125
+ npx @laitszkin/apollo-toolkit codex --symlink
126
+ npx @laitszkin/apollo-toolkit all --copy
127
+ ```
128
+
101
129
  ### Optional overrides
102
130
 
103
131
  ```bash
@@ -121,24 +149,27 @@ Installers still live in `scripts/` for local repository usage and curl / iwr in
121
149
  ```bash
122
150
  ./scripts/install_skills.sh
123
151
  ./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
152
+ ./scripts/install_skills.sh codex --symlink
153
+ ./scripts/install_skills.sh all --copy
154
+ ./scripts/install_skills.sh uninstall
155
+ ./scripts/install_skills.sh uninstall codex trae
128
156
  ```
129
157
 
130
158
  ```powershell
131
159
  ./scripts/install_skills.ps1
132
160
  ./scripts/install_skills.ps1 codex
133
- ./scripts/install_skills.ps1 agents
134
- ./scripts/install_skills.ps1 all
161
+ ./scripts/install_skills.ps1 agents --symlink
162
+ ./scripts/install_skills.ps1 all --copy
163
+ ./scripts/install_skills.ps1 uninstall
164
+ ./scripts/install_skills.ps1 uninstall codex trae
135
165
  ```
136
166
 
137
167
  ### Curl / iwr one-liners
138
168
 
139
169
  ```bash
140
170
  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
171
+ curl -fsSL https://raw.githubusercontent.com/LaiTszKin/apollo-toolkit/main/scripts/install_skills.sh | bash -s -- codex --symlink
172
+ curl -fsSL https://raw.githubusercontent.com/LaiTszKin/apollo-toolkit/main/scripts/install_skills.sh | bash -s -- uninstall
142
173
  ```
143
174
 
144
175
  ```powershell
package/lib/cli.js CHANGED
@@ -6,16 +6,20 @@ 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,
16
+ getUninstallTargetRoots,
13
17
  } = require('./installer');
14
18
  const { formatToolList, getToolCommand, runTool } = require('./tool-runner');
15
19
  const { checkForPackageUpdate } = require('./updater');
16
20
 
17
21
  const TARGET_OPTIONS = [
18
- { id: 'all', label: 'All', description: 'Install every supported target below' },
22
+ { id: 'all', label: 'All', description: 'Select every supported target below' },
19
23
  ...TARGET_DEFINITIONS,
20
24
  ];
21
25
 
@@ -41,7 +45,7 @@ function color(text, code, enabled) {
41
45
  return text;
42
46
  }
43
47
 
44
- return `\u001b[${code}m${text}\u001b[0m`;
48
+ return `[${code}m${text}`;
45
49
  }
46
50
 
47
51
  function sleep(ms) {
@@ -134,6 +138,7 @@ function buildHelpText({ version, colorEnabled }) {
134
138
  'Usage:',
135
139
  ` apltk [install] [${buildModeUsagePattern()}]...`,
136
140
  ` apollo-toolkit [install] [${buildModeUsagePattern()}]...`,
141
+ ` apltk uninstall [${buildModeUsagePattern()}]... [--yes]`,
137
142
  ' apltk tools',
138
143
  ' apltk <tool> [...args]',
139
144
  ' apltk tools <tool> [...args]',
@@ -143,6 +148,8 @@ function buildHelpText({ version, colorEnabled }) {
143
148
  'Examples:',
144
149
  ' apltk',
145
150
  ' apltk codex openclaw',
151
+ ' apltk uninstall',
152
+ ' apltk uninstall codex agents --yes',
146
153
  ' npx @laitszkin/apollo-toolkit',
147
154
  ' npx @laitszkin/apollo-toolkit codex openclaw',
148
155
  ' npm i -g @laitszkin/apollo-toolkit',
@@ -159,6 +166,7 @@ function buildHelpText({ version, colorEnabled }) {
159
166
  '',
160
167
  'Options:',
161
168
  ' --home <path> Override Apollo Toolkit home directory',
169
+ ' --yes, -y Skip uninstall confirmation',
162
170
  ' --help Show this help text',
163
171
  ].join('\n');
164
172
  }
@@ -194,8 +202,32 @@ function parseArguments(argv) {
194
202
  toolkitHome: null,
195
203
  toolName: null,
196
204
  toolArgs: [],
205
+ linkMode: null, // 'copy' | 'symlink' | null (prompt)
206
+ assumeYes: false,
197
207
  };
198
208
 
209
+ if (args[0] === 'uninstall') {
210
+ result.command = 'uninstall';
211
+ args.shift();
212
+ while (args.length > 0) {
213
+ const arg = args.shift();
214
+ if (arg === '--help' || arg === '-h') {
215
+ result.showHelp = true;
216
+ } else if (arg === '--yes' || arg === '-y') {
217
+ result.assumeYes = true;
218
+ } else if (arg === '--home') {
219
+ const toolkitHome = args.shift();
220
+ if (!toolkitHome) {
221
+ throw new Error('Missing value for --home');
222
+ }
223
+ result.toolkitHome = path.resolve(toolkitHome);
224
+ } else {
225
+ result.modes.push(arg);
226
+ }
227
+ }
228
+ return result;
229
+ }
230
+
199
231
  if (args[0] === 'tools' || args[0] === 'tool') {
200
232
  args.shift();
201
233
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
@@ -234,6 +266,16 @@ function parseArguments(argv) {
234
266
  continue;
235
267
  }
236
268
 
269
+ if (arg === '--symlink') {
270
+ result.linkMode = 'symlink';
271
+ continue;
272
+ }
273
+
274
+ if (arg === '--copy') {
275
+ result.linkMode = 'copy';
276
+ continue;
277
+ }
278
+
237
279
  if (arg === 'install') {
238
280
  continue;
239
281
  }
@@ -246,17 +288,25 @@ function parseArguments(argv) {
246
288
 
247
289
  function clearScreen(output) {
248
290
  if (output.isTTY) {
249
- output.write('\u001b[2J\u001b[H');
291
+ output.write('');
250
292
  }
251
293
  }
252
294
 
253
- function renderSelectionScreen({ output, version, cursor, selected, message, env }) {
295
+ function renderSelectionScreen({
296
+ output,
297
+ version,
298
+ cursor,
299
+ selected,
300
+ message,
301
+ env,
302
+ intro = 'Choose where Apollo Toolkit should copy managed skills.',
303
+ }) {
254
304
  const colorEnabled = supportsColor(output, env);
255
305
  const allSelected = VALID_MODES.every((mode) => selected.has(mode));
256
306
 
257
307
  clearScreen(output);
258
308
  output.write(`${buildBanner({ version, colorEnabled })}\n\n`);
259
- output.write('Choose where Apollo Toolkit should copy managed skills.\n');
309
+ output.write(`${intro}\n`);
260
310
  output.write(`${color('Use Up/Down', '1;33', colorEnabled)} (or ${color('j/k', '1;33', colorEnabled)}) to move, ${color('Space', '1;33', colorEnabled)} to toggle, ${color('Enter', '1;33', colorEnabled)} to continue.\n`);
261
311
  output.write(`Press ${color('a', '1;33', colorEnabled)} to toggle all, ${color('q', '1;33', colorEnabled)} to cancel.\n\n`);
262
312
 
@@ -277,13 +327,11 @@ function renderSelectionScreen({ output, version, cursor, selected, message, env
277
327
  }
278
328
  }
279
329
 
280
- async function promptForModes({ stdin, stdout, version, env }) {
330
+ async function promptForSelectableModes({ stdin, stdout, version, env, intro, ttyError, cancelMessage }) {
281
331
  if (!stdin.isTTY || !stdout.isTTY) {
282
- throw new Error(`Interactive install requires a TTY. Re-run with targets like ${buildInteractiveModeHint()}.`);
332
+ throw new Error(ttyError);
283
333
  }
284
334
 
285
- await animateWelcomeScreen({ output: stdout, version, env });
286
-
287
335
  return new Promise((resolve, reject) => {
288
336
  let cursor = 0;
289
337
  let message = '';
@@ -315,50 +363,50 @@ async function promptForModes({ stdin, stdout, version, env }) {
315
363
 
316
364
  const onData = (chunk) => {
317
365
  const value = chunk.toString('utf8');
318
- if (value === '\u0003') {
366
+ if (value === '') {
319
367
  cleanup();
320
- reject(new Error('Installation cancelled.'));
368
+ reject(new Error(cancelMessage));
321
369
  return;
322
370
  }
323
371
 
324
- if (value === '\u001b[A' || value === 'k') {
372
+ if (value === '' || value === 'k') {
325
373
  cursor = (cursor - 1 + TARGET_OPTIONS.length) % TARGET_OPTIONS.length;
326
374
  message = '';
327
- renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
375
+ renderSelectionScreen({ output: stdout, version, cursor, selected, message, env, intro });
328
376
  return;
329
377
  }
330
378
 
331
- if (value === '\u001b[B' || value === 'j') {
379
+ if (value === '' || value === 'j') {
332
380
  cursor = (cursor + 1) % TARGET_OPTIONS.length;
333
381
  message = '';
334
- renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
382
+ renderSelectionScreen({ output: stdout, version, cursor, selected, message, env, intro });
335
383
  return;
336
384
  }
337
385
 
338
386
  if (value === ' ') {
339
387
  toggleMode(TARGET_OPTIONS[cursor].id);
340
388
  message = '';
341
- renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
389
+ renderSelectionScreen({ output: stdout, version, cursor, selected, message, env, intro });
342
390
  return;
343
391
  }
344
392
 
345
393
  if (value.toLowerCase() === 'a') {
346
394
  toggleMode('all');
347
395
  message = '';
348
- renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
396
+ renderSelectionScreen({ output: stdout, version, cursor, selected, message, env, intro });
349
397
  return;
350
398
  }
351
399
 
352
- if (value.toLowerCase() === 'q' || value === '\u001b') {
400
+ if (value.toLowerCase() === 'q' || value === '') {
353
401
  cleanup();
354
- reject(new Error('Installation cancelled.'));
402
+ reject(new Error(cancelMessage));
355
403
  return;
356
404
  }
357
405
 
358
406
  if (value === '\r') {
359
407
  if (selected.size === 0) {
360
408
  message = 'Select at least one target before continuing.';
361
- renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
409
+ renderSelectionScreen({ output: stdout, version, cursor, selected, message, env, intro });
362
410
  return;
363
411
  }
364
412
 
@@ -370,15 +418,106 @@ async function promptForModes({ stdin, stdout, version, env }) {
370
418
  stdin.setRawMode(true);
371
419
  stdin.resume();
372
420
  stdin.on('data', onData);
373
- renderSelectionScreen({ output: stdout, version, cursor, selected, message, env });
421
+ renderSelectionScreen({ output: stdout, version, cursor, selected, message, env, intro });
422
+ });
423
+ }
424
+
425
+ async function promptForModes({ stdin, stdout, version, env }) {
426
+ await animateWelcomeScreen({ output: stdout, version, env });
427
+ return promptForSelectableModes({
428
+ stdin,
429
+ stdout,
430
+ version,
431
+ env,
432
+ intro: 'Choose where Apollo Toolkit should copy managed skills.',
433
+ ttyError: `Interactive install requires a TTY. Re-run with targets like ${buildInteractiveModeHint()}.`,
434
+ cancelMessage: 'Installation cancelled.',
435
+ });
436
+ }
437
+
438
+ async function promptForUninstallModes({ stdin, stdout, version, env }) {
439
+ return promptForSelectableModes({
440
+ stdin,
441
+ stdout,
442
+ version,
443
+ env,
444
+ intro: 'Choose which agent skill targets Apollo Toolkit should uninstall.',
445
+ ttyError: `Interactive uninstall requires a TTY. Re-run with targets like ${buildInteractiveModeHint()}.`,
446
+ cancelMessage: 'Uninstall cancelled.',
447
+ });
448
+ }
449
+
450
+ async function promptYesNo({ stdin, stdout, env, question, defaultYes = true }) {
451
+ if (!stdin.isTTY || !stdout.isTTY) {
452
+ return defaultYes;
453
+ }
454
+
455
+ const rl = createInterface({ input: stdin, output: stdout });
456
+ try {
457
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
458
+ const answer = await rl.question(`${question} ${hint} `);
459
+ const trimmed = answer.trim().toLowerCase();
460
+ if (trimmed === '') {
461
+ return defaultYes;
462
+ }
463
+ return trimmed === 'y' || trimmed === 'yes';
464
+ } finally {
465
+ rl.close();
466
+ }
467
+ }
468
+
469
+ function buildSymlinkInfo({ colorEnabled }) {
470
+ return [
471
+ '',
472
+ color('Symlink mode:', '1', colorEnabled),
473
+ ` ${color('Pro:', '1;32', colorEnabled)} Skills auto-update when you ${color('git pull', '1;33', colorEnabled)} in ~/.apollo-toolkit`,
474
+ ` ${color('Pro:', '1;32', colorEnabled)} No need to re-run installer after patch updates`,
475
+ ` ${color('Con:', '1;31', colorEnabled)} Changes pushed to the repo automatically reflect in your skills -`,
476
+ ` you may receive updates you did not intend to accept`,
477
+ '',
478
+ ].join('\n');
479
+ }
480
+
481
+ async function promptSymlinkChoice({ stdin, stdout, env, colorEnabled }) {
482
+ stdout.write(buildSymlinkInfo({ colorEnabled }));
483
+ return promptYesNo({
484
+ stdin,
485
+ stdout,
486
+ env,
487
+ question: 'Install skills as symlinks (recommended)?',
488
+ defaultYes: true,
374
489
  });
375
490
  }
376
491
 
377
- async function confirmInstall({ stdin, stdout, version, toolkitHome, modes, env }) {
492
+ // Ask user whether to include codex-exclusive skills in non-codex targets.
493
+ async function promptIncludeExclusiveSkills({ stdin, stdout, env, colorEnabled, codexSkillNames, nonCodexModes }) {
494
+ if (codexSkillNames.length === 0 || nonCodexModes.length === 0) {
495
+ return false;
496
+ }
497
+
498
+ stdout.write([
499
+ '',
500
+ color('Exclusive skills detected:', '1;33', colorEnabled),
501
+ ` The following skills are exclusive to codex: ${codexSkillNames.join(', ')}`,
502
+ ` Your selected non-codex targets: ${nonCodexModes.join(', ')}`,
503
+ '',
504
+ ].join('\n'));
505
+
506
+ return promptYesNo({
507
+ stdin,
508
+ stdout,
509
+ env,
510
+ question: `Install codex-exclusive skills to ${nonCodexModes.join(', ')} as well?`,
511
+ defaultYes: false,
512
+ });
513
+ }
514
+
515
+ async function confirmInstall({ stdin, stdout, version, toolkitHome, modes, linkMode, env }) {
378
516
  const colorEnabled = supportsColor(stdout, env);
379
517
  stdout.write(`${buildBanner({ version, colorEnabled })}\n\n`);
380
518
  stdout.write(`Apollo Toolkit home: ${toolkitHome}\n`);
381
- stdout.write(`Targets: ${modes.join(', ')}\n\n`);
519
+ stdout.write(`Targets: ${modes.join(', ')}\n`);
520
+ stdout.write(`Install mode: ${linkMode === 'symlink' ? 'symlink (auto-update via git pull)' : 'copy (manual reinstall for updates)'}\n\n`);
382
521
 
383
522
  const targets = await getTargetRoots(modes, env).catch((error) => {
384
523
  throw error;
@@ -408,6 +547,7 @@ function printSummary({ stdout, version, toolkitHome, modes, installResult, env
408
547
  stdout.write('\n');
409
548
  stdout.write(`Apollo Toolkit home: ${toolkitHome}\n`);
410
549
  stdout.write(`Installed skills: ${installResult.skillNames.length}\n`);
550
+ stdout.write(`Install mode: ${installResult.linkMode === 'symlink' ? 'symlink' : 'copy'}\n`);
411
551
  stdout.write(`Targets: ${modes.join(', ')}\n\n`);
412
552
 
413
553
  for (const target of installResult.targets) {
@@ -415,6 +555,22 @@ function printSummary({ stdout, version, toolkitHome, modes, installResult, env
415
555
  }
416
556
  }
417
557
 
558
+ function printUninstallSummary({ stdout, uninstallResult, env }) {
559
+ const colorEnabled = supportsColor(stdout, env);
560
+
561
+ if (uninstallResult.length === 0) {
562
+ stdout.write(color('No Apollo Toolkit installations found.\n', '1;33', colorEnabled));
563
+ return;
564
+ }
565
+
566
+ stdout.write(color('Uninstall complete.', '1;32', colorEnabled));
567
+ stdout.write('\n\n');
568
+ for (const result of uninstallResult) {
569
+ stdout.write(`${color(result.target, '1', colorEnabled)} (${result.root})\n`);
570
+ stdout.write(` Removed: ${result.removedSkills.length > 0 ? result.removedSkills.join(', ') : '(manifest only)'}\n`);
571
+ }
572
+ }
573
+
418
574
  async function run(argv, context = {}) {
419
575
  const sourceRoot = context.sourceRoot || path.resolve(__dirname, '..');
420
576
  const stdout = context.stdout || process.stdout;
@@ -425,14 +581,17 @@ async function run(argv, context = {}) {
425
581
 
426
582
  try {
427
583
  const parsed = parseArguments(argv);
584
+
428
585
  if (parsed.showHelp) {
429
586
  stdout.write(`${buildHelpText({ version: packageJson.version, colorEnabled: supportsColor(stdout, env) })}\n`);
430
587
  return 0;
431
588
  }
589
+
432
590
  if (parsed.showToolsHelp) {
433
591
  stdout.write(`${buildToolsHelp({ version: packageJson.version, colorEnabled: supportsColor(stdout, env) })}\n`);
434
592
  return 0;
435
593
  }
594
+
436
595
  if (parsed.command === 'tool') {
437
596
  return (context.runTool || runTool)(parsed.toolName, parsed.toolArgs, {
438
597
  sourceRoot,
@@ -443,6 +602,51 @@ async function run(argv, context = {}) {
443
602
  });
444
603
  }
445
604
 
605
+ // --- Uninstall flow ---
606
+ if (parsed.command === 'uninstall') {
607
+ const toolkitHome = parsed.toolkitHome || resolveToolkitHome(env);
608
+ const modes = parsed.modes.length > 0
609
+ ? normalizeModes(parsed.modes)
610
+ : (stdin.isTTY && stdout.isTTY
611
+ ? normalizeModes(await promptForUninstallModes({ stdin, stdout, version: packageJson.version, env }))
612
+ : null);
613
+ const modesForLookup = modes || VALID_MODES;
614
+ const targets = await getUninstallTargetRoots(modesForLookup, env);
615
+
616
+ // Show what will be removed
617
+ const allKnown = await listAllKnownSkillNames({ toolkitHome, modes: modesForLookup, env });
618
+ stdout.write(color(`Apollo Toolkit home: ${toolkitHome}\n`, '2', supportsColor(stdout, env)));
619
+ if (targets.length > 0) {
620
+ stdout.write('Targets:\n');
621
+ for (const target of targets) {
622
+ stdout.write(`- ${target.label}: ${target.root}\n`);
623
+ }
624
+ }
625
+
626
+ const confirmed = parsed.assumeYes || await promptYesNo({
627
+ stdin,
628
+ stdout,
629
+ env,
630
+ question: `This will remove Apollo Toolkit-installed skills${modes ? ` from: ${modes.join(', ')}` : ' from all targets'}. Continue?`,
631
+ defaultYes: false,
632
+ });
633
+
634
+ if (!confirmed) {
635
+ stdout.write('Uninstall cancelled.\n');
636
+ return 1;
637
+ }
638
+
639
+ const uninstallResult = await uninstallSkills({ env, modes });
640
+ printUninstallSummary({ stdout, uninstallResult, env });
641
+
642
+ if (allKnown.length > 0) {
643
+ stdout.write(`\nPreviously known skills (may still exist elsewhere): ${allKnown.join(', ')}\n`);
644
+ }
645
+
646
+ return 0;
647
+ }
648
+
649
+ // --- Install flow ---
446
650
  const updateResult = await checkForPackageUpdate({
447
651
  packageName: packageJson.name,
448
652
  currentVersion: packageJson.version,
@@ -463,12 +667,39 @@ async function run(argv, context = {}) {
463
667
  ? normalizeModes(parsed.modes)
464
668
  : normalizeModes(await promptForModes({ stdin, stdout, version: packageJson.version, env }));
465
669
 
670
+ const colorEnabled = supportsColor(stdout, env);
671
+
672
+ // Determine link mode
673
+ let linkMode = parsed.linkMode;
674
+ if (!linkMode) {
675
+ linkMode = (await promptSymlinkChoice({ stdin, stdout, env, colorEnabled })) ? 'symlink' : 'copy';
676
+ }
677
+
678
+ // Determine whether to include exclusive (codex) skills in non-codex targets
679
+ const nonCodexModes = modes.filter((m) => m !== 'codex');
680
+ const codexSkillNames = await listCodexSkillNames(toolkitHome).catch(() => []);
681
+ const includeExclusiveSkills = await promptIncludeExclusiveSkills({
682
+ stdin,
683
+ stdout,
684
+ env,
685
+ colorEnabled,
686
+ codexSkillNames,
687
+ nonCodexModes,
688
+ });
689
+
690
+ // syncToolkitHome needs to include the codex container when exclusive skills
691
+ // are requested, so the source files are available for symlink/copy.
692
+ const effectiveModes = includeExclusiveSkills
693
+ ? [...new Set([...modes, 'codex'])]
694
+ : modes;
695
+
466
696
  const confirmed = await confirmInstall({
467
697
  stdin,
468
698
  stdout,
469
699
  version: packageJson.version,
470
700
  toolkitHome,
471
701
  modes,
702
+ linkMode,
472
703
  env,
473
704
  });
474
705
 
@@ -481,13 +712,15 @@ async function run(argv, context = {}) {
481
712
  sourceRoot,
482
713
  toolkitHome,
483
714
  version: packageJson.version,
484
- modes,
715
+ modes: effectiveModes,
485
716
  });
486
717
 
487
718
  const installResult = await installLinks({
488
719
  toolkitHome,
489
720
  modes,
490
721
  previousSkillNames: syncResult.previousSkillNames,
722
+ linkMode,
723
+ includeExclusiveSkills,
491
724
  env: {
492
725
  ...env,
493
726
  APOLLO_TOOLKIT_HOME: toolkitHome,
@@ -509,6 +742,9 @@ module.exports = {
509
742
  buildToolsHelp,
510
743
  parseArguments,
511
744
  promptForModes,
745
+ promptForUninstallModes,
746
+ promptSymlinkChoice,
747
+ promptIncludeExclusiveSkills,
512
748
  readPackageJson,
513
749
  run,
514
750
  };