@enrichlayer/el-linear 1.33.0 → 1.34.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.
@@ -1042,8 +1042,30 @@ export function setupIssuesCommands(program) {
1042
1042
  const issues = program
1043
1043
  .command("issues")
1044
1044
  .alias("issue")
1045
- .description("Issue operations");
1046
- issues.action(() => issues.help());
1045
+ .description("Issue operations")
1046
+ .argument("[issueId...]", "issue ID(s) to read when no subcommand is given — shorthand for `issues read`")
1047
+ .option("--body", "Print the issue's full description as raw markdown text — no JSON " +
1048
+ "envelope, single-issue only. Exits non-zero if the issue has no " +
1049
+ "description. The whole-body sibling of --field; mutually exclusive " +
1050
+ "with --field / --sections / --with.")
1051
+ .option("--field <name>", 'Extract a single named section from the issue description (e.g. "Done when"). ' +
1052
+ "Matches H2/H3 headers and bold pseudo-headers case-insensitively. " +
1053
+ "Outputs the section text only — no JSON envelope. Single-issue only.")
1054
+ .option("--sections <names>", 'Extract multiple named description sections in one call (comma-separated, e.g. "Done when,Out of scope"). ' +
1055
+ "Single-issue only. Returns a JSON envelope { identifier, sections: { name -> text|null } }; missing sections appear as null + a _warnings entry. " +
1056
+ "Sibling of --field (singular). Named --sections rather than --fields because the program already has a global --fields for output-key filtering.")
1057
+ .option("--with <names>", "Comma-separated opt-in includes. Each value fetches an extra " +
1058
+ 'block of data and adds it to the JSON envelope. Currently supported: "relations" ' +
1059
+ "(adds an array of cross-issue relations under a top-level `relations` key).")
1060
+ .addHelpText("after", "\nNo-subcommand shorthand: `el-linear issue DEV-123` behaves like `issues read DEV-123`, " +
1061
+ "including its options (--body, --field, --sections, --with).");
1062
+ issues.action(handleAsyncCommand((issueIds = [], options, command) => {
1063
+ if (issueIds.length === 0) {
1064
+ issues.help();
1065
+ return Promise.resolve();
1066
+ }
1067
+ return readIssues(issueIds, options, command);
1068
+ }));
1047
1069
  // DEV-4480: `issues tree <ID>` lives in its own file because the
1048
1070
  // recursive query builder + ASCII formatter belong together and are
1049
1071
  // substantial enough to warrant the split.
@@ -42,6 +42,15 @@ export declare function formatLabelList(labels: unknown[]): string;
42
42
  * the row falls back to the stored direction.
43
43
  */
44
44
  export declare function formatRelationList(relations: unknown[], sourceRef?: string): string;
45
+ /**
46
+ * Table render for `issues related <id>` — the `{ data: RelatedIssueEntry[] }`
47
+ * envelope where each entry is `{ type, direction, issue }`. Distinct from
48
+ * `formatRelationList` (the `issues relate` mutation echo, `{ type, issue,
49
+ * relatedIssue }` shape): this is the read-path listing of ALL of an issue's
50
+ * relations, so direction, state, and assignee are surfaced per row instead
51
+ * of collapsing to a single source-oriented type/target/title line.
52
+ */
53
+ export declare function formatIssueRelationList(relations: unknown[]): string;
45
54
  export declare function formatUserSummary(user: Record<string, unknown>): string;
46
55
  export declare function formatUserList(users: unknown[]): string;
47
56
  export declare function formatDocumentSummary(doc: Record<string, unknown>): string;
@@ -59,7 +68,7 @@ export declare function formatSearchResultList(results: unknown[]): string;
59
68
  * the full payload. Lists fall back to a simple bulleted list.
60
69
  */
61
70
  export declare function formatGenericSummary(value: unknown): string;
62
- export type ResourceKind = "issue" | "issue-list" | "project" | "project-list" | "comment" | "comment-list" | "cycle" | "cycle-list" | "milestone" | "milestone-list" | "team-list" | "label-list" | "user" | "user-list" | "document" | "document-list" | "template" | "template-list" | "attachment-list" | "release" | "release-list" | "search-result-list" | "relation-list" | "empty-list" | "generic";
71
+ export type ResourceKind = "issue" | "issue-list" | "project" | "project-list" | "comment" | "comment-list" | "cycle" | "cycle-list" | "milestone" | "milestone-list" | "team-list" | "label-list" | "user" | "user-list" | "document" | "document-list" | "template" | "template-list" | "attachment-list" | "release" | "release-list" | "search-result-list" | "relation-list" | "issue-relation-list" | "empty-list" | "generic";
63
72
  /**
64
73
  * Heuristic — used by the central `outputSuccess` path which doesn't know
65
74
  * which command produced the payload. Looks at the shape of the data to
@@ -749,6 +749,42 @@ export function formatRelationList(relations, sourceRef = "—") {
749
749
  },
750
750
  ], { emptyText: "(no relations)", itemNoun: "relation" });
751
751
  }
752
+ /**
753
+ * Table render for `issues related <id>` — the `{ data: RelatedIssueEntry[] }`
754
+ * envelope where each entry is `{ type, direction, issue }`. Distinct from
755
+ * `formatRelationList` (the `issues relate` mutation echo, `{ type, issue,
756
+ * relatedIssue }` shape): this is the read-path listing of ALL of an issue's
757
+ * relations, so direction, state, and assignee are surfaced per row instead
758
+ * of collapsing to a single source-oriented type/target/title line.
759
+ */
760
+ export function formatIssueRelationList(relations) {
761
+ const rows = relations.map((raw) => asObj(raw) ?? {});
762
+ return renderTable(rows, [
763
+ { header: "RELATION", minWidth: 8, extract: (r) => s(r.type) },
764
+ { header: "DIRECTION", minWidth: 9, extract: (r) => s(r.direction) },
765
+ {
766
+ header: "ID",
767
+ minWidth: 2,
768
+ extract: (r) => s(asObj(r.issue)?.identifier),
769
+ },
770
+ {
771
+ header: "STATE",
772
+ minWidth: 5,
773
+ extract: (r) => getName(asObj(r.issue)?.state),
774
+ },
775
+ {
776
+ header: "ASSIGNEE",
777
+ minWidth: 8,
778
+ extract: (r) => getName(asObj(r.issue)?.assignee),
779
+ },
780
+ {
781
+ header: "TITLE",
782
+ minWidth: 5,
783
+ maxWidth: TITLE_TRUNC,
784
+ extract: (r) => s(asObj(r.issue)?.title),
785
+ },
786
+ ], { emptyText: "(no issue relations)", itemNoun: "relation" });
787
+ }
752
788
  // ── users ──────────────────────────────────────────────────────
753
789
  export function formatUserSummary(user) {
754
790
  const name = s(user.name);
@@ -1032,6 +1068,10 @@ function inferListKind(items) {
1032
1068
  // identifier/title, so this must precede the issue-list check below.
1033
1069
  if ("relatedIssue" in sample && "issue" in sample && "type" in sample)
1034
1070
  return "relation-list";
1071
+ // `issues related <id>` rows ({ type, direction, issue }) — the read-path
1072
+ // listing, distinct from the `relatedIssue`-shaped mutation echo above.
1073
+ if ("direction" in sample && "issue" in sample && "type" in sample)
1074
+ return "issue-relation-list";
1035
1075
  if ("identifier" in sample && "title" in sample) {
1036
1076
  // could be issue or search result
1037
1077
  if ("type" in sample && !("templateData" in sample)) {
@@ -1154,6 +1194,8 @@ export function dispatch(kind, payload, fields) {
1154
1194
  return formatSearchResultList(list ?? []);
1155
1195
  case "relation-list":
1156
1196
  return formatRelationList(list ?? [], s(asObj(obj?.meta)?.source));
1197
+ case "issue-relation-list":
1198
+ return formatIssueRelationList(list ?? []);
1157
1199
  case "empty-list":
1158
1200
  return "(no results)";
1159
1201
  case "generic":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enrichlayer/el-linear",
3
- "version": "1.33.0",
3
+ "version": "1.34.0",
4
4
  "description": "A pragmatic CLI for Linear.app — deterministic team/label/member resolution, structured issue validation, configurable term enforcement, and a GraphQL escape hatch.",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/main.d.ts",