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/dist/cli.js CHANGED
@@ -1,19 +1,52 @@
1
- import { readFile } from "node:fs/promises";
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 BOOLEAN_OPTIONS = new Set(["help", "json", "version", "draft", "maintainerCanModify"]);
16
- const VALUE_OPTIONS = new Set(["from", "template", "policy", "repository", "title", "head", "base"]);
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
- if (parsed.options.help === true || (parsed.positionals.length === 0 && parsed.options.version !== true)) {
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
- console.log(JSON.stringify({ templates: discovery.templates }));
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
- console.log(JSON.stringify({ contract, template: contract.templateIdentity, ...projection }));
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.createIssue(prepared.artifact);
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.createPullRequest(prepared.artifact);
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 === "get" && rest[0] !== undefined && isPositiveInteger(rest[0])) {
161
- return runExistingGet(domain, Number(rest[0]), parsed, root, dependencies);
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
- const contracts = selector === undefined
227
- ? await compileRepositoryGovernedContracts(adapter, domain)
228
- : [await compileRepositoryGovernedContract(adapter, domain, selector)];
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
- return { remote, contract: selected.contract, result: selected.result };
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 && error.code === "INPUT_INVALID_JSON")
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
- const reader = createInterface({ input: process.stdin });
418
- for await (const line of reader)
419
- chunks.push(line);
420
- return chunks.join("\n");
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