issue-flow 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/{analyze-RBNKXWL5.js → analyze-UZYJAWAP.js} +6 -6
- package/dist/{chunk-J4SICTAN.js → chunk-2LNQL4XH.js} +53 -9
- package/dist/chunk-2LNQL4XH.js.map +1 -0
- package/dist/{chunk-A4M6AKIK.js → chunk-2Z7OEZZT.js} +6 -6
- package/dist/{chunk-F52S6ZGF.js → chunk-652DJBCN.js} +17 -3
- package/dist/chunk-652DJBCN.js.map +1 -0
- package/dist/{chunk-TGP6WK65.js → chunk-67ZO5ROQ.js} +75 -11
- package/dist/chunk-67ZO5ROQ.js.map +1 -0
- package/dist/{chunk-VSF2CR3G.js → chunk-EWA3OGCM.js} +6 -6
- package/dist/{chunk-VPDWTZ3F.js → chunk-EZMOEBLU.js} +2 -2
- package/dist/{chunk-JWZKV3CL.js → chunk-MXTGCD5C.js} +2 -2
- package/dist/{chunk-QO2MWMAZ.js → chunk-PPLIXZI5.js} +33 -10
- package/dist/chunk-PPLIXZI5.js.map +1 -0
- package/dist/{chunk-HSGTZ3CT.js → chunk-S67Q4I2O.js} +219 -3
- package/dist/chunk-S67Q4I2O.js.map +1 -0
- package/dist/chunk-SP7DD4ID.js +265 -0
- package/dist/chunk-SP7DD4ID.js.map +1 -0
- package/dist/{chunk-36ECROTC.js → chunk-UH4KAJBJ.js} +7 -7
- package/dist/{chunk-RQW5W4XC.js → chunk-UUY4PCWW.js} +2 -2
- package/dist/{chunk-BH7DYADV.js → chunk-V5OHFSW4.js} +319 -181
- package/dist/chunk-V5OHFSW4.js.map +1 -0
- package/dist/{chunk-J3PJBZXJ.js → chunk-VOKNTBOO.js} +3 -3
- package/dist/{chunk-BASRRXAM.js → chunk-VYFKOUUR.js} +222 -6
- package/dist/chunk-VYFKOUUR.js.map +1 -0
- package/dist/cli.js +98 -29
- package/dist/cli.js.map +1 -1
- package/dist/{config-R7O5YC65.js → config-YCF3HIKR.js} +7 -3
- package/dist/execute-QYXA225W.js +14 -0
- package/dist/{generate-TGJQLGMV.js → generate-TW6QKSW5.js} +6 -6
- package/dist/init-G57UXKDG.js +9 -0
- package/dist/plan-YJLGBYBQ.js +16 -0
- package/dist/policy-NP4ZAVF6.js +1013 -0
- package/dist/policy-NP4ZAVF6.js.map +1 -0
- package/dist/pr-V5H65MRW.js +20 -0
- package/dist/pr-review-RCMXKXVP.js +16 -0
- package/dist/prd-3XR5KYJS.js +16 -0
- package/dist/review-ZZG3CSUO.js +14 -0
- package/dist/run-DIBC3YKX.js +1860 -0
- package/dist/run-DIBC3YKX.js.map +1 -0
- package/dist/{web-JVPK2PIF.js → web-O2FVOYZW.js} +4 -4
- package/package.json +1 -1
- package/prompts/execute.md +2 -2
- package/prompts/plan.md +10 -1
- package/prompts/pr.md +9 -4
- package/web/CLAUDE.md +22 -8
- package/web/public/app.css +178 -0
- package/web/public/app.js +336 -17
- package/web/public/index.html +24 -0
- package/dist/chunk-BASRRXAM.js.map +0 -1
- package/dist/chunk-BH7DYADV.js.map +0 -1
- package/dist/chunk-F52S6ZGF.js.map +0 -1
- package/dist/chunk-HSGTZ3CT.js.map +0 -1
- package/dist/chunk-J4SICTAN.js.map +0 -1
- package/dist/chunk-MK43N4X3.js +0 -123
- package/dist/chunk-MK43N4X3.js.map +0 -1
- package/dist/chunk-QO2MWMAZ.js.map +0 -1
- package/dist/chunk-TGP6WK65.js.map +0 -1
- package/dist/execute-LU3HWA54.js +0 -14
- package/dist/init-WP7Z2LFQ.js +0 -9
- package/dist/plan-2MWMCKRY.js +0 -16
- package/dist/pr-XVQDEQ7B.js +0 -16
- package/dist/pr-review-VO6ZXAFM.js +0 -16
- package/dist/prd-FPZAHQEO.js +0 -16
- package/dist/review-AHALLMXO.js +0 -14
- package/dist/run-2CXVR3VZ.js +0 -733
- package/dist/run-2CXVR3VZ.js.map +0 -1
- /package/dist/{analyze-RBNKXWL5.js.map → analyze-UZYJAWAP.js.map} +0 -0
- /package/dist/{chunk-A4M6AKIK.js.map → chunk-2Z7OEZZT.js.map} +0 -0
- /package/dist/{chunk-VSF2CR3G.js.map → chunk-EWA3OGCM.js.map} +0 -0
- /package/dist/{chunk-VPDWTZ3F.js.map → chunk-EZMOEBLU.js.map} +0 -0
- /package/dist/{chunk-JWZKV3CL.js.map → chunk-MXTGCD5C.js.map} +0 -0
- /package/dist/{chunk-36ECROTC.js.map → chunk-UH4KAJBJ.js.map} +0 -0
- /package/dist/{chunk-RQW5W4XC.js.map → chunk-UUY4PCWW.js.map} +0 -0
- /package/dist/{chunk-J3PJBZXJ.js.map → chunk-VOKNTBOO.js.map} +0 -0
- /package/dist/{config-R7O5YC65.js.map → config-YCF3HIKR.js.map} +0 -0
- /package/dist/{execute-LU3HWA54.js.map → execute-QYXA225W.js.map} +0 -0
- /package/dist/{generate-TGJQLGMV.js.map → generate-TW6QKSW5.js.map} +0 -0
- /package/dist/{init-WP7Z2LFQ.js.map → init-G57UXKDG.js.map} +0 -0
- /package/dist/{plan-2MWMCKRY.js.map → plan-YJLGBYBQ.js.map} +0 -0
- /package/dist/{pr-XVQDEQ7B.js.map → pr-V5H65MRW.js.map} +0 -0
- /package/dist/{pr-review-VO6ZXAFM.js.map → pr-review-RCMXKXVP.js.map} +0 -0
- /package/dist/{prd-FPZAHQEO.js.map → prd-3XR5KYJS.js.map} +0 -0
- /package/dist/{review-AHALLMXO.js.map → review-ZZG3CSUO.js.map} +0 -0
- /package/dist/{web-JVPK2PIF.js.map → web-O2FVOYZW.js.map} +0 -0
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getProjectRoot,
|
|
4
|
+
loadPolicyConfig,
|
|
5
|
+
mergeConfigLayers,
|
|
6
|
+
run
|
|
7
|
+
} from "./chunk-VYFKOUUR.js";
|
|
8
|
+
import {
|
|
9
|
+
printError,
|
|
10
|
+
printInfo,
|
|
11
|
+
printWarning
|
|
12
|
+
} from "./chunk-V5OHFSW4.js";
|
|
13
|
+
|
|
14
|
+
// src/policy/discovery.ts
|
|
15
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
16
|
+
import { join, relative, sep } from "path";
|
|
17
|
+
|
|
18
|
+
// src/policy/parsers/docs.ts
|
|
19
|
+
import { posix } from "path";
|
|
20
|
+
function classifyDocument(fileName) {
|
|
21
|
+
switch (fileName.toLowerCase()) {
|
|
22
|
+
case "agents.md":
|
|
23
|
+
return "agents";
|
|
24
|
+
case "claude.md":
|
|
25
|
+
return "claude";
|
|
26
|
+
case "contributing.md":
|
|
27
|
+
return "contributing";
|
|
28
|
+
case "code_of_conduct.md":
|
|
29
|
+
case "code-of-conduct.md":
|
|
30
|
+
return "code-of-conduct";
|
|
31
|
+
default:
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
var MAX_REFERENCED_DOCUMENTS = 20;
|
|
36
|
+
var MARKDOWN_LINK = /\[[^\]\n]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
37
|
+
function extractReferencedDocuments(content, fromPath) {
|
|
38
|
+
const fromDir = posix.dirname(fromPath.replace(/\\/g, "/"));
|
|
39
|
+
const found = [];
|
|
40
|
+
for (const match of content.matchAll(MARKDOWN_LINK)) {
|
|
41
|
+
const raw = match[1];
|
|
42
|
+
if (raw === void 0) continue;
|
|
43
|
+
const target = raw.split("#")[0]?.split("?")[0]?.trim() ?? "";
|
|
44
|
+
if (target === "") continue;
|
|
45
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(target) || target.startsWith("//")) continue;
|
|
46
|
+
if (!/\.(md|markdown)$/i.test(target)) continue;
|
|
47
|
+
const resolved = posix.normalize(
|
|
48
|
+
target.startsWith("/") ? target.slice(1) : fromDir === "." ? target : posix.join(fromDir, target)
|
|
49
|
+
);
|
|
50
|
+
if (resolved.startsWith("..") || resolved === "" || resolved === ".") continue;
|
|
51
|
+
if (resolved === fromPath || found.includes(resolved)) continue;
|
|
52
|
+
found.push(resolved);
|
|
53
|
+
if (found.length >= MAX_REFERENCED_DOCUMENTS) break;
|
|
54
|
+
}
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/policy/parsers/labels.ts
|
|
59
|
+
function parseLabels(payload) {
|
|
60
|
+
if (!Array.isArray(payload)) return [];
|
|
61
|
+
const labels = [];
|
|
62
|
+
for (const entry of payload) {
|
|
63
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
64
|
+
const record = entry;
|
|
65
|
+
const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
66
|
+
if (name === "") continue;
|
|
67
|
+
const description = typeof record.description === "string" ? record.description.trim() : "";
|
|
68
|
+
const color = typeof record.color === "string" ? record.color.trim() : "";
|
|
69
|
+
labels.push({
|
|
70
|
+
name,
|
|
71
|
+
description: description === "" ? null : description,
|
|
72
|
+
color: color === "" ? null : color
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return labels;
|
|
76
|
+
}
|
|
77
|
+
function parseIssueTypes(payload) {
|
|
78
|
+
const entries = Array.isArray(payload) ? payload : Array.isArray(payload?.issue_types) ? payload.issue_types : [];
|
|
79
|
+
const types = [];
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
const name = typeof entry === "string" ? entry : entry !== null && typeof entry === "object" ? entry.name : void 0;
|
|
82
|
+
if (typeof name !== "string") continue;
|
|
83
|
+
const trimmed = name.trim();
|
|
84
|
+
if (trimmed !== "" && !types.includes(trimmed)) {
|
|
85
|
+
types.push(trimmed);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return types;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/policy/parsers/templates.ts
|
|
92
|
+
import { basename, extname } from "path";
|
|
93
|
+
|
|
94
|
+
// src/policy/parsers/issue-forms.ts
|
|
95
|
+
function emptyTemplateMetadata() {
|
|
96
|
+
return { name: null, about: null, title: null, labels: [], type: null, assignees: [] };
|
|
97
|
+
}
|
|
98
|
+
var TOP_LEVEL_KEY = /^([A-Za-z_][A-Za-z0-9_-]*):(?:\s+(.*))?$/;
|
|
99
|
+
function topLevelRegions(source) {
|
|
100
|
+
const regions = /* @__PURE__ */ new Map();
|
|
101
|
+
const lines = source.split(/\r?\n/);
|
|
102
|
+
let current = null;
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
const match = /^[^\s#-]/.test(line) ? TOP_LEVEL_KEY.exec(line) : null;
|
|
105
|
+
if (match?.[1] !== void 0) {
|
|
106
|
+
current = { key: match[1], inline: (match[2] ?? "").trim(), block: [] };
|
|
107
|
+
if (!regions.has(current.key)) {
|
|
108
|
+
regions.set(current.key, current);
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
current?.block.push(line);
|
|
113
|
+
}
|
|
114
|
+
return regions;
|
|
115
|
+
}
|
|
116
|
+
function stripComment(value) {
|
|
117
|
+
let quote = null;
|
|
118
|
+
for (let i = 0; i < value.length; i++) {
|
|
119
|
+
const char = value[i];
|
|
120
|
+
if (quote !== null) {
|
|
121
|
+
if (char === quote) quote = null;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (char === '"' || char === "'") {
|
|
125
|
+
quote = char;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (char === "#" && (i === 0 || /\s/.test(value[i - 1] ?? ""))) {
|
|
129
|
+
return value.slice(0, i);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
function unquote(value) {
|
|
135
|
+
const trimmed = value.trim();
|
|
136
|
+
if (trimmed.length >= 2) {
|
|
137
|
+
const first = trimmed[0];
|
|
138
|
+
const last = trimmed[trimmed.length - 1];
|
|
139
|
+
if (first === '"' && last === '"') {
|
|
140
|
+
return trimmed.slice(1, -1).replace(/\\(["\\])/g, "$1");
|
|
141
|
+
}
|
|
142
|
+
if (first === "'" && last === "'") {
|
|
143
|
+
return trimmed.slice(1, -1).replace(/''/g, "'");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return trimmed;
|
|
147
|
+
}
|
|
148
|
+
function splitFlow(inner) {
|
|
149
|
+
const parts = [];
|
|
150
|
+
let buffer = "";
|
|
151
|
+
let quote = null;
|
|
152
|
+
for (const char of inner) {
|
|
153
|
+
if (quote !== null) {
|
|
154
|
+
if (char === quote) quote = null;
|
|
155
|
+
buffer += char;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (char === '"' || char === "'") {
|
|
159
|
+
quote = char;
|
|
160
|
+
buffer += char;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (char === ",") {
|
|
164
|
+
parts.push(buffer);
|
|
165
|
+
buffer = "";
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
buffer += char;
|
|
169
|
+
}
|
|
170
|
+
parts.push(buffer);
|
|
171
|
+
return parts.map(unquote).filter((part) => part !== "");
|
|
172
|
+
}
|
|
173
|
+
function readScalar(region) {
|
|
174
|
+
if (region === void 0) return null;
|
|
175
|
+
const inline = stripComment(region.inline).trim();
|
|
176
|
+
if (inline !== "" && inline !== "|" && inline !== ">" && inline !== "|-" && inline !== ">-") {
|
|
177
|
+
const value = unquote(inline);
|
|
178
|
+
return value === "" ? null : value;
|
|
179
|
+
}
|
|
180
|
+
const collected = region.block.filter((line) => line.trim() !== "").map((line) => line.trim()).join(inline.startsWith(">") ? " " : "\n").trim();
|
|
181
|
+
return collected === "" ? null : collected;
|
|
182
|
+
}
|
|
183
|
+
function readSequence(region) {
|
|
184
|
+
if (region === void 0) return [];
|
|
185
|
+
const inline = stripComment(region.inline).trim();
|
|
186
|
+
if (inline.startsWith("[")) {
|
|
187
|
+
const closing = inline.lastIndexOf("]");
|
|
188
|
+
return splitFlow(inline.slice(1, closing === -1 ? void 0 : closing));
|
|
189
|
+
}
|
|
190
|
+
if (inline !== "") {
|
|
191
|
+
const single = unquote(inline);
|
|
192
|
+
return single === "" ? [] : [single];
|
|
193
|
+
}
|
|
194
|
+
const items = [];
|
|
195
|
+
for (const line of region.block) {
|
|
196
|
+
const trimmed = line.trim();
|
|
197
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
198
|
+
if (!trimmed.startsWith("- ") && trimmed !== "-") break;
|
|
199
|
+
const value = unquote(stripComment(trimmed.slice(1)).trim());
|
|
200
|
+
if (value === "" || /^[A-Za-z_][A-Za-z0-9_-]*:\s/.test(value)) continue;
|
|
201
|
+
items.push(value);
|
|
202
|
+
}
|
|
203
|
+
return items;
|
|
204
|
+
}
|
|
205
|
+
function parseTemplateMetadata(source) {
|
|
206
|
+
const regions = topLevelRegions(source);
|
|
207
|
+
return {
|
|
208
|
+
name: readScalar(regions.get("name")),
|
|
209
|
+
// Issue Forms spell it `description`, legacy markdown templates `about`.
|
|
210
|
+
about: readScalar(regions.get("about")) ?? readScalar(regions.get("description")),
|
|
211
|
+
title: readScalar(regions.get("title")),
|
|
212
|
+
labels: readSequence(regions.get("labels")),
|
|
213
|
+
type: readScalar(regions.get("type")),
|
|
214
|
+
assignees: readSequence(regions.get("assignees"))
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function extractFrontMatter(source) {
|
|
218
|
+
const normalized = source.replace(/^\uFEFF/, "");
|
|
219
|
+
if (!/^---[ \t]*\r?\n/.test(normalized)) return null;
|
|
220
|
+
const rest = normalized.slice(normalized.indexOf("\n") + 1);
|
|
221
|
+
const closing = /^---[ \t]*$/m.exec(rest);
|
|
222
|
+
if (closing?.index === void 0) return null;
|
|
223
|
+
return rest.slice(0, closing.index);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/policy/parsers/templates.ts
|
|
227
|
+
function parseIssueTemplateFile(relPath, content) {
|
|
228
|
+
const extension = extname(relPath).toLowerCase();
|
|
229
|
+
const isForm = extension === ".yml" || extension === ".yaml";
|
|
230
|
+
const metadataSource = isForm ? content : extractFrontMatter(content);
|
|
231
|
+
const metadata = metadataSource === null ? emptyTemplateMetadata() : parseTemplateMetadata(metadataSource);
|
|
232
|
+
return {
|
|
233
|
+
path: relPath,
|
|
234
|
+
format: isForm ? "form" : "markdown",
|
|
235
|
+
origin: "filesystem",
|
|
236
|
+
// A template with no `name:` is still addressable by its file name, which
|
|
237
|
+
// is what GitHub falls back to for legacy markdown templates.
|
|
238
|
+
name: metadata.name ?? basename(relPath, extension),
|
|
239
|
+
about: metadata.about,
|
|
240
|
+
title: metadata.title,
|
|
241
|
+
labels: metadata.labels,
|
|
242
|
+
type: metadata.type,
|
|
243
|
+
assignees: metadata.assignees,
|
|
244
|
+
content
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function asString(value) {
|
|
248
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
249
|
+
}
|
|
250
|
+
function isRecord(value) {
|
|
251
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
252
|
+
}
|
|
253
|
+
function asNameList(value, key) {
|
|
254
|
+
const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.nodes) ? value.nodes : [];
|
|
255
|
+
return entries.map(
|
|
256
|
+
(entry) => typeof entry === "string" ? entry : asString(isRecord(entry) ? entry[key] : null)
|
|
257
|
+
).filter((entry) => entry !== null && entry !== "");
|
|
258
|
+
}
|
|
259
|
+
function parseOrganizationTemplates(payload) {
|
|
260
|
+
const repository = isRecord(payload) && isRecord(payload.data) ? payload.data.repository : null;
|
|
261
|
+
const entries = isRecord(repository) ? repository.issueTemplates : null;
|
|
262
|
+
if (!Array.isArray(entries)) return [];
|
|
263
|
+
const templates = [];
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
if (!isRecord(entry)) continue;
|
|
266
|
+
const name = asString(entry.name);
|
|
267
|
+
if (name === null) continue;
|
|
268
|
+
templates.push({
|
|
269
|
+
// The filename is the stable identity; the display name is not unique.
|
|
270
|
+
path: asString(entry.filename) ?? name,
|
|
271
|
+
format: "markdown",
|
|
272
|
+
origin: "organization",
|
|
273
|
+
name,
|
|
274
|
+
about: asString(entry.about),
|
|
275
|
+
title: asString(entry.title),
|
|
276
|
+
labels: asNameList(entry.labels, "name"),
|
|
277
|
+
// GraphQL exposes no Issue Type on a template, so it stays unknown here
|
|
278
|
+
// rather than being guessed from the name.
|
|
279
|
+
type: null,
|
|
280
|
+
assignees: asNameList(entry.assignees, "login"),
|
|
281
|
+
content: asString(entry.body) ?? ""
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return templates;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/policy/types.ts
|
|
288
|
+
var POLICY_SCHEMA_VERSION = 1;
|
|
289
|
+
var MAX_POLICY_DOCUMENT_BYTES = 64 * 1024;
|
|
290
|
+
|
|
291
|
+
// src/policy/discovery.ts
|
|
292
|
+
var DISCOVERY_TIMEOUT_MS = 1e4;
|
|
293
|
+
var POLICY_DIRECTORIES = [".github", "", "docs"];
|
|
294
|
+
var defaultExec = (command, args, options) => run(command, args, { cwd: options.cwd, timeout: options.timeout });
|
|
295
|
+
function toRelativePath(root, absolute) {
|
|
296
|
+
return relative(root, absolute).split(sep).join("/");
|
|
297
|
+
}
|
|
298
|
+
async function listDirectory(dir) {
|
|
299
|
+
try {
|
|
300
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
301
|
+
return entries.filter((entry) => !entry.isDirectory()).map((entry) => entry.name);
|
|
302
|
+
} catch {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function isDirectory(path) {
|
|
307
|
+
try {
|
|
308
|
+
return (await stat(path)).isDirectory();
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function readCapped(path) {
|
|
314
|
+
let raw;
|
|
315
|
+
try {
|
|
316
|
+
raw = await readFile(path, "utf-8");
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
if (Buffer.byteLength(raw, "utf-8") <= MAX_POLICY_DOCUMENT_BYTES) {
|
|
321
|
+
return { content: raw, truncated: false };
|
|
322
|
+
}
|
|
323
|
+
const clipped = Buffer.from(raw, "utf-8").subarray(0, MAX_POLICY_DOCUMENT_BYTES).toString("utf-8");
|
|
324
|
+
return { content: clipped, truncated: true };
|
|
325
|
+
}
|
|
326
|
+
async function findFile(dir, names) {
|
|
327
|
+
const entries = await listDirectory(dir);
|
|
328
|
+
const wanted = names.map((name) => name.toLowerCase());
|
|
329
|
+
for (const name of wanted) {
|
|
330
|
+
const hit = entries.find((entry) => entry.toLowerCase() === name);
|
|
331
|
+
if (hit !== void 0) return hit;
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
var TEMPLATE_EXTENSIONS = [".yml", ".yaml", ".md", ".markdown"];
|
|
336
|
+
function isTemplateFile(name) {
|
|
337
|
+
const lower = name.toLowerCase();
|
|
338
|
+
if (lower === "config.yml" || lower === "config.yaml") return false;
|
|
339
|
+
return TEMPLATE_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
340
|
+
}
|
|
341
|
+
async function discoverIssueTemplates(root) {
|
|
342
|
+
const templates = [];
|
|
343
|
+
const sources = [];
|
|
344
|
+
for (const base of POLICY_DIRECTORIES) {
|
|
345
|
+
const baseDir = base === "" ? root : join(root, base);
|
|
346
|
+
const templateDir = await findDirectory(baseDir, "ISSUE_TEMPLATE") ?? null;
|
|
347
|
+
if (templateDir !== null) {
|
|
348
|
+
const names = (await listDirectory(templateDir)).filter(isTemplateFile).sort();
|
|
349
|
+
for (const name of names) {
|
|
350
|
+
const filePath = join(templateDir, name);
|
|
351
|
+
const file = await readCapped(filePath);
|
|
352
|
+
if (file === null) continue;
|
|
353
|
+
const relPath = toRelativePath(root, filePath);
|
|
354
|
+
templates.push(parseIssueTemplateFile(relPath, file.content));
|
|
355
|
+
sources.push({
|
|
356
|
+
kind: "issue-templates",
|
|
357
|
+
origin: "filesystem",
|
|
358
|
+
path: relPath,
|
|
359
|
+
status: "found",
|
|
360
|
+
detail: file.truncated ? "content truncated" : null
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const singleFile = await findFile(baseDir, [
|
|
365
|
+
"ISSUE_TEMPLATE.md",
|
|
366
|
+
"ISSUE_TEMPLATE.yml",
|
|
367
|
+
"ISSUE_TEMPLATE.yaml",
|
|
368
|
+
"ISSUE_TEMPLATE.markdown"
|
|
369
|
+
]);
|
|
370
|
+
if (singleFile !== null) {
|
|
371
|
+
const filePath = join(baseDir, singleFile);
|
|
372
|
+
const file = await readCapped(filePath);
|
|
373
|
+
if (file !== null) {
|
|
374
|
+
const relPath = toRelativePath(root, filePath);
|
|
375
|
+
templates.push(parseIssueTemplateFile(relPath, file.content));
|
|
376
|
+
sources.push({
|
|
377
|
+
kind: "issue-templates",
|
|
378
|
+
origin: "filesystem",
|
|
379
|
+
path: relPath,
|
|
380
|
+
status: "found",
|
|
381
|
+
detail: file.truncated ? "content truncated" : null
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return { templates, sources };
|
|
387
|
+
}
|
|
388
|
+
async function findDirectory(parent, name) {
|
|
389
|
+
let entries;
|
|
390
|
+
try {
|
|
391
|
+
entries = (await readdir(parent, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
const hit = entries.find((entry) => entry.toLowerCase() === name.toLowerCase());
|
|
396
|
+
return hit === void 0 ? null : join(parent, hit);
|
|
397
|
+
}
|
|
398
|
+
var PR_TEMPLATE_FILES = [
|
|
399
|
+
"PULL_REQUEST_TEMPLATE.md",
|
|
400
|
+
"PULL_REQUEST_TEMPLATE.markdown",
|
|
401
|
+
"PULL_REQUEST_TEMPLATE.txt",
|
|
402
|
+
"PULL_REQUEST_TEMPLATE"
|
|
403
|
+
];
|
|
404
|
+
async function discoverPullRequestTemplates(root) {
|
|
405
|
+
const templates = [];
|
|
406
|
+
const sources = [];
|
|
407
|
+
const collect = async (filePath) => {
|
|
408
|
+
const file = await readCapped(filePath);
|
|
409
|
+
if (file === null) return;
|
|
410
|
+
const relPath = toRelativePath(root, filePath);
|
|
411
|
+
templates.push({
|
|
412
|
+
path: relPath,
|
|
413
|
+
name: relPath.split("/").pop() ?? relPath,
|
|
414
|
+
content: file.content
|
|
415
|
+
});
|
|
416
|
+
sources.push({
|
|
417
|
+
kind: "pull-request-template",
|
|
418
|
+
origin: "filesystem",
|
|
419
|
+
path: relPath,
|
|
420
|
+
status: "found",
|
|
421
|
+
detail: file.truncated ? "content truncated" : null
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
for (const base of POLICY_DIRECTORIES) {
|
|
425
|
+
const baseDir = base === "" ? root : join(root, base);
|
|
426
|
+
const single = await findFile(baseDir, PR_TEMPLATE_FILES);
|
|
427
|
+
if (single !== null && !await isDirectory(join(baseDir, single))) {
|
|
428
|
+
await collect(join(baseDir, single));
|
|
429
|
+
}
|
|
430
|
+
const templateDir = await findDirectory(baseDir, "PULL_REQUEST_TEMPLATE");
|
|
431
|
+
if (templateDir !== null) {
|
|
432
|
+
for (const name of (await listDirectory(templateDir)).sort()) {
|
|
433
|
+
if (!/\.(md|markdown|txt)$/i.test(name)) continue;
|
|
434
|
+
await collect(join(templateDir, name));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return { templates, sources };
|
|
439
|
+
}
|
|
440
|
+
async function discoverCodeowners(root) {
|
|
441
|
+
for (const base of POLICY_DIRECTORIES) {
|
|
442
|
+
const baseDir = base === "" ? root : join(root, base);
|
|
443
|
+
const name = await findFile(baseDir, ["CODEOWNERS"]);
|
|
444
|
+
if (name === null) continue;
|
|
445
|
+
const file = await readCapped(join(baseDir, name));
|
|
446
|
+
if (file === null) continue;
|
|
447
|
+
const relPath = toRelativePath(root, join(baseDir, name));
|
|
448
|
+
return {
|
|
449
|
+
content: file.content,
|
|
450
|
+
sources: [
|
|
451
|
+
{
|
|
452
|
+
kind: "codeowners",
|
|
453
|
+
origin: "filesystem",
|
|
454
|
+
path: relPath,
|
|
455
|
+
status: "found",
|
|
456
|
+
detail: file.truncated ? "content truncated" : null
|
|
457
|
+
}
|
|
458
|
+
]
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return { content: null, sources: [] };
|
|
462
|
+
}
|
|
463
|
+
var ROOT_DOCUMENT_FILES = ["CONTRIBUTING.md", "CODE_OF_CONDUCT.md"];
|
|
464
|
+
var SCOPED_DOCUMENT_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
465
|
+
function scopeLadder(scope) {
|
|
466
|
+
if (scope === null || scope === "") return [""];
|
|
467
|
+
const segments = scope.split("/").filter((segment) => segment !== "");
|
|
468
|
+
const ladder = [""];
|
|
469
|
+
let current = "";
|
|
470
|
+
for (const segment of segments) {
|
|
471
|
+
current = current === "" ? segment : `${current}/${segment}`;
|
|
472
|
+
ladder.push(current);
|
|
473
|
+
}
|
|
474
|
+
return ladder;
|
|
475
|
+
}
|
|
476
|
+
async function discoverDocuments(root, scope) {
|
|
477
|
+
const documents = [];
|
|
478
|
+
const sources = [];
|
|
479
|
+
const seen = /* @__PURE__ */ new Set();
|
|
480
|
+
const collect = async (filePath, kind, docScope, referencedFrom) => {
|
|
481
|
+
const relPath = toRelativePath(root, filePath);
|
|
482
|
+
if (seen.has(relPath)) return null;
|
|
483
|
+
const file = await readCapped(filePath);
|
|
484
|
+
if (file === null) return null;
|
|
485
|
+
seen.add(relPath);
|
|
486
|
+
const document = {
|
|
487
|
+
path: relPath,
|
|
488
|
+
kind,
|
|
489
|
+
scope: docScope,
|
|
490
|
+
referencedFrom,
|
|
491
|
+
content: file.content
|
|
492
|
+
};
|
|
493
|
+
documents.push(document);
|
|
494
|
+
sources.push({
|
|
495
|
+
kind: "docs",
|
|
496
|
+
origin: "filesystem",
|
|
497
|
+
path: relPath,
|
|
498
|
+
status: "found",
|
|
499
|
+
detail: file.truncated ? "content truncated" : null
|
|
500
|
+
});
|
|
501
|
+
return document;
|
|
502
|
+
};
|
|
503
|
+
for (const base of POLICY_DIRECTORIES) {
|
|
504
|
+
const baseDir = base === "" ? root : join(root, base);
|
|
505
|
+
for (const wanted of ROOT_DOCUMENT_FILES) {
|
|
506
|
+
const name = await findFile(baseDir, [wanted]);
|
|
507
|
+
if (name === null) continue;
|
|
508
|
+
const kind = classifyDocument(name);
|
|
509
|
+
if (kind === null) continue;
|
|
510
|
+
await collect(join(baseDir, name), kind, "", null);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const indexes = [];
|
|
514
|
+
for (const level of scopeLadder(scope)) {
|
|
515
|
+
const levelDir = level === "" ? root : join(root, level);
|
|
516
|
+
for (const wanted of SCOPED_DOCUMENT_FILES) {
|
|
517
|
+
const name = await findFile(levelDir, [wanted]);
|
|
518
|
+
if (name === null) continue;
|
|
519
|
+
const kind = classifyDocument(name);
|
|
520
|
+
if (kind === null) continue;
|
|
521
|
+
const document = await collect(join(levelDir, name), kind, level, null);
|
|
522
|
+
if (document !== null && kind === "agents") {
|
|
523
|
+
indexes.push(document);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
for (const index of indexes) {
|
|
528
|
+
for (const target of extractReferencedDocuments(index.content, index.path)) {
|
|
529
|
+
await collect(join(root, target), "referenced", index.scope, index.path);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return { documents, sources };
|
|
533
|
+
}
|
|
534
|
+
async function discoverBaseBranch(root, exec = defaultExec) {
|
|
535
|
+
const options = { cwd: root, timeout: DISCOVERY_TIMEOUT_MS };
|
|
536
|
+
try {
|
|
537
|
+
const remoteHead = await exec(
|
|
538
|
+
"git",
|
|
539
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
540
|
+
options
|
|
541
|
+
);
|
|
542
|
+
if (remoteHead.exitCode === 0) {
|
|
543
|
+
const name = remoteHead.stdout.trim().replace(/^origin\//, "");
|
|
544
|
+
if (name !== "") {
|
|
545
|
+
return {
|
|
546
|
+
baseBranch: name,
|
|
547
|
+
sources: [
|
|
548
|
+
{
|
|
549
|
+
kind: "base-branch",
|
|
550
|
+
origin: "git",
|
|
551
|
+
path: null,
|
|
552
|
+
status: "found",
|
|
553
|
+
detail: "origin/HEAD"
|
|
554
|
+
}
|
|
555
|
+
]
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
for (const candidate of ["main", "master"]) {
|
|
560
|
+
const check = await exec(
|
|
561
|
+
"git",
|
|
562
|
+
["rev-parse", "--verify", "--quiet", `refs/heads/${candidate}`],
|
|
563
|
+
options
|
|
564
|
+
);
|
|
565
|
+
if (check.exitCode === 0) {
|
|
566
|
+
return {
|
|
567
|
+
baseBranch: candidate,
|
|
568
|
+
sources: [
|
|
569
|
+
{
|
|
570
|
+
kind: "base-branch",
|
|
571
|
+
origin: "git",
|
|
572
|
+
path: null,
|
|
573
|
+
status: "found",
|
|
574
|
+
detail: `refs/heads/${candidate}`
|
|
575
|
+
}
|
|
576
|
+
]
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
} catch {
|
|
581
|
+
return {
|
|
582
|
+
baseBranch: null,
|
|
583
|
+
sources: [
|
|
584
|
+
{
|
|
585
|
+
kind: "base-branch",
|
|
586
|
+
origin: "git",
|
|
587
|
+
path: null,
|
|
588
|
+
status: "unavailable",
|
|
589
|
+
detail: "git could not be executed"
|
|
590
|
+
}
|
|
591
|
+
]
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
return { baseBranch: null, sources: [] };
|
|
595
|
+
}
|
|
596
|
+
async function discoverGitHubSlug(root, exec = defaultExec) {
|
|
597
|
+
let url = "";
|
|
598
|
+
try {
|
|
599
|
+
const result = await exec("git", ["remote", "get-url", "origin"], {
|
|
600
|
+
cwd: root,
|
|
601
|
+
timeout: DISCOVERY_TIMEOUT_MS
|
|
602
|
+
});
|
|
603
|
+
if (result.exitCode !== 0) return null;
|
|
604
|
+
url = result.stdout.trim();
|
|
605
|
+
} catch {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
if (url === "") return null;
|
|
609
|
+
const match = /[/:]([^/:\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(url);
|
|
610
|
+
if (match?.[1] === void 0 || match[2] === void 0) return null;
|
|
611
|
+
return { owner: match[1], repo: match[2] };
|
|
612
|
+
}
|
|
613
|
+
async function ghJson(root, args, exec) {
|
|
614
|
+
let result;
|
|
615
|
+
try {
|
|
616
|
+
result = await exec("gh", args, { cwd: root, timeout: DISCOVERY_TIMEOUT_MS });
|
|
617
|
+
} catch (err) {
|
|
618
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
619
|
+
}
|
|
620
|
+
if (result.exitCode !== 0) {
|
|
621
|
+
const stderr = result.stderr.trim().split("\n")[0] ?? "";
|
|
622
|
+
return { error: stderr === "" ? `gh exited with ${result.exitCode}` : stderr };
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
return { value: JSON.parse(result.stdout) };
|
|
626
|
+
} catch {
|
|
627
|
+
return { error: "gh returned unparseable JSON" };
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
var LABEL_PAGE_SIZE = 200;
|
|
631
|
+
async function discoverLabels(root, exec = defaultExec) {
|
|
632
|
+
const outcome = await ghJson(
|
|
633
|
+
root,
|
|
634
|
+
["label", "list", "--json", "name,description,color", "--limit", String(LABEL_PAGE_SIZE)],
|
|
635
|
+
exec
|
|
636
|
+
);
|
|
637
|
+
if ("error" in outcome) {
|
|
638
|
+
return {
|
|
639
|
+
labels: [],
|
|
640
|
+
sources: [
|
|
641
|
+
{ kind: "labels", origin: "gh", path: null, status: "unavailable", detail: outcome.error }
|
|
642
|
+
]
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
const labels = parseLabels(outcome.value);
|
|
646
|
+
return {
|
|
647
|
+
labels,
|
|
648
|
+
sources: [
|
|
649
|
+
{
|
|
650
|
+
kind: "labels",
|
|
651
|
+
origin: "gh",
|
|
652
|
+
path: null,
|
|
653
|
+
status: "found",
|
|
654
|
+
detail: `${labels.length} label(s)`
|
|
655
|
+
}
|
|
656
|
+
]
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
async function discoverIssueTypes(root, owner, exec = defaultExec) {
|
|
660
|
+
if (owner === null || owner === "") {
|
|
661
|
+
return { types: [], sources: [] };
|
|
662
|
+
}
|
|
663
|
+
const outcome = await ghJson(root, ["api", `orgs/${owner}/issue-types`], exec);
|
|
664
|
+
if ("error" in outcome) {
|
|
665
|
+
return {
|
|
666
|
+
types: [],
|
|
667
|
+
sources: [
|
|
668
|
+
{
|
|
669
|
+
kind: "issue-types",
|
|
670
|
+
origin: "gh",
|
|
671
|
+
path: null,
|
|
672
|
+
// A personal account (404) and a plan without Issue Types (403) are
|
|
673
|
+
// the common answers here, so the detail carries what gh said.
|
|
674
|
+
status: "unavailable",
|
|
675
|
+
detail: outcome.error
|
|
676
|
+
}
|
|
677
|
+
]
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
const types = parseIssueTypes(outcome.value);
|
|
681
|
+
return {
|
|
682
|
+
types,
|
|
683
|
+
sources: [
|
|
684
|
+
{
|
|
685
|
+
kind: "issue-types",
|
|
686
|
+
origin: "gh",
|
|
687
|
+
path: null,
|
|
688
|
+
status: "found",
|
|
689
|
+
detail: `${types.length} type(s)`
|
|
690
|
+
}
|
|
691
|
+
]
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
var ORGANIZATION_TEMPLATES_QUERY = `query($owner:String!,$name:String!){
|
|
695
|
+
repository(owner:$owner,name:$name){
|
|
696
|
+
issueTemplates{
|
|
697
|
+
name
|
|
698
|
+
title
|
|
699
|
+
about
|
|
700
|
+
filename
|
|
701
|
+
body
|
|
702
|
+
assignees(first:10){nodes{login}}
|
|
703
|
+
labels(first:20){nodes{name}}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}`;
|
|
707
|
+
async function discoverOrganizationTemplates(root, slug, exec = defaultExec) {
|
|
708
|
+
if (slug === null) {
|
|
709
|
+
return { templates: [], sources: [] };
|
|
710
|
+
}
|
|
711
|
+
const outcome = await ghJson(
|
|
712
|
+
root,
|
|
713
|
+
[
|
|
714
|
+
"api",
|
|
715
|
+
"graphql",
|
|
716
|
+
"-f",
|
|
717
|
+
`query=${ORGANIZATION_TEMPLATES_QUERY}`,
|
|
718
|
+
"-f",
|
|
719
|
+
`owner=${slug.owner}`,
|
|
720
|
+
"-f",
|
|
721
|
+
`name=${slug.repo}`
|
|
722
|
+
],
|
|
723
|
+
exec
|
|
724
|
+
);
|
|
725
|
+
if ("error" in outcome) {
|
|
726
|
+
return {
|
|
727
|
+
templates: [],
|
|
728
|
+
sources: [
|
|
729
|
+
{
|
|
730
|
+
kind: "issue-templates",
|
|
731
|
+
origin: "gh",
|
|
732
|
+
path: null,
|
|
733
|
+
status: "unavailable",
|
|
734
|
+
detail: outcome.error
|
|
735
|
+
}
|
|
736
|
+
]
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
const templates = parseOrganizationTemplates(outcome.value);
|
|
740
|
+
if (templates.length === 0) {
|
|
741
|
+
return { templates, sources: [] };
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
templates,
|
|
745
|
+
sources: templates.map((template) => ({
|
|
746
|
+
kind: "issue-templates",
|
|
747
|
+
origin: "gh",
|
|
748
|
+
path: null,
|
|
749
|
+
status: "found",
|
|
750
|
+
detail: `organization default "${template.name}"`
|
|
751
|
+
}))
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/policy/resolve.ts
|
|
756
|
+
import { isAbsolute, normalize, sep as sep2 } from "path";
|
|
757
|
+
function normalizeScope(scope) {
|
|
758
|
+
if (scope === null || scope === void 0) return null;
|
|
759
|
+
const trimmed = scope.trim();
|
|
760
|
+
if (trimmed === "" || trimmed === "." || isAbsolute(trimmed)) return null;
|
|
761
|
+
const normalized = normalize(trimmed).split(sep2).join("/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
762
|
+
if (normalized === "" || normalized === "." || normalized.startsWith("..")) return null;
|
|
763
|
+
return normalized;
|
|
764
|
+
}
|
|
765
|
+
function emptyPolicy(root, scope, enabled, sources) {
|
|
766
|
+
return {
|
|
767
|
+
schemaVersion: POLICY_SCHEMA_VERSION,
|
|
768
|
+
root,
|
|
769
|
+
scope,
|
|
770
|
+
enabled,
|
|
771
|
+
issues: { templates: [], types: [], labels: [], titleConvention: null },
|
|
772
|
+
pullRequests: { template: null, templates: [], baseBranch: null, titleConvention: null },
|
|
773
|
+
git: { branchConvention: null, commitConvention: null },
|
|
774
|
+
docs: [],
|
|
775
|
+
codeowners: null,
|
|
776
|
+
sources
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function disabledSource(kind) {
|
|
780
|
+
return {
|
|
781
|
+
kind,
|
|
782
|
+
origin: "config",
|
|
783
|
+
path: null,
|
|
784
|
+
status: "disabled",
|
|
785
|
+
detail: "disabled by configuration"
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
var DECLARATION_KINDS = {
|
|
789
|
+
issues: "issue-templates",
|
|
790
|
+
pullRequests: "pull-request-template",
|
|
791
|
+
git: "base-branch"
|
|
792
|
+
};
|
|
793
|
+
async function resolvePolicy(root, scope, options) {
|
|
794
|
+
const warn = options.warn ?? printWarning;
|
|
795
|
+
const config = await loadPolicyConfig({
|
|
796
|
+
projectRoot: root,
|
|
797
|
+
env: options.env,
|
|
798
|
+
cli: options.cli,
|
|
799
|
+
warn
|
|
800
|
+
});
|
|
801
|
+
if (!config.enabled) {
|
|
802
|
+
return emptyPolicy(root, scope, false, [
|
|
803
|
+
{ kind: "docs", origin: "config", path: null, status: "disabled", detail: "policy.enabled" }
|
|
804
|
+
]);
|
|
805
|
+
}
|
|
806
|
+
const toggles = config.discovery;
|
|
807
|
+
const [templateDiscovery, prTemplates, documents, codeowners] = await Promise.all([
|
|
808
|
+
toggles.issueTemplates ? discoverIssueTemplates(root) : Promise.resolve({ templates: [], sources: [disabledSource("issue-templates")] }),
|
|
809
|
+
toggles.pullRequestTemplate ? discoverPullRequestTemplates(root) : Promise.resolve({ templates: [], sources: [disabledSource("pull-request-template")] }),
|
|
810
|
+
toggles.docs ? discoverDocuments(root, scope) : Promise.resolve({ documents: [], sources: [disabledSource("docs")] }),
|
|
811
|
+
toggles.codeowners ? discoverCodeowners(root) : Promise.resolve({ content: null, sources: [disabledSource("codeowners")] })
|
|
812
|
+
]);
|
|
813
|
+
const wantsOrgTemplates = toggles.issueTemplates && templateDiscovery.templates.length === 0;
|
|
814
|
+
const [baseBranch, slug] = await Promise.all([
|
|
815
|
+
discoverBaseBranch(root, options.exec),
|
|
816
|
+
toggles.issueTypes || wantsOrgTemplates ? discoverGitHubSlug(root, options.exec) : Promise.resolve(null)
|
|
817
|
+
]);
|
|
818
|
+
const [labels, issueTypes, orgTemplates] = await Promise.all([
|
|
819
|
+
toggles.labels ? discoverLabels(root, options.exec) : Promise.resolve({ labels: [], sources: [disabledSource("labels")] }),
|
|
820
|
+
toggles.issueTypes ? discoverIssueTypes(root, slug?.owner ?? null, options.exec) : Promise.resolve({ types: [], sources: [disabledSource("issue-types")] }),
|
|
821
|
+
wantsOrgTemplates ? discoverOrganizationTemplates(root, slug, options.exec) : Promise.resolve({ templates: [], sources: [] })
|
|
822
|
+
]);
|
|
823
|
+
const sources = [
|
|
824
|
+
...templateDiscovery.sources,
|
|
825
|
+
...orgTemplates.sources,
|
|
826
|
+
...issueTypes.sources,
|
|
827
|
+
...labels.sources,
|
|
828
|
+
...prTemplates.sources,
|
|
829
|
+
...baseBranch.sources,
|
|
830
|
+
...documents.sources,
|
|
831
|
+
...codeowners.sources
|
|
832
|
+
];
|
|
833
|
+
const discoveredPullRequests = {};
|
|
834
|
+
if (baseBranch.baseBranch !== null) {
|
|
835
|
+
discoveredPullRequests.baseBranch = baseBranch.baseBranch;
|
|
836
|
+
}
|
|
837
|
+
const issues = mergeConfigLayers({
|
|
838
|
+
defaults: { titleConvention: null },
|
|
839
|
+
project: config.issues
|
|
840
|
+
});
|
|
841
|
+
const pullRequests = mergeConfigLayers({
|
|
842
|
+
defaults: { baseBranch: null, titleConvention: null },
|
|
843
|
+
discovered: discoveredPullRequests,
|
|
844
|
+
project: config.pullRequests
|
|
845
|
+
});
|
|
846
|
+
const git = mergeConfigLayers({
|
|
847
|
+
defaults: { branchConvention: null, commitConvention: null },
|
|
848
|
+
project: config.git
|
|
849
|
+
});
|
|
850
|
+
for (const group of ["issues", "pullRequests", "git"]) {
|
|
851
|
+
for (const key of Object.keys(config[group])) {
|
|
852
|
+
sources.push({
|
|
853
|
+
kind: DECLARATION_KINDS[group],
|
|
854
|
+
origin: "config",
|
|
855
|
+
path: null,
|
|
856
|
+
status: "found",
|
|
857
|
+
detail: `${group}.${key} declared`
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
schemaVersion: POLICY_SCHEMA_VERSION,
|
|
863
|
+
root,
|
|
864
|
+
scope,
|
|
865
|
+
enabled: true,
|
|
866
|
+
issues: {
|
|
867
|
+
templates: [...templateDiscovery.templates, ...orgTemplates.templates],
|
|
868
|
+
types: issueTypes.types,
|
|
869
|
+
labels: labels.labels,
|
|
870
|
+
titleConvention: issues.titleConvention ?? null
|
|
871
|
+
},
|
|
872
|
+
pullRequests: {
|
|
873
|
+
template: prTemplates.templates[0]?.content ?? null,
|
|
874
|
+
templates: prTemplates.templates,
|
|
875
|
+
baseBranch: pullRequests.baseBranch ?? null,
|
|
876
|
+
titleConvention: pullRequests.titleConvention ?? null
|
|
877
|
+
},
|
|
878
|
+
git: {
|
|
879
|
+
branchConvention: git.branchConvention ?? null,
|
|
880
|
+
commitConvention: git.commitConvention ?? null
|
|
881
|
+
},
|
|
882
|
+
docs: documents.documents,
|
|
883
|
+
codeowners: codeowners.content,
|
|
884
|
+
sources
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
var cache = /* @__PURE__ */ new Map();
|
|
888
|
+
async function loadRepositoryPolicy(options = {}) {
|
|
889
|
+
const root = options.root ?? await getProjectRoot();
|
|
890
|
+
const scope = normalizeScope(options.scope);
|
|
891
|
+
if (options.cache === false) {
|
|
892
|
+
return resolvePolicy(root, scope, options);
|
|
893
|
+
}
|
|
894
|
+
const key = `${root}\0${scope ?? ""}`;
|
|
895
|
+
const cached = cache.get(key);
|
|
896
|
+
if (cached !== void 0) {
|
|
897
|
+
return cached;
|
|
898
|
+
}
|
|
899
|
+
const pending = resolvePolicy(root, scope, options);
|
|
900
|
+
cache.set(key, pending);
|
|
901
|
+
try {
|
|
902
|
+
return await pending;
|
|
903
|
+
} catch (err) {
|
|
904
|
+
cache.delete(key);
|
|
905
|
+
throw err;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/commands/policy.ts
|
|
910
|
+
function groupSources(sources) {
|
|
911
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
912
|
+
for (const source of sources) {
|
|
913
|
+
const bucket = grouped.get(source.kind);
|
|
914
|
+
if (bucket === void 0) {
|
|
915
|
+
grouped.set(source.kind, [source]);
|
|
916
|
+
} else {
|
|
917
|
+
bucket.push(source);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return grouped;
|
|
921
|
+
}
|
|
922
|
+
function describeSource(source) {
|
|
923
|
+
const where = source.path ?? source.origin;
|
|
924
|
+
const detail = source.detail === null ? "" : ` \u2014 ${source.detail}`;
|
|
925
|
+
return `${source.status === "found" ? "" : `[${source.status}] `}${where}${detail}`;
|
|
926
|
+
}
|
|
927
|
+
function render(policy) {
|
|
928
|
+
const lines = [];
|
|
929
|
+
const push = (line = "") => {
|
|
930
|
+
lines.push(line);
|
|
931
|
+
};
|
|
932
|
+
push(`Repository: ${policy.root}`);
|
|
933
|
+
push(`Scope: ${policy.scope ?? "(root)"}`);
|
|
934
|
+
if (!policy.enabled) {
|
|
935
|
+
push();
|
|
936
|
+
push("Policy discovery is disabled (policy.enabled = false).");
|
|
937
|
+
console.log(lines.join("\n"));
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
push();
|
|
941
|
+
push("Issues");
|
|
942
|
+
push(` templates: ${policy.issues.templates.length}`);
|
|
943
|
+
for (const template of policy.issues.templates) {
|
|
944
|
+
const labels = template.labels.length > 0 ? ` labels=[${template.labels.join(", ")}]` : "";
|
|
945
|
+
const type = template.type === null ? "" : ` type=${template.type}`;
|
|
946
|
+
push(` - ${template.path} (${template.format}, ${template.origin})${type}${labels}`);
|
|
947
|
+
}
|
|
948
|
+
push(` types: ${policy.issues.types.length > 0 ? policy.issues.types.join(", ") : "(none)"}`);
|
|
949
|
+
push(` labels: ${policy.issues.labels.length}`);
|
|
950
|
+
for (const label of policy.issues.labels) {
|
|
951
|
+
push(` - ${label.name}${label.description === null ? "" : ` \u2014 ${label.description}`}`);
|
|
952
|
+
}
|
|
953
|
+
push(` titleConvention: ${policy.issues.titleConvention ?? "(none)"}`);
|
|
954
|
+
push();
|
|
955
|
+
push("Pull Requests");
|
|
956
|
+
push(` template: ${policy.pullRequests.templates[0]?.path ?? "(none)"}`);
|
|
957
|
+
for (const template of policy.pullRequests.templates.slice(1)) {
|
|
958
|
+
push(` - ${template.path}`);
|
|
959
|
+
}
|
|
960
|
+
push(` baseBranch: ${policy.pullRequests.baseBranch ?? "(none)"}`);
|
|
961
|
+
push(` titleConvention: ${policy.pullRequests.titleConvention ?? "(none)"}`);
|
|
962
|
+
push();
|
|
963
|
+
push("Git");
|
|
964
|
+
push(` branchConvention: ${policy.git.branchConvention ?? "(none)"}`);
|
|
965
|
+
push(` commitConvention: ${policy.git.commitConvention ?? "(none)"}`);
|
|
966
|
+
push();
|
|
967
|
+
push(`Documents: ${policy.docs.length}`);
|
|
968
|
+
for (const doc of policy.docs) {
|
|
969
|
+
const scope = doc.scope === "" ? "root" : doc.scope;
|
|
970
|
+
const from = doc.referencedFrom === null ? "" : ` \u2190 ${doc.referencedFrom}`;
|
|
971
|
+
push(` - ${doc.path} (${doc.kind}, scope=${scope})${from}`);
|
|
972
|
+
}
|
|
973
|
+
push();
|
|
974
|
+
push(`CODEOWNERS: ${policy.codeowners === null ? "(none)" : "found"}`);
|
|
975
|
+
push();
|
|
976
|
+
push("Sources");
|
|
977
|
+
for (const [kind, entries] of groupSources(policy.sources)) {
|
|
978
|
+
push(` ${kind}`);
|
|
979
|
+
for (const entry of entries) {
|
|
980
|
+
push(` - ${describeSource(entry)}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
console.log(lines.join("\n"));
|
|
984
|
+
}
|
|
985
|
+
async function runPolicy(options = {}) {
|
|
986
|
+
let policy;
|
|
987
|
+
try {
|
|
988
|
+
policy = await loadRepositoryPolicy({ scope: options.scope ?? null });
|
|
989
|
+
} catch (err) {
|
|
990
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
991
|
+
return 1;
|
|
992
|
+
}
|
|
993
|
+
if (options.json === true) {
|
|
994
|
+
console.log(JSON.stringify(policy, null, 2));
|
|
995
|
+
return 0;
|
|
996
|
+
}
|
|
997
|
+
render(policy);
|
|
998
|
+
const unavailable = policy.sources.filter((source) => source.status === "unavailable");
|
|
999
|
+
if (unavailable.length > 0) {
|
|
1000
|
+
console.log("");
|
|
1001
|
+
printWarning(
|
|
1002
|
+
`${unavailable.length} source(s) could not be consulted; the policy above is incomplete.`
|
|
1003
|
+
);
|
|
1004
|
+
} else if (policy.sources.length === 0) {
|
|
1005
|
+
console.log("");
|
|
1006
|
+
printInfo("This repository declares no policy sources; Issue Flow defaults apply.");
|
|
1007
|
+
}
|
|
1008
|
+
return 0;
|
|
1009
|
+
}
|
|
1010
|
+
export {
|
|
1011
|
+
runPolicy
|
|
1012
|
+
};
|
|
1013
|
+
//# sourceMappingURL=policy-NP4ZAVF6.js.map
|