skiller 0.9.11 → 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 {
@@ -119,6 +122,49 @@ async function pruneStaleLockBackedSkills(projectRoot) {
119
122
  console.log(warnings.map((warning) => `[skiller] ${warning}`).join('\n'));
120
123
  }
121
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
+ }
122
168
  function normalizeRequestedSkillNames(args) {
123
169
  if (!args || args.length === 0)
124
170
  return [];
@@ -227,6 +273,41 @@ async function readJsonObject(filePath) {
227
273
  return null;
228
274
  }
229
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
+ }
230
311
  async function cleanupLegacyClaudePluginState(projectRoot, pluginIds) {
231
312
  const pluginIdSet = new Set(pluginIds);
232
313
  if (pluginIdSet.size === 0)
@@ -721,19 +802,66 @@ async function addHandler(argv) {
721
802
  await applyAfterSkillsLifecycleStep(projectRoot, argv.verbose ?? false);
722
803
  }
723
804
  async function installHandler(argv) {
724
- await pruneStaleLockBackedSkills(argv['project-root']);
725
- await executeSkillsWrapper(argv['project-root'], [
726
- 'experimental_install',
727
- ...(argv.args ?? []),
728
- ]);
729
- const restored = await (0, AgentSourceCompatibility_1.restoreAgentSkillsFromLock)(argv['project-root']);
730
- if (restored.restored.length > 0) {
731
- console.log(`[skiller] Restored ${restored.restored.length} agent-derived skill(s): ${restored.restored.join(', ')}`);
732
- }
733
- if (restored.warnings.length > 0) {
734
- 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);
735
864
  }
736
- await applyAfterSkillsLifecycleStep(argv['project-root'], argv.verbose ?? false);
737
865
  }
738
866
  async function removeHandler(argv) {
739
867
  const projectRoot = argv['project-root'];
@@ -774,19 +902,34 @@ async function checkHandler(argv) {
774
902
  ]);
775
903
  }
776
904
  async function updateHandler(argv) {
777
- await pruneStaleLockBackedSkills(argv['project-root']);
778
- await executeSkillsWrapper(argv['project-root'], [
779
- 'update',
780
- ...(argv.args ?? []),
781
- ]);
782
- const updated = await (0, AgentSourceCompatibility_1.updateAgentSkillsFromLock)(argv['project-root']);
783
- if (updated.updated.length > 0) {
784
- 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.');
785
907
  }
786
- if (updated.warnings.length > 0) {
787
- 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);
788
932
  }
789
- await applyAfterSkillsLifecycleStep(argv['project-root'], argv.verbose ?? false);
790
933
  }
791
934
  async function outdatedHandler(argv) {
792
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;
@@ -240,6 +241,9 @@ async function withSourceWorkspace(rawSource) {
240
241
  const parsed = parseCompatibleSource(rawSource);
241
242
  return withParsedSourceWorkspace(parsed);
242
243
  }
244
+ async function createSourceWorkspace(rawSource) {
245
+ return withSourceWorkspace(rawSource);
246
+ }
243
247
  async function withParsedSourceWorkspace(parsed) {
244
248
  if (parsed.type === 'local') {
245
249
  const targetPath = parsed.subpath
@@ -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.11",
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": {