@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.
@@ -228,6 +228,13 @@ export interface ProcessorOptions {
228
228
  prTemplate?: string;
229
229
  noDelete?: boolean;
230
230
  }
231
+ /**
232
+ * Detail of a single file change for reporting
233
+ */
234
+ export interface FileChangeDetail {
235
+ path: string;
236
+ action: "create" | "update" | "delete";
237
+ }
231
238
  /**
232
239
  * Result of repository processing
233
240
  */
@@ -243,6 +250,7 @@ export interface ProcessorResult {
243
250
  message: string;
244
251
  };
245
252
  diffStats?: DiffStats;
253
+ fileChanges?: FileChangeDetail[];
246
254
  }
247
255
  /**
248
256
  * Interface for repository processing operations
@@ -292,5 +300,5 @@ export interface IPRMergeHandler {
292
300
  * Create PR and optionally merge based on repo config.
293
301
  * Returns ProcessorResult with PR URL and merge status.
294
302
  */
295
- createAndMerge(repoInfo: RepoInfo, repoConfig: RepoConfig, options: PRHandlerOptions, changedFiles: FileAction[], repoName: string, diffStats?: DiffStats): Promise<ProcessorResult>;
303
+ createAndMerge(repoInfo: RepoInfo, repoConfig: RepoConfig, options: PRHandlerOptions, changedFiles: FileAction[], repoName: string, diffStats?: DiffStats, fileChanges?: FileChangeDetail[]): Promise<ProcessorResult>;
296
304
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspruyt/xfg",
3
- "version": "3.7.7",
3
+ "version": "3.8.0",
4
4
  "description": "CLI tool for repository-as-code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,39 +0,0 @@
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;
@@ -1,84 +0,0 @@
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
- }
@@ -1,8 +0,0 @@
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;
@@ -1,110 +0,0 @@
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
- }
@@ -1,28 +0,0 @@
1
- import type { Resource } from "../output/plan-formatter.js";
2
- import type { RulesetProcessorResult } from "./rulesets/processor.js";
3
- import type { ProcessorResult } from "../sync/index.js";
4
- import type { RepoConfig } from "../config/index.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[];
@@ -1,107 +0,0 @@
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
- }