vibecodedshit 0.0.0-stage → 0.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/LICENSE +95 -0
- package/README.md +64 -2
- package/assets/fonts/NotoEmoji-OFL.txt +93 -0
- package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
- package/dist/bin.js +8 -0
- package/dist/card/previous.js +38 -0
- package/dist/card/render.js +71 -0
- package/dist/cli.js +121 -0
- package/dist/commands/card.js +69 -0
- package/dist/commands/common.js +1 -0
- package/dist/commands/diff.js +54 -0
- package/dist/commands/footer.js +25 -0
- package/dist/commands/report.js +49 -0
- package/dist/commands/route.js +62 -0
- package/dist/commands/submit.js +125 -0
- package/dist/commands/summary.js +77 -0
- package/dist/diff/compare.js +73 -0
- package/dist/diff/conditions.js +87 -0
- package/dist/diff/format.js +23 -0
- package/dist/exec/docker.js +158 -0
- package/dist/exec/findings.js +73 -0
- package/dist/exec/plan.js +50 -0
- package/dist/exec/reports.js +90 -0
- package/dist/exec/roots.js +49 -0
- package/dist/exec/run.js +182 -0
- package/dist/exec/steps/build.js +45 -0
- package/dist/exec/steps/coverage-install.js +12 -0
- package/dist/exec/steps/install.js +41 -0
- package/dist/exec/steps/knip.js +28 -0
- package/dist/exec/steps/lint.js +25 -0
- package/dist/exec/steps/lockfiles.js +25 -0
- package/dist/exec/steps/test.js +17 -0
- package/dist/exec/steps/typecheck.js +40 -0
- package/dist/exec/test-reports/coverage-py.js +23 -0
- package/dist/exec/test-reports/coverage-ranges.js +65 -0
- package/dist/exec/test-reports/coverage-summary.js +23 -0
- package/dist/exec/test-reports/jest-json.js +31 -0
- package/dist/exec/test-reports/junit.js +24 -0
- package/dist/exec/test-reports/mocha-json.js +18 -0
- package/dist/exec/test-reports/vitest-json.js +5 -0
- package/dist/exec/test-runners/detect.js +99 -0
- package/dist/exec/test-runners/index.js +6 -0
- package/dist/exec/test-runners/jest.js +27 -0
- package/dist/exec/test-runners/mocha.js +28 -0
- package/dist/exec/test-runners/node-test.js +24 -0
- package/dist/exec/test-runners/pytest.js +18 -0
- package/dist/exec/test-runners/script.js +48 -0
- package/dist/exec/test-runners/spec.js +1 -0
- package/dist/exec/test-runners/vitest.js +28 -0
- package/dist/exec/tests.js +120 -0
- package/dist/exec/types.js +1 -0
- package/dist/manifest/package-json.js +36 -0
- package/dist/manifest/python.js +106 -0
- package/dist/measure/d3/index.js +45 -0
- package/dist/measure/d3/presets.js +100 -0
- package/dist/measure/d3/py/adapter.js +67 -0
- package/dist/measure/d3/py/config.js +80 -0
- package/dist/measure/d3/py/contracts.js +57 -0
- package/dist/measure/d3/py/domain.js +57 -0
- package/dist/measure/d3/py/graph.js +214 -0
- package/dist/measure/d3/py/interpreter.js +31 -0
- package/dist/measure/d3/py/layers.js +120 -0
- package/dist/measure/d3/py/roots.js +58 -0
- package/dist/measure/d3/py/types.js +1 -0
- package/dist/measure/d3/scc.js +64 -0
- package/dist/measure/d3/ts/adapter.js +371 -0
- package/dist/measure/d3/ts/classify.js +107 -0
- package/dist/measure/d3/ts/config.js +318 -0
- package/dist/measure/d3/ts/domain.js +23 -0
- package/dist/measure/d3/ts/rules.js +65 -0
- package/dist/measure/d3/types.js +5 -0
- package/dist/measure/deps/cvss.js +66 -0
- package/dist/measure/deps/declared.js +195 -0
- package/dist/measure/deps/files.js +40 -0
- package/dist/measure/deps/lockfiles.js +308 -0
- package/dist/measure/deps/osv.js +122 -0
- package/dist/measure/deps/registry.js +142 -0
- package/dist/measure/deps/resolved.js +78 -0
- package/dist/measure/deps/units.js +5 -0
- package/dist/measure/eslint/health-plugin.js +19 -0
- package/dist/measure/ops/docs-commands.js +73 -0
- package/dist/measure/ops/docs-resolve.js +217 -0
- package/dist/measure/ops/entry-points.js +49 -0
- package/dist/measure/ops/env-example.js +20 -0
- package/dist/measure/ops/env-reads.js +46 -0
- package/dist/measure/ops/runtime.js +73 -0
- package/dist/measure/p1-1.js +8 -0
- package/dist/measure/p1-2.js +8 -0
- package/dist/measure/p1-3.js +8 -0
- package/dist/measure/p1-4.js +41 -0
- package/dist/measure/p1-5.js +59 -0
- package/dist/measure/p2-1.js +8 -0
- package/dist/measure/p2-2.js +44 -0
- package/dist/measure/p2-3.js +81 -0
- package/dist/measure/p2-4.js +288 -0
- package/dist/measure/p2-5.js +8 -0
- package/dist/measure/p3-1.js +16 -0
- package/dist/measure/p3-2.js +26 -0
- package/dist/measure/p3-3.js +37 -0
- package/dist/measure/p3-4.js +26 -0
- package/dist/measure/p3-5.js +26 -0
- package/dist/measure/p4-1.js +184 -0
- package/dist/measure/p4-2.js +27 -0
- package/dist/measure/p4-3.js +106 -0
- package/dist/measure/p4-4.js +309 -0
- package/dist/measure/p5-1.js +117 -0
- package/dist/measure/p5-2.js +80 -0
- package/dist/measure/p5-3.js +21 -0
- package/dist/measure/p5-4.js +161 -0
- package/dist/measure/p5-5.js +158 -0
- package/dist/measure/p6-1.js +20 -0
- package/dist/measure/p6-2.js +20 -0
- package/dist/measure/p6-3.js +40 -0
- package/dist/measure/p6-4.js +31 -0
- package/dist/measure/p6-5.js +60 -0
- package/dist/measure/p6-6.js +54 -0
- package/dist/measure/py/typecheck-config.js +95 -0
- package/dist/measure/registry.js +31 -0
- package/dist/measure/safety/py-tests.js +124 -0
- package/dist/measure/safety/ts-barrels.js +68 -0
- package/dist/measure/safety/ts-cases.js +111 -0
- package/dist/measure/safety/ts-credits.js +101 -0
- package/dist/measure/safety/ts-imports.js +63 -0
- package/dist/measure/safety/ts-resolve.js +43 -0
- package/dist/measure/ts/directives.js +37 -0
- package/dist/measure/ts/exports.js +60 -0
- package/dist/measure/ts/tsconfig.js +179 -0
- package/dist/measure/types.js +1 -0
- package/dist/measure/units.js +41 -0
- package/dist/methodology/load.js +159 -0
- package/dist/methodology/schema.js +105 -0
- package/dist/python/deadcode.js +27 -0
- package/dist/python/env.js +13 -0
- package/dist/python/helper.js +24 -0
- package/dist/python/imports.js +40 -0
- package/dist/python/nox.js +16 -0
- package/dist/python/scope.js +35 -0
- package/dist/python/tests.js +19 -0
- package/dist/python/typing.js +27 -0
- package/dist/report/escape.js +5 -0
- package/dist/report/format.js +75 -0
- package/dist/report/render.js +213 -0
- package/dist/report/roadmap.js +58 -0
- package/dist/report/scares.js +83 -0
- package/dist/report/style.js +27 -0
- package/dist/report-only/hotspots.js +48 -0
- package/dist/route/check.js +37 -0
- package/dist/route/files.js +74 -0
- package/dist/route/order.js +22 -0
- package/dist/route/select.js +114 -0
- package/dist/route/write.js +13 -0
- package/dist/run/read.js +89 -0
- package/dist/run/recompute.js +19 -0
- package/dist/run/scan.js +196 -0
- package/dist/run/schema.js +1 -0
- package/dist/run/score-run.js +42 -0
- package/dist/run/write.js +4 -0
- package/dist/scope/build.js +173 -0
- package/dist/scope/packages.js +87 -0
- package/dist/scope/rules.js +21 -0
- package/dist/scope/tracked.js +34 -0
- package/dist/scope/ts-lines.js +63 -0
- package/dist/scope/types.js +7 -0
- package/dist/scoring/aggregate.js +38 -0
- package/dist/scoring/pool.js +65 -0
- package/dist/scoring/score.js +81 -0
- package/dist/scoring/what-if.js +38 -0
- package/dist/scripts/checks.js +53 -0
- package/dist/scripts/ci.js +139 -0
- package/dist/scripts/entry-points/find.js +31 -0
- package/dist/scripts/entry-points/index.js +18 -0
- package/dist/scripts/entry-points/justfile.js +62 -0
- package/dist/scripts/entry-points/makefile.js +66 -0
- package/dist/scripts/entry-points/nox.js +29 -0
- package/dist/scripts/entry-points/package-json.js +21 -0
- package/dist/scripts/entry-points/pyproject.js +73 -0
- package/dist/scripts/entry-points/tox.js +130 -0
- package/dist/scripts/expand.js +300 -0
- package/dist/scripts/triggers.js +151 -0
- package/dist/scripts/types.js +1 -0
- package/dist/submit/constants.js +9 -0
- package/dist/submit/payload.js +42 -0
- package/dist/submit/repo-identity.js +46 -0
- package/dist/submit/send.js +29 -0
- package/dist/tools/catalog.js +46 -0
- package/dist/tools/image.js +180 -0
- package/dist/tools/kit.js +71 -0
- package/dist/tools/knip.js +169 -0
- package/dist/tools/outcome.js +21 -0
- package/dist/tools/resolve.js +66 -0
- package/dist/tools/workcopy.js +27 -0
- package/dist/util/ini.js +37 -0
- package/dist/util/pep440.js +163 -0
- package/dist/util/toml.js +27 -0
- package/docker/Dockerfile +142 -0
- package/docker/bin/health-capture +5 -0
- package/docker/bin/health-coverage-install +5 -0
- package/docker/bin/health-eslint +66 -0
- package/docker/bin/health-pytest +10 -0
- package/docker/bin/health-tsc +10 -0
- package/docker/bin/health-venv +11 -0
- package/docker/config/eslint.config.mjs +4 -0
- package/docker/config/eslint.rules.mjs +65 -0
- package/docker/config/eslint.untyped.config.mjs +4 -0
- package/docker/config/knip.json +1 -0
- package/docker/config/ruff.toml +7 -0
- package/methodology/LICENSE +396 -0
- package/methodology/v1.0/METHODOLOGY.md +1176 -0
- package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
- package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
- package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
- package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
- package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
- package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
- package/methodology/v1.0/methodology.yaml +242 -0
- package/methodology/v1.0/presets/django.yaml +43 -0
- package/methodology/v1.0/presets/express.yaml +47 -0
- package/methodology/v1.0/presets/fastapi.yaml +52 -0
- package/methodology/v1.0/presets/generic.yaml +41 -0
- package/methodology/v1.0/presets/nextjs.yaml +50 -0
- package/methodology/v1.0/scope.yaml +115 -0
- package/package.json +61 -4
- package/python/dead_code_helper.py +265 -0
- package/python/env_helper.py +71 -0
- package/python/graph_helper.py +277 -0
- package/python/imports_helper.py +132 -0
- package/python/nox_helper.py +79 -0
- package/python/scope_helper.py +96 -0
- package/python/tests_helper.py +171 -0
- package/python/typing_helper.py +235 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { parseTomlText } from '../../util/toml.js';
|
|
2
|
+
import { findFile } from './find.js';
|
|
3
|
+
const isObj = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
4
|
+
const cmdText = (value) => (Array.isArray(value) ? [strs(value).join(' ')] : strs(value));
|
|
5
|
+
const strs = (value) => (Array.isArray(value) ? value.filter((v) => typeof v === 'string') : typeof value === 'string' ? [value] : []);
|
|
6
|
+
const sub = (value, ...keys) => {
|
|
7
|
+
let current = value;
|
|
8
|
+
for (const key of keys)
|
|
9
|
+
current = isObj(current) ? current[key] : undefined;
|
|
10
|
+
return isObj(current) ? current : {};
|
|
11
|
+
};
|
|
12
|
+
function pdm(doc) {
|
|
13
|
+
return Object.entries(sub(doc, 'tool', 'pdm', 'scripts')).map(([name, value]) => {
|
|
14
|
+
const bodies = typeof value === 'string' ? [value] : isObj(value) ? [...cmdText(value.cmd), ...strs(value.shell), ...strs(value.composite).map((c) => `pdm run ${c}`)] : [];
|
|
15
|
+
return { key: `pdm:${name}`, invoke: `pdm run ${name}`, bodies };
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function hatch(doc) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const [env, table] of Object.entries(sub(doc, 'tool', 'hatch', 'envs'))) {
|
|
21
|
+
const scripts = isObj(table) && isObj(table.scripts) ? table.scripts : {};
|
|
22
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
23
|
+
const bodies = strs(value).map((c) => (Object.hasOwn(scripts, c.trim().split(/\s+/)[0] ?? '') ? `hatch run ${env}:${c.trim().split(/\s+/)[0]}` : c));
|
|
24
|
+
out.push({ key: `hatch:${env}:${name}`, invoke: `hatch run ${env}:${name}`, bodies });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function rye(doc) {
|
|
30
|
+
return Object.entries(sub(doc, 'tool', 'rye', 'scripts')).map(([name, value]) => {
|
|
31
|
+
const bodies = typeof value === 'string' ? [value] : isObj(value) ? [...cmdText(value.cmd), ...strs(value.shell), ...strs(value.chain).map((c) => `rye run ${c}`)] : [];
|
|
32
|
+
return { key: `rye:${name}`, invoke: `rye run ${name}`, bodies };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function poe(doc) {
|
|
36
|
+
const tasks = sub(doc, 'tool', 'poe', 'tasks');
|
|
37
|
+
const items = (list) => (Array.isArray(list) ? list : []).flatMap((item) => (typeof item === 'string' ? [Object.hasOwn(tasks, item.trim().split(/\s+/)[0] ?? '') ? `poe ${item}` : item] : isObj(item) ? [...cmdText(item.cmd), ...strs(item.shell)] : []));
|
|
38
|
+
return Object.entries(tasks).map(([name, value]) => {
|
|
39
|
+
const bodies = typeof value === 'string' ? [value] : isObj(value) ? [...cmdText(value.cmd), ...strs(value.shell), ...items(value.sequence), ...items(value.composite)] : [];
|
|
40
|
+
return { key: `poe:${name}`, invoke: `poe ${name}`, bodies };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function taskipy(doc) {
|
|
44
|
+
return Object.entries(sub(doc, 'tool', 'taskipy', 'tasks')).map(([name, value]) => ({
|
|
45
|
+
key: `taskipy:${name}`,
|
|
46
|
+
invoke: `task ${name}`,
|
|
47
|
+
bodies: typeof value === 'string' ? [value] : isObj(value) ? strs(value.cmd) : [],
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
/** `[project.scripts]` and `[tool.poetry.scripts]` targets are `module:callable`: the entry exists and runs no recognised tool. */
|
|
51
|
+
function scripts(doc) {
|
|
52
|
+
const names = [...Object.keys(sub(doc, 'project', 'scripts')), ...Object.keys(sub(doc, 'tool', 'poetry', 'scripts'))];
|
|
53
|
+
return [...new Set(names)].map((name) => ({ key: `scripts:${name}`, invoke: `uv run ${name}`, bodies: [] }));
|
|
54
|
+
}
|
|
55
|
+
function entries(repoPath, dir) {
|
|
56
|
+
const found = findFile(repoPath, dir, ['pyproject.toml'], false);
|
|
57
|
+
const doc = found ? parseTomlText(found.text) : undefined;
|
|
58
|
+
if (!found || !doc)
|
|
59
|
+
return undefined;
|
|
60
|
+
return { file: found.file, entries: [...scripts(doc), ...pdm(doc), ...hatch(doc), ...rye(doc), ...poe(doc), ...taskipy(doc)] };
|
|
61
|
+
}
|
|
62
|
+
/** Script tables of the `pyproject.toml` in `dir`. Lookup names are the namespaced keys (`pdm:lint`, `hatch:default:test`). */
|
|
63
|
+
export const pyprojectEntryPoints = {
|
|
64
|
+
id: 'pyproject',
|
|
65
|
+
list(repoPath, dir) {
|
|
66
|
+
return (entries(repoPath, dir)?.entries ?? []).map((e) => ({ name: e.key, invoke: e.invoke }));
|
|
67
|
+
},
|
|
68
|
+
lookup(repoPath, dir, name) {
|
|
69
|
+
const read = entries(repoPath, dir);
|
|
70
|
+
const entry = read?.entries.find((e) => e.key === name);
|
|
71
|
+
return read && entry ? { file: read.file, bodies: entry.bodies } : undefined;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { parseIni } from '../../util/ini.js';
|
|
2
|
+
import { parseTomlText } from '../../util/toml.js';
|
|
3
|
+
import { findFile } from './find.js';
|
|
4
|
+
const isObj = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
/** `py{311,312}-lint` becomes `py311-lint` and `py312-lint`. */
|
|
6
|
+
function expandBraces(text) {
|
|
7
|
+
const match = /^(.*?)\{([^{}]*)\}(.*)$/.exec(text);
|
|
8
|
+
if (!match)
|
|
9
|
+
return [text];
|
|
10
|
+
const [, head = '', set = '', tail = ''] = match;
|
|
11
|
+
return set.split(',').flatMap((item) => expandBraces(`${head}${item.trim()}${tail}`));
|
|
12
|
+
}
|
|
13
|
+
function lines(value) {
|
|
14
|
+
return (value ?? '')
|
|
15
|
+
.split('\n')
|
|
16
|
+
.map((l) => l.trim())
|
|
17
|
+
.filter((l) => l !== '' && !l.startsWith('#'));
|
|
18
|
+
}
|
|
19
|
+
/** Split on commas and newlines outside braces, then expand brace sets. */
|
|
20
|
+
function envList(value) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
let depth = 0;
|
|
23
|
+
let current = '';
|
|
24
|
+
for (const ch of value ?? '') {
|
|
25
|
+
if (ch === '{')
|
|
26
|
+
depth += 1;
|
|
27
|
+
else if (ch === '}')
|
|
28
|
+
depth -= 1;
|
|
29
|
+
if ((ch === ',' || ch === '\n') && depth <= 0) {
|
|
30
|
+
parts.push(current);
|
|
31
|
+
current = '';
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
current += ch;
|
|
35
|
+
}
|
|
36
|
+
parts.push(current);
|
|
37
|
+
return parts.map((e) => e.trim()).filter((e) => e !== '').flatMap(expandBraces);
|
|
38
|
+
}
|
|
39
|
+
function fromIni(file, text) {
|
|
40
|
+
const ini = parseIni(text);
|
|
41
|
+
const section = (name) => {
|
|
42
|
+
const s = ini[name] ?? {};
|
|
43
|
+
return { commandsPre: lines(s.commands_pre), commands: lines(s.commands), commandsPost: lines(s.commands_post) };
|
|
44
|
+
};
|
|
45
|
+
const envs = new Map();
|
|
46
|
+
for (const name of Object.keys(ini))
|
|
47
|
+
if (name.startsWith('testenv:'))
|
|
48
|
+
envs.set(name.slice('testenv:'.length), section(name));
|
|
49
|
+
return { file, envlist: envList(ini.tox?.envlist), base: section('testenv'), envs };
|
|
50
|
+
}
|
|
51
|
+
function commandText(value) {
|
|
52
|
+
if (typeof value === 'string')
|
|
53
|
+
return [value];
|
|
54
|
+
if (Array.isArray(value))
|
|
55
|
+
return value.map((v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string').join(' ') : typeof v === 'string' ? v : '')).filter((v) => v !== '');
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
function fromToml(file, doc) {
|
|
59
|
+
const table = file.endsWith('pyproject.toml') ? (isObj(doc.tool) && isObj(doc.tool.tox) ? doc.tool.tox : undefined) : doc;
|
|
60
|
+
if (!table)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (typeof table.legacy_tox_ini === 'string')
|
|
63
|
+
return fromIni(file, table.legacy_tox_ini);
|
|
64
|
+
const base = { commands: commandText(isObj(table.env_run_base) ? table.env_run_base.commands : undefined) };
|
|
65
|
+
const envs = new Map();
|
|
66
|
+
const rawEnvs = isObj(table.env) ? table.env : {};
|
|
67
|
+
for (const [name, value] of Object.entries(rawEnvs))
|
|
68
|
+
if (isObj(value))
|
|
69
|
+
envs.set(name, { commands: commandText(value.commands) });
|
|
70
|
+
const envlist = Array.isArray(table.env_list) ? table.env_list.filter((e) => typeof e === 'string').flatMap(expandBraces) : [];
|
|
71
|
+
return { file, envlist, base, envs };
|
|
72
|
+
}
|
|
73
|
+
function readToxConfig(repoPath, dir) {
|
|
74
|
+
const ini = findFile(repoPath, dir, ['tox.ini'], false);
|
|
75
|
+
if (ini)
|
|
76
|
+
return fromIni(ini.file, ini.text);
|
|
77
|
+
for (const name of ['tox.toml', 'pyproject.toml']) {
|
|
78
|
+
const found = findFile(repoPath, dir, [name], false);
|
|
79
|
+
const doc = found ? parseTomlText(found.text) : undefined;
|
|
80
|
+
const config = found && doc ? fromToml(found.file, doc) : undefined;
|
|
81
|
+
if (config)
|
|
82
|
+
return config;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/** `{envpython} -m` becomes `python -m`; `{posargs}`, `{toxinidir}` and other `{...}` fields are dropped. */
|
|
87
|
+
function substitute(command, factors) {
|
|
88
|
+
let text = command.trim();
|
|
89
|
+
const factor = /^([A-Za-z0-9_.!,-]+):\s+(.*)$/.exec(text);
|
|
90
|
+
if (factor?.[1] && factor[2] !== undefined) {
|
|
91
|
+
const wanted = factor[1].split(',').some((f) => (f.startsWith('!') ? !factors.includes(f.slice(1)) : factors.includes(f)));
|
|
92
|
+
if (!wanted)
|
|
93
|
+
return undefined;
|
|
94
|
+
text = factor[2];
|
|
95
|
+
}
|
|
96
|
+
text = text.replace(/\{envpython\}/g, 'python').replace(/\{[^{}]*\}/g, '').replace(/^[-!]+\s*/, '').replace(/\s+/g, ' ').trim();
|
|
97
|
+
return text === '' ? undefined : text;
|
|
98
|
+
}
|
|
99
|
+
function envBodies(config, name) {
|
|
100
|
+
const factors = name.split('-');
|
|
101
|
+
const section = config.envs.get(name) ?? factors.map((f) => config.envs.get(f)).find((e) => e !== undefined);
|
|
102
|
+
const known = section !== undefined || config.envlist.includes(name) || name === '@base';
|
|
103
|
+
if (!known)
|
|
104
|
+
return undefined;
|
|
105
|
+
const pick = (key) => section?.[key] ?? config.base[key] ?? [];
|
|
106
|
+
return [...pick('commandsPre'), ...pick('commands'), ...pick('commandsPost')].flatMap((c) => substitute(c, factors) ?? []);
|
|
107
|
+
}
|
|
108
|
+
/** `tox.ini`, `tox.toml` or `[tool.tox]` in `dir`. A call is `tox [-e <envs>]`. */
|
|
109
|
+
export const toxEntryPoints = {
|
|
110
|
+
id: 'tox',
|
|
111
|
+
fileFor: (repoPath, dir) => readToxConfig(repoPath, dir)?.file,
|
|
112
|
+
list(repoPath, dir) {
|
|
113
|
+
const config = readToxConfig(repoPath, dir);
|
|
114
|
+
if (!config)
|
|
115
|
+
return [];
|
|
116
|
+
const names = config.envlist.length > 0 ? config.envlist : [...config.envs.keys()];
|
|
117
|
+
return [...new Set(names)].map((name) => ({ name, invoke: `tox -e ${name}` }));
|
|
118
|
+
},
|
|
119
|
+
lookup(repoPath, dir, name) {
|
|
120
|
+
const config = readToxConfig(repoPath, dir);
|
|
121
|
+
if (!config)
|
|
122
|
+
return undefined;
|
|
123
|
+
if (name === '@default') {
|
|
124
|
+
const names = config.envlist.length > 0 ? config.envlist : ['@base'];
|
|
125
|
+
return { file: config.file, bodies: names.flatMap((n) => envBodies(config, n) ?? []) };
|
|
126
|
+
}
|
|
127
|
+
const bodies = envBodies(config, name);
|
|
128
|
+
return bodies === undefined ? undefined : { file: config.file, bodies };
|
|
129
|
+
},
|
|
130
|
+
};
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ciCommands } from './ci.js';
|
|
3
|
+
import { entryPointKind } from './entry-points/index.js';
|
|
4
|
+
const SKIPPED_WORDS = new Set(['sudo', 'time', 'exec', 'env', 'command', 'nohup']);
|
|
5
|
+
export const RUNNER_PREFIXES = [
|
|
6
|
+
['pnpm', 'exec'], ['pnpm', 'dlx'], ['yarn', 'exec'], ['yarn', 'dlx'], ['uv', 'run'], ['poetry', 'run'], ['pdm', 'run'],
|
|
7
|
+
['hatch', 'run'], ['pipx', 'run'], ['python', '-m'], ['python3', '-m'], ['npx'], ['bunx'], ['yarn'], ['pnpm'],
|
|
8
|
+
];
|
|
9
|
+
/** Split on newline, `&&`, `||`, `;` and `|` outside quotes. */
|
|
10
|
+
export function splitSimple(text) {
|
|
11
|
+
const out = [];
|
|
12
|
+
let current = '';
|
|
13
|
+
let quote = null;
|
|
14
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
15
|
+
const ch = text[i];
|
|
16
|
+
if (quote !== null) {
|
|
17
|
+
if (ch === quote)
|
|
18
|
+
quote = null;
|
|
19
|
+
current += ch;
|
|
20
|
+
}
|
|
21
|
+
else if (ch === '"' || ch === "'") {
|
|
22
|
+
quote = ch;
|
|
23
|
+
current += ch;
|
|
24
|
+
}
|
|
25
|
+
else if (ch === '\\' && text[i + 1] === '\n') {
|
|
26
|
+
i += 1;
|
|
27
|
+
}
|
|
28
|
+
else if ((ch === '&' && text[i + 1] === '&') || (ch === '|' && text[i + 1] === '|')) {
|
|
29
|
+
out.push({ text: current, sep: ch + ch });
|
|
30
|
+
current = '';
|
|
31
|
+
i += 1;
|
|
32
|
+
}
|
|
33
|
+
else if (ch === ';' || ch === '|' || ch === '\n') {
|
|
34
|
+
out.push({ text: current, sep: ch });
|
|
35
|
+
current = '';
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
current += ch;
|
|
39
|
+
}
|
|
40
|
+
out.push({ text: current, sep: '' });
|
|
41
|
+
return out.filter((s) => s.text.trim() !== '');
|
|
42
|
+
}
|
|
43
|
+
export function tokenize(text) {
|
|
44
|
+
const tokens = [];
|
|
45
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
46
|
+
for (let m = re.exec(text); m !== null; m = re.exec(text))
|
|
47
|
+
tokens.push(m[1] ?? m[2] ?? m[3] ?? '');
|
|
48
|
+
return tokens;
|
|
49
|
+
}
|
|
50
|
+
function stripWrappers(tokens) {
|
|
51
|
+
let rest = tokens;
|
|
52
|
+
for (;;) {
|
|
53
|
+
const first = rest[0];
|
|
54
|
+
if (first === undefined)
|
|
55
|
+
return rest;
|
|
56
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(first) || SKIPPED_WORDS.has(first)) {
|
|
57
|
+
rest = rest.slice(1);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
return rest;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function stripRunners(tokens) {
|
|
64
|
+
let rest = tokens;
|
|
65
|
+
for (;;) {
|
|
66
|
+
const prefix = RUNNER_PREFIXES.find((p) => p.every((word, i) => rest[i] === word));
|
|
67
|
+
if (!prefix)
|
|
68
|
+
return rest;
|
|
69
|
+
rest = rest.slice(prefix.length);
|
|
70
|
+
while (rest[0]?.startsWith('-'))
|
|
71
|
+
rest = rest.slice(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Flags that take a value, for the runners whose first plain argument is the script or target name. */
|
|
75
|
+
const VALUE_FLAGS = new Set(['--prefix', '-C', '--cwd', '--filter', '-F', '--workspace', '--directory']);
|
|
76
|
+
function joinDir(dir, next) {
|
|
77
|
+
return next === undefined ? dir : path.posix.normalize(path.posix.join(dir, next));
|
|
78
|
+
}
|
|
79
|
+
function detectPackageManager(base, tokens, dir) {
|
|
80
|
+
let callDir = dir;
|
|
81
|
+
const args = [];
|
|
82
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
83
|
+
const token = tokens[i];
|
|
84
|
+
if (token === '--prefix' || token === '-C' || token === '--cwd') {
|
|
85
|
+
callDir = joinDir(dir, tokens[i + 1]);
|
|
86
|
+
i += 1;
|
|
87
|
+
}
|
|
88
|
+
else if (token.startsWith('--prefix='))
|
|
89
|
+
callDir = joinDir(dir, token.slice(9));
|
|
90
|
+
else if (VALUE_FLAGS.has(token))
|
|
91
|
+
i += 1;
|
|
92
|
+
else if (token.startsWith('-'))
|
|
93
|
+
continue;
|
|
94
|
+
else
|
|
95
|
+
args.push(token);
|
|
96
|
+
}
|
|
97
|
+
let name;
|
|
98
|
+
let fallback = false;
|
|
99
|
+
if (base === 'npm') {
|
|
100
|
+
if (args[0] === 'run' || args[0] === 'run-script')
|
|
101
|
+
name = args[1];
|
|
102
|
+
else if (args[0] === 'test' || args[0] === 'start')
|
|
103
|
+
name = args[0];
|
|
104
|
+
}
|
|
105
|
+
else if (base === 'bun') {
|
|
106
|
+
if (args[0] === 'run')
|
|
107
|
+
name = args[1];
|
|
108
|
+
}
|
|
109
|
+
else if (args[0] === 'run')
|
|
110
|
+
name = args[1];
|
|
111
|
+
else if (args[0] !== undefined && !(base === 'pnpm' && (args[0] === 'exec' || args[0] === 'dlx')) && !(base === 'yarn' && (args[0] === 'exec' || args[0] === 'dlx'))) {
|
|
112
|
+
name = args[0];
|
|
113
|
+
fallback = true;
|
|
114
|
+
}
|
|
115
|
+
if (name === undefined)
|
|
116
|
+
return undefined;
|
|
117
|
+
return { runner: base, kind: 'package-json', names: [name], dir: callDir, prePost: base === 'npm' || base === 'yarn', fallback };
|
|
118
|
+
}
|
|
119
|
+
function detectMake(base, tokens, dir) {
|
|
120
|
+
let callDir = dir;
|
|
121
|
+
const targets = [];
|
|
122
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
123
|
+
const token = tokens[i];
|
|
124
|
+
if (token === '-C' || token === '--directory') {
|
|
125
|
+
callDir = joinDir(dir, tokens[i + 1]);
|
|
126
|
+
i += 1;
|
|
127
|
+
}
|
|
128
|
+
else if (token === '-f' || token === '--file')
|
|
129
|
+
i += 1;
|
|
130
|
+
else if (token.startsWith('-') || token.includes('='))
|
|
131
|
+
continue;
|
|
132
|
+
else
|
|
133
|
+
targets.push(token);
|
|
134
|
+
}
|
|
135
|
+
if (targets.length === 0)
|
|
136
|
+
return undefined;
|
|
137
|
+
return { runner: base, kind: 'makefile', names: targets, dir: callDir, prePost: false, fallback: false };
|
|
138
|
+
}
|
|
139
|
+
function detectJust(tokens, dir) {
|
|
140
|
+
let callDir = dir;
|
|
141
|
+
const names = [];
|
|
142
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
143
|
+
const token = tokens[i];
|
|
144
|
+
if (token === '-d' || token === '--working-directory') {
|
|
145
|
+
callDir = joinDir(dir, tokens[i + 1]);
|
|
146
|
+
i += 1;
|
|
147
|
+
}
|
|
148
|
+
else if (token === '-f' || token === '--justfile')
|
|
149
|
+
i += 1;
|
|
150
|
+
else if (token.startsWith('--justfile=') || token.startsWith('--working-directory='))
|
|
151
|
+
continue;
|
|
152
|
+
else if (token.startsWith('-') || token.includes('='))
|
|
153
|
+
continue;
|
|
154
|
+
else
|
|
155
|
+
names.push(token);
|
|
156
|
+
}
|
|
157
|
+
return { runner: 'just', kind: 'justfile', names: names.length === 0 ? ['@default'] : names, dir: callDir, prePost: false, fallback: false };
|
|
158
|
+
}
|
|
159
|
+
function detectTox(tokens, dir) {
|
|
160
|
+
const names = [];
|
|
161
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
162
|
+
const token = tokens[i];
|
|
163
|
+
if (token === '--')
|
|
164
|
+
break;
|
|
165
|
+
if (token === '-e') {
|
|
166
|
+
names.push(...(tokens[i + 1] ?? '').split(','));
|
|
167
|
+
i += 1;
|
|
168
|
+
}
|
|
169
|
+
else if (token.startsWith('-e') && token.length > 2)
|
|
170
|
+
names.push(...token.slice(2).split(','));
|
|
171
|
+
}
|
|
172
|
+
return { runner: 'tox', kind: 'tox', names: names.length === 0 ? ['@default'] : names.filter((n) => n !== ''), dir, prePost: false, fallback: false };
|
|
173
|
+
}
|
|
174
|
+
function detectNox(tokens, dir) {
|
|
175
|
+
const names = [];
|
|
176
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
177
|
+
const token = tokens[i];
|
|
178
|
+
if (token === '--')
|
|
179
|
+
break;
|
|
180
|
+
if (token === '-s' || token === '--session' || token === '-e' || token === '--sessions') {
|
|
181
|
+
while (tokens[i + 1] !== undefined && !tokens[i + 1].startsWith('-')) {
|
|
182
|
+
names.push(...tokens[i + 1].split(','));
|
|
183
|
+
i += 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { runner: 'nox', kind: 'nox', names: names.length === 0 ? ['@all'] : names.filter((n) => n !== ''), dir, prePost: false, fallback: false };
|
|
188
|
+
}
|
|
189
|
+
/** `uv run <n>`, `poetry run <n>`, `pdm run <n>`, `hatch run [<env>:]<n>`, `rye run <n>`, `poe <n>` and `task <n>` name a script of a pyproject table. */
|
|
190
|
+
function detectPyproject(tokens, dir) {
|
|
191
|
+
const [first, second] = tokens;
|
|
192
|
+
const rest = (from) => tokens.slice(from).find((t) => !t.startsWith('-'));
|
|
193
|
+
const make = (runner, key, name, fallback) => name === undefined ? undefined : { runner, kind: 'pyproject', names: [key(name)], dir, prePost: false, fallback };
|
|
194
|
+
if ((first === 'uv' || first === 'poetry') && second === 'run')
|
|
195
|
+
return make(first, (n) => `scripts:${n}`, rest(2), true);
|
|
196
|
+
if (first === 'pdm' && second === 'run')
|
|
197
|
+
return make(first, (n) => `pdm:${n}`, rest(2), true);
|
|
198
|
+
if (first === 'hatch' && second === 'run')
|
|
199
|
+
return make(first, (n) => (n.includes(':') ? `hatch:${n}` : `hatch:default:${n}`), rest(2), true);
|
|
200
|
+
if (first === 'rye' && second === 'run')
|
|
201
|
+
return make(first, (n) => `rye:${n}`, rest(2), false);
|
|
202
|
+
if (first === 'poe')
|
|
203
|
+
return make(first, (n) => `poe:${n}`, rest(1), false);
|
|
204
|
+
if (first === 'task')
|
|
205
|
+
return make(first, (n) => `taskipy:${n}`, rest(1), false);
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
function detectCall(tokens, dir) {
|
|
209
|
+
const first = tokens[0];
|
|
210
|
+
if (first === undefined)
|
|
211
|
+
return undefined;
|
|
212
|
+
const base = path.posix.basename(first);
|
|
213
|
+
if (base === 'npm' || base === 'yarn' || base === 'pnpm' || base === 'bun')
|
|
214
|
+
return detectPackageManager(base, tokens, dir);
|
|
215
|
+
if (base === 'make' || first === '$(MAKE)' || first === '${MAKE}')
|
|
216
|
+
return detectMake('make', tokens, dir);
|
|
217
|
+
if (base === 'just')
|
|
218
|
+
return detectJust(tokens, dir);
|
|
219
|
+
if (base === 'tox')
|
|
220
|
+
return detectTox(tokens, dir);
|
|
221
|
+
if (base === 'nox')
|
|
222
|
+
return detectNox(tokens, dir);
|
|
223
|
+
return detectPyproject(tokens, dir);
|
|
224
|
+
}
|
|
225
|
+
function expandText(repoPath, text, frame, maxDepth, out) {
|
|
226
|
+
let dir = frame.dir;
|
|
227
|
+
for (const simple of splitSimple(text)) {
|
|
228
|
+
const tokens = stripWrappers(tokenize(simple.text));
|
|
229
|
+
if (tokens.length === 0)
|
|
230
|
+
continue;
|
|
231
|
+
if (tokens[0] === 'cd' && tokens[1] !== undefined && !/[$`*?]/.test(tokens[1])) {
|
|
232
|
+
if (simple.sep === '&&')
|
|
233
|
+
dir = path.posix.normalize(path.posix.join(dir, tokens[1]));
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const call = detectCall(tokens, dir);
|
|
237
|
+
if (call && follow(repoPath, call, tokens, frame, maxDepth, out))
|
|
238
|
+
continue;
|
|
239
|
+
const stripped = stripRunners(tokens);
|
|
240
|
+
const [tool, ...argv] = stripped;
|
|
241
|
+
out.push({ tool: tool === undefined ? null : path.posix.basename(tool), argv, file: frame.file, chain: frame.chain, depth: frame.chain.length });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Replace a call by what it runs. False when the caller should read the command another way (runner prefix removed). */
|
|
245
|
+
function follow(repoPath, call, tokens, frame, maxDepth, out) {
|
|
246
|
+
const kind = entryPointKind(call.kind);
|
|
247
|
+
const plain = () => {
|
|
248
|
+
if (call.fallback && frame.chain.length < maxDepth)
|
|
249
|
+
return false;
|
|
250
|
+
const readable = kind?.fileFor === undefined ? undefined : kind.fileFor(repoPath, call.dir) !== undefined;
|
|
251
|
+
out.push({
|
|
252
|
+
tool: call.runner,
|
|
253
|
+
argv: tokens.slice(1),
|
|
254
|
+
file: frame.file,
|
|
255
|
+
chain: frame.chain,
|
|
256
|
+
depth: frame.chain.length,
|
|
257
|
+
...(readable === undefined ? {} : { readable }),
|
|
258
|
+
});
|
|
259
|
+
return true;
|
|
260
|
+
};
|
|
261
|
+
if (!kind || frame.chain.length >= maxDepth)
|
|
262
|
+
return plain();
|
|
263
|
+
let followed = false;
|
|
264
|
+
for (const name of call.names) {
|
|
265
|
+
const variants = call.prePost ? [`pre${name}`, name, `post${name}`] : [name];
|
|
266
|
+
for (const variant of variants) {
|
|
267
|
+
const found = kind.lookup(repoPath, call.dir, variant);
|
|
268
|
+
const key = found ? `${found.file}:${variant}` : '';
|
|
269
|
+
if (!found || frame.seen.has(key))
|
|
270
|
+
continue;
|
|
271
|
+
followed = true;
|
|
272
|
+
const next = {
|
|
273
|
+
dir: path.posix.dirname(found.file),
|
|
274
|
+
file: found.file,
|
|
275
|
+
chain: [...frame.chain, key],
|
|
276
|
+
seen: new Set([...frame.seen, key]),
|
|
277
|
+
};
|
|
278
|
+
for (const body of found.bodies)
|
|
279
|
+
expandText(repoPath, body, next, maxDepth, out);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return followed || plain();
|
|
283
|
+
}
|
|
284
|
+
/** The commands a text runs, calls to scripts and make targets replaced by what they run (up to `maxDepth` bodies deep, default 3). */
|
|
285
|
+
export function expandCommand(repoPath, text, from, opts = {}) {
|
|
286
|
+
const out = [];
|
|
287
|
+
expandText(repoPath, text, { dir: from.dir, file: from.file, chain: [], seen: new Set() }, opts.maxDepth ?? 3, out);
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
/** Every CI command whose expansion reaches one of `tools`. */
|
|
291
|
+
export function ciInvokes(repoPath, tools, opts = {}) {
|
|
292
|
+
const hits = [];
|
|
293
|
+
for (const command of ciCommands(repoPath, opts.warnings)) {
|
|
294
|
+
const resolved = expandCommand(repoPath, command.command, { dir: command.dir, file: command.file }, opts);
|
|
295
|
+
const match = resolved.find((r) => r.tool !== null && tools.includes(r.tool));
|
|
296
|
+
if (match)
|
|
297
|
+
hits.push({ file: command.file, job: command.job, chain: match.chain });
|
|
298
|
+
}
|
|
299
|
+
return hits;
|
|
300
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const isObj = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
4
|
+
const list = (value) => (typeof value === 'string' ? [value] : Array.isArray(value) ? value.filter((v) => typeof v === 'string') : []);
|
|
5
|
+
const defaultCache = new Map();
|
|
6
|
+
/** The target of `origin/HEAD` with `origin/` removed (when present), plus `main` and `master`. */
|
|
7
|
+
function defaultBranchNames(repoPath) {
|
|
8
|
+
const key = path.resolve(repoPath);
|
|
9
|
+
const cached = defaultCache.get(key);
|
|
10
|
+
if (cached)
|
|
11
|
+
return cached;
|
|
12
|
+
const result = spawnSync('git', ['-C', key, 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { encoding: 'utf8' });
|
|
13
|
+
const head = result.error || result.status !== 0 ? '' : result.stdout.trim().replace(/^origin\//, '');
|
|
14
|
+
const names = [...new Set([...(head === '' ? [] : [head]), 'main', 'master'])];
|
|
15
|
+
defaultCache.set(key, names);
|
|
16
|
+
return names;
|
|
17
|
+
}
|
|
18
|
+
export function resetDefaultBranchCache() {
|
|
19
|
+
defaultCache.clear();
|
|
20
|
+
}
|
|
21
|
+
const yes = (why) => ({ qualifies: true, why });
|
|
22
|
+
const no = (why) => ({ qualifies: false, why });
|
|
23
|
+
function github(doc, defaults) {
|
|
24
|
+
const on = doc.on ?? doc.true;
|
|
25
|
+
const events = new Map();
|
|
26
|
+
if (typeof on === 'string')
|
|
27
|
+
events.set(on, null);
|
|
28
|
+
else if (Array.isArray(on))
|
|
29
|
+
for (const e of list(on))
|
|
30
|
+
events.set(e, null);
|
|
31
|
+
else if (isObj(on))
|
|
32
|
+
for (const [event, value] of Object.entries(on))
|
|
33
|
+
events.set(event, value);
|
|
34
|
+
for (const event of ['pull_request', 'pull_request_target'])
|
|
35
|
+
if (events.has(event))
|
|
36
|
+
return yes(event);
|
|
37
|
+
if (events.has('push')) {
|
|
38
|
+
const push = events.get('push');
|
|
39
|
+
return pushFilter(isObj(push) ? push : {}, defaults);
|
|
40
|
+
}
|
|
41
|
+
return no(events.size === 0 ? 'no triggers' : `only ${[...events.keys()].join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
function pushFilter(push, defaults) {
|
|
44
|
+
if (push['branches-ignore'] !== undefined)
|
|
45
|
+
return yes('push, branches-ignore');
|
|
46
|
+
if (push.branches !== undefined) {
|
|
47
|
+
const branches = list(push.branches);
|
|
48
|
+
return branches.length === 0 || branches.some((b) => !defaults.includes(b)) ? yes('push to non-default branches') : no('push limited to the default branch');
|
|
49
|
+
}
|
|
50
|
+
if (push.tags !== undefined || push['tags-ignore'] !== undefined)
|
|
51
|
+
return no('push of tags only');
|
|
52
|
+
return yes('push, no filter');
|
|
53
|
+
}
|
|
54
|
+
function limitedToDefault(expression, defaults) {
|
|
55
|
+
if (/merge_request_event|CI_MERGE_REQUEST/.test(expression))
|
|
56
|
+
return false;
|
|
57
|
+
const names = defaults.map((d) => d.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
58
|
+
return new RegExp(`^\\s*\\$CI_(?:COMMIT_BRANCH|COMMIT_REF_NAME)\\s*==\\s*(["']?)(?:\\$CI_DEFAULT_BRANCH|${names})\\1\\s*$`).test(expression);
|
|
59
|
+
}
|
|
60
|
+
function gitlab(doc, jobName, defaults) {
|
|
61
|
+
const job = doc[jobName];
|
|
62
|
+
if (!isObj(job))
|
|
63
|
+
return yes('not a job table');
|
|
64
|
+
if (Array.isArray(job.rules)) {
|
|
65
|
+
for (const rule of job.rules) {
|
|
66
|
+
if (!isObj(rule))
|
|
67
|
+
continue;
|
|
68
|
+
if (typeof rule.if === 'string') {
|
|
69
|
+
if (rule.when === 'never')
|
|
70
|
+
continue;
|
|
71
|
+
if (!limitedToDefault(rule.if, defaults))
|
|
72
|
+
return yes('rule not limited to the default branch');
|
|
73
|
+
}
|
|
74
|
+
else if (rule.when !== 'never')
|
|
75
|
+
return yes('rule without a condition');
|
|
76
|
+
}
|
|
77
|
+
return no('rules limited to the default branch');
|
|
78
|
+
}
|
|
79
|
+
const only = job.only;
|
|
80
|
+
if (only !== undefined) {
|
|
81
|
+
const refs = isObj(only) ? list(only.refs) : list(only);
|
|
82
|
+
if (refs.some((r) => r === 'merge_requests' || r === 'branches'))
|
|
83
|
+
return yes('only merge_requests or branches');
|
|
84
|
+
return refs.length === 0 || refs.some((r) => !defaults.includes(r)) ? yes('only other refs') : no('only the default branch');
|
|
85
|
+
}
|
|
86
|
+
return yes('no rules or only: every branch push');
|
|
87
|
+
}
|
|
88
|
+
function circleci(doc, jobName, defaults) {
|
|
89
|
+
if (!isObj(doc.workflows))
|
|
90
|
+
return yes('no workflows key');
|
|
91
|
+
for (const [name, workflow] of Object.entries(doc.workflows)) {
|
|
92
|
+
if (!isObj(workflow))
|
|
93
|
+
continue;
|
|
94
|
+
const triggers = Array.isArray(workflow.triggers) ? workflow.triggers : [];
|
|
95
|
+
if (triggers.some((t) => isObj(t) && t.schedule !== undefined))
|
|
96
|
+
continue;
|
|
97
|
+
for (const entry of Array.isArray(workflow.jobs) ? workflow.jobs : []) {
|
|
98
|
+
const [key, value] = typeof entry === 'string' ? [entry, null] : isObj(entry) ? (Object.entries(entry)[0] ?? ['', null]) : ['', null];
|
|
99
|
+
const alias = isObj(value) && typeof value.name === 'string' ? value.name : key;
|
|
100
|
+
if (key !== jobName && alias !== jobName)
|
|
101
|
+
continue;
|
|
102
|
+
const only = isObj(value) && isObj(value.filters) && isObj(value.filters.branches) ? list(value.filters.branches.only) : [];
|
|
103
|
+
if (only.length === 0 || only.some((b) => !defaults.includes(b)))
|
|
104
|
+
return yes(`workflow ${name}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return no('in no qualifying workflow');
|
|
108
|
+
}
|
|
109
|
+
function bitbucket(section, doc, defaults) {
|
|
110
|
+
if (section === 'default' || section === 'pull-requests')
|
|
111
|
+
return yes(section);
|
|
112
|
+
if (section === 'branches') {
|
|
113
|
+
const pipelines = isObj(doc.pipelines) ? doc.pipelines : {};
|
|
114
|
+
const keys = isObj(pipelines.branches) ? Object.keys(pipelines.branches) : [];
|
|
115
|
+
return keys.length === 0 || keys.some((k) => !defaults.includes(k)) ? yes('branches other than the default') : no('branches limited to the default branch');
|
|
116
|
+
}
|
|
117
|
+
return no(`${section} pipelines`);
|
|
118
|
+
}
|
|
119
|
+
function azure(doc, defaults) {
|
|
120
|
+
const pr = doc.pr;
|
|
121
|
+
if (pr !== undefined && pr !== 'none' && pr !== null)
|
|
122
|
+
return yes('pr trigger');
|
|
123
|
+
const trigger = doc.trigger;
|
|
124
|
+
if (trigger === undefined)
|
|
125
|
+
return yes('no trigger: CI on all branches');
|
|
126
|
+
if (trigger === 'none')
|
|
127
|
+
return no('trigger: none');
|
|
128
|
+
const include = isObj(trigger) ? (isObj(trigger.branches) ? list(trigger.branches.include) : list(trigger.include)) : list(trigger);
|
|
129
|
+
return include.length === 0 || include.some((b) => !defaults.includes(b)) ? yes('trigger on other branches') : no('trigger limited to the default branch');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Whether a CI job runs on pull requests, merge requests or non-default pushes. A job is excluded only when it is positively
|
|
133
|
+
* identified as not triggered that way; a filter that cannot be read statically qualifies.
|
|
134
|
+
*/
|
|
135
|
+
export function triggersFor(format, doc, jobName, repoPath) {
|
|
136
|
+
if (!isObj(doc))
|
|
137
|
+
return yes('unreadable');
|
|
138
|
+
const defaults = defaultBranchNames(repoPath);
|
|
139
|
+
switch (format) {
|
|
140
|
+
case 'github':
|
|
141
|
+
return github(doc, defaults);
|
|
142
|
+
case 'gitlab':
|
|
143
|
+
return gitlab(doc, jobName, [...defaults, '$CI_DEFAULT_BRANCH']);
|
|
144
|
+
case 'circleci':
|
|
145
|
+
return circleci(doc, jobName, defaults);
|
|
146
|
+
case 'bitbucket':
|
|
147
|
+
return bitbucket(jobName, doc, defaults);
|
|
148
|
+
case 'azure':
|
|
149
|
+
return azure(doc, defaults);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const SUBMIT_URL = 'https://app.vibecodedshit.com/api/submissions';
|
|
2
|
+
export const PRIVACY_URL = 'https://app.vibecodedshit.com/privacy';
|
|
3
|
+
/** The server rejects larger bodies. */
|
|
4
|
+
export const MAX_PAYLOAD_BYTES = 65536;
|
|
5
|
+
/** The server rejects longer summaries. */
|
|
6
|
+
export const MAX_SUMMARY_CHARS = 4000;
|
|
7
|
+
export const SEND_TIMEOUT_MS = 15000;
|
|
8
|
+
export const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
9
|
+
export const MAX_EMAIL_CHARS = 254;
|