@nameczz/skill-sync 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/dist/src/autoSync.d.ts +36 -0
- package/dist/src/autoSync.js +235 -0
- package/dist/src/autoSync.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +211 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/codexArchive.d.ts +38 -0
- package/dist/src/codexArchive.js +340 -0
- package/dist/src/codexArchive.js.map +1 -0
- package/dist/src/config.d.ts +12 -0
- package/dist/src/config.js +78 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/copy.d.ts +1 -0
- package/dist/src/copy.js +42 -0
- package/dist/src/copy.js.map +1 -0
- package/dist/src/directoryPicker.d.ts +8 -0
- package/dist/src/directoryPicker.js +49 -0
- package/dist/src/directoryPicker.js.map +1 -0
- package/dist/src/format.d.ts +2 -0
- package/dist/src/format.js +27 -0
- package/dist/src/format.js.map +1 -0
- package/dist/src/frontmatter.d.ts +5 -0
- package/dist/src/frontmatter.js +36 -0
- package/dist/src/frontmatter.js.map +1 -0
- package/dist/src/git.d.ts +25 -0
- package/dist/src/git.js +227 -0
- package/dist/src/git.js.map +1 -0
- package/dist/src/hash.d.ts +1 -0
- package/dist/src/hash.js +34 -0
- package/dist/src/hash.js.map +1 -0
- package/dist/src/importSkill.d.ts +6 -0
- package/dist/src/importSkill.js +58 -0
- package/dist/src/importSkill.js.map +1 -0
- package/dist/src/init.d.ts +5 -0
- package/dist/src/init.js +13 -0
- package/dist/src/init.js.map +1 -0
- package/dist/src/installSkill.d.ts +6 -0
- package/dist/src/installSkill.js +62 -0
- package/dist/src/installSkill.js.map +1 -0
- package/dist/src/json.d.ts +2 -0
- package/dist/src/json.js +11 -0
- package/dist/src/json.js.map +1 -0
- package/dist/src/metadata.d.ts +11 -0
- package/dist/src/metadata.js +115 -0
- package/dist/src/metadata.js.map +1 -0
- package/dist/src/paths.d.ts +22 -0
- package/dist/src/paths.js +103 -0
- package/dist/src/paths.js.map +1 -0
- package/dist/src/removeLocalSkill.d.ts +5 -0
- package/dist/src/removeLocalSkill.js +79 -0
- package/dist/src/removeLocalSkill.js.map +1 -0
- package/dist/src/resolveConflict.d.ts +10 -0
- package/dist/src/resolveConflict.js +146 -0
- package/dist/src/resolveConflict.js.map +1 -0
- package/dist/src/scanner.d.ts +5 -0
- package/dist/src/scanner.js +57 -0
- package/dist/src/scanner.js.map +1 -0
- package/dist/src/server.d.ts +10 -0
- package/dist/src/server.js +494 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/sessionUsage.d.ts +14 -0
- package/dist/src/sessionUsage.js +180 -0
- package/dist/src/sessionUsage.js.map +1 -0
- package/dist/src/skillDependencies.d.ts +2 -0
- package/dist/src/skillDependencies.js +56 -0
- package/dist/src/skillDependencies.js.map +1 -0
- package/dist/src/skillMentions.d.ts +3 -0
- package/dist/src/skillMentions.js +111 -0
- package/dist/src/skillMentions.js.map +1 -0
- package/dist/src/status.d.ts +3 -0
- package/dist/src/status.js +134 -0
- package/dist/src/status.js.map +1 -0
- package/dist/src/stopSyncingSkill.d.ts +2 -0
- package/dist/src/stopSyncingSkill.js +31 -0
- package/dist/src/stopSyncingSkill.js.map +1 -0
- package/dist/src/sync.d.ts +48 -0
- package/dist/src/sync.js +741 -0
- package/dist/src/sync.js.map +1 -0
- package/dist/src/types.d.ts +84 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/updateLocalSkill.d.ts +6 -0
- package/dist/src/updateLocalSkill.js +19 -0
- package/dist/src/updateLocalSkill.js.map +1 -0
- package/dist/src/usage.d.ts +6 -0
- package/dist/src/usage.js +84 -0
- package/dist/src/usage.js.map +1 -0
- package/dist/src/usageMonitor.d.ts +17 -0
- package/dist/src/usageMonitor.js +90 -0
- package/dist/src/usageMonitor.js.map +1 -0
- package/dist/web/assets/index-CPJdd8n0.js +59 -0
- package/dist/web/assets/index-T4bm09OX.css +2 -0
- package/dist/web/index.html +13 -0
- package/dist/web/style-options/common.css +515 -0
- package/dist/web/style-options/console.html +143 -0
- package/dist/web/style-options/desktop.html +144 -0
- package/dist/web/style-options/index.html +36 -0
- package/dist/web/style-options/workbench.html +112 -0
- package/package.json +84 -0
package/dist/src/sync.js
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { buildStatusReport } from "./status.js";
|
|
6
|
+
import { importLocalSkill } from "./importSkill.js";
|
|
7
|
+
import { gitAdd, gitBranchSyncStatus, gitCommit, gitHasStagedChanges, gitPull, gitPush, gitRemotes, gitStatus, runGit } from "./git.js";
|
|
8
|
+
import { cleanupLegacyArchiveArtifacts, writeSkillsMetadata } from "./metadata.js";
|
|
9
|
+
import { readSkillFrontmatter } from "./frontmatter.js";
|
|
10
|
+
import { hashDirectory } from "./hash.js";
|
|
11
|
+
import { copySkillDirectory } from "./copy.js";
|
|
12
|
+
import { deriveSyncState } from "./status.js";
|
|
13
|
+
import { repoSettingsPath, repoSkillsDir, repoSkillsMetadataPath, repoUsageEventsPath, resolveSkillPath, validateSkillId } from "./paths.js";
|
|
14
|
+
let syncGate = Promise.resolve();
|
|
15
|
+
async function withSyncLock(task) {
|
|
16
|
+
const next = syncGate.then(() => task());
|
|
17
|
+
syncGate = next.then(() => undefined, () => undefined);
|
|
18
|
+
return next;
|
|
19
|
+
}
|
|
20
|
+
export async function syncSelectedSkills(config, selections) {
|
|
21
|
+
return withSyncLock(() => syncSelectedSkillsNow(config, selections));
|
|
22
|
+
}
|
|
23
|
+
export async function syncSingleSkill(config, skillId) {
|
|
24
|
+
return withSyncLock(() => syncSingleSkillNow(config, skillId));
|
|
25
|
+
}
|
|
26
|
+
async function syncSingleSkillNow(config, skillId) {
|
|
27
|
+
const id = validateSkillId(skillId);
|
|
28
|
+
await requireGitRemote(config.syncRepo);
|
|
29
|
+
const cleanup = await cleanupLegacyArchiveArtifacts(config.syncRepo);
|
|
30
|
+
await gitAdd(config.syncRepo, stagePaths(config.syncRepo, [id], cleanup.removedArchiveDir));
|
|
31
|
+
const committed = await gitHasStagedChanges(config.syncRepo);
|
|
32
|
+
const commitHash = committed ? await gitCommit(config.syncRepo, buildSyncSkillCommitMessage(id)) : null;
|
|
33
|
+
await gitPush(config.syncRepo);
|
|
34
|
+
return {
|
|
35
|
+
skillIds: [id],
|
|
36
|
+
updatedRepoSkillIds: [id],
|
|
37
|
+
committed,
|
|
38
|
+
pushed: true,
|
|
39
|
+
commitHash,
|
|
40
|
+
commitMessage: buildSyncSkillCommitMessage(id),
|
|
41
|
+
gitStatus: await gitStatus(config.syncRepo)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function buildSyncSkillCommitMessage(skillId) {
|
|
45
|
+
return [
|
|
46
|
+
`Sync skill: ${skillId}`,
|
|
47
|
+
"",
|
|
48
|
+
"Synced selected skill content and repository metadata.",
|
|
49
|
+
`- ${skillId}`
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
async function syncSelectedSkillsNow(config, selections) {
|
|
53
|
+
if (selections.length === 0) {
|
|
54
|
+
return syncRepositoryChangesNow(config);
|
|
55
|
+
}
|
|
56
|
+
const prepared = await prepareSelections(config, selections);
|
|
57
|
+
await requireGitRemote(config.syncRepo);
|
|
58
|
+
const updatedRepoSkillIds = [];
|
|
59
|
+
for (const skill of prepared) {
|
|
60
|
+
if (!skill.updateRepoFromLocal) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
await importLocalSkill(config, skill.id, { force: true, source: skill.source });
|
|
64
|
+
updatedRepoSkillIds.push(skill.id);
|
|
65
|
+
}
|
|
66
|
+
const skillIds = prepared.map((skill) => skill.id);
|
|
67
|
+
const cleanup = await cleanupLegacyArchiveArtifacts(config.syncRepo);
|
|
68
|
+
await gitAdd(config.syncRepo, stagePaths(config.syncRepo, skillIds, cleanup.removedArchiveDir));
|
|
69
|
+
const commitMessage = buildCommitMessage(skillIds, updatedRepoSkillIds);
|
|
70
|
+
const committed = await gitHasStagedChanges(config.syncRepo);
|
|
71
|
+
const commitHash = committed ? await gitCommit(config.syncRepo, commitMessage) : null;
|
|
72
|
+
await gitPush(config.syncRepo);
|
|
73
|
+
return {
|
|
74
|
+
skillIds,
|
|
75
|
+
updatedRepoSkillIds,
|
|
76
|
+
committed,
|
|
77
|
+
pushed: true,
|
|
78
|
+
commitHash,
|
|
79
|
+
commitMessage,
|
|
80
|
+
gitStatus: await gitStatus(config.syncRepo)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export async function syncRepositoryChanges(config) {
|
|
84
|
+
return withSyncLock(() => syncRepositoryChangesNow(config));
|
|
85
|
+
}
|
|
86
|
+
export async function pullRepositoryChanges(config) {
|
|
87
|
+
return withSyncLock(() => pullRepositoryChangesNow(config));
|
|
88
|
+
}
|
|
89
|
+
export async function listRepositoryConflicts(config) {
|
|
90
|
+
return withSyncLock(() => listRepositoryConflictsNow(config));
|
|
91
|
+
}
|
|
92
|
+
export async function resolveRepositoryConflicts(config, resolutions) {
|
|
93
|
+
return withSyncLock(() => resolveRepositoryConflictsNow(config, resolutions));
|
|
94
|
+
}
|
|
95
|
+
async function pullRepositoryChangesNow(config) {
|
|
96
|
+
const statusBeforePull = await gitStatus(config.syncRepo);
|
|
97
|
+
const preSync = statusBeforePull.trim() ? await syncRepositoryChangesNow(config) : null;
|
|
98
|
+
const remainingStatus = await gitStatus(config.syncRepo);
|
|
99
|
+
if (remainingStatus.trim()) {
|
|
100
|
+
throw new Error(`Cannot pull: sync repo has local uncommitted changes.\n${remainingStatus}`);
|
|
101
|
+
}
|
|
102
|
+
const branchStatus = await gitBranchSyncStatus(config.syncRepo, { fetch: true });
|
|
103
|
+
if (branchStatus.state === "behind") {
|
|
104
|
+
await gitPull(config.syncRepo);
|
|
105
|
+
await syncRepositoryChangesNow(config);
|
|
106
|
+
}
|
|
107
|
+
else if (branchStatus.state === "ahead") {
|
|
108
|
+
await gitPush(config.syncRepo);
|
|
109
|
+
}
|
|
110
|
+
else if (branchStatus.state === "diverged") {
|
|
111
|
+
if (!branchStatus.upstream) {
|
|
112
|
+
throw new Error("Cannot resolve sync conflict because no upstream branch is configured.");
|
|
113
|
+
}
|
|
114
|
+
await mergeRemoteRepositoryChanges(config, branchStatus.upstream);
|
|
115
|
+
}
|
|
116
|
+
else if (branchStatus.state === "no-upstream" || branchStatus.state === "unknown") {
|
|
117
|
+
await gitPull(config.syncRepo);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
pulled: true,
|
|
121
|
+
preSync,
|
|
122
|
+
gitStatus: await gitStatus(config.syncRepo),
|
|
123
|
+
gitBranchStatus: await gitBranchSyncStatus(config.syncRepo)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async function listRepositoryConflictsNow(config) {
|
|
127
|
+
const gitBranchStatus = await gitBranchSyncStatus(config.syncRepo, { fetch: true });
|
|
128
|
+
if (gitBranchStatus.state !== "diverged" || !gitBranchStatus.upstream) {
|
|
129
|
+
return { gitBranchStatus, conflicts: [] };
|
|
130
|
+
}
|
|
131
|
+
const conflictPaths = await previewMergeConflictPaths(config.syncRepo, gitBranchStatus.upstream);
|
|
132
|
+
const conflicts = await buildRepositorySkillConflicts(config, gitBranchStatus.upstream, conflictPaths);
|
|
133
|
+
return { gitBranchStatus, conflicts };
|
|
134
|
+
}
|
|
135
|
+
async function resolveRepositoryConflictsNow(config, resolutions) {
|
|
136
|
+
const normalizedResolutions = normalizeRepositoryConflictResolutions(resolutions);
|
|
137
|
+
const statusBeforePull = await gitStatus(config.syncRepo);
|
|
138
|
+
const preSync = statusBeforePull.trim() ? await syncRepositoryChangesNow(config) : null;
|
|
139
|
+
const remainingStatus = await gitStatus(config.syncRepo);
|
|
140
|
+
if (remainingStatus.trim()) {
|
|
141
|
+
throw new Error(`Cannot resolve repository conflicts: sync repo has local uncommitted changes.\n${remainingStatus}`);
|
|
142
|
+
}
|
|
143
|
+
const gitBranchStatus = await gitBranchSyncStatus(config.syncRepo, { fetch: true });
|
|
144
|
+
if (gitBranchStatus.state !== "diverged" || !gitBranchStatus.upstream) {
|
|
145
|
+
throw new Error("No repository conflict is waiting for resolution.");
|
|
146
|
+
}
|
|
147
|
+
const detected = await buildRepositorySkillConflicts(config, gitBranchStatus.upstream, await previewMergeConflictPaths(config.syncRepo, gitBranchStatus.upstream));
|
|
148
|
+
const detectedSkillIds = new Set(detected.map((conflict) => conflict.skillId));
|
|
149
|
+
const missingSelections = [...detectedSkillIds].filter((skillId) => !normalizedResolutions.has(skillId));
|
|
150
|
+
if (missingSelections.length > 0) {
|
|
151
|
+
throw new Error(`Choose a version for every conflicted skill: ${missingSelections.join(", ")}.`);
|
|
152
|
+
}
|
|
153
|
+
const localMetadata = await readSkillsMetadataAtRef(config.syncRepo, "HEAD");
|
|
154
|
+
const remoteMetadata = await readSkillsMetadataAtRef(config.syncRepo, gitBranchStatus.upstream);
|
|
155
|
+
const mergedMetadata = mergeSkillsMetadata(localMetadata, remoteMetadata);
|
|
156
|
+
const usageEvents = mergeJsonl(await readTextAtRef(config.syncRepo, gitBranchStatus.upstream, "metadata/usage-events.jsonl"), await readTextAtRef(config.syncRepo, "HEAD", "metadata/usage-events.jsonl"));
|
|
157
|
+
const materializedSources = await materializeRepositoryResolutionSources(config, gitBranchStatus.upstream, [...normalizedResolutions.values()]);
|
|
158
|
+
try {
|
|
159
|
+
try {
|
|
160
|
+
await runGit(config.syncRepo, ["merge", "--no-ff", "--no-commit", gitBranchStatus.upstream]);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (!(await isMergeInProgress(config.syncRepo))) {
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const statusAfterMerge = await gitStatus(config.syncRepo);
|
|
168
|
+
const unresolvedPaths = conflictedPaths(statusAfterMerge);
|
|
169
|
+
const unhandledConflicts = unresolvedPaths.filter((filePath) => {
|
|
170
|
+
if (isAutoResolvableRepositoryConflict(filePath)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
const skillId = skillIdFromConflictPath(filePath, detectedSkillIds);
|
|
174
|
+
return !skillId || !normalizedResolutions.has(skillId);
|
|
175
|
+
});
|
|
176
|
+
if (unhandledConflicts.length > 0) {
|
|
177
|
+
await abortMerge(config.syncRepo);
|
|
178
|
+
throw new Error(`Cannot resolve repository conflict. Review these paths manually: ${unhandledConflicts.join(", ")}.`);
|
|
179
|
+
}
|
|
180
|
+
await writeSkillsMetadata(config.syncRepo, mergedMetadata);
|
|
181
|
+
await writeFile(repoUsageEventsPath(config.syncRepo), usageEvents, "utf8");
|
|
182
|
+
for (const resolution of normalizedResolutions.values()) {
|
|
183
|
+
await applyRepositoryConflictResolution(config, resolution, materializedSources, mergedMetadata);
|
|
184
|
+
}
|
|
185
|
+
await writeSkillsMetadata(config.syncRepo, mergedMetadata);
|
|
186
|
+
const cleanup = await cleanupLegacyArchiveArtifacts(config.syncRepo);
|
|
187
|
+
await gitAdd(config.syncRepo, [".gitignore", "metadata", "skills", ...(cleanup.removedArchiveDir ? ["archive"] : [])]);
|
|
188
|
+
const committed = await gitHasStagedChanges(config.syncRepo);
|
|
189
|
+
if (committed) {
|
|
190
|
+
await gitCommit(config.syncRepo, buildRepositoryConflictResolutionCommitMessage([...normalizedResolutions.values()]));
|
|
191
|
+
}
|
|
192
|
+
await gitPush(config.syncRepo);
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
await cleanupMaterializedSources(config.syncRepo, materializedSources);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
pulled: true,
|
|
199
|
+
preSync,
|
|
200
|
+
gitStatus: await gitStatus(config.syncRepo),
|
|
201
|
+
gitBranchStatus: await gitBranchSyncStatus(config.syncRepo)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async function mergeRemoteRepositoryChanges(config, upstream) {
|
|
205
|
+
const localMetadata = await readSkillsMetadataAtRef(config.syncRepo, "HEAD");
|
|
206
|
+
const remoteMetadata = await readSkillsMetadataAtRef(config.syncRepo, upstream);
|
|
207
|
+
const usageEvents = mergeJsonl(await readTextAtRef(config.syncRepo, upstream, "metadata/usage-events.jsonl"), await readTextAtRef(config.syncRepo, "HEAD", "metadata/usage-events.jsonl"));
|
|
208
|
+
try {
|
|
209
|
+
await runGit(config.syncRepo, ["merge", "--no-ff", "--no-commit", upstream]);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (!(await isMergeInProgress(config.syncRepo))) {
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const statusAfterMerge = await gitStatus(config.syncRepo);
|
|
217
|
+
const unhandledConflicts = conflictedPaths(statusAfterMerge).filter((filePath) => !isAutoResolvableRepositoryConflict(filePath));
|
|
218
|
+
if (unhandledConflicts.length > 0) {
|
|
219
|
+
await abortMerge(config.syncRepo);
|
|
220
|
+
throw new Error(`Cannot auto-resolve sync conflict. Review these paths manually: ${unhandledConflicts.join(", ")}.`);
|
|
221
|
+
}
|
|
222
|
+
await writeSkillsMetadata(config.syncRepo, mergeSkillsMetadata(localMetadata, remoteMetadata));
|
|
223
|
+
await writeFile(repoUsageEventsPath(config.syncRepo), usageEvents, "utf8");
|
|
224
|
+
const cleanup = await cleanupLegacyArchiveArtifacts(config.syncRepo);
|
|
225
|
+
await gitAdd(config.syncRepo, [".gitignore", "metadata", "skills", ...(cleanup.removedArchiveDir ? ["archive"] : [])]);
|
|
226
|
+
const committed = await gitHasStagedChanges(config.syncRepo);
|
|
227
|
+
if (committed) {
|
|
228
|
+
await gitCommit(config.syncRepo, [
|
|
229
|
+
"Merge remote sync repository changes",
|
|
230
|
+
"",
|
|
231
|
+
"Merged remote skill updates, usage records, and repository metadata.",
|
|
232
|
+
"Removed legacy skill archive artifacts."
|
|
233
|
+
].join("\n"));
|
|
234
|
+
}
|
|
235
|
+
await gitPush(config.syncRepo);
|
|
236
|
+
}
|
|
237
|
+
async function syncRepositoryChangesNow(config) {
|
|
238
|
+
await requireGitRemote(config.syncRepo);
|
|
239
|
+
const cleanup = await cleanupLegacyArchiveArtifacts(config.syncRepo);
|
|
240
|
+
const statusBeforeStage = await gitStatus(config.syncRepo);
|
|
241
|
+
await gitAdd(config.syncRepo, [".gitignore", "metadata", "skills", ...(cleanup.removedArchiveDir ? ["archive"] : [])]);
|
|
242
|
+
const commitMessage = buildRepositoryCommitMessage(statusBeforeStage);
|
|
243
|
+
const committed = await gitHasStagedChanges(config.syncRepo);
|
|
244
|
+
const commitHash = committed ? await gitCommit(config.syncRepo, commitMessage) : null;
|
|
245
|
+
await gitPush(config.syncRepo);
|
|
246
|
+
return {
|
|
247
|
+
skillIds: [],
|
|
248
|
+
updatedRepoSkillIds: [],
|
|
249
|
+
committed,
|
|
250
|
+
pushed: true,
|
|
251
|
+
commitHash,
|
|
252
|
+
commitMessage,
|
|
253
|
+
gitStatus: await gitStatus(config.syncRepo)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
async function prepareSelections(config, selections) {
|
|
257
|
+
if (selections.length === 0) {
|
|
258
|
+
throw new Error("Select at least one skill before syncing.");
|
|
259
|
+
}
|
|
260
|
+
const normalized = normalizeSelections(selections);
|
|
261
|
+
const report = await buildStatusReport(config);
|
|
262
|
+
const managedById = new Map(report.managed.map((skill) => [skill.id, skill]));
|
|
263
|
+
const unmanagedByKey = new Map(report.unmanagedLocal.map((skill) => [selectionKey(skill.id, skill.source), skill]));
|
|
264
|
+
const repoOnlyIds = new Set(report.repoOnly.map((skill) => skill.id));
|
|
265
|
+
const blocked = [];
|
|
266
|
+
const prepared = [];
|
|
267
|
+
for (const selection of normalized) {
|
|
268
|
+
const managed = managedById.get(selection.skillId);
|
|
269
|
+
if (managed) {
|
|
270
|
+
const source = selection.source ?? managed.localSource ?? undefined;
|
|
271
|
+
const state = managed.syncState;
|
|
272
|
+
if (state === "clean") {
|
|
273
|
+
prepared.push({ id: managed.id, source, state, updateRepoFromLocal: false });
|
|
274
|
+
}
|
|
275
|
+
else if (state === "local_modified" || state === "missing_repo") {
|
|
276
|
+
prepared.push({ id: managed.id, source, state, updateRepoFromLocal: true });
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
blocked.push(`${managed.id} is ${state}`);
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const unmanaged = selection.source ? unmanagedByKey.get(selectionKey(selection.skillId, selection.source)) : findUnmanagedById(report.unmanagedLocal, selection.skillId);
|
|
284
|
+
if (unmanaged) {
|
|
285
|
+
prepared.push({
|
|
286
|
+
id: unmanaged.id,
|
|
287
|
+
source: unmanaged.source === "codex" || unmanaged.source === "agents" ? unmanaged.source : undefined,
|
|
288
|
+
state: "unmanaged",
|
|
289
|
+
updateRepoFromLocal: true
|
|
290
|
+
});
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (repoOnlyIds.has(selection.skillId)) {
|
|
294
|
+
blocked.push(`${selection.skillId} exists only in the sync repo`);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
blocked.push(`${selection.skillId} was not found`);
|
|
298
|
+
}
|
|
299
|
+
if (blocked.length > 0) {
|
|
300
|
+
throw new Error(`Cannot sync selected skills: ${blocked.join("; ")}.`);
|
|
301
|
+
}
|
|
302
|
+
return prepared;
|
|
303
|
+
}
|
|
304
|
+
async function requireGitRemote(syncRepo) {
|
|
305
|
+
const remotes = await gitRemotes(syncRepo);
|
|
306
|
+
if (remotes.length === 0) {
|
|
307
|
+
throw new Error("No Git remote is configured for the sync repository. Add a remote before syncing.");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function findUnmanagedById(skills, skillId) {
|
|
311
|
+
const matches = skills.filter((skill) => skill.id === skillId);
|
|
312
|
+
if (matches.length > 1) {
|
|
313
|
+
throw new Error(`Select a source for ${skillId}; it exists in multiple local skill roots.`);
|
|
314
|
+
}
|
|
315
|
+
return matches[0];
|
|
316
|
+
}
|
|
317
|
+
function normalizeSelections(selections) {
|
|
318
|
+
const byId = new Map();
|
|
319
|
+
for (const selection of selections) {
|
|
320
|
+
const skillId = validateSkillId(selection.skillId);
|
|
321
|
+
if (selection.source && selection.source !== "codex" && selection.source !== "agents") {
|
|
322
|
+
throw new Error("Skill source must be codex or agents.");
|
|
323
|
+
}
|
|
324
|
+
const existing = byId.get(skillId);
|
|
325
|
+
if (existing && existing.source !== selection.source) {
|
|
326
|
+
throw new Error(`Select only one local source for ${skillId}.`);
|
|
327
|
+
}
|
|
328
|
+
byId.set(skillId, { skillId, source: selection.source });
|
|
329
|
+
}
|
|
330
|
+
return [...byId.values()].sort((a, b) => a.skillId.localeCompare(b.skillId));
|
|
331
|
+
}
|
|
332
|
+
function stagePaths(syncRepo, skillIds, includeLegacyArchiveDeletion = false) {
|
|
333
|
+
const paths = new Set([
|
|
334
|
+
".gitignore",
|
|
335
|
+
relativeToRepo(syncRepo, repoSettingsPath(syncRepo)),
|
|
336
|
+
relativeToRepo(syncRepo, repoSkillsMetadataPath(syncRepo)),
|
|
337
|
+
relativeToRepo(syncRepo, repoUsageEventsPath(syncRepo))
|
|
338
|
+
]);
|
|
339
|
+
for (const skillId of skillIds) {
|
|
340
|
+
paths.add(path.join("skills", ...skillId.split("/")));
|
|
341
|
+
}
|
|
342
|
+
if (includeLegacyArchiveDeletion) {
|
|
343
|
+
paths.add("archive");
|
|
344
|
+
}
|
|
345
|
+
return [...paths];
|
|
346
|
+
}
|
|
347
|
+
function relativeToRepo(syncRepo, targetPath) {
|
|
348
|
+
return path.relative(syncRepo, targetPath).split(path.sep).join("/");
|
|
349
|
+
}
|
|
350
|
+
function buildCommitMessage(skillIds, updatedRepoSkillIds) {
|
|
351
|
+
const subject = skillIds.length === 1 ? `Sync skill: ${skillIds[0]}` : `Sync ${skillIds.length} skills: ${skillIds.slice(0, 3).join(", ")}${skillIds.length > 3 ? ", ..." : ""}`;
|
|
352
|
+
const lines = [
|
|
353
|
+
subject,
|
|
354
|
+
"",
|
|
355
|
+
"Synced skills:",
|
|
356
|
+
...skillIds.map((skillId) => `- ${skillId}`),
|
|
357
|
+
"",
|
|
358
|
+
updatedRepoSkillIds.length > 0 ? "Updated repo copies from local skills." : "No local skill content changed; pushed existing sync repo state."
|
|
359
|
+
];
|
|
360
|
+
return lines.join("\n");
|
|
361
|
+
}
|
|
362
|
+
function buildRepositoryCommitMessage(status) {
|
|
363
|
+
const changedLines = status
|
|
364
|
+
.split(/\r?\n/)
|
|
365
|
+
.map((line) => line.trim())
|
|
366
|
+
.filter(Boolean);
|
|
367
|
+
const lines = [
|
|
368
|
+
"Sync repository changes",
|
|
369
|
+
"",
|
|
370
|
+
"Synced repository metadata and usage records.",
|
|
371
|
+
""
|
|
372
|
+
];
|
|
373
|
+
if (changedLines.length > 0) {
|
|
374
|
+
lines.push("Git changes:");
|
|
375
|
+
lines.push(...changedLines.slice(0, 24).map((line) => `- ${line}`));
|
|
376
|
+
if (changedLines.length > 24) {
|
|
377
|
+
lines.push(`- ... ${changedLines.length - 24} more`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
lines.push("No local repository changes were detected before staging.");
|
|
382
|
+
}
|
|
383
|
+
return lines.join("\n");
|
|
384
|
+
}
|
|
385
|
+
function selectionKey(skillId, source) {
|
|
386
|
+
return `${source}:${skillId}`;
|
|
387
|
+
}
|
|
388
|
+
async function readSkillsMetadataAtRef(syncRepo, ref) {
|
|
389
|
+
const raw = await readTextAtRef(syncRepo, ref, "metadata/skills.json");
|
|
390
|
+
if (!raw.trim()) {
|
|
391
|
+
return { schemaVersion: 1, skills: [] };
|
|
392
|
+
}
|
|
393
|
+
const parsed = JSON.parse(raw);
|
|
394
|
+
if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.skills)) {
|
|
395
|
+
return { schemaVersion: 1, skills: [] };
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
schemaVersion: 1,
|
|
399
|
+
skills: parsed.skills
|
|
400
|
+
.map(toManagedSkillRecord)
|
|
401
|
+
.filter((record) => record !== null)
|
|
402
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
async function readTextAtRef(syncRepo, ref, relativePath) {
|
|
406
|
+
try {
|
|
407
|
+
const { stdout } = await runGit(syncRepo, ["show", `${ref}:${relativePath}`]);
|
|
408
|
+
return stdout;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return "";
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function mergeSkillsMetadata(local, remote) {
|
|
415
|
+
const merged = new Map();
|
|
416
|
+
for (const record of local.skills) {
|
|
417
|
+
merged.set(record.id, record);
|
|
418
|
+
}
|
|
419
|
+
for (const record of remote.skills) {
|
|
420
|
+
const previous = merged.get(record.id);
|
|
421
|
+
merged.set(record.id, mergeSkillRecord(previous, record));
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
schemaVersion: 1,
|
|
425
|
+
skills: [...merged.values()].sort((a, b) => a.id.localeCompare(b.id))
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function mergeSkillRecord(local, remote) {
|
|
429
|
+
if (!local) {
|
|
430
|
+
return remote;
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
...remote,
|
|
434
|
+
lastUsedAt: maxIsoTimestamp(local.lastUsedAt, remote.lastUsedAt)
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function mergeJsonl(...contents) {
|
|
438
|
+
const lines = [];
|
|
439
|
+
const seen = new Set();
|
|
440
|
+
for (const content of contents) {
|
|
441
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
442
|
+
const line = rawLine.trim();
|
|
443
|
+
if (!line || seen.has(line)) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
seen.add(line);
|
|
447
|
+
lines.push(line);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
451
|
+
}
|
|
452
|
+
function toManagedSkillRecord(candidate) {
|
|
453
|
+
if (!candidate || typeof candidate !== "object") {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
const record = candidate;
|
|
457
|
+
if (record.status === "archived" || typeof record.id !== "string") {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const { archivedAt: _archivedAt, ...cleaned } = record;
|
|
461
|
+
return {
|
|
462
|
+
...cleaned,
|
|
463
|
+
status: "managed",
|
|
464
|
+
lastUsedAt: typeof cleaned.lastUsedAt === "string" ? cleaned.lastUsedAt : null
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function maxIsoTimestamp(left, right) {
|
|
468
|
+
if (!left) {
|
|
469
|
+
return right ?? null;
|
|
470
|
+
}
|
|
471
|
+
if (!right) {
|
|
472
|
+
return left;
|
|
473
|
+
}
|
|
474
|
+
return new Date(left).getTime() >= new Date(right).getTime() ? left : right;
|
|
475
|
+
}
|
|
476
|
+
function conflictedPaths(status) {
|
|
477
|
+
return status
|
|
478
|
+
.split(/\r?\n/)
|
|
479
|
+
.map((line) => line.trimEnd())
|
|
480
|
+
.filter((line) => isConflictStatus(line.slice(0, 2)))
|
|
481
|
+
.map((line) => line.slice(3).trim())
|
|
482
|
+
.filter(Boolean);
|
|
483
|
+
}
|
|
484
|
+
function isConflictStatus(status) {
|
|
485
|
+
return status.includes("U") || status === "AA" || status === "DD";
|
|
486
|
+
}
|
|
487
|
+
function isAutoResolvableRepositoryConflict(filePath) {
|
|
488
|
+
return filePath === "metadata/skills.json" || filePath === "metadata/usage-events.jsonl" || filePath.startsWith("archive/");
|
|
489
|
+
}
|
|
490
|
+
async function isMergeInProgress(syncRepo) {
|
|
491
|
+
try {
|
|
492
|
+
await runGit(syncRepo, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function abortMerge(syncRepo) {
|
|
500
|
+
try {
|
|
501
|
+
await runGit(syncRepo, ["merge", "--abort"]);
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// Best effort. The next git command will surface any remaining repository state.
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function normalizeRepositoryConflictResolutions(resolutions) {
|
|
508
|
+
if (resolutions.length === 0) {
|
|
509
|
+
throw new Error("Choose at least one skill version before resolving repository conflicts.");
|
|
510
|
+
}
|
|
511
|
+
const normalized = new Map();
|
|
512
|
+
for (const resolution of resolutions) {
|
|
513
|
+
const skillId = validateSkillId(resolution.skillId);
|
|
514
|
+
if (!isRepositoryConflictSource(resolution.source)) {
|
|
515
|
+
throw new Error("Repository conflict source must be github, syncRepo, codex, or agents.");
|
|
516
|
+
}
|
|
517
|
+
normalized.set(skillId, { skillId, source: resolution.source });
|
|
518
|
+
}
|
|
519
|
+
return normalized;
|
|
520
|
+
}
|
|
521
|
+
function isRepositoryConflictSource(value) {
|
|
522
|
+
return value === "github" || value === "syncRepo" || value === "codex" || value === "agents";
|
|
523
|
+
}
|
|
524
|
+
async function previewMergeConflictPaths(syncRepo, upstream) {
|
|
525
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "csm-merge-preview-"));
|
|
526
|
+
try {
|
|
527
|
+
await runGit(syncRepo, ["worktree", "add", "--detach", tempDir, "HEAD"]);
|
|
528
|
+
try {
|
|
529
|
+
await runGit(tempDir, ["merge", "--no-ff", "--no-commit", upstream]);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// Conflicts are expected; inspect the preview worktree status below.
|
|
533
|
+
}
|
|
534
|
+
return conflictedPaths(await gitStatus(tempDir));
|
|
535
|
+
}
|
|
536
|
+
finally {
|
|
537
|
+
await removeWorktree(syncRepo, tempDir);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function buildRepositorySkillConflicts(config, upstream, conflictPaths) {
|
|
541
|
+
const grouped = new Map();
|
|
542
|
+
for (const conflictPath of conflictPaths) {
|
|
543
|
+
const skillId = await detectSkillIdForConflictPath(config.syncRepo, upstream, conflictPath);
|
|
544
|
+
if (!skillId) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
grouped.set(skillId, [...(grouped.get(skillId) ?? []), conflictPath]);
|
|
548
|
+
}
|
|
549
|
+
const conflicts = [];
|
|
550
|
+
for (const [skillId, files] of [...grouped.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
551
|
+
conflicts.push({
|
|
552
|
+
skillId,
|
|
553
|
+
files: files.sort(),
|
|
554
|
+
versions: await buildRepositoryConflictVersions(config, upstream, skillId)
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
return conflicts;
|
|
558
|
+
}
|
|
559
|
+
async function detectSkillIdForConflictPath(syncRepo, upstream, conflictPath) {
|
|
560
|
+
const normalized = conflictPath.split(path.sep).join("/");
|
|
561
|
+
if (!normalized.startsWith("skills/")) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
const parts = normalized.slice("skills/".length).split("/").filter(Boolean);
|
|
565
|
+
for (let length = parts.length; length >= 1; length -= 1) {
|
|
566
|
+
const candidate = parts.slice(0, length).join("/");
|
|
567
|
+
if (await skillExistsAtRef(syncRepo, "HEAD", candidate) || (await skillExistsAtRef(syncRepo, upstream, candidate))) {
|
|
568
|
+
return candidate;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return parts[0] ?? null;
|
|
572
|
+
}
|
|
573
|
+
function skillIdFromConflictPath(conflictPath, skillIds) {
|
|
574
|
+
const normalized = conflictPath.split(path.sep).join("/");
|
|
575
|
+
if (!normalized.startsWith("skills/")) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
const relative = normalized.slice("skills/".length);
|
|
579
|
+
const matches = [...skillIds].filter((skillId) => relative === skillId || relative.startsWith(`${skillId}/`));
|
|
580
|
+
return matches.sort((a, b) => b.length - a.length)[0] ?? null;
|
|
581
|
+
}
|
|
582
|
+
async function skillExistsAtRef(syncRepo, ref, skillId) {
|
|
583
|
+
return (await readTextAtRef(syncRepo, ref, path.join("skills", ...skillId.split("/"), "SKILL.md").split(path.sep).join("/"))).trim().length > 0;
|
|
584
|
+
}
|
|
585
|
+
async function buildRepositoryConflictVersions(config, upstream, skillId) {
|
|
586
|
+
const syncRepoPath = path.join("skills", ...skillId.split("/"), "SKILL.md").split(path.sep).join("/");
|
|
587
|
+
const codexPath = resolveSkillPath(config.codexSkillsDir, skillId);
|
|
588
|
+
const agentsPath = resolveSkillPath(config.agentsSkillsDir, skillId);
|
|
589
|
+
const codexSkillMdPath = path.join(codexPath, "SKILL.md");
|
|
590
|
+
const agentsSkillMdPath = path.join(agentsPath, "SKILL.md");
|
|
591
|
+
const githubContent = await readTextAtRef(config.syncRepo, upstream, syncRepoPath);
|
|
592
|
+
const syncRepoContent = await readTextAtRef(config.syncRepo, "HEAD", syncRepoPath);
|
|
593
|
+
return [
|
|
594
|
+
{
|
|
595
|
+
source: "github",
|
|
596
|
+
label: "GitHub version",
|
|
597
|
+
path: `${upstream}:${syncRepoPath}`,
|
|
598
|
+
exists: githubContent.trim().length > 0,
|
|
599
|
+
content: githubContent.trim().length > 0 ? githubContent : null
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
source: "syncRepo",
|
|
603
|
+
label: "Sync repo local version",
|
|
604
|
+
path: `HEAD:${syncRepoPath}`,
|
|
605
|
+
exists: syncRepoContent.trim().length > 0,
|
|
606
|
+
content: syncRepoContent.trim().length > 0 ? syncRepoContent : null
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
source: "codex",
|
|
610
|
+
label: "Codex installed copy",
|
|
611
|
+
path: codexSkillMdPath,
|
|
612
|
+
exists: existsSync(codexSkillMdPath),
|
|
613
|
+
content: existsSync(codexSkillMdPath) ? await readFile(codexSkillMdPath, "utf8") : null
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
source: "agents",
|
|
617
|
+
label: "Agents installed copy",
|
|
618
|
+
path: agentsSkillMdPath,
|
|
619
|
+
exists: existsSync(agentsSkillMdPath),
|
|
620
|
+
content: existsSync(agentsSkillMdPath) ? await readFile(agentsSkillMdPath, "utf8") : null
|
|
621
|
+
}
|
|
622
|
+
];
|
|
623
|
+
}
|
|
624
|
+
async function materializeRepositoryResolutionSources(config, upstream, resolutions) {
|
|
625
|
+
const materialized = { worktrees: [], github: null, syncRepo: null };
|
|
626
|
+
const sources = new Set(resolutions.map((resolution) => resolution.source));
|
|
627
|
+
if (sources.has("github")) {
|
|
628
|
+
materialized.github = await addDetachedWorktree(config.syncRepo, upstream);
|
|
629
|
+
materialized.worktrees.push(materialized.github);
|
|
630
|
+
}
|
|
631
|
+
if (sources.has("syncRepo")) {
|
|
632
|
+
materialized.syncRepo = await addDetachedWorktree(config.syncRepo, "HEAD");
|
|
633
|
+
materialized.worktrees.push(materialized.syncRepo);
|
|
634
|
+
}
|
|
635
|
+
return materialized;
|
|
636
|
+
}
|
|
637
|
+
async function addDetachedWorktree(syncRepo, ref) {
|
|
638
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "csm-conflict-source-"));
|
|
639
|
+
await runGit(syncRepo, ["worktree", "add", "--detach", tempDir, ref]);
|
|
640
|
+
return tempDir;
|
|
641
|
+
}
|
|
642
|
+
async function cleanupMaterializedSources(syncRepo, materialized) {
|
|
643
|
+
for (const worktree of materialized.worktrees) {
|
|
644
|
+
await removeWorktree(syncRepo, worktree);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async function removeWorktree(syncRepo, worktreePath) {
|
|
648
|
+
try {
|
|
649
|
+
await runGit(syncRepo, ["worktree", "remove", "--force", worktreePath]);
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
await rm(worktreePath, { recursive: true, force: true });
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async function applyRepositoryConflictResolution(config, resolution, materializedSources, metadata) {
|
|
656
|
+
const sourcePath = sourcePathForRepositoryResolution(config, resolution, materializedSources);
|
|
657
|
+
if (!sourcePath || !existsSync(path.join(sourcePath, "SKILL.md"))) {
|
|
658
|
+
throw new Error(`Cannot use ${resolution.source} for ${resolution.skillId}: source copy is missing.`);
|
|
659
|
+
}
|
|
660
|
+
const repoPath = path.join(repoSkillsDir(config.syncRepo), ...resolution.skillId.split("/"));
|
|
661
|
+
await replaceSkillDirectory(repoPath, sourcePath);
|
|
662
|
+
const localSources = collectInstalledSources(config, resolution.skillId);
|
|
663
|
+
for (const source of localSources) {
|
|
664
|
+
await replaceSkillDirectory(resolveSkillPath(source === "codex" ? config.codexSkillsDir : config.agentsSkillsDir, resolution.skillId), repoPath);
|
|
665
|
+
}
|
|
666
|
+
const repoHash = await hashDirectory(repoPath);
|
|
667
|
+
const currentLocalHash = await hashInstalledSources(config, resolution.skillId, localSources);
|
|
668
|
+
const frontmatter = await readSkillFrontmatter(repoPath);
|
|
669
|
+
const index = metadata.skills.findIndex((record) => record.id === resolution.skillId);
|
|
670
|
+
const existing = index >= 0 ? metadata.skills[index] : null;
|
|
671
|
+
const updated = {
|
|
672
|
+
...(existing ?? {
|
|
673
|
+
id: resolution.skillId,
|
|
674
|
+
createdAt: new Date().toISOString(),
|
|
675
|
+
lastUsedAt: null
|
|
676
|
+
}),
|
|
677
|
+
id: resolution.skillId,
|
|
678
|
+
name: frontmatter.name,
|
|
679
|
+
description: frontmatter.description,
|
|
680
|
+
status: "managed",
|
|
681
|
+
localSource: localSources.includes("codex") ? "codex" : localSources.includes("agents") ? "agents" : null,
|
|
682
|
+
localSources,
|
|
683
|
+
localCopiesDiffer: false,
|
|
684
|
+
installed: localSources.length > 0,
|
|
685
|
+
syncState: deriveSyncState(repoHash, currentLocalHash, repoHash),
|
|
686
|
+
lastSyncedHash: repoHash,
|
|
687
|
+
currentRepoHash: repoHash,
|
|
688
|
+
currentLocalHash,
|
|
689
|
+
updatedAt: new Date().toISOString()
|
|
690
|
+
};
|
|
691
|
+
if (index >= 0) {
|
|
692
|
+
metadata.skills[index] = updated;
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
metadata.skills.push(updated);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function sourcePathForRepositoryResolution(config, resolution, materializedSources) {
|
|
699
|
+
if (resolution.source === "github") {
|
|
700
|
+
return materializedSources.github ? path.join(materializedSources.github, "skills", ...resolution.skillId.split("/")) : null;
|
|
701
|
+
}
|
|
702
|
+
if (resolution.source === "syncRepo") {
|
|
703
|
+
return materializedSources.syncRepo ? path.join(materializedSources.syncRepo, "skills", ...resolution.skillId.split("/")) : null;
|
|
704
|
+
}
|
|
705
|
+
return resolveSkillPath(resolution.source === "codex" ? config.codexSkillsDir : config.agentsSkillsDir, resolution.skillId);
|
|
706
|
+
}
|
|
707
|
+
function collectInstalledSources(config, skillId) {
|
|
708
|
+
const sources = [];
|
|
709
|
+
if (existsSync(path.join(resolveSkillPath(config.codexSkillsDir, skillId), "SKILL.md"))) {
|
|
710
|
+
sources.push("codex");
|
|
711
|
+
}
|
|
712
|
+
if (existsSync(path.join(resolveSkillPath(config.agentsSkillsDir, skillId), "SKILL.md"))) {
|
|
713
|
+
sources.push("agents");
|
|
714
|
+
}
|
|
715
|
+
return sources;
|
|
716
|
+
}
|
|
717
|
+
async function hashInstalledSources(config, skillId, sources) {
|
|
718
|
+
if (sources.length === 0) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
const hashes = await Promise.all(sources.map((source) => hashDirectory(resolveSkillPath(source === "codex" ? config.codexSkillsDir : config.agentsSkillsDir, skillId))));
|
|
722
|
+
const unique = [...new Set(hashes)];
|
|
723
|
+
return unique.length === 1 ? unique[0] ?? null : null;
|
|
724
|
+
}
|
|
725
|
+
async function replaceSkillDirectory(targetPath, sourcePath) {
|
|
726
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
727
|
+
await copySkillDirectory(sourcePath, targetPath);
|
|
728
|
+
}
|
|
729
|
+
function buildRepositoryConflictResolutionCommitMessage(resolutions) {
|
|
730
|
+
const skillIds = resolutions.map((resolution) => resolution.skillId).sort();
|
|
731
|
+
return [
|
|
732
|
+
skillIds.length === 1 ? `Resolve repo conflict: ${skillIds[0]}` : `Resolve ${skillIds.length} repo skill conflicts`,
|
|
733
|
+
"",
|
|
734
|
+
"Resolved Git repository skill conflicts by selecting canonical skill versions.",
|
|
735
|
+
...skillIds.map((skillId) => {
|
|
736
|
+
const resolution = resolutions.find((candidate) => candidate.skillId === skillId);
|
|
737
|
+
return `- ${skillId}: ${resolution?.source ?? "unknown"}`;
|
|
738
|
+
})
|
|
739
|
+
].join("\n");
|
|
740
|
+
}
|
|
741
|
+
//# sourceMappingURL=sync.js.map
|