@enrichlayer/el-linear 1.36.0 → 1.37.1
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 +14 -2
- package/claude-skills/linear-operations/SKILL.md +24 -6
- package/dist/commands/config.js +2 -0
- package/dist/commands/issues.js +22 -5
- package/dist/commands/profile.js +20 -1
- package/dist/commands/projects.js +8 -5
- package/dist/config/config.d.ts +12 -1
- package/dist/config/config.js +12 -1
- package/dist/config/paths.d.ts +9 -0
- package/dist/config/paths.js +9 -1
- package/dist/main.js +3 -1
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +1 -0
- package/dist/queries/issue-tree.d.ts +2 -0
- package/dist/queries/issue-tree.js +5 -0
- package/dist/queries/issues-types.d.ts +12 -0
- package/dist/queries/issues.d.ts +27 -8
- package/dist/queries/issues.js +39 -0
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/types/linear.d.ts +1 -0
- package/dist/utils/formatters/summary.js +7 -0
- package/dist/utils/graphql-issues-service.js +33 -8
- package/dist/utils/output.js +41 -1
- package/dist/utils/root-opts.d.ts +13 -0
- package/dist/utils/root-opts.js +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,6 +160,18 @@ The active profile is selected by, in priority:
|
|
|
160
160
|
The legacy fallback means **existing single-profile users see no
|
|
161
161
|
behavior change** — profiles are purely opt-in.
|
|
162
162
|
|
|
163
|
+
**Automation and concurrent sessions: prefer `--profile`/`$EL_LINEAR_PROFILE`
|
|
164
|
+
over `profile use`.** `~/.config/el-linear/active-profile` is a single,
|
|
165
|
+
machine-global marker file — `profile use <name>` mutates it for *every*
|
|
166
|
+
el-linear invocation on the machine, including other terminals, scripts, and
|
|
167
|
+
agent sessions that don't set an explicit override. Two processes wanting
|
|
168
|
+
different workspaces at the same time will silently clobber each other. The
|
|
169
|
+
flag and env var are per-invocation and never touch the marker file, so they
|
|
170
|
+
compose safely with any number of concurrent sessions each pinned to their
|
|
171
|
+
own workspace. Reserve `profile use` for a human's own interactive default
|
|
172
|
+
switch; `profile use`/`profile add` print a loud warning when they change the
|
|
173
|
+
global marker so the blast radius is visible instead of silent.
|
|
174
|
+
|
|
163
175
|
## Migrating from v1.0–1.3
|
|
164
176
|
|
|
165
177
|
Versions 1.0–1.3 stored everything in the single-file layout
|
|
@@ -556,9 +568,9 @@ Currently wired through:
|
|
|
556
568
|
|
|
557
569
|
| Resource | Defaults | Extras you can request |
|
|
558
570
|
|----------|----------|------------------------|
|
|
559
|
-
| `issues list` | `id, title, state, assignee` | `project, cycle, milestone, labels, url, priority, estimate, createdAt, updatedAt, team` |
|
|
571
|
+
| `issues list` | `id, title, state, assignee` | `project, cycle, milestone, labels, url, priority, estimate, createdAt, updatedAt, completedAt, team` |
|
|
560
572
|
| `projects list` | `name, state, progress, lead` | `teams, target, url, updatedAt` |
|
|
561
|
-
| `issues read` (single) | `state, assignee, project, cycle, milestone, labels, url` | `priority, estimate, created, updated` |
|
|
573
|
+
| `issues read` (single) | `state, assignee, project, cycle, milestone, labels, url` | `priority, estimate, created, updated, completed` |
|
|
562
574
|
| `projects read` (single) | `state, lead, teams, target, progress, url` | (filter only — no extras) |
|
|
563
575
|
|
|
564
576
|
`identifier`, `id`, `title` and `name` are headline-only on single-resource summaries and are filtered out of the labelled block (they remain on the title line above). `status` is accepted as a synonym for `state`, `owner` for `assignee`, `targetDate` for `target`.
|
|
@@ -143,8 +143,8 @@ For tools that aren't `el-linear` (e.g. `gh`, `glab`, `kubectl`), prefer `jq` fo
|
|
|
143
143
|
|
|
144
144
|
`--format summary` is implemented for:
|
|
145
145
|
|
|
146
|
-
- **Single resources:** `issues read`, `comments read`, `projects read`, `cycles read`, `project-milestones read`, `documents read`, `templates read`, releases (`graphql` query results), `users read`
|
|
147
|
-
- **Lists:** `issues list`, `issues search`, `projects list`, `comments list`, `cycles list`, `project-milestones list`, `labels list`, `teams list`, `users list`, `documents list`, `templates list`, `attachments list`, `releases list`, and the cross-resource `search` command
|
|
146
|
+
- **Single resources:** `issues read`, `comments read`, `projects read`, `cycles read`, `project-milestones read`, `project-updates read`, `documents read`, `templates read`, releases (`graphql` query results), `users read`
|
|
147
|
+
- **Lists:** `issues list`, `issues search`, `projects list`, `comments list`, `cycles list`, `project-milestones list`, `project-updates list`, `labels list`, `teams list`, `users list`, `documents list`, `templates list`, `attachments list`, `releases list`, and the cross-resource `search` command
|
|
148
148
|
|
|
149
149
|
Commands without a dedicated formatter (e.g. `config show`, custom `graphql` queries) fall back to a generic key/value rendering of their JSON payload.
|
|
150
150
|
|
|
@@ -241,11 +241,10 @@ el-linear issues search "keywords from proposed title" --include-closed 2>&1
|
|
|
241
241
|
|
|
242
242
|
The dup-check above guards against duplicating an *issue*. This guards against duplicating *reality*: before filing an issue to **add** a flag / guard / command / subcommand, confirm it doesn't **already exist**.
|
|
243
243
|
|
|
244
|
-
- `<cli> <subcommand> --help` (
|
|
245
|
-
- `el-
|
|
246
|
-
- For a hook/guard, grep the source — e.g. `cli/el-hook/src/checks`.
|
|
244
|
+
- **`el-catalog commands --search "<intent>"`** / `el-catalog clis --search` — the **authoritative** check for a command/subcommand; it deterministically indexes every `<cli> <subcommand>`. Confirm with `<cli> <subcommand> --help` (and `<cli> --help` — a flag may be a global option absent from the subcommand's help).
|
|
245
|
+
- For a hook/guard, grep the source (e.g. `cli/el-hook/src/checks`) — but **empty grep output is inconclusive, not proof of absence**. A mis-quoted glob or bad flag makes grep exit silently with zero matches (`grep --include=*.ts …` errors under zsh, returning nothing for a symbol that exists). Never read a raw grep's silence as "doesn't exist" for the command check — that is exactly what `el-catalog` is for.
|
|
247
246
|
|
|
248
|
-
Skipping it cost
|
|
247
|
+
Skipping it cost real rework across sessions: a hook guard and `--jq`/`--fields` flags filed as "features" though both already existed and worked, and a near-duplicate `el-git` subcommand built after a raw grep silently misfired (the `el-catalog` search would have found the existing one) — every premise caught only at implementation.
|
|
249
248
|
|
|
250
249
|
When `el-linear issues search` (or the cross-resource `search`) returns rows
|
|
251
250
|
carrying issue identifiers, the JSON envelope embeds a `_warnings` line
|
|
@@ -532,6 +531,25 @@ el-linear projects add-team "Project Name" ENG 2>&1
|
|
|
532
531
|
|
|
533
532
|
**Never use raw `projectUpdate` with `teamIds`** — it replaces the entire team list. Always use `projects add-team` / `remove-team`.
|
|
534
533
|
|
|
534
|
+
### Project Updates (status posts) — mind the naming collision
|
|
535
|
+
|
|
536
|
+
Linear has two unrelated things spelled almost the same:
|
|
537
|
+
|
|
538
|
+
- `projectUpdate(id, input)` — the mutation that **edits a project** (the one warned about just above). Surfaced by the `projects` command.
|
|
539
|
+
- `ProjectUpdate` — a **status post** in a project's Updates feed (progress + a health color). Surfaced by the dedicated `project-updates` command:
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
# Post a status update to a project (appears in the Updates feed)
|
|
543
|
+
el-linear project-updates create --project "Auth Refactor" \
|
|
544
|
+
--body "Shipped the session-store migration; rollout at 60%." \
|
|
545
|
+
--health onTrack # onTrack | atRisk | offTrack — omit to leave unset
|
|
546
|
+
|
|
547
|
+
el-linear project-updates list --project "Auth Refactor"
|
|
548
|
+
el-linear project-updates read <updateId>
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
`--body` / `--body-file` are mutually exclusive (one required); `--body-file` avoids shell-quoting for markdown/tables. `--health` is validated against the enum before the API call. `--diff-hidden` hides the auto-generated progress diff on the update. `-q/--quiet` prints `<health> <url>`. A status update is not the same as a project **document** (`documents create --project`) — use a document for durable reference content, a project update for point-in-time progress.
|
|
552
|
+
|
|
535
553
|
### Discovery Before Creation
|
|
536
554
|
|
|
537
555
|
Always check if a project exists before creating: `el-linear projects list`.
|
package/dist/commands/config.js
CHANGED
|
@@ -33,6 +33,8 @@ function sourceLabel(source) {
|
|
|
33
33
|
return "teamConfigPath in personal config";
|
|
34
34
|
case "marker":
|
|
35
35
|
return "auto-discovered via ~/.config/el-tools-root";
|
|
36
|
+
case "disabled":
|
|
37
|
+
return 'disabled for this profile via teamConfigPath: "" in personal config';
|
|
36
38
|
case null:
|
|
37
39
|
return null;
|
|
38
40
|
}
|
package/dist/commands/issues.js
CHANGED
|
@@ -18,7 +18,7 @@ import { createLinearService, } from "../utils/linear-service.js";
|
|
|
18
18
|
import { logger } from "../utils/logger.js";
|
|
19
19
|
import { handleAsyncCommand, outputSuccess, outputWarning, warnIfTruncated, } from "../utils/output.js";
|
|
20
20
|
import { buildRelationCandidatePrompt } from "../utils/relation-candidate-prompt.js";
|
|
21
|
-
import { getRootOpts } from "../utils/root-opts.js";
|
|
21
|
+
import { effectiveOption, getRootOpts } from "../utils/root-opts.js";
|
|
22
22
|
import { formatCsv, formatMarkdown, formatTable, } from "../utils/table-formatter.js";
|
|
23
23
|
import { parsePositiveInt, parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
|
|
24
24
|
import { currentGitBranch, extractIssueIdentifierFromBranch, getBranchLinearIssue, gitCheckoutBranch, setBranchLinearIssue, toBranchName, } from "./issues/branch.js";
|
|
@@ -121,7 +121,17 @@ function buildUpdateArgs(issueId, options, assigneeId, delegateId) {
|
|
|
121
121
|
dueDate: options.dueDate || (options.clearDueDate ? null : undefined),
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
|
-
|
|
124
|
+
/**
|
|
125
|
+
* `--format` / `--fields` are registered on both the root program and the
|
|
126
|
+
* list/search subcommands, and commander 15 assigns the CLI token to the
|
|
127
|
+
* root registration — so the values must be resolved via
|
|
128
|
+
* {@link effectiveOption} rather than read off the subcommand's own
|
|
129
|
+
* options (DEV-5376: `--format table --fields …` silently fell through to
|
|
130
|
+
* the JSON envelope because `options.format` stayed at its default).
|
|
131
|
+
*/
|
|
132
|
+
function outputIssues(issues, command, meta) {
|
|
133
|
+
const format = effectiveOption(command, "format");
|
|
134
|
+
const fields = effectiveOption(command, "fields");
|
|
125
135
|
const fieldList = fields ? splitList(fields) : undefined;
|
|
126
136
|
if (format === "table") {
|
|
127
137
|
logger.info(formatTable(issues, fieldList));
|
|
@@ -134,6 +144,13 @@ function outputIssues(issues, format, fields, meta) {
|
|
|
134
144
|
logger.info(formatCsv(issues, fieldList));
|
|
135
145
|
}
|
|
136
146
|
else {
|
|
147
|
+
// JSON path: the `--fields` projection is applied by the global
|
|
148
|
+
// `fieldsFilter` inside `outputSuccess` (set in main.ts's preAction),
|
|
149
|
+
// NOT by `fieldList` here — `fieldList` only feeds the tabular
|
|
150
|
+
// formatters above. Keeping the projection in one place is what
|
|
151
|
+
// prevents double-application; a future refactor that routes
|
|
152
|
+
// table/csv/md through `outputSuccess` would need to drop the global
|
|
153
|
+
// filter or it would project twice.
|
|
137
154
|
outputSuccess({ data: issues, meta: { count: issues.length, ...meta } });
|
|
138
155
|
}
|
|
139
156
|
}
|
|
@@ -228,14 +245,14 @@ async function handleListIssues(options, command) {
|
|
|
228
245
|
outputWarning("excluded terminal states (Done / Canceled) by default; pass --include-closed to include them");
|
|
229
246
|
}
|
|
230
247
|
warnIfTruncated(result.length, limit);
|
|
231
|
-
outputIssues(result,
|
|
248
|
+
outputIssues(result, command, {
|
|
232
249
|
team: options.team,
|
|
233
250
|
});
|
|
234
251
|
}
|
|
235
252
|
else {
|
|
236
253
|
const result = sortIssues(await issuesService.getIssues(limit), options.sort);
|
|
237
254
|
warnIfTruncated(result.length, limit);
|
|
238
|
-
outputIssues(result,
|
|
255
|
+
outputIssues(result, command, {});
|
|
239
256
|
}
|
|
240
257
|
}
|
|
241
258
|
async function handleSearchIssues(query, options, command) {
|
|
@@ -282,7 +299,7 @@ async function handleSearchIssues(query, options, command) {
|
|
|
282
299
|
if (relationPrompt) {
|
|
283
300
|
outputWarning(relationPrompt);
|
|
284
301
|
}
|
|
285
|
-
outputIssues(result,
|
|
302
|
+
outputIssues(result, command, { query });
|
|
286
303
|
}
|
|
287
304
|
/**
|
|
288
305
|
* Run an `issuesService.createIssue` / `.updateIssue` call and, if it
|
package/dist/commands/profile.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { promises as fsp } from "node:fs";
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import { confirm } from "@inquirer/prompts";
|
|
26
|
-
import { ACTIVE_PROFILE_FILE, CONFIG_DIR, CONFIG_PATH, isSafeProfileName as isSafeName, PROFILES_DIR, profilePaths, resolveActiveProfile, setActiveProfileForSession, TOKEN_PATH, } from "../config/paths.js";
|
|
26
|
+
import { ACTIVE_PROFILE_FILE, CONFIG_DIR, CONFIG_PATH, isSafeProfileName as isSafeName, PROFILES_DIR, profilePaths, readActiveProfileMarker, resolveActiveProfile, setActiveProfileForSession, TOKEN_PATH, } from "../config/paths.js";
|
|
27
27
|
import { outputSuccess, outputWarning } from "../utils/output.js";
|
|
28
28
|
import { runFullWizard } from "./init/index.js";
|
|
29
29
|
import { registerMigrateLegacy } from "./profile/migrate-legacy.js";
|
|
@@ -120,8 +120,27 @@ export async function runProfileUse(name) {
|
|
|
120
120
|
if (!isSafeName(trimmed)) {
|
|
121
121
|
throw new Error(`Profile name "${trimmed}" must contain only [a-z0-9_-]. Pick a different name.`);
|
|
122
122
|
}
|
|
123
|
+
// DEV-5610: `active-profile` is a single, machine-global marker — every
|
|
124
|
+
// el-linear invocation anywhere on this machine that doesn't set
|
|
125
|
+
// --profile/$EL_LINEAR_PROFILE resolves through it. Warn loudly on an
|
|
126
|
+
// actual switch so the blast radius is visible instead of silently
|
|
127
|
+
// redirecting some other concurrent session/tool to a different
|
|
128
|
+
// workspace. `--profile`/$EL_LINEAR_PROFILE remain the correct,
|
|
129
|
+
// non-mutating choice for automation and concurrent sessions.
|
|
130
|
+
//
|
|
131
|
+
// Read the raw marker (readActiveProfileMarker), not
|
|
132
|
+
// resolveActiveProfile().name — the latter resolves through the
|
|
133
|
+
// --profile/$EL_LINEAR_PROFILE precedence layers too, so a concurrent
|
|
134
|
+
// override in place while `profile use` runs would make it report the
|
|
135
|
+
// wrong "previous" value (a false-positive warning when the marker
|
|
136
|
+
// didn't actually change, or a false-negative when it did) — cycle-1
|
|
137
|
+
// review finding on PR #229.
|
|
138
|
+
const previous = readActiveProfileMarker();
|
|
123
139
|
await fsp.mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
124
140
|
await fsp.writeFile(ACTIVE_PROFILE_FILE, `${trimmed}\n`, { mode: 0o644 });
|
|
141
|
+
if (previous !== trimmed) {
|
|
142
|
+
outputWarning(`Switched the GLOBAL active profile to "${trimmed}" (${ACTIVE_PROFILE_FILE}). This affects every other el-linear invocation on this machine that doesn't set --profile/$EL_LINEAR_PROFILE explicitly — concurrent sessions/tools relying on a different profile will silently start hitting the wrong workspace.`);
|
|
143
|
+
}
|
|
125
144
|
}
|
|
126
145
|
export async function runProfileAdd(name) {
|
|
127
146
|
const trimmed = name.trim();
|
|
@@ -6,7 +6,7 @@ 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
8
|
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
9
|
-
import { getRootOpts } from "../utils/root-opts.js";
|
|
9
|
+
import { effectiveOption, 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";
|
|
12
12
|
import { parsePositiveInt, splitList } from "../utils/validators.js";
|
|
@@ -516,7 +516,11 @@ export function setupProjectsCommands(program) {
|
|
|
516
516
|
// ahead of completed/canceled in every format, so the active
|
|
517
517
|
// set always fits within `--limit`. DEV-4175.
|
|
518
518
|
const sorted = sortActiveFirst(result);
|
|
519
|
-
|
|
519
|
+
// Resolved via effectiveOption because commander 15 hands the
|
|
520
|
+
// CLI token to the root program's same-named --format/--fields
|
|
521
|
+
// registration, leaving the subcommand's option at its default
|
|
522
|
+
// (DEV-5376).
|
|
523
|
+
const format = effectiveOption(command, "format") ?? "json";
|
|
520
524
|
const isTabular = format === "table" ||
|
|
521
525
|
format === "md" ||
|
|
522
526
|
format === "markdown" ||
|
|
@@ -530,9 +534,8 @@ export function setupProjectsCommands(program) {
|
|
|
530
534
|
warnProjectsTruncated(sorted.length, limit, format);
|
|
531
535
|
}
|
|
532
536
|
if (isTabular) {
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
: undefined;
|
|
537
|
+
const fields = effectiveOption(command, "fields");
|
|
538
|
+
const fieldList = fields ? splitList(fields) : undefined;
|
|
536
539
|
formatProjectsOutput(sorted, format, fieldList);
|
|
537
540
|
if (format === "table") {
|
|
538
541
|
logger.info(`\n${sorted.length} projects`);
|
package/dist/config/config.d.ts
CHANGED
|
@@ -7,9 +7,11 @@ import type { TermRule } from "./term-enforcer.js";
|
|
|
7
7
|
* - `env`: `EL_LINEAR_TEAM_CONFIG` is set in the environment (highest).
|
|
8
8
|
* - `personal`: `teamConfigPath` field in personal `config.json`.
|
|
9
9
|
* - `marker`: auto-discovered via `~/.config/el-tools-root` (DEV-4258).
|
|
10
|
+
* - `disabled`: explicitly opted out via `teamConfigPath: ""` in personal
|
|
11
|
+
* config — the layer is off even when the marker would otherwise apply.
|
|
10
12
|
* - `null`: no team layer active.
|
|
11
13
|
*/
|
|
12
|
-
export type TeamConfigSource = "env" | "personal" | "marker" | null;
|
|
14
|
+
export type TeamConfigSource = "env" | "personal" | "marker" | "disabled" | null;
|
|
13
15
|
/**
|
|
14
16
|
* Canonical shape of `~/.config/el-linear/config.json`. This is the single source
|
|
15
17
|
* of truth for the on-disk config — the wizard reads/writes the same shape
|
|
@@ -142,6 +144,15 @@ export interface ElLinearConfig {
|
|
|
142
144
|
* maps, and term rules across a team by checking the file into a shared
|
|
143
145
|
* repository. Override at runtime with the `EL_LINEAR_TEAM_CONFIG` env var
|
|
144
146
|
* (env var takes precedence over this field).
|
|
147
|
+
*
|
|
148
|
+
* Set to the empty string (`""`) to explicitly DISABLE the team layer for
|
|
149
|
+
* this profile, including `~/.config/el-tools-root` marker auto-discovery.
|
|
150
|
+
* This is the multi-account escape hatch: a marker-discovered team config
|
|
151
|
+
* carries workspace-specific member/label UUIDs, and applying it to a
|
|
152
|
+
* profile pointed at a different Linear workspace makes `--assignee <name>`
|
|
153
|
+
* resolve to a foreign UUID that the API rejects with an opaque
|
|
154
|
+
* "Entity not found in validateAccess" error. Profiles for secondary
|
|
155
|
+
* workspaces should set `"teamConfigPath": ""`.
|
|
145
156
|
*/
|
|
146
157
|
teamConfigPath?: string;
|
|
147
158
|
}
|
package/dist/config/config.js
CHANGED
|
@@ -112,6 +112,10 @@ export function loadConfig() {
|
|
|
112
112
|
// The marker is the opt-in consent signal — only developers who ran
|
|
113
113
|
// `link-project.sh` get the auto-discovery; OSS users hit the silent
|
|
114
114
|
// no-op path. DEV-4258 / ALL-964.
|
|
115
|
+
//
|
|
116
|
+
// An EMPTY string is a deliberate per-profile opt-out (see the
|
|
117
|
+
// `teamConfigPath` JSDoc): it is defined, so marker discovery is
|
|
118
|
+
// skipped, and falsy, so no team layer is loaded below.
|
|
115
119
|
personalRaw = readRawPersonalConfig(active);
|
|
116
120
|
teamConfigPath = personalRaw.teamConfigPath;
|
|
117
121
|
if (teamConfigPath === undefined) {
|
|
@@ -181,7 +185,14 @@ export function getActiveTeamConfigInfo() {
|
|
|
181
185
|
}
|
|
182
186
|
const personalRaw = readRawPersonalConfig(resolveActiveProfile());
|
|
183
187
|
const personalPath = personalRaw.teamConfigPath;
|
|
184
|
-
if (personalPath
|
|
188
|
+
if (personalPath === "") {
|
|
189
|
+
// Explicit per-profile opt-out — must be attributed as such, and must
|
|
190
|
+
// NOT fall through to the marker: loadConfig skips marker discovery
|
|
191
|
+
// for a defined-but-empty path, so reporting "marker" here would
|
|
192
|
+
// contradict what actually loads.
|
|
193
|
+
return { path: undefined, source: "disabled" };
|
|
194
|
+
}
|
|
195
|
+
if (personalPath !== undefined) {
|
|
185
196
|
return { path: personalPath, source: "personal" };
|
|
186
197
|
}
|
|
187
198
|
const markerPath = discoverTeamConfigViaMarker();
|
package/dist/config/paths.d.ts
CHANGED
|
@@ -51,3 +51,12 @@ export declare function getSessionProfileOverride(): string | null;
|
|
|
51
51
|
export declare function resolveActiveProfile(env?: NodeJS.ProcessEnv, fsImpl?: ProfileFsOps): ProfilePaths;
|
|
52
52
|
/** Build profile-relative paths for a named profile. Pure. */
|
|
53
53
|
export declare function profilePaths(name: string): ProfilePaths;
|
|
54
|
+
/**
|
|
55
|
+
* Read the raw on-disk marker content, bypassing the `--profile`/
|
|
56
|
+
* `EL_LINEAR_PROFILE` precedence layers `resolveActiveProfile` applies.
|
|
57
|
+
* Exported (DEV-5610) so `profile use` can detect whether *the file itself*
|
|
58
|
+
* is about to change, independent of any per-invocation override that might
|
|
59
|
+
* otherwise make `resolveActiveProfile().name` report a different "previous"
|
|
60
|
+
* value than what's actually on disk.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readActiveProfileMarker(fsImpl?: ProfileFsOps): string | null;
|
package/dist/config/paths.js
CHANGED
|
@@ -127,7 +127,15 @@ export function profilePaths(name) {
|
|
|
127
127
|
tokenPath: path.join(dir, "token"),
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Read the raw on-disk marker content, bypassing the `--profile`/
|
|
132
|
+
* `EL_LINEAR_PROFILE` precedence layers `resolveActiveProfile` applies.
|
|
133
|
+
* Exported (DEV-5610) so `profile use` can detect whether *the file itself*
|
|
134
|
+
* is about to change, independent of any per-invocation override that might
|
|
135
|
+
* otherwise make `resolveActiveProfile().name` report a different "previous"
|
|
136
|
+
* value than what's actually on disk.
|
|
137
|
+
*/
|
|
138
|
+
export function readActiveProfileMarker(fsImpl = DEFAULT_FS_OPS) {
|
|
131
139
|
if (!fsImpl.existsSync(ACTIVE_PROFILE_FILE))
|
|
132
140
|
return null;
|
|
133
141
|
try {
|
package/dist/main.js
CHANGED
|
@@ -70,7 +70,9 @@ program
|
|
|
70
70
|
"Warnings (e.g. --fields fields_unresolved) can't ride on a bare array, so " +
|
|
71
71
|
"they are written to stderr prefixed `_warnings: `, keeping stdout a pure JSON array")
|
|
72
72
|
.option("--jq <filter>", "apply a jq filter to the JSON output")
|
|
73
|
-
.option("--fields <fields>", "filter output to specific fields (comma-separated).
|
|
73
|
+
.option("--fields <fields>", "filter output to specific fields (comma-separated). Dot-paths resolve nested " +
|
|
74
|
+
"values; the column aliases status (state.name) and updated (updatedAt) " +
|
|
75
|
+
"resolve too. Unresolved fields are " +
|
|
74
76
|
"emitted as null plus a `fields_unresolved:` warning — in the JSON " +
|
|
75
77
|
"envelope's `_warnings`, or on stderr when output is a bare array (with --raw)")
|
|
76
78
|
.option("--no-cache", "bypass the on-disk cache for `teams list` / `labels list` / `projects list`");
|
package/dist/queries/common.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const COMPLETE_ISSUE_FRAGMENT = "\n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n";
|
|
2
|
-
export declare const COMPLETE_ISSUE_WITH_COMMENTS_FRAGMENT = "\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n";
|
|
1
|
+
export declare const COMPLETE_ISSUE_FRAGMENT = "\n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n";
|
|
2
|
+
export declare const COMPLETE_ISSUE_WITH_COMMENTS_FRAGMENT = "\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n";
|
package/dist/queries/common.js
CHANGED
|
@@ -23,6 +23,10 @@ export const DEFAULT_TREE_DEPTH = 3;
|
|
|
23
23
|
// Field set per tree node — kept minimal so the depth-N query stays small.
|
|
24
24
|
// `state.type` is required client-side for `--no-include-closed` pruning;
|
|
25
25
|
// `state.name` for the [Done]/[Canceled] suffix on terminal nodes.
|
|
26
|
+
// `completedAt` is the moment a node reached a terminal state (null while
|
|
27
|
+
// open) — consumers that reason about how long a subtree has been finished
|
|
28
|
+
// (e.g. el-sop health's completed-children grace period, DEV-5454) need the
|
|
29
|
+
// per-child timestamp, which no other field carries.
|
|
26
30
|
// `priority` was intentionally dropped after cycle-1: the ASCII formatter
|
|
27
31
|
// doesn't render it and including it inflates the query string by ~N nodes.
|
|
28
32
|
const TREE_NODE_FIELDS = `
|
|
@@ -31,6 +35,7 @@ const TREE_NODE_FIELDS = `
|
|
|
31
35
|
title
|
|
32
36
|
state { id name type }
|
|
33
37
|
assignee { id name }
|
|
38
|
+
completedAt
|
|
34
39
|
`;
|
|
35
40
|
/**
|
|
36
41
|
* Recursive children fragment generator. `depth=0` emits nothing (no
|
|
@@ -106,6 +106,7 @@ export interface IssueNode {
|
|
|
106
106
|
url: string;
|
|
107
107
|
createdAt: string;
|
|
108
108
|
updatedAt: string;
|
|
109
|
+
completedAt: string | null;
|
|
109
110
|
state: IdNameRef | null;
|
|
110
111
|
assignee: AssigneeNode | null;
|
|
111
112
|
delegate: AssigneeNode | null;
|
|
@@ -152,6 +153,17 @@ export interface GetIssuesResponse {
|
|
|
152
153
|
nodes: IssueNode[];
|
|
153
154
|
};
|
|
154
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Response shape for `TEAM_SCOPED_FILTERED_ISSUES_QUERY` (DEV-5578).
|
|
158
|
+
* `team` is null when the resolved team UUID does not exist.
|
|
159
|
+
*/
|
|
160
|
+
export interface TeamScopedFilteredIssuesResponse {
|
|
161
|
+
team: {
|
|
162
|
+
issues: {
|
|
163
|
+
nodes: IssueNode[];
|
|
164
|
+
};
|
|
165
|
+
} | null;
|
|
166
|
+
}
|
|
155
167
|
/** Response shape for `SEARCH_ISSUES_QUERY` (full-text). */
|
|
156
168
|
export interface SearchIssuesResponse {
|
|
157
169
|
searchIssues: {
|
package/dist/queries/issues.d.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
export declare const GET_ISSUES_QUERY = "\n query GetIssues($first: Int!, $orderBy: PaginationOrderBy) {\n issues(\n first: $first\n orderBy: $orderBy\n filter: {\n state: { type: { neq: \"completed\" } }\n }\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
2
|
-
export declare const SEARCH_ISSUES_QUERY = "\n query SearchIssues($term: String!, $first: Int!) {\n searchIssues(term: $term, first: $first, includeArchived: false) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
3
|
-
export declare const FILTERED_SEARCH_ISSUES_QUERY = "\n query FilteredSearchIssues(\n $first: Int!\n $filter: IssueFilter\n $orderBy: PaginationOrderBy\n ) {\n issues(\n first: $first\n filter: $filter\n orderBy: $orderBy\n includeArchived: false\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
1
|
+
export declare const GET_ISSUES_QUERY = "\n query GetIssues($first: Int!, $orderBy: PaginationOrderBy) {\n issues(\n first: $first\n orderBy: $orderBy\n filter: {\n state: { type: { neq: \"completed\" } }\n }\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
2
|
+
export declare const SEARCH_ISSUES_QUERY = "\n query SearchIssues($term: String!, $first: Int!) {\n searchIssues(term: $term, first: $first, includeArchived: false) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
3
|
+
export declare const FILTERED_SEARCH_ISSUES_QUERY = "\n query FilteredSearchIssues(\n $first: Int!\n $filter: IssueFilter\n $orderBy: PaginationOrderBy\n ) {\n issues(\n first: $first\n filter: $filter\n orderBy: $orderBy\n includeArchived: false\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
4
|
+
/**
|
|
5
|
+
* Team-scoped variant of `FILTERED_SEARCH_ISSUES_QUERY` (DEV-5578).
|
|
6
|
+
*
|
|
7
|
+
* When a `--team` filter is present, the team boundary is applied
|
|
8
|
+
* *structurally* — the `issues` connection is rooted at the `Team` node
|
|
9
|
+
* (`team(id: $teamId).issues(...)`) rather than passed as a top-level
|
|
10
|
+
* `issues(filter: { team: { id: { eq } } })` relation filter.
|
|
11
|
+
*
|
|
12
|
+
* The top-level relation-filter form is unreliable at scale: Linear's API
|
|
13
|
+
* silently leaks issues from *other* teams once `$first` grows past a small
|
|
14
|
+
* page (~20), so `issues list --team DEV --limit 100` returned issues from
|
|
15
|
+
* EMW/INF/FE too. This is the same class of bug DEV-5325 fixed for
|
|
16
|
+
* `projects list --team` (`ProjectFilter` had no working `teams` relation, so
|
|
17
|
+
* project scoping moved to `Team.projects`). Rooting at the team node keeps
|
|
18
|
+
* the team boundary server-side and exact. The remaining `$filter`
|
|
19
|
+
* (state / labels / assignee / priority / project) is applied on top of the
|
|
20
|
+
* already-team-scoped connection.
|
|
21
|
+
*/
|
|
22
|
+
export declare const TEAM_SCOPED_FILTERED_ISSUES_QUERY = "\n query TeamScopedFilteredIssues(\n $teamId: String!\n $first: Int!\n $filter: IssueFilter\n $orderBy: PaginationOrderBy\n ) {\n team(id: $teamId) {\n issues(\n first: $first\n filter: $filter\n orderBy: $orderBy\n includeArchived: false\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n }\n";
|
|
4
23
|
/**
|
|
5
24
|
* Batch-resolves a search's team/project/assignee/delegate filter inputs.
|
|
6
25
|
*
|
|
@@ -11,8 +30,8 @@ export declare const FILTERED_SEARCH_ISSUES_QUERY = "\n query FilteredSearchIss
|
|
|
11
30
|
* name-resolution arm is needed here.)
|
|
12
31
|
*/
|
|
13
32
|
export declare const BATCH_RESOLVE_FOR_SEARCH_QUERY = "\n query BatchResolveForSearch(\n $teamKey: String\n $teamName: String\n $projectName: String\n $hasProjectName: Boolean = false\n $assigneeEmail: String\n $delegateEmail: String\n ) {\n teams(\n filter: {\n or: [\n { key: { eq: $teamKey } }\n { name: { eqIgnoreCase: $teamName } }\n ]\n }\n first: 1\n ) {\n nodes {\n id\n key\n name\n }\n }\n\n projects(\n filter: { name: { eqIgnoreCase: $projectName } }\n first: 1\n ) @include(if: $hasProjectName) {\n nodes {\n id\n name\n }\n }\n\n users(filter: { email: { eq: $assigneeEmail } }, first: 1) {\n nodes {\n id\n name\n email\n }\n }\n\n delegates: users(filter: { email: { eq: $delegateEmail } }, first: 1) {\n nodes {\n id\n name\n email\n }\n }\n }\n";
|
|
14
|
-
export declare const GET_ISSUE_BY_ID_QUERY = "\n query GetIssue($id: String!) {\n issue(id: $id) {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n";
|
|
15
|
-
export declare const GET_ISSUE_BY_IDENTIFIER_QUERY = "\n query GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n issues(\n filter: {\n team: { key: { eq: $teamKey } }\n number: { eq: $number }\n }\n first: 1\n ) {\n nodes {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n }\n";
|
|
33
|
+
export declare const GET_ISSUE_BY_ID_QUERY = "\n query GetIssue($id: String!) {\n issue(id: $id) {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n";
|
|
34
|
+
export declare const GET_ISSUE_BY_IDENTIFIER_QUERY = "\n query GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n issues(\n filter: {\n team: { key: { eq: $teamKey } }\n number: { eq: $number }\n }\n first: 1\n ) {\n nodes {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n }\n";
|
|
16
35
|
/**
|
|
17
36
|
* Batch issue fetch. Used by `issues read <id...>` (DEV-4477) to collapse
|
|
18
37
|
* N parallel single-issue queries into one round-trip. The `$filter` is
|
|
@@ -34,7 +53,7 @@ export declare const GET_ISSUE_BY_IDENTIFIER_QUERY = "\n query GetIssueByIdenti
|
|
|
34
53
|
* CLAUDE.md ("Performance-critical loops where the SDK's per-edge
|
|
35
54
|
* resolver promises produce N+1 round-trips") explicitly authorizes this.
|
|
36
55
|
*/
|
|
37
|
-
export declare const BATCH_GET_ISSUES_QUERY = "\n query BatchGetIssues($filter: IssueFilter!, $first: Int!) {\n issues(filter: $filter, first: $first) {\n nodes {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n }\n";
|
|
56
|
+
export declare const BATCH_GET_ISSUES_QUERY = "\n query BatchGetIssues($filter: IssueFilter!, $first: Int!) {\n issues(filter: $filter, first: $first) {\n nodes {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n }\n";
|
|
38
57
|
/**
|
|
39
58
|
* Batch-resolves an update's project/milestone/issue inputs.
|
|
40
59
|
*
|
|
@@ -45,8 +64,8 @@ export declare const BATCH_GET_ISSUES_QUERY = "\n query BatchGetIssues($filter:
|
|
|
45
64
|
* `--project-milestone` name resolution; a name uses `projectsByName`.
|
|
46
65
|
*/
|
|
47
66
|
export declare const BATCH_RESOLVE_FOR_UPDATE_QUERY = "\n query BatchResolveForUpdate(\n $projectName: String\n $projectId: ID\n $hasProjectName: Boolean = false\n $hasProjectId: Boolean = false\n $teamKey: String\n $issueNumber: Float\n $milestoneName: String\n $hasMilestoneName: Boolean = false\n ) {\n projectsByName: projects(\n filter: { name: { eqIgnoreCase: $projectName } }\n first: 5\n ) @include(if: $hasProjectName) {\n nodes {\n id\n name\n teams {\n nodes { id key }\n }\n projectMilestones {\n nodes {\n id\n name\n }\n }\n }\n }\n\n projectsById: projects(\n filter: { id: { eq: $projectId } }\n first: 1\n ) @include(if: $hasProjectId) {\n nodes {\n id\n name\n projectMilestones {\n nodes {\n id\n name\n }\n }\n }\n }\n\n milestones: projectMilestones(\n filter: { name: { eq: $milestoneName } }\n first: 1\n ) @include(if: $hasMilestoneName) {\n nodes {\n id\n name\n }\n }\n\n issues(\n filter: {\n and: [\n { team: { key: { eq: $teamKey } } }\n { number: { eq: $issueNumber } }\n ]\n }\n first: 1\n ) {\n nodes {\n id\n identifier\n team {\n id\n key\n }\n labels {\n nodes {\n id\n name\n }\n }\n project {\n id\n projectMilestones {\n nodes {\n id\n name\n }\n }\n }\n }\n }\n }\n";
|
|
48
|
-
export declare const CREATE_ISSUE_MUTATION = "\n mutation CreateIssue($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
49
|
-
export declare const UPDATE_ISSUE_MUTATION = "\n mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n issueUpdate(id: $id, input: $input) {\n success\n issue {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
67
|
+
export declare const CREATE_ISSUE_MUTATION = "\n mutation CreateIssue($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
68
|
+
export declare const UPDATE_ISSUE_MUTATION = "\n mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n issueUpdate(id: $id, input: $input) {\n success\n issue {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
50
69
|
export declare const ARCHIVE_ISSUE_MUTATION = "\n mutation ArchiveIssue($id: String!) {\n issueArchive(id: $id) {\n success\n lastSyncId\n entity {\n id\n }\n }\n }\n";
|
|
51
70
|
export declare const DELETE_ISSUE_MUTATION = "\n mutation DeleteIssue($id: String!, $permanentlyDelete: Boolean) {\n issueDelete(id: $id, permanentlyDelete: $permanentlyDelete) {\n success\n lastSyncId\n entity {\n id\n }\n }\n }\n";
|
|
52
71
|
/**
|
package/dist/queries/issues.js
CHANGED
|
@@ -41,6 +41,45 @@ export const FILTERED_SEARCH_ISSUES_QUERY = `
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
`;
|
|
44
|
+
/**
|
|
45
|
+
* Team-scoped variant of `FILTERED_SEARCH_ISSUES_QUERY` (DEV-5578).
|
|
46
|
+
*
|
|
47
|
+
* When a `--team` filter is present, the team boundary is applied
|
|
48
|
+
* *structurally* — the `issues` connection is rooted at the `Team` node
|
|
49
|
+
* (`team(id: $teamId).issues(...)`) rather than passed as a top-level
|
|
50
|
+
* `issues(filter: { team: { id: { eq } } })` relation filter.
|
|
51
|
+
*
|
|
52
|
+
* The top-level relation-filter form is unreliable at scale: Linear's API
|
|
53
|
+
* silently leaks issues from *other* teams once `$first` grows past a small
|
|
54
|
+
* page (~20), so `issues list --team DEV --limit 100` returned issues from
|
|
55
|
+
* EMW/INF/FE too. This is the same class of bug DEV-5325 fixed for
|
|
56
|
+
* `projects list --team` (`ProjectFilter` had no working `teams` relation, so
|
|
57
|
+
* project scoping moved to `Team.projects`). Rooting at the team node keeps
|
|
58
|
+
* the team boundary server-side and exact. The remaining `$filter`
|
|
59
|
+
* (state / labels / assignee / priority / project) is applied on top of the
|
|
60
|
+
* already-team-scoped connection.
|
|
61
|
+
*/
|
|
62
|
+
export const TEAM_SCOPED_FILTERED_ISSUES_QUERY = `
|
|
63
|
+
query TeamScopedFilteredIssues(
|
|
64
|
+
$teamId: String!
|
|
65
|
+
$first: Int!
|
|
66
|
+
$filter: IssueFilter
|
|
67
|
+
$orderBy: PaginationOrderBy
|
|
68
|
+
) {
|
|
69
|
+
team(id: $teamId) {
|
|
70
|
+
issues(
|
|
71
|
+
first: $first
|
|
72
|
+
filter: $filter
|
|
73
|
+
orderBy: $orderBy
|
|
74
|
+
includeArchived: false
|
|
75
|
+
) {
|
|
76
|
+
nodes {
|
|
77
|
+
${COMPLETE_ISSUE_FRAGMENT}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
44
83
|
/**
|
|
45
84
|
* Batch-resolves a search's team/project/assignee/delegate filter inputs.
|
|
46
85
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const LIST_PROJECT_MILESTONES_QUERY = "\n query ListProjectMilestones($projectId: String!, $first: Int!) {\n project(id: $projectId) {\n id\n name\n projectMilestones(first: $first) {\n nodes {\n id\n name\n description\n targetDate\n sortOrder\n createdAt\n updatedAt\n }\n }\n }\n }\n";
|
|
2
|
-
export declare const GET_PROJECT_MILESTONE_BY_ID_QUERY = "\n query GetProjectMilestone($id: String!, $issuesFirst: Int) {\n projectMilestone(id: $id) {\n id\n name\n description\n targetDate\n sortOrder\n createdAt\n updatedAt\n project {\n id\n name\n }\n issues(first: $issuesFirst) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n }\n";
|
|
2
|
+
export declare const GET_PROJECT_MILESTONE_BY_ID_QUERY = "\n query GetProjectMilestone($id: String!, $issuesFirst: Int) {\n projectMilestone(id: $id) {\n id\n name\n description\n targetDate\n sortOrder\n createdAt\n updatedAt\n project {\n id\n name\n }\n issues(first: $issuesFirst) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n }\n";
|
|
3
3
|
export declare const FIND_PROJECT_MILESTONE_BY_NAME_SCOPED = "\n query FindProjectMilestoneScoped($name: String!, $projectId: String!) {\n project(id: $projectId) {\n projectMilestones(filter: { name: { eq: $name } }, first: 10) {\n nodes {\n id\n name\n targetDate\n sortOrder\n project {\n id\n name\n }\n }\n }\n }\n }\n";
|
|
4
4
|
export declare const FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL = "\n query FindProjectMilestoneGlobal($name: String!) {\n projectMilestones(filter: { name: { eq: $name } }, first: 10) {\n nodes {\n id\n name\n targetDate\n sortOrder\n project {\n id\n name\n }\n }\n }\n }\n";
|
|
5
5
|
export declare const CREATE_PROJECT_MILESTONE_MUTATION = "\n mutation CreateProjectMilestone($projectId: String!, $name: String!, $description: String, $targetDate: TimelessDate) {\n projectMilestoneCreate(input: {\n projectId: $projectId\n name: $name\n description: $description\n targetDate: $targetDate\n }) {\n success\n projectMilestone {\n id\n name\n description\n targetDate\n sortOrder\n createdAt\n updatedAt\n project {\n id\n name\n }\n }\n }\n }\n";
|
package/dist/types/linear.d.ts
CHANGED
|
@@ -301,6 +301,7 @@ const ISSUE_SUMMARY_SYNONYMS = {
|
|
|
301
301
|
owner: "assignee",
|
|
302
302
|
createdat: "created",
|
|
303
303
|
updatedat: "updated",
|
|
304
|
+
completedat: "completed",
|
|
304
305
|
prioritylabel: "priority",
|
|
305
306
|
projectmilestone: "milestone",
|
|
306
307
|
};
|
|
@@ -316,6 +317,7 @@ function buildIssueHeaderFields(issue) {
|
|
|
316
317
|
{ label: "Estimate", value: s(issue.estimate) },
|
|
317
318
|
{ label: "Created", value: s(issue.createdAt) },
|
|
318
319
|
{ label: "Updated", value: s(issue.updatedAt) },
|
|
320
|
+
{ label: "Completed", value: s(issue.completedAt) },
|
|
319
321
|
{ label: "URL", value: s(issue.url) },
|
|
320
322
|
];
|
|
321
323
|
}
|
|
@@ -424,6 +426,11 @@ const ISSUE_LIST_EXTRAS = {
|
|
|
424
426
|
minWidth: 7,
|
|
425
427
|
extract: (i) => s(i.updatedAt).slice(0, 10),
|
|
426
428
|
},
|
|
429
|
+
completedat: {
|
|
430
|
+
header: "COMPLETED",
|
|
431
|
+
minWidth: 9,
|
|
432
|
+
extract: (i) => s(i.completedAt).slice(0, 10),
|
|
433
|
+
},
|
|
427
434
|
team: {
|
|
428
435
|
header: "TEAM",
|
|
429
436
|
minWidth: 4,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { isRegistryConfigured, resolveViaRegistry, } from "../config/registry-resolve.js";
|
|
2
2
|
import { resolveUserDisplayName } from "../config/resolver.js";
|
|
3
|
-
import { ARCHIVE_ISSUE_MUTATION, BATCH_GET_ISSUES_QUERY, BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, DELETE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUE_CLAIM_CONTEXT_QUERY, GET_ISSUE_START_CONTEXT_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, TEAM_STARTED_STATUSES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
|
|
3
|
+
import { ARCHIVE_ISSUE_MUTATION, BATCH_GET_ISSUES_QUERY, BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, DELETE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUE_CLAIM_CONTEXT_QUERY, GET_ISSUE_START_CONTEXT_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, TEAM_SCOPED_FILTERED_ISSUES_QUERY, TEAM_STARTED_STATUSES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
|
|
4
4
|
import { CREATE_LABEL_MUTATION } from "../queries/labels.js";
|
|
5
5
|
import { TERMINAL_STATE_TYPES } from "../types/linear.js";
|
|
6
|
-
import { toISOStringOrNow } from "./date-format.js";
|
|
6
|
+
import { toISOStringOrNow, toISOStringOrUndefined } from "./date-format.js";
|
|
7
7
|
import { extractEmbeds } from "./embed-parser.js";
|
|
8
8
|
import { multipleMatchesError, notFoundError } from "./error-messages.js";
|
|
9
9
|
import { parseIssueIdentifier, tryParseIssueIdentifier, } from "./identifier-parser.js";
|
|
@@ -556,8 +556,14 @@ export class GraphQLIssuesService {
|
|
|
556
556
|
priority: args.priority,
|
|
557
557
|
});
|
|
558
558
|
}
|
|
559
|
+
// DEV-5578: the team boundary is NOT put in the IssueFilter. Linear's
|
|
560
|
+
// top-level `issues(filter: { team: { id: { eq } } })` relation filter
|
|
561
|
+
// leaks other teams' issues once `first` grows past a small page (~20),
|
|
562
|
+
// so `--team` was silently contaminated on paginated (`--limit > ~20`)
|
|
563
|
+
// results. When a team is present we scope structurally through the
|
|
564
|
+
// `Team.issues` connection below (same class of fix as DEV-5325's
|
|
565
|
+
// `Team.projects`); the rest of the filter rides on top of it.
|
|
559
566
|
const filter = this.buildSearchFilter({
|
|
560
|
-
teamId: finalTeamId,
|
|
561
567
|
assigneeId: finalAssigneeId,
|
|
562
568
|
delegateId: finalDelegateId,
|
|
563
569
|
project: projectFilter,
|
|
@@ -566,10 +572,25 @@ export class GraphQLIssuesService {
|
|
|
566
572
|
labelNames: args.labelNames,
|
|
567
573
|
priority: args.priority,
|
|
568
574
|
});
|
|
575
|
+
const filterArg = Object.keys(filter).length > 0 ? filter : undefined;
|
|
576
|
+
const orderBy = args.orderBy ?? "updatedAt";
|
|
577
|
+
if (finalTeamId) {
|
|
578
|
+
const teamScoped = await this.graphQLService.rawRequest(TEAM_SCOPED_FILTERED_ISSUES_QUERY, {
|
|
579
|
+
teamId: finalTeamId,
|
|
580
|
+
first: limit,
|
|
581
|
+
filter: filterArg,
|
|
582
|
+
orderBy,
|
|
583
|
+
});
|
|
584
|
+
const nodes = teamScoped.team?.issues?.nodes;
|
|
585
|
+
if (!nodes?.length) {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
return nodes.map((issue) => this.transformIssueData(issue));
|
|
589
|
+
}
|
|
569
590
|
const searchResult = await this.graphQLService.rawRequest(FILTERED_SEARCH_ISSUES_QUERY, {
|
|
570
591
|
first: limit,
|
|
571
|
-
filter:
|
|
572
|
-
orderBy
|
|
592
|
+
filter: filterArg,
|
|
593
|
+
orderBy,
|
|
573
594
|
});
|
|
574
595
|
const filteredIssues = searchResult.issues;
|
|
575
596
|
if (!filteredIssues?.nodes) {
|
|
@@ -999,11 +1020,12 @@ export class GraphQLIssuesService {
|
|
|
999
1020
|
}
|
|
1000
1021
|
return filtered;
|
|
1001
1022
|
}
|
|
1023
|
+
// DEV-5578: `teamId` is intentionally NOT a member — the team boundary is
|
|
1024
|
+
// applied structurally via `TEAM_SCOPED_FILTERED_ISSUES_QUERY`, never as a
|
|
1025
|
+
// top-level `team` relation filter (which Linear leaks past ~20 rows). Do
|
|
1026
|
+
// not reintroduce a `filter.team = …` branch here.
|
|
1002
1027
|
buildSearchFilter(filters) {
|
|
1003
1028
|
const filter = {};
|
|
1004
|
-
if (filters.teamId) {
|
|
1005
|
-
filter.team = { id: { eq: filters.teamId } };
|
|
1006
|
-
}
|
|
1007
1029
|
if (filters.assigneeId) {
|
|
1008
1030
|
filter.assignee = { id: { eq: filters.assigneeId } };
|
|
1009
1031
|
}
|
|
@@ -1270,6 +1292,9 @@ export class GraphQLIssuesService {
|
|
|
1270
1292
|
: [],
|
|
1271
1293
|
createdAt: toISOStringOrNow(issue.createdAt),
|
|
1272
1294
|
updatedAt: toISOStringOrNow(issue.updatedAt),
|
|
1295
|
+
// Nullable, not `-OrNow`: an open issue has no completion, so it must
|
|
1296
|
+
// stay `undefined` rather than be fabricated as the current time.
|
|
1297
|
+
completedAt: toISOStringOrUndefined(issue.completedAt),
|
|
1273
1298
|
};
|
|
1274
1299
|
}
|
|
1275
1300
|
transformIssueCoreFields(issue) {
|
package/dist/utils/output.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { resolveActiveProfile } from "../config/paths.js";
|
|
2
3
|
import { dispatch as dispatchSummary, drainSummaryFieldWarnings, formatLine, inferKindFromPayload, } from "./formatters/summary.js";
|
|
3
4
|
import { logger } from "./logger.js";
|
|
4
5
|
import { sanitizeForLog } from "./sanitize-for-log.js";
|
|
@@ -57,6 +58,19 @@ function getNestedPath(obj, path) {
|
|
|
57
58
|
}
|
|
58
59
|
return cur;
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Aliases for the table/csv column names that don't exist as literal keys
|
|
63
|
+
* on the JSON payload (DEV-5376). Tried only after the literal key and the
|
|
64
|
+
* dot-path both miss, and only kept when the alias target actually
|
|
65
|
+
* resolves — so a resource that genuinely lacks `state`/`updatedAt` still
|
|
66
|
+
* gets the explicit-null + `fields_unresolved` treatment. The requested
|
|
67
|
+
* name stays the output key (`{"status": "Todo"}`), matching the
|
|
68
|
+
* "consumers read exactly what they asked for" contract.
|
|
69
|
+
*/
|
|
70
|
+
const FIELD_ALIASES = {
|
|
71
|
+
status: "state.name",
|
|
72
|
+
updated: "updatedAt",
|
|
73
|
+
};
|
|
60
74
|
/**
|
|
61
75
|
* Project `obj` down to the requested fields.
|
|
62
76
|
*
|
|
@@ -64,6 +78,8 @@ function getNestedPath(obj, path) {
|
|
|
64
78
|
* - Dot-separated paths (DEV-5323) resolve nested values; the requested path
|
|
65
79
|
* string becomes a flat output key (`{"pipeline.status": "success"}`), so
|
|
66
80
|
* consumers read exactly what they asked for.
|
|
81
|
+
* - Documented column aliases resolve when the literal key is absent
|
|
82
|
+
* (`status` → `state.name`, `updated` → `updatedAt`; DEV-5376).
|
|
67
83
|
* - A field that resolves nowhere is emitted as an explicit `null` AND
|
|
68
84
|
* reported via `unresolved` — never silently omitted. Silent omission made
|
|
69
85
|
* a typo'd field indistinguishable from an empty value (DEV-5323).
|
|
@@ -114,6 +130,12 @@ function filterFields(obj, fields, unresolved) {
|
|
|
114
130
|
result[field] = nested;
|
|
115
131
|
continue;
|
|
116
132
|
}
|
|
133
|
+
const alias = FIELD_ALIASES[field];
|
|
134
|
+
const aliased = alias !== undefined ? getNestedPath(source, alias) : undefined;
|
|
135
|
+
if (aliased !== undefined) {
|
|
136
|
+
result[field] = aliased;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
117
139
|
result[field] = null;
|
|
118
140
|
unresolved?.add(field);
|
|
119
141
|
}
|
|
@@ -354,7 +376,25 @@ function outputError(error) {
|
|
|
354
376
|
// `lin_oauth_…` / `Bearer <payload>` in error text can't leak a token
|
|
355
377
|
// into stdout, shell history, or CI logs. The wizard already sanitizes
|
|
356
378
|
// its own log paths; this is the central error path on every command.
|
|
357
|
-
|
|
379
|
+
//
|
|
380
|
+
// `activeProfile` is included so a "not found" error is distinguishable
|
|
381
|
+
// from "wrong workspace" without manually inspecting
|
|
382
|
+
// ~/.config/el-linear/active-profile (DEV-5610) — the active profile can
|
|
383
|
+
// change between commands (an explicit `profile use`, a different
|
|
384
|
+
// $EL_LINEAR_PROFILE, or another process on the machine switching the
|
|
385
|
+
// shared marker file), and a bare "X not found" reads as data loss or an
|
|
386
|
+
// API outage when the real cause is often just the wrong workspace.
|
|
387
|
+
// Guarded: resolveActiveProfile() throws on an invalid $EL_LINEAR_PROFILE
|
|
388
|
+
// value (a real, separate user error) — outputError is the last-resort
|
|
389
|
+
// error path, so this addition must not itself throw uncaught.
|
|
390
|
+
let activeProfile = "<unknown>";
|
|
391
|
+
try {
|
|
392
|
+
activeProfile = resolveActiveProfile().name ?? "<legacy default>";
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
// leave activeProfile as "<unknown>"
|
|
396
|
+
}
|
|
397
|
+
const payload = JSON.stringify({ error: sanitizeForLog(error.message), activeProfile }, null, 2);
|
|
358
398
|
// Write to stdout (same channel as success) so machine callers always
|
|
359
399
|
// receive exactly one parseable JSON object regardless of stream capture.
|
|
360
400
|
logger.info(payload);
|
|
@@ -9,3 +9,16 @@ import type { Command, OptionValues } from "commander";
|
|
|
9
9
|
* assertions live here so call sites can stay clean and typed.
|
|
10
10
|
*/
|
|
11
11
|
export declare function getRootOpts(command: Command): OptionValues;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve an option that is registered on BOTH the root program and a
|
|
14
|
+
* subcommand (`--format` / `--fields` on `issues list|search`,
|
|
15
|
+
* `projects list`).
|
|
16
|
+
*
|
|
17
|
+
* Commander 15 hands a same-named option to the OUTERMOST registration
|
|
18
|
+
* even when it appears after the subcommand: `issues list --format table`
|
|
19
|
+
* sets the program's global `--format` and leaves the subcommand's local
|
|
20
|
+
* option at its default, so handlers reading `options.format` silently
|
|
21
|
+
* saw `"json"` (DEV-5376). Precedence here: local CLI-set value, then the
|
|
22
|
+
* nearest ancestor's CLI-set value, then the local default.
|
|
23
|
+
*/
|
|
24
|
+
export declare function effectiveOption(command: Command, key: string): string | undefined;
|
package/dist/utils/root-opts.js
CHANGED
|
@@ -11,3 +11,26 @@ export function getRootOpts(command) {
|
|
|
11
11
|
// biome-ignore lint/style/noNonNullAssertion: command.parent / .parent.parent are guaranteed by commander inside subcommand actions; see docstring
|
|
12
12
|
return (command.parent?.parent ?? command.parent).opts();
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve an option that is registered on BOTH the root program and a
|
|
16
|
+
* subcommand (`--format` / `--fields` on `issues list|search`,
|
|
17
|
+
* `projects list`).
|
|
18
|
+
*
|
|
19
|
+
* Commander 15 hands a same-named option to the OUTERMOST registration
|
|
20
|
+
* even when it appears after the subcommand: `issues list --format table`
|
|
21
|
+
* sets the program's global `--format` and leaves the subcommand's local
|
|
22
|
+
* option at its default, so handlers reading `options.format` silently
|
|
23
|
+
* saw `"json"` (DEV-5376). Precedence here: local CLI-set value, then the
|
|
24
|
+
* nearest ancestor's CLI-set value, then the local default.
|
|
25
|
+
*/
|
|
26
|
+
export function effectiveOption(command, key) {
|
|
27
|
+
if (command.getOptionValueSource(key) === "cli") {
|
|
28
|
+
return command.getOptionValue(key);
|
|
29
|
+
}
|
|
30
|
+
for (let c = command.parent; c; c = c.parent) {
|
|
31
|
+
if (c.getOptionValueSource(key) === "cli") {
|
|
32
|
+
return c.getOptionValue(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return command.getOptionValue(key);
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enrichlayer/el-linear",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.1",
|
|
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",
|