@enrichlayer/el-linear 1.2.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/LICENSE +21 -0
- package/README.md +219 -0
- package/claude-skills/linear-operations/SKILL.md +315 -0
- package/claude-skills/linear-operations/evals/evals.json +46 -0
- package/dist/commands/attachments.d.ts +2 -0
- package/dist/commands/attachments.js +57 -0
- package/dist/commands/batch.d.ts +2 -0
- package/dist/commands/batch.js +309 -0
- package/dist/commands/comments.d.ts +2 -0
- package/dist/commands/comments.js +272 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +15 -0
- package/dist/commands/cycles.d.ts +2 -0
- package/dist/commands/cycles.js +63 -0
- package/dist/commands/documents.d.ts +2 -0
- package/dist/commands/documents.js +175 -0
- package/dist/commands/embeds.d.ts +2 -0
- package/dist/commands/embeds.js +65 -0
- package/dist/commands/gdoc.d.ts +2 -0
- package/dist/commands/gdoc.js +37 -0
- package/dist/commands/graphql.d.ts +2 -0
- package/dist/commands/graphql.js +70 -0
- package/dist/commands/init/aliases.d.ts +109 -0
- package/dist/commands/init/aliases.js +569 -0
- package/dist/commands/init/defaults.d.ts +25 -0
- package/dist/commands/init/defaults.js +112 -0
- package/dist/commands/init/index.d.ts +18 -0
- package/dist/commands/init/index.js +182 -0
- package/dist/commands/init/shared.d.ts +88 -0
- package/dist/commands/init/shared.js +164 -0
- package/dist/commands/init/token.d.ts +50 -0
- package/dist/commands/init/token.js +141 -0
- package/dist/commands/init/workspace.d.ts +20 -0
- package/dist/commands/init/workspace.js +80 -0
- package/dist/commands/issue-id.d.ts +28 -0
- package/dist/commands/issue-id.js +81 -0
- package/dist/commands/issues.d.ts +2 -0
- package/dist/commands/issues.js +1145 -0
- package/dist/commands/labels.d.ts +2 -0
- package/dist/commands/labels.js +100 -0
- package/dist/commands/project-milestones.d.ts +2 -0
- package/dist/commands/project-milestones.js +143 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +336 -0
- package/dist/commands/read-shortcut.d.ts +6 -0
- package/dist/commands/read-shortcut.js +69 -0
- package/dist/commands/releases.d.ts +2 -0
- package/dist/commands/releases.js +142 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +171 -0
- package/dist/commands/teams.d.ts +2 -0
- package/dist/commands/teams.js +19 -0
- package/dist/commands/templates.d.ts +2 -0
- package/dist/commands/templates.js +58 -0
- package/dist/commands/users.d.ts +2 -0
- package/dist/commands/users.js +17 -0
- package/dist/config/config.d.ts +43 -0
- package/dist/config/config.js +81 -0
- package/dist/config/issue-validation.d.ts +39 -0
- package/dist/config/issue-validation.js +264 -0
- package/dist/config/paths.d.ts +20 -0
- package/dist/config/paths.js +22 -0
- package/dist/config/resolver.d.ts +25 -0
- package/dist/config/resolver.js +183 -0
- package/dist/config/status-defaults.d.ts +13 -0
- package/dist/config/status-defaults.js +20 -0
- package/dist/config/term-enforcer.d.ts +31 -0
- package/dist/config/term-enforcer.js +69 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +76 -0
- package/dist/queries/attachments.d.ts +3 -0
- package/dist/queries/attachments.js +35 -0
- package/dist/queries/comments.d.ts +3 -0
- package/dist/queries/comments.js +64 -0
- package/dist/queries/common.d.ts +2 -0
- package/dist/queries/common.js +109 -0
- package/dist/queries/cycles.d.ts +2 -0
- package/dist/queries/cycles.js +40 -0
- package/dist/queries/documents.d.ts +5 -0
- package/dist/queries/documents.js +67 -0
- package/dist/queries/introspect.d.ts +2 -0
- package/dist/queries/introspect.js +24 -0
- package/dist/queries/issues.d.ts +23 -0
- package/dist/queries/issues.js +380 -0
- package/dist/queries/labels.d.ts +4 -0
- package/dist/queries/labels.js +53 -0
- package/dist/queries/project-milestones.d.ts +6 -0
- package/dist/queries/project-milestones.js +125 -0
- package/dist/queries/projects.d.ts +7 -0
- package/dist/queries/projects.js +104 -0
- package/dist/queries/releases.d.ts +4 -0
- package/dist/queries/releases.js +85 -0
- package/dist/queries/search.d.ts +1 -0
- package/dist/queries/search.js +35 -0
- package/dist/queries/templates.d.ts +2 -0
- package/dist/queries/templates.js +30 -0
- package/dist/types/linear.d.ts +217 -0
- package/dist/types/linear.js +6 -0
- package/dist/utils/auth.d.ts +4 -0
- package/dist/utils/auth.js +23 -0
- package/dist/utils/auto-link-references.d.ts +47 -0
- package/dist/utils/auto-link-references.js +188 -0
- package/dist/utils/date-format.d.ts +4 -0
- package/dist/utils/date-format.js +8 -0
- package/dist/utils/download-uploads.d.ts +7 -0
- package/dist/utils/download-uploads.js +88 -0
- package/dist/utils/embed-parser.d.ts +8 -0
- package/dist/utils/embed-parser.js +54 -0
- package/dist/utils/error-messages.d.ts +4 -0
- package/dist/utils/error-messages.js +17 -0
- package/dist/utils/file-service.d.ts +13 -0
- package/dist/utils/file-service.js +239 -0
- package/dist/utils/gdoc-parser.d.ts +45 -0
- package/dist/utils/gdoc-parser.js +107 -0
- package/dist/utils/graphql-attachments-service.d.ts +12 -0
- package/dist/utils/graphql-attachments-service.js +46 -0
- package/dist/utils/graphql-documents-service.d.ts +18 -0
- package/dist/utils/graphql-documents-service.js +97 -0
- package/dist/utils/graphql-issues-service.d.ts +49 -0
- package/dist/utils/graphql-issues-service.js +925 -0
- package/dist/utils/graphql-service.d.ts +8 -0
- package/dist/utils/graphql-service.js +35 -0
- package/dist/utils/identifier-parser.d.ts +7 -0
- package/dist/utils/identifier-parser.js +20 -0
- package/dist/utils/issue-reference-extractor.d.ts +18 -0
- package/dist/utils/issue-reference-extractor.js +95 -0
- package/dist/utils/issue-reference-wrapper.d.ts +12 -0
- package/dist/utils/issue-reference-wrapper.js +91 -0
- package/dist/utils/linear-service.d.ts +26 -0
- package/dist/utils/linear-service.js +442 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.js +8 -0
- package/dist/utils/markdown-prosemirror.d.ts +24 -0
- package/dist/utils/markdown-prosemirror.js +325 -0
- package/dist/utils/mention-resolver.d.ts +31 -0
- package/dist/utils/mention-resolver.js +234 -0
- package/dist/utils/output.d.ts +7 -0
- package/dist/utils/output.js +125 -0
- package/dist/utils/table-formatter.d.ts +4 -0
- package/dist/utils/table-formatter.js +249 -0
- package/dist/utils/usage.d.ts +2 -0
- package/dist/utils/usage.js +24 -0
- package/dist/utils/uuid.d.ts +2 -0
- package/dist/utils/uuid.js +8 -0
- package/dist/utils/validate-references.d.ts +10 -0
- package/dist/utils/validate-references.js +33 -0
- package/dist/utils/validators.d.ts +7 -0
- package/dist/utils/validators.js +71 -0
- package/dist/utils/workspace-url.d.ts +4 -0
- package/dist/utils/workspace-url.js +44 -0
- package/package.json +71 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { CREATE_RELEASE_MUTATION, GET_RELEASE_BY_ID_QUERY, GET_RELEASE_PIPELINES_QUERY, GET_RELEASES_QUERY, } from "../queries/releases.js";
|
|
2
|
+
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
3
|
+
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
4
|
+
function transformRelease(release) {
|
|
5
|
+
return {
|
|
6
|
+
id: release.id,
|
|
7
|
+
name: release.name,
|
|
8
|
+
description: release.description || undefined,
|
|
9
|
+
version: release.version || undefined,
|
|
10
|
+
url: release.url || undefined,
|
|
11
|
+
startDate: release.startDate || undefined,
|
|
12
|
+
targetDate: release.targetDate || undefined,
|
|
13
|
+
startedAt: release.startedAt || undefined,
|
|
14
|
+
completedAt: release.completedAt || undefined,
|
|
15
|
+
canceledAt: release.canceledAt || undefined,
|
|
16
|
+
stage: release.stage
|
|
17
|
+
? {
|
|
18
|
+
id: release.stage.id,
|
|
19
|
+
name: release.stage.name,
|
|
20
|
+
type: release.stage.type,
|
|
21
|
+
}
|
|
22
|
+
: undefined,
|
|
23
|
+
pipeline: release.pipeline
|
|
24
|
+
? {
|
|
25
|
+
id: release.pipeline.id,
|
|
26
|
+
name: release.pipeline.name,
|
|
27
|
+
}
|
|
28
|
+
: undefined,
|
|
29
|
+
documents: release.documents?.nodes?.map((d) => ({
|
|
30
|
+
id: d.id,
|
|
31
|
+
title: d.title,
|
|
32
|
+
slugId: d.slugId,
|
|
33
|
+
})),
|
|
34
|
+
createdAt: release.createdAt,
|
|
35
|
+
updatedAt: release.updatedAt,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function handleCreateRelease(name, options, command) {
|
|
39
|
+
const rootOpts = command.parent.parent.opts();
|
|
40
|
+
const graphQLService = createGraphQLService(rootOpts);
|
|
41
|
+
const pipelines = await graphQLService.rawRequest(GET_RELEASE_PIPELINES_QUERY, { first: 50 });
|
|
42
|
+
const pipelineNodes = pipelines.releasePipelines
|
|
43
|
+
?.nodes;
|
|
44
|
+
const pipeline = pipelineNodes?.find((p) => p.id === options.pipeline ||
|
|
45
|
+
p.name.toLowerCase() === options.pipeline.toLowerCase());
|
|
46
|
+
if (!pipeline) {
|
|
47
|
+
const available = (pipelineNodes ?? [])
|
|
48
|
+
.map((p) => p.name)
|
|
49
|
+
.join(", ");
|
|
50
|
+
throw new Error(`Pipeline "${options.pipeline}" not found. Available: ${available || "none"}`);
|
|
51
|
+
}
|
|
52
|
+
const input = { name, pipelineId: pipeline.id };
|
|
53
|
+
if (options.description) {
|
|
54
|
+
input.description = options.description;
|
|
55
|
+
}
|
|
56
|
+
if (options.version) {
|
|
57
|
+
input.version = options.version;
|
|
58
|
+
}
|
|
59
|
+
if (options.stage) {
|
|
60
|
+
const stageNodes = pipeline.stages?.nodes;
|
|
61
|
+
const stage = stageNodes?.find((s) => s.id === options.stage ||
|
|
62
|
+
s.name.toLowerCase() === options.stage.toLowerCase());
|
|
63
|
+
if (stage) {
|
|
64
|
+
input.stageId = stage.id;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const result = await graphQLService.rawRequest(CREATE_RELEASE_MUTATION, {
|
|
68
|
+
input,
|
|
69
|
+
});
|
|
70
|
+
const releaseCreate = result.releaseCreate;
|
|
71
|
+
if (!releaseCreate?.success) {
|
|
72
|
+
throw new Error(`Failed to create release "${name}"`);
|
|
73
|
+
}
|
|
74
|
+
outputSuccess(transformRelease(releaseCreate.release));
|
|
75
|
+
}
|
|
76
|
+
export function setupReleasesCommands(program) {
|
|
77
|
+
const releases = program
|
|
78
|
+
.command("releases")
|
|
79
|
+
.description("Release operations");
|
|
80
|
+
releases.action(() => releases.help());
|
|
81
|
+
releases
|
|
82
|
+
.command("list")
|
|
83
|
+
.description("List releases")
|
|
84
|
+
.option("--pipeline <pipeline>", "filter by pipeline name or ID")
|
|
85
|
+
.option("-l, --limit <number>", "limit results", "25")
|
|
86
|
+
.action(handleAsyncCommand(async (options, command) => {
|
|
87
|
+
const rootOpts = command.parent.parent.opts();
|
|
88
|
+
const graphQLService = createGraphQLService(rootOpts);
|
|
89
|
+
const filter = {};
|
|
90
|
+
if (options.pipeline) {
|
|
91
|
+
filter.pipeline = { name: { eqIgnoreCase: options.pipeline } };
|
|
92
|
+
}
|
|
93
|
+
const result = await graphQLService.rawRequest(GET_RELEASES_QUERY, {
|
|
94
|
+
first: Number.parseInt(options.limit, 10),
|
|
95
|
+
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
|
96
|
+
});
|
|
97
|
+
const data = (result.releases
|
|
98
|
+
?.nodes ?? []).map(transformRelease);
|
|
99
|
+
outputSuccess({ data, meta: { count: data.length } });
|
|
100
|
+
}));
|
|
101
|
+
releases
|
|
102
|
+
.command("read <releaseId>")
|
|
103
|
+
.description("Get release details including linked documents")
|
|
104
|
+
.action(handleAsyncCommand(async (releaseId, _options, command) => {
|
|
105
|
+
const rootOpts = command.parent.parent.opts();
|
|
106
|
+
const graphQLService = createGraphQLService(rootOpts);
|
|
107
|
+
const result = await graphQLService.rawRequest(GET_RELEASE_BY_ID_QUERY, { id: releaseId });
|
|
108
|
+
if (!result.release) {
|
|
109
|
+
throw new Error(`Release "${releaseId}" not found`);
|
|
110
|
+
}
|
|
111
|
+
outputSuccess(transformRelease(result.release));
|
|
112
|
+
}));
|
|
113
|
+
releases
|
|
114
|
+
.command("create <name>")
|
|
115
|
+
.description("Create a new release")
|
|
116
|
+
.requiredOption("--pipeline <pipeline>", "pipeline name or ID")
|
|
117
|
+
.option("-d, --description <desc>", "release description")
|
|
118
|
+
.option("--version <version>", "version string")
|
|
119
|
+
.option("--stage <stage>", "stage name or ID")
|
|
120
|
+
.action(handleAsyncCommand(handleCreateRelease));
|
|
121
|
+
releases
|
|
122
|
+
.command("pipelines")
|
|
123
|
+
.description("List release pipelines and their stages")
|
|
124
|
+
.action(handleAsyncCommand(async (_options, command) => {
|
|
125
|
+
const rootOpts = command.parent.parent.opts();
|
|
126
|
+
const graphQLService = createGraphQLService(rootOpts);
|
|
127
|
+
const result = await graphQLService.rawRequest(GET_RELEASE_PIPELINES_QUERY, { first: 50 });
|
|
128
|
+
const data = (result.releasePipelines
|
|
129
|
+
?.nodes ?? []).map((p) => ({
|
|
130
|
+
id: p.id,
|
|
131
|
+
name: p.name,
|
|
132
|
+
stages: (p.stages
|
|
133
|
+
?.nodes ?? []).map((s) => ({
|
|
134
|
+
id: s.id,
|
|
135
|
+
name: s.name,
|
|
136
|
+
type: s.type,
|
|
137
|
+
color: s.color,
|
|
138
|
+
})),
|
|
139
|
+
}));
|
|
140
|
+
outputSuccess({ data, meta: { count: data.length } });
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { resolveTeam, resolveUserDisplayName } from "../config/resolver.js";
|
|
2
|
+
import { SEMANTIC_SEARCH_QUERY } from "../queries/search.js";
|
|
3
|
+
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
4
|
+
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
|
+
const TEMPLATES_QUERY = `
|
|
6
|
+
query {
|
|
7
|
+
templates {
|
|
8
|
+
id
|
|
9
|
+
name
|
|
10
|
+
type
|
|
11
|
+
description
|
|
12
|
+
team { key }
|
|
13
|
+
creator { name }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
const VALID_TYPES = new Set([
|
|
18
|
+
"issue",
|
|
19
|
+
"project",
|
|
20
|
+
"initiative",
|
|
21
|
+
"document",
|
|
22
|
+
"template",
|
|
23
|
+
]);
|
|
24
|
+
const WHITESPACE_RE = /\s+/;
|
|
25
|
+
function transformSearchResult(r) {
|
|
26
|
+
const rType = r.type;
|
|
27
|
+
switch (rType) {
|
|
28
|
+
case "issue": {
|
|
29
|
+
const issue = r.issue;
|
|
30
|
+
const state = issue?.state;
|
|
31
|
+
const team = issue?.team;
|
|
32
|
+
const assignee = issue?.assignee;
|
|
33
|
+
const project = issue?.project;
|
|
34
|
+
return {
|
|
35
|
+
type: "issue",
|
|
36
|
+
identifier: issue?.identifier,
|
|
37
|
+
title: issue?.title,
|
|
38
|
+
state: state?.name,
|
|
39
|
+
team: team?.key,
|
|
40
|
+
assignee: assignee
|
|
41
|
+
? resolveUserDisplayName(assignee.id, assignee.name)
|
|
42
|
+
: undefined,
|
|
43
|
+
priority: issue?.priority,
|
|
44
|
+
project: project?.name,
|
|
45
|
+
id: issue?.id,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
case "project": {
|
|
49
|
+
const project = r.project;
|
|
50
|
+
return {
|
|
51
|
+
type: "project",
|
|
52
|
+
name: project?.name,
|
|
53
|
+
state: project?.state,
|
|
54
|
+
id: project?.id,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
case "initiative": {
|
|
58
|
+
const initiative = r.initiative;
|
|
59
|
+
return {
|
|
60
|
+
type: "initiative",
|
|
61
|
+
name: initiative?.name,
|
|
62
|
+
status: initiative?.status,
|
|
63
|
+
id: initiative?.id,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
case "document": {
|
|
67
|
+
const doc = r.document;
|
|
68
|
+
const docProject = doc?.project;
|
|
69
|
+
return {
|
|
70
|
+
type: "document",
|
|
71
|
+
title: doc?.title,
|
|
72
|
+
project: docProject?.name,
|
|
73
|
+
slugId: doc?.slugId,
|
|
74
|
+
id: doc?.id,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
default:
|
|
78
|
+
return { type: rType, id: null };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function matchesQuery(name, query) {
|
|
82
|
+
const lower = name.toLowerCase();
|
|
83
|
+
const terms = query.toLowerCase().split(WHITESPACE_RE);
|
|
84
|
+
return terms.every((term) => lower.includes(term));
|
|
85
|
+
}
|
|
86
|
+
function searchTemplates(templates, query) {
|
|
87
|
+
return templates
|
|
88
|
+
.filter((t) => matchesQuery(t.name, query) ||
|
|
89
|
+
(t.description && matchesQuery(t.description, query)))
|
|
90
|
+
.map((t) => ({
|
|
91
|
+
type: "template",
|
|
92
|
+
name: t.name,
|
|
93
|
+
templateType: t.type,
|
|
94
|
+
team: t.team?.key ?? null,
|
|
95
|
+
creator: t.creator?.name ?? null,
|
|
96
|
+
id: t.id,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
function validateTypes(requestedTypes) {
|
|
100
|
+
for (const t of requestedTypes) {
|
|
101
|
+
if (!VALID_TYPES.has(t)) {
|
|
102
|
+
throw new Error(`Invalid type "${t}". Valid types: ${[...VALID_TYPES].join(", ")}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function parseTypeFilters(requestedTypes) {
|
|
107
|
+
const onlyTemplates = requestedTypes?.length === 1 && requestedTypes[0] === "template";
|
|
108
|
+
const includeTemplates = !requestedTypes || requestedTypes.includes("template");
|
|
109
|
+
const includeSemanticTypes = !requestedTypes || requestedTypes.some((t) => t !== "template");
|
|
110
|
+
return { onlyTemplates, includeTemplates, includeSemanticTypes };
|
|
111
|
+
}
|
|
112
|
+
function buildSemanticQuery(graphQLService, query, limit, teamOption) {
|
|
113
|
+
const filters = {};
|
|
114
|
+
if (teamOption) {
|
|
115
|
+
filters.issues = { team: { id: { eq: resolveTeam(teamOption) } } };
|
|
116
|
+
}
|
|
117
|
+
return graphQLService.rawRequest(SEMANTIC_SEARCH_QUERY, {
|
|
118
|
+
query,
|
|
119
|
+
maxResults: limit,
|
|
120
|
+
filters: Object.keys(filters).length > 0 ? filters : undefined,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function extractSemanticResults(semanticResult, requestedTypes, onlyTemplates) {
|
|
124
|
+
const semanticSearch = semanticResult.semanticSearch;
|
|
125
|
+
let results = semanticSearch?.results ?? [];
|
|
126
|
+
if (requestedTypes && !onlyTemplates) {
|
|
127
|
+
const semanticTypes = requestedTypes.filter((t) => t !== "template");
|
|
128
|
+
results = results.filter((r) => semanticTypes.includes(r.type));
|
|
129
|
+
}
|
|
130
|
+
return results.map(transformSearchResult);
|
|
131
|
+
}
|
|
132
|
+
export function setupSearchCommands(program) {
|
|
133
|
+
program
|
|
134
|
+
.command("search <query>")
|
|
135
|
+
.description("Natural language search across issues, projects, initiatives, documents, and templates.")
|
|
136
|
+
.option("--type <types>", "filter by type (comma-separated: issue,project,initiative,document,template)")
|
|
137
|
+
.option("--team <team>", "filter issue results by team key")
|
|
138
|
+
.option("-l, --limit <number>", "max results", "10")
|
|
139
|
+
.action(handleAsyncCommand(async (query, options, command) => {
|
|
140
|
+
const rootOpts = command.parent.opts();
|
|
141
|
+
const graphQLService = createGraphQLService(rootOpts);
|
|
142
|
+
const limit = Number.parseInt(options.limit, 10);
|
|
143
|
+
const requestedTypes = options.type
|
|
144
|
+
? options.type.split(",").map((t) => t.trim().toLowerCase())
|
|
145
|
+
: null;
|
|
146
|
+
if (requestedTypes) {
|
|
147
|
+
validateTypes(requestedTypes);
|
|
148
|
+
}
|
|
149
|
+
const { onlyTemplates, includeTemplates, includeSemanticTypes } = parseTypeFilters(requestedTypes);
|
|
150
|
+
const [semanticResult, templateResult] = await Promise.all([
|
|
151
|
+
includeSemanticTypes
|
|
152
|
+
? buildSemanticQuery(graphQLService, query, limit, options.team)
|
|
153
|
+
: null,
|
|
154
|
+
includeTemplates
|
|
155
|
+
? graphQLService.rawRequest(TEMPLATES_QUERY)
|
|
156
|
+
: null,
|
|
157
|
+
]);
|
|
158
|
+
let data = [];
|
|
159
|
+
if (semanticResult) {
|
|
160
|
+
data = extractSemanticResults(semanticResult, requestedTypes, onlyTemplates ?? false);
|
|
161
|
+
}
|
|
162
|
+
if (templateResult) {
|
|
163
|
+
const templates = templateResult.templates ?? [];
|
|
164
|
+
data = [...data, ...searchTemplates(templates, query)];
|
|
165
|
+
}
|
|
166
|
+
outputSuccess({
|
|
167
|
+
data: data.slice(0, limit),
|
|
168
|
+
meta: { count: Math.min(data.length, limit), query },
|
|
169
|
+
});
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createLinearService } from "../utils/linear-service.js";
|
|
2
|
+
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
3
|
+
export function setupTeamsCommands(program) {
|
|
4
|
+
const teams = program
|
|
5
|
+
.command("teams")
|
|
6
|
+
.alias("team")
|
|
7
|
+
.description("Team operations");
|
|
8
|
+
teams.action(() => teams.help());
|
|
9
|
+
teams
|
|
10
|
+
.command("list")
|
|
11
|
+
.description("List all teams")
|
|
12
|
+
.option("-l, --limit <number>", "limit results", "100")
|
|
13
|
+
.action(handleAsyncCommand(async (options, command) => {
|
|
14
|
+
const rootOpts = command.parent.parent.opts();
|
|
15
|
+
const service = createLinearService(rootOpts);
|
|
16
|
+
const result = await service.getTeams(Number.parseInt(options.limit, 10));
|
|
17
|
+
outputSuccess({ data: result, meta: { count: result.length } });
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { TEMPLATE_BY_ID_QUERY, TEMPLATES_LIST_QUERY, } from "../queries/templates.js";
|
|
2
|
+
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
3
|
+
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
4
|
+
import { parsePositiveInt } from "../utils/validators.js";
|
|
5
|
+
function formatTemplateSummary(t) {
|
|
6
|
+
return {
|
|
7
|
+
id: t.id,
|
|
8
|
+
name: t.name,
|
|
9
|
+
type: t.type,
|
|
10
|
+
description: t.description,
|
|
11
|
+
team: t.team?.key ?? null,
|
|
12
|
+
creator: t.creator?.name ?? null,
|
|
13
|
+
updatedAt: t.updatedAt,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function setupTemplatesCommands(program) {
|
|
17
|
+
const templates = program
|
|
18
|
+
.command("templates")
|
|
19
|
+
.description("Template operations (issue, document, and project templates)");
|
|
20
|
+
templates.action(() => templates.help());
|
|
21
|
+
templates
|
|
22
|
+
.command("list")
|
|
23
|
+
.alias("ls")
|
|
24
|
+
.description("List all templates")
|
|
25
|
+
.option("--type <type>", "filter by type (issue, document, project, recurringIssue)")
|
|
26
|
+
.option("-l, --limit <number>", "limit results", "50")
|
|
27
|
+
.action(handleAsyncCommand(async (options, command) => {
|
|
28
|
+
const rootOpts = command.parent.parent.opts();
|
|
29
|
+
const graphQLService = createGraphQLService(rootOpts);
|
|
30
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
31
|
+
const result = await graphQLService.rawRequest(TEMPLATES_LIST_QUERY);
|
|
32
|
+
let items = result.templates ?? [];
|
|
33
|
+
if (options.type) {
|
|
34
|
+
const filterType = options.type.toLowerCase();
|
|
35
|
+
items = items.filter((t) => t.type.toLowerCase() === filterType);
|
|
36
|
+
}
|
|
37
|
+
items = items.slice(0, limit);
|
|
38
|
+
outputSuccess({
|
|
39
|
+
data: items.map(formatTemplateSummary),
|
|
40
|
+
meta: { count: items.length },
|
|
41
|
+
});
|
|
42
|
+
}));
|
|
43
|
+
templates
|
|
44
|
+
.command("read <templateId>")
|
|
45
|
+
.description("Read a template including its full content (templateData)")
|
|
46
|
+
.action(handleAsyncCommand(async (templateId, _options, command) => {
|
|
47
|
+
const rootOpts = command.parent.parent.opts();
|
|
48
|
+
const graphQLService = createGraphQLService(rootOpts);
|
|
49
|
+
const result = await graphQLService.rawRequest(TEMPLATE_BY_ID_QUERY, {
|
|
50
|
+
id: templateId,
|
|
51
|
+
});
|
|
52
|
+
const template = result.template;
|
|
53
|
+
if (!template) {
|
|
54
|
+
throw new Error(`Template "${templateId}" not found`);
|
|
55
|
+
}
|
|
56
|
+
outputSuccess(template);
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createLinearService } from "../utils/linear-service.js";
|
|
2
|
+
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
3
|
+
export function setupUsersCommands(program) {
|
|
4
|
+
const users = program.command("users").description("User operations");
|
|
5
|
+
users.action(() => users.help());
|
|
6
|
+
users
|
|
7
|
+
.command("list")
|
|
8
|
+
.description("List all users")
|
|
9
|
+
.option("--active", "Only show active users")
|
|
10
|
+
.option("-l, --limit <number>", "limit results", "100")
|
|
11
|
+
.action(handleAsyncCommand(async (options, command) => {
|
|
12
|
+
const rootOpts = command.parent.parent.opts();
|
|
13
|
+
const service = createLinearService(rootOpts);
|
|
14
|
+
const result = await service.getUsers(options.active, Number.parseInt(options.limit, 10));
|
|
15
|
+
outputSuccess({ data: result, meta: { count: result.length } });
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { TermRule } from "./term-enforcer.js";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical shape of `~/.config/el-linear/config.json`. This is the single source
|
|
4
|
+
* of truth for the on-disk config — the wizard reads/writes the same shape
|
|
5
|
+
* (as `Partial<ElLinearConfig>` since wizard runs may only set a subset).
|
|
6
|
+
*/
|
|
7
|
+
export interface ElLinearConfig {
|
|
8
|
+
defaultLabels: string[];
|
|
9
|
+
defaultTeam: string;
|
|
10
|
+
labels: {
|
|
11
|
+
workspace: Record<string, string>;
|
|
12
|
+
teams: Record<string, Record<string, string>>;
|
|
13
|
+
};
|
|
14
|
+
members: {
|
|
15
|
+
aliases: Record<string, string>;
|
|
16
|
+
fullNames: Record<string, string>;
|
|
17
|
+
handles: Record<string, Record<string, string>>;
|
|
18
|
+
uuids: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
statusDefaults: {
|
|
21
|
+
noProject: string;
|
|
22
|
+
withAssigneeAndProject: string;
|
|
23
|
+
};
|
|
24
|
+
teamAliases: Record<string, string>;
|
|
25
|
+
teams: Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* Term-enforcement rules. Each rule has a canonical form and a list of
|
|
28
|
+
* rejected forms; rejected forms in issue titles/descriptions are flagged
|
|
29
|
+
* (or thrown on, in strict mode) with a hint to use the canonical form.
|
|
30
|
+
*/
|
|
31
|
+
terms: TermRule[];
|
|
32
|
+
validation?: {
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
typeLabels?: string[];
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Optional override for the Linear workspace URL key (the part after
|
|
38
|
+
* `linear.app/` in issue URLs). When omitted, el-linear queries the Linear API
|
|
39
|
+
* once per session and caches the result.
|
|
40
|
+
*/
|
|
41
|
+
workspaceUrlKey?: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function loadConfig(): ElLinearConfig;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { outputWarning } from "../utils/output.js";
|
|
3
|
+
import { CONFIG_PATH } from "./paths.js";
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
defaultTeam: "",
|
|
6
|
+
defaultLabels: [],
|
|
7
|
+
members: {
|
|
8
|
+
aliases: {},
|
|
9
|
+
fullNames: {},
|
|
10
|
+
handles: {},
|
|
11
|
+
uuids: {},
|
|
12
|
+
},
|
|
13
|
+
teams: {},
|
|
14
|
+
teamAliases: {},
|
|
15
|
+
labels: {
|
|
16
|
+
workspace: {},
|
|
17
|
+
teams: {},
|
|
18
|
+
},
|
|
19
|
+
statusDefaults: {
|
|
20
|
+
noProject: "Triage",
|
|
21
|
+
withAssigneeAndProject: "Todo",
|
|
22
|
+
},
|
|
23
|
+
terms: [],
|
|
24
|
+
};
|
|
25
|
+
let cachedConfig;
|
|
26
|
+
export function loadConfig() {
|
|
27
|
+
if (cachedConfig) {
|
|
28
|
+
return cachedConfig;
|
|
29
|
+
}
|
|
30
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
31
|
+
try {
|
|
32
|
+
const userConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
33
|
+
// Migration: the legacy `brand: { name, reject }` config is auto-promoted to
|
|
34
|
+
// a single entry in `terms[]`. We warn (not throw) so existing users get a
|
|
35
|
+
// grace period to update their config.
|
|
36
|
+
if (userConfig.brand &&
|
|
37
|
+
typeof userConfig.brand === "object" &&
|
|
38
|
+
!Array.isArray(userConfig.brand)) {
|
|
39
|
+
const legacy = userConfig.brand;
|
|
40
|
+
if (typeof legacy.name === "string" && Array.isArray(legacy.reject)) {
|
|
41
|
+
outputWarning("config.brand is deprecated — replace it with config.terms = [{ canonical, reject }]. See README#term-enforcer.");
|
|
42
|
+
const existing = Array.isArray(userConfig.terms)
|
|
43
|
+
? userConfig.terms
|
|
44
|
+
: [];
|
|
45
|
+
userConfig.terms = [
|
|
46
|
+
...existing,
|
|
47
|
+
{ canonical: legacy.name, reject: legacy.reject },
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
delete userConfig.brand;
|
|
51
|
+
}
|
|
52
|
+
cachedConfig = deepMerge(DEFAULT_CONFIG, userConfig);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
outputWarning(`Failed to parse ${CONFIG_PATH}, using empty defaults`);
|
|
56
|
+
cachedConfig = DEFAULT_CONFIG;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
outputWarning(`No config found at ${CONFIG_PATH}. Run with --init to create one.`);
|
|
61
|
+
cachedConfig = DEFAULT_CONFIG;
|
|
62
|
+
}
|
|
63
|
+
return cachedConfig;
|
|
64
|
+
}
|
|
65
|
+
function deepMerge(target, source) {
|
|
66
|
+
const result = { ...target };
|
|
67
|
+
for (const key of Object.keys(source)) {
|
|
68
|
+
if (source[key] &&
|
|
69
|
+
typeof source[key] === "object" &&
|
|
70
|
+
!Array.isArray(source[key]) &&
|
|
71
|
+
target[key] &&
|
|
72
|
+
typeof target[key] === "object" &&
|
|
73
|
+
!Array.isArray(target[key])) {
|
|
74
|
+
result[key] = deepMerge(target[key], source[key]);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
result[key] = source[key];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue creation validation — Phase 1 of DEV-3708.
|
|
3
|
+
*
|
|
4
|
+
* Enforces structural requirements (type labels, description, label normalization)
|
|
5
|
+
* at the CLI level rather than relying on LLM skill prompts.
|
|
6
|
+
*
|
|
7
|
+
* Gated behind `validation.enabled` in config so it can be rolled out per-user
|
|
8
|
+
* before becoming the default for the team.
|
|
9
|
+
*/
|
|
10
|
+
export interface ValidationResult {
|
|
11
|
+
errors: string[];
|
|
12
|
+
warnings: string[];
|
|
13
|
+
/** Labels after normalization (aliases resolved, case fixed). */
|
|
14
|
+
normalizedLabels: string[] | null;
|
|
15
|
+
}
|
|
16
|
+
export interface ValidationInput {
|
|
17
|
+
labels: string[] | null;
|
|
18
|
+
description: string | undefined;
|
|
19
|
+
title: string;
|
|
20
|
+
assignee: string | undefined;
|
|
21
|
+
project: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Normalize a label name: resolve known aliases and fix casing.
|
|
25
|
+
* Returns the canonical form if an alias exists, otherwise the original.
|
|
26
|
+
*/
|
|
27
|
+
export declare function normalizeLabel(label: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Validate issue creation inputs. Returns errors (block creation) and warnings (informational).
|
|
30
|
+
*
|
|
31
|
+
* Only runs when `validation.enabled` is true in config.
|
|
32
|
+
* Bypass entirely with `--no-validate`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function validateIssueCreation(input: ValidationInput): ValidationResult;
|
|
35
|
+
/**
|
|
36
|
+
* Apply validation results: emit warnings, throw on errors.
|
|
37
|
+
* Called from the create command unless `--no-validate` is set.
|
|
38
|
+
*/
|
|
39
|
+
export declare function enforceValidation(result: ValidationResult): void;
|