@aspruyt/xfg 3.7.7 → 3.8.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.
@@ -8,52 +8,43 @@ import { logger } from "../shared/logger.js";
8
8
  import { generateWorkspaceName } from "../shared/workspace-utils.js";
9
9
  import { buildErrorResult } from "../output/summary-utils.js";
10
10
  import { getManagedRulesets } from "../sync/manifest.js";
11
- import { printPlan } from "../output/plan-formatter.js";
12
- import { writePlanSummary } from "../output/plan-summary.js";
13
- import { rulesetResultToResources, repoSettingsResultToResources, } from "../settings/resource-converters.js";
11
+ import { formatSettingsReportCLI, writeSettingsReportSummary, } from "../output/settings-report.js";
12
+ import { buildSettingsReport, } from "./settings-report-builder.js";
14
13
  import { defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, } from "./types.js";
15
14
  /**
16
- * Run the settings command - manages GitHub Rulesets and repo settings.
15
+ * Collects processing results for the SettingsReport.
16
+ * Provides a centralized way to track results across rulesets and repo settings.
17
17
  */
18
- export async function runSettings(options, processorFactory = defaultRulesetProcessorFactory, repoProcessorFactory = defaultProcessorFactory, repoSettingsProcessorFactory = defaultRepoSettingsProcessorFactory) {
19
- const configPath = resolve(options.config);
20
- if (!existsSync(configPath)) {
21
- console.error(`Config file not found: ${configPath}`);
22
- process.exit(1);
23
- }
24
- console.log(`Loading config from: ${configPath}`);
25
- if (options.dryRun) {
26
- console.log("Running in DRY RUN mode - no changes will be made\n");
27
- }
28
- const rawConfig = loadRawConfig(configPath);
29
- try {
30
- validateForSettings(rawConfig);
31
- }
32
- catch (error) {
33
- console.error(error instanceof Error ? error.message : String(error));
34
- process.exit(1);
35
- }
36
- const config = normalizeConfig(rawConfig);
37
- const reposWithRulesets = config.repos.filter((r) => r.settings?.rulesets && Object.keys(r.settings.rulesets).length > 0);
38
- const reposWithRepoSettings = config.repos.filter((r) => r.settings?.repo && Object.keys(r.settings.repo).length > 0);
39
- if (reposWithRulesets.length === 0 && reposWithRepoSettings.length === 0) {
40
- console.log("No settings configured. Add settings.rulesets or settings.repo to your config.");
41
- return;
18
+ class ResultsCollector {
19
+ results = [];
20
+ getOrCreate(repoName) {
21
+ let result = this.results.find((r) => r.repoName === repoName);
22
+ if (!result) {
23
+ result = { repoName };
24
+ this.results.push(result);
25
+ }
26
+ return result;
42
27
  }
43
- if (reposWithRulesets.length > 0) {
44
- console.log(`Found ${reposWithRulesets.length} repositories with rulesets`);
28
+ appendError(repoName, error) {
29
+ const existing = this.getOrCreate(repoName);
30
+ const errorMsg = error instanceof Error ? error.message : String(error);
31
+ if (existing.error) {
32
+ existing.error += `; ${errorMsg}`;
33
+ }
34
+ else {
35
+ existing.error = errorMsg;
36
+ }
45
37
  }
46
- if (reposWithRepoSettings.length > 0) {
47
- console.log(`Found ${reposWithRepoSettings.length} repositories with repo settings`);
38
+ getAll() {
39
+ return this.results;
48
40
  }
49
- console.log("");
50
- logger.setTotal(reposWithRulesets.length + reposWithRepoSettings.length);
51
- const processor = processorFactory();
52
- const repoProcessor = repoProcessorFactory();
53
- const results = [];
54
- const plan = { resources: [], errors: [] };
55
- for (let i = 0; i < reposWithRulesets.length; i++) {
56
- const repoConfig = reposWithRulesets[i];
41
+ }
42
+ /**
43
+ * Process rulesets for all configured repositories.
44
+ */
45
+ async function processRulesets(repos, config, options, processor, repoProcessor, results, collector) {
46
+ for (let i = 0; i < repos.length; i++) {
47
+ const repoConfig = repos[i];
57
48
  let repoInfo;
58
49
  try {
59
50
  repoInfo = parseGitUrl(repoConfig.git, {
@@ -63,26 +54,12 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
63
54
  catch (error) {
64
55
  logger.error(i + 1, repoConfig.git, String(error));
65
56
  results.push(buildErrorResult(repoConfig.git, error));
66
- plan.errors.push({
67
- repo: repoConfig.git,
68
- message: error instanceof Error ? error.message : String(error),
69
- });
57
+ collector.appendError(repoConfig.git, error);
70
58
  continue;
71
59
  }
72
60
  const repoName = getRepoDisplayName(repoInfo);
73
61
  if (!isGitHubRepo(repoInfo)) {
74
62
  logger.skip(i + 1, repoName, "GitHub Rulesets only supported for GitHub repos");
75
- if (repoConfig.settings?.rulesets) {
76
- for (const rulesetName of Object.keys(repoConfig.settings.rulesets)) {
77
- plan.resources.push({
78
- type: "ruleset",
79
- repo: repoName,
80
- name: rulesetName,
81
- action: "skipped",
82
- skipReason: "GitHub Rulesets only supported for GitHub repos",
83
- });
84
- }
85
- }
86
63
  continue;
87
64
  }
88
65
  const managedRulesets = getManagedRulesets(null, config.id);
@@ -135,94 +112,140 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
135
112
  message: result.message,
136
113
  rulesetPlanDetails: result.planOutput?.entries,
137
114
  });
138
- plan.resources.push(...rulesetResultToResources(repoName, result));
115
+ if (!result.skipped) {
116
+ collector.getOrCreate(repoName).rulesetResult = result;
117
+ }
139
118
  }
140
119
  catch (error) {
141
120
  logger.error(i + 1, repoName, String(error));
142
121
  results.push(buildErrorResult(repoName, error));
143
- plan.errors.push({
144
- repo: repoName,
145
- message: error instanceof Error ? error.message : String(error),
146
- });
122
+ collector.appendError(repoName, error);
147
123
  }
148
124
  }
149
- if (reposWithRepoSettings.length > 0) {
150
- const repoSettingsProcessor = repoSettingsProcessorFactory();
151
- console.log(`\nProcessing repo settings for ${reposWithRepoSettings.length} repositories\n`);
152
- for (let i = 0; i < reposWithRepoSettings.length; i++) {
153
- const repoConfig = reposWithRepoSettings[i];
154
- let repoInfo;
155
- try {
156
- repoInfo = parseGitUrl(repoConfig.git, {
157
- githubHosts: config.githubHosts,
158
- });
159
- }
160
- catch (error) {
161
- console.error(`Failed to parse ${repoConfig.git}: ${error}`);
162
- plan.errors.push({
163
- repo: repoConfig.git,
164
- message: error instanceof Error ? error.message : String(error),
165
- });
166
- continue;
167
- }
168
- const repoName = getRepoDisplayName(repoInfo);
169
- try {
170
- const result = await repoSettingsProcessor.process(repoConfig, repoInfo, {
171
- dryRun: options.dryRun,
172
- });
173
- if (result.planOutput && result.planOutput.lines.length > 0) {
174
- console.log(`\n ${chalk.bold(repoName)}:`);
175
- console.log(" Repo Settings:");
176
- for (const line of result.planOutput.lines) {
177
- console.log(line);
178
- }
179
- if (result.warnings && result.warnings.length > 0) {
180
- for (const warning of result.warnings) {
181
- console.log(chalk.yellow(` ⚠️ Warning: ${warning}`));
182
- }
183
- }
125
+ }
126
+ /**
127
+ * Process repo settings for all configured repositories.
128
+ */
129
+ async function processRepoSettings(repos, config, options, processorFactory, results, collector) {
130
+ if (repos.length === 0) {
131
+ return;
132
+ }
133
+ const processor = processorFactory();
134
+ console.log(`\nProcessing repo settings for ${repos.length} repositories\n`);
135
+ for (let i = 0; i < repos.length; i++) {
136
+ const repoConfig = repos[i];
137
+ let repoInfo;
138
+ try {
139
+ repoInfo = parseGitUrl(repoConfig.git, {
140
+ githubHosts: config.githubHosts,
141
+ });
142
+ }
143
+ catch (error) {
144
+ console.error(`Failed to parse ${repoConfig.git}: ${error}`);
145
+ collector.appendError(repoConfig.git, error);
146
+ continue;
147
+ }
148
+ const repoName = getRepoDisplayName(repoInfo);
149
+ try {
150
+ const result = await processor.process(repoConfig, repoInfo, {
151
+ dryRun: options.dryRun,
152
+ });
153
+ if (result.planOutput && result.planOutput.lines.length > 0) {
154
+ console.log(`\n ${chalk.bold(repoName)}:`);
155
+ console.log(" Repo Settings:");
156
+ for (const line of result.planOutput.lines) {
157
+ console.log(line);
184
158
  }
185
- if (result.skipped) {
186
- // Silent skip
159
+ if (result.warnings && result.warnings.length > 0) {
160
+ for (const warning of result.warnings) {
161
+ console.log(chalk.yellow(` ⚠️ Warning: ${warning}`));
162
+ }
187
163
  }
188
- else if (result.success) {
189
- console.log(chalk.green(` ✓ ${repoName}: ${result.message}`));
164
+ }
165
+ if (result.skipped) {
166
+ // Silent skip
167
+ }
168
+ else if (result.success) {
169
+ console.log(chalk.green(` ✓ ${repoName}: ${result.message}`));
170
+ }
171
+ else {
172
+ console.log(chalk.red(` ✗ ${repoName}: ${result.message}`));
173
+ }
174
+ if (!result.skipped) {
175
+ const existing = results.find((r) => r.repoName === repoName);
176
+ if (existing) {
177
+ existing.repoSettingsPlanDetails = result.planOutput?.entries;
190
178
  }
191
179
  else {
192
- console.log(chalk.red(` ✗ ${repoName}: ${result.message}`));
193
- }
194
- if (!result.skipped) {
195
- const existing = results.find((r) => r.repoName === repoName);
196
- if (existing) {
197
- existing.repoSettingsPlanDetails = result.planOutput?.entries;
198
- }
199
- else {
200
- results.push({
201
- repoName,
202
- status: result.success ? "succeeded" : "failed",
203
- message: result.message,
204
- repoSettingsPlanDetails: result.planOutput?.entries,
205
- });
206
- }
180
+ results.push({
181
+ repoName,
182
+ status: result.success ? "succeeded" : "failed",
183
+ message: result.message,
184
+ repoSettingsPlanDetails: result.planOutput?.entries,
185
+ });
207
186
  }
208
- plan.resources.push(...repoSettingsResultToResources(repoName, result));
209
187
  }
210
- catch (error) {
211
- console.error(` ✗ ${repoName}: ${error}`);
212
- plan.errors.push({
213
- repo: repoName,
214
- message: error instanceof Error ? error.message : String(error),
215
- });
188
+ if (!result.skipped) {
189
+ collector.getOrCreate(repoName).settingsResult = result;
216
190
  }
217
191
  }
192
+ catch (error) {
193
+ console.error(` ✗ ${repoName}: ${error}`);
194
+ collector.appendError(repoName, error);
195
+ }
196
+ }
197
+ }
198
+ /**
199
+ * Run the settings command - manages GitHub Rulesets and repo settings.
200
+ */
201
+ export async function runSettings(options, processorFactory = defaultRulesetProcessorFactory, repoProcessorFactory = defaultProcessorFactory, repoSettingsProcessorFactory = defaultRepoSettingsProcessorFactory) {
202
+ const configPath = resolve(options.config);
203
+ if (!existsSync(configPath)) {
204
+ console.error(`Config file not found: ${configPath}`);
205
+ process.exit(1);
206
+ }
207
+ console.log(`Loading config from: ${configPath}`);
208
+ if (options.dryRun) {
209
+ console.log("Running in DRY RUN mode - no changes will be made\n");
210
+ }
211
+ const rawConfig = loadRawConfig(configPath);
212
+ try {
213
+ validateForSettings(rawConfig);
214
+ }
215
+ catch (error) {
216
+ console.error(error instanceof Error ? error.message : String(error));
217
+ process.exit(1);
218
+ }
219
+ const config = normalizeConfig(rawConfig);
220
+ const reposWithRulesets = config.repos.filter((r) => r.settings?.rulesets && Object.keys(r.settings.rulesets).length > 0);
221
+ const reposWithRepoSettings = config.repos.filter((r) => r.settings?.repo && Object.keys(r.settings.repo).length > 0);
222
+ if (reposWithRulesets.length === 0 && reposWithRepoSettings.length === 0) {
223
+ console.log("No settings configured. Add settings.rulesets or settings.repo to your config.");
224
+ return;
225
+ }
226
+ if (reposWithRulesets.length > 0) {
227
+ console.log(`Found ${reposWithRulesets.length} repositories with rulesets`);
228
+ }
229
+ if (reposWithRepoSettings.length > 0) {
230
+ console.log(`Found ${reposWithRepoSettings.length} repositories with repo settings`);
218
231
  }
219
232
  console.log("");
220
- printPlan(plan);
221
- writePlanSummary(plan, {
222
- title: "Repository Settings Summary",
223
- dryRun: options.dryRun ?? false,
224
- });
225
- if (plan.errors && plan.errors.length > 0) {
233
+ logger.setTotal(reposWithRulesets.length + reposWithRepoSettings.length);
234
+ const processor = processorFactory();
235
+ const repoProcessor = repoProcessorFactory();
236
+ const results = [];
237
+ const collector = new ResultsCollector();
238
+ await processRulesets(reposWithRulesets, config, options, processor, repoProcessor, results, collector);
239
+ await processRepoSettings(reposWithRepoSettings, config, options, repoSettingsProcessorFactory, results, collector);
240
+ console.log("");
241
+ const report = buildSettingsReport(collector.getAll());
242
+ const lines = formatSettingsReportCLI(report);
243
+ for (const line of lines) {
244
+ console.log(line);
245
+ }
246
+ writeSettingsReportSummary(report, options.dryRun ?? false);
247
+ const hasErrors = report.repos.some((r) => r.error);
248
+ if (hasErrors) {
226
249
  process.exit(1);
227
250
  }
228
251
  }
@@ -0,0 +1,22 @@
1
+ import type { SettingsReport } from "../output/settings-report.js";
2
+ import type { RepoSettingsPlanEntry } from "../settings/repo-settings/formatter.js";
3
+ import type { RulesetPlanEntry } from "../settings/rulesets/formatter.js";
4
+ /**
5
+ * Result from processing a repository's settings and rulesets.
6
+ * Used to collect results during settings command execution.
7
+ */
8
+ export interface ProcessorResults {
9
+ repoName: string;
10
+ settingsResult?: {
11
+ planOutput?: {
12
+ entries?: RepoSettingsPlanEntry[];
13
+ };
14
+ };
15
+ rulesetResult?: {
16
+ planOutput?: {
17
+ entries?: RulesetPlanEntry[];
18
+ };
19
+ };
20
+ error?: string;
21
+ }
22
+ export declare function buildSettingsReport(results: ProcessorResults[]): SettingsReport;
@@ -0,0 +1,64 @@
1
+ export function buildSettingsReport(results) {
2
+ const repos = [];
3
+ const totals = {
4
+ settings: { add: 0, change: 0 },
5
+ rulesets: { create: 0, update: 0, delete: 0 },
6
+ };
7
+ for (const result of results) {
8
+ const repoChanges = {
9
+ repoName: result.repoName,
10
+ settings: [],
11
+ rulesets: [],
12
+ };
13
+ // Convert settings processor output
14
+ if (result.settingsResult?.planOutput?.entries) {
15
+ for (const entry of result.settingsResult.planOutput.entries) {
16
+ // Skip settings where both values are undefined (no actual change)
17
+ if (entry.oldValue === undefined && entry.newValue === undefined) {
18
+ continue;
19
+ }
20
+ const settingChange = {
21
+ name: entry.property,
22
+ action: entry.action,
23
+ oldValue: entry.oldValue,
24
+ newValue: entry.newValue,
25
+ };
26
+ repoChanges.settings.push(settingChange);
27
+ if (entry.action === "add") {
28
+ totals.settings.add++;
29
+ }
30
+ else {
31
+ totals.settings.change++;
32
+ }
33
+ }
34
+ }
35
+ // Convert ruleset processor output
36
+ if (result.rulesetResult?.planOutput?.entries) {
37
+ for (const entry of result.rulesetResult.planOutput.entries) {
38
+ if (entry.action === "unchanged")
39
+ continue;
40
+ const rulesetChange = {
41
+ name: entry.name,
42
+ action: entry.action,
43
+ propertyDiffs: entry.propertyDiffs,
44
+ config: entry.config,
45
+ };
46
+ repoChanges.rulesets.push(rulesetChange);
47
+ if (entry.action === "create") {
48
+ totals.rulesets.create++;
49
+ }
50
+ else if (entry.action === "update") {
51
+ totals.rulesets.update++;
52
+ }
53
+ else if (entry.action === "delete") {
54
+ totals.rulesets.delete++;
55
+ }
56
+ }
57
+ }
58
+ if (result.error) {
59
+ repoChanges.error = result.error;
60
+ }
61
+ repos.push(repoChanges);
62
+ }
63
+ return { repos, totals };
64
+ }
@@ -6,11 +6,9 @@ import { parseGitUrl, getRepoDisplayName } from "../shared/repo-detector.js";
6
6
  import { sanitizeBranchName, validateBranchName } from "../vcs/git-ops.js";
7
7
  import { logger } from "../shared/logger.js";
8
8
  import { generateWorkspaceName } from "../shared/workspace-utils.js";
9
- import { buildRepoResult, buildErrorResult } from "../output/summary-utils.js";
10
- import { printPlan } from "../output/plan-formatter.js";
11
- import { writePlanSummary } from "../output/plan-summary.js";
12
- import { syncResultToResources } from "../settings/resource-converters.js";
13
9
  import { defaultProcessorFactory } from "./types.js";
10
+ import { buildSyncReport } from "./sync-report-builder.js";
11
+ import { formatSyncReportCLI, writeSyncReportSummary, } from "../output/sync-report.js";
14
12
  /**
15
13
  * Get unique file names from all repos in the config
16
14
  */
@@ -44,6 +42,20 @@ function formatFileNames(fileNames) {
44
42
  }
45
43
  return `${fileNames.length} files`;
46
44
  }
45
+ /**
46
+ * Determine merge outcome from processor result
47
+ */
48
+ function determineMergeOutcome(result) {
49
+ if (!result.success)
50
+ return undefined;
51
+ if (!result.prUrl)
52
+ return "direct";
53
+ if (result.mergeResult?.merged)
54
+ return "force";
55
+ if (result.mergeResult?.autoMergeEnabled)
56
+ return "auto";
57
+ return "manual";
58
+ }
47
59
  /**
48
60
  * Run the sync command - synchronizes files across repositories.
49
61
  */
@@ -80,8 +92,7 @@ export async function runSync(options, processorFactory = defaultProcessorFactor
80
92
  console.log(`Target files: ${formatFileNames(fileNames)}`);
81
93
  console.log(`Branch: ${branchName}\n`);
82
94
  const processor = processorFactory();
83
- const results = [];
84
- const plan = { resources: [], errors: [] };
95
+ const reportResults = [];
85
96
  for (let i = 0; i < config.repos.length; i++) {
86
97
  const repoConfig = config.repos[i];
87
98
  if (options.merge || options.mergeStrategy || options.deleteBranch) {
@@ -101,10 +112,11 @@ export async function runSync(options, processorFactory = defaultProcessorFactor
101
112
  }
102
113
  catch (error) {
103
114
  logger.error(current, repoConfig.git, String(error));
104
- results.push(buildErrorResult(repoConfig.git, error));
105
- plan.errors.push({
106
- repo: repoConfig.git,
107
- message: error instanceof Error ? error.message : String(error),
115
+ reportResults.push({
116
+ repoName: repoConfig.git,
117
+ success: false,
118
+ fileChanges: [],
119
+ error: error instanceof Error ? error.message : String(error),
108
120
  });
109
121
  continue;
110
122
  }
@@ -121,35 +133,48 @@ export async function runSync(options, processorFactory = defaultProcessorFactor
121
133
  prTemplate: config.prTemplate,
122
134
  noDelete: options.noDelete,
123
135
  });
124
- const repoResult = buildRepoResult(repoName, repoConfig, result);
125
- results.push(repoResult);
136
+ const mergeOutcome = determineMergeOutcome(result);
137
+ reportResults.push({
138
+ repoName,
139
+ success: result.success,
140
+ fileChanges: (result.fileChanges ?? []).map((f) => ({
141
+ path: f.path,
142
+ action: f.action,
143
+ })),
144
+ prUrl: result.prUrl,
145
+ mergeOutcome,
146
+ error: result.success ? undefined : result.message,
147
+ });
126
148
  if (result.skipped) {
127
149
  logger.skip(current, repoName, result.message);
128
150
  }
129
151
  else if (result.success) {
130
- logger.success(current, repoName, repoResult.message);
152
+ logger.success(current, repoName, result.message);
131
153
  }
132
154
  else {
133
155
  logger.error(current, repoName, result.message);
134
156
  }
135
- plan.resources.push(...syncResultToResources(repoName, repoConfig, result));
136
157
  }
137
158
  catch (error) {
138
159
  logger.error(current, repoName, String(error));
139
- results.push(buildErrorResult(repoName, error));
140
- plan.errors.push({
141
- repo: repoName,
142
- message: error instanceof Error ? error.message : String(error),
160
+ reportResults.push({
161
+ repoName,
162
+ success: false,
163
+ fileChanges: [],
164
+ error: error instanceof Error ? error.message : String(error),
143
165
  });
144
166
  }
145
167
  }
168
+ // Build and display report
169
+ const report = buildSyncReport(reportResults);
146
170
  console.log("");
147
- printPlan(plan);
148
- writePlanSummary(plan, {
149
- title: "Config Sync Summary",
150
- dryRun: options.dryRun ?? false,
151
- });
152
- if (plan.errors && plan.errors.length > 0) {
171
+ for (const line of formatSyncReportCLI(report)) {
172
+ console.log(line);
173
+ }
174
+ writeSyncReportSummary(report, options.dryRun ?? false);
175
+ // Exit with error if any failures
176
+ const hasErrors = reportResults.some((r) => r.error);
177
+ if (hasErrors) {
153
178
  process.exit(1);
154
179
  }
155
180
  }
@@ -0,0 +1,15 @@
1
+ import type { SyncReport } from "../output/sync-report.js";
2
+ interface FileChangeInput {
3
+ path: string;
4
+ action: "create" | "update" | "delete";
5
+ }
6
+ interface SyncResultInput {
7
+ repoName: string;
8
+ success: boolean;
9
+ fileChanges: FileChangeInput[];
10
+ prUrl?: string;
11
+ mergeOutcome?: "manual" | "auto" | "force" | "direct";
12
+ error?: string;
13
+ }
14
+ export declare function buildSyncReport(results: SyncResultInput[]): SyncReport;
15
+ export {};
@@ -0,0 +1,29 @@
1
+ export function buildSyncReport(results) {
2
+ const repos = [];
3
+ const totals = {
4
+ files: { create: 0, update: 0, delete: 0 },
5
+ };
6
+ for (const result of results) {
7
+ const files = result.fileChanges.map((f) => ({
8
+ path: f.path,
9
+ action: f.action,
10
+ }));
11
+ // Count totals
12
+ for (const file of files) {
13
+ if (file.action === "create")
14
+ totals.files.create++;
15
+ else if (file.action === "update")
16
+ totals.files.update++;
17
+ else if (file.action === "delete")
18
+ totals.files.delete++;
19
+ }
20
+ repos.push({
21
+ repoName: result.repoName,
22
+ files,
23
+ prUrl: result.prUrl,
24
+ mergeOutcome: result.mergeOutcome,
25
+ error: result.error,
26
+ });
27
+ }
28
+ return { repos, totals };
29
+ }
@@ -1,4 +1,4 @@
1
- export { formatResourceId, formatResourceLine, formatPlanSummary, formatPlan, printPlan, type ResourceType, type ResourceAction, type Resource, type ResourceDetails, type PropertyChange, type PlanCounts, type Plan, type RepoError, } from "./plan-formatter.js";
2
- export { formatPlanMarkdown, writePlanSummary, type PlanMarkdownOptions, } from "./plan-summary.js";
1
+ export { formatSyncReportCLI, formatSyncReportMarkdown, writeSyncReportSummary, type SyncReport, type RepoFileChanges, type FileChange, } from "./sync-report.js";
2
+ export { formatSettingsReportCLI, formatSettingsReportMarkdown, writeSettingsReportSummary, type SettingsReport, type RepoChanges, type RulesetChange, type SettingChange, } from "./settings-report.js";
3
3
  export { formatSummary, isGitHubActions, writeSummary, type MergeOutcome, type FileChanges, type RulesetPlanDetail, type RepoSettingsPlanDetail, type RepoResult, type SummaryData, } from "./github-summary.js";
4
4
  export { getMergeOutcome, toFileChanges, buildRepoResult, buildErrorResult, } from "./summary-utils.js";
@@ -1,7 +1,7 @@
1
- // Plan formatting (console output with chalk)
2
- export { formatResourceId, formatResourceLine, formatPlanSummary, formatPlan, printPlan, } from "./plan-formatter.js";
3
- // Plan summary (markdown output for GitHub)
4
- export { formatPlanMarkdown, writePlanSummary, } from "./plan-summary.js";
1
+ // Sync report (repo-grouped file changes)
2
+ export { formatSyncReportCLI, formatSyncReportMarkdown, writeSyncReportSummary, } from "./sync-report.js";
3
+ // Settings report (repo-grouped settings changes)
4
+ export { formatSettingsReportCLI, formatSettingsReportMarkdown, writeSettingsReportSummary, } from "./settings-report.js";
5
5
  // GitHub Actions summary
6
6
  export { formatSummary, isGitHubActions, writeSummary, } from "./github-summary.js";
7
7
  // Summary utilities
@@ -0,0 +1,37 @@
1
+ import { type PropertyDiff } from "../settings/rulesets/formatter.js";
2
+ import type { Ruleset } from "../config/index.js";
3
+ export interface SettingsReport {
4
+ repos: RepoChanges[];
5
+ totals: {
6
+ settings: {
7
+ add: number;
8
+ change: number;
9
+ };
10
+ rulesets: {
11
+ create: number;
12
+ update: number;
13
+ delete: number;
14
+ };
15
+ };
16
+ }
17
+ export interface RepoChanges {
18
+ repoName: string;
19
+ settings: SettingChange[];
20
+ rulesets: RulesetChange[];
21
+ error?: string;
22
+ }
23
+ export interface SettingChange {
24
+ name: string;
25
+ action: "add" | "change";
26
+ oldValue?: unknown;
27
+ newValue: unknown;
28
+ }
29
+ export interface RulesetChange {
30
+ name: string;
31
+ action: "create" | "update" | "delete";
32
+ propertyDiffs?: PropertyDiff[];
33
+ config?: Ruleset;
34
+ }
35
+ export declare function formatSettingsReportCLI(report: SettingsReport): string[];
36
+ export declare function formatSettingsReportMarkdown(report: SettingsReport, dryRun: boolean): string;
37
+ export declare function writeSettingsReportSummary(report: SettingsReport, dryRun: boolean): void;