log-llm-config 1.2.8 → 1.3.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.
@@ -9,23 +9,32 @@
9
9
  * Background: compliance_check_runner runs syncRemediations (network) then the same local check.
10
10
  */
11
11
  import { existsSync, readFileSync } from 'node:fs';
12
+ import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
12
13
  import { readRemediationInstructionsFile, writeRemediationInstructionsFile } from './management_storage.js';
13
- import { hookRunLog } from './hook_logger.js';
14
+ import { complianceRunnerDiag, hookRunLog } from './hook_logger.js';
14
15
  import { loadEndpointBase } from '../sender/endpoint_config.js';
15
- import { resolveHardwareUuid } from './hardware_uuid.js';
16
- import { enforceRemediation, fetchSync, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
16
+ import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
17
+ import { buildDeferredCursorRestartCommand, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
17
18
  import { sendConfigFile } from '../sender/batch_sender.js';
18
19
  import { readStoredAuthKey } from '../auth/auth_key_store.js';
19
20
  // ---------------------------------------------------------------------------
20
21
  // Helpers
21
22
  // ---------------------------------------------------------------------------
22
23
  /** Traverse a JSON object using dot-notation path. Returns undefined if any segment is missing. */
23
- function getByPath(obj, path) {
24
+ export function getByPath(obj, path) {
24
25
  const parts = path.split('.');
25
26
  let current = obj;
26
27
  for (const part of parts) {
27
28
  if (current == null || typeof current !== 'object')
28
29
  return undefined;
30
+ // Cursor composerState.modes4: non-numeric segment indexes by mode id (agent row is not always [0]).
31
+ if (Array.isArray(current) && !/^\d+$/.test(part)) {
32
+ const idx = current.findIndex((item) => item !== null &&
33
+ typeof item === 'object' &&
34
+ item.id === part);
35
+ current = idx >= 0 ? current[idx] : undefined;
36
+ continue;
37
+ }
29
38
  current = current[part];
30
39
  }
31
40
  return current;
@@ -76,6 +85,30 @@ function verifyOpsApplied(configJson, settingPath, ops) {
76
85
  }
77
86
  return { ok: true, expected: null };
78
87
  }
88
+ /** Plain JSON file or virtual `…/state.vscdb#composerState` path for ItemTable-backed settings. */
89
+ function loadRemediationConfigJson(configFilePath) {
90
+ const hashIdx = configFilePath.indexOf('#');
91
+ if (hashIdx >= 0) {
92
+ const dbPath = configFilePath.slice(0, hashIdx);
93
+ const itemKey = configFilePath.slice(hashIdx + 1).trim();
94
+ if (!itemKey)
95
+ return { ok: false, reason: 'empty_vscdb_key' };
96
+ if (!existsSync(dbPath))
97
+ return { ok: false, reason: 'db_not_found' };
98
+ const wrapped = readVscdbItemTableJson(dbPath, itemKey);
99
+ if (wrapped === null)
100
+ return { ok: false, reason: 'vscdb_read_failed' };
101
+ return { ok: true, json: wrapped };
102
+ }
103
+ if (!existsSync(configFilePath))
104
+ return { ok: false, reason: 'file_not_found' };
105
+ try {
106
+ return { ok: true, json: JSON.parse(readFileSync(configFilePath, 'utf8')) };
107
+ }
108
+ catch {
109
+ return { ok: false, reason: 'parse_error' };
110
+ }
111
+ }
79
112
  // ---------------------------------------------------------------------------
80
113
  // Check runner — Section 6: real per-check evaluation
81
114
  // ---------------------------------------------------------------------------
@@ -104,18 +137,21 @@ export function runLocalRemediationComplianceCheck() {
104
137
  const checks = compliance.checks ?? [];
105
138
  if (checks.length === 0)
106
139
  continue;
107
- if (!existsSync(entry.config_file_path)) {
108
- hookRunLog(`compliance_check: config file not found, skipping uuid=${entry.uuid}`);
109
- continue;
110
- }
111
- let configJson;
112
- try {
113
- configJson = JSON.parse(readFileSync(entry.config_file_path, 'utf8'));
114
- }
115
- catch {
116
- hookRunLog(`compliance_check: could not parse config file, skipping uuid=${entry.uuid}`);
140
+ const loaded = loadRemediationConfigJson(entry.config_file_path);
141
+ if (!loaded.ok) {
142
+ const msg = loaded.reason === 'file_not_found'
143
+ ? `compliance_check: config file not found, skipping uuid=${entry.uuid}`
144
+ : loaded.reason === 'db_not_found'
145
+ ? `compliance_check: vscdb file not found, skipping uuid=${entry.uuid}`
146
+ : loaded.reason === 'vscdb_read_failed'
147
+ ? `compliance_check: could not read vscdb (sqlite3 missing or invalid JSON?), skipping uuid=${entry.uuid}`
148
+ : loaded.reason === 'empty_vscdb_key'
149
+ ? `compliance_check: invalid vscdb path (empty # key), skipping uuid=${entry.uuid}`
150
+ : `compliance_check: could not parse config file, skipping uuid=${entry.uuid}`;
151
+ hookRunLog(msg);
117
152
  continue;
118
153
  }
154
+ const configJson = loaded.json;
119
155
  for (const check of checks) {
120
156
  // Prefer ops-based verification (matches delta apply semantics; doesn't require full after snapshot).
121
157
  if (check.ops) {
@@ -127,6 +163,8 @@ export function runLocalRemediationComplianceCheck() {
127
163
  finding_formatted_id: compliance.finding_formatted_id,
128
164
  setting_path: check.setting_path,
129
165
  description: check.description,
166
+ finding_title: entry.finding_title,
167
+ finding_description: entry.finding_description,
130
168
  severity: compliance.severity,
131
169
  autofix_allowed: compliance.autofix_allowed,
132
170
  config_file_path: entry.config_file_path,
@@ -149,6 +187,8 @@ export function runLocalRemediationComplianceCheck() {
149
187
  finding_formatted_id: compliance.finding_formatted_id,
150
188
  setting_path: check.setting_path,
151
189
  description: check.description,
190
+ finding_title: entry.finding_title,
191
+ finding_description: entry.finding_description,
152
192
  severity: compliance.severity,
153
193
  autofix_allowed: compliance.autofix_allowed,
154
194
  config_file_path: entry.config_file_path,
@@ -181,15 +221,23 @@ export function runLocalRemediationComplianceCheck() {
181
221
  export function applyAutofixViolations(violations) {
182
222
  const autofixable = violations.filter((v) => v.autofix_allowed);
183
223
  if (autofixable.length === 0)
184
- return { fixed: 0, restartCommands: [], failedViolations: [], reportPromises: [] };
224
+ return {
225
+ fixed: 0,
226
+ appliedViolations: [],
227
+ restartCommands: [],
228
+ failedViolations: [],
229
+ reportPromises: [],
230
+ };
185
231
  const { remediations } = readRemediationInstructionsFile();
186
232
  const byUuid = new Map(remediations.map((r) => [r.uuid, r]));
187
233
  let fixed = 0;
234
+ const appliedViolations = [];
188
235
  const seen = new Set();
189
236
  const restartCommands = [];
190
237
  const failedViolations = [];
191
238
  const reportPromises = [];
192
239
  const oneTimeAppliedUuids = new Set();
240
+ let deferredSqlitePending = false;
193
241
  for (const violation of autofixable) {
194
242
  if (seen.has(violation.uuid))
195
243
  continue;
@@ -200,48 +248,83 @@ export function applyAutofixViolations(violations) {
200
248
  continue;
201
249
  }
202
250
  const inst = instruction;
203
- const ok = enforceRemediation(inst);
204
- if (ok) {
205
- seen.add(violation.uuid);
206
- fixed++;
207
- hookRunLog(`autofix: applied uuid=${inst.uuid} path=${inst.config_file_path}`);
208
- reportPromises.push(reportAutofixApplied(inst.uuid, 'success'));
209
- const authKey = readStoredAuthKey();
210
- if (authKey) {
251
+ complianceRunnerDiag(`autofix: calling enforceRemediation uuid=${inst.uuid} path=${inst.config_file_path}`);
252
+ const er = enforceRemediation(inst);
253
+ if (!er.ok) {
254
+ failedViolations.push(violation);
255
+ continue;
256
+ }
257
+ if (er.deferredSqlite)
258
+ deferredSqlitePending = true;
259
+ seen.add(violation.uuid);
260
+ fixed++;
261
+ appliedViolations.push(violation);
262
+ hookRunLog(`autofix: applied uuid=${inst.uuid} path=${inst.config_file_path}`);
263
+ reportPromises.push(reportAutofixApplied(inst.uuid, 'success'));
264
+ const authKey = readStoredAuthKey();
265
+ if (authKey) {
266
+ if (er.deferredSqlite && inst.config_file_path.includes('#')) {
267
+ hookRunLog(`autofix: skip immediate vscdb upload (deferred until after restart) uuid=${inst.uuid}`);
268
+ }
269
+ else {
211
270
  let updatedContent;
212
- try {
213
- updatedContent = JSON.parse(readFileSync(inst.config_file_path, 'utf8'));
271
+ if (inst.config_file_path.includes('#')) {
272
+ const hi = inst.config_file_path.indexOf('#');
273
+ const dbPath = inst.config_file_path.slice(0, hi);
274
+ const itemKey = inst.config_file_path.slice(hi + 1).trim();
275
+ updatedContent =
276
+ itemKey ? (readVscdbItemTableJson(dbPath, itemKey) ?? undefined) : undefined;
214
277
  }
215
- catch {
216
- updatedContent = undefined;
278
+ else {
279
+ try {
280
+ updatedContent = JSON.parse(readFileSync(inst.config_file_path, 'utf8'));
281
+ }
282
+ catch {
283
+ updatedContent = undefined;
284
+ }
217
285
  }
218
286
  if (updatedContent !== undefined) {
219
287
  const fileType = (inst.file_type ?? '').trim();
220
288
  if (fileType) {
221
- reportPromises.push(sendConfigFile({ file_type: fileType, file_path: inst.config_file_path, raw_content: updatedContent }, resolveHardwareUuid(), authKey).then((sentOk) => {
222
- hookRunLog(`autofix: uploaded remediated file uuid=${inst.uuid} path=${inst.config_file_path} ok=${sentOk}`);
223
- }));
289
+ const hw = tryResolveHardwareUuid();
290
+ if (hw) {
291
+ reportPromises.push(sendConfigFile({ file_type: fileType, file_path: inst.config_file_path, raw_content: updatedContent }, hw, authKey).then((sentOk) => {
292
+ hookRunLog(`autofix: uploaded remediated file uuid=${inst.uuid} path=${inst.config_file_path} ok=${sentOk}`);
293
+ }));
294
+ }
295
+ else {
296
+ hookRunLog(`autofix: skip upload uuid=${inst.uuid} (hardware UUID unavailable)`);
297
+ }
224
298
  }
225
299
  else {
226
300
  hookRunLog(`autofix: skip upload uuid=${inst.uuid} — remediation_instructions.json missing file_type (re-sync manifest)`);
227
301
  }
228
302
  }
229
303
  }
230
- else {
231
- hookRunLog(`autofix: skip re-upload uuid=${inst.uuid} (no stored auth key)`);
232
- }
233
- const spec = remediationFixSpec(inst);
234
- if (spec?.restart_required && spec.restart_command) {
235
- hookRunLog(`autofix: restart required uuid=${inst.uuid} command=${spec.restart_command}`);
236
- restartCommands.push(spec.restart_command);
237
- }
238
- if (!inst.is_enforced) {
239
- oneTimeAppliedUuids.add(inst.uuid);
240
- }
241
304
  }
242
305
  else {
243
- failedViolations.push(violation);
306
+ hookRunLog(`autofix: skip re-upload uuid=${inst.uuid} (no stored auth key)`);
244
307
  }
308
+ const spec = remediationFixSpec(inst);
309
+ if (spec?.restart_required && spec.restart_command) {
310
+ if (!er.deferredSqlite) {
311
+ if (isTrustedRestartCommandForAutofix(spec.restart_command)) {
312
+ hookRunLog(`autofix: restart required uuid=${inst.uuid} command=${spec.restart_command}`);
313
+ restartCommands.push(spec.restart_command);
314
+ }
315
+ else {
316
+ hookRunLog(`autofix: restart command rejected (not an allowlisted template) uuid=${inst.uuid}`);
317
+ }
318
+ }
319
+ }
320
+ if (!inst.is_enforced) {
321
+ oneTimeAppliedUuids.add(inst.uuid);
322
+ }
323
+ }
324
+ if (deferredSqlitePending) {
325
+ restartCommands.length = 0;
326
+ restartCommands.push(buildDeferredCursorRestartCommand());
327
+ hookRunLog('autofix: deferred vscdb — restart command runs apply_deferred_vscdb.js then open -a Cursor');
245
328
  }
246
329
  if (oneTimeAppliedUuids.size > 0) {
247
330
  const remaining = remediations.filter((r) => !oneTimeAppliedUuids.has(r.uuid));
@@ -250,14 +333,20 @@ export function applyAutofixViolations(violations) {
250
333
  // Send a post-autofix heartbeat so the server sees the updated (reduced) UUID set immediately,
251
334
  // without waiting for the background runner (which may be locked out).
252
335
  const remainingUuids = remaining.map((r) => r.uuid);
253
- reportPromises.push(fetchSync(loadEndpointBase(), resolveHardwareUuid(), remainingUuids)
254
- .then(() => hookRunLog(`autofix: post-autofix heartbeat sent uuids=${remainingUuids.length}`))
255
- .catch(() => undefined));
336
+ const hwHeartbeat = tryResolveHardwareUuid();
337
+ if (hwHeartbeat) {
338
+ reportPromises.push(fetchSync(loadEndpointBase(), hwHeartbeat, remainingUuids)
339
+ .then(() => hookRunLog(`autofix: post-autofix heartbeat sent uuids=${remainingUuids.length}`))
340
+ .catch(() => undefined));
341
+ }
342
+ else {
343
+ hookRunLog('autofix: skip post-autofix heartbeat (hardware UUID unavailable)');
344
+ }
256
345
  }
257
346
  if (fixed > 0) {
258
347
  hookRunLog(`autofix: total_applied=${fixed}`);
259
348
  }
260
- return { fixed, restartCommands, failedViolations, reportPromises };
349
+ return { fixed, appliedViolations, restartCommands, failedViolations, reportPromises, deferredSqlitePending };
261
350
  }
262
351
  /**
263
352
  * Remove satisfied one-time remediations from local remediation_instructions.json.
@@ -285,18 +374,12 @@ export function pruneSatisfiedOneTimeRemediations() {
285
374
  remaining.push(raw);
286
375
  continue;
287
376
  }
288
- if (!existsSync(inst.config_file_path)) {
289
- remaining.push(raw);
290
- continue;
291
- }
292
- let configJson;
293
- try {
294
- configJson = JSON.parse(readFileSync(inst.config_file_path, 'utf8'));
295
- }
296
- catch {
377
+ const prLoaded = loadRemediationConfigJson(inst.config_file_path);
378
+ if (!prLoaded.ok) {
297
379
  remaining.push(raw);
298
380
  continue;
299
381
  }
382
+ const configJson = prLoaded.json;
300
383
  // Only prune when every check is ops-based and currently satisfied.
301
384
  let okAll = true;
302
385
  for (const check of checks) {
@@ -323,11 +406,16 @@ export function pruneSatisfiedOneTimeRemediations() {
323
406
  writeRemediationInstructionsFile({ remediations: remaining });
324
407
  hookRunLog(`remediation_prune: removed=${removed} remaining=${remaining.length}`);
325
408
  const remainingUuids = remaining.map((r) => r.uuid);
326
- const reportPromises = [
327
- fetchSync(loadEndpointBase(), resolveHardwareUuid(), remainingUuids)
409
+ const hw = tryResolveHardwareUuid();
410
+ const reportPromises = [];
411
+ if (hw) {
412
+ reportPromises.push(fetchSync(loadEndpointBase(), hw, remainingUuids)
328
413
  .then(() => hookRunLog(`remediation_prune: post-prune heartbeat sent uuids=${remainingUuids.length}`))
329
- .catch(() => undefined),
330
- ];
414
+ .catch(() => undefined));
415
+ }
416
+ else {
417
+ hookRunLog('remediation_prune: skip post-prune heartbeat (hardware UUID unavailable)');
418
+ }
331
419
  return { removed, reportPromises };
332
420
  }
333
421
  /**
@@ -24,4 +24,13 @@ function resolveHardwareUuid() {
24
24
  }
25
25
  throw new Error('Unable to determine hardware UUID via ioreg or system_profiler.');
26
26
  }
27
+ /** Same as {@link resolveHardwareUuid} but returns null when the host exposes no stable ID (e.g. Linux CI). */
28
+ export function tryResolveHardwareUuid() {
29
+ try {
30
+ return resolveHardwareUuid();
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
27
36
  export { resolveHardwareUuid };
@@ -1,15 +1,40 @@
1
- import { existsSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, appendFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
4
4
  const HOOK_LOG_FILENAME = 'hook_log.txt';
5
+ const COMPLIANCE_RUNNER_LOG_FILENAME = 'compliance_runner.log';
5
6
  function getHookLogPath() {
6
7
  const homeDir = process.env.HOME || process.env.USERPROFILE;
7
8
  if (!homeDir)
8
9
  return null;
9
10
  return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, HOOK_LOG_FILENAME);
10
11
  }
11
- /** Replace hook log file at the start of a run so it doesn't grow unboundedly. */
12
- function hookLogReplace() {
12
+ function getComplianceRunnerLogPath() {
13
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
14
+ if (!homeDir)
15
+ return null;
16
+ return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_RUNNER_LOG_FILENAME);
17
+ }
18
+ /**
19
+ * Append-only diagnostics (not truncated by main_runner). Use for remediation sync / compliance.
20
+ */
21
+ function complianceRunnerDiag(message) {
22
+ const logPath = getComplianceRunnerLogPath();
23
+ if (!logPath)
24
+ return;
25
+ try {
26
+ const dir = path.dirname(logPath);
27
+ if (!existsSync(dir))
28
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
29
+ const ts = new Date().toISOString();
30
+ appendFileSync(logPath, `${ts} ${message}\n`, 'utf8');
31
+ }
32
+ catch {
33
+ // best-effort
34
+ }
35
+ }
36
+ /** Visible delimiter between hook_log.txt sessions (compliance gate, upload, etc.). */
37
+ function hookLogSessionBanner(label) {
13
38
  const logPath = getHookLogPath();
14
39
  if (!logPath)
15
40
  return;
@@ -17,12 +42,20 @@ function hookLogReplace() {
17
42
  const dir = path.dirname(logPath);
18
43
  if (!existsSync(dir))
19
44
  mkdirSync(dir, { recursive: true, mode: 0o700 });
20
- writeFileSync(logPath, '', 'utf8');
45
+ const ts = new Date().toISOString();
46
+ const banner = `\n${'='.repeat(72)}\n${ts} session: ${label}\n${'='.repeat(72)}\n`;
47
+ appendFileSync(logPath, banner, 'utf8');
21
48
  }
22
49
  catch {
23
50
  // best-effort
24
51
  }
25
52
  }
53
+ /**
54
+ * @deprecated Name kept for callers: begins a log_config_files upload section (append-only, does not truncate).
55
+ */
56
+ function hookLogReplace() {
57
+ hookLogSessionBanner('log_config_files (config upload)');
58
+ }
26
59
  /** Append a timestamped line to hook_log.txt. */
27
60
  function hookRunLog(message) {
28
61
  const logPath = getHookLogPath();
@@ -48,4 +81,4 @@ function hookLogLine(message) {
48
81
  // best-effort
49
82
  }
50
83
  }
51
- export { getHookLogPath, hookLogReplace, hookRunLog, hookLogLine };
84
+ export { getHookLogPath, getComplianceRunnerLogPath, hookLogReplace, hookLogSessionBanner, hookRunLog, hookLogLine, complianceRunnerDiag, };
@@ -10,6 +10,7 @@ import { resolveHardwareUuid } from './hardware_uuid.js';
10
10
  import { ensureAuthentication } from '../auth/auth_flow.js';
11
11
  import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
12
12
  import { isVscdbVirtualPath, tryReadVscdbVirtualFile } from '../readers/vscdb_config_builder.js';
13
+ import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
13
14
  import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, determineFileTypeFromPath } from '../collection/config_collector.js';
14
15
  import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
15
16
  import { sendConfigFile, sendConfigFilesBatch, sendHookRequestCreate, sendHookRequestUpdateManifest, sendIngestSessionStart, sendIngestSessionFinish, BATCH_CHUNK_SIZE } from '../sender/batch_sender.js';
@@ -26,6 +27,7 @@ async function collectAllConfigFiles(endpointBase) {
26
27
  hookRunLog(msg);
27
28
  throw new Error(msg);
28
29
  }
30
+ persistVscdbComposerContractFromPatternsResponse(patternsResponse);
29
31
  hookRunLog(`scanning config files`);
30
32
  const configFiles = collectConfigFilesFromPatterns(patternsResponse.patterns, PROJECT_ROOT, hookRunLog, {
31
33
  vscdbEntrySpecs: patternsResponse.vscdb_entry_specs,
@@ -7,12 +7,43 @@ import { dirname, join } from 'node:path';
7
7
  import { homedir } from 'node:os';
8
8
  import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
9
9
  export const REMEDIATION_INSTRUCTIONS_BASENAME = 'remediation_instructions.json';
10
+ /** Pending state.vscdb writes applied after Cursor exits (IDE holds the DB locked). */
11
+ export const DEFERRED_VSCDB_APPLY_BASENAME = 'optimus_deferred_vscdb_apply.json';
12
+ /**
13
+ * Cached subset of GET file-patterns: reactive ItemTable key + composer shadow keys from the backend.
14
+ * Written when patterns are fetched; remediations and vscdb reads use this so the npm package stays free
15
+ * of hardcoded Cursor paths.
16
+ */
17
+ export const FILE_COLLECTION_VSCDB_CONTRACT_BASENAME = 'file_collection_vscdb_contract.json';
10
18
  export function getManagementDir() {
11
19
  return join(homedir(), OPT_AI_SEC_MANAGEMENT_REL);
12
20
  }
13
21
  export function getRemediationInstructionsPath() {
14
22
  return join(getManagementDir(), REMEDIATION_INSTRUCTIONS_BASENAME);
15
23
  }
24
+ export function getDeferredVscdbApplyPath() {
25
+ return join(getManagementDir(), DEFERRED_VSCDB_APPLY_BASENAME);
26
+ }
27
+ export function getFileCollectionVscdbContractPath() {
28
+ return join(getManagementDir(), FILE_COLLECTION_VSCDB_CONTRACT_BASENAME);
29
+ }
30
+ export function readFileCollectionVscdbContract() {
31
+ const path = getFileCollectionVscdbContractPath();
32
+ if (!existsSync(path))
33
+ return null;
34
+ try {
35
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
36
+ if (parsed?.version !== 1 || !Array.isArray(parsed.composer_shadow_keys))
37
+ return null;
38
+ return parsed;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ export function writeFileCollectionVscdbContract(contract) {
45
+ atomicWriteJson(getFileCollectionVscdbContractPath(), contract);
46
+ }
16
47
  export function atomicWriteJson(filePath, data) {
17
48
  const dir = dirname(filePath);
18
49
  if (!existsSync(dir))