@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,125 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
const warningBuffer = [];
|
|
4
|
+
let rawMode = false;
|
|
5
|
+
let jqFilter = null;
|
|
6
|
+
let fieldsFilter = null;
|
|
7
|
+
export function setRawMode(enabled) {
|
|
8
|
+
rawMode = enabled;
|
|
9
|
+
}
|
|
10
|
+
export function setJqFilter(filter) {
|
|
11
|
+
jqFilter = filter;
|
|
12
|
+
}
|
|
13
|
+
export function setFieldsFilter(fields) {
|
|
14
|
+
fieldsFilter = fields;
|
|
15
|
+
}
|
|
16
|
+
function filterFields(obj, fields) {
|
|
17
|
+
if (Array.isArray(obj)) {
|
|
18
|
+
return obj.map((item) => filterFields(item, fields));
|
|
19
|
+
}
|
|
20
|
+
if (obj !== null && typeof obj === "object") {
|
|
21
|
+
const source = obj;
|
|
22
|
+
const result = {};
|
|
23
|
+
for (const field of fields) {
|
|
24
|
+
if (field in source) {
|
|
25
|
+
result[field] = source[field];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
return obj;
|
|
31
|
+
}
|
|
32
|
+
export function outputSuccess(data) {
|
|
33
|
+
const warnings = drainWarnings();
|
|
34
|
+
let output;
|
|
35
|
+
if (warnings.length > 0 &&
|
|
36
|
+
data !== null &&
|
|
37
|
+
typeof data === "object" &&
|
|
38
|
+
!Array.isArray(data)) {
|
|
39
|
+
output = { ...data, _warnings: warnings };
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
output = data;
|
|
43
|
+
}
|
|
44
|
+
// --raw: unwrap { data: [...] } to just the array
|
|
45
|
+
if (rawMode &&
|
|
46
|
+
output !== null &&
|
|
47
|
+
typeof output === "object" &&
|
|
48
|
+
!Array.isArray(output)) {
|
|
49
|
+
const obj = output;
|
|
50
|
+
if (Array.isArray(obj.data)) {
|
|
51
|
+
output = obj.data;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// --fields: filter object keys (applies to array items or flat objects)
|
|
55
|
+
if (fieldsFilter) {
|
|
56
|
+
if (Array.isArray(output)) {
|
|
57
|
+
output = filterFields(output, fieldsFilter);
|
|
58
|
+
}
|
|
59
|
+
else if (output !== null && typeof output === "object") {
|
|
60
|
+
const obj = output;
|
|
61
|
+
if (Array.isArray(obj.data)) {
|
|
62
|
+
output = { ...obj, data: filterFields(obj.data, fieldsFilter) };
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
output = filterFields(output, fieldsFilter);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (jqFilter) {
|
|
70
|
+
const json = JSON.stringify(output);
|
|
71
|
+
// Normalize common shell-escape artifacts (zsh history expansion)
|
|
72
|
+
const filter = jqFilter.replace(/\\!/g, "!");
|
|
73
|
+
try {
|
|
74
|
+
const result = execFileSync("jq", ["-r", filter], {
|
|
75
|
+
input: json,
|
|
76
|
+
encoding: "utf8",
|
|
77
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
78
|
+
});
|
|
79
|
+
process.stdout.write(result);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
83
|
+
throw new Error(`jq filter failed: ${msg}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
logger.info(JSON.stringify(output, null, 2));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function outputWarning(message, _type) {
|
|
91
|
+
const messages = Array.isArray(message) ? message : [message];
|
|
92
|
+
for (const msg of messages) {
|
|
93
|
+
warningBuffer.push(msg);
|
|
94
|
+
}
|
|
95
|
+
// Warnings are buffered and embedded as _warnings in the next outputSuccess call.
|
|
96
|
+
// No stderr output — this prevents multi-JSON corruption when callers use 2>&1.
|
|
97
|
+
}
|
|
98
|
+
function drainWarnings() {
|
|
99
|
+
const warnings = [...warningBuffer];
|
|
100
|
+
warningBuffer.length = 0;
|
|
101
|
+
return warnings;
|
|
102
|
+
}
|
|
103
|
+
export function resetWarnings() {
|
|
104
|
+
warningBuffer.length = 0;
|
|
105
|
+
}
|
|
106
|
+
function outputError(error) {
|
|
107
|
+
const payload = JSON.stringify({ error: error.message }, null, 2);
|
|
108
|
+
// Write to stdout (same channel as success) so machine callers always
|
|
109
|
+
// receive exactly one parseable JSON object regardless of stream capture.
|
|
110
|
+
logger.info(payload);
|
|
111
|
+
if (process.env.EL_LINEAR_DEBUG ?? process.env.LINCTL_DEBUG) {
|
|
112
|
+
logger.error(error.stack ?? "");
|
|
113
|
+
}
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
export function handleAsyncCommand(asyncFn) {
|
|
117
|
+
return async (...args) => {
|
|
118
|
+
try {
|
|
119
|
+
await asyncFn(...args);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
outputError(error instanceof Error ? error : new Error(String(error)));
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { LinearIssue } from "../types/linear.js";
|
|
2
|
+
export declare function formatTable(issues: LinearIssue[], fieldNames?: string[]): string;
|
|
3
|
+
export declare function formatCsv(issues: LinearIssue[], fieldNames?: string[]): string;
|
|
4
|
+
export declare function formatMarkdown(issues: LinearIssue[], fieldNames?: string[]): string;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { PRIORITY_LABELS } from "./validators.js";
|
|
3
|
+
// ── Status colors ──────────────────────────────────────────────
|
|
4
|
+
const STATUS_COLORS = {
|
|
5
|
+
triage: pc.magenta,
|
|
6
|
+
backlog: pc.dim,
|
|
7
|
+
todo: pc.white,
|
|
8
|
+
"in progress": pc.cyan,
|
|
9
|
+
"in review": pc.blue,
|
|
10
|
+
done: pc.green,
|
|
11
|
+
canceled: (s) => pc.dim(pc.strikethrough(s)),
|
|
12
|
+
cancelled: (s) => pc.dim(pc.strikethrough(s)),
|
|
13
|
+
duplicate: (s) => pc.dim(pc.strikethrough(s)),
|
|
14
|
+
};
|
|
15
|
+
function colorizeStatus(value) {
|
|
16
|
+
const fn = STATUS_COLORS[value.toLowerCase()];
|
|
17
|
+
return fn ? fn(value) : value;
|
|
18
|
+
}
|
|
19
|
+
// ── Priority formatting ────────────────────────────────────────
|
|
20
|
+
const PRIORITY_PREFIX = {
|
|
21
|
+
0: "",
|
|
22
|
+
1: "▲▲▲ ",
|
|
23
|
+
2: "▲▲ ",
|
|
24
|
+
3: "▲ ",
|
|
25
|
+
4: "▽ ",
|
|
26
|
+
};
|
|
27
|
+
const PRIORITY_COLORS = {
|
|
28
|
+
0: pc.dim,
|
|
29
|
+
1: (s) => pc.red(pc.bold(s)),
|
|
30
|
+
2: pc.yellow,
|
|
31
|
+
3: pc.cyan,
|
|
32
|
+
4: pc.dim,
|
|
33
|
+
};
|
|
34
|
+
function extractPriority(issue) {
|
|
35
|
+
const label = PRIORITY_LABELS[issue.priority] ?? "—";
|
|
36
|
+
const prefix = PRIORITY_PREFIX[issue.priority] ?? "";
|
|
37
|
+
return `${prefix}${label}`;
|
|
38
|
+
}
|
|
39
|
+
function colorizePriority(value, issue) {
|
|
40
|
+
const fn = PRIORITY_COLORS[issue.priority];
|
|
41
|
+
return fn ? fn(value) : value;
|
|
42
|
+
}
|
|
43
|
+
// ── Hyperlinks ─────────────────────────────────────────────────
|
|
44
|
+
function hyperlink(text, url) {
|
|
45
|
+
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
46
|
+
}
|
|
47
|
+
// ── Column definitions ─────────────────────────────────────────
|
|
48
|
+
const ALL_COLUMNS = {
|
|
49
|
+
identifier: {
|
|
50
|
+
key: "identifier",
|
|
51
|
+
header: "ID",
|
|
52
|
+
width: 10,
|
|
53
|
+
extract: (i) => i.identifier,
|
|
54
|
+
colorize: (value, issue) => hyperlink(value, issue.url),
|
|
55
|
+
},
|
|
56
|
+
title: {
|
|
57
|
+
key: "title",
|
|
58
|
+
header: "Title",
|
|
59
|
+
width: 55,
|
|
60
|
+
extract: (i) => i.title,
|
|
61
|
+
},
|
|
62
|
+
status: {
|
|
63
|
+
key: "status",
|
|
64
|
+
header: "Status",
|
|
65
|
+
width: 14,
|
|
66
|
+
extract: (i) => i.state?.name ?? "—",
|
|
67
|
+
colorize: (v) => colorizeStatus(v),
|
|
68
|
+
},
|
|
69
|
+
priority: {
|
|
70
|
+
key: "priority",
|
|
71
|
+
header: "Priority",
|
|
72
|
+
width: 16,
|
|
73
|
+
extract: extractPriority,
|
|
74
|
+
colorize: colorizePriority,
|
|
75
|
+
},
|
|
76
|
+
assignee: {
|
|
77
|
+
key: "assignee",
|
|
78
|
+
header: "Assignee",
|
|
79
|
+
width: 20,
|
|
80
|
+
extract: (i) => {
|
|
81
|
+
if (!i.assignee) {
|
|
82
|
+
return "—";
|
|
83
|
+
}
|
|
84
|
+
const name = i.assignee.name;
|
|
85
|
+
const atIdx = name.indexOf("@");
|
|
86
|
+
return atIdx > 0 ? name.slice(0, atIdx) : name;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
project: {
|
|
90
|
+
key: "project",
|
|
91
|
+
header: "Project",
|
|
92
|
+
width: 28,
|
|
93
|
+
extract: (i) => i.project?.name ?? "—",
|
|
94
|
+
},
|
|
95
|
+
team: {
|
|
96
|
+
key: "team",
|
|
97
|
+
header: "Team",
|
|
98
|
+
width: 6,
|
|
99
|
+
extract: (i) => i.team?.key ?? "—",
|
|
100
|
+
},
|
|
101
|
+
labels: {
|
|
102
|
+
key: "labels",
|
|
103
|
+
header: "Labels",
|
|
104
|
+
width: 25,
|
|
105
|
+
extract: (i) => i.labels.map((l) => l.name).join(", ") || "—",
|
|
106
|
+
},
|
|
107
|
+
updated: {
|
|
108
|
+
key: "updated",
|
|
109
|
+
header: "Updated",
|
|
110
|
+
width: 12,
|
|
111
|
+
extract: (i) => i.updatedAt.slice(0, 10),
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const DEFAULT_COLUMNS = [
|
|
115
|
+
"identifier",
|
|
116
|
+
"title",
|
|
117
|
+
"status",
|
|
118
|
+
"priority",
|
|
119
|
+
"assignee",
|
|
120
|
+
"project",
|
|
121
|
+
];
|
|
122
|
+
function truncate(str, maxLen) {
|
|
123
|
+
if (str.length <= maxLen) {
|
|
124
|
+
return str;
|
|
125
|
+
}
|
|
126
|
+
return `${str.slice(0, maxLen - 1)}…`;
|
|
127
|
+
}
|
|
128
|
+
function pad(str, width) {
|
|
129
|
+
const truncated = truncate(str, width);
|
|
130
|
+
return truncated + " ".repeat(Math.max(0, width - truncated.length));
|
|
131
|
+
}
|
|
132
|
+
export function formatTable(issues, fieldNames) {
|
|
133
|
+
const columnKeys = fieldNames ?? DEFAULT_COLUMNS;
|
|
134
|
+
const columns = columnKeys
|
|
135
|
+
.map((key) => ALL_COLUMNS[key])
|
|
136
|
+
.filter((col) => col !== undefined);
|
|
137
|
+
if (columns.length === 0) {
|
|
138
|
+
return "No valid columns specified.";
|
|
139
|
+
}
|
|
140
|
+
const header = columns.map((col) => pad(col.header, col.width)).join(" ");
|
|
141
|
+
const separator = columns.map((col) => "─".repeat(col.width)).join("──");
|
|
142
|
+
const rows = issues.map((issue) => columns
|
|
143
|
+
.map((col) => {
|
|
144
|
+
const padded = pad(col.extract(issue), col.width);
|
|
145
|
+
return col.colorize ? col.colorize(padded, issue) : padded;
|
|
146
|
+
})
|
|
147
|
+
.join(" "));
|
|
148
|
+
return [header, separator, ...rows].join("\n");
|
|
149
|
+
}
|
|
150
|
+
export function formatCsv(issues, fieldNames) {
|
|
151
|
+
const columnKeys = fieldNames ?? DEFAULT_COLUMNS;
|
|
152
|
+
const columns = columnKeys
|
|
153
|
+
.map((key) => ALL_COLUMNS[key])
|
|
154
|
+
.filter((col) => col !== undefined);
|
|
155
|
+
const header = columns.map((col) => col.header).join(",");
|
|
156
|
+
const rows = issues.map((issue) => columns
|
|
157
|
+
.map((col) => {
|
|
158
|
+
const value = col.extract(issue);
|
|
159
|
+
return value.includes(",") || value.includes('"')
|
|
160
|
+
? `"${value.replace(/"/g, '""')}"`
|
|
161
|
+
: value;
|
|
162
|
+
})
|
|
163
|
+
.join(","));
|
|
164
|
+
return [header, ...rows].join("\n");
|
|
165
|
+
}
|
|
166
|
+
// ── Markdown formatting ────────────────────────────────────────
|
|
167
|
+
const STATUS_MD = {
|
|
168
|
+
triage: (s) => `**${s}**`,
|
|
169
|
+
backlog: (s) => s,
|
|
170
|
+
todo: (s) => s,
|
|
171
|
+
"in progress": (s) => `**${s}**`,
|
|
172
|
+
"in review": (s) => `*${s}*`,
|
|
173
|
+
done: (s) => `~~${s}~~`,
|
|
174
|
+
canceled: (s) => `~~${s}~~`,
|
|
175
|
+
cancelled: (s) => `~~${s}~~`,
|
|
176
|
+
duplicate: (s) => `~~${s}~~`,
|
|
177
|
+
};
|
|
178
|
+
const PRIORITY_MD = {
|
|
179
|
+
0: "—",
|
|
180
|
+
1: "**‼ Urgent**",
|
|
181
|
+
2: "**! High**",
|
|
182
|
+
3: "Medium",
|
|
183
|
+
4: "Low",
|
|
184
|
+
};
|
|
185
|
+
const MD_COLUMNS = {
|
|
186
|
+
identifier: {
|
|
187
|
+
header: "Issue",
|
|
188
|
+
extract: (i) => `[${i.identifier}](${i.url})`,
|
|
189
|
+
},
|
|
190
|
+
title: {
|
|
191
|
+
header: "Title",
|
|
192
|
+
extract: (i) => i.title,
|
|
193
|
+
},
|
|
194
|
+
status: {
|
|
195
|
+
header: "Status",
|
|
196
|
+
extract: (i) => {
|
|
197
|
+
const name = i.state?.name ?? "—";
|
|
198
|
+
const fn = STATUS_MD[name.toLowerCase()];
|
|
199
|
+
return fn ? fn(name) : name;
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
priority: {
|
|
203
|
+
header: "Priority",
|
|
204
|
+
extract: (i) => PRIORITY_MD[i.priority] ?? "—",
|
|
205
|
+
},
|
|
206
|
+
assignee: {
|
|
207
|
+
header: "Assignee",
|
|
208
|
+
extract: (i) => {
|
|
209
|
+
if (!i.assignee) {
|
|
210
|
+
return "—";
|
|
211
|
+
}
|
|
212
|
+
const name = i.assignee.name;
|
|
213
|
+
const atIdx = name.indexOf("@");
|
|
214
|
+
return atIdx > 0 ? name.slice(0, atIdx) : name;
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
project: {
|
|
218
|
+
header: "Project",
|
|
219
|
+
extract: (i) => i.project?.name ?? "—",
|
|
220
|
+
},
|
|
221
|
+
team: {
|
|
222
|
+
header: "Team",
|
|
223
|
+
extract: (i) => i.team?.key ?? "—",
|
|
224
|
+
},
|
|
225
|
+
labels: {
|
|
226
|
+
header: "Labels",
|
|
227
|
+
extract: (i) => i.labels.map((l) => l.name).join(", ") || "—",
|
|
228
|
+
},
|
|
229
|
+
updated: {
|
|
230
|
+
header: "Updated",
|
|
231
|
+
extract: (i) => i.updatedAt.slice(0, 10),
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
function escapeMarkdownCell(str) {
|
|
235
|
+
return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
236
|
+
}
|
|
237
|
+
export function formatMarkdown(issues, fieldNames) {
|
|
238
|
+
const columnKeys = fieldNames ?? DEFAULT_COLUMNS;
|
|
239
|
+
const columns = columnKeys
|
|
240
|
+
.map((key) => MD_COLUMNS[key])
|
|
241
|
+
.filter((col) => col !== undefined);
|
|
242
|
+
if (columns.length === 0) {
|
|
243
|
+
return "No valid columns specified.";
|
|
244
|
+
}
|
|
245
|
+
const header = `| ${columns.map((col) => col.header).join(" | ")} |`;
|
|
246
|
+
const divider = `| ${columns.map((col) => (col.align === "right" ? "---:" : "---")).join(" | ")} |`;
|
|
247
|
+
const rows = issues.map((issue) => `| ${columns.map((col) => escapeMarkdownCell(col.extract(issue))).join(" | ")} |`);
|
|
248
|
+
return [header, divider, ...rows].join("\n");
|
|
249
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { logger } from "./logger.js";
|
|
2
|
+
export function outputUsageInfo(program) {
|
|
3
|
+
const subcommands = [];
|
|
4
|
+
function collectSubcommands(cmd, prefix = "") {
|
|
5
|
+
const currentName = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
|
|
6
|
+
const commands = cmd.commands;
|
|
7
|
+
if (commands.length === 0) {
|
|
8
|
+
if (prefix) {
|
|
9
|
+
subcommands.push({ name: currentName, command: cmd });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
for (const subcmd of commands) {
|
|
14
|
+
collectSubcommands(subcmd, currentName);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
collectSubcommands(program);
|
|
19
|
+
subcommands.sort((a, b) => a.name.localeCompare(b.name));
|
|
20
|
+
for (const { command } of subcommands) {
|
|
21
|
+
command.outputHelp();
|
|
22
|
+
logger.info("\n---\n");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2
|
+
const UUID_PREFIX_REGEX = /^[0-9a-f]{8}$/i;
|
|
3
|
+
export function isUuid(value) {
|
|
4
|
+
return UUID_REGEX.test(value);
|
|
5
|
+
}
|
|
6
|
+
export function isUuidPrefix(value) {
|
|
7
|
+
return UUID_PREFIX_REGEX.test(value);
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LinearService } from "./linear-service.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve each candidate identifier to its UUID. Returns a map containing only
|
|
4
|
+
* identifiers that resolved successfully — non-existent IDs (e.g. "ISO-1424"
|
|
5
|
+
* false positives, or refs to issues outside the workspace) are dropped silently.
|
|
6
|
+
*
|
|
7
|
+
* Resolutions run in parallel for throughput. The map preserves the order of
|
|
8
|
+
* the input array via insertion order on the underlying Map.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateReferences(identifiers: readonly string[], linearService: LinearService): Promise<Map<string, string>>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve each candidate identifier to its UUID. Returns a map containing only
|
|
3
|
+
* identifiers that resolved successfully — non-existent IDs (e.g. "ISO-1424"
|
|
4
|
+
* false positives, or refs to issues outside the workspace) are dropped silently.
|
|
5
|
+
*
|
|
6
|
+
* Resolutions run in parallel for throughput. The map preserves the order of
|
|
7
|
+
* the input array via insertion order on the underlying Map.
|
|
8
|
+
*/
|
|
9
|
+
export async function validateReferences(identifiers, linearService) {
|
|
10
|
+
const valid = new Map();
|
|
11
|
+
if (identifiers.length === 0) {
|
|
12
|
+
return valid;
|
|
13
|
+
}
|
|
14
|
+
// Deduplicate while preserving order.
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
const unique = identifiers.filter((id) => {
|
|
17
|
+
if (seen.has(id)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
seen.add(id);
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
const settled = await Promise.allSettled(unique.map(async (id) => ({
|
|
24
|
+
id,
|
|
25
|
+
uuid: await linearService.resolveIssueId(id),
|
|
26
|
+
})));
|
|
27
|
+
for (const result of settled) {
|
|
28
|
+
if (result.status === "fulfilled") {
|
|
29
|
+
valid.set(result.value.id, result.value.uuid);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return valid;
|
|
33
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function parsePositiveInt(value: string, flagName: string): number;
|
|
2
|
+
export declare function validatePriority(value: string): number;
|
|
3
|
+
export declare function validateHexColor(value: string): string;
|
|
4
|
+
export declare function validateIsoDate(value: string): string;
|
|
5
|
+
export declare function splitList(value: string): string[];
|
|
6
|
+
export declare function parsePriorityFilter(value: string): number[];
|
|
7
|
+
export declare const PRIORITY_LABELS: Record<number, string>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { invalidParameterError } from "./error-messages.js";
|
|
2
|
+
const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
|
3
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4
|
+
export function parsePositiveInt(value, flagName) {
|
|
5
|
+
const n = Number.parseInt(value, 10);
|
|
6
|
+
if (Number.isNaN(n) || n < 1) {
|
|
7
|
+
throw invalidParameterError(flagName, `"${value}" is not a positive integer`);
|
|
8
|
+
}
|
|
9
|
+
return n;
|
|
10
|
+
}
|
|
11
|
+
export function validatePriority(value) {
|
|
12
|
+
const asName = PRIORITY_NAMES[value.toLowerCase()];
|
|
13
|
+
if (asName !== undefined && asName >= 1) {
|
|
14
|
+
return asName;
|
|
15
|
+
}
|
|
16
|
+
const n = Number.parseInt(value, 10);
|
|
17
|
+
if (Number.isNaN(n) || n < 1 || n > 4) {
|
|
18
|
+
throw invalidParameterError("--priority", `"${value}" is not valid. Use names (urgent, high, medium/normal, low) or numbers (1-4).`);
|
|
19
|
+
}
|
|
20
|
+
return n;
|
|
21
|
+
}
|
|
22
|
+
export function validateHexColor(value) {
|
|
23
|
+
if (!HEX_COLOR_RE.test(value)) {
|
|
24
|
+
throw invalidParameterError("--color", `"${value}" is not a valid hex color. Use #RRGGBB format (e.g. #e06666).`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
export function validateIsoDate(value) {
|
|
29
|
+
if (!ISO_DATE_RE.test(value)) {
|
|
30
|
+
throw invalidParameterError("--target-date", `"${value}" is not a valid date. Use YYYY-MM-DD format.`);
|
|
31
|
+
}
|
|
32
|
+
const date = new Date(value);
|
|
33
|
+
if (Number.isNaN(date.getTime())) {
|
|
34
|
+
throw invalidParameterError("--target-date", `"${value}" is not a valid date.`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
export function splitList(value) {
|
|
39
|
+
return value
|
|
40
|
+
.split(",")
|
|
41
|
+
.map((s) => s.trim())
|
|
42
|
+
.filter((s) => s.length > 0);
|
|
43
|
+
}
|
|
44
|
+
const PRIORITY_NAMES = {
|
|
45
|
+
none: 0,
|
|
46
|
+
urgent: 1,
|
|
47
|
+
high: 2,
|
|
48
|
+
medium: 3,
|
|
49
|
+
normal: 3,
|
|
50
|
+
low: 4,
|
|
51
|
+
};
|
|
52
|
+
export function parsePriorityFilter(value) {
|
|
53
|
+
return splitList(value).map((item) => {
|
|
54
|
+
const asName = PRIORITY_NAMES[item.toLowerCase()];
|
|
55
|
+
if (asName !== undefined) {
|
|
56
|
+
return asName;
|
|
57
|
+
}
|
|
58
|
+
const n = Number.parseInt(item, 10);
|
|
59
|
+
if (Number.isNaN(n) || n < 0 || n > 4) {
|
|
60
|
+
throw invalidParameterError("--priority", `"${item}" is not valid. Use names (urgent, high, medium, low, none) or numbers (0-4).`);
|
|
61
|
+
}
|
|
62
|
+
return n;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export const PRIORITY_LABELS = {
|
|
66
|
+
0: "No priority",
|
|
67
|
+
1: "Urgent",
|
|
68
|
+
2: "High",
|
|
69
|
+
3: "Medium",
|
|
70
|
+
4: "Low",
|
|
71
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GraphQLService } from "./graphql-service.js";
|
|
2
|
+
export declare function getWorkspaceUrlKey(graphQLService: GraphQLService): Promise<string>;
|
|
3
|
+
/** Test helper: clear the cached URL key. Not called from production code. */
|
|
4
|
+
export declare function _resetWorkspaceUrlKeyCache(): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { loadConfig } from "../config/config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the Linear workspace URL key (the segment after `linear.app/` in
|
|
4
|
+
* issue URLs). Used to build canonical markdown link URLs like
|
|
5
|
+
* `https://linear.app/<urlKey>/issue/<identifier>/`.
|
|
6
|
+
*
|
|
7
|
+
* Resolution order:
|
|
8
|
+
* 1. `config.workspaceUrlKey` if explicitly set in el-linear config
|
|
9
|
+
* 2. `viewer.organization.urlKey` from the Linear API (fetched once, cached)
|
|
10
|
+
*
|
|
11
|
+
* Cached in-process for the lifetime of the CLI invocation.
|
|
12
|
+
*/
|
|
13
|
+
const VIEWER_ORG_URL_KEY_QUERY = /* GraphQL */ `
|
|
14
|
+
query ViewerOrgUrlKey {
|
|
15
|
+
viewer {
|
|
16
|
+
organization {
|
|
17
|
+
urlKey
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
let cachedUrlKey;
|
|
23
|
+
export async function getWorkspaceUrlKey(graphQLService) {
|
|
24
|
+
if (cachedUrlKey) {
|
|
25
|
+
return cachedUrlKey;
|
|
26
|
+
}
|
|
27
|
+
const { workspaceUrlKey } = loadConfig();
|
|
28
|
+
if (workspaceUrlKey) {
|
|
29
|
+
cachedUrlKey = workspaceUrlKey;
|
|
30
|
+
return workspaceUrlKey;
|
|
31
|
+
}
|
|
32
|
+
const data = await graphQLService.rawRequest(VIEWER_ORG_URL_KEY_QUERY);
|
|
33
|
+
const fetched = data?.viewer?.organization?.urlKey;
|
|
34
|
+
if (!fetched) {
|
|
35
|
+
throw new Error("Could not resolve Linear workspace URL key from `viewer.organization.urlKey`. " +
|
|
36
|
+
"Set `workspaceUrlKey` in your el-linear config to override.");
|
|
37
|
+
}
|
|
38
|
+
cachedUrlKey = fetched;
|
|
39
|
+
return fetched;
|
|
40
|
+
}
|
|
41
|
+
/** Test helper: clear the cached URL key. Not called from production code. */
|
|
42
|
+
export function _resetWorkspaceUrlKeyCache() {
|
|
43
|
+
cachedUrlKey = undefined;
|
|
44
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@enrichlayer/el-linear",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "A pragmatic CLI for Linear.app — deterministic team/label/member resolution, structured issue validation, configurable term enforcement, and a GraphQL escape hatch.",
|
|
5
|
+
"main": "dist/main.js",
|
|
6
|
+
"types": "dist/main.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/main.d.ts",
|
|
11
|
+
"import": "./dist/main.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"bin": {
|
|
16
|
+
"el-linear": "dist/main.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/",
|
|
20
|
+
"claude-skills/",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22.0.0"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/enrichlayer/el-linear.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://enrichlayer.com/tools/el-linear",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/enrichlayer/el-linear/issues"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"linear",
|
|
38
|
+
"linear.app",
|
|
39
|
+
"cli",
|
|
40
|
+
"issue-tracker",
|
|
41
|
+
"graphql",
|
|
42
|
+
"claude",
|
|
43
|
+
"claude-code"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@inquirer/prompts": "^8.4.2",
|
|
50
|
+
"@linear/sdk": "^83.0.0",
|
|
51
|
+
"commander": "^14.0.0",
|
|
52
|
+
"picocolors": "^1.1.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@biomejs/biome": "^2.4.0",
|
|
56
|
+
"@types/node": "^25.0.0",
|
|
57
|
+
"graphql": "^16.0.0",
|
|
58
|
+
"tsx": "^4.20.5",
|
|
59
|
+
"typescript": "^5.0.0",
|
|
60
|
+
"vitest": "^4.0.18"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsc && chmod +x dist/main.js",
|
|
64
|
+
"clean": "rm -rf dist/",
|
|
65
|
+
"lint": "biome check .",
|
|
66
|
+
"lint:fix": "biome check --fix .",
|
|
67
|
+
"start": "tsx src/main.ts",
|
|
68
|
+
"test": "vitest run",
|
|
69
|
+
"test:watch": "vitest"
|
|
70
|
+
}
|
|
71
|
+
}
|