@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,264 @@
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
+ import { outputWarning } from "../utils/output.js";
11
+ import { loadConfig } from "./config.js";
12
+ /** Canonical type labels. Stored in config so they can be updated without code changes. */
13
+ const DEFAULT_TYPE_LABELS = ["bug", "feature", "refactor", "chore", "spike"];
14
+ /**
15
+ * Recommended leading verbs for each type label.
16
+ * Title verb and type label should express the same intent.
17
+ */
18
+ const TYPE_VERB_MAP = {
19
+ bug: ["Fix", "Resolve", "Patch", "Handle", "Address", "Correct"],
20
+ feature: [
21
+ "Add",
22
+ "Build",
23
+ "Create",
24
+ "Implement",
25
+ "Enable",
26
+ "Ship",
27
+ "Launch",
28
+ "Design",
29
+ "Wire",
30
+ "Integrate",
31
+ "Expose",
32
+ "Send",
33
+ "Track",
34
+ "Alert",
35
+ "Automate",
36
+ "Post",
37
+ ],
38
+ chore: [
39
+ "Update",
40
+ "Remove",
41
+ "Clean",
42
+ "Migrate",
43
+ "Deploy",
44
+ "Rotate",
45
+ "Set up",
46
+ "Configure",
47
+ "Document",
48
+ "Review",
49
+ "Publish",
50
+ "Standardize",
51
+ "Accept",
52
+ "Consolidate",
53
+ "Teardown",
54
+ "Upgrade",
55
+ ],
56
+ spike: [
57
+ "Research",
58
+ "Investigate",
59
+ "Explore",
60
+ "Evaluate",
61
+ "Audit",
62
+ "Benchmark",
63
+ "Test",
64
+ ],
65
+ refactor: [
66
+ "Refactor",
67
+ "Restructure",
68
+ "Extract",
69
+ "Decouple",
70
+ "Consolidate",
71
+ "Simplify",
72
+ ],
73
+ };
74
+ /**
75
+ * Common misspellings / wrong-case variants → canonical form.
76
+ * Covers the real mistakes observed in production data.
77
+ */
78
+ const LABEL_ALIASES = {
79
+ Bug: "bug",
80
+ Feature: "feature",
81
+ Refactor: "refactor",
82
+ Chore: "chore",
83
+ Spike: "spike",
84
+ "feature-request": "feature",
85
+ "bug-report": "bug",
86
+ enhancement: "feature",
87
+ };
88
+ function getValidationConfig() {
89
+ const config = loadConfig();
90
+ const validation = config.validation;
91
+ return {
92
+ enabled: validation?.enabled ?? true,
93
+ typeLabels: validation?.typeLabels ?? DEFAULT_TYPE_LABELS,
94
+ };
95
+ }
96
+ /**
97
+ * Normalize a label name: resolve known aliases and fix casing.
98
+ * Returns the canonical form if an alias exists, otherwise the original.
99
+ */
100
+ export function normalizeLabel(label) {
101
+ // Exact alias match first (case-sensitive for things like "Feature" → "feature")
102
+ if (label in LABEL_ALIASES) {
103
+ return LABEL_ALIASES[label];
104
+ }
105
+ return label;
106
+ }
107
+ /**
108
+ * Validate issue creation inputs. Returns errors (block creation) and warnings (informational).
109
+ *
110
+ * Only runs when `validation.enabled` is true in config.
111
+ * Bypass entirely with `--no-validate`.
112
+ */
113
+ export function validateIssueCreation(input) {
114
+ const vConfig = getValidationConfig();
115
+ const result = {
116
+ errors: [],
117
+ warnings: [],
118
+ normalizedLabels: null,
119
+ };
120
+ if (!vConfig.enabled) {
121
+ return result;
122
+ }
123
+ // --- Label normalization (always runs when validation is on) ---
124
+ if (input.labels && input.labels.length > 0) {
125
+ result.normalizedLabels = input.labels.map((label) => {
126
+ const normalized = normalizeLabel(label);
127
+ if (normalized !== label) {
128
+ result.warnings.push(`Label "${label}" normalized to "${normalized}" (alias)`);
129
+ }
130
+ return normalized;
131
+ });
132
+ }
133
+ const effectiveLabels = result.normalizedLabels ?? input.labels ?? [];
134
+ // --- Required: labels must be provided ---
135
+ if (effectiveLabels.length === 0) {
136
+ result.errors.push("Missing --labels. At least one label is required, including a type label.\n" +
137
+ ` Valid type labels: ${vConfig.typeLabels.join(", ")}\n` +
138
+ ' Example: --labels "bug,backend"');
139
+ }
140
+ else {
141
+ // --- Required: exactly one type label ---
142
+ const typeLabelsFound = effectiveLabels.filter((l) => vConfig.typeLabels.includes(l.toLowerCase()));
143
+ if (typeLabelsFound.length === 0) {
144
+ result.errors.push("Missing type label. Exactly one required.\n" +
145
+ ` Valid type labels: ${vConfig.typeLabels.join(", ")}\n` +
146
+ ` Provided labels: ${effectiveLabels.join(", ")}\n` +
147
+ ` Example: --labels "${vConfig.typeLabels[0]},${effectiveLabels[0]}"`);
148
+ }
149
+ else if (typeLabelsFound.length > 1) {
150
+ result.errors.push(`Multiple type labels found: ${typeLabelsFound.join(", ")}. Exactly one required.\n` +
151
+ ` Valid type labels: ${vConfig.typeLabels.join(", ")}`);
152
+ }
153
+ }
154
+ // --- Required: description must be provided ---
155
+ if (!input.description || input.description.trim().length === 0) {
156
+ result.errors.push("Missing --description. A description is required for issue creation.");
157
+ }
158
+ else {
159
+ // --- Warning: short description ---
160
+ if (input.description.trim().length < 50) {
161
+ result.warnings.push(`Description is only ${input.description.trim().length} characters. Consider adding more context.`);
162
+ }
163
+ // --- Warning: no "why" section ---
164
+ const descLower = input.description.toLowerCase();
165
+ const hasWhySection = descLower.includes("why we need") ||
166
+ descLower.includes("## why") ||
167
+ descLower.includes("**why") ||
168
+ descLower.includes("background") ||
169
+ descLower.includes("motivation") ||
170
+ descLower.includes("context:");
171
+ if (!hasWhySection) {
172
+ result.warnings.push('Consider adding a "## Why we need this" section to explain the motivation.');
173
+ }
174
+ }
175
+ // --- Warning: title style ---
176
+ if (input.title.length > 100) {
177
+ result.warnings.push(`Title is ${input.title.length} characters. Consider shortening to under 100.`);
178
+ }
179
+ if (/^(A|An|The)\s/.test(input.title)) {
180
+ result.warnings.push("Consider starting the title with an action verb instead of an article.");
181
+ }
182
+ // --- Warning: title-verb / type-label alignment ---
183
+ const typeLabelsFound = effectiveLabels.filter((l) => vConfig.typeLabels.includes(l.toLowerCase()));
184
+ if (typeLabelsFound.length === 1) {
185
+ checkTitleVerbAlignment(input.title, typeLabelsFound[0].toLowerCase(), result);
186
+ }
187
+ // --- Required: assignee ---
188
+ if (!input.assignee) {
189
+ result.errors.push("Missing --assignee. Every issue must have an assignee.\n" +
190
+ " Use `el-linear users list --active` to find valid assignees.");
191
+ }
192
+ // --- Required: project ---
193
+ if (!input.project) {
194
+ result.errors.push("Missing --project. Every issue must belong to a project.\n" +
195
+ " Use `el-linear projects list` to find valid projects.");
196
+ }
197
+ return result;
198
+ }
199
+ /**
200
+ * Check whether the title's leading verb aligns with the provided type label.
201
+ * Only warns when the first word is a recognized verb in any type's set —
202
+ * titles starting with non-verb words (e.g. "Dashboard auth failing") are left alone.
203
+ */
204
+ function checkTitleVerbAlignment(title, typeLabel, result) {
205
+ const verbs = TYPE_VERB_MAP[typeLabel];
206
+ if (!verbs)
207
+ return;
208
+ // Check multi-word verbs first (e.g. "Set up")
209
+ for (const [type, typeVerbs] of Object.entries(TYPE_VERB_MAP)) {
210
+ for (const verb of typeVerbs) {
211
+ if (!verb.includes(" "))
212
+ continue;
213
+ if (title.toLowerCase().startsWith(verb.toLowerCase() + " ") ||
214
+ title.toLowerCase() === verb.toLowerCase()) {
215
+ if (type === typeLabel)
216
+ return; // match — all good
217
+ result.warnings.push(`Title starts with "${verb}" but type is "${typeLabel}". ` +
218
+ `Consider starting with: ${verbs.slice(0, 6).join(", ")}`);
219
+ result.warnings.push(`"${verb}" is typically associated with "${type}" issues.`);
220
+ return;
221
+ }
222
+ }
223
+ }
224
+ // Single-word verb check
225
+ const firstWord = title.split(/\s/)[0];
226
+ if (!firstWord)
227
+ return;
228
+ // Is the first word in the correct type's verb set?
229
+ const matchesType = verbs.some((v) => v.toLowerCase() === firstWord.toLowerCase());
230
+ if (matchesType)
231
+ return; // match — all good
232
+ // Is the first word in any OTHER type's verb set?
233
+ let matchedOtherType = null;
234
+ for (const [type, typeVerbs] of Object.entries(TYPE_VERB_MAP)) {
235
+ if (type === typeLabel)
236
+ continue;
237
+ if (typeVerbs.some((v) => v.toLowerCase() === firstWord.toLowerCase())) {
238
+ matchedOtherType = type;
239
+ break;
240
+ }
241
+ }
242
+ // Only warn if the first word IS a recognized verb (just for the wrong type)
243
+ if (matchedOtherType) {
244
+ result.warnings.push(`Title starts with "${firstWord}" but type is "${typeLabel}". ` +
245
+ `Consider starting with: ${verbs.slice(0, 6).join(", ")}`);
246
+ result.warnings.push(`"${firstWord}" is typically associated with "${matchedOtherType}" issues.`);
247
+ }
248
+ }
249
+ /**
250
+ * Apply validation results: emit warnings, throw on errors.
251
+ * Called from the create command unless `--no-validate` is set.
252
+ */
253
+ export function enforceValidation(result) {
254
+ for (const warning of result.warnings) {
255
+ outputWarning(warning, "validation");
256
+ }
257
+ if (result.errors.length > 0) {
258
+ const errorMsg = "Issue creation blocked by validation:\n\n" +
259
+ result.errors.map((e) => ` ✗ ${e}`).join("\n\n") +
260
+ "\n\nTo skip validation, pass --skip-validation.\n" +
261
+ "To disable validation permanently, set validation.enabled: false in ~/.config/el-linear/config.json.";
262
+ throw new Error(errorMsg);
263
+ }
264
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Canonical paths to the el-linear on-disk state. Single source of truth so the
3
+ * config loader, the auth module, and the init wizard all agree on where to
4
+ * read and write.
5
+ */
6
+ export declare const CONFIG_DIR: string;
7
+ export declare const CONFIG_PATH: string;
8
+ export declare const TOKEN_PATH: string;
9
+ export declare const ALIASES_PROGRESS_PATH: string;
10
+ /**
11
+ * Legacy fallback paths kept for backward compatibility. The CLI was briefly
12
+ * published as `@enrichlayer/linctl` (binary `linctl`); reverted to el-linear
13
+ * because of an npm collision with `dorkitude/linctl`. Reads check the new
14
+ * path first and fall back to these. Writes always go to the new path.
15
+ */
16
+ export declare const LEGACY_LINCTL_CONFIG_DIR: string;
17
+ export declare const LEGACY_LINCTL_CONFIG_PATH: string;
18
+ export declare const LEGACY_LINCTL_TOKEN_PATH: string;
19
+ /** Even older fallback from before the `~/.config/...` move (one release). */
20
+ export declare const LEGACY_TOKEN_PATH: string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Canonical paths to the el-linear on-disk state. Single source of truth so the
3
+ * config loader, the auth module, and the init wizard all agree on where to
4
+ * read and write.
5
+ */
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ export const CONFIG_DIR = path.join(os.homedir(), ".config", "el-linear");
9
+ export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
10
+ export const TOKEN_PATH = path.join(CONFIG_DIR, "token");
11
+ export const ALIASES_PROGRESS_PATH = path.join(CONFIG_DIR, ".init-aliases-progress");
12
+ /**
13
+ * Legacy fallback paths kept for backward compatibility. The CLI was briefly
14
+ * published as `@enrichlayer/linctl` (binary `linctl`); reverted to el-linear
15
+ * because of an npm collision with `dorkitude/linctl`. Reads check the new
16
+ * path first and fall back to these. Writes always go to the new path.
17
+ */
18
+ export const LEGACY_LINCTL_CONFIG_DIR = path.join(os.homedir(), ".config", "linctl");
19
+ export const LEGACY_LINCTL_CONFIG_PATH = path.join(LEGACY_LINCTL_CONFIG_DIR, "config.json");
20
+ export const LEGACY_LINCTL_TOKEN_PATH = path.join(LEGACY_LINCTL_CONFIG_DIR, "token");
21
+ /** Even older fallback from before the `~/.config/...` move (one release). */
22
+ export const LEGACY_TOKEN_PATH = path.join(os.homedir(), ".linear_api_token");
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Resolve a team key/name/alias to its UUID.
3
+ * Case-insensitive: "fe" → FE UUID, "frontend" → FE UUID via alias.
4
+ */
5
+ export declare function resolveTeam(input: string): string;
6
+ /**
7
+ * Resolve a member name/alias/handle to their UUID.
8
+ * Supports: "bob" → alias, "Alice" → name, "@alice-handle" → gitlab handle. Case-insensitive.
9
+ */
10
+ export declare function resolveMember(input: string): string;
11
+ /**
12
+ * Resolve an assignee input to a UUID, supporting "me" to refer to the API token owner.
13
+ * Falls back to resolveMember for all other inputs.
14
+ */
15
+ export declare function resolveAssignee(input: string, rootOpts: Record<string, unknown>): Promise<string>;
16
+ /**
17
+ * Resolve a user's display name from their UUID via config fullNames map.
18
+ * Returns the full name if found, otherwise returns the original name.
19
+ */
20
+ export declare function resolveUserDisplayName(id: string, name: string): string;
21
+ /**
22
+ * Resolve labels for a team, returning UUIDs for known labels.
23
+ * Unknown labels are returned as-is for API resolution.
24
+ */
25
+ export declare function resolveLabels(names: string[], teamKey?: string): string[];
@@ -0,0 +1,183 @@
1
+ import { createGraphQLService } from "../utils/graphql-service.js";
2
+ import { outputWarning } from "../utils/output.js";
3
+ import { isUuid, isUuidPrefix } from "../utils/uuid.js";
4
+ import { loadConfig } from "./config.js";
5
+ /**
6
+ * Resolve a team key/name/alias to its UUID.
7
+ * Case-insensitive: "fe" → FE UUID, "frontend" → FE UUID via alias.
8
+ */
9
+ export function resolveTeam(input) {
10
+ if (isUuid(input)) {
11
+ return input;
12
+ }
13
+ const config = loadConfig();
14
+ const upper = input.toUpperCase();
15
+ const lower = input.toLowerCase();
16
+ // Check direct key match (case-insensitive)
17
+ if (config.teams[upper]) {
18
+ return config.teams[upper];
19
+ }
20
+ // Check aliases (case-insensitive) → resolve to team key → then to UUID
21
+ for (const [alias, teamKey] of Object.entries(config.teamAliases)) {
22
+ if (alias.toLowerCase() === lower) {
23
+ const uuid = config.teams[teamKey.toUpperCase()];
24
+ if (uuid) {
25
+ return uuid;
26
+ }
27
+ }
28
+ }
29
+ // Return input as-is for API resolution
30
+ return input;
31
+ }
32
+ /**
33
+ * Resolve a member name/alias/handle to their UUID.
34
+ * Supports: "bob" → alias, "Alice" → name, "@alice-handle" → gitlab handle. Case-insensitive.
35
+ */
36
+ export function resolveMember(input) {
37
+ if (isUuid(input)) {
38
+ return input;
39
+ }
40
+ const config = loadConfig();
41
+ // Strip leading @ (common in git/platform handles)
42
+ const cleaned = input.startsWith("@") ? input.slice(1) : input;
43
+ const lower = cleaned.toLowerCase();
44
+ // Check aliases first (case-insensitive)
45
+ for (const [alias, name] of Object.entries(config.members.aliases)) {
46
+ if (alias.toLowerCase() === lower) {
47
+ const uuid = config.members.uuids[name];
48
+ if (uuid) {
49
+ return uuid;
50
+ }
51
+ }
52
+ }
53
+ // Check platform handles (gitlab, github, etc.)
54
+ for (const handleMap of Object.values(config.members.handles)) {
55
+ for (const [handle, name] of Object.entries(handleMap)) {
56
+ if (handle.toLowerCase() === lower) {
57
+ const uuid = config.members.uuids[name];
58
+ if (uuid) {
59
+ return uuid;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ // Check direct name match (case-insensitive) — e.g., "David"
65
+ for (const [name, uuid] of Object.entries(config.members.uuids)) {
66
+ if (name.toLowerCase() === lower) {
67
+ return uuid;
68
+ }
69
+ }
70
+ // Check full names (case-insensitive) — e.g., "David Doe"
71
+ if (config.members.fullNames) {
72
+ for (const [uuid, fullName] of Object.entries(config.members.fullNames)) {
73
+ if (fullName.toLowerCase() === lower) {
74
+ return uuid;
75
+ }
76
+ }
77
+ // Partial match on full names — e.g., "doe" matches "David Doe"
78
+ for (const [uuid, fullName] of Object.entries(config.members.fullNames)) {
79
+ const parts = fullName.toLowerCase().split(/\s+/);
80
+ if (parts.some((part) => part === lower)) {
81
+ return uuid;
82
+ }
83
+ }
84
+ }
85
+ // Return input as-is for API resolution
86
+ return input;
87
+ }
88
+ /**
89
+ * Resolve an assignee input to a UUID, supporting "me" to refer to the API token owner.
90
+ * Falls back to resolveMember for all other inputs.
91
+ */
92
+ export async function resolveAssignee(input, rootOpts) {
93
+ if (input.toLowerCase() === "me") {
94
+ const graphQLService = createGraphQLService(rootOpts);
95
+ const result = await graphQLService.rawRequest("{ viewer { id } }");
96
+ const viewer = result.viewer;
97
+ if (!viewer?.id) {
98
+ throw new Error('Could not resolve "me" — viewer query returned no user. Check your API token.');
99
+ }
100
+ return viewer.id;
101
+ }
102
+ return resolveMember(input);
103
+ }
104
+ /**
105
+ * Resolve a user's display name from their UUID via config fullNames map.
106
+ * Returns the full name if found, otherwise returns the original name.
107
+ */
108
+ export function resolveUserDisplayName(id, name) {
109
+ const config = loadConfig();
110
+ return config.members.fullNames[id] || name;
111
+ }
112
+ /**
113
+ * Look up a label name in a label map (case-insensitive).
114
+ * Returns the UUID if valid, null if found but invalid/truncated, undefined if not found.
115
+ */
116
+ function lookupLabel(labelMap, lower, context) {
117
+ for (const [labelName, uuid] of Object.entries(labelMap)) {
118
+ if (labelName.toLowerCase() !== lower) {
119
+ continue;
120
+ }
121
+ if (isUuid(uuid)) {
122
+ return uuid;
123
+ }
124
+ if (isUuidPrefix(uuid)) {
125
+ const scope = context ? ` (${context})` : "";
126
+ outputWarning(`Config label "${labelName}"${scope} has truncated UUID "${uuid}". Falling back to API resolution.`);
127
+ }
128
+ return null;
129
+ }
130
+ return undefined;
131
+ }
132
+ /** Common label abbreviations → canonical names */
133
+ const LABEL_ALIASES = {
134
+ docs: "documentation",
135
+ doc: "documentation",
136
+ feat: "feature-request",
137
+ infra: "infrastructure",
138
+ fe: "frontend",
139
+ be: "backend",
140
+ ci: "ci/cd",
141
+ };
142
+ /**
143
+ * Resolve a label name to its UUID for a specific team.
144
+ * Checks workspace-level labels first, then team-scoped labels.
145
+ * Falls back to alias resolution and prefix matching.
146
+ */
147
+ function resolveLabel(name, teamKey) {
148
+ if (isUuid(name)) {
149
+ return name;
150
+ }
151
+ const config = loadConfig();
152
+ const lower = name.toLowerCase();
153
+ const workspace = lookupLabel(config.labels.workspace, lower);
154
+ if (workspace !== undefined) {
155
+ return workspace;
156
+ }
157
+ if (teamKey) {
158
+ const upper = teamKey.toUpperCase();
159
+ const teamLabels = config.labels.teams[upper];
160
+ if (teamLabels) {
161
+ const team = lookupLabel(teamLabels, lower, `team ${upper}`);
162
+ if (team !== undefined) {
163
+ return team;
164
+ }
165
+ }
166
+ }
167
+ // Try alias resolution — e.g., "docs" → "documentation"
168
+ const aliased = LABEL_ALIASES[lower];
169
+ if (aliased) {
170
+ return resolveLabel(aliased, teamKey);
171
+ }
172
+ return null;
173
+ }
174
+ /**
175
+ * Resolve labels for a team, returning UUIDs for known labels.
176
+ * Unknown labels are returned as-is for API resolution.
177
+ */
178
+ export function resolveLabels(names, teamKey) {
179
+ return names.map((name) => {
180
+ const resolved = resolveLabel(name, teamKey);
181
+ return resolved || name;
182
+ });
183
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Determine the default status based on issue attributes.
3
+ *
4
+ * Rules:
5
+ * - Explicit status always wins (return as-is)
6
+ * - Has assignee + project → "Todo"
7
+ * - Missing assignee OR project → "Triage"
8
+ */
9
+ export declare function resolveDefaultStatus(opts: {
10
+ explicitStatus?: string;
11
+ hasAssignee: boolean;
12
+ hasProject: boolean;
13
+ }): string | undefined;
@@ -0,0 +1,20 @@
1
+ import { loadConfig } from "./config.js";
2
+ /**
3
+ * Determine the default status based on issue attributes.
4
+ *
5
+ * Rules:
6
+ * - Explicit status always wins (return as-is)
7
+ * - Has assignee + project → "Todo"
8
+ * - Missing assignee OR project → "Triage"
9
+ */
10
+ export function resolveDefaultStatus(opts) {
11
+ // Explicit status always wins
12
+ if (opts.explicitStatus) {
13
+ return opts.explicitStatus;
14
+ }
15
+ const config = loadConfig();
16
+ if (opts.hasAssignee && opts.hasProject) {
17
+ return config.statusDefaults.withAssigneeAndProject;
18
+ }
19
+ return config.statusDefaults.noProject;
20
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A single term-enforcement rule. The canonical form is what authors should use;
3
+ * the reject list contains common misspellings or alternate forms that should be
4
+ * flagged (and corrected to the canonical form).
5
+ *
6
+ * @example
7
+ * { canonical: "Enrich Layer", reject: ["EnrichLayer", "enrichlayer", "Enrichlayer"] }
8
+ * { canonical: "Linear", reject: ["linear.app", "Linear App"] }
9
+ * { canonical: "GitHub", reject: ["Github", "github", "GitHUB"] }
10
+ */
11
+ export interface TermRule {
12
+ canonical: string;
13
+ reject: string[];
14
+ }
15
+ /**
16
+ * Enforce all configured term rules against the given texts (typically a title
17
+ * and an optional description). In strict mode, the first violation throws;
18
+ * otherwise, all violations are buffered as warnings on the JSON output.
19
+ *
20
+ * Configure rules in your el-linear config:
21
+ * {
22
+ * "terms": [
23
+ * { "canonical": "Enrich Layer", "reject": ["EnrichLayer", "enrichlayer"] }
24
+ * ]
25
+ * }
26
+ *
27
+ * If no rules are configured, this is a no-op.
28
+ */
29
+ export declare function enforceTerms(texts: Array<string | null | undefined>, options?: {
30
+ strict?: boolean;
31
+ }): void;
@@ -0,0 +1,69 @@
1
+ import { outputWarning } from "../utils/output.js";
2
+ import { loadConfig } from "./config.js";
3
+ /**
4
+ * Build a regex that matches the rejected form as a standalone token, while
5
+ * tolerating common URL/file boundaries. The look-arounds prevent us from
6
+ * flagging `enrichlayer.com` (URL), `myenrichlayer` (compound word), or
7
+ * `path/to/enrichlayer` (file path).
8
+ */
9
+ function buildRejectRegex(rejected) {
10
+ return new RegExp(`(?<!\\w|\\.|/)${escapeRegExp(rejected)}(?!\\w|\\.com|\\.co|\\.io)`, "g");
11
+ }
12
+ function findViolations(text, rules) {
13
+ const violations = [];
14
+ for (const rule of rules) {
15
+ for (const rejected of rule.reject) {
16
+ const matches = text.match(buildRejectRegex(rejected));
17
+ if (matches) {
18
+ violations.push({
19
+ rule,
20
+ matched: rejected,
21
+ occurrences: matches.length,
22
+ });
23
+ }
24
+ }
25
+ }
26
+ return violations;
27
+ }
28
+ function formatViolation(v) {
29
+ const plural = v.occurrences > 1 ? "s" : "";
30
+ return `Found "${v.matched}" — use "${v.rule.canonical}" instead (${v.occurrences} occurrence${plural})`;
31
+ }
32
+ /**
33
+ * Enforce all configured term rules against the given texts (typically a title
34
+ * and an optional description). In strict mode, the first violation throws;
35
+ * otherwise, all violations are buffered as warnings on the JSON output.
36
+ *
37
+ * Configure rules in your el-linear config:
38
+ * {
39
+ * "terms": [
40
+ * { "canonical": "Enrich Layer", "reject": ["EnrichLayer", "enrichlayer"] }
41
+ * ]
42
+ * }
43
+ *
44
+ * If no rules are configured, this is a no-op.
45
+ */
46
+ export function enforceTerms(texts, options = {}) {
47
+ const { terms } = loadConfig();
48
+ if (!terms || terms.length === 0) {
49
+ return;
50
+ }
51
+ const allViolations = [];
52
+ for (const text of texts) {
53
+ if (!text) {
54
+ continue;
55
+ }
56
+ allViolations.push(...findViolations(text, terms));
57
+ }
58
+ if (allViolations.length === 0) {
59
+ return;
60
+ }
61
+ const warnings = allViolations.map(formatViolation);
62
+ if (options.strict) {
63
+ throw new Error(`Term enforcement failed:\n${warnings.map((w) => ` - ${w}`).join("\n")}`);
64
+ }
65
+ outputWarning(warnings, "term_enforcement");
66
+ }
67
+ function escapeRegExp(string) {
68
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
69
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};