@enrichlayer/el-linear 1.9.0 → 1.15.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 +139 -10
- package/claude-skills/linear-operations/SKILL.md +41 -1
- package/dist/auth/linear-credential.d.ts +27 -0
- package/dist/auth/linear-credential.js +1 -0
- package/dist/auth/oauth-app-config.d.ts +4 -3
- package/dist/auth/oauth-app-config.js +13 -2
- package/dist/auth/oauth-callback.d.ts +2 -3
- package/dist/auth/oauth-callback.js +2 -2
- package/dist/auth/oauth-client.d.ts +8 -2
- package/dist/auth/oauth-client.js +26 -0
- package/dist/auth/oauth-fs.d.ts +2 -1
- package/dist/auth/oauth-headless.d.ts +2 -1
- package/dist/auth/oauth-storage.d.ts +5 -1
- package/dist/auth/oauth-storage.js +1 -1
- package/dist/auth/oauth-token.d.ts +4 -3
- package/dist/auth/oauth-token.js +16 -4
- package/dist/auth/token-resolver.d.ts +14 -5
- package/dist/auth/token-resolver.js +6 -1
- package/dist/commands/attachments.js +2 -1
- package/dist/commands/batch.js +18 -21
- package/dist/commands/comments.js +22 -33
- package/dist/commands/config.js +178 -5
- package/dist/commands/cycles.js +2 -1
- package/dist/commands/documents.js +2 -1
- package/dist/commands/graphql.js +4 -6
- package/dist/commands/init/aliases.js +1 -1
- package/dist/commands/init/defaults.d.ts +2 -1
- package/dist/commands/init/index.js +45 -35
- package/dist/commands/init/oauth.d.ts +4 -1
- package/dist/commands/init/oauth.js +22 -4
- package/dist/commands/init/shared.d.ts +24 -2
- package/dist/commands/init/shared.js +35 -4
- package/dist/commands/init/token.d.ts +3 -3
- package/dist/commands/init/token.js +5 -24
- package/dist/commands/init/workspace.d.ts +2 -1
- package/dist/commands/init/workspace.js +1 -1
- package/dist/commands/introspect.d.ts +27 -0
- package/dist/commands/introspect.js +178 -0
- package/dist/commands/issue-id.js +1 -3
- package/dist/commands/issues/branch.js +9 -1
- package/dist/commands/issues/description.js +2 -6
- package/dist/commands/issues/link-references.d.ts +21 -0
- package/dist/commands/issues/link-references.js +171 -0
- package/dist/commands/issues/relations.d.ts +44 -0
- package/dist/commands/issues/relations.js +132 -0
- package/dist/commands/issues.js +269 -309
- package/dist/commands/labels.js +15 -24
- package/dist/commands/profile.js +1 -0
- package/dist/commands/project-milestones.js +13 -20
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +157 -44
- package/dist/commands/read-shortcut.d.ts +1 -1
- package/dist/commands/read-shortcut.js +28 -8
- package/dist/commands/refs.js +75 -8
- package/dist/commands/releases.js +26 -30
- package/dist/commands/search.js +49 -33
- package/dist/commands/teams.js +2 -1
- package/dist/commands/templates.js +9 -14
- package/dist/commands/users.js +5 -2
- package/dist/config/config.d.ts +99 -1
- package/dist/config/config.js +264 -52
- package/dist/config/error-enrichment.d.ts +62 -0
- package/dist/config/error-enrichment.js +417 -0
- package/dist/config/issue-validation.d.ts +37 -0
- package/dist/config/issue-validation.js +63 -1
- package/dist/config/paths.d.ts +2 -8
- package/dist/config/paths.js +4 -2
- package/dist/config/resolver.d.ts +8 -1
- package/dist/config/resolver.js +11 -5
- package/dist/main.js +13 -1
- package/dist/queries/attachments-types.d.ts +30 -0
- package/dist/queries/attachments-types.js +5 -0
- package/dist/queries/comments-types.d.ts +55 -0
- package/dist/queries/comments-types.js +5 -0
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +8 -0
- package/dist/queries/documents-types.d.ts +62 -0
- package/dist/queries/documents-types.js +9 -0
- package/dist/queries/introspect-types.d.ts +58 -0
- package/dist/queries/introspect-types.js +10 -0
- package/dist/queries/issues-types.d.ts +481 -0
- package/dist/queries/issues-types.js +23 -0
- package/dist/queries/issues.d.ts +51 -10
- package/dist/queries/issues.js +147 -5
- package/dist/queries/labels-types.d.ts +65 -0
- package/dist/queries/labels-types.js +5 -0
- package/dist/queries/project-milestones-types.d.ts +92 -0
- package/dist/queries/project-milestones-types.js +10 -0
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/queries/projects-types.d.ts +76 -0
- package/dist/queries/projects-types.js +5 -0
- package/dist/queries/projects.d.ts +2 -0
- package/dist/queries/projects.js +22 -0
- package/dist/queries/releases-types.d.ts +85 -0
- package/dist/queries/releases-types.js +5 -0
- package/dist/queries/search-types.d.ts +102 -0
- package/dist/queries/search-types.js +6 -0
- package/dist/queries/templates-types.d.ts +62 -0
- package/dist/queries/templates-types.js +9 -0
- package/dist/types/linear.d.ts +21 -3
- package/dist/utils/auto-link-references.d.ts +3 -3
- package/dist/utils/auto-link-references.js +30 -34
- package/dist/utils/extract-field.d.ts +19 -0
- package/dist/utils/extract-field.js +99 -0
- package/dist/utils/file-service.d.ts +6 -13
- package/dist/utils/file-service.js +0 -2
- package/dist/utils/formatters/summary.js +6 -1
- package/dist/utils/graphql-attachments-service.js +6 -9
- package/dist/utils/graphql-documents-service.js +19 -25
- package/dist/utils/graphql-issues-service.d.ts +112 -46
- package/dist/utils/graphql-issues-service.js +398 -206
- package/dist/utils/graphql-service.d.ts +10 -12
- package/dist/utils/graphql-service.js +0 -3
- package/dist/utils/issue-reference-extractor.d.ts +7 -0
- package/dist/utils/issue-reference-extractor.js +5 -3
- package/dist/utils/issues-service-bootstrap.d.ts +28 -0
- package/dist/utils/issues-service-bootstrap.js +27 -0
- package/dist/utils/linear-service.d.ts +21 -14
- package/dist/utils/linear-service.js +73 -11
- package/dist/utils/markdown-prosemirror.js +12 -12
- package/dist/utils/mention-resolver.js +1 -1
- package/dist/utils/output.d.ts +82 -2
- package/dist/utils/output.js +76 -11
- package/dist/utils/project-slug.d.ts +21 -0
- package/dist/utils/project-slug.js +45 -0
- package/dist/utils/protected-ranges.d.ts +14 -0
- package/dist/utils/protected-ranges.js +88 -2
- package/dist/utils/sanitize-for-log.d.ts +24 -0
- package/dist/utils/sanitize-for-log.js +38 -0
- package/dist/utils/table-formatter.js +24 -0
- package/dist/utils/validators.d.ts +7 -2
- package/dist/utils/validators.js +6 -0
- package/dist/utils/workspace-url.d.ts +5 -1
- package/dist/utils/workspace-url.js +53 -7
- package/package.json +2 -2
package/dist/commands/refs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
3
3
|
import { extractIssueReferences } from "../utils/issue-reference-extractor.js";
|
|
4
4
|
import { wrapIssueReferencesAsLinks, } from "../utils/issue-reference-wrapper.js";
|
|
@@ -50,16 +50,70 @@ export async function wrapRefsCore(input, deps) {
|
|
|
50
50
|
const urlKey = await deps.resolveUrlKey();
|
|
51
51
|
return wrapIssueReferencesAsLinks(input.text, validIds, urlKey, input.target);
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Return `path` if it resolves to an existing regular file on disk, else
|
|
55
|
+
* `null`. Used by `handleWrap` to disambiguate the `el-linear refs wrap <arg>`
|
|
56
|
+
* shape between "wrap this literal text" and "wrap the contents of this file".
|
|
57
|
+
*
|
|
58
|
+
* Concerns intentionally NOT addressed:
|
|
59
|
+
* - Symlinks → resolved transparently by `statSync` (followSymlinks=true).
|
|
60
|
+
* - Directories → returned as null (statSync().isFile() is false).
|
|
61
|
+
* - Permission-denied / EACCES → returned as null; the caller falls back to
|
|
62
|
+
* treating the arg as text. The eventual `readFileSync` would surface the
|
|
63
|
+
* real error if the user did mean a file.
|
|
64
|
+
*
|
|
65
|
+
* Reference: DEV-4077 (vertical-int/tools MR !516 / !517 incident reports).
|
|
66
|
+
*/
|
|
67
|
+
function pathIfExistingFile(candidate) {
|
|
68
|
+
if (!candidate || candidate.includes("\n"))
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
|
72
|
+
return candidate;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// EACCES / EPERM / ELOOP → treat as not-a-file; downstream text path
|
|
77
|
+
// either passes through harmlessly or raises a more useful error.
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
async function handleWrap(textArgs, options, command) {
|
|
54
82
|
const target = options.target ?? "markdown";
|
|
55
83
|
if (!isWrapTarget(target)) {
|
|
56
84
|
throw new Error(`Invalid --target "${target}". Expected one of: ${[...VALID_TARGETS].join(", ")}`);
|
|
57
85
|
}
|
|
58
86
|
// `validate` is `true` by default and `false` when `--no-validate` is passed.
|
|
59
87
|
const validate = options.validate !== false;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
// Input precedence: positional args > --file > stdin. A single positional
|
|
89
|
+
// arg that resolves to an existing file is treated as a file path (the
|
|
90
|
+
// natural `el-linear refs wrap body.md` shape that users hit blind);
|
|
91
|
+
// otherwise positional args are joined as text. This auto-detect closes
|
|
92
|
+
// DEV-4077: the published v1.8.1 rejected `wrap <file>` outright, and the
|
|
93
|
+
// v1.10.0 source treated it as literal text — producing silently wrong
|
|
94
|
+
// output instead of an error. With this change, either form works.
|
|
95
|
+
let text;
|
|
96
|
+
if (textArgs.length === 1) {
|
|
97
|
+
const positionalFile = pathIfExistingFile(textArgs[0]);
|
|
98
|
+
if (positionalFile !== null) {
|
|
99
|
+
if (typeof options.file === "string" && options.file.length > 0) {
|
|
100
|
+
throw new Error(`Both a positional file path (${positionalFile}) and --file ${options.file} were provided. Use one or the other.`);
|
|
101
|
+
}
|
|
102
|
+
text = readFileSync(positionalFile, "utf8");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
text = textArgs[0];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (textArgs.length > 1) {
|
|
109
|
+
text = textArgs.join(" ");
|
|
110
|
+
}
|
|
111
|
+
else if (typeof options.file === "string" && options.file.length > 0) {
|
|
112
|
+
text = readFileSync(options.file, "utf8");
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
text = await readAllStdin();
|
|
116
|
+
}
|
|
63
117
|
const rootOpts = getRootOpts(command);
|
|
64
118
|
const deps = {
|
|
65
119
|
async resolveValidIdentifiers(ids) {
|
|
@@ -68,6 +122,13 @@ async function handleWrap(options, command) {
|
|
|
68
122
|
return new Set(map.keys());
|
|
69
123
|
},
|
|
70
124
|
async resolveUrlKey() {
|
|
125
|
+
// When --workspace-url-key or EL_LINEAR_WORKSPACE_URL_KEY supplies
|
|
126
|
+
// the key, the live lookup never fires — skip the GraphQL service
|
|
127
|
+
// instantiation entirely so `refs wrap --no-validate` works offline.
|
|
128
|
+
const override = options.workspaceUrlKey;
|
|
129
|
+
if (override || process.env.EL_LINEAR_WORKSPACE_URL_KEY) {
|
|
130
|
+
return getWorkspaceUrlKey(undefined, { override });
|
|
131
|
+
}
|
|
71
132
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
72
133
|
return getWorkspaceUrlKey(graphQLService);
|
|
73
134
|
},
|
|
@@ -87,11 +148,17 @@ export function setupRefsCommands(program) {
|
|
|
87
148
|
refs
|
|
88
149
|
.command("wrap")
|
|
89
150
|
.description("Wrap recognized Linear issue identifiers in input text as links. " +
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
151
|
+
"Input precedence: positional args > --file > stdin. A single positional " +
|
|
152
|
+
"arg that resolves to an existing file is read as a file (so " +
|
|
153
|
+
"`el-linear refs wrap body.md` works the same as `--file body.md`); " +
|
|
154
|
+
"otherwise positional args are joined as literal text. " +
|
|
155
|
+
"Writes the result to stdout. By default, each candidate identifier " +
|
|
156
|
+
"is validated against the workspace; unresolvable ones are left as " +
|
|
157
|
+
"plain text.")
|
|
158
|
+
.argument("[text...]", "input text OR a path to a file. A single positional arg that exists as a file is read; otherwise positional args are joined as literal text with a single space.")
|
|
93
159
|
.option("--file <path>", "read input from a file instead of stdin")
|
|
94
160
|
.option("--target <target>", "output format: markdown (default) or slack", "markdown")
|
|
95
161
|
.option("--no-validate", "skip workspace validation; wrap every regex match. Faster, but may produce broken links for IDs that don't exist.")
|
|
162
|
+
.option("--workspace-url-key <key>", "workspace URL key (the segment after linear.app/). Overrides config + env var. With --no-validate, makes the command fully offline.")
|
|
96
163
|
.action(handleAsyncCommand(handleWrap));
|
|
97
164
|
}
|
|
@@ -2,18 +2,24 @@ import { CREATE_RELEASE_MUTATION, GET_RELEASE_BY_ID_QUERY, GET_RELEASE_PIPELINES
|
|
|
2
2
|
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
3
3
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
4
4
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
5
|
+
import { parsePositiveInt } from "../utils/validators.js";
|
|
5
6
|
function transformRelease(release) {
|
|
7
|
+
// Lifecycle timestamps + dates only exist on the list/detail node shapes,
|
|
8
|
+
// not on the create-mutation shape; read defensively rather than splitting
|
|
9
|
+
// the function in two.
|
|
10
|
+
const r = release;
|
|
11
|
+
const documents = release.documents?.nodes;
|
|
6
12
|
return {
|
|
7
13
|
id: release.id,
|
|
8
14
|
name: release.name,
|
|
9
|
-
description: release.description
|
|
10
|
-
version: release.version
|
|
11
|
-
url: release.url
|
|
12
|
-
startDate:
|
|
13
|
-
targetDate:
|
|
14
|
-
startedAt:
|
|
15
|
-
completedAt:
|
|
16
|
-
canceledAt:
|
|
15
|
+
description: release.description ?? undefined,
|
|
16
|
+
version: release.version ?? undefined,
|
|
17
|
+
url: release.url ?? undefined,
|
|
18
|
+
startDate: r.startDate ?? undefined,
|
|
19
|
+
targetDate: r.targetDate ?? undefined,
|
|
20
|
+
startedAt: r.startedAt ?? undefined,
|
|
21
|
+
completedAt: r.completedAt ?? undefined,
|
|
22
|
+
canceledAt: r.canceledAt ?? undefined,
|
|
17
23
|
stage: release.stage
|
|
18
24
|
? {
|
|
19
25
|
id: release.stage.id,
|
|
@@ -27,7 +33,7 @@ function transformRelease(release) {
|
|
|
27
33
|
name: release.pipeline.name,
|
|
28
34
|
}
|
|
29
35
|
: undefined,
|
|
30
|
-
documents:
|
|
36
|
+
documents: documents?.map((d) => ({
|
|
31
37
|
id: d.id,
|
|
32
38
|
title: d.title,
|
|
33
39
|
slugId: d.slugId,
|
|
@@ -40,14 +46,11 @@ async function handleCreateRelease(name, options, command) {
|
|
|
40
46
|
const rootOpts = getRootOpts(command);
|
|
41
47
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
42
48
|
const pipelines = await graphQLService.rawRequest(GET_RELEASE_PIPELINES_QUERY, { first: 50 });
|
|
43
|
-
const pipelineNodes = pipelines.releasePipelines
|
|
44
|
-
|
|
45
|
-
const pipeline = pipelineNodes?.find((p) => p.id === options.pipeline ||
|
|
49
|
+
const pipelineNodes = pipelines.releasePipelines.nodes;
|
|
50
|
+
const pipeline = pipelineNodes.find((p) => p.id === options.pipeline ||
|
|
46
51
|
p.name.toLowerCase() === options.pipeline.toLowerCase());
|
|
47
52
|
if (!pipeline) {
|
|
48
|
-
const available = (
|
|
49
|
-
.map((p) => p.name)
|
|
50
|
-
.join(", ");
|
|
53
|
+
const available = pipelineNodes.map((p) => p.name).join(", ");
|
|
51
54
|
throw new Error(`Pipeline "${options.pipeline}" not found. Available: ${available || "none"}`);
|
|
52
55
|
}
|
|
53
56
|
const input = { name, pipelineId: pipeline.id };
|
|
@@ -58,21 +61,17 @@ async function handleCreateRelease(name, options, command) {
|
|
|
58
61
|
input.version = options.version;
|
|
59
62
|
}
|
|
60
63
|
if (options.stage) {
|
|
61
|
-
const
|
|
62
|
-
const stage = stageNodes?.find((s) => s.id === options.stage ||
|
|
64
|
+
const stage = pipeline.stages.nodes.find((s) => s.id === options.stage ||
|
|
63
65
|
s.name.toLowerCase() === options.stage.toLowerCase());
|
|
64
66
|
if (stage) {
|
|
65
67
|
input.stageId = stage.id;
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
|
-
const result = await graphQLService.rawRequest(CREATE_RELEASE_MUTATION, {
|
|
69
|
-
|
|
70
|
-
});
|
|
71
|
-
const releaseCreate = result.releaseCreate;
|
|
72
|
-
if (!releaseCreate?.success) {
|
|
70
|
+
const result = await graphQLService.rawRequest(CREATE_RELEASE_MUTATION, { input });
|
|
71
|
+
if (!result.releaseCreate.success || !result.releaseCreate.release) {
|
|
73
72
|
throw new Error(`Failed to create release "${name}"`);
|
|
74
73
|
}
|
|
75
|
-
outputSuccess(transformRelease(releaseCreate.release));
|
|
74
|
+
outputSuccess(transformRelease(result.releaseCreate.release));
|
|
76
75
|
}
|
|
77
76
|
export function setupReleasesCommands(program) {
|
|
78
77
|
const releases = program
|
|
@@ -92,11 +91,10 @@ export function setupReleasesCommands(program) {
|
|
|
92
91
|
filter.pipeline = { name: { eqIgnoreCase: options.pipeline } };
|
|
93
92
|
}
|
|
94
93
|
const result = await graphQLService.rawRequest(GET_RELEASES_QUERY, {
|
|
95
|
-
first:
|
|
94
|
+
first: parsePositiveInt(options.limit, "--limit"),
|
|
96
95
|
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
|
97
96
|
});
|
|
98
|
-
const data =
|
|
99
|
-
?.nodes ?? []).map(transformRelease);
|
|
97
|
+
const data = result.releases.nodes.map(transformRelease);
|
|
100
98
|
outputSuccess({ data, meta: { count: data.length } });
|
|
101
99
|
}));
|
|
102
100
|
releases
|
|
@@ -126,12 +124,10 @@ export function setupReleasesCommands(program) {
|
|
|
126
124
|
const rootOpts = getRootOpts(command);
|
|
127
125
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
128
126
|
const result = await graphQLService.rawRequest(GET_RELEASE_PIPELINES_QUERY, { first: 50 });
|
|
129
|
-
const data =
|
|
130
|
-
?.nodes ?? []).map((p) => ({
|
|
127
|
+
const data = result.releasePipelines.nodes.map((p) => ({
|
|
131
128
|
id: p.id,
|
|
132
129
|
name: p.name,
|
|
133
|
-
stages:
|
|
134
|
-
?.nodes ?? []).map((s) => ({
|
|
130
|
+
stages: p.stages.nodes.map((s) => ({
|
|
135
131
|
id: s.id,
|
|
136
132
|
name: s.name,
|
|
137
133
|
type: s.type,
|
package/dist/commands/search.js
CHANGED
|
@@ -3,6 +3,7 @@ import { SEMANTIC_SEARCH_QUERY } from "../queries/search.js";
|
|
|
3
3
|
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
4
4
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
5
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
6
|
+
import { parsePositiveInt } from "../utils/validators.js";
|
|
6
7
|
const TEMPLATES_QUERY = `
|
|
7
8
|
query {
|
|
8
9
|
templates {
|
|
@@ -22,61 +23,72 @@ const VALID_TYPES = new Set([
|
|
|
22
23
|
"document",
|
|
23
24
|
"template",
|
|
24
25
|
]);
|
|
26
|
+
/**
|
|
27
|
+
* `SemanticSearchResult`-valid types only (drops "template" — templates
|
|
28
|
+
* come from a separate query, not `semanticSearch`). Used to filter
|
|
29
|
+
* unknown types out of the response BEFORE they reach
|
|
30
|
+
* `transformSearchResult` — the transformer's switch has a
|
|
31
|
+
* `satisfies never` exhaustiveness guard that would otherwise return
|
|
32
|
+
* the raw row on a never-typed default branch.
|
|
33
|
+
*/
|
|
34
|
+
const SEMANTIC_TYPES = new Set([
|
|
35
|
+
"issue",
|
|
36
|
+
"project",
|
|
37
|
+
"initiative",
|
|
38
|
+
"document",
|
|
39
|
+
]);
|
|
40
|
+
function isKnownSemanticResult(r) {
|
|
41
|
+
return SEMANTIC_TYPES.has(r.type);
|
|
42
|
+
}
|
|
25
43
|
const WHITESPACE_RE = /\s+/;
|
|
26
44
|
function transformSearchResult(r) {
|
|
27
|
-
|
|
28
|
-
switch (rType) {
|
|
45
|
+
switch (r.type) {
|
|
29
46
|
case "issue": {
|
|
30
|
-
const issue = r
|
|
31
|
-
const state = issue?.state;
|
|
32
|
-
const team = issue?.team;
|
|
33
|
-
const assignee = issue?.assignee;
|
|
34
|
-
const project = issue?.project;
|
|
47
|
+
const { issue } = r;
|
|
35
48
|
return {
|
|
36
49
|
type: "issue",
|
|
37
50
|
identifier: issue?.identifier,
|
|
38
51
|
title: issue?.title,
|
|
39
|
-
state: state?.name,
|
|
40
|
-
team: team?.key,
|
|
41
|
-
assignee: assignee
|
|
42
|
-
? resolveUserDisplayName(assignee.id, assignee.name)
|
|
52
|
+
state: issue?.state?.name,
|
|
53
|
+
team: issue?.team?.key,
|
|
54
|
+
assignee: issue?.assignee
|
|
55
|
+
? resolveUserDisplayName(issue.assignee.id, issue.assignee.name)
|
|
43
56
|
: undefined,
|
|
44
|
-
priority: issue?.priority,
|
|
45
|
-
project: project?.name,
|
|
57
|
+
priority: issue?.priority ?? undefined,
|
|
58
|
+
project: issue?.project?.name,
|
|
46
59
|
id: issue?.id,
|
|
47
60
|
};
|
|
48
61
|
}
|
|
49
62
|
case "project": {
|
|
50
|
-
const project = r.project;
|
|
51
63
|
return {
|
|
52
64
|
type: "project",
|
|
53
|
-
name: project?.name,
|
|
54
|
-
state: project?.state,
|
|
55
|
-
id: project?.id,
|
|
65
|
+
name: r.project?.name,
|
|
66
|
+
state: r.project?.state,
|
|
67
|
+
id: r.project?.id,
|
|
56
68
|
};
|
|
57
69
|
}
|
|
58
70
|
case "initiative": {
|
|
59
|
-
const initiative = r.initiative;
|
|
60
71
|
return {
|
|
61
72
|
type: "initiative",
|
|
62
|
-
name: initiative?.name,
|
|
63
|
-
status: initiative?.status,
|
|
64
|
-
id: initiative?.id,
|
|
73
|
+
name: r.initiative?.name,
|
|
74
|
+
status: r.initiative?.status ?? undefined,
|
|
75
|
+
id: r.initiative?.id,
|
|
65
76
|
};
|
|
66
77
|
}
|
|
67
78
|
case "document": {
|
|
68
|
-
const doc = r.document;
|
|
69
|
-
const docProject = doc?.project;
|
|
70
79
|
return {
|
|
71
80
|
type: "document",
|
|
72
|
-
title:
|
|
73
|
-
project:
|
|
74
|
-
slugId:
|
|
75
|
-
id:
|
|
81
|
+
title: r.document?.title,
|
|
82
|
+
project: r.document?.project?.name,
|
|
83
|
+
slugId: r.document?.slugId ?? undefined,
|
|
84
|
+
id: r.document?.id,
|
|
76
85
|
};
|
|
77
86
|
}
|
|
78
87
|
default:
|
|
79
|
-
|
|
88
|
+
// Exhaustiveness check: SemanticSearchResult is a closed union, so
|
|
89
|
+
// `r` is `never` here — adding a new arm to the union forces a new
|
|
90
|
+
// case branch (the compiler error makes it impossible to forget).
|
|
91
|
+
return r;
|
|
80
92
|
}
|
|
81
93
|
}
|
|
82
94
|
function matchesQuery(name, query) {
|
|
@@ -122,11 +134,15 @@ function buildSemanticQuery(graphQLService, query, limit, teamOption) {
|
|
|
122
134
|
});
|
|
123
135
|
}
|
|
124
136
|
function extractSemanticResults(semanticResult, requestedTypes, onlyTemplates) {
|
|
125
|
-
|
|
126
|
-
|
|
137
|
+
// Always drop rows with unknown `type` first — defends the transformer
|
|
138
|
+
// against future Linear API expansions adding a new variant before
|
|
139
|
+
// SemanticSearchResult's union is widened. Without this, an unknown
|
|
140
|
+
// type would reach the transformer's `satisfies never` default and
|
|
141
|
+
// pass through as a raw row.
|
|
142
|
+
let results = (semanticResult.semanticSearch?.results ?? []).filter(isKnownSemanticResult);
|
|
127
143
|
if (requestedTypes && !onlyTemplates) {
|
|
128
|
-
const semanticTypes = requestedTypes.filter((t) => t !== "template");
|
|
129
|
-
results = results.filter((r) => semanticTypes.
|
|
144
|
+
const semanticTypes = new Set(requestedTypes.filter((t) => t !== "template"));
|
|
145
|
+
results = results.filter((r) => semanticTypes.has(r.type));
|
|
130
146
|
}
|
|
131
147
|
return results.map(transformSearchResult);
|
|
132
148
|
}
|
|
@@ -140,7 +156,7 @@ export function setupSearchCommands(program) {
|
|
|
140
156
|
.action(handleAsyncCommand(async (query, options, command) => {
|
|
141
157
|
const rootOpts = getRootOpts(command);
|
|
142
158
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
143
|
-
const limit =
|
|
159
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
144
160
|
const requestedTypes = options.type
|
|
145
161
|
? options.type.split(",").map((t) => t.trim().toLowerCase())
|
|
146
162
|
: null;
|
package/dist/commands/teams.js
CHANGED
|
@@ -3,6 +3,7 @@ import { cached, resolveCacheTTL } from "../utils/disk-cache.js";
|
|
|
3
3
|
import { createLinearService } from "../utils/linear-service.js";
|
|
4
4
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
5
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
6
|
+
import { parsePositiveInt } from "../utils/validators.js";
|
|
6
7
|
export function setupTeamsCommands(program) {
|
|
7
8
|
const teams = program
|
|
8
9
|
.command("teams")
|
|
@@ -15,7 +16,7 @@ export function setupTeamsCommands(program) {
|
|
|
15
16
|
.option("-l, --limit <number>", "limit results", "100")
|
|
16
17
|
.action(handleAsyncCommand(async (options, command) => {
|
|
17
18
|
const rootOpts = getRootOpts(command);
|
|
18
|
-
const limit =
|
|
19
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
19
20
|
const ttl = resolveCacheTTL({
|
|
20
21
|
configTTL: loadConfig().cacheTTLSeconds,
|
|
21
22
|
// commander's `--no-cache` produces `cache: false` on the root opts.
|
|
@@ -48,14 +48,11 @@ export function setupTemplatesCommands(program) {
|
|
|
48
48
|
.action(handleAsyncCommand(async (templateId, _options, command) => {
|
|
49
49
|
const rootOpts = getRootOpts(command);
|
|
50
50
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
51
|
-
const result = await graphQLService.rawRequest(TEMPLATE_BY_ID_QUERY, {
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
const template = result.template;
|
|
55
|
-
if (!template) {
|
|
51
|
+
const result = await graphQLService.rawRequest(TEMPLATE_BY_ID_QUERY, { id: templateId });
|
|
52
|
+
if (!result.template) {
|
|
56
53
|
throw new Error(`Template "${templateId}" not found`);
|
|
57
54
|
}
|
|
58
|
-
outputSuccess(template);
|
|
55
|
+
outputSuccess(result.template);
|
|
59
56
|
}));
|
|
60
57
|
templates
|
|
61
58
|
.command("create")
|
|
@@ -87,11 +84,10 @@ export function setupTemplatesCommands(program) {
|
|
|
87
84
|
if (options.color)
|
|
88
85
|
input.color = options.color;
|
|
89
86
|
const result = await graphQLService.rawRequest(TEMPLATE_CREATE_MUTATION, { input });
|
|
90
|
-
|
|
91
|
-
if (!payload?.success) {
|
|
87
|
+
if (!result.templateCreate.success || !result.templateCreate.template) {
|
|
92
88
|
throw new Error("templateCreate returned success=false");
|
|
93
89
|
}
|
|
94
|
-
outputSuccess(
|
|
90
|
+
outputSuccess(result.templateCreate.template);
|
|
95
91
|
}));
|
|
96
92
|
templates
|
|
97
93
|
.command("update <templateId>")
|
|
@@ -126,11 +122,11 @@ export function setupTemplatesCommands(program) {
|
|
|
126
122
|
throw new Error("templates update: at least one of --name, --description, --data, --data-file, --icon, --color, --team-id is required");
|
|
127
123
|
}
|
|
128
124
|
const result = await graphQLService.rawRequest(TEMPLATE_UPDATE_MUTATION, { id: templateId, input });
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
if (!result.templateUpdate.success ||
|
|
126
|
+
!result.templateUpdate.template) {
|
|
131
127
|
throw new Error("templateUpdate returned success=false");
|
|
132
128
|
}
|
|
133
|
-
outputSuccess(
|
|
129
|
+
outputSuccess(result.templateUpdate.template);
|
|
134
130
|
}));
|
|
135
131
|
templates
|
|
136
132
|
.command("delete <templateId>")
|
|
@@ -139,8 +135,7 @@ export function setupTemplatesCommands(program) {
|
|
|
139
135
|
const rootOpts = getRootOpts(command);
|
|
140
136
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
141
137
|
const result = await graphQLService.rawRequest(TEMPLATE_DELETE_MUTATION, { id: templateId });
|
|
142
|
-
|
|
143
|
-
if (!payload?.success) {
|
|
138
|
+
if (!result.templateDelete.success) {
|
|
144
139
|
throw new Error("templateDelete returned success=false");
|
|
145
140
|
}
|
|
146
141
|
outputSuccess({ id: templateId, deleted: true });
|
package/dist/commands/users.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createLinearService } from "../utils/linear-service.js";
|
|
2
|
-
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
2
|
+
import { handleAsyncCommand, outputSuccess, warnIfTruncated, } from "../utils/output.js";
|
|
3
3
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
4
|
+
import { parsePositiveInt } from "../utils/validators.js";
|
|
4
5
|
export function setupUsersCommands(program) {
|
|
5
6
|
const users = program.command("users").description("User operations");
|
|
6
7
|
users.action(() => users.help());
|
|
@@ -13,7 +14,9 @@ export function setupUsersCommands(program) {
|
|
|
13
14
|
.action(handleAsyncCommand(async (options, command) => {
|
|
14
15
|
const rootOpts = getRootOpts(command);
|
|
15
16
|
const service = await createLinearService(rootOpts);
|
|
16
|
-
const
|
|
17
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
18
|
+
const result = await service.getUsers(options.active, limit, options.name);
|
|
19
|
+
warnIfTruncated(result.length, limit);
|
|
17
20
|
outputSuccess({ data: result, meta: { count: result.length } });
|
|
18
21
|
}));
|
|
19
22
|
}
|
package/dist/config/config.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { TermRule } from "./term-enforcer.js";
|
|
2
|
+
/**
|
|
3
|
+
* Source attribution for the active team-config layer. Surfaced via
|
|
4
|
+
* `getActiveTeamConfigInfo()` and rendered by `el-linear config team show` so
|
|
5
|
+
* the operator can debug "which file is el-linear actually loading?".
|
|
6
|
+
*
|
|
7
|
+
* - `env`: `EL_LINEAR_TEAM_CONFIG` is set in the environment (highest).
|
|
8
|
+
* - `personal`: `teamConfigPath` field in personal `config.json`.
|
|
9
|
+
* - `marker`: auto-discovered via `~/.config/el-tools-root` (DEV-4258).
|
|
10
|
+
* - `null`: no team layer active.
|
|
11
|
+
*/
|
|
12
|
+
export type TeamConfigSource = "env" | "personal" | "marker" | null;
|
|
2
13
|
/**
|
|
3
14
|
* Canonical shape of `~/.config/el-linear/config.json`. This is the single source
|
|
4
15
|
* of truth for the on-disk config — the wizard reads/writes the same shape
|
|
@@ -27,6 +38,10 @@ export interface ElLinearConfig {
|
|
|
27
38
|
* Term-enforcement rules. Each rule has a canonical form and a list of
|
|
28
39
|
* rejected forms; rejected forms in issue titles/descriptions are flagged
|
|
29
40
|
* (or thrown on, in strict mode) with a hint to use the canonical form.
|
|
41
|
+
*
|
|
42
|
+
* When a team config layer is active, personal `terms` are appended to the
|
|
43
|
+
* team's rules (not replaced). To start fresh, omit `terms` from personal
|
|
44
|
+
* config entirely.
|
|
30
45
|
*/
|
|
31
46
|
terms: TermRule[];
|
|
32
47
|
validation?: {
|
|
@@ -61,12 +76,17 @@ export interface ElLinearConfig {
|
|
|
61
76
|
* Default assignee identifier (alias / display name / email / UUID — same
|
|
62
77
|
* shapes resolveAssignee accepts) for `issues create`. Applied when
|
|
63
78
|
* `--assignee` is not passed. Pass `--no-assignee` to override at one site.
|
|
79
|
+
*
|
|
80
|
+
* Prefer setting this in `local.json` instead of `config.json` so it stays
|
|
81
|
+
* out of shared team config.
|
|
64
82
|
*/
|
|
65
83
|
defaultAssignee?: string;
|
|
66
84
|
/**
|
|
67
85
|
* Default priority for `issues create` / `issues update`. Accepts the same
|
|
68
86
|
* keywords as the --priority flag: `none|urgent|high|medium|normal|low`
|
|
69
87
|
* or `0`–`4`. Applied when `--priority` is not passed.
|
|
88
|
+
*
|
|
89
|
+
* Prefer setting this in `local.json` instead of `config.json`.
|
|
70
90
|
*/
|
|
71
91
|
defaultPriority?: string;
|
|
72
92
|
/**
|
|
@@ -74,9 +94,87 @@ export interface ElLinearConfig {
|
|
|
74
94
|
* and `projects list`. Defaults to 3600 (1 hour) when omitted. A value of
|
|
75
95
|
* `0` disables the cache entirely. Override per-invocation with
|
|
76
96
|
* `--no-cache`.
|
|
97
|
+
*
|
|
98
|
+
* Prefer setting this in `local.json` instead of `config.json`.
|
|
77
99
|
*/
|
|
78
100
|
cacheTTLSeconds?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Path to a shared team config file. Fields in the team config are merged
|
|
103
|
+
* under the personal config (personal config wins on conflicts). Arrays such
|
|
104
|
+
* as `terms` and `defaultLabels` are concatenated — personal entries are
|
|
105
|
+
* appended to team entries. The team config accepts any ElLinearConfig fields
|
|
106
|
+
* except `teamConfigPath` itself. Useful for sharing member aliases, label
|
|
107
|
+
* maps, and term rules across a team by checking the file into a shared
|
|
108
|
+
* repository. Override at runtime with the `EL_LINEAR_TEAM_CONFIG` env var
|
|
109
|
+
* (env var takes precedence over this field).
|
|
110
|
+
*/
|
|
111
|
+
teamConfigPath?: string;
|
|
79
112
|
}
|
|
80
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Fields valid in a shared team config file (the file pointed to by
|
|
115
|
+
* `teamConfigPath` or `EL_LINEAR_TEAM_CONFIG`). All fields are optional —
|
|
116
|
+
* omitted fields fall back to defaults or the personal config layer. The type
|
|
117
|
+
* excludes `teamConfigPath` itself to prevent circular references.
|
|
118
|
+
*/
|
|
119
|
+
export type TeamConfig = Omit<ElLinearConfig, "teamConfigPath">;
|
|
120
|
+
/**
|
|
121
|
+
* User-local overrides that live in `~/.config/el-linear/local.json` (or the
|
|
122
|
+
* per-profile equivalent). These are applied on top of the merged team+personal
|
|
123
|
+
* config, so every field here takes the highest precedence.
|
|
124
|
+
*
|
|
125
|
+
* Use `local.json` for anything that is personal rather than team-wide:
|
|
126
|
+
* your own email, personal default priority, cache preferences. Never commit
|
|
127
|
+
* `local.json` to a shared repo — it is `.gitignore`-worthy by nature.
|
|
128
|
+
*
|
|
129
|
+
* Merge order (lowest → highest priority):
|
|
130
|
+
* defaults → team config file → personal config.json → local.json
|
|
131
|
+
*/
|
|
132
|
+
export interface ElLinearLocalConfig {
|
|
133
|
+
/**
|
|
134
|
+
* Your Linear account email. Used as the default `--assignee` when creating
|
|
135
|
+
* issues. Takes precedence over `config.json`'s `defaultAssignee`.
|
|
136
|
+
*
|
|
137
|
+
* Example: "ytspar@gmail.com"
|
|
138
|
+
*/
|
|
139
|
+
assigneeEmail?: string;
|
|
140
|
+
/** Same semantics as `ElLinearConfig.defaultAssignee`, but local-wins. */
|
|
141
|
+
defaultAssignee?: string;
|
|
142
|
+
/** Same semantics as `ElLinearConfig.defaultPriority`, but local-wins. */
|
|
143
|
+
defaultPriority?: string;
|
|
144
|
+
/** Same semantics as `ElLinearConfig.cacheTTLSeconds`, but local-wins. */
|
|
145
|
+
cacheTTLSeconds?: number;
|
|
146
|
+
}
|
|
147
|
+
/** Test seam — resets all caches between test cases. */
|
|
81
148
|
export declare function _resetConfigCacheForTests(): void;
|
|
82
149
|
export declare function loadConfig(): ElLinearConfig;
|
|
150
|
+
/**
|
|
151
|
+
* Returns the team config file path that is active for the current process
|
|
152
|
+
* (resolved via `EL_LINEAR_TEAM_CONFIG` env var, `teamConfigPath` in personal
|
|
153
|
+
* config, or `~/.config/el-tools-root` marker — in that order). Returns
|
|
154
|
+
* `undefined` when no team config is configured.
|
|
155
|
+
*
|
|
156
|
+
* Calling this before `loadConfig()` will trigger a `loadConfig()` internally
|
|
157
|
+
* so the path cache is populated.
|
|
158
|
+
*/
|
|
159
|
+
export declare function getActiveTeamConfigPath(): string | undefined;
|
|
160
|
+
/**
|
|
161
|
+
* Returns the active team-config path AND the source it was resolved from
|
|
162
|
+
* (env var / personal config field / onboarding-marker auto-discovery).
|
|
163
|
+
* `el-linear config team show` uses this to render which resolver fired —
|
|
164
|
+
* so an operator debugging "why isn't my team config loading?" sees the
|
|
165
|
+
* answer in one command.
|
|
166
|
+
*
|
|
167
|
+
* Re-derives source on each call rather than caching it: the lookup is cheap
|
|
168
|
+
* (one file read at most for personal config, one for the marker) and only
|
|
169
|
+
* `config team show` calls it.
|
|
170
|
+
*/
|
|
171
|
+
export declare function getActiveTeamConfigInfo(): {
|
|
172
|
+
path: string | undefined;
|
|
173
|
+
source: TeamConfigSource;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Load user-local overrides from `local.json`. Returns an empty object when
|
|
177
|
+
* no file exists — callers treat it as "no overrides". Errors are silent so
|
|
178
|
+
* a missing or malformed `local.json` never breaks a command.
|
|
179
|
+
*/
|
|
180
|
+
export declare function loadLocalConfig(): ElLinearLocalConfig;
|