gh-inari 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3,11 +3,12 @@ import { readFile, stat } from "node:fs/promises";
3
3
  import { readFileSync } from "node:fs";
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";
11
12
  import { discoverSemanticTemplates, importNativeTemplate, renderSemanticCompactSchema, syncSemanticTemplates, SEMANTIC_ISSUE_DIRECTORY, SEMANTIC_PULL_REQUEST_FILE, SEMANTIC_TEMPLATE_DIRECTORY, } from "./semantic-template.js";
12
13
  const EXIT_USAGE = 1;
13
14
  const EXIT_VALIDATION = 2;
@@ -21,6 +22,19 @@ const RUNTIME_CAPABILITIES = [
21
22
  "extension-bootstrap",
22
23
  ];
23
24
  const CANONICAL_INVOCATION = "gh inari";
25
+ const KNOWN_ARTIFACT_COMMANDS = new Set([
26
+ "schema",
27
+ "validate",
28
+ "render",
29
+ "create",
30
+ "explain",
31
+ "get",
32
+ "check",
33
+ "edit",
34
+ "normalize",
35
+ "sync",
36
+ ]);
37
+ const KNOWN_TEMPLATE_COMMANDS = new Set(["list", "sync", "import"]);
24
38
  const INSTALL_COMMAND = "gh extension install yohn-jp/gh-inari";
25
39
  const UPDATE_COMMAND = "gh extension upgrade inari";
26
40
  const FALLBACK_COMMAND = "npx --yes gh-inari";
@@ -34,6 +48,7 @@ const BOOLEAN_OPTIONS = new Set([
34
48
  "maintainerCanModify",
35
49
  "compact",
36
50
  "check",
51
+ "dryRun",
37
52
  ]);
38
53
  const VALUE_OPTIONS = new Set([
39
54
  "from",
@@ -50,6 +65,8 @@ const VALUE_OPTIONS = new Set([
50
65
  /** The installed gh-inari executable entrypoint. */
51
66
  export async function runCli(argv, dependencies = {}) {
52
67
  const metadata = dependencies.packageMetadata ?? readPackageMetadata();
68
+ if (!isOwnedInvocation(argv))
69
+ return runGhFallback(argv, dependencies);
53
70
  let parsed;
54
71
  try {
55
72
  parsed = parseArguments(argv);
@@ -68,9 +85,10 @@ export async function runCli(argv, dependencies = {}) {
68
85
  parsed.positionals[0] === "diagnose" ||
69
86
  parsed.positionals[0] === "doctor";
70
87
  const versionRequested = parsed.options.version === true || parsed.positionals[0] === "version";
71
- if (parsed.options.help === true || (parsed.positionals.length === 0 && !versionRequested && !diagnosticRequested)) {
72
- printHelp();
73
- return parsed.positionals.length === 0 && parsed.options.help !== true ? EXIT_USAGE : 0;
88
+ const helpRequested = parsed.options.help !== undefined && parsed.options.help !== false;
89
+ if (helpRequested || (parsed.positionals.length === 0 && !versionRequested && !diagnosticRequested)) {
90
+ printHelpFor(parsed.positionals, parsed.options.help);
91
+ return parsed.positionals.length === 0 && !helpRequested ? EXIT_USAGE : 0;
74
92
  }
75
93
  const json = parsed.options.json === true;
76
94
  try {
@@ -280,6 +298,17 @@ function runGhDiagnosticCommand(args) {
280
298
  };
281
299
  }
282
300
  }
301
+ /** Delegates argv gh-inari does not own to the real `gh` binary, so `gh inari` is a strict superset of `gh`. */
302
+ function runGhFallback(argv, dependencies) {
303
+ const execute = dependencies.runGhFallback ?? runGhPassthroughCommand;
304
+ return execute(argv);
305
+ }
306
+ function runGhPassthroughCommand(argv) {
307
+ const result = spawnSync("gh", [...argv], { stdio: "inherit" });
308
+ if (result.error)
309
+ throw new CliError("GH_FALLBACK_FAILED", `Cannot execute gh: ${result.error.message}.`);
310
+ return result.status ?? EXIT_INTERNAL;
311
+ }
283
312
  function hasInariExtension(output) {
284
313
  return output.split(/\r?\n/u).some((line) => /^\s*gh\s+inari(?:\s|$)/u.test(line));
285
314
  }
@@ -474,6 +503,12 @@ async function runArtifactCommand(domain, command, rest, parsed, root, dependenc
474
503
  console.log(JSON.stringify({ ok: true, artifact: created.artifact, governance: created.governance }));
475
504
  return 0;
476
505
  }
506
+ if (command === "check" || command === "edit" || command === "normalize" || command === "sync") {
507
+ if (rest[0] === undefined || !isPositiveInteger(rest[0])) {
508
+ throw invalidArtifactNumberError(domain, rest[0]);
509
+ }
510
+ return runExistingRemediation(domain, command, Number(rest[0]), parsed, root, dependencies, json);
511
+ }
477
512
  if ((command === "validate" || command === "explain") &&
478
513
  rest[0] !== undefined &&
479
514
  isPositiveInteger(rest[0]) &&
@@ -493,7 +528,9 @@ async function runArtifactCommand(domain, command, rest, parsed, root, dependenc
493
528
  }
494
529
  async function runExistingValidation(domain, number, parsed, root, dependencies, json) {
495
530
  rejectGovernedPolicyOverride(parsed.options.policy);
496
- const read = await readExistingArtifact(domain, number, parsed, root, dependencies);
531
+ const adapter = createAdapter(dependencies, root, parsed.options.repository);
532
+ await adapter.resolveRepositoryContext();
533
+ const read = await readGovernedExistingArtifact(adapter, domain, number, templateSelector(parsed, undefined));
497
534
  const { remote, result } = read;
498
535
  const projection = projectExistingArtifact(result);
499
536
  const output = {
@@ -509,7 +546,9 @@ async function runExistingValidation(domain, number, parsed, root, dependencies,
509
546
  }
510
547
  async function runExistingGet(domain, number, parsed, root, dependencies) {
511
548
  rejectGovernedPolicyOverride(parsed.options.policy);
512
- const { remote, contract, result } = await readExistingArtifact(domain, number, parsed, root, dependencies);
549
+ const adapter = createAdapter(dependencies, root, parsed.options.repository);
550
+ await adapter.resolveRepositoryContext();
551
+ const { remote, contract, result } = await readGovernedExistingArtifact(adapter, domain, number, templateSelector(parsed, undefined));
513
552
  const projection = projectExistingArtifact(result);
514
553
  const output = {
515
554
  valid: projection.valid,
@@ -527,6 +566,74 @@ async function runExistingGet(domain, number, parsed, root, dependencies) {
527
566
  console.log(JSON.stringify(output));
528
567
  return result.valid ? 0 : EXIT_VALIDATION;
529
568
  }
569
+ async function runExistingRemediation(domain, operation, number, parsed, root, dependencies, json) {
570
+ void json;
571
+ rejectGovernedPolicyOverride(parsed.options.policy);
572
+ const adapter = createAdapter(dependencies, root, parsed.options.repository);
573
+ await adapter.resolveRepositoryContext();
574
+ const read = await readGovernedExistingArtifact(adapter, domain, number, templateSelector(parsed, undefined));
575
+ const assessment = assessExistingArtifact(domain, read);
576
+ const base = {
577
+ operation,
578
+ kind: domain === "issue" ? "issue" : "pull_request",
579
+ number: read.remote.number,
580
+ url: read.remote.url,
581
+ ...(read.contract === undefined ? {} : { template: read.contract.templateIdentity }),
582
+ };
583
+ if (operation === "check") {
584
+ console.log(JSON.stringify({
585
+ ok: assessment.status === "valid-current",
586
+ ...base,
587
+ status: assessment.status,
588
+ classification: read.result.classification,
589
+ valid: assessment.status === "valid-current",
590
+ normalizable: assessment.normalizable,
591
+ diagnostics: assessment.diagnostics,
592
+ violations: assessment.status === "non-canonical" ? assessment.diagnostics : read.result.violations,
593
+ }));
594
+ return assessment.status === "valid-current" ? 0 : EXIT_VALIDATION;
595
+ }
596
+ if (read.contract === undefined) {
597
+ throw new RemediationError(operation === "normalize" ? "NORMALIZATION_UNSAFE" : "SYNC_CURRENT_UNSUPPORTED", "No authoritative template could be selected for the existing artifact.", "$.template");
598
+ }
599
+ let desiredInput;
600
+ if (operation === "normalize") {
601
+ if (!read.result.valid || !read.result.parse.parsed) {
602
+ throw new RemediationError("NORMALIZATION_UNSAFE", "Normalization requires a semantically valid artifact whose values can be round-tripped canonically.", "$.artifact");
603
+ }
604
+ desiredInput = currentArtifactInput(domain, read);
605
+ }
606
+ else {
607
+ const input = await readInputDocument(parsed.options.from);
608
+ desiredInput =
609
+ operation === "edit" ? applySemanticPatch(domain, read, input) : prepareSyncInput(domain, read, input);
610
+ }
611
+ const desired = prepareRemediationArtifact(domain, read.contract, desiredInput);
612
+ const diff = diffArtifact(domain, read, desired);
613
+ const resultBase = {
614
+ ...base,
615
+ changed: diff.changed,
616
+ noOp: !diff.changed,
617
+ diff,
618
+ };
619
+ if (!diff.changed || parsed.options.dryRun === true) {
620
+ console.log(JSON.stringify({
621
+ ok: true,
622
+ ...resultBase,
623
+ ...(parsed.options.dryRun === true ? { dryRun: true, mutation: "not-performed" } : {}),
624
+ }));
625
+ return 0;
626
+ }
627
+ const mutated = await updateGovernedExistingArtifact(adapter, domain, number, desired);
628
+ console.log(JSON.stringify({
629
+ ok: true,
630
+ ...resultBase,
631
+ mutation: "applied",
632
+ artifact: { number: mutated.artifact.number, url: mutated.artifact.url },
633
+ governance: mutated.governance,
634
+ }));
635
+ return 0;
636
+ }
530
637
  function existingArtifactMetadata(domain, remote) {
531
638
  if (domain === "issue") {
532
639
  if (!("labels" in remote) || !("assignees" in remote))
@@ -548,58 +655,6 @@ function existingArtifactMetadata(domain, remote) {
548
655
  base: remote.base,
549
656
  };
550
657
  }
551
- async function readExistingArtifact(domain, number, parsed, root, dependencies) {
552
- const adapter = createAdapter(dependencies, root, parsed.options.repository);
553
- await adapter.resolveRepositoryContext();
554
- const selector = templateSelector(parsed, undefined);
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
- }
566
- const remote = domain === "issue" ? await adapter.getIssue(number) : await adapter.getPullRequest(number);
567
- const candidates = contracts.map((contract) => ({
568
- contract,
569
- result: domain === "issue"
570
- ? validateExistingIssueArtifact(contract, remote.body)
571
- : validateExistingPullRequestArtifact(contract, remote.body),
572
- }));
573
- const selected = selectExistingArtifactCandidate(candidates);
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
- };
602
- }
603
658
  function createAdapter(dependencies, root, repository) {
604
659
  const factory = dependencies.createAdapter ?? ((options) => new GitHubAdapter(options));
605
660
  return factory({ cwd: root, ...(typeof repository === "string" ? { repository } : {}) });
@@ -677,10 +732,14 @@ function parseArguments(argv) {
677
732
  options[key] = true;
678
733
  continue;
679
734
  }
680
- const booleanValue = token.slice(equalIndex + 1);
681
- if (booleanValue !== "true" && booleanValue !== "false")
735
+ const rawValue = token.slice(equalIndex + 1);
736
+ if (key === "help" && rawValue === "full") {
737
+ options[key] = rawValue;
738
+ continue;
739
+ }
740
+ if (rawValue !== "true" && rawValue !== "false")
682
741
  throw new CliError("INVALID_OPTION", `Option --${rawKey} must be true or false.`);
683
- options[key] = booleanValue === "true";
742
+ options[key] = rawValue === "true";
684
743
  continue;
685
744
  }
686
745
  if (!VALUE_OPTIONS.has(key))
@@ -717,7 +776,9 @@ function toErrorShape(error) {
717
776
  return { code: "INTERNAL_ERROR", message: error instanceof Error ? error.message : "Operation failed." };
718
777
  }
719
778
  function classifyExitCode(error) {
720
- if (error instanceof SemanticValidationError || error instanceof ArtifactInputError)
779
+ if (error instanceof SemanticValidationError ||
780
+ error instanceof ArtifactInputError ||
781
+ error instanceof RemediationError)
721
782
  return EXIT_VALIDATION;
722
783
  if (isGitHubAdapterError(error))
723
784
  return EXIT_REMOTE;
@@ -744,14 +805,36 @@ function classifyExitCode(error) {
744
805
  return EXIT_VALIDATION;
745
806
  return EXIT_INTERNAL;
746
807
  }
808
+ /**
809
+ * True when argv targets a command gh-inari implements; false means it must fall back to the real `gh` binary.
810
+ * `--help` on an unowned domain or subcommand (e.g. `repo view --help`, `pr list --help`) is not claimed here so
811
+ * that help delegates to real `gh` the same way execution does -- Inari does not reproduce upstream help text.
812
+ */
813
+ function isOwnedInvocation(argv) {
814
+ const first = argv.find((token) => !token.startsWith("--"));
815
+ if (first === undefined)
816
+ return true;
817
+ if (first === "diagnose" || first === "doctor" || first === "version" || first === "help")
818
+ return true;
819
+ if (argv.includes("--version") || argv.includes("--diagnose") || argv.includes("--doctor"))
820
+ return true;
821
+ const helpRequested = argv.some((token) => token === "--help" || token.startsWith("--help="));
822
+ if (first === "template") {
823
+ const second = argv.slice(argv.indexOf(first) + 1).find((token) => !token.startsWith("--"));
824
+ if (helpRequested && second === undefined)
825
+ return true;
826
+ return second !== undefined && KNOWN_TEMPLATE_COMMANDS.has(second);
827
+ }
828
+ if (first === "issue" || first === "pr") {
829
+ const second = argv.slice(argv.indexOf(first) + 1).find((token) => !token.startsWith("--"));
830
+ if (helpRequested && second === undefined)
831
+ return true;
832
+ return second !== undefined && KNOWN_ARTIFACT_COMMANDS.has(second);
833
+ }
834
+ return false;
835
+ }
747
836
  function isMachineCommand(positionals) {
748
- return ((positionals.length >= 2 &&
749
- (positionals[1] === "schema" ||
750
- positionals[1] === "validate" ||
751
- positionals[1] === "render" ||
752
- positionals[1] === "create" ||
753
- positionals[1] === "explain" ||
754
- positionals[1] === "get")) ||
837
+ return ((positionals.length >= 2 && KNOWN_ARTIFACT_COMMANDS.has(positionals[1] ?? "")) ||
755
838
  positionals[0] === "diagnose" ||
756
839
  positionals[0] === "doctor" ||
757
840
  positionals[0] === "version");
@@ -765,12 +848,7 @@ function isMachineCommandTokens(argv) {
765
848
  if (domainIndex < 0)
766
849
  return false;
767
850
  const command = argv[domainIndex + 1];
768
- return (command === "schema" ||
769
- command === "validate" ||
770
- command === "render" ||
771
- command === "create" ||
772
- command === "explain" ||
773
- command === "get");
851
+ return command !== undefined && KNOWN_ARTIFACT_COMMANDS.has(command);
774
852
  }
775
853
  function isPositiveInteger(value) {
776
854
  return /^[1-9]\d*$/u.test(value);
@@ -805,8 +883,166 @@ async function readStdin() {
805
883
  }
806
884
  return Buffer.concat(chunks).toString("utf8");
807
885
  }
808
- function printHelp() {
809
- console.log(`Usage: gh-inari <command> [options]
886
+ const TEMPLATE_LEAVES = {
887
+ list: {
888
+ usage: "template list",
889
+ summary: "List discovered repository-native and semantic templates.",
890
+ example: "inari template list",
891
+ },
892
+ sync: {
893
+ usage: "template sync [--check]",
894
+ summary: "Regenerate native GitHub templates from semantic template contracts under .github/inari/.",
895
+ example: "inari template sync --check",
896
+ },
897
+ import: {
898
+ usage: "template import --from <native-template> [--to <semantic-file>]",
899
+ summary: "Import a native GitHub template into a semantic template contract. Discovered semantic paths: " +
900
+ ".github/inari/issues/<id>.json, .github/inari/pull-request.json (single PR template), or " +
901
+ ".github/inari/pull-requests/<id>.json (multiple PR templates). Other --to paths write successfully " +
902
+ "but are never discovered.",
903
+ example: "inari template import --from .github/ISSUE_TEMPLATE/feature.yml",
904
+ },
905
+ };
906
+ function artifactLeaves(domain) {
907
+ const noun = domain === "issue" ? "issue" : "pull request";
908
+ return {
909
+ schema: {
910
+ usage: `${domain} schema [template]`,
911
+ summary: `Print the semantic field schema for a ${noun} template.`,
912
+ example: `inari ${domain} schema feature --compact`,
913
+ },
914
+ validate: {
915
+ usage: `${domain} validate --template <template> --from <file.json>`,
916
+ summary: `Validate local JSON input against a template's schema. ` +
917
+ `To validate an existing ${noun} instead, use \`${domain} validate <number> [--template <template>]\`.`,
918
+ example: `inari ${domain} validate --template feature --from ${domain}.json`,
919
+ },
920
+ render: {
921
+ usage: `${domain} render --template <template> --from <file.json>`,
922
+ summary: `Render validated JSON input into canonical Markdown without mutating GitHub.`,
923
+ example: `inari ${domain} render --template feature --from ${domain}.json`,
924
+ },
925
+ create: {
926
+ usage: `${domain} create --template <template> --from <file.json>`,
927
+ summary: `Validate, render, and create a governed ${noun} on GitHub.`,
928
+ example: `inari ${domain} create --template feature --from ${domain}.json`,
929
+ },
930
+ explain: {
931
+ usage: `${domain} explain <number> [--template <template>]`,
932
+ summary: `Explain why an existing ${noun} does or does not satisfy its governed contract.`,
933
+ example: `inari ${domain} explain 123`,
934
+ },
935
+ get: {
936
+ usage: `${domain} get <number> [--template <template>] --json`,
937
+ summary: `Project an existing ${noun} as its canonical semantic JSON.`,
938
+ example: `inari ${domain} get 123 --json`,
939
+ },
940
+ check: {
941
+ usage: `${domain} check <number> [--template <template>]`,
942
+ summary: `Check whether an existing ${noun} is normalizable without mutating GitHub.`,
943
+ example: `inari ${domain} check 123`,
944
+ },
945
+ edit: {
946
+ usage: `${domain} edit <number> --from <file.json> [--dry-run]`,
947
+ summary: `Apply an explicit semantic patch to an existing ${noun}.`,
948
+ example: `inari ${domain} edit 123 --from patch.json --dry-run`,
949
+ },
950
+ normalize: {
951
+ usage: `${domain} normalize <number> [--dry-run]`,
952
+ summary: `Repair an existing ${noun}'s native projection while preserving its existing semantic values.`,
953
+ example: `inari ${domain} normalize 123 --dry-run`,
954
+ },
955
+ sync: {
956
+ usage: `${domain} sync <number> --from <file.json> [--dry-run]`,
957
+ summary: `Reconcile an existing ${noun} to a complete desired semantic state.`,
958
+ example: `inari ${domain} sync 123 --from desired.json --dry-run`,
959
+ },
960
+ };
961
+ }
962
+ const GLOBAL_OPTIONS = ` --from <path> JSON input file, or - for stdin
963
+ --template <id> Repository-native template id, path, or unique name
964
+ --policy <path> Local PR policy for schema/validate/render --from workflows; forbidden for governed remote operations
965
+ --repository <r> GitHub repository override; governed commands use its default-branch governance
966
+ --title <title> Issue/PR title for create
967
+ --head <branch> PR head branch for create
968
+ --base <branch> PR base branch for create
969
+ --compact Emit only semantic fields and constraints for schema
970
+ --check Check generated native projections without writing
971
+ --dry-run Show a bounded remediation diff without mutating GitHub
972
+ --draft Create the PR as a draft
973
+ --maintainer-can-modify
974
+ Allow maintainer edits on the PR
975
+ --json Emit structured JSON output
976
+ --version Print package version
977
+ --diagnose Check the canonical gh extension and recovery path
978
+ --require-capability <id>
979
+ Require a capability in --version/--diagnose checks
980
+ --minimum-version <v>
981
+ Require a minimum semantic version in checks
982
+ --help Print this help (--help=full for the complete reference)`;
983
+ const DOMAIN_PASSTHROUGH_EXAMPLE = {
984
+ issue: "issue list",
985
+ pr: "pr checks",
986
+ template: "template view",
987
+ };
988
+ /** Dispatches to root, domain, or leaf help by command depth; positionals are pre-parse-error tokens, so any --help value routes here. */
989
+ function printHelpFor(positionals, helpValue) {
990
+ if (helpValue === "full")
991
+ return printFullHelp();
992
+ const [domain, command] = positionals;
993
+ if (domain === "issue" || domain === "pr") {
994
+ if (command !== undefined && command in artifactLeaves(domain))
995
+ return printLeafHelp(artifactLeaves(domain)[command]);
996
+ return printDomainHelp(domain, artifactLeaves(domain));
997
+ }
998
+ if (domain === "template") {
999
+ if (command !== undefined && command in TEMPLATE_LEAVES)
1000
+ return printLeafHelp(TEMPLATE_LEAVES[command]);
1001
+ return printDomainHelp("template", TEMPLATE_LEAVES);
1002
+ }
1003
+ printRootHelp();
1004
+ }
1005
+ function printRootHelp() {
1006
+ console.log(`Usage: inari <command> [...]
1007
+
1008
+ A governed GitHub CLI. Issue and PR commands under governed templates run
1009
+ through Inari; every other command passes through to the real gh binary
1010
+ with the original argv and exit status.
1011
+
1012
+ Domains:
1013
+ issue Governed Issue schema, validation, rendering, and lifecycle
1014
+ pr Governed pull request schema, validation, rendering, and lifecycle
1015
+ template Semantic template authoring and native template sync
1016
+
1017
+ All other commands (e.g. repo, auth, pr list, issue view) are passed through to gh.
1018
+
1019
+ Run \`inari <domain> --help\` for that domain's operations.
1020
+ Run \`inari --help=full\` for the complete command and option reference.
1021
+ Run \`inari --version\` or \`inari --diagnose\` for machine-readable runtime checks.`);
1022
+ }
1023
+ function printDomainHelp(domain, leaves) {
1024
+ const lines = Object.values(leaves).map((leaf) => ` ${leaf.usage}`);
1025
+ console.log(`Usage: inari ${domain} <command> [...]
1026
+
1027
+ Operations:
1028
+ ${lines.join("\n")}
1029
+
1030
+ Commands outside this list under "${domain}" (e.g. \`${DOMAIN_PASSTHROUGH_EXAMPLE[domain]}\`) pass through to gh.
1031
+
1032
+ Run \`inari ${domain} <command> --help\` for that command's inputs and an example.`);
1033
+ }
1034
+ function printLeafHelp(leaf) {
1035
+ console.log(`Usage: inari ${leaf.usage}
1036
+
1037
+ ${leaf.summary}
1038
+
1039
+ Example:
1040
+ ${leaf.example}
1041
+
1042
+ Run \`inari --help=full\` for the complete option reference.`);
1043
+ }
1044
+ function printFullHelp() {
1045
+ console.log(`Usage: inari <command> [options]
810
1046
 
811
1047
  Commands:
812
1048
  template list
@@ -823,6 +1059,10 @@ Commands:
823
1059
  issue validate <number> [--template <template>]
824
1060
  issue explain <number> [--template <template>]
825
1061
  issue get <number> [--template <template>] --json
1062
+ issue check <number> [--template <template>]
1063
+ issue edit <number> --from <file.json> [--dry-run]
1064
+ issue normalize <number> [--dry-run]
1065
+ issue sync <number> --from <file.json> [--dry-run]
826
1066
  pr schema [template]
827
1067
  pr validate --template <template> --from <file.json>
828
1068
  pr render --template <template> --from <file.json>
@@ -830,32 +1070,21 @@ Commands:
830
1070
  pr validate <number> [--template <template>]
831
1071
  pr explain <number> [--template <template>]
832
1072
  pr get <number> [--template <template>] --json
1073
+ pr check <number> [--template <template>]
1074
+ pr edit <number> --from <file.json> [--dry-run]
1075
+ pr normalize <number> [--dry-run]
1076
+ pr sync <number> --from <file.json> [--dry-run]
833
1077
 
834
1078
  Options:
835
- --from <path> JSON input file, or - for stdin
836
- --template <id> Repository-native template id, path, or unique name
837
- --policy <path> Local PR policy for schema/validate/render --from workflows; forbidden for governed remote operations
838
- --repository <r> GitHub repository override; governed commands use its default-branch governance
839
- --title <title> Issue/PR title for create
840
- --head <branch> PR head branch for create
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
844
- --draft Create the PR as a draft
845
- --maintainer-can-modify
846
- Allow maintainer edits on the PR
847
- --json Emit structured JSON output
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
854
- --help Print this help
1079
+ ${GLOBAL_OPTIONS}
1080
+
1081
+ Create always validates and renders before invoking gh. Schema, validate, render, check, and --dry-run remediation never mutate GitHub.
1082
+ Edit applies an explicit semantic patch; normalize preserves existing semantic values; sync reconciles a complete desired semantic state.
855
1083
 
856
- Create always validates and renders before invoking gh. Schema, validate, and render never mutate GitHub.
1084
+ All other commands pass through to the real gh binary unchanged.
857
1085
 
858
- Canonical installation: gh extension install yohn-jp/gh-inari
859
- PATH-independent fallback: npx --yes gh-inari`);
1086
+ Canonical install: npm install --global gh-inari
1087
+ PATH-independent fallback: npx --yes gh-inari
1088
+ Extension compatibility path: gh extension install yohn-jp/gh-inari`);
860
1089
  }
861
1090
  //# sourceMappingURL=cli.js.map