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.
- package/dist/cli/commands.js +45 -4
- package/dist/cli/handlers.js +206 -21
- package/dist/core/AgentSourceCompatibility.js +118 -0
- package/dist/core/ConfigLoader.js +120 -33
- package/dist/core/PresetInstaller.js +291 -0
- package/dist/core/SyncEngine.js +296 -0
- package/dist/core/UnifiedConfigLoader.js +9 -21
- package/package.json +1 -1
package/dist/cli/commands.js
CHANGED
|
@@ -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 .
|
|
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 .
|
|
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..]', '
|
|
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..]', '
|
|
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',
|
package/dist/cli/handlers.js
CHANGED
|
@@ -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
|
-
|
|
685
|
-
'
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
737
|
-
'
|
|
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
|
-
|
|
745
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
168
|
+
}
|
|
169
|
+
async function readRawConfigFile(filePath, warningLabel) {
|
|
137
170
|
try {
|
|
138
|
-
const text = await fs_1.promises.readFile(
|
|
171
|
+
const text = await fs_1.promises.readFile(filePath, 'utf8');
|
|
139
172
|
const parsed = text.trim() ? (0, toml_1.parse)(text) : {};
|
|
140
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
console.warn(`[skiller] Warning: could not read
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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' &&
|