rulesync 16.16.0 → 16.17.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/dist/cli/index.cjs +9 -10
- package/dist/cli/index.js +9 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-NiPCjH6E.cjs → import-DDPTPxOX.cjs} +1013 -60
- package/dist/{import-DimKwtQ6.js → import-u8yswsGB.js} +1004 -63
- package/dist/import-u8yswsGB.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +77 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-DimKwtQ6.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ZodError } from "zod";
|
|
2
2
|
import { meta, minLength, nonnegative, optional, refine, z } from "zod/mini";
|
|
3
|
-
import { chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { chmod, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import path, { basename, dirname, extname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
5
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
6
6
|
import os from "node:os";
|
|
@@ -9,6 +9,7 @@ import { globbySync, isGitIgnoredSync } from "globby";
|
|
|
9
9
|
import matter from "gray-matter";
|
|
10
10
|
import { YAMLException, dump, load } from "js-yaml";
|
|
11
11
|
import { omit } from "es-toolkit/object";
|
|
12
|
+
import { constants } from "node:fs";
|
|
12
13
|
import { createHash } from "node:crypto";
|
|
13
14
|
import { isDeepStrictEqual } from "node:util";
|
|
14
15
|
import * as smolToml from "smol-toml";
|
|
@@ -470,9 +471,22 @@ function isEnvTest() {
|
|
|
470
471
|
}
|
|
471
472
|
//#endregion
|
|
472
473
|
//#region src/utils/file.ts
|
|
474
|
+
/**
|
|
475
|
+
* Whether a relative path leads out of the root it is relative to. Matching
|
|
476
|
+
* whole segments matters: a directory really named `..cache` relatively
|
|
477
|
+
* resolves to `..cache/file`, which a prefix test would report as an escape.
|
|
478
|
+
*/
|
|
473
479
|
function pathEscapesRoot(relativePath) {
|
|
474
480
|
return relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath);
|
|
475
481
|
}
|
|
482
|
+
/** Whether a single path segment is a hidden (dot-prefixed) name. */
|
|
483
|
+
function isHiddenPathSegment(segment) {
|
|
484
|
+
return segment.startsWith(".") && segment !== "." && segment !== "..";
|
|
485
|
+
}
|
|
486
|
+
/** Split a path on both separators, so one predicate serves either platform. */
|
|
487
|
+
function splitPathSegments(filePath) {
|
|
488
|
+
return filePath.split(/[/\\]/);
|
|
489
|
+
}
|
|
476
490
|
async function assertWritablePathInsideRoot(params) {
|
|
477
491
|
const { rootPath, targetPath } = params;
|
|
478
492
|
let existingPath = targetPath;
|
|
@@ -703,8 +717,45 @@ async function listDirectoryFiles(dir) {
|
|
|
703
717
|
return [];
|
|
704
718
|
}
|
|
705
719
|
}
|
|
720
|
+
/** How many dot-prefixed segments a path has, used to prefer a named alias over a hidden one. */
|
|
721
|
+
function countHiddenSegments(filePath) {
|
|
722
|
+
return splitPathSegments(filePath).filter(isHiddenPathSegment).length;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* The real file a path denotes, posix-separated so it compares against the globby results
|
|
726
|
+
* that produce it. Two paths share an identity when they resolve to the very same file --
|
|
727
|
+
* a link beside its target, a link into a shared tree, or a cycle that walks back into an
|
|
728
|
+
* ancestor and yields the same file forty levels down.
|
|
729
|
+
*/
|
|
730
|
+
async function realFileIdentity(filePath) {
|
|
731
|
+
try {
|
|
732
|
+
return toPosixPath(await realpath(filePath));
|
|
733
|
+
} catch {
|
|
734
|
+
return toPosixPath(filePath);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Pick the one path that represents a file among the paths that resolve to it.
|
|
739
|
+
*
|
|
740
|
+
* The path that walked through no link at all wins outright: it is already the real one,
|
|
741
|
+
* so it equals the file's identity. That keeps the real location of a file as the path
|
|
742
|
+
* callers see, rather than an alias that happens to sort first -- a directory link named
|
|
743
|
+
* `aaa` pointing at `zzz` must not make `zzz/x.md` disappear, and a cycle must not replace
|
|
744
|
+
* `sub/note.md` with the same file reached back through the cycle.
|
|
745
|
+
* Failing that, the fewest dot-prefixed segments wins: when only links are on offer, the
|
|
746
|
+
* named one represents the entry rather than a hidden alias that a hidden-entry rule may
|
|
747
|
+
* then drop, taking the named path's content with it. `candidates` arrives in sorted
|
|
748
|
+
* order, so ties keep the first one deterministically.
|
|
749
|
+
*/
|
|
750
|
+
function chooseRepresentative(candidates, identity) {
|
|
751
|
+
return candidates.reduce((best, candidate) => {
|
|
752
|
+
if (toPosixPath(best) === identity) return best;
|
|
753
|
+
if (toPosixPath(candidate) === identity) return candidate;
|
|
754
|
+
return countHiddenSegments(candidate) < countHiddenSegments(best) ? candidate : best;
|
|
755
|
+
});
|
|
756
|
+
}
|
|
706
757
|
async function findFilesByGlobs(globs, options = {}) {
|
|
707
|
-
const { type = "all", followSymbolicLinks = true, ignore } = options;
|
|
758
|
+
const { type = "all", followSymbolicLinks = true, ignore, dot = false } = options;
|
|
708
759
|
const globbyOptions = type === "file" ? {
|
|
709
760
|
onlyFiles: true,
|
|
710
761
|
onlyDirectories: false
|
|
@@ -719,23 +770,18 @@ async function findFilesByGlobs(globs, options = {}) {
|
|
|
719
770
|
const results = globbySync(normalizedGlobs, {
|
|
720
771
|
absolute: true,
|
|
721
772
|
followSymbolicLinks,
|
|
773
|
+
dot,
|
|
722
774
|
...ignore ? { ignore: ignore.map((pattern) => pattern.replaceAll("\\", "/")) } : {},
|
|
723
775
|
...globbyOptions
|
|
724
776
|
});
|
|
725
|
-
const
|
|
726
|
-
const deduped = [];
|
|
777
|
+
const candidatesByFile = /* @__PURE__ */ new Map();
|
|
727
778
|
for (const result of results.toSorted()) {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
realResult = result;
|
|
733
|
-
}
|
|
734
|
-
if (seenRealPaths.has(realResult)) continue;
|
|
735
|
-
seenRealPaths.add(realResult);
|
|
736
|
-
deduped.push(result);
|
|
779
|
+
const identity = await realFileIdentity(result);
|
|
780
|
+
const candidates = candidatesByFile.get(identity);
|
|
781
|
+
if (candidates === void 0) candidatesByFile.set(identity, [result]);
|
|
782
|
+
else candidates.push(result);
|
|
737
783
|
}
|
|
738
|
-
return
|
|
784
|
+
return [...candidatesByFile.entries()].map(([identity, candidates]) => chooseRepresentative(candidates, identity)).toSorted();
|
|
739
785
|
}
|
|
740
786
|
async function removeDirectory(dirPath) {
|
|
741
787
|
if ([
|
|
@@ -894,6 +940,25 @@ var CLIError = class extends Error {
|
|
|
894
940
|
}
|
|
895
941
|
};
|
|
896
942
|
//#endregion
|
|
943
|
+
//#region src/utils/warned-once.ts
|
|
944
|
+
/**
|
|
945
|
+
* The messages a once-per-run warning has already emitted in this process.
|
|
946
|
+
* This lives in its own module, free of imports, so the vitest setup file can
|
|
947
|
+
* clear it between tests without pulling `logger.js` into every test's module
|
|
948
|
+
* graph (which would defeat the module mocks some of those tests install).
|
|
949
|
+
*/
|
|
950
|
+
const warnedOnceMessages = /* @__PURE__ */ new Set();
|
|
951
|
+
/** Whether `message` has not been emitted yet; records it when it has not. */
|
|
952
|
+
function claimWarnOnce(message) {
|
|
953
|
+
if (warnedOnceMessages.has(message)) return false;
|
|
954
|
+
warnedOnceMessages.add(message);
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
/** Forget which warnings were already emitted, so each test starts silent. */
|
|
958
|
+
function resetWarnedOnceMessages() {
|
|
959
|
+
warnedOnceMessages.clear();
|
|
960
|
+
}
|
|
961
|
+
//#endregion
|
|
897
962
|
//#region src/utils/logger.ts
|
|
898
963
|
/**
|
|
899
964
|
* Base class for shared verbose/silent state and configuration logic
|
|
@@ -1046,6 +1111,17 @@ const fallbackLogger = new ConsoleLogger();
|
|
|
1046
1111
|
function warnWithFallback(logger, message) {
|
|
1047
1112
|
(logger ?? fallbackLogger).warn(message);
|
|
1048
1113
|
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Emit a warning at most once per run. A single `generate` reads the same source
|
|
1116
|
+
* file once per enabled tool target, so a warning that describes the source
|
|
1117
|
+
* rather than the target would otherwise be printed a dozen identical times.
|
|
1118
|
+
* Diagnostics that name the file they are about qualify; anything whose text
|
|
1119
|
+
* varies with what the user should do next does not.
|
|
1120
|
+
*/
|
|
1121
|
+
function warnOnceWithFallback(logger, message) {
|
|
1122
|
+
if (!claimWarnOnce(message)) return;
|
|
1123
|
+
warnWithFallback(logger, message);
|
|
1124
|
+
}
|
|
1049
1125
|
//#endregion
|
|
1050
1126
|
//#region src/utils/validation.ts
|
|
1051
1127
|
/**
|
|
@@ -1990,8 +2066,7 @@ var AiFile = class {
|
|
|
1990
2066
|
const fullPath = path.join(this.outputRoot, this.relativeDirPath, this.relativeFilePath);
|
|
1991
2067
|
const resolvedFull = resolve(fullPath);
|
|
1992
2068
|
const resolvedBase = resolve(this.outputRoot);
|
|
1993
|
-
|
|
1994
|
-
if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}", relativeFilePath="${this.relativeFilePath}"`);
|
|
2069
|
+
if (pathEscapesRoot(relative(resolvedBase, resolvedFull))) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}", relativeFilePath="${this.relativeFilePath}"`);
|
|
1995
2070
|
return fullPath;
|
|
1996
2071
|
}
|
|
1997
2072
|
getFileContent() {
|
|
@@ -2053,6 +2128,27 @@ var RulesyncFile = class extends AiFile {
|
|
|
2053
2128
|
}
|
|
2054
2129
|
};
|
|
2055
2130
|
//#endregion
|
|
2131
|
+
//#region src/utils/control-characters.ts
|
|
2132
|
+
/**
|
|
2133
|
+
* Matches C0 controls, DEL, the C1 range (which includes the 8-bit CSI
|
|
2134
|
+
* introducer U+009B), the bidirectional overrides and isolates, and the Unicode
|
|
2135
|
+
* line and paragraph separators, and the plain LRM/RLM marks. A name or value
|
|
2136
|
+
* copied out of an untrusted config file, a fetched repository, or a tool's own
|
|
2137
|
+
* settings file must never reach the terminal with these intact: they let the
|
|
2138
|
+
* text forge log lines, reorder what is printed around them, or inject escape
|
|
2139
|
+
* sequences. LRM/RLM open no bidi scope of their own, but they still reorder the
|
|
2140
|
+
* neutral characters beside them, so they go too — a diagnostic line is not the
|
|
2141
|
+
* place to preserve the typography of a right-to-left name.
|
|
2142
|
+
*/
|
|
2143
|
+
const CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g;
|
|
2144
|
+
/**
|
|
2145
|
+
* Removes every control character from `text` so it is safe to splice into a
|
|
2146
|
+
* log line or other terminal output.
|
|
2147
|
+
*/
|
|
2148
|
+
function stripControlCharacters(text) {
|
|
2149
|
+
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
2150
|
+
}
|
|
2151
|
+
//#endregion
|
|
2056
2152
|
//#region src/utils/type-guards.ts
|
|
2057
2153
|
/**
|
|
2058
2154
|
* Type guard to check if a value is a plain object (Record<string, unknown>).
|
|
@@ -2176,7 +2272,7 @@ function parseFrontmatter(content, filePath) {
|
|
|
2176
2272
|
let body;
|
|
2177
2273
|
let hasFrontmatter;
|
|
2178
2274
|
try {
|
|
2179
|
-
const result = matter(content);
|
|
2275
|
+
const result = matter(content, {});
|
|
2180
2276
|
frontmatter = result.data;
|
|
2181
2277
|
body = result.content;
|
|
2182
2278
|
hasFrontmatter = result.matter !== "" || content.trimStart().startsWith("---");
|
|
@@ -2190,6 +2286,104 @@ function parseFrontmatter(content, filePath) {
|
|
|
2190
2286
|
hasFrontmatter
|
|
2191
2287
|
};
|
|
2192
2288
|
}
|
|
2289
|
+
/**
|
|
2290
|
+
* A top-level `key: value` entry. Nested entries are left alone deliberately:
|
|
2291
|
+
* the repair below rewrites a value's meaning, and the failure it exists for —
|
|
2292
|
+
* an unquoted sentence with a colon in it — is a `description`, which is always
|
|
2293
|
+
* top-level.
|
|
2294
|
+
*/
|
|
2295
|
+
const TOP_LEVEL_ENTRY_PATTERN = /^([A-Za-z_][\w.-]*):[^\S\r\n]+(\S.*)$/;
|
|
2296
|
+
/** A plain scalar that already starts as some other YAML construct. */
|
|
2297
|
+
const YAML_CONSTRUCT_PREFIX_PATTERN = /^["'|>&*![{#]/;
|
|
2298
|
+
/**
|
|
2299
|
+
* Cut a plain scalar at its inline comment — whitespace followed by `#`.
|
|
2300
|
+
*
|
|
2301
|
+
* This has to happen before anything else looks at the value, or a line such as
|
|
2302
|
+
* `allowed-tools: Read # TODO: add Bash later` reads as needing repair and comes
|
|
2303
|
+
* back quoted with the comment inside it, which for a list of tool permissions
|
|
2304
|
+
* would grant what the comment had disabled. Scanning by hand rather than with
|
|
2305
|
+
* `/\s+#.*$/`: that pattern is unanchored, so a long run of spaces with no `#`
|
|
2306
|
+
* after it backtracks from every starting position, and a value padded with a
|
|
2307
|
+
* megabyte of spaces takes minutes to reject. This is linear in the value.
|
|
2308
|
+
*/
|
|
2309
|
+
function stripInlineComment(rawValue) {
|
|
2310
|
+
for (let index = 1; index < rawValue.length; index++) if (rawValue[index] === "#" && /\s/.test(rawValue[index - 1] ?? "")) return rawValue.slice(0, index).trimEnd();
|
|
2311
|
+
return rawValue.trimEnd();
|
|
2312
|
+
}
|
|
2313
|
+
function repairFrontmatterLine(line) {
|
|
2314
|
+
const unchanged = {
|
|
2315
|
+
line,
|
|
2316
|
+
droppedComment: false
|
|
2317
|
+
};
|
|
2318
|
+
const carriageReturn = line.endsWith("\r") ? "\r" : "";
|
|
2319
|
+
const bareLine = carriageReturn === "" ? line : line.slice(0, -1);
|
|
2320
|
+
const match = TOP_LEVEL_ENTRY_PATTERN.exec(bareLine);
|
|
2321
|
+
if (!match) return unchanged;
|
|
2322
|
+
const [, key = "", rawValue = ""] = match;
|
|
2323
|
+
const value = stripInlineComment(rawValue);
|
|
2324
|
+
if (value === "") return unchanged;
|
|
2325
|
+
if (!/:(?:\s|$)/.test(value)) return unchanged;
|
|
2326
|
+
if (YAML_CONSTRUCT_PREFIX_PATTERN.test(value)) return unchanged;
|
|
2327
|
+
return {
|
|
2328
|
+
line: `${key}: ${JSON.stringify(value)}${carriageReturn}`,
|
|
2329
|
+
droppedComment: value !== rawValue.trimEnd()
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Quote the unquoted scalars that make a frontmatter block unparseable, or
|
|
2334
|
+
* return `undefined` when there is nothing to repair. Only the frontmatter
|
|
2335
|
+
* block is rewritten; the body is passed through untouched.
|
|
2336
|
+
*/
|
|
2337
|
+
function repairMalformedFrontmatterYaml(content) {
|
|
2338
|
+
const opening = /^\uFEFF?---[^\S\r\n]*\r?\n/.exec(content);
|
|
2339
|
+
if (!opening) return;
|
|
2340
|
+
const blockStart = opening[0].length;
|
|
2341
|
+
const closing = /\r?\n---/.exec(content.slice(blockStart));
|
|
2342
|
+
if (!closing) return;
|
|
2343
|
+
const blockEnd = blockStart + closing.index;
|
|
2344
|
+
const block = content.slice(blockStart, blockEnd);
|
|
2345
|
+
const repairedLines = block.split("\n").map(repairFrontmatterLine);
|
|
2346
|
+
const repairedBlock = repairedLines.map(({ line }) => line).join("\n");
|
|
2347
|
+
if (repairedBlock === block) return;
|
|
2348
|
+
return {
|
|
2349
|
+
content: content.slice(0, blockStart) + repairedBlock + content.slice(blockEnd),
|
|
2350
|
+
droppedComment: repairedLines.some(({ droppedComment }) => droppedComment)
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Parse frontmatter, retrying once with unquoted colon-bearing values quoted.
|
|
2355
|
+
*
|
|
2356
|
+
* Files authored for another client routinely carry YAML that only that
|
|
2357
|
+
* client's parser accepts — `description: Use this skill when: the user asks
|
|
2358
|
+
* about PDFs` is the case the Agent Skills client guide names. Without a retry
|
|
2359
|
+
* such a file is not merely reported, it is dropped: the lenient skill import
|
|
2360
|
+
* catches the parse error and skips the whole skill. The retry is deliberately
|
|
2361
|
+
* narrow — one pass, top-level entries only, and the original error is what
|
|
2362
|
+
* surfaces if it does not help, so a genuinely broken file still fails with the
|
|
2363
|
+
* message that describes what is actually wrong with it. A file with no closing
|
|
2364
|
+
* `---`, or one whose opening fence carries a language tag, is not repaired at
|
|
2365
|
+
* all: neither is a frontmatter block gray-matter would have read.
|
|
2366
|
+
*
|
|
2367
|
+
* @see https://agentskills.io/client-implementation/adding-skills-support
|
|
2368
|
+
*/
|
|
2369
|
+
function parseFrontmatterWithYamlRepair(content, filePath, options = {}) {
|
|
2370
|
+
try {
|
|
2371
|
+
return parseFrontmatter(content, filePath);
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
const repaired = repairMalformedFrontmatterYaml(content);
|
|
2374
|
+
if (repaired === void 0) throw error;
|
|
2375
|
+
let result;
|
|
2376
|
+
try {
|
|
2377
|
+
result = parseFrontmatter(repaired.content, filePath);
|
|
2378
|
+
} catch {
|
|
2379
|
+
throw error;
|
|
2380
|
+
}
|
|
2381
|
+
if (options.quiet === true) return result;
|
|
2382
|
+
const commentNote = repaired.droppedComment ? " Text following a space and `#` was read as a YAML comment and left out of the value." : "";
|
|
2383
|
+
warnOnceWithFallback(void 0, `Recovered malformed YAML frontmatter in ${filePath === void 0 ? "the input" : stripControlCharacters(toPosixPath(filePath))} by quoting values that contain a colon.${commentNote} Quote them in the file itself so other tools can read it too.`);
|
|
2384
|
+
return result;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2193
2387
|
//#endregion
|
|
2194
2388
|
//#region src/features/checks/rulesync-check.ts
|
|
2195
2389
|
const RulesyncCheckFrontmatterSchema = z.looseObject({
|
|
@@ -5859,8 +6053,474 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
5859
6053
|
}
|
|
5860
6054
|
};
|
|
5861
6055
|
//#endregion
|
|
6056
|
+
//#region src/utils/concurrency.ts
|
|
6057
|
+
/**
|
|
6058
|
+
* Map over items with a bounded number of operations in flight.
|
|
6059
|
+
*
|
|
6060
|
+
* `Promise.all(items.map(...))` starts every operation at once, which is fine
|
|
6061
|
+
* for a handful of paths and not fine for a directory tree of unknown size: a
|
|
6062
|
+
* few thousand concurrent `realpath` calls queue on the libuv thread pool and
|
|
6063
|
+
* hold their closures alive while they wait. Results keep the input order.
|
|
6064
|
+
*
|
|
6065
|
+
* `withSemaphore` in `src/lib/github-utils.ts` bounds concurrency too, but it
|
|
6066
|
+
* wraps one call at a time: the caller still writes `Promise.all(items.map(…))`
|
|
6067
|
+
* around it, so every item's promise chain is allocated up front. This walks a
|
|
6068
|
+
* shared cursor with `limit` workers instead, so a list of unknown size costs
|
|
6069
|
+
* `limit` pending operations rather than one per item.
|
|
6070
|
+
*/
|
|
6071
|
+
async function mapWithConcurrency({ items, limit, mapper }) {
|
|
6072
|
+
const results = Array.from({ length: items.length });
|
|
6073
|
+
let nextIndex = 0;
|
|
6074
|
+
const runWorker = async () => {
|
|
6075
|
+
while (nextIndex < items.length) {
|
|
6076
|
+
const index = nextIndex;
|
|
6077
|
+
nextIndex += 1;
|
|
6078
|
+
const item = items[index];
|
|
6079
|
+
if (item === void 0) continue;
|
|
6080
|
+
results[index] = await mapper(item);
|
|
6081
|
+
}
|
|
6082
|
+
};
|
|
6083
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
6084
|
+
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
6085
|
+
return results;
|
|
6086
|
+
}
|
|
6087
|
+
//#endregion
|
|
5862
6088
|
//#region src/types/ai-dir.ts
|
|
5863
|
-
|
|
6089
|
+
/**
|
|
6090
|
+
* Directories that hold credentials. Excluding these protects something, so
|
|
6091
|
+
* their exclusion is reported rather than silent.
|
|
6092
|
+
*/
|
|
6093
|
+
const NEVER_CARRIED_CREDENTIAL_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
6094
|
+
".ssh",
|
|
6095
|
+
".aws",
|
|
6096
|
+
".gnupg"
|
|
6097
|
+
]);
|
|
6098
|
+
/**
|
|
6099
|
+
* Directories that are refused only when the path leaves the skill directory to
|
|
6100
|
+
* reach them. These are the per-application trees of a home directory, where
|
|
6101
|
+
* naming every credential file is a list always one release behind -- `gcloud`
|
|
6102
|
+
* alone writes `credentials.db` and `application_default_credentials.json`, and
|
|
6103
|
+
* `.config/anthropic/` holds an API key. A skill that ships a `.config/` of its
|
|
6104
|
+
* own still carries it: what is refused is a link that reaches the *user's*.
|
|
6105
|
+
*
|
|
6106
|
+
* A tool home is deliberately absent. `~/.claude/skills/` and `~/.codex/` hold
|
|
6107
|
+
* the global skills this feature exists to share, so refusing a link that
|
|
6108
|
+
* reaches one would refuse the ordinary case along with the bad one.
|
|
6109
|
+
*/
|
|
6110
|
+
const NEVER_CARRIED_ESCAPING_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
6111
|
+
".config",
|
|
6112
|
+
".local",
|
|
6113
|
+
".azure",
|
|
6114
|
+
".m2",
|
|
6115
|
+
".terraform.d",
|
|
6116
|
+
".docker",
|
|
6117
|
+
".kube",
|
|
6118
|
+
"keychains"
|
|
6119
|
+
]);
|
|
6120
|
+
/**
|
|
6121
|
+
* Whether an escaping real path passes through a directory only reachable by
|
|
6122
|
+
* escaping.
|
|
6123
|
+
*
|
|
6124
|
+
* Only the segments *past* the skill directory count. A global skill lives at
|
|
6125
|
+
* `~/.config/agents/skills/<name>` for Amp, Devin and Muse alike, so judging
|
|
6126
|
+
* the whole real path would refuse a link that never left the skills tree it
|
|
6127
|
+
* was already in -- the shared-directory case, reported as a credential.
|
|
6128
|
+
*/
|
|
6129
|
+
function escapesIntoCredentialDir({ realDirPath, realFilePath }) {
|
|
6130
|
+
const normalize = (segment) => normalizePathSegment(segment.toLowerCase());
|
|
6131
|
+
const dirSegments = splitPathSegments(realDirPath).map(normalize);
|
|
6132
|
+
const fileSegments = splitPathSegments(realFilePath).map(normalize);
|
|
6133
|
+
let shared = 0;
|
|
6134
|
+
while (shared < dirSegments.length && shared < fileSegments.length && dirSegments[shared] === fileSegments[shared]) shared += 1;
|
|
6135
|
+
return (dirSegments.length - shared > 1 ? fileSegments : fileSegments.slice(shared)).some((segment) => NEVER_CARRIED_ESCAPING_DIR_NAMES.has(segment));
|
|
6136
|
+
}
|
|
6137
|
+
/**
|
|
6138
|
+
* Directories that are never part of a skill for the ordinary reasons: a
|
|
6139
|
+
* nested repository, or a build/cache tree. Leaving these out is what a user
|
|
6140
|
+
* expects, so it happens quietly.
|
|
6141
|
+
*/
|
|
6142
|
+
const NEVER_CARRIED_NOISE_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
6143
|
+
".git",
|
|
6144
|
+
".hg",
|
|
6145
|
+
".svn",
|
|
6146
|
+
".cache",
|
|
6147
|
+
".venv",
|
|
6148
|
+
".tox",
|
|
6149
|
+
".mypy_cache",
|
|
6150
|
+
".pytest_cache",
|
|
6151
|
+
".ruff_cache",
|
|
6152
|
+
".gradle",
|
|
6153
|
+
".next",
|
|
6154
|
+
".nuxt",
|
|
6155
|
+
".turbo",
|
|
6156
|
+
".parcel-cache",
|
|
6157
|
+
".nyc_output",
|
|
6158
|
+
".terraform"
|
|
6159
|
+
]);
|
|
6160
|
+
/** Files that hold credentials. Compared lower-cased. */
|
|
6161
|
+
const NEVER_CARRIED_CREDENTIAL_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
6162
|
+
".npmrc",
|
|
6163
|
+
".netrc",
|
|
6164
|
+
".git-credentials",
|
|
6165
|
+
".pgpass",
|
|
6166
|
+
".pypirc",
|
|
6167
|
+
".htpasswd",
|
|
6168
|
+
".dockercfg",
|
|
6169
|
+
".envrc"
|
|
6170
|
+
]);
|
|
6171
|
+
/** Files that are local noise. Compared lower-cased. */
|
|
6172
|
+
const NEVER_CARRIED_NOISE_FILE_NAMES = /* @__PURE__ */ new Set([".ds_store"]);
|
|
6173
|
+
/** Credential files whose parent directory is otherwise ordinary content. */
|
|
6174
|
+
const NEVER_CARRIED_PATH_SUFFIXES = [
|
|
6175
|
+
".docker/config.json",
|
|
6176
|
+
".kube/config",
|
|
6177
|
+
".config/gh/hosts.yml",
|
|
6178
|
+
".config/gcloud/credentials.db",
|
|
6179
|
+
".gem/credentials",
|
|
6180
|
+
"gcloud/application_default_credentials.json",
|
|
6181
|
+
".codex/auth.json",
|
|
6182
|
+
".gemini/oauth_creds.json"
|
|
6183
|
+
];
|
|
6184
|
+
/** Whether a path ends in one of the credential files named above. */
|
|
6185
|
+
function endsWithNeverCarriedSuffix(filePath) {
|
|
6186
|
+
const posixPath = toPosixPath(filePath).toLowerCase();
|
|
6187
|
+
return NEVER_CARRIED_PATH_SUFFIXES.some((suffix) => posixPath === suffix || posixPath.endsWith(`/${suffix}`));
|
|
6188
|
+
}
|
|
6189
|
+
/**
|
|
6190
|
+
* `.env.<suffix>` spellings that are templates rather than real values.
|
|
6191
|
+
* Everything else matching `.env*` is treated as holding secrets, because
|
|
6192
|
+
* `.env.production` is no less sensitive than `.env` itself.
|
|
6193
|
+
*/
|
|
6194
|
+
const ENV_TEMPLATE_SUFFIXES = /* @__PURE__ */ new Set([
|
|
6195
|
+
"example",
|
|
6196
|
+
"sample",
|
|
6197
|
+
"template",
|
|
6198
|
+
"dist",
|
|
6199
|
+
"defaults"
|
|
6200
|
+
]);
|
|
6201
|
+
/**
|
|
6202
|
+
* Kernel pseudo-filesystems, matched against the resolved real path. A link
|
|
6203
|
+
* into one of these does not reach a file at all: `/proc/self/environ` reads
|
|
6204
|
+
* back the entire environment of the running process, API keys included, and
|
|
6205
|
+
* `stat` reports it as an ordinary file. Nothing a skill carries lives here.
|
|
6206
|
+
*/
|
|
6207
|
+
const NEVER_CARRIED_REAL_PATH_ROOTS = [
|
|
6208
|
+
"/proc",
|
|
6209
|
+
"/sys",
|
|
6210
|
+
"/dev"
|
|
6211
|
+
];
|
|
6212
|
+
/**
|
|
6213
|
+
* Whether a directory of this name is pruned during the walk, so it is never
|
|
6214
|
+
* descended into at all. Derived from the directory names above so the pruning
|
|
6215
|
+
* and the path check below cannot drift apart.
|
|
6216
|
+
*/
|
|
6217
|
+
function isNeverCarriedDirName(dirName) {
|
|
6218
|
+
const normalized = normalizePathSegment(dirName.toLowerCase());
|
|
6219
|
+
return NEVER_CARRIED_CREDENTIAL_DIR_NAMES.has(normalized) || NEVER_CARRIED_NOISE_DIR_NAMES.has(normalized) || isCredentialFileName(normalized);
|
|
6220
|
+
}
|
|
6221
|
+
/**
|
|
6222
|
+
* Windows drops trailing dots and spaces from a name, so a file called `.env `
|
|
6223
|
+
* is written as `.env` once it lands in a tool directory there. Normalizing
|
|
6224
|
+
* before every comparison means the name is judged as what it becomes.
|
|
6225
|
+
*/
|
|
6226
|
+
function normalizePathSegment(segment) {
|
|
6227
|
+
const normalized = segment.replace(/[\s.]+$/, "");
|
|
6228
|
+
return normalized === "" ? segment : normalized;
|
|
6229
|
+
}
|
|
6230
|
+
/**
|
|
6231
|
+
* Why a path reaches something that is never skill content, or `undefined`
|
|
6232
|
+
* when it does not.
|
|
6233
|
+
*
|
|
6234
|
+
* Carrying hidden entries means a secret sitting in a skill directory would be
|
|
6235
|
+
* copied into every enabled tool root, multiplying the places it can be
|
|
6236
|
+
* committed from, and a `.venv` would be copied file by file into each of them.
|
|
6237
|
+
* None of these names is ever skill content, so excluding them costs nothing.
|
|
6238
|
+
*
|
|
6239
|
+
* The check is applied to the resolved real path as well as the literal one:
|
|
6240
|
+
* the names are what makes an entry dangerous, and a symbolic link named
|
|
6241
|
+
* `vendor` pointing at `~/.aws` is exactly as dangerous as a directory called
|
|
6242
|
+
* `.aws`. Comparison is lower-cased because macOS and Windows resolve `.SSH`
|
|
6243
|
+
* and `.ssh` to the same file.
|
|
6244
|
+
*/
|
|
6245
|
+
function classifyNeverCarried(relativePath) {
|
|
6246
|
+
const segments = toPosixPath(relativePath).toLowerCase().split("/").filter((segment) => segment !== "" && segment !== ".").map(normalizePathSegment);
|
|
6247
|
+
const fileName = segments.at(-1) ?? "";
|
|
6248
|
+
const posixPath = segments.join("/");
|
|
6249
|
+
if (segments.slice(0, -1).some((segment) => isCredentialFileName(segment))) return "credential";
|
|
6250
|
+
if (segments.some((segment) => NEVER_CARRIED_CREDENTIAL_DIR_NAMES.has(segment))) return "credential";
|
|
6251
|
+
if (segments.some((segment) => NEVER_CARRIED_NOISE_DIR_NAMES.has(segment))) return "noise";
|
|
6252
|
+
if (isCredentialFileName(fileName)) return "credential";
|
|
6253
|
+
if (NEVER_CARRIED_NOISE_FILE_NAMES.has(fileName)) return "noise";
|
|
6254
|
+
if (NEVER_CARRIED_PATH_SUFFIXES.some((suffix) => posixPath === suffix || posixPath.endsWith(`/${suffix}`))) return "credential";
|
|
6255
|
+
}
|
|
6256
|
+
/**
|
|
6257
|
+
* Whether a single path segment names a credential file, whether it is the file
|
|
6258
|
+
* itself or a directory somebody gave that name to.
|
|
6259
|
+
*/
|
|
6260
|
+
function isCredentialFileName(segment) {
|
|
6261
|
+
const name = normalizePathSegment(segment.toLowerCase());
|
|
6262
|
+
if (NEVER_CARRIED_CREDENTIAL_FILE_NAMES.has(name)) return true;
|
|
6263
|
+
for (const base of [".env", ".envrc"]) {
|
|
6264
|
+
if (name === base) return true;
|
|
6265
|
+
if (name.startsWith(`${base}.`)) {
|
|
6266
|
+
const lastPiece = name.split(".").at(-1) ?? "";
|
|
6267
|
+
return !ENV_TEMPLATE_SUFFIXES.has(lastPiece);
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
return false;
|
|
6271
|
+
}
|
|
6272
|
+
/** Whether a resolved real path points into a kernel pseudo-filesystem. */
|
|
6273
|
+
function isSystemPseudoPath(absolutePath) {
|
|
6274
|
+
const posixPath = toPosixPath(absolutePath);
|
|
6275
|
+
return NEVER_CARRIED_REAL_PATH_ROOTS.some((root) => posixPath === root || posixPath.startsWith(`${root}/`));
|
|
6276
|
+
}
|
|
6277
|
+
/** How many links a chain may be followed before it is treated as a loop. */
|
|
6278
|
+
const MAX_LINK_CHAIN_HOPS = 40;
|
|
6279
|
+
/**
|
|
6280
|
+
* Whether reaching a path goes through a kernel pseudo-filesystem, even when it
|
|
6281
|
+
* does not end in one.
|
|
6282
|
+
*
|
|
6283
|
+
* Asking `realpath` alone is not enough, and the entries it is not enough for
|
|
6284
|
+
* are the dangerous ones: `/proc/<pid>/fd/N`, `exe`, `cwd` and `root` are magic
|
|
6285
|
+
* links, so resolving them lands *outside* `/proc`, on whatever file the
|
|
6286
|
+
* process happens to hold open — a private key another program is reading right
|
|
6287
|
+
* now would come back as an ordinary path and be carried. Following the chain a
|
|
6288
|
+
* hop at a time, and checking each hop, is what sees the `/proc` in the middle.
|
|
6289
|
+
*/
|
|
6290
|
+
async function resolvesThroughSystemPseudoPath(filePath) {
|
|
6291
|
+
let currentPath = resolve(filePath);
|
|
6292
|
+
let hops = 0;
|
|
6293
|
+
for (; hops < MAX_LINK_CHAIN_HOPS; hops++) {
|
|
6294
|
+
if (isSystemPseudoPath(currentPath)) return {
|
|
6295
|
+
throughPseudoPath: true,
|
|
6296
|
+
hops
|
|
6297
|
+
};
|
|
6298
|
+
try {
|
|
6299
|
+
if (isSystemPseudoPath(await realpath(dirname(currentPath)))) return {
|
|
6300
|
+
throughPseudoPath: true,
|
|
6301
|
+
hops
|
|
6302
|
+
};
|
|
6303
|
+
} catch {}
|
|
6304
|
+
let linkStats;
|
|
6305
|
+
try {
|
|
6306
|
+
linkStats = await lstat(currentPath);
|
|
6307
|
+
} catch {
|
|
6308
|
+
return {
|
|
6309
|
+
throughPseudoPath: false,
|
|
6310
|
+
hops
|
|
6311
|
+
};
|
|
6312
|
+
}
|
|
6313
|
+
if (!linkStats.isSymbolicLink()) break;
|
|
6314
|
+
let target;
|
|
6315
|
+
try {
|
|
6316
|
+
target = await readlink(currentPath);
|
|
6317
|
+
} catch {
|
|
6318
|
+
return {
|
|
6319
|
+
throughPseudoPath: false,
|
|
6320
|
+
hops
|
|
6321
|
+
};
|
|
6322
|
+
}
|
|
6323
|
+
currentPath = isAbsolute(target) ? target : resolve(dirname(currentPath), target);
|
|
6324
|
+
}
|
|
6325
|
+
return {
|
|
6326
|
+
throughPseudoPath: false,
|
|
6327
|
+
hops
|
|
6328
|
+
};
|
|
6329
|
+
}
|
|
6330
|
+
const MAX_CARRIED_FILES = 1e4;
|
|
6331
|
+
const MAX_CARRIED_DIRECTORIES = 1e4;
|
|
6332
|
+
/**
|
|
6333
|
+
* The bounds above limit what is *carried*; this one limits what is *looked at*.
|
|
6334
|
+
* A directory holding nothing but a few hundred thousand links to itself carries
|
|
6335
|
+
* no files and occupies no depth, and would still cost a `stat` apiece.
|
|
6336
|
+
*/
|
|
6337
|
+
const MAX_CARRIED_ENTRIES_EXAMINED = 2e5;
|
|
6338
|
+
const MAX_CARRIED_BYTES = 104857600;
|
|
6339
|
+
/** How many `realpath` calls the carried-file filter keeps in flight. */
|
|
6340
|
+
const CARRIED_REALPATH_CONCURRENCY = 32;
|
|
6341
|
+
/** Sort directory entries by name so a walk of the same tree is reproducible. */
|
|
6342
|
+
function compareByName(left, right) {
|
|
6343
|
+
if (left.name === right.name) return 0;
|
|
6344
|
+
return left.name < right.name ? -1 : 1;
|
|
6345
|
+
}
|
|
6346
|
+
/**
|
|
6347
|
+
* Order the routes that cross the same number of symbolic links: the one with
|
|
6348
|
+
* the fewest hidden segments first, so a named alias represents a shared tree
|
|
6349
|
+
* rather than a hidden one that a hidden-entry rule then refuses, taking the
|
|
6350
|
+
* named route's content with it.
|
|
6351
|
+
*/
|
|
6352
|
+
function comparePendingCarriedDirs(left, right) {
|
|
6353
|
+
if (left.hiddenSegments !== right.hiddenSegments) return left.hiddenSegments - right.hiddenSegments;
|
|
6354
|
+
if (left.depth !== right.depth) return left.depth - right.depth;
|
|
6355
|
+
if (left.dirPath === right.dirPath) return 0;
|
|
6356
|
+
return left.dirPath < right.dirPath ? -1 : 1;
|
|
6357
|
+
}
|
|
6358
|
+
/**
|
|
6359
|
+
* Collect the files under a carried directory, following symbolic links but
|
|
6360
|
+
* visiting each real directory exactly once, by its cheapest route.
|
|
6361
|
+
*
|
|
6362
|
+
* A glob walk cannot do this safely. Two links in one directory that both point
|
|
6363
|
+
* back at an ancestor double the paths walked per level, and the walker follows
|
|
6364
|
+
* them until the kernel's ELOOP limit (~40), so the path array alone exhausts
|
|
6365
|
+
* the heap long before anything reads a file — a depth bound only lowers the
|
|
6366
|
+
* exponent, while the base is whatever number of links the tree's author chose.
|
|
6367
|
+
* Remembering the real directories already visited removes the multiplication
|
|
6368
|
+
* itself: a cycle, and an alias for a directory already walked, both stop at the
|
|
6369
|
+
* entry that closes them.
|
|
6370
|
+
*
|
|
6371
|
+
* Which route represents a directory then matters, because the others are
|
|
6372
|
+
* dropped. The walk proceeds in rounds by the number of symbolic links crossed:
|
|
6373
|
+
* everything reachable without crossing one, then everything one link away, and
|
|
6374
|
+
* so on. A real location therefore always wins over an alias for it — at any
|
|
6375
|
+
* nesting depth, not just among siblings, which a depth-first walk could not
|
|
6376
|
+
* promise — and among aliases the named one wins over a hidden one.
|
|
6377
|
+
*
|
|
6378
|
+
* A broken link is skipped: it resolves to nothing to read.
|
|
6379
|
+
*/
|
|
6380
|
+
async function walkCarriedFiles(dirPath, { skipHiddenRoutes = false } = {}) {
|
|
6381
|
+
const filePaths = [];
|
|
6382
|
+
const visitedRealDirPaths = /* @__PURE__ */ new Set();
|
|
6383
|
+
const truncations = /* @__PURE__ */ new Set();
|
|
6384
|
+
const unreadablePaths = /* @__PURE__ */ new Set();
|
|
6385
|
+
const pseudoPaths = /* @__PURE__ */ new Set();
|
|
6386
|
+
const depthStoppedRealDirPaths = /* @__PURE__ */ new Set();
|
|
6387
|
+
let deferredLinkedDirs = [];
|
|
6388
|
+
/** Whether the walk has hit a bound that ends it rather than one branch of it. */
|
|
6389
|
+
let examinedEntries = 0;
|
|
6390
|
+
const isFull = () => truncations.has("count") || truncations.has("directories") || truncations.has("entries");
|
|
6391
|
+
const addFile = (filePath) => {
|
|
6392
|
+
if (filePaths.length >= 1e4) {
|
|
6393
|
+
truncations.add("count");
|
|
6394
|
+
return;
|
|
6395
|
+
}
|
|
6396
|
+
filePaths.push(filePath);
|
|
6397
|
+
};
|
|
6398
|
+
/** Carry what a symbolic link names, or hold its directory for the next round. */
|
|
6399
|
+
const routeLinkedEntry = async (child, entryName) => {
|
|
6400
|
+
let targetStats;
|
|
6401
|
+
try {
|
|
6402
|
+
targetStats = await stat(child.dirPath);
|
|
6403
|
+
} catch {
|
|
6404
|
+
return;
|
|
6405
|
+
}
|
|
6406
|
+
if (targetStats.isFile()) {
|
|
6407
|
+
addFile(child.dirPath);
|
|
6408
|
+
return;
|
|
6409
|
+
}
|
|
6410
|
+
if (!targetStats.isDirectory() || isNeverCarriedDirName(entryName)) return;
|
|
6411
|
+
const { throughPseudoPath, hops } = await resolvesThroughSystemPseudoPath(child.dirPath);
|
|
6412
|
+
examinedEntries += hops;
|
|
6413
|
+
if (throughPseudoPath) {
|
|
6414
|
+
pseudoPaths.add(child.dirPath);
|
|
6415
|
+
return;
|
|
6416
|
+
}
|
|
6417
|
+
deferredLinkedDirs.push(child);
|
|
6418
|
+
};
|
|
6419
|
+
/** Walk one directory and everything below it that no symbolic link leads to. */
|
|
6420
|
+
const walkWithoutCrossingLinks = async (pending) => {
|
|
6421
|
+
if (isFull()) return;
|
|
6422
|
+
let realCurrentPath;
|
|
6423
|
+
try {
|
|
6424
|
+
realCurrentPath = await realpath(pending.dirPath);
|
|
6425
|
+
} catch {
|
|
6426
|
+
unreadablePaths.add(pending.dirPath);
|
|
6427
|
+
return;
|
|
6428
|
+
}
|
|
6429
|
+
if (visitedRealDirPaths.has(realCurrentPath)) return;
|
|
6430
|
+
if (isSystemPseudoPath(realCurrentPath)) {
|
|
6431
|
+
pseudoPaths.add(pending.dirPath);
|
|
6432
|
+
return;
|
|
6433
|
+
}
|
|
6434
|
+
if (pending.depth > 12) {
|
|
6435
|
+
depthStoppedRealDirPaths.add(realCurrentPath);
|
|
6436
|
+
return;
|
|
6437
|
+
}
|
|
6438
|
+
if (visitedRealDirPaths.size >= 1e4) {
|
|
6439
|
+
truncations.add("directories");
|
|
6440
|
+
return;
|
|
6441
|
+
}
|
|
6442
|
+
visitedRealDirPaths.add(realCurrentPath);
|
|
6443
|
+
let entries;
|
|
6444
|
+
try {
|
|
6445
|
+
entries = await readdir(pending.dirPath, { withFileTypes: true });
|
|
6446
|
+
} catch {
|
|
6447
|
+
unreadablePaths.add(pending.dirPath);
|
|
6448
|
+
return;
|
|
6449
|
+
}
|
|
6450
|
+
const realSubDirs = [];
|
|
6451
|
+
for (const entry of entries.toSorted(compareByName)) {
|
|
6452
|
+
if (isFull()) return;
|
|
6453
|
+
examinedEntries += 1;
|
|
6454
|
+
if (examinedEntries > 2e5) {
|
|
6455
|
+
truncations.add("entries");
|
|
6456
|
+
return;
|
|
6457
|
+
}
|
|
6458
|
+
if (skipHiddenRoutes && isHiddenPathSegment(entry.name)) continue;
|
|
6459
|
+
const entryPath = join(pending.dirPath, entry.name);
|
|
6460
|
+
const child = {
|
|
6461
|
+
dirPath: entryPath,
|
|
6462
|
+
depth: pending.depth + 1,
|
|
6463
|
+
hiddenSegments: pending.hiddenSegments + (isHiddenPathSegment(entry.name) ? 1 : 0)
|
|
6464
|
+
};
|
|
6465
|
+
if (entry.isFile()) {
|
|
6466
|
+
addFile(entryPath);
|
|
6467
|
+
continue;
|
|
6468
|
+
}
|
|
6469
|
+
if (entry.isDirectory()) {
|
|
6470
|
+
if (!isNeverCarriedDirName(entry.name)) realSubDirs.push(child);
|
|
6471
|
+
continue;
|
|
6472
|
+
}
|
|
6473
|
+
if (!entry.isSymbolicLink()) continue;
|
|
6474
|
+
await routeLinkedEntry(child, entry.name);
|
|
6475
|
+
}
|
|
6476
|
+
for (const realSubDir of realSubDirs) {
|
|
6477
|
+
if (isFull()) return;
|
|
6478
|
+
await walkWithoutCrossingLinks(realSubDir);
|
|
6479
|
+
}
|
|
6480
|
+
};
|
|
6481
|
+
let round = [{
|
|
6482
|
+
dirPath,
|
|
6483
|
+
depth: 0,
|
|
6484
|
+
hiddenSegments: 0
|
|
6485
|
+
}];
|
|
6486
|
+
while (round.length > 0 && !isFull()) {
|
|
6487
|
+
deferredLinkedDirs = [];
|
|
6488
|
+
for (const pending of round.toSorted(comparePendingCarriedDirs)) {
|
|
6489
|
+
if (isFull()) break;
|
|
6490
|
+
await walkWithoutCrossingLinks(pending);
|
|
6491
|
+
}
|
|
6492
|
+
round = deferredLinkedDirs;
|
|
6493
|
+
}
|
|
6494
|
+
for (const realDirPath of depthStoppedRealDirPaths) if (!visitedRealDirPaths.has(realDirPath)) {
|
|
6495
|
+
truncations.add("depth");
|
|
6496
|
+
break;
|
|
6497
|
+
}
|
|
6498
|
+
return {
|
|
6499
|
+
filePaths: filePaths.toSorted(),
|
|
6500
|
+
truncations,
|
|
6501
|
+
unreadablePaths: [...unreadablePaths],
|
|
6502
|
+
pseudoPaths: [...pseudoPaths]
|
|
6503
|
+
};
|
|
6504
|
+
}
|
|
6505
|
+
/** Render a set of refused or noteworthy paths for one warning line. */
|
|
6506
|
+
function formatReportedPaths(paths) {
|
|
6507
|
+
const sorted = [...paths].toSorted();
|
|
6508
|
+
const named = sorted.slice(0, 10).map((filePath) => stripControlCharacters(toPosixPath(filePath))).join(", ");
|
|
6509
|
+
const remaining = sorted.length - 10;
|
|
6510
|
+
return {
|
|
6511
|
+
count: sorted.length,
|
|
6512
|
+
list: `${named}${remaining > 0 ? `, and ${remaining} more` : ""}`
|
|
6513
|
+
};
|
|
6514
|
+
}
|
|
6515
|
+
/** "entry" or "entries", so the warnings below read as sentences. */
|
|
6516
|
+
function entryWord(count) {
|
|
6517
|
+
return count === 1 ? "entry" : "entries";
|
|
6518
|
+
}
|
|
6519
|
+
/** "resolves" or "resolve", to agree with the entry count it follows. */
|
|
6520
|
+
function resolveWord(count) {
|
|
6521
|
+
return count === 1 ? "resolves" : "resolve";
|
|
6522
|
+
}
|
|
6523
|
+
var AiDir = class AiDir {
|
|
5864
6524
|
/**
|
|
5865
6525
|
* @example "."
|
|
5866
6526
|
*/
|
|
@@ -5910,8 +6570,7 @@ var AiDir = class {
|
|
|
5910
6570
|
const fullPath = path.join(this.outputRoot, this.relativeDirPath, this.dirName);
|
|
5911
6571
|
const resolvedFull = resolve(fullPath);
|
|
5912
6572
|
const resolvedBase = resolve(this.outputRoot);
|
|
5913
|
-
|
|
5914
|
-
if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}", dirName="${this.dirName}"`);
|
|
6573
|
+
if (pathEscapesRoot(relative(resolvedBase, resolvedFull))) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}", dirName="${this.dirName}"`);
|
|
5915
6574
|
return fullPath;
|
|
5916
6575
|
}
|
|
5917
6576
|
getMainFile() {
|
|
@@ -5937,9 +6596,227 @@ var AiDir = class {
|
|
|
5937
6596
|
};
|
|
5938
6597
|
}
|
|
5939
6598
|
/**
|
|
6599
|
+
* A nested repository inside a carried directory is the one exclusion worth
|
|
6600
|
+
* reporting: unlike `.DS_Store`, it is there on purpose, and the tree it
|
|
6601
|
+
* points at is simply not reproduced on generate. Only the top level is
|
|
6602
|
+
* checked, which is where a submodule or a stray `git init` puts it, so the
|
|
6603
|
+
* check costs one stat per directory rather than a second walk. `fileExists`
|
|
6604
|
+
* is a bare `stat`, so it answers for a submodule pointer file and for a real
|
|
6605
|
+
* `.git` directory alike.
|
|
6606
|
+
*/
|
|
6607
|
+
static async warnOnNestedGitDirectory(dirPath) {
|
|
6608
|
+
const gitEntryPath = join(dirPath, ".git");
|
|
6609
|
+
if (await fileExists(gitEntryPath)) warnOnceWithFallback(void 0, `Not carrying ${stripControlCharacters(toPosixPath(gitEntryPath))} with its directory: a nested repository is excluded, so the files it tracks are copied but its history is not.`);
|
|
6610
|
+
}
|
|
6611
|
+
/** Whether any segment of a relative path is dot-prefixed. */
|
|
6612
|
+
static hasHiddenSegment(relativePath) {
|
|
6613
|
+
return splitPathSegments(relativePath).some(isHiddenPathSegment);
|
|
6614
|
+
}
|
|
6615
|
+
/**
|
|
6616
|
+
* Whether the entry a path ends at is itself hidden, its ancestors aside.
|
|
6617
|
+
* A shared skill tree usually lives under a dot-directory, so an ancestor
|
|
6618
|
+
* says nothing about the file; its own name is what was chosen for it.
|
|
6619
|
+
*/
|
|
6620
|
+
static hasHiddenName(relativePath) {
|
|
6621
|
+
return isHiddenPathSegment(splitPathSegments(relativePath).at(-1) ?? "");
|
|
6622
|
+
}
|
|
6623
|
+
/**
|
|
6624
|
+
* Drop the entries a skill directory must not carry.
|
|
6625
|
+
*
|
|
6626
|
+
* Three rules. `classifyNeverCarried` comes first, evaluated against the
|
|
6627
|
+
* resolved real path as well as the literal one: names that are never skill
|
|
6628
|
+
* content stay out however they are reached, so renaming a symbolic link
|
|
6629
|
+
* does not turn `~/.aws` into content a skill carries. Next, a real path
|
|
6630
|
+
* inside a kernel pseudo-filesystem is refused outright — that is process
|
|
6631
|
+
* state, not a file.
|
|
6632
|
+
*
|
|
6633
|
+
* The third concerns hidden entries a symbolic link reaches outside the
|
|
6634
|
+
* directory. Following symlinks out of a source tree is deliberate and
|
|
6635
|
+
* documented — it is how a shared skill is referenced from several projects
|
|
6636
|
+
* without being duplicated (issue #1707), and the trust boundary is the tree
|
|
6637
|
+
* you point Rulesync at. Carrying hidden entries changes what that costs,
|
|
6638
|
+
* though: one ordinary-looking link to a home directory would pull in every
|
|
6639
|
+
* dotfile under it, and those are the entries with credential value. What
|
|
6640
|
+
* decides is the name in the skill directory, not what the link resolves
|
|
6641
|
+
* through: a named file keeps its documented behavior even when the target
|
|
6642
|
+
* sits under a dot-directory such as `~/.dotfiles`, because somebody chose
|
|
6643
|
+
* that name. What the link resolves *to* still counts at the end of the path,
|
|
6644
|
+
* though — `notes.md` pointing at `~/.claude/.credentials.json` reaches a
|
|
6645
|
+
* file nobody named for a skill — so a hidden final segment is refused on
|
|
6646
|
+
* either side. Reaching outside is reported either way, since content from
|
|
6647
|
+
* outside the tree is about to be copied into every enabled tool root.
|
|
6648
|
+
*
|
|
6649
|
+
* A path that cannot be resolved is kept: `realpath` fails on a broken link
|
|
6650
|
+
* or a race, and neither is a reason to silently drop a file.
|
|
6651
|
+
*/
|
|
6652
|
+
/** Report what a carried directory left out, once both walks have had their say. */
|
|
6653
|
+
static warnOnRefusedCarriedFiles(dirPath, carried) {
|
|
6654
|
+
const reportedDirPath = stripControlCharacters(toPosixPath(dirPath));
|
|
6655
|
+
if (carried.refusedCredentials.size > 0) {
|
|
6656
|
+
const { count, list } = formatReportedPaths(carried.refusedCredentials);
|
|
6657
|
+
warnOnceWithFallback(void 0, `Not carrying ${count} ${entryWord(count)} named as a credential store: ${list}. A skill must not ship secrets; read them from the environment instead.`);
|
|
6658
|
+
}
|
|
6659
|
+
if (carried.refusedPseudoPaths.size > 0) {
|
|
6660
|
+
const { count, list } = formatReportedPaths(carried.refusedPseudoPaths);
|
|
6661
|
+
warnOnceWithFallback(void 0, `Not carrying ${count} ${entryWord(count)} that ${resolveWord(count)} into a system pseudo-filesystem: ${list}. Those read back process state, not skill content.`);
|
|
6662
|
+
}
|
|
6663
|
+
if (carried.refusedNoiseAliases.size > 0) {
|
|
6664
|
+
const { count, list } = formatReportedPaths(carried.refusedNoiseAliases);
|
|
6665
|
+
warnOnceWithFallback(void 0, `Not carrying ${count} ${entryWord(count)} that ${resolveWord(count)} into a directory a skill never carries: ${list}. A nested repository, or a build or cache tree, is the usual target.`);
|
|
6666
|
+
}
|
|
6667
|
+
if (carried.refusedEscapedHidden.size > 0) {
|
|
6668
|
+
const { count, list } = formatReportedPaths(carried.refusedEscapedHidden);
|
|
6669
|
+
warnOnceWithFallback(void 0, `Not carrying ${count} hidden ${entryWord(count)} that ${resolveWord(count)} outside ${reportedDirPath}: ${list}. Copy them into the directory if the skill really needs them.`);
|
|
6670
|
+
}
|
|
6671
|
+
if (carried.carriedFromOutside.size > 0) {
|
|
6672
|
+
const { count, list } = formatReportedPaths([...carried.carriedFromOutside].map((filePath) => {
|
|
6673
|
+
const realFilePath = carried.realFilePathByPath.get(filePath);
|
|
6674
|
+
return realFilePath === void 0 ? filePath : `${filePath} -> ${realFilePath}`;
|
|
6675
|
+
}));
|
|
6676
|
+
warnOnceWithFallback(void 0, `Carrying ${count} ${entryWord(count)} that ${resolveWord(count)} outside ${reportedDirPath}: ${list}. Their content is copied into every generated tool directory.`);
|
|
6677
|
+
}
|
|
6678
|
+
}
|
|
6679
|
+
static async filterCarriedFiles(dirPath, filePaths) {
|
|
6680
|
+
let realDirPath;
|
|
6681
|
+
try {
|
|
6682
|
+
realDirPath = await realpath(dirPath);
|
|
6683
|
+
} catch {
|
|
6684
|
+
realDirPath = resolve(dirPath);
|
|
6685
|
+
}
|
|
6686
|
+
const refusedCredentials = /* @__PURE__ */ new Set();
|
|
6687
|
+
const refusedPseudoPaths = /* @__PURE__ */ new Set();
|
|
6688
|
+
const refusedEscapedHidden = /* @__PURE__ */ new Set();
|
|
6689
|
+
const refusedNoiseAliases = /* @__PURE__ */ new Set();
|
|
6690
|
+
const carriedFromOutside = /* @__PURE__ */ new Set();
|
|
6691
|
+
const realFilePathByPath = /* @__PURE__ */ new Map();
|
|
6692
|
+
const verdicts = await mapWithConcurrency({
|
|
6693
|
+
items: filePaths,
|
|
6694
|
+
limit: CARRIED_REALPATH_CONCURRENCY,
|
|
6695
|
+
mapper: async (filePath) => {
|
|
6696
|
+
const literalPath = relative(dirPath, filePath);
|
|
6697
|
+
const literalReason = classifyNeverCarried(literalPath);
|
|
6698
|
+
if (literalReason !== void 0) {
|
|
6699
|
+
if (literalReason === "credential") refusedCredentials.add(filePath);
|
|
6700
|
+
return false;
|
|
6701
|
+
}
|
|
6702
|
+
let realFilePath;
|
|
6703
|
+
try {
|
|
6704
|
+
realFilePath = await realpath(filePath);
|
|
6705
|
+
} catch {
|
|
6706
|
+
return true;
|
|
6707
|
+
}
|
|
6708
|
+
realFilePathByPath.set(filePath, realFilePath);
|
|
6709
|
+
if (isSystemPseudoPath(realFilePath)) {
|
|
6710
|
+
refusedPseudoPaths.add(filePath);
|
|
6711
|
+
return false;
|
|
6712
|
+
}
|
|
6713
|
+
const realPath = relative(realDirPath, realFilePath);
|
|
6714
|
+
const realReason = classifyNeverCarried(realPath);
|
|
6715
|
+
if (realReason !== void 0) {
|
|
6716
|
+
if (realReason === "credential") refusedCredentials.add(filePath);
|
|
6717
|
+
else refusedNoiseAliases.add(filePath);
|
|
6718
|
+
return false;
|
|
6719
|
+
}
|
|
6720
|
+
if (!pathEscapesRoot(realPath)) return true;
|
|
6721
|
+
if ((await resolvesThroughSystemPseudoPath(filePath)).throughPseudoPath) {
|
|
6722
|
+
refusedPseudoPaths.add(filePath);
|
|
6723
|
+
return false;
|
|
6724
|
+
}
|
|
6725
|
+
if (endsWithNeverCarriedSuffix(realFilePath)) {
|
|
6726
|
+
refusedCredentials.add(filePath);
|
|
6727
|
+
return false;
|
|
6728
|
+
}
|
|
6729
|
+
if (escapesIntoCredentialDir({
|
|
6730
|
+
realDirPath,
|
|
6731
|
+
realFilePath
|
|
6732
|
+
})) {
|
|
6733
|
+
refusedCredentials.add(filePath);
|
|
6734
|
+
return false;
|
|
6735
|
+
}
|
|
6736
|
+
if (AiDir.hasHiddenSegment(literalPath) || AiDir.hasHiddenName(realPath)) {
|
|
6737
|
+
refusedEscapedHidden.add(filePath);
|
|
6738
|
+
return false;
|
|
6739
|
+
}
|
|
6740
|
+
carriedFromOutside.add(filePath);
|
|
6741
|
+
return true;
|
|
6742
|
+
}
|
|
6743
|
+
});
|
|
6744
|
+
return {
|
|
6745
|
+
filePaths: filePaths.filter((_filePath, index) => verdicts[index]),
|
|
6746
|
+
realFilePathByPath,
|
|
6747
|
+
refusedCredentials,
|
|
6748
|
+
refusedPseudoPaths,
|
|
6749
|
+
refusedEscapedHidden,
|
|
6750
|
+
refusedNoiseAliases,
|
|
6751
|
+
carriedFromOutside
|
|
6752
|
+
};
|
|
6753
|
+
}
|
|
6754
|
+
/**
|
|
6755
|
+
* Report what the walk had to leave behind, so a skill that silently lost
|
|
6756
|
+
* files says so rather than generating a directory that is quietly short.
|
|
6757
|
+
*/
|
|
6758
|
+
static warnOnCarriedWalkLimits({ reportedDirPath, truncations, unreadablePaths }) {
|
|
6759
|
+
if (truncations.has("depth")) warnOnceWithFallback(void 0, `Not carrying the entries more than 12 directories below ${reportedDirPath}: a skill directory is walked to that depth only. A symbolic link that reaches a large tree is the usual cause.`);
|
|
6760
|
+
if (truncations.has("count")) warnOnceWithFallback(void 0, `Not carrying the entries under ${reportedDirPath} beyond the first ${MAX_CARRIED_FILES}: a directory may carry at most that many files. A symbolic link that reaches a large tree is the usual cause.`);
|
|
6761
|
+
if (truncations.has("directories")) warnOnceWithFallback(void 0, `Not carrying the entries under ${reportedDirPath} below the first ${MAX_CARRIED_DIRECTORIES} directories: a directory may carry files from at most that many directories. A symbolic link that reaches a large tree is the usual cause.`);
|
|
6762
|
+
if (truncations.has("entries")) warnOnceWithFallback(void 0, `Not carrying the entries under ${reportedDirPath} that come after the first ${MAX_CARRIED_ENTRIES_EXAMINED} looked at: a directory is walked over at most that many entries. A tree of symbolic links that lead back into it is the usual cause.`);
|
|
6763
|
+
if (unreadablePaths.length > 0) {
|
|
6764
|
+
const { count, list } = formatReportedPaths(unreadablePaths);
|
|
6765
|
+
warnOnceWithFallback(void 0, `Not carrying ${count} ${entryWord(count)} that could not be read: ${list}. A permission the current user does not hold is the usual cause.`);
|
|
6766
|
+
}
|
|
6767
|
+
}
|
|
6768
|
+
/**
|
|
6769
|
+
* Walk the directory once more with the hidden routes pruned, and take the
|
|
6770
|
+
* files the first walk had to leave behind because the route that reached
|
|
6771
|
+
* them ran through a hidden directory. This can only add files, and only ones
|
|
6772
|
+
* a fully named route reaches.
|
|
6773
|
+
*/
|
|
6774
|
+
static async recoverCarriedFilesFromNamedRoutes({ dirPath, carried, carriedPaths, carriedRealPaths, walk }) {
|
|
6775
|
+
const named = await walkCarriedFiles(dirPath, { skipHiddenRoutes: true });
|
|
6776
|
+
const recovered = await AiDir.filterCarriedFiles(dirPath, named.filePaths);
|
|
6777
|
+
for (const truncation of named.truncations) walk.truncations.add(truncation);
|
|
6778
|
+
for (const unreadablePath of named.unreadablePaths) if (!walk.unreadablePaths.includes(unreadablePath)) walk.unreadablePaths.push(unreadablePath);
|
|
6779
|
+
for (const pseudoPath of named.pseudoPaths) carried.refusedPseudoPaths.add(pseudoPath);
|
|
6780
|
+
for (const refused of recovered.refusedCredentials) carried.refusedCredentials.add(refused);
|
|
6781
|
+
for (const refused of recovered.refusedPseudoPaths) carried.refusedPseudoPaths.add(refused);
|
|
6782
|
+
for (const refused of recovered.refusedNoiseAliases) carried.refusedNoiseAliases.add(refused);
|
|
6783
|
+
for (const refused of recovered.refusedEscapedHidden) carried.refusedEscapedHidden.add(refused);
|
|
6784
|
+
for (const filePath of recovered.filePaths) {
|
|
6785
|
+
const realFilePath = recovered.realFilePathByPath.get(filePath) ?? filePath;
|
|
6786
|
+
if (carriedRealPaths.has(realFilePath)) continue;
|
|
6787
|
+
if (carriedPaths.length >= 1e4) {
|
|
6788
|
+
walk.truncations.add("count");
|
|
6789
|
+
break;
|
|
6790
|
+
}
|
|
6791
|
+
carriedRealPaths.add(realFilePath);
|
|
6792
|
+
carriedPaths.push(filePath);
|
|
6793
|
+
carried.realFilePathByPath.set(filePath, realFilePath);
|
|
6794
|
+
if (recovered.carriedFromOutside.has(filePath)) carried.carriedFromOutside.add(filePath);
|
|
6795
|
+
}
|
|
6796
|
+
for (const filePath of carried.refusedEscapedHidden) {
|
|
6797
|
+
const realFilePath = carried.realFilePathByPath.get(filePath) ?? filePath;
|
|
6798
|
+
if (carriedRealPaths.has(realFilePath)) carried.refusedEscapedHidden.delete(filePath);
|
|
6799
|
+
}
|
|
6800
|
+
}
|
|
6801
|
+
/**
|
|
5940
6802
|
* Recursively collects all files from a directory, excluding the specified main file.
|
|
5941
6803
|
* This is a common utility for loading additional files alongside the main file.
|
|
5942
6804
|
*
|
|
6805
|
+
* Hidden entries are included. The directories this walks are skill trees,
|
|
6806
|
+
* whose specification says a skill directory "may contain any files and
|
|
6807
|
+
* directories beyond the required `SKILL.md`" — a `.env.example` beside the
|
|
6808
|
+
* scripts that read it is content, not noise, and dropping it silently on
|
|
6809
|
+
* both import and generate loses part of the skill. What is left out is the
|
|
6810
|
+
* set of entries that are never skill content — a nested repository's `.git`,
|
|
6811
|
+
* the macOS Finder's `.DS_Store`, credential stores, build and cache trees —
|
|
6812
|
+
* as decided by `classifyNeverCarried`. Whole directories from that set are
|
|
6813
|
+
* pruned during the walk too, so it never descends into them at all.
|
|
6814
|
+
*
|
|
6815
|
+
* The walk is bounded and cycle-aware — see `walkCarriedFiles` — because the
|
|
6816
|
+
* tree may contain symbolic links that somebody else chose.
|
|
6817
|
+
*
|
|
6818
|
+
* @see https://agentskills.io/specification
|
|
6819
|
+
*
|
|
5943
6820
|
* @param outputRoot - The base directory path
|
|
5944
6821
|
* @param relativeDirPath - The relative path to the directory containing the skill
|
|
5945
6822
|
* @param dirName - The name of the directory
|
|
@@ -5948,14 +6825,57 @@ var AiDir = class {
|
|
|
5948
6825
|
*/
|
|
5949
6826
|
static async collectOtherFiles(outputRoot, relativeDirPath, dirName, excludeFileName) {
|
|
5950
6827
|
const dirPath = join(outputRoot, relativeDirPath, dirName);
|
|
5951
|
-
const
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
6828
|
+
const walk = await walkCarriedFiles(dirPath);
|
|
6829
|
+
const reportedDirPath = stripControlCharacters(toPosixPath(dirPath));
|
|
6830
|
+
await AiDir.warnOnNestedGitDirectory(dirPath);
|
|
6831
|
+
const carried = await AiDir.filterCarriedFiles(dirPath, walk.filePaths);
|
|
6832
|
+
const carriedPaths = [...carried.filePaths];
|
|
6833
|
+
const carriedRealPaths = new Set(carriedPaths.map((filePath) => carried.realFilePathByPath.get(filePath) ?? filePath));
|
|
6834
|
+
if (carried.refusedEscapedHidden.size > 0) await AiDir.recoverCarriedFilesFromNamedRoutes({
|
|
6835
|
+
dirPath,
|
|
6836
|
+
carried,
|
|
6837
|
+
carriedPaths,
|
|
6838
|
+
carriedRealPaths,
|
|
6839
|
+
walk
|
|
6840
|
+
});
|
|
6841
|
+
for (const pseudoPath of walk.pseudoPaths) carried.refusedPseudoPaths.add(pseudoPath);
|
|
6842
|
+
AiDir.warnOnCarriedWalkLimits({
|
|
6843
|
+
reportedDirPath,
|
|
6844
|
+
truncations: walk.truncations,
|
|
6845
|
+
unreadablePaths: walk.unreadablePaths
|
|
6846
|
+
});
|
|
6847
|
+
AiDir.warnOnRefusedCarriedFiles(dirPath, carried);
|
|
6848
|
+
const filteredPaths = carriedPaths.toSorted().filter((filePath) => basename(filePath) !== excludeFileName);
|
|
6849
|
+
const files = [];
|
|
6850
|
+
let carriedBytes = 0;
|
|
6851
|
+
for (const [index, filePath] of filteredPaths.entries()) {
|
|
6852
|
+
const classifiedPath = carried.realFilePathByPath.get(filePath) ?? filePath;
|
|
6853
|
+
let fileHandle;
|
|
6854
|
+
try {
|
|
6855
|
+
fileHandle = await open(classifiedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
6856
|
+
} catch (error) {
|
|
6857
|
+
warnOnceWithFallback(void 0, `Not carrying ${stripControlCharacters(toPosixPath(filePath))}: ${stripControlCharacters(formatError(error))}.`);
|
|
6858
|
+
continue;
|
|
6859
|
+
}
|
|
6860
|
+
try {
|
|
6861
|
+
const fileSize = (await fileHandle.stat()).size;
|
|
6862
|
+
if (carriedBytes + fileSize > 104857600) {
|
|
6863
|
+
warnOnceWithFallback(void 0, `Not carrying ${filteredPaths.length - index} of the ${filteredPaths.length} entries under ${reportedDirPath}: a directory may carry at most ${MAX_CARRIED_BYTES / 1024 / 1024}MB. A symbolic link that reaches a large tree is the usual cause.`);
|
|
6864
|
+
break;
|
|
6865
|
+
}
|
|
6866
|
+
const fileBuffer = await fileHandle.readFile();
|
|
6867
|
+
carriedBytes += fileBuffer.byteLength;
|
|
6868
|
+
files.push({
|
|
6869
|
+
relativeFilePathToDirPath: relative(dirPath, filePath),
|
|
6870
|
+
fileBuffer
|
|
6871
|
+
});
|
|
6872
|
+
} catch (error) {
|
|
6873
|
+
warnOnceWithFallback(void 0, `Not carrying ${stripControlCharacters(toPosixPath(filePath))}: ${stripControlCharacters(formatError(error))}.`);
|
|
6874
|
+
} finally {
|
|
6875
|
+
await fileHandle.close();
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
return files;
|
|
5959
6879
|
}
|
|
5960
6880
|
};
|
|
5961
6881
|
const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
@@ -6182,7 +7102,7 @@ var RulesyncSkill = class RulesyncSkill extends AiDir {
|
|
|
6182
7102
|
const skillDirPath = join(outputRoot, relativeDirPath, dirName);
|
|
6183
7103
|
const skillFilePath = join(skillDirPath, SKILL_FILE_NAME);
|
|
6184
7104
|
if (!await fileExists(skillFilePath)) throw new Error(`${SKILL_FILE_NAME} not found in ${skillDirPath}`);
|
|
6185
|
-
const { frontmatter, body: content, hasFrontmatter } =
|
|
7105
|
+
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatterWithYamlRepair(await readFileContent(skillFilePath), skillFilePath);
|
|
6186
7106
|
if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${skillFilePath}. Rulesync files must begin with a YAML frontmatter block delimited by '---'.`);
|
|
6187
7107
|
const result = RulesyncSkillFrontmatterSchema.safeParse(frontmatter);
|
|
6188
7108
|
if (!result.success) throw new Error(`Invalid frontmatter in ${skillFilePath}: ${formatError(result.error)}`);
|
|
@@ -6619,27 +7539,6 @@ function companionFileContentsEquivalent({ filePath, expected, existing, compose
|
|
|
6619
7539
|
return tryFileContentsEquivalent(filePath, expectedText, existingText) ?? false;
|
|
6620
7540
|
}
|
|
6621
7541
|
//#endregion
|
|
6622
|
-
//#region src/utils/control-characters.ts
|
|
6623
|
-
/**
|
|
6624
|
-
* Matches C0 controls, DEL, the C1 range (which includes the 8-bit CSI
|
|
6625
|
-
* introducer U+009B), the bidirectional overrides and isolates, and the Unicode
|
|
6626
|
-
* line and paragraph separators, and the plain LRM/RLM marks. A name or value
|
|
6627
|
-
* copied out of an untrusted config file, a fetched repository, or a tool's own
|
|
6628
|
-
* settings file must never reach the terminal with these intact: they let the
|
|
6629
|
-
* text forge log lines, reorder what is printed around them, or inject escape
|
|
6630
|
-
* sequences. LRM/RLM open no bidi scope of their own, but they still reorder the
|
|
6631
|
-
* neutral characters beside them, so they go too — a diagnostic line is not the
|
|
6632
|
-
* place to preserve the typography of a right-to-left name.
|
|
6633
|
-
*/
|
|
6634
|
-
const CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g;
|
|
6635
|
-
/**
|
|
6636
|
-
* Removes every control character from `text` so it is safe to splice into a
|
|
6637
|
-
* log line or other terminal output.
|
|
6638
|
-
*/
|
|
6639
|
-
function stripControlCharacters(text) {
|
|
6640
|
-
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
6641
|
-
}
|
|
6642
|
-
//#endregion
|
|
6643
7542
|
//#region src/types/feature-processor.ts
|
|
6644
7543
|
var FeatureProcessor = class {
|
|
6645
7544
|
outputRoot;
|
|
@@ -11566,6 +12465,26 @@ function toolSkillSearchRoots(paths) {
|
|
|
11566
12465
|
* https://agentskills.io/client-implementation/adding-skills-support
|
|
11567
12466
|
*/
|
|
11568
12467
|
const AGENT_SKILLS_INTEROP_ROOTS = /* @__PURE__ */ new Set([toPosixPath(AGENTSMD_SKILLS_DIR_PATH), toPosixPath(AMP_SKILLS_GLOBAL_DIR)]);
|
|
12468
|
+
/**
|
|
12469
|
+
* The one spec violation both sides of the conversion report. Generation warns
|
|
12470
|
+
* about it before writing the file; import warns about it because a conformant
|
|
12471
|
+
* client would skip the skill entirely, and a user who never sees that has no
|
|
12472
|
+
* reason to fix it.
|
|
12473
|
+
*/
|
|
12474
|
+
const EMPTY_SKILL_DESCRIPTION_VIOLATION = "`description` is required and must not be empty; conformant clients skip a skill without one";
|
|
12475
|
+
/**
|
|
12476
|
+
* Report a skill read from disk whose `description` is empty. This lives on the
|
|
12477
|
+
* read rather than on any one tool class because several targets share one
|
|
12478
|
+
* `.agents/skills` tree: which of them reports the skill must not depend on
|
|
12479
|
+
* which target the user happened to enable. Every loader calls it — the two
|
|
12480
|
+
* directory loaders (`loadSkillDirContent`, `SimulatedSkill.fromDirDefault`)
|
|
12481
|
+
* and the flat-file one — so the form a tool stores its skills in does not
|
|
12482
|
+
* decide whether the problem is reported either.
|
|
12483
|
+
*/
|
|
12484
|
+
function warnOnEmptyLoadedDescription({ skillFilePath, description }) {
|
|
12485
|
+
if (typeof description !== "string" || description.length > 0) return;
|
|
12486
|
+
warnOnceWithFallback(void 0, `${stripControlCharacters(toPosixPath(skillFilePath))}: ${EMPTY_SKILL_DESCRIPTION_VIOLATION}. Rulesync imports it anyway so the content is not lost; fill it in before relying on this skill.`);
|
|
12487
|
+
}
|
|
11569
12488
|
function isAgentSkillsInteropRoot(relativeDirPath) {
|
|
11570
12489
|
return AGENT_SKILLS_INTEROP_ROOTS.has(toPosixPath(relativeDirPath));
|
|
11571
12490
|
}
|
|
@@ -11678,7 +12597,11 @@ var ToolSkill = class extends AiDir {
|
|
|
11678
12597
|
const skillDirPath = join(outputRoot, actualRelativeDirPath, dirName);
|
|
11679
12598
|
const skillFilePath = join(skillDirPath, SKILL_FILE_NAME);
|
|
11680
12599
|
if (!await fileExists(skillFilePath)) throw new Error(`${SKILL_FILE_NAME} not found in ${skillDirPath}`);
|
|
11681
|
-
const { frontmatter, body: content } =
|
|
12600
|
+
const { frontmatter, body: content } = parseFrontmatterWithYamlRepair(await readFileContent(skillFilePath), skillFilePath);
|
|
12601
|
+
warnOnEmptyLoadedDescription({
|
|
12602
|
+
skillFilePath,
|
|
12603
|
+
description: frontmatter.description
|
|
12604
|
+
});
|
|
11682
12605
|
const otherFiles = await this.collectOtherFiles(outputRoot, actualRelativeDirPath, dirName, SKILL_FILE_NAME);
|
|
11683
12606
|
return {
|
|
11684
12607
|
outputRoot,
|
|
@@ -11803,6 +12726,10 @@ function toSpecConformantAgentSkillFields(section, { coerceMetadata = true } = {
|
|
|
11803
12726
|
...allowedTools !== void 0 && allowedTools.length > 0 && { "allowed-tools": allowedTools }
|
|
11804
12727
|
};
|
|
11805
12728
|
}
|
|
12729
|
+
/** The `SKILL.md` a diagnostic should point at, in the scope it is written to. */
|
|
12730
|
+
function agentSkillFilePath({ outputRoot, relativeDirPath, dirName }) {
|
|
12731
|
+
return join(outputRoot, relativeDirPath, dirName, SKILL_FILE_NAME);
|
|
12732
|
+
}
|
|
11806
12733
|
/**
|
|
11807
12734
|
* Collect the normative violations the Agent Skills spec defines for a skill
|
|
11808
12735
|
* about to be written. These are reported as warnings rather than errors:
|
|
@@ -11829,7 +12756,7 @@ function collectAgentSkillViolations({ frontmatter, dirName, sourceAllowedTools
|
|
|
11829
12756
|
if (!NAME_PATTERN.test(name)) violations.push(`\`name\` "${name}" must contain only lowercase letters, digits and single hyphens, with no leading, trailing or consecutive hyphens`);
|
|
11830
12757
|
if (name !== dirName) violations.push(`\`name\` "${name}" must match its parent directory name "${dirName}"; conformant clients require them to be equal`);
|
|
11831
12758
|
}
|
|
11832
|
-
if (description.length === 0) violations.push(
|
|
12759
|
+
if (description.length === 0) violations.push(EMPTY_SKILL_DESCRIPTION_VIOLATION);
|
|
11833
12760
|
else if (description.length > DESCRIPTION_MAX_LENGTH) violations.push(`\`description\` is ${description.length} characters; the Agent Skills spec allows at most ${DESCRIPTION_MAX_LENGTH}`);
|
|
11834
12761
|
const { compatibility } = frontmatter;
|
|
11835
12762
|
if (typeof compatibility !== "string" && compatibility !== void 0) violations.push("`compatibility` must be a string; the Agent Skills spec does not allow a mapping here");
|
|
@@ -11948,12 +12875,16 @@ var AgentsSkillsSkill = class AgentsSkillsSkill extends ToolSkill {
|
|
|
11948
12875
|
* directory rather than a same-named project path.
|
|
11949
12876
|
*/
|
|
11950
12877
|
static reportSpecViolations({ outputRoot, relativeDirPath, dirName, frontmatter, sourceAllowedTools, logger }) {
|
|
11951
|
-
const skillPath =
|
|
12878
|
+
const skillPath = agentSkillFilePath({
|
|
12879
|
+
outputRoot,
|
|
12880
|
+
relativeDirPath,
|
|
12881
|
+
dirName
|
|
12882
|
+
});
|
|
11952
12883
|
for (const violation of collectAgentSkillViolations({
|
|
11953
12884
|
frontmatter,
|
|
11954
12885
|
dirName,
|
|
11955
12886
|
sourceAllowedTools
|
|
11956
|
-
})) warnWithFallback(logger, `${skillPath}: ${violation}`);
|
|
12887
|
+
})) warnWithFallback(logger, `${stripControlCharacters(toPosixPath(skillPath))}: ${violation}`);
|
|
11957
12888
|
}
|
|
11958
12889
|
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
11959
12890
|
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
@@ -38550,9 +39481,13 @@ var SimulatedSkill = class extends ToolSkill {
|
|
|
38550
39481
|
const skillDirPath = join(outputRoot, actualRelativeDirPath, dirName);
|
|
38551
39482
|
const skillFilePath = join(skillDirPath, SKILL_FILE_NAME);
|
|
38552
39483
|
if (!await fileExists(skillFilePath)) throw new Error(`${SKILL_FILE_NAME} not found in ${skillDirPath}`);
|
|
38553
|
-
const { frontmatter, body: content } =
|
|
39484
|
+
const { frontmatter, body: content } = parseFrontmatterWithYamlRepair(await readFileContent(skillFilePath), skillFilePath);
|
|
38554
39485
|
const result = SimulatedSkillFrontmatterSchema.safeParse(frontmatter);
|
|
38555
39486
|
if (!result.success) throw new Error(`Invalid frontmatter in ${skillFilePath}: ${formatError(result.error)}`);
|
|
39487
|
+
warnOnEmptyLoadedDescription({
|
|
39488
|
+
skillFilePath,
|
|
39489
|
+
description: result.data.description
|
|
39490
|
+
});
|
|
38556
39491
|
const otherFiles = await this.collectOtherFiles(outputRoot, actualRelativeDirPath, dirName, SKILL_FILE_NAME);
|
|
38557
39492
|
return {
|
|
38558
39493
|
outputRoot,
|
|
@@ -42080,7 +43015,7 @@ var KimiCodeSkill = class KimiCodeSkill extends ToolSkill {
|
|
|
42080
43015
|
}
|
|
42081
43016
|
static async fromFlatFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
42082
43017
|
const filePath = join(outputRoot, relativeDirPath, relativeFilePath);
|
|
42083
|
-
const { frontmatter, body } =
|
|
43018
|
+
const { frontmatter, body } = parseFrontmatterWithYamlRepair(await readFileContent(filePath), filePath);
|
|
42084
43019
|
const result = KimiCodeFlatSkillFrontmatterSchema.safeParse(frontmatter);
|
|
42085
43020
|
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
42086
43021
|
const fileName = basename(relativeFilePath, extname(relativeFilePath));
|
|
@@ -42090,6 +43025,10 @@ var KimiCodeSkill = class KimiCodeSkill extends ToolSkill {
|
|
|
42090
43025
|
name: result.data.name ?? fileName,
|
|
42091
43026
|
description: result.data.description ?? firstBodyLine?.slice(0, 240) ?? "No description provided."
|
|
42092
43027
|
};
|
|
43028
|
+
warnOnEmptyLoadedDescription({
|
|
43029
|
+
skillFilePath: filePath,
|
|
43030
|
+
description: normalizedFrontmatter.description
|
|
43031
|
+
});
|
|
42093
43032
|
return new KimiCodeSkill({
|
|
42094
43033
|
outputRoot,
|
|
42095
43034
|
relativeDirPath,
|
|
@@ -43093,7 +44032,7 @@ var ReasonixSkill = class ReasonixSkill extends ToolSkill {
|
|
|
43093
44032
|
static async isDirOwned({ outputRoot, relativeDirPath, dirName }) {
|
|
43094
44033
|
const skillFilePath = join(outputRoot, relativeDirPath, dirName, SKILL_FILE_NAME);
|
|
43095
44034
|
try {
|
|
43096
|
-
const { frontmatter } =
|
|
44035
|
+
const { frontmatter } = parseFrontmatterWithYamlRepair(await readFileContent(skillFilePath), skillFilePath, { quiet: true });
|
|
43097
44036
|
return frontmatter["runAs"] !== REASONIX_SUBAGENT_RUN_AS;
|
|
43098
44037
|
} catch {
|
|
43099
44038
|
return true;
|
|
@@ -43462,8 +44401,7 @@ var TaktSkill = class TaktSkill extends ToolSkill {
|
|
|
43462
44401
|
const fullPath = join(this.outputRoot, this.relativeDirPath);
|
|
43463
44402
|
const resolvedFull = resolve(fullPath);
|
|
43464
44403
|
const resolvedBase = resolve(this.outputRoot);
|
|
43465
|
-
|
|
43466
|
-
if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}"`);
|
|
44404
|
+
if (pathEscapesRoot(relative(resolvedBase, resolvedFull))) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}"`);
|
|
43467
44405
|
return fullPath;
|
|
43468
44406
|
}
|
|
43469
44407
|
getRelativePathFromCwd() {
|
|
@@ -48430,7 +49368,7 @@ var ReasonixSubagent = class ReasonixSubagent extends ToolSubagent {
|
|
|
48430
49368
|
const paths = this.getSettablePaths({ global });
|
|
48431
49369
|
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
48432
49370
|
const fileContent = await readFileContent(filePath);
|
|
48433
|
-
const { frontmatter, body: content } =
|
|
49371
|
+
const { frontmatter, body: content } = parseFrontmatterWithYamlRepair(fileContent, filePath);
|
|
48434
49372
|
const result = ReasonixSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
48435
49373
|
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
48436
49374
|
return new ReasonixSubagent({
|
|
@@ -48459,7 +49397,7 @@ var ReasonixSubagent = class ReasonixSubagent extends ToolSubagent {
|
|
|
48459
49397
|
static async isFileOwned({ outputRoot, relativeDirPath, relativeFilePath }) {
|
|
48460
49398
|
const filePath = join(outputRoot, relativeDirPath, relativeFilePath);
|
|
48461
49399
|
try {
|
|
48462
|
-
const { frontmatter } =
|
|
49400
|
+
const { frontmatter } = parseFrontmatterWithYamlRepair(await readFileContent(filePath), filePath, { quiet: true });
|
|
48463
49401
|
return frontmatter["runAs"] === REASONIX_SUBAGENT_RUN_AS;
|
|
48464
49402
|
} catch {
|
|
48465
49403
|
return false;
|
|
@@ -55853,6 +56791,7 @@ function resolveToolOutputRoot({ outputRoot, toolTarget, global }) {
|
|
|
55853
56791
|
* `.rulesync/` files to disk. Rulesync file instances live in memory only.
|
|
55854
56792
|
*/
|
|
55855
56793
|
async function convertFromTool(params) {
|
|
56794
|
+
resetWarnedOnceMessages();
|
|
55856
56795
|
const packagingTarget = [params.fromTool, ...params.toTools].find(isPackagingToolTarget);
|
|
55857
56796
|
if (packagingTarget) throw new Error(`Plugin packaging target '${packagingTarget}' is not supported by convert. Use import and generate with explicit plugin directories.`);
|
|
55858
56797
|
const ctx = params;
|
|
@@ -56782,6 +57721,7 @@ async function collectHermesProjectPluginNames({ config, resultsById }) {
|
|
|
56782
57721
|
async function generate(params) {
|
|
56783
57722
|
const { config, logger } = params;
|
|
56784
57723
|
resetRootShadowingWarnings({ logger });
|
|
57724
|
+
resetWarnedOnceMessages();
|
|
56785
57725
|
for (const toolTarget of config.getTargets()) for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
|
|
56786
57726
|
toolTarget,
|
|
56787
57727
|
outputRoot
|
|
@@ -57341,6 +58281,7 @@ function getToolOutputRoot({ config, tool }) {
|
|
|
57341
58281
|
*/
|
|
57342
58282
|
async function importFromTool(params) {
|
|
57343
58283
|
const { config, tool, logger } = params;
|
|
58284
|
+
resetWarnedOnceMessages();
|
|
57344
58285
|
await assertPluginRootSafe({
|
|
57345
58286
|
toolTarget: tool,
|
|
57346
58287
|
outputRoot: getToolOutputRoot({
|
|
@@ -57662,6 +58603,6 @@ async function importChecksCore(params) {
|
|
|
57662
58603
|
return writtenCount;
|
|
57663
58604
|
}
|
|
57664
58605
|
//#endregion
|
|
57665
|
-
export { SourceEntrySchema as $,
|
|
58606
|
+
export { SourceEntrySchema as $, RULESYNC_MCP_LEGACY_FILE_NAME as $t, RulesyncRuleFrontmatterSchema as A, runWithDirectoryRollback as At, RulesyncCheck as B, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Bt, CODEXCLI_DIR as C, readFileContentOrNull as Ct, RulesyncSkill as D, removeFileStrict as Dt, RulesyncSubagentFrontmatterSchema as E, removeFile as Et, getRulesyncSourceCandidates as F, ALL_TOOL_TARGETS_WITH_WILDCARD as Ft, SHARED_USER_MANAGED_CONFIG_PATHS as G, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Gt, stringifyFrontmatter as H, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Ht, resolveRulesyncSourceWritePath as I, PACKAGING_TOOL_TARGETS as It, mergeInputRootConfigs as J, RULESYNC_HOOKS_LEGACY_FILE_NAME as Jt, SKILL_FILE_NAME as K, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Kt, parseJsonc as L, ToolTargetSchema as Lt, RulesyncMcp as M, writeFileBuffer as Mt, RulesyncIgnore as N, writeFileContent as Nt, RulesyncSkillFrontmatterSchema as O, removeTempDirectory as Ot, RulesyncHooks as P, ALL_TOOL_TARGETS as Pt, GITIGNORE_DESTINATION_KEY as Q, RULESYNC_MCP_FILE_NAME as Qt, RulesyncCommand as R, MAX_FILE_SIZE as Rt, CODEXCLI_BASH_RULES_FILE_NAME as S, readFileContent as St, RulesyncSubagent as T, removeDirectoryStrict as Tt, loadYaml as U, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Ut, RulesyncCheckFrontmatterSchema as V, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Vt, stripControlCharacters as W, RULESYNC_CONFIG_SCHEMA_URL as Wt, CONFLICTING_TARGET_PAIRS as X, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Xt, resolveEffectiveInputRoots as Y, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Yt, ConfigFileSchema as Z, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Zt, CLAUDECODE_LOCAL_RULE_FILE_NAME as _, getFileSize as _t, convertFromTool as a, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as an, resetWarnedOnceMessages as at, CLAUDECODE_SKILLS_DIR_PATH as b, listDirectoryFiles as bt, SubagentsProcessor as c, RULESYNC_RULES_RELATIVE_DIR_PATH as cn, assertDirectoryIfExists as ct, IgnoreProcessor as d, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as dn, checkPathTraversal as dt, RULESYNC_MCP_RELATIVE_FILE_PATH as en, findControlCharacter as et, HooksProcessor as f, ALL_FEATURES as fn, createTempDirectory as ft, CLAUDECODE_DIR as g, findFilesByGlobs as gt, QWENCODE_LOCAL_RULE_FILE_NAME as h, formatError as hn, fileExists as ht, getProcessorRegistryEntry as i, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as in, warnOnConflictingFlags as it, RulesyncPermissions as j, toPosixPath as jt, RulesyncRule as k, resolvePath as kt, SkillsProcessor as l, RULESYNC_SKILLS_RELATIVE_DIR_PATH as ln, assertTreeContainsNoSymlinks as lt, QWENCODE_DIR as m, DEPRECATED_FEATURE_REPLACEMENTS as mn, ensureDir as mt, generate as n, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as nn, JsonLogger as nt, isPackagingToolTarget as o, RULESYNC_PERMISSIONS_SCHEMA_URL as on, CLIError as ot, CommandsProcessor as p, ALL_FEATURES_WITH_WILDCARD as pn, directoryExists as pt, ConfigResolver as q, RULESYNC_HOOKS_FILE_NAME as qt, inspectInputRoots as r, RULESYNC_PERMISSIONS_FILE_NAME as rn, fallbackLogger as rt, RulesProcessor as s, RULESYNC_RELATIVE_DIR_PATH as sn, ErrorCodes as st, importFromTool as t, RULESYNC_MCP_SCHEMA_URL as tn, ConsoleLogger as tt, McpProcessor as u, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as un, assertWritablePathInsideRoot as ut, CLAUDECODE_MEMORIES_DIR_NAME as v, getHomeDirectory as vt, getLocalSkillDirNames as w, removeDirectory as wt, ChecksProcessor as x, pathEscapesRoot as xt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as y, isSymlink as yt, RulesyncCommandFrontmatterSchema as z, RULESYNC_AIIGNORE_FILE_NAME as zt };
|
|
57666
58607
|
|
|
57667
|
-
//# sourceMappingURL=import-
|
|
58608
|
+
//# sourceMappingURL=import-u8yswsGB.js.map
|