@aspruyt/xfg 3.7.7 → 3.8.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.
@@ -8,9 +8,8 @@ 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
15
  * Run the settings command - manages GitHub Rulesets and repo settings.
@@ -51,7 +50,15 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
51
50
  const processor = processorFactory();
52
51
  const repoProcessor = repoProcessorFactory();
53
52
  const results = [];
54
- const plan = { resources: [], errors: [] };
53
+ const processingResults = [];
54
+ function getOrCreateResult(repoName) {
55
+ let result = processingResults.find((r) => r.repoName === repoName);
56
+ if (!result) {
57
+ result = { repoName };
58
+ processingResults.push(result);
59
+ }
60
+ return result;
61
+ }
55
62
  for (let i = 0; i < reposWithRulesets.length; i++) {
56
63
  const repoConfig = reposWithRulesets[i];
57
64
  let repoInfo;
@@ -63,26 +70,14 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
63
70
  catch (error) {
64
71
  logger.error(i + 1, repoConfig.git, String(error));
65
72
  results.push(buildErrorResult(repoConfig.git, error));
66
- plan.errors.push({
67
- repo: repoConfig.git,
68
- message: error instanceof Error ? error.message : String(error),
69
- });
73
+ getOrCreateResult(repoConfig.git).error =
74
+ error instanceof Error ? error.message : String(error);
70
75
  continue;
71
76
  }
72
77
  const repoName = getRepoDisplayName(repoInfo);
73
78
  if (!isGitHubRepo(repoInfo)) {
74
79
  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
- }
80
+ // Skipped repos don't appear in the report
86
81
  continue;
87
82
  }
88
83
  const managedRulesets = getManagedRulesets(null, config.id);
@@ -135,15 +130,22 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
135
130
  message: result.message,
136
131
  rulesetPlanDetails: result.planOutput?.entries,
137
132
  });
138
- plan.resources.push(...rulesetResultToResources(repoName, result));
133
+ // Collect result for SettingsReport
134
+ if (!result.skipped) {
135
+ getOrCreateResult(repoName).rulesetResult = result;
136
+ }
139
137
  }
140
138
  catch (error) {
141
139
  logger.error(i + 1, repoName, String(error));
142
140
  results.push(buildErrorResult(repoName, error));
143
- plan.errors.push({
144
- repo: repoName,
145
- message: error instanceof Error ? error.message : String(error),
146
- });
141
+ const existingResult = getOrCreateResult(repoName);
142
+ const errorMsg = error instanceof Error ? error.message : String(error);
143
+ if (existingResult.error) {
144
+ existingResult.error += `; ${errorMsg}`;
145
+ }
146
+ else {
147
+ existingResult.error = errorMsg;
148
+ }
147
149
  }
148
150
  }
149
151
  if (reposWithRepoSettings.length > 0) {
@@ -159,10 +161,8 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
159
161
  }
160
162
  catch (error) {
161
163
  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
- });
164
+ getOrCreateResult(repoConfig.git).error =
165
+ error instanceof Error ? error.message : String(error);
166
166
  continue;
167
167
  }
168
168
  const repoName = getRepoDisplayName(repoInfo);
@@ -205,24 +205,33 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
205
205
  });
206
206
  }
207
207
  }
208
- plan.resources.push(...repoSettingsResultToResources(repoName, result));
208
+ // Collect result for SettingsReport
209
+ if (!result.skipped) {
210
+ getOrCreateResult(repoName).settingsResult = result;
211
+ }
209
212
  }
210
213
  catch (error) {
211
214
  console.error(` ✗ ${repoName}: ${error}`);
212
- plan.errors.push({
213
- repo: repoName,
214
- message: error instanceof Error ? error.message : String(error),
215
- });
215
+ const existingResult = getOrCreateResult(repoName);
216
+ const errorMsg = error instanceof Error ? error.message : String(error);
217
+ if (existingResult.error) {
218
+ existingResult.error += `; ${errorMsg}`;
219
+ }
220
+ else {
221
+ existingResult.error = errorMsg;
222
+ }
216
223
  }
217
224
  }
218
225
  }
219
226
  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) {
227
+ const report = buildSettingsReport(processingResults);
228
+ const lines = formatSettingsReportCLI(report);
229
+ for (const line of lines) {
230
+ console.log(line);
231
+ }
232
+ writeSettingsReportSummary(report, options.dryRun ?? false);
233
+ const hasErrors = report.repos.some((r) => r.error);
234
+ if (hasErrors) {
226
235
  process.exit(1);
227
236
  }
228
237
  }
@@ -0,0 +1,19 @@
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
+ interface ProcessorResults {
5
+ repoName: string;
6
+ settingsResult?: {
7
+ planOutput?: {
8
+ entries?: RepoSettingsPlanEntry[];
9
+ };
10
+ };
11
+ rulesetResult?: {
12
+ planOutput?: {
13
+ entries?: RulesetPlanEntry[];
14
+ };
15
+ };
16
+ error?: string;
17
+ }
18
+ export declare function buildSettingsReport(results: ProcessorResults[]): SettingsReport;
19
+ export {};
@@ -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;