@monte3l/groundwork 0.1.0-next.1 → 1.0.0-rc.3
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 +16 -8
- package/bin/m3l-groundwork.mjs +36 -2
- package/dist/assets.d.ts +10 -0
- package/dist/assets.js +14 -0
- package/dist/baseline-stage.d.ts +173 -0
- package/dist/baseline-stage.js +215 -0
- package/dist/caps.d.ts +4 -1
- package/dist/caps.js +17 -3
- package/dist/conflicts.d.ts +23 -2
- package/dist/conflicts.js +89 -11
- package/dist/customize-paths.d.ts +92 -0
- package/dist/customize-paths.js +115 -0
- package/dist/emit.d.ts +50 -1
- package/dist/emit.js +121 -13
- package/dist/fatal.d.ts +70 -0
- package/dist/fatal.js +132 -0
- package/dist/format-error.d.ts +113 -0
- package/dist/format-error.js +553 -0
- package/dist/fs-guard.d.ts +142 -0
- package/dist/fs-guard.js +222 -0
- package/dist/git.js +10 -1
- package/dist/harness/conformance.js +2 -0
- package/dist/harness/frontmatter.js +2 -13
- package/dist/harness/grade.js +37 -8
- package/dist/harness/rules.d.ts +20 -3
- package/dist/harness/rules.js +108 -6
- package/dist/harness/types.d.ts +13 -0
- package/dist/harness/types.js +3 -0
- package/dist/inventory.d.ts +77 -3
- package/dist/inventory.js +103 -12
- package/dist/jsonc.d.ts +49 -2
- package/dist/jsonc.js +140 -7
- package/dist/main.d.ts +62 -3
- package/dist/main.js +538 -67
- package/dist/merge-json.d.ts +47 -4
- package/dist/merge-json.js +120 -8
- package/dist/mode.js +12 -2
- package/dist/pack-stage.d.ts +210 -0
- package/dist/pack-stage.js +287 -0
- package/dist/packs.d.ts +19 -13
- package/dist/packs.js +231 -28
- package/dist/palette.d.ts +23 -0
- package/dist/palette.js +22 -0
- package/dist/plugin.d.ts +122 -6
- package/dist/plugin.js +687 -47
- package/dist/report.d.ts +21 -2
- package/dist/report.js +93 -5
- package/dist/staging.d.ts +176 -0
- package/dist/staging.js +375 -0
- package/dist/survey/fs-walk.d.ts +34 -2
- package/dist/survey/fs-walk.js +70 -5
- package/dist/survey/internal/blocked-path.d.ts +27 -0
- package/dist/survey/internal/blocked-path.js +129 -0
- package/dist/survey/internal/package-json.d.ts +13 -0
- package/dist/survey/internal/package-json.js +37 -0
- package/dist/survey/internal/read-guard.d.ts +178 -0
- package/dist/survey/internal/read-guard.js +276 -0
- package/dist/survey/survey-docs.d.ts +17 -2
- package/dist/survey/survey-docs.js +61 -21
- package/dist/survey/survey-harness.d.ts +20 -2
- package/dist/survey/survey-harness.js +87 -46
- package/dist/survey/survey-shape.d.ts +17 -2
- package/dist/survey/survey-shape.js +60 -46
- package/dist/survey/survey-toolchain.d.ts +16 -1
- package/dist/survey/survey-toolchain.js +69 -66
- package/dist/survey/survey.js +6 -4
- package/dist/survey/types.d.ts +79 -0
- package/dist/survey/types.js +2 -6
- package/dist/term.d.ts +80 -0
- package/dist/term.js +145 -0
- package/dist/tokens.js +2 -0
- package/dist/toolchain/conformance.js +2 -0
- package/dist/toolchain/grade.js +26 -8
- package/dist/toolchain/rules.d.ts +13 -4
- package/dist/toolchain/rules.js +16 -0
- package/dist/toolchain/tsconfig-chain.d.ts +2 -0
- package/dist/toolchain/tsconfig-chain.js +32 -8
- package/dist/toolchain/types.d.ts +16 -4
- package/dist/toolchain/types.js +3 -0
- package/package.json +4 -3
- package/plugin/skills/customize/SKILL.md +292 -18
- package/plugin/src/domain-map.ts +39 -14
- package/plugin/src/index.ts +5 -1
- package/plugin/src/kind-facet-map.ts +25 -8
- package/plugin/src/pack-map.ts +147 -25
- package/plugin/src/plugin-map.ts +236 -0
- package/templates/core/.claude/agents/Explore.md +0 -1
- package/templates/core/.claude/agents/code-implementer.md +4 -4
- package/templates/core/.claude/agents/code-reviewer.md +4 -4
- package/templates/core/.claude/agents/silent-failure-hunter.md +2 -2
- package/templates/core/.claude/agents/test-author.md +7 -5
- package/templates/core/.claude/hooks/guard-branch-isolation.mjs +56 -10
- package/templates/core/.claude/hooks/guard-double-background.mjs +13 -8
- package/templates/core/.claude/hooks/guard-git-push-signed.mjs +12 -9
- package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +1890 -38
- package/templates/core/.claude/hooks/guard-js-extension.mjs +2 -1
- package/templates/core/.claude/hooks/guard-no-commonjs.mjs +7 -5
- package/templates/core/.claude/hooks/guard-secret-writes.mjs +7 -5
- package/templates/core/.claude/hooks/inject-decision-gate.mjs +12 -9
- package/templates/core/.claude/hooks/post-edit-verify.mjs +276 -98
- package/templates/core/.claude/rules/agent-dispatch.md +7 -0
- package/templates/core/.claude/rules/tests.md +2 -2
- package/templates/core/.claude/settings.json +5 -0
- package/templates/core/.claude/skills/finishing-work/SKILL.md +58 -10
- package/templates/core/.claude/skills/harness-guidance/SKILL.md +16 -7
- package/templates/core/.claude/skills/starting-work/SKILL.md +5 -0
- package/templates/core/.claude/skills/triaging-ci/SKILL.md +9 -8
- package/templates/core/.claude/skills/typescript-guidance/SKILL.md +137 -20
- package/templates/core/.claude/skills/typescript-guidance/references/area-catalog.md +135 -0
- package/templates/core/.claude/skills/typescript-guidance/references/tooling-sources.md +113 -0
- package/templates/core/.claude/skills/writing-commits/SKILL.md +2 -2
- package/templates/core/.github/dependabot.yml +18 -0
- package/templates/core/.github/workflows/ci.yml +15 -15
- package/templates/core/.github/workflows/dependency-review.yml +2 -2
- package/templates/core/.github/workflows/security-audit.yml +10 -4
- package/templates/core/.prettierignore +4 -0
- package/templates/core/CLAUDE.md +53 -3
- package/templates/core/README.md +30 -10
- package/templates/core/_gitignore +17 -0
- package/templates/core/bin/check-exports.mjs +11 -5
- package/templates/core/bin/lib/agent-roster.mjs +1 -1
- package/templates/core/bin/lib/frontmatter.mjs +4 -2
- package/templates/core/bin/lib/harness-rules.mjs +169 -20
- package/templates/core/bin/lib/protected-paths.mjs +93 -5
- package/templates/core/bin/lib/toolchain-rules.mjs +187 -26
- package/templates/core/bin/lib/verify-steps.mjs +29 -11
- package/templates/core/bin/verify.mjs +12 -0
- package/templates/core/eslint.config.js +5 -0
- package/templates/core/package.json +7 -7
- package/templates/core/vitest.config.ts +8 -1
- package/templates/packs/README.md +35 -24
- package/templates/packs/github/files/.claude/skills/reviewing-dependabot-prs/SKILL.md +133 -0
- package/templates/packs/github/files/.claude/skills/triaging-scan-alerts/SKILL.md +88 -0
- package/templates/packs/github/files/.claude/skills/watching-pr-checks/SKILL.md +94 -0
- package/templates/packs/github/files/.github/workflows/claude-pr-review.yml +267 -0
- package/templates/packs/github/files/.github/workflows/claude.yml +106 -0
- package/templates/packs/github/pack.json +19 -0
- package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +1122 -65
- package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +147 -13
- package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline.mjs +6 -2
- package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +227 -44
- package/templates/packs/harness-extras/pack.json +18 -13
- package/templates/packs/publishing/files/.changeset/README.md +25 -0
- package/templates/packs/publishing/files/.changeset/config.json +7 -0
- package/templates/packs/publishing/files/.github/release-tools/package.json +9 -0
- package/templates/packs/publishing/files/.github/workflows/release.yml +293 -0
- package/templates/packs/publishing/files/REUSE.toml +28 -0
- package/templates/packs/publishing/files/bin/check-dts-deps.mjs +234 -0
- package/templates/packs/publishing/files/bin/check-license-headers.mjs +217 -0
- package/templates/packs/publishing/files/bin/check-publish-version.mjs +149 -0
- package/templates/packs/publishing/files/bin/lib/npm-publish-args.mjs +86 -0
- package/templates/packs/publishing/files/bin/pnpm-publish-shim.mjs +86 -0
- package/templates/packs/publishing/pack.json +35 -0
- package/templates/packs/{harness-extras → quality}/files/bin/check-file-budget.mjs +6 -4
- package/templates/packs/quality/pack.json +29 -0
- package/templates/packs/supply-chain/files/.github/workflows/gitleaks.yml +52 -0
- package/templates/packs/supply-chain/files/.github/workflows/scorecard.yml +48 -0
- package/templates/packs/supply-chain/files/.gitleaks.toml +2 -0
- package/templates/packs/supply-chain/pack.json +19 -0
- package/templates/packs/worktrees/files/.claude/hooks/ensure-worktree-deps.mjs +283 -0
- package/templates/packs/worktrees/files/.claude/hooks/guard-worktree-only.mjs +245 -0
- package/templates/packs/worktrees/files/.claude/hooks/repair-core-bare.mjs +335 -0
- package/templates/packs/worktrees/files/.claude/skills/working-in-worktrees/SKILL.md +139 -0
- package/templates/packs/worktrees/files/.worktreeinclude +11 -0
- package/templates/packs/worktrees/pack.json +64 -0
- package/templates/packs/statusline/pack.json +0 -31
- /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline-layout.mjs +0 -0
- /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/subagent-statusline.mjs +0 -0
- /package/templates/packs/{harness-extras → quality}/files/.claude/agents/type-design-analyzer.md +0 -0
- /package/templates/packs/{harness-extras → quality}/files/bin/file-budget-baseline.json +0 -0
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
1
3
|
/**
|
|
2
4
|
* Collects the whole `.claude/` surface of an existing project -- hook
|
|
3
5
|
* wiring, agent/skill/rule frontmatter, commands, and `CLAUDE.md`'s heading
|
|
4
6
|
* outline -- so `/customize`'s harness sweep starts from what is actually
|
|
5
7
|
* there instead of assuming the m3l-groundwork baseline.
|
|
6
8
|
*/
|
|
7
|
-
import {
|
|
9
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
8
10
|
import { join } from "node:path";
|
|
9
11
|
import { fieldText, parseFrontmatter } from "../harness/frontmatter.js";
|
|
12
|
+
import { guardedExists, guardedRead } from "./internal/read-guard.js";
|
|
10
13
|
/**
|
|
11
14
|
* Reads one frontmatter field as text (a list is joined with `, `), or
|
|
12
15
|
* `undefined` when the file has no frontmatter, the field is absent, or its
|
|
@@ -21,33 +24,48 @@ function extractFrontmatterField(content, field) {
|
|
|
21
24
|
const text = fieldText(parsed.fields, field);
|
|
22
25
|
return text === undefined || text === "" ? undefined : text;
|
|
23
26
|
}
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
/** Reads `path` as UTF-8 text, or `undefined` (recorded) when it is unreadable. */
|
|
28
|
+
function readText(path, undetermined) {
|
|
29
|
+
return guardedRead(path, () => readFileSync(path, "utf8"), undetermined);
|
|
30
|
+
}
|
|
31
|
+
/** Lists `dir`'s entry names: `[]` when it is absent, or unreadable (recorded). */
|
|
32
|
+
function listNames(dir, undetermined) {
|
|
33
|
+
if (!guardedExists(dir, undetermined))
|
|
26
34
|
return [];
|
|
27
|
-
return
|
|
35
|
+
return guardedRead(dir, () => readdirSync(dir), undetermined) ?? [];
|
|
36
|
+
}
|
|
37
|
+
function listMarkdownFiles(dir, undetermined) {
|
|
38
|
+
return listNames(dir, undetermined).filter((name) => name.endsWith(".md"));
|
|
28
39
|
}
|
|
29
|
-
function surveyAgents(claudeDir) {
|
|
40
|
+
function surveyAgents(claudeDir, undetermined) {
|
|
30
41
|
const agentsDir = join(claudeDir, "agents");
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
const results = [];
|
|
43
|
+
for (const name of listMarkdownFiles(agentsDir, undetermined)) {
|
|
44
|
+
const content = readText(join(agentsDir, name), undetermined);
|
|
45
|
+
if (content === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
results.push({
|
|
34
48
|
name: name.replace(/\.md$/, ""),
|
|
35
49
|
model: extractFrontmatterField(content, "model"),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
38
53
|
}
|
|
39
|
-
function surveySkills(claudeDir) {
|
|
54
|
+
function surveySkills(claudeDir, undetermined) {
|
|
40
55
|
const skillsDir = join(claudeDir, "skills");
|
|
41
|
-
if (!
|
|
56
|
+
if (!guardedExists(skillsDir, undetermined))
|
|
42
57
|
return [];
|
|
58
|
+
const entries = guardedRead(skillsDir, () => readdirSync(skillsDir, { withFileTypes: true }), undetermined) ?? [];
|
|
43
59
|
const results = [];
|
|
44
|
-
for (const entry of
|
|
60
|
+
for (const entry of entries) {
|
|
45
61
|
if (!entry.isDirectory())
|
|
46
62
|
continue;
|
|
47
63
|
const skillFile = join(skillsDir, entry.name, "SKILL.md");
|
|
48
|
-
if (!
|
|
64
|
+
if (!guardedExists(skillFile, undetermined))
|
|
65
|
+
continue;
|
|
66
|
+
const content = readText(skillFile, undetermined);
|
|
67
|
+
if (content === undefined)
|
|
49
68
|
continue;
|
|
50
|
-
const content = readFileSync(skillFile, "utf8");
|
|
51
69
|
results.push({
|
|
52
70
|
name: extractFrontmatterField(content, "name") ?? entry.name,
|
|
53
71
|
description: extractFrontmatterField(content, "description"),
|
|
@@ -55,25 +73,25 @@ function surveySkills(claudeDir) {
|
|
|
55
73
|
}
|
|
56
74
|
return results;
|
|
57
75
|
}
|
|
58
|
-
function surveyHooks(claudeDir) {
|
|
59
|
-
|
|
60
|
-
if (!existsSync(hooksDir))
|
|
61
|
-
return [];
|
|
62
|
-
return readdirSync(hooksDir).filter((name) => name.endsWith(".mjs") || name.endsWith(".js"));
|
|
76
|
+
function surveyHooks(claudeDir, undetermined) {
|
|
77
|
+
return listNames(join(claudeDir, "hooks"), undetermined).filter((name) => name.endsWith(".mjs") || name.endsWith(".js"));
|
|
63
78
|
}
|
|
64
|
-
function surveyRules(claudeDir) {
|
|
79
|
+
function surveyRules(claudeDir, undetermined) {
|
|
65
80
|
const rulesDir = join(claudeDir, "rules");
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
const results = [];
|
|
82
|
+
for (const name of listMarkdownFiles(rulesDir, undetermined)) {
|
|
83
|
+
const content = readText(join(rulesDir, name), undetermined);
|
|
84
|
+
if (content === undefined)
|
|
85
|
+
continue;
|
|
86
|
+
results.push({
|
|
69
87
|
name: name.replace(/\.md$/, ""),
|
|
70
88
|
paths: extractFrontmatterField(content, "paths"),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return results;
|
|
73
92
|
}
|
|
74
|
-
function surveyCommands(claudeDir) {
|
|
75
|
-
|
|
76
|
-
return listMarkdownFiles(commandsDir);
|
|
93
|
+
function surveyCommands(claudeDir, undetermined) {
|
|
94
|
+
return listMarkdownFiles(join(claudeDir, "commands"), undetermined);
|
|
77
95
|
}
|
|
78
96
|
function extractHeadings(content) {
|
|
79
97
|
return content
|
|
@@ -81,12 +99,37 @@ function extractHeadings(content) {
|
|
|
81
99
|
.filter((line) => /^#{1,3}\s/.test(line))
|
|
82
100
|
.map((line) => line.replace(/^#{1,3}\s*/, "").trim());
|
|
83
101
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
102
|
+
/** `CLAUDE.md`'s heading outline: `[]` when it is absent, or unreadable (recorded). */
|
|
103
|
+
function claudeMdHeadings(path, undetermined) {
|
|
104
|
+
if (!guardedExists(path, undetermined))
|
|
105
|
+
return [];
|
|
106
|
+
const content = readText(path, undetermined);
|
|
107
|
+
return content === undefined ? [] : extractHeadings(content);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Surveys the `.claude/` harness and `CLAUDE.md` at `dir`. Offline,
|
|
111
|
+
* read-only. A file or directory that exists but cannot be read
|
|
112
|
+
* (`EACCES`/`EPERM`) is left out of its collection and recorded in
|
|
113
|
+
* `undetermined` -- an unreadable `CLAUDE.md` still counts as present, with
|
|
114
|
+
* no headings. A `.claude/` this process may not enter is still `present`,
|
|
115
|
+
* with every collection empty and the directory recorded in `undetermined`
|
|
116
|
+
* -- never reported as an empty harness. Any other read failure throws, naming the path, with the
|
|
117
|
+
* original failure as `cause`.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* import { surveyHarness } from "./survey-harness.js";
|
|
122
|
+
*
|
|
123
|
+
* const undetermined: string[] = [];
|
|
124
|
+
* const harness = surveyHarness("/path/to/project", undetermined);
|
|
125
|
+
* console.log(harness.agents.map((agent) => agent.name), undetermined);
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function surveyHarness(dir, undetermined) {
|
|
86
129
|
const claudeDir = join(dir, ".claude");
|
|
87
130
|
const claudeMdPath = join(dir, "CLAUDE.md");
|
|
88
|
-
const hasClaudeMd =
|
|
89
|
-
if (!
|
|
131
|
+
const hasClaudeMd = guardedExists(claudeMdPath, undetermined);
|
|
132
|
+
if (!guardedExists(claudeDir, undetermined)) {
|
|
90
133
|
return {
|
|
91
134
|
present: false,
|
|
92
135
|
settingsFile: undefined,
|
|
@@ -97,25 +140,23 @@ export function surveyHarness(dir) {
|
|
|
97
140
|
commands: [],
|
|
98
141
|
hasSettingsLocal: false,
|
|
99
142
|
hasClaudeMd,
|
|
100
|
-
claudeMdHeadings:
|
|
101
|
-
? extractHeadings(readFileSync(claudeMdPath, "utf8"))
|
|
102
|
-
: [],
|
|
143
|
+
claudeMdHeadings: claudeMdHeadings(claudeMdPath, undetermined),
|
|
103
144
|
};
|
|
104
145
|
}
|
|
105
146
|
const settingsPath = join(claudeDir, "settings.json");
|
|
106
147
|
return {
|
|
107
148
|
present: true,
|
|
108
|
-
settingsFile:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
149
|
+
settingsFile: guardedExists(settingsPath, undetermined)
|
|
150
|
+
? "settings.json"
|
|
151
|
+
: undefined,
|
|
152
|
+
agents: surveyAgents(claudeDir, undetermined),
|
|
153
|
+
skills: surveySkills(claudeDir, undetermined),
|
|
154
|
+
hooks: surveyHooks(claudeDir, undetermined),
|
|
155
|
+
rules: surveyRules(claudeDir, undetermined),
|
|
156
|
+
commands: surveyCommands(claudeDir, undetermined),
|
|
157
|
+
hasSettingsLocal: guardedExists(join(claudeDir, "settings.local.json"), undetermined),
|
|
115
158
|
hasClaudeMd,
|
|
116
|
-
claudeMdHeadings:
|
|
117
|
-
? extractHeadings(readFileSync(claudeMdPath, "utf8"))
|
|
118
|
-
: [],
|
|
159
|
+
claudeMdHeadings: claudeMdHeadings(claudeMdPath, undetermined),
|
|
119
160
|
};
|
|
120
161
|
}
|
|
121
162
|
//# sourceMappingURL=survey-harness.js.map
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { ShapeSurvey } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Surveys codebase shape at `dir`. Offline, read-only, records evidence
|
|
4
|
+
* rather than verdicts. Appends anything it could not parse (a malformed
|
|
5
|
+
* `package.json`) or could not read (`EACCES`/`EPERM`) to `undetermined`;
|
|
6
|
+
* an absent file is not recorded. Any other read failure throws, naming the
|
|
7
|
+
* path, with the original failure as `cause`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { surveyShape } from "./survey-shape.js";
|
|
12
|
+
*
|
|
13
|
+
* const undetermined: string[] = [];
|
|
14
|
+
* const shape = surveyShape("/path/to/project", undetermined);
|
|
15
|
+
* console.log(shape.packageManager, undetermined);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function surveyShape(dir: string, undetermined: string[]): ShapeSurvey;
|
|
4
19
|
//# sourceMappingURL=survey-shape.d.ts.map
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
1
3
|
/**
|
|
2
4
|
* Collects codebase-shape facts: package manager, monorepo tooling, module
|
|
3
5
|
* system, pinned versions, source/test layout, and the evidence (not the
|
|
4
6
|
* verdict) for what kind of project this is. `/customize`'s interview step
|
|
5
7
|
* does the inferring; this module only records what is literally on disk.
|
|
6
8
|
*/
|
|
7
|
-
import {
|
|
9
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
8
10
|
import { join } from "node:path";
|
|
9
11
|
import { walkBounded } from "./fs-walk.js";
|
|
12
|
+
import { readPackageJson } from "./internal/package-json.js";
|
|
13
|
+
import { guardedExists, guardedRead } from "./internal/read-guard.js";
|
|
10
14
|
const FRAMEWORK_DEP_NAMES = [
|
|
11
15
|
"react",
|
|
12
16
|
"vue",
|
|
@@ -20,26 +24,14 @@ const FRAMEWORK_DEP_NAMES = [
|
|
|
20
24
|
"@nestjs/core",
|
|
21
25
|
"hono",
|
|
22
26
|
];
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
if (!existsSync(path)) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function detectPackageManager(dir) {
|
|
36
|
-
if (existsSync(join(dir, "pnpm-lock.yaml")))
|
|
27
|
+
function detectPackageManager(dir, undetermined) {
|
|
28
|
+
if (guardedExists(join(dir, "pnpm-lock.yaml"), undetermined))
|
|
37
29
|
return "pnpm";
|
|
38
|
-
if (
|
|
30
|
+
if (guardedExists(join(dir, "yarn.lock"), undetermined))
|
|
39
31
|
return "yarn";
|
|
40
|
-
if (
|
|
32
|
+
if (guardedExists(join(dir, "bun.lockb"), undetermined))
|
|
41
33
|
return "bun";
|
|
42
|
-
if (
|
|
34
|
+
if (guardedExists(join(dir, "package-lock.json"), undetermined))
|
|
43
35
|
return "npm";
|
|
44
36
|
return "unknown";
|
|
45
37
|
}
|
|
@@ -59,22 +51,24 @@ function extractYamlStringListUnder(content, key) {
|
|
|
59
51
|
}
|
|
60
52
|
return items;
|
|
61
53
|
}
|
|
62
|
-
function detectMonorepo(dir, packageJson) {
|
|
54
|
+
function detectMonorepo(dir, packageJson, undetermined) {
|
|
63
55
|
const pnpmWorkspacePath = join(dir, "pnpm-workspace.yaml");
|
|
64
|
-
if (
|
|
65
|
-
const content = readFileSync(pnpmWorkspacePath, "utf8");
|
|
56
|
+
if (guardedExists(pnpmWorkspacePath, undetermined)) {
|
|
57
|
+
const content = guardedRead(pnpmWorkspacePath, () => readFileSync(pnpmWorkspacePath, "utf8"), undetermined);
|
|
66
58
|
return {
|
|
67
59
|
tool: "pnpm-workspaces",
|
|
68
|
-
globs:
|
|
60
|
+
globs: content === undefined
|
|
61
|
+
? []
|
|
62
|
+
: extractYamlStringListUnder(content, "packages"),
|
|
69
63
|
};
|
|
70
64
|
}
|
|
71
|
-
if (
|
|
65
|
+
if (guardedExists(join(dir, "turbo.json"), undetermined)) {
|
|
72
66
|
return { tool: "turbo", globs: [] };
|
|
73
67
|
}
|
|
74
|
-
if (
|
|
68
|
+
if (guardedExists(join(dir, "nx.json"), undetermined)) {
|
|
75
69
|
return { tool: "nx", globs: [] };
|
|
76
70
|
}
|
|
77
|
-
if (
|
|
71
|
+
if (guardedExists(join(dir, "lerna.json"), undetermined)) {
|
|
78
72
|
return { tool: "lerna", globs: [] };
|
|
79
73
|
}
|
|
80
74
|
const workspaces = packageJson?.["workspaces"];
|
|
@@ -106,15 +100,18 @@ function detectTypescriptVersion(packageJson) {
|
|
|
106
100
|
const value = fromDevDeps ?? fromDeps;
|
|
107
101
|
return typeof value === "string" ? value : undefined;
|
|
108
102
|
}
|
|
109
|
-
function detectNodeVersionPin(dir, packageJson) {
|
|
103
|
+
function detectNodeVersionPin(dir, packageJson, undetermined) {
|
|
110
104
|
for (const [source, filename] of [
|
|
111
105
|
[".node-version", ".node-version"],
|
|
112
106
|
[".nvmrc", ".nvmrc"],
|
|
113
107
|
]) {
|
|
114
108
|
const path = join(dir, filename);
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
109
|
+
if (!guardedExists(path, undetermined))
|
|
110
|
+
continue;
|
|
111
|
+
// An unreadable pin file falls through to the next source.
|
|
112
|
+
const content = guardedRead(path, () => readFileSync(path, "utf8"), undetermined);
|
|
113
|
+
if (content !== undefined)
|
|
114
|
+
return { source, value: content.trim() };
|
|
118
115
|
}
|
|
119
116
|
const engines = packageJson?.["engines"];
|
|
120
117
|
const nodeEngine = typeof engines === "object" && engines !== null
|
|
@@ -125,24 +122,26 @@ function detectNodeVersionPin(dir, packageJson) {
|
|
|
125
122
|
}
|
|
126
123
|
return undefined;
|
|
127
124
|
}
|
|
128
|
-
function detectSourceLayout(dir) {
|
|
129
|
-
if (
|
|
125
|
+
function detectSourceLayout(dir, undetermined) {
|
|
126
|
+
if (guardedExists(join(dir, "src"), undetermined))
|
|
130
127
|
return "src";
|
|
131
|
-
if (
|
|
128
|
+
if (guardedExists(join(dir, "lib"), undetermined))
|
|
132
129
|
return "lib";
|
|
133
|
-
const
|
|
130
|
+
const names = guardedRead(dir, () => readdirSync(dir), undetermined) ?? [];
|
|
131
|
+
const rootEntries = names.filter((name) => /\.(ts|tsx|js|mjs)$/.test(name));
|
|
134
132
|
if (rootEntries.length > 0)
|
|
135
133
|
return "root";
|
|
136
134
|
return "unknown";
|
|
137
135
|
}
|
|
138
|
-
function hasColocatedTests(dir) {
|
|
139
|
-
return walkBounded(dir, 2).some((entry) => !entry.isDirectory && /\.(test|spec)\.[jt]sx?$/.test(entry.relPath));
|
|
136
|
+
function hasColocatedTests(dir, undetermined) {
|
|
137
|
+
return walkBounded(dir, 2, undetermined).some((entry) => !entry.isDirectory && /\.(test|spec)\.[jt]sx?$/.test(entry.relPath));
|
|
140
138
|
}
|
|
141
|
-
function detectTestPlacement(dir) {
|
|
142
|
-
if (
|
|
139
|
+
function detectTestPlacement(dir, undetermined) {
|
|
140
|
+
if (guardedExists(join(dir, "tests"), undetermined) ||
|
|
141
|
+
guardedExists(join(dir, "test"), undetermined)) {
|
|
143
142
|
return "tests-dir";
|
|
144
143
|
}
|
|
145
|
-
if (hasColocatedTests(dir)) {
|
|
144
|
+
if (hasColocatedTests(dir, undetermined)) {
|
|
146
145
|
return "colocated";
|
|
147
146
|
}
|
|
148
147
|
return "unknown";
|
|
@@ -163,19 +162,34 @@ function collectKindEvidence(packageJson) {
|
|
|
163
162
|
frameworkDeps: FRAMEWORK_DEP_NAMES.filter((name) => allDepNames.has(name)),
|
|
164
163
|
};
|
|
165
164
|
}
|
|
166
|
-
/**
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Surveys codebase shape at `dir`. Offline, read-only, records evidence
|
|
167
|
+
* rather than verdicts. Appends anything it could not parse (a malformed
|
|
168
|
+
* `package.json`) or could not read (`EACCES`/`EPERM`) to `undetermined`;
|
|
169
|
+
* an absent file is not recorded. Any other read failure throws, naming the
|
|
170
|
+
* path, with the original failure as `cause`.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```ts
|
|
174
|
+
* import { surveyShape } from "./survey-shape.js";
|
|
175
|
+
*
|
|
176
|
+
* const undetermined: string[] = [];
|
|
177
|
+
* const shape = surveyShape("/path/to/project", undetermined);
|
|
178
|
+
* console.log(shape.packageManager, undetermined);
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export function surveyShape(dir, undetermined) {
|
|
182
|
+
const packageJson = readPackageJson(dir, undetermined);
|
|
183
|
+
const monorepo = detectMonorepo(dir, packageJson, undetermined);
|
|
170
184
|
return {
|
|
171
|
-
packageManager: detectPackageManager(dir),
|
|
185
|
+
packageManager: detectPackageManager(dir, undetermined),
|
|
172
186
|
monorepoTool: monorepo.tool,
|
|
173
187
|
workspaceGlobs: monorepo.globs,
|
|
174
188
|
moduleType: detectModuleType(packageJson),
|
|
175
189
|
typescriptVersion: detectTypescriptVersion(packageJson),
|
|
176
|
-
nodeVersionPin: detectNodeVersionPin(dir, packageJson),
|
|
177
|
-
sourceLayout: detectSourceLayout(dir),
|
|
178
|
-
testPlacement: detectTestPlacement(dir),
|
|
190
|
+
nodeVersionPin: detectNodeVersionPin(dir, packageJson, undetermined),
|
|
191
|
+
sourceLayout: detectSourceLayout(dir, undetermined),
|
|
192
|
+
testPlacement: detectTestPlacement(dir, undetermined),
|
|
179
193
|
kindEvidence: collectKindEvidence(packageJson),
|
|
180
194
|
};
|
|
181
195
|
}
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { ToolchainSurvey } from "./types.js";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Surveys toolchain enforcement at `dir`. Appends anything it could not
|
|
4
|
+
* parse, or could not read (`EACCES`/`EPERM`, a dangling symlink, a symlink
|
|
5
|
+
* loop, a directory where a file was expected), to `undetermined`; any other
|
|
6
|
+
* read failure throws, naming the path, with the original failure as
|
|
7
|
+
* `cause`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { surveyToolchain } from "./survey-toolchain.js";
|
|
12
|
+
*
|
|
13
|
+
* const undetermined: string[] = [];
|
|
14
|
+
* const toolchain = surveyToolchain("/path/to/project", undetermined);
|
|
15
|
+
* console.log(toolchain.tsconfig.effectiveFlags, undetermined);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
3
18
|
export declare function surveyToolchain(dir: string, undetermined: string[]): ToolchainSurvey;
|
|
4
19
|
//# sourceMappingURL=survey-toolchain.d.ts.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
1
3
|
/**
|
|
2
4
|
* Collects toolchain-enforcement facts: the tsconfig `extends` chain and its
|
|
3
5
|
* effective strict-family flags, which eslint/test/formatter/git-hook tool
|
|
@@ -6,28 +8,19 @@
|
|
|
6
8
|
* here, never parsed -- see `needsReading` on each -- because this package
|
|
7
9
|
* carries no YAML dependency.
|
|
8
10
|
*/
|
|
9
|
-
import {
|
|
11
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
10
12
|
import { join } from "node:path";
|
|
11
13
|
import { STRICT_FLAGS } from "../toolchain/rules.js";
|
|
12
14
|
import { loadTsconfigChain } from "../toolchain/tsconfig-chain.js";
|
|
15
|
+
import { readPackageJson } from "./internal/package-json.js";
|
|
16
|
+
import { guardedExists, guardedRead } from "./internal/read-guard.js";
|
|
13
17
|
/** Every flag the toolchain grader judges, so `effectiveFlags` and the grade cannot disagree. */
|
|
14
18
|
const STRICT_FLAG_NAMES = [...STRICT_FLAGS, "allowUnreachableCode"];
|
|
15
19
|
const ESLINT_PLUGIN_PATTERN = /["']((?:eslint-plugin-|@typescript-eslint\/)[a-z0-9-]+)["']/gi;
|
|
16
|
-
function
|
|
17
|
-
const path = join(dir, "package.json");
|
|
18
|
-
if (!existsSync(path))
|
|
19
|
-
return undefined;
|
|
20
|
-
try {
|
|
21
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function findTsconfigPath(dir) {
|
|
20
|
+
function findTsconfigPath(dir, undetermined) {
|
|
28
21
|
for (const name of ["tsconfig.json"]) {
|
|
29
22
|
const path = join(dir, name);
|
|
30
|
-
if (
|
|
23
|
+
if (guardedExists(path, undetermined))
|
|
31
24
|
return path;
|
|
32
25
|
}
|
|
33
26
|
return undefined;
|
|
@@ -37,11 +30,12 @@ function findTsconfigPath(dir) {
|
|
|
37
30
|
* shares, so the survey and the grade cannot disagree about a project's
|
|
38
31
|
* effective flags. `files` are absolute and child-first. A file that fails to
|
|
39
32
|
* parse, or a relative `extends` that points at nothing, is a parse failure; a
|
|
40
|
-
*
|
|
33
|
+
* file in the chain that exists but cannot be read is noted "could not read",
|
|
34
|
+
* never "could not parse"; a bare package specifier that is not installed is only a note -- the flags it
|
|
41
35
|
* would contribute are simply absent.
|
|
42
36
|
*/
|
|
43
37
|
function surveyTsconfig(dir, undetermined) {
|
|
44
|
-
const entryPath = findTsconfigPath(dir);
|
|
38
|
+
const entryPath = findTsconfigPath(dir, undetermined);
|
|
45
39
|
if (entryPath === undefined) {
|
|
46
40
|
return { files: [], effectiveFlags: {}, parsed: false };
|
|
47
41
|
}
|
|
@@ -49,7 +43,8 @@ function surveyTsconfig(dir, undetermined) {
|
|
|
49
43
|
let parsed = chain.parsed;
|
|
50
44
|
for (const file of chain.files) {
|
|
51
45
|
if (file.error !== undefined) {
|
|
52
|
-
|
|
46
|
+
const verb = file.readFailed ? "could not read" : "could not parse";
|
|
47
|
+
undetermined.push(`${verb} ${file.abs}: ${file.error}`);
|
|
53
48
|
}
|
|
54
49
|
}
|
|
55
50
|
for (const link of chain.links) {
|
|
@@ -71,39 +66,30 @@ function surveyTsconfig(dir, undetermined) {
|
|
|
71
66
|
}
|
|
72
67
|
return { files: chain.files.map((file) => file.abs), effectiveFlags, parsed };
|
|
73
68
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
]
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const content = readFileSync(path, "utf8");
|
|
90
|
-
return {
|
|
91
|
-
configFile: name,
|
|
92
|
-
flat: true,
|
|
93
|
-
referencedPlugins: extractPluginNames(content),
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
for (const name of legacyCandidates) {
|
|
69
|
+
const ESLINT_CANDIDATES = [
|
|
70
|
+
["eslint.config.js", true],
|
|
71
|
+
["eslint.config.mjs", true],
|
|
72
|
+
["eslint.config.ts", true],
|
|
73
|
+
[".eslintrc.js", false],
|
|
74
|
+
[".eslintrc.cjs", false],
|
|
75
|
+
[".eslintrc.json", false],
|
|
76
|
+
[".eslintrc", false],
|
|
77
|
+
];
|
|
78
|
+
/**
|
|
79
|
+
* The first ESLint config present, flat configs first. An unreadable config
|
|
80
|
+
* is still reported (its presence is known) with no plugins, and recorded.
|
|
81
|
+
*/
|
|
82
|
+
function surveyEslint(dir, undetermined) {
|
|
83
|
+
for (const [name, flat] of ESLINT_CANDIDATES) {
|
|
98
84
|
const path = join(dir, name);
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
85
|
+
if (!guardedExists(path, undetermined))
|
|
86
|
+
continue;
|
|
87
|
+
const content = guardedRead(path, () => readFileSync(path, "utf8"), undetermined);
|
|
88
|
+
return {
|
|
89
|
+
configFile: name,
|
|
90
|
+
flat,
|
|
91
|
+
referencedPlugins: content === undefined ? [] : extractPluginNames(content),
|
|
92
|
+
};
|
|
107
93
|
}
|
|
108
94
|
return { configFile: undefined, flat: false, referencedPlugins: [] };
|
|
109
95
|
}
|
|
@@ -116,7 +102,7 @@ function extractPluginNames(content) {
|
|
|
116
102
|
}
|
|
117
103
|
return [...names];
|
|
118
104
|
}
|
|
119
|
-
function surveyTestRunner(dir, scripts) {
|
|
105
|
+
function surveyTestRunner(dir, scripts, undetermined) {
|
|
120
106
|
const candidates = [
|
|
121
107
|
["vitest.config.ts", "vitest"],
|
|
122
108
|
["vitest.config.js", "vitest"],
|
|
@@ -128,7 +114,7 @@ function surveyTestRunner(dir, scripts) {
|
|
|
128
114
|
[".mocharc.js", "mocha"],
|
|
129
115
|
];
|
|
130
116
|
for (const [name, tool] of candidates) {
|
|
131
|
-
if (
|
|
117
|
+
if (guardedExists(join(dir, name), undetermined)) {
|
|
132
118
|
return { tool, configFile: name };
|
|
133
119
|
}
|
|
134
120
|
}
|
|
@@ -138,7 +124,7 @@ function surveyTestRunner(dir, scripts) {
|
|
|
138
124
|
}
|
|
139
125
|
return { tool: "unknown", configFile: undefined };
|
|
140
126
|
}
|
|
141
|
-
function surveyFormatter(dir) {
|
|
127
|
+
function surveyFormatter(dir, undetermined) {
|
|
142
128
|
const prettierCandidates = [
|
|
143
129
|
".prettierrc.json",
|
|
144
130
|
".prettierrc.js",
|
|
@@ -147,29 +133,29 @@ function surveyFormatter(dir) {
|
|
|
147
133
|
".prettierrc.yml",
|
|
148
134
|
];
|
|
149
135
|
for (const name of prettierCandidates) {
|
|
150
|
-
if (
|
|
136
|
+
if (guardedExists(join(dir, name), undetermined)) {
|
|
151
137
|
return { tool: "prettier", configFile: name };
|
|
152
138
|
}
|
|
153
139
|
}
|
|
154
|
-
if (
|
|
140
|
+
if (guardedExists(join(dir, "biome.json"), undetermined)) {
|
|
155
141
|
return { tool: "biome", configFile: "biome.json" };
|
|
156
142
|
}
|
|
157
143
|
return { tool: "unknown", configFile: undefined };
|
|
158
144
|
}
|
|
159
145
|
function surveyGitHooks(dir, undetermined) {
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
const configFile =
|
|
146
|
+
if (guardedExists(join(dir, "lefthook.yml"), undetermined) ||
|
|
147
|
+
guardedExists(join(dir, "lefthook.yaml"), undetermined)) {
|
|
148
|
+
const configFile = guardedExists(join(dir, "lefthook.yml"), undetermined)
|
|
163
149
|
? "lefthook.yml"
|
|
164
150
|
: "lefthook.yaml";
|
|
165
151
|
undetermined.push(`${configFile} found -- its stage commands need reading, not parsing`);
|
|
166
152
|
return { manager: "lefthook", configFile, needsReading: true };
|
|
167
153
|
}
|
|
168
|
-
if (
|
|
154
|
+
if (guardedExists(join(dir, ".husky"), undetermined)) {
|
|
169
155
|
undetermined.push(".husky/ found -- its hook scripts need reading, not parsing");
|
|
170
156
|
return { manager: "husky", configFile: ".husky", needsReading: true };
|
|
171
157
|
}
|
|
172
|
-
if (
|
|
158
|
+
if (guardedExists(join(dir, "simple-git-hooks.json"), undetermined)) {
|
|
173
159
|
return {
|
|
174
160
|
manager: "simple-git-hooks",
|
|
175
161
|
configFile: "simple-git-hooks.json",
|
|
@@ -180,10 +166,12 @@ function surveyGitHooks(dir, undetermined) {
|
|
|
180
166
|
}
|
|
181
167
|
function surveyWorkflows(dir, undetermined) {
|
|
182
168
|
const workflowsDir = join(dir, ".github", "workflows");
|
|
183
|
-
if (!
|
|
169
|
+
if (!guardedExists(workflowsDir, undetermined)) {
|
|
184
170
|
return { files: [], needsReading: false };
|
|
185
171
|
}
|
|
186
|
-
const
|
|
172
|
+
const names = guardedRead(workflowsDir, () => readdirSync(workflowsDir), undetermined) ??
|
|
173
|
+
[];
|
|
174
|
+
const files = names.filter((name) => /\.ya?ml$/.test(name));
|
|
187
175
|
if (files.length > 0) {
|
|
188
176
|
undetermined.push(`${files.length} workflow file(s) under .github/workflows -- their job steps need reading, not parsing`);
|
|
189
177
|
}
|
|
@@ -200,15 +188,30 @@ function surveyScripts(packageJson) {
|
|
|
200
188
|
}
|
|
201
189
|
return result;
|
|
202
190
|
}
|
|
203
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Surveys toolchain enforcement at `dir`. Appends anything it could not
|
|
193
|
+
* parse, or could not read (`EACCES`/`EPERM`, a dangling symlink, a symlink
|
|
194
|
+
* loop, a directory where a file was expected), to `undetermined`; any other
|
|
195
|
+
* read failure throws, naming the path, with the original failure as
|
|
196
|
+
* `cause`.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* import { surveyToolchain } from "./survey-toolchain.js";
|
|
201
|
+
*
|
|
202
|
+
* const undetermined: string[] = [];
|
|
203
|
+
* const toolchain = surveyToolchain("/path/to/project", undetermined);
|
|
204
|
+
* console.log(toolchain.tsconfig.effectiveFlags, undetermined);
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
204
207
|
export function surveyToolchain(dir, undetermined) {
|
|
205
|
-
const packageJson = readPackageJson(dir);
|
|
208
|
+
const packageJson = readPackageJson(dir, undetermined);
|
|
206
209
|
const scripts = surveyScripts(packageJson);
|
|
207
210
|
return {
|
|
208
211
|
tsconfig: surveyTsconfig(dir, undetermined),
|
|
209
|
-
eslint: surveyEslint(dir),
|
|
210
|
-
testRunner: surveyTestRunner(dir, scripts),
|
|
211
|
-
formatter: surveyFormatter(dir),
|
|
212
|
+
eslint: surveyEslint(dir, undetermined),
|
|
213
|
+
testRunner: surveyTestRunner(dir, scripts, undetermined),
|
|
214
|
+
formatter: surveyFormatter(dir, undetermined),
|
|
212
215
|
gitHooks: surveyGitHooks(dir, undetermined),
|
|
213
216
|
workflows: surveyWorkflows(dir, undetermined),
|
|
214
217
|
scripts,
|