gh-inari 0.2.0 → 0.5.2
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 +77 -12
- package/dist/artifact.d.ts +1 -1
- package/dist/artifact.js +1 -1
- package/dist/artifact.js.map +1 -1
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +433 -31
- package/dist/cli.js.map +1 -1
- package/dist/contract/ir.d.ts +8 -0
- package/dist/contract/ir.js +3 -1
- package/dist/contract/ir.js.map +1 -1
- package/dist/github/adapter.d.ts +6 -3
- package/dist/github/adapter.js +65 -11
- package/dist/github/adapter.js.map +1 -1
- package/dist/github/capability.js +1 -0
- package/dist/github/capability.js.map +1 -1
- package/dist/github/errors.d.ts +16 -1
- package/dist/github/errors.js +18 -0
- package/dist/github/errors.js.map +1 -1
- package/dist/github/transport.d.ts +19 -0
- package/dist/github/transport.js +87 -25
- package/dist/github/transport.js.map +1 -1
- package/dist/github/types.d.ts +6 -0
- package/dist/governance.d.ts +64 -4
- package/dist/governance.js +251 -6
- package/dist/governance.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pr-policy.js +109 -1
- package/dist/pr-policy.js.map +1 -1
- package/dist/pull-request-template.js +5 -1
- package/dist/pull-request-template.js.map +1 -1
- package/dist/semantic-template.d.ts +107 -0
- package/dist/semantic-template.js +1151 -0
- package/dist/semantic-template.js.map +1 -0
- package/gh-inari +77 -11
- package/package.json +26 -20
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,52 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { createInterface } from "node:readline";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { ArtifactInputError, parseArtifactInputDocument, prepareIssueArtifact, preparePullRequestArtifact, projectExistingArtifact, renderIssueArtifact, renderPullRequestArtifact, selectExistingArtifactCandidate, validateExistingIssueArtifact, validateExistingPullRequestArtifact, } from "./artifact.js";
|
|
7
7
|
import { projectContract, SemanticValidationError, validateSemanticInput, } from "./contract/index.js";
|
|
8
8
|
import { GitHubAdapter, isGitHubAdapterError } from "./github/index.js";
|
|
9
|
-
import { compileLocalGovernedContract, compileRepositoryGovernedContract, compileRepositoryGovernedContracts, discoverRepositoryTemplates, rejectGovernedPolicyOverride, } from "./governance.js";
|
|
9
|
+
import { compileLocalGovernedContract, compileRepositoryGovernedContract, compileRepositoryGovernedContracts, createGovernedIssue, createGovernedPullRequest, discoverRepositoryTemplates, rejectGovernedPolicyOverride, } from "./governance.js";
|
|
10
10
|
import { discoverTemplates } from "./template-discovery.js";
|
|
11
|
+
import { discoverSemanticTemplates, importNativeTemplate, renderSemanticCompactSchema, syncSemanticTemplates, SEMANTIC_ISSUE_DIRECTORY, SEMANTIC_PULL_REQUEST_FILE, SEMANTIC_TEMPLATE_DIRECTORY, } from "./semantic-template.js";
|
|
11
12
|
const EXIT_USAGE = 1;
|
|
12
13
|
const EXIT_VALIDATION = 2;
|
|
13
14
|
const EXIT_REMOTE = 3;
|
|
14
15
|
const EXIT_INTERNAL = 4;
|
|
15
|
-
const
|
|
16
|
-
const
|
|
16
|
+
const DIAGNOSTIC_PROTOCOL_VERSION = 1;
|
|
17
|
+
const RUNTIME_CAPABILITIES = [
|
|
18
|
+
"canonical-invocation",
|
|
19
|
+
"machine-readable-version",
|
|
20
|
+
"capability-diagnostics",
|
|
21
|
+
"extension-bootstrap",
|
|
22
|
+
];
|
|
23
|
+
const CANONICAL_INVOCATION = "gh inari";
|
|
24
|
+
const INSTALL_COMMAND = "gh extension install yohn-jp/gh-inari";
|
|
25
|
+
const UPDATE_COMMAND = "gh extension upgrade inari";
|
|
26
|
+
const FALLBACK_COMMAND = "npx --yes gh-inari";
|
|
27
|
+
const BOOLEAN_OPTIONS = new Set([
|
|
28
|
+
"help",
|
|
29
|
+
"json",
|
|
30
|
+
"version",
|
|
31
|
+
"diagnose",
|
|
32
|
+
"doctor",
|
|
33
|
+
"draft",
|
|
34
|
+
"maintainerCanModify",
|
|
35
|
+
"compact",
|
|
36
|
+
"check",
|
|
37
|
+
]);
|
|
38
|
+
const VALUE_OPTIONS = new Set([
|
|
39
|
+
"from",
|
|
40
|
+
"template",
|
|
41
|
+
"policy",
|
|
42
|
+
"repository",
|
|
43
|
+
"title",
|
|
44
|
+
"head",
|
|
45
|
+
"base",
|
|
46
|
+
"to",
|
|
47
|
+
"requireCapability",
|
|
48
|
+
"minimumVersion",
|
|
49
|
+
]);
|
|
17
50
|
/** The installed gh-inari executable entrypoint. */
|
|
18
51
|
export async function runCli(argv, dependencies = {}) {
|
|
19
52
|
const metadata = dependencies.packageMetadata ?? readPackageMetadata();
|
|
@@ -30,21 +63,32 @@ export async function runCli(argv, dependencies = {}) {
|
|
|
30
63
|
console.error(`${shape.code}: ${shape.message}`);
|
|
31
64
|
return classifyExitCode(error);
|
|
32
65
|
}
|
|
33
|
-
|
|
66
|
+
const diagnosticRequested = parsed.options.diagnose === true ||
|
|
67
|
+
parsed.options.doctor === true ||
|
|
68
|
+
parsed.positionals[0] === "diagnose" ||
|
|
69
|
+
parsed.positionals[0] === "doctor";
|
|
70
|
+
const versionRequested = parsed.options.version === true || parsed.positionals[0] === "version";
|
|
71
|
+
if (parsed.options.help === true || (parsed.positionals.length === 0 && !versionRequested && !diagnosticRequested)) {
|
|
34
72
|
printHelp();
|
|
35
73
|
return parsed.positionals.length === 0 && parsed.options.help !== true ? EXIT_USAGE : 0;
|
|
36
74
|
}
|
|
37
|
-
if (parsed.options.version === true) {
|
|
38
|
-
console.log(`${metadata.name} ${metadata.version}`);
|
|
39
|
-
return 0;
|
|
40
|
-
}
|
|
41
|
-
const root = path.resolve(dependencies.repositoryRoot ?? process.cwd());
|
|
42
75
|
const json = parsed.options.json === true;
|
|
43
76
|
try {
|
|
77
|
+
if (versionRequested)
|
|
78
|
+
return runVersion(metadata, parsed.options, json);
|
|
79
|
+
if (diagnosticRequested)
|
|
80
|
+
return runDiagnostic(metadata, parsed.options, json, dependencies);
|
|
81
|
+
const root = path.resolve(dependencies.repositoryRoot ?? process.cwd());
|
|
44
82
|
const [domain, command, ...rest] = parsed.positionals;
|
|
45
83
|
if (domain === "template" && command === "list") {
|
|
46
84
|
return await runTemplateList(root, parsed.options.repository, dependencies);
|
|
47
85
|
}
|
|
86
|
+
if (domain === "template" && command === "sync") {
|
|
87
|
+
return await runTemplateSync(root, parsed.options.check === true);
|
|
88
|
+
}
|
|
89
|
+
if (domain === "template" && command === "import") {
|
|
90
|
+
return await runTemplateImport(root, rest, parsed, json);
|
|
91
|
+
}
|
|
48
92
|
if (domain === "issue" || domain === "pr") {
|
|
49
93
|
return await runArtifactCommand(domain, command, rest, parsed, root, dependencies, json);
|
|
50
94
|
}
|
|
@@ -59,6 +103,242 @@ export async function runCli(argv, dependencies = {}) {
|
|
|
59
103
|
return classifyExitCode(error);
|
|
60
104
|
}
|
|
61
105
|
}
|
|
106
|
+
function runVersion(metadata, options, json) {
|
|
107
|
+
const info = runtimeInfo(metadata);
|
|
108
|
+
const requirements = runtimeRequirements(options, false);
|
|
109
|
+
const missingCapabilities = requirements.capabilities.filter((capability) => !info.capabilities.includes(capability));
|
|
110
|
+
const versionSupported = requirements.minimumVersion === undefined || versionAtLeast(info.version, requirements.minimumVersion);
|
|
111
|
+
const ok = missingCapabilities.length === 0 && versionSupported;
|
|
112
|
+
if (json) {
|
|
113
|
+
console.log(JSON.stringify({
|
|
114
|
+
ok,
|
|
115
|
+
...info,
|
|
116
|
+
...(ok
|
|
117
|
+
? {}
|
|
118
|
+
: {
|
|
119
|
+
error: {
|
|
120
|
+
code: "RUNTIME_REQUIREMENT_UNMET",
|
|
121
|
+
message: runtimeRequirementMessage(info, missingCapabilities, requirements.minimumVersion),
|
|
122
|
+
...(missingCapabilities.length === 0 ? {} : { missingCapabilities }),
|
|
123
|
+
...(requirements.minimumVersion === undefined ? {} : { minimumVersion: requirements.minimumVersion }),
|
|
124
|
+
recovery: FALLBACK_COMMAND,
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
console.log(`${metadata.name} ${metadata.version}`);
|
|
131
|
+
if (!ok)
|
|
132
|
+
console.error(`gh-inari: ${runtimeRequirementMessage(info, missingCapabilities, requirements.minimumVersion)}`);
|
|
133
|
+
}
|
|
134
|
+
return ok ? 0 : EXIT_VALIDATION;
|
|
135
|
+
}
|
|
136
|
+
function runDiagnostic(metadata, options, json, dependencies) {
|
|
137
|
+
const info = runtimeInfo(metadata);
|
|
138
|
+
const requirements = runtimeRequirements(options, true);
|
|
139
|
+
const canonical = probeCanonicalExtension(requirements, dependencies.runDiagnosticCommand);
|
|
140
|
+
const ok = canonical.status === "ready";
|
|
141
|
+
const output = {
|
|
142
|
+
ok,
|
|
143
|
+
...info,
|
|
144
|
+
requiredCapabilities: requirements.capabilities,
|
|
145
|
+
...(requirements.minimumVersion === undefined ? {} : { minimumVersion: requirements.minimumVersion }),
|
|
146
|
+
canonical: {
|
|
147
|
+
invocation: CANONICAL_INVOCATION,
|
|
148
|
+
status: canonical.status,
|
|
149
|
+
...(canonical.version === undefined ? {} : { version: canonical.version }),
|
|
150
|
+
...(canonical.capabilities === undefined ? {} : { capabilities: canonical.capabilities }),
|
|
151
|
+
...(canonical.missingCapabilities === undefined ? {} : { missingCapabilities: canonical.missingCapabilities }),
|
|
152
|
+
...(canonical.detail === undefined ? {} : { detail: canonical.detail }),
|
|
153
|
+
recovery: canonical.recovery,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
if (json)
|
|
157
|
+
console.log(JSON.stringify(output));
|
|
158
|
+
else {
|
|
159
|
+
console.log(`${metadata.name} ${metadata.version}`);
|
|
160
|
+
if (ok)
|
|
161
|
+
console.log(`${CANONICAL_INVOCATION}: ready (${canonical.version ?? "unknown version"})`);
|
|
162
|
+
else {
|
|
163
|
+
console.error(`gh-inari: ${canonicalDiagnosticMessage(canonical)}`);
|
|
164
|
+
console.error(`Action: ${canonical.recovery}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return ok ? 0 : EXIT_VALIDATION;
|
|
168
|
+
}
|
|
169
|
+
function runtimeInfo(metadata) {
|
|
170
|
+
return {
|
|
171
|
+
name: metadata.name,
|
|
172
|
+
version: metadata.version,
|
|
173
|
+
protocol: DIAGNOSTIC_PROTOCOL_VERSION,
|
|
174
|
+
capabilities: [...RUNTIME_CAPABILITIES],
|
|
175
|
+
invocation: {
|
|
176
|
+
canonical: CANONICAL_INVOCATION,
|
|
177
|
+
direct: "gh-inari",
|
|
178
|
+
fallback: FALLBACK_COMMAND,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function runtimeRequirements(options, defaultCapabilities) {
|
|
183
|
+
const requestedCapability = options.requireCapability;
|
|
184
|
+
const capabilities = typeof requestedCapability === "string"
|
|
185
|
+
? [requestedCapability]
|
|
186
|
+
: defaultCapabilities
|
|
187
|
+
? [...RUNTIME_CAPABILITIES]
|
|
188
|
+
: [];
|
|
189
|
+
const requestedMinimum = options.minimumVersion;
|
|
190
|
+
if (requestedMinimum !== undefined && typeof requestedMinimum !== "string")
|
|
191
|
+
throw new CliError("INVALID_OPTION", "Option --minimum-version requires a version value.", "--minimum-version");
|
|
192
|
+
if (typeof requestedMinimum === "string" && parseVersion(requestedMinimum) === undefined)
|
|
193
|
+
throw new CliError("INVALID_OPTION", `Option --minimum-version must be a semantic version (received "${requestedMinimum}").`, "--minimum-version");
|
|
194
|
+
return {
|
|
195
|
+
capabilities,
|
|
196
|
+
...(typeof requestedMinimum === "string" ? { minimumVersion: requestedMinimum } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function probeCanonicalExtension(requirements, runCommand) {
|
|
200
|
+
const execute = runCommand ?? runGhDiagnosticCommand;
|
|
201
|
+
const list = execute(["extension", "list"]);
|
|
202
|
+
if (list.status !== 0) {
|
|
203
|
+
return {
|
|
204
|
+
status: "unavailable",
|
|
205
|
+
detail: diagnosticProcessDetail(list),
|
|
206
|
+
recovery: FALLBACK_COMMAND,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (!hasInariExtension(list.stdout))
|
|
210
|
+
return { status: "missing", recovery: INSTALL_COMMAND };
|
|
211
|
+
const version = execute(["inari", "--version", "--json"]);
|
|
212
|
+
if (version.status !== 0) {
|
|
213
|
+
return {
|
|
214
|
+
status: "stale",
|
|
215
|
+
detail: diagnosticProcessDetail(version),
|
|
216
|
+
recovery: UPDATE_COMMAND,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(version.stdout.trim());
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return {
|
|
225
|
+
status: "stale",
|
|
226
|
+
detail: "the installed extension does not support machine-readable version output",
|
|
227
|
+
recovery: UPDATE_COMMAND,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (!isRuntimeInfo(parsed)) {
|
|
231
|
+
return {
|
|
232
|
+
status: "stale",
|
|
233
|
+
detail: "the installed extension returned an incompatible version contract",
|
|
234
|
+
recovery: UPDATE_COMMAND,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (parsed.protocol !== DIAGNOSTIC_PROTOCOL_VERSION) {
|
|
238
|
+
return {
|
|
239
|
+
status: "stale",
|
|
240
|
+
version: parsed.version,
|
|
241
|
+
capabilities: parsed.capabilities,
|
|
242
|
+
detail: `the installed extension uses diagnostic protocol ${parsed.protocol}; expected ${DIAGNOSTIC_PROTOCOL_VERSION}`,
|
|
243
|
+
recovery: UPDATE_COMMAND,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const missingCapabilities = requirements.capabilities.filter((capability) => !parsed.capabilities.includes(capability));
|
|
247
|
+
if (missingCapabilities.length > 0 ||
|
|
248
|
+
(requirements.minimumVersion !== undefined && !versionAtLeast(parsed.version, requirements.minimumVersion))) {
|
|
249
|
+
return {
|
|
250
|
+
status: "stale",
|
|
251
|
+
version: parsed.version,
|
|
252
|
+
capabilities: parsed.capabilities,
|
|
253
|
+
...(missingCapabilities.length === 0 ? {} : { missingCapabilities }),
|
|
254
|
+
detail: runtimeRequirementMessage(parsed, missingCapabilities, requirements.minimumVersion),
|
|
255
|
+
recovery: UPDATE_COMMAND,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { status: "ready", version: parsed.version, capabilities: parsed.capabilities, recovery: UPDATE_COMMAND };
|
|
259
|
+
}
|
|
260
|
+
function runGhDiagnosticCommand(args) {
|
|
261
|
+
try {
|
|
262
|
+
const result = spawnSync("gh", [...args], {
|
|
263
|
+
encoding: "utf8",
|
|
264
|
+
maxBuffer: 64 * 1024,
|
|
265
|
+
timeout: 3_000,
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
status: result.status,
|
|
269
|
+
stdout: result.stdout ?? "",
|
|
270
|
+
stderr: result.stderr ?? "",
|
|
271
|
+
...(result.error === undefined ? {} : { error: result.error.message }),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
return {
|
|
276
|
+
status: null,
|
|
277
|
+
stdout: "",
|
|
278
|
+
stderr: "",
|
|
279
|
+
error: error instanceof Error ? error.message : "unable to execute gh",
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function hasInariExtension(output) {
|
|
284
|
+
return output.split(/\r?\n/u).some((line) => /^\s*gh\s+inari(?:\s|$)/u.test(line));
|
|
285
|
+
}
|
|
286
|
+
function isRuntimeInfo(value) {
|
|
287
|
+
if (typeof value !== "object" || value === null)
|
|
288
|
+
return false;
|
|
289
|
+
const candidate = value;
|
|
290
|
+
const invocation = candidate.invocation;
|
|
291
|
+
return (candidate.ok !== false &&
|
|
292
|
+
typeof candidate.name === "string" &&
|
|
293
|
+
candidate.name === "gh-inari" &&
|
|
294
|
+
typeof candidate.version === "string" &&
|
|
295
|
+
typeof candidate.protocol === "number" &&
|
|
296
|
+
Array.isArray(candidate.capabilities) &&
|
|
297
|
+
candidate.capabilities.every((capability) => typeof capability === "string") &&
|
|
298
|
+
typeof invocation === "object" &&
|
|
299
|
+
invocation !== null &&
|
|
300
|
+
typeof invocation.canonical === "string" &&
|
|
301
|
+
typeof invocation.direct === "string" &&
|
|
302
|
+
typeof invocation.fallback === "string");
|
|
303
|
+
}
|
|
304
|
+
function runtimeRequirementMessage(info, missingCapabilities, minimumVersion) {
|
|
305
|
+
const requirements = [];
|
|
306
|
+
if (missingCapabilities.length > 0)
|
|
307
|
+
requirements.push(`missing capability ${missingCapabilities.map((value) => `"${value}"`).join(", ")}`);
|
|
308
|
+
if (minimumVersion !== undefined && !versionAtLeast(info.version, minimumVersion))
|
|
309
|
+
requirements.push(`version ${info.version} is older than required ${minimumVersion}`);
|
|
310
|
+
return requirements.length === 0 ? "runtime requirements are not satisfied" : requirements.join("; ");
|
|
311
|
+
}
|
|
312
|
+
function canonicalDiagnosticMessage(diagnostic) {
|
|
313
|
+
if (diagnostic.status === "missing")
|
|
314
|
+
return "the canonical gh extension is not installed";
|
|
315
|
+
if (diagnostic.status === "unavailable")
|
|
316
|
+
return diagnostic.detail ?? "the GitHub CLI could not be executed";
|
|
317
|
+
if (diagnostic.status === "stale")
|
|
318
|
+
return diagnostic.detail ?? "the installed gh extension is stale";
|
|
319
|
+
return "the canonical gh extension is ready";
|
|
320
|
+
}
|
|
321
|
+
function diagnosticProcessDetail(result) {
|
|
322
|
+
const detail = (result.error ?? result.stderr ?? "").trim().split(/\r?\n/u)[0];
|
|
323
|
+
return detail === "" ? "the GitHub CLI command failed" : detail.slice(0, 240);
|
|
324
|
+
}
|
|
325
|
+
function parseVersion(value) {
|
|
326
|
+
const match = /^(?:v)?(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
|
|
327
|
+
if (match === null)
|
|
328
|
+
return undefined;
|
|
329
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
330
|
+
}
|
|
331
|
+
function versionAtLeast(actual, minimum) {
|
|
332
|
+
const actualParts = parseVersion(actual);
|
|
333
|
+
const minimumParts = parseVersion(minimum);
|
|
334
|
+
if (actualParts === undefined || minimumParts === undefined)
|
|
335
|
+
return false;
|
|
336
|
+
for (let index = 0; index < actualParts.length; index += 1) {
|
|
337
|
+
if (actualParts[index] !== minimumParts[index])
|
|
338
|
+
return actualParts[index] > minimumParts[index];
|
|
339
|
+
}
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
62
342
|
class CliError extends Error {
|
|
63
343
|
code;
|
|
64
344
|
path;
|
|
@@ -71,6 +351,17 @@ class CliError extends Error {
|
|
|
71
351
|
this.details = details;
|
|
72
352
|
}
|
|
73
353
|
}
|
|
354
|
+
/** Bound for local --from <file> and stdin artifact input, independent of semantic field constraints. */
|
|
355
|
+
const MAX_INPUT_BYTES = 1_048_576;
|
|
356
|
+
function inputTooLargeError(observedBytes) {
|
|
357
|
+
return new CliError("INPUT_TOO_LARGE", `Input exceeds the maximum allowed size of ${MAX_INPUT_BYTES} bytes.`, "--from", { limitBytes: MAX_INPUT_BYTES, observedBytes });
|
|
358
|
+
}
|
|
359
|
+
function invalidArtifactNumberError(domain, value) {
|
|
360
|
+
const message = value === undefined
|
|
361
|
+
? `A ${domain} number is required.`
|
|
362
|
+
: `"${value}" is not a valid ${domain} number. Use a positive integer.`;
|
|
363
|
+
return new CliError("INVALID_ARTIFACT_NUMBER", message, "$argv[0]", { domain, value });
|
|
364
|
+
}
|
|
74
365
|
async function runTemplateList(root, repository, dependencies) {
|
|
75
366
|
let discovery;
|
|
76
367
|
if (typeof repository === "string") {
|
|
@@ -81,7 +372,36 @@ async function runTemplateList(root, repository, dependencies) {
|
|
|
81
372
|
else {
|
|
82
373
|
discovery = await discoverTemplates(root);
|
|
83
374
|
}
|
|
84
|
-
|
|
375
|
+
const semanticTemplates = typeof repository === "string" ? [] : await discoverSemanticTemplates(root);
|
|
376
|
+
const hint = semanticTemplates.length === 0 && typeof repository !== "string"
|
|
377
|
+
? `no semantic templates found under ${SEMANTIC_TEMPLATE_DIRECTORY}/; ` +
|
|
378
|
+
`expected ${SEMANTIC_ISSUE_DIRECTORY}/<id>.json, ${SEMANTIC_PULL_REQUEST_FILE}, ` +
|
|
379
|
+
`or ${SEMANTIC_TEMPLATE_DIRECTORY}/pull-requests/<id>.json`
|
|
380
|
+
: undefined;
|
|
381
|
+
console.log(JSON.stringify({
|
|
382
|
+
templates: discovery.templates,
|
|
383
|
+
semanticTemplates,
|
|
384
|
+
...(hint === undefined ? {} : { semanticTemplatesHint: hint }),
|
|
385
|
+
}));
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
async function runTemplateSync(root, check) {
|
|
389
|
+
const result = await syncSemanticTemplates(root, check);
|
|
390
|
+
console.log(JSON.stringify(result));
|
|
391
|
+
return check && result.changed ? EXIT_VALIDATION : 0;
|
|
392
|
+
}
|
|
393
|
+
async function runTemplateImport(root, rest, parsed, json) {
|
|
394
|
+
const nativePath = typeof parsed.options.from === "string" ? parsed.options.from : rest[0];
|
|
395
|
+
if (nativePath === undefined)
|
|
396
|
+
throw new CliError("INPUT_REQUIRED", "Use template import --from <native-template>.", "--from");
|
|
397
|
+
const imported = await importNativeTemplate(root, nativePath, typeof parsed.options.to === "string" ? parsed.options.to : undefined);
|
|
398
|
+
if (json)
|
|
399
|
+
console.log(JSON.stringify({ ok: true, ...imported }));
|
|
400
|
+
else {
|
|
401
|
+
console.log(imported.path);
|
|
402
|
+
if (imported.warning !== undefined)
|
|
403
|
+
console.error(`warning: ${imported.warning}`);
|
|
404
|
+
}
|
|
85
405
|
return 0;
|
|
86
406
|
}
|
|
87
407
|
async function runArtifactCommand(domain, command, rest, parsed, root, dependencies, json) {
|
|
@@ -97,7 +417,10 @@ async function runArtifactCommand(domain, command, rest, parsed, root, dependenc
|
|
|
97
417
|
contract = await compileLocalGovernedContract(domain, root, templateSelector(parsed, rest[0]), parsed.options.policy);
|
|
98
418
|
}
|
|
99
419
|
const projection = projectContract(contract);
|
|
100
|
-
|
|
420
|
+
if (parsed.options.compact === true)
|
|
421
|
+
console.log(JSON.stringify({ schema: renderSemanticCompactSchema(contract) }));
|
|
422
|
+
else
|
|
423
|
+
console.log(JSON.stringify({ contract, template: contract.templateIdentity, ...projection }));
|
|
101
424
|
return 0;
|
|
102
425
|
}
|
|
103
426
|
if (command === "validate" || command === "render" || command === "create") {
|
|
@@ -142,13 +465,13 @@ async function runArtifactCommand(domain, command, rest, parsed, root, dependenc
|
|
|
142
465
|
const contract = await compileRepositoryGovernedContract(adapter, domain, templateSelector(parsed, rest[0]));
|
|
143
466
|
if (domain === "issue") {
|
|
144
467
|
const prepared = prepareIssueArtifact(contract, preparedDocument);
|
|
145
|
-
const created = await adapter
|
|
146
|
-
console.log(JSON.stringify({ ok: true, artifact: created }));
|
|
468
|
+
const created = await createGovernedIssue(adapter, prepared.artifact);
|
|
469
|
+
console.log(JSON.stringify({ ok: true, artifact: created.artifact, governance: created.governance }));
|
|
147
470
|
return 0;
|
|
148
471
|
}
|
|
149
472
|
const prepared = preparePullRequestArtifact(contract, preparedDocument);
|
|
150
|
-
const created = await adapter
|
|
151
|
-
console.log(JSON.stringify({ ok: true, artifact: created }));
|
|
473
|
+
const created = await createGovernedPullRequest(adapter, prepared.artifact);
|
|
474
|
+
console.log(JSON.stringify({ ok: true, artifact: created.artifact, governance: created.governance }));
|
|
152
475
|
return 0;
|
|
153
476
|
}
|
|
154
477
|
if ((command === "validate" || command === "explain") &&
|
|
@@ -157,8 +480,14 @@ async function runArtifactCommand(domain, command, rest, parsed, root, dependenc
|
|
|
157
480
|
parsed.options.from === undefined) {
|
|
158
481
|
return runExistingValidation(domain, Number(rest[0]), parsed, root, dependencies, true);
|
|
159
482
|
}
|
|
160
|
-
if (command === "
|
|
161
|
-
|
|
483
|
+
if (command === "explain" && (rest[0] === undefined || !isPositiveInteger(rest[0]))) {
|
|
484
|
+
throw invalidArtifactNumberError(domain, rest[0]);
|
|
485
|
+
}
|
|
486
|
+
if (command === "get") {
|
|
487
|
+
if (rest[0] !== undefined && isPositiveInteger(rest[0])) {
|
|
488
|
+
return runExistingGet(domain, Number(rest[0]), parsed, root, dependencies);
|
|
489
|
+
}
|
|
490
|
+
throw invalidArtifactNumberError(domain, rest[0]);
|
|
162
491
|
}
|
|
163
492
|
throw new CliError("UNKNOWN_COMMAND", `Unknown ${domain} command "${command ?? ""}".`);
|
|
164
493
|
}
|
|
@@ -223,9 +552,17 @@ async function readExistingArtifact(domain, number, parsed, root, dependencies)
|
|
|
223
552
|
const adapter = createAdapter(dependencies, root, parsed.options.repository);
|
|
224
553
|
await adapter.resolveRepositoryContext();
|
|
225
554
|
const selector = templateSelector(parsed, undefined);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
555
|
+
let contracts;
|
|
556
|
+
let failedTemplates;
|
|
557
|
+
if (selector === undefined) {
|
|
558
|
+
const outcomes = await compileRepositoryGovernedContracts(adapter, domain);
|
|
559
|
+
contracts = outcomes.filter((outcome) => outcome.status === "compiled").map((outcome) => outcome.contract);
|
|
560
|
+
failedTemplates = outcomes.filter((outcome) => outcome.status === "failed");
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
contracts = [await compileRepositoryGovernedContract(adapter, domain, selector)];
|
|
564
|
+
failedTemplates = [];
|
|
565
|
+
}
|
|
229
566
|
const remote = domain === "issue" ? await adapter.getIssue(number) : await adapter.getPullRequest(number);
|
|
230
567
|
const candidates = contracts.map((contract) => ({
|
|
231
568
|
contract,
|
|
@@ -234,7 +571,34 @@ async function readExistingArtifact(domain, number, parsed, root, dependencies)
|
|
|
234
571
|
: validateExistingPullRequestArtifact(contract, remote.body),
|
|
235
572
|
}));
|
|
236
573
|
const selected = selectExistingArtifactCandidate(candidates);
|
|
237
|
-
|
|
574
|
+
if (selected.contract !== undefined || failedTemplates.length === 0) {
|
|
575
|
+
return { remote, contract: selected.contract, result: selected.result };
|
|
576
|
+
}
|
|
577
|
+
// No compiled template matched, and at least one sibling template failed to
|
|
578
|
+
// compile: fail closed, since the malformed template could be the one that
|
|
579
|
+
// actually owns this artifact. Surface it as a bounded diagnostic rather
|
|
580
|
+
// than an opaque compile error.
|
|
581
|
+
const compileDiagnostics = failedTemplates.map((failed) => ({
|
|
582
|
+
code: "EXISTING_TEMPLATE_COMPILE_FAILED",
|
|
583
|
+
path: failed.path,
|
|
584
|
+
message: `[${failed.path}] Template failed to compile: ${failed.message}`,
|
|
585
|
+
}));
|
|
586
|
+
// selected.contract is undefined here, so selectExistingArtifactCandidate resolved this
|
|
587
|
+
// as an unmatched candidate set: its violations are always ExistingArtifactDiagnostic[].
|
|
588
|
+
const existingViolations = selected.result.violations;
|
|
589
|
+
return {
|
|
590
|
+
remote,
|
|
591
|
+
result: {
|
|
592
|
+
valid: false,
|
|
593
|
+
classification: selected.result.classification,
|
|
594
|
+
parse: {
|
|
595
|
+
parsed: false,
|
|
596
|
+
values: {},
|
|
597
|
+
diagnostics: [...selected.result.parse.diagnostics, ...compileDiagnostics],
|
|
598
|
+
},
|
|
599
|
+
violations: [...existingViolations, ...compileDiagnostics],
|
|
600
|
+
},
|
|
601
|
+
};
|
|
238
602
|
}
|
|
239
603
|
function createAdapter(dependencies, root, repository) {
|
|
240
604
|
const factory = dependencies.createAdapter ?? ((options) => new GitHubAdapter(options));
|
|
@@ -248,9 +612,16 @@ async function readInputDocument(value) {
|
|
|
248
612
|
source = await readStdin();
|
|
249
613
|
else {
|
|
250
614
|
try {
|
|
615
|
+
const stats = await stat(value);
|
|
616
|
+
if (stats.size > MAX_INPUT_BYTES)
|
|
617
|
+
throw inputTooLargeError(stats.size);
|
|
251
618
|
source = await readFile(value, "utf8");
|
|
619
|
+
if (Buffer.byteLength(source, "utf8") > MAX_INPUT_BYTES)
|
|
620
|
+
throw inputTooLargeError(Buffer.byteLength(source, "utf8"));
|
|
252
621
|
}
|
|
253
622
|
catch (cause) {
|
|
623
|
+
if (cause instanceof CliError)
|
|
624
|
+
throw cause;
|
|
254
625
|
const error = new CliError("INPUT_READ_FAILED", `Cannot read input file "${value}".`, "--from");
|
|
255
626
|
if (cause instanceof Error)
|
|
256
627
|
error.cause = cause;
|
|
@@ -360,7 +731,10 @@ function classifyExitCode(error) {
|
|
|
360
731
|
error.code === "INPUT_REQUIRED" ||
|
|
361
732
|
error.code === "INPUT_READ_FAILED"))
|
|
362
733
|
return EXIT_USAGE;
|
|
363
|
-
if (error instanceof CliError &&
|
|
734
|
+
if (error instanceof CliError &&
|
|
735
|
+
(error.code === "INPUT_INVALID_JSON" ||
|
|
736
|
+
error.code === "INPUT_TOO_LARGE" ||
|
|
737
|
+
error.code === "INVALID_ARTIFACT_NUMBER"))
|
|
364
738
|
return EXIT_VALIDATION;
|
|
365
739
|
if (isObjectWithCode(error) && error.code === "GOVERNANCE_POLICY_OVERRIDE_FORBIDDEN")
|
|
366
740
|
return EXIT_VALIDATION;
|
|
@@ -371,15 +745,22 @@ function classifyExitCode(error) {
|
|
|
371
745
|
return EXIT_INTERNAL;
|
|
372
746
|
}
|
|
373
747
|
function isMachineCommand(positionals) {
|
|
374
|
-
return (positionals.length >= 2 &&
|
|
748
|
+
return ((positionals.length >= 2 &&
|
|
375
749
|
(positionals[1] === "schema" ||
|
|
376
750
|
positionals[1] === "validate" ||
|
|
377
751
|
positionals[1] === "render" ||
|
|
378
752
|
positionals[1] === "create" ||
|
|
379
753
|
positionals[1] === "explain" ||
|
|
380
|
-
positionals[1] === "get"))
|
|
754
|
+
positionals[1] === "get")) ||
|
|
755
|
+
positionals[0] === "diagnose" ||
|
|
756
|
+
positionals[0] === "doctor" ||
|
|
757
|
+
positionals[0] === "version");
|
|
381
758
|
}
|
|
382
759
|
function isMachineCommandTokens(argv) {
|
|
760
|
+
if (argv.includes("--diagnose") || argv.includes("--doctor") || argv.includes("diagnose") || argv.includes("doctor"))
|
|
761
|
+
return true;
|
|
762
|
+
if (argv.includes("--version") || argv.includes("version"))
|
|
763
|
+
return argv.includes("--json");
|
|
383
764
|
const domainIndex = argv.findIndex((token) => token === "issue" || token === "pr");
|
|
384
765
|
if (domainIndex < 0)
|
|
385
766
|
return false;
|
|
@@ -414,16 +795,27 @@ function requireFile(filePath) {
|
|
|
414
795
|
}
|
|
415
796
|
async function readStdin() {
|
|
416
797
|
const chunks = [];
|
|
417
|
-
|
|
418
|
-
for await (const
|
|
419
|
-
|
|
420
|
-
|
|
798
|
+
let totalBytes = 0;
|
|
799
|
+
for await (const chunk of process.stdin) {
|
|
800
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
801
|
+
totalBytes += buffer.byteLength;
|
|
802
|
+
if (totalBytes > MAX_INPUT_BYTES)
|
|
803
|
+
throw inputTooLargeError(totalBytes);
|
|
804
|
+
chunks.push(buffer);
|
|
805
|
+
}
|
|
806
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
421
807
|
}
|
|
422
808
|
function printHelp() {
|
|
423
809
|
console.log(`Usage: gh-inari <command> [options]
|
|
424
810
|
|
|
425
811
|
Commands:
|
|
426
812
|
template list
|
|
813
|
+
template sync [--check]
|
|
814
|
+
template import --from <native-template> [--to <semantic-file>]
|
|
815
|
+
Discovered semantic paths: .github/inari/issues/<id>.json,
|
|
816
|
+
.github/inari/pull-request.json (single PR template), or
|
|
817
|
+
.github/inari/pull-requests/<id>.json (multiple PR templates).
|
|
818
|
+
Other --to paths write successfully but are never discovered.
|
|
427
819
|
issue schema [template]
|
|
428
820
|
issue validate --template <template> --from <file.json>
|
|
429
821
|
issue render --template <template> --from <file.json>
|
|
@@ -447,13 +839,23 @@ Options:
|
|
|
447
839
|
--title <title> Issue/PR title for create
|
|
448
840
|
--head <branch> PR head branch for create
|
|
449
841
|
--base <branch> PR base branch for create
|
|
842
|
+
--compact Emit only semantic fields and constraints for schema
|
|
843
|
+
--check Check generated native projections without writing
|
|
450
844
|
--draft Create the PR as a draft
|
|
451
845
|
--maintainer-can-modify
|
|
452
846
|
Allow maintainer edits on the PR
|
|
453
847
|
--json Emit structured JSON output
|
|
454
848
|
--version Print package version
|
|
849
|
+
--diagnose Check the canonical gh extension and recovery path
|
|
850
|
+
--require-capability <id>
|
|
851
|
+
Require a capability in --version/--diagnose checks
|
|
852
|
+
--minimum-version <v>
|
|
853
|
+
Require a minimum semantic version in checks
|
|
455
854
|
--help Print this help
|
|
456
855
|
|
|
457
|
-
Create always validates and renders before invoking gh. Schema, validate, and render never mutate GitHub
|
|
856
|
+
Create always validates and renders before invoking gh. Schema, validate, and render never mutate GitHub.
|
|
857
|
+
|
|
858
|
+
Canonical installation: gh extension install yohn-jp/gh-inari
|
|
859
|
+
PATH-independent fallback: npx --yes gh-inari`);
|
|
458
860
|
}
|
|
459
861
|
//# sourceMappingURL=cli.js.map
|