@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.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/claude-skills/linear-operations/SKILL.md +315 -0
  4. package/claude-skills/linear-operations/evals/evals.json +46 -0
  5. package/dist/commands/attachments.d.ts +2 -0
  6. package/dist/commands/attachments.js +57 -0
  7. package/dist/commands/batch.d.ts +2 -0
  8. package/dist/commands/batch.js +309 -0
  9. package/dist/commands/comments.d.ts +2 -0
  10. package/dist/commands/comments.js +272 -0
  11. package/dist/commands/config.d.ts +2 -0
  12. package/dist/commands/config.js +15 -0
  13. package/dist/commands/cycles.d.ts +2 -0
  14. package/dist/commands/cycles.js +63 -0
  15. package/dist/commands/documents.d.ts +2 -0
  16. package/dist/commands/documents.js +175 -0
  17. package/dist/commands/embeds.d.ts +2 -0
  18. package/dist/commands/embeds.js +65 -0
  19. package/dist/commands/gdoc.d.ts +2 -0
  20. package/dist/commands/gdoc.js +37 -0
  21. package/dist/commands/graphql.d.ts +2 -0
  22. package/dist/commands/graphql.js +70 -0
  23. package/dist/commands/init/aliases.d.ts +109 -0
  24. package/dist/commands/init/aliases.js +569 -0
  25. package/dist/commands/init/defaults.d.ts +25 -0
  26. package/dist/commands/init/defaults.js +112 -0
  27. package/dist/commands/init/index.d.ts +18 -0
  28. package/dist/commands/init/index.js +182 -0
  29. package/dist/commands/init/shared.d.ts +88 -0
  30. package/dist/commands/init/shared.js +164 -0
  31. package/dist/commands/init/token.d.ts +50 -0
  32. package/dist/commands/init/token.js +141 -0
  33. package/dist/commands/init/workspace.d.ts +20 -0
  34. package/dist/commands/init/workspace.js +80 -0
  35. package/dist/commands/issue-id.d.ts +28 -0
  36. package/dist/commands/issue-id.js +81 -0
  37. package/dist/commands/issues.d.ts +2 -0
  38. package/dist/commands/issues.js +1145 -0
  39. package/dist/commands/labels.d.ts +2 -0
  40. package/dist/commands/labels.js +100 -0
  41. package/dist/commands/project-milestones.d.ts +2 -0
  42. package/dist/commands/project-milestones.js +143 -0
  43. package/dist/commands/projects.d.ts +2 -0
  44. package/dist/commands/projects.js +336 -0
  45. package/dist/commands/read-shortcut.d.ts +6 -0
  46. package/dist/commands/read-shortcut.js +69 -0
  47. package/dist/commands/releases.d.ts +2 -0
  48. package/dist/commands/releases.js +142 -0
  49. package/dist/commands/search.d.ts +2 -0
  50. package/dist/commands/search.js +171 -0
  51. package/dist/commands/teams.d.ts +2 -0
  52. package/dist/commands/teams.js +19 -0
  53. package/dist/commands/templates.d.ts +2 -0
  54. package/dist/commands/templates.js +58 -0
  55. package/dist/commands/users.d.ts +2 -0
  56. package/dist/commands/users.js +17 -0
  57. package/dist/config/config.d.ts +43 -0
  58. package/dist/config/config.js +81 -0
  59. package/dist/config/issue-validation.d.ts +39 -0
  60. package/dist/config/issue-validation.js +264 -0
  61. package/dist/config/paths.d.ts +20 -0
  62. package/dist/config/paths.js +22 -0
  63. package/dist/config/resolver.d.ts +25 -0
  64. package/dist/config/resolver.js +183 -0
  65. package/dist/config/status-defaults.d.ts +13 -0
  66. package/dist/config/status-defaults.js +20 -0
  67. package/dist/config/term-enforcer.d.ts +31 -0
  68. package/dist/config/term-enforcer.js +69 -0
  69. package/dist/main.d.ts +2 -0
  70. package/dist/main.js +76 -0
  71. package/dist/queries/attachments.d.ts +3 -0
  72. package/dist/queries/attachments.js +35 -0
  73. package/dist/queries/comments.d.ts +3 -0
  74. package/dist/queries/comments.js +64 -0
  75. package/dist/queries/common.d.ts +2 -0
  76. package/dist/queries/common.js +109 -0
  77. package/dist/queries/cycles.d.ts +2 -0
  78. package/dist/queries/cycles.js +40 -0
  79. package/dist/queries/documents.d.ts +5 -0
  80. package/dist/queries/documents.js +67 -0
  81. package/dist/queries/introspect.d.ts +2 -0
  82. package/dist/queries/introspect.js +24 -0
  83. package/dist/queries/issues.d.ts +23 -0
  84. package/dist/queries/issues.js +380 -0
  85. package/dist/queries/labels.d.ts +4 -0
  86. package/dist/queries/labels.js +53 -0
  87. package/dist/queries/project-milestones.d.ts +6 -0
  88. package/dist/queries/project-milestones.js +125 -0
  89. package/dist/queries/projects.d.ts +7 -0
  90. package/dist/queries/projects.js +104 -0
  91. package/dist/queries/releases.d.ts +4 -0
  92. package/dist/queries/releases.js +85 -0
  93. package/dist/queries/search.d.ts +1 -0
  94. package/dist/queries/search.js +35 -0
  95. package/dist/queries/templates.d.ts +2 -0
  96. package/dist/queries/templates.js +30 -0
  97. package/dist/types/linear.d.ts +217 -0
  98. package/dist/types/linear.js +6 -0
  99. package/dist/utils/auth.d.ts +4 -0
  100. package/dist/utils/auth.js +23 -0
  101. package/dist/utils/auto-link-references.d.ts +47 -0
  102. package/dist/utils/auto-link-references.js +188 -0
  103. package/dist/utils/date-format.d.ts +4 -0
  104. package/dist/utils/date-format.js +8 -0
  105. package/dist/utils/download-uploads.d.ts +7 -0
  106. package/dist/utils/download-uploads.js +88 -0
  107. package/dist/utils/embed-parser.d.ts +8 -0
  108. package/dist/utils/embed-parser.js +54 -0
  109. package/dist/utils/error-messages.d.ts +4 -0
  110. package/dist/utils/error-messages.js +17 -0
  111. package/dist/utils/file-service.d.ts +13 -0
  112. package/dist/utils/file-service.js +239 -0
  113. package/dist/utils/gdoc-parser.d.ts +45 -0
  114. package/dist/utils/gdoc-parser.js +107 -0
  115. package/dist/utils/graphql-attachments-service.d.ts +12 -0
  116. package/dist/utils/graphql-attachments-service.js +46 -0
  117. package/dist/utils/graphql-documents-service.d.ts +18 -0
  118. package/dist/utils/graphql-documents-service.js +97 -0
  119. package/dist/utils/graphql-issues-service.d.ts +49 -0
  120. package/dist/utils/graphql-issues-service.js +925 -0
  121. package/dist/utils/graphql-service.d.ts +8 -0
  122. package/dist/utils/graphql-service.js +35 -0
  123. package/dist/utils/identifier-parser.d.ts +7 -0
  124. package/dist/utils/identifier-parser.js +20 -0
  125. package/dist/utils/issue-reference-extractor.d.ts +18 -0
  126. package/dist/utils/issue-reference-extractor.js +95 -0
  127. package/dist/utils/issue-reference-wrapper.d.ts +12 -0
  128. package/dist/utils/issue-reference-wrapper.js +91 -0
  129. package/dist/utils/linear-service.d.ts +26 -0
  130. package/dist/utils/linear-service.js +442 -0
  131. package/dist/utils/logger.d.ts +4 -0
  132. package/dist/utils/logger.js +8 -0
  133. package/dist/utils/markdown-prosemirror.d.ts +24 -0
  134. package/dist/utils/markdown-prosemirror.js +325 -0
  135. package/dist/utils/mention-resolver.d.ts +31 -0
  136. package/dist/utils/mention-resolver.js +234 -0
  137. package/dist/utils/output.d.ts +7 -0
  138. package/dist/utils/output.js +125 -0
  139. package/dist/utils/table-formatter.d.ts +4 -0
  140. package/dist/utils/table-formatter.js +249 -0
  141. package/dist/utils/usage.d.ts +2 -0
  142. package/dist/utils/usage.js +24 -0
  143. package/dist/utils/uuid.d.ts +2 -0
  144. package/dist/utils/uuid.js +8 -0
  145. package/dist/utils/validate-references.d.ts +10 -0
  146. package/dist/utils/validate-references.js +33 -0
  147. package/dist/utils/validators.d.ts +7 -0
  148. package/dist/utils/validators.js +71 -0
  149. package/dist/utils/workspace-url.d.ts +4 -0
  150. package/dist/utils/workspace-url.js +44 -0
  151. 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,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function setupSearchCommands(program: Command): void;
@@ -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,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function setupTeamsCommands(program: Command): void;
@@ -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,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function setupTemplatesCommands(program: Command): void;
@@ -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,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function setupUsersCommands(program: Command): void;
@@ -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;