@aspruyt/xfg 3.7.6 → 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.
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +9 -0
- package/dist/cli/program.d.ts +2 -0
- package/dist/cli/program.js +70 -0
- package/dist/cli/settings-command.d.ts +10 -0
- package/dist/cli/settings-command.js +237 -0
- package/dist/cli/settings-report-builder.d.ts +19 -0
- package/dist/cli/settings-report-builder.js +64 -0
- package/dist/cli/sync-command.d.ts +25 -0
- package/dist/cli/sync-command.js +180 -0
- package/dist/cli/sync-report-builder.d.ts +15 -0
- package/dist/cli/sync-report-builder.js +29 -0
- package/dist/cli/types.d.ts +45 -0
- package/dist/cli/types.js +15 -0
- package/dist/cli.js +2 -19
- package/dist/{file-reference-resolver.d.ts → config/file-reference-resolver.d.ts} +1 -1
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +12 -0
- package/dist/config/loader.d.ts +9 -0
- package/dist/{config.js → config/loader.js} +3 -24
- package/dist/{config-normalizer.d.ts → config/normalizer.d.ts} +1 -1
- package/dist/{config-normalizer.js → config/normalizer.js} +1 -1
- package/dist/{config.d.ts → config/types.d.ts} +5 -9
- package/dist/config/types.js +16 -0
- package/dist/{config-validator.d.ts → config/validator.d.ts} +5 -5
- package/dist/{config-validator.js → config/validator.js} +60 -372
- package/dist/config/validators/file-validator.d.ts +22 -0
- package/dist/config/validators/file-validator.js +46 -0
- package/dist/config/validators/index.d.ts +3 -0
- package/dist/config/validators/index.js +6 -0
- package/dist/config/validators/repo-settings-validator.d.ts +10 -0
- package/dist/config/validators/repo-settings-validator.js +71 -0
- package/dist/config/validators/ruleset-validator.d.ts +18 -0
- package/dist/config/validators/ruleset-validator.js +201 -0
- package/dist/index.d.ts +3 -66
- package/dist/index.js +3 -474
- package/dist/output/index.d.ts +4 -0
- package/dist/output/index.js +8 -0
- package/dist/output/settings-report.d.ts +37 -0
- package/dist/output/settings-report.js +300 -0
- package/dist/{summary-utils.d.ts → output/summary-utils.d.ts} +3 -3
- package/dist/output/sync-report.d.ts +24 -0
- package/dist/output/sync-report.js +99 -0
- package/dist/settings/index.d.ts +2 -0
- package/dist/settings/index.js +4 -0
- package/dist/{repo-settings-diff.d.ts → settings/repo-settings/diff.d.ts} +2 -2
- package/dist/{repo-settings-plan-formatter.d.ts → settings/repo-settings/formatter.d.ts} +3 -1
- package/dist/{repo-settings-plan-formatter.js → settings/repo-settings/formatter.js} +11 -2
- package/dist/{strategies → settings/repo-settings}/github-repo-settings-strategy.d.ts +4 -4
- package/dist/{strategies → settings/repo-settings}/github-repo-settings-strategy.js +3 -3
- package/dist/settings/repo-settings/index.d.ts +5 -0
- package/dist/settings/repo-settings/index.js +10 -0
- package/dist/{repo-settings-processor.d.ts → settings/repo-settings/processor.d.ts} +4 -4
- package/dist/{repo-settings-processor.js → settings/repo-settings/processor.js} +14 -8
- package/dist/{strategies/repo-settings-strategy.d.ts → settings/repo-settings/types.d.ts} +2 -2
- package/dist/settings/rulesets/diff-algorithm.d.ts +18 -0
- package/dist/settings/rulesets/diff-algorithm.js +166 -0
- package/dist/{ruleset-diff.d.ts → settings/rulesets/diff.d.ts} +2 -2
- package/dist/{ruleset-diff.js → settings/rulesets/diff.js} +1 -1
- package/dist/{ruleset-plan-formatter.d.ts → settings/rulesets/formatter.d.ts} +7 -12
- package/dist/{ruleset-plan-formatter.js → settings/rulesets/formatter.js} +10 -165
- package/dist/{strategies → settings/rulesets}/github-ruleset-strategy.d.ts +4 -4
- package/dist/{strategies → settings/rulesets}/github-ruleset-strategy.js +3 -3
- package/dist/settings/rulesets/index.d.ts +6 -0
- package/dist/settings/rulesets/index.js +10 -0
- package/dist/{ruleset-processor.d.ts → settings/rulesets/processor.d.ts} +4 -4
- package/dist/{ruleset-processor.js → settings/rulesets/processor.js} +6 -6
- package/dist/{strategies/ruleset-strategy.d.ts → settings/rulesets/types.d.ts} +2 -2
- package/dist/{command-executor.d.ts → shared/command-executor.d.ts} +10 -2
- package/dist/{command-executor.js → shared/command-executor.js} +2 -1
- package/dist/shared/index.d.ts +8 -0
- package/dist/shared/index.js +16 -0
- package/dist/{logger.d.ts → shared/logger.d.ts} +1 -1
- package/dist/{logger.js → shared/logger.js} +1 -1
- package/dist/sync/auth-options-builder.d.ts +12 -0
- package/dist/sync/auth-options-builder.js +54 -0
- package/dist/sync/branch-manager.d.ts +7 -0
- package/dist/sync/branch-manager.js +36 -0
- package/dist/sync/commit-message.d.ts +11 -0
- package/dist/sync/commit-message.js +27 -0
- package/dist/sync/commit-push-manager.d.ts +8 -0
- package/dist/sync/commit-push-manager.js +71 -0
- package/dist/sync/file-sync-orchestrator.d.ts +11 -0
- package/dist/sync/file-sync-orchestrator.js +58 -0
- package/dist/sync/file-writer.d.ts +18 -0
- package/dist/sync/file-writer.js +101 -0
- package/dist/sync/index.d.ts +14 -0
- package/dist/sync/index.js +17 -0
- package/dist/sync/manifest-manager.d.ts +10 -0
- package/dist/sync/manifest-manager.js +64 -0
- package/dist/sync/pr-merge-handler.d.ts +11 -0
- package/dist/sync/pr-merge-handler.js +63 -0
- package/dist/sync/repository-processor.d.ts +30 -0
- package/dist/sync/repository-processor.js +298 -0
- package/dist/sync/repository-session.d.ts +9 -0
- package/dist/sync/repository-session.js +35 -0
- package/dist/sync/types.d.ts +304 -0
- package/dist/{xfg-template.d.ts → sync/xfg-template.d.ts} +2 -2
- package/dist/{authenticated-git-ops.js → vcs/authenticated-git-ops.js} +3 -3
- package/dist/{strategies → vcs}/azure-pr-strategy.d.ts +2 -2
- package/dist/{strategies → vcs}/azure-pr-strategy.js +5 -5
- package/dist/{strategies → vcs}/commit-strategy-selector.d.ts +3 -3
- package/dist/{strategies → vcs}/commit-strategy-selector.js +1 -1
- package/dist/{strategies → vcs}/git-commit-strategy.d.ts +2 -2
- package/dist/{strategies → vcs}/git-commit-strategy.js +3 -3
- package/dist/{git-ops.d.ts → vcs/git-ops.d.ts} +1 -1
- package/dist/{git-ops.js → vcs/git-ops.js} +4 -4
- package/dist/{github-app-token-manager.d.ts → vcs/github-app-token-manager.d.ts} +1 -1
- package/dist/{github-app-token-manager.js → vcs/github-app-token-manager.js} +1 -1
- package/dist/{strategies → vcs}/github-pr-strategy.d.ts +2 -2
- package/dist/{strategies → vcs}/github-pr-strategy.js +30 -33
- package/dist/{strategies → vcs}/gitlab-pr-strategy.d.ts +2 -2
- package/dist/{strategies → vcs}/gitlab-pr-strategy.js +5 -5
- package/dist/{strategies → vcs}/graphql-commit-strategy.d.ts +2 -2
- package/dist/{strategies → vcs}/graphql-commit-strategy.js +3 -3
- package/dist/vcs/index.d.ts +16 -0
- package/dist/{strategies → vcs}/index.js +15 -10
- package/dist/{pr-creator.d.ts → vcs/pr-creator.d.ts} +4 -4
- package/dist/{pr-creator.js → vcs/pr-creator.js} +3 -3
- package/dist/vcs/pr-strategy.d.ts +41 -0
- package/dist/{strategies → vcs}/pr-strategy.js +1 -1
- package/dist/{strategies/pr-strategy.d.ts → vcs/types.d.ts} +32 -35
- package/dist/vcs/types.js +1 -0
- package/package.json +2 -2
- package/dist/plan-formatter.d.ts +0 -39
- package/dist/plan-formatter.js +0 -84
- package/dist/plan-summary.d.ts +0 -8
- package/dist/plan-summary.js +0 -110
- package/dist/repository-processor.d.ts +0 -79
- package/dist/repository-processor.js +0 -659
- package/dist/resource-converters.d.ts +0 -28
- package/dist/resource-converters.js +0 -107
- package/dist/strategies/commit-strategy.d.ts +0 -36
- package/dist/strategies/index.d.ts +0 -18
- /package/dist/{file-reference-resolver.js → config/file-reference-resolver.js} +0 -0
- /package/dist/{config-formatter.d.ts → config/formatter.d.ts} +0 -0
- /package/dist/{config-formatter.js → config/formatter.js} +0 -0
- /package/dist/{merge.d.ts → config/merge.d.ts} +0 -0
- /package/dist/{merge.js → config/merge.js} +0 -0
- /package/dist/{github-summary.d.ts → output/github-summary.d.ts} +0 -0
- /package/dist/{github-summary.js → output/github-summary.js} +0 -0
- /package/dist/{summary-utils.js → output/summary-utils.js} +0 -0
- /package/dist/{repo-settings-diff.js → settings/repo-settings/diff.js} +0 -0
- /package/dist/{strategies/repo-settings-strategy.js → settings/repo-settings/types.js} +0 -0
- /package/dist/{strategies/commit-strategy.js → settings/rulesets/types.js} +0 -0
- /package/dist/{env.d.ts → shared/env.d.ts} +0 -0
- /package/dist/{env.js → shared/env.js} +0 -0
- /package/dist/{repo-detector.d.ts → shared/repo-detector.d.ts} +0 -0
- /package/dist/{repo-detector.js → shared/repo-detector.js} +0 -0
- /package/dist/{retry-utils.d.ts → shared/retry-utils.d.ts} +0 -0
- /package/dist/{retry-utils.js → shared/retry-utils.js} +0 -0
- /package/dist/{sanitize-utils.d.ts → shared/sanitize-utils.d.ts} +0 -0
- /package/dist/{sanitize-utils.js → shared/sanitize-utils.js} +0 -0
- /package/dist/{shell-utils.d.ts → shared/shell-utils.d.ts} +0 -0
- /package/dist/{shell-utils.js → shared/shell-utils.js} +0 -0
- /package/dist/{workspace-utils.d.ts → shared/workspace-utils.d.ts} +0 -0
- /package/dist/{workspace-utils.js → shared/workspace-utils.js} +0 -0
- /package/dist/{diff-utils.d.ts → sync/diff-utils.d.ts} +0 -0
- /package/dist/{diff-utils.js → sync/diff-utils.js} +0 -0
- /package/dist/{manifest.d.ts → sync/manifest.d.ts} +0 -0
- /package/dist/{manifest.js → sync/manifest.js} +0 -0
- /package/dist/{strategies/ruleset-strategy.js → sync/types.js} +0 -0
- /package/dist/{xfg-template.js → sync/xfg-template.js} +0 -0
- /package/dist/{authenticated-git-ops.d.ts → vcs/authenticated-git-ops.d.ts} +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { runSync } from "./sync-command.js";
|
|
2
|
+
export { runSettings } from "./settings-command.js";
|
|
3
|
+
export { program } from "./program.js";
|
|
4
|
+
export { type IRepositoryProcessor, type ProcessorFactory, type IRulesetProcessor, type RulesetProcessorFactory, type RepoSettingsProcessorFactory, type IRepoSettingsProcessor, defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, } from "./types.js";
|
|
5
|
+
export type { SyncOptions, SharedOptions } from "./sync-command.js";
|
|
6
|
+
export type { SettingsOptions } from "./settings-command.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// CLI command implementations
|
|
2
|
+
export { runSync } from "./sync-command.js";
|
|
3
|
+
export { runSettings } from "./settings-command.js";
|
|
4
|
+
export { program } from "./program.js";
|
|
5
|
+
// Export types - using 'export type' for type aliases, but interfaces need special handling
|
|
6
|
+
// For ESM compatibility, re-export everything from types.js
|
|
7
|
+
export {
|
|
8
|
+
// Runtime values
|
|
9
|
+
defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, } from "./types.js";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { program, Command } from "commander";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { runSync } from "./sync-command.js";
|
|
6
|
+
import { runSettings } from "./settings-command.js";
|
|
7
|
+
// Get version from package.json
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, "../..", "package.json"), "utf-8"));
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Shared CLI Options
|
|
13
|
+
// =============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Adds shared options to a command.
|
|
16
|
+
*/
|
|
17
|
+
function addSharedOptions(cmd) {
|
|
18
|
+
return cmd
|
|
19
|
+
.requiredOption("-c, --config <path>", "Path to YAML config file")
|
|
20
|
+
.option("-d, --dry-run", "Show what would be done without making changes")
|
|
21
|
+
.option("-w, --work-dir <path>", "Temporary directory for cloning", "./tmp")
|
|
22
|
+
.option("-r, --retries <number>", "Number of retries for network operations (0 to disable)", (v) => parseInt(v, 10), 3)
|
|
23
|
+
.option("--no-delete", "Skip deletion of orphaned resources even if deleteOrphaned is configured");
|
|
24
|
+
}
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// CLI Program
|
|
27
|
+
// =============================================================================
|
|
28
|
+
program
|
|
29
|
+
.name("xfg")
|
|
30
|
+
.description("Sync files and manage settings across repositories")
|
|
31
|
+
.version(packageJson.version);
|
|
32
|
+
// Sync command (file synchronization)
|
|
33
|
+
const syncCommand = new Command("sync")
|
|
34
|
+
.description("Sync configuration files across repositories")
|
|
35
|
+
.option("-b, --branch <name>", "Override the branch name (default: chore/sync-{filename} or chore/sync-config)")
|
|
36
|
+
.option("-m, --merge <mode>", "PR merge mode: manual, auto (default, merge when checks pass), force (bypass requirements), direct (push to default branch, no PR)", (value) => {
|
|
37
|
+
const valid = ["manual", "auto", "force", "direct"];
|
|
38
|
+
if (!valid.includes(value)) {
|
|
39
|
+
throw new Error(`Invalid merge mode: ${value}. Valid: ${valid.join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
})
|
|
43
|
+
.option("--merge-strategy <strategy>", "Merge strategy: merge, squash (default), rebase", (value) => {
|
|
44
|
+
const valid = ["merge", "squash", "rebase"];
|
|
45
|
+
if (!valid.includes(value)) {
|
|
46
|
+
throw new Error(`Invalid merge strategy: ${value}. Valid: ${valid.join(", ")}`);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
})
|
|
50
|
+
.option("--delete-branch", "Delete source branch after merge")
|
|
51
|
+
.action((opts) => {
|
|
52
|
+
runSync(opts).catch((error) => {
|
|
53
|
+
console.error("Fatal error:", error);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
addSharedOptions(syncCommand);
|
|
58
|
+
program.addCommand(syncCommand);
|
|
59
|
+
// Settings command (ruleset management)
|
|
60
|
+
const settingsCommand = new Command("settings")
|
|
61
|
+
.description("Manage GitHub Rulesets for repositories")
|
|
62
|
+
.action((opts) => {
|
|
63
|
+
runSettings(opts).catch((error) => {
|
|
64
|
+
console.error("Fatal error:", error);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
addSharedOptions(settingsCommand);
|
|
69
|
+
program.addCommand(settingsCommand);
|
|
70
|
+
export { program };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SharedOptions } from "./sync-command.js";
|
|
2
|
+
import { ProcessorFactory, RulesetProcessorFactory, RepoSettingsProcessorFactory } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Options for the settings command.
|
|
5
|
+
*/
|
|
6
|
+
export type SettingsOptions = SharedOptions;
|
|
7
|
+
/**
|
|
8
|
+
* Run the settings command - manages GitHub Rulesets and repo settings.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runSettings(options: SettingsOptions, processorFactory?: RulesetProcessorFactory, repoProcessorFactory?: ProcessorFactory, repoSettingsProcessorFactory?: RepoSettingsProcessorFactory): Promise<void>;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { loadRawConfig, normalizeConfig } from "../config/index.js";
|
|
5
|
+
import { validateForSettings } from "../config/validator.js";
|
|
6
|
+
import { parseGitUrl, getRepoDisplayName, isGitHubRepo, } from "../shared/repo-detector.js";
|
|
7
|
+
import { logger } from "../shared/logger.js";
|
|
8
|
+
import { generateWorkspaceName } from "../shared/workspace-utils.js";
|
|
9
|
+
import { buildErrorResult } from "../output/summary-utils.js";
|
|
10
|
+
import { getManagedRulesets } from "../sync/manifest.js";
|
|
11
|
+
import { formatSettingsReportCLI, writeSettingsReportSummary, } from "../output/settings-report.js";
|
|
12
|
+
import { buildSettingsReport } from "./settings-report-builder.js";
|
|
13
|
+
import { defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, } from "./types.js";
|
|
14
|
+
/**
|
|
15
|
+
* Run the settings command - manages GitHub Rulesets and repo settings.
|
|
16
|
+
*/
|
|
17
|
+
export async function runSettings(options, processorFactory = defaultRulesetProcessorFactory, repoProcessorFactory = defaultProcessorFactory, repoSettingsProcessorFactory = defaultRepoSettingsProcessorFactory) {
|
|
18
|
+
const configPath = resolve(options.config);
|
|
19
|
+
if (!existsSync(configPath)) {
|
|
20
|
+
console.error(`Config file not found: ${configPath}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
console.log(`Loading config from: ${configPath}`);
|
|
24
|
+
if (options.dryRun) {
|
|
25
|
+
console.log("Running in DRY RUN mode - no changes will be made\n");
|
|
26
|
+
}
|
|
27
|
+
const rawConfig = loadRawConfig(configPath);
|
|
28
|
+
try {
|
|
29
|
+
validateForSettings(rawConfig);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const config = normalizeConfig(rawConfig);
|
|
36
|
+
const reposWithRulesets = config.repos.filter((r) => r.settings?.rulesets && Object.keys(r.settings.rulesets).length > 0);
|
|
37
|
+
const reposWithRepoSettings = config.repos.filter((r) => r.settings?.repo && Object.keys(r.settings.repo).length > 0);
|
|
38
|
+
if (reposWithRulesets.length === 0 && reposWithRepoSettings.length === 0) {
|
|
39
|
+
console.log("No settings configured. Add settings.rulesets or settings.repo to your config.");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (reposWithRulesets.length > 0) {
|
|
43
|
+
console.log(`Found ${reposWithRulesets.length} repositories with rulesets`);
|
|
44
|
+
}
|
|
45
|
+
if (reposWithRepoSettings.length > 0) {
|
|
46
|
+
console.log(`Found ${reposWithRepoSettings.length} repositories with repo settings`);
|
|
47
|
+
}
|
|
48
|
+
console.log("");
|
|
49
|
+
logger.setTotal(reposWithRulesets.length + reposWithRepoSettings.length);
|
|
50
|
+
const processor = processorFactory();
|
|
51
|
+
const repoProcessor = repoProcessorFactory();
|
|
52
|
+
const results = [];
|
|
53
|
+
const processingResults = [];
|
|
54
|
+
function getOrCreateResult(repoName) {
|
|
55
|
+
let result = processingResults.find((r) => r.repoName === repoName);
|
|
56
|
+
if (!result) {
|
|
57
|
+
result = { repoName };
|
|
58
|
+
processingResults.push(result);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
for (let i = 0; i < reposWithRulesets.length; i++) {
|
|
63
|
+
const repoConfig = reposWithRulesets[i];
|
|
64
|
+
let repoInfo;
|
|
65
|
+
try {
|
|
66
|
+
repoInfo = parseGitUrl(repoConfig.git, {
|
|
67
|
+
githubHosts: config.githubHosts,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
logger.error(i + 1, repoConfig.git, String(error));
|
|
72
|
+
results.push(buildErrorResult(repoConfig.git, error));
|
|
73
|
+
getOrCreateResult(repoConfig.git).error =
|
|
74
|
+
error instanceof Error ? error.message : String(error);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const repoName = getRepoDisplayName(repoInfo);
|
|
78
|
+
if (!isGitHubRepo(repoInfo)) {
|
|
79
|
+
logger.skip(i + 1, repoName, "GitHub Rulesets only supported for GitHub repos");
|
|
80
|
+
// Skipped repos don't appear in the report
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const managedRulesets = getManagedRulesets(null, config.id);
|
|
84
|
+
try {
|
|
85
|
+
logger.progress(i + 1, repoName, "Processing rulesets...");
|
|
86
|
+
const result = await processor.process(repoConfig, repoInfo, {
|
|
87
|
+
configId: config.id,
|
|
88
|
+
dryRun: options.dryRun,
|
|
89
|
+
managedRulesets,
|
|
90
|
+
noDelete: options.noDelete,
|
|
91
|
+
});
|
|
92
|
+
if (result.planOutput && result.planOutput.lines.length > 0) {
|
|
93
|
+
logger.info("");
|
|
94
|
+
logger.info(chalk.bold(`${repoName} - Rulesets:`));
|
|
95
|
+
for (const line of result.planOutput.lines) {
|
|
96
|
+
logger.info(line);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (result.skipped) {
|
|
100
|
+
logger.skip(i + 1, repoName, result.message);
|
|
101
|
+
}
|
|
102
|
+
else if (result.success) {
|
|
103
|
+
logger.success(i + 1, repoName, result.message);
|
|
104
|
+
if (result.manifestUpdate &&
|
|
105
|
+
result.manifestUpdate.rulesets.length > 0) {
|
|
106
|
+
const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(i)));
|
|
107
|
+
logger.progress(i + 1, repoName, "Updating manifest...");
|
|
108
|
+
const manifestResult = await repoProcessor.updateManifestOnly(repoInfo, repoConfig, {
|
|
109
|
+
branchName: "chore/sync-rulesets",
|
|
110
|
+
workDir,
|
|
111
|
+
configId: config.id,
|
|
112
|
+
dryRun: options.dryRun,
|
|
113
|
+
retries: options.retries,
|
|
114
|
+
}, result.manifestUpdate);
|
|
115
|
+
if (!manifestResult.success && !manifestResult.skipped) {
|
|
116
|
+
logger.info(`Warning: Failed to update manifest for ${repoName}: ${manifestResult.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
logger.error(i + 1, repoName, result.message);
|
|
122
|
+
}
|
|
123
|
+
results.push({
|
|
124
|
+
repoName,
|
|
125
|
+
status: result.skipped
|
|
126
|
+
? "skipped"
|
|
127
|
+
: result.success
|
|
128
|
+
? "succeeded"
|
|
129
|
+
: "failed",
|
|
130
|
+
message: result.message,
|
|
131
|
+
rulesetPlanDetails: result.planOutput?.entries,
|
|
132
|
+
});
|
|
133
|
+
// Collect result for SettingsReport
|
|
134
|
+
if (!result.skipped) {
|
|
135
|
+
getOrCreateResult(repoName).rulesetResult = result;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
logger.error(i + 1, repoName, String(error));
|
|
140
|
+
results.push(buildErrorResult(repoName, error));
|
|
141
|
+
const existingResult = getOrCreateResult(repoName);
|
|
142
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
143
|
+
if (existingResult.error) {
|
|
144
|
+
existingResult.error += `; ${errorMsg}`;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
existingResult.error = errorMsg;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (reposWithRepoSettings.length > 0) {
|
|
152
|
+
const repoSettingsProcessor = repoSettingsProcessorFactory();
|
|
153
|
+
console.log(`\nProcessing repo settings for ${reposWithRepoSettings.length} repositories\n`);
|
|
154
|
+
for (let i = 0; i < reposWithRepoSettings.length; i++) {
|
|
155
|
+
const repoConfig = reposWithRepoSettings[i];
|
|
156
|
+
let repoInfo;
|
|
157
|
+
try {
|
|
158
|
+
repoInfo = parseGitUrl(repoConfig.git, {
|
|
159
|
+
githubHosts: config.githubHosts,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
console.error(`Failed to parse ${repoConfig.git}: ${error}`);
|
|
164
|
+
getOrCreateResult(repoConfig.git).error =
|
|
165
|
+
error instanceof Error ? error.message : String(error);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const repoName = getRepoDisplayName(repoInfo);
|
|
169
|
+
try {
|
|
170
|
+
const result = await repoSettingsProcessor.process(repoConfig, repoInfo, {
|
|
171
|
+
dryRun: options.dryRun,
|
|
172
|
+
});
|
|
173
|
+
if (result.planOutput && result.planOutput.lines.length > 0) {
|
|
174
|
+
console.log(`\n ${chalk.bold(repoName)}:`);
|
|
175
|
+
console.log(" Repo Settings:");
|
|
176
|
+
for (const line of result.planOutput.lines) {
|
|
177
|
+
console.log(line);
|
|
178
|
+
}
|
|
179
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
180
|
+
for (const warning of result.warnings) {
|
|
181
|
+
console.log(chalk.yellow(` ⚠️ Warning: ${warning}`));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (result.skipped) {
|
|
186
|
+
// Silent skip
|
|
187
|
+
}
|
|
188
|
+
else if (result.success) {
|
|
189
|
+
console.log(chalk.green(` ✓ ${repoName}: ${result.message}`));
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log(chalk.red(` ✗ ${repoName}: ${result.message}`));
|
|
193
|
+
}
|
|
194
|
+
if (!result.skipped) {
|
|
195
|
+
const existing = results.find((r) => r.repoName === repoName);
|
|
196
|
+
if (existing) {
|
|
197
|
+
existing.repoSettingsPlanDetails = result.planOutput?.entries;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
results.push({
|
|
201
|
+
repoName,
|
|
202
|
+
status: result.success ? "succeeded" : "failed",
|
|
203
|
+
message: result.message,
|
|
204
|
+
repoSettingsPlanDetails: result.planOutput?.entries,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Collect result for SettingsReport
|
|
209
|
+
if (!result.skipped) {
|
|
210
|
+
getOrCreateResult(repoName).settingsResult = result;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
console.error(` ✗ ${repoName}: ${error}`);
|
|
215
|
+
const existingResult = getOrCreateResult(repoName);
|
|
216
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
217
|
+
if (existingResult.error) {
|
|
218
|
+
existingResult.error += `; ${errorMsg}`;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
existingResult.error = errorMsg;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
console.log("");
|
|
227
|
+
const report = buildSettingsReport(processingResults);
|
|
228
|
+
const lines = formatSettingsReportCLI(report);
|
|
229
|
+
for (const line of lines) {
|
|
230
|
+
console.log(line);
|
|
231
|
+
}
|
|
232
|
+
writeSettingsReportSummary(report, options.dryRun ?? false);
|
|
233
|
+
const hasErrors = report.repos.some((r) => r.error);
|
|
234
|
+
if (hasErrors) {
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SettingsReport } from "../output/settings-report.js";
|
|
2
|
+
import type { RepoSettingsPlanEntry } from "../settings/repo-settings/formatter.js";
|
|
3
|
+
import type { RulesetPlanEntry } from "../settings/rulesets/formatter.js";
|
|
4
|
+
interface ProcessorResults {
|
|
5
|
+
repoName: string;
|
|
6
|
+
settingsResult?: {
|
|
7
|
+
planOutput?: {
|
|
8
|
+
entries?: RepoSettingsPlanEntry[];
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
rulesetResult?: {
|
|
12
|
+
planOutput?: {
|
|
13
|
+
entries?: RulesetPlanEntry[];
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function buildSettingsReport(results: ProcessorResults[]): SettingsReport;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function buildSettingsReport(results) {
|
|
2
|
+
const repos = [];
|
|
3
|
+
const totals = {
|
|
4
|
+
settings: { add: 0, change: 0 },
|
|
5
|
+
rulesets: { create: 0, update: 0, delete: 0 },
|
|
6
|
+
};
|
|
7
|
+
for (const result of results) {
|
|
8
|
+
const repoChanges = {
|
|
9
|
+
repoName: result.repoName,
|
|
10
|
+
settings: [],
|
|
11
|
+
rulesets: [],
|
|
12
|
+
};
|
|
13
|
+
// Convert settings processor output
|
|
14
|
+
if (result.settingsResult?.planOutput?.entries) {
|
|
15
|
+
for (const entry of result.settingsResult.planOutput.entries) {
|
|
16
|
+
// Skip settings where both values are undefined (no actual change)
|
|
17
|
+
if (entry.oldValue === undefined && entry.newValue === undefined) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const settingChange = {
|
|
21
|
+
name: entry.property,
|
|
22
|
+
action: entry.action,
|
|
23
|
+
oldValue: entry.oldValue,
|
|
24
|
+
newValue: entry.newValue,
|
|
25
|
+
};
|
|
26
|
+
repoChanges.settings.push(settingChange);
|
|
27
|
+
if (entry.action === "add") {
|
|
28
|
+
totals.settings.add++;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
totals.settings.change++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Convert ruleset processor output
|
|
36
|
+
if (result.rulesetResult?.planOutput?.entries) {
|
|
37
|
+
for (const entry of result.rulesetResult.planOutput.entries) {
|
|
38
|
+
if (entry.action === "unchanged")
|
|
39
|
+
continue;
|
|
40
|
+
const rulesetChange = {
|
|
41
|
+
name: entry.name,
|
|
42
|
+
action: entry.action,
|
|
43
|
+
propertyDiffs: entry.propertyDiffs,
|
|
44
|
+
config: entry.config,
|
|
45
|
+
};
|
|
46
|
+
repoChanges.rulesets.push(rulesetChange);
|
|
47
|
+
if (entry.action === "create") {
|
|
48
|
+
totals.rulesets.create++;
|
|
49
|
+
}
|
|
50
|
+
else if (entry.action === "update") {
|
|
51
|
+
totals.rulesets.update++;
|
|
52
|
+
}
|
|
53
|
+
else if (entry.action === "delete") {
|
|
54
|
+
totals.rulesets.delete++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (result.error) {
|
|
59
|
+
repoChanges.error = result.error;
|
|
60
|
+
}
|
|
61
|
+
repos.push(repoChanges);
|
|
62
|
+
}
|
|
63
|
+
return { repos, totals };
|
|
64
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MergeMode, MergeStrategy } from "../config/index.js";
|
|
2
|
+
import { ProcessorFactory } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Shared options common to all commands.
|
|
5
|
+
*/
|
|
6
|
+
export interface SharedOptions {
|
|
7
|
+
config: string;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
workDir?: string;
|
|
10
|
+
retries?: number;
|
|
11
|
+
noDelete?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Options specific to the sync command.
|
|
15
|
+
*/
|
|
16
|
+
export interface SyncOptions extends SharedOptions {
|
|
17
|
+
branch?: string;
|
|
18
|
+
merge?: MergeMode;
|
|
19
|
+
mergeStrategy?: MergeStrategy;
|
|
20
|
+
deleteBranch?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run the sync command - synchronizes files across repositories.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runSync(options: SyncOptions, processorFactory?: ProcessorFactory): Promise<void>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { loadRawConfig, normalizeConfig, } from "../config/index.js";
|
|
4
|
+
import { validateForSync } from "../config/validator.js";
|
|
5
|
+
import { parseGitUrl, getRepoDisplayName } from "../shared/repo-detector.js";
|
|
6
|
+
import { sanitizeBranchName, validateBranchName } from "../vcs/git-ops.js";
|
|
7
|
+
import { logger } from "../shared/logger.js";
|
|
8
|
+
import { generateWorkspaceName } from "../shared/workspace-utils.js";
|
|
9
|
+
import { defaultProcessorFactory } from "./types.js";
|
|
10
|
+
import { buildSyncReport } from "./sync-report-builder.js";
|
|
11
|
+
import { formatSyncReportCLI, writeSyncReportSummary, } from "../output/sync-report.js";
|
|
12
|
+
/**
|
|
13
|
+
* Get unique file names from all repos in the config
|
|
14
|
+
*/
|
|
15
|
+
function getUniqueFileNames(config) {
|
|
16
|
+
const fileNames = new Set();
|
|
17
|
+
for (const repo of config.repos) {
|
|
18
|
+
for (const file of repo.files) {
|
|
19
|
+
fileNames.add(file.fileName);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return Array.from(fileNames);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Generate default branch name based on files being synced
|
|
26
|
+
*/
|
|
27
|
+
function generateBranchName(fileNames) {
|
|
28
|
+
if (fileNames.length === 1) {
|
|
29
|
+
return `chore/sync-${sanitizeBranchName(fileNames[0])}`;
|
|
30
|
+
}
|
|
31
|
+
return "chore/sync-config";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Format file names for display
|
|
35
|
+
*/
|
|
36
|
+
function formatFileNames(fileNames) {
|
|
37
|
+
if (fileNames.length === 1) {
|
|
38
|
+
return fileNames[0];
|
|
39
|
+
}
|
|
40
|
+
if (fileNames.length <= 3) {
|
|
41
|
+
return fileNames.join(", ");
|
|
42
|
+
}
|
|
43
|
+
return `${fileNames.length} files`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Determine merge outcome from processor result
|
|
47
|
+
*/
|
|
48
|
+
function determineMergeOutcome(result) {
|
|
49
|
+
if (!result.success)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (!result.prUrl)
|
|
52
|
+
return "direct";
|
|
53
|
+
if (result.mergeResult?.merged)
|
|
54
|
+
return "force";
|
|
55
|
+
if (result.mergeResult?.autoMergeEnabled)
|
|
56
|
+
return "auto";
|
|
57
|
+
return "manual";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Run the sync command - synchronizes files across repositories.
|
|
61
|
+
*/
|
|
62
|
+
export async function runSync(options, processorFactory = defaultProcessorFactory) {
|
|
63
|
+
const configPath = resolve(options.config);
|
|
64
|
+
if (!existsSync(configPath)) {
|
|
65
|
+
console.error(`Config file not found: ${configPath}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
console.log(`Loading config from: ${configPath}`);
|
|
69
|
+
if (options.dryRun) {
|
|
70
|
+
console.log("Running in DRY RUN mode - no changes will be made\n");
|
|
71
|
+
}
|
|
72
|
+
const rawConfig = loadRawConfig(configPath);
|
|
73
|
+
try {
|
|
74
|
+
validateForSync(rawConfig);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
const config = normalizeConfig(rawConfig);
|
|
81
|
+
const fileNames = getUniqueFileNames(config);
|
|
82
|
+
let branchName;
|
|
83
|
+
if (options.branch) {
|
|
84
|
+
validateBranchName(options.branch);
|
|
85
|
+
branchName = options.branch;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
branchName = generateBranchName(fileNames);
|
|
89
|
+
}
|
|
90
|
+
logger.setTotal(config.repos.length);
|
|
91
|
+
console.log(`Found ${config.repos.length} repositories to process`);
|
|
92
|
+
console.log(`Target files: ${formatFileNames(fileNames)}`);
|
|
93
|
+
console.log(`Branch: ${branchName}\n`);
|
|
94
|
+
const processor = processorFactory();
|
|
95
|
+
const reportResults = [];
|
|
96
|
+
for (let i = 0; i < config.repos.length; i++) {
|
|
97
|
+
const repoConfig = config.repos[i];
|
|
98
|
+
if (options.merge || options.mergeStrategy || options.deleteBranch) {
|
|
99
|
+
repoConfig.prOptions = {
|
|
100
|
+
...repoConfig.prOptions,
|
|
101
|
+
merge: options.merge ?? repoConfig.prOptions?.merge,
|
|
102
|
+
mergeStrategy: options.mergeStrategy ?? repoConfig.prOptions?.mergeStrategy,
|
|
103
|
+
deleteBranch: options.deleteBranch ?? repoConfig.prOptions?.deleteBranch,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const current = i + 1;
|
|
107
|
+
let repoInfo;
|
|
108
|
+
try {
|
|
109
|
+
repoInfo = parseGitUrl(repoConfig.git, {
|
|
110
|
+
githubHosts: config.githubHosts,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
logger.error(current, repoConfig.git, String(error));
|
|
115
|
+
reportResults.push({
|
|
116
|
+
repoName: repoConfig.git,
|
|
117
|
+
success: false,
|
|
118
|
+
fileChanges: [],
|
|
119
|
+
error: error instanceof Error ? error.message : String(error),
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const repoName = getRepoDisplayName(repoInfo);
|
|
124
|
+
const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(i)));
|
|
125
|
+
try {
|
|
126
|
+
logger.progress(current, repoName, "Processing...");
|
|
127
|
+
const result = await processor.process(repoConfig, repoInfo, {
|
|
128
|
+
branchName,
|
|
129
|
+
workDir,
|
|
130
|
+
configId: config.id,
|
|
131
|
+
dryRun: options.dryRun,
|
|
132
|
+
retries: options.retries,
|
|
133
|
+
prTemplate: config.prTemplate,
|
|
134
|
+
noDelete: options.noDelete,
|
|
135
|
+
});
|
|
136
|
+
const mergeOutcome = determineMergeOutcome(result);
|
|
137
|
+
reportResults.push({
|
|
138
|
+
repoName,
|
|
139
|
+
success: result.success,
|
|
140
|
+
fileChanges: (result.fileChanges ?? []).map((f) => ({
|
|
141
|
+
path: f.path,
|
|
142
|
+
action: f.action,
|
|
143
|
+
})),
|
|
144
|
+
prUrl: result.prUrl,
|
|
145
|
+
mergeOutcome,
|
|
146
|
+
error: result.success ? undefined : result.message,
|
|
147
|
+
});
|
|
148
|
+
if (result.skipped) {
|
|
149
|
+
logger.skip(current, repoName, result.message);
|
|
150
|
+
}
|
|
151
|
+
else if (result.success) {
|
|
152
|
+
logger.success(current, repoName, result.message);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
logger.error(current, repoName, result.message);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
logger.error(current, repoName, String(error));
|
|
160
|
+
reportResults.push({
|
|
161
|
+
repoName,
|
|
162
|
+
success: false,
|
|
163
|
+
fileChanges: [],
|
|
164
|
+
error: error instanceof Error ? error.message : String(error),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Build and display report
|
|
169
|
+
const report = buildSyncReport(reportResults);
|
|
170
|
+
console.log("");
|
|
171
|
+
for (const line of formatSyncReportCLI(report)) {
|
|
172
|
+
console.log(line);
|
|
173
|
+
}
|
|
174
|
+
writeSyncReportSummary(report, options.dryRun ?? false);
|
|
175
|
+
// Exit with error if any failures
|
|
176
|
+
const hasErrors = reportResults.some((r) => r.error);
|
|
177
|
+
if (hasErrors) {
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SyncReport } from "../output/sync-report.js";
|
|
2
|
+
interface FileChangeInput {
|
|
3
|
+
path: string;
|
|
4
|
+
action: "create" | "update" | "delete";
|
|
5
|
+
}
|
|
6
|
+
interface SyncResultInput {
|
|
7
|
+
repoName: string;
|
|
8
|
+
success: boolean;
|
|
9
|
+
fileChanges: FileChangeInput[];
|
|
10
|
+
prUrl?: string;
|
|
11
|
+
mergeOutcome?: "manual" | "auto" | "force" | "direct";
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildSyncReport(results: SyncResultInput[]): SyncReport;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function buildSyncReport(results) {
|
|
2
|
+
const repos = [];
|
|
3
|
+
const totals = {
|
|
4
|
+
files: { create: 0, update: 0, delete: 0 },
|
|
5
|
+
};
|
|
6
|
+
for (const result of results) {
|
|
7
|
+
const files = result.fileChanges.map((f) => ({
|
|
8
|
+
path: f.path,
|
|
9
|
+
action: f.action,
|
|
10
|
+
}));
|
|
11
|
+
// Count totals
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
if (file.action === "create")
|
|
14
|
+
totals.files.create++;
|
|
15
|
+
else if (file.action === "update")
|
|
16
|
+
totals.files.update++;
|
|
17
|
+
else if (file.action === "delete")
|
|
18
|
+
totals.files.delete++;
|
|
19
|
+
}
|
|
20
|
+
repos.push({
|
|
21
|
+
repoName: result.repoName,
|
|
22
|
+
files,
|
|
23
|
+
prUrl: result.prUrl,
|
|
24
|
+
mergeOutcome: result.mergeOutcome,
|
|
25
|
+
error: result.error,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return { repos, totals };
|
|
29
|
+
}
|