rulesync 7.15.1 → 7.16.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/README.md +1 -1
- package/dist/{chunk-L5AQUWUM.js → chunk-E5YWRHGW.js} +146 -40
- package/dist/cli/index.cjs +313 -174
- package/dist/cli/index.js +173 -138
- package/dist/index.cjs +144 -41
- package/dist/index.js +1 -1
- package/package.json +11 -11
package/dist/cli/index.cjs
CHANGED
|
@@ -408,6 +408,7 @@ var import_node_path5 = require("path");
|
|
|
408
408
|
|
|
409
409
|
// src/utils/frontmatter.ts
|
|
410
410
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
411
|
+
var import_js_yaml = require("js-yaml");
|
|
411
412
|
function isPlainObject(value) {
|
|
412
413
|
if (value === null || typeof value !== "object") return false;
|
|
413
414
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -446,8 +447,55 @@ function deepRemoveNullishObject(obj) {
|
|
|
446
447
|
}
|
|
447
448
|
return result;
|
|
448
449
|
}
|
|
449
|
-
function
|
|
450
|
-
|
|
450
|
+
function deepFlattenStringsValue(value) {
|
|
451
|
+
if (value === null || value === void 0) {
|
|
452
|
+
return void 0;
|
|
453
|
+
}
|
|
454
|
+
if (typeof value === "string") {
|
|
455
|
+
return value.replace(/\n+/g, " ").trim();
|
|
456
|
+
}
|
|
457
|
+
if (Array.isArray(value)) {
|
|
458
|
+
const cleanedArray = value.map((item) => deepFlattenStringsValue(item)).filter((item) => item !== void 0);
|
|
459
|
+
return cleanedArray;
|
|
460
|
+
}
|
|
461
|
+
if (isPlainObject(value)) {
|
|
462
|
+
const result = {};
|
|
463
|
+
for (const [key, val] of Object.entries(value)) {
|
|
464
|
+
const cleaned = deepFlattenStringsValue(val);
|
|
465
|
+
if (cleaned !== void 0) {
|
|
466
|
+
result[key] = cleaned;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
return value;
|
|
472
|
+
}
|
|
473
|
+
function deepFlattenStringsObject(obj) {
|
|
474
|
+
if (!obj || typeof obj !== "object") {
|
|
475
|
+
return {};
|
|
476
|
+
}
|
|
477
|
+
const result = {};
|
|
478
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
479
|
+
const cleaned = deepFlattenStringsValue(val);
|
|
480
|
+
if (cleaned !== void 0) {
|
|
481
|
+
result[key] = cleaned;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
function stringifyFrontmatter(body, frontmatter, options) {
|
|
487
|
+
const { avoidBlockScalars = false } = options ?? {};
|
|
488
|
+
const cleanFrontmatter = avoidBlockScalars ? deepFlattenStringsObject(frontmatter) : deepRemoveNullishObject(frontmatter);
|
|
489
|
+
if (avoidBlockScalars) {
|
|
490
|
+
return import_gray_matter.default.stringify(body, cleanFrontmatter, {
|
|
491
|
+
engines: {
|
|
492
|
+
yaml: {
|
|
493
|
+
parse: (input) => (0, import_js_yaml.load)(input) ?? {},
|
|
494
|
+
stringify: (data) => (0, import_js_yaml.dump)(data, { lineWidth: -1 })
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
451
499
|
return import_gray_matter.default.stringify(body, cleanFrontmatter);
|
|
452
500
|
}
|
|
453
501
|
function parseFrontmatter(content, filePath) {
|
|
@@ -1599,7 +1647,7 @@ var CursorCommand = class _CursorCommand extends ToolCommand {
|
|
|
1599
1647
|
}
|
|
1600
1648
|
super({
|
|
1601
1649
|
...rest,
|
|
1602
|
-
fileContent: stringifyFrontmatter(body, frontmatter)
|
|
1650
|
+
fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
|
|
1603
1651
|
});
|
|
1604
1652
|
this.frontmatter = frontmatter;
|
|
1605
1653
|
this.body = body;
|
|
@@ -5516,7 +5564,10 @@ var RulesyncMcp = class _RulesyncMcp extends RulesyncFile {
|
|
|
5516
5564
|
stripMcpServerFields(fields) {
|
|
5517
5565
|
if (fields.length === 0) return this;
|
|
5518
5566
|
const filteredServers = Object.fromEntries(
|
|
5519
|
-
Object.entries(this.json.mcpServers).map(([name, config]) => [
|
|
5567
|
+
Object.entries(this.json.mcpServers).map(([name, config]) => [
|
|
5568
|
+
name,
|
|
5569
|
+
Object.fromEntries(Object.entries(config).filter(([key]) => !fields.includes(key)))
|
|
5570
|
+
])
|
|
5520
5571
|
);
|
|
5521
5572
|
return new _RulesyncMcp({
|
|
5522
5573
|
baseDir: this.baseDir,
|
|
@@ -6022,12 +6073,26 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
|
|
|
6022
6073
|
json;
|
|
6023
6074
|
constructor(params) {
|
|
6024
6075
|
super(params);
|
|
6025
|
-
|
|
6076
|
+
if (this.fileContent !== void 0) {
|
|
6077
|
+
try {
|
|
6078
|
+
this.json = JSON.parse(this.fileContent);
|
|
6079
|
+
} catch (error) {
|
|
6080
|
+
throw new Error(
|
|
6081
|
+
`Failed to parse Cursor MCP config at ${(0, import_node_path46.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`,
|
|
6082
|
+
{ cause: error }
|
|
6083
|
+
);
|
|
6084
|
+
}
|
|
6085
|
+
} else {
|
|
6086
|
+
this.json = {};
|
|
6087
|
+
}
|
|
6026
6088
|
}
|
|
6027
6089
|
getJson() {
|
|
6028
6090
|
return this.json;
|
|
6029
6091
|
}
|
|
6030
|
-
|
|
6092
|
+
isDeletable() {
|
|
6093
|
+
return !this.global;
|
|
6094
|
+
}
|
|
6095
|
+
static getSettablePaths(_options) {
|
|
6031
6096
|
return {
|
|
6032
6097
|
relativeDirPath: ".cursor",
|
|
6033
6098
|
relativeFilePath: "mcp.json"
|
|
@@ -6035,41 +6100,62 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
|
|
|
6035
6100
|
}
|
|
6036
6101
|
static async fromFile({
|
|
6037
6102
|
baseDir = process.cwd(),
|
|
6038
|
-
validate = true
|
|
6103
|
+
validate = true,
|
|
6104
|
+
global = false
|
|
6039
6105
|
}) {
|
|
6040
|
-
const
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
)
|
|
6046
|
-
)
|
|
6106
|
+
const paths = this.getSettablePaths({ global });
|
|
6107
|
+
const filePath = (0, import_node_path46.join)(baseDir, paths.relativeDirPath, paths.relativeFilePath);
|
|
6108
|
+
const fileContent = await readFileContentOrNull(filePath) ?? '{"mcpServers":{}}';
|
|
6109
|
+
let json;
|
|
6110
|
+
try {
|
|
6111
|
+
json = JSON.parse(fileContent);
|
|
6112
|
+
} catch (error) {
|
|
6113
|
+
throw new Error(
|
|
6114
|
+
`Failed to parse Cursor MCP config at ${(0, import_node_path46.join)(paths.relativeDirPath, paths.relativeFilePath)}: ${formatError(error)}`,
|
|
6115
|
+
{ cause: error }
|
|
6116
|
+
);
|
|
6117
|
+
}
|
|
6118
|
+
const newJson = { ...json, mcpServers: json.mcpServers ?? {} };
|
|
6047
6119
|
return new _CursorMcp({
|
|
6048
6120
|
baseDir,
|
|
6049
|
-
relativeDirPath:
|
|
6050
|
-
relativeFilePath:
|
|
6051
|
-
fileContent,
|
|
6052
|
-
validate
|
|
6121
|
+
relativeDirPath: paths.relativeDirPath,
|
|
6122
|
+
relativeFilePath: paths.relativeFilePath,
|
|
6123
|
+
fileContent: JSON.stringify(newJson, null, 2),
|
|
6124
|
+
validate,
|
|
6125
|
+
global
|
|
6053
6126
|
});
|
|
6054
6127
|
}
|
|
6055
|
-
static fromRulesyncMcp({
|
|
6128
|
+
static async fromRulesyncMcp({
|
|
6056
6129
|
baseDir = process.cwd(),
|
|
6057
6130
|
rulesyncMcp,
|
|
6058
|
-
validate = true
|
|
6131
|
+
validate = true,
|
|
6132
|
+
global = false
|
|
6059
6133
|
}) {
|
|
6060
|
-
const
|
|
6061
|
-
const
|
|
6134
|
+
const paths = this.getSettablePaths({ global });
|
|
6135
|
+
const fileContent = await readOrInitializeFileContent(
|
|
6136
|
+
(0, import_node_path46.join)(baseDir, paths.relativeDirPath, paths.relativeFilePath),
|
|
6137
|
+
JSON.stringify({ mcpServers: {} }, null, 2)
|
|
6138
|
+
);
|
|
6139
|
+
let json;
|
|
6140
|
+
try {
|
|
6141
|
+
json = JSON.parse(fileContent);
|
|
6142
|
+
} catch (error) {
|
|
6143
|
+
throw new Error(
|
|
6144
|
+
`Failed to parse Cursor MCP config at ${(0, import_node_path46.join)(paths.relativeDirPath, paths.relativeFilePath)}: ${formatError(error)}`,
|
|
6145
|
+
{ cause: error }
|
|
6146
|
+
);
|
|
6147
|
+
}
|
|
6148
|
+
const rulesyncJson = rulesyncMcp.getJson();
|
|
6149
|
+
const mcpServers = isMcpServers(rulesyncJson.mcpServers) ? rulesyncJson.mcpServers : {};
|
|
6062
6150
|
const transformedServers = convertEnvToCursorFormat(mcpServers);
|
|
6063
|
-
const cursorConfig = {
|
|
6064
|
-
mcpServers: transformedServers
|
|
6065
|
-
};
|
|
6066
|
-
const fileContent = JSON.stringify(cursorConfig, null, 2);
|
|
6151
|
+
const cursorConfig = { ...json, mcpServers: transformedServers };
|
|
6067
6152
|
return new _CursorMcp({
|
|
6068
6153
|
baseDir,
|
|
6069
|
-
relativeDirPath:
|
|
6070
|
-
relativeFilePath:
|
|
6071
|
-
fileContent,
|
|
6072
|
-
validate
|
|
6154
|
+
relativeDirPath: paths.relativeDirPath,
|
|
6155
|
+
relativeFilePath: paths.relativeFilePath,
|
|
6156
|
+
fileContent: JSON.stringify(cursorConfig, null, 2),
|
|
6157
|
+
validate,
|
|
6158
|
+
global
|
|
6073
6159
|
});
|
|
6074
6160
|
}
|
|
6075
6161
|
toRulesyncMcp() {
|
|
@@ -6093,14 +6179,16 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
|
|
|
6093
6179
|
static forDeletion({
|
|
6094
6180
|
baseDir = process.cwd(),
|
|
6095
6181
|
relativeDirPath,
|
|
6096
|
-
relativeFilePath
|
|
6182
|
+
relativeFilePath,
|
|
6183
|
+
global = false
|
|
6097
6184
|
}) {
|
|
6098
6185
|
return new _CursorMcp({
|
|
6099
6186
|
baseDir,
|
|
6100
6187
|
relativeDirPath,
|
|
6101
6188
|
relativeFilePath,
|
|
6102
6189
|
fileContent: "{}",
|
|
6103
|
-
validate: false
|
|
6190
|
+
validate: false,
|
|
6191
|
+
global
|
|
6104
6192
|
});
|
|
6105
6193
|
}
|
|
6106
6194
|
};
|
|
@@ -6929,7 +7017,7 @@ var toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
6929
7017
|
class: CursorMcp,
|
|
6930
7018
|
meta: {
|
|
6931
7019
|
supportsProject: true,
|
|
6932
|
-
supportsGlobal:
|
|
7020
|
+
supportsGlobal: true,
|
|
6933
7021
|
supportsEnabledTools: false,
|
|
6934
7022
|
supportsDisabledTools: false
|
|
6935
7023
|
}
|
|
@@ -7642,9 +7730,15 @@ var import_node_path59 = require("path");
|
|
|
7642
7730
|
var DirFeatureProcessor = class {
|
|
7643
7731
|
baseDir;
|
|
7644
7732
|
dryRun;
|
|
7645
|
-
|
|
7733
|
+
avoidBlockScalars;
|
|
7734
|
+
constructor({
|
|
7735
|
+
baseDir = process.cwd(),
|
|
7736
|
+
dryRun = false,
|
|
7737
|
+
avoidBlockScalars = false
|
|
7738
|
+
}) {
|
|
7646
7739
|
this.baseDir = baseDir;
|
|
7647
7740
|
this.dryRun = dryRun;
|
|
7741
|
+
this.avoidBlockScalars = avoidBlockScalars;
|
|
7648
7742
|
}
|
|
7649
7743
|
/**
|
|
7650
7744
|
* Return tool targets that this feature supports.
|
|
@@ -7670,7 +7764,9 @@ var DirFeatureProcessor = class {
|
|
|
7670
7764
|
let mainFileContent;
|
|
7671
7765
|
if (mainFile) {
|
|
7672
7766
|
const mainFilePath = (0, import_node_path59.join)(dirPath, mainFile.name);
|
|
7673
|
-
const content = stringifyFrontmatter(mainFile.body, mainFile.frontmatter
|
|
7767
|
+
const content = stringifyFrontmatter(mainFile.body, mainFile.frontmatter, {
|
|
7768
|
+
avoidBlockScalars: this.avoidBlockScalars
|
|
7769
|
+
});
|
|
7674
7770
|
mainFileContent = addTrailingNewline(content);
|
|
7675
7771
|
const existingContent = await readFileContentOrNull(mainFilePath);
|
|
7676
7772
|
if (existingContent !== mainFileContent) {
|
|
@@ -10409,7 +10505,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
10409
10505
|
getFactory = defaultGetFactory4,
|
|
10410
10506
|
dryRun = false
|
|
10411
10507
|
}) {
|
|
10412
|
-
super({ baseDir, dryRun });
|
|
10508
|
+
super({ baseDir, dryRun, avoidBlockScalars: toolTarget === "cursor" });
|
|
10413
10509
|
const result = SkillsProcessorToolTargetSchema.safeParse(toolTarget);
|
|
10414
10510
|
if (!result.success) {
|
|
10415
10511
|
throw new Error(
|
|
@@ -11490,7 +11586,7 @@ var CursorSubagent = class _CursorSubagent extends ToolSubagent {
|
|
|
11490
11586
|
...cursorSection
|
|
11491
11587
|
};
|
|
11492
11588
|
const body = rulesyncSubagent.getBody();
|
|
11493
|
-
const fileContent = stringifyFrontmatter(body, cursorFrontmatter);
|
|
11589
|
+
const fileContent = stringifyFrontmatter(body, cursorFrontmatter, { avoidBlockScalars: true });
|
|
11494
11590
|
const paths = this.getSettablePaths({ global });
|
|
11495
11591
|
return new _CursorSubagent({
|
|
11496
11592
|
baseDir,
|
|
@@ -16929,13 +17025,22 @@ var import_jsonc_parser2 = require("jsonc-parser");
|
|
|
16929
17025
|
// src/config/config.ts
|
|
16930
17026
|
var import_node_path117 = require("path");
|
|
16931
17027
|
var import_mini59 = require("zod/mini");
|
|
16932
|
-
|
|
17028
|
+
|
|
17029
|
+
// src/utils/validation.ts
|
|
17030
|
+
function findControlCharacter(value) {
|
|
16933
17031
|
for (let i = 0; i < value.length; i++) {
|
|
16934
17032
|
const code = value.charCodeAt(i);
|
|
16935
|
-
if (code >= 0 && code <= 31 || code === 127)
|
|
17033
|
+
if (code >= 0 && code <= 31 || code === 127) {
|
|
17034
|
+
return { position: i, hex: `0x${code.toString(16).padStart(2, "0")}` };
|
|
17035
|
+
}
|
|
16936
17036
|
}
|
|
16937
|
-
return
|
|
17037
|
+
return null;
|
|
16938
17038
|
}
|
|
17039
|
+
function hasControlCharacters(value) {
|
|
17040
|
+
return findControlCharacter(value) !== null;
|
|
17041
|
+
}
|
|
17042
|
+
|
|
17043
|
+
// src/config/config.ts
|
|
16939
17044
|
var SourceEntrySchema = import_mini59.z.object({
|
|
16940
17045
|
source: import_mini59.z.string().check((0, import_mini59.minLength)(1, "source must be a non-empty string")),
|
|
16941
17046
|
skills: (0, import_mini59.optional)(import_mini59.z.array(import_mini59.z.string())),
|
|
@@ -18455,17 +18560,8 @@ var import_node_path122 = require("path");
|
|
|
18455
18560
|
var import_node_util = require("util");
|
|
18456
18561
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
18457
18562
|
var GIT_TIMEOUT_MS = 6e4;
|
|
18458
|
-
var ALLOWED_URL_SCHEMES = /^(https?:\/\/|ssh:\/\/|git:\/\/|file
|
|
18563
|
+
var ALLOWED_URL_SCHEMES = /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/\/).+$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9.-]+:[a-zA-Z0-9_.+/~-]+$/;
|
|
18459
18564
|
var INSECURE_URL_SCHEMES = /^(git:\/\/|http:\/\/)/;
|
|
18460
|
-
function findControlCharacter(value) {
|
|
18461
|
-
for (let i = 0; i < value.length; i++) {
|
|
18462
|
-
const code = value.charCodeAt(i);
|
|
18463
|
-
if (code >= 0 && code <= 31 || code === 127) {
|
|
18464
|
-
return { position: i, hex: `0x${code.toString(16).padStart(2, "0")}` };
|
|
18465
|
-
}
|
|
18466
|
-
}
|
|
18467
|
-
return null;
|
|
18468
|
-
}
|
|
18469
18565
|
var GitClientError = class extends Error {
|
|
18470
18566
|
constructor(message, cause) {
|
|
18471
18567
|
super(message, { cause });
|
|
@@ -18521,6 +18617,7 @@ async function resolveDefaultRef(url) {
|
|
|
18521
18617
|
const ref = stdout.match(/^ref: refs\/heads\/(.+)\tHEAD$/m)?.[1];
|
|
18522
18618
|
const sha = stdout.match(/^([0-9a-f]{40})\tHEAD$/m)?.[1];
|
|
18523
18619
|
if (!ref || !sha) throw new GitClientError(`Could not parse default branch from: ${url}`);
|
|
18620
|
+
validateRef(ref);
|
|
18524
18621
|
return { ref, sha };
|
|
18525
18622
|
} catch (error) {
|
|
18526
18623
|
if (error instanceof GitClientError) throw error;
|
|
@@ -18547,6 +18644,17 @@ async function fetchSkillFiles(params) {
|
|
|
18547
18644
|
const { url, ref, skillsPath } = params;
|
|
18548
18645
|
validateGitUrl(url);
|
|
18549
18646
|
validateRef(ref);
|
|
18647
|
+
if (skillsPath.split(/[/\\]/).includes("..") || (0, import_node_path122.isAbsolute)(skillsPath)) {
|
|
18648
|
+
throw new GitClientError(
|
|
18649
|
+
`Invalid skillsPath "${skillsPath}": must be a relative path without ".."`
|
|
18650
|
+
);
|
|
18651
|
+
}
|
|
18652
|
+
const ctrl = findControlCharacter(skillsPath);
|
|
18653
|
+
if (ctrl) {
|
|
18654
|
+
throw new GitClientError(
|
|
18655
|
+
`skillsPath contains control character ${ctrl.hex} at position ${ctrl.position}`
|
|
18656
|
+
);
|
|
18657
|
+
}
|
|
18550
18658
|
await checkGitAvailable();
|
|
18551
18659
|
const tmpDir = await createTempDirectory("rulesync-git-");
|
|
18552
18660
|
try {
|
|
@@ -18581,7 +18689,9 @@ async function fetchSkillFiles(params) {
|
|
|
18581
18689
|
}
|
|
18582
18690
|
}
|
|
18583
18691
|
var MAX_WALK_DEPTH = 20;
|
|
18584
|
-
|
|
18692
|
+
var MAX_TOTAL_FILES = 1e4;
|
|
18693
|
+
var MAX_TOTAL_SIZE = 100 * 1024 * 1024;
|
|
18694
|
+
async function walkDirectory(dir, baseDir, depth = 0, ctx = { totalFiles: 0, totalSize: 0 }) {
|
|
18585
18695
|
if (depth > MAX_WALK_DEPTH) {
|
|
18586
18696
|
throw new GitClientError(
|
|
18587
18697
|
`Directory tree exceeds max depth of ${MAX_WALK_DEPTH}: "${dir}". Aborting to prevent resource exhaustion.`
|
|
@@ -18596,7 +18706,7 @@ async function walkDirectory(dir, baseDir, depth = 0) {
|
|
|
18596
18706
|
continue;
|
|
18597
18707
|
}
|
|
18598
18708
|
if (await directoryExists(fullPath)) {
|
|
18599
|
-
results.push(...await walkDirectory(fullPath, baseDir, depth + 1));
|
|
18709
|
+
results.push(...await walkDirectory(fullPath, baseDir, depth + 1, ctx));
|
|
18600
18710
|
} else {
|
|
18601
18711
|
const size = await getFileSize(fullPath);
|
|
18602
18712
|
if (size > MAX_FILE_SIZE) {
|
|
@@ -18605,8 +18715,20 @@ async function walkDirectory(dir, baseDir, depth = 0) {
|
|
|
18605
18715
|
);
|
|
18606
18716
|
continue;
|
|
18607
18717
|
}
|
|
18718
|
+
ctx.totalFiles++;
|
|
18719
|
+
ctx.totalSize += size;
|
|
18720
|
+
if (ctx.totalFiles >= MAX_TOTAL_FILES) {
|
|
18721
|
+
throw new GitClientError(
|
|
18722
|
+
`Repository exceeds max file count of ${MAX_TOTAL_FILES}. Aborting to prevent resource exhaustion.`
|
|
18723
|
+
);
|
|
18724
|
+
}
|
|
18725
|
+
if (ctx.totalSize >= MAX_TOTAL_SIZE) {
|
|
18726
|
+
throw new GitClientError(
|
|
18727
|
+
`Repository exceeds max total size of ${MAX_TOTAL_SIZE / 1024 / 1024}MB. Aborting to prevent resource exhaustion.`
|
|
18728
|
+
);
|
|
18729
|
+
}
|
|
18608
18730
|
const content = await readFileContent(fullPath);
|
|
18609
|
-
results.push({ relativePath:
|
|
18731
|
+
results.push({ relativePath: (0, import_node_path122.relative)(baseDir, fullPath), content, size });
|
|
18610
18732
|
}
|
|
18611
18733
|
}
|
|
18612
18734
|
return results;
|
|
@@ -18622,7 +18744,7 @@ var LockedSkillSchema = import_mini60.z.object({
|
|
|
18622
18744
|
});
|
|
18623
18745
|
var LockedSourceSchema = import_mini60.z.object({
|
|
18624
18746
|
requestedRef: (0, import_mini60.optional)(import_mini60.z.string()),
|
|
18625
|
-
resolvedRef: import_mini60.z.string(),
|
|
18747
|
+
resolvedRef: import_mini60.z.string().check((0, import_mini60.refine)((v) => /^[0-9a-f]{40}$/.test(v), "resolvedRef must be a 40-character hex SHA")),
|
|
18626
18748
|
resolvedAt: (0, import_mini60.optional)(import_mini60.z.string()),
|
|
18627
18749
|
skills: import_mini60.z.record(import_mini60.z.string(), LockedSkillSchema)
|
|
18628
18750
|
});
|
|
@@ -18825,6 +18947,8 @@ async function resolveAndFetchSources(params) {
|
|
|
18825
18947
|
logger.error(`Failed to fetch source "${sourceEntry.source}": ${formatError(error)}`);
|
|
18826
18948
|
if (error instanceof GitHubClientError) {
|
|
18827
18949
|
logGitHubAuthHints(error);
|
|
18950
|
+
} else if (error instanceof GitClientError) {
|
|
18951
|
+
logGitClientHints(error);
|
|
18828
18952
|
}
|
|
18829
18953
|
}
|
|
18830
18954
|
}
|
|
@@ -18845,6 +18969,13 @@ async function resolveAndFetchSources(params) {
|
|
|
18845
18969
|
}
|
|
18846
18970
|
return { fetchedSkillCount: totalSkillCount, sourcesProcessed: sources.length };
|
|
18847
18971
|
}
|
|
18972
|
+
function logGitClientHints(error) {
|
|
18973
|
+
if (error.message.includes("not installed")) {
|
|
18974
|
+
logger.info("Hint: Install git and ensure it is available on your PATH.");
|
|
18975
|
+
} else {
|
|
18976
|
+
logger.info("Hint: Check your git credentials (SSH keys, credential helper, or access token).");
|
|
18977
|
+
}
|
|
18978
|
+
}
|
|
18848
18979
|
async function checkLockedSkillsExist(curatedDir, skillNames) {
|
|
18849
18980
|
if (skillNames.length === 0) return true;
|
|
18850
18981
|
for (const name of skillNames) {
|
|
@@ -18854,9 +18985,88 @@ async function checkLockedSkillsExist(curatedDir, skillNames) {
|
|
|
18854
18985
|
}
|
|
18855
18986
|
return true;
|
|
18856
18987
|
}
|
|
18988
|
+
async function cleanPreviousCuratedSkills(curatedDir, lockedSkillNames) {
|
|
18989
|
+
const resolvedCuratedDir = (0, import_node_path124.resolve)(curatedDir);
|
|
18990
|
+
for (const prevSkill of lockedSkillNames) {
|
|
18991
|
+
const prevDir = (0, import_node_path124.join)(curatedDir, prevSkill);
|
|
18992
|
+
if (!(0, import_node_path124.resolve)(prevDir).startsWith(resolvedCuratedDir + import_node_path124.sep)) {
|
|
18993
|
+
logger.warn(
|
|
18994
|
+
`Skipping removal of "${prevSkill}": resolved path is outside the curated directory.`
|
|
18995
|
+
);
|
|
18996
|
+
continue;
|
|
18997
|
+
}
|
|
18998
|
+
if (await directoryExists(prevDir)) {
|
|
18999
|
+
await removeDirectory(prevDir);
|
|
19000
|
+
}
|
|
19001
|
+
}
|
|
19002
|
+
}
|
|
19003
|
+
function shouldSkipSkill(params) {
|
|
19004
|
+
const { skillName, sourceKey, localSkillNames, alreadyFetchedSkillNames } = params;
|
|
19005
|
+
if (skillName.includes("..") || skillName.includes("/") || skillName.includes("\\")) {
|
|
19006
|
+
logger.warn(
|
|
19007
|
+
`Skipping skill with invalid name "${skillName}" from ${sourceKey}: contains path traversal characters.`
|
|
19008
|
+
);
|
|
19009
|
+
return true;
|
|
19010
|
+
}
|
|
19011
|
+
if (localSkillNames.has(skillName)) {
|
|
19012
|
+
logger.debug(
|
|
19013
|
+
`Skipping remote skill "${skillName}" from ${sourceKey}: local skill takes precedence.`
|
|
19014
|
+
);
|
|
19015
|
+
return true;
|
|
19016
|
+
}
|
|
19017
|
+
if (alreadyFetchedSkillNames.has(skillName)) {
|
|
19018
|
+
logger.warn(
|
|
19019
|
+
`Skipping duplicate skill "${skillName}" from ${sourceKey}: already fetched from another source.`
|
|
19020
|
+
);
|
|
19021
|
+
return true;
|
|
19022
|
+
}
|
|
19023
|
+
return false;
|
|
19024
|
+
}
|
|
19025
|
+
async function writeSkillAndComputeIntegrity(params) {
|
|
19026
|
+
const { skillName, files, curatedDir, locked, resolvedSha, sourceKey } = params;
|
|
19027
|
+
const written = [];
|
|
19028
|
+
for (const file of files) {
|
|
19029
|
+
checkPathTraversal({
|
|
19030
|
+
relativePath: file.relativePath,
|
|
19031
|
+
intendedRootDir: (0, import_node_path124.join)(curatedDir, skillName)
|
|
19032
|
+
});
|
|
19033
|
+
await writeFileContent((0, import_node_path124.join)(curatedDir, skillName, file.relativePath), file.content);
|
|
19034
|
+
written.push({ path: file.relativePath, content: file.content });
|
|
19035
|
+
}
|
|
19036
|
+
const integrity = computeSkillIntegrity(written);
|
|
19037
|
+
const lockedSkillEntry = locked?.skills[skillName];
|
|
19038
|
+
if (lockedSkillEntry?.integrity && lockedSkillEntry.integrity !== integrity && resolvedSha === locked?.resolvedRef) {
|
|
19039
|
+
logger.warn(
|
|
19040
|
+
`Integrity mismatch for skill "${skillName}" from ${sourceKey}: expected "${lockedSkillEntry.integrity}", got "${integrity}". Content may have been tampered with.`
|
|
19041
|
+
);
|
|
19042
|
+
}
|
|
19043
|
+
return { integrity };
|
|
19044
|
+
}
|
|
19045
|
+
function buildLockUpdate(params) {
|
|
19046
|
+
const { lock, sourceKey, fetchedSkills, locked, requestedRef, resolvedSha } = params;
|
|
19047
|
+
const fetchedNames = Object.keys(fetchedSkills);
|
|
19048
|
+
const mergedSkills = { ...fetchedSkills };
|
|
19049
|
+
if (locked) {
|
|
19050
|
+
for (const [skillName, skillEntry] of Object.entries(locked.skills)) {
|
|
19051
|
+
if (!(skillName in mergedSkills)) {
|
|
19052
|
+
mergedSkills[skillName] = skillEntry;
|
|
19053
|
+
}
|
|
19054
|
+
}
|
|
19055
|
+
}
|
|
19056
|
+
const updatedLock = setLockedSource(lock, sourceKey, {
|
|
19057
|
+
requestedRef,
|
|
19058
|
+
resolvedRef: resolvedSha,
|
|
19059
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19060
|
+
skills: mergedSkills
|
|
19061
|
+
});
|
|
19062
|
+
logger.info(
|
|
19063
|
+
`Fetched ${fetchedNames.length} skill(s) from ${sourceKey}: ${fetchedNames.join(", ") || "(none)"}`
|
|
19064
|
+
);
|
|
19065
|
+
return { updatedLock, fetchedNames };
|
|
19066
|
+
}
|
|
18857
19067
|
async function fetchSource(params) {
|
|
18858
19068
|
const { sourceEntry, client, baseDir, localSkillNames, alreadyFetchedSkillNames, updateSources } = params;
|
|
18859
|
-
|
|
19069
|
+
const { lock } = params;
|
|
18860
19070
|
const parsed = parseSource(sourceEntry.source);
|
|
18861
19071
|
if (parsed.provider === "gitlab") {
|
|
18862
19072
|
logger.warn(`GitLab sources are not yet supported. Skipping "${sourceEntry.source}".`);
|
|
@@ -18909,37 +19119,15 @@ async function fetchSource(params) {
|
|
|
18909
19119
|
const semaphore = new import_promise2.Semaphore(FETCH_CONCURRENCY_LIMIT);
|
|
18910
19120
|
const fetchedSkills = {};
|
|
18911
19121
|
if (locked) {
|
|
18912
|
-
|
|
18913
|
-
for (const prevSkill of lockedSkillNames) {
|
|
18914
|
-
const prevDir = (0, import_node_path124.join)(curatedDir, prevSkill);
|
|
18915
|
-
if (!(0, import_node_path124.resolve)(prevDir).startsWith(resolvedCuratedDir + import_node_path124.sep)) {
|
|
18916
|
-
logger.warn(
|
|
18917
|
-
`Skipping removal of "${prevSkill}": resolved path is outside the curated directory.`
|
|
18918
|
-
);
|
|
18919
|
-
continue;
|
|
18920
|
-
}
|
|
18921
|
-
if (await directoryExists(prevDir)) {
|
|
18922
|
-
await removeDirectory(prevDir);
|
|
18923
|
-
}
|
|
18924
|
-
}
|
|
19122
|
+
await cleanPreviousCuratedSkills(curatedDir, lockedSkillNames);
|
|
18925
19123
|
}
|
|
18926
19124
|
for (const skillDir of filteredDirs) {
|
|
18927
|
-
if (
|
|
18928
|
-
|
|
18929
|
-
|
|
18930
|
-
|
|
18931
|
-
|
|
18932
|
-
}
|
|
18933
|
-
if (localSkillNames.has(skillDir.name)) {
|
|
18934
|
-
logger.debug(
|
|
18935
|
-
`Skipping remote skill "${skillDir.name}" from ${sourceKey}: local skill takes precedence.`
|
|
18936
|
-
);
|
|
18937
|
-
continue;
|
|
18938
|
-
}
|
|
18939
|
-
if (alreadyFetchedSkillNames.has(skillDir.name)) {
|
|
18940
|
-
logger.warn(
|
|
18941
|
-
`Skipping duplicate skill "${skillDir.name}" from ${sourceKey}: already fetched from another source.`
|
|
18942
|
-
);
|
|
19125
|
+
if (shouldSkipSkill({
|
|
19126
|
+
skillName: skillDir.name,
|
|
19127
|
+
sourceKey,
|
|
19128
|
+
localSkillNames,
|
|
19129
|
+
alreadyFetchedSkillNames
|
|
19130
|
+
})) {
|
|
18943
19131
|
continue;
|
|
18944
19132
|
}
|
|
18945
19133
|
const allFiles = await listDirectoryRecursive({
|
|
@@ -18962,55 +19150,39 @@ async function fetchSource(params) {
|
|
|
18962
19150
|
const skillFiles = [];
|
|
18963
19151
|
for (const file of files) {
|
|
18964
19152
|
const relativeToSkill = file.path.substring(skillDir.path.length + 1);
|
|
18965
|
-
const localFilePath = (0, import_node_path124.join)(curatedDir, skillDir.name, relativeToSkill);
|
|
18966
|
-
checkPathTraversal({
|
|
18967
|
-
relativePath: relativeToSkill,
|
|
18968
|
-
intendedRootDir: (0, import_node_path124.join)(curatedDir, skillDir.name)
|
|
18969
|
-
});
|
|
18970
19153
|
const content = await withSemaphore(
|
|
18971
19154
|
semaphore,
|
|
18972
19155
|
() => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
|
|
18973
19156
|
);
|
|
18974
|
-
|
|
18975
|
-
skillFiles.push({ path: relativeToSkill, content });
|
|
19157
|
+
skillFiles.push({ relativePath: relativeToSkill, content });
|
|
18976
19158
|
}
|
|
18977
|
-
|
|
18978
|
-
|
|
18979
|
-
|
|
18980
|
-
|
|
18981
|
-
|
|
18982
|
-
|
|
18983
|
-
|
|
18984
|
-
|
|
19159
|
+
fetchedSkills[skillDir.name] = await writeSkillAndComputeIntegrity({
|
|
19160
|
+
skillName: skillDir.name,
|
|
19161
|
+
files: skillFiles,
|
|
19162
|
+
curatedDir,
|
|
19163
|
+
locked,
|
|
19164
|
+
resolvedSha,
|
|
19165
|
+
sourceKey
|
|
19166
|
+
});
|
|
18985
19167
|
logger.debug(`Fetched skill "${skillDir.name}" from ${sourceKey}`);
|
|
18986
19168
|
}
|
|
18987
|
-
const
|
|
18988
|
-
|
|
18989
|
-
|
|
18990
|
-
|
|
18991
|
-
|
|
18992
|
-
mergedSkills[skillName] = skillEntry;
|
|
18993
|
-
}
|
|
18994
|
-
}
|
|
18995
|
-
}
|
|
18996
|
-
lock = setLockedSource(lock, sourceKey, {
|
|
19169
|
+
const result = buildLockUpdate({
|
|
19170
|
+
lock,
|
|
19171
|
+
sourceKey,
|
|
19172
|
+
fetchedSkills,
|
|
19173
|
+
locked,
|
|
18997
19174
|
requestedRef,
|
|
18998
|
-
|
|
18999
|
-
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19000
|
-
skills: mergedSkills
|
|
19175
|
+
resolvedSha
|
|
19001
19176
|
});
|
|
19002
|
-
logger.info(
|
|
19003
|
-
`Fetched ${fetchedNames.length} skill(s) from ${sourceKey}: ${fetchedNames.join(", ") || "(none)"}`
|
|
19004
|
-
);
|
|
19005
19177
|
return {
|
|
19006
|
-
skillCount: fetchedNames.length,
|
|
19007
|
-
fetchedSkillNames: fetchedNames,
|
|
19008
|
-
updatedLock:
|
|
19178
|
+
skillCount: result.fetchedNames.length,
|
|
19179
|
+
fetchedSkillNames: result.fetchedNames,
|
|
19180
|
+
updatedLock: result.updatedLock
|
|
19009
19181
|
};
|
|
19010
19182
|
}
|
|
19011
19183
|
async function fetchSourceViaGit(params) {
|
|
19012
19184
|
const { sourceEntry, baseDir, localSkillNames, alreadyFetchedSkillNames, updateSources, frozen } = params;
|
|
19013
|
-
|
|
19185
|
+
const { lock } = params;
|
|
19014
19186
|
const url = sourceEntry.source;
|
|
19015
19187
|
const locked = getLockedSource(lock, url);
|
|
19016
19188
|
const lockedSkillNames = locked ? getLockedSkillNames(locked) : [];
|
|
@@ -19066,68 +19238,35 @@ async function fetchSourceViaGit(params) {
|
|
|
19066
19238
|
const allNames = [...skillFileMap.keys()];
|
|
19067
19239
|
const filteredNames = isWildcard ? allNames : allNames.filter((n) => skillFilter.includes(n));
|
|
19068
19240
|
if (locked) {
|
|
19069
|
-
|
|
19070
|
-
for (const prev of lockedSkillNames) {
|
|
19071
|
-
const dir = (0, import_node_path124.join)(curatedDir, prev);
|
|
19072
|
-
if ((0, import_node_path124.resolve)(dir).startsWith(base + import_node_path124.sep) && await directoryExists(dir)) {
|
|
19073
|
-
await removeDirectory(dir);
|
|
19074
|
-
}
|
|
19075
|
-
}
|
|
19241
|
+
await cleanPreviousCuratedSkills(curatedDir, lockedSkillNames);
|
|
19076
19242
|
}
|
|
19077
19243
|
const fetchedSkills = {};
|
|
19078
19244
|
for (const skillName of filteredNames) {
|
|
19079
|
-
if (
|
|
19080
|
-
logger.warn(
|
|
19081
|
-
`Skipping skill with invalid name "${skillName}" from ${url}: contains path traversal characters.`
|
|
19082
|
-
);
|
|
19245
|
+
if (shouldSkipSkill({ skillName, sourceKey: url, localSkillNames, alreadyFetchedSkillNames })) {
|
|
19083
19246
|
continue;
|
|
19084
19247
|
}
|
|
19085
|
-
|
|
19086
|
-
|
|
19087
|
-
|
|
19088
|
-
|
|
19089
|
-
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
|
|
19093
|
-
`Skipping duplicate skill "${skillName}" from ${url}: already fetched from another source.`
|
|
19094
|
-
);
|
|
19095
|
-
continue;
|
|
19096
|
-
}
|
|
19097
|
-
const files = skillFileMap.get(skillName) ?? [];
|
|
19098
|
-
const written = [];
|
|
19099
|
-
for (const file of files) {
|
|
19100
|
-
checkPathTraversal({
|
|
19101
|
-
relativePath: file.relativePath,
|
|
19102
|
-
intendedRootDir: (0, import_node_path124.join)(curatedDir, skillName)
|
|
19103
|
-
});
|
|
19104
|
-
await writeFileContent((0, import_node_path124.join)(curatedDir, skillName, file.relativePath), file.content);
|
|
19105
|
-
written.push({ path: file.relativePath, content: file.content });
|
|
19106
|
-
}
|
|
19107
|
-
const integrity = computeSkillIntegrity(written);
|
|
19108
|
-
const lockedSkillEntry = locked?.skills[skillName];
|
|
19109
|
-
if (lockedSkillEntry?.integrity && lockedSkillEntry.integrity !== integrity && resolvedSha === locked?.resolvedRef) {
|
|
19110
|
-
logger.warn(`Integrity mismatch for skill "${skillName}" from ${url}.`);
|
|
19111
|
-
}
|
|
19112
|
-
fetchedSkills[skillName] = { integrity };
|
|
19113
|
-
}
|
|
19114
|
-
const fetchedNames = Object.keys(fetchedSkills);
|
|
19115
|
-
const mergedSkills = { ...fetchedSkills };
|
|
19116
|
-
if (locked) {
|
|
19117
|
-
for (const [k, v] of Object.entries(locked.skills)) {
|
|
19118
|
-
if (!(k in mergedSkills)) mergedSkills[k] = v;
|
|
19119
|
-
}
|
|
19248
|
+
fetchedSkills[skillName] = await writeSkillAndComputeIntegrity({
|
|
19249
|
+
skillName,
|
|
19250
|
+
files: skillFileMap.get(skillName) ?? [],
|
|
19251
|
+
curatedDir,
|
|
19252
|
+
locked,
|
|
19253
|
+
resolvedSha,
|
|
19254
|
+
sourceKey: url
|
|
19255
|
+
});
|
|
19120
19256
|
}
|
|
19121
|
-
|
|
19257
|
+
const result = buildLockUpdate({
|
|
19258
|
+
lock,
|
|
19259
|
+
sourceKey: url,
|
|
19260
|
+
fetchedSkills,
|
|
19261
|
+
locked,
|
|
19122
19262
|
requestedRef,
|
|
19123
|
-
|
|
19124
|
-
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19125
|
-
skills: mergedSkills
|
|
19263
|
+
resolvedSha
|
|
19126
19264
|
});
|
|
19127
|
-
|
|
19128
|
-
|
|
19129
|
-
|
|
19130
|
-
|
|
19265
|
+
return {
|
|
19266
|
+
skillCount: result.fetchedNames.length,
|
|
19267
|
+
fetchedSkillNames: result.fetchedNames,
|
|
19268
|
+
updatedLock: result.updatedLock
|
|
19269
|
+
};
|
|
19131
19270
|
}
|
|
19132
19271
|
|
|
19133
19272
|
// src/cli/commands/install.ts
|
|
@@ -20943,7 +21082,7 @@ async function updateCommand(currentVersion, options) {
|
|
|
20943
21082
|
}
|
|
20944
21083
|
|
|
20945
21084
|
// src/cli/index.ts
|
|
20946
|
-
var getVersion = () => "7.
|
|
21085
|
+
var getVersion = () => "7.16.0";
|
|
20947
21086
|
var main = async () => {
|
|
20948
21087
|
const program = new import_commander.Command();
|
|
20949
21088
|
const version = getVersion();
|