@enrichlayer/el-linear 1.4.0 → 1.6.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 +54 -0
- package/dist/auth/oauth-callback.d.ts +40 -0
- package/dist/auth/oauth-callback.js +142 -0
- package/dist/auth/oauth-client.d.ts +55 -0
- package/dist/auth/oauth-client.js +134 -0
- package/dist/auth/oauth-fs.d.ts +1 -0
- package/dist/auth/oauth-fs.js +29 -0
- package/dist/auth/oauth-headless.d.ts +38 -0
- package/dist/auth/oauth-headless.js +50 -0
- package/dist/auth/oauth-storage.d.ts +51 -0
- package/dist/auth/oauth-storage.js +87 -0
- package/dist/auth/oauth-token.d.ts +70 -0
- package/dist/auth/oauth-token.js +141 -0
- package/dist/auth/token-resolver.d.ts +48 -0
- package/dist/auth/token-resolver.js +95 -0
- package/dist/commands/attachments.js +11 -12
- package/dist/commands/batch.js +15 -14
- package/dist/commands/comments.js +22 -10
- package/dist/commands/cycles.js +5 -4
- package/dist/commands/documents.js +18 -15
- package/dist/commands/embeds.js +4 -6
- package/dist/commands/graphql.js +5 -4
- package/dist/commands/init/aliases.js +0 -14
- package/dist/commands/init/defaults.js +0 -6
- package/dist/commands/init/index.js +22 -12
- package/dist/commands/init/oauth.d.ts +85 -0
- package/dist/commands/init/oauth.js +308 -0
- package/dist/commands/init/shared.js +0 -1
- package/dist/commands/init/token.js +0 -7
- package/dist/commands/init/workspace.js +0 -2
- package/dist/commands/issue-id.js +3 -1
- package/dist/commands/issues.js +87 -46
- package/dist/commands/labels.js +9 -8
- package/dist/commands/profile/migrate-legacy.d.ts +96 -0
- package/dist/commands/profile/migrate-legacy.js +271 -0
- package/dist/commands/profile.js +6 -0
- package/dist/commands/project-milestones.js +13 -12
- package/dist/commands/projects.js +18 -14
- package/dist/commands/read-shortcut.js +8 -7
- package/dist/commands/refs.js +4 -3
- package/dist/commands/releases.js +9 -8
- package/dist/commands/search.js +3 -2
- package/dist/commands/teams.js +3 -2
- package/dist/commands/templates.js +5 -4
- package/dist/commands/users.js +3 -2
- package/dist/config/config.d.ts +18 -0
- package/dist/config/issue-validation.js +1 -1
- package/dist/config/resolver.js +1 -1
- package/dist/main.js +1 -1
- package/dist/utils/auth.js +8 -0
- package/dist/utils/download-uploads.d.ts +2 -1
- package/dist/utils/download-uploads.js +2 -4
- package/dist/utils/file-service.d.ts +26 -2
- package/dist/utils/file-service.js +27 -5
- package/dist/utils/footer.d.ts +19 -0
- package/dist/utils/footer.js +27 -0
- package/dist/utils/gdoc-parser.js +1 -1
- package/dist/utils/graphql-attachments-service.d.ts +1 -1
- package/dist/utils/graphql-attachments-service.js +2 -2
- package/dist/utils/graphql-documents-service.d.ts +1 -1
- package/dist/utils/graphql-documents-service.js +2 -2
- package/dist/utils/graphql-service.d.ts +18 -3
- package/dist/utils/graphql-service.js +26 -11
- package/dist/utils/legacy-config-detection.d.ts +47 -0
- package/dist/utils/legacy-config-detection.js +90 -0
- package/dist/utils/linear-service.d.ts +18 -3
- package/dist/utils/linear-service.js +21 -6
- package/dist/utils/markdown-prosemirror.js +16 -9
- package/dist/utils/migration-hint.d.ts +46 -0
- package/dist/utils/migration-hint.js +90 -0
- package/dist/utils/root-opts.d.ts +11 -0
- package/dist/utils/root-opts.js +13 -0
- package/dist/utils/validators.d.ts +7 -0
- package/dist/utils/validators.js +14 -14
- package/package.json +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects "legacy drift" — the on-disk state where a user upgraded el-linear
|
|
3
|
+
* to >=1.4.0 (named profiles) while their old `~/.config/el-linear/config.json`
|
|
4
|
+
* is still present but the legacy single-file `token` (and any per-profile
|
|
5
|
+
* token) is missing or unreadable. In that state every command fails with
|
|
6
|
+
* "No API token found" and the user has no clear migration path.
|
|
7
|
+
*
|
|
8
|
+
* This module is **pure detection** — it returns a discriminated state and
|
|
9
|
+
* nothing else. The hint emission lives in `migration-hint.ts` and is wired
|
|
10
|
+
* into the auth-failure path (`auth.ts`) so the user gets a single clear
|
|
11
|
+
* stderr line *before* the regular auth error fires.
|
|
12
|
+
*
|
|
13
|
+
* The state shape is intentionally a discriminated union so callers can match
|
|
14
|
+
* exhaustively without re-checking individual booleans:
|
|
15
|
+
*
|
|
16
|
+
* { kind: 'no-drift' }
|
|
17
|
+
* — healthy: legacy single-file layout *or* a working active profile.
|
|
18
|
+
*
|
|
19
|
+
* { kind: 'legacy-no-token' }
|
|
20
|
+
* — `config.json` exists but no token (legacy or per-profile) does. This
|
|
21
|
+
* is the post-upgrade case: 1.4.0 expects per-profile tokens; legacy
|
|
22
|
+
* config was never migrated.
|
|
23
|
+
*
|
|
24
|
+
* { kind: 'broken-active-profile' }
|
|
25
|
+
* — `active-profile` points at a name whose directory doesn't exist.
|
|
26
|
+
* Typically caused by an interrupted `profile remove` or a hand-edit.
|
|
27
|
+
*/
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { ACTIVE_PROFILE_FILE, CONFIG_PATH, PROFILES_DIR, TOKEN_PATH, } from "../config/paths.js";
|
|
31
|
+
const DEFAULT_FS_OPS = {
|
|
32
|
+
existsSync: (p) => fs.existsSync(p),
|
|
33
|
+
readFileSync: (p) => fs.readFileSync(p, "utf8"),
|
|
34
|
+
readdirSync: (p) => fs.readdirSync(p),
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Detect drift between the legacy single-file layout and the >=1.4 named-
|
|
38
|
+
* profiles layout. Pure — `fsImpl` is overridable so tests can drive every
|
|
39
|
+
* branch without touching the filesystem.
|
|
40
|
+
*/
|
|
41
|
+
export function detectLegacyDrift(fsImpl = DEFAULT_FS_OPS) {
|
|
42
|
+
// Branch 1: broken active-profile pointer.
|
|
43
|
+
// The `active-profile` marker names a profile that doesn't exist on disk —
|
|
44
|
+
// classic post-`profile remove` orphan, or a hand-edit typo. We classify
|
|
45
|
+
// this *before* the legacy-no-token branch because the user explicitly
|
|
46
|
+
// asked for that profile; the right fix is `profile use <good-name>`, not
|
|
47
|
+
// migration.
|
|
48
|
+
if (fsImpl.existsSync(ACTIVE_PROFILE_FILE)) {
|
|
49
|
+
let pointedAt = "";
|
|
50
|
+
try {
|
|
51
|
+
pointedAt = fsImpl.readFileSync(ACTIVE_PROFILE_FILE).trim();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
pointedAt = "";
|
|
55
|
+
}
|
|
56
|
+
if (pointedAt.length > 0) {
|
|
57
|
+
const profileDir = path.join(PROFILES_DIR, pointedAt);
|
|
58
|
+
if (!fsImpl.existsSync(profileDir)) {
|
|
59
|
+
return { kind: "broken-active-profile", pointedAt };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Branch 2: legacy drift.
|
|
64
|
+
// Legacy `config.json` exists; legacy `token` doesn't; no profiles configured.
|
|
65
|
+
// "No profiles configured" = profiles dir missing OR exists-but-empty.
|
|
66
|
+
if (!fsImpl.existsSync(CONFIG_PATH)) {
|
|
67
|
+
return { kind: "no-drift" };
|
|
68
|
+
}
|
|
69
|
+
if (fsImpl.existsSync(TOKEN_PATH)) {
|
|
70
|
+
return { kind: "no-drift" };
|
|
71
|
+
}
|
|
72
|
+
const profilesEmpty = isProfilesDirEmpty(fsImpl);
|
|
73
|
+
if (!profilesEmpty) {
|
|
74
|
+
return { kind: "no-drift" };
|
|
75
|
+
}
|
|
76
|
+
return { kind: "legacy-no-token", legacyConfigPath: CONFIG_PATH };
|
|
77
|
+
}
|
|
78
|
+
function isProfilesDirEmpty(fsImpl) {
|
|
79
|
+
if (!fsImpl.existsSync(PROFILES_DIR))
|
|
80
|
+
return true;
|
|
81
|
+
try {
|
|
82
|
+
const entries = fsImpl.readdirSync(PROFILES_DIR);
|
|
83
|
+
return entries.length === 0;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Unreadable dir — treat as empty for the purposes of drift detection
|
|
87
|
+
// so we don't suppress the hint on a permissions edge case.
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import type { LinearComment, LinearCycleDetail, LinearCycleSummary, LinearLabel, LinearProject, LinearTeam, LinearUser } from "../types/linear.js";
|
|
2
|
-
import {
|
|
2
|
+
import type { AuthOptions } from "./auth.js";
|
|
3
|
+
/**
|
|
4
|
+
* Constructor arg shapes for `LinearService`. Three variants:
|
|
5
|
+
* - `string` → personal API token (legacy; sent without `Bearer` prefix).
|
|
6
|
+
* - `{apiKey: string}` → personal API token (explicit).
|
|
7
|
+
* - `{oauthToken: string}` → OAuth access token (sent as
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
export type LinearServiceAuth = string | {
|
|
14
|
+
apiKey: string;
|
|
15
|
+
} | {
|
|
16
|
+
oauthToken: string;
|
|
17
|
+
};
|
|
3
18
|
export declare class LinearService {
|
|
4
19
|
private readonly client;
|
|
5
|
-
constructor(
|
|
20
|
+
constructor(auth: LinearServiceAuth);
|
|
6
21
|
resolveIssueId(issueId: string): Promise<string>;
|
|
7
22
|
getTeams(limit?: number): Promise<LinearTeam[]>;
|
|
8
23
|
resolveUserId(nameOrEmailOrId: string): Promise<string>;
|
|
@@ -23,4 +38,4 @@ export declare class LinearService {
|
|
|
23
38
|
resolveCycleId(cycleNameOrId: string, teamFilter?: string): Promise<string>;
|
|
24
39
|
resolveProjectId(projectNameOrId: string): Promise<string>;
|
|
25
40
|
}
|
|
26
|
-
export declare function createLinearService(options: AuthOptions): LinearService
|
|
41
|
+
export declare function createLinearService(options: AuthOptions): Promise<LinearService>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LinearClient } from "@linear/sdk";
|
|
2
|
+
import { getActiveAuth } from "../auth/token-resolver.js";
|
|
2
3
|
import { resolveUserDisplayName } from "../config/resolver.js";
|
|
3
|
-
import { getApiToken } from "./auth.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";
|
|
@@ -21,10 +21,22 @@ function teamIdFilter(teamId) {
|
|
|
21
21
|
function nonEmptyFilter(filter) {
|
|
22
22
|
return Object.keys(filter).length > 0 ? filter : undefined;
|
|
23
23
|
}
|
|
24
|
+
function buildLinearClient(auth) {
|
|
25
|
+
if (typeof auth === "string") {
|
|
26
|
+
return new LinearClient({ apiKey: auth });
|
|
27
|
+
}
|
|
28
|
+
if ("oauthToken" in auth) {
|
|
29
|
+
// Linear's SDK natively supports OAuth via the `accessToken` option,
|
|
30
|
+
// which causes the underlying transport to send
|
|
31
|
+
// `Authorization: Bearer <token>` instead of the personal-token shape.
|
|
32
|
+
return new LinearClient({ accessToken: auth.oauthToken });
|
|
33
|
+
}
|
|
34
|
+
return new LinearClient({ apiKey: auth.apiKey });
|
|
35
|
+
}
|
|
24
36
|
export class LinearService {
|
|
25
37
|
client;
|
|
26
|
-
constructor(
|
|
27
|
-
this.client =
|
|
38
|
+
constructor(auth) {
|
|
39
|
+
this.client = buildLinearClient(auth);
|
|
28
40
|
}
|
|
29
41
|
async resolveIssueId(issueId) {
|
|
30
42
|
if (isUuid(issueId)) {
|
|
@@ -436,7 +448,10 @@ export class LinearService {
|
|
|
436
448
|
return projectsConnection.nodes[0].id;
|
|
437
449
|
}
|
|
438
450
|
}
|
|
439
|
-
export function createLinearService(options) {
|
|
440
|
-
const
|
|
441
|
-
|
|
451
|
+
export async function createLinearService(options) {
|
|
452
|
+
const auth = await getActiveAuth(options);
|
|
453
|
+
if (auth.kind === "oauth") {
|
|
454
|
+
return new LinearService({ oauthToken: auth.token });
|
|
455
|
+
}
|
|
456
|
+
return new LinearService({ apiKey: auth.token });
|
|
442
457
|
}
|
|
@@ -106,8 +106,11 @@ function parseBulletList(state, line) {
|
|
|
106
106
|
return false;
|
|
107
107
|
}
|
|
108
108
|
const items = [];
|
|
109
|
-
while (state.i < state.lines.length
|
|
109
|
+
while (state.i < state.lines.length) {
|
|
110
110
|
const m = state.lines[state.i].match(BULLET_RE);
|
|
111
|
+
if (!m) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
111
114
|
items.push({
|
|
112
115
|
type: "listItem",
|
|
113
116
|
content: [{ type: "paragraph", content: parseInline(m[1]) }],
|
|
@@ -122,9 +125,11 @@ function parseOrderedList(state, line) {
|
|
|
122
125
|
return false;
|
|
123
126
|
}
|
|
124
127
|
const items = [];
|
|
125
|
-
while (state.i < state.lines.length
|
|
126
|
-
ORDERED_RE.test(state.lines[state.i])) {
|
|
128
|
+
while (state.i < state.lines.length) {
|
|
127
129
|
const m = state.lines[state.i].match(ORDERED_RE);
|
|
130
|
+
if (!m) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
128
133
|
items.push({
|
|
129
134
|
type: "listItem",
|
|
130
135
|
content: [{ type: "paragraph", content: parseInline(m[1]) }],
|
|
@@ -139,9 +144,11 @@ function parseBlockquote(state) {
|
|
|
139
144
|
return false;
|
|
140
145
|
}
|
|
141
146
|
const quoteLines = [];
|
|
142
|
-
while (state.i < state.lines.length
|
|
143
|
-
BLOCKQUOTE_RE.test(state.lines[state.i])) {
|
|
147
|
+
while (state.i < state.lines.length) {
|
|
144
148
|
const m = state.lines[state.i].match(BLOCKQUOTE_RE);
|
|
149
|
+
if (!m) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
145
152
|
quoteLines.push(m[1]);
|
|
146
153
|
state.i++;
|
|
147
154
|
}
|
|
@@ -283,7 +290,7 @@ function findEarliestInlineMatch(text) {
|
|
|
283
290
|
const codeMatch = text.match(INLINE_CODE_RE);
|
|
284
291
|
if (codeMatch) {
|
|
285
292
|
candidates.push({
|
|
286
|
-
index: codeMatch.index,
|
|
293
|
+
index: codeMatch.index ?? 0,
|
|
287
294
|
length: codeMatch[0].length,
|
|
288
295
|
innerText: codeMatch[1],
|
|
289
296
|
marks: [{ type: "code" }],
|
|
@@ -292,7 +299,7 @@ function findEarliestInlineMatch(text) {
|
|
|
292
299
|
const linkMatch = text.match(INLINE_LINK_RE);
|
|
293
300
|
if (linkMatch) {
|
|
294
301
|
candidates.push({
|
|
295
|
-
index: linkMatch.index,
|
|
302
|
+
index: linkMatch.index ?? 0,
|
|
296
303
|
length: linkMatch[0].length,
|
|
297
304
|
innerText: linkMatch[1],
|
|
298
305
|
marks: [{ type: "link", attrs: { href: linkMatch[2] } }],
|
|
@@ -301,7 +308,7 @@ function findEarliestInlineMatch(text) {
|
|
|
301
308
|
const boldMatch = text.match(INLINE_BOLD_RE);
|
|
302
309
|
if (boldMatch) {
|
|
303
310
|
candidates.push({
|
|
304
|
-
index: boldMatch.index,
|
|
311
|
+
index: boldMatch.index ?? 0,
|
|
305
312
|
length: boldMatch[0].length,
|
|
306
313
|
innerText: boldMatch[1] ?? boldMatch[2],
|
|
307
314
|
marks: [{ type: "bold" }],
|
|
@@ -310,7 +317,7 @@ function findEarliestInlineMatch(text) {
|
|
|
310
317
|
const italicMatch = text.match(INLINE_ITALIC_RE);
|
|
311
318
|
if (italicMatch) {
|
|
312
319
|
candidates.push({
|
|
313
|
-
index: italicMatch.index,
|
|
320
|
+
index: italicMatch.index ?? 0,
|
|
314
321
|
length: italicMatch[0].length,
|
|
315
322
|
innerText: italicMatch[1] ?? italicMatch[2],
|
|
316
323
|
marks: [{ type: "italic" }],
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Once-per-process stderr hint for legacy-config drift.
|
|
3
|
+
*
|
|
4
|
+
* Wired into the auth-failure path in `auth.ts`. When the legacy single-file
|
|
5
|
+
* config exists but no token can be found anywhere (the >=1.4 post-upgrade
|
|
6
|
+
* scenario), we emit one stderr line pointing the user at
|
|
7
|
+
* `el-linear profile migrate-legacy` *before* the regular "No API token
|
|
8
|
+
* found" error fires.
|
|
9
|
+
*
|
|
10
|
+
* Constraints:
|
|
11
|
+
*
|
|
12
|
+
* - **stderr only** — stdout is reserved for the JSON error payload that
|
|
13
|
+
* machine callers parse, and the hint must not corrupt that stream.
|
|
14
|
+
* - **Once per process** — even if a script invokes el-linear in a tight
|
|
15
|
+
* loop within a single Node process (uncommon but possible), only the
|
|
16
|
+
* first failure prints. Anything more is noise.
|
|
17
|
+
* - **Suppressible** — `EL_LINEAR_SKIP_MIGRATION_HINT=1` silences the hint
|
|
18
|
+
* for users who've decided to stay on the legacy layout intentionally.
|
|
19
|
+
* The env var is read at *emission time*, not module load, so toggling
|
|
20
|
+
* it in tests works without re-importing.
|
|
21
|
+
* - **Non-blocking** — never throws, never delays the underlying auth
|
|
22
|
+
* error. The hint is purely informational.
|
|
23
|
+
*/
|
|
24
|
+
import { type DetectionFsOps, type LegacyDriftState } from "./legacy-config-detection.js";
|
|
25
|
+
/**
|
|
26
|
+
* Test seam — resets the once-per-process latch so each test starts fresh.
|
|
27
|
+
* Production code never calls this.
|
|
28
|
+
*/
|
|
29
|
+
export declare function _resetMigrationHintForTests(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Emit the migration hint to stderr if (and only if) legacy drift is
|
|
32
|
+
* detected, the latch hasn't fired this process, and the suppress env var
|
|
33
|
+
* isn't set. Returns the detection state so callers can branch (or log).
|
|
34
|
+
*
|
|
35
|
+
* Optional `fsImpl` lets tests drive detection without touching disk.
|
|
36
|
+
* Optional `stderr` lets tests capture the output without spying on the
|
|
37
|
+
* global stream.
|
|
38
|
+
*/
|
|
39
|
+
export declare function maybeEmitMigrationHint(fsImpl?: DetectionFsOps, stderr?: {
|
|
40
|
+
write: (chunk: string) => void;
|
|
41
|
+
}): LegacyDriftState;
|
|
42
|
+
/**
|
|
43
|
+
* Render the hint string for a given state. Returns null when no hint
|
|
44
|
+
* should be emitted (no-drift). Exported for unit testing.
|
|
45
|
+
*/
|
|
46
|
+
export declare function formatHint(state: LegacyDriftState): string | null;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Once-per-process stderr hint for legacy-config drift.
|
|
3
|
+
*
|
|
4
|
+
* Wired into the auth-failure path in `auth.ts`. When the legacy single-file
|
|
5
|
+
* config exists but no token can be found anywhere (the >=1.4 post-upgrade
|
|
6
|
+
* scenario), we emit one stderr line pointing the user at
|
|
7
|
+
* `el-linear profile migrate-legacy` *before* the regular "No API token
|
|
8
|
+
* found" error fires.
|
|
9
|
+
*
|
|
10
|
+
* Constraints:
|
|
11
|
+
*
|
|
12
|
+
* - **stderr only** — stdout is reserved for the JSON error payload that
|
|
13
|
+
* machine callers parse, and the hint must not corrupt that stream.
|
|
14
|
+
* - **Once per process** — even if a script invokes el-linear in a tight
|
|
15
|
+
* loop within a single Node process (uncommon but possible), only the
|
|
16
|
+
* first failure prints. Anything more is noise.
|
|
17
|
+
* - **Suppressible** — `EL_LINEAR_SKIP_MIGRATION_HINT=1` silences the hint
|
|
18
|
+
* for users who've decided to stay on the legacy layout intentionally.
|
|
19
|
+
* The env var is read at *emission time*, not module load, so toggling
|
|
20
|
+
* it in tests works without re-importing.
|
|
21
|
+
* - **Non-blocking** — never throws, never delays the underlying auth
|
|
22
|
+
* error. The hint is purely informational.
|
|
23
|
+
*/
|
|
24
|
+
import { detectLegacyDrift, } from "./legacy-config-detection.js";
|
|
25
|
+
let hintAlreadyEmitted = false;
|
|
26
|
+
/**
|
|
27
|
+
* Test seam — resets the once-per-process latch so each test starts fresh.
|
|
28
|
+
* Production code never calls this.
|
|
29
|
+
*/
|
|
30
|
+
export function _resetMigrationHintForTests() {
|
|
31
|
+
hintAlreadyEmitted = false;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Emit the migration hint to stderr if (and only if) legacy drift is
|
|
35
|
+
* detected, the latch hasn't fired this process, and the suppress env var
|
|
36
|
+
* isn't set. Returns the detection state so callers can branch (or log).
|
|
37
|
+
*
|
|
38
|
+
* Optional `fsImpl` lets tests drive detection without touching disk.
|
|
39
|
+
* Optional `stderr` lets tests capture the output without spying on the
|
|
40
|
+
* global stream.
|
|
41
|
+
*/
|
|
42
|
+
export function maybeEmitMigrationHint(fsImpl, stderr = process.stderr) {
|
|
43
|
+
const state = detectLegacyDrift(fsImpl);
|
|
44
|
+
// Read the env var at call time — the suppress flag may be flipped between
|
|
45
|
+
// commands in long-lived test processes.
|
|
46
|
+
if (process.env.EL_LINEAR_SKIP_MIGRATION_HINT === "1") {
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
if (hintAlreadyEmitted) {
|
|
50
|
+
return state;
|
|
51
|
+
}
|
|
52
|
+
const message = formatHint(state);
|
|
53
|
+
if (message === null) {
|
|
54
|
+
return state;
|
|
55
|
+
}
|
|
56
|
+
hintAlreadyEmitted = true;
|
|
57
|
+
stderr.write(`${message}\n`);
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Render the hint string for a given state. Returns null when no hint
|
|
62
|
+
* should be emitted (no-drift). Exported for unit testing.
|
|
63
|
+
*/
|
|
64
|
+
export function formatHint(state) {
|
|
65
|
+
if (state.kind === "no-drift")
|
|
66
|
+
return null;
|
|
67
|
+
if (state.kind === "broken-active-profile") {
|
|
68
|
+
return [
|
|
69
|
+
`el-linear: active profile "${state.pointedAt}" doesn't exist.`,
|
|
70
|
+
"Switch with:",
|
|
71
|
+
"",
|
|
72
|
+
" el-linear profile use <name>",
|
|
73
|
+
"",
|
|
74
|
+
"Or list available profiles:",
|
|
75
|
+
"",
|
|
76
|
+
" el-linear profile list",
|
|
77
|
+
"",
|
|
78
|
+
"Or suppress this hint with EL_LINEAR_SKIP_MIGRATION_HINT=1.",
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
81
|
+
// legacy-no-token
|
|
82
|
+
return [
|
|
83
|
+
`el-linear: legacy config detected at ${state.legacyConfigPath}`,
|
|
84
|
+
"but no token. Migrate with:",
|
|
85
|
+
"",
|
|
86
|
+
" el-linear profile migrate-legacy [--name <profile>]",
|
|
87
|
+
"",
|
|
88
|
+
"Or suppress this hint with EL_LINEAR_SKIP_MIGRATION_HINT=1.",
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Command, OptionValues } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Extract root command options from inside a subcommand action handler.
|
|
4
|
+
*
|
|
5
|
+
* Commander guarantees `command.parent` exists inside any subcommand action,
|
|
6
|
+
* and `command.parent.parent` exists inside a nested subcommand action
|
|
7
|
+
* (e.g. `el-linear comments create` — the `create` command is nested under
|
|
8
|
+
* `comments`, which is nested under the root program). The non-null
|
|
9
|
+
* assertions live here so call sites can stay clean and typed.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getRootOpts(command: Command): OptionValues;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract root command options from inside a subcommand action handler.
|
|
3
|
+
*
|
|
4
|
+
* Commander guarantees `command.parent` exists inside any subcommand action,
|
|
5
|
+
* and `command.parent.parent` exists inside a nested subcommand action
|
|
6
|
+
* (e.g. `el-linear comments create` — the `create` command is nested under
|
|
7
|
+
* `comments`, which is nested under the root program). The non-null
|
|
8
|
+
* assertions live here so call sites can stay clean and typed.
|
|
9
|
+
*/
|
|
10
|
+
export function getRootOpts(command) {
|
|
11
|
+
// biome-ignore lint/style/noNonNullAssertion: command.parent / .parent.parent are guaranteed by commander inside subcommand actions; see docstring
|
|
12
|
+
return (command.parent?.parent ?? command.parent).opts();
|
|
13
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export declare function parsePositiveInt(value: string, flagName: string): number;
|
|
2
|
+
/**
|
|
3
|
+
* Parse a single priority value (for `issues create --priority`, `issues update --priority`).
|
|
4
|
+
*
|
|
5
|
+
* Accepts:
|
|
6
|
+
* - keywords: none | urgent | high | medium | normal | low
|
|
7
|
+
* - numbers: 0 (no priority), 1 (urgent), 2 (high), 3 (medium), 4 (low)
|
|
8
|
+
*/
|
|
2
9
|
export declare function validatePriority(value: string): number;
|
|
3
10
|
export declare function validateHexColor(value: string): string;
|
|
4
11
|
export declare function validateIsoDate(value: string): string;
|
package/dist/utils/validators.js
CHANGED
|
@@ -8,14 +8,21 @@ export function parsePositiveInt(value, flagName) {
|
|
|
8
8
|
}
|
|
9
9
|
return n;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Parse a single priority value (for `issues create --priority`, `issues update --priority`).
|
|
13
|
+
*
|
|
14
|
+
* Accepts:
|
|
15
|
+
* - keywords: none | urgent | high | medium | normal | low
|
|
16
|
+
* - numbers: 0 (no priority), 1 (urgent), 2 (high), 3 (medium), 4 (low)
|
|
17
|
+
*/
|
|
11
18
|
export function validatePriority(value) {
|
|
12
19
|
const asName = PRIORITY_NAMES[value.toLowerCase()];
|
|
13
|
-
if (asName !== undefined
|
|
20
|
+
if (asName !== undefined) {
|
|
14
21
|
return asName;
|
|
15
22
|
}
|
|
16
23
|
const n = Number.parseInt(value, 10);
|
|
17
|
-
if (Number.isNaN(n) || n <
|
|
18
|
-
throw invalidParameterError("--priority", `"${value}" is not valid. Use names (urgent, high, medium/normal, low) or numbers (
|
|
24
|
+
if (Number.isNaN(n) || n < 0 || n > 4) {
|
|
25
|
+
throw invalidParameterError("--priority", `"${value}" is not valid. Use names (none, urgent, high, medium/normal, low) or numbers (0-4).`);
|
|
19
26
|
}
|
|
20
27
|
return n;
|
|
21
28
|
}
|
|
@@ -41,6 +48,9 @@ export function splitList(value) {
|
|
|
41
48
|
.map((s) => s.trim())
|
|
42
49
|
.filter((s) => s.length > 0);
|
|
43
50
|
}
|
|
51
|
+
// Shared keyword → Linear priority number map. `none` and `0` mean "No priority"
|
|
52
|
+
// (Linear stores it as a real state, not absence). `1..4` are the rated priorities,
|
|
53
|
+
// `urgent` (1) being the highest.
|
|
44
54
|
const PRIORITY_NAMES = {
|
|
45
55
|
none: 0,
|
|
46
56
|
urgent: 1,
|
|
@@ -50,17 +60,7 @@ const PRIORITY_NAMES = {
|
|
|
50
60
|
low: 4,
|
|
51
61
|
};
|
|
52
62
|
export function parsePriorityFilter(value) {
|
|
53
|
-
return splitList(value).map((item) =>
|
|
54
|
-
const asName = PRIORITY_NAMES[item.toLowerCase()];
|
|
55
|
-
if (asName !== undefined) {
|
|
56
|
-
return asName;
|
|
57
|
-
}
|
|
58
|
-
const n = Number.parseInt(item, 10);
|
|
59
|
-
if (Number.isNaN(n) || n < 0 || n > 4) {
|
|
60
|
-
throw invalidParameterError("--priority", `"${item}" is not valid. Use names (urgent, high, medium, low, none) or numbers (0-4).`);
|
|
61
|
-
}
|
|
62
|
-
return n;
|
|
63
|
-
});
|
|
63
|
+
return splitList(value).map((item) => validatePriority(item));
|
|
64
64
|
}
|
|
65
65
|
export const PRIORITY_LABELS = {
|
|
66
66
|
0: "No priority",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enrichlayer/el-linear",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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",
|