@aspruyt/xfg 3.8.2 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cli/settings-command.d.ts +2 -1
  2. package/dist/cli/settings-command.js +82 -7
  3. package/dist/cli/sync-command.d.ts +2 -1
  4. package/dist/cli/sync-command.js +80 -5
  5. package/dist/config/normalizer.js +2 -0
  6. package/dist/config/types.d.ts +9 -0
  7. package/dist/config/validator.js +62 -0
  8. package/dist/lifecycle/ado-migration-source.d.ts +15 -0
  9. package/dist/lifecycle/ado-migration-source.js +37 -0
  10. package/dist/lifecycle/github-lifecycle-provider.d.ts +56 -0
  11. package/dist/lifecycle/github-lifecycle-provider.js +314 -0
  12. package/dist/lifecycle/index.d.ts +7 -0
  13. package/dist/lifecycle/index.js +6 -0
  14. package/dist/lifecycle/lifecycle-formatter.d.ts +14 -0
  15. package/dist/lifecycle/lifecycle-formatter.js +34 -0
  16. package/dist/lifecycle/lifecycle-helpers.d.ts +27 -0
  17. package/dist/lifecycle/lifecycle-helpers.js +47 -0
  18. package/dist/lifecycle/repo-lifecycle-factory.d.ts +14 -0
  19. package/dist/lifecycle/repo-lifecycle-factory.js +57 -0
  20. package/dist/lifecycle/repo-lifecycle-manager.d.ts +20 -0
  21. package/dist/lifecycle/repo-lifecycle-manager.js +139 -0
  22. package/dist/lifecycle/types.d.ts +104 -0
  23. package/dist/lifecycle/types.js +1 -0
  24. package/dist/output/lifecycle-report.d.ts +37 -0
  25. package/dist/output/lifecycle-report.js +146 -0
  26. package/dist/output/settings-report.js +26 -26
  27. package/dist/output/sync-report.js +5 -5
  28. package/dist/output/unified-summary.d.ts +4 -0
  29. package/dist/output/unified-summary.js +147 -0
  30. package/dist/settings/repo-settings/diff.js +1 -0
  31. package/dist/settings/repo-settings/github-repo-settings-strategy.js +1 -0
  32. package/dist/shared/logger.d.ts +2 -0
  33. package/dist/shared/logger.js +5 -0
  34. package/dist/sync/manifest-strategy.js +3 -1
  35. package/dist/vcs/authenticated-git-ops.js +1 -1
  36. package/dist/vcs/git-ops.js +1 -1
  37. package/package.json +4 -2
@@ -0,0 +1,139 @@
1
+ import { join } from "node:path";
2
+ import { rm } from "node:fs/promises";
3
+ import { parseGitUrl } from "../shared/repo-detector.js";
4
+ import { logger } from "../shared/logger.js";
5
+ import { RepoLifecycleFactory } from "./repo-lifecycle-factory.js";
6
+ /**
7
+ * Orchestrates repo lifecycle operations before sync.
8
+ */
9
+ export class RepoLifecycleManager {
10
+ factory;
11
+ constructor(factory, retries) {
12
+ this.factory = factory ?? new RepoLifecycleFactory(undefined, retries);
13
+ }
14
+ async ensureRepo(repoConfig, repoInfo, options, settings) {
15
+ let provider;
16
+ try {
17
+ provider = this.factory.getProvider(repoInfo.type);
18
+ }
19
+ catch (error) {
20
+ // If user explicitly configured lifecycle (upstream/source), propagate the error
21
+ if (repoConfig.upstream || repoConfig.source) {
22
+ throw error;
23
+ }
24
+ // Platform doesn't support lifecycle operations yet - skip silently
25
+ return { repoInfo, action: "existed" };
26
+ }
27
+ const { token } = options;
28
+ // Check if repo exists
29
+ const exists = await provider.exists(repoInfo, token);
30
+ if (exists) {
31
+ // Repo exists - nothing to do (ignore upstream/source)
32
+ return {
33
+ repoInfo,
34
+ action: "existed",
35
+ };
36
+ }
37
+ // Repo doesn't exist - determine what action to take
38
+ if (repoConfig.source) {
39
+ // Migration mode
40
+ return this.migrate(repoConfig, repoInfo, options, settings);
41
+ }
42
+ if (repoConfig.upstream) {
43
+ // Fork mode
44
+ return this.fork(repoConfig, repoInfo, provider, options, settings);
45
+ }
46
+ // Create mode (no upstream or source)
47
+ return this.create(repoInfo, provider, options, settings);
48
+ }
49
+ async create(repoInfo, provider, options, settings) {
50
+ if (options.dryRun) {
51
+ return {
52
+ repoInfo,
53
+ action: "created",
54
+ skipped: true,
55
+ };
56
+ }
57
+ await provider.create(repoInfo, settings, options.token);
58
+ await this.waitForRepoReady(provider, repoInfo, options.token);
59
+ return {
60
+ repoInfo,
61
+ action: "created",
62
+ };
63
+ }
64
+ async fork(repoConfig, repoInfo, provider, options, settings) {
65
+ if (options.dryRun) {
66
+ return {
67
+ repoInfo,
68
+ action: "forked",
69
+ skipped: true,
70
+ };
71
+ }
72
+ if (!provider.fork) {
73
+ throw new Error(`Platform '${repoInfo.type}' does not support forking`);
74
+ }
75
+ // Parse upstream URL to get repo info
76
+ const upstreamInfo = parseGitUrl(repoConfig.upstream, {
77
+ githubHosts: options.githubHosts,
78
+ });
79
+ await provider.fork(upstreamInfo, repoInfo, settings, options.token);
80
+ await this.waitForRepoReady(provider, repoInfo, options.token);
81
+ return {
82
+ repoInfo,
83
+ action: "forked",
84
+ };
85
+ }
86
+ async migrate(repoConfig, repoInfo, options, settings) {
87
+ if (options.dryRun) {
88
+ return {
89
+ repoInfo,
90
+ action: "migrated",
91
+ skipped: true,
92
+ };
93
+ }
94
+ // Parse source URL to get platform and repo info
95
+ const sourceInfo = parseGitUrl(repoConfig.source, {
96
+ githubHosts: options.githubHosts,
97
+ });
98
+ const source = this.factory.getMigrationSource(sourceInfo.type);
99
+ // Clone source repo to temp directory
100
+ const sourceDir = join(options.workDir, "migration-source");
101
+ try {
102
+ await source.cloneForMigration(sourceInfo, sourceDir);
103
+ // Create target and push content
104
+ const provider = this.factory.getProvider(repoInfo.type);
105
+ await provider.receiveMigration(repoInfo, sourceDir, settings, options.token);
106
+ await this.waitForRepoReady(provider, repoInfo, options.token);
107
+ return {
108
+ repoInfo,
109
+ action: "migrated",
110
+ };
111
+ }
112
+ finally {
113
+ // Clean up migration source directory
114
+ try {
115
+ await rm(sourceDir, { recursive: true, force: true });
116
+ }
117
+ catch (cleanupError) {
118
+ // Log cleanup errors at debug level for troubleshooting
119
+ logger.debug(`Failed to clean up migration source directory ${sourceDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
120
+ }
121
+ }
122
+ }
123
+ /**
124
+ * Polls provider.exists() until the repo is visible, with timeout.
125
+ * GitHub's API may return success from create/fork before the git
126
+ * backend has fully propagated, causing subsequent clone to 403.
127
+ */
128
+ async waitForRepoReady(provider, repoInfo, token, timeoutMs = 15000, pollMs = 1000) {
129
+ const start = Date.now();
130
+ while (Date.now() - start < timeoutMs) {
131
+ if (await provider.exists(repoInfo, token)) {
132
+ return;
133
+ }
134
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
135
+ }
136
+ // Timed out — proceed anyway and let downstream operations handle it
137
+ logger.info(`Repo ${repoInfo.owner}/${repoInfo.repo} not yet visible after ${timeoutMs}ms, proceeding`);
138
+ }
139
+ }
@@ -0,0 +1,104 @@
1
+ import type { RepoInfo } from "../shared/repo-detector.js";
2
+ import type { RepoConfig } from "../config/types.js";
3
+ /**
4
+ * Supported platforms for lifecycle operations.
5
+ */
6
+ export type LifecyclePlatform = "github" | "azure-devops" | "gitlab";
7
+ /**
8
+ * Result of a lifecycle operation.
9
+ */
10
+ export interface LifecycleResult {
11
+ /** The repo info (may be updated) */
12
+ repoInfo: RepoInfo;
13
+ /** What action was taken */
14
+ action: "existed" | "created" | "forked" | "migrated";
15
+ /** True if skipped due to dry-run */
16
+ skipped?: boolean;
17
+ }
18
+ /**
19
+ * Options for lifecycle operations.
20
+ */
21
+ export interface LifecycleOptions {
22
+ /** Dry-run mode - don't make changes */
23
+ dryRun: boolean;
24
+ /** Working directory for git operations */
25
+ workDir: string;
26
+ /** GitHub Enterprise hostnames for URL detection */
27
+ githubHosts?: string[];
28
+ /** Auth token (GitHub App installation token or PAT) for gh CLI commands */
29
+ token?: string;
30
+ }
31
+ /**
32
+ * Repo settings to apply when creating a new repo.
33
+ * Subset of GitHubRepoSettings that makes sense for creation.
34
+ */
35
+ export interface CreateRepoSettings {
36
+ visibility?: "public" | "private" | "internal";
37
+ description?: string;
38
+ hasIssues?: boolean;
39
+ hasWiki?: boolean;
40
+ }
41
+ /**
42
+ * Provider for platform-specific lifecycle operations.
43
+ * Implementations handle create/fork/receive for a specific platform.
44
+ */
45
+ export interface IRepoLifecycleProvider {
46
+ /** Platform this provider handles */
47
+ readonly platform: LifecyclePlatform;
48
+ /**
49
+ * Check if a repository exists on this platform.
50
+ * @throws Error on network/auth failures (NOT for "repo not found")
51
+ */
52
+ exists(repoInfo: RepoInfo, token?: string): Promise<boolean>;
53
+ /**
54
+ * Create an empty repository.
55
+ */
56
+ create(repoInfo: RepoInfo, settings?: CreateRepoSettings, token?: string): Promise<void>;
57
+ /**
58
+ * Fork from an upstream repository.
59
+ * Optional - not all platforms support forking.
60
+ */
61
+ fork?(upstream: RepoInfo, target: RepoInfo, settings?: CreateRepoSettings, token?: string): Promise<void>;
62
+ /**
63
+ * Receive migrated content (repo already created, push content).
64
+ */
65
+ receiveMigration(repoInfo: RepoInfo, sourceDir: string, settings?: CreateRepoSettings, token?: string): Promise<void>;
66
+ }
67
+ /**
68
+ * Source for migration operations.
69
+ * Implementations handle cloning from a source platform.
70
+ */
71
+ export interface IMigrationSource {
72
+ /** Platform this source handles */
73
+ readonly platform: LifecyclePlatform;
74
+ /**
75
+ * Clone repository with all refs for migration.
76
+ * Uses --mirror to get all branches/tags.
77
+ */
78
+ cloneForMigration(repoInfo: RepoInfo, workDir: string): Promise<void>;
79
+ }
80
+ /**
81
+ * Factory for getting providers by platform.
82
+ */
83
+ export interface IRepoLifecycleFactory {
84
+ /**
85
+ * Get lifecycle provider for a platform.
86
+ * @throws Error if platform not supported as target
87
+ */
88
+ getProvider(platform: LifecyclePlatform): IRepoLifecycleProvider;
89
+ /**
90
+ * Get migration source for a platform.
91
+ * @throws Error if platform not supported as source
92
+ */
93
+ getMigrationSource(platform: LifecyclePlatform): IMigrationSource;
94
+ }
95
+ /**
96
+ * Manager that orchestrates lifecycle operations before sync.
97
+ */
98
+ export interface IRepoLifecycleManager {
99
+ /**
100
+ * Ensure repository exists, creating/forking/migrating if needed.
101
+ * Call this before sync/settings operations.
102
+ */
103
+ ensureRepo(repoConfig: RepoConfig, repoInfo: RepoInfo, options: LifecycleOptions, settings?: CreateRepoSettings): Promise<LifecycleResult>;
104
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ export interface LifecycleReport {
2
+ actions: LifecycleAction[];
3
+ totals: {
4
+ created: number;
5
+ forked: number;
6
+ migrated: number;
7
+ existed: number;
8
+ };
9
+ }
10
+ export interface LifecycleAction {
11
+ repoName: string;
12
+ action: "existed" | "created" | "forked" | "migrated";
13
+ upstream?: string;
14
+ source?: string;
15
+ settings?: {
16
+ visibility?: string;
17
+ description?: string;
18
+ };
19
+ }
20
+ export interface LifecycleReportInput {
21
+ repoName: string;
22
+ action: "existed" | "created" | "forked" | "migrated";
23
+ upstream?: string;
24
+ source?: string;
25
+ settings?: {
26
+ visibility?: string;
27
+ description?: string;
28
+ };
29
+ }
30
+ export declare function buildLifecycleReport(results: LifecycleReportInput[]): LifecycleReport;
31
+ /**
32
+ * Returns true if the report has any non-"existed" actions worth displaying.
33
+ */
34
+ export declare function hasLifecycleChanges(report: LifecycleReport): boolean;
35
+ export declare function formatLifecycleReportCLI(report: LifecycleReport): string[];
36
+ export declare function formatLifecycleReportMarkdown(report: LifecycleReport, dryRun: boolean): string;
37
+ export declare function writeLifecycleReportSummary(report: LifecycleReport, dryRun: boolean): void;
@@ -0,0 +1,146 @@
1
+ // src/output/lifecycle-report.ts
2
+ import { appendFileSync } from "node:fs";
3
+ import chalk from "chalk";
4
+ // =============================================================================
5
+ // Builder
6
+ // =============================================================================
7
+ export function buildLifecycleReport(results) {
8
+ const actions = [];
9
+ const totals = { created: 0, forked: 0, migrated: 0, existed: 0 };
10
+ for (const result of results) {
11
+ actions.push({
12
+ repoName: result.repoName,
13
+ action: result.action,
14
+ upstream: result.upstream,
15
+ source: result.source,
16
+ settings: result.settings,
17
+ });
18
+ totals[result.action]++;
19
+ }
20
+ return { actions, totals };
21
+ }
22
+ // =============================================================================
23
+ // Helpers
24
+ // =============================================================================
25
+ function formatSummary(totals) {
26
+ const total = totals.created + totals.forked + totals.migrated;
27
+ if (total === 0) {
28
+ return "No changes";
29
+ }
30
+ const parts = [];
31
+ if (totals.created > 0)
32
+ parts.push(`${totals.created} to create`);
33
+ if (totals.forked > 0)
34
+ parts.push(`${totals.forked} to fork`);
35
+ if (totals.migrated > 0)
36
+ parts.push(`${totals.migrated} to migrate`);
37
+ const repoWord = total === 1 ? "repo" : "repos";
38
+ return `Plan: ${total} ${repoWord} (${parts.join(", ")})`;
39
+ }
40
+ /**
41
+ * Returns true if the report has any non-"existed" actions worth displaying.
42
+ */
43
+ export function hasLifecycleChanges(report) {
44
+ return report.actions.some((a) => a.action !== "existed");
45
+ }
46
+ // =============================================================================
47
+ // CLI Formatter
48
+ // =============================================================================
49
+ export function formatLifecycleReportCLI(report) {
50
+ if (!hasLifecycleChanges(report)) {
51
+ return [];
52
+ }
53
+ const lines = [];
54
+ for (const action of report.actions) {
55
+ if (action.action === "existed")
56
+ continue;
57
+ switch (action.action) {
58
+ case "created":
59
+ lines.push(chalk.green(`+ CREATE ${action.repoName}`));
60
+ break;
61
+ case "forked":
62
+ lines.push(chalk.green(`+ FORK ${action.upstream ?? "upstream"} -> ${action.repoName}`));
63
+ break;
64
+ case "migrated":
65
+ lines.push(chalk.green(`+ MIGRATE ${action.source ?? "source"} -> ${action.repoName}`));
66
+ break;
67
+ }
68
+ if (action.settings) {
69
+ if (action.settings.visibility) {
70
+ lines.push(` visibility: ${action.settings.visibility}`);
71
+ }
72
+ if (action.settings.description) {
73
+ lines.push(` description: "${action.settings.description}"`);
74
+ }
75
+ }
76
+ }
77
+ lines.push("");
78
+ // Summary
79
+ lines.push(formatSummary(report.totals));
80
+ return lines;
81
+ }
82
+ // =============================================================================
83
+ // Markdown Formatter
84
+ // =============================================================================
85
+ export function formatLifecycleReportMarkdown(report, dryRun) {
86
+ if (!hasLifecycleChanges(report)) {
87
+ return "";
88
+ }
89
+ const lines = [];
90
+ // Title
91
+ const titleSuffix = dryRun ? " (Dry Run)" : "";
92
+ lines.push(`## Lifecycle Summary${titleSuffix}`);
93
+ lines.push("");
94
+ // Dry-run warning
95
+ if (dryRun) {
96
+ lines.push("> [!WARNING]");
97
+ lines.push("> This was a dry run — no changes were applied");
98
+ lines.push("");
99
+ }
100
+ // Diff block
101
+ const diffLines = [];
102
+ for (const action of report.actions) {
103
+ if (action.action === "existed")
104
+ continue;
105
+ switch (action.action) {
106
+ case "created":
107
+ diffLines.push(`+ CREATE ${action.repoName}`);
108
+ break;
109
+ case "forked":
110
+ diffLines.push(`+ FORK ${action.upstream ?? "upstream"} -> ${action.repoName}`);
111
+ break;
112
+ case "migrated":
113
+ diffLines.push(`+ MIGRATE ${action.source ?? "source"} -> ${action.repoName}`);
114
+ break;
115
+ }
116
+ if (action.settings) {
117
+ if (action.settings.visibility) {
118
+ diffLines.push(` visibility: ${action.settings.visibility}`);
119
+ }
120
+ if (action.settings.description) {
121
+ diffLines.push(` description: "${action.settings.description}"`);
122
+ }
123
+ }
124
+ }
125
+ if (diffLines.length > 0) {
126
+ lines.push("```diff");
127
+ lines.push(...diffLines);
128
+ lines.push("```");
129
+ lines.push("");
130
+ }
131
+ // Summary
132
+ lines.push(`**${formatSummary(report.totals)}**`);
133
+ return lines.join("\n");
134
+ }
135
+ // =============================================================================
136
+ // File Writer
137
+ // =============================================================================
138
+ export function writeLifecycleReportSummary(report, dryRun) {
139
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
140
+ if (!summaryPath)
141
+ return;
142
+ const markdown = formatLifecycleReportMarkdown(report, dryRun);
143
+ if (!markdown)
144
+ return;
145
+ appendFileSync(summaryPath, "\n" + markdown + "\n");
146
+ }
@@ -164,52 +164,52 @@ function formatValuePlain(val) {
164
164
  return val ? "true" : "false";
165
165
  return String(val);
166
166
  }
167
- function formatRulesetConfigPlain(config, indent) {
167
+ function formatRulesetConfigPlain(config) {
168
168
  const lines = [];
169
- function renderObject(obj, currentIndent) {
169
+ function renderObject(obj, depth) {
170
170
  for (const [k, v] of Object.entries(obj)) {
171
- renderValue(k, v, currentIndent);
171
+ renderValue(k, v, depth);
172
172
  }
173
173
  }
174
- function renderValue(key, value, currentIndent) {
175
- const pad = " ".repeat(currentIndent);
174
+ function renderValue(key, value, depth) {
175
+ const indent = " ".repeat(depth);
176
176
  if (value === null || value === undefined)
177
177
  return;
178
178
  if (Array.isArray(value)) {
179
179
  if (value.length === 0) {
180
- lines.push(`${pad}+ ${key}: []`);
180
+ lines.push(`+${indent} ${key}: []`);
181
181
  }
182
182
  else if (value.every((v) => typeof v !== "object")) {
183
- lines.push(`${pad}+ ${key}: [${value.map((v) => (typeof v === "string" ? `"${v}"` : String(v))).join(", ")}]`);
183
+ lines.push(`+${indent} ${key}: [${value.map((v) => (typeof v === "string" ? `"${v}"` : String(v))).join(", ")}]`);
184
184
  }
185
185
  else {
186
- lines.push(`${pad}+ ${key}:`);
186
+ lines.push(`+${indent} ${key}:`);
187
187
  for (let i = 0; i < value.length; i++) {
188
188
  const item = value[i];
189
189
  if (typeof item === "object" && item !== null) {
190
190
  const obj = item;
191
191
  const typeLabel = "type" in obj ? ` (${obj.type})` : "";
192
- lines.push(`${pad} + [${i}]${typeLabel}:`);
193
- renderObject(obj, currentIndent + 2);
192
+ lines.push(`+${indent} [${i}]${typeLabel}:`);
193
+ renderObject(obj, depth + 2);
194
194
  }
195
195
  else {
196
- lines.push(`${pad} + ${formatValuePlain(item)}`);
196
+ lines.push(`+${indent} ${formatValuePlain(item)}`);
197
197
  }
198
198
  }
199
199
  }
200
200
  }
201
201
  else if (typeof value === "object") {
202
- lines.push(`${pad}+ ${key}:`);
203
- renderObject(value, currentIndent + 1);
202
+ lines.push(`+${indent} ${key}:`);
203
+ renderObject(value, depth + 1);
204
204
  }
205
205
  else {
206
- lines.push(`${pad}+ ${key}: ${formatValuePlain(value)}`);
206
+ lines.push(`+${indent} ${key}: ${formatValuePlain(value)}`);
207
207
  }
208
208
  }
209
209
  for (const [key, value] of Object.entries(config)) {
210
210
  if (key === "name")
211
211
  continue;
212
- renderValue(key, value, indent);
212
+ renderValue(key, value, 1);
213
213
  }
214
214
  return lines;
215
215
  }
@@ -233,49 +233,49 @@ export function formatSettingsReportMarkdown(report, dryRun) {
233
233
  !repo.error) {
234
234
  continue;
235
235
  }
236
- diffLines.push(`~ ${repo.repoName}`);
236
+ diffLines.push(`@@ ${repo.repoName} @@`);
237
237
  for (const setting of repo.settings) {
238
238
  // Skip settings where both values are undefined
239
239
  if (setting.oldValue === undefined && setting.newValue === undefined) {
240
240
  continue;
241
241
  }
242
242
  if (setting.action === "add") {
243
- diffLines.push(` + ${setting.name}: ${formatValuePlain(setting.newValue)}`);
243
+ diffLines.push(`+ ${setting.name}: ${formatValuePlain(setting.newValue)}`);
244
244
  }
245
245
  else {
246
- diffLines.push(` ~ ${setting.name}: ${formatValuePlain(setting.oldValue)} → ${formatValuePlain(setting.newValue)}`);
246
+ diffLines.push(`! ${setting.name}: ${formatValuePlain(setting.oldValue)} → ${formatValuePlain(setting.newValue)}`);
247
247
  }
248
248
  }
249
249
  for (const ruleset of repo.rulesets) {
250
250
  if (ruleset.action === "create") {
251
- diffLines.push(` + ruleset "${ruleset.name}"`);
251
+ diffLines.push(`+ ruleset "${ruleset.name}"`);
252
252
  if (ruleset.config) {
253
- diffLines.push(...formatRulesetConfigPlain(ruleset.config, 2));
253
+ diffLines.push(...formatRulesetConfigPlain(ruleset.config));
254
254
  }
255
255
  }
256
256
  else if (ruleset.action === "update") {
257
- diffLines.push(` ~ ruleset "${ruleset.name}"`);
257
+ diffLines.push(`! ruleset "${ruleset.name}"`);
258
258
  if (ruleset.propertyDiffs && ruleset.propertyDiffs.length > 0) {
259
259
  for (const diff of ruleset.propertyDiffs) {
260
260
  const path = diff.path.join(".");
261
261
  if (diff.action === "add") {
262
- diffLines.push(` + ${path}: ${formatValuePlain(diff.newValue)}`);
262
+ diffLines.push(`+ ${path}: ${formatValuePlain(diff.newValue)}`);
263
263
  }
264
264
  else if (diff.action === "change") {
265
- diffLines.push(` ~ ${path}: ${formatValuePlain(diff.oldValue)} → ${formatValuePlain(diff.newValue)}`);
265
+ diffLines.push(`! ${path}: ${formatValuePlain(diff.oldValue)} → ${formatValuePlain(diff.newValue)}`);
266
266
  }
267
267
  else if (diff.action === "remove") {
268
- diffLines.push(` - ${path}`);
268
+ diffLines.push(`- ${path}`);
269
269
  }
270
270
  }
271
271
  }
272
272
  }
273
273
  else if (ruleset.action === "delete") {
274
- diffLines.push(` - ruleset "${ruleset.name}"`);
274
+ diffLines.push(`- ruleset "${ruleset.name}"`);
275
275
  }
276
276
  }
277
277
  if (repo.error) {
278
- diffLines.push(` ! Error: ${repo.error}`);
278
+ diffLines.push(`- Error: ${repo.error}`);
279
279
  }
280
280
  }
281
281
  if (diffLines.length > 0) {
@@ -64,20 +64,20 @@ export function formatSyncReportMarkdown(report, dryRun) {
64
64
  if (repo.files.length === 0 && !repo.error) {
65
65
  continue;
66
66
  }
67
- diffLines.push(`~ ${repo.repoName}`);
67
+ diffLines.push(`@@ ${repo.repoName} @@`);
68
68
  for (const file of repo.files) {
69
69
  if (file.action === "create") {
70
- diffLines.push(` + ${file.path}`);
70
+ diffLines.push(`+ ${file.path}`);
71
71
  }
72
72
  else if (file.action === "update") {
73
- diffLines.push(` ~ ${file.path}`);
73
+ diffLines.push(`! ${file.path}`);
74
74
  }
75
75
  else if (file.action === "delete") {
76
- diffLines.push(` - ${file.path}`);
76
+ diffLines.push(`- ${file.path}`);
77
77
  }
78
78
  }
79
79
  if (repo.error) {
80
- diffLines.push(` ! Error: ${repo.error}`);
80
+ diffLines.push(`- Error: ${repo.error}`);
81
81
  }
82
82
  }
83
83
  if (diffLines.length > 0) {
@@ -0,0 +1,4 @@
1
+ import type { LifecycleReport } from "./lifecycle-report.js";
2
+ import type { SyncReport } from "./sync-report.js";
3
+ export declare function formatUnifiedSummaryMarkdown(lifecycle: LifecycleReport, sync: SyncReport, dryRun: boolean): string;
4
+ export declare function writeUnifiedSummary(lifecycle: LifecycleReport, sync: SyncReport, dryRun: boolean): void;