@laitszkin/apollo-toolkit 3.2.2 → 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/lib/installer.js CHANGED
@@ -13,6 +13,7 @@ const TARGET_DEFINITIONS = Object.freeze([
13
13
  const VALID_MODES = TARGET_DEFINITIONS.map(({ id }) => id);
14
14
  const COPY_FILES = new Set(['AGENTS.md', 'CHANGELOG.md', 'LICENSE', 'README.md', 'package.json']);
15
15
  const COPY_DIRS = new Set(['scripts']);
16
+ const MANIFEST_FILENAME = '.apollo-toolkit-manifest.json';
16
17
 
17
18
  function resolveHomeDirectory(env = process.env) {
18
19
  return env.HOME || env.USERPROFILE || os.homedir();
@@ -82,7 +83,6 @@ async function listSkillNames(rootDir, modes = []) {
82
83
  }
83
84
  }
84
85
 
85
- // For codex mode, also include codex-specific skills
86
86
  if (modes.includes('codex')) {
87
87
  const codexDir = path.join(rootDir, 'codex');
88
88
  if (fs.existsSync(codexDir)) {
@@ -111,8 +111,62 @@ async function listCodexSkillNames(rootDir) {
111
111
  .sort();
112
112
  }
113
113
 
114
- function getTargetSkillNames({ targetMode, sharedSkillNames, codexSkillNames }) {
115
- if (targetMode !== 'codex') {
114
+ // Read manifest from a target directory, returning { skills, historicalSkills, linkMode } or null.
115
+ async function readManifest(targetRoot) {
116
+ const manifestPath = path.join(targetRoot, MANIFEST_FILENAME);
117
+ try {
118
+ const raw = await fsp.readFile(manifestPath, 'utf8');
119
+ return JSON.parse(raw);
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ // Write manifest to a target directory.
126
+ async function writeManifest(targetRoot, { version, linkMode, skills, previousSkills = [] }) {
127
+ const historicalSkills = [...new Set([...previousSkills, ...skills])].sort();
128
+ const manifest = {
129
+ version,
130
+ installedAt: new Date().toISOString(),
131
+ linkMode,
132
+ skills: [...skills].sort(),
133
+ historicalSkills,
134
+ };
135
+ await fsp.mkdir(targetRoot, { recursive: true });
136
+ await fsp.writeFile(
137
+ path.join(targetRoot, MANIFEST_FILENAME),
138
+ `${JSON.stringify(manifest, null, 2)}\n`,
139
+ 'utf8',
140
+ );
141
+ }
142
+
143
+ // Read all current + historically appeared skill names with deduplication.
144
+ async function listAllKnownSkillNames({ toolkitHome, modes = [], env = process.env }) {
145
+ const allNames = new Set();
146
+
147
+ // Current skills from toolkit home
148
+ const currentSkills = await listSkillNames(toolkitHome, modes);
149
+ for (const name of currentSkills) {
150
+ allNames.add(name);
151
+ }
152
+
153
+ // Historical skills from all target manifests
154
+ const targets = await getTargetRoots(modes, env).catch(() => []);
155
+ for (const target of targets) {
156
+ const manifest = await readManifest(target.root);
157
+ if (manifest && manifest.historicalSkills) {
158
+ for (const name of manifest.historicalSkills) {
159
+ allNames.add(name);
160
+ }
161
+ }
162
+ }
163
+
164
+ return [...allNames].sort();
165
+ }
166
+
167
+ function getTargetSkillNames({ targetMode, sharedSkillNames, codexSkillNames, includeExclusiveSkills = false }) {
168
+ const includeCodexSkills = targetMode === 'codex' || includeExclusiveSkills;
169
+ if (!includeCodexSkills || codexSkillNames.length === 0) {
116
170
  return sharedSkillNames;
117
171
  }
118
172
 
@@ -299,10 +353,23 @@ async function replaceWithCopy(sourcePath, targetPath) {
299
353
  await fsp.cp(sourcePath, targetPath, { recursive: true, force: true });
300
354
  }
301
355
 
302
- async function installLinks({ toolkitHome, modes, env = process.env, previousSkillNames = [] }) {
356
+ async function replaceWithSymlink(sourcePath, targetPath) {
357
+ await fsp.rm(targetPath, { recursive: true, force: true });
358
+ await ensureDirectory(path.dirname(targetPath));
359
+ await fsp.symlink(sourcePath, targetPath, process.platform === 'win32' ? 'junction' : 'dir');
360
+ }
361
+
362
+ // Install skills into target directories.
363
+ // linkMode: 'copy' (default) or 'symlink'
364
+ // includeExclusiveSkills: when true, non-codex targets also receive codex-exclusive skills.
365
+ async function installLinks({ toolkitHome, modes, env = process.env, previousSkillNames = [], linkMode = 'copy', includeExclusiveSkills = false }) {
303
366
  const normalizedModes = normalizeModes(modes);
367
+ // Always collect codex skill names (needed when includeExclusiveSkills is true even
368
+ // when codex isn't in the target list).
369
+ const codexSkillNames = (normalizedModes.includes('codex') || includeExclusiveSkills)
370
+ ? await listCodexSkillNames(toolkitHome)
371
+ : [];
304
372
  const sharedSkillNames = await listSkillNames(toolkitHome);
305
- const codexSkillNames = normalizedModes.includes('codex') ? await listCodexSkillNames(toolkitHome) : [];
306
373
  const skillNames = normalizedModes.includes('codex')
307
374
  ? [...new Set([...sharedSkillNames, ...codexSkillNames])].sort()
308
375
  : sharedSkillNames;
@@ -314,8 +381,18 @@ async function installLinks({ toolkitHome, modes, env = process.env, previousSki
314
381
  targetMode: target.mode,
315
382
  sharedSkillNames,
316
383
  codexSkillNames,
384
+ includeExclusiveSkills,
317
385
  });
318
- const staleSkillNames = previousSkillNames.filter((skillName) => !targetSkillNames.includes(skillName));
386
+
387
+ // Read existing manifest to carry forward historical skills
388
+ const existingManifest = await readManifest(target.root);
389
+ const allPreviousSkills = existingManifest
390
+ ? [...new Set([...existingManifest.historicalSkills, ...previousSkillNames])]
391
+ : previousSkillNames;
392
+
393
+ const staleSkillNames = allPreviousSkills.filter(
394
+ (skillName) => !targetSkillNames.includes(skillName),
395
+ );
319
396
 
320
397
  await ensureDirectory(target.root);
321
398
  for (const staleSkillName of staleSkillNames) {
@@ -329,27 +406,89 @@ async function installLinks({ toolkitHome, modes, env = process.env, previousSki
329
406
  codexSkillNames,
330
407
  });
331
408
  const targetPath = path.join(target.root, skillName);
332
- await replaceWithCopy(sourcePath, targetPath);
333
- copiedPaths.push({ target: target.label, path: targetPath, skillName });
409
+
410
+ if (linkMode === 'symlink') {
411
+ await replaceWithSymlink(sourcePath, targetPath);
412
+ } else {
413
+ await replaceWithCopy(sourcePath, targetPath);
414
+ }
415
+ copiedPaths.push({ target: target.label, path: targetPath, skillName, linkMode });
334
416
  }
417
+
418
+ // Persist manifest for future uninstall / dedup
419
+ await writeManifest(target.root, {
420
+ version: existingManifest?.version || 'unknown',
421
+ linkMode,
422
+ skills: targetSkillNames,
423
+ previousSkills: allPreviousSkills,
424
+ });
335
425
  }
336
426
 
337
427
  return {
338
428
  skillNames,
339
429
  targets,
340
430
  copiedPaths,
431
+ linkMode,
341
432
  };
342
433
  }
343
434
 
435
+ // Uninstall all skills from all target directories that have manifests.
436
+ async function uninstallSkills({ env = process.env, modes = null } = {}) {
437
+ const normalizedModes = modes ? normalizeModes(modes) : VALID_MODES;
438
+ const targets = await getTargetRoots(normalizedModes, env).catch(() => []);
439
+ const results = [];
440
+
441
+ for (const target of targets) {
442
+ const manifest = await readManifest(target.root);
443
+ if (!manifest || !manifest.skills || manifest.skills.length === 0) {
444
+ continue;
445
+ }
446
+
447
+ const removedSkills = [];
448
+ for (const skillName of manifest.skills) {
449
+ const skillPath = path.join(target.root, skillName);
450
+ try {
451
+ await fsp.rm(skillPath, { recursive: true, force: true });
452
+ removedSkills.push(skillName);
453
+ } catch {
454
+ // Skip skills that couldn't be removed
455
+ }
456
+ }
457
+
458
+ // Remove the manifest itself
459
+ try {
460
+ await fsp.rm(path.join(target.root, MANIFEST_FILENAME), { force: true });
461
+ } catch {
462
+ // ok if already gone
463
+ }
464
+
465
+ if (removedSkills.length > 0) {
466
+ results.push({
467
+ target: target.label,
468
+ root: target.root,
469
+ removedSkills,
470
+ });
471
+ }
472
+ }
473
+
474
+ return results;
475
+ }
476
+
344
477
  module.exports = {
345
478
  expandUserPath,
346
479
  TARGET_DEFINITIONS,
347
480
  VALID_MODES,
481
+ MANIFEST_FILENAME,
348
482
  getTargetRoots,
349
483
  installLinks,
484
+ listAllKnownSkillNames,
485
+ listCodexSkillNames,
350
486
  listSkillNames,
351
487
  normalizeModes,
488
+ readManifest,
352
489
  resolveHomeDirectory,
353
490
  resolveToolkitHome,
354
491
  syncToolkitHome,
492
+ uninstallSkills,
493
+ writeManifest,
355
494
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laitszkin/apollo-toolkit",
3
- "version": "3.2.2",
3
+ "version": "3.3.0",
4
4
  "description": "Apollo Toolkit npm installer for managed skill copying across Codex, OpenClaw, and Trae.",
5
5
  "license": "MIT",
6
6
  "author": "LaiTszKin",