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/dist/cli.js CHANGED
@@ -1,19 +1,54 @@
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
- import { ArtifactInputError, parseArtifactInputDocument, prepareIssueArtifact, preparePullRequestArtifact, projectExistingArtifact, renderIssueArtifact, renderPullRequestArtifact, selectExistingArtifactCandidate, validateExistingIssueArtifact, validateExistingPullRequestArtifact, } from "./artifact.js";
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, compileRepositoryGovernedContracts, createGovernedIssue, createGovernedPullRequest, discoverRepositoryTemplates, rejectGovernedPolicyOverride, } from "./governance.js";
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 BOOLEAN_OPTIONS = new Set(["help", "json", "version", "draft", "maintainerCanModify"]);
16
- const VALUE_OPTIONS = new Set(["from", "template", "policy", "repository", "title", "head", "base"]);
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
- if (parsed.options.help === true || (parsed.positionals.length === 0 && parsed.options.version !== true)) {
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
- console.log(JSON.stringify({ templates: discovery.templates }));
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
- console.log(JSON.stringify({ contract, template: contract.templateIdentity, ...projection }));
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 === "get" && rest[0] !== undefined && isPositiveInteger(rest[0])) {
161
- return runExistingGet(domain, Number(rest[0]), parsed, root, dependencies);
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 read = await readExistingArtifact(domain, number, parsed, root, dependencies);
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 { remote, contract, result } = await readExistingArtifact(domain, number, parsed, root, dependencies);
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 || error instanceof ArtifactInputError)
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 && error.code === "INPUT_INVALID_JSON")
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
- const reader = createInterface({ input: process.stdin });
418
- for await (const line of reader)
419
- chunks.push(line);
420
- return chunks.join("\n");
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 render never mutate GitHub.`);
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