@greenarmor/ges-core 1.5.1 → 1.5.2

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,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { safeWriteJson } from "../utils/index.js";
3
4
  export function loadActivityLog(projectPath) {
4
5
  const logPath = path.join(projectPath, ".ges", "activity-log.json");
5
6
  try {
@@ -14,14 +15,10 @@ export function loadActivityLog(projectPath) {
14
15
  export function appendActivityLog(projectPath, entries) {
15
16
  if (entries.length === 0)
16
17
  return;
17
- const gesDir = path.join(projectPath, ".ges");
18
- if (!fs.existsSync(gesDir)) {
19
- fs.mkdirSync(gesDir, { recursive: true });
20
- }
21
- const logPath = path.join(gesDir, "activity-log.json");
18
+ const logPath = path.join(projectPath, ".ges", "activity-log.json");
22
19
  const existing = loadActivityLog(projectPath);
23
20
  const updated = existing.concat(entries);
24
- fs.writeFileSync(logPath, JSON.stringify(updated, null, 2), "utf-8");
21
+ safeWriteJson(logPath, updated);
25
22
  }
26
23
  export function clearActivityLog(projectPath) {
27
24
  const logPath = path.join(projectPath, ".ges", "activity-log.json");
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { safeWriteJson } from "../utils/index.js";
3
4
  const GES_DIR = ".ges";
4
5
  const CONTROLS_DIR = "controls";
5
6
  const CONFIG_FILE = "config.json";
@@ -67,10 +68,6 @@ export function loadControlOverrides(projectPath) {
67
68
  }
68
69
  }
69
70
  export function saveControlOverride(projectPath, controlId, status, reason) {
70
- const gesDir = path.join(projectPath, GES_DIR);
71
- if (!fs.existsSync(gesDir)) {
72
- fs.mkdirSync(gesDir, { recursive: true });
73
- }
74
71
  const overrides = loadControlOverrides(projectPath);
75
72
  const existingIdx = overrides.findIndex(o => o.control_id === controlId);
76
73
  const entry = { control_id: controlId, status, reason };
@@ -80,8 +77,8 @@ export function saveControlOverride(projectPath, controlId, status, reason) {
80
77
  else {
81
78
  overrides.push(entry);
82
79
  }
83
- const overridesPath = path.join(gesDir, OVERRIDES_FILE);
84
- fs.writeFileSync(overridesPath, JSON.stringify(overrides, null, 2), "utf-8");
80
+ const overridesPath = path.join(projectPath, GES_DIR, OVERRIDES_FILE);
81
+ safeWriteJson(overridesPath, overrides);
85
82
  }
86
83
  export function applyOverridesToControls(controls, overrides) {
87
84
  if (overrides.length === 0)
@@ -118,7 +115,7 @@ export function addFrameworkToConfig(projectPath, framework) {
118
115
  if (fwLower.has(framework.toLowerCase()))
119
116
  return false;
120
117
  config.frameworks.push(framework);
121
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
118
+ safeWriteJson(configPath, config);
122
119
  return true;
123
120
  }
124
121
  catch {
@@ -136,7 +133,7 @@ export function removeFrameworkFromConfig(projectPath, framework) {
136
133
  config.frameworks = config.frameworks.filter((f) => f.toLowerCase() !== framework.toLowerCase());
137
134
  if (config.frameworks.length === before)
138
135
  return false;
139
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
136
+ safeWriteJson(configPath, config);
140
137
  return true;
141
138
  }
142
139
  catch {
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { safeWriteJson } from "../utils/index.js";
3
4
  const ASSIGNMENTS_FILE = "fix-assignments.json";
4
5
  function assignmentsPath(projectPath) {
5
6
  return path.join(projectPath, ".ges", ASSIGNMENTS_FILE);
@@ -16,11 +17,7 @@ export function loadFixAssignments(projectPath) {
16
17
  }
17
18
  }
18
19
  export function saveFixAssignments(projectPath, assignments) {
19
- const gesDir = path.join(projectPath, ".ges");
20
- if (!fs.existsSync(gesDir)) {
21
- fs.mkdirSync(gesDir, { recursive: true });
22
- }
23
- fs.writeFileSync(assignmentsPath(projectPath), JSON.stringify(assignments, null, 2), "utf-8");
20
+ safeWriteJson(assignmentsPath(projectPath), assignments);
24
21
  }
25
22
  let assignmentCounter = 0;
26
23
  export function generateAssignmentId() {
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { safeWriteJson } from "../utils/index.js";
3
4
  export function loadFixHistory(projectPath) {
4
5
  const histPath = path.join(projectPath, ".ges", "fix-history.json");
5
6
  try {
@@ -14,14 +15,10 @@ export function loadFixHistory(projectPath) {
14
15
  export function appendFixHistory(projectPath, entries) {
15
16
  if (entries.length === 0)
16
17
  return;
17
- const gesDir = path.join(projectPath, ".ges");
18
- if (!fs.existsSync(gesDir)) {
19
- fs.mkdirSync(gesDir, { recursive: true });
20
- }
21
- const histPath = path.join(gesDir, "fix-history.json");
18
+ const histPath = path.join(projectPath, ".ges", "fix-history.json");
22
19
  const existing = loadFixHistory(projectPath);
23
20
  const updated = existing.concat(entries);
24
- fs.writeFileSync(histPath, JSON.stringify(updated, null, 2), "utf-8");
21
+ safeWriteJson(histPath, updated);
25
22
  }
26
23
  export function clearFixHistory(projectPath) {
27
24
  const histPath = path.join(projectPath, ".ges", "fix-history.json");
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { safeWriteJson } from "../utils/index.js";
3
4
  const GOVERNANCE_FILE = "governance-records.json";
4
5
  function recordsPath(projectPath) {
5
6
  return path.join(projectPath, ".ges", GOVERNANCE_FILE);
@@ -16,11 +17,7 @@ export function loadGovernanceRecords(projectPath) {
16
17
  }
17
18
  }
18
19
  export function saveGovernanceRecords(projectPath, records) {
19
- const gesDir = path.join(projectPath, ".ges");
20
- if (!fs.existsSync(gesDir)) {
21
- fs.mkdirSync(gesDir, { recursive: true });
22
- }
23
- fs.writeFileSync(recordsPath(projectPath), JSON.stringify(records, null, 2), "utf-8");
20
+ safeWriteJson(recordsPath(projectPath), records);
24
21
  }
25
22
  let govCounter = 0;
26
23
  export function generateGovernanceId() {
package/dist/index.d.ts CHANGED
@@ -7,3 +7,4 @@ export * from "./activity-log/index.js";
7
7
  export * from "./recommendations/index.js";
8
8
  export * from "./governance/index.js";
9
9
  export * from "./fix-assignments/index.js";
10
+ export * from "./utils/index.js";
package/dist/index.js CHANGED
@@ -7,3 +7,4 @@ export * from "./activity-log/index.js";
7
7
  export * from "./recommendations/index.js";
8
8
  export * from "./governance/index.js";
9
9
  export * from "./fix-assignments/index.js";
10
+ export * from "./utils/index.js";
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { safeWriteFile } from "../utils/index.js";
3
4
  let recCounter = 0;
4
5
  export function recordAIRecommendation(projectPath, opts) {
5
6
  recCounter++;
@@ -58,7 +59,7 @@ export function recordAIRecommendation(projectPath, opts) {
58
59
  md.push(`---`);
59
60
  md.push(`*This recommendation was generated by an AI assistant using the GESF MCP server. It is logged here for developer review and is NOT automatically applied to the project.*`);
60
61
  md.push("");
61
- fs.writeFileSync(path.join(devLogsDir, fileName), md.join("\n"), "utf-8");
62
+ safeWriteFile(path.join(devLogsDir, fileName), md.join("\n"));
62
63
  return recommendation;
63
64
  }
64
65
  export function loadAIRecommendations(projectPath) {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Writes JSON to disk atomically using write-to-temp-then-rename.
3
+ *
4
+ * On POSIX systems, `fs.renameSync()` is atomic — the file is either
5
+ * the old version or the new version, never half-written. This prevents
6
+ * corruption if the process crashes mid-write (power loss, OOM, SIGKILL).
7
+ *
8
+ * The temp file is created in the same directory as the target to ensure
9
+ * the rename operation is atomic (cross-device renames are not atomic).
10
+ */
11
+ export declare function safeWriteJson(filePath: string, data: unknown, indent?: number): void;
12
+ /**
13
+ * Writes a string to disk atomically using write-to-temp-then-rename.
14
+ *
15
+ * Creates parent directories if they don't exist.
16
+ * Uses `.tmp` extension for the intermediate file, cleaned up on error.
17
+ */
18
+ export declare function safeWriteFile(filePath: string, content: string, _encoding?: BufferEncoding): void;
19
+ /**
20
+ * Reads and parses a JSON file with error handling.
21
+ * Returns the fallback value if the file doesn't exist or is malformed.
22
+ */
23
+ export declare function safeReadJson<T>(filePath: string, fallback: T): T;
@@ -0,0 +1,59 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ /**
4
+ * Writes JSON to disk atomically using write-to-temp-then-rename.
5
+ *
6
+ * On POSIX systems, `fs.renameSync()` is atomic — the file is either
7
+ * the old version or the new version, never half-written. This prevents
8
+ * corruption if the process crashes mid-write (power loss, OOM, SIGKILL).
9
+ *
10
+ * The temp file is created in the same directory as the target to ensure
11
+ * the rename operation is atomic (cross-device renames are not atomic).
12
+ */
13
+ export function safeWriteJson(filePath, data, indent = 2) {
14
+ const json = JSON.stringify(data, null, indent);
15
+ safeWriteFile(filePath, json);
16
+ }
17
+ /**
18
+ * Writes a string to disk atomically using write-to-temp-then-rename.
19
+ *
20
+ * Creates parent directories if they don't exist.
21
+ * Uses `.tmp` extension for the intermediate file, cleaned up on error.
22
+ */
23
+ export function safeWriteFile(filePath, content, _encoding = "utf-8") {
24
+ const dir = path.dirname(filePath);
25
+ if (!fs.existsSync(dir)) {
26
+ fs.mkdirSync(dir, { recursive: true });
27
+ }
28
+ const tmpPath = filePath + ".tmp";
29
+ try {
30
+ fs.writeFileSync(tmpPath, content, "utf-8");
31
+ fs.renameSync(tmpPath, filePath);
32
+ }
33
+ catch (err) {
34
+ // Clean up temp file if rename failed
35
+ try {
36
+ if (fs.existsSync(tmpPath)) {
37
+ fs.unlinkSync(tmpPath);
38
+ }
39
+ }
40
+ catch {
41
+ // ignore cleanup errors
42
+ }
43
+ throw err;
44
+ }
45
+ }
46
+ /**
47
+ * Reads and parses a JSON file with error handling.
48
+ * Returns the fallback value if the file doesn't exist or is malformed.
49
+ */
50
+ export function safeReadJson(filePath, fallback) {
51
+ try {
52
+ const raw = fs.readFileSync(filePath, "utf-8");
53
+ const data = JSON.parse(raw);
54
+ return data;
55
+ }
56
+ catch {
57
+ return fallback;
58
+ }
59
+ }
package/package.json CHANGED
@@ -24,7 +24,7 @@
24
24
  "name": "@greenarmor/ges-core",
25
25
  "type": "module",
26
26
  "types": "./dist/index.d.ts",
27
- "version": "1.5.1",
27
+ "version": "1.5.2",
28
28
  "scripts": {
29
29
  "build": "tsc",
30
30
  "clean": "rm -rf dist tsconfig.tsbuildinfo",