@damian87/omp 0.17.0 → 0.20.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/.github/dependabot.yml +42 -0
- package/.github/skills/code-review/SKILL.md +5 -1
- package/.github/skills/debug/SKILL.md +1 -1
- package/.github/skills/qa-browse/SKILL.md +125 -0
- package/.github/skills/ralplan/SKILL.md +4 -1
- package/.github/skills/tdd/SKILL.md +21 -6
- package/.github/skills/ultraqa/SKILL.md +1 -1
- package/.github/skills/verify/SKILL.md +1 -1
- package/.github/workflows/ci.yml +67 -0
- package/.github/workflows/security.yml +157 -0
- package/README.md +2 -0
- package/dist/src/cli.js +99 -10
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/models.d.ts +21 -0
- package/dist/src/commands/models.js +80 -0
- package/dist/src/commands/models.js.map +1 -0
- package/dist/src/commands/registry.js +2 -1
- package/dist/src/commands/registry.js.map +1 -1
- package/dist/src/copilot/doctor.d.ts +13 -0
- package/dist/src/copilot/doctor.js +43 -0
- package/dist/src/copilot/doctor.js.map +1 -1
- package/dist/src/copilot/models.d.ts +49 -0
- package/dist/src/copilot/models.js +67 -0
- package/dist/src/copilot/models.js.map +1 -0
- package/dist/src/copilot/paths.d.ts +3 -0
- package/dist/src/copilot/paths.js +1 -1
- package/dist/src/copilot/paths.js.map +1 -1
- package/dist/src/copilot/setup.d.ts +13 -1
- package/dist/src/copilot/setup.js +104 -6
- package/dist/src/copilot/setup.js.map +1 -1
- package/dist/src/council/engine.js +1 -5
- package/dist/src/council/engine.js.map +1 -1
- package/dist/src/council/persona.d.ts +14 -0
- package/dist/src/council/persona.js +45 -0
- package/dist/src/council/persona.js.map +1 -0
- package/dist/src/council/types.d.ts +8 -0
- package/dist/src/council/types.js +10 -1
- package/dist/src/council/types.js.map +1 -1
- package/dist/src/env/init.js +1 -1
- package/dist/src/env/init.js.map +1 -1
- package/dist/src/memory-review/config.d.ts +7 -0
- package/dist/src/memory-review/config.js +29 -0
- package/dist/src/memory-review/config.js.map +1 -1
- package/dist/src/memory-review/enable.d.ts +25 -0
- package/dist/src/memory-review/enable.js +106 -0
- package/dist/src/memory-review/enable.js.map +1 -0
- package/dist/src/memory-review/index.js +10 -0
- package/dist/src/memory-review/index.js.map +1 -1
- package/dist/src/memory-review/transcript.js +1 -1
- package/dist/src/memory-review/transcript.js.map +1 -1
- package/docs/security-pipeline.md +101 -0
- package/package.json +13 -6
- package/plugin.json +1 -1
- package/scripts/lib/memory-review-trigger.mjs +18 -8
- package/scripts/skills-safety-scan.mjs +231 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@damian87/omp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Multi-agent orchestration for GitHub Copilot CLI — autonomous loops (Autopilot, Ralph, UltraQA, Ultrawork), parallel tmux agent teams, a weighted-consensus model council, a Slack chat bridge, durable scheduled jobs, and in-session skills + custom agents. Zero learning curve.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -30,7 +30,11 @@
|
|
|
30
30
|
"catalog:list": "npm run build && node dist/src/cli.js catalog list",
|
|
31
31
|
"project:inspect": "npm run build && node dist/src/cli.js project inspect",
|
|
32
32
|
"test": "vitest run",
|
|
33
|
+
"lint": "eslint .",
|
|
34
|
+
"lint:fix": "eslint . --fix",
|
|
33
35
|
"lint:skills": "npm run build && node dist/src/cli.js lint:skills --root .",
|
|
36
|
+
"scan:skills": "node scripts/skills-safety-scan.mjs --root .",
|
|
37
|
+
"audit:ci": "npm audit --audit-level=high --omit=dev",
|
|
34
38
|
"sync:dry-run": "npm run build && node dist/src/cli.js sync:dry-run --root .",
|
|
35
39
|
"jira:dry-run": "npm run build && node dist/src/cli.js jira:dry-run --root .",
|
|
36
40
|
"omp:version": "npm run build && node dist/src/cli.js version",
|
|
@@ -57,12 +61,15 @@
|
|
|
57
61
|
"node": ">=20"
|
|
58
62
|
},
|
|
59
63
|
"devDependencies": {
|
|
60
|
-
"@
|
|
64
|
+
"@eslint/js": "^10.0.1",
|
|
65
|
+
"eslint": "^10.5.0",
|
|
66
|
+
"typescript-eslint": "^8.11.0",
|
|
67
|
+
"@types/node": "^26.0.1",
|
|
61
68
|
"@types/node-notifier": "^8.0.5",
|
|
62
|
-
"@vitest/coverage-v8": "^
|
|
63
|
-
"tsx": "^4.
|
|
64
|
-
"typescript": "^
|
|
65
|
-
"vitest": "^
|
|
69
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
70
|
+
"tsx": "^4.22.4",
|
|
71
|
+
"typescript": "^6.0.3",
|
|
72
|
+
"vitest": "^4.1.9"
|
|
66
73
|
},
|
|
67
74
|
"dependencies": {
|
|
68
75
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-copilot",
|
|
3
3
|
"description": "Multi-agent orchestration skills for GitHub Copilot CLI — autopilot, ralph, ultrawork, ultraqa, team, council, code-review and more as in-session slash skills + custom agents.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.20.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Damian Borek",
|
|
7
7
|
"email": "borekdamian@yahoo.pl"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { ompRoot } from "./omp-root.mjs";
|
|
@@ -9,20 +10,29 @@ import { ompRoot } from "./omp-root.mjs";
|
|
|
9
10
|
// downstream claim guard de-dupes against the wrapper fallback. Fail-open:
|
|
10
11
|
// any error means "don't trigger", never throw into the hook.
|
|
11
12
|
|
|
12
|
-
function
|
|
13
|
-
const env = process.env.OMP_MEMORY_MODE;
|
|
14
|
-
if (env === "on") return "on";
|
|
15
|
-
if (env === "off") return "off";
|
|
13
|
+
function readModeFrom(p) {
|
|
16
14
|
try {
|
|
17
|
-
|
|
18
|
-
if (!existsSync(p)) return "off";
|
|
15
|
+
if (!existsSync(p)) return undefined;
|
|
19
16
|
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
20
|
-
return raw && raw.memoryMode === "on"
|
|
17
|
+
return raw && (raw.memoryMode === "on" || raw.memoryMode === "off") ? raw.memoryMode : undefined;
|
|
21
18
|
} catch {
|
|
22
|
-
return
|
|
19
|
+
return undefined;
|
|
23
20
|
}
|
|
24
21
|
}
|
|
25
22
|
|
|
23
|
+
// Precedence mirrors readMemoryConfig (TS): OMP_MEMORY_MODE env > project
|
|
24
|
+
// .omp/config.json > GLOBAL ~/.omp/config.json. The global fallback is essential
|
|
25
|
+
// — `omp config set memory-mode on` writes GLOBAL, so without it the hook would
|
|
26
|
+
// never trigger for a globally-enabled (but project-unset) memory mode.
|
|
27
|
+
function readMemoryMode(cwd) {
|
|
28
|
+
const env = process.env.OMP_MEMORY_MODE;
|
|
29
|
+
if (env === "on" || env === "off") return env;
|
|
30
|
+
const projectMode = readModeFrom(join(ompRoot(cwd), ".omp", "config.json"));
|
|
31
|
+
if (projectMode) return projectMode;
|
|
32
|
+
const home = process.env.OMP_HOME_OVERRIDE || homedir();
|
|
33
|
+
return readModeFrom(join(home, ".omp", "config.json")) ?? "off";
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
function defaultDistPath() {
|
|
27
37
|
// scripts/lib/ -> packageRoot/dist/src/cli.js (present in the npm package and
|
|
28
38
|
// dev builds, but NOT in a plugin installed from GitHub — dist is gitignored).
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* skills-safety-scan — static safety audit for Agent Skills (SKILL.md) and agents.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the kinds of issues that Agent Trust Hub / Socket / Snyk flag for
|
|
6
|
+
* AI skills (see skills.sh audits), but runs locally with no network and no
|
|
7
|
+
* secrets so it can gate every PR:
|
|
8
|
+
*
|
|
9
|
+
* - Untrusted install / remote-code execution surfaces (curl | sh, npx <pkg>, -g, npm i remote)
|
|
10
|
+
* - Indirect prompt-injection surfaces (fetching + acting on third-party/user content)
|
|
11
|
+
* - Credential / secret exfiltration patterns
|
|
12
|
+
* - Obfuscation (base64 decode + exec, eval, large encoded blobs)
|
|
13
|
+
* - Destructive shell (rm -rf, dd, mkfs, chmod 777)
|
|
14
|
+
* - Frontmatter hygiene (missing name/description)
|
|
15
|
+
*
|
|
16
|
+
* Exit codes:
|
|
17
|
+
* 0 = no HIGH findings (warnings allowed)
|
|
18
|
+
* 1 = at least one HIGH finding, with --strict any MEDIUM finding, or no files
|
|
19
|
+
* scanned (target moved) unless --allow-empty is passed
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* node scripts/skills-safety-scan.mjs [--root .] [--strict] [--json] [--allow-empty]
|
|
23
|
+
*/
|
|
24
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
25
|
+
import { join, relative, sep } from "node:path";
|
|
26
|
+
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
const opt = (flag, def = null) => {
|
|
29
|
+
const i = args.indexOf(flag);
|
|
30
|
+
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : def;
|
|
31
|
+
};
|
|
32
|
+
const ROOT = typeof opt("--root") === "string" ? opt("--root") : ".";
|
|
33
|
+
const STRICT = !!opt("--strict", false);
|
|
34
|
+
const JSON_OUT = !!opt("--json", false);
|
|
35
|
+
const ALLOW_EMPTY = !!opt("--allow-empty", false);
|
|
36
|
+
|
|
37
|
+
// Where skills/agents live in this repo.
|
|
38
|
+
const SCAN_DIRS = [".github/skills", ".github/agents", "catalog"];
|
|
39
|
+
// Markdown/JSON docs plus bundled executable helpers — dangerous shell often
|
|
40
|
+
// lives in a skill's `scripts/*.sh`, not just its prose.
|
|
41
|
+
const SCAN_EXT = [".md", ".json", ".sh", ".bash", ".zsh", ".py", ".ps1", ".mjs", ".cjs", ".js"];
|
|
42
|
+
|
|
43
|
+
/** @type {{severity:'HIGH'|'MEDIUM'|'LOW',rule:string,file:string,line:number,match:string,why:string}[]} */
|
|
44
|
+
const findings = [];
|
|
45
|
+
|
|
46
|
+
const RULES = [
|
|
47
|
+
{
|
|
48
|
+
rule: "S001 remote-code-execution",
|
|
49
|
+
severity: "HIGH",
|
|
50
|
+
re: /\b(curl|wget)\b[^\n|]*\|\s*(sudo\s+)?(ba)?sh\b/i,
|
|
51
|
+
why: "Pipes a downloaded script straight into a shell (untrusted remote code execution).",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
rule: "S002 unpinned-remote-install",
|
|
55
|
+
severity: "MEDIUM",
|
|
56
|
+
re: /\bnpx\s+(?:-y\s+|--yes\s+)?(?!tsc\b|vitest\b|eslint\b)[a-z@][\w@/.-]*\s+add\b|\bnpm\s+i(nstall)?\s+(-g\s+)?https?:\/\//i,
|
|
57
|
+
why: "Installs/executes packages from an external source at runtime (supply-chain + untrusted install surface).",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
rule: "S003 global-install",
|
|
61
|
+
severity: "LOW",
|
|
62
|
+
re: /\bnpm\s+i(nstall)?\s+-g\b|\bnpx\b[^\n]*\s-g\b/i,
|
|
63
|
+
why: "Global install persists tooling at user/system level.",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
rule: "S004 prompt-injection-surface",
|
|
67
|
+
severity: "MEDIUM",
|
|
68
|
+
// Natural-language instruction, not a shell command — scan prose, not just code.
|
|
69
|
+
context: "prose",
|
|
70
|
+
re: /\b(fetch|read|download|ingest|browse)\b[^\n]*\b(untrusted|third-?party|user-generated|external (registry|source|content|repos?))\b/i,
|
|
71
|
+
why: "Fetches and may act on third-party/untrusted content — indirect prompt-injection risk (cf. Snyk W011).",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
rule: "S005 credential-exfiltration",
|
|
75
|
+
severity: "HIGH",
|
|
76
|
+
// The token keyword may be a suffix/component of the var name
|
|
77
|
+
// (`$GITHUB_TOKEN`, `$NPM_TOKEN`, `$AWS_SECRET_ACCESS_KEY`), not just a prefix.
|
|
78
|
+
re: /\b(env|printenv|cat)\b[^\n]*\b(\.env|secret|token|api[_-]?key|password|credential)\b[^\n]*\|\s*(curl|wget|nc)\b|\b(curl|wget)\b[^\n]*\$\{?\s*[A-Z0-9_]*(SECRET|TOKEN|API[_-]?KEY|PASSWORD)/i,
|
|
79
|
+
why: "Reads secrets/credentials and sends them off the machine.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
rule: "S006 obfuscation",
|
|
83
|
+
severity: "MEDIUM",
|
|
84
|
+
re: /\bbase64\b\s+(-d|--decode)\b[^\n]*\|\s*(ba)?sh|\beval\b\s*\(|\bFunction\s*\(\s*['"`]/i,
|
|
85
|
+
why: "Decodes/evaluates hidden code at runtime (obfuscation).",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
rule: "S007 destructive-shell",
|
|
89
|
+
severity: "HIGH",
|
|
90
|
+
// `chmod 777` and `chmod -R 777` (and `0777`) are all world-writable.
|
|
91
|
+
re: /\brm\s+-rf\s+[/~]|\bdd\s+if=|\bmkfs\b|\bchmod\s+(-R\s+)?0?777\b|>\s*\/dev\/sd/i,
|
|
92
|
+
why: "Destructive or overly-permissive filesystem operation.",
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
function walk(dir) {
|
|
97
|
+
let out = [];
|
|
98
|
+
let entries;
|
|
99
|
+
try {
|
|
100
|
+
entries = readdirSync(dir);
|
|
101
|
+
} catch {
|
|
102
|
+
return out; // dir may not exist in every repo
|
|
103
|
+
}
|
|
104
|
+
for (const name of entries) {
|
|
105
|
+
const p = join(dir, name);
|
|
106
|
+
let s;
|
|
107
|
+
try {
|
|
108
|
+
s = statSync(p);
|
|
109
|
+
} catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (s.isDirectory()) out = out.concat(walk(p));
|
|
113
|
+
else if (SCAN_EXT.some((e) => name.endsWith(e))) out.push(p);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Logical "code" lines for command-style rules:
|
|
119
|
+
// - Markdown (.md): only lines inside fenced code blocks (``` or ~~~), so
|
|
120
|
+
// documentation that merely *mentions* a dangerous command in prose can't
|
|
121
|
+
// trip a HIGH rule and block every PR.
|
|
122
|
+
// - Scripts/JSON: the whole file is code.
|
|
123
|
+
// Backslash line-continuations are merged into one logical line so a command
|
|
124
|
+
// wrapped across multiple lines can't slip past single-line regexes.
|
|
125
|
+
function codeLines(text, file) {
|
|
126
|
+
const isMarkdown = file.endsWith(".md");
|
|
127
|
+
const raw = text.split(/\r?\n/);
|
|
128
|
+
const out = [];
|
|
129
|
+
let inFence = false;
|
|
130
|
+
let fenceChar = "";
|
|
131
|
+
for (let i = 0; i < raw.length; i++) {
|
|
132
|
+
let line = raw[i];
|
|
133
|
+
if (isMarkdown) {
|
|
134
|
+
const fence = line.match(/^\s*(`{3,}|~{3,})/);
|
|
135
|
+
if (fence) {
|
|
136
|
+
const ch = fence[1][0];
|
|
137
|
+
if (!inFence) {
|
|
138
|
+
inFence = true;
|
|
139
|
+
fenceChar = ch;
|
|
140
|
+
} else if (ch === fenceChar) {
|
|
141
|
+
inFence = false;
|
|
142
|
+
}
|
|
143
|
+
continue; // never scan the fence marker line itself
|
|
144
|
+
}
|
|
145
|
+
if (!inFence) continue; // skip prose
|
|
146
|
+
}
|
|
147
|
+
const startLine = i + 1;
|
|
148
|
+
while (/\\[ \t]*$/.test(line) && i + 1 < raw.length) {
|
|
149
|
+
i += 1;
|
|
150
|
+
line = line.replace(/\\[ \t]*$/, " ") + raw[i];
|
|
151
|
+
}
|
|
152
|
+
out.push({ line: startLine, text: line });
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function scanFile(file) {
|
|
158
|
+
const rel = relative(ROOT, file).split(sep).join("/");
|
|
159
|
+
const text = readFileSync(file, "utf8");
|
|
160
|
+
|
|
161
|
+
// Frontmatter hygiene for SKILL.md
|
|
162
|
+
if (file.endsWith("SKILL.md")) {
|
|
163
|
+
const fmEnd = text.indexOf("\n---", 3);
|
|
164
|
+
const fm = text.startsWith("---") && fmEnd !== -1 ? text.slice(3, fmEnd) : "";
|
|
165
|
+
if (!/\bname\s*:/.test(fm))
|
|
166
|
+
findings.push({ severity: "MEDIUM", rule: "S100 missing-name", file: rel, line: 1, match: "frontmatter", why: "SKILL.md is missing a `name` in frontmatter." });
|
|
167
|
+
if (!/\bdescription\s*:/.test(fm))
|
|
168
|
+
findings.push({ severity: "MEDIUM", rule: "S101 missing-description", file: rel, line: 1, match: "frontmatter", why: "SKILL.md is missing a `description` in frontmatter." });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const push = (r, line, m) =>
|
|
172
|
+
findings.push({ severity: r.severity, rule: r.rule, file: rel, line, match: m[0].slice(0, 120), why: r.why });
|
|
173
|
+
|
|
174
|
+
// Command-style rules over code context (fenced blocks / whole scripts).
|
|
175
|
+
const cmdRules = RULES.filter((r) => r.context !== "prose");
|
|
176
|
+
for (const { line, text: lt } of codeLines(text, file)) {
|
|
177
|
+
for (const r of cmdRules) {
|
|
178
|
+
const m = r.re.exec(lt);
|
|
179
|
+
if (m) push(r, line, m);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Prose rules (e.g. prompt-injection) scan the full natural-language text.
|
|
184
|
+
const proseRules = RULES.filter((r) => r.context === "prose");
|
|
185
|
+
if (proseRules.length) {
|
|
186
|
+
text.split(/\r?\n/).forEach((line, i) => {
|
|
187
|
+
for (const r of proseRules) {
|
|
188
|
+
const m = r.re.exec(line);
|
|
189
|
+
if (m) push(r, i + 1, m);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Run
|
|
196
|
+
const files = SCAN_DIRS.flatMap((d) => walk(join(ROOT, d)));
|
|
197
|
+
files.forEach(scanFile);
|
|
198
|
+
|
|
199
|
+
const counts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
|
|
200
|
+
findings.forEach((f) => (counts[f.severity] += 1));
|
|
201
|
+
|
|
202
|
+
if (JSON_OUT) {
|
|
203
|
+
console.log(JSON.stringify({ scanned: files.length, counts, findings }, null, 2));
|
|
204
|
+
} else {
|
|
205
|
+
const C = { HIGH: "\x1b[31m", MEDIUM: "\x1b[33m", LOW: "\x1b[36m", reset: "\x1b[0m" };
|
|
206
|
+
console.log(`\nskills-safety-scan — scanned ${files.length} file(s) in ${SCAN_DIRS.join(", ")}\n`);
|
|
207
|
+
if (findings.length === 0) {
|
|
208
|
+
console.log(" ✓ No issues found.\n");
|
|
209
|
+
} else {
|
|
210
|
+
for (const f of findings.sort((a, b) => ("HML".indexOf(a.severity[0]) - "HML".indexOf(b.severity[0])))) {
|
|
211
|
+
console.log(` ${C[f.severity]}${f.severity}${C.reset} ${f.rule}`);
|
|
212
|
+
console.log(` ${f.file}:${f.line}`);
|
|
213
|
+
console.log(` match: ${f.match}`);
|
|
214
|
+
console.log(` why: ${f.why}\n`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
console.log(`Summary: ${counts.HIGH} high, ${counts.MEDIUM} medium, ${counts.LOW} low\n`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Scanning zero files almost always means the target moved (renamed skill dir,
|
|
221
|
+
// wrong --root) rather than a clean repo — fail loudly instead of passing green.
|
|
222
|
+
const emptyScan = files.length === 0 && !ALLOW_EMPTY;
|
|
223
|
+
if (emptyScan) {
|
|
224
|
+
console.error(
|
|
225
|
+
`skills-safety-scan: ERROR — scanned 0 files under ${SCAN_DIRS.join(", ")}. ` +
|
|
226
|
+
`The scan target may have moved. Pass --allow-empty if this is intentional.`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const fail = counts.HIGH > 0 || (STRICT && counts.MEDIUM > 0) || emptyScan;
|
|
231
|
+
process.exit(fail ? 1 : 0);
|