@enrichlayer/el-linear 1.10.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 +126 -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/batch.js +18 -21
- package/dist/commands/comments.js +5 -5
- package/dist/commands/config.js +178 -5
- 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/issues/branch.js +9 -1
- package/dist/commands/issues/relations.d.ts +3 -14
- package/dist/commands/issues/relations.js +3 -3
- package/dist/commands/issues.js +222 -43
- package/dist/commands/labels.js +2 -1
- package/dist/commands/profile.js +1 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +91 -7
- package/dist/commands/read-shortcut.d.ts +1 -1
- package/dist/commands/read-shortcut.js +28 -8
- package/dist/commands/refs.js +67 -8
- package/dist/commands/search.js +30 -5
- package/dist/commands/users.js +4 -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 +9 -2
- package/dist/main.js +13 -1
- package/dist/queries/comments-types.d.ts +15 -9
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +8 -0
- package/dist/queries/documents-types.d.ts +4 -3
- package/dist/queries/introspect-types.d.ts +8 -7
- package/dist/queries/issues-types.d.ts +92 -27
- package/dist/queries/issues.d.ts +49 -10
- package/dist/queries/issues.js +125 -5
- package/dist/queries/labels-types.d.ts +7 -6
- package/dist/queries/project-milestones-types.d.ts +5 -4
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/queries/projects-types.d.ts +8 -7
- package/dist/queries/releases-types.d.ts +5 -4
- package/dist/queries/search-types.d.ts +28 -12
- package/dist/queries/templates-types.d.ts +3 -2
- package/dist/types/linear.d.ts +13 -1
- package/dist/utils/auto-link-references.d.ts +3 -3
- package/dist/utils/auto-link-references.js +1 -10
- 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-issues-service.d.ts +101 -45
- package/dist/utils/graphql-issues-service.js +252 -39
- 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 +81 -3
- package/dist/utils/output.js +61 -6
- 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.js +20 -4
- package/package.json +2 -2
|
@@ -5,7 +5,7 @@ import { cached, resolveCacheTTL } from "../utils/disk-cache.js";
|
|
|
5
5
|
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
6
6
|
import { createLinearService } from "../utils/linear-service.js";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
|
-
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
8
|
+
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
9
9
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
10
10
|
import { renderCsv, renderFixedWidthTable, renderMarkdownTable, } from "../utils/table-formatter.js";
|
|
11
11
|
import { isUuid } from "../utils/uuid.js";
|
|
@@ -18,6 +18,58 @@ const VALID_PROJECT_STATES = new Set([
|
|
|
18
18
|
"completed",
|
|
19
19
|
"canceled",
|
|
20
20
|
]);
|
|
21
|
+
/**
|
|
22
|
+
* Display ordering for `projects list`: active work first, terminal states
|
|
23
|
+
* last. Without this, the SDK's `updatedAt` order surfaces recently-touched
|
|
24
|
+
* completed/canceled projects ahead of in-progress ones — so an active project
|
|
25
|
+
* can fall past the `--limit` window and an agent concludes it does not exist
|
|
26
|
+
* (the DEV-4175 silent-truncation failure). Stable sort preserves `updatedAt`
|
|
27
|
+
* within each rank.
|
|
28
|
+
*
|
|
29
|
+
* `Linear` exposes additional internal states (e.g. archived) that we don't
|
|
30
|
+
* enumerate here; anything not in this table sorts to the end via
|
|
31
|
+
* `UNRANKED_STATE`.
|
|
32
|
+
*/
|
|
33
|
+
const PROJECT_STATE_RANK = {
|
|
34
|
+
started: 0,
|
|
35
|
+
planned: 1,
|
|
36
|
+
paused: 2,
|
|
37
|
+
backlog: 3,
|
|
38
|
+
completed: 4,
|
|
39
|
+
canceled: 5,
|
|
40
|
+
};
|
|
41
|
+
// Sentinel for any state Linear surfaces that isn't in the rank table above
|
|
42
|
+
// (e.g. `archived`, future additions). Number.MAX_SAFE_INTEGER reads more
|
|
43
|
+
// deliberately than a magic `9` and stays correct if PROJECT_STATE_RANK grows.
|
|
44
|
+
const UNRANKED_STATE = Number.MAX_SAFE_INTEGER;
|
|
45
|
+
export function sortActiveFirst(projects) {
|
|
46
|
+
return [...projects].sort((a, b) => (PROJECT_STATE_RANK[a.state ?? ""] ?? UNRANKED_STATE) -
|
|
47
|
+
(PROJECT_STATE_RANK[b.state ?? ""] ?? UNRANKED_STATE));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Emit a truncation warning when `projects list` returned exactly `--limit`
|
|
51
|
+
* rows (the same `count === limit` heuristic as `warnIfTruncated` in
|
|
52
|
+
* `output.ts`). JSON buffers it into `_warnings` so the payload stays a single
|
|
53
|
+
* parseable object; every human-facing format prints to stderr so it can't be
|
|
54
|
+
* silently scrolled past. DEV-4175.
|
|
55
|
+
*
|
|
56
|
+
* The exact total is deliberately omitted from the message. Returning it would
|
|
57
|
+
* require a second `client.projects({ first: 0 })` round-trip (the SDK has no
|
|
58
|
+
* `count`-only helper) or a full pagination walk — both negate the cheap-page
|
|
59
|
+
* default. `--all` in the hint gives the caller a deterministic way to learn
|
|
60
|
+
* the real total without us guessing, so the absence is a feature, not a gap.
|
|
61
|
+
*/
|
|
62
|
+
function warnProjectsTruncated(count, limit, format) {
|
|
63
|
+
const msg = `results_truncated: returned ${count} projects matching --limit ${limit}; ` +
|
|
64
|
+
"more may exist. Re-run with --all to fetch every project " +
|
|
65
|
+
`(or --limit ${limit * 2}, or narrow with --name / --state / --team).`;
|
|
66
|
+
if (format === "json") {
|
|
67
|
+
outputWarning(msg);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
logger.error(msg);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
21
73
|
function parseStateList(value, flagName) {
|
|
22
74
|
const states = splitList(value).map((s) => s.toLowerCase());
|
|
23
75
|
for (const s of states) {
|
|
@@ -390,13 +442,22 @@ export function setupProjectsCommands(program) {
|
|
|
390
442
|
.option("--format <format>", "output format (json, summary, table, md, csv)", "json")
|
|
391
443
|
.option("--fields <fields>", "columns for table/csv (comma-separated: name,state,progress,teams,lead,targetDate)")
|
|
392
444
|
.option("--name <substring>", "filter by case-insensitive substring on project name")
|
|
445
|
+
.option("--team <key|name>", "filter to projects belonging to a specific team (e.g. DEV, FE)")
|
|
393
446
|
.option("--state <names>", "include only projects in these states (comma-separated: backlog, planned, started, paused, completed, canceled)")
|
|
394
447
|
.option("--exclude-state <names>", "exclude projects in these states (comma-separated). Mutually exclusive with --state.")
|
|
395
448
|
.option("--active", "shorthand for --exclude-state completed,canceled (mutually exclusive with --state / --exclude-state)")
|
|
449
|
+
.option("--all", "fetch every project (paginates fully). Equivalent to --limit 0; overrides --limit when both are given.")
|
|
396
450
|
.action(handleAsyncCommand(async (options, command) => {
|
|
397
451
|
const rootOpts = getRootOpts(command);
|
|
398
|
-
|
|
452
|
+
// `--all` / `--limit 0` mean unlimited. Stored as `limit = 0`,
|
|
453
|
+
// which `LinearService.getProjects` interprets as "paginate
|
|
454
|
+
// fully". DEV-4175.
|
|
455
|
+
const unlimited = options.all === true || options.limit === "0";
|
|
456
|
+
const limit = unlimited
|
|
457
|
+
? 0
|
|
458
|
+
: parsePositiveInt(options.limit, "--limit");
|
|
399
459
|
const nameFilter = options.name;
|
|
460
|
+
const teamFilter = options.team;
|
|
400
461
|
const stateFilter = resolveProjectStateFilter(options);
|
|
401
462
|
const ttl = resolveCacheTTL({
|
|
402
463
|
configTTL: loadConfig().cacheTTLSeconds,
|
|
@@ -406,31 +467,54 @@ export function setupProjectsCommands(program) {
|
|
|
406
467
|
// don't collide.
|
|
407
468
|
const cacheKey = `projects-list-limit:${limit}` +
|
|
408
469
|
`-name:${nameFilter ?? "_all"}` +
|
|
470
|
+
`-team:${teamFilter ?? "_all"}` +
|
|
409
471
|
`-states:${stateFilter.states?.join(",") ?? "_any"}` +
|
|
410
472
|
`-excl:${stateFilter.excludeStates?.join(",") ?? "_none"}`;
|
|
411
473
|
const result = await cached(cacheKey, ttl, async () => {
|
|
412
474
|
const service = await createLinearService(rootOpts);
|
|
475
|
+
let teamId;
|
|
476
|
+
if (teamFilter) {
|
|
477
|
+
const resolved = resolveTeam(teamFilter);
|
|
478
|
+
teamId = await service.resolveTeamId(resolved);
|
|
479
|
+
}
|
|
413
480
|
return service.getProjects(limit, {
|
|
414
481
|
nameFilter,
|
|
482
|
+
teamId,
|
|
415
483
|
states: stateFilter.states,
|
|
416
484
|
excludeStates: stateFilter.excludeStates,
|
|
417
485
|
});
|
|
418
486
|
});
|
|
487
|
+
// Active-first sort: keeps started/planned/paused/backlog
|
|
488
|
+
// ahead of completed/canceled in every format, so the active
|
|
489
|
+
// set always fits within `--limit`. DEV-4175.
|
|
490
|
+
const sorted = sortActiveFirst(result);
|
|
419
491
|
const format = options.format;
|
|
420
|
-
|
|
492
|
+
const isTabular = format === "table" ||
|
|
421
493
|
format === "md" ||
|
|
422
494
|
format === "markdown" ||
|
|
423
|
-
format === "csv"
|
|
495
|
+
format === "csv";
|
|
496
|
+
// Emit the truncation warning BEFORE outputting: the JSON
|
|
497
|
+
// branch buffers into `_warnings`, which the next
|
|
498
|
+
// `outputSuccess()` call drains. All other formats route to
|
|
499
|
+
// stderr via `logger.error` so the visible payload (table,
|
|
500
|
+
// markdown, summary) stays clean.
|
|
501
|
+
if (!unlimited && sorted.length === limit) {
|
|
502
|
+
warnProjectsTruncated(sorted.length, limit, format);
|
|
503
|
+
}
|
|
504
|
+
if (isTabular) {
|
|
424
505
|
const fieldList = options.fields
|
|
425
506
|
? splitList(options.fields)
|
|
426
507
|
: undefined;
|
|
427
|
-
formatProjectsOutput(
|
|
508
|
+
formatProjectsOutput(sorted, format, fieldList);
|
|
428
509
|
if (format === "table") {
|
|
429
|
-
logger.info(`\n${
|
|
510
|
+
logger.info(`\n${sorted.length} projects`);
|
|
430
511
|
}
|
|
431
512
|
}
|
|
432
513
|
else {
|
|
433
|
-
outputSuccess({
|
|
514
|
+
outputSuccess({
|
|
515
|
+
data: sorted,
|
|
516
|
+
meta: { count: sorted.length },
|
|
517
|
+
});
|
|
434
518
|
}
|
|
435
519
|
}));
|
|
436
520
|
projects
|
|
@@ -9,4 +9,4 @@ export declare function setupReadShortcut(program: Command): void;
|
|
|
9
9
|
* `issues read` (in `commands/issues.ts`) both call one implementation
|
|
10
10
|
* instead of carrying byte-equivalent duplicates. ALL-938 cleanup.
|
|
11
11
|
*/
|
|
12
|
-
export declare function readIssues(issueIds: string[],
|
|
12
|
+
export declare function readIssues(issueIds: string[], options: Record<string, unknown>, command: Command): Promise<void>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { downloadLinearUploads } from "../utils/download-uploads.js";
|
|
2
|
+
import { extractField } from "../utils/extract-field.js";
|
|
2
3
|
import { createFileService } from "../utils/file-service.js";
|
|
3
|
-
import {
|
|
4
|
-
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
5
|
-
import { createLinearService } from "../utils/linear-service.js";
|
|
4
|
+
import { createIssuesService } from "../utils/issues-service-bootstrap.js";
|
|
6
5
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
7
6
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
8
7
|
/**
|
|
@@ -21,7 +20,10 @@ export function setupReadShortcut(program) {
|
|
|
21
20
|
.alias("view")
|
|
22
21
|
.alias("show")
|
|
23
22
|
.description("Shortcut for `issues read`. Get issue details by identifier.")
|
|
24
|
-
.
|
|
23
|
+
.option("--field <name>", 'Extract a single named section from the issue description (e.g. "Done when"). ' +
|
|
24
|
+
"Matches H2/H3 headers and bold pseudo-headers case-insensitively. " +
|
|
25
|
+
"Outputs the section text only — no JSON envelope.")
|
|
26
|
+
.addHelpText("after", '\nExamples:\n el-linear read ADM-652\n el-linear get DEV-123 DEV-456\n el-linear ADM-652 (auto-detected)\n el-linear read DEV-123 --field "Done when" (just that section)')
|
|
25
27
|
.action(handleAsyncCommand(readIssues));
|
|
26
28
|
// Catch-all: if argv looks like `el-linear ADM-652 [DEV-123 ...]`, run read
|
|
27
29
|
const originalParse = program.parse.bind(program);
|
|
@@ -54,15 +56,33 @@ export function setupReadShortcut(program) {
|
|
|
54
56
|
* `issues read` (in `commands/issues.ts`) both call one implementation
|
|
55
57
|
* instead of carrying byte-equivalent duplicates. ALL-938 cleanup.
|
|
56
58
|
*/
|
|
57
|
-
export async function readIssues(issueIds,
|
|
59
|
+
export async function readIssues(issueIds, options, command) {
|
|
58
60
|
const rootOpts = getRootOpts(command);
|
|
59
|
-
const
|
|
60
|
-
const linearService = await createLinearService(rootOpts);
|
|
61
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
61
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
62
62
|
const fileService = await createFileService(rootOpts);
|
|
63
|
+
const fieldName = typeof options.field === "string" ? options.field : null;
|
|
64
|
+
// --field is single-issue only. With multiple issues, a section
|
|
65
|
+
// extraction can't sensibly fan out to N different bodies — the
|
|
66
|
+
// caller almost always wants one section from one issue.
|
|
67
|
+
if (fieldName && issueIds.length > 1) {
|
|
68
|
+
throw new Error("--field is single-issue only; pass exactly one issueId. " +
|
|
69
|
+
"For multiple issues, drop --field and use --jq or --format summary.");
|
|
70
|
+
}
|
|
63
71
|
if (issueIds.length === 1) {
|
|
64
72
|
const issue = await issuesService.getIssueById(issueIds[0]);
|
|
65
73
|
const resolved = await downloadLinearUploads(issue, fileService);
|
|
74
|
+
if (fieldName) {
|
|
75
|
+
const section = extractField(resolved.description ?? "", fieldName);
|
|
76
|
+
if (section === null) {
|
|
77
|
+
// Print nothing to stdout, exit non-zero with a stderr hint.
|
|
78
|
+
// Mirrors `grep` semantics for "not found" — scripts can
|
|
79
|
+
// branch on the exit code.
|
|
80
|
+
process.stderr.write(`el-linear: section "${fieldName}" not found in ${resolved.identifier}'s description\n`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
process.stdout.write(`${section}\n`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
66
86
|
outputSuccess(resolved);
|
|
67
87
|
}
|
|
68
88
|
else {
|
package/dist/commands/refs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
3
3
|
import { extractIssueReferences } from "../utils/issue-reference-extractor.js";
|
|
4
4
|
import { wrapIssueReferencesAsLinks, } from "../utils/issue-reference-wrapper.js";
|
|
@@ -50,16 +50,70 @@ export async function wrapRefsCore(input, deps) {
|
|
|
50
50
|
const urlKey = await deps.resolveUrlKey();
|
|
51
51
|
return wrapIssueReferencesAsLinks(input.text, validIds, urlKey, input.target);
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Return `path` if it resolves to an existing regular file on disk, else
|
|
55
|
+
* `null`. Used by `handleWrap` to disambiguate the `el-linear refs wrap <arg>`
|
|
56
|
+
* shape between "wrap this literal text" and "wrap the contents of this file".
|
|
57
|
+
*
|
|
58
|
+
* Concerns intentionally NOT addressed:
|
|
59
|
+
* - Symlinks → resolved transparently by `statSync` (followSymlinks=true).
|
|
60
|
+
* - Directories → returned as null (statSync().isFile() is false).
|
|
61
|
+
* - Permission-denied / EACCES → returned as null; the caller falls back to
|
|
62
|
+
* treating the arg as text. The eventual `readFileSync` would surface the
|
|
63
|
+
* real error if the user did mean a file.
|
|
64
|
+
*
|
|
65
|
+
* Reference: DEV-4077 (vertical-int/tools MR !516 / !517 incident reports).
|
|
66
|
+
*/
|
|
67
|
+
function pathIfExistingFile(candidate) {
|
|
68
|
+
if (!candidate || candidate.includes("\n"))
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
|
72
|
+
return candidate;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// EACCES / EPERM / ELOOP → treat as not-a-file; downstream text path
|
|
77
|
+
// either passes through harmlessly or raises a more useful error.
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
async function handleWrap(textArgs, options, command) {
|
|
54
82
|
const target = options.target ?? "markdown";
|
|
55
83
|
if (!isWrapTarget(target)) {
|
|
56
84
|
throw new Error(`Invalid --target "${target}". Expected one of: ${[...VALID_TARGETS].join(", ")}`);
|
|
57
85
|
}
|
|
58
86
|
// `validate` is `true` by default and `false` when `--no-validate` is passed.
|
|
59
87
|
const validate = options.validate !== false;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
// Input precedence: positional args > --file > stdin. A single positional
|
|
89
|
+
// arg that resolves to an existing file is treated as a file path (the
|
|
90
|
+
// natural `el-linear refs wrap body.md` shape that users hit blind);
|
|
91
|
+
// otherwise positional args are joined as text. This auto-detect closes
|
|
92
|
+
// DEV-4077: the published v1.8.1 rejected `wrap <file>` outright, and the
|
|
93
|
+
// v1.10.0 source treated it as literal text — producing silently wrong
|
|
94
|
+
// output instead of an error. With this change, either form works.
|
|
95
|
+
let text;
|
|
96
|
+
if (textArgs.length === 1) {
|
|
97
|
+
const positionalFile = pathIfExistingFile(textArgs[0]);
|
|
98
|
+
if (positionalFile !== null) {
|
|
99
|
+
if (typeof options.file === "string" && options.file.length > 0) {
|
|
100
|
+
throw new Error(`Both a positional file path (${positionalFile}) and --file ${options.file} were provided. Use one or the other.`);
|
|
101
|
+
}
|
|
102
|
+
text = readFileSync(positionalFile, "utf8");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
text = textArgs[0];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (textArgs.length > 1) {
|
|
109
|
+
text = textArgs.join(" ");
|
|
110
|
+
}
|
|
111
|
+
else if (typeof options.file === "string" && options.file.length > 0) {
|
|
112
|
+
text = readFileSync(options.file, "utf8");
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
text = await readAllStdin();
|
|
116
|
+
}
|
|
63
117
|
const rootOpts = getRootOpts(command);
|
|
64
118
|
const deps = {
|
|
65
119
|
async resolveValidIdentifiers(ids) {
|
|
@@ -94,9 +148,14 @@ export function setupRefsCommands(program) {
|
|
|
94
148
|
refs
|
|
95
149
|
.command("wrap")
|
|
96
150
|
.description("Wrap recognized Linear issue identifiers in input text as links. " +
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
151
|
+
"Input precedence: positional args > --file > stdin. A single positional " +
|
|
152
|
+
"arg that resolves to an existing file is read as a file (so " +
|
|
153
|
+
"`el-linear refs wrap body.md` works the same as `--file body.md`); " +
|
|
154
|
+
"otherwise positional args are joined as literal text. " +
|
|
155
|
+
"Writes the result to stdout. By default, each candidate identifier " +
|
|
156
|
+
"is validated against the workspace; unresolvable ones are left as " +
|
|
157
|
+
"plain text.")
|
|
158
|
+
.argument("[text...]", "input text OR a path to a file. A single positional arg that exists as a file is read; otherwise positional args are joined as literal text with a single space.")
|
|
100
159
|
.option("--file <path>", "read input from a file instead of stdin")
|
|
101
160
|
.option("--target <target>", "output format: markdown (default) or slack", "markdown")
|
|
102
161
|
.option("--no-validate", "skip workspace validation; wrap every regex match. Faster, but may produce broken links for IDs that don't exist.")
|
package/dist/commands/search.js
CHANGED
|
@@ -23,11 +23,28 @@ const VALID_TYPES = new Set([
|
|
|
23
23
|
"document",
|
|
24
24
|
"template",
|
|
25
25
|
]);
|
|
26
|
+
/**
|
|
27
|
+
* `SemanticSearchResult`-valid types only (drops "template" — templates
|
|
28
|
+
* come from a separate query, not `semanticSearch`). Used to filter
|
|
29
|
+
* unknown types out of the response BEFORE they reach
|
|
30
|
+
* `transformSearchResult` — the transformer's switch has a
|
|
31
|
+
* `satisfies never` exhaustiveness guard that would otherwise return
|
|
32
|
+
* the raw row on a never-typed default branch.
|
|
33
|
+
*/
|
|
34
|
+
const SEMANTIC_TYPES = new Set([
|
|
35
|
+
"issue",
|
|
36
|
+
"project",
|
|
37
|
+
"initiative",
|
|
38
|
+
"document",
|
|
39
|
+
]);
|
|
40
|
+
function isKnownSemanticResult(r) {
|
|
41
|
+
return SEMANTIC_TYPES.has(r.type);
|
|
42
|
+
}
|
|
26
43
|
const WHITESPACE_RE = /\s+/;
|
|
27
44
|
function transformSearchResult(r) {
|
|
28
45
|
switch (r.type) {
|
|
29
46
|
case "issue": {
|
|
30
|
-
const issue = r
|
|
47
|
+
const { issue } = r;
|
|
31
48
|
return {
|
|
32
49
|
type: "issue",
|
|
33
50
|
identifier: issue?.identifier,
|
|
@@ -68,7 +85,10 @@ function transformSearchResult(r) {
|
|
|
68
85
|
};
|
|
69
86
|
}
|
|
70
87
|
default:
|
|
71
|
-
|
|
88
|
+
// Exhaustiveness check: SemanticSearchResult is a closed union, so
|
|
89
|
+
// `r` is `never` here — adding a new arm to the union forces a new
|
|
90
|
+
// case branch (the compiler error makes it impossible to forget).
|
|
91
|
+
return r;
|
|
72
92
|
}
|
|
73
93
|
}
|
|
74
94
|
function matchesQuery(name, query) {
|
|
@@ -114,10 +134,15 @@ function buildSemanticQuery(graphQLService, query, limit, teamOption) {
|
|
|
114
134
|
});
|
|
115
135
|
}
|
|
116
136
|
function extractSemanticResults(semanticResult, requestedTypes, onlyTemplates) {
|
|
117
|
-
|
|
137
|
+
// Always drop rows with unknown `type` first — defends the transformer
|
|
138
|
+
// against future Linear API expansions adding a new variant before
|
|
139
|
+
// SemanticSearchResult's union is widened. Without this, an unknown
|
|
140
|
+
// type would reach the transformer's `satisfies never` default and
|
|
141
|
+
// pass through as a raw row.
|
|
142
|
+
let results = (semanticResult.semanticSearch?.results ?? []).filter(isKnownSemanticResult);
|
|
118
143
|
if (requestedTypes && !onlyTemplates) {
|
|
119
|
-
const semanticTypes = requestedTypes.filter((t) => t !== "template");
|
|
120
|
-
results = results.filter((r) => semanticTypes.
|
|
144
|
+
const semanticTypes = new Set(requestedTypes.filter((t) => t !== "template"));
|
|
145
|
+
results = results.filter((r) => semanticTypes.has(r.type));
|
|
121
146
|
}
|
|
122
147
|
return results.map(transformSearchResult);
|
|
123
148
|
}
|
package/dist/commands/users.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createLinearService } from "../utils/linear-service.js";
|
|
2
|
-
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
2
|
+
import { handleAsyncCommand, outputSuccess, warnIfTruncated, } from "../utils/output.js";
|
|
3
3
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
4
4
|
import { parsePositiveInt } from "../utils/validators.js";
|
|
5
5
|
export function setupUsersCommands(program) {
|
|
@@ -14,7 +14,9 @@ export function setupUsersCommands(program) {
|
|
|
14
14
|
.action(handleAsyncCommand(async (options, command) => {
|
|
15
15
|
const rootOpts = getRootOpts(command);
|
|
16
16
|
const service = await createLinearService(rootOpts);
|
|
17
|
-
const
|
|
17
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
18
|
+
const result = await service.getUsers(options.active, limit, options.name);
|
|
19
|
+
warnIfTruncated(result.length, limit);
|
|
18
20
|
outputSuccess({ data: result, meta: { count: result.length } });
|
|
19
21
|
}));
|
|
20
22
|
}
|
package/dist/config/config.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { TermRule } from "./term-enforcer.js";
|
|
2
|
+
/**
|
|
3
|
+
* Source attribution for the active team-config layer. Surfaced via
|
|
4
|
+
* `getActiveTeamConfigInfo()` and rendered by `el-linear config team show` so
|
|
5
|
+
* the operator can debug "which file is el-linear actually loading?".
|
|
6
|
+
*
|
|
7
|
+
* - `env`: `EL_LINEAR_TEAM_CONFIG` is set in the environment (highest).
|
|
8
|
+
* - `personal`: `teamConfigPath` field in personal `config.json`.
|
|
9
|
+
* - `marker`: auto-discovered via `~/.config/el-tools-root` (DEV-4258).
|
|
10
|
+
* - `null`: no team layer active.
|
|
11
|
+
*/
|
|
12
|
+
export type TeamConfigSource = "env" | "personal" | "marker" | null;
|
|
2
13
|
/**
|
|
3
14
|
* Canonical shape of `~/.config/el-linear/config.json`. This is the single source
|
|
4
15
|
* of truth for the on-disk config — the wizard reads/writes the same shape
|
|
@@ -27,6 +38,10 @@ export interface ElLinearConfig {
|
|
|
27
38
|
* Term-enforcement rules. Each rule has a canonical form and a list of
|
|
28
39
|
* rejected forms; rejected forms in issue titles/descriptions are flagged
|
|
29
40
|
* (or thrown on, in strict mode) with a hint to use the canonical form.
|
|
41
|
+
*
|
|
42
|
+
* When a team config layer is active, personal `terms` are appended to the
|
|
43
|
+
* team's rules (not replaced). To start fresh, omit `terms` from personal
|
|
44
|
+
* config entirely.
|
|
30
45
|
*/
|
|
31
46
|
terms: TermRule[];
|
|
32
47
|
validation?: {
|
|
@@ -61,12 +76,17 @@ export interface ElLinearConfig {
|
|
|
61
76
|
* Default assignee identifier (alias / display name / email / UUID — same
|
|
62
77
|
* shapes resolveAssignee accepts) for `issues create`. Applied when
|
|
63
78
|
* `--assignee` is not passed. Pass `--no-assignee` to override at one site.
|
|
79
|
+
*
|
|
80
|
+
* Prefer setting this in `local.json` instead of `config.json` so it stays
|
|
81
|
+
* out of shared team config.
|
|
64
82
|
*/
|
|
65
83
|
defaultAssignee?: string;
|
|
66
84
|
/**
|
|
67
85
|
* Default priority for `issues create` / `issues update`. Accepts the same
|
|
68
86
|
* keywords as the --priority flag: `none|urgent|high|medium|normal|low`
|
|
69
87
|
* or `0`–`4`. Applied when `--priority` is not passed.
|
|
88
|
+
*
|
|
89
|
+
* Prefer setting this in `local.json` instead of `config.json`.
|
|
70
90
|
*/
|
|
71
91
|
defaultPriority?: string;
|
|
72
92
|
/**
|
|
@@ -74,9 +94,87 @@ export interface ElLinearConfig {
|
|
|
74
94
|
* and `projects list`. Defaults to 3600 (1 hour) when omitted. A value of
|
|
75
95
|
* `0` disables the cache entirely. Override per-invocation with
|
|
76
96
|
* `--no-cache`.
|
|
97
|
+
*
|
|
98
|
+
* Prefer setting this in `local.json` instead of `config.json`.
|
|
77
99
|
*/
|
|
78
100
|
cacheTTLSeconds?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Path to a shared team config file. Fields in the team config are merged
|
|
103
|
+
* under the personal config (personal config wins on conflicts). Arrays such
|
|
104
|
+
* as `terms` and `defaultLabels` are concatenated — personal entries are
|
|
105
|
+
* appended to team entries. The team config accepts any ElLinearConfig fields
|
|
106
|
+
* except `teamConfigPath` itself. Useful for sharing member aliases, label
|
|
107
|
+
* maps, and term rules across a team by checking the file into a shared
|
|
108
|
+
* repository. Override at runtime with the `EL_LINEAR_TEAM_CONFIG` env var
|
|
109
|
+
* (env var takes precedence over this field).
|
|
110
|
+
*/
|
|
111
|
+
teamConfigPath?: string;
|
|
79
112
|
}
|
|
80
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Fields valid in a shared team config file (the file pointed to by
|
|
115
|
+
* `teamConfigPath` or `EL_LINEAR_TEAM_CONFIG`). All fields are optional —
|
|
116
|
+
* omitted fields fall back to defaults or the personal config layer. The type
|
|
117
|
+
* excludes `teamConfigPath` itself to prevent circular references.
|
|
118
|
+
*/
|
|
119
|
+
export type TeamConfig = Omit<ElLinearConfig, "teamConfigPath">;
|
|
120
|
+
/**
|
|
121
|
+
* User-local overrides that live in `~/.config/el-linear/local.json` (or the
|
|
122
|
+
* per-profile equivalent). These are applied on top of the merged team+personal
|
|
123
|
+
* config, so every field here takes the highest precedence.
|
|
124
|
+
*
|
|
125
|
+
* Use `local.json` for anything that is personal rather than team-wide:
|
|
126
|
+
* your own email, personal default priority, cache preferences. Never commit
|
|
127
|
+
* `local.json` to a shared repo — it is `.gitignore`-worthy by nature.
|
|
128
|
+
*
|
|
129
|
+
* Merge order (lowest → highest priority):
|
|
130
|
+
* defaults → team config file → personal config.json → local.json
|
|
131
|
+
*/
|
|
132
|
+
export interface ElLinearLocalConfig {
|
|
133
|
+
/**
|
|
134
|
+
* Your Linear account email. Used as the default `--assignee` when creating
|
|
135
|
+
* issues. Takes precedence over `config.json`'s `defaultAssignee`.
|
|
136
|
+
*
|
|
137
|
+
* Example: "ytspar@gmail.com"
|
|
138
|
+
*/
|
|
139
|
+
assigneeEmail?: string;
|
|
140
|
+
/** Same semantics as `ElLinearConfig.defaultAssignee`, but local-wins. */
|
|
141
|
+
defaultAssignee?: string;
|
|
142
|
+
/** Same semantics as `ElLinearConfig.defaultPriority`, but local-wins. */
|
|
143
|
+
defaultPriority?: string;
|
|
144
|
+
/** Same semantics as `ElLinearConfig.cacheTTLSeconds`, but local-wins. */
|
|
145
|
+
cacheTTLSeconds?: number;
|
|
146
|
+
}
|
|
147
|
+
/** Test seam — resets all caches between test cases. */
|
|
81
148
|
export declare function _resetConfigCacheForTests(): void;
|
|
82
149
|
export declare function loadConfig(): ElLinearConfig;
|
|
150
|
+
/**
|
|
151
|
+
* Returns the team config file path that is active for the current process
|
|
152
|
+
* (resolved via `EL_LINEAR_TEAM_CONFIG` env var, `teamConfigPath` in personal
|
|
153
|
+
* config, or `~/.config/el-tools-root` marker — in that order). Returns
|
|
154
|
+
* `undefined` when no team config is configured.
|
|
155
|
+
*
|
|
156
|
+
* Calling this before `loadConfig()` will trigger a `loadConfig()` internally
|
|
157
|
+
* so the path cache is populated.
|
|
158
|
+
*/
|
|
159
|
+
export declare function getActiveTeamConfigPath(): string | undefined;
|
|
160
|
+
/**
|
|
161
|
+
* Returns the active team-config path AND the source it was resolved from
|
|
162
|
+
* (env var / personal config field / onboarding-marker auto-discovery).
|
|
163
|
+
* `el-linear config team show` uses this to render which resolver fired —
|
|
164
|
+
* so an operator debugging "why isn't my team config loading?" sees the
|
|
165
|
+
* answer in one command.
|
|
166
|
+
*
|
|
167
|
+
* Re-derives source on each call rather than caching it: the lookup is cheap
|
|
168
|
+
* (one file read at most for personal config, one for the marker) and only
|
|
169
|
+
* `config team show` calls it.
|
|
170
|
+
*/
|
|
171
|
+
export declare function getActiveTeamConfigInfo(): {
|
|
172
|
+
path: string | undefined;
|
|
173
|
+
source: TeamConfigSource;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Load user-local overrides from `local.json`. Returns an empty object when
|
|
177
|
+
* no file exists — callers treat it as "no overrides". Errors are silent so
|
|
178
|
+
* a missing or malformed `local.json` never breaks a command.
|
|
179
|
+
*/
|
|
180
|
+
export declare function loadLocalConfig(): ElLinearLocalConfig;
|