@enrichlayer/el-linear 1.9.0 → 1.15.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/README.md +139 -10
- package/claude-skills/linear-operations/SKILL.md +41 -1
- package/dist/auth/linear-credential.d.ts +27 -0
- package/dist/auth/linear-credential.js +1 -0
- package/dist/auth/oauth-app-config.d.ts +4 -3
- package/dist/auth/oauth-app-config.js +13 -2
- package/dist/auth/oauth-callback.d.ts +2 -3
- package/dist/auth/oauth-callback.js +2 -2
- package/dist/auth/oauth-client.d.ts +8 -2
- package/dist/auth/oauth-client.js +26 -0
- package/dist/auth/oauth-fs.d.ts +2 -1
- package/dist/auth/oauth-headless.d.ts +2 -1
- package/dist/auth/oauth-storage.d.ts +5 -1
- package/dist/auth/oauth-storage.js +1 -1
- package/dist/auth/oauth-token.d.ts +4 -3
- package/dist/auth/oauth-token.js +16 -4
- package/dist/auth/token-resolver.d.ts +14 -5
- package/dist/auth/token-resolver.js +6 -1
- package/dist/commands/attachments.js +2 -1
- package/dist/commands/batch.js +18 -21
- package/dist/commands/comments.js +22 -33
- package/dist/commands/config.js +178 -5
- package/dist/commands/cycles.js +2 -1
- package/dist/commands/documents.js +2 -1
- package/dist/commands/graphql.js +4 -6
- package/dist/commands/init/aliases.js +1 -1
- package/dist/commands/init/defaults.d.ts +2 -1
- package/dist/commands/init/index.js +45 -35
- package/dist/commands/init/oauth.d.ts +4 -1
- package/dist/commands/init/oauth.js +22 -4
- package/dist/commands/init/shared.d.ts +24 -2
- package/dist/commands/init/shared.js +35 -4
- package/dist/commands/init/token.d.ts +3 -3
- package/dist/commands/init/token.js +5 -24
- package/dist/commands/init/workspace.d.ts +2 -1
- package/dist/commands/init/workspace.js +1 -1
- package/dist/commands/introspect.d.ts +27 -0
- package/dist/commands/introspect.js +178 -0
- package/dist/commands/issue-id.js +1 -3
- package/dist/commands/issues/branch.js +9 -1
- package/dist/commands/issues/description.js +2 -6
- package/dist/commands/issues/link-references.d.ts +21 -0
- package/dist/commands/issues/link-references.js +171 -0
- package/dist/commands/issues/relations.d.ts +44 -0
- package/dist/commands/issues/relations.js +132 -0
- package/dist/commands/issues.js +269 -309
- package/dist/commands/labels.js +15 -24
- package/dist/commands/profile.js +1 -0
- package/dist/commands/project-milestones.js +13 -20
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +157 -44
- package/dist/commands/read-shortcut.d.ts +1 -1
- package/dist/commands/read-shortcut.js +28 -8
- package/dist/commands/refs.js +75 -8
- package/dist/commands/releases.js +26 -30
- package/dist/commands/search.js +49 -33
- package/dist/commands/teams.js +2 -1
- package/dist/commands/templates.js +9 -14
- package/dist/commands/users.js +5 -2
- package/dist/config/config.d.ts +99 -1
- package/dist/config/config.js +264 -52
- package/dist/config/error-enrichment.d.ts +62 -0
- package/dist/config/error-enrichment.js +417 -0
- package/dist/config/issue-validation.d.ts +37 -0
- package/dist/config/issue-validation.js +63 -1
- package/dist/config/paths.d.ts +2 -8
- package/dist/config/paths.js +4 -2
- package/dist/config/resolver.d.ts +8 -1
- package/dist/config/resolver.js +11 -5
- package/dist/main.js +13 -1
- package/dist/queries/attachments-types.d.ts +30 -0
- package/dist/queries/attachments-types.js +5 -0
- package/dist/queries/comments-types.d.ts +55 -0
- package/dist/queries/comments-types.js +5 -0
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +8 -0
- package/dist/queries/documents-types.d.ts +62 -0
- package/dist/queries/documents-types.js +9 -0
- package/dist/queries/introspect-types.d.ts +58 -0
- package/dist/queries/introspect-types.js +10 -0
- package/dist/queries/issues-types.d.ts +481 -0
- package/dist/queries/issues-types.js +23 -0
- package/dist/queries/issues.d.ts +51 -10
- package/dist/queries/issues.js +147 -5
- package/dist/queries/labels-types.d.ts +65 -0
- package/dist/queries/labels-types.js +5 -0
- package/dist/queries/project-milestones-types.d.ts +92 -0
- package/dist/queries/project-milestones-types.js +10 -0
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/queries/projects-types.d.ts +76 -0
- package/dist/queries/projects-types.js +5 -0
- package/dist/queries/projects.d.ts +2 -0
- package/dist/queries/projects.js +22 -0
- package/dist/queries/releases-types.d.ts +85 -0
- package/dist/queries/releases-types.js +5 -0
- package/dist/queries/search-types.d.ts +102 -0
- package/dist/queries/search-types.js +6 -0
- package/dist/queries/templates-types.d.ts +62 -0
- package/dist/queries/templates-types.js +9 -0
- package/dist/types/linear.d.ts +21 -3
- package/dist/utils/auto-link-references.d.ts +3 -3
- package/dist/utils/auto-link-references.js +30 -34
- package/dist/utils/extract-field.d.ts +19 -0
- package/dist/utils/extract-field.js +99 -0
- package/dist/utils/file-service.d.ts +6 -13
- package/dist/utils/file-service.js +0 -2
- package/dist/utils/formatters/summary.js +6 -1
- package/dist/utils/graphql-attachments-service.js +6 -9
- package/dist/utils/graphql-documents-service.js +19 -25
- package/dist/utils/graphql-issues-service.d.ts +112 -46
- package/dist/utils/graphql-issues-service.js +398 -206
- package/dist/utils/graphql-service.d.ts +10 -12
- package/dist/utils/graphql-service.js +0 -3
- package/dist/utils/issue-reference-extractor.d.ts +7 -0
- package/dist/utils/issue-reference-extractor.js +5 -3
- package/dist/utils/issues-service-bootstrap.d.ts +28 -0
- package/dist/utils/issues-service-bootstrap.js +27 -0
- package/dist/utils/linear-service.d.ts +21 -14
- package/dist/utils/linear-service.js +73 -11
- package/dist/utils/markdown-prosemirror.js +12 -12
- package/dist/utils/mention-resolver.js +1 -1
- package/dist/utils/output.d.ts +82 -2
- package/dist/utils/output.js +76 -11
- package/dist/utils/project-slug.d.ts +21 -0
- package/dist/utils/project-slug.js +45 -0
- package/dist/utils/protected-ranges.d.ts +14 -0
- package/dist/utils/protected-ranges.js +88 -2
- package/dist/utils/sanitize-for-log.d.ts +24 -0
- package/dist/utils/sanitize-for-log.js +38 -0
- package/dist/utils/table-formatter.js +24 -0
- package/dist/utils/validators.d.ts +7 -2
- package/dist/utils/validators.js +6 -0
- package/dist/utils/workspace-url.d.ts +5 -1
- package/dist/utils/workspace-url.js +53 -7
- package/package.json +2 -2
|
@@ -1,20 +1,18 @@
|
|
|
1
|
+
import type { LinearCredential } from "../auth/linear-credential.js";
|
|
1
2
|
import type { GraphQLResponseData, GraphQLVariables } from "../types/linear.js";
|
|
2
3
|
import type { AuthOptions } from "./auth.js";
|
|
3
4
|
/**
|
|
4
|
-
* Constructor arg
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Constructor arg for `GraphQLService`. Re-exported alias of the shared
|
|
6
|
+
* `LinearCredential` union (`{ apiKey } | { oauthToken }`). Kept as a
|
|
7
|
+
* named local export so call sites that already import
|
|
8
|
+
* `GraphQLServiceAuth` keep compiling — the alias collapses through to
|
|
9
|
+
* the shared shape.
|
|
9
10
|
*
|
|
10
|
-
* The string
|
|
11
|
-
*
|
|
11
|
+
* The bare-string legacy arm was dropped in DEV-4068 T7. Tests now
|
|
12
|
+
* construct with `{ apiKey: "test-token" }` (mechanical rewrite, no
|
|
13
|
+
* semantic change — same `Authorization: <token>` header is emitted).
|
|
12
14
|
*/
|
|
13
|
-
export type GraphQLServiceAuth =
|
|
14
|
-
apiKey: string;
|
|
15
|
-
} | {
|
|
16
|
-
oauthToken: string;
|
|
17
|
-
};
|
|
15
|
+
export type GraphQLServiceAuth = LinearCredential;
|
|
18
16
|
export declare class GraphQLService {
|
|
19
17
|
private readonly graphQLClient;
|
|
20
18
|
constructor(auth: GraphQLServiceAuth);
|
|
@@ -2,9 +2,6 @@ import { LinearClient } from "@linear/sdk";
|
|
|
2
2
|
import { getActiveAuth } from "../auth/token-resolver.js";
|
|
3
3
|
function buildLinearClient(auth) {
|
|
4
4
|
const baseHeaders = { "public-file-urls-expire-in": "3600" };
|
|
5
|
-
if (typeof auth === "string") {
|
|
6
|
-
return new LinearClient({ apiKey: auth, headers: baseHeaders });
|
|
7
|
-
}
|
|
8
5
|
if ("oauthToken" in auth) {
|
|
9
6
|
// Linear's SDK natively supports OAuth via the `accessToken` option,
|
|
10
7
|
// which causes the underlying graphql-request client to send
|
|
@@ -5,6 +5,13 @@ export interface IssueReference {
|
|
|
5
5
|
reverse: boolean;
|
|
6
6
|
type: IssueRelationType;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Specificity ranking — used when the same identifier appears more than once
|
|
10
|
+
* in the text with different qualifiers. The strongest non-default inference
|
|
11
|
+
* wins. Exported because `auto-link-references` does the same merge over its
|
|
12
|
+
* own description+comments fan-in (DEV-4070 deduplication).
|
|
13
|
+
*/
|
|
14
|
+
export declare function specificity(ref: IssueReference): number;
|
|
8
15
|
/**
|
|
9
16
|
* Extract issue identifiers from text along with the inferred relation type.
|
|
10
17
|
*
|
|
@@ -45,10 +45,12 @@ function inferRelation(textBefore) {
|
|
|
45
45
|
return { type: "related", reverse: false };
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
* Specificity ranking — used when the same identifier appears more than once
|
|
49
|
-
* with different qualifiers. The strongest non-default inference
|
|
48
|
+
* Specificity ranking — used when the same identifier appears more than once
|
|
49
|
+
* in the text with different qualifiers. The strongest non-default inference
|
|
50
|
+
* wins. Exported because `auto-link-references` does the same merge over its
|
|
51
|
+
* own description+comments fan-in (DEV-4070 deduplication).
|
|
50
52
|
*/
|
|
51
|
-
function specificity(ref) {
|
|
53
|
+
export function specificity(ref) {
|
|
52
54
|
if (ref.type === "duplicate") {
|
|
53
55
|
return 3;
|
|
54
56
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service-trio bootstrap helper.
|
|
3
|
+
*
|
|
4
|
+
* The 3-line incantation
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const graphQLService = await createGraphQLService(rootOpts);
|
|
8
|
+
* const linearService = await createLinearService(rootOpts);
|
|
9
|
+
* const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* repeats verbatim across ~13 issue/batch command handlers. This helper
|
|
13
|
+
* collapses it to a single line; callers destructure only what they use.
|
|
14
|
+
*
|
|
15
|
+
* Sites that need just `graphQLService` + `linearService` (no `issuesService`)
|
|
16
|
+
* can keep their direct calls — there's no benefit to routing through a
|
|
17
|
+
* helper that constructs an unused third service.
|
|
18
|
+
*/
|
|
19
|
+
import type { AuthOptions } from "./auth.js";
|
|
20
|
+
import { GraphQLIssuesService } from "./graphql-issues-service.js";
|
|
21
|
+
import { type GraphQLService } from "./graphql-service.js";
|
|
22
|
+
import { type LinearService } from "./linear-service.js";
|
|
23
|
+
export interface IssuesServiceTrio {
|
|
24
|
+
graphQLService: GraphQLService;
|
|
25
|
+
linearService: LinearService;
|
|
26
|
+
issuesService: GraphQLIssuesService;
|
|
27
|
+
}
|
|
28
|
+
export declare function createIssuesService(options: AuthOptions): Promise<IssuesServiceTrio>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service-trio bootstrap helper.
|
|
3
|
+
*
|
|
4
|
+
* The 3-line incantation
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const graphQLService = await createGraphQLService(rootOpts);
|
|
8
|
+
* const linearService = await createLinearService(rootOpts);
|
|
9
|
+
* const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* repeats verbatim across ~13 issue/batch command handlers. This helper
|
|
13
|
+
* collapses it to a single line; callers destructure only what they use.
|
|
14
|
+
*
|
|
15
|
+
* Sites that need just `graphQLService` + `linearService` (no `issuesService`)
|
|
16
|
+
* can keep their direct calls — there's no benefit to routing through a
|
|
17
|
+
* helper that constructs an unused third service.
|
|
18
|
+
*/
|
|
19
|
+
import { GraphQLIssuesService } from "./graphql-issues-service.js";
|
|
20
|
+
import { createGraphQLService, } from "./graphql-service.js";
|
|
21
|
+
import { createLinearService } from "./linear-service.js";
|
|
22
|
+
export async function createIssuesService(options) {
|
|
23
|
+
const graphQLService = await createGraphQLService(options);
|
|
24
|
+
const linearService = await createLinearService(options);
|
|
25
|
+
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
26
|
+
return { graphQLService, linearService, issuesService };
|
|
27
|
+
}
|
|
@@ -1,20 +1,13 @@
|
|
|
1
|
+
import type { LinearCredential } from "../auth/linear-credential.js";
|
|
1
2
|
import type { LinearComment, LinearCycleDetail, LinearCycleSummary, LinearLabel, LinearProject, LinearTeam, LinearUser } from "../types/linear.js";
|
|
2
3
|
import type { AuthOptions } from "./auth.js";
|
|
3
4
|
/**
|
|
4
|
-
* Constructor arg
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `Authorization: Bearer <token>` via the SDK's accessToken option).
|
|
9
|
-
*
|
|
10
|
-
* The string variant exists because hundreds of call sites and tests pass
|
|
11
|
-
* a plain string. We continue to support it indefinitely.
|
|
5
|
+
* Constructor arg for `LinearService`. Re-exported alias of the shared
|
|
6
|
+
* `LinearCredential` union (`{ apiKey } | { oauthToken }`). See
|
|
7
|
+
* `src/auth/linear-credential.ts` for the contract. The bare-string
|
|
8
|
+
* legacy arm was dropped in DEV-4068 T7.
|
|
12
9
|
*/
|
|
13
|
-
export type LinearServiceAuth =
|
|
14
|
-
apiKey: string;
|
|
15
|
-
} | {
|
|
16
|
-
oauthToken: string;
|
|
17
|
-
};
|
|
10
|
+
export type LinearServiceAuth = LinearCredential;
|
|
18
11
|
export declare class LinearService {
|
|
19
12
|
private readonly client;
|
|
20
13
|
constructor(auth: LinearServiceAuth);
|
|
@@ -26,6 +19,8 @@ export declare class LinearService {
|
|
|
26
19
|
nameFilter?: string;
|
|
27
20
|
states?: string[];
|
|
28
21
|
excludeStates?: string[];
|
|
22
|
+
/** Resolved team UUID — applied as a server-side filter before pagination. */
|
|
23
|
+
teamId?: string;
|
|
29
24
|
}): Promise<LinearProject[]>;
|
|
30
25
|
resolveTeamId(teamKeyOrNameOrId: string): Promise<string>;
|
|
31
26
|
resolveStatusId(statusName: string, teamId?: string): Promise<string>;
|
|
@@ -40,6 +35,18 @@ export declare class LinearService {
|
|
|
40
35
|
getCycles(teamFilter?: string, activeOnly?: boolean, limit?: number): Promise<LinearCycleSummary[]>;
|
|
41
36
|
getCycleById(cycleId: string, issuesLimit?: number): Promise<LinearCycleDetail>;
|
|
42
37
|
resolveCycleId(cycleNameOrId: string, teamFilter?: string): Promise<string>;
|
|
43
|
-
resolveProjectId(
|
|
38
|
+
resolveProjectId(projectInput: string): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Normalize a user-supplied project input to a UUID when the input is
|
|
41
|
+
* a URL or slug-id form. Pass-through for UUIDs and plain names — the
|
|
42
|
+
* latter stays a name so downstream batch-resolve queries can fold the
|
|
43
|
+
* lookup into their single round-trip.
|
|
44
|
+
*
|
|
45
|
+
* Used by callers that route project resolution through a separate
|
|
46
|
+
* batch-resolve step (e.g. `GraphqlIssuesService.createIssue`) — they
|
|
47
|
+
* can pre-normalize URL/slug inputs to UUIDs so the batch query's
|
|
48
|
+
* `name eqIgnoreCase` filter doesn't have to learn the URL/slug shape.
|
|
49
|
+
*/
|
|
50
|
+
normalizeProjectInput(projectInput: string): Promise<string>;
|
|
44
51
|
}
|
|
45
52
|
export declare function createLinearService(options: AuthOptions): Promise<LinearService>;
|
|
@@ -4,6 +4,7 @@ import { resolveUserDisplayName } from "../config/resolver.js";
|
|
|
4
4
|
import { toISOStringOrNow, toISOStringOrUndefined } from "./date-format.js";
|
|
5
5
|
import { multipleMatchesError, notFoundError } from "./error-messages.js";
|
|
6
6
|
import { parseIssueIdentifier } from "./identifier-parser.js";
|
|
7
|
+
import { parseProjectSlugId } from "./project-slug.js";
|
|
7
8
|
import { isUuid } from "./uuid.js";
|
|
8
9
|
const DEFAULT_CYCLE_PAGINATION_LIMIT = 250;
|
|
9
10
|
// The Linear SDK types don't accept string orderBy values, but the API does.
|
|
@@ -22,9 +23,6 @@ function nonEmptyFilter(filter) {
|
|
|
22
23
|
return Object.keys(filter).length > 0 ? filter : undefined;
|
|
23
24
|
}
|
|
24
25
|
function buildLinearClient(auth) {
|
|
25
|
-
if (typeof auth === "string") {
|
|
26
|
-
return new LinearClient({ apiKey: auth });
|
|
27
|
-
}
|
|
28
26
|
if ("oauthToken" in auth) {
|
|
29
27
|
// Linear's SDK natively supports OAuth via the `accessToken` option,
|
|
30
28
|
// which causes the underlying transport to send
|
|
@@ -133,13 +131,28 @@ export class LinearService {
|
|
|
133
131
|
else if (options.excludeStates && options.excludeStates.length > 0) {
|
|
134
132
|
filter.state = { nin: options.excludeStates };
|
|
135
133
|
}
|
|
136
|
-
|
|
134
|
+
if (options.teamId) {
|
|
135
|
+
filter.teams = { some: { id: { eq: options.teamId } } };
|
|
136
|
+
}
|
|
137
|
+
// `limit === 0` means "no limit" — paginate the full result set.
|
|
138
|
+
// Otherwise a single page of `limit` projects. DEV-4175: `--all` /
|
|
139
|
+
// `--limit 0` must return every project so callers never make a false
|
|
140
|
+
// "does not exist" determination off a silently truncated page.
|
|
141
|
+
const unlimited = limit === 0;
|
|
142
|
+
let page = await this.client.projects({
|
|
137
143
|
filter: nonEmptyFilter(filter),
|
|
138
|
-
first: limit,
|
|
144
|
+
first: unlimited ? 250 : limit,
|
|
139
145
|
orderBy: sdkOrderBy("updatedAt"),
|
|
140
146
|
includeArchived: false,
|
|
141
147
|
});
|
|
142
|
-
const
|
|
148
|
+
const projectNodes = [...page.nodes];
|
|
149
|
+
if (unlimited) {
|
|
150
|
+
while (page.pageInfo.hasNextPage) {
|
|
151
|
+
page = await page.fetchNext();
|
|
152
|
+
projectNodes.push(...page.nodes);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const projectsWithData = await Promise.all(projectNodes.map(async (project) => {
|
|
143
156
|
const [teams, lead] = await Promise.all([
|
|
144
157
|
project.teams(),
|
|
145
158
|
project.lead,
|
|
@@ -373,6 +386,9 @@ export class LinearService {
|
|
|
373
386
|
url: issue.url,
|
|
374
387
|
title: issue.title,
|
|
375
388
|
description: issue.description || undefined,
|
|
389
|
+
// Linear SDK types `priority` as `number`; the GraphQL schema only
|
|
390
|
+
// emits 0-4, so the cast is safe and the runtime range is
|
|
391
|
+
// guaranteed by Linear's server-side schema.
|
|
376
392
|
priority: issue.priority,
|
|
377
393
|
estimate: issue.estimate || undefined,
|
|
378
394
|
state: state ? { id: state.id, name: state.name } : undefined,
|
|
@@ -455,20 +471,66 @@ export class LinearService {
|
|
|
455
471
|
}
|
|
456
472
|
return chosen.id;
|
|
457
473
|
}
|
|
458
|
-
async resolveProjectId(
|
|
459
|
-
if (isUuid(
|
|
460
|
-
return
|
|
474
|
+
async resolveProjectId(projectInput) {
|
|
475
|
+
if (isUuid(projectInput)) {
|
|
476
|
+
return projectInput;
|
|
477
|
+
}
|
|
478
|
+
const slugId = parseProjectSlugId(projectInput);
|
|
479
|
+
if (slugId) {
|
|
480
|
+
// `as Record<string, unknown>` — `@linear/sdk`'s typed ProjectFilter
|
|
481
|
+
// lags Linear's schema and doesn't yet expose `slugId`. The field is
|
|
482
|
+
// present on the server; the cast is here until the SDK ships the
|
|
483
|
+
// typing. Don't "clean up" without verifying the typed shape exists.
|
|
484
|
+
//
|
|
485
|
+
// `includeArchived: true` — URLs in the wild often point at archived
|
|
486
|
+
// projects (someone digs up an old link); silently 404-ing them is
|
|
487
|
+
// worse than resolving to an archived project, which the caller can
|
|
488
|
+
// inspect via the returned UUID. Name resolution stays active-only
|
|
489
|
+
// because names collide more readily than slug-ids do.
|
|
490
|
+
const bySlug = await this.client.projects({
|
|
491
|
+
filter: { slugId: { eq: slugId } },
|
|
492
|
+
first: 1,
|
|
493
|
+
includeArchived: true,
|
|
494
|
+
});
|
|
495
|
+
if (bySlug.nodes.length > 0) {
|
|
496
|
+
return bySlug.nodes[0].id;
|
|
497
|
+
}
|
|
498
|
+
// Slug-id form was syntactically valid but didn't match a project
|
|
499
|
+
// — fall through to name resolution would be misleading (the user
|
|
500
|
+
// clearly pasted a URL/slug, not a name). Throw the same shape of
|
|
501
|
+
// not-found error as the name path.
|
|
502
|
+
throw notFoundError("Project", projectInput);
|
|
461
503
|
}
|
|
462
|
-
const filter = { name: { eqIgnoreCase:
|
|
504
|
+
const filter = { name: { eqIgnoreCase: projectInput } };
|
|
463
505
|
const projectsConnection = await this.client.projects({
|
|
464
506
|
filter,
|
|
465
507
|
first: 1,
|
|
466
508
|
});
|
|
467
509
|
if (projectsConnection.nodes.length === 0) {
|
|
468
|
-
throw notFoundError("Project",
|
|
510
|
+
throw notFoundError("Project", projectInput);
|
|
469
511
|
}
|
|
470
512
|
return projectsConnection.nodes[0].id;
|
|
471
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Normalize a user-supplied project input to a UUID when the input is
|
|
516
|
+
* a URL or slug-id form. Pass-through for UUIDs and plain names — the
|
|
517
|
+
* latter stays a name so downstream batch-resolve queries can fold the
|
|
518
|
+
* lookup into their single round-trip.
|
|
519
|
+
*
|
|
520
|
+
* Used by callers that route project resolution through a separate
|
|
521
|
+
* batch-resolve step (e.g. `GraphqlIssuesService.createIssue`) — they
|
|
522
|
+
* can pre-normalize URL/slug inputs to UUIDs so the batch query's
|
|
523
|
+
* `name eqIgnoreCase` filter doesn't have to learn the URL/slug shape.
|
|
524
|
+
*/
|
|
525
|
+
async normalizeProjectInput(projectInput) {
|
|
526
|
+
if (isUuid(projectInput)) {
|
|
527
|
+
return projectInput;
|
|
528
|
+
}
|
|
529
|
+
if (parseProjectSlugId(projectInput)) {
|
|
530
|
+
return this.resolveProjectId(projectInput);
|
|
531
|
+
}
|
|
532
|
+
return projectInput;
|
|
533
|
+
}
|
|
472
534
|
}
|
|
473
535
|
export async function createLinearService(options) {
|
|
474
536
|
const auth = await getActiveAuth(options);
|
|
@@ -88,7 +88,7 @@ function parseFencedCodeBlock(state, line) {
|
|
|
88
88
|
attrs.language = language;
|
|
89
89
|
}
|
|
90
90
|
state.content.push({
|
|
91
|
-
type: "
|
|
91
|
+
type: "code_block",
|
|
92
92
|
...(Object.keys(attrs).length > 0 ? { attrs } : {}),
|
|
93
93
|
content: codeLines.length > 0
|
|
94
94
|
? [{ type: "text", text: codeLines.join("\n") }]
|
|
@@ -100,7 +100,7 @@ function parseHorizontalRule(state, line) {
|
|
|
100
100
|
if (!HR_RE.test(line)) {
|
|
101
101
|
return false;
|
|
102
102
|
}
|
|
103
|
-
state.content.push({ type: "
|
|
103
|
+
state.content.push({ type: "horizontal_rule" });
|
|
104
104
|
state.i++;
|
|
105
105
|
return true;
|
|
106
106
|
}
|
|
@@ -128,12 +128,12 @@ function parseBulletList(state, line) {
|
|
|
128
128
|
break;
|
|
129
129
|
}
|
|
130
130
|
items.push({
|
|
131
|
-
type: "
|
|
131
|
+
type: "list_item",
|
|
132
132
|
content: [{ type: "paragraph", content: parseInline(m[1]) }],
|
|
133
133
|
});
|
|
134
134
|
state.i++;
|
|
135
135
|
}
|
|
136
|
-
state.content.push({ type: "
|
|
136
|
+
state.content.push({ type: "bullet_list", content: items });
|
|
137
137
|
return true;
|
|
138
138
|
}
|
|
139
139
|
function parseOrderedList(state, line) {
|
|
@@ -147,12 +147,12 @@ function parseOrderedList(state, line) {
|
|
|
147
147
|
break;
|
|
148
148
|
}
|
|
149
149
|
items.push({
|
|
150
|
-
type: "
|
|
150
|
+
type: "list_item",
|
|
151
151
|
content: [{ type: "paragraph", content: parseInline(m[1]) }],
|
|
152
152
|
});
|
|
153
153
|
state.i++;
|
|
154
154
|
}
|
|
155
|
-
state.content.push({ type: "
|
|
155
|
+
state.content.push({ type: "ordered_list", content: items });
|
|
156
156
|
return true;
|
|
157
157
|
}
|
|
158
158
|
function parseBlockquote(state) {
|
|
@@ -217,9 +217,9 @@ function parseTable(state, line) {
|
|
|
217
217
|
const headerCells = splitTableRow(rowMatch[1]);
|
|
218
218
|
const cols = headerCells.length;
|
|
219
219
|
rows.push({
|
|
220
|
-
type: "
|
|
220
|
+
type: "table_row",
|
|
221
221
|
content: headerCells.map((cell) => ({
|
|
222
|
-
type: "
|
|
222
|
+
type: "table_header",
|
|
223
223
|
content: [{ type: "paragraph", content: cellContent(cell) }],
|
|
224
224
|
})),
|
|
225
225
|
});
|
|
@@ -238,9 +238,9 @@ function parseTable(state, line) {
|
|
|
238
238
|
}
|
|
239
239
|
cells.length = cols;
|
|
240
240
|
rows.push({
|
|
241
|
-
type: "
|
|
241
|
+
type: "table_row",
|
|
242
242
|
content: cells.map((cell) => ({
|
|
243
|
-
type: "
|
|
243
|
+
type: "table_cell",
|
|
244
244
|
content: [{ type: "paragraph", content: cellContent(cell) }],
|
|
245
245
|
})),
|
|
246
246
|
});
|
|
@@ -327,7 +327,7 @@ function findEarliestInlineMatch(text) {
|
|
|
327
327
|
index: boldMatch.index ?? 0,
|
|
328
328
|
length: boldMatch[0].length,
|
|
329
329
|
innerText: boldMatch[1] ?? boldMatch[2],
|
|
330
|
-
marks: [{ type: "
|
|
330
|
+
marks: [{ type: "strong" }],
|
|
331
331
|
});
|
|
332
332
|
}
|
|
333
333
|
const italicMatch = text.match(INLINE_ITALIC_RE);
|
|
@@ -336,7 +336,7 @@ function findEarliestInlineMatch(text) {
|
|
|
336
336
|
index: italicMatch.index ?? 0,
|
|
337
337
|
length: italicMatch[0].length,
|
|
338
338
|
innerText: italicMatch[1] ?? italicMatch[2],
|
|
339
|
-
marks: [{ type: "
|
|
339
|
+
marks: [{ type: "em" }],
|
|
340
340
|
});
|
|
341
341
|
}
|
|
342
342
|
if (candidates.length === 0) {
|
|
@@ -146,7 +146,7 @@ function injectMentions(doc, explicit, bare) {
|
|
|
146
146
|
return {
|
|
147
147
|
...doc,
|
|
148
148
|
content: doc.content.map((node) => {
|
|
149
|
-
if (node.content && node.type !== "
|
|
149
|
+
if (node.content && node.type !== "code_block") {
|
|
150
150
|
const processed = injectMentions(node, explicit, bare);
|
|
151
151
|
if (node.type === "paragraph" || node.type === "heading") {
|
|
152
152
|
return {
|
package/dist/utils/output.d.ts
CHANGED
|
@@ -1,11 +1,91 @@
|
|
|
1
|
-
|
|
1
|
+
type OutputFormat = "json" | "summary";
|
|
2
2
|
export declare function setRawMode(enabled: boolean): void;
|
|
3
3
|
export declare function setJqFilter(filter: string | null): void;
|
|
4
4
|
export declare function setFieldsFilter(fields: string[] | null): void;
|
|
5
5
|
export declare function setOutputFormat(format: OutputFormat): void;
|
|
6
|
+
/** @internal Test seam — consumers should not depend on the format state. */
|
|
6
7
|
export declare function getOutputFormat(): OutputFormat;
|
|
8
|
+
/**
|
|
9
|
+
* Resource-specific extra metadata that may be added to a list response
|
|
10
|
+
* alongside the canonical `count` (e.g. `query` on search, `team` on
|
|
11
|
+
* filtered list). Excludes `count` at the type level — callers can't
|
|
12
|
+
* accidentally pass a string `count` and have it silently overridden;
|
|
13
|
+
* the only way to set `count` is via `data.length` inside `outputList`.
|
|
14
|
+
*
|
|
15
|
+
* Implementation note: `count?: never` together with `Record<string, unknown>`
|
|
16
|
+
* lets TypeScript accept any other key while disallowing the literal
|
|
17
|
+
* `count` key. The `never`-typed property is impossible to assign, which
|
|
18
|
+
* is what we want for the "no count here" contract.
|
|
19
|
+
*/
|
|
20
|
+
export type ListExtraMeta = Record<string, unknown> & {
|
|
21
|
+
count?: never;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Metadata envelope for list responses. Always includes `count`; resources
|
|
25
|
+
* may add their own keys (e.g. `query` on search, `team` on filtered list).
|
|
26
|
+
*
|
|
27
|
+
* `meta.count` is the cardinality of `data[]` — downstream `jq` pipelines
|
|
28
|
+
* read it both for emptiness checks (`.meta.count == 0`) and for the
|
|
29
|
+
* actual magnitude (e.g. logging "found N issues"). A boolean isEmpty
|
|
30
|
+
* would lose the magnitude signal, so `count: number` stays.
|
|
31
|
+
*/
|
|
32
|
+
export interface ListMeta extends Record<string, unknown> {
|
|
33
|
+
count: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Canonical JSON envelope for a list response. Pre-DEV-4068 T6, every
|
|
37
|
+
* call site built this object literal inline and `outputSuccess(data:
|
|
38
|
+
* unknown)` accepted it without type-checking. Use `outputList<T>` to
|
|
39
|
+
* route a list through the same emit path with a real per-element type
|
|
40
|
+
* (so e.g. `data: T[]` and `--fields` consumers stay in lock-step).
|
|
41
|
+
*/
|
|
42
|
+
export interface CliListEnvelope<T> {
|
|
43
|
+
data: T[];
|
|
44
|
+
meta: ListMeta;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Typed wrapper around `outputSuccess` for list responses — builds the
|
|
48
|
+
* `{ data, meta: { count, ...extraMeta } }` envelope and emits it. The
|
|
49
|
+
* 81 existing inline `outputSuccess({ data, meta: { count, ... } })`
|
|
50
|
+
* call sites can migrate to this incrementally; both go through the
|
|
51
|
+
* same `outputSuccess` emit path so JSON / summary / --raw / --fields /
|
|
52
|
+
* --jq behavior is identical.
|
|
53
|
+
*
|
|
54
|
+
* @param data The array payload.
|
|
55
|
+
* @param extraMeta Optional resource-specific meta keys (e.g. `query`,
|
|
56
|
+
* `team`). The type excludes `count` — `count` is
|
|
57
|
+
* always computed from `data.length` to preserve the
|
|
58
|
+
* wire-contract invariant.
|
|
59
|
+
*/
|
|
60
|
+
export declare function outputList<T>(data: T[], extraMeta?: ListExtraMeta): void;
|
|
61
|
+
/**
|
|
62
|
+
* Typed wrapper around `outputSuccess` for a single-resource response.
|
|
63
|
+
* Pure passthrough — the envelope contract for single resources is just
|
|
64
|
+
* the resource object itself (no `data`/`meta` wrapping). Same emit
|
|
65
|
+
* path as `outputSuccess` and `outputList`; this overload exists so
|
|
66
|
+
* call sites can document their intent at the type level.
|
|
67
|
+
*
|
|
68
|
+
* The conditional return type rejects array inputs at the type level —
|
|
69
|
+
* a caller that meant `outputList` and passed an array gets a compile
|
|
70
|
+
* error pointing them at the right helper. Runtime behavior on an array
|
|
71
|
+
* is unchanged (it'd still emit JSON), but the type catches the foot-gun.
|
|
72
|
+
*/
|
|
73
|
+
export declare function outputSingle<T>(data: T extends readonly unknown[] ? "outputSingle does not accept arrays — use outputList(data) instead" : T): void;
|
|
7
74
|
export declare function outputSuccess(data: unknown): void;
|
|
75
|
+
/**
|
|
76
|
+
* Emit a `results_truncated` warning when a list command's result count
|
|
77
|
+
* equals the requested `--limit`, signaling to the caller (typically an
|
|
78
|
+
* AI agent) that more results may exist beyond the page. Heuristic, not
|
|
79
|
+
* exact — `length === limit` has a false-positive when the workspace
|
|
80
|
+
* happens to hold exactly `limit` matching items. Accurate `hasNextPage`
|
|
81
|
+
* would require widening every list-service return type; deferred until
|
|
82
|
+
* a caller cares about the false-positive rate.
|
|
83
|
+
*
|
|
84
|
+
* Message includes a concrete next-step (suggested `--limit` is 2× the
|
|
85
|
+
* current limit) so the agent doesn't have to derive it.
|
|
86
|
+
*/
|
|
87
|
+
export declare function warnIfTruncated(count: number, limit: number): void;
|
|
8
88
|
export declare function outputWarning(message: string | string[]): void;
|
|
9
89
|
export declare function resetWarnings(): void;
|
|
10
|
-
export declare function resetOutputFormat(): void;
|
|
11
90
|
export declare function handleAsyncCommand<TArgs extends unknown[]>(asyncFn: (...args: TArgs) => Promise<void>): (...args: TArgs) => Promise<void>;
|
|
91
|
+
export {};
|
package/dist/utils/output.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { dispatch as dispatchSummary, inferKindFromPayload, } from "./formatters/summary.js";
|
|
3
3
|
import { logger } from "./logger.js";
|
|
4
|
+
import { sanitizeForLog } from "./sanitize-for-log.js";
|
|
4
5
|
const warningBuffer = [];
|
|
5
6
|
let rawMode = false;
|
|
6
7
|
let jqFilter = null;
|
|
@@ -18,6 +19,7 @@ export function setFieldsFilter(fields) {
|
|
|
18
19
|
export function setOutputFormat(format) {
|
|
19
20
|
outputFormat = format;
|
|
20
21
|
}
|
|
22
|
+
/** @internal Test seam — consumers should not depend on the format state. */
|
|
21
23
|
export function getOutputFormat() {
|
|
22
24
|
return outputFormat;
|
|
23
25
|
}
|
|
@@ -38,14 +40,51 @@ function filterFields(obj, fields) {
|
|
|
38
40
|
return obj;
|
|
39
41
|
}
|
|
40
42
|
/**
|
|
41
|
-
* Emit a summary-format render to stdout for the given payload.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
43
|
+
* Emit a summary-format render to stdout for the given payload.
|
|
44
|
+
*
|
|
45
|
+
* `kind` is captured upstream from the **pre-filter** payload — if we
|
|
46
|
+
* inferred here, `--fields identifier,url` would strip `title` and
|
|
47
|
+
* the heuristic would fall through to "generic", silently breaking
|
|
48
|
+
* the issue-list table. Caching the pre-filter shape keeps the
|
|
49
|
+
* formatter accurate regardless of how the user pared the JSON.
|
|
44
50
|
*/
|
|
45
|
-
function emitSummary(payload) {
|
|
46
|
-
const kind = inferKindFromPayload(payload);
|
|
51
|
+
function emitSummary(payload, kind) {
|
|
47
52
|
logger.info(dispatchSummary(kind, payload));
|
|
48
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Typed wrapper around `outputSuccess` for list responses — builds the
|
|
56
|
+
* `{ data, meta: { count, ...extraMeta } }` envelope and emits it. The
|
|
57
|
+
* 81 existing inline `outputSuccess({ data, meta: { count, ... } })`
|
|
58
|
+
* call sites can migrate to this incrementally; both go through the
|
|
59
|
+
* same `outputSuccess` emit path so JSON / summary / --raw / --fields /
|
|
60
|
+
* --jq behavior is identical.
|
|
61
|
+
*
|
|
62
|
+
* @param data The array payload.
|
|
63
|
+
* @param extraMeta Optional resource-specific meta keys (e.g. `query`,
|
|
64
|
+
* `team`). The type excludes `count` — `count` is
|
|
65
|
+
* always computed from `data.length` to preserve the
|
|
66
|
+
* wire-contract invariant.
|
|
67
|
+
*/
|
|
68
|
+
export function outputList(data, extraMeta) {
|
|
69
|
+
const meta = { ...extraMeta, count: data.length };
|
|
70
|
+
const envelope = { data, meta };
|
|
71
|
+
outputSuccess(envelope);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Typed wrapper around `outputSuccess` for a single-resource response.
|
|
75
|
+
* Pure passthrough — the envelope contract for single resources is just
|
|
76
|
+
* the resource object itself (no `data`/`meta` wrapping). Same emit
|
|
77
|
+
* path as `outputSuccess` and `outputList`; this overload exists so
|
|
78
|
+
* call sites can document their intent at the type level.
|
|
79
|
+
*
|
|
80
|
+
* The conditional return type rejects array inputs at the type level —
|
|
81
|
+
* a caller that meant `outputList` and passed an array gets a compile
|
|
82
|
+
* error pointing them at the right helper. Runtime behavior on an array
|
|
83
|
+
* is unchanged (it'd still emit JSON), but the type catches the foot-gun.
|
|
84
|
+
*/
|
|
85
|
+
export function outputSingle(data) {
|
|
86
|
+
outputSuccess(data);
|
|
87
|
+
}
|
|
49
88
|
export function outputSuccess(data) {
|
|
50
89
|
const warnings = drainWarnings();
|
|
51
90
|
let output;
|
|
@@ -58,6 +97,11 @@ export function outputSuccess(data) {
|
|
|
58
97
|
else {
|
|
59
98
|
output = data;
|
|
60
99
|
}
|
|
100
|
+
// Capture the kind from the original envelope shape BEFORE --raw /
|
|
101
|
+
// --fields stripping. Otherwise filtering away signature fields (e.g.
|
|
102
|
+
// `title` on an issue) breaks shape inference and the summary
|
|
103
|
+
// formatter falls back to the generic key-value dump.
|
|
104
|
+
const inferredKind = outputFormat === "summary" ? inferKindFromPayload(output) : "generic";
|
|
61
105
|
// --raw: unwrap { data: [...] } to just the array
|
|
62
106
|
if (rawMode &&
|
|
63
107
|
output !== null &&
|
|
@@ -87,7 +131,7 @@ export function outputSuccess(data) {
|
|
|
87
131
|
// as a human-readable block. We bypass the jq path because jq is a
|
|
88
132
|
// JSON-shape filter — it doesn't compose with text output.
|
|
89
133
|
if (outputFormat === "summary") {
|
|
90
|
-
emitSummary(output);
|
|
134
|
+
emitSummary(output, inferredKind);
|
|
91
135
|
return;
|
|
92
136
|
}
|
|
93
137
|
if (jqFilter) {
|
|
@@ -111,6 +155,25 @@ export function outputSuccess(data) {
|
|
|
111
155
|
logger.info(JSON.stringify(output, null, 2));
|
|
112
156
|
}
|
|
113
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Emit a `results_truncated` warning when a list command's result count
|
|
160
|
+
* equals the requested `--limit`, signaling to the caller (typically an
|
|
161
|
+
* AI agent) that more results may exist beyond the page. Heuristic, not
|
|
162
|
+
* exact — `length === limit` has a false-positive when the workspace
|
|
163
|
+
* happens to hold exactly `limit` matching items. Accurate `hasNextPage`
|
|
164
|
+
* would require widening every list-service return type; deferred until
|
|
165
|
+
* a caller cares about the false-positive rate.
|
|
166
|
+
*
|
|
167
|
+
* Message includes a concrete next-step (suggested `--limit` is 2× the
|
|
168
|
+
* current limit) so the agent doesn't have to derive it.
|
|
169
|
+
*/
|
|
170
|
+
export function warnIfTruncated(count, limit) {
|
|
171
|
+
if (count !== limit) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
outputWarning(`results_truncated: returned ${count} matching --limit ${limit}; more results may exist. ` +
|
|
175
|
+
`Re-run with --limit ${limit * 2} (or narrow via filters like --name, --state, --team) to verify.`);
|
|
176
|
+
}
|
|
114
177
|
export function outputWarning(message) {
|
|
115
178
|
const messages = Array.isArray(message) ? message : [message];
|
|
116
179
|
for (const msg of messages) {
|
|
@@ -127,16 +190,18 @@ function drainWarnings() {
|
|
|
127
190
|
export function resetWarnings() {
|
|
128
191
|
warningBuffer.length = 0;
|
|
129
192
|
}
|
|
130
|
-
export function resetOutputFormat() {
|
|
131
|
-
outputFormat = "json";
|
|
132
|
-
}
|
|
133
193
|
function outputError(error) {
|
|
134
|
-
|
|
194
|
+
// Run the message and stack through sanitizeForLog so a future SDK
|
|
195
|
+
// upgrade (or proxy/MITM error body) that embeds `lin_api_…` /
|
|
196
|
+
// `lin_oauth_…` / `Bearer <payload>` in error text can't leak a token
|
|
197
|
+
// into stdout, shell history, or CI logs. The wizard already sanitizes
|
|
198
|
+
// its own log paths; this is the central error path on every command.
|
|
199
|
+
const payload = JSON.stringify({ error: sanitizeForLog(error.message) }, null, 2);
|
|
135
200
|
// Write to stdout (same channel as success) so machine callers always
|
|
136
201
|
// receive exactly one parseable JSON object regardless of stream capture.
|
|
137
202
|
logger.info(payload);
|
|
138
203
|
if (process.env.EL_LINEAR_DEBUG ?? process.env.LINCTL_DEBUG) {
|
|
139
|
-
logger.error(error.stack ?? "");
|
|
204
|
+
logger.error(sanitizeForLog(error.stack ?? ""));
|
|
140
205
|
}
|
|
141
206
|
process.exit(1);
|
|
142
207
|
}
|