@klhapp/skillmux 1.11.2 → 1.13.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/CHANGELOG.md +15 -0
- package/README.md +20 -10
- package/bin/skillmux.js +89 -0
- package/docs/README.md +4 -4
- package/docs/cli.md +15 -5
- package/docs/concepts.md +4 -3
- package/docs/configuration.md +22 -5
- package/docs/deployment.md +3 -3
- package/docs/getting-started.md +32 -7
- package/docs/releasing.md +17 -2
- package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
- package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
- package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
- package/docs/sdd/cli-remote-target-parity/think.md +183 -0
- package/docs/sdd/cli-surface-consistency/think.md +149 -0
- package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
- package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
- package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
- package/docs/sdd/feat-packaging-deployment/review.md +95 -0
- package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
- package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
- package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
- package/docs/sdd/npm-native-binaries/review.md +103 -0
- package/docs/sdd/npm-native-binaries/spec.md +148 -0
- package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
- package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
- package/docs/sdd/runtime-resource-hardening/review.md +86 -0
- package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
- package/docs/sdd/security-hardening-phase/think.md +129 -0
- package/docs/sdd/security-observability/review.md +96 -0
- package/docs/sdd/security-observability/spec.md +55 -0
- package/docs/sdd/skill-provenance-update/review.md +152 -0
- package/docs/sdd/skill-provenance-update/schema.json +193 -0
- package/docs/sdd/skill-provenance-update/spec.md +134 -0
- package/docs/sdd/supply-chain-hardening/review.md +50 -0
- package/docs/sdd/supply-chain-hardening/spec.md +42 -0
- package/docs/sdd/target-marker-rehome/spec.md +33 -0
- package/docs/skill-management.md +1 -1
- package/docs/troubleshooting.md +18 -2
- package/package.json +15 -8
- package/src/adapters.ts +0 -438
- package/src/audit.ts +0 -21
- package/src/cli.ts +0 -928
- package/src/clients.ts +0 -368
- package/src/commands/audit.ts +0 -85
- package/src/commands/config.ts +0 -224
- package/src/commands/context.ts +0 -104
- package/src/commands/core.ts +0 -56
- package/src/commands/doctor.ts +0 -97
- package/src/commands/eval.ts +0 -88
- package/src/commands/init.ts +0 -672
- package/src/commands/install.ts +0 -155
- package/src/commands/local-vault.ts +0 -60
- package/src/commands/models.ts +0 -10
- package/src/commands/outdated.ts +0 -115
- package/src/commands/project.ts +0 -580
- package/src/commands/report.ts +0 -66
- package/src/commands/scan.ts +0 -71
- package/src/commands/shared.ts +0 -38
- package/src/commands/skill.ts +0 -33
- package/src/commands/sync.ts +0 -233
- package/src/commands/target.ts +0 -225
- package/src/commands/update.ts +0 -277
- package/src/completions.ts +0 -196
- package/src/concurrency-limiter.ts +0 -61
- package/src/config-service.ts +0 -398
- package/src/config-watcher.ts +0 -253
- package/src/config.ts +0 -583
- package/src/context.ts +0 -188
- package/src/db-audit.ts +0 -286
- package/src/db-index.ts +0 -238
- package/src/db.ts +0 -3
- package/src/deployment.ts +0 -39
- package/src/doctor.ts +0 -205
- package/src/eval.ts +0 -302
- package/src/global-flags.ts +0 -46
- package/src/init-agents.ts +0 -329
- package/src/init-instructions.ts +0 -192
- package/src/init.ts +0 -389
- package/src/install.ts +0 -232
- package/src/lifecycle.ts +0 -51
- package/src/logger.ts +0 -26
- package/src/manifest.ts +0 -352
- package/src/mcp-registration.ts +0 -89
- package/src/metrics.ts +0 -121
- package/src/models.ts +0 -20
- package/src/output.ts +0 -214
- package/src/project-setup.ts +0 -36
- package/src/prompts.ts +0 -124
- package/src/provenance.ts +0 -99
- package/src/rate-limiter.ts +0 -137
- package/src/readiness.ts +0 -30
- package/src/redact.ts +0 -52
- package/src/router-core.ts +0 -666
- package/src/rrf.ts +0 -31
- package/src/scan.ts +0 -300
- package/src/server.ts +0 -930
- package/src/setup.ts +0 -145
- package/src/snapshot.ts +0 -135
- package/src/stats.ts +0 -340
- package/src/sync.ts +0 -482
- package/src/toml-writer.ts +0 -51
- package/src/types.ts +0 -212
- package/src/vault.ts +0 -207
package/src/sync.ts
DELETED
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
chmodSync,
|
|
3
|
-
existsSync,
|
|
4
|
-
lstatSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
readlinkSync,
|
|
8
|
-
readdirSync,
|
|
9
|
-
realpathSync,
|
|
10
|
-
renameSync,
|
|
11
|
-
rmSync,
|
|
12
|
-
symlinkSync,
|
|
13
|
-
unlinkSync,
|
|
14
|
-
writeFileSync,
|
|
15
|
-
} from "node:fs";
|
|
16
|
-
import { homedir } from "node:os";
|
|
17
|
-
import { basename, join, relative } from "node:path";
|
|
18
|
-
import { findSymlinks } from "./install";
|
|
19
|
-
import { resolveSkillRoot } from "./vault";
|
|
20
|
-
|
|
21
|
-
export const SKILLMUX_MARKER_FILENAME = ".skillmux";
|
|
22
|
-
export const LEGACY_MARKER_FILENAME = ".skr";
|
|
23
|
-
|
|
24
|
-
export interface SkillmuxMarker {
|
|
25
|
-
schema_version?: 1;
|
|
26
|
-
managed_by: "skillmux" | "skr";
|
|
27
|
-
role: "target" | "local_vault";
|
|
28
|
-
target?: string;
|
|
29
|
-
vault_path?: string;
|
|
30
|
-
managed_entries?: string[];
|
|
31
|
-
created_at: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function normalizeMarker(raw: Omit<SkillmuxMarker, "role"> & { role?: SkillmuxMarker["role"] }): SkillmuxMarker {
|
|
35
|
-
return { ...raw, role: raw.role ?? "target" };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function validateMarker(marker: SkillmuxMarker, path: string): SkillmuxMarker {
|
|
39
|
-
if (marker.schema_version === undefined) return marker;
|
|
40
|
-
if (marker.schema_version !== 1) throw new Error(`${path}: unsupported marker schema_version`);
|
|
41
|
-
if (marker.managed_by !== "skillmux") throw new Error(`${path}: invalid managed_by`);
|
|
42
|
-
if (marker.role !== "target" && marker.role !== "local_vault") {
|
|
43
|
-
throw new Error(`${path}: invalid role`);
|
|
44
|
-
}
|
|
45
|
-
if (marker.role === "target") {
|
|
46
|
-
if (typeof marker.target !== "string" || marker.target.length === 0) {
|
|
47
|
-
throw new Error(`${path}: target marker is missing target`);
|
|
48
|
-
}
|
|
49
|
-
if (typeof marker.vault_path !== "string" || marker.vault_path.length === 0) {
|
|
50
|
-
throw new Error(`${path}: target marker is missing vault_path`);
|
|
51
|
-
}
|
|
52
|
-
if (!Array.isArray(marker.managed_entries) || marker.managed_entries.some((entry) => typeof entry !== "string")) {
|
|
53
|
-
throw new Error(`${path}: target marker is missing managed_entries`);
|
|
54
|
-
}
|
|
55
|
-
} else if (typeof marker.vault_path !== "string" || marker.vault_path.length === 0) {
|
|
56
|
-
throw new Error(`${path}: local_vault marker is missing vault_path`);
|
|
57
|
-
}
|
|
58
|
-
if (typeof marker.created_at !== "string") throw new Error(`${path}: marker is missing created_at`);
|
|
59
|
-
return marker;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function readSkillmuxMarker(dir: string): SkillmuxMarker | null {
|
|
63
|
-
const newPath = join(dir, SKILLMUX_MARKER_FILENAME);
|
|
64
|
-
if (existsSync(newPath)) {
|
|
65
|
-
return validateMarker(normalizeMarker(JSON.parse(readFileSync(newPath, "utf-8"))), newPath);
|
|
66
|
-
}
|
|
67
|
-
const legacyPath = join(dir, LEGACY_MARKER_FILENAME);
|
|
68
|
-
if (existsSync(legacyPath)) {
|
|
69
|
-
return validateMarker(normalizeMarker(JSON.parse(readFileSync(legacyPath, "utf-8"))), legacyPath);
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function writeTargetMarker(
|
|
75
|
-
dir: string,
|
|
76
|
-
targetName: string,
|
|
77
|
-
vaultPath: string,
|
|
78
|
-
managedEntries: string[],
|
|
79
|
-
createdAt = new Date().toISOString(),
|
|
80
|
-
): void {
|
|
81
|
-
const marker: SkillmuxMarker = {
|
|
82
|
-
schema_version: 1,
|
|
83
|
-
managed_by: "skillmux",
|
|
84
|
-
role: "target",
|
|
85
|
-
target: targetName,
|
|
86
|
-
vault_path: vaultPath,
|
|
87
|
-
managed_entries: managedEntries,
|
|
88
|
-
created_at: createdAt,
|
|
89
|
-
};
|
|
90
|
-
const markerPath = join(dir, SKILLMUX_MARKER_FILENAME);
|
|
91
|
-
const serialized = JSON.stringify(marker, null, 2);
|
|
92
|
-
if (!existsSync(markerPath) || readFileSync(markerPath, "utf-8") !== serialized) {
|
|
93
|
-
writeFileAtomic(markerPath, serialized);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function writeFileAtomic(path: string, contents: string): void {
|
|
98
|
-
const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
99
|
-
try {
|
|
100
|
-
writeFileSync(temporaryPath, contents);
|
|
101
|
-
renameSync(temporaryPath, path);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
if (existsSync(temporaryPath)) unlinkSync(temporaryPath);
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function replaceSymlinkAtomic(path: string, target: string): void {
|
|
109
|
-
const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
110
|
-
try {
|
|
111
|
-
symlinkSync(target, temporaryPath);
|
|
112
|
-
renameSync(temporaryPath, path);
|
|
113
|
-
} catch (error) {
|
|
114
|
-
if (existsSync(temporaryPath)) unlinkSync(temporaryPath);
|
|
115
|
-
throw error;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function writeLocalVaultMarker(dir: string, vaultPath: string): void {
|
|
120
|
-
const marker: SkillmuxMarker = {
|
|
121
|
-
schema_version: 1,
|
|
122
|
-
managed_by: "skillmux",
|
|
123
|
-
role: "local_vault",
|
|
124
|
-
vault_path: vaultPath,
|
|
125
|
-
created_at: new Date().toISOString(),
|
|
126
|
-
};
|
|
127
|
-
writeFileSync(join(dir, SKILLMUX_MARKER_FILENAME), JSON.stringify(marker, null, 2));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export interface SyncTargetParams {
|
|
131
|
-
vaultPath: string;
|
|
132
|
-
targetDir: string;
|
|
133
|
-
targetName: string;
|
|
134
|
-
coreSkillIds: string[];
|
|
135
|
-
localVaultPaths?: string[];
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export interface SyncTargetResult {
|
|
139
|
-
added: string[];
|
|
140
|
-
removed: string[];
|
|
141
|
-
/** Core skills whose directory contains an internal symlink and were refused —
|
|
142
|
-
* syncTarget symlinks the whole skill directory into the target, so an internal
|
|
143
|
-
* symlink would otherwise be exposed to the agent regardless of how it got into
|
|
144
|
-
* the vault (install/update already reject them, but a shared git-backed vault
|
|
145
|
-
* can still be tampered with directly). */
|
|
146
|
-
skipped: string[];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export interface SyncTargetOptions {
|
|
150
|
-
dryRun?: boolean;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Splits `skillIds` into those safe to symlink into a target and those refused
|
|
154
|
-
* for containing an internal symlink (see SyncTargetResult.skipped). */
|
|
155
|
-
function partitionSyncable(
|
|
156
|
-
skillIds: string[],
|
|
157
|
-
skillSource: (skillId: string) => string,
|
|
158
|
-
): { syncable: string[]; skipped: string[] } {
|
|
159
|
-
const syncable: string[] = [];
|
|
160
|
-
const skipped: string[] = [];
|
|
161
|
-
for (const skillId of skillIds) {
|
|
162
|
-
if (findSymlinks(join(skillSource(skillId), skillId)).length > 0) skipped.push(skillId);
|
|
163
|
-
else syncable.push(skillId);
|
|
164
|
-
}
|
|
165
|
-
return { syncable, skipped };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function syncTarget(params: SyncTargetParams, options: SyncTargetOptions = {}): SyncTargetResult {
|
|
169
|
-
const { vaultPath, targetDir, targetName, coreSkillIds, localVaultPaths = [] } = params;
|
|
170
|
-
const { dryRun = false } = options;
|
|
171
|
-
const skillSource = (skillId: string) => resolveSkillRoot(skillId, vaultPath, localVaultPaths) ?? vaultPath;
|
|
172
|
-
|
|
173
|
-
if (!existsSync(targetDir)) {
|
|
174
|
-
const { syncable, skipped } = partitionSyncable(coreSkillIds, skillSource);
|
|
175
|
-
if (dryRun) return { added: syncable, removed: [], skipped };
|
|
176
|
-
mkdirSync(targetDir, { recursive: true });
|
|
177
|
-
for (const skillId of syncable) {
|
|
178
|
-
symlinkSync(join(skillSource(skillId), skillId), join(targetDir, skillId));
|
|
179
|
-
}
|
|
180
|
-
writeTargetMarker(targetDir, targetName, vaultPath, syncable);
|
|
181
|
-
return { added: syncable, removed: [], skipped };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
let marker = readSkillmuxMarker(targetDir);
|
|
185
|
-
if (!marker) {
|
|
186
|
-
throw new Error(`not owned by skillmux — run skillmux init`);
|
|
187
|
-
}
|
|
188
|
-
if (marker.role === "local_vault") {
|
|
189
|
-
throw new Error(`${targetDir} has a local_vault marker, not target ownership`);
|
|
190
|
-
}
|
|
191
|
-
if (marker.target !== targetName) {
|
|
192
|
-
throw new Error(`${targetDir} is owned by target "${marker.target}", not "${targetName}"`);
|
|
193
|
-
}
|
|
194
|
-
if (marker.schema_version === undefined) {
|
|
195
|
-
const legacyEntries = readdirSync(targetDir).filter(
|
|
196
|
-
(name) => name !== SKILLMUX_MARKER_FILENAME && name !== LEGACY_MARKER_FILENAME,
|
|
197
|
-
);
|
|
198
|
-
if (legacyEntries.length > 0) {
|
|
199
|
-
throw new Error(
|
|
200
|
-
`${targetDir} has a legacy marker with untracked entries; remove or migrate them before running skillmux sync`,
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
marker = {
|
|
204
|
-
schema_version: 1,
|
|
205
|
-
managed_by: "skillmux",
|
|
206
|
-
role: "target",
|
|
207
|
-
target: targetName,
|
|
208
|
-
vault_path: vaultPath,
|
|
209
|
-
managed_entries: [],
|
|
210
|
-
created_at: marker.created_at,
|
|
211
|
-
};
|
|
212
|
-
if (!dryRun) writeTargetMarker(targetDir, targetName, vaultPath, [], marker.created_at);
|
|
213
|
-
}
|
|
214
|
-
if (marker.vault_path !== vaultPath) {
|
|
215
|
-
throw new Error(
|
|
216
|
-
`${targetDir} marker recorded vault_path ${marker.vault_path}, currently configured vault_path is ${vaultPath}`,
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const desired = new Set(coreSkillIds);
|
|
221
|
-
const existing = readdirSync(targetDir).filter((name) => name !== SKILLMUX_MARKER_FILENAME && name !== LEGACY_MARKER_FILENAME);
|
|
222
|
-
const managedEntries = new Set(marker.managed_entries ?? []);
|
|
223
|
-
const collisions = coreSkillIds.filter((skillId) => existing.includes(skillId) && !managedEntries.has(skillId));
|
|
224
|
-
if (collisions.length > 0) {
|
|
225
|
-
throw new Error(`unmanaged entry collisions in ${targetDir}: ${collisions.join(", ")}`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const removed = [...managedEntries].filter((name) => existing.includes(name) && !desired.has(name));
|
|
229
|
-
const addedCandidates = coreSkillIds.filter((skillId) => !existing.includes(skillId));
|
|
230
|
-
const { syncable: added, skipped } = partitionSyncable(addedCandidates, skillSource);
|
|
231
|
-
if (dryRun) return { added, removed, skipped };
|
|
232
|
-
|
|
233
|
-
for (const name of removed) unlinkSync(join(targetDir, name));
|
|
234
|
-
for (const skillId of added) symlinkSync(join(skillSource(skillId), skillId), join(targetDir, skillId));
|
|
235
|
-
writeTargetMarker(
|
|
236
|
-
targetDir,
|
|
237
|
-
targetName,
|
|
238
|
-
vaultPath,
|
|
239
|
-
coreSkillIds.filter((skillId) => !skipped.includes(skillId)),
|
|
240
|
-
marker.created_at,
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
return { added, removed, skipped };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export interface AdoptTargetResult {
|
|
247
|
-
adopted: boolean;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function preflightAdoptTarget(dir: string, targetName: string, vaultPath: string): void {
|
|
251
|
-
const marker = readSkillmuxMarker(dir);
|
|
252
|
-
if (marker?.role === "local_vault") {
|
|
253
|
-
throw new Error(`${dir} has a local_vault marker, not target ownership`);
|
|
254
|
-
}
|
|
255
|
-
if (!marker) return;
|
|
256
|
-
if (marker.target !== targetName) {
|
|
257
|
-
throw new Error(`${dir} is already owned by target "${marker.target}", not "${targetName}"`);
|
|
258
|
-
}
|
|
259
|
-
if (marker.schema_version !== undefined && marker.vault_path !== vaultPath) {
|
|
260
|
-
throw new Error(
|
|
261
|
-
`${dir} marker recorded vault_path ${marker.vault_path}, currently configured vault_path is ${vaultPath}`,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export interface TargetMarkerRehomePlan {
|
|
267
|
-
dir: string;
|
|
268
|
-
markerPath: string;
|
|
269
|
-
marker: SkillmuxMarker;
|
|
270
|
-
links: TargetLinkRehomePlan[];
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
interface TargetLinkRehomePlan {
|
|
274
|
-
entryPath: string;
|
|
275
|
-
previousTarget: string;
|
|
276
|
-
nextTarget: string;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Proves every managed entry resolves either to its active source or exactly to
|
|
281
|
-
* the vault recorded in its marker. Only the latter links are re-pointed.
|
|
282
|
-
*/
|
|
283
|
-
export function planTargetMarkerRehome(
|
|
284
|
-
dir: string,
|
|
285
|
-
targetName: string,
|
|
286
|
-
vaultPath: string,
|
|
287
|
-
localVaultPaths: string[] = [],
|
|
288
|
-
): TargetMarkerRehomePlan {
|
|
289
|
-
const markerPath = join(dir, SKILLMUX_MARKER_FILENAME);
|
|
290
|
-
if (!existsSync(markerPath)) {
|
|
291
|
-
throw new Error(`${dir} has a legacy marker; rehome requires .skillmux ownership`);
|
|
292
|
-
}
|
|
293
|
-
const marker = readSkillmuxMarker(dir);
|
|
294
|
-
if (!marker) throw new Error(`${dir} is not owned by skillmux`);
|
|
295
|
-
if (marker.role !== "target") throw new Error(`${dir} has a local_vault marker, not target ownership`);
|
|
296
|
-
if (marker.schema_version !== 1) {
|
|
297
|
-
throw new Error(`${dir} has a legacy marker; rehome requires schema_version 1`);
|
|
298
|
-
}
|
|
299
|
-
if (marker.target !== targetName) {
|
|
300
|
-
throw new Error(`${dir} is owned by target "${marker.target}", not "${targetName}"`);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const links: TargetLinkRehomePlan[] = [];
|
|
304
|
-
for (const skillId of marker.managed_entries ?? []) {
|
|
305
|
-
if (basename(skillId) !== skillId) {
|
|
306
|
-
throw new Error(`${dir} marker has an invalid managed entry "${skillId}"`);
|
|
307
|
-
}
|
|
308
|
-
const entryPath = join(dir, skillId);
|
|
309
|
-
if (!lstatSync(entryPath).isSymbolicLink()) {
|
|
310
|
-
throw new Error(`${entryPath} is not the managed symlink recorded by ${dir}`);
|
|
311
|
-
}
|
|
312
|
-
const sourceRoot = resolveSkillRoot(skillId, vaultPath, localVaultPaths) ?? vaultPath;
|
|
313
|
-
const expectedPath = join(sourceRoot, skillId);
|
|
314
|
-
if (!existsSync(expectedPath)) {
|
|
315
|
-
throw new Error(`${entryPath} cannot be proven to resolve from the configured vault's ${skillId}`);
|
|
316
|
-
}
|
|
317
|
-
const resolvedEntryPath = realpathSync(entryPath);
|
|
318
|
-
if (resolvedEntryPath === realpathSync(expectedPath)) continue;
|
|
319
|
-
|
|
320
|
-
const previousPath = join(marker.vault_path!, skillId);
|
|
321
|
-
if (!existsSync(previousPath) || resolvedEntryPath !== realpathSync(previousPath)) {
|
|
322
|
-
throw new Error(`${entryPath} does not resolve to the configured vault's ${skillId}`);
|
|
323
|
-
}
|
|
324
|
-
links.push({ entryPath, previousTarget: readlinkSync(entryPath), nextTarget: expectedPath });
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return { dir, markerPath, marker, links };
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/** Applies only preflighted rehome plans. All plans are validated before the first write. */
|
|
331
|
-
export function applyTargetMarkerRehome(plans: TargetMarkerRehomePlan[], vaultPath: string): void {
|
|
332
|
-
const completedLinks: TargetLinkRehomePlan[] = [];
|
|
333
|
-
const completedMarkers: TargetMarkerRehomePlan[] = [];
|
|
334
|
-
try {
|
|
335
|
-
for (const plan of plans) {
|
|
336
|
-
for (const link of plan.links) {
|
|
337
|
-
replaceSymlinkAtomic(link.entryPath, link.nextTarget);
|
|
338
|
-
completedLinks.push(link);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
for (const plan of plans) {
|
|
342
|
-
writeTargetMarker(
|
|
343
|
-
plan.dir,
|
|
344
|
-
plan.marker.target!,
|
|
345
|
-
vaultPath,
|
|
346
|
-
plan.marker.managed_entries ?? [],
|
|
347
|
-
plan.marker.created_at,
|
|
348
|
-
);
|
|
349
|
-
completedMarkers.push(plan);
|
|
350
|
-
}
|
|
351
|
-
} catch (error) {
|
|
352
|
-
for (const plan of completedMarkers.reverse()) {
|
|
353
|
-
writeFileAtomic(plan.markerPath, JSON.stringify(plan.marker, null, 2));
|
|
354
|
-
}
|
|
355
|
-
for (const link of completedLinks.reverse()) {
|
|
356
|
-
replaceSymlinkAtomic(link.entryPath, link.previousTarget);
|
|
357
|
-
}
|
|
358
|
-
throw error;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Marks an existing directory as skillmux-owned without touching its content —
|
|
364
|
-
* the consented, one-time adoption skillmux init performs (see SkillmuxMarker in
|
|
365
|
-
* schema.json: "the only path allowed to create a .skillmux marker on a
|
|
366
|
-
* previously-unmarked directory"). syncTarget's fresh-dir case handles the
|
|
367
|
-
* "doesn't exist yet" side of that rule; this handles "already exists".
|
|
368
|
-
*/
|
|
369
|
-
export function adoptTarget(dir: string, targetName: string, vaultPath: string): AdoptTargetResult {
|
|
370
|
-
preflightAdoptTarget(dir, targetName, vaultPath);
|
|
371
|
-
if (readSkillmuxMarker(dir)) return { adopted: false };
|
|
372
|
-
writeTargetMarker(dir, targetName, vaultPath, []);
|
|
373
|
-
return { adopted: true };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
export interface RestoreMonolithResult {
|
|
377
|
-
restored: boolean;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
export function restoreMonolith(targetDir: string, vaultPath: string): RestoreMonolithResult {
|
|
381
|
-
const marker = readSkillmuxMarker(targetDir);
|
|
382
|
-
if (!marker) {
|
|
383
|
-
return { restored: false };
|
|
384
|
-
}
|
|
385
|
-
if (marker.role === "local_vault") {
|
|
386
|
-
throw new Error(`${targetDir} has a local_vault marker, not target ownership`);
|
|
387
|
-
}
|
|
388
|
-
if (marker.schema_version === undefined) {
|
|
389
|
-
throw new Error(`${targetDir} has a legacy marker; migrate it before restoring full-vault delivery`);
|
|
390
|
-
}
|
|
391
|
-
if (marker.vault_path !== vaultPath) {
|
|
392
|
-
throw new Error(
|
|
393
|
-
`${targetDir} marker recorded vault_path ${marker.vault_path}, currently configured vault_path is ${vaultPath}`,
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
const managedEntries = new Set(marker.managed_entries ?? []);
|
|
397
|
-
const unmanagedEntries = readdirSync(targetDir).filter(
|
|
398
|
-
(name) =>
|
|
399
|
-
name !== SKILLMUX_MARKER_FILENAME &&
|
|
400
|
-
name !== LEGACY_MARKER_FILENAME &&
|
|
401
|
-
!managedEntries.has(name),
|
|
402
|
-
);
|
|
403
|
-
if (unmanagedEntries.length > 0) {
|
|
404
|
-
throw new Error(`refusing to restore over unmanaged entries in ${targetDir}: ${unmanagedEntries.join(", ")}`);
|
|
405
|
-
}
|
|
406
|
-
rmSync(targetDir, { recursive: true, force: true });
|
|
407
|
-
symlinkSync(vaultPath, targetDir);
|
|
408
|
-
return { restored: true };
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
export function resolveProjectPinDir(targetDir: string, path: string): string {
|
|
412
|
-
const rel = relative(homedir(), targetDir);
|
|
413
|
-
if (rel === "" || rel.startsWith("..")) {
|
|
414
|
-
throw new Error(
|
|
415
|
-
`target dir "${targetDir}" must be inside $HOME to compute a project pin dir (got relative path "${rel}")`,
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
return join(path, rel);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
export interface ProjectGroupInput {
|
|
422
|
-
paths: string[];
|
|
423
|
-
skills: string[];
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export interface SyncProjectTargetsParams {
|
|
427
|
-
vaultPath: string;
|
|
428
|
-
targetDir: string;
|
|
429
|
-
targetName: string;
|
|
430
|
-
projectGroups: Record<string, ProjectGroupInput>;
|
|
431
|
-
localVaultPaths?: string[];
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
export interface ProjectPinSyncResult extends SyncTargetResult {
|
|
435
|
-
group: string;
|
|
436
|
-
path: string;
|
|
437
|
-
pinDir: string;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
export function syncProjectTargets(
|
|
441
|
-
params: SyncProjectTargetsParams,
|
|
442
|
-
options: SyncTargetOptions = {},
|
|
443
|
-
): ProjectPinSyncResult[] {
|
|
444
|
-
const { vaultPath, targetDir, targetName, projectGroups, localVaultPaths = [] } = params;
|
|
445
|
-
const results: ProjectPinSyncResult[] = [];
|
|
446
|
-
|
|
447
|
-
for (const [group, projectGroup] of Object.entries(projectGroups)) {
|
|
448
|
-
for (const path of projectGroup.paths) {
|
|
449
|
-
if (!existsSync(path)) continue;
|
|
450
|
-
const pinDir = resolveProjectPinDir(targetDir, path);
|
|
451
|
-
const result = syncTarget(
|
|
452
|
-
{ vaultPath, targetDir: pinDir, targetName, coreSkillIds: projectGroup.skills, localVaultPaths },
|
|
453
|
-
options,
|
|
454
|
-
);
|
|
455
|
-
results.push({ group, path, pinDir, ...result });
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return results;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
export const HOOK_MARKER = "# managed-by: skillmux sync --install-hook";
|
|
463
|
-
export const LEGACY_HOOK_MARKER = "# managed-by: skr sync --install-hook";
|
|
464
|
-
|
|
465
|
-
export interface InstallHookResult {
|
|
466
|
-
installed: boolean;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
export function installPostMergeHook(vaultPath: string): InstallHookResult {
|
|
470
|
-
const hookPath = join(vaultPath, ".git", "hooks", "post-merge");
|
|
471
|
-
|
|
472
|
-
if (existsSync(hookPath)) {
|
|
473
|
-
const existing = readFileSync(hookPath, "utf-8");
|
|
474
|
-
if (existing.includes(HOOK_MARKER) || existing.includes(LEGACY_HOOK_MARKER)) return { installed: false };
|
|
475
|
-
throw new Error(`${hookPath} already exists and is not managed by skillmux — refusing to overwrite`);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const script = `#!/bin/sh\n${HOOK_MARKER}\nskillmux sync\n`;
|
|
479
|
-
writeFileSync(hookPath, script);
|
|
480
|
-
chmodSync(hookPath, 0o755);
|
|
481
|
-
return { installed: true };
|
|
482
|
-
}
|
package/src/toml-writer.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
export function stringifyToml(obj: Record<string, any>): string {
|
|
2
|
-
let out = "";
|
|
3
|
-
const topLevel: Record<string, any> = {};
|
|
4
|
-
const sections: Record<string, any> = {};
|
|
5
|
-
|
|
6
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
7
|
-
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
8
|
-
sections[k] = v;
|
|
9
|
-
} else {
|
|
10
|
-
topLevel[k] = v;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
for (const [k, v] of Object.entries(topLevel)) {
|
|
15
|
-
out += `${k} = ${formatTomlVal(v)}\n`;
|
|
16
|
-
}
|
|
17
|
-
if (Object.keys(topLevel).length > 0) out += "\n";
|
|
18
|
-
|
|
19
|
-
for (const [secName, secObj] of Object.entries(sections)) {
|
|
20
|
-
out += stringifyTomlSection([secName], secObj);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return out;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function stringifyTomlSection(path: string[], obj: Record<string, any>): string {
|
|
27
|
-
let out = `[${path.join(".")}]\n`;
|
|
28
|
-
const subSections: Record<string, any> = {};
|
|
29
|
-
|
|
30
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
31
|
-
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
32
|
-
subSections[k] = v;
|
|
33
|
-
} else {
|
|
34
|
-
out += `${k} = ${formatTomlVal(v)}\n`;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
out += "\n";
|
|
38
|
-
|
|
39
|
-
for (const [subName, subObj] of Object.entries(subSections)) {
|
|
40
|
-
out += stringifyTomlSection([...path, subName], subObj);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return out;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function formatTomlVal(v: unknown): string {
|
|
47
|
-
if (typeof v === "string") return JSON.stringify(v);
|
|
48
|
-
if (typeof v === "boolean" || typeof v === "number") return String(v);
|
|
49
|
-
if (Array.isArray(v)) return JSON.stringify(v);
|
|
50
|
-
return JSON.stringify(v);
|
|
51
|
-
}
|