skiller 0.9.10 → 0.9.12

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.
@@ -19,6 +19,17 @@ function skillsArgsBuilder(y) {
19
19
  description: 'Arguments passed through to the local skills CLI',
20
20
  });
21
21
  }
22
+ function installArgsBuilder(y) {
23
+ return skillsArgsBuilder(y)
24
+ .positional('source', {
25
+ type: 'string',
26
+ description: 'Preset source for one-shot materialization (local path, GitHub repo, or GitHub URL)',
27
+ })
28
+ .option('preset', {
29
+ type: 'string',
30
+ description: 'Preset name to materialize from the source (defaults to auto-selecting a single preset or "default")',
31
+ });
32
+ }
22
33
  function migrateClaudePluginsArgsBuilder(y) {
23
34
  return y
24
35
  .option('project-root', {
@@ -110,12 +121,12 @@ async function run() {
110
121
  })
111
122
  .option('local-only', {
112
123
  type: 'boolean',
113
- description: 'Only search for local .claude directories, ignore global config',
124
+ description: 'Only search for local .agents directories, ignore global config',
114
125
  default: false,
115
126
  })
116
127
  .option('nested', {
117
128
  type: 'boolean',
118
- description: 'Enable nested rule loading from nested .claude directories (default: from config or disabled)',
129
+ description: 'Enable nested rule loading from nested .agents directories (default: from config or disabled)',
119
130
  })
120
131
  .option('backup', {
121
132
  type: 'boolean',
@@ -143,14 +154,44 @@ async function run() {
143
154
  .command('list [args..]', 'Run the local skills CLI list command', skillsArgsBuilder, handlers_1.listHandler)
144
155
  .command('find [args..]', 'Run the local skills CLI find command', skillsArgsBuilder, handlers_1.findHandler)
145
156
  .command('check [args..]', 'Run the local skills CLI check command', skillsArgsBuilder, handlers_1.checkHandler)
146
- .command('install [args..]', 'Restore lock-backed skills plus skiller-managed agent installs from lockfiles, then skiller apply', (y) => skillsArgsBuilder(y)
157
+ .command('install [source] [args..]', 'Materialize a preset when requested, or run inherited sync when configured, then restore lock-backed skills plus skiller-managed agent installs from lockfiles, then skiller apply', (y) => installArgsBuilder(y)
158
+ .option('nested', {
159
+ type: 'boolean',
160
+ description: 'Run the install lifecycle for every nested .agents project root',
161
+ default: false,
162
+ })
163
+ .option('no-sync', {
164
+ type: 'boolean',
165
+ description: 'Skip [sync] processing before install',
166
+ default: false,
167
+ })
168
+ .option('sync-only', {
169
+ type: 'boolean',
170
+ description: 'Run [sync] processing only, then stop before install/apply',
171
+ default: false,
172
+ })
147
173
  .option('verbose', {
148
174
  type: 'boolean',
149
175
  description: 'Enable verbose logging for the follow-up apply step',
150
176
  default: false,
151
177
  })
152
178
  .alias('verbose', 'v'), handlers_1.installHandler)
153
- .command('update [args..]', 'Update local skills CLI installs plus skiller-managed agent installs, then skiller apply', (y) => skillsArgsBuilder(y)
179
+ .command('update [args..]', 'Optionally sync inherited preset files, then update local skills CLI installs plus skiller-managed agent installs, then skiller apply', (y) => skillsArgsBuilder(y)
180
+ .option('nested', {
181
+ type: 'boolean',
182
+ description: 'Run the update lifecycle for every nested .agents project root',
183
+ default: false,
184
+ })
185
+ .option('no-sync', {
186
+ type: 'boolean',
187
+ description: 'Skip [sync] processing before update',
188
+ default: false,
189
+ })
190
+ .option('sync-only', {
191
+ type: 'boolean',
192
+ description: 'Run [sync] processing only, then stop before update/apply',
193
+ default: false,
194
+ })
154
195
  .option('verbose', {
155
196
  type: 'boolean',
156
197
  description: 'Enable verbose logging for the follow-up apply step',
@@ -62,6 +62,9 @@ const skills_cli_1 = require("./skills-cli");
62
62
  const project_paths_1 = require("../core/project-paths");
63
63
  const SkillOwnership_1 = require("../core/SkillOwnership");
64
64
  const AgentSourceCompatibility_1 = require("../core/AgentSourceCompatibility");
65
+ const SyncEngine_1 = require("../core/SyncEngine");
66
+ const FileSystemUtils_1 = require("../core/FileSystemUtils");
67
+ const PresetInstaller_1 = require("../core/PresetInstaller");
65
68
  const readline = __importStar(require("readline/promises"));
66
69
  async function executeSkillsWrapper(projectRoot, args) {
67
70
  try {
@@ -79,6 +82,89 @@ async function applyAfterSkillsLifecycleStep(projectRoot, verbose) {
79
82
  verbose,
80
83
  });
81
84
  }
85
+ async function pruneSkillOutputs(projectRoot, skillNames) {
86
+ const normalizedNames = [...new Set(skillNames.filter(Boolean))];
87
+ if (normalizedNames.length === 0)
88
+ return;
89
+ const skillDirs = new Set([(0, SkillOwnership_1.getCanonicalSkillsDir)(projectRoot)]);
90
+ for (const agent of agents_2.allAgents) {
91
+ if (!agent.supportsNativeSkills?.() || !agent.getSkillsPath)
92
+ continue;
93
+ const skillsPath = agent.getSkillsPath(projectRoot);
94
+ if (skillsPath) {
95
+ skillDirs.add(skillsPath);
96
+ }
97
+ }
98
+ for (const skillName of normalizedNames) {
99
+ for (const skillsDir of skillDirs) {
100
+ await fs.rm(path.join(skillsDir, skillName), {
101
+ force: true,
102
+ recursive: true,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ async function pruneStaleLockBackedSkills(projectRoot) {
108
+ const [nativePrune, agentPrune] = await Promise.all([
109
+ (0, AgentSourceCompatibility_1.pruneMissingNativeSkillsFromLock)(projectRoot),
110
+ (0, AgentSourceCompatibility_1.pruneMissingAgentSkillsFromLock)(projectRoot),
111
+ ]);
112
+ if (nativePrune.prunedOutputNames.length > 0) {
113
+ await pruneSkillOutputs(projectRoot, nativePrune.prunedOutputNames);
114
+ console.log(`[skiller] Pruned ${nativePrune.prunedKeys.length} stale upstream skill(s): ${nativePrune.prunedKeys.join(', ')}`);
115
+ }
116
+ if (agentPrune.prunedOutputNames.length > 0) {
117
+ await pruneSkillOutputs(projectRoot, agentPrune.prunedOutputNames);
118
+ console.log(`[skiller] Pruned ${agentPrune.prunedKeys.length} stale agent-derived skill(s): ${agentPrune.prunedKeys.join(', ')}`);
119
+ }
120
+ const warnings = [...nativePrune.warnings, ...agentPrune.warnings];
121
+ if (warnings.length > 0) {
122
+ console.log(warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
123
+ }
124
+ }
125
+ function normalizeOutputSkillNames(skillNames) {
126
+ return [...new Set(skillNames.map((name) => name.replace(/:/g, '-')))].sort((a, b) => a.localeCompare(b));
127
+ }
128
+ async function pruneOutputsForRemovedLockEntries(projectRoot, args) {
129
+ const nativeOutputNames = normalizeOutputSkillNames(args.native);
130
+ const agentOutputNames = normalizeOutputSkillNames(args.agent);
131
+ if (nativeOutputNames.length > 0) {
132
+ await pruneSkillOutputs(projectRoot, nativeOutputNames);
133
+ console.log(`[skiller] Pruned ${args.native.length} skill output(s) removed by source update: ${args.native.join(', ')}`);
134
+ }
135
+ if (agentOutputNames.length > 0) {
136
+ await pruneSkillOutputs(projectRoot, agentOutputNames);
137
+ console.log(`[skiller] Pruned ${args.agent.length} agent-derived skill output(s) removed by source update: ${args.agent.join(', ')}`);
138
+ }
139
+ }
140
+ async function getLifecycleProjectRoots(projectRoot, nested) {
141
+ if (!nested) {
142
+ return [projectRoot];
143
+ }
144
+ const skillerDirs = await (0, FileSystemUtils_1.findAllSkillerDirs)(projectRoot);
145
+ const roots = [...new Set(skillerDirs.map((dir) => path.dirname(dir)))];
146
+ return roots.sort((a, b) => a.localeCompare(b));
147
+ }
148
+ async function maybeRunSync(projectRoot, options) {
149
+ if (options.skipSync) {
150
+ return {
151
+ applied: false,
152
+ synced: [],
153
+ removed: [],
154
+ removedNativeLockSkills: [],
155
+ removedAgentLockSkills: [],
156
+ };
157
+ }
158
+ const result = await (0, SyncEngine_1.syncProjectFiles)(projectRoot);
159
+ if (!result.applied)
160
+ return result;
161
+ const modeSuffix = result.mode ? ` (${result.mode})` : '';
162
+ console.log(`[skiller] Synced ${result.synced.length} file(s) from ${result.source}${modeSuffix}`);
163
+ if (result.removed.length > 0) {
164
+ console.log(`[skiller] Removed ${result.removed.length} stale synced file(s): ${result.removed.join(', ')}`);
165
+ }
166
+ return result;
167
+ }
82
168
  function normalizeRequestedSkillNames(args) {
83
169
  if (!args || args.length === 0)
84
170
  return [];
@@ -187,6 +273,41 @@ async function readJsonObject(filePath) {
187
273
  return null;
188
274
  }
189
275
  }
276
+ async function readLockSkillKeys(filePath) {
277
+ const raw = await readJsonObject(filePath);
278
+ const skills = raw?.skills;
279
+ if (!skills || typeof skills !== 'object') {
280
+ return [];
281
+ }
282
+ return Object.keys(skills).sort((a, b) => a.localeCompare(b));
283
+ }
284
+ function subtractStringSets(before, after) {
285
+ const afterSet = new Set(after);
286
+ return before.filter((entry) => !afterSet.has(entry));
287
+ }
288
+ function shouldUsePresetInstall(argv) {
289
+ if (argv.preset) {
290
+ return true;
291
+ }
292
+ if (!argv.source) {
293
+ return false;
294
+ }
295
+ try {
296
+ (0, AgentSourceCompatibility_1.parseCompatibleSource)(argv.source);
297
+ return true;
298
+ }
299
+ catch {
300
+ return false;
301
+ }
302
+ }
303
+ function getInstallPassthroughArgs(argv) {
304
+ if (!shouldUsePresetInstall(argv)) {
305
+ return argv.source
306
+ ? [argv.source, ...(argv.args ?? [])]
307
+ : (argv.args ?? []);
308
+ }
309
+ return argv.args ?? [];
310
+ }
190
311
  async function cleanupLegacyClaudePluginState(projectRoot, pluginIds) {
191
312
  const pluginIdSet = new Set(pluginIds);
192
313
  if (pluginIdSet.size === 0)
@@ -681,18 +802,66 @@ async function addHandler(argv) {
681
802
  await applyAfterSkillsLifecycleStep(projectRoot, argv.verbose ?? false);
682
803
  }
683
804
  async function installHandler(argv) {
684
- await executeSkillsWrapper(argv['project-root'], [
685
- 'experimental_install',
686
- ...(argv.args ?? []),
687
- ]);
688
- const restored = await (0, AgentSourceCompatibility_1.restoreAgentSkillsFromLock)(argv['project-root']);
689
- if (restored.restored.length > 0) {
690
- console.log(`[skiller] Restored ${restored.restored.length} agent-derived skill(s): ${restored.restored.join(', ')}`);
691
- }
692
- if (restored.warnings.length > 0) {
693
- console.log(restored.warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
805
+ if (argv['no-sync'] && argv['sync-only']) {
806
+ throw new Error('[skiller] --no-sync and --sync-only cannot be combined.');
807
+ }
808
+ const presetInstall = shouldUsePresetInstall(argv);
809
+ if (presetInstall && !argv.source) {
810
+ throw new Error('[skiller] install --preset requires a source: skiller install <source> --preset <name>');
811
+ }
812
+ const projectRoots = await getLifecycleProjectRoots(argv['project-root'], argv.nested);
813
+ const installArgs = getInstallPassthroughArgs(argv);
814
+ for (const projectRoot of projectRoots) {
815
+ if (presetInstall) {
816
+ const [previousNativeLockSkills, previousAgentLockSkills] = await Promise.all([
817
+ readLockSkillKeys(path.join(projectRoot, 'skills-lock.json')),
818
+ readLockSkillKeys(path.join(projectRoot, 'skiller-lock.json')),
819
+ ]);
820
+ const presetResult = await (0, PresetInstaller_1.installPresetIntoProject)(projectRoot, {
821
+ preset: argv.preset,
822
+ source: argv.source,
823
+ });
824
+ console.log(`[skiller] Materialized preset '${presetResult.preset}' from ${argv.source} (${presetResult.synced.length} file(s))`);
825
+ if (presetResult.removed.length > 0) {
826
+ console.log(`[skiller] Removed ${presetResult.removed.length} stale preset-managed file(s): ${presetResult.removed.join(', ')}`);
827
+ }
828
+ const [nextNativeLockSkills, nextAgentLockSkills] = await Promise.all([
829
+ readLockSkillKeys(path.join(projectRoot, 'skills-lock.json')),
830
+ readLockSkillKeys(path.join(projectRoot, 'skiller-lock.json')),
831
+ ]);
832
+ await pruneOutputsForRemovedLockEntries(projectRoot, {
833
+ native: subtractStringSets(previousNativeLockSkills, nextNativeLockSkills),
834
+ agent: subtractStringSets(previousAgentLockSkills, nextAgentLockSkills),
835
+ });
836
+ }
837
+ else {
838
+ const syncResult = await maybeRunSync(projectRoot, {
839
+ skipSync: argv['no-sync'] ?? false,
840
+ });
841
+ if (syncResult.applied) {
842
+ await pruneOutputsForRemovedLockEntries(projectRoot, {
843
+ native: syncResult.removedNativeLockSkills,
844
+ agent: syncResult.removedAgentLockSkills,
845
+ });
846
+ }
847
+ }
848
+ if (argv['sync-only']) {
849
+ continue;
850
+ }
851
+ await pruneStaleLockBackedSkills(projectRoot);
852
+ await executeSkillsWrapper(projectRoot, [
853
+ 'experimental_install',
854
+ ...installArgs,
855
+ ]);
856
+ const restored = await (0, AgentSourceCompatibility_1.restoreAgentSkillsFromLock)(projectRoot);
857
+ if (restored.restored.length > 0) {
858
+ console.log(`[skiller] Restored ${restored.restored.length} agent-derived skill(s): ${restored.restored.join(', ')}`);
859
+ }
860
+ if (restored.warnings.length > 0) {
861
+ console.log(restored.warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
862
+ }
863
+ await applyAfterSkillsLifecycleStep(projectRoot, argv.verbose ?? false);
694
864
  }
695
- await applyAfterSkillsLifecycleStep(argv['project-root'], argv.verbose ?? false);
696
865
  }
697
866
  async function removeHandler(argv) {
698
867
  const projectRoot = argv['project-root'];
@@ -733,18 +902,34 @@ async function checkHandler(argv) {
733
902
  ]);
734
903
  }
735
904
  async function updateHandler(argv) {
736
- await executeSkillsWrapper(argv['project-root'], [
737
- 'update',
738
- ...(argv.args ?? []),
739
- ]);
740
- const updated = await (0, AgentSourceCompatibility_1.updateAgentSkillsFromLock)(argv['project-root']);
741
- if (updated.updated.length > 0) {
742
- console.log(`[skiller] Updated ${updated.updated.length} agent-derived skill(s): ${updated.updated.join(', ')}`);
905
+ if (argv['no-sync'] && argv['sync-only']) {
906
+ throw new Error('[skiller] --no-sync and --sync-only cannot be combined.');
743
907
  }
744
- if (updated.warnings.length > 0) {
745
- console.log(updated.warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
908
+ const projectRoots = await getLifecycleProjectRoots(argv['project-root'], argv.nested);
909
+ for (const projectRoot of projectRoots) {
910
+ const syncResult = await maybeRunSync(projectRoot, {
911
+ skipSync: argv['no-sync'] ?? false,
912
+ });
913
+ if (syncResult.applied) {
914
+ await pruneOutputsForRemovedLockEntries(projectRoot, {
915
+ native: syncResult.removedNativeLockSkills,
916
+ agent: syncResult.removedAgentLockSkills,
917
+ });
918
+ }
919
+ if (argv['sync-only']) {
920
+ continue;
921
+ }
922
+ await pruneStaleLockBackedSkills(projectRoot);
923
+ await executeSkillsWrapper(projectRoot, ['update', ...(argv.args ?? [])]);
924
+ const updated = await (0, AgentSourceCompatibility_1.updateAgentSkillsFromLock)(projectRoot);
925
+ if (updated.updated.length > 0) {
926
+ console.log(`[skiller] Updated ${updated.updated.length} agent-derived skill(s): ${updated.updated.join(', ')}`);
927
+ }
928
+ if (updated.warnings.length > 0) {
929
+ console.log(updated.warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
930
+ }
931
+ await applyAfterSkillsLifecycleStep(projectRoot, argv.verbose ?? false);
746
932
  }
747
- await applyAfterSkillsLifecycleStep(argv['project-root'], argv.verbose ?? false);
748
933
  }
749
934
  async function outdatedHandler(argv) {
750
935
  await executeSkillsWrapper(argv['project-root'], [
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.parseCompatibleSource = parseCompatibleSource;
37
+ exports.createSourceWorkspace = createSourceWorkspace;
37
38
  exports.hasListFlag = hasListFlag;
38
39
  exports.hasGlobalFlag = hasGlobalFlag;
39
40
  exports.extractAddSource = extractAddSource;
@@ -44,6 +45,8 @@ exports.restoreAgentSkillsFromLock = restoreAgentSkillsFromLock;
44
45
  exports.getOutdatedAgentSkills = getOutdatedAgentSkills;
45
46
  exports.updateAgentSkillsFromLock = updateAgentSkillsFromLock;
46
47
  exports.removeAgentManagedSkills = removeAgentManagedSkills;
48
+ exports.pruneMissingNativeSkillsFromLock = pruneMissingNativeSkillsFromLock;
49
+ exports.pruneMissingAgentSkillsFromLock = pruneMissingAgentSkillsFromLock;
47
50
  const crypto = __importStar(require("crypto"));
48
51
  const fs = __importStar(require("fs/promises"));
49
52
  const path = __importStar(require("path"));
@@ -67,12 +70,43 @@ const SKIP_DIRS = new Set([
67
70
  'tmp',
68
71
  'tmp-fixtures',
69
72
  ]);
73
+ const NATIVE_SKILLS_LOCK_VERSION = 1;
70
74
  function hashContent(content) {
71
75
  return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
72
76
  }
73
77
  function normalizeSkillNameForFilesystem(name) {
74
78
  return name.trim().replace(/:/g, '-');
75
79
  }
80
+ function createEmptyNativeSkillsLock() {
81
+ return {
82
+ version: NATIVE_SKILLS_LOCK_VERSION,
83
+ skills: {},
84
+ };
85
+ }
86
+ async function readNativeSkillsLock(projectRoot) {
87
+ try {
88
+ const raw = JSON.parse(await fs.readFile(path.join(projectRoot, 'skills-lock.json'), 'utf8'));
89
+ if (raw.version !== NATIVE_SKILLS_LOCK_VERSION ||
90
+ !raw.skills ||
91
+ typeof raw.skills !== 'object') {
92
+ return createEmptyNativeSkillsLock();
93
+ }
94
+ return raw;
95
+ }
96
+ catch {
97
+ return createEmptyNativeSkillsLock();
98
+ }
99
+ }
100
+ async function writeNativeSkillsLock(projectRoot, lock) {
101
+ const sortedSkills = {};
102
+ for (const key of Object.keys(lock.skills).sort((a, b) => a.localeCompare(b))) {
103
+ sortedSkills[key] = lock.skills[key];
104
+ }
105
+ await fs.writeFile(path.join(projectRoot, 'skills-lock.json'), JSON.stringify({
106
+ version: NATIVE_SKILLS_LOCK_VERSION,
107
+ skills: sortedSkills,
108
+ }, null, 2) + '\n', 'utf8');
109
+ }
76
110
  function isLocalPath(input) {
77
111
  return (path.isAbsolute(input) ||
78
112
  input.startsWith('./') ||
@@ -207,6 +241,9 @@ async function withSourceWorkspace(rawSource) {
207
241
  const parsed = parseCompatibleSource(rawSource);
208
242
  return withParsedSourceWorkspace(parsed);
209
243
  }
244
+ async function createSourceWorkspace(rawSource) {
245
+ return withSourceWorkspace(rawSource);
246
+ }
210
247
  async function withParsedSourceWorkspace(parsed) {
211
248
  if (parsed.type === 'local') {
212
249
  const targetPath = parsed.subpath
@@ -708,3 +745,84 @@ async function removeAgentManagedSkills(projectRoot, skillNames) {
708
745
  }
709
746
  return removed;
710
747
  }
748
+ async function pruneMissingNativeSkillsFromLock(projectRoot) {
749
+ const lock = await readNativeSkillsLock(projectRoot);
750
+ const prunedKeys = [];
751
+ const prunedOutputNames = [];
752
+ const warnings = [];
753
+ for (const entries of groupLockEntriesBySource(lock.skills).values()) {
754
+ const [, entry] = entries[0];
755
+ if (entry.sourceType === 'node_modules' ||
756
+ entry.sourceType === 'well-known') {
757
+ continue;
758
+ }
759
+ let workspace = null;
760
+ try {
761
+ workspace = await withSourceWorkspace(entry.source);
762
+ const availableSkillNames = new Set(await discoverSkillNames(workspace.searchPath));
763
+ for (const [skillName] of entries) {
764
+ const installName = normalizeSkillNameForFilesystem(skillName);
765
+ if (availableSkillNames.has(installName))
766
+ continue;
767
+ prunedKeys.push(skillName);
768
+ prunedOutputNames.push(installName);
769
+ }
770
+ }
771
+ catch (error) {
772
+ warnings.push(`Could not inspect '${entry.source}' for stale skills: ${error instanceof Error ? error.message : String(error)}`);
773
+ }
774
+ finally {
775
+ if (workspace) {
776
+ await workspace.cleanup();
777
+ }
778
+ }
779
+ }
780
+ if (prunedKeys.length > 0) {
781
+ for (const skillName of prunedKeys) {
782
+ delete lock.skills[skillName];
783
+ }
784
+ await writeNativeSkillsLock(projectRoot, lock);
785
+ }
786
+ return {
787
+ prunedKeys: prunedKeys.sort((a, b) => a.localeCompare(b)),
788
+ prunedOutputNames: prunedOutputNames.sort((a, b) => a.localeCompare(b)),
789
+ warnings,
790
+ };
791
+ }
792
+ async function pruneMissingAgentSkillsFromLock(projectRoot) {
793
+ const lock = await (0, SkillerLock_1.readSkillerLock)(projectRoot);
794
+ const prunedKeys = [];
795
+ const prunedOutputNames = [];
796
+ const warnings = [];
797
+ for (const entries of groupLockEntriesBySource(lock.skills).values()) {
798
+ const [, entry] = entries[0];
799
+ let workspace = null;
800
+ try {
801
+ workspace = await withParsedSourceWorkspace(parseSourceFromLockEntry(entry));
802
+ const agentSkills = await discoverAgentSkillCandidates(workspace.searchPath, workspace.rootPath);
803
+ const candidates = new Set(agentSkills.map((candidate) => candidate.sourceRelPath));
804
+ for (const [skillName, lockEntry] of entries) {
805
+ if (candidates.has(lockEntry.sourceRelPath))
806
+ continue;
807
+ prunedKeys.push(skillName);
808
+ prunedOutputNames.push(normalizeSkillNameForFilesystem(skillName));
809
+ }
810
+ }
811
+ catch (error) {
812
+ warnings.push(`Could not inspect '${entry.source}' for stale agent-derived skills: ${error instanceof Error ? error.message : String(error)}`);
813
+ }
814
+ finally {
815
+ if (workspace) {
816
+ await workspace.cleanup();
817
+ }
818
+ }
819
+ }
820
+ if (prunedKeys.length > 0) {
821
+ await (0, SkillerLock_1.removeSkillerLockEntries)(projectRoot, prunedKeys);
822
+ }
823
+ return {
824
+ prunedKeys: prunedKeys.sort((a, b) => a.localeCompare(b)),
825
+ prunedOutputNames: prunedOutputNames.sort((a, b) => a.localeCompare(b)),
826
+ warnings,
827
+ };
828
+ }
@@ -33,6 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.stripSymbols = stripSymbols;
37
+ exports.isPlainRecord = isPlainRecord;
38
+ exports.deepMergeConfig = deepMergeConfig;
39
+ exports.withoutSync = withoutSync;
40
+ exports.loadRawConfig = loadRawConfig;
36
41
  exports.loadConfig = loadConfig;
37
42
  const fs_1 = require("fs");
38
43
  const path = __importStar(require("path"));
@@ -56,10 +61,20 @@ const agentConfigSchema = zod_1.z
56
61
  mcp: mcpConfigSchema,
57
62
  })
58
63
  .optional();
64
+ const syncConfigSchema = zod_1.z
65
+ .object({
66
+ source: zod_1.z.string(),
67
+ mode: zod_1.z.enum(['auto', 'preset', 'repo']).optional(),
68
+ clean: zod_1.z.boolean().optional(),
69
+ include: zod_1.z.array(zod_1.z.string()).optional(),
70
+ exclude: zod_1.z.array(zod_1.z.string()).optional(),
71
+ })
72
+ .optional();
59
73
  const skillerConfigSchema = zod_1.z.object({
60
74
  default_agents: zod_1.z.array(zod_1.z.string()).optional(),
61
75
  root_folder: zod_1.z.string().optional(),
62
76
  agents: zod_1.z.record(zod_1.z.string(), agentConfigSchema).optional(),
77
+ sync: syncConfigSchema,
63
78
  mcp: zod_1.z
64
79
  .object({
65
80
  enabled: zod_1.z.boolean().optional(),
@@ -111,49 +126,120 @@ function stripSymbols(obj) {
111
126
  }
112
127
  return result;
113
128
  }
114
- /**
115
- * Loads and parses the skiller TOML configuration file, applying defaults.
116
- * If the file is missing or invalid, returns empty/default config.
117
- */
118
- async function loadConfig(options) {
119
- const { projectRoot, configPath, cliAgents } = options;
120
- let configFile;
121
- if (configPath) {
122
- configFile = path.resolve(configPath);
123
- }
124
- else {
125
- const localConfigFile = path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR, project_paths_1.SKILLER_CONFIG_FILE);
126
- try {
127
- await fs_1.promises.access(localConfigFile);
128
- configFile = localConfigFile;
129
+ function isPlainRecord(value) {
130
+ return !!value && typeof value === 'object' && !Array.isArray(value);
131
+ }
132
+ function deepMergeConfig(base, override) {
133
+ const result = { ...base };
134
+ for (const [key, value] of Object.entries(override)) {
135
+ if (value === undefined)
136
+ continue;
137
+ if (Array.isArray(value) || !isPlainRecord(value)) {
138
+ result[key] = value;
139
+ continue;
129
140
  }
130
- catch {
131
- // If local config doesn't exist, try global config
132
- const xdgConfigDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
133
- configFile = path.join(xdgConfigDir, 'skiller', 'skiller.toml');
141
+ const baseValue = result[key];
142
+ if (isPlainRecord(baseValue)) {
143
+ result[key] = deepMergeConfig(baseValue, value);
144
+ continue;
134
145
  }
146
+ result[key] = deepMergeConfig({}, value);
147
+ }
148
+ return result;
149
+ }
150
+ function withoutSync(raw) {
151
+ const next = { ...raw };
152
+ delete next.sync;
153
+ return next;
154
+ }
155
+ async function resolveConfigFile(projectRoot, configPath) {
156
+ if (configPath) {
157
+ return path.resolve(configPath);
158
+ }
159
+ const localConfigFile = path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR, project_paths_1.SKILLER_CONFIG_FILE);
160
+ try {
161
+ await fs_1.promises.access(localConfigFile);
162
+ return localConfigFile;
163
+ }
164
+ catch {
165
+ const xdgConfigDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
166
+ return path.join(xdgConfigDir, 'skiller', 'skiller.toml');
135
167
  }
136
- let raw = {};
168
+ }
169
+ async function readRawConfigFile(filePath, warningLabel) {
137
170
  try {
138
- const text = await fs_1.promises.readFile(configFile, 'utf8');
171
+ const text = await fs_1.promises.readFile(filePath, 'utf8');
139
172
  const parsed = text.trim() ? (0, toml_1.parse)(text) : {};
140
- // Strip Symbol properties added by @iarna/toml (required for Zod v4+)
141
- raw = stripSymbols(parsed);
142
- // Validate the configuration with zod
143
- const validationResult = skillerConfigSchema.safeParse(raw);
144
- if (!validationResult.success) {
145
- throw (0, constants_1.createSkillerError)('Invalid configuration file format', `File: ${configFile}, Errors: ${validationResult.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join(', ')}`);
146
- }
173
+ return stripSymbols(parsed);
147
174
  }
148
175
  catch (err) {
149
176
  if (err instanceof Error && err.code !== 'ENOENT') {
150
- if (err.message.includes('[skiller]')) {
151
- throw err; // Re-throw validation errors
152
- }
153
- console.warn(`[skiller] Warning: could not read config file at ${configFile}: ${err.message}`);
177
+ const prefix = warningLabel
178
+ ? `${warningLabel} at ${filePath}`
179
+ : `config file at ${filePath}`;
180
+ console.warn(`[skiller] Warning: could not read ${prefix}: ${err.message}`);
154
181
  }
155
- raw = {};
182
+ return {};
183
+ }
184
+ }
185
+ function validateRawConfig(raw, configFile) {
186
+ const validationResult = skillerConfigSchema.safeParse(raw);
187
+ if (!validationResult.success) {
188
+ throw (0, constants_1.createSkillerError)('Invalid configuration file format', `File: ${configFile}, Errors: ${validationResult.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join(', ')}`);
156
189
  }
190
+ }
191
+ function parseSyncConfig(raw, projectRoot) {
192
+ const syncSection = raw.sync;
193
+ if (!isPlainRecord(syncSection) || typeof syncSection.source !== 'string') {
194
+ return undefined;
195
+ }
196
+ const mode = syncSection.mode === 'preset' ||
197
+ syncSection.mode === 'repo' ||
198
+ syncSection.mode === 'auto'
199
+ ? syncSection.mode
200
+ : 'auto';
201
+ return {
202
+ source: path.resolve(projectRoot, syncSection.source),
203
+ mode,
204
+ clean: typeof syncSection.clean === 'boolean' ? syncSection.clean : true,
205
+ include: Array.isArray(syncSection.include)
206
+ ? syncSection.include.map((entry) => String(entry))
207
+ : undefined,
208
+ exclude: Array.isArray(syncSection.exclude)
209
+ ? syncSection.exclude.map((entry) => String(entry))
210
+ : undefined,
211
+ };
212
+ }
213
+ async function loadRawConfig(options) {
214
+ const configFile = await resolveConfigFile(options.projectRoot, options.configPath);
215
+ const localRaw = await readRawConfigFile(configFile);
216
+ validateRawConfig(localRaw, configFile);
217
+ const sync = parseSyncConfig(localRaw, options.projectRoot);
218
+ let baseRaw = {};
219
+ let baseConfigFile;
220
+ if (sync) {
221
+ baseConfigFile = path.join(sync.source, project_paths_1.CANONICAL_SKILLER_DIR, project_paths_1.SKILLER_CONFIG_FILE);
222
+ baseRaw = withoutSync(await readRawConfigFile(baseConfigFile, 'base sync config'));
223
+ validateRawConfig(baseRaw, baseConfigFile);
224
+ }
225
+ const raw = deepMergeConfig(baseRaw, localRaw);
226
+ validateRawConfig(raw, configFile);
227
+ return {
228
+ raw,
229
+ localRaw,
230
+ baseRaw,
231
+ configFile,
232
+ baseConfigFile,
233
+ sync,
234
+ };
235
+ }
236
+ /**
237
+ * Loads and parses the skiller TOML configuration file, applying defaults.
238
+ * If the file is missing or invalid, returns empty/default config.
239
+ */
240
+ async function loadConfig(options) {
241
+ const { projectRoot, cliAgents } = options;
242
+ const { raw, sync } = await loadRawConfig(options);
157
243
  const defaultAgents = Array.isArray(raw.default_agents)
158
244
  ? raw.default_agents.map((a) => String(a))
159
245
  : undefined;
@@ -267,6 +353,7 @@ async function loadConfig(options) {
267
353
  backup: backupConfig,
268
354
  skills: skillsConfig,
269
355
  rules: rulesConfig,
356
+ sync,
270
357
  nested,
271
358
  nestedDefined,
272
359
  };
@@ -0,0 +1,291 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.installPresetIntoProject = installPresetIntoProject;
37
+ const crypto_1 = require("crypto");
38
+ const fs_1 = require("fs");
39
+ const path = __importStar(require("path"));
40
+ const toml_1 = require("@iarna/toml");
41
+ const AgentSourceCompatibility_1 = require("./AgentSourceCompatibility");
42
+ const ConfigLoader_1 = require("./ConfigLoader");
43
+ const FileSystemUtils_1 = require("./FileSystemUtils");
44
+ const project_paths_1 = require("./project-paths");
45
+ const PRESET_MANIFEST_RELATIVE_PATH = path
46
+ .join(project_paths_1.CANONICAL_SKILLER_DIR, '.skiller-preset-manifest.json')
47
+ .replace(/\\/g, '/');
48
+ const HARD_DENY_PATTERNS = [
49
+ '.agents/skills/**',
50
+ '.claude/skills/**',
51
+ PRESET_MANIFEST_RELATIVE_PATH,
52
+ '.agents/.skiller-sync-manifest.json',
53
+ '.git/**',
54
+ 'node_modules/**',
55
+ ];
56
+ const COPY_EXCEPTIONS = new Set(['.agents/skiller.toml']);
57
+ function normalizeRelativePath(value) {
58
+ return value.replace(/\\/g, '/');
59
+ }
60
+ function isLikelyLocalPath(input) {
61
+ return (path.isAbsolute(input) ||
62
+ input.startsWith('./') ||
63
+ input.startsWith('../') ||
64
+ input === '.' ||
65
+ input === '..' ||
66
+ /^[a-zA-Z]:[/\\]/.test(input));
67
+ }
68
+ function hashBuffer(buffer) {
69
+ return (0, crypto_1.createHash)('sha256').update(buffer).digest('hex');
70
+ }
71
+ async function pathExists(targetPath) {
72
+ try {
73
+ await fs_1.promises.access(targetPath);
74
+ return true;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ async function readPresetManifest(projectRoot) {
81
+ try {
82
+ return JSON.parse(await fs_1.promises.readFile(path.join(projectRoot, PRESET_MANIFEST_RELATIVE_PATH), 'utf8'));
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ async function writePresetManifest(projectRoot, manifest) {
89
+ const manifestPath = path.join(projectRoot, PRESET_MANIFEST_RELATIVE_PATH);
90
+ await fs_1.promises.mkdir(path.dirname(manifestPath), { recursive: true });
91
+ await fs_1.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
92
+ }
93
+ async function removeEmptyDirectoriesUpward(fromDir, stopDir) {
94
+ let current = fromDir;
95
+ const normalizedStop = path.resolve(stopDir);
96
+ while (path.resolve(current).startsWith(normalizedStop)) {
97
+ if (path.resolve(current) === normalizedStop)
98
+ return;
99
+ try {
100
+ const entries = await fs_1.promises.readdir(current);
101
+ if (entries.length > 0)
102
+ return;
103
+ await fs_1.promises.rmdir(current);
104
+ }
105
+ catch {
106
+ return;
107
+ }
108
+ current = path.dirname(current);
109
+ }
110
+ }
111
+ async function collectPresetFiles(rootDir) {
112
+ const results = [];
113
+ async function walk(currentDir) {
114
+ const entries = await fs_1.promises.readdir(currentDir, { withFileTypes: true });
115
+ for (const entry of entries) {
116
+ if (entry.name === '.git' || entry.name === 'node_modules')
117
+ continue;
118
+ const fullPath = path.join(currentDir, entry.name);
119
+ const relativePath = normalizeRelativePath(path.relative(rootDir, fullPath));
120
+ if (entry.isDirectory()) {
121
+ await walk(fullPath);
122
+ continue;
123
+ }
124
+ if (entry.isFile()) {
125
+ results.push(relativePath);
126
+ }
127
+ }
128
+ }
129
+ await walk(rootDir);
130
+ return results.sort((a, b) => a.localeCompare(b));
131
+ }
132
+ function isHardDenied(relativePath) {
133
+ return HARD_DENY_PATTERNS.some((pattern) => (0, FileSystemUtils_1.matchesPattern)(relativePath, pattern));
134
+ }
135
+ function isPresetAllowlisted(relativePath) {
136
+ if (relativePath === 'skills-lock.json' ||
137
+ relativePath === 'skiller-lock.json') {
138
+ return true;
139
+ }
140
+ return (relativePath.startsWith('.agents/') ||
141
+ relativePath.startsWith('.claude/') ||
142
+ relativePath.startsWith('.codex/'));
143
+ }
144
+ async function readRawTomlFile(filePath) {
145
+ try {
146
+ const text = await fs_1.promises.readFile(filePath, 'utf8');
147
+ const parsed = text.trim() ? (0, toml_1.parse)(text) : {};
148
+ const stripped = (0, ConfigLoader_1.stripSymbols)(parsed);
149
+ return (0, ConfigLoader_1.isPlainRecord)(stripped) ? stripped : {};
150
+ }
151
+ catch {
152
+ return {};
153
+ }
154
+ }
155
+ async function looksLikePresetRoot(dirPath) {
156
+ try {
157
+ const stats = await fs_1.promises.stat(path.join(dirPath, '.agents'));
158
+ return stats.isDirectory();
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
164
+ async function resolvePresetRoot(workspace, presetName) {
165
+ const { searchPath } = workspace;
166
+ const namedCandidates = presetName
167
+ ? [
168
+ path.join(searchPath, 'presets', presetName),
169
+ path.join(searchPath, presetName),
170
+ path.join(searchPath, '.agents', 'presets', presetName),
171
+ ]
172
+ : [];
173
+ if (presetName) {
174
+ if (path.basename(searchPath) === presetName &&
175
+ (await looksLikePresetRoot(searchPath))) {
176
+ return { preset: presetName, presetRoot: searchPath };
177
+ }
178
+ for (const candidate of namedCandidates) {
179
+ if (await looksLikePresetRoot(candidate)) {
180
+ return { preset: presetName, presetRoot: candidate };
181
+ }
182
+ }
183
+ throw new Error(`[skiller] Preset '${presetName}' was not found under source '${workspace.parsed.source}'.`);
184
+ }
185
+ if (await looksLikePresetRoot(searchPath)) {
186
+ return { preset: path.basename(searchPath), presetRoot: searchPath };
187
+ }
188
+ const presetsDir = path.join(searchPath, 'presets');
189
+ let presetDirs = [];
190
+ try {
191
+ const entries = await fs_1.promises.readdir(presetsDir, { withFileTypes: true });
192
+ presetDirs = (await Promise.all(entries
193
+ .filter((entry) => entry.isDirectory())
194
+ .map(async (entry) => {
195
+ const candidate = path.join(presetsDir, entry.name);
196
+ return (await looksLikePresetRoot(candidate)) ? candidate : null;
197
+ }))).filter((entry) => !!entry);
198
+ }
199
+ catch {
200
+ // Ignore missing presets directories.
201
+ }
202
+ if (presetDirs.length === 1) {
203
+ return {
204
+ preset: path.basename(presetDirs[0]),
205
+ presetRoot: presetDirs[0],
206
+ };
207
+ }
208
+ const defaultPreset = presetDirs.find((candidate) => path.basename(candidate) === 'default');
209
+ if (defaultPreset) {
210
+ return { preset: 'default', presetRoot: defaultPreset };
211
+ }
212
+ throw new Error(`[skiller] No preset could be selected from source '${workspace.parsed.source}'. Pass --preset <name>.`);
213
+ }
214
+ async function resolvePresetSourceInput(projectRoot, rawSource) {
215
+ if (!isLikelyLocalPath(rawSource)) {
216
+ return rawSource;
217
+ }
218
+ const cwdCandidate = path.resolve(rawSource);
219
+ if (await pathExists(cwdCandidate)) {
220
+ return cwdCandidate;
221
+ }
222
+ const projectCandidate = path.resolve(projectRoot, rawSource);
223
+ if (await pathExists(projectCandidate)) {
224
+ return projectCandidate;
225
+ }
226
+ return cwdCandidate;
227
+ }
228
+ async function writeMergedConfig(projectRoot, presetRoot) {
229
+ const baseConfigPath = path.join(presetRoot, project_paths_1.CANONICAL_SKILLER_DIR, project_paths_1.SKILLER_CONFIG_FILE);
230
+ const localConfigPath = path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR, project_paths_1.SKILLER_CONFIG_FILE);
231
+ const [baseRaw, localRaw] = await Promise.all([
232
+ readRawTomlFile(baseConfigPath),
233
+ readRawTomlFile(localConfigPath),
234
+ ]);
235
+ const merged = (0, ConfigLoader_1.deepMergeConfig)((0, ConfigLoader_1.withoutSync)(baseRaw), (0, ConfigLoader_1.withoutSync)(localRaw));
236
+ const rendered = (0, toml_1.stringify)(merged);
237
+ await fs_1.promises.mkdir(path.dirname(localConfigPath), { recursive: true });
238
+ await fs_1.promises.writeFile(localConfigPath, rendered, 'utf8');
239
+ return hashBuffer(Buffer.from(rendered, 'utf8'));
240
+ }
241
+ async function installPresetIntoProject(projectRoot, options) {
242
+ const resolvedSource = await resolvePresetSourceInput(projectRoot, options.source);
243
+ const workspace = await (0, AgentSourceCompatibility_1.createSourceWorkspace)(resolvedSource);
244
+ try {
245
+ const { preset, presetRoot } = await resolvePresetRoot(workspace, options.preset);
246
+ const previousManifest = await readPresetManifest(projectRoot);
247
+ const selectedFiles = (await collectPresetFiles(presetRoot)).filter((relativePath) => !isHardDenied(relativePath) &&
248
+ !COPY_EXCEPTIONS.has(relativePath) &&
249
+ isPresetAllowlisted(relativePath));
250
+ const nextFiles = {};
251
+ const synced = [];
252
+ for (const relativePath of selectedFiles) {
253
+ const sourcePath = path.join(presetRoot, relativePath);
254
+ const targetPath = path.join(projectRoot, relativePath);
255
+ const content = await fs_1.promises.readFile(sourcePath);
256
+ await fs_1.promises.mkdir(path.dirname(targetPath), { recursive: true });
257
+ await fs_1.promises.writeFile(targetPath, content);
258
+ nextFiles[relativePath] = hashBuffer(content);
259
+ synced.push(relativePath);
260
+ }
261
+ const mergedConfigHash = await writeMergedConfig(projectRoot, presetRoot);
262
+ nextFiles['.agents/skiller.toml'] = mergedConfigHash;
263
+ synced.push('.agents/skiller.toml');
264
+ const removed = [];
265
+ if (previousManifest) {
266
+ for (const relativePath of Object.keys(previousManifest.files)) {
267
+ if (nextFiles[relativePath])
268
+ continue;
269
+ const targetPath = path.join(projectRoot, relativePath);
270
+ await fs_1.promises.rm(targetPath, { force: true });
271
+ await removeEmptyDirectoriesUpward(path.dirname(targetPath), projectRoot);
272
+ removed.push(relativePath);
273
+ }
274
+ }
275
+ await writePresetManifest(projectRoot, {
276
+ version: 1,
277
+ source: options.source,
278
+ preset,
279
+ files: nextFiles,
280
+ });
281
+ return {
282
+ preset,
283
+ presetRoot,
284
+ removed: removed.sort((a, b) => a.localeCompare(b)),
285
+ synced: [...new Set(synced)].sort((a, b) => a.localeCompare(b)),
286
+ };
287
+ }
288
+ finally {
289
+ await workspace.cleanup();
290
+ }
291
+ }
@@ -0,0 +1,296 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.syncProjectFiles = syncProjectFiles;
37
+ const crypto_1 = require("crypto");
38
+ const fs_1 = require("fs");
39
+ const path = __importStar(require("path"));
40
+ const FileSystemUtils_1 = require("./FileSystemUtils");
41
+ const project_paths_1 = require("./project-paths");
42
+ const ConfigLoader_1 = require("./ConfigLoader");
43
+ const SYNC_MANIFEST_RELATIVE_PATH = path
44
+ .join(project_paths_1.CANONICAL_SKILLER_DIR, '.skiller-sync-manifest.json')
45
+ .replace(/\\/g, '/');
46
+ const PRESET_ROOT_ALLOWLIST = new Set([
47
+ '.agents',
48
+ '.claude',
49
+ '.codex',
50
+ 'skills-lock.json',
51
+ 'skiller-lock.json',
52
+ ]);
53
+ const PRESET_ROOT_IGNORES = new Set(['.DS_Store', '.git', 'node_modules']);
54
+ const HARD_DENY_PATTERNS = [
55
+ '.agents/skills/**',
56
+ '.claude/skills/**',
57
+ '.agents/.skiller-sync-manifest.json',
58
+ '.git/**',
59
+ 'node_modules/**',
60
+ ];
61
+ const COPY_EXCEPTIONS = new Set(['.agents/skiller.toml']);
62
+ function normalizeRelativePath(value) {
63
+ return value.replace(/\\/g, '/');
64
+ }
65
+ function hashBuffer(buffer) {
66
+ return (0, crypto_1.createHash)('sha256').update(buffer).digest('hex');
67
+ }
68
+ async function pathExists(targetPath) {
69
+ try {
70
+ await fs_1.promises.access(targetPath);
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ async function readJsonFile(filePath) {
78
+ try {
79
+ return JSON.parse(await fs_1.promises.readFile(filePath, 'utf8'));
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ async function readLockSkillNames(filePath) {
86
+ const raw = await readJsonFile(filePath);
87
+ return raw?.skills ? Object.keys(raw.skills) : [];
88
+ }
89
+ async function readManifest(projectRoot) {
90
+ return readJsonFile(path.join(projectRoot, SYNC_MANIFEST_RELATIVE_PATH));
91
+ }
92
+ async function collectSourceFiles(rootDir) {
93
+ const results = [];
94
+ async function walk(currentDir) {
95
+ const entries = await fs_1.promises.readdir(currentDir, { withFileTypes: true });
96
+ for (const entry of entries) {
97
+ if (entry.name === '.git' || entry.name === 'node_modules')
98
+ continue;
99
+ const fullPath = path.join(currentDir, entry.name);
100
+ const relativePath = normalizeRelativePath(path.relative(rootDir, fullPath));
101
+ if (entry.isDirectory()) {
102
+ await walk(fullPath);
103
+ continue;
104
+ }
105
+ if (entry.isFile()) {
106
+ results.push(relativePath);
107
+ }
108
+ }
109
+ }
110
+ await walk(rootDir);
111
+ return results.sort((a, b) => a.localeCompare(b));
112
+ }
113
+ function isHardDenied(relativePath) {
114
+ return HARD_DENY_PATTERNS.some((pattern) => (0, FileSystemUtils_1.matchesPattern)(relativePath, pattern));
115
+ }
116
+ function isPresetAllowlisted(relativePath) {
117
+ if (relativePath === 'skills-lock.json' ||
118
+ relativePath === 'skiller-lock.json') {
119
+ return true;
120
+ }
121
+ return (relativePath.startsWith('.agents/') ||
122
+ relativePath.startsWith('.claude/') ||
123
+ relativePath.startsWith('.codex/'));
124
+ }
125
+ async function normalizePatterns(sourceRoot, patterns) {
126
+ if (!patterns || patterns.length === 0)
127
+ return undefined;
128
+ const normalized = [];
129
+ for (const pattern of patterns) {
130
+ const next = normalizeRelativePath(pattern);
131
+ if (next.includes('*')) {
132
+ normalized.push(next);
133
+ continue;
134
+ }
135
+ const candidate = path.join(sourceRoot, next);
136
+ try {
137
+ const stat = await fs_1.promises.stat(candidate);
138
+ if (stat.isDirectory()) {
139
+ normalized.push(`${next.replace(/\/$/, '')}/**`);
140
+ }
141
+ else {
142
+ normalized.push(next);
143
+ }
144
+ }
145
+ catch {
146
+ normalized.push(next);
147
+ }
148
+ }
149
+ return normalized;
150
+ }
151
+ async function detectSyncMode(sync) {
152
+ if (sync.mode === 'preset' || sync.mode === 'repo') {
153
+ return sync.mode;
154
+ }
155
+ if (sync.include && sync.include.length > 0) {
156
+ return 'repo';
157
+ }
158
+ const entries = await fs_1.promises.readdir(sync.source, { withFileTypes: true });
159
+ const interestingEntries = entries.filter((entry) => !PRESET_ROOT_IGNORES.has(entry.name));
160
+ if (interestingEntries.length > 0 &&
161
+ interestingEntries.every((entry) => PRESET_ROOT_ALLOWLIST.has(entry.name))) {
162
+ return 'preset';
163
+ }
164
+ throw new Error(`[skiller] Sync source '${sync.source}' is not a valid preset root. Set [sync].mode = "repo" and add include patterns, or point source at a curated preset directory.`);
165
+ }
166
+ async function selectSyncFiles(sourceRoot, mode, sync) {
167
+ const sourceFiles = await collectSourceFiles(sourceRoot);
168
+ const normalizedInclude = await normalizePatterns(sourceRoot, sync.include);
169
+ const normalizedExclude = await normalizePatterns(sourceRoot, sync.exclude);
170
+ if (mode === 'repo' &&
171
+ (!normalizedInclude || normalizedInclude.length === 0)) {
172
+ throw new Error('[skiller] Repo sync mode requires [sync].include to be set.');
173
+ }
174
+ return sourceFiles.filter((relativePath) => {
175
+ if (isHardDenied(relativePath) || COPY_EXCEPTIONS.has(relativePath)) {
176
+ return false;
177
+ }
178
+ if (mode === 'preset' && !isPresetAllowlisted(relativePath)) {
179
+ return false;
180
+ }
181
+ if (normalizedExclude?.some((pattern) => (0, FileSystemUtils_1.matchesPattern)(relativePath, pattern))) {
182
+ return false;
183
+ }
184
+ if (mode === 'repo' && normalizedInclude) {
185
+ return normalizedInclude.some((pattern) => (0, FileSystemUtils_1.matchesPattern)(relativePath, pattern));
186
+ }
187
+ return true;
188
+ });
189
+ }
190
+ async function readMergedConfigSourceHash(sourceRoot) {
191
+ const configPath = path.join(sourceRoot, project_paths_1.CANONICAL_SKILLER_DIR, project_paths_1.SKILLER_CONFIG_FILE);
192
+ try {
193
+ return hashBuffer(await fs_1.promises.readFile(configPath));
194
+ }
195
+ catch {
196
+ return null;
197
+ }
198
+ }
199
+ async function removeEmptyDirectoriesUpward(fromDir, stopDir) {
200
+ let current = fromDir;
201
+ const normalizedStop = path.resolve(stopDir);
202
+ while (path.resolve(current).startsWith(normalizedStop)) {
203
+ if (path.resolve(current) === normalizedStop)
204
+ return;
205
+ try {
206
+ const entries = await fs_1.promises.readdir(current);
207
+ if (entries.length > 0)
208
+ return;
209
+ await fs_1.promises.rmdir(current);
210
+ }
211
+ catch {
212
+ return;
213
+ }
214
+ current = path.dirname(current);
215
+ }
216
+ }
217
+ async function writeManifest(projectRoot, manifest) {
218
+ const manifestPath = path.join(projectRoot, SYNC_MANIFEST_RELATIVE_PATH);
219
+ await fs_1.promises.mkdir(path.dirname(manifestPath), { recursive: true });
220
+ await fs_1.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
221
+ }
222
+ async function syncProjectFiles(projectRoot) {
223
+ const config = await (0, ConfigLoader_1.loadConfig)({ projectRoot });
224
+ if (!config.sync) {
225
+ return {
226
+ applied: false,
227
+ synced: [],
228
+ removed: [],
229
+ removedNativeLockSkills: [],
230
+ removedAgentLockSkills: [],
231
+ };
232
+ }
233
+ const sync = config.sync;
234
+ if (!(await pathExists(sync.source))) {
235
+ throw new Error(`[skiller] Sync source '${sync.source}' does not exist.`);
236
+ }
237
+ const sourceStat = await fs_1.promises.stat(sync.source);
238
+ if (!sourceStat.isDirectory()) {
239
+ throw new Error(`[skiller] Sync source '${sync.source}' must be a directory.`);
240
+ }
241
+ const previousManifest = await readManifest(projectRoot);
242
+ const previousNativeLockNames = await readLockSkillNames(path.join(projectRoot, 'skills-lock.json'));
243
+ const previousAgentLockNames = await readLockSkillNames(path.join(projectRoot, 'skiller-lock.json'));
244
+ const mode = await detectSyncMode(sync);
245
+ const selectedFiles = await selectSyncFiles(sync.source, mode, sync);
246
+ const nextFiles = {};
247
+ const synced = [];
248
+ for (const relativePath of selectedFiles) {
249
+ const sourcePath = path.join(sync.source, relativePath);
250
+ const targetPath = path.join(projectRoot, relativePath);
251
+ const content = await fs_1.promises.readFile(sourcePath);
252
+ const hash = hashBuffer(content);
253
+ await fs_1.promises.mkdir(path.dirname(targetPath), { recursive: true });
254
+ await fs_1.promises.writeFile(targetPath, content);
255
+ nextFiles[relativePath] = hash;
256
+ synced.push(relativePath);
257
+ }
258
+ const removed = [];
259
+ if (sync.clean && previousManifest) {
260
+ for (const relativePath of Object.keys(previousManifest.files)) {
261
+ if (nextFiles[relativePath])
262
+ continue;
263
+ const targetPath = path.join(projectRoot, relativePath);
264
+ await fs_1.promises.rm(targetPath, { force: true });
265
+ await removeEmptyDirectoriesUpward(path.dirname(targetPath), projectRoot);
266
+ removed.push(relativePath);
267
+ }
268
+ }
269
+ const manifest = {
270
+ version: 1,
271
+ source: sync.source,
272
+ mode,
273
+ files: nextFiles,
274
+ mergedConfigSourceHash: await readMergedConfigSourceHash(sync.source),
275
+ };
276
+ await writeManifest(projectRoot, manifest);
277
+ const currentNativeLockNames = nextFiles['skills-lock.json'] || removed.includes('skills-lock.json')
278
+ ? await readLockSkillNames(path.join(projectRoot, 'skills-lock.json'))
279
+ : previousNativeLockNames;
280
+ const currentAgentLockNames = nextFiles['skiller-lock.json'] || removed.includes('skiller-lock.json')
281
+ ? await readLockSkillNames(path.join(projectRoot, 'skiller-lock.json'))
282
+ : previousAgentLockNames;
283
+ return {
284
+ applied: true,
285
+ source: sync.source,
286
+ mode,
287
+ synced: synced.sort((a, b) => a.localeCompare(b)),
288
+ removed: removed.sort((a, b) => a.localeCompare(b)),
289
+ removedNativeLockSkills: previousNativeLockNames
290
+ .filter((name) => !currentNativeLockNames.includes(name))
291
+ .sort((a, b) => a.localeCompare(b)),
292
+ removedAgentLockSkills: previousAgentLockNames
293
+ .filter((name) => !currentAgentLockNames.includes(name))
294
+ .sort((a, b) => a.localeCompare(b)),
295
+ };
296
+ }
@@ -34,7 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadUnifiedConfig = loadUnifiedConfig;
37
- const toml_1 = require("@iarna/toml");
38
37
  const fs_1 = require("fs");
39
38
  const path = __importStar(require("path"));
40
39
  const FileSystemUtils = __importStar(require("./FileSystemUtils"));
@@ -42,6 +41,7 @@ const FileSystemUtils_1 = require("./FileSystemUtils");
42
41
  const hash_1 = require("./hash");
43
42
  const RuleProcessor_1 = require("./RuleProcessor");
44
43
  const project_paths_1 = require("./project-paths");
44
+ const ConfigLoader_1 = require("./ConfigLoader");
45
45
  /**
46
46
  * Expand environment variables in a string.
47
47
  * Supports ${VAR} syntax, replacing with process.env[VAR] or empty string if not found.
@@ -66,27 +66,15 @@ async function loadUnifiedConfig(options) {
66
66
  version: '0.0.0-dev',
67
67
  };
68
68
  const diagnostics = [];
69
- // Read TOML if available
69
+ // Read merged TOML if available, including optional sync base inheritance.
70
70
  let tomlRaw = {};
71
- const tomlFile = options.configPath
72
- ? path.resolve(options.configPath)
73
- : path.join(meta.skillerDir, project_paths_1.SKILLER_CONFIG_FILE);
74
- try {
75
- const text = await fs_1.promises.readFile(tomlFile, 'utf8');
76
- tomlRaw = text.trim() ? (0, toml_1.parse)(text) : {};
77
- meta.configFile = tomlFile;
78
- }
79
- catch (err) {
80
- if (err.code !== 'ENOENT') {
81
- diagnostics.push({
82
- severity: 'warning',
83
- code: 'TOML_READ_ERROR',
84
- message: 'Failed to read skiller.toml',
85
- file: tomlFile,
86
- detail: err.message,
87
- });
88
- }
89
- }
71
+ const rawConfig = await (0, ConfigLoader_1.loadRawConfig)({
72
+ projectRoot: options.projectRoot,
73
+ configPath: options.configPath,
74
+ });
75
+ const tomlFile = rawConfig.configFile;
76
+ tomlRaw = rawConfig.raw;
77
+ meta.configFile = tomlFile;
90
78
  let defaultAgents;
91
79
  if (tomlRaw &&
92
80
  typeof tomlRaw === 'object' &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skiller",
3
- "version": "0.9.10",
3
+ "version": "0.9.12",
4
4
  "description": "Skiller — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "publishConfig": {