@mrsf/cli 0.1.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/LICENSE +21 -0
- package/README.md +346 -0
- package/dist/bin.d.ts +8 -0
- package/dist/bin.js +76 -0
- package/dist/commands/add.d.ts +6 -0
- package/dist/commands/add.js +82 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +52 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.js +95 -0
- package/dist/commands/reanchor.d.ts +6 -0
- package/dist/commands/reanchor.js +127 -0
- package/dist/commands/rename.d.ts +6 -0
- package/dist/commands/rename.js +53 -0
- package/dist/commands/resolve.d.ts +6 -0
- package/dist/commands/resolve.js +35 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +156 -0
- package/dist/commands/validate.d.ts +6 -0
- package/dist/commands/validate.js +55 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +25 -0
- package/dist/lib/comments.d.ts +52 -0
- package/dist/lib/comments.js +228 -0
- package/dist/lib/discovery.d.ts +40 -0
- package/dist/lib/discovery.js +125 -0
- package/dist/lib/fuzzy.d.ts +39 -0
- package/dist/lib/fuzzy.js +277 -0
- package/dist/lib/git.d.ts +68 -0
- package/dist/lib/git.js +246 -0
- package/dist/lib/parser.d.ts +19 -0
- package/dist/lib/parser.js +56 -0
- package/dist/lib/reanchor.d.ts +45 -0
- package/dist/lib/reanchor.js +389 -0
- package/dist/lib/resolve-files.d.ts +18 -0
- package/dist/lib/resolve-files.js +42 -0
- package/dist/lib/types.d.ts +151 -0
- package/dist/lib/types.js +5 -0
- package/dist/lib/validator.d.ts +13 -0
- package/dist/lib/validator.js +177 -0
- package/dist/lib/writer.d.ts +30 -0
- package/dist/lib/writer.js +62 -0
- package/package.json +70 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRSF Validator — JSON Schema + cross-field validation per §10.
|
|
3
|
+
*/
|
|
4
|
+
import AjvModule from "ajv";
|
|
5
|
+
import addFormatsModule from "ajv-formats";
|
|
6
|
+
// ESM interop: handle both default and namespace imports
|
|
7
|
+
const Ajv = AjvModule.default ?? AjvModule;
|
|
8
|
+
const addFormats = addFormatsModule.default ?? addFormatsModule;
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { computeHash } from "./writer.js";
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
let _schemaCache = null;
|
|
15
|
+
async function loadSchema() {
|
|
16
|
+
if (_schemaCache)
|
|
17
|
+
return _schemaCache;
|
|
18
|
+
// The schema lives at the repo root, two levels up from dist/lib/
|
|
19
|
+
const candidates = [
|
|
20
|
+
path.resolve(__dirname, "../../../mrsf.schema.json"), // from dist/lib/
|
|
21
|
+
path.resolve(__dirname, "../../../../mrsf.schema.json"), // fallback
|
|
22
|
+
path.resolve(process.cwd(), "mrsf.schema.json"), // cwd fallback
|
|
23
|
+
];
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
try {
|
|
26
|
+
const raw = await readFile(candidate, "utf-8");
|
|
27
|
+
_schemaCache = JSON.parse(raw);
|
|
28
|
+
return _schemaCache;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// try next
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error("Could not locate mrsf.schema.json");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate an MRSF document (parsed object).
|
|
38
|
+
*/
|
|
39
|
+
export async function validate(doc, options = {}) {
|
|
40
|
+
const errors = [];
|
|
41
|
+
const warnings = [];
|
|
42
|
+
// ── JSON Schema validation ──
|
|
43
|
+
const rawSchema = await loadSchema();
|
|
44
|
+
// Ajv 8 doesn't natively support 2020-12; strip $schema to use draft-07 mode
|
|
45
|
+
const { $schema, ...schema } = rawSchema;
|
|
46
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
47
|
+
addFormats(ajv);
|
|
48
|
+
const ajvValidate = ajv.compile(schema);
|
|
49
|
+
const schemaValid = ajvValidate(doc);
|
|
50
|
+
if (!schemaValid && ajvValidate.errors) {
|
|
51
|
+
for (const err of ajvValidate.errors) {
|
|
52
|
+
errors.push({
|
|
53
|
+
severity: "error",
|
|
54
|
+
message: `${err.instancePath || "/"}: ${err.message ?? "schema error"}`,
|
|
55
|
+
path: err.instancePath || "/",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ── Cross-field validation (§10) ──
|
|
60
|
+
if (Array.isArray(doc.comments)) {
|
|
61
|
+
const ids = new Set();
|
|
62
|
+
for (let i = 0; i < doc.comments.length; i++) {
|
|
63
|
+
const c = doc.comments[i];
|
|
64
|
+
const prefix = `/comments/${i}`;
|
|
65
|
+
// Unique id check
|
|
66
|
+
if (c.id) {
|
|
67
|
+
if (ids.has(c.id)) {
|
|
68
|
+
errors.push({
|
|
69
|
+
severity: "error",
|
|
70
|
+
message: `Duplicate comment id "${c.id}"`,
|
|
71
|
+
path: `${prefix}/id`,
|
|
72
|
+
commentId: c.id,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
ids.add(c.id);
|
|
76
|
+
}
|
|
77
|
+
// end_line ≥ line
|
|
78
|
+
if (c.line != null && c.end_line != null && c.end_line < c.line) {
|
|
79
|
+
errors.push({
|
|
80
|
+
severity: "error",
|
|
81
|
+
message: `end_line (${c.end_line}) must be ≥ line (${c.line})`,
|
|
82
|
+
path: `${prefix}/end_line`,
|
|
83
|
+
commentId: c.id,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// end_column ≥ start_column when same line
|
|
87
|
+
if (c.start_column != null &&
|
|
88
|
+
c.end_column != null &&
|
|
89
|
+
(c.line == null || c.end_line == null || c.line === c.end_line) &&
|
|
90
|
+
c.end_column < c.start_column) {
|
|
91
|
+
errors.push({
|
|
92
|
+
severity: "error",
|
|
93
|
+
message: `end_column (${c.end_column}) must be ≥ start_column (${c.start_column}) on the same line`,
|
|
94
|
+
path: `${prefix}/end_column`,
|
|
95
|
+
commentId: c.id,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// selected_text length
|
|
99
|
+
if (c.selected_text && c.selected_text.length > 4096) {
|
|
100
|
+
errors.push({
|
|
101
|
+
severity: "error",
|
|
102
|
+
message: `selected_text exceeds 4096 characters (${c.selected_text.length})`,
|
|
103
|
+
path: `${prefix}/selected_text`,
|
|
104
|
+
commentId: c.id,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// text length
|
|
108
|
+
if (c.text && c.text.length > 16384) {
|
|
109
|
+
warnings.push({
|
|
110
|
+
severity: "warning",
|
|
111
|
+
message: `text exceeds recommended 16384 characters (${c.text.length})`,
|
|
112
|
+
path: `${prefix}/text`,
|
|
113
|
+
commentId: c.id,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// selected_text_hash consistency
|
|
117
|
+
if (c.selected_text && c.selected_text_hash) {
|
|
118
|
+
const expected = computeHash(c.selected_text);
|
|
119
|
+
if (c.selected_text_hash !== expected) {
|
|
120
|
+
warnings.push({
|
|
121
|
+
severity: "warning",
|
|
122
|
+
message: `selected_text_hash mismatch (expected ${expected.slice(0, 12)}…, got ${c.selected_text_hash.slice(0, 12)}…)`,
|
|
123
|
+
path: `${prefix}/selected_text_hash`,
|
|
124
|
+
commentId: c.id,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// reply_to resolution
|
|
129
|
+
if (c.reply_to && !ids.has(c.reply_to)) {
|
|
130
|
+
// Check forward references too
|
|
131
|
+
const allIds = doc.comments.map((x) => x.id);
|
|
132
|
+
if (!allIds.includes(c.reply_to)) {
|
|
133
|
+
warnings.push({
|
|
134
|
+
severity: "warning",
|
|
135
|
+
message: `reply_to "${c.reply_to}" does not resolve to any comment id in this file`,
|
|
136
|
+
path: `${prefix}/reply_to`,
|
|
137
|
+
commentId: c.id,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Missing selected_text warning
|
|
142
|
+
if (c.line != null && !c.selected_text) {
|
|
143
|
+
warnings.push({
|
|
144
|
+
severity: "warning",
|
|
145
|
+
message: `Comment has line anchors but no selected_text — anchoring will be fragile across edits`,
|
|
146
|
+
path: `${prefix}/selected_text`,
|
|
147
|
+
commentId: c.id,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const valid = errors.length === 0 && (!options.strict || warnings.length === 0);
|
|
153
|
+
return { valid, errors, warnings };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Validate from a file path — convenience wrapper.
|
|
157
|
+
*/
|
|
158
|
+
export async function validateFile(filePath, options = {}) {
|
|
159
|
+
const { parseSidecar } = await import("./parser.js");
|
|
160
|
+
try {
|
|
161
|
+
const doc = await parseSidecar(filePath);
|
|
162
|
+
return validate(doc, options);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
return {
|
|
166
|
+
valid: false,
|
|
167
|
+
errors: [
|
|
168
|
+
{
|
|
169
|
+
severity: "error",
|
|
170
|
+
message: `Failed to parse: ${e.message}`,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
warnings: [],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=validator.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRSF Writer — serialize MrsfDocument back to YAML or JSON.
|
|
3
|
+
*
|
|
4
|
+
* Preserves comment order per §10.1.
|
|
5
|
+
* Auto-computes selected_text_hash when selected_text changes.
|
|
6
|
+
*/
|
|
7
|
+
import type { MrsfDocument, Comment } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Compute SHA-256 hex hash of a string (UTF-8).
|
|
10
|
+
*/
|
|
11
|
+
export declare function computeHash(text: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Ensure selected_text_hash is consistent for a comment.
|
|
14
|
+
* If selected_text is present, computes/updates the hash.
|
|
15
|
+
* If selected_text is absent, removes the hash.
|
|
16
|
+
*/
|
|
17
|
+
export declare function syncHash(comment: Comment): Comment;
|
|
18
|
+
/**
|
|
19
|
+
* Serialize an MrsfDocument to YAML.
|
|
20
|
+
*/
|
|
21
|
+
export declare function toYaml(doc: MrsfDocument): string;
|
|
22
|
+
/**
|
|
23
|
+
* Serialize an MrsfDocument to JSON.
|
|
24
|
+
*/
|
|
25
|
+
export declare function toJson(doc: MrsfDocument): string;
|
|
26
|
+
/**
|
|
27
|
+
* Write an MrsfDocument to disk. Detects format from file extension.
|
|
28
|
+
*/
|
|
29
|
+
export declare function writeSidecar(filePath: string, doc: MrsfDocument): Promise<void>;
|
|
30
|
+
//# sourceMappingURL=writer.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRSF Writer — serialize MrsfDocument back to YAML or JSON.
|
|
3
|
+
*
|
|
4
|
+
* Preserves comment order per §10.1.
|
|
5
|
+
* Auto-computes selected_text_hash when selected_text changes.
|
|
6
|
+
*/
|
|
7
|
+
import { writeFile } from "node:fs/promises";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import yaml from "js-yaml";
|
|
11
|
+
/**
|
|
12
|
+
* Compute SHA-256 hex hash of a string (UTF-8).
|
|
13
|
+
*/
|
|
14
|
+
export function computeHash(text) {
|
|
15
|
+
return createHash("sha256").update(text, "utf-8").digest("hex");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Ensure selected_text_hash is consistent for a comment.
|
|
19
|
+
* If selected_text is present, computes/updates the hash.
|
|
20
|
+
* If selected_text is absent, removes the hash.
|
|
21
|
+
*/
|
|
22
|
+
export function syncHash(comment) {
|
|
23
|
+
if (comment.selected_text != null && comment.selected_text.length > 0) {
|
|
24
|
+
comment.selected_text_hash = computeHash(comment.selected_text);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
delete comment.selected_text_hash;
|
|
28
|
+
}
|
|
29
|
+
return comment;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Serialize an MrsfDocument to YAML.
|
|
33
|
+
*/
|
|
34
|
+
export function toYaml(doc) {
|
|
35
|
+
return yaml.dump(doc, {
|
|
36
|
+
lineWidth: -1, // no wrapping
|
|
37
|
+
quotingType: '"',
|
|
38
|
+
forceQuotes: false,
|
|
39
|
+
noRefs: true,
|
|
40
|
+
sortKeys: false, // preserve insertion order
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Serialize an MrsfDocument to JSON.
|
|
45
|
+
*/
|
|
46
|
+
export function toJson(doc) {
|
|
47
|
+
return JSON.stringify(doc, null, 2) + "\n";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Write an MrsfDocument to disk. Detects format from file extension.
|
|
51
|
+
*/
|
|
52
|
+
export async function writeSidecar(filePath, doc) {
|
|
53
|
+
const abs = path.resolve(filePath);
|
|
54
|
+
const isJson = abs.endsWith(".review.json");
|
|
55
|
+
// Sync hashes for all comments before writing
|
|
56
|
+
for (const comment of doc.comments) {
|
|
57
|
+
syncHash(comment);
|
|
58
|
+
}
|
|
59
|
+
const content = isJson ? toJson(doc) : toYaml(doc);
|
|
60
|
+
await writeFile(abs, content, "utf-8");
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=writer.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mrsf/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI and library for the Markdown Review Sidecar Format (MRSF)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"mrsf": "./dist/bin.js"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"dev": "tsc --watch",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"lint": "tsc --noEmit",
|
|
26
|
+
"prepublishOnly": "cp ../LICENSE . && npm run build && npm test",
|
|
27
|
+
"postpublish": "rm -f LICENSE"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"markdown",
|
|
31
|
+
"review",
|
|
32
|
+
"sidecar",
|
|
33
|
+
"mrsf",
|
|
34
|
+
"anchoring",
|
|
35
|
+
"comments"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"author": "Wictor Wilén (https://github.com/wictorwilen)",
|
|
39
|
+
"homepage": "https://github.com/wictorwilen/MRSF/tree/main/cli#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/wictorwilen/MRSF/issues"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/wictorwilen/MRSF.git",
|
|
46
|
+
"directory": "cli"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist/**/*.js",
|
|
50
|
+
"dist/**/*.d.ts",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE"
|
|
53
|
+
],
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"ajv": "^8.17.1",
|
|
56
|
+
"ajv-formats": "^3.0.1",
|
|
57
|
+
"chalk": "^5.4.1",
|
|
58
|
+
"commander": "^13.1.0",
|
|
59
|
+
"fastest-levenshtein": "^1.0.16",
|
|
60
|
+
"js-yaml": "^4.1.0",
|
|
61
|
+
"uuid": "^11.1.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/js-yaml": "^4.0.9",
|
|
65
|
+
"@types/node": "^22.13.10",
|
|
66
|
+
"@types/uuid": "^10.0.0",
|
|
67
|
+
"typescript": "^5.8.2",
|
|
68
|
+
"vitest": "^3.0.9"
|
|
69
|
+
}
|
|
70
|
+
}
|