gh-inari 0.3.0 → 0.5.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 +97 -13
- 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 +497 -47
- package/dist/cli.js.map +1 -1
- package/dist/github/adapter.js +15 -4
- package/dist/github/adapter.js.map +1 -1
- package/dist/github/transport.js +10 -8
- package/dist/github/transport.js.map +1 -1
- package/dist/governance.d.ts +41 -5
- package/dist/governance.js +190 -15
- package/dist/governance.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -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/reconciliation.d.ts +65 -0
- package/dist/reconciliation.js +274 -0
- package/dist/reconciliation.js.map +1 -0
- 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 +7 -6
- package/package.json +26 -20
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,54 @@
|
|
|
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
|
-
import { ArtifactInputError, parseArtifactInputDocument, prepareIssueArtifact, preparePullRequestArtifact, projectExistingArtifact, renderIssueArtifact, renderPullRequestArtifact,
|
|
6
|
+
import { ArtifactInputError, parseArtifactInputDocument, prepareIssueArtifact, preparePullRequestArtifact, projectExistingArtifact, renderIssueArtifact, renderPullRequestArtifact, } 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,
|
|
9
|
+
import { compileLocalGovernedContract, compileRepositoryGovernedContract, createGovernedIssue, createGovernedPullRequest, discoverRepositoryTemplates, rejectGovernedPolicyOverride, } from "./governance.js";
|
|
10
10
|
import { discoverTemplates } from "./template-discovery.js";
|
|
11
|
+
import { applySemanticPatch, assessExistingArtifact, currentArtifactInput, diffArtifact, prepareRemediationArtifact, prepareSyncInput, readGovernedExistingArtifact, RemediationError, updateGovernedExistingArtifact, } from "./reconciliation.js";
|
|
12
|
+
import { discoverSemanticTemplates, importNativeTemplate, renderSemanticCompactSchema, syncSemanticTemplates, SEMANTIC_ISSUE_DIRECTORY, SEMANTIC_PULL_REQUEST_FILE, SEMANTIC_TEMPLATE_DIRECTORY, } from "./semantic-template.js";
|
|
11
13
|
const EXIT_USAGE = 1;
|
|
12
14
|
const EXIT_VALIDATION = 2;
|
|
13
15
|
const EXIT_REMOTE = 3;
|
|
14
16
|
const EXIT_INTERNAL = 4;
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
+
const DIAGNOSTIC_PROTOCOL_VERSION = 1;
|
|
18
|
+
const RUNTIME_CAPABILITIES = [
|
|
19
|
+
"canonical-invocation",
|
|
20
|
+
"machine-readable-version",
|
|
21
|
+
"capability-diagnostics",
|
|
22
|
+
"extension-bootstrap",
|
|
23
|
+
];
|
|
24
|
+
const CANONICAL_INVOCATION = "gh inari";
|
|
25
|
+
const INSTALL_COMMAND = "gh extension install yohn-jp/gh-inari";
|
|
26
|
+
const UPDATE_COMMAND = "gh extension upgrade inari";
|
|
27
|
+
const FALLBACK_COMMAND = "npx --yes gh-inari";
|
|
28
|
+
const BOOLEAN_OPTIONS = new Set([
|
|
29
|
+
"help",
|
|
30
|
+
"json",
|
|
31
|
+
"version",
|
|
32
|
+
"diagnose",
|
|
33
|
+
"doctor",
|
|
34
|
+
"draft",
|
|
35
|
+
"maintainerCanModify",
|
|
36
|
+
"compact",
|
|
37
|
+
"check",
|
|
38
|
+
"dryRun",
|
|
39
|
+
]);
|
|
40
|
+
const VALUE_OPTIONS = new Set([
|
|
41
|
+
"from",
|
|
42
|
+
"template",
|
|
43
|
+
"policy",
|
|
44
|
+
"repository",
|
|
45
|
+
"title",
|
|
46
|
+
"head",
|
|
47
|
+
"base",
|
|
48
|
+
"to",
|
|
49
|
+
"requireCapability",
|
|
50
|
+
"minimumVersion",
|
|
51
|
+
]);
|
|
17
52
|
/** The installed gh-inari executable entrypoint. */
|
|
18
53
|
export async function runCli(argv, dependencies = {}) {
|
|
19
54
|
const metadata = dependencies.packageMetadata ?? readPackageMetadata();
|
|
@@ -30,21 +65,32 @@ export async function runCli(argv, dependencies = {}) {
|
|
|
30
65
|
console.error(`${shape.code}: ${shape.message}`);
|
|
31
66
|
return classifyExitCode(error);
|
|
32
67
|
}
|
|
33
|
-
|
|
68
|
+
const diagnosticRequested = parsed.options.diagnose === true ||
|
|
69
|
+
parsed.options.doctor === true ||
|
|
70
|
+
parsed.positionals[0] === "diagnose" ||
|
|
71
|
+
parsed.positionals[0] === "doctor";
|
|
72
|
+
const versionRequested = parsed.options.version === true || parsed.positionals[0] === "version";
|
|
73
|
+
if (parsed.options.help === true || (parsed.positionals.length === 0 && !versionRequested && !diagnosticRequested)) {
|
|
34
74
|
printHelp();
|
|
35
75
|
return parsed.positionals.length === 0 && parsed.options.help !== true ? EXIT_USAGE : 0;
|
|
36
76
|
}
|
|
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
77
|
const json = parsed.options.json === true;
|
|
43
78
|
try {
|
|
79
|
+
if (versionRequested)
|
|
80
|
+
return runVersion(metadata, parsed.options, json);
|
|
81
|
+
if (diagnosticRequested)
|
|
82
|
+
return runDiagnostic(metadata, parsed.options, json, dependencies);
|
|
83
|
+
const root = path.resolve(dependencies.repositoryRoot ?? process.cwd());
|
|
44
84
|
const [domain, command, ...rest] = parsed.positionals;
|
|
45
85
|
if (domain === "template" && command === "list") {
|
|
46
86
|
return await runTemplateList(root, parsed.options.repository, dependencies);
|
|
47
87
|
}
|
|
88
|
+
if (domain === "template" && command === "sync") {
|
|
89
|
+
return await runTemplateSync(root, parsed.options.check === true);
|
|
90
|
+
}
|
|
91
|
+
if (domain === "template" && command === "import") {
|
|
92
|
+
return await runTemplateImport(root, rest, parsed, json);
|
|
93
|
+
}
|
|
48
94
|
if (domain === "issue" || domain === "pr") {
|
|
49
95
|
return await runArtifactCommand(domain, command, rest, parsed, root, dependencies, json);
|
|
50
96
|
}
|
|
@@ -59,6 +105,242 @@ export async function runCli(argv, dependencies = {}) {
|
|
|
59
105
|
return classifyExitCode(error);
|
|
60
106
|
}
|
|
61
107
|
}
|
|
108
|
+
function runVersion(metadata, options, json) {
|
|
109
|
+
const info = runtimeInfo(metadata);
|
|
110
|
+
const requirements = runtimeRequirements(options, false);
|
|
111
|
+
const missingCapabilities = requirements.capabilities.filter((capability) => !info.capabilities.includes(capability));
|
|
112
|
+
const versionSupported = requirements.minimumVersion === undefined || versionAtLeast(info.version, requirements.minimumVersion);
|
|
113
|
+
const ok = missingCapabilities.length === 0 && versionSupported;
|
|
114
|
+
if (json) {
|
|
115
|
+
console.log(JSON.stringify({
|
|
116
|
+
ok,
|
|
117
|
+
...info,
|
|
118
|
+
...(ok
|
|
119
|
+
? {}
|
|
120
|
+
: {
|
|
121
|
+
error: {
|
|
122
|
+
code: "RUNTIME_REQUIREMENT_UNMET",
|
|
123
|
+
message: runtimeRequirementMessage(info, missingCapabilities, requirements.minimumVersion),
|
|
124
|
+
...(missingCapabilities.length === 0 ? {} : { missingCapabilities }),
|
|
125
|
+
...(requirements.minimumVersion === undefined ? {} : { minimumVersion: requirements.minimumVersion }),
|
|
126
|
+
recovery: FALLBACK_COMMAND,
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
console.log(`${metadata.name} ${metadata.version}`);
|
|
133
|
+
if (!ok)
|
|
134
|
+
console.error(`gh-inari: ${runtimeRequirementMessage(info, missingCapabilities, requirements.minimumVersion)}`);
|
|
135
|
+
}
|
|
136
|
+
return ok ? 0 : EXIT_VALIDATION;
|
|
137
|
+
}
|
|
138
|
+
function runDiagnostic(metadata, options, json, dependencies) {
|
|
139
|
+
const info = runtimeInfo(metadata);
|
|
140
|
+
const requirements = runtimeRequirements(options, true);
|
|
141
|
+
const canonical = probeCanonicalExtension(requirements, dependencies.runDiagnosticCommand);
|
|
142
|
+
const ok = canonical.status === "ready";
|
|
143
|
+
const output = {
|
|
144
|
+
ok,
|
|
145
|
+
...info,
|
|
146
|
+
requiredCapabilities: requirements.capabilities,
|
|
147
|
+
...(requirements.minimumVersion === undefined ? {} : { minimumVersion: requirements.minimumVersion }),
|
|
148
|
+
canonical: {
|
|
149
|
+
invocation: CANONICAL_INVOCATION,
|
|
150
|
+
status: canonical.status,
|
|
151
|
+
...(canonical.version === undefined ? {} : { version: canonical.version }),
|
|
152
|
+
...(canonical.capabilities === undefined ? {} : { capabilities: canonical.capabilities }),
|
|
153
|
+
...(canonical.missingCapabilities === undefined ? {} : { missingCapabilities: canonical.missingCapabilities }),
|
|
154
|
+
...(canonical.detail === undefined ? {} : { detail: canonical.detail }),
|
|
155
|
+
recovery: canonical.recovery,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
if (json)
|
|
159
|
+
console.log(JSON.stringify(output));
|
|
160
|
+
else {
|
|
161
|
+
console.log(`${metadata.name} ${metadata.version}`);
|
|
162
|
+
if (ok)
|
|
163
|
+
console.log(`${CANONICAL_INVOCATION}: ready (${canonical.version ?? "unknown version"})`);
|
|
164
|
+
else {
|
|
165
|
+
console.error(`gh-inari: ${canonicalDiagnosticMessage(canonical)}`);
|
|
166
|
+
console.error(`Action: ${canonical.recovery}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return ok ? 0 : EXIT_VALIDATION;
|
|
170
|
+
}
|
|
171
|
+
function runtimeInfo(metadata) {
|
|
172
|
+
return {
|
|
173
|
+
name: metadata.name,
|
|
174
|
+
version: metadata.version,
|
|
175
|
+
protocol: DIAGNOSTIC_PROTOCOL_VERSION,
|
|
176
|
+
capabilities: [...RUNTIME_CAPABILITIES],
|
|
177
|
+
invocation: {
|
|
178
|
+
canonical: CANONICAL_INVOCATION,
|
|
179
|
+
direct: "gh-inari",
|
|
180
|
+
fallback: FALLBACK_COMMAND,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function runtimeRequirements(options, defaultCapabilities) {
|
|
185
|
+
const requestedCapability = options.requireCapability;
|
|
186
|
+
const capabilities = typeof requestedCapability === "string"
|
|
187
|
+
? [requestedCapability]
|
|
188
|
+
: defaultCapabilities
|
|
189
|
+
? [...RUNTIME_CAPABILITIES]
|
|
190
|
+
: [];
|
|
191
|
+
const requestedMinimum = options.minimumVersion;
|
|
192
|
+
if (requestedMinimum !== undefined && typeof requestedMinimum !== "string")
|
|
193
|
+
throw new CliError("INVALID_OPTION", "Option --minimum-version requires a version value.", "--minimum-version");
|
|
194
|
+
if (typeof requestedMinimum === "string" && parseVersion(requestedMinimum) === undefined)
|
|
195
|
+
throw new CliError("INVALID_OPTION", `Option --minimum-version must be a semantic version (received "${requestedMinimum}").`, "--minimum-version");
|
|
196
|
+
return {
|
|
197
|
+
capabilities,
|
|
198
|
+
...(typeof requestedMinimum === "string" ? { minimumVersion: requestedMinimum } : {}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function probeCanonicalExtension(requirements, runCommand) {
|
|
202
|
+
const execute = runCommand ?? runGhDiagnosticCommand;
|
|
203
|
+
const list = execute(["extension", "list"]);
|
|
204
|
+
if (list.status !== 0) {
|
|
205
|
+
return {
|
|
206
|
+
status: "unavailable",
|
|
207
|
+
detail: diagnosticProcessDetail(list),
|
|
208
|
+
recovery: FALLBACK_COMMAND,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (!hasInariExtension(list.stdout))
|
|
212
|
+
return { status: "missing", recovery: INSTALL_COMMAND };
|
|
213
|
+
const version = execute(["inari", "--version", "--json"]);
|
|
214
|
+
if (version.status !== 0) {
|
|
215
|
+
return {
|
|
216
|
+
status: "stale",
|
|
217
|
+
detail: diagnosticProcessDetail(version),
|
|
218
|
+
recovery: UPDATE_COMMAND,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
let parsed;
|
|
222
|
+
try {
|
|
223
|
+
parsed = JSON.parse(version.stdout.trim());
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return {
|
|
227
|
+
status: "stale",
|
|
228
|
+
detail: "the installed extension does not support machine-readable version output",
|
|
229
|
+
recovery: UPDATE_COMMAND,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (!isRuntimeInfo(parsed)) {
|
|
233
|
+
return {
|
|
234
|
+
status: "stale",
|
|
235
|
+
detail: "the installed extension returned an incompatible version contract",
|
|
236
|
+
recovery: UPDATE_COMMAND,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (parsed.protocol !== DIAGNOSTIC_PROTOCOL_VERSION) {
|
|
240
|
+
return {
|
|
241
|
+
status: "stale",
|
|
242
|
+
version: parsed.version,
|
|
243
|
+
capabilities: parsed.capabilities,
|
|
244
|
+
detail: `the installed extension uses diagnostic protocol ${parsed.protocol}; expected ${DIAGNOSTIC_PROTOCOL_VERSION}`,
|
|
245
|
+
recovery: UPDATE_COMMAND,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const missingCapabilities = requirements.capabilities.filter((capability) => !parsed.capabilities.includes(capability));
|
|
249
|
+
if (missingCapabilities.length > 0 ||
|
|
250
|
+
(requirements.minimumVersion !== undefined && !versionAtLeast(parsed.version, requirements.minimumVersion))) {
|
|
251
|
+
return {
|
|
252
|
+
status: "stale",
|
|
253
|
+
version: parsed.version,
|
|
254
|
+
capabilities: parsed.capabilities,
|
|
255
|
+
...(missingCapabilities.length === 0 ? {} : { missingCapabilities }),
|
|
256
|
+
detail: runtimeRequirementMessage(parsed, missingCapabilities, requirements.minimumVersion),
|
|
257
|
+
recovery: UPDATE_COMMAND,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return { status: "ready", version: parsed.version, capabilities: parsed.capabilities, recovery: UPDATE_COMMAND };
|
|
261
|
+
}
|
|
262
|
+
function runGhDiagnosticCommand(args) {
|
|
263
|
+
try {
|
|
264
|
+
const result = spawnSync("gh", [...args], {
|
|
265
|
+
encoding: "utf8",
|
|
266
|
+
maxBuffer: 64 * 1024,
|
|
267
|
+
timeout: 3_000,
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
status: result.status,
|
|
271
|
+
stdout: result.stdout ?? "",
|
|
272
|
+
stderr: result.stderr ?? "",
|
|
273
|
+
...(result.error === undefined ? {} : { error: result.error.message }),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
return {
|
|
278
|
+
status: null,
|
|
279
|
+
stdout: "",
|
|
280
|
+
stderr: "",
|
|
281
|
+
error: error instanceof Error ? error.message : "unable to execute gh",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function hasInariExtension(output) {
|
|
286
|
+
return output.split(/\r?\n/u).some((line) => /^\s*gh\s+inari(?:\s|$)/u.test(line));
|
|
287
|
+
}
|
|
288
|
+
function isRuntimeInfo(value) {
|
|
289
|
+
if (typeof value !== "object" || value === null)
|
|
290
|
+
return false;
|
|
291
|
+
const candidate = value;
|
|
292
|
+
const invocation = candidate.invocation;
|
|
293
|
+
return (candidate.ok !== false &&
|
|
294
|
+
typeof candidate.name === "string" &&
|
|
295
|
+
candidate.name === "gh-inari" &&
|
|
296
|
+
typeof candidate.version === "string" &&
|
|
297
|
+
typeof candidate.protocol === "number" &&
|
|
298
|
+
Array.isArray(candidate.capabilities) &&
|
|
299
|
+
candidate.capabilities.every((capability) => typeof capability === "string") &&
|
|
300
|
+
typeof invocation === "object" &&
|
|
301
|
+
invocation !== null &&
|
|
302
|
+
typeof invocation.canonical === "string" &&
|
|
303
|
+
typeof invocation.direct === "string" &&
|
|
304
|
+
typeof invocation.fallback === "string");
|
|
305
|
+
}
|
|
306
|
+
function runtimeRequirementMessage(info, missingCapabilities, minimumVersion) {
|
|
307
|
+
const requirements = [];
|
|
308
|
+
if (missingCapabilities.length > 0)
|
|
309
|
+
requirements.push(`missing capability ${missingCapabilities.map((value) => `"${value}"`).join(", ")}`);
|
|
310
|
+
if (minimumVersion !== undefined && !versionAtLeast(info.version, minimumVersion))
|
|
311
|
+
requirements.push(`version ${info.version} is older than required ${minimumVersion}`);
|
|
312
|
+
return requirements.length === 0 ? "runtime requirements are not satisfied" : requirements.join("; ");
|
|
313
|
+
}
|
|
314
|
+
function canonicalDiagnosticMessage(diagnostic) {
|
|
315
|
+
if (diagnostic.status === "missing")
|
|
316
|
+
return "the canonical gh extension is not installed";
|
|
317
|
+
if (diagnostic.status === "unavailable")
|
|
318
|
+
return diagnostic.detail ?? "the GitHub CLI could not be executed";
|
|
319
|
+
if (diagnostic.status === "stale")
|
|
320
|
+
return diagnostic.detail ?? "the installed gh extension is stale";
|
|
321
|
+
return "the canonical gh extension is ready";
|
|
322
|
+
}
|
|
323
|
+
function diagnosticProcessDetail(result) {
|
|
324
|
+
const detail = (result.error ?? result.stderr ?? "").trim().split(/\r?\n/u)[0];
|
|
325
|
+
return detail === "" ? "the GitHub CLI command failed" : detail.slice(0, 240);
|
|
326
|
+
}
|
|
327
|
+
function parseVersion(value) {
|
|
328
|
+
const match = /^(?:v)?(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
|
|
329
|
+
if (match === null)
|
|
330
|
+
return undefined;
|
|
331
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
332
|
+
}
|
|
333
|
+
function versionAtLeast(actual, minimum) {
|
|
334
|
+
const actualParts = parseVersion(actual);
|
|
335
|
+
const minimumParts = parseVersion(minimum);
|
|
336
|
+
if (actualParts === undefined || minimumParts === undefined)
|
|
337
|
+
return false;
|
|
338
|
+
for (let index = 0; index < actualParts.length; index += 1) {
|
|
339
|
+
if (actualParts[index] !== minimumParts[index])
|
|
340
|
+
return actualParts[index] > minimumParts[index];
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
62
344
|
class CliError extends Error {
|
|
63
345
|
code;
|
|
64
346
|
path;
|
|
@@ -71,6 +353,17 @@ class CliError extends Error {
|
|
|
71
353
|
this.details = details;
|
|
72
354
|
}
|
|
73
355
|
}
|
|
356
|
+
/** Bound for local --from <file> and stdin artifact input, independent of semantic field constraints. */
|
|
357
|
+
const MAX_INPUT_BYTES = 1_048_576;
|
|
358
|
+
function inputTooLargeError(observedBytes) {
|
|
359
|
+
return new CliError("INPUT_TOO_LARGE", `Input exceeds the maximum allowed size of ${MAX_INPUT_BYTES} bytes.`, "--from", { limitBytes: MAX_INPUT_BYTES, observedBytes });
|
|
360
|
+
}
|
|
361
|
+
function invalidArtifactNumberError(domain, value) {
|
|
362
|
+
const message = value === undefined
|
|
363
|
+
? `A ${domain} number is required.`
|
|
364
|
+
: `"${value}" is not a valid ${domain} number. Use a positive integer.`;
|
|
365
|
+
return new CliError("INVALID_ARTIFACT_NUMBER", message, "$argv[0]", { domain, value });
|
|
366
|
+
}
|
|
74
367
|
async function runTemplateList(root, repository, dependencies) {
|
|
75
368
|
let discovery;
|
|
76
369
|
if (typeof repository === "string") {
|
|
@@ -81,7 +374,36 @@ async function runTemplateList(root, repository, dependencies) {
|
|
|
81
374
|
else {
|
|
82
375
|
discovery = await discoverTemplates(root);
|
|
83
376
|
}
|
|
84
|
-
|
|
377
|
+
const semanticTemplates = typeof repository === "string" ? [] : await discoverSemanticTemplates(root);
|
|
378
|
+
const hint = semanticTemplates.length === 0 && typeof repository !== "string"
|
|
379
|
+
? `no semantic templates found under ${SEMANTIC_TEMPLATE_DIRECTORY}/; ` +
|
|
380
|
+
`expected ${SEMANTIC_ISSUE_DIRECTORY}/<id>.json, ${SEMANTIC_PULL_REQUEST_FILE}, ` +
|
|
381
|
+
`or ${SEMANTIC_TEMPLATE_DIRECTORY}/pull-requests/<id>.json`
|
|
382
|
+
: undefined;
|
|
383
|
+
console.log(JSON.stringify({
|
|
384
|
+
templates: discovery.templates,
|
|
385
|
+
semanticTemplates,
|
|
386
|
+
...(hint === undefined ? {} : { semanticTemplatesHint: hint }),
|
|
387
|
+
}));
|
|
388
|
+
return 0;
|
|
389
|
+
}
|
|
390
|
+
async function runTemplateSync(root, check) {
|
|
391
|
+
const result = await syncSemanticTemplates(root, check);
|
|
392
|
+
console.log(JSON.stringify(result));
|
|
393
|
+
return check && result.changed ? EXIT_VALIDATION : 0;
|
|
394
|
+
}
|
|
395
|
+
async function runTemplateImport(root, rest, parsed, json) {
|
|
396
|
+
const nativePath = typeof parsed.options.from === "string" ? parsed.options.from : rest[0];
|
|
397
|
+
if (nativePath === undefined)
|
|
398
|
+
throw new CliError("INPUT_REQUIRED", "Use template import --from <native-template>.", "--from");
|
|
399
|
+
const imported = await importNativeTemplate(root, nativePath, typeof parsed.options.to === "string" ? parsed.options.to : undefined);
|
|
400
|
+
if (json)
|
|
401
|
+
console.log(JSON.stringify({ ok: true, ...imported }));
|
|
402
|
+
else {
|
|
403
|
+
console.log(imported.path);
|
|
404
|
+
if (imported.warning !== undefined)
|
|
405
|
+
console.error(`warning: ${imported.warning}`);
|
|
406
|
+
}
|
|
85
407
|
return 0;
|
|
86
408
|
}
|
|
87
409
|
async function runArtifactCommand(domain, command, rest, parsed, root, dependencies, json) {
|
|
@@ -97,7 +419,10 @@ async function runArtifactCommand(domain, command, rest, parsed, root, dependenc
|
|
|
97
419
|
contract = await compileLocalGovernedContract(domain, root, templateSelector(parsed, rest[0]), parsed.options.policy);
|
|
98
420
|
}
|
|
99
421
|
const projection = projectContract(contract);
|
|
100
|
-
|
|
422
|
+
if (parsed.options.compact === true)
|
|
423
|
+
console.log(JSON.stringify({ schema: renderSemanticCompactSchema(contract) }));
|
|
424
|
+
else
|
|
425
|
+
console.log(JSON.stringify({ contract, template: contract.templateIdentity, ...projection }));
|
|
101
426
|
return 0;
|
|
102
427
|
}
|
|
103
428
|
if (command === "validate" || command === "render" || command === "create") {
|
|
@@ -143,28 +468,42 @@ async function runArtifactCommand(domain, command, rest, parsed, root, dependenc
|
|
|
143
468
|
if (domain === "issue") {
|
|
144
469
|
const prepared = prepareIssueArtifact(contract, preparedDocument);
|
|
145
470
|
const created = await createGovernedIssue(adapter, prepared.artifact);
|
|
146
|
-
console.log(JSON.stringify({ ok: true, artifact: created }));
|
|
471
|
+
console.log(JSON.stringify({ ok: true, artifact: created.artifact, governance: created.governance }));
|
|
147
472
|
return 0;
|
|
148
473
|
}
|
|
149
474
|
const prepared = preparePullRequestArtifact(contract, preparedDocument);
|
|
150
475
|
const created = await createGovernedPullRequest(adapter, prepared.artifact);
|
|
151
|
-
console.log(JSON.stringify({ ok: true, artifact: created }));
|
|
476
|
+
console.log(JSON.stringify({ ok: true, artifact: created.artifact, governance: created.governance }));
|
|
152
477
|
return 0;
|
|
153
478
|
}
|
|
479
|
+
if (command === "check" || command === "edit" || command === "normalize" || command === "sync") {
|
|
480
|
+
if (rest[0] === undefined || !isPositiveInteger(rest[0])) {
|
|
481
|
+
throw invalidArtifactNumberError(domain, rest[0]);
|
|
482
|
+
}
|
|
483
|
+
return runExistingRemediation(domain, command, Number(rest[0]), parsed, root, dependencies, json);
|
|
484
|
+
}
|
|
154
485
|
if ((command === "validate" || command === "explain") &&
|
|
155
486
|
rest[0] !== undefined &&
|
|
156
487
|
isPositiveInteger(rest[0]) &&
|
|
157
488
|
parsed.options.from === undefined) {
|
|
158
489
|
return runExistingValidation(domain, Number(rest[0]), parsed, root, dependencies, true);
|
|
159
490
|
}
|
|
160
|
-
if (command === "
|
|
161
|
-
|
|
491
|
+
if (command === "explain" && (rest[0] === undefined || !isPositiveInteger(rest[0]))) {
|
|
492
|
+
throw invalidArtifactNumberError(domain, rest[0]);
|
|
493
|
+
}
|
|
494
|
+
if (command === "get") {
|
|
495
|
+
if (rest[0] !== undefined && isPositiveInteger(rest[0])) {
|
|
496
|
+
return runExistingGet(domain, Number(rest[0]), parsed, root, dependencies);
|
|
497
|
+
}
|
|
498
|
+
throw invalidArtifactNumberError(domain, rest[0]);
|
|
162
499
|
}
|
|
163
500
|
throw new CliError("UNKNOWN_COMMAND", `Unknown ${domain} command "${command ?? ""}".`);
|
|
164
501
|
}
|
|
165
502
|
async function runExistingValidation(domain, number, parsed, root, dependencies, json) {
|
|
166
503
|
rejectGovernedPolicyOverride(parsed.options.policy);
|
|
167
|
-
const
|
|
504
|
+
const adapter = createAdapter(dependencies, root, parsed.options.repository);
|
|
505
|
+
await adapter.resolveRepositoryContext();
|
|
506
|
+
const read = await readGovernedExistingArtifact(adapter, domain, number, templateSelector(parsed, undefined));
|
|
168
507
|
const { remote, result } = read;
|
|
169
508
|
const projection = projectExistingArtifact(result);
|
|
170
509
|
const output = {
|
|
@@ -180,7 +519,9 @@ async function runExistingValidation(domain, number, parsed, root, dependencies,
|
|
|
180
519
|
}
|
|
181
520
|
async function runExistingGet(domain, number, parsed, root, dependencies) {
|
|
182
521
|
rejectGovernedPolicyOverride(parsed.options.policy);
|
|
183
|
-
const
|
|
522
|
+
const adapter = createAdapter(dependencies, root, parsed.options.repository);
|
|
523
|
+
await adapter.resolveRepositoryContext();
|
|
524
|
+
const { remote, contract, result } = await readGovernedExistingArtifact(adapter, domain, number, templateSelector(parsed, undefined));
|
|
184
525
|
const projection = projectExistingArtifact(result);
|
|
185
526
|
const output = {
|
|
186
527
|
valid: projection.valid,
|
|
@@ -198,6 +539,74 @@ async function runExistingGet(domain, number, parsed, root, dependencies) {
|
|
|
198
539
|
console.log(JSON.stringify(output));
|
|
199
540
|
return result.valid ? 0 : EXIT_VALIDATION;
|
|
200
541
|
}
|
|
542
|
+
async function runExistingRemediation(domain, operation, number, parsed, root, dependencies, json) {
|
|
543
|
+
void json;
|
|
544
|
+
rejectGovernedPolicyOverride(parsed.options.policy);
|
|
545
|
+
const adapter = createAdapter(dependencies, root, parsed.options.repository);
|
|
546
|
+
await adapter.resolveRepositoryContext();
|
|
547
|
+
const read = await readGovernedExistingArtifact(adapter, domain, number, templateSelector(parsed, undefined));
|
|
548
|
+
const assessment = assessExistingArtifact(domain, read);
|
|
549
|
+
const base = {
|
|
550
|
+
operation,
|
|
551
|
+
kind: domain === "issue" ? "issue" : "pull_request",
|
|
552
|
+
number: read.remote.number,
|
|
553
|
+
url: read.remote.url,
|
|
554
|
+
...(read.contract === undefined ? {} : { template: read.contract.templateIdentity }),
|
|
555
|
+
};
|
|
556
|
+
if (operation === "check") {
|
|
557
|
+
console.log(JSON.stringify({
|
|
558
|
+
ok: assessment.status === "valid-current",
|
|
559
|
+
...base,
|
|
560
|
+
status: assessment.status,
|
|
561
|
+
classification: read.result.classification,
|
|
562
|
+
valid: assessment.status === "valid-current",
|
|
563
|
+
normalizable: assessment.normalizable,
|
|
564
|
+
diagnostics: assessment.diagnostics,
|
|
565
|
+
violations: assessment.status === "non-canonical" ? assessment.diagnostics : read.result.violations,
|
|
566
|
+
}));
|
|
567
|
+
return assessment.status === "valid-current" ? 0 : EXIT_VALIDATION;
|
|
568
|
+
}
|
|
569
|
+
if (read.contract === undefined) {
|
|
570
|
+
throw new RemediationError(operation === "normalize" ? "NORMALIZATION_UNSAFE" : "SYNC_CURRENT_UNSUPPORTED", "No authoritative template could be selected for the existing artifact.", "$.template");
|
|
571
|
+
}
|
|
572
|
+
let desiredInput;
|
|
573
|
+
if (operation === "normalize") {
|
|
574
|
+
if (!read.result.valid || !read.result.parse.parsed) {
|
|
575
|
+
throw new RemediationError("NORMALIZATION_UNSAFE", "Normalization requires a semantically valid artifact whose values can be round-tripped canonically.", "$.artifact");
|
|
576
|
+
}
|
|
577
|
+
desiredInput = currentArtifactInput(domain, read);
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
const input = await readInputDocument(parsed.options.from);
|
|
581
|
+
desiredInput =
|
|
582
|
+
operation === "edit" ? applySemanticPatch(domain, read, input) : prepareSyncInput(domain, read, input);
|
|
583
|
+
}
|
|
584
|
+
const desired = prepareRemediationArtifact(domain, read.contract, desiredInput);
|
|
585
|
+
const diff = diffArtifact(domain, read, desired);
|
|
586
|
+
const resultBase = {
|
|
587
|
+
...base,
|
|
588
|
+
changed: diff.changed,
|
|
589
|
+
noOp: !diff.changed,
|
|
590
|
+
diff,
|
|
591
|
+
};
|
|
592
|
+
if (!diff.changed || parsed.options.dryRun === true) {
|
|
593
|
+
console.log(JSON.stringify({
|
|
594
|
+
ok: true,
|
|
595
|
+
...resultBase,
|
|
596
|
+
...(parsed.options.dryRun === true ? { dryRun: true, mutation: "not-performed" } : {}),
|
|
597
|
+
}));
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
const mutated = await updateGovernedExistingArtifact(adapter, domain, number, desired);
|
|
601
|
+
console.log(JSON.stringify({
|
|
602
|
+
ok: true,
|
|
603
|
+
...resultBase,
|
|
604
|
+
mutation: "applied",
|
|
605
|
+
artifact: { number: mutated.artifact.number, url: mutated.artifact.url },
|
|
606
|
+
governance: mutated.governance,
|
|
607
|
+
}));
|
|
608
|
+
return 0;
|
|
609
|
+
}
|
|
201
610
|
function existingArtifactMetadata(domain, remote) {
|
|
202
611
|
if (domain === "issue") {
|
|
203
612
|
if (!("labels" in remote) || !("assignees" in remote))
|
|
@@ -219,23 +628,6 @@ function existingArtifactMetadata(domain, remote) {
|
|
|
219
628
|
base: remote.base,
|
|
220
629
|
};
|
|
221
630
|
}
|
|
222
|
-
async function readExistingArtifact(domain, number, parsed, root, dependencies) {
|
|
223
|
-
const adapter = createAdapter(dependencies, root, parsed.options.repository);
|
|
224
|
-
await adapter.resolveRepositoryContext();
|
|
225
|
-
const selector = templateSelector(parsed, undefined);
|
|
226
|
-
const contracts = selector === undefined
|
|
227
|
-
? await compileRepositoryGovernedContracts(adapter, domain)
|
|
228
|
-
: [await compileRepositoryGovernedContract(adapter, domain, selector)];
|
|
229
|
-
const remote = domain === "issue" ? await adapter.getIssue(number) : await adapter.getPullRequest(number);
|
|
230
|
-
const candidates = contracts.map((contract) => ({
|
|
231
|
-
contract,
|
|
232
|
-
result: domain === "issue"
|
|
233
|
-
? validateExistingIssueArtifact(contract, remote.body)
|
|
234
|
-
: validateExistingPullRequestArtifact(contract, remote.body),
|
|
235
|
-
}));
|
|
236
|
-
const selected = selectExistingArtifactCandidate(candidates);
|
|
237
|
-
return { remote, contract: selected.contract, result: selected.result };
|
|
238
|
-
}
|
|
239
631
|
function createAdapter(dependencies, root, repository) {
|
|
240
632
|
const factory = dependencies.createAdapter ?? ((options) => new GitHubAdapter(options));
|
|
241
633
|
return factory({ cwd: root, ...(typeof repository === "string" ? { repository } : {}) });
|
|
@@ -248,9 +640,16 @@ async function readInputDocument(value) {
|
|
|
248
640
|
source = await readStdin();
|
|
249
641
|
else {
|
|
250
642
|
try {
|
|
643
|
+
const stats = await stat(value);
|
|
644
|
+
if (stats.size > MAX_INPUT_BYTES)
|
|
645
|
+
throw inputTooLargeError(stats.size);
|
|
251
646
|
source = await readFile(value, "utf8");
|
|
647
|
+
if (Buffer.byteLength(source, "utf8") > MAX_INPUT_BYTES)
|
|
648
|
+
throw inputTooLargeError(Buffer.byteLength(source, "utf8"));
|
|
252
649
|
}
|
|
253
650
|
catch (cause) {
|
|
651
|
+
if (cause instanceof CliError)
|
|
652
|
+
throw cause;
|
|
254
653
|
const error = new CliError("INPUT_READ_FAILED", `Cannot read input file "${value}".`, "--from");
|
|
255
654
|
if (cause instanceof Error)
|
|
256
655
|
error.cause = cause;
|
|
@@ -346,7 +745,9 @@ function toErrorShape(error) {
|
|
|
346
745
|
return { code: "INTERNAL_ERROR", message: error instanceof Error ? error.message : "Operation failed." };
|
|
347
746
|
}
|
|
348
747
|
function classifyExitCode(error) {
|
|
349
|
-
if (error instanceof SemanticValidationError ||
|
|
748
|
+
if (error instanceof SemanticValidationError ||
|
|
749
|
+
error instanceof ArtifactInputError ||
|
|
750
|
+
error instanceof RemediationError)
|
|
350
751
|
return EXIT_VALIDATION;
|
|
351
752
|
if (isGitHubAdapterError(error))
|
|
352
753
|
return EXIT_REMOTE;
|
|
@@ -360,7 +761,10 @@ function classifyExitCode(error) {
|
|
|
360
761
|
error.code === "INPUT_REQUIRED" ||
|
|
361
762
|
error.code === "INPUT_READ_FAILED"))
|
|
362
763
|
return EXIT_USAGE;
|
|
363
|
-
if (error instanceof CliError &&
|
|
764
|
+
if (error instanceof CliError &&
|
|
765
|
+
(error.code === "INPUT_INVALID_JSON" ||
|
|
766
|
+
error.code === "INPUT_TOO_LARGE" ||
|
|
767
|
+
error.code === "INVALID_ARTIFACT_NUMBER"))
|
|
364
768
|
return EXIT_VALIDATION;
|
|
365
769
|
if (isObjectWithCode(error) && error.code === "GOVERNANCE_POLICY_OVERRIDE_FORBIDDEN")
|
|
366
770
|
return EXIT_VALIDATION;
|
|
@@ -371,15 +775,26 @@ function classifyExitCode(error) {
|
|
|
371
775
|
return EXIT_INTERNAL;
|
|
372
776
|
}
|
|
373
777
|
function isMachineCommand(positionals) {
|
|
374
|
-
return (positionals.length >= 2 &&
|
|
778
|
+
return ((positionals.length >= 2 &&
|
|
375
779
|
(positionals[1] === "schema" ||
|
|
376
780
|
positionals[1] === "validate" ||
|
|
377
781
|
positionals[1] === "render" ||
|
|
378
782
|
positionals[1] === "create" ||
|
|
379
783
|
positionals[1] === "explain" ||
|
|
380
|
-
positionals[1] === "get"
|
|
784
|
+
positionals[1] === "get" ||
|
|
785
|
+
positionals[1] === "check" ||
|
|
786
|
+
positionals[1] === "edit" ||
|
|
787
|
+
positionals[1] === "normalize" ||
|
|
788
|
+
positionals[1] === "sync")) ||
|
|
789
|
+
positionals[0] === "diagnose" ||
|
|
790
|
+
positionals[0] === "doctor" ||
|
|
791
|
+
positionals[0] === "version");
|
|
381
792
|
}
|
|
382
793
|
function isMachineCommandTokens(argv) {
|
|
794
|
+
if (argv.includes("--diagnose") || argv.includes("--doctor") || argv.includes("diagnose") || argv.includes("doctor"))
|
|
795
|
+
return true;
|
|
796
|
+
if (argv.includes("--version") || argv.includes("version"))
|
|
797
|
+
return argv.includes("--json");
|
|
383
798
|
const domainIndex = argv.findIndex((token) => token === "issue" || token === "pr");
|
|
384
799
|
if (domainIndex < 0)
|
|
385
800
|
return false;
|
|
@@ -389,7 +804,11 @@ function isMachineCommandTokens(argv) {
|
|
|
389
804
|
command === "render" ||
|
|
390
805
|
command === "create" ||
|
|
391
806
|
command === "explain" ||
|
|
392
|
-
command === "get"
|
|
807
|
+
command === "get" ||
|
|
808
|
+
command === "check" ||
|
|
809
|
+
command === "edit" ||
|
|
810
|
+
command === "normalize" ||
|
|
811
|
+
command === "sync");
|
|
393
812
|
}
|
|
394
813
|
function isPositiveInteger(value) {
|
|
395
814
|
return /^[1-9]\d*$/u.test(value);
|
|
@@ -414,16 +833,27 @@ function requireFile(filePath) {
|
|
|
414
833
|
}
|
|
415
834
|
async function readStdin() {
|
|
416
835
|
const chunks = [];
|
|
417
|
-
|
|
418
|
-
for await (const
|
|
419
|
-
|
|
420
|
-
|
|
836
|
+
let totalBytes = 0;
|
|
837
|
+
for await (const chunk of process.stdin) {
|
|
838
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
839
|
+
totalBytes += buffer.byteLength;
|
|
840
|
+
if (totalBytes > MAX_INPUT_BYTES)
|
|
841
|
+
throw inputTooLargeError(totalBytes);
|
|
842
|
+
chunks.push(buffer);
|
|
843
|
+
}
|
|
844
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
421
845
|
}
|
|
422
846
|
function printHelp() {
|
|
423
847
|
console.log(`Usage: gh-inari <command> [options]
|
|
424
848
|
|
|
425
849
|
Commands:
|
|
426
850
|
template list
|
|
851
|
+
template sync [--check]
|
|
852
|
+
template import --from <native-template> [--to <semantic-file>]
|
|
853
|
+
Discovered semantic paths: .github/inari/issues/<id>.json,
|
|
854
|
+
.github/inari/pull-request.json (single PR template), or
|
|
855
|
+
.github/inari/pull-requests/<id>.json (multiple PR templates).
|
|
856
|
+
Other --to paths write successfully but are never discovered.
|
|
427
857
|
issue schema [template]
|
|
428
858
|
issue validate --template <template> --from <file.json>
|
|
429
859
|
issue render --template <template> --from <file.json>
|
|
@@ -431,6 +861,10 @@ Commands:
|
|
|
431
861
|
issue validate <number> [--template <template>]
|
|
432
862
|
issue explain <number> [--template <template>]
|
|
433
863
|
issue get <number> [--template <template>] --json
|
|
864
|
+
issue check <number> [--template <template>]
|
|
865
|
+
issue edit <number> --from <file.json> [--dry-run]
|
|
866
|
+
issue normalize <number> [--dry-run]
|
|
867
|
+
issue sync <number> --from <file.json> [--dry-run]
|
|
434
868
|
pr schema [template]
|
|
435
869
|
pr validate --template <template> --from <file.json>
|
|
436
870
|
pr render --template <template> --from <file.json>
|
|
@@ -438,6 +872,10 @@ Commands:
|
|
|
438
872
|
pr validate <number> [--template <template>]
|
|
439
873
|
pr explain <number> [--template <template>]
|
|
440
874
|
pr get <number> [--template <template>] --json
|
|
875
|
+
pr check <number> [--template <template>]
|
|
876
|
+
pr edit <number> --from <file.json> [--dry-run]
|
|
877
|
+
pr normalize <number> [--dry-run]
|
|
878
|
+
pr sync <number> --from <file.json> [--dry-run]
|
|
441
879
|
|
|
442
880
|
Options:
|
|
443
881
|
--from <path> JSON input file, or - for stdin
|
|
@@ -447,13 +885,25 @@ Options:
|
|
|
447
885
|
--title <title> Issue/PR title for create
|
|
448
886
|
--head <branch> PR head branch for create
|
|
449
887
|
--base <branch> PR base branch for create
|
|
888
|
+
--compact Emit only semantic fields and constraints for schema
|
|
889
|
+
--check Check generated native projections without writing
|
|
890
|
+
--dry-run Show a bounded remediation diff without mutating GitHub
|
|
450
891
|
--draft Create the PR as a draft
|
|
451
892
|
--maintainer-can-modify
|
|
452
893
|
Allow maintainer edits on the PR
|
|
453
894
|
--json Emit structured JSON output
|
|
454
895
|
--version Print package version
|
|
896
|
+
--diagnose Check the canonical gh extension and recovery path
|
|
897
|
+
--require-capability <id>
|
|
898
|
+
Require a capability in --version/--diagnose checks
|
|
899
|
+
--minimum-version <v>
|
|
900
|
+
Require a minimum semantic version in checks
|
|
455
901
|
--help Print this help
|
|
456
902
|
|
|
457
|
-
Create always validates and renders before invoking gh. Schema, validate, and
|
|
903
|
+
Create always validates and renders before invoking gh. Schema, validate, render, check, and --dry-run remediation never mutate GitHub.
|
|
904
|
+
Edit applies an explicit semantic patch; normalize preserves existing semantic values; sync reconciles a complete desired semantic state.
|
|
905
|
+
|
|
906
|
+
Canonical installation: gh extension install yohn-jp/gh-inari
|
|
907
|
+
PATH-independent fallback: npx --yes gh-inari`);
|
|
458
908
|
}
|
|
459
909
|
//# sourceMappingURL=cli.js.map
|