@vibe-agent-toolkit/resources 0.1.36 → 0.1.38
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 +2 -2
- package/dist/ajv-factory.d.ts +33 -0
- package/dist/ajv-factory.d.ts.map +1 -0
- package/dist/ajv-factory.js +51 -0
- package/dist/ajv-factory.js.map +1 -0
- package/dist/config-parser.d.ts +0 -18
- package/dist/config-parser.d.ts.map +1 -1
- package/dist/config-parser.js +5 -45
- package/dist/config-parser.js.map +1 -1
- package/dist/frontmatter-editor.d.ts +45 -0
- package/dist/frontmatter-editor.d.ts.map +1 -0
- package/dist/frontmatter-editor.js +161 -0
- package/dist/frontmatter-editor.js.map +1 -0
- package/dist/frontmatter-validator.d.ts.map +1 -1
- package/dist/frontmatter-validator.js +11 -6
- package/dist/frontmatter-validator.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/json-pointer-path.d.ts +13 -0
- package/dist/json-pointer-path.d.ts.map +1 -0
- package/dist/json-pointer-path.js +30 -0
- package/dist/json-pointer-path.js.map +1 -0
- package/dist/link-parser.js +2 -2
- package/dist/link-parser.js.map +1 -1
- package/dist/link-validator.d.ts +22 -0
- package/dist/link-validator.d.ts.map +1 -1
- package/dist/link-validator.js +126 -75
- package/dist/link-validator.js.map +1 -1
- package/dist/rewriter-helpers.d.ts +49 -0
- package/dist/rewriter-helpers.d.ts.map +1 -0
- package/dist/rewriter-helpers.js +142 -0
- package/dist/rewriter-helpers.js.map +1 -0
- package/dist/types/resource-parser.js +2 -2
- package/dist/types/resource-parser.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +40 -11
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +39 -13
- package/dist/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/ajv-factory.ts +56 -0
- package/src/config-parser.ts +5 -50
- package/src/frontmatter-editor.ts +214 -0
- package/src/frontmatter-validator.ts +11 -7
- package/src/index.ts +21 -2
- package/src/json-pointer-path.ts +29 -0
- package/src/link-parser.ts +2 -2
- package/src/link-validator.ts +153 -88
- package/src/rewriter-helpers.ts +166 -0
- package/src/types/resource-parser.ts +2 -2
- package/src/types.ts +0 -1
- package/src/utils.ts +57 -14
package/dist/types.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type { BaseResource, Heading, JsonResource, JsonSchemaResource, MarkdownR
|
|
|
20
20
|
export { getResourceAbsolutePath, isValidProjectPath, normalizeProjectPath, } from './types/resource-path-utils.js';
|
|
21
21
|
export { detectResourceType, parseJsonResource, parseJsonSchemaResource, parseMarkdownResource, parseYamlResource, } from './types/resource-parser.js';
|
|
22
22
|
export type { ValidationMode, CollectionValidation, CollectionConfig, ResourcesConfig, ProjectConfig, SkillFileEntry, SkillPackagingConfig, SkillsConfig, ClaudeConfig, ClaudeMarketplaceConfig, ClaudeMarketplacePluginEntry, } from './schemas/project-config.js';
|
|
23
|
-
export {
|
|
23
|
+
export { parseConfigFile, loadConfig, } from './config-parser.js';
|
|
24
24
|
export { isGlobPattern, expandPattern, expandPatterns, } from './pattern-expander.js';
|
|
25
25
|
export { matchesCollection, getCollectionsForFile, } from './collection-matcher.js';
|
|
26
26
|
export type { ValidateLinkOptions } from './link-validator.js';
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,YAAY,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGrD,YAAY,EACV,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,gBAAgB,GACjB,MAAM,gCAAgC,CAAC;AAGxC,YAAY,EACV,eAAe,EACf,gBAAgB,GACjB,MAAM,gCAAgC,CAAC;AAGxC,YAAY,EACV,YAAY,EACZ,uBAAuB,EACvB,eAAe,EACf,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,YAAY,EACZ,YAAY,GACb,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,YAAY,EACZ,OAAO,EACP,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,QAAQ,EACR,eAAe,EACf,YAAY,GACb,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AAGpC,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,YAAY,EACZ,YAAY,EACZ,uBAAuB,EACvB,4BAA4B,GAC7B,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,YAAY,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGrD,YAAY,EACV,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,gBAAgB,GACjB,MAAM,gCAAgC,CAAC;AAGxC,YAAY,EACV,eAAe,EACf,gBAAgB,GACjB,MAAM,gCAAgC,CAAC;AAGxC,YAAY,EACV,YAAY,EACZ,uBAAuB,EACvB,eAAe,EACf,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,YAAY,EACZ,YAAY,GACb,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,YAAY,EACZ,OAAO,EACP,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,QAAQ,EACR,eAAe,EACf,YAAY,GACb,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AAGpC,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,YAAY,EACZ,YAAY,EACZ,uBAAuB,EACvB,4BAA4B,GAC7B,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EACL,eAAe,EACf,UAAU,GACX,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,yBAAyB,CAAC;AAGjC,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGhD,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,aAAa,GACd,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,8BAA8B,GAC/B,MAAM,6BAA6B,CAAC;AAGrC,YAAY,EACV,sBAAsB,EACtB,+BAA+B,GAChC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC"}
|
package/dist/types.js
CHANGED
|
@@ -18,7 +18,7 @@ export { getResourceAbsolutePath, isValidProjectPath, normalizeProjectPath, } fr
|
|
|
18
18
|
// Resource parsing
|
|
19
19
|
export { detectResourceType, parseJsonResource, parseJsonSchemaResource, parseMarkdownResource, parseYamlResource, } from './types/resource-parser.js';
|
|
20
20
|
// Config parsing
|
|
21
|
-
export {
|
|
21
|
+
export { parseConfigFile, loadConfig, } from './config-parser.js';
|
|
22
22
|
// Pattern expansion
|
|
23
23
|
export { isGlobPattern, expandPattern, expandPatterns, } from './pattern-expander.js';
|
|
24
24
|
// Collection matching
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AA2BrD,uBAAuB;AACvB,OAAO,EACL,YAAY,EACZ,YAAY,GACb,MAAM,sBAAsB,CAAC;AAY9B,0BAA0B;AAC1B,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC;AAExC,mBAAmB;AACnB,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AAiBpC,iBAAiB;AACjB,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AA2BrD,uBAAuB;AACvB,OAAO,EACL,YAAY,EACZ,YAAY,GACb,MAAM,sBAAsB,CAAC;AAY9B,0BAA0B;AAC1B,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC;AAExC,mBAAmB;AACnB,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AAiBpC,iBAAiB;AACjB,OAAO,EACL,eAAe,EACf,UAAU,GACX,MAAM,oBAAoB,CAAC;AAE5B,oBAAoB;AACpB,OAAO,EACL,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,uBAAuB,CAAC;AAE/B,sBAAsB;AACtB,OAAO,EACL,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,oBAAoB;AACpB,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,aAAa,GACd,MAAM,wBAAwB,CAAC;AAEhC,0BAA0B;AAC1B,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,8BAA8B,GAC/B,MAAM,6BAA6B,CAAC;AAOrC,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC"}
|
package/dist/utils.d.ts
CHANGED
|
@@ -35,32 +35,60 @@ export declare function matchesGlobPattern(filePath: string, pattern: string): b
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function splitHrefAnchor(href: string): [string, string | undefined];
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* Discriminated union returned by {@link resolveLocalHref}.
|
|
39
|
+
*
|
|
40
|
+
* - `anchor_only` — the href was `#fragment` only (no file component).
|
|
41
|
+
* - `resolved` — the href resolved to an absolute filesystem path.
|
|
42
|
+
* - `absolute_no_root` — the href is an RFC 3986 §4.2 absolute-path
|
|
43
|
+
* reference (starts with `/`) but no `projectRoot` was supplied.
|
|
44
|
+
* - `absolute_escapes_root` — the absolute-path reference resolved to a
|
|
45
|
+
* location outside `projectRoot` (e.g., via `..` traversal or a symlink
|
|
46
|
+
* pointing outside the project).
|
|
47
|
+
*/
|
|
48
|
+
export type ResolveLocalHrefResult = {
|
|
49
|
+
kind: 'anchor_only';
|
|
50
|
+
} | {
|
|
51
|
+
kind: 'resolved';
|
|
52
|
+
resolvedPath: string;
|
|
53
|
+
anchor: string | undefined;
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'absolute_no_root';
|
|
56
|
+
href: string;
|
|
57
|
+
anchor: string | undefined;
|
|
58
|
+
} | {
|
|
59
|
+
kind: 'absolute_escapes_root';
|
|
60
|
+
href: string;
|
|
61
|
+
anchor: string | undefined;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a markdown link href to a filesystem path or a typed failure.
|
|
39
65
|
*
|
|
40
66
|
* Performs the standard href → path conversion used by both audit and validate:
|
|
41
67
|
* 1. Strips anchor fragment (`#section`)
|
|
42
68
|
* 2. Decodes URL-encoded characters (`%20` → space, `%26` → `&`)
|
|
43
|
-
* 3. Resolves the path
|
|
44
|
-
*
|
|
45
|
-
*
|
|
69
|
+
* 3. Resolves the path:
|
|
70
|
+
* - Leading `/` (RFC 3986 §4.2 absolute-path reference) → resolve against
|
|
71
|
+
* `projectRoot`. Requires a `projectRoot`; must not escape it.
|
|
72
|
+
* - Otherwise → resolve relative to the source file's directory.
|
|
46
73
|
*
|
|
47
74
|
* @param href - Raw href from a markdown link
|
|
48
75
|
* @param sourceFilePath - Absolute path of the file containing the link
|
|
49
|
-
* @
|
|
76
|
+
* @param projectRoot - Optional project root for absolute-path references.
|
|
77
|
+
* @returns A {@link ResolveLocalHrefResult} discriminating success vs failure modes.
|
|
50
78
|
*
|
|
51
79
|
* @example
|
|
52
80
|
* ```typescript
|
|
53
81
|
* resolveLocalHref('My%20Folder/doc.md#intro', '/project/README.md')
|
|
54
|
-
* // { resolvedPath: '/project/My Folder/doc.md', anchor: 'intro' }
|
|
82
|
+
* // { kind: 'resolved', resolvedPath: '/project/My Folder/doc.md', anchor: 'intro' }
|
|
55
83
|
*
|
|
56
84
|
* resolveLocalHref('#heading', '/project/README.md')
|
|
57
|
-
* //
|
|
85
|
+
* // { kind: 'anchor_only' }
|
|
86
|
+
*
|
|
87
|
+
* resolveLocalHref('/docs/foo.md', '/project/docs/sub/page.md', '/project')
|
|
88
|
+
* // { kind: 'resolved', resolvedPath: '/project/docs/foo.md', anchor: undefined }
|
|
58
89
|
* ```
|
|
59
90
|
*/
|
|
60
|
-
export declare function resolveLocalHref(href: string, sourceFilePath: string):
|
|
61
|
-
resolvedPath: string;
|
|
62
|
-
anchor: string | undefined;
|
|
63
|
-
} | null;
|
|
91
|
+
export declare function resolveLocalHref(href: string, sourceFilePath: string, projectRoot?: string): ResolveLocalHrefResult;
|
|
64
92
|
/**
|
|
65
93
|
* Check if a file path is within a project directory.
|
|
66
94
|
*
|
|
@@ -100,4 +128,5 @@ export declare function decodeJsonPointerSegment(segment: string): string;
|
|
|
100
128
|
* formatJsonPointerAsDotted('') // ''
|
|
101
129
|
*/
|
|
102
130
|
export declare function formatJsonPointerAsDotted(pointer: string): string;
|
|
131
|
+
export declare function isCanonicalArrayIndex(s: string): boolean;
|
|
103
132
|
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAoB7E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAS1E;AAED
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAoB7E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAS1E;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,sBAAsB,GAC9B;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,MAAM,GACnB,sBAAsB,CA6BxB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CA6B9E;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAcjE;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAUxD"}
|
package/dist/utils.js
CHANGED
|
@@ -64,32 +64,37 @@ export function splitHrefAnchor(href) {
|
|
|
64
64
|
return [filePath, anchor];
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
|
-
* Resolve a markdown link href to
|
|
67
|
+
* Resolve a markdown link href to a filesystem path or a typed failure.
|
|
68
68
|
*
|
|
69
69
|
* Performs the standard href → path conversion used by both audit and validate:
|
|
70
70
|
* 1. Strips anchor fragment (`#section`)
|
|
71
71
|
* 2. Decodes URL-encoded characters (`%20` → space, `%26` → `&`)
|
|
72
|
-
* 3. Resolves the path
|
|
73
|
-
*
|
|
74
|
-
*
|
|
72
|
+
* 3. Resolves the path:
|
|
73
|
+
* - Leading `/` (RFC 3986 §4.2 absolute-path reference) → resolve against
|
|
74
|
+
* `projectRoot`. Requires a `projectRoot`; must not escape it.
|
|
75
|
+
* - Otherwise → resolve relative to the source file's directory.
|
|
75
76
|
*
|
|
76
77
|
* @param href - Raw href from a markdown link
|
|
77
78
|
* @param sourceFilePath - Absolute path of the file containing the link
|
|
78
|
-
* @
|
|
79
|
+
* @param projectRoot - Optional project root for absolute-path references.
|
|
80
|
+
* @returns A {@link ResolveLocalHrefResult} discriminating success vs failure modes.
|
|
79
81
|
*
|
|
80
82
|
* @example
|
|
81
83
|
* ```typescript
|
|
82
84
|
* resolveLocalHref('My%20Folder/doc.md#intro', '/project/README.md')
|
|
83
|
-
* // { resolvedPath: '/project/My Folder/doc.md', anchor: 'intro' }
|
|
85
|
+
* // { kind: 'resolved', resolvedPath: '/project/My Folder/doc.md', anchor: 'intro' }
|
|
84
86
|
*
|
|
85
87
|
* resolveLocalHref('#heading', '/project/README.md')
|
|
86
|
-
* //
|
|
88
|
+
* // { kind: 'anchor_only' }
|
|
89
|
+
*
|
|
90
|
+
* resolveLocalHref('/docs/foo.md', '/project/docs/sub/page.md', '/project')
|
|
91
|
+
* // { kind: 'resolved', resolvedPath: '/project/docs/foo.md', anchor: undefined }
|
|
87
92
|
* ```
|
|
88
93
|
*/
|
|
89
|
-
export function resolveLocalHref(href, sourceFilePath) {
|
|
94
|
+
export function resolveLocalHref(href, sourceFilePath, projectRoot) {
|
|
90
95
|
const [fileHref, anchor] = splitHrefAnchor(href);
|
|
91
96
|
if (fileHref === '') {
|
|
92
|
-
return
|
|
97
|
+
return { kind: 'anchor_only' };
|
|
93
98
|
}
|
|
94
99
|
let decodedHref;
|
|
95
100
|
try {
|
|
@@ -98,9 +103,21 @@ export function resolveLocalHref(href, sourceFilePath) {
|
|
|
98
103
|
catch {
|
|
99
104
|
decodedHref = fileHref;
|
|
100
105
|
}
|
|
106
|
+
// RFC 3986 §4.2 absolute-path reference — resolve against projectRoot.
|
|
107
|
+
if (decodedHref.startsWith('/')) {
|
|
108
|
+
if (!projectRoot) {
|
|
109
|
+
return { kind: 'absolute_no_root', href: fileHref, anchor };
|
|
110
|
+
}
|
|
111
|
+
const candidate = safePath.resolve(projectRoot, decodedHref.slice(1));
|
|
112
|
+
if (!isWithinProject(candidate, projectRoot)) {
|
|
113
|
+
return { kind: 'absolute_escapes_root', href: fileHref, anchor };
|
|
114
|
+
}
|
|
115
|
+
return { kind: 'resolved', resolvedPath: candidate, anchor };
|
|
116
|
+
}
|
|
117
|
+
// Relative reference — resolve against the source file's directory.
|
|
101
118
|
const sourceDir = path.dirname(sourceFilePath);
|
|
102
119
|
const resolvedPath = safePath.resolve(sourceDir, decodedHref);
|
|
103
|
-
return { resolvedPath, anchor };
|
|
120
|
+
return { kind: 'resolved', resolvedPath, anchor };
|
|
104
121
|
}
|
|
105
122
|
/**
|
|
106
123
|
* Check if a file path is within a project directory.
|
|
@@ -120,7 +137,9 @@ export function resolveLocalHref(href, sourceFilePath) {
|
|
|
120
137
|
* ```
|
|
121
138
|
*/
|
|
122
139
|
export function isWithinProject(filePath, projectRoot) {
|
|
123
|
-
//
|
|
140
|
+
// Canonicalize both sides symmetrically. Asymmetric handling (realpath one
|
|
141
|
+
// side, resolve the other) false-flags legitimate matches when projectRoot
|
|
142
|
+
// traverses a symlink — e.g. macOS /tmp → /private/tmp, bind mounts.
|
|
124
143
|
let resolvedFilePath;
|
|
125
144
|
try {
|
|
126
145
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- filePath is validated path parameter
|
|
@@ -130,7 +149,14 @@ export function isWithinProject(filePath, projectRoot) {
|
|
|
130
149
|
// If realpath fails, file doesn't exist - use original path
|
|
131
150
|
resolvedFilePath = safePath.resolve(filePath);
|
|
132
151
|
}
|
|
133
|
-
|
|
152
|
+
let resolvedProjectRoot;
|
|
153
|
+
try {
|
|
154
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- projectRoot is validated path parameter
|
|
155
|
+
resolvedProjectRoot = fs.realpathSync(projectRoot);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
resolvedProjectRoot = safePath.resolve(projectRoot);
|
|
159
|
+
}
|
|
134
160
|
// Normalize to forward slashes for cross-platform comparison
|
|
135
161
|
const normalizedFile = toForwardSlash(resolvedFilePath);
|
|
136
162
|
const normalizedRoot = toForwardSlash(resolvedProjectRoot);
|
|
@@ -179,7 +205,7 @@ export function formatJsonPointerAsDotted(pointer) {
|
|
|
179
205
|
}
|
|
180
206
|
return out;
|
|
181
207
|
}
|
|
182
|
-
function isCanonicalArrayIndex(s) {
|
|
208
|
+
export function isCanonicalArrayIndex(s) {
|
|
183
209
|
// Canonical integer per RFC 6901 §4 + JSON canonical form: no leading zeros
|
|
184
210
|
// except for "0" itself.
|
|
185
211
|
if (s === '')
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,OAAe;IAClE,MAAM,eAAe,GAAG,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IAE1C,8DAA8D;IAC9D,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,yEAAyE;IACzE,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrC,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACvD,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjD,IAAI,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;IAC3C,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC5B,CAAC;
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,SAAS,MAAM,WAAW,CAAC;AAElC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,OAAe;IAClE,MAAM,eAAe,GAAG,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IAE1C,8DAA8D;IAC9D,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,yEAAyE;IACzE,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrC,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACvD,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjD,IAAI,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;IAC3C,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC5B,CAAC;AAmBD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,cAAsB,EACtB,WAAoB;IAEpB,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IACjD,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;QACpB,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;IACjC,CAAC;IAED,IAAI,WAAmB,CAAC;IACxB,IAAI,CAAC;QACH,WAAW,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,WAAW,GAAG,QAAQ,CAAC;IACzB,CAAC;IAED,uEAAuE;IACvE,IAAI,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAChC,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,EAAE,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAC9D,CAAC;QACD,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACtE,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,WAAW,CAAC,EAAE,CAAC;YAC7C,OAAO,EAAE,IAAI,EAAE,uBAAuB,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnE,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAC/D,CAAC;IAED,oEAAoE;IACpE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAC/C,MAAM,YAAY,GAAG,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IAC9D,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC;AACpD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,WAAmB;IACnE,2EAA2E;IAC3E,2EAA2E;IAC3E,qEAAqE;IACrE,IAAI,gBAAwB,CAAC;IAC7B,IAAI,CAAC;QACH,2GAA2G;QAC3G,gBAAgB,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,4DAA4D;QAC5D,gBAAgB,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,mBAA2B,CAAC;IAChC,IAAI,CAAC;QACH,8GAA8G;QAC9G,mBAAmB,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,mBAAmB,GAAG,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACtD,CAAC;IAED,6DAA6D;IAC7D,MAAM,cAAc,GAAG,cAAc,CAAC,gBAAgB,CAAC,CAAC;IACxD,MAAM,cAAc,GAAG,cAAc,CAAC,mBAAmB,CAAC,CAAC;IAE3D,8CAA8C;IAC9C,sDAAsD;IACtD,wCAAwC;IACxC,OAAO,cAAc,CAAC,UAAU,CAAC,cAAc,GAAG,GAAG,CAAC,IAAI,cAAc,KAAK,cAAc,CAAC;AAC9F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAY;IACnD,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AAC1D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAe;IACtD,OAAO,OAAO,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAe;IACvD,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAC9B,6GAA6G;IAC7G,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;IAE3E,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,IAAI,qBAAqB,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/B,GAAG,IAAI,IAAI,GAAG,GAAG,CAAC;QACpB,CAAC;aAAM,CAAC;YACN,GAAG,IAAI,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;QACtC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,CAAS;IAC7C,4EAA4E;IAC5E,yBAAyB;IACzB,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAC3B,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAC3B,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,KAAK,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;QACnB,IAAI,EAAE,GAAG,GAAG,IAAI,EAAE,GAAG,GAAG;YAAE,OAAO,KAAK,CAAC;IACzC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-agent-toolkit/resources",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"description": "Markdown resource parsing, validation, and link integrity checking",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"author": "Jeff Dutton",
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@vibe-agent-toolkit/agent-schema": "0.1.
|
|
37
|
-
"@vibe-agent-toolkit/utils": "0.1.
|
|
36
|
+
"@vibe-agent-toolkit/agent-schema": "0.1.38",
|
|
37
|
+
"@vibe-agent-toolkit/utils": "0.1.38",
|
|
38
38
|
"ajv": "^8.17.1",
|
|
39
|
+
"ajv-formats": "^3.0.1",
|
|
39
40
|
"github-slugger": "^2.0.0",
|
|
40
|
-
"js-yaml": "^4.1.1",
|
|
41
41
|
"markdown-link-check": "^3.14.2",
|
|
42
42
|
"picomatch": "^4.0.3",
|
|
43
43
|
"remark-frontmatter": "^5.0.0",
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
"semver": "^7.7.3",
|
|
47
47
|
"unified": "^11.0.5",
|
|
48
48
|
"unist-util-visit": "^5.0.0",
|
|
49
|
+
"yaml": "^2.6.1",
|
|
49
50
|
"zod": "^3.24.1"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
|
-
"@types/js-yaml": "^4.0.9",
|
|
53
53
|
"@types/mdast": "^4.0.4",
|
|
54
54
|
"@types/node": "^22.10.5",
|
|
55
55
|
"@types/picomatch": "^4.0.2",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ajv factory for adopters consuming VAT-generated schemas.
|
|
3
|
+
*
|
|
4
|
+
* VAT's frontmatter walker treats `format: "uri-reference"` (plus `uri`,
|
|
5
|
+
* `iri`, `iri-reference`) as first-class URI families and validates the
|
|
6
|
+
* referenced files via {@link import('./utils.js').resolveLocalHref}, not via
|
|
7
|
+
* Ajv. But adopters consuming the same schemas with vanilla
|
|
8
|
+
* `new Ajv(...)` hit Ajv's default strict mode, which upgrades
|
|
9
|
+
* `unknown format "uri-reference" ignored` from a warning to a thrown error.
|
|
10
|
+
*
|
|
11
|
+
* This helper returns an Ajv instance with the standard JSON Schema format
|
|
12
|
+
* vocabulary registered (via `ajv-formats`) plus no-op shims for
|
|
13
|
+
* `iri` / `iri-reference` (which `ajv-formats` does not ship). All
|
|
14
|
+
* URI-family schemas compile cleanly under strict mode.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Ajv, type Options as AjvOptions } from 'ajv';
|
|
18
|
+
// ajv-formats is a CJS module published with `module.exports = formatsPlugin`
|
|
19
|
+
// plus an `exports.default` alias. Under NodeNext module resolution the
|
|
20
|
+
// default import is typed as the namespace object (not callable), even
|
|
21
|
+
// though the runtime value IS the plugin function. The `.default ??
|
|
22
|
+
// namespace` pattern below resolves both at type level and runtime.
|
|
23
|
+
import * as ajvFormatsModule from 'ajv-formats';
|
|
24
|
+
|
|
25
|
+
type AddFormatsFn = (ajv: Ajv) => Ajv;
|
|
26
|
+
|
|
27
|
+
const addFormats: AddFormatsFn =
|
|
28
|
+
(ajvFormatsModule as unknown as { default?: AddFormatsFn }).default ??
|
|
29
|
+
(ajvFormatsModule as unknown as AddFormatsFn);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Construct an Ajv instance pre-registered with the URI-family formats VAT
|
|
33
|
+
* schemas use. Use this anywhere downstream code compiles a schema that may
|
|
34
|
+
* reference `format: "uri-reference"` (or `uri`, `iri`, `iri-reference`).
|
|
35
|
+
*
|
|
36
|
+
* @param options - Ajv options. Passed through unchanged — caller controls
|
|
37
|
+
* `allErrors`, `strict`, `allowUnionTypes`, `verbose`, etc.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* import { createAjvWithUriFormats } from '@vibe-agent-toolkit/resources';
|
|
41
|
+
*
|
|
42
|
+
* const ajv = createAjvWithUriFormats({ allErrors: true });
|
|
43
|
+
* const validate = ajv.compile(mySchemaWithUriReference);
|
|
44
|
+
* if (!validate(data)) console.error(validate.errors);
|
|
45
|
+
*/
|
|
46
|
+
export function createAjvWithUriFormats(options: AjvOptions = {}): Ajv {
|
|
47
|
+
const ajv = new Ajv(options);
|
|
48
|
+
addFormats(ajv);
|
|
49
|
+
// ajv-formats does not register `iri` / `iri-reference`. Adopters whose
|
|
50
|
+
// schemas declare those would still hit "unknown format" under strict
|
|
51
|
+
// mode. Register no-op validators — Ajv accepts the format token, and
|
|
52
|
+
// semantic validation happens through resolveLocalHref / equivalent.
|
|
53
|
+
ajv.addFormat('iri', true);
|
|
54
|
+
ajv.addFormat('iri-reference', true);
|
|
55
|
+
return ajv;
|
|
56
|
+
}
|
package/src/config-parser.ts
CHANGED
|
@@ -5,58 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { readFile } from 'node:fs/promises';
|
|
8
|
-
import path from 'node:path';
|
|
9
8
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { findConfigFile } from '@vibe-agent-toolkit/utils';
|
|
10
|
+
import { parse as parseYaml } from 'yaml';
|
|
12
11
|
|
|
13
12
|
import { ProjectConfigSchema, type ProjectConfig } from './schemas/project-config.js';
|
|
14
13
|
|
|
15
|
-
const CONFIG_FILENAME = 'vibe-agent-toolkit.config.yaml';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Find the config file by walking up the directory tree.
|
|
19
|
-
*
|
|
20
|
-
* Starts from the current directory and walks up until the config file is found
|
|
21
|
-
* or the root directory is reached.
|
|
22
|
-
*
|
|
23
|
-
* @param startDir - Directory to start searching from (default: process.cwd())
|
|
24
|
-
* @returns Absolute path to config file, or undefined if not found
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```typescript
|
|
28
|
-
* const configPath = await findConfigFile();
|
|
29
|
-
* if (configPath) {
|
|
30
|
-
* console.log(`Found config: ${configPath}`);
|
|
31
|
-
* }
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
export async function findConfigFile(startDir: string = process.cwd()): Promise<string | undefined> {
|
|
35
|
-
let currentDir = safePath.resolve(startDir);
|
|
36
|
-
const { root } = path.parse(currentDir);
|
|
37
|
-
|
|
38
|
-
while (true) {
|
|
39
|
-
const configPath = safePath.join(currentDir, CONFIG_FILENAME);
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
// Check if file exists by attempting to read metadata
|
|
43
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- constructing path during tree walk
|
|
44
|
-
await readFile(configPath, 'utf-8');
|
|
45
|
-
return configPath;
|
|
46
|
-
} catch {
|
|
47
|
-
// File doesn't exist, continue walking up
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Check if we've reached the root
|
|
51
|
-
if (currentDir === root) {
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Move up one directory
|
|
56
|
-
currentDir = path.dirname(currentDir);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
14
|
/**
|
|
61
15
|
* Parse a project configuration file.
|
|
62
16
|
*
|
|
@@ -81,7 +35,7 @@ export async function parseConfigFile(configPath: string): Promise<ProjectConfig
|
|
|
81
35
|
// Parse YAML
|
|
82
36
|
let parsed: unknown;
|
|
83
37
|
try {
|
|
84
|
-
parsed =
|
|
38
|
+
parsed = parseYaml(content);
|
|
85
39
|
} catch (error) {
|
|
86
40
|
throw new Error(`Invalid YAML in config file: ${error instanceof Error ? error.message : String(error)}`);
|
|
87
41
|
}
|
|
@@ -117,7 +71,8 @@ export async function parseConfigFile(configPath: string): Promise<ProjectConfig
|
|
|
117
71
|
* ```
|
|
118
72
|
*/
|
|
119
73
|
export async function loadConfig(startDir: string = process.cwd()): Promise<ProjectConfig | undefined> {
|
|
120
|
-
|
|
74
|
+
// findConfigFile from utils is synchronous; awaiting a non-promise is a no-op.
|
|
75
|
+
const configPath = findConfigFile(startDir);
|
|
121
76
|
if (!configPath) {
|
|
122
77
|
return undefined;
|
|
123
78
|
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FrontmatterEditor — round-trip-safe primitive for editing YAML frontmatter
|
|
3
|
+
* in markdown files.
|
|
4
|
+
*
|
|
5
|
+
* Public surface: openFrontmatter(markdown) → FrontmatterEditor.
|
|
6
|
+
*
|
|
7
|
+
* Round-trip identity contract: openFrontmatter(x).toString() === x for any
|
|
8
|
+
* well-formed input, byte-for-byte. Mutations preserve comments, blank lines,
|
|
9
|
+
* key ordering, quoting style, and detected EOL.
|
|
10
|
+
*
|
|
11
|
+
* See docs/superpowers/specs/2026-05-17-frontmatter-editor-and-yaml-consolidation-design.md
|
|
12
|
+
* §5 for the full contract.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Document, parseDocument } from 'yaml';
|
|
16
|
+
|
|
17
|
+
export class FrontmatterParseError extends Error {
|
|
18
|
+
public override readonly cause: unknown;
|
|
19
|
+
|
|
20
|
+
constructor(message: string, cause: unknown) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'FrontmatterParseError';
|
|
23
|
+
this.cause = cause;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Path to a value in the parsed frontmatter document. */
|
|
28
|
+
export type FrontmatterPath = string | readonly (string | number)[];
|
|
29
|
+
|
|
30
|
+
/** Scalar value type accepted by mutation methods. */
|
|
31
|
+
export type FrontmatterScalar = string | number | boolean | null;
|
|
32
|
+
|
|
33
|
+
export interface FrontmatterEditor {
|
|
34
|
+
body: string;
|
|
35
|
+
get(path: FrontmatterPath): unknown;
|
|
36
|
+
set(path: FrontmatterPath, value: FrontmatterScalar): void;
|
|
37
|
+
setArrayItem(path: FrontmatterPath, index: number, value: FrontmatterScalar): void;
|
|
38
|
+
appendArrayItem(path: FrontmatterPath, value: FrontmatterScalar): void;
|
|
39
|
+
delete(path: FrontmatterPath): void;
|
|
40
|
+
toString(): string;
|
|
41
|
+
/**
|
|
42
|
+
* Returns true if any mutating method has been called or `body` was
|
|
43
|
+
* reassigned to a different string. Use to gate writeFileSync and avoid
|
|
44
|
+
* the no-op-rewrite churn described in §"What's preserved, what isn't"
|
|
45
|
+
* of the markdown-rewriting skill.
|
|
46
|
+
*
|
|
47
|
+
* **Caveat:** any call to `set` / `setArrayItem` / `appendArrayItem` /
|
|
48
|
+
* `delete` flips the flag, even if the underlying value didn't change
|
|
49
|
+
* (e.g. `set('foo', sameValue)`). For strict byte-level dirty detection
|
|
50
|
+
* compare `editor.toString() !== originalText` instead. `body =` is the
|
|
51
|
+
* one exception — it only flips dirty on actual string change.
|
|
52
|
+
*/
|
|
53
|
+
isDirty(): boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface FrontmatterSplit {
|
|
57
|
+
hasFrontmatter: boolean;
|
|
58
|
+
frontmatterText: string;
|
|
59
|
+
body: string;
|
|
60
|
+
eol: '\n' | '\r\n';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const OPENING_FENCE = /^---\r?\n/;
|
|
64
|
+
// Closing fence: either immediately after opening (empty frontmatter), or
|
|
65
|
+
// preceded by a newline. Trailing variants accept newline or EOF.
|
|
66
|
+
const EMPTY_CLOSING_FENCE = /^---(?:\r?\n|$)/;
|
|
67
|
+
const CLOSING_FENCE = /(?:\r?\n---\r?\n|\r?\n---$)/;
|
|
68
|
+
|
|
69
|
+
function detectEol(input: string): '\n' | '\r\n' {
|
|
70
|
+
const firstBreak = input.indexOf('\n');
|
|
71
|
+
if (firstBreak === -1) return '\n';
|
|
72
|
+
return firstBreak > 0 && input.charAt(firstBreak - 1) === '\r' ? '\r\n' : '\n';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function splitFrontmatter(input: string): FrontmatterSplit {
|
|
76
|
+
const eol = detectEol(input);
|
|
77
|
+
const openingMatch = OPENING_FENCE.exec(input);
|
|
78
|
+
if (!openingMatch) {
|
|
79
|
+
return { hasFrontmatter: false, frontmatterText: '', body: input, eol };
|
|
80
|
+
}
|
|
81
|
+
const afterOpening = input.slice(openingMatch[0].length);
|
|
82
|
+
// Handle empty frontmatter (closing fence immediately follows opening fence)
|
|
83
|
+
const emptyMatch = EMPTY_CLOSING_FENCE.exec(afterOpening);
|
|
84
|
+
if (emptyMatch) {
|
|
85
|
+
const bodyStart = emptyMatch[0].length;
|
|
86
|
+
return {
|
|
87
|
+
hasFrontmatter: true,
|
|
88
|
+
frontmatterText: '',
|
|
89
|
+
body: afterOpening.slice(bodyStart),
|
|
90
|
+
eol,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const closingMatch = CLOSING_FENCE.exec(afterOpening);
|
|
94
|
+
if (!closingMatch) {
|
|
95
|
+
return { hasFrontmatter: false, frontmatterText: '', body: input, eol };
|
|
96
|
+
}
|
|
97
|
+
const frontmatterText = afterOpening.slice(0, closingMatch.index);
|
|
98
|
+
const bodyStart = closingMatch.index + closingMatch[0].length;
|
|
99
|
+
const body = afterOpening.slice(bodyStart);
|
|
100
|
+
return { hasFrontmatter: true, frontmatterText, body, eol };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class FrontmatterEditorImpl implements FrontmatterEditor {
|
|
104
|
+
private readonly doc: Document.Parsed | Document;
|
|
105
|
+
private readonly hasFrontmatter: boolean;
|
|
106
|
+
private readonly eol: '\n' | '\r\n';
|
|
107
|
+
private _body: string;
|
|
108
|
+
private _dirty = false;
|
|
109
|
+
|
|
110
|
+
get body(): string {
|
|
111
|
+
return this._body;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
set body(value: string) {
|
|
115
|
+
if (value !== this._body) {
|
|
116
|
+
this._body = value;
|
|
117
|
+
this._dirty = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
isDirty(): boolean {
|
|
122
|
+
return this._dirty;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
constructor(input: string) {
|
|
126
|
+
const split = splitFrontmatter(input);
|
|
127
|
+
this.hasFrontmatter = split.hasFrontmatter;
|
|
128
|
+
this.eol = split.eol;
|
|
129
|
+
this._body = split.body;
|
|
130
|
+
if (!split.hasFrontmatter) {
|
|
131
|
+
this.doc = new Document({});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
this.doc = parseDocument(split.frontmatterText, { prettyErrors: true });
|
|
136
|
+
if (this.doc.errors.length > 0) {
|
|
137
|
+
throw new FrontmatterParseError(
|
|
138
|
+
`Invalid YAML frontmatter: ${this.doc.errors[0]?.message ?? 'unknown error'}`,
|
|
139
|
+
this.doc.errors[0],
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (error instanceof FrontmatterParseError) throw error;
|
|
144
|
+
throw new FrontmatterParseError(
|
|
145
|
+
`Failed to parse frontmatter: ${error instanceof Error ? error.message : String(error)}`,
|
|
146
|
+
error,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private toPath(path: FrontmatterPath): readonly (string | number)[] {
|
|
152
|
+
if (Array.isArray(path)) return path;
|
|
153
|
+
if (typeof path === 'string') return [path];
|
|
154
|
+
return path as readonly (string | number)[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
get(path: FrontmatterPath): unknown {
|
|
158
|
+
const segments = this.toPath(path);
|
|
159
|
+
if (segments.length === 0) return this.doc.toJS();
|
|
160
|
+
return this.doc.getIn(segments as Iterable<unknown>, false);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
set(path: FrontmatterPath, value: FrontmatterScalar): void {
|
|
164
|
+
const segments = this.toPath(path);
|
|
165
|
+
this.doc.setIn(segments as Iterable<unknown>, value);
|
|
166
|
+
this._dirty = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
setArrayItem(path: FrontmatterPath, index: number, value: FrontmatterScalar): void {
|
|
170
|
+
const segments = [...this.toPath(path), index];
|
|
171
|
+
this.doc.setIn(segments as Iterable<unknown>, value);
|
|
172
|
+
this._dirty = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
appendArrayItem(path: FrontmatterPath, value: FrontmatterScalar): void {
|
|
176
|
+
const segments = this.toPath(path);
|
|
177
|
+
this.doc.addIn(segments as Iterable<unknown>, value);
|
|
178
|
+
this._dirty = true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
delete(path: FrontmatterPath): void {
|
|
182
|
+
const segments = this.toPath(path);
|
|
183
|
+
this.doc.deleteIn(segments as Iterable<unknown>);
|
|
184
|
+
this._dirty = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
toString(): string {
|
|
188
|
+
// No frontmatter originally, and nothing was added → return body unchanged.
|
|
189
|
+
if (!this.hasFrontmatter && this.isDocEffectivelyEmpty()) {
|
|
190
|
+
return this.body;
|
|
191
|
+
}
|
|
192
|
+
// Empty frontmatter (e.g. `---\n---\n`) where the doc remained empty —
|
|
193
|
+
// preserve the empty fence block without injecting `null` or `{}` between.
|
|
194
|
+
if (this.hasFrontmatter && this.isDocEffectivelyEmpty()) {
|
|
195
|
+
return `---${this.eol}---${this.eol}${this.body}`;
|
|
196
|
+
}
|
|
197
|
+
const fmText = this.doc.toString();
|
|
198
|
+
const normalized = this.eol === '\r\n' ? fmText.replaceAll('\n', '\r\n') : fmText;
|
|
199
|
+
return `---${this.eol}${normalized}---${this.eol}${this.body}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private isDocEffectivelyEmpty(): boolean {
|
|
203
|
+
const contents = this.doc.contents;
|
|
204
|
+
if (contents === null) return true;
|
|
205
|
+
// yaml.YAMLMap and YAMLSeq expose `items`; an empty map/seq counts as empty.
|
|
206
|
+
const maybeItems = (contents as { items?: unknown[] }).items;
|
|
207
|
+
if (Array.isArray(maybeItems) && maybeItems.length === 0) return true;
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function openFrontmatter(markdown: string): FrontmatterEditor {
|
|
213
|
+
return new FrontmatterEditorImpl(markdown);
|
|
214
|
+
}
|