@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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +346 -0
  3. package/dist/bin.d.ts +8 -0
  4. package/dist/bin.js +76 -0
  5. package/dist/commands/add.d.ts +6 -0
  6. package/dist/commands/add.js +82 -0
  7. package/dist/commands/init.d.ts +6 -0
  8. package/dist/commands/init.js +52 -0
  9. package/dist/commands/list.d.ts +6 -0
  10. package/dist/commands/list.js +95 -0
  11. package/dist/commands/reanchor.d.ts +6 -0
  12. package/dist/commands/reanchor.js +127 -0
  13. package/dist/commands/rename.d.ts +6 -0
  14. package/dist/commands/rename.js +53 -0
  15. package/dist/commands/resolve.d.ts +6 -0
  16. package/dist/commands/resolve.js +35 -0
  17. package/dist/commands/status.d.ts +6 -0
  18. package/dist/commands/status.js +156 -0
  19. package/dist/commands/validate.d.ts +6 -0
  20. package/dist/commands/validate.js +55 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.js +25 -0
  23. package/dist/lib/comments.d.ts +52 -0
  24. package/dist/lib/comments.js +228 -0
  25. package/dist/lib/discovery.d.ts +40 -0
  26. package/dist/lib/discovery.js +125 -0
  27. package/dist/lib/fuzzy.d.ts +39 -0
  28. package/dist/lib/fuzzy.js +277 -0
  29. package/dist/lib/git.d.ts +68 -0
  30. package/dist/lib/git.js +246 -0
  31. package/dist/lib/parser.d.ts +19 -0
  32. package/dist/lib/parser.js +56 -0
  33. package/dist/lib/reanchor.d.ts +45 -0
  34. package/dist/lib/reanchor.js +389 -0
  35. package/dist/lib/resolve-files.d.ts +18 -0
  36. package/dist/lib/resolve-files.js +42 -0
  37. package/dist/lib/types.d.ts +151 -0
  38. package/dist/lib/types.js +5 -0
  39. package/dist/lib/validator.d.ts +13 -0
  40. package/dist/lib/validator.js +177 -0
  41. package/dist/lib/writer.d.ts +30 -0
  42. package/dist/lib/writer.js +62 -0
  43. 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
+ }