sysprom 1.22.1 → 1.23.1
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.
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, } from "node:fs";
|
|
3
|
+
import { resolve, extname } from "node:path";
|
|
4
4
|
import { syncDocumentsOp, } from "../../operations/index.js";
|
|
5
5
|
import { detectChanges } from "../../sync.js";
|
|
6
6
|
import { markdownToJson } from "../../md-to-json.js";
|
|
7
|
-
import { jsonToMarkdownSingle } from "../../json-to-md.js";
|
|
7
|
+
import { jsonToMarkdownSingle, jsonToMarkdownMultiDoc, } from "../../json-to-md.js";
|
|
8
8
|
import { canonicalise } from "../../canonical-json.js";
|
|
9
9
|
import { SysProMDocument } from "../../schema.js";
|
|
10
10
|
/**
|
|
@@ -22,8 +22,10 @@ export function syncCommand(input) {
|
|
|
22
22
|
if (!SysProMDocument.is(jsonDoc)) {
|
|
23
23
|
throw new Error("JSON file is not a valid SysProM document");
|
|
24
24
|
}
|
|
25
|
-
// Parse Markdown to document
|
|
26
|
-
const mdDoc =
|
|
25
|
+
// Parse Markdown to document, or create empty doc if it doesn't exist yet
|
|
26
|
+
const mdDoc = existsSync(mdPath)
|
|
27
|
+
? markdownToJson(mdPath)
|
|
28
|
+
: { nodes: [], relationships: [] };
|
|
27
29
|
// Detect which side changed
|
|
28
30
|
const changes = detectChanges(jsonPath, mdPath);
|
|
29
31
|
// Perform sync operation
|
|
@@ -34,6 +36,8 @@ export function syncCommand(input) {
|
|
|
34
36
|
mdChanged: changes.mdChanged,
|
|
35
37
|
strategy,
|
|
36
38
|
});
|
|
39
|
+
// Track if markdown file existed before sync
|
|
40
|
+
const mdExistedBefore = existsSync(mdPath);
|
|
37
41
|
// Write results if not dry-run
|
|
38
42
|
if (!dryRun) {
|
|
39
43
|
// Write synced document back to both formats
|
|
@@ -41,10 +45,26 @@ export function syncCommand(input) {
|
|
|
41
45
|
// Update JSON
|
|
42
46
|
writeFileSync(jsonPath, canonicalise(result.synced, { indent: "\t" }) + "\n");
|
|
43
47
|
// Update Markdown
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
// If output path is an existing directory or doesn't look like a .md file,
|
|
49
|
+
// write multi-doc output into the directory. Otherwise write single-file MD.
|
|
50
|
+
if (mdExistedBefore && statSync(mdPath).isDirectory()) {
|
|
51
|
+
jsonToMarkdownMultiDoc(result.synced, mdPath);
|
|
52
|
+
}
|
|
53
|
+
else if (extname(mdPath) === ".md") {
|
|
54
|
+
const mdContent = jsonToMarkdownSingle(result.synced);
|
|
55
|
+
writeFileSync(mdPath, mdContent);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Treat as directory: ensure it exists and write multi-doc
|
|
59
|
+
mkdirSync(mdPath, { recursive: true });
|
|
60
|
+
jsonToMarkdownMultiDoc(result.synced, mdPath);
|
|
61
|
+
}
|
|
46
62
|
}
|
|
47
63
|
}
|
|
64
|
+
// If markdown file didn't exist before but does now (we created it), mark mdChanged as true
|
|
65
|
+
if (!mdExistedBefore && existsSync(mdPath) && !dryRun) {
|
|
66
|
+
result.mdChanged = true;
|
|
67
|
+
}
|
|
48
68
|
return result;
|
|
49
69
|
}
|
|
50
70
|
const syncOpts = z
|
|
@@ -97,5 +117,9 @@ export const syncCommandDef = {
|
|
|
97
117
|
if (result.changedNodes.length > 0) {
|
|
98
118
|
console.log(` Changed nodes: ${result.changedNodes.join(", ")}`);
|
|
99
119
|
}
|
|
120
|
+
const hasDrift = result.jsonChanged || result.mdChanged || result.conflict;
|
|
121
|
+
if ((opts.dryRun || opts.report) && hasDrift) {
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
100
124
|
},
|
|
101
125
|
};
|
|
@@ -105,6 +105,24 @@ const NODE_TYPE_SHAPES = {
|
|
|
105
105
|
export function mermaidShapeForNode(node) {
|
|
106
106
|
return NODE_TYPE_SHAPES[node.type] ?? "rectangle";
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Escape a Mermaid label by wrapping in quotes if it contains special characters.
|
|
110
|
+
* Mermaid shape delimiters and other special chars need escaping: ( ) { } [ ] / \
|
|
111
|
+
* Wrapping in double quotes allows these characters to be rendered as-is.
|
|
112
|
+
* @param label - The label text to escape
|
|
113
|
+
* @returns The label, quoted if it contains special characters
|
|
114
|
+
* @example
|
|
115
|
+
* escapeMermaidLabel("Firebase (Tenant)") // returns '"Firebase (Tenant)"'
|
|
116
|
+
*/
|
|
117
|
+
function escapeMermaidLabel(label) {
|
|
118
|
+
// Check if label contains any Mermaid special characters that need escaping
|
|
119
|
+
const specialChars = ["(", ")", "{", "}", "[", "]", "/", "\\"];
|
|
120
|
+
if (specialChars.some((char) => label.includes(char))) {
|
|
121
|
+
// Wrap in double quotes to escape
|
|
122
|
+
return `"${label}"`;
|
|
123
|
+
}
|
|
124
|
+
return label;
|
|
125
|
+
}
|
|
108
126
|
/**
|
|
109
127
|
* Render a Mermaid node definition for a node id/name/shape.
|
|
110
128
|
* @param id - Node id
|
|
@@ -116,16 +134,17 @@ export function mermaidShapeForNode(node) {
|
|
|
116
134
|
export function renderMermaidNode(id, name, shape, mode = "friendly") {
|
|
117
135
|
const safeId = sanitiseMermaidId(id);
|
|
118
136
|
const label = mode === "compact" ? id : `${id}: ${name}`;
|
|
137
|
+
const escapedLabel = escapeMermaidLabel(label);
|
|
119
138
|
switch (shape) {
|
|
120
139
|
case "rounded":
|
|
121
|
-
return `${safeId}([${
|
|
140
|
+
return `${safeId}([${escapedLabel}])`;
|
|
122
141
|
case "rhombus":
|
|
123
|
-
return `${safeId}{{${
|
|
142
|
+
return `${safeId}{{${escapedLabel}}}`;
|
|
124
143
|
case "parallelogram":
|
|
125
|
-
return `${safeId}[/${
|
|
144
|
+
return `${safeId}[/${escapedLabel}/]`;
|
|
126
145
|
case "rectangle":
|
|
127
146
|
default:
|
|
128
|
-
return `${safeId}[${
|
|
147
|
+
return `${safeId}[${escapedLabel}]`;
|
|
129
148
|
}
|
|
130
149
|
}
|
|
131
150
|
/**
|
package/dist/src/sync.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { readFileSync, statSync, existsSync } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { markdownToJson } from "./md-to-json.js";
|
|
4
4
|
import { SysProMDocument } from "./schema.js";
|
|
@@ -42,8 +42,10 @@ export function detectChanges(jsonPath, mdPath) {
|
|
|
42
42
|
if (!SysProMDocument.is(jsonDoc)) {
|
|
43
43
|
throw new Error("JSON file is not a valid SysProM document");
|
|
44
44
|
}
|
|
45
|
-
// Parse Markdown to document
|
|
46
|
-
const mdDoc =
|
|
45
|
+
// Parse Markdown to document (or treat as empty if it doesn't exist)
|
|
46
|
+
const mdDoc = existsSync(mdPath)
|
|
47
|
+
? markdownToJson(mdPath)
|
|
48
|
+
: { nodes: [], relationships: [] };
|
|
47
49
|
// Compare parsed documents
|
|
48
50
|
const jsonHash = normaliseHash(jsonDoc);
|
|
49
51
|
const mdHash = normaliseHash(mdDoc);
|
|
@@ -55,6 +57,14 @@ export function detectChanges(jsonPath, mdPath) {
|
|
|
55
57
|
conflict: false,
|
|
56
58
|
};
|
|
57
59
|
}
|
|
60
|
+
// If markdown file doesn't exist, treat as if JSON changed (JSON is the source)
|
|
61
|
+
if (!existsSync(mdPath)) {
|
|
62
|
+
return {
|
|
63
|
+
jsonChanged: true,
|
|
64
|
+
mdChanged: false,
|
|
65
|
+
conflict: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
58
68
|
// Parsed documents differ. Use file modification times to determine which changed.
|
|
59
69
|
// The file that was modified more recently is the one that diverged.
|
|
60
70
|
const jsonStats = statSync(jsonPath);
|