codex-plugin-doctor 1.72.1 → 1.74.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 +7 -3
- package/dist/core/init-ci.d.ts +6 -0
- package/dist/core/init-ci.js +74 -0
- package/dist/core/mcp-tool-impact.js +230 -13
- package/dist/run-cli.js +56 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,7 +149,11 @@ live list with `tools-capture-url` (and `tools-capture-token-env`) or, for a loc
|
|
|
149
149
|
stdio server with `runtime: "true"`, `tools-capture-server`, and skip the
|
|
150
150
|
plugin package check with `check: "false"` in repositories that only ship an MCP
|
|
151
151
|
server. With `tools-diff-comment: "true"` it also posts the comparison as one
|
|
152
|
-
pull request comment that later runs update.
|
|
152
|
+
pull request comment that later runs update. `tools-capture-sandbox: docker`
|
|
153
|
+
starts a stdio server in the Docker sandbox, and
|
|
154
|
+
`codex-plugin-doctor init-ci . --tools-diff-server <name> --sandbox docker`
|
|
155
|
+
writes a pull request workflow that captures in one job and compares and
|
|
156
|
+
comments in another.
|
|
153
157
|
|
|
154
158
|
Local stdio servers from a package's MCP configuration can be cataloged too.
|
|
155
159
|
`--runtime` is required because this starts package code; the server is stopped
|
|
@@ -585,9 +589,9 @@ jobs:
|
|
|
585
589
|
runs-on: ubuntu-latest
|
|
586
590
|
steps:
|
|
587
591
|
- uses: actions/checkout@v5
|
|
588
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
592
|
+
- uses: Esquetta/CodexPluginDoctor@v1.74.0
|
|
589
593
|
with:
|
|
590
|
-
version: "1.
|
|
594
|
+
version: "1.74.0"
|
|
591
595
|
path: .
|
|
592
596
|
runtime: "true"
|
|
593
597
|
policy: codex-publish
|
package/dist/core/init-ci.d.ts
CHANGED
|
@@ -2,4 +2,10 @@ export interface InitCiResult {
|
|
|
2
2
|
rootPath: string;
|
|
3
3
|
workflowPath: string;
|
|
4
4
|
}
|
|
5
|
+
export interface ToolsDiffWorkflowOptions {
|
|
6
|
+
server: string;
|
|
7
|
+
baseline: string;
|
|
8
|
+
sandbox: "docker" | null;
|
|
9
|
+
}
|
|
10
|
+
export declare function initToolsDiffWorkflow(targetPath: string, options: ToolsDiffWorkflowOptions): Promise<InitCiResult>;
|
|
5
11
|
export declare function initCiWorkflow(targetPath: string): Promise<InitCiResult>;
|
package/dist/core/init-ci.js
CHANGED
|
@@ -31,6 +31,80 @@ function buildWorkflow() {
|
|
|
31
31
|
""
|
|
32
32
|
].join("\n");
|
|
33
33
|
}
|
|
34
|
+
// JSON strings are valid YAML double-quoted scalars, so names and paths cannot break the document.
|
|
35
|
+
const yamlString = (value) => JSON.stringify(value);
|
|
36
|
+
/**
|
|
37
|
+
* A pull request workflow in two jobs: the first starts the stdio server and uploads its tool list,
|
|
38
|
+
* and the second compares it with the committed baseline and comments. Only the second job may write
|
|
39
|
+
* to pull requests, and it runs no package code.
|
|
40
|
+
*/
|
|
41
|
+
function buildToolsDiffWorkflow(options) {
|
|
42
|
+
return [
|
|
43
|
+
"name: MCP tool diff",
|
|
44
|
+
"",
|
|
45
|
+
"on:",
|
|
46
|
+
" pull_request:",
|
|
47
|
+
"",
|
|
48
|
+
"permissions:",
|
|
49
|
+
" contents: read",
|
|
50
|
+
"",
|
|
51
|
+
"jobs:",
|
|
52
|
+
" capture:",
|
|
53
|
+
" runs-on: ubuntu-latest",
|
|
54
|
+
" steps:",
|
|
55
|
+
" - uses: actions/checkout@v5",
|
|
56
|
+
" with:",
|
|
57
|
+
" persist-credentials: false",
|
|
58
|
+
" # Add install or build steps here if the server needs them; the Docker sandbox has no network.",
|
|
59
|
+
` - uses: Esquetta/CodexPluginDoctor@v${packageVersion}`,
|
|
60
|
+
" with:",
|
|
61
|
+
` version: \"${packageVersion}\"`,
|
|
62
|
+
" path: .",
|
|
63
|
+
" check: \"false\"",
|
|
64
|
+
" runtime: \"true\"",
|
|
65
|
+
` tools-capture-server: ${yamlString(options.server)}`,
|
|
66
|
+
...(options.sandbox === "docker" ? [" tools-capture-sandbox: docker"] : []),
|
|
67
|
+
" # Catalog findings do not fail this job; the compare job re-checks the saved list.",
|
|
68
|
+
" tools-capture-defer-findings: \"true\"",
|
|
69
|
+
" artifact-name: mcp-tools-capture",
|
|
70
|
+
"",
|
|
71
|
+
" compare:",
|
|
72
|
+
" needs: capture",
|
|
73
|
+
" runs-on: ubuntu-latest",
|
|
74
|
+
" permissions:",
|
|
75
|
+
" contents: read",
|
|
76
|
+
" pull-requests: write",
|
|
77
|
+
" steps:",
|
|
78
|
+
" - uses: actions/checkout@v5",
|
|
79
|
+
" with:",
|
|
80
|
+
" persist-credentials: false",
|
|
81
|
+
" # Outside the checkout, so files from the pull request cannot shadow the downloaded list.",
|
|
82
|
+
" - uses: actions/download-artifact@v5",
|
|
83
|
+
" with:",
|
|
84
|
+
" name: mcp-tools-capture",
|
|
85
|
+
" path: ${{ runner.temp }}/mcp-tools-capture",
|
|
86
|
+
` - uses: Esquetta/CodexPluginDoctor@v${packageVersion}`,
|
|
87
|
+
" with:",
|
|
88
|
+
` version: \"${packageVersion}\"`,
|
|
89
|
+
" check: \"false\"",
|
|
90
|
+
` tools-diff-before: ${yamlString(options.baseline)}`,
|
|
91
|
+
" tools-diff-after: ${{ runner.temp }}/mcp-tools-capture/mcp-tools-current.json",
|
|
92
|
+
" tools-diff-fail-on: breaking",
|
|
93
|
+
" tools-diff-comment: \"true\"",
|
|
94
|
+
" output-dir: ${{ runner.temp }}/codex-plugin-doctor-reports",
|
|
95
|
+
" artifact-name: mcp-tools-diff",
|
|
96
|
+
""
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
export async function initToolsDiffWorkflow(targetPath, options) {
|
|
100
|
+
const rootPath = path.resolve(targetPath);
|
|
101
|
+
const workflowPath = path.join(rootPath, ".github", "workflows", "codex-plugin-doctor-tools.yml");
|
|
102
|
+
await mkdir(path.dirname(workflowPath), { recursive: true });
|
|
103
|
+
await writeFile(workflowPath, buildToolsDiffWorkflow(options), "utf8");
|
|
104
|
+
// doctor tools --save-response writes into an existing directory, so the baseline can be captured next.
|
|
105
|
+
await mkdir(path.dirname(path.resolve(rootPath, options.baseline)), { recursive: true });
|
|
106
|
+
return { rootPath, workflowPath };
|
|
107
|
+
}
|
|
34
108
|
export async function initCiWorkflow(targetPath) {
|
|
35
109
|
const rootPath = path.resolve(targetPath);
|
|
36
110
|
const workflowPath = path.join(rootPath, ".github", "workflows", "codex-plugin-doctor.yml");
|
|
@@ -27,13 +27,15 @@ const UNCLASSIFIED_REASONS = new Set([
|
|
|
27
27
|
// Keywords that document a schema without constraining accepted or produced values.
|
|
28
28
|
const DOCUMENTATION_KEYWORDS = new Set(["title", "description", "examples", "default", "deprecated", "$comment"]);
|
|
29
29
|
const ROOT_STRUCTURAL_KEYWORDS = new Set(["type", "properties", "required", "additionalProperties", "$schema"]);
|
|
30
|
-
const LOWER_BOUND_KEYWORDS = ["
|
|
31
|
-
const UPPER_BOUND_KEYWORDS = ["
|
|
30
|
+
const LOWER_BOUND_KEYWORDS = ["minLength", "minItems"];
|
|
31
|
+
const UPPER_BOUND_KEYWORDS = ["maxLength", "maxItems"];
|
|
32
|
+
// Numeric bounds, compared as one effective bound per side because the exclusive form may replace the inclusive one.
|
|
33
|
+
const NUMERIC_BOUND_KEYWORDS = ["minimum", "exclusiveMinimum", "maximum", "exclusiveMaximum"];
|
|
32
34
|
// String keywords that can only be introduced or removed with a known effect; a changed value is unclassified.
|
|
33
35
|
const STRING_CONSTRAINT_KEYWORDS = ["pattern", "format"];
|
|
34
36
|
const NESTED_STRUCTURAL_KEYWORDS = new Set([
|
|
35
|
-
"type", "enum", "const", "properties", "required", "additionalProperties", "items",
|
|
36
|
-
...LOWER_BOUND_KEYWORDS, ...UPPER_BOUND_KEYWORDS, ...STRING_CONSTRAINT_KEYWORDS
|
|
37
|
+
"type", "enum", "const", "properties", "required", "additionalProperties", "items", "multipleOf", "uniqueItems",
|
|
38
|
+
...LOWER_BOUND_KEYWORDS, ...UPPER_BOUND_KEYWORDS, ...NUMERIC_BOUND_KEYWORDS, ...STRING_CONSTRAINT_KEYWORDS
|
|
37
39
|
]);
|
|
38
40
|
// When every reference is a same-document fragment, definition containers are compared only
|
|
39
41
|
// where a resolved `$ref` reaches into them; otherwise they are compared as written.
|
|
@@ -45,11 +47,16 @@ const NESTED_STRUCTURAL_WITH_DEFINITIONS = new Set([...NESTED_STRUCTURAL_KEYWORD
|
|
|
45
47
|
const DATA_KEYWORDS = new Set(["enum", "const", "examples", "example", "default"]);
|
|
46
48
|
// Keywords whose values map names to subschemas; their keys are names, never keywords.
|
|
47
49
|
const NAME_MAP_KEYWORDS = new Set(["properties", "patternProperties", "$defs", "definitions", "dependentSchemas", "dependencies"]);
|
|
48
|
-
// Subschemas nested deeper than this below the root are not compared and stay unclassified
|
|
50
|
+
// Subschemas nested deeper than this below the root are not compared and stay unclassified; composition
|
|
51
|
+
// wrappers keep the depth of the schema they wrap and are bounded by MAX_WRAPPER_HOPS instead.
|
|
49
52
|
const MAX_NESTED_DEPTH = 8;
|
|
50
53
|
// Keywords that only hold or name subschemas and never validate the object's own keys, so a
|
|
51
54
|
// new property beside them stays compatible. Any other unclassified keyword may govern keys.
|
|
52
55
|
const DEFINITION_KEYWORDS = new Set(["$defs", "definitions", "$id", "$anchor", "$dynamicAnchor"]);
|
|
56
|
+
// `anyOf` alternatives beyond this many on either side are not paired.
|
|
57
|
+
const MAX_ALTERNATIVES = 32;
|
|
58
|
+
// Composition wrappers (`allOf` and `anyOf`) followed along one path; deeper wrappers are unclassified.
|
|
59
|
+
const MAX_WRAPPER_HOPS = 16;
|
|
53
60
|
// A `$ref` chain longer than this, such as an alias cycle, is not resolved.
|
|
54
61
|
const MAX_REFERENCE_HOPS = 8;
|
|
55
62
|
// Schema comparisons allowed per tool field; a schema that needs more is unclassified.
|
|
@@ -168,6 +175,176 @@ function compareBounds(inner, outer, codes, reasons) {
|
|
|
168
175
|
}
|
|
169
176
|
}
|
|
170
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* The tighter of a schema's inclusive and exclusive bound on one side, undefined when it has neither,
|
|
180
|
+
* or null when either is not a finite number (such as a draft-04 boolean `exclusiveMinimum`).
|
|
181
|
+
*/
|
|
182
|
+
function numericBound(schema, inclusive, exclusive, lower) {
|
|
183
|
+
if (typeof schema[exclusive] === "boolean") {
|
|
184
|
+
// Draft-04 marks the inclusive keyword itself as exclusive; a boolean alone is invalid in every draft.
|
|
185
|
+
const value = schema[inclusive];
|
|
186
|
+
return isFiniteNumber(value) ? { value, exclusive: schema[exclusive] } : null;
|
|
187
|
+
}
|
|
188
|
+
const candidates = [];
|
|
189
|
+
for (const [keyword, isExclusive] of [[inclusive, false], [exclusive, true]]) {
|
|
190
|
+
const value = schema[keyword];
|
|
191
|
+
if (value === undefined)
|
|
192
|
+
continue;
|
|
193
|
+
if (!isFiniteNumber(value))
|
|
194
|
+
return null;
|
|
195
|
+
candidates.push({ value, exclusive: isExclusive });
|
|
196
|
+
}
|
|
197
|
+
return candidates.reduce((tightest, candidate) => {
|
|
198
|
+
if (tightest === undefined)
|
|
199
|
+
return candidate;
|
|
200
|
+
if (candidate.value === tightest.value)
|
|
201
|
+
return candidate.exclusive ? candidate : tightest;
|
|
202
|
+
return (lower ? candidate.value > tightest.value : candidate.value < tightest.value) ? candidate : tightest;
|
|
203
|
+
}, undefined);
|
|
204
|
+
}
|
|
205
|
+
/** Flags numeric bounds, `multipleOf`, and `uniqueItems` of `outer` that `inner` does not satisfy. */
|
|
206
|
+
function compareNumericAndArrayConstraints(inner, outer, codes, reasons) {
|
|
207
|
+
for (const [inclusive, exclusive, lower] of [["minimum", "exclusiveMinimum", true], ["maximum", "exclusiveMaximum", false]]) {
|
|
208
|
+
const innerBound = numericBound(inner, inclusive, exclusive, lower);
|
|
209
|
+
const outerBound = numericBound(outer, inclusive, exclusive, lower);
|
|
210
|
+
if (innerBound === null || outerBound === null) {
|
|
211
|
+
// A bound that cannot be read still means the same when it did not change.
|
|
212
|
+
const unchanged = Object.is(inner[inclusive], outer[inclusive]) && Object.is(inner[exclusive], outer[exclusive]);
|
|
213
|
+
if (!unchanged)
|
|
214
|
+
reasons.add(codes.unclassified);
|
|
215
|
+
}
|
|
216
|
+
else if (outerBound !== undefined) {
|
|
217
|
+
const covered = innerBound !== undefined && (innerBound.value === outerBound.value
|
|
218
|
+
? innerBound.exclusive || !outerBound.exclusive
|
|
219
|
+
: lower ? innerBound.value > outerBound.value : innerBound.value < outerBound.value);
|
|
220
|
+
if (!covered)
|
|
221
|
+
reasons.add(codes.constraint);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const outerMultiple = outer.multipleOf;
|
|
225
|
+
const innerMultiple = inner.multipleOf;
|
|
226
|
+
const validMultiple = (value) => value === undefined || (isFiniteNumber(value) && value > 0);
|
|
227
|
+
if (!validMultiple(outerMultiple) || !validMultiple(innerMultiple)) {
|
|
228
|
+
if (!Object.is(innerMultiple, outerMultiple))
|
|
229
|
+
reasons.add(codes.unclassified);
|
|
230
|
+
}
|
|
231
|
+
else if (outerMultiple !== undefined) {
|
|
232
|
+
if (innerMultiple === undefined) {
|
|
233
|
+
reasons.add(codes.constraint);
|
|
234
|
+
}
|
|
235
|
+
else if (Number.isInteger(innerMultiple) && Number.isInteger(outerMultiple)) {
|
|
236
|
+
// Every multiple of innerMultiple is a multiple of outerMultiple when outerMultiple divides it.
|
|
237
|
+
if (innerMultiple % outerMultiple !== 0)
|
|
238
|
+
reasons.add(codes.constraint);
|
|
239
|
+
}
|
|
240
|
+
else if (innerMultiple !== outerMultiple) {
|
|
241
|
+
// Validators test fractional multiples with floating-point division, which can reject 0.3
|
|
242
|
+
// under multipleOf 0.1, so a changed fractional multipleOf cannot be judged.
|
|
243
|
+
reasons.add(codes.unclassified);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if ((outer.uniqueItems !== undefined && typeof outer.uniqueItems !== "boolean")
|
|
247
|
+
|| (inner.uniqueItems !== undefined && typeof inner.uniqueItems !== "boolean")) {
|
|
248
|
+
if (!Object.is(inner.uniqueItems, outer.uniqueItems))
|
|
249
|
+
reasons.add(codes.unclassified);
|
|
250
|
+
}
|
|
251
|
+
else if (outer.uniqueItems === true && inner.uniqueItems !== true) {
|
|
252
|
+
reasons.add(codes.constraint);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** The subschema of a schema that is only a one-element `allOf` with documentation, or null. */
|
|
256
|
+
function singleAllOf(schema) {
|
|
257
|
+
if (!isJsonObject(schema) || !Array.isArray(schema.allOf) || schema.allOf.length !== 1)
|
|
258
|
+
return null;
|
|
259
|
+
return Object.keys(schema).every((key) => key === "allOf" || DOCUMENTATION_KEYWORDS.has(key)) ? { inner: schema.allOf[0] } : null;
|
|
260
|
+
}
|
|
261
|
+
/** The alternatives of a schema that is only `anyOf` with documentation, or null. */
|
|
262
|
+
function alternatives(schema) {
|
|
263
|
+
if (!isJsonObject(schema) || !Array.isArray(schema.anyOf))
|
|
264
|
+
return null;
|
|
265
|
+
return Object.keys(schema).every((key) => key === "anyOf" || DOCUMENTATION_KEYWORDS.has(key)) ? schema.anyOf : null;
|
|
266
|
+
}
|
|
267
|
+
function isComposition(schema) {
|
|
268
|
+
return singleAllOf(schema) !== null || alternatives(schema) !== null;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Compares two schemas when at least one is a one-element `allOf` or an `anyOf` with only
|
|
272
|
+
* documentation beside it. Wrappers keep the depth, so their own count bounds how far one path unwraps.
|
|
273
|
+
*/
|
|
274
|
+
function compareComposition(before, after, direction, depth, equal, reasons, scope) {
|
|
275
|
+
if (scope.wrappers >= MAX_WRAPPER_HOPS) {
|
|
276
|
+
reasons.add(CODES[direction].unclassified);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const wrappedBefore = singleAllOf(before);
|
|
280
|
+
const wrappedAfter = singleAllOf(after);
|
|
281
|
+
scope.wrappers += 1;
|
|
282
|
+
if (wrappedBefore !== null || wrappedAfter !== null) {
|
|
283
|
+
// A one-element allOf with only documentation beside it means the same as its subschema.
|
|
284
|
+
compareSchema(wrappedBefore === null ? before : wrappedBefore.inner, wrappedAfter === null ? after : wrappedAfter.inner, direction, depth, equal, reasons, scope);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// A schema that is not an anyOf compares as an anyOf with that schema as its only alternative.
|
|
288
|
+
compareAlternatives(alternatives(before) ?? [before], alternatives(after) ?? [after], direction, depth, equal, reasons, scope);
|
|
289
|
+
}
|
|
290
|
+
scope.wrappers -= 1;
|
|
291
|
+
}
|
|
292
|
+
/** The reference target or declared type that pairs an alternative with its counterpart. */
|
|
293
|
+
function alternativeKey(alternative) {
|
|
294
|
+
if (!isJsonObject(alternative))
|
|
295
|
+
return null;
|
|
296
|
+
if (typeof alternative.$ref === "string")
|
|
297
|
+
return `ref:${alternative.$ref}`;
|
|
298
|
+
return alternative.type === undefined ? null : `type:${JSON.stringify(alternative.type)}`;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Compares `anyOf` alternatives. A value is accepted when any alternative accepts it, so inputs stay
|
|
302
|
+
* compatible when every earlier alternative is covered by a later one, and outputs when every later
|
|
303
|
+
* alternative is covered by an earlier one. Identical alternatives match first, then alternatives with
|
|
304
|
+
* the same reference target or type, then a single remaining alternative on each side.
|
|
305
|
+
*/
|
|
306
|
+
function compareAlternatives(before, after, direction, depth, equal, reasons, scope) {
|
|
307
|
+
const codes = CODES[direction];
|
|
308
|
+
if (before.length > MAX_ALTERNATIVES || after.length > MAX_ALTERNATIVES) {
|
|
309
|
+
reasons.add(codes.unclassified);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const unpairedBefore = [...before];
|
|
313
|
+
const unpairedAfter = [...after];
|
|
314
|
+
for (let index = unpairedBefore.length - 1; index >= 0; index -= 1) {
|
|
315
|
+
const match = unpairedAfter.findIndex((candidate) => sameMeaning(unpairedBefore[index], candidate, equal, scope));
|
|
316
|
+
if (match !== -1) {
|
|
317
|
+
unpairedBefore.splice(index, 1);
|
|
318
|
+
unpairedAfter.splice(match, 1);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const pairs = [];
|
|
322
|
+
for (const key of new Set(unpairedBefore.map(alternativeKey))) {
|
|
323
|
+
if (key === null)
|
|
324
|
+
continue;
|
|
325
|
+
const beforeMatches = unpairedBefore.filter((alternative) => alternativeKey(alternative) === key);
|
|
326
|
+
const afterMatches = unpairedAfter.filter((alternative) => alternativeKey(alternative) === key);
|
|
327
|
+
if (beforeMatches.length !== 1 || afterMatches.length !== 1)
|
|
328
|
+
continue;
|
|
329
|
+
pairs.push([beforeMatches[0], afterMatches[0]]);
|
|
330
|
+
unpairedBefore.splice(unpairedBefore.indexOf(beforeMatches[0]), 1);
|
|
331
|
+
unpairedAfter.splice(unpairedAfter.indexOf(afterMatches[0]), 1);
|
|
332
|
+
}
|
|
333
|
+
if (unpairedBefore.length === 1 && unpairedAfter.length === 1) {
|
|
334
|
+
pairs.push([unpairedBefore.pop(), unpairedAfter.pop()]);
|
|
335
|
+
}
|
|
336
|
+
else if (unpairedBefore.length > 0 && unpairedAfter.length > 0) {
|
|
337
|
+
reasons.add(codes.unclassified);
|
|
338
|
+
}
|
|
339
|
+
for (const [beforeAlternative, afterAlternative] of pairs) {
|
|
340
|
+
compareSchema(beforeAlternative, afterAlternative, direction, depth, equal, reasons, scope);
|
|
341
|
+
}
|
|
342
|
+
// A dropped input alternative may reject values callers send; an added output alternative may
|
|
343
|
+
// produce values consumers do not expect. The opposite changes only narrow or widen safely.
|
|
344
|
+
const narrowedOrWidened = direction === "input" ? unpairedAfter.length === 0 && unpairedBefore.length > 0 : unpairedBefore.length === 0 && unpairedAfter.length > 0;
|
|
345
|
+
if (narrowedOrWidened)
|
|
346
|
+
reasons.add(depth === 0 ? codes.rootType : codes.propertyType);
|
|
347
|
+
}
|
|
171
348
|
/** Flags a `pattern` or `format` that `outer` adds and `inner` lacks; a changed value cannot be compared. */
|
|
172
349
|
function compareStringConstraints(inner, outer, codes, reasons) {
|
|
173
350
|
for (const keyword of STRING_CONSTRAINT_KEYWORDS) {
|
|
@@ -291,22 +468,32 @@ function referenceScope(before, after) {
|
|
|
291
468
|
fragmentsOnly: reachesDefinitionsOnlyThroughFragments(before) && reachesDefinitionsOnlyThroughFragments(after),
|
|
292
469
|
active: new Set(),
|
|
293
470
|
completed: new Map(),
|
|
471
|
+
inProgress: new Map(),
|
|
472
|
+
wrappers: 0,
|
|
294
473
|
same: new Map(),
|
|
295
474
|
pointers: new Map(),
|
|
296
475
|
remaining: MAX_COMPARISONS
|
|
297
476
|
};
|
|
298
477
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
let byAfter = scope.completed.get(before);
|
|
478
|
+
function pairDepths(pairs, before, after) {
|
|
479
|
+
let byAfter = pairs.get(before);
|
|
302
480
|
if (byAfter === undefined)
|
|
303
|
-
|
|
481
|
+
pairs.set(before, byAfter = new Map());
|
|
304
482
|
let depths = byAfter.get(after);
|
|
305
483
|
if (depths === undefined)
|
|
306
484
|
byAfter.set(after, depths = new Set());
|
|
307
|
-
|
|
485
|
+
return depths;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Records a resolved pair as compared at this depth and wrapper count; returns false when it already
|
|
489
|
+
* was. The wrapper count is part of the key because it limits how far that comparison may unwrap.
|
|
490
|
+
*/
|
|
491
|
+
function markCompleted(scope, before, after, depth) {
|
|
492
|
+
const depths = pairDepths(scope.completed, before, after);
|
|
493
|
+
const key = depth * (MAX_WRAPPER_HOPS + 1) + scope.wrappers;
|
|
494
|
+
if (depths.has(key))
|
|
308
495
|
return false;
|
|
309
|
-
depths.add(
|
|
496
|
+
depths.add(key);
|
|
310
497
|
return true;
|
|
311
498
|
}
|
|
312
499
|
/**
|
|
@@ -401,10 +588,26 @@ function compareSchema(before, after, direction, depth, equal, reasons, scope) {
|
|
|
401
588
|
return;
|
|
402
589
|
const resolvedBefore = resolveReferences(before, scope.before);
|
|
403
590
|
const resolvedAfter = resolveReferences(after, scope.after);
|
|
404
|
-
if (resolvedBefore === null || resolvedAfter === null) {
|
|
591
|
+
if (resolvedBefore === null || resolvedAfter === null || depth > MAX_NESTED_DEPTH) {
|
|
405
592
|
reasons.add(codes.unclassified);
|
|
406
593
|
return;
|
|
407
594
|
}
|
|
595
|
+
// When only one side is a composition, the other keeps its reference unresolved, so it can still
|
|
596
|
+
// pair with an alternative that refers to the same definition, as when a model becomes optional.
|
|
597
|
+
const beforeComposed = isComposition(resolvedBefore.schema);
|
|
598
|
+
const afterComposed = isComposition(resolvedAfter.schema);
|
|
599
|
+
if (beforeComposed !== afterComposed) {
|
|
600
|
+
// The reference is kept only when an alternative can pair with it; otherwise its target compares
|
|
601
|
+
// with inline alternatives by type.
|
|
602
|
+
const composite = beforeComposed ? resolvedBefore.schema : resolvedAfter.schema;
|
|
603
|
+
const original = beforeComposed ? after : before;
|
|
604
|
+
const plainKey = alternativeKey(original);
|
|
605
|
+
const keepReference = (alternatives(composite) ?? [])
|
|
606
|
+
.some((alternative) => (plainKey !== null && alternativeKey(alternative) === plainKey) || sameMeaning(original, alternative, equal, scope));
|
|
607
|
+
const plain = keepReference ? original : (beforeComposed ? resolvedAfter.schema : resolvedBefore.schema);
|
|
608
|
+
compareComposition(beforeComposed ? composite : plain, beforeComposed ? plain : composite, direction, depth, equal, reasons, scope);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
408
611
|
if (resolvedBefore.ref !== undefined || resolvedAfter.ref !== undefined) {
|
|
409
612
|
// A reference pair already being compared further up is assumed to hold, so recursive
|
|
410
613
|
// definitions terminate; a pair with one inline side is bounded by the depth limit. A pair
|
|
@@ -412,22 +615,35 @@ function compareSchema(before, after, direction, depth, equal, reasons, scope) {
|
|
|
412
615
|
const pair = resolvedBefore.ref !== undefined && resolvedAfter.ref !== undefined
|
|
413
616
|
? JSON.stringify([resolvedBefore.ref, resolvedAfter.ref])
|
|
414
617
|
: undefined;
|
|
618
|
+
// Reaching a pair again at the same depth, for example through a definition that wraps a
|
|
619
|
+
// reference to itself, is a cycle that never reaches a value, so it cannot be judged.
|
|
620
|
+
const inProgress = pairDepths(scope.inProgress, resolvedBefore.schema, resolvedAfter.schema);
|
|
621
|
+
if (inProgress.has(depth)) {
|
|
622
|
+
reasons.add(codes.unclassified);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
415
625
|
if (pair !== undefined && scope.active.has(pair))
|
|
416
626
|
return;
|
|
417
627
|
if (!markCompleted(scope, resolvedBefore.schema, resolvedAfter.schema, depth))
|
|
418
628
|
return;
|
|
419
629
|
if (pair !== undefined)
|
|
420
630
|
scope.active.add(pair);
|
|
631
|
+
inProgress.add(depth);
|
|
421
632
|
compareSchema(resolvedBefore.schema, resolvedAfter.schema, direction, depth, equal, reasons, scope);
|
|
633
|
+
inProgress.delete(depth);
|
|
422
634
|
if (pair !== undefined)
|
|
423
635
|
scope.active.delete(pair);
|
|
424
636
|
return;
|
|
425
637
|
}
|
|
638
|
+
if (beforeComposed) {
|
|
639
|
+
compareComposition(before, after, direction, depth, equal, reasons, scope);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
426
642
|
const root = depth === 0;
|
|
427
643
|
const structural = scope.fragmentsOnly
|
|
428
644
|
? root ? ROOT_STRUCTURAL_WITH_DEFINITIONS : NESTED_STRUCTURAL_WITH_DEFINITIONS
|
|
429
645
|
: root ? ROOT_STRUCTURAL_KEYWORDS : NESTED_STRUCTURAL_KEYWORDS;
|
|
430
|
-
if (
|
|
646
|
+
if (!isJsonObject(before) || !isJsonObject(after)
|
|
431
647
|
|| !sameMeaning(keywordsOutside(before, structural), keywordsOutside(after, structural), equal, scope)
|
|
432
648
|
|| !equal(before.$schema, after.$schema)) {
|
|
433
649
|
reasons.add(codes.unclassified);
|
|
@@ -446,6 +662,7 @@ function compareSchema(before, after, direction, depth, equal, reasons, scope) {
|
|
|
446
662
|
else if (!enumResult)
|
|
447
663
|
reasons.add(codes.enumChanged);
|
|
448
664
|
compareBounds(inner, outer, codes, reasons);
|
|
665
|
+
compareNumericAndArrayConstraints(inner, outer, codes, reasons);
|
|
449
666
|
compareStringConstraints(inner, outer, codes, reasons);
|
|
450
667
|
if (!sameMeaning(before.items, after.items, equal, scope)) {
|
|
451
668
|
if (unevaluated)
|
package/dist/run-cli.js
CHANGED
|
@@ -49,7 +49,7 @@ import { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorIn
|
|
|
49
49
|
import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
|
|
50
50
|
import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
|
|
51
51
|
import { buildDoctorSize, renderDoctorSize, renderDoctorSizeJson } from "./core/doctor-size.js";
|
|
52
|
-
import { initCiWorkflow } from "./core/init-ci.js";
|
|
52
|
+
import { initCiWorkflow, initToolsDiffWorkflow } from "./core/init-ci.js";
|
|
53
53
|
import { watchPlugin } from "./core/watch-plugin.js";
|
|
54
54
|
import { buildDepAudit, renderDepAudit, renderDepAuditJson, renderDepAuditSarif } from "./core/dep-audit.js";
|
|
55
55
|
import { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";
|
|
@@ -117,6 +117,40 @@ const toolsUsage = "Usage: codex-plugin-doctor doctor tools <url> --allow-networ
|
|
|
117
117
|
const discoveryUsage = "Usage: codex-plugin-doctor doctor discover <url> --allow-network [--allow-local-network] [--json] [--bearer-token-env <NAME>]";
|
|
118
118
|
const bearerTokenHelp = "--bearer-token-env reads a bearer token from the named environment variable and sends it only over HTTPS or to an approved loopback target; the token is never printed or saved.";
|
|
119
119
|
const ENVIRONMENT_VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
120
|
+
/** Reads init-ci --tools-diff-server, --baseline, and --sandbox; options is null when none is given. */
|
|
121
|
+
function parseToolsDiffWorkflowOptions(flags) {
|
|
122
|
+
const values = new Map();
|
|
123
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
124
|
+
if (!["--tools-diff-server", "--baseline", "--sandbox"].includes(flags[index]))
|
|
125
|
+
continue;
|
|
126
|
+
const value = flags[index + 1];
|
|
127
|
+
const present = value !== undefined && !value.startsWith("--");
|
|
128
|
+
values.set(flags[index], present ? value : undefined);
|
|
129
|
+
if (present)
|
|
130
|
+
index += 1;
|
|
131
|
+
}
|
|
132
|
+
if (values.size === 0)
|
|
133
|
+
return { options: null };
|
|
134
|
+
if (!values.has("--tools-diff-server"))
|
|
135
|
+
return { error: "--baseline and --sandbox require --tools-diff-server." };
|
|
136
|
+
// Control characters would break the generated workflow lines, GitHub evaluates "${{" in any
|
|
137
|
+
// workflow value, and the Action rejects values that start with "-".
|
|
138
|
+
const usable = (value) => value !== undefined && value.trim() !== "" && !value.startsWith("-") && !value.includes("${{")
|
|
139
|
+
&& !/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(value);
|
|
140
|
+
const server = values.get("--tools-diff-server");
|
|
141
|
+
if (!usable(server))
|
|
142
|
+
return { error: "--tools-diff-server requires a server name." };
|
|
143
|
+
// The workflow runs on Linux from the repository root, so the baseline is a relative POSIX path.
|
|
144
|
+
const baseline = (values.has("--baseline") ? values.get("--baseline") : "mcp/tools-baseline.json")?.replaceAll("\\", "/");
|
|
145
|
+
if (!usable(baseline) || path.posix.isAbsolute(baseline) || /^[A-Za-z]:/u.test(baseline)
|
|
146
|
+
|| path.posix.normalize(baseline).split("/").includes("..")) {
|
|
147
|
+
return { error: "--baseline requires a relative path inside the repository." };
|
|
148
|
+
}
|
|
149
|
+
const sandbox = values.get("--sandbox");
|
|
150
|
+
if (values.has("--sandbox") && sandbox !== "docker")
|
|
151
|
+
return { error: "--sandbox must be docker." };
|
|
152
|
+
return { options: { server, baseline, sandbox: sandbox === "docker" ? "docker" : null } };
|
|
153
|
+
}
|
|
120
154
|
const MAX_FORWARDED_ENVIRONMENT_VARIABLES = 32;
|
|
121
155
|
const forwardedEnvironmentHelp = "--env forwards the named host environment variable into the Docker sandbox by name; Doctor never prints its value, writes it to a report, or passes it as a command argument. Native servers already inherit Doctor's environment.";
|
|
122
156
|
function resolveBearerToken(name, env) {
|
|
@@ -340,7 +374,7 @@ function parseRemoteNetworkFlags(flags, runtime) {
|
|
|
340
374
|
return { allowNetwork, allowLocalNetwork, allowSessionLifecycle, requireRemoteReliability };
|
|
341
375
|
}
|
|
342
376
|
function printUsage(io) {
|
|
343
|
-
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>] [--fail-on <rule-id>]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--runtime [--allow-network [--allow-local-network]]] [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus [--manifest <corpus.json>] [--json] [--output <path>]|corpus metrics --manifest <corpus.json> [--json|--markdown] [--output <path>] [--min-precision <0..1>] [--min-recall <0..1>] [--max-false-positive-rate <0..1>]|runtime-plan <path> [--sandbox docker] [--json|--markdown] [--output <path>]|runtime-policy <path> [--sandbox docker] [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>] [--upload]|mcp <path> [--runtime [--allow-network [--allow-local-network]]]|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor completion bash|zsh|fish\n codex-plugin-doctor config validate <path> [--json]\n codex-plugin-doctor release check <path> [--json] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
|
|
377
|
+
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>] [--fail-on <rule-id>]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--runtime [--allow-network [--allow-local-network]]] [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus [--manifest <corpus.json>] [--json] [--output <path>]|corpus metrics --manifest <corpus.json> [--json|--markdown] [--output <path>] [--min-precision <0..1>] [--min-recall <0..1>] [--max-false-positive-rate <0..1>]|runtime-plan <path> [--sandbox docker] [--json|--markdown] [--output <path>]|runtime-policy <path> [--sandbox docker] [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>] [--upload]|mcp <path> [--runtime [--allow-network [--allow-local-network]]]|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path] [--tools-diff-server <name> [--baseline <path>] [--sandbox docker]]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor completion bash|zsh|fish\n codex-plugin-doctor config validate <path> [--json]\n codex-plugin-doctor release check <path> [--json] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
|
|
344
378
|
io.writeStderr(discoveryUsage);
|
|
345
379
|
io.writeStderr(toolsUsage);
|
|
346
380
|
io.writeStderr(toolsFileUsage);
|
|
@@ -1308,9 +1342,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1308
1342
|
(command === "doctor" &&
|
|
1309
1343
|
(["runtime-plan", "runtime-policy"].includes(maybePath ?? "") ||
|
|
1310
1344
|
(maybePath === "release-evidence" && remainingArgs[0] !== "verify") ||
|
|
1311
|
-
(maybePath === "tools" && remainingArgs.includes("--server"))))
|
|
1345
|
+
(maybePath === "tools" && remainingArgs.includes("--server")))) ||
|
|
1346
|
+
(command === "init-ci" && args.includes("--tools-diff-server"));
|
|
1312
1347
|
if (args.includes("--sandbox") && !runtimeSandboxSupported) {
|
|
1313
|
-
io.writeStderr("--sandbox is supported only by single-package check, release check, runtime-plan, runtime-policy, release-evidence,
|
|
1348
|
+
io.writeStderr("--sandbox is supported only by single-package check, release check, runtime-plan, runtime-policy, release-evidence, doctor tools --server, and init-ci --tools-diff-server.");
|
|
1314
1349
|
return 2;
|
|
1315
1350
|
}
|
|
1316
1351
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -2771,6 +2806,23 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2771
2806
|
}
|
|
2772
2807
|
if (command === "init-ci") {
|
|
2773
2808
|
const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
|
|
2809
|
+
const initFlags = maybePath && maybePath.startsWith("--") ? [maybePath, ...remainingArgs] : remainingArgs;
|
|
2810
|
+
const toolsDiff = parseToolsDiffWorkflowOptions(initFlags);
|
|
2811
|
+
if ("error" in toolsDiff) {
|
|
2812
|
+
io.writeStderr(toolsDiff.error);
|
|
2813
|
+
return 2;
|
|
2814
|
+
}
|
|
2815
|
+
if (toolsDiff.options !== null) {
|
|
2816
|
+
const { server, baseline, sandbox } = toolsDiff.options;
|
|
2817
|
+
const toolsResult = await initToolsDiffWorkflow(targetPath, toolsDiff.options);
|
|
2818
|
+
io.writeStdout([
|
|
2819
|
+
"Initialized Codex Plugin Doctor tool diff workflow",
|
|
2820
|
+
`Root: ${toolsResult.rootPath}`,
|
|
2821
|
+
`Workflow: ${toolsResult.workflowPath}`,
|
|
2822
|
+
`Capture and commit a baseline at ${baseline}, for example with: codex-plugin-doctor doctor tools ${JSON.stringify(targetPath)} --server ${JSON.stringify(server)} --runtime${sandbox === "docker" ? " --sandbox docker" : ""} --save-response ${JSON.stringify(path.join(targetPath, baseline))}`
|
|
2823
|
+
].join("\n"));
|
|
2824
|
+
return 0;
|
|
2825
|
+
}
|
|
2774
2826
|
const result = await initCiWorkflow(targetPath);
|
|
2775
2827
|
io.writeStdout([
|
|
2776
2828
|
"Initialized Codex Plugin Doctor workflow",
|
package/package.json
CHANGED