@enrichlayer/el-linear 1.20.0 → 1.21.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/README.md +24 -0
- package/dist/commands/issues.js +4 -1
- package/dist/commands/read-shortcut.js +68 -4
- package/dist/utils/with-includes.d.ts +40 -0
- package/dist/utils/with-includes.js +55 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -599,6 +599,30 @@ with `--field`. (Named `--sections` rather than the seemingly-obvious
|
|
|
599
599
|
`--fields` because `--fields` is already taken at the program level for
|
|
600
600
|
output-key filtering — `el-linear` is the namespace owner.)
|
|
601
601
|
|
|
602
|
+
### Opt-in includes: `--with`
|
|
603
|
+
|
|
604
|
+
`issues read --with <names>` adds extra blocks of related data to the
|
|
605
|
+
JSON envelope. Comma-separated; unknown values are rejected with the
|
|
606
|
+
candidate list.
|
|
607
|
+
|
|
608
|
+
Currently supported:
|
|
609
|
+
|
|
610
|
+
| Include | What it adds |
|
|
611
|
+
|---------|--------------|
|
|
612
|
+
| `relations` | A top-level `relations` array (outgoing + incoming cross-issue links), built from the same data as `issues related`. |
|
|
613
|
+
|
|
614
|
+
```bash
|
|
615
|
+
# Issue + its sidebar relations in one call:
|
|
616
|
+
el-linear issues read DEV-123 --with relations
|
|
617
|
+
|
|
618
|
+
# Across multiple issues — relations fetched per issue in parallel:
|
|
619
|
+
el-linear read DEV-1 DEV-2 --with relations | jq '.data[].relations'
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
`--with` is JSON-only — it composes with `--jq` / `--fields` / `--raw`,
|
|
623
|
+
and is mutually exclusive with `--field` (which prints raw section text,
|
|
624
|
+
no envelope).
|
|
625
|
+
|
|
602
626
|
## Wrapping Linear references in arbitrary text
|
|
603
627
|
|
|
604
628
|
`el-linear refs wrap` takes plain text on stdin (or via `--file`) and rewrites
|
package/dist/commands/issues.js
CHANGED
|
@@ -1000,7 +1000,10 @@ export function setupIssuesCommands(program) {
|
|
|
1000
1000
|
.option("--sections <names>", 'Extract multiple named description sections in one call (comma-separated, e.g. "Done when,Out of scope"). ' +
|
|
1001
1001
|
"Single-issue only. Returns a JSON envelope { identifier, sections: { name -> text|null } }; missing sections appear as null + a _warnings entry. " +
|
|
1002
1002
|
"Sibling of --field (singular). Named --sections rather than --fields because the program already has a global --fields for output-key filtering.")
|
|
1003
|
-
.
|
|
1003
|
+
.option("--with <names>", "Comma-separated opt-in includes. Each value fetches an extra " +
|
|
1004
|
+
'block of data and adds it to the JSON envelope. Currently supported: "relations" ' +
|
|
1005
|
+
"(adds an array of cross-issue relations under a top-level `relations` key).")
|
|
1006
|
+
.addHelpText("after", '\nBoth UUID and identifiers like ABC-123 are supported.\nMultiple IDs: el-linear issue get DEV-123 DEV-456 DEV-789\nExtract a section: el-linear issue read DEV-123 --field "Done when"\nMulti-section: el-linear issue read DEV-123 --sections "Done when,Out of scope"\nWith relations: el-linear issue read DEV-123 --with relations')
|
|
1004
1007
|
.action(handleAsyncCommand(readIssues));
|
|
1005
1008
|
issues
|
|
1006
1009
|
.command("update <issueId>")
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { GET_ISSUE_RELATIONS_QUERY } from "../queries/issues.js";
|
|
1
2
|
import { downloadLinearUploads } from "../utils/download-uploads.js";
|
|
2
3
|
import { extractField, extractFields } from "../utils/extract-field.js";
|
|
3
4
|
import { createFileService } from "../utils/file-service.js";
|
|
4
5
|
import { createIssuesService } from "../utils/issues-service-bootstrap.js";
|
|
5
6
|
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
6
7
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
8
|
+
import { parseWithIncludes, } from "../utils/with-includes.js";
|
|
9
|
+
import { buildIncomingRelationEntries, buildOutgoingRelationEntries, } from "./issues/relations.js";
|
|
7
10
|
/**
|
|
8
11
|
* Issue ID pattern: 1-5 uppercase letters, dash, 1+ digits (e.g. ADM-652, DEV-12).
|
|
9
12
|
*/
|
|
@@ -26,7 +29,10 @@ export function setupReadShortcut(program) {
|
|
|
26
29
|
.option("--sections <names>", 'Extract multiple named description sections in one call (comma-separated, e.g. "Done when,Out of scope,Steps"). ' +
|
|
27
30
|
"Single-issue only. Returns a JSON envelope { identifier, sections: { name -> text|null } }; missing sections appear as null + a _warnings entry. " +
|
|
28
31
|
"Sibling of --field (singular). Named --sections rather than --fields because the program already has a global --fields for output-key filtering.")
|
|
29
|
-
.
|
|
32
|
+
.option("--with <names>", "Comma-separated opt-in includes. Each value fetches an extra " +
|
|
33
|
+
'block of data and adds it to the JSON envelope. Currently supported: "relations" ' +
|
|
34
|
+
"(adds an array of cross-issue relations under a top-level `relations` key).")
|
|
35
|
+
.addHelpText("after", '\nExamples:\n el-linear read ADM-652\n el-linear get DEV-123 DEV-456\n el-linear ADM-652 (auto-detected)\n el-linear read DEV-123 --field "Done when" (just that section)\n el-linear read DEV-123 --sections "Done when,Out of scope"\n el-linear read DEV-123 --with relations (issue + cross-issue links)')
|
|
30
36
|
.action(handleAsyncCommand(readIssues));
|
|
31
37
|
// Catch-all: if argv looks like `el-linear ADM-652 [DEV-123 ...]`, run read
|
|
32
38
|
const originalParse = program.parse.bind(program);
|
|
@@ -61,10 +67,16 @@ export function setupReadShortcut(program) {
|
|
|
61
67
|
*/
|
|
62
68
|
export async function readIssues(issueIds, options, command) {
|
|
63
69
|
const rootOpts = getRootOpts(command);
|
|
64
|
-
const { issuesService } = await createIssuesService(rootOpts);
|
|
70
|
+
const { graphQLService, issuesService } = await createIssuesService(rootOpts);
|
|
65
71
|
const fileService = await createFileService(rootOpts);
|
|
66
72
|
const fieldName = typeof options.field === "string" ? options.field : null;
|
|
67
73
|
const sectionsRaw = typeof options.sections === "string" ? options.sections : null;
|
|
74
|
+
// DEV-4476: --with opt-in includes (currently `relations`). Throws on
|
|
75
|
+
// unknown values via parseWithIncludes — fail fast in the CLI per the
|
|
76
|
+
// deterministic-CLI doctrine.
|
|
77
|
+
const includes = typeof options.with === "string"
|
|
78
|
+
? parseWithIncludes(options.with)
|
|
79
|
+
: { relations: false };
|
|
68
80
|
if (fieldName && sectionsRaw) {
|
|
69
81
|
throw new Error("--field and --sections are mutually exclusive. Use --field for a single section (plain-text output) or --sections for multiple (JSON map).");
|
|
70
82
|
}
|
|
@@ -87,6 +99,12 @@ export async function readIssues(issueIds, options, command) {
|
|
|
87
99
|
if (sectionNames !== null && sectionNames.length === 0) {
|
|
88
100
|
throw new Error("--sections was empty after trimming. Pass a comma-separated list of section names.");
|
|
89
101
|
}
|
|
102
|
+
// --field is also an extract-this-section operation; pairing it with
|
|
103
|
+
// --with would produce ambiguous output (section text vs. JSON envelope).
|
|
104
|
+
// Reject up front rather than silently dropping one.
|
|
105
|
+
if (fieldName && includes.relations) {
|
|
106
|
+
throw new Error("--field and --with are mutually exclusive (--field outputs raw section text; --with extends the JSON envelope).");
|
|
107
|
+
}
|
|
90
108
|
if (issueIds.length === 1) {
|
|
91
109
|
const issue = await issuesService.getIssueById(issueIds[0]);
|
|
92
110
|
const resolved = await downloadLinearUploads(issue, fileService);
|
|
@@ -124,7 +142,15 @@ export async function readIssues(issueIds, options, command) {
|
|
|
124
142
|
});
|
|
125
143
|
return;
|
|
126
144
|
}
|
|
127
|
-
|
|
145
|
+
// DEV-4476: --with relations (mutually exclusive with --sections, which
|
|
146
|
+
// returns above). Enrich the single-issue envelope when requested.
|
|
147
|
+
const envelope = includes.relations
|
|
148
|
+
? {
|
|
149
|
+
...resolved,
|
|
150
|
+
relations: await fetchRelations(graphQLService, resolved.id),
|
|
151
|
+
}
|
|
152
|
+
: resolved;
|
|
153
|
+
outputSuccess(envelope);
|
|
128
154
|
}
|
|
129
155
|
else {
|
|
130
156
|
// DEV-4477: one batched GraphQL call instead of N parallel single-issue
|
|
@@ -133,7 +159,45 @@ export async function readIssues(issueIds, options, command) {
|
|
|
133
159
|
// that's HTTP, not GraphQL, and downloadLinearUploads is a no-op when
|
|
134
160
|
// there's nothing to download.
|
|
135
161
|
const issues = await issuesService.getIssuesByRefs(issueIds);
|
|
136
|
-
const results = await Promise.all(
|
|
162
|
+
const results = await Promise.all(
|
|
163
|
+
// Apply --with relations over the batch-fetched issues (DEV-4476),
|
|
164
|
+
// preserving DEV-4477's single batched GraphQL fetch above — don't
|
|
165
|
+
// re-fetch per id, which would defeat the batch optimization.
|
|
166
|
+
issues.map(async (issue) => {
|
|
167
|
+
const resolved = await downloadLinearUploads(issue, fileService);
|
|
168
|
+
if (!includes.relations) {
|
|
169
|
+
return resolved;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
...resolved,
|
|
173
|
+
relations: await fetchRelations(graphQLService, resolved.id),
|
|
174
|
+
};
|
|
175
|
+
}));
|
|
137
176
|
outputSuccess(results);
|
|
138
177
|
}
|
|
139
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Fetch relations for a known-UUID issue and flatten outgoing + incoming
|
|
181
|
+
* via the shared relations builders. `relatedIssue` / `issue` peers that
|
|
182
|
+
* Linear omits (rare — deleted-relation edge case) are skipped, matching
|
|
183
|
+
* the `issues related` command's behavior.
|
|
184
|
+
*
|
|
185
|
+
* Race semantics: when `result.issue` is null (the issue existed at the
|
|
186
|
+
* `getIssueById` call upstream but is missing here — Linear deleted /
|
|
187
|
+
* unarchived it between the two calls), returns `[]` rather than
|
|
188
|
+
* throwing. The base issue is still emitted, and the caller sees an
|
|
189
|
+
* empty `relations` array — preferable to failing the whole envelope
|
|
190
|
+
* for a rare race. This intentionally diverges from `handleRelatedIssues`
|
|
191
|
+
* (which throws), because that command's *primary* output is relations,
|
|
192
|
+
* whereas here relations are an opt-in side dish.
|
|
193
|
+
*/
|
|
194
|
+
async function fetchRelations(graphQLService, issueId) {
|
|
195
|
+
const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, { id: issueId });
|
|
196
|
+
if (!result.issue) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
return [
|
|
200
|
+
...buildOutgoingRelationEntries(result.issue.relations.nodes),
|
|
201
|
+
...buildIncomingRelationEntries(result.issue.inverseRelations.nodes),
|
|
202
|
+
];
|
|
203
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `issues read --with` parser (DEV-4476).
|
|
3
|
+
*
|
|
4
|
+
* Opt-in includes for `issues read`. Each value names an additional block
|
|
5
|
+
* of data to fetch alongside the base issue and inject into the JSON
|
|
6
|
+
* envelope. Comma-separated; whitespace tolerated; unknown values rejected
|
|
7
|
+
* with a structured error naming the candidates (deterministic-CLI
|
|
8
|
+
* doctrine — fail fast in the CLI, not in the consumer's script).
|
|
9
|
+
*
|
|
10
|
+
* Currently supported:
|
|
11
|
+
* - `relations` — fetches `Issue.relations` + `Issue.inverseRelations`
|
|
12
|
+
* and adds a `relations` array to the envelope.
|
|
13
|
+
*
|
|
14
|
+
* Reserved for future MRs (the value space is a closed set so adding more
|
|
15
|
+
* later is back-compat):
|
|
16
|
+
* - `children` — refetch with expanded sub-issue fragment (state,
|
|
17
|
+
* assignee, priority — beyond the default id/identifier/
|
|
18
|
+
* title trio that's already in the envelope).
|
|
19
|
+
* - `comments` — no-op for fetching (comments are already in the
|
|
20
|
+
* default envelope via `_WITH_COMMENTS` fragment) but
|
|
21
|
+
* would gate explicit summary-format rendering.
|
|
22
|
+
*
|
|
23
|
+
* Doctrine note: comments are intentionally NOT included today because
|
|
24
|
+
* adding `--with comments` would imply that comments are *off* by default,
|
|
25
|
+
* which would be a breaking JSON-shape change.
|
|
26
|
+
*/
|
|
27
|
+
export declare const WITH_INCLUDE_VALUES: readonly ["relations"];
|
|
28
|
+
export type WithInclude = (typeof WITH_INCLUDE_VALUES)[number];
|
|
29
|
+
export interface ParsedWithIncludes {
|
|
30
|
+
relations: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse a `--with <names>` argument. Returns a flag object so call sites
|
|
34
|
+
* read `if (includes.relations)` instead of `Set.has("relations")`.
|
|
35
|
+
*
|
|
36
|
+
* Empty / whitespace-only values are caller errors (commander allows
|
|
37
|
+
* `--with ""` through) — reject with the same message as unknown values
|
|
38
|
+
* so the user gets one consistent failure mode.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseWithIncludes(raw: string): ParsedWithIncludes;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `issues read --with` parser (DEV-4476).
|
|
3
|
+
*
|
|
4
|
+
* Opt-in includes for `issues read`. Each value names an additional block
|
|
5
|
+
* of data to fetch alongside the base issue and inject into the JSON
|
|
6
|
+
* envelope. Comma-separated; whitespace tolerated; unknown values rejected
|
|
7
|
+
* with a structured error naming the candidates (deterministic-CLI
|
|
8
|
+
* doctrine — fail fast in the CLI, not in the consumer's script).
|
|
9
|
+
*
|
|
10
|
+
* Currently supported:
|
|
11
|
+
* - `relations` — fetches `Issue.relations` + `Issue.inverseRelations`
|
|
12
|
+
* and adds a `relations` array to the envelope.
|
|
13
|
+
*
|
|
14
|
+
* Reserved for future MRs (the value space is a closed set so adding more
|
|
15
|
+
* later is back-compat):
|
|
16
|
+
* - `children` — refetch with expanded sub-issue fragment (state,
|
|
17
|
+
* assignee, priority — beyond the default id/identifier/
|
|
18
|
+
* title trio that's already in the envelope).
|
|
19
|
+
* - `comments` — no-op for fetching (comments are already in the
|
|
20
|
+
* default envelope via `_WITH_COMMENTS` fragment) but
|
|
21
|
+
* would gate explicit summary-format rendering.
|
|
22
|
+
*
|
|
23
|
+
* Doctrine note: comments are intentionally NOT included today because
|
|
24
|
+
* adding `--with comments` would imply that comments are *off* by default,
|
|
25
|
+
* which would be a breaking JSON-shape change.
|
|
26
|
+
*/
|
|
27
|
+
export const WITH_INCLUDE_VALUES = ["relations"];
|
|
28
|
+
/**
|
|
29
|
+
* Parse a `--with <names>` argument. Returns a flag object so call sites
|
|
30
|
+
* read `if (includes.relations)` instead of `Set.has("relations")`.
|
|
31
|
+
*
|
|
32
|
+
* Empty / whitespace-only values are caller errors (commander allows
|
|
33
|
+
* `--with ""` through) — reject with the same message as unknown values
|
|
34
|
+
* so the user gets one consistent failure mode.
|
|
35
|
+
*/
|
|
36
|
+
export function parseWithIncludes(raw) {
|
|
37
|
+
const names = raw
|
|
38
|
+
.split(",")
|
|
39
|
+
.map((s) => s.trim())
|
|
40
|
+
.filter((s) => s.length > 0);
|
|
41
|
+
if (names.length === 0) {
|
|
42
|
+
throw new Error(`--with requires at least one include name. Supported: ${WITH_INCLUDE_VALUES.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
const includes = { relations: false };
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
if (!WITH_INCLUDE_VALUES.includes(name)) {
|
|
47
|
+
throw new Error(`--with: unknown include "${name}". Supported: ${WITH_INCLUDE_VALUES.join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
// Narrowing: only assignable members of ParsedWithIncludes.
|
|
50
|
+
if (name === "relations") {
|
|
51
|
+
includes.relations = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return includes;
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enrichlayer/el-linear",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.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",
|