codex-plugin-doctor 1.30.0 → 1.32.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 +2 -2
- package/dist/core/init-git-hooks.d.ts +6 -0
- package/dist/core/init-git-hooks.js +21 -0
- package/dist/core/watch-plugin.d.ts +4 -0
- package/dist/core/watch-plugin.js +84 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/run-cli.js +52 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -400,9 +400,9 @@ jobs:
|
|
|
400
400
|
runs-on: ubuntu-latest
|
|
401
401
|
steps:
|
|
402
402
|
- uses: actions/checkout@v5
|
|
403
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
403
|
+
- uses: Esquetta/CodexPluginDoctor@v1.32.0
|
|
404
404
|
with:
|
|
405
|
-
version: "1.
|
|
405
|
+
version: "1.32.0"
|
|
406
406
|
path: .
|
|
407
407
|
runtime: "true"
|
|
408
408
|
policy: codex-publish
|
|
@@ -3,6 +3,12 @@ export interface InitGitHooksResult {
|
|
|
3
3
|
hookPaths: string[];
|
|
4
4
|
preExisting: string[];
|
|
5
5
|
}
|
|
6
|
+
export interface RemoveGitHooksResult {
|
|
7
|
+
rootPath: string;
|
|
8
|
+
removed: string[];
|
|
9
|
+
skipped: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function removeGitHooks(targetPath: string): Promise<RemoveGitHooksResult>;
|
|
6
12
|
export declare function initGitHooks(targetPath: string, options?: {
|
|
7
13
|
force?: boolean;
|
|
8
14
|
}): Promise<InitGitHooksResult>;
|
|
@@ -54,6 +54,27 @@ async function writeHook(hookPath, content, force) {
|
|
|
54
54
|
}
|
|
55
55
|
return exists;
|
|
56
56
|
}
|
|
57
|
+
export async function removeGitHooks(targetPath) {
|
|
58
|
+
const rootPath = path.resolve(targetPath);
|
|
59
|
+
const hooksDir = path.join(rootPath, ".git", "hooks");
|
|
60
|
+
const hookNames = ["pre-commit", "pre-push"];
|
|
61
|
+
const removed = [];
|
|
62
|
+
const skipped = [];
|
|
63
|
+
for (const name of hookNames) {
|
|
64
|
+
const hookPath = path.join(hooksDir, name);
|
|
65
|
+
if (await fileExists(hookPath)) {
|
|
66
|
+
const content = await import("node:fs/promises").then((m) => m.readFile(hookPath, "utf8"));
|
|
67
|
+
if (content.includes("Generated by codex-plugin-doctor init git-hooks")) {
|
|
68
|
+
await import("node:fs/promises").then((m) => m.rm(hookPath));
|
|
69
|
+
removed.push(hookPath);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
skipped.push(hookPath);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { rootPath, removed, skipped };
|
|
77
|
+
}
|
|
57
78
|
export async function initGitHooks(targetPath, options = {}) {
|
|
58
79
|
const rootPath = path.resolve(targetPath);
|
|
59
80
|
const hooksDir = path.join(rootPath, ".git", "hooks");
|
|
@@ -4,10 +4,14 @@ export interface WatchPluginOptions {
|
|
|
4
4
|
runtime?: boolean;
|
|
5
5
|
jsonOutput?: boolean;
|
|
6
6
|
outputPath?: string | null;
|
|
7
|
+
maxIterations?: number;
|
|
8
|
+
failFast?: boolean;
|
|
9
|
+
accumulateJsonPath?: string | null;
|
|
7
10
|
}
|
|
8
11
|
export interface WatchPluginResult {
|
|
9
12
|
targetPath: string;
|
|
10
13
|
validations: number;
|
|
11
14
|
failures: number;
|
|
15
|
+
iterationsReached?: boolean;
|
|
12
16
|
}
|
|
13
17
|
export declare function watchPlugin(options: WatchPluginOptions): Promise<WatchPluginResult>;
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { validatePlugin } from "./validate-plugin.js";
|
|
5
|
+
const ignoredDirs = new Set([
|
|
6
|
+
"node_modules",
|
|
7
|
+
".git",
|
|
8
|
+
"dist",
|
|
9
|
+
"coverage",
|
|
10
|
+
".cache",
|
|
11
|
+
"build",
|
|
12
|
+
".turbo",
|
|
13
|
+
".next",
|
|
14
|
+
".nuxt",
|
|
15
|
+
"__pycache__"
|
|
16
|
+
]);
|
|
17
|
+
function isIgnored(filename) {
|
|
18
|
+
for (const dir of ignoredDirs) {
|
|
19
|
+
if (filename.includes(dir) || filename.startsWith(`${dir}/`) || filename.startsWith(`${dir}\\`)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
4
25
|
function renderWatchValidation(result, iteration, jsonOutput) {
|
|
5
26
|
if (jsonOutput) {
|
|
6
27
|
const report = {
|
|
@@ -12,7 +33,7 @@ function renderWatchValidation(result, iteration, jsonOutput) {
|
|
|
12
33
|
findingsCount: result.findings.length,
|
|
13
34
|
findings: result.findings
|
|
14
35
|
};
|
|
15
|
-
return JSON.stringify(report
|
|
36
|
+
return JSON.stringify(report);
|
|
16
37
|
}
|
|
17
38
|
const icon = result.status === "pass" ? "PASS" : "FAIL";
|
|
18
39
|
return [
|
|
@@ -24,25 +45,40 @@ function renderWatchValidation(result, iteration, jsonOutput) {
|
|
|
24
45
|
].filter(Boolean).join("\n");
|
|
25
46
|
}
|
|
26
47
|
export async function watchPlugin(options) {
|
|
27
|
-
const { targetPath, debounceMs = 300, runtime = false, jsonOutput = false, outputPath = null } = options;
|
|
48
|
+
const { targetPath, debounceMs = 300, runtime = false, jsonOutput = false, outputPath = null, maxIterations = 0, failFast = false, accumulateJsonPath = null } = options;
|
|
28
49
|
const resolvedPath = path.resolve(targetPath);
|
|
29
50
|
let validations = 0;
|
|
30
51
|
let failures = 0;
|
|
31
52
|
let timer = null;
|
|
32
53
|
let pending = false;
|
|
33
54
|
const runValidation = async (iteration) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
try {
|
|
56
|
+
const result = await validatePlugin(resolvedPath, { runtime });
|
|
57
|
+
if (result.status !== "pass") {
|
|
58
|
+
failures += 1;
|
|
59
|
+
}
|
|
60
|
+
const output = renderWatchValidation(result, iteration, jsonOutput);
|
|
61
|
+
if (outputPath) {
|
|
62
|
+
await writeFile(outputPath, output, "utf8");
|
|
63
|
+
}
|
|
64
|
+
if (accumulateJsonPath && jsonOutput) {
|
|
65
|
+
await writeFile(accumulateJsonPath, `${output}\n`, { encoding: "utf8", flag: "a" });
|
|
66
|
+
}
|
|
67
|
+
if (!outputPath && !accumulateJsonPath) {
|
|
68
|
+
if (jsonOutput) {
|
|
69
|
+
process.stdout.write(`${JSON.stringify(JSON.parse(output), null, 2)}\n\n`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
process.stdout.write(`${output}\n\n`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
38
75
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
await writeFile(outputPath, output, "utf8");
|
|
76
|
+
catch (error) {
|
|
77
|
+
process.stderr.write(`Watch validation error: ${error.message}\n`);
|
|
78
|
+
failures += 1;
|
|
43
79
|
}
|
|
44
|
-
|
|
45
|
-
|
|
80
|
+
finally {
|
|
81
|
+
validations += 1;
|
|
46
82
|
}
|
|
47
83
|
};
|
|
48
84
|
return new Promise((resolve, _reject) => {
|
|
@@ -60,10 +96,29 @@ export async function watchPlugin(options) {
|
|
|
60
96
|
timer = null;
|
|
61
97
|
iteration += 1;
|
|
62
98
|
await runValidation(iteration);
|
|
99
|
+
if (failFast && failures > 0) {
|
|
100
|
+
cleanup();
|
|
101
|
+
resolve({ targetPath: resolvedPath, validations, failures });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (maxIterations > 0 && validations >= maxIterations) {
|
|
105
|
+
cleanup();
|
|
106
|
+
resolve({ targetPath: resolvedPath, validations, failures, iterationsReached: true });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (maxIterations > 0) {
|
|
110
|
+
schedule();
|
|
111
|
+
}
|
|
63
112
|
}, debounceMs);
|
|
64
113
|
};
|
|
65
114
|
const watcher = watch(resolvedPath, { recursive: true }, (_eventType, filename) => {
|
|
66
|
-
if (!filename ||
|
|
115
|
+
if (!filename || isIgnored(filename)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (outputPath && filename === path.basename(outputPath)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (accumulateJsonPath && filename === path.basename(accumulateJsonPath)) {
|
|
67
122
|
return;
|
|
68
123
|
}
|
|
69
124
|
schedule();
|
|
@@ -71,17 +126,31 @@ export async function watchPlugin(options) {
|
|
|
71
126
|
watcher.on("error", (error) => {
|
|
72
127
|
process.stderr.write(`Watch error: ${error.message}\n`);
|
|
73
128
|
});
|
|
74
|
-
const
|
|
129
|
+
const cleanup = () => {
|
|
75
130
|
if (timer !== null) {
|
|
76
131
|
clearTimeout(timer);
|
|
77
132
|
}
|
|
78
133
|
watcher.close();
|
|
134
|
+
process.removeListener("SIGINT", handleExit);
|
|
135
|
+
process.removeListener("SIGTERM", handleExit);
|
|
136
|
+
process.removeListener("SIGHUP", handleExit);
|
|
137
|
+
};
|
|
138
|
+
const handleExit = () => {
|
|
139
|
+
cleanup();
|
|
79
140
|
resolve({ targetPath: resolvedPath, validations, failures });
|
|
80
141
|
};
|
|
81
142
|
process.on("SIGINT", handleExit);
|
|
82
143
|
process.on("SIGTERM", handleExit);
|
|
83
144
|
process.on("SIGHUP", handleExit);
|
|
84
|
-
|
|
145
|
+
const flags = [];
|
|
146
|
+
if (maxIterations > 0) {
|
|
147
|
+
flags.push(`max-iterations: ${maxIterations}`);
|
|
148
|
+
}
|
|
149
|
+
if (failFast) {
|
|
150
|
+
flags.push("fail-fast");
|
|
151
|
+
}
|
|
152
|
+
const flagsStr = flags.length > 0 ? `, ${flags.join(", ")}` : "";
|
|
153
|
+
process.stdout.write(`Watching ${resolvedPath} for changes (debounce: ${debounceMs}ms, runtime: ${runtime ? "enabled" : "disabled"}${flagsStr})...\nPress Ctrl+C to stop.\n\n`);
|
|
85
154
|
schedule();
|
|
86
155
|
});
|
|
87
156
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -26,5 +26,5 @@ export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack,
|
|
|
26
26
|
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
|
|
27
27
|
export { watchPlugin, type WatchPluginOptions, type WatchPluginResult } from "./core/watch-plugin.js";
|
|
28
28
|
export { buildDepAudit, renderDepAudit, renderDepAuditJson, type DepAuditReport, type DepAuditVulnerability } from "./core/dep-audit.js";
|
|
29
|
-
export { initGitHooks, type InitGitHooksResult } from "./core/init-git-hooks.js";
|
|
29
|
+
export { initGitHooks, removeGitHooks, type InitGitHooksResult, type RemoveGitHooksResult } from "./core/init-git-hooks.js";
|
|
30
30
|
export declare function runCheck(targetPath: string, options?: CheckOptions): Promise<CheckResult>;
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack,
|
|
|
26
26
|
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
|
|
27
27
|
export { watchPlugin } from "./core/watch-plugin.js";
|
|
28
28
|
export { buildDepAudit, renderDepAudit, renderDepAuditJson } from "./core/dep-audit.js";
|
|
29
|
-
export { initGitHooks } from "./core/init-git-hooks.js";
|
|
29
|
+
export { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";
|
|
30
30
|
export async function runCheck(targetPath, options = {}) {
|
|
31
31
|
return validatePlugin(targetPath, options);
|
|
32
32
|
}
|
package/dist/run-cli.js
CHANGED
|
@@ -32,7 +32,7 @@ import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJso
|
|
|
32
32
|
import { initCiWorkflow } from "./core/init-ci.js";
|
|
33
33
|
import { watchPlugin } from "./core/watch-plugin.js";
|
|
34
34
|
import { buildDepAudit, renderDepAudit, renderDepAuditJson } from "./core/dep-audit.js";
|
|
35
|
-
import { initGitHooks } from "./core/init-git-hooks.js";
|
|
35
|
+
import { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";
|
|
36
36
|
import { initPluginPackage, initPluginTemplates, isInitPluginTemplate } from "./core/init-plugin.js";
|
|
37
37
|
import { runCheck } from "./index.js";
|
|
38
38
|
import { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
|
|
@@ -78,7 +78,7 @@ const defaultIo = {
|
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
80
|
function printUsage(io) {
|
|
81
|
-
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] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\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> [--json] [--output <path>]\n codex-plugin-doctor mcp <path> [--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>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--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 [--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 [--upload]|mcp <path>|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 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");
|
|
81
|
+
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] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\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> [--json] [--output <path>]\n codex-plugin-doctor mcp <path> [--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|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--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 [--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 [--upload]|mcp <path>|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 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");
|
|
82
82
|
io.writeStderr("Suppression governance: codex-plugin-doctor suppress list <path> [--fail-on-expired] [--fail-on-invalid] [--warn-expiring-within-days <days>]\n codex-plugin-doctor suppress prune <path> [--apply] [--json]");
|
|
83
83
|
}
|
|
84
84
|
const suppressUsageText = [
|
|
@@ -1754,8 +1754,38 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1754
1754
|
const initFlags = maybePath && maybePath.startsWith("--")
|
|
1755
1755
|
? [maybePath, ...remainingArgs]
|
|
1756
1756
|
: remainingArgs;
|
|
1757
|
+
const remove = initFlags.includes("--remove");
|
|
1757
1758
|
const force = initFlags.includes("--force");
|
|
1758
1759
|
const jsonOutput = initFlags.includes("--json");
|
|
1760
|
+
if (remove) {
|
|
1761
|
+
const removeResult = await removeGitHooks(targetPath);
|
|
1762
|
+
if (jsonOutput) {
|
|
1763
|
+
io.writeStdout(JSON.stringify({
|
|
1764
|
+
schemaVersion: "1.0.0",
|
|
1765
|
+
kind: "doctor.git.hooks.remove",
|
|
1766
|
+
rootPath: removeResult.rootPath,
|
|
1767
|
+
removed: removeResult.removed,
|
|
1768
|
+
skipped: removeResult.skipped
|
|
1769
|
+
}, null, 2));
|
|
1770
|
+
return 0;
|
|
1771
|
+
}
|
|
1772
|
+
const lines = ["Removed Codex Plugin Doctor git hooks", `Root: ${removeResult.rootPath}`];
|
|
1773
|
+
if (removeResult.removed.length > 0) {
|
|
1774
|
+
lines.push(`Removed: ${removeResult.removed.join(", ")}`);
|
|
1775
|
+
}
|
|
1776
|
+
if (removeResult.skipped.length > 0) {
|
|
1777
|
+
lines.push(`Skipped (not generated by doctor): ${removeResult.skipped.join(", ")}`);
|
|
1778
|
+
}
|
|
1779
|
+
if (removeResult.removed.length === 0 && removeResult.skipped.length === 0) {
|
|
1780
|
+
lines.push("No doctor-generated hooks found.");
|
|
1781
|
+
}
|
|
1782
|
+
io.writeStdout(lines.join("\n"));
|
|
1783
|
+
return 0;
|
|
1784
|
+
}
|
|
1785
|
+
if (force && remove) {
|
|
1786
|
+
io.writeStderr("Use either --force or --remove, not both.");
|
|
1787
|
+
return 2;
|
|
1788
|
+
}
|
|
1759
1789
|
const result = await initGitHooks(targetPath, { force });
|
|
1760
1790
|
if (jsonOutput) {
|
|
1761
1791
|
io.writeStdout(JSON.stringify({
|
|
@@ -1785,23 +1815,41 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1785
1815
|
: remainingArgs;
|
|
1786
1816
|
const runtime = watchFlags.includes("--runtime");
|
|
1787
1817
|
const jsonOutput = watchFlags.includes("--json");
|
|
1818
|
+
const failFast = watchFlags.includes("--fail-fast");
|
|
1788
1819
|
const outputIndex = watchFlags.indexOf("--output");
|
|
1789
1820
|
const outputPath = outputIndex === -1 ? null : watchFlags[outputIndex + 1];
|
|
1790
1821
|
const debounceIndex = watchFlags.indexOf("--debounce-ms");
|
|
1791
1822
|
const debounceRaw = debounceIndex === -1 ? null : watchFlags[debounceIndex + 1];
|
|
1792
1823
|
const debounceMs = debounceRaw ? Number(debounceRaw) || 300 : 300;
|
|
1824
|
+
const maxIterationsIndex = watchFlags.indexOf("--max-iterations");
|
|
1825
|
+
const maxIterationsRaw = maxIterationsIndex === -1 ? null : watchFlags[maxIterationsIndex + 1];
|
|
1826
|
+
const maxIterations = maxIterationsRaw ? Number(maxIterationsRaw) || 0 : 0;
|
|
1827
|
+
const accumulateIndex = watchFlags.indexOf("--accumulate-json");
|
|
1828
|
+
const accumulatePath = accumulateIndex === -1 ? null : watchFlags[accumulateIndex + 1];
|
|
1793
1829
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
1794
1830
|
io.writeStderr("Missing path after --output.");
|
|
1795
1831
|
return 2;
|
|
1796
1832
|
}
|
|
1833
|
+
if (maxIterationsIndex !== -1 && (!maxIterationsRaw || maxIterationsRaw.startsWith("--"))) {
|
|
1834
|
+
io.writeStderr("Missing number after --max-iterations.");
|
|
1835
|
+
return 2;
|
|
1836
|
+
}
|
|
1837
|
+
if (accumulateIndex !== -1 && (!accumulatePath || accumulatePath.startsWith("--"))) {
|
|
1838
|
+
io.writeStderr("Missing path after --accumulate-json.");
|
|
1839
|
+
return 2;
|
|
1840
|
+
}
|
|
1797
1841
|
const result = await watchPlugin({
|
|
1798
1842
|
targetPath,
|
|
1799
1843
|
debounceMs,
|
|
1800
1844
|
runtime,
|
|
1801
1845
|
jsonOutput,
|
|
1802
|
-
outputPath
|
|
1846
|
+
outputPath,
|
|
1847
|
+
maxIterations,
|
|
1848
|
+
failFast,
|
|
1849
|
+
accumulateJsonPath: accumulatePath
|
|
1803
1850
|
});
|
|
1804
|
-
|
|
1851
|
+
const reason = result.iterationsReached ? "max iterations reached" : "stopped";
|
|
1852
|
+
io.writeStdout(`\nStopped watching ${result.targetPath}: ${result.validations} validations, ${result.failures} failures (${reason}).`);
|
|
1805
1853
|
return result.failures > 0 ? 1 : 0;
|
|
1806
1854
|
}
|
|
1807
1855
|
if (command === "fix") {
|
package/package.json
CHANGED