@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/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,81 @@ 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
+ function isSafeSkillName(skillName) {
126
+ return typeof skillName === 'string'
127
+ && skillName.length > 0
128
+ && !skillName.includes('\0')
129
+ && !skillName.includes('/')
130
+ && !skillName.includes('\\')
131
+ && !path.isAbsolute(skillName)
132
+ && !path.win32.isAbsolute(skillName)
133
+ && skillName !== '.'
134
+ && skillName !== '..';
135
+ }
136
+
137
+ function getManifestSkillNames(manifest) {
138
+ return [...new Set([
139
+ ...(Array.isArray(manifest.historicalSkills) ? manifest.historicalSkills : []),
140
+ ...(Array.isArray(manifest.skills) ? manifest.skills : []),
141
+ ])].filter(isSafeSkillName).sort();
142
+ }
143
+
144
+ // Write manifest to a target directory.
145
+ async function writeManifest(targetRoot, { version, linkMode, skills, previousSkills = [] }) {
146
+ const historicalSkills = [...new Set([...previousSkills, ...skills])].sort();
147
+ const manifest = {
148
+ version,
149
+ installedAt: new Date().toISOString(),
150
+ linkMode,
151
+ skills: [...skills].sort(),
152
+ historicalSkills,
153
+ };
154
+ await fsp.mkdir(targetRoot, { recursive: true });
155
+ await fsp.writeFile(
156
+ path.join(targetRoot, MANIFEST_FILENAME),
157
+ `${JSON.stringify(manifest, null, 2)}\n`,
158
+ 'utf8',
159
+ );
160
+ }
161
+
162
+ // Read all current + historically appeared skill names with deduplication.
163
+ async function listAllKnownSkillNames({ toolkitHome, modes = [], env = process.env }) {
164
+ const allNames = new Set();
165
+
166
+ // Current skills from toolkit home
167
+ const currentSkills = await listSkillNames(toolkitHome, modes).catch(() => []);
168
+ for (const name of currentSkills) {
169
+ allNames.add(name);
170
+ }
171
+
172
+ // Historical skills from all target manifests
173
+ const targets = await getUninstallTargetRoots(modes, env);
174
+ for (const target of targets) {
175
+ const manifest = await readManifest(target.root);
176
+ if (manifest && manifest.historicalSkills) {
177
+ for (const name of getManifestSkillNames(manifest)) {
178
+ allNames.add(name);
179
+ }
180
+ }
181
+ }
182
+
183
+ return [...allNames].sort();
184
+ }
185
+
186
+ function getTargetSkillNames({ targetMode, sharedSkillNames, codexSkillNames, includeExclusiveSkills = false }) {
187
+ const includeCodexSkills = targetMode === 'codex' || includeExclusiveSkills;
188
+ if (!includeCodexSkills || codexSkillNames.length === 0) {
116
189
  return sharedSkillNames;
117
190
  }
118
191
 
@@ -289,6 +362,21 @@ async function getTargetRoots(modes, env = process.env) {
289
362
  return targets;
290
363
  }
291
364
 
365
+ async function getUninstallTargetRoots(modes = VALID_MODES, env = process.env) {
366
+ const targets = [];
367
+
368
+ for (const mode of normalizeModes(modes)) {
369
+ try {
370
+ targets.push(...await getTargetRoots([mode], env));
371
+ } catch {
372
+ // Uninstall is best-effort across agents. A missing OpenClaw workspace
373
+ // must not prevent cleanup from Codex, Trae, Agents, or Claude Code.
374
+ }
375
+ }
376
+
377
+ return targets;
378
+ }
379
+
292
380
  async function ensureDirectory(dirPath) {
293
381
  await fsp.mkdir(dirPath, { recursive: true });
294
382
  }
@@ -299,10 +387,23 @@ async function replaceWithCopy(sourcePath, targetPath) {
299
387
  await fsp.cp(sourcePath, targetPath, { recursive: true, force: true });
300
388
  }
301
389
 
302
- async function installLinks({ toolkitHome, modes, env = process.env, previousSkillNames = [] }) {
390
+ async function replaceWithSymlink(sourcePath, targetPath) {
391
+ await fsp.rm(targetPath, { recursive: true, force: true });
392
+ await ensureDirectory(path.dirname(targetPath));
393
+ await fsp.symlink(sourcePath, targetPath, process.platform === 'win32' ? 'junction' : 'dir');
394
+ }
395
+
396
+ // Install skills into target directories.
397
+ // linkMode: 'copy' (default) or 'symlink'
398
+ // includeExclusiveSkills: when true, non-codex targets also receive codex-exclusive skills.
399
+ async function installLinks({ toolkitHome, modes, env = process.env, previousSkillNames = [], linkMode = 'copy', includeExclusiveSkills = false }) {
303
400
  const normalizedModes = normalizeModes(modes);
401
+ // Always collect codex skill names (needed when includeExclusiveSkills is true even
402
+ // when codex isn't in the target list).
403
+ const codexSkillNames = (normalizedModes.includes('codex') || includeExclusiveSkills)
404
+ ? await listCodexSkillNames(toolkitHome)
405
+ : [];
304
406
  const sharedSkillNames = await listSkillNames(toolkitHome);
305
- const codexSkillNames = normalizedModes.includes('codex') ? await listCodexSkillNames(toolkitHome) : [];
306
407
  const skillNames = normalizedModes.includes('codex')
307
408
  ? [...new Set([...sharedSkillNames, ...codexSkillNames])].sort()
308
409
  : sharedSkillNames;
@@ -314,8 +415,18 @@ async function installLinks({ toolkitHome, modes, env = process.env, previousSki
314
415
  targetMode: target.mode,
315
416
  sharedSkillNames,
316
417
  codexSkillNames,
418
+ includeExclusiveSkills,
317
419
  });
318
- const staleSkillNames = previousSkillNames.filter((skillName) => !targetSkillNames.includes(skillName));
420
+
421
+ // Read existing manifest to carry forward historical skills
422
+ const existingManifest = await readManifest(target.root);
423
+ const allPreviousSkills = existingManifest
424
+ ? [...new Set([...getManifestSkillNames(existingManifest), ...previousSkillNames.filter(isSafeSkillName)])]
425
+ : previousSkillNames.filter(isSafeSkillName);
426
+
427
+ const staleSkillNames = allPreviousSkills.filter(
428
+ (skillName) => !targetSkillNames.includes(skillName),
429
+ );
319
430
 
320
431
  await ensureDirectory(target.root);
321
432
  for (const staleSkillName of staleSkillNames) {
@@ -329,27 +440,89 @@ async function installLinks({ toolkitHome, modes, env = process.env, previousSki
329
440
  codexSkillNames,
330
441
  });
331
442
  const targetPath = path.join(target.root, skillName);
332
- await replaceWithCopy(sourcePath, targetPath);
333
- copiedPaths.push({ target: target.label, path: targetPath, skillName });
443
+
444
+ if (linkMode === 'symlink') {
445
+ await replaceWithSymlink(sourcePath, targetPath);
446
+ } else {
447
+ await replaceWithCopy(sourcePath, targetPath);
448
+ }
449
+ copiedPaths.push({ target: target.label, path: targetPath, skillName, linkMode });
334
450
  }
451
+
452
+ // Persist manifest for future uninstall / dedup
453
+ await writeManifest(target.root, {
454
+ version: existingManifest?.version || 'unknown',
455
+ linkMode,
456
+ skills: targetSkillNames,
457
+ previousSkills: allPreviousSkills,
458
+ });
335
459
  }
336
460
 
337
461
  return {
338
462
  skillNames,
339
463
  targets,
340
464
  copiedPaths,
465
+ linkMode,
341
466
  };
342
467
  }
343
468
 
469
+ // Uninstall all skills from all target directories that have manifests.
470
+ async function uninstallSkills({ env = process.env, modes = null } = {}) {
471
+ const normalizedModes = modes ? normalizeModes(modes) : VALID_MODES;
472
+ const targets = await getUninstallTargetRoots(normalizedModes, env);
473
+ const results = [];
474
+
475
+ for (const target of targets) {
476
+ const manifest = await readManifest(target.root);
477
+ if (!manifest) {
478
+ continue;
479
+ }
480
+
481
+ const skillNames = getManifestSkillNames(manifest);
482
+ const removedSkills = [];
483
+ for (const skillName of skillNames) {
484
+ const skillPath = path.join(target.root, skillName);
485
+ try {
486
+ await fsp.rm(skillPath, { recursive: true, force: true });
487
+ removedSkills.push(skillName);
488
+ } catch {
489
+ // Skip skills that couldn't be removed
490
+ }
491
+ }
492
+
493
+ // Remove the manifest itself
494
+ try {
495
+ await fsp.rm(path.join(target.root, MANIFEST_FILENAME), { force: true });
496
+ } catch {
497
+ // ok if already gone
498
+ }
499
+
500
+ results.push({
501
+ target: target.label,
502
+ root: target.root,
503
+ removedSkills,
504
+ });
505
+ }
506
+
507
+ return results;
508
+ }
509
+
344
510
  module.exports = {
345
511
  expandUserPath,
346
512
  TARGET_DEFINITIONS,
347
513
  VALID_MODES,
514
+ MANIFEST_FILENAME,
348
515
  getTargetRoots,
516
+ getUninstallTargetRoots,
349
517
  installLinks,
518
+ listAllKnownSkillNames,
519
+ listCodexSkillNames,
350
520
  listSkillNames,
351
521
  normalizeModes,
522
+ readManifest,
352
523
  resolveHomeDirectory,
353
524
  resolveToolkitHome,
354
525
  syncToolkitHome,
526
+ uninstallSkills,
527
+ writeManifest,
355
528
  };
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.1",
4
4
  "description": "Apollo Toolkit npm installer for managed skill copying across Codex, OpenClaw, and Trae.",
5
5
  "license": "MIT",
6
6
  "author": "LaiTszKin",