@aspruyt/xfg 3.3.2 → 3.4.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.
- package/dist/index.js +40 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +23 -0
- package/dist/ruleset-plan-formatter.d.ts +27 -0
- package/dist/ruleset-plan-formatter.js +314 -0
- package/dist/ruleset-processor.d.ts +2 -0
- package/dist/ruleset-processor.js +3 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { program, Command } from "commander";
|
|
|
3
3
|
import { resolve, join, dirname } from "node:path";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import chalk from "chalk";
|
|
6
7
|
import { loadRawConfig, normalizeConfig, } from "./config.js";
|
|
7
8
|
import { validateForSync, validateForSettings } from "./config-validator.js";
|
|
8
9
|
// Get version from package.json
|
|
@@ -215,6 +216,11 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
215
216
|
let successCount = 0;
|
|
216
217
|
let failCount = 0;
|
|
217
218
|
let skipCount = 0;
|
|
219
|
+
// Tracking for multi-repo summary in dry-run mode
|
|
220
|
+
let totalCreates = 0;
|
|
221
|
+
let totalUpdates = 0;
|
|
222
|
+
let totalDeletes = 0;
|
|
223
|
+
let reposWithChanges = 0;
|
|
218
224
|
for (let i = 0; i < reposWithRulesets.length; i++) {
|
|
219
225
|
const repoConfig = reposWithRulesets[i];
|
|
220
226
|
let repoInfo;
|
|
@@ -248,6 +254,27 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
248
254
|
managedRulesets,
|
|
249
255
|
noDelete: options.noDelete,
|
|
250
256
|
});
|
|
257
|
+
// Display plan output for dry-run mode
|
|
258
|
+
if (options.dryRun && result.planOutput) {
|
|
259
|
+
if (result.planOutput.lines.length > 0) {
|
|
260
|
+
logger.rulesetPlan(repoName, result.planOutput.lines, {
|
|
261
|
+
creates: result.planOutput.creates,
|
|
262
|
+
updates: result.planOutput.updates,
|
|
263
|
+
deletes: result.planOutput.deletes,
|
|
264
|
+
unchanged: result.planOutput.unchanged,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// Accumulate totals for multi-repo summary
|
|
268
|
+
totalCreates += result.planOutput.creates;
|
|
269
|
+
totalUpdates += result.planOutput.updates;
|
|
270
|
+
totalDeletes += result.planOutput.deletes;
|
|
271
|
+
if (result.planOutput.creates +
|
|
272
|
+
result.planOutput.updates +
|
|
273
|
+
result.planOutput.deletes >
|
|
274
|
+
0) {
|
|
275
|
+
reposWithChanges++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
251
278
|
if (result.skipped) {
|
|
252
279
|
logger.skip(i + 1, repoName, result.message);
|
|
253
280
|
skipCount++;
|
|
@@ -292,6 +319,19 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
292
319
|
failCount++;
|
|
293
320
|
}
|
|
294
321
|
}
|
|
322
|
+
// Multi-repo summary for dry-run mode
|
|
323
|
+
if (options.dryRun && reposWithChanges > 0) {
|
|
324
|
+
console.log("");
|
|
325
|
+
console.log(chalk.gray("─".repeat(40)));
|
|
326
|
+
const totalParts = [];
|
|
327
|
+
if (totalCreates > 0)
|
|
328
|
+
totalParts.push(chalk.green(`${totalCreates} to create`));
|
|
329
|
+
if (totalUpdates > 0)
|
|
330
|
+
totalParts.push(chalk.yellow(`${totalUpdates} to update`));
|
|
331
|
+
if (totalDeletes > 0)
|
|
332
|
+
totalParts.push(chalk.red(`${totalDeletes} to delete`));
|
|
333
|
+
console.log(chalk.bold(`Total: ${totalParts.join(", ")} across ${reposWithChanges} ${reposWithChanges === 1 ? "repository" : "repositories"}`));
|
|
334
|
+
}
|
|
295
335
|
// Summary
|
|
296
336
|
console.log("\n" + "=".repeat(50));
|
|
297
337
|
console.log(`Completed: ${successCount} succeeded, ${skipCount} skipped, ${failCount} failed`);
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { FileStatus } from "./diff-utils.js";
|
|
2
|
+
export interface RulesetPlanCounts {
|
|
3
|
+
creates: number;
|
|
4
|
+
updates: number;
|
|
5
|
+
deletes: number;
|
|
6
|
+
unchanged: number;
|
|
7
|
+
}
|
|
2
8
|
export interface ILogger {
|
|
3
9
|
info(message: string): void;
|
|
4
10
|
fileDiff(fileName: string, status: FileStatus, diffLines: string[]): void;
|
|
5
11
|
diffSummary(newCount: number, modifiedCount: number, unchangedCount: number, deletedCount?: number): void;
|
|
12
|
+
rulesetPlan(repoName: string, planLines: string[], counts: RulesetPlanCounts): void;
|
|
6
13
|
setTotal(total: number): void;
|
|
7
14
|
progress(current: number, repoName: string, message: string): void;
|
|
8
15
|
success(current: number, repoName: string, message: string): void;
|
|
@@ -34,6 +41,10 @@ export declare class Logger implements ILogger {
|
|
|
34
41
|
* Display summary statistics for dry-run diff.
|
|
35
42
|
*/
|
|
36
43
|
diffSummary(newCount: number, modifiedCount: number, unchangedCount: number, deletedCount?: number): void;
|
|
44
|
+
/**
|
|
45
|
+
* Display ruleset plan output for dry-run mode.
|
|
46
|
+
*/
|
|
47
|
+
rulesetPlan(repoName: string, planLines: string[], counts: RulesetPlanCounts): void;
|
|
37
48
|
summary(): void;
|
|
38
49
|
hasFailures(): boolean;
|
|
39
50
|
}
|
package/dist/logger.js
CHANGED
|
@@ -62,6 +62,29 @@ export class Logger {
|
|
|
62
62
|
console.log(chalk.gray(` Summary: ${parts.join(", ")}`));
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Display ruleset plan output for dry-run mode.
|
|
67
|
+
*/
|
|
68
|
+
rulesetPlan(repoName, planLines, counts) {
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log(chalk.bold(`Repository: ${repoName}`));
|
|
71
|
+
for (const line of planLines) {
|
|
72
|
+
console.log(line);
|
|
73
|
+
}
|
|
74
|
+
// Summary line
|
|
75
|
+
const parts = [];
|
|
76
|
+
if (counts.creates > 0)
|
|
77
|
+
parts.push(chalk.green(`${counts.creates} to create`));
|
|
78
|
+
if (counts.updates > 0)
|
|
79
|
+
parts.push(chalk.yellow(`${counts.updates} to update`));
|
|
80
|
+
if (counts.deletes > 0)
|
|
81
|
+
parts.push(chalk.red(`${counts.deletes} to delete`));
|
|
82
|
+
const unchangedPart = counts.unchanged > 0
|
|
83
|
+
? chalk.gray(` (${counts.unchanged} unchanged)`)
|
|
84
|
+
: "";
|
|
85
|
+
const summaryLine = parts.length > 0 ? parts.join(", ") + unchangedPart : "No changes";
|
|
86
|
+
console.log(chalk.gray(`Plan: ${summaryLine}`));
|
|
87
|
+
}
|
|
65
88
|
summary() {
|
|
66
89
|
console.log("");
|
|
67
90
|
console.log(chalk.bold("Summary:"));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RulesetChange } from "./ruleset-diff.js";
|
|
2
|
+
export type DiffAction = "add" | "change" | "remove";
|
|
3
|
+
export interface PropertyDiff {
|
|
4
|
+
path: string[];
|
|
5
|
+
action: DiffAction;
|
|
6
|
+
oldValue?: unknown;
|
|
7
|
+
newValue?: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface RulesetPlanResult {
|
|
10
|
+
lines: string[];
|
|
11
|
+
creates: number;
|
|
12
|
+
updates: number;
|
|
13
|
+
deletes: number;
|
|
14
|
+
unchanged: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Recursively compute property-level diffs between two objects.
|
|
18
|
+
*/
|
|
19
|
+
export declare function computePropertyDiffs(current: Record<string, unknown>, desired: Record<string, unknown>, parentPath?: string[]): PropertyDiff[];
|
|
20
|
+
/**
|
|
21
|
+
* Format property diffs as an indented tree structure.
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatPropertyTree(diffs: PropertyDiff[]): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Format ruleset changes as a Terraform-style plan.
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatRulesetPlan(changes: RulesetChange[]): RulesetPlanResult;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// src/ruleset-plan-formatter.ts
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Property Diff Algorithm
|
|
5
|
+
// =============================================================================
|
|
6
|
+
/**
|
|
7
|
+
* Recursively compute property-level diffs between two objects.
|
|
8
|
+
*/
|
|
9
|
+
export function computePropertyDiffs(current, desired, parentPath = []) {
|
|
10
|
+
const diffs = [];
|
|
11
|
+
const allKeys = new Set([...Object.keys(current), ...Object.keys(desired)]);
|
|
12
|
+
for (const key of allKeys) {
|
|
13
|
+
const path = [...parentPath, key];
|
|
14
|
+
const currentVal = current[key];
|
|
15
|
+
const desiredVal = desired[key];
|
|
16
|
+
if (!(key in current)) {
|
|
17
|
+
// Added property
|
|
18
|
+
diffs.push({ path, action: "add", newValue: desiredVal });
|
|
19
|
+
}
|
|
20
|
+
else if (!(key in desired)) {
|
|
21
|
+
// Removed property
|
|
22
|
+
diffs.push({ path, action: "remove", oldValue: currentVal });
|
|
23
|
+
}
|
|
24
|
+
else if (!deepEqual(currentVal, desiredVal)) {
|
|
25
|
+
// Changed property
|
|
26
|
+
if (isObject(currentVal) && isObject(desiredVal)) {
|
|
27
|
+
// Recurse into nested objects
|
|
28
|
+
diffs.push(...computePropertyDiffs(currentVal, desiredVal, path));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
diffs.push({
|
|
32
|
+
path,
|
|
33
|
+
action: "change",
|
|
34
|
+
oldValue: currentVal,
|
|
35
|
+
newValue: desiredVal,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Unchanged properties are not included
|
|
40
|
+
}
|
|
41
|
+
return diffs;
|
|
42
|
+
}
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Helpers
|
|
45
|
+
// =============================================================================
|
|
46
|
+
function isObject(val) {
|
|
47
|
+
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
48
|
+
}
|
|
49
|
+
function deepEqual(a, b) {
|
|
50
|
+
if (a === b)
|
|
51
|
+
return true;
|
|
52
|
+
if (a === null || b === null || a === undefined || b === undefined)
|
|
53
|
+
return a === b;
|
|
54
|
+
if (typeof a !== typeof b)
|
|
55
|
+
return false;
|
|
56
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
57
|
+
if (a.length !== b.length)
|
|
58
|
+
return false;
|
|
59
|
+
return a.every((val, i) => deepEqual(val, b[i]));
|
|
60
|
+
}
|
|
61
|
+
if (isObject(a) && isObject(b)) {
|
|
62
|
+
const keysA = Object.keys(a);
|
|
63
|
+
const keysB = Object.keys(b);
|
|
64
|
+
if (keysA.length !== keysB.length)
|
|
65
|
+
return false;
|
|
66
|
+
return keysA.every((key) => deepEqual(a[key], b[key]));
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build a tree structure from flat property diffs.
|
|
72
|
+
*/
|
|
73
|
+
function buildTree(diffs) {
|
|
74
|
+
const root = { name: "", children: new Map() };
|
|
75
|
+
for (const diff of diffs) {
|
|
76
|
+
let current = root;
|
|
77
|
+
for (let i = 0; i < diff.path.length; i++) {
|
|
78
|
+
const segment = diff.path[i];
|
|
79
|
+
const isLast = i === diff.path.length - 1;
|
|
80
|
+
if (!current.children.has(segment)) {
|
|
81
|
+
current.children.set(segment, {
|
|
82
|
+
name: segment,
|
|
83
|
+
children: new Map(),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const child = current.children.get(segment);
|
|
87
|
+
if (isLast) {
|
|
88
|
+
child.action = diff.action;
|
|
89
|
+
child.oldValue = diff.oldValue;
|
|
90
|
+
child.newValue = diff.newValue;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Intermediate node - mark as change if any child changes
|
|
94
|
+
if (!child.action) {
|
|
95
|
+
child.action = "change";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
current = child;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return root;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Format a value for display.
|
|
105
|
+
*/
|
|
106
|
+
function formatValue(val) {
|
|
107
|
+
if (val === null)
|
|
108
|
+
return "null";
|
|
109
|
+
if (val === undefined)
|
|
110
|
+
return "undefined";
|
|
111
|
+
if (typeof val === "string")
|
|
112
|
+
return `"${val}"`;
|
|
113
|
+
if (Array.isArray(val)) {
|
|
114
|
+
if (val.length <= 3) {
|
|
115
|
+
return `[${val.map(formatValue).join(", ")}]`;
|
|
116
|
+
}
|
|
117
|
+
return `[${val.slice(0, 3).map(formatValue).join(", ")}, ... (${val.length - 3} more)]`;
|
|
118
|
+
}
|
|
119
|
+
if (typeof val === "object") {
|
|
120
|
+
return "{...}";
|
|
121
|
+
}
|
|
122
|
+
return String(val);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the symbol and color for an action.
|
|
126
|
+
*/
|
|
127
|
+
function getActionStyle(action) {
|
|
128
|
+
switch (action) {
|
|
129
|
+
case "add":
|
|
130
|
+
return { symbol: "+", color: chalk.green };
|
|
131
|
+
case "remove":
|
|
132
|
+
return { symbol: "-", color: chalk.red };
|
|
133
|
+
case "change":
|
|
134
|
+
return { symbol: "~", color: chalk.yellow };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Recursively render tree nodes to formatted lines.
|
|
139
|
+
*/
|
|
140
|
+
function renderTree(node, indent = 0) {
|
|
141
|
+
const lines = [];
|
|
142
|
+
const indentStr = " ".repeat(indent);
|
|
143
|
+
for (const [, child] of node.children) {
|
|
144
|
+
const style = child.action
|
|
145
|
+
? getActionStyle(child.action)
|
|
146
|
+
: { symbol: " ", color: chalk.gray };
|
|
147
|
+
const hasChildren = child.children.size > 0;
|
|
148
|
+
if (hasChildren) {
|
|
149
|
+
// Intermediate node
|
|
150
|
+
lines.push(style.color(`${indentStr}${style.symbol} ${child.name}:`));
|
|
151
|
+
lines.push(...renderTree(child, indent + 1));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Leaf node with value
|
|
155
|
+
let valuePart = "";
|
|
156
|
+
if (child.action === "change") {
|
|
157
|
+
valuePart = `: ${formatValue(child.oldValue)} → ${formatValue(child.newValue)}`;
|
|
158
|
+
}
|
|
159
|
+
else if (child.action === "add") {
|
|
160
|
+
valuePart = `: ${formatValue(child.newValue)}`;
|
|
161
|
+
}
|
|
162
|
+
else if (child.action === "remove") {
|
|
163
|
+
valuePart = ` (was: ${formatValue(child.oldValue)})`;
|
|
164
|
+
}
|
|
165
|
+
lines.push(style.color(`${indentStr}${style.symbol} ${child.name}${valuePart}`));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return lines;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Format property diffs as an indented tree structure.
|
|
172
|
+
*/
|
|
173
|
+
export function formatPropertyTree(diffs) {
|
|
174
|
+
if (diffs.length === 0) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
const tree = buildTree(diffs);
|
|
178
|
+
return renderTree(tree);
|
|
179
|
+
}
|
|
180
|
+
// =============================================================================
|
|
181
|
+
// Ruleset Plan Formatter
|
|
182
|
+
// =============================================================================
|
|
183
|
+
/**
|
|
184
|
+
* Normalize a GitHubRuleset or Ruleset for comparison.
|
|
185
|
+
* Converts to snake_case and removes metadata fields.
|
|
186
|
+
*/
|
|
187
|
+
function normalizeForDiff(obj) {
|
|
188
|
+
const result = {};
|
|
189
|
+
const ignoreFields = new Set(["id", "name", "source_type", "source"]);
|
|
190
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
191
|
+
if (ignoreFields.has(key) || value === undefined)
|
|
192
|
+
continue;
|
|
193
|
+
// Convert camelCase to snake_case for consistency
|
|
194
|
+
const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
195
|
+
result[snakeKey] = normalizeNestedValue(value);
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
function normalizeNestedValue(value) {
|
|
200
|
+
if (value === null || value === undefined)
|
|
201
|
+
return value;
|
|
202
|
+
if (Array.isArray(value))
|
|
203
|
+
return value.map(normalizeNestedValue);
|
|
204
|
+
if (typeof value === "object") {
|
|
205
|
+
const obj = value;
|
|
206
|
+
const result = {};
|
|
207
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
208
|
+
const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
209
|
+
result[snakeKey] = normalizeNestedValue(val);
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Format a full ruleset config as tree lines (for create action).
|
|
217
|
+
*/
|
|
218
|
+
function formatFullConfig(ruleset, indent = 2) {
|
|
219
|
+
const lines = [];
|
|
220
|
+
const style = getActionStyle("add");
|
|
221
|
+
function renderValue(key, value, currentIndent) {
|
|
222
|
+
const pad = " ".repeat(currentIndent);
|
|
223
|
+
if (value === null || value === undefined)
|
|
224
|
+
return;
|
|
225
|
+
if (Array.isArray(value)) {
|
|
226
|
+
if (value.length === 0) {
|
|
227
|
+
lines.push(style.color(`${pad}+ ${key}: []`));
|
|
228
|
+
}
|
|
229
|
+
else if (value.every((v) => typeof v !== "object")) {
|
|
230
|
+
lines.push(style.color(`${pad}+ ${key}: ${formatValue(value)}`));
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
lines.push(style.color(`${pad}+ ${key}:`));
|
|
234
|
+
for (const item of value) {
|
|
235
|
+
if (typeof item === "object" && item !== null) {
|
|
236
|
+
lines.push(style.color(`${pad} - ${JSON.stringify(item)}`));
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
lines.push(style.color(`${pad} - ${formatValue(item)}`));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else if (typeof value === "object") {
|
|
245
|
+
lines.push(style.color(`${pad}+ ${key}:`));
|
|
246
|
+
for (const [k, v] of Object.entries(value)) {
|
|
247
|
+
renderValue(k, v, currentIndent + 1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
lines.push(style.color(`${pad}+ ${key}: ${formatValue(value)}`));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const [key, value] of Object.entries(ruleset)) {
|
|
255
|
+
renderValue(key, value, indent);
|
|
256
|
+
}
|
|
257
|
+
return lines;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Format ruleset changes as a Terraform-style plan.
|
|
261
|
+
*/
|
|
262
|
+
export function formatRulesetPlan(changes) {
|
|
263
|
+
const lines = [];
|
|
264
|
+
let creates = 0;
|
|
265
|
+
let updates = 0;
|
|
266
|
+
let deletes = 0;
|
|
267
|
+
let unchanged = 0;
|
|
268
|
+
// Group by action type
|
|
269
|
+
const createChanges = changes.filter((c) => c.action === "create");
|
|
270
|
+
const updateChanges = changes.filter((c) => c.action === "update");
|
|
271
|
+
const deleteChanges = changes.filter((c) => c.action === "delete");
|
|
272
|
+
const unchangedChanges = changes.filter((c) => c.action === "unchanged");
|
|
273
|
+
creates = createChanges.length;
|
|
274
|
+
updates = updateChanges.length;
|
|
275
|
+
deletes = deleteChanges.length;
|
|
276
|
+
unchanged = unchangedChanges.length;
|
|
277
|
+
// Format creates
|
|
278
|
+
if (createChanges.length > 0) {
|
|
279
|
+
lines.push(chalk.bold(" Create:"));
|
|
280
|
+
for (const change of createChanges) {
|
|
281
|
+
lines.push(chalk.green(` + ruleset "${change.name}"`));
|
|
282
|
+
if (change.desired) {
|
|
283
|
+
lines.push(...formatFullConfig(change.desired, 2));
|
|
284
|
+
}
|
|
285
|
+
lines.push(""); // Blank line between rulesets
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Format updates
|
|
289
|
+
if (updateChanges.length > 0) {
|
|
290
|
+
lines.push(chalk.bold(" Update:"));
|
|
291
|
+
for (const change of updateChanges) {
|
|
292
|
+
lines.push(chalk.yellow(` ~ ruleset "${change.name}"`));
|
|
293
|
+
if (change.current && change.desired) {
|
|
294
|
+
const currentNorm = normalizeForDiff(change.current);
|
|
295
|
+
const desiredNorm = normalizeForDiff(change.desired);
|
|
296
|
+
const diffs = computePropertyDiffs(currentNorm, desiredNorm);
|
|
297
|
+
const treeLines = formatPropertyTree(diffs);
|
|
298
|
+
for (const line of treeLines) {
|
|
299
|
+
lines.push(` ${line}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
lines.push(""); // Blank line between rulesets
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Format deletes
|
|
306
|
+
if (deleteChanges.length > 0) {
|
|
307
|
+
lines.push(chalk.bold(" Delete:"));
|
|
308
|
+
for (const change of deleteChanges) {
|
|
309
|
+
lines.push(chalk.red(` - ruleset "${change.name}"`));
|
|
310
|
+
}
|
|
311
|
+
lines.push(""); // Blank line after deletes
|
|
312
|
+
}
|
|
313
|
+
return { lines, creates, updates, deletes, unchanged };
|
|
314
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RepoConfig } from "./config.js";
|
|
2
2
|
import type { RepoInfo } from "./repo-detector.js";
|
|
3
3
|
import { GitHubRulesetStrategy } from "./strategies/github-ruleset-strategy.js";
|
|
4
|
+
import { RulesetPlanResult } from "./ruleset-plan-formatter.js";
|
|
4
5
|
export interface IRulesetProcessor {
|
|
5
6
|
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RulesetProcessorOptions): Promise<RulesetProcessorResult>;
|
|
6
7
|
}
|
|
@@ -26,6 +27,7 @@ export interface RulesetProcessorResult {
|
|
|
26
27
|
manifestUpdate?: {
|
|
27
28
|
rulesets: string[];
|
|
28
29
|
};
|
|
30
|
+
planOutput?: RulesetPlanResult;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* Processes ruleset configuration for a repository.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isGitHubRepo, getRepoDisplayName } from "./repo-detector.js";
|
|
2
2
|
import { GitHubRulesetStrategy } from "./strategies/github-ruleset-strategy.js";
|
|
3
3
|
import { diffRulesets } from "./ruleset-diff.js";
|
|
4
|
+
import { formatRulesetPlan, } from "./ruleset-plan-formatter.js";
|
|
4
5
|
// =============================================================================
|
|
5
6
|
// Processor Implementation
|
|
6
7
|
// =============================================================================
|
|
@@ -60,12 +61,14 @@ export class RulesetProcessor {
|
|
|
60
61
|
// Dry run mode - report planned changes without applying
|
|
61
62
|
if (dryRun) {
|
|
62
63
|
const summary = this.formatChangeSummary(changeCounts);
|
|
64
|
+
const planOutput = formatRulesetPlan(changes);
|
|
63
65
|
return {
|
|
64
66
|
success: true,
|
|
65
67
|
repoName,
|
|
66
68
|
message: `[DRY RUN] ${summary}`,
|
|
67
69
|
dryRun: true,
|
|
68
70
|
changes: changeCounts,
|
|
71
|
+
planOutput,
|
|
69
72
|
manifestUpdate: this.computeManifestUpdate(desiredRulesets, deleteOrphaned),
|
|
70
73
|
};
|
|
71
74
|
}
|