@aspruyt/xfg 6.0.3 → 6.2.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/lifecycle-report-builder.d.ts +2 -2
- package/dist/cli/lifecycle-report-builder.js +3 -11
- package/dist/cli/program.d.ts +2 -1
- package/dist/cli/program.js +2 -3
- package/dist/cli/repo-sync-runner.d.ts +24 -0
- package/dist/cli/repo-sync-runner.js +156 -0
- package/dist/cli/results-collector.d.ts +1 -1
- package/dist/cli/results-collector.js +2 -2
- package/dist/cli/settings-factories.d.ts +7 -0
- package/dist/cli/settings-factories.js +27 -0
- package/dist/cli/settings-report-builder.d.ts +1 -1
- package/dist/cli/settings-report-builder.js +12 -23
- package/dist/cli/settings-runner.d.ts +2 -0
- package/dist/cli/settings-runner.js +87 -0
- package/dist/cli/sync-command.d.ts +1 -1
- package/dist/cli/sync-command.js +31 -372
- package/dist/cli/sync-report-builder.d.ts +1 -1
- package/dist/cli/sync-utils.d.ts +8 -0
- package/dist/cli/sync-utils.js +36 -0
- package/dist/cli/types.d.ts +5 -7
- package/dist/cli/unified-summary.d.ts +1 -3
- package/dist/cli/unified-summary.js +7 -5
- package/dist/cli.js +2 -1
- package/dist/{shared → config}/env.js +2 -2
- package/dist/config/extends-resolver.js +4 -3
- package/dist/config/file-reference-resolver.js +4 -2
- package/dist/config/formatter.js +18 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/loader.js +30 -6
- package/dist/config/merge.d.ts +11 -1
- package/dist/config/merge.js +78 -6
- package/dist/config/normalizer.js +53 -38
- package/dist/config/validator.d.ts +1 -4
- package/dist/config/validator.js +13 -599
- package/dist/config/validators/file-validator.d.ts +2 -1
- package/dist/config/validators/file-validator.js +9 -1
- package/dist/config/validators/group-validator.d.ts +3 -0
- package/dist/config/validators/group-validator.js +167 -0
- package/dist/config/validators/repo-entry-validator.d.ts +2 -0
- package/dist/config/validators/repo-entry-validator.js +165 -0
- package/dist/config/validators/repo-settings-validator.js +18 -7
- package/dist/config/validators/ruleset-validator.js +2 -5
- package/dist/config/validators/shared.d.ts +11 -0
- package/dist/config/validators/shared.js +242 -0
- package/dist/lifecycle/ado-migration-source.js +2 -4
- package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
- package/dist/lifecycle/github-lifecycle-provider.js +125 -136
- package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
- package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
- package/dist/lifecycle/index.d.ts +2 -2
- package/dist/lifecycle/index.js +1 -1
- package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
- package/dist/output/github-summary.js +2 -3
- package/dist/output/index.d.ts +4 -0
- package/dist/output/index.js +4 -0
- package/dist/output/lifecycle-report.d.ts +1 -1
- package/dist/output/lifecycle-report.js +5 -0
- package/dist/output/sync-report.d.ts +25 -3
- package/dist/output/sync-report.js +11 -11
- package/dist/settings/base-processor.d.ts +18 -7
- package/dist/settings/base-processor.js +26 -5
- package/dist/settings/code-scanning/diff.js +2 -2
- package/dist/settings/code-scanning/formatter.d.ts +2 -6
- package/dist/settings/code-scanning/formatter.js +2 -25
- package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
- package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
- package/dist/settings/code-scanning/processor.js +6 -4
- package/dist/settings/code-scanning/types.d.ts +10 -8
- package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
- package/dist/settings/labels/types.d.ts +12 -10
- package/dist/settings/repo-settings/diff.d.ts +1 -1
- package/dist/settings/repo-settings/diff.js +1 -1
- package/dist/settings/repo-settings/formatter.d.ts +2 -6
- package/dist/settings/repo-settings/formatter.js +4 -23
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
- package/dist/settings/repo-settings/processor.js +11 -11
- package/dist/settings/repo-settings/types.d.ts +2 -2
- package/dist/settings/rulesets/diff-algorithm.js +4 -2
- package/dist/settings/rulesets/diff.js +2 -51
- package/dist/settings/rulesets/formatter.js +4 -0
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
- package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
- package/dist/settings/rulesets/index.d.ts +1 -1
- package/dist/settings/rulesets/index.js +0 -2
- package/dist/settings/rulesets/processor.js +1 -1
- package/dist/settings/rulesets/types.d.ts +6 -2
- package/dist/shared/command-executor.d.ts +4 -4
- package/dist/shared/command-executor.js +9 -7
- package/dist/shared/diff-format.d.ts +1 -0
- package/dist/shared/diff-format.js +10 -0
- package/dist/shared/errors.d.ts +7 -4
- package/dist/shared/errors.js +8 -8
- package/dist/shared/gh-api-utils.d.ts +3 -34
- package/dist/shared/gh-api-utils.js +23 -53
- package/dist/shared/gh-token-utils.d.ts +26 -0
- package/dist/shared/gh-token-utils.js +32 -0
- package/dist/shared/json-utils.js +1 -1
- package/dist/shared/regex-utils.d.ts +1 -0
- package/dist/shared/regex-utils.js +3 -0
- package/dist/shared/retry-utils.d.ts +1 -0
- package/dist/shared/retry-utils.js +13 -7
- package/dist/sync/auth-options-builder.js +1 -1
- package/dist/sync/branch-manager.js +5 -3
- package/dist/sync/commit-push-manager.js +2 -3
- package/dist/sync/diff-utils.d.ts +0 -1
- package/dist/sync/diff-utils.js +5 -10
- package/dist/sync/file-sync-orchestrator.js +0 -2
- package/dist/sync/file-writer.d.ts +3 -0
- package/dist/sync/file-writer.js +84 -81
- package/dist/sync/index.d.ts +0 -1
- package/dist/sync/index.js +0 -1
- package/dist/sync/manifest.js +1 -1
- package/dist/sync/pr-merge-handler.js +6 -6
- package/dist/sync/sync-workflow.js +1 -1
- package/dist/sync/types.d.ts +2 -2
- package/dist/vcs/ado-pr-strategy.d.ts +3 -5
- package/dist/vcs/ado-pr-strategy.js +131 -33
- package/dist/vcs/authenticated-git-ops.js +45 -23
- package/dist/vcs/git-commit-strategy.js +10 -6
- package/dist/vcs/git-ops.js +30 -24
- package/dist/vcs/github-pr-strategy.d.ts +3 -2
- package/dist/vcs/github-pr-strategy.js +80 -30
- package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
- package/dist/vcs/gitlab-pr-strategy.js +88 -87
- package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
- package/dist/vcs/graphql-commit-strategy.js +21 -37
- package/dist/vcs/pr-creator.js +9 -2
- package/dist/vcs/pr-strategy.d.ts +2 -3
- package/dist/vcs/pr-strategy.js +0 -1
- package/dist/vcs/types.d.ts +9 -5
- package/package.json +5 -5
- package/dist/config/validators/index.d.ts +0 -3
- package/dist/config/validators/index.js +0 -6
- package/dist/output/types.d.ts +0 -20
- package/dist/output/types.js +0 -1
- package/dist/shared/shell-utils.d.ts +0 -6
- package/dist/shared/shell-utils.js +0 -17
- /package/dist/{shared → config}/env.d.ts +0 -0
- /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
- /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
- /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
- /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
package/dist/config/validator.js
CHANGED
|
@@ -1,259 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isTextContent, isObjectContent, isStructuredFileExtension, validateFileName, VALID_STRATEGIES, } from "./validators/file-validator.js";
|
|
3
|
-
import { validateRepoSettings } from "./validators/repo-settings-validator.js";
|
|
4
|
-
import { validateRuleset } from "./validators/ruleset-validator.js";
|
|
5
|
-
import { escapeRegExp } from "../shared/shell-utils.js";
|
|
1
|
+
import { validateFileName } from "./validators/file-validator.js";
|
|
6
2
|
import { isPlainObject } from "../shared/type-guards.js";
|
|
7
3
|
import { ValidationError } from "../shared/errors.js";
|
|
8
|
-
|
|
4
|
+
import { validateFileConfigFields, validateSettings, } from "./validators/shared.js";
|
|
5
|
+
import { validateGroups, validateConditionalGroups, } from "./validators/group-validator.js";
|
|
6
|
+
import { validateRepoEntry } from "./validators/repo-entry-validator.js";
|
|
9
7
|
const CONFIG_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
10
8
|
const CONFIG_ID_MAX_LENGTH = 64;
|
|
11
|
-
/**
|
|
12
|
-
* Check if a string looks like a valid git URL.
|
|
13
|
-
* Supports SSH (git@host:path) and HTTPS (https://host/path) formats.
|
|
14
|
-
*/
|
|
15
|
-
function isValidGitUrl(url) {
|
|
16
|
-
// SSH format: git@hostname:path OR HTTPS format: https://hostname/path
|
|
17
|
-
return /^git@[^:]+:.+$/.test(url) || /^https?:\/\/[^/]+\/.+$/.test(url);
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Check if a git URL points to GitHub (github.com).
|
|
21
|
-
* Used to reject GitHub URLs as migration sources (not supported).
|
|
22
|
-
*/
|
|
23
|
-
function isGitHubUrl(url, githubHosts) {
|
|
24
|
-
const hosts = ["github.com", ...(githubHosts ?? [])];
|
|
25
|
-
for (const host of hosts) {
|
|
26
|
-
if (url.startsWith(`git@${host}:`) ||
|
|
27
|
-
url.match(new RegExp(`^https?://${escapeRegExp(host)}/`))) {
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
function getGitDisplayName(git) {
|
|
34
|
-
if (Array.isArray(git)) {
|
|
35
|
-
return git[0] || "unknown";
|
|
36
|
-
}
|
|
37
|
-
return git;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Validate file config fields shared between root files and per-repo overrides.
|
|
41
|
-
*/
|
|
42
|
-
function validateFileConfigFields(fileConfig, fileName, context) {
|
|
43
|
-
if (fileConfig.content !== undefined) {
|
|
44
|
-
const hasText = isTextContent(fileConfig.content);
|
|
45
|
-
const hasObject = isObjectContent(fileConfig.content);
|
|
46
|
-
if (!hasText && !hasObject) {
|
|
47
|
-
throw new ValidationError(`${context} file '${fileName}' content must be an object, string, or array of strings`);
|
|
48
|
-
}
|
|
49
|
-
const isStructured = isStructuredFileExtension(fileName);
|
|
50
|
-
if (isStructured && hasText) {
|
|
51
|
-
throw new ValidationError(`${context} file '${fileName}' has JSON/YAML extension but string content. Use object content for structured files.`);
|
|
52
|
-
}
|
|
53
|
-
if (!isStructured && hasObject) {
|
|
54
|
-
throw new ValidationError(`${context} file '${fileName}' has text extension but object content. Use string or string[] for text files, or use .json/.yaml/.yml extension.`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (fileConfig.mergeStrategy !== undefined &&
|
|
58
|
-
!VALID_STRATEGIES.includes(fileConfig.mergeStrategy)) {
|
|
59
|
-
throw new ValidationError(`${context} file '${fileName}' has invalid mergeStrategy: ${fileConfig.mergeStrategy}. Must be one of: ${VALID_STRATEGIES.join(", ")}`);
|
|
60
|
-
}
|
|
61
|
-
const booleanFields = [
|
|
62
|
-
"createOnly",
|
|
63
|
-
"executable",
|
|
64
|
-
"template",
|
|
65
|
-
"deleteOrphaned",
|
|
66
|
-
];
|
|
67
|
-
for (const field of booleanFields) {
|
|
68
|
-
if (fileConfig[field] !== undefined &&
|
|
69
|
-
typeof fileConfig[field] !== "boolean") {
|
|
70
|
-
throw new ValidationError(`${context} file '${fileName}' ${field} must be a boolean`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
const stringFields = ["schemaUrl"];
|
|
74
|
-
for (const field of stringFields) {
|
|
75
|
-
if (fileConfig[field] !== undefined &&
|
|
76
|
-
typeof fileConfig[field] !== "string") {
|
|
77
|
-
throw new ValidationError(`${context} file '${fileName}' ${field} must be a string`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (fileConfig.header !== undefined) {
|
|
81
|
-
if (typeof fileConfig.header !== "string" &&
|
|
82
|
-
(!Array.isArray(fileConfig.header) ||
|
|
83
|
-
!fileConfig.header.every((h) => typeof h === "string"))) {
|
|
84
|
-
throw new ValidationError(`${context} file '${fileName}' header must be a string or array of strings`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (fileConfig.vars !== undefined) {
|
|
88
|
-
if (!isPlainObject(fileConfig.vars)) {
|
|
89
|
-
throw new ValidationError(`${context} file '${fileName}' vars must be an object with string values`);
|
|
90
|
-
}
|
|
91
|
-
for (const [key, value] of Object.entries(fileConfig.vars)) {
|
|
92
|
-
if (typeof value !== "string") {
|
|
93
|
-
throw new ValidationError(`${context} file '${fileName}' vars.${key} must be a string`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Validates a single label configuration.
|
|
100
|
-
*/
|
|
101
|
-
function validateLabel(label, name, context) {
|
|
102
|
-
if (!isPlainObject(label)) {
|
|
103
|
-
throw new ValidationError(`${context}: label '${name}' must be an object`);
|
|
104
|
-
}
|
|
105
|
-
const l = label;
|
|
106
|
-
if (typeof l.color !== "string" || !/^#?[0-9a-fA-F]{6}$/.test(l.color)) {
|
|
107
|
-
throw new ValidationError(`${context}: label '${name}' color must be a 6-character hex code (with or without #)`);
|
|
108
|
-
}
|
|
109
|
-
if (l.description !== undefined) {
|
|
110
|
-
if (typeof l.description !== "string") {
|
|
111
|
-
throw new ValidationError(`${context}: label '${name}' description must be a string`);
|
|
112
|
-
}
|
|
113
|
-
if (l.description.length > 100) {
|
|
114
|
-
throw new ValidationError(`${context}: label '${name}' description exceeds 100 characters (GitHub limit)`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (l.new_name !== undefined && typeof l.new_name !== "string") {
|
|
118
|
-
throw new ValidationError(`${context}: label '${name}' new_name must be a string`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function buildRootSettingsContext(config) {
|
|
122
|
-
return {
|
|
123
|
-
rulesetNames: config.settings?.rulesets
|
|
124
|
-
? Object.keys(config.settings.rulesets).filter((k) => k !== "inherit")
|
|
125
|
-
: [],
|
|
126
|
-
hasRepoSettings: config.settings?.repo !== undefined && config.settings.repo !== false,
|
|
127
|
-
hasCodeScanningSettings: config.settings?.codeScanning !== undefined &&
|
|
128
|
-
config.settings.codeScanning !== false,
|
|
129
|
-
labelNames: config.settings?.labels
|
|
130
|
-
? Object.keys(config.settings.labels).filter((k) => k !== "inherit")
|
|
131
|
-
: [],
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Validates settings object containing rulesets, labels, and repo settings.
|
|
136
|
-
*/
|
|
137
|
-
function validateSettingsRulesets(settings, context, rootCtx) {
|
|
138
|
-
if (settings.rulesets === undefined)
|
|
139
|
-
return;
|
|
140
|
-
if (!isPlainObject(settings.rulesets)) {
|
|
141
|
-
throw new ValidationError(`${context}: rulesets must be an object`);
|
|
142
|
-
}
|
|
143
|
-
const rulesets = settings.rulesets;
|
|
144
|
-
for (const [name, ruleset] of Object.entries(rulesets)) {
|
|
145
|
-
if (name === "inherit")
|
|
146
|
-
continue;
|
|
147
|
-
if (ruleset === false) {
|
|
148
|
-
if (rootCtx && !rootCtx.rulesetNames.includes(name)) {
|
|
149
|
-
throw new ValidationError(`${context}: Cannot opt out of '${name}' - not defined in root settings.rulesets`);
|
|
150
|
-
}
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
validateRuleset(ruleset, name, context);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
function validateSettingsLabels(settings, context, rootCtx) {
|
|
157
|
-
if (settings.labels === undefined)
|
|
158
|
-
return;
|
|
159
|
-
if (!isPlainObject(settings.labels)) {
|
|
160
|
-
throw new ValidationError(`${context}: labels must be an object`);
|
|
161
|
-
}
|
|
162
|
-
const labels = settings.labels;
|
|
163
|
-
for (const [name, label] of Object.entries(labels)) {
|
|
164
|
-
if (name === "inherit")
|
|
165
|
-
continue;
|
|
166
|
-
if (label === false) {
|
|
167
|
-
if (rootCtx && !rootCtx.labelNames.includes(name)) {
|
|
168
|
-
throw new ValidationError(`${context}: Cannot opt out of label '${name}' - not defined in root settings.labels`);
|
|
169
|
-
}
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
validateLabel(label, name, context);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
function validateSettingsDeleteOrphaned(settings, context) {
|
|
176
|
-
if (settings.deleteOrphaned !== undefined &&
|
|
177
|
-
typeof settings.deleteOrphaned !== "boolean") {
|
|
178
|
-
throw new ValidationError(`${context}: settings.deleteOrphaned must be a boolean`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
function validateSettingsRepo(settings, context, rootCtx) {
|
|
182
|
-
if (settings.repo === undefined)
|
|
183
|
-
return;
|
|
184
|
-
if (settings.repo === false) {
|
|
185
|
-
if (!rootCtx) {
|
|
186
|
-
throw new ValidationError(`${context}: repo: false is not valid at root level. Define repo settings or remove the field.`);
|
|
187
|
-
}
|
|
188
|
-
if (!rootCtx.hasRepoSettings) {
|
|
189
|
-
throw new ValidationError(`${context}: Cannot opt out of repo settings — not defined in root settings.repo`);
|
|
190
|
-
}
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
validateRepoSettings(settings.repo, context);
|
|
194
|
-
}
|
|
195
|
-
function validateSettingsCodeScanning(settings, context, rootCtx) {
|
|
196
|
-
if (settings.codeScanning === undefined)
|
|
197
|
-
return;
|
|
198
|
-
if (settings.codeScanning === false) {
|
|
199
|
-
if (!rootCtx) {
|
|
200
|
-
throw new ValidationError(`${context}: codeScanning: false is not valid at root level. Define codeScanning settings or remove the field.`);
|
|
201
|
-
}
|
|
202
|
-
if (!rootCtx.hasCodeScanningSettings) {
|
|
203
|
-
throw new ValidationError(`${context}: Cannot opt out of code scanning settings — not defined in root settings.codeScanning`);
|
|
204
|
-
}
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
validateCodeScanningSettings(settings.codeScanning, `${context} codeScanning`);
|
|
208
|
-
}
|
|
209
|
-
function validateSettings(settings, context, rootCtx) {
|
|
210
|
-
if (!isPlainObject(settings)) {
|
|
211
|
-
throw new ValidationError(`${context}: settings must be an object`);
|
|
212
|
-
}
|
|
213
|
-
validateSettingsRulesets(settings, context, rootCtx);
|
|
214
|
-
validateSettingsLabels(settings, context, rootCtx);
|
|
215
|
-
validateSettingsDeleteOrphaned(settings, context);
|
|
216
|
-
validateSettingsRepo(settings, context, rootCtx);
|
|
217
|
-
validateSettingsCodeScanning(settings, context, rootCtx);
|
|
218
|
-
}
|
|
219
|
-
const VALID_CODE_SCANNING_STATES = ["configured", "not-configured"];
|
|
220
|
-
const VALID_CODE_SCANNING_QUERY_SUITES = ["default", "extended"];
|
|
221
|
-
const VALID_CODE_SCANNING_LANGUAGES = [
|
|
222
|
-
"actions",
|
|
223
|
-
"c-cpp",
|
|
224
|
-
"csharp",
|
|
225
|
-
"go",
|
|
226
|
-
"java-kotlin",
|
|
227
|
-
"javascript-typescript",
|
|
228
|
-
"python",
|
|
229
|
-
"ruby",
|
|
230
|
-
"swift",
|
|
231
|
-
];
|
|
232
|
-
function validateCodeScanningSettings(settings, context) {
|
|
233
|
-
if (!isPlainObject(settings)) {
|
|
234
|
-
throw new ValidationError(`${context}: must be an object`);
|
|
235
|
-
}
|
|
236
|
-
if (settings.state === undefined) {
|
|
237
|
-
throw new ValidationError(`${context}: state is required`);
|
|
238
|
-
}
|
|
239
|
-
if (!VALID_CODE_SCANNING_STATES.includes(settings.state)) {
|
|
240
|
-
throw new ValidationError(`${context}: state must be one of: ${VALID_CODE_SCANNING_STATES.join(", ")}`);
|
|
241
|
-
}
|
|
242
|
-
if (settings.querySuite !== undefined &&
|
|
243
|
-
!VALID_CODE_SCANNING_QUERY_SUITES.includes(settings.querySuite)) {
|
|
244
|
-
throw new ValidationError(`${context}: querySuite must be one of: ${VALID_CODE_SCANNING_QUERY_SUITES.join(", ")}`);
|
|
245
|
-
}
|
|
246
|
-
if (settings.languages !== undefined) {
|
|
247
|
-
if (!Array.isArray(settings.languages)) {
|
|
248
|
-
throw new ValidationError(`${context}: languages must be an array`);
|
|
249
|
-
}
|
|
250
|
-
for (const lang of settings.languages) {
|
|
251
|
-
if (!VALID_CODE_SCANNING_LANGUAGES.includes(lang)) {
|
|
252
|
-
throw new ValidationError(`${context}: invalid language "${lang}". Valid languages: ${VALID_CODE_SCANNING_LANGUAGES.join(", ")}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
9
|
function validateConfigId(config) {
|
|
258
10
|
if (!config.id || typeof config.id !== "string") {
|
|
259
11
|
throw new ValidationError("Config requires an 'id' field. This unique identifier is used to namespace managed files in .xfg.json");
|
|
@@ -322,342 +74,6 @@ function validatePrOptions(config) {
|
|
|
322
74
|
}
|
|
323
75
|
}
|
|
324
76
|
}
|
|
325
|
-
/**
|
|
326
|
-
* Validates the extends field on a single group definition.
|
|
327
|
-
* Checks type, self-reference, and that all referenced groups exist.
|
|
328
|
-
*/
|
|
329
|
-
function validateGroupExtends(groupName, extends_, groupNames) {
|
|
330
|
-
// Type check
|
|
331
|
-
if (typeof extends_ === "string") {
|
|
332
|
-
if (extends_.length === 0) {
|
|
333
|
-
throw new ValidationError(`groups.${groupName}: 'extends' must be a non-empty string or array of strings`);
|
|
334
|
-
}
|
|
335
|
-
// Self-reference
|
|
336
|
-
if (extends_ === groupName) {
|
|
337
|
-
throw new ValidationError(`groups.${groupName}: extends cannot reference itself`);
|
|
338
|
-
}
|
|
339
|
-
// Existence
|
|
340
|
-
if (!groupNames.has(extends_)) {
|
|
341
|
-
throw new ValidationError(`groups.${groupName}: extends references undefined group '${extends_}'`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
else if (Array.isArray(extends_)) {
|
|
345
|
-
if (extends_.length === 0) {
|
|
346
|
-
throw new ValidationError(`groups.${groupName}: 'extends' must be a non-empty string or array of strings`);
|
|
347
|
-
}
|
|
348
|
-
const seen = new Set();
|
|
349
|
-
for (const entry of extends_) {
|
|
350
|
-
if (typeof entry !== "string") {
|
|
351
|
-
throw new ValidationError(`groups.${groupName}: 'extends' array entries must be strings`);
|
|
352
|
-
}
|
|
353
|
-
if (entry.length === 0) {
|
|
354
|
-
throw new ValidationError(`groups.${groupName}: 'extends' array entries must be non-empty strings`);
|
|
355
|
-
}
|
|
356
|
-
if (entry === groupName) {
|
|
357
|
-
throw new ValidationError(`groups.${groupName}: extends cannot reference itself`);
|
|
358
|
-
}
|
|
359
|
-
if (!groupNames.has(entry)) {
|
|
360
|
-
throw new ValidationError(`groups.${groupName}: extends references undefined group '${entry}'`);
|
|
361
|
-
}
|
|
362
|
-
if (seen.has(entry)) {
|
|
363
|
-
throw new ValidationError(`groups.${groupName}: duplicate '${entry}' in extends`);
|
|
364
|
-
}
|
|
365
|
-
seen.add(entry);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
throw new ValidationError(`groups.${groupName}: 'extends' must be a non-empty string or array of strings`);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Detects circular extends chains across all groups.
|
|
374
|
-
* Reuses resolveExtendsChain from extends-resolver.ts to avoid
|
|
375
|
-
* duplicating the chain-walking logic. Converts thrown errors
|
|
376
|
-
* to ValidationError.
|
|
377
|
-
*/
|
|
378
|
-
function validateNoCircularExtends(groups) {
|
|
379
|
-
for (const name of Object.keys(groups)) {
|
|
380
|
-
if (!groups[name].extends)
|
|
381
|
-
continue;
|
|
382
|
-
try {
|
|
383
|
-
resolveExtendsChain(name, groups);
|
|
384
|
-
}
|
|
385
|
-
catch (error) {
|
|
386
|
-
throw new ValidationError(error instanceof Error ? error.message : String(error));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
function validateGroups(config) {
|
|
391
|
-
if (config.groups === undefined)
|
|
392
|
-
return;
|
|
393
|
-
if (!isPlainObject(config.groups)) {
|
|
394
|
-
throw new ValidationError("groups must be an object");
|
|
395
|
-
}
|
|
396
|
-
const rootCtx = buildRootSettingsContext(config);
|
|
397
|
-
const groupNames = new Set(Object.keys(config.groups));
|
|
398
|
-
for (const [groupName, group] of Object.entries(config.groups)) {
|
|
399
|
-
if (groupName === "inherit") {
|
|
400
|
-
throw new ValidationError("'inherit' is a reserved key and cannot be used as a group name");
|
|
401
|
-
}
|
|
402
|
-
if (groupName === "extends") {
|
|
403
|
-
throw new ValidationError("'extends' is a reserved key and cannot be used as a group name");
|
|
404
|
-
}
|
|
405
|
-
// Validate extends field
|
|
406
|
-
if (group.extends !== undefined) {
|
|
407
|
-
validateGroupExtends(groupName, group.extends, groupNames);
|
|
408
|
-
}
|
|
409
|
-
if (group.files) {
|
|
410
|
-
for (const [fileName, fileConfig] of Object.entries(group.files)) {
|
|
411
|
-
if (fileName === "inherit")
|
|
412
|
-
continue;
|
|
413
|
-
if (fileConfig === false)
|
|
414
|
-
continue;
|
|
415
|
-
if (fileConfig === undefined)
|
|
416
|
-
continue;
|
|
417
|
-
validateFileConfigFields(fileConfig, fileName, `groups.${groupName}:`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
if (group.settings !== undefined) {
|
|
421
|
-
validateSettings(group.settings, `groups.${groupName}`, rootCtx);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
// Validate no circular extends after individual validation
|
|
425
|
-
validateNoCircularExtends(config.groups);
|
|
426
|
-
}
|
|
427
|
-
function validateGroupRefArray(arr, fieldName, ctx, groupNames) {
|
|
428
|
-
if (!Array.isArray(arr) || arr.length === 0) {
|
|
429
|
-
throw new ValidationError(`${ctx}: '${fieldName}' must be a non-empty array of strings`);
|
|
430
|
-
}
|
|
431
|
-
const seen = new Set();
|
|
432
|
-
for (const name of arr) {
|
|
433
|
-
if (typeof name !== "string") {
|
|
434
|
-
throw new ValidationError(`${ctx}: '${fieldName}' entries must be strings`);
|
|
435
|
-
}
|
|
436
|
-
if (!groupNames.includes(name)) {
|
|
437
|
-
throw new ValidationError(`${ctx}: group '${name}' in ${fieldName} is not defined in root 'groups'`);
|
|
438
|
-
}
|
|
439
|
-
if (seen.has(name)) {
|
|
440
|
-
throw new ValidationError(`${ctx}: duplicate group '${name}' in ${fieldName}`);
|
|
441
|
-
}
|
|
442
|
-
seen.add(name);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
function validateConditionalGroups(config) {
|
|
446
|
-
if (config.conditionalGroups === undefined)
|
|
447
|
-
return;
|
|
448
|
-
if (!Array.isArray(config.conditionalGroups)) {
|
|
449
|
-
throw new ValidationError("conditionalGroups must be an array");
|
|
450
|
-
}
|
|
451
|
-
const rootCtx = buildRootSettingsContext(config);
|
|
452
|
-
const groupNames = config.groups ? Object.keys(config.groups) : [];
|
|
453
|
-
for (let i = 0; i < config.conditionalGroups.length; i++) {
|
|
454
|
-
const entry = config.conditionalGroups[i];
|
|
455
|
-
const ctx = `conditionalGroups[${i}]`;
|
|
456
|
-
// Validate 'when' clause
|
|
457
|
-
if (!entry.when || !isPlainObject(entry.when)) {
|
|
458
|
-
throw new ValidationError(`${ctx}: 'when' is required and must be an object`);
|
|
459
|
-
}
|
|
460
|
-
const { allOf, anyOf, noneOf } = entry.when;
|
|
461
|
-
if (!allOf && !anyOf && !noneOf) {
|
|
462
|
-
throw new ValidationError(`${ctx}: 'when' must have at least one of 'allOf', 'anyOf', or 'noneOf'`);
|
|
463
|
-
}
|
|
464
|
-
if (allOf !== undefined) {
|
|
465
|
-
validateGroupRefArray(allOf, "allOf", ctx, groupNames);
|
|
466
|
-
}
|
|
467
|
-
if (anyOf !== undefined) {
|
|
468
|
-
validateGroupRefArray(anyOf, "anyOf", ctx, groupNames);
|
|
469
|
-
}
|
|
470
|
-
if (noneOf !== undefined) {
|
|
471
|
-
validateGroupRefArray(noneOf, "noneOf", ctx, groupNames);
|
|
472
|
-
}
|
|
473
|
-
// Cross-operator overlap: noneOf must not share groups with allOf or anyOf
|
|
474
|
-
if (noneOf) {
|
|
475
|
-
const noneOfSet = new Set(noneOf);
|
|
476
|
-
if (allOf) {
|
|
477
|
-
for (const g of allOf) {
|
|
478
|
-
if (noneOfSet.has(g)) {
|
|
479
|
-
throw new ValidationError(`${ctx}: noneOf group '${g}' overlaps with allOf (contradictory condition)`);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
if (anyOf) {
|
|
484
|
-
for (const g of anyOf) {
|
|
485
|
-
if (noneOfSet.has(g)) {
|
|
486
|
-
throw new ValidationError(`${ctx}: noneOf group '${g}' overlaps with anyOf (contradictory condition)`);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
// Validate files
|
|
492
|
-
if (entry.files) {
|
|
493
|
-
for (const [fileName, fileConfig] of Object.entries(entry.files)) {
|
|
494
|
-
if (fileName === "inherit")
|
|
495
|
-
continue;
|
|
496
|
-
if (fileConfig === false)
|
|
497
|
-
continue;
|
|
498
|
-
if (fileConfig === undefined)
|
|
499
|
-
continue;
|
|
500
|
-
validateFileConfigFields(fileConfig, fileName, `${ctx}:`);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
// Validate settings
|
|
504
|
-
if (entry.settings !== undefined) {
|
|
505
|
-
validateSettings(entry.settings, ctx, rootCtx);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
function validateRepoGitField(repo, index) {
|
|
510
|
-
if (!repo.git) {
|
|
511
|
-
throw new ValidationError(`Repo at index ${index} missing required field: git`);
|
|
512
|
-
}
|
|
513
|
-
if (Array.isArray(repo.git) && repo.git.length === 0) {
|
|
514
|
-
throw new ValidationError(`Repo at index ${index} has empty git array`);
|
|
515
|
-
}
|
|
516
|
-
return getGitDisplayName(repo.git);
|
|
517
|
-
}
|
|
518
|
-
function validateRepoOrigins(config, repo, repoLabel) {
|
|
519
|
-
if (repo.upstream !== undefined && repo.source !== undefined) {
|
|
520
|
-
throw new ValidationError(`Repo ${repoLabel}: 'upstream' and 'source' are mutually exclusive. ` +
|
|
521
|
-
`Use 'upstream' to fork, or 'source' to migrate, not both.`);
|
|
522
|
-
}
|
|
523
|
-
if (repo.upstream !== undefined) {
|
|
524
|
-
if (typeof repo.upstream !== "string") {
|
|
525
|
-
throw new ValidationError(`Repo ${repoLabel}: 'upstream' must be a string`);
|
|
526
|
-
}
|
|
527
|
-
if (!isValidGitUrl(repo.upstream)) {
|
|
528
|
-
throw new ValidationError(`Repo ${repoLabel}: 'upstream' must be a valid git URL ` +
|
|
529
|
-
`(SSH: git@host:path or HTTPS: https://host/path)`);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
if (repo.source !== undefined) {
|
|
533
|
-
if (typeof repo.source !== "string") {
|
|
534
|
-
throw new ValidationError(`Repo ${repoLabel}: 'source' must be a string`);
|
|
535
|
-
}
|
|
536
|
-
if (!isValidGitUrl(repo.source)) {
|
|
537
|
-
throw new ValidationError(`Repo ${repoLabel}: 'source' must be a valid git URL ` +
|
|
538
|
-
`(SSH: git@host:path or HTTPS: https://host/path)`);
|
|
539
|
-
}
|
|
540
|
-
if (isGitHubUrl(repo.source, config.githubHosts)) {
|
|
541
|
-
throw new ValidationError(`Repo ${repoLabel}: 'source' cannot be a GitHub URL. ` +
|
|
542
|
-
`Migration from GitHub is not supported. Currently supported sources: Azure DevOps`);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
function validateRepoGroups(config, repo, index) {
|
|
547
|
-
if (repo.groups === undefined)
|
|
548
|
-
return;
|
|
549
|
-
if (!Array.isArray(repo.groups) ||
|
|
550
|
-
!repo.groups.every((g) => typeof g === "string")) {
|
|
551
|
-
throw new ValidationError(`Repo at index ${index}: groups must be an array of strings`);
|
|
552
|
-
}
|
|
553
|
-
const seen = new Set();
|
|
554
|
-
for (const groupName of repo.groups) {
|
|
555
|
-
if (!config.groups || !config.groups[groupName]) {
|
|
556
|
-
throw new ValidationError(`Repo at index ${index}: group '${groupName}' is not defined in root 'groups'`);
|
|
557
|
-
}
|
|
558
|
-
if (seen.has(groupName)) {
|
|
559
|
-
throw new ValidationError(`Repo at index ${index}: duplicate group '${groupName}'`);
|
|
560
|
-
}
|
|
561
|
-
seen.add(groupName);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
function validateRepoFiles(config, repo, index, repoLabel) {
|
|
565
|
-
if (!repo.files)
|
|
566
|
-
return;
|
|
567
|
-
if (!isPlainObject(repo.files)) {
|
|
568
|
-
throw new ValidationError(`Repo at index ${index}: files must be an object`);
|
|
569
|
-
}
|
|
570
|
-
const knownFiles = new Set(config.files ? Object.keys(config.files) : []);
|
|
571
|
-
if (repo.groups && config.groups) {
|
|
572
|
-
const expandedGroups = expandRepoGroups(repo.groups, config.groups);
|
|
573
|
-
for (const groupName of expandedGroups) {
|
|
574
|
-
const group = config.groups[groupName];
|
|
575
|
-
if (group?.files) {
|
|
576
|
-
for (const fileName of Object.keys(group.files)) {
|
|
577
|
-
if (fileName !== "inherit")
|
|
578
|
-
knownFiles.add(fileName);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
if (config.conditionalGroups) {
|
|
584
|
-
for (const cg of config.conditionalGroups) {
|
|
585
|
-
if (cg.files) {
|
|
586
|
-
for (const fileName of Object.keys(cg.files)) {
|
|
587
|
-
if (fileName !== "inherit")
|
|
588
|
-
knownFiles.add(fileName);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
for (const fileName of Object.keys(repo.files)) {
|
|
594
|
-
if (fileName === "inherit") {
|
|
595
|
-
const inheritValue = repo.files.inherit;
|
|
596
|
-
if (typeof inheritValue !== "boolean") {
|
|
597
|
-
throw new ValidationError(`Repo at index ${index}: files.inherit must be a boolean`);
|
|
598
|
-
}
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
if (!knownFiles.has(fileName)) {
|
|
602
|
-
throw new ValidationError(`Repo at index ${index} references undefined file '${fileName}'. File must be defined in root 'files' object or in a referenced group.`);
|
|
603
|
-
}
|
|
604
|
-
const fileOverride = repo.files[fileName];
|
|
605
|
-
if (fileOverride === false) {
|
|
606
|
-
continue;
|
|
607
|
-
}
|
|
608
|
-
if (fileOverride.override && !fileOverride.content) {
|
|
609
|
-
throw new ValidationError(`Repo ${repoLabel} has override: true for file '${fileName}' but no content defined. ` +
|
|
610
|
-
`Use content: "" for an empty text file override, or content: {} for an empty JSON/YAML override.`);
|
|
611
|
-
}
|
|
612
|
-
validateFileConfigFields(fileOverride, fileName, `Repo ${repoLabel}:`);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
function enrichSettingsContext(rootCtx, settings) {
|
|
616
|
-
if (!settings)
|
|
617
|
-
return;
|
|
618
|
-
if (settings.rulesets) {
|
|
619
|
-
for (const name of Object.keys(settings.rulesets)) {
|
|
620
|
-
if (name !== "inherit")
|
|
621
|
-
rootCtx.rulesetNames.push(name);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
if (settings.labels) {
|
|
625
|
-
for (const name of Object.keys(settings.labels)) {
|
|
626
|
-
if (name !== "inherit")
|
|
627
|
-
rootCtx.labelNames.push(name);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
if (settings.repo !== undefined && settings.repo !== false) {
|
|
631
|
-
rootCtx.hasRepoSettings = true;
|
|
632
|
-
}
|
|
633
|
-
if (settings.codeScanning !== undefined && settings.codeScanning !== false) {
|
|
634
|
-
rootCtx.hasCodeScanningSettings = true;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
function validateRepoSettingsEntry(config, repo, repoLabel) {
|
|
638
|
-
if (repo.settings === undefined)
|
|
639
|
-
return;
|
|
640
|
-
const rootCtx = buildRootSettingsContext(config);
|
|
641
|
-
if (repo.groups && config.groups) {
|
|
642
|
-
const expandedGroups = expandRepoGroups(repo.groups, config.groups);
|
|
643
|
-
for (const groupName of expandedGroups) {
|
|
644
|
-
enrichSettingsContext(rootCtx, config.groups[groupName]?.settings);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
if (config.conditionalGroups) {
|
|
648
|
-
for (const cg of config.conditionalGroups) {
|
|
649
|
-
enrichSettingsContext(rootCtx, cg.settings);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
validateSettings(repo.settings, `Repo ${repoLabel}`, rootCtx);
|
|
653
|
-
}
|
|
654
|
-
function validateRepoEntry(config, repo, index) {
|
|
655
|
-
const repoLabel = validateRepoGitField(repo, index);
|
|
656
|
-
validateRepoOrigins(config, repo, repoLabel);
|
|
657
|
-
validateRepoGroups(config, repo, index);
|
|
658
|
-
validateRepoFiles(config, repo, index, repoLabel);
|
|
659
|
-
validateRepoSettingsEntry(config, repo, repoLabel);
|
|
660
|
-
}
|
|
661
77
|
function hasGroupFiles(config) {
|
|
662
78
|
return (isPlainObject(config.groups) &&
|
|
663
79
|
Object.values(config.groups).some((g) => g.files &&
|
|
@@ -668,9 +84,13 @@ function hasConditionalGroupFiles(config) {
|
|
|
668
84
|
config.conditionalGroups.some((cg) => cg.files &&
|
|
669
85
|
Object.keys(cg.files).filter((k) => k !== "inherit" && cg.files[k] !== false).length > 0));
|
|
670
86
|
}
|
|
671
|
-
function
|
|
87
|
+
function hasConditionalGroupSettingsPresent(config) {
|
|
88
|
+
return (Array.isArray(config.conditionalGroups) &&
|
|
89
|
+
config.conditionalGroups.some((cg) => cg.settings && isPlainObject(cg.settings)));
|
|
90
|
+
}
|
|
91
|
+
function hasConditionalGroupSettingsActionable(config) {
|
|
672
92
|
return (Array.isArray(config.conditionalGroups) &&
|
|
673
|
-
config.conditionalGroups.some((cg) => cg.settings &&
|
|
93
|
+
config.conditionalGroups.some((cg) => cg.settings && hasActionableSettings(cg.settings)));
|
|
674
94
|
}
|
|
675
95
|
function hasConditionalGroupPR(config) {
|
|
676
96
|
return (Array.isArray(config.conditionalGroups) &&
|
|
@@ -688,7 +108,7 @@ export function validateRawConfig(config) {
|
|
|
688
108
|
const hasGrpSettings = isPlainObject(config.groups) &&
|
|
689
109
|
Object.values(config.groups).some((g) => g.settings && isPlainObject(g.settings));
|
|
690
110
|
const hasCondGrpFiles = hasConditionalGroupFiles(config);
|
|
691
|
-
const hasCondGrpSettings =
|
|
111
|
+
const hasCondGrpSettings = hasConditionalGroupSettingsPresent(config);
|
|
692
112
|
const hasCondGrpPR = hasConditionalGroupPR(config);
|
|
693
113
|
if (!hasFiles &&
|
|
694
114
|
!hasSettings &&
|
|
@@ -717,9 +137,6 @@ export function validateRawConfig(config) {
|
|
|
717
137
|
validateRepoEntry(config, config.repos[i], i);
|
|
718
138
|
}
|
|
719
139
|
}
|
|
720
|
-
// =============================================================================
|
|
721
|
-
// Command-Specific Validators
|
|
722
|
-
// =============================================================================
|
|
723
140
|
/**
|
|
724
141
|
* Validates that config is suitable for the sync command.
|
|
725
142
|
* @throws ValidationError if neither files nor settings are present
|
|
@@ -732,7 +149,7 @@ export function validateForSync(config) {
|
|
|
732
149
|
const hasGroupSettings = isPlainObject(config.groups) &&
|
|
733
150
|
Object.values(config.groups).some((g) => g.settings && hasActionableSettings(g.settings));
|
|
734
151
|
const hasCondGrpFiles = hasConditionalGroupFiles(config);
|
|
735
|
-
const hasCondGrpSettings =
|
|
152
|
+
const hasCondGrpSettings = hasConditionalGroupSettingsActionable(config);
|
|
736
153
|
const hasCondGrpPR = hasConditionalGroupPR(config);
|
|
737
154
|
if (!hasRootFiles &&
|
|
738
155
|
!hasGrpFiles &&
|
|
@@ -746,9 +163,6 @@ export function validateForSync(config) {
|
|
|
746
163
|
"Use 'files' to sync configuration files, or 'settings' to manage repository settings.");
|
|
747
164
|
}
|
|
748
165
|
}
|
|
749
|
-
/**
|
|
750
|
-
* Checks if settings contain actionable configuration.
|
|
751
|
-
*/
|
|
752
166
|
export function hasActionableSettings(settings) {
|
|
753
167
|
if (!settings)
|
|
754
168
|
return false;
|
|
@@ -763,7 +177,7 @@ export function hasActionableSettings(settings) {
|
|
|
763
177
|
Object.keys(settings.labels).filter((k) => k !== "inherit").length > 0) {
|
|
764
178
|
return true;
|
|
765
179
|
}
|
|
766
|
-
if (settings.codeScanning
|
|
180
|
+
if (settings.codeScanning) {
|
|
767
181
|
return true;
|
|
768
182
|
}
|
|
769
183
|
return false;
|
|
@@ -2,7 +2,8 @@ import { isTextContent } from "../merge.js";
|
|
|
2
2
|
import { isPlainObject } from "../../shared/type-guards.js";
|
|
3
3
|
export { isTextContent };
|
|
4
4
|
export { isPlainObject as isObjectContent };
|
|
5
|
-
declare
|
|
5
|
+
export declare function validValues<T extends string>(values: readonly T[]): readonly string[];
|
|
6
|
+
declare const VALID_STRATEGIES: readonly string[];
|
|
6
7
|
/**
|
|
7
8
|
* Check if file extension is for structured output (JSON/YAML).
|
|
8
9
|
*/
|