@aspruyt/xfg 3.7.3 → 3.7.5

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 CHANGED
@@ -15,12 +15,14 @@ import { sanitizeBranchName, validateBranchName } from "./git-ops.js";
15
15
  import { logger } from "./logger.js";
16
16
  import { generateWorkspaceName } from "./workspace-utils.js";
17
17
  import { RepositoryProcessor, } from "./repository-processor.js";
18
- import { writeSummary } from "./github-summary.js";
19
18
  import { buildRepoResult, buildErrorResult } from "./summary-utils.js";
20
19
  import { RulesetProcessor, } from "./ruleset-processor.js";
21
20
  import { getManagedRulesets } from "./manifest.js";
22
21
  import { isGitHubRepo } from "./repo-detector.js";
23
22
  import { RepoSettingsProcessor, } from "./repo-settings-processor.js";
23
+ import { printPlan } from "./plan-formatter.js";
24
+ import { writePlanSummary } from "./plan-summary.js";
25
+ import { rulesetResultToResources, syncResultToResources, repoSettingsResultToResources, } from "./resource-converters.js";
24
26
  /**
25
27
  * Default factory that creates a real RepositoryProcessor.
26
28
  */
@@ -115,6 +117,8 @@ export async function runSync(options, processorFactory = defaultProcessorFactor
115
117
  console.log(`Branch: ${branchName}\n`);
116
118
  const processor = processorFactory();
117
119
  const results = [];
120
+ // Build plan for Terraform-style output
121
+ const plan = { resources: [], errors: [] };
118
122
  for (let i = 0; i < config.repos.length; i++) {
119
123
  const repoConfig = config.repos[i];
120
124
  // Apply CLI merge overrides to repo config
@@ -136,6 +140,10 @@ export async function runSync(options, processorFactory = defaultProcessorFactor
136
140
  catch (error) {
137
141
  logger.error(current, repoConfig.git, String(error));
138
142
  results.push(buildErrorResult(repoConfig.git, error));
143
+ plan.errors.push({
144
+ repo: repoConfig.git,
145
+ message: error instanceof Error ? error.message : String(error),
146
+ });
139
147
  continue;
140
148
  }
141
149
  const repoName = getRepoDisplayName(repoInfo);
@@ -162,27 +170,27 @@ export async function runSync(options, processorFactory = defaultProcessorFactor
162
170
  else {
163
171
  logger.error(current, repoName, result.message);
164
172
  }
173
+ // Collect resources for plan output
174
+ plan.resources.push(...syncResultToResources(repoName, repoConfig, result));
165
175
  }
166
176
  catch (error) {
167
177
  logger.error(current, repoName, String(error));
168
178
  results.push(buildErrorResult(repoName, error));
179
+ plan.errors.push({
180
+ repo: repoName,
181
+ message: error instanceof Error ? error.message : String(error),
182
+ });
169
183
  }
170
184
  }
171
- logger.summary();
172
- // Write GitHub Actions job summary if running in GitHub Actions
173
- const succeeded = results.filter((r) => r.status === "succeeded").length;
174
- const skipped = results.filter((r) => r.status === "skipped").length;
175
- const failed = results.filter((r) => r.status === "failed").length;
176
- writeSummary({
185
+ // Print Terraform-style plan summary
186
+ console.log("");
187
+ printPlan(plan);
188
+ // Write GitHub Actions job summary
189
+ writePlanSummary(plan, {
177
190
  title: "Config Sync Summary",
178
- dryRun: options.dryRun,
179
- total: config.repos.length,
180
- succeeded,
181
- skipped,
182
- failed,
183
- results,
191
+ dryRun: options.dryRun ?? false,
184
192
  });
185
- if (logger.hasFailures()) {
193
+ if (plan.errors && plan.errors.length > 0) {
186
194
  process.exit(1);
187
195
  }
188
196
  }
@@ -228,14 +236,8 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
228
236
  const processor = processorFactory();
229
237
  const repoProcessor = repoProcessorFactory();
230
238
  const results = [];
231
- let successCount = 0;
232
- let failCount = 0;
233
- let skipCount = 0;
234
- // Tracking for multi-repo summary in dry-run mode
235
- let totalCreates = 0;
236
- let totalUpdates = 0;
237
- let totalDeletes = 0;
238
- let reposWithChanges = 0;
239
+ // Build plan for Terraform-style output
240
+ const plan = { resources: [], errors: [] };
239
241
  for (let i = 0; i < reposWithRulesets.length; i++) {
240
242
  const repoConfig = reposWithRulesets[i];
241
243
  let repoInfo;
@@ -247,14 +249,28 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
247
249
  catch (error) {
248
250
  logger.error(i + 1, repoConfig.git, String(error));
249
251
  results.push(buildErrorResult(repoConfig.git, error));
250
- failCount++;
252
+ plan.errors.push({
253
+ repo: repoConfig.git,
254
+ message: error instanceof Error ? error.message : String(error),
255
+ });
251
256
  continue;
252
257
  }
253
258
  const repoName = getRepoDisplayName(repoInfo);
254
259
  // Skip non-GitHub repos
255
260
  if (!isGitHubRepo(repoInfo)) {
256
261
  logger.skip(i + 1, repoName, "GitHub Rulesets only supported for GitHub repos");
257
- skipCount++;
262
+ // Mark all rulesets from this repo as skipped
263
+ if (repoConfig.settings?.rulesets) {
264
+ for (const rulesetName of Object.keys(repoConfig.settings.rulesets)) {
265
+ plan.resources.push({
266
+ type: "ruleset",
267
+ repo: repoName,
268
+ name: rulesetName,
269
+ action: "skipped",
270
+ skipReason: "GitHub Rulesets only supported for GitHub repos",
271
+ });
272
+ }
273
+ }
258
274
  continue;
259
275
  }
260
276
  // Note: For settings command, we don't clone repos - we work with the API directly.
@@ -269,34 +285,19 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
269
285
  managedRulesets,
270
286
  noDelete: options.noDelete,
271
287
  });
272
- // Display plan output for dry-run mode
273
- if (options.dryRun && result.planOutput) {
274
- if (result.planOutput.lines.length > 0) {
275
- logger.rulesetPlan(repoName, result.planOutput.lines, {
276
- creates: result.planOutput.creates,
277
- updates: result.planOutput.updates,
278
- deletes: result.planOutput.deletes,
279
- unchanged: result.planOutput.unchanged,
280
- });
281
- }
282
- // Accumulate totals for multi-repo summary
283
- totalCreates += result.planOutput.creates;
284
- totalUpdates += result.planOutput.updates;
285
- totalDeletes += result.planOutput.deletes;
286
- if (result.planOutput.creates +
287
- result.planOutput.updates +
288
- result.planOutput.deletes >
289
- 0) {
290
- reposWithChanges++;
288
+ // Print detailed ruleset plan output
289
+ if (result.planOutput && result.planOutput.lines.length > 0) {
290
+ logger.info("");
291
+ logger.info(chalk.bold(`${repoName} - Rulesets:`));
292
+ for (const line of result.planOutput.lines) {
293
+ logger.info(line);
291
294
  }
292
295
  }
293
296
  if (result.skipped) {
294
297
  logger.skip(i + 1, repoName, result.message);
295
- skipCount++;
296
298
  }
297
299
  else if (result.success) {
298
300
  logger.success(i + 1, repoName, result.message);
299
- successCount++;
300
301
  // Update manifest with ruleset tracking if there are rulesets to track
301
302
  if (result.manifestUpdate &&
302
303
  result.manifestUpdate.rulesets.length > 0) {
@@ -316,7 +317,6 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
316
317
  }
317
318
  else {
318
319
  logger.error(i + 1, repoName, result.message);
319
- failCount++;
320
320
  }
321
321
  results.push({
322
322
  repoName,
@@ -328,11 +328,16 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
328
328
  message: result.message,
329
329
  rulesetPlanDetails: result.planOutput?.entries,
330
330
  });
331
+ // Collect resources for plan output
332
+ plan.resources.push(...rulesetResultToResources(repoName, result));
331
333
  }
332
334
  catch (error) {
333
335
  logger.error(i + 1, repoName, String(error));
334
336
  results.push(buildErrorResult(repoName, error));
335
- failCount++;
337
+ plan.errors.push({
338
+ repo: repoName,
339
+ message: error instanceof Error ? error.message : String(error),
340
+ });
336
341
  }
337
342
  }
338
343
  // Process repo settings
@@ -349,7 +354,10 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
349
354
  }
350
355
  catch (error) {
351
356
  console.error(`Failed to parse ${repoConfig.git}: ${error}`);
352
- failCount++;
357
+ plan.errors.push({
358
+ repo: repoConfig.git,
359
+ message: error instanceof Error ? error.message : String(error),
360
+ });
353
361
  continue;
354
362
  }
355
363
  const repoName = getRepoDisplayName(repoInfo);
@@ -374,11 +382,9 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
374
382
  }
375
383
  else if (result.success) {
376
384
  console.log(chalk.green(` ✓ ${repoName}: ${result.message}`));
377
- successCount++;
378
385
  }
379
386
  else {
380
387
  console.log(chalk.red(` ✗ ${repoName}: ${result.message}`));
381
- failCount++;
382
388
  }
383
389
  // Merge repo settings plan details into existing result or push new
384
390
  if (!result.skipped) {
@@ -395,40 +401,27 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
395
401
  });
396
402
  }
397
403
  }
404
+ // Collect resources for plan output
405
+ plan.resources.push(...repoSettingsResultToResources(repoName, result));
398
406
  }
399
407
  catch (error) {
400
408
  console.error(` ✗ ${repoName}: ${error}`);
401
- failCount++;
409
+ plan.errors.push({
410
+ repo: repoName,
411
+ message: error instanceof Error ? error.message : String(error),
412
+ });
402
413
  }
403
414
  }
404
415
  }
405
- // Multi-repo summary for dry-run mode
406
- if (options.dryRun && reposWithChanges > 0) {
407
- console.log("");
408
- console.log(chalk.gray("─".repeat(40)));
409
- const totalParts = [];
410
- if (totalCreates > 0)
411
- totalParts.push(chalk.green(`${totalCreates} to create`));
412
- if (totalUpdates > 0)
413
- totalParts.push(chalk.yellow(`${totalUpdates} to update`));
414
- if (totalDeletes > 0)
415
- totalParts.push(chalk.red(`${totalDeletes} to delete`));
416
- console.log(chalk.bold(`Total: ${totalParts.join(", ")} across ${reposWithChanges} ${reposWithChanges === 1 ? "repository" : "repositories"}`));
417
- }
418
- // Summary
419
- console.log("\n" + "=".repeat(50));
420
- console.log(`Completed: ${successCount} succeeded, ${skipCount} skipped, ${failCount} failed`);
421
- // Write GitHub Actions job summary if available
422
- writeSummary({
416
+ // Print Terraform-style plan summary
417
+ console.log("");
418
+ printPlan(plan);
419
+ // Write GitHub Actions job summary
420
+ writePlanSummary(plan, {
423
421
  title: "Repository Settings Summary",
424
- dryRun: options.dryRun,
425
- total: reposWithRulesets.length + reposWithRepoSettings.length,
426
- succeeded: successCount,
427
- skipped: skipCount,
428
- failed: failCount,
429
- results,
422
+ dryRun: options.dryRun ?? false,
430
423
  });
431
- if (failCount > 0) {
424
+ if (plan.errors && plan.errors.length > 0) {
432
425
  process.exit(1);
433
426
  }
434
427
  }
package/dist/logger.d.ts CHANGED
@@ -1,22 +1,13 @@
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
- }
8
2
  export interface ILogger {
9
3
  info(message: string): void;
10
4
  fileDiff(fileName: string, status: FileStatus, diffLines: string[]): void;
11
5
  diffSummary(newCount: number, modifiedCount: number, unchangedCount: number, deletedCount?: number): void;
12
- rulesetPlan(repoName: string, planLines: string[], counts: RulesetPlanCounts): void;
13
6
  setTotal(total: number): void;
14
7
  progress(current: number, repoName: string, message: string): void;
15
8
  success(current: number, repoName: string, message: string): void;
16
9
  skip(current: number, repoName: string, reason: string): void;
17
10
  error(current: number, repoName: string, error: string): void;
18
- summary(): void;
19
- hasFailures(): boolean;
20
11
  }
21
12
  export interface LoggerStats {
22
13
  total: number;
@@ -41,11 +32,5 @@ export declare class Logger implements ILogger {
41
32
  * Display summary statistics for dry-run diff.
42
33
  */
43
34
  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;
48
- summary(): void;
49
- hasFailures(): boolean;
50
35
  }
51
36
  export declare const logger: Logger;
package/dist/logger.js CHANGED
@@ -62,39 +62,5 @@ 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
- }
88
- summary() {
89
- console.log("");
90
- console.log(chalk.bold("Summary:"));
91
- console.log(` Total: ${this.stats.total}`);
92
- console.log(chalk.green(` Succeeded: ${this.stats.succeeded}`));
93
- console.log(chalk.yellow(` Skipped: ${this.stats.skipped}`));
94
- console.log(chalk.red(` Failed: ${this.stats.failed}`));
95
- }
96
- hasFailures() {
97
- return this.stats.failed > 0;
98
- }
99
65
  }
100
66
  export const logger = new Logger();
@@ -0,0 +1,39 @@
1
+ export type ResourceType = "file" | "ruleset" | "setting";
2
+ export type ResourceAction = "create" | "update" | "delete" | "unchanged" | "skipped";
3
+ export interface Resource {
4
+ type: ResourceType;
5
+ repo: string;
6
+ name: string;
7
+ action: ResourceAction;
8
+ details?: ResourceDetails;
9
+ skipReason?: string;
10
+ }
11
+ export interface ResourceDetails {
12
+ diff?: string[];
13
+ properties?: PropertyChange[];
14
+ }
15
+ export interface PropertyChange {
16
+ path: string;
17
+ action: "add" | "change" | "remove";
18
+ oldValue?: unknown;
19
+ newValue?: unknown;
20
+ }
21
+ export declare function formatResourceId(resource: Resource): string;
22
+ export declare function formatResourceLine(resource: Resource): string;
23
+ export interface PlanCounts {
24
+ create: number;
25
+ update: number;
26
+ delete: number;
27
+ skipped?: number;
28
+ }
29
+ export declare function formatPlanSummary(counts: PlanCounts): string;
30
+ export interface Plan {
31
+ resources: Resource[];
32
+ errors?: RepoError[];
33
+ }
34
+ export interface RepoError {
35
+ repo: string;
36
+ message: string;
37
+ }
38
+ export declare function formatPlan(plan: Plan): string[];
39
+ export declare function printPlan(plan: Plan): void;
@@ -0,0 +1,84 @@
1
+ import chalk from "chalk";
2
+ export function formatResourceId(resource) {
3
+ return `${resource.type} "${resource.repo}/${resource.name}"`;
4
+ }
5
+ export function formatResourceLine(resource) {
6
+ const id = formatResourceId(resource);
7
+ switch (resource.action) {
8
+ case "create":
9
+ return chalk.green(`+ ${id}`);
10
+ case "update":
11
+ return chalk.yellow(`~ ${id}`);
12
+ case "delete":
13
+ return chalk.red(`- ${id}`);
14
+ case "skipped":
15
+ return chalk.gray(`⊘ ${id}`);
16
+ case "unchanged":
17
+ return chalk.gray(` ${id}`);
18
+ }
19
+ }
20
+ export function formatPlanSummary(counts) {
21
+ const parts = [];
22
+ if (counts.create > 0) {
23
+ parts.push(chalk.green(`${counts.create} to create`));
24
+ }
25
+ if (counts.update > 0) {
26
+ parts.push(chalk.yellow(`${counts.update} to change`));
27
+ }
28
+ if (counts.delete > 0) {
29
+ parts.push(chalk.red(`${counts.delete} to destroy`));
30
+ }
31
+ if (parts.length === 0 && (!counts.skipped || counts.skipped === 0)) {
32
+ return "No changes. Your repositories match the configuration.";
33
+ }
34
+ let summary = parts.length > 0 ? `Plan: ${parts.join(", ")}` : "Plan:";
35
+ if (counts.skipped && counts.skipped > 0) {
36
+ summary += chalk.gray(` (${counts.skipped} skipped)`);
37
+ }
38
+ return summary;
39
+ }
40
+ export function formatPlan(plan) {
41
+ const lines = [];
42
+ // Filter to only changed resources
43
+ const changedResources = plan.resources.filter((r) => r.action !== "unchanged");
44
+ // Format each resource
45
+ for (const resource of changedResources) {
46
+ lines.push(formatResourceLine(resource));
47
+ // Add details if present (indented)
48
+ if (resource.details?.diff) {
49
+ for (const diffLine of resource.details.diff) {
50
+ lines.push(` ${diffLine}`);
51
+ }
52
+ }
53
+ }
54
+ // Add errors
55
+ if (plan.errors && plan.errors.length > 0) {
56
+ for (const error of plan.errors) {
57
+ lines.push(chalk.red(`✗ ${error.repo}`));
58
+ lines.push(chalk.red(` Error: ${error.message}`));
59
+ }
60
+ }
61
+ // Add blank line before summary
62
+ if (lines.length > 0) {
63
+ lines.push("");
64
+ }
65
+ // Count actions
66
+ const counts = {
67
+ create: plan.resources.filter((r) => r.action === "create").length,
68
+ update: plan.resources.filter((r) => r.action === "update").length,
69
+ delete: plan.resources.filter((r) => r.action === "delete").length,
70
+ skipped: plan.resources.filter((r) => r.action === "skipped").length,
71
+ };
72
+ lines.push(formatPlanSummary(counts));
73
+ // Add error count if any
74
+ if (plan.errors && plan.errors.length > 0) {
75
+ lines.push(chalk.red(`${plan.errors.length} ${plan.errors.length === 1 ? "repository" : "repositories"} failed.`));
76
+ }
77
+ return lines;
78
+ }
79
+ export function printPlan(plan) {
80
+ const lines = formatPlan(plan);
81
+ for (const line of lines) {
82
+ console.log(line);
83
+ }
84
+ }
@@ -0,0 +1,8 @@
1
+ import type { Plan, Resource, PlanCounts, RepoError } from "./plan-formatter.js";
2
+ export type { Plan, Resource, PlanCounts, RepoError };
3
+ export interface PlanMarkdownOptions {
4
+ title: string;
5
+ dryRun: boolean;
6
+ }
7
+ export declare function formatPlanMarkdown(plan: Plan, options: PlanMarkdownOptions): string;
8
+ export declare function writePlanSummary(plan: Plan, options: PlanMarkdownOptions): void;
@@ -0,0 +1,110 @@
1
+ import { appendFileSync } from "node:fs";
2
+ function getActionSymbol(action) {
3
+ switch (action) {
4
+ case "create":
5
+ return "+";
6
+ case "update":
7
+ return "~";
8
+ case "delete":
9
+ return "-";
10
+ case "skipped":
11
+ return "⊘";
12
+ default:
13
+ return "";
14
+ }
15
+ }
16
+ function formatResourceIdPlain(resource) {
17
+ return `${resource.type} "${resource.repo}/${resource.name}"`;
18
+ }
19
+ function countActions(resources) {
20
+ return {
21
+ create: resources.filter((r) => r.action === "create").length,
22
+ update: resources.filter((r) => r.action === "update").length,
23
+ delete: resources.filter((r) => r.action === "delete").length,
24
+ skipped: resources.filter((r) => r.action === "skipped").length,
25
+ };
26
+ }
27
+ function formatPlanSummaryPlain(counts) {
28
+ const parts = [];
29
+ if (counts.create > 0)
30
+ parts.push(`${counts.create} to create`);
31
+ if (counts.update > 0)
32
+ parts.push(`${counts.update} to change`);
33
+ if (counts.delete > 0)
34
+ parts.push(`${counts.delete} to destroy`);
35
+ if (parts.length === 0) {
36
+ return "No changes";
37
+ }
38
+ return parts.join(", ");
39
+ }
40
+ export function formatPlanMarkdown(plan, options) {
41
+ const lines = [];
42
+ const counts = countActions(plan.resources);
43
+ const changedResources = plan.resources.filter((r) => r.action !== "unchanged");
44
+ // Title
45
+ const titleSuffix = options.dryRun ? " (Dry Run)" : "";
46
+ lines.push(`## ${options.title}${titleSuffix}`);
47
+ lines.push("");
48
+ // Dry-run warning
49
+ if (options.dryRun) {
50
+ lines.push("> [!WARNING]");
51
+ lines.push("> This was a dry run — no changes were applied");
52
+ lines.push("");
53
+ }
54
+ // Plan summary as heading
55
+ const summaryText = formatPlanSummaryPlain(counts);
56
+ lines.push(`### Plan: ${summaryText}`);
57
+ lines.push("");
58
+ // Resource table (if any changes)
59
+ if (changedResources.length > 0) {
60
+ lines.push("<details open>");
61
+ lines.push("<summary><strong>Resources</strong></summary>");
62
+ lines.push("");
63
+ lines.push("| Resource | Action |");
64
+ lines.push("|----------|--------|");
65
+ for (const resource of changedResources) {
66
+ const symbol = getActionSymbol(resource.action);
67
+ const id = formatResourceIdPlain(resource);
68
+ lines.push(`| \`${symbol} ${id}\` | ${resource.action} |`);
69
+ }
70
+ lines.push("");
71
+ lines.push("</details>");
72
+ }
73
+ // Add diff details for resources that have them
74
+ const resourcesWithDiffs = changedResources.filter((r) => r.details?.diff && r.details.diff.length > 0);
75
+ for (const resource of resourcesWithDiffs) {
76
+ lines.push("");
77
+ lines.push("<details>");
78
+ lines.push(`<summary><strong>Diff: ${formatResourceIdPlain(resource)}</strong></summary>`);
79
+ lines.push("");
80
+ lines.push("```diff");
81
+ for (const diffLine of resource.details.diff) {
82
+ lines.push(diffLine);
83
+ }
84
+ lines.push("```");
85
+ lines.push("");
86
+ lines.push("</details>");
87
+ }
88
+ // Error section
89
+ if (plan.errors && plan.errors.length > 0) {
90
+ lines.push("");
91
+ lines.push("<details open>");
92
+ lines.push("<summary><strong>Errors</strong></summary>");
93
+ lines.push("");
94
+ lines.push("| Repository | Error |");
95
+ lines.push("|------------|-------|");
96
+ for (const error of plan.errors) {
97
+ lines.push(`| ${error.repo} | ${error.message} |`);
98
+ }
99
+ lines.push("");
100
+ lines.push("</details>");
101
+ }
102
+ return lines.join("\n");
103
+ }
104
+ export function writePlanSummary(plan, options) {
105
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
106
+ if (!summaryPath)
107
+ return;
108
+ const markdown = formatPlanMarkdown(plan, options);
109
+ appendFileSync(summaryPath, "\n" + markdown + "\n");
110
+ }
@@ -0,0 +1,28 @@
1
+ import type { Resource } from "./plan-formatter.js";
2
+ import type { RulesetProcessorResult } from "./ruleset-processor.js";
3
+ import type { ProcessorResult } from "./repository-processor.js";
4
+ import type { RepoConfig } from "./config.js";
5
+ /**
6
+ * Convert RulesetProcessorResult planOutput entries to Resource objects.
7
+ * Includes the detailed plan lines in the first resource's details for display.
8
+ */
9
+ export declare function rulesetResultToResources(repoName: string, result: RulesetProcessorResult): Resource[];
10
+ /**
11
+ * Convert sync ProcessorResult diffStats to Resource objects.
12
+ * Since we don't have per-file details, we represent each file from config
13
+ * with the aggregate action based on diffStats.
14
+ */
15
+ export declare function syncResultToResources(repoName: string, repoConfig: Pick<RepoConfig, "files">, result: ProcessorResult): Resource[];
16
+ /**
17
+ * Convert repo settings processor planOutput entries to Resource objects.
18
+ * Includes the detailed plan lines in the first resource's details for display.
19
+ */
20
+ export declare function repoSettingsResultToResources(repoName: string, result: {
21
+ planOutput?: {
22
+ entries?: Array<{
23
+ property: string;
24
+ action: string;
25
+ }>;
26
+ lines?: string[];
27
+ };
28
+ }): Resource[];
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Convert RulesetProcessorResult planOutput entries to Resource objects.
3
+ * Includes the detailed plan lines in the first resource's details for display.
4
+ */
5
+ export function rulesetResultToResources(repoName, result) {
6
+ const resources = [];
7
+ const planLines = result.planOutput?.lines ?? [];
8
+ if (result.planOutput?.entries) {
9
+ for (let i = 0; i < result.planOutput.entries.length; i++) {
10
+ const entry = result.planOutput.entries[i];
11
+ let action;
12
+ switch (entry.action) {
13
+ case "create":
14
+ action = "create";
15
+ break;
16
+ case "update":
17
+ action = "update";
18
+ break;
19
+ case "delete":
20
+ action = "delete";
21
+ break;
22
+ default:
23
+ action = "unchanged";
24
+ }
25
+ // Attach all plan lines to first resource for GitHub summary display
26
+ const details = i === 0 && planLines.length > 0 ? { diff: planLines } : undefined;
27
+ resources.push({
28
+ type: "ruleset",
29
+ repo: repoName,
30
+ name: entry.name,
31
+ action,
32
+ details,
33
+ });
34
+ }
35
+ }
36
+ return resources;
37
+ }
38
+ /**
39
+ * Convert sync ProcessorResult diffStats to Resource objects.
40
+ * Since we don't have per-file details, we represent each file from config
41
+ * with the aggregate action based on diffStats.
42
+ */
43
+ export function syncResultToResources(repoName, repoConfig, result) {
44
+ const resources = [];
45
+ if (result.skipped) {
46
+ // Mark all files as unchanged when skipped
47
+ for (const file of repoConfig.files) {
48
+ resources.push({
49
+ type: "file",
50
+ repo: repoName,
51
+ name: file.fileName,
52
+ action: "unchanged",
53
+ });
54
+ }
55
+ return resources;
56
+ }
57
+ if (!result.diffStats) {
58
+ return resources;
59
+ }
60
+ // With aggregate stats, we can show repo-level summary
61
+ // For now, create one resource per file in config with best-effort action
62
+ // Note: This is approximate since we don't have per-file tracking
63
+ const { newCount, modifiedCount, deletedCount } = result.diffStats;
64
+ for (const file of repoConfig.files) {
65
+ // Determine action based on aggregate stats - this is a simplification
66
+ let action = "unchanged";
67
+ if (newCount > 0) {
68
+ action = "create";
69
+ }
70
+ else if (modifiedCount > 0) {
71
+ action = "update";
72
+ }
73
+ else if (deletedCount > 0) {
74
+ action = "delete";
75
+ }
76
+ resources.push({
77
+ type: "file",
78
+ repo: repoName,
79
+ name: file.fileName,
80
+ action,
81
+ });
82
+ }
83
+ return resources;
84
+ }
85
+ /**
86
+ * Convert repo settings processor planOutput entries to Resource objects.
87
+ * Includes the detailed plan lines in the first resource's details for display.
88
+ */
89
+ export function repoSettingsResultToResources(repoName, result) {
90
+ const resources = [];
91
+ const planLines = result.planOutput?.lines ?? [];
92
+ if (result.planOutput?.entries) {
93
+ for (let i = 0; i < result.planOutput.entries.length; i++) {
94
+ const entry = result.planOutput.entries[i];
95
+ // Attach all plan lines to first resource for GitHub summary display
96
+ const details = i === 0 && planLines.length > 0 ? { diff: planLines } : undefined;
97
+ resources.push({
98
+ type: "setting",
99
+ repo: repoName,
100
+ name: entry.property,
101
+ action: entry.action === "add" ? "create" : "update",
102
+ details,
103
+ });
104
+ }
105
+ }
106
+ return resources;
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspruyt/xfg",
3
- "version": "3.7.3",
3
+ "version": "3.7.5",
4
4
  "description": "CLI tool for repository-as-code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,7 +27,7 @@
27
27
  "start": "node dist/cli.js",
28
28
  "dev": "ts-node src/cli.ts",
29
29
  "test": "node --import tsx scripts/run-tests.js",
30
- "test:coverage": "c8 --check-coverage --lines 95 --reporter=text --reporter=lcov --all --src=src --exclude='test/**/*.test.ts' --exclude='scripts/**' npm test",
30
+ "test:coverage": "c8 --check-coverage --lines 95 --reporter=text --reporter=lcov --all --src=src --exclude='test/**/*.test.ts' --exclude='scripts/**' --exclude='src/strategies/commit-strategy.ts' --exclude='src/strategies/ruleset-strategy.ts' --exclude='test/mocks/**' npm test",
31
31
  "test:integration:github": "npm run build && node --import tsx --test test/integration/github.test.ts",
32
32
  "test:integration:ado": "npm run build && node --import tsx --test test/integration/ado.test.ts",
33
33
  "test:integration:gitlab": "npm run build && node --import tsx --test test/integration/gitlab.test.ts",