@aspruyt/xfg 5.1.3 → 5.1.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.
Files changed (81) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/lifecycle-report-builder.d.ts +2 -0
  3. package/dist/cli/lifecycle-report-builder.js +15 -0
  4. package/dist/cli/program.js +16 -11
  5. package/dist/cli/settings-report-builder.d.ts +1 -1
  6. package/dist/cli/settings-report-builder.js +20 -32
  7. package/dist/cli/sync-command.js +59 -64
  8. package/dist/cli/types.d.ts +3 -6
  9. package/dist/config/formatter.js +1 -6
  10. package/dist/config/index.d.ts +2 -1
  11. package/dist/config/index.js +2 -0
  12. package/dist/config/merge.d.ts +0 -6
  13. package/dist/config/merge.js +0 -13
  14. package/dist/config/types.d.ts +0 -9
  15. package/dist/config/validator.d.ts +1 -1
  16. package/dist/config/validator.js +1 -1
  17. package/dist/lifecycle/github-lifecycle-provider.js +28 -45
  18. package/dist/lifecycle/lifecycle-helpers.d.ts +1 -1
  19. package/dist/lifecycle/repo-lifecycle-manager.d.ts +1 -1
  20. package/dist/lifecycle/types.d.ts +4 -4
  21. package/dist/output/github-summary.d.ts +0 -50
  22. package/dist/output/github-summary.js +0 -201
  23. package/dist/output/lifecycle-report.d.ts +0 -1
  24. package/dist/output/lifecycle-report.js +0 -15
  25. package/dist/output/settings-report.d.ts +1 -1
  26. package/dist/output/settings-report.js +20 -78
  27. package/dist/output/sync-report.d.ts +1 -0
  28. package/dist/output/sync-report.js +19 -1
  29. package/dist/output/unified-summary.d.ts +1 -2
  30. package/dist/output/unified-summary.js +1 -19
  31. package/dist/settings/base-processor.d.ts +8 -0
  32. package/dist/settings/base-processor.js +7 -0
  33. package/dist/settings/index.d.ts +2 -2
  34. package/dist/settings/index.js +3 -1
  35. package/dist/settings/labels/converter.d.ts +1 -1
  36. package/dist/settings/labels/diff.d.ts +1 -1
  37. package/dist/settings/labels/formatter.d.ts +1 -1
  38. package/dist/settings/repo-settings/processor.js +9 -5
  39. package/dist/settings/rulesets/formatter.js +97 -85
  40. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -2
  41. package/dist/settings/rulesets/github-ruleset-strategy.js +7 -4
  42. package/dist/settings/rulesets/index.d.ts +1 -1
  43. package/dist/settings/rulesets/index.js +0 -2
  44. package/dist/settings/rulesets/processor.js +5 -1
  45. package/dist/settings/rulesets/types.d.ts +6 -1
  46. package/dist/shared/gh-api-utils.d.ts +1 -1
  47. package/dist/shared/logger.d.ts +8 -8
  48. package/dist/shared/logger.js +9 -8
  49. package/dist/shared/repo-detector.js +0 -1
  50. package/dist/sync/branch-manager.js +2 -2
  51. package/dist/sync/commit-push-manager.js +2 -2
  52. package/dist/sync/diff-utils.d.ts +0 -9
  53. package/dist/sync/diff-utils.js +0 -9
  54. package/dist/sync/file-sync-orchestrator.js +1 -1
  55. package/dist/sync/file-sync-strategy.d.ts +1 -1
  56. package/dist/sync/file-writer.d.ts +1 -1
  57. package/dist/sync/file-writer.js +3 -1
  58. package/dist/sync/index.d.ts +1 -2
  59. package/dist/sync/index.js +1 -1
  60. package/dist/sync/manifest-manager.d.ts +1 -1
  61. package/dist/sync/manifest-manager.js +1 -1
  62. package/dist/sync/manifest.js +9 -1
  63. package/dist/sync/pr-merge-handler.js +2 -2
  64. package/dist/sync/sync-workflow.d.ts +1 -1
  65. package/dist/sync/types.d.ts +2 -2
  66. package/dist/vcs/authenticated-git-ops.d.ts +2 -1
  67. package/dist/vcs/commit-strategy-selector.d.ts +1 -1
  68. package/dist/vcs/commit-strategy-selector.js +1 -1
  69. package/dist/vcs/git-ops.d.ts +0 -1
  70. package/dist/vcs/git-ops.js +15 -8
  71. package/dist/vcs/github-pr-strategy.d.ts +0 -3
  72. package/dist/vcs/github-pr-strategy.js +4 -7
  73. package/dist/vcs/index.d.ts +3 -3
  74. package/dist/vcs/index.js +2 -2
  75. package/dist/vcs/pr-creator.js +9 -9
  76. package/dist/vcs/pr-strategy-factory.d.ts +1 -1
  77. package/dist/vcs/pr-strategy-factory.js +1 -1
  78. package/dist/vcs/pr-strategy.js +6 -11
  79. package/package.json +1 -1
  80. package/dist/output/summary-utils.d.ts +0 -20
  81. package/dist/output/summary-utils.js +0 -79
@@ -1,5 +1,6 @@
1
1
  import { hasLifecycleChanges } from "./lifecycle-report.js";
2
2
  import { writeGitHubStepSummary } from "./github-summary.js";
3
+ import { renderSyncLines } from "./sync-report.js";
3
4
  import { renderRepoSettingsDiffLines, formatCountEntry, } from "./settings-report.js";
4
5
  // =============================================================================
5
6
  // Helpers
@@ -130,25 +131,6 @@ function renderLifecycleLines(lcAction, diffLines) {
130
131
  }
131
132
  }
132
133
  }
133
- export function renderSyncLines(syncRepo, diffLines) {
134
- for (const file of syncRepo.files) {
135
- if (file.action === "create") {
136
- diffLines.push(`+ ${file.path}`);
137
- }
138
- else if (file.action === "update") {
139
- diffLines.push(`! ${file.path}`);
140
- }
141
- else if (file.action === "delete") {
142
- diffLines.push(`- ${file.path}`);
143
- }
144
- if (file.diffLines) {
145
- diffLines.push(...file.diffLines);
146
- }
147
- }
148
- if (syncRepo.error) {
149
- diffLines.push(`- Error: ${syncRepo.error}`);
150
- }
151
- }
152
134
  // =============================================================================
153
135
  // Markdown Formatter
154
136
  // =============================================================================
@@ -35,6 +35,14 @@ interface SettingsGuards<TOptions extends BaseProcessorOptions, TResult extends
35
35
  export declare function withGitHubGuards<TOptions extends BaseProcessorOptions, TResult extends BaseProcessorResult>(repoConfig: RepoConfig, repoInfo: RepoInfo, options: TOptions, guards: SettingsGuards<TOptions, TResult>): Promise<TResult>;
36
36
  /** Common action literals shared by all settings processors. */
37
37
  export type SettingsAction = "create" | "update" | "delete" | "unchanged";
38
+ /** Actions that represent an actual change (excludes "unchanged"). */
39
+ export type ActiveAction = Exclude<SettingsAction, "unchanged">;
40
+ /** Type predicate that narrows entries with an active (non-"unchanged") action. */
41
+ export declare function isActiveAction<T extends {
42
+ action: SettingsAction;
43
+ }>(entry: T): entry is T & {
44
+ action: ActiveAction;
45
+ };
38
46
  export interface ChangeCounts {
39
47
  create: number;
40
48
  update: number;
@@ -6,6 +6,9 @@ import { toErrorMessage } from "../shared/type-guards.js";
6
6
  */
7
7
  export async function withGitHubGuards(repoConfig, repoInfo, options, guards) {
8
8
  const repoName = getRepoDisplayName(repoInfo);
9
+ // Safe cast: all TResult subtypes (RulesetProcessorResult, LabelsProcessorResult,
10
+ // RepoSettingsProcessorResult) only extend BaseProcessorResult with optional fields.
11
+ // If adding a new processor result type, ensure all extension fields are optional.
9
12
  if (!isGitHubRepo(repoInfo)) {
10
13
  return {
11
14
  success: true,
@@ -34,6 +37,10 @@ export async function withGitHubGuards(repoConfig, repoInfo, options, guards) {
34
37
  };
35
38
  }
36
39
  }
40
+ /** Type predicate that narrows entries with an active (non-"unchanged") action. */
41
+ export function isActiveAction(entry) {
42
+ return entry.action !== "unchanged";
43
+ }
37
44
  /**
38
45
  * Count actions from a diff result array.
39
46
  * Works with any change type that has an `action` field.
@@ -1,4 +1,4 @@
1
- export { type ISettingsProcessor, type SettingsAction, } from "./base-processor.js";
2
- export { type PropertyDiff, formatPropertyTree, type RulesetPlanEntry, RulesetProcessor, type IRulesetProcessor, GitHubRulesetStrategy, } from "./rulesets/index.js";
1
+ export { type BaseProcessorResult, type ISettingsProcessor, countActions, isActiveAction, } from "./base-processor.js";
2
+ export { type PropertyDiff, type RulesetPlanEntry, RulesetProcessor, type IRulesetProcessor, GitHubRulesetStrategy, } from "./rulesets/index.js";
3
3
  export { RepoSettingsProcessor, type IRepoSettingsProcessor, type RepoSettingsPlanEntry, GitHubRepoSettingsStrategy, } from "./repo-settings/index.js";
4
4
  export { type LabelsPlanEntry, LabelsProcessor, type ILabelsProcessor, GitHubLabelsStrategy, } from "./labels/index.js";
@@ -1,5 +1,7 @@
1
+ // Base processor
2
+ export { countActions, isActiveAction, } from "./base-processor.js";
1
3
  // Rulesets
2
- export { formatPropertyTree, RulesetProcessor, GitHubRulesetStrategy, } from "./rulesets/index.js";
4
+ export { RulesetProcessor, GitHubRulesetStrategy, } from "./rulesets/index.js";
3
5
  // Repo settings
4
6
  export { RepoSettingsProcessor, GitHubRepoSettingsStrategy, } from "./repo-settings/index.js";
5
7
  // Labels
@@ -1,4 +1,4 @@
1
- import type { Label } from "../../config/types.js";
1
+ import type { Label } from "../../config/index.js";
2
2
  interface GitHubLabelPayload {
3
3
  name: string;
4
4
  new_name?: string;
@@ -1,4 +1,4 @@
1
- import type { Label } from "../../config/types.js";
1
+ import type { Label } from "../../config/index.js";
2
2
  import type { GitHubLabel } from "./types.js";
3
3
  import type { SettingsAction } from "../base-processor.js";
4
4
  export type LabelAction = SettingsAction;
@@ -1,5 +1,5 @@
1
1
  import type { LabelChange, LabelAction } from "./diff.js";
2
- import type { Label } from "../../config/types.js";
2
+ import type { Label } from "../../config/index.js";
3
3
  export interface LabelsPlanEntry {
4
4
  name: string;
5
5
  action: LabelAction;
@@ -57,12 +57,13 @@ export class RepoSettingsProcessor {
57
57
  });
58
58
  }
59
59
  // Apply changes - only send settings that actually changed
60
- const changedSettings = changes.reduce((acc, change) => {
60
+ const changedSettings = {};
61
+ for (const change of changes) {
61
62
  if (change.action !== "unchanged") {
62
- acc[change.property] = change.newValue;
63
+ changedSettings[change.property] =
64
+ change.newValue;
63
65
  }
64
- return acc;
65
- }, {});
66
+ }
66
67
  await this.applyChanges(githubRepo, changedSettings, strategyOptions);
67
68
  const appliedCount = Object.keys(changedSettings).length;
68
69
  return buildApplyResult(repoName, changeCounts, appliedCount, {
@@ -94,7 +95,10 @@ export class RepoSettingsProcessor {
94
95
  }
95
96
  validateSecuritySettings(desiredSettings, currentSettings) {
96
97
  const errors = [];
97
- const isPublic = currentSettings.visibility === "public";
98
+ // Use desired visibility if specified (repo may be transitioning to public),
99
+ // otherwise fall back to current visibility
100
+ const effectiveVisibility = desiredSettings.visibility ?? currentSettings.visibility;
101
+ const isPublic = effectiveVisibility === "public";
98
102
  // privateVulnerabilityReporting is only available on public repos
99
103
  if (desiredSettings.privateVulnerabilityReporting === true && !isPublic) {
100
104
  errors.push("privateVulnerabilityReporting is only available for public repositories");
@@ -121,46 +121,52 @@ function getActionStyle(action) {
121
121
  /**
122
122
  * Render a leaf tree node (no children) with its value.
123
123
  */
124
- function renderLeafNode(child, style, indentStr, indent) {
124
+ function hasComplexValue(value) {
125
+ return (isPlainObject(value) ||
126
+ (Array.isArray(value) && value.some((v) => isPlainObject(v))));
127
+ }
128
+ function renderComplexLeaf(child, style, indentStr, indent) {
125
129
  const lines = [];
126
- const hasComplexNew = isPlainObject(child.newValue) ||
127
- (Array.isArray(child.newValue) &&
128
- child.newValue.some((v) => isPlainObject(v)));
129
- const hasComplexOld = isPlainObject(child.oldValue) ||
130
- (Array.isArray(child.oldValue) &&
131
- child.oldValue.some((v) => isPlainObject(v)));
132
- if (child.action === "add" && hasComplexNew) {
130
+ if (child.action === "add") {
133
131
  lines.push(style.color(`${indentStr}${style.symbol} ${child.name}:`));
134
132
  lines.push(...renderNestedValue(child.newValue, child.action, indent + 1));
135
133
  }
136
- else if (child.action === "remove" && hasComplexOld) {
134
+ else if (child.action === "remove") {
137
135
  lines.push(style.color(`${indentStr}${style.symbol} ${child.name} (removed):`));
138
136
  lines.push(...renderNestedValue(child.oldValue, child.action, indent + 1));
139
137
  }
140
- else if (child.action === "change" && (hasComplexNew || hasComplexOld)) {
138
+ else {
141
139
  lines.push(style.color(`${indentStr}${style.symbol} ${child.name}:`));
142
- if (hasComplexOld) {
140
+ if (hasComplexValue(child.oldValue)) {
143
141
  lines.push(...renderNestedValue(child.oldValue, "remove", indent + 1));
144
142
  }
145
- if (hasComplexNew) {
143
+ if (hasComplexValue(child.newValue)) {
146
144
  lines.push(...renderNestedValue(child.newValue, "add", indent + 1));
147
145
  }
148
146
  }
149
- else {
150
- let valuePart = "";
151
- if (child.action === "change") {
152
- valuePart = `: ${formatValue(child.oldValue)} → ${formatValue(child.newValue)}`;
153
- }
154
- else if (child.action === "add") {
155
- valuePart = `: ${formatValue(child.newValue)}`;
156
- }
157
- else if (child.action === "remove") {
158
- valuePart = ` (was: ${formatValue(child.oldValue)})`;
159
- }
160
- lines.push(style.color(`${indentStr}${style.symbol} ${child.name}${valuePart}`));
161
- }
162
147
  return lines;
163
148
  }
149
+ function renderSimpleLeaf(child, style, indentStr) {
150
+ let valuePart = "";
151
+ if (child.action === "change") {
152
+ valuePart = `: ${formatValue(child.oldValue)} → ${formatValue(child.newValue)}`;
153
+ }
154
+ else if (child.action === "add") {
155
+ valuePart = `: ${formatValue(child.newValue)}`;
156
+ }
157
+ else if (child.action === "remove") {
158
+ valuePart = ` (was: ${formatValue(child.oldValue)})`;
159
+ }
160
+ return style.color(`${indentStr}${style.symbol} ${child.name}${valuePart}`);
161
+ }
162
+ function renderLeafNode(child, style, indentStr, indent) {
163
+ const isComplex = (child.action !== "remove" && hasComplexValue(child.newValue)) ||
164
+ (child.action !== "add" && hasComplexValue(child.oldValue));
165
+ if (isComplex) {
166
+ return renderComplexLeaf(child, style, indentStr, indent);
167
+ }
168
+ return [renderSimpleLeaf(child, style, indentStr)];
169
+ }
164
170
  /**
165
171
  * Recursively render tree nodes to formatted lines.
166
172
  */
@@ -198,85 +204,91 @@ export function formatPropertyTree(diffs) {
198
204
  * Delegates to renderNestedObject which handles recursive rendering.
199
205
  */
200
206
  function formatFullConfig(ruleset, indent = 2) {
201
- return renderNestedObject(ruleset, "add", indent);
207
+ // Object.entries works on any object; the cast avoids a double assertion
208
+ const entries = Object.entries(ruleset);
209
+ return renderNestedObject(Object.fromEntries(entries), "add", indent);
202
210
  }
203
211
  /**
204
212
  * Format ruleset changes as a Terraform-style plan.
205
213
  */
206
214
  export function formatRulesetPlan(changes) {
207
215
  const lines = [];
208
- let creates = 0;
209
- let updates = 0;
210
- let deletes = 0;
211
- let unchanged = 0;
212
216
  const entries = [];
213
- // Group by action type
214
- const createChanges = changes.filter((c) => c.action === "create");
215
- const updateChanges = changes.filter((c) => c.action === "update");
216
- const deleteChanges = changes.filter((c) => c.action === "delete");
217
- const unchangedItems = changes.filter((c) => c.action === "unchanged");
218
- creates = createChanges.length;
219
- updates = updateChanges.length;
220
- deletes = deleteChanges.length;
221
- unchanged = unchangedItems.length;
222
- if (createChanges.length > 0) {
223
- lines.push(chalk.bold(" Create:"));
224
- for (const change of createChanges) {
225
- lines.push(chalk.green(` + ruleset "${change.name}"`));
226
- if (change.desired) {
227
- lines.push(...formatFullConfig(change.desired, 2));
217
+ // Group by action in a single pass
218
+ const grouped = {
219
+ create: [],
220
+ update: [],
221
+ delete: [],
222
+ unchanged: [],
223
+ };
224
+ for (const c of changes) {
225
+ grouped[c.action].push(c);
226
+ }
227
+ for (const change of grouped.create) {
228
+ if (grouped.create.indexOf(change) === 0) {
229
+ lines.push(chalk.bold(" Create:"));
230
+ }
231
+ lines.push(chalk.green(` + ruleset "${change.name}"`));
232
+ if (change.desired) {
233
+ lines.push(...formatFullConfig(change.desired, 2));
234
+ }
235
+ const propertyCount = change.desired
236
+ ? Object.keys(change.desired).length
237
+ : 0;
238
+ entries.push({
239
+ name: change.name,
240
+ action: "create",
241
+ propertyCount,
242
+ config: change.desired,
243
+ });
244
+ lines.push("");
245
+ }
246
+ for (const change of grouped.update) {
247
+ if (grouped.update.indexOf(change) === 0) {
248
+ lines.push(chalk.bold(" Update:"));
249
+ }
250
+ lines.push(chalk.yellow(` ~ ruleset "${change.name}"`));
251
+ if (change.current && change.desired) {
252
+ const currentNorm = normalizeRuleset(change.current);
253
+ const desiredNorm = normalizeRuleset(change.desired);
254
+ const projectedCurrent = projectToDesiredShape(currentNorm, desiredNorm);
255
+ const diffs = computePropertyDiffs(projectedCurrent, desiredNorm);
256
+ const treeLines = formatPropertyTree(diffs);
257
+ for (const line of treeLines) {
258
+ lines.push(` ${line}`);
228
259
  }
229
- const propertyCount = change.desired
230
- ? Object.keys(change.desired).length
231
- : 0;
260
+ const added = diffs.filter((d) => d.action === "add").length;
261
+ const changed = diffs.filter((d) => d.action === "change").length;
262
+ const removed = diffs.filter((d) => d.action === "remove").length;
232
263
  entries.push({
233
264
  name: change.name,
234
- action: "create",
235
- propertyCount,
236
- config: change.desired,
265
+ action: "update",
266
+ propertyChanges: { added, changed, removed },
267
+ propertyDiffs: diffs,
237
268
  });
238
- lines.push(""); // Blank line between rulesets
239
269
  }
240
- }
241
- if (updateChanges.length > 0) {
242
- lines.push(chalk.bold(" Update:"));
243
- for (const change of updateChanges) {
244
- lines.push(chalk.yellow(` ~ ruleset "${change.name}"`));
245
- if (change.current && change.desired) {
246
- const currentNorm = normalizeRuleset(change.current);
247
- const desiredNorm = normalizeRuleset(change.desired);
248
- const projectedCurrent = projectToDesiredShape(currentNorm, desiredNorm);
249
- const diffs = computePropertyDiffs(projectedCurrent, desiredNorm);
250
- const treeLines = formatPropertyTree(diffs);
251
- for (const line of treeLines) {
252
- lines.push(` ${line}`);
253
- }
254
- const added = diffs.filter((d) => d.action === "add").length;
255
- const changed = diffs.filter((d) => d.action === "change").length;
256
- const removed = diffs.filter((d) => d.action === "remove").length;
257
- entries.push({
258
- name: change.name,
259
- action: "update",
260
- propertyChanges: { added, changed, removed },
261
- propertyDiffs: diffs,
262
- });
263
- }
264
- else {
265
- entries.push({ name: change.name, action: "update" });
266
- }
267
- lines.push(""); // Blank line between rulesets
270
+ else {
271
+ entries.push({ name: change.name, action: "update" });
268
272
  }
273
+ lines.push("");
269
274
  }
270
- if (deleteChanges.length > 0) {
275
+ if (grouped.delete.length > 0) {
271
276
  lines.push(chalk.bold(" Delete:"));
272
- for (const change of deleteChanges) {
277
+ for (const change of grouped.delete) {
273
278
  lines.push(chalk.red(` - ruleset "${change.name}"`));
274
279
  entries.push({ name: change.name, action: "delete" });
275
280
  }
276
- lines.push(""); // Blank line after deletes
281
+ lines.push("");
277
282
  }
278
- for (const change of unchangedItems) {
283
+ for (const change of grouped.unchanged) {
279
284
  entries.push({ name: change.name, action: "unchanged" });
280
285
  }
281
- return { lines, creates, updates, deletes, unchanged, entries };
286
+ return {
287
+ lines,
288
+ creates: grouped.create.length,
289
+ updates: grouped.update.length,
290
+ deletes: grouped.delete.length,
291
+ unchanged: grouped.unchanged.length,
292
+ entries,
293
+ };
282
294
  }
@@ -2,7 +2,7 @@ import { ICommandExecutor } from "../../shared/command-executor.js";
2
2
  import { RepoInfo } from "../../shared/repo-detector.js";
3
3
  import { type GhApiOptions } from "../../shared/gh-api-utils.js";
4
4
  import type { Ruleset } from "../../config/index.js";
5
- import type { IRulesetStrategy, GitHubRuleset, GitHubBypassActor, GitHubRulesetConditions, GitHubRule } from "./types.js";
5
+ import type { IRulesetStrategy, GitHubRuleset, GitHubBypassActor, GitHubRulesetConditions, GitHubRule, RulesetUpdateParams } from "./types.js";
6
6
  /**
7
7
  * Converts camelCase config ruleset to snake_case GitHub API format.
8
8
  */
@@ -25,7 +25,7 @@ export declare class GitHubRulesetStrategy implements IRulesetStrategy {
25
25
  list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubRuleset[]>;
26
26
  get(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<GitHubRuleset>;
27
27
  create(repoInfo: RepoInfo, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
28
- update(repoInfo: RepoInfo, rulesetId: number, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
28
+ update(repoInfo: RepoInfo, params: RulesetUpdateParams, options?: GhApiOptions): Promise<GitHubRuleset>;
29
29
  delete(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<void>;
30
30
  }
31
31
  export {};
@@ -118,11 +118,14 @@ export class GitHubRulesetStrategy {
118
118
  const result = await this.api.call("POST", endpoint, { payload, options });
119
119
  return parseApiJson(result, "ruleset response");
120
120
  }
121
- async update(repoInfo, rulesetId, name, ruleset, options) {
121
+ async update(repoInfo, params, options) {
122
122
  assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
123
- const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${rulesetId}`;
124
- const payload = configToGitHub(name, ruleset);
125
- const result = await this.api.call("PUT", endpoint, { payload, options });
123
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${params.rulesetId}`;
124
+ const payload = configToGitHub(params.name, params.ruleset);
125
+ const result = await this.api.call("PUT", endpoint, {
126
+ payload,
127
+ options,
128
+ });
126
129
  return parseApiJson(result, "ruleset response");
127
130
  }
128
131
  async delete(repoInfo, rulesetId, options) {
@@ -1,4 +1,4 @@
1
1
  export { computePropertyDiffs, deepEqual, isArrayOfObjects, type PropertyDiff, } from "./diff-algorithm.js";
2
- export { formatPropertyTree, type RulesetPlanEntry } from "./formatter.js";
2
+ export { type RulesetPlanEntry } from "./formatter.js";
3
3
  export { RulesetProcessor, type IRulesetProcessor } from "./processor.js";
4
4
  export { GitHubRulesetStrategy } from "./github-ruleset-strategy.js";
@@ -1,7 +1,5 @@
1
1
  // Diff algorithm - property-level diffing for ruleset comparisons
2
2
  export { computePropertyDiffs, deepEqual, isArrayOfObjects, } from "./diff-algorithm.js";
3
- // Formatter
4
- export { formatPropertyTree } from "./formatter.js";
5
3
  // Processor
6
4
  export { RulesetProcessor } from "./processor.js";
7
5
  // Strategy
@@ -56,7 +56,11 @@ export class RulesetProcessor {
56
56
  break;
57
57
  case "update":
58
58
  if (change.rulesetId !== undefined && change.desired) {
59
- await this.strategy.update(githubRepo, change.rulesetId, change.name, change.desired, strategyOptions);
59
+ await this.strategy.update(githubRepo, {
60
+ rulesetId: change.rulesetId,
61
+ name: change.name,
62
+ ruleset: change.desired,
63
+ }, strategyOptions);
60
64
  appliedCount++;
61
65
  }
62
66
  break;
@@ -30,10 +30,15 @@ export interface GitHubRule {
30
30
  type: string;
31
31
  parameters?: Record<string, unknown>;
32
32
  }
33
+ export interface RulesetUpdateParams {
34
+ rulesetId: number;
35
+ name: string;
36
+ ruleset: Ruleset;
37
+ }
33
38
  export interface IRulesetStrategy {
34
39
  list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubRuleset[]>;
35
40
  get(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<GitHubRuleset>;
36
41
  create(repoInfo: RepoInfo, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
37
- update(repoInfo: RepoInfo, rulesetId: number, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
42
+ update(repoInfo: RepoInfo, params: RulesetUpdateParams, options?: GhApiOptions): Promise<GitHubRuleset>;
38
43
  delete(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<void>;
39
44
  }
@@ -31,7 +31,7 @@ export declare class GhApiClient {
31
31
  constructor(executor: ICommandExecutor, retries: number, cwd: string);
32
32
  call(method: HttpMethod, endpoint: string, params?: GhApiCallParams): Promise<string>;
33
33
  }
34
- export interface ResolveGitHubTokenOptions {
34
+ interface ResolveGitHubTokenOptions {
35
35
  repoInfo: GitHubRepoInfo;
36
36
  tokenManager: ITokenManager | null;
37
37
  context: string;
@@ -27,10 +27,10 @@ export interface ILogger {
27
27
  fileDiff(fileName: string, status: FileStatus, diffLines: string[]): void;
28
28
  diffSummary(newCount: number, modifiedCount: number, unchangedCount: number, deletedCount?: number): void;
29
29
  setTotal(total: number): void;
30
- progress(current: number, repoName: string, message: string): void;
31
- success(current: number, repoName: string, message: string): void;
32
- skip(current: number, repoName: string, reason: string): void;
33
- error(current: number, repoName: string, error: string): void;
30
+ progress(repoNumber: number, repoName: string, message: string): void;
31
+ success(repoNumber: number, repoName: string, message: string): void;
32
+ skip(repoNumber: number, repoName: string, reason: string): void;
33
+ error(repoNumber: number, repoName: string, error: string): void;
34
34
  }
35
35
  export declare class Logger implements ILogger {
36
36
  private readonly debugEnabled;
@@ -38,13 +38,13 @@ export declare class Logger implements ILogger {
38
38
  constructor(debugEnabled?: boolean);
39
39
  log(message: string): void;
40
40
  setTotal(total: number): void;
41
- progress(current: number, repoName: string, message: string): void;
41
+ progress(repoNumber: number, repoName: string, message: string): void;
42
42
  info(message: string): void;
43
43
  warn(message: string): void;
44
44
  debug(message: string): void;
45
- success(current: number, repoName: string, message: string): void;
46
- skip(current: number, repoName: string, reason: string): void;
47
- error(current: number, repoName: string, error: string): void;
45
+ success(repoNumber: number, repoName: string, message: string): void;
46
+ skip(repoNumber: number, repoName: string, reason: string): void;
47
+ error(repoNumber: number, repoName: string, error: string): void;
48
48
  /**
49
49
  * Display a file diff with status badge.
50
50
  * Used in dry-run mode to show what would change.
@@ -17,8 +17,8 @@ export class Logger {
17
17
  setTotal(total) {
18
18
  this.stats.total = total;
19
19
  }
20
- progress(current, repoName, message) {
21
- console.log(chalk.blue(`[${current}/${this.stats.total}]`) +
20
+ progress(repoNumber, repoName, message) {
21
+ console.log(chalk.blue(`[${repoNumber}/${this.stats.total}]`) +
22
22
  ` ${repoName}: ${message}`);
23
23
  }
24
24
  info(message) {
@@ -32,19 +32,20 @@ export class Logger {
32
32
  console.log(chalk.dim(` [debug] ${message}`));
33
33
  }
34
34
  }
35
- success(current, repoName, message) {
35
+ success(repoNumber, repoName, message) {
36
36
  this.stats.succeeded++;
37
- console.log(chalk.green(`[${current}/${this.stats.total}] ✓`) +
37
+ console.log(chalk.green(`[${repoNumber}/${this.stats.total}] ✓`) +
38
38
  ` ${repoName}: ${message}`);
39
39
  }
40
- skip(current, repoName, reason) {
40
+ skip(repoNumber, repoName, reason) {
41
41
  this.stats.skipped++;
42
- console.log(chalk.yellow(`[${current}/${this.stats.total}] ⊘`) +
42
+ console.log(chalk.yellow(`[${repoNumber}/${this.stats.total}] ⊘`) +
43
43
  ` ${repoName}: Skipped - ${reason}`);
44
44
  }
45
- error(current, repoName, error) {
45
+ error(repoNumber, repoName, error) {
46
46
  this.stats.failed++;
47
- console.log(chalk.red(`[${current}/${this.stats.total}] ✗`) + ` ${repoName}: ${error}`);
47
+ console.log(chalk.red(`[${repoNumber}/${this.stats.total}] ✗`) +
48
+ ` ${repoName}: ${error}`);
48
49
  }
49
50
  /**
50
51
  * Display a file diff with status badge.
@@ -1,5 +1,4 @@
1
1
  import { ValidationError } from "./errors.js";
2
- // Type guards
3
2
  export function isGitHubRepo(info) {
4
3
  return info.type === "github";
5
4
  }
@@ -1,8 +1,8 @@
1
- import { getPRStrategy } from "../vcs/index.js";
1
+ import { createPRStrategy } from "../vcs/index.js";
2
2
  export class BranchManager {
3
3
  log;
4
4
  prStrategyFactory;
5
- constructor(log, prStrategyFactory = getPRStrategy) {
5
+ constructor(log, prStrategyFactory = createPRStrategy) {
6
6
  this.log = log;
7
7
  this.prStrategyFactory = prStrategyFactory;
8
8
  }
@@ -1,10 +1,10 @@
1
- import { getCommitStrategy } from "../vcs/index.js";
1
+ import { createCommitStrategy } from "../vcs/index.js";
2
2
  import { getRepoDisplayName } from "../shared/repo-detector.js";
3
3
  import { toErrorMessage } from "../shared/type-guards.js";
4
4
  export class CommitPushManager {
5
5
  log;
6
6
  commitStrategyFactory;
7
- constructor(log, commitStrategyFactory = getCommitStrategy) {
7
+ constructor(log, commitStrategyFactory = createCommitStrategy) {
8
8
  this.log = log;
9
9
  this.commitStrategyFactory = commitStrategyFactory;
10
10
  }
@@ -2,9 +2,6 @@ export type { FileStatus } from "../shared/file-status.js";
2
2
  export { formatStatusBadge } from "../shared/file-status.js";
3
3
  import type { FileStatus } from "../shared/file-status.js";
4
4
  export declare function getFileStatus(exists: boolean, changed: boolean): FileStatus;
5
- /**
6
- * Format a single diff line with appropriate color.
7
- */
8
5
  export declare function formatDiffLine(line: string): string;
9
6
  /**
10
7
  * Check if a file is likely binary based on its extension.
@@ -30,11 +27,5 @@ export interface DiffStats {
30
27
  unchangedCount: number;
31
28
  deletedCount: number;
32
29
  }
33
- /**
34
- * Create an empty diff stats object.
35
- */
36
30
  export declare function createDiffStats(): DiffStats;
37
- /**
38
- * Increment the appropriate counter in diff stats.
39
- */
40
31
  export declare function incrementDiffStats(stats: DiffStats, status: FileStatus): void;
@@ -5,9 +5,6 @@ export function getFileStatus(exists, changed) {
5
5
  return "NEW";
6
6
  return changed ? "MODIFIED" : "UNCHANGED";
7
7
  }
8
- /**
9
- * Format a single diff line with appropriate color.
10
- */
11
8
  export function formatDiffLine(line) {
12
9
  if (line.startsWith("+"))
13
10
  return chalk.green(line);
@@ -261,15 +258,9 @@ function groupIntoHunks(ops, oldLines, newLines, contextLines) {
261
258
  }
262
259
  return hunks;
263
260
  }
264
- /**
265
- * Create an empty diff stats object.
266
- */
267
261
  export function createDiffStats() {
268
262
  return { newCount: 0, modifiedCount: 0, unchangedCount: 0, deletedCount: 0 };
269
263
  }
270
- /**
271
- * Increment the appropriate counter in diff stats.
272
- */
273
264
  export function incrementDiffStats(stats, status) {
274
265
  switch (status) {
275
266
  case "NEW":
@@ -22,7 +22,7 @@ export class FileSyncOrchestrator {
22
22
  hasAppCredentials: options.hasAppCredentials,
23
23
  }, { gitOps: session.gitOps, log: this.log });
24
24
  const filesWithDeleteOrphaned = new Map(repoConfig.files.map((f) => [f.fileName, f.deleteOrphaned]));
25
- const { manifest: newManifest, existingManifest, filesToDelete, } = this.manifestManager.processOrphans(workDir, configId, filesWithDeleteOrphaned);
25
+ const { manifest: newManifest, existingManifest, filesToDelete, } = this.manifestManager.detectOrphans(workDir, configId, filesWithDeleteOrphaned);
26
26
  this.manifestManager.deleteOrphans(filesToDelete, { dryRun: dryRun, noDelete: noDelete }, { gitOps: session.gitOps, log: this.log, fileChanges });
27
27
  // Save manifest (may add to fileChanges)
28
28
  this.manifestManager.saveUpdatedManifest(workDir, newManifest, existingManifest, dryRun, fileChanges);
@@ -1,4 +1,4 @@
1
- import type { RepoConfig } from "../config/types.js";
1
+ import type { RepoConfig } from "../config/index.js";
2
2
  import type { RepoInfo } from "../shared/repo-detector.js";
3
3
  import type { IWorkStrategy, WorkResult, SessionContext, ProcessorOptions, IFileSyncOrchestrator } from "./types.js";
4
4
  /**