@aspruyt/xfg 4.0.4 → 5.0.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/README.md +1 -1
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +18 -13
- package/dist/cli/sync-command.js +62 -39
- package/dist/cli/sync-report-builder.js +7 -4
- package/dist/config/formatter.js +14 -9
- package/dist/config/merge.d.ts +2 -4
- package/dist/config/merge.js +15 -67
- package/dist/config/validator.js +2 -9
- package/dist/lifecycle/repo-lifecycle-factory.js +0 -4
- package/dist/output/github-summary.d.ts +3 -2
- package/dist/output/github-summary.js +1 -7
- package/dist/output/lifecycle-report.js +7 -14
- package/dist/output/sync-report.d.ts +2 -19
- package/dist/output/sync-report.js +16 -28
- package/dist/output/types.d.ts +19 -0
- package/dist/output/types.js +1 -0
- package/dist/output/unified-summary.d.ts +2 -1
- package/dist/output/unified-summary.js +4 -1
- package/dist/settings/base-processor.d.ts +3 -1
- package/dist/settings/base-processor.js +9 -5
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/labels/diff.d.ts +2 -1
- package/dist/settings/labels/formatter.js +2 -4
- package/dist/settings/labels/github-labels-strategy.js +2 -1
- package/dist/settings/labels/processor.js +0 -1
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +2 -1
- package/dist/settings/rulesets/diff-algorithm.js +0 -1
- package/dist/settings/rulesets/diff.d.ts +2 -1
- package/dist/settings/rulesets/diff.js +37 -21
- package/dist/settings/rulesets/formatter.js +44 -38
- package/dist/settings/rulesets/github-ruleset-strategy.js +2 -1
- package/dist/settings/rulesets/processor.js +0 -1
- package/dist/shared/gh-api-utils.d.ts +8 -7
- package/dist/shared/gh-api-utils.js +2 -16
- package/dist/shared/interpolation-engine.d.ts +3 -0
- package/dist/shared/interpolation-engine.js +0 -3
- package/dist/shared/json-utils.d.ts +6 -0
- package/dist/shared/json-utils.js +16 -0
- package/dist/shared/repo-detector.js +0 -4
- package/dist/shared/xfg-template.d.ts +3 -0
- package/dist/shared/xfg-template.js +0 -20
- package/dist/sync/auth-options-builder.js +7 -1
- package/dist/sync/branch-manager.d.ts +1 -1
- package/dist/sync/commit-message.d.ts +1 -1
- package/dist/sync/commit-push-manager.d.ts +1 -1
- package/dist/sync/commit-push-manager.js +2 -2
- package/dist/sync/diff-utils.d.ts +15 -2
- package/dist/sync/diff-utils.js +50 -14
- package/dist/sync/file-sync-orchestrator.js +2 -4
- package/dist/sync/file-sync-strategy.js +11 -4
- package/dist/sync/file-writer.js +9 -4
- package/dist/sync/index.d.ts +2 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/manifest-manager.d.ts +1 -1
- package/dist/sync/manifest-manager.js +20 -6
- package/dist/sync/pr-merge-handler.js +6 -1
- package/dist/sync/repository-processor.js +8 -1
- package/dist/sync/types.d.ts +5 -4
- package/dist/vcs/authenticated-git-ops.d.ts +9 -1
- package/dist/vcs/authenticated-git-ops.js +7 -14
- package/dist/vcs/git-ops.js +29 -12
- package/dist/vcs/github-pr-strategy.js +6 -1
- package/dist/vcs/gitlab-pr-strategy.js +7 -2
- package/dist/vcs/graphql-commit-strategy.js +2 -1
- package/dist/vcs/index.d.ts +1 -0
- package/dist/vcs/index.js +2 -0
- package/dist/vcs/pr-creator.d.ts +5 -1
- package/dist/vcs/pr-creator.js +4 -4
- package/package.json +1 -1
- package/dist/output/index.d.ts +0 -5
- package/dist/output/index.js +0 -10
- package/dist/shared/index.d.ts +0 -15
- package/dist/shared/index.js +0 -30
|
@@ -1,22 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export type ReportFileChange
|
|
3
|
-
export interface SyncReport {
|
|
4
|
-
repos: RepoFileChanges[];
|
|
5
|
-
totals: {
|
|
6
|
-
files: {
|
|
7
|
-
create: number;
|
|
8
|
-
update: number;
|
|
9
|
-
delete: number;
|
|
10
|
-
};
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
export interface RepoFileChanges {
|
|
14
|
-
repoName: string;
|
|
15
|
-
files: ReportFileChange[];
|
|
16
|
-
prUrl?: string;
|
|
17
|
-
mergeOutcome?: "manual" | "auto" | "force" | "direct";
|
|
18
|
-
error?: string;
|
|
19
|
-
}
|
|
1
|
+
import type { SyncReport, RepoFileChanges, ReportFileChange } from "./types.js";
|
|
2
|
+
export type { SyncReport, RepoFileChanges, ReportFileChange };
|
|
20
3
|
export declare function formatSyncReportCLI(report: SyncReport): string[];
|
|
21
4
|
export declare function formatSyncReportMarkdown(report: SyncReport, dryRun: boolean): string;
|
|
22
5
|
export declare function writeSyncReportSummary(report: SyncReport, dryRun: boolean, summaryPath: string | undefined): void;
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
// src/output/sync-report.ts
|
|
2
1
|
import chalk from "chalk";
|
|
3
2
|
import { writeGitHubStepSummary } from "./github-summary.js";
|
|
3
|
+
import { formatCountEntry } from "./settings-report.js";
|
|
4
|
+
import { renderSyncLines } from "./unified-summary.js";
|
|
5
|
+
import { formatDiffLine } from "../sync/index.js";
|
|
4
6
|
function formatSyncSummary(totals) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
parts.push(`${totals.files.create} to create`);
|
|
12
|
-
if (totals.files.update > 0)
|
|
13
|
-
parts.push(`${totals.files.update} to update`);
|
|
14
|
-
if (totals.files.delete > 0)
|
|
15
|
-
parts.push(`${totals.files.delete} to delete`);
|
|
16
|
-
const fileWord = total === 1 ? "file" : "files";
|
|
17
|
-
return `Plan: ${total} ${fileWord} (${parts.join(", ")})`;
|
|
7
|
+
const entry = formatCountEntry("file", "files", [
|
|
8
|
+
{ label: "to create", value: totals.files.create },
|
|
9
|
+
{ label: "to update", value: totals.files.update },
|
|
10
|
+
{ label: "to delete", value: totals.files.delete },
|
|
11
|
+
]);
|
|
12
|
+
return entry ? `Plan: ${entry}` : "No changes";
|
|
18
13
|
}
|
|
19
14
|
export function formatSyncReportCLI(report) {
|
|
20
15
|
const lines = [];
|
|
@@ -35,6 +30,12 @@ export function formatSyncReportCLI(report) {
|
|
|
35
30
|
else if (file.action === "delete") {
|
|
36
31
|
lines.push(chalk.red(` - ${file.path}`));
|
|
37
32
|
}
|
|
33
|
+
// Content diff for structured data files
|
|
34
|
+
if (file.diffLines) {
|
|
35
|
+
for (const diffLine of file.diffLines) {
|
|
36
|
+
lines.push(` ${formatDiffLine(diffLine)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
38
39
|
}
|
|
39
40
|
// Error
|
|
40
41
|
if (repo.error) {
|
|
@@ -65,20 +66,7 @@ export function formatSyncReportMarkdown(report, dryRun) {
|
|
|
65
66
|
continue;
|
|
66
67
|
}
|
|
67
68
|
diffLines.push(`@@ ${repo.repoName} @@`);
|
|
68
|
-
|
|
69
|
-
if (file.action === "create") {
|
|
70
|
-
diffLines.push(`+ ${file.path}`);
|
|
71
|
-
}
|
|
72
|
-
else if (file.action === "update") {
|
|
73
|
-
diffLines.push(`! ${file.path}`);
|
|
74
|
-
}
|
|
75
|
-
else if (file.action === "delete") {
|
|
76
|
-
diffLines.push(`- ${file.path}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (repo.error) {
|
|
80
|
-
diffLines.push(`- Error: ${repo.error}`);
|
|
81
|
-
}
|
|
69
|
+
renderSyncLines(repo, diffLines);
|
|
82
70
|
}
|
|
83
71
|
if (diffLines.length > 0) {
|
|
84
72
|
lines.push("```diff");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FileChangeDetail } from "../sync/index.js";
|
|
2
|
+
export type ReportFileChange = FileChangeDetail;
|
|
3
|
+
export interface SyncReport {
|
|
4
|
+
repos: RepoFileChanges[];
|
|
5
|
+
totals: {
|
|
6
|
+
files: {
|
|
7
|
+
create: number;
|
|
8
|
+
update: number;
|
|
9
|
+
delete: number;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface RepoFileChanges {
|
|
14
|
+
repoName: string;
|
|
15
|
+
files: ReportFileChange[];
|
|
16
|
+
prUrl?: string;
|
|
17
|
+
mergeOutcome?: "manual" | "auto" | "force" | "direct";
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LifecycleReport } from "./lifecycle-report.js";
|
|
2
|
-
import type { SyncReport } from "./
|
|
2
|
+
import type { SyncReport, RepoFileChanges } from "./types.js";
|
|
3
3
|
import type { SettingsReport } from "./settings-report.js";
|
|
4
4
|
interface UnifiedSummaryInput {
|
|
5
5
|
lifecycle?: LifecycleReport;
|
|
@@ -8,6 +8,7 @@ interface UnifiedSummaryInput {
|
|
|
8
8
|
dryRun: boolean;
|
|
9
9
|
summaryPath?: string | undefined;
|
|
10
10
|
}
|
|
11
|
+
export declare function renderSyncLines(syncRepo: RepoFileChanges, diffLines: string[]): void;
|
|
11
12
|
export declare function formatUnifiedSummaryMarkdown(input: UnifiedSummaryInput): string;
|
|
12
13
|
export declare function writeUnifiedSummary(input: UnifiedSummaryInput): void;
|
|
13
14
|
export {};
|
|
@@ -130,7 +130,7 @@ function renderLifecycleLines(lcAction, diffLines) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
-
function renderSyncLines(syncRepo, diffLines) {
|
|
133
|
+
export function renderSyncLines(syncRepo, diffLines) {
|
|
134
134
|
for (const file of syncRepo.files) {
|
|
135
135
|
if (file.action === "create") {
|
|
136
136
|
diffLines.push(`+ ${file.path}`);
|
|
@@ -141,6 +141,9 @@ function renderSyncLines(syncRepo, diffLines) {
|
|
|
141
141
|
else if (file.action === "delete") {
|
|
142
142
|
diffLines.push(`- ${file.path}`);
|
|
143
143
|
}
|
|
144
|
+
if (file.diffLines) {
|
|
145
|
+
diffLines.push(...file.diffLines);
|
|
146
|
+
}
|
|
144
147
|
}
|
|
145
148
|
if (syncRepo.error) {
|
|
146
149
|
diffLines.push(`- Error: ${syncRepo.error}`);
|
|
@@ -33,6 +33,8 @@ interface SettingsGuards<TOptions extends BaseProcessorOptions, TResult extends
|
|
|
33
33
|
* empty settings check, token resolution, and error wrapping.
|
|
34
34
|
*/
|
|
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
|
+
/** Common action literals shared by all settings processors. */
|
|
37
|
+
export type SettingsAction = "create" | "update" | "delete" | "unchanged";
|
|
36
38
|
export interface ChangeCounts {
|
|
37
39
|
create: number;
|
|
38
40
|
update: number;
|
|
@@ -44,7 +46,7 @@ export interface ChangeCounts {
|
|
|
44
46
|
* Works with any change type that has an `action` field.
|
|
45
47
|
*/
|
|
46
48
|
export declare function countActions(changes: ReadonlyArray<{
|
|
47
|
-
action:
|
|
49
|
+
action: SettingsAction;
|
|
48
50
|
}>): ChangeCounts;
|
|
49
51
|
export declare function formatChangeSummary(counts: ChangeCounts): string;
|
|
50
52
|
/**
|
|
@@ -39,12 +39,16 @@ export async function withGitHubGuards(repoConfig, repoInfo, options, guards) {
|
|
|
39
39
|
* Works with any change type that has an `action` field.
|
|
40
40
|
*/
|
|
41
41
|
export function countActions(changes) {
|
|
42
|
-
|
|
43
|
-
create:
|
|
44
|
-
update:
|
|
45
|
-
delete:
|
|
46
|
-
unchanged:
|
|
42
|
+
const counts = {
|
|
43
|
+
create: 0,
|
|
44
|
+
update: 0,
|
|
45
|
+
delete: 0,
|
|
46
|
+
unchanged: 0,
|
|
47
47
|
};
|
|
48
|
+
for (const c of changes) {
|
|
49
|
+
counts[c.action]++;
|
|
50
|
+
}
|
|
51
|
+
return counts;
|
|
48
52
|
}
|
|
49
53
|
export function formatChangeSummary(counts) {
|
|
50
54
|
const parts = [];
|
package/dist/settings/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type ISettingsProcessor } from "./base-processor.js";
|
|
1
|
+
export { type ISettingsProcessor, type SettingsAction, } from "./base-processor.js";
|
|
2
2
|
export { type PropertyDiff, formatPropertyTree, 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,6 +1,7 @@
|
|
|
1
1
|
import type { Label } from "../../config/types.js";
|
|
2
2
|
import type { GitHubLabel } from "./types.js";
|
|
3
|
-
|
|
3
|
+
import type { SettingsAction } from "../base-processor.js";
|
|
4
|
+
export type LabelAction = SettingsAction;
|
|
4
5
|
export interface LabelChange {
|
|
5
6
|
action: LabelAction;
|
|
6
7
|
name: string;
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import { countActions } from "../base-processor.js";
|
|
2
3
|
/**
|
|
3
4
|
* Format label changes as a Terraform-style plan.
|
|
4
5
|
*/
|
|
5
6
|
export function formatLabelsPlan(changes) {
|
|
6
7
|
const lines = [];
|
|
7
8
|
const entries = [];
|
|
9
|
+
const { create: creates, update: updates, delete: deletes, unchanged, } = countActions(changes);
|
|
8
10
|
const createChanges = changes.filter((c) => c.action === "create");
|
|
9
11
|
const updateChanges = changes.filter((c) => c.action === "update");
|
|
10
12
|
const deleteChanges = changes.filter((c) => c.action === "delete");
|
|
11
13
|
const unchangedItems = changes.filter((c) => c.action === "unchanged");
|
|
12
|
-
const creates = createChanges.length;
|
|
13
|
-
const updates = updateChanges.length;
|
|
14
|
-
const deletes = deleteChanges.length;
|
|
15
|
-
const unchanged = unchangedItems.length;
|
|
16
14
|
// Format creates
|
|
17
15
|
if (createChanges.length > 0) {
|
|
18
16
|
lines.push(chalk.bold(" Create:"));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assertGitHubRepo } from "../../shared/repo-detector.js";
|
|
2
|
-
import { GhApiClient
|
|
2
|
+
import { GhApiClient } from "../../shared/gh-api-utils.js";
|
|
3
|
+
import { parseApiJson } from "../../shared/json-utils.js";
|
|
3
4
|
export class GitHubLabelsStrategy {
|
|
4
5
|
api;
|
|
5
6
|
constructor(executor, options) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assertGitHubRepo, } from "../../shared/repo-detector.js";
|
|
2
|
-
import { GhApiClient,
|
|
2
|
+
import { GhApiClient, isHttp404Error, } from "../../shared/gh-api-utils.js";
|
|
3
|
+
import { parseApiJson } from "../../shared/json-utils.js";
|
|
3
4
|
import { camelToSnake } from "../../shared/string-utils.js";
|
|
4
5
|
/**
|
|
5
6
|
* Converts GitHubRepoSettings (camelCase) to GitHub API format (snake_case).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Ruleset } from "../../config/index.js";
|
|
2
|
+
import { type SettingsAction } from "../base-processor.js";
|
|
2
3
|
import type { GitHubRuleset } from "./types.js";
|
|
3
|
-
export type RulesetAction =
|
|
4
|
+
export type RulesetAction = SettingsAction;
|
|
4
5
|
export interface RulesetChange {
|
|
5
6
|
action: RulesetAction;
|
|
6
7
|
name: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RULESET_COMPARABLE_FIELDS } from "../../config/index.js";
|
|
2
2
|
import { isPlainObject } from "../../shared/type-guards.js";
|
|
3
3
|
import { camelToSnake } from "../../shared/string-utils.js";
|
|
4
|
+
import { countActions } from "../base-processor.js";
|
|
4
5
|
/**
|
|
5
6
|
* Normalizes a value recursively, converting keys to a consistent format (snake_case).
|
|
6
7
|
* This allows comparing GitHub API responses (snake_case) with config (camelCase).
|
|
@@ -128,34 +129,54 @@ function projectObjects(current, desired) {
|
|
|
128
129
|
}
|
|
129
130
|
return result;
|
|
130
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Candidate keys for matching array items by identity rather than index.
|
|
134
|
+
* Order matters — first key found across all items wins.
|
|
135
|
+
*/
|
|
136
|
+
const MATCH_KEY_CANDIDATES = ["type", "actor_id"];
|
|
137
|
+
/**
|
|
138
|
+
* Finds a key that uniquely identifies items in both arrays.
|
|
139
|
+
* Returns the first candidate key present in every item of both arrays, or undefined.
|
|
140
|
+
*/
|
|
141
|
+
function findMatchKey(current, desired) {
|
|
142
|
+
const allItems = [...current, ...desired];
|
|
143
|
+
if (allItems.length === 0)
|
|
144
|
+
return undefined;
|
|
145
|
+
for (const candidate of MATCH_KEY_CANDIDATES) {
|
|
146
|
+
const everyItemHasKey = allItems.every((item) => isPlainObject(item) && candidate in item);
|
|
147
|
+
if (everyItemHasKey)
|
|
148
|
+
return candidate;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
131
152
|
function projectArrays(current, desired) {
|
|
132
153
|
// Primitive arrays — return current as-is
|
|
133
154
|
if (desired.length === 0 || !isPlainObject(desired[0])) {
|
|
134
155
|
return current;
|
|
135
156
|
}
|
|
136
|
-
// Arrays of objects — match by
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
return
|
|
157
|
+
// Arrays of objects — match by identifying key if available
|
|
158
|
+
const matchKey = findMatchKey(current, desired);
|
|
159
|
+
if (matchKey) {
|
|
160
|
+
return matchByKey(current, desired, matchKey);
|
|
140
161
|
}
|
|
141
162
|
// Fallback: match by index
|
|
142
163
|
return matchByIndex(current, desired);
|
|
143
164
|
}
|
|
144
|
-
function
|
|
145
|
-
const
|
|
165
|
+
function matchByKey(current, desired, key) {
|
|
166
|
+
const currentByKey = new Map();
|
|
146
167
|
for (const item of current) {
|
|
147
168
|
if (isPlainObject(item)) {
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
150
|
-
|
|
169
|
+
const keyValue = item[key];
|
|
170
|
+
if (keyValue !== undefined)
|
|
171
|
+
currentByKey.set(keyValue, item);
|
|
151
172
|
}
|
|
152
173
|
}
|
|
153
|
-
const
|
|
174
|
+
const desiredKeys = new Set();
|
|
154
175
|
const result = [];
|
|
155
176
|
for (const desiredItem of desired) {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
const currentItem =
|
|
177
|
+
const keyValue = desiredItem[key];
|
|
178
|
+
desiredKeys.add(keyValue);
|
|
179
|
+
const currentItem = currentByKey.get(keyValue);
|
|
159
180
|
if (currentItem) {
|
|
160
181
|
result.push(projectToDesiredShape(currentItem, desiredItem));
|
|
161
182
|
}
|
|
@@ -165,8 +186,8 @@ function matchByType(current, desired) {
|
|
|
165
186
|
// deepEqual must detect (length mismatch). Fixes #549.
|
|
166
187
|
for (const item of current) {
|
|
167
188
|
if (isPlainObject(item)) {
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
189
|
+
const keyValue = item[key];
|
|
190
|
+
if (keyValue !== undefined && !desiredKeys.has(keyValue)) {
|
|
170
191
|
result.push(item);
|
|
171
192
|
}
|
|
172
193
|
}
|
|
@@ -286,12 +307,7 @@ export function formatDiff(changes) {
|
|
|
286
307
|
lines.push(formatChange(change));
|
|
287
308
|
}
|
|
288
309
|
// Summary
|
|
289
|
-
const counts =
|
|
290
|
-
create: changes.filter((c) => c.action === "create").length,
|
|
291
|
-
update: changes.filter((c) => c.action === "update").length,
|
|
292
|
-
delete: changes.filter((c) => c.action === "delete").length,
|
|
293
|
-
unchanged: changes.filter((c) => c.action === "unchanged").length,
|
|
294
|
-
};
|
|
310
|
+
const counts = countActions(changes);
|
|
295
311
|
lines.push("");
|
|
296
312
|
lines.push("Summary:");
|
|
297
313
|
const parts = [];
|
|
@@ -118,6 +118,49 @@ function getActionStyle(action) {
|
|
|
118
118
|
return { symbol: "~", color: chalk.yellow };
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Render a leaf tree node (no children) with its value.
|
|
123
|
+
*/
|
|
124
|
+
function renderLeafNode(child, style, indentStr, indent) {
|
|
125
|
+
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) {
|
|
133
|
+
lines.push(style.color(`${indentStr}${style.symbol} ${child.name}:`));
|
|
134
|
+
lines.push(...renderNestedValue(child.newValue, child.action, indent + 1));
|
|
135
|
+
}
|
|
136
|
+
else if (child.action === "remove" && hasComplexOld) {
|
|
137
|
+
lines.push(style.color(`${indentStr}${style.symbol} ${child.name} (removed):`));
|
|
138
|
+
lines.push(...renderNestedValue(child.oldValue, child.action, indent + 1));
|
|
139
|
+
}
|
|
140
|
+
else if (child.action === "change" && (hasComplexNew || hasComplexOld)) {
|
|
141
|
+
lines.push(style.color(`${indentStr}${style.symbol} ${child.name}:`));
|
|
142
|
+
if (hasComplexOld) {
|
|
143
|
+
lines.push(...renderNestedValue(child.oldValue, "remove", indent + 1));
|
|
144
|
+
}
|
|
145
|
+
if (hasComplexNew) {
|
|
146
|
+
lines.push(...renderNestedValue(child.newValue, "add", indent + 1));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
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
|
+
return lines;
|
|
163
|
+
}
|
|
121
164
|
/**
|
|
122
165
|
* Recursively render tree nodes to formatted lines.
|
|
123
166
|
*/
|
|
@@ -135,44 +178,7 @@ function renderTree(node, indent = 0) {
|
|
|
135
178
|
lines.push(...renderTree(child, indent + 1));
|
|
136
179
|
}
|
|
137
180
|
else {
|
|
138
|
-
|
|
139
|
-
const hasComplexNew = isPlainObject(child.newValue) ||
|
|
140
|
-
(Array.isArray(child.newValue) &&
|
|
141
|
-
child.newValue.some((v) => isPlainObject(v)));
|
|
142
|
-
const hasComplexOld = isPlainObject(child.oldValue) ||
|
|
143
|
-
(Array.isArray(child.oldValue) &&
|
|
144
|
-
child.oldValue.some((v) => isPlainObject(v)));
|
|
145
|
-
if (child.action === "add" && hasComplexNew) {
|
|
146
|
-
lines.push(style.color(`${indentStr}${style.symbol} ${child.name}:`));
|
|
147
|
-
lines.push(...renderNestedValue(child.newValue, child.action, indent + 1));
|
|
148
|
-
}
|
|
149
|
-
else if (child.action === "remove" && hasComplexOld) {
|
|
150
|
-
lines.push(style.color(`${indentStr}${style.symbol} ${child.name} (removed):`));
|
|
151
|
-
lines.push(...renderNestedValue(child.oldValue, child.action, indent + 1));
|
|
152
|
-
}
|
|
153
|
-
else if (child.action === "change" &&
|
|
154
|
-
(hasComplexNew || hasComplexOld)) {
|
|
155
|
-
lines.push(style.color(`${indentStr}${style.symbol} ${child.name}:`));
|
|
156
|
-
if (hasComplexOld) {
|
|
157
|
-
lines.push(...renderNestedValue(child.oldValue, "remove", indent + 1));
|
|
158
|
-
}
|
|
159
|
-
if (hasComplexNew) {
|
|
160
|
-
lines.push(...renderNestedValue(child.newValue, "add", indent + 1));
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
let valuePart = "";
|
|
165
|
-
if (child.action === "change") {
|
|
166
|
-
valuePart = `: ${formatValue(child.oldValue)} → ${formatValue(child.newValue)}`;
|
|
167
|
-
}
|
|
168
|
-
else if (child.action === "add") {
|
|
169
|
-
valuePart = `: ${formatValue(child.newValue)}`;
|
|
170
|
-
}
|
|
171
|
-
else if (child.action === "remove") {
|
|
172
|
-
valuePart = ` (was: ${formatValue(child.oldValue)})`;
|
|
173
|
-
}
|
|
174
|
-
lines.push(style.color(`${indentStr}${style.symbol} ${child.name}${valuePart}`));
|
|
175
|
-
}
|
|
181
|
+
lines.push(...renderLeafNode(child, style, indentStr, indent));
|
|
176
182
|
}
|
|
177
183
|
}
|
|
178
184
|
return lines;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assertGitHubRepo } from "../../shared/repo-detector.js";
|
|
2
2
|
import { camelToSnake } from "../../shared/string-utils.js";
|
|
3
|
-
import { GhApiClient
|
|
3
|
+
import { GhApiClient } from "../../shared/gh-api-utils.js";
|
|
4
|
+
import { parseApiJson } from "../../shared/json-utils.js";
|
|
4
5
|
/**
|
|
5
6
|
* Converts camelCase config ruleset to snake_case GitHub API format.
|
|
6
7
|
*/
|
|
@@ -31,12 +31,19 @@ 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 {
|
|
35
|
+
repoInfo: GitHubRepoInfo;
|
|
36
|
+
tokenManager: ITokenManager | null;
|
|
37
|
+
context: string;
|
|
38
|
+
log?: DebugLog;
|
|
39
|
+
envToken?: string;
|
|
40
|
+
}
|
|
34
41
|
/**
|
|
35
42
|
* Resolve a GitHub token for a repo: GitHub App token → envToken fallback.
|
|
36
43
|
* Returns { token, skipped } where skipped=true means no App installation found
|
|
37
44
|
* for this owner (token will be undefined). Both sync and settings paths use this.
|
|
38
45
|
*/
|
|
39
|
-
export declare function resolveGitHubToken(
|
|
46
|
+
export declare function resolveGitHubToken(options: ResolveGitHubTokenOptions): Promise<{
|
|
40
47
|
token: string | undefined;
|
|
41
48
|
skipped: boolean;
|
|
42
49
|
}>;
|
|
@@ -44,10 +51,4 @@ export declare function resolveGitHubToken(repoInfo: GitHubRepoInfo, tokenManage
|
|
|
44
51
|
* Check if an error message indicates an HTTP 404 response from the GitHub API.
|
|
45
52
|
*/
|
|
46
53
|
export declare function isHttp404Error(error: unknown): boolean;
|
|
47
|
-
/**
|
|
48
|
-
* Parse a JSON API response with a contextual error message.
|
|
49
|
-
* Wraps JSON.parse so callers get "Failed to parse <context>: ..." instead of
|
|
50
|
-
* a bare "Unexpected token" SyntaxError.
|
|
51
|
-
*/
|
|
52
|
-
export declare function parseApiJson<T>(response: string, context: string): T;
|
|
53
54
|
export {};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { escapeShellArg } from "./shell-utils.js";
|
|
2
2
|
import { withRetry } from "./retry-utils.js";
|
|
3
3
|
import { toErrorMessage } from "./type-guards.js";
|
|
4
|
-
import { SyncError } from "./errors.js";
|
|
5
4
|
/**
|
|
6
5
|
* Get the hostname flag for gh commands.
|
|
7
6
|
* Returns "--hostname HOST" for GHE, empty string for github.com.
|
|
@@ -78,7 +77,8 @@ export class GhApiClient {
|
|
|
78
77
|
* Returns { token, skipped } where skipped=true means no App installation found
|
|
79
78
|
* for this owner (token will be undefined). Both sync and settings paths use this.
|
|
80
79
|
*/
|
|
81
|
-
export async function resolveGitHubToken(
|
|
80
|
+
export async function resolveGitHubToken(options) {
|
|
81
|
+
const { repoInfo, tokenManager, context, log, envToken } = options;
|
|
82
82
|
try {
|
|
83
83
|
const appToken = await tokenManager?.getTokenForRepo(repoInfo);
|
|
84
84
|
if (appToken === null) {
|
|
@@ -102,17 +102,3 @@ export async function resolveGitHubToken(repoInfo, tokenManager, context, log, e
|
|
|
102
102
|
export function isHttp404Error(error) {
|
|
103
103
|
return toErrorMessage(error).includes("HTTP 404");
|
|
104
104
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Parse a JSON API response with a contextual error message.
|
|
107
|
-
* Wraps JSON.parse so callers get "Failed to parse <context>: ..." instead of
|
|
108
|
-
* a bare "Unexpected token" SyntaxError.
|
|
109
|
-
*/
|
|
110
|
-
export function parseApiJson(response, context) {
|
|
111
|
-
try {
|
|
112
|
-
return JSON.parse(response);
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
const preview = response.slice(0, 200);
|
|
116
|
-
throw new SyncError(`Failed to parse ${context}: ${toErrorMessage(error)} — ${preview}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
@@ -28,4 +28,7 @@ export declare function interpolateString(value: string, config: InterpolationCo
|
|
|
28
28
|
/**
|
|
29
29
|
* Recursively process a value, interpolating strings within objects and arrays.
|
|
30
30
|
*/
|
|
31
|
+
export declare function interpolateValue(value: string, config: InterpolationConfig): string;
|
|
32
|
+
export declare function interpolateValue(value: unknown[], config: InterpolationConfig): unknown[];
|
|
33
|
+
export declare function interpolateValue(value: Record<string, unknown>, config: InterpolationConfig): Record<string, unknown>;
|
|
31
34
|
export declare function interpolateValue(value: unknown, config: InterpolationConfig): unknown;
|
|
@@ -28,9 +28,6 @@ export function interpolateString(value, config) {
|
|
|
28
28
|
});
|
|
29
29
|
return processed;
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Recursively process a value, interpolating strings within objects and arrays.
|
|
33
|
-
*/
|
|
34
31
|
export function interpolateValue(value, config) {
|
|
35
32
|
if (typeof value === "string") {
|
|
36
33
|
return interpolateString(value, config);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a JSON API response with a contextual error message.
|
|
3
|
+
* Wraps JSON.parse so callers get "Failed to parse <context>: ..." instead of
|
|
4
|
+
* a bare "Unexpected token" SyntaxError.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseApiJson<T>(response: string, context: string): T;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { toErrorMessage } from "./type-guards.js";
|
|
2
|
+
import { SyncError } from "./errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* Parse a JSON API response with a contextual error message.
|
|
5
|
+
* Wraps JSON.parse so callers get "Failed to parse <context>: ..." instead of
|
|
6
|
+
* a bare "Unexpected token" SyntaxError.
|
|
7
|
+
*/
|
|
8
|
+
export function parseApiJson(response, context) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(response);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
const preview = response.slice(0, 200);
|
|
14
|
+
throw new SyncError(`Failed to parse ${context}: ${toErrorMessage(error)} — ${preview}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -83,7 +83,6 @@ function isGitLabStyleUrl(gitUrl) {
|
|
|
83
83
|
return false;
|
|
84
84
|
}
|
|
85
85
|
export function detectRepoType(gitUrl, context) {
|
|
86
|
-
// Check for GitHub Enterprise hosts first (if configured)
|
|
87
86
|
if (context?.githubHosts?.length) {
|
|
88
87
|
const host = extractHostFromUrl(gitUrl)?.toLowerCase();
|
|
89
88
|
const normalizedHosts = context.githubHosts.map((h) => h.toLowerCase());
|
|
@@ -91,19 +90,16 @@ export function detectRepoType(gitUrl, context) {
|
|
|
91
90
|
return "github";
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
|
-
// Check for Azure DevOps formats (most specific patterns)
|
|
95
93
|
for (const pattern of AZURE_DEVOPS_URL_PATTERNS) {
|
|
96
94
|
if (pattern.test(gitUrl)) {
|
|
97
95
|
return "azure-devops";
|
|
98
96
|
}
|
|
99
97
|
}
|
|
100
|
-
// Check for GitHub formats
|
|
101
98
|
for (const pattern of GITHUB_URL_PATTERNS) {
|
|
102
99
|
if (pattern.test(gitUrl)) {
|
|
103
100
|
return "github";
|
|
104
101
|
}
|
|
105
102
|
}
|
|
106
|
-
// Check for GitLab SaaS formats
|
|
107
103
|
for (const pattern of GITLAB_SAAS_URL_PATTERNS) {
|
|
108
104
|
if (pattern.test(gitUrl)) {
|
|
109
105
|
return "gitlab";
|
|
@@ -40,5 +40,8 @@ interface XfgInterpolationOptions {
|
|
|
40
40
|
* @param options - Interpolation options (default: strict mode)
|
|
41
41
|
* @returns Content with interpolated values
|
|
42
42
|
*/
|
|
43
|
+
export declare function interpolateXfgContent(content: string, ctx: XfgTemplateContext, options?: XfgInterpolationOptions): string;
|
|
44
|
+
export declare function interpolateXfgContent(content: string[], ctx: XfgTemplateContext, options?: XfgInterpolationOptions): string[];
|
|
45
|
+
export declare function interpolateXfgContent(content: Record<string, unknown>, ctx: XfgTemplateContext, options?: XfgInterpolationOptions): Record<string, unknown>;
|
|
43
46
|
export declare function interpolateXfgContent(content: TemplateContent, ctx: XfgTemplateContext, options?: XfgInterpolationOptions): TemplateContent;
|
|
44
47
|
export {};
|