@light-merlin-dark/skill-sync 0.1.1

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/index.js ADDED
@@ -0,0 +1,1713 @@
1
+ #!/usr/bin/env node
2
+
3
+ // node_modules/cac/dist/index.mjs
4
+ import { EventEmitter } from "events";
5
+ function toArr(any) {
6
+ return any == null ? [] : Array.isArray(any) ? any : [any];
7
+ }
8
+ function toVal(out, key, val, opts) {
9
+ var x, old = out[key], nxt = ~opts.string.indexOf(key) ? val == null || val === true ? "" : String(val) : typeof val === "boolean" ? val : ~opts.boolean.indexOf(key) ? val === "false" ? false : val === "true" || (out._.push((x = +val, x * 0 === 0) ? x : val), !!val) : (x = +val, x * 0 === 0) ? x : val;
10
+ out[key] = old == null ? nxt : Array.isArray(old) ? old.concat(nxt) : [old, nxt];
11
+ }
12
+ function mri2(args, opts) {
13
+ args = args || [];
14
+ opts = opts || {};
15
+ var k, arr, arg, name, val, out = { _: [] };
16
+ var i = 0, j = 0, idx = 0, len = args.length;
17
+ const alibi = opts.alias !== undefined;
18
+ const strict = opts.unknown !== undefined;
19
+ const defaults = opts.default !== undefined;
20
+ opts.alias = opts.alias || {};
21
+ opts.string = toArr(opts.string);
22
+ opts.boolean = toArr(opts.boolean);
23
+ if (alibi) {
24
+ for (k in opts.alias) {
25
+ arr = opts.alias[k] = toArr(opts.alias[k]);
26
+ for (i = 0;i < arr.length; i++) {
27
+ (opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);
28
+ }
29
+ }
30
+ }
31
+ for (i = opts.boolean.length;i-- > 0; ) {
32
+ arr = opts.alias[opts.boolean[i]] || [];
33
+ for (j = arr.length;j-- > 0; )
34
+ opts.boolean.push(arr[j]);
35
+ }
36
+ for (i = opts.string.length;i-- > 0; ) {
37
+ arr = opts.alias[opts.string[i]] || [];
38
+ for (j = arr.length;j-- > 0; )
39
+ opts.string.push(arr[j]);
40
+ }
41
+ if (defaults) {
42
+ for (k in opts.default) {
43
+ name = typeof opts.default[k];
44
+ arr = opts.alias[k] = opts.alias[k] || [];
45
+ if (opts[name] !== undefined) {
46
+ opts[name].push(k);
47
+ for (i = 0;i < arr.length; i++) {
48
+ opts[name].push(arr[i]);
49
+ }
50
+ }
51
+ }
52
+ }
53
+ const keys = strict ? Object.keys(opts.alias) : [];
54
+ for (i = 0;i < len; i++) {
55
+ arg = args[i];
56
+ if (arg === "--") {
57
+ out._ = out._.concat(args.slice(++i));
58
+ break;
59
+ }
60
+ for (j = 0;j < arg.length; j++) {
61
+ if (arg.charCodeAt(j) !== 45)
62
+ break;
63
+ }
64
+ if (j === 0) {
65
+ out._.push(arg);
66
+ } else if (arg.substring(j, j + 3) === "no-") {
67
+ name = arg.substring(j + 3);
68
+ if (strict && !~keys.indexOf(name)) {
69
+ return opts.unknown(arg);
70
+ }
71
+ out[name] = false;
72
+ } else {
73
+ for (idx = j + 1;idx < arg.length; idx++) {
74
+ if (arg.charCodeAt(idx) === 61)
75
+ break;
76
+ }
77
+ name = arg.substring(j, idx);
78
+ val = arg.substring(++idx) || (i + 1 === len || ("" + args[i + 1]).charCodeAt(0) === 45 || args[++i]);
79
+ arr = j === 2 ? [name] : name;
80
+ for (idx = 0;idx < arr.length; idx++) {
81
+ name = arr[idx];
82
+ if (strict && !~keys.indexOf(name))
83
+ return opts.unknown("-".repeat(j) + name);
84
+ toVal(out, name, idx + 1 < arr.length || val, opts);
85
+ }
86
+ }
87
+ }
88
+ if (defaults) {
89
+ for (k in opts.default) {
90
+ if (out[k] === undefined) {
91
+ out[k] = opts.default[k];
92
+ }
93
+ }
94
+ }
95
+ if (alibi) {
96
+ for (k in out) {
97
+ arr = opts.alias[k] || [];
98
+ while (arr.length > 0) {
99
+ out[arr.shift()] = out[k];
100
+ }
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+ var removeBrackets = (v) => v.replace(/[<[].+/, "").trim();
106
+ var findAllBrackets = (v) => {
107
+ const ANGLED_BRACKET_RE_GLOBAL = /<([^>]+)>/g;
108
+ const SQUARE_BRACKET_RE_GLOBAL = /\[([^\]]+)\]/g;
109
+ const res = [];
110
+ const parse = (match) => {
111
+ let variadic = false;
112
+ let value = match[1];
113
+ if (value.startsWith("...")) {
114
+ value = value.slice(3);
115
+ variadic = true;
116
+ }
117
+ return {
118
+ required: match[0].startsWith("<"),
119
+ value,
120
+ variadic
121
+ };
122
+ };
123
+ let angledMatch;
124
+ while (angledMatch = ANGLED_BRACKET_RE_GLOBAL.exec(v)) {
125
+ res.push(parse(angledMatch));
126
+ }
127
+ let squareMatch;
128
+ while (squareMatch = SQUARE_BRACKET_RE_GLOBAL.exec(v)) {
129
+ res.push(parse(squareMatch));
130
+ }
131
+ return res;
132
+ };
133
+ var getMriOptions = (options) => {
134
+ const result = { alias: {}, boolean: [] };
135
+ for (const [index, option] of options.entries()) {
136
+ if (option.names.length > 1) {
137
+ result.alias[option.names[0]] = option.names.slice(1);
138
+ }
139
+ if (option.isBoolean) {
140
+ if (option.negated) {
141
+ const hasStringTypeOption = options.some((o, i) => {
142
+ return i !== index && o.names.some((name) => option.names.includes(name)) && typeof o.required === "boolean";
143
+ });
144
+ if (!hasStringTypeOption) {
145
+ result.boolean.push(option.names[0]);
146
+ }
147
+ } else {
148
+ result.boolean.push(option.names[0]);
149
+ }
150
+ }
151
+ }
152
+ return result;
153
+ };
154
+ var findLongest = (arr) => {
155
+ return arr.sort((a, b) => {
156
+ return a.length > b.length ? -1 : 1;
157
+ })[0];
158
+ };
159
+ var padRight = (str, length) => {
160
+ return str.length >= length ? str : `${str}${" ".repeat(length - str.length)}`;
161
+ };
162
+ var camelcase = (input) => {
163
+ return input.replace(/([a-z])-([a-z])/g, (_, p1, p2) => {
164
+ return p1 + p2.toUpperCase();
165
+ });
166
+ };
167
+ var setDotProp = (obj, keys, val) => {
168
+ let i = 0;
169
+ let length = keys.length;
170
+ let t = obj;
171
+ let x;
172
+ for (;i < length; ++i) {
173
+ x = t[keys[i]];
174
+ t = t[keys[i]] = i === length - 1 ? val : x != null ? x : !!~keys[i + 1].indexOf(".") || !(+keys[i + 1] > -1) ? {} : [];
175
+ }
176
+ };
177
+ var setByType = (obj, transforms) => {
178
+ for (const key of Object.keys(transforms)) {
179
+ const transform = transforms[key];
180
+ if (transform.shouldTransform) {
181
+ obj[key] = Array.prototype.concat.call([], obj[key]);
182
+ if (typeof transform.transformFunction === "function") {
183
+ obj[key] = obj[key].map(transform.transformFunction);
184
+ }
185
+ }
186
+ }
187
+ };
188
+ var getFileName = (input) => {
189
+ const m = /([^\\\/]+)$/.exec(input);
190
+ return m ? m[1] : "";
191
+ };
192
+ var camelcaseOptionName = (name) => {
193
+ return name.split(".").map((v, i) => {
194
+ return i === 0 ? camelcase(v) : v;
195
+ }).join(".");
196
+ };
197
+
198
+ class CACError extends Error {
199
+ constructor(message) {
200
+ super(message);
201
+ this.name = this.constructor.name;
202
+ if (typeof Error.captureStackTrace === "function") {
203
+ Error.captureStackTrace(this, this.constructor);
204
+ } else {
205
+ this.stack = new Error(message).stack;
206
+ }
207
+ }
208
+ }
209
+
210
+ class Option {
211
+ constructor(rawName, description, config) {
212
+ this.rawName = rawName;
213
+ this.description = description;
214
+ this.config = Object.assign({}, config);
215
+ rawName = rawName.replace(/\.\*/g, "");
216
+ this.negated = false;
217
+ this.names = removeBrackets(rawName).split(",").map((v) => {
218
+ let name = v.trim().replace(/^-{1,2}/, "");
219
+ if (name.startsWith("no-")) {
220
+ this.negated = true;
221
+ name = name.replace(/^no-/, "");
222
+ }
223
+ return camelcaseOptionName(name);
224
+ }).sort((a, b) => a.length > b.length ? 1 : -1);
225
+ this.name = this.names[this.names.length - 1];
226
+ if (this.negated && this.config.default == null) {
227
+ this.config.default = true;
228
+ }
229
+ if (rawName.includes("<")) {
230
+ this.required = true;
231
+ } else if (rawName.includes("[")) {
232
+ this.required = false;
233
+ } else {
234
+ this.isBoolean = true;
235
+ }
236
+ }
237
+ }
238
+ var processArgs = process.argv;
239
+ var platformInfo = `${process.platform}-${process.arch} node-${process.version}`;
240
+
241
+ class Command {
242
+ constructor(rawName, description, config = {}, cli) {
243
+ this.rawName = rawName;
244
+ this.description = description;
245
+ this.config = config;
246
+ this.cli = cli;
247
+ this.options = [];
248
+ this.aliasNames = [];
249
+ this.name = removeBrackets(rawName);
250
+ this.args = findAllBrackets(rawName);
251
+ this.examples = [];
252
+ }
253
+ usage(text) {
254
+ this.usageText = text;
255
+ return this;
256
+ }
257
+ allowUnknownOptions() {
258
+ this.config.allowUnknownOptions = true;
259
+ return this;
260
+ }
261
+ ignoreOptionDefaultValue() {
262
+ this.config.ignoreOptionDefaultValue = true;
263
+ return this;
264
+ }
265
+ version(version, customFlags = "-v, --version") {
266
+ this.versionNumber = version;
267
+ this.option(customFlags, "Display version number");
268
+ return this;
269
+ }
270
+ example(example) {
271
+ this.examples.push(example);
272
+ return this;
273
+ }
274
+ option(rawName, description, config) {
275
+ const option = new Option(rawName, description, config);
276
+ this.options.push(option);
277
+ return this;
278
+ }
279
+ alias(name) {
280
+ this.aliasNames.push(name);
281
+ return this;
282
+ }
283
+ action(callback) {
284
+ this.commandAction = callback;
285
+ return this;
286
+ }
287
+ isMatched(name) {
288
+ return this.name === name || this.aliasNames.includes(name);
289
+ }
290
+ get isDefaultCommand() {
291
+ return this.name === "" || this.aliasNames.includes("!");
292
+ }
293
+ get isGlobalCommand() {
294
+ return this instanceof GlobalCommand;
295
+ }
296
+ hasOption(name) {
297
+ name = name.split(".")[0];
298
+ return this.options.find((option) => {
299
+ return option.names.includes(name);
300
+ });
301
+ }
302
+ outputHelp() {
303
+ const { name, commands } = this.cli;
304
+ const {
305
+ versionNumber,
306
+ options: globalOptions,
307
+ helpCallback
308
+ } = this.cli.globalCommand;
309
+ let sections = [
310
+ {
311
+ body: `${name}${versionNumber ? `/${versionNumber}` : ""}`
312
+ }
313
+ ];
314
+ sections.push({
315
+ title: "Usage",
316
+ body: ` $ ${name} ${this.usageText || this.rawName}`
317
+ });
318
+ const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
319
+ if (showCommands) {
320
+ const longestCommandName = findLongest(commands.map((command) => command.rawName));
321
+ sections.push({
322
+ title: "Commands",
323
+ body: commands.map((command) => {
324
+ return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
325
+ }).join(`
326
+ `)
327
+ });
328
+ sections.push({
329
+ title: `For more info, run any command with the \`--help\` flag`,
330
+ body: commands.map((command) => ` $ ${name}${command.name === "" ? "" : ` ${command.name}`} --help`).join(`
331
+ `)
332
+ });
333
+ }
334
+ let options = this.isGlobalCommand ? globalOptions : [...this.options, ...globalOptions || []];
335
+ if (!this.isGlobalCommand && !this.isDefaultCommand) {
336
+ options = options.filter((option) => option.name !== "version");
337
+ }
338
+ if (options.length > 0) {
339
+ const longestOptionName = findLongest(options.map((option) => option.rawName));
340
+ sections.push({
341
+ title: "Options",
342
+ body: options.map((option) => {
343
+ return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === undefined ? "" : `(default: ${option.config.default})`}`;
344
+ }).join(`
345
+ `)
346
+ });
347
+ }
348
+ if (this.examples.length > 0) {
349
+ sections.push({
350
+ title: "Examples",
351
+ body: this.examples.map((example) => {
352
+ if (typeof example === "function") {
353
+ return example(name);
354
+ }
355
+ return example;
356
+ }).join(`
357
+ `)
358
+ });
359
+ }
360
+ if (helpCallback) {
361
+ sections = helpCallback(sections) || sections;
362
+ }
363
+ console.log(sections.map((section) => {
364
+ return section.title ? `${section.title}:
365
+ ${section.body}` : section.body;
366
+ }).join(`
367
+
368
+ `));
369
+ }
370
+ outputVersion() {
371
+ const { name } = this.cli;
372
+ const { versionNumber } = this.cli.globalCommand;
373
+ if (versionNumber) {
374
+ console.log(`${name}/${versionNumber} ${platformInfo}`);
375
+ }
376
+ }
377
+ checkRequiredArgs() {
378
+ const minimalArgsCount = this.args.filter((arg) => arg.required).length;
379
+ if (this.cli.args.length < minimalArgsCount) {
380
+ throw new CACError(`missing required args for command \`${this.rawName}\``);
381
+ }
382
+ }
383
+ checkUnknownOptions() {
384
+ const { options, globalCommand } = this.cli;
385
+ if (!this.config.allowUnknownOptions) {
386
+ for (const name of Object.keys(options)) {
387
+ if (name !== "--" && !this.hasOption(name) && !globalCommand.hasOption(name)) {
388
+ throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
389
+ }
390
+ }
391
+ }
392
+ }
393
+ checkOptionValue() {
394
+ const { options: parsedOptions, globalCommand } = this.cli;
395
+ const options = [...globalCommand.options, ...this.options];
396
+ for (const option of options) {
397
+ const value = parsedOptions[option.name.split(".")[0]];
398
+ if (option.required) {
399
+ const hasNegated = options.some((o) => o.negated && o.names.includes(option.name));
400
+ if (value === true || value === false && !hasNegated) {
401
+ throw new CACError(`option \`${option.rawName}\` value is missing`);
402
+ }
403
+ }
404
+ }
405
+ }
406
+ }
407
+
408
+ class GlobalCommand extends Command {
409
+ constructor(cli) {
410
+ super("@@global@@", "", {}, cli);
411
+ }
412
+ }
413
+ var __assign = Object.assign;
414
+
415
+ class CAC extends EventEmitter {
416
+ constructor(name = "") {
417
+ super();
418
+ this.name = name;
419
+ this.commands = [];
420
+ this.rawArgs = [];
421
+ this.args = [];
422
+ this.options = {};
423
+ this.globalCommand = new GlobalCommand(this);
424
+ this.globalCommand.usage("<command> [options]");
425
+ }
426
+ usage(text) {
427
+ this.globalCommand.usage(text);
428
+ return this;
429
+ }
430
+ command(rawName, description, config) {
431
+ const command = new Command(rawName, description || "", config, this);
432
+ command.globalCommand = this.globalCommand;
433
+ this.commands.push(command);
434
+ return command;
435
+ }
436
+ option(rawName, description, config) {
437
+ this.globalCommand.option(rawName, description, config);
438
+ return this;
439
+ }
440
+ help(callback) {
441
+ this.globalCommand.option("-h, --help", "Display this message");
442
+ this.globalCommand.helpCallback = callback;
443
+ this.showHelpOnExit = true;
444
+ return this;
445
+ }
446
+ version(version, customFlags = "-v, --version") {
447
+ this.globalCommand.version(version, customFlags);
448
+ this.showVersionOnExit = true;
449
+ return this;
450
+ }
451
+ example(example) {
452
+ this.globalCommand.example(example);
453
+ return this;
454
+ }
455
+ outputHelp() {
456
+ if (this.matchedCommand) {
457
+ this.matchedCommand.outputHelp();
458
+ } else {
459
+ this.globalCommand.outputHelp();
460
+ }
461
+ }
462
+ outputVersion() {
463
+ this.globalCommand.outputVersion();
464
+ }
465
+ setParsedInfo({ args, options }, matchedCommand, matchedCommandName) {
466
+ this.args = args;
467
+ this.options = options;
468
+ if (matchedCommand) {
469
+ this.matchedCommand = matchedCommand;
470
+ }
471
+ if (matchedCommandName) {
472
+ this.matchedCommandName = matchedCommandName;
473
+ }
474
+ return this;
475
+ }
476
+ unsetMatchedCommand() {
477
+ this.matchedCommand = undefined;
478
+ this.matchedCommandName = undefined;
479
+ }
480
+ parse(argv = processArgs, {
481
+ run = true
482
+ } = {}) {
483
+ this.rawArgs = argv;
484
+ if (!this.name) {
485
+ this.name = argv[1] ? getFileName(argv[1]) : "cli";
486
+ }
487
+ let shouldParse = true;
488
+ for (const command of this.commands) {
489
+ const parsed = this.mri(argv.slice(2), command);
490
+ const commandName = parsed.args[0];
491
+ if (command.isMatched(commandName)) {
492
+ shouldParse = false;
493
+ const parsedInfo = __assign(__assign({}, parsed), {
494
+ args: parsed.args.slice(1)
495
+ });
496
+ this.setParsedInfo(parsedInfo, command, commandName);
497
+ this.emit(`command:${commandName}`, command);
498
+ }
499
+ }
500
+ if (shouldParse) {
501
+ for (const command of this.commands) {
502
+ if (command.name === "") {
503
+ shouldParse = false;
504
+ const parsed = this.mri(argv.slice(2), command);
505
+ this.setParsedInfo(parsed, command);
506
+ this.emit(`command:!`, command);
507
+ }
508
+ }
509
+ }
510
+ if (shouldParse) {
511
+ const parsed = this.mri(argv.slice(2));
512
+ this.setParsedInfo(parsed);
513
+ }
514
+ if (this.options.help && this.showHelpOnExit) {
515
+ this.outputHelp();
516
+ run = false;
517
+ this.unsetMatchedCommand();
518
+ }
519
+ if (this.options.version && this.showVersionOnExit && this.matchedCommandName == null) {
520
+ this.outputVersion();
521
+ run = false;
522
+ this.unsetMatchedCommand();
523
+ }
524
+ const parsedArgv = { args: this.args, options: this.options };
525
+ if (run) {
526
+ this.runMatchedCommand();
527
+ }
528
+ if (!this.matchedCommand && this.args[0]) {
529
+ this.emit("command:*");
530
+ }
531
+ return parsedArgv;
532
+ }
533
+ mri(argv, command) {
534
+ const cliOptions = [
535
+ ...this.globalCommand.options,
536
+ ...command ? command.options : []
537
+ ];
538
+ const mriOptions = getMriOptions(cliOptions);
539
+ let argsAfterDoubleDashes = [];
540
+ const doubleDashesIndex = argv.indexOf("--");
541
+ if (doubleDashesIndex > -1) {
542
+ argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);
543
+ argv = argv.slice(0, doubleDashesIndex);
544
+ }
545
+ let parsed = mri2(argv, mriOptions);
546
+ parsed = Object.keys(parsed).reduce((res, name) => {
547
+ return __assign(__assign({}, res), {
548
+ [camelcaseOptionName(name)]: parsed[name]
549
+ });
550
+ }, { _: [] });
551
+ const args = parsed._;
552
+ const options = {
553
+ "--": argsAfterDoubleDashes
554
+ };
555
+ const ignoreDefault = command && command.config.ignoreOptionDefaultValue ? command.config.ignoreOptionDefaultValue : this.globalCommand.config.ignoreOptionDefaultValue;
556
+ let transforms = Object.create(null);
557
+ for (const cliOption of cliOptions) {
558
+ if (!ignoreDefault && cliOption.config.default !== undefined) {
559
+ for (const name of cliOption.names) {
560
+ options[name] = cliOption.config.default;
561
+ }
562
+ }
563
+ if (Array.isArray(cliOption.config.type)) {
564
+ if (transforms[cliOption.name] === undefined) {
565
+ transforms[cliOption.name] = Object.create(null);
566
+ transforms[cliOption.name]["shouldTransform"] = true;
567
+ transforms[cliOption.name]["transformFunction"] = cliOption.config.type[0];
568
+ }
569
+ }
570
+ }
571
+ for (const key of Object.keys(parsed)) {
572
+ if (key !== "_") {
573
+ const keys = key.split(".");
574
+ setDotProp(options, keys, parsed[key]);
575
+ setByType(options, transforms);
576
+ }
577
+ }
578
+ return {
579
+ args,
580
+ options
581
+ };
582
+ }
583
+ runMatchedCommand() {
584
+ const { args, options, matchedCommand: command } = this;
585
+ if (!command || !command.commandAction)
586
+ return;
587
+ command.checkUnknownOptions();
588
+ command.checkOptionValue();
589
+ command.checkRequiredArgs();
590
+ const actionArgs = [];
591
+ command.args.forEach((arg, index) => {
592
+ if (arg.variadic) {
593
+ actionArgs.push(args.slice(index));
594
+ } else {
595
+ actionArgs.push(args[index]);
596
+ }
597
+ });
598
+ actionArgs.push(options);
599
+ return command.commandAction.apply(this, actionArgs);
600
+ }
601
+ }
602
+ var cac = (name = "") => new CAC(name);
603
+
604
+ // src/index.ts
605
+ import { mkdirSync as mkdirSync3 } from "node:fs";
606
+
607
+ // src/core/backup.ts
608
+ import { existsSync as existsSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, readlinkSync as readlinkSync2, realpathSync as realpathSync2, symlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
609
+ import { dirname as dirname2, join as join2, relative, resolve as resolve2 } from "node:path";
610
+
611
+ // src/core/utils.ts
612
+ import { createHash } from "node:crypto";
613
+ import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, readdirSync, rmSync, writeFileSync } from "node:fs";
614
+ import { basename, dirname, join, resolve } from "node:path";
615
+ import { homedir } from "node:os";
616
+ function resolveHomeDir(explicitHome) {
617
+ const envHome = process.env.SKILL_SYNC_HOME;
618
+ if (explicitHome) {
619
+ return resolve(explicitHome);
620
+ }
621
+ if (envHome) {
622
+ return resolve(envHome);
623
+ }
624
+ return homedir();
625
+ }
626
+ function buildRuntimeContext(options) {
627
+ const homeDir = resolveHomeDir(options.home);
628
+ const stateDir = join(homeDir, ".skill-sync");
629
+ return {
630
+ homeDir,
631
+ stateDir,
632
+ configPath: join(stateDir, "config.json"),
633
+ statePath: join(stateDir, "state.json"),
634
+ json: Boolean(options.json)
635
+ };
636
+ }
637
+ function expandHomePath(input, homeDir) {
638
+ if (input === "~") {
639
+ return homeDir;
640
+ }
641
+ if (input.startsWith("~/")) {
642
+ return join(homeDir, input.slice(2));
643
+ }
644
+ return resolve(input);
645
+ }
646
+ function ensureDir(path) {
647
+ mkdirSync(path, { recursive: true });
648
+ }
649
+ function readJsonFile(path) {
650
+ if (!existsSync(path)) {
651
+ return null;
652
+ }
653
+ return JSON.parse(readFileSync(path, "utf8"));
654
+ }
655
+ function writeJsonFile(path, value) {
656
+ ensureDir(dirname(path));
657
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
658
+ `, "utf8");
659
+ }
660
+ function nowIso() {
661
+ return new Date().toISOString();
662
+ }
663
+ function timestampId() {
664
+ return nowIso().replace(/[:.]/g, "-");
665
+ }
666
+ function slugify(input) {
667
+ return input.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9.-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
668
+ }
669
+ function parseSkillFrontmatterName(skillFilePath) {
670
+ const content = readFileSync(skillFilePath, "utf8");
671
+ if (!content.startsWith("---")) {
672
+ return;
673
+ }
674
+ const parts = content.split(`
675
+ ---`);
676
+ if (parts.length < 2) {
677
+ return;
678
+ }
679
+ const match = parts[0].match(/^name:\s*(.+)\s*$/m);
680
+ return match?.[1]?.trim();
681
+ }
682
+ function hashContent(content) {
683
+ return createHash("sha1").update(content).digest("hex");
684
+ }
685
+ function listImmediateDirectories(path) {
686
+ if (!existsSync(path)) {
687
+ return [];
688
+ }
689
+ return readdirSync(path).map((name) => join(path, name)).filter((candidate) => {
690
+ try {
691
+ return lstatSync(candidate).isDirectory();
692
+ } catch {
693
+ return false;
694
+ }
695
+ });
696
+ }
697
+ function inspectEntry(path) {
698
+ if (!existsSync(path)) {
699
+ return { exists: false, type: "missing" };
700
+ }
701
+ const stats = lstatSync(path);
702
+ if (stats.isSymbolicLink()) {
703
+ const linkTarget = readFileSyncLink(path);
704
+ let resolvedTarget;
705
+ try {
706
+ resolvedTarget = realpathSync(path);
707
+ } catch {
708
+ resolvedTarget = undefined;
709
+ }
710
+ return { exists: true, type: "symlink", linkTarget, resolvedTarget };
711
+ }
712
+ if (stats.isDirectory()) {
713
+ return { exists: true, type: "directory" };
714
+ }
715
+ return { exists: true, type: "file" };
716
+ }
717
+ function readFileSyncLink(path) {
718
+ return readlinkSync(path);
719
+ }
720
+ function pathOwnsEntry(rootPath, entryPath) {
721
+ const normalizedRoot = resolve(rootPath);
722
+ const normalizedEntry = resolve(entryPath);
723
+ return normalizedEntry === normalizedRoot || normalizedEntry.startsWith(`${normalizedRoot}/`);
724
+ }
725
+ function removePath(path) {
726
+ rmSync(path, { recursive: true, force: true });
727
+ }
728
+
729
+ // src/core/backup.ts
730
+ function createBackup(runtime, harnesses, state) {
731
+ const id = timestampId();
732
+ const backupDir = join2(runtime.stateDir, "backups", id);
733
+ ensureDir(backupDir);
734
+ const harnessSnapshots = harnesses.map((harness) => snapshotHarness(backupDir, harness));
735
+ const manifest = {
736
+ version: 1,
737
+ id,
738
+ createdAt: new Date().toISOString(),
739
+ homeDir: runtime.homeDir,
740
+ stateSnapshotIncluded: true,
741
+ harnesses: harnessSnapshots
742
+ };
743
+ writeJsonFile(join2(backupDir, "manifest.json"), manifest);
744
+ writeJsonFile(join2(backupDir, "state.json"), state);
745
+ return manifest;
746
+ }
747
+ function snapshotHarness(_backupDir, harness) {
748
+ const exists = existsSync2(harness.rootPath);
749
+ const entries = [];
750
+ if (exists) {
751
+ for (const name of readdirSync2(harness.rootPath).sort()) {
752
+ const entryPath = join2(harness.rootPath, name);
753
+ const stats = lstatSync2(entryPath);
754
+ const snapshot = {
755
+ name,
756
+ path: entryPath,
757
+ type: stats.isSymbolicLink() ? "symlink" : stats.isDirectory() ? "directory" : "file",
758
+ skillFiles: []
759
+ };
760
+ if (stats.isSymbolicLink()) {
761
+ snapshot.linkTarget = readlinkSync2(entryPath);
762
+ const inspection = inspectEntry(entryPath);
763
+ snapshot.targetExists = inspection.resolvedTarget !== undefined;
764
+ if (inspection.resolvedTarget) {
765
+ snapshot.targetType = lstatSync2(inspection.resolvedTarget).isDirectory() ? "directory" : "file";
766
+ snapshot.skillFiles = collectSkillFiles(inspection.resolvedTarget, snapshot.targetType);
767
+ }
768
+ } else {
769
+ snapshot.targetType = stats.isDirectory() ? "directory" : "file";
770
+ snapshot.skillFiles = collectSkillFiles(entryPath, snapshot.type);
771
+ }
772
+ entries.push(snapshot);
773
+ }
774
+ }
775
+ return {
776
+ id: harness.id,
777
+ label: harness.label,
778
+ rootPath: harness.rootPath,
779
+ exists,
780
+ entries
781
+ };
782
+ }
783
+ function listBackups(runtime) {
784
+ const backupsDir = join2(runtime.stateDir, "backups");
785
+ if (!existsSync2(backupsDir)) {
786
+ return [];
787
+ }
788
+ return readdirSync2(backupsDir).sort().reverse().map((id) => readJsonFile(join2(backupsDir, id, "manifest.json"))).filter((manifest) => manifest !== null);
789
+ }
790
+ function restoreBackup(runtime, backupId, selectedHarnessIds, dryRun, currentState) {
791
+ const backupDir = join2(runtime.stateDir, "backups", backupId);
792
+ const manifest = readJsonFile(join2(backupDir, "manifest.json"));
793
+ if (!manifest) {
794
+ throw new Error(`Backup not found: ${backupId}`);
795
+ }
796
+ const backupState = readJsonFile(join2(backupDir, "state.json"));
797
+ const selected = selectedHarnessIds.length > 0 ? manifest.harnesses.filter((harness) => selectedHarnessIds.includes(harness.id)) : manifest.harnesses;
798
+ for (const harness of selected) {
799
+ if (!dryRun) {
800
+ ensureDir(harness.rootPath);
801
+ }
802
+ const currentEntries = existsSync2(harness.rootPath) ? readdirSync2(harness.rootPath) : [];
803
+ const desiredEntries = new Set(harness.entries.map((entry) => entry.name));
804
+ for (const name of currentEntries) {
805
+ if (desiredEntries.has(name)) {
806
+ continue;
807
+ }
808
+ if (!dryRun) {
809
+ removePath(join2(harness.rootPath, name));
810
+ }
811
+ }
812
+ for (const entry of harness.entries) {
813
+ restoreEntry(backupDir, harness, entry, dryRun);
814
+ }
815
+ }
816
+ let nextState = currentState;
817
+ if (backupState) {
818
+ const selectedRoots = selected.map((harness) => harness.rootPath);
819
+ const managedEntries = { ...currentState.managedEntries };
820
+ for (const key of Object.keys(managedEntries)) {
821
+ if (selectedRoots.some((root) => pathOwnsEntry(root, key))) {
822
+ delete managedEntries[key];
823
+ }
824
+ }
825
+ for (const [entryPath, managed] of Object.entries(backupState.managedEntries)) {
826
+ if (selectedRoots.some((root) => pathOwnsEntry(root, entryPath))) {
827
+ managedEntries[entryPath] = managed;
828
+ }
829
+ }
830
+ nextState = {
831
+ version: backupState.version,
832
+ managedEntries
833
+ };
834
+ }
835
+ return { manifest, nextState };
836
+ }
837
+ function restoreEntry(_backupDir, harness, entry, dryRun) {
838
+ const destinationPath = join2(harness.rootPath, entry.name);
839
+ if (dryRun) {
840
+ return;
841
+ }
842
+ removePath(destinationPath);
843
+ const linkTargetExists = entry.linkTarget ? existsSync2(resolve2(dirname2(destinationPath), entry.linkTarget)) : false;
844
+ if (entry.type === "symlink" && entry.linkTarget && linkTargetExists) {
845
+ symlinkSync(entry.linkTarget, destinationPath);
846
+ return;
847
+ }
848
+ if (entry.skillFiles.length > 0) {
849
+ restoreSkillFiles(destinationPath, entry);
850
+ return;
851
+ }
852
+ throw new Error(`Cannot restore ${relative(harness.rootPath, destinationPath)}: no valid link target or backed-up SKILL.md files`);
853
+ }
854
+ function restoreSkillFiles(destinationPath, entry) {
855
+ const treatAsDirectory = entry.type === "directory" || entry.type === "symlink" || entry.targetType === "directory" || entry.skillFiles.some((file) => file.relativePath.includes("/"));
856
+ if (treatAsDirectory) {
857
+ mkdirSync2(destinationPath, { recursive: true });
858
+ for (const skillFile2 of entry.skillFiles) {
859
+ const targetPath = join2(destinationPath, skillFile2.relativePath);
860
+ mkdirSync2(dirname2(targetPath), { recursive: true });
861
+ writeFileSync2(targetPath, skillFile2.content, "utf8");
862
+ }
863
+ return;
864
+ }
865
+ const skillFile = entry.skillFiles[0];
866
+ mkdirSync2(dirname2(destinationPath), { recursive: true });
867
+ writeFileSync2(destinationPath, skillFile.content, "utf8");
868
+ }
869
+ function collectSkillFiles(entryPath, entryType) {
870
+ if (!existsSync2(entryPath)) {
871
+ return [];
872
+ }
873
+ if (entryType === "file") {
874
+ if (!entryPath.endsWith("SKILL.md")) {
875
+ return [];
876
+ }
877
+ return [{ relativePath: "SKILL.md", content: readFileSync2(entryPath, "utf8") }];
878
+ }
879
+ return walkForSkillFiles(entryPath, "", new Set);
880
+ }
881
+ function walkForSkillFiles(absoluteCurrent, currentRelative, visited) {
882
+ const stats = lstatSync2(absoluteCurrent);
883
+ const canonicalPath = getCanonicalPath(absoluteCurrent);
884
+ if (canonicalPath && visited.has(canonicalPath)) {
885
+ return [];
886
+ }
887
+ if (stats.isSymbolicLink()) {
888
+ const inspection = inspectEntry(absoluteCurrent);
889
+ if (!inspection.resolvedTarget) {
890
+ return [];
891
+ }
892
+ if (canonicalPath) {
893
+ visited.add(canonicalPath);
894
+ }
895
+ const resolvedStats = lstatSync2(inspection.resolvedTarget);
896
+ if (resolvedStats.isDirectory()) {
897
+ return walkForSkillFiles(inspection.resolvedTarget, currentRelative, visited);
898
+ }
899
+ if (absoluteCurrent.endsWith("SKILL.md")) {
900
+ return [{ relativePath: currentRelative || "SKILL.md", content: readFileSync2(absoluteCurrent, "utf8") }];
901
+ }
902
+ return [];
903
+ }
904
+ if (stats.isFile()) {
905
+ if (absoluteCurrent.endsWith("SKILL.md")) {
906
+ return [{ relativePath: currentRelative || "SKILL.md", content: readFileSync2(absoluteCurrent, "utf8") }];
907
+ }
908
+ return [];
909
+ }
910
+ if (!stats.isDirectory()) {
911
+ return [];
912
+ }
913
+ if (canonicalPath) {
914
+ visited.add(canonicalPath);
915
+ }
916
+ const snapshots = [];
917
+ for (const name of readdirSync2(absoluteCurrent).sort()) {
918
+ if (IGNORED_BACKUP_DIR_NAMES.has(name)) {
919
+ continue;
920
+ }
921
+ const nextRelative = currentRelative ? join2(currentRelative, name) : name;
922
+ snapshots.push(...walkForSkillFiles(join2(absoluteCurrent, name), nextRelative, visited));
923
+ }
924
+ return snapshots;
925
+ }
926
+ function getCanonicalPath(path) {
927
+ try {
928
+ return realpathSync2(path);
929
+ } catch {
930
+ return;
931
+ }
932
+ }
933
+ var IGNORED_BACKUP_DIR_NAMES = new Set([
934
+ ".git",
935
+ "node_modules",
936
+ "dist",
937
+ "build",
938
+ ".next",
939
+ "coverage",
940
+ "tmp",
941
+ "temp"
942
+ ]);
943
+
944
+ // src/core/config.ts
945
+ import { existsSync as existsSync3 } from "node:fs";
946
+ import { join as join3, resolve as resolve3 } from "node:path";
947
+ function getDefaultConfig(homeDir) {
948
+ return {
949
+ version: 1,
950
+ projectsRoots: [join3(homeDir, "_dev")],
951
+ discovery: {
952
+ ignorePathPrefixes: [],
953
+ preferPathPrefixes: []
954
+ },
955
+ harnesses: {
956
+ custom: []
957
+ },
958
+ aliases: {}
959
+ };
960
+ }
961
+ function getDefaultState() {
962
+ return {
963
+ version: 1,
964
+ managedEntries: {}
965
+ };
966
+ }
967
+ function loadConfig(runtime) {
968
+ const config = readJsonFile(runtime.configPath);
969
+ if (!config) {
970
+ return getDefaultConfig(runtime.homeDir);
971
+ }
972
+ return {
973
+ ...getDefaultConfig(runtime.homeDir),
974
+ ...config,
975
+ projectsRoots: (config.projectsRoots || []).map((root) => expandHomePath(root, runtime.homeDir)),
976
+ discovery: {
977
+ ignorePathPrefixes: (config.discovery?.ignorePathPrefixes || []).map((path) => expandHomePath(path, runtime.homeDir)),
978
+ preferPathPrefixes: (config.discovery?.preferPathPrefixes || []).map((path) => expandHomePath(path, runtime.homeDir))
979
+ },
980
+ harnesses: {
981
+ custom: config.harnesses?.custom || []
982
+ },
983
+ aliases: config.aliases || {}
984
+ };
985
+ }
986
+ function saveConfig(runtime, config) {
987
+ ensureDir(runtime.stateDir);
988
+ writeJsonFile(runtime.configPath, config);
989
+ }
990
+ function loadState(runtime) {
991
+ return readJsonFile(runtime.statePath) || getDefaultState();
992
+ }
993
+ function saveState(runtime, state) {
994
+ ensureDir(runtime.stateDir);
995
+ writeJsonFile(runtime.statePath, state);
996
+ }
997
+ function initConfig(runtime) {
998
+ const config = loadConfig(runtime);
999
+ saveConfig(runtime, config);
1000
+ if (!existsSync3(runtime.statePath)) {
1001
+ saveState(runtime, getDefaultState());
1002
+ }
1003
+ return config;
1004
+ }
1005
+ function addProjectsRoot(runtime, rootPath) {
1006
+ const config = loadConfig(runtime);
1007
+ const normalized = resolve3(expandHomePath(rootPath, runtime.homeDir));
1008
+ if (!config.projectsRoots.includes(normalized)) {
1009
+ config.projectsRoots.push(normalized);
1010
+ config.projectsRoots.sort();
1011
+ }
1012
+ saveConfig(runtime, config);
1013
+ return config;
1014
+ }
1015
+ function removeProjectsRoot(runtime, rootPath) {
1016
+ const config = loadConfig(runtime);
1017
+ const normalized = resolve3(expandHomePath(rootPath, runtime.homeDir));
1018
+ config.projectsRoots = config.projectsRoots.filter((root) => resolve3(root) !== normalized);
1019
+ saveConfig(runtime, config);
1020
+ return config;
1021
+ }
1022
+ function addHarness(runtime, id, rootPath) {
1023
+ const config = loadConfig(runtime);
1024
+ const normalized = resolve3(expandHomePath(rootPath, runtime.homeDir));
1025
+ const remaining = config.harnesses.custom.filter((entry) => entry.id !== id);
1026
+ remaining.push({ id, rootPath: normalized, enabled: true });
1027
+ remaining.sort((a, b) => a.id.localeCompare(b.id));
1028
+ config.harnesses.custom = remaining;
1029
+ saveConfig(runtime, config);
1030
+ return config;
1031
+ }
1032
+ function removeHarness(runtime, id) {
1033
+ const config = loadConfig(runtime);
1034
+ config.harnesses.custom = config.harnesses.custom.filter((entry) => entry.id !== id);
1035
+ saveConfig(runtime, config);
1036
+ return config;
1037
+ }
1038
+
1039
+ // src/core/harnesses.ts
1040
+ import { existsSync as existsSync4 } from "node:fs";
1041
+ var BUILT_IN_HARNESSES = [
1042
+ { id: "agents", label: "Agents", rootPath: "~/.agents/skills", aliases: ["cline", "warp", "amp", "kimi-cli", "replit", "universal"] },
1043
+ { id: "antigravity", label: "Antigravity", rootPath: "~/.gemini/antigravity/skills" },
1044
+ { id: "claude-code", label: "Claude Code", rootPath: "~/.claude/skills" },
1045
+ { id: "codex", label: "Codex", rootPath: "~/.codex/skills" },
1046
+ { id: "cursor", label: "Cursor", rootPath: "~/.cursor/skills" },
1047
+ { id: "droid", label: "Droid", rootPath: "~/.factory/skills" },
1048
+ { id: "gemini-cli", label: "Gemini CLI", rootPath: "~/.gemini/skills" },
1049
+ { id: "github-copilot", label: "GitHub Copilot", rootPath: "~/.copilot/skills" },
1050
+ { id: "hermes", label: "Hermes", rootPath: "~/.hermes/skills" },
1051
+ { id: "skills", label: "Skills Root", rootPath: "~/.skills" }
1052
+ ];
1053
+ function resolveHarnesses(homeDir, config) {
1054
+ const builtIns = BUILT_IN_HARNESSES.map((entry) => {
1055
+ const rootPath = expandHomePath(entry.rootPath, homeDir);
1056
+ return {
1057
+ id: entry.id,
1058
+ label: entry.label,
1059
+ rootPath,
1060
+ aliases: entry.aliases,
1061
+ kind: "built-in",
1062
+ detected: existsSync4(rootPath),
1063
+ enabled: existsSync4(rootPath)
1064
+ };
1065
+ });
1066
+ const custom = (config.harnesses.custom || []).map((entry) => {
1067
+ const rootPath = expandHomePath(entry.rootPath, homeDir);
1068
+ return {
1069
+ id: entry.id,
1070
+ label: entry.label || entry.id,
1071
+ rootPath,
1072
+ kind: "custom",
1073
+ detected: existsSync4(rootPath),
1074
+ enabled: entry.enabled !== false
1075
+ };
1076
+ });
1077
+ const merged = new Map;
1078
+ for (const harness of [...builtIns, ...custom]) {
1079
+ merged.set(harness.id, harness);
1080
+ }
1081
+ return [...merged.values()].sort((a, b) => a.id.localeCompare(b.id));
1082
+ }
1083
+ function filterHarnesses(harnesses, selectedIds) {
1084
+ if (selectedIds.length === 0) {
1085
+ return harnesses.filter((harness) => harness.enabled);
1086
+ }
1087
+ const selected = new Set(selectedIds);
1088
+ return harnesses.filter((harness) => selected.has(harness.id));
1089
+ }
1090
+
1091
+ // src/core/sources.ts
1092
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
1093
+ import { basename as basename2, join as join4, relative as relative2, resolve as resolve4 } from "node:path";
1094
+ function discoverSkillSet(config) {
1095
+ const discovered = [];
1096
+ const discovery = getDiscoveryConfig(config);
1097
+ for (const projectsRoot of config.projectsRoots) {
1098
+ for (const repoPath of listImmediateDirectories(projectsRoot)) {
1099
+ const topLevelSkill = join4(repoPath, "SKILL.md");
1100
+ if (existsSync5(topLevelSkill)) {
1101
+ discovered.push(buildDiscoveredSkill(projectsRoot, repoPath, repoPath, topLevelSkill, "repo-root"));
1102
+ }
1103
+ const nestedSkillsRoot = join4(repoPath, "skills");
1104
+ for (const nestedSkillDir of listImmediateDirectories(nestedSkillsRoot)) {
1105
+ const nestedSkillFile = join4(nestedSkillDir, "SKILL.md");
1106
+ if (!existsSync5(nestedSkillFile)) {
1107
+ continue;
1108
+ }
1109
+ discovered.push(buildDiscoveredSkill(projectsRoot, repoPath, nestedSkillDir, nestedSkillFile, "nested"));
1110
+ }
1111
+ }
1112
+ }
1113
+ const filtered = discovered.filter((skill) => !isIgnoredSource(skill.sourcePath, discovery.ignorePathPrefixes));
1114
+ const deduped = new Map;
1115
+ for (const skill of filtered) {
1116
+ const key = `${skill.repoPath}::${skill.canonicalSlug}`;
1117
+ const existing = deduped.get(key);
1118
+ if (!existing) {
1119
+ deduped.set(key, skill);
1120
+ continue;
1121
+ }
1122
+ if (existing.sourceType === "nested" && skill.sourceType === "repo-root") {
1123
+ deduped.set(key, skill);
1124
+ }
1125
+ }
1126
+ const { skills, sourceDiagnostics } = resolveGlobalDuplicates([...deduped.values()], discovery.preferPathPrefixes);
1127
+ return {
1128
+ skills: skills.sort((a, b) => a.sourceKey.localeCompare(b.sourceKey)),
1129
+ sourceDiagnostics
1130
+ };
1131
+ }
1132
+ function buildDiscoveredSkill(projectsRoot, repoPath, sourcePath, skillFilePath, sourceType) {
1133
+ const metadataName = parseSkillFrontmatterName(skillFilePath);
1134
+ const contentHash = hashContent(readFileSync3(skillFilePath, "utf8"));
1135
+ const fallbackName = sourceType === "repo-root" ? basename2(repoPath) : basename2(sourcePath);
1136
+ const canonicalSlug = slugify(metadataName || fallbackName);
1137
+ const sourceKey = resolve4(sourcePath);
1138
+ return {
1139
+ sourceKey,
1140
+ sourcePath: resolve4(sourcePath),
1141
+ skillFilePath: resolve4(skillFilePath),
1142
+ repoPath: resolve4(repoPath),
1143
+ projectsRoot: resolve4(projectsRoot),
1144
+ sourceType,
1145
+ metadataName,
1146
+ canonicalSlug,
1147
+ contentHash
1148
+ };
1149
+ }
1150
+ function describeSkill(skill) {
1151
+ const repoRelative = relative2(skill.projectsRoot, skill.sourcePath) || basename2(skill.sourcePath);
1152
+ return `${skill.canonicalSlug} <= ${repoRelative}`;
1153
+ }
1154
+ function isIgnoredSource(sourcePath, ignorePrefixes) {
1155
+ return ignorePrefixes.some((prefix) => sourcePath === prefix || sourcePath.startsWith(`${prefix}/`));
1156
+ }
1157
+ function resolveGlobalDuplicates(skills, preferPrefixes) {
1158
+ const grouped = new Map;
1159
+ for (const skill of skills) {
1160
+ const group = grouped.get(skill.canonicalSlug) || [];
1161
+ group.push(skill);
1162
+ grouped.set(skill.canonicalSlug, group);
1163
+ }
1164
+ const resolved = [];
1165
+ const warnings = [];
1166
+ const errors = [];
1167
+ for (const group of grouped.values()) {
1168
+ if (group.length === 1) {
1169
+ resolved.push(group[0]);
1170
+ continue;
1171
+ }
1172
+ const distinctHashes = new Set(group.map((skill) => skill.contentHash));
1173
+ if (distinctHashes.size !== 1) {
1174
+ resolved.push(...group);
1175
+ errors.push({
1176
+ slug: group[0].canonicalSlug,
1177
+ severity: "error",
1178
+ resolution: "unresolved",
1179
+ sourcePaths: group.map((skill) => skill.sourcePath).sort()
1180
+ });
1181
+ continue;
1182
+ }
1183
+ const sorted = [...group].sort((a, b) => compareDiscoveredSkills(a, b, preferPrefixes));
1184
+ resolved.push(sorted[0]);
1185
+ warnings.push({
1186
+ slug: group[0].canonicalSlug,
1187
+ severity: "warning",
1188
+ resolution: "resolved-by-preference",
1189
+ chosenSourcePath: sorted[0].sourcePath,
1190
+ sourcePaths: group.map((skill) => skill.sourcePath).sort()
1191
+ });
1192
+ }
1193
+ return {
1194
+ skills: resolved,
1195
+ sourceDiagnostics: {
1196
+ warnings: warnings.sort(compareDiagnostics),
1197
+ errors: errors.sort(compareDiagnostics)
1198
+ }
1199
+ };
1200
+ }
1201
+ function getDiscoveryConfig(config) {
1202
+ return {
1203
+ ignorePathPrefixes: config.discovery?.ignorePathPrefixes ?? [],
1204
+ preferPathPrefixes: config.discovery?.preferPathPrefixes ?? []
1205
+ };
1206
+ }
1207
+ function compareDiscoveredSkills(a, b, preferPrefixes) {
1208
+ const rankA = getPreferenceRank(a.sourcePath, preferPrefixes);
1209
+ const rankB = getPreferenceRank(b.sourcePath, preferPrefixes);
1210
+ if (rankA !== rankB) {
1211
+ return rankA - rankB;
1212
+ }
1213
+ return a.sourcePath.localeCompare(b.sourcePath);
1214
+ }
1215
+ function getPreferenceRank(sourcePath, preferPrefixes) {
1216
+ const matchIndex = preferPrefixes.findIndex((prefix) => sourcePath === prefix || sourcePath.startsWith(`${prefix}/`));
1217
+ return matchIndex === -1 ? Number.MAX_SAFE_INTEGER : matchIndex;
1218
+ }
1219
+ function compareDiagnostics(a, b) {
1220
+ return a.slug.localeCompare(b.slug) || a.sourcePaths.join(`
1221
+ `).localeCompare(b.sourcePaths.join(`
1222
+ `));
1223
+ }
1224
+
1225
+ // src/core/sync.ts
1226
+ import { join as join5, resolve as resolve5 } from "node:path";
1227
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync4, symlinkSync as symlinkSync2 } from "node:fs";
1228
+ function buildSyncPlan(skills, harnesses, config, state, sourceDiagnostics) {
1229
+ const harnessPlans = harnesses.map((harness) => ({
1230
+ harness,
1231
+ entries: []
1232
+ }));
1233
+ const desiredByHarness = new Map;
1234
+ let conflicts = 0;
1235
+ let changes = 0;
1236
+ let ok = 0;
1237
+ for (const harnessPlan of harnessPlans) {
1238
+ desiredByHarness.set(harnessPlan.harness.id, new Set);
1239
+ const pathClaims = new Map;
1240
+ for (const skill of skills) {
1241
+ const installName = resolveInstallName(skill, harnessPlan.harness.id, config);
1242
+ const destinationPath = join5(harnessPlan.harness.rootPath, installName);
1243
+ const existingClaim = pathClaims.get(destinationPath);
1244
+ if (existingClaim) {
1245
+ harnessPlan.entries.push({
1246
+ harnessId: harnessPlan.harness.id,
1247
+ harnessRoot: harnessPlan.harness.rootPath,
1248
+ installName,
1249
+ destinationPath,
1250
+ action: "conflict",
1251
+ sourcePath: skill.sourcePath,
1252
+ sourceKey: skill.sourceKey,
1253
+ message: `slug collision between ${existingClaim.sourcePath} and ${skill.sourcePath}`
1254
+ });
1255
+ conflicts += 1;
1256
+ continue;
1257
+ }
1258
+ pathClaims.set(destinationPath, skill);
1259
+ desiredByHarness.get(harnessPlan.harness.id)?.add(destinationPath);
1260
+ const planned = buildPlannedEntry(skill, harnessPlan.harness, installName, destinationPath, state, config);
1261
+ harnessPlan.entries.push(planned);
1262
+ if (planned.action === "conflict") {
1263
+ conflicts += 1;
1264
+ } else if (planned.action === "ok") {
1265
+ ok += 1;
1266
+ } else {
1267
+ changes += 1;
1268
+ }
1269
+ }
1270
+ for (const [entryPath, managed] of Object.entries(state.managedEntries)) {
1271
+ if (managed.harnessId !== harnessPlan.harness.id) {
1272
+ continue;
1273
+ }
1274
+ if (desiredByHarness.get(harnessPlan.harness.id)?.has(entryPath)) {
1275
+ continue;
1276
+ }
1277
+ const inspection = inspectEntry(entryPath);
1278
+ harnessPlan.entries.push({
1279
+ harnessId: harnessPlan.harness.id,
1280
+ harnessRoot: harnessPlan.harness.rootPath,
1281
+ installName: managed.installName,
1282
+ destinationPath: entryPath,
1283
+ action: inspection.exists ? "remove-managed" : "prune-state",
1284
+ sourcePath: managed.sourcePath,
1285
+ message: inspection.exists ? "managed entry is stale and will be removed" : "stale state entry will be pruned"
1286
+ });
1287
+ changes += 1;
1288
+ }
1289
+ harnessPlan.entries.sort((a, b) => a.destinationPath.localeCompare(b.destinationPath));
1290
+ }
1291
+ return {
1292
+ harnesses: harnessPlans,
1293
+ changes,
1294
+ conflicts,
1295
+ ok,
1296
+ sourceDiagnostics: sourceDiagnostics || { warnings: [], errors: [] }
1297
+ };
1298
+ }
1299
+ function buildPlannedEntry(skill, harness, installName, destinationPath, state, config) {
1300
+ const inspection = inspectEntry(destinationPath);
1301
+ const stateEntry = state.managedEntries[destinationPath];
1302
+ const sameSource = inspection.type === "symlink" && inspection.resolvedTarget === resolve5(skill.sourcePath);
1303
+ const compatibility = inspectCompatibility(destinationPath, skill);
1304
+ if (!inspection.exists) {
1305
+ return makePlannedEntry(skill, harness, installName, destinationPath, "create", "missing entry will be created");
1306
+ }
1307
+ if (sameSource) {
1308
+ return makePlannedEntry(skill, harness, installName, destinationPath, "ok", "already synced");
1309
+ }
1310
+ if (compatibility === "matching-skill") {
1311
+ return makePlannedEntry(skill, harness, installName, destinationPath, "ok", "compatible existing install");
1312
+ }
1313
+ if (stateEntry) {
1314
+ return makePlannedEntry(skill, harness, installName, destinationPath, "repair", "managed entry drift will be repaired");
1315
+ }
1316
+ if (compatibility === "empty-directory") {
1317
+ return makePlannedEntry(skill, harness, installName, destinationPath, "repair", "empty directory will be replaced");
1318
+ }
1319
+ return makePlannedEntry(skill, harness, installName, destinationPath, "conflict", inspection.type === "symlink" ? `existing symlink points elsewhere: ${inspection.linkTarget || "unknown target"}` : `existing ${inspection.type} is unmanaged`);
1320
+ }
1321
+ function inspectCompatibility(destinationPath, skill) {
1322
+ const inspection = inspectEntry(destinationPath);
1323
+ if (!inspection.exists) {
1324
+ return "none";
1325
+ }
1326
+ const sourceSkillText = readFileSync4(skill.skillFilePath, "utf8");
1327
+ if (inspection.type === "directory") {
1328
+ if (readdirSync3(destinationPath).length === 0) {
1329
+ return "empty-directory";
1330
+ }
1331
+ const installedSkillPath = join5(destinationPath, "SKILL.md");
1332
+ if (existsSync6(installedSkillPath) && readFileSync4(installedSkillPath, "utf8") === sourceSkillText) {
1333
+ return "matching-skill";
1334
+ }
1335
+ return "none";
1336
+ }
1337
+ if (inspection.type === "file") {
1338
+ return readFileSync4(destinationPath, "utf8") === sourceSkillText ? "matching-skill" : "none";
1339
+ }
1340
+ if (inspection.type === "symlink" && inspection.resolvedTarget) {
1341
+ const installedSkillPath = join5(inspection.resolvedTarget, "SKILL.md");
1342
+ if (existsSync6(installedSkillPath) && readFileSync4(installedSkillPath, "utf8") === sourceSkillText) {
1343
+ return "matching-skill";
1344
+ }
1345
+ }
1346
+ return "none";
1347
+ }
1348
+ function makePlannedEntry(skill, harness, installName, destinationPath, action, message) {
1349
+ return {
1350
+ harnessId: harness.id,
1351
+ harnessRoot: harness.rootPath,
1352
+ installName,
1353
+ destinationPath,
1354
+ action,
1355
+ sourcePath: skill.sourcePath,
1356
+ sourceKey: skill.sourceKey,
1357
+ message
1358
+ };
1359
+ }
1360
+ function resolveInstallName(skill, harnessId, config) {
1361
+ const override = config.aliases[skill.sourceKey];
1362
+ if (override?.harnesses?.[harnessId]) {
1363
+ return override.harnesses[harnessId];
1364
+ }
1365
+ if (override?.default) {
1366
+ return override.default;
1367
+ }
1368
+ return skill.canonicalSlug;
1369
+ }
1370
+ function applySyncPlan(plan, state, dryRun) {
1371
+ const nextState = {
1372
+ version: state.version,
1373
+ managedEntries: { ...state.managedEntries }
1374
+ };
1375
+ for (const harnessPlan of plan.harnesses) {
1376
+ ensureDir(harnessPlan.harness.rootPath);
1377
+ for (const entry of harnessPlan.entries) {
1378
+ if (entry.action === "ok" || entry.action === "conflict") {
1379
+ continue;
1380
+ }
1381
+ if (entry.action === "prune-state") {
1382
+ delete nextState.managedEntries[entry.destinationPath];
1383
+ continue;
1384
+ }
1385
+ if (dryRun) {
1386
+ continue;
1387
+ }
1388
+ if (entry.action === "remove-managed") {
1389
+ removePath(entry.destinationPath);
1390
+ delete nextState.managedEntries[entry.destinationPath];
1391
+ continue;
1392
+ }
1393
+ removePath(entry.destinationPath);
1394
+ symlinkSync2(entry.sourcePath, entry.destinationPath);
1395
+ nextState.managedEntries[entry.destinationPath] = {
1396
+ harnessId: entry.harnessId,
1397
+ sourcePath: entry.sourcePath,
1398
+ installName: entry.installName,
1399
+ updatedAt: nowIso()
1400
+ };
1401
+ }
1402
+ }
1403
+ return nextState;
1404
+ }
1405
+ function countPlanActions(plan) {
1406
+ const counts = {};
1407
+ for (const harness of plan.harnesses) {
1408
+ for (const entry of harness.entries) {
1409
+ counts[entry.action] = (counts[entry.action] || 0) + 1;
1410
+ }
1411
+ }
1412
+ return counts;
1413
+ }
1414
+ function hasConflicts(plan) {
1415
+ return plan.conflicts > 0 || plan.sourceDiagnostics.errors.length > 0;
1416
+ }
1417
+ function hasDrift(plan) {
1418
+ return plan.changes > 0;
1419
+ }
1420
+
1421
+ // src/index.ts
1422
+ var cli = cac("skill-sync");
1423
+ var version = "0.1.1";
1424
+ function normalizeList(value) {
1425
+ if (!value) {
1426
+ return [];
1427
+ }
1428
+ const raw = Array.isArray(value) ? value : [value];
1429
+ return raw.flatMap((item) => item.split(",")).map((item) => item.trim()).filter(Boolean);
1430
+ }
1431
+ function withRuntime(options, fn) {
1432
+ const runtime = buildRuntimeContext({ home: options.home, json: options.json });
1433
+ mkdirSync3(runtime.stateDir, { recursive: true });
1434
+ return fn(runtime);
1435
+ }
1436
+ function resolveProjectsOverride(configProjectsRoots, options) {
1437
+ const override = normalizeList(options.projectsRoot);
1438
+ return override.length > 0 ? override : configProjectsRoots;
1439
+ }
1440
+ function resolveSelectedHarnesses(allHarnesses, options) {
1441
+ return filterHarnesses(allHarnesses, normalizeList(options.harness));
1442
+ }
1443
+ function print(value, json) {
1444
+ if (json) {
1445
+ console.log(typeof value === "string" ? JSON.stringify({ message: value }, null, 2) : JSON.stringify(value, null, 2));
1446
+ return;
1447
+ }
1448
+ console.log(value);
1449
+ }
1450
+ function renderPlan(plan) {
1451
+ const lines = [];
1452
+ appendSourceDiagnostics(lines, plan.sourceDiagnostics);
1453
+ const counts = countPlanActions(plan);
1454
+ if (lines.length > 0) {
1455
+ lines.push("");
1456
+ }
1457
+ lines.push(`Summary: ${plan.ok} ok, ${plan.changes} change(s), ${plan.conflicts} conflict(s)`);
1458
+ lines.push(`Actions: ${Object.entries(counts).map(([action, count]) => `${action}=${count}`).join(", ")}`);
1459
+ for (const harnessPlan of plan.harnesses) {
1460
+ lines.push("");
1461
+ lines.push(`${harnessPlan.harness.id} ${harnessPlan.harness.rootPath}`);
1462
+ const interestingEntries = harnessPlan.entries.filter((entry) => entry.action !== "ok");
1463
+ const entriesToShow = interestingEntries.length > 0 ? interestingEntries : harnessPlan.entries;
1464
+ for (const entry of entriesToShow) {
1465
+ const sourceSuffix = entry.sourcePath ? ` <= ${entry.sourcePath}` : "";
1466
+ lines.push(` ${entry.action.padEnd(14)} ${entry.installName}${sourceSuffix}`);
1467
+ if (entry.message !== "already synced") {
1468
+ lines.push(` ${entry.message}`);
1469
+ }
1470
+ }
1471
+ }
1472
+ return lines.join(`
1473
+ `);
1474
+ }
1475
+ function appendSourceDiagnostics(lines, sourceDiagnostics) {
1476
+ if (sourceDiagnostics.errors.length === 0 && sourceDiagnostics.warnings.length === 0) {
1477
+ return;
1478
+ }
1479
+ if (sourceDiagnostics.errors.length > 0) {
1480
+ lines.push("Source errors:");
1481
+ for (const diagnostic of sourceDiagnostics.errors) {
1482
+ appendSourceDiagnostic(lines, diagnostic);
1483
+ }
1484
+ }
1485
+ if (sourceDiagnostics.warnings.length > 0) {
1486
+ if (lines.length > 0) {
1487
+ lines.push("");
1488
+ }
1489
+ lines.push("Source warnings:");
1490
+ for (const diagnostic of sourceDiagnostics.warnings) {
1491
+ appendSourceDiagnostic(lines, diagnostic);
1492
+ }
1493
+ }
1494
+ }
1495
+ function appendSourceDiagnostic(lines, diagnostic) {
1496
+ lines.push(`- duplicate slug: ${diagnostic.slug}`);
1497
+ for (const sourcePath of diagnostic.sourcePaths) {
1498
+ lines.push(` ${sourcePath}`);
1499
+ }
1500
+ if (diagnostic.resolution === "resolved-by-preference" && diagnostic.chosenSourcePath) {
1501
+ lines.push(` resolved by preference: ${diagnostic.chosenSourcePath}`);
1502
+ return;
1503
+ }
1504
+ lines.push(" sync blocked until one source is excluded or preferred");
1505
+ }
1506
+ function planSync(options) {
1507
+ return withRuntime(options, (runtime) => {
1508
+ const config = loadConfig(runtime);
1509
+ config.projectsRoots = resolveProjectsOverride(config.projectsRoots, options);
1510
+ const harnesses = resolveSelectedHarnesses(resolveHarnesses(runtime.homeDir, config), options);
1511
+ const { skills, sourceDiagnostics } = discoverSkillSet(config);
1512
+ const state = loadState(runtime);
1513
+ const plan = buildSyncPlan(skills, harnesses, config, state, sourceDiagnostics);
1514
+ return { runtime, plan, harnesses, skillsCount: skills.length };
1515
+ });
1516
+ }
1517
+ function printCheckResult(plan, json) {
1518
+ print(json ? plan : renderPlan(plan), json);
1519
+ process.exit(hasConflicts(plan) ? 3 : hasDrift(plan) ? 2 : 0);
1520
+ }
1521
+ cli.command("check", "Show drift without changing anything").option("--json", "Output JSON").option("--dry-run", "Accepted for parity; check is always read-only").option("--projects-root <path>", "Override configured projects root").option("--harness <id>", "Filter to one or more harness ids").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action((options) => {
1522
+ const { plan } = planSync(options);
1523
+ printCheckResult(plan, Boolean(options.json));
1524
+ });
1525
+ cli.command("sync", "Apply the desired symlink state").option("--json", "Output JSON").option("--dry-run", "Show changes without mutating").option("--projects-root <path>", "Override configured projects root").option("--harness <id>", "Filter to one or more harness ids").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action((options) => {
1526
+ const { runtime, plan } = planSync(options);
1527
+ if (hasConflicts(plan)) {
1528
+ print(options.json ? plan : renderPlan(plan), Boolean(options.json));
1529
+ process.exit(3);
1530
+ }
1531
+ const state = loadState(runtime);
1532
+ const nextState = applySyncPlan(plan, state, Boolean(options.dryRun));
1533
+ if (!options.dryRun) {
1534
+ saveState(runtime, nextState);
1535
+ }
1536
+ print(options.json ? plan : renderPlan(plan), Boolean(options.json));
1537
+ });
1538
+ cli.command("sources", "List discovered source skills").option("--json", "Output JSON").option("--projects-root <path>", "Override configured projects root").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action((options) => {
1539
+ withRuntime(options, (runtime) => {
1540
+ const config = loadConfig(runtime);
1541
+ config.projectsRoots = resolveProjectsOverride(config.projectsRoots, options);
1542
+ const { skills, sourceDiagnostics } = discoverSkillSet(config);
1543
+ if (options.json) {
1544
+ print({ skills, sourceDiagnostics }, true);
1545
+ return;
1546
+ }
1547
+ console.log(`Discovered ${skills.length} skill source(s)`);
1548
+ const sourceLines = [];
1549
+ appendSourceDiagnostics(sourceLines, sourceDiagnostics);
1550
+ if (sourceLines.length > 0) {
1551
+ console.log(sourceLines.join(`
1552
+ `));
1553
+ console.log("");
1554
+ }
1555
+ for (const skill of skills) {
1556
+ console.log(`- ${describeSkill(skill)}`);
1557
+ }
1558
+ });
1559
+ });
1560
+ cli.command("harnesses", "List known harness roots and detection status").option("--json", "Output JSON").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action((options) => {
1561
+ withRuntime(options, (runtime) => {
1562
+ const harnesses = resolveHarnesses(runtime.homeDir, loadConfig(runtime));
1563
+ if (options.json) {
1564
+ print(harnesses, true);
1565
+ return;
1566
+ }
1567
+ for (const harness of harnesses) {
1568
+ console.log(`${harness.id} ${harness.rootPath}`);
1569
+ console.log(` kind: ${harness.kind}`);
1570
+ console.log(` detected: ${harness.detected ? "yes" : "no"}`);
1571
+ console.log(` enabled: ${harness.enabled ? "yes" : "no"}`);
1572
+ }
1573
+ });
1574
+ });
1575
+ cli.command("backup <action> [target]", "Backup commands: create, list, restore").option("--json", "Output JSON").option("--dry-run", "Show what would happen without mutating").option("--home <path>", "Override HOME for skill-sync state and harness resolution").option("--harness <id>", "Filter to one or more harness ids").action((action, target, options) => {
1576
+ withRuntime(options, (runtime) => {
1577
+ if (action === "create") {
1578
+ const config = loadConfig(runtime);
1579
+ const harnesses = resolveSelectedHarnesses(resolveHarnesses(runtime.homeDir, config), options);
1580
+ const manifest = createBackup(runtime, harnesses, loadState(runtime));
1581
+ if (options.json) {
1582
+ print(manifest, true);
1583
+ return;
1584
+ }
1585
+ console.log(`Created backup ${manifest.id}`);
1586
+ for (const harness of manifest.harnesses) {
1587
+ console.log(`- ${harness.id}: ${harness.entries.length} entr${harness.entries.length === 1 ? "y" : "ies"}`);
1588
+ }
1589
+ return;
1590
+ }
1591
+ if (action === "list") {
1592
+ const backups = listBackups(runtime);
1593
+ if (options.json) {
1594
+ print(backups, true);
1595
+ return;
1596
+ }
1597
+ if (backups.length === 0) {
1598
+ console.log("No backups found");
1599
+ return;
1600
+ }
1601
+ for (const backupEntry of backups) {
1602
+ console.log(`${backupEntry.id} ${backupEntry.createdAt}`);
1603
+ console.log(` harnesses: ${backupEntry.harnesses.map((harness) => harness.id).join(", ") || "-"}`);
1604
+ }
1605
+ return;
1606
+ }
1607
+ if (action === "restore") {
1608
+ if (!target) {
1609
+ throw new Error("backup restore requires a backup id");
1610
+ }
1611
+ const { manifest, nextState } = restoreBackup(runtime, target, normalizeList(options.harness), Boolean(options.dryRun), loadState(runtime));
1612
+ if (!options.dryRun) {
1613
+ saveState(runtime, nextState);
1614
+ }
1615
+ if (options.json) {
1616
+ print(manifest, true);
1617
+ return;
1618
+ }
1619
+ console.log(`${options.dryRun ? "Would restore" : "Restored"} backup ${manifest.id}`);
1620
+ const selectedIds = normalizeList(options.harness);
1621
+ for (const harness of manifest.harnesses) {
1622
+ if (selectedIds.length > 0 && !selectedIds.includes(harness.id)) {
1623
+ continue;
1624
+ }
1625
+ console.log(`- ${harness.id}: ${harness.entries.length} entr${harness.entries.length === 1 ? "y" : "ies"}`);
1626
+ }
1627
+ return;
1628
+ }
1629
+ throw new Error(`Unknown backup action: ${action}`);
1630
+ });
1631
+ });
1632
+ cli.command("config <action>", "Config commands: init").option("--json", "Output JSON").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action((action, options) => {
1633
+ if (action !== "init") {
1634
+ throw new Error(`Unknown config action: ${action}`);
1635
+ }
1636
+ withRuntime(options, (runtime) => {
1637
+ const config = initConfig(runtime);
1638
+ print(config, Boolean(options.json));
1639
+ });
1640
+ });
1641
+ cli.command("harness <action> [id] [rootPath]", "Harness commands: list, add, remove").option("--json", "Output JSON").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action((action, id, rootPath, options) => {
1642
+ if (action === "list") {
1643
+ withRuntime(options, (runtime) => {
1644
+ const harnesses = resolveHarnesses(runtime.homeDir, loadConfig(runtime));
1645
+ print(options.json ? harnesses : harnesses.map((item) => `${item.id} ${item.rootPath}`).join(`
1646
+ `), Boolean(options.json));
1647
+ });
1648
+ return;
1649
+ }
1650
+ if (!id) {
1651
+ throw new Error(`harness ${action} requires an id`);
1652
+ }
1653
+ if (action === "add") {
1654
+ if (!rootPath) {
1655
+ throw new Error("harness add requires a root path");
1656
+ }
1657
+ const config = withRuntime(options, (runtime) => addHarness(runtime, id, rootPath));
1658
+ print(config, Boolean(options.json));
1659
+ return;
1660
+ }
1661
+ if (action === "remove") {
1662
+ const config = withRuntime(options, (runtime) => removeHarness(runtime, id));
1663
+ print(config, Boolean(options.json));
1664
+ return;
1665
+ }
1666
+ throw new Error(`Unknown harness action: ${action}`);
1667
+ });
1668
+ cli.command("roots <action> [rootPath]", "Projects root commands: list, add, remove").option("--json", "Output JSON").option("--home <path>", "Override HOME for skill-sync state and harness resolution").action((action, rootPath, options) => {
1669
+ if (action === "list") {
1670
+ withRuntime(options, (runtime) => {
1671
+ const config = loadConfig(runtime);
1672
+ print(config.projectsRoots, Boolean(options.json));
1673
+ });
1674
+ return;
1675
+ }
1676
+ if (!rootPath) {
1677
+ throw new Error(`roots ${action} requires a path`);
1678
+ }
1679
+ if (action === "add") {
1680
+ const config = withRuntime(options, (runtime) => addProjectsRoot(runtime, rootPath));
1681
+ print(config, Boolean(options.json));
1682
+ return;
1683
+ }
1684
+ if (action === "remove") {
1685
+ const config = withRuntime(options, (runtime) => removeProjectsRoot(runtime, rootPath));
1686
+ print(config, Boolean(options.json));
1687
+ return;
1688
+ }
1689
+ throw new Error(`Unknown roots action: ${action}`);
1690
+ });
1691
+ cli.help();
1692
+ cli.version(version);
1693
+ cli.option("--json", "Output JSON");
1694
+ cli.option("--dry-run", "Show changes without mutating");
1695
+ cli.option("--projects-root <path>", "Override configured projects root");
1696
+ cli.option("--harness <id>", "Filter to one or more harness ids");
1697
+ cli.option("--home <path>", "Override HOME for skill-sync state and harness resolution");
1698
+ var rawArgs = process.argv.slice(2);
1699
+ cli.parse();
1700
+ var shouldRunDefaultSync = !rawArgs.includes("--help") && !rawArgs.includes("-h") && !rawArgs.includes("--version") && !rawArgs.includes("-v") && !cli.matchedCommand;
1701
+ if (shouldRunDefaultSync) {
1702
+ const options = cli.options;
1703
+ const { runtime, plan } = planSync(options);
1704
+ if (hasConflicts(plan)) {
1705
+ console.log(renderPlan(plan));
1706
+ process.exit(3);
1707
+ }
1708
+ const nextState = applySyncPlan(plan, loadState(runtime), Boolean(options.dryRun));
1709
+ if (!options.dryRun) {
1710
+ saveState(runtime, nextState);
1711
+ }
1712
+ console.log(renderPlan(plan));
1713
+ }