@magnusekdahl/parallix 1.2.0 → 1.2.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.
Files changed (51) hide show
  1. package/lib/agents/agents.js +737 -838
  2. package/lib/agents/agents.ts +930 -0
  3. package/lib/agents/claude-telemetry.js +149 -154
  4. package/lib/agents/claude-telemetry.ts +233 -0
  5. package/lib/agents/claude.js +121 -131
  6. package/lib/agents/claude.ts +156 -0
  7. package/lib/agents/codex-telemetry.js +176 -164
  8. package/lib/agents/codex-telemetry.ts +205 -0
  9. package/lib/agents/codex.js +168 -201
  10. package/lib/agents/codex.ts +236 -0
  11. package/lib/agents/limit-hit.js +216 -226
  12. package/lib/agents/limit-hit.ts +262 -0
  13. package/lib/agents/mistral-telemetry.js +7 -10
  14. package/lib/agents/mistral-telemetry.ts +44 -0
  15. package/lib/agents/mistral.js +40 -58
  16. package/lib/agents/mistral.ts +84 -0
  17. package/lib/agents/opencode-export.js +126 -127
  18. package/lib/agents/opencode-export.ts +160 -0
  19. package/lib/agents/opencode-telemetry.js +282 -261
  20. package/lib/agents/opencode-telemetry.ts +350 -0
  21. package/lib/agents/opencode.js +263 -305
  22. package/lib/agents/opencode.ts +370 -0
  23. package/lib/agents/stage-telemetry.js +23 -24
  24. package/lib/agents/stage-telemetry.ts +47 -0
  25. package/lib/commands/integrate.js +9 -3
  26. package/lib/core/fmt.js +140 -211
  27. package/lib/core/fmt.ts +189 -0
  28. package/lib/core/git.js +121 -102
  29. package/lib/core/git.ts +146 -0
  30. package/lib/core/gitignore.js +59 -106
  31. package/lib/core/gitignore.ts +103 -0
  32. package/lib/core/mission-utils.js +833 -875
  33. package/lib/core/mission-utils.ts +998 -0
  34. package/lib/core/persistent-data-migration.js +247 -205
  35. package/lib/core/persistent-data-migration.ts +255 -0
  36. package/lib/core/product-config.js +400 -476
  37. package/lib/core/product-config.ts +518 -0
  38. package/lib/core/runtime-matrix.js +41 -87
  39. package/lib/core/runtime-matrix.ts +84 -0
  40. package/lib/core/spawn-tee.js +152 -192
  41. package/lib/core/spawn-tee.ts +202 -0
  42. package/lib/core/state-map.js +92 -92
  43. package/lib/core/state-map.ts +115 -0
  44. package/lib/core/storage.js +101 -129
  45. package/lib/core/storage.ts +182 -0
  46. package/lib/core/verification.js +148 -138
  47. package/lib/core/verification.ts +200 -0
  48. package/package.json +4 -2
  49. package/prompts/draft.md +1 -0
  50. package/prompts/portfolio.md +1 -1
  51. package/px.js +0 -2
@@ -1,241 +1,283 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const storage = require('./storage');
4
-
5
- /** @type {any} */
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports._internals = void 0;
40
+ exports.migrateStats = migrateStats;
41
+ exports.migrateAgentBlocklists = migrateAgentBlocklists;
42
+ const node_fs_1 = __importDefault(require("node:fs"));
43
+ const node_path_1 = __importDefault(require("node:path"));
44
+ const storage = __importStar(require("./storage.js"));
45
+ /** Cached stats module — loaded lazily to break circular dependency with `../commands/stats.js`. */
6
46
  let _stats = null;
47
+ /** Lazily load the stats module to break the circular dependency. */
7
48
  function getStats() {
8
- if (!_stats) {_stats = require('../commands/stats');}
9
- return _stats;
49
+ if (!_stats) {
50
+ _stats = require('../commands/stats.js');
51
+ }
52
+ return _stats;
10
53
  }
11
-
54
+ /** Resolve the canonical stats CSV headers. */
12
55
  function getStatsHeaders() {
13
- return getStats().STATS_HEADERS;
56
+ return getStats().STATS_HEADERS;
14
57
  }
15
-
16
58
  const ESSENTIAL_STATS_COLUMNS = ['date', 'mission', 'classification', 'implementer', 'pr_fix_rounds'];
17
-
18
- /** @param {Record<string, unknown>} row @param {number} rowIndex @param {string} filePath */
59
+ /**
60
+ * Validate that a stats row has all essential columns populated.
61
+ * Throws on the first missing column found.
62
+ * @param row - Row object to validate
63
+ * @param rowIndex - Zero-based row index (for error messages)
64
+ * @param filePath - Source file path (for error messages)
65
+ */
19
66
  function validateStatsRow(row, rowIndex, filePath) {
20
- const missing = ESSENTIAL_STATS_COLUMNS.filter(col => !(col in row) || row[col] === '');
21
- if (missing.length > 0) {
22
- throw new Error(
23
- `Malformed telemetry row ${rowIndex + 1} in ${path.resolve(filePath)}: ` +
24
- `missing essential columns: ${missing.join(', ')}`
25
- );
26
- }
67
+ const missing = ESSENTIAL_STATS_COLUMNS.filter(col => !(col in row) || row[col] === '');
68
+ if (missing.length > 0) {
69
+ throw new Error(`Malformed telemetry row ${rowIndex + 1} in ${node_path_1.default.resolve(filePath)}: ` +
70
+ `missing essential columns: ${missing.join(', ')}`);
71
+ }
27
72
  }
28
-
29
- /** @param {string} line */
73
+ /**
74
+ * Parse a single CSV line, respecting quoted fields and escaped quotes.
75
+ * @param line - Raw CSV line string
76
+ * @returns Array of parsed field values
77
+ */
30
78
  function parseCsvLine(line) {
31
- const values = [];
32
- let value = '';
33
- let quoted = false;
34
- for (let index = 0; index < line.length; index += 1) {
35
- const char = line[index];
36
- if (char === '"') {
37
- if (quoted && line[index + 1] === '"') {
38
- value += '"';
39
- index += 1;
40
- } else {
41
- quoted = !quoted;
42
- }
43
- } else if (char === ',' && !quoted) {
44
- values.push(value);
45
- value = '';
46
- } else {
47
- value += char;
48
- }
49
- }
50
- values.push(value);
51
- return values;
79
+ const values = [];
80
+ let value = '';
81
+ let quoted = false;
82
+ for (let index = 0; index < line.length; index += 1) {
83
+ const char = line[index];
84
+ if (char === '"') {
85
+ if (quoted && line[index + 1] === '"') {
86
+ value += '"';
87
+ index += 1;
88
+ }
89
+ else {
90
+ quoted = !quoted;
91
+ }
92
+ }
93
+ else if (char === ',' && !quoted) {
94
+ values.push(value);
95
+ value = '';
96
+ }
97
+ else {
98
+ value += char;
99
+ }
100
+ }
101
+ values.push(value);
102
+ return values;
52
103
  }
53
-
54
- /** @param {unknown} value */
104
+ /**
105
+ * Escape a single CSV value — wraps in quotes if it contains commas, double-quotes, or newlines.
106
+ * @param value - Value to escape
107
+ * @returns Escaped CSV-safe string
108
+ */
55
109
  function escapeCsvValue(value) {
56
- const text = String(value ?? '');
57
- return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
110
+ const text = String(value ?? '');
111
+ return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
58
112
  }
59
-
60
113
  /**
61
114
  * Minimal CSV loader — avoids circular dependency with stats module (which
62
115
  * imports this module at the top). Defined locally so `migrateStats()` can
63
116
  * read any schema without triggering a require-cycle.
64
117
  */
65
- /** @param {string} filePath */
66
118
  function loadCsv(filePath) {
67
- if (!fs.existsSync(filePath)) {
68
- return { headers: [], rows: [] };
69
- }
70
- const content = fs.readFileSync(filePath, 'utf8');
71
- const lines = content.split('\n').filter(line => line.trim());
72
- if (lines.length === 0) {
73
- return { headers: [], rows: [] };
74
- }
75
- const headers = parseCsvLine(lines[0]);
76
- const rows = [];
77
- for (let i = 1; i < lines.length; i += 1) {
78
- const values = parseCsvLine(lines[i]);
79
- const row = {};
80
- /** @param {string} header @param {number} idx */
81
- headers.forEach((header, idx) => {
82
- /** @type {Record<string, unknown>} */
83
- const r = /** @type {Record<string, unknown>} */(row);
84
- r[header] = values[idx] || '';
85
- });
86
- rows.push(row);
87
- }
88
- return { headers, rows };
119
+ if (!node_fs_1.default.existsSync(filePath)) {
120
+ return { headers: [], rows: [] };
121
+ }
122
+ const content = node_fs_1.default.readFileSync(filePath, 'utf8');
123
+ const lines = content.split('\n').filter(line => line.trim());
124
+ if (lines.length === 0) {
125
+ return { headers: [], rows: [] };
126
+ }
127
+ const headers = parseCsvLine(lines[0]);
128
+ const rows = [];
129
+ for (let i = 1; i < lines.length; i += 1) {
130
+ const values = parseCsvLine(lines[i]);
131
+ const row = {};
132
+ headers.forEach((header, idx) => {
133
+ row[header] = values[idx] || '';
134
+ });
135
+ rows.push(row);
136
+ }
137
+ return { headers, rows };
89
138
  }
90
-
91
139
  /**
92
140
  * Read a stats CSV file with any schema (legacy 5-col or extended 21-col).
93
141
  * Returns rows as arrays keyed by the 21-column STATS_HEADERS, with missing
94
142
  * columns filled by normalizeStatsRow defaults.
95
143
  */
96
- /** @param {string} filePath @param {{defaultRepo?: string}} [options] */
97
144
  function readStatsRows(filePath, options = {}) {
98
- if (!filePath || !fs.existsSync(filePath)) {return [];}
99
- const data = loadCsv(filePath);
100
- if (data.headers.length === 0) {return [];}
101
- const headers = getStatsHeaders();
102
- /** @type {{defaultRepo?: string}} */
103
- const opts = options;
104
- /** @type {Record<string, unknown>[]} */
105
- const validated = /** @type {Record<string, unknown>[]} */(data.rows).map((/** @type {Record<string, unknown>} */ row, idx) => {
106
- validateStatsRow(row, idx, filePath);
107
- return row;
108
- });
109
- /** @param {string} h */
110
- return validated
111
- .map(row => getStats().normalizeStatsRow(row, { repo: opts.defaultRepo }))
112
- .map(row => headers.map((/** @type {string} */ h) => row[h] || ''));
145
+ if (!filePath || !node_fs_1.default.existsSync(filePath)) {
146
+ return [];
147
+ }
148
+ const data = loadCsv(filePath);
149
+ if (data.headers.length === 0) {
150
+ return [];
151
+ }
152
+ const headers = getStatsHeaders();
153
+ const opts = options;
154
+ const validated = data.rows.map((row, idx) => {
155
+ validateStatsRow(row, idx, filePath);
156
+ return row;
157
+ });
158
+ return validated
159
+ .map(row => getStats().normalizeStatsRow(row, { repo: opts.defaultRepo }))
160
+ .map((row) => headers.map((h) => String(row[h] || '')));
113
161
  }
114
-
115
- /** @param {string[][]} rows */
162
+ /**
163
+ * Serialize an array of stats rows back to CSV format with canonical headers.
164
+ * @param rows - Array of row arrays (each row is an array of string values)
165
+ * @returns CSV string with headers and data rows
166
+ */
116
167
  function serializeStatsRows(rows) {
117
- const headers = getStatsHeaders();
118
- return `${[
119
- headers.join(','),
120
- ...rows.map(row => row.map(escapeCsvValue).join(','))
121
- ].join('\n')}\n`;
168
+ const headers = getStatsHeaders();
169
+ return `${[
170
+ headers.join(','),
171
+ ...rows.map(row => row.map(escapeCsvValue).join(','))
172
+ ].join('\n')}\n`;
122
173
  }
123
-
124
- /** @param {{sourcePaths?: string[], sourcePath?: string, destinationPath?: string}} [options] */
174
+ /**
175
+ * Merge stats from one or more source CSV files into the destination file.
176
+ * Deduplicates full rows. Creates the destination directory if needed.
177
+ * When no source data is available, returns early without writing a header-only file.
178
+ * @param options - Migration configuration (sourcePaths, sourcePath, destinationPath)
179
+ * @returns Result with destination path, import counts, and optional warning
180
+ */
125
181
  function migrateStats(options = {}) {
126
- /** @type {{sourcePaths?: string[], sourcePath?: string, destinationPath?: string}} */
127
- const opts = options;
128
- const sourcePaths = /** @type {string[]} */(opts.sourcePaths || [opts.sourcePath].filter(Boolean));
129
- const destinationPath = opts.destinationPath || storage.resolveStatsPath({ ensureDir: true });
130
- /** @param {string} sourcePath */
131
- const sourceRows = sourcePaths.flatMap((/** @type {string} */ sourcePath) => readStatsRows(sourcePath, /** @type {{defaultRepo?: string}} */({})));
132
-
133
- // Fresh-install guard: when no source file exists or all sources are empty,
134
- // there is no telemetry to import. Returning early prevents writing a
135
- // header-only destination file that would masquerade as valid stats.
136
- if (sourceRows.length === 0) {
137
- return { destinationPath, imported: 0, rows: 0, warn: 'no source data available' };
138
- }
139
-
140
- const destinationRows = readStatsRows(destinationPath, /** @type {{defaultRepo?: string}} */({}));
141
- const rows = [];
142
- const seen = new Set();
143
-
144
- for (const row of [...destinationRows, ...sourceRows]) {
145
- const key = JSON.stringify(row);
146
- if (!seen.has(key)) {
147
- seen.add(key);
148
- rows.push(row);
149
- }
150
- }
151
-
152
- const content = serializeStatsRows(rows);
153
- const current = fs.existsSync(destinationPath) ? fs.readFileSync(destinationPath, 'utf8') : null;
154
- if (current !== content) {storage.writeFileAtomic(destinationPath, content);}
155
- return { destinationPath, imported: rows.length - destinationRows.length, rows: rows.length };
182
+ const opts = options;
183
+ const sourcePaths = (opts.sourcePaths || [opts.sourcePath].filter(Boolean));
184
+ const destinationPath = opts.destinationPath || storage.resolveStatsPath({ ensureDir: true });
185
+ const sourceRows = sourcePaths.flatMap((sourcePath) => readStatsRows(sourcePath, {}));
186
+ // Fresh-install guard: when no source file exists or all sources are empty,
187
+ // there is no telemetry to import. Returning early prevents writing a
188
+ // header-only destination file that would masquerade as valid stats.
189
+ if (sourceRows.length === 0) {
190
+ return { destinationPath, imported: 0, rows: 0, warn: 'no source data available' };
191
+ }
192
+ const destinationRows = readStatsRows(destinationPath, {});
193
+ const rows = [];
194
+ const seen = new Set();
195
+ for (const row of [...destinationRows, ...sourceRows]) {
196
+ const key = JSON.stringify(row);
197
+ if (!seen.has(key)) {
198
+ seen.add(key);
199
+ rows.push(row);
200
+ }
201
+ }
202
+ const content = serializeStatsRows(rows);
203
+ const current = node_fs_1.default.existsSync(destinationPath) ? node_fs_1.default.readFileSync(destinationPath, 'utf8') : null;
204
+ if (current !== content) {
205
+ storage.writeFileAtomic(destinationPath, content);
206
+ }
207
+ return { destinationPath, imported: rows.length - destinationRows.length, rows: rows.length };
156
208
  }
157
-
158
- /** @param {string} filePath @param {Function} warn @param {boolean} [hardFailure] */
159
209
  function readBlocklistSource(filePath, warn, hardFailure = false) {
160
- if (!filePath || !fs.existsSync(filePath)) {return null;}
161
- try {
162
- const payload = JSON.parse(fs.readFileSync(filePath, 'utf8'));
163
- if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
164
- throw new Error('expected a JSON object at the file root');
165
- }
166
- if (
167
- payload.blocklist !== undefined &&
168
- (!payload.blocklist || typeof payload.blocklist !== 'object' || Array.isArray(payload.blocklist))
169
- ) {
170
- throw new Error('expected blocklist to be a JSON object');
171
- }
172
- return { filePath, payload, blocklist: payload.blocklist || {} };
173
- } catch (error) {
174
- if (hardFailure) {throw error;}
175
- /** @type {Error} */
176
- const e = /** @type {Error} */(error);
177
- warn(`Skipping malformed legacy agent blocklist ${path.resolve(filePath)}: ${e.message}`);
178
- return null;
179
- }
210
+ if (!filePath || !node_fs_1.default.existsSync(filePath)) {
211
+ return null;
212
+ }
213
+ try {
214
+ const payload = JSON.parse(node_fs_1.default.readFileSync(filePath, 'utf8'));
215
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
216
+ throw new Error('expected a JSON object at the file root');
217
+ }
218
+ if (payload.blocklist !== undefined &&
219
+ (!payload.blocklist || typeof payload.blocklist !== 'object' || Array.isArray(payload.blocklist))) {
220
+ throw new Error('expected blocklist to be a JSON object');
221
+ }
222
+ return { filePath, payload, blocklist: payload.blocklist || {} };
223
+ }
224
+ catch (error) {
225
+ if (hardFailure) {
226
+ throw error;
227
+ }
228
+ warn(`Skipping malformed legacy agent blocklist ${node_path_1.default.resolve(filePath)}: ${error.message}`);
229
+ return null;
230
+ }
180
231
  }
181
-
182
- /** @param {unknown} left @param {unknown} right */
183
232
  function sameValue(left, right) {
184
- return JSON.stringify(left) === JSON.stringify(right);
233
+ return JSON.stringify(left) === JSON.stringify(right);
185
234
  }
186
-
187
- /** @param {{warn?: Function, destinationPath?: string, sourcePaths?: string[]}} [options] */
235
+ /**
236
+ * Merge agent blocklists from legacy sources into the destination file.
237
+ * Reports conflicts via `warn` when the same agent appears in multiple sources
238
+ * with different values. Existing destination entries take lowest precedence.
239
+ * @param options - Migration configuration (warn callback, destinationPath, sourcePaths)
240
+ * @returns Result with destination path, merged blocklist, and conflict details
241
+ */
188
242
  function migrateAgentBlocklists(options = {}) {
189
- /** @type {{warn?: Function, destinationPath?: string, sourcePaths?: string[]}} */
190
- const opts = options;
191
- const warn = opts.warn || (() => {});
192
- const destinationPath = opts.destinationPath || storage.resolveAgentsLocalPath({ ensureDir: true });
193
- /** @param {string} filePath */
194
- const sources = (opts.sourcePaths || [])
195
- .map(filePath => readBlocklistSource(filePath, warn))
196
- .filter(Boolean);
197
- const destination = readBlocklistSource(destinationPath, warn, true);
198
- /** @type {Record<string, unknown>} */
199
- const selected = {};
200
- /** @type {Record<string, string>} */
201
- const selectedFrom = {};
202
- const conflicts = [];
203
-
204
- for (const source of [...sources, ...(destination ? [destination] : [])]) {
205
- if (!source) {continue;}
206
- /** @type {{filePath: string, blocklist: Record<string,unknown>}} */
207
- const s = source;
208
- for (const [agent, value] of Object.entries(s.blocklist)) {
209
- if (Object.prototype.hasOwnProperty.call(selected, agent) && !sameValue(selected[agent], value)) {
210
- const conflict = {
211
- agent,
212
- previousSource: selectedFrom[agent],
213
- previousValue: selected[agent],
214
- selectedSource: s.filePath,
215
- selectedValue: value
216
- };
217
- conflicts.push(conflict);
218
- warn(
219
- `Agent blocklist conflict for "${agent}": ${path.resolve(s.filePath)} takes precedence over ` +
220
- `${path.resolve(selectedFrom[agent])}; selected=${JSON.stringify(value)} previous=${JSON.stringify(selected[agent])}`
221
- );
222
- }
223
- selected[agent] = value;
224
- selectedFrom[agent] = s.filePath;
225
- }
226
- }
227
-
228
- /** @type{Record<string,unknown>} */
229
- const payload = destination ? { ...destination.payload } : {};
230
- payload.blocklist = selected;
231
- const content = `${JSON.stringify(payload, null, 2)}\n`;
232
- const current = fs.existsSync(destinationPath) ? fs.readFileSync(destinationPath, 'utf8') : null;
233
- if (current !== content) {storage.writeFileAtomic(destinationPath, content);}
234
- return { destinationPath, blocklist: selected, conflicts };
243
+ const opts = options;
244
+ const warn = opts.warn || (() => { });
245
+ const destinationPath = opts.destinationPath || storage.resolveAgentsLocalPath({ ensureDir: true });
246
+ const sources = (opts.sourcePaths || [])
247
+ .map(filePath => readBlocklistSource(filePath, warn))
248
+ .filter(Boolean);
249
+ const destination = readBlocklistSource(destinationPath, warn, true);
250
+ const selected = {};
251
+ const selectedFrom = {};
252
+ const conflicts = [];
253
+ for (const s of [...sources, ...(destination ? [destination] : [])]) {
254
+ if (!s) {
255
+ continue;
256
+ }
257
+ for (const [agent, value] of Object.entries(s.blocklist)) {
258
+ if (Object.prototype.hasOwnProperty.call(selected, agent) && !sameValue(selected[agent], value)) {
259
+ const conflict = {
260
+ agent,
261
+ previousSource: selectedFrom[agent],
262
+ previousValue: selected[agent],
263
+ selectedSource: s.filePath,
264
+ selectedValue: value
265
+ };
266
+ conflicts.push(conflict);
267
+ warn(`Agent blocklist conflict for "${agent}": ${node_path_1.default.resolve(s.filePath)} takes precedence over ` +
268
+ `${node_path_1.default.resolve(selectedFrom[agent])}; selected=${JSON.stringify(value)} previous=${JSON.stringify(selected[agent])}`);
269
+ }
270
+ selected[agent] = value;
271
+ selectedFrom[agent] = s.filePath;
272
+ }
273
+ }
274
+ const payload = destination ? { ...destination.payload } : {};
275
+ payload.blocklist = selected;
276
+ const content = `${JSON.stringify(payload, null, 2)}\n`;
277
+ const current = node_fs_1.default.existsSync(destinationPath) ? node_fs_1.default.readFileSync(destinationPath, 'utf8') : null;
278
+ if (current !== content) {
279
+ storage.writeFileAtomic(destinationPath, content);
280
+ }
281
+ return { destinationPath, blocklist: selected, conflicts };
235
282
  }
236
-
237
- module.exports = {
238
- migrateStats,
239
- migrateAgentBlocklists,
240
- _internals: { parseCsvLine, readStatsRows, serializeStatsRows }
241
- };
283
+ exports._internals = { parseCsvLine, readStatsRows, serializeStatsRows };