@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a Linear project URL or bare slug-id into the `slugId` form that
|
|
3
|
+
* Linear's GraphQL `ProjectFilter.slugId` accepts.
|
|
4
|
+
*
|
|
5
|
+
* Linear project URLs look like:
|
|
6
|
+
* https://linear.app/<workspace>/project/<slug>-<12-hex>/<view>
|
|
7
|
+
*
|
|
8
|
+
* The slug-id is the `<slug>-<12-hex>` segment (kebab-case name followed
|
|
9
|
+
* by a 12-character hex suffix). Linear uses this exact string as the
|
|
10
|
+
* unique `slugId` field on the Project type.
|
|
11
|
+
*
|
|
12
|
+
* Accepts:
|
|
13
|
+
* - Full URL form: extracts the slug-id from the path
|
|
14
|
+
* - Bare slug-id: `tools-and-standardization-40815d9beb16`
|
|
15
|
+
* - Trailing path / query string is tolerated (`/overview`, `?foo=bar`)
|
|
16
|
+
*
|
|
17
|
+
* Returns `null` when the input is neither — caller can then fall through
|
|
18
|
+
* to name-based or UUID-based resolution. Canonical UUIDs are rejected
|
|
19
|
+
* here so that `isUuid()` callers stay the authoritative UUID path.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseProjectSlugId(input: string): string | null;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isUuid } from "./uuid.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a Linear project URL or bare slug-id into the `slugId` form that
|
|
4
|
+
* Linear's GraphQL `ProjectFilter.slugId` accepts.
|
|
5
|
+
*
|
|
6
|
+
* Linear project URLs look like:
|
|
7
|
+
* https://linear.app/<workspace>/project/<slug>-<12-hex>/<view>
|
|
8
|
+
*
|
|
9
|
+
* The slug-id is the `<slug>-<12-hex>` segment (kebab-case name followed
|
|
10
|
+
* by a 12-character hex suffix). Linear uses this exact string as the
|
|
11
|
+
* unique `slugId` field on the Project type.
|
|
12
|
+
*
|
|
13
|
+
* Accepts:
|
|
14
|
+
* - Full URL form: extracts the slug-id from the path
|
|
15
|
+
* - Bare slug-id: `tools-and-standardization-40815d9beb16`
|
|
16
|
+
* - Trailing path / query string is tolerated (`/overview`, `?foo=bar`)
|
|
17
|
+
*
|
|
18
|
+
* Returns `null` when the input is neither — caller can then fall through
|
|
19
|
+
* to name-based or UUID-based resolution. Canonical UUIDs are rejected
|
|
20
|
+
* here so that `isUuid()` callers stay the authoritative UUID path.
|
|
21
|
+
*/
|
|
22
|
+
export function parseProjectSlugId(input) {
|
|
23
|
+
const trimmed = input.trim();
|
|
24
|
+
if (!trimmed || isUuid(trimmed)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const urlMatch = trimmed.match(/\blinear\.app\/[^/\s]+\/project\/([^/?#\s]+)/i);
|
|
28
|
+
if (urlMatch) {
|
|
29
|
+
const candidate = urlMatch[1];
|
|
30
|
+
return looksLikeSlugId(candidate) ? candidate : null;
|
|
31
|
+
}
|
|
32
|
+
if (looksLikeSlugId(trimmed)) {
|
|
33
|
+
return trimmed;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A Linear project slug-id is a kebab-case name segment followed by a
|
|
39
|
+
* 12-character hex suffix. The minimum form is just the 12 hex chars
|
|
40
|
+
* (when the project name slugifies to empty), but in practice there's
|
|
41
|
+
* always at least one name segment.
|
|
42
|
+
*/
|
|
43
|
+
function looksLikeSlugId(value) {
|
|
44
|
+
return /^[a-z0-9]+(?:-[a-z0-9]+)*-[a-f0-9]{12}$/i.test(value);
|
|
45
|
+
}
|
|
@@ -17,6 +17,20 @@
|
|
|
17
17
|
*/
|
|
18
18
|
/** Linear identifier shape: ABC-123, EMW-1, DEV-3592. */
|
|
19
19
|
export declare const IDENTIFIER_REGEX: RegExp;
|
|
20
|
+
/**
|
|
21
|
+
* Find the first position in `s` where a close-bracket character
|
|
22
|
+
* appears without a matching opener earlier in the string. Returns
|
|
23
|
+
* `s.length` if all close-brackets are balanced. Used to truncate
|
|
24
|
+
* a bare-URL match at the first unbalanced `)`, `]`, or `}` —
|
|
25
|
+
* exactly the position CommonMark treats as the URL terminator.
|
|
26
|
+
*
|
|
27
|
+
* Exported for direct unit testing. Depth counters per bracket type
|
|
28
|
+
* are independent — pathological interleavings like `[(a]b)` don't
|
|
29
|
+
* trigger an unbalance, which errs toward keeping a URL whole rather
|
|
30
|
+
* than over-truncating (no real-world URL nests bracket types this
|
|
31
|
+
* way).
|
|
32
|
+
*/
|
|
33
|
+
export declare function firstUnbalancedClose(s: string): number;
|
|
20
34
|
export interface ProtectedRange {
|
|
21
35
|
end: number;
|
|
22
36
|
start: number;
|
|
@@ -33,7 +33,87 @@ const ANGLE_AUTOLINK_REGEX = /<[^>\s]+>/g;
|
|
|
33
33
|
// Bare URLs in prose. We protect these so identifiers inside path
|
|
34
34
|
// components (e.g. "https://github.com/foo/DEV-100") don't get
|
|
35
35
|
// processed.
|
|
36
|
-
|
|
36
|
+
//
|
|
37
|
+
// Match strategy follows CommonMark's "extended autolink" rule:
|
|
38
|
+
//
|
|
39
|
+
// 1. Greedy match up to whitespace or angle/quote terminators
|
|
40
|
+
// (`<`, `>`, `"`). Parens and brackets stay IN the match — they
|
|
41
|
+
// appear inside legitimate URLs (Wikipedia article paths, Next.js
|
|
42
|
+
// route groups like `/docs/app/(group)/page`, CDN signing-key
|
|
43
|
+
// query strings, etc.).
|
|
44
|
+
// 2. Post-process via `trimBareUrlTrailingPunct` to strip UNBALANCED
|
|
45
|
+
// trailing brackets and stand-alone punctuation. So
|
|
46
|
+
// `https://example.com/foo)DEV-100` correctly terminates at
|
|
47
|
+
// `…foo` (the closing paren is unbalanced — no opener inside the
|
|
48
|
+
// match), while `https://en.wikipedia.org/wiki/Foo_(bar)` keeps
|
|
49
|
+
// its balanced parens intact.
|
|
50
|
+
//
|
|
51
|
+
// Pre-fix `https?:\/\/\S+` greedily consumed everything to the next
|
|
52
|
+
// whitespace and silently hid identifiers in position-dependent
|
|
53
|
+
// prose. A char-class exclusion of `)`, `]`, `}` over-corrected and
|
|
54
|
+
// broke Wikipedia / route-group URLs (cycle 2 finding on PR #75).
|
|
55
|
+
// The balanced-paren trim is the CommonMark-faithful middle ground.
|
|
56
|
+
const BARE_URL_REGEX = /https?:\/\/[^\s<>"]+/g;
|
|
57
|
+
/**
|
|
58
|
+
* Find the first position in `s` where a close-bracket character
|
|
59
|
+
* appears without a matching opener earlier in the string. Returns
|
|
60
|
+
* `s.length` if all close-brackets are balanced. Used to truncate
|
|
61
|
+
* a bare-URL match at the first unbalanced `)`, `]`, or `}` —
|
|
62
|
+
* exactly the position CommonMark treats as the URL terminator.
|
|
63
|
+
*
|
|
64
|
+
* Exported for direct unit testing. Depth counters per bracket type
|
|
65
|
+
* are independent — pathological interleavings like `[(a]b)` don't
|
|
66
|
+
* trigger an unbalance, which errs toward keeping a URL whole rather
|
|
67
|
+
* than over-truncating (no real-world URL nests bracket types this
|
|
68
|
+
* way).
|
|
69
|
+
*/
|
|
70
|
+
export function firstUnbalancedClose(s) {
|
|
71
|
+
let parenDepth = 0;
|
|
72
|
+
let brackDepth = 0;
|
|
73
|
+
let braceDepth = 0;
|
|
74
|
+
for (let i = 0; i < s.length; i++) {
|
|
75
|
+
const ch = s[i];
|
|
76
|
+
if (ch === "(")
|
|
77
|
+
parenDepth++;
|
|
78
|
+
else if (ch === ")") {
|
|
79
|
+
if (parenDepth === 0)
|
|
80
|
+
return i;
|
|
81
|
+
parenDepth--;
|
|
82
|
+
}
|
|
83
|
+
else if (ch === "[")
|
|
84
|
+
brackDepth++;
|
|
85
|
+
else if (ch === "]") {
|
|
86
|
+
if (brackDepth === 0)
|
|
87
|
+
return i;
|
|
88
|
+
brackDepth--;
|
|
89
|
+
}
|
|
90
|
+
else if (ch === "{")
|
|
91
|
+
braceDepth++;
|
|
92
|
+
else if (ch === "}") {
|
|
93
|
+
if (braceDepth === 0)
|
|
94
|
+
return i;
|
|
95
|
+
braceDepth--;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return s.length;
|
|
99
|
+
}
|
|
100
|
+
function trimBareUrlTrailingPunct(url) {
|
|
101
|
+
// First: truncate at the first unbalanced bracket. So
|
|
102
|
+
// `https://example.com/foo)DEV-100` (no `(` opener inside) terminates
|
|
103
|
+
// at the `)` regardless of what follows it; `Foo_(bar)/DEV` keeps
|
|
104
|
+
// the balanced `(bar)` intact.
|
|
105
|
+
let trimmed = url.slice(0, firstUnbalancedClose(url));
|
|
106
|
+
// Then: strip standard trailing sentence-terminators (`.`, `,`, `;`,
|
|
107
|
+
// `:`, `!`, `?`). These never appear in legitimate URL paths at the
|
|
108
|
+
// very end, but they're commonly adjacent to URLs in prose. The
|
|
109
|
+
// char class deliberately excludes `)`, `]`, `}` — those are
|
|
110
|
+
// already handled by firstUnbalancedClose above, where the
|
|
111
|
+
// balanced/unbalanced check lives.
|
|
112
|
+
while (trimmed.length > 0 && /[.,;:!?]$/.test(trimmed)) {
|
|
113
|
+
trimmed = trimmed.slice(0, -1);
|
|
114
|
+
}
|
|
115
|
+
return trimmed;
|
|
116
|
+
}
|
|
37
117
|
/**
|
|
38
118
|
* Find ranges of `text` that should NOT have identifiers processed.
|
|
39
119
|
* Covered: fenced code, inline backticks, existing markdown links,
|
|
@@ -54,13 +134,19 @@ export function findProtectedRanges(text) {
|
|
|
54
134
|
// clarity here.
|
|
55
135
|
SLACK_LINK_REGEX,
|
|
56
136
|
ANGLE_AUTOLINK_REGEX,
|
|
57
|
-
BARE_URL_REGEX,
|
|
58
137
|
]) {
|
|
59
138
|
for (const m of text.matchAll(re)) {
|
|
60
139
|
const start = m.index ?? 0;
|
|
61
140
|
ranges.push({ start, end: start + m[0].length });
|
|
62
141
|
}
|
|
63
142
|
}
|
|
143
|
+
// Bare URLs need the trailing-punctuation trim that's hard to express
|
|
144
|
+
// in pure regex (depends on bracket-balance inside the match).
|
|
145
|
+
for (const m of text.matchAll(BARE_URL_REGEX)) {
|
|
146
|
+
const start = m.index ?? 0;
|
|
147
|
+
const trimmed = trimBareUrlTrailingPunct(m[0]);
|
|
148
|
+
ranges.push({ start, end: start + trimmed.length });
|
|
149
|
+
}
|
|
64
150
|
return ranges;
|
|
65
151
|
}
|
|
66
152
|
export function isProtected(pos, ranges) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip anything that looks like a Linear API/OAuth token from a string.
|
|
3
|
+
*
|
|
4
|
+
* Defense in depth: today the @linear/sdk error message embeds `{ query,
|
|
5
|
+
* variables }` but not the Authorization header. A future SDK upgrade that
|
|
6
|
+
* includes headers (which upstream graphql-request has done historically)
|
|
7
|
+
* would otherwise silently write `Bearer lin_api_…` into stdout, shell
|
|
8
|
+
* history, or CI logs. The regex also catches token shapes that may show
|
|
9
|
+
* up in custom error wrappers — e.g. a network proxy that echoes the
|
|
10
|
+
* Bearer header in its 502 body.
|
|
11
|
+
*
|
|
12
|
+
* Originally lived in `commands/init/token.ts` for the wizard's error
|
|
13
|
+
* formatting. Hoisted to `utils/` so the central error path
|
|
14
|
+
* (`output.ts`'s `outputError`) can use it too — that path runs on
|
|
15
|
+
* every non-wizard CLI invocation. `init/token.ts` re-exports the
|
|
16
|
+
* symbol so existing imports under `init/` keep working.
|
|
17
|
+
*
|
|
18
|
+
* The OAuth token-exchange / refresh / revoke paths also call this
|
|
19
|
+
* function at source (`auth/oauth-token.ts`'s `postForm` + `revokeToken`,
|
|
20
|
+
* `auth/token-resolver.ts`'s refresh-failure rewrap) so a future caller
|
|
21
|
+
* that catches+rethrows or logs mid-chain can't leak a token before the
|
|
22
|
+
* error reaches `outputError`. Defense in depth (DEV-4065).
|
|
23
|
+
*/
|
|
24
|
+
export declare function sanitizeForLog(text: string): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip anything that looks like a Linear API/OAuth token from a string.
|
|
3
|
+
*
|
|
4
|
+
* Defense in depth: today the @linear/sdk error message embeds `{ query,
|
|
5
|
+
* variables }` but not the Authorization header. A future SDK upgrade that
|
|
6
|
+
* includes headers (which upstream graphql-request has done historically)
|
|
7
|
+
* would otherwise silently write `Bearer lin_api_…` into stdout, shell
|
|
8
|
+
* history, or CI logs. The regex also catches token shapes that may show
|
|
9
|
+
* up in custom error wrappers — e.g. a network proxy that echoes the
|
|
10
|
+
* Bearer header in its 502 body.
|
|
11
|
+
*
|
|
12
|
+
* Originally lived in `commands/init/token.ts` for the wizard's error
|
|
13
|
+
* formatting. Hoisted to `utils/` so the central error path
|
|
14
|
+
* (`output.ts`'s `outputError`) can use it too — that path runs on
|
|
15
|
+
* every non-wizard CLI invocation. `init/token.ts` re-exports the
|
|
16
|
+
* symbol so existing imports under `init/` keep working.
|
|
17
|
+
*
|
|
18
|
+
* The OAuth token-exchange / refresh / revoke paths also call this
|
|
19
|
+
* function at source (`auth/oauth-token.ts`'s `postForm` + `revokeToken`,
|
|
20
|
+
* `auth/token-resolver.ts`'s refresh-failure rewrap) so a future caller
|
|
21
|
+
* that catches+rethrows or logs mid-chain can't leak a token before the
|
|
22
|
+
* error reaches `outputError`. Defense in depth (DEV-4065).
|
|
23
|
+
*/
|
|
24
|
+
// Personal-API tokens (`lin_api_…`) and OAuth access/refresh tokens
|
|
25
|
+
// (`lin_oauth_…`).
|
|
26
|
+
const TOKEN_PREFIX_RE = /lin_(api|oauth)_[A-Za-z0-9_-]{16,}/g;
|
|
27
|
+
// High-entropy bearer payload fallback: catches generic Bearer-style
|
|
28
|
+
// strings adjacent to Authorization / Bearer keywords. Useful for
|
|
29
|
+
// future SDK error wrappers that might leak headers without the
|
|
30
|
+
// `lin_` prefix.
|
|
31
|
+
const BEARER_PAYLOAD_RE = /(\b(?:Authorization|Bearer)\b[:\s]*)([A-Za-z0-9_\-/+=]{40,})/gi;
|
|
32
|
+
export function sanitizeForLog(text) {
|
|
33
|
+
return text
|
|
34
|
+
.replace(TOKEN_PREFIX_RE, (m) => m.startsWith("lin_oauth_")
|
|
35
|
+
? "lin_oauth_***REDACTED***"
|
|
36
|
+
: "lin_api_***REDACTED***")
|
|
37
|
+
.replace(BEARER_PAYLOAD_RE, "$1***REDACTED***");
|
|
38
|
+
}
|
|
@@ -86,6 +86,19 @@ const ALL_COLUMNS = {
|
|
|
86
86
|
return atIdx > 0 ? name.slice(0, atIdx) : name;
|
|
87
87
|
},
|
|
88
88
|
},
|
|
89
|
+
delegate: {
|
|
90
|
+
key: "delegate",
|
|
91
|
+
header: "Delegate",
|
|
92
|
+
width: 20,
|
|
93
|
+
extract: (i) => {
|
|
94
|
+
if (!i.delegate) {
|
|
95
|
+
return "—";
|
|
96
|
+
}
|
|
97
|
+
const name = i.delegate.name;
|
|
98
|
+
const atIdx = name.indexOf("@");
|
|
99
|
+
return atIdx > 0 ? name.slice(0, atIdx) : name;
|
|
100
|
+
},
|
|
101
|
+
},
|
|
89
102
|
project: {
|
|
90
103
|
key: "project",
|
|
91
104
|
header: "Project",
|
|
@@ -229,6 +242,17 @@ const MD_COLUMNS = {
|
|
|
229
242
|
return atIdx > 0 ? name.slice(0, atIdx) : name;
|
|
230
243
|
},
|
|
231
244
|
},
|
|
245
|
+
delegate: {
|
|
246
|
+
header: "Delegate",
|
|
247
|
+
extract: (i) => {
|
|
248
|
+
if (!i.delegate) {
|
|
249
|
+
return "—";
|
|
250
|
+
}
|
|
251
|
+
const name = i.delegate.name;
|
|
252
|
+
const atIdx = name.indexOf("@");
|
|
253
|
+
return atIdx > 0 ? name.slice(0, atIdx) : name;
|
|
254
|
+
},
|
|
255
|
+
},
|
|
232
256
|
project: {
|
|
233
257
|
header: "Project",
|
|
234
258
|
extract: (i) => i.project?.name ?? "—",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LinearPriority } from "../types/linear.js";
|
|
1
2
|
export declare function parsePositiveInt(value: string, flagName: string): number;
|
|
2
3
|
/**
|
|
3
4
|
* Parse a single priority value (for `issues create --priority`, `issues update --priority`).
|
|
@@ -5,8 +6,12 @@ export declare function parsePositiveInt(value: string, flagName: string): numbe
|
|
|
5
6
|
* Accepts:
|
|
6
7
|
* - keywords: none | urgent | high | medium | normal | low
|
|
7
8
|
* - numbers: 0 (no priority), 1 (urgent), 2 (high), 3 (medium), 4 (low)
|
|
9
|
+
*
|
|
10
|
+
* Narrows arbitrary string input to the `LinearPriority` literal union so
|
|
11
|
+
* downstream call sites get type-level guarantees that the value is in
|
|
12
|
+
* range — no `as 0 | 1 | 2 | 3 | 4` cast at the boundary.
|
|
8
13
|
*/
|
|
9
|
-
export declare function validatePriority(value: string):
|
|
14
|
+
export declare function validatePriority(value: string): LinearPriority;
|
|
10
15
|
export declare function validateHexColor(value: string): string;
|
|
11
16
|
export declare function validateIsoDate(value: string): string;
|
|
12
17
|
/**
|
|
@@ -18,5 +23,5 @@ export declare function validateIsoDate(value: string): string;
|
|
|
18
23
|
* resolve to `[]`.
|
|
19
24
|
*/
|
|
20
25
|
export declare function splitList(value: string | undefined | null | false): string[];
|
|
21
|
-
export declare function parsePriorityFilter(value: string):
|
|
26
|
+
export declare function parsePriorityFilter(value: string): LinearPriority[];
|
|
22
27
|
export declare const PRIORITY_LABELS: Record<number, string>;
|
package/dist/utils/validators.js
CHANGED
|
@@ -14,6 +14,10 @@ export function parsePositiveInt(value, flagName) {
|
|
|
14
14
|
* Accepts:
|
|
15
15
|
* - keywords: none | urgent | high | medium | normal | low
|
|
16
16
|
* - numbers: 0 (no priority), 1 (urgent), 2 (high), 3 (medium), 4 (low)
|
|
17
|
+
*
|
|
18
|
+
* Narrows arbitrary string input to the `LinearPriority` literal union so
|
|
19
|
+
* downstream call sites get type-level guarantees that the value is in
|
|
20
|
+
* range — no `as 0 | 1 | 2 | 3 | 4` cast at the boundary.
|
|
17
21
|
*/
|
|
18
22
|
export function validatePriority(value) {
|
|
19
23
|
const asName = PRIORITY_NAMES[value.toLowerCase()];
|
|
@@ -24,6 +28,8 @@ export function validatePriority(value) {
|
|
|
24
28
|
if (Number.isNaN(n) || n < 0 || n > 4) {
|
|
25
29
|
throw invalidParameterError("--priority", `"${value}" is not valid. Use names (none, urgent, high, medium/normal, low) or numbers (0-4).`);
|
|
26
30
|
}
|
|
31
|
+
// The 0-4 range check above is what narrows `n` to LinearPriority;
|
|
32
|
+
// TypeScript can't infer that automatically.
|
|
27
33
|
return n;
|
|
28
34
|
}
|
|
29
35
|
export function validateHexColor(value) {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { GraphQLService } from "./graphql-service.js";
|
|
2
|
-
export
|
|
2
|
+
export interface GetWorkspaceUrlKeyOptions {
|
|
3
|
+
/** Highest-priority source. Typically the `--workspace-url-key` flag. */
|
|
4
|
+
override?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function getWorkspaceUrlKey(graphQLService?: GraphQLService, options?: GetWorkspaceUrlKeyOptions): Promise<string>;
|
|
3
7
|
/** Test helper: clear the cached URL key. Not called from production code. */
|
|
4
8
|
export declare function _resetWorkspaceUrlKeyCache(): void;
|
|
@@ -4,11 +4,20 @@ import { loadConfig } from "../config/config.js";
|
|
|
4
4
|
* issue URLs). Used to build canonical markdown link URLs like
|
|
5
5
|
* `https://linear.app/<urlKey>/issue/<identifier>/`.
|
|
6
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)
|
|
7
|
+
* Resolution order (highest priority first):
|
|
10
8
|
*
|
|
11
|
-
*
|
|
9
|
+
* 1. `options.override` — typically wired from a `--workspace-url-key` CLI flag
|
|
10
|
+
* (per-invocation, wins over everything).
|
|
11
|
+
* 2. `EL_LINEAR_WORKSPACE_URL_KEY` env var.
|
|
12
|
+
* 3. `config.workspaceUrlKey` from `~/.config/el-linear/config.json`.
|
|
13
|
+
* 4. Live `viewer.organization.urlKey` GraphQL query (cached in-process).
|
|
14
|
+
*
|
|
15
|
+
* Layers 1–3 short-circuit without touching the network — this makes
|
|
16
|
+
* `el-linear refs wrap --no-validate` work offline once any one of them
|
|
17
|
+
* is set. Layer 4 is the original behavior preserved as a fallback.
|
|
18
|
+
*
|
|
19
|
+
* The graphQLService is only needed for layer 4 — pass `undefined` (or omit it)
|
|
20
|
+
* when the caller knows the network will not be used.
|
|
12
21
|
*/
|
|
13
22
|
const VIEWER_ORG_URL_KEY_QUERY = /* GraphQL */ `
|
|
14
23
|
query ViewerOrgUrlKey {
|
|
@@ -19,15 +28,52 @@ const VIEWER_ORG_URL_KEY_QUERY = /* GraphQL */ `
|
|
|
19
28
|
}
|
|
20
29
|
}
|
|
21
30
|
`;
|
|
31
|
+
const WORKSPACE_URL_KEY_ENV = "EL_LINEAR_WORKSPACE_URL_KEY";
|
|
32
|
+
// Linear workspace URL keys are URL-safe slugs. The Linear API itself
|
|
33
|
+
// validates this shape (`viewerIsValid` in init/token.ts uses the same
|
|
34
|
+
// regex). Applying the check on env + config reads closes the gap where
|
|
35
|
+
// a malformed key (`EL_LINEAR_WORKSPACE_URL_KEY=" javascript:alert(1)#"`)
|
|
36
|
+
// would otherwise flow into markdown link URLs verbatim. Live-API path
|
|
37
|
+
// is already trusted because the value comes from `viewer.organization`.
|
|
38
|
+
// DEV-4067.
|
|
39
|
+
const VALID_URL_KEY_RE = /^[a-z0-9-]+$/i;
|
|
40
|
+
function validateUrlKey(value, source) {
|
|
41
|
+
if (!VALID_URL_KEY_RE.test(value)) {
|
|
42
|
+
throw new Error(`Invalid Linear workspace URL key from ${source}: ${JSON.stringify(value)}. ` +
|
|
43
|
+
"Must match /^[a-z0-9-]+$/i (e.g. 'verticalint').");
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
22
47
|
let cachedUrlKey;
|
|
23
|
-
export async function getWorkspaceUrlKey(graphQLService) {
|
|
48
|
+
export async function getWorkspaceUrlKey(graphQLService, options = {}) {
|
|
49
|
+
// 1. Per-invocation override — highest priority.
|
|
50
|
+
if (options.override) {
|
|
51
|
+
return validateUrlKey(options.override, "--workspace-url-key flag");
|
|
52
|
+
}
|
|
53
|
+
// 2. Env var. Read on every call (cheap; lets tests/CI flip the value
|
|
54
|
+
// mid-process without restarting). Not cached, so a test that sets
|
|
55
|
+
// EL_LINEAR_WORKSPACE_URL_KEY then unsets it sees the unset state.
|
|
56
|
+
const envKey = process.env[WORKSPACE_URL_KEY_ENV];
|
|
57
|
+
if (envKey) {
|
|
58
|
+
return validateUrlKey(envKey, `${WORKSPACE_URL_KEY_ENV} env var`);
|
|
59
|
+
}
|
|
60
|
+
// Layers 3-4 are cached in-process — config and the live lookup are
|
|
61
|
+
// both stable for the CLI invocation's lifetime.
|
|
24
62
|
if (cachedUrlKey) {
|
|
25
63
|
return cachedUrlKey;
|
|
26
64
|
}
|
|
65
|
+
// 3. Config override.
|
|
27
66
|
const { workspaceUrlKey } = loadConfig();
|
|
28
67
|
if (workspaceUrlKey) {
|
|
29
|
-
|
|
30
|
-
|
|
68
|
+
const validated = validateUrlKey(workspaceUrlKey, "config.workspaceUrlKey");
|
|
69
|
+
cachedUrlKey = validated;
|
|
70
|
+
return validated;
|
|
71
|
+
}
|
|
72
|
+
// 4. Live API lookup — requires a GraphQL service.
|
|
73
|
+
if (!graphQLService) {
|
|
74
|
+
throw new Error("Could not resolve Linear workspace URL key — no override, env, or config set, " +
|
|
75
|
+
"and no GraphQL service was provided for the live lookup. " +
|
|
76
|
+
`Set \`workspaceUrlKey\` in your el-linear config, or \`${WORKSPACE_URL_KEY_ENV}\` in the environment.`);
|
|
31
77
|
}
|
|
32
78
|
const data = await graphQLService.rawRequest(VIEWER_ORG_URL_KEY_QUERY);
|
|
33
79
|
const fetched = data?.viewer?.organization?.urlKey;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enrichlayer/el-linear",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
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
5
|
"main": "dist/main.js",
|
|
6
6
|
"types": "dist/main.d.ts",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@inquirer/prompts": "^8.4.2",
|
|
50
|
-
"@linear/sdk": "^
|
|
50
|
+
"@linear/sdk": "^84.0.0",
|
|
51
51
|
"commander": "^14.0.0",
|
|
52
52
|
"picocolors": "^1.1.1"
|
|
53
53
|
},
|